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
101		if !name.is_empty() {
102			if let Some(col) = line.find("class") {
103				symbols.push(SymbolInfo {
104					name:name.to_string(),
105					kind:SymbolKind::Class,
106					line:line_num,
107					column:col as u32,
108					full_path:format!("{}::{}", file_path.display(), name),
109				});
110			}
111		}
112	}
113
114	// Interface
115	if let Some(rest) = line_content.strip_prefix("interface ") {
116		let name = rest.split(|c| c == '{' || c == '<' || c == ' ').next().unwrap_or("").trim();
117
118		if !name.is_empty() {
119			if let Some(col) = line.find("interface") {
120				symbols.push(SymbolInfo {
121					name:name.to_string(),
122					kind:SymbolKind::Interface,
123					line:line_num,
124					column:col as u32,
125					full_path:format!("{}::{}", file_path.display(), name),
126				});
127			}
128		}
129	}
130
131	// Type
132	if let Some(rest) = line_content.strip_prefix("type ") {
133		// Handle type aliases which may end with = or {
134		let name = rest.split(|c| c == '=' || c == '{' || c == ';').next().unwrap_or("").trim();
135
136		if !name.is_empty() {
137			if let Some(col) = line.find("type") {
138				symbols.push(SymbolInfo {
139					name:name.to_string(),
140					kind:SymbolKind::TypeParameter,
141					line:line_num,
142					column:col as u32,
143					full_path:format!("{}::{}", file_path.display(), name),
144				});
145			}
146		}
147	}
148
149	// Enum
150	if let Some(rest) = line_content.strip_prefix("enum ") {
151		let name = rest.split(|c| c == '{' || c == ';').next().unwrap_or("").trim();
152
153		if !name.is_empty() {
154			if let Some(col) = line.find("enum") {
155				symbols.push(SymbolInfo {
156					name:name.to_string(),
157					kind:SymbolKind::Enum,
158					line:line_num,
159					column:col as u32,
160					full_path:format!("{}::{}", file_path.display(), name),
161				});
162			}
163		}
164	}
165
166	// Function declaration
167	if let Some(rest) = line_content.strip_prefix("function ") {
168		let name = rest.split('(').next().unwrap_or("").trim();
169
170		if !name.is_empty() {
171			// Check for arrow functions: const name = () => {}
172			if !name.contains("=") {
173				if let Some(col) = line.find("function") {
174					symbols.push(SymbolInfo {
175						name:name.to_string(),
176						kind:SymbolKind::Function,
177						line:line_num,
178						column:col as u32,
179						full_path:format!("{}::{}", file_path.display(), name),
180					});
181				}
182			}
183		}
184	}
185
186	// Arrow function
187	if line_content.contains("=>") {
188		if let Some(col) = line.find("=>") {
189			let before_arrow = &line[..col];
190
191			// Try to extract function name
192			let name_part = before_arrow.split('=').next().unwrap_or("").trim();
193
194			let func_name = if name_part.contains("(") || name_part.contains("<") {
195				let mut parts = name_part.split(|c| c == '(' || c == '<' || c == ':');
196
197				let name = parts.next().unwrap_or("").trim();
198
199				name
200			} else {
201				name_part
202			};
203
204			// Filter out keywords and non-names
205			if !func_name.is_empty() && func_name != "const" && func_name != "let" && func_name != "var" {
206				symbols.push(SymbolInfo {
207					name:func_name.to_string(),
208					kind:SymbolKind::Function,
209					line:line_num,
210					column:col as u32,
211					full_path:format!("{}::{}", file_path.display(), func_name),
212				});
213			}
214		}
215	}
216
217	// Const/let/var declarations
218	for kw in &["const ", "let ", "var "] {
219		if let Some(rest) = line_content.strip_prefix(kw) {
220			let name = rest.split(|c| c == '=' || c == ':' || c == ';').next().unwrap_or("").trim();
221
222			// Check if it's a function assignment: const myFunc = () => {}
223			let _is_function_assignment = !line_content.contains("=>")
224				&& !line_content.contains("function")
225				&& (line_content.contains("=>") || rest.to_lowercase().contains("function"));
226
227			if !name.is_empty() {
228				// Determine if it's a constant or variable
229				let kind = if line_content.starts_with("const ") {
230					SymbolKind::Constant
231				} else {
232					SymbolKind::Variable
233				};
234
235				if let Some(col) = line.find(kw) {
236					symbols.push(SymbolInfo {
237						name:name.to_string(),
238						kind,
239						line:line_num,
240						column:col as u32,
241						full_path:format!("{}::{}", file_path.display(), name),
242					});
243				}
244			}
245		}
246	}
247
248	// Namespace
249	if let Some(rest) = line_content.strip_prefix("namespace ") {
250		let name = rest.split(|c| c == '{' || c == ';').next().unwrap_or("").trim();
251
252		if !name.is_empty() {
253			if let Some(col) = line.find("namespace") {
254				symbols.push(SymbolInfo {
255					name:name.to_string(),
256					kind:SymbolKind::Namespace,
257					line:line_num,
258					column:col as u32,
259					full_path:format!("{}::{}", file_path.display(), name),
260				});
261			}
262		}
263	}
264
265	symbols
266}
267
268/// Check if a line contains a TypeScript/JavaScript class definition
269pub fn IsTypeScriptClass(line:&str) -> bool {
270	let trimmed = line.trim();
271
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
278	after_keywords.starts_with("class ") && !after_keywords.contains(" extends ")
279}
280
281/// Check if a line contains a TypeScript/JavaScript interface definition
282pub fn IsTypeScriptInterface(line:&str) -> bool {
283	let trimmed = line.trim();
284
285	let after_keywords = trimmed
286		.strip_prefix("export ")
287		.or_else(|| trimmed.strip_prefix("default "))
288		.or_else(|| trimmed.strip_prefix("declare "))
289		.unwrap_or(trimmed);
290
291	after_keywords.starts_with("interface ")
292}
293
294/// Check if a line contains a TypeScript/JavaScript function definition
295pub fn IsTypeScriptFunction(line:&str) -> bool {
296	let trimmed = line.trim();
297
298	let after_keywords = trimmed
299		.strip_prefix("export ")
300		.or_else(|| trimmed.strip_prefix("default "))
301		.or_else(|| trimmed.strip_prefix("declare "))
302		.or_else(|| trimmed.strip_prefix("async "))
303		.unwrap_or(trimmed);
304
305	after_keywords.starts_with("function ")
306}
307
308/// Extract TypeScript/JavaScript export modifier if present
309pub fn ExtractExportModifier(line:&str) -> Option<&str> {
310	let trimmed = line.trim();
311
312	if trimmed.starts_with("export ") {
313		Some("export")
314	} else if trimmed.starts_with("export default ") {
315		Some("export default")
316	} else if trimmed.starts_with("export type ") {
317		Some("export type")
318	} else if trimmed.starts_with("export const ") {
319		Some("export const")
320	} else if trimmed.starts_with("export function ") {
321		Some("export function")
322	} else if trimmed.starts_with("export interface ") {
323		Some("export interface")
324	} else if trimmed.starts_with("export class ") {
325		Some("export class")
326	} else {
327		None
328	}
329}
330
331/// Extract TypeScript/JavaScript type annotation from a declaration
332pub fn ExtractTypeAnnotation(line:&str) -> Option<String> {
333	if let Some(colon_idx) = line.find(':') {
334		let rest = &line[colon_idx + 1..];
335
336		// Find the end of the type annotation (before =, {, ;, or <)
337		let end_idx = rest
338			.find(|c| c == '=' || c == '{' || c == ';' || c == ',')
339			.unwrap_or(rest.len());
340
341		let type_str = rest[..end_idx].trim();
342
343		if !type_str.is_empty() { Some(type_str.to_string()) } else { None }
344	} else {
345		None
346	}
347}
348
349/// Parse TypeScript/JavaScript generic parameters
350pub fn ExtractGenericParameters(line:&str) -> Vec<String> {
351	let mut generics = Vec::new();
352
353	if let Some(start) = line.find('<') {
354		if let Some(end) = line.rfind('>') {
355			let content = &line[start + 1..end];
356
357			// Split by comma and trim
358			for part in content.split(',') {
359				let trimmed = part.trim();
360
361				if !trimmed.is_empty() {
362					generics.push(trimmed.to_string());
363				}
364			}
365		}
366	}
367
368	generics
369}