autosnippet
Version:
Extract code patterns into a knowledge base for AI coding assistants
476 lines (475 loc) • 17.8 kB
JavaScript
/**
* @module RustDiscoverer
* @description Rust 项目结构发现器
*
* 检测信号: Cargo.toml, Cargo.lock, *.rs
* 支持: 单 crate 项目、Cargo workspace(多 crate)、标准目录布局 (src/ tests/ benches/ examples/)
*/
import { existsSync, readdirSync, readFileSync } from 'node:fs';
import { basename, extname, join, relative } from 'node:path';
import { ProjectDiscoverer, } from './ProjectDiscoverer.js';
const SOURCE_EXTENSIONS = new Set(['.rs']);
const EXCLUDE_DIRS = new Set([
'.git',
'target',
'node_modules',
'.cargo',
'.idea',
'.vscode',
'dist',
'build',
'.cursor',
]);
export class RustDiscoverer extends ProjectDiscoverer {
#projectRoot = null;
#targets = [];
#depGraph = { nodes: [], edges: [] };
#crateName = null;
get id() {
return 'rust';
}
get displayName() {
return 'Rust (Cargo)';
}
async detect(projectRoot) {
let confidence = 0;
const reasons = [];
if (existsSync(join(projectRoot, 'Cargo.toml'))) {
confidence = 0.92;
reasons.push('Cargo.toml exists');
}
if (existsSync(join(projectRoot, 'Cargo.lock'))) {
confidence = Math.max(confidence, 0.7);
if (confidence < 0.92) {
confidence += 0.1;
}
reasons.push('Cargo.lock exists');
}
if (existsSync(join(projectRoot, 'rust-toolchain.toml')) ||
existsSync(join(projectRoot, 'rust-toolchain'))) {
confidence = Math.max(confidence, 0.85);
reasons.push('rust-toolchain exists');
}
// 兜底: 根目录有 .rs 文件
if (confidence === 0) {
try {
const entries = readdirSync(projectRoot);
if (entries.some((e) => e.endsWith('.rs'))) {
confidence = 0.5;
reasons.push('*.rs files found at root');
}
}
catch {
/* skip */
}
}
return {
match: confidence > 0,
confidence: Math.min(confidence, 1.0),
reason: reasons.join(', ') || 'No Rust markers found',
};
}
async load(projectRoot) {
this.#projectRoot = projectRoot;
this.#targets = [];
this.#depGraph = { nodes: [], edges: [] };
// 解析 Cargo.toml
const cargoInfo = this.#parseCargoToml(projectRoot);
this.#crateName = cargoInfo?.name || basename(projectRoot);
const framework = this.#detectFramework(projectRoot);
// 主 Target
this.#targets.push({
name: this.#crateName,
path: projectRoot,
type: cargoInfo?.isBin ? 'application' : 'library',
language: 'rust',
framework,
metadata: {
edition: cargoInfo?.edition || null,
crateName: this.#crateName,
},
});
this.#depGraph.nodes.push(this.#crateName);
// Cargo workspace — 发现成员 crate
const workspaceMembers = this.#discoverWorkspaceMembers(projectRoot);
for (const member of workspaceMembers) {
this.#targets.push(member);
this.#depGraph.nodes.push(member.name);
}
// examples/ 下的二进制示例
this.#discoverExamples(projectRoot, framework);
// benches/ 下的 benchmark
this.#discoverBenches(projectRoot);
// tests/ 集成测试
const testsDir = join(projectRoot, 'tests');
if (existsSync(testsDir)) {
this.#targets.push({
name: 'tests',
path: testsDir,
type: 'test',
language: 'rust',
});
}
// 解析依赖
this.#parseDependencies(projectRoot);
// 发现内部模块
this.#discoverInternalModules(projectRoot);
}
async listTargets() {
return this.#targets;
}
async getTargetFiles(target) {
const targetPath = typeof target === 'string'
? this.#targets.find((t) => t.name === target)?.path || this.#projectRoot
: target.path;
if (!targetPath || !existsSync(targetPath)) {
return [];
}
const files = [];
this.#collectRsFiles(targetPath, targetPath, files);
return files;
}
async getDependencyGraph() {
return this.#depGraph;
}
// ── 内部实现 ──
/** 简易解析 Cargo.toml(无 TOML 解析器,使用正则) */
#parseCargoToml(projectRoot) {
const cargoPath = join(projectRoot, 'Cargo.toml');
if (!existsSync(cargoPath)) {
return null;
}
try {
const content = readFileSync(cargoPath, 'utf8');
const name = content.match(/^\s*name\s*=\s*"([^"]+)"/m)?.[1];
const edition = content.match(/^\s*edition\s*=\s*"([^"]+)"/m)?.[1];
// 判断是 bin 还是 lib
const hasMainRs = existsSync(join(projectRoot, 'src', 'main.rs'));
const hasLibRs = existsSync(join(projectRoot, 'src', 'lib.rs'));
const hasBinSection = /\[\[bin\]\]/.test(content);
return {
name,
edition,
isBin: hasMainRs || hasBinSection,
isLib: hasLibRs,
};
}
catch {
return null;
}
}
/** 发现 Cargo workspace 成员 */
#discoverWorkspaceMembers(projectRoot) {
const cargoPath = join(projectRoot, 'Cargo.toml');
if (!existsSync(cargoPath)) {
return [];
}
try {
const content = readFileSync(cargoPath, 'utf8');
// [workspace] members = ["crate_a", "crate_b", "crates/*"]
const workspaceBlock = content.match(/\[workspace\]([\s\S]*?)(?:\n\[|\s*$)/);
if (!workspaceBlock) {
return [];
}
const membersLine = workspaceBlock[1].match(/members\s*=\s*\[([\s\S]*?)\]/);
if (!membersLine) {
return [];
}
const memberPatterns = membersLine[1]
.split(',')
.map((s) => s.replace(/["\s]/g, ''))
.filter(Boolean);
const members = [];
for (const pattern of memberPatterns) {
if (pattern.includes('*')) {
// Glob — 展开
const prefix = pattern.replace('/*', '');
const parentDir = join(projectRoot, prefix);
if (!existsSync(parentDir)) {
continue;
}
try {
const entries = readdirSync(parentDir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory() && !entry.name.startsWith('.')) {
const memberPath = join(parentDir, entry.name);
if (existsSync(join(memberPath, 'Cargo.toml'))) {
const info = this.#parseCargoToml(memberPath);
members.push({
name: info?.name || entry.name,
path: memberPath,
type: info?.isBin ? 'application' : 'library',
language: 'rust',
metadata: {
edition: info?.edition,
isWorkspaceMember: true,
},
});
}
}
}
}
catch {
/* skip */
}
}
else {
const memberPath = join(projectRoot, pattern);
if (existsSync(join(memberPath, 'Cargo.toml'))) {
const info = this.#parseCargoToml(memberPath);
members.push({
name: info?.name || basename(pattern),
path: memberPath,
type: info?.isBin ? 'application' : 'library',
language: 'rust',
metadata: {
edition: info?.edition,
isWorkspaceMember: true,
},
});
}
}
}
return members;
}
catch {
return [];
}
}
/** 发现 examples/ 目录 */
#discoverExamples(projectRoot, framework) {
const examplesDir = join(projectRoot, 'examples');
if (!existsSync(examplesDir)) {
return;
}
try {
const entries = readdirSync(examplesDir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isFile() && entry.name.endsWith('.rs')) {
// 单文件示例不作为独立 target,只记录目录
}
else if (entry.isDirectory()) {
const subDir = join(examplesDir, entry.name);
if (existsSync(join(subDir, 'main.rs'))) {
this.#targets.push({
name: `examples/${entry.name}`,
path: subDir,
type: 'example',
language: 'rust',
framework,
});
}
}
}
// 如果有任何 .rs 文件,添加整个 examples 目录
if (entries.some((e) => e.isFile() && e.name.endsWith('.rs'))) {
this.#targets.push({
name: 'examples',
path: examplesDir,
type: 'example',
language: 'rust',
});
}
}
catch {
/* skip */
}
}
/** 发现 benches/ 目录 */
#discoverBenches(projectRoot) {
const benchDir = join(projectRoot, 'benches');
if (!existsSync(benchDir)) {
return;
}
try {
const entries = readdirSync(benchDir);
if (entries.some((e) => e.endsWith('.rs'))) {
this.#targets.push({
name: 'benches',
path: benchDir,
type: 'benchmark',
language: 'rust',
});
}
}
catch {
/* skip */
}
}
/** 检测 Rust Web/网络框架 */
#detectFramework(projectRoot) {
const cargoPath = join(projectRoot, 'Cargo.toml');
if (!existsSync(cargoPath)) {
return null;
}
try {
const content = readFileSync(cargoPath, 'utf8');
if (/\bactix-web\b/.test(content)) {
return 'actix-web';
}
if (/\baxum\b/.test(content)) {
return 'axum';
}
if (/\brocket\b/.test(content)) {
return 'rocket';
}
if (/\bwarp\b/.test(content)) {
return 'warp';
}
if (/\btokio\b/.test(content) && /\bhyper\b/.test(content)) {
return 'hyper';
}
if (/\btokio\b/.test(content)) {
return 'tokio';
}
if (/\basync-std\b/.test(content)) {
return 'async-std';
}
if (/\btauri\b/.test(content)) {
return 'tauri';
}
if (/\bbevy\b/.test(content)) {
return 'bevy';
}
if (/\bclap\b/.test(content)) {
return 'clap-cli';
}
if (/\bserde\b/.test(content)) {
return 'serde';
}
}
catch {
/* skip */
}
return null;
}
/** 解析 Cargo.toml 的 [dependencies] 到 depGraph */
#parseDependencies(projectRoot) {
const cargoPath = join(projectRoot, 'Cargo.toml');
if (!existsSync(cargoPath)) {
return;
}
const nodeSet = new Set(this.#depGraph.nodes.map((n) => (typeof n === 'string' ? n : n.id)));
const rootNode = typeof this.#depGraph.nodes[0] === 'string'
? this.#depGraph.nodes[0]
: this.#depGraph.nodes[0]?.id || 'root';
try {
const content = readFileSync(cargoPath, 'utf8');
// 匹配 [dependencies] 和 [dev-dependencies] 块
const depSections = content.matchAll(/\[((?:dev-|build-)?dependencies)\]([\s\S]*?)(?=\n\[|$)/g);
for (const section of depSections) {
const sectionType = section[1];
const isDev = sectionType.startsWith('dev-');
const isBuild = sectionType.startsWith('build-');
const lines = section[2].split('\n');
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('[')) {
continue;
}
// dep = "version" 或 dep = { version = "...", ... }
const depMatch = trimmed.match(/^(\S+)\s*=/);
if (depMatch) {
const depName = depMatch[1].replace(/"/g, '');
if (!nodeSet.has(depName)) {
this.#depGraph.nodes.push({
id: depName,
label: depName,
type: 'external',
isDev,
isBuild,
});
nodeSet.add(depName);
}
this.#depGraph.edges.push({
from: rootNode,
to: depName,
type: isDev ? 'dev-dependency' : isBuild ? 'build-dependency' : 'dependency',
});
}
}
}
}
catch {
/* skip */
}
}
/** 发现内部模块(src/ 子目录) */
#discoverInternalModules(projectRoot) {
const srcDir = join(projectRoot, 'src');
if (!existsSync(srcDir)) {
return;
}
const nodeSet = new Set(this.#depGraph.nodes.map((n) => (typeof n === 'string' ? n : n.id)));
const walk = (dir, relPath, depth) => {
if (depth > 6) {
return;
}
try {
const entries = readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
if (!entry.isDirectory() || entry.name.startsWith('.') || EXCLUDE_DIRS.has(entry.name)) {
continue;
}
const subDir = join(dir, entry.name);
const subRel = relPath ? `${relPath}/${entry.name}` : entry.name;
try {
const subEntries = readdirSync(subDir);
const hasRsFiles = subEntries.some((e) => e.endsWith('.rs'));
if (hasRsFiles && !nodeSet.has(subRel)) {
this.#depGraph.nodes.push({ id: subRel, label: subRel, type: 'internal' });
nodeSet.add(subRel);
}
}
catch {
/* skip */
}
walk(subDir, subRel, depth + 1);
}
}
catch {
/* skip */
}
};
walk(srcDir, '', 0);
}
/** 递归收集 .rs 文件 */
#collectRsFiles(dir, rootDir, files, depth = 0) {
if (depth > 15) {
return;
}
try {
const entries = readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
if (entry.name.startsWith('.')) {
continue;
}
if (entry.isDirectory()) {
if (EXCLUDE_DIRS.has(entry.name)) {
continue;
}
this.#collectRsFiles(join(dir, entry.name), rootDir, files, depth + 1);
}
else if (entry.isFile() && SOURCE_EXTENSIONS.has(extname(entry.name))) {
const fullPath = join(dir, entry.name);
try {
const content = readFileSync(fullPath, 'utf8');
files.push({
name: entry.name,
path: fullPath,
relativePath: relative(rootDir, fullPath),
language: 'rust',
content,
});
}
catch {
/* unreadable */
}
}
}
}
catch {
/* permission error */
}
}
}