diff --git a/scripts/rule-complexity-score.php b/scripts/rule-complexity-score.php new file mode 100644 index 00000000000..febbb8d675c --- /dev/null +++ b/scripts/rule-complexity-score.php @@ -0,0 +1,234 @@ +find([ + __DIR__ . '/../rules', + __DIR__ . '/../vendor/rector/rector-doctrine', + __DIR__ . '/../vendor/rector/rector-phpunit', + __DIR__ . '/../vendor/rector/rector-symfony', + __DIR__ . '/../vendor/rector/rector-downgrade-php', +]); + +$symfonyStyle->writeln(sprintf('Found %d Rector rules', count($rectorClasses))); + +// 2. map rule short name to its fixture count (only rector-src ships tests) +$fixtureCountByRule = resolveFixtureCounts(__DIR__ . '/../rules-tests'); + +// 3. score every rule +$rows = []; + +foreach ($rectorClasses as $rectorClass) { + $reflectionClass = new ReflectionClass($rectorClass); + + $ownLines = countClassLines($reflectionClass); + + $directDeps = resolveDependencyClasses($reflectionClass); + $depsCount = count($directDeps); + + $reachable = []; + foreach ($directDeps as $dependencyClass) { + $reachable = resolveReachable($dependencyClass, $reachable); + } + + $depsLines = 0; + foreach (array_keys($reachable) as $reachableClass) { + $depsLines += countClassLines(new ReflectionClass($reachableClass)); + } + + $score = $ownLines + $depsCount * $depWeight + $depsLines; + + $shortName = $reflectionClass->getShortName(); + + $rows[] = [ + 'short' => $shortName, + 'deps' => $depsCount, + 'own' => $ownLines, + 'deps_loc' => $depsLines, + 'score' => $score, + 'fixtures' => $fixtureCountByRule[$shortName] ?? 0, + ]; +} + +// 4. sort by score, highest first +usort($rows, static fn (array $a, array $b): int => $b['score'] <=> $a['score']); + +if ($top > 0) { + $rows = array_slice($rows, 0, $top); +} + +$tableRows = array_map( + static fn (array $row): array => [ + $row['short'], + $row['deps'], + $row['own'], + $row['deps_loc'], + $row['score'], + $row['fixtures'], + ], + $rows +); + +$symfonyStyle->newLine(); +$symfonyStyle->table( + ['Rule', 'Deps', 'Own LOC', 'Deps LOC', 'Score', 'Fixtures'], + $tableRows +); + +/** + * Lines of the class file, cached per file across calls. + * + * @param ReflectionClass $reflectionClass + */ +function countClassLines(ReflectionClass $reflectionClass): int +{ + static $fileLineCache = []; + + $fileName = $reflectionClass->getFileName(); + if (! is_string($fileName)) { + return 0; + } + + if (isset($fileLineCache[$fileName])) { + return $fileLineCache[$fileName]; + } + + $lines = file($fileName); + $lineCount = $lines === false ? 0 : count($lines); + $fileLineCache[$fileName] = $lineCount; + + return $lineCount; +} + +/** + * Class-string of every non-builtin constructor dependency. + * + * @param ReflectionClass $reflectionClass + * @return class-string[] + */ +function resolveDependencyClasses(ReflectionClass $reflectionClass): array +{ + $constructorReflection = $reflectionClass->getConstructor(); + if (! $constructorReflection instanceof ReflectionMethod) { + return []; + } + + $dependencyClasses = []; + foreach ($constructorReflection->getParameters() as $parameterReflection) { + $type = $parameterReflection->getType(); + if (! $type instanceof ReflectionNamedType) { + continue; + } + + if ($type->isBuiltin()) { + continue; + } + + $typeName = $type->getName(); + + // self/parent/static point back at the class itself, not an injected service + if (in_array($typeName, ['self', 'parent', 'static'], true)) { + continue; + } + + /** @var class-string $typeName */ + $dependencyClasses[] = $typeName; + } + + return $dependencyClasses; +} + +/** + * Walk the constructor graph, collecting every reachable class once. + * + * @param class-string $class + * @param array $reachable + * @return array + */ +function resolveReachable(string $class, array $reachable): array +{ + if (isset($reachable[$class])) { + return $reachable; + } + + $reachable[$class] = true; + + foreach (resolveDependencyClasses(new ReflectionClass($class)) as $dependencyClass) { + $reachable = resolveReachable($dependencyClass, $reachable); + } + + return $reachable; +} + +/** + * Count *.php.inc fixtures grouped by the rule directory that holds them. + * + * @return array + */ +function resolveFixtureCounts(string $testsDirectory): array +{ + if (! is_dir($testsDirectory)) { + return []; + } + + $ruleTestDirectories = new Finder() + ->in($testsDirectory) + ->directories() + ->name('#Rector$#') + ->getIterator(); + + $fixtureCountByRule = []; + foreach ($ruleTestDirectories as $ruleTestDirectory) { + $basename = $ruleTestDirectory->getBasename(); + if ($basename === 'Rector') { + continue; + } + + $fixtureCountByRule[$basename] = new Finder() + ->files() + ->name('*.php.inc') + ->in($ruleTestDirectory->getPathname()) + ->count(); + } + + return $fixtureCountByRule; +}