Skip to main content

presolve_compiler/
component_graph.rs

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