Skip to content

feat(protocol): expose analyzer quick fixes in scan output#9

Merged
aksOps merged 2 commits into
mainfrom
feat/issue-quick-fixes
May 25, 2026
Merged

feat(protocol): expose analyzer quick fixes in scan output#9
aksOps merged 2 commits into
mainfrom
feat/issue-quick-fixes

Conversation

@aksOps
Copy link
Copy Markdown
Contributor

@aksOps aksOps commented May 25, 2026

Summary

  • Engine has carried Issue.quickFixes() since sonarlint-core 11.x; until now the daemon dropped them at the protocol-DTO boundary. This change carries quick fixes end-to-end so the agent pipeline and JSON CLI users get actionable remediation edits instead of guessing.
  • New DTOs: TextEdit, FileEdit, QuickFix (records, Jackson-roundtrip-safe, null-normalising compact constructors, defensively-copied lists).
  • Issue DTO gains a List<QuickFix> quickFixes field with a backwards-compatible 9-arg constructor that delegates with List.of() so the ~15 existing construction sites keep compiling.
  • IssueMapper.mapQuickFixes(List<engine.QuickFix>, baseDir) translates the engine's QuickFix / ClientInputFileEdit / TextEdit tree, resolving each edit's target path through the same resolveFilePath the primary issue uses.

Design

  • Additive JSON shape. Existing consumers that don't read .quickFixes are unaffected; new consumers gain a per-issue remediation list.
  • Path convention shared with Issue.filePath. Quick-fix file paths are baseDir-relative, '/'-separated — so a UI/agent can render edits the same way it renders the primary issue position.
  • Position convention shared with Issue.startLine/startColumn. 1-indexed lines, 0-indexed columns, matching what LSP Diagnostic consumers already expect.

Test plan (all verified locally)

  • 356/356 tests pass (was 336 baseline; +20 new)
  • Per-DTO Jackson roundtrip + null-normalisation + defensive-copy contracts (3 new *Test classes, 13 tests)
  • Issue DTO: 9-arg ctor defaults quickFixes to empty; null is normalised; roundtrip preserves a fully-populated quick-fix tree (+3 tests)
  • IssueMapper mapping logic: single-edit roundtrip, empty list, multi-file (+3 tests)
  • End-to-end integration in AnalysisServiceTest: the existing UtilityClass.java fixture raises java:S1118; assert the resulting protocol Issue carries at least one QuickFix with at least one FileEdit + TextEdit, and that the quick-fix file path matches the issue's file path. This is the canary that proves the real sonar-java 8.29 analyzer emits a quick fix that reaches our JSON output — not just schema correctness.

Version

  • pom.xml bumped 0.2.0-SNAPSHOT → 0.3.0-SNAPSHOT (next dev cycle after the 0.2.0 release we tagged on main).

Sample JSON shape after this change

{
  "ruleKey": "java:S1118",
  "filePath": "com/example/UtilityClass.java",
  "startLine": 3, "startColumn": 13, "endLine": 3, "endColumn": 26,
  "severity": "MAJOR", "type": "CODE_SMELL",
  "message": "Add a private constructor to hide the implicit public one.",
  "quickFixes": [{
    "message": "Add a private constructor",
    "fileEdits": [{
      "filePath": "com/example/UtilityClass.java",
      "edits": [{
        "startLine": 3, "startColumn": 0, "endLine": 3, "endColumn": 0,
        "replacement": "    private UtilityClass() {}\n"
      }]
    }]
  }]
}

aksOps added 2 commits May 25, 2026 12:05
Engine.Issue has carried quickFixes() since sonarlint-core 11.x and the
sonar-plugin-api has shipped the NewIssue.addQuickFix(...) builder for
several major versions. Until now the daemon silently dropped them at
the protocol-DTO boundary, leaving downstream consumers (the agent
pipeline and the JSON CLI users) to guess at remediations.

This change carries quick fixes end-to-end:

