271 lines
8.7 KiB
Markdown
271 lines
8.7 KiB
Markdown
---
|
|
title: Development Guide
|
|
description: 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`.
|
|
|
|
```bash
|
|
# 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`:
|
|
|
|
```bash
|
|
go build -trimpath -ldflags="-s -w" -o bin/core-lint ./cmd/core-lint
|
|
```
|
|
|
|
## Running Tests
|
|
|
|
```bash
|
|
# 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:
|
|
|
|
```go
|
|
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:
|
|
|
|
```go
|
|
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
|
|
|
|
```bash
|
|
# 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:
|
|
|
|
```go
|
|
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.
|