use super::*;
fn nested_rust_modules(depth: usize, inner: &str) -> String {
let mut source = String::new();
for level in 0..depth {
source.push_str(&format!("mod layer_{level} {{\n"));
}
source.push_str(inner);
source.push('\n');
for _ in 0..depth {
source.push_str("}\n");
}
source
}
#[test]
fn test_language_from_path() {
assert_eq!(
Language::from_path(std::path::Path::new("foo.rs")),
Language::Rust
);
assert_eq!(
Language::from_path(std::path::Path::new("foo.py")),
Language::Python
);
assert_eq!(
Language::from_path(std::path::Path::new("foo.zig")),
Language::Zig
);
assert_eq!(
Language::from_path(std::path::Path::new("foo.txt")),
Language::Unknown
);
}
#[test]
fn test_parse_rust_function() {
let source = r#"
fn hello_world() -> String {
"Hello".to_string()
}
fn add(a: i32, b: i32) -> i32 {
a + b
}
"#;
let parsed = ParsedFile::parse(source, Language::Rust).expect("Should parse");
let functions = parsed.extract_functions();
assert_eq!(functions.len(), 2);
assert_eq!(functions[0].name, "hello_world");
assert_eq!(functions[1].name, "add");
assert!(functions[1].signature.contains("a"));
assert!(functions[1].signature.contains("b"));
}
#[cfg(all(
feature = "lang-rust",
feature = "lang-python",
feature = "lang-javascript",
feature = "lang-typescript"
))]
#[test]
fn test_parse_common_language_functions_and_imports() {
let cases = [
(
Language::Rust,
r#"
use std::collections::HashMap;
pub async fn load_map() -> HashMap<String, usize> {
HashMap::new()
}
"#,
"load_map",
"std::collections",
),
(
Language::Python,
r#"
from pathlib import Path
@pytest.mark.slow
async def load_path(root: Path) -> Path:
return root / "file.txt"
"#,
"load_path",
"pathlib",
),
(
Language::JavaScript,
r#"
import fs from "node:fs";
export const readConfig = async (path) => {
return fs.readFileSync(path, "utf8");
};
"#,
"readConfig",
"node:fs",
),
(
Language::TypeScript,
r#"
import type { Request } from "./types";
export const handleRequest = (request: Request): string => {
return request.id;
};
"#,
"handleRequest",
"./types",
),
];
for (language, source, function_name, import_text) in cases {
let parsed = ParsedFile::parse(source, language)
.unwrap_or_else(|| panic!("{language:?} should parse"));
let functions = parsed.extract_functions();
assert!(
functions
.iter()
.any(|function| function.name == function_name),
"{language:?} should extract {function_name}: {functions:?}"
);
let imports = parsed.extract_imports();
assert!(
imports
.iter()
.any(|import| import.raw.contains(import_text)),
"{language:?} should extract import containing {import_text}: {imports:?}"
);
}
}
#[cfg(all(
feature = "lang-c",
feature = "lang-cpp",
feature = "lang-go",
feature = "lang-java"
))]
#[test]
fn test_parse_extended_language_functions_and_imports() {
let cases = [
(
Language::Go,
r#"
package main
import "context"
func Serve(ctx context.Context) error {
return nil
}
"#,
"Serve",
"context",
),
(
Language::Java,
r#"
import java.util.List;
class Handler {
public String handle(List<String> values) {
return values.get(0);
}
}
"#,
"handle",
"java.util.List",
),
(
Language::C,
r#"
#include <stdio.h>
int add(int left, int right) {
return left + right;
}
"#,
"add",
"",
),
(
Language::Cpp,
r#"
#include <vector>
int sum(std::vector<int> values) {
return values.size();
}
"#,
"sum",
"",
),
];
for (language, source, function_name, import_text) in cases {
let parsed = ParsedFile::parse(source, language)
.unwrap_or_else(|| panic!("{language:?} should parse"));
let functions = parsed.extract_functions();
assert!(
functions
.iter()
.any(|function| function.name == function_name),
"{language:?} should extract {function_name}: {functions:?}"
);
if !import_text.is_empty() {
let imports = parsed.extract_imports();
assert!(
imports
.iter()
.any(|import| import.raw.contains(import_text)),
"{language:?} should extract import containing {import_text}: {imports:?}"
);
}
}
}
#[test]
fn test_extract_rust_imports() {
let source = r#"
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
extern crate anyhow;
fn main() {}
"#;
let parsed = ParsedFile::parse(source, Language::Rust).expect("Should parse");
let imports = parsed.extract_imports();
assert_eq!(imports.len(), 3);
assert!(imports.iter().any(|i| i.raw.contains("std")));
assert!(imports.iter().any(|i| i.raw.contains("serde")));
assert!(imports.iter().any(|i| i.raw.contains("anyhow")));
}
#[test]
fn test_extract_functions_handles_deeply_nested_modules() {
let source = nested_rust_modules(512, "fn deeply_nested() -> i32 { 42 }");
let parsed = ParsedFile::parse(source, Language::Rust).expect("Should parse");
let functions = parsed.extract_functions();
assert_eq!(functions.len(), 1);
assert_eq!(functions[0].name, "deeply_nested");
}
#[test]
fn extract_functions_qualifies_impl_methods_by_container() {
let source = r#"
impl Foo {
fn run() { 1 }
}
impl Bar {
fn run() { 2 }
}
"#;
let parsed = ParsedFile::parse(source, Language::Rust).expect("Should parse");
let functions = parsed.extract_functions();
let identities: Vec<String> = functions.iter().map(|f| f.symbol_identity()).collect();
assert!(
identities
.iter()
.any(|id| id.contains("Foo") && id.contains("run")),
"Foo::run must be qualified: {identities:?}"
);
assert!(
identities
.iter()
.any(|id| id.contains("Bar") && id.contains("run")),
"Bar::run must be qualified: {identities:?}"
);
assert_ne!(
functions[0].symbol_identity(),
functions[1].symbol_identity()
);
}
#[test]
fn extract_functions_qualifies_nested_mod_impl_methods() {
let source = r#"
mod a {
impl Foo {
fn run() { 1 }
}
}
mod b {
impl Foo {
fn run() { 2 }
}
}
"#;
let parsed = ParsedFile::parse(source, Language::Rust).expect("Should parse");
let functions = parsed.extract_functions();
let identities: Vec<String> = functions.iter().map(|f| f.symbol_identity()).collect();
assert!(
identities
.iter()
.any(|id| id.contains("a") && id.contains("Foo") && id.contains("run")),
"a::Foo::run must be qualified: {identities:?}"
);
assert!(
identities
.iter()
.any(|id| id.contains("b") && id.contains("Foo") && id.contains("run")),
"b::Foo::run must be qualified: {identities:?}"
);
assert_ne!(
functions[0].symbol_identity(),
functions[1].symbol_identity()
);
}
#[test]
fn extract_functions_includes_javascript_object_literal_methods() {
let source = r#"
export const handlers = {
save: async () => {
await persist();
},
load: function () {
return fetchItem();
},
};
"#;
let parsed = ParsedFile::parse(source, Language::JavaScript).expect("Should parse");
let functions = parsed.extract_functions();
let names: Vec<&str> = functions.iter().map(|f| f.name.as_str()).collect();
assert!(
names.contains(&"save"),
"object-literal arrow method must extract: {names:?}"
);
assert!(
names.contains(&"load"),
"object-literal function method must extract: {names:?}"
);
}
#[test]
fn extract_calls_includes_python_call_nodes() {
let source = r#"
def target():
return 1
def test_target():
target()
"#;
let parsed = ParsedFile::parse(source, Language::Python).expect("Should parse");
let calls = parsed.extract_calls();
assert!(
calls
.iter()
.any(|call| call.name == "target" && call.qualifier.is_empty()),
"python call nodes must be extracted: {calls:?}"
);
}
#[test]
fn extract_own_calls_excludes_nested_function_and_closure() {
let rust = r#"
fn test_x() {
fn unused() {
target();
}
let unused_closure = || {
other();
};
}
"#;
let parsed = ParsedFile::parse(rust, Language::Rust).expect("Should parse");
let own = parsed.extract_own_calls();
assert!(
own.iter()
.all(|call| call.name != "target" && call.name != "other"),
"nested rust calls must not leak to the outer function: {own:?}"
);
let js = r#"
function test_x() {
function unused() {
target();
}
const unused_arrow = () => {
other();
};
}
"#;
let parsed = ParsedFile::parse(js, Language::JavaScript).expect("Should parse");
let own = parsed.extract_own_calls();
assert!(
own.iter()
.all(|call| call.name != "target" && call.name != "other"),
"nested js calls must not leak to the outer function: {own:?}"
);
}
#[test]
fn extract_calls_uses_tree_not_comment_or_string_substrings() {
let source = r#"
fn test_it() {
Bar::run();
foo.run();
covered();
// TODO: call orphan()
let _ = "orphan()";
}
"#;
let parsed = ParsedFile::parse(source, Language::Rust).expect("Should parse");
let calls = parsed.extract_calls();
assert!(
calls
.iter()
.any(|call| call.name == "run" && call.qualifier == ["Bar"]),
"path call Bar::run: {calls:?}"
);
assert!(
calls
.iter()
.any(|call| call.name == "run" && call.qualifier == ["foo"]),
"receiver call foo.run: {calls:?}"
);
assert!(
calls
.iter()
.any(|call| call.name == "covered" && call.qualifier.is_empty()),
"bare call covered: {calls:?}"
);
assert!(
calls.iter().all(|call| call.name != "orphan"),
"comment/string must not create call edges: {calls:?}"
);
}
#[cfg(feature = "lang-cpp")]
#[test]
fn test_cpp_templated_qualified_function_names() {
let source = r#"
template <typename U>
struct Foo {
void bar();
void baz();
};
template <typename U>
void Foo<U>::bar() {}
template <typename U>
void Foo<U>::baz() {}
"#;
let parsed = ParsedFile::parse(source, Language::Cpp).expect("Should parse");
let functions = parsed.extract_functions();
let names: Vec<&str> = functions.iter().map(|f| f.name.as_str()).collect();
assert!(
names.contains(&"bar"),
"expected templated qualified def to resolve as `bar`, got {names:?}"
);
assert!(
names.contains(&"baz"),
"expected templated qualified def to resolve as `baz`, got {names:?}"
);
let foo_count = names.iter().filter(|n| **n == "Foo").count();
assert_eq!(
foo_count, 0,
"templated qualified method defs should not resolve to scope name `Foo`: {names:?}"
);
}