Skip to content

fix(pascal): the receiver calls of a chained Delphi call are indexed - #2787

Merged
colbymchenry merged 1 commit into
mainfrom
claude/issue-2547-delphi-chained-calls
Oct 22, 2026
Merged

colbymchenry merged 1 commit into
mainfrom
claude/issue-2547-delphi-chained-calls

Conversation

@colbymchenry

@colbymchenry colbymchenry commented Oct 22, 2026 •

Copy link
Copy Markdown
Owner

Summary

In Delphi, a call made on the result of another call now has its inner calls indexed. For Result := MD5FileSized(TMeta.ExtendedPath(APath)).IsSame(AKey) and MD5FileSized(TMeta.ExtendedPath(APath)).Reset;, the issue's unit now gives TargetIsTheSame the callees MD5FileSized, ExtendedPath and IsSame, and Stmt the callees MD5FileSized, ExtendedPath and Reset. Both MD5FileSized and ExtendedPath list both routines as callers.

Along the way, a typecast like TButton(Sender).Caption or F := TFoo(Obj) now counts as a use of the type, not as creating one.

Root cause

extractPascalCall read an exprDot callee's receiver only to build the TFoo.GetInstance().DoIt name of a T/I-prefixed factory chain (#645/#608). It recorded neither the receiver call nor the calls in its arguments. extractPascalParenlessCall recorded the receiver only behind the same T/I gate. In expression position, visitPascalBlock looked only one level down, so it missed A(1) in X := A(1).B.C and GetIndex(x) in List[GetIndex(x)].Free.

Fix

