linter

package
v1.16.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 6, 2026 License: MIT Imports: 16 Imported by: 0

README

Linter Engine

This document provides an overview of the linter engine implementation.

Architecture Overview

The linter engine is a generic, spec-agnostic framework for implementing configurable linting rules across different API specifications (OpenAPI, Arazzo, Swagger).

Core Components
  1. Generic Linter Engine (linter/)

    • Linter[T] - Main linting engine with configuration support
    • Registry[T] - Rule registry with category management
    • Rule - Base rule interface and specialized interfaces
    • RuleConfig - Per-rule configuration with severity overrides
    • DocumentInfo[T] - Document + location for reference resolution
    • Format types for text and JSON output
    • Parallel rule execution for improved performance
  2. OpenAPI Linter (openapi/linter/)

    • OpenAPI-specific linter implementation
    • Rule registry with built-in rules
    • Integration with OpenAPI parser and validator
  3. Rules (openapi/linter/rules/)

  4. CLI Integration (cmd/openapi/commands/openapi/lint.go)

    • openapi spec lint command
    • Configuration file support (lint.yaml)
    • Rule documentation generation (--list-rules)

Key Features

1. Rule Configuration

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: true
2. Severity Overrides

Rules have default severities that can be overridden:

  • Fatal errors (terminate execution)
  • Error severity (build failures)
  • Warning severity (informational)
3. External Reference Resolution

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: ok

The linter:

  • Uses DocumentInfo.Location as 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
5. Quick Fix Suggestions

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: "...",
    },
)

Implemented Rules

style-path-params

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

Usage

CLI
# 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.yaml
Programmatic
import (
    "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())
}

Filtering Errors After Linting

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)

Adding New Rules

To add a new rule:

  1. 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
}
  1. Register the rule in openapi/linter/linter.go
registry.Register(&rules.MyRule{})
  1. Write tests in openapi/linter/rules/my_rule_test.go
func TestMyRule_Success(t *testing.T) {
    t.Parallel()
    // ... test implementation
}

Custom Rule Loading

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

Design Principles

  1. Generic Architecture - The core linter is spec-agnostic (Linter[T any])
  2. Type Safety - Spec-specific rules use typed interfaces (RuleRunner[*openapi.OpenAPI])
  3. Separation of Concerns - Core engine, spec linters, and rules are separate packages
  4. Extensibility - Easy to add new rules, rulesets, specs, and custom rule loaders
  5. Configuration Over Code - Rule behavior controlled via YAML config
  6. Reference Resolution - Automatic external reference resolution with proper error handling
  7. Testing - Comprehensive test coverage with parallel execution

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type CategoryConfig

type CategoryConfig struct {
	// Enabled controls whether all rules in the category are active
	Enabled *bool `yaml:"enabled,omitempty" json:"enabled,omitempty"`

	// Severity overrides the default severity for all rules in the category
	Severity *validation.Severity `yaml:"severity,omitempty" json:"severity,omitempty"`
}

CategoryConfig configures an entire category of rules

func (*CategoryConfig) UnmarshalYAML

func (c *CategoryConfig) UnmarshalYAML(value *yaml.Node) error

UnmarshalYAML allows severity aliases (warn, info) in categories.

type Config

type Config struct {
	// Extends specifies rulesets to extend (e.g., "recommended", "all")
	Extends []string `yaml:"extends,omitempty" json:"extends,omitempty"`

	// Rules contains per-rule configuration
	Rules []RuleEntry `yaml:"rules,omitempty" json:"rules,omitempty"`

	// Categories contains per-category configuration
	Categories map[string]CategoryConfig `yaml:"categories,omitempty" json:"categories,omitempty"`

	// OutputFormat specifies the output format
	OutputFormat OutputFormat `yaml:"output_format,omitempty" json:"output_format,omitempty"`

	// CustomRules configures custom rule loading (requires customrules package import)
	CustomRules *CustomRulesConfig `yaml:"custom_rules,omitempty" json:"custom_rules,omitempty"`
}

Config represents the linter configuration

func LoadConfig

func LoadConfig(r io.Reader) (*Config, error)

LoadConfig loads lint configuration from a YAML reader.

func LoadConfigFromFile

func LoadConfigFromFile(path string) (*Config, error)

LoadConfigFromFile loads lint configuration from a YAML file.

func NewConfig

func NewConfig() *Config

NewConfig creates a new default configuration

func (*Config) UnmarshalYAML

func (c *Config) UnmarshalYAML(value *yaml.Node) error

UnmarshalYAML supports "extends" as string or list and severity aliases.

func (*Config) Validate

func (c *Config) Validate() error

Validate checks for missing rule IDs in the configuration.

type ConfigurableRule

type ConfigurableRule interface {
	Rule

	// ConfigSchema returns JSON Schema for rule-specific options
	ConfigSchema() map[string]any

	// ConfigDefaults returns default values for options
	ConfigDefaults() map[string]any
}

ConfigurableRule indicates a rule has configurable options

