1use std::collections::{BTreeMap, BTreeSet};
10
11use presolve_parser::{ParsedFile, SourceSpan};
12
13use crate::{
14 normalize_authored_semantics_v1, AuthoredSemanticCandidateKindV1,
15 AuthoredSemanticNormalizationErrorV1, AuthoredSourceRangeV1, CanonicalAuthoredSemanticModelV1,
16 CanonicalIntrinsicKindV1, ResolvedAuthoredSemanticCandidateV1, ResolvedIntrinsicIdentityV1,
17};
18
19#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct ComponentInheritanceSiteV1 {
22 pub subject: String,
23 pub declaration_source: AuthoredSourceRangeV1,
24 pub heritage_source: AuthoredSourceRangeV1,
25}
26
27#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct ResolvedComponentInheritanceV1 {
33 pub heritage_source: AuthoredSourceRangeV1,
34 pub component_identity: ResolvedIntrinsicIdentityV1,
35}
36
37#[derive(Debug, Clone, PartialEq, Eq)]
39pub struct ComponentInheritanceLoweringV1 {
40 pub sites: Vec<ComponentInheritanceSiteV1>,
41 pub model: CanonicalAuthoredSemanticModelV1,
42}
43
44#[derive(Debug, Clone, PartialEq, Eq)]
46pub enum ComponentInheritanceLoweringErrorV1 {
47 DuplicateResolution { start: usize, end: usize },
48 UnknownHeritageResolution { start: usize, end: usize },
49 InvalidAuthoredSemantics(AuthoredSemanticNormalizationErrorV1),
50}
51
52impl std::fmt::Display for ComponentInheritanceLoweringErrorV1 {
53 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54 match self {
55 Self::DuplicateResolution { start, end } => {
56 write!(
57 formatter,
58 "duplicate component inheritance resolution at {start}..{end}"
59 )
60 }
61 Self::UnknownHeritageResolution { start, end } => write!(
62 formatter,
63 "component inheritance resolution has no source heritage clause at {start}..{end}"
64 ),
65 Self::InvalidAuthoredSemantics(error) => error.fmt(formatter),
66 }
67 }
68}
69
70impl std::error::Error for ComponentInheritanceLoweringErrorV1 {}
71
72#[must_use]
74pub fn component_inheritance_sites_v1(parsed: &ParsedFile) -> Vec<ComponentInheritanceSiteV1> {
75 let mut sites = parsed
76 .classes
77 .iter()
78 .filter_map(|class| {
79 let heritage = class.heritage.as_ref()?;
80 Some(ComponentInheritanceSiteV1 {
81 subject: class.name.clone(),
82 declaration_source: range(class.span),
83 heritage_source: range(heritage.span),
84 })
85 })
86 .collect::<Vec<_>>();
87 sites.sort_by_key(|site| {
88 (
89 site.heritage_source.start,
90 site.heritage_source.end,
91 site.subject.clone(),
92 )
93 });
94 sites
95}
96
97pub fn lower_component_inheritance_v1(
102 parsed: &ParsedFile,
103 resolutions: impl IntoIterator<Item = ResolvedComponentInheritanceV1>,
104) -> Result<ComponentInheritanceLoweringV1, ComponentInheritanceLoweringErrorV1> {
105 let sites = component_inheritance_sites_v1(parsed);
106 let known_sites = sites
107 .iter()
108 .map(|site| range_key(site.heritage_source))
109 .collect::<BTreeSet<_>>();
110 let mut resolution_by_site = BTreeMap::new();
111 for resolution in resolutions {
112 let key = range_key(resolution.heritage_source);
113 if !known_sites.contains(&key) {
114 return Err(
115 ComponentInheritanceLoweringErrorV1::UnknownHeritageResolution {
116 start: key.0,
117 end: key.1,
118 },
119 );
120 }
121 if resolution_by_site.insert(key, resolution).is_some() {
122 return Err(ComponentInheritanceLoweringErrorV1::DuplicateResolution {
123 start: key.0,
124 end: key.1,
125 });
126 }
127 }
128
129 let candidates = sites.iter().filter_map(|site| {
130 let resolution = resolution_by_site.get(&range_key(site.heritage_source))?;
131 Some(ResolvedAuthoredSemanticCandidateV1 {
132 subject: site.subject.clone(),
133 source: site.declaration_source,
134 kind: AuthoredSemanticCandidateKindV1::ResolvedIntrinsic {
135 intrinsic_kind: CanonicalIntrinsicKindV1::Component,
136 intrinsic_identity: resolution.component_identity.clone(),
137 },
138 })
139 });
140 let model = normalize_authored_semantics_v1(parsed, candidates)
141 .map_err(ComponentInheritanceLoweringErrorV1::InvalidAuthoredSemantics)?;
142 Ok(ComponentInheritanceLoweringV1 { sites, model })
143}
144
145fn range(span: SourceSpan) -> AuthoredSourceRangeV1 {
146 AuthoredSourceRangeV1 {
147 start: span.start,
148 end: span.end,
149 line: span.line,
150 column: span.column,
151 }
152}
153
154fn range_key(range: AuthoredSourceRangeV1) -> (usize, usize) {
155 (range.start, range.end)
156}
157
158#[cfg(test)]
159mod tests {
160 use presolve_parser::parse_file;
161
162 use crate::{CanonicalAuthoredDeclarationKindV1, ResolvedIntrinsicIdentityV1};
163
164 use super::{
165 component_inheritance_sites_v1, lower_component_inheritance_v1,
166 ComponentInheritanceLoweringErrorV1, ResolvedComponentInheritanceV1,
167 };
168
169 fn resolution(start: usize, end: usize) -> ResolvedComponentInheritanceV1 {
170 ResolvedComponentInheritanceV1 {
171 heritage_source: crate::AuthoredSourceRangeV1 {
172 start,
173 end,
174 line: 1,
175 column: start + 1,
176 },
177 component_identity: ResolvedIntrinsicIdentityV1 {
178 name: "Component".to_owned(),
179 flags: 32,
180 declaration_modules: vec!["node_modules/presolve/src/index.d.ts".to_owned()],
181 },
182 }
183 }
184
185 #[test]
186 fn lowers_only_authority_proven_decorator_free_component_inheritance() {
187 let source = r#"
188import { Component as FrameworkBase } from "presolve";
189class Base extends FrameworkBase {}
190export class Counter extends Base { render() { return <main />; } }
191@component() class LegacyOnly { render() { return <main />; } }
192class NotAPresolveComponent extends Other {}
193"#;
194 let parsed = parse_file("app/routes/index.tsx", source);
195 let sites = component_inheritance_sites_v1(&parsed);
196 assert_eq!(sites.len(), 3);
197 assert_eq!(sites[0].subject, "Base");
198 assert_eq!(sites[1].subject, "Counter");
199
200 let model = lower_component_inheritance_v1(
201 &parsed,
202 [resolution(
203 sites[1].heritage_source.start,
204 sites[1].heritage_source.end,
205 )],
206 )
207 .expect("TypeScript-proven indirect inheritance is canonical");
208
209 assert_eq!(model.model.declarations.len(), 1);
210 assert_eq!(
211 model.model.declarations[0].kind,
212 CanonicalAuthoredDeclarationKindV1::Component
213 );
214 assert_eq!(model.model.declarations[0].subject, "Counter");
215 assert!(model
216 .model
217 .declarations
218 .iter()
219 .all(|declaration| declaration.subject != "LegacyOnly"));
220 }
221
222 #[test]
223 fn rejects_duplicate_and_dangling_authority_joins() {
224 let parsed = parse_file("src/Card.tsx", "class Card extends FrameworkBase {}");
225 let site = component_inheritance_sites_v1(&parsed).pop().unwrap();
226 let duplicate = lower_component_inheritance_v1(
227 &parsed,
228 [
229 resolution(site.heritage_source.start, site.heritage_source.end),
230 resolution(site.heritage_source.start, site.heritage_source.end),
231 ],
232 )
233 .expect_err("each heritage clause can have one authority result");
234 assert!(matches!(
235 duplicate,
236 ComponentInheritanceLoweringErrorV1::DuplicateResolution { .. }
237 ));
238
239 let dangling = lower_component_inheritance_v1(&parsed, [resolution(0, 5)])
240 .expect_err("authority results must join a source-AST heritage clause");
241 assert!(matches!(
242 dangling,
243 ComponentInheritanceLoweringErrorV1::UnknownHeritageResolution { .. }
244 ));
245 }
246}