Skip to content
Open
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
6 changes: 4 additions & 2 deletions slicec/src/compilation_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,12 @@ impl CompilationState {
/// which may run identical validation. If multiple diagnostics are _exact_ duplicates, only the first will be
/// present in the returned [`Vec`].
pub fn get_annotated_diagnostics(&self, options: &SliceOptions) -> Vec<AnnotatedDiagnostic> {
let mut annotated_diagnostics = Vec::new();
let mut annotated_diagnostics = Vec::<AnnotatedDiagnostic>::new();
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I have no idea why, but the compiler insists on putting a type-hint here now.
It seems unnecessary to me, but who am I to argue with the compiler...

for diagnostic in &*self.diagnostics {
let converted = crate::diagnostics::convert_diagnostic(diagnostic, options, &self.ast, &self.files);
if !annotated_diagnostics.contains(&converted) {
if let Some(original) = annotated_diagnostics.iter_mut().find(|d| **d == converted) {
original.reported_by.extend(converted.reported_by);
Comment thread
InsertCreativityHere marked this conversation as resolved.
} else {
annotated_diagnostics.push(converted);
Comment on lines +56 to 59
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Previously, we completely threw away duplicate diagnostics.
Now if we encounter a duplicate, we update the original's reported_by before throwing away the duplicate now.

}
}
Expand Down
6 changes: 5 additions & 1 deletion slicec/src/diagnostic_emitter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ impl<'a> DiagnosticEmitter<'a> {
// Emit the message with the prefix.
writeln!(self.output, "{prefix}: {}", console::style(&diagnostic.message).bold())?;

// Emit what the diagnostic was reported by.
writeln!(self.output, " Reported by: [{}]", diagnostic.reported_by.join(", "))?;
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

See the diagnostic_output_test or my PR description for an example of what the output looks like.


// If the diagnostic contains a snippet of the offending code, display it.
if let Some(snippet) = &diagnostic.snippet {
self.emit_snippet(snippet)?;
Expand Down Expand Up @@ -109,12 +112,13 @@ impl<'a> DiagnosticEmitter<'a> {
};

let mut serializer = serde_json::Serializer::new(&mut *self.output);
let mut state = serializer.serialize_struct("Diagnostic", 5)?;
let mut state = serializer.serialize_struct("Diagnostic", 6)?;
state.serialize_field("message", &diagnostic.message)?;
state.serialize_field("severity", severity)?;
state.serialize_field("snippet", &diagnostic.snippet)?;
state.serialize_field("notes", &diagnostic.notes)?;
state.serialize_field("error_code", &diagnostic.code)?;
state.serialize_field("reported_by", &diagnostic.reported_by)?;
state.end()?;
writeln!(self.output)?; // Separate each diagnostic by a newline character.
}
Expand Down
24 changes: 23 additions & 1 deletion slicec/src/diagnostics/annotated_diagnostic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,26 @@ use serde::Serialize;

/// An annotated version of a [`Diagnostic`], whose [`DiagnosticLevel`] has been computed (taking into account any
/// 'allow' attributes or command-line flags), and that has pre-extracted text snippets to display alongside messages.
#[derive(Clone, Debug, Eq, PartialEq)]
#[derive(Clone, Debug, Eq)]
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

We implement PartialEq by hand now, so that we exclude the reported_by from the equality check.

pub struct AnnotatedDiagnostic {
pub message: String,
pub level: DiagnosticLevel,
pub code: String,
pub snippet: Option<Snippet>,
pub notes: Vec<AnnotatedNote>,
pub reported_by: Vec<String>,
}

impl PartialEq for AnnotatedDiagnostic {
fn eq(&self, other: &Self) -> bool {
// Check every field for equality except for 'reported_by'.
// Who reported the diagnostic isn't an intrinsic property of the diagnostic itself.
self.message == other.message
&& self.level == other.level
&& self.code == other.code
&& self.snippet == other.snippet
&& self.notes == other.notes
}
}

#[derive(Serialize, Clone, Debug, Eq, PartialEq)]
Expand All @@ -42,12 +55,21 @@ pub fn convert_diagnostic(
snippet: get_snippet(&n.span, files),
});

// If the diagnostic was reported by a plugin, we just use the filename of the plugin (not its entire path).
fn get_plugin_file_stem(plugin_path: &str) -> Option<&str> {
std::path::Path::new(plugin_path).file_stem()?.to_str()
}
let reported_by = diagnostic.plugin.as_deref().map_or("slicec", |plugin_path| {
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I went back and forth on this, but I decided that in the case where the diagnostic is issued by slicec, it's worthwhile to still explicitly say that this came from slicec, and not a plugin.

It also makes the implementation cleaner and keep the error format 100% consistent, instead of being a little different for plugin diagnostics vs those about bad syntax for example.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Note that AnnotatedDiagnostic is the "finished" form of a diagnostic.
These are always de-duplicated, and are what we're actually going to print to the user.

The normal Diagnostic type is what gets reported throughout the code, and before we exit, we convert the Diagnostics to AnnotatedDiagnostics so that we can print them.


Therefore Diagnostic can have at most one reporter (since only one thing reported it).
But AnnotatedDiagnostic can have multiple (since duplicate diagnostics will of been merged together).

get_plugin_file_stem(plugin_path).unwrap_or(plugin_path)
});
Comment thread
InsertCreativityHere marked this conversation as resolved.

AnnotatedDiagnostic {
message: diagnostic.message(),
level: get_diagnostic_level_for(diagnostic, options, ast, files),
code: diagnostic.code().to_owned(),
snippet: get_snippet(&diagnostic.span, files),
notes: notes.collect(),
reported_by: vec![reported_by.to_owned()],
}
}

Expand Down
4 changes: 4 additions & 0 deletions slicec/src/diagnostics/diagnostic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ pub struct Diagnostic {

/// Any additional information that should be reported alongside this diagnostic's main message.
pub notes: Vec<Note>,

/// The plugin that reported this diagnostic, or `None` if this diagnostic was reported by 'slicec' itself.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We use the term plugin and not generator in slicec?

pub plugin: Option<String>,
}

impl Diagnostic {
Expand All @@ -29,6 +32,7 @@ impl Diagnostic {
span: None,
scope: None,
notes: Vec::new(),
plugin: None,
}
}

Expand Down
7 changes: 5 additions & 2 deletions slicec/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -274,8 +274,11 @@ fn main() -> ExitCode {
}
}

// Store generator's diagnostics for later emission.
diagnostics.extend(generator_diagnostics.into_inner());
// Store generator's diagnostics for later emission, after setting the plugin that reported them.
for mut generator_diagnostic in generator_diagnostics.into_inner() {
generator_diagnostic.plugin = Some(generator.path.clone());
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Where we actually set plugin.
This is after we've finished (or tried) to run the plugin and are storing its diagnostics.

diagnostics.push(generator_diagnostic);
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion slicec/src/slice_file_converter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ pub fn convert_diagnostic(diagnostic: Diagnostic, ast: &Ast, files: &[GrammarSli
let (span, scope) = convert_source(diagnostic.source.as_deref(), ast, files, output);

// Construct and push the converted 'slicec' `Diagnostic`.
let converted_diagnostic = slicec::diagnostics::Diagnostic { kind, span, scope, notes };
let converted_diagnostic = slicec::diagnostics::Diagnostic { kind, span, scope, notes, plugin: None };
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

We construct the diagnostic in one-shot here, so had to update it to include the new field.
We manually set it to it's default value (None).

Rust doesn't have the idea of default field values like other languages.

output.push(converted_diagnostic);
}

Expand Down
47 changes: 36 additions & 11 deletions slicec/tests/diagnostic_output_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ mod output {

// Assert
let expected = concat!(
r#"{"message":"incorrect doc comment: comment has a 'param' tag for 'x', but operation 'op' has no parameter with that name","severity":"warning","snippet":{"span":{"start":{"row":5,"col":17},"end":{"row":5,"col":25},"file":"string-0"},"text":" |\n5 | /// @param x: this is an x\n | --------\n |"},"notes":[],"error_code":"IncorrectDocComment"}"#,
r#"{"message":"incorrect doc comment: comment has a 'param' tag for 'x', but operation 'op' has no parameter with that name","severity":"warning","snippet":{"span":{"start":{"row":5,"col":17},"end":{"row":5,"col":25},"file":"string-0"},"text":" |\n5 | /// @param x: this is an x\n | --------\n |"},"notes":[],"error_code":"IncorrectDocComment","reported_by":["slicec"]}"#,
"\n",
r#"{"message":"invalid enum 'E': enums must contain at least one enumerator","severity":"error","snippet":{"span":{"start":{"row":9,"col":9},"end":{"row":9,"col":15},"file":"string-0"},"text":" |\n9 | enum E : int8 {}\n | ------\n |"},"notes":[],"error_code":"E008"}"#,
r#"{"message":"invalid enum 'E': enums must contain at least one enumerator","severity":"error","snippet":{"span":{"start":{"row":9,"col":9},"end":{"row":9,"col":15},"file":"string-0"},"text":" |\n9 | enum E : int8 {}\n | ------\n |"},"notes":[],"error_code":"E008","reported_by":["slicec"]}"#,
"\n",
);
assert_eq!(expected, String::from_utf8(output).unwrap());
Expand Down Expand Up @@ -87,12 +87,14 @@ mod output {
// Assert
let expected = "\
warning [IncorrectDocComment]: incorrect doc comment: comment has a 'param' tag for 'x', but operation 'op1' has no parameter with that name
Reported by: [slicec]
--> string-0:5:17
|
5 | /// @param x: this is an x
| --------
|
error [E016]: invalid tag on member 'x': tagged members must be optional
Reported by: [slicec]
--> string-0:8:17
|
8 | op2(tag(1)
Expand All @@ -103,6 +105,7 @@ error [E016]: invalid tag on member 'x': tagged members must be optional
| -------------------------
|
error [E008]: invalid enum 'E': enums must contain at least one enumerator
Reported by: [slicec]
--> string-0:14:9
|
14 | enum E : int8 {}
Expand All @@ -114,29 +117,46 @@ error [E008]: invalid enum 'E': enums must contain at least one enumerator
}

#[test]
fn duplicate_diagnostics_are_filtered_out() {
fn duplicate_diagnostics_are_merged_together() {
// Arrange

// All of these diagnostics are equal.
let diagnostic1_1 = Diagnostic::from_lint(Lint::Other {
//
let mut diagnostic1_1 = Diagnostic::from_lint(Lint::Other {
message: "This is a test".to_owned(),
});
let diagnostic1_2 = Diagnostic::from_lint(Lint::Other {
diagnostic1_1.plugin = Some("foo".to_owned());

let mut diagnostic1_2 = Diagnostic::from_lint(Lint::Other {
message: "This is a test".to_owned(),
});
let diagnostic1_3 = Diagnostic::from_lint(Lint::Other {
diagnostic1_2.plugin = Some("/path/bar.exe".to_owned());

let mut diagnostic1_3 = Diagnostic::from_lint(Lint::Other {
message: "This is a test".to_owned(),
});
diagnostic1_3.plugin = None; // should map to 'slicec' when converted.

// This diagnostic is unique, there should be no other diagnostics equal to it.
let diagnostic2_1 = Diagnostic::from_lint(Lint::Other {
//
let mut diagnostic2_1 = Diagnostic::from_lint(Lint::Other {
message: "This is also a test".to_owned(),
});
diagnostic2_1.plugin = Some("/path/bar.exe".to_owned());

// These 2 diagnostics are equal.
let diagnostic3_1 = Diagnostic::from_error(Error::CompactStructCannotBeEmpty);
let diagnostic3_2 = Diagnostic::from_error(Error::CompactStructCannotBeEmpty);
//
let mut diagnostic3_1 = Diagnostic::from_error(Error::CompactStructCannotBeEmpty);
diagnostic3_1.plugin = None; // should map to 'slicec' when converted.

let mut diagnostic3_2 = Diagnostic::from_error(Error::CompactStructCannotBeEmpty);
diagnostic3_2.plugin = Some("foo".to_owned());

// This diagnostic should not be equal to diagnostic 3, since this one has a note.
let diagnostic4_1 = Diagnostic::from_error(Error::CompactStructCannotBeEmpty)
//
let mut diagnostic4_1 = Diagnostic::from_error(Error::CompactStructCannotBeEmpty)
.add_note("This diagnostic is different because it has a note", None);
diagnostic4_1.plugin = Some("foo".to_owned());

// Manually set the diagnostics in our compilation state.
let mut state = CompilationState::create();
Expand All @@ -156,9 +176,13 @@ error [E008]: invalid enum 'E': enums must contain at least one enumerator
// Assert
assert_eq!(converted_diagnostics.len(), 4);
assert_eq!(converted_diagnostics[0].message, "This is a test");
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Check that the reported_bys are correct after merging.

assert_eq!(converted_diagnostics[0].reported_by, ["foo", "bar", "slicec"]);
assert_eq!(converted_diagnostics[1].message, "This is also a test");
assert_eq!(converted_diagnostics[1].reported_by, ["bar"]);
assert_eq!(converted_diagnostics[2].message, "compact structs must be non-empty");
assert_eq!(converted_diagnostics[2].reported_by, ["slicec", "foo"]);
assert_eq!(converted_diagnostics[3].message, "compact structs must be non-empty");
assert_eq!(converted_diagnostics[3].reported_by, ["foo"]);

let (warnings, errors) = DiagnosticEmitter::get_totals(&converted_diagnostics);
assert_eq!(warnings, 2);
Expand Down Expand Up @@ -230,7 +254,7 @@ error [E008]: invalid enum 'E': enums must contain at least one enumerator

// Assert: Only one of the two lints should be allowed.
let expected = concat!(
r#"{"message":"incorrect doc comment: comment has a 'param' tag for 'x', but operation 'op' has no parameter with that name","severity":"warning","snippet":{"span":{"start":{"row":6,"col":21},"end":{"row":6,"col":29},"file":"string-0"},"text":" |\n6 | /// @param x: this is an x\n | --------\n |"},"notes":[],"error_code":"IncorrectDocComment"}"#,
r#"{"message":"incorrect doc comment: comment has a 'param' tag for 'x', but operation 'op' has no parameter with that name","severity":"warning","snippet":{"span":{"start":{"row":6,"col":21},"end":{"row":6,"col":29},"file":"string-0"},"text":" |\n6 | /// @param x: this is an x\n | --------\n |"},"notes":[],"error_code":"IncorrectDocComment","reported_by":["slicec"]}"#,
"\n",
);
assert_eq!(expected, String::from_utf8(output).unwrap());
Expand Down Expand Up @@ -259,6 +283,7 @@ error [E008]: invalid enum 'E': enums must contain at least one enumerator
// Assert
let expected = "\
error [E008]: invalid enum 'E': enums must contain at least one enumerator
Reported by: [slicec]
--> string-0:2:4
|
2 | enum
Expand Down