Files
rspade_system/app/RSpade/CodeQuality/Rules/JavaScript/JqhtmlDataInCreate_CodeQualityRule.php
root 9ebcc359ae Fix code quality violations and enhance ROUTE-EXISTS-01 rule
Implement JQHTML function cache ID system and fix bundle compilation
Implement underscore prefix for system tables
Fix JS syntax linter to support decorators and grant exception to Task system
SPA: Update planning docs and wishlists with remaining features
SPA: Document Navigation API abandonment and future enhancements
Implement SPA browser integration with History API (Phase 1)
Convert contacts view page to SPA action
Convert clients pages to SPA actions and document conversion procedure
SPA: Merge GET parameters and update documentation
Implement SPA route URL generation in JavaScript and PHP
Implement SPA bootstrap controller architecture
Add SPA routing manual page (rsx:man spa)
Add SPA routing documentation to CLAUDE.md
Phase 4 Complete: Client-side SPA routing implementation
Update get_routes() consumers for unified route structure
Complete SPA Phase 3: PHP-side route type detection and is_spa flag
Restore unified routes structure and Manifest_Query class
Refactor route indexing and add SPA infrastructure
Phase 3 Complete: SPA route registration in manifest
Implement SPA Phase 2: Extract router code and test decorators
Rename Jqhtml_Component to Component and complete SPA foundation setup

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-19 17:48:15 +00:00

259 lines
7.9 KiB
PHP
Executable File

