Skip to content

Commit e96424c

Browse files
committed
makefile+tools: add custom 'll' linter for extended line length checks
- Replace the default `lll` with a custom `ll` linter, enabling configurable exclusions for specific `S` log lines. - Integrate custom `ll` linter into the build system and `Makefile`. - Include relevant test cases and configuration for `golangci-lint`.
1 parent 6e6b552 commit e96424c

File tree

9 files changed

+474
-3
lines changed

9 files changed

+474
-3
lines changed

.custom-gcl.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
version: v1.64.6
2+
plugins:
3+
- module: 'github.com/lightninglabs/lightning-terminal/tools/linters'
4+
path: ./tools/linters

.golangci.yml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,17 @@ run:
1414
- dev
1515

1616
linters-settings:
17+
custom:
18+
ll:
19+
type: "module"
20+
description: "Custom lll linter with 'S' log line exclusion."
21+
settings:
22+
# Max line length, lines longer will be reported.
23+
line-length: 80
24+
# Tab width in spaces.
25+
tab-width: 8
26+
# The regex that we will use to detect the start of an `S` log line.
27+
log-regex: "^\\s*.*(L|l)og\\.(Info|Debug|Trace|Warn|Error|Critical)S\\("
1728
govet:
1829
# Don't report about shadowed variables
1930
check-shadowing: false
@@ -39,7 +50,7 @@ linters-settings:
3950

4051
linters:
4152
enable:
42-
- lll
53+
- ll
4354
- gofmt
4455
- tagliatelle
4556
- whitespace

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,7 @@ check-go-version: check-go-version-dockerfile check-go-version-yaml
304304

305305
lint: check-go-version docker-tools
306306
@$(call print, "Linting source.")
307-
$(DOCKER_TOOLS) golangci-lint run -v $(LINT_WORKERS)
307+
$(DOCKER_TOOLS) custom-gcl run -v $(LINT_WORKERS)
308308

309309
mod:
310310
@$(call print, "Tidying modules.")

tools/.custom-gcl.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
version: v1.64.6
2+
plugins:
3+
- module: 'github.com/lightninglabs/lightning-terminal/tools/linters'
4+
path: ./linters

tools/Dockerfile

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ RUN cd /tmp \
1111
&& mkdir -p /tmp/build/.cache \
1212
&& mkdir -p /tmp/build/.modcache \
1313
&& cd /tmp/tools \
14-
&& go install -trimpath github.com/golangci/golangci-lint/cmd/golangci-lint \
14+
&& CGO_ENABLED=0 go install -trimpath github.com/golangci/golangci-lint/cmd/golangci-lint \
15+
&& CGO_ENABLED=0 golangci-lint custom \
16+
&& mv ./custom-gcl /usr/local/bin/custom-gcl \
1517
&& chmod -R 777 /tmp/build/
1618

1719
WORKDIR /build

tools/linters/go.mod

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
module github.com/lightninglabs/lightning-terminal/tools/linters
2+
3+
go 1.24.9
4+
5+
require (
6+
github.com/golangci/plugin-module-register v0.1.1
7+
github.com/stretchr/testify v1.10.0
8+
golang.org/x/tools v0.30.0
9+
)
10+
11+
require (
12+
github.com/davecgh/go-spew v1.1.1 // indirect
13+
github.com/pmezard/go-difflib v1.0.0 // indirect
14+
gopkg.in/yaml.v3 v3.0.1 // indirect
15+
)

tools/linters/go.sum

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
2+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3+
github.com/golangci/plugin-module-register v0.1.1 h1:TCmesur25LnyJkpsVrupv1Cdzo+2f7zX0H6Jkw1Ol6c=
4+
github.com/golangci/plugin-module-register v0.1.1/go.mod h1:TTpqoB6KkwOJMV8u7+NyXMrkwwESJLOkfl9TxR1DGFc=
5+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
6+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
7+
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
8+
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
9+
golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY=
10+
golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY=
11+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
12+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
13+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
14+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

