Claude
Skills
Sign in
Back

ameba-custom-rules

Included with Lifetime
$97 forever

Use when creating custom Ameba rules for Crystal code analysis including rule development, AST traversal, issue reporting, and rule testing.

Data & Analytics

What this skill does


# Ameba Custom Rules

Create custom linting rules for Ameba to enforce project-specific code quality standards and catch domain-specific code smells in Crystal projects.

## Understanding Custom Rules

Custom Ameba rules allow you to:

- Enforce project-specific coding standards
- Catch domain-specific anti-patterns
- Validate business logic constraints
- Ensure consistency across large codebases
- Create reusable rule libraries for your organization
- Extend Ameba's built-in capabilities

## Rule Anatomy

### Basic Rule Structure

Every Ameba rule inherits from `Ameba::Rule::Base` and follows this structure:

```crystal
module Ameba::Rule::Custom
  # Rule that enforces documentation on public classes
  class DocumentedClasses < Base
    properties do
      description "Enforces public classes to be documented"
    end

    MSG = "Class must be documented with a comment"

    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
```

### Key Components

1. **Module namespace** - Custom rules typically use `Ameba::Rule::Custom` or `Ameba::Rule::<Category>`
2. **Base class** - All rules inherit from `Ameba::Rule::Base`
3. **Properties block** - Defines rule metadata and configuration
4. **Message constant** - The error message shown to users
5. **Test method** - Entry point that initializes the AST visitor
6. **Overloaded test methods** - Handle specific AST node types

## Creating Your First Custom Rule

### Step 1: Project Setup

Create a Crystal library for your custom rules:

```bash
# Initialize a new Crystal library
crystal init lib ameba-custom-rules
cd ameba-custom-rules
```

Update `shard.yml`:

```yaml
name: ameba-custom-rules
user-invocable: false
version: 0.1.0
authors:
  - Your Name <[email protected]>

description: Custom Ameba rules for your project

crystal: ">= 1.0.0"

license: MIT

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

**Important:** Ameba should be a development dependency to avoid version conflicts.

### Step 2: Implement a Simple Rule

Create `src/ameba-custom-rules/no_sleep_in_production.cr`:

```crystal
require "ameba"

module Ameba::Rule::Custom
  # Prevents sleep() calls in production code
  class NoSleepInProduction < Base
    properties do
      description "Prevents sleep calls in production code"
      enabled true
    end

    MSG = "Avoid using sleep() in production code; use proper background jobs"

    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
```

### Step 3: Register and Use the Rule

Create main file `src/ameba-custom-rules.cr`:

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

# Rules are automatically registered through inheritance
```

Update your project's Ameba configuration:

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

### Step 4: Test Your Rule

Create `spec/ameba-custom-rules/no_sleep_in_production_spec.cr`:

```crystal
require "../spec_helper"

module Ameba::Rule::Custom
  describe NoSleepInProduction do
    it "reports sleep calls" do
      rule = NoSleepInProduction.new

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

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

    it "allows code without sleep" do
      rule = NoSleepInProduction.new

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

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

## Advanced Rule Examples

### Enforcing Naming Conventions

```crystal
module Ameba::Rule::Custom
  # Enforces that service classes end with "Service"
  class ServiceClassNaming < Base
    properties do
      description "Service classes must end with 'Service'"
      enabled true
    end

    MSG = "Service class name should end with 'Service'"

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

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

      # Check if class is in services directory
      return unless source.path.includes?("/services/")

      # Check if name ends with Service
      unless class_name.ends_with?("Service")
        issue_for node.name, MSG
      end
    end
  end
end
```

### Detecting Dangerous Method Calls

```crystal
module Ameba::Rule::Custom
  # Prevents dangerous ActiveRecord-like methods
  class NoDangerousDatabaseCalls < Base
    properties do
      description "Prevents dangerous database operations"
      dangerous_methods ["delete_all", "destroy_all", "update_all"]
    end

    MSG = "Dangerous method %s without conditions"

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

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

      # Check if call has arguments (conditions)
      if node.args.empty?
        message = MSG % node.name
        issue_for node, message
      end
    end
  end
end
```

### Enforcing Error Handling

```crystal
module Ameba::Rule::Custom
  # Ensures HTTP client calls have error handling
  class HttpErrorHandling < Base
    properties do
      description "HTTP client calls must handle errors"
      enabled true
    end

    MSG = "HTTP client calls should be wrapped in 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  # Continue visiting children
    end

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

      # Check for HTTP client calls
      if node.obj.try(&.to_s.includes?("HTTP"))
        issue_for node, MSG
      end
    end
  end
end
```

### Validating Method Complexity

```crystal
module Ameba::Rule::Custom
  # Limits method complexity
  class MethodComplexity < Base
    properties do
      description "Methods should not be too complex"
      max_complexity 10
    end

    MSG = "Method complexity (%d) exceeds maximum (%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)
        # Count logical operators
        if node.name.in?("&&", "||")
          @complexity += 1
        end
        true
      end

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

### Enforcing Documentation Standards

```crystal
module Ameba::Rule::Custom
  # Requires documentation with specific format
  class DocumentationFormat < Base
    properties do
      description "Public methods must have documentation with examples"
      enabled true
      require_examples true
    end

    MSG_NO_DOC = "Public method must have documentation"
    MSG_NO_EXAMPLE = "Documentation must include usage examples"

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

    def test(source, node : Crystal:

Related in Data & Analytics