Ameba自定义规则Skill ameba-custom-rules

这个技能用于创建自定义Ameba linting规则,用于Crystal代码分析,包括规则开发、AST遍历和测试,帮助强制执行项目特定的代码质量标准和捕捉领域特定代码异味。关键词:Ameba, 自定义规则, Crystal, 代码分析, linting, 静态代码分析, 代码质量, 规则测试。

测试 0 次安装 0 次浏览 更新于 3/25/2026

name: ameba-custom-rules user-invocable: false description: 在创建自定义Ameba规则用于Crystal代码分析时使用,包括规则开发、AST遍历、问题报告和规则测试。 allowed-tools:

  • Bash
  • Read

Ameba自定义规则

为Ameba创建自定义linting规则,以强制执行项目特定的代码质量标准和捕捉Crystal项目中的领域特定代码异味。

理解自定义规则

自定义Ameba规则允许您:

  • 强制执行项目特定的编码标准
  • 捕捉领域特定的反模式
  • 验证业务逻辑约束
  • 确保大型代码库的一致性
  • 为您的组织创建可重用的规则库
  • 扩展Ameba的内置功能

规则结构

基本规则结构

每个Ameba规则都继承自Ameba::Rule::Base并遵循以下结构:

module Ameba::Rule::Custom
  # 强制执行公共类必须被文档化的规则
  class DocumentedClasses < Base
    properties do
      description "强制执行公共类必须被文档化"
    end

    MSG = "类必须用注释文档化"

    def test(source)
      AST::NodeVisitor.new self, source
    end

    def test(source, node : Crystal::ClassDef)
      return unless node.visibility.public?

      doc = node.doc
      issue_for(node, MSG) if doc.nil? || doc.empty?
    end
  end
end

关键组件

  1. 模块命名空间 - 自定义规则通常使用Ameba::Rule::CustomAmeba::Rule::<类别>
  2. 基类 - 所有规则都继承自Ameba::Rule::Base
  3. 属性块 - 定义规则元数据和配置
  4. 消息常量 - 显示给用户的错误消息
  5. 测试方法 - 初始化AST访问器的入口点
  6. 重载测试方法 - 处理特定的AST节点类型

创建您的第一个自定义规则

步骤1:项目设置

为您的自定义规则创建一个Crystal库:

# 初始化一个新的Crystal库
crystal init lib ameba-custom-rules
cd ameba-custom-rules

更新shard.yml

name: ameba-custom-rules
user-invocable: false
version: 0.1.0
authors:
  - 您的姓名 <your.email@example.com>

description: 为您的项目定制的Ameba规则

crystal: ">= 1.0.0"

license: MIT

development_dependencies:
  ameba:
    github: crystal-ameba/ameba
    version: ~> 1.6.0

重要: Ameba应作为开发依赖项,以避免版本冲突。

步骤2:实现一个简单规则

创建src/ameba-custom-rules/no_sleep_in_production.cr

require "ameba"

module Ameba::Rule::Custom
  # 防止在生产代码中使用sleep()调用
  class NoSleepInProduction < Base
    properties do
      description "防止在生产代码中使用sleep调用"
      enabled true
    end

    MSG = "避免在生产代码中使用sleep();使用适当的后台作业"

    def test(source)
      AST::NodeVisitor.new self, source
    end

    def test(source, node : Crystal::Call)
      return unless node.name == "sleep"

      issue_for node, MSG
    end
  end
end

步骤3:注册和使用规则

创建主文件src/ameba-custom-rules.cr

require "ameba"
require "./ameba-custom-rules/*"

# 规则通过继承自动注册

更新您项目的Ameba配置:

# .ameba.yml
Custom/NoSleepInProduction:
  Enabled: true
  Severity: Warning

步骤4:测试您的规则

创建spec/ameba-custom-rules/no_sleep_in_production_spec.cr

require "../spec_helper"

module Ameba::Rule::Custom
  describe NoSleepInProduction do
    it "报告sleep调用" do
      rule = NoSleepInProduction.new

      source = Source.new %(
        def process
          sleep 5.seconds
        end
      )

      rule.test(source)
      source.issues.size.should eq(1)
    end

    it "允许没有sleep的代码" do
      rule = NoSleepInProduction.new

      source = Source.new %(
        def process
          puts "Processing"
        end
      )

      rule.test(source)
      source.issues.should be_empty
    end
  end
