node-red-contrib-code-analyzer
Version:
A Node-RED package that provides a background service to detect debugging artifacts in function nodes across Node-RED flows. Features performance monitoring (CPU, memory, event loop), queue monitoring, and Slack alerting.
975 lines (838 loc) • 179 kB
HTML
<script type="text/javascript">
RED.nodes.registerType('code-analyzer', {
category: 'utility',
color: '#87CEEB',
defaults: {
name: {value: ""},
scanInterval: {value: 30, required: true, validate: RED.validators.number()},
detectionLevel: {value: 1, required: true, validate: RED.validators.number()},
autoStart: {value: true},
codeAnalysis: {value: true},
queueScanning: {value: false},
queueMessageFrequency: {value: 1800, required: true, validate: RED.validators.number()},
queueScanMode: {value: "all"},
selectedQueueIds: {value: []},
queueLengthThreshold: {value: 0, required: true, validate: RED.validators.number()},
slackWebhookUrl: {value: ""},
performanceMonitoring: {value: false},
performanceInterval: {value: 10, required: true, validate: RED.validators.number()},
cpuThreshold: {value: 75, required: true, validate: RED.validators.number()},
memoryThreshold: {value: 80, required: true, validate: RED.validators.number()},
eventLoopThreshold: {value: 20, required: true, validate: RED.validators.number()},
sustainedAlertDuration: {value: 300, required: true, validate: RED.validators.number()},
alertCooldown: {value: 1800, required: true, validate: RED.validators.number()},
dbRetentionDays: {value: 7, required: true, validate: RED.validators.number()}
},
inputs: 1,
outputs: 0,
icon: "debug.png",
label: function() {
return this.name || "Code Analyzer";
},
oneditprepare: function() {
const node = this;
// Initialize tabs
const tabs = RED.tabs.create({
id: "node-input-tabs",
onchange: function(tab) {
$("#node-input-tabs-content").children().hide();
$("#" + tab.id).show();
}
});
tabs.addTab({
id: "general-tab",
label: "General"
});
tabs.addTab({
id: "code-analysis-tab",
label: "Code Analysis"
});
tabs.addTab({
id: "queue-monitoring-tab",
label: "Queue Monitoring"
});
tabs.addTab({
id: "performance-monitoring-tab",
label: "Performance Monitoring"
});
// Queue monitoring logic
const queueScanningCheckbox = $('#node-input-queueScanning');
const queueScanModeSelect = $('#node-input-queueScanMode');
const selectedQueueRow = $('.form-row-selectedQueue');
const queueThresholdRow = $('.form-row-queueThreshold');
function updateQueueVisibility() {
const isQueueScanningEnabled = queueScanningCheckbox.prop('checked');
$('.form-row-queueMode').toggle(isQueueScanningEnabled);
queueThresholdRow.toggle(isQueueScanningEnabled);
if (isQueueScanningEnabled) {
const isSpecificQueue = queueScanModeSelect.val() === 'specific';
selectedQueueRow.toggle(isSpecificQueue);
if (isSpecificQueue) {
// Populate queue checkboxes
const queueContainer = $('#queue-checkbox-container');
queueContainer.empty();
// Get current flow ID
const currentFlowId = RED.workspaces.active();
const savedSelectedIds = node.selectedQueueIds || [];
RED.nodes.eachNode(function(delayNode) {
if (delayNode.type === 'delay' && delayNode.pauseType === 'rate' && delayNode.z === currentFlowId) {
const label = delayNode.name || `Delay Node (${delayNode.id.substring(0, 8)})`;
const isSelected = Array.isArray(savedSelectedIds) && savedSelectedIds.includes(delayNode.id);
const checkboxHtml = `
<div style="margin: 2px 0; width: 100%;">
<label style="display: flex; align-items: center; justify-content: flex-start; font-weight: normal; width: 100%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
<input type="checkbox" class="queue-checkbox" value="${delayNode.id}" ${isSelected ? 'checked' : ''} style="margin-right: 8px; flex-shrink: 0; flex-basis: 5%">
<span style="flex: 1; min-width: 0;">${label}</span>
</label>
</div>
`;
queueContainer.append(checkboxHtml);
}
});
}
} else {
selectedQueueRow.hide();
}
}
queueScanningCheckbox.on('change', updateQueueVisibility);
queueScanModeSelect.on('change', updateQueueVisibility);
// Initial visibility setup
updateQueueVisibility();
// Code analysis logic
const codeAnalysisCheckbox = $('#node-input-codeAnalysis');
const codeAnalysisRows = $('.form-row-codeAnalysis');
function updateCodeAnalysisVisibility() {
const isCodeAnalysisEnabled = codeAnalysisCheckbox.prop('checked');
codeAnalysisRows.toggle(isCodeAnalysisEnabled);
}
codeAnalysisCheckbox.on('change', updateCodeAnalysisVisibility);
// Initial code analysis visibility setup
updateCodeAnalysisVisibility();
// Performance monitoring logic
const performanceMonitoringCheckbox = $('#node-input-performanceMonitoring');
const performanceThresholdRows = $('.form-row-performanceThreshold');
const performanceIntervalRow = $('.form-row-performanceInterval');
function updatePerformanceVisibility() {
const isPerformanceMonitoringEnabled = performanceMonitoringCheckbox.prop('checked');
performanceThresholdRows.toggle(isPerformanceMonitoringEnabled);
performanceIntervalRow.toggle(isPerformanceMonitoringEnabled);
// Update database status display
updateDatabaseStatus();
}
function updateDatabaseStatus() {
// Check database status
fetch('/code-analyzer/api/database-status')
.then(response => response.json())
.then(data => {
updateDatabaseUI(data);
})
.catch(error => {
console.error('Error checking database status:', error);
$('#db-status-text').text('Error checking database status');
showDatabaseCreationSection(true);
setDbDependentFeaturesEnabled(false);
});
}
function updateDatabaseUI(status) {
if (status.exists && status.initialized) {
// Database exists and is ready
$('#db-status-display').css('background-color', '#d4edda').css('color', '#155724').css('border', '1px solid #c3e6cb');
$('#db-status-text').text('✓ Database is ready and initialized');
$('#db-path-text').text(status.dbPath);
// ALWAYS keep creation section visible if there's an active message OR it was previously shown
if ($('#db-creation-message').is(':visible') || $('#db-creation-section').is(':visible')) {
showDatabaseCreationSection(true); // Ensure it stays visible
} else {
showDatabaseCreationSection(false); // Only hide if never used
}
setDbDependentFeaturesEnabled(true);
} else if (status.exists && !status.initialized) {
// Database exists but not initialized
$('#db-status-display').css('background-color', '#fff3cd').css('color', '#856404').css('border', '1px solid #ffeeba');
$('#db-status-text').text('⚠ Database exists but not initialized');
$('#db-path-text').text(status.dbPath);
showDatabaseCreationSection(true);
setDbDependentFeaturesEnabled(false);
} else {
// Database does not exist
$('#db-status-display').css('background-color', '#f8d7da').css('color', '#721c24').css('border', '1px solid #f5c6cb');
$('#db-status-text').text('✗ Database not created');
$('#db-path-text').text(status.dbPath || 'Database path not available');
showDatabaseCreationSection(true);
setDbDependentFeaturesEnabled(false);
}
}
function showDatabaseCreationSection(show) {
$('#db-creation-section').toggle(show);
}
function setDbDependentFeaturesEnabled(enabled) {
// Gray out/enable database-dependent settings
const $dbSettings = $('#db-dependent-settings');
const $perfMonitoring = $('#performance-monitoring-tab .form-row-performanceThreshold, #performance-monitoring-tab .form-row-performanceInterval');
const $dashboardRow = $('.form-row').has('#dashboard-link');
if (enabled) {
$dbSettings.css('opacity', '1').find('input, select').prop('disabled', false);
$perfMonitoring.css('opacity', '1').find('input, select').prop('disabled', false);
$dashboardRow.css('opacity', '1').find('a, button').prop('disabled', false);
} else {
$dbSettings.css('opacity', '0.5').find('input, select').prop('disabled', true);
$perfMonitoring.css('opacity', '0.5').find('input, select').prop('disabled', true);
$dashboardRow.css('opacity', '0.5').find('a, button').prop('disabled', true);
// Show message about needing to create database
if (!enabled && $('#db-dependency-message').length === 0) {
$dbSettings.prepend('<div id="db-dependency-message" style="background: #fff3cd; color: #856404; padding: 8px; border-radius: 4px; margin-bottom: 10px; border: 1px solid #ffeeba;">📋 Create the database above to enable these features</div>');
}
// Add message to performance monitoring tab
if (!enabled && $('#perf-dependency-message').length === 0) {
$('#performance-monitoring-tab .form-row-performanceThreshold').first().before('<div id="perf-dependency-message" style="background: #fff3cd; color: #856404; padding: 8px; border-radius: 4px; margin-bottom: 10px; border: 1px solid #ffeeba;">📋 Create the database in General tab to enable performance monitoring storage</div>');
}
}
if (enabled) {
$('#db-dependency-message').remove();
$('#perf-dependency-message').remove();
}
}
function createDatabase() {
const $button = $('#create-db-button');
const $message = $('#db-creation-message');
// Disable button and show initial loading
$button.prop('disabled', true).html('<i class="fa fa-spinner fa-spin"></i> Initializing...');
// Clear previous messages and show progress container
$message.html(`
<button id="db-message-close" style="position: absolute; top: 4px; right: 6px; background: none; border: none; font-size: 16px; cursor: pointer; color: inherit; opacity: 0.7; z-index: 10;" title="Close message">×</button>
<div id="db-progress-log" style="max-height: 200px; overflow-y: auto; font-family: monospace; font-size: 11px; line-height: 1.4; padding-right: 20px;">
<div style="color: #6c757d; font-style: italic;">🔄 Starting database creation process...</div>
</div>
`)
.css('background-color', '#f8f9fa')
.css('color', '#495057')
.css('border', '1px solid #dee2e6')
.show();
const $progressLog = $('#db-progress-log');
// Use the regular API but display detailed messages from fallback attempts
fetch('/code-analyzer/api/create-database', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
// Simulate progressive message display for better UX
if (data.success && data.creationMessages && data.creationMessages.length > 0) {
displayMessagesProgressively(data.creationMessages, $progressLog, $button, () => {
// Success completion callback
$message.css('background-color', '#d4edda')
.css('color', '#155724')
.css('border', '1px solid #c3e6cb');
$progressLog.append(`<div style="margin: 10px 0; font-weight: bold; color: #155724;">🎉 ${data.message}</div>`);
$progressLog.append(`<div style="margin: 5px 0; color: #155724;">Database location: ${data.dbPath}</div>`);
if (data.attemptedLocations > 1) {
$progressLog.append(`<div style="margin: 5px 0; color: #856404;">⚠️ Required ${data.attemptedLocations} location attempts (some locations failed)</div>`);
} else {
$progressLog.append(`<div style="margin: 5px 0; color: #6c757d;">✨ Created successfully on first attempt</div>`);
}
$button.prop('disabled', false).html('<i class="fa fa-check"></i> Database Created');
updateDatabaseStatus();
setTimeout(() => {
$button.html('<i class="fa fa-plus"></i> Create Database');
}, 3000);
$progressLog.scrollTop($progressLog[0].scrollHeight);
});
} else if (!data.success && data.creationMessages && data.creationMessages.length > 0) {
displayMessagesProgressively(data.creationMessages, $progressLog, $button, () => {
// Error completion callback
$message.css('background-color', '#f8d7da')
.css('color', '#721c24')
.css('border', '1px solid #f5c6cb');
$progressLog.append(`<div style="margin: 10px 0; font-weight: bold; color: #721c24;">❌ ${data.error || 'Creation failed'}</div>`);
if (data.details) {
$progressLog.append(`<div style="margin: 5px 0; color: #721c24;">Details: ${data.details}</div>`);
}
$button.prop('disabled', false).html('<i class="fa fa-exclamation-triangle"></i> All Locations Failed');
setTimeout(() => {
$button.html('<i class="fa fa-plus"></i> Create Database');
}, 3000);
$progressLog.scrollTop($progressLog[0].scrollHeight);
});
} else {
// Handle case without detailed messages (shouldn't happen but fallback)
if (data.success) {
$message.css('background-color', '#d4edda').css('color', '#155724').css('border', '1px solid #c3e6cb');
$progressLog.append(`<div style="color: #155724;">✓ ${data.message}</div>`);
$button.prop('disabled', false).html('<i class="fa fa-check"></i> Database Created');
updateDatabaseStatus();
} else {
$message.css('background-color', '#f8d7da').css('color', '#721c24').css('border', '1px solid #f5c6cb');
$progressLog.append(`<div style="color: #721c24;">✗ ${data.error}</div>`);
$button.prop('disabled', false).html('<i class="fa fa-exclamation-triangle"></i> Creation Failed');
}
setTimeout(() => {
$button.html('<i class="fa fa-plus"></i> Create Database');
}, 3000);
}
})
.catch(error => {
console.error('Error creating database:', error);
$message.css('background-color', '#f8d7da')
.css('color', '#721c24')
.css('border', '1px solid #f5c6cb');
$progressLog.append(`<div style="margin: 10px 0; font-weight: bold; color: #721c24;">❌ Network error: ${error.message}</div>`);
$button.prop('disabled', false).html('<i class="fa fa-exclamation-triangle"></i> Network Error');
setTimeout(() => {
$button.html('<i class="fa fa-plus"></i> Create Database');
}, 3000);
});
}
// Function to display messages progressively for better UX
function displayMessagesProgressively(messages, $progressLog, $button, completionCallback) {
let messageIndex = 0;
let attemptNumber = 1;
function showNextMessage() {
if (messageIndex >= messages.length) {
// All messages shown, call completion callback
completionCallback();
return;
}
const msg = messages[messageIndex];
const icon = msg.type === 'success' ? '✓' :
msg.type === 'error' ? '✗' :
msg.type === 'warning' ? '⚠️' : 'ℹ';
// Update button text based on message content
if (msg.message.includes('Trying to create database')) {
const location = msg.message.match(/in (.+?) \(/);
if (location) {
$button.html(`<i class="fa fa-spinner fa-spin"></i> Attempt ${attemptNumber}: ${location[1]}`);
attemptNumber++;
}
} else if (msg.message.includes('Waiting 1.5 seconds')) {
$button.html(`<i class="fa fa-clock-o"></i> Waiting...`);
}
// Add message to log with appropriate colors
$progressLog.append(`<div style="margin: 2px 0; color: ${
msg.type === 'success' ? '#155724' :
msg.type === 'error' ? '#721c24' :
msg.type === 'warning' ? '#856404' : '#6c757d'
}; ${msg.type === 'warning' ? 'font-weight: bold;' : ''}">${icon} ${msg.message}</div>`);
// Auto-scroll to bottom
$progressLog.scrollTop($progressLog[0].scrollHeight);
messageIndex++;
// Determine delay based on message type
let delay = 500; // Default delay
if (msg.message.includes('Waiting 1.5 seconds')) {
delay = 1500; // Match the actual wait time
} else if (msg.message.includes('Trying to create database')) {
delay = 800; // Slightly longer for attempts
} else if (msg.message.includes('Failed to create database')) {
delay = 1200; // Give time to read error
} else if (msg.type === 'success') {
delay = 600; // Success messages
}
setTimeout(showNextMessage, delay);
}
// Start showing messages
showNextMessage();
}
// Bind create database button
$(document).off('click', '#create-db-button').on('click', '#create-db-button', createDatabase);
// Bind close button for database messages (persistent until manually closed)
$(document).off('click', '#db-message-close').on('click', '#db-message-close', function() {
$('#db-creation-message').hide();
});
// Auto-close database messages when configuration dialog is closed or cancelled
// Store original handlers
if (!node._originalOnEditSave) {
node._originalOnEditSave = node.oneditsave;
}
if (!node._originalOnEditCancel) {
node._originalOnEditCancel = node.oneditcancel;
}
// Override handlers to hide message on dialog close
node.oneditsave = function() {
$('#db-creation-message').hide();
if (node._originalOnEditSave) {
return node._originalOnEditSave.apply(this, arguments);
}
};
node.oneditcancel = function() {
$('#db-creation-message').hide();
if (node._originalOnEditCancel) {
return node._originalOnEditCancel.apply(this, arguments);
}
};
// Add hover effect for close button
$(document).off('mouseenter mouseleave', '#db-message-close')
.on('mouseenter', '#db-message-close', function() {
$(this).css('opacity', '1');
})
.on('mouseleave', '#db-message-close', function() {
$(this).css('opacity', '0.7');
});
performanceMonitoringCheckbox.on('change', updatePerformanceVisibility);
// Initial performance visibility setup
updatePerformanceVisibility();
// Load database status on initialization
updateDatabaseStatus();
},
oneditsave: function() {
// Collect checked queue IDs
const selectedValues = [];
$('.queue-checkbox:checked').each(function() {
selectedValues.push($(this).val());
});
this.selectedQueueIds = selectedValues;
},
});
</script>
<script type="text/javascript">
$(document).ready(function() {
if (!document.getElementById('debug-analyzer-styles')) {
const style = document.createElement('style');
style.id = 'debug-analyzer-styles';
style.textContent = `
.highlighted-line {
background-color: rgba(255, 193, 7, 0.2) !important;
border-left: 3px solid #ffc107 !important;
}
.highlighted-line-error {
background-color: rgba(220, 53, 69, 0.2) !important;
border-left: 3px solid #dc3545 !important;
}
`;
document.head.appendChild(style);
}
// Parse ignore directives from code lines (browser version)
function parseIgnoreDirectivesBrowser(lines) {
const ignoreRegions = [];
const ignoreLines = new Set();
const ignoreNextLines = new Set();
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const trimmed = line.trim();
// Check for ignore-start directive
if (trimmed.match(/\/\/\s*@nr-analyzer-ignore-start/i)) {
// Find the corresponding end directive
for (let j = i + 1; j < lines.length; j++) {
const endLine = lines[j].trim();
if (endLine.match(/\/\/\s*@nr-analyzer-ignore-end/i)) {
ignoreRegions.push({ start: i + 1, end: j + 1 }); // 1-based line numbers
break;
}
}
}
// Check for single line ignore
if (trimmed.match(/\/\/\s*@nr-analyzer-ignore-line/i)) {
ignoreLines.add(i + 1); // 1-based line numbers
}
// Check for ignore-next directive
if (trimmed.match(/\/\/\s*@nr-analyzer-ignore-next/i)) {
if (i + 1 < lines.length) {
ignoreNextLines.add(i + 2); // 1-based line numbers (next line)
}
}
}
return { ignoreRegions, ignoreLines, ignoreNextLines };
}
// Check if a line should be ignored based on ignore directives (browser version)
function shouldIgnoreLineBrowser(lineNumber, ignoreRegions, ignoreLines, ignoreNextLines) {
// Check if line is in ignore regions
for (const region of ignoreRegions) {
if (lineNumber >= region.start && lineNumber <= region.end) {
return true;
}
}
// Check if line is explicitly ignored
if (ignoreLines.has(lineNumber)) {
return true;
}
// Check if line is marked as ignore-next
if (ignoreNextLines.has(lineNumber)) {
return true;
}
return false;
}
// AST-based detection for browser using Acorn parser
async function detectDebuggingTraitsInBrowser(code, level = 1) {
const lines = code.split('\n');
// Parse ignore directives
const { ignoreRegions, ignoreLines, ignoreNextLines } = parseIgnoreDirectivesBrowser(lines);
// Try to use AST parsing (fallback to regex if AST fails)
try {
const result = detectWithAST(code, level, ignoreRegions, ignoreLines, ignoreNextLines);
// If result is a promise (when loading Acorn), await it
if (result && typeof result.then === 'function') {
return await result;
} else {
return result;
}
} catch (error) {
console.warn('Browser AST parsing failed:', error.message);
return [];
}
}
function detectWithAST(code, level, ignoreRegions, ignoreLines, ignoreNextLines) {
const issues = [];
// Load Acorn parser dynamically if not available
if (typeof window.acorn === 'undefined') {
// Try to load Acorn from CDN
return new Promise((resolve) => {
const script = document.createElement('script');
script.src = 'https://unpkg.com/acorn@8.11.3/dist/acorn.js';
script.onload = () => {
resolve(detectWithASTSync(code, level, ignoreRegions, ignoreLines, ignoreNextLines));
};
script.onerror = () => {
console.warn('Failed to load Acorn parser from CDN');
resolve([]);
};
document.head.appendChild(script);
});
}
return detectWithASTSync(code, level, ignoreRegions, ignoreLines, ignoreNextLines);
}
// Find unused variables in the AST (browser version)
function findUnusedVariablesBrowser(ast) {
const issues = [];
const scopes = [];
const globalScope = new Map(); // variable name -> { declared: Set, used: Set }
scopes.push(globalScope);
function getCurrentScope() {
return scopes[scopes.length - 1];
}
function enterScope() {
scopes.push(new Map());
}
function exitScope() {
const currentScope = scopes.pop();
// Check for unused variables in this scope
for (const [varName, info] of currentScope.entries()) {
for (const declNode of info.declared) {
// Skip if variable is used anywhere in this scope
if (info.used.size > 0) continue;
// Skip function parameters and certain variable names
if (isExemptVariableBrowser(varName, declNode)) continue;
// Create issue for unused variable with precise highlighting
const issue = createUnusedVariableIssueBrowser(varName, declNode);
if (issue) {
issues.push(issue);
}
}
}
}
function isExemptVariableBrowser(varName, declNode) {
// Skip common exempt patterns
if (varName.startsWith('_')) return true; // Underscore prefix indicates intentionally unused
if (['msg', 'node', 'context', 'flow', 'global', 'env', 'RED'].includes(varName)) return true; // Node-RED globals
// Skip function parameters - check if this is a parameter by looking at parent context
if (declNode.type === 'Identifier') {
let currentNode = declNode;
while (currentNode && currentNode.parent) {
const parentNode = currentNode.parent;
if ((parentNode.type === 'FunctionDeclaration' ||
parentNode.type === 'ArrowFunctionExpression' ||
parentNode.type === 'FunctionExpression') &&
parentNode.params && parentNode.params.includes(currentNode)) {
return true;
}
currentNode = parentNode;
}
}
// Skip function declarations that are never called (they might be intended for external use)
if (declNode.type === 'FunctionDeclaration') {
return true;
}
// Skip variable declarations assigned to functions (like arrow functions)
if (declNode.type === 'VariableDeclarator' &&
declNode.init &&
(declNode.init.type === 'FunctionExpression' ||
declNode.init.type === 'ArrowFunctionExpression')) {
return true;
}
return false;
}
function createUnusedVariableIssueBrowser(varName, declNode) {
if (!declNode.loc) return null;
let startColumn = declNode.loc.start.column + 1;
let endColumn = declNode.loc.end.column + 1;
// For variable declarations, highlight only the variable name, not the whole declaration
if (declNode.type === 'VariableDeclarator' && declNode.id) {
startColumn = declNode.id.loc.start.column + 1;
endColumn = declNode.id.loc.end.column + 1;
}
return {
type: 'unused-variable',
message: `Variable '${varName}' is declared but never used`,
line: declNode.loc.start.line,
column: startColumn,
endColumn: endColumn,
severity: 'info'
};
}
function addVariableDeclaration(varName, node) {
const currentScope = getCurrentScope();
if (!currentScope.has(varName)) {
currentScope.set(varName, { declared: new Set(), used: new Set() });
}
currentScope.get(varName).declared.add(node);
}
function addVariableUsage(varName, node) {
// Look for variable in current scope chain (from innermost to outermost)
for (let i = scopes.length - 1; i >= 0; i--) {
const scope = scopes[i];
if (scope.has(varName)) {
scope.get(varName).used.add(node);
return;
}
}
}
function analyzeNode(node, parent = null) {
if (!node || typeof node !== 'object' || !node.type) return;
node.parent = parent;
// Handle scope creation
if (node.type === 'FunctionDeclaration' ||
node.type === 'FunctionExpression' ||
node.type === 'ArrowFunctionExpression' ||
node.type === 'BlockStatement' ||
node.type === 'Program') {
enterScope();
// Add function parameters as declarations
if (node.params) {
for (const param of node.params) {
if (param.type === 'Identifier') {
addVariableDeclaration(param.name, param);
}
}
}
}
// Handle variable declarations
if (node.type === 'VariableDeclarator' && node.id && node.id.type === 'Identifier') {
addVariableDeclaration(node.id.name, node);
}
// Handle function declarations
if (node.type === 'FunctionDeclaration' && node.id && node.id.type === 'Identifier') {
addVariableDeclaration(node.id.name, node);
}
// Handle variable usage (identifier references)
if (node.type === 'Identifier' && parent) {
// Skip if this identifier is a declaration context
const isDeclaration = (
(parent.type === 'VariableDeclarator' && parent.id === node) ||
(parent.type === 'FunctionDeclaration' && parent.id === node) ||
(parent.type === 'Property' && parent.key === node && !parent.computed) ||
(parent.type === 'MemberExpression' && parent.property === node && !parent.computed)
);
if (!isDeclaration) {
addVariableUsage(node.name, node);
}
}
// Recursively analyze child nodes
for (let key in node) {
if (Object.prototype.hasOwnProperty.call(node, key) && key !== 'parent' && key !== 'loc' && key !== 'range') {
let child = node[key];
if (Array.isArray(child)) {
child.forEach(item => {
if (item && typeof item === 'object' && item.type) {
analyzeNode(item, node);
}
});
} else if (child && typeof child === 'object' && child.type) {
analyzeNode(child, node);
}
}
}
// Handle scope exit
if (node.type === 'FunctionDeclaration' ||
node.type === 'FunctionExpression' ||
node.type === 'ArrowFunctionExpression' ||
node.type === 'BlockStatement' ||
node.type === 'Program') {
exitScope();
}
}
analyzeNode(ast);
return issues;
}
function detectWithASTSync(code, level, ignoreRegions, ignoreLines, ignoreNextLines) {
const issues = [];
let ast;
try {
// Try parsing as script first
try {
ast = window.acorn.parse(code, {
ecmaVersion: 2020,
allowReturnOutsideFunction: true,
locations: true
});
} catch (scriptError) {
// If script parsing fails due to top-level return, wrap in function
if (scriptError.message.includes('return')) {
const wrappedCode = `function nodeRedWrapper() {\n${code}\n}`;
ast = window.acorn.parse(wrappedCode, {
ecmaVersion: 2020,
locations: true
});
// Adjust line numbers for wrapped code
adjustLineNumbersInAST(ast, -1);
} else {
throw scriptError;
}
}
} catch (error) {
throw error; // Will fallback to regex
}
// Traverse AST to find issues
function traverse(node, parent = null) {
if (!node || typeof node !== 'object' || !node.type) return;
const nodeLineNumber = node.loc ? node.loc.start.line : null;
// Skip if this line should be ignored
if (nodeLineNumber && shouldIgnoreLineBrowser(nodeLineNumber, ignoreRegions, ignoreLines, ignoreNextLines)) {
return;
}
// Level 1: Detect top-level return statements (ONLY empty returns)
if (level >= 1 && node.type === 'ReturnStatement') {
// IMPORTANT: Only flag empty returns (debugging artifacts), not returns with values
const isEmptyReturn = !node.argument || node.argument === null;
if (isEmptyReturn) {
// Check if this return is TRULY at the top level (not inside any control structures)
let currentNode = node;
let isInNestedFunction = false;
let isInControlStructure = false;
let isAtTopLevel = false;
while (currentNode && currentNode.parent) {
const parentNode = currentNode.parent;
// If we find a function declaration/expression that's not our wrapper, we're nested
if ((parentNode.type === 'FunctionDeclaration' ||
parentNode.type === 'FunctionExpression' ||
parentNode.type === 'ArrowFunctionExpression') &&
!(parentNode.type === 'FunctionDeclaration' &&
parentNode.id && parentNode.id.name === 'nodeRedWrapper')) {
isInNestedFunction = true;
break;
}
// Check if we're inside any control structure
if (parentNode.type === 'IfStatement' ||
parentNode.type === 'ForStatement' ||
parentNode.type === 'WhileStatement' ||
parentNode.type === 'DoWhileStatement' ||
parentNode.type === 'ForInStatement' ||
parentNode.type === 'ForOfStatement' ||
parentNode.type === 'SwitchStatement' ||
parentNode.type === 'TryStatement' ||
parentNode.type === 'CatchClause' ||
parentNode.type === 'WithStatement') {
isInControlStructure = true;
}
// If we reach the Program or our wrapper function, we're at top level
if (parentNode.type === 'Program' ||
(parentNode.type === 'FunctionDeclaration' &&
parentNode.id && parentNode.id.name === 'nodeRedWrapper')) {
isAtTopLevel = true;
break;
}
currentNode = parentNode;
}
// Only flag empty returns that are at top level AND not in control structures AND not in nested functions
if (isAtTopLevel && !isInControlStructure && !isInNestedFunction) {
if (nodeLineNumber) {
issues.push({
type: "top-level-return",
message: "Remove this top-level return statement",
line: nodeLineNumber,
column: node.loc.start.column + 1,
endColumn: node.loc.end.column + 1,
severity: "warning"
});
}
}
}
}
// Level 2: Detect console.log, node.warn, and debugger statements
if (level >= 2) {
// Detect console.log and console.* calls
if (node.type === 'CallExpression' &&
node.callee && node.callee.type === 'MemberExpression' &&
node.callee.object && node.callee.object.name === 'console') {
if (nodeLineNumber) {
issues.push({
type: 'console-log',
message: `Remove this console.${node.callee.property.name}() debugging statement`,
line: nodeLineNumber,
column: node.loc.start.column + 1,
endColumn: node.loc.end.column + 1,
severity: 'info'
});
}
}
// Detect node.warn calls
if (node.type === 'CallExpression' &&
node.callee && node.callee.type === 'MemberExpression' &&
node.callee.object && node.callee.object.name === 'node' &&
node.callee.property && node.callee.property.name === 'warn') {
if (nodeLineNumber) {
issues.push({
type: 'node-warn',
message: 'Remove this node.warn() debugging statement',
line: nodeLineNumber,
column: node.loc.start.column + 1,
endColumn: node.loc.end.column + 1,
severity: 'info'
});
}
}
// Detect debugger statements
if (node.type === 'DebuggerStatement') {
if (nodeLineNumber) {
issues.push({
type: 'debugger-statement',
message: 'Remove this debugger statement',
line: nodeLineNumber,
column: node.loc.start.column + 1,
endColumn: node.loc.end.column + 1,
severity: 'warning'
});
}
}
}
// Level 3: Detect hardcoded test values
if (level >= 3) {
// Detect hardcoded string assignments
if (node.type === 'AssignmentExpression' &&
node.right && node.right.type === 'Literal' &&
typeof node.right.value === 'string') {
const value = node.right.value.toLowerCase();
if (value === 'test' || value === 'debug' || value === 'temp') {
if (nodeLineNumber) {
issues.push({
type: `hardcoded-${value}`,
message: `Remove hardcoded ${value} value`,
line: nodeLineNumber,
column: node.right.loc.start.column + 1,
endColumn: node.right.loc.end.column + 1,
severity: 'warning'
});
}
}
}
// Detect hardcoded variable declarations
if (node.type === 'VariableDeclarator' &&
node.init && node.init.type === 'Literal') {
if (typeof node.init.value === 'string') {
const value = node.init.value.toLowerCase();
if (value === 'test' || value === 'debug' || value === 'temp') {
if (nodeLineNumber) {
issues.push({
type: `hardcoded-${value}`,
message: `Remove hardcoded ${value} value`,
line: nodeLineNumber,
column: node.init.loc.start.column + 1,
endColumn: node.init.loc.end.column + 1,
severity: 'warning'
});
}
}
} else if (typeof node.init.value === 'number' && node.init.value === 123) {
if (nodeLineNumber) {
issues.push({
type: 'hardcoded-number',
message: 'Remove hardcoded test number',
line: nodeLineNumber,