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>
571 lines
22 KiB
JavaScript
Executable File
571 lines
22 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
|
|
/**
|
|
* JavaScript Code Quality RPC Server
|
|
*
|
|
* Combines JavaScript linting (Babel parser) and this-usage analysis (Acorn)
|
|
* into a single persistent server to avoid spawning thousands of Node processes.
|
|
*
|
|
* Usage:
|
|
* Server mode: node js-code-quality-server.js --socket=/path/to/socket
|
|
*
|
|
* RPC Methods:
|
|
* - ping: Health check
|
|
* - lint: Check JavaScript syntax using Babel parser
|
|
* - analyze_this: Analyze 'this' usage patterns using Acorn
|
|
* - shutdown: Graceful server termination
|
|
*
|
|
* @FILENAME-CONVENTION-EXCEPTION - Node.js RPC server script
|
|
*/
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const net = require('net');
|
|
|
|
// Resolve to system/node_modules since that's where packages are installed
|
|
const systemDir = path.resolve(__dirname, '../../../../..');
|
|
const babelParser = require(path.join(systemDir, 'node_modules', '@babel', 'parser'));
|
|
const acorn = require(path.join(systemDir, 'node_modules', 'acorn'));
|
|
const walk = require(path.join(systemDir, 'node_modules', 'acorn-walk'));
|
|
|
|
// Parse command line arguments
|
|
let socketPath = null;
|
|
|
|
for (let i = 2; i < process.argv.length; i++) {
|
|
const arg = process.argv[i];
|
|
if (arg.startsWith('--socket=')) {
|
|
socketPath = arg.substring('--socket='.length);
|
|
}
|
|
}
|
|
|
|
if (!socketPath) {
|
|
console.error('Usage: node js-code-quality-server.js --socket=/path/to/socket');
|
|
process.exit(1);
|
|
}
|
|
|
|
// =============================================================================
|
|
// LINTING LOGIC (Babel Parser)
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Lint JavaScript file for syntax errors using Babel parser
|
|
* @param {string} content - File content
|
|
* @param {string} filePath - File path for error reporting
|
|
* @returns {object|null} Error object or null if no errors
|
|
*/
|
|
function lintFile(content, filePath) {
|
|
try {
|
|
babelParser.parse(content, {
|
|
sourceType: 'module',
|
|
plugins: [
|
|
'decorators-legacy',
|
|
'classProperties',
|
|
'classPrivateProperties',
|
|
'classPrivateMethods',
|
|
'optionalChaining',
|
|
'nullishCoalescingOperator',
|
|
'asyncGenerators',
|
|
'bigInt',
|
|
'dynamicImport',
|
|
'exportDefaultFrom',
|
|
'exportNamespaceFrom',
|
|
'objectRestSpread',
|
|
'topLevelAwait'
|
|
]
|
|
});
|
|
|
|
// No syntax errors
|
|
return null;
|
|
|
|
} catch (error) {
|
|
return {
|
|
message: error.message,
|
|
line: error.loc ? error.loc.line : null,
|
|
column: error.loc ? error.loc.column : null
|
|
};
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// THIS-USAGE ANALYSIS LOGIC (Acorn Parser)
|
|
// =============================================================================
|
|
|
|
// Known jQuery callback methods - used for better remediation messages
|
|
const JQUERY_CALLBACKS = new Set([
|
|
'click', 'dblclick', 'mouseenter', 'mouseleave', 'mousedown', 'mouseup',
|
|
'mousemove', 'mouseover', 'mouseout', 'change', 'submit', 'focus', 'blur',
|
|
'keydown', 'keyup', 'keypress', 'resize', 'scroll', 'select', 'load',
|
|
'on', 'off', 'one', 'each', 'map', 'filter',
|
|
'fadeIn', 'fadeOut', 'slideDown', 'slideUp', 'animate',
|
|
'done', 'fail', 'always', 'then', 'ready', 'hover'
|
|
]);
|
|
|
|
/**
|
|
* Analyze JavaScript file for 'this' usage violations
|
|
* @param {string} content - File content
|
|
* @param {string} filePath - File path for error reporting
|
|
* @returns {object} Result with violations array or error
|
|
*/
|
|
function analyzeThisUsage(content, filePath) {
|
|
const lines = content.split('\n');
|
|
|
|
let ast;
|
|
try {
|
|
ast = acorn.parse(content, {
|
|
ecmaVersion: 2020,
|
|
sourceType: 'module',
|
|
locations: true
|
|
});
|
|
} catch (e) {
|
|
// Parse error - return empty violations
|
|
return { violations: [], error: `Parse error: ${e.message}` };
|
|
}
|
|
|
|
const violations = [];
|
|
const classInfo = new Map();
|
|
|
|
// First pass: identify all classes and their types
|
|
walk.simple(ast, {
|
|
ClassDeclaration(node) {
|
|
const hasStaticInit = node.body.body.some(member =>
|
|
member.static && member.key?.name === 'init'
|
|
);
|
|
classInfo.set(node.id.name, {
|
|
isStatic: hasStaticInit
|
|
});
|
|
}
|
|
});
|
|
|
|
// Helper to check if first line of function has valid pattern
|
|
function checkFirstLinePattern(funcNode) {
|
|
if (!funcNode.body || !funcNode.body.body || funcNode.body.body.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
let checkIndex = 0;
|
|
const firstStmt = funcNode.body.body[0];
|
|
|
|
// Check if first statement is e.preventDefault() or similar
|
|
if (firstStmt.type === 'ExpressionStatement' &&
|
|
firstStmt.expression?.type === 'CallExpression' &&
|
|
firstStmt.expression?.callee?.type === 'MemberExpression' &&
|
|
firstStmt.expression?.callee?.property?.name === 'preventDefault') {
|
|
checkIndex = 1;
|
|
if (funcNode.body.body.length <= 1) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
const targetStmt = funcNode.body.body[checkIndex];
|
|
if (targetStmt.type !== 'VariableDeclaration') {
|
|
return null;
|
|
}
|
|
|
|
const firstDecl = targetStmt.declarations[0];
|
|
if (!firstDecl || !firstDecl.init) {
|
|
return null;
|
|
}
|
|
|
|
const varKind = targetStmt.kind;
|
|
|
|
// Check for 'that = this' pattern
|
|
if (firstDecl.id.name === 'that' &&
|
|
firstDecl.init.type === 'ThisExpression') {
|
|
if (varKind !== 'const') {
|
|
return 'that-pattern-wrong-kind';
|
|
}
|
|
return 'that-pattern';
|
|
}
|
|
|
|
// Check for 'CurrentClass = this' pattern
|
|
if (firstDecl.id.name === 'CurrentClass' &&
|
|
firstDecl.init.type === 'ThisExpression') {
|
|
if (varKind !== 'const') {
|
|
return 'currentclass-pattern-wrong-kind';
|
|
}
|
|
return 'currentclass-pattern';
|
|
}
|
|
|
|
// Check for '$var = $(this)' pattern
|
|
if (firstDecl.id.name.startsWith('$') &&
|
|
firstDecl.init.type === 'CallExpression' &&
|
|
firstDecl.init.callee.name === '$' &&
|
|
firstDecl.init.arguments.length === 1 &&
|
|
firstDecl.init.arguments[0].type === 'ThisExpression') {
|
|
if (varKind !== 'const') {
|
|
return 'jquery-pattern-wrong-kind';
|
|
}
|
|
return 'jquery-pattern';
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// Helper to detect if we're in a jQuery callback
|
|
function isLikelyJQueryCallback(ancestors) {
|
|
for (let i = ancestors.length - 1; i >= 0; i--) {
|
|
const node = ancestors[i];
|
|
if (node.type === 'CallExpression' && node.callee.type === 'MemberExpression') {
|
|
const methodName = node.callee.property.name;
|
|
if (JQUERY_CALLBACKS.has(methodName)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Walk the AST looking for 'this' usage
|
|
walk.ancestor(ast, {
|
|
ThisExpression(node, ancestors) {
|
|
// Skip arrow functions - they inherit 'this'
|
|
for (const ancestor of ancestors) {
|
|
if (ancestor.type === 'ArrowFunctionExpression') {
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Find containing function and class
|
|
let containingFunc = null;
|
|
let containingClass = null;
|
|
let isAnonymousFunc = false;
|
|
let isStaticMethod = false;
|
|
let isConstructor = false;
|
|
let isInstanceMethod = false;
|
|
let hasMethodDefinition = false;
|
|
|
|
// First pass: check if we're in a MethodDefinition
|
|
for (let i = ancestors.length - 1; i >= 0; i--) {
|
|
const ancestor = ancestors[i];
|
|
if (ancestor.type === 'MethodDefinition') {
|
|
hasMethodDefinition = true;
|
|
isStaticMethod = ancestor.static;
|
|
isConstructor = ancestor.kind === 'constructor';
|
|
isInstanceMethod = !ancestor.static && ancestor.kind !== 'constructor';
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Second pass: find function and class
|
|
for (let i = ancestors.length - 1; i >= 0; i--) {
|
|
const ancestor = ancestors[i];
|
|
|
|
if (!containingFunc && (
|
|
ancestor.type === 'FunctionExpression' ||
|
|
ancestor.type === 'FunctionDeclaration'
|
|
)) {
|
|
containingFunc = ancestor;
|
|
isAnonymousFunc = ancestor.type === 'FunctionExpression' && !hasMethodDefinition;
|
|
}
|
|
|
|
if (!containingClass && (
|
|
ancestor.type === 'ClassDeclaration' ||
|
|
ancestor.type === 'ClassExpression'
|
|
)) {
|
|
containingClass = ancestor;
|
|
}
|
|
}
|
|
|
|
if (!containingFunc) {
|
|
return;
|
|
}
|
|
|
|
// Skip constructors and instance methods
|
|
if (isConstructor || isInstanceMethod) {
|
|
return;
|
|
}
|
|
|
|
// Check if this is part of the allowed first-line pattern
|
|
const parent = ancestors[ancestors.length - 2];
|
|
const firstStmt = containingFunc.body?.body?.[0];
|
|
let checkIndex = 0;
|
|
|
|
const hasPreventDefault = firstStmt?.type === 'ExpressionStatement' &&
|
|
firstStmt?.expression?.type === 'CallExpression' &&
|
|
firstStmt?.expression?.callee?.type === 'MemberExpression' &&
|
|
firstStmt?.expression?.callee?.property?.name === 'preventDefault';
|
|
|
|
if (hasPreventDefault) {
|
|
checkIndex = 1;
|
|
}
|
|
|
|
const targetStmt = containingFunc.body?.body?.[checkIndex];
|
|
const isTargetConst = targetStmt?.type === 'VariableDeclaration' && targetStmt?.kind === 'const';
|
|
|
|
// Check if this 'this' is inside $(this) on the first or second line
|
|
if (parent && parent.type === 'CallExpression' && parent.callee?.name === '$') {
|
|
if (isTargetConst &&
|
|
targetStmt?.declarations?.[0]?.init === parent &&
|
|
targetStmt?.declarations?.[0]?.id?.name?.startsWith('$')) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Check if this 'this' is the 'const that = this' in correct position
|
|
if (parent && parent.type === 'VariableDeclarator' &&
|
|
parent.id?.name === 'that' &&
|
|
isTargetConst &&
|
|
targetStmt?.declarations?.[0]?.id?.name === 'that') {
|
|
return;
|
|
}
|
|
|
|
// Check if this 'this' is the 'const CurrentClass = this' in correct position
|
|
if (parent && parent.type === 'VariableDeclarator' &&
|
|
parent.id?.name === 'CurrentClass' &&
|
|
isTargetConst &&
|
|
targetStmt?.declarations?.[0]?.id?.name === 'CurrentClass') {
|
|
return;
|
|
}
|
|
|
|
// Check what pattern is used
|
|
const pattern = checkFirstLinePattern(containingFunc);
|
|
|
|
// Determine the violation and remediation
|
|
let message = '';
|
|
let remediation = '';
|
|
const lineNum = node.loc.start.line;
|
|
const codeSnippet = lines[lineNum - 1].trim();
|
|
const className = containingClass?.id?.name || 'unknown';
|
|
const isJQueryContext = isLikelyJQueryCallback(ancestors);
|
|
|
|
if (isAnonymousFunc) {
|
|
if (!pattern) {
|
|
const firstStmt = containingFunc.body?.body?.[0];
|
|
const hasPreventDefault = firstStmt?.type === 'ExpressionStatement' &&
|
|
firstStmt?.expression?.type === 'CallExpression' &&
|
|
firstStmt?.expression?.callee?.type === 'MemberExpression' &&
|
|
firstStmt?.expression?.callee?.property?.name === 'preventDefault';
|
|
|
|
if (isJQueryContext) {
|
|
message = `'this' in jQuery callback should be aliased for clarity.`;
|
|
if (hasPreventDefault) {
|
|
remediation = `Add 'const $element = $(this);' as the second line (after preventDefault), then use $element instead of 'this'.`;
|
|
} else {
|
|
remediation = `Add 'const $element = $(this);' as the first line of this function, then use $element instead of 'this'.`;
|
|
}
|
|
} else {
|
|
message = `Ambiguous 'this' usage in anonymous function.`;
|
|
if (hasPreventDefault) {
|
|
remediation = `If this is a jQuery callback: Add 'const $element = $(this);' as the second line (after preventDefault).\n` +
|
|
`If this is an instance context: Add 'const that = this;' as the second line.\n` +
|
|
`Then use the aliased variable instead of 'this'.`;
|
|
} else {
|
|
remediation = `If this is a jQuery callback: Add 'const $element = $(this);' as first line.\n` +
|
|
`If this is an instance context: Add 'const that = this;' as first line.\n` +
|
|
`Then use the aliased variable instead of 'this'.`;
|
|
}
|
|
}
|
|
} else if (pattern === 'that-pattern' || pattern === 'jquery-pattern') {
|
|
message = `'this' used after aliasing. Use the aliased variable instead.`;
|
|
let varDeclIndex = 0;
|
|
const firstStmt = containingFunc.body?.body?.[0];
|
|
if (firstStmt?.type === 'ExpressionStatement' &&
|
|
firstStmt?.expression?.callee?.property?.name === 'preventDefault') {
|
|
varDeclIndex = 1;
|
|
}
|
|
const varName = containingFunc.body?.body?.[varDeclIndex]?.declarations?.[0]?.id?.name;
|
|
remediation = pattern === 'jquery-pattern'
|
|
? `You already have 'const ${varName} = $(this)'. Use that variable instead of 'this'.`
|
|
: `You already have 'const that = this'. Use 'that' instead of 'this'.`;
|
|
} else if (pattern === 'that-pattern-wrong-kind') {
|
|
message = `Instance alias must use 'const', not 'let' or 'var'.`;
|
|
remediation = `Change to 'const that = this;' - the instance reference should never be reassigned.`;
|
|
} else if (pattern === 'jquery-pattern-wrong-kind') {
|
|
message = `jQuery element alias must use 'const', not 'let' or 'var'.`;
|
|
remediation = `Change to 'const $element = $(this);' - jQuery element references should never be reassigned.`;
|
|
}
|
|
} else if (isStaticMethod) {
|
|
if (!pattern) {
|
|
message = `Static method in '${className}' should not use naked 'this'.`;
|
|
remediation = `Static methods have two options:\n` +
|
|
`1. If you need the exact class (no polymorphism): Replace 'this' with '${className}'\n` +
|
|
`2. If you need polymorphism (inherited classes): Add 'const CurrentClass = this;' as first line\n` +
|
|
` Then use CurrentClass.name or CurrentClass.property for polymorphic access.\n` +
|
|
`Consider: Does this.name need to work for child classes? If yes, use CurrentClass pattern.`;
|
|
} else if (pattern === 'currentclass-pattern') {
|
|
message = `'this' used after aliasing to CurrentClass. Use 'CurrentClass' instead.`;
|
|
remediation = `You already have 'const CurrentClass = this;'. Use 'CurrentClass' for all access, not naked 'this'.`;
|
|
} else if (pattern === 'currentclass-pattern-wrong-kind') {
|
|
message = `CurrentClass pattern must use 'const', not 'let' or 'var'.`;
|
|
remediation = `Change to 'const CurrentClass = this;' - the CurrentClass reference should never be reassigned.`;
|
|
} else if (pattern === 'jquery-pattern') {
|
|
return;
|
|
} else if (pattern === 'jquery-pattern-wrong-kind') {
|
|
message = `jQuery element alias must use 'const', not 'let' or 'var'.`;
|
|
remediation = `Change to 'const $element = $(this);' - jQuery element references should never be reassigned.`;
|
|
}
|
|
|
|
if (isAnonymousFunc && !pattern) {
|
|
remediation += `\nException: If this is a jQuery callback, add 'const $element = $(this);' as the first line.`;
|
|
}
|
|
}
|
|
|
|
if (message) {
|
|
violations.push({
|
|
line: lineNum,
|
|
message: message,
|
|
codeSnippet: codeSnippet,
|
|
remediation: remediation
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
return { violations: violations };
|
|
}
|
|
|
|
// =============================================================================
|
|
// RPC SERVER
|
|
// =============================================================================
|
|
|
|
// Remove socket if exists
|
|
if (fs.existsSync(socketPath)) {
|
|
fs.unlinkSync(socketPath);
|
|
}
|
|
|
|
function handleRequest(data) {
|
|
try {
|
|
const request = JSON.parse(data);
|
|
|
|
switch (request.method) {
|
|
case 'ping':
|
|
return JSON.stringify({
|
|
id: request.id,
|
|
result: 'pong'
|
|
}) + '\n';
|
|
|
|
case 'lint':
|
|
const lintResults = {};
|
|
for (const file of request.files) {
|
|
try {
|
|
const content = fs.readFileSync(file, 'utf8');
|
|
const error = lintFile(content, file);
|
|
lintResults[file] = {
|
|
status: 'success',
|
|
error: error
|
|
};
|
|
} catch (error) {
|
|
lintResults[file] = {
|
|
status: 'error',
|
|
error: {
|
|
type: 'FileReadError',
|
|
message: error.message
|
|
}
|
|
};
|
|
}
|
|
}
|
|
return JSON.stringify({
|
|
id: request.id,
|
|
results: lintResults
|
|
}) + '\n';
|
|
|
|
case 'analyze_this':
|
|
const thisResults = {};
|
|
for (const file of request.files) {
|
|
try {
|
|
const content = fs.readFileSync(file, 'utf8');
|
|
const result = analyzeThisUsage(content, file);
|
|
thisResults[file] = {
|
|
status: 'success',
|
|
violations: result.violations,
|
|
error: result.error || null
|
|
};
|
|
} catch (error) {
|
|
thisResults[file] = {
|
|
status: 'error',
|
|
error: {
|
|
type: 'FileReadError',
|
|
message: error.message
|
|
}
|
|
};
|
|
}
|
|
}
|
|
return JSON.stringify({
|
|
id: request.id,
|
|
results: thisResults
|
|
}) + '\n';
|
|
|
|
case 'shutdown':
|
|
return JSON.stringify({
|
|
id: request.id,
|
|
result: 'shutting down'
|
|
}) + '\n';
|
|
|
|
default:
|
|
return JSON.stringify({
|
|
id: request.id,
|
|
error: 'Unknown method: ' + request.method
|
|
}) + '\n';
|
|
}
|
|
} catch (error) {
|
|
return JSON.stringify({
|
|
error: 'Invalid JSON request: ' + error.message
|
|
}) + '\n';
|
|
}
|
|
}
|
|
|
|
const server = net.createServer((socket) => {
|
|
let buffer = '';
|
|
|
|
socket.on('data', (data) => {
|
|
buffer += data.toString();
|
|
|
|
let newlineIndex;
|
|
while ((newlineIndex = buffer.indexOf('\n')) !== -1) {
|
|
const line = buffer.substring(0, newlineIndex);
|
|
buffer = buffer.substring(newlineIndex + 1);
|
|
|
|
if (line.trim()) {
|
|
const response = handleRequest(line);
|
|
socket.write(response);
|
|
|
|
try {
|
|
const request = JSON.parse(line);
|
|
if (request.method === 'shutdown') {
|
|
socket.end();
|
|
server.close(() => {
|
|
if (fs.existsSync(socketPath)) {
|
|
fs.unlinkSync(socketPath);
|
|
}
|
|
process.exit(0);
|
|
});
|
|
}
|
|
} catch (e) {
|
|
// Ignore
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
socket.on('error', (err) => {
|
|
console.error('Socket error:', err);
|
|
});
|
|
});
|
|
|
|
server.listen(socketPath, () => {
|
|
console.log('JS Code Quality RPC server listening on ' + socketPath);
|
|
});
|
|
|
|
server.on('error', (err) => {
|
|
console.error('Server error:', err);
|
|
process.exit(1);
|
|
});
|
|
|
|
process.on('SIGTERM', () => {
|
|
server.close(() => {
|
|
if (fs.existsSync(socketPath)) {
|
|
fs.unlinkSync(socketPath);
|
|
}
|
|
process.exit(0);
|
|
});
|
|
});
|
|
|
|
process.on('SIGINT', () => {
|
|
server.close(() => {
|
|
if (fs.existsSync(socketPath)) {
|
|
fs.unlinkSync(socketPath);
|
|
}
|
|
process.exit(0);
|
|
});
|
|
});
|