Skip to content

Commit 87b674e

Browse files
committed
Add diagnostic explanation registry
1 parent 423a386 commit 87b674e

5 files changed

Lines changed: 148 additions & 4 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ It currently implements:
99

1010
- `rss check <file.rss>` with human diagnostics
1111
- `rss check --json <file.rss>` with serde-backed machine-readable diagnostics
12+
- `rss check --explain <code>` for diagnostic code explanations
1213
- `rss fmt <file.rss>` as a parse/check gate that prints the source unchanged when valid
1314
- `rss review <old.rss> <new.rss>` for a first-pass API/type/effect/freshness diff
1415
- Lexer, lightweight parser, syntax AST, HIR signature table, and semantic checks for the review-critical v0.4.1 rules
@@ -61,5 +62,6 @@ cargo clippy -- -D warnings
6162
cargo test
6263
cargo run --bin rss -- check path/to/file.rss
6364
cargo run --bin rss -- check --json path/to/file.rss
65+
cargo run --bin rss -- check --explain RS0401
6466
cargo run --bin rss -- review old.rss new.rss
6567
```

src/diagnostic.rs

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,13 @@ pub struct Diagnostic {
4545
pub fixes: Vec<Fix>,
4646
}
4747

48+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49+
pub struct DiagnosticExplanation {
50+
pub code: &'static str,
51+
pub title: &'static str,
52+
pub explanation: &'static str,
53+
}
54+
4855
impl Diagnostic {
4956
pub fn error(
5057
code: &str,
@@ -100,6 +107,19 @@ impl Diagnostic {
100107
}
101108
}
102109

110+
pub fn explain_diagnostic_code(code: &str) -> Option<&'static DiagnosticExplanation> {
111+
DIAGNOSTIC_EXPLANATIONS
112+
.iter()
113+
.find(|explanation| explanation.code == code)
114+
}
115+
116+
pub fn format_diagnostic_explanation(explanation: &DiagnosticExplanation) -> String {
117+
format!(
118+
"{}: {}\n\n{}\n",
119+
explanation.code, explanation.title, explanation.explanation
120+
)
121+
}
122+
103123
pub fn format_diagnostics_human(diagnostics: &[Diagnostic]) -> String {
104124
let mut output = String::new();
105125
for diagnostic in diagnostics {
@@ -131,6 +151,94 @@ pub fn format_diagnostics_human(diagnostics: &[Diagnostic]) -> String {
131151
output
132152
}
133153

154+
static DIAGNOSTIC_EXPLANATIONS: &[DiagnosticExplanation] = &[
155+
DiagnosticExplanation {
156+
code: "RS0001",
157+
title: "missing file mode",
158+
explanation: "Every RSScript source file must declare `mode: managed` or `mode: uses-local` so reviewers can see whether local ownership features are allowed.",
159+
},
160+
DiagnosticExplanation {
161+
code: "RS0002",
162+
title: "missing return type",
163+
explanation: "Function signatures must spell out their return type. The checker applies this review rule broadly so API contracts do not rely on inference.",
164+
},
165+
DiagnosticExplanation {
166+
code: "RS0003",
167+
title: "missing parameter type",
168+
explanation: "Parameters must have explicit types so call effects, freshness, and resource rules can be checked against a stable signature.",
169+
},
170+
DiagnosticExplanation {
171+
code: "RS0004",
172+
title: "unknown effect",
173+
explanation: "The effect list contains an effect name outside the currently recognized MVP surface.",
174+
},
175+
DiagnosticExplanation {
176+
code: "RS0101",
177+
title: "file mode violation",
178+
explanation: "`mode: managed` files cannot use local-only features such as `local`, `manage`, `take`, or `ResourcePool<T>`.",
179+
},
180+
DiagnosticExplanation {
181+
code: "RS0201",
182+
title: "unnamed argument",
183+
explanation: "RSScript requires named call arguments so signature and review diffs remain readable.",
184+
},
185+
DiagnosticExplanation {
186+
code: "RS0202",
187+
title: "missing call-site data effect",
188+
explanation: "Arguments for non-Copy parameters must use an explicit `read`, `mut`, or `take` effect matching the callee signature.",
189+
},
190+
DiagnosticExplanation {
191+
code: "RS0301",
192+
title: "managed-to-local conversion",
193+
explanation: "Managed values cannot be rebound as local values. Create the value locally at its origin if local ownership is required.",
194+
},
195+
DiagnosticExplanation {
196+
code: "RS0401",
197+
title: "use after manage",
198+
explanation: "`manage value` moves a local value into the managed runtime. The original local binding cannot be used afterwards on any reachable path.",
199+
},
200+
DiagnosticExplanation {
201+
code: "RS0501",
202+
title: "local value retained",
203+
explanation: "APIs marked `effects(retains(param))` may store the argument beyond the call. Passing a clean local value directly would let local ownership escape.",
204+
},
205+
DiagnosticExplanation {
206+
code: "RS0601",
207+
title: "fresh return is not clean",
208+
explanation: "A `fresh` function may only return a newly created value, a known fresh call, or a clean local value that has not escaped through manage, take, retain, or capture.",
209+
},
210+
DiagnosticExplanation {
211+
code: "RS0602",
212+
title: "freshness unknown",
213+
explanation: "The MVP checker could not prove the returned value is fresh. Current proof support trusts clean locals, struct constructors, and known fresh calls.",
214+
},
215+
DiagnosticExplanation {
216+
code: "RS0701",
217+
title: "resource field",
218+
explanation: "Resource values cannot be stored directly in ordinary class or struct fields. Use `with` or an approved resource container such as `ResourcePool<T>`.",
219+
},
220+
DiagnosticExplanation {
221+
code: "RS0702",
222+
title: "resource escape",
223+
explanation: "A resource introduced by `with` must not escape the block through return, manage, retention, or managed closure capture.",
224+
},
225+
DiagnosticExplanation {
226+
code: "RS0801",
227+
title: "local captured by managed closure",
228+
explanation: "A closure bound with `let` is managed and may outlive clean local values. Use a local/noescape callback shape instead.",
229+
},
230+
DiagnosticExplanation {
231+
code: "RS0901",
232+
title: "take of handle field",
233+
explanation: "Handle fields are managed references. They cannot be consumed with `take` as if they were inline local fields.",
234+
},
235+
DiagnosticExplanation {
236+
code: "RS1001",
237+
title: "operator overload attempt",
238+
explanation: "The MVP language surface rejects likely user-defined operator overloads to keep review semantics explicit.",
239+
},
240+
];
241+
134242
pub fn format_diagnostics_json(diagnostics: &[Diagnostic]) -> String {
135243
let diagnostics: Vec<JsonDiagnostic<'_>> =
136244
diagnostics.iter().map(JsonDiagnostic::from).collect();

src/lib.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,8 @@ mod review;
88
pub mod syntax;
99

1010
pub use analyzer::analyze_source;
11-
pub use diagnostic::{Diagnostic, Severity, format_diagnostics_human, format_diagnostics_json};
11+
pub use diagnostic::{
12+
Diagnostic, DiagnosticExplanation, Severity, explain_diagnostic_code,
13+
format_diagnostic_explanation, format_diagnostics_human, format_diagnostics_json,
14+
};
1215
pub use review::{ReviewFinding, format_review_human, review_sources};

src/main.rs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ use std::fs;
33
use std::process::ExitCode;
44

55
use rsscript::{
6-
analyze_source, format_diagnostics_human, format_diagnostics_json, format_review_human,
7-
review_sources,
6+
analyze_source, explain_diagnostic_code, format_diagnostic_explanation,
7+
format_diagnostics_human, format_diagnostics_json, format_review_human, review_sources,
88
};
99

1010
fn main() -> ExitCode {
@@ -26,6 +26,15 @@ fn main() -> ExitCode {
2626
}
2727

2828
fn run_check(args: &[String]) -> ExitCode {
29+
if let Some(code) = parse_explain_args(args) {
30+
let Some(explanation) = explain_diagnostic_code(code) else {
31+
eprintln!("unknown diagnostic code: {code}");
32+
return ExitCode::from(2);
33+
};
34+
print!("{}", format_diagnostic_explanation(explanation));
35+
return ExitCode::SUCCESS;
36+
}
37+
2938
let (json, path) = parse_path_args(args);
3039
let Some(path) = path else {
3140
print_usage();
@@ -125,6 +134,13 @@ fn run_review(args: &[String]) -> ExitCode {
125134
ExitCode::SUCCESS
126135
}
127136

137+
fn parse_explain_args(args: &[String]) -> Option<&str> {
138+
let [flag, code] = args else {
139+
return None;
140+
};
141+
(flag == "--explain").then_some(code.as_str())
142+
}
143+
128144
fn parse_path_args(args: &[String]) -> (bool, Option<&str>) {
129145
let mut json = false;
130146
let mut path = None;
@@ -143,6 +159,7 @@ fn parse_path_args(args: &[String]) -> (bool, Option<&str>) {
143159
fn print_usage() {
144160
eprintln!("usage:");
145161
eprintln!(" rsscript check [--json] <file.rss>");
162+
eprintln!(" rsscript check --explain <code>");
146163
eprintln!(" rsscript fmt <file.rss>");
147164
eprintln!(" rsscript review <old.rss> <new.rss>");
148165
}

tests/checker.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ use std::path::{Path, PathBuf};
33

44
use rsscript::syntax::ast::Item;
55
use rsscript::syntax::parse_source;
6-
use rsscript::{analyze_source, format_diagnostics_json, review_sources};
6+
use rsscript::{
7+
analyze_source, explain_diagnostic_code, format_diagnostic_explanation,
8+
format_diagnostics_json, review_sources,
9+
};
710
use serde_json::Value;
811

912
#[test]
@@ -63,6 +66,17 @@ fn diagnostics_json_uses_protocol_shape() {
6366
assert!(first["fixes"].is_array());
6467
}
6568

69+
#[test]
70+
fn diagnostic_explanations_are_available_by_code() {
71+
let explanation = explain_diagnostic_code("RS0401").expect("RS0401 should be registered");
72+
let formatted = format_diagnostic_explanation(explanation);
73+
74+
assert_eq!(explanation.title, "use after manage");
75+
assert!(formatted.contains("RS0401"));
76+
assert!(formatted.contains("manage"));
77+
assert!(explain_diagnostic_code("RS9999").is_none());
78+
}
79+
6680
#[test]
6781
fn syntax_parser_accepts_all_fixtures() {
6882
let mut paths = fixture_paths("tests/fixtures/pass");

0 commit comments

Comments
 (0)