Skip to content

Commit

Permalink
Merge pull request #137 from garyttierney/feature/cargo-check-parser
Browse files Browse the repository at this point in the history
Add support for parsing cargo check JSON output
  • Loading branch information
uhafner authored Apr 2, 2019
2 parents e9e448a + 6768bd3 commit e13d1a8
Show file tree
Hide file tree
Showing 4 changed files with 212 additions and 0 deletions.
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)) {
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;
}
}
}
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();
}
}
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"}}

0 comments on commit e13d1a8

Please sign in to comment.