Skip to main content

presolve_compiler/
component_graph.rs

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