Skip to main content

presolve_compiler/
component_graph.rs

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    /// The caller-supplied source module that owns this component declaration.
43    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    /// Normalized authored heritage retained before unsupported inheritance is
49    /// excluded from every executable component product.
50    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    /// Source-faithful candidates retained for every Context-family decorator,
57    /// including forms that cannot produce a semantic entity.
58    pub context_declaration_candidates: Vec<AuthoredContextDeclarationCandidate>,
59    /// Source-faithful candidates retained for every `@slot()` decorator,
60    /// including forms that cannot produce a canonical Slot entity.
61    pub slot_declaration_candidates: Vec<AuthoredSlotDeclarationCandidate>,
62    /// Normalized source facts retained for every recognized `@form`
63    /// declaration, including targets without canonical Form identity inputs.
64    pub form_declaration_candidates: Vec<FormDeclarationCandidate>,
65    /// Source-faithful Resource declarations retained before endpoint resolution
66    /// and activation lowering.
67    pub resource_declaration_candidates: Vec<AuthoredResourceDeclarationFact>,
68    /// Source-faithful conventional route-loader declarations retained before
69    /// package capability resolution and server handoff planning.
70    pub route_loader_declaration_candidates: Vec<AuthoredRouteLoaderDeclarationFact>,
71    /// Normalized I3 candidates. Canonical Form resolution, type assignment,
72    /// duplicate grouping, and `FieldId` construction occur during ASM
73    /// assembly over existing immutable authorities.
74    pub form_field_declaration_candidates: Vec<FormFieldDeclarationCandidate>,
75    /// Parser-normalized source facts for every recognized `@validate`
76    /// placement. I6 lowering consumes these facts without revisiting syntax.
77    pub validation_rule_declaration_facts: Vec<AuthoredValidationRuleDeclarationFact>,
78    /// Parser-normalized source facts for every recognized `@submit` method.
79    pub submission_declaration_facts: Vec<AuthoredSubmissionDeclarationFact>,
80    /// Parser-normalized source facts for every recognized `@serialize` placement.
81    pub serialization_declaration_facts: Vec<AuthoredSerializationDeclarationFact>,
82    /// Retained N9 opaque terminal Action facts. They have no runtime lowering
83    /// authority until integrity-bound artifact publication is complete.
84    pub opaque_action_facts: Vec<AuthoredOpaqueActionFact>,
85    pub server_action_facts: Vec<AuthoredServerActionFact>,
86    /// Module imports that shadow compiler-owned validation intrinsic names.
87    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/// Source-faithful I6 declaration facts retained before semantic validation.
135#[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/// Source-faithful I9 facts retained before Form/action resolution.
154#[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/// Source-faithful `@loader()` field retained before it is associated with a
201/// compiler-selected file route and an integrity-bound package capability.
202#[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/// Authored source facts for one valid G1 `@context()` component field.
257///
258/// This is deliberately declaration syntax rather than the later canonical
259/// Context entity. ASM lowering owns the semantic identity and exposes it to
260/// future provider and consumer products.
261#[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/// A compiler-retained, compile-time-only Context designator.
273#[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/// Authored source facts for one valid G2 `@provide()` component field.
283#[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/// Authored source facts for one valid G3 `@consume()` component field.
296#[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/// Authored source facts for one valid H1 `@slot()` component field.
314#[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/// Parser/lowering-owned facts retained before canonical Slot construction.
338/// Invalid candidates have their own source identity and never acquire a
339/// `SlotId`.
340#[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/// Immutable I2 declaration candidate retained before canonical Form
400/// construction. `form_id` is present only when owner and authored field name
401/// are independently canonical, even when another declaration rule fails.
402#[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/// Immutable I3 candidate retained for every recognized `@field` placement.
473/// `field_id` and `type_assignment` remain absent unless the declaration is
474/// valid after canonical Form, type, value, and duplicate resolution.
475#[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    /// Retained N7-B syntax fact. It has no lowering authority until the
486    /// complete Form artifact/runtime/resume contract admits arity two.
487    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/// Parser/lowering-owned facts retained before semantic entity construction.
534/// No diagnostic code is assigned here.
535#[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/// A compiler-owned numeric arithmetic expression lowered from a state initializer.
554#[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/// A compiler-owned constant expression lowered from a state initializer.
587#[derive(Debug, Clone, PartialEq, Eq)]
588pub struct ConstantExpression {
589    pub kind: ConstantExpressionKind,
590    pub span: SourceSpan,
591}
592
593/// A compiler-owned supported expression lowered from an `@computed()` getter.
594#[derive(Debug, Clone, PartialEq, Eq)]
595pub struct ComputedExpression {
596    pub kind: ComputedExpressionKind,
597    pub span: SourceSpan,
598}
599
600/// Expression forms accepted by the E2 computed getter lowering slice.
601#[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/// Ordered source syntax retained from one `@effect()` body before semantic resolution.
678#[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/// An expression operand retained from an effect statement before resolution.
720#[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    /// Evaluate a finite constant numeric expression using `Presolve` arithmetic semantics.
814    ///
815    /// # Errors
816    ///
817    /// Returns an error for an invalid numeric literal, division or remainder by zero,
818    /// or a non-finite intermediate or final result.
819    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    /// Evaluate a constant state initializer using `Presolve` expression semantics.
865    ///
866    /// # Errors
867    ///
868    /// Returns an error when one of the expression's numeric arithmetic operands is invalid.
869    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    // Presolve constant expressions deliberately use exact finite numeric equality.
954    // No tolerance is compiler semantics because the language has no approximate
955    // comparison operator or configurable precision model.
956    #[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/// Explicit authored type metadata carried from the parser into canonical compiler data.
1082#[derive(Debug, Clone, PartialEq, Eq)]
1083pub struct DeclaredStateType {
1084    pub text: String,
1085    pub provenance: SourceProvenance,
1086    pub kind: Option<DeclaredStateTypeKind>,
1087}
1088
1089/// The primitive state type forms currently recognized without type inference.
1090#[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/// A compiler-owned call fact retained for computed-purity analysis.
1152#[derive(Debug, Clone, PartialEq, Eq)]
1153pub struct MethodCall {
1154    pub callee: String,
1155    pub span: SourceSpan,
1156}
1157
1158/// Additional semantic role carried by a compiler-owned method declaration.
1159#[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/// A compiler-owned declaration of a supported method parameter.
1185#[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/// Shared severity classification for compiler diagnostics.
1366#[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/// A deterministic related authored location for a compiler diagnostic.
1381#[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/// Build compiler semantics with a source-module-qualified identity root.
1393///
1394/// The canonical application model and compiler frontend use this path. The
1395/// legacy graph builder remains available for backend compatibility while
1396/// runtime artifact contracts are migrated independently.
1397#[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                &parameter.name,
1930                AuthoredDeclarationKind::Parameter,
1931                &parameter.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                &parameter.name,
2237                AuthoredDeclarationKind::Parameter,
2238                &parameter.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                &parameter.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(&parameter.name, index),
3118                AuthoredDeclarationKind::Parameter,
3119                Some(&parameter.name),
3120                &parameter.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(&parameter.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
4456/// Returns the explicit component identity or the stable compiler-derived
4457/// identity for the ergonomic `@component()` declaration form.
4458fn 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        // Phase I Form control bindings are compiler-owned candidates. Their
5195        // validity and later diagnostics consume I4 products rather than the
5196        // legacy ordinary-state attribute checker.
5197        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}