-
Notifications
You must be signed in to change notification settings - Fork 62
fix(stats): handle windows file project roots #453
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| package scip | ||
|
|
||
| import "net/url" | ||
|
|
||
| // ProjectRootToLocalPath converts an Index.Metadata.ProjectRoot value into a | ||
| // local filesystem path. | ||
| // | ||
| // It handles the Windows form "file://X:\..." (where X is a drive letter) | ||
| // which net/url.Parse rejects with `invalid port ":\..." after host`, because | ||
| // it parses "X:" as a host:port pattern. See issue #282. | ||
| // | ||
| // For all other inputs it preserves the previous behavior of using | ||
| // url.Parse(rawProjectRoot).Path. | ||
| func ProjectRootToLocalPath(rawProjectRoot string) (string, error) { | ||
| if isWindowsFileURIWithDriveLetter(rawProjectRoot) { | ||
| // Strip the "file://" prefix; the remainder is the Windows path. | ||
| // ponytail: only the "file://X:..." form is handled here; the | ||
| // canonical "file:///X:/..." form parses fine via url.Parse and is | ||
| // out of scope for this fix. | ||
| return rawProjectRoot[len("file://"):], nil | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should probably run through |
||
| } | ||
| u, err := url.Parse(rawProjectRoot) | ||
| if err != nil { | ||
| return "", err | ||
| } | ||
| return u.Path, nil | ||
| } | ||
|
|
||
| // isWindowsFileURIWithDriveLetter reports whether s matches "file://X:..." | ||
| // where X is an ASCII letter and the position after the scheme is NOT a third | ||
| // slash (which would indicate the canonical "file:///..." form). | ||
| func isWindowsFileURIWithDriveLetter(s string) bool { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This predicate seems too broad for the stated fix. It accepts any |
||
| const prefix = "file://" | ||
| if len(s) < len(prefix)+2 { | ||
| return false | ||
| } | ||
| if s[:len(prefix)] != prefix { | ||
| return false | ||
| } | ||
| c := s[len(prefix)] | ||
| if !(c >= 'a' && c <= 'z') && !(c >= 'A' && c <= 'Z') { | ||
| return false | ||
| } | ||
| return s[len(prefix)+1] == ':' | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| package scip | ||
|
|
||
| import "testing" | ||
|
|
||
| func TestProjectRootToLocalPath(t *testing.T) { | ||
| cases := []struct { | ||
| name string | ||
| in string | ||
| want string | ||
| }{ | ||
| // Regression for https://github.com/scip-code/scip/issues/282 — | ||
| // Windows ProjectRoot of the form "file://X:\..." used to fail | ||
| // url.Parse with "invalid port" because "X:" looked like host:port. | ||
| { | ||
| name: "windows file uri with drive letter and backslashes", | ||
| in: `file://D:\dev\project`, | ||
| want: `D:\dev\project`, | ||
| }, | ||
| // Unix/macOS forms must keep their existing behavior unchanged. | ||
| { | ||
| name: "unix file uri", | ||
| in: "file:///test/project", | ||
| want: "/test/project", | ||
| }, | ||
| { | ||
| name: "unix file uri single slash", | ||
| in: "file:/root", | ||
| want: "/root", | ||
| }, | ||
| } | ||
| for _, tc := range cases { | ||
| t.Run(tc.name, func(t *testing.T) { | ||
| got, err := ProjectRootToLocalPath(tc.in) | ||
| if err != nil { | ||
| t.Fatalf("ProjectRootToLocalPath(%q) unexpected error: %v", tc.in, err) | ||
| } | ||
| if got != tc.want { | ||
| t.Fatalf("ProjectRootToLocalPath(%q) = %q, want %q", tc.in, got, tc.want) | ||
| } | ||
| }) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| package main | ||
|
|
||
| import ( | ||
| "testing" | ||
|
|
||
| "github.com/scip-code/scip/bindings/go/scip" | ||
| ) | ||
|
|
||
| // Regression for https://github.com/scip-code/scip/issues/282. | ||
| // | ||
| // Before the fix, countLinesOfCode called url.Parse on Index.Metadata.ProjectRoot | ||
| // directly. A Windows ProjectRoot of the form "file://X:\..." was rejected by | ||
| // url.Parse with `invalid port ":\..." after host` (it parses "X:" as | ||
| // host:port), so `scip stats` errored before reading any document. | ||
| // | ||
| // Exercising countLinesOfCode keeps the externally visible seam covered: | ||
| // scip stats -> statsMain -> countStatistics -> countLinesOfCode. | ||
| func TestCountLinesOfCode_WindowsFileURI(t *testing.T) { | ||
| idx := &scip.Index{ | ||
| Metadata: &scip.Metadata{ProjectRoot: `file://D:\dev\scip-issue-282-does-not-exist`}, | ||
| } | ||
| if _, err := countLinesOfCode(idx, ""); err != nil { | ||
| t.Fatalf("countLinesOfCode returned error for Windows file URI: %v", err) | ||
| } | ||
| } | ||
|
|
||
| // Regression for the localSource bug surfaced in the #282 review: when | ||
| // customProjectRoot is empty and the parsed rootPath exists on disk, | ||
| // localSource was left empty and the subsequent os.Stat("") returned | ||
| // `stat : no such file or directory`. This drives the "rootPath exists" | ||
| // branch on both platforms by using t.TempDir() as the ProjectRoot: | ||
| // - POSIX: "file:///tmp/xxx" is parsed by url.Parse. | ||
| // - Windows: "file://C:\Users\..." is handled by ProjectRootToLocalPath. | ||
| func TestCountLinesOfCode_RootPathExists(t *testing.T) { | ||
| tmp := t.TempDir() | ||
| idx := &scip.Index{ | ||
| Metadata: &scip.Metadata{ProjectRoot: "file://" + tmp}, | ||
| } | ||
| if _, err := countLinesOfCode(idx, ""); err != nil { | ||
| t.Fatalf("countLinesOfCode returned error when rootPath exists: %v", err) | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we invert this flow and try
url.Parse(rawProjectRoot)first, then fall back only if parsing fails with the known Windows drive-root shape?Right now the special-case runs before
url.Parse, so it catches inputs that previously parsed successfully. For example,file://x:123/sharepreviously produced path/share, but this helper would now returnx:123/share. That conflicts with the comment below saying all other inputs preserveurl.Parse(rawProjectRoot).Path.