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