-
Notifications
You must be signed in to change notification settings - Fork 186
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for parsing cargo check JSON output #137
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any chance to write a test that hits this line? |
||
} | ||
|
||
/** | ||
* 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; | ||
} | ||
} | ||
} |
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 | ||
*/ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
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); | ||
} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be helpful to have an additional test that also checks the aggregation, i.e. has a |
||
@Override | ||
protected IssueParser createParser() { | ||
return new CargoCheckParser(); | ||
} | ||
} |
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/gh.neting.cc-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"}} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any chance to write a test that skips this line once? I.e. that loops for two times?