end

高级规则示例

强制命名约定

module Ameba::Rule::Custom
  # 强制服务类以"Service"结尾
  class ServiceClassNaming < Base
    properties do
      description "服务类必须以'Service'结尾"
      enabled true
    end

    MSG = "服务类名称应以'Service'结尾"

    def test(source)
      AST::NodeVisitor.new self, source
    end

    def test(source, node : Crystal::ClassDef)
      class_name = node.name.to_s

      # 检查类是否在services目录中
      return unless source.path.includes?("/services/")

      # 检查名称是否以Service结尾
      unless class_name.ends_with?("Service")
        issue_for node.name, MSG
      end
    end
  end
end

检测危险方法调用

module Ameba::Rule::Custom
  # 防止危险的ActiveRecord-like方法
  class NoDangerousDatabaseCalls < Base
    properties do
      description "防止危险的数据库操作"
      dangerous_methods ["delete_all", "destroy_all", "update_all"]
    end

    MSG = "危险方法 %s 没有条件"

    def test(source)
      AST::NodeVisitor.new self, source
    end

    def test(source, node : Crystal::Call)
      return unless dangerous_methods.includes?(node.name)

      # 检查调用是否有参数(条件)
      if node.args.empty?
        message = MSG % node.name
        issue_for node, message
      end
    end
  end
end

强制错误处理

module Ameba::Rule::Custom
  # 确保HTTP客户端调用有错误处理
  class HttpErrorHandling < Base
    properties do
      description "HTTP客户端调用必须处理错误"
      enabled true
    end

    MSG = "HTTP客户端调用应包装在begin/rescue中"

    def test(source)
      @in_rescue_block = false
      AST::NodeVisitor.new self, source
    end

    def test(source, node : Crystal::ExceptionHandler)
      @in_rescue_block = true
      true  # 继续访问子节点
    end

    def test(source, node : Crystal::Call)
      return if @in_rescue_block

      # 检查HTTP客户端调用
      if node.obj.try(&.to_s.includes?("HTTP"))
        issue_for node, MSG
      end
    end
  end
end

验证方法复杂性

module Ameba::Rule::Custom
  # 限制方法复杂性
  class MethodComplexity < Base
    properties do
      description "方法不应过于复杂"
      max_complexity 10
    end

    MSG = "方法复杂性(%d)超过最大值(%d)"

    def test(source)
      AST::NodeVisitor.new self, source
    end

    def test(source, node : Crystal::Def)
      complexity = calculate_complexity(node)

      if complexity > max_complexity
        message = MSG % [complexity, max_complexity]
        issue_for node, message
      end
    end

    private def calculate_complexity(node)
      counter = ComplexityCounter.new
      node.accept(counter)
      counter.complexity
    end

    private class ComplexityCounter < Crystal::Visitor
      getter complexity : Int32 = 1

      def visit(node : Crystal::If)
        @complexity += 1
        true
      end

      def visit(node : Crystal::Case)
        @complexity += 1
        true
      end

      def visit(node : Crystal::While)
        @complexity += 1
        true
      end

      def visit(node : Crystal::Call)
        # 计数逻辑运算符
        if node.name.in?("&&", "||")
          @complexity += 1
        end
        true
      end

      def visit(node : Crystal::ASTNode)
        true
      end
    end
  end
end

强制文档标准

module Ameba::Rule::Custom
  # 要求特定格式的文档
  class DocumentationFormat < Base
    properties do
      description "公共方法必须有带示例的文档"
      enabled true
      require_examples true
    end

    MSG_NO_DOC = "公共方法必须有文档"
    MSG_NO_EXAMPLE = "文档必须包括使用示例"

    def test(source)
      AST::NodeVisitor.new self, source
    end

    def test(source, node : Crystal::Def)
      return unless node.visibility.public?
      return if node.name.starts_with?("initialize")

      doc = node.doc

      if doc.nil? || doc.empty?
        issue_for node, MSG_NO_DOC
        return
      end

      if require_examples && !has_example?(doc)
        issue_for node, MSG_NO_EXAMPLE
      end
    end

    private def has_example?(doc : String)
      doc.includes?("```") || doc.includes?("Example:")
    end
  end
