From 765b7749d31675d3cbf541b146ead1aafd6d6ae9 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Tue, 21 Jul 2026 02:54:18 +0900 Subject: feat(plugin-class-classifier): add deterministic plugin-boundary classifier Decides, for every composer/composer class, how it is treated at the plugin boundary (rust-proxy / rust-snapshot / contract / two-world / php-native / unsupported) so that upstream updates re-classify new or rewritten classes without re-deriving the design by hand. Rules and category definitions live in docs/dev/plugin-class-classification.md; the tool (PHP + nikic/PHP-Parser) implements them as a reachability closure with direction marks, per-method pure/mutator analysis, and a leaf-first fixed point for unreachable classes, with three small versioned exception lists. Co-Authored-By: Claude Fable 5 --- scripts/plugin-class-classifier/src/Lists.php | 84 +++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 scripts/plugin-class-classifier/src/Lists.php (limited to 'scripts/plugin-class-classifier/src/Lists.php') diff --git a/scripts/plugin-class-classifier/src/Lists.php b/scripts/plugin-class-classifier/src/Lists.php new file mode 100644 index 00000000..7e544a88 --- /dev/null +++ b/scripts/plugin-class-classifier/src/Lists.php @@ -0,0 +1,84 @@ + */ + public array $twoWorldPrefixes = []; + + /** @var array FQCN => 'memo-cache' | 'seed-once' | 'needs-sync' */ + public array $staticDispositions = []; + + /** @var array */ + public array $overrides = []; + + public static function load(string $dir): self + { + $lists = new self(); + + foreach (self::lines("$dir/two-world.list") as $line) { + $lists->twoWorldPrefixes[] = $line; + } + + foreach (self::lines("$dir/static-state.list") as $line) { + $parts = preg_split('/\s+/', $line, 2); + if (count($parts) !== 2 || !in_array($parts[1], ['memo-cache', 'seed-once', 'needs-sync'], true)) { + throw new \RuntimeException("static-state.list: malformed line: $line"); + } + $lists->staticDispositions[$parts[0]] = $parts[1]; + } + + foreach (self::lines("$dir/overrides.list") as $line) { + $parts = preg_split('/\s+/', $line, 3); + if (count($parts) !== 3) { + throw new \RuntimeException("overrides.list: malformed line (need FQCN, category, reason): $line"); + } + if (!in_array($parts[1], Classifier::CATEGORIES, true)) { + throw new \RuntimeException("overrides.list: unknown category {$parts[1]}: $line"); + } + $lists->overrides[$parts[0]] = ['category' => $parts[1], 'reason' => $parts[2]]; + } + + return $lists; + } + + public function isTwoWorld(string $fqcn): bool + { + foreach ($this->twoWorldPrefixes as $prefix) { + if ($fqcn === $prefix || str_starts_with($fqcn, rtrim($prefix, '\\') . '\\')) { + return true; + } + } + + return false; + } + + /** @return list */ + private static function lines(string $path): array + { + if (!is_file($path)) { + throw new \RuntimeException("missing exception list: $path"); + } + $out = []; + foreach (file($path, FILE_IGNORE_NEW_LINES) as $line) { + $line = trim($line); + if ($line === '' || str_starts_with($line, '#')) { + continue; + } + $out[] = $line; + } + + return $out; + } +} -- cgit v1.3.1