1use std::collections::{BTreeMap, BTreeSet};
2use std::fmt;
3use std::path::{Path, PathBuf};
4
5use serde::{Deserialize, Serialize};
6
7use crate::instance_context::{ConsumerInstanceId, ProviderInstanceId};
8use crate::semantic_id::{
9 ComponentInstanceId, ComponentInvocationId, ComponentStructuralRegionId, ConsumerId,
10 ContextDeclarationCandidateId, ContextId, EffectId, EffectStatementId, FieldId,
11 FormDeclarationCandidateId, FormFieldDeclarationCandidateId, FormId, ProviderId, SemanticId,
12 SemanticOwner, SlotBindingId, SlotDeclarationCandidateId, SlotId,
13 SubmissionDeclarationCandidateId, ValidationRuleCandidateId,
14};
15use crate::semantic_package::SemanticPackagePureOperation;
16use crate::semantic_provenance::SourceProvenance;
17use crate::semantic_reference::{SemanticReference, SemanticReferenceKind};
18
19use presolve_parser::{
20 ParsedArithmeticExpression, ParsedArithmeticExpressionKind, ParsedArithmeticOperator,
21 ParsedClass, ParsedComparisonOperator, ParsedComputedExpression, ParsedComputedExpressionKind,
22 ParsedConstantExpression, ParsedConstantExpressionKind, ParsedDecorator, ParsedEffectBody,
23 ParsedEffectExpression, ParsedEffectExpressionKind, ParsedEffectStatementKind,
24 ParsedEventHandler, ParsedFile, ParsedJsxAttribute, ParsedJsxAttributeValue, ParsedJsxChild,
25 ParsedJsxConditional, ParsedJsxFragment, ParsedJsxList, ParsedJsxNode, ParsedLogicalOperator,
26 ParsedMethod, ParsedMethodCall, ParsedSerializableValue, ParsedStateOperation,
27 ParsedStaticMemberDesignator, ParsedUnaryOperator, ParsedUnsupportedEffectStatementKind,
28 ParsedValidationRuleArgumentKind, ParsedValidationRuleExpressionKind, SourceSpan,
29};
30
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct ComponentGraph {
33 pub components: Vec<ComponentNode>,
34 pub diagnostics: Vec<ComponentDiagnostic>,
35 pub references: Vec<SemanticReference>,
36 pub provenance: BTreeMap<SemanticId, SourceProvenance>,
37}
38
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct ComponentNode {
41 pub id: SemanticId,
42 pub module_path: PathBuf,
44 pub owner: SemanticOwner,
45 pub class_name: String,
46 pub element_name: Option<String>,
47 pub route_path: Option<String>,
48 pub heritage: Option<AuthoredComponentHeritage>,
51 pub state_fields: Vec<StateField>,
52 pub context_declarations: Vec<ContextDeclaration>,
53 pub provider_declarations: Vec<ProviderDeclaration>,
54 pub consumer_declarations: Vec<ConsumerDeclaration>,
55 pub slot_declarations: Vec<SlotDeclaration>,
56 pub context_declaration_candidates: Vec<AuthoredContextDeclarationCandidate>,
59 pub slot_declaration_candidates: Vec<AuthoredSlotDeclarationCandidate>,
62 pub form_declaration_candidates: Vec<FormDeclarationCandidate>,
65 pub resource_declaration_candidates: Vec<AuthoredResourceDeclarationFact>,
68 pub route_loader_declaration_candidates: Vec<AuthoredRouteLoaderDeclarationFact>,
71 pub form_field_declaration_candidates: Vec<FormFieldDeclarationCandidate>,
75 pub validation_rule_declaration_facts: Vec<AuthoredValidationRuleDeclarationFact>,
78 pub submission_declaration_facts: Vec<AuthoredSubmissionDeclarationFact>,
80 pub serialization_declaration_facts: Vec<AuthoredSerializationDeclarationFact>,
82 pub opaque_action_facts: Vec<AuthoredOpaqueActionFact>,
85 pub terminal_package_invocations: Vec<AuthoredTerminalPackageInvocationFact>,
88 pub server_action_facts: Vec<AuthoredServerActionFact>,
89 pub shadowed_validation_intrinsics: BTreeSet<String>,
91 pub methods: Vec<ComponentMethod>,
92 pub effect_fields: Vec<ComponentEffectField>,
95 pub action_endpoints: Vec<ActionEndpoint>,
98 pub actions: Vec<ComponentAction>,
99 pub render: Option<RenderModel>,
100}
101
102impl ComponentNode {
103 #[must_use]
107 pub fn action_endpoint_ids(&self) -> Vec<(String, SemanticId)> {
108 let mut endpoints = self
109 .methods
110 .iter()
111 .filter(|method| method.is_action())
112 .map(|method| (method.name.clone(), method.id.clone()))
113 .collect::<BTreeMap<_, _>>();
114 for endpoint in &self.action_endpoints {
115 endpoints.insert(endpoint.name.clone(), endpoint.id.clone());
116 }
117 endpoints.into_iter().collect()
118 }
119
120 #[must_use]
121 pub fn action_endpoint_id(&self, name: &str) -> Option<SemanticId> {
122 self.action_endpoint_ids()
123 .into_iter()
124 .find_map(|(candidate, id)| (candidate == name).then_some(id))
125 }
126}
127
128#[derive(Debug, Clone, PartialEq, Eq)]
129pub struct AuthoredComponentHeritage {
130 pub base: String,
131 pub provenance: SourceProvenance,
132}
133
134#[derive(Debug, Clone, PartialEq, Eq)]
135pub struct AuthoredOpaqueActionFact {
136 pub id: SemanticId,
137 pub owner_component: Option<SemanticId>,
138 pub method: Option<SemanticId>,
139 pub method_name: String,
140 pub package: Option<String>,
141 pub export: Option<String>,
142 pub invoked: bool,
143 pub argument_count: usize,
144 pub is_action: bool,
145 pub action_invoked: bool,
146 pub is_async: bool,
147 pub parameter_count: usize,
148 pub has_body_effects: bool,
149 pub provenance: SourceProvenance,
150}
151
152#[derive(Debug, Clone, PartialEq, Eq)]
153pub struct AuthoredTerminalPackageInvocationFact {
154 pub id: SemanticId,
155 pub owner_component: SemanticId,
156 pub method: SemanticId,
157 pub method_name: String,
158 pub module_specifier: String,
159 pub export_name: String,
160 pub declaration_modules: Vec<String>,
161 pub provenance: SourceProvenance,
162}
163
164#[derive(Debug, Clone, PartialEq, Eq)]
165pub struct AuthoredServerActionFact {
166 pub id: SemanticId,
167 pub owner_component: Option<SemanticId>,
168 pub method: Option<SemanticId>,
169 pub method_name: String,
170 pub endpoint_designator: Option<String>,
171 pub invoked: bool,
172 pub argument_count: usize,
173 pub is_action: bool,
174 pub action_invoked: bool,
175 pub is_async: bool,
176 pub parameter_count: usize,
177 pub has_body_effects: bool,
178 pub provenance: SourceProvenance,
179}
180
181#[derive(Debug, Clone, PartialEq, Eq)]
183pub struct AuthoredStandardSchemaValidationFact {
184 pub module_specifier: String,
185 pub export_name: String,
186 pub declaration_modules: Vec<String>,
187 pub input_type: Option<String>,
188 pub output_type: Option<String>,
189}
190
191#[derive(Debug, Clone, PartialEq, Eq)]
193pub struct AuthoredValidationRuleDeclarationFact {
194 pub id: ValidationRuleCandidateId,
195 pub owner_component: Option<SemanticId>,
196 pub declaration_field: Option<SemanticId>,
197 pub authored_name: Option<String>,
198 pub declaration_kind: AuthoredDeclarationKind,
199 pub is_static: bool,
200 pub authored_ordinal: usize,
201 pub decorator_invoked: bool,
202 pub decorator_argument_count: usize,
203 pub expression: Option<AuthoredValidationRuleExpression>,
204 pub standard_schema: Option<AuthoredStandardSchemaValidationFact>,
205 pub conflicting_decorators: Vec<String>,
206 pub decorator_provenance: SourceProvenance,
207 pub name_provenance: Option<SourceProvenance>,
208 pub provenance: SourceProvenance,
209}
210
211#[allow(clippy::struct_excessive_bools)]
213#[derive(Debug, Clone, PartialEq, Eq)]
214pub struct AuthoredSubmissionDeclarationFact {
215 pub native_inline: bool,
216 pub id: SubmissionDeclarationCandidateId,
217 pub owner_component: Option<SemanticId>,
218 pub method: Option<SemanticId>,
219 pub method_name: Option<String>,
220 pub is_static: bool,
221 pub is_async: bool,
222 pub parameter_count: usize,
223 pub return_type: Option<String>,
224 pub submit_invoked: bool,
225 pub submit_argument_count: usize,
226 pub form_designator: Option<String>,
227 pub has_action: bool,
228 pub action_invoked: bool,
229 pub action_argument_count: usize,
230 pub capability_local_name: Option<String>,
234 pub capability_provenance: Option<SourceProvenance>,
235 pub inherited: bool,
236 pub decorator_provenance: SourceProvenance,
237 pub form_designator_provenance: Option<SourceProvenance>,
238 pub method_provenance: SourceProvenance,
239}
240
241#[derive(Debug, Clone, PartialEq, Eq)]
242pub struct AuthoredSerializationDeclarationFact {
243 pub owner_component: Option<SemanticId>,
244 pub declaration_field: Option<SemanticId>,
245 pub authored_name: Option<String>,
246 pub invoked: bool,
247 pub argument_count: usize,
248 pub format: Option<String>,
249 pub provenance: SourceProvenance,
250 pub decorator_provenance: SourceProvenance,
251}
252
253#[derive(Debug, Clone, PartialEq, Eq)]
254pub struct AuthoredResourceDeclarationFact {
255 pub owner_component: SemanticId,
256 pub field: String,
257 pub decorator_invoked: bool,
258 pub decorator_argument_count: usize,
259 pub endpoint_designator: Option<String>,
260 pub declared_type: Option<DeclaredStateType>,
261 pub provenance: SourceProvenance,
262}
263
264#[derive(Debug, Clone, PartialEq, Eq)]
267pub struct AuthoredRouteLoaderDeclarationFact {
268 pub owner_component: SemanticId,
269 pub field: String,
270 pub decorator_invoked: bool,
271 pub decorator_argument_count: usize,
272 pub endpoint_designator: Option<String>,
273 pub declared_type: Option<DeclaredStateType>,
274 pub provenance: SourceProvenance,
275}
276
277#[derive(Debug, Clone, PartialEq, Eq)]
278pub struct AuthoredValidationRuleExpression {
279 pub kind: AuthoredValidationRuleExpressionKind,
280 pub provenance: SourceProvenance,
281}
282
283#[derive(Debug, Clone, PartialEq, Eq)]
284pub enum AuthoredValidationRuleExpressionKind {
285 Call {
286 callee: Option<String>,
287 arguments: Vec<AuthoredValidationRuleArgument>,
288 },
289 Identifier(String),
290 Unsupported,
291}
292
293#[derive(Debug, Clone, PartialEq, Eq)]
294pub struct AuthoredValidationRuleArgument {
295 pub kind: AuthoredValidationRuleArgumentKind,
296 pub provenance: SourceProvenance,
297}
298
299#[derive(Debug, Clone, PartialEq, Eq)]
300pub enum AuthoredValidationRuleArgumentKind {
301 StringLiteral(String),
302 Constant(ConstantExpression),
303 ThisMember {
304 name: String,
305 name_provenance: SourceProvenance,
306 },
307 Unsupported,
308}
309
310#[derive(Debug, Clone, PartialEq, Eq)]
311pub struct StateField {
312 pub id: SemanticId,
313 pub owner: SemanticOwner,
314 pub name: String,
315 pub initial_value: Option<SerializableValue>,
316 pub initial_expression: Option<ConstantExpression>,
317 pub declared_type: Option<DeclaredStateType>,
318}
319
320#[derive(Debug, Clone, PartialEq, Eq)]
326pub struct ContextDeclaration {
327 pub authored_field: SemanticId,
328 pub name: String,
329 pub declared_type: DeclaredStateType,
330 pub default_expression: Option<ConstantExpression>,
331 pub decorator_provenance: SourceProvenance,
332 pub name_provenance: SourceProvenance,
333 pub provenance: SourceProvenance,
334}
335
336#[derive(Debug, Clone, PartialEq, Eq)]
338pub struct ContextDesignator {
339 pub component_symbol: String,
340 pub context_member: String,
341 pub provenance: SourceProvenance,
342 pub component_provenance: SourceProvenance,
343 pub member_provenance: SourceProvenance,
344}
345
346#[derive(Debug, Clone, PartialEq, Eq)]
348pub struct ProviderDeclaration {
349 pub authored_field: SemanticId,
350 pub name: String,
351 pub context_designator: ContextDesignator,
352 pub declared_type: DeclaredStateType,
353 pub value_expression: ComputedExpression,
354 pub decorator_provenance: SourceProvenance,
355 pub name_provenance: SourceProvenance,
356 pub provenance: SourceProvenance,
357}
358
359#[derive(Debug, Clone, PartialEq, Eq)]
361pub struct ConsumerDeclaration {
362 pub authored_field: SemanticId,
363 pub name: String,
364 pub context_designator: ContextDesignator,
365 pub requested_type: DeclaredStateType,
366 pub decorator_provenance: SourceProvenance,
367 pub name_provenance: SourceProvenance,
368 pub provenance: SourceProvenance,
369}
370
371#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
372pub enum SlotKind {
373 Default,
374 Named,
375}
376
377#[derive(Debug, Clone, PartialEq, Eq)]
379pub struct SlotDeclaration {
380 pub authored_field: SemanticId,
381 pub name: String,
382 pub kind: SlotKind,
383 pub declared_type: DeclaredStateType,
384 pub decorator_provenance: SourceProvenance,
385 pub name_provenance: SourceProvenance,
386 pub provenance: SourceProvenance,
387}
388
389#[derive(Debug, Clone, PartialEq, Eq)]
390pub enum SlotDeclarationViolation {
391 InvalidDeclarationKind { actual: AuthoredDeclarationKind },
392 StaticDeclarationUnsupported,
393 ConflictingSemanticDecorators,
394 DecoratorArity { actual: usize, expected: usize },
395 InvalidDeclaredType { actual: Option<String> },
396 ForbiddenInitializer,
397 DefiniteAssignmentRequired,
398 DuplicateSlot,
399}
400
401#[derive(Debug, Clone, PartialEq, Eq)]
405pub struct AuthoredSlotDeclarationCandidate {
406 pub id: SlotDeclarationCandidateId,
407 pub owner_component: SemanticId,
408 pub authored_declaration: SemanticId,
409 pub declaration_kind: AuthoredDeclarationKind,
410 pub field_name: Option<String>,
411 pub declared_type: Option<DeclaredStateType>,
412 pub decorator_argument_count: usize,
413 pub decorator_provenance: SourceProvenance,
414 pub name_provenance: Option<SourceProvenance>,
415 pub provenance: SourceProvenance,
416 pub static_modifier_provenance: Option<SourceProvenance>,
417 pub initializer_provenance: Option<SourceProvenance>,
418 pub violations: Vec<SlotDeclarationViolation>,
419}
420
421#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
422pub enum ContextDeclarationCandidateKind {
423 Context,
424 Provider,
425 Consumer,
426}
427
428#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
429pub enum AuthoredDeclarationKind {
430 Class,
431 InstanceField,
432 StaticField,
433 Method,
434 Getter,
435 Setter,
436 Parameter,
437}
438
439#[derive(Debug, Clone, PartialEq, Eq)]
440pub enum FormDeclarationViolation {
441 InvalidOwner,
442 InvalidTarget { actual: AuthoredDeclarationKind },
443 UnsupportedFieldName,
444 InvalidDecoratorInvocation,
445 InvalidDecoratorArity { actual: usize, expected: usize },
446 DuplicateFormDecorator,
447 StaticField,
448 InitializedField,
449 DeclarationOnlyRequired,
450 MissingType,
451 InvalidType { actual: Option<String> },
452 DuplicateName,
453 ConflictingSemanticDecorator,
454 InheritedDeclaration,
455}
456
457#[derive(Debug, Clone, PartialEq, Eq)]
458pub enum FormDeclarationStatus {
459 Valid,
460 Invalid(Vec<FormDeclarationViolation>),
461}
462
463#[derive(Debug, Clone, PartialEq, Eq)]
467pub struct FormDeclarationCandidate {
468 pub id: FormDeclarationCandidateId,
469 pub owner_component: Option<SemanticId>,
470 pub form_id: Option<FormId>,
471 pub authored_field: Option<SemanticId>,
472 pub authored_name: Option<String>,
473 pub declaration_kind: AuthoredDeclarationKind,
474 pub decorator_invoked: bool,
475 pub decorator_argument_count: usize,
476 pub decorator_argument_provenance: Vec<SourceProvenance>,
477 pub declaration_only: bool,
478 pub declared_type: Option<DeclaredStateType>,
479 pub conflicting_decorators: Vec<String>,
480 pub decorator_provenance: SourceProvenance,
481 pub name_provenance: Option<SourceProvenance>,
482 pub initializer_provenance: Option<SourceProvenance>,
483 pub provenance: SourceProvenance,
484 pub status: FormDeclarationStatus,
485}
486
487impl FormDeclarationCandidate {
488 #[must_use]
489 pub fn violations(&self) -> &[FormDeclarationViolation] {
490 match &self.status {
491 FormDeclarationStatus::Valid => &[],
492 FormDeclarationStatus::Invalid(violations) => violations,
493 }
494 }
495}
496
497#[derive(Debug, Clone, PartialEq, Eq)]
498pub struct FormDesignatorFact {
499 pub authored_name: String,
500 pub provenance: SourceProvenance,
501 pub name_provenance: SourceProvenance,
502}
503
504#[derive(Debug, Clone, PartialEq, Eq)]
505pub struct UnsupportedFormDesignatorFact {
506 pub object: String,
507 pub member: String,
508 pub provenance: SourceProvenance,
509}
510
511#[derive(Debug, Clone, PartialEq, Eq)]
512pub enum FormFieldDeclarationViolation {
513 InvalidOwner,
514 InvalidTarget { actual: AuthoredDeclarationKind },
515 InvalidDecoratorInvocation,
516 InvalidDecoratorArity { actual: usize, expected: usize },
517 InvalidPath,
518 DuplicateFieldDecorator,
519 InvalidFormDesignator,
520 UnresolvedForm,
521 InvalidForm,
522 CrossComponentForm,
523 StaticField,
524 MissingInitializer,
525 UnsupportedInitializer,
526 InvalidDeclaredType,
527 InitializerTypeMismatch,
528 NonSerializableType,
529 DuplicateName,
530 ConflictingPath,
531 ConflictingSemanticDecorator,
532 InheritedDeclaration,
533 UnsupportedFieldName,
534}
535
536#[derive(Debug, Clone, PartialEq, Eq)]
540pub struct FormFieldDeclarationCandidate {
541 pub id: FormFieldDeclarationCandidateId,
542 pub owner_component: Option<SemanticId>,
543 pub declaration_field: Option<SemanticId>,
544 pub authored_name: Option<String>,
545 pub field_id: Option<FieldId>,
546 pub decorator_invoked: bool,
547 pub decorator_argument_count: usize,
548 pub decorator_argument_provenance: Vec<SourceProvenance>,
549 pub nested_path_segments: Option<Vec<String>>,
552 pub form_designator: Option<FormDesignatorFact>,
553 pub unsupported_form_designator: Option<UnsupportedFormDesignatorFact>,
554 pub resolved_form: Option<FormId>,
555 pub declaration_kind: AuthoredDeclarationKind,
556 pub is_static: bool,
557 pub declared_type: Option<DeclaredStateType>,
558 pub authority_type: Option<crate::SemanticType>,
562 pub semantic_type: Option<crate::SemanticType>,
563 pub type_assignment: Option<crate::SemanticTypeAssignment>,
564 pub initializer: Option<SerializableValue>,
565 pub conflicting_decorators: Vec<String>,
566 pub decorator_provenance: SourceProvenance,
567 pub name_provenance: Option<SourceProvenance>,
568 pub initializer_provenance: Option<SourceProvenance>,
569 pub provenance: SourceProvenance,
570 pub violations: Vec<FormFieldDeclarationViolation>,
571}
572
573impl FormFieldDeclarationCandidate {
574 #[must_use]
575 pub fn is_valid(&self) -> bool {
576 self.violations.is_empty()
577 }
578
579 pub(crate) fn add_violation(&mut self, violation: FormFieldDeclarationViolation) {
580 self.violations.push(violation);
581 canonicalize_form_field_violations(&mut self.violations);
582 }
583}
584
585#[derive(Debug, Clone, PartialEq, Eq)]
586pub enum ContextDeclarationViolation {
587 InvalidDeclarationKind { actual: AuthoredDeclarationKind },
588 StaticDeclarationUnsupported,
589 ConflictingSemanticDecorators,
590 DecoratorArity { actual: usize, expected: usize },
591 ContextDesignatorUnsupported,
592 UnresolvedContextDesignator,
593 MissingDeclaredType,
594 MissingInitializer,
595 ForbiddenInitializer,
596 UnsupportedInitializer,
597 DefiniteAssignmentRequired,
598 DuplicateProvider,
599}
600
601#[derive(Debug, Clone, PartialEq, Eq)]
604pub struct AuthoredContextDeclarationCandidate {
605 pub id: ContextDeclarationCandidateId,
606 pub kind: ContextDeclarationCandidateKind,
607 pub owner_component: SemanticId,
608 pub authored_declaration: SemanticId,
609 pub declaration_kind: AuthoredDeclarationKind,
610 pub field_name: Option<String>,
611 pub declared_type: Option<DeclaredStateType>,
612 pub context_designator: Option<ContextDesignator>,
613 pub decorator_argument_count: usize,
614 pub decorator_provenance: SourceProvenance,
615 pub provenance: SourceProvenance,
616 pub static_modifier_provenance: Option<SourceProvenance>,
617 pub initializer_provenance: Option<SourceProvenance>,
618 pub violations: Vec<ContextDeclarationViolation>,
619}
620
621#[derive(Debug, Clone, PartialEq, Eq)]
623pub struct ArithmeticExpression {
624 pub kind: ArithmeticExpressionKind,
625 pub span: SourceSpan,
626}
627
628#[derive(Debug, Clone, PartialEq, Eq)]
629pub enum ArithmeticExpressionKind {
630 Number(String),
631 Binary {
632 operator: ArithmeticOperator,
633 left: Box<ArithmeticExpression>,
634 right: Box<ArithmeticExpression>,
635 },
636}
637
638#[derive(Debug, Clone, Copy, PartialEq, Eq)]
639pub enum ArithmeticOperator {
640 Add,
641 Subtract,
642 Multiply,
643 Divide,
644 Remainder,
645}
646
647#[derive(Debug, Clone, PartialEq, Eq)]
648pub enum ArithmeticEvaluationError {
649 InvalidNumber(String),
650 DivisionByZero,
651 NonFiniteResult,
652}
653
654#[derive(Debug, Clone, PartialEq, Eq)]
656pub struct ConstantExpression {
657 pub kind: ConstantExpressionKind,
658 pub span: SourceSpan,
659}
660
661#[derive(Debug, Clone, PartialEq, Eq)]
663pub struct ComputedExpression {
664 pub kind: ComputedExpressionKind,
665 pub span: SourceSpan,
666}
667
668#[derive(Debug, Clone, PartialEq, Eq)]
670pub enum ComputedExpressionKind {
671 Literal(SerializableValue),
672 ThisMember(String),
673 MemberAccess {
674 object: Box<ComputedExpression>,
675 property: String,
676 optional: bool,
677 },
678 IndexAccess {
679 object: Box<ComputedExpression>,
680 index: Box<ComputedExpression>,
681 },
682 Conditional {
683 condition: Box<ComputedExpression>,
684 when_true: Box<ComputedExpression>,
685 when_false: Box<ComputedExpression>,
686 },
687 Template {
688 quasis: Vec<String>,
689 expressions: Vec<ComputedExpression>,
690 },
691 Call {
692 callee: String,
693 arguments: Vec<ComputedExpression>,
694 },
695 BuiltinPureCall {
696 operation: BuiltinPureOperation,
697 arguments: Vec<ComputedExpression>,
698 },
699 SemanticPackagePureCall {
700 local_name: String,
701 package: String,
702 version: String,
703 integrity: String,
704 export: String,
705 runtime_module: String,
706 resume_policy: String,
707 operation: SemanticPackagePureOperation,
708 arguments: Vec<ComputedExpression>,
709 },
710 Arithmetic {
711 left: Box<ComputedExpression>,
712 right: Box<ComputedExpression>,
713 operator: ArithmeticOperator,
714 },
715 Comparison {
716 left: Box<ComputedExpression>,
717 right: Box<ComputedExpression>,
718 operator: ComparisonOperator,
719 },
720 Logical {
721 left: Box<ComputedExpression>,
722 right: Box<ComputedExpression>,
723 operator: LogicalOperator,
724 },
725 NullishCoalescing {
726 left: Box<ComputedExpression>,
727 right: Box<ComputedExpression>,
728 },
729 Unary {
730 operand: Box<ComputedExpression>,
731 operator: UnaryOperator,
732 },
733}
734
735#[derive(Debug, Clone, Copy, PartialEq, Eq)]
736pub enum BuiltinPureOperation {
737 MathAbs,
738 MathFloor,
739 MathCeil,
740 MathRound,
741 MathMin,
742 MathMax,
743}
744
745#[derive(Debug, Clone, PartialEq, Eq)]
747pub struct EffectBodySyntax {
748 pub statements: Vec<EffectStatementSyntax>,
749 pub cleanup: Option<EffectCleanupSyntax>,
750}
751
752#[derive(Debug, Clone, PartialEq, Eq)]
754pub struct EffectCleanupSyntax {
755 pub is_async: bool,
756 pub body: Box<EffectBodySyntax>,
757 pub span: SourceSpan,
758}
759
760#[derive(Debug, Clone, PartialEq, Eq)]
761pub struct EffectStatementSyntax {
762 pub kind: EffectStatementSyntaxKind,
763 pub span: SourceSpan,
764}
765
766#[derive(Debug, Clone, PartialEq, Eq)]
767pub enum EffectStatementSyntaxKind {
768 StaticMemberAssignment {
769 target: EffectExpression,
770 value: EffectExpression,
771 },
772 CapabilityCall {
773 callee: EffectExpression,
774 arguments: Vec<EffectExpression>,
775 },
776 EffectReturn {
777 value: Option<EffectExpression>,
778 },
779 Empty,
780 Unsupported(UnsupportedEffectStatementKind),
781}
782
783#[derive(Debug, Clone, Copy, PartialEq, Eq)]
784pub enum UnsupportedEffectStatementKind {
785 LocalDeclaration,
786 Branch,
787 Loop,
788 NestedBlock,
789 ExceptionHandling,
790 AsyncOperation,
791 CompoundAssignment,
792 CleanupReturnCandidate,
793 UnsupportedExpression,
794}
795
796#[derive(Debug, Clone, PartialEq, Eq)]
798pub struct EffectExpression {
799 pub kind: EffectExpressionKind,
800 pub span: SourceSpan,
801}
802
803#[derive(Debug, Clone, PartialEq, Eq)]
804pub enum EffectExpressionKind {
805 Literal(SerializableValue),
806 Identifier(String),
807 ThisMember(String),
808 MemberAccess {
809 object: Box<EffectExpression>,
810 property: String,
811 },
812 Arithmetic {
813 left: Box<EffectExpression>,
814 right: Box<EffectExpression>,
815 operator: ArithmeticOperator,
816 },
817 Comparison {
818 left: Box<EffectExpression>,
819 right: Box<EffectExpression>,
820 operator: ComparisonOperator,
821 },
822 Logical {
823 left: Box<EffectExpression>,
824 right: Box<EffectExpression>,
825 operator: LogicalOperator,
826 },
827 NullishCoalescing {
828 left: Box<EffectExpression>,
829 right: Box<EffectExpression>,
830 },
831 Unary {
832 operand: Box<EffectExpression>,
833 operator: UnaryOperator,
834 },
835}
836
837#[derive(Debug, Clone, PartialEq, Eq)]
838pub enum ConstantExpressionKind {
839 Literal(SerializableValue),
840 Boolean(bool),
841 Arithmetic(ArithmeticExpression),
842 Comparison {
843 operator: ComparisonOperator,
844 left: ArithmeticExpression,
845 right: ArithmeticExpression,
846 },
847 Logical {
848 operator: LogicalOperator,
849 left: Box<ConstantExpression>,
850 right: Box<ConstantExpression>,
851 },
852 NullishCoalescing {
853 left: Box<ConstantExpression>,
854 right: Box<ConstantExpression>,
855 },
856 Unary {
857 operator: UnaryOperator,
858 operand: Box<ConstantExpression>,
859 },
860}
861
862#[derive(Debug, Clone, Copy, PartialEq, Eq)]
863pub enum ComparisonOperator {
864 Equal,
865 NotEqual,
866 LessThan,
867 LessThanOrEqual,
868 GreaterThan,
869 GreaterThanOrEqual,
870}
871
872#[derive(Debug, Clone, Copy, PartialEq, Eq)]
873pub enum LogicalOperator {
874 And,
875 Or,
876}
877#[derive(Debug, Clone, Copy, PartialEq, Eq)]
878pub enum UnaryOperator {
879 Not,
880 Plus,
881 Minus,
882}
883
884#[derive(Debug, Clone, PartialEq, Eq)]
885pub enum ConstantEvaluationError {
886 Arithmetic(ArithmeticEvaluationError),
887}
888
889impl ArithmeticExpression {
890 pub fn evaluate(&self) -> Result<SerializableValue, ArithmeticEvaluationError> {
897 self.evaluate_number()
898 .map(|value| SerializableValue::Number(value.to_string()))
899 }
900
901 fn evaluate_number(&self) -> Result<f64, ArithmeticEvaluationError> {
902 let value = match &self.kind {
903 ArithmeticExpressionKind::Number(value) => value
904 .parse::<f64>()
905 .map_err(|_| ArithmeticEvaluationError::InvalidNumber(value.clone()))?,
906 ArithmeticExpressionKind::Binary {
907 operator,
908 left,
909 right,
910 } => {
911 let left = left.evaluate_number()?;
912 let right = right.evaluate_number()?;
913 match operator {
914 ArithmeticOperator::Add => left + right,
915 ArithmeticOperator::Subtract => left - right,
916 ArithmeticOperator::Multiply => left * right,
917 ArithmeticOperator::Divide => {
918 if right == 0.0 {
919 return Err(ArithmeticEvaluationError::DivisionByZero);
920 }
921 left / right
922 }
923 ArithmeticOperator::Remainder => {
924 if right == 0.0 {
925 return Err(ArithmeticEvaluationError::DivisionByZero);
926 }
927 left % right
928 }
929 }
930 }
931 };
932
933 value
934 .is_finite()
935 .then_some(value)
936 .ok_or(ArithmeticEvaluationError::NonFiniteResult)
937 }
938}
939
940impl ConstantExpression {
941 pub fn evaluate(&self) -> Result<SerializableValue, ConstantEvaluationError> {
947 match &self.kind {
948 ConstantExpressionKind::Literal(value) => Ok(value.clone()),
949 ConstantExpressionKind::Boolean(value) => Ok(SerializableValue::Boolean(*value)),
950 ConstantExpressionKind::Arithmetic(expression) => expression
951 .evaluate()
952 .map_err(ConstantEvaluationError::Arithmetic),
953 ConstantExpressionKind::Comparison {
954 operator,
955 left,
956 right,
957 } => {
958 let left = left
959 .evaluate_number()
960 .map_err(ConstantEvaluationError::Arithmetic)?;
961 let right = right
962 .evaluate_number()
963 .map_err(ConstantEvaluationError::Arithmetic)?;
964 Ok(SerializableValue::Boolean(match operator {
965 ComparisonOperator::Equal => numbers_are_equal(left, right),
966 ComparisonOperator::NotEqual => !numbers_are_equal(left, right),
967 ComparisonOperator::LessThan => left < right,
968 ComparisonOperator::LessThanOrEqual => left <= right,
969 ComparisonOperator::GreaterThan => left > right,
970 ComparisonOperator::GreaterThanOrEqual => left >= right,
971 }))
972 }
973 ConstantExpressionKind::Logical {
974 operator,
975 left,
976 right,
977 } => {
978 let left = left.evaluate_boolean()?;
979 let value = match (operator, left) {
980 (LogicalOperator::And, false) => false,
981 (LogicalOperator::Or, true) => true,
982 (LogicalOperator::And | LogicalOperator::Or, _) => right.evaluate_boolean()?,
983 };
984 Ok(SerializableValue::Boolean(value))
985 }
986 ConstantExpressionKind::NullishCoalescing { left, right } => {
987 let value = left.evaluate()?;
988 if matches!(value, SerializableValue::Null) {
989 right.evaluate()
990 } else {
991 Ok(value)
992 }
993 }
994 ConstantExpressionKind::Unary { operator, operand } => match operator {
995 UnaryOperator::Not => Ok(SerializableValue::Boolean(!operand.evaluate_boolean()?)),
996 UnaryOperator::Plus | UnaryOperator::Minus => {
997 let SerializableValue::Number(value) = operand.evaluate()? else {
998 unreachable!("parser requires numeric unary operands");
999 };
1000 let value = value.parse::<f64>().map_err(|_| {
1001 ConstantEvaluationError::Arithmetic(
1002 ArithmeticEvaluationError::InvalidNumber(value),
1003 )
1004 })?;
1005 let value = if matches!(operator, UnaryOperator::Minus) {
1006 -value
1007 } else {
1008 value
1009 };
1010 value
1011 .is_finite()
1012 .then_some(SerializableValue::Number(value.to_string()))
1013 .ok_or(ConstantEvaluationError::Arithmetic(
1014 ArithmeticEvaluationError::NonFiniteResult,
1015 ))
1016 }
1017 },
1018 }
1019 }
1020
1021 fn evaluate_boolean(&self) -> Result<bool, ConstantEvaluationError> {
1022 let SerializableValue::Boolean(value) = self.evaluate()? else {
1023 unreachable!("parser only permits boolean constants as logical operands");
1024 };
1025 Ok(value)
1026 }
1027}
1028
1029fn numbers_are_equal(left: f64, right: f64) -> bool {
1030 #[allow(clippy::float_cmp)]
1034 {
1035 left == right
1036 }
1037}
1038
1039impl fmt::Display for ArithmeticExpression {
1040 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1041 match &self.kind {
1042 ArithmeticExpressionKind::Number(value) => value.fmt(formatter),
1043 ArithmeticExpressionKind::Binary {
1044 operator,
1045 left,
1046 right,
1047 } => write!(formatter, "({left} {operator} {right})"),
1048 }
1049 }
1050}
1051
1052impl fmt::Display for ConstantExpression {
1053 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1054 match &self.kind {
1055 ConstantExpressionKind::Literal(value) => format_constant_literal(value, formatter),
1056 ConstantExpressionKind::Boolean(value) => value.fmt(formatter),
1057 ConstantExpressionKind::Arithmetic(expression) => expression.fmt(formatter),
1058 ConstantExpressionKind::Comparison {
1059 operator,
1060 left,
1061 right,
1062 } => write!(formatter, "({left} {operator} {right})"),
1063 ConstantExpressionKind::Logical {
1064 operator,
1065 left,
1066 right,
1067 } => write!(formatter, "({left} {operator} {right})"),
1068 ConstantExpressionKind::NullishCoalescing { left, right } => {
1069 write!(formatter, "({left} ?? {right})")
1070 }
1071 ConstantExpressionKind::Unary { operator, operand } => {
1072 write!(formatter, "({operator}{operand})")
1073 }
1074 }
1075 }
1076}
1077
1078fn format_constant_literal(
1079 value: &SerializableValue,
1080 formatter: &mut fmt::Formatter<'_>,
1081) -> fmt::Result {
1082 match value {
1083 SerializableValue::Null => formatter.write_str("null"),
1084 SerializableValue::Number(value) => formatter.write_str(value),
1085 SerializableValue::String(value) => write!(formatter, "{value:?}"),
1086 SerializableValue::Boolean(value) => write!(formatter, "{value}"),
1087 SerializableValue::Array(_) | SerializableValue::Object(_) => {
1088 unreachable!("nullish constant literals are primitive values")
1089 }
1090 }
1091}
1092
1093impl fmt::Display for ArithmeticOperator {
1094 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1095 let operator = match self {
1096 Self::Add => "+",
1097 Self::Subtract => "-",
1098 Self::Multiply => "*",
1099 Self::Divide => "/",
1100 Self::Remainder => "%",
1101 };
1102 formatter.write_str(operator)
1103 }
1104}
1105
1106impl fmt::Display for ComparisonOperator {
1107 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1108 let operator = match self {
1109 Self::Equal => "===",
1110 Self::NotEqual => "!==",
1111 Self::LessThan => "<",
1112 Self::LessThanOrEqual => "<=",
1113 Self::GreaterThan => ">",
1114 Self::GreaterThanOrEqual => ">=",
1115 };
1116 formatter.write_str(operator)
1117 }
1118}
1119
1120impl fmt::Display for LogicalOperator {
1121 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1122 formatter.write_str(match self {
1123 Self::And => "&&",
1124 Self::Or => "||",
1125 })
1126 }
1127}
1128impl fmt::Display for UnaryOperator {
1129 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1130 f.write_str(match self {
1131 Self::Not => "!",
1132 Self::Plus => "+",
1133 Self::Minus => "-",
1134 })
1135 }
1136}
1137
1138impl fmt::Display for ArithmeticEvaluationError {
1139 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1140 match self {
1141 Self::InvalidNumber(value) => {
1142 write!(formatter, "unsupported numeric literal `{value}`")
1143 }
1144 Self::DivisionByZero => formatter.write_str("division or remainder by zero"),
1145 Self::NonFiniteResult => formatter.write_str("non-finite result"),
1146 }
1147 }
1148}
1149
1150impl fmt::Display for ConstantEvaluationError {
1151 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1152 match self {
1153 Self::Arithmetic(error) => error.fmt(formatter),
1154 }
1155 }
1156}
1157
1158#[derive(Debug, Clone, PartialEq, Eq)]
1160pub struct DeclaredStateType {
1161 pub text: String,
1162 pub provenance: SourceProvenance,
1163 pub kind: Option<DeclaredStateTypeKind>,
1164}
1165
1166#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1168pub enum DeclaredStateTypeKind {
1169 String,
1170 Number,
1171 Boolean,
1172 Null,
1173}
1174
1175#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1176#[serde(untagged)]
1177pub enum SerializableValue {
1178 Null,
1179 Number(String),
1180 String(String),
1181 Boolean(bool),
1182 Array(Vec<SerializableValue>),
1183 Object(BTreeMap<String, SerializableValue>),
1184}
1185
1186impl SerializableValue {
1187 #[must_use]
1188 pub fn render_text(&self) -> String {
1189 match self {
1190 Self::Number(value) | Self::String(value) => value.clone(),
1191 Self::Boolean(value) => value.to_string(),
1192 Self::Null | Self::Array(_) | Self::Object(_) => String::new(),
1193 }
1194 }
1195
1196 #[must_use]
1197 pub fn member_path_value(&self, path: &str) -> Option<&Self> {
1198 if path.is_empty() {
1199 return None;
1200 }
1201
1202 path.split('.').try_fold(self, |value, member| match value {
1203 Self::Object(values) if !member.is_empty() => values.get(member),
1204 _ => None,
1205 })
1206 }
1207}
1208
1209#[derive(Debug, Clone, PartialEq, Eq)]
1210pub struct ComponentMethod {
1211 pub id: SemanticId,
1212 pub owner: SemanticOwner,
1213 pub name: String,
1214 pub is_getter: bool,
1215 pub is_async: bool,
1216 pub is_static: bool,
1217 pub decorators: Vec<String>,
1218 pub semantic_role: MethodSemanticRole,
1219 pub local_variables: Vec<MethodLocalVariable>,
1220 pub parameters: Vec<MethodParameter>,
1221 pub declared_return_type: Option<DeclaredStateType>,
1222 pub return_values: Vec<SerializableValue>,
1223 pub computed_expression: Option<ComputedExpression>,
1224 pub effect_body: Option<EffectBodySyntax>,
1225 pub calls: Vec<MethodCall>,
1226}
1227
1228#[derive(Debug, Clone, PartialEq, Eq)]
1231pub struct ComponentEffectField {
1232 pub owner: SemanticOwner,
1233 pub name: String,
1234 pub declaration_order: u32,
1237 pub is_async: bool,
1238 pub body: EffectBodySyntax,
1239 pub provenance: SourceProvenance,
1240}
1241
1242#[derive(Debug, Clone, PartialEq, Eq)]
1244pub struct MethodCall {
1245 pub callee: String,
1246 pub span: SourceSpan,
1247}
1248
1249#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1251pub enum MethodSemanticRole {
1252 Standard,
1253 Computed,
1254 Effect,
1255 Action,
1256}
1257
1258impl ComponentMethod {
1259 #[must_use]
1260 pub const fn is_computed(&self) -> bool {
1261 matches!(self.semantic_role, MethodSemanticRole::Computed)
1262 }
1263
1264 #[must_use]
1265 pub const fn is_action(&self) -> bool {
1266 matches!(self.semantic_role, MethodSemanticRole::Action)
1267 }
1268
1269 #[must_use]
1270 pub const fn is_effect(&self) -> bool {
1271 matches!(self.semantic_role, MethodSemanticRole::Effect)
1272 }
1273}
1274
1275#[derive(Debug, Clone, PartialEq, Eq)]
1277pub struct MethodParameter {
1278 pub id: SemanticId,
1279 pub owner: SemanticOwner,
1280 pub name: String,
1281 pub span: SourceSpan,
1282 pub declared_type: Option<DeclaredStateType>,
1283}
1284
1285#[derive(Debug, Clone, PartialEq, Eq)]
1286pub struct MethodLocalVariable {
1287 pub id: SemanticId,
1288 pub owner: SemanticOwner,
1289 pub name: String,
1290 pub value: SerializableValue,
1291 pub span: SourceSpan,
1292}
1293
1294#[derive(Debug, Clone, PartialEq, Eq)]
1295pub struct ComponentAction {
1296 pub id: SemanticId,
1297 pub owner: SemanticOwner,
1298 pub method: String,
1299 pub operation: StateOperation,
1300 pub field: String,
1301}
1302
1303#[derive(Debug, Clone, PartialEq, Eq)]
1305pub struct ActionEndpoint {
1306 pub id: SemanticId,
1307 pub owner: SemanticOwner,
1308 pub name: String,
1309}
1310
1311#[derive(Debug, Clone, PartialEq, Eq)]
1312pub enum StateOperation {
1313 Increment,
1314 Decrement,
1315 AddAssign(SerializableValue),
1316 SubtractAssign(SerializableValue),
1317 Assign(SerializableValue),
1318 AssignParameter(String),
1319 Toggle,
1320}
1321
1322#[derive(Debug, Clone, PartialEq, Eq)]
1323pub enum RenderChild {
1324 Text {
1325 value: String,
1326 span: SourceSpan,
1327 },
1328 Binding {
1329 expression: String,
1330 span: SourceSpan,
1331 },
1332 Element(RenderElement),
1333 Fragment(RenderFragment),
1334 Conditional(RenderConditional),
1335 List(RenderList),
1336}
1337
1338#[derive(Debug, Clone, PartialEq, Eq)]
1339pub struct RenderElement {
1340 pub tag_name: String,
1341 pub tag_name_span: SourceSpan,
1342 pub span: SourceSpan,
1343 pub attributes: Vec<RenderAttribute>,
1344 pub event_handlers: Vec<RenderEventHandler>,
1345 pub children: Vec<RenderChild>,
1346}
1347
1348#[derive(Debug, Clone, PartialEq, Eq)]
1349pub struct RenderFragment {
1350 pub span: SourceSpan,
1351 pub children: Vec<RenderChild>,
1352}
1353
1354#[derive(Debug, Clone, PartialEq, Eq)]
1355pub struct RenderConditional {
1356 pub condition: String,
1357 pub span: SourceSpan,
1358 pub when_true: Vec<RenderChild>,
1359 pub when_false: Vec<RenderChild>,
1360}
1361
1362#[derive(Debug, Clone, PartialEq, Eq)]
1363pub struct RenderList {
1364 pub iterable: String,
1365 pub item_variable: String,
1366 pub index_variable: Option<String>,
1367 pub key_expression: String,
1368 pub span: SourceSpan,
1369 pub item_template: Vec<RenderChild>,
1370}
1371
1372#[derive(Debug, Clone, PartialEq, Eq)]
1373pub struct RenderAttribute {
1374 pub name: String,
1375 pub value: RenderAttributeValue,
1376 pub name_span: SourceSpan,
1377 pub value_span: Option<SourceSpan>,
1378 pub expression_span: Option<SourceSpan>,
1379 pub this_member: Option<presolve_parser::ParsedThisMemberDesignator>,
1380 pub constant_value: Option<SerializableValue>,
1381 pub span: SourceSpan,
1382}
1383
1384#[derive(Debug, Clone, PartialEq, Eq)]
1385pub enum RenderAttributeValue {
1386 Boolean,
1387 Static(String),
1388 Expression(Option<String>),
1389 Spread(Option<String>),
1390 Unsupported,
1391}
1392
1393#[derive(Debug, Clone, PartialEq, Eq)]
1394pub struct RenderEventHandler {
1395 pub id: SemanticId,
1396 pub owner: SemanticOwner,
1397 pub event: String,
1398 pub handler: String,
1399 pub arguments: Vec<SerializableValue>,
1400 pub span: SourceSpan,
1401}
1402
1403#[derive(Debug, Clone, PartialEq, Eq)]
1404pub struct RenderModel {
1405 pub root_element: Option<String>,
1406 pub root_element_name_span: Option<SourceSpan>,
1407 pub root_span: Option<SourceSpan>,
1408 pub root_fragment: Option<RenderFragment>,
1409 pub attributes: Vec<RenderAttribute>,
1410 pub bindings: Vec<String>,
1411 pub event_handlers: Vec<RenderEventHandler>,
1412 pub children: Vec<RenderChild>,
1413}
1414
1415#[derive(Debug, Clone, PartialEq, Eq)]
1416pub struct ComponentDiagnostic {
1417 pub code: String,
1418 pub severity: ComponentDiagnosticSeverity,
1419 pub message: String,
1420 pub provenance: Option<SourceProvenance>,
1421 pub effect_id: Option<EffectId>,
1422 pub statement_id: Option<EffectStatementId>,
1423 pub context_declaration_candidate_id: Option<ContextDeclarationCandidateId>,
1424 pub context_id: Option<ContextId>,
1425 pub provider_id: Option<ProviderId>,
1426 pub consumer_id: Option<ConsumerId>,
1427 pub slot_id: Option<SlotId>,
1428 pub invocation_id: Option<ComponentInvocationId>,
1429 pub component_instance_id: Option<ComponentInstanceId>,
1430 pub slot_binding_id: Option<SlotBindingId>,
1431 pub structural_region_id: Option<ComponentStructuralRegionId>,
1432 pub component_id: Option<SemanticId>,
1433 pub provider_instance_id: Option<ProviderInstanceId>,
1434 pub consumer_instance_id: Option<ConsumerInstanceId>,
1435 pub secondary_labels: Vec<DiagnosticSecondaryLabel>,
1436}
1437
1438impl ComponentDiagnostic {
1439 pub(crate) fn error(code: impl Into<String>, message: impl Into<String>) -> Self {
1440 Self {
1441 code: code.into(),
1442 severity: ComponentDiagnosticSeverity::Error,
1443 message: message.into(),
1444 provenance: None,
1445 effect_id: None,
1446 statement_id: None,
1447 context_declaration_candidate_id: None,
1448 context_id: None,
1449 provider_id: None,
1450 consumer_id: None,
1451 slot_id: None,
1452 invocation_id: None,
1453 component_instance_id: None,
1454 slot_binding_id: None,
1455 structural_region_id: None,
1456 component_id: None,
1457 provider_instance_id: None,
1458 consumer_instance_id: None,
1459 secondary_labels: Vec::new(),
1460 }
1461 }
1462}
1463
1464#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
1466pub enum ComponentDiagnosticSeverity {
1467 Error,
1468}
1469
1470impl ComponentDiagnosticSeverity {
1471 #[must_use]
1472 pub const fn as_str(self) -> &'static str {
1473 match self {
1474 Self::Error => "error",
1475 }
1476 }
1477}
1478
1479#[derive(Debug, Clone, PartialEq, Eq)]
1481pub struct DiagnosticSecondaryLabel {
1482 pub provenance: SourceProvenance,
1483 pub message: String,
1484}
1485
1486#[must_use]
1487pub fn build_component_graph(parsed: &ParsedFile) -> ComponentGraph {
1488 build_component_graph_with_identity(parsed, false)
1489}
1490
1491#[must_use]
1497pub fn build_component_graph_for_module(parsed: &ParsedFile) -> ComponentGraph {
1498 build_component_graph_with_identity(parsed, true)
1499}
1500
1501#[must_use]
1506pub fn build_v2_component_graph_for_module(
1507 parsed: &ParsedFile,
1508 authored: &crate::CanonicalAuthoredSemanticModelV1,
1509) -> ComponentGraph {
1510 let mut components = Vec::new();
1511 let mut diagnostics = Vec::new();
1512 let mut provenance = BTreeMap::new();
1513 let computed_sites = match crate::computed_getter_sites_v1(parsed, authored) {
1514 Ok(sites) => sites,
1515 Err(error) => {
1516 diagnostics.push(ComponentDiagnostic::error(
1517 "PSV2C1001",
1518 format!("canonical V2 computed candidate selection failed: {error}"),
1519 ));
1520 Vec::new()
1521 }
1522 };
1523 for declaration in authored.declarations.iter().filter(|declaration| {
1524 declaration.kind == crate::CanonicalAuthoredDeclarationKindV1::Component
1525 }) {
1526 let Some(class) = parsed
1527 .classes
1528 .iter()
1529 .find(|class| class.name == declaration.subject)
1530 else {
1531 diagnostics.push(ComponentDiagnostic::error(
1532 "PSV2A1001",
1533 format!(
1534 "canonical V2 component `{}` has no source class",
1535 declaration.subject
1536 ),
1537 ));
1538 continue;
1539 };
1540 let id = SemanticId::component_in_module(&parsed.path, Some(&class.name), &class.name);
1541 if class.methods.iter().all(|method| method.name != "render") {
1542 diagnostics.push(ComponentDiagnostic::error(
1543 "PSC1002",
1544 format!("class `{}` is missing render()", class.name),
1545 ));
1546 }
1547 provenance.insert(id.clone(), SourceProvenance::new(&parsed.path, class.span));
1548 let state_fields: Vec<StateField> = authored
1549 .declarations
1550 .iter()
1551 .filter(|candidate| {
1552 candidate.kind == crate::CanonicalAuthoredDeclarationKindV1::State
1553 && candidate.subject.starts_with(&format!("{}.", class.name))
1554 })
1555 .filter_map(|candidate| {
1556 let name = candidate
1557 .subject
1558 .strip_prefix(&format!("{}.", class.name))?;
1559 let property = class
1560 .properties
1561 .iter()
1562 .find(|property| property.name == name)?;
1563 provenance.insert(
1564 id.state_field(name),
1565 SourceProvenance::new(&parsed.path, property.span),
1566 );
1567 Some(StateField {
1568 id: id.state_field(name),
1569 owner: SemanticOwner::entity(id.clone()),
1570 name: name.into(),
1571 initial_value: property
1572 .state_initial_value
1573 .as_ref()
1574 .map(serializable_value_from_parsed),
1575 initial_expression: property
1576 .state_initial_expression
1577 .as_ref()
1578 .map(constant_expression_from_parsed),
1579 declared_type: property.state_type_annotation.as_ref().map(|annotation| {
1580 DeclaredStateType {
1581 text: annotation.text.clone(),
1582 provenance: SourceProvenance::new(&parsed.path, annotation.span),
1583 kind: declared_state_type_kind(&annotation.text),
1584 }
1585 }),
1586 })
1587 })
1588 .collect();
1589 let canonical_state_names = state_fields
1590 .iter()
1591 .map(|field| field.name.as_str())
1592 .collect::<BTreeSet<_>>();
1593 let mut slot_declarations = Vec::new();
1594 for candidate in authored.declarations.iter().filter(|candidate| {
1595 candidate.kind == crate::CanonicalAuthoredDeclarationKindV1::Slot
1596 && candidate.subject.starts_with(&format!("{}.", class.name))
1597 }) {
1598 let Some(name) = candidate.subject.strip_prefix(&format!("{}.", class.name)) else {
1599 continue;
1600 };
1601 let Some(property) = class
1602 .properties
1603 .iter()
1604 .find(|property| property.name == name)
1605 else {
1606 diagnostics.push(ComponentDiagnostic::error(
1607 "PSV2S1001",
1608 format!(
1609 "canonical V2 Slot `{}` has no source field",
1610 candidate.subject
1611 ),
1612 ));
1613 continue;
1614 };
1615 let Some(annotation) = property
1616 .type_annotation
1617 .as_ref()
1618 .filter(|annotation| annotation.text == "SlotContent")
1619 else {
1620 diagnostics.push(ComponentDiagnostic::error(
1621 "PSV2S1002",
1622 format!(
1623 "canonical V2 Slot `{}` requires the exact SlotContent type",
1624 candidate.subject
1625 ),
1626 ));
1627 continue;
1628 };
1629 let declared_type = DeclaredStateType {
1630 kind: declared_state_type_kind(&annotation.text),
1631 text: annotation.text.clone(),
1632 provenance: SourceProvenance::new(&parsed.path, annotation.span),
1633 };
1634 let slot_provenance = SourceProvenance::new(&parsed.path, property.span);
1635 provenance.insert(id.slot_field(name), slot_provenance.clone());
1636 slot_declarations.push(SlotDeclaration {
1637 authored_field: id.slot_field(name),
1638 name: name.to_owned(),
1639 kind: if name == "children" {
1640 SlotKind::Default
1641 } else {
1642 SlotKind::Named
1643 },
1644 declared_type,
1645 decorator_provenance: slot_provenance.clone(),
1646 name_provenance: SourceProvenance::new(&parsed.path, property.name_span),
1647 provenance: slot_provenance,
1648 });
1649 }
1650 let mut action_endpoints = Vec::new();
1651 let mut actions = Vec::new();
1652 let mut terminal_package_invocations = Vec::new();
1653 let mut v2_action_parameter_kinds = BTreeMap::new();
1654 for candidate in authored.declarations.iter().filter(|candidate| {
1655 candidate.kind == crate::CanonicalAuthoredDeclarationKindV1::Action
1656 && candidate.subject.starts_with(&format!("{}.", class.name))
1657 }) {
1658 let Some(name) = candidate.subject.strip_prefix(&format!("{}.", class.name)) else {
1659 continue;
1660 };
1661 let Some(property) = class
1662 .properties
1663 .iter()
1664 .find(|property| property.name == name)
1665 else {
1666 diagnostics.push(ComponentDiagnostic::error(
1667 "PSV2A1002",
1668 format!(
1669 "canonical V2 Action `{}` has no source field",
1670 candidate.subject
1671 ),
1672 ));
1673 continue;
1674 };
1675 let Some(handler) = property
1676 .initializer_call
1677 .as_ref()
1678 .and_then(|call| call.inline_handler.as_ref())
1679 else {
1680 diagnostics.push(ComponentDiagnostic::error(
1681 "PSV2A1003",
1682 format!(
1683 "canonical V2 Action `{}` requires one inline handler",
1684 candidate.subject
1685 ),
1686 ));
1687 continue;
1688 };
1689 let terminal_evidence = match candidate.derived_evidence.as_ref() {
1690 Some(crate::DerivedAuthoredEvidenceV2::TerminalPackageInvocation {
1691 module_specifier,
1692 export_name,
1693 declaration_modules,
1694 }) => Some((module_specifier, export_name, declaration_modules)),
1695 _ => None,
1696 };
1697 let imported_package_call = handler.direct_call.as_ref().and_then(|call| {
1698 parsed
1699 .imports
1700 .iter()
1701 .filter(|import| import.source != "presolve")
1702 .flat_map(|import| {
1703 import
1704 .specifiers
1705 .iter()
1706 .map(move |specifier| (import, specifier))
1707 })
1708 .find(|(_, specifier)| specifier.local == call.callee)
1709 });
1710 if terminal_evidence.is_none() {
1711 if let Some((import, specifier)) = imported_package_call {
1712 diagnostics.push(ComponentDiagnostic::error(
1713 "PSV2P1001",
1714 format!(
1715 "package call `{}()` in canonical V2 Action `{}` was not admitted: the beta supports only a TypeScript-resolved named import, invoked synchronously with zero arguments as the action's sole result-discarded statement; `{}` from `{}` remains ordinary Vite input outside that closed shape",
1716 handler
1717 .direct_call
1718 .as_ref()
1719 .expect("imported call")
1720 .callee,
1721 candidate.subject,
1722 specifier.imported,
1723 import.source
1724 ),
1725 ));
1726 continue;
1727 }
1728 }
1729 let terminal_call_admitted = terminal_evidence.is_some()
1730 && handler
1731 .direct_call
1732 .as_ref()
1733 .is_some_and(|call| !call.awaited && call.arguments.is_empty())
1734 && handler.parameters.is_empty()
1735 && handler.local_variables.is_empty()
1736 && handler.state_updates.is_empty()
1737 && handler.unsupported_statement_spans.len() == 1;
1738 if handler.is_expression_body
1739 || handler.is_async
1740 || (!handler.unsupported_statement_spans.is_empty() && !terminal_call_admitted)
1741 || handler
1742 .state_updates
1743 .iter()
1744 .any(|update| !canonical_state_names.contains(update.field.as_str()))
1745 {
1746 diagnostics.push(ComponentDiagnostic::error(
1747 "PSV2A1004",
1748 format!(
1749 "canonical V2 Action `{}` has unsupported handler or State ownership evidence",
1750 candidate.subject
1751 ),
1752 ));
1753 continue;
1754 }
1755 let Some(operands) = v2_action_operands(handler, class) else {
1756 diagnostics.push(ComponentDiagnostic::error(
1757 "PSV2A1004",
1758 format!(
1759 "canonical V2 Action `{}` has unsupported handler parameter or State ownership evidence",
1760 candidate.subject
1761 ),
1762 ));
1763 continue;
1764 };
1765 let endpoint_id = id.action_endpoint(name);
1766 let authority = crate::build_action_authority_v1(&[crate::ActionFactV1 {
1767 id: endpoint_id.to_string(),
1768 component: id.to_string(),
1769 asynchronous: handler.is_async,
1770 accepts_abort_signal: false,
1771 capture_coverage: crate::ActionCaptureCoverageV1::Complete,
1772 environment: crate::ActionEnvironmentV1::Browser,
1773 }]);
1774 if authority
1775 .actions
1776 .first()
1777 .is_none_or(|record| record.admission != crate::ActionAdmissionV1::Admissible)
1778 {
1779 diagnostics.push(ComponentDiagnostic::error(
1780 "PSV2A1005",
1781 format!(
1782 "canonical V2 Action `{}` was not admitted",
1783 candidate.subject
1784 ),
1785 ));
1786 continue;
1787 }
1788 provenance.insert(
1789 endpoint_id.clone(),
1790 SourceProvenance::new(&parsed.path, property.span),
1791 );
1792 action_endpoints.push(ActionEndpoint {
1793 id: endpoint_id.clone(),
1794 owner: SemanticOwner::entity(id.clone()),
1795 name: name.to_owned(),
1796 });
1797 if let Some((module_specifier, export_name, declaration_modules)) = terminal_evidence {
1798 terminal_package_invocations.push(AuthoredTerminalPackageInvocationFact {
1799 id: id.package_invocation(name),
1800 owner_component: id.clone(),
1801 method: endpoint_id.clone(),
1802 method_name: name.to_owned(),
1803 module_specifier: module_specifier.clone(),
1804 export_name: export_name.clone(),
1805 declaration_modules: declaration_modules.clone(),
1806 provenance: SourceProvenance::new(
1807 &parsed.path,
1808 handler
1809 .direct_call
1810 .as_ref()
1811 .expect("validated terminal call")
1812 .span,
1813 ),
1814 });
1815 }
1816 v2_action_parameter_kinds.insert(
1817 name.to_owned(),
1818 handler
1819 .parameters
1820 .iter()
1821 .map(|parameter| {
1822 declared_state_type_kind(
1823 ¶meter
1824 .type_annotation
1825 .as_ref()
1826 .expect("validated V2 action parameter should be typed")
1827 .text,
1828 )
1829 .expect("validated V2 action parameter should be primitive")
1830 })
1831 .collect::<Vec<_>>(),
1832 );
1833 actions.extend(
1834 handler
1835 .state_updates
1836 .iter()
1837 .enumerate()
1838 .map(|(index, update)| {
1839 let action_id = id.action(name, index);
1840 provenance.insert(
1841 action_id.clone(),
1842 SourceProvenance::new(&parsed.path, update.span),
1843 );
1844 ComponentAction {
1845 id: action_id,
1846 owner: SemanticOwner::entity(endpoint_id.clone()),
1847 method: name.to_owned(),
1848 operation: state_operation_from_v2_parsed(&update.operation, &operands),
1849 field: update.field.clone(),
1850 }
1851 }),
1852 );
1853 }
1854 let mut effect_fields = Vec::new();
1855 for candidate in authored.declarations.iter().filter(|candidate| {
1856 candidate.kind == crate::CanonicalAuthoredDeclarationKindV1::Effect
1857 && candidate.subject.starts_with(&format!("{}.", class.name))
1858 }) {
1859 let Some(name) = candidate.subject.strip_prefix(&format!("{}.", class.name)) else {
1860 continue;
1861 };
1862 let Some(property) = class
1863 .properties
1864 .iter()
1865 .find(|property| property.name == name)
1866 else {
1867 diagnostics.push(ComponentDiagnostic::error(
1868 "PSV2E1001",
1869 format!(
1870 "canonical V2 Effect `{}` has no source field",
1871 candidate.subject
1872 ),
1873 ));
1874 continue;
1875 };
1876 let declaration_order = u32::try_from(
1877 class
1878 .properties
1879 .iter()
1880 .position(|candidate| candidate.span == property.span)
1881 .expect("matched effect field should retain source position"),
1882 )
1883 .expect("component field position should fit u32");
1884 let Some(handler) = property
1885 .initializer_call
1886 .as_ref()
1887 .and_then(|call| call.inline_handler.as_ref())
1888 else {
1889 diagnostics.push(ComponentDiagnostic::error(
1890 "PSV2E1002",
1891 format!(
1892 "canonical V2 Effect `{}` requires one inline handler",
1893 candidate.subject
1894 ),
1895 ));
1896 continue;
1897 };
1898 let Some(body) = handler.effect_body.as_ref() else {
1899 diagnostics.push(ComponentDiagnostic::error(
1900 "PSV2E1003",
1901 format!(
1902 "canonical V2 Effect `{}` requires a block-bodied handler",
1903 candidate.subject
1904 ),
1905 ));
1906 continue;
1907 };
1908 provenance.insert(
1909 id.effect(name),
1910 SourceProvenance::new(&parsed.path, property.span),
1911 );
1912 effect_fields.push(ComponentEffectField {
1913 owner: SemanticOwner::entity(id.clone()),
1914 name: name.to_owned(),
1915 declaration_order,
1916 is_async: handler.is_async,
1917 body: effect_body_from_parsed(body),
1918 provenance: SourceProvenance::new(&parsed.path, property.span),
1919 });
1920 }
1921 let mut methods = Vec::new();
1922 for candidate in authored.declarations.iter().filter(|candidate| {
1923 candidate.kind == crate::CanonicalAuthoredDeclarationKindV1::Computed
1924 && candidate.subject.starts_with(&format!("{}.", class.name))
1925 }) {
1926 let Some(crate::DerivedAuthoredEvidenceV2::ComputedGetter {
1927 state_dependencies,
1928 computed_dependencies,
1929 }) = candidate.derived_evidence.as_ref()
1930 else {
1931 continue;
1932 };
1933 let Some(name) = candidate.subject.strip_prefix(&format!("{}.", class.name)) else {
1934 continue;
1935 };
1936 let Some(site) = computed_sites.iter().find(|site| {
1937 site.subject == candidate.subject
1938 && site.declaration_source == candidate.source
1939 && site.state_dependencies == *state_dependencies
1940 && site.computed_dependencies == *computed_dependencies
1941 }) else {
1942 diagnostics.push(ComponentDiagnostic::error(
1943 "PSV2C1002",
1944 format!(
1945 "canonical V2 Computed `{}` lacks matching derived getter evidence",
1946 candidate.subject
1947 ),
1948 ));
1949 continue;
1950 };
1951 let Some(method) = class.methods.iter().find(|method| {
1952 method.name == name
1953 && range_from_span(method.span) == site.declaration_source
1954 && method.is_getter
1955 }) else {
1956 diagnostics.push(ComponentDiagnostic::error(
1957 "PSV2C1003",
1958 format!(
1959 "canonical V2 Computed `{}` has no matching source getter",
1960 candidate.subject
1961 ),
1962 ));
1963 continue;
1964 };
1965 let mut computed = component_method_from_parsed(method, &parsed.path, &id);
1966 computed.semantic_role = MethodSemanticRole::Computed;
1967 computed.computed_expression = method
1968 .computed_expression
1969 .as_ref()
1970 .map(computed_expression_from_parsed);
1971 provenance.insert(
1972 computed.id.clone(),
1973 SourceProvenance::new(&parsed.path, method.span),
1974 );
1975 methods.push(computed);
1976 }
1977 let render = class
1978 .methods
1979 .iter()
1980 .find(|method| method.name == "render")
1981 .map(|method| render_model_from_parsed_method(method, &id));
1982 if let Some(render) = &render {
1983 collect_v2_action_parameter_event_diagnostics(
1984 class,
1985 render,
1986 &v2_action_parameter_kinds,
1987 &mut diagnostics,
1988 );
1989 }
1990 let mut form_declaration_candidates = Vec::new();
1991 let mut form_field_declaration_candidates = Vec::new();
1992 let mut validation_rule_declaration_facts = Vec::new();
1993 let mut serialization_declaration_facts = Vec::new();
1994 let mut submission_declaration_facts = Vec::new();
1995 for candidate in authored.declarations.iter().filter(|candidate| {
1996 candidate.kind == crate::CanonicalAuthoredDeclarationKindV1::Form
1997 && candidate.subject.starts_with(&format!("{}.", class.name))
1998 }) {
1999 let Some(name) = candidate.subject.strip_prefix(&format!("{}.", class.name)) else {
2000 continue;
2001 };
2002 let Some(property) = class
2003 .properties
2004 .iter()
2005 .find(|property| property.name == name)
2006 else {
2007 diagnostics.push(ComponentDiagnostic::error(
2008 "PSV2F1001",
2009 format!(
2010 "canonical V2 Form `{}` has no source field",
2011 candidate.subject
2012 ),
2013 ));
2014 continue;
2015 };
2016 let Some(call) = property.initializer_call.as_ref() else {
2017 diagnostics.push(ComponentDiagnostic::error(
2018 "PSV2F1002",
2019 format!(
2020 "canonical V2 Form `{}` requires defineForm({{...}})",
2021 candidate.subject
2022 ),
2023 ));
2024 continue;
2025 };
2026 if call.argument_count != 1 {
2027 diagnostics.push(ComponentDiagnostic::error(
2028 "PSV2F1003",
2029 format!(
2030 "canonical V2 Form `{}` requires exactly one static definition",
2031 candidate.subject
2032 ),
2033 ));
2034 continue;
2035 }
2036 let form_id = FormId::for_owner(&id, name);
2037 let authored_field = id.form_field(name);
2038 let form_provenance = SourceProvenance::new(&parsed.path, property.span);
2039 provenance.insert(form_id.as_semantic_id().clone(), form_provenance.clone());
2040 provenance.insert(authored_field.clone(), form_provenance.clone());
2041 form_declaration_candidates.push(FormDeclarationCandidate {
2042 id: FormDeclarationCandidateId::for_source_position(
2043 &parsed.path,
2044 property.span.start,
2045 ),
2046 owner_component: Some(id.clone()),
2047 form_id: Some(form_id),
2048 authored_field: Some(authored_field),
2049 authored_name: Some(name.to_owned()),
2050 declaration_kind: AuthoredDeclarationKind::InstanceField,
2051 decorator_invoked: false,
2052 decorator_argument_count: 0,
2053 decorator_argument_provenance: Vec::new(),
2054 declaration_only: false,
2055 declared_type: None,
2056 conflicting_decorators: Vec::new(),
2057 decorator_provenance: SourceProvenance::new(&parsed.path, call.callee_span),
2058 name_provenance: Some(SourceProvenance::new(&parsed.path, property.name_span)),
2059 initializer_provenance: property
2060 .initializer_span
2061 .map(|span| SourceProvenance::new(&parsed.path, span)),
2062 provenance: form_provenance,
2063 status: FormDeclarationStatus::Valid,
2064 });
2065 let Some(shape) = property.form_definition_shape.as_ref() else {
2066 diagnostics.push(ComponentDiagnostic::error(
2067 "PSV2F1004",
2068 format!(
2069 "canonical V2 Form `{}` requires a static object definition",
2070 candidate.subject
2071 ),
2072 ));
2073 continue;
2074 };
2075 if !shape.unknown_options.is_empty() || !shape.unsupported_fields.is_empty() {
2076 diagnostics.push(ComponentDiagnostic::error(
2077 "PSV2F1005",
2078 format!(
2079 "canonical V2 Form `{}` contains unsupported dynamic structure or options",
2080 candidate.subject
2081 ),
2082 ));
2083 }
2084 if let Some(format) = shape.serialization.as_ref() {
2085 serialization_declaration_facts.push(AuthoredSerializationDeclarationFact {
2086 owner_component: Some(id.clone()),
2087 declaration_field: Some(id.form_field(name)),
2088 authored_name: Some(name.to_owned()),
2089 invoked: true,
2090 argument_count: 1,
2091 format: Some(format.clone()),
2092 provenance: SourceProvenance::new(&parsed.path, property.span),
2093 decorator_provenance: SourceProvenance::new(
2094 &parsed.path,
2095 shape.serialization_span.unwrap_or(shape.definition_span),
2096 ),
2097 });
2098 }
2099 let canonical_field_prefix = format!("{}.", candidate.subject);
2100 for field in &shape.fields {
2101 let field_subject = format!("{}{}", canonical_field_prefix, field.path.join("."));
2102 let Some(field_declaration) = authored.declarations.iter().find(|declaration| {
2103 declaration.kind == crate::CanonicalAuthoredDeclarationKindV1::FormField
2104 && declaration.subject == field_subject
2105 }) else {
2106 continue;
2107 };
2108 let field_name = field.path.join(".");
2109 let declaration_field = id.form_field(&format!("{name}.{field_name}"));
2110 let mut violations = Vec::new();
2111 if field.argument_count != 1 {
2112 violations.push(FormFieldDeclarationViolation::InvalidDecoratorArity {
2113 actual: field.argument_count,
2114 expected: 1,
2115 });
2116 }
2117 if field.initial_value.is_none() {
2118 violations.push(FormFieldDeclarationViolation::MissingInitializer);
2119 }
2120 if !field.unknown_options.is_empty() {
2121 violations.push(FormFieldDeclarationViolation::UnsupportedInitializer);
2122 }
2123 canonicalize_form_field_violations(&mut violations);
2124 form_field_declaration_candidates.push(FormFieldDeclarationCandidate {
2125 id: FormFieldDeclarationCandidateId::for_source_position(
2126 &parsed.path,
2127 field.declaration_span.start,
2128 ),
2129 owner_component: Some(id.clone()),
2130 declaration_field: Some(declaration_field.clone()),
2131 authored_name: Some(field_name),
2132 field_id: None,
2133 decorator_invoked: true,
2134 decorator_argument_count: 1,
2135 decorator_argument_provenance: vec![SourceProvenance::new(
2136 &parsed.path,
2137 field.call_span,
2138 )],
2139 nested_path_segments: Some(field.path.clone()),
2140 form_designator: Some(FormDesignatorFact {
2141 authored_name: name.to_owned(),
2142 provenance: SourceProvenance::new(&parsed.path, property.span),
2143 name_provenance: SourceProvenance::new(&parsed.path, property.name_span),
2144 }),
2145 unsupported_form_designator: None,
2146 resolved_form: None,
2147 declaration_kind: AuthoredDeclarationKind::InstanceField,
2148 is_static: false,
2149 declared_type: None,
2150 authority_type: matches!(
2151 field_declaration.derived_evidence,
2152 Some(crate::DerivedAuthoredEvidenceV2::FormFieldFileArray)
2153 )
2154 .then(|| crate::SemanticType::Array(Box::new(crate::SemanticType::File))),
2155 semantic_type: None,
2156 type_assignment: None,
2157 initializer: field
2158 .initial_value
2159 .as_ref()
2160 .map(serializable_value_from_parsed),
2161 conflicting_decorators: Vec::new(),
2162 decorator_provenance: SourceProvenance::new(&parsed.path, field.callee_span),
2163 name_provenance: Some(SourceProvenance::new(
2164 &parsed.path,
2165 field.declaration_span,
2166 )),
2167 initializer_provenance: field
2168 .initial_span
2169 .map(|span| SourceProvenance::new(&parsed.path, span)),
2170 provenance: SourceProvenance::new(&parsed.path, field.declaration_span),
2171 violations,
2172 });
2173 for (authored_ordinal, validation) in field.validations.iter().enumerate() {
2174 let validation_subject =
2175 format!("{field_subject}.validation.{authored_ordinal}");
2176 let Some(validation_declaration) =
2177 authored.declarations.iter().find(|declaration| {
2178 declaration.kind
2179 == crate::CanonicalAuthoredDeclarationKindV1::Validation
2180 && declaration.subject == validation_subject
2181 })
2182 else {
2183 continue;
2184 };
2185 let mut expression =
2186 authored_validation_rule_expression(validation, &parsed.path);
2187 let standard_schema = match (
2188 validation_declaration.intrinsic_identity.as_ref(),
2189 validation_declaration.derived_evidence.as_ref(),
2190 ) {
2191 (Some(identity), _) => {
2192 match &mut expression.kind {
2193 AuthoredValidationRuleExpressionKind::Call { callee, .. } => {
2194 *callee = Some(identity.name.clone());
2195 }
2196 AuthoredValidationRuleExpressionKind::Identifier(name) => {
2197 *name = identity.name.clone();
2198 }
2199 AuthoredValidationRuleExpressionKind::Unsupported => continue,
2200 }
2201 None
2202 }
2203 (
2204 None,
2205 Some(crate::DerivedAuthoredEvidenceV2::StandardSchemaValidation {
2206 module_specifier,
2207 export_name,
2208 declaration_modules,
2209 input_type,
2210 output_type,
2211 }),
2212 ) => Some(AuthoredStandardSchemaValidationFact {
2213 module_specifier: module_specifier.clone(),
2214 export_name: export_name.clone(),
2215 declaration_modules: declaration_modules.clone(),
2216 input_type: input_type.clone(),
2217 output_type: output_type.clone(),
2218 }),
2219 _ => continue,
2220 };
2221 validation_rule_declaration_facts.push(AuthoredValidationRuleDeclarationFact {
2222 id: ValidationRuleCandidateId::for_source_position(
2223 &parsed.path,
2224 validation.span.start,
2225 ),
2226 owner_component: Some(id.clone()),
2227 declaration_field: Some(declaration_field.clone()),
2228 authored_name: Some(field.path.join(".")),
2229 declaration_kind: AuthoredDeclarationKind::InstanceField,
2230 is_static: false,
2231 authored_ordinal,
2232 decorator_invoked: true,
2233 decorator_argument_count: 1,
2234 expression: Some(expression),
2235 standard_schema,
2236 conflicting_decorators: Vec::new(),
2237 decorator_provenance: SourceProvenance::new(&parsed.path, validation.span),
2238 name_provenance: Some(SourceProvenance::new(
2239 &parsed.path,
2240 field.declaration_span,
2241 )),
2242 provenance: SourceProvenance::new(&parsed.path, field.declaration_span),
2243 });
2244 }
2245 }
2246 if let Some(submit) = shape.submit.as_ref() {
2247 let handler = &submit.handler;
2248 let capability_call = handler.direct_call.as_ref().filter(|call| {
2249 submit.is_async
2250 && submit.parameter_count == 1
2251 && handler.state_updates.is_empty()
2252 && handler.local_variables.is_empty()
2253 && call.arguments.len() == 2
2254 && call.arguments[0].name == "value"
2255 && call.arguments[1].name == "signal"
2256 });
2257 let state_update_handler = submit.parameter_count == 1
2258 && !handler.is_expression_body
2259 && handler.unsupported_statement_spans.is_empty()
2260 && handler
2261 .state_updates
2262 .iter()
2263 .all(|update| canonical_state_names.contains(update.field.as_str()));
2264 if capability_call.is_none() && !state_update_handler {
2265 diagnostics.push(ComponentDiagnostic::error(
2266 "PSV2F1006",
2267 format!(
2268 "canonical V2 Form `{}` has an unsupported inline submit handler",
2269 candidate.subject
2270 ),
2271 ));
2272 continue;
2273 }
2274 let Some(operands) = capability_call
2275 .is_some()
2276 .then(BTreeMap::new)
2277 .or_else(|| v2_action_operands(handler, class))
2278 else {
2279 diagnostics.push(ComponentDiagnostic::error(
2280 "PSV2F1007",
2281 format!(
2282 "canonical V2 Form `{}` has unsupported submit operands",
2283 candidate.subject
2284 ),
2285 ));
2286 continue;
2287 };
2288 let method_name = format!("__submit_{name}");
2289 let endpoint_id = id.action_endpoint(&method_name);
2290 let submit_provenance = SourceProvenance::new(&parsed.path, submit.span);
2291 provenance.insert(endpoint_id.clone(), submit_provenance.clone());
2292 action_endpoints.push(ActionEndpoint {
2293 id: endpoint_id.clone(),
2294 owner: SemanticOwner::entity(id.clone()),
2295 name: method_name.clone(),
2296 });
2297 actions.extend(
2298 handler
2299 .state_updates
2300 .iter()
2301 .enumerate()
2302 .map(|(index, update)| ComponentAction {
2303 id: id.action(&method_name, index),
2304 owner: SemanticOwner::entity(endpoint_id.clone()),
2305 method: method_name.clone(),
2306 operation: state_operation_from_v2_parsed(&update.operation, &operands),
2307 field: update.field.clone(),
2308 }),
2309 );
2310 submission_declaration_facts.push(AuthoredSubmissionDeclarationFact {
2311 native_inline: true,
2312 id: SubmissionDeclarationCandidateId::for_source_position(
2313 &parsed.path,
2314 submit.span.start,
2315 ),
2316 owner_component: Some(id.clone()),
2317 method: Some(endpoint_id),
2318 method_name: Some(method_name),
2319 is_static: false,
2320 is_async: submit.is_async,
2321 parameter_count: submit.parameter_count,
2322 return_type: None,
2323 submit_invoked: true,
2324 submit_argument_count: 1,
2325 form_designator: Some(name.to_owned()),
2326 has_action: true,
2327 action_invoked: true,
2328 action_argument_count: 0,
2329 capability_local_name: capability_call.map(|call| call.callee.clone()),
2330 capability_provenance: capability_call
2331 .map(|call| SourceProvenance::new(&parsed.path, call.callee_span)),
2332 inherited: false,
2333 decorator_provenance: submit_provenance.clone(),
2334 form_designator_provenance: Some(SourceProvenance::new(
2335 &parsed.path,
2336 property.name_span,
2337 )),
2338 method_provenance: submit_provenance,
2339 });
2340 }
2341 }
2342 components.push(ComponentNode {
2343 id: id.clone(),
2344 module_path: parsed.path.clone(),
2345 owner: SemanticOwner::Application,
2346 class_name: class.name.clone(),
2347 element_name: Some(class.name.clone()),
2348 route_path: None,
2349 heritage: class
2350 .heritage
2351 .as_ref()
2352 .map(|heritage| AuthoredComponentHeritage {
2353 base: heritage.base.clone(),
2354 provenance: SourceProvenance::new(&parsed.path, heritage.span),
2355 }),
2356 state_fields,
2357 context_declarations: Vec::new(),
2358 provider_declarations: Vec::new(),
2359 consumer_declarations: Vec::new(),
2360 slot_declarations,
2361 context_declaration_candidates: Vec::new(),
2362 slot_declaration_candidates: Vec::new(),
2363 form_declaration_candidates,
2364 resource_declaration_candidates: Vec::new(),
2365 route_loader_declaration_candidates: Vec::new(),
2366 form_field_declaration_candidates,
2367 validation_rule_declaration_facts,
2368 submission_declaration_facts,
2369 serialization_declaration_facts,
2370 opaque_action_facts: Vec::new(),
2371 terminal_package_invocations,
2372 server_action_facts: Vec::new(),
2373 shadowed_validation_intrinsics: BTreeSet::new(),
2374 methods,
2375 effect_fields,
2376 action_endpoints,
2377 actions,
2378 render,
2379 });
2380 }
2381 ComponentGraph {
2382 components,
2383 diagnostics,
2384 references: Vec::new(),
2385 provenance,
2386 }
2387}
2388
2389fn range_from_span(span: SourceSpan) -> crate::AuthoredSourceRangeV1 {
2390 crate::AuthoredSourceRangeV1 {
2391 start: span.start,
2392 end: span.end,
2393 line: span.line,
2394 column: span.column,
2395 }
2396}
2397
2398fn build_component_graph_with_identity(
2399 parsed: &ParsedFile,
2400 module_qualified_identity: bool,
2401) -> ComponentGraph {
2402 let builtin_types = crate::BuiltinTypeAuthority::for_file(parsed);
2403 let mut components = Vec::new();
2404 let mut diagnostics = Vec::new();
2405 let mut references = Vec::new();
2406 let mut provenance = BTreeMap::new();
2407
2408 for class in &parsed.classes {
2409 let element_name = component_element_name(class);
2410 let id = if module_qualified_identity {
2411 SemanticId::component_in_module(&parsed.path, element_name.as_deref(), &class.name)
2412 } else {
2413 SemanticId::component(element_name.as_deref(), &class.name)
2414 };
2415 let shadowed_validation_intrinsics = parsed
2416 .imports
2417 .iter()
2418 .flat_map(|import| import.specifiers.iter())
2419 .map(|specifier| specifier.local.clone())
2420 .chain(parsed.local_value_bindings.iter().cloned())
2421 .filter(|name| is_validation_intrinsic_name(name))
2422 .collect();
2423 let component = build_component_node(
2424 class,
2425 &parsed.path,
2426 id,
2427 builtin_types,
2428 shadowed_validation_intrinsics,
2429 &mut diagnostics,
2430 );
2431 let component_provenance = collect_component_provenance(class, &component, &parsed.path);
2432 references.extend(collect_semantic_references(
2433 &component,
2434 &component_provenance,
2435 ));
2436 provenance.extend(component_provenance);
2437 components.push(component);
2438 }
2439
2440 if parsed.classes.is_empty() && parsed.diagnostics.is_empty() {
2441 diagnostics.push(ComponentDiagnostic {
2442 severity: ComponentDiagnosticSeverity::Error,
2443 effect_id: None,
2444 statement_id: None,
2445 context_declaration_candidate_id: None,
2446 context_id: None,
2447 provider_id: None,
2448 consumer_id: None,
2449 slot_id: None,
2450 invocation_id: None,
2451 component_instance_id: None,
2452 slot_binding_id: None,
2453 structural_region_id: None,
2454 component_id: None,
2455 provider_instance_id: None,
2456 consumer_instance_id: None,
2457 secondary_labels: Vec::new(),
2458 provenance: None,
2459 code: "PSC1000".to_string(),
2460 message: "no component classes found".to_string(),
2461 });
2462 }
2463
2464 ComponentGraph {
2465 components,
2466 diagnostics,
2467 references,
2468 provenance,
2469 }
2470}
2471
2472#[allow(clippy::too_many_lines)]
2473fn build_component_node(
2474 class: &ParsedClass,
2475 path: &Path,
2476 id: SemanticId,
2477 builtin_types: crate::BuiltinTypeAuthority,
2478 shadowed_validation_intrinsics: BTreeSet<String>,
2479 diagnostics: &mut Vec<ComponentDiagnostic>,
2480) -> ComponentNode {
2481 let element_name = component_element_name(class);
2482 let route_path = decorator_argument(class, "route");
2483
2484 if element_name.is_none() {
2485 diagnostics.push(ComponentDiagnostic::error(
2486 "PSC1001",
2487 format!("class `{}` is missing @component(...)", class.name),
2488 ));
2489 }
2490
2491 let state_fields = state_fields_from_class(class, path, &id);
2492 let context_declarations = context_declarations_from_class(class, path, &id);
2493 let provider_declarations = provider_declarations_from_class(class, path, &id);
2494 let consumer_declarations = consumer_declarations_from_class(class, path, &id);
2495 let context_declaration_candidates =
2496 context_declaration_candidates_from_class(class, path, &id);
2497 let slot_declaration_candidates = slot_declaration_candidates_from_class(class, path, &id);
2498 let slot_declarations = slot_declarations_from_candidates(&slot_declaration_candidates);
2499 let form_declaration_candidates = form_declaration_candidates_from_class(
2500 class,
2501 path,
2502 element_name.is_some(),
2503 &id,
2504 builtin_types,
2505 );
2506 let resource_declaration_candidates =
2507 resource_declaration_candidates_from_class(class, path, &id);
2508 let route_loader_declaration_candidates =
2509 route_loader_declaration_candidates_from_class(class, path, &id);
2510 let form_field_declaration_candidates =
2511 form_field_declaration_candidates_from_class(class, path, element_name.is_some(), &id);
2512 let validation_rule_declaration_facts =
2513 validation_rule_declaration_facts_from_class(class, path, element_name.is_some(), &id);
2514
2515 let methods = class
2516 .methods
2517 .iter()
2518 .map(|method| component_method_from_parsed(method, path, &id))
2519 .collect::<Vec<_>>();
2520
2521 let actions = class
2522 .methods
2523 .iter()
2524 .flat_map(|method| {
2525 method
2526 .state_updates
2527 .iter()
2528 .enumerate()
2529 .map(|(index, update)| ComponentAction {
2530 id: id.action(&method.name, index),
2531 owner: SemanticOwner::entity(id.method(&method.name)),
2532 method: method.name.clone(),
2533 operation: state_operation_from_parsed_in_method(&update.operation, method),
2534 field: update.field.clone(),
2535 })
2536 })
2537 .collect::<Vec<_>>();
2538
2539 let render = class
2540 .methods
2541 .iter()
2542 .find(|method| method.name == "render")
2543 .map(|method| render_model_from_parsed_method(method, &id));
2544
2545 if render.is_none() {
2546 diagnostics.push(ComponentDiagnostic::error(
2547 "PSC1002",
2548 format!("class `{}` is missing render()", class.name),
2549 ));
2550 }
2551
2552 collect_action_parameter_assignment_diagnostics(class, diagnostics);
2553
2554 if let Some(render) = &render {
2555 collect_render_binding_diagnostics(class, render, diagnostics);
2556 collect_render_event_diagnostics(class, render, diagnostics);
2557 collect_duplicate_event_diagnostics(render, &class.name, diagnostics);
2558 collect_render_attribute_diagnostics(render, &state_fields, &class.name, diagnostics);
2559 collect_render_list_diagnostics(render, &state_fields, &class.name, diagnostics);
2560 }
2561
2562 let submission_declaration_facts =
2563 submission_declaration_facts_from_class(class, path, element_name.is_some(), &id);
2564 let serialization_declaration_facts =
2565 serialization_declaration_facts_from_class(class, path, element_name.is_some(), &id);
2566 let opaque_action_facts =
2567 opaque_action_facts_from_class(class, path, element_name.is_some(), &id);
2568 collect_opaque_action_diagnostics(&opaque_action_facts, diagnostics);
2569 let server_action_facts =
2570 server_action_facts_from_class(class, path, element_name.is_some(), &id);
2571 collect_server_action_diagnostics(&server_action_facts, diagnostics);
2572 collect_route_loader_diagnostics(&route_loader_declaration_candidates, diagnostics);
2573
2574 ComponentNode {
2575 id,
2576 module_path: path.to_path_buf(),
2577 owner: SemanticOwner::Application,
2578 class_name: class.name.clone(),
2579 element_name,
2580 route_path,
2581 heritage: class
2582 .heritage
2583 .as_ref()
2584 .map(|heritage| AuthoredComponentHeritage {
2585 base: heritage.base.clone(),
2586 provenance: SourceProvenance::new(path, heritage.span),
2587 }),
2588 state_fields,
2589 context_declarations,
2590 provider_declarations,
2591 consumer_declarations,
2592 slot_declarations,
2593 context_declaration_candidates,
2594 slot_declaration_candidates,
2595 form_declaration_candidates,
2596 resource_declaration_candidates,
2597 route_loader_declaration_candidates,
2598 form_field_declaration_candidates,
2599 validation_rule_declaration_facts,
2600 submission_declaration_facts,
2601 serialization_declaration_facts,
2602 opaque_action_facts,
2603 terminal_package_invocations: Vec::new(),
2604 server_action_facts,
2605 shadowed_validation_intrinsics,
2606 methods,
2607 effect_fields: Vec::new(),
2608 action_endpoints: Vec::new(),
2609 actions,
2610 render,
2611 }
2612}
2613
2614fn collect_route_loader_diagnostics(
2615 facts: &[AuthoredRouteLoaderDeclarationFact],
2616 diagnostics: &mut Vec<ComponentDiagnostic>,
2617) {
2618 for fact in facts {
2619 diagnostics.push(ComponentDiagnostic::error(
2620 "PSC1132",
2621 format!(
2622 "route loader field `{}` requires a compiler route-loader plan",
2623 fact.field
2624 ),
2625 ));
2626 }
2627}
2628
2629fn collect_opaque_action_diagnostics(
2630 facts: &[AuthoredOpaqueActionFact],
2631 diagnostics: &mut Vec<ComponentDiagnostic>,
2632) {
2633 for fact in facts {
2634 if !is_valid_opaque_action_fact(fact) {
2635 diagnostics.push(ComponentDiagnostic::error(
2636 "PSC1130",
2637 format!(
2638 "opaque Action `{}` must be @opaque(\"package\", \"export\") on an empty synchronous zero-parameter @action() method",
2639 fact.method_name
2640 ),
2641 ));
2642 }
2643 }
2644}
2645
2646fn collect_server_action_diagnostics(
2647 facts: &[AuthoredServerActionFact],
2648 diagnostics: &mut Vec<ComponentDiagnostic>,
2649) {
2650 for fact in facts {
2651 diagnostics.push(ComponentDiagnostic::error(
2652 "PSC1133",
2653 format!(
2654 "server Action `{}` requires a compiler server-action plan",
2655 fact.method_name
2656 ),
2657 ));
2658 }
2659}
2660
2661#[must_use]
2662pub fn is_valid_opaque_action_fact(fact: &AuthoredOpaqueActionFact) -> bool {
2663 fact.owner_component.is_some()
2664 && fact.invoked
2665 && fact.argument_count == 2
2666 && fact.package.as_ref().is_some_and(|value| !value.is_empty())
2667 && fact.export.as_ref().is_some_and(|value| !value.is_empty())
2668 && fact.is_action
2669 && fact.action_invoked
2670 && !fact.is_async
2671 && fact.parameter_count == 0
2672 && !fact.has_body_effects
2673}
2674
2675fn opaque_action_facts_from_class(
2676 class: &ParsedClass,
2677 path: &Path,
2678 is_canonical_component: bool,
2679 component: &SemanticId,
2680) -> Vec<AuthoredOpaqueActionFact> {
2681 let mut facts = class
2682 .methods
2683 .iter()
2684 .flat_map(|method| {
2685 let action = method
2686 .decorators
2687 .iter()
2688 .find(|decorator| decorator.name == "action");
2689 method
2690 .decorators
2691 .iter()
2692 .filter(move |decorator| decorator.name == "opaque")
2693 .map(move |decorator| AuthoredOpaqueActionFact {
2694 id: component.opaque_activation(&method.name),
2695 owner_component: is_canonical_component.then(|| component.clone()),
2696 method: is_canonical_component.then(|| component.method(&method.name)),
2697 method_name: method.name.clone(),
2698 package: decorator.arguments.first().and_then(Clone::clone),
2699 export: decorator.arguments.get(1).and_then(Clone::clone),
2700 invoked: decorator.is_invoked,
2701 argument_count: decorator.argument_count,
2702 is_action: action.is_some(),
2703 action_invoked: action.is_some_and(|decorator| decorator.is_invoked),
2704 is_async: method.is_async,
2705 parameter_count: method.parameters.len(),
2706 has_body_effects: !method.state_updates.is_empty()
2707 || !method.local_variables.is_empty()
2708 || !method.return_values.is_empty()
2709 || !method.calls.is_empty()
2710 || method.effect_body.is_some(),
2711 provenance: SourceProvenance::new(path, decorator.span),
2712 })
2713 })
2714 .collect::<Vec<_>>();
2715 facts.sort_by(|left, right| left.id.cmp(&right.id));
2716 facts
2717}
2718
2719fn server_action_facts_from_class(
2720 class: &ParsedClass,
2721 path: &Path,
2722 is_canonical_component: bool,
2723 component: &SemanticId,
2724) -> Vec<AuthoredServerActionFact> {
2725 let mut facts = class
2726 .methods
2727 .iter()
2728 .flat_map(|method| {
2729 let action = method
2730 .decorators
2731 .iter()
2732 .find(|decorator| decorator.name == "action");
2733 method
2734 .decorators
2735 .iter()
2736 .filter(move |decorator| decorator.name == "serverAction")
2737 .map(move |decorator| AuthoredServerActionFact {
2738 id: component.server_action(&method.name),
2739 owner_component: is_canonical_component.then(|| component.clone()),
2740 method: is_canonical_component.then(|| component.method(&method.name)),
2741 method_name: method.name.clone(),
2742 endpoint_designator: decorator.argument.clone(),
2743 invoked: decorator.is_invoked,
2744 argument_count: decorator.argument_count,
2745 is_action: action.is_some(),
2746 action_invoked: action.is_some_and(|decorator| decorator.is_invoked),
2747 is_async: method.is_async,
2748 parameter_count: method.parameters.len(),
2749 has_body_effects: !method.state_updates.is_empty()
2750 || !method.local_variables.is_empty()
2751 || !method.return_values.is_empty()
2752 || !method.calls.is_empty()
2753 || method.effect_body.is_some(),
2754 provenance: SourceProvenance::new(path, decorator.span),
2755 })
2756 })
2757 .collect::<Vec<_>>();
2758 facts.sort_by(|left, right| left.id.cmp(&right.id));
2759 facts
2760}
2761
2762#[allow(clippy::too_many_lines)]
2763fn form_declaration_candidates_from_class(
2764 class: &ParsedClass,
2765 path: &Path,
2766 is_canonical_component: bool,
2767 component: &SemanticId,
2768 builtin_types: crate::BuiltinTypeAuthority,
2769) -> Vec<FormDeclarationCandidate> {
2770 let owner = is_canonical_component.then_some(component.clone());
2771 let mut candidates = Vec::new();
2772
2773 retain_non_field_form_candidates(
2774 &mut candidates,
2775 owner.as_ref(),
2776 path,
2777 &class.name,
2778 AuthoredDeclarationKind::Class,
2779 &class.decorators,
2780 class.span,
2781 );
2782
2783 for property in &class.properties {
2784 let form_decorator_count = property
2785 .decorators
2786 .iter()
2787 .filter(|decorator| decorator.name == "form")
2788 .count();
2789 for decorator in property
2790 .decorators
2791 .iter()
2792 .filter(|decorator| decorator.name == "form")
2793 {
2794 let declaration_kind = if property.is_static {
2795 AuthoredDeclarationKind::StaticField
2796 } else {
2797 AuthoredDeclarationKind::InstanceField
2798 };
2799 let identity_capable = owner.is_some() && property.is_identifier_name;
2800 let form_id = identity_capable.then(|| {
2801 FormId::for_owner(
2802 owner.as_ref().expect("identity-capable Form has an owner"),
2803 &property.name,
2804 )
2805 });
2806 let authored_field = identity_capable.then(|| {
2807 owner
2808 .as_ref()
2809 .expect("identity-capable Form has an owner")
2810 .form_field(&property.name)
2811 });
2812 let declared_type =
2813 property
2814 .type_annotation
2815 .as_ref()
2816 .map(|annotation| DeclaredStateType {
2817 kind: declared_state_type_kind(&annotation.text),
2818 text: annotation.text.clone(),
2819 provenance: SourceProvenance::new(path, annotation.span),
2820 });
2821 let mut conflicting_decorators = property
2822 .decorators
2823 .iter()
2824 .filter(|other| other.name != "form" && is_presolve_semantic_decorator(&other.name))
2825 .map(|other| other.name.clone())
2826 .collect::<Vec<_>>();
2827 if property.initializer.as_deref() == Some("state(...)") {
2828 conflicting_decorators.push("state".to_string());
2829 }
2830 conflicting_decorators.sort();
2831 conflicting_decorators.dedup();
2832
2833 let declaration_only = property.initializer.is_none()
2834 && (property.is_definite_assignment || property.is_declare);
2835 let mut violations = Vec::new();
2836 if owner.is_none() {
2837 violations.push(FormDeclarationViolation::InvalidOwner);
2838 }
2839 if !property.is_identifier_name {
2840 violations.push(FormDeclarationViolation::UnsupportedFieldName);
2841 }
2842 if !decorator.is_invoked {
2843 violations.push(FormDeclarationViolation::InvalidDecoratorInvocation);
2844 }
2845 if decorator.argument_count != 0 {
2846 violations.push(FormDeclarationViolation::InvalidDecoratorArity {
2847 actual: decorator.argument_count,
2848 expected: 0,
2849 });
2850 }
2851 if form_decorator_count != 1 {
2852 violations.push(FormDeclarationViolation::DuplicateFormDecorator);
2853 }
2854 if property.is_static {
2855 violations.push(FormDeclarationViolation::StaticField);
2856 }
2857 if property.initializer.is_some() {
2858 violations.push(FormDeclarationViolation::InitializedField);
2859 } else if !declaration_only {
2860 violations.push(FormDeclarationViolation::DeclarationOnlyRequired);
2861 }
2862 match &declared_type {
2863 None => violations.push(FormDeclarationViolation::MissingType),
2864 Some(declared) if !builtin_types.recognizes_form_marker(&declared.text) => {
2865 violations.push(FormDeclarationViolation::InvalidType {
2866 actual: Some(declared.text.clone()),
2867 });
2868 }
2869 Some(_) => {}
2870 }
2871 if !conflicting_decorators.is_empty() {
2872 violations.push(FormDeclarationViolation::ConflictingSemanticDecorator);
2873 }
2874 canonicalize_form_violations(&mut violations);
2875
2876 candidates.push(FormDeclarationCandidate {
2877 id: FormDeclarationCandidateId::for_source_position(path, decorator.span.start),
2878 owner_component: owner.clone(),
2879 form_id,
2880 authored_field,
2881 authored_name: property.is_identifier_name.then(|| property.name.clone()),
2882 declaration_kind,
2883 decorator_invoked: decorator.is_invoked,
2884 decorator_argument_count: decorator.argument_count,
2885 decorator_argument_provenance: decorator
2886 .argument_spans
2887 .iter()
2888 .copied()
2889 .map(|span| SourceProvenance::new(path, span))
2890 .collect(),
2891 declaration_only,
2892 declared_type,
2893 conflicting_decorators,
2894 decorator_provenance: SourceProvenance::new(path, decorator.span),
2895 name_provenance: property
2896 .is_identifier_name
2897 .then(|| SourceProvenance::new(path, property.name_span)),
2898 initializer_provenance: property
2899 .initializer_span
2900 .map(|span| SourceProvenance::new(path, span)),
2901 provenance: SourceProvenance::new(path, property.span),
2902 status: form_declaration_status(violations),
2903 });
2904 }
2905 }
2906
2907 for method in &class.methods {
2908 retain_non_field_form_candidates(
2909 &mut candidates,
2910 owner.as_ref(),
2911 path,
2912 &method.name,
2913 if method.is_getter {
2914 AuthoredDeclarationKind::Getter
2915 } else if method.is_setter {
2916 AuthoredDeclarationKind::Setter
2917 } else {
2918 AuthoredDeclarationKind::Method
2919 },
2920 &method.decorators,
2921 method.span,
2922 );
2923 for parameter in &method.parameters {
2924 retain_non_field_form_candidates(
2925 &mut candidates,
2926 owner.as_ref(),
2927 path,
2928 ¶meter.name,
2929 AuthoredDeclarationKind::Parameter,
2930 ¶meter.decorators,
2931 parameter.span,
2932 );
2933 }
2934 }
2935
2936 let mut duplicate_groups = BTreeMap::<(SemanticId, String), Vec<usize>>::new();
2937 for (index, candidate) in candidates.iter().enumerate() {
2938 if candidate.form_id.is_none() {
2939 continue;
2940 }
2941 if let (Some(owner), Some(name)) = (
2942 candidate.owner_component.as_ref(),
2943 candidate.authored_name.as_ref(),
2944 ) {
2945 duplicate_groups
2946 .entry((owner.clone(), name.clone()))
2947 .or_default()
2948 .push(index);
2949 }
2950 }
2951 let duplicate_groups = duplicate_groups
2952 .into_values()
2953 .filter(|indexes| {
2954 indexes
2955 .iter()
2956 .map(|index| {
2957 let provenance = &candidates[*index].provenance;
2958 (
2959 provenance.path.as_path(),
2960 provenance.span.start,
2961 provenance.span.end,
2962 )
2963 })
2964 .collect::<BTreeSet<_>>()
2965 .len()
2966 > 1
2967 })
2968 .collect::<Vec<_>>();
2969 for group in duplicate_groups {
2970 for index in group {
2971 let candidate = &mut candidates[index];
2972 let mut violations = candidate.violations().to_vec();
2973 violations.push(FormDeclarationViolation::DuplicateName);
2974 canonicalize_form_violations(&mut violations);
2975 candidate.status = FormDeclarationStatus::Invalid(violations);
2976 }
2977 }
2978 candidates.sort_by(|left, right| {
2979 (
2980 left.provenance.path.as_path(),
2981 left.provenance.span.start,
2982 left.provenance.span.end,
2983 left.id.as_str(),
2984 )
2985 .cmp(&(
2986 right.provenance.path.as_path(),
2987 right.provenance.span.start,
2988 right.provenance.span.end,
2989 right.id.as_str(),
2990 ))
2991 });
2992 candidates
2993}
2994
2995fn retain_non_field_form_candidates(
2996 candidates: &mut Vec<FormDeclarationCandidate>,
2997 owner: Option<&SemanticId>,
2998 path: &Path,
2999 authored_name: &str,
3000 declaration_kind: AuthoredDeclarationKind,
3001 decorators: &[presolve_parser::ParsedDecorator],
3002 span: SourceSpan,
3003) {
3004 let form_decorator_count = decorators
3005 .iter()
3006 .filter(|decorator| decorator.name == "form")
3007 .count();
3008 for decorator in decorators
3009 .iter()
3010 .filter(|decorator| decorator.name == "form")
3011 {
3012 let mut violations = vec![FormDeclarationViolation::InvalidTarget {
3013 actual: declaration_kind,
3014 }];
3015 if owner.is_none() {
3016 violations.push(FormDeclarationViolation::InvalidOwner);
3017 }
3018 if !decorator.is_invoked {
3019 violations.push(FormDeclarationViolation::InvalidDecoratorInvocation);
3020 }
3021 if decorator.argument_count != 0 {
3022 violations.push(FormDeclarationViolation::InvalidDecoratorArity {
3023 actual: decorator.argument_count,
3024 expected: 0,
3025 });
3026 }
3027 if form_decorator_count != 1 {
3028 violations.push(FormDeclarationViolation::DuplicateFormDecorator);
3029 }
3030 canonicalize_form_violations(&mut violations);
3031 candidates.push(FormDeclarationCandidate {
3032 id: FormDeclarationCandidateId::for_source_position(path, decorator.span.start),
3033 owner_component: owner.cloned(),
3034 form_id: None,
3035 authored_field: None,
3036 authored_name: Some(authored_name.to_string()),
3037 declaration_kind,
3038 decorator_invoked: decorator.is_invoked,
3039 decorator_argument_count: decorator.argument_count,
3040 decorator_argument_provenance: decorator
3041 .argument_spans
3042 .iter()
3043 .copied()
3044 .map(|span| SourceProvenance::new(path, span))
3045 .collect(),
3046 declaration_only: false,
3047 declared_type: None,
3048 conflicting_decorators: Vec::new(),
3049 decorator_provenance: SourceProvenance::new(path, decorator.span),
3050 name_provenance: None,
3051 initializer_provenance: None,
3052 provenance: SourceProvenance::new(path, span),
3053 status: FormDeclarationStatus::Invalid(violations),
3054 });
3055 }
3056}
3057
3058#[allow(clippy::too_many_lines)]
3059fn form_field_declaration_candidates_from_class(
3060 class: &ParsedClass,
3061 path: &Path,
3062 is_canonical_component: bool,
3063 component: &SemanticId,
3064) -> Vec<FormFieldDeclarationCandidate> {
3065 let owner = is_canonical_component.then_some(component.clone());
3066 let mut candidates = Vec::new();
3067
3068 retain_non_property_form_field_candidates(
3069 &mut candidates,
3070 owner.as_ref(),
3071 path,
3072 &class.name,
3073 AuthoredDeclarationKind::Class,
3074 &class.decorators,
3075 class.span,
3076 );
3077
3078 for property in &class.properties {
3079 let field_decorator_count = property
3080 .decorators
3081 .iter()
3082 .filter(|decorator| decorator.name == "field")
3083 .count();
3084 for decorator in property
3085 .decorators
3086 .iter()
3087 .filter(|decorator| decorator.name == "field")
3088 {
3089 let declaration_kind = if property.is_static {
3090 AuthoredDeclarationKind::StaticField
3091 } else {
3092 AuthoredDeclarationKind::InstanceField
3093 };
3094 let initializer = property
3095 .initializer_literal
3096 .as_ref()
3097 .map(serializable_value_from_parsed)
3098 .or_else(|| {
3099 property
3100 .initializer_constant_expression
3101 .as_ref()
3102 .map(constant_expression_from_parsed)
3103 .and_then(|expression| expression.evaluate().ok())
3104 });
3105 let declared_type =
3106 property
3107 .type_annotation
3108 .as_ref()
3109 .map(|annotation| DeclaredStateType {
3110 kind: declared_state_type_kind(&annotation.text),
3111 text: annotation.text.clone(),
3112 provenance: SourceProvenance::new(path, annotation.span),
3113 });
3114 let mut conflicting_decorators = property
3115 .decorators
3116 .iter()
3117 .filter(|other| !matches!(other.name.as_str(), "field" | "validate"))
3118 .map(|other| other.name.clone())
3119 .collect::<Vec<_>>();
3120 conflicting_decorators.sort();
3121 conflicting_decorators.dedup();
3122 let (form_designator, unsupported_form_designator) =
3123 normalized_form_designator(decorator, path);
3124 let nested_path_segments = decorator
3125 .arguments
3126 .get(1)
3127 .and_then(Option::as_deref)
3128 .and_then(parse_static_form_field_path);
3129 let mut violations = Vec::new();
3130 if owner.is_none() {
3131 violations.push(FormFieldDeclarationViolation::InvalidOwner);
3132 }
3133 if !property.is_identifier_name {
3134 violations.push(FormFieldDeclarationViolation::UnsupportedFieldName);
3135 }
3136 if !decorator.is_invoked {
3137 violations.push(FormFieldDeclarationViolation::InvalidDecoratorInvocation);
3138 }
3139 if !(1..=2).contains(&decorator.argument_count) {
3140 violations.push(FormFieldDeclarationViolation::InvalidDecoratorArity {
3141 actual: decorator.argument_count,
3142 expected: 1,
3143 });
3144 }
3145 if decorator.argument_count == 2 && nested_path_segments.is_none() {
3146 violations.push(FormFieldDeclarationViolation::InvalidPath);
3147 }
3148 if field_decorator_count != 1 {
3149 violations.push(FormFieldDeclarationViolation::DuplicateFieldDecorator);
3150 }
3151 if form_designator.is_none() {
3152 violations.push(FormFieldDeclarationViolation::InvalidFormDesignator);
3153 }
3154 if property.is_static {
3155 violations.push(FormFieldDeclarationViolation::StaticField);
3156 }
3157 if property.initializer_span.is_none() {
3158 violations.push(FormFieldDeclarationViolation::MissingInitializer);
3159 } else if initializer.is_none() {
3160 violations.push(FormFieldDeclarationViolation::UnsupportedInitializer);
3161 }
3162 if !conflicting_decorators.is_empty() {
3163 violations.push(FormFieldDeclarationViolation::ConflictingSemanticDecorator);
3164 }
3165 canonicalize_form_field_violations(&mut violations);
3166
3167 candidates.push(FormFieldDeclarationCandidate {
3168 id: FormFieldDeclarationCandidateId::for_source_position(
3169 path,
3170 decorator.span.start,
3171 ),
3172 owner_component: owner.clone(),
3173 declaration_field: (owner.is_some() && property.is_identifier_name).then(|| {
3174 owner
3175 .as_ref()
3176 .expect("authored field identity requires component owner")
3177 .field(&property.name)
3178 }),
3179 authored_name: property.is_identifier_name.then(|| property.name.clone()),
3180 field_id: None,
3181 decorator_invoked: decorator.is_invoked,
3182 decorator_argument_count: decorator.argument_count,
3183 decorator_argument_provenance: decorator
3184 .argument_spans
3185 .iter()
3186 .copied()
3187 .map(|span| SourceProvenance::new(path, span))
3188 .collect(),
3189 nested_path_segments,
3190 form_designator,
3191 unsupported_form_designator,
3192 resolved_form: None,
3193 declaration_kind,
3194 is_static: property.is_static,
3195 declared_type,
3196 authority_type: None,
3197 semantic_type: None,
3198 type_assignment: None,
3199 initializer,
3200 conflicting_decorators,
3201 decorator_provenance: SourceProvenance::new(path, decorator.span),
3202 name_provenance: property
3203 .is_identifier_name
3204 .then(|| SourceProvenance::new(path, property.name_span)),
3205 initializer_provenance: property
3206 .initializer_span
3207 .map(|span| SourceProvenance::new(path, span)),
3208 provenance: SourceProvenance::new(path, property.span),
3209 violations,
3210 });
3211 }
3212 }
3213
3214 for method in &class.methods {
3215 let kind = if method.is_getter {
3216 AuthoredDeclarationKind::Getter
3217 } else if method.is_setter {
3218 AuthoredDeclarationKind::Setter
3219 } else {
3220 AuthoredDeclarationKind::Method
3221 };
3222 retain_non_property_form_field_candidates(
3223 &mut candidates,
3224 owner.as_ref(),
3225 path,
3226 &method.name,
3227 kind,
3228 &method.decorators,
3229 method.span,
3230 );
3231 for parameter in &method.parameters {
3232 retain_non_property_form_field_candidates(
3233 &mut candidates,
3234 owner.as_ref(),
3235 path,
3236 ¶meter.name,
3237 AuthoredDeclarationKind::Parameter,
3238 ¶meter.decorators,
3239 parameter.span,
3240 );
3241 }
3242 }
3243
3244 candidates.sort_by(form_field_candidate_source_order);
3245 candidates
3246}
3247
3248#[allow(clippy::too_many_lines)]
3249fn validation_rule_declaration_facts_from_class(
3250 class: &ParsedClass,
3251 path: &Path,
3252 is_canonical_component: bool,
3253 component: &SemanticId,
3254) -> Vec<AuthoredValidationRuleDeclarationFact> {
3255 let owner = is_canonical_component.then_some(component);
3256 let mut facts = Vec::new();
3257
3258 retain_validation_rule_facts(
3259 &mut facts,
3260 owner,
3261 None,
3262 Some(class.name.as_str()),
3263 AuthoredDeclarationKind::Class,
3264 false,
3265 &class.decorators,
3266 None,
3267 class.span,
3268 path,
3269 );
3270
3271 for property in &class.properties {
3272 let declaration_field = (owner.is_some() && property.is_identifier_name)
3273 .then(|| component.field(&property.name));
3274 let kind = if property.is_static {
3275 AuthoredDeclarationKind::StaticField
3276 } else {
3277 AuthoredDeclarationKind::InstanceField
3278 };
3279 retain_validation_rule_facts(
3280 &mut facts,
3281 owner,
3282 declaration_field.as_ref(),
3283 property
3284 .is_identifier_name
3285 .then_some(property.name.as_str()),
3286 kind,
3287 property.is_static,
3288 &property.decorators,
3289 property.is_identifier_name.then_some(property.name_span),
3290 property.span,
3291 path,
3292 );
3293 }
3294
3295 for method in &class.methods {
3296 let kind = if method.is_getter {
3297 AuthoredDeclarationKind::Getter
3298 } else if method.is_setter {
3299 AuthoredDeclarationKind::Setter
3300 } else {
3301 AuthoredDeclarationKind::Method
3302 };
3303 retain_validation_rule_facts(
3304 &mut facts,
3305 owner,
3306 None,
3307 Some(method.name.as_str()),
3308 kind,
3309 false,
3310 &method.decorators,
3311 None,
3312 method.span,
3313 path,
3314 );
3315 for parameter in &method.parameters {
3316 retain_validation_rule_facts(
3317 &mut facts,
3318 owner,
3319 None,
3320 Some(parameter.name.as_str()),
3321 AuthoredDeclarationKind::Parameter,
3322 false,
3323 ¶meter.decorators,
3324 None,
3325 parameter.span,
3326 path,
3327 );
3328 }
3329 }
3330
3331 facts.sort_by(|left, right| {
3332 (
3333 left.provenance.path.as_path(),
3334 left.decorator_provenance.span.start,
3335 left.id.as_str(),
3336 )
3337 .cmp(&(
3338 right.provenance.path.as_path(),
3339 right.decorator_provenance.span.start,
3340 right.id.as_str(),
3341 ))
3342 });
3343 facts
3344}
3345
3346fn submission_declaration_facts_from_class(
3347 class: &ParsedClass,
3348 path: &Path,
3349 is_canonical_component: bool,
3350 component: &SemanticId,
3351) -> Vec<AuthoredSubmissionDeclarationFact> {
3352 let mut facts = class
3353 .methods
3354 .iter()
3355 .flat_map(|method| {
3356 let action = method
3357 .decorators
3358 .iter()
3359 .find(|decorator| decorator.name == "action");
3360 method
3361 .decorators
3362 .iter()
3363 .filter(move |decorator| decorator.name == "submit")
3364 .map(move |decorator| AuthoredSubmissionDeclarationFact {
3365 native_inline: false,
3366 id: SubmissionDeclarationCandidateId::for_source_position(
3367 path,
3368 decorator.span.start,
3369 ),
3370 owner_component: is_canonical_component.then(|| component.clone()),
3371 method: is_canonical_component.then(|| component.method(&method.name)),
3372 method_name: Some(method.name.clone()),
3373 is_static: method.is_static,
3374 is_async: method.is_async,
3375 parameter_count: method.parameters.len(),
3376 return_type: method
3377 .return_type_annotation
3378 .as_ref()
3379 .map(|annotation| annotation.text.clone()),
3380 submit_invoked: decorator.is_invoked,
3381 submit_argument_count: decorator.argument_count,
3382 form_designator: normalized_submission_form_designator(decorator),
3383 has_action: action.is_some(),
3384 action_invoked: action.is_some_and(|decorator| decorator.is_invoked),
3385 action_argument_count: action.map_or(0, |decorator| decorator.argument_count),
3386 capability_local_name: None,
3387 capability_provenance: None,
3388 inherited: class.heritage.is_some(),
3389 decorator_provenance: SourceProvenance::new(path, decorator.span),
3390 form_designator_provenance: decorator
3391 .argument_spans
3392 .first()
3393 .map(|span| SourceProvenance::new(path, *span)),
3394 method_provenance: SourceProvenance::new(path, method.span),
3395 })
3396 })
3397 .collect::<Vec<_>>();
3398 facts.sort_by(|left, right| {
3399 (
3400 left.decorator_provenance.path.as_path(),
3401 left.decorator_provenance.span.start,
3402 left.id.as_str(),
3403 )
3404 .cmp(&(
3405 right.decorator_provenance.path.as_path(),
3406 right.decorator_provenance.span.start,
3407 right.id.as_str(),
3408 ))
3409 });
3410 facts
3411}
3412
3413fn serialization_declaration_facts_from_class(
3414 class: &ParsedClass,
3415 path: &Path,
3416 is_canonical_component: bool,
3417 component: &SemanticId,
3418) -> Vec<AuthoredSerializationDeclarationFact> {
3419 let mut facts = class
3420 .properties
3421 .iter()
3422 .flat_map(|property| {
3423 property
3424 .decorators
3425 .iter()
3426 .filter(|decorator| decorator.name == "serialize")
3427 .map(|decorator| AuthoredSerializationDeclarationFact {
3428 owner_component: is_canonical_component.then(|| component.clone()),
3429 declaration_field: (is_canonical_component && property.is_identifier_name)
3430 .then(|| component.form_field(&property.name)),
3431 authored_name: property.is_identifier_name.then(|| property.name.clone()),
3432 invoked: decorator.is_invoked,
3433 argument_count: decorator.argument_count,
3434 format: decorator.argument.clone(),
3435 provenance: SourceProvenance::new(path, property.span),
3436 decorator_provenance: SourceProvenance::new(path, decorator.span),
3437 })
3438 })
3439 .collect::<Vec<_>>();
3440 facts.sort_by(|left, right| {
3441 (
3442 left.decorator_provenance.path.as_path(),
3443 left.decorator_provenance.span.start,
3444 )
3445 .cmp(&(
3446 right.decorator_provenance.path.as_path(),
3447 right.decorator_provenance.span.start,
3448 ))
3449 });
3450 facts
3451}
3452
3453#[allow(clippy::too_many_arguments)]
3454fn retain_validation_rule_facts(
3455 facts: &mut Vec<AuthoredValidationRuleDeclarationFact>,
3456 owner: Option<&SemanticId>,
3457 declaration_field: Option<&SemanticId>,
3458 authored_name: Option<&str>,
3459 declaration_kind: AuthoredDeclarationKind,
3460 is_static: bool,
3461 decorators: &[presolve_parser::ParsedDecorator],
3462 name_span: Option<SourceSpan>,
3463 declaration_span: SourceSpan,
3464 path: &Path,
3465) {
3466 let mut conflicting_decorators = decorators
3467 .iter()
3468 .filter(|decorator| {
3469 !matches!(decorator.name.as_str(), "field" | "validate")
3470 && is_presolve_semantic_decorator(&decorator.name)
3471 })
3472 .map(|decorator| decorator.name.clone())
3473 .collect::<Vec<_>>();
3474 conflicting_decorators.sort();
3475 conflicting_decorators.dedup();
3476
3477 for (authored_ordinal, decorator) in decorators
3478 .iter()
3479 .filter(|decorator| decorator.name == "validate")
3480 .enumerate()
3481 {
3482 facts.push(AuthoredValidationRuleDeclarationFact {
3483 id: ValidationRuleCandidateId::for_source_position(path, decorator.span.start),
3484 owner_component: owner.cloned(),
3485 declaration_field: declaration_field.cloned(),
3486 authored_name: authored_name.map(str::to_string),
3487 declaration_kind,
3488 is_static,
3489 authored_ordinal,
3490 decorator_invoked: decorator.is_invoked,
3491 decorator_argument_count: decorator.argument_count,
3492 expression: decorator
3493 .validation_rule_expression
3494 .as_ref()
3495 .map(|expression| authored_validation_rule_expression(expression, path)),
3496 standard_schema: None,
3497 conflicting_decorators: conflicting_decorators.clone(),
3498 decorator_provenance: SourceProvenance::new(path, decorator.span),
3499 name_provenance: name_span.map(|span| SourceProvenance::new(path, span)),
3500 provenance: SourceProvenance::new(path, declaration_span),
3501 });
3502 }
3503}
3504
3505fn authored_validation_rule_expression(
3506 expression: &presolve_parser::ParsedValidationRuleExpression,
3507 path: &Path,
3508) -> AuthoredValidationRuleExpression {
3509 let kind = match &expression.kind {
3510 ParsedValidationRuleExpressionKind::Call { callee, arguments } => {
3511 AuthoredValidationRuleExpressionKind::Call {
3512 callee: callee.clone(),
3513 arguments: arguments
3514 .iter()
3515 .map(|argument| AuthoredValidationRuleArgument {
3516 kind: match &argument.kind {
3517 ParsedValidationRuleArgumentKind::StringLiteral(value) => {
3518 AuthoredValidationRuleArgumentKind::StringLiteral(value.clone())
3519 }
3520 ParsedValidationRuleArgumentKind::Constant(constant) => {
3521 AuthoredValidationRuleArgumentKind::Constant(
3522 constant_expression_from_parsed(constant),
3523 )
3524 }
3525 ParsedValidationRuleArgumentKind::ThisMember(designator) => {
3526 AuthoredValidationRuleArgumentKind::ThisMember {
3527 name: designator.member.clone(),
3528 name_provenance: SourceProvenance::new(
3529 path,
3530 designator.member_span,
3531 ),
3532 }
3533 }
3534 ParsedValidationRuleArgumentKind::Unsupported => {
3535 AuthoredValidationRuleArgumentKind::Unsupported
3536 }
3537 },
3538 provenance: SourceProvenance::new(path, argument.span),
3539 })
3540 .collect(),
3541 }
3542 }
3543 ParsedValidationRuleExpressionKind::Identifier(identifier) => {
3544 AuthoredValidationRuleExpressionKind::Identifier(identifier.clone())
3545 }
3546 ParsedValidationRuleExpressionKind::Unsupported => {
3547 AuthoredValidationRuleExpressionKind::Unsupported
3548 }
3549 };
3550 AuthoredValidationRuleExpression {
3551 kind,
3552 provenance: SourceProvenance::new(path, expression.span),
3553 }
3554}
3555
3556fn retain_non_property_form_field_candidates(
3557 candidates: &mut Vec<FormFieldDeclarationCandidate>,
3558 owner: Option<&SemanticId>,
3559 path: &Path,
3560 authored_name: &str,
3561 declaration_kind: AuthoredDeclarationKind,
3562 decorators: &[presolve_parser::ParsedDecorator],
3563 span: SourceSpan,
3564) {
3565 let field_decorator_count = decorators
3566 .iter()
3567 .filter(|decorator| decorator.name == "field")
3568 .count();
3569 for decorator in decorators
3570 .iter()
3571 .filter(|decorator| decorator.name == "field")
3572 {
3573 let (form_designator, unsupported_form_designator) =
3574 normalized_form_designator(decorator, path);
3575 let mut violations = vec![FormFieldDeclarationViolation::InvalidTarget {
3576 actual: declaration_kind,
3577 }];
3578 if owner.is_none() {
3579 violations.push(FormFieldDeclarationViolation::InvalidOwner);
3580 }
3581 if !decorator.is_invoked {
3582 violations.push(FormFieldDeclarationViolation::InvalidDecoratorInvocation);
3583 }
3584 if decorator.argument_count != 1 {
3585 violations.push(FormFieldDeclarationViolation::InvalidDecoratorArity {
3586 actual: decorator.argument_count,
3587 expected: 1,
3588 });
3589 }
3590 if field_decorator_count != 1 {
3591 violations.push(FormFieldDeclarationViolation::DuplicateFieldDecorator);
3592 }
3593 if form_designator.is_none() {
3594 violations.push(FormFieldDeclarationViolation::InvalidFormDesignator);
3595 }
3596 canonicalize_form_field_violations(&mut violations);
3597 candidates.push(FormFieldDeclarationCandidate {
3598 id: FormFieldDeclarationCandidateId::for_source_position(path, decorator.span.start),
3599 owner_component: owner.cloned(),
3600 declaration_field: None,
3601 authored_name: Some(authored_name.to_string()),
3602 field_id: None,
3603 decorator_invoked: decorator.is_invoked,
3604 decorator_argument_count: decorator.argument_count,
3605 decorator_argument_provenance: decorator
3606 .argument_spans
3607 .iter()
3608 .copied()
3609 .map(|argument| SourceProvenance::new(path, argument))
3610 .collect(),
3611 nested_path_segments: decorator
3612 .arguments
3613 .get(1)
3614 .and_then(Option::as_deref)
3615 .and_then(parse_static_form_field_path),
3616 form_designator,
3617 unsupported_form_designator,
3618 resolved_form: None,
3619 declaration_kind,
3620 is_static: false,
3621 declared_type: None,
3622 authority_type: None,
3623 semantic_type: None,
3624 type_assignment: None,
3625 initializer: None,
3626 conflicting_decorators: Vec::new(),
3627 decorator_provenance: SourceProvenance::new(path, decorator.span),
3628 name_provenance: None,
3629 initializer_provenance: None,
3630 provenance: SourceProvenance::new(path, span),
3631 violations,
3632 });
3633 }
3634}
3635
3636fn normalized_form_designator(
3637 decorator: &presolve_parser::ParsedDecorator,
3638 path: &Path,
3639) -> (
3640 Option<FormDesignatorFact>,
3641 Option<UnsupportedFormDesignatorFact>,
3642) {
3643 let designator = decorator
3644 .this_member_argument
3645 .as_ref()
3646 .map(|designator| FormDesignatorFact {
3647 authored_name: designator.member.clone(),
3648 provenance: SourceProvenance::new(path, designator.span),
3649 name_provenance: SourceProvenance::new(path, designator.member_span),
3650 })
3651 .or_else(|| {
3652 let span = *decorator.argument_spans.first()?;
3653 decorator
3654 .argument
3655 .as_ref()
3656 .filter(|name| form_designator_name_is_valid(name))
3657 .map(|name| FormDesignatorFact {
3658 authored_name: name.clone(),
3659 provenance: SourceProvenance::new(path, span),
3660 name_provenance: SourceProvenance::new(path, span),
3661 })
3662 });
3663 let unsupported =
3664 decorator
3665 .static_member_argument
3666 .as_ref()
3667 .map(|designator| UnsupportedFormDesignatorFact {
3668 object: designator.object.clone(),
3669 member: designator.member.clone(),
3670 provenance: SourceProvenance::new(path, designator.span),
3671 });
3672 (designator, unsupported)
3673}
3674
3675fn normalized_submission_form_designator(
3676 decorator: &presolve_parser::ParsedDecorator,
3677) -> Option<String> {
3678 decorator
3679 .this_member_argument
3680 .as_ref()
3681 .map(|designator| designator.member.clone())
3682 .or_else(|| {
3683 decorator
3684 .argument
3685 .as_ref()
3686 .filter(|name| form_designator_name_is_valid(name))
3687 .cloned()
3688 })
3689}
3690
3691fn form_designator_name_is_valid(name: &str) -> bool {
3692 let mut characters = name.chars();
3693 matches!(characters.next(), Some(character) if character == '_' || character == '$' || character.is_ascii_alphabetic())
3694 && characters.all(|character| {
3695 character == '_' || character == '$' || character.is_ascii_alphanumeric()
3696 })
3697}
3698
3699fn parse_static_form_field_path(path: &str) -> Option<Vec<String>> {
3700 let segments = path.split('.').map(str::to_string).collect::<Vec<_>>();
3701 ((1..=16).contains(&segments.len())
3702 && segments.iter().all(|segment| {
3703 let mut characters = segment.chars();
3704 matches!(characters.next(), Some(character) if character == '_' || character.is_ascii_alphabetic())
3705 && characters.all(|character| character == '_' || character.is_ascii_alphanumeric())
3706 }))
3707 .then_some(segments)
3708}
3709
3710fn canonicalize_form_field_violations(violations: &mut Vec<FormFieldDeclarationViolation>) {
3711 violations.sort_by_key(form_field_declaration_violation_rank);
3712 violations.dedup();
3713}
3714
3715fn form_field_declaration_violation_rank(violation: &FormFieldDeclarationViolation) -> u8 {
3716 match violation {
3717 FormFieldDeclarationViolation::InvalidOwner => 0,
3718 FormFieldDeclarationViolation::InvalidTarget { .. }
3719 | FormFieldDeclarationViolation::StaticField
3720 | FormFieldDeclarationViolation::InheritedDeclaration
3721 | FormFieldDeclarationViolation::UnsupportedFieldName => 1,
3722 FormFieldDeclarationViolation::InvalidDecoratorInvocation
3723 | FormFieldDeclarationViolation::InvalidDecoratorArity { .. }
3724 | FormFieldDeclarationViolation::DuplicateFieldDecorator
3725 | FormFieldDeclarationViolation::InvalidPath => 2,
3726 FormFieldDeclarationViolation::InvalidFormDesignator
3727 | FormFieldDeclarationViolation::UnresolvedForm
3728 | FormFieldDeclarationViolation::InvalidForm
3729 | FormFieldDeclarationViolation::CrossComponentForm => 3,
3730 FormFieldDeclarationViolation::MissingInitializer
3731 | FormFieldDeclarationViolation::UnsupportedInitializer => 4,
3732 FormFieldDeclarationViolation::InvalidDeclaredType
3733 | FormFieldDeclarationViolation::InitializerTypeMismatch
3734 | FormFieldDeclarationViolation::NonSerializableType => 5,
3735 FormFieldDeclarationViolation::ConflictingSemanticDecorator => 6,
3736 FormFieldDeclarationViolation::DuplicateName
3737 | FormFieldDeclarationViolation::ConflictingPath => 7,
3738 }
3739}
3740
3741fn form_field_candidate_source_order(
3742 left: &FormFieldDeclarationCandidate,
3743 right: &FormFieldDeclarationCandidate,
3744) -> std::cmp::Ordering {
3745 (
3746 left.provenance.path.as_path(),
3747 left.provenance.span.start,
3748 left.provenance.span.end,
3749 left.id.as_str(),
3750 )
3751 .cmp(&(
3752 right.provenance.path.as_path(),
3753 right.provenance.span.start,
3754 right.provenance.span.end,
3755 right.id.as_str(),
3756 ))
3757}
3758
3759fn is_presolve_semantic_decorator(name: &str) -> bool {
3760 matches!(
3761 name,
3762 "form"
3763 | "state"
3764 | "context"
3765 | "provide"
3766 | "provider"
3767 | "consume"
3768 | "consumer"
3769 | "computed"
3770 | "effect"
3771 | "action"
3772 | "resource"
3773 | "slot"
3774 | "field"
3775 | "submit"
3776 | "validate"
3777 | "opaque"
3778 )
3779}
3780
3781fn is_validation_intrinsic_name(name: &str) -> bool {
3782 matches!(
3783 name,
3784 "required"
3785 | "min"
3786 | "max"
3787 | "minLength"
3788 | "maxLength"
3789 | "pattern"
3790 | "email"
3791 | "equals"
3792 | "notEquals"
3793 )
3794}
3795
3796fn form_declaration_status(violations: Vec<FormDeclarationViolation>) -> FormDeclarationStatus {
3797 if violations.is_empty() {
3798 FormDeclarationStatus::Valid
3799 } else {
3800 FormDeclarationStatus::Invalid(violations)
3801 }
3802}
3803
3804fn canonicalize_form_violations(violations: &mut Vec<FormDeclarationViolation>) {
3805 violations.sort_by_key(form_declaration_violation_rank);
3806 violations.dedup();
3807}
3808
3809fn form_declaration_violation_rank(violation: &FormDeclarationViolation) -> u8 {
3810 match violation {
3811 FormDeclarationViolation::InvalidOwner => 0,
3812 FormDeclarationViolation::InvalidTarget { .. }
3813 | FormDeclarationViolation::UnsupportedFieldName
3814 | FormDeclarationViolation::InheritedDeclaration => 1,
3815 FormDeclarationViolation::InvalidDecoratorInvocation
3816 | FormDeclarationViolation::InvalidDecoratorArity { .. }
3817 | FormDeclarationViolation::DuplicateFormDecorator => 2,
3818 FormDeclarationViolation::StaticField => 3,
3819 FormDeclarationViolation::InitializedField
3820 | FormDeclarationViolation::DeclarationOnlyRequired => 4,
3821 FormDeclarationViolation::MissingType | FormDeclarationViolation::InvalidType { .. } => 5,
3822 FormDeclarationViolation::ConflictingSemanticDecorator => 6,
3823 FormDeclarationViolation::DuplicateName => 7,
3824 }
3825}
3826
3827#[allow(clippy::too_many_lines)]
3828fn context_declaration_candidates_from_class(
3829 class: &ParsedClass,
3830 path: &Path,
3831 component: &SemanticId,
3832) -> Vec<AuthoredContextDeclarationCandidate> {
3833 let mut candidates = Vec::new();
3834 for property in &class.properties {
3835 for decorator in property.decorators.iter().filter(|decorator| {
3836 matches!(decorator.name.as_str(), "context" | "provide" | "consume")
3837 }) {
3838 let kind = match decorator.name.as_str() {
3839 "context" => ContextDeclarationCandidateKind::Context,
3840 "provide" => ContextDeclarationCandidateKind::Provider,
3841 "consume" => ContextDeclarationCandidateKind::Consumer,
3842 _ => unreachable!("filtered Context decorator"),
3843 };
3844 let declaration_kind = if property.is_static {
3845 AuthoredDeclarationKind::StaticField
3846 } else {
3847 AuthoredDeclarationKind::InstanceField
3848 };
3849 let mut violations = Vec::new();
3850 let expected_arity = usize::from(kind != ContextDeclarationCandidateKind::Context);
3851 if decorator.argument_count != expected_arity {
3852 violations.push(ContextDeclarationViolation::DecoratorArity {
3853 actual: decorator.argument_count,
3854 expected: expected_arity,
3855 });
3856 }
3857 if property.is_static && kind != ContextDeclarationCandidateKind::Context {
3858 violations.push(ContextDeclarationViolation::StaticDeclarationUnsupported);
3859 }
3860 let has_conflict = property.decorators.iter().any(|other| {
3861 other.name != decorator.name
3862 && matches!(
3863 other.name.as_str(),
3864 "state"
3865 | "context"
3866 | "provide"
3867 | "consume"
3868 | "computed"
3869 | "effect"
3870 | "action"
3871 | "resource"
3872 | "slot"
3873 | "form"
3874 | "field"
3875 )
3876 });
3877 if has_conflict {
3878 violations.push(ContextDeclarationViolation::ConflictingSemanticDecorators);
3879 }
3880 let declared_type =
3881 property
3882 .type_annotation
3883 .as_ref()
3884 .map(|annotation| DeclaredStateType {
3885 kind: declared_state_type_kind(&annotation.text),
3886 text: annotation.text.clone(),
3887 provenance: SourceProvenance::new(path, annotation.span),
3888 });
3889 if declared_type.is_none() {
3890 violations.push(ContextDeclarationViolation::MissingDeclaredType);
3891 }
3892 let designator = context_designator_from_decorator(decorator, path);
3893 match kind {
3894 ContextDeclarationCandidateKind::Context => {
3895 if property.initializer.is_some() && property.initializer_literal.is_none() {
3896 violations.push(ContextDeclarationViolation::UnsupportedInitializer);
3897 }
3898 }
3899 ContextDeclarationCandidateKind::Provider => {
3900 if decorator.argument_count == 1 && designator.is_none() {
3901 violations.push(ContextDeclarationViolation::ContextDesignatorUnsupported);
3902 }
3903 if property.initializer.is_none() {
3904 violations.push(ContextDeclarationViolation::MissingInitializer);
3905 } else if property.initializer_expression.is_none() {
3906 violations.push(ContextDeclarationViolation::UnsupportedInitializer);
3907 }
3908 }
3909 ContextDeclarationCandidateKind::Consumer => {
3910 if decorator.argument_count == 1 && designator.is_none() {
3911 violations.push(ContextDeclarationViolation::ContextDesignatorUnsupported);
3912 }
3913 if property.initializer.is_some() {
3914 violations.push(ContextDeclarationViolation::ForbiddenInitializer);
3915 }
3916 if !property.is_definite_assignment {
3917 violations.push(ContextDeclarationViolation::DefiniteAssignmentRequired);
3918 }
3919 }
3920 }
3921 candidates.push(AuthoredContextDeclarationCandidate {
3922 id: ContextDeclarationCandidateId::for_component_position(
3923 component,
3924 decorator.span.start,
3925 ),
3926 kind,
3927 owner_component: component.clone(),
3928 authored_declaration: component
3929 .context_declaration_candidate(property.name_span.start),
3930 declaration_kind,
3931 field_name: Some(property.name.clone()),
3932 declared_type,
3933 context_designator: designator,
3934 decorator_argument_count: decorator.argument_count,
3935 decorator_provenance: SourceProvenance::new(path, decorator.span),
3936 provenance: SourceProvenance::new(path, property.span),
3937 static_modifier_provenance: property
3938 .is_static
3939 .then(|| SourceProvenance::new(path, property.span)),
3940 initializer_provenance: property
3941 .initializer_span
3942 .map(|span| SourceProvenance::new(path, span)),
3943 violations,
3944 });
3945 }
3946 }
3947 for method in &class.methods {
3948 for decorator in method.decorators.iter().filter(|decorator| {
3949 matches!(decorator.name.as_str(), "context" | "provide" | "consume")
3950 }) {
3951 let kind = match decorator.name.as_str() {
3952 "context" => ContextDeclarationCandidateKind::Context,
3953 "provide" => ContextDeclarationCandidateKind::Provider,
3954 "consume" => ContextDeclarationCandidateKind::Consumer,
3955 _ => unreachable!("filtered Context decorator"),
3956 };
3957 let expected_arity = usize::from(kind != ContextDeclarationCandidateKind::Context);
3958 let mut violations = vec![ContextDeclarationViolation::InvalidDeclarationKind {
3959 actual: if method.is_getter {
3960 AuthoredDeclarationKind::Getter
3961 } else {
3962 AuthoredDeclarationKind::Method
3963 },
3964 }];
3965 if decorator.argument_count != expected_arity {
3966 violations.push(ContextDeclarationViolation::DecoratorArity {
3967 actual: decorator.argument_count,
3968 expected: expected_arity,
3969 });
3970 }
3971 candidates.push(AuthoredContextDeclarationCandidate {
3972 id: ContextDeclarationCandidateId::for_component_position(
3973 component,
3974 decorator.span.start,
3975 ),
3976 kind,
3977 owner_component: component.clone(),
3978 authored_declaration: component.method(&method.name),
3979 declaration_kind: if method.is_getter {
3980 AuthoredDeclarationKind::Getter
3981 } else {
3982 AuthoredDeclarationKind::Method
3983 },
3984 field_name: None,
3985 declared_type: None,
3986 context_designator: decorator
3987 .static_member_argument
3988 .as_ref()
3989 .map(|value| context_designator_from_parsed(value, path)),
3990 decorator_argument_count: decorator.argument_count,
3991 decorator_provenance: SourceProvenance::new(path, decorator.span),
3992 provenance: SourceProvenance::new(path, method.span),
3993 static_modifier_provenance: None,
3994 initializer_provenance: None,
3995 violations,
3996 });
3997 }
3998 }
3999 candidates.sort_by(|left, right| left.id.cmp(&right.id));
4000 candidates
4001}
4002
4003#[allow(clippy::too_many_lines)]
4004fn slot_declaration_candidates_from_class(
4005 class: &ParsedClass,
4006 path: &Path,
4007 component: &SemanticId,
4008) -> Vec<AuthoredSlotDeclarationCandidate> {
4009 let mut candidates = Vec::new();
4010
4011 for property in &class.properties {
4012 let legacy_decorators = property
4013 .decorators
4014 .iter()
4015 .filter(|decorator| decorator.name == "slot")
4016 .collect::<Vec<_>>();
4017 let declaration_sources = legacy_decorators
4018 .into_iter()
4019 .map(|decorator| (decorator.span, decorator.argument_count, true));
4020 for (declaration_span, argument_count, legacy_declaration) in declaration_sources {
4021 let declaration_kind = if property.is_static {
4022 AuthoredDeclarationKind::StaticField
4023 } else {
4024 AuthoredDeclarationKind::InstanceField
4025 };
4026 let mut violations = Vec::new();
4027 if argument_count != 0 {
4028 violations.push(SlotDeclarationViolation::DecoratorArity {
4029 actual: argument_count,
4030 expected: 0,
4031 });
4032 }
4033 if property.is_static {
4034 violations.push(SlotDeclarationViolation::StaticDeclarationUnsupported);
4035 }
4036 let has_conflict = property.initializer.as_deref() == Some("state(...)")
4037 || property.decorators.iter().any(|other| {
4038 (legacy_declaration || other.name != "slot")
4039 && matches!(
4040 other.name.as_str(),
4041 "context"
4042 | "provide"
4043 | "consume"
4044 | "computed"
4045 | "effect"
4046 | "action"
4047 | "resource"
4048 | "form"
4049 | "field"
4050 )
4051 });
4052 if has_conflict {
4053 violations.push(SlotDeclarationViolation::ConflictingSemanticDecorators);
4054 }
4055 let declared_type =
4056 property
4057 .type_annotation
4058 .as_ref()
4059 .map(|annotation| DeclaredStateType {
4060 kind: declared_state_type_kind(&annotation.text),
4061 text: annotation.text.clone(),
4062 provenance: SourceProvenance::new(path, annotation.span),
4063 });
4064 if declared_type
4065 .as_ref()
4066 .is_none_or(|declared| declared.text != "SlotContent")
4067 {
4068 violations.push(SlotDeclarationViolation::InvalidDeclaredType {
4069 actual: declared_type.as_ref().map(|declared| declared.text.clone()),
4070 });
4071 }
4072 if legacy_declaration && property.initializer.is_some() {
4073 violations.push(SlotDeclarationViolation::ForbiddenInitializer);
4074 }
4075 if legacy_declaration && !property.is_definite_assignment {
4076 violations.push(SlotDeclarationViolation::DefiniteAssignmentRequired);
4077 }
4078 candidates.push(AuthoredSlotDeclarationCandidate {
4079 id: SlotDeclarationCandidateId::for_component_position(
4080 component,
4081 declaration_span.start,
4082 ),
4083 owner_component: component.clone(),
4084 authored_declaration: component.slot_field(&property.name),
4085 declaration_kind,
4086 field_name: Some(property.name.clone()),
4087 declared_type,
4088 decorator_argument_count: argument_count,
4089 decorator_provenance: SourceProvenance::new(path, declaration_span),
4090 name_provenance: Some(SourceProvenance::new(path, property.name_span)),
4091 provenance: SourceProvenance::new(path, property.span),
4092 static_modifier_provenance: property
4093 .is_static
4094 .then(|| SourceProvenance::new(path, property.span)),
4095 initializer_provenance: property
4096 .initializer_span
4097 .map(|span| SourceProvenance::new(path, span)),
4098 violations,
4099 });
4100 }
4101 }
4102
4103 for method in &class.methods {
4104 retain_invalid_slot_candidate(
4105 &mut candidates,
4106 component,
4107 &component.method(&method.name),
4108 if method.is_getter {
4109 AuthoredDeclarationKind::Getter
4110 } else if method.is_setter {
4111 AuthoredDeclarationKind::Setter
4112 } else {
4113 AuthoredDeclarationKind::Method
4114 },
4115 None,
4116 &method.decorators,
4117 path,
4118 method.span,
4119 );
4120 for (index, parameter) in method.parameters.iter().enumerate() {
4121 retain_invalid_slot_candidate(
4122 &mut candidates,
4123 component,
4124 &component
4125 .method(&method.name)
4126 .parameter(¶meter.name, index),
4127 AuthoredDeclarationKind::Parameter,
4128 Some(¶meter.name),
4129 ¶meter.decorators,
4130 path,
4131 parameter.span,
4132 );
4133 }
4134 }
4135
4136 let mut counts = BTreeMap::<String, usize>::new();
4137 for name in candidates
4138 .iter()
4139 .filter_map(|candidate| candidate.field_name.as_ref())
4140 {
4141 *counts.entry(name.clone()).or_default() += 1;
4142 }
4143 for candidate in &mut candidates {
4144 if candidate
4145 .field_name
4146 .as_ref()
4147 .is_some_and(|name| counts[name] > 1)
4148 {
4149 candidate
4150 .violations
4151 .push(SlotDeclarationViolation::DuplicateSlot);
4152 }
4153 }
4154 candidates.sort_by(|left, right| left.id.cmp(&right.id));
4155 candidates
4156}
4157
4158#[allow(clippy::too_many_arguments)]
4159fn retain_invalid_slot_candidate(
4160 candidates: &mut Vec<AuthoredSlotDeclarationCandidate>,
4161 component: &SemanticId,
4162 authored_declaration: &SemanticId,
4163 declaration_kind: AuthoredDeclarationKind,
4164 field_name: Option<&str>,
4165 decorators: &[presolve_parser::ParsedDecorator],
4166 path: &Path,
4167 span: SourceSpan,
4168) {
4169 for decorator in decorators
4170 .iter()
4171 .filter(|decorator| decorator.name == "slot")
4172 {
4173 let mut violations = vec![SlotDeclarationViolation::InvalidDeclarationKind {
4174 actual: declaration_kind,
4175 }];
4176 if decorator.argument_count != 0 {
4177 violations.push(SlotDeclarationViolation::DecoratorArity {
4178 actual: decorator.argument_count,
4179 expected: 0,
4180 });
4181 }
4182 candidates.push(AuthoredSlotDeclarationCandidate {
4183 id: SlotDeclarationCandidateId::for_component_position(component, decorator.span.start),
4184 owner_component: component.clone(),
4185 authored_declaration: authored_declaration.clone(),
4186 declaration_kind,
4187 field_name: field_name.map(str::to_string),
4188 declared_type: None,
4189 decorator_argument_count: decorator.argument_count,
4190 decorator_provenance: SourceProvenance::new(path, decorator.span),
4191 name_provenance: field_name.map(|_| SourceProvenance::new(path, span)),
4192 provenance: SourceProvenance::new(path, span),
4193 static_modifier_provenance: None,
4194 initializer_provenance: None,
4195 violations,
4196 });
4197 }
4198}
4199
4200fn slot_declarations_from_candidates(
4201 candidates: &[AuthoredSlotDeclarationCandidate],
4202) -> Vec<SlotDeclaration> {
4203 candidates
4204 .iter()
4205 .filter(|candidate| candidate.violations.is_empty())
4206 .map(|candidate| {
4207 let name = candidate
4208 .field_name
4209 .clone()
4210 .expect("valid Slot candidates are fields");
4211 SlotDeclaration {
4212 authored_field: candidate.authored_declaration.clone(),
4213 kind: if name == "children" {
4214 SlotKind::Default
4215 } else {
4216 SlotKind::Named
4217 },
4218 name,
4219 declared_type: candidate
4220 .declared_type
4221 .clone()
4222 .expect("valid Slot candidates have a declared type"),
4223 decorator_provenance: candidate.decorator_provenance.clone(),
4224 name_provenance: candidate
4225 .name_provenance
4226 .clone()
4227 .expect("valid Slot candidates retain field-name provenance"),
4228 provenance: candidate.provenance.clone(),
4229 }
4230 })
4231 .collect()
4232}
4233
4234fn component_method_from_parsed(
4235 method: &ParsedMethod,
4236 path: &Path,
4237 component_id: &SemanticId,
4238) -> ComponentMethod {
4239 let id = component_id.method(&method.name);
4240 let semantic_role = method_semantic_role(method);
4241 let computed_expression = match semantic_role {
4242 MethodSemanticRole::Computed => method
4243 .computed_expression
4244 .as_ref()
4245 .map(computed_expression_from_parsed),
4246 MethodSemanticRole::Standard | MethodSemanticRole::Effect | MethodSemanticRole::Action => {
4247 None
4248 }
4249 };
4250 ComponentMethod {
4251 id: id.clone(),
4252 owner: SemanticOwner::entity(component_id.clone()),
4253 name: method.name.clone(),
4254 is_getter: method.is_getter,
4255 is_async: method.is_async,
4256 is_static: method.is_static,
4257 decorators: method
4258 .decorators
4259 .iter()
4260 .map(|decorator| decorator.name.clone())
4261 .collect(),
4262 semantic_role,
4263 local_variables: method
4264 .local_variables
4265 .iter()
4266 .enumerate()
4267 .map(|(index, local)| MethodLocalVariable {
4268 id: id.local_variable(&local.name, index),
4269 owner: SemanticOwner::entity(id.clone()),
4270 name: local.name.clone(),
4271 value: serializable_value_from_parsed(&local.value),
4272 span: local.span,
4273 })
4274 .collect(),
4275 parameters: method
4276 .parameters
4277 .iter()
4278 .enumerate()
4279 .map(|(index, parameter)| MethodParameter {
4280 id: id.parameter(¶meter.name, index),
4281 owner: SemanticOwner::entity(id.clone()),
4282 name: parameter.name.clone(),
4283 span: parameter.span,
4284 declared_type: parameter.type_annotation.as_ref().map(|annotation| {
4285 DeclaredStateType {
4286 kind: declared_state_type_kind(&annotation.text),
4287 text: annotation.text.clone(),
4288 provenance: SourceProvenance::new(path, annotation.span),
4289 }
4290 }),
4291 })
4292 .collect(),
4293 declared_return_type: method.return_type_annotation.as_ref().map(|annotation| {
4294 DeclaredStateType {
4295 kind: declared_state_type_kind(&annotation.text),
4296 text: annotation.text.clone(),
4297 provenance: SourceProvenance::new(path, annotation.span),
4298 }
4299 }),
4300 return_values: method
4301 .return_values
4302 .iter()
4303 .map(serializable_value_from_parsed)
4304 .collect(),
4305 computed_expression,
4306 effect_body: method.effect_body.as_ref().map(effect_body_from_parsed),
4307 calls: method.calls.iter().map(method_call_from_parsed).collect(),
4308 }
4309}
4310
4311fn method_call_from_parsed(call: &ParsedMethodCall) -> MethodCall {
4312 MethodCall {
4313 callee: call.callee.clone(),
4314 span: call.span,
4315 }
4316}
4317
4318fn method_semantic_role(method: &ParsedMethod) -> MethodSemanticRole {
4319 if method.is_getter
4320 && method
4321 .decorators
4322 .iter()
4323 .any(|decorator| decorator.name == "computed")
4324 {
4325 MethodSemanticRole::Computed
4326 } else if method
4327 .decorators
4328 .iter()
4329 .any(|decorator| decorator.name == "effect")
4330 {
4331 MethodSemanticRole::Effect
4332 } else if method
4333 .decorators
4334 .iter()
4335 .any(|decorator| decorator.name == "action")
4336 {
4337 MethodSemanticRole::Action
4338 } else {
4339 MethodSemanticRole::Standard
4340 }
4341}
4342
4343fn state_fields_from_class(class: &ParsedClass, path: &Path, id: &SemanticId) -> Vec<StateField> {
4344 class
4345 .properties
4346 .iter()
4347 .filter(|property| {
4348 property.initializer.as_deref() == Some("state(...)")
4349 && !property.decorators.iter().any(|decorator| {
4350 matches!(
4351 decorator.name.as_str(),
4352 "context" | "provide" | "consume" | "slot" | "form" | "field"
4353 )
4354 })
4355 })
4356 .map(|property| {
4357 let initial_expression = property
4358 .state_initial_expression
4359 .as_ref()
4360 .map(constant_expression_from_parsed);
4361 let initial_value = property
4362 .state_initial_value
4363 .as_ref()
4364 .map(serializable_value_from_parsed);
4365
4366 StateField {
4367 id: id.state_field(&property.name),
4368 owner: SemanticOwner::entity(id.clone()),
4369 name: property.name.clone(),
4370 initial_value,
4371 initial_expression,
4372 declared_type: property.state_type_annotation.as_ref().map(|annotation| {
4373 DeclaredStateType {
4374 text: annotation.text.clone(),
4375 provenance: SourceProvenance::new(path, annotation.span),
4376 kind: declared_state_type_kind(&annotation.text),
4377 }
4378 }),
4379 }
4380 })
4381 .collect()
4382}
4383
4384fn resource_declaration_candidates_from_class(
4385 class: &ParsedClass,
4386 path: &Path,
4387 component_id: &SemanticId,
4388) -> Vec<AuthoredResourceDeclarationFact> {
4389 class
4390 .properties
4391 .iter()
4392 .filter_map(|property| {
4393 let decorator = property
4394 .decorators
4395 .iter()
4396 .find(|decorator| decorator.name == "resource")?;
4397 Some(AuthoredResourceDeclarationFact {
4398 owner_component: component_id.clone(),
4399 field: property.name.clone(),
4400 decorator_invoked: decorator.is_invoked,
4401 decorator_argument_count: decorator.argument_count,
4402 endpoint_designator: decorator.argument.clone(),
4403 declared_type: property.type_annotation.as_ref().map(|annotation| {
4404 DeclaredStateType {
4405 text: annotation.text.clone(),
4406 provenance: SourceProvenance::new(path, annotation.span),
4407 kind: declared_state_type_kind(&annotation.text),
4408 }
4409 }),
4410 provenance: SourceProvenance::new(path, property.span),
4411 })
4412 })
4413 .collect()
4414}
4415
4416fn route_loader_declaration_candidates_from_class(
4417 class: &ParsedClass,
4418 path: &Path,
4419 component_id: &SemanticId,
4420) -> Vec<AuthoredRouteLoaderDeclarationFact> {
4421 class
4422 .properties
4423 .iter()
4424 .filter_map(|property| {
4425 let decorator = property
4426 .decorators
4427 .iter()
4428 .find(|decorator| decorator.name == "loader")?;
4429 Some(AuthoredRouteLoaderDeclarationFact {
4430 owner_component: component_id.clone(),
4431 field: property.name.clone(),
4432 decorator_invoked: decorator.is_invoked,
4433 decorator_argument_count: decorator.argument_count,
4434 endpoint_designator: decorator.argument.clone(),
4435 declared_type: property.type_annotation.as_ref().map(|annotation| {
4436 DeclaredStateType {
4437 text: annotation.text.clone(),
4438 provenance: SourceProvenance::new(path, annotation.span),
4439 kind: declared_state_type_kind(&annotation.text),
4440 }
4441 }),
4442 provenance: SourceProvenance::new(path, property.span),
4443 })
4444 })
4445 .collect()
4446}
4447
4448fn context_declarations_from_class(
4449 class: &ParsedClass,
4450 path: &Path,
4451 id: &SemanticId,
4452) -> Vec<ContextDeclaration> {
4453 class
4454 .properties
4455 .iter()
4456 .filter_map(|property| {
4457 let decorator = property
4458 .decorators
4459 .iter()
4460 .find(|decorator| decorator.name == "context")?;
4461 let declared_type = property.type_annotation.as_ref()?;
4462
4463 (decorator.argument_count == 0
4464 && !property.decorators.iter().any(|decorator| {
4465 matches!(
4466 decorator.name.as_str(),
4467 "provide" | "consume" | "slot" | "form" | "field"
4468 )
4469 })
4470 && property
4471 .initializer
4472 .as_ref()
4473 .is_none_or(|_| property.initializer_literal.is_some()))
4474 .then(|| ContextDeclaration {
4475 authored_field: id.context_field(&property.name),
4476 name: property.name.clone(),
4477 declared_type: DeclaredStateType {
4478 kind: declared_state_type_kind(&declared_type.text),
4479 text: declared_type.text.clone(),
4480 provenance: SourceProvenance::new(path, declared_type.span),
4481 },
4482 default_expression: property.initializer_literal.as_ref().map(|value| {
4483 ConstantExpression {
4484 kind: ConstantExpressionKind::Literal(serializable_value_from_parsed(
4485 value,
4486 )),
4487 span: property
4488 .initializer_span
4489 .expect("literal context defaults should retain a source span"),
4490 }
4491 }),
4492 decorator_provenance: SourceProvenance::new(path, decorator.span),
4493 name_provenance: SourceProvenance::new(path, property.name_span),
4494 provenance: SourceProvenance::new(path, property.span),
4495 })
4496 })
4497 .collect()
4498}
4499
4500fn provider_declarations_from_class(
4501 class: &ParsedClass,
4502 path: &Path,
4503 id: &SemanticId,
4504) -> Vec<ProviderDeclaration> {
4505 class
4506 .properties
4507 .iter()
4508 .filter_map(|property| {
4509 let decorator = property
4510 .decorators
4511 .iter()
4512 .find(|decorator| decorator.name == "provide")?;
4513 let designator = context_designator_from_decorator(decorator, path)?;
4514 let declared_type = property.type_annotation.as_ref()?;
4515 let value_expression = property.initializer_expression.as_ref()?;
4516
4517 (decorator.argument_count == 1
4518 && !property.is_static
4519 && !property.decorators.iter().any(|decorator| {
4520 matches!(
4521 decorator.name.as_str(),
4522 "context" | "consume" | "slot" | "form" | "field"
4523 )
4524 }))
4525 .then(|| ProviderDeclaration {
4526 authored_field: id.provider_field(&property.name),
4527 name: property.name.clone(),
4528 context_designator: designator,
4529 declared_type: DeclaredStateType {
4530 kind: declared_state_type_kind(&declared_type.text),
4531 text: declared_type.text.clone(),
4532 provenance: SourceProvenance::new(path, declared_type.span),
4533 },
4534 value_expression: computed_expression_from_parsed(value_expression),
4535 decorator_provenance: SourceProvenance::new(path, decorator.span),
4536 name_provenance: SourceProvenance::new(path, property.name_span),
4537 provenance: SourceProvenance::new(path, property.span),
4538 })
4539 })
4540 .collect()
4541}
4542
4543fn consumer_declarations_from_class(
4544 class: &ParsedClass,
4545 path: &Path,
4546 id: &SemanticId,
4547) -> Vec<ConsumerDeclaration> {
4548 class
4549 .properties
4550 .iter()
4551 .filter_map(|property| {
4552 let decorator = property
4553 .decorators
4554 .iter()
4555 .find(|decorator| decorator.name == "consume")?;
4556 let designator = context_designator_from_decorator(decorator, path)?;
4557 let requested_type = property.type_annotation.as_ref()?;
4558 let has_conflicting_decorator = property.decorators.iter().any(|decorator| {
4559 matches!(
4560 decorator.name.as_str(),
4561 "state"
4562 | "context"
4563 | "provide"
4564 | "computed"
4565 | "effect"
4566 | "action"
4567 | "resource"
4568 | "slot"
4569 | "form"
4570 | "field"
4571 )
4572 });
4573
4574 (decorator.argument_count == 1
4575 && !property.is_static
4576 && property.is_definite_assignment
4577 && property.initializer.is_none()
4578 && !has_conflicting_decorator)
4579 .then(|| ConsumerDeclaration {
4580 authored_field: id.consumer_field(&property.name),
4581 name: property.name.clone(),
4582 context_designator: designator,
4583 requested_type: DeclaredStateType {
4584 kind: declared_state_type_kind(&requested_type.text),
4585 text: requested_type.text.clone(),
4586 provenance: SourceProvenance::new(path, requested_type.span),
4587 },
4588 decorator_provenance: SourceProvenance::new(path, decorator.span),
4589 name_provenance: SourceProvenance::new(path, property.name_span),
4590 provenance: SourceProvenance::new(path, property.span),
4591 })
4592 })
4593 .collect()
4594}
4595
4596fn context_designator_from_parsed(
4597 designator: &ParsedStaticMemberDesignator,
4598 path: &Path,
4599) -> ContextDesignator {
4600 ContextDesignator {
4601 component_symbol: designator.object.clone(),
4602 context_member: designator.member.clone(),
4603 provenance: SourceProvenance::new(path, designator.span),
4604 component_provenance: SourceProvenance::new(path, designator.object_span),
4605 member_provenance: SourceProvenance::new(path, designator.member_span),
4606 }
4607}
4608
4609fn context_designator_from_decorator(
4610 decorator: &ParsedDecorator,
4611 path: &Path,
4612) -> Option<ContextDesignator> {
4613 decorator
4614 .static_member_argument
4615 .as_ref()
4616 .map(|value| context_designator_from_parsed(value, path))
4617 .or_else(|| {
4618 let value = decorator.argument.as_deref()?;
4619 let (component_symbol, context_member) = value.split_once('.')?;
4620 if component_symbol.is_empty()
4621 || context_member.is_empty()
4622 || context_member.contains('.')
4623 || !context_designator_segment(component_symbol)
4624 || !context_designator_segment(context_member)
4625 {
4626 return None;
4627 }
4628 let provenance = SourceProvenance::new(path, *decorator.argument_spans.first()?);
4629 Some(ContextDesignator {
4630 component_symbol: component_symbol.to_string(),
4631 context_member: context_member.to_string(),
4632 provenance: provenance.clone(),
4633 component_provenance: provenance.clone(),
4634 member_provenance: provenance,
4635 })
4636 })
4637}
4638
4639fn context_designator_segment(value: &str) -> bool {
4640 let mut characters = value.chars();
4641 matches!(characters.next(), Some(character) if character == '_' || character == '$' || character.is_ascii_alphabetic())
4642 && characters.all(|character| {
4643 character == '_' || character == '$' || character.is_ascii_alphanumeric()
4644 })
4645}
4646
4647fn arithmetic_expression_from_parsed(
4648 expression: &ParsedArithmeticExpression,
4649) -> ArithmeticExpression {
4650 let kind = match &expression.kind {
4651 ParsedArithmeticExpressionKind::Number(value) => {
4652 ArithmeticExpressionKind::Number(value.clone())
4653 }
4654 ParsedArithmeticExpressionKind::Binary {
4655 operator,
4656 left,
4657 right,
4658 } => ArithmeticExpressionKind::Binary {
4659 operator: arithmetic_operator_from_parsed(*operator),
4660 left: Box::new(arithmetic_expression_from_parsed(left)),
4661 right: Box::new(arithmetic_expression_from_parsed(right)),
4662 },
4663 };
4664
4665 ArithmeticExpression {
4666 kind,
4667 span: expression.span,
4668 }
4669}
4670
4671fn constant_expression_from_parsed(expression: &ParsedConstantExpression) -> ConstantExpression {
4672 let kind = match &expression.kind {
4673 ParsedConstantExpressionKind::Primitive(value) => {
4674 ConstantExpressionKind::Literal(serializable_value_from_parsed(value))
4675 }
4676 ParsedConstantExpressionKind::Boolean(value) => ConstantExpressionKind::Boolean(*value),
4677 ParsedConstantExpressionKind::Arithmetic(expression) => {
4678 ConstantExpressionKind::Arithmetic(arithmetic_expression_from_parsed(expression))
4679 }
4680 ParsedConstantExpressionKind::Comparison {
4681 operator,
4682 left,
4683 right,
4684 } => ConstantExpressionKind::Comparison {
4685 operator: comparison_operator_from_parsed(*operator),
4686 left: arithmetic_expression_from_parsed(left),
4687 right: arithmetic_expression_from_parsed(right),
4688 },
4689 ParsedConstantExpressionKind::Logical {
4690 operator,
4691 left,
4692 right,
4693 } => ConstantExpressionKind::Logical {
4694 operator: logical_operator_from_parsed(*operator),
4695 left: Box::new(constant_expression_from_parsed(left)),
4696 right: Box::new(constant_expression_from_parsed(right)),
4697 },
4698 ParsedConstantExpressionKind::NullishCoalescing { left, right } => {
4699 ConstantExpressionKind::NullishCoalescing {
4700 left: Box::new(constant_expression_from_parsed(left)),
4701 right: Box::new(constant_expression_from_parsed(right)),
4702 }
4703 }
4704 ParsedConstantExpressionKind::Unary { operator, operand } => {
4705 ConstantExpressionKind::Unary {
4706 operator: match operator {
4707 ParsedUnaryOperator::Not => UnaryOperator::Not,
4708 ParsedUnaryOperator::Plus => UnaryOperator::Plus,
4709 ParsedUnaryOperator::Minus => UnaryOperator::Minus,
4710 },
4711 operand: Box::new(constant_expression_from_parsed(operand)),
4712 }
4713 }
4714 };
4715
4716 ConstantExpression {
4717 kind,
4718 span: expression.span,
4719 }
4720}
4721
4722fn computed_expression_from_parsed(expression: &ParsedComputedExpression) -> ComputedExpression {
4723 let kind = match &expression.kind {
4724 ParsedComputedExpressionKind::Literal(value) => {
4725 ComputedExpressionKind::Literal(serializable_value_from_parsed(value))
4726 }
4727 ParsedComputedExpressionKind::ThisMember(name) => {
4728 ComputedExpressionKind::ThisMember(name.clone())
4729 }
4730 ParsedComputedExpressionKind::MemberAccess {
4731 object,
4732 property,
4733 optional,
4734 } => ComputedExpressionKind::MemberAccess {
4735 object: Box::new(computed_expression_from_parsed(object)),
4736 property: property.clone(),
4737 optional: *optional,
4738 },
4739 ParsedComputedExpressionKind::IndexAccess { object, index } => {
4740 ComputedExpressionKind::IndexAccess {
4741 object: Box::new(computed_expression_from_parsed(object)),
4742 index: Box::new(computed_expression_from_parsed(index)),
4743 }
4744 }
4745 ParsedComputedExpressionKind::Conditional {
4746 condition,
4747 when_true,
4748 when_false,
4749 } => ComputedExpressionKind::Conditional {
4750 condition: Box::new(computed_expression_from_parsed(condition)),
4751 when_true: Box::new(computed_expression_from_parsed(when_true)),
4752 when_false: Box::new(computed_expression_from_parsed(when_false)),
4753 },
4754 ParsedComputedExpressionKind::Template {
4755 quasis,
4756 expressions,
4757 } => ComputedExpressionKind::Template {
4758 quasis: quasis.clone(),
4759 expressions: expressions
4760 .iter()
4761 .map(computed_expression_from_parsed)
4762 .collect(),
4763 },
4764 ParsedComputedExpressionKind::Call { callee, arguments } => ComputedExpressionKind::Call {
4765 callee: callee.clone(),
4766 arguments: arguments
4767 .iter()
4768 .map(computed_expression_from_parsed)
4769 .collect(),
4770 },
4771 ParsedComputedExpressionKind::Arithmetic {
4772 left,
4773 right,
4774 operator,
4775 } => ComputedExpressionKind::Arithmetic {
4776 left: Box::new(computed_expression_from_parsed(left)),
4777 right: Box::new(computed_expression_from_parsed(right)),
4778 operator: arithmetic_operator_from_parsed(*operator),
4779 },
4780 ParsedComputedExpressionKind::Comparison {
4781 left,
4782 right,
4783 operator,
4784 } => ComputedExpressionKind::Comparison {
4785 left: Box::new(computed_expression_from_parsed(left)),
4786 right: Box::new(computed_expression_from_parsed(right)),
4787 operator: comparison_operator_from_parsed(*operator),
4788 },
4789 ParsedComputedExpressionKind::Logical {
4790 left,
4791 right,
4792 operator,
4793 } => ComputedExpressionKind::Logical {
4794 left: Box::new(computed_expression_from_parsed(left)),
4795 right: Box::new(computed_expression_from_parsed(right)),
4796 operator: logical_operator_from_parsed(*operator),
4797 },
4798 ParsedComputedExpressionKind::NullishCoalescing { left, right } => {
4799 ComputedExpressionKind::NullishCoalescing {
4800 left: Box::new(computed_expression_from_parsed(left)),
4801 right: Box::new(computed_expression_from_parsed(right)),
4802 }
4803 }
4804 ParsedComputedExpressionKind::Unary { operand, operator } => {
4805 ComputedExpressionKind::Unary {
4806 operand: Box::new(computed_expression_from_parsed(operand)),
4807 operator: match operator {
4808 ParsedUnaryOperator::Not => UnaryOperator::Not,
4809 ParsedUnaryOperator::Plus => UnaryOperator::Plus,
4810 ParsedUnaryOperator::Minus => UnaryOperator::Minus,
4811 },
4812 }
4813 }
4814 };
4815
4816 ComputedExpression {
4817 kind,
4818 span: expression.span,
4819 }
4820}
4821
4822fn effect_body_from_parsed(body: &ParsedEffectBody) -> EffectBodySyntax {
4823 EffectBodySyntax {
4824 statements: body
4825 .statements
4826 .iter()
4827 .map(|statement| EffectStatementSyntax {
4828 kind: effect_statement_syntax_kind_from_parsed(&statement.kind),
4829 span: statement.span,
4830 })
4831 .collect(),
4832 cleanup: body.cleanup.as_ref().map(|cleanup| EffectCleanupSyntax {
4833 is_async: cleanup.is_async,
4834 body: Box::new(effect_body_from_parsed(&cleanup.body)),
4835 span: cleanup.span,
4836 }),
4837 }
4838}
4839
4840fn effect_statement_syntax_kind_from_parsed(
4841 kind: &ParsedEffectStatementKind,
4842) -> EffectStatementSyntaxKind {
4843 match kind {
4844 ParsedEffectStatementKind::StaticMemberAssignment { target, value } => {
4845 EffectStatementSyntaxKind::StaticMemberAssignment {
4846 target: effect_expression_from_parsed(target),
4847 value: effect_expression_from_parsed(value),
4848 }
4849 }
4850 ParsedEffectStatementKind::CapabilityCall { callee, arguments } => {
4851 EffectStatementSyntaxKind::CapabilityCall {
4852 callee: effect_expression_from_parsed(callee),
4853 arguments: arguments
4854 .iter()
4855 .map(effect_expression_from_parsed)
4856 .collect(),
4857 }
4858 }
4859 ParsedEffectStatementKind::EffectReturn { value } => {
4860 EffectStatementSyntaxKind::EffectReturn {
4861 value: value.as_ref().map(effect_expression_from_parsed),
4862 }
4863 }
4864 ParsedEffectStatementKind::Empty => EffectStatementSyntaxKind::Empty,
4865 ParsedEffectStatementKind::Unsupported(kind) => EffectStatementSyntaxKind::Unsupported(
4866 unsupported_effect_statement_kind_from_parsed(*kind),
4867 ),
4868 }
4869}
4870
4871fn unsupported_effect_statement_kind_from_parsed(
4872 kind: ParsedUnsupportedEffectStatementKind,
4873) -> UnsupportedEffectStatementKind {
4874 match kind {
4875 ParsedUnsupportedEffectStatementKind::LocalDeclaration => {
4876 UnsupportedEffectStatementKind::LocalDeclaration
4877 }
4878 ParsedUnsupportedEffectStatementKind::Branch => UnsupportedEffectStatementKind::Branch,
4879 ParsedUnsupportedEffectStatementKind::Loop => UnsupportedEffectStatementKind::Loop,
4880 ParsedUnsupportedEffectStatementKind::NestedBlock => {
4881 UnsupportedEffectStatementKind::NestedBlock
4882 }
4883 ParsedUnsupportedEffectStatementKind::ExceptionHandling => {
4884 UnsupportedEffectStatementKind::ExceptionHandling
4885 }
4886 ParsedUnsupportedEffectStatementKind::AsyncOperation => {
4887 UnsupportedEffectStatementKind::AsyncOperation
4888 }
4889 ParsedUnsupportedEffectStatementKind::CompoundAssignment => {
4890 UnsupportedEffectStatementKind::CompoundAssignment
4891 }
4892 ParsedUnsupportedEffectStatementKind::CleanupReturnCandidate => {
4893 UnsupportedEffectStatementKind::CleanupReturnCandidate
4894 }
4895 ParsedUnsupportedEffectStatementKind::UnsupportedExpression => {
4896 UnsupportedEffectStatementKind::UnsupportedExpression
4897 }
4898 }
4899}
4900
4901fn effect_expression_from_parsed(expression: &ParsedEffectExpression) -> EffectExpression {
4902 let kind = match &expression.kind {
4903 ParsedEffectExpressionKind::Literal(value) => {
4904 EffectExpressionKind::Literal(serializable_value_from_parsed(value))
4905 }
4906 ParsedEffectExpressionKind::Identifier(name) => {
4907 EffectExpressionKind::Identifier(name.clone())
4908 }
4909 ParsedEffectExpressionKind::ThisMember(name) => {
4910 EffectExpressionKind::ThisMember(name.clone())
4911 }
4912 ParsedEffectExpressionKind::MemberAccess { object, property } => {
4913 EffectExpressionKind::MemberAccess {
4914 object: Box::new(effect_expression_from_parsed(object)),
4915 property: property.clone(),
4916 }
4917 }
4918 ParsedEffectExpressionKind::Arithmetic {
4919 left,
4920 right,
4921 operator,
4922 } => EffectExpressionKind::Arithmetic {
4923 left: Box::new(effect_expression_from_parsed(left)),
4924 right: Box::new(effect_expression_from_parsed(right)),
4925 operator: arithmetic_operator_from_parsed(*operator),
4926 },
4927 ParsedEffectExpressionKind::Comparison {
4928 left,
4929 right,
4930 operator,
4931 } => EffectExpressionKind::Comparison {
4932 left: Box::new(effect_expression_from_parsed(left)),
4933 right: Box::new(effect_expression_from_parsed(right)),
4934 operator: comparison_operator_from_parsed(*operator),
4935 },
4936 ParsedEffectExpressionKind::Logical {
4937 left,
4938 right,
4939 operator,
4940 } => EffectExpressionKind::Logical {
4941 left: Box::new(effect_expression_from_parsed(left)),
4942 right: Box::new(effect_expression_from_parsed(right)),
4943 operator: logical_operator_from_parsed(*operator),
4944 },
4945 ParsedEffectExpressionKind::NullishCoalescing { left, right } => {
4946 EffectExpressionKind::NullishCoalescing {
4947 left: Box::new(effect_expression_from_parsed(left)),
4948 right: Box::new(effect_expression_from_parsed(right)),
4949 }
4950 }
4951 ParsedEffectExpressionKind::Unary { operand, operator } => EffectExpressionKind::Unary {
4952 operand: Box::new(effect_expression_from_parsed(operand)),
4953 operator: match operator {
4954 ParsedUnaryOperator::Not => UnaryOperator::Not,
4955 ParsedUnaryOperator::Plus => UnaryOperator::Plus,
4956 ParsedUnaryOperator::Minus => UnaryOperator::Minus,
4957 },
4958 },
4959 };
4960 EffectExpression {
4961 kind,
4962 span: expression.span,
4963 }
4964}
4965
4966fn arithmetic_operator_from_parsed(operator: ParsedArithmeticOperator) -> ArithmeticOperator {
4967 match operator {
4968 ParsedArithmeticOperator::Add => ArithmeticOperator::Add,
4969 ParsedArithmeticOperator::Subtract => ArithmeticOperator::Subtract,
4970 ParsedArithmeticOperator::Multiply => ArithmeticOperator::Multiply,
4971 ParsedArithmeticOperator::Divide => ArithmeticOperator::Divide,
4972 ParsedArithmeticOperator::Remainder => ArithmeticOperator::Remainder,
4973 }
4974}
4975
4976fn comparison_operator_from_parsed(operator: ParsedComparisonOperator) -> ComparisonOperator {
4977 match operator {
4978 ParsedComparisonOperator::Equal => ComparisonOperator::Equal,
4979 ParsedComparisonOperator::NotEqual => ComparisonOperator::NotEqual,
4980 ParsedComparisonOperator::LessThan => ComparisonOperator::LessThan,
4981 ParsedComparisonOperator::LessThanOrEqual => ComparisonOperator::LessThanOrEqual,
4982 ParsedComparisonOperator::GreaterThan => ComparisonOperator::GreaterThan,
4983 ParsedComparisonOperator::GreaterThanOrEqual => ComparisonOperator::GreaterThanOrEqual,
4984 }
4985}
4986
4987fn logical_operator_from_parsed(operator: ParsedLogicalOperator) -> LogicalOperator {
4988 match operator {
4989 ParsedLogicalOperator::And => LogicalOperator::And,
4990 ParsedLogicalOperator::Or => LogicalOperator::Or,
4991 }
4992}
4993
4994fn declared_state_type_kind(text: &str) -> Option<DeclaredStateTypeKind> {
4995 match text {
4996 "string" => Some(DeclaredStateTypeKind::String),
4997 "number" => Some(DeclaredStateTypeKind::Number),
4998 "boolean" => Some(DeclaredStateTypeKind::Boolean),
4999 "null" => Some(DeclaredStateTypeKind::Null),
5000 _ => None,
5001 }
5002}
5003
5004fn render_model_from_parsed_method(
5005 method: &presolve_parser::ParsedMethod,
5006 component_id: &SemanticId,
5007) -> RenderModel {
5008 let root = method.jsx_roots.first();
5009 let root_element = root.and_then(parsed_root_element);
5010 let root_fragment = root.and_then(parsed_root_fragment);
5011 let mut event_ids = EventIdAllocator::default();
5012
5013 RenderModel {
5014 root_element: root_element.map(|element| element.name.clone()),
5015 root_element_name_span: root_element.map(|element| element.name_span),
5016 root_span: root_element.map(|element| element.span),
5017 root_fragment: root_fragment
5018 .map(|fragment| render_fragment_from_parsed(fragment, component_id, &mut event_ids)),
5019 attributes: root_element.map_or_else(Vec::new, |element| {
5020 element
5021 .attributes
5022 .iter()
5023 .map(render_attribute_from_parsed)
5024 .collect()
5025 }),
5026 event_handlers: root_element.map_or_else(Vec::new, |element| {
5027 element
5028 .event_handlers
5029 .iter()
5030 .map(|handler| {
5031 render_event_handler_from_parsed(handler, component_id, &mut event_ids)
5032 })
5033 .collect()
5034 }),
5035 children: root_element.map_or_else(Vec::new, |element| {
5036 element
5037 .children
5038 .iter()
5039 .map(|child| render_child_from_parsed(child, component_id, &mut event_ids))
5040 .collect()
5041 }),
5042 bindings: method.bindings.clone(),
5043 }
5044}
5045
5046fn parsed_root_element(root: &ParsedJsxNode) -> Option<&presolve_parser::ParsedJsxElement> {
5047 match root {
5048 ParsedJsxNode::Element(element) => Some(element),
5049 ParsedJsxNode::Fragment(_) => None,
5050 }
5051}
5052
5053fn parsed_root_fragment(root: &ParsedJsxNode) -> Option<&ParsedJsxFragment> {
5054 match root {
5055 ParsedJsxNode::Element(_) => None,
5056 ParsedJsxNode::Fragment(fragment) => Some(fragment),
5057 }
5058}
5059
5060fn collect_render_binding_diagnostics(
5061 class: &ParsedClass,
5062 render: &RenderModel,
5063 diagnostics: &mut Vec<ComponentDiagnostic>,
5064) {
5065 let property_names = class
5066 .properties
5067 .iter()
5068 .map(|property| property.name.as_str())
5069 .chain(
5070 class
5071 .methods
5072 .iter()
5073 .filter(|method| {
5074 method.is_getter
5075 && method
5076 .decorators
5077 .iter()
5078 .any(|decorator| decorator.name == "computed")
5079 })
5080 .map(|method| method.name.as_str()),
5081 )
5082 .collect::<Vec<_>>();
5083
5084 for binding in &render.bindings {
5085 if let Some(name) = this_member_name(binding) {
5086 if !property_names.contains(&name) {
5087 diagnostics.push(ComponentDiagnostic {
5088 severity: ComponentDiagnosticSeverity::Error,
5089 effect_id: None,
5090 statement_id: None,
5091 context_declaration_candidate_id: None,
5092 context_id: None,
5093 provider_id: None,
5094 consumer_id: None,
5095 slot_id: None,
5096 invocation_id: None,
5097 component_instance_id: None,
5098 slot_binding_id: None,
5099 structural_region_id: None,
5100 component_id: None,
5101 provider_instance_id: None,
5102 consumer_instance_id: None,
5103 secondary_labels: Vec::new(),
5104 provenance: None,
5105 code: "PSC1003".to_string(),
5106 message: format!(
5107 "render binding `{binding}` references unknown field `{name}` in class `{}`",
5108 class.name
5109 ),
5110 });
5111 }
5112 }
5113 }
5114}
5115
5116fn collect_render_event_diagnostics(
5117 class: &ParsedClass,
5118 render: &RenderModel,
5119 diagnostics: &mut Vec<ComponentDiagnostic>,
5120) {
5121 for event_handler in render_event_handlers(render) {
5122 if !matches!(event_handler.event.as_str(), "click" | "keydown") {
5123 diagnostics.push(ComponentDiagnostic {
5124 severity: ComponentDiagnosticSeverity::Error,
5125 effect_id: None,
5126 statement_id: None,
5127 context_declaration_candidate_id: None,
5128 context_id: None,
5129 provider_id: None,
5130 consumer_id: None,
5131 slot_id: None,
5132 invocation_id: None,
5133 component_instance_id: None,
5134 slot_binding_id: None,
5135 structural_region_id: None,
5136 component_id: None,
5137 provider_instance_id: None,
5138 consumer_instance_id: None,
5139 secondary_labels: Vec::new(),
5140 provenance: None,
5141 code: "PSC1005".to_string(),
5142 message: format!(
5143 "event `{}` is not supported yet in class `{}`",
5144 event_handler.event, class.name
5145 ),
5146 });
5147 }
5148
5149 if let Some(name) = this_member_name(&event_handler.handler) {
5150 if let Some(method) = class.methods.iter().find(|method| method.name == name) {
5151 if method.parameters.len() != event_handler.arguments.len() {
5152 diagnostics.push(ComponentDiagnostic::error(
5153 "PSC1042",
5154 format!(
5155 "event handler `{}` supplies {} static argument(s), but method `{name}` in class `{}` declares {} parameter(s)",
5156 event_handler.handler,
5157 event_handler.arguments.len(),
5158 class.name,
5159 method.parameters.len(),
5160 ),
5161 ));
5162 } else {
5163 for (argument, parameter) in
5164 event_handler.arguments.iter().zip(&method.parameters)
5165 {
5166 if !static_argument_matches_annotation(
5167 argument,
5168 parameter
5169 .type_annotation
5170 .as_ref()
5171 .map(|annotation| annotation.text.as_str()),
5172 ) {
5173 diagnostics.push(ComponentDiagnostic::error(
5174 "PSC1043",
5175 format!(
5176 "event handler `{}` supplies an incompatible static argument for parameter `{}` of method `{name}` in class `{}`",
5177 event_handler.handler, parameter.name, class.name,
5178 ),
5179 ));
5180 }
5181 }
5182 }
5183 } else {
5184 diagnostics.push(ComponentDiagnostic {
5185 severity: ComponentDiagnosticSeverity::Error,
5186 effect_id: None,
5187 statement_id: None,
5188 context_declaration_candidate_id: None,
5189 context_id: None,
5190 provider_id: None,
5191 consumer_id: None,
5192 slot_id: None,
5193 invocation_id: None,
5194 component_instance_id: None,
5195 slot_binding_id: None,
5196 structural_region_id: None,
5197 component_id: None,
5198 provider_instance_id: None,
5199 consumer_instance_id: None,
5200 secondary_labels: Vec::new(),
5201 provenance: None,
5202 code: "PSC1004".to_string(),
5203 message: format!(
5204 "event handler `{}` references unknown method `{name}` in class `{}`",
5205 event_handler.handler, class.name
5206 ),
5207 });
5208 }
5209 }
5210 }
5211}
5212
5213fn collect_action_parameter_assignment_diagnostics(
5214 class: &ParsedClass,
5215 diagnostics: &mut Vec<ComponentDiagnostic>,
5216) {
5217 for method in &class.methods {
5218 for update in &method.state_updates {
5219 let ParsedStateOperation::AssignParameter(parameter_name) = &update.operation else {
5220 continue;
5221 };
5222 if let Some(local) = method
5223 .local_variables
5224 .iter()
5225 .find(|local| local.name == *parameter_name)
5226 {
5227 if !method
5228 .decorators
5229 .iter()
5230 .any(|decorator| decorator.name == "action" && decorator.is_invoked)
5231 {
5232 diagnostics.push(ComponentDiagnostic::error(
5233 "PSC1045",
5234 format!(
5235 "serializable local `{parameter_name}` assigned to state in method `{}` of class `{}` requires @action()",
5236 method.name, class.name,
5237 ),
5238 ));
5239 }
5240 if !serializable_local_matches_state_field(class, &update.field, &local.value) {
5241 diagnostics.push(ComponentDiagnostic::error(
5242 "PSC1045",
5243 format!(
5244 "serializable local `{parameter_name}` in method `{}` of class `{}` is not primitively compatible with State field `{}`",
5245 method.name, class.name, update.field,
5246 ),
5247 ));
5248 }
5249 continue;
5250 }
5251 let Some(parameter) = method
5252 .parameters
5253 .iter()
5254 .find(|parameter| parameter.name == *parameter_name)
5255 else {
5256 diagnostics.push(ComponentDiagnostic::error(
5257 "PSC1041",
5258 format!(
5259 "state assignment in method `{}` of class `{}` references unknown parameter `{parameter_name}`",
5260 method.name, class.name,
5261 ),
5262 ));
5263 continue;
5264 };
5265 if !method
5266 .decorators
5267 .iter()
5268 .any(|decorator| decorator.name == "action" && decorator.is_invoked)
5269 {
5270 diagnostics.push(ComponentDiagnostic::error(
5271 "PSC1041",
5272 format!(
5273 "parameter `{parameter_name}` assigned to state in method `{}` of class `{}` requires @action()",
5274 method.name, class.name,
5275 ),
5276 ));
5277 }
5278 if parameter.type_annotation.is_none() {
5279 diagnostics.push(ComponentDiagnostic::error(
5280 "PSC1041",
5281 format!(
5282 "parameter `{parameter_name}` assigned to state in method `{}` of class `{}` requires a primitive TypeScript annotation",
5283 method.name, class.name,
5284 ),
5285 ));
5286 }
5287 let parameter_kind = parameter
5288 .type_annotation
5289 .as_ref()
5290 .and_then(|annotation| declared_state_type_kind(annotation.text.trim()));
5291 let state_kind = state_field_primitive_kind(class, &update.field);
5292 if parameter_kind.is_none() || state_kind.is_none() || parameter_kind != state_kind {
5293 diagnostics.push(ComponentDiagnostic::error(
5294 "PSC1044",
5295 format!(
5296 "parameter `{parameter_name}` in method `{}` of class `{}` is not primitively compatible with State field `{}`",
5297 method.name, class.name, update.field,
5298 ),
5299 ));
5300 }
5301 }
5302 }
5303}
5304
5305fn state_field_primitive_kind(
5306 class: &ParsedClass,
5307 field_name: &str,
5308) -> Option<DeclaredStateTypeKind> {
5309 let property = class.properties.iter().find(|property| {
5310 property.name == field_name && property.initializer.as_deref() == Some("state(...)")
5311 })?;
5312 property
5313 .state_type_annotation
5314 .as_ref()
5315 .and_then(|annotation| declared_state_type_kind(annotation.text.trim()))
5316 .or_else(|| {
5317 property
5318 .state_initial_value
5319 .as_ref()
5320 .and_then(serializable_primitive_kind)
5321 })
5322}
5323
5324fn serializable_primitive_kind(value: &ParsedSerializableValue) -> Option<DeclaredStateTypeKind> {
5325 match value {
5326 ParsedSerializableValue::Null => Some(DeclaredStateTypeKind::Null),
5327 ParsedSerializableValue::Number(_) => Some(DeclaredStateTypeKind::Number),
5328 ParsedSerializableValue::String(_) => Some(DeclaredStateTypeKind::String),
5329 ParsedSerializableValue::Boolean(_) => Some(DeclaredStateTypeKind::Boolean),
5330 ParsedSerializableValue::Array(_) | ParsedSerializableValue::Object(_) => None,
5331 }
5332}
5333
5334fn serializable_local_matches_state_field(
5335 class: &ParsedClass,
5336 field_name: &str,
5337 local_value: &ParsedSerializableValue,
5338) -> bool {
5339 let Some(property) = class.properties.iter().find(|property| {
5340 property.name == field_name && property.initializer.as_deref() == Some("state(...)")
5341 }) else {
5342 return false;
5343 };
5344 if let Some(kind) = property
5345 .state_type_annotation
5346 .as_ref()
5347 .and_then(|annotation| declared_state_type_kind(annotation.text.trim()))
5348 {
5349 return serializable_primitive_kind(local_value) == Some(kind);
5350 }
5351 property
5352 .state_initial_value
5353 .as_ref()
5354 .is_some_and(|initial_value| {
5355 serializable_values_have_compatible_shape(initial_value, local_value)
5356 })
5357}
5358
5359fn serializable_values_have_compatible_shape(
5360 left: &ParsedSerializableValue,
5361 right: &ParsedSerializableValue,
5362) -> bool {
5363 match (left, right) {
5364 (ParsedSerializableValue::Null, ParsedSerializableValue::Null)
5365 | (ParsedSerializableValue::Number(_), ParsedSerializableValue::Number(_))
5366 | (ParsedSerializableValue::String(_), ParsedSerializableValue::String(_))
5367 | (ParsedSerializableValue::Boolean(_), ParsedSerializableValue::Boolean(_)) => true,
5368 (ParsedSerializableValue::Array(left), ParsedSerializableValue::Array(right)) => {
5369 match (left.first(), right.first()) {
5370 (None, None) => true,
5371 (Some(left_first), Some(right_first)) => {
5372 left.iter()
5373 .all(|value| serializable_values_have_compatible_shape(left_first, value))
5374 && right.iter().all(|value| {
5375 serializable_values_have_compatible_shape(right_first, value)
5376 })
5377 && serializable_values_have_compatible_shape(left_first, right_first)
5378 }
5379 _ => false,
5380 }
5381 }
5382 (ParsedSerializableValue::Object(left), ParsedSerializableValue::Object(right)) => {
5383 left.len() == right.len()
5384 && left.iter().all(|(key, left_value)| {
5385 right.get(key).is_some_and(|right_value| {
5386 serializable_values_have_compatible_shape(left_value, right_value)
5387 })
5388 })
5389 }
5390 _ => false,
5391 }
5392}
5393
5394fn static_argument_matches_annotation(
5395 argument: &SerializableValue,
5396 annotation: Option<&str>,
5397) -> bool {
5398 match annotation.map(str::trim) {
5399 Some("string") => matches!(argument, SerializableValue::String(_)),
5400 Some("number") => matches!(argument, SerializableValue::Number(_)),
5401 Some("boolean") => matches!(argument, SerializableValue::Boolean(_)),
5402 Some("null") => matches!(argument, SerializableValue::Null),
5403 Some(_) | None => false,
5404 }
5405}
5406
5407#[derive(Debug, Clone)]
5408enum V2ActionOperand {
5409 Parameter(usize),
5410 Local(SerializableValue),
5411}
5412
5413fn v2_action_operands(
5418 handler: &presolve_parser::ParsedInlineHandler,
5419 class: &ParsedClass,
5420) -> Option<BTreeMap<String, V2ActionOperand>> {
5421 let parameter_indices = handler
5422 .parameters
5423 .iter()
5424 .enumerate()
5425 .map(|(index, parameter)| {
5426 let kind = parameter
5427 .type_annotation
5428 .as_ref()
5429 .and_then(|annotation| declared_state_type_kind(&annotation.text))?;
5430 Some((parameter.name.clone(), (index, kind)))
5431 })
5432 .collect::<Option<BTreeMap<_, _>>>()?;
5433 if parameter_indices.len() != handler.parameters.len() {
5434 return None;
5435 }
5436 let locals = handler
5437 .local_variables
5438 .iter()
5439 .map(|local| {
5440 let kind = serializable_primitive_kind(&local.value)?;
5441 Some((local.name.clone(), (local.span, kind, &local.value)))
5442 })
5443 .collect::<Option<BTreeMap<_, _>>>()?;
5444 if locals.len() != handler.local_variables.len()
5445 || parameter_indices
5446 .keys()
5447 .any(|name| locals.contains_key(name))
5448 {
5449 return None;
5450 }
5451 let uses = handler
5452 .state_updates
5453 .iter()
5454 .filter_map(|update| match &update.operation {
5455 ParsedStateOperation::AssignParameter(name) => Some((name, update)),
5456 _ => None,
5457 })
5458 .collect::<Vec<_>>();
5459 if uses.len() != handler.parameters.len() + handler.local_variables.len() {
5460 return None;
5461 }
5462 let mut operands = BTreeMap::new();
5463 for (name, update) in uses {
5464 let state_kind = state_field_primitive_kind(class, &update.field);
5465 let operand = if let Some((index, parameter_kind)) = parameter_indices.get(name) {
5466 (state_kind == Some(*parameter_kind)).then_some(V2ActionOperand::Parameter(*index))
5467 } else if let Some((local_span, local_kind, local_value)) = locals.get(name) {
5468 (local_span.start < update.span.start && state_kind == Some(*local_kind)).then_some(
5469 V2ActionOperand::Local(serializable_value_from_parsed(local_value)),
5470 )
5471 } else {
5472 None
5473 }?;
5474 if operands.insert(name.clone(), operand).is_some() {
5475 return None;
5476 }
5477 }
5478 Some(operands)
5479}
5480
5481fn state_operation_from_v2_parsed(
5482 operation: &ParsedStateOperation,
5483 operands: &BTreeMap<String, V2ActionOperand>,
5484) -> StateOperation {
5485 match operation {
5486 ParsedStateOperation::AssignParameter(name) => match operands
5487 .get(name)
5488 .expect("validated V2 action operand should be available")
5489 {
5490 V2ActionOperand::Parameter(index) => StateOperation::AssignParameter(index.to_string()),
5491 V2ActionOperand::Local(value) => StateOperation::Assign(value.clone()),
5492 },
5493 _ => state_operation_from_parsed(operation),
5494 }
5495}
5496
5497fn collect_v2_action_parameter_event_diagnostics(
5498 class: &ParsedClass,
5499 render: &RenderModel,
5500 parameter_kinds: &BTreeMap<String, Vec<DeclaredStateTypeKind>>,
5501 diagnostics: &mut Vec<ComponentDiagnostic>,
5502) {
5503 for event in render_event_handlers(render) {
5504 let name = event
5505 .handler
5506 .strip_prefix("this.")
5507 .unwrap_or(&event.handler);
5508 let Some(expected_kinds) = parameter_kinds.get(name) else {
5509 continue;
5510 };
5511 if event.arguments.len() != expected_kinds.len()
5512 || event
5513 .arguments
5514 .iter()
5515 .zip(expected_kinds)
5516 .any(|(argument, expected)| {
5517 serializable_value_primitive_kind(argument) != Some(*expected)
5518 })
5519 {
5520 diagnostics.push(ComponentDiagnostic::error(
5521 "PSV2A1006",
5522 format!(
5523 "event handler `{}` supplies arguments incompatible with canonical V2 Action `{name}` in class `{}`",
5524 event.handler, class.name,
5525 ),
5526 ));
5527 }
5528 }
5529}
5530
5531fn serializable_value_primitive_kind(value: &SerializableValue) -> Option<DeclaredStateTypeKind> {
5532 match value {
5533 SerializableValue::Null => Some(DeclaredStateTypeKind::Null),
5534 SerializableValue::Number(_) => Some(DeclaredStateTypeKind::Number),
5535 SerializableValue::String(_) => Some(DeclaredStateTypeKind::String),
5536 SerializableValue::Boolean(_) => Some(DeclaredStateTypeKind::Boolean),
5537 SerializableValue::Array(_) | SerializableValue::Object(_) => None,
5538 }
5539}
5540
5541fn state_operation_from_parsed(operation: &ParsedStateOperation) -> StateOperation {
5542 match operation {
5543 ParsedStateOperation::Increment => StateOperation::Increment,
5544 ParsedStateOperation::Decrement => StateOperation::Decrement,
5545 ParsedStateOperation::AddAssign(value) => {
5546 StateOperation::AddAssign(serializable_value_from_parsed(value))
5547 }
5548 ParsedStateOperation::SubtractAssign(value) => {
5549 StateOperation::SubtractAssign(serializable_value_from_parsed(value))
5550 }
5551 ParsedStateOperation::Assign(value) => {
5552 StateOperation::Assign(serializable_value_from_parsed(value))
5553 }
5554 ParsedStateOperation::AssignParameter(parameter) => {
5555 StateOperation::AssignParameter(parameter.clone())
5556 }
5557 ParsedStateOperation::Toggle => StateOperation::Toggle,
5558 }
5559}
5560
5561fn state_operation_from_parsed_in_method(
5562 operation: &ParsedStateOperation,
5563 method: &ParsedMethod,
5564) -> StateOperation {
5565 match operation {
5566 ParsedStateOperation::AssignParameter(name) => method
5567 .local_variables
5568 .iter()
5569 .find(|local| local.name == *name)
5570 .map_or_else(
5571 || state_operation_from_parsed(operation),
5572 |local| StateOperation::Assign(serializable_value_from_parsed(&local.value)),
5573 ),
5574 _ => state_operation_from_parsed(operation),
5575 }
5576}
5577
5578fn serializable_value_from_parsed(value: &ParsedSerializableValue) -> SerializableValue {
5579 match value {
5580 ParsedSerializableValue::Null => SerializableValue::Null,
5581 ParsedSerializableValue::Number(value) => SerializableValue::Number(value.clone()),
5582 ParsedSerializableValue::String(value) => SerializableValue::String(value.clone()),
5583 ParsedSerializableValue::Boolean(value) => SerializableValue::Boolean(*value),
5584 ParsedSerializableValue::Array(values) => {
5585 SerializableValue::Array(values.iter().map(serializable_value_from_parsed).collect())
5586 }
5587 ParsedSerializableValue::Object(values) => SerializableValue::Object(
5588 values
5589 .iter()
5590 .map(|(key, value)| (key.clone(), serializable_value_from_parsed(value)))
5591 .collect(),
5592 ),
5593 }
5594}
5595
5596fn decorator_argument(class: &ParsedClass, name: &str) -> Option<String> {
5597 class
5598 .decorators
5599 .iter()
5600 .find(|decorator| decorator.name == name)
5601 .and_then(|decorator| decorator.argument.clone())
5602}
5603
5604fn component_element_name(class: &ParsedClass) -> Option<String> {
5607 let decorator = class
5608 .decorators
5609 .iter()
5610 .find(|decorator| decorator.name == "component")?;
5611 decorator.argument.clone().or_else(|| {
5612 (decorator.is_invoked && decorator.argument_count == 0).then(|| {
5613 let mut slug = String::new();
5614 for (index, character) in class.name.chars().enumerate() {
5615 if character.is_ascii_uppercase() && index > 0 {
5616 slug.push('-');
5617 }
5618 if character.is_ascii_alphanumeric() {
5619 slug.push(character.to_ascii_lowercase());
5620 }
5621 }
5622 format!("presolve-{slug}")
5623 })
5624 })
5625}
5626
5627fn render_child_from_parsed(
5628 child: &ParsedJsxChild,
5629 component_id: &SemanticId,
5630 event_ids: &mut EventIdAllocator,
5631) -> RenderChild {
5632 match child {
5633 ParsedJsxChild::Text { value, span } => RenderChild::Text {
5634 value: value.clone(),
5635 span: *span,
5636 },
5637 ParsedJsxChild::Binding { expression, span } => RenderChild::Binding {
5638 expression: expression.clone(),
5639 span: *span,
5640 },
5641 ParsedJsxChild::Element(element) => RenderChild::Element(RenderElement {
5642 tag_name: element.name.clone(),
5643 tag_name_span: element.name_span,
5644 span: element.span,
5645 attributes: element
5646 .attributes
5647 .iter()
5648 .map(render_attribute_from_parsed)
5649 .collect(),
5650 event_handlers: element
5651 .event_handlers
5652 .iter()
5653 .map(|handler| render_event_handler_from_parsed(handler, component_id, event_ids))
5654 .collect(),
5655 children: element
5656 .children
5657 .iter()
5658 .map(|child| render_child_from_parsed(child, component_id, event_ids))
5659 .collect::<Vec<_>>(),
5660 }),
5661 ParsedJsxChild::Fragment(fragment) => RenderChild::Fragment(render_fragment_from_parsed(
5662 fragment,
5663 component_id,
5664 event_ids,
5665 )),
5666 ParsedJsxChild::Conditional(conditional) => RenderChild::Conditional(
5667 render_conditional_from_parsed(conditional, component_id, event_ids),
5668 ),
5669 ParsedJsxChild::List(list) => {
5670 RenderChild::List(render_list_from_parsed(list, component_id, event_ids))
5671 }
5672 }
5673}
5674
5675fn render_list_from_parsed(
5676 list: &ParsedJsxList,
5677 component_id: &SemanticId,
5678 event_ids: &mut EventIdAllocator,
5679) -> RenderList {
5680 RenderList {
5681 iterable: list.iterable.clone(),
5682 item_variable: list.item_variable.clone(),
5683 index_variable: list.index_variable.clone(),
5684 key_expression: list.key_expression.clone(),
5685 span: list.span,
5686 item_template: render_children_from_parsed_node(
5687 &list.item_template,
5688 component_id,
5689 event_ids,
5690 ),
5691 }
5692}
5693
5694fn render_conditional_from_parsed(
5695 conditional: &ParsedJsxConditional,
5696 component_id: &SemanticId,
5697 event_ids: &mut EventIdAllocator,
5698) -> RenderConditional {
5699 RenderConditional {
5700 condition: conditional.condition.clone(),
5701 span: conditional.span,
5702 when_true: render_children_from_parsed_node(
5703 &conditional.when_true,
5704 component_id,
5705 event_ids,
5706 ),
5707 when_false: conditional
5708 .when_false
5709 .as_ref()
5710 .map(|node| render_children_from_parsed_node(node, component_id, event_ids))
5711 .unwrap_or_default(),
5712 }
5713}
5714
5715fn render_children_from_parsed_node(
5716 node: &ParsedJsxNode,
5717 component_id: &SemanticId,
5718 event_ids: &mut EventIdAllocator,
5719) -> Vec<RenderChild> {
5720 match node {
5721 ParsedJsxNode::Element(element) => vec![RenderChild::Element(RenderElement {
5722 tag_name: element.name.clone(),
5723 tag_name_span: element.name_span,
5724 span: element.span,
5725 attributes: element
5726 .attributes
5727 .iter()
5728 .map(render_attribute_from_parsed)
5729 .collect(),
5730 event_handlers: element
5731 .event_handlers
5732 .iter()
5733 .map(|handler| render_event_handler_from_parsed(handler, component_id, event_ids))
5734 .collect(),
5735 children: element
5736 .children
5737 .iter()
5738 .map(|child| render_child_from_parsed(child, component_id, event_ids))
5739 .collect::<Vec<_>>(),
5740 })],
5741 ParsedJsxNode::Fragment(fragment) => fragment
5742 .children
5743 .iter()
5744 .map(|child| render_child_from_parsed(child, component_id, event_ids))
5745 .collect(),
5746 }
5747}
5748
5749fn render_fragment_from_parsed(
5750 fragment: &ParsedJsxFragment,
5751 component_id: &SemanticId,
5752 event_ids: &mut EventIdAllocator,
5753) -> RenderFragment {
5754 RenderFragment {
5755 span: fragment.span,
5756 children: fragment
5757 .children
5758 .iter()
5759 .map(|child| render_child_from_parsed(child, component_id, event_ids))
5760 .collect(),
5761 }
5762}
5763
5764fn render_attribute_from_parsed(attribute: &ParsedJsxAttribute) -> RenderAttribute {
5765 RenderAttribute {
5766 name: attribute.name.clone(),
5767 value: match &attribute.value {
5768 ParsedJsxAttributeValue::Boolean => RenderAttributeValue::Boolean,
5769 ParsedJsxAttributeValue::Static(value) => RenderAttributeValue::Static(value.clone()),
5770 ParsedJsxAttributeValue::Expression(expression) => {
5771 RenderAttributeValue::Expression(expression.clone())
5772 }
5773 ParsedJsxAttributeValue::Spread(expression) => {
5774 RenderAttributeValue::Spread(expression.clone())
5775 }
5776 ParsedJsxAttributeValue::Unsupported => RenderAttributeValue::Unsupported,
5777 },
5778 name_span: attribute.name_span,
5779 value_span: attribute.value_span,
5780 expression_span: attribute.expression_span,
5781 this_member: attribute.this_member.clone(),
5782 constant_value: attribute
5783 .constant_value
5784 .as_ref()
5785 .map(serializable_value_from_parsed),
5786 span: attribute.span,
5787 }
5788}
5789
5790fn render_event_handler_from_parsed(
5791 event_handler: &ParsedEventHandler,
5792 component_id: &SemanticId,
5793 event_ids: &mut EventIdAllocator,
5794) -> RenderEventHandler {
5795 RenderEventHandler {
5796 id: component_id.event_handler(&event_handler.event, event_ids.next()),
5797 owner: SemanticOwner::entity(component_id.template()),
5798 event: event_handler.event.clone(),
5799 handler: event_handler.handler.clone(),
5800 arguments: event_handler
5801 .arguments
5802 .iter()
5803 .map(serializable_value_from_parsed)
5804 .collect(),
5805 span: event_handler.span,
5806 }
5807}
5808
5809#[derive(Debug, Default)]
5810struct EventIdAllocator {
5811 next: usize,
5812}
5813
5814impl EventIdAllocator {
5815 fn next(&mut self) -> usize {
5816 let current = self.next;
5817 self.next += 1;
5818 current
5819 }
5820}
5821
5822fn collect_component_provenance(
5823 class: &ParsedClass,
5824 component: &ComponentNode,
5825 path: &Path,
5826) -> BTreeMap<SemanticId, SourceProvenance> {
5827 let mut provenance = BTreeMap::new();
5828 provenance.insert(
5829 component.id.clone(),
5830 SourceProvenance::new(path, class.span),
5831 );
5832
5833 for property in &class.properties {
5834 if property.initializer.as_deref() != Some("state(...)") {
5835 continue;
5836 }
5837
5838 if let Some(field) = component
5839 .state_fields
5840 .iter()
5841 .find(|field| field.name == property.name)
5842 {
5843 provenance.insert(field.id.clone(), SourceProvenance::new(path, property.span));
5844 }
5845 }
5846
5847 for method in &class.methods {
5848 if let Some(component_method) = component
5849 .methods
5850 .iter()
5851 .find(|component_method| component_method.name == method.name)
5852 {
5853 provenance.insert(
5854 component_method.id.clone(),
5855 SourceProvenance::new(path, method.span),
5856 );
5857 }
5858
5859 if method.name == "render" {
5860 provenance.insert(
5861 component.id.template(),
5862 SourceProvenance::new(path, method.span),
5863 );
5864 }
5865
5866 for (index, update) in method.state_updates.iter().enumerate() {
5867 provenance.insert(
5868 component.id.action(&method.name, index),
5869 SourceProvenance::new(path, update.span),
5870 );
5871 }
5872
5873 if let Some(component_method) = component
5874 .methods
5875 .iter()
5876 .find(|component_method| component_method.name == method.name)
5877 {
5878 for parameter in &component_method.parameters {
5879 provenance.insert(
5880 parameter.id.clone(),
5881 SourceProvenance::new(path, parameter.span),
5882 );
5883 }
5884 for local in &component_method.local_variables {
5885 provenance.insert(local.id.clone(), SourceProvenance::new(path, local.span));
5886 }
5887 }
5888 }
5889
5890 if let Some(render) = &component.render {
5891 for handler in render_event_handlers(render) {
5892 provenance.insert(
5893 handler.id.clone(),
5894 SourceProvenance::new(path, handler.span),
5895 );
5896 }
5897 }
5898
5899 provenance
5900}
5901
5902fn collect_semantic_references(
5903 component: &ComponentNode,
5904 provenance: &BTreeMap<SemanticId, SourceProvenance>,
5905) -> Vec<SemanticReference> {
5906 let mut references = component
5907 .actions
5908 .iter()
5909 .filter_map(|action| {
5910 component
5911 .state_fields
5912 .iter()
5913 .find(|field| field.name == action.field)
5914 .map(|field| SemanticReference {
5915 kind: SemanticReferenceKind::ActionState,
5916 source: action.id.clone(),
5917 target: field.id.clone(),
5918 provenance: provenance
5919 .get(&action.id)
5920 .expect("action semantic provenance should exist")
5921 .clone(),
5922 })
5923 })
5924 .collect::<Vec<_>>();
5925
5926 if let Some(render) = &component.render {
5927 references.extend(
5928 render_event_handlers(render)
5929 .into_iter()
5930 .filter_map(|handler| {
5931 let method_name = this_member_name(&handler.handler)?;
5932 component
5933 .methods
5934 .iter()
5935 .find(|method| method.name == method_name)
5936 .map(|method| SemanticReference {
5937 kind: SemanticReferenceKind::EventMethod,
5938 source: handler.id.clone(),
5939 target: method.id.clone(),
5940 provenance: provenance
5941 .get(&handler.id)
5942 .expect("event semantic provenance should exist")
5943 .clone(),
5944 })
5945 }),
5946 );
5947 }
5948
5949 references
5950}
5951
5952pub(crate) fn render_event_handlers(render: &RenderModel) -> Vec<&RenderEventHandler> {
5953 let mut event_handlers = render.event_handlers.iter().collect::<Vec<_>>();
5954
5955 for child in &render.children {
5956 collect_child_event_handlers(child, &mut event_handlers);
5957 }
5958 if let Some(fragment) = &render.root_fragment {
5959 for child in &fragment.children {
5960 collect_child_event_handlers(child, &mut event_handlers);
5961 }
5962 }
5963
5964 event_handlers
5965}
5966
5967fn collect_child_event_handlers<'a>(
5968 child: &'a RenderChild,
5969 event_handlers: &mut Vec<&'a RenderEventHandler>,
5970) {
5971 match child {
5972 RenderChild::Element(element) => {
5973 event_handlers.extend(element.event_handlers.iter());
5974
5975 for child in &element.children {
5976 collect_child_event_handlers(child, event_handlers);
5977 }
5978 }
5979 RenderChild::Fragment(fragment) => {
5980 for child in &fragment.children {
5981 collect_child_event_handlers(child, event_handlers);
5982 }
5983 }
5984 RenderChild::Conditional(conditional) => {
5985 for child in &conditional.when_true {
5986 collect_child_event_handlers(child, event_handlers);
5987 }
5988 for child in &conditional.when_false {
5989 collect_child_event_handlers(child, event_handlers);
5990 }
5991 }
5992 RenderChild::List(list) => {
5993 for child in &list.item_template {
5994 collect_child_event_handlers(child, event_handlers);
5995 }
5996 }
5997 RenderChild::Text { .. } | RenderChild::Binding { .. } => {}
5998 }
5999}
6000
6001fn collect_duplicate_event_diagnostics(
6002 render: &RenderModel,
6003 class_name: &str,
6004 diagnostics: &mut Vec<ComponentDiagnostic>,
6005) {
6006 collect_duplicate_events_for_handlers(&render.event_handlers, class_name, diagnostics);
6007
6008 for child in &render.children {
6009 collect_duplicate_child_event_diagnostics(child, class_name, diagnostics);
6010 }
6011 if let Some(fragment) = &render.root_fragment {
6012 for child in &fragment.children {
6013 collect_duplicate_child_event_diagnostics(child, class_name, diagnostics);
6014 }
6015 }
6016}
6017
6018fn collect_render_attribute_diagnostics(
6019 render: &RenderModel,
6020 state_fields: &[StateField],
6021 class_name: &str,
6022 diagnostics: &mut Vec<ComponentDiagnostic>,
6023) {
6024 collect_attribute_diagnostics_for_attributes(
6025 &render.attributes,
6026 state_fields,
6027 class_name,
6028 diagnostics,
6029 None,
6030 );
6031
6032 for child in &render.children {
6033 collect_child_attribute_diagnostics(child, state_fields, class_name, diagnostics);
6034 }
6035 if let Some(fragment) = &render.root_fragment {
6036 for child in &fragment.children {
6037 collect_child_attribute_diagnostics(child, state_fields, class_name, diagnostics);
6038 }
6039 }
6040}
6041
6042fn collect_render_list_diagnostics(
6043 render: &RenderModel,
6044 state_fields: &[StateField],
6045 class_name: &str,
6046 diagnostics: &mut Vec<ComponentDiagnostic>,
6047) {
6048 for child in &render.children {
6049 collect_child_list_diagnostics(child, state_fields, class_name, diagnostics);
6050 }
6051 if let Some(fragment) = &render.root_fragment {
6052 for child in &fragment.children {
6053 collect_child_list_diagnostics(child, state_fields, class_name, diagnostics);
6054 }
6055 }
6056}
6057
6058fn collect_child_list_diagnostics(
6059 child: &RenderChild,
6060 state_fields: &[StateField],
6061 class_name: &str,
6062 diagnostics: &mut Vec<ComponentDiagnostic>,
6063) {
6064 match child {
6065 RenderChild::Element(element) => {
6066 for child in &element.children {
6067 collect_child_list_diagnostics(child, state_fields, class_name, diagnostics);
6068 }
6069 }
6070 RenderChild::Fragment(fragment) => {
6071 for child in &fragment.children {
6072 collect_child_list_diagnostics(child, state_fields, class_name, diagnostics);
6073 }
6074 }
6075 RenderChild::Conditional(conditional) => {
6076 for child in &conditional.when_true {
6077 collect_child_list_diagnostics(child, state_fields, class_name, diagnostics);
6078 }
6079 for child in &conditional.when_false {
6080 collect_child_list_diagnostics(child, state_fields, class_name, diagnostics);
6081 }
6082 }
6083 RenderChild::List(list) => {
6084 collect_list_diagnostics(list, state_fields, class_name, diagnostics);
6085
6086 for child in &list.item_template {
6087 collect_child_list_diagnostics(child, state_fields, class_name, diagnostics);
6088 }
6089 }
6090 RenderChild::Text { .. } | RenderChild::Binding { .. } => {}
6091 }
6092}
6093
6094#[allow(clippy::too_many_lines)]
6095fn collect_list_diagnostics(
6096 list: &RenderList,
6097 state_fields: &[StateField],
6098 class_name: &str,
6099 diagnostics: &mut Vec<ComponentDiagnostic>,
6100) {
6101 if list.key_expression.is_empty() {
6102 diagnostics.push(ComponentDiagnostic {
6103 severity: ComponentDiagnosticSeverity::Error,
6104 effect_id: None,
6105 statement_id: None,
6106 context_declaration_candidate_id: None,
6107 context_id: None,
6108 provider_id: None,
6109 consumer_id: None,
6110 slot_id: None,
6111 invocation_id: None,
6112 component_instance_id: None,
6113 slot_binding_id: None,
6114 structural_region_id: None,
6115 component_id: None,
6116 provider_instance_id: None,
6117 consumer_instance_id: None,
6118 secondary_labels: Vec::new(),
6119 provenance: None,
6120 code: "PSC1011".to_string(),
6121 message: format!(
6122 "list over `{}` in class `{class_name}` is missing a `key={{...}}` attribute; stable keys are required for retained-node reconciliation",
6123 list.iterable
6124 ),
6125 });
6126 return;
6127 }
6128
6129 if list.index_variable.as_deref() == Some(list.key_expression.as_str()) {
6130 diagnostics.push(ComponentDiagnostic {
6131 severity: ComponentDiagnosticSeverity::Error,
6132 effect_id: None,
6133 statement_id: None,
6134 context_declaration_candidate_id: None,
6135 context_id: None,
6136 provider_id: None,
6137 consumer_id: None,
6138 slot_id: None,
6139 invocation_id: None,
6140 component_instance_id: None,
6141 slot_binding_id: None,
6142 structural_region_id: None,
6143 component_id: None,
6144 provider_instance_id: None,
6145 consumer_instance_id: None,
6146 secondary_labels: Vec::new(),
6147 provenance: None,
6148 code: "PSC1012".to_string(),
6149 message: format!(
6150 "list key `{}` in class `{class_name}` uses the iteration index; index keys are unstable when items move",
6151 list.key_expression
6152 ),
6153 });
6154 return;
6155 }
6156
6157 let member_path = list_member_key_path(list);
6158 if list.key_expression != list.item_variable && member_path.is_none() {
6159 diagnostics.push(ComponentDiagnostic {
6160 severity: ComponentDiagnosticSeverity::Error,
6161 effect_id: None,
6162 statement_id: None,
6163 context_declaration_candidate_id: None,
6164 context_id: None,
6165 provider_id: None,
6166 consumer_id: None,
6167 slot_id: None,
6168 invocation_id: None,
6169 component_instance_id: None,
6170 slot_binding_id: None,
6171 structural_region_id: None,
6172 component_id: None,
6173 provider_instance_id: None,
6174 consumer_instance_id: None,
6175 secondary_labels: Vec::new(),
6176 provenance: None,
6177 code: "PSC1013".to_string(),
6178 message: format!(
6179 "list key `{}` in class `{class_name}` is not supported yet; use the item variable `{}` or one of its object members",
6180 list.key_expression, list.item_variable
6181 ),
6182 });
6183 return;
6184 }
6185
6186 let Some(field_name) = this_member_name(&list.iterable) else {
6187 return;
6188 };
6189 let Some(SerializableValue::Array(values)) = state_fields
6190 .iter()
6191 .find(|field| field.name == field_name)
6192 .and_then(|field| field.initial_value.as_ref())
6193 else {
6194 return;
6195 };
6196
6197 let mut keys = Vec::new();
6198 for (index, value) in values.iter().enumerate() {
6199 let key_value = member_path.map_or(Some(value), |path| value.member_path_value(path));
6200 let Some(key) = key_value.and_then(list_key_from_static_value) else {
6201 diagnostics.push(ComponentDiagnostic {
6202 severity: ComponentDiagnosticSeverity::Error,
6203 effect_id: None,
6204 statement_id: None,
6205 context_declaration_candidate_id: None,
6206 context_id: None,
6207 provider_id: None,
6208 consumer_id: None,
6209 slot_id: None,
6210 invocation_id: None,
6211 component_instance_id: None,
6212 slot_binding_id: None,
6213 structural_region_id: None,
6214 component_id: None,
6215 provider_instance_id: None,
6216 consumer_instance_id: None,
6217 secondary_labels: Vec::new(),
6218 provenance: None,
6219 code: "PSC1015".to_string(),
6220 message: member_path.map_or_else(
6221 || format!(
6222 "list key `{}` resolves to a non-primitive initial item at index {index} in class `{class_name}`; keyed reconciliation requires primitive keys",
6223 list.key_expression
6224 ),
6225 |_| format!(
6226 "list key `{}` cannot resolve a primitive member value for initial item at index {index} in class `{class_name}`; every item must provide that member",
6227 list.key_expression
6228 ),
6229 ),
6230 });
6231 return;
6232 };
6233
6234 if keys.contains(&key) {
6235 diagnostics.push(ComponentDiagnostic {
6236 severity: ComponentDiagnosticSeverity::Error,
6237 effect_id: None,
6238 statement_id: None,
6239 context_declaration_candidate_id: None,
6240 context_id: None,
6241 provider_id: None,
6242 consumer_id: None,
6243 slot_id: None,
6244 invocation_id: None,
6245 component_instance_id: None,
6246 slot_binding_id: None,
6247 structural_region_id: None,
6248 component_id: None,
6249 provider_instance_id: None,
6250 consumer_instance_id: None,
6251 secondary_labels: Vec::new(),
6252 provenance: None,
6253 code: "PSC1014".to_string(),
6254 message: format!(
6255 "list key `{}` resolves to duplicate initial value `{key}` in class `{class_name}`; keyed reconciliation requires unique keys",
6256 list.key_expression
6257 ),
6258 });
6259 return;
6260 }
6261
6262 keys.push(key);
6263 }
6264}
6265
6266fn list_member_key_path(list: &RenderList) -> Option<&str> {
6267 list.key_expression
6268 .strip_prefix(&list.item_variable)
6269 .and_then(|suffix| suffix.strip_prefix('.'))
6270 .filter(|path| !path.is_empty() && !path.split('.').any(str::is_empty))
6271}
6272
6273fn list_key_from_static_value(value: &SerializableValue) -> Option<String> {
6274 match value {
6275 SerializableValue::Null => Some("null".to_string()),
6276 SerializableValue::Number(value) | SerializableValue::String(value) => Some(value.clone()),
6277 SerializableValue::Boolean(value) => Some(value.to_string()),
6278 SerializableValue::Array(_) | SerializableValue::Object(_) => None,
6279 }
6280}
6281
6282fn collect_child_attribute_diagnostics(
6283 child: &RenderChild,
6284 state_fields: &[StateField],
6285 class_name: &str,
6286 diagnostics: &mut Vec<ComponentDiagnostic>,
6287) {
6288 match child {
6289 RenderChild::Element(element) => {
6290 collect_attribute_diagnostics_for_attributes(
6291 &element.attributes,
6292 state_fields,
6293 class_name,
6294 diagnostics,
6295 None,
6296 );
6297
6298 for child in &element.children {
6299 collect_child_attribute_diagnostics(child, state_fields, class_name, diagnostics);
6300 }
6301 }
6302 RenderChild::Fragment(fragment) => {
6303 for child in &fragment.children {
6304 collect_child_attribute_diagnostics(child, state_fields, class_name, diagnostics);
6305 }
6306 }
6307 RenderChild::Conditional(conditional) => {
6308 for child in &conditional.when_true {
6309 collect_child_attribute_diagnostics(child, state_fields, class_name, diagnostics);
6310 }
6311 for child in &conditional.when_false {
6312 collect_child_attribute_diagnostics(child, state_fields, class_name, diagnostics);
6313 }
6314 }
6315 RenderChild::List(list) => {
6316 for child in &list.item_template {
6317 collect_list_item_attribute_diagnostics(
6318 child,
6319 state_fields,
6320 class_name,
6321 diagnostics,
6322 &list.item_variable,
6323 list.index_variable.as_deref(),
6324 );
6325 }
6326 }
6327 RenderChild::Text { .. } | RenderChild::Binding { .. } => {}
6328 }
6329}
6330
6331#[allow(clippy::too_many_lines)]
6332fn collect_attribute_diagnostics_for_attributes(
6333 attributes: &[RenderAttribute],
6334 state_fields: &[StateField],
6335 class_name: &str,
6336 diagnostics: &mut Vec<ComponentDiagnostic>,
6337 list_scope: Option<(&str, Option<&str>)>,
6338) {
6339 let mut seen = Vec::<&str>::new();
6340
6341 for attribute in attributes {
6342 if attribute.name == "field" {
6346 continue;
6347 }
6348 if !attribute.name.starts_with("on") {
6349 if seen.contains(&attribute.name.as_str()) {
6350 diagnostics.push(ComponentDiagnostic {
6351 severity: ComponentDiagnosticSeverity::Error,
6352 effect_id: None,
6353 statement_id: None,
6354 context_declaration_candidate_id: None,
6355 context_id: None,
6356 provider_id: None,
6357 consumer_id: None,
6358 slot_id: None,
6359 invocation_id: None,
6360 component_instance_id: None,
6361 slot_binding_id: None,
6362 structural_region_id: None,
6363 component_id: None,
6364 provider_instance_id: None,
6365 consumer_instance_id: None,
6366 secondary_labels: Vec::new(),
6367 provenance: None,
6368 code: "PSC1007".to_string(),
6369 message: format!(
6370 "attribute `{}` is declared more than once on the same element in class `{}`",
6371 attribute.name, class_name
6372 ),
6373 });
6374 } else if attribute.name != "{...}" {
6375 seen.push(&attribute.name);
6376 }
6377 }
6378
6379 match &attribute.value {
6380 RenderAttributeValue::Expression(_) if attribute.name == "key" => {}
6381 RenderAttributeValue::Expression(expression)
6382 if !is_event_attribute(&attribute.name) =>
6383 {
6384 if expression.as_deref().is_some_and(|expression| {
6385 list_scope
6386 .is_some_and(|scope| list_item_attribute_expression(expression, scope))
6387 }) {
6388 continue;
6389 }
6390
6391 match expression.as_deref().and_then(this_member_name) {
6392 Some(field_name)
6393 if state_fields.iter().any(|field| field.name == field_name) => {}
6394 Some(field_name) => diagnostics.push(ComponentDiagnostic {
6395 severity: ComponentDiagnosticSeverity::Error,
6396 effect_id: None,
6397 statement_id: None,
6398 context_declaration_candidate_id: None,
6399 context_id: None,
6400 provider_id: None,
6401 consumer_id: None,
6402 slot_id: None,
6403 invocation_id: None,
6404 component_instance_id: None,
6405 slot_binding_id: None,
6406 structural_region_id: None,
6407 component_id: None,
6408 provider_instance_id: None,
6409 consumer_instance_id: None,
6410 secondary_labels: Vec::new(),
6411 provenance: None,
6412 code: "PSC1003".to_string(),
6413 message: format!(
6414 "attribute binding `{}` references unknown state field `{field_name}` in class `{}`",
6415 attribute.name, class_name
6416 ),
6417 }),
6418 None => diagnostics.push(ComponentDiagnostic {
6419 severity: ComponentDiagnosticSeverity::Error,
6420 effect_id: None,
6421 statement_id: None,
6422 context_declaration_candidate_id: None,
6423 context_id: None,
6424 provider_id: None,
6425 consumer_id: None,
6426 slot_id: None,
6427 invocation_id: None,
6428 component_instance_id: None,
6429 slot_binding_id: None,
6430 structural_region_id: None,
6431 component_id: None,
6432 provider_instance_id: None,
6433 consumer_instance_id: None,
6434 secondary_labels: Vec::new(),
6435 provenance: None,
6436 code: "PSC1008".to_string(),
6437 message: format!(
6438 "attribute `{}` uses an unsupported expression value in class `{}`",
6439 attribute.name, class_name
6440 ),
6441 }),
6442 }
6443 }
6444 RenderAttributeValue::Spread(_) => {
6445 diagnostics.push(ComponentDiagnostic {
6446 severity: ComponentDiagnosticSeverity::Error,
6447 effect_id: None,
6448 statement_id: None,
6449 context_declaration_candidate_id: None,
6450 context_id: None,
6451 provider_id: None,
6452 consumer_id: None,
6453 slot_id: None,
6454 invocation_id: None,
6455 component_instance_id: None,
6456 slot_binding_id: None,
6457 structural_region_id: None,
6458 component_id: None,
6459 provider_instance_id: None,
6460 consumer_instance_id: None,
6461 secondary_labels: Vec::new(),
6462 provenance: None,
6463 code: "PSC1009".to_string(),
6464 message: format!(
6465 "JSX spread attributes are not supported yet in class `{class_name}`"
6466 ),
6467 });
6468 }
6469 RenderAttributeValue::Unsupported if !is_event_attribute(&attribute.name) => {
6470 diagnostics.push(ComponentDiagnostic {
6471 severity: ComponentDiagnosticSeverity::Error,
6472 effect_id: None,
6473 statement_id: None,
6474 context_declaration_candidate_id: None,
6475 context_id: None,
6476 provider_id: None,
6477 consumer_id: None,
6478 slot_id: None,
6479 invocation_id: None,
6480 component_instance_id: None,
6481 slot_binding_id: None,
6482 structural_region_id: None,
6483 component_id: None,
6484 provider_instance_id: None,
6485 consumer_instance_id: None,
6486 secondary_labels: Vec::new(),
6487 provenance: None,
6488 code: "PSC1010".to_string(),
6489 message: format!(
6490 "attribute `{}` uses an unsupported JSX value in class `{}`",
6491 attribute.name, class_name
6492 ),
6493 });
6494 }
6495 _ => {}
6496 }
6497 }
6498}
6499
6500fn collect_list_item_attribute_diagnostics(
6501 child: &RenderChild,
6502 state_fields: &[StateField],
6503 class_name: &str,
6504 diagnostics: &mut Vec<ComponentDiagnostic>,
6505 item_variable: &str,
6506 index_variable: Option<&str>,
6507) {
6508 match child {
6509 RenderChild::Element(element) => {
6510 collect_attribute_diagnostics_for_attributes(
6511 &element.attributes,
6512 state_fields,
6513 class_name,
6514 diagnostics,
6515 Some((item_variable, index_variable)),
6516 );
6517
6518 for child in &element.children {
6519 collect_list_item_attribute_diagnostics(
6520 child,
6521 state_fields,
6522 class_name,
6523 diagnostics,
6524 item_variable,
6525 index_variable,
6526 );
6527 }
6528 }
6529 RenderChild::Fragment(fragment) => {
6530 for child in &fragment.children {
6531 collect_list_item_attribute_diagnostics(
6532 child,
6533 state_fields,
6534 class_name,
6535 diagnostics,
6536 item_variable,
6537 index_variable,
6538 );
6539 }
6540 }
6541 RenderChild::Conditional(conditional) => {
6542 for child in &conditional.when_true {
6543 collect_list_item_attribute_diagnostics(
6544 child,
6545 state_fields,
6546 class_name,
6547 diagnostics,
6548 item_variable,
6549 index_variable,
6550 );
6551 }
6552 for child in &conditional.when_false {
6553 collect_list_item_attribute_diagnostics(
6554 child,
6555 state_fields,
6556 class_name,
6557 diagnostics,
6558 item_variable,
6559 index_variable,
6560 );
6561 }
6562 }
6563 RenderChild::List(list) => {
6564 for child in &list.item_template {
6565 collect_list_item_attribute_diagnostics(
6566 child,
6567 state_fields,
6568 class_name,
6569 diagnostics,
6570 &list.item_variable,
6571 list.index_variable.as_deref(),
6572 );
6573 }
6574 }
6575 RenderChild::Text { .. } | RenderChild::Binding { .. } => {}
6576 }
6577}
6578
6579fn list_item_attribute_expression(expression: &str, scope: (&str, Option<&str>)) -> bool {
6580 expression == scope.0
6581 || scope.1 == Some(expression)
6582 || expression
6583 .strip_prefix(scope.0)
6584 .and_then(|suffix| suffix.strip_prefix('.'))
6585 .is_some_and(|path| !path.is_empty() && !path.split('.').any(str::is_empty))
6586}
6587
6588fn is_event_attribute(name: &str) -> bool {
6589 name.strip_prefix("on")
6590 .and_then(|event| event.chars().next())
6591 .is_some_and(char::is_uppercase)
6592}
6593
6594fn collect_duplicate_child_event_diagnostics(
6595 child: &RenderChild,
6596 class_name: &str,
6597 diagnostics: &mut Vec<ComponentDiagnostic>,
6598) {
6599 match child {
6600 RenderChild::Element(element) => {
6601 collect_duplicate_events_for_handlers(&element.event_handlers, class_name, diagnostics);
6602
6603 for child in &element.children {
6604 collect_duplicate_child_event_diagnostics(child, class_name, diagnostics);
6605 }
6606 }
6607 RenderChild::Fragment(fragment) => {
6608 for child in &fragment.children {
6609 collect_duplicate_child_event_diagnostics(child, class_name, diagnostics);
6610 }
6611 }
6612 RenderChild::Conditional(conditional) => {
6613 for child in &conditional.when_true {
6614 collect_duplicate_child_event_diagnostics(child, class_name, diagnostics);
6615 }
6616 for child in &conditional.when_false {
6617 collect_duplicate_child_event_diagnostics(child, class_name, diagnostics);
6618 }
6619 }
6620 RenderChild::List(list) => {
6621 for child in &list.item_template {
6622 collect_duplicate_child_event_diagnostics(child, class_name, diagnostics);
6623 }
6624 }
6625 RenderChild::Text { .. } | RenderChild::Binding { .. } => {}
6626 }
6627}
6628
6629fn collect_duplicate_events_for_handlers(
6630 event_handlers: &[RenderEventHandler],
6631 class_name: &str,
6632 diagnostics: &mut Vec<ComponentDiagnostic>,
6633) {
6634 let mut seen = Vec::<&str>::new();
6635
6636 for event_handler in event_handlers {
6637 if seen.contains(&event_handler.event.as_str()) {
6638 diagnostics.push(ComponentDiagnostic {
6639 severity: ComponentDiagnosticSeverity::Error,
6640 effect_id: None,
6641 statement_id: None,
6642 context_declaration_candidate_id: None,
6643 context_id: None,
6644 provider_id: None,
6645 consumer_id: None,
6646 slot_id: None,
6647 invocation_id: None,
6648 component_instance_id: None,
6649 slot_binding_id: None,
6650 structural_region_id: None,
6651 component_id: None,
6652 provider_instance_id: None,
6653 consumer_instance_id: None,
6654 secondary_labels: Vec::new(),
6655 provenance: None,
6656 code: "PSC1006".to_string(),
6657 message: format!(
6658 "event `{}` is declared more than once on the same element in class `{}`",
6659 event_handler.event, class_name
6660 ),
6661 });
6662 } else {
6663 seen.push(&event_handler.event);
6664 }
6665 }
6666}
6667
6668fn this_member_name(reference: &str) -> Option<&str> {
6669 reference.strip_prefix("this.")
6670}