use std::collections::{BTreeMap, BTreeSet};
use omena_parser::{
ParsedAnimationFactKind, ParsedCssModuleComposesEdgeKind, ParsedExtendTargetFactKind,
ParsedSassModuleEdgeFact, ParsedSassModuleEdgeFactKind, ParsedSelectorFactKind,
ParsedVariableFactKind,
};
use omena_query_checker_orchestrator::{
ModuleGraphEdgeV0, ModuleGraphV0, OutcomeMode, REPLICA_ENSEMBLE_FEATURE_GATE_V0,
REPLICA_ENSEMBLE_LAYER_MARKER_V0, REPLICA_ENSEMBLE_SCHEMA_VERSION_V0, ReplicaSnapshotV0,
ReportOptionsV0, ReportRecommendation, build_cross_file_inconsistency_report,
};
use omena_query_checker_orchestrator::{
OmenaCheckerReplicaEnsembleInputV0, OmenaCheckerReplicaEnsembleReportInputV0,
run_omena_query_checker_replica_ensemble_gate_v0,
};
use super::cascade_checker::collect_query_replica_ensemble_site_outcomes;
use super::cascade_checker::summarize_query_cascade_checker_diagnostics_with_deep_analysis;
use super::diagnostic_suppressions::OmenaStrictnessLevelV0;
use super::diagnostic_suppressions::apply_omena_query_style_diagnostic_suppressions;
use super::diagnostic_suppressions::parse_omena_query_style_strictness_level;
use super::parser_facade::collect_omena_query_omena_parser_style_facts_raw;
use super::*;
const LSP_DIAGNOSTIC_TAG_UNNECESSARY: u8 = 1;
const LSP_DIAGNOSTIC_TAG_DEPRECATED: u8 = 2;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OmenaQueryExternalModuleModeV0 {
Ignored,
Sif,
}
pub fn summarize_omena_query_missing_custom_property_diagnostics(
style_uri: &str,
source: &str,
candidates: &[OmenaQueryStyleHoverCandidateV0],
) -> Vec<OmenaQueryStyleDiagnosticV0> {
let declaration_names = candidates
.iter()
.filter(|candidate| candidate.kind == "customPropertyDeclaration")
.map(|candidate| candidate.name.as_str())
.collect::<BTreeSet<_>>();
if declaration_names.is_empty() {
return Vec::new();
}
let dialect = omena_parser_dialect_for_style_path(style_uri);
let facts = collect_omena_query_omena_parser_style_facts_raw(source, dialect);
let fallback_ranges = facts
.variables
.iter()
.filter(|fact| {
fact.kind == ParsedVariableFactKind::CustomPropertyReference && fact.has_fallback
})
.map(|fact| {
let byte_span = ParserByteSpanV0 {
start: u32::from(fact.range.start()) as usize,
end: u32::from(fact.range.end()) as usize,
};
(
fact.name.clone(),
parser_range_for_byte_span(source, byte_span),
)
})
.collect::<BTreeSet<_>>();
let insertion_range = end_of_source_range(source);
candidates
.iter()
.filter(|candidate| {
candidate.kind == "customPropertyReference"
&& !declaration_names.contains(candidate.name.as_str())
&& !fallback_ranges.contains(&(candidate.name.clone(), candidate.range))
})
.map(|candidate| OmenaQueryStyleDiagnosticV0 {
code: "missingCustomProperty",
severity: "warning",
provenance: vec![
"omena-parser.custom-property-facts",
"omena-query.style-diagnostics",
],
range: candidate.range,
message: format!(
"CSS custom property '{}' not found in indexed style tokens.",
candidate.name
),
tags: Vec::new(),
create_custom_property: Some(OmenaQueryCreateCustomPropertyActionV0 {
uri: style_uri.to_string(),
range: insertion_range,
new_text: format!("\n\n:root {{\n {}: ;\n}}\n", candidate.name),
property_name: candidate.name.clone(),
}),
})
.collect()
}
pub fn summarize_omena_query_cascade_aware_style_diagnostics(
style_uri: &str,
source: &str,
candidates: &[OmenaQueryStyleHoverCandidateV0],
) -> Vec<OmenaQueryStyleDiagnosticV0> {
summarize_omena_query_cascade_aware_style_diagnostics_with_deep_analysis(
style_uri, source, candidates, false,
)
}
pub fn summarize_omena_query_cascade_aware_style_diagnostics_with_deep_analysis(
style_uri: &str,
source: &str,
candidates: &[OmenaQueryStyleHoverCandidateV0],
deep_analysis: bool,
) -> Vec<OmenaQueryStyleDiagnosticV0> {
let declarations_by_name = candidates
.iter()
.filter(|candidate| candidate.kind == "customPropertyDeclaration")
.map(|candidate| (candidate.name.as_str(), candidate.range))
.collect::<BTreeMap<_, _>>();
let dialect = omena_parser_dialect_for_style_path(style_uri);
let mut diagnostics =
summarize_static_css_custom_property_fixed_point_from_source(source, dialect)
.entries
.into_iter()
.filter(|entry| entry.guaranteed_invalid)
.filter_map(|entry| {
declarations_by_name
.get(entry.name.as_str())
.copied()
.map(|range| OmenaQueryStyleDiagnosticV0 {
code: "guaranteedInvalidCustomProperty",
severity: "warning",
provenance: vec![
"omena-transform-passes.custom-property-lfp",
"omena-query.cascade-aware-diagnostics",
],
range,
message: format!(
"CSS custom property '{}' resolves to the guaranteed-invalid value.",
entry.name
),
tags: Vec::new(),
create_custom_property: None,
})
})
.collect::<Vec<_>>();
diagnostics.extend(
summarize_query_cascade_checker_diagnostics_with_deep_analysis(
style_uri,
source,
deep_analysis,
),
);
diagnostics
}
pub fn summarize_omena_query_missing_keyframes_diagnostics(
style_uri: &str,
source: &str,
) -> Vec<OmenaQueryStyleDiagnosticV0> {
let dialect = omena_parser_dialect_for_style_path(style_uri);
let facts = collect_omena_query_omena_parser_style_facts_raw(source, dialect);
let declared_keyframes = facts
.animations
.iter()
.filter(|animation| animation.kind == ParsedAnimationFactKind::KeyframesDeclaration)
.map(|animation| animation.name.clone())
.collect::<BTreeSet<_>>();
let mut emitted = BTreeSet::new();
facts
.animations
.into_iter()
.filter(|animation| animation.kind == ParsedAnimationFactKind::AnimationNameReference)
.filter(|animation| !declared_keyframes.contains(animation.name.as_str()))
.filter_map(|animation| {
let start: u32 = animation.range.start().into();
let end: u32 = animation.range.end().into();
let byte_span = ParserByteSpanV0 {
start: start as usize,
end: end as usize,
};
if !emitted.insert((animation.name.clone(), byte_span.start, byte_span.end)) {
return None;
}
Some((animation, parser_range_for_byte_span(source, byte_span)))
})
.map(|(animation, range)| OmenaQueryStyleDiagnosticV0 {
code: "missingKeyframes",
severity: "warning",
provenance: vec![
"omena-parser.animation-facts",
"omena-query.style-diagnostics",
],
range,
message: format!("@keyframes '{}' not found in this file.", animation.name),
tags: Vec::new(),
create_custom_property: None,
})
.collect()
}
pub fn summarize_omena_query_missing_sass_symbol_diagnostics(
style_uri: &str,
source: &str,
) -> Vec<OmenaQueryStyleDiagnosticV0> {
let dialect = omena_parser_dialect_for_style_path(style_uri);
let facts = collect_omena_query_omena_parser_style_facts_raw(source, dialect);
let mut declarations = BTreeSet::<SassSymbolKey>::new();
let mut emitted = BTreeSet::new();
let mut diagnostics = Vec::new();
for symbol in facts.sass_symbols {
let key = sass_symbol_key(
symbol.symbol_kind,
symbol.namespace.clone(),
symbol.name.clone(),
);
if omena_query_sass_symbol_fact_kind_is_declaration(symbol.kind) {
declarations.insert(key);
continue;
}
if !omena_query_sass_symbol_fact_kind_is_reference(symbol.kind) {
continue;
}
if declarations.contains(&key) {
continue;
}
if is_omena_query_sass_builtin_symbol_reference_resolved(
&facts.sass_module_edges,
symbol.symbol_kind,
symbol.namespace.as_deref(),
symbol.name.as_str(),
) {
continue;
}
let start: u32 = symbol.range.start().into();
let end: u32 = symbol.range.end().into();
let byte_span = ParserByteSpanV0 {
start: start as usize,
end: end as usize,
};
if !emitted.insert((
symbol.symbol_kind,
symbol.namespace.clone(),
symbol.name.clone(),
byte_span.start,
byte_span.end,
)) {
continue;
}
diagnostics.push(OmenaQueryStyleDiagnosticV0 {
code: "missingSassSymbol",
severity: "warning",
provenance: vec![
"omena-parser.sass-symbol-facts",
"omena-query.style-diagnostics",
],
range: parser_range_for_byte_span(source, byte_span),
message: format!(
"{} not found in this file.",
format_query_sass_symbol_label(symbol.symbol_kind, symbol.name.as_str())
),
tags: Vec::new(),
create_custom_property: None,
});
}
diagnostics
}
pub fn summarize_omena_query_missing_extend_target_diagnostics(
style_uri: &str,
source: &str,
) -> Vec<OmenaQueryStyleDiagnosticV0> {
let dialect = omena_parser_dialect_for_style_path(style_uri);
if !matches!(
dialect,
OmenaParserStyleDialect::Scss | OmenaParserStyleDialect::Sass
) {
return Vec::new();
}
let facts = collect_omena_query_omena_parser_style_facts_raw(source, dialect);
if facts.extend_targets.is_empty() {
return Vec::new();
}
let mut declared_placeholders = BTreeSet::new();
let mut declared_classes = BTreeSet::new();
for selector in &facts.selectors {
match selector.kind {
ParsedSelectorFactKind::Placeholder => {
declared_placeholders.insert(selector.name.clone());
}
ParsedSelectorFactKind::Class => {
declared_classes.insert(selector.name.clone());
}
ParsedSelectorFactKind::Id => {}
}
}
let mut emitted = BTreeSet::new();
let mut diagnostics = Vec::new();
for target in &facts.extend_targets {
if target.optional {
continue;
}
let (resolved, label) = match target.kind {
ParsedExtendTargetFactKind::Placeholder => (
declared_placeholders.contains(&target.name),
format!("%{}", target.name),
),
ParsedExtendTargetFactKind::Class => (
declared_classes.contains(&target.name),
format!(".{}", target.name),
),
};
if resolved {
continue;
}
let start: u32 = target.range.start().into();
let end: u32 = target.range.end().into();
let byte_span = ParserByteSpanV0 {
start: start as usize,
end: end as usize,
};
if !emitted.insert((byte_span.start, byte_span.end)) {
continue;
}
diagnostics.push(OmenaQueryStyleDiagnosticV0 {
code: "missingExtendTarget",
severity: "error",
provenance: vec![
"omena-parser.extend-target-facts",
"omena-query.missing-extend-target-diagnostics",
],
range: parser_range_for_byte_span(source, byte_span),
message: format!(
"@extend target '{label}' does not exist in this file. dart-sass rejects this as a hard error."
),
tags: Vec::new(),
create_custom_property: None,
});
}
diagnostics
}
fn summarize_omena_query_missing_extend_target_diagnostics_for_workspace(
target_style_path: &str,
style_sources: &[OmenaQueryStyleSourceInputV0],
) -> Vec<OmenaQueryStyleDiagnosticV0> {
let Some(target) = style_sources
.iter()
.find(|source| source.style_path == target_style_path)
else {
return Vec::new();
};
let dialect = omena_parser_dialect_for_style_path(target_style_path);
if !matches!(
dialect,
OmenaParserStyleDialect::Scss | OmenaParserStyleDialect::Sass
) {
return Vec::new();
}
let target_facts =
collect_omena_query_omena_parser_style_facts_raw(target.style_source.as_str(), dialect);
if target_facts.extend_targets.is_empty() {
return Vec::new();
}
let style_source_refs = style_sources
.iter()
.map(|source| (source.style_path.as_str(), source.style_source.as_str()))
.collect::<Vec<_>>();
let style_fact_entries = collect_omena_query_style_fact_entries(style_source_refs.as_slice());
let resolution = summarize_sass_module_cross_file_resolution(&style_fact_entries, &[]);
let reachable_paths =
collect_sass_module_graph_reachable_style_paths(target_style_path, &resolution);
let mut declared_placeholders = BTreeSet::new();
let mut declared_classes = BTreeSet::new();
for source in style_sources {
if !reachable_paths.contains(source.style_path.as_str()) {
continue;
}
let facts = collect_omena_query_omena_parser_style_facts_raw(
source.style_source.as_str(),
omena_parser_dialect_for_style_path(source.style_path.as_str()),
);
for selector in facts.selectors {
match selector.kind {
ParsedSelectorFactKind::Placeholder => {
declared_placeholders.insert(selector.name);
}
ParsedSelectorFactKind::Class => {
declared_classes.insert(selector.name);
}
ParsedSelectorFactKind::Id => {}
}
}
}
let mut emitted = BTreeSet::new();
let mut diagnostics = Vec::new();
for extend_target in &target_facts.extend_targets {
if extend_target.optional {
continue;
}
let (resolved, label) = match extend_target.kind {
ParsedExtendTargetFactKind::Placeholder => (
declared_placeholders.contains(&extend_target.name),
format!("%{}", extend_target.name),
),
ParsedExtendTargetFactKind::Class => (
declared_classes.contains(&extend_target.name),
format!(".{}", extend_target.name),
),
};
if resolved {
continue;
}
let start: u32 = extend_target.range.start().into();
let end: u32 = extend_target.range.end().into();
let byte_span = ParserByteSpanV0 {
start: start as usize,
end: end as usize,
};
if !emitted.insert((byte_span.start, byte_span.end)) {
continue;
}
diagnostics.push(OmenaQueryStyleDiagnosticV0 {
code: "missingExtendTarget",
severity: "error",
provenance: vec![
"omena-parser.extend-target-facts",
"omena-query.missing-extend-target-diagnostics",
],
range: parser_range_for_byte_span(target.style_source.as_str(), byte_span),
message: format!(
"@extend target '{label}' does not exist in the visible Sass module graph. dart-sass rejects this as a hard error."
),
tags: Vec::new(),
create_custom_property: None,
});
}
diagnostics
}
fn collect_sass_module_graph_reachable_style_paths<'a>(
target_style_path: &'a str,
resolution: &'a OmenaQuerySassModuleCrossFileResolutionV0,
) -> BTreeSet<&'a str> {
let mut reachable = BTreeSet::new();
let mut stack = vec![target_style_path];
while let Some(current) = stack.pop() {
if !reachable.insert(current) {
continue;
}
for edge in resolution
.edges
.iter()
.filter(|edge| edge.from_style_path == current && edge.status == "resolved")
{
if let Some(next) = edge.resolved_style_path.as_deref() {
stack.push(next);
}
}
}
reachable
}
fn summarize_omena_query_replica_ensemble_inconsistency_diagnostics_for_workspace(
target_style_path: &str,
style_sources: &[OmenaQueryStyleSourceInputV0],
package_manifests: &[OmenaQueryStylePackageManifestV0],
) -> Vec<OmenaQueryStyleDiagnosticV0> {
let Some(target) = style_sources
.iter()
.find(|source| source.style_path == target_style_path)
else {
return Vec::new();
};
let style_source_refs = style_sources
.iter()
.map(|source| (source.style_path.as_str(), source.style_source.as_str()))
.collect::<Vec<_>>();
let style_fact_entries = collect_omena_query_style_fact_entries(style_source_refs.as_slice());
let resolution =
summarize_sass_module_cross_file_resolution(&style_fact_entries, package_manifests);
let reachable_paths =
collect_sass_module_graph_reachable_style_paths(target_style_path, &resolution);
if reachable_paths.len() < 2 {
return Vec::new();
}
let replicas = style_sources
.iter()
.filter(|source| reachable_paths.contains(source.style_path.as_str()))
.filter_map(|source| {
let sites = collect_query_replica_ensemble_site_outcomes(source.style_source.as_str());
if sites.is_empty() {
return None;
}
Some(ReplicaSnapshotV0 {
schema_version: REPLICA_ENSEMBLE_SCHEMA_VERSION_V0,
product: "omena-ensemble.replica-snapshot",
layer_marker: REPLICA_ENSEMBLE_LAYER_MARKER_V0,
feature_gate: REPLICA_ENSEMBLE_FEATURE_GATE_V0,
path: source.style_path.clone(),
sites,
})
})
.collect::<Vec<_>>();
if replicas.len() < 2 {
return Vec::new();
}
let module_graph = replica_ensemble_module_graph_from_resolution(
target_style_path,
&resolution,
&reachable_paths,
&replicas,
);
let report = build_cross_file_inconsistency_report(
target_style_path,
replicas.clone(),
&module_graph,
OutcomeMode::DefiniteOnly,
ReportOptionsV0::default(),
None,
);
let genuine_disagreement_pair_count = report
.top_disagreement_pairs
.iter()
.filter(|pair| pair.shared_site_count > 0 && pair.overlap_q < 1.0)
.count();
let recommendation = replica_ensemble_recommendation_name(report.recommendation);
let gate =
run_omena_query_checker_replica_ensemble_gate_v0(OmenaCheckerReplicaEnsembleInputV0 {
reports: vec![OmenaCheckerReplicaEnsembleReportInputV0 {
workspace_root: target_style_path.to_string(),
recommendation: recommendation.to_string(),
mean_q: report.distribution.mean_q,
variance_q: report.distribution.variance_q,
top_disagreement_pair_count: genuine_disagreement_pair_count,
}],
});
if !gate.enforcement_passed {
return Vec::new();
}
let whole_file_range = parser_range_for_byte_span(
target.style_source.as_str(),
ParserByteSpanV0 {
start: 0,
end: target.style_source.len(),
},
);
gate.evaluations
.into_iter()
.map(|evaluation| {
let mut provenance = vec![
"omena-query-checker-orchestrator.replica-ensemble-gate",
"omena-checker.replica-ensemble-rules",
"omena-ensemble.cross-file-inconsistency-report",
"omena-query.cross-file-replica-ensemble",
];
provenance.extend(evaluation.mechanism_products.iter().copied());
OmenaQueryStyleDiagnosticV0 {
code: "replicaEnsembleInconsistency",
severity: "hint",
provenance,
range: whole_file_range,
message: evaluation.message,
tags: Vec::new(),
create_custom_property: None,
}
})
.collect()
}
fn replica_ensemble_module_graph_from_resolution(
workspace_root: &str,
resolution: &OmenaQuerySassModuleCrossFileResolutionV0,
reachable_paths: &BTreeSet<&str>,
replicas: &[ReplicaSnapshotV0],
) -> ModuleGraphV0 {
let nodes = replicas
.iter()
.map(|replica| replica.path.clone())
.collect::<Vec<_>>();
let node_set = nodes.iter().map(String::as_str).collect::<BTreeSet<_>>();
let edges = resolution
.edges
.iter()
.filter(|edge| edge.status == "resolved")
.filter(|edge| reachable_paths.contains(edge.from_style_path.as_str()))
.filter_map(|edge| {
let to = edge.resolved_style_path.as_deref()?;
if node_set.contains(edge.from_style_path.as_str()) && node_set.contains(to) {
Some(ModuleGraphEdgeV0 {
schema_version: REPLICA_ENSEMBLE_SCHEMA_VERSION_V0,
product: "omena-ensemble.module-graph-edge",
layer_marker: REPLICA_ENSEMBLE_LAYER_MARKER_V0,
feature_gate: REPLICA_ENSEMBLE_FEATURE_GATE_V0,
from_module: edge.from_style_path.clone(),
to_module: to.to_string(),
edge_kind: "resolvedModuleEdge",
})
} else {
None
}
})
.collect::<Vec<_>>();
ModuleGraphV0 {
schema_version: REPLICA_ENSEMBLE_SCHEMA_VERSION_V0,
product: "omena-ensemble.module-graph",
layer_marker: REPLICA_ENSEMBLE_LAYER_MARKER_V0,
feature_gate: REPLICA_ENSEMBLE_FEATURE_GATE_V0,
workspace_root: workspace_root.to_string(),
nodes,
edges,
}
}
fn replica_ensemble_recommendation_name(recommendation: ReportRecommendation) -> &'static str {
match recommendation {
ReportRecommendation::NoActionNeeded => "noActionNeeded",
ReportRecommendation::InvestigateRsbBroken => "investigateRsbBroken",
ReportRecommendation::UndetectablePhase => "undetectablePhase",
}
}
pub fn summarize_omena_query_sass_import_deprecation_hints(
style_uri: &str,
source: &str,
) -> Vec<OmenaQueryStyleDiagnosticV0> {
let dialect = omena_parser_dialect_for_style_path(style_uri);
if !matches!(
dialect,
OmenaParserStyleDialect::Scss | OmenaParserStyleDialect::Sass
) {
return Vec::new();
}
let facts = collect_omena_query_omena_parser_style_facts_raw(source, dialect);
facts
.sass_module_edges
.into_iter()
.filter(|edge| edge.kind == ParsedSassModuleEdgeFactKind::Import)
.filter(|edge| !edge.media_qualified && !sass_import_is_plain_css(edge.source.as_str()))
.map(|edge| {
let start: u32 = edge.range.start().into();
let end: u32 = edge.range.end().into();
OmenaQueryStyleDiagnosticV0 {
code: "deprecatedSassImport",
severity: "information",
provenance: vec![
"omena-parser.sass-module-edges",
"omena-query.sass-import-deprecation-hints",
],
range: parser_range_for_byte_span(
source,
ParserByteSpanV0 {
start: start as usize,
end: end as usize,
},
),
message: "Sass @import is deprecated; prefer @use or @forward.".to_string(),
tags: vec![LSP_DIAGNOSTIC_TAG_DEPRECATED],
create_custom_property: None,
}
})
.collect()
}
fn sass_import_is_plain_css(source: &str) -> bool {
let trimmed = source.trim();
let lower = trimmed.to_ascii_lowercase();
if lower.starts_with("url(") {
return true;
}
if lower.starts_with("//") || lower.contains("://") {
return true;
}
if lower.ends_with(".css") {
return true;
}
false
}
pub fn summarize_omena_query_missing_sass_symbol_diagnostics_for_workspace(
target_style_path: &str,
style_sources: &[OmenaQueryStyleSourceInputV0],
package_manifests: &[OmenaQueryStylePackageManifestV0],
) -> Vec<OmenaQueryStyleDiagnosticV0> {
summarize_omena_query_missing_sass_symbol_diagnostics_for_workspace_with_sifs(
target_style_path,
style_sources,
package_manifests,
&[],
)
}
fn summarize_omena_query_missing_sass_symbol_diagnostics_for_workspace_with_sifs(
target_style_path: &str,
style_sources: &[OmenaQueryStyleSourceInputV0],
package_manifests: &[OmenaQueryStylePackageManifestV0],
external_sifs: &[OmenaQueryExternalSifInputV0],
) -> Vec<OmenaQueryStyleDiagnosticV0> {
let Some(target) = style_sources
.iter()
.find(|source| source.style_path == target_style_path)
else {
return Vec::new();
};
let style_source_refs = style_sources
.iter()
.map(|source| (source.style_path.as_str(), source.style_source.as_str()))
.collect::<Vec<_>>();
let style_fact_entries = collect_omena_query_style_fact_entries(style_source_refs.as_slice());
let facts_by_path = style_fact_entries
.iter()
.map(|entry| (entry.style_path.as_str(), &entry.facts))
.collect::<BTreeMap<_, _>>();
let resolution =
summarize_sass_module_cross_file_resolution(&style_fact_entries, package_manifests);
let visible_symbols = collect_visible_sass_symbol_keys(
target_style_path,
&facts_by_path,
&resolution,
external_sifs,
);
let facts = collect_omena_query_omena_parser_style_facts_raw(
target.style_source.as_str(),
omena_parser_dialect_for_style_path(target_style_path),
);
let mut emitted = BTreeSet::new();
let mut diagnostics = Vec::new();
for symbol in facts.sass_symbols {
if !omena_query_sass_symbol_fact_kind_is_reference(symbol.kind) {
continue;
}
let key = sass_symbol_key(
symbol.symbol_kind,
symbol.namespace.clone(),
symbol.name.clone(),
);
if visible_symbols.contains(&key) {
continue;
}
if is_omena_query_sass_builtin_symbol_reference_resolved(
&facts.sass_module_edges,
symbol.symbol_kind,
symbol.namespace.as_deref(),
symbol.name.as_str(),
) {
continue;
}
let start: u32 = symbol.range.start().into();
let end: u32 = symbol.range.end().into();
let byte_span = ParserByteSpanV0 {
start: start as usize,
end: end as usize,
};
if !emitted.insert((
symbol.symbol_kind,
symbol.namespace.clone(),
symbol.name.clone(),
byte_span.start,
byte_span.end,
)) {
continue;
}
diagnostics.push(OmenaQueryStyleDiagnosticV0 {
code: "missingSassSymbol",
severity: "warning",
provenance: vec![
"omena-parser.sass-symbol-facts",
"omena-query.graph-aware-sass-diagnostics",
],
range: parser_range_for_byte_span(target.style_source.as_str(), byte_span),
message: format!(
"{} not found in the visible Sass module graph.",
format_query_sass_symbol_label(symbol.symbol_kind, symbol.name.as_str())
),
tags: Vec::new(),
create_custom_property: None,
});
}
diagnostics
}
fn summarize_omena_query_sass_use_cycle_diagnostics_for_workspace(
target_style_path: &str,
style_sources: &[OmenaQueryStyleSourceInputV0],
package_manifests: &[OmenaQueryStylePackageManifestV0],
) -> Vec<OmenaQueryStyleDiagnosticV0> {
let Some(target) = style_sources
.iter()
.find(|source| source.style_path == target_style_path)
else {
return Vec::new();
};
let style_source_refs = style_sources
.iter()
.map(|source| (source.style_path.as_str(), source.style_source.as_str()))
.collect::<Vec<_>>();
let style_fact_entries = collect_omena_query_style_fact_entries(style_source_refs.as_slice());
let resolution =
summarize_sass_module_cross_file_resolution(&style_fact_entries, package_manifests);
if resolution.cycles.is_empty() {
return Vec::new();
}
let target_facts = collect_omena_query_omena_parser_style_facts_raw(
target.style_source.as_str(),
omena_parser_dialect_for_style_path(target_style_path),
);
let mut emitted = BTreeSet::new();
let mut diagnostics = Vec::new();
for cycle in &resolution.cycles {
if !cycle.path.iter().any(|node| node == target_style_path) {
continue;
}
let canonical_cycle = canonical_sass_module_cycle(&cycle.path);
let Some(next_module) = cycle
.path
.windows(2)
.find(|window| window[0] == target_style_path)
.map(|window| window[1].clone())
else {
continue;
};
let Some(loop_edge) = resolution.edges.iter().find(|edge| {
edge.from_style_path == target_style_path
&& edge.resolved_style_path.as_deref() == Some(next_module.as_str())
}) else {
continue;
};
let Some(fact) = target_facts.sass_module_edges.iter().find(|fact| {
fact.source == loop_edge.source
&& parsed_sass_module_edge_fact_kind_matches(fact.kind, loop_edge.edge_kind)
}) else {
continue;
};
let start: u32 = fact.range.start().into();
let end: u32 = fact.range.end().into();
let byte_span = ParserByteSpanV0 {
start: start as usize,
end: end as usize,
};
if !emitted.insert((byte_span.start, byte_span.end, canonical_cycle.clone())) {
continue;
}
diagnostics.push(OmenaQueryStyleDiagnosticV0 {
code: "sassUseCycle",
severity: "error",
provenance: vec![
"omena-query.sass-module-cross-file-resolution",
"omena-query.sass-use-cycle-diagnostics",
],
range: parser_range_for_byte_span(target.style_source.as_str(), byte_span),
message: format!(
"Sass module loop: {}. dart-sass rejects this as a hard error.",
render_sass_module_cycle_from(&canonical_cycle, target_style_path)
),
tags: Vec::new(),
create_custom_property: None,
});
}
diagnostics
}
fn summarize_omena_query_unresolved_sass_import_diagnostics_for_workspace(
target_style_path: &str,
style_sources: &[OmenaQueryStyleSourceInputV0],
package_manifests: &[OmenaQueryStylePackageManifestV0],
) -> Vec<OmenaQueryStyleDiagnosticV0> {
let Some(target) = style_sources
.iter()
.find(|source| source.style_path == target_style_path)
else {
return Vec::new();
};
let style_source_refs = style_sources
.iter()
.map(|source| (source.style_path.as_str(), source.style_source.as_str()))
.collect::<Vec<_>>();
let style_fact_entries = collect_omena_query_style_fact_entries(style_source_refs.as_slice());
let resolution =
summarize_sass_module_cross_file_resolution(&style_fact_entries, package_manifests);
let target_facts = collect_omena_query_omena_parser_style_facts_raw(
target.style_source.as_str(),
omena_parser_dialect_for_style_path(target_style_path),
);
let mut emitted = BTreeSet::new();
let mut diagnostics = Vec::new();
for edge in resolution.edges.iter().filter(|edge| {
edge.from_style_path == target_style_path
&& edge.status == "unresolved"
&& sass_module_source_is_workspace_local(edge.source.as_str())
}) {
let Some(fact) = target_facts.sass_module_edges.iter().find(|fact| {
fact.source == edge.source
&& parsed_sass_module_edge_fact_kind_matches(fact.kind, edge.edge_kind)
}) else {
continue;
};
let start: u32 = fact.range.start().into();
let end: u32 = fact.range.end().into();
let byte_span = ParserByteSpanV0 {
start: start as usize,
end: end as usize,
};
if !emitted.insert((byte_span.start, byte_span.end)) {
continue;
}
diagnostics.push(OmenaQueryStyleDiagnosticV0 {
code: "missingModule",
severity: "error",
provenance: vec![
"omena-query.sass-module-cross-file-resolution",
"omena-query.unresolved-sass-import-diagnostics",
],
range: parser_range_for_byte_span(target.style_source.as_str(), byte_span),
message: format!(
"Cannot resolve Sass module '{}'. dart-sass rejects this as a hard error.",
edge.source
),
tags: Vec::new(),
create_custom_property: None,
});
}
diagnostics
}
fn sass_module_source_is_workspace_local(source: &str) -> bool {
let trimmed = source.trim();
trimmed.starts_with("./") || trimmed.starts_with("../") || trimmed.starts_with('/')
}
fn parsed_sass_module_edge_fact_kind_matches(
fact_kind: ParsedSassModuleEdgeFactKind,
edge_kind: &str,
) -> bool {
matches!(
(fact_kind, edge_kind),
(ParsedSassModuleEdgeFactKind::Use, "sassUse")
| (ParsedSassModuleEdgeFactKind::Forward, "sassForward")
| (ParsedSassModuleEdgeFactKind::Import, "sassImport")
)
}
fn canonical_sass_module_cycle(path: &[String]) -> Vec<String> {
let ring: &[String] = match path.split_last() {
Some((last, head)) if Some(last) == path.first() && !head.is_empty() => head,
_ => path,
};
if ring.is_empty() {
return path.to_vec();
}
let len = ring.len();
(0..len)
.map(|offset| {
(0..len)
.map(|index| ring[(offset + index) % len].clone())
.collect::<Vec<_>>()
})
.min()
.unwrap_or_else(|| ring.to_vec())
}
fn render_sass_module_cycle_from(canonical_cycle: &[String], start: &str) -> String {
let len = canonical_cycle.len();
let begin = canonical_cycle
.iter()
.position(|node| node == start)
.unwrap_or(0);
let mut ordered = (0..len)
.map(|index| canonical_cycle[(begin + index) % len].clone())
.collect::<Vec<_>>();
ordered.push(canonical_cycle[begin].clone());
ordered.join(" -> ")
}
pub fn summarize_omena_query_style_diagnostics_for_file(
style_uri: &str,
source: &str,
candidates: &[OmenaQueryStyleHoverCandidateV0],
) -> OmenaQueryStyleDiagnosticsForFileV0 {
summarize_omena_query_style_diagnostics_for_file_with_deep_analysis(
style_uri, source, candidates, false,
)
}
pub fn summarize_omena_query_style_diagnostics_for_file_with_deep_analysis(
style_uri: &str,
source: &str,
candidates: &[OmenaQueryStyleHoverCandidateV0],
deep_analysis: bool,
) -> OmenaQueryStyleDiagnosticsForFileV0 {
let mut diagnostics =
summarize_omena_query_missing_custom_property_diagnostics(style_uri, source, candidates);
diagnostics.extend(
summarize_omena_query_cascade_aware_style_diagnostics_with_deep_analysis(
style_uri,
source,
candidates,
deep_analysis,
),
);
diagnostics.extend(summarize_omena_query_missing_keyframes_diagnostics(
style_uri, source,
));
diagnostics.extend(summarize_omena_query_sass_import_deprecation_hints(
style_uri, source,
));
diagnostics.extend(summarize_omena_query_missing_sass_symbol_diagnostics(
style_uri, source,
));
diagnostics.extend(summarize_omena_query_missing_extend_target_diagnostics(
style_uri, source,
));
apply_omena_query_checker_product_gate_to_style_diagnostics(&mut diagnostics);
let mut summary = OmenaQueryStyleDiagnosticsForFileV0 {
schema_version: "0",
product: "omena-query.diagnostics-for-file",
file_uri: style_uri.to_string(),
file_kind: "style",
diagnostic_count: diagnostics.len(),
diagnostics,
ready_surfaces: vec![
"missingCustomPropertyDiagnostics",
"cascadeAwareDiagnostics",
"missingKeyframesDiagnostics",
"sassImportDeprecationHints",
"missingSassSymbolDiagnostics",
"missingExtendTargetDiagnostics",
"checkerProductDiagnosticGate",
],
};
apply_omena_query_style_diagnostic_suppressions(source, &mut summary);
summary
}
pub fn summarize_omena_query_style_diagnostics_for_file_with_local_composes(
style_uri: &str,
source: &str,
candidates: &[OmenaQueryStyleHoverCandidateV0],
) -> OmenaQueryStyleDiagnosticsForFileV0 {
summarize_omena_query_style_diagnostics_for_file_with_local_composes_and_deep_analysis(
style_uri, source, candidates, false,
)
}
pub fn summarize_omena_query_style_diagnostics_for_file_with_local_composes_and_deep_analysis(
style_uri: &str,
source: &str,
candidates: &[OmenaQueryStyleHoverCandidateV0],
deep_analysis: bool,
) -> OmenaQueryStyleDiagnosticsForFileV0 {
let mut summary = summarize_omena_query_style_diagnostics_for_file_with_deep_analysis(
style_uri,
source,
candidates,
deep_analysis,
);
let mut local_composes =
summarize_omena_query_css_modules_local_composes_style_diagnostics(style_uri, source);
apply_omena_query_checker_product_gate_to_style_diagnostics(&mut local_composes);
if !local_composes.is_empty() {
summary.diagnostics.extend(local_composes);
push_omena_query_ready_surface(
&mut summary.ready_surfaces,
"cssModulesComposesResolutionDiagnostics",
);
apply_omena_query_style_diagnostic_suppressions(source, &mut summary);
summary.diagnostic_count = summary.diagnostics.len();
}
summary
}
pub fn summarize_omena_query_style_diagnostics_for_workspace_file(
target_style_path: &str,
style_sources: &[OmenaQueryStyleSourceInputV0],
source_documents: &[OmenaQuerySourceDocumentInputV0],
package_manifests: &[OmenaQueryStylePackageManifestV0],
classname_transform: Option<&str>,
) -> Option<OmenaQueryStyleDiagnosticsForFileV0> {
summarize_omena_query_style_diagnostics_for_workspace_file_with_external_mode(
target_style_path,
style_sources,
source_documents,
package_manifests,
classname_transform,
OmenaQueryExternalModuleModeV0::Ignored,
)
}
pub fn summarize_omena_query_style_diagnostics_for_workspace_file_with_external_mode(
target_style_path: &str,
style_sources: &[OmenaQueryStyleSourceInputV0],
source_documents: &[OmenaQuerySourceDocumentInputV0],
package_manifests: &[OmenaQueryStylePackageManifestV0],
classname_transform: Option<&str>,
external_mode: OmenaQueryExternalModuleModeV0,
) -> Option<OmenaQueryStyleDiagnosticsForFileV0> {
summarize_omena_query_style_diagnostics_for_workspace_file_with_external_mode_and_sifs(
target_style_path,
style_sources,
source_documents,
package_manifests,
classname_transform,
external_mode,
&[],
)
}
pub fn summarize_omena_query_style_diagnostics_for_workspace_file_with_external_mode_and_sifs(
target_style_path: &str,
style_sources: &[OmenaQueryStyleSourceInputV0],
source_documents: &[OmenaQuerySourceDocumentInputV0],
package_manifests: &[OmenaQueryStylePackageManifestV0],
classname_transform: Option<&str>,
external_mode: OmenaQueryExternalModuleModeV0,
external_sifs: &[OmenaQueryExternalSifInputV0],
) -> Option<OmenaQueryStyleDiagnosticsForFileV0> {
summarize_omena_query_style_diagnostics_for_workspace_file_with_external_mode_and_sifs_and_resolution_inputs(
target_style_path,
style_sources,
source_documents,
package_manifests,
classname_transform,
external_mode,
external_sifs,
&OmenaQueryStyleResolutionInputsV0 {
package_manifests: package_manifests.to_vec(),
..Default::default()
},
)
}
#[allow(clippy::too_many_arguments)]
pub fn summarize_omena_query_style_diagnostics_for_workspace_file_with_external_mode_and_sifs_and_resolution_inputs(
target_style_path: &str,
style_sources: &[OmenaQueryStyleSourceInputV0],
source_documents: &[OmenaQuerySourceDocumentInputV0],
package_manifests: &[OmenaQueryStylePackageManifestV0],
classname_transform: Option<&str>,
external_mode: OmenaQueryExternalModuleModeV0,
external_sifs: &[OmenaQueryExternalSifInputV0],
resolution_inputs: &OmenaQueryStyleResolutionInputsV0,
) -> Option<OmenaQueryStyleDiagnosticsForFileV0> {
let target = style_sources
.iter()
.find(|source| source.style_path == target_style_path)?;
let candidates =
summarize_omena_query_style_hover_candidates(target_style_path, &target.style_source)?;
let mut summary = summarize_omena_query_style_diagnostics_for_file(
target_style_path,
&target.style_source,
candidates.candidates.as_slice(),
);
summary
.diagnostics
.retain(|diagnostic| diagnostic.code != "missingSassSymbol");
summary
.diagnostics
.retain(|diagnostic| diagnostic.code != "missingExtendTarget");
summary.diagnostics.extend(
summarize_omena_query_missing_extend_target_diagnostics_for_workspace(
target_style_path,
style_sources,
),
);
summary.diagnostics.extend(
summarize_omena_query_missing_sass_symbol_diagnostics_for_workspace_with_sifs(
target_style_path,
style_sources,
package_manifests,
external_sifs,
),
);
summary.diagnostics.extend(
summarize_omena_query_css_modules_resolution_style_diagnostics(
target_style_path,
&target.style_source,
style_sources,
package_manifests,
),
);
summary.diagnostics.extend(
summarize_omena_query_sass_use_cycle_diagnostics_for_workspace(
target_style_path,
style_sources,
package_manifests,
),
);
summary.diagnostics.extend(
summarize_omena_query_unresolved_sass_import_diagnostics_for_workspace(
target_style_path,
style_sources,
package_manifests,
),
);
summary.diagnostics.extend(
summarize_omena_query_unused_selector_style_diagnostics_with_path_mappings(
target_style_path,
&target.style_source,
style_sources,
source_documents,
package_manifests,
classname_transform,
resolution_inputs.bundler_path_mappings.as_slice(),
resolution_inputs.tsconfig_path_mappings.as_slice(),
),
);
summary.diagnostics.extend(
summarize_omena_query_replica_ensemble_inconsistency_diagnostics_for_workspace(
target_style_path,
style_sources,
package_manifests,
),
);
summary.diagnostic_count = summary.diagnostics.len();
push_omena_query_ready_surface(
&mut summary.ready_surfaces,
"cssModulesComposesResolutionDiagnostics",
);
push_omena_query_ready_surface(
&mut summary.ready_surfaces,
"cssModulesValueResolutionDiagnostics",
);
push_omena_query_ready_surface(&mut summary.ready_surfaces, "unusedSelectorDiagnostics");
push_omena_query_ready_surface(&mut summary.ready_surfaces, "sassUseCycleDiagnostics");
push_omena_query_ready_surface(
&mut summary.ready_surfaces,
"unresolvedSassImportDiagnostics",
);
push_omena_query_ready_surface(
&mut summary.ready_surfaces,
"missingExtendTargetDiagnostics",
);
push_omena_query_ready_surface(
&mut summary.ready_surfaces,
"graphAwareSassSymbolDiagnostics",
);
push_omena_query_ready_surface(
&mut summary.ready_surfaces,
"crossFileReplicaEnsembleDiagnostics",
);
if external_mode == OmenaQueryExternalModuleModeV0::Sif {
let strictness = parse_omena_query_style_strictness_level(&target.style_source);
let top_any_external_symbol_ranges =
collect_omena_query_external_top_any_sass_symbol_ranges(
target_style_path,
style_sources,
package_manifests,
external_sifs,
);
if strictness.suppresses_top_any_external_symbols() {
summary.diagnostics.retain(|diagnostic| {
diagnostic.code != "missingSassSymbol"
|| !top_any_external_symbol_ranges.contains(&diagnostic.range)
});
} else {
for diagnostic in summary.diagnostics.iter_mut() {
if diagnostic.code == "missingSassSymbol"
&& top_any_external_symbol_ranges.contains(&diagnostic.range)
{
diagnostic.severity = "error";
}
}
}
if strictness.emits_external_boundary_diagnostics() {
summary
.diagnostics
.extend(summarize_omena_query_external_sif_boundary_diagnostics(
target_style_path,
style_sources,
package_manifests,
external_sifs,
strictness,
));
}
push_omena_query_ready_surface(
&mut summary.ready_surfaces,
"externalSifBoundaryDiagnostics",
);
push_omena_query_ready_surface(&mut summary.ready_surfaces, "strictnessSigilGating");
}
apply_omena_query_checker_product_gate_to_style_diagnostics(&mut summary.diagnostics);
push_omena_query_ready_surface(&mut summary.ready_surfaces, "checkerProductDiagnosticGate");
apply_omena_query_style_diagnostic_suppressions(&target.style_source, &mut summary);
summary.diagnostic_count = summary.diagnostics.len();
Some(summary)
}
fn collect_omena_query_external_top_any_sass_symbol_ranges(
target_style_path: &str,
style_sources: &[OmenaQueryStyleSourceInputV0],
package_manifests: &[OmenaQueryStylePackageManifestV0],
external_sifs: &[OmenaQueryExternalSifInputV0],
) -> BTreeSet<ParserRangeV0> {
let Some(target) = style_sources
.iter()
.find(|source| source.style_path == target_style_path)
else {
return BTreeSet::new();
};
let style_source_refs = style_sources
.iter()
.map(|source| (source.style_path.as_str(), source.style_source.as_str()))
.collect::<Vec<_>>();
let style_fact_entries = collect_omena_query_style_fact_entries(style_source_refs.as_slice());
let resolution =
summarize_sass_module_cross_file_resolution(&style_fact_entries, package_manifests);
let external_sources = resolution
.edges
.iter()
.filter(|edge| edge.from_style_path == target_style_path)
.filter(|edge| edge.status == "external")
.map(|edge| edge.source.as_str())
.collect::<BTreeSet<_>>();
if external_sources.is_empty() {
return BTreeSet::new();
}
let facts = collect_omena_query_omena_parser_style_facts_raw(
target.style_source.as_str(),
omena_parser_dialect_for_style_path(target_style_path),
);
let top_any_namespaces = facts
.sass_module_edges
.iter()
.filter(|edge| external_sources.contains(edge.source.as_str()))
.filter(|edge| {
let sif = find_omena_query_external_sif(edge.source.as_str(), external_sifs);
classify_external_boundary_state(edge, sif, &facts, external_sifs).top
== OmenaResolverBoundaryTopV0::TopAny
})
.filter_map(|edge| match edge.kind {
ParsedSassModuleEdgeFactKind::Use
if edge.namespace_kind == Some("default")
|| edge.namespace_kind == Some("alias") =>
{
edge.namespace.clone().map(Some)
}
ParsedSassModuleEdgeFactKind::Use if edge.namespace_kind == Some("wildcard") => {
Some(None)
}
ParsedSassModuleEdgeFactKind::Import => Some(None),
_ => None,
})
.collect::<BTreeSet<_>>();
if top_any_namespaces.is_empty() {
return BTreeSet::new();
}
facts
.sass_symbols
.into_iter()
.filter(|symbol| omena_query_sass_symbol_fact_kind_is_reference(symbol.kind))
.filter(|symbol| top_any_namespaces.contains(&symbol.namespace))
.map(|symbol| {
let start: u32 = symbol.range.start().into();
let end: u32 = symbol.range.end().into();
parser_range_for_byte_span(
target.style_source.as_str(),
ParserByteSpanV0 {
start: start as usize,
end: end as usize,
},
)
})
.collect()
}
fn classify_external_boundary_state(
edge: &ParsedSassModuleEdgeFact,
sif: Option<&OmenaQueryExternalSifInputV0>,
target_facts: &omena_parser::ParsedStyleFacts,
external_sifs: &[OmenaQueryExternalSifInputV0],
) -> OmenaResolverBoundaryStateV0 {
let Some(sif) = sif else {
return OmenaResolverBoundaryStateV0::missing(
None,
"SIF mode requires a local SIF artifact for this external Sass module",
);
};
let canonical_url = OmenaResolverCanonicalUrlV0 {
url: edge.source.clone(),
};
if let Some(dependency) = sif.sif.dependencies.iter().find(|dependency| {
find_omena_query_external_sif(dependency.canonical_url.as_str(), external_sifs)
.map(|dependency_sif| {
dependency_sif.sif.fingerprints.interface_hash != dependency.interface_hash
})
.unwrap_or(false)
}) {
return OmenaResolverBoundaryStateV0::stale(
canonical_url,
format!(
"external SIF dependency '{}' interface hash drifted from the lockfile-recorded hash",
dependency.canonical_url
),
);
}
let exported = collect_sif_exported_sass_symbol_keys(&sif.sif);
let mut referenced = 0usize;
let mut covered = 0usize;
for symbol in &target_facts.sass_symbols {
if !omena_query_sass_symbol_fact_kind_is_reference(symbol.kind) {
continue;
}
if !sass_symbol_reference_belongs_to_edge(edge, symbol.namespace.as_deref()) {
continue;
}
referenced += 1;
if exported.contains(&(symbol.symbol_kind, fold_sass_symbol_name(&symbol.name))) {
covered += 1;
}
}
if referenced > 0 && covered < referenced {
return OmenaResolverBoundaryStateV0::partial(format!(
"external SIF for '{}' exports only {}/{} referenced symbol(s)",
edge.source, covered, referenced
));
}
OmenaResolverBoundaryStateV0::resolved(canonical_url)
}
fn sass_symbol_reference_belongs_to_edge(
edge: &ParsedSassModuleEdgeFact,
reference_namespace: Option<&str>,
) -> bool {
match edge.kind {
ParsedSassModuleEdgeFactKind::Use
if edge.namespace_kind == Some("default") || edge.namespace_kind == Some("alias") =>
{
edge.namespace.as_deref() == reference_namespace
}
ParsedSassModuleEdgeFactKind::Use if edge.namespace_kind == Some("wildcard") => {
reference_namespace.is_none()
}
ParsedSassModuleEdgeFactKind::Import | ParsedSassModuleEdgeFactKind::Forward => {
reference_namespace.is_none()
}
_ => false,
}
}
fn summarize_omena_query_external_sif_boundary_diagnostics(
target_style_path: &str,
style_sources: &[OmenaQueryStyleSourceInputV0],
package_manifests: &[OmenaQueryStylePackageManifestV0],
external_sifs: &[OmenaQueryExternalSifInputV0],
strictness: OmenaStrictnessLevelV0,
) -> Vec<OmenaQueryStyleDiagnosticV0> {
let Some(target) = style_sources
.iter()
.find(|source| source.style_path == target_style_path)
else {
return Vec::new();
};
let style_source_refs = style_sources
.iter()
.map(|source| (source.style_path.as_str(), source.style_source.as_str()))
.collect::<Vec<_>>();
let style_fact_entries = collect_omena_query_style_fact_entries(style_source_refs.as_slice());
let resolution =
summarize_sass_module_cross_file_resolution(&style_fact_entries, package_manifests);
let external_sources = resolution
.edges
.iter()
.filter(|edge| edge.from_style_path == target_style_path)
.filter(|edge| edge.status == "external")
.map(|edge| edge.source.as_str())
.collect::<BTreeSet<_>>();
let unresolved_sources = resolution
.edges
.iter()
.filter(|edge| edge.from_style_path == target_style_path)
.filter(|edge| edge.status == "unresolved")
.filter(|edge| !sass_module_source_is_workspace_local(edge.source.as_str()))
.map(|edge| edge.source.as_str())
.collect::<BTreeSet<_>>();
if external_sources.is_empty() && unresolved_sources.is_empty() {
return Vec::new();
}
let facts = collect_omena_query_omena_parser_style_facts_raw(
target.style_source.as_str(),
omena_parser_dialect_for_style_path(target_style_path),
);
let mut emitted = BTreeSet::new();
let mut diagnostics = Vec::new();
for edge in &facts.sass_module_edges {
let is_external = external_sources.contains(edge.source.as_str());
let is_unresolved = unresolved_sources.contains(edge.source.as_str());
if !is_external && !is_unresolved {
continue;
}
if !emitted.insert((edge.kind, edge.source.clone())) {
continue;
}
let state = if is_unresolved {
omena_resolver_boundary_state_for_unresolved_reference_v0(edge.source.as_str())
} else {
let sif = find_omena_query_external_sif(edge.source.as_str(), external_sifs);
classify_external_boundary_state(edge, sif, &facts, external_sifs)
};
let (code, default_severity) = match state.state {
OmenaResolverBoundaryStateKindV0::Resolved => continue,
OmenaResolverBoundaryStateKindV0::Stale => ("staleExternalSif", "warning"),
OmenaResolverBoundaryStateKindV0::Partial => ("partialExternalSif", "information"),
OmenaResolverBoundaryStateKindV0::Missing => ("missingExternalSif", "warning"),
OmenaResolverBoundaryStateKindV0::Unresolved => {
("unresolvedExternalReference", "warning")
}
};
let severity = strictness.boundary_severity(default_severity);
let start: u32 = edge.range.start().into();
let end: u32 = edge.range.end().into();
diagnostics.push(OmenaQueryStyleDiagnosticV0 {
code,
severity,
provenance: vec![
"omena-resolver.boundary-state",
"omena-query.external-sif-boundary-diagnostics",
],
range: parser_range_for_byte_span(
target.style_source.as_str(),
ParserByteSpanV0 {
start: start as usize,
end: end as usize,
},
),
message: format!(
"External Sass module '{}' is {} ({}); {}",
edge.source,
state.state_name,
state.top_name,
external_boundary_remediation_hint(state.state)
),
tags: Vec::new(),
create_custom_property: None,
});
}
diagnostics
}
fn external_boundary_remediation_hint(state: OmenaResolverBoundaryStateKindV0) -> &'static str {
match state {
OmenaResolverBoundaryStateKindV0::Missing => {
"generate or provide a SIF artifact, or use --external ignored."
}
OmenaResolverBoundaryStateKindV0::Stale => {
"regenerate the SIF/lockfile so its dependency interface hashes match."
}
OmenaResolverBoundaryStateKindV0::Partial => {
"some referenced symbols are absent from the SIF interface; regenerate the SIF or fix the reference."
}
OmenaResolverBoundaryStateKindV0::Unresolved => {
"the resolver cannot canonicalize this reference; fix the specifier or add it to the workspace."
}
OmenaResolverBoundaryStateKindV0::Resolved => "",
}
}
type SassSymbolKey = (&'static str, Option<String>, String);
fn fold_sass_symbol_name(name: &str) -> String {
name.replace('_', "-")
}
fn sass_symbol_key(
symbol_kind: &'static str,
namespace: Option<String>,
name: String,
) -> SassSymbolKey {
let folded = fold_sass_symbol_name(&name);
(symbol_kind, namespace, folded)
}
fn collect_visible_sass_symbol_keys(
target_style_path: &str,
facts_by_path: &BTreeMap<&str, &OmenaQueryOmenaParserStyleFactsV0>,
resolution: &OmenaQuerySassModuleCrossFileResolutionV0,
external_sifs: &[OmenaQueryExternalSifInputV0],
) -> BTreeSet<SassSymbolKey> {
let mut visible = BTreeSet::new();
if let Some(facts) = facts_by_path.get(target_style_path) {
visible.extend(
own_sass_symbol_declaration_keys(facts)
.into_iter()
.map(|(symbol_kind, name)| sass_symbol_key(symbol_kind, None, name)),
);
}
for edge in resolution
.edges
.iter()
.filter(|edge| edge.from_style_path == target_style_path)
{
let exported = if let Some(module_name) = sass_builtin_module_name(edge.source.as_str()) {
builtin_sass_symbol_exports(module_name)
} else if edge.status == "resolved" {
let mut visiting = BTreeSet::new();
edge.resolved_style_path
.as_deref()
.map(|path| {
collect_exported_sass_symbol_keys(
path,
facts_by_path,
resolution,
external_sifs,
&mut visiting,
)
})
.unwrap_or_default()
} else if edge.status == "external" {
find_omena_query_external_sif(edge.source.as_str(), external_sifs)
.map(|sif| collect_sif_exported_sass_symbol_keys(&sif.sif))
.unwrap_or_default()
} else {
BTreeSet::new()
};
match edge.edge_kind {
"sassUse"
if edge.namespace_kind == Some("default")
|| edge.namespace_kind == Some("alias") =>
{
if let Some(namespace) = edge.namespace.clone() {
visible.extend(exported.into_iter().map(|(symbol_kind, name)| {
sass_symbol_key(symbol_kind, Some(namespace.clone()), name)
}));
}
}
"sassUse" if edge.namespace_kind == Some("wildcard") => {
visible.extend(
exported
.into_iter()
.map(|(symbol_kind, name)| sass_symbol_key(symbol_kind, None, name)),
);
}
"sassImport" => {
visible.extend(
exported
.into_iter()
.map(|(symbol_kind, name)| sass_symbol_key(symbol_kind, None, name)),
);
}
_ => {}
}
}
visible
}
fn collect_exported_sass_symbol_keys(
style_path: &str,
facts_by_path: &BTreeMap<&str, &OmenaQueryOmenaParserStyleFactsV0>,
resolution: &OmenaQuerySassModuleCrossFileResolutionV0,
external_sifs: &[OmenaQueryExternalSifInputV0],
visiting: &mut BTreeSet<String>,
) -> BTreeSet<(&'static str, String)> {
if !visiting.insert(style_path.to_string()) {
return BTreeSet::new();
}
let mut exported = facts_by_path
.get(style_path)
.map(|facts| own_sass_symbol_declaration_keys(facts))
.unwrap_or_default();
for edge in resolution
.edges
.iter()
.filter(|edge| edge.from_style_path == style_path)
.filter(|edge| edge.edge_kind == "sassForward" || edge.edge_kind == "sassImport")
{
let module_exports =
if let Some(module_name) = sass_builtin_module_name(edge.source.as_str()) {
builtin_sass_symbol_exports(module_name)
} else if edge.status == "resolved" {
edge.resolved_style_path
.as_deref()
.map(|path| {
collect_exported_sass_symbol_keys(
path,
facts_by_path,
resolution,
external_sifs,
visiting,
)
})
.unwrap_or_default()
} else if edge.status == "external" {
find_omena_query_external_sif(edge.source.as_str(), external_sifs)
.map(|sif| collect_sif_exported_sass_symbol_keys(&sif.sif))
.unwrap_or_default()
} else {
BTreeSet::new()
};
for (symbol_kind, name) in module_exports {
if !sass_forward_visibility_allows(edge, symbol_kind, name.as_str()) {
continue;
}
let exported_name = if edge.edge_kind == "sassForward" {
apply_sass_forward_prefix(edge.forward_prefix.as_deref(), name.as_str())
} else {
name
};
exported.insert((symbol_kind, exported_name));
}
}
visiting.remove(style_path);
exported
}
fn own_sass_symbol_declaration_keys(
facts: &OmenaQueryOmenaParserStyleFactsV0,
) -> BTreeSet<(&'static str, String)> {
facts
.sass_symbol_facts
.iter()
.filter(|fact| is_omena_query_sass_symbol_declaration_kind(fact.kind))
.map(|fact| (fact.symbol_kind, fact.name.clone()))
.collect()
}
fn collect_sif_exported_sass_symbol_keys(
sif: &omena_sif::OmenaSifV1,
) -> BTreeSet<(&'static str, String)> {
let mut exported = BTreeSet::new();
exported.extend(sif.exports.variables.iter().map(|variable| {
(
"variable",
variable.name.trim_start_matches('$').to_string(),
)
}));
exported.extend(
sif.exports
.mixins
.iter()
.map(|mixin| ("mixin", mixin.name.clone())),
);
exported.extend(
sif.exports
.functions
.iter()
.map(|function| ("function", function.name.clone())),
);
exported
}
fn find_omena_query_external_sif<'a>(
canonical_url: &str,
external_sifs: &'a [OmenaQueryExternalSifInputV0],
) -> Option<&'a OmenaQueryExternalSifInputV0> {
external_sifs.iter().find(|input| {
input.canonical_url == canonical_url || input.sif.canonical_url == canonical_url
})
}
fn sass_forward_visibility_allows(
edge: &OmenaQuerySassModuleEdgeResolutionV0,
symbol_kind: &'static str,
name: &str,
) -> bool {
let prefixed = apply_sass_forward_prefix(edge.forward_prefix.as_deref(), name);
let matches_filter = |filter_name: &String| {
filter_name == name
|| filter_name == prefixed.as_str()
|| filter_name.trim_start_matches('$') == name
|| filter_name.trim_start_matches('$') == prefixed.as_str()
|| (symbol_kind != "variable" && filter_name.trim_start_matches('@') == name)
};
match edge.visibility_filter_kind {
Some("show") => edge.visibility_filter_names.iter().any(matches_filter),
Some("hide") => !edge.visibility_filter_names.iter().any(matches_filter),
_ => true,
}
}
fn apply_sass_forward_prefix(prefix: Option<&str>, name: &str) -> String {
match prefix {
Some(prefix) if prefix.contains('*') => prefix.replace('*', name),
Some(prefix) => format!("{prefix}{name}"),
None => name.to_string(),
}
}
fn is_omena_query_sass_builtin_symbol_reference_resolved(
edges: &[omena_parser::ParsedSassModuleEdgeFact],
symbol_kind: &'static str,
namespace: Option<&str>,
name: &str,
) -> bool {
edges
.iter()
.filter(|edge| edge.kind == ParsedSassModuleEdgeFactKind::Use)
.filter_map(|edge| {
sass_builtin_module_name(edge.source.as_str()).map(|module| (edge, module))
})
.any(|(edge, module)| {
let namespace_matches =
match (namespace, edge.namespace_kind, edge.namespace.as_deref()) {
(Some(reference_namespace), Some("default" | "alias"), Some(use_namespace)) => {
reference_namespace == use_namespace
}
(None, Some("wildcard"), _) => true,
_ => false,
};
namespace_matches && sass_builtin_module_has_symbol(module, symbol_kind, name)
})
}
fn sass_builtin_module_name(source: &str) -> Option<&str> {
source.strip_prefix("sass:")
}
fn builtin_sass_symbol_exports(module: &str) -> BTreeSet<(&'static str, String)> {
let mut exports = BTreeSet::new();
for name in sass_builtin_module_function_names(module) {
exports.insert(("function", (*name).to_string()));
}
for name in sass_builtin_module_mixin_names(module) {
exports.insert(("mixin", (*name).to_string()));
}
for name in sass_builtin_module_variable_names(module) {
exports.insert(("variable", (*name).to_string()));
}
exports
}
fn sass_builtin_module_has_symbol(module: &str, symbol_kind: &'static str, name: &str) -> bool {
match symbol_kind {
"function" => sass_builtin_module_function_names(module).contains(&name),
"mixin" => sass_builtin_module_mixin_names(module).contains(&name),
"variable" => sass_builtin_module_variable_names(module).contains(&name),
_ => false,
}
}
fn sass_builtin_module_function_names(module: &str) -> &'static [&'static str] {
match module {
"color" => &[
"adjust",
"alpha",
"blue",
"channel",
"change",
"complement",
"desaturate",
"fade-in",
"fade-out",
"grayscale",
"green",
"hsl",
"hsla",
"hue",
"ie-hex-str",
"invert",
"is-legacy",
"is-missing",
"is-powerless",
"lighten",
"lightness",
"mix",
"opacify",
"opacity",
"red",
"same",
"saturate",
"saturation",
"scale",
"space",
"to-gamut",
"to-space",
"transparentize",
],
"math" => &[
"abs",
"acos",
"asin",
"atan",
"atan2",
"ceil",
"clamp",
"compatible",
"cos",
"div",
"floor",
"hypot",
"is-unitless",
"log",
"max",
"min",
"percentage",
"pow",
"random",
"round",
"sin",
"sqrt",
"tan",
"unit",
],
"list" => &[
"append",
"index",
"is-bracketed",
"join",
"length",
"separator",
"set-nth",
"slash",
"nth",
"zip",
],
"map" => &[
"deep-merge",
"deep-remove",
"get",
"has-key",
"keys",
"merge",
"remove",
"set",
"values",
],
"string" => &[
"index",
"insert",
"length",
"quote",
"slice",
"split",
"to-lower-case",
"to-upper-case",
"unique-id",
"unquote",
],
"selector" => &[
"append",
"extend",
"is-superselector",
"nest",
"parse",
"replace",
"simple-selectors",
"unify",
],
"meta" => &[
"accepts-content",
"calc-args",
"calc-name",
"call",
"content-exists",
"feature-exists",
"function-exists",
"get-function",
"get-mixin",
"global-variable-exists",
"inspect",
"keywords",
"mixin-exists",
"module-functions",
"module-mixins",
"module-variables",
"type-of",
"variable-exists",
],
_ => &[],
}
}
fn sass_builtin_module_mixin_names(module: &str) -> &'static [&'static str] {
match module {
"meta" => &["apply", "load-css"],
_ => &[],
}
}
fn sass_builtin_module_variable_names(module: &str) -> &'static [&'static str] {
match module {
"math" => &["e", "epsilon", "max-safe-integer", "min-safe-integer", "pi"],
_ => &[],
}
}
pub fn summarize_omena_query_css_modules_local_composes_style_diagnostics(
target_style_path: &str,
target_source: &str,
) -> Vec<OmenaQueryStyleDiagnosticV0> {
let dialect = omena_parser_dialect_for_style_path(target_style_path);
let target_facts = collect_omena_query_omena_parser_style_facts_raw(target_source, dialect);
let target_class_names = target_facts
.selectors
.iter()
.filter(|selector| selector.kind == ParsedSelectorFactKind::Class)
.map(|selector| selector.name.as_str())
.collect::<BTreeSet<_>>();
let mut diagnostics = Vec::new();
for edge in target_facts.css_module_composes_edges {
if edge.kind != ParsedCssModuleComposesEdgeKind::Local {
continue;
}
let start: u32 = edge.range.start().into();
let end: u32 = edge.range.end().into();
let range = parser_range_for_byte_span(
target_source,
ParserByteSpanV0 {
start: start as usize,
end: end as usize,
},
);
for target_name in edge.target_names {
if target_class_names.contains(target_name.as_str()) {
continue;
}
diagnostics.push(OmenaQueryStyleDiagnosticV0 {
code: "missingComposedSelector",
severity: "warning",
provenance: vec![
"omena-parser.css-modules-composes-facts",
"omena-query.css-modules-resolution-diagnostics",
],
range,
message: format!(
"Selector '.{}' not found in this file for composes.",
target_name
),
tags: Vec::new(),
create_custom_property: None,
});
}
}
diagnostics
}
pub fn summarize_omena_query_css_modules_resolution_style_diagnostics(
target_style_path: &str,
target_source: &str,
style_sources: &[OmenaQueryStyleSourceInputV0],
package_manifests: &[OmenaQueryStylePackageManifestV0],
) -> Vec<OmenaQueryStyleDiagnosticV0> {
let style_source_refs = style_sources
.iter()
.map(|source| (source.style_path.as_str(), source.style_source.as_str()))
.collect::<Vec<_>>();
let style_fact_entries = collect_omena_query_style_fact_entries(style_source_refs.as_slice());
let available_style_paths = style_fact_entries
.iter()
.map(|entry| entry.style_path.as_str())
.collect::<BTreeSet<_>>();
let facts_by_path = style_fact_entries
.iter()
.map(|entry| (entry.style_path.as_str(), entry.facts.clone()))
.collect::<BTreeMap<_, _>>();
let dialect = omena_parser_dialect_for_style_path(target_style_path);
let target_facts = collect_omena_query_omena_parser_style_facts_raw(target_source, dialect);
let mut diagnostics = Vec::new();
for edge in target_facts.css_module_composes_edges {
if edge.kind == ParsedCssModuleComposesEdgeKind::Global {
continue;
}
let start: u32 = edge.range.start().into();
let end: u32 = edge.range.end().into();
let range = parser_range_for_byte_span(
target_source,
ParserByteSpanV0 {
start: start as usize,
end: end as usize,
},
);
let target_style = if edge.kind == ParsedCssModuleComposesEdgeKind::External {
let Some(source) = edge.import_source.as_deref() else {
continue;
};
let Some(resolved_style_path) = resolve_style_module_source(
target_style_path,
source,
&available_style_paths,
package_manifests,
) else {
diagnostics.push(OmenaQueryStyleDiagnosticV0 {
code: "missingComposedModule",
severity: "warning",
provenance: vec![
"omena-parser.css-modules-composes-facts",
"omena-resolver.style-module-resolution",
],
range,
message: format!("Cannot resolve composed CSS Module '{}'.", source),
tags: Vec::new(),
create_custom_property: None,
});
continue;
};
resolved_style_path
} else {
target_style_path.to_string()
};
let target_class_names = facts_by_path
.get(target_style.as_str())
.map(|facts| facts.class_selector_names.as_slice())
.unwrap_or(&[])
.iter()
.map(String::as_str)
.collect::<BTreeSet<_>>();
for target_name in edge.target_names {
if target_class_names.contains(target_name.as_str()) {
continue;
}
let message = if let Some(source) = edge.import_source.as_deref() {
format!(
"Selector '.{}' not found in composed module '{}'.",
target_name, source
)
} else {
format!(
"Selector '.{}' not found in this file for composes.",
target_name
)
};
diagnostics.push(OmenaQueryStyleDiagnosticV0 {
code: "missingComposedSelector",
severity: "warning",
provenance: vec![
"omena-parser.css-modules-composes-facts",
"omena-query.css-modules-resolution-diagnostics",
],
range,
message,
tags: Vec::new(),
create_custom_property: None,
});
}
}
let mut reported_missing_value_modules = BTreeSet::new();
for edge in target_facts.css_module_value_import_edges {
let start: u32 = edge.range.start().into();
let end: u32 = edge.range.end().into();
let range = parser_range_for_byte_span(
target_source,
ParserByteSpanV0 {
start: start as usize,
end: end as usize,
},
);
let Some(resolved_style_path) = resolve_style_module_source(
target_style_path,
&edge.import_source,
&available_style_paths,
package_manifests,
) else {
if reported_missing_value_modules.insert(edge.import_source.clone()) {
diagnostics.push(OmenaQueryStyleDiagnosticV0 {
code: "missingValueModule",
severity: "warning",
provenance: vec![
"omena-parser.css-modules-value-facts",
"omena-resolver.style-module-resolution",
],
range,
message: format!(
"Cannot resolve imported @value module '{}'.",
edge.import_source
),
tags: Vec::new(),
create_custom_property: None,
});
}
continue;
};
let target_value_names = facts_by_path
.get(resolved_style_path.as_str())
.map(|facts| facts.css_module_value_definition_names.as_slice())
.unwrap_or(&[])
.iter()
.map(String::as_str)
.collect::<BTreeSet<_>>();
if target_value_names.contains(edge.remote_name.as_str()) {
continue;
}
let message = if edge.local_name == edge.remote_name {
format!(
"@value '{}' not found in '{}'.",
edge.remote_name, edge.import_source
)
} else {
format!(
"@value '{}' not found in '{}' for local binding '{}'.",
edge.remote_name, edge.import_source, edge.local_name
)
};
diagnostics.push(OmenaQueryStyleDiagnosticV0 {
code: "missingImportedValue",
severity: "warning",
provenance: vec![
"omena-parser.css-modules-value-facts",
"omena-query.css-modules-resolution-diagnostics",
],
range,
message,
tags: Vec::new(),
create_custom_property: None,
});
}
diagnostics
}
pub fn summarize_omena_query_unused_selector_style_diagnostics(
target_style_path: &str,
target_source: &str,
style_sources: &[OmenaQueryStyleSourceInputV0],
source_documents: &[OmenaQuerySourceDocumentInputV0],
package_manifests: &[OmenaQueryStylePackageManifestV0],
classname_transform: Option<&str>,
) -> Vec<OmenaQueryStyleDiagnosticV0> {
summarize_omena_query_unused_selector_style_diagnostics_with_path_mappings(
target_style_path,
target_source,
style_sources,
source_documents,
package_manifests,
classname_transform,
&[],
&[],
)
}
#[allow(clippy::too_many_arguments)]
pub fn summarize_omena_query_unused_selector_style_diagnostics_with_path_mappings(
target_style_path: &str,
target_source: &str,
style_sources: &[OmenaQueryStyleSourceInputV0],
source_documents: &[OmenaQuerySourceDocumentInputV0],
package_manifests: &[OmenaQueryStylePackageManifestV0],
classname_transform: Option<&str>,
bundler_path_mappings: &[OmenaResolverBundlerPathAliasMappingV0],
tsconfig_path_mappings: &[OmenaResolverTsconfigPathMappingV0],
) -> Vec<OmenaQueryStyleDiagnosticV0> {
if source_documents.is_empty() {
return Vec::new();
}
let style_source_refs = style_sources
.iter()
.map(|source| (source.style_path.as_str(), source.style_source.as_str()))
.collect::<Vec<_>>();
let style_fact_entries = collect_omena_query_style_fact_entries(style_source_refs.as_slice());
let available_style_paths = style_fact_entries
.iter()
.map(|entry| entry.style_path.as_str())
.collect::<BTreeSet<_>>();
let facts_by_path = style_fact_entries
.iter()
.map(|entry| (entry.style_path.as_str(), entry.facts.clone()))
.collect::<BTreeMap<_, _>>();
let aliases_by_path = collect_classname_transform_aliases(&facts_by_path, classname_transform);
let (mut used_selectors, unresolved_dynamic_usage, has_unresolved_style_import) =
collect_omena_query_source_selector_usage_by_style(
&available_style_paths,
source_documents,
package_manifests,
&aliases_by_path,
bundler_path_mappings,
tsconfig_path_mappings,
);
if unresolved_dynamic_usage.contains(target_style_path) {
return Vec::new();
}
if has_unresolved_style_import {
return Vec::new();
}
let composes_graph = collect_css_modules_composes_adjacency(
&facts_by_path,
&available_style_paths,
package_manifests,
);
propagate_omena_query_composes_usage(&composes_graph, &mut used_selectors);
let dialect = omena_parser_dialect_for_style_path(target_style_path);
let target_facts = collect_omena_query_omena_parser_style_facts_raw(target_source, dialect);
let used_in_target = used_selectors
.get(target_style_path)
.cloned()
.unwrap_or_default();
let mut emitted = BTreeSet::new();
target_facts
.selectors
.into_iter()
.filter(|selector| selector.kind == ParsedSelectorFactKind::Class)
.filter(|selector| !used_in_target.contains(selector.name.as_str()))
.filter_map(|selector| {
let start: u32 = selector.range.start().into();
let end: u32 = selector.range.end().into();
if !emitted.insert(selector.name.clone()) {
return None;
}
Some(OmenaQueryStyleDiagnosticV0 {
code: "unusedSelector",
severity: "hint",
provenance: vec![
"omena-parser.selector-facts",
"omena-query.source-selector-usage",
],
range: parser_range_for_byte_span(
target_source,
ParserByteSpanV0 {
start: start as usize,
end: end as usize,
},
),
message: format!("Selector '.{}' is declared but never used.", selector.name),
tags: vec![LSP_DIAGNOSTIC_TAG_UNNECESSARY],
create_custom_property: None,
})
})
.collect()
}
fn collect_omena_query_source_selector_usage_by_style(
available_style_paths: &BTreeSet<&str>,
source_documents: &[OmenaQuerySourceDocumentInputV0],
package_manifests: &[OmenaQueryStylePackageManifestV0],
aliases_by_path: &BTreeMap<String, BTreeMap<String, BTreeSet<String>>>,
bundler_path_mappings: &[OmenaResolverBundlerPathAliasMappingV0],
tsconfig_path_mappings: &[OmenaResolverTsconfigPathMappingV0],
) -> (BTreeMap<String, BTreeSet<String>>, BTreeSet<String>, bool) {
let mut used_selectors = BTreeMap::<String, BTreeSet<String>>::new();
let mut unresolved_dynamic_usage = BTreeSet::<String>::new();
let mut has_unresolved_style_import = false;
for document in source_documents {
let imports = summarize_omena_query_source_import_declarations_for_source_language(
document.source_path.as_str(),
&document.source_source,
None,
);
let mut imported_style_bindings = Vec::new();
let mut classnames_bind_bindings = Vec::new();
for import in imports.imports {
if import.specifier == "classnames/bind" {
classnames_bind_bindings.push(import.binding);
continue;
}
let Some(style_path) = resolve_style_module_source_with_path_mappings(
&document.source_path,
&import.specifier,
available_style_paths,
package_manifests,
bundler_path_mappings,
tsconfig_path_mappings,
) else {
if specifier_targets_style_module(&import.specifier) {
has_unresolved_style_import = true;
}
continue;
};
imported_style_bindings.push(OmenaQuerySourceImportedStyleBindingV0 {
binding: import.binding,
style_uri: style_path,
});
}
if imported_style_bindings.is_empty() {
continue;
}
let index = summarize_omena_query_source_syntax_index_for_source_language(
document.source_path.as_str(),
&document.source_source,
None,
imported_style_bindings,
classnames_bind_bindings,
);
for reference in index.selector_references {
let Some(target_style_path) = reference.target_style_uri else {
continue;
};
let Some(selector_name) = reference.selector_name.or_else(|| {
source_reference_text_selector_name(&document.source_source, reference.byte_span)
}) else {
unresolved_dynamic_usage.insert(target_style_path);
continue;
};
let used_for_style = used_selectors.entry(target_style_path.clone()).or_default();
if let Some(canonical_names) = aliases_by_path
.get(target_style_path.as_str())
.and_then(|aliases| aliases.get(selector_name.as_str()))
{
used_for_style.extend(canonical_names.iter().cloned());
} else {
used_for_style.insert(selector_name);
}
}
}
(
used_selectors,
unresolved_dynamic_usage,
has_unresolved_style_import,
)
}
fn specifier_targets_style_module(specifier: &str) -> bool {
let path = specifier
.split(['?', '#'])
.next()
.unwrap_or(specifier)
.to_ascii_lowercase();
path.ends_with(".css")
|| path.ends_with(".scss")
|| path.ends_with(".sass")
|| path.ends_with(".less")
}
fn collect_classname_transform_aliases(
facts_by_path: &BTreeMap<&str, OmenaQueryOmenaParserStyleFactsV0>,
classname_transform: Option<&str>,
) -> BTreeMap<String, BTreeMap<String, BTreeSet<String>>> {
let mut aliases_by_path = BTreeMap::<String, BTreeMap<String, BTreeSet<String>>>::new();
for (style_path, facts) in facts_by_path {
let aliases = aliases_by_path
.entry((*style_path).to_string())
.or_default();
for selector_name in &facts.class_selector_names {
for alias in classname_transform_aliases(selector_name.as_str(), classname_transform) {
aliases
.entry(alias)
.or_default()
.insert(selector_name.clone());
}
}
}
aliases_by_path
}
fn classname_transform_aliases(name: &str, classname_transform: Option<&str>) -> Vec<String> {
match classname_transform.unwrap_or("asIs") {
"camelCase" => keep_original_plus_transformed(name, to_ascii_camel_case(name)),
"camelCaseOnly" => vec![to_ascii_camel_case(name)],
"dashes" => keep_original_plus_transformed(name, dashes_to_ascii_camel(name)),
"dashesOnly" => vec![dashes_to_ascii_camel(name)],
_ => vec![name.to_string()],
}
}
fn keep_original_plus_transformed(name: &str, transformed: String) -> Vec<String> {
if transformed == name {
vec![name.to_string()]
} else {
vec![name.to_string(), transformed]
}
}
fn dashes_to_ascii_camel(name: &str) -> String {
transform_ascii_separated_name(name, |byte| byte == b'-')
}
fn to_ascii_camel_case(name: &str) -> String {
transform_ascii_separated_name(name, |byte| byte == b'-' || byte == b'_' || byte == b' ')
}
fn transform_ascii_separated_name(name: &str, is_separator: impl Fn(u8) -> bool) -> String {
let mut output = String::with_capacity(name.len());
let mut capitalize_next = false;
for byte in name.bytes() {
if is_separator(byte) {
capitalize_next = true;
continue;
}
if capitalize_next {
output.push((byte as char).to_ascii_uppercase());
capitalize_next = false;
continue;
}
output.push(byte as char);
}
output
}
fn propagate_omena_query_composes_usage(
composes_graph: &BTreeMap<CssModulesComposesNode, BTreeSet<CssModulesComposesNode>>,
used_selectors: &mut BTreeMap<String, BTreeSet<String>>,
) {
let mut used_nodes = used_selectors
.iter()
.flat_map(|(style_path, selectors)| {
selectors
.iter()
.map(|selector_name| CssModulesComposesNode {
style_path: style_path.clone(),
selector_name: selector_name.clone(),
})
})
.collect::<BTreeSet<_>>();
let mut changed = true;
while changed {
changed = false;
for (owner, targets) in composes_graph {
if !used_nodes.contains(owner) {
continue;
}
for target in targets {
if used_nodes.insert(target.clone()) {
used_selectors
.entry(target.style_path.clone())
.or_default()
.insert(target.selector_name.clone());
changed = true;
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sass_import_is_plain_css_classifies_css_forms() {
assert!(sass_import_is_plain_css("url(theme.css)"));
assert!(sass_import_is_plain_css("url(theme.scss)")); assert!(sass_import_is_plain_css("vendor.css"));
assert!(sass_import_is_plain_css("VENDOR.CSS")); assert!(sass_import_is_plain_css("//cdn.example/x.css"));
assert!(sass_import_is_plain_css("https://x.com/y.css"));
assert!(sass_import_is_plain_css("http://x.com/y")); }
#[test]
fn sass_import_is_plain_css_keeps_partials_flaggable() {
assert!(!sass_import_is_plain_css("partial"));
assert!(!sass_import_is_plain_css("./legacy"));
assert!(!sass_import_is_plain_css("foundation/buttons"));
assert!(!sass_import_is_plain_css("legacy")); }
#[test]
fn media_qualified_import_is_not_deprecated() {
let screen = summarize_omena_query_sass_import_deprecation_hints(
"theme.scss",
"@import \"foo\" screen;",
);
assert!(
screen.is_empty(),
"media-qualified (Ident) @import must NOT warn deprecatedSassImport, got {screen:?}"
);
let feature = summarize_omena_query_sass_import_deprecation_hints(
"theme.scss",
"@import \"foo\" (min-width: 100px);",
);
assert!(
feature.is_empty(),
"media-feature-qualified @import must NOT warn deprecatedSassImport, got {feature:?}"
);
}
#[test]
fn bare_partial_import_still_deprecated() {
let diagnostics =
summarize_omena_query_sass_import_deprecation_hints("theme.scss", "@import 'partial';");
assert_eq!(
diagnostics.len(),
1,
"bare Sass partial @import must still warn, got {diagnostics:?}"
);
assert_eq!(diagnostics[0].code, "deprecatedSassImport");
}
#[test]
fn media_qualified_comma_peer_classifies_per_target() {
let diagnostics = summarize_omena_query_sass_import_deprecation_hints(
"theme.scss",
"@import \"a\", \"b\" screen;",
);
assert_eq!(
diagnostics.len(),
1,
"exactly the bare partial peer must warn, got {diagnostics:?}"
);
}
#[test]
fn sass_meta_allowlist_matches_pinned_1_77_surface() {
let pinned_functions: BTreeSet<&str> = [
"accepts-content",
"calc-args",
"calc-name",
"call",
"content-exists",
"feature-exists",
"function-exists",
"get-function",
"get-mixin",
"global-variable-exists",
"inspect",
"keywords",
"mixin-exists",
"module-functions",
"module-mixins",
"module-variables",
"type-of",
"variable-exists",
]
.into_iter()
.collect();
let pinned_mixins: BTreeSet<&str> = ["apply", "load-css"].into_iter().collect();
let actual_functions: BTreeSet<&str> = sass_builtin_module_function_names("meta")
.iter()
.copied()
.collect();
let actual_mixins: BTreeSet<&str> = sass_builtin_module_mixin_names("meta")
.iter()
.copied()
.collect();
assert_eq!(
actual_functions, pinned_functions,
"sass:meta function allowlist drifted from pinned Sass 1.77 surface; \
update both the allowlist and this pinned set together"
);
assert_eq!(
actual_mixins, pinned_mixins,
"sass:meta mixin allowlist drifted from pinned Sass 1.77 surface; \
update both the allowlist and this pinned set together"
);
}
#[test]
fn local_composes_flags_real_same_file_typo() {
let source = ".base { color: red; }\n.button { composes: missing; }\n";
let diagnostics = summarize_omena_query_css_modules_local_composes_style_diagnostics(
"/tmp/foo.module.scss",
source,
);
assert_eq!(diagnostics.len(), 1, "expected one missingComposedSelector");
assert_eq!(diagnostics[0].code, "missingComposedSelector");
assert!(
diagnostics[0].message.contains("not found in this file"),
"message should reference same-file resolution, got: {}",
diagnostics[0].message
);
}
#[test]
fn local_composes_keeps_resolvable_target_silent() {
let source = ".base { color: red; }\n.button { composes: base; }\n";
let diagnostics = summarize_omena_query_css_modules_local_composes_style_diagnostics(
"/tmp/foo.module.scss",
source,
);
assert!(
diagnostics.is_empty(),
"resolvable local composes target should not be flagged, got: {diagnostics:?}"
);
}
#[test]
fn local_composes_does_not_flag_external_target_without_source() {
let source = ".button { composes: shared from './other.module.scss'; color: blue; }\n";
let diagnostics = summarize_omena_query_css_modules_local_composes_style_diagnostics(
"/tmp/foo.module.scss",
source,
);
assert!(
diagnostics.is_empty(),
"external composes target must not be flagged without cross-file source, got: {diagnostics:?}"
);
}
#[test]
fn local_composes_does_not_flag_global_target() {
let source = ".button { composes: someGlobal from global; color: blue; }\n";
let diagnostics = summarize_omena_query_css_modules_local_composes_style_diagnostics(
"/tmp/foo.module.scss",
source,
);
assert!(
diagnostics.is_empty(),
"global composes target must not be flagged, got: {diagnostics:?}"
);
}
#[test]
fn single_file_summary_includes_local_composes_typo() -> Result<(), String> {
let style_uri = "/tmp/foo.module.scss";
let source = ".base { color: red; }\n.button { composes: missing; }\n";
let candidates = summarize_omena_query_style_hover_candidates(style_uri, source)
.ok_or("hover candidates")?;
let summary = summarize_omena_query_style_diagnostics_for_file_with_local_composes(
style_uri,
source,
candidates.candidates.as_slice(),
);
assert!(
summary
.diagnostics
.iter()
.any(|diagnostic| diagnostic.code == "missingComposedSelector"),
"bare single-file summary should surface the local composes typo, got: {:?}",
summary.diagnostics
);
assert_eq!(summary.diagnostic_count, summary.diagnostics.len());
Ok(())
}
#[test]
fn single_file_summary_does_not_flag_external_composes() -> Result<(), String> {
let style_uri = "/tmp/foo.module.scss";
let source = ".button { composes: shared from './other.module.scss'; color: blue; }\n";
let candidates = summarize_omena_query_style_hover_candidates(style_uri, source)
.ok_or("hover candidates")?;
let summary = summarize_omena_query_style_diagnostics_for_file_with_local_composes(
style_uri,
source,
candidates.candidates.as_slice(),
);
assert!(
!summary.diagnostics.iter().any(|diagnostic| {
diagnostic.code == "missingComposedSelector"
|| diagnostic.code == "missingComposedModule"
}),
"external composes target must not be flagged in single-file mode, got: {:?}",
summary.diagnostics
);
Ok(())
}
}