1use engine_input_producers::{
2 EngineInputV2, ExpressionDomainControlFlowAnalysisV0, ExpressionDomainFlowAnalysisV0,
3 ExpressionSemanticsCanonicalProducerSignalV0, ExpressionSemanticsQueryFragmentsV0,
4 SelectorUsageCanonicalProducerSignalV0, SelectorUsageQueryFragmentsV0,
5 SourceResolutionCanonicalProducerSignalV0, SourceResolutionQueryFragmentsV0,
6 collect_expression_domain_flow_graphs, summarize_expression_domain_control_flow_analysis_input,
7 summarize_expression_domain_flow_analysis_input,
8 summarize_expression_semantics_canonical_producer_signal_input,
9 summarize_expression_semantics_query_fragments_input,
10 summarize_selector_usage_canonical_producer_signal_input,
11 summarize_selector_usage_query_fragments_input,
12};
13use std::collections::{BTreeMap, BTreeSet, VecDeque};
14use std::path::{Component, Path, PathBuf};
15
16use engine_style_parser::{
17 AtRuleKind, Stylesheet, SyntaxNodePayload, parse_style_module,
18 summarize_css_modules_intermediate,
19};
20pub use engine_style_parser::{ParserByteSpanV0, ParserPositionV0, ParserRangeV0, StyleLanguage};
21use omena_abstract_value::{
22 AbstractValueDomainSummaryV0, ClassValueFlowAnalysisV0, ClassValueFlowIncrementalAnalysisV0,
23 SelectorProjectionCertaintyV0, analyze_class_value_flow_incremental_with_database,
24 project_abstract_value_selectors, summarize_omena_abstract_value_domain,
25};
26use omena_bridge::{
27 DesignTokenExternalDeclarationCandidateScopeV0, DesignTokenWorkspaceDeclarationFactV0,
28 StyleSemanticGraphSummaryV0, collect_omena_bridge_design_token_workspace_declarations,
29 summarize_omena_bridge_style_semantic_graph_for_path_with_scoped_workspace_declarations,
30 summarize_omena_bridge_style_semantic_graph_from_source,
31};
32pub use omena_bridge::{
33 SourceImportDeclarationSummaryV0 as OmenaQuerySourceImportDeclarationSummaryV0,
34 SourceImportDeclarationV0 as OmenaQuerySourceImportDeclarationV0,
35 SourceImportedStyleBindingV0 as OmenaQuerySourceImportedStyleBindingV0,
36 SourceSelectorReferenceFactV0 as OmenaQuerySourceSelectorReferenceFactV0,
37 SourceSelectorReferenceMatchKindV0 as OmenaQuerySourceSelectorReferenceMatchKindV0,
38 SourceSyntaxIndexV0 as OmenaQuerySourceSyntaxIndexV0,
39 SourceTypeFactTargetV0 as OmenaQuerySourceTypeFactTargetV0,
40};
41use omena_incremental::OmenaIncrementalDatabaseV0;
42use omena_resolver::{
43 OmenaResolverSourceResolutionRuntimeIndexV0,
44 summarize_omena_resolver_canonical_producer_signal, summarize_omena_resolver_query_fragments,
45 summarize_omena_resolver_source_resolution_runtime,
46};
47use serde::Serialize;
48
49pub fn summarize_omena_query_source_import_declarations(
50 source: &str,
51) -> OmenaQuerySourceImportDeclarationSummaryV0 {
52 omena_bridge::summarize_omena_bridge_source_import_declarations(source)
53}
54
55pub fn resolve_omena_query_style_uri_for_specifier(
56 base_document_uri: &str,
57 workspace_folder_uri: Option<&str>,
58 specifier: &str,
59) -> Option<String> {
60 omena_bridge::resolve_omena_bridge_style_uri_for_specifier(
61 base_document_uri,
62 workspace_folder_uri,
63 specifier,
64 )
65}
66
67pub fn summarize_omena_query_source_syntax_index(
68 source: &str,
69 imported_style_bindings: Vec<OmenaQuerySourceImportedStyleBindingV0>,
70 classnames_bind_bindings: Vec<String>,
71) -> OmenaQuerySourceSyntaxIndexV0 {
72 omena_bridge::summarize_omena_bridge_source_syntax_index(
73 source,
74 imported_style_bindings,
75 classnames_bind_bindings,
76 )
77}
78
79pub fn canonicalize_omena_query_source_selector_references(
80 references: &mut Vec<OmenaQuerySourceSelectorReferenceFactV0>,
81) {
82 omena_bridge::canonicalize_source_selector_references(references);
83}
84
85#[derive(Debug, Serialize)]
86#[serde(rename_all = "camelCase")]
87pub struct OmenaQueryBoundarySummaryV0 {
88 pub schema_version: &'static str,
89 pub product: &'static str,
90 pub query_engine_name: &'static str,
91 pub input_version: String,
92 pub abstract_value_domain: AbstractValueDomainSummaryV0,
93 pub selected_query_adapter_capabilities: SelectedQueryAdapterCapabilitiesV0,
94 pub delegated_fragment_products: Vec<&'static str>,
95 pub expression_semantics_query_count: usize,
96 pub source_resolution_query_count: usize,
97 pub selector_usage_query_count: usize,
98 pub total_query_count: usize,
99 pub ready_surfaces: Vec<&'static str>,
100 pub cme_coupled_surfaces: Vec<&'static str>,
101 pub next_decoupling_targets: Vec<&'static str>,
102}
103
104#[derive(Debug, Serialize)]
105#[serde(rename_all = "camelCase")]
106pub struct OmenaQueryFragmentBundleV0 {
107 pub schema_version: &'static str,
108 pub product: &'static str,
109 pub input_version: String,
110 pub expression_semantics: ExpressionSemanticsQueryFragmentsV0,
111 pub source_resolution: SourceResolutionQueryFragmentsV0,
112 pub selector_usage: SelectorUsageQueryFragmentsV0,
113}
114
115#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
116#[serde(rename_all = "camelCase")]
117pub struct SelectedQueryAdapterCapabilitiesV0 {
118 pub schema_version: &'static str,
119 pub product: &'static str,
120 pub default_candidate_backend: &'static str,
121 pub backend_kinds: Vec<SelectedQueryBackendCapabilityV0>,
122 pub runner_commands: Vec<SelectedQueryRunnerCommandV0>,
123 pub expression_semantics_payload_contracts: Vec<&'static str>,
124 pub required_input_contracts: Vec<&'static str>,
125 pub adapter_readiness: Vec<&'static str>,
126 pub routing_status: &'static str,
127}
128
129#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
130#[serde(rename_all = "camelCase")]
131pub struct SelectedQueryBackendCapabilityV0 {
132 pub backend_kind: &'static str,
133 pub source_resolution: bool,
134 pub expression_semantics: bool,
135 pub selector_usage: bool,
136 pub style_semantic_graph: bool,
137}
138
139#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
140#[serde(rename_all = "camelCase")]
141pub struct SelectedQueryRunnerCommandV0 {
142 pub surface: &'static str,
143 pub command: &'static str,
144 pub input_contract: &'static str,
145 pub output_product: &'static str,
146}
147
148#[derive(Debug, Serialize)]
149#[serde(rename_all = "camelCase")]
150pub struct OmenaQueryStyleSemanticGraphBatchOutputV0 {
151 pub schema_version: &'static str,
152 pub product: &'static str,
153 pub graphs: Vec<OmenaQueryStyleSemanticGraphBatchEntryV0>,
154}
155
156#[derive(Debug, Serialize)]
157#[serde(rename_all = "camelCase")]
158pub struct OmenaQueryStyleSemanticGraphBatchEntryV0 {
159 pub style_path: String,
160 pub graph: Option<StyleSemanticGraphSummaryV0>,
161}
162
163#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
164#[serde(rename_all = "camelCase")]
165pub struct OmenaQueryStyleDocumentSummaryV0 {
166 pub schema_version: &'static str,
167 pub product: &'static str,
168 pub language: &'static str,
169 pub selector_names: Vec<String>,
170 pub custom_property_decl_names: Vec<String>,
171 pub custom_property_ref_names: Vec<String>,
172 pub sass_module_use_sources: Vec<String>,
173 pub sass_module_forward_sources: Vec<String>,
174 pub diagnostic_count: usize,
175}
176
177#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
178#[serde(rename_all = "camelCase")]
179pub struct OmenaQueryStyleHoverCandidateV0 {
180 pub kind: &'static str,
181 pub name: String,
182 pub range: ParserRangeV0,
183 pub source: &'static str,
184 pub namespace: Option<String>,
185}
186
187#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
188#[serde(rename_all = "camelCase")]
189pub struct OmenaQueryStyleHoverCandidatesV0 {
190 pub schema_version: &'static str,
191 pub product: &'static str,
192 pub language: &'static str,
193 pub candidates: Vec<OmenaQueryStyleHoverCandidateV0>,
194}
195
196#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
197#[serde(rename_all = "camelCase")]
198pub struct OmenaQueryStyleHoverRenderPartsV0 {
199 pub schema_version: &'static str,
200 pub product: &'static str,
201 pub snippet: String,
202 pub value: Option<String>,
203 pub signature: Option<String>,
204 pub render_source: &'static str,
205}
206
207#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
208#[serde(rename_all = "camelCase")]
209pub struct OmenaQueryStyleDiagnosticV0 {
210 pub code: &'static str,
211 pub range: ParserRangeV0,
212 pub message: String,
213 pub create_custom_property: Option<OmenaQueryCreateCustomPropertyActionV0>,
214}
215
216#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
217#[serde(rename_all = "camelCase")]
218pub struct OmenaQueryCreateCustomPropertyActionV0 {
219 pub uri: String,
220 pub range: ParserRangeV0,
221 pub new_text: String,
222 pub property_name: String,
223}
224
225#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
226#[serde(rename_all = "camelCase")]
227pub struct OmenaQuerySourceDiagnosticV0 {
228 pub code: &'static str,
229 pub range: ParserRangeV0,
230 pub message: String,
231 pub create_selector: Option<OmenaQueryCreateSelectorActionV0>,
232}
233
234#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
235#[serde(rename_all = "camelCase")]
236pub struct OmenaQueryCreateSelectorActionV0 {
237 pub uri: String,
238 pub range: ParserRangeV0,
239 pub new_text: String,
240 pub selector_name: String,
241}
242
243#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
244#[serde(rename_all = "camelCase")]
245pub struct OmenaQuerySourceSelectorCandidateV0 {
246 pub kind: &'static str,
247 pub name: String,
248 pub range: ParserRangeV0,
249 pub source: &'static str,
250 pub target_style_uri: Option<String>,
251}
252
253#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
254#[serde(rename_all = "camelCase")]
255pub struct OmenaQueryStyleSelectorDefinitionV0 {
256 pub uri: String,
257 pub name: String,
258 pub range: ParserRangeV0,
259}
260
261#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
262#[serde(rename_all = "camelCase")]
263pub struct OmenaQuerySourceProviderCandidateResolutionV0 {
264 pub schema_version: &'static str,
265 pub product: &'static str,
266 pub matched: Vec<OmenaQuerySourceSelectorCandidateV0>,
267 pub unresolved: Vec<OmenaQuerySourceSelectorCandidateV0>,
268}
269
270#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
271#[serde(rename_all = "camelCase")]
272pub struct OmenaQuerySourceSelectorReferenceEditTargetV0 {
273 pub uri: String,
274 pub name: String,
275 pub range: ParserRangeV0,
276 pub target_style_uri: Option<String>,
277}
278
279#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
280#[serde(rename_all = "camelCase")]
281pub struct OmenaQueryWorkspaceTextEditV0 {
282 pub uri: String,
283 pub range: ParserRangeV0,
284 pub new_text: String,
285}
286
287#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
288#[serde(rename_all = "camelCase")]
289pub struct OmenaQuerySassModuleUseEdgeV0 {
290 pub source: String,
291 pub namespace_kind: &'static str,
292 pub namespace: Option<String>,
293}
294
295#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
296#[serde(rename_all = "camelCase")]
297pub struct OmenaQuerySassModuleSourcesV0 {
298 pub schema_version: &'static str,
299 pub product: &'static str,
300 pub module_use_edges: Vec<OmenaQuerySassModuleUseEdgeV0>,
301 pub module_forward_sources: Vec<String>,
302}
303
304#[derive(Debug, Clone, PartialEq, Eq)]
305pub struct OmenaQueryStylePackageManifestV0 {
306 pub package_json_path: String,
307 pub package_json_source: String,
308}
309
310#[derive(Debug, Serialize)]
311#[serde(rename_all = "camelCase")]
312pub struct OmenaQueryExpressionDomainIncrementalFlowAnalysisV0 {
313 pub schema_version: &'static str,
314 pub product: &'static str,
315 pub input_version: String,
316 pub revision: u64,
317 pub graph_count: usize,
318 pub dirty_graph_count: usize,
319 pub reused_graph_count: usize,
320 pub analyses: Vec<OmenaQueryExpressionDomainIncrementalFlowAnalysisEntryV0>,
321}
322
323#[derive(Debug, Serialize)]
324#[serde(rename_all = "camelCase")]
325pub struct OmenaQueryExpressionDomainIncrementalFlowAnalysisEntryV0 {
326 pub graph_id: String,
327 pub file_path: String,
328 pub analysis: ClassValueFlowIncrementalAnalysisV0,
329}
330
331#[derive(Debug, Serialize)]
332#[serde(rename_all = "camelCase")]
333pub struct OmenaQueryExpressionDomainSelectorProjectionV0 {
334 pub schema_version: &'static str,
335 pub product: &'static str,
336 pub input_version: String,
337 pub projection_count: usize,
338 pub projections: Vec<OmenaQueryExpressionDomainSelectorProjectionEntryV0>,
339}
340
341#[derive(Debug, Serialize)]
342#[serde(rename_all = "camelCase")]
343pub struct OmenaQueryExpressionDomainSelectorProjectionEntryV0 {
344 pub graph_id: String,
345 pub file_path: String,
346 pub node_id: String,
347 pub target_style_paths: Vec<String>,
348 pub value_kind: &'static str,
349 pub selector_names: Vec<String>,
350 pub certainty: SelectorProjectionCertaintyV0,
351}
352
353#[derive(Default)]
354pub struct OmenaQueryExpressionDomainFlowRuntimeV0 {
355 revision: u64,
356 databases_by_graph_id: BTreeMap<String, OmenaIncrementalDatabaseV0>,
357 previous_analyses_by_graph_id: BTreeMap<String, ClassValueFlowAnalysisV0>,
358}
359
360pub fn summarize_omena_query_boundary(input: &EngineInputV2) -> OmenaQueryBoundarySummaryV0 {
361 let fragment_bundle = summarize_omena_query_fragment_bundle(input);
362 let expression_semantics_query_count = fragment_bundle.expression_semantics.fragments.len();
363 let source_resolution_query_count = fragment_bundle.source_resolution.fragments.len();
364 let selector_usage_query_count = fragment_bundle.selector_usage.fragments.len();
365
366 OmenaQueryBoundarySummaryV0 {
367 schema_version: "0",
368 product: "omena-query.boundary",
369 query_engine_name: "omena-query",
370 input_version: input.version.clone(),
371 abstract_value_domain: summarize_omena_abstract_value_domain(),
372 selected_query_adapter_capabilities:
373 summarize_omena_query_selected_query_adapter_capabilities(),
374 delegated_fragment_products: vec![
375 "engine-input-producers.expression-semantics-query-fragments",
376 "engine-input-producers.source-resolution-query-fragments",
377 "omena-resolver.boundary",
378 "omena-resolver.source-resolution-runtime-index",
379 "engine-input-producers.selector-usage-query-fragments",
380 "engine-input-producers.expression-domain-flow-analysis",
381 "engine-input-producers.expression-domain-control-flow-analysis",
382 "omena-query.expression-domain-incremental-flow-analysis",
383 "omena-query.expression-domain-selector-projection",
384 ],
385 expression_semantics_query_count,
386 source_resolution_query_count,
387 selector_usage_query_count,
388 total_query_count: expression_semantics_query_count
389 + source_resolution_query_count
390 + selector_usage_query_count,
391 ready_surfaces: vec![
392 "queryFragmentBundle",
393 "abstractValueProjectionContract",
394 "sourceResolutionResolverBoundary",
395 "sourceResolutionRuntimeIndex",
396 "expressionDomainFlowAnalysisBoundary",
397 "expressionDomainControlFlowAnalysisBoundary",
398 "expressionDomainSalsaRuntime",
399 "expressionDomainSelectorProjection",
400 "styleHoverRenderParts",
401 "styleMissingCustomPropertyDiagnostics",
402 "sourceMissingSelectorDiagnostics",
403 "sourceProviderCandidateResolution",
404 "selectorRenameEditPlanning",
405 "sassSymbolResolutionPrimitives",
406 "sassModuleSourceSelection",
407 "queryBoundarySummary",
408 ],
409 cme_coupled_surfaces: vec!["EngineInputV2", "producerQueryFragments"],
410 next_decoupling_targets: vec!["queryEvaluationRuntime", "selectedQueryBackendAdapter"],
411 }
412}
413
414pub fn summarize_omena_query_selected_query_adapter_capabilities()
415-> SelectedQueryAdapterCapabilitiesV0 {
416 SelectedQueryAdapterCapabilitiesV0 {
417 schema_version: "0",
418 product: "omena-query.selected-query-adapter-capabilities",
419 default_candidate_backend: "rust-selected-query",
420 backend_kinds: vec![
421 SelectedQueryBackendCapabilityV0 {
422 backend_kind: "typescript-current",
423 source_resolution: false,
424 expression_semantics: false,
425 selector_usage: false,
426 style_semantic_graph: false,
427 },
428 SelectedQueryBackendCapabilityV0 {
429 backend_kind: "rust-source-resolution",
430 source_resolution: true,
431 expression_semantics: false,
432 selector_usage: false,
433 style_semantic_graph: false,
434 },
435 SelectedQueryBackendCapabilityV0 {
436 backend_kind: "rust-expression-semantics",
437 source_resolution: false,
438 expression_semantics: true,
439 selector_usage: false,
440 style_semantic_graph: false,
441 },
442 SelectedQueryBackendCapabilityV0 {
443 backend_kind: "rust-selector-usage",
444 source_resolution: false,
445 expression_semantics: false,
446 selector_usage: true,
447 style_semantic_graph: false,
448 },
449 SelectedQueryBackendCapabilityV0 {
450 backend_kind: "rust-selected-query",
451 source_resolution: true,
452 expression_semantics: true,
453 selector_usage: true,
454 style_semantic_graph: true,
455 },
456 ],
457 runner_commands: vec![
458 SelectedQueryRunnerCommandV0 {
459 surface: "sourceResolution",
460 command: "input-source-resolution-canonical-producer",
461 input_contract: "EngineInputV2",
462 output_product: "engine-input-producers.source-resolution-canonical-producer",
463 },
464 SelectedQueryRunnerCommandV0 {
465 surface: "sourceResolutionRuntime",
466 command: "input-omena-resolver-source-resolution-runtime",
467 input_contract: "EngineInputV2",
468 output_product: "omena-resolver.source-resolution-runtime-index",
469 },
470 SelectedQueryRunnerCommandV0 {
471 surface: "expressionSemantics",
472 command: "input-expression-semantics-canonical-producer",
473 input_contract: "EngineInputV2",
474 output_product: "engine-input-producers.expression-semantics-canonical-producer",
475 },
476 SelectedQueryRunnerCommandV0 {
477 surface: "expressionDomainFlowAnalysis",
478 command: "input-expression-domain-flow-analysis",
479 input_contract: "EngineInputV2",
480 output_product: "engine-input-producers.expression-domain-flow-analysis",
481 },
482 SelectedQueryRunnerCommandV0 {
483 surface: "expressionDomainControlFlowAnalysis",
484 command: "input-expression-domain-control-flow-analysis",
485 input_contract: "EngineInputV2",
486 output_product: "engine-input-producers.expression-domain-control-flow-analysis",
487 },
488 SelectedQueryRunnerCommandV0 {
489 surface: "expressionDomainIncrementalFlowAnalysis",
490 command: "input-expression-domain-incremental-flow-analysis",
491 input_contract: "EngineInputV2 + OmenaQueryExpressionDomainFlowRuntimeV0",
492 output_product: "omena-query.expression-domain-incremental-flow-analysis",
493 },
494 SelectedQueryRunnerCommandV0 {
495 surface: "expressionDomainSelectorProjection",
496 command: "input-expression-domain-selector-projection",
497 input_contract: "EngineInputV2",
498 output_product: "omena-query.expression-domain-selector-projection",
499 },
500 SelectedQueryRunnerCommandV0 {
501 surface: "selectorUsage",
502 command: "input-selector-usage-canonical-producer",
503 input_contract: "EngineInputV2",
504 output_product: "engine-input-producers.selector-usage-canonical-producer",
505 },
506 SelectedQueryRunnerCommandV0 {
507 surface: "styleSemanticGraph",
508 command: "style-semantic-graph",
509 input_contract: "StyleSemanticGraphInputV0",
510 output_product: "omena-semantic.style-semantic-graph",
511 },
512 SelectedQueryRunnerCommandV0 {
513 surface: "styleSemanticGraphBatch",
514 command: "style-semantic-graph-batch",
515 input_contract: "StyleSemanticGraphBatchInputV0",
516 output_product: "omena-semantic.style-semantic-graph-batch",
517 },
518 ],
519 expression_semantics_payload_contracts: vec!["valueDomainKind", "valueDomainDerivation"],
520 required_input_contracts: vec![
521 "EngineInputV2",
522 "StyleSemanticGraphInputV0",
523 "StyleSemanticGraphBatchInputV0",
524 ],
525 adapter_readiness: vec![
526 "backendCapabilityMatrix",
527 "canonicalProducerWrapperBoundary",
528 "styleSemanticGraphBridgeBoundary",
529 "runnerCommandContract",
530 "fragmentBundleBoundary",
531 "sourceResolutionRuntimeIndex",
532 "expressionSemanticsDerivationPayload",
533 "expressionDomainFlowAnalysisRunner",
534 "expressionDomainControlFlowAnalysisRunner",
535 "expressionDomainSalsaRuntime",
536 "expressionDomainSelectorProjection",
537 ],
538 routing_status: "declaredOnly",
539 }
540}
541
542pub fn summarize_omena_query_fragment_bundle(input: &EngineInputV2) -> OmenaQueryFragmentBundleV0 {
543 OmenaQueryFragmentBundleV0 {
544 schema_version: "0",
545 product: "omena-query.fragment-bundle",
546 input_version: input.version.clone(),
547 expression_semantics: summarize_omena_query_expression_semantics_query_fragments(input),
548 source_resolution: summarize_omena_query_source_resolution_query_fragments(input),
549 selector_usage: summarize_omena_query_selector_usage_query_fragments(input),
550 }
551}
552
553pub fn summarize_omena_query_expression_semantics_query_fragments(
554 input: &EngineInputV2,
555) -> ExpressionSemanticsQueryFragmentsV0 {
556 summarize_expression_semantics_query_fragments_input(input)
557}
558
559pub fn summarize_omena_query_expression_domain_flow_analysis(
560 input: &EngineInputV2,
561) -> ExpressionDomainFlowAnalysisV0 {
562 summarize_expression_domain_flow_analysis_input(input)
563}
564
565pub fn summarize_omena_query_expression_domain_control_flow_analysis(
566 input: &EngineInputV2,
567) -> ExpressionDomainControlFlowAnalysisV0 {
568 summarize_expression_domain_control_flow_analysis_input(input)
569}
570
571pub fn summarize_omena_query_expression_domain_incremental_flow_analysis(
572 input: &EngineInputV2,
573 runtime: &mut OmenaQueryExpressionDomainFlowRuntimeV0,
574) -> OmenaQueryExpressionDomainIncrementalFlowAnalysisV0 {
575 runtime.analyze_input(input)
576}
577
578pub fn summarize_omena_query_expression_domain_selector_projection(
579 input: &EngineInputV2,
580) -> OmenaQueryExpressionDomainSelectorProjectionV0 {
581 let style_selectors_by_path = style_selector_universe_by_path(input);
582 let expression_targets = expression_target_style_paths(input);
583 let flow_analysis = summarize_omena_query_expression_domain_flow_analysis(input);
584 let mut projections = Vec::new();
585
586 for graph in flow_analysis.analyses {
587 for node in graph.analysis.nodes {
588 let target_style_paths = target_style_paths_for_flow_node(
589 node.id.as_str(),
590 node.predecessor_ids.as_slice(),
591 &expression_targets,
592 );
593 let selector_universe = selector_universe_for_targets(
594 target_style_paths.as_slice(),
595 &style_selectors_by_path,
596 );
597 let projection = project_abstract_value_selectors(&node.value, &selector_universe);
598 projections.push(OmenaQueryExpressionDomainSelectorProjectionEntryV0 {
599 graph_id: graph.graph_id.clone(),
600 file_path: graph.file_path.clone(),
601 node_id: node.id,
602 target_style_paths,
603 value_kind: node.value_kind,
604 selector_names: projection.selector_names,
605 certainty: projection.certainty,
606 });
607 }
608 }
609
610 OmenaQueryExpressionDomainSelectorProjectionV0 {
611 schema_version: "0",
612 product: "omena-query.expression-domain-selector-projection",
613 input_version: input.version.clone(),
614 projection_count: projections.len(),
615 projections,
616 }
617}
618
619impl OmenaQueryExpressionDomainFlowRuntimeV0 {
620 pub fn revision(&self) -> u64 {
621 self.revision
622 }
623
624 pub fn graph_count(&self) -> usize {
625 self.databases_by_graph_id.len()
626 }
627
628 pub fn analyze_input(
629 &mut self,
630 input: &EngineInputV2,
631 ) -> OmenaQueryExpressionDomainIncrementalFlowAnalysisV0 {
632 self.revision += 1;
633 let revision = self.revision;
634 let flow_graphs = collect_expression_domain_flow_graphs(input);
635 let live_graph_ids = flow_graphs
636 .iter()
637 .map(|entry| entry.graph_id.clone())
638 .collect::<BTreeSet<_>>();
639
640 self.databases_by_graph_id
641 .retain(|graph_id, _| live_graph_ids.contains(graph_id));
642 self.previous_analyses_by_graph_id
643 .retain(|graph_id, _| live_graph_ids.contains(graph_id));
644
645 let analyses = flow_graphs
646 .into_iter()
647 .map(|entry| {
648 let database = self
649 .databases_by_graph_id
650 .entry(entry.graph_id.clone())
651 .or_default();
652 let previous_analysis = self.previous_analyses_by_graph_id.get(&entry.graph_id);
653 let analysis = analyze_class_value_flow_incremental_with_database(
654 &entry.graph,
655 database,
656 previous_analysis,
657 revision,
658 );
659 self.previous_analyses_by_graph_id
660 .insert(entry.graph_id.clone(), analysis.analysis.clone());
661
662 OmenaQueryExpressionDomainIncrementalFlowAnalysisEntryV0 {
663 graph_id: entry.graph_id,
664 file_path: entry.file_path,
665 analysis,
666 }
667 })
668 .collect::<Vec<_>>();
669
670 let dirty_graph_count = analyses
671 .iter()
672 .filter(|entry| entry.analysis.incremental_plan.dirty_node_count > 0)
673 .count();
674 let reused_graph_count = analyses
675 .iter()
676 .filter(|entry| entry.analysis.reused_previous_analysis)
677 .count();
678
679 OmenaQueryExpressionDomainIncrementalFlowAnalysisV0 {
680 schema_version: "0",
681 product: "omena-query.expression-domain-incremental-flow-analysis",
682 input_version: input.version.clone(),
683 revision,
684 graph_count: analyses.len(),
685 dirty_graph_count,
686 reused_graph_count,
687 analyses,
688 }
689 }
690}
691
692fn expression_target_style_paths(input: &EngineInputV2) -> BTreeMap<String, String> {
693 input
694 .sources
695 .iter()
696 .flat_map(|source| source.document.class_expressions.iter())
697 .map(|expression| (expression.id.clone(), expression.scss_module_path.clone()))
698 .collect()
699}
700
701fn style_selector_universe_by_path(input: &EngineInputV2) -> BTreeMap<String, Vec<String>> {
702 input
703 .styles
704 .iter()
705 .map(|style| {
706 let selector_names = style
707 .document
708 .selectors
709 .iter()
710 .map(|selector| {
711 selector
712 .canonical_name
713 .clone()
714 .unwrap_or_else(|| selector.name.clone())
715 })
716 .collect::<BTreeSet<_>>()
717 .into_iter()
718 .collect::<Vec<_>>();
719 (style.file_path.clone(), selector_names)
720 })
721 .collect()
722}
723
724fn target_style_paths_for_flow_node(
725 node_id: &str,
726 predecessor_ids: &[String],
727 expression_targets: &BTreeMap<String, String>,
728) -> Vec<String> {
729 let mut targets = BTreeSet::new();
730 if let Some(target) = expression_targets.get(node_id) {
731 targets.insert(target.clone());
732 }
733 for predecessor_id in predecessor_ids {
734 if let Some(target) = expression_targets.get(predecessor_id) {
735 targets.insert(target.clone());
736 }
737 }
738 targets.into_iter().collect()
739}
740
741fn selector_universe_for_targets(
742 target_style_paths: &[String],
743 style_selectors_by_path: &BTreeMap<String, Vec<String>>,
744) -> Vec<String> {
745 let mut selectors = BTreeSet::new();
746 if target_style_paths.is_empty() {
747 for selector_names in style_selectors_by_path.values() {
748 selectors.extend(selector_names.iter().cloned());
749 }
750 } else {
751 for target_style_path in target_style_paths {
752 if let Some(selector_names) = style_selectors_by_path.get(target_style_path) {
753 selectors.extend(selector_names.iter().cloned());
754 }
755 }
756 }
757 selectors.into_iter().collect()
758}
759
760pub fn summarize_omena_query_source_resolution_query_fragments(
761 input: &EngineInputV2,
762) -> SourceResolutionQueryFragmentsV0 {
763 summarize_omena_resolver_query_fragments(input)
764}
765
766pub fn summarize_omena_query_selector_usage_query_fragments(
767 input: &EngineInputV2,
768) -> SelectorUsageQueryFragmentsV0 {
769 summarize_selector_usage_query_fragments_input(input)
770}
771
772pub fn summarize_omena_query_source_resolution_canonical_producer_signal(
773 input: &EngineInputV2,
774) -> SourceResolutionCanonicalProducerSignalV0 {
775 summarize_omena_resolver_canonical_producer_signal(input)
776}
777
778pub fn summarize_omena_query_source_resolution_runtime(
779 input: &EngineInputV2,
780) -> OmenaResolverSourceResolutionRuntimeIndexV0 {
781 summarize_omena_resolver_source_resolution_runtime(input)
782}
783
784pub fn summarize_omena_query_expression_semantics_canonical_producer_signal(
785 input: &EngineInputV2,
786) -> ExpressionSemanticsCanonicalProducerSignalV0 {
787 summarize_expression_semantics_canonical_producer_signal_input(input)
788}
789
790pub fn summarize_omena_query_selector_usage_canonical_producer_signal(
791 input: &EngineInputV2,
792) -> SelectorUsageCanonicalProducerSignalV0 {
793 summarize_selector_usage_canonical_producer_signal_input(input)
794}
795
796pub fn summarize_omena_query_style_semantic_graph_from_source(
797 style_path: &str,
798 style_source: &str,
799 input: &EngineInputV2,
800) -> Option<StyleSemanticGraphSummaryV0> {
801 summarize_omena_bridge_style_semantic_graph_from_source(style_path, style_source, input)
802}
803
804pub fn summarize_omena_query_style_document(
805 style_path: &str,
806 style_source: &str,
807) -> Option<OmenaQueryStyleDocumentSummaryV0> {
808 let sheet = parse_style_module(style_path, style_source)?;
809 let index = summarize_css_modules_intermediate(&sheet);
810 Some(OmenaQueryStyleDocumentSummaryV0 {
811 schema_version: "0",
812 product: "omena-query.style-document-summary",
813 language: style_language_label(sheet.language),
814 selector_names: index.selectors.names,
815 custom_property_decl_names: index.custom_properties.decl_names,
816 custom_property_ref_names: index.custom_properties.ref_names,
817 sass_module_use_sources: index.sass.module_use_sources,
818 sass_module_forward_sources: index.sass.module_forward_sources,
819 diagnostic_count: sheet.diagnostics.len(),
820 })
821}
822
823pub fn summarize_omena_query_style_hover_candidates(
824 style_path: &str,
825 style_source: &str,
826) -> Option<OmenaQueryStyleHoverCandidatesV0> {
827 let sheet = parse_style_module(style_path, style_source)?;
828 let index = summarize_css_modules_intermediate(&sheet);
829 let mut seen = BTreeSet::new();
830 let mut candidates = Vec::new();
831 collect_style_selector_hover_candidates_from_parser_facts(
832 index.selectors.definition_facts.as_slice(),
833 &mut seen,
834 &mut candidates,
835 );
836 collect_custom_property_hover_candidates(
837 sheet.source.as_str(),
838 index.custom_properties.decl_facts.as_slice(),
839 index.custom_properties.ref_names.as_slice(),
840 &mut seen,
841 &mut candidates,
842 );
843 collect_sass_symbol_hover_candidates(
844 index.sass.symbol_decl_facts.as_slice(),
845 index.sass.selector_symbol_facts.as_slice(),
846 &mut seen,
847 &mut candidates,
848 );
849 collect_sass_partial_evaluator_selector_candidates(
850 sheet.source.as_str(),
851 sheet.nodes.as_slice(),
852 &mut seen,
853 &mut candidates,
854 );
855 candidates.sort();
856 Some(OmenaQueryStyleHoverCandidatesV0 {
857 schema_version: "0",
858 product: "omena-query.style-hover-candidates",
859 language: style_language_label(sheet.language),
860 candidates,
861 })
862}
863
864pub fn summarize_omena_query_style_hover_render_parts(
865 source: &str,
866 kind: &str,
867 name: &str,
868 position: ParserPositionV0,
869) -> OmenaQueryStyleHoverRenderPartsV0 {
870 let mut parts = OmenaQueryStyleHoverRenderPartsV0 {
871 schema_version: "0",
872 product: "omena-query.style-hover-render-parts",
873 snippet: String::new(),
874 value: None,
875 signature: None,
876 render_source: "lineSnippet",
877 };
878
879 match kind {
880 "selector" => {
881 parts.snippet = rule_snippet_around_position(source, position).unwrap_or_else(|| {
882 parts.render_source = "selectorFallback";
883 format!(".{name} {{ ... }}")
884 });
885 if parts.render_source != "selectorFallback" {
886 parts.render_source = "ruleSnippet";
887 }
888 }
889 "customPropertyReference" | "customPropertyDeclaration" => {
890 parts.snippet = line_snippet_at_position(source, position).unwrap_or_default();
891 }
892 kind if is_sass_symbol_candidate_kind(kind) => {
893 parts.snippet = line_snippet_at_position(source, position).unwrap_or_default();
894 if sass_symbol_kind_from_candidate_kind(kind) == Some("variable")
895 && is_sass_symbol_declaration_kind(kind)
896 {
897 parts.value = sass_variable_value_from_declaration_line(parts.snippet.as_str());
898 } else if matches!(
899 sass_symbol_kind_from_candidate_kind(kind),
900 Some("mixin" | "function")
901 ) && is_sass_symbol_declaration_kind(kind)
902 && let Some((signature, snippet)) =
903 sass_callable_definition_render_parts(source, position)
904 {
905 parts.signature = Some(signature);
906 parts.snippet = snippet;
907 parts.render_source = "callableBlockSnippet";
908 }
909 }
910 _ => {
911 parts.snippet = name.to_string();
912 parts.render_source = "candidateNameFallback";
913 }
914 }
915
916 parts
917}
918
919pub fn summarize_omena_query_missing_custom_property_diagnostics(
920 style_uri: &str,
921 source: &str,
922 candidates: &[OmenaQueryStyleHoverCandidateV0],
923) -> Vec<OmenaQueryStyleDiagnosticV0> {
924 let declaration_names = candidates
925 .iter()
926 .filter(|candidate| candidate.kind == "customPropertyDeclaration")
927 .map(|candidate| candidate.name.as_str())
928 .collect::<BTreeSet<_>>();
929 if declaration_names.is_empty() {
930 return Vec::new();
931 }
932
933 let insertion_range = end_of_source_range(source);
934 candidates
935 .iter()
936 .filter(|candidate| {
937 candidate.kind == "customPropertyReference"
938 && !declaration_names.contains(candidate.name.as_str())
939 })
940 .map(|candidate| OmenaQueryStyleDiagnosticV0 {
941 code: "missingCustomProperty",
942 range: candidate.range,
943 message: format!(
944 "CSS custom property '{}' not found in indexed style tokens.",
945 candidate.name
946 ),
947 create_custom_property: Some(OmenaQueryCreateCustomPropertyActionV0 {
948 uri: style_uri.to_string(),
949 range: insertion_range,
950 new_text: format!("\n\n:root {{\n {}: ;\n}}\n", candidate.name),
951 property_name: candidate.name.clone(),
952 }),
953 })
954 .collect()
955}
956
957pub fn summarize_omena_query_missing_selector_diagnostic(
958 target_style_uri: &str,
959 target_style_source: &str,
960 selector_name: &str,
961 source_reference_range: ParserRangeV0,
962) -> OmenaQuerySourceDiagnosticV0 {
963 let insertion_range = end_of_source_range(target_style_source);
964 let has_existing_style_content = !target_style_source.trim().is_empty();
965 OmenaQuerySourceDiagnosticV0 {
966 code: "missingSelector",
967 range: source_reference_range,
968 message: format!(
969 "CSS Module selector '.{selector_name}' not found in indexed style tokens."
970 ),
971 create_selector: Some(OmenaQueryCreateSelectorActionV0 {
972 uri: target_style_uri.to_string(),
973 range: insertion_range,
974 new_text: if has_existing_style_content {
975 format!("\n\n.{selector_name} {{\n}}\n")
976 } else {
977 format!(".{selector_name} {{\n}}\n")
978 },
979 selector_name: selector_name.to_string(),
980 }),
981 }
982}
983
984pub fn resolve_omena_query_source_provider_candidates(
985 source_candidates: Vec<OmenaQuerySourceSelectorCandidateV0>,
986 definitions: &[OmenaQueryStyleSelectorDefinitionV0],
987) -> OmenaQuerySourceProviderCandidateResolutionV0 {
988 if definitions.is_empty() {
989 return OmenaQuerySourceProviderCandidateResolutionV0 {
990 schema_version: "0",
991 product: "omena-query.source-provider-candidate-resolution",
992 matched: Vec::new(),
993 unresolved: Vec::new(),
994 };
995 }
996
997 let (mut matched, mut unresolved): (Vec<_>, Vec<_>) =
998 source_candidates.into_iter().partition(|candidate| {
999 definitions.iter().any(|definition| {
1000 source_selector_candidate_matches_definition(candidate, definition)
1001 })
1002 });
1003 matched.sort();
1004 unresolved.sort();
1005 OmenaQuerySourceProviderCandidateResolutionV0 {
1006 schema_version: "0",
1007 product: "omena-query.source-provider-candidate-resolution",
1008 matched,
1009 unresolved,
1010 }
1011}
1012
1013pub fn resolve_omena_query_style_selector_definitions_for_source_candidate(
1014 candidate: &OmenaQuerySourceSelectorCandidateV0,
1015 definitions: &[OmenaQueryStyleSelectorDefinitionV0],
1016) -> Vec<OmenaQueryStyleSelectorDefinitionV0> {
1017 let mut matched = definitions
1018 .iter()
1019 .filter(|definition| source_selector_candidate_matches_definition(candidate, definition))
1020 .cloned()
1021 .collect::<Vec<_>>();
1022 matched.sort_by_key(|definition| {
1023 (
1024 definition.uri.clone(),
1025 definition.range.start.line,
1026 definition.range.start.character,
1027 definition.name.clone(),
1028 )
1029 });
1030 matched.dedup();
1031 matched
1032}
1033
1034pub fn resolve_omena_query_source_candidate_selector_names(
1035 candidate: &OmenaQuerySourceSelectorCandidateV0,
1036 definitions: &[OmenaQueryStyleSelectorDefinitionV0],
1037 target_style_uri: Option<&str>,
1038) -> Vec<String> {
1039 if candidate.kind != "sourceSelectorPrefixReference" {
1040 return vec![candidate.name.clone()];
1041 }
1042
1043 let mut names = definitions
1044 .iter()
1045 .filter(|definition| source_selector_candidate_matches_definition(candidate, definition))
1046 .filter(|definition| {
1047 candidate
1048 .target_style_uri
1049 .as_deref()
1050 .or(target_style_uri)
1051 .is_none_or(|target_uri| target_uri == definition.uri)
1052 })
1053 .map(|definition| definition.name.clone())
1054 .collect::<Vec<_>>();
1055 names.sort();
1056 names.dedup();
1057 names
1058}
1059
1060pub fn resolve_omena_query_selector_rename_edits(
1061 selector_name: &str,
1062 new_name: &str,
1063 target_style_uri: Option<&str>,
1064 definitions: &[OmenaQueryStyleSelectorDefinitionV0],
1065 references: &[OmenaQuerySourceSelectorReferenceEditTargetV0],
1066) -> Vec<OmenaQueryWorkspaceTextEditV0> {
1067 let replacement = new_name.trim_start_matches('.');
1068 if replacement.is_empty() {
1069 return Vec::new();
1070 }
1071
1072 let mut edits = definitions
1073 .iter()
1074 .filter(|definition| definition.name == selector_name)
1075 .filter(|definition| target_style_uri.is_none_or(|target_uri| target_uri == definition.uri))
1076 .map(|definition| OmenaQueryWorkspaceTextEditV0 {
1077 uri: definition.uri.clone(),
1078 range: definition.range,
1079 new_text: replacement.to_string(),
1080 })
1081 .chain(
1082 references
1083 .iter()
1084 .filter(|reference| reference.name == selector_name)
1085 .filter(|reference| {
1086 source_reference_matches_target_style(reference, target_style_uri)
1087 })
1088 .map(|reference| OmenaQueryWorkspaceTextEditV0 {
1089 uri: reference.uri.clone(),
1090 range: reference.range,
1091 new_text: replacement.to_string(),
1092 }),
1093 )
1094 .collect::<Vec<_>>();
1095 edits.sort_by_key(|edit| {
1096 (
1097 edit.uri.clone(),
1098 edit.range.start.line,
1099 edit.range.start.character,
1100 edit.range.end.line,
1101 edit.range.end.character,
1102 )
1103 });
1104 edits
1105}
1106
1107pub fn is_omena_query_sass_symbol_candidate_kind(kind: &str) -> bool {
1108 omena_query_sass_symbol_kind_from_candidate_kind(kind).is_some()
1109}
1110
1111pub fn is_omena_query_sass_symbol_reference_kind(kind: &str) -> bool {
1112 matches!(
1113 kind,
1114 "sassVariableReference"
1115 | "sassMixinInclude"
1116 | "sassFunctionCall"
1117 | "sassMixinReference"
1118 | "sassFunctionReference"
1119 | "sassSymbolReference"
1120 )
1121}
1122
1123pub fn is_omena_query_sass_symbol_declaration_kind(kind: &str) -> bool {
1124 matches!(
1125 kind,
1126 "sassVariableDeclaration"
1127 | "sassMixinDeclaration"
1128 | "sassFunctionDeclaration"
1129 | "sassSymbolDeclaration"
1130 )
1131}
1132
1133pub fn omena_query_sass_symbol_kind_from_candidate_kind(kind: &str) -> Option<&'static str> {
1134 match kind {
1135 "sassVariableDeclaration" | "sassVariableReference" => Some("variable"),
1136 "sassMixinDeclaration" | "sassMixinInclude" | "sassMixinReference" => Some("mixin"),
1137 "sassFunctionDeclaration" | "sassFunctionCall" | "sassFunctionReference" => {
1138 Some("function")
1139 }
1140 "sassSymbolDeclaration" | "sassSymbolReference" => Some("symbol"),
1141 _ => None,
1142 }
1143}
1144
1145pub fn omena_query_sass_symbol_target_matches(
1146 candidate_kind: &str,
1147 candidate_name: &str,
1148 candidate_namespace: Option<&str>,
1149 target_kind: &str,
1150 target_name: &str,
1151 target_namespace: Option<&str>,
1152) -> bool {
1153 candidate_name == target_name
1154 && candidate_namespace == target_namespace
1155 && omena_query_sass_symbol_kind_from_candidate_kind(candidate_kind)
1156 == omena_query_sass_symbol_kind_from_candidate_kind(target_kind)
1157}
1158
1159pub fn resolve_omena_query_sass_symbol_declarations(
1160 candidates: &[OmenaQueryStyleHoverCandidateV0],
1161 symbol_kind: &str,
1162 name: &str,
1163) -> Vec<OmenaQueryStyleHoverCandidateV0> {
1164 candidates
1165 .iter()
1166 .filter(|target| {
1167 is_omena_query_sass_symbol_declaration_kind(target.kind)
1168 && omena_query_sass_symbol_kind_from_candidate_kind(target.kind)
1169 == Some(symbol_kind)
1170 && target.name == name
1171 })
1172 .cloned()
1173 .collect()
1174}
1175
1176pub fn resolve_omena_query_sass_module_use_sources_for_candidate(
1177 sources: &OmenaQuerySassModuleSourcesV0,
1178 namespace: Option<&str>,
1179) -> Vec<String> {
1180 let mut selected = sources
1181 .module_use_edges
1182 .iter()
1183 .filter(|edge| {
1184 if let Some(namespace) = namespace {
1185 edge.namespace.as_deref() == Some(namespace)
1186 } else {
1187 edge.namespace_kind == "wildcard"
1188 }
1189 })
1190 .filter(|edge| !is_sass_builtin_module_source(edge.source.as_str()))
1191 .map(|edge| edge.source.clone())
1192 .collect::<Vec<_>>();
1193 selected.sort();
1194 selected.dedup();
1195 selected
1196}
1197
1198pub fn resolve_omena_query_sass_forward_sources(
1199 sources: &OmenaQuerySassModuleSourcesV0,
1200) -> Vec<String> {
1201 let mut selected = sources
1202 .module_forward_sources
1203 .iter()
1204 .filter(|source| !is_sass_builtin_module_source(source.as_str()))
1205 .cloned()
1206 .collect::<Vec<_>>();
1207 selected.sort();
1208 selected.dedup();
1209 selected
1210}
1211
1212pub fn summarize_omena_query_sass_module_sources(
1213 style_path: &str,
1214 style_source: &str,
1215) -> Option<OmenaQuerySassModuleSourcesV0> {
1216 let sheet = parse_style_module(style_path, style_source)?;
1217 let index = summarize_css_modules_intermediate(&sheet);
1218 Some(OmenaQuerySassModuleSourcesV0 {
1219 schema_version: "0",
1220 product: "omena-query.sass-module-sources",
1221 module_use_edges: index
1222 .sass
1223 .module_use_edges
1224 .into_iter()
1225 .map(|edge| OmenaQuerySassModuleUseEdgeV0 {
1226 source: edge.source,
1227 namespace_kind: edge.namespace_kind,
1228 namespace: edge.namespace,
1229 })
1230 .collect(),
1231 module_forward_sources: index.sass.module_forward_sources,
1232 })
1233}
1234
1235pub fn summarize_omena_query_style_semantic_graph_batch_from_sources<'a>(
1236 styles: impl IntoIterator<Item = (&'a str, &'a str)>,
1237 input: &EngineInputV2,
1238) -> OmenaQueryStyleSemanticGraphBatchOutputV0 {
1239 summarize_omena_query_style_semantic_graph_batch_from_sources_with_package_manifests(
1240 styles,
1241 input,
1242 &[],
1243 )
1244}
1245
1246pub fn summarize_omena_query_style_semantic_graph_batch_from_sources_with_package_manifests<'a>(
1247 styles: impl IntoIterator<Item = (&'a str, &'a str)>,
1248 input: &EngineInputV2,
1249 package_manifests: &[OmenaQueryStylePackageManifestV0],
1250) -> OmenaQueryStyleSemanticGraphBatchOutputV0 {
1251 let style_sources = styles.into_iter().collect::<Vec<_>>();
1252 let parsed_styles = style_sources
1253 .iter()
1254 .filter_map(|(style_path, style_source)| {
1255 parse_style_module(style_path, style_source)
1256 .map(|sheet| ((*style_path).to_string(), sheet))
1257 })
1258 .collect::<Vec<_>>();
1259 let workspace_declarations = parsed_styles
1260 .iter()
1261 .flat_map(|(style_path, sheet)| {
1262 collect_omena_bridge_design_token_workspace_declarations(style_path, sheet)
1263 })
1264 .collect::<Vec<_>>();
1265 let graphs = style_sources
1266 .into_iter()
1267 .map(
1268 |(style_path, _style_source)| OmenaQueryStyleSemanticGraphBatchEntryV0 {
1269 style_path: style_path.to_string(),
1270 graph: parsed_style_by_path(&parsed_styles, style_path).map(|sheet| {
1271 let import_reachable_declarations =
1272 filter_import_reachable_design_token_workspace_declarations(
1273 style_path,
1274 &parsed_styles,
1275 &workspace_declarations,
1276 package_manifests,
1277 );
1278 summarize_omena_bridge_style_semantic_graph_for_path_with_scoped_workspace_declarations(
1279 sheet,
1280 input,
1281 Some(style_path),
1282 &import_reachable_declarations,
1283 DesignTokenExternalDeclarationCandidateScopeV0::CrossFileImportGraph,
1284 )
1285 }),
1286 },
1287 )
1288 .collect::<Vec<_>>();
1289
1290 OmenaQueryStyleSemanticGraphBatchOutputV0 {
1291 schema_version: "0",
1292 product: "omena-semantic.style-semantic-graph-batch",
1293 graphs,
1294 }
1295}
1296
1297fn parsed_style_by_path<'a>(
1298 parsed_styles: &'a [(String, Stylesheet)],
1299 style_path: &str,
1300) -> Option<&'a Stylesheet> {
1301 parsed_styles
1302 .iter()
1303 .find(|(parsed_style_path, _sheet)| parsed_style_path == style_path)
1304 .map(|(_style_path, sheet)| sheet)
1305}
1306
1307fn filter_import_reachable_design_token_workspace_declarations(
1308 target_style_path: &str,
1309 parsed_styles: &[(String, Stylesheet)],
1310 workspace_declarations: &[DesignTokenWorkspaceDeclarationFactV0],
1311 package_manifests: &[OmenaQueryStylePackageManifestV0],
1312) -> Vec<DesignTokenWorkspaceDeclarationFactV0> {
1313 let reachable_style_paths = collect_import_reachable_style_path_metadata(
1314 target_style_path,
1315 parsed_styles,
1316 package_manifests,
1317 );
1318 workspace_declarations
1319 .iter()
1320 .filter_map(|declaration| {
1321 if declaration.file_path == target_style_path {
1322 return Some(declaration.clone());
1323 }
1324 let reachability = reachable_style_paths.get(declaration.file_path.as_str())?;
1325 let mut declaration = declaration.clone();
1326 declaration.import_graph_distance = Some(reachability.distance);
1327 declaration.import_graph_order = Some(reachability.order);
1328 Some(declaration)
1329 })
1330 .collect()
1331}
1332
1333#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1334struct ImportReachability {
1335 distance: usize,
1336 order: usize,
1337}
1338
1339fn collect_import_reachable_style_path_metadata(
1340 target_style_path: &str,
1341 parsed_styles: &[(String, Stylesheet)],
1342 package_manifests: &[OmenaQueryStylePackageManifestV0],
1343) -> BTreeMap<String, ImportReachability> {
1344 let mut reachable_style_paths = BTreeMap::new();
1345 let available_style_paths = parsed_styles
1346 .iter()
1347 .map(|(style_path, _sheet)| style_path.as_str())
1348 .collect::<BTreeSet<_>>();
1349 let mut pending_style_paths = collect_import_reachable_direct_style_paths(
1350 target_style_path,
1351 parsed_styles,
1352 &available_style_paths,
1353 package_manifests,
1354 )
1355 .into_iter()
1356 .map(|style_path| (style_path, 1usize))
1357 .collect::<VecDeque<_>>();
1358 let style_by_path = parsed_styles
1359 .iter()
1360 .map(|(style_path, sheet)| (style_path.as_str(), sheet))
1361 .collect::<BTreeMap<_, _>>();
1362 let mut visit_order = 0usize;
1363
1364 while let Some((style_path, distance)) = pending_style_paths.pop_front() {
1365 if style_path == target_style_path || reachable_style_paths.contains_key(&style_path) {
1366 continue;
1367 }
1368 reachable_style_paths.insert(
1369 style_path.clone(),
1370 ImportReachability {
1371 distance,
1372 order: visit_order,
1373 },
1374 );
1375 visit_order += 1;
1376
1377 let Some(sheet) = style_by_path.get(style_path.as_str()) else {
1378 continue;
1379 };
1380 for source in collect_sass_module_sources(sheet) {
1381 if let Some(next_style_path) = resolve_style_module_source(
1382 &style_path,
1383 &source,
1384 &available_style_paths,
1385 package_manifests,
1386 ) {
1387 pending_style_paths.push_back((next_style_path, distance + 1));
1388 }
1389 }
1390 }
1391
1392 reachable_style_paths
1393}
1394
1395fn collect_import_reachable_direct_style_paths(
1396 target_style_path: &str,
1397 parsed_styles: &[(String, Stylesheet)],
1398 available_style_paths: &BTreeSet<&str>,
1399 package_manifests: &[OmenaQueryStylePackageManifestV0],
1400) -> Vec<String> {
1401 let Some(target_sheet) = parsed_style_by_path(parsed_styles, target_style_path) else {
1402 return Vec::new();
1403 };
1404 collect_sass_module_sources(target_sheet)
1405 .into_iter()
1406 .filter_map(|source| {
1407 resolve_style_module_source(
1408 target_style_path,
1409 &source,
1410 available_style_paths,
1411 package_manifests,
1412 )
1413 })
1414 .collect()
1415}
1416
1417fn collect_sass_module_sources(sheet: &Stylesheet) -> Vec<String> {
1418 let summary = summarize_css_modules_intermediate(sheet);
1419 let mut sources = Vec::new();
1420 for edge in summary.sass.module_use_edges {
1421 push_unique_string(&mut sources, edge.source);
1422 }
1423 for source in summary.sass.module_forward_sources {
1424 push_unique_string(&mut sources, source);
1425 }
1426 for source in summary.sass.module_import_sources {
1427 push_unique_string(&mut sources, source);
1428 }
1429 sources
1430}
1431
1432fn resolve_style_module_source(
1433 from_style_path: &str,
1434 source: &str,
1435 available_style_paths: &BTreeSet<&str>,
1436 package_manifests: &[OmenaQueryStylePackageManifestV0],
1437) -> Option<String> {
1438 if source.starts_with("sass:")
1439 || source.starts_with("http://")
1440 || source.starts_with("https://")
1441 {
1442 return None;
1443 }
1444
1445 style_module_source_candidates(from_style_path, source, package_manifests)
1446 .into_iter()
1447 .find(|candidate| available_style_paths.contains(candidate.as_str()))
1448}
1449
1450fn style_module_source_candidates(
1451 from_style_path: &str,
1452 source: &str,
1453 package_manifests: &[OmenaQueryStylePackageManifestV0],
1454) -> Vec<String> {
1455 let source_path = Path::new(source);
1456 let base_path = if source_path.is_absolute() {
1457 PathBuf::from(source)
1458 } else {
1459 Path::new(from_style_path)
1460 .parent()
1461 .map(|parent| parent.join(source))
1462 .unwrap_or_else(|| PathBuf::from(source))
1463 };
1464 let mut candidates = Vec::new();
1465 push_style_module_path_candidates(
1466 &mut candidates,
1467 base_path,
1468 source_path.extension().is_none(),
1469 );
1470 for package_manifest_base_path in
1471 package_manifest_style_module_base_candidates(from_style_path, source, package_manifests)
1472 {
1473 push_style_module_path_candidates(&mut candidates, package_manifest_base_path, true);
1474 }
1475 for package_base_path in package_style_module_base_candidates(from_style_path, source) {
1476 push_style_module_path_candidates(&mut candidates, package_base_path, true);
1477 }
1478
1479 candidates
1480}
1481
1482fn push_style_module_path_candidates(
1483 candidates: &mut Vec<String>,
1484 base_path: PathBuf,
1485 include_extension_variants: bool,
1486) {
1487 push_style_path_candidate(candidates, base_path.clone());
1488 push_partial_style_path_candidate(candidates, &base_path);
1489
1490 if !include_extension_variants {
1491 return;
1492 }
1493
1494 for extension in [
1495 ".module.scss",
1496 ".module.css",
1497 ".module.less",
1498 ".scss",
1499 ".css",
1500 ".less",
1501 ] {
1502 let candidate = PathBuf::from(format!("{}{}", base_path.display(), extension));
1503 push_style_path_candidate(candidates, candidate.clone());
1504 push_partial_style_path_candidate(candidates, &candidate);
1505 }
1506}
1507
1508fn package_style_module_base_candidates(from_style_path: &str, source: &str) -> Vec<PathBuf> {
1509 let Some(package_source) = parse_package_style_source(source) else {
1510 return Vec::new();
1511 };
1512 let Some(from_dir) = Path::new(from_style_path).parent() else {
1513 return Vec::new();
1514 };
1515 let mut candidates = Vec::new();
1516 let mut current_dir = Some(from_dir);
1517 while let Some(dir) = current_dir {
1518 let package_root = dir.join("node_modules").join(package_source.package_name);
1519 let package_entry = match package_source.subpath {
1520 Some(subpath) => package_root.join(subpath),
1521 None => package_root.clone(),
1522 };
1523 push_unique_pathbuf(&mut candidates, package_entry.clone());
1524 if let Some(subpath) = package_source.subpath {
1525 push_unique_pathbuf(&mut candidates, package_root.join("src").join(subpath));
1526 } else {
1527 push_unique_pathbuf(&mut candidates, package_root.join("index"));
1528 push_unique_pathbuf(&mut candidates, package_root.join("src").join("index"));
1529 }
1530 current_dir = dir.parent();
1531 }
1532 candidates
1533}
1534
1535fn package_manifest_style_module_base_candidates(
1536 from_style_path: &str,
1537 source: &str,
1538 package_manifests: &[OmenaQueryStylePackageManifestV0],
1539) -> Vec<PathBuf> {
1540 let Some(package_source) = parse_package_style_source(source) else {
1541 return Vec::new();
1542 };
1543 let Some(from_dir) = Path::new(from_style_path).parent() else {
1544 return Vec::new();
1545 };
1546 let manifest_by_package_dir = package_manifests
1547 .iter()
1548 .map(|manifest| {
1549 (
1550 package_dir_from_package_json_path(&manifest.package_json_path),
1551 manifest.package_json_source.as_str(),
1552 )
1553 })
1554 .collect::<BTreeMap<_, _>>();
1555
1556 let mut candidates = Vec::new();
1557 let mut current_dir = Some(from_dir);
1558 while let Some(dir) = current_dir {
1559 let package_root = dir.join("node_modules").join(package_source.package_name);
1560 let package_root_key = normalize_style_path(package_root.clone());
1561 if let Some(package_json_source) = manifest_by_package_dir.get(&package_root_key)
1562 && let Some(entry) =
1563 read_package_manifest_style_entry(package_json_source, package_source.subpath)
1564 {
1565 push_unique_pathbuf(&mut candidates, package_root.join(entry));
1566 }
1567 current_dir = dir.parent();
1568 }
1569 candidates
1570}
1571
1572fn package_dir_from_package_json_path(package_json_path: &str) -> String {
1573 Path::new(package_json_path)
1574 .parent()
1575 .map(|path| normalize_style_path(path.to_path_buf()))
1576 .unwrap_or_default()
1577}
1578
1579fn read_package_manifest_style_entry(
1580 package_json_source: &str,
1581 subpath: Option<&str>,
1582) -> Option<PathBuf> {
1583 let package_json = serde_json::from_str::<serde_json::Value>(package_json_source).ok()?;
1584 let package_object = package_json.as_object()?;
1585 let entry = if let Some(subpath) = subpath {
1586 read_package_export_subpath_entry(package_object.get("exports"), subpath)
1587 } else {
1588 read_package_json_string_field(package_object, "sass")
1589 .or_else(|| read_package_json_string_field(package_object, "scss"))
1590 .or_else(|| read_package_json_string_field(package_object, "style"))
1591 .or_else(|| read_package_export_entry(package_object.get("exports")))
1592 }?;
1593 Some(PathBuf::from(normalize_package_json_entry(&entry)))
1594}
1595
1596fn read_package_export_subpath_entry(
1597 exports_value: Option<&serde_json::Value>,
1598 subpath: &str,
1599) -> Option<String> {
1600 let exports_object = exports_value?.as_object()?;
1601 for key in package_export_subpath_keys(subpath) {
1602 if let Some(entry) = read_package_export_entry(exports_object.get(&key)) {
1603 return Some(entry);
1604 }
1605 }
1606 for (key, export_value) in exports_object {
1607 let Some(pattern_match) = match_package_export_subpath_pattern(key, subpath) else {
1608 continue;
1609 };
1610 let Some(entry) = read_package_export_entry(Some(export_value)) else {
1611 continue;
1612 };
1613 return Some(substitute_package_export_pattern(&entry, &pattern_match));
1614 }
1615 None
1616}
1617
1618fn package_export_subpath_keys(subpath: &str) -> Vec<String> {
1619 let normalized = subpath
1620 .trim_start_matches("./")
1621 .trim_start_matches('/')
1622 .to_string();
1623 vec![
1624 format!("./{normalized}"),
1625 format!("./{normalized}.scss"),
1626 format!("./{normalized}.sass"),
1627 format!("./{normalized}.css"),
1628 ]
1629}
1630
1631fn match_package_export_subpath_pattern(pattern_key: &str, subpath: &str) -> Option<String> {
1632 let normalized_pattern = pattern_key.trim_start_matches("./").trim_start_matches('/');
1633 let (prefix, suffix) = normalized_pattern.split_once('*')?;
1634 if suffix.contains('*') {
1635 return None;
1636 }
1637
1638 for candidate_key in package_export_subpath_keys(subpath) {
1639 let normalized_candidate = candidate_key
1640 .trim_start_matches("./")
1641 .trim_start_matches('/')
1642 .to_string();
1643 if !normalized_candidate.starts_with(prefix) || !normalized_candidate.ends_with(suffix) {
1644 continue;
1645 }
1646 return Some(
1647 normalized_candidate[prefix.len()..normalized_candidate.len() - suffix.len()]
1648 .to_string(),
1649 );
1650 }
1651 None
1652}
1653
1654fn substitute_package_export_pattern(entry: &str, pattern_match: &str) -> String {
1655 if entry.contains('*') {
1656 entry.replace('*', pattern_match)
1657 } else {
1658 entry.to_string()
1659 }
1660}
1661
1662fn read_package_export_entry(exports_value: Option<&serde_json::Value>) -> Option<String> {
1663 let exports_value = exports_value?;
1664 if let Some(entry) = exports_value.as_str() {
1665 return Some(entry.to_string());
1666 }
1667 if let Some(entries) = exports_value.as_array() {
1668 for entry_value in entries {
1669 if let Some(entry) = read_package_export_entry(Some(entry_value)) {
1670 return Some(entry);
1671 }
1672 }
1673 return None;
1674 }
1675 let exports_object = exports_value.as_object()?;
1676 if let Some(root_entry) = read_package_export_entry(exports_object.get(".")) {
1677 return Some(root_entry);
1678 }
1679 for key in ["sass", "scss", "style", "default", "import", "require"] {
1680 if let Some(entry) = read_package_export_entry(exports_object.get(key)) {
1681 return Some(entry);
1682 }
1683 }
1684 None
1685}
1686
1687fn read_package_json_string_field(
1688 package_object: &serde_json::Map<String, serde_json::Value>,
1689 key: &str,
1690) -> Option<String> {
1691 package_object
1692 .get(key)
1693 .and_then(|value| value.as_str())
1694 .map(ToString::to_string)
1695}
1696
1697fn normalize_package_json_entry(entry: &str) -> String {
1698 entry
1699 .trim_start_matches("./")
1700 .trim_start_matches('/')
1701 .to_string()
1702}
1703
1704#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1705struct PackageStyleSource<'a> {
1706 package_name: &'a str,
1707 subpath: Option<&'a str>,
1708}
1709
1710fn parse_package_style_source(source: &str) -> Option<PackageStyleSource<'_>> {
1711 if source.starts_with('.')
1712 || source.starts_with('/')
1713 || source.starts_with("sass:")
1714 || source.starts_with("http://")
1715 || source.starts_with("https://")
1716 {
1717 return None;
1718 }
1719
1720 if source.starts_with('@') {
1721 let mut segments = source.splitn(3, '/');
1722 let scope = segments.next()?;
1723 let package = segments.next()?;
1724 if scope.len() <= 1 || package.is_empty() {
1725 return None;
1726 }
1727 let package_name_end = scope.len() + 1 + package.len();
1728 let package_name = &source[..package_name_end];
1729 let subpath = segments.next().filter(|subpath| !subpath.is_empty());
1730 return Some(PackageStyleSource {
1731 package_name,
1732 subpath,
1733 });
1734 }
1735
1736 let mut segments = source.splitn(2, '/');
1737 let package_name = segments.next()?;
1738 if package_name.is_empty() {
1739 return None;
1740 }
1741 let subpath = segments.next().filter(|subpath| !subpath.is_empty());
1742 Some(PackageStyleSource {
1743 package_name,
1744 subpath,
1745 })
1746}
1747
1748fn push_unique_pathbuf(candidates: &mut Vec<PathBuf>, value: PathBuf) {
1749 if !candidates.contains(&value) {
1750 candidates.push(value);
1751 }
1752}
1753
1754fn push_partial_style_path_candidate(candidates: &mut Vec<String>, path: &Path) {
1755 let Some(file_name) = path.file_name().and_then(|file_name| file_name.to_str()) else {
1756 return;
1757 };
1758 if file_name.starts_with('_') {
1759 return;
1760 }
1761 let mut partial_path = path.to_path_buf();
1762 partial_path.set_file_name(format!("_{file_name}"));
1763 push_style_path_candidate(candidates, partial_path);
1764}
1765
1766fn push_style_path_candidate(candidates: &mut Vec<String>, path: PathBuf) {
1767 let candidate = normalize_style_path(path);
1768 if !candidates.contains(&candidate) {
1769 candidates.push(candidate);
1770 }
1771}
1772
1773fn normalize_style_path(path: PathBuf) -> String {
1774 let mut normalized = PathBuf::new();
1775 for component in path.components() {
1776 match component {
1777 Component::CurDir => {}
1778 Component::ParentDir => {
1779 normalized.pop();
1780 }
1781 Component::Normal(part) => normalized.push(part),
1782 Component::RootDir | Component::Prefix(_) => normalized.push(component.as_os_str()),
1783 }
1784 }
1785 normalized.to_string_lossy().replace('\\', "/")
1786}
1787
1788fn collect_style_selector_hover_candidates_from_parser_facts(
1789 definition_facts: &[engine_style_parser::ParserIndexSelectorDefinitionFactV0],
1790 seen: &mut BTreeSet<(usize, usize, String)>,
1791 candidates: &mut Vec<OmenaQueryStyleHoverCandidateV0>,
1792) {
1793 for fact in definition_facts {
1794 if seen.insert((fact.byte_span.start, fact.byte_span.end, fact.name.clone())) {
1795 candidates.push(OmenaQueryStyleHoverCandidateV0 {
1796 kind: "selector",
1797 name: fact.name.clone(),
1798 range: fact.range,
1799 source: "engineStyleParserSelectorDefinitionFacts",
1800 namespace: None,
1801 });
1802 }
1803 }
1804}
1805
1806fn collect_custom_property_hover_candidates(
1807 source: &str,
1808 decl_facts: &[engine_style_parser::ParserIndexCustomPropertyDeclFactV0],
1809 ref_names: &[String],
1810 seen: &mut BTreeSet<(usize, usize, String)>,
1811 candidates: &mut Vec<OmenaQueryStyleHoverCandidateV0>,
1812) {
1813 for fact in decl_facts {
1814 if seen.insert((fact.byte_span.start, fact.byte_span.end, fact.name.clone())) {
1815 candidates.push(OmenaQueryStyleHoverCandidateV0 {
1816 kind: "customPropertyDeclaration",
1817 name: fact.name.clone(),
1818 range: fact.range,
1819 source: "openedStyleDocumentIndex",
1820 namespace: None,
1821 });
1822 }
1823 }
1824
1825 for name in ref_names {
1826 for byte_span in custom_property_ref_byte_spans(source, name) {
1827 if seen.insert((byte_span.start, byte_span.end, name.clone())) {
1828 candidates.push(OmenaQueryStyleHoverCandidateV0 {
1829 kind: "customPropertyReference",
1830 name: name.clone(),
1831 range: parser_range_for_byte_span(source, byte_span),
1832 source: "openedStyleDocumentIndex",
1833 namespace: None,
1834 });
1835 }
1836 }
1837 }
1838}
1839
1840fn collect_sass_symbol_hover_candidates(
1841 decl_facts: &[engine_style_parser::ParserIndexSassSymbolDeclFactV0],
1842 ref_facts: &[engine_style_parser::ParserIndexSassSelectorSymbolFactV0],
1843 seen: &mut BTreeSet<(usize, usize, String)>,
1844 candidates: &mut Vec<OmenaQueryStyleHoverCandidateV0>,
1845) {
1846 for fact in decl_facts {
1847 if seen.insert((
1848 fact.byte_span.start,
1849 fact.byte_span.end,
1850 format!("{}:{}", fact.symbol_kind, fact.name),
1851 )) {
1852 candidates.push(OmenaQueryStyleHoverCandidateV0 {
1853 kind: sass_symbol_declaration_candidate_kind(fact.symbol_kind),
1854 name: fact.name.clone(),
1855 range: fact.range,
1856 source: "engineStyleParserSassSymbolFacts",
1857 namespace: None,
1858 });
1859 }
1860 }
1861
1862 for fact in ref_facts {
1863 if seen.insert((
1864 fact.byte_span.start,
1865 fact.byte_span.end,
1866 format!(
1867 "{}:{}:{}",
1868 fact.symbol_kind,
1869 fact.namespace.as_deref().unwrap_or_default(),
1870 fact.name
1871 ),
1872 )) {
1873 candidates.push(OmenaQueryStyleHoverCandidateV0 {
1874 kind: sass_symbol_reference_candidate_kind(fact.symbol_kind, fact.role),
1875 name: fact.name.clone(),
1876 range: fact.range,
1877 source: "engineStyleParserSassSymbolFacts",
1878 namespace: fact.namespace.clone(),
1879 });
1880 }
1881 }
1882}
1883
1884fn collect_sass_partial_evaluator_selector_candidates(
1885 source: &str,
1886 nodes: &[engine_style_parser::SyntaxNode],
1887 seen: &mut BTreeSet<(usize, usize, String)>,
1888 candidates: &mut Vec<OmenaQueryStyleHoverCandidateV0>,
1889) {
1890 for node in nodes {
1891 if let Some(SyntaxNodePayload::AtRule(at_rule)) = &node.payload
1892 && at_rule.kind == AtRuleKind::Include
1893 {
1894 let range_span = ParserByteSpanV0 {
1895 start: node.header_span.unwrap_or(node.span).start,
1896 end: node.header_span.unwrap_or(node.span).end,
1897 };
1898 for selector_name in infer_sass_include_generated_selector_names(&at_rule.params) {
1899 if seen.insert((range_span.start, range_span.end, selector_name.clone())) {
1900 candidates.push(OmenaQueryStyleHoverCandidateV0 {
1901 kind: "selector",
1902 name: selector_name,
1903 range: parser_range_for_byte_span(source, range_span),
1904 source: "sassPartialEvaluatorGeneratedSelectors",
1905 namespace: None,
1906 });
1907 }
1908 }
1909 }
1910 collect_sass_partial_evaluator_selector_candidates(
1911 source,
1912 &node.children,
1913 seen,
1914 candidates,
1915 );
1916 }
1917}
1918
1919fn infer_sass_include_generated_selector_names(params: &str) -> Vec<String> {
1920 let Some(prefix) = sass_named_argument_string_value(params, "prefix") else {
1921 return Vec::new();
1922 };
1923 if prefix.is_empty() || !prefix.chars().all(is_css_identifier_continue) {
1924 return Vec::new();
1925 }
1926 let mut selectors = sass_first_map_string_keys(params)
1927 .into_iter()
1928 .filter(|key| !key.is_empty() && key.chars().all(is_css_identifier_continue))
1929 .map(|key| format!("{prefix}-{key}"))
1930 .collect::<Vec<_>>();
1931 selectors.sort();
1932 selectors.dedup();
1933 selectors
1934}
1935
1936fn sass_named_argument_string_value(params: &str, name: &str) -> Option<String> {
1937 let needle = format!("${name}");
1938 let mut cursor = 0usize;
1939 while let Some(relative_match) = params[cursor..].find(needle.as_str()) {
1940 let name_start = cursor + relative_match;
1941 let name_end = name_start + needle.len();
1942 if !sass_identifier_boundary(params, name_start, name_end) {
1943 cursor = name_end;
1944 continue;
1945 }
1946 let colon_offset = skip_ascii_whitespace(params, name_end);
1947 if params.as_bytes().get(colon_offset) != Some(&b':') {
1948 cursor = name_end;
1949 continue;
1950 }
1951 let value_start = skip_ascii_whitespace(params, colon_offset + 1);
1952 return sass_string_literal_value(params, value_start).map(|(value, _)| value);
1953 }
1954 None
1955}
1956
1957fn sass_first_map_string_keys(params: &str) -> Vec<String> {
1958 let mut cursor = 0usize;
1959 while cursor < params.len() {
1960 let Some(open_relative) = params[cursor..].find('(') else {
1961 break;
1962 };
1963 let open = cursor + open_relative;
1964 let Some(close) = matching_style_block_end(params, open, b'(', b')') else {
1965 break;
1966 };
1967 let keys = sass_map_string_keys(params, open + 1, close);
1968 if !keys.is_empty() {
1969 return keys;
1970 }
1971 cursor = open + 1;
1972 }
1973 Vec::new()
1974}
1975
1976fn sass_map_string_keys(params: &str, start: usize, end: usize) -> Vec<String> {
1977 split_top_level_style_segments(params, start, end, b',')
1978 .into_iter()
1979 .filter_map(|(entry_start, entry_end)| {
1980 let key_start = skip_ascii_whitespace(params, entry_start);
1981 let (key, key_end) = sass_string_literal_value(params, key_start)?;
1982 let colon_offset = skip_ascii_whitespace(params, key_end);
1983 (colon_offset < entry_end && params.as_bytes().get(colon_offset) == Some(&b':'))
1984 .then_some(key)
1985 })
1986 .collect()
1987}
1988
1989fn sass_string_literal_value(source: &str, quote_offset: usize) -> Option<(String, usize)> {
1990 let quote = source.as_bytes().get(quote_offset).copied()?;
1991 if !matches!(quote, b'\'' | b'"') {
1992 return None;
1993 }
1994 let literal_end = skip_style_string_literal(source, quote_offset, source.len())?;
1995 let value_end = literal_end.saturating_sub(1);
1996 source
1997 .get(quote_offset + 1..value_end)
1998 .map(|value| (value.to_string(), literal_end))
1999}
2000
2001fn sass_identifier_boundary(source: &str, start: usize, end: usize) -> bool {
2002 let before = source
2003 .get(..start)
2004 .and_then(|prefix| prefix.chars().next_back())
2005 .is_none_or(|ch| !is_css_identifier_continue(ch) && ch != '$');
2006 let after = source
2007 .get(end..)
2008 .and_then(|suffix| suffix.chars().next())
2009 .is_none_or(|ch| !is_css_identifier_continue(ch));
2010 before && after
2011}
2012
2013fn sass_symbol_declaration_candidate_kind(symbol_kind: &str) -> &'static str {
2014 match symbol_kind {
2015 "variable" => "sassVariableDeclaration",
2016 "mixin" => "sassMixinDeclaration",
2017 "function" => "sassFunctionDeclaration",
2018 _ => "sassSymbolDeclaration",
2019 }
2020}
2021
2022fn is_sass_symbol_candidate_kind(kind: &str) -> bool {
2023 sass_symbol_kind_from_candidate_kind(kind).is_some()
2024}
2025
2026fn is_sass_symbol_declaration_kind(kind: &str) -> bool {
2027 matches!(
2028 kind,
2029 "sassVariableDeclaration"
2030 | "sassMixinDeclaration"
2031 | "sassFunctionDeclaration"
2032 | "sassSymbolDeclaration"
2033 )
2034}
2035
2036fn sass_symbol_kind_from_candidate_kind(kind: &str) -> Option<&'static str> {
2037 match kind {
2038 "sassVariableDeclaration" | "sassVariableReference" => Some("variable"),
2039 "sassMixinDeclaration" | "sassMixinInclude" | "sassMixinReference" => Some("mixin"),
2040 "sassFunctionDeclaration" | "sassFunctionCall" | "sassFunctionReference" => {
2041 Some("function")
2042 }
2043 "sassSymbolDeclaration" | "sassSymbolReference" => Some("symbol"),
2044 _ => None,
2045 }
2046}
2047
2048fn sass_symbol_reference_candidate_kind(symbol_kind: &str, role: &str) -> &'static str {
2049 match (symbol_kind, role) {
2050 ("variable", _) => "sassVariableReference",
2051 ("mixin", "include") => "sassMixinInclude",
2052 ("function", "call") => "sassFunctionCall",
2053 ("mixin", _) => "sassMixinReference",
2054 ("function", _) => "sassFunctionReference",
2055 _ => "sassSymbolReference",
2056 }
2057}
2058
2059fn sass_variable_value_from_declaration_line(line: &str) -> Option<String> {
2060 let (_, value) = line.split_once(':')?;
2061 let value = value
2062 .trim()
2063 .trim_end_matches(';')
2064 .trim()
2065 .trim_end_matches("!default")
2066 .trim();
2067 (!value.is_empty()).then(|| value.to_string())
2068}
2069
2070fn sass_callable_definition_render_parts(
2071 source: &str,
2072 position: ParserPositionV0,
2073) -> Option<(String, String)> {
2074 let line_start = byte_offset_for_parser_position(
2075 source,
2076 ParserPositionV0 {
2077 line: position.line,
2078 character: 0,
2079 },
2080 )?;
2081 let open_brace = source[line_start..].find('{')? + line_start;
2082 let close_brace = matching_style_block_end(source, open_brace, b'{', b'}')?;
2083 let signature = source[line_start..open_brace].trim().to_string();
2084 let body = source[open_brace + 1..close_brace].trim();
2085 if signature.is_empty() || body.is_empty() {
2086 return None;
2087 }
2088 Some((signature, trim_hover_snippet(body)))
2089}
2090
2091fn rule_snippet_around_position(source: &str, position: ParserPositionV0) -> Option<String> {
2092 let line_start = byte_offset_for_parser_position(
2093 source,
2094 ParserPositionV0 {
2095 line: position.line,
2096 character: 0,
2097 },
2098 )?;
2099 let open_brace = source[line_start..].find('{')? + line_start;
2100 let mut depth = 0usize;
2101 let mut cursor = open_brace;
2102 while cursor < source.len() {
2103 match source.as_bytes().get(cursor).copied()? {
2104 b'{' => depth += 1,
2105 b'}' => {
2106 depth = depth.saturating_sub(1);
2107 if depth == 0 {
2108 let snippet = source[line_start..=cursor].trim();
2109 return Some(trim_hover_snippet(snippet));
2110 }
2111 }
2112 _ => {}
2113 }
2114 cursor = advance_style_scan_cursor(source, cursor, source.len());
2115 }
2116 None
2117}
2118
2119fn line_snippet_at_position(source: &str, position: ParserPositionV0) -> Option<String> {
2120 let line_start = byte_offset_for_parser_position(
2121 source,
2122 ParserPositionV0 {
2123 line: position.line,
2124 character: 0,
2125 },
2126 )?;
2127 let line_end = source[line_start..]
2128 .find('\n')
2129 .map(|offset| line_start + offset)
2130 .unwrap_or(source.len());
2131 Some(source[line_start..line_end].trim().to_string())
2132}
2133
2134fn trim_hover_snippet(snippet: &str) -> String {
2135 const MAX_SNIPPET_LEN: usize = 1200;
2136 if snippet.len() <= MAX_SNIPPET_LEN {
2137 return snippet.to_string();
2138 }
2139 let end = char_boundary_floor(snippet, MAX_SNIPPET_LEN);
2140 format!("{}...", snippet[..end].trim_end())
2141}
2142
2143fn custom_property_ref_byte_spans(source: &str, name: &str) -> Vec<ParserByteSpanV0> {
2144 let mut spans = Vec::new();
2145 let mut search_offset = 0usize;
2146
2147 while let Some(relative_match) = source[search_offset..].find(name) {
2148 let name_start = search_offset + relative_match;
2149 let name_end = name_start + name.len();
2150 if source[..name_start].trim_end().ends_with("var(")
2151 && is_selector_name_boundary(source, name_end)
2152 {
2153 spans.push(ParserByteSpanV0 {
2154 start: name_start,
2155 end: name_end,
2156 });
2157 }
2158 search_offset += relative_match + name.len();
2159 }
2160
2161 spans
2162}
2163
2164fn is_selector_name_boundary(source: &str, byte_offset: usize) -> bool {
2165 source[byte_offset..]
2166 .chars()
2167 .next()
2168 .is_none_or(|ch| !is_css_identifier_continue(ch))
2169}
2170
2171fn is_css_identifier_continue(ch: char) -> bool {
2172 ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_')
2173}
2174
2175fn parser_range_for_byte_span(source: &str, span: ParserByteSpanV0) -> ParserRangeV0 {
2176 ParserRangeV0 {
2177 start: parser_position_for_byte_offset(source, span.start),
2178 end: parser_position_for_byte_offset(source, span.end),
2179 }
2180}
2181
2182fn end_of_source_range(source: &str) -> ParserRangeV0 {
2183 let position = parser_position_for_byte_offset(source, source.len());
2184 ParserRangeV0 {
2185 start: position,
2186 end: position,
2187 }
2188}
2189
2190fn parser_position_for_byte_offset(source: &str, offset: usize) -> ParserPositionV0 {
2191 let clamped_offset = offset.min(source.len());
2192 let mut line = 0usize;
2193 let mut character = 0usize;
2194
2195 for (byte_index, ch) in source.char_indices() {
2196 if byte_index >= clamped_offset {
2197 break;
2198 }
2199 if ch == '\n' {
2200 line += 1;
2201 character = 0;
2202 } else {
2203 character += ch.len_utf16();
2204 }
2205 }
2206
2207 ParserPositionV0 { line, character }
2208}
2209
2210fn byte_offset_for_parser_position(source: &str, position: ParserPositionV0) -> Option<usize> {
2211 let mut current_line = 0usize;
2212 let mut current_character = 0usize;
2213
2214 if position.line == 0 && position.character == 0 {
2215 return Some(0);
2216 }
2217
2218 for (byte_index, ch) in source.char_indices() {
2219 if current_line == position.line && current_character == position.character {
2220 return Some(byte_index);
2221 }
2222 if ch == '\n' {
2223 current_line += 1;
2224 current_character = 0;
2225 if current_line == position.line && position.character == 0 {
2226 return Some(byte_index + ch.len_utf8());
2227 }
2228 } else if current_line == position.line {
2229 current_character += ch.len_utf16();
2230 }
2231 }
2232
2233 (current_line == position.line && current_character == position.character)
2234 .then_some(source.len())
2235}
2236
2237fn skip_ascii_whitespace(source: &str, mut offset: usize) -> usize {
2238 while source
2239 .as_bytes()
2240 .get(offset)
2241 .is_some_and(u8::is_ascii_whitespace)
2242 {
2243 offset += 1;
2244 }
2245 offset
2246}
2247
2248fn matching_style_block_end(
2249 source: &str,
2250 open_offset: usize,
2251 open: u8,
2252 close: u8,
2253) -> Option<usize> {
2254 if source.as_bytes().get(open_offset) != Some(&open) {
2255 return None;
2256 }
2257 let mut cursor = advance_style_scan_cursor(source, open_offset, source.len());
2258 let mut depth = 1usize;
2259 while cursor < source.len() {
2260 match source.as_bytes().get(cursor).copied()? {
2261 b'\'' | b'"' | b'`' => {
2262 cursor = skip_style_string_literal(source, cursor, source.len())?;
2263 }
2264 byte if byte == open => {
2265 depth += 1;
2266 cursor = advance_style_scan_cursor(source, cursor, source.len());
2267 }
2268 byte if byte == close => {
2269 depth -= 1;
2270 if depth == 0 {
2271 return Some(cursor);
2272 }
2273 cursor = advance_style_scan_cursor(source, cursor, source.len());
2274 }
2275 _ => cursor = advance_style_scan_cursor(source, cursor, source.len()),
2276 }
2277 }
2278 None
2279}
2280
2281fn split_top_level_style_segments(
2282 source: &str,
2283 start: usize,
2284 end: usize,
2285 delimiter: u8,
2286) -> Vec<(usize, usize)> {
2287 let mut segments = Vec::new();
2288 let end = char_boundary_floor(source, end);
2289 let mut segment_start = char_boundary_ceil(source, start).min(end);
2290 let mut cursor = segment_start;
2291 let mut depth = 0usize;
2292 while cursor < end {
2293 match source.as_bytes().get(cursor).copied() {
2294 Some(b'\'' | b'"' | b'`') => {
2295 cursor = skip_style_string_literal(source, cursor, end).unwrap_or(end);
2296 }
2297 Some(b'(' | b'[' | b'{') => {
2298 depth += 1;
2299 cursor = advance_style_scan_cursor(source, cursor, end);
2300 }
2301 Some(b')' | b']' | b'}') => {
2302 depth = depth.saturating_sub(1);
2303 cursor = advance_style_scan_cursor(source, cursor, end);
2304 }
2305 Some(byte) if byte == delimiter && depth == 0 => {
2306 segments.push((segment_start, cursor));
2307 cursor = advance_style_scan_cursor(source, cursor, end);
2308 segment_start = cursor;
2309 }
2310 Some(_) => cursor = advance_style_scan_cursor(source, cursor, end),
2311 None => break,
2312 }
2313 }
2314 if segment_start <= end {
2315 segments.push((segment_start, end));
2316 }
2317 segments
2318}
2319
2320fn skip_style_string_literal(source: &str, quote_offset: usize, limit: usize) -> Option<usize> {
2321 let quote = source.as_bytes().get(quote_offset).copied()?;
2322 let limit = char_boundary_floor(source, limit);
2323 let mut cursor = quote_offset + 1;
2324 while cursor < limit {
2325 let byte = source.as_bytes().get(cursor).copied()?;
2326 if byte == b'\\' {
2327 cursor = advance_style_escaped_char(source, cursor, limit);
2328 continue;
2329 }
2330 if byte == quote {
2331 return Some(cursor + 1);
2332 }
2333 cursor = advance_style_scan_cursor(source, cursor, limit);
2334 }
2335 None
2336}
2337
2338fn advance_style_escaped_char(source: &str, slash_offset: usize, limit: usize) -> usize {
2339 let after_slash = advance_style_scan_cursor(source, slash_offset, limit);
2340 advance_style_scan_cursor(source, after_slash, limit)
2341}
2342
2343fn advance_style_scan_cursor(source: &str, cursor: usize, limit: usize) -> usize {
2344 let cursor = char_boundary_ceil(source, cursor);
2345 let limit = char_boundary_floor(source, limit);
2346 if cursor >= limit {
2347 return limit;
2348 }
2349 char_boundary_ceil(source, cursor + 1).min(limit)
2350}
2351
2352fn char_boundary_floor(source: &str, index: usize) -> usize {
2353 let mut index = index.min(source.len());
2354 while index > 0 && !source.is_char_boundary(index) {
2355 index -= 1;
2356 }
2357 index
2358}
2359
2360fn char_boundary_ceil(source: &str, index: usize) -> usize {
2361 let mut index = index.min(source.len());
2362 while index < source.len() && !source.is_char_boundary(index) {
2363 index += 1;
2364 }
2365 index
2366}
2367
2368fn source_selector_candidate_matches_definition(
2369 candidate: &OmenaQuerySourceSelectorCandidateV0,
2370 definition: &OmenaQueryStyleSelectorDefinitionV0,
2371) -> bool {
2372 let selector_matches = if candidate.kind == "sourceSelectorPrefixReference" {
2373 definition.name.starts_with(candidate.name.as_str())
2374 } else {
2375 definition.name == candidate.name
2376 };
2377 selector_matches
2378 && candidate
2379 .target_style_uri
2380 .as_deref()
2381 .is_none_or(|target_uri| target_uri == definition.uri)
2382}
2383
2384fn source_reference_matches_target_style(
2385 reference: &OmenaQuerySourceSelectorReferenceEditTargetV0,
2386 target_style_uri: Option<&str>,
2387) -> bool {
2388 target_style_uri.is_none_or(|target_uri| {
2389 reference
2390 .target_style_uri
2391 .as_deref()
2392 .is_none_or(|candidate_target_uri| candidate_target_uri == target_uri)
2393 })
2394}
2395
2396fn is_sass_builtin_module_source(source: &str) -> bool {
2397 source.starts_with("sass:")
2398}
2399
2400fn style_language_label(language: StyleLanguage) -> &'static str {
2401 match language {
2402 StyleLanguage::Css => "css",
2403 StyleLanguage::Scss => "scss",
2404 StyleLanguage::Less => "less",
2405 }
2406}
2407
2408fn push_unique_string(values: &mut Vec<String>, value: String) {
2409 if !values.contains(&value) {
2410 values.push(value);
2411 }
2412}
2413
2414#[cfg(test)]
2415mod tests {
2416 use engine_input_producers::{
2417 ClassExpressionInputV2, EngineInputV2, PositionV2, RangeV2, SourceAnalysisInputV2,
2418 SourceDocumentV2, StringTypeFactsV2, StyleAnalysisInputV2, StyleDocumentV2,
2419 StyleSelectorV2, TypeFactEntryV2,
2420 };
2421 use omena_abstract_value::SelectorProjectionCertaintyV0;
2422
2423 use super::{
2424 OmenaQueryExpressionDomainFlowRuntimeV0, OmenaQueryStylePackageManifestV0,
2425 SelectedQueryAdapterCapabilitiesV0, summarize_omena_query_boundary,
2426 summarize_omena_query_expression_domain_control_flow_analysis,
2427 summarize_omena_query_expression_domain_flow_analysis,
2428 summarize_omena_query_expression_domain_incremental_flow_analysis,
2429 summarize_omena_query_expression_domain_selector_projection,
2430 summarize_omena_query_expression_semantics_canonical_producer_signal,
2431 summarize_omena_query_expression_semantics_query_fragments,
2432 summarize_omena_query_fragment_bundle,
2433 summarize_omena_query_selected_query_adapter_capabilities,
2434 summarize_omena_query_selector_usage_canonical_producer_signal,
2435 summarize_omena_query_selector_usage_query_fragments,
2436 summarize_omena_query_source_resolution_canonical_producer_signal,
2437 summarize_omena_query_source_resolution_query_fragments,
2438 summarize_omena_query_source_resolution_runtime,
2439 summarize_omena_query_style_semantic_graph_batch_from_sources,
2440 summarize_omena_query_style_semantic_graph_batch_from_sources_with_package_manifests,
2441 summarize_omena_query_style_semantic_graph_from_source,
2442 };
2443
2444 #[test]
2445 fn summarizes_query_boundary_over_producer_fragments() {
2446 let input = sample_input();
2447 let summary = summarize_omena_query_boundary(&input);
2448
2449 assert_eq!(summary.schema_version, "0");
2450 assert_eq!(summary.product, "omena-query.boundary");
2451 assert_eq!(summary.query_engine_name, "omena-query");
2452 assert_eq!(summary.input_version, "2");
2453 assert_eq!(
2454 summary.abstract_value_domain.product,
2455 "omena-abstract-value.domain"
2456 );
2457 assert_eq!(
2458 summary.selected_query_adapter_capabilities.product,
2459 "omena-query.selected-query-adapter-capabilities"
2460 );
2461 assert_eq!(summary.expression_semantics_query_count, 2);
2462 assert_eq!(summary.source_resolution_query_count, 2);
2463 assert_eq!(summary.selector_usage_query_count, 2);
2464 assert_eq!(summary.total_query_count, 6);
2465 assert!(
2466 summary
2467 .ready_surfaces
2468 .contains(&"abstractValueProjectionContract")
2469 );
2470 assert!(
2471 summary
2472 .ready_surfaces
2473 .contains(&"sourceResolutionResolverBoundary")
2474 );
2475 assert!(
2476 summary
2477 .delegated_fragment_products
2478 .contains(&"omena-resolver.boundary")
2479 );
2480 assert!(
2481 summary
2482 .delegated_fragment_products
2483 .contains(&"omena-resolver.source-resolution-runtime-index")
2484 );
2485 assert!(
2486 summary
2487 .delegated_fragment_products
2488 .contains(&"engine-input-producers.expression-domain-flow-analysis")
2489 );
2490 assert!(
2491 summary
2492 .delegated_fragment_products
2493 .contains(&"engine-input-producers.expression-domain-control-flow-analysis")
2494 );
2495 assert!(
2496 summary
2497 .delegated_fragment_products
2498 .contains(&"omena-query.expression-domain-incremental-flow-analysis")
2499 );
2500 assert!(
2501 summary
2502 .delegated_fragment_products
2503 .contains(&"omena-query.expression-domain-selector-projection")
2504 );
2505 assert!(
2506 summary
2507 .ready_surfaces
2508 .contains(&"expressionDomainFlowAnalysisBoundary")
2509 );
2510 assert!(
2511 summary
2512 .ready_surfaces
2513 .contains(&"expressionDomainControlFlowAnalysisBoundary")
2514 );
2515 assert!(
2516 summary
2517 .ready_surfaces
2518 .contains(&"expressionDomainSalsaRuntime")
2519 );
2520 assert!(
2521 summary
2522 .ready_surfaces
2523 .contains(&"expressionDomainSelectorProjection")
2524 );
2525 assert!(
2526 summary
2527 .ready_surfaces
2528 .contains(&"sourceResolutionRuntimeIndex")
2529 );
2530 assert!(
2531 summary
2532 .cme_coupled_surfaces
2533 .contains(&"producerQueryFragments")
2534 );
2535 }
2536
2537 #[test]
2538 fn bundles_expression_source_and_selector_query_fragments() {
2539 let input = sample_input();
2540 let bundle = summarize_omena_query_fragment_bundle(&input);
2541
2542 assert_eq!(bundle.schema_version, "0");
2543 assert_eq!(bundle.product, "omena-query.fragment-bundle");
2544 assert_eq!(bundle.input_version, "2");
2545 assert_eq!(bundle.expression_semantics.fragments.len(), 2);
2546 assert_eq!(bundle.expression_semantics.fragments[0].query_id, "expr-1");
2547 assert_eq!(bundle.source_resolution.fragments.len(), 2);
2548 assert_eq!(bundle.source_resolution.fragments[1].query_id, "expr-2");
2549 assert_eq!(bundle.selector_usage.fragments.len(), 2);
2550 assert_eq!(bundle.selector_usage.fragments[0].query_id, "btn-active");
2551
2552 let expression = summarize_omena_query_expression_semantics_query_fragments(&input);
2553 let source = summarize_omena_query_source_resolution_query_fragments(&input);
2554 let selector = summarize_omena_query_selector_usage_query_fragments(&input);
2555
2556 assert_eq!(expression.schema_version, "0");
2557 assert_eq!(source.schema_version, "0");
2558 assert_eq!(selector.schema_version, "0");
2559 assert_eq!(expression.input_version, "2");
2560 assert_eq!(source.input_version, "2");
2561 assert_eq!(selector.input_version, "2");
2562 assert_eq!(
2563 expression.fragments.len(),
2564 bundle.expression_semantics.fragments.len()
2565 );
2566 assert_eq!(
2567 source.fragments.len(),
2568 bundle.source_resolution.fragments.len()
2569 );
2570 assert_eq!(
2571 selector.fragments.len(),
2572 bundle.selector_usage.fragments.len()
2573 );
2574 }
2575
2576 #[test]
2577 fn declares_selected_query_adapter_capabilities_without_flipping_runtime_routing() {
2578 let summary = summarize_omena_query_selected_query_adapter_capabilities();
2579
2580 assert_eq!(summary.schema_version, "0");
2581 assert_eq!(
2582 summary.product,
2583 "omena-query.selected-query-adapter-capabilities"
2584 );
2585 assert_eq!(summary.default_candidate_backend, "rust-selected-query");
2586 assert_eq!(summary.routing_status, "declaredOnly");
2587
2588 let unified = backend(&summary, "rust-selected-query");
2589 assert!(unified.is_some());
2590 let Some(unified) = unified else {
2591 return;
2592 };
2593 assert!(unified.source_resolution);
2594 assert!(unified.expression_semantics);
2595 assert!(unified.selector_usage);
2596 assert!(unified.style_semantic_graph);
2597
2598 let source_only = backend(&summary, "rust-source-resolution");
2599 assert!(source_only.is_some());
2600 let Some(source_only) = source_only else {
2601 return;
2602 };
2603 assert!(source_only.source_resolution);
2604 assert!(!source_only.expression_semantics);
2605 assert!(!source_only.selector_usage);
2606 assert!(!source_only.style_semantic_graph);
2607
2608 assert!(
2609 summary
2610 .runner_commands
2611 .iter()
2612 .any(|command| command.command == "input-omena-resolver-source-resolution-runtime")
2613 );
2614 assert!(
2615 summary
2616 .runner_commands
2617 .iter()
2618 .any(|command| command.command == "input-expression-domain-flow-analysis")
2619 );
2620 assert!(
2621 summary.runner_commands.iter().any(|command| {
2622 command.command == "input-expression-domain-control-flow-analysis"
2623 })
2624 );
2625 assert!(summary.runner_commands.iter().any(|command| {
2626 command.command == "input-expression-domain-incremental-flow-analysis"
2627 }));
2628 assert!(
2629 summary
2630 .runner_commands
2631 .iter()
2632 .any(|command| command.command == "input-expression-domain-selector-projection")
2633 );
2634 assert!(
2635 summary
2636 .runner_commands
2637 .iter()
2638 .any(|command| command.command == "style-semantic-graph-batch")
2639 );
2640 assert!(
2641 summary
2642 .expression_semantics_payload_contracts
2643 .contains(&"valueDomainDerivation")
2644 );
2645 assert!(summary.adapter_readiness.contains(&"runnerCommandContract"));
2646 assert!(
2647 summary
2648 .adapter_readiness
2649 .contains(&"canonicalProducerWrapperBoundary")
2650 );
2651 assert!(
2652 summary
2653 .adapter_readiness
2654 .contains(&"styleSemanticGraphBridgeBoundary")
2655 );
2656 assert!(
2657 summary
2658 .adapter_readiness
2659 .contains(&"expressionDomainFlowAnalysisRunner")
2660 );
2661 assert!(
2662 summary
2663 .adapter_readiness
2664 .contains(&"expressionDomainControlFlowAnalysisRunner")
2665 );
2666 assert!(
2667 summary
2668 .adapter_readiness
2669 .contains(&"expressionDomainSalsaRuntime")
2670 );
2671 assert!(
2672 summary
2673 .adapter_readiness
2674 .contains(&"expressionDomainSelectorProjection")
2675 );
2676 assert!(
2677 summary
2678 .adapter_readiness
2679 .contains(&"sourceResolutionRuntimeIndex")
2680 );
2681 }
2682
2683 #[test]
2684 fn owns_expression_domain_flow_analysis_wrapper_without_changing_product() {
2685 let input = sample_input();
2686 let summary = summarize_omena_query_expression_domain_flow_analysis(&input);
2687
2688 assert_eq!(summary.schema_version, "0");
2689 assert_eq!(
2690 summary.product,
2691 "engine-input-producers.expression-domain-flow-analysis"
2692 );
2693 assert_eq!(summary.input_version, "2");
2694 assert_eq!(summary.analyses.len(), 2);
2695 assert!(
2696 summary
2697 .analyses
2698 .iter()
2699 .all(|entry| entry.analysis.product == "omena-abstract-value.flow-analysis")
2700 );
2701 assert!(
2702 summary
2703 .analyses
2704 .iter()
2705 .all(|entry| entry.analysis.converged)
2706 );
2707 }
2708
2709 #[test]
2710 fn owns_expression_domain_control_flow_analysis_wrapper_without_changing_product() {
2711 let input = sample_input();
2712 let summary = summarize_omena_query_expression_domain_control_flow_analysis(&input);
2713
2714 assert_eq!(summary.schema_version, "0");
2715 assert_eq!(
2716 summary.product,
2717 "engine-input-producers.expression-domain-control-flow-analysis"
2718 );
2719 assert_eq!(summary.input_version, "2");
2720 assert_eq!(summary.analyses.len(), 2);
2721 assert!(
2722 summary
2723 .analyses
2724 .iter()
2725 .all(|entry| entry.analysis.product
2726 == "omena-abstract-value.control-flow-analysis")
2727 );
2728 }
2729
2730 #[test]
2731 fn reuses_expression_domain_flow_analysis_through_salsa_runtime() {
2732 let input = sample_input();
2733 let mut runtime = OmenaQueryExpressionDomainFlowRuntimeV0::default();
2734
2735 let first =
2736 summarize_omena_query_expression_domain_incremental_flow_analysis(&input, &mut runtime);
2737 assert_eq!(
2738 first.product,
2739 "omena-query.expression-domain-incremental-flow-analysis"
2740 );
2741 assert_eq!(first.revision, 1);
2742 assert_eq!(first.graph_count, 2);
2743 assert_eq!(first.dirty_graph_count, 2);
2744 assert_eq!(first.reused_graph_count, 0);
2745 assert_eq!(runtime.graph_count(), 2);
2746
2747 let second =
2748 summarize_omena_query_expression_domain_incremental_flow_analysis(&input, &mut runtime);
2749 assert_eq!(second.revision, 2);
2750 assert_eq!(second.graph_count, 2);
2751 assert_eq!(second.dirty_graph_count, 0);
2752 assert_eq!(second.reused_graph_count, 2);
2753 assert!(
2754 second
2755 .analyses
2756 .iter()
2757 .all(|entry| entry.analysis.reused_previous_analysis)
2758 );
2759 }
2760
2761 #[test]
2762 fn projects_reduced_product_flow_to_target_style_selectors() {
2763 let input = reduced_product_projection_input();
2764 let summary = summarize_omena_query_expression_domain_selector_projection(&input);
2765
2766 assert_eq!(summary.schema_version, "0");
2767 assert_eq!(
2768 summary.product,
2769 "omena-query.expression-domain-selector-projection"
2770 );
2771 assert_eq!(summary.input_version, "2");
2772 assert_eq!(summary.projection_count, 3);
2773
2774 let merge = summary
2775 .projections
2776 .iter()
2777 .find(|projection| projection.node_id == "file-merge");
2778 assert!(merge.is_some());
2779 let Some(merge) = merge else {
2780 return;
2781 };
2782 assert_eq!(merge.graph_id, "/tmp/App.tsx:expression-domain-flow");
2783 assert_eq!(merge.file_path, "/tmp/App.tsx");
2784 assert_eq!(
2785 merge.target_style_paths,
2786 vec!["/tmp/App.module.scss".to_string()]
2787 );
2788 assert_eq!(merge.value_kind, "composite");
2789 assert_eq!(
2790 merge.selector_names,
2791 vec![
2792 "btn-primary--active".to_string(),
2793 "btn-secondary--active".to_string()
2794 ]
2795 );
2796 assert_eq!(merge.certainty, SelectorProjectionCertaintyV0::Inferred);
2797 }
2798
2799 #[test]
2800 fn owns_selected_query_canonical_producer_wrappers_without_changing_products() {
2801 let input = sample_input();
2802
2803 let source = summarize_omena_query_source_resolution_canonical_producer_signal(&input);
2804 assert_eq!(source.schema_version, "0");
2805 assert_eq!(source.input_version, "2");
2806 assert_eq!(source.canonical_bundle.query_fragments.len(), 2);
2807 assert_eq!(source.evaluator_candidates.results.len(), 2);
2808
2809 let expression =
2810 summarize_omena_query_expression_semantics_canonical_producer_signal(&input);
2811 assert_eq!(expression.schema_version, "0");
2812 assert_eq!(expression.input_version, "2");
2813 assert_eq!(expression.canonical_bundle.query_fragments.len(), 2);
2814 assert_eq!(expression.evaluator_candidates.results.len(), 2);
2815 assert_eq!(
2816 expression.evaluator_candidates.results[0]
2817 .payload
2818 .value_domain_derivation
2819 .product,
2820 "omena-abstract-value.reduced-class-value-derivation"
2821 );
2822 assert_eq!(
2823 expression.evaluator_candidates.results[0]
2824 .payload
2825 .value_domain_derivation
2826 .reduced_kind,
2827 "prefixSuffix"
2828 );
2829
2830 let selector = summarize_omena_query_selector_usage_canonical_producer_signal(&input);
2831 assert_eq!(selector.schema_version, "0");
2832 assert_eq!(selector.input_version, "2");
2833 assert_eq!(selector.canonical_bundle.query_fragments.len(), 2);
2834 assert_eq!(selector.evaluator_candidates.results.len(), 2);
2835 }
2836
2837 #[test]
2838 fn owns_source_resolution_runtime_index_wrapper() {
2839 let input = sample_input();
2840 let runtime_index = summarize_omena_query_source_resolution_runtime(&input);
2841
2842 assert_eq!(
2843 runtime_index.product,
2844 "omena-resolver.source-resolution-runtime-index"
2845 );
2846 assert_eq!(runtime_index.expression_count, 2);
2847 assert_eq!(runtime_index.resolved_expression_count, 2);
2848 assert_eq!(runtime_index.unresolved_expression_count, 0);
2849 assert!(
2850 runtime_index
2851 .entries
2852 .iter()
2853 .any(|entry| entry.expression_id == "expr-1"
2854 && entry.selector_names == ["btn-active"])
2855 );
2856 }
2857
2858 #[test]
2859 fn owns_style_semantic_graph_adapter_boundary_without_changing_graph_product() {
2860 let input = sample_input();
2861 let graph = summarize_omena_query_style_semantic_graph_from_source(
2862 "/tmp/App.module.scss",
2863 ".btn-active { color: red; }",
2864 &input,
2865 );
2866 assert!(graph.is_some());
2867 let Some(graph) = graph else {
2868 return;
2869 };
2870 assert_eq!(graph.schema_version, "0");
2871 assert_eq!(graph.product, "omena-semantic.style-semantic-graph");
2872 assert_eq!(graph.selector_identity_engine.canonical_ids.len(), 1);
2873
2874 let batch = summarize_omena_query_style_semantic_graph_batch_from_sources(
2875 [
2876 ("/tmp/App.module.scss", ".btn-active { color: red; }"),
2877 ("/tmp/Card.module.scss", ".card-header { color: blue; }"),
2878 ],
2879 &input,
2880 );
2881 assert_eq!(batch.schema_version, "0");
2882 assert_eq!(batch.product, "omena-semantic.style-semantic-graph-batch");
2883 assert_eq!(batch.graphs.len(), 2);
2884 assert_eq!(batch.graphs[0].style_path, "/tmp/App.module.scss");
2885 assert!(batch.graphs[0].graph.is_some());
2886 assert!(batch.graphs[1].graph.is_some());
2887 }
2888
2889 #[test]
2890 fn style_semantic_graph_batch_feeds_workspace_design_token_candidates() {
2891 let input = sample_input();
2892 let batch = summarize_omena_query_style_semantic_graph_batch_from_sources(
2893 [
2894 ("/tmp/tokens.module.scss", ":root { --brand: red; }"),
2895 ("/tmp/theme.module.scss", "@forward \"./tokens\";"),
2896 ("/tmp/unrelated.module.scss", ":root { --brand: blue; }"),
2897 (
2898 "/tmp/App.module.scss",
2899 "@use \"./theme\";\n.button { color: var(--brand); }",
2900 ),
2901 ],
2902 &input,
2903 );
2904
2905 let app_graph = batch
2906 .graphs
2907 .iter()
2908 .find(|entry| entry.style_path == "/tmp/App.module.scss")
2909 .and_then(|entry| entry.graph.as_ref());
2910 assert!(app_graph.is_some());
2911 let Some(app_graph) = app_graph else {
2912 return;
2913 };
2914 let design_tokens = &app_graph.design_token_semantics;
2915
2916 assert_eq!(
2917 design_tokens.status,
2918 "cross-file-import-cascade-ranking-seed"
2919 );
2920 assert_eq!(
2921 design_tokens.resolution_scope,
2922 "cross-file-import-candidate"
2923 );
2924 assert!(
2925 design_tokens
2926 .capabilities
2927 .workspace_cascade_candidate_signal_ready
2928 );
2929 assert!(design_tokens.capabilities.cross_file_import_graph_ready);
2930 assert_eq!(
2931 design_tokens
2932 .resolution_signal
2933 .cross_file_declaration_fact_count,
2934 1
2935 );
2936 assert_eq!(
2937 design_tokens
2938 .resolution_signal
2939 .workspace_occurrence_resolved_reference_count,
2940 1
2941 );
2942 assert_eq!(
2943 design_tokens
2944 .cascade_ranking_signal
2945 .cross_file_candidate_declaration_count,
2946 1
2947 );
2948 assert_eq!(
2949 design_tokens
2950 .cascade_ranking_signal
2951 .cross_file_winner_declaration_count,
2952 1
2953 );
2954 assert_eq!(
2955 design_tokens.cascade_ranking_signal.ranked_references[0]
2956 .winner_declaration_file_path
2957 .as_deref(),
2958 Some("/tmp/tokens.module.scss")
2959 );
2960 let winner_range =
2961 design_tokens.cascade_ranking_signal.ranked_references[0].winner_declaration_range;
2962 assert_eq!(winner_range.map(|range| range.start.line), Some(0));
2963 assert_eq!(winner_range.map(|range| range.start.character), Some(8));
2964 assert_eq!(design_tokens.declaration_candidates.len(), 1);
2965 let declaration_candidate = &design_tokens.declaration_candidates[0];
2966 assert_eq!(declaration_candidate.name, "--brand");
2967 assert_eq!(declaration_candidate.file_path, "/tmp/tokens.module.scss");
2968 assert_eq!(
2969 declaration_candidate.candidate_scope,
2970 "cross-file-import-candidate"
2971 );
2972 assert!(declaration_candidate.import_graph_distance.is_some());
2973 assert_eq!(
2974 design_tokens.cascade_ranking_signal.ranked_references[0]
2975 .cross_file_candidate_declaration_count,
2976 1
2977 );
2978 }
2979
2980 #[test]
2981 fn style_semantic_graph_batch_prefers_nearer_import_graph_token_candidates() {
2982 let input = sample_input();
2983 let batch = summarize_omena_query_style_semantic_graph_batch_from_sources(
2984 [
2985 ("/tmp/a-direct.module.scss", ":root { --brand: direct; }"),
2986 ("/tmp/mid.module.scss", "@forward \"./z-transitive\";"),
2987 (
2988 "/tmp/z-transitive.module.scss",
2989 ":root { --brand: transitive; }",
2990 ),
2991 (
2992 "/tmp/App.module.scss",
2993 "@use \"./a-direct\";\n@use \"./mid\";\n.button { color: var(--brand); }",
2994 ),
2995 ],
2996 &input,
2997 );
2998
2999 let app_graph = batch
3000 .graphs
3001 .iter()
3002 .find(|entry| entry.style_path == "/tmp/App.module.scss")
3003 .and_then(|entry| entry.graph.as_ref());
3004 assert!(app_graph.is_some());
3005 let Some(app_graph) = app_graph else {
3006 return;
3007 };
3008 let ranked_reference = &app_graph
3009 .design_token_semantics
3010 .cascade_ranking_signal
3011 .ranked_references[0];
3012
3013 assert_eq!(
3014 ranked_reference.winner_declaration_file_path.as_deref(),
3015 Some("/tmp/a-direct.module.scss")
3016 );
3017 assert_eq!(ranked_reference.winner_import_graph_distance, Some(1));
3018 assert_eq!(ranked_reference.winner_import_graph_order, Some(0));
3019 assert_eq!(ranked_reference.cross_file_candidate_declaration_count, 2);
3020 assert_eq!(ranked_reference.cross_file_shadowed_declaration_count, 1);
3021 }
3022
3023 #[test]
3024 fn style_semantic_graph_batch_resolves_package_root_forward_chain_token_candidates() {
3025 let input = sample_input();
3026 let batch = summarize_omena_query_style_semantic_graph_batch_from_sources(
3027 [
3028 (
3029 "/fake/workspace/node_modules/@design/tokens/src/index.scss",
3030 "@forward \"./colors\";",
3031 ),
3032 (
3033 "/fake/workspace/node_modules/@design/tokens/src/_colors.scss",
3034 ":root { --brand: package; }",
3035 ),
3036 (
3037 "/fake/workspace/src/_utils.scss",
3038 "@forward \"@design/tokens\" as ds_*;",
3039 ),
3040 (
3041 "/fake/workspace/src/App.module.scss",
3042 "@use \"./utils\";\n.button { color: var(--brand); }",
3043 ),
3044 ],
3045 &input,
3046 );
3047
3048 let app_graph = batch
3049 .graphs
3050 .iter()
3051 .find(|entry| entry.style_path == "/fake/workspace/src/App.module.scss")
3052 .and_then(|entry| entry.graph.as_ref());
3053 assert!(app_graph.is_some());
3054 let Some(app_graph) = app_graph else {
3055 return;
3056 };
3057 let ranked_reference = &app_graph
3058 .design_token_semantics
3059 .cascade_ranking_signal
3060 .ranked_references[0];
3061
3062 assert_eq!(
3063 ranked_reference.winner_declaration_file_path.as_deref(),
3064 Some("/fake/workspace/node_modules/@design/tokens/src/_colors.scss")
3065 );
3066 assert_eq!(ranked_reference.winner_import_graph_distance, Some(3));
3067 assert_eq!(ranked_reference.cross_file_candidate_declaration_count, 1);
3068 }
3069
3070 #[test]
3071 fn style_semantic_graph_batch_resolves_package_manifest_style_exports() {
3072 let input = sample_input();
3073 let batch =
3074 summarize_omena_query_style_semantic_graph_batch_from_sources_with_package_manifests(
3075 [
3076 (
3077 "/fake/workspace/node_modules/@design/tokens/dist/theme.css",
3078 ":root { --brand: package; }",
3079 ),
3080 (
3081 "/fake/workspace/src/App.module.scss",
3082 "@use \"@design/tokens/theme\";\n.button { color: var(--brand); }",
3083 ),
3084 ],
3085 &input,
3086 &[OmenaQueryStylePackageManifestV0 {
3087 package_json_path: "/fake/workspace/node_modules/@design/tokens/package.json"
3088 .to_string(),
3089 package_json_source: r#"{"exports":{"./theme":{"style":"./dist/theme.css"}}}"#
3090 .to_string(),
3091 }],
3092 );
3093
3094 let app_graph = batch
3095 .graphs
3096 .iter()
3097 .find(|entry| entry.style_path == "/fake/workspace/src/App.module.scss")
3098 .and_then(|entry| entry.graph.as_ref());
3099 assert!(app_graph.is_some());
3100 let Some(app_graph) = app_graph else {
3101 return;
3102 };
3103 let ranked_reference = &app_graph
3104 .design_token_semantics
3105 .cascade_ranking_signal
3106 .ranked_references[0];
3107
3108 assert_eq!(
3109 ranked_reference.winner_declaration_file_path.as_deref(),
3110 Some("/fake/workspace/node_modules/@design/tokens/dist/theme.css")
3111 );
3112 assert_eq!(ranked_reference.winner_import_graph_distance, Some(1));
3113 assert_eq!(ranked_reference.cross_file_candidate_declaration_count, 1);
3114 let declaration_candidate = &app_graph.design_token_semantics.declaration_candidates[0];
3115 assert_eq!(declaration_candidate.name, "--brand");
3116 assert_eq!(
3117 declaration_candidate.file_path,
3118 "/fake/workspace/node_modules/@design/tokens/dist/theme.css"
3119 );
3120 assert_eq!(
3121 declaration_candidate.candidate_scope,
3122 "cross-file-import-candidate"
3123 );
3124 assert_eq!(declaration_candidate.import_graph_distance, Some(1));
3125 }
3126
3127 #[test]
3128 fn style_hover_candidates_are_query_owned() {
3129 let candidates = super::summarize_omena_query_style_hover_candidates(
3130 "Component.module.scss",
3131 r#"
3132@mixin variants($prefix, $map) {
3133 @each $name, $value in $map {
3134 .#{$prefix}-#{$name} { color: $value; }
3135 }
3136}
3137
3138$accent: red;
3139.button { color: var(--brand); }
3140:root { --brand: blue; }
3141@include variants($prefix: "tone", $map: ("warm": red));
3142"#,
3143 );
3144 assert!(candidates.is_some());
3145 let Some(candidates) = candidates else {
3146 return;
3147 };
3148
3149 assert_eq!(candidates.product, "omena-query.style-hover-candidates");
3150 assert!(
3151 candidates
3152 .candidates
3153 .iter()
3154 .any(|candidate| candidate.kind == "selector" && candidate.name == "button")
3155 );
3156 assert!(candidates.candidates.iter().any(|candidate| {
3157 candidate.kind == "customPropertyReference" && candidate.name == "--brand"
3158 }));
3159 assert!(candidates.candidates.iter().any(|candidate| {
3160 candidate.kind == "sassVariableDeclaration" && candidate.name == "accent"
3161 }));
3162 assert!(
3163 candidates
3164 .candidates
3165 .iter()
3166 .any(
3167 |candidate| candidate.source == "sassPartialEvaluatorGeneratedSelectors"
3168 && candidate.name == "tone-warm"
3169 )
3170 );
3171 }
3172
3173 #[test]
3174 fn style_hover_render_parts_are_query_owned() {
3175 let source = r#"$accent: red !default;
3176@mixin tone($color) {
3177 color: $color;
3178}
3179.button { color: var(--brand); }
3180"#;
3181
3182 let variable = super::summarize_omena_query_style_hover_render_parts(
3183 source,
3184 "sassVariableDeclaration",
3185 "accent",
3186 engine_style_parser::ParserPositionV0 {
3187 line: 0,
3188 character: 1,
3189 },
3190 );
3191 assert_eq!(variable.product, "omena-query.style-hover-render-parts");
3192 assert_eq!(variable.value.as_deref(), Some("red"));
3193 assert_eq!(variable.snippet, "$accent: red !default;");
3194
3195 let mixin = super::summarize_omena_query_style_hover_render_parts(
3196 source,
3197 "sassMixinDeclaration",
3198 "tone",
3199 engine_style_parser::ParserPositionV0 {
3200 line: 1,
3201 character: 7,
3202 },
3203 );
3204 assert_eq!(mixin.signature.as_deref(), Some("@mixin tone($color)"));
3205 assert_eq!(mixin.snippet, "color: $color;");
3206 assert_eq!(mixin.render_source, "callableBlockSnippet");
3207
3208 let selector = super::summarize_omena_query_style_hover_render_parts(
3209 source,
3210 "selector",
3211 "button",
3212 engine_style_parser::ParserPositionV0 {
3213 line: 4,
3214 character: 1,
3215 },
3216 );
3217 assert_eq!(selector.snippet, ".button { color: var(--brand); }");
3218 assert_eq!(selector.render_source, "ruleSnippet");
3219 }
3220
3221 #[test]
3222 fn missing_custom_property_diagnostics_are_query_owned() {
3223 let source = ":root { --brand: red; }\n.alert { color: var(--missing); }";
3224 let candidates =
3225 super::summarize_omena_query_style_hover_candidates("Component.module.scss", source);
3226 assert!(candidates.is_some());
3227 let Some(candidates) = candidates else {
3228 return;
3229 };
3230
3231 let diagnostics = super::summarize_omena_query_missing_custom_property_diagnostics(
3232 "file:///workspace/src/Component.module.scss",
3233 source,
3234 candidates.candidates.as_slice(),
3235 );
3236
3237 assert_eq!(diagnostics.len(), 1);
3238 let diagnostic = &diagnostics[0];
3239 assert_eq!(diagnostic.code, "missingCustomProperty");
3240 assert_eq!(
3241 diagnostic.message,
3242 "CSS custom property '--missing' not found in indexed style tokens."
3243 );
3244 assert_eq!(
3245 diagnostic.range,
3246 engine_style_parser::ParserRangeV0 {
3247 start: engine_style_parser::ParserPositionV0 {
3248 line: 1,
3249 character: 20,
3250 },
3251 end: engine_style_parser::ParserPositionV0 {
3252 line: 1,
3253 character: 29,
3254 },
3255 }
3256 );
3257 assert_eq!(
3258 diagnostic
3259 .create_custom_property
3260 .as_ref()
3261 .map(|action| action.new_text.as_str()),
3262 Some("\n\n:root {\n --missing: ;\n}\n")
3263 );
3264 assert_eq!(
3265 diagnostic
3266 .create_custom_property
3267 .as_ref()
3268 .map(|action| action.range),
3269 Some(engine_style_parser::ParserRangeV0 {
3270 start: engine_style_parser::ParserPositionV0 {
3271 line: 1,
3272 character: 33,
3273 },
3274 end: engine_style_parser::ParserPositionV0 {
3275 line: 1,
3276 character: 33,
3277 },
3278 })
3279 );
3280 }
3281
3282 #[test]
3283 fn missing_selector_diagnostics_are_query_owned() {
3284 let diagnostic = super::summarize_omena_query_missing_selector_diagnostic(
3285 "file:///workspace/src/App.module.scss",
3286 ".root {\n}\n",
3287 "missing",
3288 engine_style_parser::ParserRangeV0 {
3289 start: engine_style_parser::ParserPositionV0 {
3290 line: 2,
3291 character: 18,
3292 },
3293 end: engine_style_parser::ParserPositionV0 {
3294 line: 2,
3295 character: 25,
3296 },
3297 },
3298 );
3299
3300 assert_eq!(diagnostic.code, "missingSelector");
3301 assert_eq!(
3302 diagnostic.message,
3303 "CSS Module selector '.missing' not found in indexed style tokens."
3304 );
3305 assert_eq!(
3306 diagnostic
3307 .create_selector
3308 .as_ref()
3309 .map(|action| action.new_text.as_str()),
3310 Some("\n\n.missing {\n}\n")
3311 );
3312 assert_eq!(
3313 diagnostic
3314 .create_selector
3315 .as_ref()
3316 .map(|action| action.range),
3317 Some(engine_style_parser::ParserRangeV0 {
3318 start: engine_style_parser::ParserPositionV0 {
3319 line: 2,
3320 character: 0,
3321 },
3322 end: engine_style_parser::ParserPositionV0 {
3323 line: 2,
3324 character: 0,
3325 },
3326 })
3327 );
3328 }
3329
3330 #[test]
3331 fn source_provider_candidate_resolution_is_query_owned() {
3332 let source_range = engine_style_parser::ParserRangeV0 {
3333 start: engine_style_parser::ParserPositionV0 {
3334 line: 0,
3335 character: 0,
3336 },
3337 end: engine_style_parser::ParserPositionV0 {
3338 line: 0,
3339 character: 4,
3340 },
3341 };
3342 let definition_range = engine_style_parser::ParserRangeV0 {
3343 start: engine_style_parser::ParserPositionV0 {
3344 line: 1,
3345 character: 1,
3346 },
3347 end: engine_style_parser::ParserPositionV0 {
3348 line: 1,
3349 character: 5,
3350 },
3351 };
3352
3353 let resolution = super::resolve_omena_query_source_provider_candidates(
3354 vec![
3355 super::OmenaQuerySourceSelectorCandidateV0 {
3356 kind: "sourceSelectorReference",
3357 name: "root".to_string(),
3358 range: source_range,
3359 source: "omenaQuerySourceSyntaxIndex",
3360 target_style_uri: Some("file:///workspace/src/App.module.scss".to_string()),
3361 },
3362 super::OmenaQuerySourceSelectorCandidateV0 {
3363 kind: "sourceSelectorPrefixReference",
3364 name: "btn-".to_string(),
3365 range: source_range,
3366 source: "omenaQuerySourceSyntaxIndex",
3367 target_style_uri: Some("file:///workspace/src/App.module.scss".to_string()),
3368 },
3369 super::OmenaQuerySourceSelectorCandidateV0 {
3370 kind: "sourceSelectorReference",
3371 name: "ghost".to_string(),
3372 range: source_range,
3373 source: "omenaQuerySourceSyntaxIndex",
3374 target_style_uri: Some("file:///workspace/src/Other.module.scss".to_string()),
3375 },
3376 ],
3377 &[
3378 super::OmenaQueryStyleSelectorDefinitionV0 {
3379 uri: "file:///workspace/src/App.module.scss".to_string(),
3380 name: "root".to_string(),
3381 range: definition_range,
3382 },
3383 super::OmenaQueryStyleSelectorDefinitionV0 {
3384 uri: "file:///workspace/src/App.module.scss".to_string(),
3385 name: "btn-primary".to_string(),
3386 range: definition_range,
3387 },
3388 ],
3389 );
3390
3391 assert_eq!(
3392 resolution
3393 .matched
3394 .iter()
3395 .map(|candidate| candidate.name.as_str())
3396 .collect::<Vec<_>>(),
3397 vec!["btn-", "root"]
3398 );
3399 assert_eq!(
3400 resolution
3401 .unresolved
3402 .iter()
3403 .map(|candidate| candidate.name.as_str())
3404 .collect::<Vec<_>>(),
3405 vec!["ghost"]
3406 );
3407
3408 let prefix_candidate = &resolution.matched[0];
3409 let definitions = vec![
3410 super::OmenaQueryStyleSelectorDefinitionV0 {
3411 uri: "file:///workspace/src/App.module.scss".to_string(),
3412 name: "root".to_string(),
3413 range: definition_range,
3414 },
3415 super::OmenaQueryStyleSelectorDefinitionV0 {
3416 uri: "file:///workspace/src/App.module.scss".to_string(),
3417 name: "btn-primary".to_string(),
3418 range: definition_range,
3419 },
3420 ];
3421 assert_eq!(
3422 super::resolve_omena_query_source_candidate_selector_names(
3423 prefix_candidate,
3424 definitions.as_slice(),
3425 None,
3426 ),
3427 vec!["btn-primary".to_string()]
3428 );
3429 assert_eq!(
3430 super::resolve_omena_query_style_selector_definitions_for_source_candidate(
3431 prefix_candidate,
3432 definitions.as_slice(),
3433 )
3434 .into_iter()
3435 .map(|definition| definition.name)
3436 .collect::<Vec<_>>(),
3437 vec!["btn-primary".to_string()]
3438 );
3439 }
3440
3441 #[test]
3442 fn source_syntax_index_adapter_is_query_owned_without_changing_product() {
3443 let style_uri = super::resolve_omena_query_style_uri_for_specifier(
3444 "file:///workspace/src/Button.tsx",
3445 Some("file:///workspace"),
3446 "./Button.module.scss",
3447 );
3448 assert_eq!(
3449 style_uri.as_deref(),
3450 Some("file:///workspace/src/Button.module.scss")
3451 );
3452 let style_uri = style_uri.unwrap_or_default();
3453 assert_eq!(style_uri, "file:///workspace/src/Button.module.scss");
3454
3455 let import_summary = super::summarize_omena_query_source_import_declarations(
3456 "import styles from './Button.module.scss';",
3457 );
3458 assert_eq!(import_summary.import_count, 1);
3459 assert_eq!(import_summary.imports[0].binding, "styles");
3460
3461 let source = "import styles from './Button.module.scss';\nconst el = styles.root;\n";
3462 let mut index = super::summarize_omena_query_source_syntax_index(
3463 source,
3464 vec![super::OmenaQuerySourceImportedStyleBindingV0 {
3465 binding: "styles".to_string(),
3466 style_uri,
3467 }],
3468 Vec::new(),
3469 );
3470 assert_eq!(index.product, "omena-bridge.source-syntax-index");
3471 assert_eq!(index.selector_references.len(), 1);
3472 let reference = &index.selector_references[0];
3473 assert_eq!(
3474 &source[reference.byte_span.start..reference.byte_span.end],
3475 "root"
3476 );
3477
3478 super::canonicalize_omena_query_source_selector_references(&mut index.selector_references);
3479 assert_eq!(index.selector_references.len(), 1);
3480 }
3481
3482 #[test]
3483 fn selector_rename_edit_planning_is_query_owned() {
3484 let source_range = engine_style_parser::ParserRangeV0 {
3485 start: engine_style_parser::ParserPositionV0 {
3486 line: 3,
3487 character: 16,
3488 },
3489 end: engine_style_parser::ParserPositionV0 {
3490 line: 3,
3491 character: 20,
3492 },
3493 };
3494 let definition_range = engine_style_parser::ParserRangeV0 {
3495 start: engine_style_parser::ParserPositionV0 {
3496 line: 0,
3497 character: 1,
3498 },
3499 end: engine_style_parser::ParserPositionV0 {
3500 line: 0,
3501 character: 5,
3502 },
3503 };
3504
3505 let edits = super::resolve_omena_query_selector_rename_edits(
3506 "root",
3507 ".shell",
3508 Some("file:///workspace/src/App.module.scss"),
3509 &[super::OmenaQueryStyleSelectorDefinitionV0 {
3510 uri: "file:///workspace/src/App.module.scss".to_string(),
3511 name: "root".to_string(),
3512 range: definition_range,
3513 }],
3514 &[super::OmenaQuerySourceSelectorReferenceEditTargetV0 {
3515 uri: "file:///workspace/src/App.tsx".to_string(),
3516 name: "root".to_string(),
3517 range: source_range,
3518 target_style_uri: Some("file:///workspace/src/App.module.scss".to_string()),
3519 }],
3520 );
3521
3522 assert_eq!(
3523 edits
3524 .iter()
3525 .map(|edit| (edit.uri.as_str(), edit.new_text.as_str()))
3526 .collect::<Vec<_>>(),
3527 vec![
3528 ("file:///workspace/src/App.module.scss", "shell"),
3529 ("file:///workspace/src/App.tsx", "shell"),
3530 ]
3531 );
3532 }
3533
3534 #[test]
3535 fn sass_symbol_matching_is_query_owned() {
3536 let source = "$accent: red;\n.button { color: $accent; }\n";
3537 let Some(candidates) =
3538 super::summarize_omena_query_style_hover_candidates("Component.module.scss", source)
3539 else {
3540 return;
3541 };
3542
3543 assert!(super::is_omena_query_sass_symbol_candidate_kind(
3544 "sassVariableDeclaration"
3545 ));
3546 assert!(super::is_omena_query_sass_symbol_reference_kind(
3547 "sassVariableReference"
3548 ));
3549 assert_eq!(
3550 super::omena_query_sass_symbol_kind_from_candidate_kind("sassVariableReference"),
3551 Some("variable")
3552 );
3553 assert!(super::omena_query_sass_symbol_target_matches(
3554 "sassVariableReference",
3555 "accent",
3556 None,
3557 "sassVariableDeclaration",
3558 "accent",
3559 None,
3560 ));
3561
3562 let declarations = super::resolve_omena_query_sass_symbol_declarations(
3563 candidates.candidates.as_slice(),
3564 "variable",
3565 "accent",
3566 );
3567 assert_eq!(declarations.len(), 1);
3568 assert_eq!(declarations[0].kind, "sassVariableDeclaration");
3569 }
3570
3571 #[test]
3572 fn sass_module_sources_are_query_owned() {
3573 let sources = super::summarize_omena_query_sass_module_sources(
3574 "Component.module.scss",
3575 r#"
3576@use "./tokens" as tokens;
3577@use "./reset" as *;
3578@use "sass:map";
3579@forward "./theme";
3580@forward "sass:color";
3581"#,
3582 );
3583 assert!(sources.is_some());
3584 let Some(sources) = sources else {
3585 return;
3586 };
3587
3588 assert_eq!(sources.product, "omena-query.sass-module-sources");
3589 assert!(sources.module_use_edges.iter().any(|edge| {
3590 edge.source == "./tokens"
3591 && edge.namespace.as_deref() == Some("tokens")
3592 && edge.namespace_kind == "alias"
3593 }));
3594 assert!(sources.module_use_edges.iter().any(|edge| {
3595 edge.source == "./reset"
3596 && edge.namespace.is_none()
3597 && edge.namespace_kind == "wildcard"
3598 }));
3599 assert_eq!(
3600 super::resolve_omena_query_sass_module_use_sources_for_candidate(&sources, None),
3601 vec!["./reset".to_string()]
3602 );
3603 assert_eq!(
3604 super::resolve_omena_query_sass_module_use_sources_for_candidate(
3605 &sources,
3606 Some("tokens"),
3607 ),
3608 vec!["./tokens".to_string()]
3609 );
3610 assert_eq!(
3611 super::resolve_omena_query_sass_forward_sources(&sources),
3612 vec!["./theme".to_string()]
3613 );
3614 }
3615
3616 fn backend<'a>(
3617 summary: &'a SelectedQueryAdapterCapabilitiesV0,
3618 backend_kind: &str,
3619 ) -> Option<&'a super::SelectedQueryBackendCapabilityV0> {
3620 summary
3621 .backend_kinds
3622 .iter()
3623 .find(|backend| backend.backend_kind == backend_kind)
3624 }
3625
3626 fn sample_input() -> EngineInputV2 {
3627 EngineInputV2 {
3628 version: "2".to_string(),
3629 sources: vec![SourceAnalysisInputV2 {
3630 document: SourceDocumentV2 {
3631 class_expressions: vec![
3632 ClassExpressionInputV2 {
3633 id: "expr-1".to_string(),
3634 kind: "symbolRef".to_string(),
3635 scss_module_path: "/tmp/App.module.scss".to_string(),
3636 range: range(4, 12, 4, 16),
3637 class_name: None,
3638 root_binding_decl_id: Some("decl-1".to_string()),
3639 access_path: None,
3640 },
3641 ClassExpressionInputV2 {
3642 id: "expr-2".to_string(),
3643 kind: "styleAccess".to_string(),
3644 scss_module_path: "/tmp/Card.module.scss".to_string(),
3645 range: range(6, 9, 6, 20),
3646 class_name: Some("card-header".to_string()),
3647 root_binding_decl_id: None,
3648 access_path: Some(vec!["card".to_string(), "header".to_string()]),
3649 },
3650 ],
3651 },
3652 }],
3653 styles: vec![
3654 StyleAnalysisInputV2 {
3655 file_path: "/tmp/App.module.scss".to_string(),
3656 document: StyleDocumentV2 {
3657 selectors: vec![StyleSelectorV2 {
3658 name: "btn-active".to_string(),
3659 view_kind: "canonical".to_string(),
3660 canonical_name: Some("btn-active".to_string()),
3661 range: range(1, 1, 1, 12),
3662 nested_safety: Some("safe".to_string()),
3663 composes: None,
3664 bem_suffix: None,
3665 }],
3666 },
3667 },
3668 StyleAnalysisInputV2 {
3669 file_path: "/tmp/Card.module.scss".to_string(),
3670 document: StyleDocumentV2 {
3671 selectors: vec![StyleSelectorV2 {
3672 name: "card-header".to_string(),
3673 view_kind: "canonical".to_string(),
3674 canonical_name: Some("card-header".to_string()),
3675 range: range(3, 1, 3, 13),
3676 nested_safety: Some("unsafe".to_string()),
3677 composes: None,
3678 bem_suffix: None,
3679 }],
3680 },
3681 },
3682 ],
3683 type_facts: vec![
3684 TypeFactEntryV2 {
3685 file_path: "/tmp/App.tsx".to_string(),
3686 expression_id: "expr-1".to_string(),
3687 facts: StringTypeFactsV2 {
3688 kind: "constrained".to_string(),
3689 constraint_kind: Some("prefixSuffix".to_string()),
3690 values: None,
3691 prefix: Some("btn-".to_string()),
3692 suffix: Some("-active".to_string()),
3693 min_len: Some(10),
3694 max_len: None,
3695 char_must: None,
3696 char_may: None,
3697 may_include_other_chars: None,
3698 },
3699 },
3700 TypeFactEntryV2 {
3701 file_path: "/tmp/Card.tsx".to_string(),
3702 expression_id: "expr-2".to_string(),
3703 facts: StringTypeFactsV2 {
3704 kind: "finiteSet".to_string(),
3705 constraint_kind: None,
3706 values: Some(vec!["card-header".to_string(), "card-body".to_string()]),
3707 prefix: None,
3708 suffix: None,
3709 min_len: None,
3710 max_len: None,
3711 char_must: None,
3712 char_may: None,
3713 may_include_other_chars: None,
3714 },
3715 },
3716 ],
3717 }
3718 }
3719
3720 fn reduced_product_projection_input() -> EngineInputV2 {
3721 EngineInputV2 {
3722 version: "2".to_string(),
3723 sources: vec![SourceAnalysisInputV2 {
3724 document: SourceDocumentV2 {
3725 class_expressions: vec![
3726 ClassExpressionInputV2 {
3727 id: "expr-primary".to_string(),
3728 kind: "symbolRef".to_string(),
3729 scss_module_path: "/tmp/App.module.scss".to_string(),
3730 range: range(4, 12, 4, 16),
3731 class_name: None,
3732 root_binding_decl_id: Some("decl-primary".to_string()),
3733 access_path: None,
3734 },
3735 ClassExpressionInputV2 {
3736 id: "expr-secondary".to_string(),
3737 kind: "symbolRef".to_string(),
3738 scss_module_path: "/tmp/App.module.scss".to_string(),
3739 range: range(5, 12, 5, 16),
3740 class_name: None,
3741 root_binding_decl_id: Some("decl-secondary".to_string()),
3742 access_path: None,
3743 },
3744 ],
3745 },
3746 }],
3747 styles: vec![StyleAnalysisInputV2 {
3748 file_path: "/tmp/App.module.scss".to_string(),
3749 document: StyleDocumentV2 {
3750 selectors: vec![
3751 style_selector("btn--active"),
3752 style_selector("btn-primary--active"),
3753 style_selector("btn-secondary--active"),
3754 style_selector("card-active"),
3755 ],
3756 },
3757 }],
3758 type_facts: vec![
3759 TypeFactEntryV2 {
3760 file_path: "/tmp/App.tsx".to_string(),
3761 expression_id: "expr-primary".to_string(),
3762 facts: StringTypeFactsV2 {
3763 kind: "constrained".to_string(),
3764 constraint_kind: Some("prefixSuffix".to_string()),
3765 values: None,
3766 prefix: Some("btn-primary-".to_string()),
3767 suffix: Some("-active".to_string()),
3768 min_len: Some("btn-primary--active".len()),
3769 max_len: None,
3770 char_must: None,
3771 char_may: None,
3772 may_include_other_chars: None,
3773 },
3774 },
3775 TypeFactEntryV2 {
3776 file_path: "/tmp/App.tsx".to_string(),
3777 expression_id: "expr-secondary".to_string(),
3778 facts: StringTypeFactsV2 {
3779 kind: "constrained".to_string(),
3780 constraint_kind: Some("prefixSuffix".to_string()),
3781 values: None,
3782 prefix: Some("btn-secondary-".to_string()),
3783 suffix: Some("-active".to_string()),
3784 min_len: Some("btn-secondary--active".len()),
3785 max_len: None,
3786 char_must: None,
3787 char_may: None,
3788 may_include_other_chars: None,
3789 },
3790 },
3791 ],
3792 }
3793 }
3794
3795 fn style_selector(name: &str) -> StyleSelectorV2 {
3796 StyleSelectorV2 {
3797 name: name.to_string(),
3798 view_kind: "canonical".to_string(),
3799 canonical_name: Some(name.to_string()),
3800 range: range(1, 1, 1, 1 + name.len()),
3801 nested_safety: Some("safe".to_string()),
3802 composes: None,
3803 bem_suffix: None,
3804 }
3805 }
3806
3807 fn range(
3808 start_line: usize,
3809 start_character: usize,
3810 end_line: usize,
3811 end_character: usize,
3812 ) -> RangeV2 {
3813 RangeV2 {
3814 start: PositionV2 {
3815 line: start_line,
3816 character: start_character,
3817 },
3818 end: PositionV2 {
3819 line: end_line,
3820 character: end_character,
3821 },
3822 }
3823 }
3824}