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