Skip to main content

strixonomy_refactor/
usages.rs

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
12/// Find all usages of `target_iri` across the indexed workspace.
13pub fn find_usages(catalog: &OntologyCatalog, target_iri: &str) -> Vec<Usage> {
14    find_usages_with_overrides(catalog, target_iri, &HashMap::new())
15}
16
17/// Find usages, preferring unsaved document text from `document_overrides`.
18pub 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    // SWRL JSON annotation references (inside string literals).
308    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        // #398
411        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}