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