goxlint

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Oct 28, 2026 License: MIT Imports: 8 Imported by: 0

README

The goxlint mascot, a blue oxpecker that cleans itself with a lint roller

goxlint

CI Go Reference

goxlint is an engine for building your own Go linter. You write the rules that only your codebase has, in Go, against an API shaped like oxlint's and ESLint's. goxlint does the rest: loading, the AST walk, configuration, fixes, suppressions, output, and a test harness.

The rules a team cares about most are usually its own: which package may read the clock, which logger handlers use, who may call charge(). They live in review comments and in AGENTS.md, and people and coding agents follow them until they don't. A linter doesn't forget, and it says the same thing every time.

A rule

var NoWallClock = &goxlint.Rule{
	Meta: goxlint.Meta{
		Name:       "no-wall-clock",
		Category:   goxlint.Restriction,
		NeedsTypes: true,
		Messages:   map[string]string{"now": "`time.Now` reads the wall clock. Tests cannot control it."},
	},
	Create: func(ctx *goxlint.Context) goxlint.Visitor {
		if ctx.Pkg.Name() == "clock" {
			return nil
		}
		return goxlint.Visit(goxlint.On(func(call *ast.CallExpr) {
			if ctx.IsPkgFunc(call, "time", "Now") {
				ctx.Report(goxlint.Diagnostic{Node: call, MessageID: "now", Help: "Take a clock.Clock and call its Now method."})
			}
		}))
	},
}

Your linter is a Go program that lists its plugins:

func main() { cli.Main(goxlint.Plugin{Name: "acme", Rules: []*goxlint.Rule{NoWallClock}}) }
  × acme(no-wall-clock): `time.Now` reads the wall clock. Tests cannot control it.
   ╭─[billing/invoice.go:14:39]
13 │ func NewInvoice(total int) Invoice {
14 │     return Invoice{Total: total, Issued: time.Now()}
   ·                                          ──────────
15 │ }
   ╰────
  help: Take a clock.Clock and call its Now method.

Found 0 warnings and 1 error.

To see it on your own code, run the example linter:

go run github.com/egoisutolabs/goxlint/examples/goxlint@latest init
go run github.com/egoisutolabs/goxlint/examples/goxlint@latest ./...

Then write your first rule with Getting started.

What goxlint does well

One walk for every rule. Rules don't traverse the AST. They register handlers for the node types they care about, and the engine walks each file once and dispatches by type. Handlers run only for the node types they asked for, and a file with no handlers is not walked at all. Adding a rule adds handlers, not traversals.

Fixes you can run without reading every diff. Each fix declares how safe it is: safe, suggestion, or dangerous, and each level has its own flag. goxlint applies fixes in passes, so one fix can build on another, up to ten passes. An edit that overlaps an earlier one waits for the next pass. Every batch is parsed before the file is written, so a broken fix is rejected, not saved. A file that was gofmt-clean stays gofmt-clean, and a file that was not is left with only the rule's edits. A package that did not load cleanly gets no fixes at all.

Suppressions that stay honest. //goxlint:ignore <rule> <reason> needs a reason. goxlint reports a suppression that no longer suppresses anything, or that repeats an earlier one. A suppression for a rule that is turned off in this run is kept, so an exception for an opt-in rule survives the runs that skip it.

Failures stay where they happen. A rule that panics, or reports a position outside its file, becomes an internal-error diagnostic for that rule, and the other rules keep reporting. A package that fails to load marks its own files, and the files of packages that import it, as incomplete. Everything else gets normal results.

Rules that see the whole package. AnalyzePackage gets every file of a package at once, for problems that cross files. A file where the rule is turned off still counts as evidence. A diagnostic can point at related code in another file, and each related position is checked against the exact files that were loaded for this run.

Positions you can trust. goxlint reports physical positions and ignores //line directives. SARIF output converts Go's byte columns to Unicode code points, as the format requires.

Rules are code, so they get tests. goxlinttest works like ESLint's RuleTester: inline samples that must stay quiet and samples that must report. Every sample is type-checked, so a case that does not compile fails instead of passing by accident. RunPackage tests cross-file rules, including their related locations.

