Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions blast_radius.go
Original file line number Diff line number Diff line change
Expand Up @@ -1412,7 +1412,13 @@ func renderCoverage(w io.Writer, status string, notes []string) {
}

func buildImportersReportFromGraph(root, file string, fg *scanner.FileGraph) scanner.ImportersReport {
if canonical, err := filepath.EvalSymlinks(root); err == nil {
root = canonical
}
if filepath.IsAbs(file) {
if canonical, err := filepath.EvalSymlinks(file); err == nil {
file = canonical
}
if rel, err := filepath.Rel(root, file); err == nil {
file = rel
}
Expand Down
53 changes: 42 additions & 11 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ type watchReadiness struct {
}

func main() {
args, err := applyGlobalRootOptions(os.Args[1:])
args, projectRootExplicit, err := applyGlobalRootOptions(os.Args[1:])
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(2)
Expand Down Expand Up @@ -318,7 +318,12 @@ func main() {
os.Exit(0)
}

root := flag.Arg(0)
root, importer, err := resolveImportersInvocation(flag.Arg(0), *importersMode, projectRootExplicit)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(2)
}
*importersMode = importer
if root == "" {
root = "."
}
Expand Down Expand Up @@ -512,45 +517,71 @@ func main() {
}
}

func applyGlobalRootOptions(args []string) ([]string, error) {
func applyGlobalRootOptions(args []string) ([]string, bool, error) {
opts, remaining, err := cmd.ParseGlobalRootOptions(args)
if err != nil {
return nil, err
return nil, false, err
}
if !opts.Active() {
launchDir, err := os.Getwd()
if err != nil {
return nil, fmt.Errorf("get working directory: %w", err)
return nil, false, fmt.Errorf("get working directory: %w", err)
}
if _, err := projectpath.Select(launchDir); err != nil {
return nil, err
return nil, false, err
}
return remaining, nil
return remaining, false, nil
}

launchDir, err := os.Getwd()
if err != nil {
return nil, fmt.Errorf("get working directory: %w", err)
return nil, false, fmt.Errorf("get working directory: %w", err)
}
roots, err := cmd.ResolveGlobalRoots(opts, launchDir)
if err != nil {
return nil, err
return nil, false, err
}
// Canonicalize the stored setup root so projectpath and daemon args agree
// with the resolved project root (e.g. macOS /var -> /private/var).
if canonical, err := filepath.EvalSymlinks(roots.Setup); err == nil {
roots.Setup = canonical
}
if err := os.Chdir(roots.Project); err != nil {
return nil, fmt.Errorf("change to project root %q: %w", roots.Project, err)
return nil, false, fmt.Errorf("change to project root %q: %w", roots.Project, err)
}
if opts.SetupRoot != "" {
projectpath.SetSetupRoot(roots.Setup)
} else {
projectpath.ResetSetupRoot()
}

return remaining, nil
return remaining, opts.Directory != "", nil
}

func resolveImportersInvocation(root, importer string, projectRootExplicit bool) (string, string, error) {
if root != "" || projectRootExplicit || importer == "" || !filepath.IsAbs(importer) {
// Canonicalize the importer so the relative path matches the
// canonicalized project root (e.g. macOS /tmp -> /private/tmp).
if filepath.IsAbs(importer) {
if canonical, err := filepath.EvalSymlinks(importer); err == nil {
importer = canonical
}
}
return root, importer, nil
}

canonicalImporter, err := filepath.EvalSymlinks(importer)
if err != nil {
return "", "", fmt.Errorf("resolve --importers file %q: %w", importer, err)
}
inferred, found, err := cmd.ResolveNearestGitRoot(filepath.Dir(canonicalImporter))
if err != nil {
return "", "", fmt.Errorf("infer project root from --importers %q: %w", importer, err)
}
if !found {
return "", "", fmt.Errorf("infer project root from --importers %q: file is not inside a Git repository", importer)
}
return inferred, canonicalImporter, nil
}

// stdinManifest is the JSON format accepted by --stdin.
Expand Down
178 changes: 175 additions & 3 deletions main_more_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ func TestApplyGlobalRootOptions(t *testing.T) {
projectpath.ResetSetupRoot()
t.Cleanup(projectpath.ResetSetupRoot)

args, err := applyGlobalRootOptions([]string{
args, projectRootExplicit, err := applyGlobalRootOptions([]string{
"context", "--project-root", projectNested,
"--setup-root", setupNested, "--compact",
})
Expand All @@ -67,6 +67,9 @@ func TestApplyGlobalRootOptions(t *testing.T) {
if got := strings.Join(args, "|"); got != "context|--compact" {
t.Fatalf("args = %q, want context|--compact", got)
}
if !projectRootExplicit {
t.Fatal("expected explicit project root")
}
gotDir, err := os.Getwd()
if err != nil {
t.Fatal(err)
Expand Down Expand Up @@ -116,7 +119,7 @@ func TestApplyGlobalRootOptionsDirectoryKeepsAutomaticLinkedSelection(t *testing
projectpath.ResetSetupRoot()
t.Cleanup(projectpath.ResetSetupRoot)

if _, err := applyGlobalRootOptions([]string{"-C", filepath.Join(linked, "pkg"), "context"}); err != nil {
if _, _, err := applyGlobalRootOptions([]string{"-C", filepath.Join(linked, "pkg"), "context"}); err != nil {
t.Fatalf("applyGlobalRootOptions() error: %v", err)
}
if got := projectpath.ConfiguredSetupRoot(); got != "" {
Expand Down Expand Up @@ -146,7 +149,7 @@ func TestApplyGlobalRootOptionsRejectsMalformedLinkedMetadataWithoutFlags(t *tes
}
t.Cleanup(func() { _ = os.Chdir(originalDir) })

if _, err := applyGlobalRootOptions([]string{"context"}); err == nil || !strings.Contains(err.Error(), "resolve linked worktree setup") {
if _, _, err := applyGlobalRootOptions([]string{"context"}); err == nil || !strings.Contains(err.Error(), "resolve linked worktree setup") {
t.Fatalf("applyGlobalRootOptions() error = %v, want bounded linked-worktree error", err)
}
}
Expand Down Expand Up @@ -620,6 +623,175 @@ func TestRunImportersMode(t *testing.T) {
}
}

func TestResolveImportersProjectRoot(t *testing.T) {
repo := makeMainGitRepo(t, "main")
absoluteFile := filepath.Join(repo, "main.go")
canonicalRepo, err := filepath.EvalSymlinks(repo)
if err != nil {
t.Fatal(err)
}

t.Run("infers repository from absolute importer", func(t *testing.T) {
gotRoot, gotFile, err := resolveImportersInvocation("", absoluteFile, false)
if err != nil {
t.Fatalf("resolveImportersInvocation() error: %v", err)
}
if gotRoot != canonicalRepo {
t.Fatalf("root = %q, want %q", gotRoot, canonicalRepo)
}
canonicalFile, err := filepath.EvalSymlinks(absoluteFile)
if err != nil {
t.Fatal(err)
}
if gotFile != canonicalFile {
t.Fatalf("file = %q, want %q", gotFile, canonicalFile)
}
})

t.Run("explicit roots win", func(t *testing.T) {
for _, test := range []struct {
name string
positionalRoot string
projectRootExplicit bool
want string
}{
{name: "positional", positionalRoot: "/explicit/positional", want: "/explicit/positional"},
{name: "project flag", projectRootExplicit: true, want: ""},
} {
t.Run(test.name, func(t *testing.T) {
gotRoot, gotFile, err := resolveImportersInvocation(test.positionalRoot, absoluteFile, test.projectRootExplicit)
if err != nil {
t.Fatalf("resolveImportersInvocation() error: %v", err)
}
if gotRoot != test.want {
t.Fatalf("root = %q, want %q", gotRoot, test.want)
}
// The explicit root wins, but the absolute importer is still
// canonicalized so its relative path matches the canonical
// project root (e.g. macOS /tmp -> /private/tmp).
canonicalFile, err := filepath.EvalSymlinks(absoluteFile)
if err != nil {
t.Fatal(err)
}
if gotFile != canonicalFile {
t.Fatalf("file = %q, want canonical %q", gotFile, canonicalFile)
}
})
}
})

t.Run("absolute importer outside repository fails", func(t *testing.T) {
file := filepath.Join(t.TempDir(), "outside.go")
if err := os.WriteFile(file, []byte("package outside\n"), 0o644); err != nil {
t.Fatal(err)
}
if _, _, err := resolveImportersInvocation("", file, false); err == nil ||
!strings.Contains(err.Error(), "is not inside a Git repository") {
t.Fatalf("resolveImportersInvocation() error = %v, want repository error", err)
}
})
}

func TestAbsoluteImporterInfersProjectRootFromUnrelatedDirectory(t *testing.T) {
if !scanner.NewAstGrepAnalyzer().Available() {
t.Skip("ast-grep not available")
}

repo := makeMainGitRepo(t, "main")
canonicalRepo, err := filepath.EvalSymlinks(repo)
if err != nil {
t.Fatal(err)
}
out, err := runRootOptionsBinary(
t.TempDir(),
"--json",
"--importers", filepath.Join(repo, "main.go"),
)
if err != nil {
t.Fatalf("codemap failed: %v\n%s", err, out)
}

var report scanner.ImportersReport
if err := json.Unmarshal([]byte(out), &report); err != nil {
t.Fatalf("decode importers report: %v\n%s", err, out)
}
if report.Root != canonicalRepo {
t.Fatalf("report root = %q, want %q", report.Root, canonicalRepo)
}
if report.File != "main.go" {
t.Fatalf("report file = %q, want main.go", report.File)
}
}

// TestExplicitRootImportersReportUsesCanonicalRelativePath ensures an
// absolute importer with a differently spelled root still reports a
// canonical root-relative path.
func TestExplicitRootImportersReportUsesCanonicalRelativePath(t *testing.T) {
if !scanner.NewAstGrepAnalyzer().Available() {
t.Skip("ast-grep not available")
}

repo := makeMainGitRepo(t, "main")
canonicalRepo, err := filepath.EvalSymlinks(repo)
if err != nil {
t.Fatal(err)
}
out, err := runRootOptionsBinary(
t.TempDir(),
"--json",
"--importers", filepath.Join(repo, "main.go"),
canonicalRepo,
)
if err != nil {
t.Fatalf("codemap failed: %v\n%s", err, out)
}

var report scanner.ImportersReport
if err := json.Unmarshal([]byte(out), &report); err != nil {
t.Fatalf("decode importers report: %v\n%s", err, out)
}
if report.Root != canonicalRepo {
t.Fatalf("report root = %q, want %q", report.Root, canonicalRepo)
}
if report.File != "main.go" {
t.Fatalf("report file = %q, want main.go (canonical-relative)", report.File)
}
}

func TestExplicitRootImportersReportCanonicalizesSymlinkedRoot(t *testing.T) {
if !scanner.NewAstGrepAnalyzer().Available() {
t.Skip("ast-grep not available")
}

repo := makeMainGitRepo(t, "main")
aliasParent := t.TempDir()
alias := filepath.Join(aliasParent, "repo")
if err := os.Symlink(repo, alias); err != nil {
t.Fatal(err)
}
canonicalRepo, err := filepath.EvalSymlinks(repo)
if err != nil {
t.Fatal(err)
}
out, err := runRootOptionsBinary(
t.TempDir(),
"--json",
"--importers", filepath.Join(repo, "main.go"),
alias,
)
if err != nil {
t.Fatalf("codemap failed: %v\n%s", err, out)
}

var report scanner.ImportersReport
if err := json.Unmarshal([]byte(out), &report); err != nil {
t.Fatalf("decode importers report: %v\n%s", err, out)
}
if report.Root != canonicalRepo || report.File != "main.go" {
t.Fatalf("report = root %q, file %q; want root %q and main.go", report.Root, report.File, canonicalRepo)
}
}

func TestRunDepsModeJSONAndMainDispatchesDepsAndImporters(t *testing.T) {
if !scanner.NewAstGrepAnalyzer().Available() {
t.Skip("ast-grep not available")
Expand Down
Loading