Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions internal/cmd/assets/.claude/hooks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,15 @@ Claude Code 钩子系统,用于自动化 ABCoder 代码分析工作流。

**功能**: 自动检测项目语言并生成最新 AST 到 `~/.asts/` 目录( `~/.claude.json` 需要配置abcoder目录 `abcoder mcp ${HOME}/.asts` )

- 自动检测 Go/TypeScript 项目
- 执行 `abcoder parse go/ts . -o ~/.asts/repo.json` 生成 AST 文件
- 自动检测 Go/TypeScript/Java 项目
- 执行 `abcoder parse go/ts/java . -o ~/.asts/repo.json` 生成 AST 文件

**检测规则**:
- Go: 存在 `go.mod` 或 `main.go`
- TypeScript: 存在 `package.json`, `tsconfig.json` 或 `.ts/.tsx` 文件
- Java (Maven): 存在 `pom.xml`
- Java (Gradle): 存在 `settings.gradle(.kts)` 或 `build.gradle(.kts)`
- Java (Monorepo): 根目录无构建文件时,向下搜索 2 层查找 `pom.xml` / `build.gradle(.kts)`

### 2. prompt.sh (PostToolUse)

Expand Down
46 changes: 43 additions & 3 deletions internal/cmd/assets/.claude/hooks/abcoder/parse.sh
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,47 @@ detect_project_info() {
return 0
fi

# 4. 未检测到目标语言
# 4. 检测 Java 项目(Maven / Gradle,支持 monorepo)
# 4.1 Maven:根目录有 pom.xml
if [[ -f "${target_dir}/pom.xml" ]]; then
local artifact_id=$(grep -m1 '<artifactId>' "${target_dir}/pom.xml" | sed 's/.*<artifactId>\(.*\)<\/artifactId>.*/\1/' | tr -d '[:space:]')
if [[ -n "$artifact_id" ]]; then
echo "java|${artifact_id}"
return 0
fi
echo "java|$(get_basename "$target_dir")"
return 0
fi

# 4.2 Gradle:settings.gradle(.kts) 表示多模块项目
if [[ -f "${target_dir}/settings.gradle" || -f "${target_dir}/settings.gradle.kts" ]]; then
local settings_file="${target_dir}/settings.gradle"
if [[ ! -f "$settings_file" ]]; then
settings_file="${target_dir}/settings.gradle.kts"
fi
local root_name=$(grep -oP "rootProject\.name\s*=\s*['\"]\\K[^'\"]+" "$settings_file" 2>/dev/null)
if [[ -n "$root_name" ]]; then
echo "java|${root_name}"
return 0
fi
echo "java|$(get_basename "$target_dir")"
return 0
fi

# 4.3 Gradle 单模块(仅 build.gradle 无 settings)
if [[ -f "${target_dir}/build.gradle" || -f "${target_dir}/build.gradle.kts" ]]; then
echo "java|$(get_basename "$target_dir")"
return 0
fi

# 4.4 Maven/Gradle monorepo fallback:根目录无构建文件但子目录有
local java_build_file=$(find "${target_dir}" -maxdepth 2 -type f \( -name "pom.xml" -o -name "build.gradle" -o -name "build.gradle.kts" \) | head -1)
if [[ -n "$java_build_file" ]]; then
echo "java|$(get_basename "$target_dir")"
return 0
fi

# 5. 未检测到目标语言
echo "unknown|$(get_basename "$target_dir")"
return 1
}
Expand Down Expand Up @@ -155,7 +195,7 @@ if [[ "$project_lang" != "unknown" ]]; then

