Skip to content

[linter-miner] linter: add stringsindexcontains — flag strings.Index comparisons that should use strings.Contains#43253

Merged
pelikhan merged 3 commits into
mainfrom
linter-miner/stringsindexcontains-759210f852eeb03c
Jul 4, 2026
Merged

[linter-miner] linter: add stringsindexcontains — flag strings.Index comparisons that should use strings.Contains#43253
pelikhan merged 3 commits into
mainfrom
linter-miner/stringsindexcontains-759210f852eeb03c

Conversation

@github-actions

@github-actions github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a new stringsindexcontains custom go/analysis linter to pkg/linters/ that flags strings.Index(s, substr) comparisons used as containment checks and rewrites them to strings.Contains or !strings.Contains. The linter was motivated by 42 existing occurrences of the anti-pattern in the codebase and follows the established go/analysis pass pattern already used by stringreplaceminusone, lenstringzero, and similar analyzers. It emits SuggestedFix text edits to enable automated bulk remediation.

Changes

Implementation

  • pkg/linters/stringsindexcontains/stringsindexcontains.go (new, 242 lines) — Full go/analysis analyzer:
    • Registers Analyzer with name, doc, URL, and inspect.Analyzer dependency.
    • run() walks *ast.BinaryExpr nodes; skips test files and //nolint:stringsindexcontains directives.
    • matchIndexComparison() normalizes yoda-order operands, identifies strings.Index calls via IsPkgSelector, reads constant integer values from pass.TypesInfo, and matches all six operator/literal combos: != -1, >= 0, > -1 (contains) and == -1, < 0, <= -1 (not-contains).
    • buildContainsFix() emits a SuggestedFix rewriting the entire binary expression to [!]strings.Contains(s, substr).
  • cmd/linters/main.go — Added import and registered stringsindexcontains.Analyzer in the multi-analyzer runner.

Tests

  • pkg/linters/stringsindexcontains/stringsindexcontains_test.go (new) — Standard analysistest.Run harness.
  • pkg/linters/stringsindexcontains/testdata/src/stringsindexcontains/stringsindexcontains.go (new, 66 lines) — Test fixture with 10 // want cases (all 6 operators + 4 yoda-order variants) and 5 intentional non-flagged cases: direct strings.Contains, index used for position (no comparison), comparison against non-sentinel (> 3), and == 0 (prefix check).
  • pkg/linters/spec_test.go — Added stringsindexcontains to documentedAnalyzers(); updated documented analyzer count from 36 to 37.

Docs / Infra

  • pkg/linters/README.md — Added stringsindexcontains to the description list, the subpackages table, and the package list; updated count from 36 to 37.
  • docs/adr/43253-add-stringsindexcontains-linter.md (new) — Draft ADR documenting context, decision, alternatives considered (manual review, third-party linters), and consequences.
  • linters (binary) — Recompiled binary updated.

Design notes

  • Yoda-order support: normalizeOperands swaps operands and sets flipped=true; flipOp inverts the comparison token so the switch table stays canonical (index on left, literal on right).
  • == 0 intentionally excluded: strings.Index(s, sub) == 0 is a prefix check, not a containment check, so it is not flagged.
  • Type-safe package identification: Uses astutil.IsPkgSelector (type-info-based) rather than string matching to avoid false positives from shadowed strings identifiers.
  • Test files skipped: filecheck.IsTestFile gates the visitor, keeping the linter focused on production code.

Testing

The analysistest fixture covers all six canonical operator/literal combinations and four yoda-order variants as positive cases, plus five negative cases verifying that legitimate strings.Index uses (position lookup, non-sentinel comparisons, prefix check with == 0) are not flagged.

Generated by PR Description Updater for #43253 · 56.7 AIC · ⌖ 9.03 AIC · ⊞ 4.7K ·

Add a new custom go/analysis linter that flags strings.Index(s, sub)
comparisons with -1 or 0 that should use the more readable
strings.Contains(s, sub) (or !strings.Contains for the negated form).

Pattern caught:
  strings.Index(s, sub) != -1  →  strings.Contains(s, sub)
  strings.Index(s, sub) >= 0   →  strings.Contains(s, sub)
  strings.Index(s, sub) > -1   →  strings.Contains(s, sub)
  strings.Index(s, sub) == -1  →  !strings.Contains(s, sub)
  strings.Index(s, sub) < 0    →  !strings.Contains(s, sub)
  strings.Index(s, sub) <= -1  →  !strings.Contains(s, sub)
  (plus yoda-order variants)

