1use std::collections::{HashMap, HashSet};
2
3use bevy_ecs::prelude::*;
4use rdf_parsers::{
5 model::{BlankNode, Literal, NamedNode, Term, Turtle},
6 Spanned,
7};
8use swls_lov::LocalPrefix;
9use tracing::instrument;
10
11use crate::{
12 feature::{code_action::CodeActionRequest, diagnostics::DiagnosticPublisher},
13 lsp_types::{
14 CodeAction, CodeActionKind, Diagnostic, DiagnosticSeverity, Position, Range, TextEdit,
15 WorkspaceEdit,
16 },
17 prelude::*,
18 systems::PrefixEntry,
19 util::offset_to_position,
20};
21
22pub fn prefix_diagnostic_helper<'a>(
44 turtle: &Turtle,
45 prefixes: &Prefixes,
46 rope: &RopeC,
47 label: &Label,
48 lovs: impl Iterator<Item = &'a LocalPrefix>,
49 prefix_cc: impl Iterator<Item = &'a PrefixEntry>,
50 mut make_fix_edit: impl FnMut(&str, &str) -> Vec<TextEdit>,
51 report_undefined: bool,
52 report_unused: bool,
53) -> (Vec<Diagnostic>, Vec<CodeAction>) {
54 let declared: HashSet<&str> = prefixes.iter().map(|p| p.prefix.as_str()).collect();
55
56 let mut uses: Vec<(String, std::ops::Range<usize>)> = Vec::new();
60 let mut resolved: HashSet<String> = HashSet::new();
64 for triple in &turtle.triples {
65 let triple = triple.value();
66 collect_prefix_uses(&triple.subject, &mut uses, &mut resolved);
67 for po in &triple.po {
68 let po = po.value();
69 collect_prefix_uses(&po.predicate, &mut uses, &mut resolved);
70 for object in &po.object {
71 collect_prefix_uses(object, &mut uses, &mut resolved);
72 }
73 }
74 }
75
76 let mut used: HashSet<String> = HashSet::new();
77 let mut undefined_occurrences: Vec<(String, Range)> = Vec::new();
82 let mut undefined_names: HashSet<String> = HashSet::new();
84 let mut seen_spans: HashSet<(String, usize, usize)> = HashSet::new();
85
86 for (prefix_name, span) in &uses {
87 used.insert(prefix_name.clone());
88
89 if declared.contains(prefix_name.as_str()) {
90 continue;
91 }
92 if seen_spans.insert((prefix_name.clone(), span.start, span.end)) {
94 if let (Some(start), Some(end)) = (
95 offset_to_position(span.start, &rope.0),
96 offset_to_position(span.end, &rope.0),
97 ) {
98 undefined_occurrences.push((prefix_name.clone(), Range::new(start, end)));
99 undefined_names.insert(prefix_name.clone());
100 }
101 }
102 }
103
104 let mut url_lookup: HashMap<&str, String> = HashMap::new();
106 if !undefined_names.is_empty() {
107 for lp in lovs {
108 if undefined_names.contains(lp.name.as_ref()) {
109 url_lookup
110 .entry(lp.name.as_ref())
111 .or_insert_with(|| lp.namespace.to_string());
112 }
113 }
114 for pe in prefix_cc {
115 if undefined_names.contains(pe.name.as_ref()) {
116 url_lookup
117 .entry(pe.name.as_ref())
118 .or_insert_with(|| pe.namespace.to_string());
119 }
120 }
121 }
122
123 let mut diagnostics: Vec<Diagnostic> = Vec::new();
124 let mut code_actions: Vec<CodeAction> = Vec::new();
125
126 if report_undefined {
128 let mut diags_by_prefix: HashMap<&str, Vec<Diagnostic>> = HashMap::new();
132 for (prefix_name, range) in &undefined_occurrences {
133 let diag = Diagnostic {
134 range: range.clone(),
135 severity: Some(DiagnosticSeverity::ERROR),
136 message: format!("Undefined prefix \"{}\"", prefix_name),
137 ..Default::default()
138 };
139 diagnostics.push(diag.clone());
140 diags_by_prefix
141 .entry(prefix_name.as_str())
142 .or_default()
143 .push(diag);
144 }
145
146 let mut emitted: HashSet<&str> = HashSet::new();
152 for (prefix_name, _) in &undefined_occurrences {
153 if !emitted.insert(prefix_name.as_str()) {
154 continue;
155 }
156 let suggested_url = url_lookup
157 .get(prefix_name.as_str())
158 .map(|s| s.as_str())
159 .unwrap_or("");
160
161 let fix_edits = make_fix_edit(prefix_name, suggested_url);
162 if fix_edits.is_empty() {
163 continue;
164 }
165
166 let mut changes = std::collections::HashMap::new();
167 changes.insert(label.0.clone(), fix_edits);
168 code_actions.push(CodeAction {
169 title: format!("Add prefix declaration for \"{}\"", prefix_name),
170 kind: Some(CodeActionKind::QUICKFIX),
171 diagnostics: diags_by_prefix.remove(prefix_name.as_str()),
172 edit: Some(WorkspaceEdit {
173 changes: Some(changes),
174 ..Default::default()
175 }),
176 ..Default::default()
177 });
178 }
179 }
180
181 for prefix in report_unused.then(|| prefixes.iter()).into_iter().flatten() {
183 if used.contains(prefix.prefix.as_str()) {
184 continue;
185 }
186
187 let target = prefix.url.as_str();
192 let is_alias = !target.ends_with('/') && !target.ends_with('#');
193 if is_alias && resolved.contains(target) {
194 continue;
195 }
196
197 let (start, end) = find_prefix_declaration_range(&rope.0, &prefix.prefix);
198 diagnostics.push(Diagnostic {
199 range: Range::new(start, end),
200 severity: Some(DiagnosticSeverity::WARNING),
201 message: format!("Prefix \"{}\" is declared but never used", prefix.prefix),
202 ..Default::default()
203 });
204 }
205
206 (diagnostics, code_actions)
207}
208
209fn collect_prefix_uses(
214 term: &Spanned<Term>,
215 uses: &mut Vec<(String, std::ops::Range<usize>)>,
216 resolved: &mut HashSet<String>,
217) {
218 match term.value() {
219 Term::NamedNode(nn) => record_named_node(nn, term.span(), uses, resolved),
220 Term::Literal(Literal::RDF(lit)) => {
221 if let Some(datatype) = &lit.ty {
224 record_named_node(datatype.value(), datatype.span(), uses, resolved);
225 }
226 }
227 Term::Collection(items) => {
228 for item in items {
229 collect_prefix_uses(item, uses, resolved);
230 }
231 }
232 Term::BlankNode(BlankNode::Unnamed(pos, _, _)) => {
233 for po in pos {
234 let po = po.value();
235 collect_prefix_uses(&po.predicate, uses, resolved);
236 for object in &po.object {
237 collect_prefix_uses(object, uses, resolved);
238 }
239 }
240 }
241 _ => {}
242 }
243}
244
245fn record_named_node(
247 nn: &NamedNode,
248 span: &std::ops::Range<usize>,
249 uses: &mut Vec<(String, std::ops::Range<usize>)>,
250 resolved: &mut HashSet<String>,
251) {
252 match nn {
253 NamedNode::Prefixed {
254 prefix, computed, ..
255 } => {
256 if !prefix.contains(':') {
262 uses.push((prefix.clone(), span.clone()));
263 }
264 if let Some(computed) = computed {
265 resolved.insert(computed.clone());
266 }
267 }
268 NamedNode::Full(iri, _) => {
269 resolved.insert(iri.clone());
270 }
271 _ => {}
272 }
273}
274
275fn find_prefix_declaration_range(rope: &LineIndex, prefix_name: &str) -> (Position, Position) {
282 let turtle_needle = format!("@prefix {}:", prefix_name);
285 let sparql_needle = format!("PREFIX {}:", prefix_name);
286 let jsonld_needle = format!("\"{}\":", prefix_name);
290
291 let patterns = [
293 (&turtle_needle, "@prefix ".len(), prefix_name.len()),
294 (&sparql_needle, "PREFIX ".len(), prefix_name.len()),
295 (&jsonld_needle, 0, prefix_name.len() + 2),
297 ];
298
299 let candidate = (0..rope.len_lines()).find_map(|line_idx| {
300 let line = rope.line_str(line_idx)?;
301 let line_start = rope.line_start(line_idx)?;
302
303 for (needle, key_off, key_len) in patterns.iter() {
304 if let Some(idx) = line.find(needle.as_str()) {
305 let key_byte = line_start + idx + key_off;
308 let start = offset_to_position(key_byte, rope)?;
309 let end = offset_to_position(key_byte + key_len, rope)?;
310 return Some((start, end));
311 }
312 }
313 None
314 });
315
316 candidate.unwrap_or((Position::new(0, 0), Position::new(0, 0)))
317}
318
319#[instrument(skip(query, client, lovs, prefix_cc, config))]
327pub fn prefix_diagnostics(
328 query: Query<
329 (
330 &Element,
331 &Prefixes,
332 &Source,
333 &RopeC,
334 &Label,
335 &Wrapped<crate::lsp_types::TextDocumentItem>,
336 &DynLang,
337 ),
338 (Or<(Changed<Element>, Changed<Prefixes>)>, With<Open>),
339 >,
340 mut client: ResMut<DiagnosticPublisher>,
341 lovs: Query<&LocalPrefix>,
342 prefix_cc: Query<&PrefixEntry>,
343 config: Res<ServerConfig>,
344) {
345 let fmt = config.config.local.prefix_format.unwrap_or_default();
346 let report_undefined = !config.config.local.is_disabled(Disabled::UndefinedPrefix);
347 let report_unused = !config.config.local.is_disabled(Disabled::UnusedPrefix);
348 for (element, prefixes, source, rope, label, params, lang) in &query {
349 if !lang.0.supports_prefix_diagnostics() {
350 let _ = client.publish(¶ms.0, vec![], "prefix");
352 continue;
353 }
354
355 let (diagnostics, _) = prefix_diagnostic_helper(
356 element.value(),
357 prefixes,
358 rope,
359 label,
360 lovs.iter(),
361 prefix_cc.iter(),
362 |name, url| {
363 lang.0
364 .prefix_edits(&source.0, &rope.0, name, url, fmt)
365 .unwrap_or_default()
366 },
367 report_undefined,
368 report_unused,
369 );
370
371 let _ = client.publish(¶ms.0, diagnostics, "prefix");
372 }
373}
374
375#[instrument(skip(query, lovs, prefix_cc, config))]
380pub fn add_missing_prefix_code_action(
381 mut query: Query<
382 (
383 &Element,
384 &Prefixes,
385 &Source,
386 &RopeC,
387 &Label,
388 &DynLang,
389 &mut CodeActionRequest,
390 ),
391 With<Open>,
392 >,
393 lovs: Query<&LocalPrefix>,
394 prefix_cc: Query<&PrefixEntry>,
395 config: Res<ServerConfig>,
396) {
397 let fmt = config.config.local.prefix_format.unwrap_or_default();
398 if config.config.local.is_disabled(Disabled::UndefinedPrefix) {
399 return;
400 }
401 for (element, prefixes, source, rope, label, lang, mut req) in &mut query {
402 if !lang.0.supports_prefix_diagnostics() {
403 continue;
404 }
405
406 let (_, actions) = prefix_diagnostic_helper(
407 element.value(),
408 prefixes,
409 rope,
410 label,
411 lovs.iter(),
412 prefix_cc.iter(),
413 |name, url| {
414 lang.0
415 .prefix_edits(&source.0, &rope.0, name, url, fmt)
416 .unwrap_or_default()
417 },
418 true,
419 false,
420 );
421
422 req.0.extend(actions);
423 }
424}