Protocol DTOs (new)
  - TextEdit(startLine, startColumn, endLine, endColumn, replacement) —
    a single in-file replacement edit
  - FileEdit(filePath, edits) — all TextEdits a quick fix applies to one
    file; null-normalised and defensively copied
  - QuickFix(message, fileEdits) — an analyzer-supplied remediation,
    possibly spanning multiple files

Issue DTO
  - Added List<QuickFix> quickFixes field (compact ctor null-normalises
    and copies). A backwards-compatible 9-arg constructor delegates with
    List.of(), so the ~15 existing test call sites keep compiling.

IssueMapper
  - mapQuickFixes(List<engine.QuickFix>, baseDir) translates the engine's
    QuickFix / ClientInputFileEdit / TextEdit tree into our DTOs,
    resolving each edit's target file path through the same
    resolveFilePath() the primary issue uses (so quick-fix paths share
    the baseDir-relative, '/'-separated convention).
  - The original 5-arg map(...) overload is preserved and delegates to a
    new 6-arg overload that takes the quickFixes list.

Test coverage (356/356 pass, +20 new):
  - Per-DTO Jackson roundtrip + null-normalisation + defensive-copy
    contracts (TextEditTest x3, FileEditTest x5, QuickFixTest x5,
    IssueTest +3).
  - IssueMapperTest +3: single-edit roundtrip, empty list, multi-file.
  - AnalysisServiceTest +1: the existing UtilityClass.java fixture
    raises java:S1118; assert the resulting protocol Issue carries at
    least one QuickFix with one FileEdit + TextEdit, and that the
    quick-fix file path matches the issue's file path. This is the
    canary that proves the full daemon -> JSON pipeline emits real
    analyzer-supplied edits, not just the schema.

JSON shape is additive: existing consumers that don't read .quickFixes
remain unaffected; new consumers gain a per-issue remediation list.

Targeting 0.3.0-SNAPSHOT (0.2.0 was tagged at HEAD of main).
The previous commit added quickFixes to the protocol DTO. JsonReporter
builds the per-issue ObjectNode by hand and was never updated, so the
new field rode in the daemon -> CLI pipeline but never reached the
user-facing JSON.

This adds a quickFixNode() builder and emits a "quickFixes" array on
issues that carry at least one analyzer-supplied fix. Issues with no
quick fix omit the field entirely (rather than emitting "quickFixes":[])
to keep the wire format token-lean for the common case.

Schema:
  "quickFixes": [{
    "message": "Replace with isEmpty()",
    "fileEdits": [{
      "filePath": "src/main/java/.../SonarCommand.java",
      "edits": [
        { "startLine":254, "startColumn":20, "endLine":254, "endColumn":20,
          "replacement":"!" },
        ...
      ]
    }]
  }]

Field order in each node matches the DTO so emitted JSON roundtrips
cleanly through Json.mapper().readValue(QuickFix.class).

Test coverage (+2 in ReportersTest, 358/358 total):
  - jsonOmitsEmptyQuickFixes: existing WITH_ISSUES fixture has no
    quick fixes; the field must be absent from the rendered JSON.
  - jsonEmitsQuickFixes: a synthetic Issue carrying a real QuickFix
    serializes through JsonReporter; verifies message, fileEdits[0]
    .filePath, and edits[0].{startLine,startColumn,endLine,endColumn,
    replacement} all roundtrip with the right values.

Self-scan canary (ran sonar against this worktree): 3 of 29 issues
now carry quick fixes (2x java:S7158 "isEmpty()" replacement in
SonarCommand.java, 1x java:S1450 field-to-local in DaemonServer.java).
Real machine-applicable edits, not guesses.

SARIF reporter is intentionally unchanged in this commit — SARIF has
its own 'fixes' schema that warrants a separate, dedicated mapping.
@sonarqubecloud
Copy link
Copy Markdown

@aksOps aksOps merged commit 707fec8 into main May 25, 2026
12 checks passed
@aksOps aksOps deleted the feat/issue-quick-fixes branch May 25, 2026 12:44
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.

1 participant