Skip to content

isolint: add skip-fields config for domain collisions#201

Merged
yanyi-wego merged 2 commits into
mainfrom
isolint-skip-fields
Apr 27, 2026
Merged

isolint: add skip-fields config for domain collisions#201
yanyi-wego merged 2 commits into
mainfrom
isolint-skip-fields

Conversation

@yanyi-wego
Copy link
Copy Markdown
Contributor

Summary

  • Adds a skip-fields setting to isolint that suppresses ISO-code diagnostics on string literals assigned to named struct fields.
  • Motivating false positive: a.CardSchemes = pq.StringArray{"MC"} in payments flagged as Monaco even though "MC" is the MasterCard scheme abbreviation. "AX" (American Express vs Åland Islands) is the same shape.
  • Configurable via .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:

a.CardSchemes = pq.StringArray{"MC"}                          // AssignStmt + SelectorExpr LHS
_ = entity.CardAvailability{CardSchemes: pq.StringArray{"MC"}} // KeyValueExpr inside CompositeLit

The walk stops at function boundaries so a literal beyond a FuncLit/FuncDecl is no longer treated as part of the original assignment.

Two alternatives were considered and rejected — see docs/decisions.md:

  • Type-based match (e.g. "skip everything assigned to a pq.StringArray") would require pass.TypesInfo and force LoadModeTypesInfo on the whole golangci-lint batch — a project-wide cost called out in docs/decisions.md "LoadModeSyntax". Also coarser — would suppress legitimate pq.StringArray of country codes too.
  • Flat value allowlist (allow-values: [MC]) would suppress the same literal everywhere, including in genuine country contexts.

Settings flows through both entry points: the module plugin decodes via register.DecodeSettings[Settings] in plugin.go, and the standalone CLI registers a -skip-fields flag on the analyzer's FlagSet in NewAnalyzer, seeded from Settings.SkipFields so plugin and analystest callers (which don't parse flags) get the configured behaviour from the seeded default. The package-level Analyzer = NewAnalyzer(Settings{}) is preserved for backward-compatibility with existing tests and the standalone CLI's zero-config default.

Test plan

  • TestAnalyzerWithSkipFields runs analystest.Run against a new testdata/skipfields/ package with Settings{SkipFields: ["CardSchemes"]}.
    • skip_fields.go — Pattern A and Pattern B fixtures with no // want annotations (asserts zero diagnostics under the configured skip).
    • still_flags.go — same package, same values, but assigned to IssuerCountries or as bare literals; asserts the skip is field-scoped, not global.
  • TestSkipFieldsFlag and TestSkipFieldsFlagSeededFromSettings in flags_test.go cover the flag.Value wiring on the analyzer's FlagSet.
  • TestRunWithNilPkg updated for the new run(pass, skipFields) signature; original golangci-lint nil-pass.Pkg guard still verified.
  • Existing TestAnalyzer, TestAnalyzerWithFixes continue to pass against ./example (unchanged, asserts zero regressions on the original surface).
  • CLI sanity check: isolint -skip-fields=CardSchemes against testdata/skipfields/ reports 5 diagnostics; without the flag, 10. The 5 suppressed are the configured CardSchemes assignments.

yanyi-wego and others added 2 commits April 27, 2026 12:46
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>
@yanyi-wego yanyi-wego marked this pull request as ready for review April 27, 2026 06:12
@yanyi-wego yanyi-wego merged commit 303d1af into main Apr 27, 2026
2 checks passed
@yanyi-wego yanyi-wego deleted the isolint-skip-fields branch April 27, 2026 06:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants