Skip to main content

hotspots_core/language/
mod.rs

1//! Language detection and abstraction layer
2//!
3//! This module provides language-agnostic interfaces for parsing and analyzing
4//! source code across multiple programming languages.
5
6pub mod c;
7pub mod cfg_builder;
8pub mod csharp;
9pub mod ecmascript;
10pub mod function_body;
11pub mod go;
12pub mod java;
13pub mod parser;
14pub mod python;
15pub mod rust;
16pub mod span;
17pub mod tree_sitter_utils;
18
19use std::path::Path;
20
21use serde::{Deserialize, Serialize};
22
23pub use c::{CCfgBuilder, CParser};
24pub use cfg_builder::{get_builder_for_function, CfgBuilder};
25pub use csharp::{CSharpCfgBuilder, CSharpParser};
26pub use ecmascript::{ECMAScriptCfgBuilder, ECMAScriptParser, VueParser};
27pub use function_body::FunctionBody;
28pub use go::{GoCfgBuilder, GoParser};
29pub use java::{JavaCfgBuilder, JavaParser};
30pub use parser::{LanguageParser, ParsedModule};
31pub use python::{PythonCfgBuilder, PythonParser};
32pub use rust::{RustCfgBuilder, RustParser};
33pub use span::SourceSpan;
34
35/// Supported programming languages
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
37pub enum Language {
38    /// TypeScript (.ts, .mts, .cts)
39    TypeScript,
40    /// TypeScript with JSX (.tsx, .mtsx, .ctsx)
41    TypeScriptReact,
42    /// JavaScript (.js, .mjs, .cjs)
43    JavaScript,
44    /// JavaScript with JSX (.jsx, .mjsx, .cjsx)
45    JavaScriptReact,
46    /// Go (.go)
47    Go,
48    /// Java (.java)
49    Java,
50    /// Python (.py, .pyw)
51    Python,
52    /// Rust (.rs)
53    Rust,
54    /// Vue Single File Component (.vue)
55    Vue,
56    /// C# (.cs)
57    CSharp,
58    /// C (.c)
59    C,
60    /// C header (.h)
61    CHeader,
62}
63
64impl Language {
65    /// Detect language from file extension
66    ///
67    /// Returns `None` if the extension is not recognized.
68    ///
69    /// # Examples
70    ///
71    /// ```
72    /// use hotspots_core::language::Language;
73    ///
74    /// assert_eq!(Language::from_extension("ts"), Some(Language::TypeScript));
75    /// assert_eq!(Language::from_extension("go"), Some(Language::Go));
76    /// assert_eq!(Language::from_extension("py"), Some(Language::Python));
77    /// assert_eq!(Language::from_extension("rs"), Some(Language::Rust));
78    /// ```
79    pub fn from_extension(ext: &str) -> Option<Self> {
80        match ext {
81            // TypeScript
82            "ts" | "mts" | "cts" => Some(Language::TypeScript),
83            "tsx" | "mtsx" | "ctsx" => Some(Language::TypeScriptReact),
84            // JavaScript
85            "js" | "mjs" | "cjs" => Some(Language::JavaScript),
86            "jsx" | "mjsx" | "cjsx" => Some(Language::JavaScriptReact),
87            // Go
88            "go" => Some(Language::Go),
89            // Java
90            "java" => Some(Language::Java),
91            // Python
92            "py" | "pyw" => Some(Language::Python),
93            // Rust
94            "rs" => Some(Language::Rust),
95            // Vue Single File Component
96            "vue" => Some(Language::Vue),
97            // C#
98            "cs" => Some(Language::CSharp),
99            // C
100            "c" => Some(Language::C),
101            "h" => Some(Language::CHeader),
102            // Unknown
103            _ => None,
104        }
105    }
106
107    /// Line-comment prefix used to recognize `hotspots-ignore` suppression
108    /// comments in this language's source syntax.
109    ///
110    /// Every supported language uses `//` line comments except Python, which
111    /// uses `#`.
112    pub fn suppression_comment_prefix(&self) -> &'static str {
113        match self {
114            Language::Python => "#",
115            _ => "//",
116        }
117    }
118
119    /// Detect language from file path
120    ///
121    /// Returns `None` if the file has no extension or the extension is not recognized.
122    ///
123    /// # Examples
124    ///
125    /// ```
126    /// use hotspots_core::language::Language;
127    /// use std::path::Path;
128    ///
129    /// assert_eq!(
130    ///     Language::from_path(Path::new("src/main.ts")),
131    ///     Some(Language::TypeScript)
132    /// );
133    /// assert_eq!(
134    ///     Language::from_path(Path::new("main.go")),
135    ///     Some(Language::Go)
136    /// );
137    /// assert_eq!(
138    ///     Language::from_path(Path::new("lib.rs")),
139    ///     Some(Language::Rust)
140    /// );
141    /// ```
142    pub fn from_path(path: &Path) -> Option<Self> {
143        path.extension()
144            .and_then(|ext| ext.to_str())
145            .and_then(Self::from_extension)
146    }
147
148    /// Get the canonical name of the language
149    ///
150    /// # Examples
151    ///
152    /// ```
153    /// use hotspots_core::language::Language;
154    ///
155    /// assert_eq!(Language::TypeScript.name(), "TypeScript");
156    /// assert_eq!(Language::Go.name(), "Go");
157    /// assert_eq!(Language::Python.name(), "Python");
158    /// assert_eq!(Language::Rust.name(), "Rust");
159    /// ```
160    pub fn name(&self) -> &'static str {
161        match self {
162            Language::TypeScript => "TypeScript",
163            Language::TypeScriptReact => "TypeScript React",
164            Language::JavaScript => "JavaScript",
165            Language::JavaScriptReact => "JavaScript React",
166            Language::Go => "Go",
167            Language::Java => "Java",
168            Language::Python => "Python",
169            Language::Rust => "Rust",
170            Language::Vue => "Vue",
171            Language::CSharp => "C#",
172            Language::C => "C",
173            Language::CHeader => "C Header",
174        }
175    }
176
177    /// Check if this is a TypeScript variant
178    pub fn is_typescript(&self) -> bool {
179        matches!(self, Language::TypeScript | Language::TypeScriptReact)
180    }
181
182    /// Check if this is a JavaScript variant
183    pub fn is_javascript(&self) -> bool {
184        matches!(self, Language::JavaScript | Language::JavaScriptReact)
185    }
186
187    /// Check if this is a TypeScript or JavaScript variant
188    pub fn is_ecmascript(&self) -> bool {
189        self.is_typescript() || self.is_javascript()
190    }
191
192    /// Get file extensions for this language
193    ///
194    /// Returns a list of file extensions (without the dot) that this language uses.
195    pub fn extensions(&self) -> &[&'static str] {
196        match self {
197            Language::TypeScript => &["ts", "mts", "cts"],
198            Language::TypeScriptReact => &["tsx", "mtsx", "ctsx"],
199            Language::JavaScript => &["js", "mjs", "cjs"],
200            Language::JavaScriptReact => &["jsx", "mjsx", "cjsx"],
201            Language::Go => &["go"],
202            Language::Java => &["java"],
203            Language::Python => &["py", "pyw"],
204            Language::Rust => &["rs"],
205            Language::Vue => &["vue"],
206            Language::CSharp => &["cs"],
207            Language::C => &["c"],
208            Language::CHeader => &["h"],
209        }
210    }
211
212    /// Parse from canonical name string (as returned by `name()`).
213    pub fn from_name(s: &str) -> Option<Self> {
214        match s {
215            "TypeScript" => Some(Language::TypeScript),
216            "TypeScript React" => Some(Language::TypeScriptReact),
217            "JavaScript" => Some(Language::JavaScript),
218            "JavaScript React" => Some(Language::JavaScriptReact),
219            "Go" => Some(Language::Go),
220            "Java" => Some(Language::Java),
221            "Python" => Some(Language::Python),
222            "Rust" => Some(Language::Rust),
223            "Vue" => Some(Language::Vue),
224            "C#" => Some(Language::CSharp),
225            "C" => Some(Language::C),
226            "C Header" => Some(Language::CHeader),
227            _ => None,
228        }
229    }
230}
231
232impl Serialize for Language {
233    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
234        s.serialize_str(self.name())
235    }
236}
237
238impl<'de> Deserialize<'de> for Language {
239    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
240        let s = String::deserialize(d)?;
241        Language::from_name(&s)
242            .ok_or_else(|| serde::de::Error::custom(format!("unknown language: {s}")))
243    }
244}
245
246#[cfg(test)]
247mod tests {
248    use super::*;
249
250    #[test]
251    fn test_from_extension_typescript() {
252        assert_eq!(Language::from_extension("ts"), Some(Language::TypeScript));
253        assert_eq!(Language::from_extension("mts"), Some(Language::TypeScript));
254        assert_eq!(Language::from_extension("cts"), Some(Language::TypeScript));
255        assert_eq!(
256            Language::from_extension("tsx"),
257            Some(Language::TypeScriptReact)
258        );
259    }
260
261    #[test]
262    fn test_from_extension_javascript() {
263        assert_eq!(Language::from_extension("js"), Some(Language::JavaScript));
264        assert_eq!(Language::from_extension("mjs"), Some(Language::JavaScript));
265        assert_eq!(Language::from_extension("cjs"), Some(Language::JavaScript));
266        assert_eq!(
267            Language::from_extension("jsx"),
268            Some(Language::JavaScriptReact)
269        );
270    }
271
272    #[test]
273    fn test_from_extension_go() {
274        assert_eq!(Language::from_extension("go"), Some(Language::Go));
275    }
276
277    #[test]
278    fn test_from_extension_rust() {
279        assert_eq!(Language::from_extension("rs"), Some(Language::Rust));
280    }
281
282    #[test]
283    fn test_from_extension_java() {
284        assert_eq!(Language::from_extension("java"), Some(Language::Java));
285    }
286
287    #[test]
288    fn test_from_extension_unknown() {
289        assert_eq!(Language::from_extension("cpp"), None);
290        assert_eq!(Language::from_extension(""), None);
291    }
292
293    #[test]
294    fn test_from_extension_c() {
295        assert_eq!(Language::from_extension("c"), Some(Language::C));
296        assert_eq!(Language::from_extension("h"), Some(Language::CHeader));
297    }
298
299    #[test]
300    fn test_from_path() {
301        assert_eq!(
302            Language::from_path(Path::new("src/main.ts")),
303            Some(Language::TypeScript)
304        );
305        assert_eq!(
306            Language::from_path(Path::new("src/component.tsx")),
307            Some(Language::TypeScriptReact)
308        );
309        assert_eq!(
310            Language::from_path(Path::new("main.go")),
311            Some(Language::Go)
312        );
313        assert_eq!(
314            Language::from_path(Path::new("lib.rs")),
315            Some(Language::Rust)
316        );
317        assert_eq!(Language::from_path(Path::new("README.md")), None);
318        assert_eq!(Language::from_path(Path::new("Makefile")), None);
319    }
320
321    #[test]
322    fn test_name() {
323        assert_eq!(Language::TypeScript.name(), "TypeScript");
324        assert_eq!(Language::TypeScriptReact.name(), "TypeScript React");
325        assert_eq!(Language::JavaScript.name(), "JavaScript");
326        assert_eq!(Language::JavaScriptReact.name(), "JavaScript React");
327        assert_eq!(Language::Go.name(), "Go");
328        assert_eq!(Language::Rust.name(), "Rust");
329    }
330
331    #[test]
332    fn test_is_typescript() {
333        assert!(Language::TypeScript.is_typescript());
334        assert!(Language::TypeScriptReact.is_typescript());
335        assert!(!Language::JavaScript.is_typescript());
336        assert!(!Language::Go.is_typescript());
337        assert!(!Language::Rust.is_typescript());
338    }
339
340    #[test]
341    fn test_is_javascript() {
342        assert!(Language::JavaScript.is_javascript());
343        assert!(Language::JavaScriptReact.is_javascript());
344        assert!(!Language::TypeScript.is_javascript());
345        assert!(!Language::Go.is_javascript());
346        assert!(!Language::Rust.is_javascript());
347    }
348
349    #[test]
350    fn test_is_ecmascript() {
351        assert!(Language::TypeScript.is_ecmascript());
352        assert!(Language::TypeScriptReact.is_ecmascript());
353        assert!(Language::JavaScript.is_ecmascript());
354        assert!(Language::JavaScriptReact.is_ecmascript());
355        assert!(!Language::Go.is_ecmascript());
356        assert!(!Language::Rust.is_ecmascript());
357    }
358
359    #[test]
360    fn test_extensions() {
361        assert_eq!(Language::TypeScript.extensions(), &["ts", "mts", "cts"]);
362        assert_eq!(
363            Language::TypeScriptReact.extensions(),
364            &["tsx", "mtsx", "ctsx"]
365        );
366        assert_eq!(Language::Go.extensions(), &["go"]);
367        assert_eq!(Language::Rust.extensions(), &["rs"]);
368    }
369}