Skip to content

feat(linters): add stringscountcontains linter#44820

Open
pelikhan with Copilot wants to merge 4 commits into
mainfrom
copilot/linter-minerstringscountcontains-a96c73762fdd
Open

feat(linters): add stringscountcontains linter#44820
pelikhan with Copilot wants to merge 4 commits into
mainfrom
copilot/linter-minerstringscountcontains-a96c73762fdd

Conversation

Copilot AI commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Adds a new stringscountcontains custom Go analysis linter that flags strings.Count(s, sub) comparisons with 0 or 1 used purely as containment checks. strings.Count walks the entire string while strings.Contains short-circuits on first match — making the latter both clearer in intent and more efficient.

Flagged patterns → suggested fix

// flagged → strings.Contains(s, sub)
strings.Count(s, sub) > 0
strings.Count(s, sub) >= 1
strings.Count(s, sub) != 0
0 < strings.Count(s, sub)   // yoda variants

// flagged → !strings.Contains(s, sub)
strings.Count(s, sub) == 0
strings.Count(s, sub) < 1
strings.Count(s, sub) <= 0
0 == strings.Count(s, sub)  // yoda variants

// not flagged — actual count usage
strings.Count(s, sub) > 2

Changes

  • pkg/linters/stringscountcontains/ — new analyzer package; emits SuggestedFix text edits for automated batch repair; respects //nolint:stringscountcontains directives; skips _test.go files
  • stringscountcontains_test.go — uses analysistest.RunWithSuggestedFixes with a .go.golden file, consistent with other fix-emitting linters
  • cmd/linters/main.go — registered in the multichecker binary
  • pkg/linters/README.md — documented in overview list, subpackages table, and import paths section
  • pkg/linters/spec_test.go — added to documentedAnalyzers() (39 → 40 documented analyzers)

Copilot AI and others added 2 commits July 10, 2026 19:28
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
… comment

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Add stringscountcontains linter feat(linters): add stringscountcontains linter Jul 10, 2026
Copilot AI requested a review from pelikhan July 10, 2026 19:30
@github-actions

Copy link
Copy Markdown
Contributor

PR Triage

Category: feature | Risk: low | Priority: low | Score: 30/100 (impact:15, urgency:5, quality:10) | Action: batch_review | Batch: pr-batch:draft-review

Adds stringscountcontains linter flagging inefficient strings.Count comparisons. Additive/low-risk. Still a draft; no CI yet. Grouped with other draft PRs for batch review.

Generated by 🔧 PR Triage Agent · 31.2 AIC · ⌖ 8.39 AIC · ⊞ 5.4K ·

@pelikhan pelikhan marked this pull request as ready for review July 11, 2026 05:21
Copilot AI review requested due to automatic review settings July 11, 2026 05:21

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 custom Go analyzer that replaces containment-only strings.Count comparisons with strings.Contains.

Changes:

  • Implements diagnostics, suggested fixes, exclusions, and comparison variants.
  • Adds analyzer fixtures and documentation.
  • Registers the analyzer in the multichecker and documentation tests.
Show a summary per file
File Description
cmd/linters/main.go Registers the analyzer.
pkg/linters/README.md Documents the new linter.
pkg/linters/spec_test.go Adds documentation validation.
pkg/linters/stringscountcontains/stringscountcontains.go Implements the analyzer.
pkg/linters/stringscountcontains/stringscountcontains_test.go Runs analyzer tests.
pkg/linters/stringscountcontains/testdata/src/stringscountcontains/stringscountcontains.go Provides analyzer fixtures.
pkg/linters/stringscountcontains/testdata/src/stringscountcontains/stringscountcontains.go.golden Defines expected fixes.

Review details

Tip

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

  • Files reviewed: 7/7 changed files
  • Comments generated: 2
  • Review effort level: Medium

Comment thread cmd/linters/main.go
strconvparseignorederror.Analyzer,
stringreplaceminusone.Analyzer,
stringsindexcontains.Analyzer,
stringscountcontains.Analyzer,

