Skip to main content

presolve_compiler/
form_field_definition_lowering.rs

1//! Decorator-free V2 `field({...})` recognition inside canonical Forms.
2
3use 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
76/// Select nested call leaves from static object arguments of component fields.
77/// The sites remain syntax-only until the outer Form and each leaf callee are
78/// independently proven by TypeScript authority.
79pub 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}