This document provides an overview of the linter engine implementation.
The linter engine is a generic, spec-agnostic framework for implementing configurable linting rules across different API specifications (OpenAPI, Arazzo, Swagger).
-
Generic Linter Engine (
linter/)Linter[T]- Main linting engine with configuration supportRegistry[T]- Rule registry with category managementRule- Base rule interface and specialized interfacesRuleConfig- Per-rule configuration with severity overridesDocumentInfo[T]- Document + location for reference resolution- Format types for text and JSON output
- Parallel rule execution for improved performance
-
OpenAPI Linter (
openapi/linter/)- OpenAPI-specific linter implementation
- Rule registry with built-in rules
- Integration with OpenAPI parser and validator
-
Rules (
openapi/linter/rules/)- Individual linting rules (e.g.,
style-path-params) - Each rule implements the
RuleRunner[*openapi.OpenAPI]interface
- Individual linting rules (e.g.,
-
CLI Integration (
cmd/openapi/commands/openapi/lint.go)openapi spec lintcommand- Configuration file support (
lint.yaml) - Rule documentation generation (
--list-rules)
Rules can be configured via YAML configuration file:
extends:
- all # or specific rulesets like "recommended", "strict"
categories:
style:
enabled: true
severity: warning
rules:
- id: style-path-params
severity: error
- id: validation-required-field
match: ".*info\\.title is required.*"
disabled: trueRules have default severities that can be overridden:
- Fatal errors (terminate execution)
- Error severity (build failures)
- Warning severity (informational)
Rules automatically resolve external references (HTTP URLs, file paths):
paths:
/users/{userId}:
get:
parameters:
- $ref: "https://example.com/params/user-id.yaml"
responses:
'200':
description: okThe linter:
- Uses
DocumentInfo.Locationas the base for resolving relative references - Supports custom HTTP clients and virtual filesystems via
LintOptions.ResolveOptions - Reports resolution errors as validation errors with proper severity and location
Rules can suggest fixes using validation.Error with quick fix support:
validation.NewValidationErrorWithQuickFix(
severity,
rule,
fmt.Errorf("path parameter {%s} is not defined", param),
node,
&validation.QuickFix{
Description: "Add missing path parameter",
Replacement: "...",
},
)Ensures path template variables (e.g., {userId}) have corresponding parameter definitions with in='path'.
Checks:
- All template params must have corresponding parameter definitions
- All path parameters must be used in the template
- Works with parameters at PathItem level (inherited) and Operation level (can override)
- Resolves external references to parameters
Example:
# ✅ Valid
paths:
/users/{userId}:
get:
parameters:
- name: userId
in: path
required: true
# ❌ Invalid - missing parameter definition
paths:
/users/{userId}:
get:
responses:
'200':
description: ok# Lint with default configuration
openapi spec lint openapi.yaml
# Lint with custom config
openapi spec lint --config /path/to/lint.yaml openapi.yaml
# List all available rules
openapi spec lint --list-rules
# Output in JSON format
openapi spec lint --format json openapi.yamlimport (
"context"
"github.com/speakeasy-api/openapi/linter"
openapiLinter "github.com/speakeasy-api/openapi/openapi/linter"
)
// Create linter with configuration
config := &linter.Config{
Extends: []string{"all"},
}
lntr := openapiLinter.NewOpenAPILinter(config)
// Lint document
docInfo := &linter.DocumentInfo[*openapi.OpenAPI]{
Document: doc,
Location: "/path/to/openapi.yaml",
}
output, err := lntr.Lint(ctx, docInfo, nil, nil)
if err != nil {
// Handle error
}
// Check results
if output.HasErrors() {
fmt.Println(output.FormatText())
}To apply the config filters to additional errors after the initial lint (for example, errors discovered during lazy reference resolution), use FilterErrors:
filtered := lntr.FilterErrors(extraErrors)To add a new rule:
- Create the rule in
openapi/linter/rules/
type MyRule struct{}
func (r *MyRule) ID() string { return "style-my-rule" }
func (r *MyRule) Category() string { return "style" }
func (r *MyRule) Description() string { return "..." }
func (r *MyRule) Link() string { return "..." }
func (r *MyRule) DefaultSeverity() validation.Severity {
return validation.SeverityWarning
}
func (r *MyRule) Versions() []string { return nil }
func (r *MyRule) Run(ctx context.Context, docInfo *linter.DocumentInfo[*openapi.OpenAPI], config *linter.RuleConfig) []error {
doc := docInfo.Document
// Implement rule logic
// Use openapi.Walk() to traverse the document
// Return validation.Error instances for violations
return nil
}- Register the rule in
openapi/linter/linter.go
registry.Register(&rules.MyRule{})- Write tests in
openapi/linter/rules/my_rule_test.go
func TestMyRule_Success(t *testing.T) {
t.Parallel()
// ... test implementation
}The linter engine supports custom rule loaders that can be registered via the RegisterCustomRuleLoader function. This allows spec-specific linters to support custom rules written in different languages or formats.
// CustomRuleLoaderFunc loads custom rules from configuration
type CustomRuleLoaderFunc func(config *CustomRulesConfig) ([]RuleRunner[T], error)
// Register a custom rule loader
linter.RegisterCustomRuleLoader(myLoader)Custom rules loaded through registered loaders:
- Are automatically registered with the rule registry
- Support the same configuration options as built-in rules (severity, disabled, match)
- Integrate seamlessly with category-based configuration
- Generic Architecture - The core linter is spec-agnostic (
Linter[T any]) - Type Safety - Spec-specific rules use typed interfaces (
RuleRunner[*openapi.OpenAPI]) - Separation of Concerns - Core engine, spec linters, and rules are separate packages
- Extensibility - Easy to add new rules, rulesets, specs, and custom rule loaders
- Configuration Over Code - Rule behavior controlled via YAML config
- Reference Resolution - Automatic external reference resolution with proper error handling
- Testing - Comprehensive test coverage with parallel execution