Skip to content

Commit

Permalink
Implemented an ignore-file (-i) option to the lint command which take…
Browse files Browse the repository at this point in the history
…s a YAML file that lists the paths for each rule for which to ignore errors for.
  • Loading branch information
Calvin Lobo committed Jul 2, 2024
1 parent a24db7e commit 5090621
Show file tree
Hide file tree
Showing 8 changed files with 188 additions and 2 deletions.
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
67 changes: 67 additions & 0 deletions cmd/lint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -507,3 +507,70 @@ 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)

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)
}
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
}
32 changes: 32 additions & 0 deletions cmd/vacuum_report_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,3 +190,35 @@ 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)

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)
}
2 changes: 2 additions & 0 deletions lint-ignore.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
url-starts-with-major-version:
- $.paths['/api/hootsuite-analytics/resolved-conversation/table']
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

0 comments on commit 5090621

Please sign in to comment.