end

使用AST节点

常见的AST节点类型

# 类定义
def test(source, node : Crystal::ClassDef)
  node.name            # 类名称
  node.visibility      # public?, private?, protected?
  node.doc            # 文档注释
  node.abstract?      # 是抽象类吗?
  node.superclass     # 父类
end

# 方法定义
def test(source, node : Crystal::Def)
  node.name           # 方法名称
  node.args           # 参数
  node.body           # 方法体
  node.return_type    # 返回类型注释
  node.visibility     # 可见性修饰符
  node.doc           # 文档
end

# 方法调用
def test(source, node : Crystal::Call)
  node.name          # 方法名称
  node.obj           # 接收器对象
  node.args          # 参数
  node.named_args    # 命名参数
  node.block         # 块参数
end

# 变量赋值
def test(source, node : Crystal::Assign)
  node.target        # 左侧(变量)
  node.value         # 右侧(值)
end

# 条件语句
def test(source, node : Crystal::If)
  node.cond          # 条件
  node.then          # then分支
  node.else          # else分支
end

# 循环
def test(source, node : Crystal::While)
  node.cond          # 循环条件
  node.body          # 循环体
end

遍历模式

# 模式1:在遍历期间跟踪状态
class MyRule < Base
  def test(source)
    @inside_block = false
    @depth = 0
    AST::NodeVisitor.new self, source
  end

  def test(source, node : Crystal::Block)
    @inside_block = true
    @depth += 1
    true  # 继续访问子节点
  end
end

# 模式2:收集信息然后分析
class MyRule < Base
  def test(source)
    @method_names = [] of String
    visitor = AST::NodeVisitor.new self, source
    analyze_collected_data(source)
  end

  def test(source, node : Crystal::Def)
    @method_names << node.name
  end

  private def analyze_collected_data(source)
    # 分析@method_names
  end
end

# 模式3:父子关系
class MyRule < Base
  def test(source, node : Crystal::ClassDef)
    # 仅访问此类中的方法
    node.body.accept(MethodVisitor.new(self, source))
  end

  private class MethodVisitor < Crystal::Visitor
    def initialize(@rule : MyRule, @source : Source)
    end

    def visit(node : Crystal::Def)
      @rule.check_method(node, @source)
      true
    end

    def visit(node : Crystal::ASTNode)
      true
    end
  end
end

规则配置

可配置属性

module Ameba::Rule::Custom
  class ConfigurableRule < Base
    properties do
      description "具有可配置属性的规则"

      # 布尔属性
      enabled true
      strict_mode false

      # 数字属性
      max_length 100
      min_length 3

      # 字符串属性
      prefix "test_"
      suffix "_spec"

      # 数组属性
      allowed_names ["foo", "bar", "baz"]
      excluded_paths ["spec/**/*", "lib/**/*"]
    end

    def test(source)
      # 使用属性
      return unless enabled

      if strict_mode
        # 应用严格检查
      end

      AST::NodeVisitor.new self, source
    end
  end
end

.ameba.yml中的配置

Custom/ConfigurableRule:
  Enabled: true
  Severity: Warning
  StrictMode: true
  MaxLength: 120
  MinLength: 5
  Prefix: "app_"
  AllowedNames:
    - "primary"
    - "secondary"
  ExcludedPaths:
    - "spec/fixtures/**"
    - "db/migrations/**"

问题报告

基本问题报告

# 简单问题
issue_for node, "错误消息"

# 带插值的问题
issue_for node, "发现 #{count} 个违规"

# 特定位置的问题
issue_for node.name, "方法名称违反约定"

# 自定义位置的问题
issue_for(
  {node.location.try(&.line_number) || 1, 1},
  node.end_location,
  "自定义消息"
)

问题严重性

# 由配置控制
Custom/MyRule:
  Severity: Error    # 阻止CI
  # 或
  Severity: Warning  # 重要但不阻止
  # 或
  Severity: Convention  # 样式偏好

丰富的问题消息