jq -n --arg code "$exit_code" --arg err "$error_msg" --arg lang "$project_lang" --arg repo "$project_identifier" '{
"decision": "block",
"reason": ("abcoder parse 失败(语言:" + $lang + ",仓库:" + $repo + ",退出码: " + $code + ")。错误信息:\n" + $err + "\n\n可能的原因:\n1. 项目配置文件有问题(Go: go.mod;TS: tsconfig.json)\n2. 缺少依赖包\n3. 代码语法错误\n\n建议:\n- Go 项目:运行 'go mod tidy' 和 'go build' 检查\n- TS 项目:运行 'npm install' 和 'tsc --noEmit' 检查"),
"reason": ("abcoder parse 失败(语言:" + $lang + ",仓库:" + $repo + ",退出码: " + $code + ")。错误信息:\n" + $err + "\n\n可能的原因:\n1. 项目配置文件有问题(Go: go.mod;TS: tsconfig.json;Java: pom.xml/build.gradle)\n2. 缺少依赖包\n3. 代码语法错误\n\n建议:\n- Go 项目:运行 'go mod tidy' 和 'go build' 检查\n- TS 项目:运行 'npm install' 和 'tsc --noEmit' 检查\n- Java 项目:运行 'mvn compile' 或 'gradle build' 检查"),
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"additionalContext": "解析失败,需要修复后重试"
Expand All @@ -169,7 +209,7 @@ else
# 当前目录不是支持的项目,返回空对象
jq -n '{
"decision": "block",
"reason": "当前目录未检测到支持的语言(仅支持 GoTypeScript),请确保项目是 Go 或 TypeScript 类型"
"reason": "当前目录未检测到支持的语言(支持 GoTypeScript、Java),请确保项目包含对应的构建文件(go.mod / package.json / tsconfig.json / pom.xml / build.gradle)"
}'
fi

