1#[derive(Clone, Copy, Debug, Eq, PartialEq)]
7pub struct FormDiagnosticReservation {
8 pub code: &'static str,
9 pub meaning: &'static str,
10}
11
12pub const FORM_DIAGNOSTIC_RESERVATIONS: [FormDiagnosticReservation; 12] = [
14 FormDiagnosticReservation {
15 code: "PSC1084",
16 meaning: "Duplicate form",
17 },
18 FormDiagnosticReservation {
19 code: "PSC1085",
20 meaning: "Duplicate field",
21 },
22 FormDiagnosticReservation {
23 code: "PSC1086",
24 meaning: "Missing submit",
25 },
26 FormDiagnosticReservation {
27 code: "PSC1087",
28 meaning: "Invalid validator",
29 },
30 FormDiagnosticReservation {
31 code: "PSC1088",
32 meaning: "Cyclic validation dependency",
33 },
34 FormDiagnosticReservation {
35 code: "PSC1089",
36 meaning: "Invalid serialization",
37 },
38 FormDiagnosticReservation {
39 code: "PSC1090",
40 meaning: "Invalid reset",
41 },
42 FormDiagnosticReservation {
43 code: "PSC1091",
44 meaning: "Duplicate binding",
45 },
46 FormDiagnosticReservation {
47 code: "PSC1092",
48 meaning: "Nested forms",
49 },
50 FormDiagnosticReservation {
51 code: "PSC1093",
52 meaning: "Invalid ownership",
53 },
54 FormDiagnosticReservation {
55 code: "PSC1094",
56 meaning: "Invalid submit signature",
57 },
58 FormDiagnosticReservation {
59 code: "PSC1095",
60 meaning: "Invalid field scope",
61 },
62];
63
64#[must_use]
76#[allow(clippy::too_many_lines)]
77pub fn collect_form_diagnostics(
78 model: &crate::ApplicationSemanticModel,
79) -> Vec<crate::ComponentDiagnostic> {
80 use crate::{
81 FormDeclarationViolation, FormFieldBindingViolation, FormFieldDeclarationViolation,
82 SubmissionDeclarationViolation, SubmissionHostViolation,
83 };
84
85 let mut diagnostics = Vec::new();
86 for candidate in model
87 .components
88 .iter()
89 .flat_map(|component| &component.form_declaration_candidates)
90 {
91 if candidate
92 .violations()
93 .contains(&FormDeclarationViolation::DuplicateName)
94 {
95 diagnostics.push(form_diagnostic(
96 "PSC1084",
97 candidate.provenance.clone(),
98 candidate.id.as_semantic_id().clone(),
99 ));
100 } else if !candidate.violations().is_empty() {
101 diagnostics.push(form_diagnostic(
102 "PSC1093",
103 candidate.provenance.clone(),
104 candidate.id.as_semantic_id().clone(),
105 ));
106 }
107 }
108 for candidate in model
109 .components
110 .iter()
111 .flat_map(|component| &component.form_field_declaration_candidates)
112 {
113 let code = if candidate
114 .violations
115 .contains(&FormFieldDeclarationViolation::DuplicateName)
116 {
117 Some("PSC1085")
118 } else if candidate.violations.iter().any(|violation| {
119 matches!(
120 violation,
121 FormFieldDeclarationViolation::InvalidFormDesignator
122 | FormFieldDeclarationViolation::UnresolvedForm
123 | FormFieldDeclarationViolation::InvalidForm
124 | FormFieldDeclarationViolation::CrossComponentForm
125 | FormFieldDeclarationViolation::InvalidOwner
126 )
127 }) {
128 Some("PSC1093")
129 } else if candidate.violations.is_empty() {
130 None
131 } else {
132 Some("PSC1095")
133 };
134 if let Some(code) = code {
135 diagnostics.push(form_diagnostic(
136 code,
137 candidate.provenance.clone(),
138 candidate.id.as_semantic_id().clone(),
139 ));
140 }
141 }
142 for candidate in &model.validation_rule_candidates {
143 if !candidate.violations.is_empty() {
144 diagnostics.push(form_diagnostic(
145 "PSC1087",
146 candidate.decorator_provenance.clone(),
147 candidate.id.as_semantic_id().clone(),
148 ));
149 }
150 }
151 for cycle in &model.validation_graph.cycles {
152 for candidate in &cycle.candidates {
153 let provenance = model
154 .validation_rule_candidates
155 .iter()
156 .find(|item| item.id == *candidate)
157 .map(|item| item.decorator_provenance.clone())
158 .expect("validation cycle candidate retained");
159 diagnostics.push(form_diagnostic(
160 "PSC1088",
161 provenance,
162 candidate.as_semantic_id().clone(),
163 ));
164 }
165 }
166 for candidate in &model.submissions.candidates {
167 let code = if candidate.violations.iter().any(|violation| {
168 matches!(
169 violation,
170 SubmissionDeclarationViolation::StaticMethod
171 | SubmissionDeclarationViolation::AsyncMethod
172 | SubmissionDeclarationViolation::ParameterizedMethod
173 | SubmissionDeclarationViolation::InvalidReturnType
174 )
175 }) {
176 Some("PSC1094")
177 } else if candidate.violations.is_empty() {
178 None
179 } else {
180 Some("PSC1093")
181 };
182 if let Some(code) = code {
183 diagnostics.push(form_diagnostic(
184 code,
185 candidate.provenance.clone(),
186 candidate.id.as_semantic_id().clone(),
187 ));
188 }
189 }
190 for declaration in &model.serialization.declarations {
191 if let Some(form) = &declaration.form {
192 if !declaration.invoked
193 || declaration.argument_count != 1
194 || !matches!(
195 declaration.format.as_deref(),
196 Some("json" | "form-data" | "url-encoded")
197 )
198 {
199 diagnostics.push(form_diagnostic(
200 "PSC1089",
201 declaration.provenance.clone(),
202 form.as_semantic_id().clone(),
203 ));
204 }
205 }
206 }
207 for candidate in &model.form_field_binding_candidates {
208 if candidate.violations.iter().any(|violation| {
209 matches!(
210 violation,
211 FormFieldBindingViolation::DuplicateFieldControl
212 | FormFieldBindingViolation::DuplicateBindingAttribute
213 | FormFieldBindingViolation::CompetingValueBinding
214 | FormFieldBindingViolation::CompetingCheckedBinding
215 )
216 }) {
217 diagnostics.push(form_diagnostic(
218 "PSC1091",
219 candidate.provenance.clone(),
220 candidate.id.as_semantic_id().clone(),
221 ));
222 }
223 }
224 for candidate in &model.submission_host_candidates {
225 let code = if candidate.violations.iter().any(|violation| {
226 matches!(
227 violation,
228 SubmissionHostViolation::NestedHost
229 | SubmissionHostViolation::InvalidHostElement
230 | SubmissionHostViolation::DuplicateHostAttribute
231 | SubmissionHostViolation::InvalidHostExpression
232 )
233 }) {
234 Some("PSC1092")
235 } else if candidate
236 .violations
237 .contains(&SubmissionHostViolation::MissingSubmissionPlan)
238 {
239 Some("PSC1086")
240 } else if candidate.violations.iter().any(|violation| {
241 matches!(
242 violation,
243 SubmissionHostViolation::CrossComponentForm
244 | SubmissionHostViolation::ContainedControlForDifferentForm
245 )
246 }) {
247 Some("PSC1093")
248 } else {
249 None
250 };
251 if let Some(code) = code {
252 diagnostics.push(form_diagnostic(
253 code,
254 candidate.provenance.clone(),
255 candidate.id.as_semantic_id().clone(),
256 ));
257 }
258 }
259 for form in model.forms.values() {
260 if !model
261 .reset
262 .plans
263 .contains_key(&crate::ResetPlanId::for_form(&form.id))
264 {
265 diagnostics.push(form_diagnostic(
266 "PSC1090",
267 form.provenance.clone(),
268 form.id.as_semantic_id().clone(),
269 ));
270 }
271 }
272 diagnostics.sort_by(|left, right| {
273 (
274 left.code.as_str(),
275 left.provenance
276 .as_ref()
277 .map(|item| (&item.path, item.span.start)),
278 left.component_id.as_ref(),
279 )
280 .cmp(&(
281 right.code.as_str(),
282 right
283 .provenance
284 .as_ref()
285 .map(|item| (&item.path, item.span.start)),
286 right.component_id.as_ref(),
287 ))
288 });
289 diagnostics
290 .dedup_by(|left, right| left.code == right.code && left.component_id == right.component_id);
291 diagnostics
292}
293
294fn form_diagnostic(
295 code: &str,
296 provenance: crate::SourceProvenance,
297 subject: crate::SemanticId,
298) -> crate::ComponentDiagnostic {
299 let message = FORM_DIAGNOSTIC_RESERVATIONS
300 .iter()
301 .find(|reservation| reservation.code == code)
302 .expect("I18 reserved code")
303 .meaning;
304 crate::ComponentDiagnostic {
305 code: code.to_string(),
306 severity: crate::ComponentDiagnosticSeverity::Error,
307 message: format!("{message}."),
308 provenance: Some(provenance),
309 effect_id: None,
310 statement_id: None,
311 context_declaration_candidate_id: None,
312 context_id: None,
313 provider_id: None,
314 consumer_id: None,
315 slot_id: None,
316 invocation_id: None,
317 component_instance_id: None,
318 slot_binding_id: None,
319 structural_region_id: None,
320 component_id: Some(subject),
321 provider_instance_id: None,
322 consumer_instance_id: None,
323 secondary_labels: Vec::new(),
324 }
325}
326
327#[cfg(test)]
328mod tests {
329 use std::collections::BTreeSet;
330
331 use crate::{
332 COMPONENT_DIAGNOSTIC_CONTRACTS, RESUME_MANIFEST_SCHEMA_VERSION,
333 RUNTIME_COMPONENT_ARTIFACT_SCHEMA_VERSION, RUNTIME_CONTEXT_ARTIFACT_SCHEMA_VERSION,
334 SEMANTIC_GRAPH_SCHEMA_VERSION, TEMPLATE_MANIFEST_SCHEMA_VERSION,
335 };
336
337 use super::FORM_DIAGNOSTIC_RESERVATIONS;
338
339 #[test]
340 fn projects_retained_form_candidates_without_runtime_evidence() {
341 let model = crate::build_application_semantic_model(&presolve_parser::parse_file(
342 "src/Duplicate.tsx",
343 r#"@component("duplicate") class Duplicate { @form() profile!: Form; @form() profile!: Form; render() { return <main />; } }"#,
344 ));
345 let diagnostics = super::collect_form_diagnostics(&model);
346 assert!(diagnostics
347 .iter()
348 .all(|diagnostic| diagnostic.code == "PSC1084"));
349 assert_eq!(diagnostics.len(), 2);
350 assert!(diagnostics
351 .iter()
352 .all(|diagnostic| diagnostic.component_id.is_some()));
353 }
354
355 #[test]
356 fn phase_i_entry_reserves_the_next_diagnostic_range_after_the_frozen_h_baseline() {
357 let phase_h_codes = COMPONENT_DIAGNOSTIC_CONTRACTS
358 .iter()
359 .map(|contract| contract.code)
360 .collect::<BTreeSet<_>>();
361 let form_codes = FORM_DIAGNOSTIC_RESERVATIONS
362 .iter()
363 .map(|reservation| reservation.code)
364 .collect::<Vec<_>>();
365
366 assert_eq!(phase_h_codes.last(), Some(&"PSC1083"));
367 assert_eq!(form_codes.first(), Some(&"PSC1084"));
368 assert_eq!(form_codes.last(), Some(&"PSC1095"));
369 assert_eq!(
370 form_codes.len(),
371 form_codes.iter().collect::<BTreeSet<_>>().len()
372 );
373
374 for (offset, reservation) in FORM_DIAGNOSTIC_RESERVATIONS.iter().enumerate() {
375 assert_eq!(reservation.code, format!("PSC{}", 1084 + offset));
376 assert!(!reservation.meaning.is_empty());
377 }
378 }
379
380 #[test]
381 fn phase_i_i17_updates_the_forms_inspection_schema_versions() {
382 assert_eq!(RUNTIME_COMPONENT_ARTIFACT_SCHEMA_VERSION, 4);
383 assert_eq!(RESUME_MANIFEST_SCHEMA_VERSION, 6);
384 assert_eq!(TEMPLATE_MANIFEST_SCHEMA_VERSION, 5);
385 assert_eq!(RUNTIME_CONTEXT_ARTIFACT_SCHEMA_VERSION, 2);
386 assert_eq!(SEMANTIC_GRAPH_SCHEMA_VERSION, 6);
387 }
388}