aboutsummaryrefslogtreecommitdiffhomepage
path: root/scripts/linters/src/Linters/NoBannedUse.php
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-07-21 08:08:15 +0900
committernsfisis <nsfisis@gmail.com>2026-07-21 08:08:45 +0900
commitc899a4675ca90507a420d901ef7fa26d8b285f23 (patch)
tree405b8049c3ac59c65d56792c96f4a8c316bf605b /scripts/linters/src/Linters/NoBannedUse.php
parent6b16a2cd672edcbfeaf7988591db92ed4c594ddc (diff)
downloadphp-shirabe-c899a4675ca90507a420d901ef7fa26d8b285f23.tar.gz
php-shirabe-c899a4675ca90507a420d901ef7fa26d8b285f23.tar.zst
php-shirabe-c899a4675ca90507a420d901ef7fa26d8b285f23.zip
refactor(lint): rewrite structural linters from Ruby to PHP
Fold scripts/lint and scripts/linters/*.rb into a standalone Composer project under scripts/linters/, matching the scripts/plugin-class-classifier/ convention. Uses no external packages, only PHP + Composer autoloading. Verified byte-for-byte identical output against the original Ruby implementation, both on the current repo (all linters pass) and on a synthetic fixture exercising every violation type. Entry point moves from `scripts/lint` to `scripts/linters/lint`.
Diffstat (limited to 'scripts/linters/src/Linters/NoBannedUse.php')
-rw-r--r--scripts/linters/src/Linters/NoBannedUse.php170
1 files changed, 170 insertions, 0 deletions
diff --git a/scripts/linters/src/Linters/NoBannedUse.php b/scripts/linters/src/Linters/NoBannedUse.php
new file mode 100644
index 00000000..a6bda77e
--- /dev/null
+++ b/scripts/linters/src/Linters/NoBannedUse.php
@@ -0,0 +1,170 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Shirabe\Lint\Linters;
+
+use Shirabe\Lint\Linter;
+use Shirabe\Lint\Support\FileFinder;
+use Shirabe\Lint\Support\Paths;
+
+final class NoBannedUse implements Linter
+{
+ private const BANNED_USE_PATHS = [
+ 'anyhow::Result',
+ 'std::any::Any',
+ 'std::cell::RefCell',
+ 'std::io::Read',
+ 'std::io::Write',
+ 'std::process::Command',
+ 'std::rc::Rc',
+ ];
+
+ private const USE_START_RE = '/\A(?:pub(?:\([^)]*\))?\s+)?use\b/';
+
+ public function name(): string
+ {
+ return 'no_banned_use';
+ }
+
+ public function failureIntro(): string
+ {
+ return "Found banned `use` imports.\n"
+ . "These items must always be referenced by their fully-qualified path.\n"
+ . 'For imports to use trait methods, use `as _` (e.g., `use std::io::Write as _;`).';
+ }
+
+ public function check(string $rootDir, array $excludes): array
+ {
+ $errors = [];
+
+ foreach (FileFinder::rustFiles($rootDir) as $path) {
+ $relative = Paths::relativeTo($rootDir, $path);
+ if (in_array($relative, $excludes, true)) {
+ continue;
+ }
+
+ array_push($errors, ...$this->findBannedUses($path, $relative));
+ }
+
+ return $errors;
+ }
+
+ /** @return list<string> */
+ private function findBannedUses(string $path, string $relative): array
+ {
+ $errors = [];
+ $buffer = null;
+ $startIdx = null;
+
+ foreach (file($path) as $idx => $raw) {
+ $code = explode('//', $raw, 2)[0];
+ $stripped = trim($code);
+
+ if ($buffer === null) {
+ if (!preg_match(self::USE_START_RE, $stripped)) {
+ continue;
+ }
+ $buffer = '';
+ $startIdx = $idx;
+ }
+
+ $buffer .= ' ' . $stripped;
+ if (!str_contains($buffer, ';')) {
+ continue;
+ }
+
+ preg_match('/\buse\s+(.*?);/s', $buffer, $m);
+ $tree = $m[1] ?? null;
+
+ foreach ($this->expandUseTree($tree) as $full) {
+ if (!in_array($full, self::BANNED_USE_PATHS, true)) {
+ continue;
+ }
+ $errors[] = "{$relative}:" . ($startIdx + 1) . ": `use {$full}` is banned (fully qualify as `{$full}` instead)";
+ }
+
+ $buffer = null;
+ }
+
+ return array_values(array_unique($errors));
+ }
+
+ /** @return list<string> */
+ private function expandUseTree(?string $tree): array
+ {
+ if ($tree === null) {
+ return [];
+ }
+
+ $tree = trim($tree);
+ $brace = strpos($tree, '{');
+
+ if ($brace === false) {
+ $stripped = $this->stripUseAlias($tree);
+
+ return $stripped === null || $stripped === '' ? [] : [$stripped];
+ }
+
+ $prefix = trim(preg_replace('/::\s*\z/', '', substr($tree, 0, $brace)));
+ $inner = preg_replace('/\}\s*\z/', '', substr($tree, $brace + 1));
+
+ $result = [];
+ foreach ($this->splitTopLevel($inner) as $child) {
+ foreach ($this->expandUseTree($child) as $sub) {
+ if ($sub === '' || $sub === 'self') {
+ $result[] = $prefix;
+ } elseif ($prefix === '') {
+ $result[] = $sub;
+ } else {
+ $result[] = "{$prefix}::{$sub}";
+ }
+ }
+ }
+
+ return $result;
+ }
+
+ private function stripUseAlias(string $segment): ?string
+ {
+ if (preg_match('/\s+as\s+_\s*\z/', $segment)) {
+ return null;
+ }
+
+ return trim(preg_replace('/\s+as\s+\S+\s*\z/', '', $segment));
+ }
+
+ /** @return list<string> */
+ private function splitTopLevel(string $str): array
+ {
+ $parts = [];
+ $current = '';
+ $depth = 0;
+
+ foreach (str_split($str) as $ch) {
+ switch ($ch) {
+ case '{':
+ $depth++;
+ $current .= $ch;
+ break;
+ case '}':
+ $depth--;
+ $current .= $ch;
+ break;
+ case ',':
+ if ($depth === 0) {
+ $parts[] = $current;
+ $current = '';
+ } else {
+ $current .= $ch;
+ }
+ break;
+ default:
+ $current .= $ch;
+ }
+ }
+ $parts[] = $current;
+
+ return array_values(array_filter(array_map('trim', $parts), static fn (string $p): bool => $p !== ''));
+ }
+}