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  }