rubocop

package module
v0.0.0-...-6d1a4cf Latest Latest
Warning

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

Go to latest
Published: Jul 17, 2026 License: BSD-3-Clause Imports: 8 Imported by: 0

README

go-ruby-rubocop/rubocop

rubocop — go-ruby-rubocop

Docs License Go Coverage

A pure-Go (no cgo) reimplementation of the core of Ruby's RuboCop linter — the offense/cop framework, a faithful core cop set, the .rubocop.yml configuration model and the default formatters — built on the go-ruby-parser Ruby AST and lexer. It inspects a Ruby source string (its lines, its token stream and its AST) and emits offenses whose cop name, location and message byte-match those the rubocop gem produces, without any Ruby runtime.

It is the RuboCop backend for go-embedded-ruby, but a standalone, reusable module with no dependency on the Ruby runtime — a sibling of go-ruby-parser, go-ruby-regexp, go-ruby-erb and go-ruby-yaml (whose YAML loader parses the .rubocop.yml config here).

What it is — and isn't. Detecting an offense from a source string — its cop name, its line/column, its message text — is fully deterministic and needs no interpreter, so it lives here as pure Go. The gem's file-walking and its ~500-cop breadth are out of scope: this is a core cop set (22 cops across Layout / Style / Lint / Metrics) implemented faithfully rather than a fake of the whole. Applying autocorrections is left to the host; each cop computes the edit (a source-range Correction) but does not rewrite your file.

Features

  • Cop framework — a Cop interface (Inspect(*Source, CopConfig) []Offense), an Offense{CopName, Location, Message, Severity, Correctable, Correction} model, a Registry, and a Runner (the commissioner) that runs every enabled cop over a lexed+parsed Source and returns offenses in RuboCop's report order.
  • ConfigurationParseConfig reads a .rubocop.yml (via go-ruby-yaml) into a Config: AllCops.DisabledByDefault, per-cop Enabled, and per-cop params (Max, EnforcedStyle, …). A cop's effective config is its built-in default merged with the file's overrides.
  • FormattersSimpleTextFormatter (--format simple) and ProgressFormatter (--format progress), including the byte-faithful summary line (N files inspected, M offenses detected[, K offenses autocorrectable]).
  • A faithful core cop set — detection and message text validated differentially against the rubocop gem (see Cops).

CGO-free, 100% test coverage, gofmt + go vet clean, and green across the six 64-bit Go targets (amd64, arm64, riscv64, loong64, ppc64le, s390x) and three operating systems.

Install

go get github.com/go-ruby-rubocop/rubocop

Usage

package main

import (
	"fmt"

	"github.com/go-ruby-rubocop/rubocop"
)

func main() {
	src := "def foo(a, b)\n  return a + b\nend\n"

	// Parse an optional .rubocop.yml (empty => every cop at its default).
	cfg, _ := rubocop.ParseConfig("")

	// Run the built-in core cop set.
	run := rubocop.NewRunner(rubocop.DefaultRegistry(), cfg)
	offenses := run.Inspect("foo.rb", src)

	// Render like `rubocop --format simple`.
	report := rubocop.SimpleTextFormatter{}.Format([]rubocop.FileResult{
		{Path: "foo.rb", Offenses: offenses},
	})
	fmt.Print(report)
	// == foo.rb ==
	// C:  2:  3: [Correctable] Style/RedundantReturn: Redundant return detected.
	// W:  1: 12: [Correctable] Lint/UnusedMethodArgument: Unused method argument - b. ...
	//
	// 1 file inspected, 2 offenses detected, 2 offenses autocorrectable
}

Cops

Implemented faithfully (detection + message + location validated against the gem):

Department Cops
Layout TrailingWhitespace, TrailingEmptyLines, SpaceAfterComma, EmptyLines, IndentationWidth, LineLength
Style StringLiterals, FrozenStringLiteralComment, MethodDefParentheses, RedundantReturn, Not, IfUnlessModifier, GuardClause, NumericLiterals
Lint UselessAssignment, UnusedMethodArgument, DuplicateMethods, AmbiguousOperator, ShadowingOuterLocalVariable
Metrics MethodLength, LineLength, ClassLength (each with a configurable Max)