Evidence: 42 occurrences of this pattern were found in the pkg/ and
cmd/ directories of this repository during the automated code scan,
confirming it is a recurring pattern worth linting.

The linter provides SuggestedFix text edits for automated repair.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor Author

Great work from the Linter Miner workflow! 🎉 The stringsindexcontains analyzer is well-structured and ready for review.

  • Focused and complete — the PR does exactly one thing: adds the new go/analysis pass, its tests, testdata fixtures, and wires the analyzer into cmd/linters/main.go.
  • Well-evidenced — the 42-occurrence codebase scan motivates the linter clearly, and the pattern table in the body covers all the meaningful variants (including yoda forms and the less-common > -1 / <= -1 cases).
  • Tests present — both a standard analysistest.Run harness and rich golden-comment fixtures covering positive and negative cases.
  • No new dependencies — the implementation reuses existing internal helpers (astutil, filecheck, nolint) and the already-imported golang.org/x/tools/go/analysis.

This looks ready for maintainer review! 🚀

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • patchdiff.githubusercontent.com

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "patchdiff.githubusercontent.com"

See Network Configuration for more information.

Generated by ✅ Contribution Check · 269.8 AIC · ⌖ 22.3 AIC · ⊞ 6.3K ·

@pelikhan pelikhan marked this pull request as ready for review July 4, 2026 03:27
Copilot AI review requested due to automatic review settings July 4, 2026 03:27
@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor Author

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor Author

Design Decision Gate 🏗️ completed the design decision gate check.

@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor Author

⚠️ PR Code Quality Reviewer failed during code quality review.

@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor Author

Test Quality Sentinel completed test quality analysis.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new custom go/analysis analyzer to improve readability by replacing common strings.Index(...) containment checks with strings.Contains(...) (or its negation), and wires it into the cmd/linters multichecker so it runs with the rest of gh-aw’s linters.

Changes:

  • Implemented pkg/linters/stringsindexcontains analyzer with SuggestedFix rewrites for supported comparison patterns.
  • Added analysistest-based unit test and testdata fixtures for the new analyzer.
  • Registered the analyzer in cmd/linters/main.go.
Show a summary per file
File Description
pkg/linters/stringsindexcontains/stringsindexcontains.go New analyzer implementation to detect strings.Index comparisons that can be simplified to strings.Contains.
pkg/linters/stringsindexcontains/stringsindexcontains_test.go Test harness for running the analyzer via analysistest.
pkg/linters/stringsindexcontains/testdata/src/stringsindexcontains/stringsindexcontains.go Test fixtures that assert reported diagnostics (and indirectly validate suggested fixes).
cmd/linters/main.go Registers the new analyzer in the multichecker binary.

Review details

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 4/4 changed files
  • Comments generated: 4
  • Review effort level: Low

Comment on lines +19 to +26
// Analyzer is the strings-index-contains analysis pass.
var Analyzer = &analysis.Analyzer{
Name: "stringsindexcontains",
Doc: "reports strings.Index(s, substr) comparisons with -1 that should use strings.Contains(s, substr)",
URL: "https://github.com/github/gh-aw/tree/main/pkg/linters/stringsindexcontains",
Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: run,
}
Comment on lines +42 to +45
func goodIndexComparesNonMinusOne(s, sub string) bool {
// Comparing against a value other than -1/0 is fine.
return strings.Index(s, sub) > 3
}
Comment thread cmd/linters/main.go
Comment on lines 93 to 97
sprintfint.Analyzer,
strconvparseignorederror.Analyzer,
stringreplaceminusone.Analyzer,
stringsindexcontains.Analyzer,
jsonmarshalignoredeerror.Analyzer,
@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor Author

🧪 Test Quality Sentinel Report

Test Quality Score: 100/100 — Excellent

Analyzed 1 test(s): 1 design, 0 implementation, 0 violation(s).

📊 Metrics (1 test)
Metric Value
Analyzed 1 (Go: 1, JS: 0)
✅ Design 1 (100%)
⚠️ Implementation 0 (0%)
Edge/error coverage 1 (100%)
Duplicate clusters 0
Inflation No (test lines 16 + testdata 45 = 61 vs prod 241, ratio ≈ 0.25)
🚨 Violations 0
Test File Classification Issues
TestAnalyzer stringsindexcontains_test.go:13 design_test / behavioral_contract / high_value None
🔍 Test Analysis

