lint/docs/development.md
Snider e876b62045 docs: add human-friendly documentation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 13:02:40 +00:00

8.7 KiB

title description
Development Guide How to build, test, and contribute to core/lint

Development Guide

Prerequisites

  • Go 1.26 or later
  • core CLI (for build and QA commands)
  • gh CLI (only needed for the qa watch, qa review, qa health, and qa issues commands)

Building

The project uses the core build system. Configuration lives in .core/build.yaml.

# Build the binary (outputs to ./bin/core-lint)
core build

# Build targets: linux/amd64, linux/arm64, darwin/arm64, windows/amd64
# CGO is disabled; the binary is fully static.

To build manually with go build:

go build -trimpath -ldflags="-s -w" -o bin/core-lint ./cmd/core-lint

Running Tests

# Run all tests
core go test

# Run a single test by name
core go test --run TestRule_Validate_Good

# Generate coverage report
core go cov
core go cov --open    # Opens HTML report in browser

The test suite covers all packages:

Package Test count Focus
pkg/lint ~89 Rule validation, catalog loading, matcher, scanner, report, complexity, coverage, vulncheck, toolkit
pkg/detect 6 Project type detection
pkg/php ~125 All PHP tool wrappers (format, analyse, audit, security, refactor, mutation, test, pipeline, runner)

Tests follow the _Good, _Bad, _Ugly suffix convention:

  • _Good -- happy path
  • _Bad -- expected error conditions
  • _Ugly -- edge cases and panics

Test Examples

Testing rules against source content:

func TestMatcher_Match_Good(t *testing.T) {
    rules := []Rule{
        {
            ID:        "test-001",
            Title:     "TODO found",
            Severity:  "low",
            Pattern:   `TODO`,
            Detection: "regex",
        },
    }
    m, err := NewMatcher(rules)
    require.NoError(t, err)

    findings := m.Match("example.go", []byte("// TODO: fix this"))
    assert.Len(t, findings, 1)
    assert.Equal(t, "test-001", findings[0].RuleID)
    assert.Equal(t, 1, findings[0].Line)
}

Testing complexity analysis without file I/O:

func TestAnalyseComplexitySource_Good(t *testing.T) {
    src := `package example
func simple() { if true {} }
func complex() {
    if a {} else if b {} else if c {}
    for i := range items {
        switch {
        case x: if y {}
        case z:
        }
    }
}`
    results, err := AnalyseComplexitySource(src, "test.go", 3)
    require.NoError(t, err)
    assert.NotEmpty(t, results)
}

Quality Assurance

# Full QA pipeline: format, vet, lint, test
core go qa

# Extended QA: includes race detection, vulnerability scan, security checks
core go qa full

# Individual checks
core go fmt        # Format code
core go vet        # Run go vet
core go lint       # Run linter

Project Structure

lint/
├── .core/
│   └── build.yaml          # Build configuration
├── bin/                     # Build output (gitignored)
├── catalog/
│   ├── go-correctness.yaml  # Correctness rules (7 rules)
│   ├── go-modernise.yaml    # Modernisation rules (5 rules)
│   └── go-security.yaml     # Security rules (6 rules)
├── cmd/
│   ├── core-lint/
│   │   └── main.go          # CLI binary entry point
│   └── qa/
│       ├── cmd_qa.go         # QA command group registration
│       ├── cmd_watch.go      # GitHub Actions monitoring
│       ├── cmd_review.go     # PR review status
│       ├── cmd_health.go     # Aggregate CI health
│       ├── cmd_issues.go     # Issue triage
│       ├── cmd_docblock.go   # Docblock coverage
│       └── cmd_php.go        # PHP QA subcommands
├── pkg/
│   ├── detect/
│   │   ├── detect.go         # Project type detection
│   │   └── detect_test.go
│   ├── lint/
│   │   ├── catalog.go        # Catalog loading and querying
│   │   ├── complexity.go     # Cyclomatic complexity (native AST)
│   │   ├── coverage.go       # Coverage tracking and comparison
│   │   ├── matcher.go        # Regex matching engine
│   │   ├── report.go         # Output formatters (text, JSON, JSONL)
│   │   ├── rule.go           # Rule type and validation
│   │   ├── scanner.go        # Directory walking and file scanning
│   │   ├── tools.go          # Toolkit (subprocess wrappers)
│   │   ├── vulncheck.go      # govulncheck JSON parser
│   │   ├── testdata/
│   │   │   └── catalog/
│   │   │       └── test-rules.yaml
│   │   └── *_test.go
│   └── php/
│       ├── analyse.go         # PHPStan/Larastan/Psalm wrappers
│       ├── audit.go           # Composer audit + npm audit
│       ├── format.go          # Laravel Pint wrapper
│       ├── mutation.go        # Infection wrapper
│       ├── pipeline.go        # QA stage definitions
│       ├── refactor.go        # Rector wrapper
│       ├── runner.go          # Process spec builder
│       ├── security.go        # Security checks (.env, filesystem)
│       ├── test.go            # Pest/PHPUnit wrapper
│       └── *_test.go
├── lint.go                    # Root package: embedded catalog loader
├── go.mod
├── go.sum
├── CLAUDE.md
└── README.md

