aboutsummaryrefslogtreecommitdiffhomepage
path: root/scripts/plugin-class-classifier/src/Lists.php
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-07-21 02:54:18 +0900
committernsfisis <nsfisis@gmail.com>2026-07-23 22:07:16 +0900
commit765b7749d31675d3cbf541b146ead1aafd6d6ae9 (patch)
tree11f8a07cd9e3d4ef92bb2f75a0811e41b1d928ba /scripts/plugin-class-classifier/src/Lists.php
parent770013c7097b6b5c0c49c7fa46d9235c308c4ad5 (diff)
downloadphp-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.php84
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;
+ }
+}