1use super::css_modules::{
2 derive_class_name_rewrites_for_transform_context,
3 derive_css_module_composes_resolutions_for_transform_context,
4 derive_css_module_value_resolutions_for_transform_context,
5};
6use super::design_tokens::derive_design_token_routes_for_transform_context;
7use super::imports::derive_import_inlines_for_transform_context;
8use super::static_stylesheet::{
9 derive_static_scss_module_use_evaluations_for_transform_context,
10 derive_static_stylesheet_module_evaluation_for_transform_context,
11};
12use super::*;
13use crate::types::{
14 OmenaQueryEngineInputModuleAttributionV0, normalize_omena_query_style_path,
15 resolve_omena_query_style_path_against_known,
16};
17use omena_syntax::ident::ClassNameV0;
18use std::collections::{BTreeMap, BTreeSet};
19
20#[derive(Clone, Copy)]
21pub(super) struct TransformResolutionContext<'a> {
22 pub(super) package_manifests: &'a [OmenaQueryStylePackageManifestV0],
23 pub(super) bundler_path_mappings: &'a [OmenaResolverBundlerPathAliasMappingV0],
24 pub(super) tsconfig_path_mappings: &'a [OmenaResolverTsconfigPathMappingV0],
25 pub(super) disk_style_path_identities: &'a [OmenaResolverStyleModuleDiskCandidateIdentityV0],
26}
27
28impl<'a> TransformResolutionContext<'a> {
29 pub(super) fn from_resolution_inputs(
30 resolution_inputs: &'a OmenaQueryStyleResolutionInputsV0,
31 ) -> Self {
32 Self {
33 package_manifests: resolution_inputs.package_manifests.as_slice(),
34 bundler_path_mappings: resolution_inputs.bundler_path_mappings.as_slice(),
35 tsconfig_path_mappings: resolution_inputs.tsconfig_path_mappings.as_slice(),
36 disk_style_path_identities: resolution_inputs.disk_style_path_identities.as_slice(),
37 }
38 }
39
40 pub(super) fn resolve_style_module_source(
41 self,
42 from_style_path: &str,
43 source: &str,
44 available_style_paths: &BTreeSet<&str>,
45 ) -> Option<String> {
46 self.resolve_style_module(from_style_path, source, available_style_paths)
47 .resolved_style_path
48 }
49
50 pub(super) fn resolve_style_module(
51 self,
52 from_style_path: &str,
53 source: &str,
54 available_style_paths: &BTreeSet<&str>,
55 ) -> OmenaResolverStyleModuleResolutionV0 {
56 let load_path_roots = super::super::collect_load_path_roots(available_style_paths);
57 let load_path_root_refs = load_path_roots
58 .iter()
59 .map(String::as_str)
60 .collect::<Vec<_>>();
61 let resolver_package_manifests = self
62 .package_manifests
63 .iter()
64 .map(|manifest| OmenaResolverStylePackageManifestV0 {
65 package_json_path: manifest.package_json_path.clone(),
66 package_json_source: manifest.package_json_source.clone(),
67 })
68 .collect::<Vec<_>>();
69 summarize_omena_resolver_style_module_resolution_with_confirmation_inputs(
70 from_style_path,
71 source,
72 available_style_paths,
73 self.disk_style_path_identities,
74 &resolver_package_manifests,
75 self.bundler_path_mappings,
76 self.tsconfig_path_mappings,
77 &load_path_root_refs,
78 OmenaResolverStyleModuleConfirmationOptionsV0 {
79 allow_disk_confirmation: true,
80 ..OmenaResolverStyleModuleConfirmationOptionsV0::default()
81 },
82 )
83 }
84}
85
86pub(super) fn merge_transform_context(
87 mut merged: TransformExecutionContextV0,
88 context: &TransformExecutionContextV0,
89) -> TransformExecutionContextV0 {
90 merged.drop_dark_mode_media_queries =
91 merged.drop_dark_mode_media_queries || context.drop_dark_mode_media_queries;
92 if context.vendor_prefix_policy.is_some() {
93 merged.vendor_prefix_policy = context.vendor_prefix_policy;
94 }
95 if context.supports_target_capability.is_some() {
96 merged.supports_target_capability = context.supports_target_capability;
97 }
98 merge_context_list(
99 &mut merged.reachable_class_names,
100 &context.reachable_class_names,
101 );
102 merge_context_list(
103 &mut merged.reachable_keyframe_names,
104 &context.reachable_keyframe_names,
105 );
106 merge_context_list(
107 &mut merged.reachable_value_names,
108 &context.reachable_value_names,
109 );
110 merge_context_list(
111 &mut merged.reachable_custom_property_names,
112 &context.reachable_custom_property_names,
113 );
114
115 if context.scss_module_evaluation.is_some() {
116 merged.scss_module_evaluation = context.scss_module_evaluation.clone();
117 }
118 if context.less_module_evaluation.is_some() {
119 merged.less_module_evaluation = context.less_module_evaluation.clone();
120 }
121 if !context.import_inlines.is_empty() {
122 merge_context_records_by_key(
123 &mut merged.import_inlines,
124 &context.import_inlines,
125 |inline| inline.import_source.as_str(),
126 );
127 }
128 if !context.class_name_rewrites.is_empty() {
129 merge_class_context_records_by_key(
130 &mut merged.class_name_rewrites,
131 &context.class_name_rewrites,
132 |rewrite| rewrite.original_name.as_str(),
133 );
134 }
135 if !context.css_module_composes_resolutions.is_empty() {
136 merge_class_context_records_by_key(
137 &mut merged.css_module_composes_resolutions,
138 &context.css_module_composes_resolutions,
139 |resolution| resolution.local_class_name.as_str(),
140 );
141 }
142 if !context.css_module_value_resolutions.is_empty() {
143 merge_context_records_by_key(
144 &mut merged.css_module_value_resolutions,
145 &context.css_module_value_resolutions,
146 |resolution| resolution.local_name.as_str(),
147 );
148 }
149 if !context.design_token_routes.is_empty() {
150 merge_context_records_by_key(
151 &mut merged.design_token_routes,
152 &context.design_token_routes,
153 |route| route.token_name.as_str(),
154 );
155 }
156 if context.cascade_environment.is_some() {
157 merged.cascade_environment = context.cascade_environment.clone();
158 }
159
160 expand_reachable_class_names_through_composes(&mut merged);
161 merged
162}
163
164fn expand_reachable_class_names_through_composes(context: &mut TransformExecutionContextV0) {
165 let mut changed = true;
166 while changed {
167 changed = false;
168 for resolution in &context.css_module_composes_resolutions {
169 if !class_name_is_reachable(
170 &resolution.local_class_name,
171 &context.reachable_class_names,
172 ) {
173 continue;
174 }
175 for exported_class_name in &resolution.exported_class_names {
176 if !class_name_is_reachable(exported_class_name, &context.reachable_class_names) {
177 context
178 .reachable_class_names
179 .push(exported_class_name.clone());
180 changed = true;
181 }
182 }
183 }
184 }
185 context.reachable_class_names.sort();
186 context.reachable_class_names.dedup();
187}
188
189fn class_name_is_reachable(class_name: &str, reachable_class_names: &[String]) -> bool {
190 let Some(normalized_class_name) = normalize_reachable_class_name(class_name) else {
191 return false;
192 };
193 reachable_class_names
194 .iter()
195 .filter_map(|name| normalize_reachable_class_name(name))
196 .any(|name| css_identifier_names_match(name, normalized_class_name))
197}
198
199fn normalize_reachable_class_name(name: &str) -> Option<&str> {
200 let name = name.trim().strip_prefix('.').unwrap_or(name.trim());
201 (!name.is_empty()).then_some(name)
202}
203
204pub(super) fn css_identifier_names_match(left: &str, right: &str) -> bool {
205 ClassNameV0::new(left).same_as(&ClassNameV0::new(right))
206}
207
208pub(super) fn merge_target_options_transform_context(
209 context: &TransformExecutionContextV0,
210 target_options: OmenaQueryTargetTransformOptionsV0,
211) -> TransformExecutionContextV0 {
212 let mut merged = context.clone();
213 if target_options.drop_dark_mode_media_queries {
214 merged.drop_dark_mode_media_queries = true;
215 }
216 merged
217}
218
219pub(super) fn find_target_style_source<'a>(
220 target_style_path: &str,
221 style_sources: &'a [OmenaQueryStyleSourceInputV0],
222) -> Option<&'a str> {
223 style_sources
224 .iter()
225 .find(|source| source.style_path == target_style_path)
226 .map(|source| source.style_source.as_str())
227}
228
229pub(super) fn summarize_omena_query_transform_context_from_sources_with_resolution_context<'a>(
230 target_style_path: &str,
231 styles: impl IntoIterator<Item = (&'a str, &'a str)>,
232 resolution_context: TransformResolutionContext<'_>,
233) -> OmenaQueryTransformContextFromSourcesSummaryV0 {
234 derive_omena_query_transform_context_from_sources_with_resolution_context(
235 target_style_path,
236 styles,
237 resolution_context,
238 )
239 .summary
240}
241
242pub(super) struct OmenaQueryTransformContextFromSourcesDerivationV0 {
243 pub(super) summary: OmenaQueryTransformContextFromSourcesSummaryV0,
244 pub(super) style_fact_entries: Vec<OmenaQueryStyleFactEntry>,
245}
246
247pub(super) fn derive_omena_query_transform_context_from_sources_with_resolution_context<'a>(
248 target_style_path: &str,
249 styles: impl IntoIterator<Item = (&'a str, &'a str)>,
250 resolution_context: TransformResolutionContext<'_>,
251) -> OmenaQueryTransformContextFromSourcesDerivationV0 {
252 let style_sources = styles.into_iter().collect::<Vec<_>>();
253 let style_count = style_sources.len();
254 let style_fact_entries = collect_omena_query_style_fact_entries(style_sources.as_slice());
255 let source_by_path = style_sources
256 .iter()
257 .map(|(style_path, style_source)| ((*style_path).to_string(), (*style_source).to_string()))
258 .collect::<BTreeMap<_, _>>();
259 let available_style_paths = style_fact_entries
260 .iter()
261 .map(|entry| entry.style_path.as_str())
262 .collect::<BTreeSet<_>>();
263 let known_style_paths = available_style_paths
264 .iter()
265 .map(|path| normalize_omena_query_style_path(path))
266 .collect::<Vec<_>>();
267 let canonical_target_style_path =
268 resolve_omena_query_style_path_against_known(target_style_path, &known_style_paths)
269 .unwrap_or_else(|| normalize_omena_query_style_path(target_style_path));
270 let target_entry = style_fact_entries.iter().find(|entry| {
271 normalize_omena_query_style_path(entry.style_path.as_str()) == canonical_target_style_path
272 });
273
274 let mut context = TransformExecutionContextV0::default();
275
276 if let Some(entry) = target_entry {
277 context.import_inlines = derive_import_inlines_for_transform_context(
278 entry,
279 &style_fact_entries,
280 &available_style_paths,
281 &source_by_path,
282 resolution_context,
283 );
284 let scss_module_uses = derive_static_scss_module_use_evaluations_for_transform_context(
285 entry,
286 &available_style_paths,
287 &source_by_path,
288 resolution_context,
289 );
290 match omena_parser_dialect_for_style_path(entry.style_path.as_str()) {
291 OmenaParserStyleDialect::Scss | OmenaParserStyleDialect::Sass => {
292 let dialect = omena_parser_dialect_for_style_path(entry.style_path.as_str());
293 context.scss_module_evaluation =
294 derive_static_stylesheet_module_evaluation_for_transform_context(
295 entry.style_source.as_str(),
296 dialect,
297 &context.import_inlines,
298 &scss_module_uses,
299 );
300 }
301 OmenaParserStyleDialect::Less => {
302 context.less_module_evaluation =
303 derive_static_stylesheet_module_evaluation_for_transform_context(
304 entry.style_source.as_str(),
305 OmenaParserStyleDialect::Less,
306 &context.import_inlines,
307 &[],
308 );
309 }
310 OmenaParserStyleDialect::Css => {}
311 }
312 context.class_name_rewrites = derive_class_name_rewrites_for_transform_context(entry);
313 context.css_module_composes_resolutions =
314 derive_css_module_composes_resolutions_for_transform_context(
315 entry,
316 &style_fact_entries,
317 &available_style_paths,
318 resolution_context,
319 );
320 context.css_module_value_resolutions =
321 derive_css_module_value_resolutions_for_transform_context(
322 entry,
323 &style_fact_entries,
324 &available_style_paths,
325 &source_by_path,
326 resolution_context,
327 );
328 context.design_token_routes = derive_design_token_routes_for_transform_context(
329 entry,
330 &style_fact_entries,
331 resolution_context,
332 );
333 }
334
335 let summary = OmenaQueryTransformContextFromSourcesSummaryV0 {
336 schema_version: "0",
337 product: "omena-query.transform-context",
338 target_style_path: target_style_path.to_string(),
339 style_count,
340 import_inline_count: context.import_inlines.len(),
341 class_name_rewrite_count: context.class_name_rewrites.len(),
342 css_module_composes_resolution_count: context.css_module_composes_resolutions.len(),
343 css_module_value_resolution_count: context.css_module_value_resolutions.len(),
344 design_token_route_count: context.design_token_routes.len(),
345 reachable_class_name_count: context.reachable_class_names.len(),
346 reachable_keyframe_name_count: context.reachable_keyframe_names.len(),
347 reachable_value_name_count: context.reachable_value_names.len(),
348 reachable_custom_property_name_count: context.reachable_custom_property_names.len(),
349 context,
350 ready_surfaces: vec![
351 "transformContextProducer",
352 "stylesheetModuleEvaluationProducer",
353 "cssModuleClassRewriteProducer",
354 "cssModuleComposesResolutionProducer",
355 "cssModuleValueResolutionProducer",
356 "designTokenRouteProducer",
357 "transitiveImportInlineProducer",
358 ],
359 };
360 OmenaQueryTransformContextFromSourcesDerivationV0 {
361 summary,
362 style_fact_entries,
363 }
364}
365
366pub(super) struct OmenaQueryEngineInputTransformContextDerivationV0 {
367 pub(super) module_reachability: OmenaQueryEngineInputModuleReachabilityV0,
368 pub(super) reachability_precision: Option<FactPrecision>,
369 pub(super) closed_set_enumeration_candidate: bool,
370}
371
372pub fn summarize_omena_query_transform_context_from_engine_input(
373 input: &EngineInputV2,
374 target_style_path: &str,
375 closed_world_requested: bool,
376) -> OmenaQueryTransformContextFromEngineInputSummaryV0 {
377 derive_omena_query_transform_context_from_engine_input(
378 input,
379 target_style_path,
380 closed_world_requested,
381 )
382 .module_reachability
383 .into_summary()
384}
385
386pub fn derive_omena_query_module_reachability_from_engine_input(
387 input: &EngineInputV2,
388 target_style_path: &str,
389 closed_world_requested: bool,
390) -> OmenaQueryEngineInputModuleReachabilityV0 {
391 derive_omena_query_transform_context_from_engine_input(
392 input,
393 target_style_path,
394 closed_world_requested,
395 )
396 .module_reachability
397}
398
399pub(super) fn derive_omena_query_transform_context_from_engine_input(
400 input: &EngineInputV2,
401 target_style_path: &str,
402 closed_world_requested: bool,
403) -> OmenaQueryEngineInputTransformContextDerivationV0 {
404 let mut known_style_paths = input
405 .styles
406 .iter()
407 .map(|style| normalize_omena_query_style_path(style.file_path.as_str()))
408 .collect::<Vec<_>>();
409 known_style_paths.sort();
410 known_style_paths.dedup();
411 let canonical_target_style_path = resolve_omena_query_style_path_against_known(
412 target_style_path,
413 known_style_paths.as_slice(),
414 )
415 .unwrap_or_else(|| normalize_omena_query_style_path(target_style_path));
416 let (projection_summary, projection_precisions) =
417 omena_query_core::summarize_omena_query_expression_domain_selector_projection_with_precision_and_style_path_resolver(
418 input,
419 resolve_omena_query_style_path_against_known,
420 );
421 let precision_by_projection = projection_precisions
422 .iter()
423 .map(|entry| {
424 (
425 (entry.graph_id.as_str(), entry.node_id.as_str()),
426 entry.precision,
427 )
428 })
429 .collect::<BTreeMap<_, _>>();
430 let mut reachable_class_names = BTreeSet::new();
431 let mut reachability_sources = Vec::new();
432 let mut reachability_precision_ceiling: Option<FactPrecision> = None;
433 let mut closed_set_enumeration_candidate = true;
434 let mut selected_projection_count = 0_usize;
435 let mut targeted_class_names_by_style_path = BTreeMap::<String, BTreeSet<String>>::new();
436 let mut targeted_projection_count_by_style_path = BTreeMap::<String, usize>::new();
437 let mut unattributed_class_names = BTreeSet::new();
438 let mut unattributed_projection_count = 0_usize;
439 let mut projected_class_names = BTreeSet::new();
440
441 for projection in &projection_summary.projections {
442 projected_class_names.extend(projection.selector_names.iter().cloned());
443 if projection.target_style_paths.is_empty() {
444 unattributed_projection_count += 1;
445 unattributed_class_names.extend(projection.selector_names.iter().cloned());
446 } else {
447 let mut has_unresolved_target = false;
448 for target_style_path in projection
449 .target_style_paths
450 .iter()
451 .collect::<BTreeSet<_>>()
452 {
453 let Some(target_style_path) = resolve_omena_query_style_path_against_known(
454 target_style_path,
455 known_style_paths.as_slice(),
456 ) else {
457 let target_style_path = normalize_omena_query_style_path(target_style_path);
458 *targeted_projection_count_by_style_path
459 .entry(target_style_path.clone())
460 .or_default() += 1;
461 targeted_class_names_by_style_path
462 .entry(target_style_path)
463 .or_default()
464 .extend(projection.selector_names.iter().cloned());
465 has_unresolved_target = true;
466 continue;
467 };
468 *targeted_projection_count_by_style_path
469 .entry(target_style_path.clone())
470 .or_default() += 1;
471 targeted_class_names_by_style_path
472 .entry(target_style_path)
473 .or_default()
474 .extend(projection.selector_names.iter().cloned());
475 }
476 if has_unresolved_target {
477 unattributed_projection_count += 1;
478 unattributed_class_names.extend(projection.selector_names.iter().cloned());
479 }
480 }
481 let projection_targets_current_style = projection.target_style_paths.is_empty()
482 || projection.target_style_paths.iter().any(|path| {
483 resolve_omena_query_style_path_against_known(path, known_style_paths.as_slice())
484 .is_none_or(|path| path == canonical_target_style_path)
485 });
486 if projection_targets_current_style {
487 selected_projection_count += 1;
488 closed_set_enumeration_candidate &=
489 matches!(projection.value_kind, "bottom" | "exact" | "finiteSet");
490 reachable_class_names.extend(projection.selector_names.iter().cloned());
491 let projection_precision = precision_by_projection
492 .get(&(projection.graph_id.as_str(), projection.node_id.as_str()))
493 .copied()
494 .unwrap_or(FactPrecision::Unknown);
495 reachability_precision_ceiling = Some(
496 reachability_precision_ceiling.map_or(projection_precision, |current| {
497 current.bounded_by(projection_precision)
498 }),
499 );
500 reachability_sources.push(OmenaQuerySemanticReachabilitySourceV0 {
501 graph_id: projection.graph_id.clone(),
502 file_path: projection.file_path.clone(),
503 node_id: projection.node_id.clone(),
504 target_style_paths: projection.target_style_paths.clone(),
505 value_kind: projection.value_kind,
506 reduced_product: projection.reduced_product.clone(),
507 selector_names: projection.selector_names.clone(),
508 certainty: projection.certainty,
509 });
510 }
511 }
512
513 let semantic_context = TransformExecutionContextV0 {
514 reachable_class_names: reachable_class_names.into_iter().collect(),
515 ..TransformExecutionContextV0::default()
516 };
517 let style_sources = input
518 .styles
519 .iter()
520 .filter_map(|style| {
521 style
522 .source
523 .as_deref()
524 .map(|source| (style.file_path.as_str(), source))
525 })
526 .collect::<Vec<_>>();
527 let source_context_summary = (!style_sources.is_empty()).then(|| {
528 super::summarize_omena_query_transform_context_from_sources(
529 target_style_path,
530 style_sources,
531 &[],
532 )
533 });
534 let context = if let Some(source_context_summary) = &source_context_summary {
535 merge_transform_context(source_context_summary.context.clone(), &semantic_context)
536 } else {
537 semantic_context
538 };
539
540 let mut ready_surfaces = vec![
541 "expressionDomainSelectorProjection",
542 "semanticReachabilityTransformContext",
543 ];
544 if source_context_summary.is_some() {
545 ready_surfaces.push("engineInputStyleSourceTransformContext");
546 }
547
548 let summary = OmenaQueryTransformContextFromEngineInputSummaryV0 {
549 schema_version: "0",
550 product: "omena-query.transform-context-from-engine-input",
551 input_version: input.version.clone(),
552 target_style_path: target_style_path.to_string(),
553 closed_world_requested,
554 style_source_count: source_context_summary
555 .as_ref()
556 .map_or(0, |summary| summary.style_count),
557 projection_count: projection_summary.projection_count,
558 selected_projection_count: reachability_sources.len(),
559 import_inline_count: context.import_inlines.len(),
560 class_name_rewrite_count: context.class_name_rewrites.len(),
561 css_module_composes_resolution_count: context.css_module_composes_resolutions.len(),
562 css_module_value_resolution_count: context.css_module_value_resolutions.len(),
563 design_token_route_count: context.design_token_routes.len(),
564 reachable_class_name_count: context.reachable_class_names.len(),
565 reachable_keyframe_name_count: context.reachable_keyframe_names.len(),
566 reachable_value_name_count: context.reachable_value_names.len(),
567 reachable_custom_property_name_count: context.reachable_custom_property_names.len(),
568 reachability_sources,
569 context,
570 ready_surfaces,
571 };
572 let targeted_class_names_by_style_path = targeted_class_names_by_style_path
573 .into_iter()
574 .map(|(path, names)| (path, names.into_iter().collect()))
575 .collect();
576 let declared_class_names_by_style_path = input
577 .styles
578 .iter()
579 .map(|style| {
580 let mut class_names = style
581 .document
582 .selectors
583 .iter()
584 .filter_map(|selector| {
585 let name = selector
586 .canonical_name
587 .as_deref()
588 .unwrap_or(selector.name.as_str())
589 .trim();
590 let name = name.strip_prefix('.').unwrap_or(name);
591 (!name.is_empty()).then(|| name.to_string())
592 })
593 .collect::<Vec<_>>();
594 class_names.sort();
595 class_names.dedup();
596 (
597 normalize_omena_query_style_path(style.file_path.as_str()),
598 class_names,
599 )
600 })
601 .collect::<BTreeMap<_, _>>();
602 let module_reachability = OmenaQueryEngineInputModuleReachabilityV0::new(
603 summary,
604 known_style_paths,
605 OmenaQueryEngineInputModuleAttributionV0::new(
606 declared_class_names_by_style_path,
607 targeted_class_names_by_style_path,
608 targeted_projection_count_by_style_path,
609 ),
610 unattributed_class_names.into_iter().collect(),
611 unattributed_projection_count,
612 projected_class_names.into_iter().collect(),
613 );
614
615 OmenaQueryEngineInputTransformContextDerivationV0 {
616 reachability_precision: reachability_precision_ceiling,
617 closed_set_enumeration_candidate: selected_projection_count > 0
618 && closed_set_enumeration_candidate,
619 module_reachability,
620 }
621}
622
623fn merge_context_list(target: &mut Vec<String>, additional: &[String]) {
624 for item in additional {
625 if !target.contains(item) {
626 target.push(item.clone());
627 }
628 }
629 target.sort();
630}
631
632fn merge_context_records_by_key<T, F>(target: &mut Vec<T>, overrides: &[T], key: F)
633where
634 T: Clone,
635 F: Fn(&T) -> &str,
636{
637 for item in overrides {
638 let item_key = key(item);
639 if let Some(existing) = target.iter_mut().find(|existing| key(existing) == item_key) {
640 *existing = item.clone();
641 } else {
642 target.push(item.clone());
643 }
644 }
645 target.sort_by(|left, right| key(left).cmp(key(right)));
646}
647
648fn merge_class_context_records_by_key<T, F>(target: &mut Vec<T>, overrides: &[T], key: F)
649where
650 T: Clone,
651 F: Fn(&T) -> &str,
652{
653 let mut merged = Vec::with_capacity(target.len() + overrides.len());
658 append_class_context_records_first_witness(&mut merged, overrides, &key);
659 append_class_context_records_first_witness(&mut merged, target, &key);
660 *target = merged;
661}
662
663fn append_class_context_records_first_witness<T, F>(target: &mut Vec<T>, candidates: &[T], key: &F)
664where
665 T: Clone,
666 F: Fn(&T) -> &str,
667{
668 let occupied_canonical_keys = target
669 .iter()
670 .map(|existing| ClassNameV0::new(key(existing)).canonical_key())
671 .collect::<BTreeSet<_>>();
672 let mut admitted_raw_keys = target
673 .iter()
674 .map(|existing| key(existing).to_string())
675 .collect::<BTreeSet<_>>();
676 for item in candidates {
677 let item_key = ClassNameV0::new(key(item));
678 if !occupied_canonical_keys.contains(&item_key.canonical_key())
679 && admitted_raw_keys.insert(key(item).to_string())
680 {
681 target.push(item.clone());
682 }
683 }
684}
685
686pub(super) fn merge_module_css_module_contexts_first_witness(
687 left: &[TransformModuleCssModuleContextV0],
688 right: &[TransformModuleCssModuleContextV0],
689) -> Vec<TransformModuleCssModuleContextV0> {
690 let mut merged = Vec::<TransformModuleCssModuleContextV0>::new();
691 for context in left.iter().chain(right) {
692 let target = if let Some(index) = merged
693 .iter()
694 .position(|candidate| candidate.module_instance == context.module_instance)
695 {
696 &mut merged[index]
697 } else {
698 let index = merged.len();
699 merged.push(TransformModuleCssModuleContextV0::new(
700 context.module_instance.clone(),
701 ));
702 &mut merged[index]
703 };
704 append_class_context_records_first_witness(
705 &mut target.class_name_rewrites,
706 context.class_name_rewrites.as_slice(),
707 &|rewrite: &TransformClassNameRewriteV0| rewrite.original_name.as_str(),
708 );
709 append_class_context_records_first_witness(
710 &mut target.composes_resolutions,
711 context.composes_resolutions.as_slice(),
712 &|resolution: &TransformCssModuleComposesResolutionV0| {
713 resolution.local_class_name.as_str()
714 },
715 );
716 }
717 merged
718}
719
720#[cfg(test)]
721mod tests {
722 use super::*;
723 use omena_query_transform_runner::{
724 TransformClassNameRewriteV0, TransformCssModuleComposesResolutionV0,
725 TransformCssModuleValueResolutionV0, TransformDesignTokenRouteV0, TransformImportInlineV0,
726 };
727
728 fn class_rewrite(original_name: &str, rewritten_name: &str) -> TransformClassNameRewriteV0 {
729 TransformClassNameRewriteV0 {
730 original_name: original_name.to_string(),
731 rewritten_name: rewritten_name.to_string(),
732 }
733 }
734
735 #[test]
736 fn explicit_class_rewrite_precedence_ignores_escape_spelling_order() {
737 for (derived_name, explicit_name) in [(r#"\E9 tat"#, "état"), ("état", r#"\E9 tat"#)] {
738 let derived = TransformExecutionContextV0 {
739 class_name_rewrites: vec![class_rewrite(derived_name, "_derived")],
740 css_module_composes_resolutions: vec![TransformCssModuleComposesResolutionV0 {
741 local_class_name: derived_name.to_string(),
742 exported_class_names: vec!["derived".to_string()],
743 }],
744 ..TransformExecutionContextV0::default()
745 };
746 let explicit = TransformExecutionContextV0 {
747 class_name_rewrites: vec![class_rewrite(explicit_name, "_explicit")],
748 css_module_composes_resolutions: vec![TransformCssModuleComposesResolutionV0 {
749 local_class_name: explicit_name.to_string(),
750 exported_class_names: vec!["explicit".to_string()],
751 }],
752 ..TransformExecutionContextV0::default()
753 };
754
755 let merged = merge_transform_context(derived, &explicit);
756
757 assert_eq!(
758 merged.class_name_rewrites,
759 vec![class_rewrite(explicit_name, "_explicit")]
760 );
761 assert_eq!(
762 merged.css_module_composes_resolutions,
763 vec![TransformCssModuleComposesResolutionV0 {
764 local_class_name: explicit_name.to_string(),
765 exported_class_names: vec!["explicit".to_string()],
766 }]
767 );
768 println!(
769 "canonical-merge derived={derived_name:?} explicit={explicit_name:?} winner={:?}",
770 merged.class_name_rewrites
771 );
772 }
773 }
774
775 #[test]
776 fn class_merge_normalization_does_not_apply_to_other_context_keys() {
777 let derived = TransformExecutionContextV0 {
778 import_inlines: vec![TransformImportInlineV0 {
779 import_source: r#"\E9 tat"#.to_string(),
780 replacement_css: "derived".to_string(),
781 }],
782 css_module_value_resolutions: vec![TransformCssModuleValueResolutionV0 {
783 local_name: r#"\E9 tat"#.to_string(),
784 resolved_value: "derived".to_string(),
785 }],
786 design_token_routes: vec![TransformDesignTokenRouteV0 {
787 token_name: r#"\E9 tat"#.to_string(),
788 routed_value: "derived".to_string(),
789 }],
790 ..TransformExecutionContextV0::default()
791 };
792 let explicit = TransformExecutionContextV0 {
793 import_inlines: vec![TransformImportInlineV0 {
794 import_source: "état".to_string(),
795 replacement_css: "explicit".to_string(),
796 }],
797 css_module_value_resolutions: vec![TransformCssModuleValueResolutionV0 {
798 local_name: "état".to_string(),
799 resolved_value: "explicit".to_string(),
800 }],
801 design_token_routes: vec![TransformDesignTokenRouteV0 {
802 token_name: "état".to_string(),
803 routed_value: "explicit".to_string(),
804 }],
805 ..TransformExecutionContextV0::default()
806 };
807
808 let merged = merge_transform_context(derived, &explicit);
809
810 assert_eq!(merged.import_inlines.len(), 2);
811 assert_eq!(merged.css_module_value_resolutions.len(), 2);
812 assert_eq!(merged.design_token_routes.len(), 2);
813 }
814
815 fn module_context(
816 module: &str,
817 original_name: &str,
818 rewritten_name: &str,
819 ) -> TransformModuleCssModuleContextV0 {
820 TransformModuleCssModuleContextV0::new(omena_parser::ModuleInstanceKeyV0::unconfigured(
821 omena_parser::ModuleIdV0::new(module),
822 ))
823 .with_class_name_rewrites(vec![class_rewrite(original_name, rewritten_name)])
824 }
825
826 fn merge_module_css_module_contexts_last_witness(
827 left: &[TransformModuleCssModuleContextV0],
828 right: &[TransformModuleCssModuleContextV0],
829 ) -> Vec<TransformModuleCssModuleContextV0> {
830 merge_module_css_module_contexts_first_witness(right, left)
831 }
832
833 #[test]
834 fn module_context_explicit_precedence_ignores_escape_spelling_order() {
835 for (derived_name, explicit_name) in [(r#"\E9 tat"#, "état"), ("état", r#"\E9 tat"#)] {
836 let explicit = vec![module_context(
837 "src/app.module.css",
838 explicit_name,
839 "_explicit",
840 )];
841 let derived = vec![module_context(
842 "src/app.module.css",
843 derived_name,
844 "_derived",
845 )];
846
847 let merged = merge_module_css_module_contexts_first_witness(&explicit, &derived);
848
849 assert_eq!(
853 merged,
854 vec![module_context(
855 "src/app.module.css",
856 explicit_name,
857 "_explicit",
858 )]
859 );
860 println!(
861 "module-canonical-merge derived={derived_name:?} explicit={explicit_name:?} winner={:?}",
862 merged[0].class_name_rewrites
863 );
864 }
865 }
866
867 #[test]
868 fn module_context_first_witness_merge_obeys_left_regular_band_laws() {
869 let u = vec![
870 module_context("src/a.module.css", "shared", "_u"),
871 module_context("src/b.module.css", "own", "_b"),
872 ];
873 let v = vec![
874 module_context("src/a.module.css", "shared", "_v"),
875 module_context("src/c.module.css", "own", "_c"),
876 ];
877 let w = vec![module_context("src/a.module.css", "third", "_w")];
878
879 let uv = merge_module_css_module_contexts_first_witness(&u, &v);
880 assert_eq!(
881 merge_module_css_module_contexts_first_witness(&uv, &w),
882 merge_module_css_module_contexts_first_witness(
883 &u,
884 &merge_module_css_module_contexts_first_witness(&v, &w),
885 ),
886 "associativity"
887 );
888 assert_eq!(
889 merge_module_css_module_contexts_first_witness(&u, &u),
890 u,
891 "idempotence"
892 );
893 assert_eq!(
894 merge_module_css_module_contexts_first_witness(&uv, &u),
895 uv,
896 "left-regular-band absorption"
897 );
898
899 let last_wins_uv = merge_module_css_module_contexts_last_witness(&u, &v);
900 let last_wins_uv_then_u = merge_module_css_module_contexts_last_witness(&last_wins_uv, &u);
901 assert_ne!(
902 last_wins_uv_then_u, last_wins_uv,
903 "a last-wins variant must fail the absorption control"
904 );
905 }
906}