@sun-asterisk/sunlint
Version:
☀️ SunLint - Multi-language static analysis tool for code quality and security | Sun* Engineering Standards
482 lines (397 loc) • 13.4 kB
JavaScript
/**
* C012 Heuristic Analyzer - Command Query Separation
*
* Uses regex and pattern matching to detect violations of CQS principle:
* - Functions that both modify state and return meaningful values
* - Methods that mix command and query operations
* - Fallback for when AST parsing fails
*/
const fs = require('fs');
const path = require('path');
class C012Analyzer {
constructor() {
this.ruleId = 'C012';
this.ruleName = 'Command Query Separation';
this.description = 'Separate commands (modify state) from queries (return data)';
this.severity = 'warning';
}
async analyze(files, language, config = {}) {
const violations = [];
for (const filePath of files) {
try {
const content = fs.readFileSync(filePath, 'utf8');
const fileViolations = await this.analyzeFile(filePath, content, language, config);
violations.push(...fileViolations);
} catch (error) {
console.warn(`C012 analysis failed for ${filePath}:`, error.message);
}
}
return violations;
}
async analyzeFile(filePath, content, language, config) {
const violations = [];
const lines = content.split('\n');
// Skip non-supported files
if (!this.isSupportedFile(filePath)) {
return violations;
}
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const violation = this.analyzeLine(line, i + 1, lines, filePath);
if (violation) {
violations.push(violation);
}
}
return violations;
}
isSupportedFile(filePath) {
const supportedExtensions = ['.js', '.jsx', '.ts', '.tsx', '.java', '.kt', '.dart', '.cs'];
return supportedExtensions.some(ext => filePath.endsWith(ext));
}
analyzeLine(line, lineNumber, allLines, filePath) {
const trimmedLine = line.trim();
// Skip comments and empty lines
if (!trimmedLine || this.isComment(trimmedLine)) {
return null;
}
// Check for function declarations
const functionInfo = this.extractFunctionInfo(trimmedLine, lineNumber, allLines);
if (!functionInfo) {
return null;
}
// Skip allowed functions
if (this.isAllowedFunction(functionInfo.name)) {
return null;
}
// Extract function body
const functionBody = this.extractFunctionBody(allLines, lineNumber - 1);
if (!functionBody) {
return null;
}
// Check for CQS violation
const violation = this.checkCQSViolation(functionInfo, functionBody, lineNumber, filePath);
return violation;
}
isComment(line) {
return line.startsWith('//') || line.startsWith('/*') || line.startsWith('*');
}
extractFunctionInfo(line, lineNumber, allLines) {
// Function declaration patterns
const patterns = [
// JavaScript/TypeScript functions
/(?:async\s+)?function\s+(\w+)\s*\(/,
/(\w+)\s*:\s*(?:async\s+)?\([^)]*\)\s*=>/,
/(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*=>/,
/(\w+)\s*\([^)]*\)\s*\{/,
// Method definitions
/(?:public|private|protected)?\s*(?:async\s+)?(\w+)\s*\([^)]*\)\s*[:{]/,
/(\w+)\s*\([^)]*\)\s*:\s*\w+\s*\{/, // TypeScript with return type
// Java/Kotlin/C# methods
/(?:public|private|protected)\s+(?:static\s+)?(?:\w+\s+)?(\w+)\s*\([^)]*\)\s*\{/,
// Dart methods
/(?:\w+\s+)?(\w+)\s*\([^)]*\)\s*(?:async\s*)?\{/
];
for (const pattern of patterns) {
const match = line.match(pattern);
if (match) {
return {
name: match[1],
fullLine: line,
lineNumber
};
}
}
return null;
}
extractFunctionBody(lines, startIndex) {
let braceCount = 0;
let body = '';
let inFunction = false;
for (let i = startIndex; i < lines.length; i++) {
const line = lines[i];
// Count braces
for (const char of line) {
if (char === '{') {
braceCount++;
inFunction = true;
} else if (char === '}') {
braceCount--;
}
}
if (inFunction) {
body += line + '\n';
}
// Function complete
if (inFunction && braceCount === 0) {
break;
}
// Safety limit
if (i - startIndex > 100) {
break;
}
}
return body;
}
checkCQSViolation(functionInfo, functionBody, lineNumber, filePath) {
const hasStateModification = this.hasStateModification(functionBody);
const hasReturnValue = this.hasReturnValue(functionBody);
// CQS violation: both command and query behavior
if (hasStateModification && hasReturnValue) {
// NEW: Check if this is an acceptable pattern
if (this.isAcceptablePattern(functionInfo.name, functionBody)) {
return null; // Allow acceptable patterns
}
return {
ruleId: this.ruleId,
file: filePath,
line: lineNumber,
column: functionInfo.fullLine.indexOf(functionInfo.name) + 1,
message: `Function '${functionInfo.name}' violates Command Query Separation: both modifies state and returns value`,
severity: this.severity,
code: functionInfo.fullLine.trim(),
type: 'cqs_violation',
confidence: this.calculateConfidence(hasStateModification, hasReturnValue, functionBody),
suggestion: this.getSuggestion(functionInfo.name)
};
}
return null;
}
hasStateModification(functionBody) {
const modificationPatterns = [
// Assignment operations
/\w+\s*=\s*[^=]/,
/this\.\w+\s*=/,
/\w+\.\w+\s*=/,
// Update operations
/\+\+\w+/,
/\w+\+\+/,
/--\w+/,
/\w+--/,
/\w+\s*\+=\s*/,
/\w+\s*-=\s*/,
/\w+\s*\*=\s*/,
/\w+\s*\/=\s*/,
// Method calls that modify state
/\.(?:push|pop|shift|unshift|splice|sort|reverse|fill)\s*\(/,
/\.(?:set|delete|clear|add|remove|update)\s*\(/,
/\.(?:save|store|persist|insert|append|prepend)\s*\(/,
/\.(?:setState|dispatch|emit|trigger)\s*\(/,
// Array/object mutations
/\[\w+\]\s*=/,
/\{\s*\w+\s*:\s*\w+\s*\}/,
// Property modifications
/\w+\[\w+\]\s*=/,
/Object\.assign\s*\(/,
// State modification keywords
/\b(?:increment|decrement|modify|change|alter|mutate)\s*\(/,
/\bset[A-Z]\w*\s*\(/,
/\bupdate[A-Z]\w*\s*\(/
];
return modificationPatterns.some(pattern => pattern.test(functionBody));
}
hasReturnValue(functionBody) {
const returnPatterns = [
// Return statements with values (excluding simple booleans)
/return\s+(?!true|false|null|undefined|;|\}|$)\w+/,
/return\s+(?!true|false)[^;\}]+[^;\s\}]/,
/return\s+\w+\s*\([^)]*\)/,
/return\s+\w+\.\w+/,
/return\s+new\s+\w+/,
/return\s+\{[^}]+\}/,
/return\s+\[[^\]]+\]/,
/return\s+`[^`]+`/,
/return\s+["'][^"']+["']/,
/return\s+\d+/,
// Arrow function expressions
/=>\s*(?!true|false|null|undefined)\w+/,
/=>\s*\w+\s*\([^)]*\)/,
/=>\s*\w+\.\w+/,
/=>\s*\{[^}]+\}/,
/=>\s*\[[^\]]+\]/,
// Function expressions that return values
/function[^}]*return\s+(?!true|false|null|undefined)\w+/
];
// Exclude simple success/failure indicators
const simpleReturnPatterns = [
/return\s+true\s*;?$/,
/return\s+false\s*;?$/,
/return\s+null\s*;?$/,
/return\s+undefined\s*;?$/,
/return\s*;?$/
];
const hasComplexReturn = returnPatterns.some(pattern => pattern.test(functionBody));
const hasOnlySimpleReturn = simpleReturnPatterns.some(pattern => pattern.test(functionBody));
return hasComplexReturn && !hasOnlySimpleReturn;
}
isAllowedFunction(functionName) {
const allowedPatterns = [
// Constructor and lifecycle
/^constructor$/,
/^componentDidMount$/,
/^componentWillUnmount$/,
/^useEffect$/,
/^init$/,
/^setup$/,
// Test functions
/^test/,
/^it$/,
/^describe$/,
/^before/,
/^after/,
// Getters/setters
/^get\w+$/,
/^set\w+$/,
// Factory/builder patterns (allowed to create and return)
/^create\w+$/,
/^build\w+$/,
/^make\w+$/,
/^new\w+$/,
/Factory$/,
/Builder$/,
// Configuration and initialization
/^configure\w+$/,
/^initialize\w+$/,
/^setup\w+$/,
// Toggle operations (expected to modify and return new state)
/^toggle\w+$/,
/^switch\w+$/,
// Array operations (modify and return)
/^push$/,
/^pop$/,
/^shift$/,
/^unshift$/,
/^splice$/,
// Standard methods
/^toString$/,
/^valueOf$/,
/^render$/,
/^main$/,
// Event handlers
/^on[A-Z]/,
/^handle[A-Z]/,
/Handler$/,
// Utility and helper methods
/Helper$/,
/Util$/,
/Utils$/
];
return allowedPatterns.some(pattern => pattern.test(functionName));
}
calculateConfidence(hasStateModification, hasReturnValue, functionBody) {
let confidence = 0.6;
// Higher confidence for clear violations
if (hasStateModification && hasReturnValue) {
confidence = 0.8;
// Even higher if multiple modification patterns
const modificationCount = this.countModificationPatterns(functionBody);
if (modificationCount > 2) {
confidence = 0.9;
}
// Higher if complex return values
if (this.hasComplexReturnValue(functionBody)) {
confidence = Math.min(0.95, confidence + 0.1);
}
}
return confidence;
}
countModificationPatterns(functionBody) {
const patterns = [
/\w+\s*=/,
/\+\+|\--/,
/\+=|\-=|\*=|\/=/,
/\.(?:push|pop|set|delete|save|update)\s*\(/
];
let count = 0;
patterns.forEach(pattern => {
const matches = functionBody.match(new RegExp(pattern.source, 'g'));
if (matches) {
count += matches.length;
}
});
return count;
}
isAcceptablePattern(functionName, functionBody) {
// NEW: Practical CQS - Allow acceptable patterns per strategy
// 1. CRUD Operations (single operation + return)
const crudPatterns = [
/^(create|insert|add|save|store)\w*$/i,
/^(update|modify|edit|change)\w*$/i,
/^(upsert|merge)\w*$/i,
/^(delete|remove|destroy)\w*$/i
];
if (crudPatterns.some(pattern => pattern.test(functionName))) {
// Check if it's a simple CRUD - single operation
const queryCount = this.countDatabaseOperations(functionBody);
if (queryCount <= 1) {
return true; // Single query + return is acceptable
}
}
// 2. Transaction-based Operations
if (this.isTransactionBased(functionBody)) {
return true; // Multiple operations in transaction are atomic
}
// 3. ORM Standard Patterns
const ormPatterns = [
/^findOrCreate\w*$/i,
/^findAndUpdate\w*$/i,
/^findAndModify\w*$/i,
/^saveAndReturn\w*$/i,
/^selectForUpdate\w*$/i
];
if (ormPatterns.some(pattern => pattern.test(functionName))) {
return true; // Standard ORM patterns including selectForUpdate
}
// 4. Factory patterns (create and return by design)
const factoryPatterns = [
/^(build|construct|generate|produce)\w*$/i,
/^(transform|convert|map)\w*$/i
];
if (factoryPatterns.some(pattern => pattern.test(functionName))) {
return true; // Factory patterns expected to create and return
}
return false; // Not an acceptable pattern - flag as violation
}
countDatabaseOperations(functionBody) {
const dbOperationPatterns = [
/\.(save|insert|create|update|delete|remove)\s*\(/gi,
/\.(find|findOne|findBy|query|execute)\s*\(/gi,
/\.(upsert|merge|replace)\s*\(/gi,
/repository\.\w+\s*\(/gi,
/manager\.\w+\s*\(/gi
];
let count = 0;
dbOperationPatterns.forEach(pattern => {
const matches = functionBody.match(pattern);
if (matches) {
count += matches.length;
}
});
return count;
}
isTransactionBased(functionBody) {
const transactionPatterns = [
/\.transaction\s*\(/gi,
/withTransaction/gi,
/runInTransaction/gi,
/beginTransaction/gi,
/startTransaction/gi,
/manager\.transaction/gi,
/queryRunner\.startTransaction/gi
];
return transactionPatterns.some(pattern => pattern.test(functionBody));
}
hasComplexReturnValue(functionBody) {
const complexReturnPatterns = [
/return\s+\w+\s*\([^)]*\)/, // Function calls
/return\s+new\s+\w+/, // Object creation
/return\s+\{[^}]+\}/, // Object literals
/return\s+\[[^\]]+\]/, // Array literals
/return\s+\w+\.\w+/ // Property access
];
return complexReturnPatterns.some(pattern => pattern.test(functionBody));
}
getSuggestion(functionName) {
return `Split '${functionName}' into separate command (modify state) and query (return data) functions`;
}
}
module.exports = C012Analyzer;