Skip to content

Commit 06bcaf1

Browse files
authored
Merge pull request #5 from EdgarPsda/v0.5.0/python-java-detection
Add Python and Java project detection
2 parents d08f347 + 577fbe5 commit 06bcaf1

5 files changed

Lines changed: 726 additions & 1 deletion

File tree

cli/detectors/detector.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ func DetectProject(dir string) (*ProjectInfo, error) {
3030
detectors := []Detector{
3131
&NodeDetector{},
3232
&GoDetector{},
33-
// Add more detectors here in the future
33+
&PythonDetector{},
34+
&JavaDetector{},
3435
}
3536

3637
var bestMatch *ProjectInfo

cli/detectors/java.go

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
// cli/detectors/java.go
2+
package detectors
3+
4+
import (
5+
"bufio"
6+
"errors"
7+
"os"
8+
"path/filepath"
9+
"strings"
10+
)
11+
12+
type JavaDetector struct {
13+
confidence int
14+
}
15+
16+
func (d *JavaDetector) Detect(dir string) (*ProjectInfo, error) {
17+
// Check for Java project files
18+
packageFiles := []string{
19+
"pom.xml",
20+
"build.gradle",
21+
"build.gradle.kts",
22+
}
23+
24+
var foundFile string
25+
for _, f := range packageFiles {
26+
if fileExists(filepath.Join(dir, f)) {
27+
foundFile = f
28+
break
29+
}
30+
}
31+
32+
if foundFile == "" {
33+
d.confidence = 0
34+
return nil, errors.New("no Java project file found")
35+
}
36+
37+
deps := collectJavaDeps(filepath.Join(dir, foundFile), foundFile)
38+
framework := detectJavaFramework(deps, dir, foundFile)
39+
40+
d.confidence = 95
41+
42+
return &ProjectInfo{
43+
Language: "java",
44+
Framework: framework,
45+
PackageFile: foundFile,
46+
RootDir: dir,
47+
Dependencies: deps,
48+
}, nil
49+
}
50+
51+
func (d *JavaDetector) Confidence() int {
52+
return d.confidence
53+
}
54+
55+
func detectJavaFramework(deps []string, dir, packageFile string) string {
56+
// Check dependencies for known frameworks
57+
for _, dep := range deps {
58+
lower := strings.ToLower(dep)
59+
switch {
60+
case strings.Contains(lower, "spring-boot"):
61+
return "spring-boot"
62+
case strings.Contains(lower, "quarkus"):
63+
return "quarkus"
64+
case strings.Contains(lower, "micronaut"):
65+
return "micronaut"
66+
case strings.Contains(lower, "jakarta.ee") || strings.Contains(lower, "javax.servlet"):
67+
return "jakarta-ee"
68+
case strings.Contains(lower, "dropwizard"):
69+
return "dropwizard"
70+
}
71+
}
72+
73+
// Also check the file content directly for Spring Boot parent POM
74+
if packageFile == "pom.xml" {
75+
data, err := os.ReadFile(filepath.Join(dir, packageFile))
76+
if err == nil {
77+
content := strings.ToLower(string(data))
78+
if strings.Contains(content, "spring-boot-starter-parent") ||
79+
strings.Contains(content, "spring-boot-starter") {
80+
return "spring-boot"
81+
}
82+
}
83+
}
84+
85+
return ""
86+
}
87+
88+
func collectJavaDeps(path, packageFile string) []string {
89+
switch {
90+
case packageFile == "pom.xml":
91+
return parsePomXML(path)
92+
case strings.HasPrefix(packageFile, "build.gradle"):
93+
return parseGradle(path)
94+
}
95+
return nil
96+
}
97+
98+
// parsePomXML does a minimal parse to extract dependency artifact IDs from pom.xml
99+
func parsePomXML(path string) []string {
100+
f, err := os.Open(path)
101+
if err != nil {
102+
return nil
103+
}
104+
defer f.Close()
105+
106+
var deps []string
107+
scanner := bufio.NewScanner(f)
108+
inDependency := false
109+
var currentGroup, currentArtifact string
110+
111+
for scanner.Scan() {
112+
line := strings.TrimSpace(scanner.Text())
113+
114+
if strings.Contains(line, "<dependency>") {
115+
inDependency = true
116+
currentGroup = ""
117+
currentArtifact = ""
118+
continue
119+
}
120+
121+
if strings.Contains(line, "</dependency>") {
122+
if inDependency && (currentGroup != "" || currentArtifact != "") {
123+
dep := currentGroup
124+
if currentArtifact != "" {
125+
if dep != "" {
126+
dep += ":"
127+
}
128+
dep += currentArtifact
129+
}
130+
deps = append(deps, dep)
131+
}
132+
inDependency = false
133+
continue
134+
}
135+
136+
if inDependency {
137+
if groupID := extractXMLValue(line, "groupId"); groupID != "" {
138+
currentGroup = groupID
139+
}
140+
if artifactID := extractXMLValue(line, "artifactId"); artifactID != "" {
141+
currentArtifact = artifactID
142+
}
143+
}
144+
}
145+
146+
return deps
147+
}
148+
149+
// parseGradle does a minimal parse to extract dependencies from build.gradle
150+
func parseGradle(path string) []string {
151+
f, err := os.Open(path)
152+
if err != nil {
153+
return nil
154+
}
155+
defer f.Close()
156+
157+
var deps []string
158+
scanner := bufio.NewScanner(f)
159+
160+
for scanner.Scan() {
161+
line := strings.TrimSpace(scanner.Text())
162+
163+
// Match patterns like: implementation 'group:artifact:version'
164+
// or: implementation "group:artifact:version"
165+
for _, keyword := range []string{"implementation", "api", "compileOnly", "runtimeOnly", "testImplementation"} {
166+
if strings.HasPrefix(line, keyword+" ") || strings.HasPrefix(line, keyword+"(") {
167+
dep := extractGradleDep(line)
168+
if dep != "" {
169+
deps = append(deps, dep)
170+
}
171+
}
172+
}
173+
}
174+
175+
return deps
176+
}
177+
178+
// extractXMLValue extracts the text content of a simple XML element
179+
func extractXMLValue(line, tag string) string {
180+
openTag := "<" + tag + ">"
181+
closeTag := "</" + tag + ">"
182+
183+
startIdx := strings.Index(line, openTag)
184+
endIdx := strings.Index(line, closeTag)
185+
186+
if startIdx >= 0 && endIdx > startIdx {
187+
return strings.TrimSpace(line[startIdx+len(openTag) : endIdx])
188+
}
189+
return ""
190+
}
191+
192+
// extractGradleDep extracts the dependency coordinate from a Gradle dependency line
193+
func extractGradleDep(line string) string {
194+
// Find the quoted string (single or double)
195+
for _, quote := range []string{"'", "\""} {
196+
start := strings.Index(line, quote)
197+
if start >= 0 {
198+
end := strings.Index(line[start+1:], quote)
199+
if end >= 0 {
200+
return line[start+1 : start+1+end]
201+
}
202+
}
203+
}
204+
return ""
205+
}

cli/detectors/java_test.go

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
package detectors
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"testing"
7+
)
8+
9+
func TestJavaDetector_Detect_SpringBoot_Maven(t *testing.T) {
10+
dir := t.TempDir()
11+
12+
pomXML := `<?xml version="1.0" encoding="UTF-8"?>
13+
<project>
14+
<parent>
15+
<groupId>org.springframework.boot</groupId>
16+
<artifactId>spring-boot-starter-parent</artifactId>
17+
<version>3.2.0</version>
18+
</parent>
19+
<dependencies>
20+
<dependency>
21+
<groupId>org.springframework.boot</groupId>
22+
<artifactId>spring-boot-starter-web</artifactId>
23+
</dependency>
24+
<dependency>
25+
<groupId>org.springframework.boot</groupId>
26+
<artifactId>spring-boot-starter-data-jpa</artifactId>
27+
</dependency>
28+
<dependency>
29+
<groupId>org.postgresql</groupId>
30+
<artifactId>postgresql</artifactId>
31+
</dependency>
32+
</dependencies>
33+
</project>`
34+
35+
if err := os.WriteFile(filepath.Join(dir, "pom.xml"), []byte(pomXML), 0o644); err != nil {
36+
t.Fatalf("failed to write pom.xml: %v", err)
37+
}
38+
39+
d := &JavaDetector{}
40+
info, err := d.Detect(dir)
41+
if err != nil {
42+
t.Fatalf("Detect returned error: %v", err)
43+
}
44+
45+
if info.Language != "java" {
46+
t.Errorf("expected Language=java, got %s", info.Language)
47+
}
48+
if info.Framework != "spring-boot" {
49+
t.Errorf("expected Framework=spring-boot, got %s", info.Framework)
50+
}
51+
if info.PackageFile != "pom.xml" {
52+
t.Errorf("expected PackageFile=pom.xml, got %s", info.PackageFile)
53+
}
54+
if d.Confidence() <= 0 {
55+
t.Errorf("expected confidence > 0, got %d", d.Confidence())
56+
}
57+
if len(info.Dependencies) != 3 {
58+
t.Errorf("expected 3 dependencies, got %d", len(info.Dependencies))
59+
}
60+
}
61+
62+
func TestJavaDetector_Detect_Gradle(t *testing.T) {
63+
dir := t.TempDir()
64+
65+
buildGradle := `plugins {
66+
id 'java'
67+
id 'org.springframework.boot' version '3.2.0'
68+
}
69+
70+
dependencies {
71+
implementation 'org.springframework.boot:spring-boot-starter-web:3.2.0'
72+
implementation 'org.springframework.boot:spring-boot-starter-data-jpa:3.2.0'
73+
testImplementation 'org.springframework.boot:spring-boot-starter-test:3.2.0'
74+
runtimeOnly 'org.postgresql:postgresql:42.7.0'
75+
}
76+
`
77+
78+
if err := os.WriteFile(filepath.Join(dir, "build.gradle"), []byte(buildGradle), 0o644); err != nil {
79+
t.Fatalf("failed to write build.gradle: %v", err)
80+
}
81+
82+
d := &JavaDetector{}
83+
info, err := d.Detect(dir)
84+
if err != nil {
85+
t.Fatalf("Detect returned error: %v", err)
86+
}
87+
88+
if info.Language != "java" {
89+
t.Errorf("expected Language=java, got %s", info.Language)
90+
}
91+
if info.Framework != "spring-boot" {
92+
t.Errorf("expected Framework=spring-boot, got %s", info.Framework)
93+
}
94+
if info.PackageFile != "build.gradle" {
95+
t.Errorf("expected PackageFile=build.gradle, got %s", info.PackageFile)
96+
}
97+
if len(info.Dependencies) != 4 {
98+
t.Errorf("expected 4 dependencies, got %d", len(info.Dependencies))
99+
}
100+
}
101+
102+
func TestJavaDetector_Detect_Quarkus(t *testing.T) {
103+
dir := t.TempDir()
104+
105+
pomXML := `<?xml version="1.0" encoding="UTF-8"?>
106+
<project>
107+
<dependencies>
108+
<dependency>
109+
<groupId>io.quarkus</groupId>
110+
<artifactId>quarkus-resteasy</artifactId>
111+
</dependency>
112+
<dependency>
113+
<groupId>io.quarkus</groupId>
114+
<artifactId>quarkus-hibernate-orm</artifactId>
115+
</dependency>
116+
</dependencies>
117+
</project>`
118+
119+
if err := os.WriteFile(filepath.Join(dir, "pom.xml"), []byte(pomXML), 0o644); err != nil {
120+
t.Fatalf("failed to write pom.xml: %v", err)
121+
}
122+
123+
d := &JavaDetector{}
124+
info, err := d.Detect(dir)
125+
if err != nil {
126+
t.Fatalf("Detect returned error: %v", err)
127+
}
128+
129+
if info.Framework != "quarkus" {
130+
t.Errorf("expected Framework=quarkus, got %s", info.Framework)
131+
}
132+
}
133+
134+
func TestJavaDetector_Detect_NoProject(t *testing.T) {
135+
dir := t.TempDir()
136+
137+
d := &JavaDetector{}
138+
_, err := d.Detect(dir)
139+
if err == nil {
140+
t.Error("expected error for empty directory, got nil")
141+
}
142+
if d.Confidence() != 0 {
143+
t.Errorf("expected confidence=0, got %d", d.Confidence())
144+
}
145+
}

0 commit comments

Comments
 (0)