type CustomRulesConfig

type CustomRulesConfig struct {
	// Paths are glob patterns for rule files (e.g., "./rules/*.ts")
	Paths []string `yaml:"paths,omitempty" json:"paths,omitempty"`

	// Timeout is the maximum execution time per rule (default: 30s)
	Timeout time.Duration `yaml:"timeout,omitempty" json:"timeout,omitempty"`
}

CustomRulesConfig configures custom rule loading. This is the YAML-serializable configuration. The customrules package extends this with additional programmatic options like Logger.

type DocGenerator

type DocGenerator[T any] struct {
	// contains filtered or unexported fields
}

DocGenerator generates documentation from registered rules

func NewDocGenerator

func NewDocGenerator[T any](registry *Registry[T]) *DocGenerator[T]

NewDocGenerator creates a new documentation generator

func (*DocGenerator[T]) GenerateAllRuleDocs

func (g *DocGenerator[T]) GenerateAllRuleDocs() []*RuleDoc

GenerateAllRuleDocs generates documentation for all registered rules

func (*DocGenerator[T]) GenerateCategoryDocs

func (g *DocGenerator[T]) GenerateCategoryDocs() map[string][]*RuleDoc

GenerateCategoryDocs groups rules by category

func (*DocGenerator[T]) GenerateRuleDoc

func (g *DocGenerator[T]) GenerateRuleDoc(rule RuleRunner[T]) *RuleDoc

GenerateRuleDoc generates documentation for a single rule

func (*DocGenerator[T]) WriteJSON

func (g *DocGenerator[T]) WriteJSON(w io.Writer) error

WriteJSON writes rule documentation as JSON

func (*DocGenerator[T]) WriteMarkdown

func (g *DocGenerator[T]) WriteMarkdown(w io.Writer) error

WriteMarkdown writes rule documentation as Markdown

type DocumentInfo

type DocumentInfo[T any] struct {
	// Document is the parsed document to lint
	Document T

	// Location is the absolute location (URL or file path) of the document
	// This is used for resolving relative references
	Location string

	// Index contains an index of various nodes from the provided document
	Index *openapi.Index
}

DocumentInfo contains a document and its metadata for linting

func NewDocumentInfo

func NewDocumentInfo[T any](doc T, location string) *DocumentInfo[T]

NewDocumentInfo creates a new DocumentInfo with the given document and location

func NewDocumentInfoWithIndex

func NewDocumentInfoWithIndex[T any](doc T, location string, index *openapi.Index) *DocumentInfo[T]

NewDocumentInfoWithIndex creates a new DocumentInfo with a pre-computed index

type DocumentedRule

type DocumentedRule interface {
	Rule

	// GoodExample returns YAML showing correct usage
	GoodExample() string

	// BadExample returns YAML showing incorrect usage
	BadExample() string

	// Rationale explains why this rule exists
	Rationale() string

	// FixAvailable returns true if the rule provides auto-fix suggestions
	FixAvailable() bool
}

DocumentedRule provides extended documentation for a rule

type LintOptions

type LintOptions struct {
	// ResolveOptions contains options for reference resolution
	// If nil, default options will be used
	ResolveOptions *references.ResolveOptions

	// VersionFilter is the document version (e.g., "3.0", "3.1")
	// If set, only rules that apply to this version will be run
	// Rules with nil/empty Versions() apply to all versions
	VersionFilter *string
}

LintOptions contains runtime options for linting

type Linter

type Linter[T any] struct {
	// contains filtered or unexported fields
}

Linter is the main linting engine

func NewLinter

func NewLinter[T any](config *Config, registry *Registry[T]) *Linter[T]

NewLinter creates a new linter with the given configuration

func (*Linter[T]) FilterErrors

func (l *Linter[T]) FilterErrors(errs []error) []error

FilterErrors applies rule-level overrides and match filters to any errors.

func (*Linter[T]) Lint

func (l *Linter[T]) Lint(ctx context.Context, docInfo *DocumentInfo[T], preExistingErrors []error, opts *LintOptions) (*Output, error)

Lint runs all configured rules against the document

func (*Linter[T]) Registry

func (l *Linter[T]) Registry() *Registry[T]

Registry returns the rule registry for documentation generation

type Output

type Output struct {
	Results []error
	Format  OutputFormat
}

Output represents the result of linting

func (*Output) ErrorCount

func (o *Output) ErrorCount() int

func (*Output) FormatJSON

func (o *Output) FormatJSON() string

func (*Output) FormatText

func (o *Output) FormatText() string

func (*Output) HasErrors

func (o *Output) HasErrors() bool

type OutputFormat

type OutputFormat string
const (
	OutputFormatText OutputFormat = "text"
	OutputFormatJSON OutputFormat = "json"
)

type Registry

type Registry[T any] struct {
	// contains filtered or unexported fields
}

Registry holds registered rules

func NewRegistry

func NewRegistry[T any]() *Registry[T]

NewRegistry creates a new rule registry

func (*Registry[T]) AllCategories

