Skip to content

refactor(linters): consolidate duplicated import-editing and file-lookup helpers into internal/astutil#47912

Merged
pelikhan merged 6 commits into
mainfrom
copilot/refactor-consolidate-import-helpers
Jul 25, 2026
Merged

refactor(linters): consolidate duplicated import-editing and file-lookup helpers into internal/astutil#47912
pelikhan merged 6 commits into
mainfrom
copilot/refactor-consolidate-import-helpers

Conversation

Copilot AI commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Five import-manipulation helpers were copy-pasted nearly verbatim between sprintfbool and sprintfint, and a file-containing-position lookup was re-implemented inline in 5+ packages. This promotes both clusters into internal/astutil as shared, tested utilities.

New helpers in internal/astutil

Function Replaces
FileForPos(files, pos) inline for _, f := range pass.Files { if f.Pos() <= pos ...} (5 sites)
CountPkgUsesInFile(pass, file, pkgPath) local countPkgUsesInFile in sprintfbool and sprintfint
ImportSpecLineRange(fset, spec) local importSpecLineRange in both packages
AddImportEdit(pass, file, pkg) local addImportEdit in both packages
RemoveImportEdit(fset, file, pkg) local removeImportEdit in both packages
SwapImportEdits(fset, file, addPkg, removePkg) local addStrconvRemoveFmtEdits in both packages

Call-site updates

  • sprintfbool — removes 7 local functions (~190 lines); import-presence checks switch from importSpecPathEquals to astutil.ImportedAs (already handles backtick-quoted paths correctly)
  • sprintfint — removes 5 local functions + 1 inline loop (~170 lines)
  • writebytestring, bytescomparestring, uncheckedtypeassertion — inline fileForPos loops replaced with astutil.FileForPos

Note: bytescomparestring's buildBytesImportTextEdit is intentionally not migrated to AddImportEdit — it inserts the new import first in single-import expansion, which differs from the shared helper's trailing-append behavior and is covered by existing golden tests.

Tests

Added unit tests for FileForPos, CountPkgUsesInFile, and ImportSpecLineRange in astutil_test.go. All existing linter golden-file tests continue to pass.


Generated by 👨‍🍳 PR Sous Chef · gpt54 · 17.6 AIC · ⌖ 8.18 AIC · ⊞ 7.1K ·
Comment /souschef to run again

…ernal/astutil

Add FileForPos, CountPkgUsesInFile, ImportSpecLineRange, AddImportEdit,
RemoveImportEdit, and SwapImportEdits to pkg/linters/internal/astutil.

Replace duplicated local implementations in:
- sprintfbool: remove 7 local functions (countPkgUsesInFile, addStrconvRemoveFmtEdits,
  addImportEdit, removeImportEdit, importSpecLineRange, fileForPos, importSpecPathEquals)
- sprintfint: remove 5 local functions + inline fileForPos loop
- writebytestring: remove local fileForPos function
- bytescomparestring: replace 2 inline fileForPos loops
- uncheckedtypeassertion: replace 1 inline fileForPos loop

Add unit tests for FileForPos, CountPkgUsesInFile, and ImportSpecLineRange.

Closes #47897

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Refactor consolidate duplicated import-editing and file-lookup helpers refactor(linters): consolidate duplicated import-editing and file-lookup helpers into internal/astutil Jul 25, 2026
Copilot AI requested a review from pelikhan July 25, 2026 05:07
@pelikhan
pelikhan marked this pull request as ready for review July 25, 2026 05:08
Copilot AI review requested due to automatic review settings July 25, 2026 05:08

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

Consolidates repeated AST file lookup and import-editing logic into shared internal/astutil helpers.

Changes:

  • Adds shared file-position and import-editing utilities.
  • Migrates five linters to the shared helpers.
  • Adds focused unit tests for core utility behavior.
Show a summary per file
File Description
pkg/linters/internal/astutil/astutil.go Adds shared AST and import-editing helpers.
pkg/linters/internal/astutil/astutil_test.go Tests new lookup and import-analysis helpers.
pkg/linters/sprintfbool/sprintfbool.go Replaces duplicated helper implementations.
pkg/linters/sprintfint/sprintfint.go Replaces duplicated and inline logic.
pkg/linters/writebytestring/writebytestring.go Uses shared file lookup.
pkg/linters/bytescomparestring/bytescomparestring.go Uses shared file lookup.
pkg/linters/uncheckedtypeassertion/uncheckedtypeassertion.go Uses shared file lookup for parent maps.

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: 0
  • Review effort level: Medium