Not yet ported. RuboCop bundles ~500 cops; the ~480 beyond the set above are deliberately absent rather than stubbed. Parameterised cops here honour the subset of their gem params they document (Max, Width, MinDigits, EnforcedStyle, MinBodyLength); the double_quotes inversion of Style/StringLiterals and non-always styles of a few cops are recognised (they switch the cop off) but not yet re-implemented.

Oracle

The test suite is a differential oracle against the rubocop gem (version-gated on RUBY_VERSION >= "4.0"): each implemented cop runs on a corpus of Ruby snippets and its offenses (cop name + line/col + message) plus the formatter output are compared byte-for-byte with the gem's. The oracle skips itself where the gem (or a target-arch Ruby) is absent — the deterministic golden vectors alone hold coverage at 100%, so every no-Ruby lane still passes the gate.

License

BSD-3-Clause — see LICENSE. Copyright (c) the go-ruby-rubocop/rubocop authors.

WebAssembly

Being pure Go (CGO=0), this library also compiles to WebAssembly — both GOOS=js GOARCH=wasm (browser / Node.js) and GOOS=wasip1 GOARCH=wasm (WASI). CI builds both targets on every push, alongside the six 64-bit native/qemu arches.

GOOS=js     GOARCH=wasm go build ./...   # browser / Node
GOOS=wasip1 GOARCH=wasm go build ./...   # WASI (wasmtime, wasmer, wasmedge, …)

Documentation

Overview

Package rubocop is a pure-Go (no cgo) reimplementation of the core of Ruby's RuboCop linter — the offense/cop framework, a faithful core cop set, the .rubocop.yml configuration model and the default formatters — built on the go-ruby-parser Ruby AST and lexer.

It is interpreter-independent: cops inspect a Ruby source string (its lines, its token stream and its abstract syntax tree) and emit Offenses whose cop name, location and message byte-match those the `rubocop` gem produces. The file-walking that the gem performs is a thin host seam; this library runs on source strings, which is the shape [go-embedded-ruby] binds into `rbgo`.

It is a sibling of the other go-ruby-* front-end libraries (go-ruby-parser, go-ruby-regexp, go-ruby-erb, go-ruby-yaml) and, like them, hands the host an explicit value model rather than driving a live interpreter.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Config

type Config struct {
	// contains filtered or unexported fields
}

Config is a whole-run configuration: the AllCops defaults plus per-cop overrides parsed from a .rubocop.yml document. A cop's effective CopConfig is its built-in defaults merged with any overrides here (overrides win).

func NewConfig

func NewConfig() *Config

NewConfig returns an empty Config: every cop takes its built-in default (enabled, default params). This is the "no .rubocop.yml present" case.

func ParseConfig

func ParseConfig(src string) (*Config, error)

ParseConfig parses a .rubocop.yml document into a Config. It understands the subset RuboCop configs use in practice: an AllCops section (DisabledByDefault) and per-cop sections carrying Enabled plus arbitrary scalar/param keys. Unknown keys are preserved as params so parameterised cops can read them. A YAML syntax error is returned; an empty document yields the empty (all-default) Config.

func (*Config) For

func (c *Config) For(name string, def CopConfig) CopConfig

