AirLibrary/Indexing/Language/
ParseRust.rs

1//! # ParseRust
2//!
3//! ## File: Indexing/Language/ParseRust.rs
4//!
5//! ## Role in Air Architecture
6//!
7//! Provides Rust-specific symbol extraction functionality for the File Indexer
8//! service, identifying Rust language constructs like structs, impl blocks,
9//! functions, modules, enums, and traits.
10//!
11//! ## Primary Responsibility
12//!
13//! Extract Rust code symbols from source files for VSCode Outline View and
14//! Go to Symbol features.
15//!
16//! ## Secondary Responsibilities
17//!
18//! - Extract struct definitions
19//! - Extract impl blocks
20//! - Extract function definitions
21//! - Extract module declarations
22//! - Extract enum definitions
23//! - Extract trait definitions
24//! - Extract type aliases
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 Rust 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-Rust 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 Rust symbols (struct, impl, fn, mod, enum, trait)
71pub fn ExtractRustSymbols(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		// Check for comments and skip them
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(ExtractRustSymbolsFromLine(line_content, line_num, line, file_path));
86	}
87
88	symbols
89}
90
91/// Extract symbols from a single line of Rust code
92fn ExtractRustSymbolsFromLine(line_content:&str, line_num:u32, line:&str, file_path:&PathBuf) -> Vec<SymbolInfo> {
93	let mut symbols = Vec::new();
94
95	// Struct
96	if let Some(rest) = line_content.strip_prefix("struct ") {
97		let name = rest.split_whitespace().next().unwrap_or("").trim_end_matches('{');
98		if !name.is_empty() {
99			if let Some(col) = line.find("struct") {
100				symbols.push(SymbolInfo {
101					name:name.to_string(),
102					kind:SymbolKind::Struct,
103					line:line_num,
104					column:col as u32,
105					full_path:format!("{}::{}", file_path.display(), name),
106				});
107			}
108		}
109	}
110
111	// impl
112	if let Some(rest) = line_content.strip_prefix("impl ") {
113		let name = rest.split_whitespace().next().unwrap_or("").trim_end_matches('{');
114		if !name.is_empty() {
115			if let Some(col) = line.find("impl") {
116				symbols.push(SymbolInfo {
117					name:name.to_string(),
118					kind:SymbolKind::Method,
119					line:line_num,
120					column:col as u32,
121					full_path:format!("{}::{}::", file_path.display(), name),
122				});
123			}
124		}
125	}
126
127	// Function
128	if let Some(rest) = line_content.strip_prefix("fn ") {
129		let name = rest.split(|c| c == '(' || c == '<' || c == ':').next().unwrap_or("").trim();
130		if !name.is_empty() {
131			if let Some(col) = line.find("fn") {
132				symbols.push(SymbolInfo {
133					name:name.to_string(),
134					kind:SymbolKind::Function,
135					line:line_num,
136					column:col as u32,
137					full_path:format!("{}::{}", file_path.display(), name),
138				});
139			}
140		}
141	}
142
143	// Module
144	if let Some(rest) = line_content.strip_prefix("mod ") {
145		let name = rest.split_whitespace().next().unwrap_or("").trim_end_matches('{');
146		if !name.is_empty() {
147			if let Some(col) = line.find("mod") {
148				symbols.push(SymbolInfo {
149					name:name.to_string(),
150					kind:SymbolKind::Module,
151					line:line_num,
152					column:col as u32,
153					full_path:format!("{}::{}::", file_path.display(), name),
154				});
155			}
156		}
157	}
158
159	// Enum
160	if let Some(rest) = line_content.strip_prefix("enum ") {
161		let name = rest.split_whitespace().next().unwrap_or("").trim_end_matches('{');
162		if !name.is_empty() {
163			if let Some(col) = line.find("enum") {
164				symbols.push(SymbolInfo {
165					name:name.to_string(),
166					kind:SymbolKind::Enum,
167					line:line_num,
168					column:col as u32,
169					full_path:format!("{}::{}", file_path.display(), name),
170				});
171			}
172		}
173	}
174
175	// Trait
176	if let Some(rest) = line_content.strip_prefix("trait ") {
177		let name = rest.split_whitespace().next().unwrap_or("").trim_end_matches('{');
178		if !name.is_empty() {
179			if let Some(col) = line.find("trait") {
180				symbols.push(SymbolInfo {
181					name:name.to_string(),
182					kind:SymbolKind::Interface,
183					line:line_num,
184					column:col as u32,
185					full_path:format!("{}::{}", file_path.display(), name),
186				});
187			}
188		}
189	}
190
191	// Type alias
192	if let Some(rest) = line_content.strip_prefix("type ") {
193		let name = rest.split('=').next().unwrap_or("").trim().trim_end_matches(';');
194		if !name.is_empty() {
195			if let Some(col) = line.find("type") {
196				symbols.push(SymbolInfo {
197					name:name.to_string(),
198					kind:SymbolKind::TypeParameter,
199					line:line_num,
200					column:col as u32,
201					full_path:format!("{}::{}", file_path.display(), name),
202				});
203			}
204		}
205	}
206
207	// Const
208	if line_content.starts_with("const ") && !line_content.contains('=') {
209		if let Some(rest) = line_content.strip_prefix("const ") {
210			let name = rest.split(|c| c == ':' || c == '=').next().unwrap_or("").trim();
211			if !name.is_empty() {
212				if let Some(col) = line.find("const") {
213					symbols.push(SymbolInfo {
214						name:name.to_string(),
215						kind:SymbolKind::Constant,
216						line:line_num,
217						column:col as u32,
218						full_path:format!("{}::{}", file_path.display(), name),
219					});
220				}
221			}
222		}
223	}
224
225	// Static
226	if line_content.starts_with("static ") {
227		if let Some(rest) = line_content.strip_prefix("static ") {
228			let name = rest.split(|c| c == ':' || c == '=').next().unwrap_or("").trim();
229			if !name.is_empty() {
230				if let Some(col) = line.find("static") {
231					symbols.push(SymbolInfo {
232						name:name.to_string(),
233						kind:SymbolKind::Variable,
234						line:line_num,
235						column:col as u32,
236						full_path:format!("{}::{}", file_path.display(), name),
237					});
238				}
239			}
240		}
241	}
242
243	symbols
244}
245
246/// Check if a line contains a Rust struct definition
247pub fn IsRustStruct(line:&str) -> bool {
248	let trimmed = line.trim();
249	let after_keywords = trimmed
250		.strip_prefix("pub ")
251		.or_else(|| trimmed.strip_prefix("unsafe "))
252		.or_else(|| trimmed.strip_prefix("pub(crate) "))
253		.unwrap_or(trimmed);
254	after_keywords.starts_with("struct ")
255}
256
257/// Check if a line contains a Rust function definition
258pub fn IsRustFunction(line:&str) -> bool {
259	let trimmed = line.trim();
260	let after_keywords = trimmed
261		.strip_prefix("pub ")
262		.or_else(|| trimmed.strip_prefix("pub(crate) "))
263		.or_else(|| trimmed.strip_prefix("unsafe "))
264		.or_else(|| trimmed.strip_prefix("async "))
265		.unwrap_or(trimmed);
266	after_keywords.starts_with("fn ")
267}
268
269/// Check if a line contains a Rust impl block
270pub fn IsRustImpl(line:&str) -> bool {
271	// Handle variations: impl, pub impl, unsafe impl
272	let trimmed = line.trim();
273	let after_keywords = trimmed
274		.strip_prefix("pub ")
275		.or_else(|| trimmed.strip_prefix("unsafe "))
276		.unwrap_or(trimmed);
277	after_keywords.starts_with("impl ")
278}
279
280/// Extract Rust visibility modifier if present
281pub fn ExtractVisibilityModifier(line:&str) -> Option<&str> {
282	let trimmed = line.trim();
283	if trimmed.starts_with("pub ") {
284		Some("pub")
285	} else if trimmed.starts_with("pub(crate) ") {
286		Some("pub(crate)")
287	} else if trimmed.starts_with("pub(super) ") {
288		Some("pub(super)")
289	} else if trimmed.starts_with("pub(in ") {
290		// Extract the path part
291		let rest = trimmed.strip_prefix("pub(in ").unwrap_or("");
292		let path = rest.split(')').next().unwrap_or("");
293		if !path.is_empty() {
294			Some(&trimmed[0..trimmed.find(')').unwrap_or(trimmed.len()) + 1])
295		} else {
296			None
297		}
298	} else {
299		None
300	}
301}