ontocore-refactor 0.11.1

Workspace refactoring for OntoCore (ontocore-*) (rename, migrate, move, extract)
Documentation
use crate::model::{Usage, UsageKind};
use crate::source::read_source_text;
use crate::text::is_token_match_at;
use ontocore_catalog::OntologyCatalog;
use ontocore_core::{OntologyFormat, ParseStatus};
use ontocore_owl::{namespaces_for_text, short_name_from_iri};
use std::collections::{BTreeSet, HashMap};
use std::path::PathBuf;

/// Find all usages of `target_iri` across the indexed workspace.
pub fn find_usages(catalog: &OntologyCatalog, target_iri: &str) -> Vec<Usage> {
    find_usages_with_overrides(catalog, target_iri, &HashMap::new())
}

/// Find usages, preferring unsaved document text from `document_overrides`.
pub fn find_usages_with_overrides(
    catalog: &OntologyCatalog,
    target_iri: &str,
    document_overrides: &HashMap<PathBuf, String>,
) -> Vec<Usage> {
    let mut usages = Vec::new();
    let data = catalog.data();
    let mut seen = BTreeSet::new();

    for entity in &data.entities {
        if entity.iri == target_iri {
            if let Some(doc) = catalog.entity_document(&entity.iri) {
                let key = (doc.path.clone(), UsageKind::EntityDeclaration, entity.iri.clone());
                if seen.insert(key.clone()) {
                    usages.push(Usage {
                        iri: entity.iri.clone(),
                        referenced_iri: target_iri.to_string(),
                        file: doc.path.clone(),
                        line: entity.source_location.line,
                        column: entity.source_location.column,
                        start_byte: entity.source_location.start_byte,
                        end_byte: entity.source_location.end_byte,
                        kind: UsageKind::EntityDeclaration,
                        context: format!("{} declaration", entity.kind.as_str()),
                    });
                }
            }
        }
    }

    for axiom in &data.axioms {
        if axiom.subject == target_iri {
            if let Some(doc) = data.documents.iter().find(|d| d.id == axiom.ontology_id) {
                let key = (doc.path.clone(), UsageKind::AxiomSubject, axiom.id.clone());
                if seen.insert(key) {
                    usages.push(usage_from_axiom(
                        doc.path.clone(),
                        target_iri,
                        axiom,
                        UsageKind::AxiomSubject,
                    ));
                }
            }
        }
        if axiom.object == target_iri || is_named_ref(&axiom.object, target_iri) {
            if let Some(doc) = data.documents.iter().find(|d| d.id == axiom.ontology_id) {
                let key = (doc.path.clone(), UsageKind::AxiomObject, axiom.id.clone());
                if seen.insert(key) {
                    usages.push(usage_from_axiom(
                        doc.path.clone(),
                        target_iri,
                        axiom,
                        UsageKind::AxiomObject,
                    ));
                }
            }
        }
    }

    for ann in &data.annotations {
        if ann.subject == target_iri {
            if let Some(doc) = data.documents.iter().find(|d| d.id == ann.ontology_id) {
                let key = (doc.path.clone(), UsageKind::AnnotationSubject, ann.subject.clone());
                if seen.insert(key) {
                    usages.push(Usage {
                        iri: ann.subject.clone(),
                        referenced_iri: target_iri.to_string(),
                        file: doc.path.clone(),
                        line: ann.source_location.line,
                        column: ann.source_location.column,
                        start_byte: ann.source_location.start_byte,
                        end_byte: ann.source_location.end_byte,
                        kind: UsageKind::AnnotationSubject,
                        context: format!("annotation subject {}", ann.predicate),
                    });
                }
            }
        }
        if ann.object == target_iri {
            if let Some(doc) = data.documents.iter().find(|d| d.id == ann.ontology_id) {
                let key = (
                    doc.path.clone(),
                    UsageKind::AnnotationObject,
                    format!("{}-{}", ann.subject, ann.predicate),
                );
                if seen.insert(key) {
                    usages.push(Usage {
                        iri: ann.subject.clone(),
                        referenced_iri: target_iri.to_string(),
                        file: doc.path.clone(),
                        line: ann.source_location.line,
                        column: ann.source_location.column,
                        start_byte: ann.source_location.start_byte,
                        end_byte: ann.source_location.end_byte,
                        kind: UsageKind::AnnotationObject,
                        context: format!("annotation object {}", ann.predicate),
                    });
                }
            }
        }
    }

    for imp in &data.imports {
        if imp.import_iri == target_iri {
            if let Some(doc) = data.documents.iter().find(|d| d.id == imp.ontology_id) {
                let key = (doc.path.clone(), UsageKind::Import, imp.import_iri.clone());
                if seen.insert(key) {
                    usages.push(Usage {
                        iri: imp.ontology_id.clone(),
                        referenced_iri: target_iri.to_string(),
                        file: doc.path.clone(),
                        line: None,
                        column: None,
                        start_byte: None,
                        end_byte: None,
                        kind: UsageKind::Import,
                        context: format!("import {}", imp.import_iri),
                    });
                }
            }
        }
    }

    for doc in &data.documents {
        if doc.format != OntologyFormat::Turtle || doc.parse_status != ParseStatus::Ok {
            continue;
        }
        let text = match read_source_text(&doc.path, document_overrides) {
            Ok(t) => t,
            Err(_) => continue,
        };
        let namespaces = namespaces_for_text(&text, &doc.namespaces);
        let short = short_name_from_iri(target_iri);
        let angle_needle = format!("<{target_iri}>");
        for (line_idx, line) in text.lines().enumerate() {
            if line.contains(angle_needle.as_str()) {
                let mut search_from = 0usize;
                while let Some(col) = line[search_from..].find(&angle_needle) {
                    let col = search_from + col;
                    if is_token_match_at(line, &angle_needle, col) {
                        let key = (
                            doc.path.clone(),
                            UsageKind::TextReference,
                            format!("{line_idx}-{col}-{angle_needle}"),
                        );
                        if seen.insert(key) {
                            usages.push(text_usage(
                                doc,
                                target_iri,
                                line_idx,
                                col,
                                line,
                                UsageKind::TextReference,
                            ));
                        }
                    }
                    search_from = col + angle_needle.len();
                }
            }
            for (prefix, ns) in &namespaces {
                if target_iri.starts_with(ns) && !prefix.is_empty() {
                    let token = format!("{prefix}:{short}");
                    if !line.contains(&token) {
                        continue;
                    }
                    let mut search_from = 0usize;
                    while let Some(col) = line[search_from..].find(&token) {
                        let col = search_from + col;
                        if is_token_match_at(line, &token, col) {
                            let key = (
                                doc.path.clone(),
                                UsageKind::TextReference,
                                format!("{line_idx}-{col}-{token}"),
                            );
                            if seen.insert(key) {
                                usages.push(text_usage(
                                    doc,
                                    target_iri,
                                    line_idx,
                                    col,
                                    line,
                                    UsageKind::TextReference,
                                ));
                            }
                        }
                        search_from = col + token.len();
                    }
                }
            }
        }
    }

    usages.sort_by(|a, b| {
        a.file
            .cmp(&b.file)
            .then(a.line.unwrap_or(0).cmp(&b.line.unwrap_or(0)))
            .then(a.column.unwrap_or(0).cmp(&b.column.unwrap_or(0)))
    });
    usages
}

