normalize_languages/
prolog.rs1use std::path::Path;
4
5use crate::{
6 Import, ImportSpec, Language, LanguageSymbols, ModuleId, ModuleResolver, Resolution,
7 ResolverConfig,
8};
9use tree_sitter::Node;
10
11pub struct Prolog;
13
14impl Language for Prolog {
15 fn name(&self) -> &'static str {
16 "Prolog"
17 }
18 fn extensions(&self) -> &'static [&'static str] {
19 &["pl", "pro", "prolog"]
20 }
21 fn grammar_name(&self) -> &'static str {
22 "prolog"
23 }
24
25 fn as_symbols(&self) -> Option<&dyn LanguageSymbols> {
26 Some(self)
27 }
28
29 fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
30 if node.kind() != "directive_term" {
31 return Vec::new();
32 }
33
34 let text = &content[node.byte_range()];
35 if text.contains("use_module(") {
36 return vec![Import {
37 module: text.trim().to_string(),
38 names: Vec::new(),
39 alias: None,
40 is_wildcard: false,
41 is_relative: false,
42 line: node.start_position().row + 1,
43 }];
44 }
45
46 Vec::new()
47 }
48
49 fn format_import(&self, import: &Import, names: Option<&[&str]>) -> String {
50 let names_to_use: Vec<&str> = names
52 .map(|n| n.to_vec())
53 .unwrap_or_else(|| import.names.iter().map(|s| s.as_str()).collect());
54 if names_to_use.is_empty() {
55 format!(":- use_module({}).", import.module)
56 } else {
57 format!(
58 ":- use_module({}, [{}]).",
59 import.module,
60 names_to_use.join(", ")
61 )
62 }
63 }
64
65 fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
66 let name = symbol.name.as_str();
67 match symbol.kind {
68 crate::SymbolKind::Function | crate::SymbolKind::Method => name.starts_with("test_"),
69 crate::SymbolKind::Module => name == "tests" || name == "test",
70 _ => false,
71 }
72 }
73
74 fn node_name<'a>(&self, node: &Node, content: &'a str) -> Option<&'a str> {
75 let mut cursor = node.walk();
82 for child in node.children(&mut cursor) {
83 match child.kind() {
84 "atom" => return Some(&content[child.byte_range()]),
85 "functional_notation" => {
86 if let Some(name_node) = child.child_by_field_name("function") {
87 return Some(&content[name_node.byte_range()]);
88 }
89 }
90 "operator_notation" => {
91 let mut inner = child.walk();
93 for inner_child in child.children(&mut inner) {
94 if inner_child.kind() == "functional_notation"
95 && let Some(name_node) = inner_child.child_by_field_name("function")
96 {
97 return Some(&content[name_node.byte_range()]);
98 }
99 }
100 }
101 _ => {}
102 }
103 }
104 None
105 }
106
107 fn module_resolver(&self) -> Option<&dyn ModuleResolver> {
108 static RESOLVER: PrologModuleResolver = PrologModuleResolver;
109 Some(&RESOLVER)
110 }
111}
112
113impl LanguageSymbols for Prolog {}
114
115pub struct PrologModuleResolver;
125
126impl ModuleResolver for PrologModuleResolver {
127 fn workspace_config(&self, root: &Path) -> ResolverConfig {
128 ResolverConfig {
129 workspace_root: root.to_path_buf(),
130 path_mappings: Vec::new(),
131 search_roots: Vec::new(),
132 }
133 }
134
135 fn module_of_file(&self, _root: &Path, file: &Path, _cfg: &ResolverConfig) -> Vec<ModuleId> {
136 let ext = file.extension().and_then(|e| e.to_str()).unwrap_or("");
137 if ext != "pl" && ext != "pro" && ext != "prolog" {
138 return Vec::new();
139 }
140
141 if let Some(stem) = file.file_stem().and_then(|s| s.to_str()) {
142 return vec![ModuleId {
143 canonical_path: stem.to_string(),
144 }];
145 }
146
147 Vec::new()
148 }
149
150 fn resolve(&self, from_file: &Path, spec: &ImportSpec, cfg: &ResolverConfig) -> Resolution {
151 let ext = from_file.extension().and_then(|e| e.to_str()).unwrap_or("");
152 if ext != "pl" && ext != "pro" && ext != "prolog" {
153 return Resolution::NotApplicable;
154 }
155
156 let raw = &spec.raw;
157
158 if raw.starts_with("library(") {
160 return Resolution::NotFound;
161 }
162
163 if raw.starts_with("./") || raw.starts_with("../") {
165 let base_dir = from_file.parent().unwrap_or(&cfg.workspace_root);
166 for suffix in &["", ".pl", ".pro"] {
168 let candidate = base_dir.join(format!("{}{}", raw, suffix));
169 if candidate.exists() {
170 return Resolution::Resolved(candidate, String::new());
171 }
172 }
173 return Resolution::NotFound;
174 }
175
176 for suffix in &[".pl", ".pro"] {
178 let candidate = cfg.workspace_root.join(format!("{}{}", raw, suffix));
179 if candidate.exists() {
180 return Resolution::Resolved(candidate, String::new());
181 }
182 }
183
184 Resolution::NotFound
185 }
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191 use crate::validate_unused_kinds_audit;
192
193 #[test]
194 fn unused_node_kinds_audit() {
195 #[rustfmt::skip]
196 let documented_unused: &[&str] = &[
197 "binary_operator", "prefix_operator", "prexif_operator",
198 ];
199 validate_unused_kinds_audit(&Prolog, documented_unused)
200 .expect("Prolog unused node kinds audit failed");
201 }
202}