Files
Claude-Code-Workflow/.ccw-cache/graph-impl.js
catlog22 811382775d feat: Implement fuzzy search functionality in smart-search.js
- Added buildFuzzyRegex function for approximate matching.
- Enhanced buildRipgrepCommand to support fuzzy parameter.
- Updated executeAutoMode to handle fuzzy search case.
- Implemented executeFuzzyMode for executing fuzzy search using ripgrep.
- Refactored import and export parsing functions for better modularity.
- Improved dependency graph building and circular dependency detection.
- Added caching mechanism for dependency graph to optimize performance.
2025-12-11 23:28:35 +08:00

378 lines
11 KiB
JavaScript

/**
* Parse import statements from file content
* @param {string} fileContent - File content to parse
* @returns {Array<{source: string, specifiers: string[]}>}
*/
function parseImports(fileContent) {
const imports = [];
// Pattern 1: ES6 import statements
const es6ImportPattern = /import\s+(?:(?:(\*\s+as\s+\w+)|(\w+)|(?:\{([^}]+)\}))\s+from\s+)?['"]([^'"]+)['"]/g;
let match;
while ((match = es6ImportPattern.exec(fileContent)) !== null) {
const source = match[4];
const specifiers = [];
if (match[1]) specifiers.push(match[1]);
else if (match[2]) specifiers.push(match[2]);
else if (match[3]) {
const named = match[3].split(',').map(s => s.trim());
specifiers.push(...named);
}
imports.push({ source, specifiers });
}
// Pattern 2: CommonJS require()
const requirePattern = /require\(['"]([^'"]+)['"]\)/g;
while ((match = requirePattern.exec(fileContent)) !== null) {
imports.push({ source: match[1], specifiers: [] });
}
// Pattern 3: Dynamic import()
const dynamicImportPattern = /import\(['"]([^'"]+)['"]\)/g;
while ((match = dynamicImportPattern.exec(fileContent)) !== null) {
imports.push({ source: match[1], specifiers: [] });
}
// Pattern 4: TypeScript import type
const typeImportPattern = /import\s+type\s+(?:\{([^}]+)\})\s+from\s+['"]([^'"]+)['"]/g;
while ((match = typeImportPattern.exec(fileContent)) !== null) {
const source = match[2];
const specifiers = match[1].split(',').map(s => s.trim());
imports.push({ source, specifiers });
}
return imports;
}
/**
* Parse export statements from file content
* @param {string} fileContent - File content to parse
* @returns {Array<{name: string, type: string}>}
*/
function parseExports(fileContent) {
const exports = [];
// Pattern 1: export default
const defaultExportPattern = /export\s+default\s+(?:class|function|const|let|var)?\s*(\w+)?/g;
let match;
while ((match = defaultExportPattern.exec(fileContent)) !== null) {
exports.push({ name: match[1] || 'default', type: 'default' });
}
// Pattern 2: export named declarations
const namedDeclPattern = /export\s+(?:const|let|var|function|class)\s+(\w+)/g;
while ((match = namedDeclPattern.exec(fileContent)) !== null) {
exports.push({ name: match[1], type: 'named' });
}
// Pattern 3: export { ... }
const namedExportPattern = /export\s+\{([^}]+)\}/g;
while ((match = namedExportPattern.exec(fileContent)) !== null) {
const names = match[1].split(',').map(s => {
const parts = s.trim().split(/\s+as\s+/);
return parts[parts.length - 1];
});
names.forEach(name => {
exports.push({ name: name.trim(), type: 'named' });
});
}
return exports;
}
/**
* Build dependency graph by scanning project files
* @param {string} rootPath - Root directory to scan
* @param {string[]} gitignorePatterns - Patterns to exclude
* @returns {{nodes: Array, edges: Array, metadata: Object}}
*/
function buildDependencyGraph(rootPath, gitignorePatterns = []) {
const { readFileSync, readdirSync, existsSync } = require('fs');
const { join, relative, resolve: resolvePath } = require('path');
const nodes = [];
const edges = [];
const processedFiles = new Set();
const SYSTEM_EXCLUDES = [
'.git', 'node_modules', '.npm', '.yarn', '.pnpm',
'dist', 'build', 'out', 'coverage', '.cache',
'.next', '.nuxt', '.vite', '__pycache__', 'venv'
];
function shouldExclude(name) {
if (SYSTEM_EXCLUDES.includes(name)) return true;
for (const pattern of gitignorePatterns) {
if (name === pattern) return true;
if (pattern.includes('*')) {
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
if (regex.test(name)) return true;
}
}
return false;
}
function scanDirectory(dirPath) {
if (!existsSync(dirPath)) return;
try {
const entries = readdirSync(dirPath, { withFileTypes: true });
for (const entry of entries) {
if (shouldExclude(entry.name)) continue;
const fullPath = join(dirPath, entry.name);
if (entry.isDirectory()) {
scanDirectory(fullPath);
} else if (entry.isFile()) {
const ext = entry.name.split('.').pop();
if (['js', 'mjs', 'cjs', 'ts', 'tsx', 'jsx'].includes(ext)) {
processFile(fullPath);
}
}
}
} catch (err) {
// Skip directories we can't read
}
}
function processFile(filePath) {
if (processedFiles.has(filePath)) return;
processedFiles.add(filePath);
try {
const content = readFileSync(filePath, 'utf8');
const relativePath = './' + relative(rootPath, filePath).replace(/\\/g, '/');
const fileExports = parseExports(content);
nodes.push({
id: relativePath,
path: filePath,
exports: fileExports
});
const imports = parseImports(content);
imports.forEach(imp => {
let targetPath = imp.source;
if (!targetPath.startsWith('.') && !targetPath.startsWith('/')) {
return;
}
try {
targetPath = resolvePath(join(filePath, '..', targetPath));
const targetRelative = './' + relative(rootPath, targetPath).replace(/\\/g, '/');
edges.push({
from: relativePath,
to: targetRelative,
imports: imp.specifiers
});
} catch (err) {
// Skip invalid paths
}
});
} catch (err) {
// Skip files we can't read or parse
}
}
scanDirectory(rootPath);
const circularDeps = detectCircularDependencies(edges);
return {
nodes,
edges,
metadata: {
timestamp: Date.now(),
rootPath,
nodeCount: nodes.length,
edgeCount: edges.length,
circular_deps_detected: circularDeps.length > 0,
circular_deps: circularDeps
}
};
}
/**
* Detect circular dependencies in the graph
* @param {Array} edges - Graph edges
* @returns {Array} List of circular dependency chains
*/
function detectCircularDependencies(edges) {
const cycles = [];
const visited = new Set();
const recStack = new Set();
const graph = {};
edges.forEach(edge => {
if (!graph[edge.from]) graph[edge.from] = [];
graph[edge.from].push(edge.to);
});
function dfs(node, path = []) {
if (recStack.has(node)) {
const cycleStart = path.indexOf(node);
if (cycleStart !== -1) {
cycles.push(path.slice(cycleStart).concat(node));
}
return;
}
if (visited.has(node)) return;
visited.add(node);
recStack.add(node);
path.push(node);
const neighbors = graph[node] || [];
for (const neighbor of neighbors) {
dfs(neighbor, [...path]);
}
recStack.delete(node);
}
Object.keys(graph).forEach(node => {
if (!visited.has(node)) {
dfs(node);
}
});
return cycles;
}
/**
* Mode: graph - Dependency and relationship traversal
* Analyzes code relationships (imports, exports, dependencies)
*/
async function executeGraphMode(params) {
const { readFileSync, writeFileSync, mkdirSync, existsSync } = await import('fs');
const { join, resolve: resolvePath } = await import('path');
const { query, paths = [], maxResults = 100 } = params;
const rootPath = resolvePath(process.cwd(), paths[0] || '.');
const cacheDir = join(process.cwd(), '.ccw-cache');
const cacheFile = join(cacheDir, 'dependency-graph.json');
const CACHE_TTL = 5 * 60 * 1000;
let graph;
if (existsSync(cacheFile)) {
try {
const cached = JSON.parse(readFileSync(cacheFile, 'utf8'));
const age = Date.now() - cached.metadata.timestamp;
if (age < CACHE_TTL) {
graph = cached;
}
} catch (err) {
// Cache invalid, will rebuild
}
}
if (!graph) {
const gitignorePatterns = [];
const gitignorePath = join(rootPath, '.gitignore');
if (existsSync(gitignorePath)) {
const content = readFileSync(gitignorePath, 'utf8');
content.split('\n').forEach(line => {
line = line.trim();
if (!line || line.startsWith('#')) return;
gitignorePatterns.push(line.replace(/\/$/, ''));
});
}
graph = buildDependencyGraph(rootPath, gitignorePatterns);
try {
mkdirSync(cacheDir, { recursive: true });
writeFileSync(cacheFile, JSON.stringify(graph, null, 2), 'utf8');
} catch (err) {
// Cache write failed, continue
}
}
const queryLower = query.toLowerCase();
let queryType = 'unknown';
let filteredNodes = [];
let filteredEdges = [];
let paths = [];
if (queryLower.match(/imports?\s+(\S+)/)) {
queryType = 'imports';
const target = queryLower.match(/imports?\s+(\S+)/)[1];
filteredEdges = graph.edges.filter(edge =>
edge.to.includes(target) || edge.imports.some(imp => imp.toLowerCase().includes(target))
);
const nodeIds = new Set(filteredEdges.map(e => e.from));
filteredNodes = graph.nodes.filter(n => nodeIds.has(n.id));
} else if (queryLower.match(/exports?\s+(\S+)/)) {
queryType = 'exports';
const target = queryLower.match(/exports?\s+(\S+)/)[1];
filteredNodes = graph.nodes.filter(node =>
node.exports.some(exp => exp.name.toLowerCase().includes(target))
);
const nodeIds = new Set(filteredNodes.map(n => n.id));
filteredEdges = graph.edges.filter(e => nodeIds.has(e.from) || nodeIds.has(e.to));
} else if (queryLower.includes('dependency') || queryLower.includes('chain') || queryLower.includes('depends')) {
queryType = 'dependency_chain';
filteredNodes = graph.nodes.slice(0, maxResults);
filteredEdges = graph.edges;
if (graph.metadata.circular_deps && graph.metadata.circular_deps.length > 0) {
paths = graph.metadata.circular_deps.slice(0, 10);
}
} else {
queryType = 'module_search';
filteredNodes = graph.nodes.filter(node =>
node.id.toLowerCase().includes(queryLower) ||
node.path.toLowerCase().includes(queryLower)
);
const nodeIds = new Set(filteredNodes.map(n => n.id));
filteredEdges = graph.edges.filter(e => nodeIds.has(e.from) || nodeIds.has(e.to));
}
if (filteredNodes.length > maxResults) {
filteredNodes = filteredNodes.slice(0, maxResults);
}
return {
success: true,
graph: {
nodes: filteredNodes,
edges: filteredEdges,
paths
},
metadata: {
mode: 'graph',
storage: 'json',
query_type: queryType,
total_nodes: graph.metadata.nodeCount,
total_edges: graph.metadata.edgeCount,
filtered_nodes: filteredNodes.length,
filtered_edges: filteredEdges.length,
circular_deps_detected: graph.metadata.circular_deps_detected,
cached: existsSync(cacheFile),
query
}
};
}