func TestAnalyzer(t *testing.T) {
testdata := analysistest.TestData()
analysistest.RunWithSuggestedFixes(t, testdata, stringscountcontains.Analyzer, "stringscountcontains")
@github-actions

github-actions Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

@github-actions

github-actions Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

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

@github-actions

github-actions Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

@github-actions

github-actions Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

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

@github-actions github-actions Bot 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.

Review: stringscountcontains linter

The implementation is correct and follows all established linter conventions in this repo.

Logic verified:

  • All 6 canonical patterns (> 0, >= 1, != 0, == 0, < 1, <= 0) handled correctly
  • Yoda-order variants handled via normalizeOperands + FlipComparisonOp — traced manually, correct
  • nolint directive support, test-file skip, and SuggestedFix text edits all present
  • Registration in cmd/linters/main.go and spec_test.go is correct; doc count updated 39→40

Minor gap (non-blocking): The testdata covers yoda variants for 5 of the 6 operators but omits the yoda-LEQ case (0 >= strings.Count(s, sub)). The logic handles it correctly, but a test case would complete coverage parity.

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · 28.1 AIC · ⌖ 5.43 AIC · ⊞ 4.8K

@github-actions

Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

Test Quality Score: 95/100 — Excellent

Analyzed 1 test: 1 design, 0 implementation, 0 violations.

📊 Metrics (1 test)
Metric Value
Analyzed 1 (Go: 1, JS: 0)
✅ Design 1 (100%)
⚠️ Implementation 0 (0%)
Edge/error coverage 12/13 scenarios (92%)
Duplicate clusters 0
Inflation No (0.07:1 ratio)
🚨 Violations 0
Test File Classification Coverage
TestAnalyzer stringscountcontains_test.go:13 Design (go/analysis) 13 scenarios (6 operators × 2 orderings + 2 control cases)

Coverage Detail

Test Name: TestAnalyzer (pkg/linters/stringscountcontains/stringscountcontains_test.go)

Pattern: Standard go/analysis analyzer test using analysistest.RunWithSuggestedFixes

Scenarios Covered:

  • ✅ 6 comparison operators: >, >=, ==, <, <=, !=
  • ✅ 2 orderings: normal (strings.Count(...) > 0) and yoda (0 < strings.Count(...))
  • ✅ 2 control cases: valid count threshold (> 2) and already-using-Contains

Design Contract: The test verifies the Analyzer public API exposes correct diagnostic messages and suggested fixes for all comparison patterns that should be replaced with strings.Contains() or !strings.Contains().

Verdict

APPROVED. 0% implementation tests (threshold: 30%). No violations. Test follows standard go/analysis patterns with comprehensive testdata coverage (13 scenarios).

The single TestAnalyzer function is exemplary: it delegates to analysistest.RunWithSuggestedFixes, which validates both diagnostics and fixes against testdata. This avoids unit test inflation (test:prod ratio 0.07:1) while maintaining high behavioral coverage. Pattern is consistent with 40+ other linters in the repository.

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 · 29 AIC · ⌖ 7.32 AIC · ⊞ 6.8K ·
Comment /review to run again

@github-actions github-actions Bot 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.

✅ Test Quality Sentinel: 95/100. 0% implementation tests (threshold: 30%). No violations. Pattern is consistent with 40+ other linters.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

🏗️ Design Decision Gate — ADR Required

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

📄 Draft ADR committed: docs/adr/44820-add-stringscountcontains-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 couldn't 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-44820: Add stringscountcontains 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., 44820-add-stringscountcontains-linter.md for PR #44820).

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

@github-actions github-actions Bot 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.

Skills-Based Review 🧠

Applied /tdd — 2 small test-coverage gaps found; no blocking issues in the implementation.

📋 Key Themes & Highlights

Issues

  • Missing yoda-LEQ test case: 0 >= strings.Count(s, sub) is the 6th yoda variant documented in the PR description but absent from testdata.
  • No nolint smoke-test: The //nolint:stringscountcontains suppression path is wired but never exercised in testdata.

Positive Highlights

  • ✅ All 6 comparison operators × 2 orientations (normal + yoda) handled correctly in the implementation
  • SuggestedFix with TextEdit enables automated batch repair — consistent with stringsindexcontains
  • normalizeOperands / matchCountComparison decomposition is clean and readable
  • filecheck.IsTestFile + nolint.HasDirective guards are in place
  • ✅ Documentation, README entry, and spec_test.go registration are all correct

@copilot please address the review comments above.

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

return 0 == strings.Count(s, sub) // want `use !strings\.Contains\(s, sub\) instead of strings\.Count comparison`
}

