소스 검색

feat(impact): pair Delphi forms with their .pas code-behind (.dfm/.fmx ↔ .pas)

A Delphi form unit (UFRMAbout.pas) owns its visual form definition
(UFRMAbout.dfm / .fmx) via the `{$R *.dfm}` directive, not a `uses` clause — so a
form file used only as a definition was reported as having no dependents. New
pascalFormEdges synthesizer pairs each .dfm/.fmx with its same-basename sibling
.pas unit (same directory), linking unit → form so editing a form surfaces its
code-behind. PascalCoin 73.0% -> 75.7%. Gated to .dfm/.fmx (Delphi-only); false
edges 0; regression test fails without the fix; suite green (1187).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Colby McHenry 2 주 전
부모
커밋
2f30a3b
3개의 변경된 파일64개의 추가작업 그리고 0개의 파일을 삭제
  1. 1 0
      CHANGELOG.md
  2. 32 0
      __tests__/extraction.test.ts
  3. 31 0
      src/resolution/callback-synthesizer.ts

+ 1 - 0
CHANGELOG.md

@@ -32,6 +32,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 - Nuxt nested components are now connected to where they're used. Nuxt auto-imports a component in a subdirectory by a directory-prefixed name — `components/media/Card.vue` is used in templates as `<MediaCard/>` — but it was tracked by its file name (`Card`), so the usage didn't resolve and the component looked unused. PascalCase component tags (`<MediaCard>`, `<NavBar>`) in a `.vue` template are now matched, falling back to the Nuxt directory-prefixed name, so editing a nested component surfaces every page and component that renders it. (Vue, Nuxt)
 - Lua and Luau `require` calls now connect to their module files. A dotted module path (`require("telescope.config")` → `telescope/config.lua` or `.../config/init.lua`) and a Roblox/Luau instance-path require (`require(script.Parent.Signal)` → the `Signal` module) now link to the file they load, so editing a module surfaces every file that requires it. Previously requires resolved to nothing, so a Lua/Luau module looked like it had no dependents. (Lua, Luau)
 - Shopify OS 2.0 sections now connect to the JSON templates that use them. Modern Shopify themes define templates as JSON (`templates/*.json`, plus section groups `sections/*.json`) that list sections by `type` rather than with a `{% section %}` Liquid tag, so a section used only from a JSON template was reported as having no dependents. Those JSON files are now read and each section `type` is linked to its `sections/<type>.liquid`, so editing a section surfaces the templates that render it. (Liquid, Shopify)
+- Delphi form definitions now connect to their code-behind units. A `.dfm` (VCL) or `.fmx` (FireMonkey) form is owned by its same-named `.pas` unit through the `{$R *.dfm}` directive rather than a `uses` clause, so a form file used only as a definition was reported as having no dependents. The unit is now linked to its form, so editing a form surfaces the unit that owns it. (Pascal/Delphi)
 - Swift property wrappers and attributes are now connected. A `@Argument` / `@Published` / `@State` / custom `@propertyWrapper` on a property — and attributes on types, methods, and functions (`@objc`, `@MainActor`, …) — now record a dependency on the wrapper/attribute type. Previously these were dropped entirely (Swift attributes parse differently from other languages, and stored properties weren't being inspected), so the wrapper type looked unused and the file using it depended on nothing — a big gap for SwiftUI and argument-parser-style code.
 - Swift Fluent relationship models are no longer orphaned. A type referenced only through a property-wrapper *argument* — `@Siblings(through: AcronymCategoryPivot.self, …)`, the many-to-many pivot/join model — now records a dependency on that type. Previously only the wrapper itself (`Siblings`) and the property's declared type were captured, so a pivot model reached solely through the relationship looked like nothing depended on it and editing it surfaced no impact. (Swift, Vapor/Fluent)
 - Java annotations are now connected. Annotation definitions (`@interface Foo`) are indexed as types, and every `@Foo` usage on a class, method, or field is recorded as a dependency on it. Previously neither side was captured — annotation usages were dropped (they live inside the declaration's modifiers) and `@interface` types weren't indexed at all — so annotation-driven code (Spring `@GetMapping`, JPA `@Entity`, Gson `@SerializedName`, …) showed the annotation as having no users and the annotated class as not depending on it.

+ 32 - 0
__tests__/extraction.test.ts

@@ -4210,6 +4210,38 @@ describe('Same-directory include + KMP import resolution', () => {
   });
 });
 
