Skip to content

Commit 262b22d

Browse files
authored
feat: suppress errors/warnings/diagnostics via comments (#385)
1 parent 7d91c41 commit 262b22d

File tree

7 files changed

+1068
-0
lines changed

7 files changed

+1068
-0
lines changed

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,18 @@ CircleCI Configuration files. It offers:
103103
<img src="https://images.ctfassets.net/il1yandlcjgk/3jXaQvhOQgayhV9O4nfAhZ/b6d55e689ddfcf7673ab7e9b76ba0a53/config_helper_autocomplete.png" alt="circleci-vscode-autocomplete" width="50%"/>
104104
</p>
105105

106+
- **Diagnostic suppression** - allows you to suppress specific warnings and errors using special comments:
107+
- `# cci-ignore` - suppress diagnostic on the same line
108+
- `# cci-ignore-next-line` - suppress diagnostic on the next line
109+
- `# cci-ignore-start` / `# cci-ignore-end` - suppress diagnostics in a range
110+
- `# cci-ignore-file` - suppress all diagnostics in the file
111+
112+
Quick-fix code actions are available to automatically insert suppression comments.
113+
114+
<p align="center">
115+
<img src="https://images.ctfassets.net/il1yandlcjgk/2HsFnWKVRDavrKYN6gns6T/f30a25920e1a0c47fc7beaa2e81b93a0/cci-ignore-next-line.gif" alt="circleci-ignore-comments" width="50%"/>
116+
</p>
117+
106118
## <a name="platforms"></a>Platforms, Deployment and Package Managers
107119

108120
The tool is deployed through

pkg/parser/suppression.go

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
package parser
2+
3+
import (
4+
"regexp"
5+
6+
"github.com/CircleCI-Public/circleci-yaml-language-server/pkg/utils"
7+
sitter "github.com/smacker/go-tree-sitter"
8+
"go.lsp.dev/protocol"
9+
)
10+
11+
type SuppressionInfo struct {
12+
FileWideSuppression bool
13+
SuppressedLines map[uint32]bool
14+
SuppressedRanges []SuppressionRange
15+
}
16+
17+
type SuppressionRange struct {
18+
StartLine uint32
19+
EndLine uint32
20+
}
21+
22+
var (
23+
ignoreFileRegex = regexp.MustCompile(`^\s*#\s*cci-ignore-file\s*$`)
24+
ignoreInlineRegex = regexp.MustCompile(`#\s*cci-ignore\s*$`)
25+
ignoreNextLineRegex = regexp.MustCompile(`^\s*#\s*cci-ignore-next-line\s*$`)
26+
ignoreRangeStartRegex = regexp.MustCompile(`^\s*#\s*cci-ignore-start\s*$`)
27+
ignoreRangeEndRegex = regexp.MustCompile(`^\s*#\s*cci-ignore-end\s*$`)
28+
)
29+
30+
func ParseSuppressionComments(doc *YamlDocument) *SuppressionInfo {
31+
rootNode := doc.RootNode
32+
suppressionInfo := &SuppressionInfo{
33+
FileWideSuppression: false,
34+
SuppressedLines: make(map[uint32]bool),
35+
SuppressedRanges: []SuppressionRange{},
36+
}
37+
38+
isRangeOpen := false
39+
suppressionRange := SuppressionRange{}
40+
41+
// fetch all comments via tree-sitter and build up the suppression info
42+
ExecQuery(rootNode, "(comment) @comment", func(match *sitter.QueryMatch) {
43+
for _, capture := range match.Captures {
44+
node := capture.Node
45+
commentText := doc.GetNodeText(node)
46+
47+
// cci-ignore-file
48+
if ignoreFileRegex.MatchString(commentText) {
49+
suppressionInfo.FileWideSuppression = true
50+
return
51+
}
52+
53+
// cci-ignore
54+
if ignoreInlineRegex.MatchString(commentText) {
55+
line := doc.NodeToRange(node).Start.Line
56+
suppressionInfo.SuppressedLines[line] = true
57+
}
58+
59+
// cci-ignore-next-line
60+
if ignoreNextLineRegex.MatchString(commentText) {
61+
line := doc.NodeToRange(node).Start.Line
62+
suppressionInfo.SuppressedLines[line+1] = true
63+
}
64+
65+
// cci-ignore-start
66+
if ignoreRangeStartRegex.MatchString(commentText) {
67+
if isRangeOpen {
68+
diagnostic := utils.CreateErrorDiagnosticFromNode(node, "cci-ignore-start must have a closing cci-ignore-end before trying to open a new ignore-range")
69+
doc.addDiagnostic(diagnostic)
70+
return
71+
}
72+
isRangeOpen = true
73+
suppressionRange.StartLine = doc.NodeToRange(node).Start.Line
74+
}
75+
76+
// cci-ignore-end
77+
if ignoreRangeEndRegex.MatchString(commentText) {
78+
if !isRangeOpen {
79+
diagnostic := utils.CreateErrorDiagnosticFromNode(node, "cci-ignore-end must have an opening cci-ignore-start")
80+
doc.addDiagnostic(diagnostic)
81+
return
82+
}
83+
isRangeOpen = false
84+
suppressionRange.EndLine = doc.NodeToRange(node).Start.Line
85+
suppressionInfo.SuppressedRanges = append(suppressionInfo.SuppressedRanges, suppressionRange)
86+
}
87+
}
88+
})
89+
90+
// Check if a range was left open without closing
91+
if isRangeOpen {
92+
// Create diagnostic at the cci-ignore-start line
93+
diagnostic := protocol.Diagnostic{
94+
Range: protocol.Range{
95+
Start: protocol.Position{Line: suppressionRange.StartLine, Character: 0},
96+
End: protocol.Position{Line: suppressionRange.StartLine, Character: 100},
97+
},
98+
Severity: protocol.DiagnosticSeverityError,
99+
Source: "circleci",
100+
Message: "cci-ignore-start is missing a closing cci-ignore-end",
101+
}
102+
doc.addDiagnostic(diagnostic)
103+
}
104+
105+
return suppressionInfo
106+
}
107+
108+
// isDiagnosticSuppressed returns true if the diagnostic is suppressed by any one of the cci-ignore comments in the file
109+
func isDiagnosticSuppressed(suppressionInfo *SuppressionInfo, diagnostic protocol.Diagnostic) bool {
110+
if suppressionInfo == nil {
111+
return false
112+
}
113+
114+
if suppressionInfo.FileWideSuppression {
115+
return true
116+
}
117+
118+
// NOTE: for a multi-line diagnostic, having `# cci-ignore-next-line` before it would ignore the whole diagnostic
119+
if suppressionInfo.SuppressedLines[diagnostic.Range.Start.Line] {
120+
return true
121+
}
122+
123+
for _, suppressionRange := range suppressionInfo.SuppressedRanges {
124+
if diagnosticOverlapsRange(suppressionRange, diagnostic) {
125+
return true
126+
}
127+
}
128+
129+
return false
130+
}
131+
132+
// diagnosticOverlapsRange returns true if the diagnostic's line range overlaps with the cci-ignore suppression range
133+
func diagnosticOverlapsRange(r SuppressionRange, diagnostic protocol.Diagnostic) bool {
134+
return r.StartLine <= diagnostic.Range.Start.Line && r.EndLine >= diagnostic.Range.Start.Line
135+
}
136+
137+
// FilterSuppressedDiagnostics returns a new slice of diagnostics that are not suppressed by any of the cci-ignore comments in the file
138+
func FilterSuppressedDiagnostics(diagnostics []protocol.Diagnostic, suppression *SuppressionInfo) []protocol.Diagnostic {
139+
remainingDiagnostics := []protocol.Diagnostic{}
140+
141+
for _, diagnostic := range diagnostics {
142+
if !isDiagnosticSuppressed(suppression, diagnostic) {
143+
remainingDiagnostics = append(remainingDiagnostics, diagnostic)
144+
}
145+
}
146+
147+
return remainingDiagnostics
148+
}

0 commit comments

Comments
 (0)