TestAnalyzer (stringsindexcontains_test.go:13)

Uses analysistest.Run — the standard idiomatic framework for Go static-analysis passes. The // want directives in testdata/src/stringsindexcontains/stringsindexcontains.go function as precise assertions on linter output (user-visible diagnostics), making this a strong behavioral/design test.

Build tag //go:build !integration is present on line 1. ✅ No forbidden mock libraries used. ✅

Testdata coverage (10 cases):

Pattern Kind Covered
strings.Index(s,sub) != -1 bad — contains
strings.Index(s,sub) >= 0 bad — contains
strings.Index(s,sub) == -1 bad — !contains
strings.Index(s,sub) < 0 bad — !contains
-1 != strings.Index(s,sub) bad yoda — contains
-1 == strings.Index(s,sub) bad yoda — !contains
strings.Contains(s,sub) good (no diagnostic)
!strings.Contains(s,sub) good (no diagnostic)
strings.Index(s,sub) (return value) good (no diagnostic)
strings.Index(s,sub) > 3 good (non-standard constant)
strings.Index(s,sub) > -1 bad — contains (GTR/-1) ❌ missing
strings.Index(s,sub) <= -1 bad — !contains (LEQ/-1) ❌ missing
0 <= strings.Index(s,sub) bad yoda — GEQ/0 ❌ missing
0 > strings.Index(s,sub) bad yoda — LSS/0 ❌ missing

The four missing cases correspond to operator branches in matchIndexComparison (GTR/-1, LEQ/-1, and their yoda counterparts) that are implemented in production but not exercised by testdata. This is a minor gap — the core semantic patterns are well-covered — but adding these would achieve full branch coverage.

Verdict

Passed. 0% implementation tests (threshold: 30%). No guideline violations. The analysistest.Run-based approach correctly validates linter behavioral contracts. Consider adding testdata cases for the > -1, <= -1, and corresponding yoda operator patterns to achieve full branch coverage of matchIndexComparison.

References:

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • awmgmcpg

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "awmgmcpg"

See Network Configuration for more information.

🧪 Test quality analysis by Test Quality Sentinel · 27.5 AIC · ⌖ 8.62 AIC · ⊞ 6.8K ·
Comment /review to run again

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Test Quality Sentinel: 100/100. 0% implementation tests (threshold: 30%). No violations. The analysistest.Run-based approach correctly validates linter behavioral contracts via // want directives.

@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor Author

🏗️ Design Decision Gate — ADR Required

This PR makes significant changes to core business logic (302 new lines in pkg/ and cmd/) but does not have a linked Architecture Decision Record (ADR).

📄 Draft ADR committed: docs/adr/43253-add-stringsindexcontains-linter.md — review and complete it before merging.

🔒 This PR cannot merge until an ADR is linked in the PR body.

📋 What to do next
  1. Review the draft ADR committed to your branch — it was generated from the PR diff
  2. Complete the missing sections — add context the AI could not infer, refine the decision rationale, and list real alternatives you considered
  3. Commit the finalized ADR to docs/adr/ on your branch
  4. Reference the ADR in this PR body by adding a line such as:

    ADR: ADR-43253: Add stringsindexcontains Custom Go Analysis Linter

Once an ADR is linked in the PR body, this gate will re-run and verify the implementation matches the decision.

❓ Why ADRs Matter

"AI made me procrastinate on key design decisions. Because refactoring was cheap, I could always say 'I'll deal with this later.' Deferring decisions corroded my ability to think clearly."

ADRs create a searchable, permanent record of why the codebase looks the way it does. Future contributors (and your future self) will thank you.

📋 Michael Nygard ADR Format Reference

An ADR must contain these four sections to be considered complete:

  • Context — What is the problem? What forces are at play?
  • Decision — What did you decide? Why?
  • Alternatives Considered — What else could have been done?
  • Consequences — What are the trade-offs (positive and negative)?