+describe('Delphi form code-behind pairing', () => {
+  let tempDir: string;
+  let cg: CodeGraph;
+
+  beforeEach(() => {
+    tempDir = createTempDir();
+  });
+
+  afterEach(() => {
+    if (cg) cg.close();
+    if (fs.existsSync(tempDir)) fs.rmSync(tempDir, { recursive: true, force: true });
+  });
+
+  it('links a `.dfm` form to its sibling `.pas` code-behind unit', async () => {
+    // A Delphi form unit owns its visual form definition via `{$R *.dfm}`, not a
+    // `uses` clause — so a `.dfm` used only as a form definition looked orphaned.
+    fs.writeFileSync(path.join(tempDir, 'UFRMAbout.dfm'),
+      `object FRMAbout: TFRMAbout\n  Caption = 'About'\nend\n`);
+    fs.writeFileSync(path.join(tempDir, 'UFRMAbout.pas'),
+      `unit UFRMAbout;\ninterface\nuses Forms;\ntype\n  TFRMAbout = class(TForm)\n  end;\nimplementation\n{$R *.dfm}\nend.\n`);
+
+    cg = CodeGraph.initSync(tempDir);
+    await cg.indexAll();
+    cg.resolveReferences();
+
+    const dfm = cg.getNodesByKind('file').find((n) => n.filePath.endsWith('UFRMAbout.dfm'));
+    expect(dfm, 'UFRMAbout.dfm file node').toBeDefined();
+    const deps = cg.getFileDependents(dfm!.filePath);
+    expect(deps.some((p) => p.endsWith('UFRMAbout.pas')), 'the .pas unit links its .dfm form').toBe(true);
+  });
+});
+
 describe('Liquid Shopify JSON template section resolution', () => {
   let tempDir: string;
   let cg: CodeGraph;

+ 31 - 0
src/resolution/callback-synthesizer.ts

@@ -1499,6 +1499,35 @@ function ginMiddlewareChainEdges(queries: QueryBuilder, ctx: ResolutionContext):
   return edges;
 }
 
+/**
+ * Delphi form code-behind: a form unit `UFRMAbout.pas` owns its visual form
+ * definition `UFRMAbout.dfm` (VCL) / `.fmx` (FireMonkey) — paired by basename in
+ * the same directory, wired by the `{$R *.dfm}` directive rather than a `uses`
+ * clause. Link the unit → its form so a `.dfm`/`.fmx` used only as a form
+ * definition isn't orphaned, and editing the form surfaces its code-behind unit.
+ */
+function pascalFormEdges(ctx: ResolutionContext): Edge[] {
+  const edges: Edge[] = [];
+  const allFiles = new Set(ctx.getAllFiles());
+  for (const file of allFiles) {
+    if (!/\.(dfm|fmx)$/i.test(file)) continue;
+    const pasFile = file.replace(/\.(dfm|fmx)$/i, '.pas');
+    if (!allFiles.has(pasFile)) continue;
+    const formNode = ctx.getNodesInFile(file).find((n) => n.kind === 'file');
+    const unitNode = ctx.getNodesInFile(pasFile).find((n) => n.kind === 'file');
+    if (!formNode || !unitNode) continue;
+    edges.push({
+      source: unitNode.id,
+      target: formNode.id,
+      kind: 'references',
+      line: unitNode.startLine,
+      provenance: 'heuristic',
+      metadata: { synthesizedBy: 'pascal-form', registeredAt: pasFile },
+    });
+  }
+  return edges;
+}
+
 /**
  * SvelteKit file-convention data flow. A route directory's `+page.svelte` (a
  * `component` node) receives its `data` from the sibling `+page.server.{ts,js}`
@@ -1570,6 +1599,7 @@ export function synthesizeCallbackEdges(queries: QueryBuilder, ctx: ResolutionCo
   const jsxEdges = reactJsxChildEdges(ctx);
   const vueEdges = vueTemplateEdges(ctx);
   const svelteKitEdges = svelteKitLoadEdges(ctx);
+  const pascalEdges = pascalFormEdges(ctx);
   const flutterEdges = flutterBuildEdges(queries, ctx);
   const cppEdges = cppOverrideEdges(queries);
   const ifaceEdges = interfaceOverrideEdges(queries);
@@ -1592,6 +1622,7 @@ export function synthesizeCallbackEdges(queries: QueryBuilder, ctx: ResolutionCo
     ...jsxEdges,
     ...vueEdges,
     ...svelteKitEdges,
+    ...pascalEdges,
     ...flutterEdges,
     ...cppEdges,
     ...ifaceEdges,