@sun-asterisk/sunlint
Version:
☀️ SunLint - Multi-language static analysis tool for code quality and security | Sun* Engineering Standards
552 lines (483 loc) • 17.3 kB
JavaScript
const fs = require('fs');
const path = require('path');
class C017Analyzer {
constructor() {
this.ruleId = 'C017';
this.ruleName = 'Constructor Logic Limitation';
this.description = 'Không gán logic xử lý vào constructor';
}
async analyze(files, language, options = {}) {
const violations = [];
for (const filePath of files) {
if (options.verbose) {
console.log(`🔍 Running C017 analysis on ${path.basename(filePath)}`);
}
try {
const content = fs.readFileSync(filePath, 'utf8');
const fileViolations = await this.analyzeFile(filePath, content, language, options);
violations.push(...fileViolations);
} catch (error) {
console.warn(`⚠️ Failed to analyze ${filePath}: ${error.message}`);
}
}
return violations;
}
async analyzeFile(filePath, content, language, config) {
switch (language) {
case 'typescript':
case 'javascript':
return this.analyzeTypeScript(filePath, content, config);
default:
return [];
}
}
async analyzeTypeScript(filePath, content, config) {
const violations = [];
const lines = content.split('\n');
// Find constructor blocks and analyze their content
lines.forEach((line, index) => {
const lineNumber = index + 1;
const trimmedLine = line.trim();
// Detect constructor start
if (this.isConstructorStart(trimmedLine)) {
const constructorInfo = this.extractConstructorInfo(lines, index);
// Debug logging to understand boundary detection
if (config?.verbose) {
console.log(`[DEBUG] Constructor found at line ${lineNumber}`);
console.log(`[DEBUG] Constructor body lines: ${constructorInfo.body.length}`);
console.log(`[DEBUG] Constructor content:`, constructorInfo.body.map((l, i) => `${lineNumber + i}: ${l}`));
}
const complexLogic = this.findComplexLogic(constructorInfo.body);
complexLogic.forEach(logic => {
violations.push({
ruleId: this.ruleId,
file: filePath,
line: lineNumber + logic.lineOffset,
column: logic.column,
message: `Constructor contains complex logic: ${logic.description}. Move to initialization methods`,
severity: 'warning',
code: logic.code,
type: logic.type,
confidence: logic.confidence,
suggestion: 'Move complex logic to separate initialization methods or lifecycle hooks'
});
});
}
});
return violations;
}
isConstructorStart(line) {
return line.includes('constructor(') || line.match(/constructor\s*\(/);
}
extractConstructorInfo(lines, startIndex) {
const constructorLines = [];
let braceDepth = 0;
let foundConstructorBrace = false;
let inConstructor = false;
let parenthesesDepth = 0;
let inCallback = false;
for (let i = startIndex; i < lines.length; i++) {
const line = lines[i];
const trimmedLine = line.trim();
// Check if this is the constructor line with opening brace
if (this.isConstructorStart(trimmedLine)) {
inConstructor = true;
constructorLines.push(line);
// Special case: empty constructor () {}
if (trimmedLine.includes(') {}')) {
foundConstructorBrace = true;
braceDepth = 0; // Already closed
break; // Stop immediately for empty constructor
}
// Count braces and parentheses in the constructor line itself
for (const char of line) {
if (char === '{') {
foundConstructorBrace = true;
braceDepth = 1; // Start counting from 1 for the constructor block
} else if (char === '(') {
parenthesesDepth++;
} else if (char === ')') {
parenthesesDepth--;
}
}
} else if (inConstructor && foundConstructorBrace) {
constructorLines.push(line);
// Track callback functions and nested structures
for (const char of line) {
if (char === '(') {
parenthesesDepth++;
// Detect callback patterns: .use(, .then(, .catch(, etc.
if (line.includes('.use(') || line.includes('.then(') ||
line.includes('.catch(') || line.includes('.finally(') ||
line.includes('=>')) {
inCallback = true;
}
} else if (char === ')') {
parenthesesDepth--;
if (parenthesesDepth <= 0) {
inCallback = false;
}
} else if (char === '{') {
braceDepth++;
} else if (char === '}') {
braceDepth--;
}
}
// If we've closed all braces and not in callback, we're done
if (braceDepth === 0 && !inCallback) {
break;
}
} else if (inConstructor) {
// Haven't found the opening brace yet, keep looking
constructorLines.push(line);
// Check for empty constructor pattern: ) {}
if (trimmedLine.includes(') {}')) {
foundConstructorBrace = true;
braceDepth = 0; // Already closed
break; // Stop immediately
}
for (const char of line) {
if (char === '{') {
foundConstructorBrace = true;
braceDepth = 1;
break;
}
}
}
}
for (const char of line) {
if (char === '{') {
foundConstructorBrace = true;
braceDepth = 1;
break;
}
}
}
}
return {
body: constructorLines,
startLine: startIndex
};
}
findComplexLogic(constructorBody) {
const complexLogic = [];
let inCallbackFunction = false;
let callbackDepth = 0;
// Check if this is an empty constructor first
// TEMPORARILY DISABLED for debugging
// if (this.isEmptyConstructor(constructorBody)) {
// return complexLogic; // Return empty array - no violations for empty constructors
// }
constructorBody.forEach((line, index) => {
const trimmedLine = line.trim();
// Skip comments, empty lines, and constructor declaration
if (!trimmedLine ||
trimmedLine.startsWith('//') ||
trimmedLine.startsWith('/*') ||
trimmedLine.startsWith('*') ||
this.isConstructorStart(trimmedLine) ||
trimmedLine === '{' ||
trimmedLine === '}') {
return;
}
// Track callback functions to avoid flagging their content
if (this.isCallbackStart(trimmedLine)) {
inCallbackFunction = true;
callbackDepth = 1;
return;
}
if (inCallbackFunction) {
// Count braces to track callback boundaries
const openBraces = (trimmedLine.match(/\{/g) || []).length;
const closeBraces = (trimmedLine.match(/\}/g) || []).length;
callbackDepth += openBraces - closeBraces;
if (callbackDepth <= 0) {
inCallbackFunction = false;
callbackDepth = 0;
}
return; // Skip analysis inside callbacks
}
// Analyze line for complex logic patterns only if not in callback
const logicType = this.analyzeLineComplexity(trimmedLine);
if (logicType) {
complexLogic.push({
lineOffset: index,
column: line.indexOf(trimmedLine) + 1,
code: trimmedLine,
type: logicType.type,
description: logicType.description,
confidence: logicType.confidence
});
}
});
return complexLogic;
}
isEmptyConstructor(constructorBody) {
// Check if constructor body contains only dependency injection parameters and closing brace
const meaningfulLines = constructorBody.filter(line => {
const trimmed = line.trim();
// Skip empty lines, comments, and constructor declaration
if (!trimmed ||
trimmed.startsWith('//') ||
trimmed.startsWith('/*') ||
trimmed.startsWith('*') ||
this.isConstructorStart(trimmed) ||
trimmed === '{' ||
trimmed === '}' ||
trimmed === ') {}') {
return false;
}
// Skip constructor parameter declarations (NestJS style)
if (trimmed.startsWith('@') || // Decorators like @InjectRepository
trimmed.match(/^\s*(private|public|protected)\s+readonly\s+\w+/) || // DI parameters
trimmed.match(/^\s*(private|public|protected)\s+\w+/) || // Other parameters
trimmed.endsWith(',') || // Parameter continuation
trimmed.endsWith(') {')) { // Constructor parameter closing
return false;
}
// This is a meaningful line (logic, assignments, calls, etc.)
return true;
});
return meaningfulLines.length === 0;
}
isCallbackStart(line) {
// Patterns that indicate callback function starts
const callbackPatterns = [
/\.(use|then|catch|finally|map|filter|forEach|reduce)\s*\(/,
/=>\s*\{/,
/function\s*\(/,
/\(\s*\w+\s*\)\s*=>/,
/interceptors\.(request|response)\.use\(/,
/\.addEventListener\(/,
/setTimeout\(/,
/setInterval\(/
];
return callbackPatterns.some(pattern => pattern.test(line));
}
analyzeLineComplexity(line) {
// Simple property assignments are OK
if (this.isSimpleAssignment(line)) {
return null;
}
// Super calls are OK
if (this.isSuperCall(line)) {
return null;
}
// Parameter assignments are OK
if (this.isParameterAssignment(line)) {
return null;
}
// MobX decorators/observables setup are OK
if (this.isMobXSetup(line)) {
return null;
}
// Allowed method calls are OK
if (this.isAllowedMethodCall(line)) {
return null;
}
// Configuration setup is OK
if (this.isConfigurationSetup(line)) {
return null;
}
// Interceptor setup is OK (axios, etc.)
if (this.isInterceptorSetup(line)) {
return null;
}
// Detect complex logic patterns
const complexPatterns = [
{
pattern: /\bif\s*\(|\belse\s|\bswitch\s*\(/,
type: 'conditional_logic',
description: 'conditional statements (if/else/switch)',
confidence: 0.9
},
{
pattern: /\bfor\s*\(|\bwhile\s*\(|\bdo\s+/,
type: 'loop_logic',
description: 'loops (for/while)',
confidence: 0.95
},
{
pattern: /\btry\s*{|\bcatch\s*\(|\bfinally\s*{/,
type: 'exception_handling',
description: 'exception handling (try/catch/finally)',
confidence: 0.8
},
{
pattern: /\.then\s*\(|\.catch\s*\(|await\s+/,
type: 'async_logic',
description: 'asynchronous operations',
confidence: 0.9
},
{
pattern: /\w+\s*\(\s*[^)]*\)\s*[;{]/,
type: 'method_call',
description: 'complex method calls',
confidence: 0.4 // Reduced confidence to minimize false positives
},
{
pattern: /new\s+\w+\s*\([^)]*\)\s*\./,
type: 'chained_instantiation',
description: 'chained object instantiation',
confidence: 0.8
}
];
for (const { pattern, type, description, confidence } of complexPatterns) {
if (pattern.test(line)) {
return { type, description, confidence };
}
}
// Check for complex expressions
if (this.hasComplexExpression(line)) {
return {
type: 'complex_expression',
description: 'complex expression or calculation',
confidence: 0.6
};
}
return null;
}
isSimpleAssignment(line) {
// Patterns for simple assignments - expanded to catch more legitimate patterns
const simplePatterns = [
/^this\.\w+\s*=\s*[^;]+;?$/, // this.property = value;
/^this\.\w+\s*=\s*\w+;?$/, // this.property = parameter;
/^this\.\w+\s*=\s*(null|undefined|true|false|\d+|'[^']*'|"[^"]*");?$/, // this.property = literal;
/^this\.\w+\s*=\s*this\.\w+\s*\.\s*get\s*\(/, // this.property = this.configService.get(
/^this\.\w+\s*=\s*new\s+\w+\s*\(/, // this.property = new SomeClass(
/^this\.\w+\s*=\s*\w+\s*\.\s*\w+\s*\(/, // this.property = service.method(
// Enhanced patterns for configuration initialization
/^this\.\w+\s*=\s*new\s+\w+Client\s*\(/, // this.s3 = new S3Client(
/^this\.\w+\s*=\s*new\s+\w+\s*\(\s*\{/, // this.prop = new Class({ config })
/^this\.\w+\s*=\s*\w+\s*\.\s*create\s*\(/, // this.prop = Factory.create(
/^this\.\w+\s*=\s*\w+\s*\.\s*getInstance\s*\(/, // this.prop = Service.getInstance(
];
return simplePatterns.some(pattern => pattern.test(line));
}
isConfigurationSetup(line) {
// Check if this is configuration/options object setup
const configPatterns = [
/^\s*\w+:\s*/, // Property definition in object literal
/level:\s*/, // Log level setting
/timestamp:\s*/, // Timestamp configuration
/formatters:\s*/, // Formatter configuration
/mixin:\s*/, // Mixin configuration
/transport:\s*/, // Transport configuration
/options:\s*/, // Options configuration
/target:\s*/, // Target configuration
/baseURL:\s*/, // Axios baseURL
/timeout:\s*/, // Axios timeout
/region:\s*/, // AWS region configuration
/credentials:\s*/, // AWS credentials
/endpoint:\s*/, // API endpoint
/apiVersion:\s*/, // API version
/maxRetries:\s*/, // Retry configuration
/headers:\s*/, // HTTP headers
/defaultHeaders:\s*/, // Default headers
/interceptors:\s*/, // Request/response interceptors
/transformRequest:\s*/, // Request transformation
/transformResponse:\s*/, // Response transformation
/validateStatus:\s*/, // Status validation
/responseType:\s*/, // Response type
/maxContentLength:\s*/, // Content length limit
/ssl:\s*/, // SSL configuration
/auth:\s*/, // Authentication
/withCredentials:\s*/, // CORS credentials
/maxRedirects:\s*/, // Max redirects
];
return configPatterns.some(pattern => pattern.test(line));
}
isInterceptorSetup(line) {
// Check if this is axios interceptor setup (should be allowed in constructor)
const interceptorPatterns = [
/interceptors\.(request|response)\.use/,
/\.use\s*\(/,
/\.then\s*\(/,
/\.catch\s*\(/,
/\.finally\s*\(/,
/=>\s*\{?/,
/=>\s*[^{]/, // Arrow function without braces
/function\s*\(/
];
return interceptorPatterns.some(pattern => pattern.test(line));
}
isSuperCall(line) {
return line.includes('super(') || line.startsWith('super.');
}
isParameterAssignment(line) {
// Check if it's just assigning constructor parameters to properties
return /^this\.\w+\s*=\s*\w+;?$/.test(line);
}
isMobXSetup(line) {
// MobX patterns that are OK in constructor
const mobxPatterns = [
'makeObservable',
'makeAutoObservable',
'observable',
'action',
'computed',
'reaction',
'autorun',
'@observable',
'@action',
'@computed'
];
const lowerLine = line.toLowerCase();
return mobxPatterns.some(pattern =>
lowerLine.includes(pattern.toLowerCase())
);
}
isAllowedMethodCall(line) {
// Method calls that are typically OK in constructors - expanded list
const allowedMethods = [
'makeObservable',
'makeAutoObservable',
'bind',
'addEventListener',
'removeEventListener',
'Object.assign',
'Object.defineProperty',
'console.log',
'console.warn',
// Common initialization patterns
'createLogger',
'initializeLogger',
'setupConfiguration',
'initializeService',
'configure',
'init',
'setup',
// Common service/config patterns
'get',
'getService',
'getInstance',
// Dependency injection patterns
'inject',
'resolve',
// Axios/HTTP setup patterns
'interceptors',
'use',
'defaults',
'timeout',
'baseURL',
'headers'
];
const lowerLine = line.toLowerCase();
return allowedMethods.some(method =>
lowerLine.includes(method.toLowerCase())
);
}
hasComplexExpression(line) {
// Check for complex expressions (multiple operators, complex calculations)
// Made more restrictive to reduce false positives
const complexityIndicators = [
/[+\-*/]\s*[+\-*/]/, // Multiple arithmetic operators
/\?\s*.*\s*:/, // Ternary operators
/&&.*&&|\|\|.*\|\|/, // Multiple logical operators (more restrictive)
/\[[^\]]*\].*\[[^\]]*\]/, // Multiple array accesses
/\.[^.]*\.[^.]*\./ // Triple+ chained property access (more restrictive)
];
return complexityIndicators.some(pattern => pattern.test(line));
}
}
module.exports = new C017Analyzer();