use std::path::{Path, PathBuf};
use super::languages::LanguageRegistry;
use super::types::parse_source;
use super::var_types::{
extract_go_var_types, extract_java_var_types, extract_kotlin_var_types, extract_php_var_types,
extract_python_definitions, extract_rust_var_types, extract_ts_var_types, FileParseResult,
};
pub(crate) fn extract_definitions(
source: &str,
file_path: &Path,
language: &str,
) -> FileParseResult {
if language.to_lowercase() == "python" {
return extract_python_definitions(source, file_path);
}
let registry = LanguageRegistry::with_defaults();
if let Some(handler) = registry.get(language) {
let imports = handler.parse_imports(source, file_path).unwrap_or_default();
let tree = match parse_source(source, language) {
Ok(t) => t,
Err(e) => {
return FileParseResult {
error: Some(format!("Parse failed: {}", e)),
..Default::default()
};
}
};
let calls = handler
.extract_calls(file_path, source, &tree)
.unwrap_or_default();
let (funcs, classes_defs) = handler
.extract_definitions(source, file_path, &tree)
.unwrap_or_default();
let var_types = match language.to_lowercase().as_str() {
"go" => extract_go_var_types(&tree, source.as_bytes()),
"typescript" | "javascript" => extract_ts_var_types(&tree, source.as_bytes()),
"java" => extract_java_var_types(&tree, source.as_bytes()),
"rust" => extract_rust_var_types(&tree, source.as_bytes()),
"kotlin" => extract_kotlin_var_types(&tree, source.as_bytes()),
"php" => extract_php_var_types(&tree, source.as_bytes()),
_ => Vec::new(),
};
drop(tree);
FileParseResult {
funcs,
classes: classes_defs,
imports,
calls,
var_types,
error: None,
}
} else {
FileParseResult::default()
}
}
pub(crate) fn normalize_path_relative_to_root(
file_path: &Path,
root: &Path,
canonical_root: Option<&Path>,
) -> PathBuf {
if let (Some(canon_root), Ok(canon_file)) = (canonical_root, file_path.canonicalize()) {
if let Ok(relative) = canon_file.strip_prefix(canon_root) {
let rel_str = relative.to_string_lossy().replace('\\', "/");
return PathBuf::from(rel_str);
}
}
if let Ok(relative) = file_path.strip_prefix(root) {
let rel_str = relative.to_string_lossy().replace('\\', "/");
return PathBuf::from(rel_str);
}
let file_str = file_path.to_string_lossy();
let root_str = root.to_string_lossy();
if let Some(stripped) = file_str.strip_prefix(root_str.as_ref()) {
let cleaned = stripped.trim_start_matches('/').trim_start_matches('\\');
return PathBuf::from(cleaned.replace('\\', "/"));
}
PathBuf::from(file_str.replace('\\', "/"))
}
pub fn path_to_module(path: &Path, language: &str) -> String {
let lang = language.to_lowercase();
match lang.as_str() {
"typescript" | "javascript" => path_to_module_typescript(path),
"go" => path_to_module_go(path),
"rust" => path_to_module_rust(path),
"java" => path_to_module_java(path),
"kotlin" => path_to_module_kotlin(path),
"scala" => path_to_module_scala(path),
"csharp" | "c#" => path_to_module_csharp(path),
"php" => path_to_module_php(path),
"ruby" => path_to_module_ruby(path),
"lua" | "luau" => path_to_module_lua(path),
"elixir" => path_to_module_elixir(path),
"swift" => path_to_module_swift(path),
"c" => path_to_module_c(path),
"cpp" | "c++" => path_to_module_cpp(path),
"ocaml" => path_to_module_ocaml(path),
_ => path_to_module_python(path),
}
}
fn path_to_module_python(path: &Path) -> String {
let path_str = path.to_string_lossy().replace('\\', "/");
let path_str = path_str
.strip_prefix("src/")
.or_else(|| path_str.strip_prefix("lib/"))
.unwrap_or(&path_str);
let path_str = path_str
.strip_suffix(".py")
.or_else(|| path_str.strip_suffix(".rs"))
.or_else(|| path_str.strip_suffix(".ts"))
.or_else(|| path_str.strip_suffix(".tsx"))
.or_else(|| path_str.strip_suffix(".js"))
.or_else(|| path_str.strip_suffix(".jsx"))
.or_else(|| path_str.strip_suffix(".go"))
.unwrap_or(path_str);
let path_str = path_str.strip_suffix("/__init__").unwrap_or(path_str);
path_str.replace('/', ".")
}
fn path_to_module_typescript(path: &Path) -> String {
let path_str = path.to_string_lossy().replace('\\', "/");
let file_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
if file_name == "index.ts" || file_name == "index.tsx" || file_name == "index.js" {
let parent = path.parent().unwrap_or(Path::new(""));
let parent_str = parent.to_string_lossy().replace('\\', "/");
return format!("./{}", parent_str);
}
let stripped = path_str
.strip_suffix(".ts")
.or_else(|| path_str.strip_suffix(".tsx"))
.or_else(|| path_str.strip_suffix(".js"))
.or_else(|| path_str.strip_suffix(".jsx"))
.or_else(|| path_str.strip_suffix(".mjs"))
.or_else(|| path_str.strip_suffix(".cjs"))
.unwrap_or(&path_str);
format!("./{}", stripped)
}
fn path_to_module_go(path: &Path) -> String {
path.parent()
.map(|p| p.to_string_lossy().replace('\\', "/"))
.unwrap_or_default()
}
fn path_to_module_rust(path: &Path) -> String {
let path_str = path.to_string_lossy().replace('\\', "/");
let file_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
if file_name == "lib.rs" || file_name == "main.rs" {
return "crate".to_string();
}
if file_name == "mod.rs" {
let parent = path.parent().unwrap_or(Path::new(""));
let parts: Vec<&str> = parent
.iter()
.filter_map(|s| s.to_str())
.filter(|s| *s != "src" && !s.is_empty())
.collect();
if parts.is_empty() {
return "crate".to_string();
}
return format!("crate::{}", parts.join("::"));
}
let stripped = path_str.strip_suffix(".rs").unwrap_or(&path_str);
let parts: Vec<&str> = stripped
.split('/')
.filter(|s| *s != "src" && !s.is_empty())
.collect();
if parts.is_empty() {
return "crate".to_string();
}
format!("crate::{}", parts.join("::"))
}
const JAVA_PREFIXES: [&str; 5] = ["src/main/java/", "src/test/java/", "src/", "lib/", "app/"];
const KOTLIN_PREFIXES: [&str; 5] = [
"src/main/kotlin/",
"src/test/kotlin/",
"src/",
"lib/",
"app/",
];
const SCALA_PREFIXES: [&str; 5] = ["src/main/scala/", "src/test/scala/", "src/", "lib/", "app/"];
const CSHARP_PREFIXES: [&str; 3] = ["src/", "lib/", "app/"];
const PHP_PREFIXES: [&str; 5] = ["src/", "lib/", "app/", "public/", "includes/"];
const RUBY_PREFIXES: [&str; 3] = ["lib/", "src/", "app/"];
const LUA_PREFIXES: [&str; 3] = ["src/", "lib/", "scripts/"];
const SWIFT_PREFIXES: [&str; 2] = ["src/", "lib/"];
const OCAML_PREFIXES: [&str; 3] = ["src/", "lib/", "app/"];
fn normalize_rel_str(path: &Path) -> String {
let mut rel = path.to_string_lossy().replace('\\', "/");
if let Some(stripped) = rel.strip_prefix("./") {
rel = stripped.to_string();
}
rel.trim_start_matches('/').to_string()
}
fn strip_known_prefixes<'a>(path: &'a str, prefixes: &[&str]) -> &'a str {
let mut best_end: Option<usize> = None;
let mut best_prefix_len: usize = 0;
for prefix in prefixes {
if let Some(pos) = path.find(prefix) {
if (pos == 0 || path.as_bytes()[pos - 1] == b'/')
&& prefix.len() > best_prefix_len {
best_prefix_len = prefix.len();
best_end = Some(pos + prefix.len());
}
}
}
if let Some(end) = best_end {
&path[end..]
} else {
path
}
}
fn strip_extension_any<'a>(path: &'a str, extensions: &[&str]) -> &'a str {
for ext in extensions {
if let Some(stripped) = path.strip_suffix(ext) {
return stripped;
}
}
path
}
fn dot_module_from_path(path: &Path, prefixes: &[&str], extensions: &[&str]) -> String {
let rel = normalize_rel_str(path);
let rel = strip_known_prefixes(&rel, prefixes);
let rel = strip_extension_any(rel, extensions);
if rel.is_empty() {
return String::new();
}
rel.split('/')
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join(".")
}
fn separator_module_from_path(
path: &Path,
prefixes: &[&str],
extensions: &[&str],
separator: char,
) -> String {
let rel = normalize_rel_str(path);
let rel = strip_known_prefixes(&rel, prefixes);
let rel = strip_extension_any(rel, extensions);
if rel.is_empty() {
return String::new();
}
if separator == '/' {
rel.to_string()
} else {
rel.replace('/', &separator.to_string())
}
}
fn snake_to_camel(segment: &str) -> String {
segment
.split(['_', '-'])
.filter(|s| !s.is_empty())
.map(|part| {
let mut chars = part.chars();
match chars.next() {
Some(first) => {
let mut out = String::new();
out.push(first.to_ascii_uppercase());
out.push_str(chars.as_str());
out
}
None => String::new(),
}
})
.collect::<Vec<_>>()
.join("")
}
fn swift_module_from_sources(rest: &str) -> String {
let rest = strip_extension_any(rest, &[".swift"]);
let mut parts = rest.split('/').filter(|s| !s.is_empty());
let module = parts.next().unwrap_or("");
if module.is_empty() {
return String::new();
}
let remainder: Vec<&str> = parts.collect();
if remainder.is_empty() {
module.to_string()
} else {
format!("{}.{}", module, remainder.join("."))
}
}
fn path_to_module_java(path: &Path) -> String {
dot_module_from_path(path, &JAVA_PREFIXES, &[".java"])
}
fn path_to_module_kotlin(path: &Path) -> String {
dot_module_from_path(path, &KOTLIN_PREFIXES, &[".kt", ".kts"])
}
fn path_to_module_scala(path: &Path) -> String {
dot_module_from_path(path, &SCALA_PREFIXES, &[".scala"])
}
fn path_to_module_csharp(path: &Path) -> String {
dot_module_from_path(path, &CSHARP_PREFIXES, &[".cs"])
}
fn path_to_module_php(path: &Path) -> String {
separator_module_from_path(path, &PHP_PREFIXES, &[".php"], '\\')
}
fn path_to_module_ruby(path: &Path) -> String {
separator_module_from_path(path, &RUBY_PREFIXES, &[".rb"], '/')
}
fn path_to_module_lua(path: &Path) -> String {
dot_module_from_path(path, &LUA_PREFIXES, &[".lua", ".luau"])
}
fn path_to_module_elixir(path: &Path) -> String {
let rel = normalize_rel_str(path);
let mut module_parts: Vec<String> = Vec::new();
if let Some(rest) = rel.strip_prefix("apps/") {
let mut parts = rest.splitn(2, '/');
if let Some(app) = parts.next() {
if let Some(after_app) = parts.next() {
if let Some(after_lib) = after_app.strip_prefix("lib/") {
module_parts.push(snake_to_camel(app));
let stripped = strip_extension_any(after_lib, &[".ex", ".exs"]);
for seg in stripped.split('/') {
if seg.is_empty() {
continue;
}
module_parts.push(snake_to_camel(seg));
}
return module_parts.join(".");
}
}
}
}
let rel = if let Some(after_lib) = rel.strip_prefix("lib/") {
after_lib
} else {
rel.as_str()
};
let rel = strip_extension_any(rel, &[".ex", ".exs"]);
for seg in rel.split('/') {
if seg.is_empty() {
continue;
}
module_parts.push(snake_to_camel(seg));
}
module_parts.join(".")
}
fn path_to_module_swift(path: &Path) -> String {
let rel = normalize_rel_str(path);
if let Some(rest) = rel.strip_prefix("Sources/") {
return swift_module_from_sources(rest);
}
if let Some(rest) = rel.strip_prefix("Tests/") {
return swift_module_from_sources(rest);
}
dot_module_from_path(path, &SWIFT_PREFIXES, &[".swift"])
}
fn path_to_module_c(path: &Path) -> String {
normalize_rel_str(path)
}
fn path_to_module_cpp(path: &Path) -> String {
normalize_rel_str(path)
}
fn path_to_module_ocaml(path: &Path) -> String {
dot_module_from_path(path, &OCAML_PREFIXES, &[".ml", ".mli"])
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
#[test]
fn test_extract_definitions_typescript_imports() {
let ts_source = r#"
import { useState } from 'react';
import axios from 'axios';
function App() {
const [data, setData] = useState(null);
axios.get('/api').then(res => setData(res.data));
}
"#;
let result = extract_definitions(ts_source, Path::new("app.tsx"), "typescript");
assert!(
!result.imports.is_empty(),
"TypeScript extract_definitions should produce imports, got empty"
);
let import_modules: Vec<&str> = result.imports.iter().map(|i| i.module.as_str()).collect();
assert!(
import_modules.iter().any(|m| m.contains("react")),
"Should find react import, got: {:?}",
import_modules
);
}
#[test]
fn test_extract_definitions_typescript_calls() {
let ts_source = r#"
import { readFile } from 'fs';
function loadConfig(path: string) {
readFile(path, (err, data) => {
console.log(data);
});
}
function main() {
loadConfig('./config.json');
}
"#;
let result = extract_definitions(ts_source, Path::new("main.ts"), "typescript");
assert!(
!result.calls.is_empty(),
"TypeScript extract_definitions should produce calls, got empty"
);
}
#[test]
fn test_extract_definitions_python_regression() {
let py_source = r#"
import os
def greet(name):
print(f"Hello, {name}")
def main():
greet("world")
os.path.exists("/tmp")
"#;
let result = extract_definitions(py_source, Path::new("main.py"), "python");
assert!(
!result.funcs.is_empty(),
"Python should still find functions"
);
assert!(
!result.imports.is_empty(),
"Python should still find imports"
);
assert!(!result.calls.is_empty(), "Python should still find calls");
}
#[test]
fn test_extract_definitions_unknown_language() {
let source = "some code";
let result = extract_definitions(source, Path::new("test.bf"), "brainfuck");
assert!(result.funcs.is_empty());
assert!(result.classes.is_empty());
assert!(result.imports.is_empty());
assert!(result.calls.is_empty());
}
#[test]
fn test_extract_definitions_go_imports() {
let go_source = r#"
package main
import (
"fmt"
"os"
)
func main() {
fmt.Println("hello")
os.Exit(0)
}
"#;
let result = extract_definitions(go_source, Path::new("main.go"), "go");
assert!(
!result.imports.is_empty(),
"Go extract_definitions should produce imports, got empty"
);
}
#[test]
fn test_extract_definitions_rust_imports() {
let rust_source = r#"
use std::collections::HashMap;
use std::path::Path;
fn process(map: &HashMap<String, String>) {
println!("{:?}", map);
}
"#;
let result = extract_definitions(rust_source, Path::new("lib.rs"), "rust");
assert!(
!result.imports.is_empty(),
"Rust extract_definitions should produce imports, got empty"
);
}
#[test]
fn test_path_to_module_python_unchanged() {
assert_eq!(
path_to_module(Path::new("myapp/utils.py"), "python"),
"myapp.utils"
);
assert_eq!(
path_to_module(Path::new("pkg/sub/module.py"), "python"),
"pkg.sub.module"
);
assert_eq!(path_to_module(Path::new("module.py"), "python"), "module");
assert_eq!(
path_to_module(Path::new("pkg/__init__.py"), "python"),
"pkg"
);
}
#[test]
fn test_path_to_module_python_strips_src_prefix() {
assert_eq!(
path_to_module(Path::new("src/pkg/module.py"), "python"),
"pkg.module"
);
assert_eq!(
path_to_module(Path::new("lib/pkg/module.py"), "python"),
"pkg.module"
);
}
#[test]
fn test_path_to_module_typescript_uses_dot_slash_prefix() {
assert_eq!(
path_to_module(Path::new("errors.ts"), "typescript"),
"./errors"
);
assert_eq!(
path_to_module(Path::new("v4/core/errors.ts"), "typescript"),
"./v4/core/errors"
);
assert_eq!(
path_to_module(Path::new("utils.tsx"), "typescript"),
"./utils"
);
assert_eq!(
path_to_module(Path::new("helpers.js"), "javascript"),
"./helpers"
);
}
#[test]
fn test_path_to_module_typescript_index_files() {
assert_eq!(
path_to_module(Path::new("utils/index.ts"), "typescript"),
"./utils"
);
assert_eq!(
path_to_module(Path::new("v4/core/index.tsx"), "typescript"),
"./v4/core"
);
}
#[test]
fn test_path_to_module_go_slash_separated() {
assert_eq!(
path_to_module(Path::new("pkg/utils/helpers.go"), "go"),
"pkg/utils"
);
assert_eq!(path_to_module(Path::new("cmd/main.go"), "go"), "cmd");
assert_eq!(path_to_module(Path::new("main.go"), "go"), "");
}
#[test]
fn test_path_to_module_rust_crate_prefix() {
assert_eq!(path_to_module(Path::new("src/lib.rs"), "rust"), "crate");
assert_eq!(path_to_module(Path::new("src/main.rs"), "rust"), "crate");
assert_eq!(
path_to_module(Path::new("src/utils/mod.rs"), "rust"),
"crate::utils"
);
assert_eq!(
path_to_module(Path::new("src/utils/helpers.rs"), "rust"),
"crate::utils::helpers"
);
}
#[test]
fn test_path_to_module_default_language_uses_python_style() {
assert_eq!(
path_to_module(Path::new("module.rb"), "unknown"),
"module.rb"
);
}
#[test]
fn test_path_to_module_java_flat() {
assert_eq!(
path_to_module(Path::new("src/main/java/com/example/Foo.java"), "java"),
"com.example.Foo"
);
assert_eq!(
path_to_module(Path::new("com/example/Foo.java"), "java"),
"com.example.Foo"
);
}
#[test]
fn test_path_to_module_java_nested() {
assert_eq!(
path_to_module(Path::new("backend/src/main/java/com/example/service/UserService.java"), "java"),
"com.example.service.UserService"
);
assert_eq!(
path_to_module(Path::new("modules/core/src/test/java/com/example/FooTest.java"), "java"),
"com.example.FooTest"
);
}
#[test]
fn test_path_to_module_kotlin_flat() {
assert_eq!(
path_to_module(Path::new("src/main/kotlin/com/example/Bar.kt"), "kotlin"),
"com.example.Bar"
);
}
#[test]
fn test_path_to_module_kotlin_nested() {
assert_eq!(
path_to_module(Path::new("app/src/main/kotlin/com/example/ui/MainScreen.kt"), "kotlin"),
"com.example.ui.MainScreen"
);
assert_eq!(
path_to_module(Path::new("feature/auth/src/main/kotlin/com/example/auth/Login.kt"), "kotlin"),
"com.example.auth.Login"
);
}
#[test]
fn test_path_to_module_scala_flat() {
assert_eq!(
path_to_module(Path::new("src/main/scala/com/example/Baz.scala"), "scala"),
"com.example.Baz"
);
}
#[test]
fn test_path_to_module_scala_nested() {
assert_eq!(
path_to_module(Path::new("core/src/main/scala/com/example/domain/Model.scala"), "scala"),
"com.example.domain.Model"
);
assert_eq!(
path_to_module(Path::new("project/sub/src/test/scala/com/example/SpecTest.scala"), "scala"),
"com.example.SpecTest"
);
}
#[test]
fn test_path_to_module_csharp_flat() {
assert_eq!(
path_to_module(Path::new("src/Models/User.cs"), "csharp"),
"Models.User"
);
}
#[test]
fn test_path_to_module_csharp_nested() {
assert_eq!(
path_to_module(Path::new("MyProject/src/Models/User.cs"), "csharp"),
"Models.User"
);
assert_eq!(
path_to_module(Path::new("backend/app/Controllers/HomeController.cs"), "csharp"),
"Controllers.HomeController"
);
}
}