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        .filter_map(|candidate| candidate.property_key.clone())
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        .filter_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            Some((
37                fact.name.as_custom_property()?.to_custom_key(),
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_map(|candidate| {
47            if candidate.kind != "customPropertyReference" {
48                return None;
49            }
50            let property_key = candidate.property_key.as_ref()?;
51            if declaration_names.contains(property_key)
52                || fallback_ranges.contains(&(property_key.clone(), candidate.range))
53            {
54                return None;
55            }
56            let mut authored_name = String::new();
57            let _ = omena_syntax::ident::render_authored(&candidate.name, &mut authored_name);
58            Some(OmenaQueryStyleDiagnosticV0 {
59                code: "missingCustomProperty",
60                severity: "warning",
61                provenance: omena_query_evidence_graph_provenance![
62                    "omena-parser.custom-property-facts",
63                    "omena-query.style-diagnostics",
64                ],
65                range: candidate.range,
66                message: format!(
67                    "CSS custom property '{authored_name}' not found in indexed style tokens."
68                ),
69                tags: Vec::new(),
70                create_custom_property: Some(OmenaQueryCreateCustomPropertyActionV0 {
71                    uri: style_uri.to_string(),
72                    range: insertion_range,
73                    new_text: format!("\n\n:root {{\n  {authored_name}: ;\n}}\n"),
74                    property_name: candidate.name.clone(),
75                    property_key: property_key.clone(),
76                }),
77                cascade_narrowing: None,
78                cascade_confidence: None,
79                polynomial_provenance: None,
80                cross_file_scc: None,
81            })
82        })
83        .collect()
84}
85
86pub fn summarize_omena_query_cascade_aware_style_diagnostics(
87    style_uri: &str,
88    source: &str,
89    candidates: &[OmenaQueryStyleHoverCandidateV0],
90) -> Vec<OmenaQueryStyleDiagnosticV0> {
91    summarize_omena_query_cascade_aware_style_diagnostics_with_deep_analysis(
92        style_uri, source, candidates, false,
93    )
94}
95
96/// Cascade-aware diagnostics with an explicit opt-in deep-analysis switch. With
97/// `deep_analysis == false` (the default surface) only the product cascade gate
98/// diagnostics are emitted; `deep_analysis == true` additionally surfaces the
99/// multiscale-complexity-heuristic / categorical theory hints, deduplicated against `circularVar`.
100pub fn summarize_omena_query_cascade_aware_style_diagnostics_with_deep_analysis(
101    style_uri: &str,
102    source: &str,
103    candidates: &[OmenaQueryStyleHoverCandidateV0],
104    deep_analysis: bool,
105) -> Vec<OmenaQueryStyleDiagnosticV0> {
106    let declarations_by_name = candidates
107        .iter()
108        .filter(|candidate| candidate.kind == "customPropertyDeclaration")
109        .filter_map(|candidate| Some((candidate.property_key.clone()?, candidate.range)))
110        .collect::<BTreeMap<_, _>>();
111
112    let dialect = omena_parser_dialect_for_style_path(style_uri);
113    let mut diagnostics =
114        summarize_static_css_custom_property_fixed_point_from_source(source, dialect)
115            .entries
116            .into_iter()
117            .filter(|entry| entry.guaranteed_invalid)
118            .filter_map(|entry| {
119                declarations_by_name.get(&entry.name).copied().map(|range| {
120                    OmenaQueryStyleDiagnosticV0 {
121                        code: "guaranteedInvalidCustomProperty",
122                        severity: "warning",
123                        provenance: omena_query_evidence_graph_provenance![
124                            "omena-transform-passes.custom-property-lfp",
125                            "omena-query.cascade-aware-diagnostics",
126                        ],
127                        range,
128                        message: guaranteed_invalid_custom_property_message(&entry),
129                        tags: Vec::new(),
130                        create_custom_property: None,
131                        cascade_narrowing: None,
132                        cascade_confidence: None,
133                        polynomial_provenance: None,
134                        cross_file_scc: None,
135                    }
136                })
137            })
138            .collect::<Vec<_>>();
139
140    diagnostics.extend(
141        summarize_query_cascade_checker_diagnostics_with_deep_analysis(
142            style_uri,
143            source,
144            deep_analysis,
145        ),
146    );
147
148    diagnostics
149}
150
151fn guaranteed_invalid_custom_property_message(
152    entry: &omena_cascade::CustomPropertyLeastFixedPointEntryV0,
153) -> String {
154    let cause = match entry.guaranteed_invalid_reason {
155        Some(omena_cascade::CustomPropertyGuaranteedInvalidReasonV0::CycleMember) => {
156            "is a member of a cyclic custom-property dependency component"
157        }
158        Some(omena_cascade::CustomPropertyGuaranteedInvalidReasonV0::MissingReference) => {
159            "references a missing custom property without a fallback"
160        }
161        Some(
162            omena_cascade::CustomPropertyGuaranteedInvalidReasonV0::InvalidDependencyWithoutFallback,
163        ) => "depends on a guaranteed-invalid custom property without a fallback",
164        None => "has no classified guaranteed-invalid cause",
165    };
166    format!(
167        "CSS custom property '{}' computes to the guaranteed-invalid value because it {cause}.",
168        entry.name.as_str()
169    )
170}
171
172pub fn summarize_omena_query_missing_keyframes_diagnostics(
173    style_uri: &str,
174    source: &str,
175) -> Vec<OmenaQueryStyleDiagnosticV0> {
176    let dialect = omena_parser_dialect_for_style_path(style_uri);
177    let facts = collect_omena_query_omena_parser_style_facts_raw(source, dialect);
178    let declared_keyframes = facts
179        .animations
180        .iter()
181        .filter(|animation| animation.kind == ParsedAnimationFactKind::KeyframesDeclaration)
182        .map(|animation| animation.name.clone())
183        .collect::<BTreeSet<_>>();
184    let mut emitted = BTreeSet::new();
185
186    facts
187        .animations
188        .into_iter()
189        .filter(|animation| animation.kind == ParsedAnimationFactKind::AnimationNameReference)
190        .filter(|animation| !declared_keyframes.contains(animation.name.as_str()))
191        .filter_map(|animation| {
192            let start: u32 = animation.range.start().into();
193            let end: u32 = animation.range.end().into();
194            let byte_span = ParserByteSpanV0 {
195                start: start as usize,
196                end: end as usize,
197            };
198            if !emitted.insert((animation.name.clone(), byte_span.start, byte_span.end)) {
199                return None;
200            }
201            Some((animation, parser_range_for_byte_span(source, byte_span)))
202        })
203        .map(|(animation, range)| OmenaQueryStyleDiagnosticV0 {
204            code: "missingKeyframes",
205            severity: "warning",
206            provenance: omena_query_evidence_graph_provenance![
207                "omena-parser.animation-facts",
208                "omena-query.style-diagnostics",
209            ],
210            range,
211            message: format!("@keyframes '{}' not found in this file.", animation.name),
212            tags: Vec::new(),
213            create_custom_property: None,
214            cascade_narrowing: None,
215            cascade_confidence: None,
216            polynomial_provenance: None,
217            cross_file_scc: None,
218        })
219        .collect()
220}