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

Implemented an --ignore-file option to lint/report commands to ignore specific errors #515

Merged
merged 3 commits into from
Jul 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,35 @@ recognizes a compressed report file and will deal with it automatically when rea

> When using compression, the file name will be `vacuum-report-MM-DD-YY-HH_MM_SS.json.gz`. vacuum uses gzip internally.

## Ignoring specific linting errors

You can ignore specific linting errors by providing an `--ignore-file` argument to the `lint` and `report` commands.

```
./vacuum lint --ignore-file <path-to-ignore-file.yaml> -d <your-openapi-spec.yaml>
```

```
./vacuum report --ignore-file <path-to-ignore-file.yaml> -c <your-openapi-spec.yaml> <report-prefix>
```

The ignore-file should point to a .yaml file that contains a list of errors to be ignored by vacuum. The structure of the
yaml file is as follows:

```
<rule-id-1>:
- <json_path_to_error_or_warning_1>
- <json_path_to_error_or_warning_2>
<rule-id-2>:
- <json_path_to_error_or_warning_1>
- <json_path_to_error_or_warning_2>
...
```

Ignoring errors is useful for when you want to implement new rules to existing production APIs. In some cases,
correcting the lint errors would result in a breaking change. Having a way to ignore these errors allows you to implement
the new rules for new APIs while maintaining backwards compatibility for existing ones.

---