All ADRs are stored in docs/adr/ as Markdown files numbered by PR number (e.g., 0042-use-postgresql.md for PR #42).

🏗️ ADR gate enforced by Design Decision Gate 🏗️ · 49.8 AIC · ⌖ 10.3 AIC · ⊞ 8.5K ·
Comment /review to run again

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skills-Based Review 🧠

Applied /tdd and /grill-with-docs — requesting changes on test coverage gaps and documentation accuracy.

📋 Key Themes & Highlights

Key Themes

  • Test coverage gaps: The > -1 (GTR) and <= -1 (LEQ) operator branches have no fixture tests; named-constant patterns are also untested. These code paths could regress silently.
  • Documentation inaccuracy: Analyzer.Doc and the inline comment block both undercount the number of supported patterns, making the spec harder to reason about.
  • Minor interface noise: buildContainsFix carries an unused pass parameter.

Positive Highlights

  • ✅ Solid pattern decomposition: normalizeOperands + flipOp cleanly handles yoda-order comparisons
  • constIntValue correctly uses the type-checker rather than syntactic literal matching — handles aliased constants robustly
  • ✅ Follows established linter conventions: nolint directives, test-file skipping, SuggestedFix emission
  • ✅ Well-documented godoc on matchIndexComparison listing all matched patterns clearly

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · 80.7 AIC · ⌖ 5.63 AIC · ⊞ 6.6K
Comment /matt to run again


func TestAnalyzer(t *testing.T) {
testdata := analysistest.TestData()
analysistest.Run(t, testdata, stringsindexcontains.Analyzer, "stringsindexcontains")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] The > -1 (GTR) and <= -1 (LEQ) patterns implemented in matchIndexComparison have no corresponding testdata fixtures — these code paths are untested and could regress silently.

💡 Suggested fixture additions

Add to testdata/src/stringsindexcontains/stringsindexcontains.go:

func badGTRMinusOne(s, sub string) bool {
	return strings.Index(s, sub) > -1 // want `use strings\.Contains\(s, sub\) instead of strings\.Index comparison`
}

func badLEQMinusOne(s, sub string) bool {
	return strings.Index(s, sub) <= -1 // want `use !strings\.Contains\(s, sub\) instead of strings\.Index comparison`
}

Similarly, the yoda-order variants for 0 <= strings.Index(...) and 0 > strings.Index(...) are documented in the function godoc but have no fixture coverage.

@copilot please address this.

// Analyzer is the strings-index-contains analysis pass.
var Analyzer = &analysis.Analyzer{
Name: "stringsindexcontains",
Doc: "reports strings.Index(s, substr) comparisons with -1 that should use strings.Contains(s, substr)",

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/grill-with-docs] The Doc field says "comparisons with -1" but the linter also catches >= 0 and < 0 comparisons (and > -1 / <= -1). The description is misleading and will confuse users checking go vet -json output.

💡 Suggested fix
Doc: "reports strings.Index(s, substr) comparisons with -1 or 0 that should use strings.Contains(s, substr)",

@copilot please address this.

}

// buildContainsFix builds the suggested fix rewriting the comparison to strings.Contains.
func buildContainsFix(pass *analysis.Pass, expr *ast.BinaryExpr, pkgText, sText, subText string, negated bool) []analysis.SuggestedFix {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/codebase-design] buildContainsFix accepts pass *analysis.Pass but never uses it. The unused parameter adds noise to the interface and suggests the function signature was copied from a similar helper that did use pass.

💡 Suggested fix

Remove the pass parameter since all needed information is already provided via the explicit text arguments:

