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 */ 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 */ 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 $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; } }