Skip to content

Commit 0769330

Browse files
committed
Add JSON output for review findings
1 parent 9904ff4 commit 0769330

5 files changed

Lines changed: 93 additions & 13 deletions

File tree

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ It currently implements:
1515
- `rss check --json <file.rss>` with serde-backed machine-readable diagnostics
1616
- `rss check --explain <code>` for diagnostic code explanations
1717
- `rss fmt <file.rss>` as a parse/check gate that prints the source unchanged when valid
18-
- `rss review <old.rss> <new.rss>` for a first-pass API/type/effect/freshness diff with structured risk categories and path-aware local/manage boundary summaries
18+
- `rss review <old.rss> <new.rss>` and `rss review --json <old.rss> <new.rss>` for first-pass API/type/effect/freshness diffs with structured risk categories and path-aware local/manage boundary summaries
1919
- Lexer, lightweight parser, syntax AST, HIR signature table, and semantic checks for the review-critical v0.4.1 rules
2020
- Builtin signatures for the current fixture stdlib surface, including `Image`, `File`, `Map`, `ResourcePool`, `Json`, `Csv`, database/resource helpers, and cache/config helpers
2121
- HIR type and field tables for class/struct/resource declarations and handle fields
@@ -61,7 +61,7 @@ Implemented diagnostic classes include:
6161
- local capture by managed closures
6262
- local capture by closures passed to retaining APIs
6363
- `take` of handle fields
64-
- API, type layout, path-aware local/manage boundary, effect, mode, and freshness changes in `rss review`, tagged with structured risk categories
64+
- API, type layout, path-aware local/manage boundary, effect, mode, and freshness changes in `rss review`, tagged with structured risk categories in human and JSON output
6565
- likely operator overload attempts
6666

6767
Non-goals for this stage:
@@ -79,7 +79,7 @@ Non-goals for this stage:
7979
Near-term roadmap:
8080

8181
1. Expand local/resource dataflow precision around typed `ResourcePool<T>` lease propagation and expression-order coverage for short-circuiting/control expressions.
82-
2. Expand `rss review` risk categories into machine-readable JSON output.
82+
2. Expand `rss review` JSON with spans and structured before/after fields.
8383

8484
Run:
8585

