fix(extraction): map PHP include/require to file→file dependency edges (#660) (#663)

PHP's importTypes only captured namespace_use_declaration, so
include/require(_once) — the dependency mechanism in procedural and
script-style PHP — never produced edges. callers, impact, and trace
missed the entire file-include graph; only namespace `use` became a
dependency edge.

Capture the four include/require expression types and emit file→file
imports edges, reusing the path-based resolution that C/C++ #include
already goes through. Only static string-literal paths are resolved
(relative to the including file); dynamic forms (include $var,
require __DIR__ . '/x', interpolated strings) are skipped.

Include PATHS are distinguished from namespace `use` symbols by shape: a
path contains '/' or '.', which PHP identifiers and FQNs never do. A
path-shaped include that doesn't resolve to a known project file is left
unresolved and does NOT fall back to the symbol name-matcher, which would
otherwise mis-connect "inc/db.php" to an unrelated db.php elsewhere — a
wrong edge is worse than a missing one.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Colby McHenry <me@colbymchenry.com>
This commit is contained in:
Max Hsu
2026-06-08 20:31:15 -04:00
committed by GitHub
co-authored by Claude Opus 4.8 Colby McHenry
parent fd03f31b2c
commit 6e2a24d96a
6 changed files with 295 additions and 3 deletions
+37
View File
@@ -2247,6 +2247,43 @@ use Closure;
expect(names).toContain('Illuminate\\Support\\Str');
expect(names).toContain('Closure');
});
it('should extract include/require (+_once) static paths as imports (#660)', () => {
const code = `<?php
require_once("lib.php");
include 'other.php';
require 'r.php';
include_once("io.php");
`;
const result = extractFromSource('page.php', code);
const names = result.nodes.filter((n) => n.kind === 'import').map((n) => n.name);
expect(names).toContain('lib.php');
expect(names).toContain('other.php');
expect(names).toContain('r.php');
expect(names).toContain('io.php');
});
it('should skip dynamic include/require with no static path (#660)', () => {
const code = `<?php
require_once(__DIR__ . '/dyn.php');
include $file;
include "tpl/{$name}.php";
`;
const result = extractFromSource('page.php', code);
const imports = result.nodes.filter((n) => n.kind === 'import');
expect(imports).toHaveLength(0);
});
it('should extract include alongside namespace use without interference (#660)', () => {
const code = `<?php
use App\\Service\\Mailer;
require_once("bootstrap.php");
`;
const result = extractFromSource('page.php', code);
const names = result.nodes.filter((n) => n.kind === 'import').map((n) => n.name);
expect(names).toContain('App\\Service\\Mailer');
expect(names).toContain('bootstrap.php');
});
});
describe('Ruby imports', () => {