diff --git a/.gitignore b/.gitignore index 5000924..51c8d01 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,6 @@ src/**/*.js.map test_*.py test_*.js simple_test.py + +# Sample project files (for testing module analysis) +sample_project/ diff --git a/README.md b/README.md index f9dca7e..d61e10f 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,28 @@ ## ✨ Features +### šŸ”„ **Data Flow Analysis (NEW!)** + +- **Global State Tracking**: Understand how global variables and state flow through your functions +- **Cross-Function Dependencies**: See which functions share data and how they interact +- **Dual Visualization Modes**: + - **Data Flow Graph**: Shows global state usage and data relationships + - **Function Call Graph**: Traditional call graph with data flow annotations +- **Smart Expansion**: Start from current function and automatically find related functions that share data +- **Large Codebase Navigation**: Trace data dependencies without manual ctrl+clicking through code +- **Multi-language Support**: Works with TypeScript, JavaScript, Python, Java, and more + +### šŸ—ļø **Module Analysis** + +- **Workspace-wide Analysis**: Get a 30,000 ft view of your entire codebase structure +- **Dependency Mapping**: Visualize how modules interact through imports, exports, and function calls +- **Multi-language Support**: Works with Python, TypeScript/JavaScript, and Java projects +- **Three Visualization Modes**: + - **Dependency Graph**: Shows module relationships and connections + - **Module Overview**: Displays imports and exports for each module + - **Dependency Matrix**: Connection counts and interaction patterns +- **Interactive Analysis**: Click to analyze workspace or focus on current file context + ### šŸ”„ **Real-Time Flowchart Generation** - Automatically generates flowcharts for functions as you navigate your code @@ -88,6 +110,37 @@ def complex_function(data): This function will show **Medium complexity (CC=6)** with āš ļø indicators on decision nodes. +**Module Analysis Example:** + +Create a simple Python project: + +```python +# utils.py +def calculate_sum(numbers): + return sum(numbers) + +class DataProcessor: + def process(self, data): + return calculate_sum(data) +``` + +```python +# main.py +from utils import DataProcessor, calculate_sum +import math + +def main(): + processor = DataProcessor() + result = processor.process([1, 2, 3, 4, 5]) + sqrt_result = math.sqrt(result) + print(f"Result: {sqrt_result}") +``` + +The module analysis will show: +- **Dependencies**: main.py → utils.py, main.py → math (built-in) +- **Exports**: utils.py exports `calculate_sum` function and `DataProcessor` class +- **Function Calls**: Cross-module calls from main.py to utils.py functions + ## Enhanced Node Readability ### Semantic Node Categories @@ -133,6 +186,23 @@ Access theme and complexity settings via VS Code Settings (`Cmd/Ctrl + ,`) under ## šŸš€ How to Use +### Module Analysis (NEW!) + +1. **Open the Module Analysis View**: Click the Visor icon in the Activity Bar and select the "Module Analysis" tab +2. **Analyze Your Codebase**: + - **Workspace Analysis**: Click "🌐 Analyze Workspace" to get a complete overview of all modules + - **Current File Context**: Click "šŸ“„ Analyze Current File" to focus on the active file and its dependencies +3. **Explore Different Views**: + - **Dependencies**: Shows how modules connect to each other + - **Overview**: Detailed view of imports and exports per module + - **Matrix**: Connection counts and dependency patterns +4. **Interactive Features**: + - Pan and zoom the diagram + - Switch between visualization modes + - Export diagrams as SVG (coming soon) + +### Function-Level Flowcharts + ### Getting Started 1. **Install the Extension**: Search for "Visor" in the VS Code Extensions marketplace @@ -236,6 +306,7 @@ Visor supports opening flowcharts in dedicated external windows for enhanced pro ### Core Components +#### Function-Level Analysis - **AbstractParser**: Enhanced base class with semantic node creation and complexity analysis - **ComplexityAnalyzer**: McCabe cyclomatic complexity calculation engine with language-specific support - **ComplexityConfig**: Configuration management for complexity thresholds, indicators, and display options @@ -247,6 +318,13 @@ Visor supports opening flowcharts in dedicated external windows for enhanced pro - **FlowchartViewProvider**: Sidebar integration with complexity display and external window launcher - **FlowchartPanelProvider**: Dedicated external window management with singleton pattern and proper cleanup +#### Module-Level Analysis (NEW!) +- **ModuleAnalyzer**: Core analyzer for workspace and file-context module analysis +- **ModuleAnalysisIR**: Intermediate representation for module dependencies, imports, exports, and function calls +- **Language Module Parsers**: Python, TypeScript/JavaScript, and Java parsers for extracting module-level information +- **ModuleMermaidGenerator**: Generates dependency graphs, module overviews, and dependency matrices +- **ModuleAnalysisProvider**: Webview provider for interactive module analysis visualization + ### Performance Features - **Object Pooling**: Reduces garbage collection overhead @@ -303,6 +381,18 @@ code --extensionDevelopmentPath=. - `yarn publish:patch`: Publishes a patch version - `yarn release`: Full release workflow (test + publish + git tags) +### Commands + +#### Function-Level Analysis +- **Visor: Generate Flowchart** - Generates flowchart for the current function +- **Visor: Open Flowchart in Panel** - Opens flowchart in a new panel/window +- **Visor: Open Flowchart to Side** - Opens flowchart beside current editor +- **Visor: Open Flowchart in New Column** - Opens flowchart in new column + +#### Module-Level Analysis +- **Visor: Analyze Workspace Modules** - Analyzes all modules in the workspace +- **Visor: Analyze Current File Modules** - Analyzes current file and its dependencies + ### Project Structure ``` diff --git a/package.json b/package.json index 1248160..496f204 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,12 @@ "name": "Flowchart", "type": "webview", "icon": "media/icon.png" + }, + { + "id": "visor.dataFlowView", + "name": "Data Flow", + "type": "webview", + "icon": "media/icon.png" } ] }, @@ -62,6 +68,16 @@ "command": "visor.maximizeFlowchartPanel", "title": "Maximize Flowchart Panel", "icon": "$(screen-full)" + }, + { + "command": "visor.analyzeWorkspaceModules", + "title": "Analyze Workspace Modules", + "icon": "$(globe)" + }, + { + "command": "visor.analyzeCurrentFileModules", + "title": "Analyze Current File Modules", + "icon": "$(file-code)" } ], "menus": { @@ -100,6 +116,13 @@ { "command": "visor.maximizeFlowchartPanel", "when": "editorTextFocus" + }, + { + "command": "visor.analyzeWorkspaceModules" + }, + { + "command": "visor.analyzeCurrentFileModules", + "when": "editorTextFocus" } ] }, diff --git a/sample_project/MainApplication.java b/sample_project/MainApplication.java new file mode 100644 index 0000000..323706b --- /dev/null +++ b/sample_project/MainApplication.java @@ -0,0 +1,39 @@ +// MainApplication.java +package com.example.main; + +import com.example.service.UserService; +import java.util.List; + +public class MainApplication { + private UserService userService; + + public MainApplication() { + this.userService = new UserService(); + initializeData(); + } + + private void initializeData() { + userService.addUser("john_doe"); + userService.addUser("jane_smith"); + userService.addUser("admin"); + } + + public void run() { + System.out.println("Application started"); + + List users = userService.getAllUsers(); + System.out.println("Total users: " + userService.getUserCount()); + + for (String user : users) { + System.out.println("User: " + user); + } + + boolean hasAdmin = userService.hasUser("admin"); + System.out.println("Has admin: " + hasAdmin); + } + + public static void main(String[] args) { + MainApplication app = new MainApplication(); + app.run(); + } +} \ No newline at end of file diff --git a/sample_project/UserService.java b/sample_project/UserService.java new file mode 100644 index 0000000..95893cf --- /dev/null +++ b/sample_project/UserService.java @@ -0,0 +1,29 @@ +// UserService.java +package com.example.service; + +import java.util.List; +import java.util.ArrayList; + +public class UserService { + private List users; + + public UserService() { + this.users = new ArrayList<>(); + } + + public void addUser(String username) { + users.add(username); + } + + public List getAllUsers() { + return new ArrayList<>(users); + } + + public int getUserCount() { + return users.size(); + } + + public boolean hasUser(String username) { + return users.contains(username); + } +} \ No newline at end of file diff --git a/sample_project/app.ts b/sample_project/app.ts new file mode 100644 index 0000000..0b14725 --- /dev/null +++ b/sample_project/app.ts @@ -0,0 +1,34 @@ +// app.ts +import { Calculator, add } from './math_utils'; + +class Application { + private calculator: Calculator; + + constructor() { + this.calculator = new Calculator(); + } + + run(): void { + console.log("Starting application..."); + + // Use imported functions + const sum = add(10, 20); + console.log(`Direct addition: ${sum}`); + + // Use calculator class + const calcResult1 = this.calculator.calculate('add', 5, 15); + const calcResult2 = this.calculator.calculate('multiply', 3, 7); + + console.log(`Calculator results: ${calcResult1}, ${calcResult2}`); + console.log(`History: ${this.calculator.getHistory()}`); + + this.displayResults([sum, calcResult1, calcResult2]); + } + + private displayResults(results: number[]): void { + console.log("All results:", results); + } +} + +const app = new Application(); +app.run(); \ No newline at end of file diff --git a/sample_project/main.py b/sample_project/main.py new file mode 100644 index 0000000..396f2f0 --- /dev/null +++ b/sample_project/main.py @@ -0,0 +1,24 @@ +# main.py +from utils import DataProcessor, calculate_average +import math + +def main(): + """Main function that demonstrates module usage.""" + data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + + # Use the DataProcessor class + processor = DataProcessor(data) + result = processor.process() + + print(f"Data processing results: {result}") + + # Use imported function directly + average = calculate_average(data) + print(f"Direct average calculation: {average}") + + # Use standard library + sqrt_avg = math.sqrt(average) + print(f"Square root of average: {sqrt_avg}") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/sample_project/math_utils.ts b/sample_project/math_utils.ts new file mode 100644 index 0000000..b1cb30f --- /dev/null +++ b/sample_project/math_utils.ts @@ -0,0 +1,34 @@ +// math_utils.ts +export function add(a: number, b: number): number { + return a + b; +} + +export function multiply(a: number, b: number): number { + return a * b; +} + +export class Calculator { + private history: number[] = []; + + calculate(operation: string, a: number, b: number): number { + let result: number; + + switch (operation) { + case 'add': + result = add(a, b); + break; + case 'multiply': + result = multiply(a, b); + break; + default: + result = 0; + } + + this.history.push(result); + return result; + } + + getHistory(): number[] { + return this.history.slice(); + } +} \ No newline at end of file diff --git a/sample_project/utils.py b/sample_project/utils.py new file mode 100644 index 0000000..57ca163 --- /dev/null +++ b/sample_project/utils.py @@ -0,0 +1,25 @@ +# utils.py +def calculate_sum(numbers): + """Calculate the sum of a list of numbers.""" + total = 0 + for num in numbers: + total += num + return total + +def calculate_average(numbers): + """Calculate the average of a list of numbers.""" + if not numbers: + return 0 + return calculate_sum(numbers) / len(numbers) + +class DataProcessor: + def __init__(self, data): + self.data = data + + def process(self): + """Process the data using the utility functions.""" + return { + 'sum': calculate_sum(self.data), + 'average': calculate_average(self.data), + 'count': len(self.data) + } \ No newline at end of file diff --git a/src/extension.ts b/src/extension.ts index 0db51a2..f7f071a 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,6 +1,7 @@ import * as vscode from "vscode"; import { FlowchartViewProvider } from "./view/FlowchartViewProvider"; import { FlowchartPanelProvider } from "./view/FlowchartPanelProvider"; +import { DataFlowProvider } from "./view/ModuleAnalysisProvider"; import { initLanguageServices } from "./logic/language-services"; export async function activate(context: vscode.ExtensionContext) { @@ -26,6 +27,15 @@ export async function activate(context: vscode.ExtensionContext) { ) ); + // Register data flow analysis provider + const dataFlowProvider = new DataFlowProvider(context.extensionUri); + context.subscriptions.push( + vscode.window.registerWebviewViewProvider( + DataFlowProvider.viewType, + dataFlowProvider + ) + ); + // Register panel commands context.subscriptions.push( vscode.commands.registerCommand("visor.openFlowchartInPanel", async () => { @@ -102,6 +112,25 @@ export async function activate(context: vscode.ExtensionContext) { panelProvider.refresh(); } } + }), + + // Data flow analysis commands + vscode.commands.registerCommand("visor.analyzeWorkspaceDataFlow", async () => { + try { + await dataFlowProvider.analyzeWorkspaceDataFlow(); + vscode.window.showInformationMessage("Workspace data flow analysis completed!"); + } catch (error) { + vscode.window.showErrorMessage(`Data flow analysis failed: ${error}`); + } + }), + + vscode.commands.registerCommand("visor.analyzeCurrentFunctionDataFlow", async () => { + try { + await dataFlowProvider.analyzeCurrentFunction(); + vscode.window.showInformationMessage("Current function data flow analysis completed!"); + } catch (error) { + vscode.window.showErrorMessage(`Data flow analysis failed: ${error}`); + } }) ); } diff --git a/src/ir/dataFlowIr.ts b/src/ir/dataFlowIr.ts new file mode 100644 index 0000000..68e0616 --- /dev/null +++ b/src/ir/dataFlowIr.ts @@ -0,0 +1,72 @@ +/** + * Data Flow Intermediate Representation for tracking global state and data dependencies + */ + +export interface DataFlowLocation { + start: number; + end: number; + line: number; + column: number; +} + +export interface GlobalStateAccess { + variableName: string; + accessType: 'read' | 'write' | 'read-write'; + location: DataFlowLocation; + context?: string; // Additional context about the access +} + +export interface FunctionInfo { + name: string; + filePath: string; + location: DataFlowLocation; + globalStateAccesses: GlobalStateAccess[]; + parameters: string[]; + returnType?: string; + calls: FunctionCallInfo[]; + isAsync: boolean; + complexity?: number; +} + +export interface FunctionCallInfo { + functionName: string; + targetFile?: string; + targetModule?: string; + location: DataFlowLocation; + arguments: DataFlowValue[]; +} + +export interface DataFlowValue { + name: string; + type: 'variable' | 'literal' | 'expression' | 'global'; + sourceLocation?: DataFlowLocation; +} + +export interface DataFlowEdge { + from: string; // function identifier + to: string; // function identifier + dataExchanged: DataFlowValue[]; + edgeType: 'function_call' | 'global_state' | 'parameter_passing' | 'return_value'; +} + +export interface GlobalStateVariable { + name: string; + type: string; + declarationLocation: DataFlowLocation; + accessedBy: string[]; // function identifiers + modifications: { + functionId: string; + location: DataFlowLocation; + operation: 'assign' | 'modify' | 'delete'; + }[]; +} + +export interface DataFlowAnalysisIR { + functions: FunctionInfo[]; + globalStateVariables: GlobalStateVariable[]; + dataFlowEdges: DataFlowEdge[]; + title: string; + rootFunction?: string; + scope: 'function' | 'file' | 'module' | 'workspace'; + analysisTimestamp: number; +} \ No newline at end of file diff --git a/src/ir/moduleIr.ts b/src/ir/moduleIr.ts new file mode 100644 index 0000000..cf049e4 --- /dev/null +++ b/src/ir/moduleIr.ts @@ -0,0 +1,53 @@ +export interface ModuleLocation { + start: number; + end: number; + line: number; + column: number; +} + +export interface ImportInfo { + name: string; + source: string; + type: 'default' | 'named' | 'namespace' | 'all'; + alias?: string; + location: ModuleLocation; +} + +export interface ExportInfo { + name: string; + type: 'function' | 'class' | 'variable' | 'type' | 'default'; + location: ModuleLocation; +} + +export interface FunctionCallInfo { + functionName: string; + module?: string; // If it's a cross-module call + location: ModuleLocation; +} + +export interface ModuleInfo { + filePath: string; + fileName: string; + language: string; + imports: ImportInfo[]; + exports: ExportInfo[]; + functionCalls: FunctionCallInfo[]; + functions: string[]; + classes: string[]; + variables: string[]; +} + +export interface ModuleDependency { + from: string; // module path + to: string; // module path + importedItems: string[]; + dependencyType: 'import' | 'function_call' | 'class_usage'; +} + +export interface ModuleAnalysisIR { + modules: ModuleInfo[]; + dependencies: ModuleDependency[]; + title: string; + rootModule?: string; + analysisTimestamp: number; +} \ No newline at end of file diff --git a/src/logic/DataFlowAnalyzer.ts b/src/logic/DataFlowAnalyzer.ts new file mode 100644 index 0000000..39df4ee --- /dev/null +++ b/src/logic/DataFlowAnalyzer.ts @@ -0,0 +1,590 @@ +import * as vscode from "vscode"; +import * as fs from "fs"; +import * as path from "path"; +import { DataFlowAnalysisIR, FunctionInfo, GlobalStateVariable, DataFlowEdge, DataFlowLocation, GlobalStateAccess, FunctionCallInfo, DataFlowValue } from "../ir/dataFlowIr"; + +export class DataFlowAnalyzer { + private workspaceRoot: string; + + constructor() { + this.workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || ""; + } + + /** + * Analyze data flow starting from the current function context + */ + public async analyzeCurrentFunctionContext(): Promise { + console.log("DataFlowAnalyzer: Starting current function context analysis"); + + const activeEditor = vscode.window.activeTextEditor; + if (!activeEditor) { + console.error("DataFlowAnalyzer: No active editor found"); + throw new Error("No active editor found"); + } + + console.log("DataFlowAnalyzer: Active editor found:", { + languageId: activeEditor.document.languageId, + fileName: activeEditor.document.fileName, + lineCount: activeEditor.document.lineCount, + cursorLine: activeEditor.selection.active.line, + cursorCharacter: activeEditor.selection.active.character + }); + + const document = activeEditor.document; + const position = activeEditor.selection.active; + const sourceCode = document.getText(); + const currentOffset = document.offsetAt(position); + + console.log("DataFlowAnalyzer: Document info:", { + sourceCodeLength: sourceCode.length, + currentOffset, + positionLine: position.line, + positionCharacter: position.character, + firstChars: sourceCode.substring(0, 100).replace(/\n/g, '\\n') + }); + + try { + // Start with current function and expand outward + const currentFunction = await this.extractCurrentFunction(sourceCode, document.languageId, currentOffset); + if (!currentFunction) { + console.warn("DataFlowAnalyzer: No function found at current position"); + // Instead of throwing error, create a minimal analysis + const analysis: DataFlowAnalysisIR = { + functions: [], + globalStateVariables: [], + dataFlowEdges: [], + title: `No function found at cursor`, + scope: 'function', + analysisTimestamp: Date.now() + }; + + // Try to find all functions in the file + console.log("DataFlowAnalyzer: Attempting to find all functions in file"); + const allFunctions = this.findAllFunctions(sourceCode, document.languageId); + console.log("DataFlowAnalyzer: Found functions:", allFunctions.map(f => f.name)); + + if (allFunctions.length > 0) { + analysis.functions = allFunctions; + analysis.title = `Functions in ${document.fileName.split('/').pop() || 'file'}`; + } + + return analysis; + } + + console.log("DataFlowAnalyzer: Found current function:", { + name: currentFunction.name, + parameters: currentFunction.parameters, + isAsync: currentFunction.isAsync, + locationLine: currentFunction.location.line + }); + + const analysis: DataFlowAnalysisIR = { + functions: [currentFunction], + globalStateVariables: [], + dataFlowEdges: [], + title: `Data Flow: ${currentFunction.name}`, + rootFunction: currentFunction.name, + scope: 'function', + analysisTimestamp: Date.now() + }; + + // Extract global state accesses from the current function + console.log("DataFlowAnalyzer: Analyzing global state usage"); + await this.analyzeGlobalStateUsage(analysis, [document.uri.fsPath]); + + console.log("DataFlowAnalyzer: Global state analysis complete:", { + globalVarsFound: analysis.globalStateVariables.length, + globalVarNames: analysis.globalStateVariables.map(g => g.name) + }); + + // Expand to find functions that share data with the current function + console.log("DataFlowAnalyzer: Expanding data flow analysis"); + await this.expandDataFlowAnalysis(analysis, currentFunction); + + console.log("DataFlowAnalyzer: Analysis complete:", { + totalFunctions: analysis.functions.length, + totalGlobalVars: analysis.globalStateVariables.length, + totalDataFlowEdges: analysis.dataFlowEdges.length + }); + + return analysis; + } catch (error) { + console.error("DataFlowAnalyzer: Error in analysis:", error); + throw error; + } + } + + /** + * Analyze data flow for the entire workspace + */ + public async analyzeWorkspaceDataFlow(): Promise { + console.log("DataFlowAnalyzer: Starting workspace data flow analysis"); + + if (!this.workspaceRoot) { + console.error("DataFlowAnalyzer: No workspace found"); + throw new Error("No workspace found"); + } + + console.log("DataFlowAnalyzer: Workspace root:", this.workspaceRoot); + + try { + // Find all source files in the workspace + console.log("DataFlowAnalyzer: Finding source files in workspace"); + const sourceFiles = await this.findSourceFiles(this.workspaceRoot); + console.log("DataFlowAnalyzer: Found source files:", { + count: sourceFiles.length, + files: sourceFiles.slice(0, 10).map(f => f.split('/').pop()) // Show first 10 filenames + }); + + const analysis: DataFlowAnalysisIR = { + functions: [], + globalStateVariables: [], + dataFlowEdges: [], + title: "Workspace Data Flow", + scope: 'workspace', + analysisTimestamp: Date.now() + }; + + // Analyze each file for functions and global state + let filesAnalyzed = 0; + for (const filePath of sourceFiles.slice(0, 20)) { // Limit to first 20 files for performance + try { + console.log(`DataFlowAnalyzer: Analyzing file ${filesAnalyzed + 1}/${Math.min(sourceFiles.length, 20)}: ${filePath.split('/').pop()}`); + await this.analyzeFileForDataFlow(analysis, filePath); + filesAnalyzed++; + } catch (error) { + console.warn(`DataFlowAnalyzer: Failed to analyze file ${filePath}:`, error); + } + } + + console.log("DataFlowAnalyzer: File analysis complete:", { + filesAnalyzed, + functionsFound: analysis.functions.length, + globalVarsFound: analysis.globalStateVariables.length + }); + + // Build data flow edges between functions + console.log("DataFlowAnalyzer: Building data flow connections"); + await this.buildDataFlowConnections(analysis); + + console.log("DataFlowAnalyzer: Workspace analysis complete:", { + totalFunctions: analysis.functions.length, + totalGlobalVars: analysis.globalStateVariables.length, + totalDataFlowEdges: analysis.dataFlowEdges.length + }); + + return analysis; + } catch (error) { + console.error("DataFlowAnalyzer: Error in workspace analysis:", error); + throw error; + } + } + + /** + * Find all functions in the source code (fallback when cursor detection fails) + */ + private findAllFunctions(sourceCode: string, languageId: string): FunctionInfo[] { + console.log("DataFlowAnalyzer: Finding all functions in file"); + const lines = sourceCode.split('\n'); + const functions: FunctionInfo[] = []; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const functionMatch = this.matchFunctionDefinition(line, languageId); + if (functionMatch) { + console.log(`DataFlowAnalyzer: Found function '${functionMatch.name}' at line ${i + 1}`); + + const functionStart = lines.slice(0, i).join('\n').length + (i > 0 ? 1 : 0); + const functionEnd = this.findFunctionEnd(lines, i, languageId); + + const functionInfo: FunctionInfo = { + name: functionMatch.name, + filePath: vscode.window.activeTextEditor?.document.uri.fsPath || '', + location: { + start: functionStart, + end: functionEnd, + line: i + 1, + column: functionMatch.column + }, + globalStateAccesses: [], + parameters: functionMatch.parameters, + calls: [], + isAsync: functionMatch.isAsync, + }; + + functions.push(functionInfo); + } + } + + console.log(`DataFlowAnalyzer: Found ${functions.length} functions total`); + return functions; + } + + /** + * Extract the current function at the given position + */ + private async extractCurrentFunction(sourceCode: string, languageId: string, position: number): Promise { + // This is a simplified implementation - would use proper AST parsing in production + const lines = sourceCode.split('\n'); + let currentLine = 0; + let currentPos = 0; + + // Find the line containing the position + for (let i = 0; i < lines.length; i++) { + if (currentPos + lines[i].length >= position) { + currentLine = i; + break; + } + currentPos += lines[i].length + 1; // +1 for newline + } + + // Look backwards to find a function definition + for (let i = currentLine; i >= 0; i--) { + const line = lines[i]; + const functionMatch = this.matchFunctionDefinition(line, languageId); + if (functionMatch) { + const functionStart = lines.slice(0, i).join('\n').length + (i > 0 ? 1 : 0); + const functionEnd = this.findFunctionEnd(lines, i, languageId); + + return { + name: functionMatch.name, + filePath: vscode.window.activeTextEditor?.document.uri.fsPath || '', + location: { + start: functionStart, + end: functionEnd, + line: i + 1, + column: functionMatch.column + }, + globalStateAccesses: [], + parameters: functionMatch.parameters, + calls: [], + isAsync: functionMatch.isAsync, + }; + } + } + + return null; + } + + /** + * Match function definition patterns for different languages + */ + private matchFunctionDefinition(line: string, languageId: string): {name: string, parameters: string[], column: number, isAsync: boolean} | null { + let patterns: RegExp[] = []; + + switch (languageId) { + case 'typescript': + case 'javascript': + patterns = [ + /(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/, + /(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*=>/, + /(\w+)\s*:\s*\([^)]*\)\s*=>/, + /(?:async\s+)?(\w+)\s*\([^)]*\)\s*{/ + ]; + break; + case 'python': + patterns = [ + /def\s+(\w+)\s*\(([^)]*)\):/, + /async\s+def\s+(\w+)\s*\(([^)]*)\):/ + ]; + break; + default: + patterns = [/function\s+(\w+)\s*\(([^)]*)\)/]; + } + + for (const pattern of patterns) { + const match = line.match(pattern); + if (match) { + const isAsync = line.includes('async'); + const parameters = match[2] ? match[2].split(',').map(p => p.trim()) : []; + return { + name: match[1], + parameters, + column: match.index || 0, + isAsync + }; + } + } + + return null; + } + + /** + * Find the end of a function (simplified implementation) + */ + private findFunctionEnd(lines: string[], startLine: number, languageId: string): number { + let braceCount = 0; + let inFunction = false; + + for (let i = startLine; i < lines.length; i++) { + const line = lines[i]; + + for (const char of line) { + if (char === '{') { + braceCount++; + inFunction = true; + } else if (char === '}') { + braceCount--; + if (inFunction && braceCount === 0) { + return lines.slice(0, i + 1).join('\n').length; + } + } + } + } + + return lines.join('\n').length; + } + + /** + * Analyze global state usage in the given files + */ + private async analyzeGlobalStateUsage(analysis: DataFlowAnalysisIR, filePaths: string[]): Promise { + for (const filePath of filePaths) { + try { + const content = await fs.promises.readFile(filePath, 'utf-8'); + const languageId = this.getLanguageFromPath(filePath); + + // Extract global variables and their usage + const globals = this.extractGlobalVariables(content, languageId, filePath); + analysis.globalStateVariables.push(...globals); + + // Update function global accesses + for (const func of analysis.functions) { + if (func.filePath === filePath) { + func.globalStateAccesses = this.extractGlobalAccesses(content, func, globals); + } + } + } catch (error) { + console.warn(`Failed to analyze file ${filePath}:`, error); + } + } + } + + /** + * Extract global variables from source code (simplified implementation) + */ + private extractGlobalVariables(content: string, languageId: string, filePath: string): GlobalStateVariable[] { + const globals: GlobalStateVariable[] = []; + const lines = content.split('\n'); + + lines.forEach((line, index) => { + let patterns: RegExp[] = []; + + switch (languageId) { + case 'typescript': + case 'javascript': + patterns = [ + /(?:export\s+)?(?:let|const|var)\s+(\w+)/g, + /(?:export\s+)?class\s+(\w+)/g, + /(?:export\s+)?interface\s+(\w+)/g + ]; + break; + case 'python': + patterns = [/^(\w+)\s*=/g]; + break; + } + + for (const pattern of patterns) { + let match; + while ((match = pattern.exec(line)) !== null) { + globals.push({ + name: match[1], + type: 'unknown', + declarationLocation: { + start: content.split('\n').slice(0, index).join('\n').length, + end: content.split('\n').slice(0, index + 1).join('\n').length, + line: index + 1, + column: match.index || 0 + }, + accessedBy: [], + modifications: [] + }); + } + } + }); + + return globals; + } + + /** + * Extract global accesses within a function + */ + private extractGlobalAccesses(content: string, func: FunctionInfo, globals: GlobalStateVariable[]): GlobalStateAccess[] { + const accesses: GlobalStateAccess[] = []; + const funcContent = content.substring(func.location.start, func.location.end); + + globals.forEach(globalVar => { + const readPattern = new RegExp(`\\b${globalVar.name}\\b(?!\\s*=)`, 'g'); + const writePattern = new RegExp(`\\b${globalVar.name}\\s*=`, 'g'); + + let match; + while ((match = readPattern.exec(funcContent)) !== null) { + accesses.push({ + variableName: globalVar.name, + accessType: 'read', + location: { + start: func.location.start + match.index, + end: func.location.start + match.index + match[0].length, + line: 0, // Would calculate properly in production + column: match.index + } + }); + } + + while ((match = writePattern.exec(funcContent)) !== null) { + accesses.push({ + variableName: globalVar.name, + accessType: 'write', + location: { + start: func.location.start + match.index, + end: func.location.start + match.index + match[0].length, + line: 0, + column: match.index + } + }); + } + }); + + return accesses; + } + + /** + * Expand analysis to include functions that share data with the current function + */ + private async expandDataFlowAnalysis(analysis: DataFlowAnalysisIR, rootFunction: FunctionInfo): Promise { + // Find functions that read/write the same global state + const relatedGlobals = rootFunction.globalStateAccesses.map(access => access.variableName); + + if (relatedGlobals.length === 0) { + return; + } + + // Find other files that might contain functions using the same globals + const workspaceFiles = await this.findSourceFiles(this.workspaceRoot); + + for (const filePath of workspaceFiles.slice(0, 10)) { // Limit for performance + if (filePath === rootFunction.filePath) continue; + + try { + const content = await fs.promises.readFile(filePath, 'utf-8'); + const hasSharedGlobals = relatedGlobals.some(global => + content.includes(global) + ); + + if (hasSharedGlobals) { + // Extract functions from this file that use shared globals + const functions = await this.extractFunctionsFromFile(filePath, content); + const relevantFunctions = functions.filter(func => + func.globalStateAccesses.some(access => + relatedGlobals.includes(access.variableName) + ) + ); + + analysis.functions.push(...relevantFunctions); + } + } catch (error) { + console.warn(`Failed to expand analysis for ${filePath}:`, error); + } + } + + // Update scope to reflect expansion + analysis.scope = 'module'; + analysis.title = `Data Flow: ${rootFunction.name} + Dependencies`; + } + + /** + * Find all source files in the workspace + */ + private async findSourceFiles(rootPath: string): Promise { + const sourceFiles: string[] = []; + const extensions = ['.ts', '.js', '.py', '.java', '.cpp', '.c', '.rs']; + + const traverseDirectory = async (dirPath: string) => { + try { + const entries = await fs.promises.readdir(dirPath, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = path.join(dirPath, entry.name); + + if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules') { + await traverseDirectory(fullPath); + } else if (entry.isFile() && extensions.some(ext => entry.name.endsWith(ext))) { + sourceFiles.push(fullPath); + } + } + } catch (error) { + console.warn(`Failed to traverse directory ${dirPath}:`, error); + } + }; + + await traverseDirectory(rootPath); + return sourceFiles.slice(0, 50); // Limit for performance + } + + /** + * Extract functions from a file + */ + private async extractFunctionsFromFile(filePath: string, content: string): Promise { + const functions: FunctionInfo[] = []; + const languageId = this.getLanguageFromPath(filePath); + const lines = content.split('\n'); + + lines.forEach((line, index) => { + const functionMatch = this.matchFunctionDefinition(line, languageId); + if (functionMatch) { + const functionStart = lines.slice(0, index).join('\n').length + (index > 0 ? 1 : 0); + const functionEnd = this.findFunctionEnd(lines, index, languageId); + + functions.push({ + name: functionMatch.name, + filePath, + location: { + start: functionStart, + end: functionEnd, + line: index + 1, + column: functionMatch.column + }, + globalStateAccesses: [], + parameters: functionMatch.parameters, + calls: [], + isAsync: functionMatch.isAsync, + }); + } + }); + + return functions; + } + + /** + * Get language ID from file path + */ + private getLanguageFromPath(filePath: string): string { + const ext = path.extname(filePath).toLowerCase(); + switch (ext) { + case '.ts': return 'typescript'; + case '.js': return 'javascript'; + case '.py': return 'python'; + case '.java': return 'java'; + case '.cpp': case '.cc': case '.cxx': return 'cpp'; + case '.c': return 'c'; + case '.rs': return 'rust'; + default: return 'unknown'; + } + } + + /** + * Analyze a file for data flow (stub implementation) + */ + private async analyzeFileForDataFlow(analysis: DataFlowAnalysisIR, filePath: string): Promise { + // Implementation would extract functions and global state from the file + // This is a simplified stub + } + + /** + * Build data flow connections between functions + */ + private async buildDataFlowConnections(analysis: DataFlowAnalysisIR): Promise { + // Implementation would analyze function calls and shared global state + // to create data flow edges + } +} \ No newline at end of file diff --git a/src/logic/DataFlowMermaidGenerator.ts b/src/logic/DataFlowMermaidGenerator.ts new file mode 100644 index 0000000..a6139e9 --- /dev/null +++ b/src/logic/DataFlowMermaidGenerator.ts @@ -0,0 +1,310 @@ +import { DataFlowAnalysisIR, FunctionInfo, GlobalStateVariable, DataFlowEdge } from "../ir/dataFlowIr"; + +export class DataFlowMermaidGenerator { + private theme: string = "default"; + private vsCodeTheme: string = "light"; + + public setTheme(selectedTheme: string, vsCodeTheme: string): void { + this.theme = selectedTheme; + this.vsCodeTheme = vsCodeTheme; + } + + /** + * Generate a Mermaid diagram for data flow analysis + */ + public generateDataFlowGraph(analysis: DataFlowAnalysisIR): string { + const lines: string[] = []; + + // Start with graph definition + lines.push("graph TD"); + lines.push(""); + + // Add function nodes + const functionNodes = this.generateFunctionNodes(analysis.functions); + lines.push(...functionNodes); + lines.push(""); + + // Add global state nodes + const globalNodes = this.generateGlobalStateNodes(analysis.globalStateVariables); + lines.push(...globalNodes); + lines.push(""); + + // Add data flow connections + const connections = this.generateDataFlowConnections(analysis); + lines.push(...connections); + lines.push(""); + + // Add styling + const styling = this.generateStyling(analysis); + lines.push(...styling); + + return lines.join("\n"); + } + + /** + * Generate function nodes for the diagram + */ + private generateFunctionNodes(functions: FunctionInfo[]): string[] { + const lines: string[] = []; + + functions.forEach((func, index) => { + const nodeId = `func_${this.sanitizeId(func.name)}`; + const isAsync = func.isAsync ? "⚔ " : ""; + const complexity = func.complexity ? ` (${func.complexity})` : ""; + + // Different shapes based on function characteristics + let shape = "rect"; + if (func.isAsync) { + shape = "round"; + } else if (func.globalStateAccesses.length > 0) { + shape = "stadium"; + } + + const label = `${isAsync}${func.name}${complexity}`; + + switch (shape) { + case "round": + lines.push(` ${nodeId}((${label}))`); + break; + case "stadium": + lines.push(` ${nodeId}([${label}])`); + break; + default: + lines.push(` ${nodeId}[${label}]`); + } + + // Add file information as a comment + const fileName = func.filePath.split('/').pop() || 'unknown'; + lines.push(` %% ${func.name} in ${fileName}`); + }); + + return lines; + } + + /** + * Generate global state variable nodes + */ + private generateGlobalStateNodes(globals: GlobalStateVariable[]): string[] { + const lines: string[] = []; + + if (globals.length === 0) { + return lines; + } + + lines.push(" %% Global State Variables"); + + globals.forEach(global => { + const nodeId = `global_${this.sanitizeId(global.name)}`; + const readCount = global.accessedBy.length; + const writeCount = global.modifications.length; + + let label = `šŸ“Š ${global.name}`; + if (readCount > 0 || writeCount > 0) { + label += `
R:${readCount} W:${writeCount}`; + } + + // Use diamond shape for global variables + lines.push(` ${nodeId}{${label}}`); + }); + + return lines; + } + + /** + * Generate data flow connections between nodes + */ + private generateDataFlowConnections(analysis: DataFlowAnalysisIR): string[] { + const lines: string[] = []; + const connections = new Set(); // Prevent duplicate edges + + // Connect functions to global state they access + analysis.functions.forEach(func => { + const funcId = `func_${this.sanitizeId(func.name)}`; + + func.globalStateAccesses.forEach(access => { + const globalId = `global_${this.sanitizeId(access.variableName)}`; + + let edgeStyle = ""; + let label = ""; + + switch (access.accessType) { + case "read": + edgeStyle = "-.->"; // Dashed arrow for reads + label = "reads"; + break; + case "write": + edgeStyle = "==>"; // Thick arrow for writes + label = "writes"; + break; + case "read-write": + edgeStyle = "<--->"; // Bidirectional for read-write + label = "modifies"; + break; + } + + const connection = ` ${funcId} ${edgeStyle}|${label}| ${globalId}`; + if (!connections.has(connection)) { + connections.add(connection); + lines.push(connection); + } + }); + }); + + // Connect functions that share global state + const globalGroups = this.groupFunctionsByGlobalState(analysis); + + Object.entries(globalGroups).forEach(([globalVar, funcs]) => { + if (funcs.length > 1) { + // Connect functions that share the same global state + for (let i = 0; i < funcs.length - 1; i++) { + for (let j = i + 1; j < funcs.length; j++) { + const func1Id = `func_${this.sanitizeId(funcs[i].name)}`; + const func2Id = `func_${this.sanitizeId(funcs[j].name)}`; + + // Use dotted line to show shared state relationship + const connection = ` ${func1Id} -.->|shares ${globalVar}| ${func2Id}`; + if (!connections.has(connection)) { + connections.add(connection); + lines.push(connection); + } + } + } + } + }); + + // Add function call relationships (if available) + analysis.functions.forEach(func => { + const funcId = `func_${this.sanitizeId(func.name)}`; + + func.calls.forEach(call => { + const targetFunc = analysis.functions.find(f => f.name === call.functionName); + if (targetFunc) { + const targetId = `func_${this.sanitizeId(targetFunc.name)}`; + const connection = ` ${funcId} -->|calls| ${targetId}`; + + if (!connections.has(connection)) { + connections.add(connection); + lines.push(connection); + } + } + }); + }); + + return lines; + } + + /** + * Group functions by the global state they access + */ + private groupFunctionsByGlobalState(analysis: DataFlowAnalysisIR): Record { + const groups: Record = {}; + + analysis.functions.forEach(func => { + func.globalStateAccesses.forEach(access => { + if (!groups[access.variableName]) { + groups[access.variableName] = []; + } + if (!groups[access.variableName].includes(func)) { + groups[access.variableName].push(func); + } + }); + }); + + return groups; + } + + /** + * Generate styling for the diagram + */ + private generateStyling(analysis: DataFlowAnalysisIR): string[] { + const lines: string[] = []; + + lines.push(" %% Styling"); + + // Style function nodes + analysis.functions.forEach((func, index) => { + const nodeId = `func_${this.sanitizeId(func.name)}`; + const classNumber = index % 4; // Cycle through 4 different styles + + if (func.name === analysis.rootFunction) { + // Highlight the root function + lines.push(` classDef rootFunction fill:#e1f5fe,stroke:#0277bd,stroke-width:3px,color:#000`); + lines.push(` class ${nodeId} rootFunction;`); + } else if (func.isAsync) { + lines.push(` classDef asyncFunction fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px,color:#000`); + lines.push(` class ${nodeId} asyncFunction;`); + } else if (func.globalStateAccesses.length > 0) { + lines.push(` classDef stateFunction fill:#fff3e0,stroke:#f57c00,stroke-width:2px,color:#000`); + lines.push(` class ${nodeId} stateFunction;`); + } else { + lines.push(` classDef normalFunction fill:#f5f5f5,stroke:#616161,stroke-width:1px,color:#000`); + lines.push(` class ${nodeId} normalFunction;`); + } + }); + + // Style global state nodes + analysis.globalStateVariables.forEach(global => { + const nodeId = `global_${this.sanitizeId(global.name)}`; + const writeCount = global.modifications.length; + + if (writeCount > 0) { + // Mutable global state + lines.push(` classDef mutableGlobal fill:#ffebee,stroke:#c62828,stroke-width:2px,color:#000`); + lines.push(` class ${nodeId} mutableGlobal;`); + } else { + // Read-only global state + lines.push(` classDef readonlyGlobal fill:#e8f5e8,stroke:#2e7d32,stroke-width:2px,color:#000`); + lines.push(` class ${nodeId} readonlyGlobal;`); + } + }); + + return lines; + } + + /** + * Sanitize node IDs for Mermaid compatibility + */ + private sanitizeId(input: string): string { + return input.replace(/[^a-zA-Z0-9_]/g, "_"); + } + + /** + * Generate a simplified function call graph + */ + public generateFunctionCallGraph(analysis: DataFlowAnalysisIR): string { + const lines: string[] = []; + + lines.push("graph LR"); + lines.push(""); + + // Add function nodes (simplified) + analysis.functions.forEach(func => { + const nodeId = `func_${this.sanitizeId(func.name)}`; + const fileName = func.filePath.split('/').pop() || 'unknown'; + const label = `${func.name}
${fileName}`; + + if (func.name === analysis.rootFunction) { + lines.push(` ${nodeId}["šŸŽÆ ${label}"]`); + } else { + lines.push(` ${nodeId}["${label}"]`); + } + }); + + lines.push(""); + + // Add call relationships + analysis.functions.forEach(func => { + const funcId = `func_${this.sanitizeId(func.name)}`; + + func.calls.forEach(call => { + const targetFunc = analysis.functions.find(f => f.name === call.functionName); + if (targetFunc) { + const targetId = `func_${this.sanitizeId(targetFunc.name)}`; + lines.push(` ${funcId} --> ${targetId}`); + } + }); + }); + + return lines.join("\n"); + } +} \ No newline at end of file diff --git a/src/logic/ModuleAnalyzer.ts b/src/logic/ModuleAnalyzer.ts new file mode 100644 index 0000000..a839050 --- /dev/null +++ b/src/logic/ModuleAnalyzer.ts @@ -0,0 +1,332 @@ +import * as vscode from "vscode"; +import * as path from "path"; +import { + ModuleAnalysisIR, + ModuleInfo, + ImportInfo, + ExportInfo, + FunctionCallInfo, + ModuleDependency, + ModuleLocation, +} from "../ir/moduleIr"; +import { analyzePythonModule } from "./language-services/python/PyModuleParser"; +import { analyzeTypeScriptModule } from "./language-services/typescript/TsModuleParser"; +import { analyzeJavaModule } from "./language-services/java/JavaModuleParser"; + +export class ModuleAnalyzer { + private supportedLanguages = ["python", "typescript", "javascript", "java"]; + private supportedExtensions = new Map([ + [".py", "python"], + [".ts", "typescript"], + [".js", "javascript"], + [".tsx", "typescript"], + [".jsx", "javascript"], + [".java", "java"], + ]); + + /** + * Analyzes all modules in the current workspace + */ + public async analyzeWorkspace(): Promise { + const workspaceFolders = vscode.workspace.workspaceFolders; + if (!workspaceFolders || workspaceFolders.length === 0) { + throw new Error("No workspace folder found"); + } + + const rootPath = workspaceFolders[0].uri.fsPath; + const modules: ModuleInfo[] = []; + + // Find all supported files in the workspace + const files = await this.findSupportedFiles(rootPath); + console.log( + `[ModuleAnalyzer] Found ${files.length} supported files:`, + files + ); + + // Analyze each file + for (const file of files) { + try { + console.log(`[ModuleAnalyzer] Analyzing file: ${file}`); + const moduleInfo = await this.analyzeFile(file); + if (moduleInfo) { + console.log(`[ModuleAnalyzer] Successfully analyzed ${file}:`, { + imports: moduleInfo.imports.length, + exports: moduleInfo.exports.length, + functionCalls: moduleInfo.functionCalls.length, + }); + modules.push(moduleInfo); + } else { + console.log(`[ModuleAnalyzer] No module info returned for ${file}`); + } + } catch (error) { + console.warn(`Failed to analyze file ${file}:`, error); + } + } + + console.log(`[ModuleAnalyzer] Total modules analyzed: ${modules.length}`); + + // Build dependency graph + const dependencies = this.buildDependencyGraph(modules); + console.log( + `[ModuleAnalyzer] Built dependency graph with ${dependencies.length} dependencies:`, + dependencies.map( + (d) => + `${path.basename(d.from)} -> ${path.basename(d.to)} (${ + d.dependencyType + })` + ) + ); + + return { + modules, + dependencies, + title: `Module Analysis - ${path.basename(rootPath)}`, + rootModule: modules.length > 0 ? modules[0].filePath : undefined, + analysisTimestamp: Date.now(), + }; + } + + /** + * Analyzes modules related to the current active file + */ + public async analyzeActiveFileContext(): Promise { + const activeEditor = vscode.window.activeTextEditor; + if (!activeEditor) { + throw new Error("No active editor"); + } + + const currentFile = activeEditor.document.fileName; + const workspaceFolder = vscode.workspace.getWorkspaceFolder( + activeEditor.document.uri + ); + if (!workspaceFolder) { + throw new Error("File is not in a workspace"); + } + + const modules: ModuleInfo[] = []; + const analyzed = new Set(); + + // Analyze current file and its dependencies recursively + await this.analyzeFileRecursive(currentFile, modules, analyzed, 2); // Max depth of 2 + + // Build dependency graph + const dependencies = this.buildDependencyGraph(modules); + + return { + modules, + dependencies, + title: `Module Context - ${path.basename(currentFile)}`, + rootModule: currentFile, + analysisTimestamp: Date.now(), + }; + } + + private async findSupportedFiles(rootPath: string): Promise { + const files: string[] = []; + const pattern = "**/*.{py,ts,js,tsx,jsx,java}"; + const exclude = "**/node_modules/**"; + + const fileUris = await vscode.workspace.findFiles(pattern, exclude); + return fileUris.map((uri) => uri.fsPath); + } + + private async analyzeFile(filePath: string): Promise { + const extension = path.extname(filePath); + const language = this.supportedExtensions.get(extension); + + if (!language) { + return null; + } + + try { + const document = await vscode.workspace.openTextDocument(filePath); + const code = document.getText(); + + switch (language) { + case "python": + return await analyzePythonModule(code, filePath); + case "typescript": + case "javascript": + return await analyzeTypeScriptModule(code, filePath, language); + case "java": + return await analyzeJavaModule(code, filePath); + default: + return null; + } + } catch (error) { + console.warn(`Failed to analyze file ${filePath}:`, error); + return null; + } + } + + private async analyzeFileRecursive( + filePath: string, + modules: ModuleInfo[], + analyzed: Set, + maxDepth: number + ): Promise { + if (analyzed.has(filePath) || maxDepth <= 0) { + return; + } + + analyzed.add(filePath); + const moduleInfo = await this.analyzeFile(filePath); + + if (moduleInfo) { + modules.push(moduleInfo); + + // Analyze imported modules + for (const importInfo of moduleInfo.imports) { + const resolvedPath = this.resolveImportPath( + importInfo.source, + filePath + ); + if (resolvedPath && !analyzed.has(resolvedPath)) { + await this.analyzeFileRecursive( + resolvedPath, + modules, + analyzed, + maxDepth - 1 + ); + } + } + } + } + + private resolveImportPath( + importSource: string, + currentFile: string + ): string | null { + const currentDir = path.dirname(currentFile); + const fileExt = path.extname(currentFile); + const language = this.supportedExtensions.get(fileExt); + + // Handle relative imports (./something or ../something) + if (importSource.startsWith("./") || importSource.startsWith("../")) { + const resolvedPath = path.resolve(currentDir, importSource); + + // Try different extensions + for (const [ext, _] of this.supportedExtensions) { + const fullPath = resolvedPath + ext; + if (this.fileExists(fullPath)) { + return fullPath; + } + } + + // Try index files + for (const [ext, _] of this.supportedExtensions) { + const indexPath = path.join(resolvedPath, "index" + ext); + if (this.fileExists(indexPath)) { + return indexPath; + } + } + return null; + } + + // Handle Python-style imports (no ./ prefix, but same directory) + if ( + language === "python" && + !importSource.includes("/") && + !importSource.includes(".") + ) { + const pythonPath = path.join(currentDir, importSource + ".py"); + if (this.fileExists(pythonPath)) { + return pythonPath; + } + + // Check for package with __init__.py + const packageInit = path.join(currentDir, importSource, "__init__.py"); + if (this.fileExists(packageInit)) { + return packageInit; + } + } + + // Handle JavaScript/TypeScript imports without extensions + if ( + (language === "typescript" || language === "javascript") && + !importSource.includes("/") + ) { + // Try same directory first + for (const [ext, lang] of this.supportedExtensions) { + if (lang === language) { + const sameDirPath = path.join(currentDir, importSource + ext); + if (this.fileExists(sameDirPath)) { + return sameDirPath; + } + } + } + } + + return null; + } + + private fileExists(filePath: string): boolean { + try { + const fs = require("fs"); + return fs.existsSync(filePath); + } catch { + return false; + } + } + + private buildDependencyGraph(modules: ModuleInfo[]): ModuleDependency[] { + const dependencies: ModuleDependency[] = []; + const moduleMap = new Map(modules.map((m) => [m.filePath, m])); + const dependencySet = new Set(); // Avoid duplicates + + for (const module of modules) { + // Process imports + for (const importInfo of module.imports) { + const resolvedPath = this.resolveImportPath( + importInfo.source, + module.filePath + ); + if (resolvedPath && moduleMap.has(resolvedPath)) { + const key = `${module.filePath}->${resolvedPath}:import`; + if (!dependencySet.has(key)) { + dependencies.push({ + from: module.filePath, + to: resolvedPath, + importedItems: [importInfo.name], + dependencyType: "import", + }); + dependencySet.add(key); + } + } + } + + // Process function calls for better dependency tracking + for (const call of module.functionCalls) { + if (call.module) { + // Find target module by matching import sources + for (const importInfo of module.imports) { + if ( + importInfo.source === call.module || + importInfo.alias === call.module + ) { + const resolvedPath = this.resolveImportPath( + importInfo.source, + module.filePath + ); + if (resolvedPath && moduleMap.has(resolvedPath)) { + const key = `${module.filePath}->${resolvedPath}:function_call`; + if (!dependencySet.has(key)) { + dependencies.push({ + from: module.filePath, + to: resolvedPath, + importedItems: [call.functionName], + dependencyType: "function_call", + }); + dependencySet.add(key); + } + break; + } + } + } + } + } + } + + return dependencies; + } +} diff --git a/src/logic/ModuleMermaidGenerator.ts b/src/logic/ModuleMermaidGenerator.ts new file mode 100644 index 0000000..bc6657c --- /dev/null +++ b/src/logic/ModuleMermaidGenerator.ts @@ -0,0 +1,265 @@ +import { + ModuleAnalysisIR, + ModuleInfo, + ModuleDependency, + ExportInfo, + ImportInfo, +} from "../ir/moduleIr"; +import { SubtleThemeManager, ThemeStyles } from "./utils/ThemeManager"; +import * as path from "path"; + +export class ModuleMermaidGenerator { + private themeStyles: ThemeStyles; + private vsCodeTheme: "light" | "dark" = "dark"; + + constructor() { + // Initialize with default theme + this.themeStyles = SubtleThemeManager.getThemeStyles("monokai", "dark"); + } + + /** + * Set theme configuration (matching BaseFlowchartProvider pattern) + */ + public setTheme(themeKey: string, vsCodeTheme: "light" | "dark"): void { + this.themeStyles = SubtleThemeManager.getThemeStyles(themeKey, vsCodeTheme); + this.vsCodeTheme = vsCodeTheme; + } + /** + * Generates a Mermaid graph showing module dependencies and interactions + */ + public generateModuleGraph(analysis: ModuleAnalysisIR): string { + let mermaid = "graph TD\n"; + + // Add styling + mermaid += this.generateStyling(); + + // Generate nodes for each module (simplified) + const nodeIds = new Map(); + let nodeCounter = 0; + + for (const module of analysis.modules) { + const nodeId = `M${nodeCounter++}`; + nodeIds.set(module.filePath, nodeId); + + const displayName = this.getModuleDisplayName(module); + + // Simple, clean node labels - just the module name + mermaid += ` ${nodeId}["${displayName}"]\n`; + + // Style based on language + const styleClass = this.getLanguageStyleClass(module.language); + mermaid += ` class ${nodeId} ${styleClass}\n`; + + // Mark root module differently + if (module.filePath === analysis.rootModule) { + mermaid += ` class ${nodeId} rootModule\n`; + } + } + + mermaid += "\n"; + + // Generate edges for dependencies (simplified) + for (const dependency of analysis.dependencies) { + const fromId = nodeIds.get(dependency.from); + const toId = nodeIds.get(dependency.to); + + if (fromId && toId) { + const edgeStyle = this.getDependencyEdgeStyle( + dependency.dependencyType + ); + // Simple arrows without labels to reduce clutter + mermaid += ` ${fromId} ${edgeStyle} ${toId}\n`; + } + } + + return mermaid; + } + + /** + * Generates a detailed module overview showing exports and imports + */ + public generateModuleOverview(analysis: ModuleAnalysisIR): string { + let mermaid = "graph TB\n"; + mermaid += this.generateStyling(); + + let nodeCounter = 0; + + for (const module of analysis.modules) { + const moduleId = `M${nodeCounter++}`; + const displayName = this.getModuleDisplayName(module); + + // Main module node - escape quotes + const escapedDisplayName = displayName.replace(/"/g, """); + mermaid += ` ${moduleId}["šŸ“ ${escapedDisplayName}"]\n`; + mermaid += ` class ${moduleId} ${this.getLanguageStyleClass( + module.language + )}\n`; + + // Exports subgraph + if (module.exports.length > 0) { + const exportsId = `E${moduleId}`; + const exportsList = module.exports + .map((e: ExportInfo) => `${e.type}: ${e.name}`) + .join("
"); + const escapedExportsList = exportsList.replace(/"/g, """); + mermaid += ` ${exportsId}["šŸ“¤ Exports
${escapedExportsList}"]\n`; + mermaid += ` class ${exportsId} exportsNode\n`; + mermaid += ` ${moduleId} --> ${exportsId}\n`; + } + + // Imports subgraph + if (module.imports.length > 0) { + const importsId = `I${moduleId}`; + const importsList = module.imports + .map((i: ImportInfo) => `${i.name} from ${i.source}`) + .join("
"); + const escapedImportsList = importsList.replace(/"/g, """); + mermaid += ` ${importsId}["šŸ“„ Imports
${escapedImportsList}"]\n`; + mermaid += ` class ${importsId} importsNode\n`; + mermaid += ` ${importsId} --> ${moduleId}\n`; + } + } + + return mermaid; + } + + /** + * Generates a dependency matrix view + */ + public generateDependencyMatrix(analysis: ModuleAnalysisIR): string { + const modules = analysis.modules; + const dependencies = analysis.dependencies; + + let mermaid = "flowchart LR\n"; + mermaid += this.generateStyling(); + + // Create a matrix-like representation + let nodeCounter = 0; + const moduleIds = new Map(); + + // Create module nodes + for (const module of modules) { + const nodeId = `M${nodeCounter++}`; + moduleIds.set(module.filePath, nodeId); + + const displayName = this.getModuleDisplayName(module); + const dependencyCount = dependencies.filter( + (d: ModuleDependency) => d.from === module.filePath + ).length; + const dependentCount = dependencies.filter( + (d: ModuleDependency) => d.to === module.filePath + ).length; + + // Escape quotes in display name + const escapedDisplayName = displayName.replace(/"/g, """); + mermaid += ` ${nodeId}["${escapedDisplayName}
→${dependencyCount} ←${dependentCount}"]\n`; + mermaid += ` class ${nodeId} ${this.getLanguageStyleClass( + module.language + )}\n`; + + // Mark root module differently + if (module.filePath === analysis.rootModule) { + mermaid += ` class ${nodeId} rootModule\n`; + } + } + + // Add dependency relationships + for (const dep of dependencies) { + const fromId = moduleIds.get(dep.from); + const toId = moduleIds.get(dep.to); + + if (fromId && toId) { + const edgeStyle = this.getDependencyEdgeStyle(dep.dependencyType); + mermaid += ` ${fromId} ${edgeStyle} ${toId}\n`; + } + } + + return mermaid; + } + + private generateStyling(): string { + return ` + classDef pythonModule fill:#3776ab,stroke:#2d5aa0,stroke-width:2px,color:#fff + classDef typescriptModule fill:#3178c6,stroke:#2761a3,stroke-width:2px,color:#fff + classDef javascriptModule fill:#f7df1e,stroke:#d4c21a,stroke-width:2px,color:#000 + classDef javaModule fill:#f89820,stroke:#e8751a,stroke-width:2px,color:#fff + classDef rootModule fill:#ff6b6b,stroke:#ff5252,stroke-width:4px,color:#fff + classDef defaultNode fill:#f5f5f5,stroke:#ddd,stroke-width:1px,color:#333 + +`; + } + + private getModuleDisplayName(module: ModuleInfo): string { + const name = path.basename(module.fileName, path.extname(module.fileName)); + // Keep it simple - just return the clean module name + return name.length > 20 ? name.substring(0, 17) + "..." : name; + } + + private getModuleInfoString(module: ModuleInfo): string { + const parts = []; + + if (module.functions.length > 0) { + parts.push(`${module.functions.length}f`); + } + if (module.classes.length > 0) { + parts.push(`${module.classes.length}c`); + } + if (module.exports.length > 0) { + parts.push(`${module.exports.length}exp`); + } + if (module.imports.length > 0) { + parts.push(`${module.imports.length}imp`); + } + + return parts.join(" | "); + } + + private getLanguageStyleClass(language: string): string { + switch (language) { + case "python": + return "pythonModule"; + case "typescript": + return "typescriptModule"; + case "javascript": + return "javascriptModule"; + case "java": + return "javaModule"; + default: + return "defaultNode"; + } + } + + private getDependencyLabel(dependency: ModuleDependency): string { + if (!dependency.importedItems || dependency.importedItems.length === 0) { + return ""; + } + + // Filter out empty or invalid items + const validItems = dependency.importedItems.filter( + (item) => item && item.trim().length > 0 + ); + + if (validItems.length === 0) { + return ""; + } + + if (validItems.length <= 3) { + return validItems.join(", "); + } else { + return `${validItems.slice(0, 2).join(", ")}... +${ + validItems.length - 2 + }`; + } + } + + private getDependencyEdgeStyle(type: string): string { + switch (type) { + case "import": + return "-->"; // Solid arrow for imports + case "function_call": + return "-..->"; // Dotted arrow for function calls + default: + return "-->"; // Default to solid arrow + } + } +} diff --git a/src/logic/language-services/java/JavaModuleParser.ts b/src/logic/language-services/java/JavaModuleParser.ts new file mode 100644 index 0000000..cfd3fb1 --- /dev/null +++ b/src/logic/language-services/java/JavaModuleParser.ts @@ -0,0 +1,173 @@ +import * as path from "path"; +import { ModuleInfo, ImportInfo, ExportInfo, FunctionCallInfo, ModuleLocation } from "../../../ir/moduleIr"; + +/** + * Analyzes Java module structure to extract imports, exports (public methods/classes), and function calls + */ +export async function analyzeJavaModule(code: string, filePath: string): Promise { + const fileName = path.basename(filePath); + const imports: ImportInfo[] = []; + const exports: ExportInfo[] = []; + const functionCalls: FunctionCallInfo[] = []; + const functions: string[] = []; + const classes: string[] = []; + const variables: string[] = []; + + const lines = code.split('\n'); + + for (let i = 0; i < lines.length; i++) { + const line = lines[i].trim(); + const location: ModuleLocation = { start: 0, end: line.length, line: i + 1, column: 0 }; + + // Parse imports + if (line.startsWith('import ')) { + const match = line.match(/^import\s+(?:static\s+)?([a-zA-Z_][a-zA-Z0-9_.]*(?:\.\*)?);?/); + if (match) { + const importPath = match[1]; + const parts = importPath.split('.'); + const name = parts[parts.length - 1]; + + imports.push({ + name: name === '*' ? parts[parts.length - 2] : name, + source: importPath, + type: name === '*' ? 'all' : 'named', + location + }); + } + } + + // Parse public class definitions + if (line.includes('class ')) { + const match = line.match(/(?:public\s+)?class\s+([a-zA-Z_][a-zA-Z0-9_]*)/); + if (match) { + const className = match[1]; + classes.push(className); + + // Public classes are exportable + if (line.includes('public ')) { + exports.push({ + name: className, + type: 'class', + location + }); + } + } + } + + // Parse method definitions + if (line.includes('(') && line.includes(')') && + (line.includes('public ') || line.includes('private ') || line.includes('protected '))) { + // Simple regex to match method signatures + const match = line.match(/(?:public|private|protected)\s+(?:static\s+)?(?:[a-zA-Z_][a-zA-Z0-9_<>[\]]*\s+)?([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/); + if (match) { + const methodName = match[1]; + + // Exclude constructors (methods with same name as class) + const isConstructor = classes.some(className => className === methodName); + if (!isConstructor && !isJavaKeyword(methodName)) { + functions.push(methodName); + + // Public methods are exportable + if (line.includes('public ')) { + exports.push({ + name: methodName, + type: 'function', + location + }); + } + } + } + } + + // Parse field definitions + if ((line.includes('public ') || line.includes('private ') || line.includes('protected ')) && + line.includes('=') && !line.includes('(')) { + const match = line.match(/(?:public|private|protected)\s+(?:static\s+)?(?:final\s+)?[a-zA-Z_][a-zA-Z0-9_<>[\]]*\s+([a-zA-Z_][a-zA-Z0-9_]*)/); + if (match) { + const fieldName = match[1]; + variables.push(fieldName); + + // Public fields are exportable + if (line.includes('public ')) { + exports.push({ + name: fieldName, + type: 'variable', + location + }); + } + } + } + + // Parse method calls + const funcCallMatches = line.matchAll(/([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/g); + for (const match of funcCallMatches) { + const callName = match[1]; + + if (!isJavaKeyword(callName) && !isBuiltinMethod(callName) && + !line.includes(' class ') && !line.includes('public ') && !line.includes('private ')) { + let module: string | undefined; + + // Check for static method calls (ClassName.methodName) + const staticCallMatch = line.match(/([a-zA-Z_][a-zA-Z0-9_]*)\\.([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/); + if (staticCallMatch) { + const className = staticCallMatch[1]; + const methodName = staticCallMatch[2]; + + // Check if this class was imported + const importedClass = imports.find(imp => imp.name === className); + if (importedClass) { + module = importedClass.source; + } + + functionCalls.push({ + functionName: `${className}.${methodName}`, + module, + location + }); + } else { + functionCalls.push({ + functionName: callName, + module, + location + }); + } + } + } + } + + return { + filePath, + fileName, + language: 'java', + imports, + exports, + functionCalls, + functions, + classes, + variables + }; +} + +function isJavaKeyword(name: string): boolean { + const keywords = [ + 'abstract', 'assert', 'boolean', 'break', 'byte', 'case', 'catch', 'char', + 'class', 'const', 'continue', 'default', 'do', 'double', 'else', 'enum', + 'extends', 'final', 'finally', 'float', 'for', 'goto', 'if', 'implements', + 'import', 'instanceof', 'int', 'interface', 'long', 'native', 'new', + 'package', 'private', 'protected', 'public', 'return', 'short', 'static', + 'strictfp', 'super', 'switch', 'synchronized', 'this', 'throw', 'throws', + 'transient', 'try', 'void', 'volatile', 'while' + ]; + + return keywords.includes(name); +} + +function isBuiltinMethod(name: string): boolean { + const builtins = [ + 'println', 'print', 'toString', 'equals', 'hashCode', 'getClass', + 'notify', 'notifyAll', 'wait', 'finalize', 'clone', 'length', 'size', + 'isEmpty', 'contains', 'add', 'remove', 'clear', 'get', 'set', 'put' + ]; + + return builtins.includes(name); +} \ No newline at end of file diff --git a/src/logic/language-services/python/PyModuleParser.ts b/src/logic/language-services/python/PyModuleParser.ts new file mode 100644 index 0000000..eec5912 --- /dev/null +++ b/src/logic/language-services/python/PyModuleParser.ts @@ -0,0 +1,253 @@ +import * as path from "path"; +import { + ModuleInfo, + ImportInfo, + ExportInfo, + FunctionCallInfo, + ModuleLocation, +} from "../../../ir/moduleIr"; + +/** + * Analyzes Python module structure to extract imports, exports, and function calls + */ +export async function analyzePythonModule( + code: string, + filePath: string +): Promise { + const fileName = path.basename(filePath); + const imports: ImportInfo[] = []; + const exports: ExportInfo[] = []; + const functionCalls: FunctionCallInfo[] = []; + const functions: string[] = []; + const classes: string[] = []; + const variables: string[] = []; + + const lines = code.split("\n"); + + for (let i = 0; i < lines.length; i++) { + const line = lines[i].trim(); + const location: ModuleLocation = { + start: 0, + end: line.length, + line: i + 1, + column: 0, + }; + + // Parse imports + if (line.startsWith("import ")) { + const match = line.match(/^import\s+(.+)/); + if (match) { + const modules = match[1].split(",").map((m) => m.trim()); + for (const module of modules) { + const parts = module.split(" as "); + imports.push({ + name: parts.length > 1 ? parts[1] : parts[0], + source: parts[0], + type: "namespace", + alias: parts.length > 1 ? parts[1] : undefined, + location, + }); + } + } + } + + // Parse from imports + if (line.startsWith("from ")) { + const match = line.match(/^from\s+(.+?)\s+import\s+(.+)/); + if (match) { + const source = match[1]; + const imports_str = match[2]; + + if (imports_str.includes("*")) { + imports.push({ + name: "*", + source, + type: "all", + location, + }); + } else { + const importNames = imports_str.split(",").map((i) => i.trim()); + for (const importName of importNames) { + const parts = importName.split(" as "); + imports.push({ + name: parts.length > 1 ? parts[1] : parts[0], + source, + type: "named", + alias: parts.length > 1 ? parts[1] : undefined, + location, + }); + } + } + } + } + + // Parse function definitions + if (line.startsWith("def ")) { + const match = line.match(/^def\s+([a-zA-Z_][a-zA-Z0-9_]*)/); + if (match) { + const funcName = match[1]; + functions.push(funcName); + + // Functions are exportable in Python (unless they start with _) + if (!funcName.startsWith("_")) { + exports.push({ + name: funcName, + type: "function", + location, + }); + } + } + } + + // Parse class definitions + if (line.startsWith("class ")) { + const match = line.match(/^class\s+([a-zA-Z_][a-zA-Z0-9_]*)/); + if (match) { + const className = match[1]; + classes.push(className); + + // Classes are exportable in Python (unless they start with _) + if (!className.startsWith("_")) { + exports.push({ + name: className, + type: "class", + location, + }); + } + } + } + + // Parse variable assignments (top-level only) + if ( + line.includes("=") && + !line.includes("==") && + !line.includes("!=") && + !line.includes("<=") && + !line.includes(">=") && + line.indexOf("=") > 0 && + !line.trim().startsWith("#") + ) { + const match = line.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\s*=/); + if (match) { + const varName = match[1]; + variables.push(varName); + + // Top-level variables are exportable (unless they start with _) + if (!varName.startsWith("_")) { + exports.push({ + name: varName, + type: "variable", + location, + }); + } + } + } + + // Parse function calls with better module detection + const funcCallRegex = /([a-zA-Z_][a-zA-Z0-9_.]*)\s*\(/g; + let match; + while ((match = funcCallRegex.exec(line)) !== null) { + const callName = match[1]; + + // Skip built-in functions and method definitions + if ( + !isBuiltinFunction(callName) && + !line.startsWith("def ") && + !line.startsWith("class ") && + !line.includes("#") // Skip comments + ) { + let module: string | undefined; + let functionName = callName; + + // Check if it's a module.function call (e.g., math.sqrt) + if (callName.includes(".")) { + const parts = callName.split("."); + const potentialModule = parts[0]; + functionName = parts.slice(1).join("."); + + // Check if this module was imported + const importedModule = imports.find( + (imp) => + imp.name === potentialModule || imp.alias === potentialModule + ); + if (importedModule) { + module = importedModule.source; + } + } else { + // Check if it's a directly imported function (e.g., calculate_average from utils) + const directImport = imports.find( + (imp) => imp.name === callName && imp.type === "named" + ); + if (directImport) { + module = directImport.source; + } + } + + functionCalls.push({ + functionName, + module, + location, + }); + } + } + } + + const moduleInfo: ModuleInfo = { + filePath, + fileName, + language: "python", + imports, + exports, + functionCalls, + functions, + classes, + variables, + }; + + console.log(`[PyModuleParser] Analysis complete for ${fileName}:`, { + imports: imports.map((i) => `${i.name} from ${i.source} (${i.type})`), + exports: exports.map((e) => `${e.name} (${e.type})`), + functionCalls: functionCalls.map( + (f) => `${f.functionName}${f.module ? ` from ${f.module}` : ""}` + ), + }); + + return moduleInfo; +} + +function isBuiltinFunction(name: string): boolean { + const builtins = [ + "print", + "len", + "str", + "int", + "float", + "list", + "dict", + "tuple", + "set", + "range", + "enumerate", + "zip", + "map", + "filter", + "sum", + "max", + "min", + "abs", + "round", + "sorted", + "reversed", + "open", + "input", + "type", + "isinstance", + "hasattr", + "getattr", + "setattr", + "delattr", + ]; + + const simpleName = name.split(".")[0]; + return builtins.includes(simpleName); +} diff --git a/src/logic/language-services/typescript/TsModuleParser.ts b/src/logic/language-services/typescript/TsModuleParser.ts new file mode 100644 index 0000000..00e76f9 --- /dev/null +++ b/src/logic/language-services/typescript/TsModuleParser.ts @@ -0,0 +1,250 @@ +import * as path from "path"; +import { ModuleInfo, ImportInfo, ExportInfo, FunctionCallInfo, ModuleLocation } from "../../../ir/moduleIr"; + +/** + * Analyzes TypeScript/JavaScript module structure to extract imports, exports, and function calls + */ +export async function analyzeTypeScriptModule(code: string, filePath: string, language: string): Promise { + const fileName = path.basename(filePath); + const imports: ImportInfo[] = []; + const exports: ExportInfo[] = []; + const functionCalls: FunctionCallInfo[] = []; + const functions: string[] = []; + const classes: string[] = []; + const variables: string[] = []; + + const lines = code.split('\n'); + + for (let i = 0; i < lines.length; i++) { + const line = lines[i].trim(); + const location: ModuleLocation = { start: 0, end: line.length, line: i + 1, column: 0 }; + + // Parse ES6 imports + if (line.startsWith('import ')) { + parseESImport(line, imports, location); + } + + // Parse CommonJS require + if (line.includes('require(')) { + parseCommonJSImport(line, imports, location); + } + + // Parse exports + if (line.startsWith('export ')) { + parseExport(line, exports, location); + } + + // Parse function definitions + if (line.includes('function ')) { + const match = line.match(/function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/); + if (match) { + const funcName = match[1]; + functions.push(funcName); + + if (line.startsWith('export ')) { + exports.push({ + name: funcName, + type: 'function', + location + }); + } + } + } + + // Parse arrow functions + const arrowFunctionMatch = line.match(/(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*\(/); + if (arrowFunctionMatch && line.includes('=>')) { + const funcName = arrowFunctionMatch[1]; + functions.push(funcName); + + if (line.startsWith('export ')) { + exports.push({ + name: funcName, + type: 'function', + location + }); + } + } + + // Parse class definitions + if (line.includes('class ')) { + const match = line.match(/class\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/); + if (match) { + const className = match[1]; + classes.push(className); + + if (line.startsWith('export ')) { + exports.push({ + name: className, + type: 'class', + location + }); + } + } + } + + // Parse variable declarations + const varMatch = line.match(/(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/); + if (varMatch && !line.includes('=>')) { + const varName = varMatch[1]; + variables.push(varName); + + if (line.startsWith('export ')) { + exports.push({ + name: varName, + type: 'variable', + location + }); + } + } + + // Parse function calls + const funcCallMatches = line.matchAll(/([a-zA-Z_$][a-zA-Z0-9_$.]*)\s*\(/g); + for (const match of funcCallMatches) { + const callName = match[1]; + + if (!isBuiltinFunction(callName) && !line.startsWith('function ') && !line.startsWith('class ')) { + let module: string | undefined; + + // Check if it's a module.function call + if (callName.includes('.')) { + const parts = callName.split('.'); + const potentialModule = parts[0]; + + // Check if this module was imported + const importedModule = imports.find(imp => imp.name === potentialModule || imp.alias === potentialModule); + if (importedModule) { + module = importedModule.source; + } + } + + functionCalls.push({ + functionName: callName, + module, + location + }); + } + } + } + + return { + filePath, + fileName, + language, + imports, + exports, + functionCalls, + functions, + classes, + variables + }; +} + +function parseESImport(line: string, imports: ImportInfo[], location: ModuleLocation) { + // Default import: import React from 'react' + let match = line.match(/^import\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s+from\s+['"`]([^'"`]+)['"`]/); + if (match) { + imports.push({ + name: match[1], + source: match[2], + type: 'default', + location + }); + return; + } + + // Named imports: import { a, b as c } from 'module' + match = line.match(/^import\s+\{\s*([^}]+)\s*\}\s+from\s+['"`]([^'"`]+)['"`]/); + if (match) { + const namedImports = match[1].split(',').map(i => i.trim()); + for (const namedImport of namedImports) { + const parts = namedImport.split(' as ').map(p => p.trim()); + imports.push({ + name: parts.length > 1 ? parts[1] : parts[0], + source: match[2], + type: 'named', + alias: parts.length > 1 ? parts[1] : undefined, + location + }); + } + return; + } + + // Namespace import: import * as fs from 'fs' + match = line.match(/^import\s+\*\s+as\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s+from\s+['"`]([^'"`]+)['"`]/); + if (match) { + imports.push({ + name: match[1], + source: match[2], + type: 'namespace', + location + }); + return; + } + + // Side-effect import: import 'module' + match = line.match(/^import\s+['"`]([^'"`]+)['"`]/); + if (match) { + imports.push({ + name: '*', + source: match[1], + type: 'all', + location + }); + } +} + +function parseCommonJSImport(line: string, imports: ImportInfo[], location: ModuleLocation) { + // const fs = require('fs') + const match = line.match(/(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*require\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/); + if (match) { + imports.push({ + name: match[1], + source: match[2], + type: 'namespace', + location + }); + } +} + +function parseExport(line: string, exports: ExportInfo[], location: ModuleLocation) { + // export default + if (line.includes('export default')) { + const match = line.match(/export\s+default\s+(?:function\s+)?([a-zA-Z_$][a-zA-Z0-9_$]*)/); + if (match) { + exports.push({ + name: match[1], + type: 'default', + location + }); + } + return; + } + + // export { a, b } + const namedMatch = line.match(/export\s+\{\s*([^}]+)\s*\}/); + if (namedMatch) { + const exportNames = namedMatch[1].split(',').map(e => e.trim()); + for (const exportName of exportNames) { + exports.push({ + name: exportName, + type: 'variable', + location + }); + } + } +} + +function isBuiltinFunction(name: string): boolean { + const builtins = [ + 'console', 'parseInt', 'parseFloat', 'isNaN', 'isFinite', 'decodeURI', + 'decodeURIComponent', 'encodeURI', 'encodeURIComponent', 'escape', 'unescape', + 'Object', 'Array', 'String', 'Number', 'Boolean', 'Date', 'RegExp', + 'Error', 'EvalError', 'RangeError', 'ReferenceError', 'SyntaxError', + 'TypeError', 'URIError', 'JSON', 'Math', 'Promise', 'setTimeout', + 'clearTimeout', 'setInterval', 'clearInterval' + ]; + + const simpleName = name.split('.')[0]; + return builtins.includes(simpleName); +} \ No newline at end of file diff --git a/src/manual-test.ts b/src/manual-test.ts new file mode 100644 index 0000000..6aa06ee --- /dev/null +++ b/src/manual-test.ts @@ -0,0 +1,194 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { DataFlowAnalyzer } from './logic/DataFlowAnalyzer'; +import { DataFlowMermaidGenerator } from './logic/DataFlowMermaidGenerator'; + +/** + * Manual test runner for data flow analysis - validates components individually + */ +async function runManualTests() { + console.log('=== Manual Data Flow Analysis Tests ===\n'); + + try { + // Test 1: Create analyzer and generator + console.log('1. Creating analyzer and generator...'); + const analyzer = new DataFlowAnalyzer(); + const generator = new DataFlowMermaidGenerator(); + console.log('āœ… Components created successfully\n'); + + // Test 2: Read test file + console.log('2. Reading test file...'); + const testFilePath = path.join(__dirname, 'test-dataflow-simple.ts'); + const testFileExists = fs.existsSync(testFilePath); + console.log('Test file exists:', testFileExists); + + if (testFileExists) { + const content = fs.readFileSync(testFilePath, 'utf8'); + console.log('Test file size:', content.length, 'characters'); + console.log('First 100 characters:', content.substring(0, 100)); + } else { + console.log('āŒ Test file not found, creating a simple one...'); + const simpleTestContent = ` +let globalVar = 0; +function testFunc() { + globalVar++; + return globalVar; +} + `.trim(); + fs.writeFileSync(testFilePath, simpleTestContent); + console.log('āœ… Created simple test file'); + } + console.log(); + + // Test 3: Test mermaid generation with minimal data + console.log('3. Testing mermaid generation...'); + const sampleAnalysis = { + functions: [{ + name: 'testFunction', + filePath: testFilePath, + location: { start: 0, end: 50, line: 1, column: 0 }, + globalStateAccesses: [], + parameters: [], + calls: [], + isAsync: false + }], + globalStateVariables: [], + dataFlowEdges: [], + title: "Manual Test", + scope: 'function' as const, + analysisTimestamp: Date.now() + }; + + const mermaidCode = generator.generateDataFlowGraph(sampleAnalysis); + console.log('Generated mermaid code:'); + console.log('---'); + console.log(mermaidCode); + console.log('---'); + console.log('Mermaid code length:', mermaidCode.length); + + // Validate mermaid syntax + const isValidMermaid = mermaidCode.includes('graph TD') && mermaidCode.trim().length > 10; + console.log('Valid mermaid syntax:', isValidMermaid ? 'āœ…' : 'āŒ'); + console.log(); + + // Test 4: Test with global variables + console.log('4. Testing with global variables...'); + const analysisWithGlobals = { + ...sampleAnalysis, + globalStateVariables: [{ + name: 'globalVar', + type: 'number', + declarationLocation: { start: 0, end: 20, line: 1, column: 0 }, + accessedBy: ['testFunction'], + modifications: [] + }], + functions: [{ + ...sampleAnalysis.functions[0], + globalStateAccesses: [ + { variableName: 'globalVar', accessType: 'write' as const, location: { start: 30, end: 40, line: 2, column: 0 } } + ] + }] + }; + + const mermaidWithGlobals = generator.generateDataFlowGraph(analysisWithGlobals); + console.log('Generated mermaid with globals:'); + console.log('---'); + console.log(mermaidWithGlobals); + console.log('---'); + console.log('Contains global node:', mermaidWithGlobals.includes('global_globalVar') ? 'āœ…' : 'āŒ'); + console.log('Contains function node:', mermaidWithGlobals.includes('func_testFunction') ? 'āœ…' : 'āŒ'); + console.log(); + + console.log('=== Manual Tests Completed Successfully ==='); + return true; + + } catch (error) { + console.error('āŒ Manual test failed:', error); + console.error('Stack trace:', error instanceof Error ? error.stack : 'Unknown error'); + return false; + } +} + +// Test analyzer methods individually +async function testAnalyzerMethods() { + console.log('=== Testing Analyzer Methods ===\n'); + + try { + // Test regex patterns for finding functions and globals + const testCode = ` +let globalCounter = 0; +const userState = { name: "", active: false }; + +function processData(input) { + if (!userState.active) return null; + globalCounter++; + return input.toUpperCase(); +} + +async function asyncProcess(data) { + globalCounter += 2; + return processData(data); +} + `.trim(); + + console.log('Test code length:', testCode.length); + + // Test function detection + const functionRegex = /(?:^|\s)(?:async\s+)?function\s+(\w+)\s*\(/gm; + const functions = []; + let match; + while ((match = functionRegex.exec(testCode)) !== null) { + functions.push(match[1]); + } + console.log('Functions found:', functions); + + // Test global variable detection + const globalRegex = /(?:^|\s)(?:let|const|var)\s+(\w+)\s*=/gm; + const globals: string[] = []; + while ((match = globalRegex.exec(testCode)) !== null) { + globals.push(match[1]); + } + console.log('Globals found:', globals); + + // Test global usage detection + console.log('\nGlobal usage analysis:'); + functions.forEach(func => { + const funcStart = testCode.indexOf(`function ${func}`); + const funcEnd = testCode.indexOf('}', funcStart); + const funcBody = testCode.substring(funcStart, funcEnd + 1); + + console.log(`\n${func}:`); + globals.forEach(global => { + const usage = funcBody.includes(global); + if (usage) { + const writePattern = new RegExp(`${global}\\s*[\\+\\-\\*\\/]?=`, 'g'); + const isWrite = writePattern.test(funcBody); + console.log(` - ${global}: ${isWrite ? 'WRITE' : 'READ'}`); + } + }); + }); + + console.log('\nāœ… Analyzer method tests completed'); + } catch (error) { + console.error('āŒ Analyzer method test failed:', error); + } +} + +// Run all tests +if (require.main === module) { + runManualTests() + .then((success) => { + if (success) { + return testAnalyzerMethods(); + } + }) + .then(() => { + console.log('\nšŸŽ‰ All manual tests completed!'); + }) + .catch(error => { + console.error('šŸ’„ Test suite failed:', error); + process.exit(1); + }); +} + +export { runManualTests, testAnalyzerMethods }; \ No newline at end of file diff --git a/src/test-dataflow-simple.ts b/src/test-dataflow-simple.ts new file mode 100644 index 0000000..0ace36d --- /dev/null +++ b/src/test-dataflow-simple.ts @@ -0,0 +1,34 @@ +// Simple test file for data flow analysis +let globalCounter = 0; +let userState = { name: "", isActive: false }; +const CONFIG = { maxRetries: 3, timeout: 5000 }; + +function processUserData(userId: string) { + if (!userState.isActive) return null; // reads userState + globalCounter++; // modifies globalCounter + return `Processed: ${userId}`; +} + +function resetUserState() { + userState.name = ""; // writes userState + userState.isActive = false; // writes userState + globalCounter = 0; // writes globalCounter +} + +function getUserInfo(): string { + return `User: ${userState.name}, Active: ${userState.isActive}, Count: ${globalCounter}`; +} + +function initializeApp() { + userState.name = "Default User"; + userState.isActive = true; + console.log("App initialized"); +} + +async function asyncProcessor(data: string) { + if (globalCounter > CONFIG.maxRetries) { + throw new Error("Max retries exceeded"); + } + globalCounter++; + return processUserData(data); +} \ No newline at end of file diff --git a/src/view/ModuleAnalysisProvider.ts b/src/view/ModuleAnalysisProvider.ts new file mode 100644 index 0000000..f5f316b --- /dev/null +++ b/src/view/ModuleAnalysisProvider.ts @@ -0,0 +1,1090 @@ +import * as vscode from "vscode"; +import { DataFlowAnalyzer } from "../logic/DataFlowAnalyzer"; +import { DataFlowMermaidGenerator } from "../logic/DataFlowMermaidGenerator"; +import { DataFlowAnalysisIR } from "../ir/dataFlowIr"; + +const MERMAID_VERSION = "11.8.0"; +const SVG_PAN_ZOOM_VERSION = "3.6.1"; + +// Define message types for consistency with function-level analysis +export type ExportMessage = { + command: "export"; + payload: { fileType: "svg" | "png"; data: string }; +}; + +export type ExportErrorMessage = { + command: "exportError"; + payload: { error: string }; +}; + +export type CopyMermaidMessage = { + command: "copyMermaid"; + payload: { code: string }; +}; + +export type ModuleWebviewMessage = + | ExportMessage + | ExportErrorMessage + | CopyMermaidMessage; + +export class DataFlowProvider implements vscode.WebviewViewProvider { + public static readonly viewType = "visor.dataFlowView"; + + private _view?: vscode.WebviewView; + private _analyzer: DataFlowAnalyzer; + private _generator: DataFlowMermaidGenerator; + private _currentAnalysis?: DataFlowAnalysisIR; + private _currentView: 'dataflow' | 'callgraph' = 'dataflow'; + + constructor(private readonly _extensionUri: vscode.Uri) { + this._analyzer = new DataFlowAnalyzer(); + this._generator = new DataFlowMermaidGenerator(); + + // Listen for configuration changes to update themes (matching BaseFlowchartProvider) + vscode.workspace.onDidChangeConfiguration((e) => { + if (e.affectsConfiguration("visor.nodeReadability.theme")) { + // Refresh the current view when theme changes + this._updateWebview(); + } + }); + } + + public resolveWebviewView( + webviewView: vscode.WebviewView, + context: vscode.WebviewViewResolveContext, + _token: vscode.CancellationToken + ) { + this._view = webviewView; + + webviewView.webview.options = { + enableScripts: true, + localResourceRoots: [this._extensionUri], + }; + + this._updateWebview(); + + // Handle messages from the webview + webviewView.webview.onDidReceiveMessage(async (message) => { + switch (message.command) { + case "refresh": + this._refreshAnalysis(); + break; + case "reset": + this._resetView(); + break; + case "analyzeCurrentFunction": + this.analyzeCurrentFunction(); + break; + case "analyzeWorkspace": + this.analyzeWorkspaceDataFlow(); + break; + case "switchView": + this.switchView(message.payload.viewType); + break; + case "export": + await this.handleExport(message.payload); + break; + case "exportError": + vscode.window.showErrorMessage( + `Export failed: ${message.payload.error}` + ); + break; + case "copyMermaid": + await vscode.env.clipboard.writeText(message.payload.code); + vscode.window.showInformationMessage( + "Mermaid code copied to clipboard!" + ); + break; + } + }); + } + + public async analyzeCurrentFunction(): Promise { + if (!this._view) return; + + try { + this._showLoading("Analyzing current function data flow..."); + console.log("DataFlowProvider: Starting current function analysis..."); + + // Add detailed logging to understand failures + const activeEditor = vscode.window.activeTextEditor; + console.log("DataFlowProvider: Active editor:", { + hasEditor: !!activeEditor, + languageId: activeEditor?.document.languageId, + fileName: activeEditor?.document.fileName, + cursorLine: activeEditor?.selection.active.line, + documentLength: activeEditor?.document.getText().length + }); + + this._currentAnalysis = await this._analyzer.analyzeCurrentFunctionContext(); + + console.log("DataFlowProvider: Analysis complete:", { + functionCount: this._currentAnalysis.functions.length, + globalVariableCount: this._currentAnalysis.globalStateVariables.length, + dataFlowEdges: this._currentAnalysis.dataFlowEdges.length, + rootFunction: this._currentAnalysis.rootFunction, + scope: this._currentAnalysis.scope, + title: this._currentAnalysis.title + }); + + // Log analysis details for debugging + if (this._currentAnalysis.functions.length > 0) { + console.log("DataFlowProvider: Functions found:", + this._currentAnalysis.functions.map(f => ({ name: f.name, globalAccesses: f.globalStateAccesses.length })) + ); + } + if (this._currentAnalysis.globalStateVariables.length > 0) { + console.log("DataFlowProvider: Global variables found:", + this._currentAnalysis.globalStateVariables.map(g => ({ name: g.name, type: g.type, accessedBy: g.accessedBy })) + ); + } + + this._updateWebview(); + } catch (error) { + console.error("DataFlowProvider: Current function analysis failed:", error); + console.error("DataFlowProvider: Error details:", { + message: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : 'No stack trace', + errorType: error?.constructor?.name || 'Unknown' + }); + this._showError( + `Failed to analyze current function: ${ + error instanceof Error ? error.message : String(error) + }` + ); + } + } + + public async analyzeWorkspaceDataFlow(): Promise { + if (!this._view) return; + + try { + this._showLoading("Analyzing workspace data flow..."); + console.log("DataFlowProvider: Starting workspace data flow analysis..."); + + // Add workspace validation logging + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; + console.log("DataFlowProvider: Workspace info:", { + hasWorkspace: !!workspaceFolder, + workspacePath: workspaceFolder?.uri.fsPath, + name: workspaceFolder?.name + }); + + this._currentAnalysis = await this._analyzer.analyzeWorkspaceDataFlow(); + + console.log("DataFlowProvider: Workspace analysis complete:", { + functionCount: this._currentAnalysis.functions.length, + globalVariableCount: this._currentAnalysis.globalStateVariables.length, + dataFlowEdges: this._currentAnalysis.dataFlowEdges.length, + scope: this._currentAnalysis.scope, + title: this._currentAnalysis.title + }); + + // Log analysis details for debugging + if (this._currentAnalysis.functions.length > 0) { + console.log("DataFlowProvider: Functions found:", + this._currentAnalysis.functions.map(f => ({ name: f.name, file: f.filePath.split('/').pop(), globalAccesses: f.globalStateAccesses.length })) + ); + } + if (this._currentAnalysis.globalStateVariables.length > 0) { + console.log("DataFlowProvider: Global variables found:", + this._currentAnalysis.globalStateVariables.map(g => ({ name: g.name, type: g.type, accessedBy: g.accessedBy })) + ); + } + + this._updateWebview(); + } catch (error) { + console.error("DataFlowProvider: Workspace analysis failed:", error); + console.error("DataFlowProvider: Error details:", { + message: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : 'No stack trace', + errorType: error?.constructor?.name || 'Unknown' + }); + this._showError( + `Failed to analyze workspace data flow: ${ + error instanceof Error ? error.message : String(error) + }` + ); + } + } + + public switchView(viewType: 'dataflow' | 'callgraph'): void { + this._currentView = viewType; + this._updateWebview(); + } + + private _resetView(): void { + console.log("DataFlowProvider: Resetting view"); + this._currentAnalysis = undefined; + this._currentView = 'dataflow'; + + if (this._view) { + this._view.webview.html = this._getInitialHtml(); + } + } + + private async _refreshAnalysis(): Promise { + if (this._currentAnalysis) { + // Re-run the same type of analysis + if (this._currentAnalysis.scope === 'workspace') { + await this.analyzeWorkspaceDataFlow(); + } else { + await this.analyzeCurrentFunction(); + } + } + } + + private _updateWebview(): void { + if (!this._view) return; + + if (!this._currentAnalysis) { + this._view.webview.html = this._getInitialHtml(); + return; + } + + try { + // Use the same theme configuration logic as BaseFlowchartProvider + const vsCodeTheme = + vscode.window.activeColorTheme.kind === vscode.ColorThemeKind.Dark + ? "dark" + : "light"; + + // Read the selected theme from user configuration (same as BaseFlowchartProvider) + const config = vscode.workspace.getConfiguration("visor"); + const selectedTheme = config.get( + "nodeReadability.theme", + "monokai" + ); + + // Pass theme configuration to the generator + this._generator.setTheme(selectedTheme, vsCodeTheme); + + // Generate the appropriate graph based on current view + let mermaidGraph: string; + let viewTitle: string; + + if (this._currentView === 'callgraph') { + mermaidGraph = this._generator.generateFunctionCallGraph(this._currentAnalysis); + viewTitle = "Function Call Graph"; + } else { + mermaidGraph = this._generator.generateDataFlowGraph(this._currentAnalysis); + viewTitle = "Data Flow Analysis"; + } + + // Validate the generated mermaid graph + if (!mermaidGraph || mermaidGraph.trim().length === 0) { + this._showError( + "Failed to generate graph visualization. No valid graph data found." + ); + return; + } + + this._view.webview.html = this._getWebviewHtml( + mermaidGraph, + viewTitle, + this._currentView, + this._getNonce() + ); + } catch (error) { + console.error("Error updating module analysis webview:", error); + this._showError( + `Failed to update view: ${ + error instanceof Error ? error.message : String(error) + }` + ); + } + } + + private _showLoading(message: string): void { + if (!this._view) return; + + this._view.webview.html = ` + + + + + + Data Flow Analysis + + + +
+
+