func buildContainsFix(expr *ast.BinaryExpr, pkgText, sText, subText string, negated bool) []analysis.SuggestedFix {

Update the call site at line 81 accordingly.

@copilot please address this.

// strings.Index(s, sub) >= 0 → strings.Contains(s, sub)
// strings.Index(s, sub) == -1 → !strings.Contains(s, sub)
// strings.Index(s, sub) < 0 → !strings.Contains(s, sub)
// (and yoda variants: -1 != strings.Index(...), etc.)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/grill-with-docs] The inline comment block only lists 4 of the 6 supported patterns — it omits strings.Index(s, sub) > -1 and strings.Index(s, sub) <= -1. A reader relying on this comment as spec would miss two valid patterns.

💡 Suggested fix
// Match patterns:
//   strings.Index(s, sub) != -1  → strings.Contains(s, sub)
//   strings.Index(s, sub) >= 0   → strings.Contains(s, sub)
//   strings.Index(s, sub) > -1   → strings.Contains(s, sub)
//   strings.Index(s, sub) == -1  → !strings.Contains(s, sub)
//   strings.Index(s, sub) < 0    → !strings.Contains(s, sub)
//   strings.Index(s, sub) <= -1  → !strings.Contains(s, sub)
// (and yoda variants: -1 != strings.Index(...), etc.)

@copilot please address this.

func goodIndexComparesNonMinusOne(s, sub string) bool {
// Comparing against a value other than -1/0 is fine.
return strings.Index(s, sub) > 3
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] The test fixtures cover "good" cases where strings.Index is used to retrieve a position or compared against a non-sentinel value — but there is no test for a named constant like const notFound = -1; strings.Index(s, sub) == notFound. Other linters in this codebase (e.g. stringreplaceminusone) explicitly test named-constant variants.

💡 Suggested fixture addition
const notFound = -1

func badNamedConst(s, sub string) bool {
\treturn strings.Index(s, sub) == notFound // want `use !strings\.Contains\(s, sub\) instead of strings\.Index comparison`
}

This confirms that constIntValue correctly resolves named constants (not just literals), matching the behavior documented via constant.Int64Val.

@copilot please address this.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: stringsindexcontains linter

The implementation is clean, well-structured, and follows the repo's linter conventions. The logic for matching operator/literal combinations (including yoda variants) is correct. Two non-blocking suggestions and one blocking gap:

Blocking

  • Missing test fixtures (comment on test file, line 14): The operator table in the implementation's doc comment covers 6 operators × 2 yoda variants = 12 logical patterns, but the fixture only tests 6 of them. The > -1, <= -1, yoda-GEQ, and yoda-LSS branches have no test coverage. This should be filled in before merge.

Non-blocking

  • Unused pass parameter in buildContainsFix (line 225): The parameter is never referenced in the body; removing it reduces noise.
  • normalizeOperands "no match" semantic (line 166): When neither operand is an Index call the function returns (Y, X, flipped=true), which is harmless because the caller immediately re-checks, but the invariant is surprising. A brief doc note would help future readers.
Patterns with no test fixture coverage
Pattern Operator Expected replacement
strings.Index(s, sub) > -1 GTR / -1 strings.Contains(s, sub)
strings.Index(s, sub) <= -1 LEQ / -1 !strings.Contains(s, sub)
0 <= strings.Index(s, sub) yoda-GEQ strings.Contains(s, sub)
0 > strings.Index(s, sub) yoda-LSS !strings.Contains(s, sub)

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · 83.5 AIC · ⌖ 6.45 AIC · ⊞ 4.9K

}

