Files
rspade_system/app/RSpade/CodeQuality/Rules/PHP/RequireParentCall_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

180 lines
6.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 overridden methods must call parent implementation when marked with #[AuthParentCall]
*
* When a parent class method has the #[AuthParentCall] attribute, all subclasses that
* override this method MUST call parent::methodName() at some point in their implementation.
* This ensures critical parent functionality is not accidentally skipped.
*/
class RequireParentCall_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'PHP-PARENT-CALL-01';
}
public function get_name(): string
{
return 'RequireParentCall Enforcement';
}
public function get_description(): string
{
return 'Ensures methods marked with #[AuthParentCall] are properly called by overriding subclasses';
}
public function get_file_patterns(): array
{
return ['*.php'];
}
public function get_default_severity(): string
{
return 'critical';
}
/**
* This rule runs during manifest scan to check parent call 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();
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;
}
$class_name = $file_metadata['class'];
// Check both static and instance methods for RequireParentCall attribute
$methods_to_check = [];
if (!empty($file_metadata['public_static_methods'])) {
foreach ($file_metadata['public_static_methods'] as $method_name => $method_data) {
$methods_to_check[$method_name] = $method_data;
}
}
if (!empty($file_metadata['public_instance_methods'])) {
foreach ($file_metadata['public_instance_methods'] as $method_name => $method_data) {
$methods_to_check[$method_name] = $method_data;
}
}
// Check each method for RequireParentCall attribute
foreach ($methods_to_check as $method_name => $method_data) {
// Check if this method has RequireParentCall attribute
if (empty($method_data['attributes']) || !isset($method_data['attributes']['RequireParentCall'])) {
continue;
}
// This method requires parent call - check all subclasses
$this->check_subclasses_for_parent_call($class_name, $method_name, $file_path);
}
}
}
private function check_subclasses_for_parent_call(string $parent_class, string $method_name, string $parent_file): void
{
// Get all subclasses (including abstract ones)
$subclasses = Manifest::php_get_subclasses_of($parent_class, false);
foreach ($subclasses as $subclass_name) {
// Get the subclass metadata
$subclass_data = Manifest::php_get_metadata_by_class($subclass_name);
if (!$subclass_data) {
continue;
}
// Check if subclass overrides this method (in either static or instance methods)
$subclass_method = null;
if (!empty($subclass_data['public_static_methods'][$method_name])) {
$subclass_method = $subclass_data['public_static_methods'][$method_name];
} elseif (!empty($subclass_data['public_instance_methods'][$method_name])) {
$subclass_method = $subclass_data['public_instance_methods'][$method_name];
}
if (!$subclass_method) {
continue;
}
// Check if the overridden method calls parent
if (empty($subclass_method['__calls_parent'])) {
// Get line number if available
$line = 0;
// Get the actual parent FQCN for clearer error message
$parent_fqcn = Manifest::php_get_metadata_by_class($parent_class)['fqcn'] ?? $parent_class;
$subclass_fqcn = $subclass_data['fqcn'] ?? $subclass_name;
$message = "Method {$subclass_name}::{$method_name}() must call parent::{$method_name}() because {$parent_class}::{$method_name}() is marked with #[AuthParentCall]. ";
$message .= "The parent method implements critical functionality that must not be skipped in overriding implementations.";
$suggestion = "Add a call to parent::{$method_name}() in the {$subclass_name}::{$method_name}() implementation. ";
$suggestion .= "This ensures the essential parent logic is executed.";
// Get code snippet if possible
$code_snippet = '';
if (file_exists($subclass_data['file'])) {
$file_contents = file_get_contents($subclass_data['file']);
if (preg_match('/public\s+static\s+function\s+' . preg_quote($method_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]);
}
}
}
$this->add_violation(
$subclass_data['file'],
$line,
$message,
$code_snippet,
$suggestion,
'critical'
);
}
}
}
}