Skip to content

Commit 412391d

Browse files
committed
Add Rust backend verification command
1 parent b1904ad commit 412391d

5 files changed

Lines changed: 208 additions & 37 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,7 @@ rss review [--json] --map <file-or-directory>
380380
rss lower --rust <file.rss>
381381
rss lower --rust <file.rss> --out-dir <directory>
382382
rss remap-rustc [--json] <rsscript-source-map.json> <rustc-json-lines>
383+
rss verify-rust [--json] <file.rss>
383384
```
384385

385386
---

src/lib.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@ pub use review::{
1919
review_sources,
2020
};
2121
pub use rust_lower::{
22-
GeneratedRustPackage, LoweredRust, RustSourceMapEntry, lower_program_to_rust,
22+
GeneratedRustPackage, LoweredRust, RemappedRustcDiagnostic, RustBackendCheckResult,
23+
RustSourceMapEntry, check_generated_rust_package, lower_program_to_rust,
2324
lower_program_to_rust_with_map, lower_source_to_rust, lower_source_to_rust_package,
2425
lower_source_to_rust_with_map, parse_source_map_json, remap_rustc_diagnostic_json,
25-
remap_rustc_diagnostic_json_lines,
26+
remap_rustc_diagnostic_json_lines, write_generated_rust_package,
2627
};

src/main.rs

Lines changed: 111 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@ use std::env;
22
use std::fs;
33
use std::path::{Path, PathBuf};
44
use std::process::ExitCode;
5+
use std::time::{SystemTime, UNIX_EPOCH};
56

67
use rsscript::{
7-
analyze_source, explain_diagnostic_code, format_diagnostic_explanation,
8-
format_diagnostics_human, format_diagnostics_json, format_review_human, format_review_json,
9-
format_review_map_human, format_review_map_json, lower_source_to_rust,
10-
lower_source_to_rust_package, parse_source_map_json, remap_rustc_diagnostic_json_lines,
11-
review_map_sources, review_sources,
8+
Diagnostic, analyze_source, check_generated_rust_package, explain_diagnostic_code,
9+
format_diagnostic_explanation, format_diagnostics_human, format_diagnostics_json,
10+
format_review_human, format_review_json, format_review_map_human, format_review_map_json,
11+
lower_source_to_rust, lower_source_to_rust_package, parse_source_map_json,
12+
remap_rustc_diagnostic_json_lines, review_map_sources, review_sources,
13+
write_generated_rust_package,
1214
};
1315

1416
fn main() -> ExitCode {
@@ -24,6 +26,7 @@ fn main() -> ExitCode {
2426
"review" => run_review(&args[2..]),
2527
"lower" => run_lower(&args[2..]),
2628
"remap-rustc" => run_remap_rustc(&args[2..]),
29+
"verify-rust" => run_verify_rust(&args[2..]),
2730
_ => {
2831
print_usage();
2932
ExitCode::from(2)
@@ -175,33 +178,8 @@ fn run_lower_rust_package(path: &str, source: &str, out_dir: &str) -> ExitCode {
175178
};
176179

177180
let out_dir = Path::new(out_dir);
178-
let src_dir = out_dir.join("src");
179-
if let Err(error) = fs::create_dir_all(&src_dir) {
180-
eprintln!("failed to create {}: {error}", src_dir.display());
181-
return ExitCode::from(2);
182-
}
183-
if let Err(error) = fs::write(out_dir.join("Cargo.toml"), package.cargo_toml) {
184-
eprintln!(
185-
"failed to write {}: {error}",
186-
out_dir.join("Cargo.toml").display()
187-
);
188-
return ExitCode::from(2);
189-
}
190-
if let Err(error) = fs::write(src_dir.join("lib.rs"), package.lib_rs) {
191-
eprintln!(
192-
"failed to write {}: {error}",
193-
src_dir.join("lib.rs").display()
194-
);
195-
return ExitCode::from(2);
196-
}
197-
if let Err(error) = fs::write(
198-
out_dir.join("rsscript-source-map.json"),
199-
package.source_map_json,
200-
) {
201-
eprintln!(
202-
"failed to write {}: {error}",
203-
out_dir.join("rsscript-source-map.json").display()
204-
);
181+
if let Err(error) = write_generated_rust_package(out_dir, &package) {
182+
eprintln!("{error}");
205183
return ExitCode::from(2);
206184
}
207185

@@ -213,6 +191,83 @@ fn run_lower_rust_package(path: &str, source: &str, out_dir: &str) -> ExitCode {
213191
ExitCode::SUCCESS
214192
}
215193

194+
fn run_verify_rust(args: &[String]) -> ExitCode {
195+
let (json, path) = parse_path_args(args);
196+
let Some(path) = path else {
197+
print_usage();
198+
return ExitCode::from(2);
199+
};
200+
let source = match fs::read_to_string(path) {
201+
Ok(source) => source,
202+
Err(error) => {
203+
eprintln!("failed to read {path}: {error}");
204+
return ExitCode::from(2);
205+
}
206+
};
207+
let runtime_path = match default_runtime_path() {
208+
Ok(path) => path,
209+
Err(error) => {
210+
eprintln!("{error}");
211+
return ExitCode::from(2);
212+
}
213+
};
214+
let package_name = generated_package_name(path);
215+
let package = match lower_source_to_rust_package(
216+
path,
217+
&source,
218+
&package_name,
219+
&runtime_path.display().to_string(),
220+
) {
221+
Ok(package) => package,
222+
Err(diagnostics) => {
223+
print_diagnostics(json, &diagnostics);
224+
return ExitCode::from(1);
225+
}
226+
};
227+
let temp_dir = verify_temp_dir(&package.package_name);
228+
if let Err(error) = write_generated_rust_package(&temp_dir, &package) {
229+
eprintln!("{error}");
230+
cleanup_temp_dir(&temp_dir);
231+
return ExitCode::from(2);
232+
}
233+
let result = match check_generated_rust_package(&temp_dir) {
234+
Ok(result) => result,
235+
Err(error) => {
236+
eprintln!("{error}");
237+
cleanup_temp_dir(&temp_dir);
238+
return ExitCode::from(2);
239+
}
240+
};
241+
cleanup_temp_dir(&temp_dir);
242+
243+
if result.diagnostics.is_empty() {
244+
if result.success {
245+
if !json {
246+
println!("{path}: rust backend ok");
247+
} else {
248+
println!("[]");
249+
}
250+
return ExitCode::SUCCESS;
251+
}
252+
if !result.stderr.trim().is_empty() {
253+
eprintln!("{}", result.stderr.trim());
254+
}
255+
eprintln!("rust backend check failed without mappable diagnostics");
256+
return ExitCode::from(1);
257+
}
258+
259+
print_diagnostics(json, &result.diagnostics);
260+
if result
261+
.diagnostics
262+
.iter()
263+
.any(|diagnostic| diagnostic.severity.is_error())
264+
{
265+
ExitCode::from(1)
266+
} else {
267+
ExitCode::SUCCESS
268+
}
269+
}
270+
216271
fn run_remap_rustc(args: &[String]) -> ExitCode {
217272
let (json, paths) = parse_multi_path_args(args);
218273
let [source_map_path, rustc_json_path] = paths.as_slice() else {
@@ -270,6 +325,14 @@ fn run_remap_rustc(args: &[String]) -> ExitCode {
270325
}
271326
}
272327

328+
fn print_diagnostics(json: bool, diagnostics: &[Diagnostic]) {
329+
if json {
330+
println!("{}", format_diagnostics_json(diagnostics));
331+
} else {
332+
print!("{}", format_diagnostics_human(diagnostics));
333+
}
334+
}
335+
273336
fn parse_explain_args(args: &[String]) -> Option<&str> {
274337
let [flag, code] = args else {
275338
return None;
@@ -360,6 +423,21 @@ fn generated_package_name(path: &str) -> String {
360423
.to_string()
361424
}
362425

426+
fn verify_temp_dir(package_name: &str) -> PathBuf {
427+
let now = SystemTime::now()
428+
.duration_since(UNIX_EPOCH)
429+
.map(|duration| duration.as_nanos())
430+
.unwrap_or(0);
431+
env::temp_dir().join(format!(
432+
"rsscript-verify-{package_name}-{}-{now}",
433+
std::process::id()
434+
))
435+
}
436+
437+
fn cleanup_temp_dir(path: &Path) {
438+
let _ = fs::remove_dir_all(path);
439+
}
440+
363441
enum ReviewCommand<'a> {
364442
Diff {
365443
json: bool,
@@ -528,6 +606,7 @@ fn print_usage() {
528606
eprintln!(" rsscript lower --rust <file.rss>");
529607
eprintln!(" rsscript lower --rust <file.rss> --out-dir <directory>");
530608
eprintln!(" rsscript remap-rustc [--json] <rsscript-source-map.json> <rustc-json-lines>");
609+
eprintln!(" rsscript verify-rust [--json] <file.rss>");
531610
eprintln!(" rsscript review [--json] --diff <old.rss> <new.rss>");
532611
eprintln!(" rsscript review [--json] --map <file-or-directory>");
533612
}

src/rust_lower.rs

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
use std::collections::{BTreeMap, BTreeSet};
2+
use std::fs;
3+
use std::path::Path;
4+
use std::process::Command;
25

36
use crate::analyzer::analyze_source;
47
use crate::diagnostic::{Diagnostic, Severity, Span, code};
@@ -35,6 +38,14 @@ pub struct RemappedRustcDiagnostic {
3538
pub mapped: bool,
3639
}
3740

41+
#[derive(Debug, Clone, PartialEq, Eq)]
42+
pub struct RustBackendCheckResult {
43+
pub success: bool,
44+
pub diagnostics: Vec<Diagnostic>,
45+
pub cargo_status: Option<i32>,
46+
pub stderr: String,
47+
}
48+
3849
pub fn lower_source_to_rust(file: &str, source: &str) -> Result<String, Vec<Diagnostic>> {
3950
lower_source_to_rust_with_map(file, source).map(|lowered| lowered.rust_source)
4051
}
@@ -78,6 +89,38 @@ pub fn lower_source_to_rust_package(
7889
})
7990
}
8091

92+
pub fn write_generated_rust_package(
93+
out_dir: &Path,
94+
package: &GeneratedRustPackage,
95+
) -> Result<(), String> {
96+
let src_dir = out_dir.join("src");
97+
fs::create_dir_all(&src_dir)
98+
.map_err(|error| format!("failed to create {}: {error}", src_dir.display()))?;
99+
fs::write(out_dir.join("Cargo.toml"), &package.cargo_toml).map_err(|error| {
100+
format!(
101+
"failed to write {}: {error}",
102+
out_dir.join("Cargo.toml").display()
103+
)
104+
})?;
105+
fs::write(src_dir.join("lib.rs"), &package.lib_rs).map_err(|error| {
106+
format!(
107+
"failed to write {}: {error}",
108+
src_dir.join("lib.rs").display()
109+
)
110+
})?;
111+
fs::write(
112+
out_dir.join("rsscript-source-map.json"),
113+
&package.source_map_json,
114+
)
115+
.map_err(|error| {
116+
format!(
117+
"failed to write {}: {error}",
118+
out_dir.join("rsscript-source-map.json").display()
119+
)
120+
})?;
121+
Ok(())
122+
}
123+
81124
pub fn lower_program_to_rust(program: &Program) -> String {
82125
lower_program_to_rust_with_map(program).rust_source
83126
}
@@ -95,7 +138,12 @@ pub fn remap_rustc_diagnostic_json(
95138
source_map: &[RustSourceMapEntry],
96139
rustc_json: &str,
97140
) -> Result<Option<RemappedRustcDiagnostic>, String> {
98-
let rustc: RustcJsonDiagnostic = serde_json::from_str(rustc_json)
141+
let value: serde_json::Value = serde_json::from_str(rustc_json)
142+
.map_err(|error| format!("failed to parse rustc JSON line: {error}"))?;
143+
let Some(value) = rustc_diagnostic_value(&value) else {
144+
return Ok(None);
145+
};
146+
let rustc: RustcJsonDiagnostic = serde_json::from_value(value.clone())
99147
.map_err(|error| format!("failed to parse rustc JSON diagnostic: {error}"))?;
100148
if !matches!(rustc.level.as_str(), "error" | "warning") {
101149
return Ok(None);
@@ -183,6 +231,38 @@ pub fn remap_rustc_diagnostic_json_lines(
183231
Ok(diagnostics)
184232
}
185233

234+
pub fn check_generated_rust_package(package_dir: &Path) -> Result<RustBackendCheckResult, String> {
235+
let source_map_json = fs::read_to_string(package_dir.join("rsscript-source-map.json"))
236+
.map_err(|error| {
237+
format!(
238+
"failed to read {}: {error}",
239+
package_dir.join("rsscript-source-map.json").display()
240+
)
241+
})?;
242+
let source_map = parse_source_map_json(&source_map_json)?;
243+
let manifest_path = package_dir.join("Cargo.toml");
244+
let output = Command::new("cargo")
245+
.arg("check")
246+
.arg("--manifest-path")
247+
.arg(&manifest_path)
248+
.arg("--message-format=json")
249+
.output()
250+
.map_err(|error| format!("failed to run cargo check: {error}"))?;
251+
let stdout = String::from_utf8_lossy(&output.stdout);
252+
let remapped = remap_rustc_diagnostic_json_lines(&source_map, &stdout)?;
253+
let diagnostics = remapped
254+
.into_iter()
255+
.map(|remapped| remapped.diagnostic)
256+
.collect();
257+
258+
Ok(RustBackendCheckResult {
259+
success: output.status.success(),
260+
diagnostics,
261+
cargo_status: output.status.code(),
262+
stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
263+
})
264+
}
265+
186266
struct RustLowerer<'a> {
187267
program: &'a Program,
188268
type_kinds: BTreeMap<String, TypeKind>,
@@ -717,6 +797,16 @@ fn rustc_severity(level: &str) -> Severity {
717797
}
718798
}
719799

800+
fn rustc_diagnostic_value(value: &serde_json::Value) -> Option<&serde_json::Value> {
801+
if value.get("level").is_some() && value.get("message").is_some() {
802+
return Some(value);
803+
}
804+
if value.get("reason").and_then(serde_json::Value::as_str) == Some("compiler-message") {
805+
return value.get("message");
806+
}
807+
None
808+
}
809+
720810
fn generated_span_from_rustc(span: &RustcJsonSpan) -> Span {
721811
Span {
722812
file: span.file_name.clone(),

tests/checker.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -198,8 +198,8 @@ fn rustc_diagnostics_report_unmappable_generated_spans() {
198198

199199
#[test]
200200
fn rustc_diagnostic_line_remap_ignores_non_diagnostic_messages() {
201-
let lines = r#"{"message":"build finished","level":"note","spans":[]}
202-
{"message":"cannot find value","code":{"code":"E0425","explanation":null},"level":"error","spans":[{"file_name":"src/lib.rs","line_start":99,"line_end":99,"column_start":5,"column_end":10,"is_primary":true}]}"#;
201+
let lines = r#"{"reason":"compiler-artifact","target":{"name":"generated"}}
202+
{"reason":"compiler-message","message":{"message":"cannot find value","code":{"code":"E0425","explanation":null},"level":"error","spans":[{"file_name":"src/lib.rs","line_start":99,"line_end":99,"column_start":5,"column_end":10,"is_primary":true}]}}"#;
203203

204204
let remapped =
205205
remap_rustc_diagnostic_json_lines(&[], lines).expect("rustc JSON lines should parse");

0 commit comments

Comments
 (0)