go-ruleguard

repository·master·Indexed 21 days ago

https://github.com/quasilyte/go-ruleguard

An analysis-based Go linter that enables users to write and run custom linting rules dynamically using a Go-based Domain Specific Language (DSL). It utilizes gogrep-based rules to match Go code and comments, providing capabilities for filtering matches via a DSL API, reporting warnings, and applying automated quickfixes using the -fix CLI flag.

Tokens
6.9K
Snippets
24
Records
33
Agent score
74%

What's inside go-ruleguard

  1. Use filters to refine rule matches

    master

    Filters are used within a Where() call to reduce false positives. A rule matches only if an AST pattern is found and all filters in Where() return true.

    Submatch Filters

    Access named pattern variables using m["varname"]. Common filters include:

    • m["a"].Type.Is("T"): Submatch type is identical to T.
    • m["a"].Type.AssignableTo("T"): Submatch type is assignable to T.
    • m["a"].Type.Implements("I"): Submatch type implements interface I.
    • m["a"].Const: Checks if the submatch is a constant expression.
    • m["a"].Text.Matches("regexp"): Submatch text matches a regex.

    Context Filters

    Apply filters based on the file context using the m object:

    • m.File().Imports("package/path"): Checks if the current file imports a specific package.

    Note: When using MatchComment, submatch variables have the type *ast.Comment.

    // Example: $a must be int, and $b must NOT be assignable to []string
    Where(m["a"].Type.Is(`int`) && !m["b"].Type.AssignableTo(`[]string`))
    
    // Example: File-level filter
    Where(m.File().Imports("io/ioutil"))
  2. Handle types and imports in rules

    master

    When using type filters (e.g., .Is("Type")), ruleguard needs to resolve type names. You have two options:

    1. Use m.Import("package/path"): This allows you to use qualified names (e.g., bar.Baz).
    2. Use fully-qualified names: Use the full path directly in the string (e.g., foo/bar.Baz).

    Note on Standard Library: Standard library packages are pre-loaded. However, if there are collisions (like text/template vs html/template), you can override the default by calling m.Import("html/template") explicitly.

    To match the underlying type instead of the named type, use the m["var"].Type.Underlying() method.

    // Option 1: Explicit Import
    func qualifiedName(m dsl.Matcher) {
    	m.Import(`foo/bar`)
    	m.Match(`f($x)`).Where(m["x"].Type.Is(`bar.Baz`)).Report("$x is bar.Baz")
    }
    
    // Option 2: Fully Qualified Name
    func fullyQualifiedName(m dsl.Matcher) {
    	m.Match(`f($x)`).Where(m["x"].Type.Is(`foo/bar.Baz`)).Report("$x is bar.Baz")
    }
  3. Ruleguard DSL file structure

    master

    Ruleguard configuration files are written in Go-compatible syntax but are parsed into an internal representation rather than compiled by go build. A valid ruleguard file must follow this structure:

    1. Package clause: Must use package gorules.
    2. Import clause: Must include the github.com/quasilyte/go-ruleguard/dsl package.
    3. Function declarations: Can include Matcher functions (rule groups), Custom filter functions, or an init() function.

    Note: The Go code in these files is never executed by go run or go build; it is interpreted by the ruleguard engine.

    package gorules
    
    import "github.com/quasilyte/go-ruleguard/dsl"
    
    func myRule(m dsl.Matcher) {
        // rules go here
    }
  4. Enable quickfixes with Suggest()

    master

    The Suggest(pattern) method enables support for the -fix flag in the ruleguard CLI. When invoked with -fix, the matched code chunk is automatically replaced by the pattern provided in Suggest.

    • If you use Suggest() without Report(), the suggested string is used as the basis for the report message.
    • You can use both Report() and Suggest() to provide a detailed warning while still enabling automated fixes.

    Warning for MatchComment: Since regex might match only a subset of a comment, Suggest() will only replace that specific portion. To replace an entire comment, ensure your pattern uses ^ and $ anchors.

    // Simple suggestion
    m.Match(`!!$x`).Suggest(`$x`)
    
    // Suggestion with custom report
    m.Match(`!!$x`).Suggest(`$x`).Report(`suggested: $x`)
  5. Define rules using Matcher functions

    master

    A matcher function defines a group of rules. It accepts exactly one argument of type dsl.Matcher.

    Each rule within the function is defined by:

    1. Matching:
      • Match(pattern...): Uses gogrep AST patterns to match Go code.
      • MatchComment(regex...): Uses regular expressions to match comments.
    2. Filtering: Where(filter...) to refine matches (see Filters).
    3. Action:
      • Report(message): Prints a warning message. Supports $<varname> for submatch interpolation and $$ for the entire match.
      • Suggest(pattern): Provides a quickfix (syntax rewrite) for use with the -fix flag.

    You can use both Report() and Suggest() together.

    func regexpMust(m dsl.Matcher) {
    	// Matches regexp.Compile($pat) where $pat is a constant
    	m.Match(`regexp.Compile($pat)`, `regexp.CompilePOSIX($pat)`).
    		Where(m["pat"].Const).
    		Report(`can use MustCompile for const patterns`).
    		Suggest(`regexp.MustCompile($pat)`)
    }
  6. How ruleguard rules work

    master

    Ruleguard works by parsing rule files (e.g., rules.go) written using the dsl API.

    1. Rule Groups: A rule file contains functions that serve as rule groups. Each function accepts a single dsl.Matcher argument.
    2. Lifecycle: A rule definition typically starts with a .Match(patterns...) call and ends with a .Report(message) call.
    3. Filtering: You can insert .Where(cond) calls between Match and Report to apply constraints. A match will only trigger a report if it satisfies the Where() condition.
    4. Suggestions: You can use .Suggest(replacement) to provide quickfix actions that can be applied using the -fix CLI flag.
    // Example rule structure
    func myRule(m dsl.Matcher) {
    	m.Match(`pattern`).
    		Where(condition).
    		Report(`message`)
    }
  7. Write custom filter functions

    master

    If built-in DSL filters are insufficient, you can write custom filter functions. These functions are byte-compiled and interpreted.

    A custom filter must accept a *dsl.VarFilterContext and return a bool.

    Example: A filter to check if a type implements fmt.Stringer:

    func implementsStringer(ctx *dsl.VarFilterContext) bool {
    	stringer := ctx.GetInterface(`fmt.Stringer`)
    	return types.Implements(ctx.Type, stringer) ||
    		types.Implements(types.NewPointer(ctx.Type), stringer)
    }
    
    func myRule(m dsl.Matcher) {
    	m.Match(`$x{$*_}`).
    		Where(m["x"].Filter(implementsStringer)).
    		Report("$x implements stringer")
    }
    func implementsStringer(ctx *dsl.VarFilterContext) bool {
    	stringer := ctx.GetInterface(`fmt.Stringer`)
    	return types.Implements(ctx.Type, stringer) ||
    		types.Implements(types.NewPointer(ctx.Type), stringer)
    }
    
    func stringerLiteral(m dsl.Matcher) {
    	m.Match(`$x{$*_}`).
    		Where(m["x"].Filter(implementsStringer)).
    		Report("$x implements stringer")
    }
  8. Document rules with pragmas

    master

    You can add structured documentation to matcher functions using special comment pragmas. This helps categorize and explain rules.

    Supported pragmas:

    • //doc:summary: A short one-sentence description.
    • //doc:before: A code snippet that violates the rule.
    • //doc:after: A code snippet that complies with the rule.
    • //doc:tags: A space-separated list of custom tags (e.g., diagnostic experimental).
    • //doc:note: Extra information, such as links to issues or documentation.
    //doc:summary reports always false/true conditions
    //doc:before  strings.Count(s, "/") >= 0
    //doc:after   strings.Count(s, "/") > 0
    //doc:tags    diagnostic experimental
    func badCond(m dsl.Matcher) {
    	m.Match(`strings.Count($_, $_) >= 0`).Report(`statement always true`)
    }
  9. Create a Ruleguard bundle

    master

    To distribute your own rules as a bundle, create a separate Go module and define a Bundle variable exported from the package. This variable must be of type dsl.Bundle.

    A single Go module can contain multiple ruleguard files, but only one file should define the Bundle object. When the package is imported, all files in that module are exported.

    // Bundle holds the rules package metadata.
    // In order to be importable from other gorules package,
    // a package must define a Bundle variable.
    var Bundle = dsl.Bundle{}
  10. Use Ruleguard bundles for external rules

    master

    Instead of copying external ruleguard files into your repository, you can import them as bundles. This allows for easier versioning via go.mod and prevents name collisions with your own rules.

    To use a bundle, you must:

    1. Install the bundle package using go get.
    2. Import the package in your ruleguard file.
    3. Call dsl.ImportRules() within an init() function to register the rules.
    package gorules
    
    import (
    	"github.com/quasilyte/go-ruleguard/dsl"
    	quasilyterules "github.com/quasilyte/ruleguard-rules-test"
    )
    
    func init() {
    	// Imported rules will have a "qrules" prefix.
    	dsl.ImportRules("qrules", quasilyterules.Bundle)
    }
    
    func myRule(m dsl.Matcher) {
    	// Your rules here...
    }
  11. Install a Ruleguard bundle via go get

    master

    Bundles are installed using standard Go module commands. If the ruleguard file in the bundle contains a // +build ignore tag, go get will treat it as an indirect dependency. To ensure it is tracked as a direct dependency, use the tools.go idiom.

    # Enable Go modules
    export GO111MODULE=on
    
    # Install the bundle
    go get -v -u github.com/quasilyte/ruleguard-rules-test@master

    To track a bundle with an ignore tag as a direct dependency, create a tools.go file:

    // +build tools
    
    package tools
    
    import (
    	"github.com/quasilyte/go-ruleguard/dsl"
    )
  12. Install ruleguard from source

    master

    To install the ruleguard binary and the required DSL package from source, run the following commands. The binary will be installed under your $(go env GOPATH)/bin and the dsl package will be added to your current module (or globally if not in a module).

    # Installs a `ruleguard` binary under your `$(go env GOPATH)/bin`
    $ go install -v github.com/quasilyte/go-ruleguard/cmd/ruleguard@latest
    
    # Get the DSL package (needed to execute the ruleguard files)
    $ go get -v -u github.com/quasilyte/go-ruleguard/dsl@latest