Files
rspade_system/app/RSpade/CodeQuality/Rules/Manifest/Monoprogenic_CodeQualityRule.php
root f02a04f37a Framework pull: auto-revert use statement changes before update
Class override system: .upstream file handling and restore logic
Php_Fixer: Redirect use statements to correct manifest FQCN
Fix: Only match PHP files in __find_class_fqcn_in_manifest
Complete Php_Fixer use statement redirection implementation (checkpoint 2)
WIP: Php_Fixer use statement redirection for class overrides (checkpoint)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-10 07:04:05 +00:00

230 lines
8.1 KiB
PHP
Executable File

<?php
namespace App\RSpade\CodeQuality\Rules\Manifest;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
/**
* MonoprogenicRule - Enforces single generation of concrete descendants
*
* Classes marked with #[Monoprogenic] attribute can only have one generation
* of concrete (non-abstract) children. This prevents inheritance ambiguity
* in reflection-based operations.
*
* Valid patterns:
* - Monoprogenic (abstract) -> Child (concrete)
* - Monoprogenic (abstract) -> Child (abstract) -> Grandchild (concrete)
*
* Invalid patterns:
* - Monoprogenic (abstract) -> Child (concrete) -> Grandchild (concrete)
*
* This rule prevents situations where concrete classes extend other concrete
* classes in a Monoprogenic hierarchy, which would create ambiguity about
* which class should handle operations discovered through reflection (such as
* routing, command discovery, or other framework operations).
*/
class Monoprogenic_CodeQualityRule extends CodeQualityRule_Abstract
{
/**
* Get the unique rule identifier
*/
public function get_id(): string
{
return 'MANIFEST-MONO-01';
}
/**
* Get human-readable rule name
*/
public function get_name(): string
{
return 'Monoprogenic Inheritance Validation';
}
/**
* Get rule description
*/
public function get_description(): string
{
return 'Enforces that #[Monoprogenic] classes only have one generation of concrete descendants';
}
/**
* Get file patterns this rule applies to
*
* Note: This returns PHP files, but the rule actually checks relationships
* across the entire manifest, not individual files
*/
public function get_file_patterns(): array
{
return ['*.php'];
}
/**
* This rule runs during manifest scan to validate inheritance patterns
*/
public function is_called_during_manifest_scan(): bool
{
return true;
}
/**
* Check the manifest for Monoprogenic violations
*
* This method is called once per file during manifest scan, but we only
* need to check the entire manifest once. We use a static flag to ensure
* the check only runs once per manifest build.
*/
public function check(string $file_path, string $contents, array $metadata = []): void
{
static $already_checked = false;
// Only check once per manifest build
if ($already_checked) {
return;
}
$already_checked = true;
// Get all manifest files
$files = \App\RSpade\Core\Manifest\Manifest::get_all();
if (empty($files)) {
return;
}
// Step 1: Find all classes with Monoprogenic attribute
$monoprogenic_classes = [];
foreach ($files as $file => $file_metadata) {
// Skip .upstream files - they are framework files overridden by rsx/
$extension = $file_metadata['extension'] ?? '';
if ($extension === 'php.upstream') {
continue;
}
if (isset($file_metadata['attributes']['Monoprogenic'])) {
$fqcn = $file_metadata['fqcn'] ?? null;
if ($fqcn) {
$monoprogenic_classes[$fqcn] = $file;
}
}
}
if (empty($monoprogenic_classes)) {
return;
}
// Step 2: For each Monoprogenic class, check all its descendants
foreach ($monoprogenic_classes as $monoprogenic_class => $monoprogenic_file) {
$this->check_descendants($monoprogenic_class, $monoprogenic_file, $files);
}
}
/**
* Check all descendants of a Monoprogenic class for violations
*/
private function check_descendants(string $monoprogenic_class, string $monoprogenic_file, array $files): void
{
// Find all classes that extend from any Monoprogenic class
foreach ($files as $file => $metadata) {
// Skip .upstream files - they are framework files overridden by rsx/
$extension = $metadata['extension'] ?? '';
if ($extension === 'php.upstream') {
continue;
}
$fqcn = $metadata['fqcn'] ?? null;
if (!$fqcn || !class_exists($fqcn)) {
continue;
}
// Check if this class is a subclass of the Monoprogenic class
if (!is_subclass_of($fqcn, $monoprogenic_class)) {
continue;
}
// Check if the class is abstract
if (\App\RSpade\Core\Manifest\Manifest::php_is_abstract($fqcn)) {
continue; // Abstract classes are allowed at any level
}
// This is a concrete class that extends from Monoprogenic
// Check if its direct parent is also concrete (violation)
// Get the parent class name from manifest data
$class_metadata = \App\RSpade\Core\Manifest\Manifest::php_get_metadata_by_fqcn($fqcn);
$parent_class_name = $class_metadata['extends'] ?? null;
if (!$parent_class_name) {
continue;
}
// If parent is the Monoprogenic class itself, that's fine
if ($parent_class_name === $monoprogenic_class) {
continue;
}
// Check if parent is abstract
if (!\App\RSpade\Core\Manifest\Manifest::php_is_abstract($parent_class_name)) {
// VIOLATION: Concrete class extending another concrete class in Monoprogenic hierarchy
$line_number = $this->find_class_line($file, $metadata['class'] ?? '');
$this->add_violation(
$file,
$line_number,
sprintf(
"Class '%s' violates Monoprogenic inheritance pattern.\n\n" .
"This class is a concrete (non-abstract) class that extends another concrete class '%s'.\n" .
"The base class '%s' has the #[Monoprogenic] attribute, which means concrete descendants " .
"can only extend from it through abstract intermediary classes.\n\n" .
"Monoprogenic classes are used in reflection-based operations (like route discovery or " .
"command registration). Having concrete classes extend other concrete classes creates " .
"ambiguity about which class should handle the discovered operations.",
$metadata['class'] ?? $fqcn,
$this->get_short_name($parent_class_name),
$this->get_short_name($monoprogenic_class)
),
"class " . ($metadata['class'] ?? '') . " extends",
sprintf(
"To fix this violation, choose one of these approaches:\n" .
"1. Make the parent class '%s' abstract\n" .
"2. Make this class '%s' extend directly from an abstract class\n" .
"3. Remove the concrete inheritance chain by not extending '%s'",
$this->get_short_name($parent_class_name),
$metadata['class'] ?? '',
$this->get_short_name($parent_class_name)
),
'critical'
);
}
}
}
/**
* Find the line number where a class is declared
*/
private function find_class_line(string $file_path, string $class_name): int
{
$absolute_path = base_path($file_path);
if (!file_exists($absolute_path)) {
return 1;
}
$contents = file_get_contents($absolute_path);
$lines = explode("\n", $contents);
foreach ($lines as $index => $line) {
if (preg_match('/^\s*(?:abstract\s+)?class\s+' . preg_quote($class_name) . '\b/', $line)) {
return $index + 1;
}
}
return 1;
}
/**
* Get short class name from FQCN
*/
private function get_short_name(string $fqcn): string
{
$parts = explode('\\', $fqcn);
return end($parts);
}
}