Skip to content

Preserve expression precedence in stringsjoinone autofix replacements#47482

Merged
pelikhan merged 2 commits into
mainfrom
copilot/resolve-issue-stringsjoinone
Jul 23, 2026
Merged

Preserve expression precedence in stringsjoinone autofix replacements#47482
pelikhan merged 2 commits into
mainfrom
copilot/resolve-issue-stringsjoinone

Conversation

Copilot AI commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

stringsjoinone replaced strings.Join([]string{x}, sep) with raw x text, which is unsafe when x is compound and the call is used in tighter postfix contexts (index/slice). That could change behavior or emit non-compiling code after -fix.

  • Autofix correctness: precedence-safe replacement

    • Updated replacement construction to use the element AST node plus text.
    • Added a formatting guard: keep atomic expressions unwrapped; wrap non-atomic expressions in (...) before replacing the full strings.Join(...) call.
    • This preserves the original evaluation grouping when the call is followed by [...] / [i:j].
  • Coverage for known failure modes

    • Added suggested-fix golden cases for:
      • strings.Join([]string{a + b}, "")[0]
      • strings.Join([]string{a + b}, "")[1:]
    • Expected fixes now require:
      • (a + b)[0]
      • (a + b)[1:]

Example of the behavior addressed:

// before fix application
tail := strings.Join([]string{a + b}, "")[1:]

// old (incorrect) autofix
tail := a + b[1:]

// new autofix
tail := (a + b)[1:]

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix stringsjoinone autofix to preserve precedence Preserve expression precedence in stringsjoinone autofix replacements Jul 23, 2026
Copilot AI requested a review from pelikhan July 23, 2026 06:25
@pelikhan
pelikhan marked this pull request as ready for review July 23, 2026 06:34
Copilot AI review requested due to automatic review settings July 23, 2026 06:34

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

Fixes stringsjoinone autofixes to preserve expression precedence.

Changes:

  • Parenthesizes non-atomic replacement expressions.
  • Returns the element AST alongside its source text.
  • Adds index and slice regression tests.
Show a summary per file
File Description
pkg/linters/stringsjoinone/stringsjoinone.go Builds precedence-safe replacements.
pkg/linters/stringsjoinone/testdata/src/stringsjoinone/stringsjoinone.go Adds regression inputs.
pkg/linters/stringsjoinone/testdata/src/stringsjoinone/stringsjoinone.go.golden Verifies parenthesized fixes.

Review details

Tip

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

  • Files reviewed: 3/3 changed files
  • Comments generated: 0
  • Review effort level: Medium

@github-actions

github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

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

No ADR enforcement needed: PR does not have the 'implementation' label and has ≤100 new lines of code in business logic directories (43 additions across 3 files).

@github-actions

github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

@github-actions

github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

@github-actions

github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

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

@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.

The precedence-preservation logic in formatReplacementText is correct: high-precedence expression types (identifiers, literals, parens, selectors, index expressions, call expressions) are passed through as-is, while lower-precedence expressions (binary, unary, etc.) get wrapped in parentheses.

Test coverage for a + b in index and slice contexts confirms the fix.

Minor non-blocking note: *ast.SliceExpr is not in the safe list, so strings.Join([]string{s[1:2]}, "") would produce (s[1:2]) — redundant parens but semantically correct and harmless.

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · sonnet46 15.5 AIC · ⌖ 5.42 AIC · ⊞ 5K

@github-actions

Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

Test Quality Score: 100/100 — Excellent

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

📊 Metrics (2 tests)
Metric Value
Analyzed 2 (Go: 2, JS: 0)
✅ Design 2 (100%)
⚠️ Implementation 0 (0%)
Edge/error coverage 2 (100%)
Duplicate clusters 0
Inflation No (0.43:1)
🚨 Violations 0
Test File Classification Issues
joinOneIndexed testdata/.../stringsjoinone.go:21 design_test None
joinOneSliced testdata/.../stringsjoinone.go:27 design_test None

Note: Tests are expressed as analysistest testdata (// want annotations + golden file) rather than direct _test.go functions. The existing stringsjoinone_test.go correctly carries //go:build !integration and was not modified.

Verdict

Passed. 0% implementation tests (threshold: 30%). Both new test cases are high-value design tests verifying the core behavioral contract: autofix replacements for compound expressions must be wrapped in parentheses to preserve operator precedence (e.g., (a + b)[0], (a + b)[1:]). No violations detected.

🧪 Test quality analysis by Test Quality Sentinel · sonnet46 31.7 AIC · ⌖ 7.69 AIC · ⊞ 7K ·
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: 100/100. 0% implementation tests (threshold: 30%). Both new analysistest scenarios are high-value design tests covering the precedence-preservation contract.

@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.

The core fix is correct — wrapping compound elements in parens before replacing the full strings.Join(...) call properly preserves operator precedence in postfix contexts.

Issues found

Incomplete allowlist in formatReplacementText

*ast.SliceExpr (s[1:3]) and *ast.TypeAssertExpr (x.(string)) fall into the default wrapping case even though neither requires parens. The fix compiles and runs correctly but produces a cosmetically wrong suggestion with spurious outer parens.

Diagnostic message shows parentheses for compound elements

The warning text uses replacementText rather than the raw elemText, so users see use (a + b) directly instead of use a + b directly. The paren-wrapping is an autofix detail and should not leak into the human-readable message.

🔎 Code quality review by PR Code Quality Reviewer · sonnet46 41.7 AIC · ⌖ 4.77 AIC · ⊞ 5.7K
Comment /review to run again

return "(" + elemText + ")"
}
return text, true
}

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.

