@sun-asterisk/impact-analyzer 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,241 @@
1
+ /**
2
+ * AST Parser Utilities
3
+ * Handles parsing and symbol extraction from JavaScript/TypeScript code
4
+ */
5
+
6
+ import { parse } from '@babel/parser';
7
+ import traverse from '@babel/traverse';
8
+
9
+ export class ASTParser {
10
+ /**
11
+ * Parse code and extract symbols
12
+ */
13
+ static extractSymbols(content, filePath) {
14
+ try {
15
+ const ast = parse(content, {
16
+ sourceType: 'module',
17
+ plugins: [
18
+ 'jsx',
19
+ 'typescript',
20
+ 'decorators-legacy',
21
+ 'classProperties',
22
+ 'dynamicImport',
23
+ 'optionalChaining',
24
+ 'nullishCoalescingOperator',
25
+ ],
26
+ });
27
+
28
+ return this.traverseAST(ast, filePath);
29
+ } catch (error) {
30
+ console.warn(`Failed to parse ${filePath}:`, error.message);
31
+ return [];
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Traverse AST and extract symbols
37
+ */
38
+ static traverseAST(ast, filePath) {
39
+ const symbols = [];
40
+ const self = ASTParser;
41
+
42
+ traverse.default(ast, {
43
+ // Function declarations
44
+ FunctionDeclaration(path) {
45
+ if (path.node.id) {
46
+ symbols.push({
47
+ type: 'function',
48
+ name: path.node.id.name,
49
+ signature: self.getSignature(path),
50
+ startLine: path.node.loc?.start.line || 0,
51
+ endLine: path.node.loc?.end.line || 0,
52
+ file: filePath,
53
+ });
54
+ }
55
+ },
56
+
57
+ // Arrow functions and function expressions assigned to variables
58
+ VariableDeclarator(path) {
59
+ if (
60
+ path.node.id.type === 'Identifier' &&
61
+ (path.node.init?.type === 'ArrowFunctionExpression' ||
62
+ path.node.init?.type === 'FunctionExpression')
63
+ ) {
64
+ symbols.push({
65
+ type: 'function',
66
+ name: path.node.id.name,
67
+ signature: self.getSignature(path.get('init')),
68
+ startLine: path.node.loc?.start.line || 0,
69
+ endLine: path.node.loc?.end.line || 0,
70
+ file: filePath,
71
+ });
72
+ }
73
+ },
74
+
75
+ // Class declarations
76
+ ClassDeclaration(path) {
77
+ if (path.node.id) {
78
+ symbols.push({
79
+ type: 'class',
80
+ name: path.node.id.name,
81
+ startLine: path.node.loc?.start.line || 0,
82
+ endLine: path.node.loc?.end.line || 0,
83
+ file: filePath,
84
+ });
85
+
86
+ // Extract class methods
87
+ path.node.body.body.forEach((member) => {
88
+ if (member.type === 'ClassMethod' && member.key.type === 'Identifier') {
89
+ symbols.push({
90
+ type: 'method',
91
+ name: `${path.node.id.name}.${member.key.name}`,
92
+ signature: self.getMethodSignature(member),
93
+ startLine: member.loc?.start.line || 0,
94
+ endLine: member.loc?.end.line || 0,
95
+ file: filePath,
96
+ });
97
+ }
98
+ });
99
+ }
100
+ },
101
+
102
+ // Export declarations
103
+ ExportNamedDeclaration(path) {
104
+ if (path.node.declaration) {
105
+ // Handle exported functions, classes, etc.
106
+ const declaration = path.node.declaration;
107
+ if (declaration.id) {
108
+ symbols.push({
109
+ type: declaration.type.toLowerCase().replace('declaration', ''),
110
+ name: declaration.id.name,
111
+ exported: true,
112
+ startLine: declaration.loc?.start.line || 0,
113
+ endLine: declaration.loc?.end.line || 0,
114
+ file: filePath,
115
+ });
116
+ }
117
+ }
118
+ },
119
+ });
120
+
121
+ return symbols;
122
+ }
123
+
124
+ /**
125
+ * Get function signature
126
+ */
127
+ static getSignature(path) {
128
+ try {
129
+ if (!path || !path.node) {
130
+ return '(...)';
131
+ }
132
+ const params = path.node.params || [];
133
+ const paramNames = params.map(p => {
134
+ if (p.type === 'Identifier') return p.name;
135
+ if (p.type === 'RestElement') return `...${p.argument.name}`;
136
+ return '_';
137
+ });
138
+ return `(${paramNames.join(', ')})`;
139
+ } catch (error) {
140
+ return '(...)';
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Get method signature
146
+ */
147
+ static getMethodSignature(node) {
148
+ try {
149
+ const params = node.params || [];
150
+ const paramNames = params.map(p => p.name || '_');
151
+ return `(${paramNames.join(', ')})`;
152
+ } catch (error) {
153
+ return '(...)';
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Extract imports from code
159
+ */
160
+ static extractImports(content) {
161
+ const imports = [];
162
+
163
+ try {
164
+ const ast = parse(content, {
165
+ sourceType: 'module',
166
+ plugins: ['jsx', 'typescript', 'dynamicImport'],
167
+ });
168
+
169
+ traverse.default(ast, {
170
+ ImportDeclaration(path) {
171
+ imports.push({
172
+ source: path.node.source.value,
173
+ specifiers: path.node.specifiers.map(s => ({
174
+ imported: s.imported?.name || s.local.name,
175
+ local: s.local.name,
176
+ })),
177
+ });
178
+ },
179
+
180
+ // Dynamic imports
181
+ CallExpression(path) {
182
+ if (path.node.callee.type === 'Import') {
183
+ const arg = path.node.arguments[0];
184
+ if (arg?.type === 'StringLiteral') {
185
+ imports.push({
186
+ source: arg.value,
187
+ dynamic: true,
188
+ });
189
+ }
190
+ }
191
+ },
192
+ });
193
+ } catch (error) {
194
+ // Ignore parse errors
195
+ }
196
+
197
+ return imports;
198
+ }
199
+
200
+ /**
201
+ * Find function calls in code
202
+ */
203
+ static findFunctionCalls(content, functionName) {
204
+ const calls = [];
205
+
206
+ try {
207
+ const ast = parse(content, {
208
+ sourceType: 'module',
209
+ plugins: ['jsx', 'typescript'],
210
+ });
211
+
212
+ traverse.default(ast, {
213
+ CallExpression(path) {
214
+ const callee = path.node.callee;
215
+
216
+ // Direct function call
217
+ if (callee.type === 'Identifier' && callee.name === functionName) {
218
+ calls.push({
219
+ line: path.node.loc?.start.line || 0,
220
+ type: 'direct',
221
+ });
222
+ }
223
+
224
+ // Method call
225
+ if (callee.type === 'MemberExpression' &&
226
+ callee.property.type === 'Identifier' &&
227
+ callee.property.name === functionName) {
228
+ calls.push({
229
+ line: path.node.loc?.start.line || 0,
230
+ type: 'method',
231
+ });
232
+ }
233
+ },
234
+ });
235
+ } catch (error) {
236
+ // Ignore parse errors
237
+ }
238
+
239
+ return calls;
240
+ }
241
+ }
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Dependency Graph Builder
3
+ * Builds and queries dependency relationships between code modules
4
+ */
5
+
6
+ export class DependencyGraph {
7
+ constructor() {
8
+ this.graph = new Map(); // file -> Set of dependencies
9
+ this.reverseGraph = new Map(); // file -> Set of dependents
10
+ this.symbolGraph = new Map(); // symbol -> Set of callers
11
+ }
12
+
13
+ /**
14
+ * Add a dependency edge
15
+ */
16
+ addDependency(from, to) {
17
+ if (! this.graph.has(from)) {
18
+ this.graph.set(from, new Set());
19
+ }
20
+ this.graph.get(from).add(to);
21
+
22
+ // Build reverse graph
23
+ if (!this.reverseGraph.has(to)) {
24
+ this.reverseGraph.set(to, new Set());
25
+ }
26
+ this.reverseGraph.get(to).add(from);
27
+ }
28
+
29
+ /**
30
+ * Add symbol usage
31
+ */
32
+ addSymbolUsage(symbol, usedIn) {
33
+ if (!this.symbolGraph.has(symbol)) {
34
+ this.symbolGraph.set(symbol, new Set());
35
+ }
36
+ this.symbolGraph.get(symbol).add(usedIn);
37
+ }
38
+
39
+ /**
40
+ * Get dependencies of a file
41
+ */
42
+ getDependencies(file) {
43
+ return Array.from(this.graph.get(file) || []);
44
+ }
45
+
46
+ /**
47
+ * Get dependents of a file (files that depend on this file)
48
+ */
49
+ getDependents(file) {
50
+ return Array.from(this.reverseGraph.get(file) || []);
51
+ }
52
+
53
+ /**
54
+ * Get callers of a symbol
55
+ */
56
+ getSymbolCallers(symbol) {
57
+ return Array.from(this.symbolGraph.get(symbol) || []);
58
+ }
59
+
60
+ /**
61
+ * Get transitive dependencies up to maxDepth
62
+ */
63
+ getTransitiveDependencies(file, maxDepth = 3) {
64
+ const visited = new Set();
65
+ const result = new Set();
66
+
67
+ const traverse = (current, depth) => {
68
+ if (depth > maxDepth || visited.has(current)) return;
69
+
70
+ visited.add(current);
71
+ const deps = this.getDependencies(current);
72
+
73
+ for (const dep of deps) {
74
+ result.add(dep);
75
+ traverse(dep, depth + 1);
76
+ }
77
+ };
78
+
79
+ traverse(file, 0);
80
+ return Array.from(result);
81
+ }
82
+
83
+ /**
84
+ * Get transitive dependents (reverse dependencies)
85
+ */
86
+ getTransitiveDependents(file, maxDepth = 3) {
87
+ const visited = new Set();
88
+ const result = new Set();
89
+
90
+ const traverse = (current, depth) => {
91
+ if (depth > maxDepth || visited.has(current)) return;
92
+
93
+ visited.add(current);
94
+ const dependents = this.getDependents(current);
95
+
96
+ for (const dependent of dependents) {
97
+ result.add(dependent);
98
+ traverse(dependent, depth + 1);
99
+ }
100
+ };
101
+
102
+ traverse(file, 0);
103
+ return Array.from(result);
104
+ }
105
+
106
+ /**
107
+ * Calculate impact radius for a file
108
+ */
109
+ getImpactRadius(file) {
110
+ const directDeps = this.getDependents(file);
111
+ const transitiveDeps = this.getTransitiveDependents(file, 2);
112
+
113
+ return {
114
+ direct: directDeps.length,
115
+ transitive: transitiveDeps.length,
116
+ total: transitiveDeps.length + directDeps.length,
117
+ };
118
+ }
119
+
120
+ /**
121
+ * Find path between two files
122
+ */
123
+ findPath(from, to, maxDepth = 5) {
124
+ const queue = [[from]];
125
+ const visited = new Set([from]);
126
+
127
+ while (queue.length > 0) {
128
+ const path = queue.shift();
129
+ const current = path[path.length - 1];
130
+
131
+ if (path.length > maxDepth) continue;
132
+ if (current === to) return path;
133
+
134
+ const deps = this.getDependencies(current);
135
+ for (const dep of deps) {
136
+ if (!visited.has(dep)) {
137
+ visited.add(dep);
138
+ queue.push([...path, dep]);
139
+ }
140
+ }
141
+ }
142
+
143
+ return null; // No path found
144
+ }
145
+
146
+ /**
147
+ * Get graph statistics
148
+ */
149
+ getStats() {
150
+ return {
151
+ totalFiles: this.graph.size,
152
+ totalEdges: Array.from(this.graph.values()).reduce((sum, set) => sum + set.size, 0),
153
+ totalSymbols: this.symbolGraph.size,
154
+ avgDependencies: this.graph.size > 0
155
+ ? Array.from(this.graph.values()).reduce((sum, set) => sum + set.size, 0) / this.graph.size
156
+ : 0,
157
+ };
158
+ }
159
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * File Utilities
3
+ * Helper functions for file operations
4
+ */
5
+
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+ import { glob } from 'glob';
9
+
10
+ export class FileUtils {
11
+ /**
12
+ * Check if file is a source code file
13
+ */
14
+ static isSourceFile(filePath) {
15
+ const extensions = ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs'];
16
+ return extensions.some(ext => filePath.endsWith(ext));
17
+ }
18
+
19
+ /**
20
+ * Check if file is a test file
21
+ */
22
+ static isTestFile(filePath) {
23
+ return (
24
+ filePath.includes('.test.') ||
25
+ filePath.includes('.spec.') ||
26
+ filePath.includes('__tests__') ||
27
+ filePath.includes('__mocks__')
28
+ );
29
+ }
30
+
31
+ /**
32
+ * Check if file is a config file
33
+ */
34
+ static isConfigFile(filePath) {
35
+ return (
36
+ filePath.includes('config') ||
37
+ filePath.endsWith('.config.js') ||
38
+ filePath.endsWith('.config.ts') ||
39
+ filePath.includes('jest.') ||
40
+ filePath.includes('webpack.') ||
41
+ filePath.includes('vite.')
42
+ );
43
+ }
44
+
45
+ /**
46
+ * Categorize file type
47
+ */
48
+ static categorizeFile(filePath) {
49
+ if (this.isTestFile(filePath)) return 'test';
50
+ if (this.isConfigFile(filePath)) return 'config';
51
+ if (this.isSourceFile(filePath)) return 'source';
52
+ return 'other';
53
+ }
54
+
55
+ /**
56
+ * Get all source files in directory
57
+ */
58
+ static getAllSourceFiles(sourceDir, excludePaths = []) {
59
+ const pattern = `${sourceDir}/**/*.{js,jsx,ts,tsx,mjs,cjs}`;
60
+ const ignore = excludePaths.map(p => `${sourceDir}/**/${p}/**`);
61
+
62
+ try {
63
+ return glob.sync(pattern, { ignore, absolute: true });
64
+ } catch (error) {
65
+ console.error('Error scanning source files:', error.message);
66
+ return [];
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Read file content safely
72
+ */
73
+ static readFile(filePath) {
74
+ try {
75
+ return fs.readFileSync(filePath, 'utf-8');
76
+ } catch (error) {
77
+ console.warn(`Failed to read ${filePath}:`, error.message);
78
+ return '';
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Calculate line changes between two file versions
84
+ */
85
+ static calculateLineChanges(oldContent, newContent) {
86
+ const oldLines = oldContent ? oldContent.split('\n').length : 0;
87
+ const newLines = newContent ? newContent.split('\n').length : 0;
88
+
89
+ return {
90
+ added: Math.max(0, newLines - oldLines),
91
+ deleted: Math.max(0, oldLines - newLines),
92
+ total: Math.abs(newLines - oldLines),
93
+ };
94
+ }
95
+
96
+ /**
97
+ * Extract module name from file path
98
+ */
99
+ static extractModuleName(filePath) {
100
+ const parts = filePath.split(path.sep);
101
+ const srcIndex = parts.indexOf('src');
102
+
103
+ if (srcIndex !== -1 && parts.length > srcIndex + 1) {
104
+ return parts[srcIndex + 1];
105
+ }
106
+
107
+ return path.basename(filePath, path.extname(filePath));
108
+ }
109
+
110
+ /**
111
+ * Get relative path from project root
112
+ */
113
+ static getRelativePath(filePath) {
114
+ return path.relative(process.cwd(), filePath);
115
+ }
116
+ }
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Git Utilities
3
+ * Helper functions for git operations
4
+ */
5
+
6
+ import { execSync } from 'child_process';
7
+ import path from 'path';
8
+ import fs from 'fs';
9
+
10
+ export class GitUtils {
11
+ /**
12
+ * Get list of changed files between two refs
13
+ * @param {string} baseRef - Base git reference
14
+ * @param {string} headRef - Head git reference
15
+ * @param {string} workDir - Working directory (optional, defaults to cwd)
16
+ */
17
+ static getChangedFiles(baseRef, headRef, workDir = null) {
18
+ try {
19
+ const cwd = workDir ? path.resolve(workDir) : process.cwd();
20
+
21
+ // Verify the directory exists and is a git repo
22
+ if (! fs.existsSync(cwd)) {
23
+ throw new Error(`Directory does not exist: ${cwd}`);
24
+ }
25
+
26
+ const diffCommand = `git diff --name-status ${baseRef}...${headRef}`;
27
+ const diffOutput = execSync(diffCommand, {
28
+ encoding: 'utf-8',
29
+ cwd: cwd // FIXED: Execute git command in the source directory
30
+ });
31
+
32
+ const changedFiles = [];
33
+ const lines = diffOutput.trim().split('\n').filter(line => line);
34
+
35
+ for (const line of lines) {
36
+ const parts = line.split('\t');
37
+ const status = parts[0];
38
+ const filePath = parts[1];
39
+
40
+ changedFiles.push({
41
+ status: this.parseStatus(status),
42
+ path: filePath,
43
+ });
44
+ }
45
+
46
+ return changedFiles;
47
+ } catch (error) {
48
+ console.error('Error getting changed files:', error.message);
49
+ console.error(' Working directory:', workDir || process.cwd());
50
+ console.error(' Base ref:', baseRef);
51
+ console.error(' Head ref:', headRef);
52
+ return [];
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Get file content at specific ref
58
+ * @param {string} filePath - Relative file path
59
+ * @param {string} ref - Git reference
60
+ * @param {string} workDir - Working directory (optional)
61
+ */
62
+ static getFileContent(filePath, ref, workDir = null) {
63
+ try {
64
+ const cwd = workDir ? path.resolve(workDir) : process.cwd();
65
+
66
+ return execSync(`git show ${ref}:${filePath}`, {
67
+ encoding: 'utf-8',
68
+ cwd: cwd // FIXED: Execute in source directory
69
+ });
70
+ } catch (error) {
71
+ // File might be new or deleted
72
+ return '';
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Get diff between two versions of a file
78
+ * @param {string} filePath - File path relative to git root (e.g., 'src/modules/file.ts')
79
+ * @param {string} baseRef - Base git reference
80
+ * @param {string} headRef - Head git reference
81
+ * @param {string} workDir - Working directory to run git from (optional)
82
+ */
83
+ static getFileDiff(filePath, baseRef, headRef, workDir = null) {
84
+ try {
85
+ // Determine working directory (should be git root or a path inside it)
86
+ const cwd = workDir ? path.resolve(workDir) : process.cwd();
87
+
88
+ // Get git root from the working directory
89
+ const gitRoot = this.getGitRoot(cwd);
90
+
91
+ if (!gitRoot) {
92
+ console.error('Not a git repository:', cwd);
93
+ return '';
94
+ }
95
+
96
+ // filePath is already relative to git root (from git diff --name-status)
97
+ // Execute git diff from git root
98
+ const diffCommand = `git diff ${baseRef}...${headRef} -- ${filePath}`;
99
+
100
+ const result = execSync(diffCommand, {
101
+ encoding: 'utf-8',
102
+ cwd: gitRoot,
103
+ maxBuffer: 10 * 1024 * 1024, // 10MB buffer for large diffs
104
+ });
105
+
106
+ return result;
107
+ } catch (error) {
108
+ console.error('Error getting file diff:', error.message);
109
+ console.error(' File:', filePath);
110
+ console.error(' Base:', baseRef);
111
+ console.error(' Head:', headRef);
112
+ console.error(' Working dir:', workDir);
113
+ return '';
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Parse git status code
119
+ */
120
+ static parseStatus(statusCode) {
121
+ const statusMap = {
122
+ 'A': 'added',
123
+ 'M': 'modified',
124
+ 'D': 'deleted',
125
+ 'R': 'renamed',
126
+ 'C': 'copied',
127
+ };
128
+ return statusMap[statusCode[0]] || 'unknown';
129
+ }
130
+
131
+ /**
132
+ * Get current branch name
133
+ */
134
+ static getCurrentBranch(workDir = null) {
135
+ try {
136
+ const cwd = workDir ? path.resolve(workDir) : process.cwd();
137
+
138
+ return execSync('git rev-parse --abbrev-ref HEAD', {
139
+ encoding: 'utf-8',
140
+ cwd: cwd
141
+ }).trim();
142
+ } catch (error) {
143
+ return 'unknown';
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Check if ref exists
149
+ */
150
+ static refExists(ref, workDir = null) {
151
+ try {
152
+ const cwd = workDir ? path.resolve(workDir) : process.cwd();
153
+
154
+ execSync(`git rev-parse --verify ${ref}`, {
155
+ encoding: 'utf-8',
156
+ stdio: 'ignore',
157
+ cwd: cwd
158
+ });
159
+ return true;
160
+ } catch (error) {
161
+ return false;
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Check if directory is a git repository
167
+ */
168
+ static isGitRepo(workDir = null) {
169
+ try {
170
+ const cwd = workDir ? path.resolve(workDir) : process.cwd();
171
+
172
+ execSync('git rev-parse --git-dir', {
173
+ encoding: 'utf-8',
174
+ stdio: 'ignore',
175
+ cwd: cwd
176
+ });
177
+ return true;
178
+ } catch (error) {
179
+ return false;
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Get git root directory
185
+ */
186
+ static getGitRoot(workDir = null) {
187
+ try {
188
+ const cwd = workDir ? path.resolve(workDir) : process.cwd();
189
+
190
+ return execSync('git rev-parse --show-toplevel', {
191
+ encoding: 'utf-8',
192
+ cwd: cwd
193
+ }).trim();
194
+ } catch (error) {
195
+ return null;
196
+ }
197
+ }
198
+ }