1pub mod dump;
13pub mod error;
14pub mod types;
15mod validate;
16pub(crate) mod visit;
17
18pub use error::HirError;
19pub use types::{
20 Annotation, AnnotationArg, Declaration, DictEntry, DirectiveRecord, DirectiveValue, Event,
21 Expr, Generator, OptimizationState, Position, PreprocessingSnapshot, PreprocessingState,
22 Program, Protocol, Rule, RuleEntry, Settings, SettingsListElement, SettingsNode, SourceFile,
23 Span, Stmt, SwitchArm, TranslationState, default_var_index,
24};
25
26use serde_json::Value;
27
28pub fn parse_str(input: &str) -> Result<Program, HirError> {
34 let value: Value = serde_json::from_str(input)?;
35 parse_value(value)
36}
37
38pub fn parse_value(value: Value) -> Result<Program, HirError> {
40 validate::check_envelope(&value)?;
41 validate::check_unknown_kinds(&value)?;
42 let program: Program = serde_json::from_value(value)?;
43 program.validate()?;
44 Ok(program)
45}
46
47impl Program {
48 pub fn validate(&self) -> Result<(), HirError> {
52 validate::validate_program(self)
53 }
54
55 pub fn dump(&self) -> String {
58 dump::dump(self)
59 }
60}
61
62#[cfg(test)]
63mod tests {
64 use serde_json::json;
65
66 use super::parse_value;
67 use super::validate::check_envelope;
68
69 #[test]
70 fn v2_envelope_is_accepted_and_v1_is_rejected_before_body_inspection() {
71 let v2 = json!({
72 "protocol": { "name": "wright/opy-hir", "version": "2.0.0" }
73 });
74 assert!(check_envelope(&v2).is_ok());
75
76 let v1 = json!({
77 "protocol": { "name": "wright/opy-hir", "version": "1.1.0" },
78 "rules": [{ "kind": "malformed-v1-body" }]
79 });
80 let error = check_envelope(&v1).expect_err("v1 payload must not enter the v2 parser");
81 assert_eq!(error.code(), "incompatible-protocol");
82 }
83
84 #[test]
85 fn expression_errors_precede_invalid_statement_structure() {
86 let payload = json!({
87 "protocol": { "name": "wright/opy-hir", "version": "2.0.0" },
88 "generator": { "name": "test", "version": "0", "frontend": "test" },
89 "files": [],
90 "rules": [{
91 "name": "test",
92 "event": { "name": "Ongoing - Global", "args": [] },
93 "conditions": [],
94 "actions": [{
95 "kind": "delete",
96 "target": { "kind": "globalVar", "name": "missing" }
97 }]
98 }]
99 });
100
101 let program: super::Program = serde_json::from_value(payload.clone()).unwrap();
102 assert_eq!(
103 program.validate().unwrap_err().code(),
104 "unresolved-reference"
105 );
106 assert_eq!(
107 parse_value(payload).unwrap_err().code(),
108 "unresolved-reference"
109 );
110 }
111
112 #[test]
113 fn unknown_conditional_condition_kind_preserves_unsupported_node_span() {
114 let error = parse_value(json!({
115 "protocol": { "name": "wright/opy-hir", "version": "2.0.0" },
116 "rules": [{
117 "event": { "args": [] },
118 "conditions": [],
119 "actions": [{
120 "kind": "expr",
121 "expr": {
122 "kind": "conditional",
123 "thenValue": { "kind": "number", "value": 1 },
124 "condition": {
125 "kind": "future-expression",
126 "span": {
127 "file": 0,
128 "start": { "line": 4, "col": 12 },
129 "end": { "line": 4, "col": 20 }
130 }
131 },
132 "elseValue": { "kind": "number", "value": 0 }
133 }
134 }]
135 }]
136 }))
137 .expect_err("unknown kinds nested in conditional conditions must be rejected");
138
139 assert_eq!(error.code(), "unsupported-node");
140 assert_eq!(error.message(), "unsupported node kind 'future-expression'");
141 assert_eq!(error.span().unwrap().start.line, 4);
142 assert_eq!(error.span().unwrap().start.col, 12);
143 }
144}