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