ruleguard rules package management

Bundles is a new feature coming to the ruleguard. It’ll make it possible to re-use third-party rules without having to copy/paste them.

Creating an importable bundle

A package that exports rules must define a Bundle object:

package gorules

import "github.com/quasilyte/go-ruleguard/dsl"

// 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{}

func boolComparison(m dsl.Matcher) {
	m.Match(`$x == true`,
		`$x != true`,
		`$x == false`,
		`$x != false`).
		Report(`omit bool literal in expression`)
}

That package should be a separate Go module. A rules bundle is versioned by its Go module.

It’s possible to have several ruleguard files inside one Go module. Only one file should define a Bundle object. During a bundle import, all files will be exported.

The metadata object is called a Bundle to avoid confusion with Go packages and Go modules. It’s useful to have a dedicated word for them.

Importing a bundle

To use an external rule set:

  1. Import the containing package
  2. In init() function, use its Bundle variable in ImportRules() call
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)
}

// Then you can define your own rules.

func emptyStringTest(m dsl.Matcher) {
	m.Match(`len($s) == 0`).
		Where(m["s"].Type.Is("string")).
		Report(`maybe use $s == "" instead?`)

	m.Match(`len($s) != 0`).
		Where(m["s"].Type.Is("string")).
		Report(`maybe use $s != "" instead?`)
}

Now all you need is to install the imported github.com/quasilyte/ruleguard-rules-test package. Since bundles are Go modules, it’s as simple as installing any other Go module:

go get -v github.com/quasilyte/ruleguard-rules-test

It’s possible to use an empty ("") prefix, but you’ll risk getting a name collision. If you don’t define your own rules, then it’s perfectly fine to use an empty prefix.

All ruleguard packages are named gorules, so you’ll need to assign a local package name. In the example above, we used quasilyterules name.

Running the ruleguard

If you installed the bundle, you should be able to run your main rules file normally:

$ ruleguard -rules rules.go test.go 
test.go:4:6: emptyStringTest: maybe use s == "" instead? (rules.go:13)
test.go:5:6: qrules/boolComparison: omit bool literal in expression (rules1.go:8)

Using ruleguard from the go-critic or golangci-lint stays the same. As long as bundles are installed and they can be located by the go list $package_path, everything should work fine.

Limitations:

  • Imported packages can’t import other bundle packages (could be addressed later)
  • Bundles are tied to Go modules; they might now work properly without them