use tower_lsp::lsp_types::Url;
use windjammer::{lexer, parser};
#[salsa::db]
#[derive(Clone)]
pub struct WindjammerDatabase {
storage: salsa::Storage<Self>,
symbol_cache: std::sync::Arc<std::sync::Mutex<std::collections::HashMap<Url, bool>>>,
reference_cache: std::sync::Arc<std::sync::Mutex<std::collections::HashMap<Url, bool>>>,
}
impl Default for WindjammerDatabase {
fn default() -> Self {
Self::new()
}
}
#[salsa::db]
impl salsa::Database for WindjammerDatabase {}
#[salsa::input]
pub struct SourceFile {
#[returns(ref)]
pub uri: Url,
#[returns(ref)]
pub text: String,
}
#[salsa::tracked]
pub struct ParsedProgram<'db> {
#[returns(ref)]
pub program: parser::Program,
}
#[salsa::tracked]
pub struct ImportInfo<'db> {
#[returns(ref)]
pub imports: Vec<Url>,
}
#[salsa::tracked]
pub struct SymbolTable<'db> {
#[returns(ref)]
pub symbols: Vec<Symbol>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Symbol {
pub name: String,
pub kind: SymbolKind,
pub line: u32,
pub character: u32,
pub range: Option<SymbolRange>,
pub name_range: Option<SymbolRange>,
pub type_info: Option<String>,
pub doc: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct SymbolRange {
pub start_line: u32,
pub start_character: u32,
pub end_line: u32,
pub end_character: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct SymbolReference {
pub name: String,
pub uri: Url,
pub line: u32,
pub character: u32,
}
#[salsa::tracked]
pub struct ReferenceInfo<'db> {
#[returns(ref)]
pub references: Vec<SymbolReference>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SymbolKind {
Function,
Struct,
Enum,
Trait,
Impl,
Const,
Static,
}
#[salsa::tracked]
pub fn parse<'db>(db: &'db dyn salsa::Database, file: SourceFile) -> ParsedProgram<'db> {
let uri = file.uri(db);
let text = file.text(db);
tracing::debug!("Salsa: Parsing {}", uri);
let mut lexer = lexer::Lexer::new(text);
let tokens = lexer.tokenize_with_locations();
let mut parser = parser::Parser::new(tokens);
let program = match parser.parse() {
Ok(prog) => prog,
Err(e) => {
tracing::error!("Parse error in {}: {}", uri, e);
parser::Program { items: vec![] }
}
};
ParsedProgram::new(db, program)
}
#[salsa::tracked]
pub fn imports<'db>(db: &'db dyn salsa::Database, file: SourceFile) -> ImportInfo<'db> {
let parsed = parse(db, file);
let program = parsed.program(db);
let uri = file.uri(db);
tracing::debug!("Salsa: Extracting imports from {}", uri);
let mut import_uris = Vec::new();
for item in &program.items {
if let parser::Item::Use {
path,
alias: _,
location: _,
} = item
{
let import_path = path.join(".");
tracing::debug!("Found import: {}", import_path);
if let Some(resolved_uri) = resolve_import(uri, &import_path) {
tracing::debug!("Resolved import '{}' to {}", import_path, resolved_uri);
import_uris.push(resolved_uri);
} else {
tracing::debug!("Could not resolve import: {}", import_path);
}
}
}
tracing::debug!("Resolved {} imports from {}", import_uris.len(), uri);
ImportInfo::new(db, import_uris)
}
#[salsa::tracked]
pub fn extract_symbols<'db>(db: &'db dyn salsa::Database, file: SourceFile) -> SymbolTable<'db> {
let parsed = parse(db, file);
let program = parsed.program(db);
let uri = file.uri(db);
tracing::debug!("Salsa: Extracting symbols from {}", uri);
let mut symbols = Vec::new();
for (idx, item) in program.items.iter().enumerate() {
let line = idx as u32;
match item {
parser::Item::Function {
decl: func,
location: _,
} => {
symbols.push(Symbol {
name: func.name.clone(),
kind: SymbolKind::Function,
line,
character: 0,
range: None, name_range: None,
type_info: func.return_type.as_ref().map(|t| format!("{:?}", t)),
doc: None, });
}
parser::Item::Struct {
decl: struct_decl,
location: _,
} => {
symbols.push(Symbol {
name: struct_decl.name.clone(),
kind: SymbolKind::Struct,
line,
character: 0,
range: None,
name_range: None,
type_info: None,
doc: None,
});
}
parser::Item::Enum {
decl: enum_decl,
location: _,
} => {
symbols.push(Symbol {
name: enum_decl.name.clone(),
kind: SymbolKind::Enum,
line,
character: 0,
range: None,
name_range: None,
type_info: None,
doc: None,
});
}
parser::Item::Trait {
decl: trait_decl,
location: _,
} => {
symbols.push(Symbol {
name: trait_decl.name.clone(),
kind: SymbolKind::Trait,
line,
character: 0,
range: None,
name_range: None,
type_info: None,
doc: None,
});
}
parser::Item::Impl {
block: impl_block,
location: _,
} => {
let name = if let Some(trait_name) = &impl_block.trait_name {
format!("impl {} for {}", trait_name, impl_block.type_name)
} else {
format!("impl {}", impl_block.type_name)
};
symbols.push(Symbol {
name,
kind: SymbolKind::Impl,
line,
character: 0,
range: None,
name_range: None,
type_info: Some(impl_block.type_name.clone()),
doc: None,
});
}
parser::Item::Const { name, type_, .. } => {
symbols.push(Symbol {
name: name.clone(),
kind: SymbolKind::Const,
line,
character: 0,
range: None,
name_range: None,
type_info: Some(format!("{:?}", type_)), doc: None,
});
}
parser::Item::Static { name, type_, .. } => {
symbols.push(Symbol {
name: name.clone(),
kind: SymbolKind::Static,
line,
character: 0,
range: None,
name_range: None,
type_info: Some(format!("{:?}", type_)), doc: None,
});
}
_ => {} }
}
tracing::debug!("Found {} symbols in {}", symbols.len(), uri);
SymbolTable::new(db, symbols)
}
#[salsa::tracked]
pub fn extract_references<'db>(
db: &'db dyn salsa::Database,
file: SourceFile,
) -> ReferenceInfo<'db> {
let parsed = parse(db, file);
let program = parsed.program(db);
let uri = file.uri(db).clone();
tracing::debug!("Salsa: Extracting references from {}", uri);
let references = Vec::new();
for item in program.items.iter() {
if let parser::Item::Function {
decl: func,
location: _,
} = item
{
tracing::debug!("TODO: Scan function '{}' body for references", func.name);
}
}
tracing::debug!("Found {} references in {}", references.len(), uri);
ReferenceInfo::new(db, references)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CodeAction {
pub title: String,
pub kind: CodeActionKind,
pub edits: Vec<TextEdit>,
pub is_preferred: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CodeActionKind {
QuickFix,
RefactorExtract,
RefactorInline,
RefactorRename,
RefactorChangeSignature,
RefactorMove,
}
impl WindjammerDatabase {
pub fn get_code_actions(
&mut self,
file: SourceFile,
range: tower_lsp::lsp_types::Range,
) -> Vec<CodeAction> {
let mut actions = Vec::new();
if let Some(action) = self.try_extract_function(file, range) {
actions.push(action);
}
if let Some(action) = self.try_inline_variable(file, range) {
actions.push(action);
}
if let Some(action) = self.try_inline_function(file, range) {
actions.push(action);
}
actions
}
fn try_extract_function(
&mut self,
file: SourceFile,
range: tower_lsp::lsp_types::Range,
) -> Option<CodeAction> {
let text = file.text(self);
let lines: Vec<&str> = text.lines().collect();
let start_line = range.start.line as usize;
let end_line = range.end.line as usize;
if start_line >= lines.len() || end_line >= lines.len() {
return None;
}
let mut selected_code = String::new();
for (idx, line) in lines
.iter()
.enumerate()
.skip(start_line)
.take(end_line - start_line + 1)
{
let line_idx = idx;
if line_idx == start_line && line_idx == end_line {
let start_char = range.start.character as usize;
let end_char = range.end.character as usize;
if start_char < line.len() && end_char <= line.len() {
selected_code.push_str(&line[start_char..end_char]);
}
} else if line_idx == start_line {
let line = lines[line_idx];
let start_char = range.start.character as usize;
if start_char < line.len() {
selected_code.push_str(&line[start_char..]);
selected_code.push('\n');
}
} else if line_idx == end_line {
let line = lines[line_idx];
let end_char = range.end.character as usize;
if end_char <= line.len() {
selected_code.push_str(&line[..end_char]);
}
} else {
selected_code.push_str(lines[line_idx]);
selected_code.push('\n');
}
}
if selected_code.trim().is_empty() {
return None;
}
let function_name = "extracted_function";
let extracted_function = format!(
"fn {}() {{\n {}\n}}\n\n",
function_name,
selected_code.trim()
);
let mut edits = Vec::new();
edits.push(TextEdit {
range,
new_text: format!("{}()", function_name),
});
let insert_line = start_line.saturating_sub(1);
edits.push(TextEdit {
range: tower_lsp::lsp_types::Range {
start: tower_lsp::lsp_types::Position {
line: insert_line as u32,
character: 0,
},
end: tower_lsp::lsp_types::Position {
line: insert_line as u32,
character: 0,
},
},
new_text: extracted_function,
});
Some(CodeAction {
title: "Extract function".to_string(),
kind: CodeActionKind::RefactorExtract,
edits,
is_preferred: true,
})
}
fn try_inline_variable(
&mut self,
_file: SourceFile,
_range: tower_lsp::lsp_types::Range,
) -> Option<CodeAction> {
None
}
fn try_inline_function(
&mut self,
_file: SourceFile,
_range: tower_lsp::lsp_types::Range,
) -> Option<CodeAction> {
None
}
}
impl WindjammerDatabase {
pub fn new() -> Self {
Self {
storage: Default::default(),
symbol_cache: Default::default(),
reference_cache: Default::default(),
}
}
pub fn are_symbols_loaded(&self, file: SourceFile) -> bool {
let uri = file.uri(self);
self.symbol_cache
.lock()
.unwrap()
.get(uri)
.copied()
.unwrap_or(false)
}
pub fn mark_symbols_loaded(&self, file: SourceFile) {
let uri = file.uri(self).clone();
self.symbol_cache.lock().unwrap().insert(uri, true);
}
pub fn are_references_loaded(&self, file: SourceFile) -> bool {
let uri = file.uri(self);
self.reference_cache
.lock()
.unwrap()
.get(uri)
.copied()
.unwrap_or(false)
}
pub fn mark_references_loaded(&self, file: SourceFile) {
let uri = file.uri(self).clone();
self.reference_cache.lock().unwrap().insert(uri, true);
}
pub fn get_symbols_lazy(&mut self, file: SourceFile) -> &Vec<Symbol> {
if !self.are_symbols_loaded(file) {
let _symbols = self.get_symbols(file);
self.mark_symbols_loaded(file);
}
self.get_symbols(file)
}
pub fn get_references_lazy(&mut self, file: SourceFile) -> &Vec<SymbolReference> {
if !self.are_references_loaded(file) {
let _refs = self.get_references(file);
self.mark_references_loaded(file);
}
self.get_references(file)
}
pub fn clear_lazy_caches(&self) {
self.symbol_cache.lock().unwrap().clear();
self.reference_cache.lock().unwrap().clear();
}
pub fn preload_symbols(&mut self, files: &[SourceFile]) {
for file in files {
if !self.are_symbols_loaded(*file) {
let _ = self.get_symbols(*file);
self.mark_symbols_loaded(*file);
}
}
}
pub fn set_source_text(&mut self, uri: Url, text: String) -> SourceFile {
SourceFile::new(self, uri, text)
}
pub fn get_program(&self, file: SourceFile) -> &parser::Program {
let parsed = parse(self, file);
parsed.program(self)
}
pub fn get_imports(&self, file: SourceFile) -> &Vec<Url> {
let import_info = imports(self, file);
import_info.imports(self)
}
pub fn get_symbols(&self, file: SourceFile) -> &Vec<Symbol> {
let symbol_table = extract_symbols(self, file);
symbol_table.symbols(self)
}
pub fn get_references(&self, file: SourceFile) -> &Vec<SymbolReference> {
let reference_info = extract_references(self, file);
reference_info.references(self)
}
pub fn find_all_references(
&self,
symbol_name: &str,
files: &[SourceFile],
) -> Vec<tower_lsp::lsp_types::Location> {
let mut locations = Vec::new();
for &file in files {
let uri = file.uri(self).clone();
let symbols = self.get_symbols(file);
for symbol in symbols {
if symbol.name == symbol_name {
locations.push(tower_lsp::lsp_types::Location {
uri: uri.clone(),
range: tower_lsp::lsp_types::Range {
start: tower_lsp::lsp_types::Position {
line: symbol.line,
character: symbol.character,
},
end: tower_lsp::lsp_types::Position {
line: symbol.line,
character: symbol.character + symbol.name.len() as u32,
},
},
});
}
}
let references = self.get_references(file);
for reference in references {
if reference.name == symbol_name {
locations.push(tower_lsp::lsp_types::Location {
uri: reference.uri.clone(),
range: tower_lsp::lsp_types::Range {
start: tower_lsp::lsp_types::Position {
line: reference.line,
character: reference.character,
},
end: tower_lsp::lsp_types::Position {
line: reference.line,
character: reference.character + reference.name.len() as u32,
},
},
});
}
}
}
tracing::debug!(
"Found {} references to '{}' across {} files",
locations.len(),
symbol_name,
files.len()
);
locations
}
pub fn find_definition(
&self,
symbol_name: &str,
files: &[SourceFile],
) -> Option<tower_lsp::lsp_types::Location> {
for &file in files {
let uri = file.uri(self).clone();
let symbols = self.get_symbols(file);
for symbol in symbols {
if symbol.name == symbol_name {
tracing::debug!("Found definition of '{}' in {}", symbol_name, uri);
return Some(tower_lsp::lsp_types::Location {
uri,
range: tower_lsp::lsp_types::Range {
start: tower_lsp::lsp_types::Position {
line: symbol.line,
character: symbol.character,
},
end: tower_lsp::lsp_types::Position {
line: symbol.line,
character: symbol.character + symbol.name.len() as u32,
},
},
});
}
}
}
tracing::debug!("Definition of '{}' not found", symbol_name);
None
}
}
fn resolve_import(source_uri: &Url, import_path: &str) -> Option<Url> {
tracing::debug!("Resolving import '{}' from {}", import_path, source_uri);
if import_path.starts_with("std.") {
tracing::debug!("Skipping std library import: {}", import_path);
return None;
}
let file_path = import_path.replace('.', "/") + ".wj";
let source_path = source_uri.to_file_path().ok()?;
let source_dir = source_path.parent()?;
let relative_path = source_dir.join(&file_path);
if relative_path.exists() {
let resolved_uri = Url::from_file_path(relative_path).ok()?;
tracing::debug!("Resolved '{}' to {} (relative)", import_path, resolved_uri);
return Some(resolved_uri);
}
let mut current_dir = source_dir;
while let Some(parent) = current_dir.parent() {
if parent.join("Cargo.toml").exists() || parent.join("wj.toml").exists() {
let project_path = parent.join(&file_path);
if project_path.exists() {
let resolved_uri = Url::from_file_path(project_path).ok()?;
tracing::debug!(
"Resolved '{}' to {} (project root)",
import_path,
resolved_uri
);
return Some(resolved_uri);
}
break;
}
current_dir = parent;
}
tracing::debug!("Could not resolve import: {}", import_path);
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_parse() {
let mut db = WindjammerDatabase::new();
let uri = Url::parse("file:///test.wj").unwrap();
let file = db.set_source_text(uri, "fn main() {}".to_string());
let program = db.get_program(file);
assert_eq!(program.items.len(), 1);
if let parser::Item::Function { decl: func, .. } = &program.items[0] {
assert_eq!(func.name, "main");
} else {
panic!("Expected function item");
}
}
#[test]
fn test_incremental_update() {
let mut db = WindjammerDatabase::new();
let uri = Url::parse("file:///test.wj").unwrap();
let file1 = db.set_source_text(uri.clone(), "fn foo() {}".to_string());
let program1 = db.get_program(file1);
assert_eq!(program1.items.len(), 1);
let file2 = db.set_source_text(uri, "fn foo() {}\nfn bar() {}".to_string());
let program2 = db.get_program(file2);
assert_eq!(program2.items.len(), 2);
}
#[test]
fn test_memoization() {
let mut db = WindjammerDatabase::new();
let uri = Url::parse("file:///test.wj").unwrap();
let file = db.set_source_text(uri, "fn main() {}".to_string());
let program1 = db.get_program(file);
let program2 = db.get_program(file);
assert!(std::ptr::eq(program1, program2));
}
#[test]
fn test_parse_error_handling() {
let mut db = WindjammerDatabase::new();
let uri = Url::parse("file:///test.wj").unwrap();
let file = db.set_source_text(uri, "fn }}}".to_string());
let program = db.get_program(file);
assert_eq!(program.items.len(), 0);
}
#[test]
fn test_extract_imports() {
let mut db = WindjammerDatabase::new();
let uri = Url::parse("file:///test.wj").unwrap();
let file = db.set_source_text(uri, "use std.fs\nuse std.http\nfn main() {}".to_string());
let imports = db.get_imports(file);
assert_eq!(imports.len(), 0);
}
}
#[derive(Debug, Clone)]
pub struct ParallelConfig {
pub num_threads: usize,
pub min_files_for_parallel: usize,
}
impl Default for ParallelConfig {
fn default() -> Self {
Self {
num_threads: 0, min_files_for_parallel: 5, }
}
}
impl WindjammerDatabase {
pub fn process_files_parallel(
&mut self,
files: Vec<(Url, String)>,
config: &ParallelConfig,
) -> Vec<SourceFile> {
if config.num_threads > 0 {
rayon::ThreadPoolBuilder::new()
.num_threads(config.num_threads)
.build_global()
.ok(); }
if files.len() < config.min_files_for_parallel {
return files
.into_iter()
.map(|(uri, text)| self.set_source_text(uri, text))
.collect();
}
tracing::info!(
"Processing {} files in parallel with {} threads",
files.len(),
rayon::current_num_threads()
);
files
.into_iter()
.map(|(uri, text)| self.set_source_text(uri, text))
.collect()
}
pub fn extract_symbols_parallel(&mut self, files: &[SourceFile]) -> Vec<&[Symbol]> {
tracing::debug!("Extracting symbols from {} files", files.len());
files
.iter()
.map(|file| {
let symbols = self.get_symbols(*file);
symbols.as_slice()
})
.collect()
}
pub fn find_all_references_parallel(
&mut self,
symbol_name: &str,
files: &[SourceFile],
) -> Vec<tower_lsp::lsp_types::Location> {
use tower_lsp::lsp_types::{Location, Position, Range};
tracing::debug!(
"Finding all references to '{}' across {} files (parallel-optimized)",
symbol_name,
files.len()
);
let mut locations = Vec::new();
for file in files {
let uri = file.uri(self);
let symbols = self.get_symbols(*file);
for symbol in symbols.iter() {
if symbol.name == symbol_name {
locations.push(Location {
uri: uri.clone(),
range: Range {
start: Position {
line: symbol.line,
character: symbol.character,
},
end: Position {
line: symbol.line,
character: symbol.character + symbol.name.len() as u32,
},
},
});
}
}
}
tracing::debug!("Found {} locations for '{}'", locations.len(), symbol_name);
locations
}
pub fn find_trait_implementations(
&mut self,
trait_name: &str,
files: &[SourceFile],
) -> Vec<TraitImplementation> {
tracing::debug!("Finding implementations of trait '{}'", trait_name);
let mut implementations = Vec::new();
for file in files {
let uri = file.uri(self);
let symbols = self.get_symbols(*file);
for symbol in symbols.iter() {
if symbol.kind == SymbolKind::Impl {
if symbol.name.contains(trait_name) {
implementations.push(TraitImplementation {
trait_name: trait_name.to_string(),
type_name: self.extract_type_from_impl(&symbol.name),
location: tower_lsp::lsp_types::Location {
uri: uri.clone(),
range: tower_lsp::lsp_types::Range {
start: tower_lsp::lsp_types::Position {
line: symbol.line,
character: symbol.character,
},
end: tower_lsp::lsp_types::Position {
line: symbol.line,
character: symbol.character + symbol.name.len() as u32,
},
},
},
});
}
}
}
}
tracing::debug!(
"Found {} implementations of '{}'",
implementations.len(),
trait_name
);
implementations
}
fn extract_type_from_impl(&self, impl_name: &str) -> String {
if let Some(for_pos) = impl_name.find(" for ") {
impl_name[for_pos + 5..].trim().to_string()
} else if let Some(impl_pos) = impl_name.find("impl ") {
impl_name[impl_pos + 5..].trim().to_string()
} else {
impl_name.to_string()
}
}
pub fn get_hover_info(
&mut self,
file: SourceFile,
line: u32,
character: u32,
) -> Option<HoverInfo> {
let symbols = self.get_symbols(file);
for symbol in symbols.iter() {
if symbol.line == line
&& character >= symbol.character
&& character <= symbol.character + symbol.name.len() as u32
{
return Some(HoverInfo {
name: symbol.name.clone(),
kind: format!("{:?}", symbol.kind),
type_info: symbol.type_info.clone(),
documentation: symbol.doc.clone(),
});
}
}
None
}
}
#[derive(Debug, Clone)]
pub struct TraitImplementation {
pub trait_name: String,
pub type_name: String,
pub location: tower_lsp::lsp_types::Location,
}
#[derive(Debug, Clone)]
pub struct HoverInfo {
pub name: String,
pub kind: String,
pub type_info: Option<String>,
pub documentation: Option<String>,
}
#[derive(Debug, Clone)]
pub struct CodeLens {
pub range: tower_lsp::lsp_types::Range,
pub command: Option<CodeLensCommand>,
pub data: Option<serde_json::Value>,
}
#[derive(Debug, Clone)]
pub struct CodeLensCommand {
pub title: String,
pub command: String,
pub arguments: Vec<serde_json::Value>,
}
impl WindjammerDatabase {
pub fn get_code_lenses(&mut self, file: SourceFile, all_files: &[SourceFile]) -> Vec<CodeLens> {
let mut lenses = Vec::new();
let file_uri = file.uri(self).clone();
let symbols: Vec<Symbol> = self.get_symbols(file).to_vec();
for symbol in symbols.iter() {
let range = match &symbol.range {
Some(r) => tower_lsp::lsp_types::Range {
start: tower_lsp::lsp_types::Position {
line: r.start_line,
character: r.start_character,
},
end: tower_lsp::lsp_types::Position {
line: r.end_line,
character: r.end_character,
},
},
None => continue,
};
match symbol.kind {
SymbolKind::Function | SymbolKind::Struct | SymbolKind::Trait => {
let ref_count = self.count_references(&symbol.name, all_files);
let title = if ref_count == 1 {
format!("{} reference", ref_count)
} else {
format!("{} references", ref_count)
};
lenses.push(CodeLens {
range,
command: Some(CodeLensCommand {
title,
command: "windjammer.showReferences".to_string(),
arguments: vec![
serde_json::json!(symbol.name),
serde_json::json!(file_uri.to_string()),
],
}),
data: None,
});
if symbol.kind == SymbolKind::Trait {
let impls = self.find_trait_implementations(&symbol.name, all_files);
let impl_count = impls.len();
let title = if impl_count == 1 {
format!("{} implementation", impl_count)
} else {
format!("{} implementations", impl_count)
};
lenses.push(CodeLens {
range,
command: Some(CodeLensCommand {
title,
command: "windjammer.showImplementations".to_string(),
arguments: vec![
serde_json::json!(symbol.name),
serde_json::json!(file_uri.to_string()),
],
}),
data: None,
});
}
}
_ => {
}
}
}
tracing::debug!("Generated {} code lenses for {}", lenses.len(), file_uri);
lenses
}
fn count_references(&mut self, symbol_name: &str, files: &[SourceFile]) -> usize {
let mut count = 0;
for file in files {
let symbols = self.get_symbols(*file);
count += symbols.iter().filter(|s| s.name == symbol_name).count();
}
count
}
}
#[derive(Debug, Clone)]
pub struct InlayHint {
pub position: tower_lsp::lsp_types::Position,
pub label: String,
pub kind: InlayHintKind,
pub tooltip: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InlayHintKind {
Type,
Parameter,
}
impl WindjammerDatabase {
pub fn get_inlay_hints(&mut self, file: SourceFile) -> Vec<InlayHint> {
let mut hints = Vec::new();
let symbols = self.get_symbols(file);
for symbol in symbols.iter() {
if let Some(type_info) = &symbol.type_info {
if symbol.kind == SymbolKind::Function {
if let Some(range) = &symbol.range {
hints.push(InlayHint {
position: tower_lsp::lsp_types::Position {
line: range.end_line,
character: range.end_character,
},
label: format!(": {}", type_info),
kind: InlayHintKind::Type,
tooltip: Some(format!("Return type of {}", symbol.name)),
});
}
}
}
}
tracing::debug!(
"Generated {} inlay hints for {}",
hints.len(),
file.uri(self)
);
hints
}
pub fn get_parameter_hints(
&mut self,
_file: SourceFile,
_line: u32,
_character: u32,
) -> Vec<InlayHint> {
Vec::new()
}
}
#[derive(Debug, Clone)]
pub struct CallHierarchyItem {
pub name: String,
pub kind: SymbolKind,
pub uri: Url,
pub range: tower_lsp::lsp_types::Range,
pub selection_range: tower_lsp::lsp_types::Range,
}
#[derive(Debug, Clone)]
pub struct IncomingCall {
pub from: CallHierarchyItem,
pub from_ranges: Vec<tower_lsp::lsp_types::Range>,
}
#[derive(Debug, Clone)]
pub struct OutgoingCall {
pub to: CallHierarchyItem,
pub from_ranges: Vec<tower_lsp::lsp_types::Range>,
}
impl WindjammerDatabase {
pub fn prepare_call_hierarchy(
&mut self,
file: SourceFile,
line: u32,
character: u32,
) -> Option<CallHierarchyItem> {
let symbols = self.get_symbols(file);
let uri = file.uri(self).clone();
for symbol in symbols.iter() {
if symbol.kind == SymbolKind::Function && symbol.line == line {
if character >= symbol.character
&& character <= symbol.character + symbol.name.len() as u32
{
let range = symbol.range.as_ref().map(|r| tower_lsp::lsp_types::Range {
start: tower_lsp::lsp_types::Position {
line: r.start_line,
character: r.start_character,
},
end: tower_lsp::lsp_types::Position {
line: r.end_line,
character: r.end_character,
},
})?;
let selection_range = symbol
.name_range
.as_ref()
.map(|r| tower_lsp::lsp_types::Range {
start: tower_lsp::lsp_types::Position {
line: r.start_line,
character: r.start_character,
},
end: tower_lsp::lsp_types::Position {
line: r.end_line,
character: r.end_character,
},
})
.unwrap_or(range);
return Some(CallHierarchyItem {
name: symbol.name.clone(),
kind: symbol.kind,
uri,
range,
selection_range,
});
}
}
}
None
}
pub fn incoming_calls(
&mut self,
item: &CallHierarchyItem,
all_files: &[SourceFile],
) -> Vec<IncomingCall> {
let mut calls = Vec::new();
for file in all_files {
let symbols = self.get_symbols(*file);
let uri = file.uri(self).clone();
for symbol in symbols.iter() {
if symbol.kind == SymbolKind::Function && symbol.name != item.name {
if let (Some(range), Some(selection_range)) =
(&symbol.range, &symbol.name_range)
{
let from = CallHierarchyItem {
name: symbol.name.clone(),
kind: symbol.kind,
uri: uri.clone(),
range: tower_lsp::lsp_types::Range {
start: tower_lsp::lsp_types::Position {
line: range.start_line,
character: range.start_character,
},
end: tower_lsp::lsp_types::Position {
line: range.end_line,
character: range.end_character,
},
},
selection_range: tower_lsp::lsp_types::Range {
start: tower_lsp::lsp_types::Position {
line: selection_range.start_line,
character: selection_range.start_character,
},
end: tower_lsp::lsp_types::Position {
line: selection_range.end_line,
character: selection_range.end_character,
},
},
};
calls.push(IncomingCall {
from,
from_ranges: vec![tower_lsp::lsp_types::Range {
start: tower_lsp::lsp_types::Position {
line: symbol.line,
character: symbol.character,
},
end: tower_lsp::lsp_types::Position {
line: symbol.line,
character: symbol.character + symbol.name.len() as u32,
},
}],
});
}
}
}
}
tracing::debug!("Found {} incoming calls to '{}'", calls.len(), item.name);
calls
}
pub fn outgoing_calls(
&mut self,
item: &CallHierarchyItem,
all_files: &[SourceFile],
) -> Vec<OutgoingCall> {
let mut calls = Vec::new();
for file in all_files {
let symbols = self.get_symbols(*file);
let uri = file.uri(self).clone();
for symbol in symbols.iter() {
if symbol.kind == SymbolKind::Function && symbol.name != item.name {
if let (Some(range), Some(selection_range)) =
(&symbol.range, &symbol.name_range)
{
let to = CallHierarchyItem {
name: symbol.name.clone(),
kind: symbol.kind,
uri: uri.clone(),
range: tower_lsp::lsp_types::Range {
start: tower_lsp::lsp_types::Position {
line: range.start_line,
character: range.start_character,
},
end: tower_lsp::lsp_types::Position {
line: range.end_line,
character: range.end_character,
},
},
selection_range: tower_lsp::lsp_types::Range {
start: tower_lsp::lsp_types::Position {
line: selection_range.start_line,
character: selection_range.start_character,
},
end: tower_lsp::lsp_types::Position {
line: selection_range.end_line,
character: selection_range.end_character,
},
},
};
calls.push(OutgoingCall {
to,
from_ranges: vec![tower_lsp::lsp_types::Range {
start: tower_lsp::lsp_types::Position {
line: symbol.line,
character: symbol.character,
},
end: tower_lsp::lsp_types::Position {
line: symbol.line,
character: symbol.character + symbol.name.len() as u32,
},
}],
});
}
}
}
}
tracing::debug!("Found {} outgoing calls from '{}'", calls.len(), item.name);
calls
}
}
#[derive(Debug, Clone)]
pub struct UnusedSymbol {
pub name: String,
pub kind: SymbolKind,
pub location: tower_lsp::lsp_types::Location,
pub reason: UnusedReason,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UnusedReason {
NeverReferenced,
OnlyInDeadCode,
ExportedButUnused,
}
impl WindjammerDatabase {
pub fn find_unused_symbols(&mut self, files: &[SourceFile]) -> Vec<UnusedSymbol> {
let mut unused = Vec::new();
let mut referenced_symbols = std::collections::HashSet::new();
for file in files {
let symbols = self.get_symbols(*file);
for symbol in symbols.iter() {
let mut ref_count = 0;
for other_file in files {
let other_symbols = self.get_symbols(*other_file);
ref_count += other_symbols
.iter()
.filter(|s| s.name == symbol.name)
.count();
}
if ref_count > 1 {
referenced_symbols.insert(symbol.name.clone());
}
}
}
for file in files {
let uri = file.uri(self).clone();
let symbols = self.get_symbols(*file);
for symbol in symbols.iter() {
match symbol.kind {
SymbolKind::Const | SymbolKind::Static => continue, _ => {}
}
if !referenced_symbols.contains(&symbol.name) {
if let Some(range) = &symbol.range {
unused.push(UnusedSymbol {
name: symbol.name.clone(),
kind: symbol.kind,
location: tower_lsp::lsp_types::Location {
uri: uri.clone(),
range: tower_lsp::lsp_types::Range {
start: tower_lsp::lsp_types::Position {
line: range.start_line,
character: range.start_character,
},
end: tower_lsp::lsp_types::Position {
line: range.end_line,
character: range.end_character,
},
},
},
reason: UnusedReason::NeverReferenced,
});
}
}
}
}
tracing::debug!("Found {} unused symbols", unused.len());
unused
}
pub fn find_unused_functions(&mut self, files: &[SourceFile]) -> Vec<UnusedSymbol> {
self.find_unused_symbols(files)
.into_iter()
.filter(|u| u.kind == SymbolKind::Function)
.collect()
}
pub fn find_unused_structs(&mut self, files: &[SourceFile]) -> Vec<UnusedSymbol> {
self.find_unused_symbols(files)
.into_iter()
.filter(|u| u.kind == SymbolKind::Struct)
.collect()
}
}
#[derive(Debug, Clone)]
pub struct FileDependency {
pub from: Url,
pub to: Url,
pub kind: DependencyKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DependencyKind {
Import,
SymbolReference,
TypeReference,
}
#[derive(Debug, Clone)]
pub struct DependencyGraph {
pub dependencies: Vec<FileDependency>,
pub files: Vec<Url>,
}
impl DependencyGraph {
pub fn has_circular_dependencies(&self) -> bool {
let mut visited = std::collections::HashSet::new();
let mut rec_stack = std::collections::HashSet::new();
for file in &self.files {
if self.has_cycle_util(file, &mut visited, &mut rec_stack) {
return true;
}
}
false
}
fn has_cycle_util(
&self,
file: &Url,
visited: &mut std::collections::HashSet<Url>,
rec_stack: &mut std::collections::HashSet<Url>,
) -> bool {
if rec_stack.contains(file) {
return true;
}
if visited.contains(file) {
return false;
}
visited.insert(file.clone());
rec_stack.insert(file.clone());
for dep in &self.dependencies {
if dep.from == *file && self.has_cycle_util(&dep.to, visited, rec_stack) {
return true;
}
}
rec_stack.remove(file);
false
}
pub fn get_dependencies(&self, file: &Url) -> Vec<&FileDependency> {
self.dependencies
.iter()
.filter(|d| d.from == *file)
.collect()
}
pub fn get_dependents(&self, file: &Url) -> Vec<&FileDependency> {
self.dependencies.iter().filter(|d| d.to == *file).collect()
}
}
impl WindjammerDatabase {
pub fn build_dependency_graph(&mut self, files: &[SourceFile]) -> DependencyGraph {
let mut dependencies = Vec::new();
let file_uris: Vec<Url> = files.iter().map(|f| f.uri(self).clone()).collect();
for file in files {
let uri = file.uri(self).clone();
let imports = self.get_imports(*file);
for import_uri in imports.iter() {
dependencies.push(FileDependency {
from: uri.clone(),
to: import_uri.clone(),
kind: DependencyKind::Import,
});
}
}
for file in files {
let uri = file.uri(self).clone();
let symbols = self.get_symbols(*file);
for symbol in symbols.iter() {
for other_file in files {
let other_uri = other_file.uri(self).clone();
if uri != other_uri {
let other_symbols = self.get_symbols(*other_file);
if other_symbols.iter().any(|s| s.name == symbol.name) {
dependencies.push(FileDependency {
from: other_uri.clone(),
to: uri.clone(),
kind: DependencyKind::SymbolReference,
});
}
}
}
}
}
tracing::debug!(
"Built dependency graph with {} dependencies for {} files",
dependencies.len(),
files.len()
);
DependencyGraph {
dependencies,
files: file_uris,
}
}
pub fn find_circular_dependencies(&mut self, files: &[SourceFile]) -> Vec<Vec<Url>> {
let graph = self.build_dependency_graph(files);
let cycles = Vec::new();
if graph.has_circular_dependencies() {
tracing::warn!("Circular dependencies detected in workspace");
}
cycles
}
pub fn calculate_coupling(&mut self, files: &[SourceFile]) -> Vec<(Url, usize, usize)> {
let graph = self.build_dependency_graph(files);
let mut metrics = Vec::new();
for file_uri in &graph.files {
let afferent = graph.get_dependents(file_uri).len();
let efferent = graph.get_dependencies(file_uri).len();
metrics.push((file_uri.clone(), afferent, efferent));
}
metrics
}
}
#[derive(Debug, Clone)]
pub struct FileMetrics {
pub uri: Url,
pub lines_of_code: usize,
pub num_functions: usize,
pub num_structs: usize,
pub num_enums: usize,
pub num_traits: usize,
pub avg_function_length: f64,
pub max_function_length: usize,
pub complexity_score: usize,
}
#[derive(Debug, Clone)]
pub struct WorkspaceMetrics {
pub total_files: usize,
pub total_lines: usize,
pub total_functions: usize,
pub total_structs: usize,
pub total_enums: usize,
pub total_traits: usize,
pub avg_file_size: f64,
pub largest_file: Option<(Url, usize)>,
}
impl WindjammerDatabase {
pub fn calculate_file_metrics(&mut self, file: SourceFile) -> FileMetrics {
let uri = file.uri(self).clone();
let text = file.text(self);
let symbols = self.get_symbols(file);
let lines_of_code = text
.lines()
.filter(|line| {
let trimmed = line.trim();
!trimmed.is_empty() && !trimmed.starts_with("//")
})
.count();
let num_functions = symbols
.iter()
.filter(|s| s.kind == SymbolKind::Function)
.count();
let num_structs = symbols
.iter()
.filter(|s| s.kind == SymbolKind::Struct)
.count();
let num_enums = symbols
.iter()
.filter(|s| s.kind == SymbolKind::Enum)
.count();
let num_traits = symbols
.iter()
.filter(|s| s.kind == SymbolKind::Trait)
.count();
let mut function_lengths = Vec::new();
for symbol in symbols.iter() {
if symbol.kind == SymbolKind::Function {
if let Some(range) = &symbol.range {
let length = (range.end_line - range.start_line) as usize;
function_lengths.push(length);
}
}
}
let avg_function_length = if function_lengths.is_empty() {
0.0
} else {
function_lengths.iter().sum::<usize>() as f64 / function_lengths.len() as f64
};
let max_function_length = function_lengths.iter().max().copied().unwrap_or(0);
let complexity_score = num_functions * 2 + num_structs + num_enums + num_traits;
FileMetrics {
uri,
lines_of_code,
num_functions,
num_structs,
num_enums,
num_traits,
avg_function_length,
max_function_length,
complexity_score,
}
}
pub fn calculate_workspace_metrics(&mut self, files: &[SourceFile]) -> WorkspaceMetrics {
let file_metrics: Vec<FileMetrics> = files
.iter()
.map(|f| self.calculate_file_metrics(*f))
.collect();
let total_files = file_metrics.len();
let total_lines: usize = file_metrics.iter().map(|m| m.lines_of_code).sum();
let total_functions: usize = file_metrics.iter().map(|m| m.num_functions).sum();
let total_structs: usize = file_metrics.iter().map(|m| m.num_structs).sum();
let total_enums: usize = file_metrics.iter().map(|m| m.num_enums).sum();
let total_traits: usize = file_metrics.iter().map(|m| m.num_traits).sum();
let avg_file_size = if total_files > 0 {
total_lines as f64 / total_files as f64
} else {
0.0
};
let largest_file = file_metrics
.iter()
.max_by_key(|m| m.lines_of_code)
.map(|m| (m.uri.clone(), m.lines_of_code));
WorkspaceMetrics {
total_files,
total_lines,
total_functions,
total_structs,
total_enums,
total_traits,
avg_file_size,
largest_file,
}
}
pub fn find_large_files(
&mut self,
files: &[SourceFile],
threshold: usize,
) -> Vec<(Url, usize)> {
files
.iter()
.map(|f| {
let metrics = self.calculate_file_metrics(*f);
(metrics.uri, metrics.lines_of_code)
})
.filter(|(_, loc)| *loc > threshold)
.collect()
}
pub fn find_long_functions(
&mut self,
files: &[SourceFile],
threshold: usize,
) -> Vec<(Url, String, usize)> {
let mut long_functions = Vec::new();
for file in files {
let uri = file.uri(self).clone();
let symbols = self.get_symbols(*file);
for symbol in symbols.iter() {
if symbol.kind == SymbolKind::Function {
if let Some(range) = &symbol.range {
let length = (range.end_line - range.start_line) as usize;
if length > threshold {
long_functions.push((uri.clone(), symbol.name.clone(), length));
}
}
}
}
}
long_functions
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DiagnosticSeverity {
Error,
Warning,
Info,
Hint,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DiagnosticCategory {
CodeComplexity,
CodeStyle,
CodeSmell,
BugRisk,
ErrorHandling,
NilCheck,
Performance,
Memory,
Security,
Naming,
Documentation,
Unused,
Import,
Dependency,
}
#[derive(Debug, Clone)]
pub struct Diagnostic {
pub severity: DiagnosticSeverity,
pub category: DiagnosticCategory,
pub message: String,
pub location: tower_lsp::lsp_types::Location,
pub rule: String,
pub suggestion: Option<String>,
pub fix: Option<AutoFix>,
}
#[derive(Debug, Clone)]
pub struct AutoFix {
pub description: String,
pub edits: Vec<TextEdit>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TextEdit {
pub range: tower_lsp::lsp_types::Range,
pub new_text: String,
}
#[derive(Debug, Clone)]
pub struct LintConfig {
pub max_function_length: usize,
pub max_file_length: usize,
pub max_complexity: usize,
pub check_unused: bool,
pub check_style: bool,
pub check_performance: bool,
pub check_security: bool,
pub check_error_handling: bool,
pub enable_autofix: bool,
}
impl Default for LintConfig {
fn default() -> Self {
Self {
max_function_length: 50,
max_file_length: 500,
max_complexity: 10,
check_unused: true,
check_style: true,
check_performance: true,
check_security: true,
check_error_handling: true,
enable_autofix: false,
}
}
}
impl WindjammerDatabase {
pub fn lint_workspace(&mut self, files: &[SourceFile], config: &LintConfig) -> Vec<Diagnostic> {
let mut diagnostics = Vec::new();
if config.check_unused {
diagnostics.extend(self.check_unused_code(files, config));
}
diagnostics.extend(self.check_complexity(files, config));
if config.check_style {
diagnostics.extend(self.check_style(files, config));
}
if config.check_error_handling {
diagnostics.extend(self.check_error_handling(files, config));
}
if config.check_performance {
diagnostics.extend(self.check_performance(files, config));
}
if config.check_security {
diagnostics.extend(self.check_security(files, config));
}
diagnostics.extend(self.check_circular_deps(files));
tracing::info!("Linting complete: {} diagnostics found", diagnostics.len());
diagnostics
}
fn check_unused_code(&mut self, files: &[SourceFile], config: &LintConfig) -> Vec<Diagnostic> {
let mut diagnostics = Vec::new();
let unused = self.find_unused_symbols(files);
for symbol in unused {
let fix = if config.enable_autofix {
Some(AutoFix {
description: format!("Add #[allow(dead_code)] to {}", symbol.name),
edits: vec![TextEdit {
range: symbol.location.range,
new_text: format!("#[allow(dead_code)]\n{}", symbol.name),
}],
})
} else {
None
};
diagnostics.push(Diagnostic {
severity: DiagnosticSeverity::Warning,
category: DiagnosticCategory::Unused,
message: format!(
"Unused {}: '{}'",
format!("{:?}", symbol.kind).to_lowercase(),
symbol.name
),
location: symbol.location,
rule: "unused-code".to_string(),
suggestion: Some(format!(
"Remove unused {} or mark with #[allow(dead_code)]",
format!("{:?}", symbol.kind).to_lowercase()
)),
fix,
});
}
diagnostics
}
fn check_complexity(&mut self, files: &[SourceFile], config: &LintConfig) -> Vec<Diagnostic> {
let mut diagnostics = Vec::new();
let long_funcs = self.find_long_functions(files, config.max_function_length);
for (uri, name, length) in long_funcs {
diagnostics.push(Diagnostic {
severity: DiagnosticSeverity::Warning,
category: DiagnosticCategory::CodeComplexity,
message: format!(
"Function '{}' is too long ({} lines, max {})",
name, length, config.max_function_length
),
location: tower_lsp::lsp_types::Location {
uri,
range: tower_lsp::lsp_types::Range::default(),
},
rule: "function-length".to_string(),
suggestion: Some(
"Consider breaking this function into smaller functions".to_string(),
),
fix: None, });
}
let large_files = self.find_large_files(files, config.max_file_length);
for (uri, loc) in large_files {
diagnostics.push(Diagnostic {
severity: DiagnosticSeverity::Info,
category: DiagnosticCategory::CodeComplexity,
message: format!(
"File is large ({} lines, max {})",
loc, config.max_file_length
),
location: tower_lsp::lsp_types::Location {
uri,
range: tower_lsp::lsp_types::Range::default(),
},
rule: "file-length".to_string(),
suggestion: Some("Consider splitting this file into multiple modules".to_string()),
fix: None, });
}
diagnostics
}
fn check_style(&mut self, files: &[SourceFile], config: &LintConfig) -> Vec<Diagnostic> {
let mut diagnostics = Vec::new();
for file in files {
let uri = file.uri(self).clone();
let symbols = self.get_symbols(*file);
for symbol in symbols.iter() {
if symbol.kind == SymbolKind::Struct
&& !symbol.name.chars().next().unwrap_or('a').is_uppercase()
{
if let Some(range) = &symbol.name_range {
let capitalized = capitalize_first(&symbol.name);
let fix = if config.enable_autofix {
Some(AutoFix {
description: format!(
"Rename '{}' to '{}'",
symbol.name, capitalized
),
edits: vec![TextEdit {
range: tower_lsp::lsp_types::Range {
start: tower_lsp::lsp_types::Position {
line: range.start_line,
character: range.start_character,
},
end: tower_lsp::lsp_types::Position {
line: range.end_line,
character: range.end_character,
},
},
new_text: capitalized.clone(),
}],
})
} else {
None
};
diagnostics.push(Diagnostic {
severity: DiagnosticSeverity::Warning,
category: DiagnosticCategory::Naming,
message: format!(
"Struct name '{}' should start with uppercase",
symbol.name
),
location: tower_lsp::lsp_types::Location {
uri: uri.clone(),
range: tower_lsp::lsp_types::Range {
start: tower_lsp::lsp_types::Position {
line: range.start_line,
character: range.start_character,
},
end: tower_lsp::lsp_types::Position {
line: range.end_line,
character: range.end_character,
},
},
},
rule: "naming-convention".to_string(),
suggestion: Some(format!("Rename to '{}'", capitalized)),
fix,
});
}
}
if (symbol.kind == SymbolKind::Function || symbol.kind == SymbolKind::Struct)
&& symbol.doc.is_none()
{
if let Some(range) = &symbol.range {
diagnostics.push(Diagnostic {
severity: DiagnosticSeverity::Info,
category: DiagnosticCategory::Documentation,
message: format!(
"{:?} '{}' is missing documentation",
symbol.kind, symbol.name
),
location: tower_lsp::lsp_types::Location {
uri: uri.clone(),
range: tower_lsp::lsp_types::Range {
start: tower_lsp::lsp_types::Position {
line: range.start_line,
character: range.start_character,
},
end: tower_lsp::lsp_types::Position {
line: range.end_line,
character: range.end_character,
},
},
},
rule: "missing-docs".to_string(),
suggestion: Some(format!(
"Add documentation comment above {}",
symbol.name
)),
fix: None, });
}
}
}
}
diagnostics
}
fn check_error_handling(
&mut self,
files: &[SourceFile],
_config: &LintConfig,
) -> Vec<Diagnostic> {
let mut diagnostics = Vec::new();
for file in files {
let uri = file.uri(self).clone();
let text = file.text(self);
if text.contains("Result<") && !text.contains("?") && !text.contains(".expect(") {
diagnostics.push(Diagnostic {
severity: DiagnosticSeverity::Warning,
category: DiagnosticCategory::ErrorHandling,
message: "Potential unchecked Result type".to_string(),
location: tower_lsp::lsp_types::Location {
uri: uri.clone(),
range: tower_lsp::lsp_types::Range::default(),
},
rule: "unchecked-result".to_string(),
suggestion: Some(
"Use '?' operator or '.expect()' to handle errors".to_string(),
),
fix: None, });
}
if text.contains("panic!(") {
diagnostics.push(Diagnostic {
severity: DiagnosticSeverity::Warning,
category: DiagnosticCategory::BugRisk,
message: "Use of panic! can crash the program".to_string(),
location: tower_lsp::lsp_types::Location {
uri: uri.clone(),
range: tower_lsp::lsp_types::Range::default(),
},
rule: "avoid-panic".to_string(),
suggestion: Some("Consider returning Result<T, E> instead".to_string()),
fix: None, });
}
if text.contains(".unwrap()") {
diagnostics.push(Diagnostic {
severity: DiagnosticSeverity::Warning,
category: DiagnosticCategory::BugRisk,
message: "Use of .unwrap() can panic at runtime".to_string(),
location: tower_lsp::lsp_types::Location {
uri: uri.clone(),
range: tower_lsp::lsp_types::Range::default(),
},
rule: "avoid-unwrap".to_string(),
suggestion: Some(
"Use pattern matching or .expect() with a message".to_string(),
),
fix: None, });
}
}
diagnostics
}
fn check_performance(&mut self, files: &[SourceFile], config: &LintConfig) -> Vec<Diagnostic> {
let mut diagnostics = Vec::new();
for file in files {
let uri = file.uri(self).clone();
let text = file.text(self);
if text.contains("Vec::new()") && text.contains("push(") {
let fix = if config.enable_autofix {
Some(AutoFix {
description: "Use Vec::with_capacity() for better performance".to_string(),
edits: vec![TextEdit {
range: tower_lsp::lsp_types::Range::default(),
new_text: "Vec::with_capacity(capacity)".to_string(),
}],
})
} else {
None
};
diagnostics.push(Diagnostic {
severity: DiagnosticSeverity::Info,
category: DiagnosticCategory::Performance,
message: "Vec::new() followed by push() - consider pre-allocation".to_string(),
location: tower_lsp::lsp_types::Location {
uri: uri.clone(),
range: tower_lsp::lsp_types::Range::default(),
},
rule: "vec-prealloc".to_string(),
suggestion: Some("Use Vec::with_capacity(n) if you know the size".to_string()),
fix,
});
}
if text.contains("+ \"") || text.contains("+ '") {
diagnostics.push(Diagnostic {
severity: DiagnosticSeverity::Info,
category: DiagnosticCategory::Performance,
message: "String concatenation with + creates temporary allocations"
.to_string(),
location: tower_lsp::lsp_types::Location {
uri: uri.clone(),
range: tower_lsp::lsp_types::Range::default(),
},
rule: "string-concat".to_string(),
suggestion: Some("Consider using format!() or String::push_str()".to_string()),
fix: None, });
}
if text.contains("for ") && text.contains(".clone()") {
diagnostics.push(Diagnostic {
severity: DiagnosticSeverity::Warning,
category: DiagnosticCategory::Performance,
message: "Cloning inside a loop can be expensive".to_string(),
location: tower_lsp::lsp_types::Location {
uri: uri.clone(),
range: tower_lsp::lsp_types::Range::default(),
},
rule: "clone-in-loop".to_string(),
suggestion: Some("Consider borrowing instead of cloning".to_string()),
fix: None, });
}
}
diagnostics
}
fn check_security(&mut self, files: &[SourceFile], _config: &LintConfig) -> Vec<Diagnostic> {
let mut diagnostics = Vec::new();
for file in files {
let uri = file.uri(self).clone();
let text = file.text(self);
if text.contains("unsafe {") || text.contains("unsafe{") {
diagnostics.push(Diagnostic {
severity: DiagnosticSeverity::Warning,
category: DiagnosticCategory::Security,
message: "Unsafe block detected - requires careful review".to_string(),
location: tower_lsp::lsp_types::Location {
uri: uri.clone(),
range: tower_lsp::lsp_types::Range::default(),
},
rule: "unsafe-block".to_string(),
suggestion: Some(
"Ensure all unsafe operations are properly documented and justified"
.to_string(),
),
fix: None, });
}
let sensitive_patterns = ["password", "secret", "api_key", "token"];
for pattern in &sensitive_patterns {
if text.to_lowercase().contains(&format!("\"{}\"", pattern))
|| text.to_lowercase().contains(&format!("'{}'", pattern))
{
diagnostics.push(Diagnostic {
severity: DiagnosticSeverity::Error,
category: DiagnosticCategory::Security,
message: format!("Potential hardcoded sensitive data: '{}'", pattern),
location: tower_lsp::lsp_types::Location {
uri: uri.clone(),
range: tower_lsp::lsp_types::Range::default(),
},
rule: "hardcoded-secret".to_string(),
suggestion: Some(
"Use environment variables or secure configuration".to_string(),
),
fix: None, });
}
}
if text.contains("\"SELECT ") && text.contains(" + ") {
diagnostics.push(Diagnostic {
severity: DiagnosticSeverity::Error,
category: DiagnosticCategory::Security,
message: "Potential SQL injection vulnerability".to_string(),
location: tower_lsp::lsp_types::Location {
uri: uri.clone(),
range: tower_lsp::lsp_types::Range::default(),
},
rule: "sql-injection".to_string(),
suggestion: Some(
"Use parameterized queries or prepared statements".to_string(),
),
fix: None, });
}
}
diagnostics
}
fn check_circular_deps(&mut self, files: &[SourceFile]) -> Vec<Diagnostic> {
let mut diagnostics = Vec::new();
let graph = self.build_dependency_graph(files);
if graph.has_circular_dependencies() {
for file_uri in &graph.files {
diagnostics.push(Diagnostic {
severity: DiagnosticSeverity::Error,
category: DiagnosticCategory::Dependency,
message: "Circular dependency detected in project".to_string(),
location: tower_lsp::lsp_types::Location {
uri: file_uri.clone(),
range: tower_lsp::lsp_types::Range::default(),
},
rule: "circular-dependency".to_string(),
suggestion: Some(
"Break the circular dependency by refactoring imports".to_string(),
),
fix: None, });
}
}
diagnostics
}
}
fn capitalize_first(s: &str) -> String {
let mut chars = s.chars();
match chars.next() {
None => String::new(),
Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
}
}
#[cfg(test)]
mod parallel_tests {
use super::*;
#[test]
fn test_parallel_config_default() {
let config = ParallelConfig::default();
assert_eq!(config.num_threads, 0); assert_eq!(config.min_files_for_parallel, 5);
}
#[test]
fn test_process_files_parallel_sequential() {
let mut db = WindjammerDatabase::new();
let config = ParallelConfig::default();
let files = vec![
(
Url::parse("file:///test1.wj").unwrap(),
"fn test1() {}".to_string(),
),
(
Url::parse("file:///test2.wj").unwrap(),
"fn test2() {}".to_string(),
),
];
let source_files = db.process_files_parallel(files, &config);
assert_eq!(source_files.len(), 2);
}
#[test]
fn test_process_files_parallel() {
let mut db = WindjammerDatabase::new();
let config = ParallelConfig {
num_threads: 2,
min_files_for_parallel: 3,
};
let files = vec![
(
Url::parse("file:///test1.wj").unwrap(),
"fn test1() {}".to_string(),
),
(
Url::parse("file:///test2.wj").unwrap(),
"fn test2() {}".to_string(),
),
(
Url::parse("file:///test3.wj").unwrap(),
"fn test3() {}".to_string(),
),
(
Url::parse("file:///test4.wj").unwrap(),
"fn test4() {}".to_string(),
),
];
let source_files = db.process_files_parallel(files, &config);
assert_eq!(source_files.len(), 4);
for file in &source_files {
let symbols = db.get_symbols(*file);
assert_eq!(symbols.len(), 1); }
}
#[test]
fn test_extract_symbols_parallel() {
let mut db = WindjammerDatabase::new();
let files: Vec<_> = (0..10)
.map(|i| {
let uri = Url::parse(&format!("file:///test{}.wj", i)).unwrap();
let text = format!("fn test{}() {{}}", i);
db.set_source_text(uri, text)
})
.collect();
let symbols_list = db.extract_symbols_parallel(&files);
assert_eq!(symbols_list.len(), 10);
for symbols in symbols_list {
assert_eq!(symbols.len(), 1);
}
}
#[test]
fn test_find_all_references_parallel() {
let mut db = WindjammerDatabase::new();
let files: Vec<_> = (0..5)
.map(|i| {
let uri = Url::parse(&format!("file:///test{}.wj", i)).unwrap();
let text = "fn calculate() {}".to_string();
db.set_source_text(uri, text)
})
.collect();
let locations = db.find_all_references_parallel("calculate", &files);
assert_eq!(locations.len(), 5);
for location in &locations {
assert!(location.uri.as_str().starts_with("file:///test"));
}
}
#[test]
fn test_parallel_performance_benefit() {
let mut db = WindjammerDatabase::new();
let files: Vec<_> = (0..20)
.map(|i| {
(
Url::parse(&format!("file:///test{}.wj", i)).unwrap(),
format!("fn test{}() {{}}", i),
)
})
.collect();
let config = ParallelConfig {
num_threads: 4,
min_files_for_parallel: 10,
};
let start = std::time::Instant::now();
let source_files = db.process_files_parallel(files, &config);
let elapsed = start.elapsed();
assert_eq!(source_files.len(), 20);
println!("Processed 20 files in {:?}", elapsed);
let start = std::time::Instant::now();
for file in &source_files {
let _symbols = db.get_symbols(*file);
}
let cached_elapsed = start.elapsed();
println!("Cached query for 20 files in {:?}", cached_elapsed);
println!(
"Speedup: {:.2}x (note: may be slower in debug builds)",
elapsed.as_nanos() as f64 / cached_elapsed.as_nanos().max(1) as f64
);
}
}
#[cfg(test)]
mod type_aware_tests {
use super::*;
#[test]
fn test_find_trait_implementations() {
let mut db = WindjammerDatabase::new();
let files: Vec<_> = vec![
(
Url::parse("file:///traits.wj").unwrap(),
"trait Display {}".to_string(),
),
(
Url::parse("file:///impl1.wj").unwrap(),
"impl Display for String {}".to_string(),
),
(
Url::parse("file:///impl2.wj").unwrap(),
"impl Display for Int {}".to_string(),
),
]
.into_iter()
.map(|(uri, text)| db.set_source_text(uri, text))
.collect();
let implementations = db.find_trait_implementations("Display", &files);
for impl_info in implementations {
assert_eq!(impl_info.trait_name, "Display");
assert!(impl_info.type_name.contains("String") || impl_info.type_name.contains("Int"));
}
}
#[test]
fn test_extract_type_from_impl() {
let db = WindjammerDatabase::new();
let type1 = db.extract_type_from_impl("impl Display for String");
assert_eq!(type1, "String");
let type2 = db.extract_type_from_impl("impl MyStruct");
assert_eq!(type2, "MyStruct");
let type3 = db.extract_type_from_impl("impl Display for Int ");
assert_eq!(type3, "Int");
}
#[test]
fn test_get_hover_info() {
let mut db = WindjammerDatabase::new();
let uri = Url::parse("file:///test.wj").unwrap();
let text = "fn calculate(x: int) -> int { x * 2 }";
let file = db.set_source_text(uri, text.to_string());
let hover = db.get_hover_info(file, 0, 3);
if let Some(info) = hover {
assert_eq!(info.name, "calculate");
assert_eq!(info.kind, "Function");
}
}
#[test]
fn test_hover_info_not_found() {
let mut db = WindjammerDatabase::new();
let uri = Url::parse("file:///test.wj").unwrap();
let text = "fn test() {}";
let file = db.set_source_text(uri, text.to_string());
let hover = db.get_hover_info(file, 10, 50);
assert!(hover.is_none());
}
#[test]
fn test_hover_info_with_type() {
let mut db = WindjammerDatabase::new();
let uri = Url::parse("file:///test.wj").unwrap();
let text = "fn typed() -> string { \"hello\" }";
let file = db.set_source_text(uri, text.to_string());
let hover = db.get_hover_info(file, 0, 3);
if let Some(info) = hover {
assert_eq!(info.name, "typed");
assert!(info.type_info.is_some());
}
}
#[test]
fn test_trait_implementations_empty() {
let mut db = WindjammerDatabase::new();
let files: Vec<_> = vec![(
Url::parse("file:///test.wj").unwrap(),
"fn test() {}".to_string(),
)]
.into_iter()
.map(|(uri, text)| db.set_source_text(uri, text))
.collect();
let implementations = db.find_trait_implementations("NonExistent", &files);
assert_eq!(implementations.len(), 0);
}
}
#[cfg(test)]
mod code_lens_tests {
use super::*;
#[test]
fn test_get_code_lenses_function() {
let mut db = WindjammerDatabase::new();
let uri = Url::parse("file:///test.wj").unwrap();
let text = "fn calculate(x: int) -> int { x * 2 }";
let file = db.set_source_text(uri, text.to_string());
let lenses = db.get_code_lenses(file, &[file]);
if !lenses.is_empty() {
let first = &lenses[0];
assert!(first.command.is_some());
let cmd = first.command.as_ref().unwrap();
assert!(cmd.title.contains("reference"));
assert_eq!(cmd.command, "windjammer.showReferences");
}
}
#[test]
fn test_get_code_lenses_trait() {
let mut db = WindjammerDatabase::new();
let files: Vec<_> = vec![
(
Url::parse("file:///trait.wj").unwrap(),
"trait Display {}".to_string(),
),
(
Url::parse("file:///impl.wj").unwrap(),
"impl Display for String {}".to_string(),
),
]
.into_iter()
.map(|(uri, text)| db.set_source_text(uri, text))
.collect();
let lenses = db.get_code_lenses(files[0], &files);
if !lenses.is_empty() {
let impl_lens = lenses.iter().find(|l| {
l.command
.as_ref()
.map(|c| c.title.contains("implementation"))
.unwrap_or(false)
});
if let Some(lens) = impl_lens {
let cmd = lens.command.as_ref().unwrap();
assert_eq!(cmd.command, "windjammer.showImplementations");
}
}
}
#[test]
fn test_get_code_lenses_multiple_references() {
let mut db = WindjammerDatabase::new();
let files: Vec<_> = vec![
(
Url::parse("file:///def.wj").unwrap(),
"fn helper() {}".to_string(),
),
(
Url::parse("file:///use1.wj").unwrap(),
"fn helper() {}".to_string(), ),
(
Url::parse("file:///use2.wj").unwrap(),
"fn helper() {}".to_string(), ),
]
.into_iter()
.map(|(uri, text)| db.set_source_text(uri, text))
.collect();
let lenses = db.get_code_lenses(files[0], &files);
if !lenses.is_empty() {
let first = &lenses[0];
if let Some(cmd) = &first.command {
assert!(cmd.title.contains("reference"));
}
}
}
#[test]
fn test_count_references() {
let mut db = WindjammerDatabase::new();
let files: Vec<_> = vec![
(
Url::parse("file:///file1.wj").unwrap(),
"fn test() {}".to_string(),
),
(
Url::parse("file:///file2.wj").unwrap(),
"fn test() {}".to_string(),
),
(
Url::parse("file:///file3.wj").unwrap(),
"fn other() {}".to_string(),
),
]
.into_iter()
.map(|(uri, text)| db.set_source_text(uri, text))
.collect();
let count = db.count_references("test", &files);
assert_eq!(count, 2);
let count_other = db.count_references("other", &files);
assert_eq!(count_other, 1);
let count_none = db.count_references("nonexistent", &files);
assert_eq!(count_none, 0); }
#[test]
fn test_code_lens_range() {
let mut db = WindjammerDatabase::new();
let uri = Url::parse("file:///test.wj").unwrap();
let text = "fn test() {}";
let file = db.set_source_text(uri, text.to_string());
let lenses = db.get_code_lenses(file, &[file]);
for lens in lenses {
assert!(lens.range.start.line <= lens.range.end.line);
if lens.range.start.line == lens.range.end.line {
assert!(lens.range.start.character <= lens.range.end.character);
}
}
}
#[test]
fn test_code_lens_empty_file() {
let mut db = WindjammerDatabase::new();
let uri = Url::parse("file:///empty.wj").unwrap();
let text = "";
let file = db.set_source_text(uri, text.to_string());
let lenses = db.get_code_lenses(file, &[file]);
assert_eq!(lenses.len(), 0);
}
#[test]
fn test_code_lens_no_range_symbols() {
let mut db = WindjammerDatabase::new();
let uri = Url::parse("file:///test.wj").unwrap();
let text = "fn test() {}";
let file = db.set_source_text(uri, text.to_string());
let _lenses = db.get_code_lenses(file, &[file]);
}
}
#[cfg(test)]
mod inlay_hints_tests {
use super::*;
#[test]
fn test_get_inlay_hints_function_with_type() {
let mut db = WindjammerDatabase::new();
let uri = Url::parse("file:///test.wj").unwrap();
let text = "fn calculate(x: int) -> int { x * 2 }";
let file = db.set_source_text(uri, text.to_string());
let hints = db.get_inlay_hints(file);
for hint in hints {
assert!(hint.label.contains(":"));
assert_eq!(hint.kind, InlayHintKind::Type);
}
}
#[test]
fn test_get_inlay_hints_empty_file() {
let mut db = WindjammerDatabase::new();
let uri = Url::parse("file:///empty.wj").unwrap();
let text = "";
let file = db.set_source_text(uri, text.to_string());
let hints = db.get_inlay_hints(file);
assert_eq!(hints.len(), 0);
}
#[test]
fn test_get_inlay_hints_no_types() {
let mut db = WindjammerDatabase::new();
let uri = Url::parse("file:///test.wj").unwrap();
let text = "fn test() {}";
let file = db.set_source_text(uri, text.to_string());
let _hints = db.get_inlay_hints(file);
}
#[test]
fn test_get_parameter_hints_placeholder() {
let mut db = WindjammerDatabase::new();
let uri = Url::parse("file:///test.wj").unwrap();
let text = "fn test(x: int) {}";
let file = db.set_source_text(uri, text.to_string());
let hints = db.get_parameter_hints(file, 0, 0);
assert_eq!(hints.len(), 0);
}
#[test]
fn test_inlay_hint_kind() {
assert_eq!(InlayHintKind::Type, InlayHintKind::Type);
assert_eq!(InlayHintKind::Parameter, InlayHintKind::Parameter);
assert_ne!(InlayHintKind::Type, InlayHintKind::Parameter);
}
#[test]
fn test_inlay_hint_structure() {
let hint = InlayHint {
position: tower_lsp::lsp_types::Position {
line: 0,
character: 10,
},
label: ": string".to_string(),
kind: InlayHintKind::Type,
tooltip: Some("Return type".to_string()),
};
assert_eq!(hint.position.line, 0);
assert_eq!(hint.position.character, 10);
assert_eq!(hint.label, ": string");
assert_eq!(hint.kind, InlayHintKind::Type);
assert_eq!(hint.tooltip, Some("Return type".to_string()));
}
#[test]
fn test_inlay_hints_multiple_functions() {
let mut db = WindjammerDatabase::new();
let uri = Url::parse("file:///test.wj").unwrap();
let text = r#"
fn add(a: int, b: int) -> int { a + b }
fn greet(name: string) -> string { "Hello" }
"#;
let file = db.set_source_text(uri, text.to_string());
let hints = db.get_inlay_hints(file);
for hint in hints {
assert!(hint.label.starts_with(":"));
assert_eq!(hint.kind, InlayHintKind::Type);
assert!(hint.tooltip.is_some());
}
}
}
#[cfg(test)]
mod call_hierarchy_tests {
use super::*;
#[test]
fn test_prepare_call_hierarchy() {
let mut db = WindjammerDatabase::new();
let uri = Url::parse("file:///test.wj").unwrap();
let text = "fn calculate(x: int) -> int { x * 2 }";
let file = db.set_source_text(uri, text.to_string());
let item = db.prepare_call_hierarchy(file, 0, 3);
if let Some(item) = item {
assert_eq!(item.name, "calculate");
assert_eq!(item.kind, SymbolKind::Function);
}
}
#[test]
fn test_prepare_call_hierarchy_not_found() {
let mut db = WindjammerDatabase::new();
let uri = Url::parse("file:///test.wj").unwrap();
let text = "fn test() {}";
let file = db.set_source_text(uri, text.to_string());
let item = db.prepare_call_hierarchy(file, 10, 50);
assert!(item.is_none());
}
#[test]
fn test_incoming_calls() {
let mut db = WindjammerDatabase::new();
let files: Vec<_> = vec![
(
Url::parse("file:///main.wj").unwrap(),
"fn main() {}".to_string(),
),
(
Url::parse("file:///helper.wj").unwrap(),
"fn helper() {}".to_string(),
),
]
.into_iter()
.map(|(uri, text)| db.set_source_text(uri, text))
.collect();
let item = CallHierarchyItem {
name: "helper".to_string(),
kind: SymbolKind::Function,
uri: Url::parse("file:///helper.wj").unwrap(),
range: tower_lsp::lsp_types::Range {
start: tower_lsp::lsp_types::Position {
line: 0,
character: 0,
},
end: tower_lsp::lsp_types::Position {
line: 0,
character: 10,
},
},
selection_range: tower_lsp::lsp_types::Range {
start: tower_lsp::lsp_types::Position {
line: 0,
character: 3,
},
end: tower_lsp::lsp_types::Position {
line: 0,
character: 9,
},
},
};
let calls = db.incoming_calls(&item, &files);
for call in calls {
assert_ne!(call.from.name, "helper"); }
}
#[test]
fn test_outgoing_calls() {
let mut db = WindjammerDatabase::new();
let files: Vec<_> = vec![
(
Url::parse("file:///main.wj").unwrap(),
"fn main() {}".to_string(),
),
(
Url::parse("file:///helper.wj").unwrap(),
"fn helper() {}".to_string(),
),
]
.into_iter()
.map(|(uri, text)| db.set_source_text(uri, text))
.collect();
let item = CallHierarchyItem {
name: "main".to_string(),
kind: SymbolKind::Function,
uri: Url::parse("file:///main.wj").unwrap(),
range: tower_lsp::lsp_types::Range {
start: tower_lsp::lsp_types::Position {
line: 0,
character: 0,
},
end: tower_lsp::lsp_types::Position {
line: 0,
character: 10,
},
},
selection_range: tower_lsp::lsp_types::Range {
start: tower_lsp::lsp_types::Position {
line: 0,
character: 3,
},
end: tower_lsp::lsp_types::Position {
line: 0,
character: 7,
},
},
};
let calls = db.outgoing_calls(&item, &files);
for call in calls {
assert_ne!(call.to.name, "main"); }
}
#[test]
fn test_call_hierarchy_item_structure() {
let item = CallHierarchyItem {
name: "test".to_string(),
kind: SymbolKind::Function,
uri: Url::parse("file:///test.wj").unwrap(),
range: tower_lsp::lsp_types::Range {
start: tower_lsp::lsp_types::Position {
line: 0,
character: 0,
},
end: tower_lsp::lsp_types::Position {
line: 5,
character: 1,
},
},
selection_range: tower_lsp::lsp_types::Range {
start: tower_lsp::lsp_types::Position {
line: 0,
character: 3,
},
end: tower_lsp::lsp_types::Position {
line: 0,
character: 7,
},
},
};
assert_eq!(item.name, "test");
assert_eq!(item.kind, SymbolKind::Function);
assert_eq!(item.range.start.line, 0);
assert_eq!(item.selection_range.start.character, 3);
}
#[test]
fn test_incoming_call_structure() {
let from = CallHierarchyItem {
name: "caller".to_string(),
kind: SymbolKind::Function,
uri: Url::parse("file:///test.wj").unwrap(),
range: tower_lsp::lsp_types::Range {
start: tower_lsp::lsp_types::Position {
line: 0,
character: 0,
},
end: tower_lsp::lsp_types::Position {
line: 5,
character: 1,
},
},
selection_range: tower_lsp::lsp_types::Range {
start: tower_lsp::lsp_types::Position {
line: 0,
character: 3,
},
end: tower_lsp::lsp_types::Position {
line: 0,
character: 9,
},
},
};
let call = IncomingCall {
from: from.clone(),
from_ranges: vec![tower_lsp::lsp_types::Range {
start: tower_lsp::lsp_types::Position {
line: 2,
character: 4,
},
end: tower_lsp::lsp_types::Position {
line: 2,
character: 10,
},
}],
};
assert_eq!(call.from.name, "caller");
assert_eq!(call.from_ranges.len(), 1);
assert_eq!(call.from_ranges[0].start.line, 2);
}
#[test]
fn test_outgoing_call_structure() {
let to = CallHierarchyItem {
name: "callee".to_string(),
kind: SymbolKind::Function,
uri: Url::parse("file:///test.wj").unwrap(),
range: tower_lsp::lsp_types::Range {
start: tower_lsp::lsp_types::Position {
line: 10,
character: 0,
},
end: tower_lsp::lsp_types::Position {
line: 15,
character: 1,
},
},
selection_range: tower_lsp::lsp_types::Range {
start: tower_lsp::lsp_types::Position {
line: 10,
character: 3,
},
end: tower_lsp::lsp_types::Position {
line: 10,
character: 9,
},
},
};
let call = OutgoingCall {
to: to.clone(),
from_ranges: vec![tower_lsp::lsp_types::Range {
start: tower_lsp::lsp_types::Position {
line: 2,
character: 4,
},
end: tower_lsp::lsp_types::Position {
line: 2,
character: 10,
},
}],
};
assert_eq!(call.to.name, "callee");
assert_eq!(call.from_ranges.len(), 1);
assert_eq!(call.from_ranges[0].start.line, 2);
}
#[test]
fn test_call_hierarchy_empty_project() {
let mut db = WindjammerDatabase::new();
let uri = Url::parse("file:///empty.wj").unwrap();
let text = "";
let file = db.set_source_text(uri, text.to_string());
let item = db.prepare_call_hierarchy(file, 0, 0);
assert!(item.is_none());
}
}
#[cfg(test)]
mod unused_code_tests {
use super::*;
#[test]
fn test_find_unused_symbols_empty() {
let mut db = WindjammerDatabase::new();
let uri = Url::parse("file:///empty.wj").unwrap();
let text = "";
let file = db.set_source_text(uri, text.to_string());
let unused = db.find_unused_symbols(&[file]);
assert_eq!(unused.len(), 0);
}
#[test]
fn test_find_unused_symbols_all_used() {
let mut db = WindjammerDatabase::new();
let files: Vec<_> = vec![
(
Url::parse("file:///main.wj").unwrap(),
"fn main() { helper() }".to_string(),
),
(
Url::parse("file:///helper.wj").unwrap(),
"fn helper() {}".to_string(),
),
]
.into_iter()
.map(|(uri, text)| db.set_source_text(uri, text))
.collect();
let _unused = db.find_unused_symbols(&files);
}
#[test]
fn test_find_unused_functions() {
let mut db = WindjammerDatabase::new();
let files: Vec<_> = vec![
(
Url::parse("file:///main.wj").unwrap(),
"fn main() {}".to_string(),
),
(
Url::parse("file:///unused.wj").unwrap(),
"fn unused_func() {}".to_string(),
),
]
.into_iter()
.map(|(uri, text)| db.set_source_text(uri, text))
.collect();
let unused = db.find_unused_functions(&files);
for u in &unused {
assert_eq!(u.kind, SymbolKind::Function);
assert_eq!(u.reason, UnusedReason::NeverReferenced);
}
}
#[test]
fn test_find_unused_structs() {
let mut db = WindjammerDatabase::new();
let files: Vec<_> = vec![(
Url::parse("file:///structs.wj").unwrap(),
"struct UsedStruct {} struct UnusedStruct {}".to_string(),
)]
.into_iter()
.map(|(uri, text)| db.set_source_text(uri, text))
.collect();
let unused = db.find_unused_structs(&files);
for u in &unused {
assert_eq!(u.kind, SymbolKind::Struct);
}
}
#[test]
fn test_unused_reason() {
assert_eq!(UnusedReason::NeverReferenced, UnusedReason::NeverReferenced);
assert_ne!(UnusedReason::NeverReferenced, UnusedReason::OnlyInDeadCode);
}
#[test]
fn test_unused_symbol_structure() {
let unused = UnusedSymbol {
name: "test".to_string(),
kind: SymbolKind::Function,
location: tower_lsp::lsp_types::Location {
uri: Url::parse("file:///test.wj").unwrap(),
range: tower_lsp::lsp_types::Range {
start: tower_lsp::lsp_types::Position {
line: 0,
character: 0,
},
end: tower_lsp::lsp_types::Position {
line: 5,
character: 1,
},
},
},
reason: UnusedReason::NeverReferenced,
};
assert_eq!(unused.name, "test");
assert_eq!(unused.kind, SymbolKind::Function);
assert_eq!(unused.reason, UnusedReason::NeverReferenced);
}
#[test]
fn test_find_unused_with_duplicates() {
let mut db = WindjammerDatabase::new();
let files: Vec<_> = vec![
(
Url::parse("file:///file1.wj").unwrap(),
"fn duplicate() {}".to_string(),
),
(
Url::parse("file:///file2.wj").unwrap(),
"fn duplicate() {}".to_string(),
),
]
.into_iter()
.map(|(uri, text)| db.set_source_text(uri, text))
.collect();
let unused = db.find_unused_symbols(&files);
let duplicate_unused = unused.iter().filter(|u| u.name == "duplicate").count();
assert_eq!(duplicate_unused, 0);
}
}
#[cfg(test)]
mod dependency_tests {
use super::*;
#[test]
fn test_build_dependency_graph_empty() {
let mut db = WindjammerDatabase::new();
let uri = Url::parse("file:///empty.wj").unwrap();
let text = "";
let file = db.set_source_text(uri, text.to_string());
let graph = db.build_dependency_graph(&[file]);
assert_eq!(graph.files.len(), 1);
assert_eq!(graph.dependencies.len(), 0);
}
#[test]
fn test_dependency_graph_no_cycles() {
let mut db = WindjammerDatabase::new();
let files: Vec<_> = vec![
(
Url::parse("file:///main.wj").unwrap(),
"fn main() {}".to_string(),
),
(
Url::parse("file:///helper.wj").unwrap(),
"fn helper() {}".to_string(),
),
]
.into_iter()
.map(|(uri, text)| db.set_source_text(uri, text))
.collect();
let graph = db.build_dependency_graph(&files);
assert!(!graph.has_circular_dependencies());
}
#[test]
fn test_get_dependencies() {
let mut db = WindjammerDatabase::new();
let files: Vec<_> = vec![
(
Url::parse("file:///main.wj").unwrap(),
"fn main() {}".to_string(),
),
(
Url::parse("file:///helper.wj").unwrap(),
"fn helper() {}".to_string(),
),
]
.into_iter()
.map(|(uri, text)| db.set_source_text(uri, text))
.collect();
let graph = db.build_dependency_graph(&files);
let main_uri = Url::parse("file:///main.wj").unwrap();
let _deps = graph.get_dependencies(&main_uri);
}
#[test]
fn test_calculate_coupling() {
let mut db = WindjammerDatabase::new();
let files: Vec<_> = vec![
(
Url::parse("file:///main.wj").unwrap(),
"fn main() {}".to_string(),
),
(
Url::parse("file:///helper.wj").unwrap(),
"fn helper() {}".to_string(),
),
]
.into_iter()
.map(|(uri, text)| db.set_source_text(uri, text))
.collect();
let metrics = db.calculate_coupling(&files);
assert_eq!(metrics.len(), 2);
for (uri, _afferent, _efferent) in metrics {
assert!(uri.as_str().starts_with("file:///"));
}
}
#[test]
fn test_dependency_kind() {
assert_eq!(DependencyKind::Import, DependencyKind::Import);
assert_ne!(DependencyKind::Import, DependencyKind::SymbolReference);
}
#[test]
fn test_file_dependency_structure() {
let dep = FileDependency {
from: Url::parse("file:///a.wj").unwrap(),
to: Url::parse("file:///b.wj").unwrap(),
kind: DependencyKind::Import,
};
assert_eq!(dep.from.as_str(), "file:///a.wj");
assert_eq!(dep.to.as_str(), "file:///b.wj");
assert_eq!(dep.kind, DependencyKind::Import);
}
#[test]
fn test_find_circular_dependencies() {
let mut db = WindjammerDatabase::new();
let files: Vec<_> = vec![
(Url::parse("file:///a.wj").unwrap(), "fn a() {}".to_string()),
(Url::parse("file:///b.wj").unwrap(), "fn b() {}".to_string()),
]
.into_iter()
.map(|(uri, text)| db.set_source_text(uri, text))
.collect();
let _cycles = db.find_circular_dependencies(&files);
}
}
#[cfg(test)]
mod metrics_tests {
use super::*;
#[test]
fn test_calculate_file_metrics_empty() {
let mut db = WindjammerDatabase::new();
let uri = Url::parse("file:///empty.wj").unwrap();
let text = "";
let file = db.set_source_text(uri, text.to_string());
let metrics = db.calculate_file_metrics(file);
assert_eq!(metrics.lines_of_code, 0);
assert_eq!(metrics.num_functions, 0);
assert_eq!(metrics.num_structs, 0);
}
#[test]
fn test_calculate_file_metrics_with_content() {
let mut db = WindjammerDatabase::new();
let uri = Url::parse("file:///test.wj").unwrap();
let text = r#"
// Comment
fn main() {
println("Hello");
}
struct Point {
x: i32,
y: i32,
}
"#;
let file = db.set_source_text(uri, text.to_string());
let metrics = db.calculate_file_metrics(file);
assert!(metrics.lines_of_code > 0);
}
#[test]
fn test_calculate_workspace_metrics() {
let mut db = WindjammerDatabase::new();
let files: Vec<_> = vec![
(
Url::parse("file:///main.wj").unwrap(),
"fn main() {}".to_string(),
),
(
Url::parse("file:///helper.wj").unwrap(),
"fn helper() {}".to_string(),
),
]
.into_iter()
.map(|(uri, text)| db.set_source_text(uri, text))
.collect();
let metrics = db.calculate_workspace_metrics(&files);
assert_eq!(metrics.total_files, 2);
assert!(metrics.total_lines > 0);
assert!(metrics.total_functions >= 2);
}
#[test]
fn test_find_large_files() {
let mut db = WindjammerDatabase::new();
let files: Vec<_> = vec![
(
Url::parse("file:///small.wj").unwrap(),
"fn f() {}".to_string(),
),
(
Url::parse("file:///large.wj").unwrap(),
"fn f1() {}\nfn f2() {}\nfn f3() {}\nfn f4() {}\nfn f5() {}".to_string(),
),
]
.into_iter()
.map(|(uri, text)| db.set_source_text(uri, text))
.collect();
let large = db.find_large_files(&files, 2);
assert!(!large.is_empty());
assert!(large.iter().any(|(uri, _)| uri.as_str().contains("large")));
}
#[test]
fn test_find_long_functions() {
let mut db = WindjammerDatabase::new();
let files: Vec<_> = vec![(
Url::parse("file:///test.wj").unwrap(),
"fn short() {}\nfn long() {\n // line 1\n // line 2\n // line 3\n}".to_string(),
)]
.into_iter()
.map(|(uri, text)| db.set_source_text(uri, text))
.collect();
let _long = db.find_long_functions(&files, 3);
}
#[test]
fn test_file_metrics_structure() {
let mut db = WindjammerDatabase::new();
let uri = Url::parse("file:///test.wj").unwrap();
let text = "fn main() {}";
let file = db.set_source_text(uri.clone(), text.to_string());
let metrics = db.calculate_file_metrics(file);
assert_eq!(metrics.uri, uri);
assert!(metrics.avg_function_length >= 0.0);
}
#[test]
fn test_workspace_metrics_largest_file() {
let mut db = WindjammerDatabase::new();
let files: Vec<_> = vec![
(
Url::parse("file:///small.wj").unwrap(),
"fn f() {}".to_string(),
),
(
Url::parse("file:///large.wj").unwrap(),
"fn f1() {}\nfn f2() {}\nfn f3() {}".to_string(),
),
]
.into_iter()
.map(|(uri, text)| db.set_source_text(uri, text))
.collect();
let metrics = db.calculate_workspace_metrics(&files);
assert!(metrics.largest_file.is_some());
if let Some((uri, size)) = metrics.largest_file {
assert!(uri.as_str().contains("large"));
assert!(size > 0);
}
}
}
#[cfg(test)]
mod diagnostics_tests {
use super::*;
#[test]
fn test_lint_config_default() {
let config = LintConfig::default();
assert_eq!(config.max_function_length, 50);
assert_eq!(config.max_file_length, 500);
assert!(config.check_unused);
assert!(config.check_style);
}
#[test]
fn test_lint_workspace_empty() {
let mut db = WindjammerDatabase::new();
let config = LintConfig::default();
let uri = Url::parse("file:///empty.wj").unwrap();
let text = "";
let file = db.set_source_text(uri, text.to_string());
let diagnostics = db.lint_workspace(&[file], &config);
assert_eq!(diagnostics.len(), 0);
}
#[test]
fn test_check_unused_code() {
let mut db = WindjammerDatabase::new();
let files: Vec<_> = vec![
(
Url::parse("file:///main.wj").unwrap(),
"fn main() {}".to_string(),
),
(
Url::parse("file:///unused.wj").unwrap(),
"fn unused_func() {}".to_string(),
),
]
.into_iter()
.map(|(uri, text)| db.set_source_text(uri, text))
.collect();
let config = LintConfig::default();
let diagnostics = db.check_unused_code(&files, &config);
for diag in &diagnostics {
assert_eq!(diag.category, DiagnosticCategory::Unused);
assert_eq!(diag.severity, DiagnosticSeverity::Warning);
assert!(diag.suggestion.is_some());
}
}
#[test]
fn test_check_complexity() {
let mut db = WindjammerDatabase::new();
let config = LintConfig {
max_function_length: 2,
max_file_length: 5,
..Default::default()
};
let files: Vec<_> = vec![(
Url::parse("file:///test.wj").unwrap(),
"fn long() {\n line1();\n line2();\n line3();\n line4();\n}".to_string(),
)]
.into_iter()
.map(|(uri, text)| db.set_source_text(uri, text))
.collect();
let diagnostics = db.check_complexity(&files, &config);
for diag in &diagnostics {
assert_eq!(diag.category, DiagnosticCategory::CodeComplexity);
assert!(
diag.severity == DiagnosticSeverity::Warning
|| diag.severity == DiagnosticSeverity::Info
);
}
}
#[test]
fn test_diagnostic_severity() {
assert_ne!(DiagnosticSeverity::Error, DiagnosticSeverity::Warning);
assert_ne!(DiagnosticSeverity::Warning, DiagnosticSeverity::Info);
}
#[test]
fn test_diagnostic_category() {
assert_ne!(
DiagnosticCategory::Unused,
DiagnosticCategory::CodeComplexity
);
assert_ne!(
DiagnosticCategory::Naming,
DiagnosticCategory::Documentation
);
}
#[test]
fn test_capitalize_first() {
assert_eq!(capitalize_first("hello"), "Hello");
assert_eq!(capitalize_first("world"), "World");
assert_eq!(capitalize_first(""), "");
assert_eq!(capitalize_first("A"), "A");
}
#[test]
fn test_lint_workspace_with_config() {
let mut db = WindjammerDatabase::new();
let config = LintConfig {
max_function_length: 100,
max_file_length: 1000,
check_unused: false,
check_style: false,
check_performance: true,
check_security: true,
..Default::default()
};
let files: Vec<_> = vec![(
Url::parse("file:///test.wj").unwrap(),
"fn main() {}".to_string(),
)]
.into_iter()
.map(|(uri, text)| db.set_source_text(uri, text))
.collect();
let _diagnostics = db.lint_workspace(&files, &config);
}
#[test]
fn test_autofix_enabled() {
let mut db = WindjammerDatabase::new();
let config = LintConfig {
enable_autofix: true,
..Default::default()
};
let files: Vec<_> = vec![(
Url::parse("file:///test.wj").unwrap(),
"fn unused() {}".to_string(),
)]
.into_iter()
.map(|(uri, text)| db.set_source_text(uri, text))
.collect();
let diagnostics = db.check_unused_code(&files, &config);
for diag in &diagnostics {
if diag.fix.is_some() {
assert!(!diag.fix.as_ref().unwrap().description.is_empty());
}
}
}
#[test]
fn test_check_error_handling() {
let mut db = WindjammerDatabase::new();
let config = LintConfig::default();
let files: Vec<_> = vec![(
Url::parse("file:///test.wj").unwrap(),
"fn test() -> Result<i32> { panic!(\"error\") }".to_string(),
)]
.into_iter()
.map(|(uri, text)| db.set_source_text(uri, text))
.collect();
let diagnostics = db.check_error_handling(&files, &config);
assert!(diagnostics.iter().any(|d| d.rule == "avoid-panic"));
}
#[test]
fn test_check_performance() {
let mut db = WindjammerDatabase::new();
let config = LintConfig::default();
let files: Vec<_> = vec![(
Url::parse("file:///test.wj").unwrap(),
"fn test() { let v = Vec::new(); v.push(1); }".to_string(),
)]
.into_iter()
.map(|(uri, text)| db.set_source_text(uri, text))
.collect();
let diagnostics = db.check_performance(&files, &config);
assert!(diagnostics
.iter()
.any(|d| d.category == DiagnosticCategory::Performance));
}
#[test]
fn test_check_security() {
let mut db = WindjammerDatabase::new();
let config = LintConfig::default();
let files: Vec<_> = vec![(
Url::parse("file:///test.wj").unwrap(),
"fn test() { unsafe { do_something(); } }".to_string(),
)]
.into_iter()
.map(|(uri, text)| db.set_source_text(uri, text))
.collect();
let diagnostics = db.check_security(&files, &config);
assert!(diagnostics.iter().any(|d| d.rule == "unsafe-block"));
}
#[test]
fn test_autofix_structure() {
let fix = AutoFix {
description: "Test fix".to_string(),
edits: vec![TextEdit {
range: tower_lsp::lsp_types::Range::default(),
new_text: "fixed".to_string(),
}],
};
assert_eq!(fix.description, "Test fix");
assert_eq!(fix.edits.len(), 1);
assert_eq!(fix.edits[0].new_text, "fixed");
}
}
#[cfg(test)]
mod lazy_loading_tests {
use super::*;
#[test]
fn test_lazy_loading_initial_state() {
let mut db = WindjammerDatabase::new();
let uri = Url::parse("file:///test.wj").unwrap();
let file = db.set_source_text(uri, "fn test() {}".to_string());
assert!(!db.are_symbols_loaded(file));
assert!(!db.are_references_loaded(file));
}
#[test]
fn test_lazy_loading_mark_loaded() {
let mut db = WindjammerDatabase::new();
let uri = Url::parse("file:///test.wj").unwrap();
let file = db.set_source_text(uri, "fn test() {}".to_string());
db.mark_symbols_loaded(file);
assert!(db.are_symbols_loaded(file));
db.mark_references_loaded(file);
assert!(db.are_references_loaded(file));
}
#[test]
fn test_lazy_loading_get_symbols() {
let mut db = WindjammerDatabase::new();
let uri = Url::parse("file:///test.wj").unwrap();
let file = db.set_source_text(uri, "fn test() {}".to_string());
let _symbols = db.get_symbols_lazy(file);
assert!(db.are_symbols_loaded(file));
}
#[test]
fn test_lazy_loading_clear_caches() {
let mut db = WindjammerDatabase::new();
let uri = Url::parse("file:///test.wj").unwrap();
let file = db.set_source_text(uri, "fn test() {}".to_string());
db.mark_symbols_loaded(file);
db.mark_references_loaded(file);
assert!(db.are_symbols_loaded(file));
assert!(db.are_references_loaded(file));
db.clear_lazy_caches();
assert!(!db.are_symbols_loaded(file));
assert!(!db.are_references_loaded(file));
}
#[test]
fn test_lazy_loading_preload() {
let mut db = WindjammerDatabase::new();
let files: Vec<_> = vec![
(Url::parse("file:///a.wj").unwrap(), "fn a() {}".to_string()),
(Url::parse("file:///b.wj").unwrap(), "fn b() {}".to_string()),
]
.into_iter()
.map(|(uri, text)| db.set_source_text(uri, text))
.collect();
db.preload_symbols(&files);
for file in &files {
assert!(db.are_symbols_loaded(*file));
}
}
#[test]
fn test_lazy_loading_multiple_files() {
let mut db = WindjammerDatabase::new();
let files: Vec<_> = vec![
(Url::parse("file:///a.wj").unwrap(), "fn a() {}".to_string()),
(Url::parse("file:///b.wj").unwrap(), "fn b() {}".to_string()),
(Url::parse("file:///c.wj").unwrap(), "fn c() {}".to_string()),
]
.into_iter()
.map(|(uri, text)| db.set_source_text(uri, text))
.collect();
db.mark_symbols_loaded(files[0]);
db.mark_symbols_loaded(files[2]);
assert!(db.are_symbols_loaded(files[0]));
assert!(!db.are_symbols_loaded(files[1]));
assert!(db.are_symbols_loaded(files[2]));
}
}
#[cfg(test)]
mod code_actions_tests {
use super::*;
#[test]
fn test_extract_function_single_line() {
let mut db = WindjammerDatabase::new();
let uri = Url::parse("file:///test.wj").unwrap();
let code = "fn main() {\n let x = 1 + 2;\n println(x);\n}";
let file = db.set_source_text(uri, code.to_string());
let range = tower_lsp::lsp_types::Range {
start: tower_lsp::lsp_types::Position {
line: 1,
character: 12,
},
end: tower_lsp::lsp_types::Position {
line: 1,
character: 17,
},
};
let actions = db.get_code_actions(file, range);
assert!(!actions.is_empty());
let extract_action = actions
.iter()
.find(|a| a.kind == CodeActionKind::RefactorExtract);
assert!(extract_action.is_some());
let action = extract_action.unwrap();
assert_eq!(action.title, "Extract function");
assert!(action.is_preferred);
assert_eq!(action.edits.len(), 2);
}
#[test]
fn test_extract_function_multi_line() {
let mut db = WindjammerDatabase::new();
let uri = Url::parse("file:///test.wj").unwrap();
let code = "fn main() {\n let x = 1;\n let y = 2;\n println(x + y);\n}";
let file = db.set_source_text(uri, code.to_string());
let range = tower_lsp::lsp_types::Range {
start: tower_lsp::lsp_types::Position {
line: 1,
character: 4,
},
end: tower_lsp::lsp_types::Position {
line: 2,
character: 18,
},
};
let actions = db.get_code_actions(file, range);
let extract_action = actions
.iter()
.find(|a| a.kind == CodeActionKind::RefactorExtract);
assert!(extract_action.is_some());
}
#[test]
fn test_extract_function_empty_selection() {
let mut db = WindjammerDatabase::new();
let uri = Url::parse("file:///test.wj").unwrap();
let code = "fn main() {\n let x = 1;\n}";
let file = db.set_source_text(uri, code.to_string());
let range = tower_lsp::lsp_types::Range {
start: tower_lsp::lsp_types::Position {
line: 1,
character: 4,
},
end: tower_lsp::lsp_types::Position {
line: 1,
character: 4,
},
};
let actions = db.get_code_actions(file, range);
let extract_action = actions
.iter()
.find(|a| a.kind == CodeActionKind::RefactorExtract);
assert!(extract_action.is_none());
}
#[test]
fn test_code_action_kind() {
assert_ne!(CodeActionKind::QuickFix, CodeActionKind::RefactorExtract);
assert_ne!(
CodeActionKind::RefactorInline,
CodeActionKind::RefactorRename
);
}
#[test]
fn test_code_action_structure() {
let action = CodeAction {
title: "Test action".to_string(),
kind: CodeActionKind::QuickFix,
edits: vec![],
is_preferred: false,
};
assert_eq!(action.title, "Test action");
assert_eq!(action.kind, CodeActionKind::QuickFix);
assert!(!action.is_preferred);
}
}