<?php
namespace App\RSpade\CodeQuality\Rules\JavaScript;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
class JqhtmlDataInCreate_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'JS-JQHTML-01';
}
public function get_name(): string
{
return 'Jqhtml Component this.data in on_create() Check';
}
public function get_description(): string
{
return 'Ensures this.data is not used in on_create() method of Jqhtml components';
}
public function get_file_patterns(): array
{
return ['*.js'];
}
public function get_default_severity(): string
{
return 'high';
}
/**
* Check for improper this.data usage in on_create() methods of Jqhtml components
*/
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Skip vendor and node_modules
if (str_contains($file_path, '/vendor/') || str_contains($file_path, '/node_modules/')) {
return;
}
// Skip CodeQuality directory
if (str_contains($file_path, '/CodeQuality/')) {
return;
}
// Get violations from AST parser (with caching)
$violations = $this->parse_with_acorn($file_path);
if (empty($violations)) {
return;
}
// Process violations
foreach ($violations as $violation) {
$line_number = $violation['line'];
$class_name = $violation['className'] ?? 'unknown';
$code_snippet = $violation['codeSnippet'] ?? 'this.data';
$this->add_violation(
$file_path,
$line_number,
"Jqhtml Component Error: 'this.data' used in on_create() method of class '{$class_name}'. " .
"The 'this.data' property is only available during on_load() and later lifecycle steps. " .
"It is used to store data fetched from AJAX or other async operations.",
$code_snippet,
"Use 'this.args' instead to access the parameters passed to the component at creation time. " .
"The args contain attributes from the component's invocation in templates or JavaScript. " .
"Example: Change 'this.data.initial_value' to 'this.args.initial_value'.",
'high'
);
}
}
/**
* Parse JavaScript file with acorn AST parser
* Results are cached based on file modification time
*/
protected function parse_with_acorn(string $file_path): array
{
// Create cache directory if needed
$cache_dir = storage_path('rsx-tmp/persistent/code-quality-jqhtml-data');
if (!is_dir($cache_dir)) {
mkdir($cache_dir, 0777, true);
}
// Generate cache key based on file path and mtime
$file_mtime = filemtime($file_path);
$file_size = filesize($file_path);
$cache_key = md5($file_path . ':jqhtml-data');
$cache_file = $cache_dir . '/' . $cache_key . '.json';
// Check if cached result exists and is valid
if (file_exists($cache_file)) {
$cache_data = json_decode(file_get_contents($cache_file), true);
if ($cache_data &&
isset($cache_data['mtime']) && $cache_data['mtime'] == $file_mtime &&
isset($cache_data['size']) && $cache_data['size'] == $file_size) {
// Cache is valid
return $cache_data['violations'] ?? [];
}
}
// Create parser script if it doesn't exist
$parser_script = storage_path('rsx-tmp/persistent/parse-jqhtml-data.js');
if (!file_exists($parser_script)) {
$this->create_parser_script($parser_script);
}
// Run parser
$command = sprintf(
'node %s %s 2>&1',
escapeshellarg($parser_script),
escapeshellarg($file_path)
);
$output = shell_exec($command);
if (!$output) {
return [];
}
$result = json_decode($output, true);
if (!$result || !isset($result['violations'])) {
// Parser error - don't cache
return [];
}
// Cache the result
$cache_data = [
'mtime' => $file_mtime,
'size' => $file_size,
'violations' => $result['violations']
];
file_put_contents($cache_file, json_encode($cache_data));
return $result['violations'];
}
/**
* Create the Node.js parser script
*/
protected function create_parser_script(string $script_path): void
{
$dir = dirname($script_path);
if (!is_dir($dir)) {
mkdir($dir, 0777, true);
}
$script_content = <<<'JAVASCRIPT'
#!/usr/bin/env node
const fs = require('fs');
const acorn = require('acorn');
const walk = require('acorn-walk');
// Classes that are Jqhtml components
const JQHTML_COMPONENTS = new Set([
'Component', '_Base_Jqhtml_Component', 'Component'
]);
function analyzeFile(filePath) {
const code = fs.readFileSync(filePath, 'utf8');
const lines = code.split('\n');
let ast;
try {
ast = acorn.parse(code, {
ecmaVersion: 2020,
sourceType: 'module',
locations: true
});
} catch (e) {
// Parse error - return empty violations
console.log(JSON.stringify({ violations: [] }));
return;
}
const violations = [];
let currentClass = null;
let inOnCreate = false;
// Helper to check if a class extends Component
function isJqhtmlComponent(extendsClass) {
if (!extendsClass) return false;
return JQHTML_COMPONENTS.has(extendsClass) ||
extendsClass.includes('Component') ||
extendsClass.includes('Jqhtml');
}
// Walk the AST
walk.simple(ast, {
ClassDeclaration(node) {
currentClass = {
name: node.id.name,
extends: node.superClass?.name,
isJqhtml: isJqhtmlComponent(node.superClass?.name)
};
},
ClassExpression(node) {
currentClass = {
name: node.id?.name || 'anonymous',
extends: node.superClass?.name,
isJqhtml: isJqhtmlComponent(node.superClass?.name)
};
},
MethodDefinition(node) {
// Check if this is on_create method
if (node.key.name === 'on_create' && currentClass?.isJqhtml) {
inOnCreate = true;
// Walk the method body looking for this.data
walk.simple(node.value.body, {
MemberExpression(memberNode) {
// Check for this.data pattern
if (memberNode.object.type === 'ThisExpression' &&
memberNode.property.name === 'data') {
// Found this.data in on_create
const lineContent = lines[memberNode.loc.start.line - 1] || '';
violations.push({
line: memberNode.loc.start.line,
column: memberNode.loc.start.column,
className: currentClass.name,
codeSnippet: lineContent.trim()
});
}
}
});
inOnCreate = false;
}
}
});
console.log(JSON.stringify({ violations }));
}
// Main
if (process.argv.length < 3) {
console.error('Usage: node parse-jqhtml-data.js <file-path>');
process.exit(1);
}
try {
analyzeFile(process.argv[2]);
} catch (e) {
console.error('Error:', e.message);
console.log(JSON.stringify({ violations: [] }));
}
JAVASCRIPT;
file_put_contents($script_path, $script_content);
chmod($script_path, 0755);
}
}