@sun-asterisk/sunlint
Version:
☀️ SunLint - Multi-language static analysis tool for code quality and security | Sun* Engineering Standards
1,268 lines (1,056 loc) • 42.6 kB
JavaScript
const { SyntaxKind } = require('ts-morph');
/**
* C019 System Log Analyzer - Simplified Version
*
* Focus Areas:
* 1. Đúng chỗ, đúng level (không bàn chuyện message/cause/fields)
* 2. Thiếu hay thừa log (ở những điểm bắt buộc phải có / nên không có)
*/
class C019SystemLogAnalyzer {
constructor(semanticEngine = null) {
this.semanticEngine = semanticEngine;
this.verbose = false;
// Configuration for system-level logging rules
this.config = {
layerClassifier: {
controller: ['controller', 'route', 'handler', 'api', 'endpoint'],
job: ['job', 'worker', 'cron', 'task', 'queue', 'processor'],
service: ['service', 'business', 'domain', 'logic', 'usecase'],
infra: ['client', 'adapter', 'gateway', 'repository', 'dao', 'external']
},
requiredLogEvents: {
'http_5xx_boundary': {
level: 'error',
confidence: 0.9,
message: 'HTTP 5xx responses must have error log at boundary',
suggestion: 'Add error log before returning 5xx status'
},
'retry_exhausted': {
level: 'error',
confidence: 0.8,
message: 'Retry exhaustion must be logged as error',
suggestion: 'Add error log when all retry attempts fail'
}
},
overusedLogPatterns: {
'hot_path_over_logging': {
threshold: 8, // Max logs per function (increased for business logic)
confidence: 0.5, // Lower confidence for less strict enforcement
message: 'Too many log statements in hot path function',
suggestion: 'Reduce logging frequency or use conditional logging'
},
'loop_over_logging': {
threshold: 2, // Max logs per loop
confidence: 0.8,
message: 'Logging inside loops can impact performance',
suggestion: 'Move logs outside loop or use sampling'
}
},
missingLogPatterns: {
'auth_failure_silent': {
confidence: 0.9,
message: 'Authentication failures should be logged for security',
suggestion: 'Add warn/error log for failed authentication attempts'
},
'payment_transaction_silent': {
confidence: 0.9,
message: 'Payment transactions should be logged for audit',
suggestion: 'Add info log for payment processing events'
}
},
redundancyPatterns: {
'duplicate_log_events': {
maxDistance: 10, // Lines between similar logs
confidence: 0.7,
message: 'Duplicate log events detected',
suggestion: 'Consolidate similar log statements'
}
},
distributedPatterns: {
'external_call_silent': {
confidence: 0.7, // Reduced confidence for better precision
message: 'External service calls should be logged for monitoring',
suggestion: 'Add logs for external API/service interactions or use centralized logging'
}
},
wrongLevelPatterns: {
'missing_data_error': {
expectedLevel: 'warn',
confidence: 0.6,
message: 'Missing/invalid data should use warn level',
suggestion: 'Use warn for expected validation failures'
},
'retry_attempt_error': {
expectedLevel: 'warn',
confidence: 0.8,
message: 'Individual retry attempts should use warn level',
suggestion: 'Use warn for retry attempts, error only when exhausted'
}
}
};
}
async initialize(semanticEngine = null) {
if (semanticEngine) {
this.semanticEngine = semanticEngine;
}
this.verbose = semanticEngine?.verbose || false;
}
async analyzeFileBasic(filePath, options = {}) {
const violations = [];
try {
let sourceFile = this.semanticEngine.project.getSourceFile(filePath);
if (!sourceFile) {
sourceFile = this.semanticEngine.project.addSourceFileAtPath(filePath);
}
if (!sourceFile) {
sourceFile = this.semanticEngine.project.createSourceFile(filePath, '');
}
if (!sourceFile) {
throw new Error(`Could not load or create source file: ${filePath}`);
}
if (this.verbose) {
console.log(`[DEBUG] 🎯 C019: Using comprehensive system-level analysis for ${filePath.split('/').pop()}`);
}
// Skip test files - logs in tests have no production value
if (this.isTestFile(filePath)) {
if (this.verbose) {
console.log(`[DEBUG] ❌ Skipping test file: ${filePath}`);
}
return [];
}
// Skip client-side files - client logs have limited operational value
if (this.isClientSideFile(filePath, sourceFile)) {
if (this.verbose) {
console.log(`[DEBUG] ❌ Skipping client-side file: ${filePath}`);
}
return [];
}
// Classify file layer
const layer = this.classifyFileLayer(filePath, sourceFile);
// Find logging events and patterns
const logCalls = this.findLogCalls(sourceFile);
const httpReturns = this.findHttpStatusReturns(sourceFile);
const retryPatterns = this.findRetryPatterns(sourceFile);
if (this.verbose) {
console.log(`[DEBUG] 🔍 C019-System: Analyzing logging patterns in ${filePath.split('/').pop()}`);
}
// Phase 1: Analyze must-have logs
violations.push(...this.analyzeRequiredLogs(filePath, sourceFile, layer, {
logCalls, httpReturns, retryPatterns
}));
// Phase 1: Analyze wrong level usage
violations.push(...this.analyzeWrongLevelUsage(filePath, sourceFile, layer, {
logCalls, httpReturns, retryPatterns
}));
// Phase 2: Analyze overused logs
violations.push(...this.analyzeOverusedLogs(filePath, sourceFile, layer, {
logCalls
}));
// Phase 2: Analyze missing critical logs
violations.push(...this.analyzeMissingCriticalLogs(filePath, sourceFile, layer, {
logCalls, httpReturns
}));
// Phase 2: Analyze log redundancy
violations.push(...this.analyzeLogRedundancy(filePath, sourceFile, layer, {
logCalls
}));
// Phase 3: Only essential distributed logging
violations.push(...this.analyzeDistributedPatterns(filePath, sourceFile, layer, {
logCalls, httpReturns
}));
if (this.verbose) {
console.log(`[DEBUG] 🔍 C019-System: Found ${violations.length} system-level violations`);
}
return violations;
} catch (error) {
if (this.verbose) {
console.error(`[DEBUG] ❌ C019-System: Analysis error: ${error.message}`);
}
throw error;
}
}
// ===== FILE FILTERING =====
isTestFile(filePath) {
const testPatterns = [
/\.test\./i, /\.spec\./i, /__tests__/i, /__test__/i,
/test\//i, /tests\//i, /spec\//i, /specs\//i,
/\.test$/i, /\.spec$/i, /mock/i, /fixture/i
];
return testPatterns.some(pattern => pattern.test(filePath));
}
isClientSideFile(filePath, sourceFile) {
// API routes are server-side even in frontend projects
if (/\/api\/.*\/route\./i.test(filePath)) {
return false;
}
if (this.verbose) {
console.log(`[DEBUG] 🔍 Checking client-side for: ${filePath}`);
}
const hasUseClient = sourceFile.getFullText().includes("'use client'") ||
sourceFile.getFullText().includes('"use client"');
const isReactComponent = /component/i.test(filePath) ||
/\.tsx?$/.test(filePath) && sourceFile.getFullText().includes('React');
const clientSidePaths = [
/\/components\//i, /\/pages\//i,
/\/hooks\//i, /\/context\//i, /\/providers\//i
];
const serverSidePatterns = [
/\/api\//i, /\/server\//i, /\/backend\//i,
/\/utils\/.*(?:server|api|request)/i,
/\/lib\/.*(?:thunk|api|server)/i,
/middleware\./i, /route\./i
];
const isServerSide = serverSidePatterns.some(pattern => pattern.test(filePath));
const isClientPath = clientSidePaths.some(pattern => pattern.test(filePath));
if (this.verbose) {
console.log(`[DEBUG] 📊 Analysis for ${filePath}:`);
console.log(`[DEBUG] - hasUseClient: ${hasUseClient}`);
console.log(`[DEBUG] - isReactComponent: ${isReactComponent}`);
console.log(`[DEBUG] - isServerSide: ${isServerSide}`);
console.log(`[DEBUG] - isClientPath: ${isClientPath}`);
}
if (isServerSide) {
if (this.verbose) {
console.log(`[DEBUG] ✅ Keeping server-side file: ${filePath}`);
}
return false;
}
const shouldExclude = hasUseClient || (isReactComponent && isClientPath);
if (this.verbose) {
console.log(`[DEBUG] ${shouldExclude ? '❌ Excluding' : '✅ Keeping'} file: ${filePath}`);
}
return shouldExclude;
}
classifyFileLayer(filePath, sourceFile) {
const lowerPath = filePath.toLowerCase();
const fileContent = sourceFile.getFullText().toLowerCase();
for (const [layer, patterns] of Object.entries(this.config.layerClassifier)) {
if (patterns.some(pattern =>
lowerPath.includes(pattern) || fileContent.includes(pattern)
)) {
return layer;
}
}
return 'unknown';
}
// ===== LOG DETECTION =====
findLogCalls(sourceFile) {
const logCalls = [];
const traverse = (node) => {
if (node.getKind() === SyntaxKind.CallExpression) {
const callExpr = node;
const logInfo = this.extractLogInfo(callExpr, sourceFile);
if (logInfo) {
logCalls.push({
node: callExpr,
level: logInfo.level,
message: logInfo.message,
fullCall: logInfo.fullCall,
position: sourceFile.getLineAndColumnAtPos(callExpr.getStart()),
surroundingCode: this.getSurroundingCode(callExpr, sourceFile)
});
}
}
node.forEachChild(child => traverse(child));
};
traverse(sourceFile);
return logCalls;
}
extractLogInfo(callExpr, sourceFile) {
const callText = callExpr.getText();
const logPatterns = [
{ pattern: /(?:console|logger|log|winston|bunyan|pino)\.error\(/i, level: 'error' },
{ pattern: /(?:console|logger|log|winston|bunyan|pino)\.warn\(/i, level: 'warn' },
{ pattern: /(?:console|logger|log|winston|bunyan|pino)\.info\(/i, level: 'info' },
{ pattern: /(?:console|logger|log|winston|bunyan|pino)\.debug\(/i, level: 'debug' },
{ pattern: /Log\.e\(/i, level: 'error' },
{ pattern: /Timber\.e\(/i, level: 'error' },
{ pattern: /\.logError\(/i, level: 'error' },
{ pattern: /\.logWarn\(/i, level: 'warn' },
{ pattern: /\.logInfo\(/i, level: 'info' }
];
for (const { pattern, level } of logPatterns) {
if (pattern.test(callText)) {
return {
level,
fullCall: callText,
message: this.extractLogMessage(callExpr)
};
}
}
return null;
}
extractLogMessage(callExpr) {
const args = callExpr.getArguments();
if (args.length === 0) return '';
const firstArg = args[0];
if (firstArg.getKind() === SyntaxKind.StringLiteral) {
return firstArg.getLiteralText();
}
if (firstArg.getKind() === SyntaxKind.TemplateExpression) {
return firstArg.getText();
}
return firstArg.getText();
}
getSurroundingCode(node, sourceFile) {
const startPos = Math.max(0, node.getStart() - 150);
const endPos = Math.min(sourceFile.getFullText().length, node.getEnd() + 150);
return sourceFile.getFullText().slice(startPos, endPos);
}
findHttpStatusReturns(sourceFile) {
const httpReturns = [];
const traverse = (node) => {
if (node.getKind() === SyntaxKind.CallExpression) {
const callExpr = node;
const callText = callExpr.getText();
// Next.js patterns
const nextJsMatch = callText.match(/NextResponse\.json\([^,]*,\s*{\s*status:\s*(\d+)/i);
if (nextJsMatch) {
httpReturns.push({
node: callExpr,
status: nextJsMatch[1],
type: 'NextResponse',
position: sourceFile.getLineAndColumnAtPos(callExpr.getStart()),
surroundingCode: this.getSurroundingCode(callExpr, sourceFile)
});
}
// Express patterns
const expressMatch = callText.match(/\.status\((\d+)\)/i);
if (expressMatch) {
httpReturns.push({
node: callExpr,
status: expressMatch[1],
type: 'Express',
position: sourceFile.getLineAndColumnAtPos(callExpr.getStart()),
surroundingCode: this.getSurroundingCode(callExpr, sourceFile)
});
}
}
node.forEachChild(child => traverse(child));
};
traverse(sourceFile);
return httpReturns;
}
findRetryPatterns(sourceFile) {
const retryPatterns = [];
const fileText = sourceFile.getFullText();
const retryIndicators = [
'retry', 'attempt', 'backoff', 'maxRetries', 'retryCount',
'maxAttempts', 'attemptCount', 'retryable', 'canRetry'
];
const hasRetryPattern = retryIndicators.some(indicator =>
new RegExp(indicator, 'i').test(fileText)
);
if (hasRetryPattern) {
const traverse = (node) => {
if (node.getKind() === SyntaxKind.ForStatement ||
node.getKind() === SyntaxKind.WhileStatement) {
const loopText = node.getText();
const isRetryLoop = retryIndicators.some(indicator =>
new RegExp(indicator, 'i').test(loopText)
);
if (isRetryLoop) {
retryPatterns.push({
node: node,
type: 'retry_loop',
position: sourceFile.getLineAndColumnAtPos(node.getStart()),
surroundingCode: this.getSurroundingCode(node, sourceFile)
});
}
}
node.forEachChild(child => traverse(child));
};
traverse(sourceFile);
}
return retryPatterns;
}
// ===== PHASE 1: REQUIRED LOGS & WRONG LEVELS =====
analyzeRequiredLogs(filePath, sourceFile, layer, patterns) {
const violations = [];
const { logCalls, httpReturns, retryPatterns } = patterns;
// Rule 1: HTTP 5xx at boundary must have error log
if (layer === 'controller') {
const http5xxReturns = httpReturns.filter(ret =>
ret.status.startsWith('5')
);
for (const http5xx of http5xxReturns) {
const hasNearbyErrorLog = this.hasNearbyLog(http5xx, logCalls, 'error', 5);
if (!hasNearbyErrorLog) {
violations.push({
ruleId: 'C019',
type: 'missing_required_log',
message: this.config.requiredLogEvents.http_5xx_boundary.message,
filePath: filePath,
line: http5xx.position.line,
column: http5xx.position.column,
severity: 'warning',
category: 'logging',
confidence: this.config.requiredLogEvents.http_5xx_boundary.confidence,
suggestion: this.config.requiredLogEvents.http_5xx_boundary.suggestion,
context: {
eventType: 'http_5xx_boundary',
layer: layer,
statusCode: http5xx.status
}
});
}
}
}
// Rule 2: Retry exhausted must have error log
for (const retryPattern of retryPatterns) {
const hasExhaustedErrorLog = this.hasRetryExhaustedLog(retryPattern, logCalls);
if (!hasExhaustedErrorLog) {
violations.push({
ruleId: 'C019',
type: 'missing_required_log',
message: this.config.requiredLogEvents.retry_exhausted.message,
filePath: filePath,
line: retryPattern.position.line,
column: retryPattern.position.column,
severity: 'warning',
category: 'logging',
confidence: this.config.requiredLogEvents.retry_exhausted.confidence,
suggestion: this.config.requiredLogEvents.retry_exhausted.suggestion,
context: {
eventType: 'retry_exhausted',
layer: layer
}
});
}
}
return violations;
}
analyzeWrongLevelUsage(filePath, sourceFile, layer, patterns) {
const violations = [];
const { logCalls, httpReturns } = patterns;
for (const logCall of logCalls) {
if (logCall.level !== 'error') continue;
// Skip error logs in catch blocks (legitimate exceptions)
if (this.isInCatchBlock(logCall.node)) {
continue;
}
// Rule 1: 4xx validation should not be error
const nearby4xx = this.findNearbyHttpStatus(logCall, httpReturns, '4');
if (nearby4xx && this.isMissingDataValidation(logCall)) {
violations.push({
ruleId: 'C019',
type: 'wrong_log_level',
message: this.config.wrongLevelPatterns.missing_data_error.message,
filePath: filePath,
line: logCall.position.line,
column: logCall.position.column,
severity: 'warning',
category: 'logging',
confidence: this.config.wrongLevelPatterns.missing_data_error.confidence,
suggestion: this.config.wrongLevelPatterns.missing_data_error.suggestion,
context: {
currentLevel: 'error',
suggestedLevel: this.config.wrongLevelPatterns.missing_data_error.expectedLevel,
eventType: 'missing_data_validation',
statusCode: nearby4xx.status
}
});
}
// Rule 2: Retry attempts should not be error
if (this.isRetryAttemptLog(logCall)) {
violations.push({
ruleId: 'C019',
type: 'wrong_log_level',
message: this.config.wrongLevelPatterns.retry_attempt_error.message,
filePath: filePath,
line: logCall.position.line,
column: logCall.position.column,
severity: 'warning',
category: 'logging',
confidence: this.config.wrongLevelPatterns.retry_attempt_error.confidence,
suggestion: this.config.wrongLevelPatterns.retry_attempt_error.suggestion,
context: {
currentLevel: 'error',
suggestedLevel: this.config.wrongLevelPatterns.retry_attempt_error.expectedLevel,
eventType: 'retry_attempt'
}
});
}
}
return violations;
}
// ===== PHASE 2: OVERUSED & MISSING LOGS =====
analyzeOverusedLogs(filePath, sourceFile, layer, patterns) {
const violations = [];
const { logCalls } = patterns;
// Group logs by function/method
const functionLogs = this.groupLogsByFunction(sourceFile, logCalls);
// Check for hot path over-logging
for (const [funcNode, logs] of functionLogs) {
if (logs.length > this.config.overusedLogPatterns.hot_path_over_logging.threshold) {
const funcName = this.getFunctionName(funcNode);
violations.push({
ruleId: 'C019',
type: 'overused_logs',
message: this.config.overusedLogPatterns.hot_path_over_logging.message,
filePath: filePath,
line: logs[0].position.line,
column: logs[0].position.column,
severity: 'info',
category: 'performance',
confidence: this.config.overusedLogPatterns.hot_path_over_logging.confidence,
suggestion: this.config.overusedLogPatterns.hot_path_over_logging.suggestion,
context: {
functionName: funcName,
logCount: logs.length,
threshold: this.config.overusedLogPatterns.hot_path_over_logging.threshold,
eventType: 'hot_path_over_logging'
}
});
}
}
// Check for loop over-logging
const loopLogs = this.findLogsInLoops(sourceFile, logCalls);
for (const loopLog of loopLogs) {
violations.push({
ruleId: 'C019',
type: 'overused_logs',
message: this.config.overusedLogPatterns.loop_over_logging.message,
filePath: filePath,
line: loopLog.position.line,
column: loopLog.position.column,
severity: 'warning',
category: 'performance',
confidence: this.config.overusedLogPatterns.loop_over_logging.confidence,
suggestion: this.config.overusedLogPatterns.loop_over_logging.suggestion,
context: {
eventType: 'loop_over_logging',
loopType: loopLog.loopType
}
});
}
return violations;
}
analyzeMissingCriticalLogs(filePath, sourceFile, layer, patterns) {
const violations = [];
const { logCalls, httpReturns } = patterns;
// Check for authentication failures without logs
const authFailures = this.findAuthFailures(sourceFile, httpReturns);
for (const authFailure of authFailures) {
const hasNearbyLog = this.hasNearbyLog(authFailure, logCalls, ['warn', 'error'], 5);
if (!hasNearbyLog) {
violations.push({
ruleId: 'C019',
type: 'missing_critical_log',
message: this.config.missingLogPatterns.auth_failure_silent.message,
filePath: filePath,
line: authFailure.position.line,
column: authFailure.position.column,
severity: 'warning',
category: 'security',
confidence: this.config.missingLogPatterns.auth_failure_silent.confidence,
suggestion: this.config.missingLogPatterns.auth_failure_silent.suggestion,
context: {
eventType: 'auth_failure_silent',
statusCode: authFailure.status
}
});
}
}
// Check for payment transactions without logs
const paymentEvents = this.findPaymentEvents(sourceFile);
for (const paymentEvent of paymentEvents) {
const hasNearbyLog = this.hasNearbyLog(paymentEvent, logCalls, ['info', 'warn', 'error'], 10);
if (!hasNearbyLog) {
violations.push({
ruleId: 'C019',
type: 'missing_critical_log',
message: this.config.missingLogPatterns.payment_transaction_silent.message,
filePath: filePath,
line: paymentEvent.position.line,
column: paymentEvent.position.column,
severity: 'warning',
category: 'audit',
confidence: this.config.missingLogPatterns.payment_transaction_silent.confidence,
suggestion: this.config.missingLogPatterns.payment_transaction_silent.suggestion,
context: {
eventType: 'payment_transaction_silent',
operation: paymentEvent.operation
}
});
}
}
return violations;
}
analyzeLogRedundancy(filePath, sourceFile, layer, patterns) {
const violations = [];
const { logCalls } = patterns;
// Find duplicate log patterns
for (let i = 0; i < logCalls.length; i++) {
for (let j = i + 1; j < logCalls.length; j++) {
const log1 = logCalls[i];
const log2 = logCalls[j];
if (this.isDuplicateLogViolation(log1, log2, this.config.redundancyPatterns.duplicate_log_events.maxDistance)) {
const distance = Math.abs(log1.position.line - log2.position.line);
const similarity = this.calculateLogSimilarity(log1.message, log2.message);
violations.push({
ruleId: 'C019',
type: 'redundant_logs',
message: this.config.redundancyPatterns.duplicate_log_events.message,
filePath: filePath,
line: log2.position.line,
column: log2.position.column,
severity: 'info',
category: 'maintainability',
confidence: this.config.redundancyPatterns.duplicate_log_events.confidence,
suggestion: this.config.redundancyPatterns.duplicate_log_events.suggestion,
context: {
eventType: 'duplicate_log_events',
firstLogLine: log1.position.line,
secondLogLine: log2.position.line,
similarity: Math.round(similarity * 100),
distance: distance
}
});
}
}
}
return violations;
}
// ===== PHASE 3: ESSENTIAL DISTRIBUTED LOGGING =====
analyzeDistributedPatterns(filePath, sourceFile, layer, patterns) {
const violations = [];
const { logCalls, httpReturns } = patterns;
// Check for centralized logging first
const hasCentralizedLogging = this.hasProjectCentralizedLogging(sourceFile, filePath);
if (this.verbose) {
console.log(`[DEBUG] 🔧 Centralized logging detected: ${hasCentralizedLogging} for ${filePath.split('/').pop()}`);
}
// Skip external call logging check if centralized logging is detected
if (hasCentralizedLogging) {
if (this.verbose) {
console.log(`[DEBUG] ✅ Skipping external call logging check - centralized logging detected`);
}
return violations;
}
// Check for silent external calls only if no centralized logging
const externalCalls = this.findExternalServiceCalls(sourceFile);
for (const extCall of externalCalls) {
const hasNearbyLog = this.hasNearbyLog(extCall, logCalls, ['info', 'warn', 'error'], 5);
if (!hasNearbyLog) {
violations.push({
ruleId: 'C019',
type: 'distributed_gap',
message: this.config.distributedPatterns.external_call_silent.message,
filePath: filePath,
line: extCall.position.line,
column: extCall.position.column,
severity: 'warning',
category: 'monitoring',
confidence: this.config.distributedPatterns.external_call_silent.confidence,
suggestion: this.config.distributedPatterns.external_call_silent.suggestion,
context: {
eventType: 'external_call_silent',
serviceUrl: extCall.url,
method: extCall.method
}
});
}
}
return violations;
}
// ===== HELPER METHODS =====
groupLogsByFunction(sourceFile, logCalls) {
const functionLogs = new Map();
for (const logCall of logCalls) {
const funcNode = this.findContainingFunction(logCall.node);
if (funcNode) {
if (!functionLogs.has(funcNode)) {
functionLogs.set(funcNode, []);
}
functionLogs.get(funcNode).push(logCall);
}
}
return functionLogs;
}
findContainingFunction(node) {
let current = node.getParent();
while (current) {
const kind = current.getKind();
if (kind === SyntaxKind.FunctionDeclaration ||
kind === SyntaxKind.MethodDeclaration ||
kind === SyntaxKind.ArrowFunction ||
kind === SyntaxKind.FunctionExpression) {
return current;
}
current = current.getParent();
}
return null;
}
getFunctionName(funcNode) {
if (!funcNode) return 'anonymous';
const kind = funcNode.getKind();
if (kind === SyntaxKind.FunctionDeclaration || kind === SyntaxKind.MethodDeclaration) {
const nameNode = funcNode.getNameNode();
return nameNode ? nameNode.getText() : 'anonymous';
}
if (kind === SyntaxKind.ArrowFunction || kind === SyntaxKind.FunctionExpression) {
const parent = funcNode.getParent();
if (parent && parent.getKind() === SyntaxKind.VariableDeclaration) {
const nameNode = parent.getNameNode();
return nameNode ? nameNode.getText() : 'anonymous';
}
if (parent && parent.getKind() === SyntaxKind.PropertyAssignment) {
const propName = parent.getNameNode();
return propName ? propName.getText() : 'anonymous';
}
if (parent && parent.getKind() === SyntaxKind.BinaryExpression) {
const left = parent.getLeft();
if (left && left.getKind() === SyntaxKind.PropertyAccessExpression) {
const prop = left.getNameNode();
return prop ? prop.getText() : 'anonymous';
}
}
return 'anonymous';
}
return 'anonymous';
}
findLogsInLoops(sourceFile, logCalls) {
const loopLogs = [];
for (const logCall of logCalls) {
let current = logCall.node.getParent();
while (current) {
const kind = current.getKind();
if (kind === SyntaxKind.ForStatement ||
kind === SyntaxKind.WhileStatement ||
kind === SyntaxKind.DoStatement ||
kind === SyntaxKind.ForInStatement ||
kind === SyntaxKind.ForOfStatement) {
loopLogs.push({
...logCall,
loopType: this.getLoopTypeName(kind)
});
break;
}
current = current.getParent();
}
}
return loopLogs;
}
getLoopTypeName(kind) {
switch (kind) {
case SyntaxKind.ForStatement: return 'for';
case SyntaxKind.WhileStatement: return 'while';
case SyntaxKind.DoStatement: return 'do-while';
case SyntaxKind.ForInStatement: return 'for-in';
case SyntaxKind.ForOfStatement: return 'for-of';
default: return 'unknown';
}
}
findAuthFailures(sourceFile, httpReturns) {
return httpReturns.filter(ret =>
ret.status === '401' || ret.status === '403'
);
}
findPaymentEvents(sourceFile) {
const paymentEvents = [];
const fileText = sourceFile.getFullText().toLowerCase();
// Skip Redux slices and frontend state management
if (fileText.includes('createslice') || fileText.includes('createappslice') ||
fileText.includes('extrareducers') || fileText.includes('state.')) {
if (this.verbose) {
console.log(`[DEBUG] 💰 Skipping payment detection - Redux slice detected`);
}
return paymentEvents;
}
const paymentPatterns = [
'payment', 'transaction', 'charge', 'refund', 'billing',
'invoice', 'subscription', 'purchase', 'checkout'
];
const hasPaymentPattern = paymentPatterns.some(pattern =>
new RegExp(pattern, 'i').test(fileText)
);
if (hasPaymentPattern) {
const traverse = (node) => {
if (node.getKind() === SyntaxKind.CallExpression) {
const callText = node.getText().toLowerCase();
// Only detect actual payment processing calls, not UI calculations
const paymentActionPatterns = [
/payment.*(?:process|execute|submit|create|confirm)/i,
/transaction.*(?:process|execute|submit|create|confirm)/i,
/charge.*(?:process|execute|submit|create)/i,
/refund.*(?:process|execute|submit|create)/i,
/purchase.*(?:process|execute|submit|create|complete)/i,
/checkout.*(?:process|execute|submit|complete)/i
];
if (paymentActionPatterns.some(pattern => pattern.test(callText))) {
paymentEvents.push({
node: node,
operation: callText,
position: sourceFile.getLineAndColumnAtPos(node.getStart())
});
}
}
node.forEachChild(child => traverse(child));
};
traverse(sourceFile);
}
return paymentEvents;
}
findExternalServiceCalls(sourceFile) {
const externalCalls = [];
const traverse = (node) => {
if (node.getKind() === SyntaxKind.CallExpression) {
const callText = node.getText();
// Exclude common false positives first
const excludePatterns = [
// Config service calls (not external)
/configService\.get/i,
/process\.env\./i,
/config\.get/i,
// Local library operations (not external services)
/jwt\.(?:verify|sign|decode)/i,
/bcrypt\.(?:hash|compare)/i,
/crypto\.(?:createHash|randomBytes)/i,
// Database ORM operations (not external service calls)
/(?:repository|entity|model)\.(?:find|save|update|delete)/i,
/queryBuilder\./i,
// Internal service dependencies (NestJS/DI pattern)
/this\.[\w]+Service\./i,
/this\.[\w]+Repository\./i,
/this\.[\w]+Manager\./i,
/this\.[\w]+Client\.(?!http|fetch|post|get)/i,
// Specific service calls that are internal
/this\.service\./i,
/this\.commonCustomerService\./i,
/[\w]+Service\.get[\w]+/i,
// Cache operations (not external)
/cacheManager\./i,
/redis\.(?:get|set|del)/i,
// Local file/path operations
/path\.(?:join|resolve)/i,
/fs\.(?:readFile|writeFile)/i,
/__dirname|__filename/i
];
const isExcluded = excludePatterns.some(pattern => pattern.test(callText));
if (isExcluded) {
return;
}
// More specific patterns for REAL external service calls
const realExternalPatterns = [
// HTTP calls with URLs
/(?:fetch|axios|http).*https?:\/\//i,
// API service calls
/(?:api|service|client)\.(?:get|post|put|delete|call|request)/i,
// Third-party service integrations
/(?:stripe|paypal|payment|billing)\.(?:charge|process|create)/i,
/(?:twilio|sendgrid|mailgun)\.(?:send|create)/i,
/(?:aws|gcp|azure)\.(?:upload|send|publish)/i,
// External auth providers
/(?:google|facebook|auth0)\.(?:verify|authenticate)/i
];
const isRealExternal = realExternalPatterns.some(pattern => pattern.test(callText));
if (isRealExternal) {
externalCalls.push({
node: node,
url: this.extractUrl(callText),
method: this.extractHttpMethod(callText),
position: sourceFile.getLineAndColumnAtPos(node.getStart())
});
}
}
node.forEachChild(child => traverse(child));
};
traverse(sourceFile);
return externalCalls;
}
extractUrl(callText) {
const urlMatch = callText.match(/['"`]([^'"`]*(?:api|http)[^'"`]*)['"`]/i);
return urlMatch ? urlMatch[1] : 'unknown';
}
extractHttpMethod(callText) {
const methodPatterns = ['get', 'post', 'put', 'delete', 'patch'];
for (const method of methodPatterns) {
if (new RegExp(`\\.${method}\\s*\\(`, 'i').test(callText)) {
return method.toUpperCase();
}
}
return 'UNKNOWN';
}
hasNearbyLog(targetNode, logCalls, levels, maxDistance = 10) {
const targetLine = targetNode.position.line;
return logCalls.some(logCall => {
const logLine = logCall.position.line;
const distance = Math.abs(targetLine - logLine);
const levelMatch = Array.isArray(levels) ? levels.includes(logCall.level) : logCall.level === levels;
return levelMatch && distance <= maxDistance;
});
}
hasProjectCentralizedLogging(sourceFile, filePath) {
const text = sourceFile.getFullText();
// Check for centralized logging patterns in the file
const centralizedLoggingPatterns = [
// Error handling with built-in logging
/handleAxiosErrorWithModal/i,
/handleError.*Modal/i,
/interceptors\.response\.use/i,
/interceptors\.request\.use/i,
// Global error handlers
/globalErrorHandler/i,
/global.*error.*handler/i,
/centralized.*error/i,
/error.*interceptor/i,
// API services with built-in logging
/apiService/i,
/service\..*error/i,
/\.catch\(\s*handleError/i,
// Redux/Thunk error handlers with logging
/rejectWithValue/i,
/\.unwrap\(\)/i,
// Logger imports/usage indicating centralized approach
/import.*logger.*from/i,
/const.*logger.*=.*require/i,
/logger\.error/i,
/logger\.warn/i,
// Try-catch with error handling that includes logging
/catch\s*\([^)]*\)\s*\{[^}]*(?:console\.error|logger\.error|handleError)[^}]*\}/s
];
const hasCentralizedPattern = centralizedLoggingPatterns.some(pattern => pattern.test(text));
// Additional check: if it's a thunk file, check for Redux error patterns
if (filePath.includes('thunk') || filePath.includes('Thunk')) {
const reduxErrorPatterns = [
/rejectWithValue/i,
/extraReducers/i,
/\.rejected/i,
/handleError/i,
/errorHandler/i
];
const hasReduxErrorHandling = reduxErrorPatterns.some(pattern => pattern.test(text));
if (hasReduxErrorHandling && this.verbose) {
console.log(`[DEBUG] 🔄 Redux error handling patterns detected in thunk file`);
}
return hasCentralizedPattern || hasReduxErrorHandling;
}
return hasCentralizedPattern;
}
findNearbyHttpStatus(logCall, httpReturns, statusPrefix) {
const logLine = logCall.position.line;
return httpReturns.find(httpReturn => {
const distance = Math.abs(logLine - httpReturn.position.line);
return httpReturn.status.startsWith(statusPrefix) && distance <= 10;
});
}
hasRetryExhaustedLog(retryPattern, logCalls) {
const retryText = retryPattern.surroundingCode.toLowerCase();
const exhaustedPatterns = [
'exhausted', 'failed', 'max.*attempt', 'max.*retr',
'give.*up', 'no.*more', 'final.*attempt'
];
return logCalls.some(logCall => {
if (logCall.level !== 'error') return false;
const logText = logCall.surroundingCode.toLowerCase();
return exhaustedPatterns.some(pattern =>
new RegExp(pattern, 'i').test(logText)
);
});
}
isInCatchBlock(node) {
let current = node.getParent();
while (current) {
if (current.getKind() === SyntaxKind.CatchClause) {
return true;
}
current = current.getParent();
}
return false;
}
isMissingDataValidation(logCall) {
const message = logCall.message.toLowerCase();
const surroundingCode = logCall.surroundingCode.toLowerCase();
const missingDataPatterns = [
'missing', 'not.*found', 'empty', 'null', 'undefined',
'required', 'invalid.*format', 'invalid.*input'
];
return missingDataPatterns.some(pattern =>
new RegExp(pattern, 'i').test(message + ' ' + surroundingCode)
);
}
isRetryAttemptLog(logCall) {
const message = logCall.message.toLowerCase();
const surroundingCode = logCall.surroundingCode.toLowerCase();
const combinedText = message + ' ' + surroundingCode;
const retryAttemptPatterns = [
/attempt\s*\d+.*fail/i,
/retry\s*\d+.*fail/i,
/try\s*\d+.*fail/i,
/retrying.*\(\s*\d+\s*\/\s*\d+\s*\)/i,
/attempt.*\(\s*\d+\s*\/\s*\d+\s*\)/i
];
const hasRetryPattern = retryAttemptPatterns.some(pattern =>
pattern.test(combinedText)
);
const isExhausted = /exhausted|final|last|max|all.*attempts|no.*more|after.*retries/i.test(combinedText);
return hasRetryPattern && !isExhausted;
}
isDuplicateLogViolation(log1, log2, maxDistance) {
// Skip if either log is in a utility function
const log1Function = this.findContainingFunction(log1.node);
const log2Function = this.findContainingFunction(log2.node);
const isLog1Utility = this.isUtilityFunction(log1Function);
const isLog2Utility = this.isUtilityFunction(log2Function);
if (this.verbose) {
console.log(`[DEBUG] 🔧 Checking duplicate logs at lines ${log1.position.line} and ${log2.position.line}`);
console.log(`[DEBUG] 🔧 Log1 function: ${this.getFunctionName(log1Function) || 'unknown'}, utility: ${isLog1Utility}`);
console.log(`[DEBUG] 🔧 Log2 function: ${this.getFunctionName(log2Function) || 'unknown'}, utility: ${isLog2Utility}`);
}
if (isLog1Utility || isLog2Utility) {
if (this.verbose) {
console.log(`[DEBUG] ✅ Skipping duplicate log check - utility function detected`);
}
return false;
}
// Skip if logs are in different functions (legitimate error handling)
if (log1Function !== log2Function) {
if (this.verbose) {
console.log(`[DEBUG] ✅ Skipping duplicate log check - different functions`);
}
return false;
}
const distance = Math.abs(log1.position.line - log2.position.line);
if (distance > maxDistance) {
return false;
}
// Check if they are error handling logs (legitimate duplicates)
const isErrorHandling = log1.level === 'error' || log2.level === 'error' ||
log1.message.toLowerCase().includes('error') ||
log2.message.toLowerCase().includes('error') ||
log1.surroundingCode.includes('catch') ||
log2.surroundingCode.includes('catch');
if (isErrorHandling && distance > 3) {
if (this.verbose) {
console.log(`[DEBUG] ✅ Skipping duplicate log check - error handling context`);
}
return false;
}
// Check message similarity
const similarity = this.calculateLogSimilarity(log1.message, log2.message);
return similarity > 0.8; // 80% similar
}
isUtilityFunction(functionNode) {
if (!functionNode) return false;
const functionName = this.getFunctionName(functionNode);
if (!functionName) return false;
const utilityPatterns = [
/^write/, /^log/, /^handle/, /^process/, /^format/,
/helper/, /util/, /wrapper/, /middleware/
];
return utilityPatterns.some(pattern => pattern.test(functionName.toLowerCase()));
}
calculateLogSimilarity(message1, message2) {
if (!message1 || !message2) return 0;
const clean1 = message1.toLowerCase().replace(/[^a-z0-9\s]/g, '').trim();
const clean2 = message2.toLowerCase().replace(/[^a-z0-9\s]/g, '').trim();
if (clean1 === clean2) return 1;
const words1 = clean1.split(/\s+/);
const words2 = clean2.split(/\s+/);
const intersection = words1.filter(word => words2.includes(word));
const union = [...new Set([...words1, ...words2])];
return intersection.length / union.length;
}
}
module.exports = C019SystemLogAnalyzer;