Skip to content

Commit 421b4f1

Browse files
a-orenclaude
andauthored
fix(go): handle replace directives in go list -m all parsing (guacsec#468)
## Summary - Fix `go list -m all` parsing in `GoModulesProvider.getFinalPackagesVersionsForModule()` to handle Go replace directives (5-part format: `name v1 => replacement v2`) - Extract `parseModuleVersions()` method mirroring JS client PR guacsec#505 - Add replace directive test fixture to `go_mod_light_no_ignore` and update expected SBOM Implements [TC-4359](https://redhat.atlassian.net/browse/TC-4359) ## Test plan - [x] All 18 Go module tests pass (all 6 parameterized folders, both stack and component analysis) - [x] `go_mod_no_path` fixture with no-op replace still passes - [x] MVS-related tests pass (`Test_Golang_MvS_Logic_Disabled`, `Test_Golang_MvS_Enabled_Preserves_All_Transitive_Dependencies`) - [x] `mvn spotless:apply` passes (code formatted) - [x] `mvn verify` passes (only pre-existing Python env failures) 🤖 Generated with [Claude Code](https://claude.com/claude-code) [TC-4359]: https://redhat.atlassian.net/browse/TC-4359?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ ## Summary by Sourcery Handle Go module replace directives when resolving final package versions from `go list -m all` output. Bug Fixes: - Correct resolution of final Go module versions by supporting replace directive lines in `go list -m all` output. Enhancements: - Extract shared `parseModuleVersions` helper to parse `go list -m all` output into a module-to-version map. Tests: - Extend Go module test fixtures with a replace directive example and update the expected SBOM for stack analysis to cover the new behavior. --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent bbeb043 commit 421b4f1

4 files changed

Lines changed: 112 additions & 10 deletions

File tree

src/main/java/io/github/guacsec/trustifyda/providers/GoModulesProvider.java

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -347,12 +347,7 @@ private Map<String, List<String>> getFinalPackagesVersionsForModule(
347347
Operations.runProcessGetOutput(manifestPath.getParent(), goExecutable, "mod", "download");
348348
String finalVersionsForAllModules =
349349
Operations.runProcessGetOutput(manifestPath.getParent(), goExecutable, "list", "-m", "all");
350-
Map<String, String> finalModulesVersions =
351-
Arrays.stream(finalVersionsForAllModules.split(Operations.GENERIC_LINE_SEPARATOR))
352-
.filter(string -> string.trim().split(" ").length == 2)
353-
.collect(
354-
Collectors.toMap(
355-
t -> t.split(" ")[0], t -> t.split(" ")[1], (first, second) -> second));
350+
Map<String, String> finalModulesVersions = parseModuleVersions(finalVersionsForAllModules);
356351
Map<String, List<String>> listWithModifiedVersions = new HashMap<>();
357352
// Process all entries, including those without versions (like the root module)
358353
edges.forEach(
@@ -387,6 +382,25 @@ private Map<String, List<String>> getFinalPackagesVersionsForModule(
387382
return listWithModifiedVersions;
388383
}
389384

385+
/**
386+
* Parses {@code go list -m all} output into a map of module names to their final resolved
387+
* versions. Handles both standard lines ({@code name version}) and replace directive lines
388+
* ({@code name v1 => replacement v2}).
389+
*/
390+
static Map<String, String> parseModuleVersions(String goListOutput) {
391+
return Arrays.stream(goListOutput.split(Operations.GENERIC_LINE_SEPARATOR))
392+
.map(String::trim)
393+
.filter(line -> !line.isEmpty())
394+
.map(line -> line.split("\\s+"))
395+
.filter(parts -> parts.length == 2 || (parts.length >= 4 && parts[2].equals("=>")))
396+
.collect(
397+
Collectors.toMap(
398+
parts -> parts[0],
399+
parts ->
400+
parts.length >= 4 && parts[2].equals("=>") ? parts[parts.length - 1] : parts[1],
401+
(first, second) -> second));
402+
}
403+
390404
private List<String> getListOfPackagesWithFinalVersions(
391405
Map<String, String> finalModulesVersions, List<String> packages) {
392406
return packages.stream()
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
* Copyright 2023-2025 Trustify Dependency Analytics Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
*
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package io.github.guacsec.trustifyda.providers;
18+
19+
import static org.assertj.core.api.Assertions.assertThat;
20+
21+
import java.util.Map;
22+
import org.junit.jupiter.api.Test;
23+
24+
class GoModulesParseModuleVersionsTest {
25+
26+
@Test
27+
void parseStandardModuleLines() {
28+
String input = "github.com/foo/bar v1.2.3\ngithub.com/baz/qux v0.1.0\n";
29+
Map<String, String> result = GoModulesProvider.parseModuleVersions(input);
30+
assertThat(result)
31+
.containsEntry("github.com/foo/bar", "v1.2.3")
32+
.containsEntry("github.com/baz/qux", "v0.1.0")
33+
.hasSize(2);
34+
}
35+
36+
@Test
37+
void parseReplaceDirectiveLines() {
38+
String input = "github.com/old/mod v1.0.0 => github.com/new/mod v2.0.0\n";
39+
Map<String, String> result = GoModulesProvider.parseModuleVersions(input);
40+
assertThat(result).containsEntry("github.com/old/mod", "v2.0.0").hasSize(1);
41+
}
42+
43+
@Test
44+
void parseWithMultipleSpacesAndTabs() {
45+
String input = "github.com/foo/bar v1.2.3\ngithub.com/baz/qux\tv0.1.0\n";
46+
Map<String, String> result = GoModulesProvider.parseModuleVersions(input);
47+
assertThat(result)
48+
.containsEntry("github.com/foo/bar", "v1.2.3")
49+
.containsEntry("github.com/baz/qux", "v0.1.0")
50+
.hasSize(2);
51+
}
52+
53+
@Test
54+
void parseWithBlankAndWhitespaceOnlyLines() {
55+
String input = "\n \ngithub.com/foo/bar v1.0.0\n\n";
56+
Map<String, String> result = GoModulesProvider.parseModuleVersions(input);
57+
assertThat(result).containsEntry("github.com/foo/bar", "v1.0.0").hasSize(1);
58+
}
59+
60+
@Test
61+
void parseSkipsMalformedLines() {
62+
String input = "github.com/foo/bar v1.0.0\nsingle-token\nthree tokens here\n";
63+
Map<String, String> result = GoModulesProvider.parseModuleVersions(input);
64+
assertThat(result).containsEntry("github.com/foo/bar", "v1.0.0").hasSize(1);
65+
}
66+
67+
@Test
68+
void parseEmptyInput() {
69+
Map<String, String> result = GoModulesProvider.parseModuleVersions("");
70+
assertThat(result).isEmpty();
71+
}
72+
73+
@Test
74+
void parseReplaceDirectiveWithTabSeparation() {
75+
String input = "github.com/old/mod\tv1.0.0\t=>\tgithub.com/new/mod\tv2.0.0\n";
76+
Map<String, String> result = GoModulesProvider.parseModuleVersions(input);
77+
assertThat(result).containsEntry("github.com/old/mod", "v2.0.0").hasSize(1);
78+
}
79+
80+
@Test
81+
void parseDuplicateModuleKeepsLast() {
82+
String input = "github.com/foo/bar v1.0.0\ngithub.com/foo/bar v2.0.0\n";
83+
Map<String, String> result = GoModulesProvider.parseModuleVersions(input);
84+
assertThat(result).containsEntry("github.com/foo/bar", "v2.0.0").hasSize(1);
85+
}
86+
}

src/test/resources/tst_manifests/golang/go_mod_light_no_ignore/expected_sbom_stack_analysis.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -263,11 +263,11 @@
263263
},
264264
{
265265
"type": "library",
266-
"bom-ref": "pkg:golang/gopkg.in/yaml.v3@v3.0.1",
266+
"bom-ref": "pkg:golang/gopkg.in/yaml.v3@v3.0.0",
267267
"group": "gopkg.in",
268268
"name": "yaml.v3",
269-
"version": "v3.0.1",
270-
"purl": "pkg:golang/gopkg.in/yaml.v3@v3.0.1"
269+
"version": "v3.0.0",
270+
"purl": "pkg:golang/gopkg.in/yaml.v3@v3.0.0"
271271
},
272272
{
273273
"type": "library",
@@ -497,7 +497,7 @@
497497
"dependsOn": []
498498
},
499499
{
500-
"ref": "pkg:golang/gopkg.in/yaml.v3@v3.0.1",
500+
"ref": "pkg:golang/gopkg.in/yaml.v3@v3.0.0",
501501
"dependsOn": [
502502
"pkg:golang/gopkg.in/check.v1@v0.0.0-20161208181325-20d25e280405"
503503
]

src/test/resources/tst_manifests/golang/go_mod_light_no_ignore/go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,5 @@ require golang.org/x/tools v0.0.0-20210112183307-1e6ecd4bf1b0
66
require github.com/spf13/cobra v0.0.5
77

88
require gopkg.in/yaml.v3 v3.0.1 // indirect
9+
10+
replace gopkg.in/yaml.v3 v3.0.1 => gopkg.in/yaml.v3 v3.0.0

0 commit comments

Comments
 (0)