From 0bddd2aec31595edc7443a2953da825e1aac2f53 Mon Sep 17 00:00:00 2001 From: Bruno Carvalho Date: Sat, 27 Jun 2026 09:59:23 -0300 Subject: [PATCH] ci: enable strict golangci-lint --- .github/workflows/golangci-lint.yml | 28 + .golangci.yml | 54 ++ addons/auth/auth_test.go | 20 +- addons/auth/password.go | 3 +- addons/auth/session.go | 57 +- addons/db/db.go | 4 +- addons/db/db_test.go | 2 +- addons/realtime/realtime_test.go | 4 +- addons/tailwind/tailwind.go | 4 +- docs/engineering/ci.md | 26 + examples/contracts/patients/contracts.go | 16 +- examples/endpoints/src/endpoints/handlers.go | 4 +- examples/flagship/src/contracts/contracts.go | 12 +- gowdk.go | 16 +- helperruntime/runtime.go | 19 +- internal/addonregistry/handshake.go | 8 +- internal/appgen/appgen_test.go | 127 ++-- internal/appgen/audit_tests.go | 8 +- internal/appgen/ir.go | 2 +- internal/appgen/module.go | 7 +- internal/appgen/source.go | 48 +- internal/appgen/source_actions.go | 27 +- internal/appgen/source_api.go | 2 +- internal/appgen/source_backend.go | 12 - internal/appgen/source_backend_app.go | 4 - internal/appgen/source_guards.go | 4 - internal/appgen/source_lifecycle.go | 2 +- internal/appgen/source_ssr.go | 25 +- internal/auditsarif/sarif.go | 2 +- internal/auditspec/engine_test.go | 12 +- internal/buildgen/build.go | 22 +- internal/buildgen/build_data_runner.go | 10 +- internal/buildgen/build_data_values.go | 7 +- internal/buildgen/components.go | 13 +- internal/buildgen/css_minify.go | 4 +- internal/buildgen/css_scope.go | 8 +- internal/buildgen/data.go | 6 +- internal/buildgen/island_source_map.go | 5 +- internal/buildgen/islands_test.go | 14 +- internal/buildgen/render.go | 2 +- internal/buildgen/runtime_asset_paths.go | 9 +- internal/buildgen/runtime_islands.go | 25 - internal/buildgen/runtime_wasm_assets.go | 12 +- internal/buildgen/seo.go | 4 +- internal/buildgen/ssr_list_test.go | 1 + internal/buildgen/ssr_url_placeholders.go | 2 +- internal/clientlang/clientlang.go | 17 +- internal/clientlang/expr_eval.go | 6 +- internal/clientlang/expr_parser.go | 10 +- internal/clientlang/island_validation.go | 4 +- .../clientrt/expression_conformance_test.go | 2 +- internal/compiler/backend_bindings_test.go | 87 ++- .../compiler/backend_inline_signatures.go | 14 +- internal/compiler/backend_input_fields.go | 12 - internal/compiler/route_bindings_test.go | 18 +- internal/compiler/routes.go | 6 +- internal/compiler/routes_related_test.go | 29 +- internal/compiler/validate_component_view.go | 5 - .../validate_component_view_contract.go | 4 - .../compiler/validate_contract_refs_test.go | 13 +- internal/compiler/validate_packages.go | 37 +- internal/compiler/validate_page.go | 12 +- .../compiler/validate_page_contract_client.go | 2 +- .../compiler/validate_page_persist_test.go | 20 +- internal/compiler/validate_scripts.go | 4 +- internal/compiler/validate_test.go | 678 +++++++++++++++--- internal/contractscan/ast_helpers.go | 2 +- internal/contractscan/scan.go | 6 +- internal/discover/discover.go | 14 +- internal/discover/discover_test.go | 5 +- internal/doclint/main.go | 4 +- internal/doclint/parse.go | 6 +- internal/gotypes/gotypes.go | 14 +- internal/gowdkcmd/add.go | 18 +- internal/gowdkcmd/audit.go | 46 +- internal/gowdkcmd/audit_diff_test.go | 31 +- internal/gowdkcmd/build.go | 98 +-- internal/gowdkcmd/build_audit.go | 13 +- internal/gowdkcmd/build_timings.go | 12 +- internal/gowdkcmd/clean.go | 6 +- internal/gowdkcmd/config_helper.go | 24 +- internal/gowdkcmd/config_helper_test.go | 4 +- internal/gowdkcmd/dev.go | 11 +- internal/gowdkcmd/dev_listener_unix_test.go | 2 +- internal/gowdkcmd/dev_loop.go | 19 +- internal/gowdkcmd/dev_proxy_response_test.go | 4 + internal/gowdkcmd/docker.go | 4 +- internal/gowdkcmd/fix.go | 3 +- internal/gowdkcmd/generate.go | 3 +- internal/gowdkcmd/init.go | 23 +- internal/gowdkcmd/inspect.go | 3 +- internal/gowdkcmd/lsp.go | 4 +- internal/gowdkcmd/main_test.go | 26 +- internal/gowdkcmd/operation_error.go | 3 +- internal/gowdkcmd/playground.go | 44 +- internal/gowdkcmd/preview.go | 4 +- internal/gowdkcmd/serve.go | 21 +- internal/gowdkcmd/test.go | 21 +- internal/gwdkanalysis/ir_bindings.go | 5 +- internal/gwdkanalysis/ir_contracts.go | 7 +- internal/gwdkir/identity.go | 12 +- internal/lang/accessibility.go | 18 +- internal/lang/outline.go | 16 +- internal/lang/tools.go | 35 +- internal/lsp/completion_fields.go | 7 +- internal/lsp/components.go | 18 +- internal/lsp/rpc.go | 2 +- internal/lsp/server_test.go | 14 +- internal/parser/audit.go | 6 +- internal/parser/metadata.go | 61 -- internal/parser/patterns.go | 32 +- internal/parser/route_helpers.go | 12 - internal/parser/syntax.go | 55 +- internal/parser/syntax_test.go | 2 +- internal/playground/playground.go | 22 +- internal/playground/playground_test.go | 8 +- internal/project/config_exec.go | 20 +- internal/publicapi/gowdk_test.go | 4 +- internal/securitymanifest/manifest_test.go | 2 +- internal/source/routes.go | 2 +- internal/source/source.go | 24 +- internal/syntax/declparse.go | 6 +- internal/syntax/lexer.go | 3 +- internal/syntax/syntax_test.go | 6 +- internal/viewparse/events.go | 4 +- internal/viewparse/model.go | 14 +- internal/viewparse/parser.go | 6 +- internal/viewparse/patterns.go | 30 +- internal/viewparse/safety.go | 16 +- internal/viewrender/action_input_attrs.go | 10 +- internal/viewrender/api.go | 40 +- internal/viewrender/bindings.go | 3 +- internal/viewrender/conditional.go | 3 +- internal/viewrender/directives.go | 96 +-- internal/viewrender/element.go | 11 +- internal/viewrender/events.go | 4 +- internal/viewrender/interpolate.go | 5 - internal/viewrender/island_helpers.go | 3 +- internal/viewrender/node_loop.go | 5 +- internal/viewrender/patterns.go | 53 +- internal/viewrender/safety.go | 29 +- internal/viewrender/server_list.go | 12 +- runtime/app/app.go | 9 +- runtime/app/app_test.go | 24 +- runtime/app/boundary.go | 12 +- runtime/app/cors_vary_test.go | 4 +- runtime/app/lifecycle.go | 2 +- runtime/app/listener.go | 9 +- runtime/app/listener_test.go | 4 +- runtime/app/listener_unix_test.go | 16 +- runtime/app/response_writer_optional.go | 4 + runtime/contracts/concurrency_test.go | 24 +- runtime/contracts/fileoutbox/fileoutbox.go | 4 +- .../contracts/fileoutbox/fileoutbox_test.go | 4 +- runtime/contracts/fileoutbox/seenstore.go | 4 +- runtime/contracts/schedule.go | 2 +- runtime/contracts/sse/sse.go | 6 - runtime/contracts/sse/sse_test.go | 31 +- runtime/envfile/envfile.go | 4 +- runtime/guard/guard.go | 25 +- runtime/guard/guard_test.go | 21 +- runtime/ratelimit/concurrency_test.go | 2 +- runtime/response/response_test.go | 4 +- runtime/ssr/list.go | 12 +- runtime/ssr/list_test.go | 12 +- runtime/ssr/regions_test.go | 2 +- runtime/ssr/ssr_test.go | 15 +- runtime/testkit/contracts.go | 20 +- runtime/testkit/contracts_test.go | 6 +- runtime/testkit/testkit.go | 186 ++--- runtime/testkit/testkit_test.go | 6 +- runtime/trace/collector.go | 4 +- runtime/trace/trace_test.go | 32 +- runtime/trace/types.go | 6 +- runtime/validation/pattern.go | 26 +- runtime/wasm/wasm_test.go | 9 +- scripts/check-golangci-lint.sh | 25 + 177 files changed, 1975 insertions(+), 1525 deletions(-) create mode 100644 .github/workflows/golangci-lint.yml create mode 100644 .golangci.yml create mode 100755 scripts/check-golangci-lint.sh diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml new file mode 100644 index 00000000..b353b41f --- /dev/null +++ b/.github/workflows/golangci-lint.yml @@ -0,0 +1,28 @@ +name: GolangCI-Lint + +on: + workflow_dispatch: + +permissions: + contents: read + +jobs: + golangci-lint: + name: Strict lint + runs-on: ubuntu-latest + + steps: + - name: Check out repository + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + + - name: Set up Go + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6 + with: + go-version: '1.26.4' + cache: true + + - name: Install golangci-lint + run: go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2 + + - name: Run strict GolangCI-Lint + run: scripts/check-golangci-lint.sh diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 00000000..ec8a2959 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,54 @@ +version: "2" + +run: + timeout: 10m + tests: true + modules-download-mode: readonly + +linters: + default: none + enable: + - asasalint + - asciicheck + - bidichk + - bodyclose + - copyloopvar + - durationcheck + - errcheck + - errchkjson + - errorlint + - fatcontext + - gocheckcompilerdirectives + - gocritic + - gomoddirectives + - govet + - ineffassign + - makezero + - misspell + - nilerr + - nilnesserr + - nilnil + - nolintlint + - nosprintfhostport + - predeclared + - rowserrcheck + - sqlclosecheck + - staticcheck + - testableexamples + - thelper + - unconvert + - unparam + - unused + - wastedassign + +issues: + max-issues-per-linter: 0 + max-same-issues: 0 + uniq-by-line: false + +formatters: + enable: + - gci + - gofmt + - gofumpt + - goimports diff --git a/addons/auth/auth_test.go b/addons/auth/auth_test.go index 80564281..d6ad6ceb 100644 --- a/addons/auth/auth_test.go +++ b/addons/auth/auth_test.go @@ -691,7 +691,7 @@ func TestSessionRejectsTamperedCookie(t *testing.T) { } cookie := recorder.Result().Cookies()[0] // Flip the role inside the signed payload without re-signing. - cookie.Value = cookie.Value + "x" + cookie.Value += "x" request := httptest.NewRequest(http.MethodGet, "/", nil) request.AddCookie(cookie) @@ -825,9 +825,9 @@ func TestSessionCookieDefaultsToSecureAttributes(t *testing.T) { if !cookie.Expires.Equal(now.Add(DefaultSessionTTL)) { t.Fatalf("cookie expires at %v, want %v", cookie.Expires, now.Add(DefaultSessionTTL)) } - clear := sessions.ClearCookie() - if clear.Name != DefaultSessionCookie || clear.Path != "/" || !clear.HttpOnly || !clear.Secure || clear.SameSite != http.SameSiteLaxMode { - t.Fatalf("unexpected default clear-cookie attributes: %#v", clear) + clearCookie := sessions.ClearCookie() + if clearCookie.Name != DefaultSessionCookie || clearCookie.Path != "/" || !clearCookie.HttpOnly || !clearCookie.Secure || clearCookie.SameSite != http.SameSiteLaxMode { + t.Fatalf("unexpected default clear-cookie attributes: %#v", clearCookie) } } @@ -851,9 +851,9 @@ func TestSessionCookieHelpers(t *testing.T) { if cookie.Name != DefaultSessionCookie || cookie.Value == "" || !cookie.HttpOnly { t.Fatalf("unexpected issued cookie: %#v", cookie) } - clear := sessions.ClearCookie() - if clear.Name != DefaultSessionCookie || clear.MaxAge >= 0 { - t.Fatalf("unexpected clear cookie: %#v", clear) + clearCookie := sessions.ClearCookie() + if clearCookie.Name != DefaultSessionCookie || clearCookie.MaxAge >= 0 { + t.Fatalf("unexpected clear cookie: %#v", clearCookie) } } @@ -1085,5 +1085,7 @@ func TestNewReportsEnvNameWithoutLeakingSecret(t *testing.T) { // Sessions must satisfy the Provider interface so it can be registered with the // generated RegisterAuthProvider hook. -var _ Provider = (*Sessions)(nil) -var _ PasswordHasher = PBKDF2Hasher{} +var ( + _ Provider = (*Sessions)(nil) + _ PasswordHasher = PBKDF2Hasher{} +) diff --git a/addons/auth/password.go b/addons/auth/password.go index dae10c71..322de31f 100644 --- a/addons/auth/password.go +++ b/addons/auth/password.go @@ -8,7 +8,6 @@ import ( "encoding/base64" "errors" "fmt" - "hash" "strconv" "strings" ) @@ -144,5 +143,5 @@ func decodeHash(encoded string) (iterations int, salt, key []byte, err error) { } func pbkdf2SHA256(password string, salt []byte, iterations, keyLength int) ([]byte, error) { - return pbkdf2.Key(func() hash.Hash { return sha256.New() }, password, salt, iterations, keyLength) + return pbkdf2.Key(sha256.New, password, salt, iterations, keyLength) } diff --git a/addons/auth/session.go b/addons/auth/session.go index 651d4216..4543135c 100644 --- a/addons/auth/session.go +++ b/addons/auth/session.go @@ -263,7 +263,7 @@ func sessionSigningKeys(options Options) (SigningKey, map[string]SigningKey, err func validateSigningKeyID(label string, id string) error { if strings.Contains(id, ".") { - return fmt.Errorf("gowdk auth: %s id %q must not contain .", label, id) + return fmt.Errorf("gowdk auth: %s id %q must not contain dot", label, id) } return nil } @@ -406,12 +406,12 @@ func (sessions *Sessions) Revoke(ctx context.Context, request *http.Request) err if sessions.mode != SessionModeRevocable || request == nil { return nil } - cookie, err := request.Cookie(sessions.cookie) - if err != nil { + cookie, ok := sessions.requestCookie(request) + if !ok { return nil } - payload, err := sessions.verify(cookie.Value) - if err != nil { + payload, ok := sessions.verifiedPayload(cookie.Value) + if !ok { return nil } if strings.TrimSpace(payload.SessionID) == "" { @@ -456,24 +456,24 @@ func (sessions *Sessions) ClearCookie() http.Cookie { // principal and no error, meaning unauthenticated. func (sessions *Sessions) Principal(request *http.Request) (*Principal, error) { if request == nil { - return nil, nil + return unauthenticatedPrincipal() } - cookie, err := request.Cookie(sessions.cookie) - if err != nil { - return nil, nil + cookie, ok := sessions.requestCookie(request) + if !ok { + return unauthenticatedPrincipal() } - payload, err := sessions.verify(cookie.Value) - if err != nil { - return nil, nil + payload, ok := sessions.verifiedPayload(cookie.Value) + if !ok { + return unauthenticatedPrincipal() } if sessions.now().Unix() >= payload.Expires { - return nil, nil + return unauthenticatedPrincipal() } if sessions.mode == SessionModeRevocable { return sessions.revocablePrincipal(request.Context(), payload) } if strings.TrimSpace(payload.ID) == "" { - return nil, nil + return unauthenticatedPrincipal() } return &Principal{ ID: payload.ID, @@ -485,21 +485,21 @@ func (sessions *Sessions) Principal(request *http.Request) (*Principal, error) { func (sessions *Sessions) revocablePrincipal(ctx context.Context, payload sessionPayload) (*Principal, error) { if strings.TrimSpace(payload.SessionID) == "" { - return nil, nil + return unauthenticatedPrincipal() } record, err := sessions.store.LookupSession(ctx, payload.SessionID) if err != nil { if errors.Is(err, ErrSessionNotFound) { - return nil, nil + return unauthenticatedPrincipal() } return nil, err } now := sessions.now() if record.Revoked || record.expired(now) || strings.TrimSpace(record.Principal.ID) == "" { - return nil, nil + return unauthenticatedPrincipal() } if sessionRecordAuthorizationVersion(record) != payload.AuthorizationVersion { - return nil, nil + return unauthenticatedPrincipal() } if sessions.idleTTL > 0 { toucher, ok := sessions.store.(SessionToucher) @@ -516,6 +516,27 @@ func (sessions *Sessions) revocablePrincipal(ctx context.Context, payload sessio return &principal, nil } +func unauthenticatedPrincipal() (*Principal, error) { + var principal *Principal + return principal, nil +} + +func (sessions *Sessions) requestCookie(request *http.Request) (*http.Cookie, bool) { + cookie, err := request.Cookie(sessions.cookie) + if err != nil { + return nil, false + } + return cookie, true +} + +func (sessions *Sessions) verifiedPayload(token string) (sessionPayload, bool) { + payload, err := sessions.verify(token) + if err != nil { + return sessionPayload{}, false + } + return payload, true +} + func sessionRecordAuthorizationVersion(record SessionRecord) string { if record.Principal.AuthorizationVersion != "" { return record.Principal.AuthorizationVersion diff --git a/addons/db/db.go b/addons/db/db.go index 6966c406..25f0def7 100644 --- a/addons/db/db.go +++ b/addons/db/db.go @@ -81,7 +81,9 @@ func OpenWithOptions(driver, dsn string, options Options) (*sql.DB, error) { ctx, cancel := context.WithTimeout(context.Background(), pingTimeout) defer cancel() if err := database.PingContext(ctx); err != nil { - database.Close() + if closeErr := database.Close(); closeErr != nil { + return nil, fmt.Errorf("gowdk db: ping %q: %w; close: %w", driver, err, closeErr) + } return nil, fmt.Errorf("gowdk db: ping %q: %w", driver, err) } return database, nil diff --git a/addons/db/db_test.go b/addons/db/db_test.go index 184fe98d..ebcbd60f 100644 --- a/addons/db/db_test.go +++ b/addons/db/db_test.go @@ -186,7 +186,7 @@ func TestApplyMigrationsReservesBeforeRunningMigrationSQL(t *testing.T) { if reserve < 0 || migration < 0 || finalize < 0 { t.Fatalf("did not find reservation, migration, and finalize statements in %#v", state.executed) } - if !(reserve < migration && migration < finalize) { + if reserve >= migration || migration >= finalize { t.Fatalf("migration was not reserved before user SQL and finalized after it: %#v", state.executed) } } diff --git a/addons/realtime/realtime_test.go b/addons/realtime/realtime_test.go index f55c415e..0248a0dc 100644 --- a/addons/realtime/realtime_test.go +++ b/addons/realtime/realtime_test.go @@ -22,7 +22,5 @@ func TestNewSSEReturnsPresentationFanout(t *testing.T) { WithSSEReplayLimit(4), WithSSEAudienceFromRequest(func(*http.Request) []string { return []string{"tenant:test"} }), ) - if fanout == nil { - t.Fatal("expected SSE fanout") - } + _ = fanout } diff --git a/addons/tailwind/tailwind.go b/addons/tailwind/tailwind.go index 7057a0ea..d35e5e51 100644 --- a/addons/tailwind/tailwind.go +++ b/addons/tailwind/tailwind.go @@ -72,7 +72,9 @@ func (p processor) ProcessCSS(context gowdk.CSSContext) (gowdk.CSSResult, error) if err != nil { return gowdk.CSSResult{}, err } - defer os.RemoveAll(tempDir) + defer func() { + _ = os.RemoveAll(tempDir) + }() tempOutput := filepath.Join(tempDir, "app.css") workingDir := cssWorkingDir(context) diff --git a/docs/engineering/ci.md b/docs/engineering/ci.md index 3e6406f3..2a07b0ba 100644 --- a/docs/engineering/ci.md +++ b/docs/engineering/ci.md @@ -37,6 +37,10 @@ Required pull-request lanes: The release lanes live outside the pull-request CI workflow: +- `.github/workflows/golangci-lint.yml`: manual strict lint workflow. It runs + `scripts/check-golangci-lint.sh` with the pinned GolangCI-Lint version. Do + not make this a required pull-request lane until the repository is clean under + the configured lint set. - `.github/workflows/release-dry-run.yml`: scheduled weekly and manual; packages CLI/VS Code artifacts, writes checksums, and uploads workflow artifacts. This is GitHub-only because it uses Actions artifact upload. @@ -58,6 +62,7 @@ Run the same local checks before handoff when relevant: scripts/vulncheck-go-modules.sh go build ./cmd/gowdk scripts/check-dead-code.sh + scripts/check-golangci-lint.sh ``` - VS Code extension checks: @@ -177,6 +182,27 @@ after confirming a clean baseline without broad suppressions; if an intentional entry point needs a suppression, keep it local to the declaration and document why external reachability is expected. +## GolangCI-Lint + +`.golangci.yml` is intentionally strict: + +```sh +scripts/check-golangci-lint.sh +``` + +The gate verifies the config and runs `golangci-lint run` with correctness, +resource-handling, formatting, maintainability, and dead-code linters enabled. +Test files are included, module downloads are readonly, issue counts are +uncapped, and existing findings are not hidden through exclusions. +The wrapper uses `golangci-lint` when `v2.12.2` is on `PATH`; otherwise it falls back to +`go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2`. CI +also installs that pinned version before running the wrapper. + +Do not add broad `linters.exclusions` or `issues.exclude` entries to make a +dirty baseline pass. Fix the finding, add a narrow `//nolint:` with a +reason where the code is intentionally exceptional, or lower strictness only +with an owning engineering note that explains the tradeoff. + ## Release Smoke After publishing a tag, verify the current machine's release artifact locally: diff --git a/examples/contracts/patients/contracts.go b/examples/contracts/patients/contracts.go index 57aada70..7a30ff00 100644 --- a/examples/contracts/patients/contracts.go +++ b/examples/contracts/patients/contracts.go @@ -39,11 +39,17 @@ type PatientCreated struct { } func Register(registry *contracts.Registry) { - contracts.RegisterQuery[GetPatientPage, PatientPageData](registry, LoadPatientPage, contracts.RoleWeb) - contracts.RegisterCommand[CreatePatient, CreatePatientResult](registry, HandleCreatePatient, contracts.RoleWeb) - contracts.RegisterPresentationEvent[PatientNotice](registry, PublishPatientNotice, contracts.RoleWeb) - contracts.RegisterDomainEvent[PatientCreated](registry, SendWelcomeEmail, contracts.RoleWorker) - contracts.RegisterInvalidation[PatientCreated, GetPatientPage](registry) + mustRegister(contracts.RegisterQuery[GetPatientPage, PatientPageData](registry, LoadPatientPage, contracts.RoleWeb)) + mustRegister(contracts.RegisterCommand[CreatePatient, CreatePatientResult](registry, HandleCreatePatient, contracts.RoleWeb)) + mustRegister(contracts.RegisterPresentationEvent[PatientNotice](registry, PublishPatientNotice, contracts.RoleWeb)) + mustRegister(contracts.RegisterDomainEvent[PatientCreated](registry, SendWelcomeEmail, contracts.RoleWorker)) + mustRegister(contracts.RegisterInvalidation[PatientCreated, GetPatientPage](registry)) +} + +func mustRegister(err error) { + if err != nil { + panic(err) + } } func LoadPatientPage(ctx context.Context, query GetPatientPage) (PatientPageData, error) { diff --git a/examples/endpoints/src/endpoints/handlers.go b/examples/endpoints/src/endpoints/handlers.go index 0ae92d3b..b10c55ad 100644 --- a/examples/endpoints/src/endpoints/handlers.go +++ b/examples/endpoints/src/endpoints/handlers.go @@ -49,7 +49,9 @@ func UploadAvatar(_ context.Context, input UploadInput) (response.Response, erro if err != nil { return response.FragmentFor("#upload-result", alertUploadHTML("Upload could not be opened.")), nil } - defer uploaded.Close() + defer func() { + _ = uploaded.Close() + }() bytes, err := io.Copy(io.Discard, uploaded) if err != nil { return response.FragmentFor("#upload-result", alertUploadHTML("Upload could not be read.")), nil diff --git a/examples/flagship/src/contracts/contracts.go b/examples/flagship/src/contracts/contracts.go index d3d0ea00..95b26343 100644 --- a/examples/flagship/src/contracts/contracts.go +++ b/examples/flagship/src/contracts/contracts.go @@ -28,9 +28,15 @@ type WorkflowStarted struct { } func Register(registry *gowdkcontracts.Registry) { - gowdkcontracts.RegisterQuery[GetDashboardSnapshot, DashboardSnapshot](registry, LoadDashboardSnapshot, gowdkcontracts.RoleWeb) - gowdkcontracts.RegisterCommand[StartWorkflow, StartWorkflowResult](registry, HandleStartWorkflow, gowdkcontracts.RoleWeb) - gowdkcontracts.RegisterDomainEvent[WorkflowStarted](registry, RecordWorkflowStarted, gowdkcontracts.RoleWorker) + mustRegister(gowdkcontracts.RegisterQuery[GetDashboardSnapshot, DashboardSnapshot](registry, LoadDashboardSnapshot, gowdkcontracts.RoleWeb)) + mustRegister(gowdkcontracts.RegisterCommand[StartWorkflow, StartWorkflowResult](registry, HandleStartWorkflow, gowdkcontracts.RoleWeb)) + mustRegister(gowdkcontracts.RegisterDomainEvent[WorkflowStarted](registry, RecordWorkflowStarted, gowdkcontracts.RoleWorker)) +} + +func mustRegister(err error) { + if err != nil { + panic(err) + } } func LoadDashboardSnapshot(context.Context, GetDashboardSnapshot) (DashboardSnapshot, error) { diff --git a/gowdk.go b/gowdk.go index ec0f6910..42b75f66 100644 --- a/gowdk.go +++ b/gowdk.go @@ -916,26 +916,26 @@ func ValidateAddons(addons []Addon) error { features := map[Feature]int{} for index, addon := range addons { if addonIsNil(addon) { - return fmt.Errorf("Addons[%d] is nil", index) + return fmt.Errorf("addons[%d] is nil", index) } name := strings.TrimSpace(addon.Name()) if name == "" { - return fmt.Errorf("Addons[%d].Name is required", index) + return fmt.Errorf("addons[%d].Name is required", index) } if previous, ok := names[name]; ok { - return fmt.Errorf("Addons[%d] %q duplicates Addons[%d]", index, name, previous) + return fmt.Errorf("addons[%d] %q duplicates addons[%d]", index, name, previous) } names[name] = index addonFeatures := addon.Features() if len(addonFeatures) == 0 { - return fmt.Errorf("Addons[%d] %q must declare at least one feature", index, name) + return fmt.Errorf("addons[%d] %q must declare at least one feature", index, name) } for featureIndex, feature := range addonFeatures { if strings.TrimSpace(string(feature)) == "" { - return fmt.Errorf("Addons[%d] %q declares empty feature at index %d", index, name, featureIndex) + return fmt.Errorf("addons[%d] %q declares empty feature at index %d", index, name, featureIndex) } if previous, ok := features[feature]; ok && !duplicateFeatureAllowed(feature) { - return fmt.Errorf("Addons[%d] %q duplicates feature %q already owned by Addons[%d]", index, name, feature, previous) + return fmt.Errorf("addons[%d] %q duplicates feature %q already owned by addons[%d]", index, name, feature, previous) } if _, ok := features[feature]; !ok { features[feature] = index @@ -953,11 +953,11 @@ func validateAddonFeatureContracts(index int, name string, addon Addon, features switch feature { case FeatureSEO: if _, ok := addon.(SEOProvider); !ok { - return fmt.Errorf("Addons[%d] %q declares feature %q but does not implement gowdk.SEOProvider", index, name, feature) + return fmt.Errorf("addons[%d] %q declares feature %q but does not implement gowdk.SEOProvider", index, name, feature) } case FeatureAuth: if _, ok := addon.(AuthSessionProvider); !ok { - return fmt.Errorf("Addons[%d] %q declares feature %q but does not implement gowdk.AuthSessionProvider", index, name, feature) + return fmt.Errorf("addons[%d] %q declares feature %q but does not implement gowdk.AuthSessionProvider", index, name, feature) } } } diff --git a/helperruntime/runtime.go b/helperruntime/runtime.go index 21ddad8d..c8314f2e 100644 --- a/helperruntime/runtime.go +++ b/helperruntime/runtime.go @@ -25,11 +25,11 @@ type Options struct { func Main(options Options) { if options.Config == nil { - fatal(2, "missing gowdk.Config") + fatal("missing gowdk.Config") } handshake() if len(os.Args) < 2 { - fatal(2, "missing helper command") + fatal("missing helper command") } if err := gowdkcmd.RunWithConfig(os.Args[1:], options.Config, options.ProjectRoot); err != nil { if _, silent := err.(interface{ SilentCLIError() }); !silent { @@ -44,13 +44,12 @@ func handshake() int { cliMax := envInt("GOWDK_HELPER_PROTOCOL_MAX", -1) cliVersion := os.Getenv("GOWDK_CLI_VERSION") if cliMin < 0 || cliMax < 0 { - fatal(2, "missing GOWDK helper protocol environment") + fatal("missing GOWDK helper protocol environment") } - selected := min(cliMax, ProtocolMax) - if selected < max(cliMin, ProtocolMin) { + selected := minInt(cliMax, ProtocolMax) + if selected < maxInt(cliMin, ProtocolMin) { fatal( - 2, "GOWDK helper protocol mismatch\n\nCLI supports protocol v%d..v%d.\nProject helper supports protocol v%d..v%d.\nCLI GOWDK version: %s\nProject GOWDK version: %s", cliMin, cliMax, @@ -63,9 +62,9 @@ func handshake() int { return selected } -func fatal(code int, format string, args ...any) { +func fatal(format string, args ...any) { fmt.Fprintf(os.Stderr, format+"\n", args...) - os.Exit(code) + os.Exit(2) } func envInt(name string, fallback int) int { @@ -80,14 +79,14 @@ func envInt(name string, fallback int) int { return n } -func min(a, b int) int { +func minInt(a, b int) int { if a < b { return a } return b } -func max(a, b int) int { +func maxInt(a, b int) int { if a > b { return a } diff --git a/internal/addonregistry/handshake.go b/internal/addonregistry/handshake.go index 21f2e9e7..d906a4da 100644 --- a/internal/addonregistry/handshake.go +++ b/internal/addonregistry/handshake.go @@ -36,20 +36,20 @@ func (e Entry) SupportsVersion(version string) VersionSupport { } if strings.TrimSpace(e.MinGOWDK) != "" { - min, ok := parseVersion(e.MinGOWDK) + minimum, ok := parseVersion(e.MinGOWDK) if !ok { return VersionUnknown } - if compareVersion(target, min) < 0 { + if compareVersion(target, minimum) < 0 { return VersionUnsupported } } if strings.TrimSpace(e.MaxGOWDK) != "" { - max, ok := parseVersion(e.MaxGOWDK) + maximum, ok := parseVersion(e.MaxGOWDK) if !ok { return VersionUnknown } - if compareVersion(target, max) > 0 { + if compareVersion(target, maximum) > 0 { return VersionUnsupported } } diff --git a/internal/appgen/appgen_test.go b/internal/appgen/appgen_test.go index 4859778c..416bbed2 100644 --- a/internal/appgen/appgen_test.go +++ b/internal/appgen/appgen_test.go @@ -724,11 +724,12 @@ func TestStandaloneAuditTestRejectsDynamicEndpointExpectations(t *testing.T) { } func TestAuditSecurityHeadersCanonicalizesCaseVariants(t *testing.T) { + spacedContentSecurityPolicy := " content-security-policy " headers := auditSecurityHeaders(gowdk.Config{Build: gowdk.BuildConfig{ SecurityHeaders: gowdk.SecurityHeadersConfig{ Enabled: true, Headers: map[string]string{ - " content-security-policy ": "default-src 'self'", + spacedContentSecurityPolicy: "default-src 'self'", "X-Frame-Options": "DENY", "x-frame-options": "DENY", }, @@ -1629,46 +1630,47 @@ func TestGeneratedGoMatchesGoldenFixture(t *testing.T) { writeTestFile(t, filepath.Join(outputDir, "patients", "index.html"), "
Patients
") program := &gwdkir.Program{ - ContractRefs: []gwdkir.ContractReference{{ - Kind: gwdkir.ContractCommand, - Name: "patients.CreatePatient", - ImportAlias: "patients", - ImportPath: "example.com/app/contracts/patients", - Type: "CreatePatient", - Result: "CreatePatientResult", - InputFields: []source.BackendInputField{ - {FieldName: "Name", FormName: "name", Type: "string"}, - {FieldName: "Tags", FormName: "tag", Type: "[]string"}, - {FieldName: "Age", FormName: "age", Type: "int"}, - {FieldName: "Remember", FormName: "remember", Type: "bool"}, - }, - Method: "POST", - Path: "/patients", - Status: gwdkir.ContractBindingBound, - Handler: "HandleCreatePatient", - Register: "Register", - OwnerKind: gwdkir.SourcePage, - OwnerID: "patients", - Guards: []string{"public"}, - }, { - Kind: gwdkir.ContractQuery, - Name: "patients.GetPatientPage", - ImportAlias: "patients", - ImportPath: "example.com/app/contracts/patients", - Type: "GetPatientPage", - Result: "PatientPageData", - InputFields: []source.BackendInputField{ - {FieldName: "Filter", FormName: "filter", Type: "string"}, + ContractRefs: []gwdkir.ContractReference{ + { + Kind: gwdkir.ContractCommand, + Name: "patients.CreatePatient", + ImportAlias: "patients", + ImportPath: "example.com/app/contracts/patients", + Type: "CreatePatient", + Result: "CreatePatientResult", + InputFields: []source.BackendInputField{ + {FieldName: "Name", FormName: "name", Type: "string"}, + {FieldName: "Tags", FormName: "tag", Type: "[]string"}, + {FieldName: "Age", FormName: "age", Type: "int"}, + {FieldName: "Remember", FormName: "remember", Type: "bool"}, + }, + Method: "POST", + Path: "/patients", + Status: gwdkir.ContractBindingBound, + Handler: "HandleCreatePatient", + Register: "Register", + OwnerKind: gwdkir.SourcePage, + OwnerID: "patients", + Guards: []string{"public"}, + }, { + Kind: gwdkir.ContractQuery, + Name: "patients.GetPatientPage", + ImportAlias: "patients", + ImportPath: "example.com/app/contracts/patients", + Type: "GetPatientPage", + Result: "PatientPageData", + InputFields: []source.BackendInputField{ + {FieldName: "Filter", FormName: "filter", Type: "string"}, + }, + Method: "GET", + Path: "/patients", + Status: gwdkir.ContractBindingBound, + Handler: "LoadPatientPage", + Register: "Register", + OwnerKind: gwdkir.SourcePage, + OwnerID: "patients", + Guards: []string{"public"}, }, - Method: "GET", - Path: "/patients", - Status: gwdkir.ContractBindingBound, - Handler: "LoadPatientPage", - Register: "Register", - OwnerKind: gwdkir.SourcePage, - OwnerID: "patients", - Guards: []string{"public"}, - }, }, } actions := []ActionEndpoint{{ @@ -3151,7 +3153,7 @@ func TestAppShellSourceEmitterDoesNotUseRawTemplates(t *testing.T) { } func TestActionHandlerSourceReturnsInvalidGeneratedIdentifierError(t *testing.T) { - _, err := actionHandlerSource([]ActionEndpoint{invalidGeneratedIdentifierActionEndpoint()}, false) + _, err := actionHandlerSource([]ActionEndpoint{invalidGeneratedIdentifierActionEndpoint()}) assertInvalidGeneratedIdentifierError(t, err) } @@ -3237,7 +3239,7 @@ func TestGeneratedDeclarationSnippetIsGoFormatted(t *testing.T) { Route: "/newsletter", InputFields: []string{"email"}, Redirect: "/newsletter?ok=1", - }}, false) + }}) if err != nil { t.Fatalf("actionHandlerSource: %v", err) } @@ -6281,7 +6283,9 @@ func LoadDashboard(ctx ssr.LoadContext) (map[string]any, error) { if err != nil { t.Fatal(err) } - defer response.Body.Close() + defer func() { + _ = response.Body.Close() + }() payload, err := io.ReadAll(response.Body) if err != nil { t.Fatal(err) @@ -6365,7 +6369,9 @@ func LoadDashboard(ctx ssr.LoadContext) (map[string]any, error) { if err != nil { t.Fatal(err) } - defer response.Body.Close() + defer func() { + _ = response.Body.Close() + }() payload, err := io.ReadAll(response.Body) if err != nil { t.Fatal(err) @@ -6442,7 +6448,9 @@ func LoadDashboard(ctx ssr.LoadContext) (map[string]any, error) { if err != nil { t.Fatal(err) } - defer response.Body.Close() + defer func() { + _ = response.Body.Close() + }() payload, err := io.ReadAll(response.Body) if err != nil { t.Fatal(err) @@ -6514,7 +6522,9 @@ func LoadDashboard(ctx ssr.LoadContext) map[string]any { if err != nil { t.Fatal(err) } - defer response.Body.Close() + defer func() { + _ = response.Body.Close() + }() payload, err := io.ReadAll(response.Body) if err != nil { t.Fatal(err) @@ -6591,7 +6601,9 @@ func Health(ctx context.Context, request *http.Request) (response.Response, erro if err != nil { t.Fatal(err) } - defer response.Body.Close() + defer func() { + _ = response.Body.Close() + }() payload, err := io.ReadAll(response.Body) if err != nil { t.Fatal(err) @@ -7556,9 +7568,11 @@ func HandleCreatePatient(ctx context.Context, command CreatePatient) (CreatePati _ = command.Process.Kill() _, _ = command.Process.Wait() }() - if _, err := waitForHTTPStatus("http://"+addr+"/_gowdk/health", http.MethodGet, ""); err != nil { + healthResponse, err := waitForHTTPStatus("http://"+addr+"/_gowdk/health", http.MethodGet, "") + if err != nil { t.Fatal(err) } + _ = healthResponse.Body.Close() streamCtx, cancelStream := context.WithCancel(context.Background()) defer cancelStream() @@ -7570,7 +7584,9 @@ func HandleCreatePatient(ctx context.Context, command CreatePatient) (CreatePati if err != nil { t.Fatal(err) } - defer streamResponse.Body.Close() + defer func() { + _ = streamResponse.Body.Close() + }() if streamResponse.StatusCode != http.StatusOK { t.Fatalf("expected realtime stream status 200, got %d", streamResponse.StatusCode) } @@ -7684,9 +7700,11 @@ func GOWDKGuardRegistry() gowdkguard.Registry { _ = command.Process.Kill() _, _ = command.Process.Wait() }() - if _, err := waitForHTTPStatus("http://"+addr+"/_gowdk/health", http.MethodGet, ""); err != nil { + healthResponse, err := waitForHTTPStatus("http://"+addr+"/_gowdk/health", http.MethodGet, "") + if err != nil { t.Fatal(err) } + _ = healthResponse.Body.Close() response, err := waitForHTTPStatus("http://"+addr+"/_gowdk/realtime/events?path=/dashboard", http.MethodGet, "") if err != nil { @@ -9451,11 +9469,6 @@ func actionEndpointsFromManifestFixture(app gwdkanalysis.Sources) ([]ActionEndpo return actionEndpointsFromIR(gowdk.Config{}, gwdkanalysis.BuildProgram(gowdk.Config{}, app)) } -func apiEndpointsFromManifestFixture(app gwdkanalysis.Sources) ([]APIEndpoint, error) { - app = normalizeEndpointFixtureSources(app) - return apiEndpointsFromIR(gwdkanalysis.BuildProgram(gowdk.Config{}, app)) -} - func fragmentEndpointsFromManifestFixture(app gwdkanalysis.Sources) ([]FragmentEndpoint, error) { app = normalizeEndpointFixtureSources(app) return fragmentEndpointsFromIR(gwdkanalysis.BuildProgram(gowdk.Config{}, app)) @@ -9541,7 +9554,7 @@ func TestGuardlessActionAndAPIAreDeniedByOmission(t *testing.T) { const deny = `gowdkresponse.WriteNoStoreError(response, http.StatusForbidden, "403 forbidden")` const denyJSON = `gowdkresponse.WriteNoStoreJSONError(response, http.StatusForbidden, "403 forbidden")` - actionSrc, err := actionHandlerSource([]ActionEndpoint{{PageID: "p", ActionName: "Sub", Route: "/sub"}}, false) + actionSrc, err := actionHandlerSource([]ActionEndpoint{{PageID: "p", ActionName: "Sub", Route: "/sub"}}) if err != nil { t.Fatal(err) } @@ -9644,7 +9657,7 @@ func TestGuardlessActionAndAPIAreDeniedByOmission(t *testing.T) { // An endpoint that declares `guard public` is intentionally reachable and // must NOT be denied by omission. - publicSrc, err := actionHandlerSource([]ActionEndpoint{{PageID: "p", ActionName: "Sub", Route: "/sub", Guards: []string{"public"}}}, false) + publicSrc, err := actionHandlerSource([]ActionEndpoint{{PageID: "p", ActionName: "Sub", Route: "/sub", Guards: []string{"public"}}}) if err != nil { t.Fatal(err) } diff --git a/internal/appgen/audit_tests.go b/internal/appgen/audit_tests.go index 39bd8967..467ff3ac 100644 --- a/internal/appgen/audit_tests.go +++ b/internal/appgen/audit_tests.go @@ -413,7 +413,7 @@ func auditSecurityHeaders(config gowdk.Config) []auditHeaderExpectation { normalized := normalizedSecurityHeaders(config.Build.SecurityHeaders.Headers) headers := make([]auditHeaderExpectation, 0, len(normalized)) for _, header := range normalized { - headers = append(headers, auditHeaderExpectation{Name: header.Name, Value: header.Value}) + headers = append(headers, auditHeaderExpectation(header)) } return headers } @@ -511,8 +511,10 @@ func auditEndpointPathMatches(endpointPath string, requestPath string) bool { return false } -const generatedAuditEnvSeed = "gowdk-audit-test" -const generatedAuditCSRFSecretSeed = "gowdk-audit-test-csrf-secret-32-bytes" +const ( + generatedAuditEnvSeed = "gowdk-audit-test" + generatedAuditCSRFSecretSeed = "gowdk-audit-test-csrf-secret-32-bytes" +) func writeGeneratedAuditEnvSeeds(builder *strings.Builder, config gowdk.Config, manifest securitymanifest.SecurityManifest) { csrfSecretName := "" diff --git a/internal/appgen/ir.go b/internal/appgen/ir.go index 29845366..f809896e 100644 --- a/internal/appgen/ir.go +++ b/internal/appgen/ir.go @@ -98,7 +98,7 @@ func actionEndpointRoutes(config gowdk.I18NConfig, pageRoute string, route strin func actionFormSchemaFromBlocks(blocks gwdkir.Blocks) (map[string][]view.ActionFormField, error) { if len(blocks.ViewNodes) == 0 { if strings.TrimSpace(blocks.ViewBody) == "" { - return nil, nil + return map[string][]view.ActionFormField{}, nil } return nil, fmt.Errorf("view {} has source body but no parsed nodes") } diff --git a/internal/appgen/module.go b/internal/appgen/module.go index 21f7320e..a41107f2 100644 --- a/internal/appgen/module.go +++ b/internal/appgen/module.go @@ -48,7 +48,8 @@ func moduleSourceForImportPaths(importPaths map[string]bool) (string, error) { runtimeReplaceDir = root } appModule, err := currentAppModule() - if err != nil { + switch { + case err != nil: // A missing/broken main module is only fatal when the generated app // imports app-owned packages: without the module path we cannot add the // require/replace, so the generated app would otherwise fail to build @@ -57,9 +58,9 @@ func moduleSourceForImportPaths(importPaths map[string]bool) (string, error) { if importPathsHasLocalModuleImports(importPaths) { return "", fmt.Errorf("cannot determine the app Go module for generated app imports: %w", err) } - } else if appModule.Path == gowdkRuntimeModulePath { + case appModule.Path == gowdkRuntimeModulePath: runtimeReplaceDir = appModule.Dir - } else if importPathsUseModule(importPaths, appModule.Path) { + case importPathsUseModule(importPaths, appModule.Path): lines = append(lines, "", "require "+appModule.Path+" v0.0.0", diff --git a/internal/appgen/source.go b/internal/appgen/source.go index 6c78c7d0..b3f44b85 100644 --- a/internal/appgen/source.go +++ b/internal/appgen/source.go @@ -9,7 +9,6 @@ import ( "go/token" "net/http" "sort" - "strconv" "strings" "github.com/cssbruno/gowdk" @@ -33,10 +32,6 @@ func appPackageSource(options Options) (source string, err error) { return printGoFile("gowdkapp", imports, append(appShellDecls(options), appGeneratedDecls(direct, options)...)) } -func runtimeImportSource(options Options) string { - return importSpecSource(runtimeImportMap(options)) -} - func runtimeImportMap(options Options) map[string]string { imports := map[string]string{ "gowdkruntime": "github.com/cssbruno/gowdk/runtime/app", @@ -279,7 +274,7 @@ func appShellDecls(options Options) []ast.Decl { appDecl(options), handlerDecl(), newServeMuxDecl(options, true), - serveMuxDecl(options, true), + serveMuxDecl(), ) if len(providers) == 0 { decls = append(decls, configuredServicesDecl(nil)) @@ -301,7 +296,7 @@ func backendShellDecls(options Options) []ast.Decl { appDecl(options), handlerDecl(), newServeMuxDecl(options, false), - serveMuxDecl(options, false), + serveMuxDecl(), ) if len(providers) == 0 { decls = append(decls, configuredServicesDecl(nil)) @@ -452,7 +447,7 @@ func handlerDecl() ast.Decl { }) } -func serveMuxDecl(options Options, embedded bool) ast.Decl { +func serveMuxDecl() ast.Decl { return funcDecl("ServeMux", nil, []*ast.Field{ {Type: &ast.StarExpr{X: sel("http", "ServeMux")}}, {Type: id("error")}, @@ -897,30 +892,6 @@ func backendOnlyHandlerFunc() ast.Expr { } } -// importSpecSource is retained for narrow tests and package-level helpers. New -// generated Go files use importDecl through the AST file builder. -func importSpecSource(imports map[string]string) string { - if len(imports) == 0 { - return "" - } - aliases := make([]string, 0, len(imports)) - for alias := range imports { - aliases = append(aliases, alias) - } - sort.Strings(aliases) - - specs := make([]string, 0, len(aliases)) - for _, alias := range aliases { - spec := "\t" - if alias != imports[alias] { - spec += alias + " " - } - spec += strconv.Quote(imports[alias]) - specs = append(specs, spec) - } - return "\n" + strings.Join(specs, "\n") -} - func csrfEnabled(options Options) bool { adapter := backendAdapterIR(options) return options.Config.Build.CSRF.EnabledForGeneratedEndpoints() && (adapter.HasEndpointKind(BackendEndpointAction) || adapter.HasCSRFSensitiveAPI() || contractExposuresParseForm(executableContractExposures(adapter.ContractExposures))) @@ -935,19 +906,6 @@ func (ir BackendAdapterIR) HasCSRFSensitiveAPI() bool { return false } -func csrfHelperSource(options Options) (source string, err error) { - defer recoverGeneratedIdentifierError(&err) - - if !csrfEnabled(options) { - return "", nil - } - return printActionDecls([]ast.Decl{ - csrfTokenSourceVarDecl(), - csrfValidatorVarDecl(), - csrfNewFuncDecl(options), - }) -} - func csrfTokenSourceVarDecl() ast.Decl { return &ast.GenDecl{ Tok: token.VAR, diff --git a/internal/appgen/source_actions.go b/internal/appgen/source_actions.go index 4ad01e02..02837431 100644 --- a/internal/appgen/source_actions.go +++ b/internal/appgen/source_actions.go @@ -14,11 +14,11 @@ import ( "github.com/cssbruno/gowdk/internal/source" ) -func actionHandlerSource(actions []ActionEndpoint, csrf bool) (source string, err error) { +func actionHandlerSource(actions []ActionEndpoint) (source string, err error) { defer recoverGeneratedIdentifierError(&err) sorted := backendAdapterIR(Options{Actions: actions}).Actions - decls := []ast.Decl{actionFuncDecl(sorted, csrf, false)} + decls := []ast.Decl{actionFuncDecl(sorted, false, false)} if len(sorted) > 0 { decls = append(decls, actionRequestPathDecl()) decls = append(decls, actionDecoderDecls(sorted)...) @@ -139,7 +139,7 @@ func actionFuncDecl(actions []BackendActionAdapter, csrf bool, rateLimit bool) * } results := boolResults() if actionsUseErrorPages(actions) { - results = namedBoolResults("handled") + results = namedBoolResults() } var clauses []ast.Stmt for _, action := range actions { @@ -185,12 +185,13 @@ func actionCaseStmts(action BackendActionAdapter, csrf bool, rateLimit bool) []a return stmts } stmts = append(stmts, actionParseFormStmts(action, csrf)...) - if actionUsesMultipart(action) { + switch { + case actionUsesMultipart(action): stmts = append(stmts, define([]ast.Expr{id("data")}, call(sel("gowdkform", "FromMultipartForm"), selExpr(id("request"), "MultipartForm"))), define([]ast.Expr{id("values")}, selExpr(id("data"), "Values")), ) - } else if actionNeedsData(action) { + case actionNeedsData(action): stmts = append(stmts, define([]ast.Expr{id("values")}, call(sel("gowdkform", "FromURLValues"), selExpr(id("request"), "PostForm"))), define([]ast.Expr{id("data")}, &ast.CompositeLit{ @@ -201,7 +202,7 @@ func actionCaseStmts(action BackendActionAdapter, csrf bool, rateLimit bool) []a }, }), ) - } else if actionNeedsValues(action) { + case actionNeedsValues(action): stmts = append(stmts, define([]ast.Expr{id("values")}, call(sel("gowdkform", "FromURLValues"), selExpr(id("request"), "PostForm")))) } stmts = append(stmts, actionInputDecodeStmts(action)...) @@ -752,7 +753,7 @@ func boundActionDecoderDecl(action BackendActionAdapter) *ast.FuncDecl { define([]ast.Expr{id("input")}, &ast.CompositeLit{Type: inputType}), } paramName := "values" - paramType := ast.Expr(sel("gowdkform", "Values")) + paramType := sel("gowdkform", "Values") if boundActionDecoderUsesData(action) { paramName = "data" paramType = sel("gowdkform", "Data") @@ -976,8 +977,8 @@ func boolResults() []*ast.Field { return []*ast.Field{{Type: id("bool")}} } -func namedBoolResults(name string) []*ast.Field { - return []*ast.Field{{Names: []*ast.Ident{id(name)}, Type: id("bool")}} +func namedBoolResults() []*ast.Field { + return []*ast.Field{{Names: []*ast.Ident{id("handled")}, Type: id("bool")}} } func funcDecl(name string, params []*ast.Field, results []*ast.Field, stmts []ast.Stmt) *ast.FuncDecl { @@ -1061,14 +1062,6 @@ func int64Lit(value int64) *ast.BasicLit { return &ast.BasicLit{Kind: token.INT, Value: strconv.FormatInt(value, 10)} } -func goString(value string) string { - return strconv.Quote(value) -} - -func quote(value string) string { - return strconv.Quote(path.Clean("/" + value)) -} - func cleanRoutePath(value string) string { return path.Clean("/" + value) } diff --git a/internal/appgen/source_api.go b/internal/appgen/source_api.go index 5ee22b9e..eb42a3c7 100644 --- a/internal/appgen/source_api.go +++ b/internal/appgen/source_api.go @@ -23,7 +23,7 @@ func apiFuncDecl(apis []BackendAPIAdapter, csrf bool, rateLimit bool) *ast.FuncD } results := boolResults() if apisUseErrorPages(apis) { - results = namedBoolResults("handled") + results = namedBoolResults() } var clauses []ast.Stmt for _, api := range apis { diff --git a/internal/appgen/source_backend.go b/internal/appgen/source_backend.go index 7a23c79d..9088fdb1 100644 --- a/internal/appgen/source_backend.go +++ b/internal/appgen/source_backend.go @@ -207,18 +207,6 @@ func sortAPIEndpoints(apis []APIEndpoint) { }) } -func backendProxySource(options Options) (source string, err error) { - defer recoverGeneratedIdentifierError(&err) - - if !options.ProxyBackend || !hasBackendRoutes(options) { - return "", nil - } - return printActionDecls([]ast.Decl{ - backendProxyDecl(false, generatedObservabilityEnabled(options)), - isBackendRouteDecl(backendAdapterIR(options)), - }) -} - func backendProxyDecl(rateLimit bool, trace bool) *ast.FuncDecl { stmts := []ast.Stmt{ define([]ast.Expr{id("routeMethod")}, selExpr(id("request"), "Method")), diff --git a/internal/appgen/source_backend_app.go b/internal/appgen/source_backend_app.go index ba38a3e9..71ec93bf 100644 --- a/internal/appgen/source_backend_app.go +++ b/internal/appgen/source_backend_app.go @@ -8,10 +8,6 @@ func backendAppPackageSource(options Options) (source string, err error) { return printGoFile("gowdkapp", imports, append(backendShellDecls(options), backendGeneratedDecls(options)...)) } -func backendRuntimeImportSource(options Options) string { - return importSpecSource(backendRuntimeImportMap(options)) -} - func backendRuntimeImportMap(options Options) map[string]string { imports := map[string]string{ "gowdkruntime": "github.com/cssbruno/gowdk/runtime/app", diff --git a/internal/appgen/source_guards.go b/internal/appgen/source_guards.go index d602740e..f2740c85 100644 --- a/internal/appgen/source_guards.go +++ b/internal/appgen/source_guards.go @@ -161,10 +161,6 @@ func denyByOmissionJSONStmts() []ast.Stmt { } } -func generatedUsesCustomGuards(options Options) bool { - return generatedRequiresAppGuardRegistry(options) -} - func generatedRequiresAppGuardRegistry(options Options) bool { for _, name := range generatedGuardNames(options) { if auth.IsPublicGuard(name) { diff --git a/internal/appgen/source_lifecycle.go b/internal/appgen/source_lifecycle.go index 20082f2b..ee1f334d 100644 --- a/internal/appgen/source_lifecycle.go +++ b/internal/appgen/source_lifecycle.go @@ -109,7 +109,7 @@ func configuredServicesDecl(providers []lifecycleServiceProvider) ast.Decl { func lifecycleServiceFileSources(options Options) (map[string][]byte, error) { providers := lifecycleServiceProviders(options) if len(providers) == 0 { - return nil, nil + return map[string][]byte{}, nil } source, err := lifecycleServiceFileSource("!js", providers) if err != nil { diff --git a/internal/appgen/source_ssr.go b/internal/appgen/source_ssr.go index 054d9c13..0bd5ee62 100644 --- a/internal/appgen/source_ssr.go +++ b/internal/appgen/source_ssr.go @@ -9,27 +9,6 @@ import ( "github.com/cssbruno/gowdk/internal/source" ) -func ssrHandlerSource(routes []SSRRoute) (source string, err error) { - defer recoverGeneratedIdentifierError(&err) - - sorted := sortedSSRRoutes(routes) - return printActionDecls([]ast.Decl{ - ssrExactDecl(sorted, false, false, false), - ssrDynamicDecl(sorted, false, false, false), - }) -} - -func sortedSSRRoutes(routes []SSRRoute) []SSRRoute { - sorted := append([]SSRRoute(nil), routes...) - sort.Slice(sorted, func(i, j int) bool { - if sorted[i].Route == sorted[j].Route { - return sorted[i].PageID < sorted[j].PageID - } - return sorted[i].Route < sorted[j].Route - }) - return sorted -} - func ssrExactDecl(routes []SSRRoute, rateLimit bool, csrf bool, trace bool) *ast.FuncDecl { clauses := []ast.Stmt{} for _, route := range routes { @@ -41,7 +20,7 @@ func ssrExactDecl(routes []SSRRoute, rateLimit bool, csrf bool, trace bool) *ast Body: ssrRouteBodyStmts(route, false, rateLimit, csrf, trace), }) } - return funcDecl("ssrExact", actionParams(), namedBoolResults("handled"), []ast.Stmt{ + return funcDecl("ssrExact", actionParams(), namedBoolResults(), []ast.Stmt{ &ast.SwitchStmt{ Tag: selExpr(selExpr(id("request"), "URL"), "Path"), Body: &ast.BlockStmt{List: clauses}, @@ -59,7 +38,7 @@ func ssrDynamicDecl(routes []SSRRoute, rateLimit bool, csrf bool, trace bool) *a body = append(body, ssrDynamicIfStmt(route, rateLimit, csrf, trace)) } body = append(body, returnBool(false)) - return funcDecl("ssrDynamic", actionParams(), namedBoolResults("handled"), body) + return funcDecl("ssrDynamic", actionParams(), namedBoolResults(), body) } func ssrDynamicIfStmt(route SSRRoute, rateLimit bool, csrf bool, trace bool) ast.Stmt { diff --git a/internal/auditsarif/sarif.go b/internal/auditsarif/sarif.go index ebad9665..3ebf89ee 100644 --- a/internal/auditsarif/sarif.go +++ b/internal/auditsarif/sarif.go @@ -317,7 +317,7 @@ func looksLikeFilePath(uri string) bool { return false } for _, char := range uri[dot+1:] { - if !(char >= 'a' && char <= 'z') && !(char >= 'A' && char <= 'Z') { + if (char < 'a' || char > 'z') && (char < 'A' || char > 'Z') { return false } } diff --git a/internal/auditspec/engine_test.go b/internal/auditspec/engine_test.go index 8e7fb9ce..d778389c 100644 --- a/internal/auditspec/engine_test.go +++ b/internal/auditspec/engine_test.go @@ -80,11 +80,15 @@ func TestBaselineFlagsMissingAndUnsafeRequestLimits(t *testing.T) { // No raw cap recorded at all -> missing. {ID: "Submit", Kind: "action", Method: "POST", Path: "/signup", Guards: []string{"auth.required"}, CSRF: true}, // Cap recorded but installed after parse -> phase unsafe. - {ID: "Ingest", Kind: "api", Method: "POST", Path: "/api/ingest", Guards: []string{"permission:x"}, CSRF: true, - RequestLimits: securitymanifest.RequestLimitPosture{EndpointKind: "api", RawBodyBytes: 1 << 20, InstalledBeforeParse: false}}, + { + ID: "Ingest", Kind: "api", Method: "POST", Path: "/api/ingest", Guards: []string{"permission:x"}, CSRF: true, + RequestLimits: securitymanifest.RequestLimitPosture{EndpointKind: "api", RawBodyBytes: 1 << 20, InstalledBeforeParse: false}, + }, // Multipart accepted without a multipart cap -> unbounded multipart. - {ID: "Upload", Kind: "action", Method: "POST", Path: "/upload", Guards: []string{"auth.required"}, CSRF: true, - RequestLimits: securitymanifest.RequestLimitPosture{EndpointKind: "action", RawBodyBytes: 1 << 20, InstalledBeforeParse: true, MultipartEnabled: true}}, + { + ID: "Upload", Kind: "action", Method: "POST", Path: "/upload", Guards: []string{"auth.required"}, CSRF: true, + RequestLimits: securitymanifest.RequestLimitPosture{EndpointKind: "action", RawBodyBytes: 1 << 20, InstalledBeforeParse: true, MultipartEnabled: true}, + }, }, } got := codes(Evaluate(manifest, Baseline())) diff --git a/internal/buildgen/build.go b/internal/buildgen/build.go index 9afadba2..f8c6ae33 100644 --- a/internal/buildgen/build.go +++ b/internal/buildgen/build.go @@ -201,19 +201,19 @@ func recordWriteStat(result *Result, wrote bool) { } func finalizeCSSArtifact(artifact *plannedCSSArtifact) { - artifact.CSSArtifact.Hash = contentHash(artifact.contents) - artifact.CSSArtifact.CachePolicy = immutableAssetCachePolicy - artifact.CSSArtifact.SizeBytes = int64(len(artifact.contents)) + artifact.Hash = contentHash(artifact.contents) + artifact.CachePolicy = immutableAssetCachePolicy + artifact.SizeBytes = int64(len(artifact.contents)) } func finalizeAssetArtifact(artifact *plannedAssetArtifact) { - if artifact.AssetArtifact.Hash == "" { - artifact.AssetArtifact.Hash = contentHash(artifact.contents) + if artifact.Hash == "" { + artifact.Hash = contentHash(artifact.contents) } - if artifact.AssetArtifact.CachePolicy == "" { - artifact.AssetArtifact.CachePolicy = noCacheAssetCachePolicy + if artifact.CachePolicy == "" { + artifact.CachePolicy = noCacheAssetCachePolicy } - artifact.AssetArtifact.SizeBytes = int64(len(artifact.contents)) + artifact.SizeBytes = int64(len(artifact.contents)) } func BuildMemory(config gowdk.Config, sources gwdkanalysis.Sources, outputDir string) (MemoryResult, error) { @@ -746,7 +746,8 @@ func BuildIncrementalFromValidatedProgram(config gowdk.Config, validated compile if err != nil { return Result{}, reporter.fail("plan", err) } - runtime = append(componentAssets, append(scopedJS, runtime...)...) + runtime = append(scopedJS, runtime...) + runtime = append(componentAssets, runtime...) var obfuscations []assetObfuscationRecord runtime, obfuscations, err = applyAssetObfuscation(config, outputDir, runtime) if err != nil { @@ -1116,7 +1117,8 @@ func planFromIR(config gowdk.Config, ir gwdkir.Program, outputDir string) (build if err != nil { return buildPlan{}, err } - assets := append(componentAssets, scopedJS...) + assets := append([]plannedAssetArtifact{}, componentAssets...) + assets = append(assets, scopedJS...) assets = append(assets, runtime...) assets, obfuscations, err := applyAssetObfuscation(config, outputDir, assets) if err != nil { diff --git a/internal/buildgen/build_data_runner.go b/internal/buildgen/build_data_runner.go index 6e9844b0..786e17c3 100644 --- a/internal/buildgen/build_data_runner.go +++ b/internal/buildgen/build_data_runner.go @@ -68,7 +68,7 @@ func isStaticPackageGoBlockTarget(target string) bool { func runInlineBuildDataCall(script gwdkir.GoBlock, imports []gwdkir.Import, source string, function string, routeParams map[string]string, locale string) (map[string]string, error) { var lastErr error - for _, candidate := range buildDataRunnerCandidates(routeParams) { + for _, candidate := range buildDataRunnerCandidates() { runnerSource, err := inlineBuildDataRunnerSource(script, imports, source, function, candidate, routeParams, locale) if err != nil { return nil, err @@ -87,7 +87,7 @@ type buildDataRunnerCandidate struct { withParams bool } -func buildDataRunnerCandidates(routeParams map[string]string) []buildDataRunnerCandidate { +func buildDataRunnerCandidates() []buildDataRunnerCandidate { return []buildDataRunnerCandidate{ {returnsError: true}, {returnsError: false}, @@ -293,7 +293,7 @@ func sourceDir(source string) string { func runBuildDataCall(alias, importPath, function string, workDir string, routeParams map[string]string, locale string) (map[string]string, error) { var lastErr error - for _, candidate := range buildDataRunnerCandidates(routeParams) { + for _, candidate := range buildDataRunnerCandidates() { source, err := buildDataRunnerSource(alias, importPath, function, candidate, routeParams, locale) if err != nil { return nil, err @@ -486,7 +486,9 @@ func runBuildDataRunner(source string, workDir string, label string) (map[string return nil, err } path := file.Name() - defer os.Remove(path) + defer func() { + _ = os.Remove(path) + }() if err := file.Close(); err != nil { return nil, err } diff --git a/internal/buildgen/build_data_values.go b/internal/buildgen/build_data_values.go index 8d5be139..74a0ab0d 100644 --- a/internal/buildgen/build_data_values.go +++ b/internal/buildgen/build_data_values.go @@ -1,7 +1,6 @@ package buildgen import ( - "encoding/json" "fmt" "go/ast" "go/parser" @@ -75,8 +74,7 @@ func buildObjectValue(order []string, fields map[string]buildValue) buildValue { func (v buildValue) jsonText() string { switch v.kind { case buildValueString: - encoded, _ := json.Marshal(v.text) - return string(encoded) + return strconv.Quote(v.text) case buildValueNumber: return canonicalJSONNumber(v.text, v.number) case buildValueBool: @@ -92,8 +90,7 @@ func (v buildValue) jsonText() string { case buildValueObject: parts := make([]string, 0, len(v.order)) for _, name := range v.order { - key, _ := json.Marshal(name) - parts = append(parts, string(key)+":"+v.fields[name].jsonText()) + parts = append(parts, strconv.Quote(name)+":"+v.fields[name].jsonText()) } return "{" + strings.Join(parts, ",") + "}" default: diff --git a/internal/buildgen/components.go b/internal/buildgen/components.go index c53d47da..6146788a 100644 --- a/internal/buildgen/components.go +++ b/internal/buildgen/components.go @@ -380,10 +380,7 @@ func componentInitialState(component gwdkir.Component) (map[string]string, map[s // A typed `use ` declaration contributes the store's fields to // the island seed so SSR and the initial client state carry the right shape; // the store registry merges its actual (init or persisted) value on mount. - added, err := mergeComponentStoreSeed(component, state, stateTypes, raw) - if err != nil { - return nil, nil, "", err - } + added := mergeComponentStoreSeed(component, state, stateTypes, raw) if len(state) == 0 && !added { return nil, nil, "", nil } @@ -404,13 +401,13 @@ func componentInitialState(component gwdkir.Component) (map[string]string, map[s // contract. It reports whether any field was added so the caller knows to // re-marshal the seed JSON. Type-resolution failures are reported by contract // validation, so they are swallowed here. -func mergeComponentStoreSeed(component gwdkir.Component, state map[string]string, stateTypes map[string]clientlang.ValueType, raw map[string]any) (bool, error) { +func mergeComponentStoreSeed(component gwdkir.Component, state map[string]string, stateTypes map[string]clientlang.ValueType, raw map[string]any) bool { if strings.TrimSpace(component.Blocks.ClientBody) == "" { - return false, nil + return false } program, err := clientlang.Parse(component.Blocks.ClientBody) if err != nil { - return false, nil + return false } added := false for _, use := range program.Uses { @@ -439,7 +436,7 @@ func mergeComponentStoreSeed(component gwdkir.Component, state map[string]string } } } - return added, nil + return added } func zeroStateValue(typ clientlang.ValueType) any { diff --git a/internal/buildgen/css_minify.go b/internal/buildgen/css_minify.go index 90c84973..3857b4bb 100644 --- a/internal/buildgen/css_minify.go +++ b/internal/buildgen/css_minify.go @@ -35,7 +35,7 @@ func minifyCSS(contents []byte) []byte { } if current == '/' && index+1 < len(runes) && runes[index+1] == '*' { index++ - for index+1 < len(runes) && !(runes[index] == '*' && runes[index+1] == '/') { + for index+1 < len(runes) && (runes[index] != '*' || runes[index+1] != '/') { index++ } if index+1 < len(runes) { @@ -57,7 +57,7 @@ func minifyCSS(contents []byte) []byte { pendingSpace = true continue } - if isCSSPunctuation(current) && !(parenDepth > 0 && current == '+') { + if isCSSPunctuation(current) && (parenDepth <= 0 || current != '+') { if current == '(' { parenDepth++ } diff --git a/internal/buildgen/css_scope.go b/internal/buildgen/css_scope.go index 7441d51a..f3cbf9c6 100644 --- a/internal/buildgen/css_scope.go +++ b/internal/buildgen/css_scope.go @@ -212,13 +212,13 @@ func scopeCSSRules(contents string, scopeSelector string) string { break } open += cursor - close := matchingCSSBrace(contents, open) - if close < 0 { + closeIndex := matchingCSSBrace(contents, open) + if closeIndex < 0 { parts = append(parts, contents[cursor:]) break } prefix := contents[cursor:open] - body := contents[open+1 : close] + body := contents[open+1 : closeIndex] selector := strings.TrimSpace(prefix) switch { case selector == "": @@ -236,7 +236,7 @@ func scopeCSSRules(contents string, scopeSelector string) string { trailing := prefix[len(strings.TrimRight(prefix, " \n\r\t\f")):] parts = append(parts, leading, scopeCSSSelectorList(selector, scopeSelector), trailing, "{", body, "}") } - cursor = close + 1 + cursor = closeIndex + 1 } return strings.Join(parts, "") } diff --git a/internal/buildgen/data.go b/internal/buildgen/data.go index 03b34bc6..048d1f70 100644 --- a/internal/buildgen/data.go +++ b/internal/buildgen/data.go @@ -39,10 +39,6 @@ func parsePathDeclarationsFromBlocks(blocks gwdkir.Blocks) ([]map[string]string, return declarations, nil } -func parsePathParams(source string) (map[string]string, error) { - return parseLiteralStringMap(source, "path param") -} - func parseBuildData(body string, routeParams map[string]string, locale string, imports []gwdkir.Import, scripts []gwdkir.GoBlock, source string) (map[string]string, error) { lines := significantBuildLines(body) if len(lines) == 1 { @@ -81,7 +77,7 @@ func parseBuildData(body string, routeParams map[string]string, locale string, i } } if declarations == 0 { - return nil, nil + return map[string]string{}, nil } return buildValueStrings(data), nil } diff --git a/internal/buildgen/island_source_map.go b/internal/buildgen/island_source_map.go index 887f803d..c68f3b87 100644 --- a/internal/buildgen/island_source_map.go +++ b/internal/buildgen/island_source_map.go @@ -68,12 +68,10 @@ func sourceMapMappings(component gwdkir.Component, generatedSource string) strin } mappings := make([]string, 0, maxLine) - previousGeneratedColumn := 0 previousSourceIndex := 0 previousSourceLine := 0 previousSourceColumn := 0 for line := 1; line <= maxLine; line++ { - previousGeneratedColumn = 0 anchor, ok := byLine[line] if !ok { mappings = append(mappings, "") @@ -82,12 +80,11 @@ func sourceMapMappings(component gwdkir.Component, generatedSource string) strin sourceLine := anchor.sourceLine - 1 sourceColumn := anchor.sourceColumn - 1 mappings = append(mappings, - sourceMapVLQ(0-previousGeneratedColumn)+ + sourceMapVLQ(0)+ sourceMapVLQ(0-previousSourceIndex)+ sourceMapVLQ(sourceLine-previousSourceLine)+ sourceMapVLQ(sourceColumn-previousSourceColumn), ) - previousGeneratedColumn = 0 previousSourceIndex = 0 previousSourceLine = sourceLine previousSourceColumn = sourceColumn diff --git a/internal/buildgen/islands_test.go b/internal/buildgen/islands_test.go index f5678ced..cf535dd2 100644 --- a/internal/buildgen/islands_test.go +++ b/internal/buildgen/islands_test.go @@ -2732,7 +2732,7 @@ func TestBuildRejectsWASMIslandPackageMissingABIExports(t *testing.T) { t.Fatalf("expected %q in error: %v", expected, err) } } - requireBuildDiagnostic(t, err, "wasm_package_export_error", "Counter") + requireBuildDiagnostic(t, err, "wasm_package_export_error") } func TestBuildRejectsWASMIslandPackageBadABIExportSignature(t *testing.T) { @@ -2769,7 +2769,7 @@ func main() {} if !strings.Contains(err.Error(), `WASM export GOWDKMountCounter must have signature func() uint32`) { t.Fatalf("unexpected bad wasm export signature error: %v", err) } - requireBuildDiagnostic(t, err, "wasm_package_export_error", "Counter") + requireBuildDiagnostic(t, err, "wasm_package_export_error") } func TestBuildRejectsWASMIslandPackageWithoutMainEntrypoint(t *testing.T) { @@ -2797,7 +2797,7 @@ func TestBuildRejectsWASMIslandPackageWithoutMainEntrypoint(t *testing.T) { !strings.Contains(err.Error(), "declare a package main with a main function") { t.Fatalf("unexpected error: %v", err) } - requireBuildDiagnostic(t, err, "wasm_package_entrypoint_error", "Counter") + requireBuildDiagnostic(t, err, "wasm_package_entrypoint_error") } func TestBuildSurfacesWASMIslandPackageImportErrors(t *testing.T) { @@ -2832,7 +2832,7 @@ func main() {}`) t.Fatalf("expected %q in error: %v", expected, err) } } - requireBuildDiagnostic(t, err, "wasm_package_build_error", "Counter") + requireBuildDiagnostic(t, err, "wasm_package_build_error") } func TestBuildRejectsUnsupportedWASMIslandPackageImports(t *testing.T) { @@ -2867,10 +2867,10 @@ func main() {}`) t.Fatalf("expected %q in error: %v", expected, err) } } - requireBuildDiagnostic(t, err, "unsupported_wasm_import", "Counter") + requireBuildDiagnostic(t, err, "unsupported_wasm_import") } -func requireBuildDiagnostic(t *testing.T, err error, code string, componentName string) { +func requireBuildDiagnostic(t *testing.T, err error, code string) { t.Helper() var buildErr *BuildError if !errors.As(err, &buildErr) { @@ -2880,7 +2880,7 @@ func requireBuildDiagnostic(t *testing.T, err error, code string, componentName t.Fatalf("expected one build diagnostic, got %#v", buildErr.Diagnostics) } diagnostic := buildErr.Diagnostics[0] - if diagnostic.Code != code || diagnostic.ComponentName != componentName { + if diagnostic.Code != code || diagnostic.ComponentName != "Counter" { t.Fatalf("unexpected build diagnostic: %#v", diagnostic) } } diff --git a/internal/buildgen/render.go b/internal/buildgen/render.go index e51a004a..8e6e3433 100644 --- a/internal/buildgen/render.go +++ b/internal/buildgen/render.go @@ -563,7 +563,7 @@ func isInternalNavigationHref(value string) bool { if value == "" || strings.ContainsAny(value, "{}") { return false } - return value[0] == '/' && !(len(value) > 1 && (value[1] == '/' || value[1] == '\\')) + return value[0] == '/' && (len(value) <= 1 || (value[1] != '/' && value[1] != '\\')) } type pageStoreSeed struct { diff --git a/internal/buildgen/runtime_asset_paths.go b/internal/buildgen/runtime_asset_paths.go index 502a5eba..40293dbb 100644 --- a/internal/buildgen/runtime_asset_paths.go +++ b/internal/buildgen/runtime_asset_paths.go @@ -2,6 +2,7 @@ package buildgen import ( "fmt" + "go/build" "os" "path" "path/filepath" @@ -18,7 +19,7 @@ import ( // matches the emitted glue even when this binary was compiled by a different // toolchain. It falls back to the build version when VERSION is unreadable. func islandWASMExecGoVersion() string { - if contents, err := os.ReadFile(filepath.Join(runtime.GOROOT(), "VERSION")); err == nil { + if contents, err := os.ReadFile(filepath.Join(wasmGoRoot(), "VERSION")); err == nil { line, _, _ := strings.Cut(strings.TrimSpace(string(contents)), "\n") if line = strings.TrimSpace(line); line != "" { return line @@ -47,7 +48,7 @@ func clientGoBlockWASMLoaderArtifact(outputDir string, page gwdkir.Page) planned func islandWASMExecArtifact(outputDir string) (plannedAssetArtifact, error) { assetPath := islandWASMExecAssetPath() - contents, err := os.ReadFile(filepath.Join(runtime.GOROOT(), "lib", "wasm", "wasm_exec.js")) + contents, err := os.ReadFile(filepath.Join(wasmGoRoot(), "lib", "wasm", "wasm_exec.js")) if err != nil { return plannedAssetArtifact{}, fmt.Errorf("read Go wasm_exec.js runtime: %w", err) } @@ -57,6 +58,10 @@ func islandWASMExecArtifact(outputDir string) (plannedAssetArtifact, error) { }, nil } +func wasmGoRoot() string { + return strings.TrimSpace(build.Default.GOROOT) +} + func islandJSAssetPath(packageName, componentName string) string { return islandComponentAssetPath(packageName, componentName, ".js") } diff --git a/internal/buildgen/runtime_islands.go b/internal/buildgen/runtime_islands.go index 880a9d6f..feda59a8 100644 --- a/internal/buildgen/runtime_islands.go +++ b/internal/buildgen/runtime_islands.go @@ -71,10 +71,6 @@ func islandRuntimeArtifacts(config gowdk.Config, pages []gwdkir.Page, allCompone return artifacts, nil } -func islandScriptHrefs(source string, components map[string]view.Component, ownerPackage string, uses map[string]string) ([]string, error) { - return islandScriptHrefsForView(source, nil, components, ownerPackage, uses) -} - func islandScriptHrefsForView(source string, nodes []view.Node, components map[string]view.Component, ownerPackage string, uses map[string]string) ([]string, error) { usages, err := recursiveComponentCallUsagesForView(source, nodes, components, ownerPackage, uses, viewComponentResolver) if err != nil { @@ -154,18 +150,10 @@ var manifestComponentResolver = componentResolver[gwdkir.Component]{ Uses: func(component gwdkir.Component) map[string]string { return componentUses(component.Uses) }, } -func recursiveViewComponentCallUsages(source string, components map[string]view.Component, ownerPackage string, uses map[string]string) ([]resolvedComponentCallUsage[view.Component], error) { - return recursiveViewComponentCallUsagesForView(source, nil, components, ownerPackage, uses) -} - func recursiveViewComponentCallUsagesForView(source string, nodes []view.Node, components map[string]view.Component, ownerPackage string, uses map[string]string) ([]resolvedComponentCallUsage[view.Component], error) { return recursiveComponentCallUsagesForView(source, nodes, components, ownerPackage, uses, viewComponentResolver) } -func recursiveComponentCallUsages[T any](source string, components map[string]T, ownerPackage string, uses map[string]string, resolver componentResolver[T]) ([]resolvedComponentCallUsage[T], error) { - return recursiveComponentCallUsagesForView(source, nil, components, ownerPackage, uses, resolver) -} - func recursiveComponentCallUsagesForView[T any](source string, nodes []view.Node, components map[string]T, ownerPackage string, uses map[string]string, resolver componentResolver[T]) ([]resolvedComponentCallUsage[T], error) { var usages []resolvedComponentCallUsage[T] visiting := map[string]bool{} @@ -231,19 +219,6 @@ func lookupComponent[T any](components map[string]T, name string, ownerPackage s return component, ok } -func statefulComponentNames(components []gwdkir.Component) map[string]bool { - out := map[string]bool{} - for _, component := range components { - if componentNeedsJSIsland(component) { - out[component.Name] = true - if component.Package != "" { - out[component.Package+"."+component.Name] = true - } - } - } - return out -} - func componentNeedsJSIsland(component gwdkir.Component) bool { return component.State.Type.Name != "" || component.Blocks.Client || len(component.Emits) > 0 || componentViewHasAwait(component.Blocks.ViewBody, component.Blocks.ViewNodes) } diff --git a/internal/buildgen/runtime_wasm_assets.go b/internal/buildgen/runtime_wasm_assets.go index 16f6bf88..f5822091 100644 --- a/internal/buildgen/runtime_wasm_assets.go +++ b/internal/buildgen/runtime_wasm_assets.go @@ -102,7 +102,9 @@ func buildWASMIslandPackage(component gwdkir.Component) ([]byte, error) { _ = os.Remove(tempPath) return nil, wasmIslandDiagnosticError(component, "wasm_package_build_error", packagePath, fmt.Errorf("close temp output: %w", err)) } - defer os.Remove(tempPath) + defer func() { + _ = os.Remove(tempPath) + }() dir, buildPackage, err := wasmIslandBuildContext(packagePath) if err != nil { @@ -155,7 +157,9 @@ func buildClientGoBlockWASM(page gwdkir.Page, script gwdkir.GoBlock) ([]byte, er _ = os.Remove(tempPath) return nil, clientGoBlockDiagnosticError(page, "client_go_block_wasm_build_error", fmt.Errorf("close temp output: %w", err)) } - defer os.Remove(tempPath) + defer func() { + _ = os.Remove(tempPath) + }() source, err := clientGoBlockWASMSource(page, script) if err != nil { @@ -174,7 +178,9 @@ func buildClientGoBlockWASM(page gwdkir.Page, script gwdkir.GoBlock) ([]byte, er _ = os.Remove(sourcePath) return nil, clientGoBlockDiagnosticError(page, "client_go_block_wasm_build_error", fmt.Errorf("write temp source: %w", err)) } - defer os.Remove(sourcePath) + defer func() { + _ = os.Remove(sourcePath) + }() if err := validateClientGoBlockWASMImports(page, sourcePath); err != nil { return nil, err } diff --git a/internal/buildgen/seo.go b/internal/buildgen/seo.go index c924aa43..cb3097ca 100644 --- a/internal/buildgen/seo.go +++ b/internal/buildgen/seo.go @@ -122,9 +122,7 @@ func RuntimeSitemapPlanFromValidatedProgram(config gowdk.Config, validated compi urls = append(urls, runtimeseo.URL{Loc: output.route}) } } - for _, extra := range options.ExtraURLs { - urls = append(urls, runtimeseo.URL(extra)) - } + urls = append(urls, options.ExtraURLs...) return RuntimeSitemapPlan{ Enabled: true, BaseURL: options.BaseURL, diff --git a/internal/buildgen/ssr_list_test.go b/internal/buildgen/ssr_list_test.go index 38be2a76..e48f161f 100644 --- a/internal/buildgen/ssr_list_test.go +++ b/internal/buildgen/ssr_list_test.go @@ -53,6 +53,7 @@ func toRuntimeCondSpecs(specs []source.SSRCondSpec) []gowdkssr.CondSpec { } func buildSSRListArtifact(t *testing.T, view string) SSRArtifact { + t.Helper() return buildSSRRegionArtifact(t, `=> { columns }`, view) } diff --git a/internal/buildgen/ssr_url_placeholders.go b/internal/buildgen/ssr_url_placeholders.go index 4aa27ce8..55e4349b 100644 --- a/internal/buildgen/ssr_url_placeholders.go +++ b/internal/buildgen/ssr_url_placeholders.go @@ -133,7 +133,7 @@ func markURLPlaceholdersInValue(value string, replacements []SSRReplacement, loa } func urlPlaceholder(base string, next *int) string { - *next = *next + 1 + *next++ suffix := "_URL_" + strconv.Itoa(*next) if strings.HasSuffix(base, "__") { return strings.TrimSuffix(base, "__") + suffix + "__" diff --git a/internal/clientlang/clientlang.go b/internal/clientlang/clientlang.go index 24d97334..30b8ce70 100644 --- a/internal/clientlang/clientlang.go +++ b/internal/clientlang/clientlang.go @@ -104,24 +104,25 @@ func parseFunctionHeader(line string) (functionHeader, bool) { async = true line = strings.TrimSpace(strings.TrimPrefix(line, "async ")) } - if strings.HasPrefix(line, "fn ") { + switch { + case strings.HasPrefix(line, "fn "): line = strings.TrimSpace(strings.TrimPrefix(line, "fn ")) - } else if strings.HasPrefix(line, "func ") { + case strings.HasPrefix(line, "func "): line = strings.TrimSpace(strings.TrimPrefix(line, "func ")) - } else { + default: return functionHeader{}, false } open := strings.Index(line, "(") - close := strings.LastIndex(line, ")") - if open <= 0 || close < open || close == -1 { + closeIndex := strings.LastIndex(line, ")") + if open <= 0 || closeIndex < open || closeIndex == -1 { return functionHeader{}, false } name := strings.TrimSpace(line[:open]) if !isIdentifier(name) { return functionHeader{}, false } - params := line[open+1 : close] - returnType := strings.TrimSpace(line[close+1:]) + params := line[open+1 : closeIndex] + returnType := strings.TrimSpace(line[closeIndex+1:]) if returnType != "" && !isIdentifier(returnType) { return functionHeader{}, false } @@ -1117,7 +1118,7 @@ func ParseEmitCall(expr string) (EmitCall, bool) { if !ok { return EmitCall{}, false } - return EmitCall{Name: call.Name, Args: call.Args}, true + return EmitCall(call), true } func parseParams(source string) ([]Param, error) { diff --git a/internal/clientlang/expr_eval.go b/internal/clientlang/expr_eval.go index d96fb5d2..8bc673cc 100644 --- a/internal/clientlang/expr_eval.go +++ b/internal/clientlang/expr_eval.go @@ -188,12 +188,16 @@ func evalLiteral(expr LiteralExpr) (any, error) { case TypeBool: return expr.Value == "true", nil case TypeNil: - return nil, nil + return nilLiteralValue(), nil default: return nil, fmt.Errorf("unknown literal type %q", expr.Type) } } +func nilLiteralValue() any { + return nil +} + func evalBinary(op string, left, right any) (any, error) { switch op { case "+": diff --git a/internal/clientlang/expr_parser.go b/internal/clientlang/expr_parser.go index d8fa3b80..7cd5d0d2 100644 --- a/internal/clientlang/expr_parser.go +++ b/internal/clientlang/expr_parser.go @@ -200,7 +200,7 @@ func (parser *exprParser) parsePostfix() (Expr, error) { if token.value != "" { return nil, fmt.Errorf("expected field name after ., got %q", token.value) } - return nil, fmt.Errorf("expected field name after .") + return nil, fmt.Errorf("expected field name after dot") } expr = MemberExpr{X: expr, Name: token.value, Span: mergeExprSpans(ExprSpan(expr), tokenSpan(token))} case tokenLBracket: @@ -222,11 +222,11 @@ func (parser *exprParser) parsePostfix() (Expr, error) { if !ok { return nil, fmt.Errorf("only helper names can be called") } - args, close, err := parser.parseCallArgs() + args, closeToken, err := parser.parseCallArgs() if err != nil { return nil, err } - expr = CallExpr{Name: name.Name, Args: args, Span: mergeExprSpans(ExprSpan(name), tokenSpan(close))} + expr = CallExpr{Name: name.Name, Args: args, Span: mergeExprSpans(ExprSpan(name), tokenSpan(closeToken))} default: return expr, nil } @@ -238,8 +238,8 @@ func (parser *exprParser) parseCallArgs() ([]Expr, exprToken, error) { return nil, exprToken{}, parser.expected("opening ( for helper call", token) } if parser.peek().kind == tokenRParen { - close := parser.consume() - return nil, close, nil + closeToken := parser.consume() + return nil, closeToken, nil } var args []Expr for { diff --git a/internal/clientlang/island_validation.go b/internal/clientlang/island_validation.go index d2fab9ac..84335596 100644 --- a/internal/clientlang/island_validation.go +++ b/internal/clientlang/island_validation.go @@ -754,12 +754,12 @@ func parseIslandLet(source string) (string, string, string, bool) { if !ok { return "", "", "", false } - rest = strings.TrimLeftFunc(rest, func(r rune) bool { return isSpaceRune(r) }) + rest = strings.TrimLeftFunc(rest, isSpaceRune) typ, rest, ok := nextIslandIdent(rest) if !ok { return "", "", "", false } - rest = strings.TrimLeftFunc(rest, func(r rune) bool { return isSpaceRune(r) }) + rest = strings.TrimLeftFunc(rest, isSpaceRune) if !strings.HasPrefix(rest, "=") { return "", "", "", false } diff --git a/internal/clientrt/expression_conformance_test.go b/internal/clientrt/expression_conformance_test.go index d3ae33d6..a95af46c 100644 --- a/internal/clientrt/expression_conformance_test.go +++ b/internal/clientrt/expression_conformance_test.go @@ -307,7 +307,7 @@ type expressionConformanceResult struct { func runIslandExpressionConformanceHarness(t *testing.T, node string, cases []expressionConformanceCase) []expressionConformanceResult { t.Helper() - script := islandExpressionConformanceHarness(t, string(IslandRuntimeSource()), cases) + script := islandExpressionConformanceHarness(t, IslandRuntimeSource(), cases) path := filepath.Join(t.TempDir(), "gowdk-expression-conformance.js") if err := os.WriteFile(path, []byte(script), 0o600); err != nil { t.Fatal(err) diff --git a/internal/compiler/backend_bindings_test.go b/internal/compiler/backend_bindings_test.go index c4ed7fe1..895df206 100644 --- a/internal/compiler/backend_bindings_test.go +++ b/internal/compiler/backend_bindings_test.go @@ -1,6 +1,7 @@ package compiler import ( + "errors" "fmt" "os" "path/filepath" @@ -123,13 +124,13 @@ func BrokenFragment(context.Context, *http.Request) (response.Response, error) { }}}) bindings := compilerBindingsByBlock(app.BackendBindings) - assertBinding(t, bindings["Ping"], source.BackendBindingBound, source.BackendSignatureAction0, "", false) - assertBinding(t, bindings["Login"], source.BackendBindingBound, source.BackendSignatureActionForm, "LoginInput", false) - assertBinding(t, bindings["LoginPtr"], source.BackendBindingBound, source.BackendSignatureActionFormPtr, "LoginInput", true) - assertBinding(t, bindings["Raw"], source.BackendBindingBound, source.BackendSignatureActionValues, "", false) - assertBinding(t, bindings["RawData"], source.BackendBindingBound, source.BackendSignatureActionData, "", false) - assertBinding(t, bindings["Upload"], source.BackendBindingBound, source.BackendSignatureActionForm, "UploadInput", false) - assertBinding(t, bindings["Session"], source.BackendBindingBound, source.BackendSignatureAPI, "", false) + assertBinding(t, bindings["Ping"], source.BackendSignatureAction0, "", false) + assertBinding(t, bindings["Login"], source.BackendSignatureActionForm, "LoginInput", false) + assertBinding(t, bindings["LoginPtr"], source.BackendSignatureActionFormPtr, "LoginInput", true) + assertBinding(t, bindings["Raw"], source.BackendSignatureActionValues, "", false) + assertBinding(t, bindings["RawData"], source.BackendSignatureActionData, "", false) + assertBinding(t, bindings["Upload"], source.BackendSignatureActionForm, "UploadInput", false) + assertBinding(t, bindings["Session"], source.BackendSignatureAPI, "", false) assertInputFields(t, bindings["Login"].InputFields, "Email:email:string,Tags:tag:[]string,Remember:remember:bool,Age:age:int,Score:score:uint64,Code:code:byte,Letter:letter:rune") assertInputFields(t, bindings["Upload"].InputFields, "Avatar:avatar:form.File,Photos:photos:[]form.File,Caption:caption:string") if got := bindings["Broken"]; got.Status != source.BackendBindingUnsupportedSignature { @@ -144,7 +145,7 @@ func BrokenFragment(context.Context, *http.Request) (response.Response, error) { if got := bindings["Missing"]; got.Status != source.BackendBindingMissing { t.Fatalf("expected Missing binding, got %#v", got) } - assertBinding(t, bindings["List"], source.BackendBindingBound, source.BackendSignatureFragment, "", false) + assertBinding(t, bindings["List"], source.BackendSignatureFragment, "", false) if got := bindings["BrokenFragment"]; got.Status != source.BackendBindingUnsupportedSignature { t.Fatalf("expected BrokenFragment unsupported signature, got %#v", got) } @@ -217,13 +218,13 @@ func Bad(ctx context.Context, input UploadInput) (SearchResult, error) { }}}) bindings := compilerBindingsByBlock(app.BackendBindings) - assertBinding(t, bindings["Session"], source.BackendBindingBound, source.BackendSignatureAPI, "", false) - assertBinding(t, bindings["Summary"], source.BackendBindingBound, source.BackendSignatureAPI0, "", false) + assertBinding(t, bindings["Session"], source.BackendSignatureAPI, "", false) + assertBinding(t, bindings["Summary"], source.BackendSignatureAPI0, "", false) assertResultFields(t, bindings["Summary"].ResultFields, "count:Count:int,next:Next:string") - assertBinding(t, bindings["Search"], source.BackendBindingBound, source.BackendSignatureAPIInput, "SearchInput", false) + assertBinding(t, bindings["Search"], source.BackendSignatureAPIInput, "SearchInput", false) assertInputFields(t, bindings["Search"].InputFields, "Query:q:string,Page:page:int") assertResultFields(t, bindings["Search"].ResultFields, "count:Count:int,next:Next:string") - assertBinding(t, bindings["SearchPtr"], source.BackendBindingBound, source.BackendSignatureAPIInputPtr, "SearchInput", true) + assertBinding(t, bindings["SearchPtr"], source.BackendSignatureAPIInputPtr, "SearchInput", true) if got := bindings["Bad"]; got.Status != source.BackendBindingUnsupportedSignature || !strings.Contains(got.Message, "typed API input UploadInput uses unsupported field type") { t.Fatalf("expected unsupported typed API input, got %#v", got) } @@ -278,9 +279,9 @@ func hidden(ctx.Context) (ActionResult, error) { }}}) bindings := compilerBindingsByBlock(app.BackendBindings) - assertBinding(t, bindings["Login"], source.BackendBindingBound, source.BackendSignatureActionForm, "AliasInput", false) + assertBinding(t, bindings["Login"], source.BackendSignatureActionForm, "AliasInput", false) assertInputFields(t, bindings["Login"].InputFields, "Email:email:string") - assertBinding(t, bindings["Session"], source.BackendBindingBound, source.BackendSignatureAPI, "", false) + assertBinding(t, bindings["Session"], source.BackendSignatureAPI, "", false) if got := bindings["hidden"]; got.Status != source.BackendBindingMissing { t.Fatalf("expected unexported handler to remain missing, got %#v", got) } @@ -444,9 +445,9 @@ func LoadBroken() map[string]any { }}) bindings := compilerBindingsByBlock(app.BackendBindings) - assertBinding(t, bindings["LoadDashboard"], source.BackendBindingBound, source.BackendSignatureLoadError, "", false) - assertBinding(t, bindings["LoadProfile"], source.BackendBindingBound, source.BackendSignatureLoad, "", false) - assertBinding(t, bindings["LoadTyped"], source.BackendBindingBound, source.BackendSignatureLoadStructError, "", false) + assertBinding(t, bindings["LoadDashboard"], source.BackendSignatureLoadError, "", false) + assertBinding(t, bindings["LoadProfile"], source.BackendSignatureLoad, "", false) + assertBinding(t, bindings["LoadTyped"], source.BackendSignatureLoadStructError, "", false) if bindings["LoadTyped"].ResultType != "TypedData" { t.Fatalf("expected typed load result type, got %#v", bindings["LoadTyped"]) } @@ -559,9 +560,9 @@ func List(context.Context) (response.Response, error) { t.Fatalf("expected %s to bind generated inline go package, got %#v", name, bindings[name]) } } - assertBinding(t, bindings["Subscribe"], source.BackendBindingBound, source.BackendSignatureAction0, "", false) - assertBinding(t, bindings["Session"], source.BackendBindingBound, source.BackendSignatureAPI, "", false) - assertBinding(t, bindings["List"], source.BackendBindingBound, source.BackendSignatureFragment, "", false) + assertBinding(t, bindings["Subscribe"], source.BackendSignatureAction0, "", false) + assertBinding(t, bindings["Session"], source.BackendSignatureAPI, "", false) + assertBinding(t, bindings["List"], source.BackendSignatureFragment, "", false) } func TestDiscoverGoEndpointCommentsBindsStandaloneEndpoints(t *testing.T) { @@ -626,9 +627,9 @@ func Refresh(context.Context, *http.Request) (response.Response, error) { t.Fatal(err) } bindings := compilerBindingsByBlock(BindBackendHandlers(&ir)) - assertBinding(t, bindings["Login"], source.BackendBindingBound, source.BackendSignatureAction0, "", false) - assertBinding(t, bindings["Session"], source.BackendBindingBound, source.BackendSignatureAPI, "", false) - assertBinding(t, bindings["Refresh"], source.BackendBindingBound, source.BackendSignatureAPI, "", false) + assertBinding(t, bindings["Login"], source.BackendSignatureAction0, "", false) + assertBinding(t, bindings["Session"], source.BackendSignatureAPI, "", false) + assertBinding(t, bindings["Refresh"], source.BackendSignatureAPI, "", false) } func TestDiscoverGoEndpointCommentsRejectsMalformedComments(t *testing.T) { @@ -689,7 +690,11 @@ func Session(context.Context, *http.Request) (response.Response, error) { if err == nil { t.Fatal("expected malformed endpoint comment diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if len(diagnostics) != 1 || diagnostics[0].Code != "malformed_go_endpoint_comment" { t.Fatalf("unexpected diagnostics: %#v", diagnostics) } @@ -803,7 +808,11 @@ func TestValidateManifestRejectsGoEndpointConflictWithGOWDKEndpoint(t *testing.T if err == nil { t.Fatal("expected route conflict diagnostic") } - if !hasDiagnosticCode(err.(ValidationErrors), "route_method_conflict") { + if !hasDiagnosticCode(func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }(), "route_method_conflict") { t.Fatalf("missing route_method_conflict diagnostic: %#v", err) } } @@ -846,7 +855,11 @@ func TestValidateManifestReportsDuplicateStandaloneEndpointsAsAuthorDiagnostic(t if strings.Contains(err.Error(), "internal compiler error") { t.Fatalf("duplicate standalone endpoints should not produce internal compiler error: %v", err) } - if !hasDiagnosticCode(err.(ValidationErrors), "route_method_conflict") { + if !hasDiagnosticCode(func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }(), "route_method_conflict") { t.Fatalf("missing route_method_conflict diagnostic: %#v", err) } } @@ -865,7 +878,11 @@ func TestValidateBackendBindingPolicyFailsProductionMissingHandler(t *testing.T) if err == nil { t.Fatal("expected production missing handler diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "backend_binding_required") { t.Fatalf("missing backend_binding_required diagnostic: %#v", diagnostics) } @@ -910,9 +927,9 @@ func TestValidateBackendBindingPolicyAllowsExplicitProductionStubMode(t *testing } } -func assertBinding(t *testing.T, binding source.BackendBinding, status source.BackendBindingStatus, signature source.BackendSignatureKind, inputType string, inputPointer bool) { +func assertBinding(t *testing.T, binding source.BackendBinding, signature source.BackendSignatureKind, inputType string, inputPointer bool) { t.Helper() - if binding.Status != status || binding.Signature != signature || binding.InputType != inputType || binding.InputPointer != inputPointer { + if binding.Status != source.BackendBindingBound || binding.Signature != signature || binding.InputType != inputType || binding.InputPointer != inputPointer { t.Fatalf("unexpected binding: %#v", binding) } } @@ -1034,7 +1051,11 @@ func TestValidateBackendBindingPolicyIRSeesMissingLoadBinding(t *testing.T) { if err == nil { t.Fatal("expected missing load binding to fail the production policy even when endpoint bindings exist") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "backend_binding_required") { t.Fatalf("missing backend_binding_required diagnostic: %#v", diagnostics) } @@ -1064,7 +1085,11 @@ func TestValidateBackendBindingPolicyIRFailsProductionUnsupportedFragmentBinding if err == nil { t.Fatal("expected unsupported fragment binding to fail the production policy") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "backend_binding_required") { t.Fatalf("missing backend_binding_required diagnostic: %#v", diagnostics) } diff --git a/internal/compiler/backend_inline_signatures.go b/internal/compiler/backend_inline_signatures.go index 19e8a536..7a59aaa9 100644 --- a/internal/compiler/backend_inline_signatures.go +++ b/internal/compiler/backend_inline_signatures.go @@ -52,25 +52,27 @@ func inspectInlineScriptFeaturePackage(page gwdkir.Page, target string) featureP var supportMessage string if signature == source.BackendSignatureActionForm || signature == source.BackendSignatureActionFormPtr { inputStruct, ok := inputStructs[inputType] - if !ok { + switch { + case !ok: supportMessage = fmt.Sprintf("typed action input %s must be an exported struct in the same package", inputType) signature = "" - } else if inputStruct.Message != "" { + case inputStruct.Message != "": supportMessage = inputStruct.Message signature = "" - } else { + default: inputFields = append([]source.BackendInputField(nil), inputStruct.Fields...) } } if signature == source.BackendSignatureLoadStruct || signature == source.BackendSignatureLoadStructError { resultStruct, ok := resultStructs[resultType] - if !ok { + switch { + case !ok: supportMessage = fmt.Sprintf("typed load result %s must be an exported struct in the same package", resultType) signature = "" - } else if resultStruct.Message != "" { + case resultStruct.Message != "": supportMessage = resultStruct.Message signature = "" - } else { + default: resultFields = append([]source.BackendResultField(nil), resultStruct.Fields...) } } diff --git a/internal/compiler/backend_input_fields.go b/internal/compiler/backend_input_fields.go index eaf56973..b623ad01 100644 --- a/internal/compiler/backend_input_fields.go +++ b/internal/compiler/backend_input_fields.go @@ -43,10 +43,6 @@ func backendInputStruct(typeName string, structType *ast.StructType, imports map return backendTaggedInputStruct("typed action input", typeName, structType, imports, "form", true) } -func backendAPIInputStruct(typeName string, structType *ast.StructType, imports map[string]string) inputStruct { - return backendTaggedInputStruct("typed API input", typeName, structType, imports, "json", false) -} - func backendTaggedInputStruct(label string, typeName string, structType *ast.StructType, imports map[string]string, tagKey string, allowFiles bool) inputStruct { if structType == nil || structType.Fields == nil { return inputStruct{} @@ -143,10 +139,6 @@ func backendTypedTaggedInputStruct(label string, typeName string, typ types.Type return inputStruct{Fields: fields} } -func formTagName(field *ast.Field) (string, bool, bool, error) { - return inputTagName(field, "form") -} - func inputTagName(field *ast.Field, key string) (string, bool, bool, error) { if field == nil || field.Tag == nil { return "", false, false, nil @@ -158,10 +150,6 @@ func inputTagName(field *ast.Field, key string) (string, bool, bool, error) { return inputTagNameValue(tag, key) } -func formTagNameValue(tag string) (string, bool, bool, error) { - return inputTagNameValue(tag, "form") -} - func inputTagNameValue(tag string, key string) (string, bool, bool, error) { value, ok, err := structTagValue(tag, key) if err != nil || !ok { diff --git a/internal/compiler/route_bindings_test.go b/internal/compiler/route_bindings_test.go index ecb17669..2a600138 100644 --- a/internal/compiler/route_bindings_test.go +++ b/internal/compiler/route_bindings_test.go @@ -48,8 +48,8 @@ func TestBuildRouteMetadataSeparatesRoutesFromEndpoints(t *testing.T) { t.Fatal(err) } - assertRoute(t, metadata.Routes, RouteSPA, "GET", "/newsletter", `embedded.SPA("pages/newsletter.html")`) - assertRoute(t, metadata.Routes, RouteSSR, "GET", "/dashboard", "ssr.RenderDashboard") + assertRoute(t, metadata.Routes, RouteSPA, "/newsletter", `embedded.SPA("pages/newsletter.html")`) + assertRoute(t, metadata.Routes, RouteSSR, "/dashboard", "ssr.RenderDashboard") assertEndpoint(t, metadata.Endpoints, EndpointAction, "POST", "/newsletter", "actions.NewsletterSubscribe") assertEndpoint(t, metadata.Endpoints, EndpointAPI, "GET", "/api/patients", "api.PatientsIndexList") fragment := findEndpoint(t, metadata.Endpoints, EndpointFragment, "GET", "/patients/{id:int}/table", "fragments.PatientsIndexTable") @@ -111,7 +111,7 @@ func TestBuildRouteMetadataMapsHybridWithoutExplicitLoadToHybridRoute(t *testing if err != nil { t.Fatal(err) } - assertRoute(t, metadata.Routes, RouteHybrid, "GET", "/dashboard", "hybrid.RenderDashboard") + assertRoute(t, metadata.Routes, RouteHybrid, "/dashboard", "hybrid.RenderDashboard") } func TestBuildRouteMetadataMapsHybridWithLoadToHybridRoute(t *testing.T) { @@ -129,7 +129,7 @@ func TestBuildRouteMetadataMapsHybridWithLoadToHybridRoute(t *testing.T) { if err != nil { t.Fatal(err) } - assertRoute(t, metadata.Routes, RouteHybrid, "GET", "/dashboard", "hybrid.RenderDashboard") + assertRoute(t, metadata.Routes, RouteHybrid, "/dashboard", "hybrid.RenderDashboard") } func TestBuildRouteMetadataFromIR(t *testing.T) { @@ -184,8 +184,8 @@ func TestBuildRouteMetadataFromIR(t *testing.T) { }}, }) - assertRoute(t, metadata.Routes, RouteSPA, "GET", "/newsletter", `embedded.SPA("pages/newsletter.html")`) - assertRoute(t, metadata.Routes, RouteSSR, "GET", "/dashboard", "ssr.RenderDashboard") + assertRoute(t, metadata.Routes, RouteSPA, "/newsletter", `embedded.SPA("pages/newsletter.html")`) + assertRoute(t, metadata.Routes, RouteSSR, "/dashboard", "ssr.RenderDashboard") assertEndpoint(t, metadata.Endpoints, EndpointAction, "POST", "/newsletter", "actions.NewsletterSubscribe") assertEndpoint(t, metadata.Endpoints, EndpointFragment, "GET", "/newsletter/list", "fragments.NewsletterList") assertEndpoint(t, metadata.Endpoints, EndpointCommand, "POST", "/patients", "contracts.command.patients.CreatePatient") @@ -241,13 +241,13 @@ func TestBuildRouteMetadataIncludesDerivedCommandEndpointRoute(t *testing.T) { } } -func assertRoute(t *testing.T, routes []RouteBinding, kind RouteKind, method, route, handler string) { +func assertRoute(t *testing.T, routes []RouteBinding, kind RouteKind, route, handler string) { t.Helper() - binding := findRoute(t, routes, kind, method, route) + binding := findRoute(t, routes, kind, "GET", route) if binding.Handler == handler { return } - t.Fatalf("Missing route kind=%s method=%s route=%s handler=%s in %#v", kind, method, route, handler, routes) + t.Fatalf("Missing route kind=%s method=%s route=%s handler=%s in %#v", kind, "GET", route, handler, routes) } func findRoute(t *testing.T, routes []RouteBinding, kind RouteKind, method, route string) RouteBinding { diff --git a/internal/compiler/routes.go b/internal/compiler/routes.go index eb59dcfe..e0ba5f5b 100644 --- a/internal/compiler/routes.go +++ b/internal/compiler/routes.go @@ -518,8 +518,10 @@ func routeDiagnostics(page gwdkir.Page, label string, issues []routeIssue, route const restPatternPlaceholder = source.RestRoutePatternPlaceholder -type routeInfo = source.RoutePattern -type routeIssue = source.RouteIssue +type ( + routeInfo = source.RoutePattern + routeIssue = source.RouteIssue +) func routeIssueSpan(issue routeIssue, routeSpan source.SourceSpan, paramSpans []source.NamedSpan) source.SourceSpan { if issue.Param != "" { diff --git a/internal/compiler/routes_related_test.go b/internal/compiler/routes_related_test.go index 9c6e8424..f96d9660 100644 --- a/internal/compiler/routes_related_test.go +++ b/internal/compiler/routes_related_test.go @@ -8,6 +8,13 @@ import ( "github.com/cssbruno/gowdk/internal/source" ) +func testSpan(line, endColumn int) source.SourceSpan { + return source.SourceSpan{ + Start: source.SourcePosition{Line: line, Column: 1}, + End: source.SourcePosition{Line: line, Column: endColumn}, + } +} + func span(line, startColumn, endColumn int) source.SourceSpan { return source.SourceSpan{ Start: source.SourcePosition{Line: line, Column: startColumn}, @@ -26,8 +33,8 @@ func findByCode(diagnostics []ValidationError, code string) (ValidationError, bo func TestDuplicateRouteCarriesRelatedFirstDeclaration(t *testing.T) { pages := []gwdkir.Page{ - {ID: "home", Source: "home.page.gwdk", Route: "/", Spans: gwdkir.PageSpans{Route: span(2, 1, 9)}}, - {ID: "index", Source: "index.page.gwdk", Route: "/", Spans: gwdkir.PageSpans{Route: span(3, 1, 9)}}, + {ID: "home", Source: "home.page.gwdk", Route: "/", Spans: gwdkir.PageSpans{Route: testSpan(2, 9)}}, + {ID: "index", Source: "index.page.gwdk", Route: "/", Spans: gwdkir.PageSpans{Route: testSpan(3, 9)}}, } diagnostic, ok := findByCode(validateUniquePageRoutes(gowdk.Config{}, pages), "duplicate_route") @@ -44,7 +51,7 @@ func TestDuplicateRouteCarriesRelatedFirstDeclaration(t *testing.T) { if related.Source != "home.page.gwdk" { t.Fatalf("related location should point at the first declaration; got %q", related.Source) } - if related.Span != span(2, 1, 9) { + if related.Span != testSpan(2, 9) { t.Fatalf("related span should be the first route span; got %+v", related.Span) } if related.Message == "" { @@ -54,8 +61,8 @@ func TestDuplicateRouteCarriesRelatedFirstDeclaration(t *testing.T) { func TestDuplicatePageIDCarriesRelatedFirstDeclaration(t *testing.T) { pages := []gwdkir.Page{ - {ID: "home", Source: "a.page.gwdk", Spans: gwdkir.PageSpans{Page: span(1, 1, 9)}}, - {ID: "home", Source: "b.page.gwdk", Spans: gwdkir.PageSpans{Page: span(1, 1, 9)}}, + {ID: "home", Source: "a.page.gwdk", Spans: gwdkir.PageSpans{Page: testSpan(1, 9)}}, + {ID: "home", Source: "b.page.gwdk", Spans: gwdkir.PageSpans{Page: testSpan(1, 9)}}, } diagnostic, ok := findByCode(validateUniquePages(pages), "duplicate_page_id") if !ok { @@ -71,15 +78,15 @@ func TestDuplicatePageStoreCarriesRelatedFirstDeclaration(t *testing.T) { ID: "home", Source: "home.page.gwdk", Stores: []gwdkir.Store{ - {Name: "Counter", Span: span(3, 1, 8)}, - {Name: "Counter", Span: span(6, 1, 8)}, + {Name: "Counter", Span: testSpan(3, 8)}, + {Name: "Counter", Span: testSpan(6, 8)}, }, } diagnostic, ok := findByCode(validatePageStores(page), "duplicate_page_store") if !ok { t.Fatal("expected a duplicate_page_store diagnostic") } - if len(diagnostic.Related) != 1 || diagnostic.Related[0].Span != span(3, 1, 8) { + if len(diagnostic.Related) != 1 || diagnostic.Related[0].Span != testSpan(3, 8) { t.Fatalf("expected related location at first store; got %+v", diagnostic.Related) } } @@ -89,8 +96,8 @@ func TestContractRouteConflictCarriesRelatedFirstDeclaration(t *testing.T) { // through the shared route-registration path; the conflict must point back // at the first contract's declaration. refs := []gwdkir.ContractReference{ - {Kind: gwdkir.ContractQuery, Name: "Reports", Method: "GET", Path: "/reports", Source: "reports.gwdk", Span: span(4, 1, 12)}, - {Kind: gwdkir.ContractQuery, Name: "Summary", Method: "GET", Path: "/reports", Source: "summary.gwdk", Span: span(7, 1, 12)}, + {Kind: gwdkir.ContractQuery, Name: "Reports", Method: "GET", Path: "/reports", Source: "reports.gwdk", Span: testSpan(4, 12)}, + {Kind: gwdkir.ContractQuery, Name: "Summary", Method: "GET", Path: "/reports", Source: "summary.gwdk", Span: testSpan(7, 12)}, } diagnostic, ok := findByCode(validateRouteMethodConflicts(gowdk.Config{}, nil, nil, gwdkir.SourceMap{}, refs), "route_method_conflict") @@ -104,7 +111,7 @@ func TestContractRouteConflictCarriesRelatedFirstDeclaration(t *testing.T) { if related.Source != "reports.gwdk" { t.Fatalf("related location should point at the first contract; got %q", related.Source) } - if related.Span != span(4, 1, 12) { + if related.Span != testSpan(4, 12) { t.Fatalf("related span should be the first contract span; got %+v", related.Span) } } diff --git a/internal/compiler/validate_component_view.go b/internal/compiler/validate_component_view.go index dc23b3e8..b12c657d 100644 --- a/internal/compiler/validate_component_view.go +++ b/internal/compiler/validate_component_view.go @@ -346,11 +346,6 @@ func literalAttrValue(attrs []viewmodel.Attr, name string) string { return "" } -func collectSimpleInterpolations(value string, fields map[string]bool) { - refs := componentViewRefs{Fields: fields} - collectSimpleInterpolationRefs(value, 0, &refs) -} - func collectSimpleInterpolationRefs(value string, base int, refs *componentViewRefs) { for index := 0; index < len(value); index++ { if value[index] != '{' { diff --git a/internal/compiler/validate_component_view_contract.go b/internal/compiler/validate_component_view_contract.go index 1424c665..08c326ca 100644 --- a/internal/compiler/validate_component_view_contract.go +++ b/internal/compiler/validate_component_view_contract.go @@ -253,10 +253,6 @@ func sortedUsedRefNames(refs map[string]source.SourceSpan) []string { return names } -func componentFieldError(component gwdkir.Component, message string) ValidationError { - return componentFieldErrorAt(component, firstSpan(component.Blocks.Spans.View, component.Span), message) -} - func componentFieldErrorAt(component gwdkir.Component, span source.SourceSpan, message string) ValidationError { return ValidationError{ Code: "component_field_error", diff --git a/internal/compiler/validate_contract_refs_test.go b/internal/compiler/validate_contract_refs_test.go index 6993578a..dcc3cadf 100644 --- a/internal/compiler/validate_contract_refs_test.go +++ b/internal/compiler/validate_contract_refs_test.go @@ -1,6 +1,7 @@ package compiler import ( + "errors" "strings" "testing" @@ -25,7 +26,11 @@ func TestValidateContractReferencesRejectsBoundNonWebRole(t *testing.T) { if err == nil { t.Fatal("expected non-web role diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if len(diagnostics) != 1 || diagnostics[0].Code != "contract_reference_role_not_allowed" { t.Fatalf("unexpected diagnostics: %#v", diagnostics) } @@ -106,7 +111,11 @@ func TestValidateRealtimeSubscriptionBindings(t *testing.T) { if err == nil { t.Fatal("expected subscription diagnostics") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if len(diagnostics) != 3 { t.Fatalf("expected three diagnostics, got %#v", diagnostics) } diff --git a/internal/compiler/validate_packages.go b/internal/compiler/validate_packages.go index 152ce34e..462d9473 100644 --- a/internal/compiler/validate_packages.go +++ b/internal/compiler/validate_packages.go @@ -3,6 +3,7 @@ package compiler import ( "bytes" "encoding/json" + "errors" "fmt" "go/ast" "go/importer" @@ -257,8 +258,8 @@ func inspectGoPackageForValidation(dir string, group []packageDeclaration) goPac } func parseGoBlockPackageFileForValidation(fileSet *token.FileSet, declaration packageDeclaration, block gwdkir.GoBlock) (*ast.File, *ValidationError) { - src, err := goBlockPackageSourceForValidation(declaration, block) - if err != nil { + src, ok := goBlockPackageSourceForValidation(declaration, block) + if !ok { return nil, nil } file, parseErr := parser.ParseFile(fileSet, declaration.Source, src, parser.AllErrors|parser.ParseComments) @@ -276,19 +277,19 @@ func parseGoBlockPackageFileForValidation(fileSet *token.FileSet, declaration pa return file, nil } -func goBlockPackageSourceForValidation(declaration packageDeclaration, block gwdkir.GoBlock) (string, error) { +func goBlockPackageSourceForValidation(declaration packageDeclaration, block gwdkir.GoBlock) (string, bool) { packageName := strings.TrimSpace(declaration.Package) if packageName == "" { - return "", fmt.Errorf("go block package is missing") + return "", false } body := strings.TrimSpace(block.Body) if body == "" { - return "package " + packageName + "\n", nil + return "package " + packageName + "\n", true } bodyFileSet := token.NewFileSet() bodyFile, err := parser.ParseFile(bodyFileSet, declaration.Source, "package "+packageName+"\n\n"+block.Body, parser.AllErrors) if err != nil { - return "", err + return "", false } file := &ast.File{ @@ -310,9 +311,9 @@ func goBlockPackageSourceForValidation(declaration packageDeclaration, block gwd var buffer bytes.Buffer if err := printer.Fprint(&buffer, bodyFileSet, file); err != nil { - return "", fmt.Errorf("print go block package source: %w", err) + return "", false } - return buffer.String(), nil + return buffer.String(), true } func goBlockGOWDKImportSpecsForValidation(imports []gwdkir.Import, bodyFile *ast.File) []ast.Spec { @@ -542,16 +543,20 @@ func goTypeCheckDiagnostic(fileSet *token.FileSet, err error) ValidationError { Message: fmt.Sprintf("Go package failed type-check: %v", err), } var typeError types.Error - switch typed := err.(type) { - case types.Error: - typeError = typed - case *types.Error: - if typed == nil { + { + var typed types.Error + var typed1 *types.Error + switch { + case errors.As(err, &typed): + typeError = typed + case errors.As(err, &typed1): + if typed1 == nil { + return diagnostic + } + typeError = *typed1 + default: return diagnostic } - typeError = *typed - default: - return diagnostic } position := fileSet.PositionFor(typeError.Pos, true) if position.IsValid() { diff --git a/internal/compiler/validate_page.go b/internal/compiler/validate_page.go index 80e6eb70..0f8f811e 100644 --- a/internal/compiler/validate_page.go +++ b/internal/compiler/validate_page.go @@ -130,7 +130,7 @@ func ValidatePage(config gowdk.Config, page gwdkir.Page) []ValidationError { diagnostics = append(diagnostics, routeDiagnostics(page, fmt.Sprintf("fragment %s endpoint path", fragment.Name), issues, fragment.RouteSpan, fragment.RouteParams)...) } - if requiresSSRFeature(mode, page) && !config.HasFeature(gowdk.FeatureSSR) { + if requiresSSRFeature(mode) && !config.HasFeature(gowdk.FeatureSSR) { diagnostics = append(diagnostics, ValidationError{ Code: "missing_ssr_addon", PageID: page.ID, @@ -159,7 +159,7 @@ func ValidatePage(config gowdk.Config, page gwdkir.Page) []ValidationError { if len(pageRouteIssues) == 0 { params = pageRoute.Params } - if isBuildTimeRoute(mode, page) && len(pageRouteIssues) == 0 && pageRoute.RestParam != "" { + if isBuildTimeRoute(mode) && len(pageRouteIssues) == 0 && pageRoute.RestParam != "" { diagnostics = append(diagnostics, ValidationError{ Code: "malformed_route", PageID: page.ID, @@ -172,7 +172,7 @@ func ValidatePage(config gowdk.Config, page gwdkir.Page) []ValidationError { mode, ), }) - } else if isBuildTimeRoute(mode, page) && len(params) > 0 && !page.Blocks.Paths { + } else if isBuildTimeRoute(mode) && len(params) > 0 && !page.Blocks.Paths { diagnostics = append(diagnostics, ValidationError{ Code: "spa_dynamic_route_missing_paths", PageID: page.ID, @@ -304,7 +304,7 @@ func validatePageGuards(page gwdkir.Page) []ValidationError { } func validateProtectedPageGuardRender(page gwdkir.Page, mode gowdk.RenderMode) []ValidationError { - if !isBuildTimeRoute(mode, page) || !hasProtectedPageGuard(page) { + if !isBuildTimeRoute(mode) || !hasProtectedPageGuard(page) { return nil } return []ValidationError{{ @@ -338,11 +338,11 @@ func firstGoBlockSpan(page gwdkir.Page, target string) source.SourceSpan { return source.SourceSpan{} } -func requiresSSRFeature(mode gowdk.RenderMode, page gwdkir.Page) bool { +func requiresSSRFeature(mode gowdk.RenderMode) bool { return mode == gowdk.SSR || mode == gowdk.Hybrid } -func isBuildTimeRoute(mode gowdk.RenderMode, page gwdkir.Page) bool { +func isBuildTimeRoute(mode gowdk.RenderMode) bool { switch mode { case gowdk.SPA: return true diff --git a/internal/compiler/validate_page_contract_client.go b/internal/compiler/validate_page_contract_client.go index 02ab9b96..bdf43fd0 100644 --- a/internal/compiler/validate_page_contract_client.go +++ b/internal/compiler/validate_page_contract_client.go @@ -25,7 +25,7 @@ import ( // invalidation graph, which ValidatePage does not receive; it is tracked as a // follow-up. func validatePageContractClient(page gwdkir.Page, mode gowdk.RenderMode) []ValidationError { - if isBuildTimeRoute(mode, page) || !page.Blocks.View { + if isBuildTimeRoute(mode) || !page.Blocks.View { return nil } refs, err := pageCommandReferences(page) diff --git a/internal/compiler/validate_page_persist_test.go b/internal/compiler/validate_page_persist_test.go index f283c14f..12a3caa7 100644 --- a/internal/compiler/validate_page_persist_test.go +++ b/internal/compiler/validate_page_persist_test.go @@ -8,7 +8,7 @@ import ( "github.com/cssbruno/gowdk/internal/gwdkir" ) -func persistedStorePageNamed(id, route, storeName, typeName, initName, scope string) gwdkir.Page { +func persistedStorePageNamed(id, route, typeName, initName, scope string) gwdkir.Page { return gwdkir.Page{ ID: id, Route: route, @@ -19,7 +19,7 @@ func persistedStorePageNamed(id, route, storeName, typeName, initName, scope str Path: "github.com/cssbruno/gowdk/testfixture/islands", }}, Stores: []gwdkir.Store{{ - Name: storeName, + Name: "cart", Type: gwdkir.GoRef{Alias: "ui", Name: typeName}, Init: gwdkir.GoRef{Alias: "ui", Name: initName}, Persist: scope, @@ -132,8 +132,8 @@ func TestValidateWarnsOnPersistedStoreScopeConflict(t *testing.T) { // Same store name and shape but different persist scopes share one storage // key; the effective scope then depends on navigation order. app := appFixture{Pages: []gwdkir.Page{ - persistedStorePageNamed("shop", "/shop", "cart", "CounterState", "NewCounterState", "local"), - persistedStorePageNamed("checkout", "/checkout", "cart", "CounterState", "NewCounterState", "session"), + persistedStorePageNamed("shop", "/shop", "CounterState", "NewCounterState", "local"), + persistedStorePageNamed("checkout", "/checkout", "CounterState", "NewCounterState", "session"), }} diagnostics := ValidateProgramReport(gowdk.Config{}, app.program(gowdk.Config{})) d := firstDiagnostic(diagnostics, "page_store_persist_scope_conflict") @@ -148,7 +148,7 @@ func TestValidateWarnsOnPersistedStoreScopeConflict(t *testing.T) { if other := firstDiagnostic(diagnostics, "page_store_persist_key_conflict"); other != nil { t.Fatalf("same-shape scope conflict must not also raise a key conflict: %#v", other) } - if ValidationErrors(diagnostics).HasErrors() { + if diagnostics.HasErrors() { t.Fatalf("scope conflict alone must not fail the build: %#v", diagnostics) } } @@ -165,8 +165,8 @@ func TestValidatePageDoesNotPersistCheckUnpersistedStore(t *testing.T) { func TestValidateWarnsOnPersistedStoreKeyConflict(t *testing.T) { // Same store name, different shapes, both persisted -> shared key, divergent hash. app := appFixture{Pages: []gwdkir.Page{ - persistedStorePageNamed("shop", "/shop", "cart", "CounterState", "NewCounterState", "local"), - persistedStorePageNamed("notes", "/notes", "cart", "TextState", "NewTextState", "local"), + persistedStorePageNamed("shop", "/shop", "CounterState", "NewCounterState", "local"), + persistedStorePageNamed("notes", "/notes", "TextState", "NewTextState", "local"), }} diagnostics := ValidateProgramReport(gowdk.Config{}, app.program(gowdk.Config{})) d := firstDiagnostic(diagnostics, "page_store_persist_key_conflict") @@ -176,7 +176,7 @@ func TestValidateWarnsOnPersistedStoreKeyConflict(t *testing.T) { if d.Severity != SeverityWarning { t.Fatalf("key conflict should be a warning, got %v", d.Severity) } - if ValidationErrors(diagnostics).HasErrors() { + if diagnostics.HasErrors() { t.Fatalf("key conflict alone must not fail the build: %#v", diagnostics) } } @@ -184,8 +184,8 @@ func TestValidateWarnsOnPersistedStoreKeyConflict(t *testing.T) { func TestValidateAllowsSharedPersistedStoreAcrossPages(t *testing.T) { // Same name AND same shape across pages is intentional cross-route sharing. app := appFixture{Pages: []gwdkir.Page{ - persistedStorePageNamed("shop", "/shop", "cart", "CounterState", "NewCounterState", "local"), - persistedStorePageNamed("checkout", "/checkout", "cart", "CounterState", "NewCounterState", "local"), + persistedStorePageNamed("shop", "/shop", "CounterState", "NewCounterState", "local"), + persistedStorePageNamed("checkout", "/checkout", "CounterState", "NewCounterState", "local"), }} diagnostics := ValidateProgramReport(gowdk.Config{}, app.program(gowdk.Config{})) if d := firstDiagnostic(diagnostics, "page_store_persist_key_conflict"); d != nil { diff --git a/internal/compiler/validate_scripts.go b/internal/compiler/validate_scripts.go index 5906d6c8..41862e30 100644 --- a/internal/compiler/validate_scripts.go +++ b/internal/compiler/validate_scripts.go @@ -18,7 +18,7 @@ func validateGoBlocks(config gowdk.Config, app gwdkir.Program) []ValidationError mode := page.RenderMode(config.Render.DefaultMode()) for _, block := range page.Blocks.GoBlocks { diagnostics = append(diagnostics, validateGoBlockSyntax(page.Package, page.Source, page.ID, "", block)...) - diagnostics = append(diagnostics, validateGoBlockTarget(config, enabledAddons, page.ID, "", page.Source, page.Package, mode, page.Blocks.Server, block)...) + diagnostics = append(diagnostics, validateGoBlockTarget(enabledAddons, page.ID, "", page.Source, page.Package, mode, block)...) } } for _, component := range app.Components { @@ -57,7 +57,7 @@ func validateGoBlockSyntax(packageName string, sourcePath string, pageID string, }} } -func validateGoBlockTarget(config gowdk.Config, enabledAddons map[string]gowdk.Addon, pageID string, componentName string, sourcePath string, packageName string, mode gowdk.RenderMode, hasLoad bool, block gwdkir.GoBlock) []ValidationError { +func validateGoBlockTarget(enabledAddons map[string]gowdk.Addon, pageID string, componentName string, sourcePath string, packageName string, mode gowdk.RenderMode, block gwdkir.GoBlock) []ValidationError { target := strings.TrimSpace(block.Target) switch { case target == "" || target == "client": diff --git a/internal/compiler/validate_test.go b/internal/compiler/validate_test.go index d6304d9d..ffb46c59 100644 --- a/internal/compiler/validate_test.go +++ b/internal/compiler/validate_test.go @@ -1,6 +1,7 @@ package compiler import ( + "errors" "fmt" "go/ast" "go/parser" @@ -34,7 +35,11 @@ func TestValidateManifestRejectsMissingPackageDeclaration(t *testing.T) { if err == nil { t.Fatal("expected missing package diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "missing_package_declaration") { t.Fatalf("missing package diagnostic: %#v", diagnostics) } @@ -62,7 +67,11 @@ func TestValidateManifestRejectsPackageMismatchWithSiblingGoFile(t *testing.T) { if err == nil { t.Fatal("expected package mismatch diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticMessage(diagnostics, "package_mismatch", "views", "app") { t.Fatalf("missing package mismatch diagnostic: %#v", diagnostics) } @@ -138,7 +147,11 @@ func TestValidateManifestReportsGoPackageParseErrors(t *testing.T) { if err == nil { t.Fatal("expected Go package error diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "go_package_error") { t.Fatalf("missing Go package error diagnostic: %#v", diagnostics) } @@ -166,7 +179,11 @@ func TestValidateManifestReportsGoPackageTypeErrors(t *testing.T) { if err == nil { t.Fatal("expected Go package type-check diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() diagnostic := firstDiagnostic(diagnostics, "go_package_error") if diagnostic == nil { t.Fatalf("missing Go package error diagnostic: %#v", diagnostics) @@ -272,7 +289,11 @@ func TestValidateManifestReportsDefaultScriptTypeErrors(t *testing.T) { if err == nil { t.Fatal("expected inline go block type-check diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() diagnostic := firstDiagnostic(diagnostics, "go_package_error") if diagnostic == nil { t.Fatalf("missing Go package error diagnostic: %#v", diagnostics) @@ -290,17 +311,17 @@ func TestValidateManifestReportsDefaultScriptTypeErrors(t *testing.T) { func TestGoBlockPackageSourceForValidationPreservesLineDirective(t *testing.T) { sourcePath := filepath.Join(t.TempDir(), "home.page.gwdk") - payload, err := goBlockPackageSourceForValidation(packageDeclaration{ + payload, ok := goBlockPackageSourceForValidation(packageDeclaration{ Source: sourcePath, Package: "app", }, gwdkir.GoBlock{ Span: source.SourceSpan{Start: source.SourcePosition{Line: 8, Column: 1}}, Body: `func BrokenCopy() string { - return MissingTitle + return MissingTitle }`, }) - if err != nil { - t.Fatal(err) + if !ok { + t.Fatal("expected validation source") } if !strings.Contains(payload, "//line "+filepath.ToSlash(sourcePath)+":8") { t.Fatalf("missing line directive in validation source:\n%s", payload) @@ -450,7 +471,11 @@ func TestValidateManifestRejectsUnknownGOWDKUsePackage(t *testing.T) { if err == nil { t.Fatal("expected unknown use package diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "unknown_gowdk_use_package") { t.Fatalf("missing unknown package diagnostic: %#v", diagnostics) } @@ -471,7 +496,11 @@ func TestValidateManifestRejectsUnknownGOWDKUseAlias(t *testing.T) { if err == nil { t.Fatal("expected unknown use alias diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "unknown_gowdk_use_alias") { t.Fatalf("missing unknown alias diagnostic: %#v", diagnostics) } @@ -499,7 +528,11 @@ func TestValidateManifestUnknownGOWDKUseAliasPointsToComponentTag(t *testing.T) if err == nil { t.Fatal("expected unknown use alias diagnostic") } - diagnostic := firstDiagnostic(err.(ValidationErrors), "unknown_gowdk_use_alias") + diagnostic := firstDiagnostic(func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }(), "unknown_gowdk_use_alias") if diagnostic == nil { t.Fatalf("missing unknown alias diagnostic: %#v", err) } @@ -522,7 +555,11 @@ func TestValidateManifestRejectsUnknownQualifiedComponent(t *testing.T) { if err == nil { t.Fatal("expected unknown component diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "unknown_gowdk_component") { t.Fatalf("missing unknown component diagnostic: %#v", diagnostics) } @@ -551,7 +588,11 @@ func TestValidateManifestUnknownQualifiedComponentPointsToComponentTag(t *testin if err == nil { t.Fatal("expected unknown component diagnostic") } - diagnostic := firstDiagnostic(err.(ValidationErrors), "unknown_gowdk_component") + diagnostic := firstDiagnostic(func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }(), "unknown_gowdk_component") if diagnostic == nil { t.Fatalf("missing unknown component diagnostic: %#v", err) } @@ -574,7 +615,11 @@ func TestValidateManifestRejectsComponentRefToLayoutOnlyUsePackage(t *testing.T) if err == nil { t.Fatal("expected unknown component diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "unknown_gowdk_component") { t.Fatalf("missing unknown component diagnostic: %#v", diagnostics) } @@ -609,7 +654,11 @@ func TestValidateManifestRejectsComponentScopedComponentRefToStoreOnlyUsePackage if err == nil { t.Fatal("expected unknown component diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "unknown_gowdk_component") { t.Fatalf("missing unknown component diagnostic: %#v", diagnostics) } @@ -646,7 +695,11 @@ func TestValidateManifestRejectsUnknownComponentScopedGOWDKUseAlias(t *testing.T if err == nil { t.Fatal("expected unknown component use alias diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "unknown_gowdk_use_alias") { t.Fatalf("missing unknown alias diagnostic: %#v", diagnostics) } @@ -664,7 +717,11 @@ func TestValidateManifestRejectsUnknownComponentScopedGOWDKUsePackage(t *testing if err == nil { t.Fatal("expected unknown component use package diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "unknown_gowdk_use_package") { t.Fatalf("missing unknown package diagnostic: %#v", diagnostics) } @@ -891,7 +948,8 @@ func TestValidateManifestRejectsDuplicatePageIDsAndComponentNames(t *testing.T) if err == nil { t.Fatal("expected duplicate identity diagnostics") } - diagnostics, ok := err.(ValidationErrors) + var diagnostics ValidationErrors + ok := errors.As(err, &diagnostics) if !ok { t.Fatalf("expected ValidationErrors, got %T", err) } @@ -956,7 +1014,11 @@ func TestValidateManifestRejectsDuplicatePageStore(t *testing.T) { if err == nil { t.Fatal("expected duplicate store diagnostic") } - diagnostic := firstDiagnostic(err.(ValidationErrors), "duplicate_page_store") + diagnostic := firstDiagnostic(func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }(), "duplicate_page_store") if diagnostic == nil { t.Fatalf("Missing duplicate_page_store diagnostic: %v", err) } @@ -989,7 +1051,11 @@ func TestValidateManifestRejectsUnknownComponentStoreUse(t *testing.T) { if err == nil { t.Fatal("expected unknown store diagnostic") } - diagnostic := firstDiagnostic(err.(ValidationErrors), "unknown_component_store") + diagnostic := firstDiagnostic(func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }(), "unknown_component_store") if diagnostic == nil { t.Fatalf("Missing unknown_component_store diagnostic: %v", err) } @@ -1137,7 +1203,11 @@ func TestValidateManifestComputedConflictUsesComputedSpan(t *testing.T) { if err == nil { t.Fatal("expected computed conflict diagnostic") } - diagnostic := firstDiagnostic(err.(ValidationErrors), "component_client_error") + diagnostic := firstDiagnostic(func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }(), "component_client_error") if diagnostic == nil { t.Fatalf("missing component_client_error diagnostic: %#v", err) } @@ -1175,7 +1245,11 @@ func TestValidateManifestRejectsInvalidTypedUseStoreType(t *testing.T) { if err == nil { t.Fatal("expected diagnostic for an unresolvable store type annotation") } - if !hasDiagnosticCode(err.(ValidationErrors), "component_client_error") || !strings.Contains(err.Error(), "NoSuchType") { + if !hasDiagnosticCode(func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }(), "component_client_error") || !strings.Contains(err.Error(), "NoSuchType") { t.Fatalf("unexpected diagnostics: %v", err) } } @@ -1218,7 +1292,11 @@ func TestValidateManifestRejectsTypedUseConflictingWithLocalState(t *testing.T) if err == nil { t.Fatal("expected diagnostic for store/state field type mismatch") } - if !hasDiagnosticCode(err.(ValidationErrors), "component_client_error") || !strings.Contains(err.Error(), `field "Count"`) { + if !hasDiagnosticCode(func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }(), "component_client_error") || !strings.Contains(err.Error(), `field "Count"`) { t.Fatalf("unexpected diagnostics: %v", err) } } @@ -1302,7 +1380,11 @@ fn Checkout() { if err == nil { t.Fatal("expected diagnostic for clearing an unused store") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "component_client_error") || !strings.Contains(err.Error(), `clear references store "prefs"`) { t.Fatalf("unexpected diagnostics: %v", err) } @@ -1333,7 +1415,11 @@ func TestValidateManifestRejectsUnknownQualifiedComponentStoreUseAlias(t *testin if err == nil { t.Fatal("expected unknown store alias diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "unknown_gowdk_use_alias") { t.Fatalf("missing unknown alias diagnostic: %#v", diagnostics) } @@ -1365,7 +1451,11 @@ func TestValidateManifestRejectsUnknownQualifiedComponentStoreName(t *testing.T) if err == nil { t.Fatal("expected unknown store diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "unknown_component_store") { t.Fatalf("missing unknown store diagnostic: %#v", diagnostics) } @@ -1394,7 +1484,11 @@ func TestValidateManifestRejectsRedundantComponentImplementations(t *testing.T) if err == nil { t.Fatal("expected redundant component diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "redundant_component_implementation") { t.Fatalf("Missing redundant component diagnostic: %#v", diagnostics) } @@ -1420,7 +1514,11 @@ func TestValidateManifestRejectsRedundantComponentImplementationsWithNormalizedA if err == nil { t.Fatal("expected redundant component diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "redundant_component_implementation") { t.Fatalf("Missing redundant component diagnostic: %#v", diagnostics) } @@ -1462,7 +1560,11 @@ func TestValidateManifestRejectsRedundantTypedComponentsWithCanonicalImportsAndE if err == nil { t.Fatal("expected redundant component diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "redundant_component_implementation") { t.Fatalf("Missing redundant component diagnostic: %#v", diagnostics) } @@ -1584,7 +1686,11 @@ func TestValidateManifestRejectsBadEventModifier(t *testing.T) { if err == nil { t.Fatal("expected unsupported event modifier diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "component_field_error") { t.Fatalf("Missing component_field_error diagnostic: %#v", diagnostics) } @@ -1609,7 +1715,11 @@ func TestValidateManifestRejectsBadDebounceDuration(t *testing.T) { if err == nil { t.Fatal("expected invalid debounce duration diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "component_field_error") { t.Fatalf("Missing component_field_error diagnostic: %#v", diagnostics) } @@ -1634,7 +1744,11 @@ func TestValidateManifestRejectsDebounceThrottleCombination(t *testing.T) { if err == nil { t.Fatal("expected debounce/throttle compatibility diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "component_field_error") { t.Fatalf("Missing component_field_error diagnostic: %#v", diagnostics) } @@ -1683,7 +1797,11 @@ func TestValidateManifestRejectsMissingGoTypedComponentField(t *testing.T) { if err == nil { t.Fatal("expected Missing field diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "component_field_error") { t.Fatalf("Missing component_field_error diagnostic: %#v", diagnostics) } @@ -1708,7 +1826,11 @@ func TestValidateManifestRejectsMissingGoTypedComponentPackage(t *testing.T) { if err == nil { t.Fatal("expected Missing package diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "component_contract_error") { t.Fatalf("Missing component_contract_error diagnostic: %#v", diagnostics) } @@ -1733,7 +1855,11 @@ func TestValidateManifestRejectsMissingGoTypedComponentType(t *testing.T) { if err == nil { t.Fatal("expected Missing type diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "component_contract_error") { t.Fatalf("Missing component_contract_error diagnostic: %#v", diagnostics) } @@ -1760,7 +1886,11 @@ func TestValidateManifestRejectsReservedActiveExportName(t *testing.T) { if err == nil { t.Fatal("expected reserved export diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() diagnostic := firstDiagnostic(diagnostics, "component_contract_error") if diagnostic == nil { t.Fatalf("missing component_contract_error diagnostic: %#v", diagnostics) @@ -1849,7 +1979,11 @@ func TestValidateManifestRejectsDuplicateComponentEmitNames(t *testing.T) { if err == nil { t.Fatal("expected duplicate component emit diagnostic") } - diagnostic := firstDiagnostic(err.(ValidationErrors), "duplicate_component_emit") + diagnostic := firstDiagnostic(func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }(), "duplicate_component_emit") if diagnostic == nil { t.Fatalf("Missing duplicate_component_emit diagnostic: %v", err) } @@ -1882,7 +2016,11 @@ func TestValidateManifestRejectsUnknownComponentEmit(t *testing.T) { if err == nil { t.Fatal("expected unknown component emit diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "component_client_error") || !strings.Contains(err.Error(), `unknown component event "select"`) { t.Fatalf("unexpected diagnostics: %v", err) } @@ -1910,7 +2048,11 @@ func TestValidateManifestClientParseErrorPointsToClientLine(t *testing.T) { if err == nil { t.Fatal("expected client parse diagnostic") } - diagnostic := firstDiagnostic(err.(ValidationErrors), "component_client_error") + diagnostic := firstDiagnostic(func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }(), "component_client_error") if diagnostic == nil { t.Fatalf("Missing component_client_error diagnostic: %v", err) } @@ -1947,7 +2089,11 @@ func TestValidateManifestRejectsComponentEmitPayloadTypeMismatch(t *testing.T) { if err == nil { t.Fatal("expected component emit payload type diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "component_client_error") || !strings.Contains(err.Error(), "component event select argument 1 expects string, got int") { t.Fatalf("unexpected diagnostics: %v", err) } @@ -2062,7 +2208,11 @@ fn Add() { if err == nil { t.Fatal("expected helper local mismatch diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "component_client_error") || !strings.Contains(err.Error(), "local next expects int, got bool") { t.Fatalf("Missing helper local mismatch diagnostic: %#v", diagnostics) } @@ -2143,7 +2293,11 @@ func TestValidateManifestRejectsAwaitOutsideAsyncClientFunction(t *testing.T) { if err == nil { t.Fatal("expected await outside async diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "component_client_error") || !strings.Contains(err.Error(), "await is only supported inside async client functions") { t.Fatalf("Missing async await diagnostic: %#v", diagnostics) } @@ -2172,7 +2326,11 @@ func TestValidateManifestRejectsAsyncFetchJSONNonStringURL(t *testing.T) { if err == nil { t.Fatal("expected fetchJSON URL diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "component_client_error") || !strings.Contains(err.Error(), "fetchJSON url must be string") { t.Fatalf("Missing fetchJSON URL diagnostic: %#v", diagnostics) } @@ -2201,7 +2359,11 @@ func TestValidateManifestRejectsBadClientBuiltinArg(t *testing.T) { if err == nil { t.Fatal("expected Bad built-in argument diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "component_client_error") { t.Fatalf("Missing component_client_error diagnostic: %#v", diagnostics) } @@ -2230,7 +2392,11 @@ func TestValidateManifestRejectsHelperAsEventHandler(t *testing.T) { if err == nil { t.Fatal("expected helper event handler diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "component_field_error") { t.Fatalf("Missing component_field_error diagnostic: %#v", diagnostics) } @@ -2263,7 +2429,11 @@ fn Add() { if err == nil { t.Fatal("expected helper return mismatch diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "component_client_error") { t.Fatalf("Missing component_client_error diagnostic: %#v", diagnostics) } @@ -2300,7 +2470,11 @@ fn Add() { if err == nil { t.Fatal("expected helper call cycle diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "component_client_error") { t.Fatalf("Missing component_client_error diagnostic: %#v", diagnostics) } @@ -2338,7 +2512,11 @@ fn Add() { if err == nil { t.Fatal("expected helper local call cycle diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "component_client_error") || !strings.Contains(err.Error(), "helper call graph is invalid") { t.Fatalf("Missing component_client_error diagnostic: %#v", diagnostics) } @@ -2367,7 +2545,11 @@ func TestValidateManifestRejectsClientExpressionTypeMismatch(t *testing.T) { if err == nil { t.Fatal("expected expression type mismatch diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "component_client_error") { t.Fatalf("Missing component_client_error diagnostic: %#v", diagnostics) } @@ -2396,7 +2578,11 @@ func TestValidateManifestRejectsClientFunctionArgumentMismatch(t *testing.T) { if err == nil { t.Fatal("expected argument mismatch diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "component_field_error") { t.Fatalf("Missing component_field_error diagnostic: %#v", diagnostics) } @@ -2425,7 +2611,11 @@ func TestValidateManifestRejectsUnknownClientFunctionEventCall(t *testing.T) { if err == nil { t.Fatal("expected unknown client function diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "component_field_error") { t.Fatalf("Missing component_field_error diagnostic: %#v", diagnostics) } @@ -2454,7 +2644,11 @@ func TestValidateManifestRejectsClientFunctionUnknownStateField(t *testing.T) { if err == nil { t.Fatal("expected client function field diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "component_client_error") { t.Fatalf("Missing component_client_error diagnostic: %#v", diagnostics) } @@ -2487,7 +2681,11 @@ func TestValidateManifestRejectsClientFunctionMutatingProp(t *testing.T) { if err == nil { t.Fatal("expected prop mutation diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "component_client_error") { t.Fatalf("Missing component_client_error diagnostic: %#v", diagnostics) } @@ -2551,7 +2749,11 @@ func TestValidateManifestRejectsEffectUnknownDependency(t *testing.T) { if err == nil { t.Fatal("expected unknown effect dependency diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "component_client_error") { t.Fatalf("Missing component_client_error diagnostic: %#v", diagnostics) } @@ -2604,7 +2806,11 @@ func TestValidateManifestRejectsUnknownDOMRefBinding(t *testing.T) { if err == nil { t.Fatal("expected unknown DOM ref binding diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "component_field_error") { t.Fatalf("Missing component_field_error diagnostic: %#v", diagnostics) } @@ -2631,7 +2837,11 @@ func TestValidateManifestRejectsDuplicateDOMRefBinding(t *testing.T) { if err == nil { t.Fatal("expected duplicate DOM ref binding diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "component_field_error") { t.Fatalf("Missing component_field_error diagnostic: %#v", diagnostics) } @@ -2665,7 +2875,11 @@ fn FocusSearch() { if err == nil { t.Fatal("expected unbound DOM ref diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "component_client_error") { t.Fatalf("Missing component_client_error diagnostic: %#v", diagnostics) } @@ -2732,7 +2946,11 @@ func TestValidateManifestRejectsGElseIfNonBoolExpression(t *testing.T) { if err == nil { t.Fatal("expected g:else-if non-bool diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "component_field_error") { t.Fatalf("Missing component_field_error diagnostic: %#v", diagnostics) } @@ -2757,7 +2975,11 @@ func TestValidateManifestRejectsGIfNonBoolExpression(t *testing.T) { if err == nil { t.Fatal("expected g:if non-bool diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "component_field_error") { t.Fatalf("Missing component_field_error diagnostic: %#v", diagnostics) } @@ -2878,7 +3100,11 @@ func TestValidateManifestRejectsBadAppendItemField(t *testing.T) { if err == nil { t.Fatal("expected Bad append item diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "component_client_error") || !strings.Contains(err.Error(), "unknown field") { t.Fatalf("Missing Bad append field diagnostic: %#v", diagnostics) } @@ -2903,7 +3129,11 @@ func TestValidateManifestRejectsGForWithoutKey(t *testing.T) { if err == nil { t.Fatal("expected Missing g:key diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "component_field_error") || !strings.Contains(err.Error(), "g:for requires g:key") { t.Fatalf("Missing g:for Missing key diagnostic: %#v", diagnostics) } @@ -2928,7 +3158,11 @@ func TestValidateManifestRejectsUnknownGForItemField(t *testing.T) { if err == nil { t.Fatal("expected unknown item field diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "component_field_error") || !strings.Contains(err.Error(), "item.Missing") { t.Fatalf("Missing unknown item field diagnostic: %#v", diagnostics) } @@ -2960,7 +3194,11 @@ func TestValidateManifestLoopBodyEventDiagnosticPointsToExpression(t *testing.T) if err == nil { t.Fatal("expected invalid loop-body event expression diagnostic") } - diagnostic := firstDiagnostic(err.(ValidationErrors), "component_field_error") + diagnostic := firstDiagnostic(func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }(), "component_field_error") if diagnostic == nil || !strings.Contains(diagnostic.Message, "Missing()") { t.Fatalf("Missing loop-body event diagnostic: %#v", err) } @@ -2991,7 +3229,11 @@ func TestValidateManifestLoopBodyInterpolationDiagnosticPointsToTextNode(t *test if err == nil { t.Fatal("expected unknown loop-item field diagnostic") } - diagnostic := firstDiagnostic(err.(ValidationErrors), "component_field_error") + diagnostic := firstDiagnostic(func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }(), "component_field_error") if diagnostic == nil || !strings.Contains(diagnostic.Message, "item.Missing") { t.Fatalf("Missing loop-body interpolation diagnostic: %#v", err) } @@ -3020,7 +3262,11 @@ func TestValidateManifestViewEventDiagnosticPointsToExpression(t *testing.T) { if err == nil { t.Fatal("expected invalid event expression diagnostic") } - diagnostic := firstDiagnostic(err.(ValidationErrors), "component_field_error") + diagnostic := firstDiagnostic(func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }(), "component_field_error") if diagnostic == nil || !strings.Contains(diagnostic.Message, "Missing()") { t.Fatalf("Missing event expression diagnostic: %#v", err) } @@ -3044,7 +3290,11 @@ func TestValidateManifestUnknownViewFieldDiagnosticPointsToIdentifier(t *testing if err == nil { t.Fatal("expected unknown field diagnostic") } - diagnostic := firstDiagnostic(err.(ValidationErrors), "component_field_error") + diagnostic := firstDiagnostic(func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }(), "component_field_error") if diagnostic == nil || !strings.Contains(diagnostic.Message, `"Missing"`) { t.Fatalf("Missing unknown field diagnostic: %#v", err) } @@ -3074,7 +3324,11 @@ func TestValidateManifestRepeatedViewExpressionDiagnosticPointsToOccurrence(t *t if err == nil { t.Fatal("expected class toggle diagnostic") } - diagnostic := firstDiagnostic(err.(ValidationErrors), "component_field_error") + diagnostic := firstDiagnostic(func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }(), "component_field_error") if diagnostic == nil || !strings.Contains(diagnostic.Message, "class toggle") { t.Fatalf("Missing class toggle diagnostic: %#v", err) } @@ -3098,7 +3352,11 @@ func TestValidateManifestBadGForDiagnosticPointsToDirectiveValue(t *testing.T) { if err == nil { t.Fatal("expected Bad g:for diagnostic") } - diagnostic := firstDiagnostic(err.(ValidationErrors), "component_field_error") + diagnostic := firstDiagnostic(func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }(), "component_field_error") if diagnostic == nil || !strings.Contains(diagnostic.Message, `g:for must use`) { t.Fatalf("Missing Bad g:for diagnostic: %#v", err) } @@ -3178,7 +3436,11 @@ func TestValidateManifestRejectsLocalVariableBeforeDeclaration(t *testing.T) { if err == nil { t.Fatal("expected local-before-declaration diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "component_client_error") || !strings.Contains(err.Error(), "next") { t.Fatalf("Missing local-before-declaration diagnostic: %#v", diagnostics) } @@ -3207,7 +3469,11 @@ func TestValidateManifestRejectsGoishConditionalTypeMismatch(t *testing.T) { if err == nil { t.Fatal("expected conditional branch mismatch diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "component_client_error") { t.Fatalf("Missing component_client_error diagnostic: %#v", diagnostics) } @@ -3327,7 +3593,11 @@ computed Second string { if err == nil { t.Fatal("expected computed cycle diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "component_client_error") { t.Fatalf("Missing component_client_error diagnostic: %#v", diagnostics) } @@ -3356,7 +3626,11 @@ func TestValidateManifestRejectsComputedMutation(t *testing.T) { if err == nil { t.Fatal("expected computed mutation diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "component_client_error") { t.Fatalf("Missing component_client_error diagnostic: %#v", diagnostics) } @@ -3381,7 +3655,11 @@ func TestValidateManifestRejectsUnknownNestedField(t *testing.T) { if err == nil { t.Fatal("expected unknown nested field diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "component_field_error") { t.Fatalf("Missing component_field_error diagnostic: %#v", diagnostics) } @@ -3426,7 +3704,11 @@ func TestValidateManifestRejectsValueBindingToNonStringState(t *testing.T) { if err == nil { t.Fatal("expected non-string value binding diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "component_field_error") { t.Fatalf("Missing component_field_error diagnostic: %#v", diagnostics) } @@ -3471,7 +3753,11 @@ func TestValidateManifestRejectsNumericValueBindingOutsideNumberInput(t *testing if err == nil { t.Fatal("expected numeric value binding target diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "component_field_error") { t.Fatalf("Missing component_field_error diagnostic: %#v", diagnostics) } @@ -3516,7 +3802,11 @@ func TestValidateManifestRejectsRadioValueBindingWithoutValue(t *testing.T) { if err == nil { t.Fatal("expected radio Missing value diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "component_field_error") { t.Fatalf("Missing component_field_error diagnostic: %#v", diagnostics) } @@ -3541,7 +3831,11 @@ func TestValidateManifestRejectsValueBindingToProp(t *testing.T) { if err == nil { t.Fatal("expected prop value binding diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "component_field_error") { t.Fatalf("Missing component_field_error diagnostic: %#v", diagnostics) } @@ -3586,7 +3880,11 @@ func TestValidateManifestRejectsCheckedBindingToNonBoolState(t *testing.T) { if err == nil { t.Fatal("expected non-bool checked binding diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "component_field_error") { t.Fatalf("Missing component_field_error diagnostic: %#v", diagnostics) } @@ -3634,7 +3932,11 @@ func TestValidateManifestRejectsNonBoolReactiveBooleanAttribute(t *testing.T) { if err == nil { t.Fatal("expected non-bool boolean attr diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "component_field_error") { t.Fatalf("Missing component_field_error diagnostic: %#v", diagnostics) } @@ -3661,7 +3963,11 @@ func TestValidateManifestRejectsUnsafeReactiveURLAttribute(t *testing.T) { if err == nil { t.Fatal("expected unsafe reactive URL attr diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "component_field_error") { t.Fatalf("Missing component_field_error diagnostic: %#v", diagnostics) } @@ -3709,7 +4015,11 @@ func TestValidateManifestRejectsNonBoolClassToggle(t *testing.T) { if err == nil { t.Fatal("expected non-bool class toggle diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "component_field_error") { t.Fatalf("Missing component_field_error diagnostic: %#v", diagnostics) } @@ -3759,7 +4069,11 @@ func TestValidateManifestRejectsBoolStyleBinding(t *testing.T) { if err == nil { t.Fatal("expected bool style binding diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "component_field_error") { t.Fatalf("Missing component_field_error diagnostic: %#v", diagnostics) } @@ -3782,7 +4096,11 @@ func TestValidateManifestRejectsRelativeGoTypedImportPath(t *testing.T) { if err == nil { t.Fatal("expected invalid import diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "invalid_go_import") { t.Fatalf("Missing invalid_go_import diagnostic: %#v", diagnostics) } @@ -3807,7 +4125,11 @@ func TestValidateManifestRejectsStateInitReturnMismatch(t *testing.T) { if err == nil { t.Fatal("expected state init mismatch diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "component_contract_error") { t.Fatalf("Missing component_contract_error diagnostic: %#v", diagnostics) } @@ -3832,7 +4154,11 @@ func TestValidateManifestResolvesLayoutsByID(t *testing.T) { if err == nil { t.Fatal("expected unknown layout diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "unknown_layout_id") { t.Fatalf("Missing unknown_layout_id diagnostic: %#v", diagnostics) } @@ -3881,7 +4207,11 @@ func TestValidateManifestRejectsUnqualifiedCrossPackageLayout(t *testing.T) { if err == nil { t.Fatal("expected unknown layout diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "unknown_layout_id") { t.Fatalf("Missing unknown_layout_id diagnostic: %#v", diagnostics) } @@ -3899,7 +4229,11 @@ func TestValidateManifestRejectsDuplicateLayoutIDs(t *testing.T) { if err == nil { t.Fatal("expected duplicate layout diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "duplicate_layout_id") { t.Fatalf("Missing duplicate_layout_id diagnostic: %#v", diagnostics) } @@ -3930,7 +4264,11 @@ func TestValidateManifestRejectsDuplicateLayoutIDsInSamePackage(t *testing.T) { if err == nil { t.Fatal("expected duplicate layout diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "duplicate_layout_id") { t.Fatalf("Missing duplicate_layout_id diagnostic: %#v", diagnostics) } @@ -3947,7 +4285,11 @@ func TestValidateManifestRejectsLayoutSelfReference(t *testing.T) { if err == nil { t.Fatal("expected layout self-reference diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "layout_self_reference") { t.Fatalf("Missing layout_self_reference diagnostic: %#v", diagnostics) } @@ -3965,7 +4307,11 @@ func TestValidateManifestRejectsCyclicLayoutInheritance(t *testing.T) { if err == nil { t.Fatal("expected cyclic layout diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "cyclic_layout_reference") { t.Fatalf("Missing cyclic_layout_reference diagnostic: %#v", diagnostics) } @@ -4011,7 +4357,11 @@ func TestValidateManifestRejectsLayoutFileUseAndQualifiedParentLayout(t *testing if err == nil { t.Fatal("expected layout use diagnostics") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "unsupported_gowdk_use_scope") { t.Fatalf("Missing unsupported_gowdk_use_scope diagnostic: %#v", diagnostics) } @@ -4046,7 +4396,11 @@ func TestValidateManifestRejectsUnknownLayoutParent(t *testing.T) { if err == nil { t.Fatal("expected unknown layout parent diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "unknown_layout_id") { t.Fatalf("Missing unknown_layout_id diagnostic: %#v", diagnostics) } @@ -4070,7 +4424,11 @@ func TestValidateManifestReportsContractReferenceParseErrors(t *testing.T) { if err == nil { t.Fatal("expected contract reference parse diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "contract_reference_parse_error") { t.Fatalf("Missing contract_reference_parse_error diagnostic: %#v", diagnostics) } @@ -4127,7 +4485,11 @@ func TestValidateManifestRejectsInvalidContractReferenceRoutes(t *testing.T) { if err == nil { t.Fatal("expected invalid contract route diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "contract_route_invalid") { t.Fatalf("Missing contract_route_invalid diagnostic: %#v", diagnostics) } @@ -4160,7 +4522,11 @@ func TestValidateManifestRejectsDefaultContractRouteOnDynamicPage(t *testing.T) if err == nil { t.Fatal("expected invalid dynamic default contract route diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticMessage(diagnostics, "contract_route_invalid", "dynamic page route", "/blog/{slug}") { t.Fatalf("Missing dynamic default contract_route_invalid diagnostic: %#v", diagnostics) } @@ -4184,7 +4550,11 @@ func TestValidateManifestRejectsDefaultQueryRouteWithDynamicParams(t *testing.T) if err == nil { t.Fatal("expected invalid contract query route diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "contract_route_invalid") { t.Fatalf("Missing contract_route_invalid diagnostic: %#v", diagnostics) } @@ -4204,7 +4574,11 @@ func TestValidateManifestRejectsLayoutWithoutSlot(t *testing.T) { if err == nil { t.Fatal("expected layout slot diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "layout_slot_count") { t.Fatalf("Missing layout_slot_count diagnostic: %#v", diagnostics) } @@ -4221,7 +4595,11 @@ func TestValidateManifestRejectsLayoutWithMultipleSlots(t *testing.T) { if err == nil { t.Fatal("expected layout slot diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "layout_slot_count") { t.Fatalf("Missing layout_slot_count diagnostic: %#v", diagnostics) } @@ -4239,7 +4617,11 @@ func TestValidateManifestRejectsDuplicatePageRoutes(t *testing.T) { if err == nil { t.Fatal("expected duplicate route diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "duplicate_route") { t.Fatalf("Missing duplicate_route diagnostic: %#v", diagnostics) } @@ -4276,7 +4658,11 @@ func TestValidateManifestRejectsAmbiguousDynamicPageRoutes(t *testing.T) { if err == nil { t.Fatal("expected ambiguous dynamic route diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "ambiguous_dynamic_route") { t.Fatalf("Missing ambiguous_dynamic_route diagnostic: %#v", diagnostics) } @@ -4344,7 +4730,11 @@ func TestValidateManifestRejectsDynamicFragmentEndpointOverlap(t *testing.T) { if err == nil { t.Fatal("expected ambiguous dynamic route diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticMessage(diagnostics, "ambiguous_dynamic_route", test.expectMsg...) { t.Fatalf("Missing ambiguous_dynamic_route diagnostic: %#v", diagnostics) } @@ -4377,7 +4767,11 @@ func TestValidateManifestRejectsDynamicFragmentContractOverlap(t *testing.T) { if err == nil { t.Fatal("expected ambiguous dynamic route diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticMessage(diagnostics, "ambiguous_dynamic_route", "/patients/42/vitals", "query contract patients.GetVitals", "fragment patients.Vitals") { t.Fatalf("Missing ambiguous_dynamic_route diagnostic: %#v", diagnostics) } @@ -4425,7 +4819,11 @@ func TestValidateManifestRejectsRouteMethodConflicts(t *testing.T) { if err == nil { t.Fatal("expected route method conflict") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "route_method_conflict") { t.Fatalf("Missing route_method_conflict diagnostic: %#v", diagnostics) } @@ -4447,7 +4845,11 @@ func TestValidateManifestRejectsRouteMethodConflicts(t *testing.T) { if err == nil { t.Fatal("expected route method conflict") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "route_method_conflict") { t.Fatalf("Missing route_method_conflict diagnostic: %#v", diagnostics) } @@ -4470,7 +4872,11 @@ func TestValidateManifestRejectsRouteMethodConflicts(t *testing.T) { if err == nil { t.Fatal("expected command/action route method conflict") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticMessage(diagnostics, "route_method_conflict", "POST", "/patients", "command contract patients.CreatePatient", "action patients.CreatePatient") { t.Fatalf("Missing command/action route_method_conflict diagnostic: %#v", diagnostics) } @@ -4493,7 +4899,11 @@ func TestValidateManifestRejectsRouteMethodConflicts(t *testing.T) { if err == nil { t.Fatal("expected default command/action route method conflict") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticMessage(diagnostics, "route_method_conflict", "POST", "/patients", "command contract patients.CreatePatient", "action patients.Save") { t.Fatalf("Missing default command/action route_method_conflict diagnostic: %#v", diagnostics) } @@ -4518,7 +4928,11 @@ func TestValidateManifestRejectsRouteMethodConflicts(t *testing.T) { if err == nil { t.Fatal("expected duplicate command route method conflict") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticMessage(diagnostics, "route_method_conflict", "POST", "/patients", "command contract patients.UpdatePatient", "command contract patients.CreatePatient") { t.Fatalf("Missing duplicate command route_method_conflict diagnostic: %#v", diagnostics) } @@ -4561,7 +4975,11 @@ func TestValidateManifestRejectsRouteMethodConflicts(t *testing.T) { if err == nil { t.Fatal("expected query/api route method conflict") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticMessage(diagnostics, "route_method_conflict", "GET", "/patients", "query contract patients.ListPatients", "api patients.ListPatients") { t.Fatalf("Missing query/api route_method_conflict diagnostic: %#v", diagnostics) } @@ -4586,7 +5004,11 @@ func TestValidateManifestRejectsRouteMethodConflicts(t *testing.T) { if err == nil { t.Fatal("expected duplicate query route method conflict") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticMessage(diagnostics, "route_method_conflict", "GET", "/patients", "query contract patients.SearchPatients", "query contract patients.ListPatients") { t.Fatalf("Missing duplicate query route_method_conflict diagnostic: %#v", diagnostics) } @@ -4642,7 +5064,11 @@ func TestValidateManifestRejectsLocalizedRouteMethodConflicts(t *testing.T) { if err == nil { t.Fatal("expected localized route conflict") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticMessage(diagnostics, "route_method_conflict", "GET", "/en", "api api.Status", "page home") { t.Fatalf("missing localized route conflict diagnostic: %#v", diagnostics) } @@ -4958,7 +5384,11 @@ func TestValidateManifestRejectsDuplicateRestRoutes(t *testing.T) { if err == nil { t.Fatal("expected duplicate route diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "duplicate_route") { t.Fatalf("Missing duplicate_route diagnostic: %#v", diagnostics) } @@ -4987,7 +5417,11 @@ func TestValidateManifestRejectsAmbiguousRestRoutes(t *testing.T) { if err == nil { t.Fatal("expected ambiguous dynamic route diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "ambiguous_dynamic_route") { t.Fatalf("Missing ambiguous_dynamic_route diagnostic: %#v", diagnostics) } @@ -5077,7 +5511,11 @@ func TestValidateManifestRejectsEndpointInsideRestRouteNamespace(t *testing.T) { if err == nil { t.Fatal("expected ambiguous dynamic route diagnostic for endpoint inside rest namespace") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "ambiguous_dynamic_route") { t.Fatalf("Missing ambiguous_dynamic_route diagnostic: %#v", diagnostics) } @@ -5431,7 +5869,11 @@ func TestValidateManifestRejectsUnknownQualifiedCSSAssetUseAlias(t *testing.T) { if err == nil { t.Fatal("expected unknown CSS asset alias diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "unknown_gowdk_use_alias") { t.Fatalf("missing unknown alias diagnostic: %#v", diagnostics) } @@ -5660,7 +6102,11 @@ func TestValidateSourceProgramKeepsSingleFileUseChecks(t *testing.T) { if err == nil { t.Fatal("expected duplicate alias diagnostic") } - diagnostics := err.(ValidationErrors) + diagnostics := func() ValidationErrors { + var target ValidationErrors + _ = errors.As(err, &target) + return target + }() if !hasDiagnosticCode(diagnostics, "duplicate_gowdk_use_alias") { t.Fatalf("missing duplicate alias diagnostic: %#v", diagnostics) } diff --git a/internal/contractscan/ast_helpers.go b/internal/contractscan/ast_helpers.go index 22ceb02e..11ca4685 100644 --- a/internal/contractscan/ast_helpers.go +++ b/internal/contractscan/ast_helpers.go @@ -53,7 +53,7 @@ func goImportAliases(file *ast.File) map[string]string { if err != nil || importPath == "" { continue } - alias := "" + var alias string switch { case importSpec.Name == nil: alias = pathpkg.Base(importPath) diff --git a/internal/contractscan/scan.go b/internal/contractscan/scan.go index abecc0b9..a27f3fa0 100644 --- a/internal/contractscan/scan.go +++ b/internal/contractscan/scan.go @@ -14,8 +14,10 @@ import ( runtimecontracts "github.com/cssbruno/gowdk/runtime/contracts" ) -const RuntimeImportPath = "github.com/cssbruno/gowdk/runtime/contracts" -const generatedAppModulePath = "gowdk-generated-app" +const ( + RuntimeImportPath = "github.com/cssbruno/gowdk/runtime/contracts" + generatedAppModulePath = "gowdk-generated-app" +) // Contract describes one discovered registration call. type Contract struct { diff --git a/internal/discover/discover.go b/internal/discover/discover.go index 19c71c44..313a5239 100644 --- a/internal/discover/discover.go +++ b/internal/discover/discover.go @@ -21,14 +21,8 @@ func Files(root string, includes, excludes []string) ([]string, error) { func FilesAndDirs(root string, includes, excludes []string) ([]string, []string, error) { var files []string var dirs []string - includeMatchers, err := compileGlobs(includes) - if err != nil { - return nil, nil, err - } - excludeMatchers, err := compileGlobs(excludes) - if err != nil { - return nil, nil, err - } + includeMatchers := compileGlobs(includes) + excludeMatchers := compileGlobs(excludes) if err := filepath.WalkDir(root, func(path string, entry fs.DirEntry, err error) error { if err != nil { @@ -77,7 +71,7 @@ func FilesAndDirs(root string, includes, excludes []string) ([]string, []string, type globPattern string -func compileGlobs(patterns []string) ([]globPattern, error) { +func compileGlobs(patterns []string) []globPattern { var matchers []globPattern for _, pattern := range patterns { if strings.TrimSpace(pattern) == "" { @@ -85,7 +79,7 @@ func compileGlobs(patterns []string) ([]globPattern, error) { } matchers = append(matchers, globPattern(filepath.ToSlash(pattern))) } - return matchers, nil + return matchers } // matchesExcludedDir reports whether a directory (given by its slash-separated diff --git a/internal/discover/discover_test.go b/internal/discover/discover_test.go index 5198cfae..ef5e0aba 100644 --- a/internal/discover/discover_test.go +++ b/internal/discover/discover_test.go @@ -50,10 +50,7 @@ func TestFilesPrunesExcludedDirectories(t *testing.T) { } func TestMatchesExcludedDir(t *testing.T) { - matchers, err := compileGlobs([]string{"node_modules/**", "vendor/**", "**/testdata/**", "src/**/card.cmp.gwdk"}) - if err != nil { - t.Fatal(err) - } + matchers := compileGlobs([]string{"node_modules/**", "vendor/**", "**/testdata/**", "src/**/card.cmp.gwdk"}) tests := []struct { dir string want bool diff --git a/internal/doclint/main.go b/internal/doclint/main.go index d59ea848..1ddd50f5 100644 --- a/internal/doclint/main.go +++ b/internal/doclint/main.go @@ -131,7 +131,7 @@ func Check(cfg Config) ([]Problem, error) { if rel == "" { rel = file } - if p, ok := checkLink(root, file, rel, link, anchorsFor); ok { + if p, ok := checkLink(file, rel, link, anchorsFor); ok { problems = append(problems, p) } } @@ -145,7 +145,7 @@ type link struct { Line int } -func checkLink(root, file, rel string, l link, anchorsFor func(string) (map[string]bool, error)) (Problem, bool) { +func checkLink(file, rel string, l link, anchorsFor func(string) (map[string]bool, error)) (Problem, bool) { target := l.Target if target == "" || isExternal(target) { return Problem{}, false diff --git a/internal/doclint/parse.go b/internal/doclint/parse.go index 5602d4eb..aa270ced 100644 --- a/internal/doclint/parse.go +++ b/internal/doclint/parse.go @@ -205,11 +205,11 @@ func referenceDefinition(line string) (string, bool) { if !strings.HasPrefix(trimmed, "[") { return "", false } - close := strings.Index(trimmed, "]:") - if close < 1 { + closeIndex := strings.Index(trimmed, "]:") + if closeIndex < 1 { return "", false } - rest := strings.TrimSpace(trimmed[close+2:]) + rest := strings.TrimSpace(trimmed[closeIndex+2:]) if rest == "" { return "", false } diff --git a/internal/gotypes/gotypes.go b/internal/gotypes/gotypes.go index f6d8091a..8df6f972 100644 --- a/internal/gotypes/gotypes.go +++ b/internal/gotypes/gotypes.go @@ -211,7 +211,9 @@ func RunStateInitJSON(imports []gwdkir.Import, state gwdkir.StateContract) ([]by return nil, err } path := file.Name() - defer os.Remove(path) + defer func() { + _ = os.Remove(path) + }() if err := file.Close(); err != nil { return nil, err } @@ -414,7 +416,7 @@ func stateInitEncodeGuardStmt() ast.Stmt { // alias and rejects relative import paths. func ImportPathForAlias(imports []gwdkir.Import, alias string) (string, error) { if strings.TrimSpace(alias) == "" { - return "", fmt.Errorf("Go import alias is required") + return "", fmt.Errorf("go import alias is required") } for _, item := range imports { effective, err := EffectiveImportAlias(item) @@ -425,7 +427,7 @@ func ImportPathForAlias(imports []gwdkir.Import, alias string) (string, error) { return item.Path, nil } } - return "", fmt.Errorf("Go import alias %q is not declared", alias) + return "", fmt.Errorf("go import alias %q is not declared", alias) } // EffectiveImportAlias returns the explicit import alias or the package name @@ -449,10 +451,10 @@ func EffectiveImportAlias(item gwdkir.Import) (string, error) { func ValidateImportPath(importPath string) error { path := strings.TrimSpace(importPath) if path == "" { - return fmt.Errorf("Go import path is required") + return fmt.Errorf("go import path is required") } if strings.HasPrefix(path, ".") || strings.HasPrefix(path, "/") || strings.Contains(path, `\`) { - return fmt.Errorf("Go import path %q must be a module import path, not a relative path", importPath) + return fmt.Errorf("go import path %q must be a module import path, not a relative path", importPath) } return nil } @@ -523,7 +525,7 @@ func loadPackage(importPath string) (loadedPackage, error) { files = append(files, file) } if len(files) == 0 { - return loadedPackage{}, fmt.Errorf("Go package %q has no buildable Go files", importPath) + return loadedPackage{}, fmt.Errorf("go package %q has no buildable Go files", importPath) } config := types.Config{Importer: importer.Default()} checked, err := config.Check(info.ImportPath, fileSet, files, nil) diff --git a/internal/gowdkcmd/add.go b/internal/gowdkcmd/add.go index a8c42572..0ca21cbe 100644 --- a/internal/gowdkcmd/add.go +++ b/internal/gowdkcmd/add.go @@ -269,8 +269,7 @@ func listAddons(registryList bool, jsonOutput bool) error { fmt.Println(string(payload)) return nil } - writeAddonRegistryList(os.Stdout, registry.Addons) - return nil + return writeAddonRegistryList(os.Stdout, registry.Addons) } if jsonOutput { var addable []addonregistry.Entry @@ -298,16 +297,23 @@ func listAddons(registryList bool, jsonOutput bool) error { return nil } -func writeAddonRegistryList(writer io.Writer, entries []addonregistry.Entry) { - fmt.Fprintln(writer, "Addon registry (metadata only; external installation stays explicit):") - fmt.Fprintf(writer, " %-14s %-19s %-12s %-13s %-5s %s\n", "NAME", "KIND", "LIFECYCLE", "COMPAT", "ADD", "SUMMARY") +func writeAddonRegistryList(writer io.Writer, entries []addonregistry.Entry) error { + if _, err := fmt.Fprintln(writer, "Addon registry (metadata only; external installation stays explicit):"); err != nil { + return err + } + if _, err := fmt.Fprintf(writer, " %-14s %-19s %-12s %-13s %-5s %s\n", "NAME", "KIND", "LIFECYCLE", "COMPAT", "ADD", "SUMMARY"); err != nil { + return err + } for _, entry := range entries { addable := "no" if entry.Constructor.Addable { addable = "yes" } - fmt.Fprintf(writer, " %-14s %-19s %-12s %-13s %-5s %s\n", entry.Name, entry.Kind, entry.Lifecycle, entry.Compatibility, addable, entry.Summary) + if _, err := fmt.Fprintf(writer, " %-14s %-19s %-12s %-13s %-5s %s\n", entry.Name, entry.Kind, entry.Lifecycle, entry.Compatibility, addable, entry.Summary); err != nil { + return err + } } + return nil } // findConfigLiteral returns the composite literal for `var Config = ...`. diff --git a/internal/gowdkcmd/audit.go b/internal/gowdkcmd/audit.go index bc4016d7..326f3347 100644 --- a/internal/gowdkcmd/audit.go +++ b/internal/gowdkcmd/audit.go @@ -5,6 +5,7 @@ import ( "context" "crypto/sha256" "encoding/json" + "errors" "fmt" "go/parser" "go/token" @@ -336,7 +337,7 @@ func parseAuditCommandOptions(args []string) (auditCommandOptions, []string, err options.EmitTests = true options.TestPath = strings.TrimSpace(strings.TrimPrefix(arg, "--emit-tests=")) if options.TestPath == "" { - return options, nil, fmt.Errorf(auditUsage) + return options, nil, errors.New(auditUsage) } case arg == "--check-tests": options.CheckTests = true @@ -344,7 +345,7 @@ func parseAuditCommandOptions(args []string) (auditCommandOptions, []string, err options.CheckTests = true options.TestPath = strings.TrimSpace(strings.TrimPrefix(arg, "--check-tests=")) if options.TestPath == "" { - return options, nil, fmt.Errorf(auditUsage) + return options, nil, errors.New(auditUsage) } case arg == "--force": options.Force = true @@ -365,7 +366,7 @@ func parseAuditCommandOptions(args []string) (auditCommandOptions, []string, err case strings.HasPrefix(arg, "--schema="): options.Schema = strings.TrimSpace(strings.TrimPrefix(arg, "--schema=")) if options.Schema == "" { - return options, nil, fmt.Errorf(auditUsage) + return options, nil, errors.New(auditUsage) } case arg == "--sarif": options.SARIF = true @@ -373,21 +374,21 @@ func parseAuditCommandOptions(args []string) (auditCommandOptions, []string, err options.SARIF = true options.SARIFPath = strings.TrimSpace(strings.TrimPrefix(arg, "--sarif=")) if options.SARIFPath == "" { - return options, nil, fmt.Errorf(auditUsage) + return options, nil, errors.New(auditUsage) } case strings.HasPrefix(arg, "--diff="): options.DiffPath = strings.TrimSpace(strings.TrimPrefix(arg, "--diff=")) if options.DiffPath == "" { - return options, nil, fmt.Errorf(auditUsage) + return options, nil, errors.New(auditUsage) } case arg == "--diff": if index+1 >= len(args) { - return options, nil, fmt.Errorf(auditUsage) + return options, nil, errors.New(auditUsage) } index++ options.DiffPath = strings.TrimSpace(args[index]) if options.DiffPath == "" { - return options, nil, fmt.Errorf(auditUsage) + return options, nil, errors.New(auditUsage) } default: projectArgs = append(projectArgs, arg) @@ -488,15 +489,28 @@ func handleAuditTests(auditOptions auditCommandOptions, options cliOptions, ir g } func standaloneAuditPackageName(dir string) string { - packages, err := parser.ParseDir(token.NewFileSet(), dir, func(info os.FileInfo) bool { - name := info.Name() - return strings.HasSuffix(name, ".go") && !strings.HasSuffix(name, "_test.go") - }, parser.PackageClauseOnly) - if err != nil || len(packages) == 0 { + entries, err := os.ReadDir(dir) + if err != nil { + return "gowdkaudit_test" + } + packageNames := map[string]bool{} + fileSet := token.NewFileSet() + for _, entry := range entries { + name := entry.Name() + if entry.IsDir() || !strings.HasSuffix(name, ".go") || strings.HasSuffix(name, "_test.go") { + continue + } + file, err := parser.ParseFile(fileSet, filepath.Join(dir, name), nil, parser.PackageClauseOnly) + if err != nil || file.Name == nil { + continue + } + packageNames[file.Name.Name] = true + } + if len(packageNames) == 0 { return "gowdkaudit_test" } - names := make([]string, 0, len(packages)) - for name := range packages { + names := make([]string, 0, len(packageNames)) + for name := range packageNames { names = append(names, name) } sort.Strings(names) @@ -524,7 +538,9 @@ func runGeneratedAppAuditTests(options cliOptions, ir gwdkir.Program, runOptions if err != nil { return "", auditRunResult{}, err } - defer os.RemoveAll(tempRoot) + defer func() { + _ = os.RemoveAll(tempRoot) + }() outputDir := filepath.Join(tempRoot, "output") appDir := filepath.Join(tempRoot, "app") diff --git a/internal/gowdkcmd/audit_diff_test.go b/internal/gowdkcmd/audit_diff_test.go index 7835a104..82ed3d1f 100644 --- a/internal/gowdkcmd/audit_diff_test.go +++ b/internal/gowdkcmd/audit_diff_test.go @@ -9,27 +9,27 @@ import ( "github.com/cssbruno/gowdk/internal/diagnostics" ) -func enrichedFinding(code, target, source, message string, severity diagnostics.Severity) auditspec.Finding { +func enrichedFinding(code, target, source, message string) auditspec.Finding { enriched := auditspec.EnrichFindings([]auditspec.Finding{{ Code: code, + Severity: diagnostics.SeverityError, Target: target, Source: source, Message: message, - Severity: severity, }}) return enriched[0] } func TestComputeAuditDiffClassifiesIntroducedResolvedUnchanged(t *testing.T) { previous := []auditspec.Finding{ - enrichedFinding("audit_action_missing_csrf", "action:A", "a.gwdk:10", "missing csrf", diagnostics.SeverityError), - enrichedFinding("audit_raw_html_sink", "component:Old", "old.gwdk:3", "raw html", diagnostics.SeverityError), + enrichedFinding("audit_action_missing_csrf", "action:A", "a.gwdk:10", "missing csrf"), + enrichedFinding("audit_raw_html_sink", "component:Old", "old.gwdk:3", "raw html"), } current := []auditspec.Finding{ // previous[0] moved to a different line: same fingerprint -> unchanged. - enrichedFinding("audit_action_missing_csrf", "action:A", "a.gwdk:42", "missing csrf", diagnostics.SeverityError), + enrichedFinding("audit_action_missing_csrf", "action:A", "a.gwdk:42", "missing csrf"), // A brand new finding -> introduced. - enrichedFinding("audit_contract_roleless", "contract:New", "new.gwdk:7", "roleless", diagnostics.SeverityError), + enrichedFinding("audit_contract_roleless", "contract:New", "new.gwdk:7", "roleless"), } diff := computeAuditDiff("sha256:prev", previous, current) @@ -48,19 +48,19 @@ func TestComputeAuditDiffClassifiesIntroducedResolvedUnchanged(t *testing.T) { } func TestFindingFingerprintIsStableAcrossLineMovement(t *testing.T) { - a := enrichedFinding("audit_action_missing_csrf", "action:A", "pages/a.gwdk:10", "missing csrf", diagnostics.SeverityError) - b := enrichedFinding("audit_action_missing_csrf", "action:A", "pages/a.gwdk:999", "missing csrf", diagnostics.SeverityError) + a := enrichedFinding("audit_action_missing_csrf", "action:A", "pages/a.gwdk:10", "missing csrf") + b := enrichedFinding("audit_action_missing_csrf", "action:A", "pages/a.gwdk:999", "missing csrf") if a.Fingerprint == "" || a.Fingerprint != b.Fingerprint { t.Fatalf("fingerprint must ignore line numbers: %q vs %q", a.Fingerprint, b.Fingerprint) } - c := enrichedFinding("audit_action_missing_csrf", "action:B", "pages/a.gwdk:10", "missing csrf", diagnostics.SeverityError) + c := enrichedFinding("audit_action_missing_csrf", "action:B", "pages/a.gwdk:10", "missing csrf") if a.Fingerprint == c.Fingerprint { t.Fatal("fingerprint must change when the target changes") } } func TestComputeAuditDiffIgnoresWaivedFindings(t *testing.T) { - waived := enrichedFinding("audit_action_missing_csrf", "action:A", "a.gwdk:1", "missing csrf", diagnostics.SeverityError) + waived := enrichedFinding("audit_action_missing_csrf", "action:A", "a.gwdk:1", "missing csrf") waived.Suppression = &auditspec.Suppression{Owner: "sec"} diff := computeAuditDiff("base", nil, []auditspec.Finding{waived}) if len(diff.Introduced) != 0 || diff.IntroducedErrors != 0 { @@ -69,9 +69,9 @@ func TestComputeAuditDiffIgnoresWaivedFindings(t *testing.T) { } func TestComputeAuditDiffTreatsNowWaivedFindingAsUnchangedNotResolved(t *testing.T) { - active := enrichedFinding("audit_action_missing_csrf", "action:A", "a.gwdk:10", "missing csrf", diagnostics.SeverityError) + active := enrichedFinding("audit_action_missing_csrf", "action:A", "a.gwdk:10", "missing csrf") // The same finding, still present, but waived in the current report. - waivedNow := enrichedFinding("audit_action_missing_csrf", "action:A", "a.gwdk:10", "missing csrf", diagnostics.SeverityError) + waivedNow := enrichedFinding("audit_action_missing_csrf", "action:A", "a.gwdk:10", "missing csrf") waivedNow.Suppression = &auditspec.Suppression{Owner: "sec"} diff := computeAuditDiff("base", []auditspec.Finding{active}, []auditspec.Finding{waivedNow}) @@ -84,9 +84,9 @@ func TestComputeAuditDiffTreatsNowWaivedFindingAsUnchangedNotResolved(t *testing } func TestComputeAuditDiffTreatsUnwaivedFindingAsUnchangedNotIntroduced(t *testing.T) { - waivedBefore := enrichedFinding("audit_action_missing_csrf", "action:A", "a.gwdk:10", "missing csrf", diagnostics.SeverityError) + waivedBefore := enrichedFinding("audit_action_missing_csrf", "action:A", "a.gwdk:10", "missing csrf") waivedBefore.Suppression = &auditspec.Suppression{Owner: "sec"} - activeNow := enrichedFinding("audit_action_missing_csrf", "action:A", "a.gwdk:10", "missing csrf", diagnostics.SeverityError) + activeNow := enrichedFinding("audit_action_missing_csrf", "action:A", "a.gwdk:10", "missing csrf") diff := computeAuditDiff("base", []auditspec.Finding{waivedBefore}, []auditspec.Finding{activeNow}) if len(diff.Introduced) != 0 || diff.IntroducedErrors != 0 { @@ -135,7 +135,8 @@ func TestAuditErrorExitCodePrefersRuntimeFailure(t *testing.T) { if got := auditErrorExitCode(staticOnly); got != auditExitErrorFindings { t.Fatalf("static error findings should exit %d, got %d", auditExitErrorFindings, got) } - withRuntime := append(staticOnly, auditspec.Finding{Code: "audit_test_failed", Severity: diagnostics.SeverityError}) + withRuntime := append([]auditspec.Finding{}, staticOnly...) + withRuntime = append(withRuntime, auditspec.Finding{Code: "audit_test_failed", Severity: diagnostics.SeverityError}) if got := auditErrorExitCode(withRuntime); got != auditExitRuntimeFailure { t.Fatalf("a runtime test failure should exit %d, got %d", auditExitRuntimeFailure, got) } diff --git a/internal/gowdkcmd/build.go b/internal/gowdkcmd/build.go index bcf42a51..bd2ec2cf 100644 --- a/internal/gowdkcmd/build.go +++ b/internal/gowdkcmd/build.go @@ -220,7 +220,7 @@ func buildOnce(options cliOptions, request buildRequest, timings *buildTimingRec if request.hasRoleArtifacts() { outputDir = filepath.ToSlash(filepath.Join("gowdk_cache", "roles")) } else { - return fmt.Errorf(buildUsage) + return errors.New(buildUsage) } } request.OutputDir = outputDir @@ -236,7 +236,7 @@ func buildOnce(options cliOptions, request buildRequest, timings *buildTimingRec discovered, discoverErr = discoverBuildFiles(options.Config, outputDir, request.Modules, options.ProjectRoot) return discoverErr }); err != nil { - return operationErrorFromCause("build failed", err) + return operationErrorFromCause(err) } if len(discovered) == 0 && !request.hasRoleArtifacts() { return fmt.Errorf("no .gwdk files found") @@ -247,10 +247,12 @@ func buildOnce(options cliOptions, request buildRequest, timings *buildTimingRec var app gwdkanalysis.Sources var diagnostics lang.Diagnostics - timings.measure("parse_lower", func() error { + if err := timings.measure("parse_lower", func() error { app, diagnostics = lang.ParseBuildFiles(paths) return nil - }) + }); err != nil { + return operationErrorFromCause(err) + } if diagnostics.HasErrors() { return operationErrorFromLang("build failed", diagnostics) } @@ -258,10 +260,12 @@ func buildOnce(options cliOptions, request buildRequest, timings *buildTimingRec fmt.Fprintln(os.Stderr, diagnostic.String()) } var ir gwdkir.Program - timings.measure("ir_assembly", func() error { + if err := timings.measure("ir_assembly", func() error { ir = gwdkanalysis.BuildProgram(options.Config, app) return nil - }) + }); err != nil { + return operationErrorFromCause(err) + } timings.counter("pages", len(ir.Pages)) timings.counter("components", len(ir.Components)) timings.counter("layouts", len(ir.Layouts)) @@ -276,13 +280,15 @@ func buildOnce(options cliOptions, request buildRequest, timings *buildTimingRec } return bindErr }); err != nil { - return operationErrorFromCause("build failed", err) + return operationErrorFromCause(err) } var report compiler.ValidationErrors - timings.measure("ir_validation", func() error { + if err := timings.measure("ir_validation", func() error { _, report = compiler.ValidateAnalyzedProgramReport(options.Config, analyzed) return nil - }) + }); err != nil { + return operationErrorFromCause(err) + } if report.HasErrors() { return operationErrorFromCompiler("build failed", report, report) } @@ -309,7 +315,7 @@ func buildOnce(options cliOptions, request buildRequest, timings *buildTimingRec } return compiler.ValidateQueryInvalidations(options.Config, ir.QueryInvalidations) }); err != nil { - return operationErrorFromCause("build failed", err) + return operationErrorFromCause(err) } var validated compiler.ValidatedProgram @@ -318,7 +324,7 @@ func buildOnce(options cliOptions, request buildRequest, timings *buildTimingRec validated, validateErr = compiler.ValidateIR(options.Config, ir) return validateErr }); err != nil { - return operationErrorFromCause("build failed", err) + return operationErrorFromCause(err) } if err := timings.measure("security_audit", func() error { @@ -338,12 +344,12 @@ func buildOnce(options cliOptions, request buildRequest, timings *buildTimingRec return buildErr }); err != nil { printBuildgenBuildErrorReport(err, options.Debug) - return operationErrorFromCause("build failed", err) + return operationErrorFromCause(err) } if err := timings.measure("final_security_audit", func() error { return enforceFinalBuildArtifactSecurityAudit(options, result) }); err != nil { - return operationErrorFromCause("build failed", err) + return operationErrorFromCause(err) } timings.counter("artifacts", len(result.Artifacts)) timings.counter("css_artifacts", len(result.CSSArtifacts)) @@ -383,7 +389,7 @@ func buildOnce(options cliOptions, request buildRequest, timings *buildTimingRec asyncAPIPath, writeErr = contractscan.WriteAsyncAPI(outputDir, contractReport, contractscan.AsyncAPIOptions{}) return writeErr }); err != nil { - return operationErrorFromCause("build failed", err) + return operationErrorFromCause(err) } if asyncAPIPath != "" { fmt.Println(asyncAPIPath) @@ -415,7 +421,7 @@ func buildOnce(options cliOptions, request buildRequest, timings *buildTimingRec app, appErr = appgen.GenerateWithPlan(outputDir, appDir, appPlan) return appErr }); err != nil { - return operationErrorFromCause("build failed", err) + return operationErrorFromCause(err) } fmt.Println(app.ModulePath) fmt.Println(app.PackagePath) @@ -427,7 +433,7 @@ func buildOnce(options cliOptions, request buildRequest, timings *buildTimingRec built, buildErr = appgen.BuildBinary(app.AppDir, binaryPath) return buildErr }); err != nil { - return operationErrorFromCause("build failed", err) + return operationErrorFromCause(err) } fmt.Println(built) buildReportEvents = append(buildReportEvents, buildgen.BuildEvent{ @@ -444,7 +450,7 @@ func buildOnce(options cliOptions, request buildRequest, timings *buildTimingRec artifacts, dockerErr = writeDockerArtifacts(built, request.DockerBase) return dockerErr }); err != nil { - return operationErrorFromCause("build failed", err) + return operationErrorFromCause(err) } fmt.Println(artifacts.DockerfilePath) fmt.Println(artifacts.DockerignorePath) @@ -477,7 +483,7 @@ func buildOnce(options cliOptions, request buildRequest, timings *buildTimingRec built, buildErr = appgen.BuildWASM(app.AppDir, wasmPath) return buildErr }); err != nil { - return operationErrorFromCause("build failed", err) + return operationErrorFromCause(err) } fmt.Println(built) } @@ -493,7 +499,7 @@ func buildOnce(options cliOptions, request buildRequest, timings *buildTimingRec app, appErr = appgen.GenerateBackendWithPlan(backendAppDir, appPlan) return appErr }); err != nil { - return operationErrorFromCause("build failed", err) + return operationErrorFromCause(err) } fmt.Println(app.ModulePath) fmt.Println(app.PackagePath) @@ -505,7 +511,7 @@ func buildOnce(options cliOptions, request buildRequest, timings *buildTimingRec built, buildErr = appgen.BuildBinary(app.AppDir, backendBinaryPath) return buildErr }); err != nil { - return operationErrorFromCause("build failed", err) + return operationErrorFromCause(err) } fmt.Println(built) } @@ -517,7 +523,7 @@ func buildOnce(options cliOptions, request buildRequest, timings *buildTimingRec app, appErr = appgen.GenerateContractWorker(workerAppDir, contractReport, request.Worker) return appErr }); err != nil { - return operationErrorFromCause("build failed", err) + return operationErrorFromCause(err) } fmt.Println(app.ModulePath) fmt.Println(app.PackagePath) @@ -530,7 +536,7 @@ func buildOnce(options cliOptions, request buildRequest, timings *buildTimingRec built, buildErr = appgen.BuildWorkerBinary(app.AppDir, workerBinaryPath) return buildErr }); err != nil { - return operationErrorFromCause("build failed", err) + return operationErrorFromCause(err) } fmt.Println(built) buildReportEvents = append(buildReportEvents, buildgen.BuildEvent{ @@ -552,7 +558,7 @@ func buildOnce(options cliOptions, request buildRequest, timings *buildTimingRec app, appErr = appgen.GenerateContractCron(cronAppDir, contractReport, request.Cron) return appErr }); err != nil { - return operationErrorFromCause("build failed", err) + return operationErrorFromCause(err) } fmt.Println(app.ModulePath) fmt.Println(app.PackagePath) @@ -565,7 +571,7 @@ func buildOnce(options cliOptions, request buildRequest, timings *buildTimingRec built, buildErr = appgen.BuildCronBinary(app.AppDir, cronBinaryPath) return buildErr }); err != nil { - return operationErrorFromCause("build failed", err) + return operationErrorFromCause(err) } fmt.Println(built) buildReportEvents = append(buildReportEvents, buildgen.BuildEvent{ @@ -594,7 +600,7 @@ func buildOnce(options cliOptions, request buildRequest, timings *buildTimingRec }) return recipeErr }); err != nil { - return operationErrorFromCause("build failed", err) + return operationErrorFromCause(err) } for _, artifact := range recipeArtifacts { fmt.Println(artifact.Path) @@ -602,10 +608,10 @@ func buildOnce(options cliOptions, request buildRequest, timings *buildTimingRec buildReportEvents = append(buildReportEvents, deploymentRecipeBuildEvents(recipeArtifacts)...) } if err := appendBuildReportEvents(result.BuildReportPath, buildReportEvents...); err != nil { - return operationErrorFromCause("build failed", err) + return operationErrorFromCause(err) } - if _, err := timings.write(outputDir, request.TimingsPath); err != nil { - return operationErrorFromCause("build failed", err) + if err := timings.write(outputDir, request.TimingsPath); err != nil { + return operationErrorFromCause(err) } return nil } @@ -832,7 +838,7 @@ func parseBuildOptions(args []string) (buildOptions, error) { arg := args[i] if value, next, ok, missing := consumeValueFlag(args, i, "--out", false); ok { if missing { - return buildOptions{}, fmt.Errorf(buildUsage) + return buildOptions{}, errors.New(buildUsage) } plan.OutputDir = value i = next @@ -840,7 +846,7 @@ func parseBuildOptions(args []string) (buildOptions, error) { } if value, next, ok, missing := consumeValueFlag(args, i, "--app", false); ok { if missing { - return buildOptions{}, fmt.Errorf(buildUsage) + return buildOptions{}, errors.New(buildUsage) } plan.AppDir = value if strings.TrimSpace(plan.AppDir) == "" { @@ -851,7 +857,7 @@ func parseBuildOptions(args []string) (buildOptions, error) { } if value, next, ok, missing := consumeValueFlag(args, i, "--bin", false); ok { if missing { - return buildOptions{}, fmt.Errorf(buildUsage) + return buildOptions{}, errors.New(buildUsage) } plan.BinaryPath = value if strings.TrimSpace(plan.BinaryPath) == "" { @@ -862,18 +868,18 @@ func parseBuildOptions(args []string) (buildOptions, error) { } if value, next, ok, missing := consumeValueFlag(args, i, "--docker-base", true); ok { if missing { - return buildOptions{}, fmt.Errorf(buildUsage) + return buildOptions{}, errors.New(buildUsage) } plan.DockerBase = value if strings.TrimSpace(plan.DockerBase) == "" { - return buildOptions{}, fmt.Errorf("Docker base is required") + return buildOptions{}, fmt.Errorf("docker base is required") } i = next continue } if value, next, ok, missing := consumeValueFlag(args, i, "--deploy-recipe", true); ok { if missing { - return buildOptions{}, fmt.Errorf(buildUsage) + return buildOptions{}, errors.New(buildUsage) } plan.DeployRecipes = appendNames(plan.DeployRecipes, value) i = next @@ -881,7 +887,7 @@ func parseBuildOptions(args []string) (buildOptions, error) { } if value, next, ok, missing := consumeValueFlag(args, i, "--wasm", false); ok { if missing { - return buildOptions{}, fmt.Errorf(buildUsage) + return buildOptions{}, errors.New(buildUsage) } plan.WASMPath = value if strings.TrimSpace(plan.WASMPath) == "" { @@ -892,7 +898,7 @@ func parseBuildOptions(args []string) (buildOptions, error) { } if value, next, ok, missing := consumeValueFlag(args, i, "--backend-app", false); ok { if missing { - return buildOptions{}, fmt.Errorf(buildUsage) + return buildOptions{}, errors.New(buildUsage) } plan.BackendAppDir = value if strings.TrimSpace(plan.BackendAppDir) == "" { @@ -903,7 +909,7 @@ func parseBuildOptions(args []string) (buildOptions, error) { } if value, next, ok, missing := consumeValueFlag(args, i, "--backend-bin", false); ok { if missing { - return buildOptions{}, fmt.Errorf(buildUsage) + return buildOptions{}, errors.New(buildUsage) } plan.BackendBinaryPath = value if strings.TrimSpace(plan.BackendBinaryPath) == "" { @@ -914,7 +920,7 @@ func parseBuildOptions(args []string) (buildOptions, error) { } if value, next, ok, missing := consumeValueFlag(args, i, "--worker-app", false); ok { if missing { - return buildOptions{}, fmt.Errorf(buildUsage) + return buildOptions{}, errors.New(buildUsage) } plan.WorkerAppDir = value if strings.TrimSpace(plan.WorkerAppDir) == "" { @@ -925,7 +931,7 @@ func parseBuildOptions(args []string) (buildOptions, error) { } if value, next, ok, missing := consumeValueFlag(args, i, "--worker-bin", false); ok { if missing { - return buildOptions{}, fmt.Errorf(buildUsage) + return buildOptions{}, errors.New(buildUsage) } plan.WorkerBinaryPath = value if strings.TrimSpace(plan.WorkerBinaryPath) == "" { @@ -936,7 +942,7 @@ func parseBuildOptions(args []string) (buildOptions, error) { } if value, next, ok, missing := consumeValueFlag(args, i, "--cron-app", false); ok { if missing { - return buildOptions{}, fmt.Errorf(buildUsage) + return buildOptions{}, errors.New(buildUsage) } plan.CronAppDir = value if strings.TrimSpace(plan.CronAppDir) == "" { @@ -947,7 +953,7 @@ func parseBuildOptions(args []string) (buildOptions, error) { } if value, next, ok, missing := consumeValueFlag(args, i, "--cron-bin", false); ok { if missing { - return buildOptions{}, fmt.Errorf(buildUsage) + return buildOptions{}, errors.New(buildUsage) } plan.CronBinaryPath = value if strings.TrimSpace(plan.CronBinaryPath) == "" { @@ -958,7 +964,7 @@ func parseBuildOptions(args []string) (buildOptions, error) { } if value, next, ok, missing := consumeValueFlag(args, i, "--config", false); ok { if missing { - return buildOptions{}, fmt.Errorf(buildUsage) + return buildOptions{}, errors.New(buildUsage) } plan.ConfigPath = value i = next @@ -966,7 +972,7 @@ func parseBuildOptions(args []string) (buildOptions, error) { } if value, next, ok, missing := consumeValueFlag(args, i, "--project-root", false); ok { if missing { - return buildOptions{}, fmt.Errorf(buildUsage) + return buildOptions{}, errors.New(buildUsage) } plan.Options.ProjectRoot = value i = next @@ -974,7 +980,7 @@ func parseBuildOptions(args []string) (buildOptions, error) { } if value, next, ok, missing := consumeValueFlag(args, i, "--env-file", false); ok { if missing { - return buildOptions{}, fmt.Errorf(buildUsage) + return buildOptions{}, errors.New(buildUsage) } plan.Options.EnvFilePath = value i = next @@ -982,7 +988,7 @@ func parseBuildOptions(args []string) (buildOptions, error) { } if value, next, ok, missing := consumeValueFlag(args, i, "--target", false); ok { if missing { - return buildOptions{}, fmt.Errorf(buildUsage) + return buildOptions{}, errors.New(buildUsage) } plan.TargetNames = appendNames(plan.TargetNames, value) i = next @@ -990,7 +996,7 @@ func parseBuildOptions(args []string) (buildOptions, error) { } if value, next, ok, missing := consumeValueFlag(args, i, "--module", false); ok { if missing { - return buildOptions{}, fmt.Errorf(buildUsage) + return buildOptions{}, errors.New(buildUsage) } plan.ModuleNames = appendNames(plan.ModuleNames, value) i = next diff --git a/internal/gowdkcmd/build_audit.go b/internal/gowdkcmd/build_audit.go index 4f8db73c..9478700b 100644 --- a/internal/gowdkcmd/build_audit.go +++ b/internal/gowdkcmd/build_audit.go @@ -2,6 +2,7 @@ package gowdkcmd import ( "bytes" + "errors" "fmt" "io" "os" @@ -253,12 +254,15 @@ func classifyFinalArtifact(path string) (finalArtifactScanDecision, error) { if err != nil { return finalArtifactScanDecision{}, err } - defer file.Close() header := make([]byte, 512) n, err := file.Read(header) - if err != nil && err != io.EOF { + closeErr := file.Close() + if err != nil && !errors.Is(err, io.EOF) { return finalArtifactScanDecision{}, err } + if closeErr != nil { + return finalArtifactScanDecision{}, closeErr + } return finalArtifactScanDecision{ scanText: finalArtifactHeaderIsText(header[:n]), size: info.Size(), @@ -294,13 +298,16 @@ func readFinalArtifactText(path string, size int64) (string, error) { if err != nil { return "", err } - defer file.Close() limit := size if limit > finalArtifactMaxTextBytes { limit = finalArtifactMaxTextBytes } payload, err := io.ReadAll(io.LimitReader(file, limit)) if err != nil { + _ = file.Close() + return "", err + } + if err := file.Close(); err != nil { return "", err } return string(payload), nil diff --git a/internal/gowdkcmd/build_timings.go b/internal/gowdkcmd/build_timings.go index 9b55fbbe..d5a1a5bc 100644 --- a/internal/gowdkcmd/build_timings.go +++ b/internal/gowdkcmd/build_timings.go @@ -77,9 +77,9 @@ func (recorder *buildTimingRecorder) counter(name string, value int) { recorder.counters[name] += value } -func (recorder *buildTimingRecorder) write(outputDir string, explicitPath string) (string, error) { +func (recorder *buildTimingRecorder) write(outputDir string, explicitPath string) error { if recorder == nil || !recorder.enabled { - return "", nil + return nil } path := strings.TrimSpace(explicitPath) if path == "" { @@ -94,16 +94,16 @@ func (recorder *buildTimingRecorder) write(outputDir string, explicitPath string } payload, err := json.MarshalIndent(report, "", " ") if err != nil { - return "", err + return err } payload = append(payload, '\n') if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { - return "", err + return err } if err := os.WriteFile(path, payload, 0o644); err != nil { - return "", err + return err } - return path, nil + return nil } func sortedTimingCounters(counters map[string]int) map[string]int { diff --git a/internal/gowdkcmd/clean.go b/internal/gowdkcmd/clean.go index 69c8a2fc..eb25fe4c 100644 --- a/internal/gowdkcmd/clean.go +++ b/internal/gowdkcmd/clean.go @@ -60,10 +60,10 @@ func clean(args []string) error { i = next continue } - switch { - case arg == "--dry-run": + switch arg { + case "--dry-run": dryRun = true - case arg == "--json": + case "--json": jsonOutput = true default: return fmt.Errorf("unknown clean flag %q\n%s", arg, cleanUsage) diff --git a/internal/gowdkcmd/config_helper.go b/internal/gowdkcmd/config_helper.go index a9917fda..051d2eee 100644 --- a/internal/gowdkcmd/config_helper.go +++ b/internal/gowdkcmd/config_helper.go @@ -5,6 +5,7 @@ import ( "crypto/sha256" "encoding/hex" "encoding/json" + "errors" "fmt" goformat "go/format" "os" @@ -65,7 +66,8 @@ func runProjectHelperIfNeeded(args []string) (bool, error) { "GOWDK_CLI_VERSION="+version, ) if err := cmd.Run(); err != nil { - if exitErr, ok := err.(*exec.ExitError); ok { + exitErr := &exec.ExitError{} + if errors.As(err, &exitErr) { return true, helperExitError{code: exitErr.ExitCode()} } return true, err @@ -81,7 +83,7 @@ func normalizeProjectHelperArgs(args []string) ([]string, error) { out := append([]string(nil), args...) switch out[0] { case "build", "check": - return normalizeProjectCommandPathArgs(out, cwd, 1), nil + return normalizeProjectCommandPathArgs(out, cwd), nil case "dev": return normalizeDevHelperArgs(out, cwd), nil default: @@ -101,16 +103,16 @@ func normalizeDevHelperArgs(args []string, cwd string) []string { continue } buildArgs := append([]string{"build"}, out[index:]...) - buildArgs = normalizeProjectCommandPathArgs(buildArgs, cwd, 1) + buildArgs = normalizeProjectCommandPathArgs(buildArgs, cwd) copy(out[index:], buildArgs[1:]) return out } return out } -func normalizeProjectCommandPathArgs(args []string, cwd string, start int) []string { +func normalizeProjectCommandPathArgs(args []string, cwd string) []string { out := append([]string(nil), args...) - for index := start; index < len(out); index++ { + for index := 1; index < len(out); index++ { if next, ok := normalizeHelperValueFlag(out, index, cwd, "--config"); ok { index = next continue @@ -280,7 +282,8 @@ func (err helperUnavailableError) Unwrap() error { } func helperUnavailable(err error) bool { - _, ok := err.(helperUnavailableError) + var helperUnavailableError helperUnavailableError + ok := errors.As(err, &helperUnavailableError) return ok } @@ -293,10 +296,7 @@ func ensureProjectHelper(configPath string) (helperPath string, projectRoot stri if err != nil { return "", "", err } - key, err := projectHelperCacheKey(packageInfo, configPath, source) - if err != nil { - return "", "", err - } + key := projectHelperCacheKey(packageInfo, configPath, source) cacheDir := filepath.Join(packageInfo.Module.Dir, ".gowdk", "helper", key) if err := os.MkdirAll(cacheDir, 0o755); err != nil { return "", "", err @@ -422,7 +422,7 @@ func main() { return string(formatted), nil } -func projectHelperCacheKey(packageInfo helperPackage, configPath string, source string) (string, error) { +func projectHelperCacheKey(packageInfo helperPackage, configPath string, source string) string { hash := sha256.New() writeHash := func(value string) { _, _ = hash.Write([]byte(value)) @@ -446,7 +446,7 @@ func projectHelperCacheKey(packageInfo helperPackage, configPath string, source _, _ = hash.Write([]byte{0}) } sum := hash.Sum(nil) - return hex.EncodeToString(sum[:12]), nil + return hex.EncodeToString(sum[:12]) } func helperGoVersion(dir string) (string, error) { diff --git a/internal/gowdkcmd/config_helper_test.go b/internal/gowdkcmd/config_helper_test.go index 293ae388..d4c11bb2 100644 --- a/internal/gowdkcmd/config_helper_test.go +++ b/internal/gowdkcmd/config_helper_test.go @@ -15,7 +15,7 @@ func TestNormalizeProjectCommandPathArgsAbsolutizesExistingInputs(t *testing.T) pagePath := writeHelperArgTestFile(t, root, pageRel) args := []string{"build", "--config", configRel, "--out", "dist", pageRel} - got := normalizeProjectCommandPathArgs(args, root, 1) + got := normalizeProjectCommandPathArgs(args, root) want := []string{"build", "--config", configPath, "--out", "dist", pagePath} if !reflect.DeepEqual(got, want) { t.Fatalf("normalizeProjectCommandPathArgs() = %#v, want %#v", got, want) @@ -33,7 +33,7 @@ func TestNormalizeProjectCommandPathArgsHandlesEqualsAndBooleanFlags(t *testing. pagePath := writeHelperArgTestFile(t, root, pageRel) args := []string{"build", "--config=" + configRel, "--timings", pageRel} - got := normalizeProjectCommandPathArgs(args, root, 1) + got := normalizeProjectCommandPathArgs(args, root) want := []string{"build", "--config=" + configPath, "--timings", pagePath} if !reflect.DeepEqual(got, want) { t.Fatalf("normalizeProjectCommandPathArgs() = %#v, want %#v", got, want) diff --git a/internal/gowdkcmd/dev.go b/internal/gowdkcmd/dev.go index 4aaa48e9..7047bd03 100644 --- a/internal/gowdkcmd/dev.go +++ b/internal/gowdkcmd/dev.go @@ -295,7 +295,7 @@ func (serve *devServeState) useRuntime(runtime devRuntime) error { server, err := startDevRuntimeProxy(serve.addr, targetAddr, serve.reload) if err != nil { if listener != nil { - listener.Close() + _ = listener.Close() } return err } @@ -483,7 +483,9 @@ func (process *devRuntimeProcess) restart() error { if err != nil { return fmt.Errorf("share dev runtime listener: %w", err) } - defer file.Close() + defer func() { + _ = file.Close() + }() command.ExtraFiles = []*os.File{file} command.Env = append(command.Env, fmt.Sprintf("%s=%d", gowdkListenerFDEnv, devRuntimeListenerFD)) } @@ -604,10 +606,7 @@ func parseDevOptions(args []string) (devOptions, error) { i = next continue } - switch { - default: - options.BuildArgs = append(options.BuildArgs, arg) - } + options.BuildArgs = append(options.BuildArgs, arg) } if strings.TrimSpace(options.Addr) == "" { return devOptions{}, fmt.Errorf("dev address is required") diff --git a/internal/gowdkcmd/dev_listener_unix_test.go b/internal/gowdkcmd/dev_listener_unix_test.go index 84ab4c22..78c57161 100644 --- a/internal/gowdkcmd/dev_listener_unix_test.go +++ b/internal/gowdkcmd/dev_listener_unix_test.go @@ -57,7 +57,7 @@ func main() { // The reserved port stays bound for the whole session, so an independent // bind on it must fail — there is no window in which it is free. if listener, err := net.Listen("tcp", serve.process.addr); err == nil { - listener.Close() + _ = listener.Close() t.Fatalf("expected reserved port %q to stay held by the dev session", serve.process.addr) } } diff --git a/internal/gowdkcmd/dev_loop.go b/internal/gowdkcmd/dev_loop.go index b101609b..83e89c35 100644 --- a/internal/gowdkcmd/dev_loop.go +++ b/internal/gowdkcmd/dev_loop.go @@ -62,7 +62,7 @@ func buildIncrementalSPALoaded(plan buildOptions, change inputChange) (bool, err outputDir = options.Config.Build.Output } if outputDir == "" { - return true, fmt.Errorf(buildUsage) + return true, errors.New(buildUsage) } options.Config.Build.Output = outputDir if len(paths) == 0 { @@ -79,10 +79,12 @@ func buildIncrementalSPALoaded(plan buildOptions, change inputChange) (bool, err timings.counter("incremental_input_changes", len(change.Changed)) var app gwdkanalysis.Sources var diagnostics lang.Diagnostics - timings.measure("parse_lower", func() error { + if err := timings.measure("parse_lower", func() error { app, diagnostics = lang.ParseBuildFiles(paths) return nil - }) + }); err != nil { + return true, err + } for _, diagnostic := range diagnostics { fmt.Fprintln(os.Stderr, diagnostic.String()) } @@ -164,7 +166,7 @@ func buildIncrementalSPALoaded(plan buildOptions, change inputChange) (bool, err fmt.Println(result.BuildReportPath) } printBuildgenBuildReport(result.Report, options.Debug) - if _, err := timings.write(outputDir, plan.TimingsPath); err != nil { + if err := timings.write(outputDir, plan.TimingsPath); err != nil { return true, err } return true, nil @@ -887,7 +889,8 @@ func buildInputPaths(plan buildOptions) (devInputPaths, error) { outputDir := plan.OutputDir paths := append([]string(nil), plan.Paths...) inputs := devInputPaths{} - if plan.shouldBuildConfiguredTargets() { + switch { + case plan.shouldBuildConfiguredTargets(): targets, err := selectBuildTargets(options.Config.Build.Targets, plan.TargetNames) if err != nil { return devInputPaths{}, err @@ -906,7 +909,7 @@ func buildInputPaths(plan buildOptions) (devInputPaths, error) { inputs.addFiles(css...) inputs.addDirs(cssDirs...) } - } else if outputDir == "" { + case outputDir == "": outputDir = options.Config.Build.Output if len(paths) == 0 { discovered, dirs, err := discoverBuildFilesAndDirs(options.Config, outputDir, plan.ModuleNames, options.ProjectRoot) @@ -925,7 +928,7 @@ func buildInputPaths(plan buildOptions) (devInputPaths, error) { } inputs.addFiles(css...) inputs.addDirs(cssDirs...) - } else if len(paths) == 0 { + case len(paths) == 0: discovered, dirs, err := discoverBuildFilesAndDirs(options.Config, outputDir, plan.ModuleNames, options.ProjectRoot) if err != nil { return devInputPaths{}, err @@ -938,7 +941,7 @@ func buildInputPaths(plan buildOptions) (devInputPaths, error) { } inputs.addFiles(css...) inputs.addDirs(cssDirs...) - } else { + default: inputs.addFiles(paths...) inputs.addParentDirs(paths...) css, cssDirs, err := discoverBuildCSSFilesAndDirs(options.Config, outputDir, options.ProjectRoot) diff --git a/internal/gowdkcmd/dev_proxy_response_test.go b/internal/gowdkcmd/dev_proxy_response_test.go index 496483a5..3d1d8392 100644 --- a/internal/gowdkcmd/dev_proxy_response_test.go +++ b/internal/gowdkcmd/dev_proxy_response_test.go @@ -157,11 +157,15 @@ func TestModifyDevRuntimeProxyResponsePropagatesReadAndCloseErrors(t *testing.T) if original.closes != 1 { t.Fatalf("expected errored body to close once, got %d", original.closes) } + _ = response.Body.Close() } func TestModifyDevRuntimeProxyResponseKeepsRuntimeErrorEventForOversizedHTML(t *testing.T) { original := &trackingReadCloser{reader: strings.NewReader("unread")} response := devProxyHTMLResponse(http.StatusInternalServerError, maxDevProxyHTMLBytes+1, original) + defer func() { + _ = response.Body.Close() + }() broker := newLiveReloadBroker() events := make(chan liveReloadEvent, 2) broker.clients[events] = true diff --git a/internal/gowdkcmd/docker.go b/internal/gowdkcmd/docker.go index c817ef4c..39ed6158 100644 --- a/internal/gowdkcmd/docker.go +++ b/internal/gowdkcmd/docker.go @@ -95,7 +95,9 @@ func inspectDockerBinary(path string) (dockerBinaryInfo, error) { } return dockerBinaryInfo{}, fmt.Errorf("gowdk build --docker requires a Linux ELF binary; set GOOS=linux GOARCH= when building") } - defer file.Close() + defer func() { + _ = file.Close() + }() info := dockerBinaryInfo{ELF: true, Static: true} for _, program := range file.Progs { diff --git a/internal/gowdkcmd/fix.go b/internal/gowdkcmd/fix.go index 62ea7aed..71d58577 100644 --- a/internal/gowdkcmd/fix.go +++ b/internal/gowdkcmd/fix.go @@ -1,6 +1,7 @@ package gowdkcmd import ( + "errors" "fmt" "os" "sort" @@ -60,7 +61,7 @@ func parseFixOptions(args []string) (fixOptions, error) { case arg == "--code": i++ if i >= len(args) { - return fixOptions{}, fmt.Errorf(fixUsage) + return fixOptions{}, errors.New(fixUsage) } options.Code = args[i] case strings.HasPrefix(arg, "--code="): diff --git a/internal/gowdkcmd/generate.go b/internal/gowdkcmd/generate.go index 46ee10ef..723ae7e1 100644 --- a/internal/gowdkcmd/generate.go +++ b/internal/gowdkcmd/generate.go @@ -2,6 +2,7 @@ package gowdkcmd import ( "bytes" + "errors" "fmt" "go/ast" goformat "go/format" @@ -23,7 +24,7 @@ const generateUsage = "usage: gowdk generate stubs [--config ] [--project- func generate(args []string) error { if len(args) == 0 { - return fmt.Errorf(generateUsage) + return errors.New(generateUsage) } switch args[0] { case "stubs": diff --git a/internal/gowdkcmd/init.go b/internal/gowdkcmd/init.go index 8c46e4b5..2d382076 100644 --- a/internal/gowdkcmd/init.go +++ b/internal/gowdkcmd/init.go @@ -2,6 +2,7 @@ package gowdkcmd import ( "bytes" + "errors" "fmt" "go/ast" goformat "go/format" @@ -322,22 +323,22 @@ func assertGET(t *testing.T, target string, status int, contains string) { func initConfigExpr() ast.Expr { return &ast.CompositeLit{ - Type: initSel("gowdk", "Config"), + Type: initSel("Config"), Elts: []ast.Expr{ initKeyValue("AppName", initStringLit("GOWDK App")), initKeyValue("Source", &ast.CompositeLit{ - Type: initSel("gowdk", "SourceConfig"), + Type: initSel("SourceConfig"), Elts: []ast.Expr{ initKeyValue("Include", initStringSlice("src/**/*.gwdk")), }, }), initKeyValue("Build", &ast.CompositeLit{ - Type: initSel("gowdk", "BuildConfig"), + Type: initSel("BuildConfig"), Elts: []ast.Expr{ initKeyValue("Targets", &ast.CompositeLit{ - Type: &ast.ArrayType{Elt: initSel("gowdk", "BuildTargetConfig")}, + Type: &ast.ArrayType{Elt: initSel("BuildTargetConfig")}, Elts: []ast.Expr{&ast.CompositeLit{ - Type: initSel("gowdk", "BuildTargetConfig"), + Type: initSel("BuildTargetConfig"), Elts: []ast.Expr{ initKeyValue("Name", initStringLit("site")), initKeyValue("App", initStringLit(".gowdk/site")), @@ -348,7 +349,7 @@ func initConfigExpr() ast.Expr { }, }), initKeyValue("CSS", &ast.CompositeLit{ - Type: initSel("gowdk", "CSSConfig"), + Type: initSel("CSSConfig"), Elts: []ast.Expr{ initKeyValue("Include", initStringSlice("styles/**/*.css")), initKeyValue("Default", initStringSlice("global")), @@ -370,8 +371,8 @@ func initKeyValue(key string, value ast.Expr) ast.Expr { return &ast.KeyValueExpr{Key: ast.NewIdent(key), Value: value} } -func initSel(pkg string, name string) ast.Expr { - return &ast.SelectorExpr{X: ast.NewIdent(pkg), Sel: ast.NewIdent(name)} +func initSel(name string) ast.Expr { + return &ast.SelectorExpr{X: ast.NewIdent("gowdk"), Sel: ast.NewIdent(name)} } func initStringLit(value string) *ast.BasicLit { @@ -390,11 +391,11 @@ func parseInitOptions(args []string) (initOptions, error) { case "--template": index++ if index >= len(args) { - return initOptions{}, fmt.Errorf(initUsage) + return initOptions{}, errors.New(initUsage) } options.Template = args[index] case "-h", "--help": - return initOptions{}, fmt.Errorf(initUsage) + return initOptions{}, errors.New(initUsage) default: if strings.HasPrefix(arg, "--template=") { options.Template = strings.TrimPrefix(arg, "--template=") @@ -404,7 +405,7 @@ func parseInitOptions(args []string) (initOptions, error) { return initOptions{}, fmt.Errorf("unknown init flag %q", arg) } if options.Dir != "." { - return initOptions{}, fmt.Errorf(initUsage) + return initOptions{}, errors.New(initUsage) } options.Dir = arg } diff --git a/internal/gowdkcmd/inspect.go b/internal/gowdkcmd/inspect.go index f8a6565b..1212b8fb 100644 --- a/internal/gowdkcmd/inspect.go +++ b/internal/gowdkcmd/inspect.go @@ -2,6 +2,7 @@ package gowdkcmd import ( "encoding/json" + "errors" "fmt" "os" @@ -15,7 +16,7 @@ const inspectUsage = "usage: gowdk inspect ir|tree|endpoint-graph|asset-graph|go func inspect(args []string) error { if len(args) == 0 { - return fmt.Errorf(inspectUsage) + return errors.New(inspectUsage) } switch args[0] { case "ir": diff --git a/internal/gowdkcmd/lsp.go b/internal/gowdkcmd/lsp.go index be2406cf..14efcb08 100644 --- a/internal/gowdkcmd/lsp.go +++ b/internal/gowdkcmd/lsp.go @@ -45,8 +45,8 @@ func languageServerConfig(args []string) (gowdk.Config, error) { i = next continue } - switch { - case arg == "--ssr": + switch arg { + case "--ssr": options.Config.Addons = append(options.Config.Addons, ssr.Addon()) default: return gowdk.Config{}, errors.New(lspUsage) diff --git a/internal/gowdkcmd/main_test.go b/internal/gowdkcmd/main_test.go index eb0e700b..a5846845 100644 --- a/internal/gowdkcmd/main_test.go +++ b/internal/gowdkcmd/main_test.go @@ -1511,7 +1511,7 @@ func TestAddCommandListsRegistryJSON(t *testing.T) { func TestAddonRegistryListDistinguishesDiscoveryCategories(t *testing.T) { var builder strings.Builder - writeAddonRegistryList(&builder, []addonregistry.Entry{ + if err := writeAddonRegistryList(&builder, []addonregistry.Entry{ { Name: "builtin", Kind: "built-in", @@ -1534,7 +1534,9 @@ func TestAddonRegistryListDistinguishesDiscoveryCategories(t *testing.T) { Compatibility: "incompatible", Summary: "old addon", }, - }) + }); err != nil { + t.Fatal(err) + } output := builder.String() for _, expected := range []string{ "built-in", @@ -2066,18 +2068,24 @@ func TestDevServeStateRebindsStaticOutputDir(t *testing.T) { serve := newDevServeState("127.0.0.1:0") t.Cleanup(serve.close) - serve.useStatic(firstAbs) + if err := serve.useStatic(firstAbs); err != nil { + t.Fatal(err) + } firstServer := serve.server if firstServer == nil || serve.staticDir != firstAbs { t.Fatalf("expected initial static server for %q, got server=%v dir=%q", firstAbs, firstServer, serve.staticDir) } - serve.useStatic(firstAbs) + if err := serve.useStatic(firstAbs); err != nil { + t.Fatal(err) + } if serve.server != firstServer { t.Fatal("expected unchanged static output dir to keep the existing server") } - serve.useStatic(secondAbs) + if err := serve.useStatic(secondAbs); err != nil { + t.Fatal(err) + } if serve.server == nil || serve.staticDir != secondAbs { t.Fatalf("expected rebound static server for %q, got server=%v dir=%q", secondAbs, serve.server, serve.staticDir) } @@ -2109,7 +2117,9 @@ func main() { serve := newDevServeState("127.0.0.1:0") t.Cleanup(serve.close) - serve.useStatic(staticDir) + if err := serve.useStatic(staticDir); err != nil { + t.Fatal(err) + } if serve.server == nil { t.Fatal("expected static server before runtime transition") } @@ -2249,7 +2259,9 @@ func TestPlaygroundExportArchivesSourceProjectOnly(t *testing.T) { if err != nil { t.Fatal(err) } - defer reader.Close() + defer func() { + _ = reader.Close() + }() var names []string for _, file := range reader.File { names = append(names, file.Name) diff --git a/internal/gowdkcmd/operation_error.go b/internal/gowdkcmd/operation_error.go index 000cb76f..676ef1cd 100644 --- a/internal/gowdkcmd/operation_error.go +++ b/internal/gowdkcmd/operation_error.go @@ -92,7 +92,8 @@ func operationErrorFromCompiler(summary string, diagnostics compiler.ValidationE } } -func operationErrorFromCause(summary string, cause error) error { +func operationErrorFromCause(cause error) error { + const summary = "build failed" if cause == nil { return &OperationError{Summary: summary} } diff --git a/internal/gowdkcmd/playground.go b/internal/gowdkcmd/playground.go index 1400f92c..757e3026 100644 --- a/internal/gowdkcmd/playground.go +++ b/internal/gowdkcmd/playground.go @@ -7,7 +7,6 @@ import ( "os" "os/exec" "path/filepath" - "runtime" "strings" "github.com/cssbruno/gowdk/internal/playground" @@ -22,7 +21,7 @@ const sandboxBuildSubcommand = "__sandbox-build" func playgroundCommand(args []string) error { if len(args) == 0 { - return fmt.Errorf(playgroundUsage) + return errors.New(playgroundUsage) } switch args[0] { case "policy": @@ -34,7 +33,7 @@ func playgroundCommand(args []string) error { case sandboxBuildSubcommand: return playgroundSandboxBuild(args[1:]) default: - return fmt.Errorf(playgroundUsage) + return errors.New(playgroundUsage) } } @@ -45,7 +44,7 @@ func playgroundPolicy(args []string) error { case "--json": jsonOutput = true default: - return fmt.Errorf(playgroundUsage) + return errors.New(playgroundUsage) } } policy := playground.DefaultPolicy() @@ -72,7 +71,7 @@ func playgroundExport(args []string) error { return err } if strings.TrimSpace(options.Output) == "" { - return fmt.Errorf(playgroundUsage) + return errors.New(playgroundUsage) } result, err := playground.ExportArchive(options.Dir, options.Output, playground.Options{}) if err != nil { @@ -105,7 +104,7 @@ func playgroundRun(args []string) error { return fmt.Errorf("hosted playground execution is disabled by default; pass --allow-hosted-execution only inside the documented sandbox") } if strings.TrimSpace(options.Output) == "" { - return fmt.Errorf(playgroundUsage) + return errors.New(playgroundUsage) } // Fail closed: hosted execution only runs inside the OS-level sandbox. If @@ -118,7 +117,9 @@ func playgroundRun(args []string) error { if err != nil { return err } - defer cleanup() + defer func() { + _ = cleanup() + }() outputDir, err := filepath.Abs(options.Output) if err != nil { @@ -219,13 +220,14 @@ func playgroundSandboxBuild(args []string) error { // resolveGoRoot returns the GOROOT to expose read-only inside the sandbox. // -// It comes from runtime.GOROOT(), which is baked into this binary at build time -// and so cannot be redirected by an attacker-controlled PATH; the toolchain is -// then addressed by absolute path rather than a PATH lookup, since this runs -// before any namespace/pivot confinement and a hosted wrapper may have an -// attacker-writable directory on PATH. +// It comes from `go env GOROOT`, then the toolchain is addressed by absolute +// path rather than a PATH lookup inside the sandbox. func resolveGoRoot() (string, error) { - goRoot := strings.TrimSpace(runtime.GOROOT()) + output, err := exec.Command("go", "env", "GOROOT").Output() + if err != nil { + return "", fmt.Errorf("resolve GOROOT: %w", err) + } + goRoot := strings.TrimSpace(string(output)) if goRoot == "" { return "", fmt.Errorf("could not resolve GOROOT for the sandbox toolchain") } @@ -310,7 +312,7 @@ func parsePlaygroundFileOptions(args []string) (playgroundFileOptions, error) { arg := args[index] if value, next, ok, missing := consumeValueFlag(args, index, "--dir", true); ok { if missing { - return playgroundFileOptions{}, fmt.Errorf(playgroundUsage) + return playgroundFileOptions{}, errors.New(playgroundUsage) } options.Dir = value index = next @@ -318,7 +320,7 @@ func parsePlaygroundFileOptions(args []string) (playgroundFileOptions, error) { } if value, next, ok, missing := consumeValueFlag(args, index, "--out", true); ok { if missing { - return playgroundFileOptions{}, fmt.Errorf(playgroundUsage) + return playgroundFileOptions{}, errors.New(playgroundUsage) } options.Output = value index = next @@ -326,21 +328,21 @@ func parsePlaygroundFileOptions(args []string) (playgroundFileOptions, error) { } if value, next, ok, missing := consumeValueFlag(args, index, "--module-cache", true); ok { if missing { - return playgroundFileOptions{}, fmt.Errorf(playgroundUsage) + return playgroundFileOptions{}, errors.New(playgroundUsage) } options.ModuleCache = value index = next continue } - switch { - case arg == "--json": + switch arg { + case "--json": options.JSON = true - case arg == "--allow-hosted-execution": + case "--allow-hosted-execution": options.AllowExecution = true - case arg == "--allow-shared-module-cache": + case "--allow-shared-module-cache": options.AllowSharedModuleCache = true default: - return playgroundFileOptions{}, fmt.Errorf(playgroundUsage) + return playgroundFileOptions{}, errors.New(playgroundUsage) } } if strings.TrimSpace(options.Dir) == "" { diff --git a/internal/gowdkcmd/preview.go b/internal/gowdkcmd/preview.go index 65a613ea..deaa27e8 100644 --- a/internal/gowdkcmd/preview.go +++ b/internal/gowdkcmd/preview.go @@ -74,8 +74,8 @@ func parsePreviewOptions(args []string) (previewOptions, error) { i = next continue } - switch { - case arg == "--hot": + switch arg { + case "--hot": options.Hot = true default: options.BuildArgs = append(options.BuildArgs, arg) diff --git a/internal/gowdkcmd/serve.go b/internal/gowdkcmd/serve.go index 6a25d379..825a491b 100644 --- a/internal/gowdkcmd/serve.go +++ b/internal/gowdkcmd/serve.go @@ -103,7 +103,9 @@ func outputFileHandler(root string) http.Handler { http.NotFound(w, request) return } - defer file.Close() + defer func() { + _ = file.Close() + }() http.ServeContent(w, request, info.Name(), info.ModTime(), file) }) } @@ -201,7 +203,9 @@ func liveReloadFileHandler(root string, reload *liveReloadBroker) http.Handler { files.ServeHTTP(w, request) return } - defer file.Close() + defer func() { + _ = file.Close() + }() payload, err := io.ReadAll(file) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) @@ -219,11 +223,12 @@ func devRuntimeProxyHandler(targetAddr string, reload *liveReloadBroker) http.Ha http.Error(w, err.Error(), http.StatusInternalServerError) }) } - proxy := httputil.NewSingleHostReverseProxy(target) - originalDirector := proxy.Director - proxy.Director = func(request *http.Request) { - originalDirector(request) - request.Header.Del("Accept-Encoding") + proxy := &httputil.ReverseProxy{ + Rewrite: func(proxyRequest *httputil.ProxyRequest) { + proxyRequest.SetURL(target) + proxyRequest.SetXForwarded() + proxyRequest.Out.Header.Del("Accept-Encoding") + }, } proxy.ModifyResponse = func(response *http.Response) error { return modifyDevRuntimeProxyResponse(response, reload) @@ -612,7 +617,7 @@ func (files *rootedOutputFiles) openPublicRegularFile(rel string) (*os.File, os. } info, err := file.Stat() if err != nil || !info.Mode().IsRegular() { - file.Close() + _ = file.Close() return nil, nil, false } return file, info, true diff --git a/internal/gowdkcmd/test.go b/internal/gowdkcmd/test.go index ff13c326..ce6da5bb 100644 --- a/internal/gowdkcmd/test.go +++ b/internal/gowdkcmd/test.go @@ -2,6 +2,7 @@ package gowdkcmd import ( "context" + "errors" "fmt" "io" "net" @@ -137,7 +138,7 @@ func parseTestOptions(args []string) (testOptions, error) { arg := args[index] if value, next, ok, missing := consumeValueFlag(args, index, "--config", true); ok { if missing { - return testOptions{}, fmt.Errorf(testUsage) + return testOptions{}, errors.New(testUsage) } options.ConfigPath = value index = next @@ -145,7 +146,7 @@ func parseTestOptions(args []string) (testOptions, error) { } if value, next, ok, missing := consumeValueFlag(args, index, "--env-file", true); ok { if missing { - return testOptions{}, fmt.Errorf(testUsage) + return testOptions{}, errors.New(testUsage) } options.EnvFilePath = value index = next @@ -153,7 +154,7 @@ func parseTestOptions(args []string) (testOptions, error) { } if value, next, ok, missing := consumeValueFlag(args, index, "--module", true); ok { if missing { - return testOptions{}, fmt.Errorf(testUsage) + return testOptions{}, errors.New(testUsage) } options.ModuleNames = appendModuleNames(options.ModuleNames, value) index = next @@ -161,7 +162,7 @@ func parseTestOptions(args []string) (testOptions, error) { } if value, next, ok, missing := consumeValueFlag(args, index, "--target", true); ok { if missing { - return testOptions{}, fmt.Errorf(testUsage) + return testOptions{}, errors.New(testUsage) } options.TargetNames = appendNames(options.TargetNames, value) index = next @@ -169,7 +170,7 @@ func parseTestOptions(args []string) (testOptions, error) { } if value, next, ok, missing := consumeValueFlag(args, index, "--stage", true); ok { if missing { - return testOptions{}, fmt.Errorf(testUsage) + return testOptions{}, errors.New(testUsage) } options.Stages = appendTestStages(options.Stages, value) index = next @@ -177,7 +178,7 @@ func parseTestOptions(args []string) (testOptions, error) { } if value, next, ok, missing := consumeValueFlag(args, index, "--run", true); ok { if missing { - return testOptions{}, fmt.Errorf(testUsage) + return testOptions{}, errors.New(testUsage) } options.RunPattern = value index = next @@ -185,7 +186,7 @@ func parseTestOptions(args []string) (testOptions, error) { } if value, next, ok, missing := consumeValueFlag(args, index, "--timeout", true); ok { if missing { - return testOptions{}, fmt.Errorf(testUsage) + return testOptions{}, errors.New(testUsage) } timeout, err := normalizeTestTimeout(value) if err != nil { @@ -197,7 +198,7 @@ func parseTestOptions(args []string) (testOptions, error) { } if value, next, ok, missing := consumeValueFlag(args, index, "--count", true); ok { if missing { - return testOptions{}, fmt.Errorf(testUsage) + return testOptions{}, errors.New(testUsage) } count, err := normalizeTestCount(value) if err != nil { @@ -209,7 +210,7 @@ func parseTestOptions(args []string) (testOptions, error) { } if value, next, ok, missing := consumeValueFlag(args, index, "--browser-command", true); ok { if missing { - return testOptions{}, fmt.Errorf(testUsage) + return testOptions{}, errors.New(testUsage) } options.BrowserCommand = value index = next @@ -217,7 +218,7 @@ func parseTestOptions(args []string) (testOptions, error) { } switch { case arg == "-h" || arg == "--help": - return testOptions{}, fmt.Errorf(testUsage) + return testOptions{}, errors.New(testUsage) case arg == "--ssr": options.SSR = true case arg == "--cover": diff --git a/internal/gwdkanalysis/ir_bindings.go b/internal/gwdkanalysis/ir_bindings.go index 1d6fe0f9..8e160524 100644 --- a/internal/gwdkanalysis/ir_bindings.go +++ b/internal/gwdkanalysis/ir_bindings.go @@ -18,9 +18,10 @@ func AttachBackendBindings(program *gwdkir.Program, bindings []source.BackendBin continue } kind := gwdkir.EndpointAction - if binding.Kind == "api" { + switch binding.Kind { + case "api": kind = gwdkir.EndpointAPI - } else if binding.Kind == "fragment" { + case "fragment": kind = gwdkir.EndpointFragment } byEndpoint[gwdkir.EndpointIdentity(kind, binding.PageID, binding.BlockName, binding.Method, binding.Route)] = binding diff --git a/internal/gwdkanalysis/ir_contracts.go b/internal/gwdkanalysis/ir_contracts.go index fd21d85c..8be498f5 100644 --- a/internal/gwdkanalysis/ir_contracts.go +++ b/internal/gwdkanalysis/ir_contracts.go @@ -25,16 +25,17 @@ func appendContractReferences(program *gwdkir.Program, template gwdkir.Template) path := ref.Path if path == "" && template.Route != "" { routeIsDynamic := routeHasDynamicParams(template.Route) - if routeIsDynamic { + switch { + case routeIsDynamic: program.Diagnostics = append(program.Diagnostics, gwdkir.Diagnostic{ Code: "contract_route_invalid", Source: template.Source, Span: templateOffsetSpan(template, ref.Start, ref.End), Message: fmt.Sprintf("%s %s must declare an explicit route on dynamic page route %q", irContractReferenceKind(ref.Kind), ref.Name, template.Route), }) - } else if ref.Kind == viewanalysis.ContractReferenceQuery { + case ref.Kind == viewanalysis.ContractReferenceQuery: method = "GET" - } else if method == "" { + case method == "": method = "POST" } if !routeIsDynamic { diff --git a/internal/gwdkir/identity.go b/internal/gwdkir/identity.go index f23669bc..6067fcb5 100644 --- a/internal/gwdkir/identity.go +++ b/internal/gwdkir/identity.go @@ -8,11 +8,13 @@ import ( // PageID, ComponentID, LayoutID, RouteID, and EndpointID are stable semantic // identities used across compiler stages. Legacy string fields remain for now // while consumers migrate toward typed stage boundaries. -type PageID string -type ComponentID string -type LayoutID string -type RouteID string -type EndpointID string +type ( + PageID string + ComponentID string + LayoutID string + RouteID string + EndpointID string +) func (id PageID) String() string { return string(id) } func (id ComponentID) String() string { return string(id) } diff --git a/internal/lang/accessibility.go b/internal/lang/accessibility.go index d8fc0fbb..65eb40e2 100644 --- a/internal/lang/accessibility.go +++ b/internal/lang/accessibility.go @@ -32,15 +32,23 @@ func viewAccessibilityDiagnostics(file string, blocks gwdkir.Blocks) Diagnostics } nodes := blocks.ViewNodes if len(nodes) == 0 { - var err error - nodes, err = viewparse.Parse(blocks.ViewBody) - if err != nil { + var ok bool + nodes, ok = parsedAccessibilityNodes(blocks.ViewBody) + if !ok { return nil } } return accessibilityDiagnosticsForNodes(file, blocks.ViewBody, blocks.Spans.ViewBodyStart, nodes) } +func parsedAccessibilityNodes(body string) ([]viewmodel.Node, bool) { + nodes, err := viewparse.Parse(body) + if err != nil { + return nil, false + } + return nodes, true +} + func accessibilityDiagnosticsForNodes(file string, body string, bodyStart source.SourcePosition, nodes []viewmodel.Node) Diagnostics { facts := collectAccessibilityFacts(nodes) check := accessibilityCheck{ @@ -281,7 +289,7 @@ func (check *accessibilityCheck) validateAccessibleName(element viewmodel.Elemen if controlNeedsAccessibleName(element, role) && strings.TrimSpace(accessibleText(element)) == "" { check.add(element, "missing_accessible_name", "<"+element.Name+"> has no accessible name", `Add visible text, aria-label, aria-labelledby, title, alt text, or a form label as appropriate.`) } - if landmarkNeedsAccessibleName(element, role) && !hasExplicitAccessibleName(element.Attrs) { + if landmarkNeedsAccessibleName(role) && !hasExplicitAccessibleName(element.Attrs) { check.add(element, "missing_landmark_name", "<"+element.Name+"> landmark has no accessible name", `Add aria-label or aria-labelledby to distinguish this landmark.`) } } @@ -463,7 +471,7 @@ func controlNeedsAccessibleName(element viewmodel.Element, role string) bool { return interactiveRoles[role] && !strings.EqualFold(role, nativeRole(element)) } -func landmarkNeedsAccessibleName(element viewmodel.Element, role string) bool { +func landmarkNeedsAccessibleName(role string) bool { if role == "" { return false } diff --git a/internal/lang/outline.go b/internal/lang/outline.go index 337189dc..47ff7a8a 100644 --- a/internal/lang/outline.go +++ b/internal/lang/outline.go @@ -92,13 +92,13 @@ func classifyLine(line []Token) (OutlineSymbol, bool) { switch { case first.Kind == TokenIdentifier && first.Lexeme == "package": - return OutlineSymbol{Kind: OutlineKindPackage, Name: "package " + nextLexeme(line, 0), Span: span}, true + return OutlineSymbol{Kind: OutlineKindPackage, Name: "package " + nextLexeme(line), Span: span}, true case first.Kind == TokenIdentifier && first.Lexeme == "import": return OutlineSymbol{Kind: OutlineKindImport, Name: "import", Detail: lineValue(line, 1), Span: span}, true case first.Kind == TokenIdentifier && first.Lexeme == "use": - return OutlineSymbol{Kind: OutlineKindUse, Name: "use " + nextLexeme(line, 0), Detail: lineValue(line, 2), Span: span}, true + return OutlineSymbol{Kind: OutlineKindUse, Name: "use " + nextLexeme(line), Detail: lineValue(line, 2), Span: span}, true case first.Kind == TokenIdentifier && (first.Lexeme == "act" || first.Lexeme == "api"): - return OutlineSymbol{Kind: OutlineKindEndpoint, Name: first.Lexeme + " " + nextLexeme(line, 0), Detail: lineValue(line, 2), Span: span}, true + return OutlineSymbol{Kind: OutlineKindEndpoint, Name: first.Lexeme + " " + nextLexeme(line), Detail: lineValue(line, 2), Span: span}, true case first.Kind == TokenMetadata: return classifyMetadata(first, line, span), true default: @@ -107,7 +107,7 @@ func classifyLine(line []Token) (OutlineSymbol, bool) { } func classifyMetadata(first Token, line []Token, span source.SourceSpan) OutlineSymbol { - name := nextLexeme(line, 0) + name := nextLexeme(line) switch first.Lexeme { case "component": if name != "" { @@ -121,10 +121,10 @@ func classifyMetadata(first Token, line []Token, span source.SourceSpan) Outline return OutlineSymbol{Kind: OutlineKindMetadata, Name: first.Lexeme, Detail: lineValue(line, 1), Span: span} } -// nextLexeme returns the lexeme of the first identifier or string after position -// at in the line, unquoted. -func nextLexeme(line []Token, at int) string { - for index := at + 1; index < len(line); index++ { +// nextLexeme returns the lexeme of the first identifier or string after the first +// token in the line, unquoted. +func nextLexeme(line []Token) string { + for index := 1; index < len(line); index++ { switch line[index].Kind { case TokenIdentifier, TokenText: return line[index].Lexeme diff --git a/internal/lang/tools.go b/internal/lang/tools.go index 816ec1f0..c5bd13bc 100644 --- a/internal/lang/tools.go +++ b/internal/lang/tools.go @@ -2,6 +2,7 @@ package lang import ( "encoding/json" + "errors" "fmt" "os" "path/filepath" @@ -555,28 +556,22 @@ func applyDefaultRenderMode(pages []gwdkir.Page, defaultMode gowdk.RenderMode) [ func compilerDiagnostics(err error, ir gwdkir.Program) Diagnostics { sources := pageSources(ir) - switch typed := err.(type) { - case compiler.ValidationErrors: - diagnostics := make(Diagnostics, 0, len(typed)) - for _, validation := range typed { - severity := "error" - if validation.Severity == compiler.SeverityWarning { - severity = "warning" + { + var typed compiler.ValidationErrors + switch { + case errors.As(err, &typed): + diagnostics := make(Diagnostics, 0, len(typed)) + for _, validation := range typed { + severity := "error" + if validation.Severity == compiler.SeverityWarning { + severity = "warning" + } + diagnostics = append(diagnostics, Diagnostic{File: diagnosticSource(validation, sources), Code: validation.Code, Pos: sourcePosition(validation.Span.Start), Range: sourceSpanRange(validation.Span), Severity: severity, Message: validation.Error(), Suggestion: diagnosticSuggestion(validation), Related: relatedLocations(validation.Related)}) } - diagnostics = append(diagnostics, Diagnostic{ - File: diagnosticSource(validation, sources), - Code: validation.Code, - Pos: sourcePosition(validation.Span.Start), - Range: sourceSpanRange(validation.Span), - Severity: severity, - Message: validation.Error(), - Suggestion: diagnosticSuggestion(validation), - Related: relatedLocations(validation.Related), - }) + return diagnostics + default: + return Diagnostics{{Severity: "error", Message: fmt.Sprint(err)}} } - return diagnostics - default: - return Diagnostics{{Severity: "error", Message: fmt.Sprint(err)}} } } diff --git a/internal/lsp/completion_fields.go b/internal/lsp/completion_fields.go index f1299521..4a6a829b 100644 --- a/internal/lsp/completion_fields.go +++ b/internal/lsp/completion_fields.go @@ -40,11 +40,12 @@ func collectBindingFields(source string, fields map[string]bool) { } index += cursor rest := source[index+len("g:bind:"):] - if strings.HasPrefix(rest, "value") { + switch { + case strings.HasPrefix(rest, "value"): rest = rest[len("value"):] - } else if strings.HasPrefix(rest, "checked") { + case strings.HasPrefix(rest, "checked"): rest = rest[len("checked"):] - } else { + default: cursor = index + len("g:bind:") continue } diff --git a/internal/lsp/components.go b/internal/lsp/components.go index d1bb299e..991b8270 100644 --- a/internal/lsp/components.go +++ b/internal/lsp/components.go @@ -127,7 +127,7 @@ func (server *Server) loadWorkspaceComponentDefinitions(root string) (map[string payloads := map[string]string{} _ = filepath.WalkDir(root, func(filePath string, entry os.DirEntry, err error) error { if err != nil { - return nil + return ignoreWorkspaceWalkError() } if entry.IsDir() { if shouldSkipWorkspaceDir(entry.Name()) && filePath != root { @@ -142,8 +142,8 @@ func (server *Server) loadWorkspaceComponentDefinitions(root string) (map[string if _, open := server.openDocumentByPath(filePath); open { return nil } - payload, err := os.ReadFile(filePath) - if err != nil { + payload, ok := readWorkspaceComponentPayload(filePath) + if !ok { return nil } if lang.ClassifySource(filePath, payload) != lang.FileKindComponent { @@ -179,6 +179,18 @@ func (server *Server) loadWorkspaceComponentDefinitions(root string) (map[string return definitions, key, paths, dirs } +func ignoreWorkspaceWalkError() error { + return nil +} + +func readWorkspaceComponentPayload(filePath string) ([]byte, bool) { + payload, err := os.ReadFile(filePath) + if err != nil { + return nil, false + } + return payload, true +} + func workspaceComponentCacheKey(files, dirs []string) string { var parts []string for _, file := range files { diff --git a/internal/lsp/rpc.go b/internal/lsp/rpc.go index 0fdb0b4b..851acc62 100644 --- a/internal/lsp/rpc.go +++ b/internal/lsp/rpc.go @@ -90,7 +90,7 @@ func (server *Server) logf(format string, args ...any) { if server.log == nil { return } - fmt.Fprintf(server.log, format+"\n", args...) + _, _ = fmt.Fprintf(server.log, format+"\n", args...) } func readMessage(reader *bufio.Reader) ([]byte, error) { diff --git a/internal/lsp/server_test.go b/internal/lsp/server_test.go index af6a7cc1..3bcad5df 100644 --- a/internal/lsp/server_test.go +++ b/internal/lsp/server_test.go @@ -457,9 +457,9 @@ func TestServerReturnsDefinitionForComponentCalls(t *testing.T) { t.Fatalf("expected definition provider capability, got %#v", capabilities) } assertResponseID(t, messages[4], float64(2)) - assertLocation(t, messages[4], localComponentURI, 2, 0) + assertLocation(t, messages[4], localComponentURI, 0) assertResponseID(t, messages[5], float64(3)) - assertLocation(t, messages[5], importedComponentURI, 2, 0) + assertLocation(t, messages[5], importedComponentURI, 0) assertResponseID(t, messages[6], float64(4)) } @@ -504,7 +504,7 @@ func TestServerReturnsDefinitionForWorkspaceComponentFile(t *testing.T) { t.Fatalf("expected 4 output messages, got %d", len(messages)) } assertResponseID(t, messages[2], float64(2)) - assertLocation(t, messages[2], componentURI, 2, 0) + assertLocation(t, messages[2], componentURI, 0) } func TestServerWorkspaceComponentCacheRefreshesWhenDiskComponentChanges(t *testing.T) { @@ -587,7 +587,7 @@ func TestServerReturnsDefinitionForOpenGoHandlerSymbols(t *testing.T) { } assertResponseID(t, messages[3], float64(2)) - assertLocation(t, messages[3], goURI, 2, 5) + assertLocation(t, messages[3], goURI, 5) assertResponseID(t, messages[4], float64(3)) } @@ -862,15 +862,15 @@ func assertNumberPrefix(t *testing.T, values []any, expected []float64) { } } -func assertLocation(t *testing.T, message map[string]any, uri string, line int, character int) { +func assertLocation(t *testing.T, message map[string]any, uri string, character int) { t.Helper() result := message["result"].(map[string]any) if result["uri"] != uri { t.Fatalf("expected location uri %q, got %#v", uri, result) } start := result["range"].(map[string]any)["start"].(map[string]any) - if start["line"] != float64(line) || start["character"] != float64(character) { - t.Fatalf("expected location start %d:%d, got %#v", line, character, result["range"]) + if start["line"] != float64(2) || start["character"] != float64(character) { + t.Fatalf("expected location start %d:%d, got %#v", 2, character, result["range"]) } } diff --git a/internal/parser/audit.go b/internal/parser/audit.go index d1c32905..2f40b2e3 100644 --- a/internal/parser/audit.go +++ b/internal/parser/audit.go @@ -208,13 +208,13 @@ func parseAuditApply(line string, lineNumber int, rawLine string) (gwdkast.Audit if len(tokens) == 0 { return gwdkast.AuditApply{}, false, nil } - switch { - case tokens[0].Lexeme == "match": + switch tokens[0].Lexeme { + case "match": if len(tokens) != 2 || tokens[1].Kind != syntax.TokenString { return gwdkast.AuditApply{}, true, fmt.Errorf("line %d: match must use match \"\"", lineNumber) } return gwdkast.AuditApply{Selector: decodeStringLiteral(tokens[1].Lexeme), Span: sourceLineSpan(lineNumber, rawLine)}, true, nil - case tokens[0].Lexeme == "apply": + case "apply": if len(tokens) != 3 || tokens[1].Lexeme != "to" || tokens[2].Kind != syntax.TokenString { return gwdkast.AuditApply{}, true, fmt.Errorf("line %d: apply must use apply to \"\"", lineNumber) } diff --git a/internal/parser/metadata.go b/internal/parser/metadata.go index 86ce4c3c..04abae11 100644 --- a/internal/parser/metadata.go +++ b/internal/parser/metadata.go @@ -270,64 +270,3 @@ func revalidateSecondsValue(value string) (string, error) { } return strconv.FormatInt(int64(duration/time.Second), 10), nil } - -func applyLayoutMetadata(layout *gwdkir.Layout, name, rawValue string, lineNumber int, rawLine string) error { - value := strings.TrimSpace(rawValue) - switch name { - case "layout": - if value == "" { - return fmt.Errorf("layout requires a value") - } - refs := splitList(value) - layout.Layouts = append(layout.Layouts, refs...) - layout.LayoutSpans = append(layout.LayoutSpans, namedValueSpans(refs, lineNumber, rawLine)...) - if (layout.Span == source.SourceSpan{}) { - layout.Span = sourceLineSpan(lineNumber, rawLine) - } - case "error": - errorPage, err := source.ErrorPagePath(trimQuotes(value)) - if err != nil { - return err - } - layout.ErrorPage = errorPage - layout.ErrorPageSpan = sourceLineSpan(lineNumber, rawLine) - default: - return lineDiagnosticError(DiagnosticUnsupportedLayoutMetadata, lineNumber, rawLine, "unsupported metadata %s", name) - } - return nil -} - -func applyComponentMetadata(component *gwdkir.Component, name, rawValue string, lineNumber int, rawLine string) error { - value := strings.TrimSpace(rawValue) - switch name { - case "component": - if value == "" { - return fmt.Errorf("component requires a value") - } - component.Name = value - component.Span = sourceLineSpan(lineNumber, rawLine) - case "wasm": - if value == "" { - return fmt.Errorf("wasm requires a package path") - } - component.WASM = gwdkir.WASMContract{ - Package: trimQuotes(value), - Span: sourceLineSpan(lineNumber, rawLine), - } - case "css": - if value == "" { - return fmt.Errorf("css requires a value") - } - component.CSS = splitCSSList(value) - component.Spans.CSS = namedValueSpans(component.CSS, lineNumber, rawLine) - case "asset": - if value == "" { - return fmt.Errorf("asset requires a value") - } - component.Assets = splitCSSList(value) - component.Spans.Assets = namedValueSpans(component.Assets, lineNumber, rawLine) - default: - return fmt.Errorf("unsupported metadata %s", name) - } - return nil -} diff --git a/internal/parser/patterns.go b/internal/parser/patterns.go index a9eb4590..8e513053 100644 --- a/internal/parser/patterns.go +++ b/internal/parser/patterns.go @@ -18,8 +18,6 @@ var ( jsPattern = linePattern{parse: parseJSLine} jsBlockPattern = linePattern{parse: parseJSBlockLine} buildCallPattern = linePattern{parse: parseBuildCallLine} - actionEndpointPattern = linePattern{parse: parseActionEndpointLine} - apiEndpointPattern = linePattern{parse: parseAPIEndpointLine} fragmentEndpointPattern = linePattern{parse: parseFragmentEndpointLine} actionPattern = linePattern{parse: parseActionBlockLine} apiPattern = linePattern{parse: parseAPIBlockLine} @@ -215,30 +213,6 @@ func parseBuildCallLine(line string) []string { return []string{line, alias, function} } -func parseActionEndpointLine(line string) []string { - spec, ok, _ := parseEndpointLineSpec(line, "act", true) - if !ok { - return nil - } - return []string{line, spec.Name, spec.Method, spec.Route, spec.ErrorPath} -} - -func parseAPIEndpointLine(line string) []string { - spec, ok, _ := parseEndpointLineSpec(line, "api", false) - if !ok { - return nil - } - return []string{line, spec.Name, spec.Method, spec.Route, spec.ErrorPath} -} - -func parseEndpointLine(line, keyword string, action bool) []string { - spec, ok, _ := parseEndpointLineSpec(line, keyword, action) - if !ok { - return nil - } - return []string{line, spec.Name, spec.Method, spec.Route, spec.ErrorPath} -} - type endpointLineSpec struct { Name string Method string @@ -469,15 +443,15 @@ func parsePropLine(line string) []string { func parseEmitLine(line string) []string { line = strings.TrimSpace(line) open := strings.Index(line, "(") - close := strings.LastIndex(line, ")") - if open <= 0 || close != len(line)-1 { + closeIndex := strings.LastIndex(line, ")") + if open <= 0 || closeIndex != len(line)-1 { return nil } name := strings.TrimSpace(line[:open]) if !isStrictIdent(name) { return nil } - return []string{line, name, line[open+1 : close]} + return []string{line, name, line[open+1 : closeIndex]} } func parseIdentifierLine(line string) []string { diff --git a/internal/parser/route_helpers.go b/internal/parser/route_helpers.go index 1f7b6be2..ee634be1 100644 --- a/internal/parser/route_helpers.go +++ b/internal/parser/route_helpers.go @@ -50,18 +50,6 @@ func sourceLineSpan(lineNumber int, rawLine string) source.SourceSpan { } } -func sourceBodyStart(lines []string, firstLineNumber int) source.SourcePosition { - for offset, rawLine := range lines { - for index, char := range []rune(rawLine) { - if strings.TrimSpace(string(char)) == "" { - continue - } - return source.SourcePosition{Line: firstLineNumber + offset, Column: index + 1} - } - } - return source.SourcePosition{} -} - func namedValueSpans(values []string, lineNumber int, rawLine string) []source.NamedSpan { if len(values) == 0 { return nil diff --git a/internal/parser/syntax.go b/internal/parser/syntax.go index 1b9acfe0..3963f6a4 100644 --- a/internal/parser/syntax.go +++ b/internal/parser/syntax.go @@ -13,26 +13,28 @@ import ( "github.com/cssbruno/gowdk/internal/viewparse" ) -type SyntaxFile = gwdkast.File -type SyntaxPackage = gwdkast.Package -type SyntaxMetadata = gwdkast.MetadataDecl -type SyntaxImport = gwdkast.Import -type SyntaxUse = gwdkast.Use -type SyntaxBlock = gwdkast.Block -type SyntaxEndpoint = gwdkast.Endpoint -type SyntaxFragmentEndpoint = gwdkast.FragmentEndpoint -type GoTypeRef = gwdkast.GoTypeRef -type GoFuncRef = gwdkast.GoFuncRef -type StateContract = gwdkast.StateContract -type WASMContract = gwdkast.WASMContract -type LiteralRecord = gwdkast.LiteralRecord -type BuildCall = gwdkast.BuildCall -type Prop = gwdkast.Prop -type Export = gwdkast.Export -type Emit = gwdkast.Emit -type EmitParam = gwdkast.EmitParam -type ActionStatement = gwdkast.ActionStatement -type APIStatement = gwdkast.APIStatement +type ( + SyntaxFile = gwdkast.File + SyntaxPackage = gwdkast.Package + SyntaxMetadata = gwdkast.MetadataDecl + SyntaxImport = gwdkast.Import + SyntaxUse = gwdkast.Use + SyntaxBlock = gwdkast.Block + SyntaxEndpoint = gwdkast.Endpoint + SyntaxFragmentEndpoint = gwdkast.FragmentEndpoint + GoTypeRef = gwdkast.GoTypeRef + GoFuncRef = gwdkast.GoFuncRef + StateContract = gwdkast.StateContract + WASMContract = gwdkast.WASMContract + LiteralRecord = gwdkast.LiteralRecord + BuildCall = gwdkast.BuildCall + Prop = gwdkast.Prop + Export = gwdkast.Export + Emit = gwdkast.Emit + EmitParam = gwdkast.EmitParam + ActionStatement = gwdkast.ActionStatement + APIStatement = gwdkast.APIStatement +) // ParseSyntax parses a .gwdk source file into a typed syntax AST for the // current compiler subset. @@ -533,10 +535,7 @@ func finishSyntaxBlock(block SyntaxBlock, body []syntaxBodyLine) (SyntaxBlock, e } block.Records = records case "build": - call, ok, err := parseBuildCall(body) - if err != nil { - return SyntaxBlock{}, err - } + call, ok := parseBuildCall(body) if ok { block.Call = &call return block, nil @@ -594,7 +593,7 @@ func syntaxBodyStart(body []syntaxBodyLine) source.SourcePosition { return source.SourcePosition{} } -func parseBuildCall(body []syntaxBodyLine) (BuildCall, bool, error) { +func parseBuildCall(body []syntaxBodyLine) (BuildCall, bool) { var significant []syntaxBodyLine for _, raw := range body { line := strings.TrimSpace(raw.Text) @@ -604,18 +603,18 @@ func parseBuildCall(body []syntaxBodyLine) (BuildCall, bool, error) { significant = append(significant, raw) } if len(significant) != 1 { - return BuildCall{}, false, nil + return BuildCall{}, false } line := strings.TrimSpace(significant[0].Text) match := buildCallPattern.FindStringSubmatch(line) if match == nil { - return BuildCall{}, false, nil + return BuildCall{}, false } return BuildCall{ Alias: match[1], Function: match[2], Span: sourceLineSpan(significant[0].Line, significant[0].Text), - }, true, nil + }, true } func joinSyntaxBody(body []syntaxBodyLine) string { diff --git a/internal/parser/syntax_test.go b/internal/parser/syntax_test.go index f6e8cd22..0b85dccf 100644 --- a/internal/parser/syntax_test.go +++ b/internal/parser/syntax_test.go @@ -300,7 +300,7 @@ view { } func TestParseSyntaxReturnsGOWDKAST(t *testing.T) { - var _ gwdkast.File = mustParseSyntax(t, []byte(`package ui + _ = mustParseSyntax(t, []byte(`package ui component Counter wasm ./counter/browser css "./counter.css" diff --git a/internal/playground/playground.go b/internal/playground/playground.go index ff0a240a..65cceef9 100644 --- a/internal/playground/playground.go +++ b/internal/playground/playground.go @@ -115,11 +115,13 @@ func ExportArchive(sourceDir string, archivePath string, options Options) (Expor if err != nil { return ExportResult{}, err } - defer file.Close() + defer func() { + _ = file.Close() + }() writer := zip.NewWriter(file) for _, item := range files { if err := writeZipFile(writer, sourceDir, item.Path); err != nil { - writer.Close() + _ = writer.Close() return ExportResult{}, err } } @@ -143,11 +145,11 @@ func StageWorkspace(sourceDir string, options Options) (Workspace, func() error, sourcePath := filepath.Join(sourceDir, filepath.FromSlash(item.Path)) targetPath := filepath.Join(root, filepath.FromSlash(item.Path)) if err := os.MkdirAll(filepath.Dir(targetPath), 0o755); err != nil { - cleanup() + _ = cleanup() return Workspace{}, nil, err } if err := copyFile(sourcePath, targetPath); err != nil { - cleanup() + _ = cleanup() return Workspace{}, nil, err } } @@ -352,7 +354,9 @@ func writeZipFile(writer *zip.Writer, sourceDir string, rel string) error { if err != nil { return err } - defer source.Close() + defer func() { + _ = source.Close() + }() _, err = io.Copy(target, source) return err } @@ -362,12 +366,16 @@ func copyFile(sourcePath string, targetPath string) error { if err != nil { return err } - defer source.Close() + defer func() { + _ = source.Close() + }() target, err := os.OpenFile(targetPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644) if err != nil { return err } - defer target.Close() + defer func() { + _ = target.Close() + }() _, err = io.Copy(target, source) return err } diff --git a/internal/playground/playground_test.go b/internal/playground/playground_test.go index d1785fe9..2097e1fc 100644 --- a/internal/playground/playground_test.go +++ b/internal/playground/playground_test.go @@ -59,7 +59,9 @@ func TestExportArchiveWritesNormalProjectFiles(t *testing.T) { if err != nil { t.Fatal(err) } - defer reader.Close() + defer func() { + _ = reader.Close() + }() var names []string for _, file := range reader.File { names = append(names, file.Name) @@ -83,7 +85,9 @@ func TestStageWorkspaceCopiesAllowedFilesOnly(t *testing.T) { if err != nil { t.Fatal(err) } - defer cleanup() + defer func() { + _ = cleanup() + }() if _, err := os.Stat(filepath.Join(workspace.Root, "src", "pages", "home.page.gwdk")); err != nil { t.Fatalf("expected staged source file: %v", err) } diff --git a/internal/project/config_exec.go b/internal/project/config_exec.go index 8e6a14e6..bdece212 100644 --- a/internal/project/config_exec.go +++ b/internal/project/config_exec.go @@ -166,11 +166,11 @@ func cloneExecutableSEOOptions(options gowdk.SEOOptions) gowdk.SEOOptions { } func (addon executableCSSAddon) ProcessCSS(context gowdk.CSSContext) (gowdk.CSSResult, error) { - return addon.executableAddon.processCSS(context) + return addon.processCSS(context) } func (addon executableCSSGoBlockAddon) ProcessCSS(context gowdk.CSSContext) (gowdk.CSSResult, error) { - return addon.executableAddon.processCSS(context) + return addon.processCSS(context) } func (addon executableAddon) processCSS(context gowdk.CSSContext) (gowdk.CSSResult, error) { @@ -193,11 +193,11 @@ func (addon executableAddon) processCSS(context gowdk.CSSContext) (gowdk.CSSResu } func (addon executableGoBlockAddon) GoBlockTargets() []string { - return addon.executableAddon.goBlockTargetsCopy() + return addon.goBlockTargetsCopy() } func (addon executableCSSGoBlockAddon) GoBlockTargets() []string { - return addon.executableAddon.goBlockTargetsCopy() + return addon.goBlockTargetsCopy() } func (addon executableAddon) goBlockTargetsCopy() []string { @@ -205,11 +205,11 @@ func (addon executableAddon) goBlockTargetsCopy() []string { } func (addon executableGoBlockAddon) ValidateGoBlock(target gowdk.GoBlockTarget, context gowdk.GoBlockContext) []gowdk.GoBlockDiagnostic { - return addon.executableAddon.validateGoBlock(target, context) + return addon.validateGoBlock(target, context) } func (addon executableCSSGoBlockAddon) ValidateGoBlock(target gowdk.GoBlockTarget, context gowdk.GoBlockContext) []gowdk.GoBlockDiagnostic { - return addon.executableAddon.validateGoBlock(target, context) + return addon.validateGoBlock(target, context) } func (addon executableAddon) validateGoBlock(target gowdk.GoBlockTarget, context gowdk.GoBlockContext) []gowdk.GoBlockDiagnostic { @@ -232,11 +232,11 @@ func (addon executableAddon) validateGoBlock(target gowdk.GoBlockTarget, context } func (addon executableGoBlockAddon) GeneratedGo(target gowdk.GoBlockTarget, context gowdk.GoBlockContext) ([]gowdk.GoBlockFile, error) { - return addon.executableAddon.generatedGo(target, context) + return addon.generatedGo(target, context) } func (addon executableCSSGoBlockAddon) GeneratedGo(target gowdk.GoBlockTarget, context gowdk.GoBlockContext) ([]gowdk.GoBlockFile, error) { - return addon.executableAddon.generatedGo(target, context) + return addon.generatedGo(target, context) } func (addon executableAddon) generatedGo(target gowdk.GoBlockTarget, context gowdk.GoBlockContext) ([]gowdk.GoBlockFile, error) { @@ -275,7 +275,9 @@ func runConfigHelper(configPath string, command string, input []byte, args ...st if err != nil { return nil, err } - defer os.RemoveAll(helperDir) + defer func() { + _ = os.RemoveAll(helperDir) + }() source, err := configHelperSource(packageInfo.ImportPath) if err != nil { diff --git a/internal/publicapi/gowdk_test.go b/internal/publicapi/gowdk_test.go index a2373f9b..b3bcbd1f 100644 --- a/internal/publicapi/gowdk_test.go +++ b/internal/publicapi/gowdk_test.go @@ -42,14 +42,14 @@ func TestValidateAddonsRejectsInvalidIdentityAndOwnership(t *testing.T) { addons []gowdk.Addon want string }{ - {name: "nil", addons: []gowdk.Addon{nil}, want: "Addons[0] is nil"}, + {name: "nil", addons: []gowdk.Addon{nil}, want: "addons[0] is nil"}, {name: "empty name", addons: []gowdk.Addon{gowdk.NewAddon("", gowdk.FeatureCSS)}, want: "Name is required"}, {name: "empty features", addons: []gowdk.Addon{gowdk.NewAddon("empty")}, want: "at least one feature"}, {name: "empty feature", addons: []gowdk.Addon{gowdk.NewAddon("empty-feature", gowdk.Feature(""))}, want: "empty feature"}, {name: "duplicate names", addons: []gowdk.Addon{ gowdk.NewAddon("css", gowdk.FeatureCSS), gowdk.NewAddon("css", gowdk.FeatureSEO), - }, want: "duplicates Addons[0]"}, + }, want: "duplicates addons[0]"}, {name: "duplicate features", addons: []gowdk.Addon{ gowdk.NewAddon("css-a", gowdk.FeatureCSS), gowdk.NewAddon("css-b", gowdk.FeatureCSS), diff --git a/internal/securitymanifest/manifest_test.go b/internal/securitymanifest/manifest_test.go index 69bd4107..8d4c39f8 100644 --- a/internal/securitymanifest/manifest_test.go +++ b/internal/securitymanifest/manifest_test.go @@ -60,7 +60,7 @@ func TestRelativizeLeavesNonFileAndRelativeRefsUntouched(t *testing.T) { {PageID: "b", Route: "/b", Source: "already/relative.page.gwdk:3"}, }, } - out := manifest.Relativize(filepath.Join(string(filepath.Separator) + "root")) + out := manifest.Relativize(string(filepath.Separator) + "root") if out.Routes[0].Source != "config:Build.SecurityHeaders" { t.Fatalf("non-file source must pass through, got %q", out.Routes[0].Source) } diff --git a/internal/source/routes.go b/internal/source/routes.go index 1bf2262b..9ffdaeeb 100644 --- a/internal/source/routes.go +++ b/internal/source/routes.go @@ -272,7 +272,7 @@ func RouteParamsFromPath(route string) []RouteParam { } end += index param, ok := ParseRouteParamSegment(route[index : end+1]) - if ok && IsRouteParamName(param.Name) && IsRouteParamType(param.Type) && !(param.Rest && param.HasType) { + if ok && IsRouteParamName(param.Name) && IsRouteParamType(param.Type) && (!param.Rest || !param.HasType) { params = append(params, RouteParam{Name: param.Name, Type: param.Type}) } index = end diff --git a/internal/source/source.go b/internal/source/source.go index 348da72b..c88f1b01 100644 --- a/internal/source/source.go +++ b/internal/source/source.go @@ -214,7 +214,7 @@ func BackendRoutePath(value string) string { // ValidateBackendRoutePath rejects paths that would be unsafe or ambiguous when // registered as generated backend routes. func ValidateBackendRoutePath(value string) error { - if _, err := validateBackendRoutePreflight(value, "must be a concrete path without query, fragment, or params"); err != nil { + if err := validateBackendRoutePreflight(value, "must be a concrete path without query, fragment, or params"); err != nil { return err } if strings.ContainsAny(value, "{}") { @@ -226,7 +226,7 @@ func ValidateBackendRoutePath(value string) error { // ValidateBackendRoutePattern rejects unsafe generated route patterns while // allowing whole-segment route params such as /patients/{id:int}. func ValidateBackendRoutePattern(value string) error { - if _, err := validateBackendRoutePreflight(value, "must not contain query strings or fragments"); err != nil { + if err := validateBackendRoutePreflight(value, "must not contain query strings or fragments"); err != nil { return err } if value == "/" { @@ -272,35 +272,35 @@ func ValidateBackendRoutePattern(value string) error { return nil } -func validateBackendRoutePreflight(value, queryFragmentRule string) (string, error) { +func validateBackendRoutePreflight(value, queryFragmentRule string) error { if strings.TrimSpace(value) != value { - return "", fmt.Errorf("endpoint path %q must not contain surrounding whitespace", value) + return fmt.Errorf("endpoint path %q must not contain surrounding whitespace", value) } if value == "" { - return "", fmt.Errorf("endpoint path must not be empty") + return fmt.Errorf("endpoint path must not be empty") } if value[0] != '/' { - return "", fmt.Errorf("endpoint path %q must be a local absolute path", value) + return fmt.Errorf("endpoint path %q must be a local absolute path", value) } if len(value) > 1 && (value[1] == '/' || value[1] == '\\') { - return "", fmt.Errorf("endpoint path %q must not be protocol-relative", value) + return fmt.Errorf("endpoint path %q must not be protocol-relative", value) } if strings.Contains(value, "\\") { - return "", fmt.Errorf("endpoint path %q must not contain backslashes", value) + return fmt.Errorf("endpoint path %q must not contain backslashes", value) } if strings.ContainsAny(value, "?#") { - return "", fmt.Errorf("endpoint path %q %s", value, queryFragmentRule) + return fmt.Errorf("endpoint path %q %s", value, queryFragmentRule) } for _, char := range value { if char < 0x20 || char == 0x7f { - return "", fmt.Errorf("endpoint path %q must not contain control characters", value) + return fmt.Errorf("endpoint path %q must not contain control characters", value) } } cleaned := BackendRoutePath(value) if cleaned != value { - return "", fmt.Errorf("endpoint path %q must be a clean absolute path without dot segments, duplicate slashes, or trailing slash", value) + return fmt.Errorf("endpoint path %q must be a clean absolute path without dot segments, duplicate slashes, or trailing slash", value) } - return cleaned, nil + return nil } type backendRouteParam struct { diff --git a/internal/syntax/declparse.go b/internal/syntax/declparse.go index abd194a1..2890d4aa 100644 --- a/internal/syntax/declparse.go +++ b/internal/syntax/declparse.go @@ -76,8 +76,8 @@ func ParseTopLevel(src string) TopLevel { line := tokens[index:lineEnd] first := line[0] - switch { - case first.Kind == TokenIdentifier: + switch first.Kind { + case TokenIdentifier: // ParseSyntax requires eof() after every identifier-led // declaration, but the shared tokenizer strips // comments, so a // trailing comment leaves a clean token line here. Reject any line @@ -123,7 +123,7 @@ func ParseTopLevel(src string) TopLevel { result.APIs = append(result.APIs, endpoint) } } - case first.Kind == TokenMetadata: + case TokenMetadata: result.applyMetadata(first, line, metadataValue(src, first, tokens[lineEnd])) } index = lineEnd diff --git a/internal/syntax/lexer.go b/internal/syntax/lexer.go index 0292a221..3ac84cf3 100644 --- a/internal/syntax/lexer.go +++ b/internal/syntax/lexer.go @@ -201,7 +201,7 @@ func (scanner *scanner) peekNext() rune { return scanner.source[scanner.index+1] } -func (scanner *scanner) advance() rune { +func (scanner *scanner) advance() { ch := scanner.source[scanner.index] scanner.index++ if ch == '\n' { @@ -210,7 +210,6 @@ func (scanner *scanner) advance() rune { } else { scanner.column++ } - return ch } func (scanner *scanner) position() Position { diff --git a/internal/syntax/syntax_test.go b/internal/syntax/syntax_test.go index e04a712a..f4f9c8c1 100644 --- a/internal/syntax/syntax_test.go +++ b/internal/syntax/syntax_test.go @@ -52,9 +52,9 @@ func TestLineExtentAndMatchBrace(t *testing.T) { if tokens[end].Kind != TokenNewline { t.Fatalf("LineExtent stopped at %s, want newline", tokens[end].Kind) } - close := MatchBrace(tokens, 0) - if tokens[close].Kind != TokenRBrace { - t.Fatalf("MatchBrace landed on %s, want rbrace", tokens[close].Kind) + closeIndex := MatchBrace(tokens, 0) + if tokens[closeIndex].Kind != TokenRBrace { + t.Fatalf("MatchBrace landed on %s, want rbrace", tokens[closeIndex].Kind) } } diff --git a/internal/viewparse/events.go b/internal/viewparse/events.go index 4545df86..e7e5fc24 100644 --- a/internal/viewparse/events.go +++ b/internal/viewparse/events.go @@ -20,7 +20,7 @@ type EventDirective struct { // ParseEventDirective validates and splits a g:on directive name. func ParseEventDirective(name string) (EventDirective, error) { if !strings.HasPrefix(name, "g:on:") { - return EventDirective{}, fmt.Errorf("event directive %q must start with g:on:", name) + return EventDirective{}, fmt.Errorf("event directive %q must start with g:on", name) } raw := strings.TrimPrefix(name, "g:on:") if raw == "" { @@ -87,7 +87,7 @@ func parseEventDurationMS(value string) (int, error) { return 0, fmt.Errorf("duration is empty") } multiplier := 1 - number := value + var number string switch { case strings.HasSuffix(value, "ms"): number = strings.TrimSuffix(value, "ms") diff --git a/internal/viewparse/model.go b/internal/viewparse/model.go index 3aa0b571..f8852eec 100644 --- a/internal/viewparse/model.go +++ b/internal/viewparse/model.go @@ -7,12 +7,14 @@ import ( "github.com/cssbruno/gowdk/internal/viewmodel" ) -type Attr = viewmodel.Attr -type AwaitBlock = viewmodel.AwaitBlock -type ComponentCall = viewmodel.ComponentCall -type Element = viewmodel.Element -type Node = viewmodel.Node -type Text = viewmodel.Text +type ( + Attr = viewmodel.Attr + AwaitBlock = viewmodel.AwaitBlock + ComponentCall = viewmodel.ComponentCall + Element = viewmodel.Element + Node = viewmodel.Node + Text = viewmodel.Text +) const ( escapedOpenBrace = "\x00GOWDK_OPEN_BRACE\x00" diff --git a/internal/viewparse/parser.go b/internal/viewparse/parser.go index 284999f9..3cbde9bb 100644 --- a/internal/viewparse/parser.go +++ b/internal/viewparse/parser.go @@ -455,7 +455,7 @@ func (parser *parser) attrWithOptions(allowComponentBind bool) (Attr, error) { if !isAttrName(name) { return Attr{}, parser.errorf("unsupported attribute name %q", name) } - if strings.HasPrefix(name, "g:") && !isSupportedDirectiveName(name) && !(allowComponentBind && isComponentBindDirective(name)) { + if strings.HasPrefix(name, "g:") && !isSupportedDirectiveName(name) && (!allowComponentBind || !isComponentBindDirective(name)) { return Attr{}, parser.errorf("%s", unsupportedDirectiveMessage(name)) } @@ -546,9 +546,7 @@ func normalizeHTMLAttrs(attrs []Attr) ([]Attr, error) { out = append(out, attr) continue } - for _, className := range strings.Fields(attr.Value) { - classValues = append(classValues, className) - } + classValues = append(classValues, strings.Fields(attr.Value)...) case "id": if attr.Boolean { out = append(out, attr) diff --git a/internal/viewparse/patterns.go b/internal/viewparse/patterns.go index 185c8120..8388c8cf 100644 --- a/internal/viewparse/patterns.go +++ b/internal/viewparse/patterns.go @@ -25,27 +25,12 @@ func (pattern viewPattern) FindStringSubmatch(source string) []string { } var ( - islandFieldPattern = viewPattern{match: isIdentifier} - islandTextBindingPattern = viewPattern{submatch: parseIslandTextBinding} forDirectivePattern = viewPattern{submatch: parseForDirectivePattern} - contractReferencePattern = viewPattern{match: isContractReference} eventNamePattern = viewPattern{match: isEventName} stylePropertyPattern = viewPattern{match: isStyleProperty} styleCustomPropertyPattern = viewPattern{match: isStyleCustomProperty} ) -func parseIslandTextBinding(source string) []string { - trimmed := strings.TrimSpace(source) - if !strings.HasPrefix(trimmed, "{") || !strings.HasSuffix(trimmed, "}") { - return nil - } - name := strings.TrimSpace(trimmed[1 : len(trimmed)-1]) - if !isIdentifier(name) { - return nil - } - return []string{source, name} -} - func parseForDirectivePattern(source string) []string { inStart, inEnd, ok := findForInOperator(source) if !ok { @@ -92,21 +77,8 @@ func findForInOperator(source string) (int, int, bool) { return 0, 0, false } -func isContractReference(source string) bool { - parts := strings.Split(source, ".") - if len(parts) < 2 { - return false - } - for _, part := range parts { - if !isIdentifier(part) { - return false - } - } - return true -} - func isEventName(source string) bool { - if source == "" || !((source[0] >= 'A' && source[0] <= 'Z') || (source[0] >= 'a' && source[0] <= 'z')) { + if source == "" || (source[0] < 'A' || source[0] > 'Z') && (source[0] < 'a' || source[0] > 'z') { return false } for index := 1; index < len(source); index++ { diff --git a/internal/viewparse/safety.go b/internal/viewparse/safety.go index 2ea08f60..b5278b71 100644 --- a/internal/viewparse/safety.go +++ b/internal/viewparse/safety.go @@ -28,16 +28,6 @@ func validateParsedHTMLAttrSafety(attr Attr) error { return validateURLAttrValue(attr.Name, attr.Value) } -func validateRenderedHTMLAttrSafety(name, value string) error { - if inlineEventHandlerAttr(name) { - return fmt.Errorf("inline event handler attribute %q is not supported; use g:on:* inside stateful components", name) - } - if strings.EqualFold(strings.TrimSpace(name), "srcdoc") { - return fmt.Errorf("srcdoc attribute is not supported; use g:unsafe-html only with trusted sanitized HTML") - } - return validateURLAttrValue(name, value) -} - func inlineEventHandlerAttr(name string) bool { name = strings.ToLower(strings.TrimSpace(name)) return strings.HasPrefix(name, "on") && len(name) > 2 @@ -107,8 +97,8 @@ func explicitURLScheme(value string) (string, bool) { return "", false } for index, char := range value { - switch { - case char == ':': + switch char { + case ':': if index == 0 { return "", false } @@ -117,7 +107,7 @@ func explicitURLScheme(value string) (string, bool) { return "", false } return strings.ToLower(candidate), true - case char == '/', char == '?', char == '#': + case '/', '?', '#': return "", false } } diff --git a/internal/viewrender/action_input_attrs.go b/internal/viewrender/action_input_attrs.go index 56a3f29b..49ab0b40 100644 --- a/internal/viewrender/action_input_attrs.go +++ b/internal/viewrender/action_input_attrs.go @@ -53,12 +53,12 @@ func synthesizedInputAttrs(fieldType string, attrs []Attr) []Attr { if !hasLiteralAttr(attrs, "inputmode") { out = append(out, Attr{Name: "inputmode", Value: "numeric"}) } - if min, max, ok := integerActionInputBounds(fieldType); ok { - if min != "" && !hasLiteralAttr(attrs, "min") { - out = append(out, Attr{Name: "min", Value: min}) + if minimum, maximum, ok := integerActionInputBounds(fieldType); ok { + if minimum != "" && !hasLiteralAttr(attrs, "min") { + out = append(out, Attr{Name: "min", Value: minimum}) } - if max != "" && !hasLiteralAttr(attrs, "max") { - out = append(out, Attr{Name: "max", Value: max}) + if maximum != "" && !hasLiteralAttr(attrs, "max") { + out = append(out, Attr{Name: "max", Value: maximum}) } } return out diff --git a/internal/viewrender/api.go b/internal/viewrender/api.go index e8a28f6f..94b3d2ec 100644 --- a/internal/viewrender/api.go +++ b/internal/viewrender/api.go @@ -8,14 +8,16 @@ import ( "github.com/cssbruno/gowdk/internal/viewparse" ) -type Attr = viewmodel.Attr -type AwaitBlock = viewmodel.AwaitBlock -type Component = viewmodel.Component -type ComponentCall = viewmodel.ComponentCall -type Element = viewmodel.Element -type InlineScript = viewmodel.InlineScript -type Node = viewmodel.Node -type Text = viewmodel.Text +type ( + Attr = viewmodel.Attr + AwaitBlock = viewmodel.AwaitBlock + Component = viewmodel.Component + ComponentCall = viewmodel.ComponentCall + Element = viewmodel.Element + InlineScript = viewmodel.InlineScript + Node = viewmodel.Node + Text = viewmodel.Text +) // Parse parses a view markup fragment. func Parse(source string) ([]Node, error) { @@ -84,21 +86,25 @@ type ActionFormField struct { PatternMessage string } -type Dependencies = viewanalysis.Dependencies -type ComponentIslandUsage = viewanalysis.ComponentIslandUsage -type ComponentCallUsage = viewanalysis.ComponentCallUsage -type ComponentReference = viewanalysis.ComponentReference -type ContractReference = viewanalysis.ContractReference -type ContractReferenceKind = viewanalysis.ContractReferenceKind +type ( + Dependencies = viewanalysis.Dependencies + ComponentIslandUsage = viewanalysis.ComponentIslandUsage + ComponentCallUsage = viewanalysis.ComponentCallUsage + ComponentReference = viewanalysis.ComponentReference + ContractReference = viewanalysis.ContractReference + ContractReferenceKind = viewanalysis.ContractReferenceKind +) const ( ContractReferenceCommand = viewanalysis.ContractReferenceCommand ContractReferenceQuery = viewanalysis.ContractReferenceQuery ) -type CommandReference = viewanalysis.CommandReference -type QueryReference = viewanalysis.QueryReference -type SubscriptionReference = viewanalysis.SubscriptionReference +type ( + CommandReference = viewanalysis.CommandReference + QueryReference = viewanalysis.QueryReference + SubscriptionReference = viewanalysis.SubscriptionReference +) // RenderWithOptions renders a view markup fragment with component support, // interpolation data, and page-scoped action endpoints. diff --git a/internal/viewrender/bindings.go b/internal/viewrender/bindings.go index c609e234..cb71d1f4 100644 --- a/internal/viewrender/bindings.go +++ b/internal/viewrender/bindings.go @@ -2,8 +2,9 @@ package viewrender import ( "fmt" - "github.com/cssbruno/gowdk/internal/clientlang" "strings" + + "github.com/cssbruno/gowdk/internal/clientlang" ) type styleBinding struct { diff --git a/internal/viewrender/conditional.go b/internal/viewrender/conditional.go index fd210c32..75f17439 100644 --- a/internal/viewrender/conditional.go +++ b/internal/viewrender/conditional.go @@ -2,9 +2,10 @@ package viewrender import ( "fmt" - "github.com/cssbruno/gowdk/internal/clientlang" "strconv" "strings" + + "github.com/cssbruno/gowdk/internal/clientlang" ) func renderNodes(nodes []Node, ctx *renderContext) (string, error) { diff --git a/internal/viewrender/directives.go b/internal/viewrender/directives.go index 61fa740c..52bf481e 100644 --- a/internal/viewrender/directives.go +++ b/internal/viewrender/directives.go @@ -1,10 +1,6 @@ package viewrender -import ( - "fmt" - "sort" - "strings" -) +import "sort" // supportedDirectiveNames is the closed set of exact-name g: directives owned // by the GOWDK view {} markup contract. Prefixed families (g:on:* and @@ -38,14 +34,6 @@ var supportedDirectiveNames = map[string]bool{ "g:target": true, } -// supportedMessageDirectives are the g:message:* validation-message rules. -var supportedMessageDirectives = map[string]bool{ - "g:message:required": true, - "g:message:minlength": true, - "g:message:maxlength": true, - "g:message:pattern": true, -} - // SupportedDirectiveNames returns the sorted closed set of exact-name g: // directives owned by the current view contract (excluding the g:on:* event // family and the g:message:* rules, which are validated separately). It is the @@ -58,85 +46,3 @@ func SupportedDirectiveNames() []string { sort.Strings(names) return names } - -func isSupportedDirectiveName(name string) bool { - if supportedDirectiveNames[name] { - return true - } - if strings.HasPrefix(name, "g:on:") { - // Event names and modifiers are validated by ParseEventDirective via - // isAttrName before this check runs. - return true - } - return supportedMessageDirectives[name] -} - -func isComponentBindDirective(name string) bool { - return name == "g:bind" || strings.HasPrefix(name, "g:bind:") -} - -// unsupportedDirectiveMessage is the canonical unsupported_markup_directive -// message for a g: attribute outside the owned directive contract. Deferred -// construct families get explicit guidance instead of a generic rejection. -func unsupportedDirectiveMessage(name string) string { - switch { - case name == "g:html": - return "g:html was renamed to g:unsafe-html to make the raw-HTML XSS surface explicit; use g:unsafe-html={Expr} to opt into trusted raw HTML" - case name == "g:each": - return "g:each was unified into g:for; use g:for={item in collection} — the compiler renders it server-side when the collection is a server {} field and as a client island over state/store" - case name == "g:when": - return "g:when was unified into g:if; use g:if={field} (or g:if={!field}) — the compiler renders it server-side when the condition is a server {} field and as a client conditional over state/store" - case strings.HasPrefix(name, "g:transition") || strings.HasPrefix(name, "g:animate"): - return fmt.Sprintf("unsupported g: directive %q; supported motion directives are g:transition and g:animate", name) - case name == "g:window" || name == "g:document" || name == "g:body" || name == "g:head": - return fmt.Sprintf("unsupported g: directive %q; document, window, body, and head targets are deferred from the view {} contract — use page metadata such as title, or g:on:* on rendered elements", name) - case name == "g:await" || name == "g:async": - return fmt.Sprintf("unsupported g: directive %q; use a bounded {#await fetchJSON[T](urlExpr)} block inside a client island for local async placeholders", name) - case name == "g:use" || name == "g:action" || name == "g:attach": - return fmt.Sprintf("unsupported g: directive %q; DOM actions and attachments are deferred from the view {} contract — use component client {} blocks with g:ref", name) - case strings.HasPrefix(name, "g:bind:") || name == "g:bind": - return fmt.Sprintf("unsupported g: directive %q; supported g:bind targets are g:bind:value and g:bind:checked", name) - case strings.HasPrefix(name, "g:message:") || name == "g:message": - return fmt.Sprintf("unsupported g: directive %q; supported g:message rules are required, minlength, maxlength, and pattern", name) - default: - return fmt.Sprintf("unsupported g: directive %q; supported g: directives are listed in docs/language/markup.md", name) - } -} - -// validateRawHTMLDirective enforces the parse-time g:unsafe-html contract: one g:unsafe-html -// per element, expression value only, no markup children, no void elements, -// and no combination with g:for/g:key or g:bind:* directives. -func validateRawHTMLDirective(name string, attrs []Attr, children []Node) error { - count := 0 - var raw Attr - for _, attr := range attrs { - if attr.Name == "g:unsafe-html" { - count++ - raw = attr - } - } - if count == 0 { - return nil - } - if count > 1 { - return fmt.Errorf("element declares multiple g:unsafe-html directives") - } - if voidElements[name] { - return fmt.Errorf("g:unsafe-html is not supported on void element <%s>", name) - } - if raw.Boolean || strings.TrimSpace(raw.Value) == "" { - return fmt.Errorf("g:unsafe-html requires an expression value such as g:unsafe-html={Body}") - } - if !raw.Expression { - return fmt.Errorf("g:unsafe-html must use an expression value such as g:unsafe-html={Body}, not a string literal") - } - if len(children) > 0 { - return fmt.Errorf("element with g:unsafe-html must not declare children; the g:unsafe-html expression provides the element content") - } - for _, attr := range attrs { - if attr.Name == "g:for" || attr.Name == "g:key" || strings.HasPrefix(attr.Name, "g:bind:") { - return fmt.Errorf("g:unsafe-html cannot combine with %s", attr.Name) - } - } - return nil -} diff --git a/internal/viewrender/element.go b/internal/viewrender/element.go index c0c33fe4..286320b2 100644 --- a/internal/viewrender/element.go +++ b/internal/viewrender/element.go @@ -2,12 +2,12 @@ package viewrender import ( "fmt" + "strconv" + "strings" "github.com/cssbruno/gowdk/internal/clientlang" "github.com/cssbruno/gowdk/internal/viewvalidation" gowhtml "github.com/cssbruno/gowdk/runtime/html" - "strconv" - "strings" ) func renderElement(node Element, ctx *renderContext, out *renderOutput) error { @@ -425,11 +425,12 @@ func renderElement(node Element, ctx *renderContext, out *renderOutput) error { next.formAction = directives.Action childCtx = &next } - if hasRawHTML { + switch { + case hasRawHTML: out.write(rawHTML) - } else if node.Name == "textarea" && valueBinding != "" { + case node.Name == "textarea" && valueBinding != "": out.write(gowhtml.Escape(ctx.values[valueBinding])) - } else { + default: for _, child := range node.Children { if err := renderNode(child, childCtx, out); err != nil { return err diff --git a/internal/viewrender/events.go b/internal/viewrender/events.go index 6210a305..74845794 100644 --- a/internal/viewrender/events.go +++ b/internal/viewrender/events.go @@ -20,7 +20,7 @@ type EventDirective struct { // ParseEventDirective validates and splits a g:on directive name. func ParseEventDirective(name string) (EventDirective, error) { if !strings.HasPrefix(name, "g:on:") { - return EventDirective{}, fmt.Errorf("event directive %q must start with g:on:", name) + return EventDirective{}, fmt.Errorf("event directive %q must start with g:on", name) } raw := strings.TrimPrefix(name, "g:on:") if raw == "" { @@ -87,7 +87,7 @@ func parseEventDurationMS(value string) (int, error) { return 0, fmt.Errorf("duration is empty") } multiplier := 1 - number := value + var number string switch { case strings.HasSuffix(value, "ms"): number = strings.TrimSuffix(value, "ms") diff --git a/internal/viewrender/interpolate.go b/internal/viewrender/interpolate.go index 87e204e8..4ff533da 100644 --- a/internal/viewrender/interpolate.go +++ b/internal/viewrender/interpolate.go @@ -19,11 +19,6 @@ func renderText(ctx *renderContext, out *renderOutput, value string) error { return nil } -func interpolate(ctx *renderContext, value string) (string, error) { - resolved, _, err := interpolateValue(ctx, value) - return resolved, err -} - func interpolateValue(ctx *renderContext, value string) (string, bool, error) { if !strings.Contains(value, "{") { return value, false, nil diff --git a/internal/viewrender/island_helpers.go b/internal/viewrender/island_helpers.go index 68bc1622..b564408a 100644 --- a/internal/viewrender/island_helpers.go +++ b/internal/viewrender/island_helpers.go @@ -3,8 +3,9 @@ package viewrender import ( "encoding/json" "fmt" - "github.com/cssbruno/gowdk/internal/clientlang" "strconv" + + "github.com/cssbruno/gowdk/internal/clientlang" ) func validateIslandField(field string, fields map[string]bool) error { diff --git a/internal/viewrender/node_loop.go b/internal/viewrender/node_loop.go index 8ab4e7ff..30944530 100644 --- a/internal/viewrender/node_loop.go +++ b/internal/viewrender/node_loop.go @@ -3,11 +3,12 @@ package viewrender import ( "encoding/json" "fmt" + "strconv" + "strings" + "github.com/cssbruno/gowdk/internal/clientlang" "github.com/cssbruno/gowdk/internal/viewparse" gowhtml "github.com/cssbruno/gowdk/runtime/html" - "strconv" - "strings" ) func renderTextNode(node Text, ctx *renderContext, out *renderOutput) error { diff --git a/internal/viewrender/patterns.go b/internal/viewrender/patterns.go index 066a470c..e94aca59 100644 --- a/internal/viewrender/patterns.go +++ b/internal/viewrender/patterns.go @@ -27,7 +27,6 @@ func (pattern viewPattern) FindStringSubmatch(source string) []string { var ( islandFieldPattern = viewPattern{match: isIdentifier} islandTextBindingPattern = viewPattern{submatch: parseIslandTextBinding} - forDirectivePattern = viewPattern{submatch: parseForDirectivePattern} contractReferencePattern = viewPattern{match: isContractReference} eventNamePattern = viewPattern{match: isEventName} stylePropertyPattern = viewPattern{match: isStyleProperty} @@ -46,52 +45,6 @@ func parseIslandTextBinding(source string) []string { return []string{source, name} } -func parseForDirectivePattern(source string) []string { - inStart, inEnd, ok := findForInOperator(source) - if !ok { - return nil - } - left := source[:inStart] - collection := strings.TrimSpace(source[inEnd:]) - if collection == "" { - return nil - } - item := strings.TrimSpace(left) - index := "" - if before, after, ok := strings.Cut(left, ","); ok { - item = strings.TrimSpace(before) - index = strings.TrimSpace(after) - if index == "" { - return nil - } - } - if !isIdentifier(item) || (index != "" && !isIdentifier(index)) { - return nil - } - return []string{source, item, index, collection} -} - -func findForInOperator(source string) (int, int, bool) { - for index := 0; index < len(source); index++ { - if source[index] != 'i' || index+2 > len(source) || source[index:index+2] != "in" { - continue - } - if index == 0 || index+2 == len(source) || !isPatternSpace(source[index-1]) || !isPatternSpace(source[index+2]) { - continue - } - start := index - 1 - for start > 0 && isPatternSpace(source[start-1]) { - start-- - } - end := index + 2 - for end < len(source) && isPatternSpace(source[end]) { - end++ - } - return start, end, true - } - return 0, 0, false -} - func isContractReference(source string) bool { parts := strings.Split(source, ".") if len(parts) < 2 { @@ -106,7 +59,7 @@ func isContractReference(source string) bool { } func isEventName(source string) bool { - if source == "" || !((source[0] >= 'A' && source[0] <= 'Z') || (source[0] >= 'a' && source[0] <= 'z')) { + if source == "" || (source[0] < 'A' || source[0] > 'Z') && (source[0] < 'a' || source[0] > 'z') { return false } for index := 1; index < len(source); index++ { @@ -147,7 +100,3 @@ func isStyleCustomProperty(source string) bool { } return true } - -func isPatternSpace(char byte) bool { - return char == ' ' || char == '\t' || char == '\n' || char == '\r' -} diff --git a/internal/viewrender/safety.go b/internal/viewrender/safety.go index 13f7223e..e9215bb3 100644 --- a/internal/viewrender/safety.go +++ b/internal/viewrender/safety.go @@ -5,29 +5,6 @@ import ( "strings" ) -func blockedViewElement(name string) bool { - return strings.EqualFold(strings.TrimSpace(name), "script") -} - -func validateParsedHTMLAttrSafety(attr Attr) error { - if inlineEventHandlerAttr(attr.Name) { - return fmt.Errorf("inline event handler attribute %q is not supported; use g:on:* inside stateful components", attr.Name) - } - if strings.EqualFold(strings.TrimSpace(attr.Name), "srcdoc") { - return fmt.Errorf("srcdoc attribute is not supported; use g:unsafe-html only with trusted sanitized HTML") - } - if attr.Boolean { - if urlBearingAttr(attr.Name) { - return fmt.Errorf("%q attributes require a URL value", attr.Name) - } - return nil - } - if strings.Contains(attr.Value, "{") { - return nil - } - return validateURLAttrValue(attr.Name, attr.Value) -} - func validateRenderedHTMLAttrSafety(name, value string) error { if inlineEventHandlerAttr(name) { return fmt.Errorf("inline event handler attribute %q is not supported; use g:on:* inside stateful components", name) @@ -107,8 +84,8 @@ func explicitURLScheme(value string) (string, bool) { return "", false } for index, char := range value { - switch { - case char == ':': + switch char { + case ':': if index == 0 { return "", false } @@ -117,7 +94,7 @@ func explicitURLScheme(value string) (string, bool) { return "", false } return strings.ToLower(candidate), true - case char == '/', char == '?', char == '#': + case '/', '?', '#': return "", false } } diff --git a/internal/viewrender/server_list.go b/internal/viewrender/server_list.go index 2bf271ea..dc76f658 100644 --- a/internal/viewrender/server_list.go +++ b/internal/viewrender/server_list.go @@ -113,8 +113,8 @@ func (ctx *renderContext) forDirectiveLane(node Element) (directiveLane, error) if attr.Name != "g:for" || attr.Boolean { continue } - loop, err := ParseForDirective(strings.TrimSpace(attr.Value)) - if err != nil { + loop, ok := parseForDirectiveLaneInput(attr.Value) + if !ok { return laneClient, nil } return ctx.resolveDirectiveLane(exprRootName(loop.Collection)) @@ -122,6 +122,14 @@ func (ctx *renderContext) forDirectiveLane(node Element) (directiveLane, error) return laneClient, nil } +func parseForDirectiveLaneInput(value string) (ForDirective, bool) { + loop, err := ParseForDirective(strings.TrimSpace(value)) + if err != nil { + return ForDirective{}, false + } + return loop, true +} + // ifDirectiveLane resolves the lane of an element's g:if from its condition's // root identifier (after stripping a leading server-style negation). func (ctx *renderContext) ifDirectiveLane(node Element) (directiveLane, error) { diff --git a/runtime/app/app.go b/runtime/app/app.go index a8709c25..31c2a721 100644 --- a/runtime/app/app.go +++ b/runtime/app/app.go @@ -793,7 +793,14 @@ func (handler Handler) health(response http.ResponseWriter) { if handler.Tracer != nil { payload["trace"] = handler.Tracer.HealthSnapshot() } - _ = json.NewEncoder(response).Encode(payload) + encoded, err := json.Marshal(payload) + if err != nil { + response.WriteHeader(http.StatusInternalServerError) + _, _ = response.Write([]byte(`{"status":"error"}` + "\n")) + return + } + encoded = append(encoded, '\n') + _, _ = response.Write(encoded) } func (handler Handler) SPAFile(requestPath string) ([]byte, fs.FileInfo, string, bool) { diff --git a/runtime/app/app_test.go b/runtime/app/app_test.go index 3fe0878f..bae736e4 100644 --- a/runtime/app/app_test.go +++ b/runtime/app/app_test.go @@ -1216,7 +1216,7 @@ func TestRecoverSSRRoutePanicAbortsAfterHeaders(t *testing.T) { return nil }() - if recovered != http.ErrAbortHandler { + if !isAbortHandlerPanic(recovered) { t.Fatalf("expected started response to abort the connection, got %v", recovered) } if recorder.Code != http.StatusAccepted { @@ -2127,7 +2127,8 @@ func TestHandlerRequestTimeoutCancelsSlowHandler(t *testing.T) { func TestActionFormRejectsInvalidForm(t *testing.T) { decode := func(values form.Values) (struct { Email string `form:"email"` - }, error) { + }, error, + ) { email, _, err := form.String(values, "email") return struct { Email string `form:"email"` @@ -2135,7 +2136,8 @@ func TestActionFormRejectsInvalidForm(t *testing.T) { } handler := ActionForm(decode, func(context.Context, struct { Email string `form:"email"` - }) (response.Response, error) { + }, + ) (response.Response, error) { t.Fatal("handler should not run for invalid form") return response.Response{}, nil }) @@ -2262,7 +2264,7 @@ func TestBoundaryRepanicsErrAbortHandler(t *testing.T) { handler(recorder, request) return nil }() - if recovered != http.ErrAbortHandler { + if !isAbortHandlerPanic(recovered) { t.Fatalf("expected http.ErrAbortHandler to propagate, got %v", recovered) } if logged != "" { @@ -2290,7 +2292,7 @@ func TestBoundaryAbortsConnectionAfterResponseStarted(t *testing.T) { handler(recorder, request) return nil }() - if recovered != http.ErrAbortHandler { + if !isAbortHandlerPanic(recovered) { t.Fatalf("expected started response to abort the connection, got %v", recovered) } if logged == "" { @@ -2348,7 +2350,7 @@ func TestTracedBackendRouteMarksServerStatusError(t *testing.T) { if !handler(recorder, request) { t.Fatal("expected backend handler to handle request") } - spans := waitForSpans(t, ring, 1) + spans := waitForSpans(t, ring) if len(spans) != 1 { t.Fatalf("spans = %d, want 1", len(spans)) } @@ -2383,7 +2385,7 @@ func TestTracedBackendRouteRecordsEndpointLanes(t *testing.T) { t.Fatal("expected backend handler to handle request") } - spans := waitForSpans(t, ring, 1) + spans := waitForSpans(t, ring) if len(spans) != 1 { t.Fatalf("spans = %d, want 1", len(spans)) } @@ -2445,7 +2447,7 @@ func TestTracedBackendRouteRecordsPanicAsRedactedError(t *testing.T) { _ = handler(recorder, request) }() - spans := waitForSpans(t, ring, 1) + spans := waitForSpans(t, ring) if len(spans) != 1 { t.Fatalf("spans = %d, want 1", len(spans)) } @@ -2466,12 +2468,12 @@ func spanAttr(span gowdktrace.Snapshot, key string) any { return nil } -func waitForSpans(t *testing.T, ring *gowdktrace.RingSink, want int) []gowdktrace.Snapshot { +func waitForSpans(t *testing.T, ring *gowdktrace.RingSink) []gowdktrace.Snapshot { t.Helper() deadline := time.Now().Add(time.Second) for { spans := ring.Spans() - if len(spans) >= want { + if len(spans) >= 1 { return spans } if time.Now().After(deadline) { @@ -2525,7 +2527,7 @@ func TestFinishHTTPTraceMarksServerStatusError(t *testing.T) { FinishHTTPTrace(recorder, span) - spans := waitForSpans(t, ring, 1) + spans := waitForSpans(t, ring) if len(spans) != 1 { t.Fatalf("spans = %d, want 1", len(spans)) } diff --git a/runtime/app/boundary.go b/runtime/app/boundary.go index 98d8cee1..31e9f69d 100644 --- a/runtime/app/boundary.go +++ b/runtime/app/boundary.go @@ -1,6 +1,7 @@ package app import ( + "errors" "fmt" "log" "net/http" @@ -39,7 +40,7 @@ func Boundary(kind string, handler HandlerFunc) HandlerFunc { wrappedWriter := wrapBoundaryResponseWriter(boundaryWriter) defer func() { if value := recover(); value != nil { - if value == http.ErrAbortHandler { + if isAbortHandlerPanic(value) { // Deliberate abort signal: let net/http kill the // connection without logging it as a failure. panic(value) @@ -128,7 +129,7 @@ func RecoverSSRRoutePanic(writer http.ResponseWriter, request *http.Request, val if value == nil { return } - if value == http.ErrAbortHandler { + if isAbortHandlerPanic(value) { panic(value) } logBoundaryPanic("ssr", value) @@ -144,7 +145,7 @@ func RecoverEndpointPanic(writer http.ResponseWriter, request *http.Request, val if value == nil { return } - if value == http.ErrAbortHandler { + if isAbortHandlerPanic(value) { panic(value) } logBoundaryPanic("endpoint", value) @@ -164,3 +165,8 @@ func boundaryKindLabel(kind string) string { return kind } } + +func isAbortHandlerPanic(value any) bool { + err, ok := value.(error) + return ok && errors.Is(err, http.ErrAbortHandler) +} diff --git a/runtime/app/cors_vary_test.go b/runtime/app/cors_vary_test.go index 08d5682e..0e3cef29 100644 --- a/runtime/app/cors_vary_test.go +++ b/runtime/app/cors_vary_test.go @@ -67,8 +67,8 @@ func TestCORSPreflightVaryMergesExistingValuesCaseInsensitively(t *testing.T) { t.Fatal("expected preflight to be handled") } assertVaryTokens(t, recorder.Header(), map[string]int{ - "accept-encoding": 1, - "origin": 1, + "accept-encoding": 1, + "origin": 1, "access-control-request-method": 1, "access-control-request-headers": 1, }) diff --git a/runtime/app/lifecycle.go b/runtime/app/lifecycle.go index 695f0d3d..73512667 100644 --- a/runtime/app/lifecycle.go +++ b/runtime/app/lifecycle.go @@ -186,7 +186,7 @@ func startServices(ctx context.Context, serviceContext ServiceContext, services done := make(chan struct{}) var wait sync.WaitGroup for _, service := range services { - service := service + wait.Add(1) go func() { defer wait.Done() diff --git a/runtime/app/listener.go b/runtime/app/listener.go index e054e653..6d026def 100644 --- a/runtime/app/listener.go +++ b/runtime/app/listener.go @@ -25,9 +25,9 @@ const inheritedListenerEnv = "GOWDK_LISTENER_FD" func inheritedListener() (net.Listener, error) { raw := strings.TrimSpace(os.Getenv(inheritedListenerEnv)) if raw == "" { - return nil, nil + return noInheritedListener() } - os.Unsetenv(inheritedListenerEnv) + _ = os.Unsetenv(inheritedListenerEnv) fd, err := strconv.Atoi(raw) if err != nil || fd < 0 { @@ -55,3 +55,8 @@ func inheritedListener() (net.Listener, error) { } return listener, nil } + +func noInheritedListener() (net.Listener, error) { + var listener net.Listener + return listener, nil +} diff --git a/runtime/app/listener_test.go b/runtime/app/listener_test.go index 40dfac5c..03d2f75d 100644 --- a/runtime/app/listener_test.go +++ b/runtime/app/listener_test.go @@ -9,7 +9,7 @@ func TestInheritedListenerWithoutEnv(t *testing.T) { t.Fatalf("unexpected error: %v", err) } if listener != nil { - listener.Close() + _ = listener.Close() t.Fatal("expected no inherited listener when GOWDK_LISTENER_FD is unset") } } @@ -21,7 +21,7 @@ func TestInheritedListenerInvalidFD(t *testing.T) { listener, err := inheritedListener() if err == nil { if listener != nil { - listener.Close() + _ = listener.Close() } t.Fatalf("expected error for GOWDK_LISTENER_FD=%q", value) } diff --git a/runtime/app/listener_unix_test.go b/runtime/app/listener_unix_test.go index 8a0e8288..2a2ee819 100644 --- a/runtime/app/listener_unix_test.go +++ b/runtime/app/listener_unix_test.go @@ -17,13 +17,17 @@ func TestInheritedListenerRoundTrip(t *testing.T) { if err != nil { t.Fatal(err) } - defer base.Close() + defer func() { + _ = base.Close() + }() file, err := base.(*net.TCPListener).File() if err != nil { t.Fatal(err) } - defer file.Close() + defer func() { + _ = file.Close() + }() // Hand inheritedListener its own duplicate so the descriptor it consumes and // closes is independent of the ones this test owns. @@ -40,7 +44,9 @@ func TestInheritedListenerRoundTrip(t *testing.T) { if got == nil { t.Fatal("expected a listener from an inherited descriptor") } - defer got.Close() + defer func() { + _ = got.Close() + }() if got.Addr().String() != base.Addr().String() { t.Fatalf("inherited listener addr = %q, want %q", got.Addr(), base.Addr()) @@ -53,7 +59,7 @@ func TestInheritedListenerRoundTrip(t *testing.T) { accepted <- err return } - conn.Close() + _ = conn.Close() accepted <- nil }() @@ -61,7 +67,7 @@ func TestInheritedListenerRoundTrip(t *testing.T) { if err != nil { t.Fatalf("dial inherited listener: %v", err) } - client.Close() + _ = client.Close() if err := <-accepted; err != nil { t.Fatalf("accept on inherited listener: %v", err) } diff --git a/runtime/app/response_writer_optional.go b/runtime/app/response_writer_optional.go index d24b0c41..dddb4231 100644 --- a/runtime/app/response_writer_optional.go +++ b/runtime/app/response_writer_optional.go @@ -99,6 +99,7 @@ type boundaryHijackerPusher struct{ *boundaryResponseWriter } func (writer boundaryHijackerPusher) Hijack() (net.Conn, *bufio.ReadWriter, error) { return writer.hijack() } + func (writer boundaryHijackerPusher) Push(target string, options *http.PushOptions) error { return writer.push(target, options) } @@ -109,6 +110,7 @@ func (writer boundaryFlusherHijackerPusher) Flush() { writer.flush() } func (writer boundaryFlusherHijackerPusher) Hijack() (net.Conn, *bufio.ReadWriter, error) { return writer.hijack() } + func (writer boundaryFlusherHijackerPusher) Push(target string, options *http.PushOptions) error { return writer.push(target, options) } @@ -184,6 +186,7 @@ type traceHijackerPusher struct{ *traceResponseWriter } func (writer traceHijackerPusher) Hijack() (net.Conn, *bufio.ReadWriter, error) { return writer.hijack() } + func (writer traceHijackerPusher) Push(target string, options *http.PushOptions) error { return writer.push(target, options) } @@ -194,6 +197,7 @@ func (writer traceFlusherHijackerPusher) Flush() { writer.flush() } func (writer traceFlusherHijackerPusher) Hijack() (net.Conn, *bufio.ReadWriter, error) { return writer.hijack() } + func (writer traceFlusherHijackerPusher) Push(target string, options *http.PushOptions) error { return writer.push(target, options) } diff --git a/runtime/contracts/concurrency_test.go b/runtime/contracts/concurrency_test.go index d8c6cadd..f4779f38 100644 --- a/runtime/contracts/concurrency_test.go +++ b/runtime/contracts/concurrency_test.go @@ -9,15 +9,17 @@ import ( "time" ) -type raceQueryA struct{} -type raceQueryB struct{} -type raceCommandA struct{} -type raceCommandB struct{} -type raceEventA struct{} -type raceEventB struct{} -type raceJobA struct{} -type raceJobB struct{} -type raceResult struct{} +type ( + raceQueryA struct{} + raceQueryB struct{} + raceCommandA struct{} + raceCommandB struct{} + raceEventA struct{} + raceEventB struct{} + raceJobA struct{} + raceJobB struct{} + raceResult struct{} +) func TestRegistryConcurrentRegistrationAndReads(t *testing.T) { registry := NewRegistry() @@ -65,7 +67,7 @@ func TestRegistryConcurrentRegistrationAndReads(t *testing.T) { var writers sync.WaitGroup for _, registration := range registrations { - registration := registration + writers.Add(1) go func() { defer writers.Done() @@ -93,7 +95,7 @@ func TestMemorySeenStoreConcurrentAccessEvictsSafely(t *testing.T) { var done sync.WaitGroup for worker := 0; worker < workers; worker++ { - worker := worker + done.Add(1) go func() { defer done.Done() diff --git a/runtime/contracts/fileoutbox/fileoutbox.go b/runtime/contracts/fileoutbox/fileoutbox.go index 703e788e..f14e56fe 100644 --- a/runtime/contracts/fileoutbox/fileoutbox.go +++ b/runtime/contracts/fileoutbox/fileoutbox.go @@ -285,7 +285,9 @@ func (store *Store) readRecordsFromPathLocked(path string) ([]Record, error) { } return nil, err } - defer file.Close() + defer func() { + _ = file.Close() + }() var records []Record scanner := bufio.NewScanner(file) diff --git a/runtime/contracts/fileoutbox/fileoutbox_test.go b/runtime/contracts/fileoutbox/fileoutbox_test.go index 56c72c7e..6ad1730e 100644 --- a/runtime/contracts/fileoutbox/fileoutbox_test.go +++ b/runtime/contracts/fileoutbox/fileoutbox_test.go @@ -468,12 +468,12 @@ func TestReceiveEventBatchDoesNotDuplicateDeadLetterAfterPendingRewriteFailure(t renameErr := errors.New("pending rewrite failed") var renames int - store.rename = func(old, new string) error { + store.rename = func(oldName, newName string) error { renames++ if renames == 2 { return renameErr } - return os.Rename(old, new) + return os.Rename(oldName, newName) } first, err := store.ReceiveEventBatch(context.Background()) diff --git a/runtime/contracts/fileoutbox/seenstore.go b/runtime/contracts/fileoutbox/seenstore.go index 535f8a55..919d29a6 100644 --- a/runtime/contracts/fileoutbox/seenstore.go +++ b/runtime/contracts/fileoutbox/seenstore.go @@ -156,7 +156,9 @@ func (store *SeenStore) readRecordsLocked() ([]SeenRecord, error) { } return nil, err } - defer file.Close() + defer func() { + _ = file.Close() + }() var records []SeenRecord scanner := bufio.NewScanner(file) diff --git a/runtime/contracts/schedule.go b/runtime/contracts/schedule.go index 6a76fab6..b7e1c68a 100644 --- a/runtime/contracts/schedule.go +++ b/runtime/contracts/schedule.go @@ -64,7 +64,7 @@ func RunScheduledJobs(ctx context.Context, jobs []ScheduledJob) error { errs := make(chan error, len(parsed)) var wait sync.WaitGroup for _, job := range parsed { - job := job + wait.Add(1) go func() { defer wait.Done() diff --git a/runtime/contracts/sse/sse.go b/runtime/contracts/sse/sse.go index 036281f7..a54f7f3c 100644 --- a/runtime/contracts/sse/sse.go +++ b/runtime/contracts/sse/sse.go @@ -285,12 +285,6 @@ func (hub *Hub) recordReplayLocked(message sseMessage) { } } -func (hub *Hub) replayAfter(lastEventID string, clientAudience map[string]struct{}) []sseMessage { - hub.mu.Lock() - defer hub.mu.Unlock() - return hub.replayAfterLocked(lastEventID, clientAudience) -} - func (hub *Hub) replayAfterLocked(lastEventID string, clientAudience map[string]struct{}) []sseMessage { lastEventID = strings.TrimSpace(lastEventID) if lastEventID == "" || hub.replayLimit <= 0 { diff --git a/runtime/contracts/sse/sse_test.go b/runtime/contracts/sse/sse_test.go index 7d1167b8..2934b6dd 100644 --- a/runtime/contracts/sse/sse_test.go +++ b/runtime/contracts/sse/sse_test.go @@ -3,6 +3,7 @@ package sse import ( "bufio" "context" + "errors" "io" "net/http" "net/http/httptest" @@ -26,7 +27,9 @@ func TestHubSendsRetryDirective(t *testing.T) { if err != nil { t.Fatal(err) } - defer response.Body.Close() + defer func() { + _ = response.Body.Close() + }() line, err := bufio.NewReader(response.Body).ReadString('\n') if err != nil { @@ -46,7 +49,9 @@ func TestHubSendsConfiguredRetryDirective(t *testing.T) { if err != nil { t.Fatal(err) } - defer response.Body.Close() + defer func() { + _ = response.Body.Close() + }() line, err := bufio.NewReader(response.Body).ReadString('\n') if err != nil { @@ -66,7 +71,9 @@ func TestHubStreamsPresentationEvents(t *testing.T) { if err != nil { t.Fatal(err) } - defer response.Body.Close() + defer func() { + _ = response.Body.Close() + }() deadline := time.Now().Add(2 * time.Second) for hub.ClientCount() == 0 && time.Now().Before(deadline) { @@ -120,12 +127,16 @@ func TestHubFiltersPresentationEventsByAudience(t *testing.T) { if err != nil { t.Fatal(err) } - defer broadcast.Body.Close() + defer func() { + _ = broadcast.Body.Close() + }() ada, err := http.Get(server.URL + "?client=ada") if err != nil { t.Fatal(err) } - defer ada.Body.Close() + defer func() { + _ = ada.Body.Close() + }() deadline := time.Now().Add(2 * time.Second) for hub.ClientCount() < 2 && time.Now().Before(deadline) { @@ -204,7 +215,9 @@ func TestHubReplaysEventsAfterLastEventID(t *testing.T) { if err != nil { t.Fatal(err) } - defer response.Body.Close() + defer func() { + _ = response.Body.Close() + }() reader := bufio.NewReader(response.Body) deadline := time.Now().Add(2 * time.Second) @@ -259,7 +272,9 @@ func TestHubReplayKeepsAudienceScope(t *testing.T) { if err != nil { t.Fatal(err) } - defer response.Body.Close() + defer func() { + _ = response.Body.Close() + }() data := readSSEDataLines(t, response.Body, 1)[0] if !strings.Contains(data, `"id":"private-clinic"`) || strings.Contains(data, "private-other") { @@ -368,7 +383,7 @@ func TestHubReturnsContextError(t *testing.T) { Type: "PatientNotice", Value: patientNotice{ID: "patient-1"}, }}) - if err != context.Canceled { + if !errors.Is(err, context.Canceled) { t.Fatalf("SendPresentationEvents error = %v, want context canceled", err) } } diff --git a/runtime/envfile/envfile.go b/runtime/envfile/envfile.go index 7204a402..fa8ea20e 100644 --- a/runtime/envfile/envfile.go +++ b/runtime/envfile/envfile.go @@ -107,7 +107,9 @@ func ParseFile(path string) ([]Entry, error) { if err != nil { return nil, err } - defer file.Close() + defer func() { + _ = file.Close() + }() var entries []Entry scanner := bufio.NewScanner(file) diff --git a/runtime/guard/guard.go b/runtime/guard/guard.go index 18ec6afb..3e545a55 100644 --- a/runtime/guard/guard.go +++ b/runtime/guard/guard.go @@ -32,19 +32,26 @@ func RunGuardsWithAuth(ctx Context, names []string, registry Registry, provider if guard == nil { return fmt.Errorf("guard %q is not registered", name) } - guardCtx, span := startGuardTrace(ctx, name) - ctx.Context = guardCtx - if err := guard(ctx); err != nil { - if span != nil { - span.SetStatus(gowdktrace.StatusError, security.RedactSecrets(err.Error())) - span.End() - } - return fmt.Errorf("guard %q failed: %w", name, err) + if err := runGuard(ctx, name, guard); err != nil { + return err } + } + return nil +} + +func runGuard(ctx Context, name string, guard Func) error { + guardCtx, span := startGuardTrace(ctx, name) + ctx.Context = guardCtx + if err := guard(ctx); err != nil { if span != nil { - span.SetStatus(gowdktrace.StatusOK, "") + span.SetStatus(gowdktrace.StatusError, security.RedactSecrets(err.Error())) span.End() } + return fmt.Errorf("guard %q failed: %w", name, err) + } + if span != nil { + span.SetStatus(gowdktrace.StatusOK, "") + span.End() } return nil } diff --git a/runtime/guard/guard_test.go b/runtime/guard/guard_test.go index 59a869a3..5fb66267 100644 --- a/runtime/guard/guard_test.go +++ b/runtime/guard/guard_test.go @@ -20,15 +20,22 @@ func (invalidTraceIDGenerator) NewTraceID() gowdktrace.TraceID { return "" } func (invalidTraceIDGenerator) NewSpanID() gowdktrace.SpanID { return "" } +type guardTestContextKey string + +func unauthenticatedGuardPrincipal(*http.Request) (*gowdkauth.Principal, error) { + var principal *gowdkauth.Principal + return principal, nil +} + func TestNewContextCarriesRequestContext(t *testing.T) { - request, err := http.NewRequestWithContext(context.WithValue(context.Background(), "trace", "abc"), http.MethodGet, "/dashboard", nil) + request, err := http.NewRequestWithContext(context.WithValue(context.Background(), guardTestContextKey("trace"), "abc"), http.MethodGet, "/dashboard", nil) if err != nil { t.Fatal(err) } ctx := NewContext(request, map[string]any{"user": "Ada"}) - if ctx.Request != request || ctx.Context.Value("trace") != "abc" || ctx.Session["user"] != "Ada" { + if ctx.Request != request || ctx.Context.Value(guardTestContextKey("trace")) != "abc" || ctx.Session["user"] != "Ada" { t.Fatalf("unexpected guard context: %#v", ctx) } } @@ -133,9 +140,7 @@ func TestRunGuardsWithAuthFailsClosedForNativeRBACGuards(t *testing.T) { t.Fatalf("expected missing auth provider error, got %v", err) } - if err := RunGuardsWithAuth(Context{}, []string{"role:admin"}, nil, gowdkauth.ProviderFunc(func(*http.Request) (*gowdkauth.Principal, error) { - return nil, nil - })); !errors.Is(err, gowdkauth.ErrUnauthenticated) { + if err := RunGuardsWithAuth(Context{}, []string{"role:admin"}, nil, gowdkauth.ProviderFunc(unauthenticatedGuardPrincipal)); !errors.Is(err, gowdkauth.ErrUnauthenticated) { t.Fatalf("expected unauthenticated error, got %v", err) } @@ -237,10 +242,8 @@ func TestWriteNoStoreFailureRedactsGuardRuntimeErrors(t *testing.T) { blocked: []string{"role:admin", "requires an auth provider"}, }, { - name: "native unauthenticated", - err: RunGuardsWithAuth(NewContext(request, nil), []string{"role:admin"}, nil, gowdkauth.ProviderFunc(func(*http.Request) (*gowdkauth.Principal, error) { - return nil, nil - })), + name: "native unauthenticated", + err: RunGuardsWithAuth(NewContext(request, nil), []string{"role:admin"}, nil, gowdkauth.ProviderFunc(unauthenticatedGuardPrincipal)), blocked: []string{"role:admin", gowdkauth.ErrUnauthenticated.Error()}, }, { diff --git a/runtime/ratelimit/concurrency_test.go b/runtime/ratelimit/concurrency_test.go index 8cce9a43..8b6aadc1 100644 --- a/runtime/ratelimit/concurrency_test.go +++ b/runtime/ratelimit/concurrency_test.go @@ -15,7 +15,7 @@ func TestInMemoryStoreConcurrentTakeAndCleanup(t *testing.T) { var done sync.WaitGroup for worker := 0; worker < workers; worker++ { - worker := worker + done.Add(1) go func() { defer done.Done() diff --git a/runtime/response/response_test.go b/runtime/response/response_test.go index 5cf24d19..bbeeefe5 100644 --- a/runtime/response/response_test.go +++ b/runtime/response/response_test.go @@ -392,7 +392,9 @@ func TestWriteHTTPSkipsBodyForNoBodyStatuses(t *testing.T) { if err != nil { t.Fatal(err) } - defer resp.Body.Close() + defer func() { + _ = resp.Body.Close() + }() if resp.StatusCode != status { t.Fatalf("status = %d, want %d", resp.StatusCode, status) diff --git a/runtime/ssr/list.go b/runtime/ssr/list.go index 13d04675..9583775a 100644 --- a/runtime/ssr/list.go +++ b/runtime/ssr/list.go @@ -168,7 +168,7 @@ func evalExpr(expr ast.Expr, container any) (any, error) { case "false": return false, nil case "nil": - return nil, nil + return nilEvalValue(), nil default: value, ok := ElementPath(container, typed.Name) if !ok { @@ -236,6 +236,10 @@ func evalExpr(expr ast.Expr, container any) (any, error) { } } +func nilEvalValue() any { + return nil +} + func evalBasicLit(expr *ast.BasicLit) (any, error) { switch expr.Kind { case token.STRING: @@ -324,8 +328,7 @@ func evalCallExpr(expr *ast.CallExpr, container any) (any, error) { if err != nil { return nil, err } - switch typed := value.(type) { - case string: + if typed, ok := value.(string); ok { return len(typed), nil } reflected := reflect.ValueOf(value) @@ -484,8 +487,7 @@ func convertValue(kind string, value any) (any, error) { } func normalizeEvalValue(value any) any { - switch typed := value.(type) { - case json.Number: + if typed, ok := value.(json.Number); ok { return typed } reflected := reflect.ValueOf(value) diff --git a/runtime/ssr/list_test.go b/runtime/ssr/list_test.go index f0361a1a..a45d6e56 100644 --- a/runtime/ssr/list_test.go +++ b/runtime/ssr/list_test.go @@ -191,10 +191,14 @@ func TestTruthy(t *testing.T) { value any want bool }{ - {true, true}, {false, false}, - {"x", true}, {"", false}, - {1, true}, {0, false}, - {[]any{1}, true}, {[]any{}, false}, + {true, true}, + {false, false}, + {"x", true}, + {"", false}, + {1, true}, + {0, false}, + {[]any{1}, true}, + {[]any{}, false}, {nil, false}, } for _, tc := range cases { diff --git a/runtime/ssr/regions_test.go b/runtime/ssr/regions_test.go index 155379ef..f670bd7d 100644 --- a/runtime/ssr/regions_test.go +++ b/runtime/ssr/regions_test.go @@ -213,7 +213,7 @@ func TestRenderInvalidatedRegionsSkipsFormmethodPostControls(t *testing.T) { func TestRegisterRegionIgnoresIncomplete(t *testing.T) { resetRegions() defer resetRegions() - RegisterRegion(RegionRenderer{QueryType: "", Load: func(*http.Request) (map[string]any, error) { return nil, nil }}) + RegisterRegion(RegionRenderer{QueryType: "", Load: func(*http.Request) (map[string]any, error) { return map[string]any{}, nil }}) RegisterRegion(RegionRenderer{QueryType: "example.com/app.Q", Load: nil}) if patches := RenderInvalidatedRegions(httptest.NewRequest(http.MethodPost, "/", nil), []string{"example.com/app.Q"}); len(patches) != 0 { t.Fatalf("expected incomplete renderers to be ignored, got %+v", patches) diff --git a/runtime/ssr/ssr_test.go b/runtime/ssr/ssr_test.go index 24b7ada8..41be9bce 100644 --- a/runtime/ssr/ssr_test.go +++ b/runtime/ssr/ssr_test.go @@ -11,8 +11,15 @@ import ( gowdkauth "github.com/cssbruno/gowdk/runtime/auth" ) +type ssrTestContextKey string + +func unauthenticatedSSRPrincipal(*http.Request) (*gowdkauth.Principal, error) { + var principal *gowdkauth.Principal + return principal, nil +} + func TestLoadFuncContract(t *testing.T) { - request, err := http.NewRequestWithContext(context.WithValue(context.Background(), "trace", "abc"), http.MethodGet, "/dashboard", nil) + request, err := http.NewRequestWithContext(context.WithValue(context.Background(), ssrTestContextKey("trace"), "abc"), http.MethodGet, "/dashboard", nil) if err != nil { t.Fatal(err) } @@ -20,7 +27,7 @@ func TestLoadFuncContract(t *testing.T) { load := LoadFunc(func(ctx LoadContext) (map[string]any, error) { return map[string]any{ "name": "GOWDK", - "trace": ctx.Context.Value("trace"), + "trace": ctx.Context.Value(ssrTestContextKey("trace")), "path": ctx.Request.URL.Path, "userID": ctx.Session["userID"], }, nil @@ -219,9 +226,7 @@ func TestRunGuardsWithAuthFailsClosedForNativeRBACGuards(t *testing.T) { t.Fatalf("expected missing auth provider error, got %v", err) } - if err := RunGuardsWithAuth(LoadContext{}, []string{"role:admin"}, nil, gowdkauth.ProviderFunc(func(*http.Request) (*gowdkauth.Principal, error) { - return nil, nil - })); !errors.Is(err, gowdkauth.ErrUnauthenticated) { + if err := RunGuardsWithAuth(LoadContext{}, []string{"role:admin"}, nil, gowdkauth.ProviderFunc(unauthenticatedSSRPrincipal)); !errors.Is(err, gowdkauth.ErrUnauthenticated) { t.Fatalf("expected unauthenticated error, got %v", err) } diff --git a/runtime/testkit/contracts.go b/runtime/testkit/contracts.go index 4cc39aad..813b0a46 100644 --- a/runtime/testkit/contracts.go +++ b/runtime/testkit/contracts.go @@ -19,18 +19,18 @@ func ContractRegistry(register ...func(*contracts.Registry)) *contracts.Registry } // CaptureCommandEvents runs a command and fails the test on command errors. -func CaptureCommandEvents[C, R any](t testing.TB, registry *contracts.Registry, command C) (R, []contracts.EventEnvelope) { - t.Helper() +func CaptureCommandEvents[C, R any](tb testing.TB, registry *contracts.Registry, command C) (R, []contracts.EventEnvelope) { + tb.Helper() result, events, err := contracts.CaptureCommandEvents[C, R](context.Background(), registry, command) if err != nil { - t.Fatalf("capture command events: %v", err) + tb.Fatalf("capture command events: %v", err) } return result, events } // AssertEmitted finds one captured event with the requested category and type. -func AssertEmitted[E any](t testing.TB, events []contracts.EventEnvelope, category contracts.EventCategory, check func(E)) { - t.Helper() +func AssertEmitted[E any](tb testing.TB, events []contracts.EventEnvelope, category contracts.EventCategory, check func(E)) { + tb.Helper() wantType := contracts.ContractName[E]() for _, event := range events { if event.Category != category || event.Type != wantType { @@ -39,20 +39,20 @@ func AssertEmitted[E any](t testing.TB, events []contracts.EventEnvelope, catego value, ok := event.Value.(E) if !ok { var zero E - t.Fatalf("captured %s event %s has value type %T, want %T", category, event.Type, event.Value, zero) + tb.Fatalf("captured %s event %s has value type %T, want %T", category, event.Type, event.Value, zero) } if check != nil { check(value) } return } - t.Fatalf("missing captured %s event %s in %#v", category, wantType, events) + tb.Fatalf("missing captured %s event %s in %#v", category, wantType, events) } // AssertNoEvents fails when a command unexpectedly emitted events. -func AssertNoEvents(t testing.TB, events []contracts.EventEnvelope) { - t.Helper() +func AssertNoEvents(tb testing.TB, events []contracts.EventEnvelope) { + tb.Helper() if len(events) != 0 { - t.Fatalf("expected no captured events, got %#v", events) + tb.Fatalf("expected no captured events, got %#v", events) } } diff --git a/runtime/testkit/contracts_test.go b/runtime/testkit/contracts_test.go index 6b3b10de..4b3c366f 100644 --- a/runtime/testkit/contracts_test.go +++ b/runtime/testkit/contracts_test.go @@ -21,12 +21,14 @@ type testEvent struct { func TestContractRegistryCaptureAndAssertEmitted(t *testing.T) { registry := ContractRegistry(func(registry *contracts.Registry) { - contracts.RegisterCommand[testCommand, testCommandResult](registry, func(ctx context.Context, command testCommand) (testCommandResult, error) { + if err := contracts.RegisterCommand[testCommand, testCommandResult](registry, func(ctx context.Context, command testCommand) (testCommandResult, error) { if err := contracts.EmitDomain(ctx, testEvent{ID: "event-1"}); err != nil { return testCommandResult{}, err } return testCommandResult{ID: command.Name}, nil - }, contracts.RoleWeb) + }, contracts.RoleWeb); err != nil { + t.Fatal(err) + } }) result, events := CaptureCommandEvents[testCommand, testCommandResult](t, registry, testCommand{Name: "command-1"}) diff --git a/runtime/testkit/testkit.go b/runtime/testkit/testkit.go index 13cf0e19..31fb9965 100644 --- a/runtime/testkit/testkit.go +++ b/runtime/testkit/testkit.go @@ -102,34 +102,34 @@ func (request Request) WithQuery(name, value string) Request { // NewClient creates a cookie-preserving client that executes requests directly // against handler without opening a listener. -func NewClient(t testing.TB, handler http.Handler) *Client { - t.Helper() +func NewClient(tb testing.TB, handler http.Handler) *Client { + tb.Helper() if handler == nil { - t.Fatal("testkit client requires a handler") + tb.Fatal("testkit client requires a handler") } jar, err := cookiejar.New(nil) if err != nil { - t.Fatalf("create cookie jar: %v", err) + tb.Fatalf("create cookie jar: %v", err) } return &Client{handler: handler, jar: jar, baseURL: "http://gowdk.test"} } // NewServerClient creates a cookie-preserving client backed by an httptest // server. Use this when behavior depends on the HTTP transport. -func NewServerClient(t testing.TB, handler http.Handler) *Client { - t.Helper() +func NewServerClient(tb testing.TB, handler http.Handler) *Client { + tb.Helper() if handler == nil { - t.Fatal("testkit server client requires a handler") + tb.Fatal("testkit server client requires a handler") } jar, err := cookiejar.New(nil) if err != nil { - t.Fatalf("create cookie jar: %v", err) + tb.Fatalf("create cookie jar: %v", err) } server := httptest.NewServer(handler) httpClient := server.Client() httpClient.Jar = jar client := &Client{server: server, httpClient: httpClient, jar: jar, baseURL: server.URL} - t.Cleanup(client.Close) + tb.Cleanup(client.Close) return client } @@ -150,132 +150,132 @@ func (client *Client) Close() { } // Do executes one request and returns the captured result. -func (client *Client) Do(t testing.TB, request Request) Result { - t.Helper() +func (client *Client) Do(tb testing.TB, request Request) Result { + tb.Helper() if client == nil { - t.Fatal("testkit client is nil") + tb.Fatal("testkit client is nil") + return Result{} } if client.server != nil { - return client.doServer(t, request) + return client.doServer(tb, request) } - return client.doDirect(t, request) + return client.doDirect(tb, request) } // Get executes a GET request. -func (client *Client) Get(t testing.TB, path string) Result { - t.Helper() - return client.Do(t, Get(path)) +func (client *Client) Get(tb testing.TB, path string) Result { + tb.Helper() + return client.Do(tb, Get(path)) } // PostForm executes a form POST request. -func (client *Client) PostForm(t testing.TB, path string, form url.Values) Result { - t.Helper() - return client.Do(t, PostForm(path, form)) +func (client *Client) PostForm(tb testing.TB, path string, form url.Values) Result { + tb.Helper() + return client.Do(tb, PostForm(path, form)) } // PostJSON executes a JSON POST request. -func (client *Client) PostJSON(t testing.TB, path string, value any) Result { - t.Helper() - return client.Do(t, PostJSON(path, value)) +func (client *Client) PostJSON(tb testing.TB, path string, value any) Result { + tb.Helper() + return client.Do(tb, PostJSON(path, value)) } // AssertStatus checks the response status code. -func (result Result) AssertStatus(t testing.TB, want int) { - t.Helper() +func (result Result) AssertStatus(tb testing.TB, want int) { + tb.Helper() if result.StatusCode != want { - t.Fatalf("status = %d, want %d with body %s", result.StatusCode, want, responseBodySummary(result.Body)) + tb.Fatalf("status = %d, want %d with body %s", result.StatusCode, want, responseBodySummary(result.Body)) } } -// AssertStatusRange checks that the response status code is within [min, max]. -func (result Result) AssertStatusRange(t testing.TB, min, max int) { - t.Helper() - if result.StatusCode < min || result.StatusCode > max { - t.Fatalf("status = %d, want range %d..%d with body %s", result.StatusCode, min, max, responseBodySummary(result.Body)) +// AssertStatusRange checks that the response status code is within [minimum, maximum]. +func (result Result) AssertStatusRange(tb testing.TB, minimum, maximum int) { + tb.Helper() + if result.StatusCode < minimum || result.StatusCode > maximum { + tb.Fatalf("status = %d, want range %d..%d with body %s", result.StatusCode, minimum, maximum, responseBodySummary(result.Body)) } } // AssertHeader checks one exact response header value. -func (result Result) AssertHeader(t testing.TB, name, want string) { - t.Helper() +func (result Result) AssertHeader(tb testing.TB, name, want string) { + tb.Helper() if got := result.Header.Get(name); got != want { - t.Fatalf("header %s = %q, want %q", name, got, want) + tb.Fatalf("header %s = %q, want %q", name, got, want) } } // AssertHeaderContains checks that one response header contains text. -func (result Result) AssertHeaderContains(t testing.TB, name, want string) { - t.Helper() +func (result Result) AssertHeaderContains(tb testing.TB, name, want string) { + tb.Helper() if got := result.Header.Get(name); !strings.Contains(got, want) { - t.Fatalf("header %s = %q, want it to contain %q", name, got, want) + tb.Fatalf("header %s = %q, want it to contain %q", name, got, want) } } // AssertContentType checks the response Content-Type prefix. -func (result Result) AssertContentType(t testing.TB, want string) { - t.Helper() +func (result Result) AssertContentType(tb testing.TB, want string) { + tb.Helper() if got := result.Header.Get("Content-Type"); !strings.HasPrefix(got, want) { - t.Fatalf("content type = %q, want prefix %q", got, want) + tb.Fatalf("content type = %q, want prefix %q", got, want) } } // AssertCookie checks that a response Set-Cookie with name exists. -func (result Result) AssertCookie(t testing.TB, name string) *http.Cookie { - t.Helper() +func (result Result) AssertCookie(tb testing.TB, name string) *http.Cookie { + tb.Helper() for _, cookie := range result.Cookies { if cookie.Name == name { return cookie } } - t.Fatalf("missing Set-Cookie %q in %#v", name, result.Cookies) + tb.Fatalf("missing Set-Cookie %q in %#v", name, result.Cookies) return nil } // AssertBodyEquals checks the complete response body. -func (result Result) AssertBodyEquals(t testing.TB, want string) { - t.Helper() +func (result Result) AssertBodyEquals(tb testing.TB, want string) { + tb.Helper() if result.Body != want { - t.Fatalf("body = %q, want %q", result.Body, want) + tb.Fatalf("body = %q, want %q", result.Body, want) } } // AssertBodyContains checks that the response body contains text. -func (result Result) AssertBodyContains(t testing.TB, want string) { - t.Helper() +func (result Result) AssertBodyContains(tb testing.TB, want string) { + tb.Helper() if !strings.Contains(result.Body, want) { - t.Fatalf("body does not contain %q: %s", want, responseBodySummary(result.Body)) + tb.Fatalf("body does not contain %q: %s", want, responseBodySummary(result.Body)) } } // AssertJSONEqual compares the response body with want after JSON // normalization. -func (result Result) AssertJSONEqual(t testing.TB, want any) { - t.Helper() +func (result Result) AssertJSONEqual(tb testing.TB, want any) { + tb.Helper() var gotValue any if err := json.Unmarshal([]byte(result.Body), &gotValue); err != nil { - t.Fatalf("decode response JSON: %v with body %s", err, responseBodySummary(result.Body)) + tb.Fatalf("decode response JSON: %v with body %s", err, responseBodySummary(result.Body)) } wantPayload, err := json.Marshal(want) if err != nil { - t.Fatalf("encode expected JSON: %v", err) + tb.Fatalf("encode expected JSON: %v", err) } var wantValue any if err := json.Unmarshal(wantPayload, &wantValue); err != nil { - t.Fatalf("decode expected JSON: %v", err) + tb.Fatalf("decode expected JSON: %v", err) } if !reflect.DeepEqual(gotValue, wantValue) { - t.Fatalf("JSON response = %#v, want %#v", gotValue, wantValue) + tb.Fatalf("JSON response = %#v, want %#v", gotValue, wantValue) } } // Run executes scenarios against handler. -func Run(t testing.TB, handler http.Handler, scenarios []Scenario) { - t.Helper() - if runner, ok := t.(interface { +func Run(tb testing.TB, handler http.Handler, scenarios []Scenario) { + tb.Helper() + if runner, ok := tb.(interface { Run(string, func(*testing.T)) bool }); ok { for _, scenario := range scenarios { - scenario := scenario runner.Run(scenarioName(scenario), func(t *testing.T) { t.Helper() runScenario(t, handler, scenario) @@ -284,30 +284,30 @@ func Run(t testing.TB, handler http.Handler, scenarios []Scenario) { return } for _, scenario := range scenarios { - runScenario(t, handler, scenario) + runScenario(tb, handler, scenario) } } -func runScenario(t testing.TB, handler http.Handler, scenario Scenario) { - t.Helper() +func runScenario(tb testing.TB, handler http.Handler, scenario Scenario) { + tb.Helper() response := Response(handler, scenario) if scenario.WantStatus != 0 && response.Code != scenario.WantStatus { - t.Errorf("expected status %d, got %d with body %s", scenario.WantStatus, response.Code, responseBodySummary(response.Body.String())) + tb.Errorf("expected status %d, got %d with body %s", scenario.WantStatus, response.Code, responseBodySummary(response.Body.String())) } for name, want := range scenario.WantHeader { if got := response.Header().Get(name); got != want { - t.Errorf("expected header %s=%q, got %q", name, want, got) + tb.Errorf("expected header %s=%q, got %q", name, want, got) } } if want := strings.TrimSpace(scenario.WantBodyContains); want != "" && !strings.Contains(response.Body.String(), want) { - t.Errorf("expected body to contain %q, got %s", want, responseBodySummary(response.Body.String())) + tb.Errorf("expected body to contain %q, got %s", want, responseBodySummary(response.Body.String())) } } // AssertStatus checks one request's response status. -func AssertStatus(t testing.TB, handler http.Handler, method, requestPath, body string, want int) { - t.Helper() - Run(t, handler, []Scenario{{ +func AssertStatus(tb testing.TB, handler http.Handler, method, requestPath, body string, want int) { + tb.Helper() + Run(tb, handler, []Scenario{{ Name: method + " " + requestPath, Method: method, Path: requestPath, @@ -317,9 +317,9 @@ func AssertStatus(t testing.TB, handler http.Handler, method, requestPath, body } // AssertHeader checks one response header value. -func AssertHeader(t testing.TB, handler http.Handler, method, requestPath, name, want string) { - t.Helper() - Run(t, handler, []Scenario{{ +func AssertHeader(tb testing.TB, handler http.Handler, method, requestPath, name, want string) { + tb.Helper() + Run(tb, handler, []Scenario{{ Name: method + " " + requestPath, Method: method, Path: requestPath, @@ -346,9 +346,9 @@ func Response(handler http.Handler, scenario Scenario) *httptest.ResponseRecorde return response } -func (client *Client) doDirect(t testing.TB, request Request) Result { - t.Helper() - httpRequest := client.newHTTPRequest(t, request) +func (client *Client) doDirect(tb testing.TB, request Request) Result { + tb.Helper() + httpRequest := client.newHTTPRequest(tb, request) for _, cookie := range client.jar.Cookies(httpRequest.URL) { httpRequest.AddCookie(cookie) } @@ -365,17 +365,19 @@ func (client *Client) doDirect(t testing.TB, request Request) Result { } } -func (client *Client) doServer(t testing.TB, request Request) Result { - t.Helper() - httpRequest := client.newHTTPRequest(t, request) +func (client *Client) doServer(tb testing.TB, request Request) Result { + tb.Helper() + httpRequest := client.newHTTPRequest(tb, request) response, err := client.httpClient.Do(httpRequest) if err != nil { - t.Fatalf("execute %s %s: %v", httpRequest.Method, httpRequest.URL.String(), err) + tb.Fatalf("execute %s %s: %v", httpRequest.Method, httpRequest.URL.String(), err) } - defer response.Body.Close() + defer func() { + _ = response.Body.Close() + }() body, err := io.ReadAll(response.Body) if err != nil { - t.Fatalf("read response body: %v", err) + tb.Fatalf("read response body: %v", err) } return Result{ StatusCode: response.StatusCode, @@ -386,21 +388,21 @@ func (client *Client) doServer(t testing.TB, request Request) Result { } } -func (client *Client) newHTTPRequest(t testing.TB, request Request) *http.Request { - t.Helper() +func (client *Client) newHTTPRequest(tb testing.TB, request Request) *http.Request { + tb.Helper() method := strings.TrimSpace(request.Method) if method == "" { method = http.MethodGet } - target := client.requestURL(t, request) - body, contentType := requestBody(t, request) + target := client.requestURL(tb, request) + body, contentType := requestBody(tb, request) ctx := request.Context if ctx == nil { ctx = context.Background() } httpRequest, err := http.NewRequestWithContext(ctx, method, target, body) if err != nil { - t.Fatalf("build request %s %s: %v", method, target, err) + tb.Fatalf("build request %s %s: %v", method, target, err) } if contentType != "" { httpRequest.Header.Set("Content-Type", contentType) @@ -420,8 +422,8 @@ func (client *Client) newHTTPRequest(t testing.TB, request Request) *http.Reques return httpRequest } -func (client *Client) requestURL(t testing.TB, request Request) string { - t.Helper() +func (client *Client) requestURL(tb testing.TB, request Request) string { + tb.Helper() baseURL := strings.TrimRight(client.baseURL, "/") if baseURL == "" { baseURL = "http://gowdk.test" @@ -432,7 +434,7 @@ func (client *Client) requestURL(t testing.TB, request Request) string { } parsed, err := url.Parse(requestPath) if err != nil { - t.Fatalf("parse request path %q: %v", requestPath, err) + tb.Fatalf("parse request path %q: %v", requestPath, err) } if !parsed.IsAbs() { if !strings.HasPrefix(requestPath, "/") { @@ -440,7 +442,7 @@ func (client *Client) requestURL(t testing.TB, request Request) string { } parsed, err = url.Parse(baseURL + requestPath) if err != nil { - t.Fatalf("parse request URL %q: %v", baseURL+requestPath, err) + tb.Fatalf("parse request URL %q: %v", baseURL+requestPath, err) } } query := parsed.Query() @@ -453,12 +455,12 @@ func (client *Client) requestURL(t testing.TB, request Request) string { return parsed.String() } -func requestBody(t testing.TB, request Request) (io.Reader, string) { - t.Helper() +func requestBody(tb testing.TB, request Request) (io.Reader, string) { + tb.Helper() if request.JSON != nil { payload, err := json.Marshal(request.JSON) if err != nil { - t.Fatalf("encode request JSON: %v", err) + tb.Fatalf("encode request JSON: %v", err) } return bytes.NewReader(payload), "application/json" } diff --git a/runtime/testkit/testkit_test.go b/runtime/testkit/testkit_test.go index 822a2065..3d9202a2 100644 --- a/runtime/testkit/testkit_test.go +++ b/runtime/testkit/testkit_test.go @@ -69,10 +69,12 @@ func TestClientBuildsJSONRequestsAndAssertions(t *testing.T) { } response.Header().Set("Content-Type", "application/json; charset=utf-8") response.Header().Set("X-GOWDK-Fragment-Target", "#result") - _ = json.NewEncoder(response).Encode(map[string]any{ + if err := json.NewEncoder(response).Encode(map[string]any{ "ok": true, "email": payload["email"], - }) + }); err != nil { + t.Fatalf("encode response: %v", err) + } }) client := NewClient(t, handler) diff --git a/runtime/trace/collector.go b/runtime/trace/collector.go index 2b35d0b9..c8a489c4 100644 --- a/runtime/trace/collector.go +++ b/runtime/trace/collector.go @@ -441,7 +441,9 @@ func collectorRateKey(request *http.Request) string { } func (collector *Collector) recordJSON(ctx context.Context, request *http.Request) error { - defer request.Body.Close() + defer func() { + _ = request.Body.Close() + }() payload, err := io.ReadAll(io.LimitReader(request.Body, maxCollectorBodyBytes+1)) if err != nil { return err diff --git a/runtime/trace/trace_test.go b/runtime/trace/trace_test.go index 8b8f23ae..c70c0e8c 100644 --- a/runtime/trace/trace_test.go +++ b/runtime/trace/trace_test.go @@ -363,7 +363,9 @@ func TestCollectorHandlerServesJSONAndSSE(t *testing.T) { if err != nil { t.Fatal(err) } - defer resp.Body.Close() + defer func() { + _ = resp.Body.Close() + }() if contentType := resp.Header.Get("Content-Type"); !strings.Contains(contentType, "text/event-stream") { t.Fatalf("expected event stream, got %q", contentType) } @@ -391,7 +393,7 @@ func TestCollectorAcceptsValidSingleAndBatchPayloads(t *testing.T) { t.Fatal(err) } response := httptest.NewRecorder() - handler.ServeHTTP(response, jsonPostRequest(http.MethodPost, "/", singlePayload)) + handler.ServeHTTP(response, jsonPostRequest("/", singlePayload)) if response.Code != http.StatusNoContent { t.Fatalf("single POST status = %d body=%q, want 204", response.Code, response.Body.String()) } @@ -401,7 +403,7 @@ func TestCollectorAcceptsValidSingleAndBatchPayloads(t *testing.T) { t.Fatal(err) } response = httptest.NewRecorder() - handler.ServeHTTP(response, jsonPostRequest(http.MethodPost, "/", batchPayload)) + handler.ServeHTTP(response, jsonPostRequest("/", batchPayload)) if response.Code != http.StatusNoContent { t.Fatalf("batch POST status = %d body=%q, want 204", response.Code, response.Body.String()) } @@ -427,7 +429,7 @@ func TestCollectorRejectsInvalidBatchWithoutPartialRecord(t *testing.T) { t.Fatal(err) } response := httptest.NewRecorder() - collector.Handler().ServeHTTP(response, jsonPostRequest(http.MethodPost, "/", payload)) + collector.Handler().ServeHTTP(response, jsonPostRequest("/", payload)) if response.Code != http.StatusBadRequest { t.Fatalf("POST status = %d body=%q, want 400", response.Code, response.Body.String()) } @@ -453,7 +455,7 @@ func TestCollectorRejectsAmbiguousOrOversizedPayloads(t *testing.T) { t.Run(tt.name, func(t *testing.T) { collector := trace.NewCollector(4) response := httptest.NewRecorder() - collector.Handler().ServeHTTP(response, jsonPostRequest(http.MethodPost, "/", []byte(tt.body))) + collector.Handler().ServeHTTP(response, jsonPostRequest("/", []byte(tt.body))) if response.Code != tt.status { t.Fatalf("POST status = %d body=%q, want %d", response.Code, response.Body.String(), tt.status) } @@ -514,7 +516,7 @@ func TestCollectorRejectsUnsupportedMethods(t *testing.T) { func TestCollectorRejectsCrossOriginBrowserIngest(t *testing.T) { collector := trace.NewCollector(4) payload := []byte(snapshotJSON(t, validSnapshot("browser"))) - request := jsonPostRequest(http.MethodPost, "http://trace.local/browser", payload) + request := jsonPostRequest("http://trace.local/browser", payload) request.Header.Set("Origin", "http://evil.local") response := httptest.NewRecorder() @@ -539,7 +541,7 @@ func TestCollectorAcceptsSameOriginAndMissingOriginBrowserIngest(t *testing.T) { t.Run(tt.name, func(t *testing.T) { collector := trace.NewCollector(4) payload := []byte(snapshotJSON(t, validSnapshot("browser"))) - request := jsonPostRequest(http.MethodPost, "http://trace.local/browser", payload) + request := jsonPostRequest("http://trace.local/browser", payload) if tt.origin != "" { request.Header.Set("Origin", tt.origin) } @@ -563,13 +565,13 @@ func TestCollectorRateLimitsIngest(t *testing.T) { payload := []byte(snapshotJSON(t, validSnapshot("limited"))) response := httptest.NewRecorder() - handler.ServeHTTP(response, jsonPostRequest(http.MethodPost, "/", payload)) + handler.ServeHTTP(response, jsonPostRequest("/", payload)) if response.Code != http.StatusNoContent { t.Fatalf("first POST status = %d body=%q, want 204", response.Code, response.Body.String()) } response = httptest.NewRecorder() - handler.ServeHTTP(response, jsonPostRequest(http.MethodPost, "/", payload)) + handler.ServeHTTP(response, jsonPostRequest("/", payload)) if response.Code != http.StatusTooManyRequests { t.Fatalf("second POST status = %d body=%q, want 429", response.Code, response.Body.String()) } @@ -590,7 +592,9 @@ func TestCollectorSSELimit(t *testing.T) { if err != nil { t.Fatal(err) } - defer first.Body.Close() + defer func() { + _ = first.Body.Close() + }() if first.StatusCode != http.StatusOK { t.Fatalf("first SSE status = %d, want 200", first.StatusCode) } @@ -599,7 +603,9 @@ func TestCollectorSSELimit(t *testing.T) { if err != nil { t.Fatal(err) } - defer second.Body.Close() + defer func() { + _ = second.Body.Close() + }() if second.StatusCode != http.StatusTooManyRequests { t.Fatalf("second SSE status = %d, want 429", second.StatusCode) } @@ -847,8 +853,8 @@ func snapshotJSON(t *testing.T, span trace.Snapshot) string { return string(payload) } -func jsonPostRequest(method string, target string, payload []byte) *http.Request { - request := httptest.NewRequest(method, target, bytes.NewReader(payload)) +func jsonPostRequest(target string, payload []byte) *http.Request { + request := httptest.NewRequest(http.MethodPost, target, bytes.NewReader(payload)) request.Header.Set("Content-Type", "application/json") return request } diff --git a/runtime/trace/types.go b/runtime/trace/types.go index 8e2dd5f2..915562f8 100644 --- a/runtime/trace/types.go +++ b/runtime/trace/types.go @@ -125,8 +125,10 @@ const ( const maxTracestateMembers = 32 -type contextKey struct{} -type tracerContextKey struct{} +type ( + contextKey struct{} + tracerContextKey struct{} +) // NewTraceID returns a valid W3C trace ID from the default ID generator // (CryptoIDGenerator). It returns "" only when the system CSPRNG cannot diff --git a/runtime/validation/pattern.go b/runtime/validation/pattern.go index 34034950..843659ce 100644 --- a/runtime/validation/pattern.go +++ b/runtime/validation/pattern.go @@ -297,7 +297,7 @@ func (parser *patternParser) parseEscape() (patternNode, error) { case 'd', 'D', 'w', 'W', 's', 'S': return shorthandNode(current), nil case 'p', 'P': - return nil, fmt.Errorf("Unicode character class escapes are not supported") + return nil, fmt.Errorf("unicode character class escapes are not supported") default: return literalNode(current), nil } @@ -363,7 +363,7 @@ func (parser *patternParser) parseClassPart() (classPart, bool, error) { case 'd', 'w', 's': return shorthandClassPart(escaped), false, nil case 'p', 'P': - return nil, false, fmt.Errorf("Unicode character class escapes are not supported") + return nil, false, fmt.Errorf("unicode character class escapes are not supported") default: return literalClassPart(escaped), true, nil } @@ -392,14 +392,14 @@ func (parser *patternParser) parseQuantifier(atom patternNode) (patternNode, err if parser.index >= len(parser.source) { return atom, nil } - min, max, ok, err := parser.readQuantifier() + minimum, maximum, ok, err := parser.readQuantifier() if err != nil || !ok { return atom, err } if parser.index < len(parser.source) && parser.source[parser.index] == '?' { return nil, fmt.Errorf("quantifier %q has no target", parser.source[parser.index]) } - return repeatNode{child: atom, min: min, max: max}, nil + return repeatNode{child: atom, min: minimum, max: maximum}, nil } func (parser *patternParser) readQuantifier() (int, int, bool, error) { @@ -430,11 +430,11 @@ func (parser *patternParser) readBraceQuantifier() (int, int, bool, error) { if index == minStart { return 0, 0, false, nil } - min, err := strconv.Atoi(string(parser.source[minStart:index])) + minimum, err := strconv.Atoi(string(parser.source[minStart:index])) if err != nil { return 0, 0, false, err } - max := min + maximum := minimum if index < len(parser.source) && parser.source[index] == ',' { index++ maxStart := index @@ -442,9 +442,9 @@ func (parser *patternParser) readBraceQuantifier() (int, int, bool, error) { index++ } if index == maxStart { - max = -1 + maximum = -1 } else { - max, err = strconv.Atoi(string(parser.source[maxStart:index])) + maximum, err = strconv.Atoi(string(parser.source[maxStart:index])) if err != nil { return 0, 0, false, err } @@ -454,11 +454,11 @@ func (parser *patternParser) readBraceQuantifier() (int, int, bool, error) { parser.index = start return 0, 0, false, nil } - if max >= 0 && max < min { + if maximum >= 0 && maximum < minimum { return 0, 0, false, fmt.Errorf("invalid repeat count") } parser.index = index + 1 - return min, max, true, nil + return minimum, maximum, true, nil } func matchShorthand(kind rune, r rune) bool { @@ -466,15 +466,15 @@ func matchShorthand(kind rune, r rune) bool { case 'd': return r >= '0' && r <= '9' case 'D': - return !(r >= '0' && r <= '9') + return r < '0' || r > '9' case 'w': return r == '_' || (r >= '0' && r <= '9') || (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') case 'W': - return !(r == '_' || (r >= '0' && r <= '9') || (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z')) + return r != '_' && (r < '0' || r > '9') && (r < 'A' || r > 'Z') && (r < 'a' || r > 'z') case 's': return r == '\t' || r == '\n' || r == '\f' || r == '\r' || r == ' ' case 'S': - return !(r == '\t' || r == '\n' || r == '\f' || r == '\r' || r == ' ') + return r != '\t' && r != '\n' && r != '\f' && r != '\r' && r != ' ' default: return false } diff --git a/runtime/wasm/wasm_test.go b/runtime/wasm/wasm_test.go index 42ada404..5220b650 100644 --- a/runtime/wasm/wasm_test.go +++ b/runtime/wasm/wasm_test.go @@ -1,13 +1,16 @@ package wasm -import "testing" +import ( + "errors" + "testing" +) func TestStubHelpersReportUnavailable(t *testing.T) { var payload Payload - if err := DecodePayload(&payload); err != ErrUnavailable { + if err := DecodePayload(&payload); !errors.Is(err, ErrUnavailable) { t.Fatalf("DecodePayload error = %v, want ErrUnavailable", err) } - if _, err := CurrentPayload(); err != ErrUnavailable { + if _, err := CurrentPayload(); !errors.Is(err, ErrUnavailable) { t.Fatalf("CurrentPayload error = %v, want ErrUnavailable", err) } if pointer := Return(Result{}); pointer != 0 { diff --git a/scripts/check-golangci-lint.sh b/scripts/check-golangci-lint.sh new file mode 100755 index 00000000..1da52f4d --- /dev/null +++ b/scripts/check-golangci-lint.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env sh +set -eu + +repo_root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +golangci_lint_version='2.12.2' + +cd "${repo_root}" + +run_golangci_lint() { + gopath_bin="$(go env GOPATH)/bin/golangci-lint" + path_bin="$(command -v golangci-lint 2>/dev/null || true)" + + for candidate in "${path_bin}" "${gopath_bin}"; do + if [ -x "${candidate}" ] && + "${candidate}" version | grep -q "version ${golangci_lint_version}"; then + "${candidate}" "$@" + return + fi + done + + go run "github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v${golangci_lint_version}" "$@" +} + +run_golangci_lint config verify +run_golangci_lint run