@github-actions

github-actions Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

@github-actions

github-actions Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

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

@github-actions

github-actions Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

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

@github-actions

github-actions Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality 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.

Review: consolidate import-editing helpers into internal/astutil

The refactoring is clean and well-motivated — 360+ lines of near-verbatim duplication removed, good unit tests added, and the intentional non-migration of buildBytesImportTextEdit is clearly documented.

One non-blocking suggestion (inline comment posted):

AddImportEdit takes *analysis.Pass but uses only pass.Fset, while its sibling RemoveImportEdit correctly takes *token.FileSet directly. Aligning the signature would remove a framework coupling and make the function easier to unit-test in isolation.

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · sonnet46 · 29.6 AIC · ⌖ 4.63 AIC · ⊞ 5K

// choosing the least-invasive insertion point: append to an existing grouped
// block, convert a single non-grouped import to a grouped block, or insert a
// standalone declaration after the package name.
func AddImportEdit(pass *analysis.Pass, file *ast.File, pkg string) (analysis.TextEdit, 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.

API inconsistency: AddImportEdit takes *analysis.Pass but only uses pass.Fset

Its sibling RemoveImportEdit correctly takes *token.FileSet directly, making it easy to unit-test and reuse outside analysis passes. AddImportEdit takes the full *analysis.Pass solely to access pass.Fset on the NodeText call inside the single-import expansion branch — an unnecessary coupling.

Suggested fix:

func AddImportEdit(fset *token.FileSet, file *ast.File, pkg string) (analysis.TextEdit, bool) {
    ...
    specText := NodeText(fset, genDecl.Specs[0])
    ...
}

All call sites already have pass.Fset available and can pass it directly.

@copilot please address this.

@github-actions

Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

Test Quality Score: 100/100 — Excellent

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

📊 Metrics (3 tests)
Metric Value
Analyzed 3 (Go: 3, JS: 0)
✅ Design 3 (100%)
⚠️ Implementation 0 (0%)
Edge/error coverage 3 (100%)
Duplicate clusters 0
Inflation No (0.47:1 ratio)
🚨 Violations 0
Test File Classification Issues
TestFileForPos astutil_test.go:543 Design, high-value 4 assertions, covers boundary cases
TestCountPkgUsesInFile astutil_test.go:581 Design, high-value 2 assertions, edge cases for existing/missing packages
TestImportSpecLineRange astutil_test.go:607 Design, high-value 3 assertions, verifies range bounds and containment

Verdict

Passed. 0% implementation tests (threshold: 30%). All tests are design-level behavioral contracts with comprehensive edge-case coverage and descriptive failure messages. No code-quality violations detected.

🧪 Test quality analysis by Test Quality Sentinel · haiku45 · 18 AIC · ⌖ 8.37 AIC · ⊞ 8.1K ·
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%).

…rs into internal/astutil

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 (316 new lines in pkg/linters/internal/astutil/) but does not have a linked Architecture Decision Record (ADR).

📄 Draft ADR committed: docs/adr/47912-consolidate-import-editing-helpers-into-astutil.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 (particularly the Deciders field and any organizational constraints), refine the decision rationale, and confirm the alternatives listed reflect the options you actually 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-47912: Consolidate Import-Editing and File-Lookup Helpers into internal/astutil

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., 47912-consolidate-import-editing-helpers-into-astutil.md for PR #47912).

🏗️ ADR gate enforced by Design Decision Gate 🏗️ · sonnet46 · 62 AIC · ⌖ 11.2 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 and /codebase-design — requesting changes on two issues.

📋 Key Themes & Highlights

Key Themes

  • Test coverage gap: AddImportEdit, RemoveImportEdit, and SwapImportEdits each have 3–4 structural branches with no unit tests, unlike the simpler helpers added in the same PR.
  • Potential edit-ordering regression: SwapImportEdits' grouped case emits edits in descending position order, reversed from the original addStrconvRemoveFmtEdits. Worth verifying this is safe for the analysis framework.
  • Minor: importSpecPathEquals is now embedded in the shared package as unexported; exporting it would complete the API surface.