module Ameba::Rule::Custom
  class RichMessages < Base
    MSG_TEMPLATE = <<-MSG
      方法 '%{method}' 过长(%{actual} 行,最大允许 %{max} 行)
      考虑提取到较小的方法或使用组合。
      MSG

    def test(source, node : Crystal::Def)
      line_count = count_lines(node)

      if line_count > max_lines
        message = MSG_TEMPLATE % {
          method: node.name,
          actual: line_count,
          max: max_lines
        }
        issue_for node, message
      end
    end
  end
end

测试自定义规则

全面的测试套件

require "../spec_helper"

module Ameba::Rule::Custom
  describe DocumentedClasses do
    context "带文档的类" do
      it "通过" do
        rule = DocumentedClasses.new

        source = Source.new %(
          # 这是一个带文档的类
          class MyClass
          end
        )

        rule.test(source)
        source.issues.should be_empty
      end
    end

    context "带未文档化的公共类" do
      it "报告问题" do
        rule = DocumentedClasses.new

        source = Source.new %(
          class MyClass
          end
        )

        rule.test(source)
        source.issues.size.should eq(1)
        source.issues.first.message.should contain("documented")
      end
    end

    context "带私有类" do
      it "允许未文档化的私有类" do
        rule = DocumentedClasses.new

        source = Source.new %(
          private class InternalClass
          end
        )

        rule.test(source)
        source.issues.should be_empty
      end
    end

    context "带空文档" do
      it "报告问题" do
        rule = DocumentedClasses.new

        source = Source.new %(
          #
          class MyClass
          end
        )

        rule.test(source)
        source.issues.size.should eq(1)
      end
    end

    context "配置" do
      it "可以禁用" do
        rule = DocumentedClasses.new
        rule.enabled = false

        source = Source.new %(
          class MyClass
          end
        )

        rule.test(source)
        source.issues.should be_empty
      end
    end
  end
end

测试助手

# spec/spec_helper.cr
require "spec"
require "ameba"
require "../src/ameba-custom-rules"

module Ameba
  # 助手创建测试源
  def self.source(code : String, path = "source.cr")
    Source.new(code, path)
  end

  # 助手期望问题
  def self.expect_issue(rule, code)
    source = Source.new(code)
    rule.test(source)
    source.issues.empty?.should be_false
  end

  # 助手期望无问题
  def self.expect_no_issue(rule, code)
    source = Source.new(code)
    rule.test(source)
    source.issues.should be_empty
  end
end

# 在spec中的用法
describe MyRule do
  it "报告违规" do
    rule = MyRule.new
    Ameba.expect_issue rule, %(
      def bad_code
      end
    )
  end
end

打包和分发

创建可重用的规则包

# shard.yml
name: ameba-company-rules
user-invocable: false
version: 1.0.0

description: |
  用于Crystal项目的公司特定Ameba规则。
  强制执行编码标准和最佳实践。

authors:
  - Company DevTools <devtools@company.com>

crystal: ">= 1.0.0"
license: MIT

development_dependencies:
  ameba:
    github: crystal-ameba/ameba
    version: ~> 1.6.0

# 可选:添加到目标以构建二进制
targets:
  ameba-company:
    main: src/cli.cr

分发策略

# 选项1:作为shard依赖项
# 在用户的shard.yml中
development_dependencies:
  ameba:
    github: crystal-ameba/ameba
  ameba-company-rules:
    github: company/ameba-company-rules

# 选项2:作为供应商规则
# 复制规则文件到项目的lib/ameba-rules/
# 在自定义ameba二进制中包含

# 选项3:作为插件
# 创建独立的可执行文件以扩展ameba

自定义Ameba二进制

# bin/ameba-custom.cr
require "ameba/cli"
require "../lib/ameba-company-rules/src/ameba-company-rules"

# 规则自动发现
Ameba::CLI.run

构建和分发:

crystal build bin/ameba-custom.cr -o bin/ameba-custom
# 分发二进制或从源代码构建

真实世界规则示例

防止N+1查询