@@ -91,4 +91,5 @@ cargo run --bin rss -- check path/to/file.rss
9191
cargo run --bin rss -- check --json path/to/file.rss
9292
cargo run --bin rss -- check --explain RS0401
9393
cargo run --bin rss -- review old.rss new.rss
94+
cargo run --bin rss -- review --json old.rss new.rss
9495
```

src/lib.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,6 @@ pub use diagnostic::{
1111
Diagnostic, DiagnosticExplanation, Severity, explain_diagnostic_code,
1212
format_diagnostic_explanation, format_diagnostics_human, format_diagnostics_json,
1313
};
14-
pub use review::{ReviewFinding, ReviewRisk, format_review_human, review_sources};
14+
pub use review::{
15+
ReviewFinding, ReviewRisk, format_review_human, format_review_json, review_sources,
16+
};

src/main.rs

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ use std::process::ExitCode;
44

55
use rsscript::{
66
analyze_source, explain_diagnostic_code, format_diagnostic_explanation,
7-
format_diagnostics_human, format_diagnostics_json, format_review_human, review_sources,
7+
format_diagnostics_human, format_diagnostics_json, format_review_human, format_review_json,
8+
review_sources,
89
};
910

1011
fn main() -> ExitCode {
@@ -97,7 +98,8 @@ fn run_fmt(args: &[String]) -> ExitCode {
9798
}
9899

99100
fn run_review(args: &[String]) -> ExitCode {
100-
let [old_path, new_path] = args else {
101+
let (json, old_path, new_path) = parse_review_args(args);
102+
let (Some(old_path), Some(new_path)) = (old_path, new_path) else {
101103
print_usage();
102104
return ExitCode::from(2);
103105
};
@@ -124,13 +126,23 @@ fn run_review(args: &[String]) -> ExitCode {
124126
.chain(new_diagnostics.iter())
125127
.any(|diagnostic| diagnostic.severity.is_error());
126128
if has_errors {
127-
print!("{}", format_diagnostics_human(&old_diagnostics));
128-
print!("{}", format_diagnostics_human(&new_diagnostics));
129+
if json {
130+
let mut diagnostics = old_diagnostics;
131+
diagnostics.extend(new_diagnostics);
132+
println!("{}", format_diagnostics_json(&diagnostics));
133+
} else {
134+
print!("{}", format_diagnostics_human(&old_diagnostics));
135+
print!("{}", format_diagnostics_human(&new_diagnostics));
136+
}
129137
return ExitCode::from(1);
130138
}
131139

132140
let findings = review_sources(old_path, &old_source, new_path, &new_source);
133-
print!("{}", format_review_human(&findings));
141+
if json {
142+
println!("{}", format_review_json(&findings));
143+
} else {
144+
print!("{}", format_review_human(&findings));
145+
}
134146
ExitCode::SUCCESS
135147
}
136148

@@ -156,10 +168,28 @@ fn parse_path_args(args: &[String]) -> (bool, Option<&str>) {
156168
(json, path)
157169
}
158170

171+
fn parse_review_args(args: &[String]) -> (bool, Option<&str>, Option<&str>) {
172+
let mut json = false;
173+
let mut paths = Vec::new();
174+
175+
for arg in args {
176+
if arg == "--json" {
177+
json = true;
178+
} else {
179+
paths.push(arg.as_str());
180+
}
181+
}
182+
183+
if paths.len() != 2 {
184+
return (json, None, None);
185+
}
186+
(json, Some(paths[0]), Some(paths[1]))
187+
}
188+
159189
fn print_usage() {
160190
eprintln!("usage:");
161191
eprintln!(" rsscript check [--json] <file.rss>");
162192
eprintln!(" rsscript check --explain <code>");
163193
eprintln!(" rsscript fmt <file.rss>");
164-
eprintln!(" rsscript review <old.rss> <new.rss>");
194+
eprintln!(" rsscript review [--json] <old.rss> <new.rss>");
165195
}

src/review.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,23 @@
11
use std::collections::{BTreeMap, BTreeSet};
22

3+
use serde::Serialize;
4+
35
use crate::diagnostic::code;
46
use crate::syntax::ast::{
57
Block, CallArg, DataEffect, EffectDecl, Expr, FieldDecl, FileMode, FunctionDecl, Item, LetKind,
68
Param, Stmt, TypeDecl, TypeKind, TypeRef,
79
};
810
use crate::syntax::parse_source;
911

10-
#[derive(Debug, Clone, PartialEq, Eq)]
12+
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1113
pub struct ReviewFinding {
1214
pub code: String,
1315
pub risk: ReviewRisk,
1416
pub summary: String,
1517
}
1618

17-
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
20+
#[serde(rename_all = "kebab-case")]
1821
pub enum ReviewRisk {
1922
Mode,
2023
Api,
@@ -123,6 +126,10 @@ pub fn format_review_human(findings: &[ReviewFinding]) -> String {
123126
output
124127
}
125128

129+
pub fn format_review_json(findings: &[ReviewFinding]) -> String {
130+
serde_json::to_string(findings).expect("review JSON serialization should not fail")
131+
}
132+
126133
fn review_finding(code: &str, risk: ReviewRisk, summary: impl Into<String>) -> ReviewFinding {
127134
ReviewFinding {
128135
code: code.to_string(),

tests/checker.rs

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use rsscript::syntax::ast::Item;
55
use rsscript::syntax::parse_source;
66
use rsscript::{
77
ReviewRisk, analyze_source, explain_diagnostic_code, format_diagnostic_explanation,
8-
format_diagnostics_json, format_review_human, review_sources,
8+
format_diagnostics_json, format_review_human, format_review_json, review_sources,
99
};
1010
use serde_json::Value;
1111

@@ -77,6 +77,46 @@ fn diagnostic_explanations_are_available_by_code() {
7777
assert!(explain_diagnostic_code("RS9999").is_none());
7878
}
7979

80+
#[test]
81+
fn review_json_uses_protocol_shape() {
82+
let old_source = r#"
83+
mode: managed
84+
85+
fn render(path: read Path) -> Image {
86+
Image.load(path: read path)
87+
}
88+
"#;
89+
let new_source = r#"
90+
mode: uses-local
91+
92+
fn render(path: take Path) -> fresh Image
93+
effects(retains(path))
94+
{
95+
Image.load(path: read path)
96+
}
97+
"#;
98+
let findings = review_sources("old.rss", old_source, "new.rss", new_source);
99+
let json = format_review_json(&findings);
100+
let value: Value = serde_json::from_str(&json).expect("review JSON should parse");
101+
let items = value.as_array().expect("review JSON should be an array");
102+
103+
assert!(
104+
items
105+
.iter()
106+
.any(|item| item["code"] == "RSR001" && item["risk"] == "mode")
107+
);
108+
assert!(
109+
items
110+
.iter()
111+
.any(|item| item["code"] == "RSR006" && item["risk"] == "effect")
112+
);
113+
assert!(items.iter().all(|item| {
114+
item["summary"]
115+
.as_str()
116+
.is_some_and(|summary| !summary.is_empty())
117+
}));
118+
}
119+
80120
#[test]
81121
fn syntax_parser_accepts_all_fixtures() {
82122
let mut paths = fixture_paths("tests/fixtures/pass");

0 commit comments

Comments
 (0)