Skip to main content

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