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