1use std::collections::{BTreeMap, BTreeSet};
4
5use presolve_parser::{ParsedFile, SourceSpan};
6
7use crate::{
8 normalize_authored_semantics_v1, AuthoredSemanticCandidateKindV1,
9 AuthoredSemanticNormalizationErrorV1, AuthoredSourceRangeV1,
10 CanonicalAuthoredDeclarationKindV1, CanonicalAuthoredSemanticModelV1, CanonicalIntrinsicKindV1,
11 ResolvedAuthoredSemanticCandidateV1, ResolvedIntrinsicIdentityV1,
12};
13
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct FormFieldDefinitionSiteV1 {
16 pub subject: String,
17 pub owner_form_subject: String,
18 pub declaration_source: AuthoredSourceRangeV1,
19 pub callee_source: AuthoredSourceRangeV1,
20 pub initial_source: Option<AuthoredSourceRangeV1>,
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum ResolvedFormFieldValueClassificationV1 {
25 FileArray,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct ResolvedFormFieldDefinitionV1 {
30 pub callee_source: AuthoredSourceRangeV1,
31 pub field_identity: ResolvedIntrinsicIdentityV1,
32 pub value_classification: Option<ResolvedFormFieldValueClassificationV1>,
33}
34
35#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct FormFieldDefinitionLoweringV1 {
37 pub sites: Vec<FormFieldDefinitionSiteV1>,
38 pub model: CanonicalAuthoredSemanticModelV1,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq)]
42pub enum FormFieldDefinitionLoweringErrorV1 {
43 SourcePathMismatch,
44 DuplicateResolution { start: usize, end: usize },
45 UnknownFieldResolution { start: usize, end: usize },
46 FieldOutsideCanonicalForm { subject: String },
47 InvalidAuthoredSemantics(AuthoredSemanticNormalizationErrorV1),
48}
49
50impl std::fmt::Display for FormFieldDefinitionLoweringErrorV1 {
51 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52 match self {
53 Self::SourcePathMismatch => write!(
54 formatter,
55 "component, Form, and Form Field products must describe the same source file"
56 ),
57 Self::DuplicateResolution { start, end } => write!(
58 formatter,
59 "duplicate Form Field definition resolution at {start}..{end}"
60 ),
61 Self::UnknownFieldResolution { start, end } => write!(
62 formatter,
63 "Form Field resolution has no static source field call at {start}..{end}"
64 ),
65 Self::FieldOutsideCanonicalForm { subject } => write!(
66 formatter,
67 "resolved Form Field `{subject}` is not owned by a canonical V2 Form"
68 ),
69 Self::InvalidAuthoredSemantics(error) => error.fmt(formatter),
70 }
71 }
72}
73
74impl std::error::Error for FormFieldDefinitionLoweringErrorV1 {}
75
76pub fn form_field_definition_sites_v1(
80 parsed: &ParsedFile,
81 component_model: &CanonicalAuthoredSemanticModelV1,
82) -> Result<Vec<FormFieldDefinitionSiteV1>, FormFieldDefinitionLoweringErrorV1> {
83 if component_model.source_path != parsed.path {
84 return Err(FormFieldDefinitionLoweringErrorV1::SourcePathMismatch);
85 }
86 let component_names = component_model
87 .declarations
88 .iter()
89 .filter(|declaration| declaration.kind == CanonicalAuthoredDeclarationKindV1::Component)
90 .map(|declaration| declaration.subject.as_str())
91 .collect::<BTreeSet<_>>();
92 let mut sites = parsed
93 .classes
94 .iter()
95 .filter(|class| component_names.contains(class.name.as_str()))
96 .flat_map(|class| {
97 class.properties.iter().flat_map(move |property| {
98 let owner_form_subject = format!("{}.{}", class.name, property.name);
99 property
100 .form_definition_shape
101 .iter()
102 .flat_map(|shape| &shape.fields)
103 .map(move |field| FormFieldDefinitionSiteV1 {
104 subject: format!("{owner_form_subject}.{}", field.path.join(".")),
105 owner_form_subject: owner_form_subject.clone(),
106 declaration_source: range(field.declaration_span),
107 callee_source: range(field.callee_span),
108 initial_source: field.initial_span.map(range),
109 })
110 })
111 })
112 .collect::<Vec<_>>();
113 sites.sort_by_key(|site| {
114 (
115 site.callee_source.start,
116 site.callee_source.end,
117 site.subject.clone(),
118 )
119 });
120 Ok(sites)
121}
122
123pub fn lower_form_field_definitions_v1(
124 parsed: &ParsedFile,
125 component_model: &CanonicalAuthoredSemanticModelV1,
126 form_model: &CanonicalAuthoredSemanticModelV1,
127 resolutions: impl IntoIterator<Item = ResolvedFormFieldDefinitionV1>,
128) -> Result<FormFieldDefinitionLoweringV1, FormFieldDefinitionLoweringErrorV1> {
129 if form_model.source_path != parsed.path {
130 return Err(FormFieldDefinitionLoweringErrorV1::SourcePathMismatch);
131 }
132 let sites = form_field_definition_sites_v1(parsed, component_model)?;
133 let known_sites = sites
134 .iter()
135 .map(|site| range_key(site.callee_source))
136 .collect::<BTreeSet<_>>();
137 let forms = form_model
138 .declarations
139 .iter()
140 .filter(|declaration| declaration.kind == CanonicalAuthoredDeclarationKindV1::Form)
141 .map(|declaration| declaration.subject.as_str())
142 .collect::<BTreeSet<_>>();
143 let mut resolution_by_site = BTreeMap::new();
144 for resolution in resolutions {
145 let key = range_key(resolution.callee_source);
146 if !known_sites.contains(&key) {
147 return Err(FormFieldDefinitionLoweringErrorV1::UnknownFieldResolution {
148 start: key.0,
149 end: key.1,
150 });
151 }
152 if resolution_by_site.insert(key, resolution).is_some() {
153 return Err(FormFieldDefinitionLoweringErrorV1::DuplicateResolution {
154 start: key.0,
155 end: key.1,
156 });
157 }
158 }
159 let candidates = sites
160 .iter()
161 .filter_map(|site| {
162 let resolution = resolution_by_site.get(&range_key(site.callee_source))?;
163 Some((site, resolution))
164 })
165 .map(|(site, resolution)| {
166 if !forms.contains(site.owner_form_subject.as_str()) {
167 return Err(
168 FormFieldDefinitionLoweringErrorV1::FieldOutsideCanonicalForm {
169 subject: site.subject.clone(),
170 },
171 );
172 }
173 Ok(ResolvedAuthoredSemanticCandidateV1 {
174 subject: site.subject.clone(),
175 source: site.declaration_source,
176 kind: AuthoredSemanticCandidateKindV1::ResolvedIntrinsic {
177 intrinsic_kind: CanonicalIntrinsicKindV1::Field,
178 intrinsic_identity: resolution.field_identity.clone(),
179 },
180 })
181 })
182 .collect::<Result<Vec<_>, _>>()?;
183 let mut model = normalize_authored_semantics_v1(parsed, candidates)
184 .map_err(FormFieldDefinitionLoweringErrorV1::InvalidAuthoredSemantics)?;
185 for declaration in &mut model.declarations {
186 let Some((_, resolution)) = sites
187 .iter()
188 .filter_map(|site| {
189 resolution_by_site
190 .get(&range_key(site.callee_source))
191 .map(|resolution| (site, resolution))
192 })
193 .find(|(site, _)| site.subject == declaration.subject)
194 else {
195 continue;
196 };
197 if resolution.value_classification
198 == Some(ResolvedFormFieldValueClassificationV1::FileArray)
199 {
200 declaration.derived_evidence =
201 Some(crate::DerivedAuthoredEvidenceV2::FormFieldFileArray);
202 }
203 }
204 Ok(FormFieldDefinitionLoweringV1 { sites, model })
205}
206
207fn range(span: SourceSpan) -> AuthoredSourceRangeV1 {
208 AuthoredSourceRangeV1 {
209 start: span.start,
210 end: span.end,
211 line: span.line,
212 column: span.column,
213 }
214}
215
216fn range_key(range: AuthoredSourceRangeV1) -> (usize, usize) {
217 (range.start, range.end)
218}
219
220#[cfg(test)]
221mod tests {
222 use std::collections::BTreeSet;
223
224 use presolve_parser::parse_file;
225
226 use crate::{
227 lower_component_inheritance_v1, lower_form_definitions_v1, ResolvedComponentInheritanceV1,
228 ResolvedFormDefinitionV1, ResolvedIntrinsicIdentityV1,
229 };
230
231 use super::{
232 form_field_definition_sites_v1, lower_form_field_definitions_v1,
233 ResolvedFormFieldDefinitionV1,
234 };
235
236 fn identity(name: &str) -> ResolvedIntrinsicIdentityV1 {
237 ResolvedIntrinsicIdentityV1 {
238 name: name.to_owned(),
239 flags: 32,
240 declaration_modules: vec!["presolve".to_owned()],
241 }
242 }
243
244 #[test]
245 fn lowers_nested_fields_only_below_authority_proven_forms() {
246 let parsed = parse_file(
247 "src/Profile.tsx",
248 r#"class Profile extends Base {
249 profile = makeForm({ fields: { name: makeField({ initial: "" }), address: { street: makeField({ initial: "" }) } } });
250 lookalike = helper({ fields: { ignored: makeField({ initial: "" }) } });
251}"#,
252 );
253 let component_site = crate::component_inheritance_sites_v1(&parsed)
254 .pop()
255 .unwrap();
256 let components = lower_component_inheritance_v1(
257 &parsed,
258 [ResolvedComponentInheritanceV1 {
259 heritage_source: component_site.heritage_source,
260 component_identity: identity("Component"),
261 }],
262 )
263 .unwrap()
264 .model;
265 let forms = crate::form_definition_sites_v1(&parsed, &components).unwrap();
266 let form_model = lower_form_definitions_v1(
267 &parsed,
268 &components,
269 [ResolvedFormDefinitionV1 {
270 callee_source: forms[0].callee_source,
271 form_identity: identity("defineForm"),
272 }],
273 )
274 .unwrap()
275 .model;
276 let sites = form_field_definition_sites_v1(&parsed, &components).unwrap();
277 let lowering = lower_form_field_definitions_v1(
278 &parsed,
279 &components,
280 &form_model,
281 sites[..2].iter().map(|site| ResolvedFormFieldDefinitionV1 {
282 callee_source: site.callee_source,
283 field_identity: identity("field"),
284 value_classification: None,
285 }),
286 )
287 .unwrap();
288 assert_eq!(lowering.model.declarations.len(), 2);
289 let subjects = lowering
290 .model
291 .declarations
292 .iter()
293 .map(|declaration| declaration.subject.as_str())
294 .collect::<BTreeSet<_>>();
295 assert_eq!(
296 subjects,
297 BTreeSet::from(["Profile.profile.address.street", "Profile.profile.name",])
298 );
299 }
300}