⎇ 72758e75ce515f355f27615671c95ecaf8c2a8bc
crates/modules/semantic_dht/minga-core/src/parse.rs
History
Who touched what
raw
feat(minga): multi-lenguaje en parser — Python, TypeScript, JavaScript, Go
1//! Adaptadores de parsing por dialecto.
2//!
3//! Cada función devuelve un [`SemanticNode`] normalizado a partir del
4//! source code. La normalización vive en `ast::SemanticNode::from_tree_sitter`
5//! y es agnóstica al lenguaje — cualquier tree-sitter grammar produce
6//! el mismo shape de árbol semántico (sin whitespace, sin comentarios).
7//!
8//! Lenguajes soportados (cada uno son ~6 LOC + dep tree-sitter-X):
9//! - [`rust`] — Rust completo (con α-hashing en `alpha::hash_node_alpha`).
10//! - [`python`] — Python 3.x.
11//! - [`typescript`] — TypeScript (no TSX).
12//! - [`javascript`] — JavaScript / ECMAScript.
13//! - [`go`] — Go.
14//!
15//! Para hashing α-equivalente, sólo Rust tiene implementación dedicada
16//! hoy. Otros lenguajes caen al [`crate::cas::hash_node`] estructural,
17//! que es α-NO-equivalente: dos versiones del mismo término que
18//! difieren en nombres de variables ligadas tendrán hashes distintos.
19//! Suficiente para detección de cambios; no para detección de
20//! equivalencia semántica.
21//!
22//! ## Auto-detección por extensión
23//!
24//! [`detect_by_extension`] mapea `.rs` → Rust, `.py` → Python, etc.
25//! Útil para `minga ingest` cuando el caller no quiere especificar
26//! el dialecto a mano.
27
28use crate::ast::SemanticNode;
29use thiserror::Error;
30use tree_sitter::{Language, Parser};
31
32#[derive(Debug, Error)]
33pub enum ParseError {
34 #[error("tree-sitter no pudo configurar el lenguaje")]
35 Language,
36 #[error("tree-sitter no produjo árbol para la entrada")]
37 NoTree,
38}
39
40/// Identificadores estables de cada dialecto soportado.
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
42pub enum Dialect {
43 Rust,
44 Python,
45 TypeScript,
46 JavaScript,
47 Go,
48}
49
50impl Dialect {
51 /// Nombre canónico para logging / display.
52 pub fn name(self) -> &'static str {
53 match self {
54 Dialect::Rust => "rust",
55 Dialect::Python => "python",
56 Dialect::TypeScript => "typescript",
57 Dialect::JavaScript => "javascript",
58 Dialect::Go => "go",
59 }
60 }
61
62 /// Parsea `source` con la gramática de este dialecto.
63 pub fn parse(self, source: &str) -> Result<SemanticNode, ParseError> {
64 match self {
65 Dialect::Rust => rust(source),
66 Dialect::Python => python(source),
67 Dialect::TypeScript => typescript(source),
68 Dialect::JavaScript => javascript(source),
69 Dialect::Go => go(source),
70 }
71 }
72}
73
74/// Mapea una extensión de archivo (sin el `.`) al dialecto correspondiente.
75/// `None` si la extensión no corresponde a un lenguaje soportado.
76///
77/// ```
78/// use minga_core::parse::{detect_by_extension, Dialect};
79/// assert_eq!(detect_by_extension("rs"), Some(Dialect::Rust));
80/// assert_eq!(detect_by_extension("py"), Some(Dialect::Python));
81/// assert_eq!(detect_by_extension("unknown"), None);
82/// ```
83pub fn detect_by_extension(ext: &str) -> Option<Dialect> {
84 match ext.to_ascii_lowercase().as_str() {
85 "rs" => Some(Dialect::Rust),
86 "py" | "pyi" => Some(Dialect::Python),
87 "ts" => Some(Dialect::TypeScript),
88 "js" | "mjs" | "cjs" => Some(Dialect::JavaScript),
89 "go" => Some(Dialect::Go),
90 _ => None,
91 }
92}
93
94fn parse_with(lang: Language, source: &str) -> Result<SemanticNode, ParseError> {
95 let mut parser = Parser::new();
96 parser.set_language(&lang).map_err(|_| ParseError::Language)?;
97 let tree = parser.parse(source, None).ok_or(ParseError::NoTree)?;
98 Ok(SemanticNode::from_tree_sitter(tree.root_node(), source.as_bytes()))
99}
100
101pub fn rust(source: &str) -> Result<SemanticNode, ParseError> {
102 parse_with(tree_sitter_rust::LANGUAGE.into(), source)
103}
104
105pub fn python(source: &str) -> Result<SemanticNode, ParseError> {
106 parse_with(tree_sitter_python::LANGUAGE.into(), source)
107}
108
109pub fn typescript(source: &str) -> Result<SemanticNode, ParseError> {
110 parse_with(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(), source)
111}
112
113pub fn javascript(source: &str) -> Result<SemanticNode, ParseError> {
114 parse_with(tree_sitter_javascript::LANGUAGE.into(), source)
115}
116
117pub fn go(source: &str) -> Result<SemanticNode, ParseError> {
118 parse_with(tree_sitter_go::LANGUAGE.into(), source)
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124
125 fn assert_parses(d: Dialect, source: &str) -> SemanticNode {
126 let node = d.parse(source).expect("parse should succeed");
127 // Sanity: el root siempre tiene al menos un child para code real.
128 assert!(
129 !node.children.is_empty(),
130 "{}: root node sin children — parse posiblemente vacío",
131 d.name()
132 );
133 node
134 }
135
136 #[test]
137 fn rust_parses_basic() {
138 assert_parses(Dialect::Rust, "fn add(a: i32, b: i32) -> i32 { a + b }");
139 }
140
141 #[test]
142 fn python_parses_basic() {
143 assert_parses(
144 Dialect::Python,
145 "def add(a: int, b: int) -> int:\n return a + b\n",
146 );
147 }
148
149 #[test]
150 fn typescript_parses_basic() {
151 assert_parses(
152 Dialect::TypeScript,
153 "function add(a: number, b: number): number { return a + b; }",
154 );
155 }
156
157 #[test]
158 fn javascript_parses_basic() {
159 assert_parses(
160 Dialect::JavaScript,
161 "function add(a, b) { return a + b; }",
162 );
163 }
164
165 #[test]
166 fn go_parses_basic() {
167 assert_parses(
168 Dialect::Go,
169 "package main\n\nfunc add(a, b int) int {\n return a + b\n}\n",
170 );
171 }
172
173 #[test]
174 fn detect_extension_canonical() {
175 assert_eq!(detect_by_extension("rs"), Some(Dialect::Rust));
176 assert_eq!(detect_by_extension("py"), Some(Dialect::Python));
177 assert_eq!(detect_by_extension("pyi"), Some(Dialect::Python));
178 assert_eq!(detect_by_extension("ts"), Some(Dialect::TypeScript));
179 assert_eq!(detect_by_extension("js"), Some(Dialect::JavaScript));
180 assert_eq!(detect_by_extension("mjs"), Some(Dialect::JavaScript));
181 assert_eq!(detect_by_extension("cjs"), Some(Dialect::JavaScript));
182 assert_eq!(detect_by_extension("go"), Some(Dialect::Go));
183 assert_eq!(detect_by_extension("unknown"), None);
184 assert_eq!(detect_by_extension(""), None);
185 }
186
187 #[test]
188 fn detect_extension_case_insensitive() {
189 assert_eq!(detect_by_extension("RS"), Some(Dialect::Rust));
190 assert_eq!(detect_by_extension("Py"), Some(Dialect::Python));
191 assert_eq!(detect_by_extension("TS"), Some(Dialect::TypeScript));
192 }
193
194 #[test]
195 fn dialect_name_canonical() {
196 assert_eq!(Dialect::Rust.name(), "rust");
197 assert_eq!(Dialect::Python.name(), "python");
198 assert_eq!(Dialect::TypeScript.name(), "typescript");
199 assert_eq!(Dialect::JavaScript.name(), "javascript");
200 assert_eq!(Dialect::Go.name(), "go");
201 }
202
203 #[test]
204 fn structural_hash_distinguishes_languages() {
205 // Mismo "shape" textual pero distintos lenguajes producen
206 // árboles distintos (las gramáticas no coinciden) y por tanto
207 // hashes estructurales distintos. Importante para evitar
208 // colisiones en el CAS cuando el mismo source se ingiere
209 // bajo dialectos distintos.
210 use crate::cas::hash_node;
211 let py = Dialect::Python.parse("x = 1").unwrap();
212 let js = Dialect::JavaScript.parse("x = 1").unwrap();
213 assert_ne!(
214 hash_node(&py),
215 hash_node(&js),
216 "py y js deberían tener hashes distintos para el mismo source"
217 );
218 }
219}