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#"
[
(exprCall entity: (identifier) @callee)
(exprCall entity: (exprDot lhs: (identifier) @qualifier rhs: (identifier) @callee))
]
"#;
pub struct PascalExtractor;
impl Extractor for PascalExtractor {
fn lang(&self) -> Language {
Language::Pascal
}
fn extract(&self, source: &str, file: &str) -> Result<FileFacts> {
let ts_language = crate::grammar::pascal();
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 = pascal_namespaces(&root, bytes, file);
let ctx = ExtractCtx {
bytes,
file,
lang: Language::Pascal,
};
let defs = collect_symbols(&root, &ctx, &namespaces);
let def_bindings = definition_bindings(&defs);
let mut symbols = defs;
let mod_sym = super::module_symbol(Language::Pascal, &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::Pascal,
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::Pascal.as_str().to_owned(),
symbols,
references,
scopes,
bindings,
ffi_exports: Vec::new(),
})
}
}
fn pascal_namespaces(root: &Node, bytes: &[u8], file: &str) -> Vec<String> {
for top in root.children(&mut root.walk()) {
if top.kind() == "unit" || top.kind() == "program" {
for child in top.children(&mut top.walk()) {
if child.kind() == "moduleName" {
for id in child.children(&mut child.walk()) {
if id.kind() == "identifier" {
return vec![node_text(&id, bytes).to_owned()];
}
}
}
}
}
}
let p = file
.strip_suffix(".pas")
.or_else(|| file.strip_suffix(".dpr"))
.or_else(|| file.strip_suffix(".dpk"))
.or_else(|| file.strip_suffix(".lpr"))
.unwrap_or(file);
let p = 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();
for top in root.children(&mut root.walk()) {
match top.kind() {
"unit" => collect_unit(&top, ctx, &ns_descriptors, &mut out),
"program" => collect_program(&top, ctx, &ns_descriptors, &mut out),
_ => {}
}
}
out
}
fn collect_unit(unit: &Node, ctx: &ExtractCtx, prefix: &[Descriptor], out: &mut Vec<Symbol>) {
for child in unit.children(&mut unit.walk()) {
match child.kind() {
"interface" => collect_decl_types(&child, ctx, prefix, out),
"implementation" => collect_impl_procs(&child, ctx, prefix, out),
_ => {}
}
}
}
fn collect_program(prog: &Node, ctx: &ExtractCtx, prefix: &[Descriptor], out: &mut Vec<Symbol>) {
collect_impl_procs(prog, ctx, prefix, out);
}
fn collect_decl_types(node: &Node, ctx: &ExtractCtx, prefix: &[Descriptor], out: &mut Vec<Symbol>) {
for child in node.children(&mut node.walk()) {
if child.kind() == "declTypes" {
for decl in child.children(&mut child.walk()) {
if decl.kind() == "declType" {
collect_decl_type(&decl, ctx, prefix, out);
}
}
}
}
}
fn collect_decl_type(node: &Node, ctx: &ExtractCtx, prefix: &[Descriptor], out: &mut Vec<Symbol>) {
let Some(name) = field_text(node, "name", ctx.bytes) else {
return;
};
let Some(type_node) = node.child_by_field_name("type") else {
return;
};
let inner = unwrap_type_node(&type_node);
let (kind, members) = classify_decl_type(&inner);
let mut type_descriptors = prefix.to_vec();
type_descriptors.push(Descriptor::Type(name.clone()));
out.push(make_symbol(
ctx,
node,
name.clone(),
kind,
Visibility::Public,
type_descriptors.clone(),
one_line_signature(node_text(node, ctx.bytes), &['{', ';']),
));
if kind == SymbolKind::Enum {
collect_enum_values(&inner, ctx, &type_descriptors, out);
} else {
collect_members(&inner, ctx, &type_descriptors, members, out);
}
}
fn unwrap_type_node<'a>(node: &Node<'a>) -> Node<'a> {
if node.kind() == "type" {
if let Some(inner) = node.named_children(&mut node.walk()).next() {
return inner;
}
}
*node
}
fn classify_decl_type(node: &Node) -> (SymbolKind, bool) {
match node.kind() {
"declClass" => {
let is_record = node
.children(&mut node.walk())
.any(|c| c.kind() == "kRecord");
if is_record {
(SymbolKind::Struct, true)
} else {
(SymbolKind::Class, true)
}
}
"declIntf" => (SymbolKind::Interface, true),
"declEnum" => (SymbolKind::Enum, false),
_ => (SymbolKind::Class, false),
}
}
fn collect_enum_values(
enum_node: &Node,
ctx: &ExtractCtx,
type_prefix: &[Descriptor],
out: &mut Vec<Symbol>,
) {
for child in enum_node.children(&mut enum_node.walk()) {
if child.kind() == "declEnumValue" {
if let Some(val_name) = field_text(&child, "name", ctx.bytes) {
let mut descriptors = type_prefix.to_vec();
descriptors.push(Descriptor::Term(val_name.clone()));
out.push(make_symbol(
ctx,
&child,
val_name,
SymbolKind::Const,
Visibility::Public,
descriptors,
one_line_signature(node_text(&child, ctx.bytes), &['{', ';', ',']),
));
}
}
}
}
fn section_visibility(node: &Node) -> Visibility {
let mut has_strict = false;
let mut vis_kw: Option<&str> = None;
for child in node.children(&mut node.walk()) {
match child.kind() {
"kStrict" => has_strict = true,
"kPublished" | "kPublic" => vis_kw = Some("public"),
"kProtected" => vis_kw = Some("protected"),
"kPrivate" => vis_kw = Some("private"),
_ => {}
}
}
match vis_kw {
Some("public") => Visibility::Public,
Some("protected") => Visibility::Protected,
Some("private") => Visibility::Private,
None if has_strict => Visibility::Private,
_ => Visibility::Public, }
}
fn collect_members(
body: &Node,
ctx: &ExtractCtx,
type_prefix: &[Descriptor],
emit: bool,
out: &mut Vec<Symbol>,
) {
if !emit {
return;
}
collect_members_in(body, ctx, type_prefix, Visibility::Public, out);
}
fn collect_members_in(
node: &Node,
ctx: &ExtractCtx,
type_prefix: &[Descriptor],
current_vis: Visibility,
out: &mut Vec<Symbol>,
) {
for child in node.children(&mut node.walk()) {
match child.kind() {
"declSection" => {
let vis = section_visibility(&child);
collect_members_in(&child, ctx, type_prefix, vis, out);
}
"declProc" => {
emit_method(&child, ctx, type_prefix, current_vis, out);
}
"declField" => {
emit_field(&child, ctx, type_prefix, current_vis, out);
}
_ => {}
}
}
}
fn emit_method(
node: &Node,
ctx: &ExtractCtx,
type_prefix: &[Descriptor],
vis: Visibility,
out: &mut Vec<Symbol>,
) {
let Some(name) = field_text(node, "name", ctx.bytes) else {
return;
};
if name.contains('.') {
return;
}
let mut descriptors = type_prefix.to_vec();
descriptors.push(Descriptor::Method {
name: name.clone(),
disambiguator: String::new(),
});
out.push(make_symbol(
ctx,
node,
name,
SymbolKind::Method,
vis,
descriptors,
one_line_signature(node_text(node, ctx.bytes), &['{', ';']),
));
}
fn emit_field(
node: &Node,
ctx: &ExtractCtx,
type_prefix: &[Descriptor],
vis: Visibility,
out: &mut Vec<Symbol>,
) {
let Some(name) = field_text(node, "name", ctx.bytes) else {
return;
};
let mut descriptors = type_prefix.to_vec();
descriptors.push(Descriptor::Term(name.clone()));
out.push(make_symbol(
ctx,
node,
name,
SymbolKind::Static,
vis,
descriptors,
one_line_signature(node_text(node, ctx.bytes), &['{', ';']),
));
}
fn collect_impl_procs(node: &Node, ctx: &ExtractCtx, prefix: &[Descriptor], out: &mut Vec<Symbol>) {
for child in node.children(&mut node.walk()) {
if child.kind() == "defProc" {
if let Some(header) = child.child_by_field_name("header") {
if header.kind() == "declProc" {
let name_is_plain_ident = header
.child_by_field_name("name")
.map(|n| n.kind() == "identifier")
.unwrap_or(false);
if name_is_plain_ident {
if let Some(name) = field_text(&header, "name", ctx.bytes) {
let mut descriptors = prefix.to_vec();
descriptors.push(Descriptor::Method {
name: name.clone(),
disambiguator: String::new(),
});
out.push(make_symbol(
ctx,
&child,
name,
SymbolKind::Function,
Visibility::Public,
descriptors,
one_line_signature(node_text(&header, ctx.bytes), &[';']),
));
}
}
}
}
}
}
}
fn collect_inheritance(node: &Node, bytes: &[u8], file: &str, out: &mut Vec<Reference>) {
if matches!(node.kind(), "declClass" | "declIntf") {
for child in node.children(&mut node.walk()) {
if child.kind() != "typeref" {
continue;
}
for id in child.children(&mut child.walk()) {
if id.kind() == "identifier" {
push_ref(
out,
node_text(&id, bytes),
&id,
file,
RefRole::IsImplementation,
);
}
}
}
}
for child in node.children(&mut node.walk()) {
collect_inheritance(&child, bytes, file, out);
}
}
fn collect_imports(
node: &Node,
bytes: &[u8],
file: &str,
out: &mut Vec<Reference>,
module_id: &str,
) {
if node.kind() == "declUses" {
for child in node.children(&mut node.walk()) {
if child.kind() == "moduleName" {
for id in child.children(&mut child.walk()) {
if id.kind() == "identifier" {
let name = node_text(&id, bytes);
push_import_ref(out, name, &id, file, module_id, name);
}
}
}
}
return;
}
for child in node.children(&mut node.walk()) {
collect_imports(&child, bytes, file, out, module_id);
}
}
fn collect_type_references(node: &Node, bytes: &[u8], file: &str, out: &mut Vec<Reference>) {
match node.kind() {
"declArg" => {
if let Some(ty) = node.child_by_field_name("type") {
type_leaf(&ty, bytes, file, TypeRefContext::ParameterType, out);
}
}
"declField" => {
if let Some(ty) = node.child_by_field_name("type") {
type_leaf(&ty, bytes, file, TypeRefContext::Field, out);
}
}
"declProc" => {
if let Some(ty) = node.child_by_field_name("type") {
type_leaf(&ty, bytes, file, TypeRefContext::ReturnType, out);
}
}
_ => {}
}
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() {
"typeref" => {
for id in node.children(&mut node.walk()) {
if id.kind() == "identifier" {
let name = node_text(&id, bytes);
push_type_ref(out, name, &id, file, ctx);
}
}
}
"type" => {
for child in node.named_children(&mut node.walk()) {
type_leaf(&child, bytes, file, ctx, out);
}
}
"identifier" => {
let name = node_text(node, bytes);
push_type_ref(out, name, node, file, ctx);
}
_ => {
let name = simple_type_name(node_text(node, bytes), ".");
if !name.is_empty() {
push_type_ref(out, name, node, file, ctx);
}
}
}
}
fn is_non_read_position(node: &Node) -> bool {
let parent = match node.parent() {
Some(p) => p,
None => return true, };
match parent.kind() {
"exprCall" => parent.child_by_field_name("entity").as_ref() == Some(node),
"exprDot" => parent.child_by_field_name("rhs").as_ref() == Some(node),
"assignment" => parent.child_by_field_name("lhs").as_ref() == Some(node),
"varAssignDef" => true,
"declProc" => parent.child_by_field_name("name").as_ref() == Some(node),
"declType" => parent.child_by_field_name("name").as_ref() == Some(node),
"declVar" => is_field_child(&parent, "name", node),
"declConst" => is_field_child(&parent, "name", node),
"declField" => is_field_child(&parent, "name", node),
"declArg" => is_field_child(&parent, "name", node),
"declEnumValue" => parent.child_by_field_name("name").as_ref() == Some(node),
"declLabel" => parent.child_by_field_name("name").as_ref() == Some(node),
"moduleName" => true,
"declUses" => true,
"typeref" => true,
"type" => true,
_ => false,
}
}
fn is_field_child(parent: &Node, field: &str, node: &Node) -> bool {
parent
.children_by_field_name(field, &mut parent.walk())
.any(|c| c == *node)
}
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" {
if let Some(lhs) = node.child_by_field_name("lhs") {
if lhs.kind() == "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() {
"unit" | "program" => {
let mod_id = push_scope(scopes, Some(parent_id), node_span(node), ScopeKind::Module);
for child in node.children(&mut node.walk()) {
scope_dfs(&child, mod_id, scopes);
}
}
"declClass" | "declIntf" => {
let type_id = push_scope(scopes, Some(parent_id), node_span(node), ScopeKind::Type);
for child in node.children(&mut node.walk()) {
scope_dfs(&child, type_id, scopes);
}
}
"defProc" => {
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>) {
if node.kind() == "declArg" {
let mut cursor = node.walk();
for name_node in node.children_by_field_name("name", &mut cursor) {
let name = node_text(&name_node, bytes).to_owned();
let intro = name_node.start_byte();
if name.len() >= MIN_REF_LEN && innermost_scope(intro, scopes) != Some(0) {
push_binding(out, name, intro, BindingKind::Param, scopes);
}
}
}
for child in node.children(&mut node.walk()) {
collect_bindings_dfs(&child, bytes, scopes, out);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn extract(src: &str, file: &str) -> FileFacts {
PascalExtractor.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#"
unit MyUnit;
interface
type
TFoo = class(TObject)
public
procedure Run;
end;
implementation
end.
"#;
let facts = extract(src, "src/MyUnit.pas");
let foo = by_name(&facts, "TFoo").unwrap();
assert_eq!(foo.kind, SymbolKind::Class);
assert_eq!(foo.id.to_scip_string(), "codegraph . . . MyUnit/TFoo#");
let run = by_name(&facts, "Run").unwrap();
assert_eq!(run.kind, SymbolKind::Method);
assert_eq!(
run.id.to_scip_string(),
"codegraph . . . MyUnit/TFoo#Run()."
);
assert_eq!(facts.lang, "pascal");
}
#[test]
fn record_with_field_is_extracted() {
let src = r#"
unit MyUnit;
interface
type
TPoint = record
X: Integer;
end;
implementation
end.
"#;
let facts = extract(src, "src/MyUnit.pas");
let tp = by_name(&facts, "TPoint").unwrap();
assert_eq!(tp.kind, SymbolKind::Struct);
assert_eq!(tp.id.to_scip_string(), "codegraph . . . MyUnit/TPoint#");
let x = by_name(&facts, "X").unwrap();
assert_eq!(x.kind, SymbolKind::Static);
assert_eq!(x.id.to_scip_string(), "codegraph . . . MyUnit/TPoint#X.");
}
#[test]
fn enum_and_values_are_extracted() {
let src = r#"
unit MyUnit;
interface
type
TColor = (Red, Green);
implementation
end.
"#;
let facts = extract(src, "src/MyUnit.pas");
let color = by_name(&facts, "TColor").unwrap();
assert_eq!(color.kind, SymbolKind::Enum);
assert_eq!(color.id.to_scip_string(), "codegraph . . . MyUnit/TColor#");
let red = by_name(&facts, "Red").unwrap();
assert_eq!(red.kind, SymbolKind::Const);
assert_eq!(
red.id.to_scip_string(),
"codegraph . . . MyUnit/TColor#Red."
);
let green = by_name(&facts, "Green").unwrap();
assert_eq!(green.kind, SymbolKind::Const);
assert_eq!(
green.id.to_scip_string(),
"codegraph . . . MyUnit/TColor#Green."
);
}
#[test]
fn free_call_captured_as_call_ref() {
let src = r#"
program Greeter;
procedure Greet;
begin
Bar();
end;
begin
end.
"#;
let facts = extract(src, "src/Greeter.dpr");
let bar_ref = facts
.references
.iter()
.find(|r| r.name == "Bar" && r.role == RefRole::Call);
assert!(bar_ref.is_some(), "expected Call ref for 'Bar'");
}
#[test]
fn qualified_call_captures_qualifier() {
let src = r#"
program Greeter;
procedure Greet;
begin
obj.Method();
end;
begin
end.
"#;
let facts = extract(src, "src/Greeter.dpr");
let method_ref = facts
.references
.iter()
.find(|r| r.name == "Method" && r.role == RefRole::Call)
.expect("expected Call ref for 'Method'");
assert_eq!(
method_ref.qualifier.as_deref(),
Some("obj"),
"expected qualifier 'obj' on Method call ref",
);
}
#[test]
fn uses_clause_produces_import_refs() {
let src = r#"
unit MyUnit;
interface
uses SysUtils, Classes;
implementation
end.
"#;
let facts = extract(src, "src/MyUnit.pas");
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(&"SysUtils"),
"expected 'SysUtils' in import refs: {import_names:?}"
);
assert!(
import_names.contains(&"Classes"),
"expected 'Classes' in import refs: {import_names:?}"
);
}
#[test]
fn class_parent_produces_is_implementation_ref() {
let src = r#"
unit MyUnit;
interface
type
TFoo = class(TObject)
end;
implementation
end.
"#;
let facts = extract(src, "src/MyUnit.pas");
let inherit: Vec<&str> = facts
.references
.iter()
.filter(|r| r.role == RefRole::IsImplementation)
.map(|r| r.name.as_str())
.collect();
assert!(
inherit.contains(&"TObject"),
"expected 'TObject' in IsImplementation refs: {inherit:?}"
);
}
#[test]
fn standalone_proc_is_function_and_method_impl_is_skipped() {
let src = r#"
unit MyUnit;
interface
type
TFoo = class
public
procedure Run;
end;
implementation
procedure Greet;
begin
end;
procedure TFoo.Run;
begin
end;
end.
"#;
let facts = extract(src, "src/MyUnit.pas");
let greet = by_name(&facts, "Greet").unwrap();
assert_eq!(greet.kind, SymbolKind::Function);
let run_count = facts.symbols.iter().filter(|s| s.name == "Run").count();
assert_eq!(
run_count, 1,
"Run should appear exactly once (the declaration, not the impl)"
);
}
#[test]
fn standalone_proc_visibility_is_public() {
let src = r#"
unit MyUnit;
interface
implementation
procedure Greet;
begin
end;
end.
"#;
let facts = extract(src, "src/MyUnit.pas");
let greet = by_name(&facts, "Greet").unwrap();
assert_eq!(greet.visibility, Visibility::Public);
}
#[test]
fn top_level_type_visibility_is_public() {
let src = r#"
unit MyUnit;
interface
type
TPoint = record
X: Integer;
end;
implementation
end.
"#;
let facts = extract(src, "src/MyUnit.pas");
let tp = by_name(&facts, "TPoint").unwrap();
assert_eq!(tp.visibility, Visibility::Public);
}
#[test]
fn enum_value_visibility_is_public() {
let src = r#"
unit MyUnit;
interface
type
TColor = (Red, Green);
implementation
end.
"#;
let facts = extract(src, "src/MyUnit.pas");
let red = by_name(&facts, "Red").unwrap();
assert_eq!(red.visibility, Visibility::Public);
}
#[test]
fn class_member_under_public_section_is_public() {
let src = r#"
unit MyUnit;
interface
type
TFoo = class
public
procedure Run;
end;
implementation
end.
"#;
let facts = extract(src, "src/MyUnit.pas");
let run = by_name(&facts, "Run").unwrap();
assert_eq!(run.visibility, Visibility::Public);
}
#[test]
fn class_member_under_published_section_is_public() {
let src = r#"
unit MyUnit;
interface
type
TFoo = class
published
procedure Run;
end;
implementation
end.
"#;
let facts = extract(src, "src/MyUnit.pas");
let run = by_name(&facts, "Run").unwrap();
assert_eq!(run.visibility, Visibility::Public);
}
#[test]
fn class_member_under_protected_section_is_protected() {
let src = r#"
unit MyUnit;
interface
type
TFoo = class
protected
procedure InternalRun;
end;
implementation
end.
"#;
let facts = extract(src, "src/MyUnit.pas");
let m = by_name(&facts, "InternalRun").unwrap();
assert_eq!(m.visibility, Visibility::Protected);
}
#[test]
fn class_member_under_private_section_is_private() {
let src = r#"
unit MyUnit;
interface
type
TFoo = class
private
FValue: Integer;
end;
implementation
end.
"#;
let facts = extract(src, "src/MyUnit.pas");
let f = by_name(&facts, "FValue").unwrap();
assert_eq!(f.visibility, Visibility::Private);
}
#[test]
fn class_member_under_strict_private_section_is_private() {
let src = r#"
unit MyUnit;
interface
type
TFoo = class
strict private
FSecret: Integer;
end;
implementation
end.
"#;
let facts = extract(src, "src/MyUnit.pas");
let f = by_name(&facts, "FSecret").unwrap();
assert_eq!(f.visibility, Visibility::Private);
}
#[test]
fn class_member_under_strict_protected_section_is_protected() {
let src = r#"
unit MyUnit;
interface
type
TFoo = class
strict protected
procedure HalfHidden;
end;
implementation
end.
"#;
let facts = extract(src, "src/MyUnit.pas");
let m = by_name(&facts, "HalfHidden").unwrap();
assert_eq!(m.visibility, Visibility::Protected);
}
#[test]
fn class_member_before_any_section_keyword_is_public() {
let src = r#"
unit MyUnit;
interface
type
TFoo = class
DefaultField: Integer;
end;
implementation
end.
"#;
let facts = extract(src, "src/MyUnit.pas");
let f = by_name(&facts, "DefaultField").unwrap();
assert_eq!(f.visibility, Visibility::Public);
}
#[test]
fn class_members_track_sections_independently() {
let src = r#"
unit MyUnit;
interface
type
TFoo = class
public
procedure PubMethod;
private
FField: Integer;
protected
procedure ProMethod;
end;
implementation
end.
"#;
let facts = extract(src, "src/MyUnit.pas");
let pub_m = by_name(&facts, "PubMethod").unwrap();
assert_eq!(pub_m.visibility, Visibility::Public);
let priv_f = by_name(&facts, "FField").unwrap();
assert_eq!(priv_f.visibility, Visibility::Private);
let pro_m = by_name(&facts, "ProMethod").unwrap();
assert_eq!(pro_m.visibility, Visibility::Protected);
}
#[test]
fn assignment_emits_write_and_reads() {
let src = r#"
program Greeter;
procedure Run;
var Total: Integer;
var Bonus: Integer;
begin
Total := Total + Bonus;
end;
begin
end.
"#;
let facts = extract(src, "src/Greeter.dpr");
let writes: Vec<&str> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Write)
.map(|r| r.name.as_str())
.collect();
assert!(
writes.iter().any(|n| n.eq_ignore_ascii_case("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.iter().any(|n| n.eq_ignore_ascii_case("Total")),
"expected Read ref for 'Total' on RHS: {reads:?}"
);
assert!(
reads.iter().any(|n| n.eq_ignore_ascii_case("Bonus")),
"expected Read ref for 'Bonus': {reads:?}"
);
}
#[test]
fn var_declaration_does_not_emit_write() {
let src = r#"
program Greeter;
procedure Run;
var Result: Integer;
begin
end;
begin
end.
"#;
let facts = extract(src, "src/Greeter.dpr");
let writes: Vec<&str> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Write)
.map(|r| r.name.as_str())
.collect();
assert!(
!writes.iter().any(|n| n.eq_ignore_ascii_case("Result")),
"expected NO Write ref for var declaration 'Result': {writes:?}"
);
}
#[test]
fn call_argument_is_read_callee_is_not() {
let src = r#"
program Greeter;
procedure Run;
var Arg: Integer;
begin
Process(Arg);
end;
begin
end.
"#;
let facts = extract(src, "src/Greeter.dpr");
let reads: Vec<&str> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Read)
.map(|r| r.name.as_str())
.collect();
assert!(
reads.iter().any(|n| n.eq_ignore_ascii_case("Arg")),
"expected Read ref for call argument 'Arg': {reads:?}"
);
assert!(
!reads.iter().any(|n| n.eq_ignore_ascii_case("Process")),
"callee 'Process' must NOT appear as a Read ref: {reads:?}"
);
}
#[test]
fn field_access_base_is_read_leaf_is_not() {
let src = r#"
program Greeter;
procedure Run;
var Value: Integer;
begin
Value := Source.Field;
end;
begin
end.
"#;
let facts = extract(src, "src/Greeter.dpr");
let reads: Vec<&str> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Read)
.map(|r| r.name.as_str())
.collect();
assert!(
reads.iter().any(|n| n.eq_ignore_ascii_case("Source")),
"expected Read ref for field-access base 'Source': {reads:?}"
);
assert!(
!reads.iter().any(|n| n.eq_ignore_ascii_case("Field")),
"field-access leaf 'Field' must NOT appear as a Read ref: {reads:?}"
);
}
#[test]
fn type_name_in_var_decl_is_not_a_read() {
let src = r#"
program Greeter;
procedure Run;
var Total: TMyType;
begin
Total := Source;
end;
begin
end.
"#;
let facts = extract(src, "src/Greeter.dpr");
let reads: Vec<&str> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Read)
.map(|r| r.name.as_str())
.collect();
assert!(
!reads.iter().any(|n| n.eq_ignore_ascii_case("TMyType")),
"type name 'TMyType' must NOT appear as a Read ref: {reads:?}"
);
assert!(
reads.iter().any(|n| n.eq_ignore_ascii_case("Source")),
"expected Read ref for value identifier 'Source': {reads:?}"
);
}
#[test]
fn comma_grouped_var_decl_names_are_not_reads() {
let src = r#"
program Greeter;
procedure Run;
var Total, Bonus: Integer;
begin
end;
begin
end.
"#;
let facts = extract(src, "src/Greeter.dpr");
let reads: Vec<&str> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Read)
.map(|r| r.name.as_str())
.collect();
assert!(
!reads.iter().any(|n| n.eq_ignore_ascii_case("Total")),
"declaration name 'Total' must NOT appear as a Read ref: {reads:?}"
);
assert!(
!reads.iter().any(|n| n.eq_ignore_ascii_case("Bonus")),
"declaration name 'Bonus' must NOT appear as a Read ref: {reads:?}"
);
}
#[test]
fn comma_grouped_params_both_get_param_bindings() {
let src = r#"
program Greeter;
procedure Foo(Alpha, Bravo: Integer);
begin
end;
begin
end.
"#;
let facts = extract(src, "src/Greeter.dpr");
let param_names: Vec<&str> = facts
.bindings
.iter()
.filter(|b| b.kind == BindingKind::Param)
.map(|b| b.name.as_str())
.collect();
assert!(
param_names.iter().any(|n| n.eq_ignore_ascii_case("Alpha")),
"expected Param binding for 'Alpha': {param_names:?}"
);
assert!(
param_names.iter().any(|n| n.eq_ignore_ascii_case("Bravo")),
"expected Param binding for 'Bravo': {param_names:?}"
);
}
}