github.com/vanstinator/golangci-lint@v0.0.0-20240223191551-cc572f00d9d1/pkg/golinters/lll.go (about) 1 package golinters 2 3 import ( 4 "bufio" 5 "errors" 6 "fmt" 7 "go/token" 8 "os" 9 "strings" 10 "sync" 11 "unicode/utf8" 12 13 "golang.org/x/tools/go/analysis" 14 15 "github.com/vanstinator/golangci-lint/pkg/config" 16 "github.com/vanstinator/golangci-lint/pkg/golinters/goanalysis" 17 "github.com/vanstinator/golangci-lint/pkg/lint/linter" 18 "github.com/vanstinator/golangci-lint/pkg/result" 19 ) 20 21 const lllName = "lll" 22 23 const goCommentDirectivePrefix = "//go:" 24 25 //nolint:dupl 26 func NewLLL(settings *config.LllSettings) *goanalysis.Linter { 27 var mu sync.Mutex 28 var resIssues []goanalysis.Issue 29 30 analyzer := &analysis.Analyzer{ 31 Name: lllName, 32 Doc: goanalysis.TheOnlyanalyzerDoc, 33 Run: func(pass *analysis.Pass) (any, error) { 34 issues, err := runLll(pass, settings) 35 if err != nil { 36 return nil, err 37 } 38 39 if len(issues) == 0 { 40 return nil, nil 41 } 42 43 mu.Lock() 44 resIssues = append(resIssues, issues...) 45 mu.Unlock() 46 47 return nil, nil 48 }, 49 } 50 51 return goanalysis.NewLinter( 52 lllName, 53 "Reports long lines", 54 []*analysis.Analyzer{analyzer}, 55 nil, 56 ).WithIssuesReporter(func(*linter.Context) []goanalysis.Issue { 57 return resIssues 58 }).WithLoadMode(goanalysis.LoadModeSyntax) 59 } 60 61 func runLll(pass *analysis.Pass, settings *config.LllSettings) ([]goanalysis.Issue, error) { 62 fileNames := getFileNames(pass) 63 64 spaces := strings.Repeat(" ", settings.TabWidth) 65 66 var issues []goanalysis.Issue 67 for _, f := range fileNames { 68 lintIssues, err := getLLLIssuesForFile(f, settings.LineLength, spaces) 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 getLLLIssuesForFile(filename string, maxLineLen int, tabSpaces string) ([]result.Issue, error) { 82 var res []result.Issue 83 84 f, err := os.Open(filename) 85 if err != nil { 86 return nil, fmt.Errorf("can't open file %s: %w", filename, err) 87 } 88 defer f.Close() 89 90 lineNumber := 0 91 multiImportEnabled := false 92 93 scanner := bufio.NewScanner(f) 94 for scanner.Scan() { 95 lineNumber++ 96 97 line := scanner.Text() 98 line = strings.ReplaceAll(line, "\t", tabSpaces) 99 100 if strings.HasPrefix(line, goCommentDirectivePrefix) { 101 continue 102 } 103 104 if strings.HasPrefix(line, "import") { 105 multiImportEnabled = strings.HasSuffix(line, "(") 106 continue 107 } 108 109 if multiImportEnabled { 110 if line == ")" { 111 multiImportEnabled = false 112 } 113 114 continue 115 } 116 117 lineLen := utf8.RuneCountInString(line) 118 if lineLen > maxLineLen { 119 res = append(res, result.Issue{ 120 Pos: token.Position{ 121 Filename: filename, 122 Line: lineNumber, 123 }, 124 Text: fmt.Sprintf("line is %d characters", lineLen), 125 FromLinter: lllName, 126 }) 127 } 128 } 129 130 if err := scanner.Err(); err != nil { 131 if errors.Is(err, bufio.ErrTooLong) && maxLineLen < bufio.MaxScanTokenSize { 132 // scanner.Scan() might fail if the line is longer than bufio.MaxScanTokenSize 133 // In the case where the specified maxLineLen is smaller than bufio.MaxScanTokenSize 134 // we can return this line as a long line instead of returning an error. 135 // The reason for this change is that this case might happen with autogenerated files 136 // The go-bindata tool for instance might generate a file with a very long line. 137 // In this case, as it's an auto generated file, the warning returned by lll will 138 // be ignored. 139 // But if we return a linter error here, and this error happens for an autogenerated 140 // file the error will be discarded (fine), but all the subsequent errors for lll will 141 // be discarded for other files, and we'll miss legit error. 142 res = append(res, result.Issue{ 143 Pos: token.Position{ 144 Filename: filename, 145 Line: lineNumber, 146 Column: 1, 147 }, 148 Text: fmt.Sprintf("line is more than %d characters", bufio.MaxScanTokenSize), 149 FromLinter: lllName, 150 }) 151 } else { 152 return nil, fmt.Errorf("can't scan file %s: %w", filename, err) 153 } 154 } 155 156 return res, nil 157 }