feat: Divi manifests, adapter register and boundary test

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-30 00:46:16 +02:00
co-authored by Claude Sonnet 5
parent 905bb8c2f3
commit 5e6d5ff3e1
9 changed files with 543 additions and 0 deletions
@@ -0,0 +1,112 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Release;
defined('ABSPATH') || exit;
/**
* Finds where PHP code touches Divi (DiviBoundaryTest, spec §3): calls of et_* functions, ET\… / ET_Core… /
* ET_Builder… classes (also as strings), ET_* constants (also as strings) and the strings 'et_…'/'_et_…' (post
* types, meta keys, options, function names for function_exists()). Comments are ignored; method calls named
* et_* ($x->et_a(), Foo::et_b()) are not Divi's.
*/
final class DiviTouches
{
private const IGNORED = [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT];
private const NOT_A_CALL = [
T_OBJECT_OPERATOR, T_NULLSAFE_OBJECT_OPERATOR, T_DOUBLE_COLON, T_FUNCTION, T_NEW, T_CONST,
];
/** @return list<array{kind: string, name: string, line: int}> */
public static function inFile(string $path): array
{
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Local PHP source, not a remote request.
$source = file_get_contents($path);
return is_string($source) ? self::inSource($source) : [];
}
/** @return list<array{kind: string, name: string, line: int}> */
public static function inSource(string $source): array
{
$tokens = array_values(array_filter(
token_get_all($source),
static fn (mixed $token): bool => ! is_array($token) || ! in_array($token[0], self::IGNORED, true)
));
$touches = [];
foreach ($tokens as $i => $token) {
if (! is_array($token)) {
continue;
}
$touch = match ($token[0]) {
T_STRING, T_NAME_FULLY_QUALIFIED => self::name($tokens, $i, ltrim($token[1], '\\')),
T_NAME_QUALIFIED => str_starts_with($token[1], 'ET\\') ? ['class', $token[1]] : null,
T_CONSTANT_ENCAPSED_STRING => self::literal($token[1]),
default => null,
};
if ($touch !== null) {
$touches[] = ['kind' => $touch[0], 'name' => $touch[1], 'line' => (int) $token[2]];
}
}
return $touches;
}
/** Whether inSource() reports $name when the code uses it (DiviBoundaryTest's unused-entry check). */
public static function detects(string $name): bool
{
return self::classify($name) !== null || preg_match('/^_?et_[a-z0-9_]*$/', $name) === 1;
}
/**
* @param array<int, mixed> $tokens
* @return array{0: string, 1: string}|null
*/
private static function name(array $tokens, int $i, string $name): ?array
{
$classified = self::classify($name);
if ($classified !== null) {
return $classified;
}
if (preg_match('/^et_[a-z0-9_]*$/', $name) !== 1 || ($tokens[$i + 1] ?? null) !== '(') {
return null;
}
$previous = $tokens[$i - 1] ?? null;
return is_array($previous) && in_array($previous[0], self::NOT_A_CALL, true) ? null : ['function', $name];
}
/** @return array{0: string, 1: string}|null */
private static function literal(string $text): ?array
{
$value = substr($text, 1, -1);
if ($text[0] === "'") {
$value = str_replace(['\\\\', "\\'"], ['\\', "'"], $value);
}
$value = ltrim($value, '\\');
$classified = self::classify($value);
if ($classified !== null) {
return $classified;
}
return preg_match('/^_?et_[a-z0-9_]*$/', $value) === 1 ? ['string', $value] : null;
}
/** @return array{0: string, 1: string}|null A Divi class or constant name. */
private static function classify(string $name): ?array
{
if (
preg_match('/^ET\\\\[A-Za-z0-9_\\\\]+$/', $name) === 1
|| preg_match('/^ET_(?:Core|Builder)[A-Za-z0-9_]*$/', $name) === 1
) {
return ['class', $name];
}
return preg_match('/^ET_[A-Z0-9_]+$/', $name) === 1 ? ['constant', $name] : null;
}
}