-
Notifications
You must be signed in to change notification settings - Fork 184
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #137 from garyttierney/feature/cargo-check-parser
Add support for parsing cargo check JSON output
- Loading branch information
Showing
4 changed files
with
212 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
158 changes: 158 additions & 0 deletions
158
src/main/java/edu/hm/hafner/analysis/parser/CargoCheckParser.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,158 @@ | ||
package edu.hm.hafner.analysis.parser; | ||
|
||
import java.util.Optional; | ||
import java.util.stream.Stream; | ||
|
||
import org.json.JSONArray; | ||
import org.json.JSONObject; | ||
import org.json.JSONTokener; | ||
|
||
import edu.hm.hafner.analysis.Issue; | ||
import edu.hm.hafner.analysis.IssueBuilder; | ||
import edu.hm.hafner.analysis.IssueParser; | ||
import edu.hm.hafner.analysis.ParsingCanceledException; | ||
import edu.hm.hafner.analysis.ParsingException; | ||
import edu.hm.hafner.analysis.ReaderFactory; | ||
import edu.hm.hafner.analysis.Report; | ||
import edu.hm.hafner.analysis.Severity; | ||
|
||
/** | ||
* A parser for {@code rustc} compiler messages in the JSON format emitted by {@code cargo check --message-format | ||
* json}. | ||
* | ||
* @author Gary Tierney | ||
*/ | ||
public class CargoCheckParser extends IssueParser { | ||
private static final long serialVersionUID = 7953467739178377581L; | ||
|
||
/** The {@link #REASON} associated with messages that have code analysis information. */ | ||
private static final String ANALYSIS_MESSAGE_REASON = "compiler-message"; | ||
|
||
/** Top-level key indicating the reason for a message to be emitted, we only care about compiler-message. */ | ||
private static final String REASON = "reason"; | ||
|
||
/** Top-level key containing the code analysis message. */ | ||
private static final String MESSAGE = "message"; | ||
|
||
/** Key for {@code message.code}, an object containing the message category. */ | ||
private static final String MESSAGE_CODE = "code"; | ||
|
||
/** Key for {@code message.code.code}, a string representation of the message category. */ | ||
private static final String MESSAGE_CODE_CATEGORY = "code"; | ||
|
||
/** Key for {@code message.rendered}, the rendered string representation of the message. */ | ||
private static final String MESSAGE_RENDERED = "message"; | ||
|
||
/** Key for {@code message.level}, the string representation of the message severity. */ | ||
private static final String MESSAGE_LEVEL = "level"; | ||
|
||
/** Key for {@code message.spans}, an array of message location information. */ | ||
private static final String MESSAGE_SPANS = "spans"; | ||
|
||
/** Key for {@code message.spans.is_primary}, a boolean indicating if this is the primary error location". */ | ||
private static final String MESSAGE_SPAN_IS_PRIMARY = "is_primary"; | ||
|
||
/** Key for {@code message.spans.file_name}, a relative path to the file the message was emitted for. */ | ||
private static final String MESSAGE_SPAN_FILE_NAME = "file_name"; | ||
|
||
/** Key for {@code message.spans.line_start}, the line number where the associated code starts. */ | ||
private static final String MESSAGE_SPAN_LINE_START = "line_start"; | ||
|
||
/** Key for {@code message.spans.line_end}, the line number where the associated code ends. */ | ||
private static final String MESSAGE_SPAN_LINE_END = "line_end"; | ||
|
||
/** Key for {@code message.spans.column_start}, the column number where the associated code starts. */ | ||
private static final String MESSAGE_SPAN_COLUMN_START = "column_start"; | ||
|
||
/** Key for {@code message.spans.column_end}, the column number where the associated code ends. */ | ||
private static final String MESSAGE_SPAN_COLUMN_END = "column_end"; | ||
|
||
@Override | ||
public Report parse(final ReaderFactory readerFactory) throws ParsingException, ParsingCanceledException { | ||
Report report = new Report(); | ||
|
||
try (Stream<String> lines = readerFactory.readStream()) { | ||
lines.map(line -> (JSONObject) new JSONTokener(line).nextValue()) | ||
.map(this::extractIssue) | ||
.filter(Optional::isPresent) | ||
.map(Optional::get) | ||
.forEach(report::add); | ||
} | ||
|
||
return report; | ||
} | ||
|
||
/** | ||
* Extract the compiler message from a cargo event if any is present. | ||
* | ||
* @param object | ||
* A cargo event that may contain a compiler message. | ||
* | ||
* @return a built {@link Issue} object if any was present. | ||
*/ | ||
private Optional<Issue> extractIssue(final JSONObject object) { | ||
String reason = object.getString(REASON); | ||
|
||
if (!ANALYSIS_MESSAGE_REASON.equals(reason)) { | ||
return Optional.empty(); | ||
} | ||
|
||
JSONObject message = object.getJSONObject(MESSAGE); | ||
JSONObject code = message.getJSONObject(MESSAGE_CODE); | ||
String category = code.getString(MESSAGE_CODE_CATEGORY); | ||
String renderedMessage = message.getString(MESSAGE_RENDERED); | ||
Severity severity = Severity.guessFromString(message.getString(MESSAGE_LEVEL)); | ||
|
||
return parseDetails(message) | ||
.map(details -> new IssueBuilder() | ||
.setFileName(details.fileName) | ||
.setLineStart(details.lineStart) | ||
.setLineEnd(details.lineEnd) | ||
.setColumnStart(details.columnStart) | ||
.setColumnEnd(details.columnEnd) | ||
.setCategory(category) | ||
.setMessage(renderedMessage) | ||
.setSeverity(severity) | ||
.build()); | ||
} | ||
|
||
private Optional<CompilerMessageDetails> parseDetails(final JSONObject message) { | ||
JSONArray spans = message.getJSONArray(MESSAGE_SPANS); | ||
|
||
for (int index = 0; index < spans.length(); index++) { | ||
JSONObject span = spans.getJSONObject(index); | ||
|
||
if (span.getBoolean(MESSAGE_SPAN_IS_PRIMARY)) { | ||
String fileName = span.getString(MESSAGE_SPAN_FILE_NAME); | ||
int lineStart = span.getInt(MESSAGE_SPAN_LINE_START); | ||
int lineEnd = span.getInt(MESSAGE_SPAN_LINE_END); | ||
int columnStart = span.getInt(MESSAGE_SPAN_COLUMN_START); | ||
int columnEnd = span.getInt(MESSAGE_SPAN_COLUMN_END); | ||
|
||
return Optional.of(new CompilerMessageDetails(fileName, lineStart, lineEnd, columnStart, columnEnd)); | ||
} | ||
} | ||
|
||
return Optional.empty(); | ||
} | ||
|
||
/** | ||
* A simplified representation of a primary {@code span} object in the {@code message.spans} an array. | ||
*/ | ||
private static final class CompilerMessageDetails { | ||
private final String fileName; | ||
private final int lineStart; | ||
private final int lineEnd; | ||
private final int columnStart; | ||
private final int columnEnd; | ||
|
||
CompilerMessageDetails(final String fileName, final int lineStart, final int lineEnd, final int columnStart, | ||
final int columnEnd) { | ||
this.fileName = fileName; | ||
this.lineStart = lineStart; | ||
this.lineEnd = lineEnd; | ||
this.columnStart = columnStart; | ||
this.columnEnd = columnEnd; | ||
} | ||
} | ||
} |
48 changes: 48 additions & 0 deletions
48
src/test/java/edu/hm/hafner/analysis/parser/CargoCheckParserTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
package edu.hm.hafner.analysis.parser; | ||
|
||
import edu.hm.hafner.analysis.AbstractParserTest; | ||
import edu.hm.hafner.analysis.IssueParser; | ||
import edu.hm.hafner.analysis.Report; | ||
import edu.hm.hafner.analysis.Severity; | ||
import edu.hm.hafner.analysis.assertj.SoftAssertions; | ||
|
||
/** | ||
* Tests the class {@link CargoCheckParser}. | ||
* | ||
* @author Gary Tierney | ||
*/ | ||
class CargoCheckParserTest extends AbstractParserTest { | ||
CargoCheckParserTest() { | ||
super("CargoCheck.json"); | ||
} | ||
|
||
@Override | ||
protected void assertThatIssuesArePresent(final Report report, final SoftAssertions softly) { | ||
softly.assertThat(report).hasSize(2); | ||
|
||
softly.assertThat(report.get(0)) | ||
.hasFileName("packages/secspc/src/main.rs") | ||
.hasMessage("unused import: `secsp_analysis::input::FileId`") | ||
.hasCategory("unused_imports") | ||
.hasSeverity(Severity.WARNING_NORMAL) | ||
.hasLineStart(14) | ||
.hasLineEnd(14) | ||
.hasColumnStart(5) | ||
.hasColumnEnd(34); | ||
|
||
softly.assertThat(report.get(1)) | ||
.hasFileName("packages/secspc/src/main.rs") | ||
.hasMessage("redundant closure found") | ||
.hasCategory("clippy::redundant_closure") | ||
.hasSeverity(Severity.WARNING_NORMAL) | ||
.hasLineStart(68) | ||
.hasLineEnd(68) | ||
.hasColumnStart(14) | ||
.hasColumnEnd(34); | ||
} | ||
|
||
@Override | ||
protected IssueParser createParser() { | ||
return new CargoCheckParser(); | ||
} | ||
} |
4 changes: 4 additions & 0 deletions
4
src/test/resources/edu/hm/hafner/analysis/parser/CargoCheck.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
{"reason":"compiler-artifact","package_id":"smol_str 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)","target":{"kind":["lib"],"crate_types":["lib"],"name":"smol_str","src_path":".cargo/registry/src/github.com-1ecc6299db9ec823/smol_str-0.1.9/src/lib.rs","edition":"2015"},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/target/debug/deps/libsmol_str-60034903e8f9c710.rmeta"],"executable":null,"fresh":false} | ||
{"reason":"compiler-message","package_id":"dummy-pkg","target":{"kind":["bin"],"crate_types":["bin"],"name":"secspc","src_path":"src/main.rs","edition":"2018"},"message":{"message":"unused import: `secsp_analysis::input::FileId`","code":{"code":"unused_imports","explanation":null},"level":"warning","spans":[{"file_name":"packages/secspc/src/main.rs","byte_start":199,"byte_end":228,"line_start":14,"line_end":14,"column_start":5,"column_end":34,"is_primary":true,"text":[{"text":"use secsp_analysis::input::FileId;","highlight_start":5,"highlight_end":34}],"label":null,"suggested_replacement":null,"suggestion_applicability":null,"expansion":null}],"children":[{"message":"#[warn(unused_imports)] on by default","code":null,"level":"note","spans":[],"children":[],"rendered":null}],"rendered":"warning: unused import: `secsp_analysis::input::FileId`\n --> packages/secspc/src/main.rs:14:5\n |\n14 | use secsp_analysis::input::FileId;\n | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n |\n = note: #[warn(unused_imports)] on by default\n\n"}} | ||
{"reason":"compiler-message","package_id":"dummy-pkg","target":{"kind":["bin"],"crate_types":["bin"],"name":"secspc","src_path":"packages/secspc/src/main.rs","edition":"2018"},"message":{"message":"dummy message","code":{"code":"dummy_code","explanation":null},"level":"warning","spans":[],"children":[],"rendered":"dummy message"}} | ||
{"reason":"compiler-message","package_id":"dummy-pkg","target":{"kind":["bin"],"crate_types":["bin"],"name":"secspc","src_path":"packages/secspc/src/main.rs","edition":"2018"},"message":{"message":"redundant closure found","code":{"code":"clippy::redundant_closure","explanation":null},"level":"warning","spans":[{"file_name":"packages/secspc/src/main.rs","byte_start":1651,"byte_end":1671,"line_start":68,"line_end":68,"column_start":14,"column_end":34,"is_primary":false,"label":"secondary text here","suggested_replacement":null,"suggestion_applicability":null,"expansion":null},{"file_name":"packages/secspc/src/main.rs","byte_start":1651,"byte_end":1671,"line_start":68,"line_end":68,"column_start":14,"column_end":34,"is_primary":true,"text":[{"text":" .map(|i| PathBuf::from(i))","highlight_start":14,"highlight_end":34}],"label":null,"suggested_replacement":null,"suggestion_applicability":null,"expansion":null}],"children":[{"message":"#[warn(clippy::redundant_closure)] on by default","code":null,"level":"note","spans":[],"children":[],"rendered":null},{"message":"for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#redundant_closure","code":null,"level":"help","spans":[],"children":[],"rendered":null},{"message":"remove closure as shown","code":null,"level":"help","spans":[{"file_name":"packages/secspc/src/main.rs","byte_start":1651,"byte_end":1671,"line_start":68,"line_end":68,"column_start":14,"column_end":34,"is_primary":true,"text":[{"text":" .map(|i| PathBuf::from(i))","highlight_start":14,"highlight_end":34}],"label":null,"suggested_replacement":"PathBuf::from","suggestion_applicability":"MachineApplicable","expansion":null}],"children":[],"rendered":null}],"rendered":"warning: redundant closure found\n --> packages/secspc/src/main.rs:68:14\n |\n68 | .map(|i| PathBuf::from(i))\n | ^^^^^^^^^^^^^^^^^^^^ help: remove closure as shown: `PathBuf::from`\n |\n = note: #[warn(clippy::redundant_closure)] on by default\n = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#redundant_closure\n\n"}} |