## Try out the dashboard
Expand Down
63 changes: 61 additions & 2 deletions cmd/lint.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package cmd
import (
"errors"
"fmt"
"gopkg.in/yaml.v3"
"log/slog"
"os"
"path/filepath"
Expand Down Expand Up @@ -61,6 +62,7 @@ func GetLintCommand() *cobra.Command {
hardModeFlag, _ := cmd.Flags().GetBool("hard-mode")
ignoreArrayCircleRef, _ := cmd.Flags().GetBool("ignore-array-circle-ref")
ignorePolymorphCircleRef, _ := cmd.Flags().GetBool("ignore-polymorph-circle-ref")
ignoreFile, _ := cmd.Flags().GetString("ignore-file")

// disable color and styling, for CI/CD use.
// https://github.com/daveshanley/vacuum/issues/234
Expand Down Expand Up @@ -175,6 +177,25 @@ func GetLintCommand() *cobra.Command {
}
}

if len(ignoreFile) > 1 {
if !silent {
pterm.Info.Printf("Using ignore file '%s'", ignoreFile)
pterm.Println()
}
}

ignoredItems := model.IgnoredItems{}
if ignoreFile != "" {
raw, ferr := os.ReadFile(ignoreFile)
if ferr != nil {
return fmt.Errorf("failed to read ignore file: %w", ferr)
}
ferr = yaml.Unmarshal(raw, &ignoredItems)
if ferr != nil {
return fmt.Errorf("failed to read ignore file: %w", ferr)
}
}

start := time.Now()

var filesProcessedSize int64
Expand Down Expand Up @@ -214,6 +235,7 @@ func GetLintCommand() *cobra.Command {
TimeoutFlag: timeoutFlag,
IgnoreArrayCircleRef: ignoreArrayCircleRef,
IgnorePolymorphCircleRef: ignorePolymorphCircleRef,
IgnoredResults: ignoredItems,
}
fs, fp, err := lintFile(lfr)

Expand Down Expand Up @@ -261,6 +283,7 @@ func GetLintCommand() *cobra.Command {
cmd.Flags().StringP("fail-severity", "n", model.SeverityError, "Results of this level or above will trigger a failure exit code")
cmd.Flags().Bool("ignore-array-circle-ref", false, "Ignore circular array references")
cmd.Flags().Bool("ignore-polymorph-circle-ref", false, "Ignore circular polymorphic references")
cmd.Flags().String("ignore-file", "", "Path to ignore file")
// TODO: Add globbed-files flag to other commands as well
cmd.Flags().String("globbed-files", "", "Glob pattern of files to lint")

Expand Down Expand Up @@ -320,7 +343,7 @@ func lintFile(req utils.LintFileRequest) (int64, int, error) {
IgnoreCircularPolymorphicRef: req.IgnorePolymorphCircleRef,
})

results := result.Results
result.Results = filterIgnoredResults(result.Results, req.IgnoredResults)

if len(result.Errors) > 0 {
for _, err := range result.Errors {
Expand All @@ -330,7 +353,7 @@ func lintFile(req utils.LintFileRequest) (int64, int, error) {
return result.FileSize, result.FilesProcessed, fmt.Errorf("linting failed due to %d issues", len(result.Errors))
}

resultSet := model.NewRuleResultSet(results)
resultSet := model.NewRuleResultSet(result.Results)
resultSet.SortResultsByLineNumber()
warnings := resultSet.GetWarnCount()
errs := resultSet.GetErrorCount()
Expand Down Expand Up @@ -362,6 +385,42 @@ func lintFile(req utils.LintFileRequest) (int64, int, error) {
return result.FileSize, result.FilesProcessed, CheckFailureSeverity(req.FailSeverityFlag, errs, warnings, informs)
}

// filterIgnoredResultsPtr filters the given results slice, taking out any (RuleID, Path) combos that were listed in the
// ignore file
func filterIgnoredResultsPtr(results []*model.RuleFunctionResult, ignored model.IgnoredItems) []*model.RuleFunctionResult {
var filteredResults []*model.RuleFunctionResult

for _, r := range results {

var found bool
for _, i := range ignored[r.Rule.Id] {
if r.Path == i {
found = true
break
}
}
if !found {
filteredResults = append(filteredResults, r)
}
}

return filteredResults
}

// filterIgnoredResults does the filtering of ignored results on non-pointer result elements
func filterIgnoredResults(results []model.RuleFunctionResult, ignored model.IgnoredItems) []model.RuleFunctionResult {
resultsPtrs := make([]*model.RuleFunctionResult, 0, len(results))
for _, r := range results {
r := r // prevent loop memory aliasing
resultsPtrs = append(resultsPtrs, &r)
}
resultsFiltered := make([]model.RuleFunctionResult, 0, len(results))
for _, r := range filterIgnoredResultsPtr(resultsPtrs, ignored) {
resultsFiltered = append(resultsFiltered, *r)
}
return resultsFiltered
}

func processResults(results []*model.RuleFunctionResult,
specData []string,
snippets,
Expand Down
72 changes: 72 additions & 0 deletions cmd/lint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"fmt"
"github.com/daveshanley/vacuum/model"
"github.com/pterm/pterm"
"github.com/stretchr/testify/assert"
"io"
"os"
Expand Down Expand Up @@ -507,3 +508,74 @@ rules:
assert.NoError(t, err)
assert.NotNil(t, outBytes)
}

func TestFilterIgnoredResults(t *testing.T) {

results := []model.RuleFunctionResult{
{Path: "a/b/c", Rule: &model.Rule{Id: "XXX"}},
{Path: "a/b", Rule: &model.Rule{Id: "XXX"}},
{Path: "a", Rule: &model.Rule{Id: "XXX"}},
{Path: "a/b/c", Rule: &model.Rule{Id: "YYY"}},
{Path: "a/b", Rule: &model.Rule{Id: "YYY"}},
{Path: "a", Rule: &model.Rule{Id: "YYY"}},
{Path: "a/b/c", Rule: &model.Rule{Id: "ZZZ"}},
{Path: "a/b", Rule: &model.Rule{Id: "ZZZ"}},
{Path: "a", Rule: &model.Rule{Id: "ZZZ"}},
}

igItems := model.IgnoredItems{
"XXX": []string{"a/b/c"},
"YYY": []string{"a/b"},
}

results = filterIgnoredResults(results, igItems)

expected := []model.RuleFunctionResult{
{Path: "a/b", Rule: &model.Rule{Id: "XXX"}},
{Path: "a", Rule: &model.Rule{Id: "XXX"}},
{Path: "a/b/c", Rule: &model.Rule{Id: "YYY"}},
{Path: "a", Rule: &model.Rule{Id: "YYY"}},
{Path: "a/b/c", Rule: &model.Rule{Id: "ZZZ"}},
{Path: "a/b", Rule: &model.Rule{Id: "ZZZ"}},
{Path: "a", Rule: &model.Rule{Id: "ZZZ"}},
}
assert.Len(t, results, 7)
assert.Equal(t, expected, expected)
}

func TestGetLintCommand_Details_WithIgnoreFile(t *testing.T) {

yaml := `
extends: [[spectral:oas, recommended]]
rules:
url-starts-with-major-version:
description: Major version must be the first URL component
message: All paths must start with a version number, eg /v1, /v2
given: $.paths
severity: error
then:
function: pattern
functionOptions:
match: "/v[0-9]+/"
`

tmp, _ := os.CreateTemp("", "")
_, _ = io.WriteString(tmp, yaml)

b := bytes.NewBufferString("")
pterm.SetDefaultOutput(b)

cmd := GetLintCommand()
cmd.PersistentFlags().StringP("ruleset", "r", "", "")
cmd.SetArgs([]string{
"-d",
"--ignore-file",
"../model/test_files/burgershop.ignorefile.yaml",
"-r",
tmp.Name(),
"../model/test_files/burgershop.openapi.yaml",
})
cmdErr := cmd.Execute()
assert.NoError(t, cmdErr)
assert.Contains(t, b.String(), "Linting passed")
}
17 changes: 17 additions & 0 deletions cmd/vacuum_report.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
vacuum_report "github.com/daveshanley/vacuum/vacuum-report"
"github.com/pterm/pterm"
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
"os"
"time"
)
Expand Down Expand Up @@ -46,6 +47,7 @@ func GetVacuumReportCommand() *cobra.Command {
skipCheckFlag, _ := cmd.Flags().GetBool("skip-check")
timeoutFlag, _ := cmd.Flags().GetInt("timeout")
hardModeFlag, _ := cmd.Flags().GetBool("hard-mode")
ignoreFile, _ := cmd.Flags().GetString("ignore-file")

// disable color and styling, for CI/CD use.
// https://github.com/daveshanley/vacuum/issues/234
Expand Down Expand Up @@ -102,6 +104,18 @@ func GetVacuumReportCommand() *cobra.Command {
return fileError
}

ignoredItems := model.IgnoredItems{}
if ignoreFile != "" {
raw, ferr := os.ReadFile(ignoreFile)
if ferr != nil {
return fmt.Errorf("failed to read ignore file: %w", ferr)
}
ferr = yaml.Unmarshal(raw, &ignoredItems)
if ferr != nil {
return fmt.Errorf("failed to read ignore file: %w", ferr)
}
}

// read spec and parse to dashboard.
defaultRuleSets := rulesets.BuildDefaultRuleSets()

Expand Down Expand Up @@ -165,6 +179,8 @@ func GetVacuumReportCommand() *cobra.Command {
resultSet := model.NewRuleResultSet(ruleset.Results)
resultSet.SortResultsByLineNumber()

resultSet.Results = filterIgnoredResultsPtr(resultSet.Results, ignoredItems)

duration := time.Since(start)

// if we want jUnit output, then build the report and be done with it.
Expand Down Expand Up @@ -262,5 +278,6 @@ func GetVacuumReportCommand() *cobra.Command {
cmd.Flags().BoolP("compress", "c", false, "Compress results using gzip")
cmd.Flags().BoolP("no-pretty", "n", false, "Render JSON with no formatting")
cmd.Flags().BoolP("no-style", "q", false, "Disable styling and color output, just plain text (useful for CI/CD)")
cmd.Flags().String("ignore-file", "", "Path to ignore file")
return cmd
}
37 changes: 37 additions & 0 deletions cmd/vacuum_report_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package cmd
import (
"bytes"
"fmt"
"github.com/pterm/pterm"
"github.com/stretchr/testify/assert"
"io"
"os"
Expand Down Expand Up @@ -190,3 +191,39 @@ func TestGetVacuumReportCommand_BadFile(t *testing.T) {
assert.Error(t, cmdErr)

}

func TestGetVacuumReport_WithIgnoreFile(t *testing.T) {

yaml := `
extends: [[spectral:oas, recommended]]
rules:
url-starts-with-major-version:
description: Major version must be the first URL component
message: All paths must start with a version number, eg /v1, /v2
given: $.paths
severity: error
then:
function: pattern
functionOptions:
match: "/v[0-9]+/"
`

tmp, _ := os.CreateTemp("", "")
_, _ = io.WriteString(tmp, yaml)

b := bytes.NewBufferString("")
pterm.SetDefaultOutput(b)

cmd := GetVacuumReportCommand()
cmd.PersistentFlags().StringP("ruleset", "r", "", "")
cmd.SetArgs([]string{
"--ignore-file",
"../model/test_files/burgershop.ignorefile.yaml",
"-r",
tmp.Name(),
"../model/test_files/burgershop.openapi.yaml",
})
cmdErr := cmd.Execute()
assert.NoError(t, cmdErr)
assert.Contains(t, b.String(), "SUCCESS")
}
2 changes: 2 additions & 0 deletions model/rules.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ type RuleFunctionResult struct {
ModelContext any `json:"-" yaml:"-"`
}

type IgnoredItems map[string][]string

// RuleResultSet contains all the results found during a linting run, and all the methods required to
// filter, sort and calculate counts.
type RuleResultSet struct {
Expand Down
6 changes: 6 additions & 0 deletions model/test_files/burgershop.ignorefile.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
url-starts-with-major-version:
- $.paths['/burgers']
- $.paths['/burgers/{burgerId}']
- $.paths['/burgers/{burgerId}/dressings']
- $.paths['/dressings/{dressingId}']
- $.paths['/dressings']
1 change: 1 addition & 0 deletions utils/lint_file_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type LintFileRequest struct {
TimeoutFlag int
IgnoreArrayCircleRef bool
IgnorePolymorphCircleRef bool
IgnoredResults model.IgnoredItems
DefaultRuleSets rulesets.RuleSets
SelectedRS *rulesets.RuleSet
Functions map[string]model.RuleFunction
Expand Down
Loading