Repository: ysugimoto/falco
Pull request #7 (2021-07-06 15:59:43)

Override severity via config
This PR supports overriding lint error severity from the user configuration file.

The fixed configuration file is `.falcorc`, and should be placed in the current working directory.

The configuration file format is a simple key-value object like YAML:

```yaml
regex/matched-value-override: IGNORE
```

And then falco overrides error severity as specified severity matched its rule.



Patch:
diff --git a/README.md b/README.md
index 6195321..81aa2b9 100644
--- a/README.md
+++ b/README.md
@@ -21,7 +21,7 @@ Typically our deployment flow using custom VCLs is following:
 1. Clone active service and create new version
 2. Delete existing custom VCLs
 3. Upload new VCL files to the Fastly
-4. Activate new sevice version // <= Validate VCLs on the Fastly cloud
+4. Activate new sevice version **<= Validate VCLs on the Fastly cloud**
 
 Above flows take a time, and then if we have some mistakes on VCL e.g. missing semicolon X(, the deployment will fail.
 Additionally, unnecessary service version will be created by our trivial issue.
@@ -57,7 +57,7 @@ Example:
 ```
 
 ### Note:
-Youe VCL will have dependent modules and loaded via `include [module]`. `falco` accept include path from `-I, --include_path` flag and search and load destination module from include path.
+Your VCL will have dependent modules and loaded via `include [module]`. `falco` accept include path from `-I, --include_path` flag and search and load destination module from include path.
 
 ## User defined subroutine
 
@@ -133,7 +133,21 @@ However, we're planning to solve them using Fastly API.
 
 ## Lint error
 
-`falco` has builtin lint rules. see [rules](https://github.com/ysugimoto/falco/blob/main/docs/rules.md) in detail.
+`falco` has builtin lint rules. see [rules](https://github.com/ysugimoto/falco/blob/main/docs/rules.md) in detail. `falco` may report lots of errors and warnings because falco lints with strict type checks, disallows implicit type conversions even VCL is fuzzy typed language. 
+
+## Overriding Severity
+
+To avoid them, you can override severity levels by putting configuration file named `.falcorc` on working directory. the configuration file contents format is following:
+
+```yaml
+## /path/to/working/directory/.falcorc
+regex/matched-value-override: IGNORE
+...
+```
+
+Format is simply yaml key-value object. The key is rule name, see [rules.md](https://github.com/ysugimoto/falco/blob/main/docs/rules.md) and value should be one of `IGNORE`, `INFO`, `WARGNING` and `ERROR`, case insensitive.
+
+On above case, the rule of `regex/matched-value-override` reports `INFO` as default, but override to `IGNORE` which does not report it.
 
 ## Error Levels
 
diff --git a/cli/runner.go b/cli/runner.go
index 00ce622..f30a232 100644
--- a/cli/runner.go
+++ b/cli/runner.go
@@ -8,6 +8,7 @@ import (
 
 	"path/filepath"
 
+	"github.com/goccy/go-yaml"
 	"github.com/pkg/errors"
 	"github.com/ysugimoto/falco/ast"
 	"github.com/ysugimoto/falco/context"
@@ -18,7 +19,8 @@ import (
 )
 
 var (
-	ErrParser = fmt.Errorf("parser error")
+	ErrParser   = fmt.Errorf("parser error")
+	DotfileName = ".falcorc"
 )
 
 type Level int
@@ -40,6 +42,7 @@ type Runner struct {
 	transformers []*Transformer
 	includePaths []string
 	mainVclFile  string
+	overrides    map[string]linter.Severity
 
 	level Level
 
@@ -57,6 +60,7 @@ func NewRunner(mainVcl string, c *Config) (*Runner, error) {
 		mainVclFile: mainVcl,
 		context:     context.New(),
 		level:       LevelError,
+		overrides:   make(map[string]linter.Severity),
 	}
 
 	// Directory which placed main VCL adds to include path
@@ -88,9 +92,61 @@ func NewRunner(mainVcl string, c *Config) (*Runner, error) {
 		r.level = LevelWarning
 	}
 
+	// Make overriding error level setting from rc file
+	r.initOverrides()
+
 	return r, nil
 }
 
+func (r *Runner) initOverrides() {
+	// find up rc file
+	cwd, err := os.Getwd()
+	if err != nil {
+		return
+	}
+	var rcFile string
+	for {
+		rcFile = filepath.Join(cwd, DotfileName)
+		if _, err := os.Stat(rcFile); err == nil {
+			break
+		}
+		cwd = filepath.Dir(cwd)
+		if cwd == "/" {
+			// find up to root directory, stop it
+			return
+		}
+	}
+
+	fp, err := os.Open(rcFile)
+	if err != nil {
+		writeln(yellow, "Configuration file found at %s but could not open", rcFile)
+		return
+	}
+	defer fp.Close()
+	o := make(map[string]string)
+	if err := yaml.NewDecoder(fp).Decode(&o); err != nil {
+		writeln(yellow, "Failed to decode configuration file at %s: %s", rcFile, err)
+		return
+	}
+
+	// validate configuration file
+	for k, v := range o {
+		switch strings.ToUpper(v) {
+		case "ERROR":
+			r.overrides[k] = linter.ERROR
+		case "WARNING":
+			r.overrides[k] = linter.WARNING
+		case "INFO":
+			r.overrides[k] = linter.INFO
+		case "IGNORE":
+			r.overrides[k] = linter.IGNORE
+		default:
+			writeln(yellow, "level for rule %s has invalid value %s, skip it.", k, v)
+			return
+		}
+	}
+}
+
 func (r *Runner) Transform(vcls []*plugin.VCL) error {
 	// VCL data is shared between parser and transformar through the falco/io package.
 	encoded, err := plugin.Encode(vcls)
@@ -223,7 +279,13 @@ func (r *Runner) printLinterError(lx *lexer.Lexer, err *linter.LintError) {
 		file = "in " + err.Token.File + " "
 	}
 
-	switch err.Severity {
+	// check severity with overrides
+	severity := err.Severity
+	if v, ok := r.overrides[string(err.Rule)]; ok {
+		severity = v
+	}
+
+	switch severity {
 	case linter.ERROR:
 		r.errors++
 		writeln(red, ":fire:[ERROR] %s%s", err.Message, rule)
@@ -239,6 +301,8 @@ func (r *Runner) printLinterError(lx *lexer.Lexer, err *linter.LintError) {
 			return
 		}
 		writeln(cyan, ":speaker:[INFO] %s%s", err.Message, rule)
+	case linter.IGNORE:
+		return
 	}
 
 	writeln(white, "%sat line %d, position %d", file, err.Token.Line, err.Token.Position)
diff --git a/linter/errors.go b/linter/errors.go
index 6882253..6bcfdf9 100644
--- a/linter/errors.go
+++ b/linter/errors.go
@@ -15,6 +15,7 @@ const (
 	WARNING Severity = "Warning"
 	ERROR   Severity = "Error"
 	INFO    Severity = "Info"
+	IGNORE  Severity = "Ignore"
 )
 
 type LintError struct {
diff --git a/linter/linter.go b/linter/linter.go
index a7d9a44..27f7372 100644
--- a/linter/linter.go
+++ b/linter/linter.go
@@ -491,7 +491,7 @@ func (l *Linter) lintFastlyBoilerPlateMacro(sub *ast.SubroutineDeclaration, phra
 
 	// Macro not found
 	err := &LintError{
-		Severity: ERROR,
+		Severity: WARNING,
 		Token:    sub.GetMeta().Token,
 		Message: fmt.Sprintf(
 			`Subroutine "%s" does not have fastly boilerplate comment "%s" inside definition`, sub.Name.Value, phrase,
@@ -813,7 +813,7 @@ func (l *Linter) lintIfStatement(stmt *ast.IfStatement, ctx *context.Context) ty
 	// push regex captured variables
 	if err := pushRegexGroupVars(stmt.Condition, ctx); err != nil {
 		err := &LintError{
-			Severity: WARNING,
+			Severity: INFO,
 			Token:    stmt.Condition.GetMeta().Token,
 			Message:  err.Error(),
 		}
@@ -825,7 +825,7 @@ func (l *Linter) lintIfStatement(stmt *ast.IfStatement, ctx *context.Context) ty
 		l.lintIfCondition(a.Condition, ctx)
 		if err := pushRegexGroupVars(a.Condition, ctx); err != nil {
 			err := &LintError{
-				Severity: WARNING,
+				Severity: INFO,
 				Token:    a.Condition.GetMeta().Token,
 				Message:  err.Error(),
 			}


Test patch:
diff --git a/linter/linter_test.go b/linter/linter_test.go
index d13e158..0ee58c6 100644
--- a/linter/linter_test.go
+++ b/linter/linter_test.go
@@ -641,7 +641,7 @@ sub foo {
 	}
 	set var.S = re.group.1;
 }`
-		assertErrorWithSeverity(t, input, WARNING)
+		assertErrorWithSeverity(t, input, INFO)
 	})
 
 	t.Run("condition type is not expected", func(t *testing.T) {