use std::collections::{BTreeMap, BTreeSet};
use omena_cascade::{
CascadeDeclaration, CascadeKey, CascadeLevel, CascadeValue, LayerRank, Specificity,
cascade_property, parse_simple_selector_signature,
};
use omena_query_checker_orchestrator::{
OmenaCheckerCascadeDeclarationInputV0, OmenaCheckerCascadeInputV0,
OmenaCheckerCategoricalInputV0, OmenaCheckerCategoricalPrimitiveRolePairInputV0,
OmenaCheckerCategoricalRoleMappingInputV0, OmenaCheckerCustomPropertyInputV0,
OmenaCheckerRgFlowCouplingInputV0, OmenaCheckerRgFlowCouplingSpaceInputV0,
OmenaCheckerRgFlowInputV0, OmenaCheckerSmtInputV0, OmenaCheckerSmtObligationInputV0,
checker_cascade_primitive_role_catalog_v0, run_omena_query_checker_cascade_gate_v0,
run_omena_query_checker_categorical_gate_v0, run_omena_query_checker_rg_flow_gate_v0,
run_omena_query_checker_smt_gate_v0,
};
use omena_query_checker_orchestrator::{
REPLICA_ENSEMBLE_FEATURE_GATE_V0, REPLICA_ENSEMBLE_LAYER_MARKER_V0,
REPLICA_ENSEMBLE_SCHEMA_VERSION_V0, ReplicaSiteOutcomeV0, site as replica_ensemble_site,
};
use omena_query_transform_runner::expand_css_nested_selector;
use super::{
OmenaQueryStyleDiagnosticV0, ParserByteSpanV0, ParserRangeV0,
omena_parser_dialect_for_style_path, parser_range_for_byte_span,
summarize_static_css_custom_property_fixed_point_from_source,
};
const LSP_DIAGNOSTIC_TAG_UNNECESSARY: u8 = 1;
pub(super) fn summarize_query_cascade_checker_diagnostics_with_deep_analysis(
style_uri: &str,
source: &str,
deep_analysis: bool,
) -> Vec<OmenaQueryStyleDiagnosticV0> {
let (checker_input, declaration_ranges, custom_property_ranges) =
collect_query_checker_cascade_input(style_uri, source);
let mut diagnostics = Vec::new();
let (rg_flow_diagnostics, categorical_diagnostics, smt_diagnostics) = if deep_analysis {
(
summarize_query_rg_flow_coupling_diagnostics(source, &checker_input.custom_properties),
summarize_query_categorical_cascade_evidence_diagnostics(
source,
&checker_input.custom_properties,
),
summarize_query_smt_cascade_obligation_diagnostics(
source,
&checker_input.declarations,
&declaration_ranges,
),
)
} else {
(Vec::new(), Vec::new(), Vec::new())
};
let gate = run_omena_query_checker_cascade_gate_v0(checker_input);
if !gate.enforcement_passed {
return vec![OmenaQueryStyleDiagnosticV0 {
code: "checkerDiagnosticGateFailed",
severity: "warning",
provenance: vec![
"omena-query-checker-orchestrator.cascade-gate",
"omena-query.cascade-checker",
],
range: parser_range_for_byte_span(
source,
ParserByteSpanV0 {
start: 0,
end: source.len(),
},
),
message: "Checker diagnostic gate rejected unregistered rule output.".to_string(),
tags: Vec::new(),
create_custom_property: None,
}];
}
for evaluation in gate.evaluations {
if evaluation.rule_code_name == "iacvt-prone"
&& evaluation
.custom_property_names
.iter()
.all(|name| !custom_property_ranges.contains_key(name))
{
continue;
}
let range = evaluation
.declaration_ids
.iter()
.find_map(|declaration_id| declaration_ranges.get(declaration_id).copied())
.or_else(|| {
evaluation
.custom_property_names
.iter()
.find_map(|name| custom_property_ranges.get(name).copied())
})
.unwrap_or_else(|| {
parser_range_for_byte_span(
source,
ParserByteSpanV0 {
start: 0,
end: source.len(),
},
)
});
let mut provenance = vec![
"omena-query-checker-orchestrator.cascade-gate",
"omena-checker.cascade-rules",
"omena-query.cascade-checker",
];
provenance.extend(evaluation.mechanism_products.iter().copied());
diagnostics.push(OmenaQueryStyleDiagnosticV0 {
code: query_cascade_checker_code(evaluation.rule_code_name),
severity: query_cascade_checker_diagnostic_severity(evaluation.rule_code_name),
provenance,
range,
message: evaluation.message,
tags: query_cascade_checker_diagnostic_tags(evaluation.rule_code_name),
create_custom_property: None,
});
}
if deep_analysis {
deduplicate_query_theory_hints_against_circular_var(
&mut diagnostics,
rg_flow_diagnostics,
categorical_diagnostics,
);
diagnostics.extend(smt_diagnostics);
}
diagnostics
}
fn deduplicate_query_theory_hints_against_circular_var(
diagnostics: &mut Vec<OmenaQueryStyleDiagnosticV0>,
theory_hints: impl IntoIterator<Item = OmenaQueryStyleDiagnosticV0>,
extra_theory_hints: impl IntoIterator<Item = OmenaQueryStyleDiagnosticV0>,
) {
let circular_var_ranges = diagnostics
.iter()
.filter(|diagnostic| diagnostic.code == "circularVar")
.map(|diagnostic| diagnostic.range)
.collect::<BTreeSet<_>>();
for hint in theory_hints.into_iter().chain(extra_theory_hints) {
let overlaps = !circular_var_ranges.is_empty()
&& (circular_var_ranges.contains(&hint.range)
|| query_theory_hint_range_is_whole_file(&hint.range));
if overlaps {
for diagnostic in diagnostics
.iter_mut()
.filter(|diagnostic| diagnostic.code == "circularVar")
{
for label in hint.provenance.iter().copied() {
if !diagnostic.provenance.contains(&label) {
diagnostic.provenance.push(label);
}
}
}
} else {
diagnostics.push(hint);
}
}
}
fn query_theory_hint_range_is_whole_file(range: &ParserRangeV0) -> bool {
range.start.line == 0 && range.start.character == 0
}
fn summarize_query_rg_flow_coupling_diagnostics(
source: &str,
custom_properties: &[OmenaCheckerCustomPropertyInputV0],
) -> Vec<OmenaQueryStyleDiagnosticV0> {
let Some(flow) = query_rg_flow_coupling_for_custom_properties(custom_properties) else {
return Vec::new();
};
let gate =
run_omena_query_checker_rg_flow_gate_v0(OmenaCheckerRgFlowInputV0 { flows: vec![flow] });
if !gate.enforcement_passed {
return Vec::new();
}
let whole_file_range = parser_range_for_byte_span(
source,
ParserByteSpanV0 {
start: 0,
end: source.len(),
},
);
gate.evaluations
.into_iter()
.map(|evaluation| {
let mut provenance = vec![
"omena-query-checker-orchestrator.rg-flow-gate",
"omena-checker.rg-flow-rules",
"omena-query.cascade-checker",
];
provenance.extend(evaluation.mechanism_products.iter().copied());
OmenaQueryStyleDiagnosticV0 {
code: "rgFlowRelevantOperator",
severity: "hint",
provenance,
range: whole_file_range,
message: evaluation.message,
tags: Vec::new(),
create_custom_property: None,
}
})
.collect()
}
fn summarize_query_categorical_cascade_evidence_diagnostics(
source: &str,
custom_properties: &[OmenaCheckerCustomPropertyInputV0],
) -> Vec<OmenaQueryStyleDiagnosticV0> {
let Some(mapping) = query_categorical_role_mapping_for_cascade(custom_properties) else {
return Vec::new();
};
let gate = run_omena_query_checker_categorical_gate_v0(OmenaCheckerCategoricalInputV0 {
mappings: vec![mapping],
});
if !gate.enforcement_passed {
return Vec::new();
}
let whole_file_range = parser_range_for_byte_span(
source,
ParserByteSpanV0 {
start: 0,
end: source.len(),
},
);
gate.evaluations
.into_iter()
.map(|evaluation| {
let mut provenance = vec![
"omena-query-checker-orchestrator.categorical-gate",
"omena-checker.categorical-rules",
"omena-query.cascade-checker",
];
provenance.extend(evaluation.mechanism_products.iter().copied());
OmenaQueryStyleDiagnosticV0 {
code: "categoricalCascadeEvidenceInconsistency",
severity: "hint",
provenance,
range: whole_file_range,
message:
"Cascade custom-property ranking forms a reference cycle, so the categorical \
cosheaf-colimit witness for the cascade-ranking primitive is not functorial: \
the ranking primitive plays conflicting categorical roles in this stylesheet."
.to_string(),
tags: Vec::new(),
create_custom_property: None,
}
})
.collect()
}
fn summarize_query_smt_cascade_obligation_diagnostics(
source: &str,
declarations: &[OmenaCheckerCascadeDeclarationInputV0],
declaration_ranges: &BTreeMap<String, ParserRangeV0>,
) -> Vec<OmenaQueryStyleDiagnosticV0> {
let obligations = query_smt_box_shorthand_obligations(declarations);
if obligations.is_empty() {
return Vec::new();
}
let anchor_ranges = obligations
.iter()
.filter_map(|(obligation, anchor_declaration_id)| {
declaration_ranges
.get(anchor_declaration_id)
.copied()
.map(|range| (obligation.obligation_id.clone(), range))
})
.collect::<BTreeMap<_, _>>();
let gate = run_omena_query_checker_smt_gate_v0(OmenaCheckerSmtInputV0 {
obligations: obligations
.into_iter()
.map(|(obligation, _)| obligation)
.collect(),
});
if !gate.enforcement_passed {
return Vec::new();
}
let whole_file_range = parser_range_for_byte_span(
source,
ParserByteSpanV0 {
start: 0,
end: source.len(),
},
);
gate.evaluations
.into_iter()
.map(|evaluation| {
let range = anchor_ranges
.get(&evaluation.obligation_id)
.copied()
.unwrap_or(whole_file_range);
let mut provenance = vec![
"omena-query-checker-orchestrator.smt-gate",
"omena-checker.smt-rules",
"omena-query.cascade-checker",
];
provenance.extend(evaluation.mechanism_products.iter().copied());
OmenaQueryStyleDiagnosticV0 {
code: "cascadeSmtViolation",
severity: "warning",
provenance,
range,
message:
"Box-shorthand combination proof obligation is unsatisfiable: these longhands \
cannot be safely combined into the shorthand without changing the cascade \
outcome."
.to_string(),
tags: Vec::new(),
create_custom_property: None,
}
})
.collect()
}
fn query_smt_box_shorthand_obligations(
declarations: &[OmenaCheckerCascadeDeclarationInputV0],
) -> Vec<(OmenaCheckerSmtObligationInputV0, String)> {
let mut obligations = Vec::new();
let mut by_selector = BTreeMap::<&str, Vec<&OmenaCheckerCascadeDeclarationInputV0>>::new();
for declaration in declarations {
by_selector
.entry(declaration.selector.as_str())
.or_default()
.push(declaration);
}
for (selector, selector_declarations) in by_selector {
for (shorthand, expected_longhands) in query_smt_box_shorthand_longhand_quartets() {
let mut quartet = Vec::with_capacity(expected_longhands.len());
for expected in expected_longhands {
let Some(declaration) = selector_declarations
.iter()
.copied()
.find(|declaration| declaration.property == *expected)
else {
break;
};
quartet.push(declaration);
}
if quartet.len() != expected_longhands.len() {
continue;
}
let canonical_order = quartet
.iter()
.zip(expected_longhands.iter())
.all(|(declaration, expected)| declaration.property == *expected);
let no_important = quartet.iter().all(|declaration| !declaration.important);
let no_empty_value = quartet
.iter()
.all(|declaration| !declaration.value.trim().is_empty());
let adjacent_source_order = quartet
.windows(2)
.all(|pair| pair[1].source_order == pair[0].source_order + 1);
let canonical_terms = vec![
"require:supported-shorthand-property=true".to_string(),
format!("require:canonical-longhand-quartet={canonical_order}"),
format!("require:no-important-longhand={no_important}"),
format!("require:no-empty-longhand-value={no_empty_value}"),
format!("require:adjacent-source-order={adjacent_source_order}"),
];
let anchor_declaration_id = quartet
.iter()
.find(|declaration| declaration.important || declaration.value.trim().is_empty())
.map(|declaration| declaration.declaration_id.clone())
.unwrap_or_else(|| quartet[0].declaration_id.clone());
obligations.push((
OmenaCheckerSmtObligationInputV0 {
obligation_id: format!(
"stylesheet://{selector}::{shorthand}-shorthand-combination"
),
l1_primitive: "boxShorthandCombination".to_string(),
canonical_terms,
},
anchor_declaration_id,
));
}
}
obligations
}
fn query_smt_box_shorthand_longhand_quartets() -> Vec<(&'static str, [&'static str; 4])> {
vec![
(
"margin",
["margin-top", "margin-right", "margin-bottom", "margin-left"],
),
(
"padding",
[
"padding-top",
"padding-right",
"padding-bottom",
"padding-left",
],
),
(
"border-color",
[
"border-top-color",
"border-right-color",
"border-bottom-color",
"border-left-color",
],
),
(
"border-style",
[
"border-top-style",
"border-right-style",
"border-bottom-style",
"border-left-style",
],
),
(
"border-width",
[
"border-top-width",
"border-right-width",
"border-bottom-width",
"border-left-width",
],
),
(
"scroll-margin",
[
"scroll-margin-top",
"scroll-margin-right",
"scroll-margin-bottom",
"scroll-margin-left",
],
),
(
"scroll-padding",
[
"scroll-padding-top",
"scroll-padding-right",
"scroll-padding-bottom",
"scroll-padding-left",
],
),
]
}
fn query_categorical_role_mapping_for_cascade(
custom_properties: &[OmenaCheckerCustomPropertyInputV0],
) -> Option<OmenaCheckerCategoricalRoleMappingInputV0> {
if custom_properties.is_empty() {
return None;
}
let declared = custom_properties
.iter()
.map(|property| property.name.as_str())
.collect::<BTreeSet<_>>();
let dependencies = custom_properties
.iter()
.map(|property| {
(
property.name.as_str(),
property
.dependencies
.iter()
.map(String::as_str)
.filter(|dependency| declared.contains(dependency))
.collect::<BTreeSet<_>>(),
)
})
.collect::<BTreeMap<_, _>>();
let has_reference_cycle = declared
.iter()
.any(|name| query_custom_property_in_reference_cycle(name, &dependencies));
let mut primitive_role_pairs = Vec::new();
if has_reference_cycle {
primitive_role_pairs.push(OmenaCheckerCategoricalPrimitiveRolePairInputV0 {
primitive_name: "cascade_property".to_string(),
categorical_role: "cascade-ranking non-convergent witness".to_string(),
});
}
primitive_role_pairs.extend(checker_cascade_primitive_role_catalog_v0().into_iter().map(
|(primitive_name, categorical_role)| OmenaCheckerCategoricalPrimitiveRolePairInputV0 {
primitive_name: primitive_name.to_string(),
categorical_role: categorical_role.to_string(),
},
));
Some(OmenaCheckerCategoricalRoleMappingInputV0 {
mapping_id: "stylesheet://cascade-primitive-role-evidence".to_string(),
primitive_role_pairs,
})
}
pub(super) fn query_exercised_cascade_primitive_role_pairs_from_source(
source: &str,
) -> Vec<(String, String)> {
let (checker_input, _, _) = collect_query_checker_cascade_input("query://categorical", source);
match query_categorical_role_mapping_for_cascade(&checker_input.custom_properties) {
Some(mapping) => mapping
.primitive_role_pairs
.into_iter()
.map(|pair| (pair.primitive_name, pair.categorical_role))
.collect(),
None => Vec::new(),
}
}
fn query_rg_flow_coupling_for_custom_properties(
custom_properties: &[OmenaCheckerCustomPropertyInputV0],
) -> Option<OmenaCheckerRgFlowCouplingInputV0> {
if custom_properties.is_empty() {
return None;
}
let declared = custom_properties
.iter()
.map(|property| property.name.as_str())
.collect::<BTreeSet<_>>();
let dependencies = custom_properties
.iter()
.map(|property| {
(
property.name.as_str(),
property
.dependencies
.iter()
.map(String::as_str)
.filter(|dependency| declared.contains(dependency))
.collect::<BTreeSet<_>>(),
)
})
.collect::<BTreeMap<_, _>>();
let k_env = custom_properties.len();
let k_decl = custom_properties
.iter()
.filter(|property| {
property
.dependencies
.iter()
.any(|dependency| declared.contains(dependency.as_str()))
})
.count();
let k_cycle = declared
.iter()
.filter(|name| query_custom_property_in_reference_cycle(name, &dependencies))
.count();
let k_dirty = custom_properties
.iter()
.filter(|property| property.guaranteed_invalid)
.count();
let acyclic_high_gain_pressure = if k_cycle == 0 {
query_acyclic_high_gain_coupling_pressure(&dependencies)
} else {
0
};
let after_k_decl = k_decl.saturating_add(acyclic_high_gain_pressure);
Some(OmenaCheckerRgFlowCouplingInputV0 {
workspace_path: "stylesheet://custom-property-coupling".to_string(),
before: OmenaCheckerRgFlowCouplingSpaceInputV0 {
k_env,
k_decl,
k_cycle: 0,
k_dirty: 0,
},
after: OmenaCheckerRgFlowCouplingSpaceInputV0 {
k_env,
k_decl: after_k_decl,
k_cycle,
k_dirty,
},
})
}
fn query_acyclic_high_gain_coupling_pressure(
dependencies: &BTreeMap<&str, BTreeSet<&str>>,
) -> usize {
let mut fanout_by_dependency = BTreeMap::<&str, usize>::new();
for edges in dependencies.values() {
for dependency in edges {
*fanout_by_dependency.entry(*dependency).or_default() += 1;
}
}
fanout_by_dependency
.values()
.filter(|count| **count >= 3)
.map(|count| count.saturating_mul(count.saturating_sub(1)) / 2)
.sum()
}
fn query_custom_property_in_reference_cycle(
start: &str,
dependencies: &BTreeMap<&str, BTreeSet<&str>>,
) -> bool {
let mut stack = dependencies
.get(start)
.map(|edges| edges.iter().copied().collect::<Vec<_>>())
.unwrap_or_default();
let mut visited = BTreeSet::new();
while let Some(node) = stack.pop() {
if node == start {
return true;
}
if !visited.insert(node) {
continue;
}
if let Some(edges) = dependencies.get(node) {
stack.extend(edges.iter().copied());
}
}
false
}
fn query_cascade_checker_code(code: &'static str) -> &'static str {
match code {
"unreachable-declaration" => "unreachableDeclaration",
"dead-cascade-layer" => "deadCascadeLayer",
"iacvt-prone" => "iacvtProne",
"circular-var" => "circularVar",
"unspecified-cascade-tie" => "unspecifiedCascadeTie",
"designer-intent-inconsistency" => "designerIntentInconsistency",
_ => "cascadeAware",
}
}
fn query_cascade_checker_diagnostic_severity(code: &'static str) -> &'static str {
match code {
"unreachable-declaration" | "dead-cascade-layer" | "designer-intent-inconsistency" => {
"hint"
}
_ => "warning",
}
}
fn query_cascade_checker_diagnostic_tags(code: &'static str) -> Vec<u8> {
match code {
"unreachable-declaration" | "dead-cascade-layer" => {
vec![LSP_DIAGNOSTIC_TAG_UNNECESSARY]
}
_ => Vec::new(),
}
}
fn collect_query_checker_cascade_input(
style_uri: &str,
source: &str,
) -> (
OmenaCheckerCascadeInputV0,
BTreeMap<String, ParserRangeV0>,
BTreeMap<String, ParserRangeV0>,
) {
let declarations = collect_query_checker_cascade_declarations(source);
let declaration_ranges = declarations
.iter()
.map(|declaration| {
(
declaration.input.declaration_id.clone(),
parser_range_for_byte_span(source, declaration.byte_span),
)
})
.collect::<BTreeMap<_, _>>();
let dialect = omena_parser_dialect_for_style_path(style_uri);
let guaranteed_invalid_custom_properties =
summarize_static_css_custom_property_fixed_point_from_source(source, dialect)
.entries
.into_iter()
.filter(|entry| entry.guaranteed_invalid)
.map(|entry| entry.name)
.collect::<BTreeSet<_>>();
let mut custom_properties_by_name =
BTreeMap::<String, (BTreeSet<String>, bool, ParserByteSpanV0)>::new();
for declaration in &declarations {
if !declaration.input.property.starts_with("--") {
continue;
}
let entry = custom_properties_by_name
.entry(declaration.input.property.clone())
.or_insert_with(|| {
(
BTreeSet::new(),
guaranteed_invalid_custom_properties.contains(&declaration.input.property),
declaration.byte_span,
)
});
entry.1 |= guaranteed_invalid_custom_properties.contains(&declaration.input.property);
for dependency in collect_query_var_references_in_value(&declaration.input.value) {
entry.0.insert(dependency);
}
}
let custom_property_ranges = custom_properties_by_name
.iter()
.map(|(name, (_, _, byte_span))| {
(name.clone(), parser_range_for_byte_span(source, *byte_span))
})
.collect::<BTreeMap<_, _>>();
let custom_properties = custom_properties_by_name
.into_iter()
.map(
|(name, (dependencies, guaranteed_invalid, _))| OmenaCheckerCustomPropertyInputV0 {
name,
dependencies: dependencies.into_iter().collect(),
guaranteed_invalid,
},
)
.collect::<Vec<_>>();
(
OmenaCheckerCascadeInputV0 {
declarations: declarations
.into_iter()
.map(|declaration| declaration.input)
.collect(),
custom_properties,
},
declaration_ranges,
custom_property_ranges,
)
}
pub(super) fn collect_query_replica_ensemble_site_outcomes(
source: &str,
) -> Vec<ReplicaSiteOutcomeV0> {
let declarations = collect_query_checker_cascade_declarations(source);
let mut by_site: BTreeMap<(String, String), Vec<CascadeDeclaration>> = BTreeMap::new();
for declaration in &declarations {
let property = declaration.input.property.as_str();
if property.starts_with("--") {
continue;
}
let cascade_declaration = query_cascade_declaration_from_input(&declaration.input);
by_site
.entry((
declaration.input.selector.clone(),
declaration.input.property.clone(),
))
.or_default()
.push(cascade_declaration);
}
by_site
.into_iter()
.filter_map(|((selector, property), site_declarations)| {
let outcome = cascade_property(site_declarations, &property);
if !matches!(outcome, omena_cascade::CascadeOutcome::Definite { .. }) {
return None;
}
Some(ReplicaSiteOutcomeV0 {
schema_version: REPLICA_ENSEMBLE_SCHEMA_VERSION_V0,
product: "omena-ensemble.replica-site-outcome",
layer_marker: REPLICA_ENSEMBLE_LAYER_MARKER_V0,
feature_gate: REPLICA_ENSEMBLE_FEATURE_GATE_V0,
site: replica_ensemble_site(selector, property),
outcome,
provenance: None,
})
})
.collect()
}
fn query_cascade_declaration_from_input(
input: &OmenaCheckerCascadeDeclarationInputV0,
) -> CascadeDeclaration {
let level = if input.important {
CascadeLevel::AuthorImportant
} else {
CascadeLevel::AuthorNormal
};
let layer_rank = LayerRank(input.layer_order.unwrap_or(0));
let specificity = parse_simple_selector_signature(&input.selector)
.map(|signature| signature.specificity)
.unwrap_or(Specificity::ZERO);
let value = input.value.trim().to_string();
CascadeDeclaration {
id: value.clone(),
property: input.property.clone(),
value: CascadeValue::Literal(value),
key: CascadeKey::new(level, layer_rank, 0, specificity, input.source_order),
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct QueryCheckerCascadeDeclaration {
input: OmenaCheckerCascadeDeclarationInputV0,
byte_span: ParserByteSpanV0,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct QueryCheckerCascadeScope {
condition_context: Vec<String>,
layer_name: Option<String>,
layer_order: Option<i32>,
}
fn collect_query_checker_cascade_declarations(source: &str) -> Vec<QueryCheckerCascadeDeclaration> {
let mut declarations = Vec::new();
let mut layer_orders = BTreeMap::new();
let mut next_layer_order = 0i32;
collect_query_checker_cascade_blocks(
source,
0,
source.len(),
None,
Vec::new(),
None,
None,
&mut layer_orders,
&mut next_layer_order,
&mut declarations,
);
declarations
}
#[allow(clippy::too_many_arguments)]
fn collect_query_checker_cascade_blocks(
source: &str,
start: usize,
end: usize,
parent_selector: Option<String>,
condition_context: Vec<String>,
layer_name: Option<String>,
layer_order: Option<i32>,
layer_orders: &mut BTreeMap<String, i32>,
next_layer_order: &mut i32,
declarations: &mut Vec<QueryCheckerCascadeDeclaration>,
) {
let mut index = start;
while let Some(open_index) = find_query_top_level_byte(source, index, end, b'{') {
let Some(close_index) = matching_query_block_end(source, open_index, end) else {
break;
};
let prelude_start = query_prelude_start(source, start, open_index);
let prelude = source[prelude_start..open_index].trim();
let body_start = open_index + 1;
if let Some(layer) = query_layer_name_from_prelude(prelude) {
let order = *layer_orders.entry(layer.clone()).or_insert_with(|| {
let order = *next_layer_order;
*next_layer_order += 1;
order
});
collect_query_checker_cascade_blocks(
source,
body_start,
close_index,
parent_selector.clone(),
condition_context.clone(),
Some(layer),
Some(order),
layer_orders,
next_layer_order,
declarations,
);
} else if let Some(at_root_selector) = query_at_root_selector_from_prelude(prelude) {
let mut canonical_members = Vec::new();
for member in split_query_selector_list(&at_root_selector) {
let canonical_selector = canonical_query_checker_selector(None, &member);
if !canonical_members.contains(&canonical_selector) {
canonical_members.push(canonical_selector);
}
}
for canonical_selector in canonical_members {
collect_query_checker_direct_declarations(
source,
body_start,
close_index,
&canonical_selector,
QueryCheckerCascadeScope {
condition_context: condition_context.clone(),
layer_name: layer_name.clone(),
layer_order,
},
declarations,
);
collect_query_checker_cascade_blocks(
source,
body_start,
close_index,
Some(canonical_selector),
condition_context.clone(),
layer_name.clone(),
layer_order,
layer_orders,
next_layer_order,
declarations,
);
}
} else if prelude.starts_with('@') {
let mut nested_condition_context = condition_context.clone();
nested_condition_context.push(normalize_query_condition_prelude(prelude));
collect_query_checker_cascade_blocks(
source,
body_start,
close_index,
parent_selector.clone(),
nested_condition_context,
layer_name.clone(),
layer_order,
layer_orders,
next_layer_order,
declarations,
);
} else if !prelude.is_empty() {
let mut canonical_members = Vec::new();
for member in split_query_selector_list(prelude) {
let canonical_selector =
canonical_query_checker_selector(parent_selector.as_deref(), &member);
if !canonical_members.contains(&canonical_selector) {
canonical_members.push(canonical_selector);
}
}
for canonical_selector in canonical_members {
collect_query_checker_direct_declarations(
source,
body_start,
close_index,
&canonical_selector,
QueryCheckerCascadeScope {
condition_context: condition_context.clone(),
layer_name: layer_name.clone(),
layer_order,
},
declarations,
);
collect_query_checker_cascade_blocks(
source,
body_start,
close_index,
Some(canonical_selector),
condition_context.clone(),
layer_name.clone(),
layer_order,
layer_orders,
next_layer_order,
declarations,
);
}
}
index = close_index + 1;
}
}
fn split_query_selector_list(prelude: &str) -> Vec<String> {
let mut members = Vec::new();
let mut segment_start = 0usize;
let mut index = 0usize;
let mut quote: Option<char> = None;
let mut paren_depth = 0usize;
let mut bracket_depth = 0usize;
while index < prelude.len() {
let Some(ch) = prelude[index..].chars().next() else {
break;
};
if let Some(quote_ch) = quote {
index += ch.len_utf8();
if ch == '\\' {
if let Some(escaped) = prelude[index..].chars().next() {
index += escaped.len_utf8();
}
} else if ch == quote_ch {
quote = None;
}
continue;
}
match ch {
'"' | '\'' => quote = Some(ch),
'(' => paren_depth += 1,
')' => paren_depth = paren_depth.saturating_sub(1),
'[' => bracket_depth += 1,
']' => bracket_depth = bracket_depth.saturating_sub(1),
',' if paren_depth == 0 && bracket_depth == 0 => {
let member = prelude[segment_start..index].trim();
if !member.is_empty() {
members.push(member.to_string());
}
segment_start = index + ch.len_utf8();
}
_ => {}
}
index += ch.len_utf8();
}
let tail = prelude[segment_start..].trim();
if !tail.is_empty() {
members.push(tail.to_string());
}
if members.is_empty() {
members.push(prelude.trim().to_string());
}
members
}
fn canonical_query_checker_selector(parent_selector: Option<&str>, selector: &str) -> String {
let selector = selector.trim();
match parent_selector {
Some(parent_selector) => expand_css_nested_selector(parent_selector, selector)
.unwrap_or_else(|| fallback_expand_query_nested_selector(parent_selector, selector)),
None => selector.to_string(),
}
}
fn fallback_expand_query_nested_selector(parent_selector: &str, selector: &str) -> String {
if selector.contains('&') {
selector.replace('&', parent_selector)
} else {
format!("{parent_selector} {selector}")
}
}
fn collect_query_checker_direct_declarations(
source: &str,
body_start: usize,
body_end: usize,
selector: &str,
scope: QueryCheckerCascadeScope,
declarations: &mut Vec<QueryCheckerCascadeDeclaration>,
) {
let mut statement_start = body_start;
let mut index = body_start;
while index < body_end {
if let Some(open_index) = find_query_top_level_byte(source, index, body_end, b'{') {
while let Some(semicolon_index) =
find_query_top_level_byte(source, index, open_index, b';')
{
push_query_checker_declaration(
source,
statement_start,
semicolon_index,
selector,
&scope,
declarations,
);
statement_start = semicolon_index + 1;
index = statement_start;
}
let Some(close_index) = matching_query_block_end(source, open_index, body_end) else {
break;
};
statement_start = close_index + 1;
index = statement_start;
continue;
}
while let Some(semicolon_index) = find_query_top_level_byte(source, index, body_end, b';') {
push_query_checker_declaration(
source,
statement_start,
semicolon_index,
selector,
&scope,
declarations,
);
statement_start = semicolon_index + 1;
index = statement_start;
}
break;
}
push_query_checker_declaration(
source,
statement_start,
body_end,
selector,
&scope,
declarations,
);
}
fn push_query_checker_declaration(
source: &str,
start: usize,
end: usize,
selector: &str,
scope: &QueryCheckerCascadeScope,
declarations: &mut Vec<QueryCheckerCascadeDeclaration>,
) {
let Some((trimmed_start, trimmed_end)) = trimmed_query_span(source, start, end) else {
return;
};
let raw_statement = &source[trimmed_start..trimmed_end];
let statement = strip_query_statement_comments(raw_statement);
let statement = statement.as_str();
let Some(colon_offset) = find_query_top_level_colon(statement) else {
return;
};
let property = statement[..colon_offset].trim();
if property.is_empty()
|| property.starts_with('@')
|| property.starts_with('$')
|| property.contains(char::is_whitespace)
|| property.contains('{')
|| property.contains('}')
{
return;
}
let mut value = statement[colon_offset + 1..].trim().to_string();
let important = query_value_has_important_suffix(&value);
if important {
value = value
.trim_end()
.trim_end_matches(|ch: char| ch.is_ascii_whitespace())
.trim_end_matches("!important")
.trim_end()
.to_string();
}
let source_order = declarations.len();
let declaration_id = format!("decl-{source_order}");
declarations.push(QueryCheckerCascadeDeclaration {
input: OmenaCheckerCascadeDeclarationInputV0 {
declaration_id,
selector: selector.to_string(),
property: property.to_string(),
value: value.clone(),
source_order: source_order.min(u32::MAX as usize) as u32,
condition_context: scope.condition_context.clone(),
layer_name: scope.layer_name.clone(),
layer_order: scope.layer_order,
important,
var_references: collect_query_var_references_in_value(&value),
},
byte_span: ParserByteSpanV0 {
start: trimmed_start,
end: trimmed_end,
},
});
}
fn query_value_has_important_suffix(value: &str) -> bool {
value
.trim_end()
.to_ascii_lowercase()
.ends_with("!important")
}
fn trimmed_query_span(source: &str, start: usize, end: usize) -> Option<(usize, usize)> {
let mut trimmed_start = start;
let mut trimmed_end = end;
while trimmed_start < trimmed_end
&& source[trimmed_start..]
.chars()
.next()
.is_some_and(char::is_whitespace)
{
trimmed_start += source[trimmed_start..].chars().next()?.len_utf8();
}
while trimmed_end > trimmed_start
&& source[..trimmed_end]
.chars()
.next_back()
.is_some_and(char::is_whitespace)
{
trimmed_end -= source[..trimmed_end].chars().next_back()?.len_utf8();
}
(trimmed_start < trimmed_end).then_some((trimmed_start, trimmed_end))
}
fn strip_query_statement_comments(statement: &str) -> String {
let mut out = String::with_capacity(statement.len());
let mut index = 0usize;
let mut quote: Option<char> = None;
let mut paren_depth = 0usize;
while index < statement.len() {
let Some(ch) = statement[index..].chars().next() else {
break;
};
if let Some(quote_ch) = quote {
out.push(ch);
index += ch.len_utf8();
if ch == '\\' {
if let Some(escaped) = statement[index..].chars().next() {
out.push(escaped);
index += escaped.len_utf8();
}
} else if ch == quote_ch {
quote = None;
}
continue;
}
if statement[index..].starts_with("/*") {
match statement[index + 2..].find("*/") {
Some(close_offset) => {
out.push(' ');
index += close_offset + 4;
}
None => break,
}
continue;
}
if paren_depth == 0 && statement[index..].starts_with("//") {
match statement[index..].find('\n') {
Some(newline_offset) => {
out.push('\n');
index += newline_offset + 1;
}
None => break,
}
continue;
}
match ch {
'"' | '\'' => quote = Some(ch),
'(' => paren_depth += 1,
')' => paren_depth = paren_depth.saturating_sub(1),
_ => {}
}
out.push(ch);
index += ch.len_utf8();
}
out
}
fn find_query_top_level_colon(statement: &str) -> Option<usize> {
let mut index = 0usize;
let mut quote: Option<char> = None;
let mut paren_depth = 0usize;
while index < statement.len() {
let ch = statement[index..].chars().next()?;
if let Some(quote_ch) = quote {
index += ch.len_utf8();
if ch == '\\' {
if let Some(escaped) = statement[index..].chars().next() {
index += escaped.len_utf8();
}
} else if ch == quote_ch {
quote = None;
}
continue;
}
match ch {
'"' | '\'' => {
quote = Some(ch);
index += ch.len_utf8();
}
'(' => {
paren_depth += 1;
index += ch.len_utf8();
}
')' => {
paren_depth = paren_depth.saturating_sub(1);
index += ch.len_utf8();
}
':' if paren_depth == 0 => return Some(index),
_ => index += ch.len_utf8(),
}
}
None
}
fn query_prelude_start(source: &str, search_start: usize, open_index: usize) -> usize {
source[search_start..open_index]
.rfind(['{', '}', ';'])
.map(|offset| search_start + offset + 1)
.unwrap_or(search_start)
}
fn query_layer_name_from_prelude(prelude: &str) -> Option<String> {
let rest = prelude.trim_start().strip_prefix("@layer")?.trim();
let name = rest
.split(|ch: char| ch.is_ascii_whitespace() || matches!(ch, ',' | '{' | ';'))
.next()
.unwrap_or_default()
.trim_matches(['"', '\'']);
if name.is_empty() {
Some("(anonymous-layer)".to_string())
} else {
Some(name.to_string())
}
}
fn normalize_query_condition_prelude(prelude: &str) -> String {
prelude.split_whitespace().collect::<Vec<_>>().join(" ")
}
fn query_at_root_selector_from_prelude(prelude: &str) -> Option<String> {
let rest = prelude.trim_start().strip_prefix("@at-root")?;
if let Some(next) = rest.chars().next()
&& !next.is_ascii_whitespace()
{
return None;
}
let selector = rest.trim();
if selector.is_empty() || selector.starts_with('(') {
return None;
}
Some(selector.to_string())
}
fn collect_query_var_references_in_value(value: &str) -> Vec<String> {
let mut refs = BTreeSet::new();
let mut index = 0usize;
let mut quote: Option<char> = None;
while index < value.len() {
let Some(ch) = value[index..].chars().next() else {
break;
};
if let Some(quote_ch) = quote {
index += ch.len_utf8();
if ch == '\\' {
if let Some(escaped) = value[index..].chars().next() {
index += escaped.len_utf8();
}
} else if ch == quote_ch {
quote = None;
}
continue;
}
match ch {
'"' | '\'' => {
quote = Some(ch);
index += ch.len_utf8();
}
_ if query_function_name_starts_at(value, index, "var") => {
let open_index = index + "var".len();
let Some(close_index) = matching_query_paren_end(value, open_index, value.len())
else {
index += ch.len_utf8();
continue;
};
collect_query_var_references_from_arguments(
&value[open_index + 1..close_index],
&mut refs,
);
index = close_index + 1;
}
_ => {
index += ch.len_utf8();
}
}
}
refs.into_iter().collect()
}
fn collect_query_var_references_from_arguments(arguments: &str, refs: &mut BTreeSet<String>) {
let parts = split_query_top_level_arguments(arguments);
let Some(first_argument) = parts.first().map(|part| part.trim()) else {
return;
};
if first_argument.starts_with("--") {
refs.insert(first_argument.to_string());
}
for fallback in parts.iter().skip(1) {
for reference in collect_query_var_references_in_value(fallback) {
refs.insert(reference);
}
}
}
fn split_query_top_level_arguments(arguments: &str) -> Vec<&str> {
let mut parts = Vec::new();
let mut start = 0usize;
let mut index = 0usize;
let mut quote: Option<char> = None;
let mut paren_depth = 0usize;
while index < arguments.len() {
let Some(ch) = arguments[index..].chars().next() else {
break;
};
if let Some(quote_ch) = quote {
index += ch.len_utf8();
if ch == '\\' {
if let Some(escaped) = arguments[index..].chars().next() {
index += escaped.len_utf8();
}
} else if ch == quote_ch {
quote = None;
}
continue;
}
match ch {
'"' | '\'' => {
quote = Some(ch);
index += ch.len_utf8();
}
'(' => {
paren_depth += 1;
index += ch.len_utf8();
}
')' => {
paren_depth = paren_depth.saturating_sub(1);
index += ch.len_utf8();
}
',' if paren_depth == 0 => {
parts.push(&arguments[start..index]);
index += ch.len_utf8();
start = index;
}
_ => {
index += ch.len_utf8();
}
}
}
parts.push(&arguments[start..]);
parts
}
fn query_function_name_starts_at(value: &str, index: usize, function_name: &str) -> bool {
value
.get(index..index + function_name.len())
.is_some_and(|name| name.eq_ignore_ascii_case(function_name))
&& value[index + function_name.len()..].starts_with('(')
}
fn find_query_top_level_byte(source: &str, start: usize, end: usize, needle: u8) -> Option<usize> {
let mut index = start;
let mut quote: Option<char> = None;
let mut paren_depth = 0usize;
while index < end {
let ch = source[index..].chars().next()?;
if let Some(quote_ch) = quote {
index += ch.len_utf8();
if ch == '\\' {
if let Some(escaped) = source[index..].chars().next() {
index += escaped.len_utf8();
}
} else if ch == quote_ch {
quote = None;
}
continue;
}
if source[index..].starts_with("/*")
&& let Some(close_offset) = source[index + 2..end].find("*/")
{
index += close_offset + 4;
continue;
}
if paren_depth == 0 && source[index..end].starts_with("//") {
match source[index..end].find('\n') {
Some(newline_offset) => {
index += newline_offset + 1;
continue;
}
None => return None,
}
}
if ch.len_utf8() == 1 && source.as_bytes()[index] == needle {
return Some(index);
}
match ch {
'"' | '\'' => {
quote = Some(ch);
index += ch.len_utf8();
}
'(' => {
paren_depth += 1;
index += ch.len_utf8();
}
')' => {
paren_depth = paren_depth.saturating_sub(1);
index += ch.len_utf8();
}
_ => index += ch.len_utf8(),
}
}
None
}
fn matching_query_block_end(source: &str, open_index: usize, end: usize) -> Option<usize> {
matching_query_delimiter_end(source, open_index, end, b'{', b'}')
}
fn matching_query_paren_end(source: &str, open_index: usize, end: usize) -> Option<usize> {
matching_query_delimiter_end(source, open_index, end, b'(', b')')
}
fn matching_query_delimiter_end(
source: &str,
open_index: usize,
end: usize,
open: u8,
close: u8,
) -> Option<usize> {
if source.as_bytes().get(open_index).copied()? != open {
return None;
}
let mut index = open_index + 1;
let mut depth = 1usize;
let mut quote: Option<char> = None;
let track_line_comments = open == b'{';
let mut inner_paren_depth = 0usize;
while index < end {
let ch = source[index..].chars().next()?;
if let Some(quote_ch) = quote {
index += ch.len_utf8();
if ch == '\\' {
if let Some(escaped) = source[index..].chars().next() {
index += escaped.len_utf8();
}
} else if ch == quote_ch {
quote = None;
}
continue;
}
if source[index..].starts_with("/*")
&& let Some(close_offset) = source[index + 2..end].find("*/")
{
index += close_offset + 4;
continue;
}
if track_line_comments && inner_paren_depth == 0 && source[index..end].starts_with("//") {
match source[index..end].find('\n') {
Some(newline_offset) => {
index += newline_offset + 1;
continue;
}
None => return None,
}
}
if track_line_comments {
match ch {
'(' => inner_paren_depth += 1,
')' => inner_paren_depth = inner_paren_depth.saturating_sub(1),
_ => {}
}
}
match ch {
'"' | '\'' => {
quote = Some(ch);
index += ch.len_utf8();
}
_ if ch.len_utf8() == 1 && source.as_bytes()[index] == open => {
depth += 1;
index += 1;
}
_ if ch.len_utf8() == 1 && source.as_bytes()[index] == close => {
depth -= 1;
if depth == 0 {
return Some(index);
}
index += 1;
}
_ => index += ch.len_utf8(),
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
fn recorded(source: &str) -> Vec<(String, String, String)> {
collect_query_checker_cascade_declarations(source)
.into_iter()
.map(|declaration| {
(
declaration.input.selector,
declaration.input.property,
declaration.input.value,
)
})
.collect()
}
fn diagnostic_codes(source: &str) -> Vec<&'static str> {
summarize_query_cascade_checker_diagnostics_with_deep_analysis(
"file:///tmp/test.scss",
source,
false,
)
.into_iter()
.map(|diagnostic| diagnostic.code)
.collect()
}
fn diagnostic_codes_with_deep_analysis(source: &str, deep_analysis: bool) -> Vec<&'static str> {
summarize_query_cascade_checker_diagnostics_with_deep_analysis(
"file:///tmp/test.scss",
source,
deep_analysis,
)
.into_iter()
.map(|diagnostic| diagnostic.code)
.collect()
}
fn cascade_codes(source: &str) -> Vec<&'static str> {
diagnostic_codes(source)
.into_iter()
.filter(|code| matches!(*code, "unreachableDeclaration" | "unspecifiedCascadeTie"))
.collect()
}
#[test]
fn b1_block_comment_before_property_does_not_drop_declaration() {
let recorded = recorded(".a { /* primary */ color: red; color: blue; }");
let properties: Vec<_> = recorded.iter().map(|(_, property, _)| property).collect();
assert_eq!(properties, vec!["color", "color"], "{recorded:?}");
}
#[test]
fn b1_block_comment_repro_fires_tie_and_unreachable() {
let cascade = cascade_codes(".a { /* primary */ color: red; color: blue; }");
assert!(
cascade.contains(&"unreachableDeclaration")
&& cascade.contains(&"unspecifiedCascadeTie"),
"expected both cascade diagnostics, got {cascade:?}"
);
}
#[test]
fn b1_line_comment_before_declarations_does_not_drop_them() {
let cascade = cascade_codes(".a { // primary\ncolor: red; color: blue; }");
assert!(
cascade.contains(&"unreachableDeclaration")
&& cascade.contains(&"unspecifiedCascadeTie"),
"expected both cascade diagnostics, got {cascade:?}"
);
}
#[test]
fn b1_value_comment_is_stripped_but_property_survives() {
let recorded = recorded(".a { color /* c */ : red /* d */; }");
assert_eq!(
recorded,
vec![(".a".to_string(), "color".to_string(), "red".to_string())],
"comment-laden declaration should still record cleanly"
);
}
#[test]
fn b1_line_commented_out_declaration_is_not_analyzed_as_live() {
let cascade = cascade_codes(".a { color: red; // color: blue;\n}");
assert!(
cascade.is_empty(),
"commented-out decl must not tie: {cascade:?}"
);
}
#[test]
fn b1_block_commented_out_declaration_is_not_analyzed_as_live() {
let cascade = cascade_codes(".a { color: red; /* color: blue; */ }");
assert!(
cascade.is_empty(),
"commented-out decl must not tie: {cascade:?}"
);
}
#[test]
fn b1_url_with_double_slash_value_is_preserved_and_later_tie_still_fires() {
let source = ".a { background: url(http://example.com/a.png); color: red; color: blue; }";
let recorded = recorded(source);
assert!(
recorded
.iter()
.any(|(_, property, value)| property == "background"
&& value == "url(http://example.com/a.png)"),
"url value should survive intact: {recorded:?}"
);
let cascade = cascade_codes(source);
assert!(
cascade.contains(&"unspecifiedCascadeTie"),
"later real tie should still fire: {cascade:?}"
);
}
#[test]
fn b2_selector_list_member_records_separately() {
let recorded = recorded(".a, .b { color: red; }");
let selectors: Vec<_> = recorded.iter().map(|(selector, ..)| selector).collect();
assert_eq!(selectors, vec![".a", ".b"], "{recorded:?}");
}
#[test]
fn b2_selector_list_member_ties_with_sibling_rule() {
let cascade = cascade_codes(".a, .b { color: red; }\n.a { color: blue; }");
assert!(
cascade.contains(&"unreachableDeclaration")
&& cascade.contains(&"unspecifiedCascadeTie"),
"list member .a should tie with .a sibling: {cascade:?}"
);
}
#[test]
fn b2_distinct_list_member_does_not_tie_with_unrelated_rule() {
let cascade = cascade_codes(".a, .b { color: red; }\n.c { color: blue; }");
assert!(
cascade.is_empty(),
"unrelated rule must not tie: {cascade:?}"
);
}
#[test]
fn b2_duplicate_member_in_one_prelude_is_deduplicated() {
let recorded = recorded(".a, .a { color: red; }");
assert_eq!(
recorded.len(),
1,
"identical members must be de-duplicated: {recorded:?}"
);
let cascade = cascade_codes(".a, .a { color: red; }");
assert!(
cascade.is_empty(),
"deduped member must not self-tie: {cascade:?}"
);
}
#[test]
fn b2_comma_inside_functional_pseudo_is_not_split() {
let recorded = recorded(":is(.a, .b) { color: red; }");
let selectors: Vec<_> = recorded.iter().map(|(selector, ..)| selector).collect();
assert_eq!(selectors, vec![":is(.a, .b)"], "{recorded:?}");
}
const VAR_CYCLE_SOURCE: &str = ":root { --a: var(--b); --b: var(--a); }";
#[test]
fn wp7b_var_cycle_still_fires_circular_var_warning() {
for deep_analysis in [false, true] {
let codes = diagnostic_codes_with_deep_analysis(VAR_CYCLE_SOURCE, deep_analysis);
assert!(
codes.contains(&"circularVar"),
"circularVar must still fire on a real var cycle (deep_analysis={deep_analysis}): {codes:?}"
);
}
}
#[test]
fn wp7b_default_surface_var_cycle_emits_only_circular_var() {
let codes = diagnostic_codes(VAR_CYCLE_SOURCE);
assert!(
codes.contains(&"circularVar"),
"circularVar must fire on the default surface: {codes:?}"
);
assert!(
!codes.contains(&"rgFlowRelevantOperator"),
"rg-flow theory hint must be OFF by default: {codes:?}"
);
assert!(
!codes.contains(&"categoricalCascadeEvidenceInconsistency"),
"categorical theory hint must be OFF by default: {codes:?}"
);
assert!(
codes.iter().all(|code| !matches!(
*code,
"rgFlowRelevantOperator" | "categoricalCascadeEvidenceInconsistency"
)),
"default surface must surface no theory hints for a lone var cycle: {codes:?}"
);
}
#[test]
fn wp7b_deep_analysis_dedups_theory_hints_into_circular_var() -> Result<(), &'static str> {
let codes = diagnostic_codes_with_deep_analysis(VAR_CYCLE_SOURCE, true);
assert!(
codes.contains(&"circularVar"),
"circularVar must fire with deep analysis ON: {codes:?}"
);
assert!(
!codes.contains(&"rgFlowRelevantOperator"),
"rg-flow hint must be deduplicated against circularVar: {codes:?}"
);
assert!(
!codes.contains(&"categoricalCascadeEvidenceInconsistency"),
"categorical hint must be deduplicated against circularVar: {codes:?}"
);
let diagnostics = summarize_query_cascade_checker_diagnostics_with_deep_analysis(
"file:///tmp/test.scss",
VAR_CYCLE_SOURCE,
true,
);
let circular_var = diagnostics
.iter()
.find(|diagnostic| diagnostic.code == "circularVar")
.ok_or("circularVar diagnostic must exist")?;
assert!(
circular_var
.provenance
.iter()
.any(|label| label.contains("rg-flow")),
"rg-flow provenance should be folded into circularVar: {:?}",
circular_var.provenance
);
assert!(
circular_var
.provenance
.iter()
.any(|label| label.contains("categorical")),
"categorical provenance should be folded into circularVar: {:?}",
circular_var.provenance
);
Ok(())
}
#[test]
fn wp7b_deep_analysis_reaches_theory_gate_on_cyclic_input() {
let (checker_input, _, _) =
collect_query_checker_cascade_input("file:///tmp/test.scss", VAR_CYCLE_SOURCE);
let rg_flow = summarize_query_rg_flow_coupling_diagnostics(
VAR_CYCLE_SOURCE,
&checker_input.custom_properties,
);
let categorical = summarize_query_categorical_cascade_evidence_diagnostics(
VAR_CYCLE_SOURCE,
&checker_input.custom_properties,
);
assert!(
!rg_flow.is_empty(),
"rg-flow theory gate should fire on a cyclic stylesheet when reached"
);
assert!(
!categorical.is_empty(),
"categorical theory gate should fire on a cyclic stylesheet when reached"
);
}
#[test]
fn wp7b_acyclic_stylesheet_emits_no_theory_hints_even_with_deep_analysis() {
let acyclic = ":root { --a: 1px; --b: var(--a); }";
let codes = diagnostic_codes_with_deep_analysis(acyclic, true);
assert!(
!codes.contains(&"rgFlowRelevantOperator")
&& !codes.contains(&"categoricalCascadeEvidenceInconsistency"),
"acyclic stylesheet must not surface theory hints: {codes:?}"
);
}
#[test]
fn wp7b_acyclic_high_gain_hub_surfaces_standalone_rg_flow_hint() {
let high_gain = r#"
:root {
--seed: 1px;
--a: var(--seed);
--b: var(--seed);
--c: var(--seed);
--d: var(--seed);
}
"#;
let default_codes = diagnostic_codes_with_deep_analysis(high_gain, false);
assert!(
!default_codes.contains(&"rgFlowRelevantOperator"),
"rg-flow theory hint must stay off on the default surface: {default_codes:?}"
);
let deep_codes = diagnostic_codes_with_deep_analysis(high_gain, true);
assert!(
deep_codes.contains(&"rgFlowRelevantOperator"),
"acyclic high-gain hub should surface a standalone rg-flow hint: {deep_codes:?}"
);
assert!(
!deep_codes.contains(&"circularVar"),
"standalone rg-flow hint must not depend on circularVar: {deep_codes:?}"
);
}
}