Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,11 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
```

`Generation::inputs()` contains every resolved root, import, and token
vocabulary. `Generation::outputs()` and `Generation::warnings()` expose the
other build artifacts without process-global CLI state. Projects that commit
generated source can omit the build dependency and use the command instead.
vocabulary. `Generation::outputs()` exposes the written artifacts,
`Generation::diagnostics()` exposes structured compiler warnings with exact
source byte spans, and `Generation::warnings()` preserves CLI-form compiler and
generator messages. Projects that commit generated source can omit the build
dependency and use the command instead.

### 4. Generate from the command line

Expand Down
5 changes: 5 additions & 0 deletions crates/antlr-rust-codegen/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
}
```

Successful generations expose structured compiler warnings through
`Generation::diagnostics()`, including the diagnostic code, severity, source
path, and exact UTF-8 byte span. `Generation::warnings()` retains the rendered
CLI messages.

Or install the command:

```bash
Expand Down
12 changes: 11 additions & 1 deletion crates/antlr-rust-codegen/src/artifact.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,29 @@ use std::fs;
use std::io;
use std::path::{Component, Path, PathBuf};

use crate::error::Diagnostic;

/// Inventory returned by a completed generation.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Generation {
inputs: Vec<PathBuf>,
outputs: Vec<PathBuf>,
warnings: Vec<String>,
diagnostics: Vec<Diagnostic>,
}

impl Generation {
pub(crate) const fn new(
inputs: Vec<PathBuf>,
outputs: Vec<PathBuf>,
warnings: Vec<String>,
diagnostics: Vec<Diagnostic>,
) -> Self {
Self {
inputs,
outputs,
warnings,
diagnostics,
}
}

Expand All @@ -34,11 +39,16 @@ impl Generation {
&self.outputs
}

/// Non-fatal compiler and transform diagnostics in CLI display form.
/// Non-fatal compiler and generator messages in CLI display form.
pub fn warnings(&self) -> &[String] {
&self.warnings
}

/// Structured non-fatal grammar compiler diagnostics.
pub fn diagnostics(&self) -> &[Diagnostic] {
&self.diagnostics
}

/// Emits Cargo rebuild directives for the complete resolved input graph.
#[allow(clippy::print_stdout)]
pub fn emit_rerun_if_changed(&self) {
Expand Down
73 changes: 52 additions & 21 deletions crates/antlr-rust-codegen/src/driver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@ pub(crate) fn generate(
action_reference_parser,
)
.map_err(|error| compilation_error(&error, &args.roots))?;
let mut warnings = compilation_warning_messages(&compilation);
let diagnostics = compilation_diagnostics(&compilation);
let mut warnings = diagnostics
.iter()
.map(render_diagnostic)
.collect::<Vec<_>>();
for warning in &warnings {
report(warning).map_err(Error::generation)?;
}
Expand All @@ -58,7 +62,7 @@ pub(crate) fn generate(
optimization_manifest.expect("report mode enables the optimization manifest"),
)?;
let outputs = artifacts.write_to(&args.out_dir)?;
return Ok(Generation::new(inputs, outputs, warnings));
return Ok(Generation::new(inputs, outputs, warnings, diagnostics));
}

let mut grammar_options = Vec::new();
Expand Down Expand Up @@ -171,7 +175,7 @@ pub(crate) fn generate(
artifacts.remove_if_present("optimizations.json")?;
}
let outputs = artifacts.write_to(&args.out_dir)?;
Ok(Generation::new(inputs, outputs, warnings))
Ok(Generation::new(inputs, outputs, warnings, diagnostics))
}

fn insert_rendered_module(
Expand Down Expand Up @@ -242,30 +246,57 @@ fn compilation_error(error: &grammar::diagnostic::CompilationError, roots: &[Pat
diagnostic.message.clone(),
path,
position,
location.map(|_| source_byte_span(&diagnostic.primary)),
Comment thread
tinovyatkin marked this conversation as resolved.
Outdated
));
}
Error::compilation(message, diagnostics)
}

fn compilation_warning_messages(compilation: &grammar::compiler::Compilation) -> Vec<String> {
let mut messages = Vec::new();
for diagnostic in compilation
fn compilation_diagnostics(compilation: &grammar::compiler::Compilation) -> Vec<Diagnostic> {
compilation
.diagnostics
.iter()
.filter(|diagnostic| diagnostic.severity == grammar::diagnostic::Severity::Warning)
{
let source = compilation.sources.get(diagnostic.primary.source);
let path = source.map_or_else(
|| "<grammar>".to_owned(),
|source| source.logical_path().display().to_string(),
);
let position = source
.and_then(|source| source.line_column(diagnostic.primary.bytes.start))
.map_or_else(String::new, |(line, column)| format!(":{line}:{column}"));
messages.push(format!(
"warning[{}]: {path}{position}: {}",
diagnostic.code, diagnostic.message
));
}
messages
.map(|diagnostic| {
let path = compilation
.sources
.logical_path(diagnostic.primary.source)
.map(Path::to_path_buf);
let byte_span = path.as_ref().map(|_| source_byte_span(&diagnostic.primary));
let path = path.unwrap_or_else(|| PathBuf::from("<grammar>"));
let position = compilation
.sources
.line_column(diagnostic.primary.source, diagnostic.primary.bytes.start);
Diagnostic::new(
diagnostic.code,
Severity::Warning,
diagnostic.message.clone(),
path,
position,
byte_span,
)
})
.collect()
}

fn render_diagnostic(diagnostic: &Diagnostic) -> String {
let severity = match diagnostic.severity() {
Severity::Warning => "warning",
Severity::Error => "error",
};
let position = diagnostic
.line()
.zip(diagnostic.column())
.map_or_else(String::new, |(line, column)| format!(":{line}:{column}"));
format!(
"{severity}[{}]: {}{position}: {}",
diagnostic.code(),
diagnostic.path().display(),
diagnostic.message()
)
}

fn source_byte_span(span: &SourceSpan) -> std::ops::Range<usize> {
usize::try_from(span.bytes.start).expect("source offset exceeds usize")
..usize::try_from(span.bytes.end).expect("source offset exceeds usize")
}
9 changes: 9 additions & 0 deletions crates/antlr-rust-codegen/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::fmt;
use std::io;
use std::ops::Range;
use std::path::PathBuf;

/// Broad category of a generator failure.
Expand Down Expand Up @@ -28,6 +29,7 @@ pub struct Diagnostic {
path: PathBuf,
line: Option<usize>,
column: Option<usize>,
byte_span: Option<Range<usize>>,
}

impl Diagnostic {
Expand All @@ -37,6 +39,7 @@ impl Diagnostic {
message: String,
path: PathBuf,
position: Option<(usize, usize)>,
byte_span: Option<Range<usize>>,
) -> Self {
Self {
code,
Expand All @@ -45,6 +48,7 @@ impl Diagnostic {
path,
line: position.map(|(line, _)| line),
column: position.map(|(_, column)| column),
byte_span,
}
}

Expand All @@ -71,6 +75,11 @@ impl Diagnostic {
pub const fn column(&self) -> Option<usize> {
self.column
}

/// Half-open UTF-8 byte range of the primary subject within [`Self::path`].
pub fn byte_span(&self) -> Option<Range<usize>> {
self.byte_span.clone()
}
}

/// Structured error returned by [`crate::Builder`].
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ fn builder_and_cli_share_artifacts_warnings_and_structured_diagnostics() {
[grammar.canonicalize().expect("grammar")]
);
assert!(generation.warnings().is_empty());
assert!(generation.diagnostics().is_empty());
assert_generated_project(
temp.path(),
&["parity_lexer.rs", "parity_parser.rs"],
Expand Down Expand Up @@ -130,6 +131,47 @@ mod generated_source_without_codegen_dependency {
}
}

#[test]
fn builder_exposes_structured_non_fatal_diagnostics_with_exact_spans() {
let temp = temporary_directory("builder-structured-diagnostics");
let grammar = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/antlr4-rust-gen/unreachable-rules/Reachability.g4");
let source = fs::read_to_string(&grammar).expect("fixture grammar should be readable");
let generation = Builder::new()
.grammar(&grammar)
.out_dir(temp.path().join("generated"))
.generate()
.expect("unreachable rules should remain non-fatal");

let unreachable = generation
.diagnostics()
.iter()
.filter(|diagnostic| diagnostic.code() == "G4S078")
.map(|diagnostic| {
assert_eq!(diagnostic.path(), grammar);
let span = diagnostic
.byte_span()
.expect("authored diagnostics should retain their source span");
let subject = source
.get(span.clone())
.expect("diagnostic span should be a valid UTF-8 source range");
(
diagnostic.code(),
diagnostic.severity(),
diagnostic.line(),
diagnostic.column(),
span,
subject,
)
})
.collect::<Vec<_>>();

insta::assert_debug_snapshot!(
"builder_structured_non_fatal_diagnostics_with_exact_spans",
unreachable
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

#[test]
fn builder_tracks_token_vocabulary_and_semantic_pattern_inputs() {
let temp = temporary_directory("builder-external-inputs");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
source: crates/antlr-rust-codegen/tests/antlr4_rust_gen_cli/multi_recognizer.rs
expression: unreachable
---
[
(
"G4S078",
Warning,
Some(
28,
),
Some(
0,
),
183..191,
"deadRoot",
),
(
"G4S078",
Warning,
Some(
32,
),
Some(
0,
),
220..230,
"deadHelper",
),
]
Loading