fn text_usage(
    doc: &ontocore_core::OntologyDocument,
    target_iri: &str,
    line_idx: usize,
    col: usize,
    line: &str,
    kind: UsageKind,
) -> Usage {
    Usage {
        iri: doc.id.clone(),
        referenced_iri: target_iri.to_string(),
        file: doc.path.clone(),
        line: Some((line_idx + 1) as u64),
        column: Some(col as u64),
        start_byte: None,
        end_byte: None,
        kind,
        context: line.trim().to_string(),
    }
}

fn usage_from_axiom(
    path: PathBuf,
    target_iri: &str,
    axiom: &ontocore_core::Axiom,
    kind: UsageKind,
) -> Usage {
    Usage {
        iri: axiom.subject.clone(),
        referenced_iri: target_iri.to_string(),
        file: path,
        line: axiom.source_location.line,
        column: axiom.source_location.column,
        start_byte: axiom.source_location.start_byte,
        end_byte: axiom.source_location.end_byte,
        kind,
        context: format!("{} {}", axiom.axiom_kind, axiom.object),
    }
}

fn is_named_ref(object: &str, target_iri: &str) -> bool {
    object == target_iri || object == format!("<{target_iri}>")
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn token_boundary_rejects_person_in_person_type() {
        let line = "ex:PersonType a owl:Class .";
        assert!(!is_token_match_at(line, "ex:Person", line.find("ex:Person").unwrap()));
        assert!(is_token_match_at(line, "ex:PersonType", line.find("ex:PersonType").unwrap()));
    }
}