Extraction (src/extraction/tree-sitter.ts):

  • A new receiver walk records every parenthesized call in a call's receiver chain, together with the calls in its arguments and in subscript indexes. Routines passed by name in a receiver call's arguments are captured as for any call (Can CodeGraph Identify Callback Functions in C Code? #756).
  • A paren-less link (.B) is still a read, not a call. Pascal/Delphi: parameterless method calls without () produce no calls edge (callers/impact miss most call sites) #2514 is about those.
  • A link inside a chain is recorded only when it can be named precisely: Obj.Method, a bare routine (MD5FileSized(…), an own-class Get(i)), or a TFoo.Factory().X chain. It is never recorded as the bare method name that a chain like Client.New.BaseURL(…) or RESTClient.HandleRedirects(False).Accept(…) would leave. In the first A/B those bare middle links were half wrong (13 of 25 sampled right), matching namesakes like an unrelated TAppConfig.BaseURL.
  • The calls main already read keep their names, so nothing main links goes away.

Resolution (src/resolution/index.ts):

  • A receiver is very often a typecast (TButton(Sender).Caption, PInt64Rec(P)^.Lo), and syntax can't tell NullableUInt64(x) from the issue's MD5FileSized(x).
  • Delphi builds an object only through a constructor (TFoo.Create). So a bare-name Pascal call that resolves to a type (class, record, interface, enum, type alias) is a cast, and it now gets a references edge to the type instead of instantiates or calls.
  • Main already made those wrong edges for every value cast. Every instantiates → class edge in the five Pascal repos is a typecast, so this corrects them too.

Validation

Tests: new __tests__/pascal-chained-calls.test.ts, 7 tests:

  • the issue's unit, both as extracted refs and as indexed edges;
  • receiver chains in every context: statement, assignment, condition, subscript index, a call of a call result;
  • a fluent chain whose inner links are named only where precise;
  • a factory chain that gives GetInstance its caller and still resolves DoIt on what it returns;
  • typecasts written every way (receiver, value, a record type without a T prefix, a pointer type alias), each a references edge with no construction;
  • a routine passed by name to a receiver call.

All 7 fail with main's sources. Removing only the resolver rule fails the typecast test, and removing only the chain-link gate fails the fluent-chain test.

A/B against main 88ebefa, both arms built from their own sources, kernel included, diffed by natural keys. The branch was then rebased onto c465230; the three commits in between (#2782 and #2784 for Go, #2785 for context ranking) leave Pascal untouched, and the SynEdit, mORMot2 and DelphiMVCFramework graphs on the rebased build are byte-identical to the ones triaged here:

Repo (commit) Removed: casts changing kind at the same site Other removed Added
SynEdit (2fc8a5d) 491 0 55
HeidiSQL (6f5bd12) 1,262 0 249
mORMot2 (587ce16) 4,832 7 (moved) 1,196
DWScript (1dbf8a9) 3,115 0 690
DelphiMVCFramework (3c96afa) 2,761 3 (moved) 2,727
leveldb (bb74ef7), C++ control byte-identical
SDWebImage (c3ad5e1), Objective-C control byte-identical
  • Removed. Each of the 12,461 cast edges reappears as references with the same source, target and position. They were instantiates → class, or calls → type alias / enum / interface. The "moved" ones are 10 function references whose first use in the same routine is now an earlier line.

  • Added.

    • 3,508 are casts read as references to their type: 14 of 14 sampled right, including DelphiMVCFramework's Firebird XxxVTable(vTable) casts and NullableUInt64(…) records, which have no T prefix.
    • 1,360 are receiver calls.
    • 49 are routines passed by name.
  • Precision of the new calls. I sampled 178 of the new call edges and 140 are right:

    Bucket Right
    resolved through the receiver's type 30 of 30
    qualified names 10 of 10
    bare functions 26 of 30
    bare methods 70 of 92
    calls landing on a property, field or variable 0 of 12

    The misses follow rules main applies to every call:

    • TypeInfo(…), a compiler intrinsic. Main already links 1,709 such calls to a project method named TypeInfo in mORMot2 and DelphiMVCFramework, and this adds 76.
    • high(V), which the built-ins list misses because the check is case-sensitive.
    • Calls through local procedural variables (F(ACtx, NextRef), Next()) matched to unrelated methods.
    • Calls now read inside a receiver call's arguments that main names by their bare method name, e.g. Memo1.Lines.Add inside an anonymous method.

Cost: within noise. Index time medians of 3 interleaved rounds: HeidiSQL 7.10 → 7.30 s wall (22.8 → 23.1 s CPU), mORMot2 18.80 → 18.90 s (60.2 → 61.4 s), DWScript 20.00 → 19.40 s (48.0 → 47.5 s).

Suite: the full suite ran on this commit (CODEGRAPH_KERNEL_EXPECT=1, kernel built): 714 files (708 passed, 6 skipped) and 8,558 tests (8,476 passed, 0 failed, 82 skipped).

Fixes #2547

Notes

🤖 Generated with Claude Code

A Pascal call whose receiver is itself a call recorded only its outer
method: in `MD5FileSized(TMeta.ExtendedPath(APath)).IsSame(AKey)` the
receiver call `MD5FileSized` and the `TMeta.ExtendedPath` in its
arguments were dropped, as were paren'd calls deeper in a chain
(`A(1).B.C`) or in an index (`List[GetIndex(x)].Free`). The extractor
read a receiver call only for a `TFoo.GetInstance()` factory chain.

The extractor now walks a call's receiver chain and records each paren'd
call in it, with the calls in its arguments and indexes. A paren-less
link stays a read. A link inside a chain is recorded only when it can be
named precisely (`Obj.Method`, a bare routine, a `TFoo.Factory().X`
chain), never as the bare method name a chain like
`Client.New.BaseURL(…)` leaves, which matched namesakes. The calls main
already read keep their names.

Receivers are very often typecasts (`TButton(Sender).Caption`), which
read like calls. Delphi builds objects only through a constructor
(`TFoo.Create`), so a bare-name Pascal call that resolves to a type is a
cast: resolution now gives it a `references` edge to the type instead of
`instantiates` or `calls`. That also corrects the edges main already
made for value casts (`F := TFoo(Obj)`).

Validation:
- New __tests__/pascal-chained-calls.test.ts (7 tests, the issue's unit
  included): all 7 fail with main's sources. Without only the resolver
  rule the typecast test fails; without only the chain-link gate the
  fluent-chain test fails.
- A/B against main 88ebefa, both arms built from their own sources,
  diffed by natural keys:
  - Every removed edge is a cast changing kind at the same site, with
    the same source and target (SynEdit 491, HeidiSQL 1,262, mORMot2
    4,832, DWScript 3,115, DelphiMVCFramework 2,761), apart from 10
    function references that moved to an earlier use in the same
    routine.
  - New edges: SynEdit 55, HeidiSQL 249, mORMot2 1,196, DWScript 690,
    DelphiMVCFramework 2,727. Mostly casts read as references to their
    type (14 of 14 sampled right) and receiver calls.
  - 178 new call edges sampled: 140 right. instance-method 30/30,
    qualified-name 10/10, bare functions 26/30, bare methods 70/92,
    calls landing on a property, field or variable 0/12. The wrong ones
    follow rules main applies to every call: the TypeInfo intrinsic
    (main already links 1,709 such calls to a project method), calls
    through local procedural variables, and calls now read inside a
    receiver's arguments, named as main names them.
- Controls leveldb (C++, kernel) and SDWebImage (Objective-C, wasm) are
  byte-identical in dump-graph.
- Cost within noise: index time medians of 3 interleaved rounds,
  HeidiSQL 7.10 to 7.30 s, mORMot2 18.80 to 18.90 s, DWScript 20.00 to
  19.40 s wall.

Fixes #2547

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Delphi extractor drops the receiver call of a chained call expression and everything inside it

1 participant