Files
rspade_system/app/RSpade/CodeQuality/Rules/Manifest/InstanceMethods_CodeQualityRule.php
root 84ca3dfe42 Fix code quality violations and rename select input components
Move small tasks from wishlist to todo, update npm packages
Replace #[Auth] attributes with manual auth checks and code quality rule
Remove on_jqhtml_ready lifecycle method from framework
Complete ACL system with 100-based role indexing and /dev/acl tester
WIP: ACL system implementation with debug instrumentation
Convert rsx:check JS linting to RPC socket server
Clean up docs and fix $id→$sid in man pages, remove SSR/FPC feature
Reorganize wishlists: priority order, mark sublayouts complete, add email
Update model_fetch docs: mark MVP complete, fix enum docs, reorganize
Comprehensive documentation overhaul: clarity, compression, and critical rules
Convert Contacts/Projects CRUD to Model.fetch() and add fetch_or_null()
Add JS ORM relationship lazy-loading and fetch array handling
Add JS ORM relationship fetching and CRUD documentation
Fix ORM hydration and add IDE resolution for Base_* model stubs
Rename Json_Tree_Component to JS_Tree_Debug_Component and move to framework
Enhance JS ORM infrastructure and add Json_Tree class name badges

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-23 21:39:43 +00:00

390 lines
15 KiB
PHP
Executable File