For resolves the effective CopConfig for cop name, merging def (the cop's built-in defaults) with any override from the parsed config. Enabled follows RuboCop's precedence: an explicit per-cop Enabled always wins; otherwise the cop is on unless AllCops.DisabledByDefault turned everything off. Params from the override are layered over the defaults key-by-key.

type Cop

type Cop interface {
	// Name is the department-qualified cop name, e.g. "Style/StringLiterals".
	Name() string
	// Inspect returns the offenses this cop finds in src, in source order.
	Inspect(src *Source, cfg CopConfig) []Offense
}

Cop is a single lint rule. Inspect examines a Source and returns the offenses it finds, in source order. It receives its resolved per-cop configuration (the merged params from .rubocop.yml plus the cop's own defaults) so parameterised cops (Metrics/*, Layout/LineLength, …) read their Max and friends from it.

This mirrors RuboCop's cop protocol: the gem's cops implement on_<node> callbacks the commissioner dispatches while walking the AST, but the contract that matters to a consumer is the same — given a source, yield offenses. A cop here walks whichever view (lines, tokens, AST) it needs itself; the Runner is the commissioner that invokes each enabled cop once per source.

type CopConfig

type CopConfig struct {
	Enabled bool
	Params  map[string]any
}

CopConfig is the resolved configuration for a single cop: whether it is enabled and its parameter map (Max, EnforcedStyle, …). Params values are the Go shapes go-ruby-yaml decodes YAML scalars to (int, float64, bool, string, []any).

func (CopConfig) Bool

func (c CopConfig) Bool(key string, def bool) bool

Bool returns the boolean parameter key, or def when absent / not a bool.

func (CopConfig) Int

func (c CopConfig) Int(key string, def int) int

Int returns the integer parameter key, or def when it is absent or not an integer. RuboCop's numeric params (Max, …) are plain integers.

func (CopConfig) Str

func (c CopConfig) Str(key, def string) string

Str returns the string parameter key, or def when absent / not a string.

type Correction

type Correction struct {
	Begin       int
	End         int
	Replacement string
}

Correction is a single source edit an autocorrecting cop proposes: replace the half-open byte range [Begin, End) of the original source with Replacement. Applying corrections is the host's job (see Runner.Autocorrect for a reference applier); the cop only computes them.

type FileResult

type FileResult struct {
	Path     string
	Offenses []Offense
}

FileResult pairs a file path with the offenses found in it (in report order).

type Formatter

type Formatter interface {
	// Format renders the offenses for the files in results (in the given order)
	// plus the trailing summary, returning the complete report text.
	Format(results []FileResult) string
}

Formatter renders a run's offenses to text. The two built-ins reproduce RuboCop's `--format simple` (SimpleTextFormatter) and `--format progress` (ProgressFormatter) output byte-for-byte, including the summary line.

type Location

type Location struct {
	Line   int
	Column int
	Length int
}

Location is a 1-based line/column span within the inspected source, exactly as RuboCop reports it: Line and Column point at the first offending character and Length is the number of columns the caret underline spans in the clang-style formatter (0 when the cop does not define a precise span).

type Offense

type Offense struct {
	CopName     string
	Location    Location
	Message     string
	Severity    Severity
	Correctable bool
	// Correction, when non-nil, is the autocorrection the cop would apply. It is
	// advisory: this library computes the edit but leaves applying it to the host.
	Correction *Correction
}

Offense is a single reported violation. CopName is the department-qualified cop name (e.g. "Layout/TrailingWhitespace"); Message is the human-readable text the cop emits (byte-identical to the gem's); Severity is its level; Correctable records whether the cop offers an autocorrection (the "[Correctable]" marker).

func (Offense) String

func (o Offense) String() string

String renders an offense in RuboCop's canonical one-line form used by the clang / progress formatters: "line:col: S: [Correctable] Cop: Message".

type ProgressFormatter

type ProgressFormatter struct{}

ProgressFormatter reproduces `rubocop --format progress`: a run of per-file status dots/letters (`.` clean, or the offenses' severity codes), a blank line, an "Offenses:" listing in clang style, a blank line, then the summary.

func (ProgressFormatter) Format

func (ProgressFormatter) Format(results []FileResult) string

Format renders results in the progress style.

type Registry

type Registry struct {
	// contains filtered or unexported fields
}

Registry is an ordered, name-indexed set of cops. The zero Registry is not usable; construct one with NewRegistry (empty) or DefaultRegistry (the built-in core cop set). Registration is idempotent-by-replacement: registering a cop whose name already exists replaces it, so a host can override a built-in.

func DefaultRegistry

func DefaultRegistry() *Registry

DefaultRegistry returns a Registry populated with the built-in core cop set.

func NewRegistry

func NewRegistry() *Registry

NewRegistry returns an empty Registry.

func (*Registry) Cops

func (r *Registry) Cops() []Cop

Cops returns the registered cops in Names() order.

func (*Registry) Get

func (r *Registry) Get(name string) (Cop, bool)

Get returns the cop registered under name and whether it was present.

func (*Registry) Names

func (r *Registry) Names() []string

Names returns the registered cop names sorted the way RuboCop orders them for output: by department then cop name (i.e. plain lexicographic on the qualified name), which is the order the formatters group offenses within a line.

func (*Registry) Register

func (r *Registry) Register(cops ...Cop) *Registry

Register adds (or replaces) a cop. It returns the registry for chaining.

type Runner

type Runner struct {
	// contains filtered or unexported fields
}

Runner drives a registry of cops over a source with a configuration — the counterpart of RuboCop's Commissioner + team. Construct one with NewRunner; Inspect and InspectSource return the offenses in RuboCop's report order.

func NewRunner

func NewRunner(reg *Registry, cfg *Config) *Runner

NewRunner returns a Runner over reg and cfg. A nil registry defaults to the built-in core cop set (DefaultRegistry); a nil config to the all-defaults Config (NewConfig).

func (*Runner) Autocorrect

func (r *Runner) Autocorrect(path, source string) string

Autocorrect applies, to source, the corrections of every offense that carries one, returning the rewritten source. Non-overlapping corrections are applied right-to-left so earlier byte offsets stay valid; overlapping corrections keep the first (lowest-offset) and skip the rest, mirroring how RuboCop's corrector refuses to clobber an already-edited range in a single pass. It is a reference applier for hosts that want the whole edit in one call.

func (*Runner) Inspect

func (r *Runner) Inspect(path, source string) []Offense

Inspect lexes/parses source (named path for file-naming cops) and runs every enabled cop, returning the offenses sorted the way RuboCop reports them.

func (*Runner) InspectSource

func (r *Runner) InspectSource(src *Source) []Offense

InspectSource runs every enabled cop over an already-built Source. Splitting this out lets a host lex/parse once and reuse the Source.

type Severity

type Severity int

Severity is an offense severity level, matching RuboCop's five-level scale. Its single-letter code is what the formatters print (I/R/C/W/E/F).

const (
	Convention Severity = iota // C — a style/convention offense (the common case)
	Warning                    // W — a Lint warning (a probable mistake)
	Error                      // E — a definite problem
	Fatal                      // F — an internal/parse failure
	Info                       // I — informational only
	Refactor                   // R — a refactoring suggestion
)

The severity levels, in ascending order of seriousness. The zero value is Convention, RuboCop's default for most Style/Layout/Metrics cops.

type SimpleTextFormatter

type SimpleTextFormatter struct{}

SimpleTextFormatter reproduces `rubocop --format simple`: for each file with offenses a "== path ==" header followed by one aligned line per offense, then a blank line and the summary.

func (SimpleTextFormatter) Format

func (SimpleTextFormatter) Format(results []FileResult) string

Format renders results in the simple text style.

type Source

type Source struct {
	// Path is the host-supplied filename used in messages that name the file
	// (e.g. Lint/DuplicateMethods); "" when inspecting an anonymous string.
	Path string
	// Raw is the original source, verbatim.
	Raw string
	// Lines are Raw split on "\n" with the newlines removed; Lines[0] is line 1.
	// A trailing newline does not produce a final empty element, matching how
	// RuboCop counts physical lines.
	Lines []string

	// Tokens is the full lexer output including the terminating EOF token.
	Tokens []token.Token
	// AST is the parsed program, or nil when the source failed to parse.
	AST *ast.Program
	// ParseErr is the parse error, or nil.
	ParseErr error
	// contains filtered or unexported fields
}

Source is the processed view of one Ruby source string handed to every cop: the raw bytes, the lines split for the line-oriented Layout cops, the lexed token stream (each token carrying its 1-based Line/Col and MRI's SpaceBefore flag), and the parsed AST (nil if the source does not parse). RuboCop cops likewise mix line, token and AST views; go-ruby-parser's AST nodes carry no positions, so token positions are how the AST cops locate their offenses.

func NewSource

func NewSource(path, src string) *Source

NewSource lexes and parses src, building the shared Source view. It never fails: a parse error is recorded in ParseErr / leaves AST nil so line- and token-oriented cops still run on unparseable input, exactly as RuboCop keeps its Layout cops working when the AST is unavailable.

Jump to

Keyboard shortcuts

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