What happened
On a procedural / script-style PHP codebase (the kind that organizes code with include/require rather than Composer/PSR-4 namespaces — plenty of legacy apps, plain-PHP sites, and older CMS themes still look like this), codegraph builds an excellent call graph but produces zero file→file dependency edges for include / require / include_once / require_once.
Concretely: a page file foo.php whose first line is include("../loader.php"); yields callers(loader.php) = 0 and no edge of any kind linking foo.php → loader.php. The same holds for require_once. Only namespace use statements become import/dependency edges.
The call graph itself is great here — callers/callees/impact correctly attribute top-level (file-scope) function calls to the file node, which is exactly right for procedural code. This report is only about the missing include/require dependency layer.
Why it matters
For namespaced codebases this is a non-issue: use covers the dependency graph. But for include-based PHP, the include statement is the dependency mechanism — it's how a file gains access to the functions/constants it calls. Without those edges:
impact/callers can't answer "if I change helpers.php, which files that include it (directly or transitively) are affected?" beyond the direct function-call edges.
- The file-level architecture graph is missing its primary structural relationship for this style of code.
Root cause (source)
src/extraction/languages/php.ts:
importTypes: ['namespace_use_declaration'],
importTypes lists only namespace_use_declaration. The PHP tree-sitter grammar represents includes as include_expression, require_expression (and the _once variants), which appear in none of the captured node-type lists (importTypes, callTypes, variableTypes, …), and visitNode only special-cases const_declaration and trait use_declaration. So include/require nodes are simply never visited as dependency edges.
Suggested fix
Capture include_expression / require_expression (+ _once variants) and emit a file→file dependency/import edge, resolving the argument string to a path (relative to the including file / configured roots). Even resolving only static string literals (the common case) would cover the overwhelming majority of real include sites; dynamic includes (include $var) can be skipped.
Repro
- Two files in the same dir:
lib.php: <?php function greet() { return "hi"; }
page.php: <?php require_once("lib.php"); echo greet();
- Index the directory.
callers lib.php → returns 0 (no include edge from page.php).
callees page.php → shows the greet call edge, but no dependency edge to lib.php.
Environment
- codegraph
@colbymchenry/codegraph@0.9.9 (via npx -y)
- Node v24.2.0
- Windows 11 (x64)
Verified on a real ~650-file procedural PHP project: the call graph matched grep ground truth at 100% file-level coverage for a representative helper, while every include/require relationship was absent from the graph.
What happened
On a procedural / script-style PHP codebase (the kind that organizes code with
include/requirerather than Composer/PSR-4 namespaces — plenty of legacy apps, plain-PHP sites, and older CMS themes still look like this), codegraph builds an excellent call graph but produces zero file→file dependency edges forinclude/require/include_once/require_once.Concretely: a page file
foo.phpwhose first line isinclude("../loader.php");yieldscallers(loader.php) = 0and no edge of any kind linkingfoo.php → loader.php. The same holds forrequire_once. Only namespaceusestatements become import/dependency edges.The call graph itself is great here —
callers/callees/impactcorrectly attribute top-level (file-scope) function calls to the file node, which is exactly right for procedural code. This report is only about the missing include/require dependency layer.Why it matters
For namespaced codebases this is a non-issue:
usecovers the dependency graph. But for include-based PHP, the include statement is the dependency mechanism — it's how a file gains access to the functions/constants it calls. Without those edges:impact/callerscan't answer "if I changehelpers.php, which files thatincludeit (directly or transitively) are affected?" beyond the direct function-call edges.Root cause (source)
src/extraction/languages/php.ts:importTypeslists onlynamespace_use_declaration. The PHP tree-sitter grammar represents includes asinclude_expression,require_expression(and the_oncevariants), which appear in none of the captured node-type lists (importTypes,callTypes,variableTypes, …), andvisitNodeonly special-casesconst_declarationand traituse_declaration. So include/require nodes are simply never visited as dependency edges.Suggested fix
Capture
include_expression/require_expression(+_oncevariants) and emit a file→file dependency/import edge, resolving the argument string to a path (relative to the including file / configured roots). Even resolving only static string literals (the common case) would cover the overwhelming majority of real include sites; dynamic includes (include $var) can be skipped.Repro
lib.php:<?php function greet() { return "hi"; }page.php:<?php require_once("lib.php"); echo greet();callers lib.php→ returns 0 (no include edge frompage.php).callees page.php→ shows thegreetcall edge, but no dependency edge tolib.php.Environment
@colbymchenry/codegraph@0.9.9(vianpx -y)Verified on a real ~650-file procedural PHP project: the call graph matched grep ground truth at 100% file-level coverage for a representative helper, while every
include/requirerelationship was absent from the graph.