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