github.com/vanstinator/golangci-lint@v0.0.0-20240223191551-cc572f00d9d1/pkg/golinters/nolintlint/nolintlint.go (about) 1 // Package nolintlint provides a linter to ensure that all //nolint directives are followed by explanations 2 package nolintlint 3 4 import ( 5 "fmt" 6 "go/ast" 7 "go/token" 8 "regexp" 9 "strings" 10 "unicode" 11 12 "github.com/vanstinator/golangci-lint/pkg/result" 13 ) 14 15 type BaseIssue struct { 16 fullDirective string 17 directiveWithOptionalLeadingSpace string 18 position token.Position 19 replacement *result.Replacement 20 } 21 22 func (b BaseIssue) Position() token.Position { 23 return b.position 24 } 25 26 func (b BaseIssue) Replacement() *result.Replacement { 27 return b.replacement 28 } 29 30 type ExtraLeadingSpace struct { 31 BaseIssue 32 } 33 34 func (i ExtraLeadingSpace) Details() string { 35 return fmt.Sprintf("directive `%s` should not have more than one leading space", i.fullDirective) 36 } 37 38 func (i ExtraLeadingSpace) String() string { return toString(i) } 39 40 type NotMachine struct { 41 BaseIssue 42 } 43 44 func (i NotMachine) Details() string { 45 expected := i.fullDirective[:2] + strings.TrimLeftFunc(i.fullDirective[2:], unicode.IsSpace) 46 return fmt.Sprintf("directive `%s` should be written without leading space as `%s`", 47 i.fullDirective, expected) 48 } 49 50 func (i NotMachine) String() string { return toString(i) } 51 52 type NotSpecific struct { 53 BaseIssue 54 } 55 56 func (i NotSpecific) Details() string { 57 return fmt.Sprintf("directive `%s` should mention specific linter such as `%s:my-linter`", 58 i.fullDirective, i.directiveWithOptionalLeadingSpace) 59 } 60 61 func (i NotSpecific) String() string { return toString(i) } 62 63 type ParseError struct { 64 BaseIssue 65 } 66 67 func (i ParseError) Details() string { 68 return fmt.Sprintf("directive `%s` should match `%s[:<comma-separated-linters>] [// <explanation>]`", 69 i.fullDirective, 70 i.directiveWithOptionalLeadingSpace) 71 } 72 73 func (i ParseError) String() string { return toString(i) } 74 75 type NoExplanation struct { 76 BaseIssue 77 fullDirectiveWithoutExplanation string 78 } 79 80 func (i NoExplanation) Details() string { 81 return fmt.Sprintf("directive `%s` should provide explanation such as `%s // this is why`", 82 i.fullDirective, i.fullDirectiveWithoutExplanation) 83 } 84 85 func (i NoExplanation) String() string { return toString(i) } 86 87 type UnusedCandidate struct { 88 BaseIssue 89 ExpectedLinter string 90 } 91 92 func (i UnusedCandidate) Details() string { 93 details := fmt.Sprintf("directive `%s` is unused", i.fullDirective) 94 if i.ExpectedLinter != "" { 95 details += fmt.Sprintf(" for linter %q", i.ExpectedLinter) 96 } 97 return details 98 } 99 100 func (i UnusedCandidate) String() string { return toString(i) } 101 102 func toString(i Issue) string { 103 return fmt.Sprintf("%s at %s", i.Details(), i.Position()) 104 } 105 106 type Issue interface { 107 Details() string 108 Position() token.Position 109 String() string 110 Replacement() *result.Replacement 111 } 112 113 type Needs uint 114 115 const ( 116 NeedsMachineOnly Needs = 1 << iota 117 NeedsSpecific 118 NeedsExplanation 119 NeedsUnused 120 NeedsAll = NeedsMachineOnly | NeedsSpecific | NeedsExplanation 121 ) 122 123 var commentPattern = regexp.MustCompile(`^//\s*(nolint)(:\s*[\w-]+\s*(?:,\s*[\w-]+\s*)*)?\b`) 124 125 // matches a complete nolint directive 126 var fullDirectivePattern = regexp.MustCompile(`^//\s*nolint(?::(\s*[\w-]+\s*(?:,\s*[\w-]+\s*)*))?\s*(//.*)?\s*\n?$`) 127 128 type Linter struct { 129 needs Needs // indicates which linter checks to perform 130 excludeByLinter map[string]bool 131 } 132 133 // NewLinter creates a linter that enforces that the provided directives fulfill the provided requirements 134 func NewLinter(needs Needs, excludes []string) (*Linter, error) { 135 excludeByName := make(map[string]bool) 136 for _, e := range excludes { 137 excludeByName[e] = true 138 } 139 140 return &Linter{ 141 needs: needs | NeedsMachineOnly, 142 excludeByLinter: excludeByName, 143 }, nil 144 } 145 146 var leadingSpacePattern = regexp.MustCompile(`^//(\s*)`) 147 var trailingBlankExplanation = regexp.MustCompile(`\s*(//\s*)?$`) 148 149 //nolint:funlen,gocyclo 150 func (l Linter) Run(fset *token.FileSet, nodes ...ast.Node) ([]Issue, error) { 151 var issues []Issue 152 153 for _, node := range nodes { 154 file, ok := node.(*ast.File) 155 if !ok { 156 continue 157 } 158 159 for _, c := range file.Comments { 160 for _, comment := range c.List { 161 if !commentPattern.MatchString(comment.Text) { 162 continue 163 } 164 165 // check for a space between the "//" and the directive 166 leadingSpaceMatches := leadingSpacePattern.FindStringSubmatch(comment.Text) 167 168 var leadingSpace string 169 if len(leadingSpaceMatches) > 0 { 170 leadingSpace = leadingSpaceMatches[1] 171 } 172 173 directiveWithOptionalLeadingSpace := "//" 174 if leadingSpace != "" { 175 directiveWithOptionalLeadingSpace += " " 176 } 177 178 split := strings.Split(strings.SplitN(comment.Text, ":", 2)[0], "//") 179 directiveWithOptionalLeadingSpace += strings.TrimSpace(split[1]) 180 181 pos := fset.Position(comment.Pos()) 182 end := fset.Position(comment.End()) 183 184 base := BaseIssue{ 185 fullDirective: comment.Text, 186 directiveWithOptionalLeadingSpace: directiveWithOptionalLeadingSpace, 187 position: pos, 188 } 189 190 // check for, report and eliminate leading spaces, so we can check for other issues 191 if leadingSpace != "" { 192 removeWhitespace := &result.Replacement{ 193 Inline: &result.InlineFix{ 194 StartCol: pos.Column + 1, 195 Length: len(leadingSpace), 196 NewString: "", 197 }, 198 } 199 if (l.needs & NeedsMachineOnly) != 0 { 200 issue := NotMachine{BaseIssue: base} 201 issue.BaseIssue.replacement = removeWhitespace 202 issues = append(issues, issue) 203 } else if len(leadingSpace) > 1 { 204 issue := ExtraLeadingSpace{BaseIssue: base} 205 issue.BaseIssue.replacement = removeWhitespace 206 issue.BaseIssue.replacement.Inline.NewString = " " // assume a single space was intended 207 issues = append(issues, issue) 208 } 209 } 210 211 fullMatches := fullDirectivePattern.FindStringSubmatch(comment.Text) 212 if len(fullMatches) == 0 { 213 issues = append(issues, ParseError{BaseIssue: base}) 214 continue 215 } 216 217 lintersText, explanation := fullMatches[1], fullMatches[2] 218 219 var linters []string 220 if lintersText != "" && !strings.HasPrefix(lintersText, "all") { 221 lls := strings.Split(lintersText, ",") 222 linters = make([]string, 0, len(lls)) 223 rangeStart := (pos.Column - 1) + len("//") + len(leadingSpace) + len("nolint:") 224 for i, ll := range lls { 225 rangeEnd := rangeStart + len(ll) 226 if i < len(lls)-1 { 227 rangeEnd++ // include trailing comma 228 } 229 trimmedLinterName := strings.TrimSpace(ll) 230 if trimmedLinterName != "" { 231 linters = append(linters, trimmedLinterName) 232 } 233 rangeStart = rangeEnd 234 } 235 } 236 237 if (l.needs & NeedsSpecific) != 0 { 238 if len(linters) == 0 { 239 issues = append(issues, NotSpecific{BaseIssue: base}) 240 } 241 } 242 243 // when detecting unused directives, we send all the directives through and filter them out in the nolint processor 244 if (l.needs & NeedsUnused) != 0 { 245 removeNolintCompletely := &result.Replacement{ 246 Inline: &result.InlineFix{ 247 StartCol: pos.Column - 1, 248 Length: end.Column - pos.Column, 249 NewString: "", 250 }, 251 } 252 253 if len(linters) == 0 { 254 issue := UnusedCandidate{BaseIssue: base} 255 issue.replacement = removeNolintCompletely 256 issues = append(issues, issue) 257 } else { 258 for _, linter := range linters { 259 issue := UnusedCandidate{BaseIssue: base, ExpectedLinter: linter} 260 // only offer replacement if there is a single linter 261 // because of issues around commas and the possibility of all 262 // linters being removed 263 if len(linters) == 1 { 264 issue.replacement = removeNolintCompletely 265 } 266 issues = append(issues, issue) 267 } 268 } 269 } 270 271 if (l.needs&NeedsExplanation) != 0 && (explanation == "" || strings.TrimSpace(explanation) == "//") { 272 needsExplanation := len(linters) == 0 // if no linters are mentioned, we must have explanation 273 // otherwise, check if we are excluding all the mentioned linters 274 for _, ll := range linters { 275 if !l.excludeByLinter[ll] { // if a linter does require explanation 276 needsExplanation = true 277 break 278 } 279 } 280 281 if needsExplanation { 282 fullDirectiveWithoutExplanation := trailingBlankExplanation.ReplaceAllString(comment.Text, "") 283 issues = append(issues, NoExplanation{ 284 BaseIssue: base, 285 fullDirectiveWithoutExplanation: fullDirectiveWithoutExplanation, 286 }) 287 } 288 } 289 } 290 } 291 } 292 293 return issues, nil 294 }