func (r *Registry[T]) AllCategories() []string

AllCategories returns all unique categories

func (*Registry[T]) AllRuleIDs

func (r *Registry[T]) AllRuleIDs() []string

AllRuleIDs returns all registered rule IDs

func (*Registry[T]) AllRules

func (r *Registry[T]) AllRules() []RuleRunner[T]

AllRules returns all registered rules

func (*Registry[T]) AllRulesets

func (r *Registry[T]) AllRulesets() []string

AllRulesets returns all registered ruleset names

func (*Registry[T]) GetRule

func (r *Registry[T]) GetRule(id string) (RuleRunner[T], bool)

GetRule returns a rule by ID

func (*Registry[T]) GetRuleset

func (r *Registry[T]) GetRuleset(name string) ([]string, bool)

GetRuleset returns rule IDs for a ruleset

func (*Registry[T]) Register

func (r *Registry[T]) Register(rule RuleRunner[T])

Register registers a rule

func (*Registry[T]) RegisterRuleset

func (r *Registry[T]) RegisterRuleset(name string, ruleIDs []string) error

RegisterRuleset registers a ruleset

func (*Registry[T]) RulesetsContaining

func (r *Registry[T]) RulesetsContaining(ruleID string) []string

RulesetsContaining returns names of rulesets that contain the given rule ID

type Rule

type Rule interface {
	// ID returns the unique identifier for this rule (e.g., "style-path-params")
	ID() string

	// Category returns the rule category (e.g., "style", "validation", "security")
	Category() string

	// Description returns a human-readable description of what the rule checks
	Description() string

	// Summary returns a short summary of what the rule checks
	Summary() string

	// Link returns an optional URL to documentation for this rule
	Link() string

	// DefaultSeverity returns the default severity level for this rule
	DefaultSeverity() validation.Severity

	// Versions returns the spec versions this rule applies to (nil = all versions)
	Versions() []string
}

Rule represents a single linting rule

type RuleConfig

type RuleConfig struct {
	// Enabled controls whether the rule is active
	Enabled *bool `yaml:"enabled,omitempty" json:"enabled,omitempty"`

	// Severity overrides the default severity
	Severity *validation.Severity `yaml:"severity,omitempty" json:"severity,omitempty"`

	// ResolveOptions contains runtime options for reference resolution (not serialized)
	// These are set by the linter engine when running rules
	ResolveOptions *references.ResolveOptions `yaml:"-" json:"-"`
}

RuleConfig configures a specific rule

func (*RuleConfig) GetSeverity

func (c *RuleConfig) GetSeverity(defaultSeverity validation.Severity) validation.Severity

GetSeverity returns the effective severity, falling back to default if not overridden

type RuleDoc

type RuleDoc struct {
	ID              string         `json:"id" yaml:"id"`
	Category        string         `json:"category" yaml:"category"`
	Summary         string         `json:"summary" yaml:"summary"`
	Description     string         `json:"description" yaml:"description"`
	Rationale       string         `json:"rationale,omitempty" yaml:"rationale,omitempty"`
	Link            string         `json:"link,omitempty" yaml:"link,omitempty"`
	DefaultSeverity string         `json:"default_severity" yaml:"default_severity"`
	Versions        []string       `json:"versions,omitempty" yaml:"versions,omitempty"`
	GoodExample     string         `json:"good_example,omitempty" yaml:"good_example,omitempty"`
	BadExample      string         `json:"bad_example,omitempty" yaml:"bad_example,omitempty"`
	FixAvailable    bool           `json:"fix_available" yaml:"fix_available"`
	ConfigSchema    map[string]any `json:"config_schema,omitempty" yaml:"config_schema,omitempty"`
	ConfigDefaults  map[string]any `json:"config_defaults,omitempty" yaml:"config_defaults,omitempty"`
	Rulesets        []string       `json:"rulesets" yaml:"rulesets"`
}

RuleDoc represents documentation for a single rule

type RuleEntry

type RuleEntry struct {
	ID       string               `yaml:"id" json:"id"`
	Severity *validation.Severity `yaml:"severity,omitempty" json:"severity,omitempty"`
	Disabled *bool                `yaml:"disabled,omitempty" json:"disabled,omitempty"`
	Match    *regexp.Regexp       `yaml:"match,omitempty" json:"match,omitempty"`
}

RuleEntry configures rule behavior in lint.yaml.

func (*RuleEntry) UnmarshalYAML

func (r *RuleEntry) UnmarshalYAML(value *yaml.Node) error

UnmarshalYAML allows severity aliases (warn, info) in rule entries.

type RuleRunner

type RuleRunner[T any] interface {
	Rule

	// Run executes the rule against the provided document
	// DocumentInfo provides both the document and its location for resolving external references
	// Returns any issues found as validation errors
	Run(ctx context.Context, docInfo *DocumentInfo[T], config *RuleConfig) []error
}

RuleRunner is the interface rules must implement to execute their logic This is separate from Rule to allow different runner types for different specs

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL