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
关键组件
- 模块命名空间 - 自定义规则通常使用
Ameba::Rule::Custom或Ameba::Rule::<类别> - 基类 - 所有规则都继承自
Ameba::Rule::Base - 属性块 - 定义规则元数据和配置
- 消息常量 - 显示给用户的错误消息
- 测试方法 - 初始化AST访问器的入口点
- 重载测试方法 - 处理特定的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标准
- 从其他语言迁移并强制执行新模式
- 防止生产中出现的特定错误
- 确保跨微服务的一致性
- 通过自动反馈向团队成员教授代码质量
- 强制执行架构决策(例如,层边界)
- 标准化错误处理、日志记录或监控模式
最佳实践
- 从简单开始 - 先处理基本的规则,再处理复杂的AST遍历
- 彻底测试 - 编写全面的规范,覆盖边缘情况
- 提供清晰的消息 - 错误消息应解释问题并建议修复
- 使规则可配置 - 使用属性进行阈值和选项设置
- 记录您的规则 - 在属性块中包含描述和示例
- 使用特定的节点类型 - 为特定的AST节点重载
test,而非通用遍历 - 考虑性能 - 避免在热路径中进行复杂操作;可能时缓存结果
- 遵循命名约定 - 使用描述性规则名称以匹配其目的
- 提供修复建议 - 可能时,解释如何解决问题
- 适当作用域 - 仅检查相关文件(使用source.path检查)
- 安全处理nil - 访问可能为nil的AST属性时使用try(&.)
- 避免误报 - 更好的是错过一些情况,而不是标记正确的代码
- 版本化您的规则 - 跟踪规则版本和重大更改
- 保持规则聚焦 - 一个规则应检查一件事(单一责任)
- 与CI集成 - 确保自定义规则在自动化环境中工作
常见陷阱
- 匹配过于广泛 - 捕获过多情况并产生误报
- 未处理nil - AST节点可能具有nil属性,导致崩溃
- 忽略可见性 - 检查私有方法,而仅公开API重要
- 复杂的访问器逻辑 - 使遍历代码难以理解和维护
- 缺失边缘情况 - 未测试不寻常但有效的代码模式
- 错误消息差 - 模糊的消息,不帮助开发人员修复问题
- 硬编码值 - 未使阈值和选项可配置
- 检查生成的代码 - 标记不应更改的自动生成文件
- 性能问题 - 复杂的规则显著减慢分析速度
- 依赖冲突 - 将Ameba作为常规依赖项而非development_dependencies
- 未使用属性 - 硬编码配置而非使用属性块
- 不完全测试 - 未测试禁用状态、边缘情况或配置
- 紧耦合 - 依赖其他规则或特定文件结构的规则
- 不明确作用域 - 应用于错误文件或上下文的规则
- 版本不兼容 - 未针对多个Ameba版本进行测试