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