github.com/elek/golangci-lint@v1.42.2-0.20211208090441-c05b7fcb3a9a/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/elek/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, 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 := comment.Text 188 if len(leadingSpace) > 0 { 189 split := strings.Split(strings.SplitN(comment.Text, ":", 2)[0], "//") 190 directiveWithOptionalLeadingSpace = "// " + strings.TrimSpace(split[1]) 191 } 192 193 pos := fset.Position(comment.Pos()) 194 end := fset.Position(comment.End()) 195 196 base := BaseIssue{ 197 fullDirective: comment.Text, 198 directiveWithOptionalLeadingSpace: directiveWithOptionalLeadingSpace, 199 position: pos, 200 } 201 202 // check for, report and eliminate leading spaces so we can check for other issues 203 if len(leadingSpace) > 0 { 204 removeWhitespace := &result.Replacement{ 205 Inline: &result.InlineFix{ 206 StartCol: pos.Column + 1, 207 Length: len(leadingSpace), 208 NewString: "", 209 }, 210 } 211 if (l.needs & NeedsMachineOnly) != 0 { 212 issue := NotMachine{BaseIssue: base} 213 issue.BaseIssue.replacement = removeWhitespace 214 issues = append(issues, issue) 215 } else if len(leadingSpace) > 1 { 216 issue := ExtraLeadingSpace{BaseIssue: base} 217 issue.BaseIssue.replacement = removeWhitespace 218 issue.BaseIssue.replacement.Inline.NewString = " " // assume a single space was intended 219 issues = append(issues, issue) 220 } 221 } 222 223 fullMatches := fullDirectivePattern.FindStringSubmatch(comment.Text) 224 if len(fullMatches) == 0 { 225 issues = append(issues, ParseError{BaseIssue: base}) 226 continue 227 } 228 229 lintersText, explanation := fullMatches[1], fullMatches[2] 230 var linters []string 231 if len(lintersText) > 0 { 232 lls := strings.Split(lintersText, ",") 233 linters = make([]string, 0, len(lls)) 234 rangeStart := (pos.Column - 1) + len("//") + len(leadingSpace) + len("nolint:") 235 for i, ll := range lls { 236 rangeEnd := rangeStart + len(ll) 237 if i < len(lls)-1 { 238 rangeEnd++ // include trailing comma 239 } 240 trimmedLinterName := strings.TrimSpace(ll) 241 if trimmedLinterName != "" { 242 linters = append(linters, trimmedLinterName) 243 } 244 rangeStart = rangeEnd 245 } 246 } 247 248 if (l.needs & NeedsSpecific) != 0 { 249 if len(linters) == 0 { 250 issues = append(issues, NotSpecific{BaseIssue: base}) 251 } 252 } 253 254 // when detecting unused directives, we send all the directives through and filter them out in the nolint processor 255 if (l.needs & NeedsUnused) != 0 { 256 removeNolintCompletely := &result.Replacement{ 257 Inline: &result.InlineFix{ 258 StartCol: pos.Column - 1, 259 Length: end.Column - pos.Column, 260 NewString: "", 261 }, 262 } 263 264 if len(linters) == 0 { 265 issue := UnusedCandidate{BaseIssue: base} 266 issue.replacement = removeNolintCompletely 267 issues = append(issues, issue) 268 } else { 269 for _, linter := range linters { 270 issue := UnusedCandidate{BaseIssue: base, ExpectedLinter: linter} 271 // only offer replacement if there is a single linter 272 // because of issues around commas and the possibility of all 273 // linters being removed 274 if len(linters) == 1 { 275 issue.replacement = removeNolintCompletely 276 } 277 issues = append(issues, issue) 278 } 279 } 280 } 281 282 if (l.needs&NeedsExplanation) != 0 && (explanation == "" || strings.TrimSpace(explanation) == "//") { 283 needsExplanation := len(linters) == 0 // if no linters are mentioned, we must have explanation 284 // otherwise, check if we are excluding all of the mentioned linters 285 for _, ll := range linters { 286 if !l.excludeByLinter[ll] { // if a linter does require explanation 287 needsExplanation = true 288 break 289 } 290 } 291 292 if needsExplanation { 293 fullDirectiveWithoutExplanation := trailingBlankExplanation.ReplaceAllString(comment.Text, "") 294 issues = append(issues, NoExplanation{ 295 BaseIssue: base, 296 fullDirectiveWithoutExplanation: fullDirectiveWithoutExplanation, 297 }) 298 } 299 } 300 } 301 } 302 } 303 304 return issues, nil 305 }