tools/linters/ll.go

Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
// The following code is based on code from GolangCI.
2+
// Source: https://github.com/golangci-lint/pkg/golinters/lll/lll.go
3+
// License: GNU
4+
5+
package linters
6+
7+
import (
8+
"bufio"
9+
"errors"
10+
"fmt"
11+
"go/ast"
12+
"go/token"
13+
"os"
14+
"path/filepath"
15+
"regexp"
16+
"strings"
17+
"unicode/utf8"
18+
19+
"github.com/golangci/plugin-module-register/register"
20+
"golang.org/x/tools/go/analysis"
21+
)
22+
23+
const (
24+
linterName = "ll"
25+
goCommentDirectivePrefix = "//go:"
26+
27+
defaultMaxLineLen = 80
28+
defaultTabWidthInSpaces = 8
29+
defaultLogRegex = `^\s*.*(L|l)og\.`
30+
)
31+
32+
// LLConfig is the configuration for the ll linter.
33+
type LLConfig struct {
34+
LineLength int `json:"line-length"`
35+
TabWidth int `json:"tab-width"`
36+
LogRegex string `json:"log-regex"`
37+
}
38+
39+
// New creates a new LLPlugin from the given settings. It satisfies the
40+
// signature required by the golangci-lint linter for plugins.
41+
func New(settings any) (register.LinterPlugin, error) {
42+
cfg, err := register.DecodeSettings[LLConfig](settings)
43+
if err != nil {
44+
return nil, err
45+
}
46+
47+
// Fill in default config values if they are not set.
48+
if cfg.LineLength == 0 {
49+
cfg.LineLength = defaultMaxLineLen
50+
}
51+
if cfg.TabWidth == 0 {
52+
cfg.TabWidth = defaultTabWidthInSpaces
53+
}
54+
if cfg.LogRegex == "" {
55+
cfg.LogRegex = defaultLogRegex
56+
}
57+
58+
return &LLPlugin{cfg: cfg}, nil
59+
}
60+
61+
// LLPlugin is a golangci-linter plugin that can be used to check that code line
62+
// lengths do not exceed a certain limit.
63+
type LLPlugin struct {
64+
cfg LLConfig
65+
}
66+
67+
// BuildAnalyzers creates the analyzers for the ll linter.
68+
//
69+
// NOTE: This is part of the register.LinterPlugin interface.
70+
func (l *LLPlugin) BuildAnalyzers() ([]*analysis.Analyzer, error) {
71+
return []*analysis.Analyzer{
72+
{
73+
Name: linterName,
74+
Doc: "Reports long lines",
75+
Run: l.run,
76+
},
77+
}, nil
78+
}
79+
80+
// GetLoadMode returns the load mode for the ll linter.
81+
//
82+
// NOTE: This is part of the register.LinterPlugin interface.
83+
func (l *LLPlugin) GetLoadMode() string {
84+
return register.LoadModeSyntax
85+
}
86+
87+
func (l *LLPlugin) run(pass *analysis.Pass) (any, error) {
88+
var (
89+
spaces = strings.Repeat(" ", l.cfg.TabWidth)
90+
logRegex = regexp.MustCompile(l.cfg.LogRegex)
91+
)
92+
93+
for _, f := range pass.Files {
94+
fileName := getFileName(pass, f)
95+
96+
issues, err := getLLLIssuesForFile(
97+
fileName, l.cfg.LineLength, spaces, logRegex,
98+
)
99+
if err != nil {
100+
return nil, err
101+
}
102+
103+
file := pass.Fset.File(f.Pos())
104+
for _, issue := range issues {
105+
pos := file.LineStart(issue.pos.Line)
106+
107+
pass.Report(analysis.Diagnostic{
108+
Pos: pos,
109+
End: 0,
110+
Category: linterName,
111+
Message: issue.text,
112+
})
113+
}
114+
115+
}
116+
117+
return nil, nil
118+
}
119+
120+
type issue struct {
121+
pos token.Position
122+
text string
123+
}
124+
125+
func getLLLIssuesForFile(filename string, maxLineLen int,
126+
tabSpaces string, logRegex *regexp.Regexp) ([]*issue, error) {
127+
128+
f, err := os.Open(filename)
129+
if err != nil {
130+
return nil, fmt.Errorf("can't open file %s: %w", filename, err)
131+
}
132+
defer f.Close()
133+
134+
var (
135+
res []*issue
136+
lineNumber int
137+
multiImportEnabled bool
138+
multiLinedLog bool
139+
)
140+
141+
// Scan over each line.
142+
scanner := bufio.NewScanner(f)
143+
for scanner.Scan() {
144+
lineNumber++
145+
146+
// Replace all tabs with spaces.
147+
line := scanner.Text()
148+
line = strings.ReplaceAll(line, "\t", tabSpaces)
149+
150+
// Ignore any //go: directives since these cant be wrapped onto
151+
// a new line.
152+
if strings.HasPrefix(line, goCommentDirectivePrefix) {
153+
continue
154+
}
155+
156+
// We never want the linter to run on imports since these cannot
157+
// be wrapped onto a new line. If this is a single line import
158+
// we can skip the line entirely. If this is a multi-line import
159+
// skip until the closing bracket.
160+
//
161+
// NOTE: We trim the line space around the line here purely for
162+
// the purpose of being able to test this part of the linter
163+
// without the risk of the `gosimports` tool reformatting the
164+
// test case and removing the import.
165+
if strings.HasPrefix(strings.TrimSpace(line), "import") {
166+
multiImportEnabled = strings.HasSuffix(line, "(")
167+
continue
168+
}
169+
170+
// If we have marked the start of a multi-line import, we should
171+
// skip until the closing bracket of the import block.
172+
if multiImportEnabled {
173+
if line == ")" {
174+
multiImportEnabled = false
175+
}
176+
177+
continue
178+
}
179+
180+
// Check if the line matches the log pattern.
181+
if logRegex.MatchString(line) {
182+
multiLinedLog = !strings.HasSuffix(line, ")")
183+
continue
184+
}
185+
186+
if multiLinedLog {
187+
// Check for the end of a multiline log call.
188+
if strings.HasSuffix(line, ")") {
189+
multiLinedLog = false
190+
}
191+
192+
continue
193+
}
194+
195+
// Otherwise, we can check the length of the line and report if
196+
// it exceeds the maximum line length.
197+
lineLen := utf8.RuneCountInString(line)
198+
if lineLen > maxLineLen {
199+
res = append(res, &issue{
200+
pos: token.Position{
201+
Filename: filename,
202+
Line: lineNumber,
203+
},
204+
text: fmt.Sprintf("the line is %d "+
205+
"characters long, which exceeds the "+
206+
"maximum of %d characters.", lineLen,
207+
maxLineLen),
208+
})
209+
}
210+
}
211+
212+
if err := scanner.Err(); err != nil {
213+
if errors.Is(err, bufio.ErrTooLong) &&
214+
maxLineLen < bufio.MaxScanTokenSize {
215+
216+
// scanner.Scan() might fail if the line is longer than
217+
// bufio.MaxScanTokenSize. In the case where the
218+
// specified maxLineLen is smaller than
219+
// bufio.MaxScanTokenSize we can return this line as a
220+
// long line instead of returning an error. The reason
221+
// for this change is that this case might happen with
222+
// autogenerated files. The go-bindata tool for instance
223+
// might generate a file with a very long line. In this
224+
// case, as it's an auto generated file, the warning
225+
// returned by lll will be ignored.
226+
// But if we return a linter error here, and this error
227+
// happens for an autogenerated file the error will be
228+
// discarded (fine), but all the subsequent errors for
229+
// lll will be discarded for other files, and we'll miss
230+
// legit error.
231+
res = append(res, &issue{
232+
pos: token.Position{
233+
Filename: filename,
234+
Line: lineNumber,
235+
Column: 1,
236+
},
237+
text: fmt.Sprintf("line is more than "+
238+
"%d characters",
239+
bufio.MaxScanTokenSize),
240+
})
241+
} else {
242+
return nil, fmt.Errorf("can't scan file %s: %w",
243+
filename, err)
244+
}
245+
}
246+
247+
return res, nil
248+
}
249+
250+
func getFileName(pass *analysis.Pass, file *ast.File) string {
251+
fileName := pass.Fset.PositionFor(file.Pos(), true).Filename
252+
ext := filepath.Ext(fileName)
253+
if ext != "" && ext != ".go" {
254+
// The position has been adjusted to a non-go file,
255+
// revert to original file.
256+
position := pass.Fset.PositionFor(file.Pos(), false)
257+
fileName = position.Filename
258+
}
259+
260+
return fileName
261+
}
262+
263+
func init() {
264+
// Register the linter with the plugin module register.
265+
register.Plugin(linterName, New)
266+
}

0 commit comments

Comments
 (0)