UNPKG

autosnippet

Version:

Extract code patterns into a knowledge base for AI coding assistants

457 lines (456 loc) 16.7 kB
/** * @module JvmDiscoverer * @description Java / Kotlin 项目结构发现器 * * 检测信号: build.gradle, build.gradle.kts, pom.xml, settings.gradle * 支持: Gradle (单模块/多模块), Maven (单模块/多模块) * * ⚠️ 不尝试精确解析 Gradle DSL,仅用正则启发式提取关键信息 */ import { existsSync, readdirSync, readFileSync } from 'node:fs'; import { basename, extname, join, relative, resolve } from 'node:path'; import { LanguageService } from '../../shared/LanguageService.js'; import { ProjectDiscoverer, } from './ProjectDiscoverer.js'; const SOURCE_EXTENSIONS = new Set(['.java', '.kt', '.kts']); const EXCLUDE_DIRS = new Set([ '.gradle', '.idea', '.cursor', 'build', 'target', '.git', 'node_modules', 'out', '.kotlin', ]); export class JvmDiscoverer extends ProjectDiscoverer { #projectRoot = null; #targets = []; #depGraph = { nodes: [], edges: [] }; #buildSystem = null; // 'gradle' | 'maven' get id() { return 'jvm'; } get displayName() { return `JVM (${this.#buildSystem === 'maven' ? 'Maven' : 'Gradle'})`; } async detect(projectRoot) { let confidence = 0; const reasons = []; // Gradle if (existsSync(join(projectRoot, 'build.gradle')) || existsSync(join(projectRoot, 'build.gradle.kts'))) { confidence = 0.9; reasons.push('build.gradle(.kts) exists'); } if (existsSync(join(projectRoot, 'settings.gradle')) || existsSync(join(projectRoot, 'settings.gradle.kts'))) { confidence = Math.max(confidence, 0.85); confidence = Math.min(confidence + 0.05, 1.0); reasons.push('settings.gradle(.kts) exists'); } // Maven if (existsSync(join(projectRoot, 'pom.xml'))) { confidence = Math.max(confidence, 0.85); reasons.push('pom.xml exists'); } return { match: confidence > 0, confidence: Math.min(confidence, 1.0), reason: reasons.join(', ') || 'No JVM markers found', }; } async load(projectRoot) { this.#projectRoot = projectRoot; this.#targets = []; this.#depGraph = { nodes: [], edges: [] }; // 判断构建系统 const hasGradle = existsSync(join(projectRoot, 'build.gradle')) || existsSync(join(projectRoot, 'build.gradle.kts')); const hasMaven = existsSync(join(projectRoot, 'pom.xml')); if (hasGradle) { this.#buildSystem = 'gradle'; this.#loadGradle(projectRoot); } else if (hasMaven) { this.#buildSystem = 'maven'; this.#loadMaven(projectRoot); } } async listTargets() { return this.#targets; } async getTargetFiles(target) { const targetObj = typeof target === 'string' ? this.#targets.find((t) => t.name === target) : target; if (!targetObj?.path || !existsSync(targetObj.path)) { return []; } const files = []; // JVM 约定: src/main/java, src/main/kotlin, src/test/java, src/test/kotlin const sourceDirs = [ join(targetObj.path, 'src', 'main', 'java'), join(targetObj.path, 'src', 'main', 'kotlin'), join(targetObj.path, 'src', 'test', 'java'), join(targetObj.path, 'src', 'test', 'kotlin'), ]; // 也支持非标准布局 — 直接在 target 路径下搜索 const hasSrcDir = sourceDirs.some((d) => existsSync(d)); if (hasSrcDir) { for (const srcDir of sourceDirs) { if (existsSync(srcDir)) { this.#collectFiles(srcDir, targetObj.path, files); } } } else { this.#collectFiles(targetObj.path, targetObj.path, files); } return files; } async getDependencyGraph() { return this.#depGraph; } // ── Gradle ── #loadGradle(projectRoot) { // 解析 settings.gradle 找子模块 const submodules = this.#parseGradleSettings(projectRoot); if (submodules.length > 0) { // 多模块 Gradle 项目 for (const mod of submodules) { const modPath = resolve(projectRoot, mod.replace(/:/g, '/')); if (!existsSync(modPath)) { continue; } const framework = this.#detectGradleFramework(modPath); const lang = this.#detectPrimaryLang(modPath); this.#targets.push({ name: mod, path: modPath, type: this.#inferGradleTargetType(modPath, mod), language: lang, framework, metadata: { buildSystem: 'gradle', module: mod }, }); this.#depGraph.nodes.push(mod); } // 提取模块间依赖 this.#parseGradleModuleDeps(projectRoot, submodules); } else { // 单模块 Gradle 项目 const framework = this.#detectGradleFramework(projectRoot); const lang = this.#detectPrimaryLang(projectRoot); const name = basename(projectRoot); this.#targets.push({ name, path: projectRoot, type: 'app', language: lang, framework, metadata: { buildSystem: 'gradle' }, }); this.#depGraph.nodes.push(name); } // 提取外部依赖 this.#parseGradleExternalDeps(projectRoot); } #parseGradleSettings(projectRoot) { const modules = []; for (const fname of ['settings.gradle', 'settings.gradle.kts']) { const settingsPath = join(projectRoot, fname); if (!existsSync(settingsPath)) { continue; } try { const content = readFileSync(settingsPath, 'utf8'); // include ':app', ':lib:core', ... const includeMatches = content.matchAll(/include\s*\(?[\s]*['"]([^'"]+)['"](?:\s*,\s*['"]([^'"]+)['"])*/g); for (const m of includeMatches) { for (let i = 1; i < m.length; i++) { if (m[i]) { modules.push(m[i].replace(/^:/, '')); } } } // include(":app") const includeKtsMatches = content.matchAll(/include\s*\(\s*["']([^"']+)["']\s*\)/g); for (const m of includeKtsMatches) { if (m[1]) { modules.push(m[1].replace(/^:/, '')); } } } catch { /* skip */ } } return [...new Set(modules)]; } #detectGradleFramework(dir) { for (const fname of ['build.gradle', 'build.gradle.kts']) { const buildPath = join(dir, fname); if (!existsSync(buildPath)) { continue; } try { const content = readFileSync(buildPath, 'utf8'); if (/com\.android|android\s*\{|apply.*android/.test(content)) { return 'android'; } if (/org\.springframework|spring-boot/.test(content)) { return 'spring'; } if (/io\.ktor/.test(content)) { return 'ktor'; } if (/org\.jetbrains\.compose/.test(content)) { return 'compose'; } } catch { /* skip */ } } return null; } #inferGradleTargetType(dir, name) { for (const fname of ['build.gradle', 'build.gradle.kts']) { const buildPath = join(dir, fname); if (!existsSync(buildPath)) { continue; } try { const content = readFileSync(buildPath, 'utf8'); if (/application|com\.android\.application/.test(content)) { return 'app'; } if (/java-library|com\.android\.library/.test(content)) { return 'library'; } } catch { /* skip */ } } if (/test/i.test(name)) { return 'test'; } return 'library'; } #parseGradleModuleDeps(projectRoot, submodules) { const moduleSet = new Set(submodules); for (const mod of submodules) { const modPath = resolve(projectRoot, mod.replace(/:/g, '/')); for (const fname of ['build.gradle', 'build.gradle.kts']) { const buildPath = join(modPath, fname); if (!existsSync(buildPath)) { continue; } try { const content = readFileSync(buildPath, 'utf8'); // project(':lib:core'), project(":lib:core") const projDeps = content.matchAll(/project\s*\(\s*['"][:.]?([^'"]+)['"]\s*\)/g); for (const m of projDeps) { const depMod = m[1].replace(/^:/, ''); if (moduleSet.has(depMod)) { this.#depGraph.edges.push({ from: mod, to: depMod, type: 'depends_on' }); } } } catch { /* skip */ } } } } #parseGradleExternalDeps(projectRoot) { for (const fname of ['build.gradle', 'build.gradle.kts']) { const buildPath = join(projectRoot, fname); if (!existsSync(buildPath)) { continue; } try { const content = readFileSync(buildPath, 'utf8'); const rootTarget = this.#targets[0]?.name; if (!rootTarget) { return; } // implementation 'group:artifact:version' or implementation("group:artifact:version") const depMatches = content.matchAll(/(?:implementation|api|compileOnly|runtimeOnly)\s*[("']+([^)'"]+)[)'"]+/g); for (const m of depMatches) { const parts = m[1].split(':'); if (parts.length >= 2) { const depName = `${parts[0]}:${parts[1]}`; this.#depGraph.edges.push({ from: rootTarget, to: depName, type: 'depends_on' }); } } } catch { /* skip */ } } } // ── Maven ── #loadMaven(projectRoot) { const pomPath = join(projectRoot, 'pom.xml'); if (!existsSync(pomPath)) { return; } const pomContent = readFileSync(pomPath, 'utf8'); const projectName = this.#extractXmlValue(pomContent, 'artifactId') || basename(projectRoot); // 提取子模块 const modules = this.#parseMavenModules(pomContent); if (modules.length > 0) { for (const mod of modules) { const modPath = resolve(projectRoot, mod); if (!existsSync(modPath)) { continue; } const lang = this.#detectPrimaryLang(modPath); const framework = this.#detectMavenFramework(modPath); this.#targets.push({ name: mod, path: modPath, type: /test/i.test(mod) ? 'test' : 'library', language: lang, framework, metadata: { buildSystem: 'maven', module: mod }, }); this.#depGraph.nodes.push(mod); } } else { const lang = this.#detectPrimaryLang(projectRoot); const framework = this.#detectMavenFramework(projectRoot); this.#targets.push({ name: projectName, path: projectRoot, type: 'app', language: lang, framework, metadata: { buildSystem: 'maven' }, }); this.#depGraph.nodes.push(projectName); } // 提取外部依赖 this.#parseMavenDeps(pomContent); } #parseMavenModules(pomContent) { const modules = []; const moduleMatches = pomContent.matchAll(/<module>([^<]+)<\/module>/g); for (const m of moduleMatches) { modules.push(m[1].trim()); } return modules; } #detectMavenFramework(dir) { const pomPath = join(dir, 'pom.xml'); if (!existsSync(pomPath)) { return null; } try { const content = readFileSync(pomPath, 'utf8'); if (/spring-boot|springframework/.test(content)) { return 'spring'; } if (/android/.test(content)) { return 'android'; } } catch { /* skip */ } return null; } #parseMavenDeps(pomContent) { const rootTarget = this.#targets[0]?.name; if (!rootTarget) { return; } // 简化: 提取 <dependency> 中的 groupId:artifactId const depBlocks = pomContent.matchAll(/<dependency>([\s\S]*?)<\/dependency>/g); for (const block of depBlocks) { const groupId = this.#extractXmlValue(block[1], 'groupId'); const artifactId = this.#extractXmlValue(block[1], 'artifactId'); if (groupId && artifactId) { this.#depGraph.edges.push({ from: rootTarget, to: `${groupId}:${artifactId}`, type: 'depends_on', }); } } } // ── 共用工具 ── #detectPrimaryLang(dir) { let javaCount = 0; let kotlinCount = 0; const srcMain = join(dir, 'src', 'main'); if (existsSync(join(srcMain, 'kotlin'))) { kotlinCount += 10; } if (existsSync(join(srcMain, 'java'))) { javaCount += 10; } // 快速采样 const srcDirs = [join(srcMain, 'java'), join(srcMain, 'kotlin'), dir]; for (const sd of srcDirs) { if (!existsSync(sd)) { continue; } try { const files = readdirSync(sd).slice(0, 20); for (const f of files) { if (f.endsWith('.kt') || f.endsWith('.kts')) { kotlinCount++; } if (f.endsWith('.java')) { javaCount++; } } } catch { /* skip */ } } return kotlinCount > javaCount ? 'kotlin' : 'java'; } #collectFiles(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 (EXCLUDE_DIRS.has(entry.name)) { continue; } const fullPath = join(dir, entry.name); if (entry.isDirectory()) { this.#collectFiles(fullPath, rootDir, files, depth + 1); } else if (entry.isFile()) { const ext = extname(entry.name); if (SOURCE_EXTENSIONS.has(ext)) { files.push({ name: entry.name, path: fullPath, relativePath: relative(rootDir, fullPath), language: LanguageService.inferLang(entry.name), }); } } } } catch { /* skip */ } } #extractXmlValue(xml, tag) { const match = xml.match(new RegExp(`<${tag}>([^<]*)</${tag}>`)); return match ? match[1].trim() : null; } }