1use super::*;
20
21use omena_query_checker_orchestrator::{
22 AbstractClassValueV0, OmenaQueryCheckerKLimitedFlowContextV0,
23 run_omena_query_checker_k_limited_flow_m_tier_gate_v0, top_class_value,
24};
25
26#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
29#[serde(
30 tag = "kind",
31 rename_all = "camelCase",
32 rename_all_fields = "camelCase"
33)]
34pub enum OmenaQueryDynamicClassValueInputV0 {
35 Bottom,
36 Exact {
37 value: String,
38 },
39 FiniteSet {
40 values: Vec<String>,
41 },
42 Prefix {
43 prefix: String,
44 },
45 Suffix {
46 suffix: String,
47 },
48 PrefixSuffix {
49 prefix: String,
50 suffix: String,
51 #[serde(default)]
52 min_length: usize,
53 },
54 Top,
55}
56
57impl OmenaQueryDynamicClassValueInputV0 {
58 fn into_abstract_class_value(self) -> AbstractClassValueV0 {
59 match self {
60 Self::Bottom => AbstractClassValueV0::Bottom,
61 Self::Exact { value } => AbstractClassValueV0::Exact { value },
62 Self::FiniteSet { values } => AbstractClassValueV0::FiniteSet { values },
63 Self::Prefix { prefix } => AbstractClassValueV0::Prefix {
64 prefix,
65 provenance: None,
66 },
67 Self::Suffix { suffix } => AbstractClassValueV0::Suffix {
68 suffix,
69 provenance: None,
70 },
71 Self::PrefixSuffix {
72 prefix,
73 suffix,
74 min_length,
75 } => AbstractClassValueV0::PrefixSuffix {
76 prefix,
77 suffix,
78 min_length,
79 provenance: None,
80 },
81 Self::Top => top_class_value(),
82 }
83 }
84}
85
86#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
88#[serde(rename_all = "camelCase")]
89pub struct OmenaQueryDynamicClassnameCallSiteV0 {
90 pub callee_key: String,
91 pub call_site_stack: Vec<String>,
92 pub exit_value: OmenaQueryDynamicClassValueInputV0,
93 pub reference_range: ParserRangeV0,
94}
95
96#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
99#[serde(rename_all = "camelCase")]
100pub struct OmenaQueryDynamicClassnameMTierInputV0 {
101 pub source_uri: String,
102 pub selector_universe: Vec<String>,
103 pub max_context_depth: usize,
104 pub call_sites: Vec<OmenaQueryDynamicClassnameCallSiteV0>,
105}
106
107pub fn summarize_omena_query_dynamic_classname_m_tier_diagnostics_with_context_depth(
116 input: &OmenaQueryDynamicClassnameMTierInputV0,
117) -> OmenaQuerySourceDiagnosticsForFileV0 {
118 let mut diagnostics = collect_omena_query_dynamic_classname_m_tier_diagnostics(
119 &input.call_sites,
120 &input.selector_universe,
121 input.max_context_depth,
122 );
123
124 diagnostics.sort_by(|left, right| {
125 (
126 left.range.start.line,
127 left.range.start.character,
128 left.code,
129 &left.message,
130 )
131 .cmp(&(
132 right.range.start.line,
133 right.range.start.character,
134 right.code,
135 &right.message,
136 ))
137 });
138 apply_omena_query_checker_product_gate_to_source_diagnostics(&mut diagnostics);
139
140 OmenaQuerySourceDiagnosticsForFileV0 {
141 schema_version: "0",
142 product: "omena-query.diagnostics-for-file",
143 file_uri: input.source_uri.clone(),
144 file_kind: "source",
145 diagnostic_count: diagnostics.len(),
146 diagnostics,
147 ready_surfaces: vec![
148 "dynamicClassnameMTierDiagnostics",
149 "kLimitedCallSiteFlow",
150 "checkerMTierEvaluation",
151 "checkerProductDiagnosticGate",
152 ],
153 }
154}
155
156pub(super) const OMENA_QUERY_WORKSPACE_DYNAMIC_CLASSNAME_CONTEXT_DEPTH: usize = 2;
162
163pub(super) fn harvest_omena_query_dynamic_classname_m_tier_diagnostics(
194 source_path: &str,
195 source: &str,
196 source_syntax_index: &OmenaQuerySourceSyntaxIndexV0,
197 union_selector_universe: &[String],
198 selector_universe_by_uri: &BTreeMap<String, Vec<String>>,
199 max_context_depth: usize,
200) -> Vec<OmenaQuerySourceDiagnosticV0> {
201 let mut call_sites_by_scope: BTreeMap<
205 Option<String>,
206 Vec<OmenaQueryDynamicClassnameCallSiteV0>,
207 > = BTreeMap::new();
208 for target in &source_syntax_index.type_fact_targets {
209 let Some(exit_value) =
210 harvested_abstract_class_value(target.prefix.as_str(), target.suffix.as_str())
211 else {
212 continue;
213 };
214 let callee_key = harvested_callee_key(target.expression_id.as_str());
215 call_sites_by_scope
216 .entry(target.target_style_uri.clone())
217 .or_default()
218 .push(OmenaQueryDynamicClassnameCallSiteV0 {
219 callee_key,
220 call_site_stack: vec![
221 source_path.to_string(),
222 format!("{}:{}", target.byte_span.start, target.byte_span.end),
223 ],
224 exit_value,
225 reference_range: parser_range_for_byte_span(source, target.byte_span),
226 });
227 }
228 for skipped in &source_syntax_index.type_fact_target_skipped {
229 let exact_references = source_syntax_index
230 .selector_references
231 .iter()
232 .filter(|reference| {
233 reference.match_kind == OmenaQuerySourceSelectorReferenceMatchKindV0::Exact
234 && reference.surface
235 == OmenaQuerySourceSelectorReferenceSurfaceV0::OmenaQuerySourceSyntaxIndex
236 && reference.target_style_uri == skipped.target_style_uri
237 && reference.byte_span.start <= skipped.byte_span.start
238 && skipped.byte_span.end <= reference.byte_span.end
239 })
240 .collect::<Vec<_>>();
241 let Some(reference_span) = exact_references
242 .iter()
243 .map(|reference| reference.byte_span)
244 .min_by_key(|span| (span.end.saturating_sub(span.start), span.start, span.end))
245 else {
246 continue;
247 };
248 let mut values = exact_references
249 .into_iter()
250 .filter(|reference| reference.byte_span == reference_span)
251 .filter_map(|reference| reference.selector_name.clone())
252 .collect::<Vec<_>>();
253 values.sort();
254 values.dedup();
255 if values.is_empty() {
256 continue;
257 }
258 let exit_value = match values.as_slice() {
259 [value] => OmenaQueryDynamicClassValueInputV0::Exact {
260 value: value.clone(),
261 },
262 _ => OmenaQueryDynamicClassValueInputV0::FiniteSet { values },
263 };
264 call_sites_by_scope
265 .entry(skipped.target_style_uri.clone())
266 .or_default()
267 .push(OmenaQueryDynamicClassnameCallSiteV0 {
268 callee_key: harvested_callee_key(skipped.expression_id.as_str()),
269 call_site_stack: vec![
270 source_path.to_string(),
271 format!("{}:{}", skipped.byte_span.start, skipped.byte_span.end),
272 ],
273 exit_value,
274 reference_range: parser_range_for_byte_span(source, reference_span),
275 });
276 }
277
278 if call_sites_by_scope.is_empty() {
279 return Vec::new();
280 }
281
282 let mut diagnostics = Vec::new();
283 for (scope_uri, call_sites) in &call_sites_by_scope {
284 let scoped_universe = match scope_uri {
285 Some(uri) => selector_universe_by_uri
286 .get(uri)
287 .map(Vec::as_slice)
288 .unwrap_or(union_selector_universe),
289 None => union_selector_universe,
290 };
291 diagnostics.extend(collect_omena_query_dynamic_classname_m_tier_diagnostics(
292 call_sites,
293 scoped_universe,
294 max_context_depth,
295 ));
296 }
297
298 diagnostics.retain(|diagnostic| diagnostic.code != "noImpreciseValue");
301 diagnostics
302}
303
304fn harvested_abstract_class_value(
309 prefix: &str,
310 suffix: &str,
311) -> Option<OmenaQueryDynamicClassValueInputV0> {
312 match (prefix.is_empty(), suffix.is_empty()) {
313 (true, true) => None,
314 (false, true) => Some(OmenaQueryDynamicClassValueInputV0::Prefix {
315 prefix: prefix.to_string(),
316 }),
317 (true, false) => Some(OmenaQueryDynamicClassValueInputV0::Suffix {
318 suffix: suffix.to_string(),
319 }),
320 (false, false) => Some(OmenaQueryDynamicClassValueInputV0::PrefixSuffix {
321 prefix: prefix.to_string(),
322 suffix: suffix.to_string(),
323 min_length: prefix.len() + suffix.len(),
324 }),
325 }
326}
327
328fn harvested_callee_key(expression_id: &str) -> String {
333 let trimmed = expression_id
334 .strip_prefix("omena-bridge-source-type-fact:")
335 .unwrap_or(expression_id);
336 let mut segments = trimmed.rsplitn(3, ':');
338 let _end = segments.next();
339 let _start = segments.next();
340 match segments.next() {
341 Some(path) if !path.is_empty() => path.to_string(),
342 _ => trimmed.to_string(),
343 }
344}
345
346pub(super) fn collect_omena_query_dynamic_classname_m_tier_diagnostics(
359 call_sites: &[OmenaQueryDynamicClassnameCallSiteV0],
360 selector_universe: &[String],
361 max_context_depth: usize,
362) -> Vec<OmenaQuerySourceDiagnosticV0> {
363 let mut range_by_context: BTreeMap<(String, Vec<String>), ParserRangeV0> = BTreeMap::new();
364 let contexts = call_sites
365 .iter()
366 .map(|call_site| {
367 range_by_context.insert(
368 (
369 call_site.callee_key.clone(),
370 call_site.call_site_stack.clone(),
371 ),
372 call_site.reference_range,
373 );
374 OmenaQueryCheckerKLimitedFlowContextV0 {
375 callee_key: call_site.callee_key.clone(),
376 call_site_stack: call_site.call_site_stack.clone(),
377 exit_value: call_site.exit_value.clone().into_abstract_class_value(),
378 }
379 })
380 .collect::<Vec<_>>();
381
382 let gate = run_omena_query_checker_k_limited_flow_m_tier_gate_v0(
383 &contexts,
384 selector_universe,
385 max_context_depth,
386 );
387
388 let mut diagnostics = Vec::new();
389 if gate.enforcement_passed {
390 for context in &gate.contexts {
391 let range = range_by_context
392 .get(&(context.callee_key.clone(), context.call_site_stack.clone()))
393 .copied()
394 .unwrap_or_default();
395 for evaluation in &context.evaluations {
396 diagnostics.push(OmenaQuerySourceDiagnosticV0 {
397 code: dynamic_classname_m_tier_diagnostic_code(evaluation.rule_code_name),
398 severity: evaluation.severity_name,
399 provenance: omena_query_evidence_graph_provenance![
400 "omena-query-checker-orchestrator.k-limited-flow-m-tier-gate",
401 "omena-abstract-value.k-limited-call-site-flow",
402 "omena-checker.m-tier-rules",
403 "omena-query.dynamic-classname",
404 ],
405 range,
406 message: evaluation.message.clone(),
407 precision: Some(source_diagnostic_precision(
408 "classValueFlow",
409 "kLimitedCallSiteFlow",
410 "kLimitedDynamicClassname",
411 )),
412 suggestion: None,
413 create_selector: None,
414 });
415 }
416 }
417 }
418 diagnostics
419}
420
421fn dynamic_classname_m_tier_diagnostic_code(rule_code_name: &str) -> &'static str {
422 match rule_code_name {
423 "no-unknown-dynamic-class" => "noUnknownDynamicClass",
424 "no-imprecise-value" => "noImpreciseValue",
425 "no-impossible-selector" => "noImpossibleSelector",
426 _ => "dynamicClassDomain",
427 }
428}