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