Writing New Rules

Rule Schema

Each YAML file in catalog/ contains an array of rule objects. Required fields:

Field Type Description
id string Unique identifier (convention: {lang}-{category}-{number}, e.g., go-sec-001)
title string Short human-readable description
severity string One of: info, low, medium, high, critical
languages []string Target languages (e.g., [go], [go, php])
pattern string Detection pattern (regex for detection: regex)
fix string Remediation guidance
detection string Detection type (currently only regex)

Optional fields:

Field Type Description
tags []string Categorisation tags (e.g., [security, injection])
exclude_pattern string Regex to suppress false positives
found_in []string Repos where the pattern was originally observed
example_bad string Code example that triggers the rule
example_good string Corrected code example
first_seen string Date the pattern was first catalogued
auto_fixable bool Whether automated fixing is feasible

Naming Convention

Rule IDs follow the pattern {lang}-{category}-{number}:

  • go-sec-* -- Security rules
  • go-cor-* -- Correctness rules
  • go-mod-* -- Modernisation rules

Testing a New Rule

Create a test that verifies the pattern matches expected code and does not match exclusions:

func TestNewRule_Matches(t *testing.T) {
    rules := []Rule{
        {
            ID:             "go-xxx-001",
            Title:          "My new rule",
            Severity:       "medium",
            Languages:      []string{"go"},
            Pattern:        `my-pattern`,
            ExcludePattern: `safe-variant`,
            Detection:      "regex",
        },
    }

    m, err := NewMatcher(rules)
    require.NoError(t, err)

    // Should match
    findings := m.Match("example.go", []byte("code with my-pattern here"))
    assert.Len(t, findings, 1)

    // Should not match (exclusion)
    findings = m.Match("example.go", []byte("code with safe-variant here"))
    assert.Empty(t, findings)
}

Adding PHP Tool Support

To add support for a new PHP tool:

  1. Create a new file in pkg/php/ (e.g., newtool.go).
  2. Add a detection function that checks for config files or vendor binaries.
  3. Add an options struct and an execution function.
  4. Add a command in cmd/qa/cmd_php.go that wires the tool to the CLI.
  5. Add the tool to the pipeline stages in pipeline.go if appropriate.
  6. Write tests in a corresponding *_test.go file.

Follow the existing pattern -- each tool module exports:

  • Detect*() -- returns whether the tool is available
  • Run*() or the tool function -- executes the tool with options
  • A *Options struct -- configures behaviour

Coding Standards

  • UK English throughout: colour, organisation, centre, modernise, analyse, serialise
  • Strict typing: All function parameters and return values must have explicit types
  • Testing: Use testify assertions (assert, require)
  • Error wrapping: Use fmt.Errorf("context: %w", err) for error chains
  • Formatting: Standard Go formatting via gofmt / core go fmt

Licence

This project is licenced under the EUPL-1.2.