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