Expand Down
21 changes: 17 additions & 4 deletions lang/collect/collect.go
Original file line number Diff line number Diff line change
Expand Up @@ -474,7 +474,11 @@ func (c *Collector) ScannerByJavaIPC(ctx context.Context) ([]*DocumentSymbol, er
// Module paths (same as ScannerByTreeSitter)
modulePaths := []string{c.repo}
rootPomPath := filepath.Join(c.repo, "pom.xml")
if rootModule, err := parser.ParseMavenProject(rootPomPath); err == nil && rootModule != nil {
rootModule, pomErr := parser.ParseMavenProject(rootPomPath)
if pomErr != nil {
rootModule, pomErr = parser.ParseGradleProject(c.repo)
}
if pomErr == nil && rootModule != nil {
modulePaths = parser.GetModulePaths(rootModule)
if len(modulePaths) == 0 {
modulePaths = []string{c.repo}
Expand Down Expand Up @@ -883,11 +887,16 @@ func (c *Collector) ScannerByJavaIPC(ctx context.Context) ([]*DocumentSymbol, er
}
}
// Fallback: any remaining local file not under modulePaths
for fp, cls := range fileToClasses {
if visitedFile[fp] {
continue
var remainingFiles []string
for fp := range fileToClasses {
if !visitedFile[fp] {
remainingFiles = append(remainingFiles, fp)
}
}
sort.Strings(remainingFiles)
for _, fp := range remainingFiles {
visitedFile[fp] = true
cls := fileToClasses[fp]
if len(cls) == 0 {
continue
}
Expand Down Expand Up @@ -999,6 +1008,10 @@ func (c *Collector) ScannerByTreeSitter(ctx context.Context) ([]*DocumentSymbol,
if c.Language == uniast.Java {
rootPomPath := filepath.Join(c.repo, "pom.xml")
rootModule, err := parser.ParseMavenProject(rootPomPath)
if err != nil {
// Try Gradle
rootModule, err = parser.ParseGradleProject(c.repo)
}
if err != nil {
// 尝试直接遍历文件
modulePaths = append(modulePaths, c.repo)
Expand Down
10 changes: 10 additions & 0 deletions lang/collect/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"fmt"
"os"
"path/filepath"
"sort"
"strings"

"github.com/cloudwego/abcoder/lang/log"
Expand Down Expand Up @@ -296,6 +297,15 @@ func (c *Collector) exportSymbol(repo *uniast.Repository, symbol *DocumentSymbol
receivers[rec.Method.Receiver.Symbol] = append(receivers[rec.Method.Receiver.Symbol], method)
}
}
// Sort methods within each receiver by source location for deterministic output
for _, methods := range receivers {
sort.Slice(methods, func(i, j int) bool {
if methods[i].Location.Range.Start.Line != methods[j].Location.Range.Start.Line {
return methods[i].Location.Range.Start.Line < methods[j].Location.Range.Start.Line
}
return methods[i].Location.Range.Start.Character < methods[j].Location.Range.Start.Character
})
}

switch k := symbol.Kind; k {
// Function
Expand Down
14 changes: 11 additions & 3 deletions lang/java/ipc/converter.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,9 +214,17 @@ func extractPackageFromPath(filePath string) string {
// Remove file extension and convert path separators to dots
dir := filepath.Dir(filePath)

// Try to find src/main/java or src/ prefix
if idx := strings.Index(dir, "src/main/java/"); idx != -1 {
return strings.ReplaceAll(dir[idx+len("src/main/java/"):], "/", ".")
// Try standard Maven/Gradle source directory prefixes
prefixes := []string{
"src/main/java/",
"src/test/java/",
"src/main/kotlin/",
"src/test/kotlin/",
}
for _, prefix := range prefixes {
if idx := strings.Index(dir, prefix); idx != -1 {
return strings.ReplaceAll(dir[idx+len(prefix):], "/", ".")
}
}
if idx := strings.Index(dir, "src/"); idx != -1 {
return strings.ReplaceAll(dir[idx+len("src/"):], "/", ".")
Expand Down
211 changes: 211 additions & 0 deletions lang/java/parser/gradle_parser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
// Copyright 2025 CloudWeGo Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package parser

import (
"fmt"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
)

var (
rootProjectNameRegex = regexp.MustCompile(`rootProject\.name\s*=\s*['"]([^'"]+)['"]`)
includeRegex = regexp.MustCompile(`include\s*\(?([^)\n]+)\)?`)
includeItemRegex = regexp.MustCompile(`['"]([^'"]+)['"]`)
groupRegex = regexp.MustCompile(`(?m)^\s*group\s*=\s*['"]([^'"]+)['"]`)
versionRegex = regexp.MustCompile(`(?m)^\s*version\s*=\s*['"]([^'"]+)['"]`)
gradlePropRegex = regexp.MustCompile(`(?m)^(\w[\w.]*)\s*=\s*(.+)$`)
)

// ParseGradleProject parses a Gradle project from the given root directory.
// It reads settings.gradle(.kts) for subproject includes, and build.gradle(.kts) for
// group/version information. Returns a ModuleInfo tree compatible with the Maven parser output.
func ParseGradleProject(rootDir string) (*ModuleInfo, error) {
// Find settings file
settingsContent, err := readGradleFile(rootDir, "settings.gradle", "settings.gradle.kts")
if err != nil {
// No settings file — try single-project build
_, buildErr := readGradleFile(rootDir, "build.gradle", "build.gradle.kts")
if buildErr != nil {
return nil, fmt.Errorf("no Gradle build files found in %s", rootDir)
}
return parseSingleGradleProject(rootDir)
}

// Read optional gradle.properties for property substitution
properties := readGradleProperties(rootDir)

// Extract root project name
rootName := filepath.Base(rootDir)
if m := rootProjectNameRegex.FindStringSubmatch(settingsContent); len(m) > 1 {
rootName = m[1]
}

// Parse root build.gradle for group/version
group, version := "com.example", "1.0.0"
if buildContent, err := readGradleFile(rootDir, "build.gradle", "build.gradle.kts"); err == nil {
g, v := extractGroupVersion(buildContent, properties)
if g != "" {
group = g
}
if v != "" {
version = v
}
}

rootModule := &ModuleInfo{
ArtifactID: rootName,
GroupID: group,
Version: version,
Coordinates: fmt.Sprintf("%s:%s:%s", group, rootName, version),
Path: rootDir,
SourcePath: filepath.Join(rootDir, "src", "main", "java"),
TestSourcePath: filepath.Join(rootDir, "src", "test", "java"),
TargetPath: filepath.Join(rootDir, "build"),
SubModules: []*ModuleInfo{},
Properties: properties,
}

// Extract included subprojects
subprojects := extractSubprojects(settingsContent)
sort.Strings(subprojects)

for _, sub := range subprojects {
// Gradle uses ":" as separator, e.g. ":app" or ":core:utils"
subDir := strings.ReplaceAll(strings.TrimPrefix(sub, ":"), ":", string(filepath.Separator))
subPath := filepath.Join(rootDir, subDir)

subGroup := group
subVersion := version
if buildContent, err := readGradleFile(subPath, "build.gradle", "build.gradle.kts"); err == nil {
g, v := extractGroupVersion(buildContent, properties)
if g != "" {
subGroup = g
}
if v != "" {
subVersion = v
}
}

artifactID := filepath.Base(subDir)
subModule := &ModuleInfo{
ArtifactID: artifactID,
GroupID: subGroup,
Version: subVersion,
Coordinates: fmt.Sprintf("%s:%s:%s", subGroup, artifactID, subVersion),
Path: subPath,
SourcePath: filepath.Join(subPath, "src", "main", "java"),
TestSourcePath: filepath.Join(subPath, "src", "test", "java"),
TargetPath: filepath.Join(subPath, "build"),
SubModules: []*ModuleInfo{},
Properties: properties,
}
rootModule.SubModules = append(rootModule.SubModules, subModule)
}

return rootModule, nil
}

func parseSingleGradleProject(rootDir string) (*ModuleInfo, error) {
properties := readGradleProperties(rootDir)
group, version := "com.example", "1.0.0"
rootName := filepath.Base(rootDir)

if buildContent, err := readGradleFile(rootDir, "build.gradle", "build.gradle.kts"); err == nil {
g, v := extractGroupVersion(buildContent, properties)
if g != "" {
group = g
}
if v != "" {
version = v
}
}

return &ModuleInfo{
ArtifactID: rootName,
GroupID: group,
Version: version,
Coordinates: fmt.Sprintf("%s:%s:%s", group, rootName, version),
Path: rootDir,
SourcePath: filepath.Join(rootDir, "src", "main", "java"),
TestSourcePath: filepath.Join(rootDir, "src", "test", "java"),
TargetPath: filepath.Join(rootDir, "build"),
SubModules: []*ModuleInfo{},
Properties: properties,
}, nil
}

func readGradleFile(dir string, names ...string) (string, error) {
for _, name := range names {
p := filepath.Join(dir, name)
data, err := os.ReadFile(p)
if err == nil {
return string(data), nil
}
}
return "", fmt.Errorf("no gradle file found in %s", dir)
}

func readGradleProperties(dir string) map[string]string {
props := make(map[string]string)
data, err := os.ReadFile(filepath.Join(dir, "gradle.properties"))
if err != nil {
return props
}
for _, m := range gradlePropRegex.FindAllStringSubmatch(string(data), -1) {
props[m[1]] = strings.TrimSpace(m[2])
}
return props
}

func extractGroupVersion(content string, props map[string]string) (group, version string) {
if m := groupRegex.FindStringSubmatch(content); len(m) > 1 {
group = resolveGradleProperty(m[1], props)
}
if m := versionRegex.FindStringSubmatch(content); len(m) > 1 {
version = resolveGradleProperty(m[1], props)
}
return
}

func resolveGradleProperty(value string, props map[string]string) string {
// Resolve ${property} references
return propRegex.ReplaceAllStringFunc(value, func(match string) string {
key := match[2 : len(match)-1]
if val, ok := props[key]; ok {
return val
}
return match
})
}

func extractSubprojects(settingsContent string) []string {
var subs []string
seen := make(map[string]bool)
for _, m := range includeRegex.FindAllStringSubmatch(settingsContent, -1) {
items := includeItemRegex.FindAllStringSubmatch(m[1], -1)
for _, item := range items {
name := item[1]
if !seen[name] {
seen[name] = true
subs = append(subs, name)
}
}
}
return subs
}
Loading