isolint: add skip-fields config for domain collisions#201
Merged
Conversation
Some uppercase 2- and 3-letter strings overlap with ISO codes by accident — most notably card scheme abbreviations like "MC" (MasterCard, also Monaco) and "AX" (American Express, also Aland Islands). Until now the only escape hatch was per-line //nolint:isolint, which clutters real test code and offers no guard against new collisions. skip-fields names struct fields whose ISO-like values should be ignored (e.g. CardSchemes). The check is syntactic — it walks the AST stack for an enclosing assignment and matches by field name — so the analyzer stays on LoadModeSyntax and a real pq.StringArray of country codes assigned to a non-skipped field is still flagged. This was preferred over a flat allow-values allowlist (too coarse, suppresses genuine country usages) and over type-based matching (would force LoadModeTypesInfo on the whole golangci-lint batch). Settings flow through both the module plugin (decoded via register.DecodeSettings) and the standalone CLI (via a new -skip-fields flag on the analyzer's FlagSet). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pre-merge review on PR #201 surfaced three correctness gaps in the field-name skip logic: - Tuple assignments over-suppressed. The AssignStmt branch iterated every Lhs element and matched if ANY was in the skip list. For `a.CardSchemes, a.IssuerCountries = stringArray{"MC"}, stringArray{"SG"}` the literal "SG" (destined for IssuerCountries, not in the skip list) was silently skipped because CardSchemes appeared elsewhere on the LHS. Without type information the analyzer cannot correlate which RHS literal belongs to which LHS target, so tuple assignments now fall through and flag every literal as usual. - Local variables shadowing skip-field names were silently skipped. fieldNameFromLHS accepted plain *ast.Ident on top of *ast.SelectorExpr, so `CardSchemes := "MC"` matched the skip-fields set even though the LHS is a local variable, not a struct field. The original GoDoc hand-waved this as "rare but cheap to handle", but it expanded the linter's silent-skip surface beyond the documented design and was uncovered by tests. - decisions.md drifted from the implementation. The doc described the AssignStmt shape as `Lhs[0]` while the code iterated all Lhs. The numbered "Guard ordering" list also did not include isAssignToSkipField, leaving future extenders with a stale picture. The fix narrows the AssignStmt branch to single-target SelectorExpr LHS only and aligns the doc, including explicit notes on the tuple and local-var carve-outs so the design intent is auditable from the repo alone. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
kates-wego
approved these changes
Apr 27, 2026
lei-wego
approved these changes
Apr 27, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
skip-fieldssetting toisolintthat suppresses ISO-code diagnostics on string literals assigned to named struct fields.a.CardSchemes = pq.StringArray{"MC"}inpaymentsflagged as Monaco even though"MC"is the MasterCard scheme abbreviation."AX"(American Express vs Åland Islands) is the same shape..golangci.yml(linters.settings.custom.isolint.settings.skip-fields) when used as a module plugin, and via-skip-fields=...on the standalone CLI.Approach
The check is field-name syntactic, not type-based. Two AST shapes are recognised in
isAssignToSkipField:The walk stops at function boundaries so a literal beyond a
FuncLit/FuncDeclis no longer treated as part of the original assignment.Two alternatives were considered and rejected — see
docs/decisions.md:pq.StringArray") would requirepass.TypesInfoand forceLoadModeTypesInfoon the whole golangci-lint batch — a project-wide cost called out indocs/decisions.md"LoadModeSyntax". Also coarser — would suppress legitimatepq.StringArrayof country codes too.allow-values: [MC]) would suppress the same literal everywhere, including in genuine country contexts.Settingsflows through both entry points: the module plugin decodes viaregister.DecodeSettings[Settings]inplugin.go, and the standalone CLI registers a-skip-fieldsflag on the analyzer'sFlagSetinNewAnalyzer, seeded fromSettings.SkipFieldsso plugin andanalystestcallers (which don't parse flags) get the configured behaviour from the seeded default. The package-levelAnalyzer = NewAnalyzer(Settings{})is preserved for backward-compatibility with existing tests and the standalone CLI's zero-config default.Test plan
TestAnalyzerWithSkipFieldsrunsanalystest.Runagainst a newtestdata/skipfields/package withSettings{SkipFields: ["CardSchemes"]}.skip_fields.go— Pattern A and Pattern B fixtures with no// wantannotations (asserts zero diagnostics under the configured skip).still_flags.go— same package, same values, but assigned toIssuerCountriesor as bare literals; asserts the skip is field-scoped, not global.TestSkipFieldsFlagandTestSkipFieldsFlagSeededFromSettingsinflags_test.gocover theflag.Valuewiring on the analyzer'sFlagSet.TestRunWithNilPkgupdated for the newrun(pass, skipFields)signature; original golangci-lint nil-pass.Pkgguard still verified.TestAnalyzer,TestAnalyzerWithFixescontinue to pass against./example(unchanged, asserts zero regressions on the original surface).isolint -skip-fields=CardSchemesagainsttestdata/skipfields/reports 5 diagnostics; without the flag, 10. The 5 suppressed are the configuredCardSchemesassignments.