1use crate::model::{Usage, UsageKind};
2use crate::source::read_source_text;
3use crate::text::is_token_match_at;
4use std::collections::{BTreeSet, HashMap};
5use std::path::PathBuf;
6use strixonomy_catalog::OntologyCatalog;
7use strixonomy_core::{
8 document_matches_ontology_id, OntologyDocument, OntologyFormat, ParseStatus,
9};
10use strixonomy_owl::{namespaces_for_text, short_name_from_iri};
11
12pub fn find_usages(catalog: &OntologyCatalog, target_iri: &str) -> Vec<Usage> {
14 find_usages_with_overrides(catalog, target_iri, &HashMap::new())
15}
16
17pub fn find_usages_with_overrides(
19 catalog: &OntologyCatalog,
20 target_iri: &str,
21 document_overrides: &HashMap<PathBuf, String>,
22) -> Vec<Usage> {
23 let mut usages = Vec::new();
24 let data = catalog.data();
25 let mut seen = BTreeSet::new();
26
27 for entity in &data.entities {
28 if entity.iri == target_iri {
29 if let Some(doc) = catalog.entity_document(&entity.iri) {
30 let key = (doc.path.clone(), UsageKind::EntityDeclaration, entity.iri.clone());
31 if seen.insert(key.clone()) {
32 usages.push(Usage {
33 iri: entity.iri.clone(),
34 referenced_iri: target_iri.to_string(),
35 file: doc.path.clone(),
36 line: entity.source_location.line,
37 column: entity.source_location.column,
38 start_byte: entity.source_location.start_byte,
39 end_byte: entity.source_location.end_byte,
40 kind: UsageKind::EntityDeclaration,
41 context: format!("{} declaration", entity.kind.as_str()),
42 });
43 }
44 }
45 }
46 }
47
48 for axiom in &data.axioms {
49 if axiom.subject == target_iri {
50 if let Some(doc) =
51 document_for_ontology_id(data.documents.as_slice(), &axiom.ontology_id)
52 {
53 let key = (doc.path.clone(), UsageKind::AxiomSubject, axiom.id.clone());
54 if seen.insert(key) {
55 usages.push(usage_from_axiom(
56 doc.path.clone(),
57 target_iri,
58 axiom,
59 UsageKind::AxiomSubject,
60 ));
61 }
62 }
63 }
64 if axiom.predicate == target_iri {
65 if let Some(doc) =
66 document_for_ontology_id(data.documents.as_slice(), &axiom.ontology_id)
67 {
68 let key =
69 (doc.path.clone(), UsageKind::AxiomPredicate, format!("{}-pred", axiom.id));
70 if seen.insert(key) {
71 usages.push(usage_from_axiom(
72 doc.path.clone(),
73 target_iri,
74 axiom,
75 UsageKind::AxiomPredicate,
76 ));
77 }
78 }
79 }
80 if axiom.object == target_iri || is_named_ref(&axiom.object, target_iri) {
81 if let Some(doc) =
82 document_for_ontology_id(data.documents.as_slice(), &axiom.ontology_id)
83 {
84 let key = (doc.path.clone(), UsageKind::AxiomObject, axiom.id.clone());
85 if seen.insert(key) {
86 usages.push(usage_from_axiom(
87 doc.path.clone(),
88 target_iri,
89 axiom,
90 UsageKind::AxiomObject,
91 ));
92 }
93 }
94 }
95 }
96
97 for ann in &data.annotations {
98 if ann.subject == target_iri {
99 if let Some(doc) = document_for_ontology_id(data.documents.as_slice(), &ann.ontology_id)
100 {
101 let key = (
102 doc.path.clone(),
103 UsageKind::AnnotationSubject,
104 format!(
105 "{}-{}-{}-{}",
106 ann.subject,
107 ann.predicate,
108 ann.source_location.line.unwrap_or(0),
109 ann.source_location.start_byte.unwrap_or(0),
110 ),
111 );
112 if seen.insert(key) {
113 usages.push(Usage {
114 iri: ann.subject.clone(),
115 referenced_iri: target_iri.to_string(),
116 file: doc.path.clone(),
117 line: ann.source_location.line,
118 column: ann.source_location.column,
119 start_byte: ann.source_location.start_byte,
120 end_byte: ann.source_location.end_byte,
121 kind: UsageKind::AnnotationSubject,
122 context: format!("annotation subject {}", ann.predicate),
123 });
124 }
125 }
126 }
127 if ann.predicate == target_iri {
128 if let Some(doc) = document_for_ontology_id(data.documents.as_slice(), &ann.ontology_id)
129 {
130 let key = (
131 doc.path.clone(),
132 UsageKind::AnnotationPredicate,
133 format!(
134 "{}-{}-{}-pred",
135 ann.subject,
136 ann.predicate,
137 ann.source_location.line.unwrap_or(0),
138 ),
139 );
140 if seen.insert(key) {
141 usages.push(Usage {
142 iri: ann.subject.clone(),
143 referenced_iri: target_iri.to_string(),
144 file: doc.path.clone(),
145 line: ann.source_location.line,
146 column: ann.source_location.column,
147 start_byte: ann.source_location.start_byte,
148 end_byte: ann.source_location.end_byte,
149 kind: UsageKind::AnnotationPredicate,
150 context: format!("annotation predicate {}", ann.predicate),
151 });
152 }
153 }
154 }
155 if ann.object == target_iri {
156 if let Some(doc) = document_for_ontology_id(data.documents.as_slice(), &ann.ontology_id)
157 {
158 let key = (
159 doc.path.clone(),
160 UsageKind::AnnotationObject,
161 format!(
162 "{}-{}-{}-{}",
163 ann.subject,
164 ann.predicate,
165 ann.source_location.line.unwrap_or(0),
166 ann.source_location.start_byte.unwrap_or(0),
167 ),
168 );
169 if seen.insert(key) {
170 usages.push(Usage {
171 iri: ann.subject.clone(),
172 referenced_iri: target_iri.to_string(),
173 file: doc.path.clone(),
174 line: ann.source_location.line,
175 column: ann.source_location.column,
176 start_byte: ann.source_location.start_byte,
177 end_byte: ann.source_location.end_byte,
178 kind: UsageKind::AnnotationObject,
179 context: format!("annotation object {}", ann.predicate),
180 });
181 }
182 }
183 }
184 }
185
186 for imp in &data.imports {
187 if imp.import_iri == target_iri {
188 if let Some(doc) = document_for_ontology_id(data.documents.as_slice(), &imp.ontology_id)
189 {
190 let key = (doc.path.clone(), UsageKind::Import, imp.import_iri.clone());
191 if seen.insert(key) {
192 usages.push(Usage {
193 iri: imp.ontology_id.clone(),
194 referenced_iri: target_iri.to_string(),
195 file: doc.path.clone(),
196 line: None,
197 column: None,
198 start_byte: None,
199 end_byte: None,
200 kind: UsageKind::Import,
201 context: format!("import {}", imp.import_iri),
202 });
203 }
204 }
205 }
206 }
207
208 for doc in &data.documents {
209 if doc.format != OntologyFormat::Turtle || doc.parse_status != ParseStatus::Ok {
210 continue;
211 }
212 let text = match read_source_text(&doc.path, document_overrides) {
213 Ok(t) => t,
214 Err(_) => continue,
215 };
216 let namespaces = namespaces_for_text(&text, &doc.namespaces);
217 let short = short_name_from_iri(target_iri);
218 let angle_needle = format!("<{target_iri}>");
219 for (line_idx, line) in text.lines().enumerate() {
220 if line.contains(angle_needle.as_str()) {
221 let mut search_from = 0usize;
222 while let Some(col) = line[search_from..].find(&angle_needle) {
223 let col = search_from + col;
224 if is_token_match_at(line, &angle_needle, col) {
225 let key = (
226 doc.path.clone(),
227 UsageKind::TextReference,
228 format!("{line_idx}-{col}-{angle_needle}"),
229 );
230 if seen.insert(key) {
231 usages.push(text_usage(
232 doc,
233 target_iri,
234 line_idx,
235 col,
236 line,
237 UsageKind::TextReference,
238 ));
239 }
240 }
241 search_from = col + angle_needle.len();
242 }
243 }
244 for (prefix, ns) in &namespaces {
245 if target_iri.starts_with(ns) && !prefix.is_empty() {
246 let token = format!("{prefix}:{short}");
247 if !line.contains(&token) {
248 continue;
249 }
250 let mut search_from = 0usize;
251 while let Some(col) = line[search_from..].find(&token) {
252 let col = search_from + col;
253 if is_token_match_at(line, &token, col) {
254 let key = (
255 doc.path.clone(),
256 UsageKind::TextReference,
257 format!("{line_idx}-{col}-{token}"),
258 );
259 if seen.insert(key) {
260 usages.push(text_usage(
261 doc,
262 target_iri,
263 line_idx,
264 col,
265 line,
266 UsageKind::TextReference,
267 ));
268 }
269 }
270 search_from = col + token.len();
271 }
272 }
273 }
274 if let Some(default_ns) = namespaces.get("") {
275 if target_iri.starts_with(default_ns.as_str()) {
276 let token = format!(":{short}");
277 if !line.contains(&token) {
278 continue;
279 }
280 let mut search_from = 0usize;
281 while let Some(col) = line[search_from..].find(&token) {
282 let col = search_from + col;
283 if is_token_match_at(line, &token, col) {
284 let key = (
285 doc.path.clone(),
286 UsageKind::TextReference,
287 format!("{line_idx}-{col}-{token}"),
288 );
289 if seen.insert(key) {
290 usages.push(text_usage(
291 doc,
292 target_iri,
293 line_idx,
294 col,
295 line,
296 UsageKind::TextReference,
297 ));
298 }
299 }
300 search_from = col + token.len();
301 }
302 }
303 }
304 }
305 }
306
307 for doc in &data.documents {
309 if doc.format != OntologyFormat::Turtle || doc.parse_status != ParseStatus::Ok {
310 continue;
311 }
312 let Ok(text) = read_source_text(&doc.path, document_overrides) else {
313 continue;
314 };
315 for (rule_idx, rule) in
316 strixonomy_swrl::rules_from_turtle_document(&text).into_iter().enumerate()
317 {
318 if rule.referenced_iris().iter().any(|iri| iri == target_iri) {
319 let key = (doc.path.clone(), UsageKind::SwrlReference, format!("swrl-{rule_idx}"));
320 if seen.insert(key) {
321 usages.push(Usage {
322 iri: rule.id.clone().unwrap_or_else(|| format!("rule-{rule_idx}")),
323 referenced_iri: target_iri.to_string(),
324 file: doc.path.clone(),
325 line: None,
326 column: None,
327 start_byte: None,
328 end_byte: None,
329 kind: UsageKind::SwrlReference,
330 context: format!("SWRL rule {}", rule.id.as_deref().unwrap_or("anonymous")),
331 });
332 }
333 }
334 }
335 }
336
337 usages.sort_by(|a, b| {
338 a.file
339 .cmp(&b.file)
340 .then(a.line.unwrap_or(0).cmp(&b.line.unwrap_or(0)))
341 .then(a.column.unwrap_or(0).cmp(&b.column.unwrap_or(0)))
342 });
343 usages
344}
345
346fn text_usage(
347 doc: &strixonomy_core::OntologyDocument,
348 target_iri: &str,
349 line_idx: usize,
350 col: usize,
351 line: &str,
352 kind: UsageKind,
353) -> Usage {
354 Usage {
355 iri: doc.id.clone(),
356 referenced_iri: target_iri.to_string(),
357 file: doc.path.clone(),
358 line: Some((line_idx + 1) as u64),
359 column: Some(col as u64),
360 start_byte: None,
361 end_byte: None,
362 kind,
363 context: line.trim().to_string(),
364 }
365}
366
367fn usage_from_axiom(
368 path: PathBuf,
369 target_iri: &str,
370 axiom: &strixonomy_core::Axiom,
371 kind: UsageKind,
372) -> Usage {
373 Usage {
374 iri: axiom.subject.clone(),
375 referenced_iri: target_iri.to_string(),
376 file: path,
377 line: axiom.source_location.line,
378 column: axiom.source_location.column,
379 start_byte: axiom.source_location.start_byte,
380 end_byte: axiom.source_location.end_byte,
381 kind,
382 context: format!("{} {}", axiom.axiom_kind, axiom.object),
383 }
384}
385
386fn is_named_ref(object: &str, target_iri: &str) -> bool {
387 object == target_iri || object == format!("<{target_iri}>")
388}
389
390fn document_for_ontology_id<'a>(
391 documents: &'a [OntologyDocument],
392 ontology_id: &str,
393) -> Option<&'a OntologyDocument> {
394 documents.iter().find(|d| document_matches_ontology_id(ontology_id, d))
395}
396
397#[cfg(test)]
398mod tests {
399 use super::*;
400
401 #[test]
402 fn token_boundary_rejects_person_in_person_type() {
403 let line = "ex:PersonType a owl:Class .";
404 assert!(!is_token_match_at(line, "ex:Person", line.find("ex:Person").unwrap()));
405 assert!(is_token_match_at(line, "ex:PersonType", line.find("ex:PersonType").unwrap()));
406 }
407
408 #[test]
409 fn find_usages_includes_annotation_predicate() {
410 use strixonomy_catalog::IndexBuilder;
412
413 let dir = tempfile::tempdir().unwrap();
414 std::fs::write(
415 dir.path().join("ann.ttl"),
416 concat!(
417 "@prefix ex: <http://example.org#> .\n",
418 "@prefix owl: <http://www.w3.org/2002/07/owl#> .\n",
419 "ex:seeAlsoProp a owl:AnnotationProperty .\n",
420 "ex:Person a owl:Class ;\n",
421 " ex:seeAlsoProp ex:Other .\n",
422 ),
423 )
424 .unwrap();
425 let catalog = IndexBuilder::new().workspace(dir.path()).build().expect("index");
426 let pred = "http://example.org#seeAlsoProp";
427 let usages = find_usages(&catalog, pred);
428 assert!(
429 usages.iter().any(|u| matches!(
430 u.kind,
431 UsageKind::AnnotationPredicate | UsageKind::AxiomPredicate
432 )),
433 "must include predicate kind, not only declaration: {usages:?}"
434 );
435 }
436}