diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 1c84690..ed9e622 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -1,17 +1,20 @@ #!/bin/sh # Pre-commit hook to run goimports, gofmt, go vet, staticcheck, and golangci-lint -# Run goimports on all Go files +# Run goimports on all Go files but generated ones: gsx (*.x.go) and templ +# (*_templ.go) output must stay as the generator wrote it, since +# structpages-lint maps a _templ.go to its .templ only when the file is +# exactly what templ generates. echo "Running goimports..." -find . -name "*.go" -not -name "*.x.go" -not -path "./vendor/*" -not -path "./.git/*" | xargs goimports -w +find . -name "*.go" -not -name "*.x.go" -not -name "*_templ.go" -not -path "./vendor/*" -not -path "./.git/*" | xargs goimports -w if [ $? -ne 0 ]; then echo "goimports failed" exit 1 fi -# Run gofmt on all Go files +# Run gofmt on all Go files but generated ones echo "Running gofmt..." -find . -name "*.go" -not -name "*.x.go" -not -path "./vendor/*" -not -path "./.git/*" | xargs gofmt -w -s +find . -name "*.go" -not -name "*.x.go" -not -name "*_templ.go" -not -path "./vendor/*" -not -path "./.git/*" | xargs gofmt -w -s if [ $? -ne 0 ]; then echo "gofmt failed" exit 1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7db5127..adbf65e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -132,3 +132,18 @@ jobs: working-directory: ./examples/${{ matrix.example }} run: go test ./... + test-lint-tool: + name: Test tools/lint + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: "1.26" + + - name: Test + working-directory: ./tools/lint + run: go test ./... diff --git a/docs/lint.md b/docs/lint.md index 169780c..cd5b06f 100644 --- a/docs/lint.md +++ b/docs/lint.md @@ -183,9 +183,11 @@ The attribute is matched exactly: only if the result is byte-for-byte the `_templ.go` on disk. The one exception is the `// templ: version:` comment line, which may differ or be missing (`templ generate -include-version=false`): it is a comment, so it changes no code. If anything else differs (another - templ version that generates different code, `-include-timestamp`, a hand edit), the file's positions can't be - trusted, and its attributes are skipped by `region` without a finding. To have them checked, regenerate with - the templ version the linter is built with (the `github.com/a-h/templ` version in the linter's `go.mod`). + templ version that generates different code, `-include-timestamp`, a hand edit, or a formatter run over the + generated file: `goimports` regroups its imports), the file's positions can't be trusted, and its attributes are + skipped by `region` and `keyed` without a finding. To have them checked, regenerate with the templ version the + linter is built with (the `github.com/a-h/templ` version in the linter's `go.mod`), and leave `*_templ.go` out of + any `goimports` or `gofmt -s` run, as you would gsx's `*.x.go`. Limits: diff --git a/tools/lint/templscan/exprattr_test.go b/tools/lint/templscan/exprattr_test.go new file mode 100644 index 0000000..889f20d --- /dev/null +++ b/tools/lint/templscan/exprattr_test.go @@ -0,0 +1,30 @@ +package templscan + +import ( + "errors" + "path/filepath" + "testing" +) + +// The linter's templ fixtures are joined to their .templ only when the +// _templ.go is exactly what templ generates, so a formatter run over one +// (goimports regroups its imports) silently drops its findings. +// stale.templ's _templ.go differs on purpose. +func TestExprAttrs_fixturesAreTemplOutput(t *testing.T) { + files, err := filepath.Glob("../testdata/*/tapp/*.templ") + if err != nil || len(files) == 0 { + t.Fatalf("no .templ fixtures: %v", err) + } + for _, f := range files { + _, err := ExprAttrs(f) + switch { + case filepath.Base(f) == "stale.templ": + if !errors.Is(err, ErrStale) { + t.Errorf("%s: err = %v, want ErrStale", f, err) + } + case err != nil: + t.Errorf("%s: %v (commit a _templ.go exactly as `templ generate` writes it: "+ + "no goimports, no gofmt -s)", f, err) + } + } +} diff --git a/tools/lint/testdata/keyed/tapp/pages_templ.go b/tools/lint/testdata/keyed/tapp/pages_templ.go index 6f9ce19..230ff44 100644 --- a/tools/lint/testdata/keyed/tapp/pages_templ.go +++ b/tools/lint/testdata/keyed/tapp/pages_templ.go @@ -5,11 +5,10 @@ package tapp //lint:file-ignore SA4006 This context is only used if a nested component is present. -import ( - "github.com/a-h/templ" - templruntime "github.com/a-h/templ/runtime" - "github.com/jackielii/structpages" -) +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +import "github.com/jackielii/structpages" func (p list) Page(rows []row) templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { diff --git a/tools/lint/testdata/region/tapp/noversion_templ.go b/tools/lint/testdata/region/tapp/noversion_templ.go index cae6a8d..6df12f6 100644 --- a/tools/lint/testdata/region/tapp/noversion_templ.go +++ b/tools/lint/testdata/region/tapp/noversion_templ.go @@ -4,11 +4,10 @@ package tapp //lint:file-ignore SA4006 This context is only used if a nested component is present. -import ( - "github.com/a-h/templ" - templruntime "github.com/a-h/templ/runtime" - "github.com/jackielii/structpages" -) +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +import "github.com/jackielii/structpages" func (p ace) NoVersion() templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { diff --git a/tools/lint/testdata/region/tapp/pages_templ.go b/tools/lint/testdata/region/tapp/pages_templ.go index 2c70862..2dd0e68 100644 --- a/tools/lint/testdata/region/tapp/pages_templ.go +++ b/tools/lint/testdata/region/tapp/pages_templ.go @@ -5,11 +5,10 @@ package tapp //lint:file-ignore SA4006 This context is only used if a nested component is present. -import ( - "github.com/a-h/templ" - templruntime "github.com/a-h/templ/runtime" - "github.com/jackielii/structpages" -) +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +import "github.com/jackielii/structpages" func (p ace) Page() templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { diff --git a/tools/lint/testdata/region/tapp/requests_templ.go b/tools/lint/testdata/region/tapp/requests_templ.go index 73b4abd..5b4e6e5 100644 --- a/tools/lint/testdata/region/tapp/requests_templ.go +++ b/tools/lint/testdata/region/tapp/requests_templ.go @@ -5,11 +5,10 @@ package tapp //lint:file-ignore SA4006 This context is only used if a nested component is present. -import ( - "github.com/a-h/templ" - templruntime "github.com/a-h/templ/runtime" - "github.com/jackielii/structpages" -) +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +import "github.com/jackielii/structpages" func (p ace) Requests() templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { diff --git a/tools/lint/testdata/region/tapp/stale_templ.go b/tools/lint/testdata/region/tapp/stale_templ.go index caeda34..6783321 100644 --- a/tools/lint/testdata/region/tapp/stale_templ.go +++ b/tools/lint/testdata/region/tapp/stale_templ.go @@ -5,11 +5,10 @@ package tapp //lint:file-ignore SA4006 This context is only used if a nested component is present -import ( - "github.com/a-h/templ" - templruntime "github.com/a-h/templ/runtime" - "github.com/jackielii/structpages" -) +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +import "github.com/jackielii/structpages" func (p stale) Page() templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {