Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add linter for error/trace/log messages #102

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
12 changes: 7 additions & 5 deletions cmd/lintroller/lintroller.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/getoutreach/lintroller/internal/config"
"github.com/getoutreach/lintroller/internal/copyright"
"github.com/getoutreach/lintroller/internal/doculint"
"github.com/getoutreach/lintroller/internal/errorlint"
"github.com/getoutreach/lintroller/internal/header"
"github.com/getoutreach/lintroller/internal/todo"
"github.com/getoutreach/lintroller/internal/why"
Expand Down Expand Up @@ -60,13 +61,13 @@ func main() {
Enabled bool
Analyzer *analysis.Analyzer
}{
{cfg.Header.Enabled, header.NewAnalyzerWithOptions(strings.Join(cfg.Header.Fields, ","))},
{cfg.Copyright.Enabled, copyright.NewAnalyzerWithOptions(cfg.Copyright.Text, cfg.Copyright.Pattern)},
{cfg.Header.Enabled, header.NewAnalyzerWithOptions(strings.Join(cfg.Header.Fields, ","), cfg.Header.Warn)},
{cfg.Copyright.Enabled, copyright.NewAnalyzerWithOptions(cfg.Copyright.Text, cfg.Copyright.Pattern, cfg.Copyright.Warn)},
{cfg.Doculint.Enabled, doculint.NewAnalyzerWithOptions(cfg.Doculint.MinFunLen,
cfg.Doculint.ValidatePackages, cfg.Doculint.ValidateFunctions, cfg.Doculint.ValidateVariables,
cfg.Doculint.ValidateConstants, cfg.Doculint.ValidateTypes)},
{cfg.Todo.Enabled, &todo.Analyzer},
{cfg.Why.Enabled, &why.Analyzer},
cfg.Doculint.ValidateConstants, cfg.Doculint.ValidateTypes, cfg.Doculint.Warn)},
{cfg.Todo.Enabled, todo.NewAnalyzerWithOptions(cfg.Todo.Warn)},
{cfg.Why.Enabled, why.NewAnalyzerWithOptions(cfg.Why.Warn)},
}

