feat(pascal): extract paren-less method calls (Obj.Free; / TFoo.GetInstance.DoIt;) (#793)

Pascal/Delphi lets a no-arg method or procedure drop its parens, so the call
parses as a bare `exprDot` (not an `exprCall`) and was never recorded as a call —
callers/impact/trace missed all of them (e.g. `Obj.Free`, `List.Clear`, the
paren-less factory chain `TFoo.GetInstance.DoIt`).

extractPascalParenlessCall handles these, wired into visitPascalBlock scoped to
STATEMENT position only: a bare `Obj.Field;` statement is a no-op, so a
statement-level dot expression is a call — but a dot in assignment LHS/RHS or a
condition is left alone, since there it's genuinely ambiguous with a
field/property access. The chained paren-less form reuses the #750 chain encoding
(gated on the Delphi `TFoo`/`IFoo` type convention) and resolves the same way.

PascalCoin A/B: +1131 / -1 — purely additive, and all 1131 new edges resolve to
METHOD nodes (zero field/property false positives, confirming the statement-level
gate). 3 new synthetic tests (paren-less call, paren-less chained factory, and the
property-write/read non-extraction guard). EXTRACTION_VERSION 16->17. Full suite green.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-06-11 08:54:17 -04:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 4c35b72136
commit 35dce04e1f
4 changed files with 164 additions and 6 deletions
+87
View File
@@ -3265,5 +3265,92 @@ end.
// TBar has no OnlyOther — must not mis-attach to the same-named TOther::OnlyOther.
expect(isCalled('TOther::OnlyOther')).toBe(false);
});
it('extracts paren-less method calls (Pascal lets a no-arg method drop its parens)', async () => {
fs.writeFileSync(
path.join(tempDir, 'main.pas'),
`unit Main;
interface
type
TFoo = class
procedure DoThing;
procedure Reset;
end;
implementation
procedure TFoo.DoThing; begin end;
procedure TFoo.Reset; begin end;
procedure Run(f: TFoo);
begin
f.DoThing;
f.Reset;
end;
end.
`
);
cg = await CodeGraph.init(tempDir, { index: true });
expect(isCalled('TFoo::DoThing')).toBe(true);
expect(isCalled('TFoo::Reset')).toBe(true);
});
it('resolves a PAREN-LESS chained factory call TFoo.GetInstance.DoIt via the return type', async () => {
fs.writeFileSync(
path.join(tempDir, 'main.pas'),
`unit Main;
interface
type
TBar = class
procedure DoIt;
end;
TDecoy = class
procedure DoIt;
end;
TFoo = class
class function GetInstance: TBar;
end;
implementation
procedure TBar.DoIt; begin end;
procedure TDecoy.DoIt; begin end;
class function TFoo.GetInstance: TBar; begin Result := nil; end;
procedure Run;
begin
TFoo.GetInstance.DoIt;
end;
end.
`
);
cg = await CodeGraph.init(tempDir, { index: true });
expect(isCalled('TBar::DoIt')).toBe(true);
expect(isCalled('TDecoy::DoIt')).toBe(false);
});
it('does NOT turn a property write/read into a call edge (only statement-level dots are calls)', async () => {
fs.writeFileSync(
path.join(tempDir, 'main.pas'),
`unit Main;
interface
type
TFoo = class
function GetValue: Integer;
procedure SetValue(v: Integer);
property Value: Integer read GetValue write SetValue;
end;
implementation
function TFoo.GetValue: Integer; begin Result := 0; end;
procedure TFoo.SetValue(v: Integer); begin end;
procedure Run(f: TFoo);
var x: Integer;
begin
f.Value := 5;
x := f.Value;
end;
end.
`
);
cg = await CodeGraph.init(tempDir, { index: true });
// A property read/write is a bare dot in assignment position, not a statement,
// so it must not be mis-extracted as a call to the property's getter/setter.
expect(isCalled('TFoo::GetValue')).toBe(false);
expect(isCalled('TFoo::SetValue')).toBe(false);
});
});
});