UNPKG

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
<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,