Skip to main content

agentshield/parser/typescript/
mod.rs

1pub(crate) mod ast;
2pub(crate) mod classify;
3pub(crate) mod fallback;
4pub(crate) mod patterns;
5
6#[cfg(test)]
7mod tests;
8
9#[cfg(feature = "typescript")]
10use std::collections::HashSet;
11use std::path::Path;
12#[cfg(feature = "typescript")]
13use std::path::PathBuf;
14
15#[cfg(feature = "typescript")]
16use ast::{collect_params, walk_node};
17#[cfg(feature = "typescript")]
18use classify::detect_sanitizer_assignments;
19#[cfg(not(feature = "typescript"))]
20use fallback::parse_file_fallback;
21
22use crate::error::Result;
23use crate::ir::Language;
24use crate::parser::LanguageParser;
25use crate::parser::ParsedFile;
26
27/// Parser for TypeScript and JavaScript source files (.ts, .tsx, .js, .jsx).
28pub struct TypeScriptParser;
29
30#[cfg(feature = "typescript")]
31impl LanguageParser for TypeScriptParser {
32    fn language(&self) -> Language {
33        Language::TypeScript
34    }
35
36    fn parse_file(&self, path: &Path, content: &str) -> Result<ParsedFile> {
37        let mut parser = tree_sitter::Parser::new();
38        let is_tsx = path
39            .extension()
40            .is_some_and(|ext| ext == "tsx" || ext == "jsx");
41
42        let lang = if is_tsx {
43            tree_sitter_typescript::LANGUAGE_TSX
44        } else {
45            tree_sitter_typescript::LANGUAGE_TYPESCRIPT
46        };
47
48        parser
49            .set_language(&lang.into())
50            .map_err(|e| crate::error::ShieldError::Parse {
51                file: path.display().to_string(),
52                message: format!("Failed to load TypeScript grammar: {e}"),
53            })?;
54
55        let tree = parser
56            .parse(content, None)
57            .ok_or_else(|| crate::error::ShieldError::Parse {
58                file: path.display().to_string(),
59                message: "tree-sitter failed to parse TypeScript".into(),
60            })?;
61
62        let file_path = PathBuf::from(path);
63        let source = content.as_bytes();
64        let mut parsed = ParsedFile::default();
65        let mut param_names = HashSet::new();
66
67        // Phase 0: Detect sanitizer assignments via regex on source text
68        detect_sanitizer_assignments(content, &mut parsed.sanitized_vars);
69
70        // Phase 1: Collect function parameters + function defs
71        collect_params(
72            tree.root_node(),
73            source,
74            &file_path,
75            &mut param_names,
76            &mut parsed,
77        );
78
79        // Phase 2: Walk AST for call expressions, call sites, and env accesses
80        walk_node(
81            tree.root_node(),
82            source,
83            &file_path,
84            &param_names,
85            &mut parsed,
86        );
87
88        Ok(parsed)
89    }
90}
91
92#[cfg(not(feature = "typescript"))]
93impl LanguageParser for TypeScriptParser {
94    fn language(&self) -> Language {
95        Language::TypeScript
96    }
97
98    fn parse_file(&self, path: &Path, content: &str) -> Result<ParsedFile> {
99        parse_file_fallback(path, content)
100    }
101}