Skip to main content

AirLibrary/Indexing/Language/
ParseTypeScript.rs

1//! # ParseTypeScript
2//!
3//! ## File: Indexing/Language/ParseTypeScript.rs
4//!
5//! ## Role in Air Architecture
6//!
7//! Provides TypeScript/JavaScript-specific symbol extraction functionality for
8//! the File Indexer service, identifying TS/JS language constructs like
9//! classes, interfaces, functions, constants, and types.
10//!
11//! ## Primary Responsibility
12//!
13//! Extract TypeScript/JavaScript code symbols from source files for VSCode
14//! Outline View and Go to Symbol features.
15//!
16//! ## Secondary Responsibilities
17//!
18//! - Extract class definitions
19//! - Extract interface definitions
20//! - Extract function declarations
21//! - Extract arrow functions
22//! - Extract variable declarations (const, let, var)
23//! - Extract type definitions
24//! - Extract enum definitions
25//!
26//! ## Dependencies
27//!
28//! **External Crates:**
29//! - None (uses std library)
30//!
31//! **Internal Modules:**
32//! - `crate::Result` - Error handling type
33//! - `super::super::SymbolInfo` - Symbol structure definitions
34//!
35//! ## Dependents
36//!
37//! - `Indexing::Process::ExtractSymbols` - Language routing
38//!
39//! ## VSCode Pattern Reference
40//!
41//! Inspired by VSCode's TypeScript symbol extraction in
42//! `src/vs/workbench/services/search/common/`
43//!
44//! ## Security Considerations
45//!
46//! - Line-by-line parsing without eval
47//! - No code execution during extraction
48//! - Safe string handling
49//!
50//! ## Performance Considerations
51//!
52//! - Efficient line-based parsing
53//! - Minimal allocations per file
54//! - Early termination for non-TS/JS files
55//!
56//! ## Error Handling Strategy
57//!
58//! Symbol extraction returns empty vectors on parse errors rather than
59//! failures, allowing indexing to continue for other files.
60//!
61//! ## Thread Safety
62//!
63//! Symbol extraction functions are pure and safe to call from
64//! parallel indexing tasks.
65
66use std::path::PathBuf;
67
68use crate::Indexing::State::CreateState::{SymbolInfo, SymbolKind};
69
70/// Extract TypeScript/JavaScript symbols (class, interface, function, etc.)
71pub fn ExtractTypeScriptSymbols(content:&str, file_path:&PathBuf) -> Vec<SymbolInfo> {
72	let mut symbols = Vec::new();
73
74	let lines:Vec<&str> = content.lines().collect();
75
76	for (line_idx, line) in lines.iter().enumerate() {
77		let line_content = line.trim();
78
79		let line_num = line_idx as u32 + 1;
80
81		// Skip comments
82		if line_content.starts_with("//") || line_content.starts_with("/*") || line_content.starts_with("*") {
83			continue;
84		}
85
86		// Extract symbols from this line
87		symbols.extend(ExtractTypeScriptSymbolsFromLine(line_content, line_num, line, file_path));
88	}
89
90	symbols
91}
92
93/// Extract symbols from a single line of TypeScript/JavaScript code
94fn ExtractTypeScriptSymbolsFromLine(line_content:&str, line_num:u32, line:&str, file_path:&PathBuf) -> Vec<SymbolInfo> {
95	let mut symbols = Vec::new();
96
97	// Class
98	if let Some(rest) = line_content.strip_prefix("class ") {
99		let name = rest.split(|c| c == '{' || c == '<' || c == ' ').next().unwrap_or("").trim();
100		if !name.is_empty() {
101			if let Some(col) = line.find("class") {
102				symbols.push(SymbolInfo {
103					name:name.to_string(),
104					kind:SymbolKind::Class,
105					line:line_num,
106					column:col as u32,
107					full_path:format!("{}::{}", file_path.display(), name),
108				});
109			}
110		}
111	}
112
113	// Interface
114	if let Some(rest) = line_content.strip_prefix("interface ") {
115		let name = rest.split(|c| c == '{' || c == '<' || c == ' ').next().unwrap_or("").trim();
116		if !name.is_empty() {
117			if let Some(col) = line.find("interface") {
118				symbols.push(SymbolInfo {
119					name:name.to_string(),
120					kind:SymbolKind::Interface,
121					line:line_num,
122					column:col as u32,
123					full_path:format!("{}::{}", file_path.display(), name),
124				});
125			}
126		}
127	}
128
129	// Type
130	if let Some(rest) = line_content.strip_prefix("type ") {
131		// Handle type aliases which may end with = or {
132		let name = rest.split(|c| c == '=' || c == '{' || c == ';').next().unwrap_or("").trim();
133		if !name.is_empty() {
134			if let Some(col) = line.find("type") {
135				symbols.push(SymbolInfo {
136					name:name.to_string(),
137					kind:SymbolKind::TypeParameter,
138					line:line_num,
139					column:col as u32,
140					full_path:format!("{}::{}", file_path.display(), name),
141				});
142			}
143		}
144	}
145
146	// Enum
147	if let Some(rest) = line_content.strip_prefix("enum ") {
148		let name = rest.split(|c| c == '{' || c == ';').next().unwrap_or("").trim();
149		if !name.is_empty() {
150			if let Some(col) = line.find("enum") {
151				symbols.push(SymbolInfo {
152					name:name.to_string(),
153					kind:SymbolKind::Enum,
154					line:line_num,
155					column:col as u32,
156					full_path:format!("{}::{}", file_path.display(), name),
157				});
158			}
159		}
160	}
161
162	// Function declaration
163	if let Some(rest) = line_content.strip_prefix("function ") {
164		let name = rest.split('(').next().unwrap_or("").trim();
165		if !name.is_empty() {
166			// Check for arrow functions: const name = () => {}
167			if !name.contains("=") {
168				if let Some(col) = line.find("function") {
169					symbols.push(SymbolInfo {
170						name:name.to_string(),
171						kind:SymbolKind::Function,
172						line:line_num,
173						column:col as u32,
174						full_path:format!("{}::{}", file_path.display(), name),
175					});
176				}
177			}
178		}
179	}
180
181	// Arrow function
182	if line_content.contains("=>") {
183		if let Some(col) = line.find("=>") {
184			let before_arrow = &line[..col];
185			// Try to extract function name
186			let name_part = before_arrow.split('=').next().unwrap_or("").trim();
187
188			let func_name = if name_part.contains("(") || name_part.contains("<") {
189				let mut parts = name_part.split(|c| c == '(' || c == '<' || c == ':');
190				let name = parts.next().unwrap_or("").trim();
191				name
192			} else {
193				name_part
194			};
195
196			// Filter out keywords and non-names
197			if !func_name.is_empty() && func_name != "const" && func_name != "let" && func_name != "var" {
198				symbols.push(SymbolInfo {
199					name:func_name.to_string(),
200					kind:SymbolKind::Function,
201					line:line_num,
202					column:col as u32,
203					full_path:format!("{}::{}", file_path.display(), func_name),
204				});
205			}
206		}
207	}
208
209	// Const/let/var declarations
210	for kw in &["const ", "let ", "var "] {
211		if let Some(rest) = line_content.strip_prefix(kw) {
212			let name = rest.split(|c| c == '=' || c == ':' || c == ';').next().unwrap_or("").trim();
213			// Check if it's a function assignment: const myFunc = () => {}
214			let _is_function_assignment = !line_content.contains("=>")
215				&& !line_content.contains("function")
216				&& (line_content.contains("=>") || rest.to_lowercase().contains("function"));
217
218			if !name.is_empty() {
219				// Determine if it's a constant or variable
220				let kind = if line_content.starts_with("const ") {
221					SymbolKind::Constant
222				} else {
223					SymbolKind::Variable
224				};
225
226				if let Some(col) = line.find(kw) {
227					symbols.push(SymbolInfo {
228						name:name.to_string(),
229						kind,
230						line:line_num,
231						column:col as u32,
232						full_path:format!("{}::{}", file_path.display(), name),
233					});
234				}
235			}
236		}
237	}
238
239	// Namespace
240	if let Some(rest) = line_content.strip_prefix("namespace ") {
241		let name = rest.split(|c| c == '{' || c == ';').next().unwrap_or("").trim();
242		if !name.is_empty() {
243			if let Some(col) = line.find("namespace") {
244				symbols.push(SymbolInfo {
245					name:name.to_string(),
246					kind:SymbolKind::Namespace,
247					line:line_num,
248					column:col as u32,
249					full_path:format!("{}::{}", file_path.display(), name),
250				});
251			}
252		}
253	}
254
255	symbols
256}
257
258/// Check if a line contains a TypeScript/JavaScript class definition
259pub fn IsTypeScriptClass(line:&str) -> bool {
260	let trimmed = line.trim();
261	let after_keywords = trimmed
262		.strip_prefix("export ")
263		.or_else(|| trimmed.strip_prefix("default "))
264		.or_else(|| trimmed.strip_prefix("declare "))
265		.unwrap_or(trimmed);
266	after_keywords.starts_with("class ") && !after_keywords.contains(" extends ")
267}
268
269/// Check if a line contains a TypeScript/JavaScript interface definition
270pub fn IsTypeScriptInterface(line:&str) -> bool {
271	let trimmed = line.trim();
272	let after_keywords = trimmed
273		.strip_prefix("export ")
274		.or_else(|| trimmed.strip_prefix("default "))
275		.or_else(|| trimmed.strip_prefix("declare "))
276		.unwrap_or(trimmed);
277	after_keywords.starts_with("interface ")
278}
279
280/// Check if a line contains a TypeScript/JavaScript function definition
281pub fn IsTypeScriptFunction(line:&str) -> bool {
282	let trimmed = line.trim();
283	let after_keywords = trimmed
284		.strip_prefix("export ")
285		.or_else(|| trimmed.strip_prefix("default "))
286		.or_else(|| trimmed.strip_prefix("declare "))
287		.or_else(|| trimmed.strip_prefix("async "))
288		.unwrap_or(trimmed);
289	after_keywords.starts_with("function ")
290}
291
292/// Extract TypeScript/JavaScript export modifier if present
293pub fn ExtractExportModifier(line:&str) -> Option<&str> {
294	let trimmed = line.trim();
295	if trimmed.starts_with("export ") {
296		Some("export")
297	} else if trimmed.starts_with("export default ") {
298		Some("export default")
299	} else if trimmed.starts_with("export type ") {
300		Some("export type")
301	} else if trimmed.starts_with("export const ") {
302		Some("export const")
303	} else if trimmed.starts_with("export function ") {
304		Some("export function")
305	} else if trimmed.starts_with("export interface ") {
306		Some("export interface")
307	} else if trimmed.starts_with("export class ") {
308		Some("export class")
309	} else {
310		None
311	}
312}
313
314/// Extract TypeScript/JavaScript type annotation from a declaration
315pub fn ExtractTypeAnnotation(line:&str) -> Option<String> {
316	if let Some(colon_idx) = line.find(':') {
317		let rest = &line[colon_idx + 1..];
318		// Find the end of the type annotation (before =, {, ;, or <)
319		let end_idx = rest
320			.find(|c| c == '=' || c == '{' || c == ';' || c == ',')
321			.unwrap_or(rest.len());
322		let type_str = rest[..end_idx].trim();
323		if !type_str.is_empty() { Some(type_str.to_string()) } else { None }
324	} else {
325		None
326	}
327}
328
329/// Parse TypeScript/JavaScript generic parameters
330pub fn ExtractGenericParameters(line:&str) -> Vec<String> {
331	let mut generics = Vec::new();
332	if let Some(start) = line.find('<') {
333		if let Some(end) = line.rfind('>') {
334			let content = &line[start + 1..end];
335			// Split by comma and trim
336			for part in content.split(',') {
337				let trimmed = part.trim();
338				if !trimmed.is_empty() {
339					generics.push(trimmed.to_string());
340				}
341			}
342		}
343	}
344	generics
345}