1use std::collections::{BTreeMap, BTreeSet};
9
10use presolve_parser::{ParsedFile, SourceSpan};
11
12use crate::{
13 normalize_authored_semantics_v1, AuthoredSemanticCandidateKindV1,
14 AuthoredSemanticNormalizationErrorV1, AuthoredSourceRangeV1,
15 CanonicalAuthoredDeclarationKindV1, CanonicalAuthoredSemanticModelV1, CanonicalIntrinsicKindV1,
16 ResolvedAuthoredSemanticCandidateV1, ResolvedIntrinsicIdentityV1,
17};
18
19#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct StateInitializerSiteV1 {
22 pub subject: String,
23 pub declaration_source: AuthoredSourceRangeV1,
24 pub callee_source: AuthoredSourceRangeV1,
25}
26
27#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct ResolvedStateInitializerV1 {
30 pub callee_source: AuthoredSourceRangeV1,
31 pub state_identity: ResolvedIntrinsicIdentityV1,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct StateInitializerLoweringV1 {
37 pub sites: Vec<StateInitializerSiteV1>,
38 pub model: CanonicalAuthoredSemanticModelV1,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq)]
43pub enum StateInitializerLoweringErrorV1 {
44 ComponentSourcePathMismatch,
45 UnknownComponentDeclaration { start: usize, end: usize },
46 DuplicateResolution { start: usize, end: usize },
47 UnknownStateResolution { start: usize, end: usize },
48 InvalidAuthoredSemantics(AuthoredSemanticNormalizationErrorV1),
49}
50
51impl std::fmt::Display for StateInitializerLoweringErrorV1 {
52 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53 match self {
54 Self::ComponentSourcePathMismatch => write!(
55 formatter,
56 "component and State authored-semantic products must describe the same source file"
57 ),
58 Self::UnknownComponentDeclaration { start, end } => write!(
59 formatter,
60 "canonical component declaration has no source class at {start}..{end}"
61 ),
62 Self::DuplicateResolution { start, end } => {
63 write!(
64 formatter,
65 "duplicate State initializer resolution at {start}..{end}"
66 )
67 }
68 Self::UnknownStateResolution { start, end } => write!(
69 formatter,
70 "State initializer resolution has no source field callee at {start}..{end}"
71 ),
72 Self::InvalidAuthoredSemantics(error) => error.fmt(formatter),
73 }
74 }
75}
76
77impl std::error::Error for StateInitializerLoweringErrorV1 {}
78
79pub fn state_initializer_sites_v1(
81 parsed: &ParsedFile,
82 component_model: &CanonicalAuthoredSemanticModelV1,
83) -> Result<Vec<StateInitializerSiteV1>, StateInitializerLoweringErrorV1> {
84 if component_model.source_path != parsed.path {
85 return Err(StateInitializerLoweringErrorV1::ComponentSourcePathMismatch);
86 }
87 let components = component_model
88 .declarations
89 .iter()
90 .filter(|declaration| declaration.kind == CanonicalAuthoredDeclarationKindV1::Component)
91 .map(|declaration| (declaration.subject.clone(), range_key(declaration.source)))
92 .collect::<BTreeSet<_>>();
93 for (subject, declaration_key) in &components {
94 if !parsed
95 .classes
96 .iter()
97 .any(|class| class.name == *subject && range_key(range(class.span)) == *declaration_key)
98 {
99 return Err(
100 StateInitializerLoweringErrorV1::UnknownComponentDeclaration {
101 start: declaration_key.0,
102 end: declaration_key.1,
103 },
104 );
105 }
106 }
107
108 let mut sites = parsed
109 .classes
110 .iter()
111 .filter(|class| components.contains(&(class.name.clone(), range_key(range(class.span)))))
112 .flat_map(|class| {
113 class.properties.iter().filter_map(move |property| {
114 let call = property.initializer_call.as_ref()?;
115 (!property.is_static).then_some(StateInitializerSiteV1 {
116 subject: format!("{}.{}", class.name, property.name),
117 declaration_source: range(property.span),
118 callee_source: range(call.callee_span),
119 })
120 })
121 })
122 .collect::<Vec<_>>();
123 sites.sort_by_key(|site| {
124 (
125 site.callee_source.start,
126 site.callee_source.end,
127 site.subject.clone(),
128 )
129 });
130 Ok(sites)
131}
132
133pub fn lower_state_initializers_v1(
135 parsed: &ParsedFile,
136 component_model: &CanonicalAuthoredSemanticModelV1,
137 resolutions: impl IntoIterator<Item = ResolvedStateInitializerV1>,
138) -> Result<StateInitializerLoweringV1, StateInitializerLoweringErrorV1> {
139 let sites = state_initializer_sites_v1(parsed, component_model)?;
140 let known_sites = sites
141 .iter()
142 .map(|site| range_key(site.callee_source))
143 .collect::<BTreeSet<_>>();
144 let mut resolution_by_site = BTreeMap::new();
145 for resolution in resolutions {
146 let key = range_key(resolution.callee_source);
147 if !known_sites.contains(&key) {
148 return Err(StateInitializerLoweringErrorV1::UnknownStateResolution {
149 start: key.0,
150 end: key.1,
151 });
152 }
153 if resolution_by_site.insert(key, resolution).is_some() {
154 return Err(StateInitializerLoweringErrorV1::DuplicateResolution {
155 start: key.0,
156 end: key.1,
157 });
158 }
159 }
160 let candidates = sites.iter().filter_map(|site| {
161 let resolution = resolution_by_site.get(&range_key(site.callee_source))?;
162 Some(ResolvedAuthoredSemanticCandidateV1 {
163 subject: site.subject.clone(),
164 source: site.declaration_source,
165 kind: AuthoredSemanticCandidateKindV1::ResolvedIntrinsic {
166 intrinsic_kind: CanonicalIntrinsicKindV1::State,
167 intrinsic_identity: resolution.state_identity.clone(),
168 },
169 })
170 });
171 let model = normalize_authored_semantics_v1(parsed, candidates)
172 .map_err(StateInitializerLoweringErrorV1::InvalidAuthoredSemantics)?;
173 Ok(StateInitializerLoweringV1 { sites, model })
174}
175
176fn range(span: SourceSpan) -> AuthoredSourceRangeV1 {
177 AuthoredSourceRangeV1 {
178 start: span.start,
179 end: span.end,
180 line: span.line,
181 column: span.column,
182 }
183}
184
185fn range_key(range: AuthoredSourceRangeV1) -> (usize, usize) {
186 (range.start, range.end)
187}
188
189#[cfg(test)]
190mod tests {
191 use presolve_parser::parse_file;
192
193 use crate::{
194 lower_component_inheritance_v1, CanonicalAuthoredDeclarationKindV1,
195 ResolvedComponentInheritanceV1, ResolvedIntrinsicIdentityV1,
196 };
197
198 use super::{
199 lower_state_initializers_v1, state_initializer_sites_v1, ResolvedStateInitializerV1,
200 StateInitializerLoweringErrorV1,
201 };
202
203 fn identity(name: &str) -> ResolvedIntrinsicIdentityV1 {
204 ResolvedIntrinsicIdentityV1 {
205 name: name.to_owned(),
206 flags: 32,
207 declaration_modules: vec!["node_modules/presolve/src/index.d.ts".to_owned()],
208 }
209 }
210
211 #[test]
212 fn lowers_only_authority_proven_instance_state_calls_of_canonical_components() {
213 let source = r#"
214class Counter extends Base {
215 count = reactiveCell(0);
216 label = ordinaryHelper("count");
217 static invalid = reactiveCell(1);
218}
219@component() class LegacyOnly { count = reactiveCell(0); }
220"#;
221 let parsed = parse_file("src/Counter.tsx", source);
222 let component_site = crate::component_inheritance_sites_v1(&parsed)
223 .into_iter()
224 .next()
225 .unwrap();
226 let components = lower_component_inheritance_v1(
227 &parsed,
228 [ResolvedComponentInheritanceV1 {
229 heritage_source: component_site.heritage_source,
230 component_identity: identity("Component"),
231 }],
232 )
233 .unwrap()
234 .model;
235 let sites = state_initializer_sites_v1(&parsed, &components).unwrap();
236 assert_eq!(sites.len(), 2);
237 assert_eq!(sites[0].subject, "Counter.count");
238 assert_eq!(sites[1].subject, "Counter.label");
239
240 let model = lower_state_initializers_v1(
241 &parsed,
242 &components,
243 [ResolvedStateInitializerV1 {
244 callee_source: sites[0].callee_source,
245 state_identity: identity("state"),
246 }],
247 )
248 .unwrap();
249 assert_eq!(model.model.declarations.len(), 1);
250 assert_eq!(
251 model.model.declarations[0].kind,
252 CanonicalAuthoredDeclarationKindV1::State
253 );
254 assert_eq!(model.model.declarations[0].subject, "Counter.count");
255 }
256
257 #[test]
258 fn rejects_dangling_state_authority_results() {
259 let parsed = parse_file(
260 "src/Counter.tsx",
261 "class Counter extends Base { value = cell(0); }",
262 );
263 let component_site = crate::component_inheritance_sites_v1(&parsed)
264 .into_iter()
265 .next()
266 .unwrap();
267 let components = lower_component_inheritance_v1(
268 &parsed,
269 [ResolvedComponentInheritanceV1 {
270 heritage_source: component_site.heritage_source,
271 component_identity: identity("Component"),
272 }],
273 )
274 .unwrap()
275 .model;
276 let error = lower_state_initializers_v1(
277 &parsed,
278 &components,
279 [ResolvedStateInitializerV1 {
280 callee_source: crate::AuthoredSourceRangeV1 {
281 start: 0,
282 end: 5,
283 line: 1,
284 column: 1,
285 },
286 state_identity: identity("state"),
287 }],
288 )
289 .expect_err("resolved calls must join an instance field initializer");
290 assert!(matches!(
291 error,
292 StateInitializerLoweringErrorV1::UnknownStateResolution { .. }
293 ));
294 }
295}