package compiler_test import ( "fmt" "path/filepath" "slices" "strings" "testing" "github.com/microsoft/typescript-go/internal/bundled" "github.com/microsoft/typescript-go/internal/compiler" "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/repo" "github.com/microsoft/typescript-go/internal/tsoptions" "github.com/microsoft/typescript-go/internal/tspath" "github.com/microsoft/typescript-go/internal/vfs/osvfs" "github.com/microsoft/typescript-go/internal/vfs/vfstest" "gotest.tools/v3/assert" ) type testFile struct { fileName string contents string } type programTest struct { testName string files []testFile expectedFiles []string target core.ScriptTarget } var esnextLibs = []string{ "lib.es5.d.ts", "lib.es2015.d.ts", "lib.es2016.d.ts", "lib.es2017.d.ts", "lib.es2018.d.ts", "lib.es2019.d.ts", "lib.es2020.d.ts", "lib.es2021.d.ts", "lib.es2022.d.ts", "lib.es2023.d.ts", "lib.es2024.d.ts", "lib.es2025.d.ts", "lib.esnext.d.ts", "lib.dom.d.ts", "lib.dom.iterable.d.ts", "lib.dom.asynciterable.d.ts", "lib.webworker.importscripts.d.ts", "lib.scripthost.d.ts", "lib.es2015.core.d.ts", "lib.es2015.collection.d.ts", "lib.es2015.generator.d.ts", "lib.es2015.iterable.d.ts", "lib.es2015.promise.d.ts", "lib.es2015.proxy.d.ts", "lib.es2015.reflect.d.ts", "lib.es2015.symbol.d.ts", "lib.es2015.symbol.wellknown.d.ts", "lib.es2016.array.include.d.ts", "lib.es2016.intl.d.ts", "lib.es2017.arraybuffer.d.ts", "lib.es2017.date.d.ts", "lib.es2017.object.d.ts", "lib.es2017.sharedmemory.d.ts", "lib.es2017.string.d.ts", "lib.es2017.intl.d.ts", "lib.es2017.typedarrays.d.ts", "lib.es2018.asyncgenerator.d.ts", "lib.es2018.asynciterable.d.ts", "lib.es2018.intl.d.ts", "lib.es2018.promise.d.ts", "lib.es2018.regexp.d.ts", "lib.es2019.array.d.ts", "lib.es2019.object.d.ts", "lib.es2019.string.d.ts", "lib.es2019.symbol.d.ts", "lib.es2019.intl.d.ts", "lib.es2020.bigint.d.ts", "lib.es2020.date.d.ts", "lib.es2020.promise.d.ts", "lib.es2020.sharedmemory.d.ts", "lib.es2020.string.d.ts", "lib.es2020.symbol.wellknown.d.ts", "lib.es2020.intl.d.ts", "lib.es2020.number.d.ts", "lib.es2021.promise.d.ts", "lib.es2021.string.d.ts", "lib.es2021.weakref.d.ts", "lib.es2021.intl.d.ts", "lib.es2022.array.d.ts", "lib.es2022.error.d.ts", "lib.es2022.intl.d.ts", "lib.es2022.object.d.ts", "lib.es2022.string.d.ts", "lib.es2022.regexp.d.ts", "lib.es2023.array.d.ts", "lib.es2023.collection.d.ts", "lib.es2023.intl.d.ts", "lib.es2024.arraybuffer.d.ts", "lib.es2024.collection.d.ts", "lib.es2024.object.d.ts", "lib.es2024.promise.d.ts", "lib.es2024.regexp.d.ts", "lib.es2024.sharedmemory.d.ts", "lib.es2024.string.d.ts", "lib.es2025.collection.d.ts", "lib.es2025.float16.d.ts", "lib.es2025.intl.d.ts", "lib.es2025.iterator.d.ts", "lib.es2025.promise.d.ts", "lib.es2025.regexp.d.ts", "lib.esnext.array.d.ts", "lib.esnext.collection.d.ts", "lib.esnext.date.d.ts", "lib.esnext.decorators.d.ts", "lib.esnext.disposable.d.ts", "lib.esnext.error.d.ts", "lib.esnext.intl.d.ts", "lib.esnext.sharedmemory.d.ts", "lib.esnext.temporal.d.ts", "lib.esnext.typedarrays.d.ts", "lib.decorators.d.ts", "lib.decorators.legacy.d.ts", "lib.esnext.full.d.ts", } var programTestCases = []programTest{ { testName: "BasicFileOrdering", files: []testFile{ {fileName: "c:/dev/src/index.ts", contents: "/// \n/// "}, {fileName: "c:/dev/src2/a/5.ts", contents: "/// "}, {fileName: "c:/dev/src2/a/4.ts", contents: "/// "}, {fileName: "c:/dev/src2/a/b/3.ts", contents: "/// "}, {fileName: "c:/dev/src2/a/b/2.ts", contents: "/// "}, {fileName: "c:/dev/src2/a/b/c/1.ts", contents: "console.log('hello');"}, {fileName: "c:/dev/src2/a/10.ts", contents: "/// "}, {fileName: "c:/dev/src2/a/b/c/d/9.ts", contents: "/// "}, {fileName: "c:/dev/src2/a/b/c/d/e/8.ts", contents: "/// "}, {fileName: "c:/dev/src2/a/b/c/d/e/7.ts", contents: "/// "}, {fileName: "c:/dev/src2/a/b/c/d/e/f/6.ts", contents: "console.log('world!');"}, }, expectedFiles: slices.Concat(esnextLibs, []string{ "c:/dev/src2/a/b/c/1.ts", "c:/dev/src2/a/b/2.ts", "c:/dev/src2/a/b/3.ts", "c:/dev/src2/a/4.ts", "c:/dev/src2/a/5.ts", "c:/dev/src2/a/b/c/d/e/f/6.ts", "c:/dev/src2/a/b/c/d/e/7.ts", "c:/dev/src2/a/b/c/d/e/8.ts", "c:/dev/src2/a/b/c/d/9.ts", "c:/dev/src2/a/10.ts", "c:/dev/src/index.ts", }), target: core.ScriptTargetESNext, }, { testName: "FileOrderingImports", files: []testFile{ {fileName: "c:/dev/src/index.ts", contents: "import * as five from '../src2/a/5.ts';\nimport * as ten from '../src2/a/10.ts';"}, {fileName: "c:/dev/src2/a/5.ts", contents: "import * as four from './4.ts';"}, {fileName: "c:/dev/src2/a/4.ts", contents: "import * as three from './b/3.ts';"}, {fileName: "c:/dev/src2/a/b/3.ts", contents: "import * as two from './2.ts';"}, {fileName: "c:/dev/src2/a/b/2.ts", contents: "import * as one from './c/1.ts';"}, {fileName: "c:/dev/src2/a/b/c/1.ts", contents: "console.log('hello');"}, {fileName: "c:/dev/src2/a/10.ts", contents: "import * as nine from './b/c/d/9.ts';"}, {fileName: "c:/dev/src2/a/b/c/d/9.ts", contents: "import * as eight from './e/8.ts';"}, {fileName: "c:/dev/src2/a/b/c/d/e/8.ts", contents: "import * as seven from './7.ts';"}, {fileName: "c:/dev/src2/a/b/c/d/e/7.ts", contents: "import * as six from './f/6.ts';"}, {fileName: "c:/dev/src2/a/b/c/d/e/f/6.ts", contents: "console.log('world!');"}, }, expectedFiles: slices.Concat(esnextLibs, []string{ "c:/dev/src2/a/b/c/1.ts", "c:/dev/src2/a/b/2.ts", "c:/dev/src2/a/b/3.ts", "c:/dev/src2/a/4.ts", "c:/dev/src2/a/5.ts", "c:/dev/src2/a/b/c/d/e/f/6.ts", "c:/dev/src2/a/b/c/d/e/7.ts", "c:/dev/src2/a/b/c/d/e/8.ts", "c:/dev/src2/a/b/c/d/9.ts", "c:/dev/src2/a/10.ts", "c:/dev/src/index.ts", }), target: core.ScriptTargetESNext, }, { testName: "FileOrderingCycles", files: []testFile{ {fileName: "c:/dev/src/index.ts", contents: "import * as five from '../src2/a/5.ts';\nimport * as ten from '../src2/a/10.ts';"}, {fileName: "c:/dev/src2/a/5.ts", contents: "import * as four from './4.ts';"}, {fileName: "c:/dev/src2/a/4.ts", contents: "import * as three from './b/3.ts';"}, {fileName: "c:/dev/src2/a/b/3.ts", contents: "import * as two from './2.ts';\nimport * as cycle from 'c:/dev/src/index.ts'; "}, {fileName: "c:/dev/src2/a/b/2.ts", contents: "import * as one from './c/1.ts';"}, {fileName: "c:/dev/src2/a/b/c/1.ts", contents: "console.log('hello');"}, {fileName: "c:/dev/src2/a/10.ts", contents: "import * as nine from './b/c/d/9.ts';"}, {fileName: "c:/dev/src2/a/b/c/d/9.ts", contents: "import * as eight from './e/8.ts';\nimport * as cycle from 'c:/dev/src/index.ts';"}, {fileName: "c:/dev/src2/a/b/c/d/e/8.ts", contents: "import * as seven from './7.ts';"}, {fileName: "c:/dev/src2/a/b/c/d/e/7.ts", contents: "import * as six from './f/6.ts';"}, {fileName: "c:/dev/src2/a/b/c/d/e/f/6.ts", contents: "console.log('world!');"}, }, expectedFiles: slices.Concat(esnextLibs, []string{ "c:/dev/src2/a/b/c/1.ts", "c:/dev/src2/a/b/2.ts", "c:/dev/src2/a/b/3.ts", "c:/dev/src2/a/4.ts", "c:/dev/src2/a/5.ts", "c:/dev/src2/a/b/c/d/e/f/6.ts", "c:/dev/src2/a/b/c/d/e/7.ts", "c:/dev/src2/a/b/c/d/e/8.ts", "c:/dev/src2/a/b/c/d/9.ts", "c:/dev/src2/a/10.ts", "c:/dev/src/index.ts", }), target: core.ScriptTargetESNext, }, } func TestProgram(t *testing.T) { t.Parallel() if !bundled.Embedded { // Without embedding, we'd need to read all of the lib files out from disk into the MapFS. // Just skip this for now. t.Skip("bundled files are not embedded") } for _, testCase := range programTestCases { t.Run(testCase.testName, func(t *testing.T) { t.Parallel() libPrefix := bundled.LibPath() + "/" fs := vfstest.FromMap[any](nil, false /*useCaseSensitiveFileNames*/) fs = bundled.WrapFS(fs) for _, testFile := range testCase.files { _ = fs.WriteFile(testFile.fileName, testFile.contents) } opts := core.CompilerOptions{Target: testCase.target} program := compiler.NewProgram(compiler.ProgramOptions{ Config: &tsoptions.ParsedCommandLine{ ParsedConfig: &core.ParsedOptions{ FileNames: []string{"c:/dev/src/index.ts"}, CompilerOptions: &opts, }, }, Host: compiler.NewCompilerHost("c:/dev/src", fs, bundled.LibPath(), nil, nil), }) actualFiles := []string{} for _, file := range program.GetSourceFiles() { actualFiles = append(actualFiles, strings.TrimPrefix(file.FileName(), libPrefix)) } assert.DeepEqual(t, testCase.expectedFiles, actualFiles) }) } } func TestIncludeProcessorDiagnosticsWithMissingFileCasing(t *testing.T) { t.Parallel() if !bundled.Embedded { t.Skip("bundled files are not embedded") } // Use case-sensitive file names so that /src/MyFile.ts and /src/myFile.ts // have different canonical paths but the same lower-case path, triggering // file casing diagnostics in the include processor. fs := vfstest.FromMap[any](nil, true /*useCaseSensitiveFileNames*/) fs = bundled.WrapFS(fs) // Only create the lowercase version; /src/MyFile.ts does not exist. _ = fs.WriteFile("/src/myFile.ts", `export const y = 2;`) opts := core.CompilerOptions{SkipDefaultLibCheck: core.TSTrue} // List both casings as root files. The first one (/src/MyFile.ts) will fail // to load because it does not exist on the case-sensitive filesystem. program := compiler.NewProgram(compiler.ProgramOptions{ Config: &tsoptions.ParsedCommandLine{ ParsedConfig: &core.ParsedOptions{ FileNames: []string{"/src/MyFile.ts", "/src/myFile.ts"}, CompilerOptions: &opts, }, }, Host: compiler.NewCompilerHost("/", fs, bundled.LibPath(), nil, nil), }) // GetProgramDiagnostics triggers getDiagnostics which processes all // include processor diagnostics including the casing diagnostic whose // file path points to the missing /src/MyFile.ts. Before the fix this // panicked with a nil pointer dereference. assert.NilError(t, func() (err error) { defer func() { if r := recover(); r != nil { err = fmt.Errorf("panic: %v", r) } }() program.GetProgramDiagnostics() return nil }()) } func BenchmarkNewProgram(b *testing.B) { if !bundled.Embedded { // Without embedding, we'd need to read all of the lib files out from disk into the MapFS. // Just skip this for now. b.Skip("bundled files are not embedded") } for _, testCase := range programTestCases { b.Run(testCase.testName, func(b *testing.B) { fs := vfstest.FromMap[any](nil, false /*useCaseSensitiveFileNames*/) fs = bundled.WrapFS(fs) for _, testFile := range testCase.files { _ = fs.WriteFile(testFile.fileName, testFile.contents) } opts := core.CompilerOptions{Target: testCase.target} programOpts := compiler.ProgramOptions{ Config: &tsoptions.ParsedCommandLine{ ParsedConfig: &core.ParsedOptions{ FileNames: []string{"c:/dev/src/index.ts"}, CompilerOptions: &opts, }, }, Host: compiler.NewCompilerHost("c:/dev/src", fs, bundled.LibPath(), nil, nil), } for b.Loop() { compiler.NewProgram(programOpts) } }) } b.Run("compiler", func(b *testing.B) { repo.SkipIfNoTypeScriptSubmodule(b) rootPath := tspath.NormalizeSlashes(filepath.Join(repo.TypeScriptSubmodulePath(), "src", "compiler")) fs := osvfs.FS() fs = bundled.WrapFS(fs) host := compiler.NewCompilerHost(rootPath, fs, bundled.LibPath(), nil, nil) parsed, errors := tsoptions.GetParsedCommandLineOfConfigFile(tspath.CombinePaths(rootPath, "tsconfig.json"), nil, nil, host, nil) assert.Equal(b, len(errors), 0, "Expected no errors in parsed command line") opts := compiler.ProgramOptions{ Config: parsed, Host: host, } for b.Loop() { compiler.NewProgram(opts) } }) }