| | | 1 | | package repometa |
| | | 2 | | |
| | | 3 | | import ( |
| | | 4 | | "encoding/xml" |
| | | 5 | | "path/filepath" |
| | | 6 | | "regexp" |
| | | 7 | | "strings" |
| | | 8 | | ) |
| | | 9 | | |
| | | 10 | | type javaDetector struct{} |
| | | 11 | | |
| | | 12 | | // mavenPom captures the two shapes we care about from a pom.xml: the |
| | | 13 | | // packaging discriminator (unused for now, kept to record its position) |
| | | 14 | | // and the <modules> block that turns a POM into a multi-module root. |
| | | 15 | | type mavenPom struct { |
| | | 16 | | XMLName xml.Name `xml:"project"` |
| | | 17 | | Modules struct { |
| | | 18 | | Module []string `xml:"module"` |
| | | 19 | | } `xml:"modules"` |
| | | 20 | | } |
| | | 21 | | |
| | | 22 | | // gradleIncludeRE matches Gradle settings-file `include` statements in |
| | | 23 | | // both Groovy and Kotlin DSLs — parentheses are optional in Groovy, |
| | | 24 | | // mandatory in Kotlin. Only the first argument is captured; multi-arg |
| | | 25 | | // `include('a', 'b')` calls are handled by matching each quoted literal |
| | | 26 | | // independently via gradleQuotedLiteralRE. |
| | | 27 | | var gradleIncludeRE = regexp.MustCompile(`(?m)^\s*include\b`) |
| | | 28 | | |
| | | 29 | | // gradleQuotedLiteralRE extracts every single- or double-quoted string |
| | | 30 | | // from a Gradle settings-file line. Combined with gradleIncludeRE we |
| | | 31 | | // capture every module argument regardless of arity. |
| | | 32 | | var gradleQuotedLiteralRE = regexp.MustCompile(`['"]([^'"]+)['"]`) |
| | | 33 | | |
| | 84 | 34 | | func (javaDetector) detect(dv dirVisit, cfg options) []finding { |
| | 84 | 35 | | var out []finding |
| | 84 | 36 | | |
| | 5 | 37 | | if hasFile(dv.files, "pom.xml") { |
| | 5 | 38 | | out = append(out, detectMaven(dv, cfg)) |
| | 5 | 39 | | } |
| | | 40 | | |
| | 9 | 41 | | if hasGradleMarker(dv) { |
| | 9 | 42 | | out = append(out, detectGradle(dv, cfg)) |
| | 9 | 43 | | } |
| | | 44 | | |
| | | 45 | | // Ant. build.xml with a <project> root is the canonical Ant marker; |
| | | 46 | | // Ivy adjuncts (ivy.xml) count only as supporting evidence. |
| | 2 | 47 | | if hasFile(dv.files, "build.xml") { |
| | 2 | 48 | | if f := detectAnt(dv); f != nil { |
| | 2 | 49 | | out = append(out, *f) |
| | 2 | 50 | | } |
| | | 51 | | } |
| | | 52 | | |
| | 84 | 53 | | return out |
| | | 54 | | } |
| | | 55 | | |
| | | 56 | | // detectMaven emits a java-project finding for a directory containing |
| | | 57 | | // pom.xml. When the POM declares <modules>, a maven-multi-module |
| | | 58 | | // workspace is attached with the listed members. |
| | | 59 | | func detectMaven(dv dirVisit, cfg options) finding { |
| | | 60 | | attrs := map[string]string{"java.build": "maven"} |
| | | 61 | | evidence := []Evidence{{ |
| | | 62 | | Path: relJoin(dv.rel, "pom.xml"), |
| | | 63 | | Reason: "pom.xml at directory root", |
| | | 64 | | }} |
| | | 65 | | var workspaces []Workspace |
| | | 66 | | confidence := 1.0 |
| | | 67 | | |
| | | 68 | | data, err := readManifest(filepath.Join(dv.abs, "pom.xml"), cfg) |
| | | 69 | | switch { |
| | | 70 | | case err != nil: |
| | | 71 | | confidence = confidenceUnreadable |
| | | 72 | | evidence = append(evidence, evidenceUnreadable(dv.rel, "pom.xml", err)) |
| | | 73 | | default: |
| | | 74 | | var pom mavenPom |
| | | 75 | | if uerr := xml.Unmarshal(data, &pom); uerr != nil { |
| | | 76 | | confidence = confidenceUnparsable |
| | | 77 | | evidence = append(evidence, evidenceUnparsable(dv.rel, "pom.xml", uerr)) |
| | | 78 | | } else if len(pom.Modules.Module) > 0 { |
| | | 79 | | workspaces = append(workspaces, Workspace{ |
| | | 80 | | Kind: WorkspaceMavenMultiModule, |
| | | 81 | | Members: expandMembers(dv.abs, pom.Modules.Module, dv.rel), |
| | | 82 | | }) |
| | | 83 | | evidence = append(evidence, Evidence{ |
| | | 84 | | Path: relJoin(dv.rel, "pom.xml"), |
| | | 85 | | Reason: "pom.xml declares <modules>", |
| | | 86 | | }) |
| | | 87 | | } |
| | | 88 | | } |
| | | 89 | | return finding{ |
| | | 90 | | Kind: KindJavaProject, |
| | | 91 | | Confidence: confidence, |
| | | 92 | | Evidence: evidence, |
| | | 93 | | Attributes: attrs, |
| | | 94 | | Workspaces: workspaces, |
| | | 95 | | } |
| | | 96 | | } |
| | | 97 | | |
| | | 98 | | // hasGradleMarker reports whether the directory has any Gradle build or |
| | | 99 | | // settings file — either DSL. |
| | | 100 | | func hasGradleMarker(dv dirVisit) bool { |
| | | 101 | | return hasFile(dv.files, "build.gradle") || |
| | | 102 | | hasFile(dv.files, "build.gradle.kts") || |
| | | 103 | | hasFile(dv.files, "settings.gradle") || |
| | | 104 | | hasFile(dv.files, "settings.gradle.kts") |
| | | 105 | | } |
| | | 106 | | |
| | | 107 | | // detectGradle emits a java-project finding for a directory containing |
| | | 108 | | // any Gradle marker. The Kotlin DSL takes precedence over Groovy when |
| | | 109 | | // both are present in a directory (rare but legal during a migration). |
| | | 110 | | // A settings file attaches a gradle-multi-project workspace with members |
| | | 111 | | // parsed from `include` statements. |
| | | 112 | | func detectGradle(dv dirVisit, cfg options) finding { |
| | | 113 | | attrs := map[string]string{"java.build": "gradle"} |
| | | 114 | | var evidence []Evidence |
| | | 115 | | var workspaces []Workspace |
| | | 116 | | |
| | | 117 | | buildFile := "" |
| | | 118 | | switch { |
| | | 119 | | case hasFile(dv.files, "build.gradle.kts"): |
| | | 120 | | buildFile = "build.gradle.kts" |
| | | 121 | | case hasFile(dv.files, "build.gradle"): |
| | | 122 | | buildFile = "build.gradle" |
| | | 123 | | } |
| | | 124 | | if buildFile != "" { |
| | | 125 | | evidence = append(evidence, Evidence{ |
| | | 126 | | Path: relJoin(dv.rel, buildFile), |
| | | 127 | | Reason: buildFile + " at directory root", |
| | | 128 | | }) |
| | | 129 | | if strings.HasSuffix(buildFile, ".kts") { |
| | | 130 | | attrs["java.gradle.dsl"] = "kotlin" |
| | | 131 | | } else { |
| | | 132 | | attrs["java.gradle.dsl"] = "groovy" |
| | | 133 | | } |
| | | 134 | | } |
| | | 135 | | |
| | | 136 | | settingsFile := "" |
| | | 137 | | switch { |
| | | 138 | | case hasFile(dv.files, "settings.gradle.kts"): |
| | | 139 | | settingsFile = "settings.gradle.kts" |
| | | 140 | | case hasFile(dv.files, "settings.gradle"): |
| | | 141 | | settingsFile = "settings.gradle" |
| | | 142 | | } |
| | | 143 | | if settingsFile != "" { |
| | | 144 | | evidence = append(evidence, Evidence{ |
| | | 145 | | Path: relJoin(dv.rel, settingsFile), |
| | | 146 | | Reason: settingsFile + " at directory root", |
| | | 147 | | }) |
| | | 148 | | members := parseGradleIncludes(filepath.Join(dv.abs, settingsFile), cfg) |
| | | 149 | | if len(members) > 0 { |
| | | 150 | | workspaces = append(workspaces, Workspace{ |
| | | 151 | | Kind: WorkspaceGradleMultiProject, |
| | | 152 | | Members: expandMembers(dv.abs, members, dv.rel), |
| | | 153 | | }) |
| | | 154 | | } else { |
| | | 155 | | workspaces = append(workspaces, Workspace{Kind: WorkspaceGradleMultiProject}) |
| | | 156 | | } |
| | | 157 | | } |
| | | 158 | | |
| | | 159 | | return finding{ |
| | | 160 | | Kind: KindJavaProject, |
| | | 161 | | Confidence: 1.0, |
| | | 162 | | Evidence: evidence, |
| | | 163 | | Attributes: attrs, |
| | | 164 | | Workspaces: workspaces, |
| | | 165 | | } |
| | | 166 | | } |
| | | 167 | | |
| | | 168 | | // detectAnt emits a java-project finding for a directory whose build.xml |
| | | 169 | | // looks like an Ant project (root element is <project>). Returns nil if |
| | | 170 | | // the file is unreadable or doesn't have the Ant shape — build.xml is |
| | | 171 | | // used by other tools too and a bare presence check would over-detect. |
| | | 172 | | func detectAnt(dv dirVisit) *finding { |
| | | 173 | | attrs := map[string]string{"java.build": "ant"} |
| | | 174 | | evidence := []Evidence{{ |
| | | 175 | | Path: relJoin(dv.rel, "build.xml"), |
| | | 176 | | Reason: "build.xml at directory root", |
| | | 177 | | }} |
| | | 178 | | |
| | | 179 | | if hasFile(dv.files, "ivy.xml") { |
| | | 180 | | evidence = append(evidence, Evidence{ |
| | | 181 | | Path: relJoin(dv.rel, "ivy.xml"), |
| | | 182 | | Reason: "ivy.xml present (Ivy dependency descriptor)", |
| | | 183 | | }) |
| | | 184 | | } |
| | | 185 | | |
| | | 186 | | return &finding{ |
| | | 187 | | Kind: KindJavaProject, |
| | | 188 | | Confidence: 1.0, |
| | | 189 | | Evidence: evidence, |
| | | 190 | | Attributes: attrs, |
| | | 191 | | } |
| | | 192 | | } |
| | | 193 | | |
| | | 194 | | // parseGradleIncludes returns the module paths declared by `include` |
| | | 195 | | // statements in a Gradle settings file. Gradle uses ":a:b" to denote |
| | | 196 | | // nested paths in the project tree; these are converted to "a/b" so the |
| | | 197 | | // result is directly usable as a filesystem-relative member path. |
| | | 198 | | func parseGradleIncludes(path string, cfg options) []string { |
| | | 199 | | data := readManifestOrNil(path, cfg) |
| | | 200 | | if data == nil { |
| | | 201 | | return nil |
| | | 202 | | } |
| | | 203 | | var members []string |
| | | 204 | | seen := make(map[string]bool) |
| | | 205 | | for line := range strings.SplitSeq(string(data), "\n") { |
| | | 206 | | if !gradleIncludeRE.MatchString(line) { |
| | | 207 | | continue |
| | | 208 | | } |
| | | 209 | | for _, m := range gradleQuotedLiteralRE.FindAllStringSubmatch(line, -1) { |
| | | 210 | | p := strings.ReplaceAll(strings.TrimPrefix(m[1], ":"), ":", "/") |
| | | 211 | | if p == "" || seen[p] { |
| | | 212 | | continue |
| | | 213 | | } |
| | | 214 | | seen[p] = true |
| | | 215 | | members = append(members, p) |
| | | 216 | | } |
| | | 217 | | } |
| | | 218 | | return members |
| | | 219 | | } |