Skip to content
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

Merged
merged 3 commits into from
Apr 2, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- [PR#132](https://github.com/jenkinsci/analysis-model/pull/132):
Added a parser for CMake warnings.
- [PR#137](https://github.com/jenkinsci/analysis-model/pull/137):
Added a parser for JSON output from Cargo.

### Fixed
- [JENKINS-56333](https://issues.jenkins-ci.org/browse/JENKINS-56333):
Expand Down
158 changes: 158 additions & 0 deletions src/main/java/edu/hm/hafner/analysis/parser/CargoCheckParser.java
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)) {
Copy link
Member

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?

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();
Copy link
Member

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 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
*/
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@author

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);
}

Copy link
Member

Choose a reason for hiding this comment

The 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 size of 2 issues.

@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/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"}}