module Ameba::Rule::Custom
  class PreventNPlusOne < Base
    properties do
      description "检测潜在的N+1查询模式"
    end

    MSG = "潜在的N+1查询:在循环中访问关联"

    def test(source)
      @in_loop = false
      AST::NodeVisitor.new self, source
    end

    def test(source, node : Crystal::While | Crystal::Call)
      if node.is_a?(Crystal::Call) && node.name.in?("each", "map")
        @in_loop = true
        node.block.try(&.accept(self))
        @in_loop = false
        return false  # 不要再次访问块
      end
      true
    end

    def test(source, node : Crystal::Call)
      return unless @in_loop

      # 检测关联访问模式
      if looks_like_association?(node)
        issue_for node, MSG
      end
    end

    private def looks_like_association?(node)
      # 简化检测
      node.name.in?("user", "posts", "comments") &&
        node.obj != nil
    end
  end
end

强制API版本控制

module Ameba::Rule::Custom
  class ApiVersioning < Base
    properties do
      description "API控制器必须版本化"
    end

    MSG = "API控制器必须在版本化的命名空间中(例如,V1::)"

    def test(source)
      return unless source.path.includes?("/api/")
      AST::NodeVisitor.new self, source
    end

    def test(source, node : Crystal::ClassDef)
      return unless node.name.to_s.ends_with?("Controller")

      unless has_version_namespace?(node)
        issue_for node.name, MSG
      end
    end

    private def has_version_namespace?(node)
      # 检查类名称是否包含版本(V1::, V2::, 等)
      node.name.to_s.matches?(/V\d+::/)
    end
  end
end

何时使用此技能

使用ameba-custom-rules技能时:

  • 强制执行项目特定的编码标准,这些标准未被内置规则覆盖
  • 检测领域特定的反模式或代码异味
  • 验证代码中的业务逻辑约束
  • 创建组织范围内的linting标准
  • 从其他语言迁移并强制执行新模式
  • 防止生产中出现的特定错误
  • 确保跨微服务的一致性
  • 通过自动反馈向团队成员教授代码质量
  • 强制执行架构决策(例如,层边界)
  • 标准化错误处理、日志记录或监控模式

最佳实践

  1. 从简单开始 - 先处理基本的规则,再处理复杂的AST遍历
  2. 彻底测试 - 编写全面的规范,覆盖边缘情况
  3. 提供清晰的消息 - 错误消息应解释问题并建议修复
  4. 使规则可配置 - 使用属性进行阈值和选项设置
  5. 记录您的规则 - 在属性块中包含描述和示例
  6. 使用特定的节点类型 - 为特定的AST节点重载test,而非通用遍历
  7. 考虑性能 - 避免在热路径中进行复杂操作;可能时缓存结果
  8. 遵循命名约定 - 使用描述性规则名称以匹配其目的
  9. 提供修复建议 - 可能时,解释如何解决问题
  10. 适当作用域 - 仅检查相关文件(使用source.path检查)
  11. 安全处理nil - 访问可能为nil的AST属性时使用try(&.)
  12. 避免误报 - 更好的是错过一些情况,而不是标记正确的代码
  13. 版本化您的规则 - 跟踪规则版本和重大更改
  14. 保持规则聚焦 - 一个规则应检查一件事(单一责任)
  15. 与CI集成 - 确保自定义规则在自动化环境中工作

常见陷阱

  1. 匹配过于广泛 - 捕获过多情况并产生误报
  2. 未处理nil - AST节点可能具有nil属性,导致崩溃
  3. 忽略可见性 - 检查私有方法,而仅公开API重要
  4. 复杂的访问器逻辑 - 使遍历代码难以理解和维护
  5. 缺失边缘情况 - 未测试不寻常但有效的代码模式
  6. 错误消息差 - 模糊的消息,不帮助开发人员修复问题
  7. 硬编码值 - 未使阈值和选项可配置
  8. 检查生成的代码 - 标记不应更改的自动生成文件
  9. 性能问题 - 复杂的规则显著减慢分析速度
  10. 依赖冲突 - 将Ameba作为常规依赖项而非development_dependencies
  11. 未使用属性 - 硬编码配置而非使用属性块
  12. 不完全测试 - 未测试禁用状态、边缘情况或配置
  13. 紧耦合 - 依赖其他规则或特定文件结构的规则
  14. 不明确作用域 - 应用于错误文件或上下文的规则
  15. 版本不兼容 - 未针对多个Ameba版本进行测试

资源