Files
rspade_system/app/RSpade/CodeQuality/Rules/PHP/RequireStaticProperty_CodeQualityRule.php
root f6fac6c4bc Fix bin/publish: copy docs.dist from project root
Fix bin/publish: use correct .env path for rspade_system
Fix bin/publish script: prevent grep exit code 1 from terminating script

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-21 02:08:33 +00:00

203 lines
7.6 KiB
PHP
Executable File

<?php
namespace App\RSpade\CodeQuality\Rules\PHP;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
use App\RSpade\Core\Manifest\Manifest;
/**
* Enforces that concrete classes must define static properties that their abstract parent declares without values
*
* When an abstract class declares a static property without assigning a value (e.g., public static $enums;),
* all concrete subclasses that directly extend this abstract class MUST define that property.
* This ensures critical static properties are not accidentally omitted in implementations.
*/
class RequireStaticProperty_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'PHP-STATIC-PROP-01';
}
public function get_name(): string
{
return 'RequireStaticProperty Enforcement';
}
public function get_description(): string
{
return 'Ensures concrete classes define static properties required by their abstract parent classes';
}
public function get_file_patterns(): array
{
return ['*.php'];
}
public function get_default_severity(): string
{
return 'critical';
}
/**
* This rule runs during manifest scan to check static property requirements
*/
public function is_called_during_manifest_scan(): bool
{
return true;
}
/**
* Main check method - processes all PHP classes from manifest
*/
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Only process during manifest-time when we have all files
static $already_run = false;
if ($already_run) {
return;
}
// On the first PHP file, process all files
if (!empty($metadata) && $metadata['extension'] === 'php') {
$this->process_all_files();
$already_run = true;
}
}
private function process_all_files(): void
{
// Get all PHP classes from manifest
$files = Manifest::get_all();
// First pass: Find all abstract classes with static properties without values
$abstract_requirements = [];
foreach ($files as $file_path => $file_metadata) {
// Skip non-PHP files
if (($file_metadata['extension'] ?? '') !== 'php') {
continue;
}
// Skip files without classes
if (empty($file_metadata['class'])) {
continue;
}
// Only process abstract classes
if (empty($file_metadata['abstract']) || !$file_metadata['abstract']) {
continue;
}
$class_name = $file_metadata['class'];
// Check for static properties without values
if (!empty($file_metadata['static_properties'])) {
foreach ($file_metadata['static_properties'] as $property_name => $property_info) {
// If property has no value, it's a requirement for subclasses
if (!$property_info['has_value']) {
if (!isset($abstract_requirements[$class_name])) {
$abstract_requirements[$class_name] = [];
}
$abstract_requirements[$class_name][$property_name] = [
'comment' => $property_info['comment'],
'visibility' => $property_info['visibility'],
'file' => $file_path,
];
}
}
}
}
// Second pass: Check concrete classes that directly extend abstract classes
foreach ($files as $file_path => $file_metadata) {
// Skip non-PHP files
if (($file_metadata['extension'] ?? '') !== 'php') {
continue;
}
// Skip files without classes
if (empty($file_metadata['class'])) {
continue;
}
// Skip abstract classes - we only check concrete classes
if (!empty($file_metadata['abstract']) && $file_metadata['abstract']) {
continue;
}
$class_name = $file_metadata['class'];
// Check if this class extends an abstract class
if (empty($file_metadata['extends'])) {
continue;
}
$parent_class = $file_metadata['extends'];
// Check if the parent class is abstract and has requirements
if (!isset($abstract_requirements[$parent_class])) {
continue;
}
// Get the parent metadata to verify it's actually abstract
$parent_metadata = Manifest::php_get_metadata_by_class($parent_class);
if (!$parent_metadata || empty($parent_metadata['abstract'])) {
continue;
}
// Check each required static property
foreach ($abstract_requirements[$parent_class] as $property_name => $requirement) {
// Check if the concrete class defines this static property
$property_defined = false;
// Check in the concrete class's static properties
if (!empty($file_metadata['static_properties']) && isset($file_metadata['static_properties'][$property_name])) {
$property_defined = true;
}
// If property is not defined, report violation
if (!$property_defined) {
$line = 0;
$code_snippet = '';
// Try to find the class declaration line
if (file_exists($file_path)) {
$file_contents = file_get_contents($file_path);
if (preg_match('/class\s+' . preg_quote($class_name, '/') . '\s/m', $file_contents, $matches, PREG_OFFSET_CAPTURE)) {
$offset = $matches[0][1];
$line = substr_count(substr($file_contents, 0, $offset), "\n") + 1;
$lines = explode("\n", $file_contents);
if ($line > 0 && $line <= count($lines)) {
$code_snippet = trim($lines[$line - 1]);
}
}
}
// Build the error message
$message = "Concrete class {$class_name} must define static property \${$property_name} as required by abstract parent class {$parent_class}. ";
// Add comment documentation if available
if ($requirement['comment'] !== true && !empty($requirement['comment'])) {
$message .= "Property documentation: \"{$requirement['comment']}\". ";
}
// Build the suggestion
$suggestion = "Add '{$requirement['visibility']} static \${$property_name};' to class {$class_name}. ";
$suggestion .= "Alternatively, if this property should not be required for all subclasses, ";
$suggestion .= "modify the parent class {$parent_class} to assign a default value: ";
$suggestion .= "'{$requirement['visibility']} static \${$property_name} = null;' to exclude subclasses from this requirement.";
$this->add_violation(
$file_path,
$line,
$message,
$code_snippet,
$suggestion,
'critical'
);
}
}
}
}
}