Repository: ysugimoto/falco
Pull request #140 (2023-04-20 16:11:19)

implement ignoring feature
Fixes https://github.com/ysugimoto/falco/issues/110

This PR implements the ignoring feature by appending/prepending specific comments.
Regarding https://github.com/ysugimoto/falco/issues/110 , this is partial support but this PR is enough I think for now.
I implemented following signatures:

- `// falco-ignore-next-line` on leading comment
- `// falco-ignore` on trailing comment
- `// falco-ignore-start` and `// falco-ignore-end` on leading comment

And, we does not support ignoring specific rules like `// falco-ignore unused/variable` for now.
This should be implemented when we need but we can do it above this implementation. This PR is underlying.

Here are examples:

### Ignore any errors whole subroutine

```vcl
// falco-ignore-start
sub vcl_recv {
  ...  any statement/expression errors will be ingored
}
// falco-ignore-end
...
```

### Ignore any errors for the next statement

```vcl
sub vcl_recv {
  // falco-ignore-next-line
  set req.http.X-Value = some.undefined.value;
}
```

### Ignore any errors in the current statement

```vcl
sub vcl_recv {
  set req.http.X-Value = some.undefined.value; // falco-ignore
}
```

If anyone have opinion, let's discuss for that.




Patch:
diff --git a/linter/ignore.go b/linter/ignore.go
new file mode 100644
index 0000000..e61ddf9
--- /dev/null
+++ b/linter/ignore.go
@@ -0,0 +1,98 @@
+package linter
+
+import (
+	"strings"
+
+	"github.com/ysugimoto/falco/ast"
+)
+
+// ignore signatures
+const (
+	falcoIgnoreNextLine = "falco-ignore-next-line"
+	falcoIgnoreThisLine = "falco-ignore"
+	falcoIgnoreStart    = "falco-ignore-start"
+	falcoIgnoreEnd      = "falco-ignore-end"
+)
+
+type ignore struct {
+	ignoreNextLine bool
+	ignoreThisLine bool
+	ignoreRange    bool
+}
+
+// Setup ignores for common statements, declarations.
+// For common statements:
+//
+// // leading comments
+// [STATEMENT] // trailing comments
+//
+// Then leading comments accept falco-ignore-next-line, falco-ignore-start, falco-ignore-end
+// trailing comments accept falco-ignore
+func (i *ignore) SetupStatement(meta *ast.Meta) {
+	// Find ignore signature in leading comments
+	for _, c := range meta.Leading {
+		line := strings.TrimLeft(c.String(), "#@*/ ")
+		switch {
+		case strings.HasPrefix(line, falcoIgnoreNextLine):
+			i.ignoreNextLine = true
+		case strings.HasPrefix(line, falcoIgnoreStart):
+			i.ignoreRange = true
+		case strings.HasPrefix(line, falcoIgnoreEnd):
+			i.ignoreRange = false
+		}
+	}
+
+	// Find ignore signature in trailing comments
+	for _, c := range meta.Trailing {
+		line := strings.TrimLeft(c.String(), "#@*/ ")
+		if strings.HasPrefix(line, falcoIgnoreThisLine) {
+			i.ignoreThisLine = true
+		}
+	}
+}
+
+// Clean up common statements, declarations
+func (i *ignore) TeardownStatement() {
+	i.ignoreNextLine = false
+	i.ignoreThisLine = false
+}
+
+// Block statement is special, the comment placing is following:
+//
+//	sub foo {
+//	 // leading comments
+//	 [STATEMENT]
+//	 [STATEMENT]
+//	 ...
+//	 // trailing comments
+//	}
+//
+// So we need to divide parsing leading and trailing comment by setup and teardown
+func (i *ignore) SetupBlockStatement(meta *ast.Meta) {
+	for _, c := range meta.Leading {
+		line := strings.TrimLeft(c.String(), "#@*/ ")
+		switch {
+		case strings.HasPrefix(line, falcoIgnoreNextLine):
+			i.ignoreNextLine = true
+		case strings.HasPrefix(line, falcoIgnoreStart):
+			i.ignoreRange = true
+		case strings.HasPrefix(line, falcoIgnoreEnd):
+			i.ignoreRange = false
+		}
+	}
+}
+func (i *ignore) TeardownBlockStatement(meta *ast.Meta) {
+	i.ignoreNextLine = false
+	i.ignoreThisLine = false
+
+	for _, c := range meta.Trailing {
+		line := strings.TrimLeft(c.String(), "#@*/ ")
+		if strings.HasPrefix(line, falcoIgnoreEnd) {
+			i.ignoreRange = false
+		}
+	}
+}
+
+func (i *ignore) IsEnable() bool {
+	return i.ignoreNextLine || i.ignoreThisLine || i.ignoreRange
+}
diff --git a/linter/linter.go b/linter/linter.go
index 66abe20..ffb1e0e 100644
--- a/linter/linter.go
+++ b/linter/linter.go
@@ -19,11 +19,13 @@ type Linter struct {
 	Errors         []error
 	FatalError     *FatalError
 	includexLexers map[string]*lexer.Lexer
+	ignore         *ignore
 }
 
 func New() *Linter {
 	return &Linter{
 		includexLexers: make(map[string]*lexer.Lexer),
+		ignore:         &ignore{},
 	}
 }
 
