Skip to content
Merged
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
83 changes: 83 additions & 0 deletions scanner/aliastarget_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package scanner

import (
"context"
"reflect"
"sort"
"testing"
)

// create-next-app has shipped "@/*": ["./*"] for years, so this is the most
// common TypeScript layout in the wild. The substituted target was "./lib/a1"
// while the file index holds "lib/a1", so nothing matched and a file imported
// everywhere reported no importers at all.
func TestTsconfigDotSlashAliasResolvesImporters(t *testing.T) {
graph, err := BuildFileGraph(context.Background(), "../testdata/tsconfig-alias-dotslash", Filters{})
if err != nil {
t.Fatalf("build alias fixture graph: %v", err)
}
// Compare as a set: the graph appends importers in analysis order, which
// is not deterministic across runs (see the note in the PR for #173). The
// exactness this asserts is membership, not sequence.
got := append([]string(nil), graph.Importers["lib/a1.ts"]...)
sort.Strings(got)
want := []string{"app/layout.tsx", "app/page.tsx"}
if !reflect.DeepEqual(got, want) {
t.Fatalf("lib/a1.ts importers = %v, want exactly %v", got, want)
}
}

func TestNormalizeAliasTarget(t *testing.T) {
for _, tc := range []struct {
name string
target string
want string
}{
{"create-next-app default", "./lib/a1", "lib/a1"},
{"bare wildcard target", "lib/a1", "lib/a1"},
{"nested with dot slash", "./src/lib/s1", "src/lib/s1"},
{"redundant separators", "./src//lib/../lib/s1", "src/lib/s1"},
{"bare dot collapses to root", ".", ""},
{"dot slash only", "./", ""},
{"empty stays empty", "", ""},
// A target escaping the project must not be silently rewritten into a
// path that could match an unrelated file.
{"parent traversal preserved", "../shared/x", "../shared/x"},
} {
t.Run(tc.name, func(t *testing.T) {
if got := normalizeAliasTarget(tc.target); got != tc.want {
t.Fatalf("normalizeAliasTarget(%q) = %q, want %q", tc.target, got, tc.want)
}
})
}
}

// Every alias target shape has to keep resolving, so the "./" fix cannot
// regress the forms that already worked.
func TestPathAliasTargetShapes(t *testing.T) {
files := []FileInfo{{Path: "lib/a1.ts"}, {Path: "src/lib/s1.ts"}, {Path: "app/page.tsx"}}
idx := buildFileIndex(files, "")

for _, tc := range []struct {
name string
imp string
aliases map[string][]string
baseURL string
want []string
}{
{"dot slash wildcard", "@/lib/a1", map[string][]string{"@/*": {"./*"}}, "", []string{"lib/a1.ts"}},
{"bare wildcard", "@/lib/a1", map[string][]string{"@/*": {"*"}}, "", []string{"lib/a1.ts"}},
{"nested dot slash", "@/lib/s1", map[string][]string{"@/*": {"./src/*"}}, "", []string{"src/lib/s1.ts"}},
{"nested bare", "@/lib/s1", map[string][]string{"@/*": {"src/*"}}, "", []string{"src/lib/s1.ts"}},
{"base url with dot slash", "@/lib/a1", map[string][]string{"@/*": {"./*"}}, ".", []string{"lib/a1.ts"}},
{"exact alias with dot slash", "@app", map[string][]string{"@app": {"./lib/a1"}}, "", []string{"lib/a1.ts"}},
{"unmatched alias resolves to nothing", "@/nope", map[string][]string{"@/*": {"./*"}}, "", nil},
} {
t.Run(tc.name, func(t *testing.T) {
got := resolvePathAlias(tc.imp, tc.aliases, tc.baseURL, idx, "typescript")
if !reflect.DeepEqual(got, tc.want) {
t.Fatalf("resolvePathAlias(%q, %v, baseURL=%q) = %v, want %v", tc.imp, tc.aliases, tc.baseURL, got, tc.want)
}
})
}
}
20 changes: 20 additions & 0 deletions scanner/filegraph.go
Original file line number Diff line number Diff line change
Expand Up @@ -811,6 +811,23 @@ func readTSConfig(configPath, root string) (map[string][]string, string) {
return paths, baseURL
}

// normalizeAliasTarget puts a substituted tsconfig alias target into the same
// shape as the file index, which stores repository-relative paths with no "./"
// prefix. create-next-app has shipped "@/*": ["./*"] for years, so the
// substituted target is "./lib/a1" while the index holds "lib/a1" and nothing
// matches. filepath.Join already cleaned the target when a baseUrl was set,
// which is why this only ever bit projects without one.
func normalizeAliasTarget(target string) string {
if target == "" {
return target
}
cleaned := filepath.ToSlash(filepath.Clean(target))
if cleaned == "." {
return ""
}
return cleaned
}

// resolvePathAlias attempts to resolve an import using TypeScript path aliases
// e.g., "@modules/auth" with alias "@modules/*" -> ["src/modules/*"] becomes "src/modules/auth"
func resolvePathAlias(imp string, pathAliases map[string][]string, baseURL string, idx *fileIndex, sourceLanguage string) []string {
Expand All @@ -828,6 +845,7 @@ func resolvePathAlias(imp string, pathAliases map[string][]string, baseURL strin
if baseURL != "" && !filepath.IsAbs(resolved) {
resolved = filepath.Join(baseURL, resolved)
}
resolved = normalizeAliasTarget(resolved)
if files := tryExactMatch(resolved, idx, sourceLanguage); len(files) > 0 {
return files
}
Expand Down Expand Up @@ -863,6 +881,8 @@ func resolvePathAlias(imp string, pathAliases map[string][]string, baseURL strin
resolved = filepath.Join(baseURL, resolved)
}

resolved = normalizeAliasTarget(resolved)

// Try to find matching files
if files := tryExactMatch(resolved, idx, sourceLanguage); len(files) > 0 {
return files
Expand Down
5 changes: 5 additions & 0 deletions testdata/tsconfig-alias-dotslash/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { a1 } from "@/lib/a1";

export function Layout() {
return a1();
}
5 changes: 5 additions & 0 deletions testdata/tsconfig-alias-dotslash/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { a1 } from "@/lib/a1";

export default function Page() {
return a1();
}
3 changes: 3 additions & 0 deletions testdata/tsconfig-alias-dotslash/lib/a1.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function a1(): string {
return "a1";
}
7 changes: 7 additions & 0 deletions testdata/tsconfig-alias-dotslash/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"compilerOptions": {
"paths": {
"@/*": ["./*"]
}
}
}
Loading