Missing *ast.SliceExpr and *ast.TypeAssertExpr from the allowlist causes unnecessary wrapping in valid cases.

💡 Details and suggested fix

The allowlist in formatReplacementText omits *ast.SliceExpr and *ast.TypeAssertExpr. Both are already atomic for postfix indexing and don't need parens:

  • []string{s[1:3]} → suggested fix becomes (s[1:3]) instead of s[1:3]
  • []string{x.(string)} → suggested fix becomes (x.(string)) instead of x.(string)

Suggested fix:

case *ast.Ident, *ast.BasicLit, *ast.ParenExpr, *ast.SelectorExpr,
     *ast.IndexExpr, *ast.CallExpr, *ast.SliceExpr, *ast.TypeAssertExpr:
    return elemText

This is not a correctness regression (the wrapped form compiles identically), but it produces misleading suggested fixes for these common patterns.

@@ -84,13 +85,13 @@ func analyzeJoinOne(pass *analysis.Pass, n ast.Node, generatedFiles filecheck.Ge
pass.Report(analysis.Diagnostic{
Pos: call.Pos(),
End: call.End(),

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.

Diagnostic message leaks implementation parens into user-visible output for compound expressions.

💡 Details

When the element is a + b, replacementText becomes (a + b). The diagnostic message then reads:

strings.Join called with a single-element slice; use (a + b) directly

The parentheses are an autofix-internal detail and look confusing in the warning message. The message should use the raw elemText while the SuggestedFix uses replacementText:

Message: fmt.Sprintf("strings.Join called with a single-element slice; use %s directly", elemText),
SuggestedFixes: []analysis.SuggestedFix{{
    Message:   "Replace strings.Join call with " + replacementText,
    TextEdits: []analysis.TextEdit{{NewText: []byte(replacementText), ...}},
}},

@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 /diagnosing-bugs — two focused observations on the fix.

Positive Highlights:

  • Root cause correctly identified at the AST level
  • Regression tests cover both indexing and slicing postfix contexts
  • formatReplacementText is clean and covers common primary-expression cases

Issues Found:

  1. *ast.SliceExpr missing from atomic allowlist (line 141) — s[1:] as a slice element would be unnecessarily parenthesized to (s[1:]). See inline comment.
  2. Diagnostic message uses parenthesized text (line 88) — 'use (a + b) directly' reads awkwardly; elemText is the better fit for the human-facing message. See inline comment.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · sonnet46 40.7 AIC · ⌖ 4.8 AIC · ⊞ 6.7K
Comment /matt to run again


func formatReplacementText(elem ast.Expr, elemText string) string {
switch elem.(type) {
case *ast.Ident, *ast.BasicLit, *ast.ParenExpr, *ast.SelectorExpr, *ast.IndexExpr, *ast.CallExpr:

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.

[/diagnosing-bugs] *ast.SliceExpr is missing from the atomic-expression allowlist, so strings.Join([]string{s[1:]}, sep) would be autofixed to (s[1:]) — syntactically valid, but unnecessarily parenthesized.

💡 Suggested fix

Add *ast.SliceExpr (and *ast.TypeAssertExpr for completeness) to the safe list:

case *ast.Ident, *ast.BasicLit, *ast.ParenExpr, *ast.SelectorExpr,
     *ast.IndexExpr, *ast.SliceExpr, *ast.CallExpr, *ast.TypeAssertExpr:
    return elemText

Both slice expressions (s[1:]) and type assertions (v.(string)) are already at primary-expression precedence in Go — they do not need wrapping when used as the operand of a further [i] or [i:j] suffix.

Companion test case to add:

func joinOneSliceElem(s string) string {
    return strings.Join([]string{s[1:]}, "") // want `strings\.Join called with a single-element slice`
}

Golden:

func joinOneSliceElem(s string) string {
    return s[1:] // want `strings\.Join called with a single-element slice`
}

@copilot please address this.

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.

Fixed in 37c8f45. Added *ast.SliceExpr and *ast.TypeAssertExpr to the allowlist in formatReplacementText, and added a regression test for the s[1:] element case (golden: s[1:] with no spurious parens).

Pos: call.Pos(),
End: call.End(),
Message: fmt.Sprintf("strings.Join called with a single-element slice; use %s directly", elemText),
Message: fmt.Sprintf("strings.Join called with a single-element slice; use %s directly", replacementText),

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.

[/diagnosing-bugs] The diagnostic Message now shows the parenthesized form (e.g. (a + b)) even when the call appears in a plain value position, making the message slightly misleading for users who see it outside a postfix context.

💡 Suggested improvement

Consider using the raw elemText in the diagnostic message and reserving replacementText only for the NewText of the suggested fix:

Message: fmt.Sprintf("strings.Join called with a single-element slice; use %s directly", elemText),
// ...
Message: "Replace strings.Join call with " + replacementText,
// ...
NewText: []byte(replacementText),

This keeps the human-readable message clean ("use a + b directly") while the machine-applied fix still emits the correctly-parenthesized text.

@copilot please address this.

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.

Fixed in 37c8f45. The diagnostic Message now uses the raw elemText (e.g. use a + b directly), while replacementText is kept only for the SuggestedFix message and NewText.

@pelikhan

Copy link
Copy Markdown
Collaborator

@copilot run pr-finisher skill

@pelikhan
pelikhan merged commit 56ac9e7 into main Jul 23, 2026
83 of 85 checks passed
Copilot stopped work on behalf of pelikhan due to an error July 23, 2026 06:58
@github-actions

Copy link
Copy Markdown
Contributor

🎉 This pull request is included in a new release.

Release: v0.83.1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants