feat(kernel): R7b PHP walker — php module, tree-sitter-php 0.24.2 bump, php default-routed (#1380)

Fourth and final R7b batch-2 port, checklist-first recipe
(docs/design/php-kernel-port-checklist.md).

Grammar bump first, validated standalone with the diff ENUMERATED + CLASSIFIED
(unlike rust/ruby the php bump is NOT graph-neutral): tree-sitter-php ^0.22
(tree-sitter-wasms, 2023) → v0.24.2, the full HTML-interleaving `php` variant
(the walker calls LANGUAGE_PHP, never PHP_ONLY) — crate pinned =0.24.2, wasm
built from tag 5b5627f's checked-in php/src/parser.c + scanner.c + shared
common/scanner.h (all sha-matched against the crates.io tarball, ABI 14→15).
Old-vs-new full-init diffs decompose completely into: (1) the anonymous_class
wrapper shape (anon-class nodes/methods re-shape — 2,532 rows), (2) grouped
nested-clause skip (absent in the gate repos, fixture-pinned), (3) 32
formerly-erroring files parsing clean (monolog Level.php, symfony
Request/Response with 8.4 property hooks), (4) a survey-missed category found
at gate time: the 8.4 parenthesis-free `new X()->m()` chaining misparse fix
(86 garbage instantiates refs disappear, precision-positive), plus resolution
RIPPLE proven mechanically (every remaining ref-table flip pairs 1:1 with a
resolved edge on the opposite side; node rows byte-stable outside 1/3/4).

Walker (java.rs chassis + the php specifics) preserves bug-for-bug: the
visitNode hook (const_declaration at ANY scope → bare `constant` nodes, values
never walked; trait-use → implements refs WITH filePath via the ruby port's
REF_FLAG_FILE_PATH wire slot), FIRST-namespace whole-file scoping (braced
namespaces scope nothing; namespaced files DROP top-level const value-ref
targets), the import trio (single/aliased/grouped incl. the nested-clause
skip, include/require static-literal-only, `Foo\Bar::Baz` use refs), the
call-encoding zoo (DOT-joined scoped calls, `this->prop.m` #1251 encoding,
`Cls::factory().m` fluent with inner args dropped, nullsafe `?->` emitting
nothing, unsuppressed literal receivers), interface multi-extends
first-base-only drop, anon-class methods as file-level functions (top) or
vanishing (in-body), property type-hints emitting no field refs, the
final-modifier-as-type signature quirk, HOF-gated string callables
(skipGate) + array callables, and the `name`-node value-ref reader.

Gates: sweeps 0-diff monolog 217/217, laravel-framework 3007/3008, symfony
10726/10737 (13,950 files byte-parity; 12 deferrals = exactly the predicted
genuinely-broken fixtures, ≈0–0.1%); full-init dumps byte-identical ×3
(16.1k/354.2k/702.8k lines); kernel-php-parity suite (torture + drupal
.module + leading-HTML fixtures, CRLF variants, wire-flag pin, defer) + php
grammar-parity row; full suite 2,622 green ×2 under CODEGRAPH_KERNEL_EXPECT=1.
DEFAULT_ROUTED += php (13 languages).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-07-20 16:08:22 -05:00
committed by GitHub
co-authored by Claude Fable 5
parent 1909931238
commit a6c62d77df
18 changed files with 2934 additions and 9 deletions
@@ -0,0 +1,12 @@
<html><body>
<h1>Leading HTML keeps absolute rows</h1>
<?php
function html_helper(): void
{
html_call();
}
?>
<p>interleaved text</p>
<?= html_echo() ?>
</body></html>
@@ -0,0 +1,11 @@
<?php
const MODULE_MAX = 25;
/**
* @Implements hook_cron().
*/
function torture_cron() {
$v = MODULE_MAX;
other_module_call();
}
@@ -0,0 +1,219 @@
<?php
declare(strict_types=1);
namespace App\Services;
use App\Contracts\Logger;
use App\Contracts\Cache as CacheAlias;
use Countable;
use function App\Helpers\format_id;
use const App\Config\MAX_TRIES;
use App\Models\{User, Post as PostAlias, Sub\Deep};
require 'lib/plain.php';
require_once('lib/parens.php');
include 'lib/inc.php';
include_once 'lib/inc_once.php';
require __DIR__ . '/dynamic.php';
const TOP_LEVEL_MAX = 10;
// non-ASCII before a symbol: café ünïcode line
function top_helper(?Logger $log, User|PostAlias $u, Logger&Countable $lc, (Foo&Bar)|Baz $dnf, \App\Models\User $qual, string $s, int ...$rest): UserModel
{
top_body_call();
return new UserModel();
}
/** Doc for Documented. */
#[Registry(param: Logger::class)]
class Documented extends BaseThing implements HasColor, \JsonSerializable
{
use SoftDeletes, Notify\Deeper;
use ConflictA, ConflictB {
ConflictA::greet insteadof ConflictB;
ConflictB::greet as protected greetB;
}
const MULTI_A = 1, MULTI_B = 2;
final public const int TYPED_MAX = 5;
public ?Logger $logger, $fallback;
private CacheAlias|string $union;
protected static iterable $registry;
public readonly int $count;
var $legacy;
final public Foo $finalTyped;
public function __construct(private Logger $promoted, protected string $name = 'x', ICache $plain = new NullMailer())
{
}
/** Doc over attribute. */
#[Route('/x')]
public function withAttr(): void
{
}
public static function make(): static
{
return new static();
}
public function selfRet(): self
{
return $this;
}
public function nullableRet(): ?Logger
{
return null;
}
public function unionRet(): Foo|Bar
{
return new Foo();
}
protected function callsZoo($x, User $u, $obj, $var, $cls, $arr, $a)
{
helper();
\App\Helpers\format_id(1);
App\Helpers\other(2);
$x->m1();
$this->m2();
$this->prop->m3();
$this->a->b->m4();
$obj->prop->m5();
UserModel::query();
self::sHelper();
static::sHelper();
parent::pHelper();
$var::vm();
\Qual\Cls::qm();
UserModel::factory($x)->where('a');
$this->factory($x)->go();
foo()->fluent();
$a?->maybe()->chained();
"chain"->upper();
$fn = 'x';
$fn();
($x)('arg');
strlen(...);
$this->m2(...);
Cls::sm(...);
new UserModel();
new \App\Models\User();
new Models\User(1);
new static();
new self();
new parent();
new $cls();
$anon = new class extends BaseAnon implements IAnon {
public function anonMethod(): void
{
inner_anon_call();
}
};
new Widget(make_arg());
$m = match ($x) {
1 => one_case(),
default => other_case(),
};
$$x = 5;
$interp = "{$this->x} and $u prefix";
$here = <<<EOT
heredoc {$this->y} text
EOT;
$now = <<<'EOT'
nowdoc plain
EOT;
echo SomeCls::CONST_READ;
$clsName = UserModel::class;
$propRead = UserModel::$conn;
$rel = self::REL_CONST;
$qualRead = \Qual\Cls::QCONST;
$suit = Suit::Hearts;
}
public function nester($arr, $x): void
{
function innerNamed(): void
{
inner_call();
}
if (!class_exists('Poly')) {
class Poly
{
const POLY_MAX = 3;
public function pm(): void
{
poly_call();
}
}
}
$c = function () use (&$x) {
closure_call();
};
$a = fn ($v) => arrow_call($v);
usort($arr, 'cmp_items');
array_map('App\Svc\namespaced_fn', $arr);
call_user_func([$this, 'm2']);
call_user_func([UserModel::class, 'sm']);
call_user_func(['Cls', 'sm']);
register_shutdown_function('Cls::shutdown');
$x->map('not_captured');
plain_call('not_a_hof_string');
}
public function reader(): int
{
$sum = MULTI_A + TYPED_MAX;
$s = "interp TYPED_MAX read: {$this->x} MULTI_B";
$varOccurrence = $MULTI_A;
return $sum + self::TYPED_MAX;
}
}
interface Shape extends Base1, Base2, \Qual\Base3
{
public function area(): float;
const SHAPE_KIND = 'geo';
}
trait SoftDeletes
{
const TRAIT_CONST = 1;
public function restore(): void
{
$this->doRestore();
}
}
enum Suit: string implements HasColor
{
case Hearts = 'H';
case Spades = 'S';
const ENUM_MAX = 4;
public function color(): string
{
return enum_color($this);
}
}
enum Pure
{
case A;
case B;
}
abstract class AbstractBase
{
abstract protected function hook(): void;
}