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