func badYodaLSS(s, sub string) bool {

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.

[/tdd] The yoda-LEQ case (0 >= strings.Count(s, sub)) is missing from testdata — 5 of the 6 yoda variants are covered but this one is absent.

💡 Suggested addition

After line 45 (badYodaLSS), add:

func badYodaLEQ(s, sub string) bool {
	return 0 >= strings.Count(s, sub) // want `use !strings\.Contains\(s, sub\) instead of strings\.Count comparison`
}

And its golden counterpart:

func badYodaLEQ(s, sub string) bool {
	return !strings.Contains(s, sub) // want `use !strings\.Contains\(s, sub\) instead of strings\.Count comparison`
}

The peer linter stringsindexcontains has a gap of its own (4 yoda cases vs 6), so it is fine to note this is already a step up — but since all 6 operators are documented in the PR description, full testdata coverage would prevent a silent regression if the LEQ case were ever broken.

@copilot please address this.

return
}

countCall, negated, matched := matchCountComparison(pass, expr)

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.

[/tdd] The //nolint:stringscountcontains directive is documented as supported but there is no testdata case exercising it — a future refactor could silently break nolint suppression.

💡 Suggested testdata addition

Add a case in testdata/src/stringscountcontains/stringscountcontains.go:

func nolintCase(s, sub string) bool {
	return strings.Count(s, sub) > 0 (nolint/redacted):stringscountcontains
}

This function should produce no diagnostic, confirming the nolint path is exercised.

@copilot please address this.

@github-actions github-actions Bot 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.

Three gaps need fixing before merge: a missing testdata case, a broken normalizeOperands contract, and an untested suppression path.

Findings summary

Medium — missing badYodaLEQ test case

0 >= strings.Count(s, sub) is the only yoda variant not covered by testdata, leaving the token.LEQ flip path unverified. All other yoda variants have tests; this inconsistency is an explicit gap.

Medium — normalizeOperands contract is broken

When neither side of the binary expression is a strings.Count call, the function returns (expr.Y, expr.X, true)flipped=true despite Count being absent. The caller's early-return saves us today, but the invariant is wrong and will silently mislead future readers or callers.

Medium — //nolint:stringscountcontains suppression has no test

run() calls nolint.HasDirective but no testdata case asserts it suppresses a diagnostic. A regression here would be invisible.

Low — diagnostic message/fix disagree for aliased imports

pkgText drives the fix (correct), but the message hardcodes strings.Contains. Minor but visibly inconsistent in tooling output.

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

Comments that could not be inline-anchored

pkg/linters/stringscountcontains/testdata/src/stringscountcontains/stringscountcontains.go:47

Missing test for badYodaLEQ — the only documented yoda variant without coverage.

<details>
<summary>💡 Details and suggested fix</summary>

The PR comment in normalizeOperands lists 0 &gt;= strings.Count(s, sub) as a pattern (via 1 &gt; strings.Count above it), but neither the testdata nor the golden file includes it. When flipped, GEQLEQ with litVal=0 should hit the token.LEQ branch and return negated=true. Without a test case, a regression here would be silent.

Add to the…

pkg/linters/stringscountcontains/stringscountcontains.go:255

normalizeOperands returns flipped=true even when neither operand is a strings.Count call — the contract is silently wrong.

<details>
<summary>💡 Details and suggested fix</summary>

When expr.X is not a Count call, the function unconditionally returns (expr.Y, expr.X, true) — including when expr.Y is also not a Count call. The caller does check asStringsCountCall on the returned left and returns early if it fails, so no wrong diagnostic is emitted today. But flipped=true

pkg/linters/stringscountcontains/stringscountcontains_test.go:15

No test for //nolint:stringscountcontains suppression — a documented feature with zero coverage.

<details>
<summary>💡 Details and suggested fix</summary>

run() calls nolint.HasDirective to suppress diagnostics, but no testdata case exercises this path. If nolint.BuildLineIndex or HasDirective silently breaks for this linter name, it goes undetected.

Add a testdata function with a //nolint:stringscountcontains annotation that should produce no diagnostic:

func suppress</details>

<details><summary>pkg/linters/stringscountcontains/stringscountcontains.go:163</summary>

**Diagnostic message hardcodes `strings.Contains` but the fix uses the actual import aliasthey diverge for aliased imports.**

&lt;details&gt;
&lt;summary&gt;💡 Details and suggested fix&lt;/summary&gt;

`countPkgText` returns the local identifier (e.g., `str` for `import str &quot;strings&quot;`). The `SuggestedFix` correctly uses `pkgText`, producing `str.Contains(...)`. But the diagnostic message is hardcoded:

```go
msg = fmt.Sprintf(&quot;use strings.Contains(%s, %s) instead ...&quot;, sText, subText)

For an aliased im…

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.

[linter-miner] feat(linters): add stringscountcontains linter

3 participants