@@ -33,7 +35,9 @@ func (l *Linter) Lexers() map[string]*lexer.Lexer {
 
 func (l *Linter) Error(err error) {
 	if le, ok := err.(*LintError); ok {
-		l.Errors = append(l.Errors, le)
+		if !l.ignore.IsEnable() {
+			l.Errors = append(l.Errors, le)
+		}
 	} else {
 		l.Errors = append(l.Errors, &LintError{
 			Severity: ERROR,
@@ -268,12 +272,19 @@ func (l *Linter) lintVCL(vcl *ast.VCL, ctx *context.Context) types.Type {
 
 	// Lint each statement/declaration logics
 	for _, s := range statements {
-		l.lint(s, ctx)
+		l.lintStatement(s, ctx)
 	}
 
 	return types.NeverType
 }
 
+func (l *Linter) lintStatement(s ast.Statement, ctx *context.Context) {
+	// Any statements may have ignoring comments so we do setup and teardown
+	l.ignore.SetupStatement(s.GetMeta())
+	defer l.ignore.TeardownStatement()
+	l.lint(s, ctx)
+}
+
 func (l *Linter) loadSnippetVCL(file, content string) []ast.Statement {
 	lx := lexer.NewFromString(content, lexer.WithFile(file))
 	l.includexLexers[file] = lx
@@ -938,9 +949,16 @@ func (l *Linter) lintFastlyBoilerPlateMacro(sub *ast.SubroutineDeclaration, ctx
 }
 
 func (l *Linter) lintBlockStatement(block *ast.BlockStatement, ctx *context.Context) types.Type {
+	l.ignore.SetupBlockStatement(block.GetMeta())
+	defer l.ignore.TeardownBlockStatement(block.GetMeta())
+
 	statements := l.resolveIncludeStatements(block.Statements, ctx, false)
 	for _, stmt := range statements {
-		l.lint(stmt, ctx)
+		func(v ast.Statement, c *context.Context) {
+			l.ignore.SetupStatement(v.GetMeta())
+			defer l.ignore.TeardownStatement()
+			l.lint(v, c)
+		}(stmt, ctx)
 	}
 
 	return types.NeverType


Test patch:
diff --git a/linter/linter_test.go b/linter/linter_test.go
index 3891f64..e16d5ba 100644
--- a/linter/linter_test.go
+++ b/linter/linter_test.go
@@ -2541,7 +2541,71 @@ func TestFastlyInfoH2FingerPrintCouldLint(t *testing.T) {
 sub vcl_recv {
    #FASTLY RECV
    set req.http.H2-Fingerprint = fastly_info.h2.fingerprint;
+}`
+	assertNoError(t, input)
 }
-		`
+
+func TestIgnoreErrorNextLine(t *testing.T) {
+	input := `
+sub vcl_recv {
+   #FASTLY RECV
+   # falco-ignore-next-line
+   set req.http.H2-Fingerprint = fastly_info.h2.undefined; // undefined but ignore
+}`
+	assertNoError(t, input)
+}
+
+func TestIgnoreErrorNextLineOnly(t *testing.T) {
+	input := `
+sub vcl_recv {
+   #FASTLY RECV
+   # falco-ignore-next-line
+   set req.http.H2-Fingerprint = fastly_info.h2.undefined; // undefined but ignore
+   set req.http.H2-Fingerprint = fastly_info.h2.undefined; // raise an error
+}`
+	assertError(t, input)
+}
+
+func TestIgnoreErrorThisLine(t *testing.T) {
+	input := `
+sub vcl_recv {
+   #FASTLY RECV
+   set req.http.H2-Fingerprint = fastly_info.h2.undefined; // falco-ignore
+}`
+	assertNoError(t, input)
+}
+
+func TestIgnoreErrorStartEnd(t *testing.T) {
+	input := `
+sub vcl_recv {
+	// falco-ignore-start
+   #FASTLY RECV
+   set req.http.H2-Fingerprint = fastly_info.h2.undefined;
+	// falco-ignore-end
+   set req.http.H2-Fingerprint = fastly_info.h2.fingerprint;
+}`
+	assertNoError(t, input)
+}
+
+func TestIgnoreErrorStartEndRangeOnly(t *testing.T) {
+	input := `
+sub vcl_recv {
+	// falco-ignore-start
+   #FASTLY RECV
+   set req.http.H2-Fingerprint = fastly_info.h2.undefined;
+	// falco-ignore-end
+   set req.http.H2-Fingerprint = fastly_info.h2.undefined;
+}`
+	assertError(t, input)
+}
+
+func TestIgnoreErrorStartEndWholeDeclaration(t *testing.T) {
+	input := `
+// falco-ignore-start
+sub vcl_recv {
+   #FASTLY RECV
+   set req.http.H2-Fingerprint = fastly_info.h2.undefined;
+   set req.http.H2-Fingerprint = fastly_info.h2.fingerprint;
+}`
 	assertNoError(t, input)
 }