blast-radius-analyzer
Advanced tools
| { | ||
| "/Users/didi/Documents/code3/logic-inverse-v2/blast-radius-analyzer/dist/index.js": { | ||
| "mtime": 1774934096510.6582, | ||
| "hash": "16fb35ae" | ||
| }, | ||
| "/Users/didi/Documents/code3/logic-inverse-v2/blast-radius-analyzer/src/index.ts": { | ||
| "mtime": 1774934091686.9543, | ||
| "hash": "4feb087c" | ||
| } | ||
| } |
+168
-1
@@ -33,2 +33,3 @@ #!/usr/bin/env node | ||
| let clearCache = false; | ||
| let autoDetect = false; // 自动检测 git diff | ||
| let threshold = undefined; | ||
@@ -84,2 +85,5 @@ for (let i = 2; i < argv.length; i++) { | ||
| } | ||
| else if (arg === '--auto' || arg === '-a') { | ||
| autoDetect = true; | ||
| } | ||
| else if (arg === '--threshold' && argv[i + 1]) { | ||
@@ -120,2 +124,3 @@ // 解析阈值: --threshold files:5,score:100,typeErrors:0 | ||
| clearCache, | ||
| autoDetect, | ||
| threshold: threshold, | ||
@@ -138,2 +143,3 @@ }; | ||
| --type <type> Change type: add|modify|delete|rename (default: modify) | ||
| -a, --auto Auto-detect changes via git diff | ||
| --max-depth <n> Maximum analysis depth (default: 10) | ||
@@ -158,2 +164,5 @@ -t, --include-tests Include test files in analysis | ||
| Examples: | ||
| # Auto-detect all changes via git diff | ||
| blast-radius -p ./src --auto | ||
| # Analyze a single file change | ||
@@ -768,7 +777,165 @@ blast-radius -p ./src -c src/api/user.ts | ||
| } | ||
| /** | ||
| * 使用 git diff 自动检测改动的文件和符号 | ||
| */ | ||
| async function autoDetectChanges(projectRoot) { | ||
| const { execSync } = await import('child_process'); | ||
| try { | ||
| // 获取 staged + unstaged 的改动 | ||
| const diffOutput = execSync('git diff --cached --name-only HEAD 2>/dev/null || git diff --name-only HEAD 2>/dev/null || git diff --name-only 2>/dev/null', { | ||
| cwd: projectRoot, | ||
| encoding: 'utf8', | ||
| stdio: ['pipe', 'pipe', 'pipe'], | ||
| }); | ||
| const changedFiles = diffOutput.trim().split('\n').filter(f => f.trim()); | ||
| if (changedFiles.length === 0 || changedFiles[0] === '') { | ||
| // 没有 git 改动,检查工作区 | ||
| const unstaged = execSync('git diff --name-only 2>/dev/null || echo ""', { | ||
| cwd: projectRoot, | ||
| encoding: 'utf8', | ||
| }).trim().split('\n').filter(f => f.trim()); | ||
| if (unstaged.length === 0 || unstaged[0] === '') { | ||
| return []; | ||
| } | ||
| changedFiles.length = 0; | ||
| changedFiles.push(...unstaged); | ||
| } | ||
| // 过滤只保留 TypeScript/JavaScript 文件 | ||
| const codeFiles = changedFiles.filter(f => /\.(ts|tsx|js|jsx)$/.test(f) && !f.includes('node_modules')); | ||
| if (codeFiles.length === 0) { | ||
| return []; | ||
| } | ||
| // 获取每个文件的详细 diff,包括函数/变量名 | ||
| const changedSymbols = []; | ||
| for (const file of codeFiles) { | ||
| try { | ||
| // 获取文件的 diff | ||
| const fileDiff = execSync(`git diff HEAD -- "${file}" 2>/dev/null || git diff -- "${file}" 2>/dev/null || echo ""`, { | ||
| cwd: projectRoot, | ||
| encoding: 'utf8', | ||
| stdio: ['pipe', 'pipe', 'pipe'], | ||
| }); | ||
| // 解析 diff 中的符号 (函数名、变量名等) | ||
| const symbols = parseDiffSymbols(fileDiff, file); | ||
| if (symbols.length > 0) { | ||
| for (const sym of symbols) { | ||
| changedSymbols.push({ | ||
| file: path.resolve(projectRoot, file), | ||
| symbol: sym, | ||
| type: 'modify', | ||
| diff: fileDiff, | ||
| }); | ||
| } | ||
| } | ||
| else { | ||
| // 如果无法解析符号,仍添加文件 | ||
| changedSymbols.push({ | ||
| file: path.resolve(projectRoot, file), | ||
| symbol: '', // 空符号表示分析整个文件 | ||
| type: 'modify', | ||
| diff: fileDiff, | ||
| }); | ||
| } | ||
| } | ||
| catch { | ||
| // 单个文件出错继续处理其他的 | ||
| } | ||
| } | ||
| return changedSymbols; | ||
| } | ||
| catch (error) { | ||
| console.error('⚠️ 无法获取 git diff:', error.message); | ||
| return []; | ||
| } | ||
| } | ||
| /** | ||
| * 从 diff 中解析改动的符号名 | ||
| */ | ||
| function parseDiffSymbols(diffContent, file) { | ||
| const symbols = []; | ||
| const lines = diffContent.split('\n'); | ||
| // 匹配函数定义: +functionName, +const functionName, +export functionName | ||
| const functionPattern = /^[+]export\s+(?:async\s+)?(?:function\s+(\w+)|const\s+(\w+)|(\w+)\s*=)/; | ||
| // 匹配变量声明: +export const name, +export let name, +export var name | ||
| const constPattern = /^[+]export\s+(?:const|let|var)\s+(\w+)/; | ||
| // 匹配类型定义: +export type Name, +export interface Name | ||
| const typePattern = /^[+]export\s+(?:type|interface|class|enum)\s+(\w+)/; | ||
| // 匹配 class 定义: +class ClassName | ||
| const classPattern = /^[+]class\s+(\w+)/; | ||
| for (const line of lines) { | ||
| // 跳过 diff header | ||
| if (line.startsWith('@@') || line.startsWith('---') || line.startsWith('+++') || line.startsWith('diff')) { | ||
| continue; | ||
| } | ||
| // 匹配函数 | ||
| let match = line.match(functionPattern); | ||
| if (match) { | ||
| const name = match[1] || match[2] || match[3]; | ||
| if (name && !symbols.includes(name)) { | ||
| symbols.push(name); | ||
| } | ||
| } | ||
| // 匹配 const/let/var | ||
| match = line.match(constPattern); | ||
| if (match && match[1]) { | ||
| if (!symbols.includes(match[1])) { | ||
| symbols.push(match[1]); | ||
| } | ||
| } | ||
| // 匹配类型 | ||
| match = line.match(typePattern); | ||
| if (match && match[1]) { | ||
| if (!symbols.includes(match[1])) { | ||
| symbols.push(match[1]); | ||
| } | ||
| } | ||
| // 匹配 class | ||
| match = line.match(classPattern); | ||
| if (match && match[1]) { | ||
| if (!symbols.includes(match[1])) { | ||
| symbols.push(match[1]); | ||
| } | ||
| } | ||
| } | ||
| return symbols; | ||
| } | ||
| // ─── Main ──────────────────────────────────────────────────────────────────── | ||
| async function main() { | ||
| const args = parseArgs(process.argv); | ||
| // 如果指定了 --auto,自动检测 git 改动 | ||
| if (args.autoDetect) { | ||
| console.log('🔍 Auto-detecting changes via git diff...'); | ||
| const detectedChanges = await autoDetectChanges(args.projectRoot); | ||
| if (detectedChanges.length === 0) { | ||
| console.log('✅ No code changes detected (or not a git repository)'); | ||
| process.exit(0); | ||
| } | ||
| console.log(`📝 Found ${detectedChanges.length} changed symbol(s):\n`); | ||
| for (const change of detectedChanges) { | ||
| const relPath = path.relative(args.projectRoot, change.file); | ||
| const symDisplay = change.symbol ? `#${change.symbol}` : '(file)'; | ||
| console.log(` • ${relPath}${symDisplay}`); | ||
| } | ||
| console.log(''); | ||
| // 将检测到的改动合并到 args.changes | ||
| for (const change of detectedChanges) { | ||
| // 检查是否已存在相同的文件 | ||
| const existing = args.changes.find(c => c.file === change.file); | ||
| if (existing) { | ||
| // 如果已有符号,保留;否则添加新符号 | ||
| if (!existing.symbol && change.symbol) { | ||
| existing.symbol = change.symbol; | ||
| } | ||
| } | ||
| else { | ||
| args.changes.push({ | ||
| file: change.file, | ||
| symbol: change.symbol || undefined, | ||
| type: change.type, | ||
| }); | ||
| } | ||
| } | ||
| } | ||
| if (args.changes.length === 0) { | ||
| console.error('❌ Error: No changes specified. Use --change <file>'); | ||
| console.error('❌ Error: No changes specified. Use --change <file> or --auto'); | ||
| console.error(' Run with --help for usage information'); | ||
@@ -775,0 +942,0 @@ process.exit(1); |
+2
-2
| { | ||
| "name": "blast-radius-analyzer", | ||
| "version": "1.2.1", | ||
| "version": "1.3.0", | ||
| "description": "Analyze code change impact and blast radius - 改动影响范围分析器", | ||
@@ -27,3 +27,3 @@ "type": "module", | ||
| "type": "git", | ||
| "url": "" | ||
| "url": "https://github.com/huabuyu05100510/blast-radius-analyzer" | ||
| }, | ||
@@ -30,0 +30,0 @@ "dependencies": { |
+199
-1
@@ -42,2 +42,3 @@ #!/usr/bin/env node | ||
| clearCache: boolean; | ||
| autoDetect: boolean; // 自动检测 git diff | ||
| threshold?: { | ||
@@ -61,2 +62,3 @@ files?: number; | ||
| let clearCache = false; | ||
| let autoDetect = false; // 自动检测 git diff | ||
| let threshold: CLIArgs['threshold'] = undefined; | ||
@@ -100,2 +102,4 @@ | ||
| clearCache = true; | ||
| } else if (arg === '--auto' || arg === '-a') { | ||
| autoDetect = true; | ||
| } else if (arg === '--threshold' && argv[i + 1]) { | ||
@@ -134,2 +138,3 @@ // 解析阈值: --threshold files:5,score:100,typeErrors:0 | ||
| clearCache, | ||
| autoDetect, | ||
| threshold: threshold, | ||
@@ -153,2 +158,3 @@ }; | ||
| --type <type> Change type: add|modify|delete|rename (default: modify) | ||
| -a, --auto Auto-detect changes via git diff | ||
| --max-depth <n> Maximum analysis depth (default: 10) | ||
@@ -173,2 +179,5 @@ -t, --include-tests Include test files in analysis | ||
| Examples: | ||
| # Auto-detect all changes via git diff | ||
| blast-radius -p ./src --auto | ||
| # Analyze a single file change | ||
@@ -841,2 +850,154 @@ blast-radius -p ./src -c src/api/user.ts | ||
| // ─── Auto Detect Changes via Git ─────────────────────────────────────────── | ||
| interface ChangedSymbol { | ||
| file: string; | ||
| symbol: string; | ||
| type: 'modify' | 'delete' | 'rename' | 'add'; | ||
| diff: string; // diff content for context | ||
| } | ||
| /** | ||
| * 使用 git diff 自动检测改动的文件和符号 | ||
| */ | ||
| async function autoDetectChanges(projectRoot: string): Promise<ChangedSymbol[]> { | ||
| const { execSync } = await import('child_process'); | ||
| try { | ||
| // 获取 staged + unstaged 的改动 | ||
| const diffOutput = execSync('git diff --cached --name-only HEAD 2>/dev/null || git diff --name-only HEAD 2>/dev/null || git diff --name-only 2>/dev/null', { | ||
| cwd: projectRoot, | ||
| encoding: 'utf8', | ||
| stdio: ['pipe', 'pipe', 'pipe'], | ||
| }); | ||
| const changedFiles = diffOutput.trim().split('\n').filter(f => f.trim()); | ||
| if (changedFiles.length === 0 || changedFiles[0] === '') { | ||
| // 没有 git 改动,检查工作区 | ||
| const unstaged = execSync('git diff --name-only 2>/dev/null || echo ""', { | ||
| cwd: projectRoot, | ||
| encoding: 'utf8', | ||
| }).trim().split('\n').filter(f => f.trim()); | ||
| if (unstaged.length === 0 || unstaged[0] === '') { | ||
| return []; | ||
| } | ||
| changedFiles.length = 0; | ||
| changedFiles.push(...unstaged); | ||
| } | ||
| // 过滤只保留 TypeScript/JavaScript 文件 | ||
| const codeFiles = changedFiles.filter(f => | ||
| /\.(ts|tsx|js|jsx)$/.test(f) && !f.includes('node_modules') | ||
| ); | ||
| if (codeFiles.length === 0) { | ||
| return []; | ||
| } | ||
| // 获取每个文件的详细 diff,包括函数/变量名 | ||
| const changedSymbols: ChangedSymbol[] = []; | ||
| for (const file of codeFiles) { | ||
| try { | ||
| // 获取文件的 diff | ||
| const fileDiff = execSync(`git diff HEAD -- "${file}" 2>/dev/null || git diff -- "${file}" 2>/dev/null || echo ""`, { | ||
| cwd: projectRoot, | ||
| encoding: 'utf8', | ||
| stdio: ['pipe', 'pipe', 'pipe'], | ||
| }); | ||
| // 解析 diff 中的符号 (函数名、变量名等) | ||
| const symbols = parseDiffSymbols(fileDiff, file); | ||
| if (symbols.length > 0) { | ||
| for (const sym of symbols) { | ||
| changedSymbols.push({ | ||
| file: path.resolve(projectRoot, file), | ||
| symbol: sym, | ||
| type: 'modify', | ||
| diff: fileDiff, | ||
| }); | ||
| } | ||
| } else { | ||
| // 如果无法解析符号,仍添加文件 | ||
| changedSymbols.push({ | ||
| file: path.resolve(projectRoot, file), | ||
| symbol: '', // 空符号表示分析整个文件 | ||
| type: 'modify', | ||
| diff: fileDiff, | ||
| }); | ||
| } | ||
| } catch { | ||
| // 单个文件出错继续处理其他的 | ||
| } | ||
| } | ||
| return changedSymbols; | ||
| } catch (error) { | ||
| console.error('⚠️ 无法获取 git diff:', (error as Error).message); | ||
| return []; | ||
| } | ||
| } | ||
| /** | ||
| * 从 diff 中解析改动的符号名 | ||
| */ | ||
| function parseDiffSymbols(diffContent: string, file: string): string[] { | ||
| const symbols: string[] = []; | ||
| const lines = diffContent.split('\n'); | ||
| // 匹配函数定义: +functionName, +const functionName, +export functionName | ||
| const functionPattern = /^[+]export\s+(?:async\s+)?(?:function\s+(\w+)|const\s+(\w+)|(\w+)\s*=)/; | ||
| // 匹配变量声明: +export const name, +export let name, +export var name | ||
| const constPattern = /^[+]export\s+(?:const|let|var)\s+(\w+)/; | ||
| // 匹配类型定义: +export type Name, +export interface Name | ||
| const typePattern = /^[+]export\s+(?:type|interface|class|enum)\s+(\w+)/; | ||
| // 匹配 class 定义: +class ClassName | ||
| const classPattern = /^[+]class\s+(\w+)/; | ||
| for (const line of lines) { | ||
| // 跳过 diff header | ||
| if (line.startsWith('@@') || line.startsWith('---') || line.startsWith('+++') || line.startsWith('diff')) { | ||
| continue; | ||
| } | ||
| // 匹配函数 | ||
| let match = line.match(functionPattern); | ||
| if (match) { | ||
| const name = match[1] || match[2] || match[3]; | ||
| if (name && !symbols.includes(name)) { | ||
| symbols.push(name); | ||
| } | ||
| } | ||
| // 匹配 const/let/var | ||
| match = line.match(constPattern); | ||
| if (match && match[1]) { | ||
| if (!symbols.includes(match[1])) { | ||
| symbols.push(match[1]); | ||
| } | ||
| } | ||
| // 匹配类型 | ||
| match = line.match(typePattern); | ||
| if (match && match[1]) { | ||
| if (!symbols.includes(match[1])) { | ||
| symbols.push(match[1]); | ||
| } | ||
| } | ||
| // 匹配 class | ||
| match = line.match(classPattern); | ||
| if (match && match[1]) { | ||
| if (!symbols.includes(match[1])) { | ||
| symbols.push(match[1]); | ||
| } | ||
| } | ||
| } | ||
| return symbols; | ||
| } | ||
| // ─── Main ──────────────────────────────────────────────────────────────────── | ||
@@ -847,4 +1008,41 @@ | ||
| // 如果指定了 --auto,自动检测 git 改动 | ||
| if (args.autoDetect) { | ||
| console.log('🔍 Auto-detecting changes via git diff...'); | ||
| const detectedChanges = await autoDetectChanges(args.projectRoot); | ||
| if (detectedChanges.length === 0) { | ||
| console.log('✅ No code changes detected (or not a git repository)'); | ||
| process.exit(0); | ||
| } | ||
| console.log(`📝 Found ${detectedChanges.length} changed symbol(s):\n`); | ||
| for (const change of detectedChanges) { | ||
| const relPath = path.relative(args.projectRoot, change.file); | ||
| const symDisplay = change.symbol ? `#${change.symbol}` : '(file)'; | ||
| console.log(` • ${relPath}${symDisplay}`); | ||
| } | ||
| console.log(''); | ||
| // 将检测到的改动合并到 args.changes | ||
| for (const change of detectedChanges) { | ||
| // 检查是否已存在相同的文件 | ||
| const existing = args.changes.find(c => c.file === change.file); | ||
| if (existing) { | ||
| // 如果已有符号,保留;否则添加新符号 | ||
| if (!existing.symbol && change.symbol) { | ||
| existing.symbol = change.symbol; | ||
| } | ||
| } else { | ||
| args.changes.push({ | ||
| file: change.file, | ||
| symbol: change.symbol || undefined, | ||
| type: change.type, | ||
| }); | ||
| } | ||
| } | ||
| } | ||
| if (args.changes.length === 0) { | ||
| console.error('❌ Error: No changes specified. Use --change <file>'); | ||
| console.error('❌ Error: No changes specified. Use --change <file> or --auto'); | ||
| console.error(' Run with --help for usage information'); | ||
@@ -851,0 +1049,0 @@ process.exit(1); |
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
No repository
Supply chain riskPackage does not have a linked source code repository. Without this field, a package will have no reference to the location of the source code use to generate the package.
Found 1 instance in 1 package
535439
2.56%50
2.04%14168
2.5%19
-5%