It runs where the code is.

  • Output for people (code frames) and for tools: unix, json, github annotations, and sarif for code scanning.
  • --stdin-filename lints an unsaved buffer. In typed mode the buffer replaces the file on disk through a go/packages overlay. With a fix flag, it writes the fixed source to stdout. This is the hook for editors and coding agents.
  • The go/analysis adapter runs the same rules under go vet -vettool, multichecker, and golangci-lint module plugins.

goxlint depends only on golang.org/x/tools, and it lints itself in CI on Linux, macOS, and Windows.

When not to use goxlint

  • You want a large set of ready-made checks. goxlint ships no rule set. The plugins in examples/goxlint show the API, and they are not a product. For general Go checks, staticcheck is excellent.
  • You need data-flow analysis. Rules see the syntax tree and go/types, not how values move through a function. A rule like "every path closes the rows" is hard to write today (#34).
  • You want results in your editor as you type. There is no language server yet (#15). The pieces it needs, such as stdin overlays and related locations, exist.
  • You lint a very large repository on every keystroke. There is no result cache. Each run loads and lints everything again, and in typed mode most of the time goes to loading packages. We have no benchmarks yet (#25), so we make no speed claims.
  • You need a stable API today. goxlint is v0.x. The rule API can still change before v1.0.0, and each release says what changed.

Status

The next release is v0.1.0. goxlint needs Go 1.26 or later. Before v0.0.2 the module was named goose-lint.

Documentation · Discussions · Contributing · Security · MIT License

Documentation

Overview

Package goxlint is the rule-authoring API for goxlint.

A rule declares metadata and a Create function, an AnalyzePackage function, or both. Create runs once per file and returns a Visitor: a set of node handlers that the engine calls during a single shared AST walk. Rules report problems through the Context.

var NoFoo = &goxlint.Rule{
	Meta: goxlint.Meta{
		Name:     "no-foo",
		Category: goxlint.Suspicious,
		Messages: map[string]string{"foo": "Avoid `{{name}}`."},
	},
	Create: func(ctx *goxlint.Context) goxlint.Visitor {
		return goxlint.Visit(
			goxlint.On(func(n *ast.Ident) {
				if n.Name == "foo" {
					ctx.Report(goxlint.Diagnostic{Node: n, MessageID: "foo", Data: goxlint.Data{"name": n.Name}})
				}
			}),
		)
	},
}

Index

Constants

This section is empty.

Variables

Categories lists every category in display order.

Functions

func Interpolate

func Interpolate(tmpl string, data Data) string

Interpolate replaces {{key}} placeholders in tmpl with values from data.

Types

type Category

type Category string

Category groups rules by intent, mirroring oxlint's categories.

const (
	// Correctness rules catch code that is wrong or useless. Enabled by default.
	Correctness Category = "correctness"
	// Suspicious rules catch code that is most likely wrong.
	Suspicious Category = "suspicious"
	// Pedantic rules are strict and may have false positives.
	Pedantic Category = "pedantic"
	// Style rules enforce idiomatic or readable code.
	Style Category = "style"
	// Restriction rules ban patterns that are valid but unwanted by policy.
	Restriction Category = "restriction"
	// Nursery rules are under development.
	Nursery Category = "nursery"
)

type Context

type Context struct {
	// Enabled is false for package-analysis evidence files where the rule is
	// disabled. Reports from those contexts are ignored by the engine.
	Enabled  bool
	Rule     *Rule
	RuleID   string // "<plugin>/<rule>"
	Fset     *token.FileSet
	File     *ast.File
	Filename string
	Source   []byte
	// Pkg and TypesInfo are nil unless the rule's Meta.NeedsTypes is set.
	Pkg       *types.Package
	TypesInfo *types.Info
	// contains filtered or unexported fields
}

Context is a rule's view of the file being linted.

func NewContext

func NewContext(rule *Rule, ruleID string, fset *token.FileSet, file *ast.File, filename string, src []byte,
	pkg *types.Package, info *types.Info, options json.RawMessage, stack *[]ast.Node, report func(Diagnostic)) *Context

NewContext is used by the engine. Rules receive a ready Context from Create.

func (*Context) Ancestors

func (c *Context) Ancestors() []ast.Node

Ancestors returns the ancestors of the current node, nearest first.

func (*Context) CalleeFunc

func (c *Context) CalleeFunc(call *ast.CallExpr) *types.Func

CalleeFunc returns the function or method a call statically invokes, or nil.

func (*Context) CommentsBefore

func (c *Context) CommentsBefore(pos token.Pos) *ast.CommentGroup

CommentsBefore returns the comment group that ends on the line directly above pos, or nil. A blank line between the comment and pos detaches it.

func (*Context) IsPkgFunc

func (c *Context) IsPkgFunc(call *ast.CallExpr, pkgPath string, names ...string) bool

IsPkgFunc reports whether call invokes the package-level function pkgPath.name.

func (*Context) IsTestFile

func (c *Context) IsTestFile() bool

IsTestFile reports whether the file is a _test.go file.

func (*Context) Line

func (c *Context) Line(pos token.Pos) int

Line returns the 1-based line of pos.

func (*Context) ObjectOf

func (c *Context) ObjectOf(id *ast.Ident) types.Object

ObjectOf returns the object id denotes, or nil without type information.

func (*Context) Options

func (c *Context) Options(v any) bool

Options decodes the rule's configured options into v. It leaves v unchanged when no options are configured, so callers set defaults before calling. Invalid options are reported as an error diagnostic and Options returns false; the rule should then return a nil Visitor.

func (*Context) OwnLine

func (c *Context) OwnLine(pos token.Pos) bool

OwnLine reports whether only whitespace precedes pos on its line.

func (*Context) Parent

func (c *Context) Parent() ast.Node

Parent returns the parent of the node currently being visited, or nil at the root.

func (*Context) Report

func (c *Context) Report(d Diagnostic)

Report records a diagnostic. It panics when the rule itself is broken (an unknown message ID, or a fix more dangerous than Meta.Fixable); the engine recovers and reports the rule as failed.

func (*Context) Snippet

func (c *Context) Snippet(node ast.Node) string

Snippet returns node's source collapsed to one line and shortened, for quoting code in messages.

func (*Context) Text

func (c *Context) Text(node ast.Node) string

Text returns the source text of node.

func (*Context) TrailingComment

func (c *Context) TrailingComment(pos token.Pos) *ast.CommentGroup

TrailingComment returns the comment group that starts on the same line as pos, after it, or nil.

func (*Context) TypeOf

func (c *Context) TypeOf(expr ast.Expr) types.Type

TypeOf returns the type of expr, or nil without type information.

type Data

type Data map[string]string

Data supplies values for {{key}} placeholders in message templates.

type Diagnostic

type Diagnostic struct {
	// Node locates the problem. Pos and End may be set instead.
	Node     ast.Node
	Pos, End token.Pos
	// MessageID selects a template from Meta.Messages. Message may be set instead.
	MessageID string
	Message   string
	Data      Data
	// Help is an optional hint printed under the diagnostic.
	Help string
	// Related contains supporting locations in loaded files of the same package.
	// They do not change the primary location used for suppressions and fixes.
	Related []Related
	// Fix is an optional fix. Its kind must not exceed Meta.Fixable.
	Fix *Fix
}

Diagnostic is a problem reported by a rule.

type Edit

type Edit struct {
	Pos, End token.Pos
	NewText  string
}

Edit replaces the source between Pos and End with NewText.

func InsertBefore

func InsertBefore(pos token.Pos, text string) Edit

InsertBefore returns an edit that inserts text at pos.

func ReplaceNode

func ReplaceNode(node ast.Node, text string) Edit

ReplaceNode returns an edit that replaces node with text.

type Fix

type Fix struct {
	Kind    FixKind
	Message string
	Edits   []Edit
}

Fix is a set of non-overlapping text edits.

type FixKind

type FixKind int

FixKind describes how safe a rule's fixes are to apply automatically.

const (
	// FixNone means the rule provides no fixes.
	FixNone FixKind = iota
	// FixSafe fixes never change program behavior. Applied by --fix.
	FixSafe
	// FixSuggestion fixes may change behavior and need review. Applied by --fix-suggestions.
	FixSuggestion
	// FixDangerous fixes can break code. Applied by --fix-dangerously.
	FixDangerous
)

func (FixKind) String

func (k FixKind) String() string

type Handler

type Handler struct {
	NodeType reflect.Type
	Exit     bool
	Fn       func(ast.Node)
}

Handler is a callback for one AST node type, on entry or exit.

func On

func On[N ast.Node](fn func(N)) Handler

On registers fn for every node of type N, called before the node's children.

func OnExit

func OnExit[N ast.Node](fn func(N)) Handler

OnExit registers fn for every node of type N, called after the node's children.

type Meta

type Meta struct {
	// Name is the rule name without the plugin prefix, for example "no-any-parameters".
	Name string
	// Description is a one-line summary shown by `goxlint rules`.
	Description string
	Category    Category
	// Messages maps message IDs to templates. Templates interpolate Data with {{key}}.
	Messages map[string]string
	// Fixable is the most dangerous kind of fix the rule can produce.
	Fixable FixKind
	// NeedsTypes requests type information. When no enabled rule needs types,
	// goxlint parses files directly without loading packages, which is much faster.
	NeedsTypes bool
}

Meta describes a rule.

type PackageContext

type PackageContext struct {
	Name  string
	Files []*Context
}

PackageContext provides the loaded files in one directory and Go package. Files are ordered by filename, include files where this rule is disabled, and exclude ignored and generated files. A partial selection (such as an explicit file pattern in syntax mode) includes only the loaded files. Treat syntax trees and type information as read-only.

type Plugin

type Plugin struct {
	Name  string
	Rules []*Rule
}

Plugin is a named set of rules. Rules are configured as "<plugin>/<rule>".

type Related struct {
	Node     ast.Node
	Pos, End token.Pos
	Message  string
}

Related is a supporting location for a diagnostic. Positions use the reporting context's FileSet. Node takes precedence over Pos and End. An omitted End denotes a point at Pos. Message labels the location.

type Rule

type Rule struct {
	Meta Meta
	// Create runs once per file and returns the handlers to call during the walk.
	// It may be nil for a package-only rule.
	Create func(ctx *Context) Visitor
	// AnalyzePackage runs once per package after file visitors, before suppressions.
	// A rule may provide either or both callbacks. Package state must be local
	// to this invocation: different packages can be analyzed concurrently.
	AnalyzePackage func(ctx *PackageContext)
}

Rule is a lint rule.

type Visitor

type Visitor []Handler

Visitor is the set of node handlers a rule registers for one file.

func Visit

func Visit(handlers ...Handler) Visitor

Visit collects handlers into a Visitor.

Directories

Path Synopsis
Package cli implements the goxlint command.
Package cli implements the goxlint command.
examples
goxlint command
Command goxlint demonstrates a linter built from the goxlint engine and example plugins.
Command goxlint demonstrates a linter built from the goxlint engine and example plugins.
goxlint/plugins/antislop
Package antislop rejects low-evidence and low-signal Go patterns.
Package antislop rejects low-evidence and low-signal Go patterns.
goxlint/plugins/maintainability
Package maintainability provides configurable complexity and duplication checks.
Package maintainability provides configurable complexity and duplication checks.
Package goxlintanalysis adapts goxlint rules to go/analysis drivers.
Package goxlintanalysis adapts goxlint rules to go/analysis drivers.
Package goxlinttest is the RuleTester for goxlint rules.
Package goxlinttest is the RuleTester for goxlint rules.
internal
config
Package config loads .goxlintrc.json and resolves which rules apply to each file.
Package config loads .goxlintrc.json and resolves which rules apply to each file.
engine
Package engine runs goxlint rules over parsed files.
Package engine runs goxlint rules over parsed files.
output
Package output formats diagnostics.
Package output formats diagnostics.

Jump to

Keyboard shortcuts

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