omena_query/style/diagnostics/
single_file.rs1use 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 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
96pub 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}