use crate::analysis::rust::macro_analyzer::{MacroAnalyzer, SummerMacro};
use crate::protocol::types::{LocationResponse, PositionResponse, RangeResponse};
use lsp_types::Url;
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::Path;
use walkdir::WalkDir;
pub struct ComponentScanner {
macro_analyzer: MacroAnalyzer,
}
impl ComponentScanner {
pub fn new() -> Self {
Self {
macro_analyzer: MacroAnalyzer::new(),
}
}
pub fn scan_components(
&self,
project_path: &Path,
) -> Result<Vec<ComponentInfoResponse>, ScanError> {
tracing::info!("Starting component scan in: {:?}", project_path);
let mut components = Vec::new();
let src_path = project_path.join("src");
if src_path.exists() && src_path.is_dir() {
tracing::info!("Found src directory, scanning single project");
components.extend(self.scan_single_project(project_path)?);
} else {
tracing::info!("No src directory found, searching for summer-rs projects in workspace");
components.extend(self.scan_workspace(project_path)?);
}
tracing::info!("Total components found: {}", components.len());
Ok(components)
}
fn scan_single_project(
&self,
project_path: &Path,
) -> Result<Vec<ComponentInfoResponse>, ScanError> {
let mut components = Vec::new();
let src_path = project_path.join("src");
tracing::info!("Looking for src directory: {:?}", src_path);
if !src_path.exists() {
tracing::error!("src directory not found at: {:?}", src_path);
return Err(ScanError::InvalidProject(
"src directory not found".to_string(),
));
}
tracing::info!("Found src directory, starting file scan...");
let mut file_count = 0;
let mut parsed_count = 0;
let mut macro_count = 0;
for entry in WalkDir::new(&src_path)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| e.path().extension().is_some_and(|ext| ext == "rs"))
{
file_count += 1;
let file_path = entry.path();
tracing::info!("Scanning file {}: {:?}", file_count, file_path);
let content = match fs::read_to_string(file_path) {
Ok(content) => {
tracing::info!("Successfully read file, size: {} bytes", content.len());
content
}
Err(e) => {
tracing::warn!("Failed to read file {:?}: {}", file_path, e);
continue;
}
};
let file_url = match Url::from_file_path(file_path) {
Ok(url) => {
tracing::info!("Converted to URL: {}", url);
url
}
Err(_) => {
tracing::warn!("Failed to convert path to URL: {:?}", file_path);
continue;
}
};
let rust_doc = match self.macro_analyzer.parse(file_url.clone(), content) {
Ok(doc) => {
parsed_count += 1;
tracing::info!("Successfully parsed file");
doc
}
Err(e) => {
tracing::warn!("Failed to parse file {:?}: {}", file_path, e);
continue;
}
};
let rust_doc = match self.macro_analyzer.extract_macros(rust_doc) {
Ok(doc) => {
tracing::info!("Extracted {} macros from file", doc.macros.len());
macro_count += doc.macros.len();
doc
}
Err(e) => {
tracing::warn!("Failed to extract macros from {:?}: {}", file_path, e);
continue;
}
};
for summer_macro in &rust_doc.macros {
match summer_macro {
SummerMacro::DeriveService(service_macro) => {
tracing::info!(
"Found Service component: {} in {:?}",
service_macro.struct_name,
file_path
);
components.push(ComponentInfoResponse {
name: service_macro.struct_name.clone(),
type_name: service_macro.struct_name.clone(),
scope: ComponentScope::Singleton, source: ComponentSource::Service,
dependencies: service_macro
.fields
.iter()
.filter_map(|field| {
field.inject.as_ref().map(|_| field.type_name.clone())
})
.collect(),
location: LocationResponse {
uri: file_url.to_string(),
range: RangeResponse {
start: PositionResponse {
line: service_macro.range.start.line,
character: service_macro.range.start.character,
},
end: PositionResponse {
line: service_macro.range.end.line,
character: service_macro.range.end.character,
},
},
},
});
}
SummerMacro::Component(component_macro) => {
tracing::info!(
"Found Component function: {} -> {} in {:?}",
component_macro.function_name,
component_macro.component_type,
file_path
);
components.push(ComponentInfoResponse {
name: component_macro.component_type.clone(),
type_name: component_macro.component_type.clone(),
scope: ComponentScope::Singleton, source: ComponentSource::Component,
dependencies: component_macro
.dependencies
.iter()
.map(|dep| dep.type_name.clone())
.collect(),
location: LocationResponse {
uri: file_url.to_string(),
range: RangeResponse {
start: PositionResponse {
line: component_macro.range.start.line,
character: component_macro.range.start.character,
},
end: PositionResponse {
line: component_macro.range.end.line,
character: component_macro.range.end.character,
},
},
},
});
}
_ => {}
}
}
}
tracing::info!(
"Scanned {} Rust files, parsed {} files, found {} macros, extracted {} components",
file_count,
parsed_count,
macro_count,
components.len()
);
Ok(components)
}
fn scan_workspace(
&self,
workspace_path: &Path,
) -> Result<Vec<ComponentInfoResponse>, ScanError> {
let mut all_components = Vec::new();
let mut project_count = 0;
for entry in WalkDir::new(workspace_path)
.max_depth(5) .into_iter()
.filter_map(|e| e.ok())
.filter(|e| e.file_name() == "Cargo.toml")
{
let cargo_toml_path = entry.path();
let project_dir = cargo_toml_path.parent().unwrap();
let src_dir = project_dir.join("src");
if !src_dir.exists() {
continue;
}
if !self.is_summer_rs_project(cargo_toml_path) {
continue;
}
tracing::info!("Found summer-rs project: {:?}", project_dir);
project_count += 1;
match self.scan_single_project(project_dir) {
Ok(components) => {
tracing::info!("Found {} components in {:?}", components.len(), project_dir);
all_components.extend(components);
}
Err(e) => {
tracing::warn!("Failed to scan project {:?}: {}", project_dir, e);
}
}
}
tracing::info!("Scanned {} summer-rs projects in workspace", project_count);
Ok(all_components)
}
fn is_summer_rs_project(&self, cargo_toml_path: &Path) -> bool {
let content = match fs::read_to_string(cargo_toml_path) {
Ok(content) => content,
Err(_) => return false,
};
content.contains("summer")
&& (content.contains("summer-web")
|| content.contains("summer-sqlx")
|| content.contains("summer-redis")
|| content.contains("\"summer\""))
}
}
impl Default for ComponentScanner {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ComponentScope {
Singleton,
Prototype,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ComponentSource {
#[serde(rename = "service")]
Service,
#[serde(rename = "component")]
Component,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ComponentInfoResponse {
pub name: String,
#[serde(rename = "typeName")]
pub type_name: String,
pub scope: ComponentScope,
pub source: ComponentSource,
pub dependencies: Vec<String>,
pub location: LocationResponse,
}
#[derive(Debug, Deserialize)]
pub struct ComponentsRequest {
#[serde(rename = "appPath")]
pub app_path: String,
}
#[derive(Debug, Serialize)]
pub struct ComponentsResponse {
pub components: Vec<ComponentInfoResponse>,
}
#[derive(Debug, thiserror::Error)]
pub enum ScanError {
#[error("Failed to read file: {0}")]
FileRead(#[from] std::io::Error),
#[error("Invalid project structure: {0}")]
InvalidProject(String),
#[error("No components found")]
NoComponents,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_component_scanner_new() {
let _scanner = ComponentScanner::new();
}
#[test]
fn test_component_scanner_default() {
let _scanner = ComponentScanner::default();
}
}