Skip to content

Commit 17ef58e

Browse files
committed
Apply comment
1 parent 25a4c27 commit 17ef58e

File tree

2 files changed

+205
-253
lines changed

2 files changed

+205
-253
lines changed
Lines changed: 71 additions & 253 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016-2025 DiffPlug
2+
* Copyright 2025 DiffPlug
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -15,307 +15,125 @@
1515
*/
1616
package com.diffplug.spotless.extra.middleware;
1717

18-
import java.io.File;
19-
import java.io.IOException;
20-
import java.nio.charset.StandardCharsets;
21-
import java.util.ArrayList;
2218
import java.util.List;
23-
import java.util.Map;
24-
import java.util.Objects;
25-
import java.util.regex.Matcher;
26-
import java.util.regex.Pattern;
2719

28-
import com.diffplug.spotless.Formatter;
2920
import com.diffplug.spotless.FormatterStep;
30-
import com.diffplug.spotless.LineEnding;
3121
import com.diffplug.spotless.Lint;
32-
import com.diffplug.spotless.LintState;
33-
import com.diffplug.spotless.extra.integration.DiffMessageFormatter;
3422

3523
/**
36-
* ReviewDogGenerator generates ReviewDog formatted output for linting issues
37-
* based on Spotless formatting results.
24+
* Utility class for generating ReviewDog compatible output in the rdjsonl format.
25+
* This class provides methods to create diff and lint reports that can be used by ReviewDog.
3826
*/
3927
public final class ReviewDogGenerator {
40-
private final Formatter formatter;
41-
private final File projectDir;
4228

43-
/**
44-
* Constructor for ReviewDogGenerator.
45-
*
46-
* @param projectDir the root directory of the project
47-
* @param steps the list of FormatterStep to apply
48-
*/
49-
public ReviewDogGenerator(File projectDir, List<FormatterStep> steps) {
50-
this.projectDir = projectDir;
51-
this.formatter = Formatter.builder()
52-
.encoding(StandardCharsets.UTF_8)
53-
.lineEndingsPolicy(LineEnding.UNIX.createPolicy())
54-
.steps(steps)
55-
.build();
29+
private ReviewDogGenerator() {
30+
// Prevent instantiation
5631
}
5732

5833
/**
59-
* Generates ReviewDog formatted output for the given list of files.
34+
* Generates a ReviewDog compatible JSON line (rdjsonl) for a diff between
35+
* the actual content and the formatted content of a file.
6036
*
61-
* @param files the list of files to check
62-
* @return a String containing ReviewDog formatted lint messages with code suggestions
63-
* @throws IOException if file reading fails
37+
* @param path The file path
38+
* @param actualContent The content as it currently exists in the file
39+
* @param formattedContent The content after formatting is applied
40+
* @return A string in rdjsonl format representing the diff
6441
*/
65-
public String generateReviewDogFormat(List<File> files) throws IOException {
66-
StringBuilder reviewDogOutput = new StringBuilder();
67-
68-
for (File file : files) {
69-
LintState lintState = LintState.of(formatter, file);
70-
71-
if (!lintState.getDirtyState().isClean()) {
72-
String relativePath = getRelativePath(file);
73-
74-
Map.Entry<Integer, String> diffResult = DiffMessageFormatter.diff(
75-
projectDir.toPath(), formatter, file);
76-
77-
List<DiffHunk> hunks = parseDiffHunks(diffResult.getValue());
78-
List<ReviewDogIssue> issues = processLints(lintState, hunks);
79-
80-
for (ReviewDogIssue issue : issues) {
81-
reviewDogOutput.append(formatReviewDogLine(relativePath, issue));
82-
}
83-
}
42+
public static String rdjsonlDiff(String path, String actualContent, String formattedContent) {
43+
if (actualContent.equals(formattedContent)) {
44+
return "";
8445
}
8546

86-
return reviewDogOutput.toString();
87-
}
47+
String diff = createUnifiedDiff(path, actualContent, formattedContent);
8848

89-
/**
90-
* Converts an absolute file path to a relative path based on the project directory.
91-
*
92-
* @param file the file to convert
93-
* @return relative path string
94-
*/
95-
private String getRelativePath(File file) {
96-
return projectDir.toURI().relativize(file.toURI()).getPath();
49+
return String.format(
50+
"{\"message\":{\"path\":\"%s\",\"message\":\"File requires formatting\",\"diff\":\"%s\"}}",
51+
escapeJson(path),
52+
escapeJson(diff));
9753
}
9854

9955
/**
100-
* Parses git-style diff content into structured diff hunks.
56+
* Generates ReviewDog compatible JSON lines (rdjsonl) for lint issues
57+
* identified by formatting steps.
10158
*
102-
* @param diffContent the git-style diff content
103-
* @return list of parsed DiffHunk objects
59+
* @param path The file path
60+
* @param formattedContent The content after formatting is applied
61+
* @param steps The list of formatter steps applied
62+
* @param lintsPerStep The list of lints produced by each step
63+
* @return A string in rdjsonl format representing the lints
10464
*/
105-
private List<DiffHunk> parseDiffHunks(String diffContent) {
106-
List<DiffHunk> hunks = new ArrayList<>();
107-
if (diffContent == null || diffContent.isEmpty()) {
108-
return hunks;
65+
public static String rdjsonlLints(String path, String formattedContent,
66+
List<FormatterStep> steps, List<List<Lint>> lintsPerStep) {
67+
if (lintsPerStep == null || lintsPerStep.isEmpty()) {
68+
return "";
10969
}
11070

111-
Pattern hunkHeaderPattern = Pattern.compile("@@ -(\\d+),(\\d+) \\+(\\d+),(\\d+) @@");
112-
String[] lines = diffContent.split("\n");
113-
114-
DiffHunk currentHunk = null;
115-
StringBuilder hunkContent = new StringBuilder();
71+
StringBuilder builder = new StringBuilder();
11672

117-
for (String line : lines) {
118-
// Skip file header lines (--- and +++)
119-
if (line.startsWith("---") || line.startsWith("+++")) {
73+
for (int i = 0; i < lintsPerStep.size(); i++) {
74+
List<Lint> lints = lintsPerStep.get(i);
75+
if (lints == null || lints.isEmpty()) {
12076
continue;
12177
}
12278

123-
if (line.startsWith("@@")) {
124-
if (currentHunk != null) {
125-
currentHunk.content = hunkContent.toString().trim();
126-
hunks.add(currentHunk);
127-
hunkContent = new StringBuilder();
128-
}
129-
130-
Matcher matcher = hunkHeaderPattern.matcher(line);
131-
if (matcher.find()) {
132-
int originalStart = Integer.parseInt(matcher.group(1));
133-
int originalLength = Integer.parseInt(matcher.group(2));
134-
int newStart = Integer.parseInt(matcher.group(3));
135-
int newLength = Integer.parseInt(matcher.group(4));
136-
137-
currentHunk = new DiffHunk(originalStart, originalLength, newStart, newLength);
138-
currentHunk.header = line;
139-
}
140-
hunkContent.append(line).append("\n");
141-
} else if (currentHunk != null) {
142-
hunkContent.append(line).append("\n");
143-
}
144-
}
145-
146-
if (currentHunk != null) {
147-
currentHunk.content = hunkContent.toString().trim();
148-
hunks.add(currentHunk);
149-
}
150-
151-
return hunks;
152-
}
153-
154-
/**
155-
* Processes lint information and associates them with appropriate diff hunks.
156-
*
157-
* @param lintState the LintState containing all lint information
158-
* @param hunks list of diff hunks
159-
* @return list of ReviewDogIssue
160-
*/
161-
private List<ReviewDogIssue> processLints(LintState lintState, List<DiffHunk> hunks) {
162-
List<ReviewDogIssue> issues = new ArrayList<>();
163-
164-
List<List<Lint>> lintsPerStep = lintState.getLintsPerStep();
165-
166-
for (List<Lint> stepLints : Objects.requireNonNull(lintsPerStep)) {
167-
for (Lint lint : stepLints) {
168-
DiffHunk relevantHunk = findRelevantHunk(hunks, lint.getLineStart());
169-
170-
String suggestion = "";
171-
if (relevantHunk != null) {
172-
suggestion = extractSuggestionFromHunk(relevantHunk);
173-
}
174-
175-
ReviewDogIssue issue = new ReviewDogIssue(
176-
lint.getLineStart(),
177-
1,
178-
lint.getShortCode() + ": " + lint.getDetail(),
179-
suggestion);
180-
181-
issues.add(issue);
182-
}
183-
}
184-
185-
// If no specific lints were found but file is dirty, add a general formatting issue
186-
if (issues.isEmpty() && !hunks.isEmpty()) {
187-
DiffHunk firstHunk = hunks.get(0);
188-
String suggestion = extractSuggestionFromHunk(firstHunk);
189-
190-
issues.add(new ReviewDogIssue(
191-
firstHunk.originalStart,
192-
1,
193-
"General formatting issue: file needs to be reformatted.",
194-
suggestion));
195-
}
196-
197-
return issues;
198-
}
199-
200-
/**
201-
* Finds the hunk that is relevant for a specific line number.
202-
*
203-
* @param hunks list of diff hunks
204-
* @param lineNumber the line number to find (1-based)
205-
* @return the relevant hunk or null if not found
206-
*/
207-
private DiffHunk findRelevantHunk(List<DiffHunk> hunks, int lineNumber) {
208-
for (DiffHunk hunk : hunks) {
209-
if (lineNumber >= hunk.originalStart &&
210-
lineNumber < hunk.originalStart + hunk.originalLength) {
211-
return hunk;
79+
String stepName = (i < steps.size()) ? steps.get(i).getName() : "unknown";
80+
for (Lint lint : lints) {
81+
builder.append(formatLintAsJson(path, lint, stepName)).append('\n');
21282
}
21383
}
21484

215-
// If no exact match is found, find the closest hunk before the line number
216-
DiffHunk closestHunk = null;
217-
int closestDistance = Integer.MAX_VALUE;
218-
219-
for (DiffHunk hunk : hunks) {
220-
if (hunk.originalStart <= lineNumber) {
221-
int distance = lineNumber - hunk.originalStart;
222-
if (distance < closestDistance) {
223-
closestDistance = distance;
224-
closestHunk = hunk;
225-
}
226-
}
227-
}
228-
return closestHunk;
85+
return builder.toString().trim();
22986
}
23087

23188
/**
232-
* Extracts suggestion content from a diff hunk.
233-
*
234-
* @param hunk the diff hunk
235-
* @return the suggestion content
89+
* Creates a unified diff between two text contents.
23690
*/
237-
private String extractSuggestionFromHunk(DiffHunk hunk) {
238-
StringBuilder suggestion = new StringBuilder();
91+
private static String createUnifiedDiff(String path, String actualContent, String formattedContent) {
92+
String[] actualLines = actualContent.split("\\r?\\n", -1);
93+
String[] formattedLines = formattedContent.split("\\r?\\n", -1);
23994

240-
String[] lines = hunk.content.split("\n");
241-
boolean headerProcessed = false;
95+
StringBuilder diff = new StringBuilder();
96+
diff.append("--- a/").append(path).append('\n');
97+
diff.append("+++ b/").append(path).append('\n');
98+
diff.append("@@ -1,").append(actualLines.length).append(" +1,").append(formattedLines.length).append(" @@\n");
24299

243-
for (String line : lines) {
244-
if (!headerProcessed && line.startsWith("@@")) {
245-
headerProcessed = true;
246-
continue;
247-
}
248-
249-
if (headerProcessed) {
250-
if (line.startsWith("+") && !line.startsWith("+++")) {
251-
suggestion.append(line.substring(1));
252-
suggestion.append("\n");
253-
} else if (!line.startsWith("-") && !line.startsWith("---")) {
254-
suggestion.append(line);
255-
suggestion.append("\n");
256-
}
257-
}
100+
for (String line : actualLines) {
101+
diff.append('-').append(line).append('\n');
258102
}
259103

260-
return suggestion.toString().trim();
261-
}
262-
263-
/**
264-
* Formats a single ReviewDog issue line according to ReviewDog's expected format
265-
* with suggestion support.
266-
*
267-
* @param filePath the relative file path
268-
* @param issue the ReviewDogIssue object
269-
* @return formatted string line with suggestion
270-
*/
271-
private String formatReviewDogLine(String filePath, ReviewDogIssue issue) {
272-
StringBuilder builder = new StringBuilder();
273-
274-
builder.append(String.format("%s:%d:%d: %s\n",
275-
filePath, issue.lineNumber, issue.column, issue.message));
276-
277-
if (issue.suggestion != null && !issue.suggestion.isEmpty()) {
278-
builder.append("```suggestion\n");
279-
builder.append(issue.suggestion);
280-
builder.append("\n```\n");
104+
for (String line : formattedLines) {
105+
diff.append('+').append(line).append('\n');
281106
}
282107

283-
return builder.toString();
108+
return diff.toString();
284109
}
285110

286111
/**
287-
* Inner class representing a diff hunk.
112+
* Formats a single lint issue as a JSON line.
288113
*/
289-
private static class DiffHunk {
290-
final int originalStart;
291-
final int originalLength;
292-
final int newStart;
293-
final int newLength;
294-
String header;
295-
String content;
296-
297-
DiffHunk(int originalStart, int originalLength, int newStart, int newLength) {
298-
this.originalStart = originalStart;
299-
this.originalLength = originalLength;
300-
this.newStart = newStart;
301-
this.newLength = newLength;
302-
}
114+
private static String formatLintAsJson(String path, Lint lint, String stepName) {
115+
return String.format(
116+
"{\"message\":{\"path\":\"%s\",\"line\":%d,\"column\":1,\"message\":\"%s: %s\"}}",
117+
escapeJson(path),
118+
lint.getLineStart(),
119+
escapeJson(lint.getShortCode()),
120+
escapeJson(lint.getDetail()));
303121
}
304122

305123
/**
306-
* Inner class representing a ReviewDog issue.
124+
* Escapes special characters in a string for JSON compatibility.
307125
*/
308-
private static class ReviewDogIssue {
309-
final int lineNumber;
310-
final int column;
311-
final String message;
312-
final String suggestion;
313-
314-
ReviewDogIssue(int lineNumber, int column, String message, String suggestion) {
315-
this.lineNumber = lineNumber;
316-
this.column = column;
317-
this.message = message;
318-
this.suggestion = suggestion;
126+
private static String escapeJson(String str) {
127+
if (str == null) {
128+
return "";
319129
}
130+
return str
131+
.replace("\\", "\\\\")
132+
.replace("\"", "\\\"")
133+
.replace("\n", "\\n")
134+
.replace("\r", "\\r")
135+
.replace("\t", "\\t")
136+
.replace("\b", "\\b")
137+
.replace("\f", "\\f");
320138
}
321139
}

0 commit comments

Comments
 (0)