var analyzers []*analysis.Analyzer
Expand All @@ -88,5 +89,6 @@ func main() {
&copyright.Analyzer,
&todo.Analyzer,
&why.Analyzer,
&errorlint.Analyzer,
)
}
2 changes: 1 addition & 1 deletion internal/common/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ func LoadOtherFilesIntoFset(pass *analysis.Pass) error {
for _, fn := range pass.OtherFiles {
content, err := os.ReadFile(fn)
if err != nil {
return errors.Wrapf(err, "return file content for \"%s\"", fn)
return errors.Wrapf(err, "return file content for %s", fn)
}

tf := pass.Fset.AddFile(fn, -1, len(content))
Expand Down
37 changes: 37 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ type Lintroller struct {
Doculint Doculint `yaml:"doculint"`
Todo Todo `yaml:"todo"`
Why Why `yaml:"why"`
Errorlint Errorlint `yaml:"errorlint"`
}

// MarshalLog implements the log.Marshaler interface.
Expand All @@ -68,6 +69,7 @@ func (lr *Lintroller) MarshalLog(addField func(key string, value interface{})) {
addField("doculint", lr.Doculint)
addField("todo", lr.Todo)
addField("why", lr.Why)
addField("errorlint", lr.Errorlint)
}

// Header is the configuration type that matches the flags exposed by the header
Expand All @@ -76,6 +78,10 @@ type Header struct {
// Enabled denotes whether or not this linter is enabled. Defaults to true.
Enabled bool `yaml:"enabled"`

// Warn denotes whether or not hits from this linter will result in warnings or
// errors. Defaults to false.
Warn bool `yaml:"warn"`

// Fields is a list of fields required to be filled out in the header. Defaults
// to []string{"Description"}.
Fields []string `yaml:"fields"`
Expand All @@ -93,6 +99,10 @@ type Copyright struct {
// Enabled denotes whether or not this linter is enabled. Defaults to true.
Enabled bool `yaml:"enabled"`

// Warn denotes whether or not hits from this linter will result in warnings or
// errors. Defaults to false.
Warn bool `yaml:"warn"`

// Text is the copyright literal string required at the top of each .go file. If this
// and pattern are empty this linter is a no-op. Pattern will always take precedence
// over text if both are provided. Defaults to an empty string.
Expand All @@ -117,6 +127,10 @@ type Doculint struct {
// Enabled denotes whether or not this linter is enabled. Defaults to true.
Enabled bool `yaml:"enabled"`

// Warn denotes whether or not hits from this linter will result in warnings or
// errors. Defaults to false.
Warn bool `yaml:"warn"`

// MinFunLen is the minimum function length that doculint will report on if said
// function has no related documentation. Defaults to 10.
MinFunLen int `yaml:"minFunLen"`
Expand Down Expand Up @@ -157,6 +171,10 @@ func (d *Doculint) MarshalLog(addField func(key string, value interface{})) {
type Todo struct {
// Enabled denotes whether or not this linter is enabled. Defaults to true.
Enabled bool `yaml:"enabled"`

// Warn denotes whether or not hits from this linter will result in warnings or
// errors. Defaults to false.
Warn bool `yaml:"warn"`
}

// MarshalLog implements the log.Marshaler interface.
Expand All @@ -168,9 +186,28 @@ func (t *Todo) MarshalLog(addField func(key string, value interface{})) {
type Why struct {
// Enabled denotes whether or not this linter is enabled. Defaults to true.
Enabled bool `yaml:"enabled"`

// Warn denotes whether or not hits from this linter will result in warnings or
// errors. Defaults to false.
Warn bool `yaml:"warn"`
}

// MarshalLog implements the log.Marshaler interface.
func (w *Why) MarshalLog(addField func(key string, value interface{})) {
addField("enabled", w.Enabled)
}

// Errorlint is the configuration type that matches the flags exposed by the errorlint linter.
type Errorlint struct {
// Enabled denotes whether or not this linter is enabled. Defaults to true.
Enabled bool `yaml:"enabled"`

// Warn denotes whether or not hits from this linter will result in warnings or
// errors. Defaults to false.
Warn bool `yaml:"warn"`
}

// MarshalLog implements the log.Marshaler interface.
func (e *Errorlint) MarshalLog(addField func(key string, value interface{})) {
addField("warn", e.Warn)
}
30 changes: 23 additions & 7 deletions internal/config/tiers.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,23 +49,23 @@ func (l *Lintroller) ValidateTier() error {
switch strings.ToLower(*l.Tier) {
case TierBronze:
if err := l.EnsureMinimums(&TierBronzeConfiguration); err != nil {
return errors.Wrap(err, "ensure given configuration meets minimum requirments for bronze tier")
return errors.Wrap(err, "ensure given configuration meets minimum requirements for bronze tier")
}
case TierSilver:
if err := l.EnsureMinimums(&TierSilverConfiguration); err != nil {
return errors.Wrap(err, "ensure given configuration meets minimum requirments for silver tier")
return errors.Wrap(err, "ensure given configuration meets minimum requirements for silver tier")
}
case TierGold:
if err := l.EnsureMinimums(&TierGoldConfiguration); err != nil {
return errors.Wrap(err, "ensure given configuration meets minimum requirments for gold tier")
return errors.Wrap(err, "ensure given configuration meets minimum requirements for gold tier")
}
case TierPlatinum:
if err := l.EnsureMinimums(&TierPlatinumConfiguration); err != nil {
return errors.Wrap(err, "ensure given configuration meets minimum requirments for platinum tier")
return errors.Wrap(err, "ensure given configuration meets minimum requirements for platinum tier")
}
default:
log.Warn(context.Background(),
fmt.Sprintf("provided does not match any of the following: %q, %q, %q, %q (sans-quotes)",
fmt.Sprintf("Provided does not match any of the following: %q, %q, %q, %q (sans-quotes)",
TierBronze, TierSilver, TierGold, TierPlatinum),
log.F{
"tier": *l.Tier,
Expand Down Expand Up @@ -116,7 +116,7 @@ func (l *Lintroller) EnsureMinimums(desired *Lintroller) error { //nolint:funlen

if !found {
return fmt.Errorf(
"deviation detected from tier minimum defaults in lintroller.header.fields, fields must contain \"%s\"",
"deviation detected from tier minimum defaults in lintroller.header.fields, fields must contain %s",
desired.Header.Fields[i])
}
}
Expand Down Expand Up @@ -164,7 +164,7 @@ func (l *Lintroller) EnsureMinimums(desired *Lintroller) error { //nolint:funlen
l.Doculint.MinFunLen = desired.Doculint.MinFunLen
} else if l.Doculint.MinFunLen > desired.Doculint.MinFunLen || l.Doculint.MinFunLen < 0 {
return fmt.Errorf(
"deviation detected from tier minimum defaults in lintroller.doculint.minFunLen, minFunLen must be set within (0, %d]",
"deviation detected from tier minimum defaults in lintroller.doculint.minfunlen, minfunlen must be set within 0 and %d",
desired.Doculint.MinFunLen)
}
}
Expand Down Expand Up @@ -205,6 +205,10 @@ var TierBronzeConfiguration = Lintroller{
Why: Why{
Enabled: false,
},
Errorlint: Errorlint{
nirupama7 marked this conversation as resolved.
Show resolved Hide resolved
Enabled: true,
Warn: true,
},
}

// TierSilverConfiguration is the Lintroller configuration minumums that correspond
Expand Down Expand Up @@ -234,6 +238,10 @@ var TierSilverConfiguration = Lintroller{
Why: Why{
Enabled: true,
},
Errorlint: Errorlint{
Enabled: true,
jkinkead marked this conversation as resolved.
Show resolved Hide resolved
Warn: true,
},
}

// TierGoldConfiguration is the Lintroller configuration minumums that correspond
Expand Down Expand Up @@ -263,6 +271,10 @@ var TierGoldConfiguration = Lintroller{
Why: Why{
Enabled: true,
},
Errorlint: Errorlint{
Enabled: true,
Warn: true,
},
}

// TierPlatinumConfiguration is the Lintroller configuration minumums that correspond
Expand Down Expand Up @@ -292,4 +304,8 @@ var TierPlatinumConfiguration = Lintroller{
Why: Why{
Enabled: true,
},
Errorlint: Errorlint{
Enabled: true,
Warn: true,
},
}
33 changes: 25 additions & 8 deletions internal/copyright/copyright.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"sync"

"github.com/getoutreach/lintroller/internal/common"
"github.com/getoutreach/lintroller/internal/reporter"
"golang.org/x/tools/go/analysis"
)

Expand All @@ -36,9 +37,10 @@ var Analyzer = analysis.Analyzer{
// that would have been defined via flags if this was ran as a vet tool. This is so the
// analyzers can be ran outside of the context of a vet tool and config can be gathered
// from elsewhere.
func NewAnalyzerWithOptions(_text, _pattern string) *analysis.Analyzer {
func NewAnalyzerWithOptions(_text, _pattern string, _warn bool) *analysis.Analyzer {
text = strings.TrimSpace(_text)
pattern = strings.TrimSpace(_pattern)
warn = _warn
return &Analyzer
}

Expand All @@ -52,6 +54,10 @@ var (
// pattern is a variable that gets collected via flags. This variable contains the copyright
// string as a regular expression pattern that is required to be at the top of each .go file.
pattern string

// warn denotes whether or not lint reports from this linter will result in warnings or
// errors.
warn bool
)

// comparer is a convience type used to conditionally compare using either a string or a
Expand Down Expand Up @@ -126,10 +132,12 @@ func (c *comparer) trackUniqueness(copyrightString string) {

func init() { //nolint:gochecknoinits // Why: This is necessary to grab flags.
// Setup flags.
//nolint:lll // Why: usage long
Analyzer.Flags.StringVar(&text, "text", "", "the copyright string required at the top of each .go file. if this and pattern are empty the linter is a no-op")
//nolint:lll // Why: usage long
Analyzer.Flags.StringVar(&pattern, "pattern", "", "the copyright pattern (as a regular expression) required at the top of each .go file. if this and pattern are empty the linter is a no-op. pattern takes precedence over text if both are supplied")
Analyzer.Flags.StringVar(&text, "text", "",
"the copyright string required at the top of each .go file. if this and pattern are empty the linter is a no-op")
Analyzer.Flags.StringVar(&pattern, "pattern", "",
"the copyright pattern (as a regular expression) required at the top of each .go file. if this and pattern are empty the linter is a no-op. pattern takes precedence over text if both are supplied") //nolint:lll // Why: usage long
Analyzer.Flags.BoolVar(&warn, "warn", false,
"controls whether or not reports from this linter will result in errors or warnings")

// Trim space around the passed in variables just in case.
text = strings.TrimSpace(text)
Expand All @@ -138,22 +146,31 @@ func init() { //nolint:gochecknoinits // Why: This is necessary to grab flags.

// copyright is the function that gets passed to the Analyzer which runs the actual
// analysis for the copyright linter on a set of files.
func copyright(pass *analysis.Pass) (interface{}, error) { //nolint:funlen // Why: Doesn't make sense to break this function up anymore.
func copyright(_pass *analysis.Pass) (interface{}, error) { //nolint:funlen // Why: Doesn't make sense to break this function up anymore.
// Ignore test packages.
if common.IsTestPackage(pass) {
if common.IsTestPackage(_pass) {
return nil, nil
}

if text == "" && pattern == "" {
return nil, nil
}

var opts []reporter.PassOption
if warn {
opts = append(opts, reporter.Warn())
}

// Wrap _pass with reporter.Pass to take nolint directives into account and potentially
// warn instead of error.
pass := reporter.NewPass(name, _pass, opts...)

// comparer to use on this pass.
var c comparer

for _, file := range pass.Files {
// Ignore generated files and test files.
if common.IsGenerated(file) || common.IsTestFile(pass, file) {
if common.IsGenerated(file) || common.IsTestFile(pass.Pass, file) {
continue
}

Expand Down
20 changes: 17 additions & 3 deletions internal/doculint/doculint.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,15 @@ var Analyzer = analysis.Analyzer{
// analyzers can be ran outside of the context of a vet tool and config can be gathered
// from elsewhere.
func NewAnalyzerWithOptions(
_minFunLen int, _validatePackages, _validateFunctions, _validateVariables, _validateConstants, _validateTypes bool) *analysis.Analyzer {
_minFunLen int, _validatePackages, _validateFunctions,
_validateVariables, _validateConstants, _validateTypes, _warn bool) *analysis.Analyzer {
minFunLen = _minFunLen
validatePackages = _validatePackages
validateFunctions = _validateFunctions
validateVariables = _validateVariables
validateConstants = _validateConstants
validateTypes = _validateTypes
warn = _warn

return &Analyzer
}
Expand Down Expand Up @@ -81,6 +83,10 @@ var (
// flag that denotes whether or not the linter should validate that types have
// satisfactory comments.
validateTypes bool

// warn denotes whether or not lint reports from this linter will result in warnings or
// errors.
warn bool
)

func init() { //nolint:gochecknoinits // Why: This is necessary to grab flags.
Expand All @@ -96,6 +102,8 @@ func init() { //nolint:gochecknoinits // Why: This is necessary to grab flags.
&validateConstants, "validateConstants", true, "a boolean flag that denotes whether or not to validate constant comments")
Analyzer.Flags.BoolVar(
&validateTypes, "validateTypes", true, "a boolean flag that denotes whether or not to validate type comments")
Analyzer.Flags.BoolVar(
&warn, "warn", false, "controls whether or not reports from this linter will result in errors or warnings")

if minFunLen == 0 {
minFunLen = 10
Expand All @@ -110,8 +118,14 @@ func doculint(_pass *analysis.Pass) (interface{}, error) { //nolint:funlen // Wh
return nil, nil
}

// Wrap _pass with reporter.Pass to take nolint directives into account.
pass := reporter.NewPass(name, _pass)
var opts []reporter.PassOption
if warn {
opts = append(opts, reporter.Warn())
}

// Wrap _pass with reporter.Pass to take nolint directives into account and potentially
// warn instead of error.
pass := reporter.NewPass(name, _pass, opts...)

// Variable to keep track of whether or not this current package has a file with
// the same name as the package. This is where the package comment should exist.
Expand Down
Loading