|
| 1 | +package compiler |
| 2 | + |
| 3 | +import ( |
| 4 | + "context" |
| 5 | + "slices" |
| 6 | + "strings" |
| 7 | + "testing" |
| 8 | + |
| 9 | + "github.com/microsoft/TypeScript/tsc/internal/core" |
| 10 | + "github.com/microsoft/TypeScript/tsc/internal/outputpaths" |
| 11 | + "github.com/microsoft/TypeScript/tsc/internal/tsoptions" |
| 12 | + "github.com/microsoft/TypeScript/tsc/internal/vfs/vfstest" |
| 13 | + "gotest.tools/v3/assert" |
| 14 | +) |
| 15 | + |
| 16 | +// sourceExtensionsProgram builds the program /src/tsconfig.json defines over files. |
| 17 | +func sourceExtensionsProgram(t *testing.T, files map[string]any) (*Program, *tsoptions.ParsedCommandLine, []string) { |
| 18 | + t.Helper() |
| 19 | + host := NewCompilerHost("/", vfstest.FromMap(files, true), "", nil, nil, nil) |
| 20 | + config, configDiagnostics := tsoptions.GetParsedCommandLineOfConfigFile("/src/tsconfig.json", nil, nil, host, nil) |
| 21 | + messages := []string{} |
| 22 | + for _, diagnostic := range append(configDiagnostics, config.GetConfigFileParsingDiagnostics()...) { |
| 23 | + messages = append(messages, diagnostic.String()) |
| 24 | + } |
| 25 | + program := NewProgram(ProgramOptions{Config: config, Host: host}) |
| 26 | + return program, config, messages |
| 27 | +} |
| 28 | + |
| 29 | +// fileNames are a program's own files: no lib. |
| 30 | +func fileNames(program *Program) []string { |
| 31 | + names := []string{} |
| 32 | + for _, file := range program.GetSourceFiles() { |
| 33 | + if strings.HasPrefix(file.FileName(), "/src/") { |
| 34 | + names = append(names, file.FileName()) |
| 35 | + } |
| 36 | + } |
| 37 | + slices.Sort(names) |
| 38 | + return names |
| 39 | +} |
| 40 | + |
| 41 | +// A config's "sourceExtensions" makes files with that extension TypeScript source in its program, under their |
| 42 | +// own names: enumerated by include, parsed as TypeScript, and reached by an import that names the extension, |
| 43 | +// which resolves to the file itself, never to an X.a.ts. A config without the key never reads such a file, |
| 44 | +// so a static library named libfoo.a beside the source is not parsed as TypeScript. The key is inherited |
| 45 | +// through extends, and an invalid entry is reported and left out. |
| 46 | +func TestSourceExtensions(t *testing.T) { |
| 47 | + t.Parallel() |
| 48 | + source := map[string]any{ |
| 49 | + "/src/geometry.a": "export function area(width: number, height: number): number { return width * height; }\n", |
| 50 | + "/src/main.a": "import { area } from './geometry.a';\nexport const total: number = area(2, 3);\nexport const wrong: string = area(1, 1);\n", |
| 51 | + "/src/plain.ts": "export const plain = 1;\n", |
| 52 | + "/src/libfoo.a": "!<arch>\nnot TypeScript at all {{{\n", |
| 53 | + } |
| 54 | + with := func(config string) map[string]any { |
| 55 | + files := map[string]any{"/src/tsconfig.json": config} |
| 56 | + for name, contents := range source { |
| 57 | + files[name] = contents |
| 58 | + } |
| 59 | + return files |
| 60 | + } |
| 61 | + |
| 62 | + t.Run("opted in", func(t *testing.T) { |
| 63 | + t.Parallel() |
| 64 | + files := with(`{"sourceExtensions": [".a"], "compilerOptions": {"noLib": true, "strict": true, "noEmit": true, "allowImportingTsExtensions": true, "module": "esnext", "moduleResolution": "bundler"}, "include": ["*.a", "*.ts"], "exclude": ["libfoo.a"]}`) |
| 65 | + program, config, configMessages := sourceExtensionsProgram(t, files) |
| 66 | + assert.Equal(t, len(configMessages), 0, configMessages) |
| 67 | + assert.DeepEqual(t, config.SourceExtensions(), []string{".a"}) |
| 68 | + assert.DeepEqual(t, fileNames(program), []string{"/src/geometry.a", "/src/main.a", "/src/plain.ts"}) |
| 69 | + |
| 70 | + main := program.GetSourceFile("/src/main.a") |
| 71 | + assert.Assert(t, main != nil) |
| 72 | + assert.Equal(t, main.ScriptKind, core.ScriptKindTS) |
| 73 | + // The one error is the planted one, in main.a under its own name: the import resolved, so area's |
| 74 | + // return type is known, and nothing about geometry.a.ts or an unresolved module appears. |
| 75 | + diagnostics := program.GetSemanticDiagnostics(context.Background(), main) |
| 76 | + assert.Equal(t, len(diagnostics), 1) |
| 77 | + assert.Equal(t, diagnostics[0].Code(), int32(2322)) |
| 78 | + assert.Equal(t, diagnostics[0].File().FileName(), "/src/main.a") |
| 79 | + resolvedFileName := "" |
| 80 | + for key, resolved := range program.GetResolvedModules()[main.Path()] { |
| 81 | + if key.Name == "./geometry.a" && resolved.IsResolved() { |
| 82 | + resolvedFileName = resolved.ResolvedFileName |
| 83 | + } |
| 84 | + } |
| 85 | + assert.Equal(t, resolvedFileName, "/src/geometry.a") |
| 86 | + }) |
| 87 | + |
| 88 | + t.Run("not opted in", func(t *testing.T) { |
| 89 | + t.Parallel() |
| 90 | + files := with(`{"compilerOptions": {"noLib": true, "noEmit": true}, "include": ["**/*"]}`) |
| 91 | + program, _, _ := sourceExtensionsProgram(t, files) |
| 92 | + assert.DeepEqual(t, fileNames(program), []string{"/src/plain.ts"}) |
| 93 | + }) |
| 94 | + |
| 95 | + t.Run("inherited through extends", func(t *testing.T) { |
| 96 | + t.Parallel() |
| 97 | + files := with(`{"extends": "./base.json", "compilerOptions": {"noLib": true, "noEmit": true}, "include": ["main.a", "geometry.a"]}`) |
| 98 | + files["/src/base.json"] = `{"sourceExtensions": [".a"]}` |
| 99 | + _, config, configMessages := sourceExtensionsProgram(t, files) |
| 100 | + assert.Equal(t, len(configMessages), 0, configMessages) |
| 101 | + assert.DeepEqual(t, config.SourceExtensions(), []string{".a"}) |
| 102 | + assert.DeepEqual(t, config.FileNames(), []string{"/src/main.a", "/src/geometry.a"}) |
| 103 | + }) |
| 104 | + |
| 105 | + t.Run("invalid entries are reported and left out", func(t *testing.T) { |
| 106 | + t.Parallel() |
| 107 | + files := with(`{"sourceExtensions": ["a", ".ts", ".a", ".a"], "compilerOptions": {"noLib": true, "noEmit": true}, "include": ["*.ts"]}`) |
| 108 | + _, config, configMessages := sourceExtensionsProgram(t, files) |
| 109 | + assert.DeepEqual(t, config.SourceExtensions(), []string{".a"}) |
| 110 | + assert.Equal(t, len(configMessages), 3, configMessages) |
| 111 | + }) |
| 112 | +} |
| 113 | + |
| 114 | +// A source extension's JavaScript output is X.a.js and its declaration output X.d.a.ts. ChangeExtension |
| 115 | +// doesn't strip an extension TypeScript doesn't know, so the JavaScript name used to be the input's own. |
| 116 | +func TestSourceExtensionOutputPaths(t *testing.T) { |
| 117 | + t.Parallel() |
| 118 | + files := map[string]any{ |
| 119 | + "/src/tsconfig.json": `{"sourceExtensions": [".a"], "compilerOptions": {"noLib": true, "declaration": true, "outDir": "/out"}, "include": ["*.a"]}`, |
| 120 | + "/src/main.a": "export const value = 1;\n", |
| 121 | + } |
| 122 | + program, _, _ := sourceExtensionsProgram(t, files) |
| 123 | + options := program.Options() |
| 124 | + assert.Equal(t, outputpaths.GetOutputJSFileName("/src/main.a", options, program), "/out/main.a.js") |
| 125 | + assert.Equal(t, outputpaths.GetOutputDeclarationFileNameWorker("/src/main.a", options, program), "/out/main.d.a.ts") |
| 126 | +} |
0 commit comments