Positive Highlights

  • ✅ Excellent reduction in duplication — ~360 lines removed across sprintfbool, sprintfint, writebytestring, bytescomparestring, uncheckedtypeassertion.
  • ✅ Good switch from raw string comparison to astutil.ImportedAs — handles backtick-quoted paths correctly.
  • ✅ Unit tests added for FileForPos, CountPkgUsesInFile, ImportSpecLineRange — good pattern to follow for the missing helpers.
  • ✅ PR description clearly documents the one intentional non-migration (buildBytesImportTextEdit).

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

Comments that could not be inline-anchored

pkg/linters/internal/astutil/astutil.go:549

[/tdd] AddImportEdit, RemoveImportEdit, and SwapImportEdits — the three most complex new helpers with multiple branching paths — have no unit tests, while simpler peers (FileForPos, CountPkgUsesInFile, ImportSpecLineRange) do. If a branch is wrong it will silently produce a malformed TextEdit or a no-op fix.

<details>
<summary>💡 Suggested coverage</summary>

For each helper, cover the distinct structural cases documented in the godoc:

  • AddImportEdit: grouped block (append)…
pkg/linters/internal/astutil/astutil.go:240

[/codebase-design] The edit ordering in SwapImportEdits' grouped case is reversed relative to the original sprintfbool.addStrconvRemoveFmtEdits. The original returned [lineStart remove, Rparen insert] (ascending positions); this helper returns [Rparen insert, lineStart remove] (descending). If the analysis framework or downstream consumers require non-overlapping edits in ascending position order, the new order can produce unexpected results.

<details>
<summary>💡 Suggested fix</…

pkg/linters/internal/astutil/astutil.go:474

[/codebase-design] importSpecPathEquals is a private helper embedded inside the shared astutil package. Since callers outside this package (e.g., future linters) will need to check import-spec equality too, keeping it unexported creates pressure to re-duplicate it. Exporting it as ImportSpecPathEquals would complete the surface area of the import-manipulation API.

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

REQUEST_CHANGES — two blocking issues, one medium gap

The refactor is directionally correct and reduces significant duplication, but there is one likely behavioral regression and one missing safety measure in the new shared helpers.

Blocking findings

1. reverses TextEdit position order (high)
The new grouped-block branch emits — descending position order. Both original implementations always emitted delete first (lower Pos), then insert (higher Pos). If the go/analysis framework sorts edits before applying, this is harmless; if it doesn't, the grouped-block swap path will corrupt the generated fix. The fix is a one-line swap of the two slice elements.

2. Unsanitized pkg string in AddImportEdit / SwapImportEdits (medium)
Package path is concatenated directly into import source text. As a public shared helper this needs strconv.Quote(pkg) to stay safe for all future callers.

3. Missing unit tests for AddImportEdit, RemoveImportEdit, SwapImportEdits (medium)
Three of the six new helpers have no direct unit tests. The golden-file coverage is too coarse to catch TextEdit position bugs.

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

Comments that could not be inline-anchored

pkg/linters/internal/astutil/astutil.go:229

SwapImportEdits emits TextEdits in descending position order, inverting the order used by both original implementations.

The new helper emits insert-at-Rparen first, then delete-spec-line. Since lineStart &lt; Rparen, this is descending position order. Both sprintfbool and sprintfint consistently emitted delete (lower pos) first, then insert (higher pos). If the framework assumes ascending order or applies edits without sorting, the output will be corrupted.

<details>
<summary>💡 S…

pkg/linters/internal/astutil/astutil.go:137

AddImportEdit performs unsanitized string concatenation with the pkg argument to produce import source text.

As a newly public API, any caller that passes a malformed or attacker-controlled package path (containing quotes, backslashes, or newlines) will produce a syntactically broken import statement that silently corrupts the suggested fix.

<details>
<summary>💡 Suggested fix</summary>

Use strconv.Quote so the output is always a well-formed string literal:

NewText: []byte(&quot;…

</details>

<details><summary>pkg/linters/internal/astutil/astutil_test.go:346</summary>

**`AddImportEdit`, `RemoveImportEdit`, and `SwapImportEdits` have no direct unit testsonly the pre-existing golden-file tests cover them indirectly.**

Three of the six new public helpers are completely untested in `astutil_test.go`. Given that these are the most structurally complex helpers (they mutate import blocks), a regression in their edge cases (zero imports, single ungrouped import, grouped single-spec, multi-spec) won&#39;t be caught without explicit unit tests.

&lt;details&gt;
&lt;summary&gt;💡 …

</details>

@github-actions

Copy link
Copy Markdown
Contributor

Hey @copilot-swe-agent 👋 — great work executing the refactor plan from #47897! This consolidation of duplicated import-editing and file-lookup helpers into internal/astutil is well-scoped and well-executed.

What stands out:

  • ✓ Comprehensive implementation of P1 recommendations (helpers + tests)
  • ✓ Thoughtful edge-case handling (intentional non-migration of bytescomparestring's buildBytesImportTextEdit)
  • ✓ Solid test coverage for new FileForPos, CountPkgUsesInFile, and ImportSpecLineRange utilities
  • ✓ Clear explanation with call-site mapping
  • ✓ Includes ADR documentation

This PR is ready for maintainer review and appears aligned with the project's contribution guidelines and refactoring priorities. The consolidation eliminates ~360 lines of duplicated code across 5+ packages while improving maintainability.

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 · sonnet46 · 59.5 AIC · ⌖ 7.62 AIC · ⊞ 6.2K ·

@gh-aw-bot

Copy link
Copy Markdown
Collaborator

@copilot please run the pr-finisher skill, address the unresolved blocking review feedback, complete the ADR link/body follow-up if still needed, and rerun checks once the branch is up to date. Priority review items:

Generated by 👨‍🍳 PR Sous Chef · gpt54 · 17.6 AIC · ⌖ 8.18 AIC · ⊞ 7.1K ·
Comment /souschef to run again

Copilot AI and others added 2 commits July 25, 2026 06:28
…kg paths, add unit tests for AddImportEdit/RemoveImportEdit/SwapImportEdits

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

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

Copilot AI commented Jul 25, 2026

Copy link
Copy Markdown
Contributor Author

@copilot please run the pr-finisher skill, address the unresolved blocking review feedback, complete the ADR link/body follow-up if still needed, and rerun checks once the branch is up to date. Prio...

Addressed all three blocking items:

  1. SwapImportEdits edit order — in the grouped multi-import case, the deletion edit (lower source position) now appears before the insertion edit (at Rparen), satisfying the position-ordering requirement. The TestSwapImportEdits/grouped_block_with_multiple_imports subtest explicitly asserts the sort invariant.

  2. Safe package-path quotingAddImportEdit and SwapImportEdits now use strconv.Quote(pkg) instead of "\"" + pkg + "\"". (RemoveImportEdit only reads the path via importSpecPathEquals; it doesn't emit path literals, so no change was needed there.)

  3. Unit tests — added TestAddImportEdit, TestRemoveImportEdit, and TestSwapImportEdits covering grouped/ungrouped import blocks, sole-spec vs. multi-spec grouped blocks, missing imports, and edit-ordering validation. All 35 astutil tests pass.

Copilot AI requested a review from gh-aw-bot July 25, 2026 06:30
@github-actions

Copy link
Copy Markdown
Contributor

🤖 PR Triage

Field Value
Category refactor
Risk medium
Score 52 / 100
Score breakdown impact 32 + urgency 8 + quality 12
Action fast_track

Rationale: Consolidates 5 copy-pasted import-manipulation helpers and a file-position lookup into internal/astutil, removing real duplication risk. CHANGES_REQUESTED present — resolve before merge. CI unknown.

Next step: Address review feedback and confirm CI passes.

Generated by 🔧 PR Triage Agent · sonnet46 · 44.5 AIC · ⌖ 5.13 AIC · ⊞ 5.7K ·

@pelikhan

Copy link
Copy Markdown
Collaborator

@copilot run pr-finisher skill

Copilot AI commented Jul 25, 2026

Copy link
Copy Markdown
Contributor Author

@copilot run pr-finisher skill

PR-finisher run complete. All previously blocking items were addressed in commits e32f9fb and eb4f358 (from the prior run). Local validation is clean:

  • make fmt — no changes
  • make test-unit — all 35 astutil tests + all 5 affected linter suites pass
  • make agent-report-progress — zero lint errors, lock files in sync, tests pass

Ready for human CI re-trigger and merge.

@pelikhan
pelikhan merged commit ef97a18 into main Jul 25, 2026
24 checks passed
@pelikhan
pelikhan deleted the copilot/refactor-consolidate-import-helpers branch July 25, 2026 08:48
@github-actions

Copy link
Copy Markdown
Contributor

🎉 This pull request is included in a new release.

Release: v0.83.3

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[refactor] Consolidate duplicated import-editing and file-lookup helpers across pkg/linters

4 participants