Skip to main content

sqry_lang_puppet/
lib.rs

1//! Puppet language plugin for sqry.
2//!
3//! Provides production-ready graph and scope extraction for Puppet manifests.
4//! Supports: class, defined type, resource, function, include/require relations.
5
6mod relations;
7
8pub use relations::PuppetGraphBuilder;
9
10use anyhow::Result;
11use sqry_core::ast::{Scope, ScopeId, link_nested_scopes};
12use sqry_core::plugin::error::{ParseError, ScopeError};
13use sqry_core::plugin::{LanguageMetadata, LanguagePlugin};
14use std::path::Path;
15use tree_sitter::{Language, Node, Parser, Tree};
16
17/// Puppet language plugin
18pub struct PuppetPlugin {
19    graph_builder: PuppetGraphBuilder,
20}
21
22impl PuppetPlugin {
23    #[must_use]
24    pub fn new() -> Self {
25        Self {
26            graph_builder: PuppetGraphBuilder,
27        }
28    }
29}
30
31impl Default for PuppetPlugin {
32    fn default() -> Self {
33        Self::new()
34    }
35}
36
37impl PuppetPlugin {
38    /// Walk AST to extract scopes
39    fn walk_ast_scopes(node: Node, content: &[u8], file_path: &Path, scopes: &mut Vec<Scope>) {
40        match node.kind() {
41            "class_definition" | "defined_resource_type" => {
42                let mut cursor = node.walk();
43                let mut name = None;
44
45                for child in node.children(&mut cursor) {
46                    if child.kind() == "identifier" || child.kind() == "class_identifier" {
47                        name = child
48                            .utf8_text(content)
49                            .ok()
50                            .map(std::string::ToString::to_string);
51                        break;
52                    }
53                }
54
55                if let Some(name) = name {
56                    let start = node.start_position();
57                    let end = node.end_position();
58                    let scope_type = if node.kind() == "class_definition" {
59                        "class"
60                    } else {
61                        "define"
62                    };
63
64                    scopes.push(Scope {
65                        id: ScopeId::new(0), // Will be reassigned by link_nested_scopes
66                        scope_type: scope_type.to_string(),
67                        name,
68                        file_path: file_path.to_path_buf(),
69                        start_line: start.row + 1,
70                        start_column: start.column + 1,
71                        end_line: end.row + 1,
72                        end_column: end.column + 1,
73                        parent_id: None,
74                    });
75                }
76            }
77            _ => {}
78        }
79
80        // Recurse into children
81        let mut cursor = node.walk();
82        for child in node.children(&mut cursor) {
83            Self::walk_ast_scopes(child, content, file_path, scopes);
84        }
85    }
86}
87
88impl LanguagePlugin for PuppetPlugin {
89    fn metadata(&self) -> LanguageMetadata {
90        LanguageMetadata {
91            id: "puppet",
92            name: "Puppet",
93            version: env!("CARGO_PKG_VERSION"),
94            author: "Verivus Pty Ltd",
95            description: "Puppet language support with graph-native extraction",
96            tree_sitter_version: "0.25",
97        }
98    }
99
100    fn extensions(&self) -> &'static [&'static str] {
101        &["pp"]
102    }
103
104    fn language(&self) -> Language {
105        tree_sitter_puppet::LANGUAGE.into()
106    }
107
108    fn parse_ast(&self, content: &[u8]) -> Result<Tree, ParseError> {
109        let mut parser = Parser::new();
110        let lang = self.language();
111        parser
112            .set_language(&lang)
113            .map_err(|e| ParseError::LanguageSetFailed(e.to_string()))?;
114        parser
115            .parse(content, None)
116            .ok_or(ParseError::TreeSitterFailed)
117    }
118
119    fn extract_scopes(
120        &self,
121        tree: &Tree,
122        content: &[u8],
123        file_path: &Path,
124    ) -> Result<Vec<Scope>, ScopeError> {
125        let root = tree.root_node();
126        let mut scopes = Vec::new();
127        Self::walk_ast_scopes(root, content, file_path, &mut scopes);
128
129        // Sort scopes by position (required for link_nested_scopes)
130        scopes.sort_by_key(|s| (s.start_line, s.start_column));
131
132        // Build parent-child relationships
133        link_nested_scopes(&mut scopes);
134
135        Ok(scopes)
136    }
137
138    fn graph_builder(&self) -> Option<&dyn sqry_core::graph::GraphBuilder> {
139        Some(&self.graph_builder)
140    }
141}