use crate::ca_cache;
use crate::parsers;
use normalize_facts_core::SymbolKind;
use normalize_languages::{Language, Symbol, Visibility, support_for_path};
use std::path::Path;
use streaming_iterator::StreamingIterator;
use tree_sitter;
pub struct ExtractResult {
pub symbols: Vec<Symbol>,
pub file_path: String,
}
impl ExtractResult {
pub fn filter_types(&self) -> ExtractResult {
use normalize_facts_core::SymbolKind;
fn is_type_kind(kind: SymbolKind) -> bool {
matches!(
kind,
SymbolKind::Class
| SymbolKind::Struct
| SymbolKind::Enum
| SymbolKind::Trait
| SymbolKind::Interface
| SymbolKind::Type
| SymbolKind::Module
)
}
fn filter_symbol(sym: &Symbol) -> Option<Symbol> {
if is_type_kind(sym.kind) {
let type_children: Vec<_> = sym.children.iter().filter_map(filter_symbol).collect();
Some(Symbol {
name: sym.name.clone(),
kind: sym.kind,
signature: sym.signature.clone(),
docstring: sym.docstring.clone(),
attributes: Vec::new(),
start_line: sym.start_line,
end_line: sym.end_line,
visibility: sym.visibility,
children: type_children,
is_interface_impl: sym.is_interface_impl,
implements: sym.implements.clone(),
})
} else {
None
}
}
let filtered_symbols: Vec<_> = self.symbols.iter().filter_map(filter_symbol).collect();
ExtractResult {
symbols: filtered_symbols,
file_path: self.file_path.clone(),
}
}
pub fn filter_tests(&self) -> ExtractResult {
use normalize_languages::support_for_path;
use std::path::Path;
let lang = support_for_path(Path::new(&self.file_path));
fn filter_symbol(sym: &Symbol, lang: Option<&dyn Language>) -> Option<Symbol> {
let is_test = match lang {
Some(l) => l.is_test_symbol(sym),
None => false, };
if is_test {
return None;
}
let filtered_children: Vec<_> = sym
.children
.iter()
.filter_map(|c| filter_symbol(c, lang))
.collect();
Some(Symbol {
name: sym.name.clone(),
kind: sym.kind,
signature: sym.signature.clone(),
docstring: sym.docstring.clone(),
attributes: sym.attributes.clone(),
start_line: sym.start_line,
end_line: sym.end_line,
visibility: sym.visibility,
children: filtered_children,
is_interface_impl: sym.is_interface_impl,
implements: sym.implements.clone(),
})
}
let lang_ref: Option<&dyn Language> = lang.map(|l| l as &dyn Language);
let filtered_symbols: Vec<_> = self
.symbols
.iter()
.filter_map(|s| filter_symbol(s, lang_ref))
.collect();
ExtractResult {
symbols: filtered_symbols,
file_path: self.file_path.clone(),
}
}
}
#[derive(Clone)]
pub struct ExtractOptions {
pub include_private: bool,
}
impl Default for ExtractOptions {
fn default() -> Self {
Self {
include_private: true,
}
}
}
pub trait InterfaceResolver {
fn resolve_interface_methods(&self, name: &str, current_file: &str) -> Option<Vec<String>>;
}
pub struct OnDemandResolver<'a> {
root: &'a std::path::Path,
}
impl<'a> OnDemandResolver<'a> {
pub fn new(root: &'a std::path::Path) -> Self {
Self { root }
}
}
impl InterfaceResolver for OnDemandResolver<'_> {
fn resolve_interface_methods(&self, name: &str, current_file: &str) -> Option<Vec<String>> {
use normalize_languages::support_for_path;
let current_path = std::path::Path::new(current_file);
let current_dir = current_path.parent()?;
let candidates = [
"types.ts",
"interfaces.ts",
"index.ts",
"../types.ts",
"../interfaces.ts",
"../index.ts",
];
for candidate in candidates {
let candidate_path = if candidate.starts_with("..") {
current_dir.parent()?.join(&candidate[3..])
} else {
current_dir.join(candidate)
};
let full_path = self.root.join(&candidate_path);
if !full_path.exists() {
continue;
}
let content = std::fs::read_to_string(&full_path).ok()?;
let _support = support_for_path(&full_path)?;
let extractor = Extractor::new();
let result = extractor.extract(&full_path, &content);
for sym in &result.symbols {
if sym.name == name
&& matches!(
sym.kind,
normalize_languages::SymbolKind::Interface
| normalize_languages::SymbolKind::Class
)
{
let methods: Vec<String> = sym
.children
.iter()
.filter(|c| {
matches!(
c.kind,
normalize_languages::SymbolKind::Method
| normalize_languages::SymbolKind::Function
)
})
.map(|c| c.name.clone())
.collect();
if !methods.is_empty() {
return Some(methods);
}
}
}
}
None
}
}
pub struct Extractor {
options: ExtractOptions,
}
impl Default for Extractor {
fn default() -> Self {
Self::new()
}
}
impl Extractor {
pub fn new() -> Self {
Self {
options: ExtractOptions::default(),
}
}
pub fn with_options(options: ExtractOptions) -> Self {
Self { options }
}
pub fn extract(&self, path: &Path, content: &str) -> ExtractResult {
self.extract_with_resolver(path, content, None)
}
pub fn extract_with_resolver(
&self,
path: &Path,
content: &str,
resolver: Option<&dyn InterfaceResolver>,
) -> ExtractResult {
let file_path = path.to_string_lossy().to_string();
let symbols = match support_for_path(path) {
Some(support) => self.extract_with_support(content, support, resolver, &file_path),
None => Vec::new(),
};
ExtractResult { symbols, file_path }
}
fn extract_with_support(
&self,
content: &str,
support: &dyn Language,
resolver: Option<&dyn InterfaceResolver>,
current_file: &str,
) -> Vec<Symbol> {
let grammar_name = support.grammar_name();
let cache_ver = if self.options.include_private {
"symbols-v1-all"
} else {
"symbols-v1-public"
};
if resolver.is_none()
&& let Some(cache) = ca_cache::symbol_cache()
{
let hash = blake3::hash(content.as_bytes());
match cache.get::<Vec<Symbol>>(hash.as_bytes(), cache_ver, grammar_name) {
Ok(Some(cached)) => return cached,
Ok(None) => {} Err(e) => {
tracing::debug!(
"normalize-facts: symbol cache get error for {}: {}",
current_file,
e
);
}
}
}
let tree = match parsers::parse_with_grammar(grammar_name, content) {
Some(t) => t,
None => return Vec::new(),
};
let loader = parsers::grammar_loader();
let mut symbols = if let Some(tags_query_str) = loader.get_tags(grammar_name) {
loader
.get_compiled_query(grammar_name, "tags", &tags_query_str)
.and_then(|query| {
collect_symbols_from_tags(
&tree,
&query,
content,
support,
self.options.include_private,
)
})
.unwrap_or_default()
} else {
Vec::new()
};
if grammar_name == "rust" {
Self::merge_rust_impl_blocks(&mut symbols);
}
if grammar_name == "haskell" {
Self::dedup_haskell_functions(&mut symbols);
}
if grammar_name == "typescript" || grammar_name == "javascript" {
Self::mark_interface_implementations(&mut symbols, resolver, current_file);
}
if resolver.is_none()
&& let Some(cache) = ca_cache::symbol_cache()
{
let hash = blake3::hash(content.as_bytes());
if let Err(e) = cache.put(hash.as_bytes(), cache_ver, grammar_name, &symbols) {
tracing::debug!(
"normalize-facts: symbol cache put error for {}: {}",
current_file,
e
);
}
}
symbols
}
fn dedup_haskell_functions(symbols: &mut Vec<Symbol>) {
let mut seen: Vec<(String, SymbolKind)> = Vec::new();
let mut i = 0;
while i < symbols.len() {
let key = (symbols[i].name.clone(), symbols[i].kind);
if seen.contains(&key) {
symbols.remove(i);
} else {
seen.push(key);
i += 1;
}
}
}
fn merge_rust_impl_blocks(symbols: &mut Vec<Symbol>) {
use std::collections::HashMap;
let mut impl_methods: HashMap<String, Vec<Symbol>> = HashMap::new();
let mut impl_implements: HashMap<String, Vec<String>> = HashMap::new();
symbols.retain(|sym| {
if sym.signature.starts_with("impl ") {
impl_methods
.entry(sym.name.clone())
.or_default()
.extend(sym.children.clone());
impl_implements
.entry(sym.name.clone())
.or_default()
.extend(sym.implements.clone());
return false;
}
true
});
for sym in symbols.iter_mut() {
if matches!(
sym.kind,
normalize_languages::SymbolKind::Struct | normalize_languages::SymbolKind::Enum
) {
if let Some(methods) = impl_methods.remove(&sym.name) {
sym.children.extend(methods);
}
if let Some(impls) = impl_implements.remove(&sym.name) {
sym.implements.extend(impls);
}
}
}
for (name, methods) in impl_methods {
let impls = impl_implements.remove(&name).unwrap_or_default();
if !methods.is_empty() {
symbols.push(Symbol {
name: name.clone(),
kind: normalize_languages::SymbolKind::Module, signature: format!("impl {}", name),
docstring: None,
attributes: Vec::new(),
start_line: methods.first().map(|m| m.start_line).unwrap_or(0),
end_line: methods.last().map(|m| m.end_line).unwrap_or(0),
visibility: Visibility::Public,
children: methods,
is_interface_impl: !impls.is_empty(),
implements: impls,
});
}
}
}
fn mark_interface_implementations(
symbols: &mut [Symbol],
resolver: Option<&dyn InterfaceResolver>,
current_file: &str,
) {
use std::collections::{HashMap, HashSet};
let mut type_methods: HashMap<String, HashSet<String>> = HashMap::new();
fn collect_type_methods(
symbols: &[Symbol],
type_methods: &mut HashMap<String, HashSet<String>>,
) {
for sym in symbols {
if matches!(
sym.kind,
normalize_languages::SymbolKind::Interface
| normalize_languages::SymbolKind::Class
) {
let methods: HashSet<String> = sym
.children
.iter()
.filter(|c| {
matches!(
c.kind,
normalize_languages::SymbolKind::Method
| normalize_languages::SymbolKind::Function
)
})
.map(|c| c.name.clone())
.collect();
if !methods.is_empty() {
type_methods.insert(sym.name.clone(), methods);
}
}
collect_type_methods(&sym.children, type_methods);
}
}
collect_type_methods(symbols, &mut type_methods);
let mut cross_file_cache: HashMap<String, Option<HashSet<String>>> = HashMap::new();
fn mark_methods(
symbols: &mut [Symbol],
type_methods: &HashMap<String, HashSet<String>>,
resolver: Option<&dyn InterfaceResolver>,
current_file: &str,
cross_file_cache: &mut HashMap<String, Option<HashSet<String>>>,
) {
for sym in symbols.iter_mut() {
if !sym.implements.is_empty() {
let mut interface_methods: HashSet<String> = HashSet::new();
for parent_name in &sym.implements {
if let Some(methods) = type_methods.get(parent_name) {
interface_methods.extend(methods.clone());
} else if let Some(resolver) = resolver {
let cached = cross_file_cache
.entry(parent_name.clone())
.or_insert_with(|| {
resolver
.resolve_interface_methods(parent_name, current_file)
.map(|v| v.into_iter().collect())
});
if let Some(methods) = cached {
interface_methods.extend(methods.clone());
}
}
}
for child in &mut sym.children {
if matches!(
child.kind,
normalize_languages::SymbolKind::Method
| normalize_languages::SymbolKind::Function
) && interface_methods.contains(&child.name)
{
child.is_interface_impl = true;
}
}
}
mark_methods(
&mut sym.children,
type_methods,
resolver,
current_file,
cross_file_cache,
);
}
}
mark_methods(
symbols,
&type_methods,
resolver,
current_file,
&mut cross_file_cache,
);
}
}
fn tags_capture_to_kind(capture_name: &str) -> Option<SymbolKind> {
match capture_name {
"definition.function" => Some(SymbolKind::Function),
"definition.method" => Some(SymbolKind::Function),
"definition.class" => Some(SymbolKind::Class),
"definition.interface" => Some(SymbolKind::Interface),
"definition.module" => Some(SymbolKind::Module),
"definition.type" => Some(SymbolKind::Type),
"definition.enum" => Some(SymbolKind::Enum),
"definition.heading" => Some(SymbolKind::Heading),
"definition.macro" => Some(SymbolKind::Function),
"definition.constant" => Some(SymbolKind::Constant),
"definition.var" => Some(SymbolKind::Variable),
_ => None,
}
}
fn is_container_kind(kind: SymbolKind) -> bool {
matches!(
kind,
SymbolKind::Class
| SymbolKind::Interface
| SymbolKind::Module
| SymbolKind::Enum
| SymbolKind::Heading
)
}
struct TagDef<'tree> {
node: tree_sitter::Node<'tree>,
kind: SymbolKind,
is_method_capture: bool,
is_container: bool,
start_line: usize,
end_line: usize,
}
fn build_symbol_from_def<'tree>(
def: &TagDef<'tree>,
content: &str,
support: &dyn Language,
in_container: bool,
) -> Option<Symbol> {
let name = support.node_name(&def.node, content)?;
let tag_kind = support.refine_kind(&def.node, content, def.kind);
let kind =
if def.is_method_capture || (in_container && matches!(tag_kind, SymbolKind::Function)) {
SymbolKind::Method
} else {
tag_kind
};
let implements_info = if def.is_container {
support.extract_implements(&def.node, content)
} else {
normalize_languages::ImplementsInfo::default()
};
Some(Symbol {
name: name.to_string(),
kind,
signature: support.build_signature(&def.node, content),
docstring: support.extract_docstring(&def.node, content),
attributes: support.extract_attributes(&def.node, content),
start_line: def.node.start_position().row + 1,
end_line: def.node.end_position().row + 1,
visibility: support.get_visibility(&def.node, content),
children: Vec::new(),
is_interface_impl: implements_info.is_interface,
implements: implements_info.implements,
})
}
fn collect_symbols_from_tags<'tree>(
tree: &'tree tree_sitter::Tree,
query: &tree_sitter::Query,
content: &str,
support: &dyn Language,
include_private: bool,
) -> Option<Vec<Symbol>> {
let capture_names = query.capture_names();
let name_idx = capture_names.iter().position(|n| *n == "name")?;
let _ = name_idx;
let root = tree.root_node();
let mut qcursor = tree_sitter::QueryCursor::new();
let mut matches = qcursor.matches(query, root, content.as_bytes());
let mut defs: Vec<TagDef<'tree>> = Vec::new();
while let Some(m) = matches.next() {
let mut def_capture: Option<(tree_sitter::Node<'tree>, &str)> = None;
for capture in m.captures {
let cn = &capture_names[capture.index as usize];
if tags_capture_to_kind(cn).is_some() {
let node = capture.node;
def_capture = Some((node, cn));
}
}
let Some((def_node, capture_name)) = def_capture else {
continue;
};
let kind = match tags_capture_to_kind(capture_name) {
Some(k) => k,
None => continue,
};
let refined_kind = support.refine_kind(&def_node, content, kind);
defs.push(TagDef {
node: def_node,
kind,
is_method_capture: capture_name == "definition.method",
is_container: is_container_kind(refined_kind),
start_line: def_node.start_position().row + 1,
end_line: def_node.end_position().row + 1,
});
}
if defs.is_empty() {
return None;
}
defs.sort_by(|a, b| {
a.start_line
.cmp(&b.start_line)
.then(b.end_line.cmp(&a.end_line))
});
defs.dedup_by(|b, a| {
a.node.start_byte() == b.node.start_byte() && a.node.end_byte() == b.node.end_byte()
});
let container_idxs: Vec<usize> = (0..defs.len()).filter(|&i| defs[i].is_container).collect();
let mut symbols: Vec<Option<Symbol>> = Vec::with_capacity(defs.len());
let mut parent_of: Vec<Option<usize>> = Vec::with_capacity(defs.len());
for i in 0..defs.len() {
let def = &defs[i];
let enclosing_ci = container_idxs
.iter()
.filter(|&&ci| ci != i)
.rev()
.find(|&&ci| {
let c = &defs[ci];
c.start_line <= def.start_line && c.end_line >= def.end_line
});
let in_container = enclosing_ci.is_some();
let Some(mut sym) = build_symbol_from_def(def, content, support, in_container) else {
symbols.push(None);
parent_of.push(None);
continue;
};
if !include_private
&& matches!(
sym.visibility,
Visibility::Private | Visibility::Protected | Visibility::Internal
)
{
symbols.push(None);
parent_of.push(None);
continue;
}
if def.is_container {
sym.children.clear();
}
symbols.push(Some(sym));
parent_of.push(enclosing_ci.copied());
}
for i in (0..symbols.len()).rev() {
if let Some(pi) = parent_of[i]
&& symbols[pi].is_some()
&& symbols[i].is_some()
{
let child = symbols[i].take().unwrap();
symbols[pi].as_mut().unwrap().children.push(child);
}
}
let mut top_level: Vec<Symbol> = Vec::new();
for sym_opt in &mut symbols {
if let Some(mut sym) = sym_opt.take() {
sym.children.reverse();
reverse_children_recursive(&mut sym.children);
top_level.push(sym);
}
}
if top_level.is_empty() {
None
} else {
Some(top_level)
}
}
fn reverse_children_recursive(children: &mut [Symbol]) {
for child in children.iter_mut() {
child.children.reverse();
reverse_children_recursive(&mut child.children);
}
}
pub fn compute_complexity(
node: &tree_sitter::Node,
support: &dyn Language,
source: &[u8],
) -> usize {
let grammar_name = support.grammar_name();
let loader = parsers::grammar_loader();
if let Some(scm) = loader.get_complexity(grammar_name)
&& let Some(query) = loader.get_compiled_query(grammar_name, "complexity", &scm)
{
return count_complexity_with_query(node, source, &query);
}
1
}
fn count_complexity_with_query(
node: &tree_sitter::Node,
source: &[u8],
query: &tree_sitter::Query,
) -> usize {
let complexity_idx = query
.capture_names()
.iter()
.position(|n| *n == "complexity");
let Some(complexity_idx) = complexity_idx else {
return 1;
};
let mut qcursor = tree_sitter::QueryCursor::new();
qcursor.set_byte_range(node.byte_range());
let mut complexity = 1usize;
let mut matches = qcursor.matches(query, *node, source);
while let Some(m) = matches.next() {
for capture in m.captures {
if capture.index as usize == complexity_idx {
complexity += 1;
}
}
}
complexity
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn test_extract_python() {
let extractor = Extractor::new();
let content = r#"
def foo(x: int) -> str:
"""Convert int to string."""
return str(x)
class Bar:
"""A bar class."""
def method(self):
pass
"#;
let result = extractor.extract(&PathBuf::from("test.py"), content);
assert_eq!(result.symbols.len(), 2);
let foo = &result.symbols[0];
assert_eq!(foo.name, "foo");
assert!(foo.signature.contains("def foo"));
assert_eq!(foo.docstring.as_deref(), Some("Convert int to string."));
let bar = &result.symbols[1];
assert_eq!(bar.name, "Bar");
assert_eq!(bar.children.len(), 1);
assert_eq!(bar.children[0].name, "method");
}
#[test]
fn test_extract_rust() {
let extractor = Extractor::new();
let content = r#"
/// A simple struct
pub struct Foo {
x: i32,
}
impl Foo {
/// Create a new Foo
pub fn new(x: i32) -> Self {
Self { x }
}
}
"#;
let result = extractor.extract(&PathBuf::from("test.rs"), content);
let foo = result.symbols.iter().find(|s| s.name == "Foo").unwrap();
assert!(foo.signature.contains("pub struct Foo"));
assert_eq!(foo.children.len(), 1);
assert_eq!(foo.children[0].name, "new");
}
#[test]
fn test_include_private() {
let extractor = Extractor::with_options(ExtractOptions {
include_private: true,
});
let content = r#"
fn private_fn() {}
pub fn public_fn() {}
"#;
let result = extractor.extract(&PathBuf::from("test.rs"), content);
let names: Vec<_> = result.symbols.iter().map(|s| s.name.as_str()).collect();
assert!(names.contains(&"private_fn"));
assert!(names.contains(&"public_fn"));
}
#[test]
fn test_typescript_interface_impl_detection() {
let extractor = Extractor::new();
let content = r#"
interface IFoo {
bar(): void;
baz(): number;
}
class Foo implements IFoo {
bar() {}
baz() { return 1; }
other() {}
}
"#;
let result = extractor.extract(&PathBuf::from("test.ts"), content);
assert_eq!(result.symbols.len(), 2);
let interface = &result.symbols[0];
assert_eq!(interface.name, "IFoo");
assert_eq!(interface.children.len(), 2);
let class = &result.symbols[1];
assert_eq!(class.name, "Foo");
assert_eq!(class.implements, vec!["IFoo"]);
assert_eq!(class.children.len(), 3);
let bar = class.children.iter().find(|c| c.name == "bar").unwrap();
let baz = class.children.iter().find(|c| c.name == "baz").unwrap();
let other = class.children.iter().find(|c| c.name == "other").unwrap();
assert!(
bar.is_interface_impl,
"bar should be marked as interface impl"
);
assert!(
baz.is_interface_impl,
"baz should be marked as interface impl"
);
assert!(
!other.is_interface_impl,
"other should NOT be marked as interface impl"
);
}
#[test]
fn test_cross_file_interface_impl_with_mock_resolver() {
struct MockResolver;
impl InterfaceResolver for MockResolver {
fn resolve_interface_methods(
&self,
name: &str,
_current_file: &str,
) -> Option<Vec<String>> {
if name == "IRemote" {
Some(vec![
"remoteMethod".to_string(),
"anotherRemote".to_string(),
])
} else {
None
}
}
}
let extractor = Extractor::new();
let content = r#"
class Foo implements IRemote {
remoteMethod() {}
anotherRemote() { return 1; }
localMethod() {}
}
"#;
let resolver = MockResolver;
let result =
extractor.extract_with_resolver(&PathBuf::from("test.ts"), content, Some(&resolver));
assert_eq!(result.symbols.len(), 1);
let class = &result.symbols[0];
assert_eq!(class.name, "Foo");
assert_eq!(class.implements, vec!["IRemote"]);
assert_eq!(class.children.len(), 3);
let remote_method = class
.children
.iter()
.find(|c| c.name == "remoteMethod")
.unwrap();
let another_remote = class
.children
.iter()
.find(|c| c.name == "anotherRemote")
.unwrap();
let local_method = class
.children
.iter()
.find(|c| c.name == "localMethod")
.unwrap();
assert!(
remote_method.is_interface_impl,
"remoteMethod should be marked as interface impl"
);
assert!(
another_remote.is_interface_impl,
"anotherRemote should be marked as interface impl"
);
assert!(
!local_method.is_interface_impl,
"localMethod should NOT be marked as interface impl"
);
}
#[test]
fn test_cross_file_resolver_not_found() {
struct NotFoundResolver;
impl InterfaceResolver for NotFoundResolver {
fn resolve_interface_methods(
&self,
_name: &str,
_current_file: &str,
) -> Option<Vec<String>> {
None
}
}
let extractor = Extractor::new();
let content = r#"
class Foo implements IUnknown {
someMethod() {}
}
"#;
let resolver = NotFoundResolver;
let result =
extractor.extract_with_resolver(&PathBuf::from("test.ts"), content, Some(&resolver));
let class = &result.symbols[0];
let some_method = class
.children
.iter()
.find(|c| c.name == "someMethod")
.unwrap();
assert!(
!some_method.is_interface_impl,
"someMethod should NOT be marked when interface not found"
);
}
fn extract_implements(file: &str, code: &str) -> Option<Vec<(String, Vec<String>)>> {
let path = PathBuf::from(file);
let support = normalize_languages::support_for_path(&path)?;
let grammar = support.grammar_name();
parsers::parse_with_grammar(grammar, code)?;
let extractor = Extractor::new();
let result = extractor.extract(&path, code);
fn collect(symbols: &[normalize_languages::Symbol]) -> Vec<(String, Vec<String>)> {
let mut out = Vec::new();
for s in symbols {
if !s.implements.is_empty() {
out.push((s.name.clone(), s.implements.clone()));
}
out.extend(collect(&s.children));
}
out
}
Some(collect(&result.symbols))
}
#[test]
fn test_implements_python() {
let Some(results) = extract_implements("test.py", "class Foo(Bar, Baz):\n pass\n")
else {
return;
};
assert_eq!(
results,
vec![("Foo".into(), vec!["Bar".into(), "Baz".into()])]
);
}
#[test]
fn test_implements_rust() {
let Some(results) = extract_implements(
"test.rs",
"pub trait MyTrait {}\npub struct Foo;\nimpl MyTrait for Foo {}\n",
) else {
return;
};
let impl_sym = results.iter().find(|(n, _)| n == "Foo").unwrap();
assert_eq!(impl_sym.1, vec!["MyTrait"]);
}
#[test]
fn test_implements_java() {
let Some(results) = extract_implements(
"test.java",
"class Foo extends Bar implements Baz, Qux {}\n",
) else {
return;
};
assert_eq!(
results,
vec![("Foo".into(), vec!["Bar".into(), "Baz".into(), "Qux".into()])]
);
}
#[test]
fn test_implements_javascript() {
let Some(results) = extract_implements("test.js", "class Foo extends Bar {}\n") else {
return;
};
assert_eq!(results, vec![("Foo".into(), vec!["Bar".into()])]);
}
#[test]
fn test_implements_typescript() {
let Some(results) =
extract_implements("test.ts", "class Foo extends Bar implements Baz {}\n")
else {
return;
};
assert_eq!(
results,
vec![("Foo".into(), vec!["Bar".into(), "Baz".into()])]
);
}
#[test]
fn test_implements_cpp() {
let Some(results) = extract_implements(
"test.cpp",
"class Derived : public Base, public Other {};\n",
) else {
return;
};
assert_eq!(
results,
vec![("Derived".into(), vec!["Base".into(), "Other".into()])]
);
}
#[test]
fn test_implements_scala() {
let Some(results) = extract_implements("test.scala", "class Foo extends Bar with Baz {}\n")
else {
return;
};
assert_eq!(
results,
vec![("Foo".into(), vec!["Bar".into(), "Baz".into()])]
);
}
#[test]
fn test_implements_ruby() {
let Some(results) = extract_implements("test.rb", "class Foo < Bar\nend\n") else {
return;
};
assert_eq!(results, vec![("Foo".into(), vec!["Bar".into()])]);
}
#[test]
fn test_implements_dart() {
let Some(results) =
extract_implements("test.dart", "class Foo extends Bar implements Baz {}\n")
else {
return;
};
assert_eq!(
results,
vec![("Foo".into(), vec!["Bar".into(), "Baz".into()])]
);
}
#[test]
fn test_implements_d() {
let Some(results) = extract_implements("test.d", "class Derived : Base, IFoo {}\n") else {
return;
};
assert_eq!(
results,
vec![("Derived".into(), vec!["Base".into(), "IFoo".into()])]
);
}
#[test]
fn test_implements_csharp() {
let Some(results) = extract_implements("test.cs", "class Foo : Bar, IBaz {}\n") else {
return;
};
assert_eq!(
results,
vec![("Foo".into(), vec!["Bar".into(), "IBaz".into()])]
);
}
#[test]
fn test_implements_kotlin() {
let Some(results) = extract_implements("test.kt", "class Foo : Bar(), IBaz {}\n") else {
return;
};
assert_eq!(
results,
vec![("Foo".into(), vec!["Bar".into(), "IBaz".into()])]
);
}
#[test]
fn test_implements_swift() {
let Some(results) = extract_implements("test.swift", "class Foo: Bar, Proto {}\n") else {
return;
};
assert_eq!(
results,
vec![("Foo".into(), vec!["Bar".into(), "Proto".into()])]
);
}
#[test]
fn test_implements_php() {
let Some(results) = extract_implements(
"test.php",
"<?php\nclass Foo extends Bar implements Baz {}\n",
) else {
return;
};
assert_eq!(
results,
vec![("Foo".into(), vec!["Bar".into(), "Baz".into()])]
);
}
#[test]
fn test_implements_objc() {
let Some(results) = extract_implements("test.mm", "@interface Foo : Bar <Proto>\n@end\n")
else {
return;
};
assert_eq!(
results,
vec![("Foo".into(), vec!["Bar".into(), "Proto".into()])]
);
}
#[test]
fn test_implements_matlab() {
let Some(results) = extract_implements("test.m", "classdef Foo < Bar & Baz\nend\n") else {
return;
};
if results.is_empty() {
return;
}
assert_eq!(
results,
vec![("Foo".into(), vec!["Bar".into(), "Baz".into()])]
);
}
#[test]
fn test_implements_graphql() {
let Some(results) = extract_implements(
"test.graphql",
"type Foo implements Bar & Baz { id: ID! }\n",
) else {
return;
};
assert_eq!(
results,
vec![("Foo".into(), vec!["Bar".into(), "Baz".into()])]
);
}
#[test]
fn test_implements_haskell() {
let Some(results) =
extract_implements("test.hs", "instance MyClass Foo where\n doStuff f = y f\n")
else {
return;
};
assert_eq!(results, vec![("MyClass".into(), vec!["MyClass".into()])]);
}
#[test]
fn test_go_extract() {
if parsers::parse_with_grammar("go", "package x").is_none() {
return; }
let extractor = Extractor::new();
let content = "package main\n\nfunc helper() {}\n\ntype MyStruct struct {\n Field int\n}\n\nfunc (m *MyStruct) Method() {}\n\ntype MyInterface interface {\n Required()\n}\n";
let result = extractor.extract(&std::path::PathBuf::from("test.go"), content);
let names: Vec<_> = result.symbols.iter().map(|s| s.name.as_str()).collect();
assert!(names.contains(&"helper"), "Should have function helper");
assert!(names.contains(&"MyStruct"), "Should have struct MyStruct");
assert!(
names.contains(&"MyInterface"),
"Should have interface MyInterface"
);
}
}