Skip to content

Commit 3b45d0b

Browse files
committed
fix(python): extract helper methods for direct ref and path detection, add ~/Windows path support
Address code review feedback: - Extract isDirectReference() and isPathDependency() helper methods for clearer naming and a single place to extend edge case handling - Add ~/home-relative and Windows drive path (C:\) detection - Add test for # via graph corruption after skipped path dependencies Implements TC-4538 Signed-off-by: Adva Oren <aoren@redhat.com> Assisted-by: Claude Code
1 parent 6549a64 commit 3b45d0b

2 files changed

Lines changed: 57 additions & 6 deletions

File tree

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

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -195,14 +195,14 @@ UvDependencyData parseUvExport(String exportOutput) throws IOException {
195195
inViaBlock = false;
196196

197197
// PEP 440 direct references (name @ url) — skip, no pinned version available
198-
if (trimmed.contains(" @ ")) {
198+
if (isDirectReference(trimmed)) {
199199
log.fine("Skipping PEP 440 direct reference: " + trimmed);
200200
currentKey = null;
201201
continue;
202202
}
203203

204-
// Path dependencies (./local, ../local, /absolute) — skip, no pinned version available
205-
if (trimmed.startsWith("./") || trimmed.startsWith("../") || trimmed.startsWith("/")) {
204+
// Path dependencies (./local, ../local, /absolute, ~/home, C:\win) — skip
205+
if (isPathDependency(trimmed)) {
206206
log.fine("Skipping path dependency: " + trimmed);
207207
currentKey = null;
208208
continue;
@@ -299,6 +299,22 @@ private static String parseEditableInstall(
299299
}
300300
}
301301

302+
private static final Pattern WINDOWS_DRIVE_PATH = Pattern.compile("^[a-zA-Z]:[/\\\\]");
303+
304+
/** Returns {@code true} if the line is a PEP 440 direct reference ({@code name @ url}). */
305+
static boolean isDirectReference(String trimmedLine) {
306+
return trimmedLine.contains(" @ ");
307+
}
308+
309+
/** Returns {@code true} if the line is a local or absolute path dependency. */
310+
static boolean isPathDependency(String trimmedLine) {
311+
return trimmedLine.startsWith("./")
312+
|| trimmedLine.startsWith("../")
313+
|| trimmedLine.startsWith("/")
314+
|| trimmedLine.startsWith("~/")
315+
|| WINDOWS_DRIVE_PATH.matcher(trimmedLine).find();
316+
}
317+
302318
private static final Pattern BARE_PACKAGE_NAME = Pattern.compile("[A-Za-z0-9][A-Za-z0-9._-]*");
303319

304320
private static void recordViaParent(

src/test/java/io/github/guacsec/trustifyda/providers/Python_Uv_Provider_Test.java

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -430,24 +430,59 @@ void test_parseUvExport_skips_path_dependencies() throws IOException {
430430
+ " # via test-project\n"
431431
+ "/absolute/path/package\n"
432432
+ " # via test-project\n"
433+
+ "~/home-relative/package\n"
434+
+ " # via test-project\n"
435+
+ "C:\\Users\\dev\\my-package\n"
436+
+ " # via test-project\n"
433437
+ "anyio==3.6.2\n"
434438
+ " # via test-project\n";
435439

436440
// Given/When
437441
var data = provider.parseUvExport(exportOutput);
438442

439-
// Then — path dependencies are skipped
443+
// Then — all path dependency forms are skipped
440444
assertThat(data.graph()).doesNotContainKey("./local-package");
441445
assertThat(data.graph()).doesNotContainKey("../sibling-package");
442446
assertThat(data.graph()).doesNotContainKey("/absolute/path/package");
447+
assertThat(data.graph()).doesNotContainKey("~/home-relative/package");
448+
assertThat(data.graph()).doesNotContainKey("c:\\users\\dev\\my-package");
443449
// anyio is still parsed correctly
444450
assertThat(data.graph()).containsKey("anyio");
445451
assertThat(data.directDeps()).contains("anyio");
446452
}
447453

448454
/**
449-
* Verifies that # via comments after skipped packages do not create incorrect parent-child
450-
* relationships with the previous package.
455+
* Verifies that # via comments after a skipped path dependency do not corrupt the graph by
456+
* attaching to the previous package.
457+
*/
458+
@Test
459+
void test_parseUvExport_via_after_skipped_path_dep_does_not_corrupt_graph() throws IOException {
460+
Path pyprojectPath = Path.of(UV_FIXTURE, "pyproject.toml");
461+
var provider = new PythonUvProvider(pyprojectPath);
462+
463+
// Given — anyio is parsed first, then a path dependency is skipped. The "# via requests"
464+
// after the skipped path dep should NOT make anyio a child of requests.
465+
String exportOutput =
466+
"# This file was autogenerated by uv\n"
467+
+ "anyio==3.6.2\n"
468+
+ " # via test-project\n"
469+
+ "./local-package\n"
470+
+ " # via requests\n"
471+
+ "requests==2.25.1\n"
472+
+ " # via test-project\n";
473+
474+
// When
475+
var data = provider.parseUvExport(exportOutput);
476+
477+
// Then — requests should NOT have anyio as a child (that would be a corruption)
478+
assertThat(data.graph().get("requests").children()).doesNotContain("anyio");
479+
// Both anyio and requests are direct deps
480+
assertThat(data.directDeps()).containsExactlyInAnyOrder("anyio", "requests");
481+
}
482+
483+
/**
484+
* Verifies that # via comments after skipped direct references do not create incorrect
485+
* parent-child relationships with the previous package.
451486
*/
452487
@Test
453488
void test_parseUvExport_via_after_skipped_does_not_corrupt_graph() throws IOException {

0 commit comments

Comments
 (0)