<?php
namespace App\RSpade\CodeQuality\Rules\Manifest;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
/**
* InstanceMethodsRule - Enforces static-only classes unless marked Instantiatable
*
* By default, classes should use static methods only (namespace pattern).
* Classes can have instance methods only if:
* 1. The class itself has #[Instantiatable] attribute (PHP) or @Instantiatable decorator (JS)
* 2. Any ancestor class has the Instantiatable attribute/decorator
*
* This enforces the framework's static-first architecture while allowing
* instance methods for specific use cases (UI components, ORM models, etc.)
*/
class InstanceMethods_CodeQualityRule extends CodeQualityRule_Abstract
{
/**
* Get the unique rule identifier
*/
public function get_id(): string
{
return 'MANIFEST-INST-01';
}
/**
* Get human-readable rule name
*/
public function get_name(): string
{
return 'Instance Methods Validation';
}
/**
* Get rule description
*/
public function get_description(): string
{
return 'Enforces static-only classes unless class or ancestor has Instantiatable attribute/decorator';
}
/**
* Get file patterns this rule applies to
*/
public function get_file_patterns(): array
{
return ['*.php', '*.js'];
}
/**
* This rule runs during manifest scan
*/
public function is_called_during_manifest_scan(): bool
{
return true;
}
/**
* Check the manifest for instance method violations
*/
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;
}
// Check PHP classes
$this->check_php_classes($files);
// Check JavaScript classes
$this->check_javascript_classes($files);
// Check JS model monoprogenic enforcement
$this->check_js_model_monoprogenic($files);
}
/**
* HACK #3 - JS Model Monoprogenic Enforcement
*
* Ensures that JS model classes cannot be extended. Only Base_ stubs can be extended,
* not the PHP model class names directly. This prevents broken inheritance chains.
*
* Monoprogenic = can only produce one level of offspring
*/
private function check_js_model_monoprogenic(array $files): void
{
foreach ($files as $file => $metadata) {
// Skip if not a JavaScript class
if (!isset($metadata['class']) || ($metadata['extension'] ?? '') !== 'js') {
continue;
}
// Skip if no parent class
$parent_class = $metadata['extends'] ?? null;
if (!$parent_class) {
continue;
}
// Check if parent is a PHP model class name (not Base_)
if (\App\RSpade\Core\Manifest\Manifest::is_php_model_class($parent_class)) {
$this->add_violation(
$file,
1,
"Class '{$metadata['class']}' extends PHP model '{$parent_class}' directly. " .
"JS model classes are monoprogenic - they cannot be extended further.",
"extends {$parent_class}",
"JavaScript model classes must extend the Base_ stub instead:\n" .
" - Change: class {$metadata['class']} extends Base_{$parent_class}\n" .
" - The Base_ stub is auto-generated and properly inherits from Rsx_Js_Model",
'critical'
);
}
}
}
/**
* Check PHP classes for instance method violations
*/
private function check_php_classes(array $files): void
{
foreach ($files as $file => $metadata) {
// Skip if not a PHP class
if (!isset($metadata['class']) || !isset($metadata['fqcn'])) {
continue;
}
// Skip if no public instance methods
if (empty($metadata['public_instance_methods'])) {
continue;
}
// Check if this class is allowed to have instance methods
if ($this->is_php_class_instantiatable($metadata['fqcn'], $files)) {
continue;
}
// Found violation - class has instance methods but is not instantiatable
// Build remediation text based on whether class has parent
$parent_class = $metadata['extends'] ?? null;
$is_framework_file = str_starts_with($file, 'app/RSpade/');
if ($parent_class) {
$suggestion = "RSpade discourages creating classes solely to perform operations (utility/service pattern). " .
"Classes should be static unless each instance represents a distinct entity in the system. " .
"Recommended fixes:\n" .
"1. Convert all methods to static if this is a utility/service class\n" .
"2. If instances truly represent distinct objects (components, models, etc.):\n" .
" - STRONGLY RECOMMENDED: Add #[Instantiatable] to the parent class '{$parent_class}'\n" .
" - Only add #[Instantiatable] to this class if '{$parent_class}' should NOT be instantiatable";
// Add note for framework files
if ($is_framework_file) {
$suggestion .= "\n - NOTE: If '{$parent_class}' is a Laravel/vendor class, add #[Instantiatable] to THIS class instead";
}
} else {
$suggestion = "RSpade discourages creating classes solely to perform operations (utility/service pattern). " .
"Classes should be static unless each instance represents a distinct entity in the system. " .
"Recommended fixes:\n" .
"1. Convert all methods to static if this is a utility/service class\n" .
"2. If instances truly represent distinct objects (components, models, etc.):\n" .
" - Add #[Instantiatable] to this class";
}
foreach ($metadata['public_instance_methods'] as $method_name => $method_info) {
$line = $method_info['line'] ?? 1;
$this->add_violation(
$file,
$line,
"Instance method '{$method_name}' found in class '{$metadata['class']}'. RSpade classes should use static methods unless instances represent distinct objects (like UI components or ORM records).",
"public function {$method_name}(...)",
$suggestion,
'medium'
);
}
}
}
/**
* Check JavaScript classes for instance method violations
*/
private function check_javascript_classes(array $files): void
{
foreach ($files as $file => $metadata) {
// Skip if not a JavaScript class
if (!isset($metadata['class']) || ($metadata['extension'] ?? '') !== 'js') {
continue;
}
// Skip if no public instance methods
if (empty($metadata['public_instance_methods'])) {
continue;
}
// Check if this class is allowed to have instance methods
if ($this->is_js_class_instantiatable($metadata['class'], $files)) {
continue;
}
// Found violation - class has instance methods but is not instantiatable
// Build remediation text based on whether class has parent
$parent_class = $metadata['extends'] ?? null;
if ($parent_class) {
$suggestion = "RSpade discourages creating classes solely to perform operations (utility/service pattern). " .
"Classes should be static unless each instance represents a distinct entity in the system. " .
"Recommended fixes:\n" .
"1. Convert all methods to static if this is a utility/service class\n" .
"2. If instances truly represent distinct objects (components, models, etc.):\n" .
" - STRONGLY RECOMMENDED: Add @Instantiatable to the parent class '{$parent_class}' JSDoc\n" .
" - Only add @Instantiatable to this class's JSDoc if '{$parent_class}' should NOT be instantiatable";
} else {
$suggestion = "RSpade discourages creating classes solely to perform operations (utility/service pattern). " .
"Classes should be static unless each instance represents a distinct entity in the system. " .
"Recommended fixes:\n" .
"1. Convert all methods to static if this is a utility/service class\n" .
"2. If instances truly represent distinct objects (components, models, etc.):\n" .
" - Add @Instantiatable to this class's JSDoc";
}
foreach ($metadata['public_instance_methods'] as $method_name => $method_info) {
$line = $method_info['line'] ?? 1;
$this->add_violation(
$file,
$line,
"Instance method '{$method_name}' found in class '{$metadata['class']}'. RSpade classes should use static methods unless instances represent distinct objects (like UI components or ORM records).",
"{$method_name}(...)",
$suggestion,
'medium'
);
}
}
}
/**
* Check if a PHP class is instantiatable (has Instantiatable attribute in ancestry)
*/
private function is_php_class_instantiatable(string $fqcn, array $files): bool
{
// Check the class itself
try {
$metadata = \App\RSpade\Core\Manifest\Manifest::php_get_metadata_by_fqcn($fqcn);
if (isset($metadata['attributes']['Instantiatable'])) {
return true;
}
// Walk up the parent chain using manifest data
$current_fqcn = $fqcn;
$checked = []; // Prevent infinite loops
while (true) {
if (isset($checked[$current_fqcn])) {
break; // Already checked this class
}
$checked[$current_fqcn] = true;
try {
$current_metadata = \App\RSpade\Core\Manifest\Manifest::php_get_metadata_by_fqcn($current_fqcn);
$parent_class = $current_metadata['extends'] ?? null;
if (!$parent_class) {
break; // No more parents
}
// Get parent FQCN if we only have simple name
$parent_fqcn = $current_metadata['extends_fqcn'] ?? $parent_class;
// Check if parent has Instantiatable
try {
$parent_metadata = \App\RSpade\Core\Manifest\Manifest::php_get_metadata_by_fqcn($parent_fqcn);
if (isset($parent_metadata['attributes']['Instantiatable'])) {
return true;
}
$current_fqcn = $parent_fqcn; // Continue up the chain
} catch (\Exception $e) {
// Parent not in manifest, stop here
break;
}
} catch (\Exception $e) {
// Can't get metadata, stop
break;
}
}
} catch (\Exception $e) {
// Class not in manifest
return false;
}
return false;
}
/**
* Check if a JavaScript class is instantiatable (has Instantiatable decorator in ancestry)
*/
private function is_js_class_instantiatable(string $class_name, array $files): bool
{
// HACK #2 - JS Model instantiatable bypass: If this is a PHP model class name or
// a Base_ stub for a PHP model, it's automatically instantiatable because model
// stubs are generated with @Instantiatable during bundle compilation.
$model_check_name = $class_name;
if (str_starts_with($class_name, 'Base_')) {
$model_check_name = substr($class_name, 5); // Remove "Base_" prefix
}
if (\App\RSpade\Core\Manifest\Manifest::is_php_model_class($model_check_name)) {
return true;
}
// Find the class metadata
$class_metadata = null;
foreach ($files as $file => $metadata) {
if (($metadata['class'] ?? '') === $class_name && ($metadata['extension'] ?? '') === 'js') {
$class_metadata = $metadata;
break;
}
}
if (!$class_metadata) {
return false;
}
// Check the class itself
// Decorators are in compact format: [[name, [args]], ...]
if (isset($class_metadata['decorators'])) {
foreach ($class_metadata['decorators'] as $decorator) {
if (($decorator[0] ?? '') === 'Instantiatable') {
return true;
}
}
}
// Walk up the parent chain using manifest data
$current_class = $class_name;
$checked = []; // Prevent infinite loops
while (true) {
if (isset($checked[$current_class])) {
break; // Already checked this class
}
$checked[$current_class] = true;
// Find current class metadata
$current_metadata = null;
foreach ($files as $file => $metadata) {
if (($metadata['class'] ?? '') === $current_class && ($metadata['extension'] ?? '') === 'js') {
$current_metadata = $metadata;
break;
}
}
if (!$current_metadata) {
break; // Class not found
}
$parent_class = $current_metadata['extends'] ?? null;
if (!$parent_class) {
break; // No more parents
}
// Check if parent has Instantiatable
foreach ($files as $file => $metadata) {
if (($metadata['class'] ?? '') === $parent_class && ($metadata['extension'] ?? '') === 'js') {
// Check parent decorators in compact format
if (isset($metadata['decorators'])) {
foreach ($metadata['decorators'] as $decorator) {
if (($decorator[0] ?? '') === 'Instantiatable') {
return true;
}
}
}
$current_class = $parent_class; // Continue up the chain
break;
}
}
}
return false;
}
}