1use rdx_ast::{AttributeValue, ComponentNode, Node, Root};
2
3use crate::{PropType, Schema, type_matches, value_type_name};
4
5#[derive(Debug, Clone, PartialEq, Eq)]
7pub enum Severity {
8 Error,
9 Warning,
10}
11
12#[derive(Debug, Clone)]
14pub struct Diagnostic {
15 pub severity: Severity,
16 pub message: String,
17 pub component: String,
19 pub line: usize,
21 pub column: usize,
23}
24
25impl std::fmt::Display for Diagnostic {
26 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27 let level = match self.severity {
28 Severity::Error => "error",
29 Severity::Warning => "warning",
30 };
31 write!(
32 f,
33 "{}:{}:{}: {}: {}",
34 self.component, self.line, self.column, level, self.message
35 )
36 }
37}
38
39pub fn validate(root: &Root, schema: &Schema) -> Vec<Diagnostic> {
60 let mut diagnostics = Vec::new();
61 validate_nodes(&root.children, schema, &mut diagnostics, None);
62 diagnostics
63}
64
65fn validate_nodes(
66 nodes: &[Node],
67 schema: &Schema,
68 diagnostics: &mut Vec<Diagnostic>,
69 parent_allowed_children: Option<&[String]>,
70) {
71 for node in nodes {
72 if let Node::Component(comp) = node {
73 validate_component(comp, schema, diagnostics, parent_allowed_children);
74 }
75 if !matches!(node, Node::Component(_))
77 && let Some(children) = node.children()
78 {
79 validate_nodes(children, schema, diagnostics, None);
80 }
81 }
82}
83
84fn validate_component(
85 comp: &ComponentNode,
86 schema: &Schema,
87 diagnostics: &mut Vec<Diagnostic>,
88 parent_allowed_children: Option<&[String]>,
89) {
90 let line = comp.position.start.line;
91 let column = comp.position.start.column;
92 let name = &comp.name;
93
94 if let Some(allowed) = parent_allowed_children
96 && !allowed.iter().any(|a| a == name)
97 {
98 diagnostics.push(Diagnostic {
99 severity: Severity::Error,
100 message: format!("<{name}> is not allowed as a child here"),
101 component: name.clone(),
102 line,
103 column,
104 });
105 }
106
107 let Some(comp_schema) = schema.components.get(name.as_str()) else {
108 if schema.strict {
110 diagnostics.push(Diagnostic {
111 severity: Severity::Error,
112 message: format!("unknown component <{name}>"),
113 component: name.clone(),
114 line,
115 column,
116 });
117 }
118 return;
119 };
120
121 if comp_schema.self_closing && !comp.children.is_empty() {
123 diagnostics.push(Diagnostic {
124 severity: Severity::Error,
125 message: format!("<{name}> must be self-closing (no children)"),
126 component: name.clone(),
127 line,
128 column,
129 });
130 }
131
132 for (prop_name, prop_schema) in &comp_schema.props {
134 if prop_schema.required {
135 let found = comp.attributes.iter().any(|a| a.name == *prop_name);
136 if !found {
137 diagnostics.push(Diagnostic {
138 severity: Severity::Error,
139 message: format!("missing required prop `{prop_name}`"),
140 component: name.clone(),
141 line,
142 column,
143 });
144 }
145 }
146 }
147
148 for attr in &comp.attributes {
150 let attr_line = attr.position.start.line;
151 let attr_col = attr.position.start.column;
152
153 let Some(prop_schema) = comp_schema.props.get(&attr.name) else {
154 diagnostics.push(Diagnostic {
156 severity: Severity::Warning,
157 message: format!("unknown prop `{}` on <{name}>", attr.name),
158 component: name.clone(),
159 line: attr_line,
160 column: attr_col,
161 });
162 continue;
163 };
164
165 if matches!(attr.value, AttributeValue::Variable(_))
167 && prop_schema.prop_type != PropType::Variable
168 {
169 continue;
171 }
172
173 if !type_matches(&attr.value, &prop_schema.prop_type) {
174 diagnostics.push(Diagnostic {
175 severity: Severity::Error,
176 message: format!(
177 "prop `{}` on <{name}> expects {}, got {}",
178 attr.name,
179 format_expected_type(&prop_schema.prop_type),
180 value_type_name(&attr.value),
181 ),
182 component: name.clone(),
183 line: attr_line,
184 column: attr_col,
185 });
186 }
187
188 if prop_schema.prop_type == PropType::Enum
190 && let (Some(allowed), AttributeValue::String(val)) = (&prop_schema.values, &attr.value)
191 && !allowed.contains(val)
192 {
193 diagnostics.push(Diagnostic {
194 severity: Severity::Error,
195 message: format!(
196 "prop `{}` on <{name}> must be one of [{}], got \"{}\"",
197 attr.name,
198 allowed.join(", "),
199 val,
200 ),
201 component: name.clone(),
202 line: attr_line,
203 column: attr_col,
204 });
205 }
206 }
207
208 validate_nodes(
210 &comp.children,
211 schema,
212 diagnostics,
213 comp_schema.allowed_children.as_deref(),
214 );
215}
216
217fn format_expected_type(t: &PropType) -> &'static str {
218 match t {
219 PropType::String => "string",
220 PropType::Number => "number",
221 PropType::Boolean => "boolean",
222 PropType::Enum => "string (enum)",
223 PropType::Object => "object",
224 PropType::Array => "array",
225 PropType::Variable => "variable",
226 PropType::Any => "any",
227 }
228}