1use crate::external_packages::ResolvedPackage;
4use crate::{Export, Import, Language, Symbol, SymbolKind, Visibility, VisibilityMechanism};
5use std::path::{Path, PathBuf};
6use tree_sitter::Node;
7
8pub struct Prolog;
10
11impl Language for Prolog {
12 fn name(&self) -> &'static str {
13 "Prolog"
14 }
15 fn extensions(&self) -> &'static [&'static str] {
16 &["pl", "pro", "prolog"]
17 }
18 fn grammar_name(&self) -> &'static str {
19 "prolog"
20 }
21
22 fn has_symbols(&self) -> bool {
23 true
24 }
25
26 fn container_kinds(&self) -> &'static [&'static str] {
27 &["directive_term"] }
29
30 fn function_kinds(&self) -> &'static [&'static str] {
31 &["clause_term"]
32 }
33
34 fn type_kinds(&self) -> &'static [&'static str] {
35 &[]
36 }
37
38 fn import_kinds(&self) -> &'static [&'static str] {
39 &["directive_term"] }
41
42 fn public_symbol_kinds(&self) -> &'static [&'static str] {
43 &["clause_term", "directive_term"]
44 }
45
46 fn visibility_mechanism(&self) -> VisibilityMechanism {
47 VisibilityMechanism::ExplicitExport
48 }
49
50 fn extract_public_symbols(&self, node: &Node, content: &str) -> Vec<Export> {
51 if node.kind() != "clause_term" {
52 return Vec::new();
53 }
54
55 if let Some(name) = self.node_name(node, content) {
56 return vec![Export {
57 name: name.to_string(),
58 kind: SymbolKind::Function,
59 line: node.start_position().row + 1,
60 }];
61 }
62
63 Vec::new()
64 }
65
66 fn scope_creating_kinds(&self) -> &'static [&'static str] {
67 &["clause_term"]
68 }
69
70 fn control_flow_kinds(&self) -> &'static [&'static str] {
71 &[] }
73
74 fn complexity_nodes(&self) -> &'static [&'static str] {
75 &["clause_term"] }
77
78 fn nesting_nodes(&self) -> &'static [&'static str] {
79 &["clause_term"]
80 }
81
82 fn signature_suffix(&self) -> &'static str {
83 ""
84 }
85
86 fn extract_function(&self, node: &Node, content: &str, _in_container: bool) -> Option<Symbol> {
87 if node.kind() != "clause_term" {
88 return None;
89 }
90
91 let name = self.node_name(node, content)?;
92 let text = &content[node.byte_range()];
93 let first_line = text.lines().next().unwrap_or(text);
94
95 Some(Symbol {
96 name: name.to_string(),
97 kind: SymbolKind::Function,
98 signature: first_line.trim().to_string(),
99 docstring: None,
100 attributes: Vec::new(),
101 start_line: node.start_position().row + 1,
102 end_line: node.end_position().row + 1,
103 visibility: Visibility::Public,
104 children: Vec::new(),
105 is_interface_impl: false,
106 implements: Vec::new(),
107 })
108 }
109
110 fn extract_container(&self, node: &Node, content: &str) -> Option<Symbol> {
111 if node.kind() != "directive_term" {
112 return None;
113 }
114
115 let text = &content[node.byte_range()];
116 if !text.contains("module(") {
117 return None;
118 }
119
120 let name = self.node_name(node, content)?;
121 let first_line = text.lines().next().unwrap_or(text);
122
123 Some(Symbol {
124 name: name.to_string(),
125 kind: SymbolKind::Module,
126 signature: first_line.trim().to_string(),
127 docstring: None,
128 attributes: Vec::new(),
129 start_line: node.start_position().row + 1,
130 end_line: node.end_position().row + 1,
131 visibility: Visibility::Public,
132 children: Vec::new(),
133 is_interface_impl: false,
134 implements: Vec::new(),
135 })
136 }
137
138 fn extract_type(&self, _node: &Node, _content: &str) -> Option<Symbol> {
139 None
140 }
141 fn extract_docstring(&self, _node: &Node, _content: &str) -> Option<String> {
142 None
143 }
144
145 fn extract_attributes(&self, _node: &Node, _content: &str) -> Vec<String> {
146 Vec::new()
147 }
148
149 fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
150 if node.kind() != "directive_term" {
151 return Vec::new();
152 }
153
154 let text = &content[node.byte_range()];
155 if text.contains("use_module(") {
156 return vec![Import {
157 module: text.trim().to_string(),
158 names: Vec::new(),
159 alias: None,
160 is_wildcard: false,
161 is_relative: false,
162 line: node.start_position().row + 1,
163 }];
164 }
165
166 Vec::new()
167 }
168
169 fn format_import(&self, import: &Import, names: Option<&[&str]>) -> String {
170 let names_to_use: Vec<&str> = names
172 .map(|n| n.to_vec())
173 .unwrap_or_else(|| import.names.iter().map(|s| s.as_str()).collect());
174 if names_to_use.is_empty() {
175 format!(":- use_module({}).", import.module)
176 } else {
177 format!(
178 ":- use_module({}, [{}]).",
179 import.module,
180 names_to_use.join(", ")
181 )
182 }
183 }
184
185 fn is_public(&self, _node: &Node, _content: &str) -> bool {
186 true
187 }
188 fn get_visibility(&self, _node: &Node, _content: &str) -> Visibility {
189 Visibility::Public
190 }
191
192 fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
193 let name = symbol.name.as_str();
194 match symbol.kind {
195 crate::SymbolKind::Function | crate::SymbolKind::Method => name.starts_with("test_"),
196 crate::SymbolKind::Module => name == "tests" || name == "test",
197 _ => false,
198 }
199 }
200
201 fn embedded_content(&self, _node: &Node, _content: &str) -> Option<crate::EmbeddedBlock> {
202 None
203 }
204
205 fn container_body<'a>(&self, _node: &'a Node<'a>) -> Option<Node<'a>> {
206 None
207 }
208 fn body_has_docstring(&self, _body: &Node, _content: &str) -> bool {
209 false
210 }
211
212 fn node_name<'a>(&self, node: &Node, content: &'a str) -> Option<&'a str> {
213 let head = if let Some(h) = node.child_by_field_name("head") {
215 h
216 } else {
217 let mut cursor = node.walk();
218 let mut found = None;
219 for child in node.children(&mut cursor) {
220 if child.kind() == "atom" || child.kind() == "compound_term" {
221 found = Some(child);
222 break;
223 }
224 }
225 found?
226 };
227
228 let mut cursor = head.walk();
230 for child in head.children(&mut cursor) {
231 if child.kind() == "atom" {
232 return Some(&content[child.byte_range()]);
233 }
234 }
235 Some(&content[head.byte_range()])
236 }
237
238 fn file_path_to_module_name(&self, path: &Path) -> Option<String> {
239 let ext = path.extension()?.to_str()?;
240 if !["pl", "pro", "prolog"].contains(&ext) {
241 return None;
242 }
243 let stem = path.file_stem()?.to_str()?;
244 Some(stem.to_string())
245 }
246
247 fn module_name_to_paths(&self, module: &str) -> Vec<String> {
248 vec![
249 format!("{}.pl", module),
250 format!("{}.pro", module),
251 format!("{}.prolog", module),
252 ]
253 }
254
255 fn lang_key(&self) -> &'static str {
256 "prolog"
257 }
258
259 fn is_stdlib_import(&self, _: &str, _: &Path) -> bool {
260 false
261 }
262 fn find_stdlib(&self, _project_root: &Path) -> Option<PathBuf> {
263 None
264 }
265 fn resolve_local_import(&self, _: &str, _: &Path, _: &Path) -> Option<PathBuf> {
266 None
267 }
268 fn resolve_external_import(&self, _: &str, _: &Path) -> Option<ResolvedPackage> {
269 None
270 }
271 fn get_version(&self, _: &Path) -> Option<String> {
272 None
273 }
274 fn find_package_cache(&self, _: &Path) -> Option<PathBuf> {
275 None
276 }
277 fn indexable_extensions(&self) -> &'static [&'static str] {
278 &["pl", "pro", "prolog"]
279 }
280 fn package_sources(&self, _: &Path) -> Vec<crate::PackageSource> {
281 Vec::new()
282 }
283
284 fn should_skip_package_entry(&self, name: &str, is_dir: bool) -> bool {
285 use crate::traits::{has_extension, skip_dotfiles};
286 if skip_dotfiles(name) {
287 return true;
288 }
289 !is_dir && !has_extension(name, self.indexable_extensions())
290 }
291
292 fn discover_packages(&self, _: &crate::PackageSource) -> Vec<(String, PathBuf)> {
293 Vec::new()
294 }
295
296 fn package_module_name(&self, entry_name: &str) -> String {
297 entry_name
298 .strip_suffix(".pl")
299 .or_else(|| entry_name.strip_suffix(".pro"))
300 .or_else(|| entry_name.strip_suffix(".prolog"))
301 .unwrap_or(entry_name)
302 .to_string()
303 }
304
305 fn find_package_entry(&self, path: &Path) -> Option<PathBuf> {
306 if path.is_file() {
307 Some(path.to_path_buf())
308 } else {
309 None
310 }
311 }
312}
313
314#[cfg(test)]
315mod tests {
316 use super::*;
317 use crate::validate_unused_kinds_audit;
318
319 #[test]
320 fn unused_node_kinds_audit() {
321 #[rustfmt::skip]
322 let documented_unused: &[&str] = &[
323 "binary_operator", "functional_notation",
324 "operator_notation", "prefix_operator", "prexif_operator",
325 ];
326 validate_unused_kinds_audit(&Prolog, documented_unused)
327 .expect("Prolog unused node kinds audit failed");
328 }
329}