// buildContainsFix builds the suggested fix rewriting the comparison to strings.Contains.
func buildContainsFix(pass *analysis.Pass, expr *ast.BinaryExpr, pkgText, sText, subText string, negated bool) []analysis.SuggestedFix {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pass *analysis.Pass parameter is declared but never used in this function — all the needed data (the replacement text and AST position) is already passed in as pre-computed strings and the expr node. Consider removing it to keep the signature minimal:

func buildContainsFix(expr *ast.BinaryExpr, pkgText, sText, subText string, negated bool) []analysis.SuggestedFix {

And update the call site at line ~82 accordingly.

@copilot please address this.


// normalizeOperands returns (left, right) such that if the strings.Index call
// is on the right side, the operands are swapped and flipped=true.
func normalizeOperands(pass *analysis.Pass, expr *ast.BinaryExpr) (left, right ast.Expr, flipped bool) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pass *analysis.Pass parameter is passed through here but is only used by the inner asStringsIndexCall call. This is fine structurally, but it's worth noting that normalizeOperands has a subtle issue: when neither operand is a strings.Index call, it returns (expr.Y, expr.X, true) — putting the right side as the left result with flipped=true. The caller in matchIndexComparison then immediately calls asStringsIndexCall(pass, left) on that value, which will correctly return false. But the semantic is slightly confusing: "flipped" should mean "we found Index on the right", yet here it can also mean "we found nothing". Consider renaming the return value or adding a doc comment clarifying the "no match" case to prevent future misuse.

@copilot please address this.

)

func TestAnalyzer(t *testing.T) {
testdata := analysistest.TestData()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test coverage is minimal — only 6 bad cases and 4 good cases. Missing coverage for the patterns listed in the PR description and in the doc comment of matchIndexComparison:

  • strings.Index(s, sub) > -1 (GTR / -1 → contains)
  • strings.Index(s, sub) <= -1 (LEQ / -1 → not-contains)
  • Yoda variants for GEQ (0 <= strings.Index(...)) and LSS (0 > strings.Index(...))

Please add fixtures for these cases to testdata/src/stringsindexcontains/stringsindexcontains.go so the full operator coverage documented in the implementation is actually tested.

@copilot please address this.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

REQUEST_CHANGES — Two issues must be addressed before merging; one structural nit is non-blocking.

🔍 Summary of findings

Blocking

  1. Suggested fixes are unvalidated (stringsindexcontains_test.go:15) — The test uses analysistest.Run instead of analysistest.RunWithSuggestedFixes. The SuggestedFix text edits in buildContainsFix — the linter's main automated-repair value — are never applied or checked. Every other fix-emitting linter in this repo (ctxbackground, lenstringsplit, lenstringzero, fprintlnsprintf, execcommandwithoutcontext) uses RunWithSuggestedFixes plus .golden files. Fix: swap the call and add a golden file.

  2. Two supported patterns have zero test coverage (testdata/.../stringsindexcontains.go:45) — strings.Index(s, sub) > -1 (GTR case) and strings.Index(s, sub) <= -1 (LEQ case) are handled in matchIndexComparison and listed in the PR description table, but no // want annotation exercises them. These branches can silently regress.

Non-blocking

  1. normalizeOperands forces a redundant asStringsIndexCall traversal (stringsindexcontains.go:166) — Minor design issue: the call expr is discarded and immediately re-looked-up. Refactoring to return *ast.CallExpr would be cleaner, but this does not affect correctness or performance in any meaningful way at linter scale.

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • proxy.golang.org

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "proxy.golang.org"

See Network Configuration for more information.

🔎 Code quality review by PR Code Quality Reviewer · 225.2 AIC · ⌖ 9.29 AIC · ⊞ 5.4K
Comment /review to run again


func TestAnalyzer(t *testing.T) {
testdata := analysistest.TestData()
analysistest.Run(t, testdata, stringsindexcontains.Analyzer, "stringsindexcontains")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested fixes are emitted but never validated — this is the primary value of the linter and it is completely untested.

The test calls analysistest.Run but the linter emits SuggestedFixes. analysistest.Run only checks that diagnostics fire on the expected lines; it never applies or validates the fix text edits. Every other linter in this repo that emits fixes uses RunWithSuggestedFixes with matching .golden files (e.g., ctxbackground, lenstringsplit, lenstringzero, fprintlnsprintf, execcommandwithoutcontext).

Given the stated purpose — automated repair of 42 occurrences — silently shipping unvalidated fix text is a serious quality gap.

💡 How to fix
  1. Change analysistest.Runanalysistest.RunWithSuggestedFixes in the test.
  2. Create a .golden file under testdata/src/stringsindexcontains/stringsindexcontains.go.golden containing the expected post-fix source.
// stringsindexcontains_test.go
func TestAnalyzer(t *testing.T) {
	testdata := analysistest.TestData()
	analysistest.RunWithSuggestedFixes(t, testdata, stringsindexcontains.Analyzer, "stringsindexcontains")
}

Without this change, buildContainsFix could produce incorrect replacements (wrong parenthesization, wrong package alias) and all tests would still pass.

func goodIndexComparesNonMinusOne(s, sub string) bool {
// Comparing against a value other than -1/0 is fine.
return strings.Index(s, sub) > 3
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

> -1 and <= -1 patterns are in matchIndexComparison but have zero test coverage — they can silently regress.

matchIndexComparison handles token.GTR / litVal==-1 (→ strings.Index(s,sub) > -1) and token.LEQ / litVal==-1 (→ strings.Index(s,sub) <= -1), but no testdata fixture or // want annotation exercises these two cases. They also appear in the PR description table as supported patterns. If either branch is broken today — or broken by a future refactor — the tests will not catch it.

💡 Suggested additions to testdata
func badContainsGTR(s, sub string) bool {
	return strings.Index(s, sub) > -1 // want `use strings\.Contains\(s, sub\) instead of strings\.Index comparison`
}

func badNotContainsLEQ(s, sub string) bool {
	return strings.Index(s, sub) <= -1 // want `use !strings\.Contains\(s, sub\) instead of strings\.Index comparison`
}

And the corresponding yoda variants if the PR claims to support them.


// normalizeOperands returns (left, right) such that if the strings.Index call
// is on the right side, the operands are swapped and flipped=true.
func normalizeOperands(pass *analysis.Pass, expr *ast.BinaryExpr) (left, right ast.Expr, flipped bool) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

normalizeOperands discards the *ast.CallExpr and forces asStringsIndexCall to re-traverse the same node immediately after.

normalizeOperands calls asStringsIndexCall to decide which side is the index call, returns expr.X/expr.Y (raw ast.Expr), and then matchIndexComparison immediately calls asStringsIndexCall again on the returned left. The same type assertion and selector walk runs twice on every binary expression that has a strings.Index call on either side.

💡 Suggested fix

Return the *ast.CallExpr from normalizeOperands to avoid redundant work:

func normalizeOperands(pass *analysis.Pass, expr *ast.BinaryExpr) (indexCall *ast.CallExpr, right ast.Expr, flipped bool, ok bool) {
	if call, found := asStringsIndexCall(pass, expr.X); found {
		return call, expr.Y, false, true
	}
	if call, found := asStringsIndexCall(pass, expr.Y); found {
		return call, expr.X, true, true
	}
	return nil, nil, false, false
}

This is a minor structural issue, not blocking correctness, but the current design exists only because the return type was chosen poorly.

@gh-aw-bot

Copy link
Copy Markdown
Collaborator

@copilot please run the pr-finisher skill, address the latest review feedback about missing test fixtures for the uncovered operator/yoda branches and the analyzer documentation mismatch, add and link the required ADR in the PR body, refresh the branch from main if needed, and rerun checks once updates are pushed.

Generated by 👨‍🍳 PR Sous Chef · 5.58 AIC · ⌖ 7.65 AIC · ⊞ 6.7K ·
Comment /souschef to run again

…EADME, spec

- Update Analyzer.Doc and package doc comment to describe all six
  supported operator/literal patterns (> -1, <= -1, yoda variants)
  not just comparisons with -1
- Add missing testdata fixtures for > -1, <= -1, 0 <= (yoda), 0 >
  (yoda) patterns to prevent regressions
- Fix misleading comment about index==0 comparisons: clarify it is a
  prefix check and is intentionally not flagged
- Add stringsindexcontains to pkg/linters/README.md (description list,
  subpackages table, and dependencies section)
- Add stringsindexcontains to pkg/linters/spec_test.go (import,
  documentedAnalyzers slice, and count comment updated 36→37)

Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>

Copilot AI commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

@copilot please run the pr-finisher skill, address the latest review feedback about missing test fixtures for the uncovered operator/yoda branches and the analyzer documentation mismatch, add and li...

Addressed all review feedback in commit fix(stringsindexcontains):

  • Doc mismatch fixedAnalyzer.Doc and the package doc comment now enumerate all six supported operator/literal patterns (!= -1, >= 0, > -1, == -1, < 0, <= -1) and their yoda-order variants.
  • Missing test fixtures added — testdata now covers > -1, <= -1, 0 <= (yoda contains), and 0 > (yoda not-contains) to guard the matchIndexComparison/flipOp logic.
  • Misleading comment fixed — the == 0 "good" case now explains it is a prefix check and is intentionally not flagged.
  • README + spec in syncstringsindexcontains added to pkg/linters/README.md (description list, subpackages table, dependencies section) and to pkg/linters/spec_test.go (import, documentedAnalyzers slice, count updated 36→37).
  • ADR — ADR-43253 was already present from the previous commit.

Local validation: all pkg/linters/... tests pass, go build ./cmd/linters succeeds. The pre-existing stringscutprefix lint failure in pkg/workflow/arc_dind_artifacts.go is unrelated to this PR. CI needs a human re-trigger to confirm.

Copilot AI requested a review from gh-aw-bot July 4, 2026 04:01
@pelikhan pelikhan merged commit 29b32b5 into main Jul 4, 2026
@pelikhan pelikhan deleted the linter-miner/stringsindexcontains-759210f852eeb03c branch July 4, 2026 04:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

automation cookie Issue Monster Loves Cookies! go-linters

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants