Skip to main content

normalize_languages/
prolog.rs

1//! Prolog language support.
2
3use crate::external_packages::ResolvedPackage;
4use crate::{Export, Import, Language, Symbol, SymbolKind, Visibility, VisibilityMechanism};
5use std::path::{Path, PathBuf};
6use tree_sitter::Node;
7
8/// Prolog language support.
9pub 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"] // module declarations
28    }
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"] // use_module directives
40    }
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        &[] // Prolog uses pattern matching and backtracking
72    }
73
74    fn complexity_nodes(&self) -> &'static [&'static str] {
75        &["clause_term"] // Each clause adds complexity
76    }
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        // Prolog: :- use_module(module) or :- use_module(module, [pred/arity])
171        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        // For clauses, get the predicate name
214        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        // Get first atom child as the predicate name
229        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}