github.com/vanstinator/golangci-lint@v0.0.0-20240223191551-cc572f00d9d1/pkg/golinters/misspell.go (about) 1 package golinters 2 3 import ( 4 "fmt" 5 "go/token" 6 "strings" 7 "sync" 8 9 "github.com/golangci/misspell" 10 "golang.org/x/tools/go/analysis" 11 12 "github.com/vanstinator/golangci-lint/pkg/config" 13 "github.com/vanstinator/golangci-lint/pkg/golinters/goanalysis" 14 "github.com/vanstinator/golangci-lint/pkg/lint/linter" 15 "github.com/vanstinator/golangci-lint/pkg/result" 16 ) 17 18 const misspellName = "misspell" 19 20 func NewMisspell(settings *config.MisspellSettings) *goanalysis.Linter { 21 var mu sync.Mutex 22 var resIssues []goanalysis.Issue 23 24 analyzer := &analysis.Analyzer{ 25 Name: misspellName, 26 Doc: goanalysis.TheOnlyanalyzerDoc, 27 Run: goanalysis.DummyRun, 28 } 29 30 return goanalysis.NewLinter( 31 misspellName, 32 "Finds commonly misspelled English words", 33 []*analysis.Analyzer{analyzer}, 34 nil, 35 ).WithContextSetter(func(lintCtx *linter.Context) { 36 replacer, ruleErr := createMisspellReplacer(settings) 37 38 analyzer.Run = func(pass *analysis.Pass) (any, error) { 39 if ruleErr != nil { 40 return nil, ruleErr 41 } 42 43 issues, err := runMisspell(lintCtx, pass, replacer, settings.Mode) 44 if err != nil { 45 return nil, err 46 } 47 48 if len(issues) == 0 { 49 return nil, nil 50 } 51 52 mu.Lock() 53 resIssues = append(resIssues, issues...) 54 mu.Unlock() 55 56 return nil, nil 57 } 58 }).WithIssuesReporter(func(*linter.Context) []goanalysis.Issue { 59 return resIssues 60 }).WithLoadMode(goanalysis.LoadModeSyntax) 61 } 62 63 func runMisspell(lintCtx *linter.Context, pass *analysis.Pass, replacer *misspell.Replacer, mode string) ([]goanalysis.Issue, error) { 64 fileNames := getFileNames(pass) 65 66 var issues []goanalysis.Issue 67 for _, filename := range fileNames { 68 lintIssues, err := runMisspellOnFile(lintCtx, filename, replacer, mode) 69 if err != nil { 70 return nil, err 71 } 72 73 for i := range lintIssues { 74 issues = append(issues, goanalysis.NewIssue(&lintIssues[i], pass)) 75 } 76 } 77 78 return issues, nil 79 } 80 81 func createMisspellReplacer(settings *config.MisspellSettings) (*misspell.Replacer, error) { 82 replacer := &misspell.Replacer{ 83 Replacements: misspell.DictMain, 84 } 85 86 // Figure out regional variations 87 switch strings.ToUpper(settings.Locale) { 88 case "": 89 // nothing 90 case "US": 91 replacer.AddRuleList(misspell.DictAmerican) 92 case "UK", "GB": 93 replacer.AddRuleList(misspell.DictBritish) 94 case "NZ", "AU", "CA": 95 return nil, fmt.Errorf("unknown locale: %q", settings.Locale) 96 } 97 98 if len(settings.IgnoreWords) != 0 { 99 replacer.RemoveRule(settings.IgnoreWords) 100 } 101 102 // It can panic. 103 replacer.Compile() 104 105 return replacer, nil 106 } 107 108 func runMisspellOnFile(lintCtx *linter.Context, filename string, replacer *misspell.Replacer, mode string) ([]result.Issue, error) { 109 fileContent, err := lintCtx.FileCache.GetFileBytes(filename) 110 if err != nil { 111 return nil, fmt.Errorf("can't get file %s contents: %w", filename, err) 112 } 113 114 // `r.ReplaceGo` doesn't find issues inside strings: it searches only inside comments. 115 // `r.Replace` searches all words: it treats input as a plain text. 116 // The standalone misspell tool uses `r.Replace` by default. 117 var replace func(input string) (string, []misspell.Diff) 118 switch strings.ToLower(mode) { 119 case "restricted": 120 replace = replacer.ReplaceGo 121 default: 122 replace = replacer.Replace 123 } 124 125 _, diffs := replace(string(fileContent)) 126 127 var res []result.Issue 128 129 for _, diff := range diffs { 130 text := fmt.Sprintf("`%s` is a misspelling of `%s`", diff.Original, diff.Corrected) 131 132 pos := token.Position{ 133 Filename: filename, 134 Line: diff.Line, 135 Column: diff.Column + 1, 136 } 137 138 replacement := &result.Replacement{ 139 Inline: &result.InlineFix{ 140 StartCol: diff.Column, 141 Length: len(diff.Original), 142 NewString: diff.Corrected, 143 }, 144 } 145 146 res = append(res, result.Issue{ 147 Pos: pos, 148 Text: text, 149 FromLinter: misspellName, 150 Replacement: replacement, 151 }) 152 } 153 154 return res, nil 155 }