Documentation
¶
Overview ¶
Package lint provides static analysis for ELPS lisp source files.
The linter is modeled after go vet: each check is an independent Analyzer that receives a parsed AST and reports diagnostics. The framework handles parsing, running analyzers, collecting results, and formatting output.
Analyzers are composable and extensible — embedders can define custom checks alongside the built-in set.
Index ¶
- Variables
- func AnalyzerDoc() string
- func AnalyzerNames() []string
- func ArgCount(sexpr *lisp.LVal) int
- func BuildAnalysisConfig(cfg *LintConfig) (*analysis.Config, error)
- func FormatJSON(w io.Writer, diags []Diagnostic) error
- func FormatText(w io.Writer, diags []Diagnostic)
- func HeadSymbol(sexpr *lisp.LVal) string
- func ShouldFail(diags []Diagnostic, threshold Severity) bool
- func SourceOf(v *lisp.LVal) *lisp.LVal
- func UserDefined(exprs []*lisp.LVal) map[string]bool
- func Walk(exprs []*lisp.LVal, fn func(node *lisp.LVal, parent *lisp.LVal, depth int))
- func WalkSExprs(exprs []*lisp.LVal, fn func(sexpr *lisp.LVal, depth int))
- type Analyzer
- type Diagnostic
- type LintConfig
- type Linter
- func (l *Linter) LintFile(source []byte, filename string) ([]Diagnostic, error)
- func (l *Linter) LintFileWithAnalysis(source []byte, filename string, cfg *analysis.Config) ([]Diagnostic, error)
- func (l *Linter) LintFileWithContext(source []byte, filename string, semantics *analysis.Result) ([]Diagnostic, error)
- func (l *Linter) LintFiles(cfg *LintConfig, files []string) ([]Diagnostic, error)
- type Pass
- type Position
- type Severity
Constants ¶
This section is empty.
Variables ¶
var AnalyzerBuiltinArity = &Analyzer{ Name: "builtin-arity", Severity: SeverityError, Doc: "Check argument counts for calls to known builtin functions and special forms.\n\nELPS builtin functions have well-defined argument signatures. This check catches calls with too few or too many arguments before runtime. User-defined functions that shadow builtin names are automatically excluded. Formals lists and threading macro children are also excluded.", Run: func(pass *Pass) error { userDefs := UserDefined(pass.Exprs) skipNodes := aritySkipNodes(pass.Exprs) WalkSExprs(pass.Exprs, func(sexpr *lisp.LVal, depth int) { if skipNodes[sexpr] { return } head := HeadSymbol(sexpr) if head == "" { return } if userDefs[head] { return } spec, ok := builtinArityTable[head] if !ok { return } argc := ArgCount(sexpr) helpNote := fmt.Sprintf("see (help '%s) or `elps doc %s` for usage", head, head) if argc < spec.min { src := SourceOf(sexpr) pass.Report(Diagnostic{ Message: fmt.Sprintf("%s requires at least %d argument(s), got %d", head, spec.min, argc), Pos: posFromSource(src.Source), Notes: []string{helpNote}, }) } if spec.max >= 0 && argc > spec.max { src := SourceOf(sexpr) pass.Report(Diagnostic{ Message: fmt.Sprintf("%s accepts at most %d argument(s), got %d", head, spec.max, argc), Pos: posFromSource(src.Source), Notes: []string{helpNote}, }) } }) return nil }, }
AnalyzerBuiltinArity checks for wrong argument counts to known builtin functions.
var AnalyzerCondMissingElse = &Analyzer{ Name: "cond-missing-else", Severity: SeverityInfo, Doc: "Warn when a cond expression has no default clause.\n\nWithout an else or (true ...) clause, cond returns nil when no condition matches. This is a common source of unexpected nil values. Add (else ...) or (true ...) as the last clause to handle the default case.", Run: func(pass *Pass) error { WalkSExprs(pass.Exprs, func(sexpr *lisp.LVal, depth int) { if HeadSymbol(sexpr) != "cond" { return } if ArgCount(sexpr) == 0 { return } last := sexpr.Cells[len(sexpr.Cells)-1] if last.Type != lisp.LSExpr || len(last.Cells) == 0 { return } head := last.Cells[0] if head.Type == lisp.LSymbol && isCondDefault(head.Str) { return } src := SourceOf(sexpr) pass.Report(Diagnostic{ Message: "cond has no default (else) clause", Pos: posFromSource(src.Source), Notes: []string{"add (else ...) or (true ...) as the last clause to handle unmatched cases"}, }) }) return nil }, }
AnalyzerCondMissingElse warns when a cond has no default (else or true) clause.
var AnalyzerCondStructure = &Analyzer{ Name: "cond-structure", Severity: SeverityError, Doc: "Check for malformed `cond` clauses.\n\nEach `cond` clause must be a non-empty list. The `else` clause, if present, must be last. Common mistakes include bare values instead of lists, or misplaced `else`.", Run: func(pass *Pass) error { WalkSExprs(pass.Exprs, func(sexpr *lisp.LVal, depth int) { if HeadSymbol(sexpr) != "cond" { return } src := SourceOf(sexpr) last := len(sexpr.Cells) - 1 for i := 1; i < len(sexpr.Cells); i++ { clause := sexpr.Cells[i] clauseSrc := SourceOf(clause) if clauseSrc.Source == nil || clauseSrc.Source.Line == 0 { clauseSrc = src } if clause.Type != lisp.LSExpr { pass.Report(Diagnostic{ Message: fmt.Sprintf("cond clause %d is not a list", i), Pos: posFromSource(clauseSrc.Source), Notes: []string{"cond clauses must be lists: (cond ((test1) body1) ((test2) body2) (else default))"}, }) continue } if len(clause.Cells) == 0 { pass.Reportf(clauseSrc.Source, "cond clause %d is empty", i) continue } if clause.Cells[0].Type == lisp.LSymbol && isCondDefault(clause.Cells[0].Str) { if i != last { pass.Reportf(clauseSrc.Source, "cond else clause must be last (is clause %d of %d)", i, last) } } } }) return nil }, }
AnalyzerCondStructure checks for malformed `cond` clauses.
var AnalyzerDefunStructure = &Analyzer{ Name: "defun-structure", Severity: SeverityError, Doc: "Check for malformed `defun`/`defmacro` definitions.\n\nA `defun` requires a symbol name and a formals list. An empty body (no-op) is valid. Common mistakes include non-symbol names or a non-list formals argument.", Run: func(pass *Pass) error { WalkSExprs(pass.Exprs, func(sexpr *lisp.LVal, depth int) { head := HeadSymbol(sexpr) if head != "defun" && head != "defmacro" { return } src := SourceOf(sexpr) argc := ArgCount(sexpr) if argc < 2 { pass.Reportf(src.Source, "%s requires at least a name and formals list (got %d argument(s))", head, argc) return } name := sexpr.Cells[1] if name.Type != lisp.LSymbol { pass.Reportf(src.Source, "%s name must be a symbol, got %s", head, name.Type) } formals := sexpr.Cells[2] if formals.Type != lisp.LSExpr { pass.Reportf(src.Source, "%s formals must be a list, got %s", head, formals.Type) } }) return nil }, }
AnalyzerDefunStructure checks for malformed `defun` and `defmacro` forms.
var AnalyzerIfArity = &Analyzer{ Name: "if-arity", Severity: SeverityError, Doc: "Check that `if` has exactly 3 arguments: condition, then-branch, else-branch.\n\nA missing else branch is a common source of subtle nil-return bugs. Extra arguments are silently ignored at parse time but indicate a structural error.", Run: func(pass *Pass) error { WalkSExprs(pass.Exprs, func(sexpr *lisp.LVal, depth int) { if HeadSymbol(sexpr) != "if" { return } argc := ArgCount(sexpr) if argc == 3 { return } src := SourceOf(sexpr) if argc < 3 { pass.Report(Diagnostic{ Message: fmt.Sprintf("if requires 3 arguments (condition, then, else), got too few (%d)", argc), Pos: posFromSource(src.Source), Notes: []string{"use cond for multi-branch conditionals, or provide an else branch"}, }) } else { pass.Report(Diagnostic{ Message: fmt.Sprintf("if requires 3 arguments (condition, then, else), got too many (%d)", argc), Pos: posFromSource(src.Source), Notes: []string{"if takes exactly (condition then-expr else-expr); use progn to group multiple expressions"}, }) } }) return nil }, }
AnalyzerIfArity checks that `if` has exactly 3 arguments (condition, then, else).
var AnalyzerInPackageToplevel = &Analyzer{ Name: "in-package-toplevel", Severity: SeverityWarning, Doc: "Warn when `in-package` is used inside nested expressions.\n\n`in-package` only has meaningful effect at the top level of a file. Using it inside a `defun`, `let`, `lambda`, or other nested form is almost certainly a mistake.", Run: func(pass *Pass) error { WalkSExprs(pass.Exprs, func(sexpr *lisp.LVal, depth int) { if HeadSymbol(sexpr) == "in-package" && depth > 0 { src := SourceOf(sexpr) pass.Reportf(src.Source, "in-package should only be used at the top level") } }) return nil }, }
AnalyzerInPackageToplevel warns when `in-package` is used inside nested expressions (function bodies, let forms, etc.) where it has no useful effect.
var AnalyzerLetBindings = &Analyzer{ Name: "let-bindings", Severity: SeverityError, Doc: "Check for malformed `let`/`let*` binding lists.\n\nThe first argument to `let` or `let*` must be a list of (symbol value) pairs. Common mistakes include forgetting the outer list: `(let (x 1) ...)` instead of `(let ((x 1)) ...)`.", Run: func(pass *Pass) error { WalkSExprs(pass.Exprs, func(sexpr *lisp.LVal, depth int) { head := HeadSymbol(sexpr) if head != "let" && head != "let*" { return } if ArgCount(sexpr) < 1 { src := SourceOf(sexpr) pass.Reportf(src.Source, "%s requires a binding list and body", head) return } bindings := sexpr.Cells[1] src := SourceOf(sexpr) if bindings.Type != lisp.LSExpr { pass.Reportf(src.Source, "%s bindings must be a list, got %s", head, bindings.Type) return } for i, binding := range bindings.Cells { if binding.Type != lisp.LSExpr { pass.Report(Diagnostic{ Message: fmt.Sprintf("%s binding %d is not a list (did you forget the outer parentheses?)", head, i+1), Pos: posFromSource(bindingSource(binding, src)), Notes: []string{"correct form: (let ((x 1) (y 2)) body...)"}, }) continue } if len(binding.Cells) == 0 { pass.Reportf(bindingSource(binding, src), "%s binding %d is empty", head, i+1) continue } if binding.Cells[0].Type != lisp.LSymbol && HeadSymbol(binding.Cells[0]) != "unquote" { pass.Reportf(bindingSource(binding, src), "%s binding %d: first element must be a symbol, got %s", head, i+1, binding.Cells[0].Type) continue } if len(binding.Cells) != 2 { pass.Reportf(bindingSource(binding, src), "%s binding %d (%s): expected 2 elements (symbol value), got %d", head, i+1, binding.Cells[0].Str, len(binding.Cells)) } } }) return nil }, }
AnalyzerLetBindings checks for malformed `let` and `let*` binding lists.
var AnalyzerQuoteCall = &Analyzer{ Name: "quote-call", Severity: SeverityWarning, Doc: "Warn when set is called with an unquoted symbol argument.\n\nThe first argument to set should be a quoted symbol: (set 'x 42). Writing (set x 42) evaluates x first, which is rarely intended. This check does not flag set!, which takes an unquoted symbol by design.", Run: func(pass *Pass) error { WalkSExprs(pass.Exprs, func(sexpr *lisp.LVal, depth int) { head := HeadSymbol(sexpr) if head != "set" && head != "defconst" { return } if ArgCount(sexpr) < 1 { return } arg := sexpr.Cells[1] if arg.Type == lisp.LSymbol && !arg.Quoted { src := SourceOf(sexpr) pass.Report(Diagnostic{ Message: fmt.Sprintf("%s first argument should be quoted: (set '%s ...) not (set %s ...)", head, arg.Str, arg.Str), Pos: posFromSource(src.Source), Notes: []string{fmt.Sprintf("did you mean (%s '%s ...)?", head, arg.Str)}, }) } }) return nil }, }
AnalyzerQuoteCall warns when set or defconst is called with an unquoted symbol as the first argument, which is almost always a mistake.
var AnalyzerRethrowContext = &Analyzer{ Name: "rethrow-context", Severity: SeverityError, Doc: "Warn when `rethrow` is used outside a `handler-bind` form.\n\n`rethrow` re-raises the current error being handled by handler-bind, preserving the original stack trace. Calling it outside any handler-bind always produces an error at runtime.", Run: func(pass *Pass) error { walkRethrowContext(pass.Exprs, 0, func(sexpr *lisp.LVal) { src := SourceOf(sexpr) pass.Report(Diagnostic{ Message: "rethrow used outside handler-bind", Pos: posFromSource(src.Source), Notes: []string{"rethrow can only be called from within a handler-bind handler"}, }) }) return nil }, }
AnalyzerRethrowContext warns when `rethrow` is used outside of a `handler-bind` form. At runtime, rethrow can only be called from within a handler-bind handler; calling it elsewhere always produces an error.
var AnalyzerSetUsage = &Analyzer{ Name: "set-usage", Severity: SeverityWarning, Doc: "Warn when `set` is used to reassign an already-bound symbol.\n\nThe first `set` creating a new binding is fine — ELPS has no `defvar`, so `set` is the standard way to create top-level bindings. However, subsequent `set` calls on the same symbol should use `set!` to clearly signal mutation intent.", Run: func(pass *Pass) error { seen := make(map[string]bool) WalkSExprs(pass.Exprs, func(sexpr *lisp.LVal, depth int) { if HeadSymbol(sexpr) == "in-package" && depth == 0 { seen = make(map[string]bool) return } if HeadSymbol(sexpr) != "set" { return } if ArgCount(sexpr) < 1 { return } arg := sexpr.Cells[1] name := "" if arg.Type == lisp.LSymbol { name = arg.Str } else if arg.Type == lisp.LSExpr && arg.Quoted && len(arg.Cells) > 0 && arg.Cells[0].Type == lisp.LSymbol { name = arg.Cells[0].Str } if name == "" { return } if seen[name] { src := SourceOf(sexpr) pass.Report(Diagnostic{ Message: fmt.Sprintf("use set! instead of set to mutate '%s (already bound)", name), Pos: posFromSource(src.Source), Notes: []string{"set creates a new binding; set! mutates an existing one"}, }) } seen[name] = true }) return nil }, }
AnalyzerSetUsage warns when `set` is used to reassign a symbol that was already bound by a prior `set` in the same file. The first `set` creating a binding is fine (ELPS has no `defvar`), but subsequent mutations of the same symbol should use `set!` to signal intent.
var AnalyzerShadowing = &Analyzer{ Name: "shadowing", Severity: SeverityInfo, Semantic: true, Doc: "Report when a local binding shadows a name from an enclosing scope.\n\nRequires semantic analysis (--workspace flag). Shadowing is valid but can cause confusion, especially when it hides a builtin or outer variable.", Run: func(pass *Pass) error { if pass.Semantics == nil { return nil } for _, sym := range pass.Semantics.Symbols { if sym.Scope == nil || sym.Scope == pass.Semantics.RootScope { continue } if sym.Scope.Parent == nil { continue } outer := sym.Scope.Parent.Lookup(sym.Name) if outer == nil { continue } if outer.External { continue } pass.Report(Diagnostic{ Message: fmt.Sprintf("%s '%s' shadows %s from enclosing scope", sym.Kind, sym.Name, outer.Kind), Pos: posFromSource(sym.Source), Notes: []string{fmt.Sprintf("rename '%s' to avoid confusion with the outer %s", sym.Name, outer.Kind)}, }) } return nil }, }
AnalyzerShadowing reports when a local binding shadows a name from an enclosing scope. This is informational — shadowing is valid but can cause confusion. Requires semantic analysis (pass.Semantics != nil).
var AnalyzerUndefinedSymbol = &Analyzer{ Name: "undefined-symbol", Severity: SeverityError, Semantic: true, Doc: "Report symbols that cannot be resolved in any enclosing scope.\n\nRequires semantic analysis (--workspace flag). Keywords and qualified symbols are excluded. Builtins, special operators, and macros are pre-populated.", Run: func(pass *Pass) error { if pass.Semantics == nil { return nil } for _, u := range pass.Semantics.Unresolved { pass.Report(Diagnostic{ Message: fmt.Sprintf("undefined symbol: %s", u.Name), Pos: posFromSource(u.Source), Notes: []string{fmt.Sprintf("'%s' is not defined in any enclosing scope; did you mean a different name?", u.Name)}, }) } return nil }, }
AnalyzerUndefinedSymbol reports symbols that could not be resolved in any enclosing scope. Requires semantic analysis (pass.Semantics != nil).
var AnalyzerUnnecessaryProgn = &Analyzer{ Name: "unnecessary-progn", Severity: SeverityInfo, Doc: "Warn when `progn` wraps the body of a form that already supports multiple expressions.\n\nForms like `defun`, `defmacro`, `lambda`, `let`, and others evaluate their body as an implicit progn. Wrapping the body in an explicit `(progn ...)` is redundant. This does not flag `progn` inside `if` branches, where it is needed.", Run: func(pass *Pass) error { WalkSExprs(pass.Exprs, func(sexpr *lisp.LVal, depth int) { head := HeadSymbol(sexpr) bodyStart, ok := implicitPrognForms[head] if !ok { return } bodyExprs := len(sexpr.Cells) - bodyStart if bodyExprs != 1 { return } body := sexpr.Cells[bodyStart] if HeadSymbol(body) != "progn" { return } src := SourceOf(body) var msg string if head == "progn" { msg = "nested progn is redundant" } else { msg = fmt.Sprintf("progn is unnecessary in %s body (it supports multiple expressions)", head) } pass.Report(Diagnostic{ Message: msg, Pos: posFromSource(src.Source), Notes: []string{fmt.Sprintf("remove the progn and move its contents directly into the %s body", head)}, }) }) WalkSExprs(pass.Exprs, func(sexpr *lisp.LVal, depth int) { if HeadSymbol(sexpr) != "cond" { return } for i := 1; i < len(sexpr.Cells); i++ { clause := sexpr.Cells[i] if clause.Type != lisp.LSExpr || len(clause.Cells) < 2 { continue } if len(clause.Cells) == 2 && HeadSymbol(clause.Cells[1]) == "progn" { src := SourceOf(clause.Cells[1]) pass.Report(Diagnostic{ Message: "progn is unnecessary in cond clause body (it supports multiple expressions)", Pos: posFromSource(src.Source), Notes: []string{"remove the progn and move its contents directly into the cond clause"}, }) } } }) return nil }, }
AnalyzerUnnecessaryProgn warns when progn is used as the sole body expression in a form that already supports multiple body expressions.
var AnalyzerUnusedFunction = &Analyzer{ Name: "unused-function", Severity: SeverityWarning, Semantic: true, Doc: "Warn about top-level functions and macros that are defined but never referenced.\n\nRequires semantic analysis (--workspace flag). Exported symbols and functions with underscore prefix are excluded.", Run: func(pass *Pass) error { if pass.Semantics == nil { return nil } for _, sym := range pass.Semantics.Symbols { if sym.References > 0 { continue } if sym.Kind != analysis.SymFunction && sym.Kind != analysis.SymMacro { continue } if sym.Scope != pass.Semantics.RootScope { continue } if sym.Exported { continue } if len(sym.Name) > 0 && sym.Name[0] == '_' { continue } pass.Report(Diagnostic{ Message: fmt.Sprintf("unused %s: %s", sym.Kind, sym.Name), Pos: posFromSource(sym.Source), Notes: []string{"if this is a public API, add it to an (export ...) form"}, }) } return nil }, }
AnalyzerUnusedFunction warns about functions and macros that are defined at the top level but never referenced. Requires semantic analysis.
var AnalyzerUnusedVariable = &Analyzer{ Name: "unused-variable", Severity: SeverityWarning, Semantic: true, Doc: "Warn about variables and parameters that are defined but never referenced.\n\nRequires semantic analysis (--workspace flag). Skips variables with underscore prefix (conventional \"ignored\" marker) and top-level (global scope) variables.", Run: func(pass *Pass) error { if pass.Semantics == nil { return nil } for _, sym := range pass.Semantics.Symbols { if sym.References > 0 { continue } if sym.Kind != analysis.SymVariable && sym.Kind != analysis.SymParameter { continue } if sym.Scope == pass.Semantics.RootScope { continue } if len(sym.Name) > 0 && sym.Name[0] == '_' { continue } pass.Report(Diagnostic{ Message: fmt.Sprintf("unused %s: %s", sym.Kind, sym.Name), Pos: posFromSource(sym.Source), Notes: []string{fmt.Sprintf("if '%s' is intentionally unused, prefix it with '_'", sym.Name)}, }) } return nil }, }
AnalyzerUnusedVariable warns about variables and parameters that are defined but never referenced. Requires semantic analysis (pass.Semantics != nil).
var AnalyzerUserArity = &Analyzer{ Name: "user-arity", Severity: SeverityError, Semantic: true, Doc: "Check argument counts for calls to user-defined functions and macros.\n\nRequires semantic analysis (--workspace flag). Only checks calls to functions with known signatures (Source != nil). Complements builtin-arity which covers builtins.", Run: func(pass *Pass) error { if pass.Semantics == nil { return nil } skipNodes := aritySkipNodes(pass.Exprs) locallyShadowed := make(map[string]bool) for _, sym := range pass.Semantics.Symbols { if sym.Scope == nil || sym.Scope == pass.Semantics.RootScope { continue } rootSym := pass.Semantics.RootScope.LookupLocal(sym.Name) if rootSym != nil && rootSym.Signature != nil && (rootSym.Kind == analysis.SymFunction || rootSym.Kind == analysis.SymMacro) { locallyShadowed[sym.Name] = true } } WalkSExprs(pass.Exprs, func(sexpr *lisp.LVal, depth int) { if skipNodes[sexpr] { return } head := HeadSymbol(sexpr) if head == "" { return } sym := pass.Semantics.RootScope.Lookup(head) if sym == nil || sym.Signature == nil || sym.Source == nil { return } if sym.External { return } if sym.Kind != analysis.SymFunction && sym.Kind != analysis.SymMacro { return } if locallyShadowed[head] { return } argc := ArgCount(sexpr) minArity := sym.Signature.MinArity() maxArity := sym.Signature.MaxArity() if argc < minArity { src := SourceOf(sexpr) pass.Report(Diagnostic{ Message: fmt.Sprintf("%s requires at least %d argument(s), got %d", head, minArity, argc), Pos: posFromSource(src.Source), Notes: []string{fmt.Sprintf("defined at %s", sourceString(sym.Source))}, }) } if maxArity >= 0 && argc > maxArity { src := SourceOf(sexpr) pass.Report(Diagnostic{ Message: fmt.Sprintf("%s accepts at most %d argument(s), got %d", head, maxArity, argc), Pos: posFromSource(src.Source), Notes: []string{fmt.Sprintf("defined at %s", sourceString(sym.Source))}, }) } }) return nil }, }
AnalyzerUserArity checks argument counts for calls to user-defined functions and macros whose signatures are known from the same file. Requires semantic analysis (pass.Semantics != nil).
Functions ¶
func AnalyzerDoc ¶
func AnalyzerDoc() string
AnalyzerDoc returns a formatted documentation string for all analyzers.
func AnalyzerNames ¶
func AnalyzerNames() []string
AnalyzerNames returns a sorted list of all default analyzer names.
func BuildAnalysisConfig ¶ added in v1.17.0
func BuildAnalysisConfig(cfg *LintConfig) (*analysis.Config, error)
BuildAnalysisConfig constructs an analysis.Config from a LintConfig. It scans the workspace, extracts stdlib exports, and merges embedder registry symbols. This is exported for callers (like the CLI stdin path) that need to build the config separately from file linting.
func FormatJSON ¶
func FormatJSON(w io.Writer, diags []Diagnostic) error
FormatJSON writes diagnostics as JSON.
func FormatText ¶
func FormatText(w io.Writer, diags []Diagnostic)
FormatText writes diagnostics in go vet text format.
func HeadSymbol ¶
HeadSymbol returns the symbol name at the head of an s-expression, or "".
func ShouldFail ¶ added in v1.19.0
func ShouldFail(diags []Diagnostic, threshold Severity) bool
ShouldFail returns true if the diagnostics contain at least one finding at or above the given severity threshold.
func SourceOf ¶
SourceOf returns the best source location for a node. Prefers the node's own source, falls back to first child's source.
func UserDefined ¶
UserDefined returns the set of names defined or bound in the source that shadow builtins. This includes:
- Function/macro names from defun/defmacro
- Parameter names from defun/defmacro/lambda formals lists
The result is file-global (not scope-aware), which is conservative: it may suppress a valid finding but will never produce a false positive.
Types ¶
type Analyzer ¶
type Analyzer struct {
// Name is a short identifier for this check (e.g. "set-usage").
Name string
// Doc is a human-readable description. The first line is a short summary.
Doc string
// Severity is the default severity for diagnostics from this analyzer.
Severity Severity
// Semantic indicates that this analyzer requires semantic analysis
// (pass.Semantics != nil) to produce diagnostics. When true and
// semantic analysis is not available, nolint directives targeting
// this analyzer are treated as conditionally valid rather than unused.
Semantic bool
// Run executes the check. It should call pass.Report() for each finding.
Run func(pass *Pass) error
}
Analyzer defines a single lint check.
func DefaultAnalyzers ¶
func DefaultAnalyzers() []*Analyzer
DefaultAnalyzers returns the built-in set of lint checks.
type Diagnostic ¶
type Diagnostic struct {
// Pos is the source location of the problem.
Pos Position `json:"pos"`
// Message is a human-readable description of the problem.
Message string `json:"message"`
// Analyzer is the name of the check that found this problem.
Analyzer string `json:"analyzer"`
// Severity is the severity level of the diagnostic.
Severity Severity `json:"severity"`
// Notes are optional hint text lines for the user.
Notes []string `json:"notes,omitempty"`
}
Diagnostic is a single reported problem.
func (Diagnostic) String ¶
func (d Diagnostic) String() string
String returns the diagnostic in go vet style: file:line: message (analyzer) with optional note lines appended.
type LintConfig ¶ added in v1.17.0
type LintConfig struct {
// Workspace is the root directory for cross-file scanning.
// Empty string disables workspace scanning.
Workspace string
// Registry provides Go-registered symbols (builtins, special ops, macros)
// from an embedder's environment. These are merged with stdlib and
// workspace symbols for semantic analysis. When nil, only stdlib symbols
// are used.
Registry *lisp.PackageRegistry
// Excludes are glob patterns for files to skip during workspace scanning.
Excludes []string
// StdlibExports provides pre-extracted stdlib package exports. When nil,
// LintFiles uses the default stdlib (loaded via lisplib.LoadLibrary).
// Embedders that already have a configured env can pass
// analysis.ExtractPackageExports(env.Runtime.Registry) here to avoid
// the overhead of creating a temporary environment.
StdlibExports map[string][]analysis.ExternalSymbol
}
LintConfig configures the linter for a single run.
type Linter ¶
type Linter struct {
Analyzers []*Analyzer
}
Linter runs a set of analyzers over source files.
func (*Linter) LintFile ¶
func (l *Linter) LintFile(source []byte, filename string) ([]Diagnostic, error)
LintFile analyzes a single source file and returns all diagnostics. Semantic analyzers are no-ops because no analysis.Result is provided.
func (*Linter) LintFileWithAnalysis ¶ added in v1.17.0
func (l *Linter) LintFileWithAnalysis(source []byte, filename string, cfg *analysis.Config) ([]Diagnostic, error)
LintFileWithAnalysis parses, analyzes, and lints a source file in one call. This is a convenience that runs semantic analysis and passes the result to all analyzers.
func (*Linter) LintFileWithContext ¶ added in v1.17.0
func (l *Linter) LintFileWithContext(source []byte, filename string, semantics *analysis.Result) ([]Diagnostic, error)
LintFileWithContext analyzes a source file with optional semantic analysis results. When semantics is nil, semantic analyzers (undefined-symbol, unused-variable, etc.) are no-ops. When non-nil, they use the scope and reference data for deeper checks.
func (*Linter) LintFiles ¶ added in v1.17.0
func (l *Linter) LintFiles(cfg *LintConfig, files []string) ([]Diagnostic, error)
LintFiles analyzes source files with full workspace + embedder context. It handles workspace scanning, stdlib/registry symbol extraction, and semantic analysis configuration. The files slice contains resolved file paths (no glob expansion — callers handle that).
When cfg is nil, all files are linted with syntactic checks only.
type Pass ¶
type Pass struct {
// Analyzer is the currently running check.
Analyzer *Analyzer
// Filename is the source file being analyzed.
Filename string
// Exprs are the top-level parsed expressions.
Exprs []*lisp.LVal
// Semantics holds the result of semantic analysis, if available.
// Nil when semantic analysis has not been run (e.g. no --workspace flag).
// Semantic analyzers should check for nil and return early.
Semantics *analysis.Result
// contains filtered or unexported fields
}
Pass provides context to a running analyzer.
func (*Pass) ReportWithNotes ¶ added in v1.16.12
func (p *Pass) ReportWithNotes(d Diagnostic, notes ...string)
ReportWithNotes records a diagnostic with additional hint text.
type Position ¶
type Position struct {
File string `json:"file"`
Line int `json:"line"`
Col int `json:"col,omitempty"`
}
Position identifies a location in source code.
type Severity ¶ added in v1.17.0
type Severity int
Severity indicates the severity level of a lint diagnostic.
const ( SeverityError Severity SeverityWarning SeverityInfo )
func MaxSeverity ¶ added in v1.19.0
func MaxSeverity(diags []Diagnostic) Severity
MaxSeverity returns the most severe level found in the given diagnostics. Returns SeverityInfo if diags is empty (i.e., least severe / no problems). Severity ordering: SeverityError > SeverityWarning > SeverityInfo.
func ParseSeverity ¶ added in v1.19.0
ParseSeverity converts a string to a Severity value. Valid inputs: "error", "warning", "info".
func (Severity) MarshalJSON ¶ added in v1.17.0
MarshalJSON serializes the severity as a JSON string. An unset severity (zero value) is marshaled as "warning".
func (*Severity) UnmarshalJSON ¶ added in v1.17.0
UnmarshalJSON deserializes a severity from a JSON string.