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