Skip to main content

omena_query/style/diagnostics/
single_file.rs

1use super::shared::*;
2
3pub fn summarize_omena_query_missing_custom_property_diagnostics(
4    style_uri: &str,
5    source: &str,
6    candidates: &[OmenaQueryStyleHoverCandidateV0],
7) -> Vec<OmenaQueryStyleDiagnosticV0> {
8    let declaration_names = candidates
9        .iter()
10        .filter(|candidate| candidate.kind == "customPropertyDeclaration")
11        .map(|candidate| candidate.name.as_str())
12        .collect::<BTreeSet<_>>();
13    if declaration_names.is_empty() {
14        return Vec::new();
15    }
16
17    // `var(--x, fallback)` references cannot be "missing" in any observable way — the
18    // fallback guarantees a value — so suppress the lint per-reference. The fallback fact
19    // range and the candidate range both derive from the same parser byte span via
20    // `parser_range_for_byte_span`, so matching on the rendered range scopes the suppression
21    // to the exact `var()` argument (a nested fallback-less `var(--b)` in
22    // `var(--a, var(--b))` stays a live candidate).
23    let dialect = omena_parser_dialect_for_style_path(style_uri);
24    let facts = collect_omena_query_omena_parser_style_facts_raw(source, dialect);
25    let fallback_ranges = facts
26        .variables
27        .iter()
28        .filter(|fact| {
29            fact.kind == ParsedVariableFactKind::CustomPropertyReference && fact.has_fallback
30        })
31        .map(|fact| {
32            let byte_span = ParserByteSpanV0 {
33                start: u32::from(fact.range.start()) as usize,
34                end: u32::from(fact.range.end()) as usize,
35            };
36            (
37                fact.name.clone(),
38                parser_range_for_byte_span(source, byte_span),
39            )
40        })
41        .collect::<BTreeSet<_>>();
42
43    let insertion_range = end_of_source_range(source);
44    candidates
45        .iter()
46        .filter(|candidate| {
47            candidate.kind == "customPropertyReference"
48                && !declaration_names.contains(candidate.name.as_str())
49                && !fallback_ranges.contains(&(candidate.name.clone(), candidate.range))
50        })
51        .map(|candidate| OmenaQueryStyleDiagnosticV0 {
52            code: "missingCustomProperty",
53            severity: "warning",
54            provenance: omena_query_evidence_graph_provenance![
55                "omena-parser.custom-property-facts",
56                "omena-query.style-diagnostics",
57            ],
58            range: candidate.range,
59            message: format!(
60                "CSS custom property '{}' not found in indexed style tokens.",
61                candidate.name
62            ),
63            tags: Vec::new(),
64            create_custom_property: Some(OmenaQueryCreateCustomPropertyActionV0 {
65                uri: style_uri.to_string(),
66                range: insertion_range,
67                new_text: format!("\n\n:root {{\n  {}: ;\n}}\n", candidate.name),
68                property_name: candidate.name.clone(),
69            }),
70            cascade_narrowing: None,
71            cascade_confidence: None,
72            polynomial_provenance: None,
73            cross_file_scc: None,
74        })
75        .collect()
76}
77
78pub fn summarize_omena_query_cascade_aware_style_diagnostics(
79    style_uri: &str,
80    source: &str,
81    candidates: &[OmenaQueryStyleHoverCandidateV0],
82) -> Vec<OmenaQueryStyleDiagnosticV0> {
83    summarize_omena_query_cascade_aware_style_diagnostics_with_deep_analysis(
84        style_uri, source, candidates, false,
85    )
86}
87
88/// Cascade-aware diagnostics with an explicit opt-in deep-analysis switch. With
89/// `deep_analysis == false` (the default surface) only the product cascade gate
90/// diagnostics are emitted; `deep_analysis == true` additionally surfaces the
91/// rg-flow / categorical theory hints, deduplicated against `circularVar`.
92pub fn summarize_omena_query_cascade_aware_style_diagnostics_with_deep_analysis(
93    style_uri: &str,
94    source: &str,
95    candidates: &[OmenaQueryStyleHoverCandidateV0],
96    deep_analysis: bool,
97) -> Vec<OmenaQueryStyleDiagnosticV0> {
98    let declarations_by_name = candidates
99        .iter()
100        .filter(|candidate| candidate.kind == "customPropertyDeclaration")
101        .map(|candidate| (candidate.name.as_str(), candidate.range))
102        .collect::<BTreeMap<_, _>>();
103
104    let dialect = omena_parser_dialect_for_style_path(style_uri);
105    let mut diagnostics =
106        summarize_static_css_custom_property_fixed_point_from_source(source, dialect)
107            .entries
108            .into_iter()
109            .filter(|entry| entry.guaranteed_invalid)
110            .filter_map(|entry| {
111                declarations_by_name
112                    .get(entry.name.as_str())
113                    .copied()
114                    .map(|range| OmenaQueryStyleDiagnosticV0 {
115                        code: "guaranteedInvalidCustomProperty",
116                        severity: "warning",
117                        provenance: omena_query_evidence_graph_provenance![
118                            "omena-transform-passes.custom-property-lfp",
119                            "omena-query.cascade-aware-diagnostics",
120                        ],
121                        range,
122                        message: format!(
123                            "CSS custom property '{}' resolves to the guaranteed-invalid value.",
124                            entry.name
125                        ),
126                        tags: Vec::new(),
127                        create_custom_property: None,
128                        cascade_narrowing: None,
129                        cascade_confidence: None,
130                        polynomial_provenance: None,
131                        cross_file_scc: None,
132                    })
133            })
134            .collect::<Vec<_>>();
135
136    diagnostics.extend(
137        summarize_query_cascade_checker_diagnostics_with_deep_analysis(
138            style_uri,
139            source,
140            deep_analysis,
141        ),
142    );
143
144    diagnostics
145}
146
147pub fn summarize_omena_query_missing_keyframes_diagnostics(
148    style_uri: &str,
149    source: &str,
150) -> Vec<OmenaQueryStyleDiagnosticV0> {
151    let dialect = omena_parser_dialect_for_style_path(style_uri);
152    let facts = collect_omena_query_omena_parser_style_facts_raw(source, dialect);
153    let declared_keyframes = facts
154        .animations
155        .iter()
156        .filter(|animation| animation.kind == ParsedAnimationFactKind::KeyframesDeclaration)
157        .map(|animation| animation.name.clone())
158        .collect::<BTreeSet<_>>();
159    let mut emitted = BTreeSet::new();
160
161    facts
162        .animations
163        .into_iter()
164        .filter(|animation| animation.kind == ParsedAnimationFactKind::AnimationNameReference)
165        .filter(|animation| !declared_keyframes.contains(animation.name.as_str()))
166        .filter_map(|animation| {
167            let start: u32 = animation.range.start().into();
168            let end: u32 = animation.range.end().into();
169            let byte_span = ParserByteSpanV0 {
170                start: start as usize,
171                end: end as usize,
172            };
173            if !emitted.insert((animation.name.clone(), byte_span.start, byte_span.end)) {
174                return None;
175            }
176            Some((animation, parser_range_for_byte_span(source, byte_span)))
177        })
178        .map(|(animation, range)| OmenaQueryStyleDiagnosticV0 {
179            code: "missingKeyframes",
180            severity: "warning",
181            provenance: omena_query_evidence_graph_provenance![
182                "omena-parser.animation-facts",
183                "omena-query.style-diagnostics",
184            ],
185            range,
186            message: format!("@keyframes '{}' not found in this file.", animation.name),
187            tags: Vec::new(),
188            create_custom_property: None,
189            cascade_narrowing: None,
190            cascade_confidence: None,
191            polynomial_provenance: None,
192            cross_file_scc: None,
193        })
194        .collect()
195}