diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-07-21 02:54:18 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-07-23 22:07:16 +0900 |
| commit | 765b7749d31675d3cbf541b146ead1aafd6d6ae9 (patch) | |
| tree | 11f8a07cd9e3d4ef92bb2f75a0811e41b1d928ba /scripts/plugin-class-classifier/src/Lists.php | |
| parent | 770013c7097b6b5c0c49c7fa46d9235c308c4ad5 (diff) | |
| download | php-shirabe-765b7749d31675d3cbf541b146ead1aafd6d6ae9.tar.gz php-shirabe-765b7749d31675d3cbf541b146ead1aafd6d6ae9.tar.zst php-shirabe-765b7749d31675d3cbf541b146ead1aafd6d6ae9.zip | |
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 <noreply@anthropic.com>
Diffstat (limited to 'scripts/plugin-class-classifier/src/Lists.php')
| -rw-r--r-- | scripts/plugin-class-classifier/src/Lists.php | 84 |
1 files changed, 84 insertions, 0 deletions
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 @@ +<?php + +declare(strict_types=1); + +namespace Shirabe\PluginClassifier; + +/** + * The three versioned exception lists. Formats are line-based; `#` starts a + * comment, blank lines are skipped. + * + * two-world.list one namespace prefix (or exact FQCN) per line + * static-state.list "FQCN memo-cache|seed-once|needs-sync" per line + * overrides.list "FQCN category reason..." per line + */ +final class Lists +{ + /** @var list<string> */ + public array $twoWorldPrefixes = []; + + /** @var array<string, string> FQCN => 'memo-cache' | 'seed-once' | 'needs-sync' */ + public array $staticDispositions = []; + + /** @var array<string, array{category: string, reason: string}> */ + 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<string> */ + 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; + } +} |
