use tree_sitter::{Node, Parser};
use crate::error::{CodegraphError, Result};
use crate::graph::types::{
Binding, BindingKind, ByteSpan, FileFacts, RefRole, Reference, Scope, ScopeId, ScopeKind,
Symbol, SymbolKind, TypeRefContext, Visibility,
};
use crate::lang::Language;
use crate::symbol::Descriptor;
use super::{
ExtractCtx, Extractor, MIN_REF_LEN, attach_reference_scopes, collect_call_references,
definition_bindings, field_text, import_bindings, innermost_scope, make_symbol, node_span,
node_text, one_line_signature, push_binding, push_import_ref, push_ref, push_scope,
push_type_ref, simple_type_name,
};
const CALL_QUERY: &str = r#"
[
(call_expression function: (identifier) @callee)
(call_expression function: (member_expression object: (_) @qualifier property: (identifier) @callee))
]
"#;
pub struct DartExtractor;
impl Extractor for DartExtractor {
fn lang(&self) -> Language {
Language::Dart
}
fn extract(&self, source: &str, file: &str) -> Result<FileFacts> {
let ts_language = crate::grammar::dart();
let mut parser = Parser::new();
parser
.set_language(&ts_language)
.map_err(|_| CodegraphError::Parse {
path: file.to_owned(),
})?;
let tree = parser
.parse(source, None)
.ok_or_else(|| CodegraphError::Parse {
path: file.to_owned(),
})?;
let root = tree.root_node();
let bytes = source.as_bytes();
let namespaces = dart_namespaces(file);
let ctx = ExtractCtx {
bytes,
file,
lang: Language::Dart,
};
let defs = collect_symbols(&root, &ctx, &namespaces);
let def_bindings = definition_bindings(&defs);
let mut symbols = defs;
let mod_sym = super::module_symbol(Language::Dart, &namespaces, file, source.len());
let module_id = mod_sym.id.to_scip_string();
symbols.push(mod_sym);
let mut references =
collect_call_references(&root, &ts_language, CALL_QUERY, Language::Dart, bytes, file)?;
collect_inheritance(&root, bytes, file, &mut references);
collect_imports(&root, bytes, file, &mut references, &module_id);
collect_type_references(&root, bytes, file, &mut references);
collect_read_references(&root, bytes, file, &mut references);
collect_write_references(&root, bytes, file, &mut references);
let scopes = collect_scopes(&root, source.len());
attach_reference_scopes(&mut references, &scopes);
let mut bindings = collect_bindings(&root, bytes, &scopes);
bindings.extend(def_bindings);
bindings.extend(import_bindings(&references, &scopes));
Ok(FileFacts {
file: file.to_owned(),
lang: Language::Dart.as_str().to_owned(),
symbols,
references,
scopes,
bindings,
ffi_exports: Vec::new(),
})
}
}
fn dart_namespaces(file: &str) -> Vec<String> {
let p = file.strip_suffix(".dart").unwrap_or(file);
let p = p
.strip_prefix("lib/")
.or_else(|| p.strip_prefix("src/"))
.unwrap_or(p);
p.split('/')
.filter(|s| !s.is_empty())
.map(str::to_owned)
.collect()
}
fn collect_symbols(root: &Node, ctx: &ExtractCtx, namespaces: &[String]) -> Vec<Symbol> {
let ns_descriptors: Vec<Descriptor> = namespaces
.iter()
.cloned()
.map(Descriptor::Namespace)
.collect();
let mut out = Vec::new();
collect_top_level(root, ctx, &ns_descriptors, &mut out);
out
}
fn collect_top_level(node: &Node, ctx: &ExtractCtx, prefix: &[Descriptor], out: &mut Vec<Symbol>) {
for child in node.children(&mut node.walk()) {
match child.kind() {
"class_declaration" => {
collect_class(&child, ctx, prefix, SymbolKind::Class, out);
}
"mixin_declaration" => {
collect_mixin(&child, ctx, prefix, out);
}
"enum_declaration" => {
collect_enum(&child, ctx, prefix, out);
}
"extension_declaration" => {
collect_extension(&child, ctx, prefix, out);
}
"type_alias" => {
collect_type_alias(&child, ctx, prefix, out);
}
"function_declaration" => {
collect_top_function(&child, ctx, prefix, out);
}
"top_level_variable_declaration" => {
collect_top_level_vars(&child, ctx, prefix, out);
}
_ => {}
}
}
}
fn collect_class(
node: &Node,
ctx: &ExtractCtx,
prefix: &[Descriptor],
kind: SymbolKind,
out: &mut Vec<Symbol>,
) {
let Some(name) = field_text(node, "name", ctx.bytes) else {
return;
};
let mut descriptors = prefix.to_vec();
descriptors.push(Descriptor::Type(name.clone()));
out.push(make_symbol(
ctx,
node,
name,
kind,
Visibility::Unknown,
descriptors.clone(),
one_line_signature(node_text(node, ctx.bytes), &['{', ';']),
));
if let Some(body) = node.child_by_field_name("body") {
collect_class_members(&body, ctx, &descriptors, out);
}
}
fn collect_mixin(node: &Node, ctx: &ExtractCtx, prefix: &[Descriptor], out: &mut Vec<Symbol>) {
let Some(name) = field_text(node, "name", ctx.bytes) else {
return;
};
let mut descriptors = prefix.to_vec();
descriptors.push(Descriptor::Type(name.clone()));
out.push(make_symbol(
ctx,
node,
name,
SymbolKind::Trait,
Visibility::Unknown,
descriptors.clone(),
one_line_signature(node_text(node, ctx.bytes), &['{', ';']),
));
if let Some(body) = node.child_by_field_name("body") {
collect_class_members(&body, ctx, &descriptors, out);
}
}
fn collect_enum(node: &Node, ctx: &ExtractCtx, prefix: &[Descriptor], out: &mut Vec<Symbol>) {
let Some(name) = field_text(node, "name", ctx.bytes) else {
return;
};
let mut descriptors = prefix.to_vec();
descriptors.push(Descriptor::Type(name.clone()));
out.push(make_symbol(
ctx,
node,
name,
SymbolKind::Enum,
Visibility::Unknown,
descriptors.clone(),
one_line_signature(node_text(node, ctx.bytes), &['{', ';']),
));
if let Some(body) = node.child_by_field_name("body") {
for member in body.children(&mut body.walk()) {
if member.kind() == "enum_constant" {
if let Some(const_name) = field_text(&member, "name", ctx.bytes) {
let mut const_desc = descriptors.clone();
const_desc.push(Descriptor::Term(const_name.clone()));
out.push(make_symbol(
ctx,
&member,
const_name,
SymbolKind::Const,
Visibility::Unknown,
const_desc,
one_line_signature(node_text(&member, ctx.bytes), &['{', ';', ',']),
));
}
}
}
}
}
fn collect_extension(node: &Node, ctx: &ExtractCtx, prefix: &[Descriptor], out: &mut Vec<Symbol>) {
let Some(name) = field_text(node, "name", ctx.bytes) else {
return;
};
let mut descriptors = prefix.to_vec();
descriptors.push(Descriptor::Type(name.clone()));
out.push(make_symbol(
ctx,
node,
name,
SymbolKind::Class,
Visibility::Unknown,
descriptors.clone(),
one_line_signature(node_text(node, ctx.bytes), &['{', ';']),
));
if let Some(body) = node.child_by_field_name("body") {
collect_class_members(&body, ctx, &descriptors, out);
}
}
fn collect_type_alias(node: &Node, ctx: &ExtractCtx, prefix: &[Descriptor], out: &mut Vec<Symbol>) {
let name_node = node
.children(&mut node.walk())
.find(|c| c.kind() == "type_identifier");
let Some(name_node) = name_node else { return };
let name = node_text(&name_node, ctx.bytes).to_owned();
let mut descriptors = prefix.to_vec();
descriptors.push(Descriptor::Type(name.clone()));
out.push(make_symbol(
ctx,
node,
name,
SymbolKind::TypeAlias,
Visibility::Unknown,
descriptors,
one_line_signature(node_text(node, ctx.bytes), &['{', ';']),
));
}
fn collect_top_function(
node: &Node,
ctx: &ExtractCtx,
prefix: &[Descriptor],
out: &mut Vec<Symbol>,
) {
let name_opt = node
.child_by_field_name("signature")
.and_then(|sig| sig.child_by_field_name("name"))
.map(|n| node_text(&n, ctx.bytes).to_owned());
let Some(name) = name_opt else { return };
let mut descriptors = prefix.to_vec();
descriptors.push(Descriptor::Method {
name: name.clone(),
disambiguator: String::new(),
});
out.push(make_symbol(
ctx,
node,
name,
SymbolKind::Function,
Visibility::Unknown,
descriptors,
one_line_signature(node_text(node, ctx.bytes), &['{', ';', '=']),
));
}
fn collect_top_level_vars(
node: &Node,
ctx: &ExtractCtx,
prefix: &[Descriptor],
out: &mut Vec<Symbol>,
) {
for child in node.children(&mut node.walk()) {
if child.kind() == "initialized_identifier_list" {
emit_initialized_identifiers(&child, node, ctx, prefix, out);
}
}
}
fn emit_initialized_identifiers(
list_node: &Node,
decl_node: &Node,
ctx: &ExtractCtx,
prefix: &[Descriptor],
out: &mut Vec<Symbol>,
) {
for item in list_node.children(&mut list_node.walk()) {
if item.kind() == "initialized_identifier" {
if let Some(name) = field_text(&item, "name", ctx.bytes) {
let mut descriptors = prefix.to_vec();
descriptors.push(Descriptor::Term(name.clone()));
out.push(make_symbol(
ctx,
decl_node,
name,
SymbolKind::Static,
Visibility::Unknown,
descriptors,
one_line_signature(node_text(decl_node, ctx.bytes), &['{', ';']),
));
}
}
}
}
fn collect_class_members(
body: &Node,
ctx: &ExtractCtx,
type_prefix: &[Descriptor],
out: &mut Vec<Symbol>,
) {
for wrapper in body.children(&mut body.walk()) {
if wrapper.kind() != "class_member" {
continue;
}
for member in wrapper.children(&mut wrapper.walk()) {
match member.kind() {
"method_declaration" => {
let name_opt = member
.child_by_field_name("signature")
.and_then(|ms| {
ms.children(&mut ms.walk())
.find(|c| c.kind() == "function_signature")
})
.and_then(|fs| fs.child_by_field_name("name"))
.map(|n| node_text(&n, ctx.bytes).to_owned())
.or_else(|| {
member
.child_by_field_name("signature")
.and_then(|ms| ms.child_by_field_name("name"))
.map(|n| node_text(&n, ctx.bytes).to_owned())
});
if let Some(name) = name_opt {
emit_method(name, &member, ctx, type_prefix, out);
}
}
"declaration" => {
let has_constructor = member
.children(&mut member.walk())
.any(|c| c.kind() == "constructor_signature");
if has_constructor {
for child in member.children(&mut member.walk()) {
if child.kind() == "constructor_signature" {
if let Some(name) = field_text(&child, "name", ctx.bytes) {
emit_method(name, &child, ctx, type_prefix, out);
}
}
}
} else {
for child in member.children(&mut member.walk()) {
if child.kind() == "initialized_identifier_list" {
emit_initialized_identifiers(
&child,
&member,
ctx,
type_prefix,
out,
);
}
}
}
}
_ => {}
}
}
}
}
fn emit_method(
name: String,
node: &Node,
ctx: &ExtractCtx,
prefix: &[Descriptor],
out: &mut Vec<Symbol>,
) {
let mut descriptors = prefix.to_vec();
descriptors.push(Descriptor::Method {
name: name.clone(),
disambiguator: String::new(),
});
out.push(make_symbol(
ctx,
node,
name,
SymbolKind::Method,
Visibility::Unknown,
descriptors,
one_line_signature(node_text(node, ctx.bytes), &['{', ';', '=']),
));
}
fn collect_inheritance(node: &Node, bytes: &[u8], file: &str, out: &mut Vec<Reference>) {
match node.kind() {
"class_declaration" => {
if let Some(superclass) = node.child_by_field_name("superclass") {
emit_type_identifier_refs(&superclass, bytes, file, RefRole::IsImplementation, out);
}
if let Some(interfaces) = node.child_by_field_name("interfaces") {
emit_type_identifier_refs(&interfaces, bytes, file, RefRole::IsImplementation, out);
}
}
"mixin_declaration" => {
let mut saw_on = false;
for child in node.children(&mut node.walk()) {
match child.kind() {
"on" => saw_on = true,
"type" if saw_on => {
emit_type_identifier_refs(
&child,
bytes,
file,
RefRole::IsImplementation,
out,
);
}
"class_body" => break,
_ => {}
}
}
if let Some(interfaces) = node.child_by_field_name("interfaces") {
emit_type_identifier_refs(&interfaces, bytes, file, RefRole::IsImplementation, out);
}
}
_ => {}
}
for child in node.children(&mut node.walk()) {
collect_inheritance(&child, bytes, file, out);
}
}
fn emit_type_identifier_refs(
node: &Node,
bytes: &[u8],
file: &str,
role: RefRole,
out: &mut Vec<Reference>,
) {
if node.kind() == "type_identifier" {
push_ref(out, node_text(node, bytes), node, file, role);
return;
}
for child in node.children(&mut node.walk()) {
emit_type_identifier_refs(&child, bytes, file, role, out);
}
}
fn collect_imports(
node: &Node,
bytes: &[u8],
file: &str,
out: &mut Vec<Reference>,
module_id: &str,
) {
if node.kind() == "import_or_export" {
collect_import_or_export(node, bytes, file, out, module_id);
return;
}
for child in node.children(&mut node.walk()) {
collect_imports(&child, bytes, file, out, module_id);
}
}
fn collect_import_or_export(
node: &Node,
bytes: &[u8],
file: &str,
out: &mut Vec<Reference>,
module_id: &str,
) {
for child in node.children(&mut node.walk()) {
if child.kind() == "library_import" {
collect_library_import(&child, bytes, file, out, module_id);
}
}
}
fn collect_library_import(
node: &Node,
bytes: &[u8],
file: &str,
out: &mut Vec<Reference>,
module_id: &str,
) {
for child in node.children(&mut node.walk()) {
if child.kind() == "import_specification" {
collect_import_specification(&child, bytes, file, out, module_id);
}
}
}
fn collect_import_specification(
node: &Node,
bytes: &[u8],
file: &str,
out: &mut Vec<Reference>,
module_id: &str,
) {
let uri_text = extract_uri_text(node, bytes);
let Some(from_path) = uri_text else { return };
let mut show_names: Vec<(String, Node)> = Vec::new();
let mut alias_node: Option<Node> = None;
let mut has_show = false;
for child in node.children(&mut node.walk()) {
match child.kind() {
"combinator" => {
let keyword = child
.children(&mut child.walk())
.find(|c| matches!(c.kind(), "show" | "hide"))
.map(|c| c.kind());
if keyword == Some("show") {
has_show = true;
for id in child.children(&mut child.walk()) {
if id.kind() == "identifier" {
let name = node_text(&id, bytes).to_owned();
show_names.push((name, id));
}
}
}
}
"identifier" => {
alias_node = Some(child);
}
_ => {}
}
}
if has_show {
for (name, id_node) in &show_names {
push_import_ref(out, name, id_node, file, module_id, &from_path);
}
} else if let Some(alias) = alias_node {
let name = node_text(&alias, bytes);
push_import_ref(out, name, &alias, file, module_id, &from_path);
}
}
fn extract_uri_text(node: &Node, bytes: &[u8]) -> Option<String> {
let uri_field = node.child_by_field_name("uri")?;
let raw = find_string_literal(&uri_field, bytes)?;
let stripped = raw
.strip_prefix('\'')
.and_then(|s| s.strip_suffix('\''))
.or_else(|| raw.strip_prefix('"').and_then(|s| s.strip_suffix('"')))
.unwrap_or(raw);
Some(stripped.to_owned())
}
fn find_string_literal<'a>(node: &Node, bytes: &'a [u8]) -> Option<&'a str> {
if node.kind() == "string_literal" {
return Some(node_text(node, bytes));
}
for child in node.children(&mut node.walk()) {
if let Some(s) = find_string_literal(&child, bytes) {
return Some(s);
}
}
None
}
fn collect_type_references(node: &Node, bytes: &[u8], file: &str, out: &mut Vec<Reference>) {
match node.kind() {
"function_declaration" => {
if let Some(sig) = node.child_by_field_name("signature") {
if let Some(ret) = sig.child_by_field_name("return_type") {
type_leaf(&ret, bytes, file, TypeRefContext::ReturnType, out);
}
}
}
"method_declaration" => {
if let Some(sig) = node.child_by_field_name("signature") {
let fs = sig
.children(&mut sig.walk())
.find(|c| c.kind() == "function_signature");
if let Some(fs) = fs {
if let Some(ret) = fs.child_by_field_name("return_type") {
type_leaf(&ret, bytes, file, TypeRefContext::ReturnType, out);
}
}
}
}
"formal_parameter" => {
for child in node.children(&mut node.walk()) {
if child.kind() == "type" {
type_leaf(&child, bytes, file, TypeRefContext::ParameterType, out);
break;
}
}
}
"top_level_variable_declaration" | "declaration" => {
for child in node.children(&mut node.walk()) {
if child.kind() == "type" {
type_leaf(&child, bytes, file, TypeRefContext::Field, out);
break;
}
}
}
_ => {}
}
for child in node.children(&mut node.walk()) {
collect_type_references(&child, bytes, file, out);
}
}
fn type_leaf(node: &Node, bytes: &[u8], file: &str, ctx: TypeRefContext, out: &mut Vec<Reference>) {
match node.kind() {
"void_type" => {}
"type_identifier" => {
let name = node_text(node, bytes);
if !matches!(
name,
"int" | "double" | "num" | "bool" | "String" | "Object" | "dynamic" | "Never"
) {
push_type_ref(out, name, node, file, ctx);
}
}
"type" => {
for child in node.named_children(&mut node.walk()) {
type_leaf(&child, bytes, file, ctx, out);
}
}
_ => {
let name = simple_type_name(node_text(node, bytes), ".");
if !name.is_empty() {
push_type_ref(out, name, node, file, ctx);
}
}
}
}
fn is_bare_assignable_target(node: &Node) -> bool {
let Some(assignable) = node.parent() else {
return false;
};
if assignable.kind() != "assignable_expression" {
return false;
}
let Some(assignment) = assignable.parent() else {
return false;
};
if assignment.kind() != "assignment_expression" {
return false;
}
if assignment.child_by_field_name("left").as_ref() != Some(&assignable) {
return false;
}
let id_count = assignable
.children(&mut assignable.walk())
.filter(|c| c.kind() == "identifier")
.count();
id_count == 1
}
fn is_non_read_position(node: &Node) -> bool {
let parent = match node.parent() {
Some(p) => p,
None => return true, };
match parent.kind() {
"call_expression" => parent.child_by_field_name("function").as_ref() == Some(node),
"function_signature" | "getter_signature" | "setter_signature" => {
parent.child_by_field_name("name").as_ref() == Some(node)
}
"constructor_signature" => parent.child_by_field_name("name").as_ref() == Some(node),
"class_declaration"
| "mixin_declaration"
| "enum_declaration"
| "extension_declaration"
| "extension_type_declaration" => parent.child_by_field_name("name").as_ref() == Some(node),
"typed_identifier" => parent.child_by_field_name("name").as_ref() == Some(node),
"initialized_identifier" => parent.child_by_field_name("name").as_ref() == Some(node),
"import_specification" | "combinator" => true,
"assignable_expression" => is_bare_assignable_target(node),
"member_expression"
| "null_aware_member_expression"
| "cascade_member_expression"
| "cascade_null_aware_member_expression" => {
parent.child_by_field_name("property").as_ref() == Some(node)
}
_ => false,
}
}
fn collect_read_references(node: &Node, bytes: &[u8], file: &str, out: &mut Vec<Reference>) {
if node.kind() == "identifier" {
let name = node_text(node, bytes);
if name.len() >= MIN_REF_LEN && !is_non_read_position(node) {
push_ref(out, name, node, file, RefRole::Read);
}
return;
}
for child in node.children(&mut node.walk()) {
collect_read_references(&child, bytes, file, out);
}
}
fn collect_write_references(node: &Node, bytes: &[u8], file: &str, out: &mut Vec<Reference>) {
if node.kind() == "assignment_expression" {
if let Some(lhs) = node.child_by_field_name("left") {
match lhs.kind() {
"assignable_expression" => {
let id_children: Vec<_> = lhs
.children(&mut lhs.walk())
.filter(|c| c.kind() == "identifier")
.collect();
if let [id_node] = id_children.as_slice() {
let name = node_text(id_node, bytes);
if name.len() >= MIN_REF_LEN {
push_ref(out, name, id_node, file, RefRole::Write);
}
}
}
"identifier" => {
let name = node_text(&lhs, bytes);
if name.len() >= MIN_REF_LEN {
push_ref(out, name, &lhs, file, RefRole::Write);
}
}
_ => {}
}
}
}
for child in node.children(&mut node.walk()) {
collect_write_references(&child, bytes, file, out);
}
}
fn collect_scopes(root: &Node, source_len: usize) -> Vec<Scope> {
let mut scopes = Vec::new();
push_scope(
&mut scopes,
None,
ByteSpan {
start: 0,
end: source_len,
},
ScopeKind::Module,
);
for child in root.children(&mut root.walk()) {
scope_dfs(&child, 0, &mut scopes);
}
scopes
}
fn scope_dfs(node: &Node, parent_id: ScopeId, scopes: &mut Vec<Scope>) {
match node.kind() {
"class_declaration"
| "mixin_declaration"
| "enum_declaration"
| "extension_declaration" => {
let type_id = push_scope(scopes, Some(parent_id), node_span(node), ScopeKind::Type);
if let Some(body) = node.child_by_field_name("body") {
for child in body.children(&mut body.walk()) {
scope_dfs(&child, type_id, scopes);
}
}
}
"function_declaration" | "method_declaration" => {
let fn_id = push_scope(
scopes,
Some(parent_id),
node_span(node),
ScopeKind::Function,
);
if let Some(body) = node.child_by_field_name("body") {
for child in body.children(&mut body.walk()) {
scope_dfs(&child, fn_id, scopes);
}
}
}
"block" => {
let block_id = push_scope(scopes, Some(parent_id), node_span(node), ScopeKind::Block);
for child in node.children(&mut node.walk()) {
scope_dfs(&child, block_id, scopes);
}
}
_ => {
for child in node.children(&mut node.walk()) {
scope_dfs(&child, parent_id, scopes);
}
}
}
}
fn collect_bindings(root: &Node, bytes: &[u8], scopes: &[Scope]) -> Vec<Binding> {
let mut out = Vec::new();
collect_bindings_dfs(root, bytes, scopes, &mut out);
out
}
fn collect_bindings_dfs(node: &Node, bytes: &[u8], scopes: &[Scope], out: &mut Vec<Binding>) {
match node.kind() {
"function_declaration" | "method_declaration" => {
let sig = node.child_by_field_name("signature");
let fs = sig.as_ref().and_then(|s| {
s.children(&mut s.walk())
.find(|c| c.kind() == "function_signature")
});
let params_node = fs
.as_ref()
.and_then(|f| f.child_by_field_name("parameters"))
.or_else(|| {
sig.as_ref()
.and_then(|s| s.child_by_field_name("parameters"))
});
if let Some(params) = params_node {
collect_params(¶ms, bytes, scopes, out);
}
}
"local_variable_declaration" => {
for child in node.children(&mut node.walk()) {
if child.kind() == "initialized_identifier_list" {
for item in child.children(&mut child.walk()) {
if item.kind() == "initialized_identifier" {
if let Some(name) = field_text(&item, "name", bytes) {
let intro = item.start_byte();
if name.len() >= MIN_REF_LEN
&& innermost_scope(intro, scopes) != Some(0)
{
push_binding(out, name, intro, BindingKind::Local, scopes);
}
}
}
}
}
}
}
_ => {}
}
for child in node.children(&mut node.walk()) {
collect_bindings_dfs(&child, bytes, scopes, out);
}
}
fn collect_params(params: &Node, bytes: &[u8], scopes: &[Scope], out: &mut Vec<Binding>) {
for child in params.named_children(&mut params.walk()) {
if child.kind() == "formal_parameter" {
if let Some(name) = field_text(&child, "name", bytes) {
let intro = child.start_byte();
push_binding(out, name, intro, BindingKind::Param, scopes);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn extract(src: &str, file: &str) -> FileFacts {
DartExtractor.extract(src, file).unwrap()
}
fn by_name(facts: &FileFacts, name: &str) -> Option<Symbol> {
facts.symbols.iter().find(|s| s.name == name).cloned()
}
#[test]
fn class_and_method_get_correct_scip_strings() {
let src = r#"
class User {
String getName() { return ''; }
}
"#;
let facts = extract(src, "lib/models/user.dart");
let user = by_name(&facts, "User").unwrap();
assert_eq!(user.kind, SymbolKind::Class);
assert_eq!(
user.id.to_scip_string(),
"codegraph . . . models/user/User#"
);
let get_name = by_name(&facts, "getName").unwrap();
assert_eq!(get_name.kind, SymbolKind::Method);
assert_eq!(
get_name.id.to_scip_string(),
"codegraph . . . models/user/User#getName()."
);
assert_eq!(facts.lang, "dart");
}
#[test]
fn top_level_function_is_extracted() {
let src = r#"
void greet(String name) {
print(name);
}
"#;
let facts = extract(src, "lib/utils/greeter.dart");
let greet = by_name(&facts, "greet").unwrap();
assert_eq!(greet.kind, SymbolKind::Function);
assert_eq!(
greet.id.to_scip_string(),
"codegraph . . . utils/greeter/greet()."
);
}
#[test]
fn mixin_is_extracted_as_trait() {
let src = r#"
mixin Flyable on Animal {
void fly() {}
}
"#;
let facts = extract(src, "lib/mixins/flyable.dart");
let mixin = by_name(&facts, "Flyable").unwrap();
assert_eq!(mixin.kind, SymbolKind::Trait);
assert_eq!(
mixin.id.to_scip_string(),
"codegraph . . . mixins/flyable/Flyable#"
);
}
#[test]
fn enum_and_constants_are_extracted() {
let src = r#"
enum Color { red, green, blue }
"#;
let facts = extract(src, "lib/models/color.dart");
let color = by_name(&facts, "Color").unwrap();
assert_eq!(color.kind, SymbolKind::Enum);
assert_eq!(
color.id.to_scip_string(),
"codegraph . . . models/color/Color#"
);
let red = by_name(&facts, "red").unwrap();
assert_eq!(red.kind, SymbolKind::Const);
assert_eq!(
red.id.to_scip_string(),
"codegraph . . . models/color/Color#red."
);
}
#[test]
fn type_alias_is_extracted() {
let src = r#"
typedef Callback = void Function(String);
"#;
let facts = extract(src, "lib/types/aliases.dart");
let alias = by_name(&facts, "Callback").unwrap();
assert_eq!(alias.kind, SymbolKind::TypeAlias);
}
#[test]
fn top_level_variable_is_extracted_as_static() {
let src = r#"
String appName = 'MyApp';
"#;
let facts = extract(src, "lib/config/constants.dart");
let var_sym = by_name(&facts, "appName").unwrap();
assert_eq!(var_sym.kind, SymbolKind::Static);
}
#[test]
fn qualified_call_captures_qualifier() {
let src = r#"
class Client {
void run() {
var svc = Service();
svc.process();
}
}
"#;
let facts = extract(src, "lib/client.dart");
let process = facts
.references
.iter()
.find(|r| r.name == "process")
.expect("expected Call ref for 'process'");
assert_eq!(process.role, RefRole::Call);
assert_eq!(
process.qualifier.as_deref(),
Some("svc"),
"expected qualifier 'svc' on the process call ref",
);
}
#[test]
fn import_show_produces_import_references() {
let src = r#"
import 'package:a/b.dart' show Foo, Bar;
class C {}
"#;
let facts = extract(src, "lib/c.dart");
let import_names: Vec<&str> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Import)
.map(|r| r.name.as_str())
.collect();
assert!(
import_names.contains(&"Foo"),
"expected 'Foo' in import refs: {import_names:?}"
);
assert!(
import_names.contains(&"Bar"),
"expected 'Bar' in import refs: {import_names:?}"
);
let foo_ref = facts
.references
.iter()
.find(|r| r.role == RefRole::Import && r.name == "Foo")
.unwrap();
assert!(
foo_ref
.from_path
.as_deref()
.is_some_and(|p| p.contains("package:a/b.dart")),
"from_path should contain the URI, got {:?}",
foo_ref.from_path
);
}
#[test]
fn superclass_and_interface_produce_is_implementation_refs() {
let src = r#"
class Dog extends Animal implements Pet {
void bark() {}
}
"#;
let facts = extract(src, "lib/dog.dart");
let inherit_names: Vec<&str> = facts
.references
.iter()
.filter(|r| r.role == RefRole::IsImplementation)
.map(|r| r.name.as_str())
.collect();
assert!(
inherit_names.contains(&"Animal"),
"expected 'Animal' in IsImplementation refs: {inherit_names:?}"
);
assert!(
inherit_names.contains(&"Pet"),
"expected 'Pet' in IsImplementation refs: {inherit_names:?}"
);
}
#[test]
fn reassignment_emits_write_for_lhs_and_reads_for_rhs() {
let src = r#"
void run() {
var total = 0;
var bonus = 5;
total = total + bonus;
}
"#;
let facts = extract(src, "lib/run.dart");
let writes: Vec<&str> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Write)
.map(|r| r.name.as_str())
.collect();
assert!(
writes.contains(&"total"),
"expected Write ref for 'total': {writes:?}"
);
let reads: Vec<&str> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Read)
.map(|r| r.name.as_str())
.collect();
assert!(
reads.contains(&"bonus"),
"expected Read ref for 'bonus': {reads:?}"
);
assert!(
reads.contains(&"total"),
"expected Read ref for RHS 'total': {reads:?}"
);
}
#[test]
fn declaration_does_not_emit_write_for_bound_name() {
let src = r#"
void run() {
var result = compute();
}
"#;
let facts = extract(src, "lib/run.dart");
let write_names: Vec<&str> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Write)
.map(|r| r.name.as_str())
.collect();
assert!(
!write_names.contains(&"result"),
"declaration name 'result' must NOT produce a Write ref: {write_names:?}"
);
}
#[test]
fn call_argument_is_read_but_callee_is_not() {
let src = r#"
void run() {
logger(config);
}
"#;
let facts = extract(src, "lib/run.dart");
let reads: Vec<&str> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Read)
.map(|r| r.name.as_str())
.collect();
assert!(
reads.contains(&"config"),
"expected Read ref for 'config': {reads:?}"
);
assert!(
!reads.contains(&"logger"),
"callee 'logger' must NOT be emitted as a Read ref: {reads:?}"
);
}
#[test]
fn member_access_object_is_read_but_property_is_not() {
let src = r#"
void run() {
var value = 0;
value = source.field;
}
"#;
let facts = extract(src, "lib/run.dart");
let reads: Vec<&str> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Read)
.map(|r| r.name.as_str())
.collect();
assert!(
reads.contains(&"source"),
"expected Read ref for 'source': {reads:?}"
);
assert!(
!reads.contains(&"field"),
"member property 'field' must NOT be emitted as a Read ref: {reads:?}"
);
}
}