strixonomy_diagnostics/
location.rs1use std::collections::BTreeMap;
2use strixonomy_core::SourceLocation;
3
4pub fn find_in_source(source_text: &str, needles: &[String]) -> SourceLocation {
5 for (line_idx, line) in source_text.lines().enumerate() {
6 for needle in needles {
7 if let Some(col) = find_token_col(line, needle) {
8 return SourceLocation::at_line_col((line_idx + 1) as u64, col as u64);
9 }
10 }
11 }
12 SourceLocation::default()
13}
14
15pub fn find_import_in_source(source_text: &str, import_iri: &str) -> SourceLocation {
17 let needles = [import_iri.to_string(), format!("<{import_iri}>")];
18 for (line_idx, line) in source_text.lines().enumerate() {
19 let lower = line.to_ascii_lowercase();
20 if !lower.contains("owl:imports") {
21 continue;
22 }
23 for needle in &needles {
24 if let Some(col) = find_token_col(line, needle) {
25 return SourceLocation::at_line_col((line_idx + 1) as u64, col as u64);
26 }
27 }
28 }
29 find_in_source(source_text, &needles)
30}
31
32pub fn find_prefix_in_source(source_text: &str, prefix: &str) -> SourceLocation {
33 let needles = vec![
34 format!("@prefix {prefix}:"),
35 format!("@PREFIX {prefix}:"),
36 format!("PREFIX {prefix}:"),
37 format!("{prefix}:"),
38 ];
39 find_in_source(source_text, &needles)
40}
41
42pub fn entity_needles(
43 iri: &str,
44 short_name: &str,
45 namespaces: &BTreeMap<String, String>,
46) -> Vec<String> {
47 let mut needles = vec![iri.to_string(), format!("<{iri}>")];
48 for (prefix, ns) in namespaces {
49 if iri.starts_with(ns) && !prefix.is_empty() {
50 needles.push(format!("{prefix}:{short_name}"));
51 }
52 }
53 needles.push(format!(":{short_name}"));
55 needles
56}
57
58fn find_token_col(line: &str, needle: &str) -> Option<usize> {
59 if needle.is_empty() {
60 return None;
61 }
62 let mut start = 0usize;
63 while let Some(rel) = line[start..].find(needle) {
64 let col = start + rel;
65 if is_token_match_at(line, needle, col) {
66 return Some(col);
67 }
68 start = col + 1;
69 }
70 None
71}
72
73fn is_token_match_at(line: &str, needle: &str, col: usize) -> bool {
75 if !line[col..].starts_with(needle) {
76 return false;
77 }
78 is_safe_replacement_boundary(line, col, col + needle.len())
79}
80
81fn is_iri_continuation(b: u8) -> bool {
82 b.is_ascii_alphanumeric()
83 || matches!(b, b'_' | b'-' | b'.' | b'~' | b':' | b'#' | b'/' | b'%' | b'?' | b'=' | b'&')
84}
85
86fn is_safe_replacement_boundary(text: &str, start: usize, end: usize) -> bool {
87 let before = text.as_bytes().get(start.wrapping_sub(1)).copied();
88 let after = text.as_bytes().get(end).copied();
89 let ok_before = before.is_none_or(|b| !is_iri_continuation(b));
90 let ok_after = after.is_none_or(|b| !is_iri_continuation(b));
91 ok_before && ok_after
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97 use std::collections::BTreeMap;
98
99 #[test]
100 fn entity_needles_prefixed_match_before_bare_local() {
101 let mut namespaces = BTreeMap::new();
102 namespaces.insert("ex".to_string(), "http://example.org/ex#".to_string());
103 let needles = entity_needles("http://example.org/ex#Person", "Person", &namespaces);
104 let line = "ex:Person a owl:Class .";
105 let loc = find_in_source(line, &needles);
106 assert_eq!(loc.line, Some(1));
107 assert_eq!(loc.column, Some(0));
108 }
109
110 #[test]
111 fn entity_needles_bare_local_still_matches_default_prefix() {
112 let namespaces = BTreeMap::new();
113 let needles = entity_needles("http://example.org/ex#Person", "Person", &namespaces);
114 let line = ":Person a owl:Class .";
115 let loc = find_in_source(line, &needles);
116 assert_eq!(loc.line, Some(1));
117 assert_eq!(loc.column, Some(0));
118 }
119
120 #[test]
121 fn find_in_source_skips_substring_inside_longer_qname() {
122 let mut namespaces = BTreeMap::new();
123 namespaces.insert("ex".to_string(), "http://example.org/org#".to_string());
124 let needles = entity_needles("http://example.org/org#Person", "Person", &namespaces);
125 let text = "ex:PersonType a owl:Class .\nex:Person a owl:Class .\n";
126 let loc = find_in_source(text, &needles);
127 assert_eq!(loc.line, Some(2));
128 assert_eq!(loc.column, Some(0));
129 }
130
131 #[test]
132 fn find_import_prefers_owl_imports_over_earlier_comment() {
133 let iri = "http://example.org/missing";
134 let text = format!(
135 "# see <{iri}>\n@prefix owl: <http://www.w3.org/2002/07/owl#> .\n<> owl:imports <{iri}> .\n"
136 );
137 let loc = find_import_in_source(&text, iri);
138 assert_eq!(loc.line, Some(3));
139 }
140}