${message}

+
+ + + `; + } + + private _showError(message: string): void { + if (!this._view) return; + + this._view.webview.html = ` + + + + + + Data Flow Analysis + + + +
+

āŒ Analysis Failed

+

${message}

+ + +
+ + + + `; + } + + private _getInitialHtml(): string { + return ` + + + + + + Data Flow Analysis + + + +
+
šŸ”„
+

Data Flow Analysis

+

+ Understand how global state flows through your functions.
+ Track data dependencies and global variable usage across your codebase. +

+ + +
+ +
+ + + + `; + } + + private _getWebviewHtml( + mermaidGraph: string, + title: string, + currentView: string, + nonce: string + ): string { + const analysis = this._currentAnalysis!; + const functionCount = analysis.functions.length; + const globalVarCount = analysis.globalStateVariables.length; + const dataFlowCount = analysis.dataFlowEdges.length; + + // Use the same theme logic as BaseFlowchartProvider for consistency + const theme = + vscode.window.activeColorTheme.kind === vscode.ColorThemeKind.Dark + ? "dark" + : "default"; + + return ` + + + + + + + Data Flow Analysis + + + + + +
+
+
+

${title}

+
+
āš™ļø ${functionCount} functions
+
šŸ“Š ${globalVarCount} global vars
+
šŸ”— ${dataFlowCount} data flows
+
+
+ +
+
+ + +
+ +
+ +
+ + + + + +
+
+
+ +
+
+${mermaidGraph} +
+
+
+ + +
${mermaidGraph + .replace(//g, ">")}
+ + + + + `; + } + + /** + * Handle export functionality (matches BaseFlowchartProvider implementation) + */ + private async handleExport(payload: { + fileType: "svg" | "png"; + data: string; + }): Promise { + const activeEditor = vscode.window.activeTextEditor; + if (!activeEditor) { + vscode.window.showErrorMessage( + "Cannot export: No active text editor found." + ); + return; + } + + const { fileType, data } = payload; + const documentUri = activeEditor.document.uri; + + const defaultDirectory = vscode.Uri.file( + require("path").dirname(documentUri.fsPath) + ); + const defaultFileUri = vscode.Uri.file( + require("path").join( + defaultDirectory.fsPath, + `data-flow-analysis.${fileType}` + ) + ); + + const filters: { [name: string]: string[] } = + fileType === "svg" + ? { "SVG Images": ["svg"] } + : { "PNG Images": ["png"] }; + + const uri = await vscode.window.showSaveDialog({ + filters, + defaultUri: defaultFileUri, + }); + + if (uri) { + const buffer = Buffer.from(data, fileType === "png" ? "base64" : "utf-8"); + try { + await vscode.workspace.fs.writeFile(uri, buffer); + vscode.window.showInformationMessage( + `Successfully exported data flow analysis to ${uri.fsPath}` + ); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + vscode.window.showErrorMessage( + `Failed to export data flow analysis: ${message}` + ); + } + } + } + + /** + * Generates a random nonce for Content Security Policy (matches BaseFlowchartProvider) + */ + private _getNonce(): string { + let text = ""; + const possible = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + for (let i = 0; i < 32; i++) { + text += possible.charAt(Math.floor(Math.random() * possible.length)); + } + return text; + } +} diff --git a/test-dataflow-simple.js b/test-dataflow-simple.js new file mode 100644 index 0000000..9f87518 --- /dev/null +++ b/test-dataflow-simple.js @@ -0,0 +1,32 @@ +"use strict"; +// Simple test file for data flow analysis +let globalCounter = 0; +let userState = { name: "", isActive: false }; +const CONFIG = { maxRetries: 3, timeout: 5000 }; +function processUserData(userId) { + if (!userState.isActive) + return null; // reads userState + globalCounter++; // modifies globalCounter + return `Processed: ${userId}`; +} +function resetUserState() { + userState.name = ""; // writes userState + userState.isActive = false; // writes userState + globalCounter = 0; // writes globalCounter +} +function getUserInfo() { + return `User: ${userState.name}, Active: ${userState.isActive}, Count: ${globalCounter}`; +} +function initializeApp() { + userState.name = "Default User"; + userState.isActive = true; + console.log("App initialized"); +} +async function asyncProcessor(data) { + if (globalCounter > CONFIG.maxRetries) { + throw new Error("Max retries exceeded"); + } + globalCounter++; + return processUserData(data); +} +//# sourceMappingURL=test-dataflow-simple.js.map \ No newline at end of file diff --git a/test-dataflow-simple.js.map b/test-dataflow-simple.js.map new file mode 100644 index 0000000..58bb612 --- /dev/null +++ b/test-dataflow-simple.js.map @@ -0,0 +1 @@ +{"version":3,"file":"test-dataflow-simple.js","sourceRoot":"","sources":["test-dataflow-simple.ts"],"names":[],"mappings":";AAAA,0CAA0C;AAC1C,IAAI,aAAa,GAAG,CAAC,CAAC;AACtB,IAAI,SAAS,GAAG,EAAE,IAAI,EAAE,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;AAC9C,MAAM,MAAM,GAAG,EAAE,UAAU,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;AAEhD,SAAS,eAAe,CAAC,MAAc;IACrC,IAAI,CAAC,SAAS,CAAC,QAAQ;QAAE,OAAO,IAAI,CAAC,CAAE,kBAAkB;IACzD,aAAa,EAAE,CAAC,CAAuB,yBAAyB;IAChE,OAAO,cAAc,MAAM,EAAE,CAAC;AAChC,CAAC;AAED,SAAS,cAAc;IACrB,SAAS,CAAC,IAAI,GAAG,EAAE,CAAC,CAAK,mBAAmB;IAC5C,SAAS,CAAC,QAAQ,GAAG,KAAK,CAAC,CAAC,mBAAmB;IAC/C,aAAa,GAAG,CAAC,CAAC,CAAO,uBAAuB;AAClD,CAAC;AAED,SAAS,WAAW;IAClB,OAAO,SAAS,SAAS,CAAC,IAAI,aAAa,SAAS,CAAC,QAAQ,YAAY,aAAa,EAAE,CAAC;AAC3F,CAAC;AAED,SAAS,aAAa;IACpB,SAAS,CAAC,IAAI,GAAG,cAAc,CAAC;IAChC,SAAS,CAAC,QAAQ,GAAG,IAAI,CAAC;IAC1B,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;AACjC,CAAC;AAED,KAAK,UAAU,cAAc,CAAC,IAAY;IACxC,IAAI,aAAa,GAAG,MAAM,CAAC,UAAU,EAAE,CAAC;QACtC,MAAM,IAAI,KAAK,CAAC,sBAAsB,CAAC,CAAC;IAC1C,CAAC;IACD,aAAa,EAAE,CAAC;IAChB,OAAO,eAAe,CAAC,IAAI,CAAC,CAAC;AAC/B,CAAC"} \ No newline at end of file diff --git a/test-dataflow.js b/test-dataflow.js new file mode 100644 index 0000000..aefd7f4 --- /dev/null +++ b/test-dataflow.js @@ -0,0 +1,40 @@ +"use strict"; +// Sample TypeScript file to test data flow analysis +let globalCounter = 0; +let userState = { name: "", isActive: false }; +const CONFIG = { maxRetries: 3, timeout: 5000 }; +function initializeApp() { + globalCounter = 1; + userState.name = "Default User"; + userState.isActive = true; + console.log("App initialized with global state"); +} +async function processUserData(userId) { + if (!userState.isActive) { + console.log("User state not active"); + return null; + } + const result = await fetchData(userId); + globalCounter++; + return result; +} +function fetchData(userId) { + console.log(`Fetching data for ${userId}, retry count: ${globalCounter}`); + if (globalCounter > CONFIG.maxRetries) { + throw new Error("Max retries exceeded"); + } + return Promise.resolve({ id: userId, data: "sample" }); +} +function resetSystem() { + globalCounter = 0; + userState = { name: "", isActive: false }; + console.log("System reset"); +} +function getCurrentState() { + return { + counter: globalCounter, + user: userState, + config: CONFIG + }; +} +//# sourceMappingURL=test-dataflow.js.map \ No newline at end of file diff --git a/test-dataflow.js.map b/test-dataflow.js.map new file mode 100644 index 0000000..d874c58 --- /dev/null +++ b/test-dataflow.js.map @@ -0,0 +1 @@ +{"version":3,"file":"test-dataflow.js","sourceRoot":"","sources":["test-dataflow.ts"],"names":[],"mappings":";AAAA,oDAAoD;AACpD,IAAI,aAAa,GAAG,CAAC,CAAC;AACtB,IAAI,SAAS,GAAG,EAAE,IAAI,EAAE,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;AAC9C,MAAM,MAAM,GAAG,EAAE,UAAU,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;AAEhD,SAAS,aAAa;IACpB,aAAa,GAAG,CAAC,CAAC;IAClB,SAAS,CAAC,IAAI,GAAG,cAAc,CAAC;IAChC,SAAS,CAAC,QAAQ,GAAG,IAAI,CAAC;IAC1B,OAAO,CAAC,GAAG,CAAC,mCAAmC,CAAC,CAAC;AACnD,CAAC;AAED,KAAK,UAAU,eAAe,CAAC,MAAc;IAC3C,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,CAAC;QACxB,OAAO,CAAC,GAAG,CAAC,uBAAuB,CAAC,CAAC;QACrC,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,MAAM,CAAC,CAAC;IACvC,aAAa,EAAE,CAAC;IAChB,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,SAAS,SAAS,CAAC,MAAc;IAC/B,OAAO,CAAC,GAAG,CAAC,qBAAqB,MAAM,kBAAkB,aAAa,EAAE,CAAC,CAAC;IAE1E,IAAI,aAAa,GAAG,MAAM,CAAC,UAAU,EAAE,CAAC;QACtC,MAAM,IAAI,KAAK,CAAC,sBAAsB,CAAC,CAAC;IAC1C,CAAC;IAED,OAAO,OAAO,CAAC,OAAO,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC;AACzD,CAAC;AAED,SAAS,WAAW;IAClB,aAAa,GAAG,CAAC,CAAC;IAClB,SAAS,GAAG,EAAE,IAAI,EAAE,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;IAC1C,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;AAC9B,CAAC;AAED,SAAS,eAAe;IACtB,OAAO;QACL,OAAO,EAAE,aAAa;QACtB,IAAI,EAAE,SAAS;QACf,MAAM,EAAE,MAAM;KACf,CAAC;AACJ,CAAC"} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 6b176f2..6699129 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,5 +18,5 @@ // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ // "noUnusedParameters": true, /* Report errors on unused parameters. */ }, - "exclude": ["test-files/**/*", "node_modules", "dist", "out"] + "exclude": ["test-files/**/*", "node_modules", "dist", "out", "sample_project/**/*"] } diff --git a/yarn.lock b/yarn.lock index e23584a..f92a24f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -165,7 +165,7 @@ "@nodelib/fs.stat" "2.0.5" run-parallel "^1.1.9" -"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": +"@nodelib/fs.stat@^2.0.2", "@nodelib/fs.stat@2.0.5": version "2.0.5" resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz" integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== @@ -258,7 +258,7 @@ natural-compare "^1.4.0" ts-api-utils "^2.1.0" -"@typescript-eslint/parser@^8.31.1": +"@typescript-eslint/parser@^8.31.1", "@typescript-eslint/parser@^8.36.0": version "8.36.0" resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.36.0.tgz" integrity sha512-FuYgkHwZLuPbZjQHzJXrtXreJdFMKl16BFYyRrLxDhWr6Qr7Kbcu2s1Yhu8tsiMXw1S0W1pjfFfYEt+R604s+Q== @@ -286,7 +286,7 @@ "@typescript-eslint/types" "8.36.0" "@typescript-eslint/visitor-keys" "8.36.0" -"@typescript-eslint/tsconfig-utils@8.36.0", "@typescript-eslint/tsconfig-utils@^8.36.0": +"@typescript-eslint/tsconfig-utils@^8.36.0", "@typescript-eslint/tsconfig-utils@8.36.0": version "8.36.0" resolved "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.36.0.tgz" integrity sha512-Nhh3TIEgN18mNbdXpd5Q8mSCBnrZQeY9V7Ca3dqYvNDStNIGRmJA6dmrIPMJ0kow3C7gcQbpsG2rPzy1Ks/AnA== @@ -301,7 +301,7 @@ debug "^4.3.4" ts-api-utils "^2.1.0" -"@typescript-eslint/types@8.36.0", "@typescript-eslint/types@^8.36.0": +"@typescript-eslint/types@^8.36.0", "@typescript-eslint/types@8.36.0": version "8.36.0" resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.36.0.tgz" integrity sha512-xGms6l5cTJKQPZOKM75Dl9yBfNdGeLRsIyufewnxT4vZTrjC0ImQT4fj8QmtJK84F58uSh5HVBSANwcfiXxABQ== @@ -371,7 +371,7 @@ resolved "https://registry.npmjs.org/@vscode/tree-sitter-wasm/-/tree-sitter-wasm-0.1.4.tgz" integrity sha512-kQVVg/CamCYDM+/XYCZuNTQyixjZd8ts/Gf84UzjEY0eRnbg6kiy5I9z2/2i3XdqwhI87iG07rkMR2KwhqcSbA== -"@webassemblyjs/ast@1.14.1", "@webassemblyjs/ast@^1.14.1": +"@webassemblyjs/ast@^1.14.1", "@webassemblyjs/ast@1.14.1": version "1.14.1" resolved "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz" integrity sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ== @@ -472,7 +472,7 @@ "@webassemblyjs/wasm-gen" "1.14.1" "@webassemblyjs/wasm-parser" "1.14.1" -"@webassemblyjs/wasm-parser@1.14.1", "@webassemblyjs/wasm-parser@^1.14.1": +"@webassemblyjs/wasm-parser@^1.14.1", "@webassemblyjs/wasm-parser@1.14.1": version "1.14.1" resolved "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz" integrity sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ== @@ -527,7 +527,7 @@ acorn-jsx@^5.3.2: resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -acorn@^8.14.0, acorn@^8.15.0: +"acorn@^6.0.0 || ^7.0.0 || ^8.0.0", acorn@^8.14.0, acorn@^8.15.0: version "8.15.0" resolved "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz" integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg== @@ -561,7 +561,7 @@ ajv@^6.12.4: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ajv@^8.0.0, ajv@^8.9.0: +ajv@^8.0.0, ajv@^8.8.2, ajv@^8.9.0: version "8.17.1" resolved "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz" integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g== @@ -643,7 +643,7 @@ browser-stdout@^1.3.1: resolved "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz" integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== -browserslist@^4.24.0: +browserslist@^4.24.0, "browserslist@>= 4.21.0": version "4.25.1" resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz" integrity sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw== @@ -823,7 +823,7 @@ cross-spawn@^7.0.3, cross-spawn@^7.0.6: shebang-command "^2.0.0" which "^2.0.1" -debug@4, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.5: +debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.5, debug@4: version "4.4.1" resolved "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz" integrity sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ== @@ -898,14 +898,6 @@ escape-string-regexp@^4.0.0: resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== -eslint-scope@5.1.1: - version "5.1.1" - resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz" - integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== - dependencies: - esrecurse "^4.3.0" - estraverse "^4.1.1" - eslint-scope@^8.4.0: version "8.4.0" resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz" @@ -914,6 +906,14 @@ eslint-scope@^8.4.0: esrecurse "^4.3.0" estraverse "^5.2.0" +eslint-scope@5.1.1: + version "5.1.1" + resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz" + integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== + dependencies: + esrecurse "^4.3.0" + estraverse "^4.1.1" + eslint-visitor-keys@^3.4.3: version "3.4.3" resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz" @@ -924,7 +924,7 @@ eslint-visitor-keys@^4.2.1: resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz" integrity sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ== -eslint@^9.25.1: +"eslint@^6.0.0 || ^7.0.0 || >=8.0.0", "eslint@^8.57.0 || ^9.0.0", eslint@^9.25.1: version "9.31.0" resolved "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz" integrity sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ== @@ -1112,11 +1112,6 @@ fs.realpath@^1.0.0: resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== -fsevents@~2.3.2: - version "2.3.3" - resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz" - integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== - function-bind@^1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" @@ -1139,7 +1134,14 @@ glob-parent@^5.1.2, glob-parent@~5.1.2: dependencies: is-glob "^4.0.1" -glob-parent@^6.0.1, glob-parent@^6.0.2: +glob-parent@^6.0.1: + version "6.0.2" + resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + +glob-parent@^6.0.2: version "6.0.2" resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz" integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== @@ -1245,7 +1247,12 @@ ignore@^5.2.0: resolved "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz" integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== -ignore@^7.0.0, ignore@^7.0.3: +ignore@^7.0.0: + version "7.0.5" + resolved "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz" + integrity sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg== + +ignore@^7.0.3: version "7.0.5" resolved "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz" integrity sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg== @@ -1284,7 +1291,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@~2.0.3: +inherits@~2.0.3, inherits@2: version "2.0.4" resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -1580,7 +1587,21 @@ minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" -minimatch@^9.0.3, minimatch@^9.0.4, minimatch@^9.0.5: +minimatch@^9.0.3: + version "9.0.5" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz" + integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== + dependencies: + brace-expansion "^2.0.1" + +minimatch@^9.0.4: + version "9.0.5" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz" + integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== + dependencies: + brace-expansion "^2.0.1" + +minimatch@^9.0.5: version "9.0.5" resolved "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz" integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== @@ -1988,6 +2009,13 @@ stdin-discarder@^0.2.2: resolved "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz" integrity sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ== +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + "string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" @@ -2024,13 +2052,6 @@ string-width@^7.2.0: get-east-asian-width "^1.0.0" strip-ansi "^7.1.0" -string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz" - integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== - dependencies: - safe-buffer "~5.1.0" - "strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" @@ -2045,7 +2066,14 @@ strip-ansi@^6.0.0, strip-ansi@^6.0.1: dependencies: ansi-regex "^5.0.1" -strip-ansi@^7.0.1, strip-ansi@^7.1.0: +strip-ansi@^7.0.1: + version "7.1.0" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz" + integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== + dependencies: + ansi-regex "^6.0.1" + +strip-ansi@^7.1.0: version "7.1.0" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz" integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== @@ -2064,7 +2092,14 @@ supports-color@^7.1.0: dependencies: has-flag "^4.0.0" -supports-color@^8.0.0, supports-color@^8.1.1: +supports-color@^8.0.0: + version "8.1.1" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + +supports-color@^8.1.1: version "8.1.1" resolved "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz" integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== @@ -2146,7 +2181,7 @@ type-check@^0.4.0, type-check@~0.4.0: dependencies: prelude-ls "^1.2.1" -typescript@^5.8.3: +typescript@*, typescript@^5.8.3, typescript@>=4.8.4, "typescript@>=4.8.4 <5.9.0": version "5.8.3" resolved "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz" integrity sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ== @@ -2208,7 +2243,7 @@ web-tree-sitter@^0.22.2: resolved "https://registry.npmjs.org/web-tree-sitter/-/web-tree-sitter-0.22.6.tgz" integrity sha512-hS87TH71Zd6mGAmYCvlgxeGDjqd9GTeqXNqTT+u0Gs51uIozNIaaq/kUAbV/Zf56jb2ZOyG8BxZs2GG9wbLi6Q== -webpack-cli@^6.0.1: +webpack-cli@^6.0.1, webpack-cli@6.x.x: version "6.0.1" resolved "https://registry.npmjs.org/webpack-cli/-/webpack-cli-6.0.1.tgz" integrity sha512-MfwFQ6SfwinsUVi0rNJm7rHZ31GyTcpVE5pgVA3hwFRb7COD4TzjUUwhGWKfO50+xdc2MQPuEBBJoqIMGt3JDw== @@ -2241,7 +2276,7 @@ webpack-sources@^3.3.3: resolved "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz" integrity sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg== -webpack@^5.99.7: +webpack@^5.0.0, webpack@^5.1.0, webpack@^5.82.0, webpack@^5.99.7: version "5.100.1" resolved "https://registry.npmjs.org/webpack/-/webpack-5.100.1.tgz" integrity sha512-YJB/ESPUe2Locd0NKXmw72Dx8fZQk1gTzI6rc9TAT4+Sypbnhl8jd8RywB1bDsDF9Dy1RUR7gn3q/ZJTd0OZZg==