use tree_sitter::{Node, Parser};
use crate::error::{CodegraphError, Result};
use crate::graph::types::{
Binding, BindingKind, ByteSpan, EntryPoint, 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_ref, push_scope, push_type_ref,
};
const CALL_QUERY: &str = r#"
[
(call_expression (simple_identifier) @callee)
(call_expression (navigation_expression suffix: (navigation_suffix suffix: (simple_identifier) @callee)))
]
"#;
pub struct SwiftExtractor;
impl Extractor for SwiftExtractor {
fn lang(&self) -> Language {
Language::Swift
}
fn extract(&self, source: &str, file: &str) -> Result<FileFacts> {
let ts_language = crate::grammar::swift();
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 ctx = ExtractCtx {
bytes,
file,
lang: Language::Swift,
};
let ns_strings = swift_namespaces(file);
let ns_descriptors: Vec<Descriptor> = ns_strings
.iter()
.cloned()
.map(Descriptor::Namespace)
.collect();
let mut defs = Vec::new();
collect_decls(root, &ns_descriptors, &ctx, &mut defs);
let def_bindings = definition_bindings(&defs);
let mut symbols = defs;
symbols.push(super::module_symbol(
Language::Swift,
&ns_strings,
file,
source.len(),
));
let mut references = collect_call_references(
&root,
&ts_language,
CALL_QUERY,
Language::Swift,
bytes,
file,
)?;
collect_inheritance(&root, bytes, file, &mut references);
collect_imports(&root, bytes, file, &mut references);
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::Swift.as_str().to_owned(),
symbols,
references,
scopes,
bindings,
ffi_exports: Vec::new(),
})
}
}
fn swift_namespaces(file: &str) -> Vec<String> {
let mut parts: Vec<String> = file
.split('/')
.filter(|s| !s.is_empty())
.map(str::to_owned)
.collect();
if let Some(last) = parts.pop() {
let stem = last
.rsplit_once('.')
.map_or(last.as_str(), |(stem, _)| stem);
parts.push(stem.to_owned());
}
parts
}
fn read_visibility(node: &Node, bytes: &[u8]) -> Visibility {
for child in node.children(&mut node.walk()) {
if child.kind() != "modifiers" {
continue;
}
for modifier in child.children(&mut child.walk()) {
if modifier.kind() == "visibility_modifier" {
return match node_text(&modifier, bytes) {
"open" | "public" => Visibility::Public,
"package" | "internal" => Visibility::Internal,
"private" | "fileprivate" => Visibility::Private,
_ => Visibility::Internal,
};
}
}
return Visibility::Internal;
}
Visibility::Internal
}
fn leaf_type_name(node: Node, bytes: &[u8]) -> Option<String> {
match node.kind() {
"type_identifier" | "simple_identifier" => Some(node_text(&node, bytes).to_owned()),
_ => {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.is_named() {
if let Some(name) = leaf_type_name(child, bytes) {
return Some(name);
}
}
}
None
}
}
}
fn collect_inheritance(node: &Node, bytes: &[u8], file: &str, out: &mut Vec<Reference>) {
match node.kind() {
"class_declaration" | "protocol_declaration" => {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "inheritance_specifier" {
if let Some(inherits_from) = child.child_by_field_name("inherits_from") {
super::push_ref(
out,
super::simple_type_name(node_text(&inherits_from, bytes), "."),
&inherits_from,
file,
RefRole::IsImplementation,
);
}
}
}
}
_ => {}
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
collect_inheritance(&child, bytes, file, out);
}
}
fn collect_imports(node: &Node, bytes: &[u8], file: &str, out: &mut Vec<Reference>) {
if node.kind() == "import_declaration" {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "identifier" {
let leaf = super::simple_type_name(node_text(&child, bytes), ".");
super::push_ref(out, leaf, &child, file, RefRole::Import);
break;
}
}
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
collect_imports(&child, bytes, file, out);
}
}
fn push_symbol(
out: &mut Vec<Symbol>,
ctx: &ExtractCtx,
node: &Node,
name: String,
kind: SymbolKind,
visibility: Visibility,
descriptors: Vec<Descriptor>,
) {
let signature = one_line_signature(node_text(node, ctx.bytes), &['{', '\n']);
out.push(make_symbol(
ctx,
node,
name,
kind,
visibility,
descriptors,
signature,
));
}
fn collect_decls(container: Node, prefix: &[Descriptor], ctx: &ExtractCtx, out: &mut Vec<Symbol>) {
let inside_type = matches!(prefix.last(), Some(Descriptor::Type(_)));
let mut cursor = container.walk();
for child in container.children(&mut cursor) {
match child.kind() {
"class_declaration" => handle_class_declaration(child, prefix, ctx, out),
"protocol_declaration" => handle_protocol_declaration(child, prefix, ctx, out),
"function_declaration" => handle_function(child, prefix, ctx, out, inside_type),
"init_declaration" => handle_init(child, prefix, ctx, out),
"property_declaration" => handle_property(child, prefix, ctx, out),
"typealias_declaration" => handle_typealias(child, prefix, ctx, out),
"enum_entry" => handle_enum_entry(child, prefix, ctx, out),
_ => {}
}
}
}
fn handle_class_declaration(
node: Node,
prefix: &[Descriptor],
ctx: &ExtractCtx,
out: &mut Vec<Symbol>,
) {
let visibility = read_visibility(&node, ctx.bytes);
let kind_text = field_text(&node, "declaration_kind", ctx.bytes).unwrap_or_default();
let name_node = match node.child_by_field_name("name") {
Some(n) => n,
None => return,
};
let type_name = match leaf_type_name(name_node, ctx.bytes) {
Some(n) => n,
None => return,
};
let body = match node.child_by_field_name("body") {
Some(b) => b,
None => return,
};
if kind_text == "extension" {
let mut member_prefix = prefix.to_vec();
member_prefix.push(Descriptor::Type(type_name));
collect_decls(body, &member_prefix, ctx, out);
return;
}
let sym_kind = match kind_text.as_str() {
"class" => SymbolKind::Class,
"struct" => SymbolKind::Struct,
"enum" => SymbolKind::Enum,
"actor" => SymbolKind::Class,
_ => SymbolKind::Other,
};
let mut type_descriptors = prefix.to_vec();
type_descriptors.push(Descriptor::Type(type_name.clone()));
push_symbol(
out,
ctx,
&node,
type_name,
sym_kind,
visibility,
type_descriptors.clone(),
);
collect_decls(body, &type_descriptors, ctx, out);
}
fn handle_protocol_declaration(
node: Node,
prefix: &[Descriptor],
ctx: &ExtractCtx,
out: &mut Vec<Symbol>,
) {
let visibility = read_visibility(&node, ctx.bytes);
let type_name = match field_text(&node, "name", ctx.bytes) {
Some(n) => n,
None => return,
};
let mut type_descriptors = prefix.to_vec();
type_descriptors.push(Descriptor::Type(type_name.clone()));
push_symbol(
out,
ctx,
&node,
type_name,
SymbolKind::Interface,
visibility,
type_descriptors.clone(),
);
if let Some(proto_body) = node.child_by_field_name("body") {
collect_protocol_members(proto_body, &type_descriptors, ctx, out);
}
}
fn collect_protocol_members(
body: Node,
prefix: &[Descriptor],
ctx: &ExtractCtx,
out: &mut Vec<Symbol>,
) {
let mut cursor = body.walk();
for member in body.children(&mut cursor) {
match member.kind() {
"protocol_function_declaration" => {
let visibility = read_visibility(&member, ctx.bytes);
let name = match field_text(&member, "name", ctx.bytes) {
Some(n) => n,
None => continue,
};
let mut descriptors = prefix.to_vec();
descriptors.push(Descriptor::Method {
name: name.clone(),
disambiguator: String::new(),
});
push_symbol(
out,
ctx,
&member,
name,
SymbolKind::Method,
visibility,
descriptors,
);
}
"protocol_property_declaration" => {
let visibility = read_visibility(&member, ctx.bytes);
let name = match member.child_by_field_name("name") {
Some(pat) => match pat.child_by_field_name("bound_identifier") {
Some(bi) => node_text(&bi, ctx.bytes).to_owned(),
None => continue,
},
None => continue,
};
let mut descriptors = prefix.to_vec();
descriptors.push(Descriptor::Term(name.clone()));
push_symbol(
out,
ctx,
&member,
name,
SymbolKind::Const,
visibility,
descriptors,
);
}
_ => {}
}
}
}
fn handle_function(
node: Node,
prefix: &[Descriptor],
ctx: &ExtractCtx,
out: &mut Vec<Symbol>,
inside_type: bool,
) {
let visibility = read_visibility(&node, ctx.bytes);
let name = match field_text(&node, "name", ctx.bytes) {
Some(n) => n,
None => return,
};
let kind = if inside_type {
SymbolKind::Method
} else {
SymbolKind::Function
};
let is_main = name == "main";
let mut descriptors = prefix.to_vec();
descriptors.push(Descriptor::Method {
name: name.clone(),
disambiguator: String::new(),
});
push_symbol(out, ctx, &node, name, kind, visibility, descriptors);
if is_main {
if let Some(s) = out.last_mut() {
s.entry_points.push(EntryPoint::Main);
}
}
}
fn handle_init(node: Node, prefix: &[Descriptor], ctx: &ExtractCtx, out: &mut Vec<Symbol>) {
let visibility = read_visibility(&node, ctx.bytes);
let mut descriptors = prefix.to_vec();
descriptors.push(Descriptor::Method {
name: "init".to_owned(),
disambiguator: String::new(),
});
push_symbol(
out,
ctx,
&node,
"init".to_owned(),
SymbolKind::Method,
visibility,
descriptors,
);
}
fn handle_property(node: Node, prefix: &[Descriptor], ctx: &ExtractCtx, out: &mut Vec<Symbol>) {
let visibility = read_visibility(&node, ctx.bytes);
let is_let = {
let mut found_let = true; let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "value_binding_pattern" {
found_let = field_text(&child, "mutability", ctx.bytes).is_none_or(|m| m == "let");
break;
}
}
found_let
};
let name_node = match node.child_by_field_name("name") {
Some(n) => n,
None => return,
};
let var_name = match name_node.child_by_field_name("bound_identifier") {
Some(bi) => node_text(&bi, ctx.bytes).to_owned(),
None => return,
};
let kind = if is_let {
SymbolKind::Const
} else {
SymbolKind::Static
};
let mut descriptors = prefix.to_vec();
descriptors.push(Descriptor::Term(var_name.clone()));
push_symbol(out, ctx, &node, var_name, kind, visibility, descriptors);
}
fn handle_typealias(node: Node, prefix: &[Descriptor], ctx: &ExtractCtx, out: &mut Vec<Symbol>) {
let visibility = read_visibility(&node, ctx.bytes);
let name = match field_text(&node, "name", ctx.bytes) {
Some(n) => n,
None => return,
};
let mut descriptors = prefix.to_vec();
descriptors.push(Descriptor::Type(name.clone()));
push_symbol(
out,
ctx,
&node,
name,
SymbolKind::TypeAlias,
visibility,
descriptors,
);
}
fn handle_enum_entry(node: Node, prefix: &[Descriptor], ctx: &ExtractCtx, out: &mut Vec<Symbol>) {
let visibility = read_visibility(&node, ctx.bytes);
let name = match field_text(&node, "name", ctx.bytes) {
Some(n) => n,
None => return,
};
let mut descriptors = prefix.to_vec();
descriptors.push(Descriptor::Term(name.clone()));
push_symbol(
out,
ctx,
&node,
name,
SymbolKind::Const,
visibility,
descriptors,
);
}
fn type_leaf_swift(
node: &Node,
bytes: &[u8],
file: &str,
ctx: TypeRefContext,
out: &mut Vec<Reference>,
) {
match node.kind() {
"user_type" => {
let mut type_args_node: Option<Node> = None;
for child in node.named_children(&mut node.walk()) {
match child.kind() {
"type_identifier" => {
let name = node_text(&child, bytes);
push_type_ref(out, name, &child, file, ctx);
}
"type_arguments" => {
type_args_node = Some(child);
}
_ => {}
}
}
if let Some(args) = type_args_node {
for child in args.named_children(&mut args.walk()) {
type_leaf_swift(&child, bytes, file, TypeRefContext::GenericArg, out);
}
}
}
"optional_type" => {
if let Some(inner) = node.child_by_field_name("wrapped") {
type_leaf_swift(&inner, bytes, file, ctx, out);
}
}
"array_type" => {
if let Some(elem) = node.child_by_field_name("element") {
type_leaf_swift(&elem, bytes, file, ctx, out);
}
}
"dictionary_type" => {
if let Some(key) = node.child_by_field_name("key") {
type_leaf_swift(&key, bytes, file, ctx, out);
}
if let Some(val) = node.child_by_field_name("value") {
type_leaf_swift(&val, bytes, file, ctx, out);
}
}
_ => {
for child in node.named_children(&mut node.walk()) {
type_leaf_swift(&child, bytes, file, ctx, out);
}
}
}
}
fn collect_type_references(node: &Node, bytes: &[u8], file: &str, out: &mut Vec<Reference>) {
match node.kind() {
"parameter" => {
if let Some(type_node) = node.child_by_field_name("type") {
type_leaf_swift(&type_node, bytes, file, TypeRefContext::ParameterType, out);
}
return;
}
"function_declaration" => {
if let Some(ret) = node.child_by_field_name("return_type") {
type_leaf_swift(&ret, bytes, file, TypeRefContext::ReturnType, out);
}
}
"property_declaration" | "protocol_property_declaration" => {
for child in node.children(&mut node.walk()) {
if child.kind() == "type_annotation" {
if let Some(type_node) = child.child_by_field_name("type") {
type_leaf_swift(&type_node, bytes, file, TypeRefContext::Field, out);
}
}
}
}
_ => {}
}
for child in node.children(&mut node.walk()) {
collect_type_references(&child, bytes, file, out);
}
}
fn is_non_read_position(node: &Node) -> bool {
let parent = match node.parent() {
Some(p) => p,
None => return true, };
match parent.kind() {
"call_expression" => true,
"navigation_suffix" => true,
"function_declaration" | "protocol_function_declaration" => {
parent.child_by_field_name("name").as_ref() == Some(node)
}
"enum_entry" => parent.child_by_field_name("name").as_ref() == Some(node),
"pattern" => parent.child_by_field_name("bound_identifier").as_ref() == Some(node),
"parameter" => {
parent.child_by_field_name("name").as_ref() == Some(node)
|| parent.child_by_field_name("external_name").as_ref() == Some(node)
}
"value_argument_label" => true,
"directly_assignable_expression" => true,
_ => false,
}
}
fn collect_read_references(node: &Node, bytes: &[u8], file: &str, out: &mut Vec<Reference>) {
if node.kind() == "simple_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" {
if let Some(target) = node.child_by_field_name("target") {
if let Some(lhs) = target.named_child(0) {
if lhs.kind() == "simple_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" | "protocol_declaration" => {
let type_id = push_scope(scopes, Some(parent_id), node_span(node), ScopeKind::Type);
for child in node.children(&mut node.walk()) {
if matches!(
child.kind(),
"class_body" | "enum_class_body" | "protocol_body"
) {
for body_child in child.children(&mut child.walk()) {
scope_dfs(&body_child, type_id, scopes);
}
}
}
}
"function_declaration" | "init_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 body_child in body.children(&mut body.walk()) {
if body_child.kind() == "statements" {
for stmt_child in body_child.children(&mut body_child.walk()) {
scope_dfs(&stmt_child, fn_id, scopes);
}
break;
}
}
}
}
"lambda_literal" => {
let fn_id = push_scope(
scopes,
Some(parent_id),
node_span(node),
ScopeKind::Function,
);
for child in node.children(&mut node.walk()) {
scope_dfs(&child, fn_id, scopes);
}
}
"statements" => {
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" | "init_declaration" => {
collect_swift_params(node, bytes, scopes, out);
for child in node.children(&mut node.walk()) {
collect_bindings_dfs(&child, bytes, scopes, out);
}
}
"lambda_literal" => {
if let Some(lft) = node.child_by_field_name("type") {
for lft_child in lft.children(&mut lft.walk()) {
if lft_child.kind() == "lambda_function_type_parameters" {
for param in lft_child.children(&mut lft_child.walk()) {
if param.kind() == "lambda_parameter" {
if let Some(name_node) = param.child_by_field_name("name") {
if name_node.kind() == "simple_identifier" {
let name = node_text(&name_node, bytes);
let intro = name_node.start_byte();
push_binding(
out,
name.to_owned(),
intro,
BindingKind::Param,
scopes,
);
}
}
}
}
}
}
}
for child in node.children(&mut node.walk()) {
collect_bindings_dfs(&child, bytes, scopes, out);
}
}
"property_declaration" => {
if let Some(name_node) = node.child_by_field_name("name") {
if let Some(bi) = name_node.child_by_field_name("bound_identifier") {
let intro = bi.start_byte();
let sid = innermost_scope(intro, scopes).unwrap_or(0);
if matches!(scopes[sid].kind, ScopeKind::Function | ScopeKind::Block) {
let name = node_text(&bi, bytes);
push_binding(out, name.to_owned(), intro, BindingKind::Local, scopes);
}
}
}
for child in node.children(&mut node.walk()) {
collect_bindings_dfs(&child, bytes, scopes, out);
}
}
"for_statement" => {
if let Some(item) = node.child_by_field_name("item") {
if let Some(bi) = item.child_by_field_name("bound_identifier") {
let intro = bi.start_byte();
let sid = innermost_scope(intro, scopes).unwrap_or(0);
if matches!(scopes[sid].kind, ScopeKind::Function | ScopeKind::Block) {
let name = node_text(&bi, bytes);
push_binding(out, name.to_owned(), intro, BindingKind::Local, scopes);
}
}
}
for child in node.children(&mut node.walk()) {
collect_bindings_dfs(&child, bytes, scopes, out);
}
}
_ => {
for child in node.children(&mut node.walk()) {
collect_bindings_dfs(&child, bytes, scopes, out);
}
}
}
}
fn collect_swift_params(func_node: &Node, bytes: &[u8], scopes: &[Scope], out: &mut Vec<Binding>) {
collect_params_dfs(func_node, bytes, scopes, out, true);
}
fn collect_params_dfs(
node: &Node,
bytes: &[u8],
scopes: &[Scope],
out: &mut Vec<Binding>,
is_root: bool,
) {
if !is_root
&& matches!(
node.kind(),
"function_declaration" | "init_declaration" | "lambda_literal"
)
{
return;
}
if node.kind() == "parameter" {
if let Some(name_node) = node.child_by_field_name("name") {
if name_node.kind() == "simple_identifier" {
let name = node_text(&name_node, bytes);
let intro = name_node.start_byte();
push_binding(out, name.to_owned(), intro, BindingKind::Param, scopes);
}
}
return;
}
for child in node.children(&mut node.walk()) {
collect_params_dfs(&child, bytes, scopes, out, false);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::types::TypeRefContext;
fn extract(src: &str, path: &str) -> FileFacts {
SwiftExtractor.extract(src, path).unwrap()
}
fn by_name(facts: &FileFacts, name: &str) -> Option<Symbol> {
facts.symbols.iter().find(|s| s.name == name).cloned()
}
#[test]
fn main_function_is_entry_point() {
let facts = extract("func main() {}", "src/main.swift");
let main = by_name(&facts, "main").unwrap();
assert!(
main.entry_points
.iter()
.any(|e| matches!(e, EntryPoint::Main))
);
}
#[test]
fn public_class_visibility_gate() {
let src = r#"
public class Session {
public func validate() -> Bool { return true }
private func secret() {}
let token = ""
}
"#;
let facts = extract(src, "Sources/Auth/Session.swift");
let session = by_name(&facts, "Session").unwrap();
assert_eq!(session.kind, SymbolKind::Class);
assert_eq!(session.visibility, Visibility::Public);
assert_eq!(
session.id.to_scip_string(),
"codegraph . . . Sources/Auth/Session/Session#"
);
let validate = by_name(&facts, "validate").unwrap();
assert_eq!(validate.kind, SymbolKind::Method);
assert_eq!(validate.visibility, Visibility::Public);
assert_eq!(
validate.id.to_scip_string(),
"codegraph . . . Sources/Auth/Session/Session#validate()."
);
let secret = by_name(&facts, "secret").unwrap();
assert_eq!(secret.kind, SymbolKind::Method);
assert_eq!(secret.visibility, Visibility::Private);
assert_eq!(
secret.id.to_scip_string(),
"codegraph . . . Sources/Auth/Session/Session#secret()."
);
let token = by_name(&facts, "token").unwrap();
assert_eq!(token.kind, SymbolKind::Const);
assert_eq!(token.visibility, Visibility::Internal);
assert_eq!(
token.id.to_scip_string(),
"codegraph . . . Sources/Auth/Session/Session#token."
);
}
#[test]
fn struct_with_let_property() {
let src = r#"
struct Point {
let x: Double
var y: Double
}
"#;
let facts = extract(src, "Sources/Models/Point.swift");
let point = by_name(&facts, "Point").unwrap();
assert_eq!(point.kind, SymbolKind::Struct);
assert_eq!(
point.id.to_scip_string(),
"codegraph . . . Sources/Models/Point/Point#"
);
let x = by_name(&facts, "x").unwrap();
assert_eq!(x.kind, SymbolKind::Const);
assert_eq!(
x.id.to_scip_string(),
"codegraph . . . Sources/Models/Point/Point#x."
);
let y = by_name(&facts, "y").unwrap();
assert_eq!(y.kind, SymbolKind::Static);
assert_eq!(
y.id.to_scip_string(),
"codegraph . . . Sources/Models/Point/Point#y."
);
}
#[test]
fn enum_with_cases() {
let src = r#"
enum Direction {
case north
case south
case east
case west
}
"#;
let facts = extract(src, "Sources/Direction.swift");
let dir = by_name(&facts, "Direction").unwrap();
assert_eq!(dir.kind, SymbolKind::Enum);
assert_eq!(
dir.id.to_scip_string(),
"codegraph . . . Sources/Direction/Direction#"
);
for case in &["north", "south", "east", "west"] {
let sym = by_name(&facts, case).unwrap();
assert_eq!(sym.kind, SymbolKind::Const);
assert_eq!(
sym.id.to_scip_string(),
format!("codegraph . . . Sources/Direction/Direction#{case}.")
);
}
}
#[test]
fn protocol_with_function_requirement() {
let src = r#"
public protocol Readable {
func read() -> String
}
"#;
let facts = extract(src, "Sources/Protocols/Readable.swift");
let proto = by_name(&facts, "Readable").unwrap();
assert_eq!(proto.kind, SymbolKind::Interface);
assert_eq!(
proto.id.to_scip_string(),
"codegraph . . . Sources/Protocols/Readable/Readable#"
);
let read = by_name(&facts, "read").unwrap();
assert_eq!(read.kind, SymbolKind::Method);
assert_eq!(
read.id.to_scip_string(),
"codegraph . . . Sources/Protocols/Readable/Readable#read()."
);
}
#[test]
fn extension_members_without_type_symbol() {
let src = r#"
extension Foo {
public func bar() {}
}
"#;
let facts = extract(src, "Sources/Foo+Ext.swift");
let bar = by_name(&facts, "bar").unwrap();
assert_eq!(bar.kind, SymbolKind::Method);
assert_eq!(
bar.id.to_scip_string(),
"codegraph . . . Sources/Foo+Ext/Foo#bar()."
);
}
#[test]
fn top_level_function() {
let src = r#"
public func greet(name: String) -> String {
return "Hello " + name
}
"#;
let facts = extract(src, "Sources/Utils/Greeting.swift");
let greet = by_name(&facts, "greet").unwrap();
assert_eq!(greet.kind, SymbolKind::Function);
assert_eq!(
greet.id.to_scip_string(),
"codegraph . . . Sources/Utils/Greeting/greet()."
);
}
#[test]
fn call_references_captured() {
let src = r#"
func main() {
validate("t")
let obj = Foo()
obj.process()
}
"#;
let facts = extract(src, "Sources/main.swift");
let names: Vec<&str> = facts.references.iter().map(|r| r.name.as_str()).collect();
assert!(
names.contains(&"validate"),
"expected 'validate' in {names:?}"
);
assert!(
names.contains(&"process"),
"expected 'process' in {names:?}"
);
}
#[test]
fn lang_tag() {
let facts = extract("func foo() {}", "Sources/Foo.swift");
assert_eq!(facts.lang, "swift");
}
#[test]
fn class_inheritance_and_conformance() {
let src = "class Sub: Base, Proto {}";
let facts = extract(src, "Sources/Sub.swift");
let inherit: Vec<&str> = facts
.references
.iter()
.filter(|r| r.role == RefRole::IsImplementation)
.map(|r| r.name.as_str())
.collect();
assert!(inherit.contains(&"Base"), "expected 'Base' in {inherit:?}");
assert!(
inherit.contains(&"Proto"),
"expected 'Proto' in {inherit:?}"
);
}
#[test]
fn protocol_inheritance() {
let src = "protocol P: Q {}";
let facts = extract(src, "Sources/P.swift");
let inherit: Vec<&str> = facts
.references
.iter()
.filter(|r| r.role == RefRole::IsImplementation)
.map(|r| r.name.as_str())
.collect();
assert!(inherit.contains(&"Q"), "expected 'Q' in {inherit:?}");
}
#[test]
fn struct_conformance() {
let src = "struct S: Equatable {}";
let facts = extract(src, "Sources/S.swift");
let inherit: Vec<&str> = facts
.references
.iter()
.filter(|r| r.role == RefRole::IsImplementation)
.map(|r| r.name.as_str())
.collect();
assert!(
inherit.contains(&"Equatable"),
"expected 'Equatable' in {inherit:?}"
);
}
#[test]
fn import_foundation() {
let src = "import Foundation";
let facts = extract(src, "Sources/Foo.swift");
let imports: Vec<&str> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Import)
.map(|r| r.name.as_str())
.collect();
assert!(
imports.contains(&"Foundation"),
"expected 'Foundation' in {imports:?}"
);
}
#[test]
fn import_submodule_leaf() {
let src = "import os.log";
let facts = extract(src, "Sources/Bar.swift");
let imports: Vec<&str> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Import)
.map(|r| r.name.as_str())
.collect();
assert!(
imports.contains(&"log"),
"expected 'log' (leaf of os.log) in {imports:?}"
);
}
#[test]
fn func_params_emit_param_bindings() {
let src = "func f(label a: Int, b: String) {}";
let facts = extract(src, "Sources/F.swift");
let fn_scope_id = facts
.scopes
.iter()
.position(|s| s.kind == ScopeKind::Function)
.expect("expected a Function scope");
let mut param_names: Vec<(&str, ScopeId)> = facts
.bindings
.iter()
.filter(|b| b.kind == BindingKind::Param)
.map(|b| (b.name.as_str(), b.scope))
.collect();
param_names.sort_by_key(|(n, _)| *n);
assert_eq!(
param_names,
vec![("a", fn_scope_id), ("b", fn_scope_id)],
"expected Param bindings for a and b (internal names), got {param_names:?}"
);
}
#[test]
fn local_let_emits_local_binding() {
let src = "func f() { let x = 1 }";
let facts = extract(src, "Sources/F.swift");
let x = facts
.bindings
.iter()
.find(|b| b.kind == BindingKind::Local && b.name == "x")
.expect("expected a Local binding for 'x'");
assert!(
matches!(
facts.scopes[x.scope].kind,
ScopeKind::Function | ScopeKind::Block
),
"x should be in a Function or Block scope, got {:?}",
facts.scopes[x.scope].kind
);
}
#[test]
fn local_var_emits_local_binding() {
let src = "func f() { var y = 2 }";
let facts = extract(src, "Sources/F.swift");
let y = facts
.bindings
.iter()
.find(|b| b.kind == BindingKind::Local && b.name == "y")
.expect("expected a Local binding for 'y'");
assert!(
matches!(
facts.scopes[y.scope].kind,
ScopeKind::Function | ScopeKind::Block
),
"y should be in a Function or Block scope, got {:?}",
facts.scopes[y.scope].kind
);
}
#[test]
fn for_in_var_emits_local_binding() {
let src = "func f() { for x in [1, 2] {} }";
let facts = extract(src, "Sources/F.swift");
let x = facts
.bindings
.iter()
.find(|b| b.kind == BindingKind::Local && b.name == "x")
.expect("expected a Local binding for for-in 'x'");
assert!(
matches!(
facts.scopes[x.scope].kind,
ScopeKind::Function | ScopeKind::Block
),
"for-in x should be in a Function or Block scope, got {:?}",
facts.scopes[x.scope].kind
);
}
#[test]
fn class_property_not_local_but_is_definition() {
let src = "class C { let count = 0 }";
let facts = extract(src, "Sources/C.swift");
assert!(
!facts
.bindings
.iter()
.any(|b| b.kind == BindingKind::Local && b.name == "count"),
"class property 'count' must NOT be a Local binding"
);
assert!(
facts
.bindings
.iter()
.any(|b| b.kind == BindingKind::Definition && b.name == "count"),
"class property 'count' must have a Definition binding"
);
}
#[test]
fn nested_class_fun_scope_chain() {
let src = "class C { func f() {} }";
let facts = extract(src, "Sources/C.swift");
assert_eq!(
facts.scopes[0].kind,
ScopeKind::Module,
"scopes[0] must be Module"
);
let type_scope_id = facts
.scopes
.iter()
.position(|s| s.kind == ScopeKind::Type)
.expect("expected a Type scope");
let fn_scope_id = facts
.scopes
.iter()
.position(|s| s.kind == ScopeKind::Function)
.expect("expected a Function scope");
assert_eq!(
facts.scopes[type_scope_id].parent,
Some(0),
"Type scope parent should be Module (0)"
);
assert_eq!(
facts.scopes[fn_scope_id].parent,
Some(type_scope_id),
"Function scope parent should be the Type scope"
);
}
#[test]
fn lambda_params_emit_param_bindings() {
let src = "func f() { let g: (Int) -> Int = { a in a + 1 } }";
let facts = extract(src, "Sources/F.swift");
let a = facts
.bindings
.iter()
.find(|b| b.kind == BindingKind::Param && b.name == "a")
.expect("expected a Param binding for lambda param 'a'");
assert_eq!(
facts.scopes[a.scope].kind,
ScopeKind::Function,
"lambda param 'a' should be in a Function scope"
);
}
#[test]
fn init_params_emit_param_bindings() {
let src = "class C { init(x: Int) {} }";
let facts = extract(src, "Sources/C.swift");
let x = facts
.bindings
.iter()
.find(|b| b.kind == BindingKind::Param && b.name == "x")
.expect("expected a Param binding for init param 'x'");
assert_eq!(
facts.scopes[x.scope].kind,
ScopeKind::Function,
"init param 'x' should be in a Function scope"
);
}
#[test]
fn same_file_call_ref_has_scope() {
let src = "func greet() {}\nfunc main() { greet() }";
let facts = extract(src, "Sources/Greet.swift");
assert!(
by_name(&facts, "greet").is_some(),
"expected 'greet' Definition"
);
let greet_ref = facts
.references
.iter()
.find(|r| r.role == RefRole::Call && r.name == "greet")
.expect("expected a Call ref for 'greet'");
assert!(
greet_ref.scope.is_some() && greet_ref.scope != Some(0),
"greet() call ref should be in a non-root scope, got {:?}",
greet_ref.scope
);
}
#[test]
fn import_emits_import_binding() {
let src = "import Foundation\nfunc f() {}";
let facts = extract(src, "Sources/F.swift");
assert!(
facts
.bindings
.iter()
.any(|b| b.kind == BindingKind::Import && b.name == "Foundation"),
"expected an Import binding named 'Foundation', got {:?}",
facts
.bindings
.iter()
.filter(|b| b.kind == BindingKind::Import)
.map(|b| b.name.as_str())
.collect::<Vec<_>>()
);
}
#[test]
fn struct_property_not_local_but_is_definition() {
let src = "struct S { var count: Int = 0 }";
let facts = extract(src, "Sources/S.swift");
assert!(
!facts
.bindings
.iter()
.any(|b| b.kind == BindingKind::Local && b.name == "count"),
"struct property 'count' must NOT be a Local binding"
);
assert!(
facts
.bindings
.iter()
.any(|b| b.kind == BindingKind::Definition && b.name == "count"),
"struct property 'count' must have a Definition binding"
);
}
#[test]
fn read_ref_emitted_at_use_not_declaration() {
let src = "func f() -> Int { let base = 1; return base }";
let facts = extract(src, "Sources/F.swift");
let read_refs: Vec<_> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Read && r.name == "base")
.collect();
assert!(
!read_refs.is_empty(),
"expected at least one Read ref for 'base', got none; refs = {:?}",
facts
.references
.iter()
.map(|r| (&r.name, r.role))
.collect::<Vec<_>>()
);
let use_ref = read_refs
.iter()
.find(|r| r.occ.byte > 20)
.expect("expected Read ref for 'base' in the return expression (byte > 20)");
assert!(
use_ref.occ.byte > 20,
"Read ref should be at the use site, not the declaration"
);
}
#[test]
fn write_ref_emitted_for_assignment() {
let src = "func f() { var cnt = 0; cnt = 5 }";
let facts = extract(src, "Sources/F.swift");
let write_refs: Vec<_> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Write && r.name == "cnt")
.collect();
assert!(
!write_refs.is_empty(),
"expected at least one Write ref for 'cnt', got none; refs = {:?}",
facts
.references
.iter()
.map(|r| (&r.name, r.role))
.collect::<Vec<_>>()
);
}
#[test]
fn call_not_also_read() {
let src = "func f() { helper() }";
let facts = extract(src, "Sources/F.swift");
let call_refs: Vec<_> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Call && r.name == "helper")
.collect();
assert!(!call_refs.is_empty(), "expected a Call ref for 'helper'");
let read_refs: Vec<_> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Read && r.name == "helper")
.collect();
assert!(
read_refs.is_empty(),
"helper() must NOT produce a Read ref; got: {read_refs:?}"
);
}
#[test]
fn type_ref_parameter_type() {
let src = "func f(c: Config) {}";
let facts = extract(src, "Sources/F.swift");
let type_refs: Vec<_> = facts
.references
.iter()
.filter(|r| {
r.role == RefRole::TypeRef
&& r.name == "Config"
&& r.type_ref_ctx == Some(TypeRefContext::ParameterType)
})
.collect();
assert!(
!type_refs.is_empty(),
"expected TypeRef 'Config' with ParameterType context; refs = {:?}",
facts
.references
.iter()
.filter(|r| r.role == RefRole::TypeRef)
.map(|r| (&r.name, r.type_ref_ctx))
.collect::<Vec<_>>()
);
}
#[test]
fn type_ref_property_field() {
let src = "class C { let conf: Config }";
let facts = extract(src, "Sources/C.swift");
let type_refs: Vec<_> = facts
.references
.iter()
.filter(|r| {
r.role == RefRole::TypeRef
&& r.name == "Config"
&& r.type_ref_ctx == Some(TypeRefContext::Field)
})
.collect();
assert!(
!type_refs.is_empty(),
"expected TypeRef 'Config' with Field context; refs = {:?}",
facts
.references
.iter()
.filter(|r| r.role == RefRole::TypeRef)
.map(|r| (&r.name, r.type_ref_ctx))
.collect::<Vec<_>>()
);
}
#[test]
fn type_ref_generic_arg() {
let src = "func f(xs: Array<Config>) {}";
let facts = extract(src, "Sources/F.swift");
let type_refs_all: Vec<_> = facts
.references
.iter()
.filter(|r| r.role == RefRole::TypeRef)
.map(|r| (&r.name, r.type_ref_ctx))
.collect();
let has_config_generic_arg = facts.references.iter().any(|r| {
r.role == RefRole::TypeRef
&& r.name == "Config"
&& r.type_ref_ctx == Some(TypeRefContext::GenericArg)
});
assert!(
has_config_generic_arg,
"expected TypeRef 'Config' with GenericArg context; type_refs = {type_refs_all:?}"
);
}
#[test]
fn type_ref_return_type() {
let src = "func f() -> Config { fatalError() }";
let facts = extract(src, "Sources/F.swift");
let type_refs: Vec<_> = facts
.references
.iter()
.filter(|r| {
r.role == RefRole::TypeRef
&& r.name == "Config"
&& r.type_ref_ctx == Some(TypeRefContext::ReturnType)
})
.collect();
assert!(
!type_refs.is_empty(),
"expected TypeRef 'Config' with ReturnType context; refs = {:?}",
facts
.references
.iter()
.filter(|r| r.role == RefRole::TypeRef)
.map(|r| (&r.name, r.type_ref_ctx))
.collect::<Vec<_>>()
);
}
#[test]
fn navigation_member_not_a_read() {
let src = "func f(obj: Cls) { use(obj.field) }";
let facts = extract(src, "Sources/F.swift");
let field_reads: Vec<_> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Read && r.name == "field")
.collect();
assert!(
field_reads.is_empty(),
"navigation member 'field' must NOT be a Read ref; got: {field_reads:?}"
);
let obj_reads: Vec<_> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Read && r.name == "obj")
.collect();
assert!(
!obj_reads.is_empty(),
"base 'obj' of navigation expression must be a Read ref; refs = {:?}",
facts
.references
.iter()
.map(|r| (&r.name, r.role))
.collect::<Vec<_>>()
);
}
#[test]
fn visibility_public_modifier() {
let src = "public class Foo {}";
let facts = extract(src, "Sources/Foo.swift");
let foo = by_name(&facts, "Foo").unwrap();
assert_eq!(
foo.visibility,
Visibility::Public,
"expected Public for 'public class'"
);
}
#[test]
fn visibility_open_modifier() {
let src = "open class Base {}";
let facts = extract(src, "Sources/Base.swift");
let base = by_name(&facts, "Base").unwrap();
assert_eq!(
base.visibility,
Visibility::Public,
"expected Public for 'open class'"
);
}
#[test]
fn visibility_private_emitted_with_private_tag() {
let src = "class Outer { private func hidden() {} }";
let facts = extract(src, "Sources/Outer.swift");
let hidden = by_name(&facts, "hidden").expect("private func must be emitted");
assert_eq!(
hidden.visibility,
Visibility::Private,
"expected Private for 'private func'"
);
}
#[test]
fn visibility_fileprivate_emitted_with_private_tag() {
let src = "fileprivate struct Internal {}";
let facts = extract(src, "Sources/Internal.swift");
let sym = by_name(&facts, "Internal").expect("fileprivate struct must be emitted");
assert_eq!(
sym.visibility,
Visibility::Private,
"expected Private for 'fileprivate struct'"
);
}
#[test]
fn visibility_no_modifier_is_internal() {
let src = "func helper() {}";
let facts = extract(src, "Sources/Helper.swift");
let helper = by_name(&facts, "helper").unwrap();
assert_eq!(
helper.visibility,
Visibility::Internal,
"expected Internal for unmodified func"
);
}
#[test]
fn visibility_internal_modifier() {
let src = "internal class Cache {}";
let facts = extract(src, "Sources/Cache.swift");
let cache = by_name(&facts, "Cache").unwrap();
assert_eq!(
cache.visibility,
Visibility::Internal,
"expected Internal for 'internal class'"
);
}
#[test]
fn visibility_package_modifier() {
let src = "package func packageApi() {}";
let facts = extract(src, "Sources/Api.swift");
let api = by_name(&facts, "packageApi").unwrap();
assert_eq!(
api.visibility,
Visibility::Internal,
"expected Internal for 'package func'"
);
}
#[test]
fn visibility_mixed_class_all_emitted() {
let src = r#"
public class Service {
public func publicOp() {}
internal func internalOp() {}
private func privateOp() {}
fileprivate func fileprivateOp() {}
}
"#;
let facts = extract(src, "Sources/Service.swift");
let public_op = by_name(&facts, "publicOp").expect("publicOp must be emitted");
assert_eq!(public_op.visibility, Visibility::Public);
let internal_op = by_name(&facts, "internalOp").expect("internalOp must be emitted");
assert_eq!(internal_op.visibility, Visibility::Internal);
let private_op = by_name(&facts, "privateOp").expect("privateOp must be emitted");
assert_eq!(private_op.visibility, Visibility::Private);
let fileprivate_op =
by_name(&facts, "fileprivateOp").expect("fileprivateOp must be emitted");
assert_eq!(fileprivate_op.visibility, Visibility::Private);
}
}