vendor tsgo

This commit is contained in:
2026-07-09 16:50:43 -04:00
parent c06ea2e5a4
commit 98978e4930
5804 changed files with 1556156 additions and 101 deletions

View File

@@ -0,0 +1,200 @@
package tsoptions
import (
"github.com/microsoft/typescript-go/internal/collections"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/diagnostics"
)
type CommandLineOptionKind string
const (
CommandLineOptionTypeString CommandLineOptionKind = "string"
CommandLineOptionTypeNumber CommandLineOptionKind = "number"
CommandLineOptionTypeBoolean CommandLineOptionKind = "boolean"
CommandLineOptionTypeObject CommandLineOptionKind = "object"
CommandLineOptionTypeList CommandLineOptionKind = "list"
CommandLineOptionTypeListOrElement CommandLineOptionKind = "listOrElement"
CommandLineOptionTypeEnum CommandLineOptionKind = "enum" // map
)
type CommandLineOption struct {
Name, ShortName string
Kind CommandLineOptionKind
// used in parsing
IsFilePath bool
IsTSConfigOnly bool
IsCommandLineOnly bool
// used in output
Description *diagnostics.Message
DefaultValueDescription any
ShowInSimplifiedHelpView bool
// used in output in serializing and generate tsconfig
Category *diagnostics.Message
// What kind of extra validation `validateJsonOptionValue` should do
extraValidation extraValidation
// checks that option with number type has value >= minValue
minValue int
// true or undefined
// used for configDirTemplateSubstitutionOptions
allowConfigDirTemplateSubstitution bool
// used for filter in compilerrunner
AffectsDeclarationPath bool
AffectsProgramStructure bool
AffectsSemanticDiagnostics bool
AffectsBuildInfo bool
AffectsBindDiagnostics bool
AffectsSourceFile bool
AffectsModuleResolution bool
AffectsEmit bool
allowJsFlag bool
strictFlag bool
// used in transpileoptions worker
// todo: revisit to see if this can be reduced to boolean
transpileOptionValue core.Tristate
// used for CommandLineOptionTypeList
listPreserveFalsyValues bool
// used for compilerOptionsDeclaration
ElementOptions CommandLineOptionNameMap
}
type extraValidation string
const (
extraValidationNone extraValidation = ""
extraValidationSpec extraValidation = "spec"
extraValidationLocale extraValidation = "locale"
)
func (o *CommandLineOption) DeprecatedKeys() *collections.Set[string] {
if o.Kind != CommandLineOptionTypeEnum {
return nil
}
return commandLineOptionDeprecated[o.Name]
}
func (o *CommandLineOption) EnumMap() *collections.OrderedMap[string, any] {
if o.Kind != CommandLineOptionTypeEnum {
return nil
}
return commandLineOptionEnumMap[o.Name]
}
func (o *CommandLineOption) Elements() *CommandLineOption {
if o.Kind != CommandLineOptionTypeList && o.Kind != CommandLineOptionTypeListOrElement {
return nil
}
return commandLineOptionElements[o.Name]
}
func (o *CommandLineOption) DisallowNullOrUndefined() bool {
return o.Name == "extends"
}
// CommandLineOption.Elements()
var commandLineOptionElements = map[string]*CommandLineOption{
"lib": {
Name: "lib",
Kind: CommandLineOptionTypeEnum, // libMap,
DefaultValueDescription: core.TSUnknown,
},
"rootDirs": {
Name: "rootDirs",
Kind: CommandLineOptionTypeString,
IsFilePath: true,
},
"typeRoots": {
Name: "typeRoots",
Kind: CommandLineOptionTypeString,
IsFilePath: true,
},
"types": {
Name: "types",
Kind: CommandLineOptionTypeString,
},
"moduleSuffixes": {
Name: "moduleSuffixes",
Kind: CommandLineOptionTypeString,
},
"customConditions": {
Name: "condition",
Kind: CommandLineOptionTypeString,
},
"plugins": {
Name: "plugin",
Kind: CommandLineOptionTypeObject,
},
// For tsconfig root options
"references": {
Name: "references",
Kind: CommandLineOptionTypeObject,
},
"files": {
Name: "files",
Kind: CommandLineOptionTypeString,
},
"include": {
Name: "include",
Kind: CommandLineOptionTypeString,
},
"exclude": {
Name: "exclude",
Kind: CommandLineOptionTypeString,
},
"extends": {
Name: "extends",
Kind: CommandLineOptionTypeString,
},
// For Watch options
"excludeDirectories": {
Name: "excludeDirectory",
Kind: CommandLineOptionTypeString,
IsFilePath: true,
extraValidation: extraValidationSpec,
},
"excludeFiles": {
Name: "excludeFile",
Kind: CommandLineOptionTypeString,
IsFilePath: true,
extraValidation: extraValidationSpec,
},
// Test infra options
"libFiles": {
Name: "libFiles",
Kind: CommandLineOptionTypeString,
},
}
// CommandLineOption.EnumMap()
var commandLineOptionEnumMap = map[string]*collections.OrderedMap[string, any]{
"lib": LibMap,
"moduleResolution": moduleResolutionOptionMap,
"module": moduleOptionMap,
"target": targetOptionMap,
"moduleDetection": moduleDetectionOptionMap,
"jsx": jsxOptionMap,
"newLine": newLineOptionMap,
"watchFile": watchFileEnumMap,
"watchDirectory": watchDirectoryEnumMap,
"fallbackPolling": fallbackEnumMap,
}
// CommandLineOption.DeprecatedKeys()
var commandLineOptionDeprecated = map[string]*collections.Set[string]{
"module": collections.NewSetFromItems("none", "amd", "system", "umd"),
"moduleResolution": collections.NewSetFromItems("node", "classic", "node10"),
"target": collections.NewSetFromItems("es5"),
}
// todo: revisit to see if this can be improved
type CompilerOptionsValue any

View File

@@ -0,0 +1,403 @@
package tsoptions
import (
"strconv"
"strings"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/collections"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/diagnostics"
"github.com/microsoft/typescript-go/internal/stringutil"
"github.com/microsoft/typescript-go/internal/tspath"
"github.com/microsoft/typescript-go/internal/vfs"
)
func (p *commandLineParser) AlternateMode() *AlternateModeDiagnostics {
return p.workerDiagnostics.didYouMean.alternateMode
}
func (p *commandLineParser) OptionsDeclarations() []*CommandLineOption {
return p.workerDiagnostics.didYouMean.OptionDeclarations
}
func (p *commandLineParser) UnknownOptionDiagnostic() *diagnostics.Message {
return p.workerDiagnostics.didYouMean.UnknownOptionDiagnostic
}
func (p *commandLineParser) UnknownDidYouMeanDiagnostic() *diagnostics.Message {
return p.workerDiagnostics.didYouMean.UnknownDidYouMeanDiagnostic
}
type commandLineParser struct {
workerDiagnostics *ParseCommandLineWorkerDiagnostics
optionsMap *NameMap
fs vfs.FS
currentDirectory string
options *collections.OrderedMap[string, any]
fileNames []string
errors []*ast.Diagnostic
}
func ParseCommandLine(
commandLine []string,
host ParseConfigHost,
) *ParsedCommandLine {
if commandLine == nil {
commandLine = []string{}
}
parser := parseCommandLineWorker(CompilerOptionsDidYouMeanDiagnostics, commandLine, host.FS(), host.GetCurrentDirectory())
optionsWithAbsolutePaths := convertToOptionsWithAbsolutePaths(parser.options.Clone(), CommandLineCompilerOptionsMap, host.GetCurrentDirectory())
compilerOptions := convertMapToOptions(optionsWithAbsolutePaths, &compilerOptionsParser{&core.CompilerOptions{}}).CompilerOptions
watchOptions := convertMapToOptions(optionsWithAbsolutePaths, &watchOptionsParser{&core.WatchOptions{}}).WatchOptions
result := NewParsedCommandLine(compilerOptions, parser.fileNames, tspath.ComparePathsOptions{
UseCaseSensitiveFileNames: host.FS().UseCaseSensitiveFileNames(),
CurrentDirectory: host.GetCurrentDirectory(),
})
result.ParsedConfig.WatchOptions = watchOptions
result.Errors = parser.errors
result.Raw = parser.options
return result
}
func ParseBuildCommandLine(
commandLine []string,
host ParseConfigHost,
) *ParsedBuildCommandLine {
if commandLine == nil {
commandLine = []string{}
}
parser := parseCommandLineWorker(buildOptionsDidYouMeanDiagnostics, commandLine, host.FS(), host.GetCurrentDirectory())
compilerOptions := &core.CompilerOptions{}
for key, value := range parser.options.Entries() {
buildOption := BuildNameMap.Get(key)
if buildOption == &TscBuildOption || buildOption == CompilerNameMap.Get(key) {
ParseCompilerOptions(key, value, compilerOptions)
}
}
result := &ParsedBuildCommandLine{
BuildOptions: convertMapToOptions(parser.options, &buildOptionsParser{&core.BuildOptions{}}).BuildOptions,
CompilerOptions: compilerOptions,
WatchOptions: convertMapToOptions(parser.options, &watchOptionsParser{&core.WatchOptions{}}).WatchOptions,
Projects: parser.fileNames,
Errors: parser.errors,
Raw: parser.options,
comparePathsOptions: tspath.ComparePathsOptions{
UseCaseSensitiveFileNames: host.FS().UseCaseSensitiveFileNames(),
CurrentDirectory: host.GetCurrentDirectory(),
},
}
if len(result.Projects) == 0 {
// tsc -b invoked with no extra arguments; act as if invoked with "tsc -b ."
result.Projects = append(result.Projects, ".")
}
// Nonsensical combinations
if result.BuildOptions.Clean.IsTrue() && result.BuildOptions.Force.IsTrue() {
result.Errors = append(result.Errors, ast.NewCompilerDiagnostic(diagnostics.Options_0_and_1_cannot_be_combined, "clean", "force"))
}
if result.BuildOptions.Clean.IsTrue() && result.BuildOptions.Verbose.IsTrue() {
result.Errors = append(result.Errors, ast.NewCompilerDiagnostic(diagnostics.Options_0_and_1_cannot_be_combined, "clean", "verbose"))
}
if result.BuildOptions.Clean.IsTrue() && result.CompilerOptions.Watch.IsTrue() {
result.Errors = append(result.Errors, ast.NewCompilerDiagnostic(diagnostics.Options_0_and_1_cannot_be_combined, "clean", "watch"))
}
if result.CompilerOptions.Watch.IsTrue() && result.BuildOptions.Dry.IsTrue() {
result.Errors = append(result.Errors, ast.NewCompilerDiagnostic(diagnostics.Options_0_and_1_cannot_be_combined, "watch", "dry"))
}
return result
}
func parseCommandLineWorker(
parseCommandLineWithDiagnostics *ParseCommandLineWorkerDiagnostics,
commandLine []string,
fs vfs.FS,
currentDirectory string,
) *commandLineParser {
parser := &commandLineParser{
fs: fs,
currentDirectory: currentDirectory,
workerDiagnostics: parseCommandLineWithDiagnostics,
fileNames: []string{},
options: &collections.OrderedMap[string, any]{},
errors: []*ast.Diagnostic{},
}
parser.optionsMap = GetNameMapFromList(parser.OptionsDeclarations())
parser.parseStrings(commandLine)
return parser
}
func (p *commandLineParser) parseStrings(args []string) {
i := 0
for i < len(args) {
s := args[i]
i++
if s == "" {
continue
}
switch s[0] {
case '@':
p.parseResponseFile(s[1:])
case '-':
inputOptionName := getInputOptionName(s)
opt := p.optionsMap.GetOptionDeclarationFromName(inputOptionName, true /*allowShort*/)
if opt != nil {
i = p.parseOptionValue(args, i, opt, p.workerDiagnostics.OptionTypeMismatchDiagnostic)
} else {
watchOpt := WatchNameMap.GetOptionDeclarationFromName(inputOptionName, true /*allowShort*/)
if watchOpt != nil {
i = p.parseOptionValue(args, i, watchOpt, watchOptionsDidYouMeanDiagnostics.OptionTypeMismatchDiagnostic)
} else {
p.errors = append(p.errors, p.createUnknownOptionError(inputOptionName, s, nil, nil))
}
}
default:
p.fileNames = append(p.fileNames, s)
}
}
}
func getInputOptionName(input string) string {
// removes at most two leading '-' from the input string
return strings.TrimPrefix(strings.TrimPrefix(input, "-"), "-")
}
func (p *commandLineParser) parseResponseFile(fileName string) {
fileName = tspath.GetNormalizedAbsolutePath(fileName, p.currentDirectory)
fileContents, errors := tryReadFile(fileName, func(fileName string) (string, bool) {
if p.fs == nil {
return "", false
}
read, err := p.fs.ReadFile(fileName)
return read, err
}, p.errors)
p.errors = errors
if fileContents == "" {
return
}
var args []string
text := []rune(fileContents)
textLength := len(text)
pos := 0
for pos < textLength {
for pos < textLength && text[pos] <= ' ' {
pos++
}
if pos >= textLength {
break
}
start := pos
if text[pos] == '"' {
pos++
for pos < textLength && text[pos] != '"' {
pos++
}
if pos < textLength {
args = append(args, string(text[start+1:pos]))
pos++
} else {
p.errors = append(p.errors, ast.NewCompilerDiagnostic(diagnostics.Unterminated_quoted_string_in_response_file_0, fileName))
}
} else {
for text[pos] > ' ' {
pos++
}
args = append(args, string(text[start:pos]))
}
}
p.parseStrings(args)
}
func tryReadFile(fileName string, readFile func(string) (string, bool), errors []*ast.Diagnostic) (string, []*ast.Diagnostic) {
// this function adds a compiler diagnostic if the file cannot be read
text, e := readFile(fileName)
if !e {
// !!! Divergence: the returned error will not give a useful message
// errors = append(errors, ast.NewCompilerDiagnostic(diagnostics.Cannot_read_file_0_Colon_1, *e));
text = ""
errors = append(errors, ast.NewCompilerDiagnostic(diagnostics.Cannot_read_file_0, fileName))
}
return text, errors
}
func (p *commandLineParser) parseOptionValue(
args []string,
i int,
opt *CommandLineOption,
diag *diagnostics.Message,
) int {
if opt.IsTSConfigOnly && i <= len(args) {
optValue := ""
if i < len(args) {
optValue = args[i]
}
if optValue == "null" {
p.options.Set(opt.Name, nil)
i++
} else if opt.Kind == "boolean" {
if optValue == "false" {
p.options.Set(opt.Name, false)
i++
} else {
if optValue == "true" {
i++
}
p.errors = append(p.errors, ast.NewCompilerDiagnostic(diagnostics.Option_0_can_only_be_specified_in_tsconfig_json_file_or_set_to_false_or_null_on_command_line, opt.Name))
}
} else {
p.errors = append(p.errors, ast.NewCompilerDiagnostic(diagnostics.Option_0_can_only_be_specified_in_tsconfig_json_file_or_set_to_null_on_command_line, opt.Name))
if len(optValue) != 0 && !strings.HasPrefix(optValue, "-") {
i++
}
}
} else {
// Check to see if no argument was provided (e.g. "--locale" is the last command-line argument).
if i >= len(args) {
if opt.Kind != "boolean" {
p.errors = append(p.errors, ast.NewCompilerDiagnostic(diag, opt.Name, getCompilerOptionValueTypeString(opt)))
if opt.Kind == "list" {
p.options.Set(opt.Name, []string{})
} else if opt.Kind == "enum" {
p.errors = append(p.errors, createDiagnosticForInvalidEnumType(opt, nil, nil))
}
} else {
p.options.Set(opt.Name, true)
}
return i
}
if args[i] != "null" {
switch opt.Kind {
case "number":
// !!! Make sure this parseInt matches JS parseInt
num, e := strconv.Atoi(args[i])
if e == nil {
if num >= opt.minValue {
p.options.Set(opt.Name, num)
} else {
p.errors = append(p.errors, ast.NewCompilerDiagnostic(diagnostics.Option_0_requires_value_to_be_greater_than_1, opt.Name, strconv.Itoa(opt.minValue)))
}
} else {
p.errors = append(p.errors, ast.NewCompilerDiagnostic(diag, opt.Name, "number"))
}
i++
case "boolean":
// boolean flag has optional value true, false, others
optValue := args[i]
// check next argument as boolean flag value
if optValue == "false" {
p.options.Set(opt.Name, false)
} else {
p.options.Set(opt.Name, true)
}
// try to consume next argument as value for boolean flag; do not consume argument if it is not "true" or "false"
if optValue == "false" || optValue == "true" {
i++
}
case "string":
val, err := validateJsonOptionValue(opt, args[i], nil, nil)
if err == nil {
p.options.Set(opt.Name, val)
} else {
p.errors = append(p.errors, err...)
}
i++
case "list":
result, err := p.parseListTypeOption(opt, args[i])
p.options.Set(opt.Name, result)
p.errors = append(p.errors, err...)
if len(result) > 0 || len(err) > 0 {
i++
}
case "listOrElement":
// If not a primitive, the possible types are specified in what is effectively a map of options.
panic("listOrElement not supported here")
default:
val, err := convertJsonOptionOfEnumType(opt, strings.TrimFunc(args[i], stringutil.IsWhiteSpaceLike), nil, nil)
p.options.Set(opt.Name, val)
p.errors = append(p.errors, err...)
i++
}
} else {
p.options.Set(opt.Name, nil)
i++
}
}
return i
}
func (p *commandLineParser) parseListTypeOption(opt *CommandLineOption, value string) ([]any, []*ast.Diagnostic) {
return ParseListTypeOption(opt, value)
}
func ParseListTypeOption(opt *CommandLineOption, value string) ([]any, []*ast.Diagnostic) {
value = strings.TrimSpace(value)
var errors []*ast.Diagnostic
if strings.HasPrefix(value, "-") {
return []any{}, errors
}
if opt.Kind == "listOrElement" && !strings.ContainsRune(value, ',') {
val, err := validateJsonOptionValue(opt, value, nil, nil)
if err != nil {
return []any{}, err
}
return []any{val.(string)}, errors
}
if value == "" {
return []any{}, errors
}
values := strings.Split(value, ",")
switch opt.Elements().Kind {
case "string":
elements := core.MapFiltered(values, func(v string) (any, bool) {
val, err := validateJsonOptionValue(opt.Elements(), v, nil, nil)
if s, ok := val.(string); ok && len(err) == 0 && s != "" {
return s, true
}
errors = append(errors, err...)
return "", false
})
return elements, errors
case "boolean", "object", "number":
// do nothing: only string and enum/object types currently allowed as list entries
// !!! we don't actually have number list options, so I didn't implement number list parsing
panic("List of " + opt.Elements().Kind + " is not yet supported.")
default:
result := core.MapFiltered(values, func(v string) (any, bool) {
val, err := convertJsonOptionOfEnumType(opt.Elements(), strings.TrimFunc(v, stringutil.IsWhiteSpaceLike), nil, nil)
if s, ok := val.(string); ok && len(err) == 0 && s != "" {
return s, true
}
errors = append(errors, err...)
return "", false
})
return result, errors
}
}
func convertJsonOptionOfEnumType(
opt *CommandLineOption,
value string,
valueExpression *ast.Expression,
sourceFile *ast.SourceFile,
) (any, []*ast.Diagnostic) {
if value == "" {
return nil, nil
}
key := strings.ToLower(value)
typeMap := opt.EnumMap()
if typeMap == nil {
return nil, nil
}
val, ok := typeMap.Get(key)
if ok {
return validateJsonOptionValue(opt, val, valueExpression, sourceFile)
}
return nil, []*ast.Diagnostic{createDiagnosticForInvalidEnumType(opt, sourceFile, valueExpression)}
}

View File

@@ -0,0 +1,572 @@
package tsoptions_test
import (
"path/filepath"
"slices"
"strings"
"testing"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/microsoft/typescript-go/internal/collections"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/diagnostics"
"github.com/microsoft/typescript-go/internal/diagnosticwriter"
"github.com/microsoft/typescript-go/internal/json"
"github.com/microsoft/typescript-go/internal/repo"
"github.com/microsoft/typescript-go/internal/testutil/baseline"
"github.com/microsoft/typescript-go/internal/testutil/filefixture"
"github.com/microsoft/typescript-go/internal/tsoptions"
"github.com/microsoft/typescript-go/internal/tsoptions/tsoptionstest"
"github.com/microsoft/typescript-go/internal/tspath"
"github.com/microsoft/typescript-go/internal/vfs/osvfs"
"gotest.tools/v3/assert"
)
func TestCommandLineParseResult(t *testing.T) {
t.Parallel()
repo.SkipIfNoTypeScriptSubmodule(t)
parseCommandLineSubScenarios := []*subScenarioInput{
// --lib es6 0.ts
{"Parse single option of library flag", []string{"--lib", "es6", "0.ts"}},
{"Handles may only be used with --build flags", []string{"--build", "--clean", "--dry", "--force", "--verbose"}},
// --declarations --allowTS
{"Handles did you mean for misspelt flags", []string{"--declarations", "--allowTS"}},
// --lib es5,es2015.symbol.wellknown 0.ts
{"Parse multiple options of library flags", []string{"--lib", "es5,es2015.symbol.wellknown", "0.ts"}},
// --lib es5,invalidOption 0.ts
{"Parse invalid option of library flags", []string{"--lib", "es5,invalidOption", "0.ts"}},
// 0.ts --jsx
{"Parse empty options of --jsx", []string{"0.ts", "--jsx"}},
// 0.ts --
{"Parse empty options of --module", []string{"0.ts", "--module"}},
// 0.ts --newLine
{"Parse empty options of --newLine", []string{"0.ts", "--newLine"}},
// 0.ts --target
{"Parse empty options of --target", []string{"0.ts", "--target"}},
// 0.ts --moduleResolution
{"Parse empty options of --moduleResolution", []string{"0.ts", "--moduleResolution"}},
// 0.ts --lib
{"Parse empty options of --lib", []string{"0.ts", "--lib"}},
// 0.ts --lib
// This test is an error because the empty string is falsey
{"Parse empty string of --lib", []string{"0.ts", "--lib", ""}},
// 0.ts --lib
{"Parse immediately following command line argument of --lib", []string{"0.ts", "--lib", "--sourcemap"}},
// --lib es5, es7 0.ts
{"Parse --lib option with extra comma", []string{"--lib", "es5,", "es7", "0.ts"}},
// --lib es5, es7 0.ts
{"Parse --lib option with trailing white-space", []string{"--lib", "es5, ", "es7", "0.ts"}},
// --lib es5,es2015.symbol.wellknown --target es5 0.ts
{"Parse multiple compiler flags with input files at the end", []string{"--lib", "es5,es2015.symbol.wellknown", "--target", "es5", "0.ts"}},
// --module commonjs --target es5 0.ts --lib es5,es2015.symbol.wellknown
{"Parse multiple compiler flags with input files in the middle", []string{"--module", "commonjs", "--target", "es5", "0.ts", "--lib", "es5,es2015.symbol.wellknown"}},
// --module commonjs --target es5 --lib es5 0.ts --library es2015.array,es2015.symbol.wellknown
{"Parse multiple library compiler flags ", []string{"--module", "commonjs", "--target", "es5", "--lib", "es5", "0.ts", "--lib", "es2015.core, es2015.symbol.wellknown "}},
{"Parse explicit boolean flag value", []string{"--strictNullChecks", "false", "0.ts"}},
{"Parse non boolean argument after boolean flag", []string{"--noImplicitAny", "t", "0.ts"}},
{"Parse implicit boolean flag value", []string{"--strictNullChecks"}},
{"parse --incremental", []string{"--incremental", "0.ts"}},
{"parse --tsBuildInfoFile", []string{"--tsBuildInfoFile", "build.tsbuildinfo", "0.ts"}},
{"allows tsconfig only option to be set to null", []string{"--composite", "null", "-tsBuildInfoFile", "null", "0.ts"}},
// ****** Watch Options ******
{"parse --watchFile", []string{"--watchFile", "UseFsEvents", "0.ts"}},
{"parse --watchDirectory", []string{"--watchDirectory", "FixedPollingInterval", "0.ts"}},
{"parse --fallbackPolling", []string{"--fallbackPolling", "PriorityInterval", "0.ts"}},
{"parse --synchronousWatchDirectory", []string{"--synchronousWatchDirectory", "0.ts"}},
{"errors on missing argument to --fallbackPolling", []string{"0.ts", "--fallbackPolling"}},
{"parse --excludeDirectories", []string{"--excludeDirectories", "**/temp", "0.ts"}},
{"errors on invalid excludeDirectories", []string{"--excludeDirectories", "**/../*", "0.ts"}},
{"parse --excludeFiles", []string{"--excludeFiles", "**/temp/*.ts", "0.ts"}},
{"errors on invalid excludeFiles", []string{"--excludeFiles", "**/../*", "0.ts"}},
}
for _, testCase := range parseCommandLineSubScenarios {
testCase.createSubScenario("parseCommandLine").assertParseResult(t)
}
}
func TestResponseFileDoesNotPanic(t *testing.T) {
t.Parallel()
// Passing `@` with an empty or relative filename should not panic.
// It should produce a diagnostic error instead.
cwd := t.TempDir()
t.Run("empty response file", func(t *testing.T) {
t.Parallel()
parsed := tsoptions.ParseCommandLineTestWorker(nil, []string{"@"}, osvfs.FS(), cwd)
assert.Assert(t, len(parsed.Errors) > 0, "expected an error for empty response file name")
})
t.Run("relative response file", func(t *testing.T) {
t.Parallel()
parsed := tsoptions.ParseCommandLineTestWorker(nil, []string{"@blah"}, osvfs.FS(), cwd)
assert.Assert(t, len(parsed.Errors) > 0, "expected an error for non-existent response file")
})
}
func TestParseCommandLineTypeRootsRelativePath(t *testing.T) {
t.Parallel()
host := tsoptionstest.NewVFSParseConfigHost(map[string]string{
"/home/project/bug.ts": `let x = 1;`,
}, "/home/project", true)
cmdLine := tsoptions.ParseCommandLine([]string{"--typeRoots", "t", "bug.ts"}, host)
typeRoots := cmdLine.CompilerOptions().TypeRoots
assert.Assert(t, typeRoots != nil, "typeRoots should not be nil")
assert.Equal(t, len(typeRoots), 1)
assert.Assert(t, tspath.IsRootedDiskPath(typeRoots[0]), "typeRoots entry should be an absolute path, got: %s", typeRoots[0])
assert.Assert(t, strings.HasSuffix(typeRoots[0], "/t"), "typeRoots entry should end with '/t', got: %s", typeRoots[0])
}
func TestCustomConditionsNullOverride(t *testing.T) {
t.Parallel()
files := map[string]string{
"/project/tsconfig.json": `{
"compilerOptions": {
"customConditions": ["condition1", "condition2"]
}
}`,
"/project/index.ts": `console.log("Hello, World!");`,
}
host := tsoptionstest.NewVFSParseConfigHost(files, "/project", true)
// Parse command line with --customConditions null
cmdLine := tsoptions.ParseCommandLine([]string{"--project", "/project", "--customConditions", "null"}, host)
// Check that the raw options contain null for customConditions
if rawMap, ok := cmdLine.Raw.(*collections.OrderedMap[string, any]); ok {
customConditionsRaw, exists := rawMap.Get("customConditions")
assert.Assert(t, exists, "customConditions should exist in raw options")
assert.Assert(t, customConditionsRaw == nil, "customConditions should be nil in raw options, got: %v", customConditionsRaw)
} else {
t.Fatal("Raw options should be an OrderedMap")
}
// Now parse the config file with the command line options
// Wrap command line options in "compilerOptions" key to match tsconfig.json structure
wrappedRaw := &collections.OrderedMap[string, any]{}
wrappedRaw.Set("compilerOptions", cmdLine.Raw.(*collections.OrderedMap[string, any]))
parsedConfig, errors := tsoptions.GetParsedCommandLineOfConfigFile(
"/project/tsconfig.json",
cmdLine.CompilerOptions(),
wrappedRaw,
host,
nil,
)
assert.Assert(t, len(errors) == 0, "Should not have errors: %v", errors)
// Check that customConditions is nil (overridden by command line)
customConditions := parsedConfig.CompilerOptions().CustomConditions
assert.Assert(t, customConditions == nil, "customConditions should be nil after override, got: %v", customConditions)
}
func TestParseCommandLineVerifyNull(t *testing.T) {
t.Parallel()
repo.SkipIfNoTypeScriptSubmodule(t)
// run test for boolean
subScenarioInput{"allows setting option type boolean to false", []string{"--composite", "false", "0.ts"}}.createSubScenario("parseCommandLine").assertParseResult(t)
verifyNullSubScenarios := []verifyNull{
{
subScenario: "option of type boolean",
optionName: "composite",
nonNullValue: "true",
},
{
subScenario: "option of type object",
optionName: "paths",
},
{
subScenario: "option of type list",
optionName: "rootDirs",
nonNullValue: "abc,xyz",
},
createVerifyNullForNonNullIncluded("option of type string", tsoptions.CommandLineOptionTypeString, "hello"),
createVerifyNullForNonNullIncluded("option of type number", tsoptions.CommandLineOptionTypeNumber, "10"),
// todo: make the following work for tests -- currently it is difficult to do extra options of enum type
// createVerifyNullForNonNullIncluded("option of type custom map", CommandLineOptionTypeEnum, "node"),
}
for _, verifyNullCase := range verifyNullSubScenarios {
createSubScenario(
"parseCommandLine",
verifyNullCase.subScenario+" allows setting it to null",
[]string{"--" + verifyNullCase.optionName, "null", "0.ts"},
verifyNullCase.optDecls,
).assertParseResult(t)
if verifyNullCase.nonNullValue != "" {
createSubScenario(
"parseCommandLine",
verifyNullCase.subScenario+" errors if non null value is passed",
[]string{"--" + verifyNullCase.optionName, verifyNullCase.nonNullValue, "0.ts"},
verifyNullCase.optDecls,
).assertParseResult(t)
}
createSubScenario(
"parseCommandLine",
verifyNullCase.subScenario+" errors if its followed by another option",
[]string{"0.ts", "--strictNullChecks", "--" + verifyNullCase.optionName},
verifyNullCase.optDecls,
).assertParseResult(t)
createSubScenario(
"parseCommandLine",
verifyNullCase.subScenario+" errors if its last option",
[]string{"0.ts", "--" + verifyNullCase.optionName},
verifyNullCase.optDecls,
).assertParseResult(t)
}
}
func createVerifyNullForNonNullIncluded(subScenario string, kind tsoptions.CommandLineOptionKind, nonNullValue string) verifyNull {
return verifyNull{
subScenario: subScenario,
optionName: "optionName",
nonNullValue: nonNullValue,
optDecls: slices.Concat(tsoptions.OptionsDeclarations, []*tsoptions.CommandLineOption{{
Name: "optionName",
Kind: kind,
IsTSConfigOnly: true,
Category: diagnostics.Backwards_Compatibility,
Description: diagnostics.Enable_project_compilation,
DefaultValueDescription: nil,
}}),
}
}
func (f commandLineSubScenario) assertParseResult(t *testing.T) {
t.Helper()
t.Run(f.testName, func(t *testing.T) {
t.Parallel()
originalBaseline := f.baseline.ReadFile(t)
tsBaseline := parseExistingCompilerBaseline(t, originalBaseline)
// f.workerDiagnostic is either defined or set to default pointer in `createSubScenario`
parsed := tsoptions.ParseCommandLineTestWorker(f.optDecls, f.commandLine, osvfs.FS(), t.TempDir())
newBaselineFileNames := strings.Join(parsed.FileNames, ",")
assert.Equal(t, tsBaseline.fileNames, newBaselineFileNames)
o, _ := json.Marshal(parsed.Options)
newParsedCompilerOptions := &core.CompilerOptions{}
e := json.Unmarshal(o, newParsedCompilerOptions)
assert.NilError(t, e)
assert.DeepEqual(t, tsBaseline.options, newParsedCompilerOptions, cmpopts.IgnoreUnexported(core.CompilerOptions{}))
newParsedWatchOptions := core.WatchOptions{}
e = json.Unmarshal(o, &newParsedWatchOptions)
assert.NilError(t, e)
// !!! useful for debugging but will not pass due to `none` as enum options
// assert.DeepEqual(t, tsBaseline.watchoptions, newParsedWatchOptions)
var formattedErrors strings.Builder
diagnosticwriter.WriteFormatDiagnostics(&formattedErrors, diagnosticwriter.FromASTDiagnostics(parsed.Errors), &diagnosticwriter.FormattingOptions{NewLine: "\n"})
newBaselineErrors := formattedErrors.String()
// !!!
// useful for debugging--compares the new errors with the old errors. currently will NOT pass because of unimplemented options, not completely identical enum options, etc
// assert.Equal(t, tsBaseline.errors, newBaselineErrors)
baseline.Run(t, f.testName+".js", formatNewBaseline(f.commandLine, o, newBaselineFileNames, newBaselineErrors), baseline.Options{Subfolder: "tsoptions/commandLineParsing"})
})
}
func parseExistingCompilerBaseline(t *testing.T, baseline string) *TestCommandLineParser {
_, rest, _ := strings.Cut(baseline, "CompilerOptions::\n")
compilerOptions, rest, watchFound := strings.Cut(rest, "\nWatchOptions::\n")
watchOptions, rest, _ := strings.Cut(rest, "\nFileNames::\n")
fileNames, errors, _ := strings.Cut(rest, "\nErrors::\n")
baselineCompilerOptions := &core.CompilerOptions{}
e := json.Unmarshal([]byte(compilerOptions), &baselineCompilerOptions)
assert.NilError(t, e)
baselineWatchOptions := &core.WatchOptions{}
if watchFound && watchOptions != "" {
e2 := json.Unmarshal([]byte(watchOptions), &baselineWatchOptions)
assert.NilError(t, e2)
}
return &TestCommandLineParser{
options: baselineCompilerOptions,
watchoptions: baselineWatchOptions,
fileNames: fileNames,
errors: errors,
}
}
func formatNewBaseline(
commandLine []string,
opts []byte,
fileNames string,
errors string,
) string {
var formatted strings.Builder
formatted.WriteString("Args::\n")
formatted.WriteByte('[')
for i, arg := range commandLine {
if i > 0 {
formatted.WriteString(", ")
}
formatted.WriteByte('"')
formatted.WriteString(arg)
formatted.WriteByte('"')
}
formatted.WriteByte(']')
formatted.WriteString("\n\nCompilerOptions::\n")
formatted.Write(opts)
// todo: watch options not implemented
// formatted.WriteString("WatchOptions::\n")
formatted.WriteString("\n\nFileNames::\n")
formatted.WriteString(fileNames)
formatted.WriteString("\n\nErrors::\n")
formatted.WriteString(errors)
return formatted.String()
}
func (f commandLineSubScenario) assertBuildParseResult(t *testing.T) {
t.Helper()
f.assertBuildParseResultWithTsBaseline(t, func() *TestCommandLineParserBuild {
originalBaseline := f.baseline.ReadFile(t)
return parseExistingCompilerBaselineBuild(t, originalBaseline)
})
}
func (f commandLineSubScenario) assertBuildParseResultWithTsBaseline(t *testing.T, getTsBaseline func() *TestCommandLineParserBuild) {
t.Helper()
t.Run(f.testName, func(t *testing.T) {
t.Parallel()
var tsBaseline *TestCommandLineParserBuild
if getTsBaseline != nil {
tsBaseline = getTsBaseline()
}
// f.workerDiagnostic is either defined or set to default pointer in `createSubScenario`
parsed := tsoptions.ParseBuildCommandLine(f.commandLine, &tsoptionstest.VfsParseConfigHost{
Vfs: osvfs.FS(),
CurrentDirectory: tspath.NormalizeSlashes(repo.TypeScriptSubmodulePath()),
})
newBaselineProjects := strings.Join(parsed.Projects, ",")
if getTsBaseline != nil {
assert.Equal(t, tsBaseline.projects, newBaselineProjects)
}
o, _ := json.Marshal(parsed.BuildOptions)
newParsedBuildOptions := &core.BuildOptions{}
e := json.Unmarshal(o, newParsedBuildOptions)
assert.NilError(t, e)
if getTsBaseline != nil {
assert.DeepEqual(t, tsBaseline.options, newParsedBuildOptions, cmpopts.IgnoreUnexported(core.BuildOptions{}))
}
compilerOpts, _ := json.Marshal(parsed.CompilerOptions)
newParsedCompilerOptions := &core.CompilerOptions{}
e = json.Unmarshal(compilerOpts, newParsedCompilerOptions)
assert.NilError(t, e)
if getTsBaseline != nil {
assert.DeepEqual(t, tsBaseline.compilerOptions, newParsedCompilerOptions, cmpopts.IgnoreUnexported(core.CompilerOptions{}))
}
newParsedWatchOptions := core.WatchOptions{}
e = json.Unmarshal(o, &newParsedWatchOptions)
assert.NilError(t, e)
// !!! useful for debugging but will not pass due to `none` as enum options
// assert.DeepEqual(t, tsBaseline.watchoptions, newParsedWatchOptions)
var formattedErrors strings.Builder
diagnosticwriter.WriteFormatDiagnostics(&formattedErrors, diagnosticwriter.FromASTDiagnostics(parsed.Errors), &diagnosticwriter.FormattingOptions{NewLine: "\n"})
newBaselineErrors := formattedErrors.String()
// !!!
// useful for debugging--compares the new errors with the old errors. currently will NOT pass because of unimplemented options, not completely identical enum options, etc
// assert.Equal(t, tsBaseline.errors, newBaselineErrors)
baseline.Run(t, f.testName+".js", formatNewBaselineBuild(f.commandLine, o, compilerOpts, newBaselineProjects, newBaselineErrors), baseline.Options{Subfolder: "tsoptions/commandLineParsing"})
})
}
func parseExistingCompilerBaselineBuild(t *testing.T, baseline string) *TestCommandLineParserBuild {
_, rest, _ := strings.Cut(baseline, "buildOptions::\n")
buildOptions, rest, watchFound := strings.Cut(rest, "\nWatchOptions::\n")
watchOptions, rest, _ := strings.Cut(rest, "\nProjects::\n")
projects, errors, _ := strings.Cut(rest, "\nErrors::\n")
baselineBuildOptions := &core.BuildOptions{}
e := json.Unmarshal([]byte(buildOptions), &baselineBuildOptions)
assert.NilError(t, e)
baselineCompilerOptions := &core.CompilerOptions{}
e = json.Unmarshal([]byte(buildOptions), &baselineCompilerOptions)
assert.NilError(t, e)
baselineWatchOptions := &core.WatchOptions{}
if watchFound && watchOptions != "" {
e2 := json.Unmarshal([]byte(watchOptions), &baselineWatchOptions)
assert.NilError(t, e2)
}
return &TestCommandLineParserBuild{
options: baselineBuildOptions,
compilerOptions: baselineCompilerOptions,
watchoptions: baselineWatchOptions,
projects: projects,
errors: errors,
}
}
func formatNewBaselineBuild(
commandLine []string,
opts []byte,
compilerOpts []byte,
projects string,
errors string,
) string {
var formatted strings.Builder
formatted.WriteString("Args::\n")
formatted.WriteByte('[')
for i, arg := range commandLine {
if i > 0 {
formatted.WriteString(", ")
}
formatted.WriteByte('"')
formatted.WriteString(arg)
formatted.WriteByte('"')
}
formatted.WriteByte(']')
formatted.WriteString("\n\nbuildOptions::\n")
formatted.Write(opts)
formatted.WriteString("\n\ncompilerOptions::\n")
formatted.Write(compilerOpts)
// todo: watch options not implemented
// formatted.WriteString("WatchOptions::\n")
formatted.WriteString("\n\nProjects::\n")
formatted.WriteString(projects)
formatted.WriteString("\n\nErrors::\n")
formatted.WriteString(errors)
return formatted.String()
}
func createSubScenario(scenarioKind string, subScenarioName string, commandline []string, opts ...[]*tsoptions.CommandLineOption) *commandLineSubScenario {
subScenarioName = scenarioKind + "/" + subScenarioName
baselineFileName := "tests/baselines/reference/config/commandLineParsing/" + subScenarioName + ".js"
result := &commandLineSubScenario{
filefixture.FromFile(subScenarioName, filepath.Join(repo.TypeScriptSubmodulePath(), baselineFileName)),
subScenarioName,
commandline,
nil,
}
if len(opts) > 0 {
result.optDecls = opts[0]
}
return result
}
type subScenarioInput struct {
name string
commandLineArgs []string
}
func (f subScenarioInput) createSubScenario(scenarioKind string) *commandLineSubScenario {
return createSubScenario(scenarioKind, f.name, f.commandLineArgs)
}
type commandLineSubScenario struct {
baseline filefixture.Fixture
testName string
commandLine []string
optDecls []*tsoptions.CommandLineOption
}
type verifyNull struct {
subScenario string
optionName string
nonNullValue string
optDecls []*tsoptions.CommandLineOption
}
type TestCommandLineParser struct {
options *core.CompilerOptions
watchoptions *core.WatchOptions
fileNames, errors string
}
type TestCommandLineParserBuild struct {
options *core.BuildOptions
compilerOptions *core.CompilerOptions
watchoptions *core.WatchOptions
projects, errors string
}
func TestParseBuildCommandLine(t *testing.T) {
t.Parallel()
repo.SkipIfNoTypeScriptSubmodule(t)
parseCommandLineSubScenarios := []*subScenarioInput{
{"parse build without any options ", []string{}},
{"Parse multiple options", []string{"--verbose", "--force", "tests"}},
{"Parse option with invalid option", []string{"--verbose", "--invalidOption"}},
{"Parse multiple flags with input projects at the end", []string{"--force", "--verbose", "src", "tests"}},
{"Parse multiple flags with input projects in the middle", []string{"--force", "src", "tests", "--verbose"}},
{"Parse multiple flags with input projects in the beginning", []string{"src", "tests", "--force", "--verbose"}},
{"parse build with --incremental", []string{"--incremental", "tests"}},
{"parse build with --locale en-us", []string{"--locale", "en-us", "src"}},
{"parse build with --tsBuildInfoFile", []string{"--tsBuildInfoFile", "build.tsbuildinfo", "tests"}},
{"reports other common may not be used with --build flags", []string{"--strict"}},
{`--clean and --force together is invalid`, []string{"--clean", "--force"}},
{`--clean and --verbose together is invalid`, []string{"--clean", "--verbose"}},
{`--clean and --watch together is invalid`, []string{"--clean", "--watch"}},
{`--watch and --dry together is invalid`, []string{"--watch", "--dry"}},
{"parse --watchFile", []string{"--watchFile", "UseFsEvents", "--verbose"}},
{"parse --watchDirectory", []string{"--watchDirectory", "FixedPollingInterval", "--verbose"}},
{"parse --fallbackPolling", []string{"--fallbackPolling", "PriorityInterval", "--verbose"}},
{"parse --synchronousWatchDirectory", []string{"--synchronousWatchDirectory", "--verbose"}},
{"errors on missing argument", []string{"--verbose", "--fallbackPolling"}},
{"errors on invalid excludeDirectories", []string{"--excludeDirectories", "**/../*"}},
{"parse --excludeFiles", []string{"--excludeFiles", "**/temp/*.ts"}},
{"errors on invalid excludeFiles", []string{"--excludeFiles", "**/../*"}},
}
for _, testCase := range parseCommandLineSubScenarios {
testCase.createSubScenario("parseBuildOptions").assertBuildParseResult(t)
}
extraScenarios := []*subScenarioInput{
{`parse --builders`, []string{"--builders", "2"}},
{`--singleThreaded and --builders together`, []string{"--singleThreaded", "--builders", "2"}},
{`reports error when --builders is 0`, []string{"--builders", "0"}},
{`reports error when --builders is negative`, []string{"--builders", "-1"}},
{`reports error when --builders is invalid type`, []string{"--builders", "invalid"}},
}
for _, testCase := range extraScenarios {
testCase.createSubScenario("parseBuildOptions").assertBuildParseResultWithTsBaseline(t, nil)
}
}
func TestAffectsBuildInfo(t *testing.T) {
t.Parallel()
t.Run("should have affectsBuildInfo true for every option with affectsSemanticDiagnostics", func(t *testing.T) {
t.Parallel()
for _, option := range tsoptions.OptionsDeclarations {
if option.AffectsSemanticDiagnostics {
// semantic diagnostics affect the build info, so ensure they're included
assert.Assert(t, option.AffectsBuildInfo)
}
}
})
}

View File

@@ -0,0 +1,79 @@
package tsoptions_test
import (
"reflect"
"strings"
"testing"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/tsoptions"
)
func TestCompilerOptionsDeclaration(t *testing.T) {
t.Parallel()
decls := make(map[string]*tsoptions.CommandLineOption)
for _, decl := range tsoptions.OptionsDeclarations {
decls[strings.ToLower(decl.Name)] = decl
}
internalOptions := []string{
"allowNonTsExtensions",
"build",
"configFilePath",
"noDtsResolution",
"noEmitForJsFiles",
"pathsBasePath",
"suppressOutputPathCheck",
"build",
}
internalOptionsMap := make(map[string]string)
for _, opt := range internalOptions {
internalOptionsMap[strings.ToLower(opt)] = opt
}
compilerOptionsType := reflect.TypeFor[core.CompilerOptions]()
for field := range compilerOptionsType.Fields() {
if !field.IsExported() {
continue
}
lowerName := strings.ToLower(field.Name)
decl := decls[lowerName]
if decl == nil {
if name, ok := internalOptionsMap[lowerName]; ok {
checkCompilerOptionJsonTagName(t, field, name)
continue
}
t.Errorf("CompilerOptions.%s has no options declaration", field.Name)
continue
}
delete(decls, lowerName)
checkCompilerOptionJsonTagName(t, field, decl.Name)
}
skippedOptions := []string{
"plugins",
}
for _, opt := range skippedOptions {
delete(decls, strings.ToLower(opt))
}
for _, decl := range decls {
t.Errorf("Option declaration %s is not present in CompilerOptions", decl.Name)
}
}
func checkCompilerOptionJsonTagName(t *testing.T, field reflect.StructField, name string) {
t.Helper()
want := name + ",omitzero"
got := field.Tag.Get("json")
if got != want {
t.Errorf("Field %s has json tag %s, but the option declaration has name %s", field.Name, got, want)
}
}

View File

@@ -0,0 +1,69 @@
package tsoptions
import (
"slices"
"github.com/microsoft/typescript-go/internal/diagnostics"
)
var TscBuildOption = CommandLineOption{
Name: "build",
Kind: "boolean",
ShortName: "b",
ShowInSimplifiedHelpView: true,
Category: diagnostics.Command_line_Options,
Description: diagnostics.Build_one_or_more_projects_and_their_dependencies_if_out_of_date,
DefaultValueDescription: false,
}
var OptionsForBuild = []*CommandLineOption{
&TscBuildOption,
{
Name: "verbose",
ShortName: "v",
Category: diagnostics.Command_line_Options,
Description: diagnostics.Enable_verbose_logging,
Kind: "boolean",
DefaultValueDescription: false,
},
{
Name: "dry",
ShortName: "d",
Category: diagnostics.Command_line_Options,
Description: diagnostics.Show_what_would_be_built_or_deleted_if_specified_with_clean,
Kind: "boolean",
DefaultValueDescription: false,
},
{
Name: "force",
ShortName: "f",
Category: diagnostics.Command_line_Options,
Description: diagnostics.Build_all_projects_including_those_that_appear_to_be_up_to_date,
Kind: "boolean",
DefaultValueDescription: false,
},
{
Name: "clean",
Category: diagnostics.Command_line_Options,
Description: diagnostics.Delete_the_outputs_of_all_projects,
Kind: "boolean",
DefaultValueDescription: false,
},
{
Name: "builders",
Kind: CommandLineOptionTypeNumber,
Category: diagnostics.Command_line_Options,
Description: diagnostics.Set_the_number_of_projects_to_build_concurrently,
DefaultValueDescription: diagnostics.X_4_unless_singleThreaded_is_passed,
minValue: 1,
},
{
Name: "stopBuildOnErrors",
Category: diagnostics.Command_line_Options,
Description: diagnostics.Skip_building_downstream_projects_on_error_in_upstream_project,
Kind: "boolean",
DefaultValueDescription: false,
},
}
var BuildOpts = slices.Concat(commonOptionsWithBuild, OptionsForBuild)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,29 @@
package tsoptions
var typeAcquisitionDeclaration = &CommandLineOption{
Name: "typeAcquisition",
Kind: CommandLineOptionTypeObject,
ElementOptions: commandLineOptionsToMap(typeAcquisitionDecls),
}
// Do not delete this without updating the website's tsconfig generation.
var typeAcquisitionDecls = []*CommandLineOption{
{
Name: "enable",
Kind: CommandLineOptionTypeBoolean,
DefaultValueDescription: false,
},
{
Name: "include",
Kind: CommandLineOptionTypeList,
},
{
Name: "exclude",
Kind: CommandLineOptionTypeList,
},
{
Name: "disableFilenameBasedTypeAcquisition",
Kind: CommandLineOptionTypeBoolean,
DefaultValueDescription: false,
},
}

View File

@@ -0,0 +1,88 @@
package tsoptions
import (
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/diagnostics"
)
var OptionsForWatch = []*CommandLineOption{
{
Name: "watchInterval",
Kind: CommandLineOptionTypeNumber,
Category: diagnostics.Watch_and_Build_Modes,
},
{
Name: "watchFile",
Kind: CommandLineOptionTypeEnum,
// new Map(Object.entries({
// fixedpollinginterval: WatchFileKind.FixedPollingInterval,
// prioritypollinginterval: WatchFileKind.PriorityPollingInterval,
// dynamicprioritypolling: WatchFileKind.DynamicPriorityPolling,
// fixedchunksizepolling: WatchFileKind.FixedChunkSizePolling,
// usefsevents: WatchFileKind.UseFsEvents,
// usefseventsonparentdirectory: WatchFileKind.UseFsEventsOnParentDirectory,
// })),
Category: diagnostics.Watch_and_Build_Modes,
Description: diagnostics.Specify_how_the_TypeScript_watch_mode_works,
DefaultValueDescription: core.WatchFileKindUseFsEvents,
},
{
Name: "watchDirectory",
Kind: CommandLineOptionTypeEnum,
// new Map(Object.entries({
// usefsevents: WatchDirectoryKind.UseFsEvents,
// fixedpollinginterval: WatchDirectoryKind.FixedPollingInterval,
// dynamicprioritypolling: WatchDirectoryKind.DynamicPriorityPolling,
// fixedchunksizepolling: WatchDirectoryKind.FixedChunkSizePolling,
// })),
Category: diagnostics.Watch_and_Build_Modes,
Description: diagnostics.Specify_how_directories_are_watched_on_systems_that_lack_recursive_file_watching_functionality,
DefaultValueDescription: core.WatchDirectoryKindUseFsEvents,
},
{
Name: "fallbackPolling",
Kind: CommandLineOptionTypeEnum,
// new Map(Object.entries({
// fixedinterval: PollingWatchKind.FixedInterval,
// priorityinterval: PollingWatchKind.PriorityInterval,
// dynamicpriority: PollingWatchKind.DynamicPriority,
// fixedchunksize: PollingWatchKind.FixedChunkSize,
// })),
Category: diagnostics.Watch_and_Build_Modes,
Description: diagnostics.Specify_what_approach_the_watcher_should_use_if_the_system_runs_out_of_native_file_watchers,
DefaultValueDescription: core.PollingKindPriorityInterval,
},
{
Name: "synchronousWatchDirectory",
Kind: CommandLineOptionTypeBoolean,
Category: diagnostics.Watch_and_Build_Modes,
Description: diagnostics.Synchronously_call_callbacks_and_update_the_state_of_directory_watchers_on_platforms_that_don_t_support_recursive_watching_natively,
DefaultValueDescription: false,
},
{
Name: "excludeDirectories",
Kind: CommandLineOptionTypeList,
// element: {
// Name: "excludeDirectory",
// Kind: "string",
// isFilePath: true,
// extraValidation: specToDiagnostic,
// },
allowConfigDirTemplateSubstitution: true,
Category: diagnostics.Watch_and_Build_Modes,
Description: diagnostics.Remove_a_list_of_directories_from_the_watch_process,
},
{
Name: "excludeFiles",
Kind: CommandLineOptionTypeList,
// element: {
// Name: "excludeFile",
// Kind: "string",
// isFilePath: true,
// extraValidation: specToDiagnostic,
// },
allowConfigDirTemplateSubstitution: true,
Category: diagnostics.Watch_and_Build_Modes,
Description: diagnostics.Remove_a_list_of_files_from_the_watch_mode_s_processing,
},
}

View File

@@ -0,0 +1,67 @@
package tsoptions
import (
"sync"
"github.com/microsoft/typescript-go/internal/diagnostics"
)
type DidYouMeanOptionsDiagnostics struct {
alternateMode *AlternateModeDiagnostics
OptionDeclarations []*CommandLineOption
UnknownOptionDiagnostic *diagnostics.Message
UnknownDidYouMeanDiagnostic *diagnostics.Message
}
type AlternateModeDiagnostics struct {
diagnostic *diagnostics.Message
optionsNameMap *NameMap
}
type ParseCommandLineWorkerDiagnostics struct {
didYouMean DidYouMeanOptionsDiagnostics
optionsNameMap *NameMap
optionsNameMapOnce sync.Once
OptionTypeMismatchDiagnostic *diagnostics.Message
}
var CompilerOptionsDidYouMeanDiagnostics = getParseCommandLineWorkerDiagnostics(OptionsDeclarations)
func getParseCommandLineWorkerDiagnostics(decls []*CommandLineOption) *ParseCommandLineWorkerDiagnostics {
// this will only return the correct diagnostics for `compiler` mode, and is factored into a function for testing reasons.
return &ParseCommandLineWorkerDiagnostics{
didYouMean: DidYouMeanOptionsDiagnostics{
alternateMode: &AlternateModeDiagnostics{
diagnostic: diagnostics.Compiler_option_0_may_only_be_used_with_build,
optionsNameMap: BuildNameMap,
},
OptionDeclarations: decls,
UnknownOptionDiagnostic: diagnostics.Unknown_compiler_option_0,
UnknownDidYouMeanDiagnostic: diagnostics.Unknown_compiler_option_0_Did_you_mean_1,
},
OptionTypeMismatchDiagnostic: diagnostics.Compiler_option_0_expects_an_argument,
}
}
var watchOptionsDidYouMeanDiagnostics = &ParseCommandLineWorkerDiagnostics{
didYouMean: DidYouMeanOptionsDiagnostics{
// no alternateMode
OptionDeclarations: OptionsForWatch,
UnknownOptionDiagnostic: diagnostics.Unknown_watch_option_0,
UnknownDidYouMeanDiagnostic: diagnostics.Unknown_watch_option_0_Did_you_mean_1,
},
OptionTypeMismatchDiagnostic: diagnostics.Watch_option_0_requires_a_value_of_type_1,
}
var buildOptionsDidYouMeanDiagnostics = &ParseCommandLineWorkerDiagnostics{
didYouMean: DidYouMeanOptionsDiagnostics{
alternateMode: &AlternateModeDiagnostics{
diagnostic: diagnostics.Compiler_option_0_may_not_be_used_with_build,
optionsNameMap: CompilerNameMap,
},
OptionDeclarations: BuildOpts,
UnknownOptionDiagnostic: diagnostics.Unknown_build_option_0,
UnknownDidYouMeanDiagnostic: diagnostics.Unknown_build_option_0_Did_you_mean_1,
},
OptionTypeMismatchDiagnostic: diagnostics.Build_option_0_requires_a_value_of_type_1,
}

View File

@@ -0,0 +1,255 @@
package tsoptions
import (
"slices"
"github.com/microsoft/typescript-go/internal/collections"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/tspath"
)
var LibMap = collections.NewOrderedMapFromList([]collections.MapEntry[string, any]{
// JavaScript only
{Key: "es5", Value: "lib.es5.d.ts"},
{Key: "es6", Value: "lib.es2015.d.ts"},
{Key: "es2015", Value: "lib.es2015.d.ts"},
{Key: "es7", Value: "lib.es2016.d.ts"},
{Key: "es2016", Value: "lib.es2016.d.ts"},
{Key: "es2017", Value: "lib.es2017.d.ts"},
{Key: "es2018", Value: "lib.es2018.d.ts"},
{Key: "es2019", Value: "lib.es2019.d.ts"},
{Key: "es2020", Value: "lib.es2020.d.ts"},
{Key: "es2021", Value: "lib.es2021.d.ts"},
{Key: "es2022", Value: "lib.es2022.d.ts"},
{Key: "es2023", Value: "lib.es2023.d.ts"},
{Key: "es2024", Value: "lib.es2024.d.ts"},
{Key: "es2025", Value: "lib.es2025.d.ts"},
{Key: "esnext", Value: "lib.esnext.d.ts"},
// Host only
{Key: "dom", Value: "lib.dom.d.ts"},
{Key: "dom.iterable", Value: "lib.dom.iterable.d.ts"},
{Key: "dom.asynciterable", Value: "lib.dom.asynciterable.d.ts"},
{Key: "webworker", Value: "lib.webworker.d.ts"},
{Key: "webworker.importscripts", Value: "lib.webworker.importscripts.d.ts"},
{Key: "webworker.iterable", Value: "lib.webworker.iterable.d.ts"},
{Key: "webworker.asynciterable", Value: "lib.webworker.asynciterable.d.ts"},
{Key: "scripthost", Value: "lib.scripthost.d.ts"},
// ES2015 and later By-feature options
{Key: "es2015.core", Value: "lib.es2015.core.d.ts"},
{Key: "es2015.collection", Value: "lib.es2015.collection.d.ts"},
{Key: "es2015.generator", Value: "lib.es2015.generator.d.ts"},
{Key: "es2015.iterable", Value: "lib.es2015.iterable.d.ts"},
{Key: "es2015.promise", Value: "lib.es2015.promise.d.ts"},
{Key: "es2015.proxy", Value: "lib.es2015.proxy.d.ts"},
{Key: "es2015.reflect", Value: "lib.es2015.reflect.d.ts"},
{Key: "es2015.symbol", Value: "lib.es2015.symbol.d.ts"},
{Key: "es2015.symbol.wellknown", Value: "lib.es2015.symbol.wellknown.d.ts"},
{Key: "es2016.array.include", Value: "lib.es2016.array.include.d.ts"},
{Key: "es2016.intl", Value: "lib.es2016.intl.d.ts"},
{Key: "es2017.arraybuffer", Value: "lib.es2017.arraybuffer.d.ts"},
{Key: "es2017.date", Value: "lib.es2017.date.d.ts"},
{Key: "es2017.object", Value: "lib.es2017.object.d.ts"},
{Key: "es2017.sharedmemory", Value: "lib.es2017.sharedmemory.d.ts"},
{Key: "es2017.string", Value: "lib.es2017.string.d.ts"},
{Key: "es2017.intl", Value: "lib.es2017.intl.d.ts"},
{Key: "es2017.typedarrays", Value: "lib.es2017.typedarrays.d.ts"},
{Key: "es2018.asyncgenerator", Value: "lib.es2018.asyncgenerator.d.ts"},
{Key: "es2018.asynciterable", Value: "lib.es2018.asynciterable.d.ts"},
{Key: "es2018.intl", Value: "lib.es2018.intl.d.ts"},
{Key: "es2018.promise", Value: "lib.es2018.promise.d.ts"},
{Key: "es2018.regexp", Value: "lib.es2018.regexp.d.ts"},
{Key: "es2019.array", Value: "lib.es2019.array.d.ts"},
{Key: "es2019.object", Value: "lib.es2019.object.d.ts"},
{Key: "es2019.string", Value: "lib.es2019.string.d.ts"},
{Key: "es2019.symbol", Value: "lib.es2019.symbol.d.ts"},
{Key: "es2019.intl", Value: "lib.es2019.intl.d.ts"},
{Key: "es2020.bigint", Value: "lib.es2020.bigint.d.ts"},
{Key: "es2020.date", Value: "lib.es2020.date.d.ts"},
{Key: "es2020.promise", Value: "lib.es2020.promise.d.ts"},
{Key: "es2020.sharedmemory", Value: "lib.es2020.sharedmemory.d.ts"},
{Key: "es2020.string", Value: "lib.es2020.string.d.ts"},
{Key: "es2020.symbol.wellknown", Value: "lib.es2020.symbol.wellknown.d.ts"},
{Key: "es2020.intl", Value: "lib.es2020.intl.d.ts"},
{Key: "es2020.number", Value: "lib.es2020.number.d.ts"},
{Key: "es2021.promise", Value: "lib.es2021.promise.d.ts"},
{Key: "es2021.string", Value: "lib.es2021.string.d.ts"},
{Key: "es2021.weakref", Value: "lib.es2021.weakref.d.ts"},
{Key: "es2021.intl", Value: "lib.es2021.intl.d.ts"},
{Key: "es2022.array", Value: "lib.es2022.array.d.ts"},
{Key: "es2022.error", Value: "lib.es2022.error.d.ts"},
{Key: "es2022.intl", Value: "lib.es2022.intl.d.ts"},
{Key: "es2022.object", Value: "lib.es2022.object.d.ts"},
{Key: "es2022.string", Value: "lib.es2022.string.d.ts"},
{Key: "es2022.regexp", Value: "lib.es2022.regexp.d.ts"},
{Key: "es2023.array", Value: "lib.es2023.array.d.ts"},
{Key: "es2023.collection", Value: "lib.es2023.collection.d.ts"},
{Key: "es2023.intl", Value: "lib.es2023.intl.d.ts"},
{Key: "es2024.arraybuffer", Value: "lib.es2024.arraybuffer.d.ts"},
{Key: "es2024.collection", Value: "lib.es2024.collection.d.ts"},
{Key: "es2024.object", Value: "lib.es2024.object.d.ts"},
{Key: "es2024.promise", Value: "lib.es2024.promise.d.ts"},
{Key: "es2024.regexp", Value: "lib.es2024.regexp.d.ts"},
{Key: "es2024.sharedmemory", Value: "lib.es2024.sharedmemory.d.ts"},
{Key: "es2024.string", Value: "lib.es2024.string.d.ts"},
{Key: "es2025.collection", Value: "lib.es2025.collection.d.ts"},
{Key: "es2025.float16", Value: "lib.es2025.float16.d.ts"},
{Key: "es2025.intl", Value: "lib.es2025.intl.d.ts"},
{Key: "es2025.iterator", Value: "lib.es2025.iterator.d.ts"},
{Key: "es2025.promise", Value: "lib.es2025.promise.d.ts"},
{Key: "es2025.regexp", Value: "lib.es2025.regexp.d.ts"},
// Fallback for backward compatibility
{Key: "esnext.asynciterable", Value: "lib.es2018.asynciterable.d.ts"},
{Key: "esnext.symbol", Value: "lib.es2019.symbol.d.ts"},
{Key: "esnext.bigint", Value: "lib.es2020.bigint.d.ts"},
{Key: "esnext.weakref", Value: "lib.es2021.weakref.d.ts"},
{Key: "esnext.object", Value: "lib.es2024.object.d.ts"},
{Key: "esnext.regexp", Value: "lib.es2024.regexp.d.ts"},
{Key: "esnext.string", Value: "lib.es2024.string.d.ts"},
{Key: "esnext.float16", Value: "lib.es2025.float16.d.ts"},
{Key: "esnext.iterator", Value: "lib.es2025.iterator.d.ts"},
{Key: "esnext.promise", Value: "lib.es2025.promise.d.ts"},
// ESNext By-feature options
{Key: "esnext.array", Value: "lib.esnext.array.d.ts"},
{Key: "esnext.collection", Value: "lib.esnext.collection.d.ts"},
{Key: "esnext.date", Value: "lib.esnext.date.d.ts"},
{Key: "esnext.decorators", Value: "lib.esnext.decorators.d.ts"},
{Key: "esnext.disposable", Value: "lib.esnext.disposable.d.ts"},
{Key: "esnext.error", Value: "lib.esnext.error.d.ts"},
{Key: "esnext.intl", Value: "lib.esnext.intl.d.ts"},
{Key: "esnext.sharedmemory", Value: "lib.esnext.sharedmemory.d.ts"},
{Key: "esnext.temporal", Value: "lib.esnext.temporal.d.ts"},
{Key: "esnext.typedarrays", Value: "lib.esnext.typedarrays.d.ts"},
// Decorators
{Key: "decorators", Value: "lib.decorators.d.ts"},
{Key: "decorators.legacy", Value: "lib.decorators.legacy.d.ts"},
})
var (
Libs = slices.Collect(LibMap.Keys())
LibFilesSet = collections.NewSetFromItems(core.Map(slices.Collect(LibMap.Values()), func(s any) string { return s.(string) })...)
)
func GetLibFileName(libName string) (string, bool) {
// checks if the libName is a valid lib name or file name and converts the lib name to the filename if needed
libName = tspath.ToFileNameLowerCase(libName)
if LibFilesSet.Has(libName) {
return libName, true
}
lib, ok := LibMap.Get(libName)
if !ok {
return "", false
}
return lib.(string), true
}
var moduleResolutionOptionMap = collections.NewOrderedMapFromList([]collections.MapEntry[string, any]{
{Key: "node16", Value: core.ModuleResolutionKindNode16},
{Key: "nodenext", Value: core.ModuleResolutionKindNodeNext},
{Key: "bundler", Value: core.ModuleResolutionKindBundler},
{Key: "classic", Value: core.ModuleResolutionKindClassic},
{Key: "node", Value: core.ModuleResolutionKindNode10},
{Key: "node10", Value: core.ModuleResolutionKindNode10},
})
var targetOptionMap = collections.NewOrderedMapFromList([]collections.MapEntry[string, any]{
{Key: "es5", Value: core.ScriptTargetES5},
{Key: "es6", Value: core.ScriptTargetES2015},
{Key: "es2015", Value: core.ScriptTargetES2015},
{Key: "es2016", Value: core.ScriptTargetES2016},
{Key: "es2017", Value: core.ScriptTargetES2017},
{Key: "es2018", Value: core.ScriptTargetES2018},
{Key: "es2019", Value: core.ScriptTargetES2019},
{Key: "es2020", Value: core.ScriptTargetES2020},
{Key: "es2021", Value: core.ScriptTargetES2021},
{Key: "es2022", Value: core.ScriptTargetES2022},
{Key: "es2023", Value: core.ScriptTargetES2023},
{Key: "es2024", Value: core.ScriptTargetES2024},
{Key: "es2025", Value: core.ScriptTargetES2025},
{Key: "esnext", Value: core.ScriptTargetESNext},
})
var moduleOptionMap = collections.NewOrderedMapFromList([]collections.MapEntry[string, any]{
{Key: "commonjs", Value: core.ModuleKindCommonJS},
{Key: "amd", Value: core.ModuleKindAMD},
{Key: "system", Value: core.ModuleKindSystem},
{Key: "umd", Value: core.ModuleKindUMD},
{Key: "es6", Value: core.ModuleKindES2015},
{Key: "es2015", Value: core.ModuleKindES2015},
{Key: "es2020", Value: core.ModuleKindES2020},
{Key: "es2022", Value: core.ModuleKindES2022},
{Key: "esnext", Value: core.ModuleKindESNext},
{Key: "node16", Value: core.ModuleKindNode16},
{Key: "node18", Value: core.ModuleKindNode18},
{Key: "node20", Value: core.ModuleKindNode20},
{Key: "nodenext", Value: core.ModuleKindNodeNext},
{Key: "preserve", Value: core.ModuleKindPreserve},
})
var moduleDetectionOptionMap = collections.NewOrderedMapFromList([]collections.MapEntry[string, any]{
{Key: "auto", Value: core.ModuleDetectionKindAuto},
{Key: "legacy", Value: core.ModuleDetectionKindLegacy},
{Key: "force", Value: core.ModuleDetectionKindForce},
})
var jsxOptionMap = collections.NewOrderedMapFromList([]collections.MapEntry[string, any]{
{Key: "preserve", Value: core.JsxEmitPreserve},
{Key: "react-native", Value: core.JsxEmitReactNative},
{Key: "react-jsx", Value: core.JsxEmitReactJSX},
{Key: "react-jsxdev", Value: core.JsxEmitReactJSXDev},
{Key: "react", Value: core.JsxEmitReact},
})
var newLineOptionMap = collections.NewOrderedMapFromList([]collections.MapEntry[string, any]{
{Key: "crlf", Value: core.NewLineKindCRLF},
{Key: "lf", Value: core.NewLineKindLF},
})
var targetToLibMap = map[core.ScriptTarget]string{
core.ScriptTargetESNext: "lib.esnext.full.d.ts",
core.ScriptTargetES2025: "lib.es2025.full.d.ts",
core.ScriptTargetES2024: "lib.es2024.full.d.ts",
core.ScriptTargetES2023: "lib.es2023.full.d.ts",
core.ScriptTargetES2022: "lib.es2022.full.d.ts",
core.ScriptTargetES2021: "lib.es2021.full.d.ts",
core.ScriptTargetES2020: "lib.es2020.full.d.ts",
core.ScriptTargetES2019: "lib.es2019.full.d.ts",
core.ScriptTargetES2018: "lib.es2018.full.d.ts",
core.ScriptTargetES2017: "lib.es2017.full.d.ts",
core.ScriptTargetES2016: "lib.es2016.full.d.ts",
core.ScriptTargetES2015: "lib.es6.d.ts", // We don't use lib.es2015.full.d.ts due to breaking change.
}
func TargetToLibMap() map[core.ScriptTarget]string {
return targetToLibMap
}
func GetDefaultLibFileName(options *core.CompilerOptions) string {
name, ok := targetToLibMap[options.GetEmitScriptTarget()]
if !ok {
return "lib.d.ts"
}
return name
}
var watchFileEnumMap = collections.NewOrderedMapFromList([]collections.MapEntry[string, any]{
{Key: "fixedpollinginterval", Value: core.WatchFileKindFixedPollingInterval},
{Key: "prioritypollinginterval", Value: core.WatchFileKindPriorityPollingInterval},
{Key: "dynamicprioritypolling", Value: core.WatchFileKindDynamicPriorityPolling},
{Key: "fixedchunksizepolling", Value: core.WatchFileKindFixedChunkSizePolling},
{Key: "usefsevents", Value: core.WatchFileKindUseFsEvents},
{Key: "usefseventsonparentdirectory", Value: core.WatchFileKindUseFsEventsOnParentDirectory},
})
var watchDirectoryEnumMap = collections.NewOrderedMapFromList([]collections.MapEntry[string, any]{
{Key: "usefsevents", Value: core.WatchDirectoryKindUseFsEvents},
{Key: "fixedpollinginterval", Value: core.WatchDirectoryKindFixedPollingInterval},
{Key: "dynamicprioritypolling", Value: core.WatchDirectoryKindDynamicPriorityPolling},
{Key: "fixedchunksizepolling", Value: core.WatchDirectoryKindFixedChunkSizePolling},
})
var fallbackEnumMap = collections.NewOrderedMapFromList([]collections.MapEntry[string, any]{
{Key: "fixedinterval", Value: core.PollingKindFixedInterval},
{Key: "priorityinterval", Value: core.PollingKindPriorityInterval},
{Key: "dynamicpriority", Value: core.PollingKindDynamicPriority},
{Key: "fixedchunksize", Value: core.PollingKindFixedChunkSize},
})

View File

@@ -0,0 +1,120 @@
package tsoptions
import (
"fmt"
"slices"
"strings"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/diagnostics"
"github.com/microsoft/typescript-go/internal/scanner"
)
func createDiagnosticForInvalidEnumType(opt *CommandLineOption, sourceFile *ast.SourceFile, node *ast.Node) *ast.Diagnostic {
namesOfType := slices.Collect(opt.EnumMap().Keys())
stringNames := formatEnumTypeKeys(opt, namesOfType)
optName := "--" + opt.Name
return CreateDiagnosticForNodeInSourceFileOrCompilerDiagnostic(sourceFile, node, diagnostics.Argument_for_0_option_must_be_Colon_1, optName, stringNames)
}
func formatEnumTypeKeys(opt *CommandLineOption, keys []string) string {
if opt.DeprecatedKeys() != nil {
keys = core.Filter(keys, func(key string) bool { return !opt.DeprecatedKeys().Has(key) })
}
return "'" + strings.Join(keys, "', '") + "'"
}
func getCompilerOptionValueTypeString(option *CommandLineOption) string {
switch option.Kind {
case CommandLineOptionTypeListOrElement:
return fmt.Sprintf("%v or Array", getCompilerOptionValueTypeString(option.Elements()))
case CommandLineOptionTypeList:
return "Array"
default:
return string(option.Kind)
}
}
func (parser *commandLineParser) createUnknownOptionError(
unknownOption string,
unknownOptionErrorText string,
node *ast.Node,
sourceFile *ast.SourceFile,
) *ast.Diagnostic {
return createUnknownOptionError(
unknownOption,
parser.UnknownOptionDiagnostic(),
unknownOptionErrorText,
node,
sourceFile,
parser.AlternateMode(),
)
}
func createUnknownOptionError(
unknownOption string,
unknownOptionDiagnostic *diagnostics.Message,
unknownOptionErrorText string, // optional
node *ast.Node, // optional
sourceFile *ast.SourceFile, // optional
alternateMode *AlternateModeDiagnostics, // optional
) *ast.Diagnostic {
if alternateMode != nil && alternateMode.optionsNameMap != nil {
otherOption := alternateMode.optionsNameMap.Get(strings.ToLower(unknownOption))
if otherOption != nil {
// tscbuildoption
diagnostic := alternateMode.diagnostic
if otherOption.Name == "build" {
diagnostic = diagnostics.Option_build_must_be_the_first_command_line_argument
}
return CreateDiagnosticForNodeInSourceFileOrCompilerDiagnostic(sourceFile, node, diagnostic, unknownOption)
}
}
if unknownOptionErrorText == "" {
unknownOptionErrorText = unknownOption
}
// TODO: possibleOption := spelling suggestion
return CreateDiagnosticForNodeInSourceFileOrCompilerDiagnostic(sourceFile, node, unknownOptionDiagnostic, unknownOptionErrorText)
}
func CreateDiagnosticForNodeInSourceFile(sourceFile *ast.SourceFile, node *ast.Node, message *diagnostics.Message, args ...any) *ast.Diagnostic {
return ast.NewDiagnostic(sourceFile, core.NewTextRange(scanner.SkipTrivia(sourceFile.Text(), node.Loc.Pos()), node.End()), message, args...)
}
func CreateDiagnosticForNodeInSourceFileOrCompilerDiagnostic(sourceFile *ast.SourceFile, node *ast.Node, message *diagnostics.Message, args ...any) *ast.Diagnostic {
if sourceFile != nil && node != nil {
return CreateDiagnosticForNodeInSourceFile(sourceFile, node, message, args...)
}
return ast.NewCompilerDiagnostic(message, args...)
}
func extraKeyDiagnostics(s string) *diagnostics.Message {
switch s {
case "compilerOptions":
return diagnostics.Unknown_compiler_option_0
case "watchOptions":
return diagnostics.Unknown_watch_option_0
case "typeAcquisition":
return diagnostics.Unknown_type_acquisition_option_0
case "buildOptions":
return diagnostics.Unknown_build_option_0
default:
return nil
}
}
func extraKeyDidYouMeanDiagnostics(s string) *diagnostics.Message {
switch s {
case "compilerOptions":
return diagnostics.Unknown_compiler_option_0_Did_you_mean_1
case "watchOptions":
return diagnostics.Unknown_watch_option_0_Did_you_mean_1
case "typeAcquisition":
return diagnostics.Unknown_type_acquisition_option_0_Did_you_mean_1
case "buildOptions":
return diagnostics.Unknown_build_option_0_Did_you_mean_1
default:
return nil
}
}

View File

@@ -0,0 +1,51 @@
package tsoptions
import (
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/collections"
"github.com/microsoft/typescript-go/internal/vfs"
)
func getTestParseCommandLineWorkerDiagnostics(decls []*CommandLineOption) *ParseCommandLineWorkerDiagnostics {
if len(decls) == 0 {
return CompilerOptionsDidYouMeanDiagnostics
}
return getParseCommandLineWorkerDiagnostics(decls)
}
func ParseCommandLineTestWorker(
decls []*CommandLineOption,
commandLine []string,
fs vfs.FS,
currentDirectory string,
) *TestCommandLineParser {
parser := &commandLineParser{
fs: fs,
currentDirectory: currentDirectory,
workerDiagnostics: CompilerOptionsDidYouMeanDiagnostics,
fileNames: []string{},
options: &collections.OrderedMap[string, any]{},
errors: []*ast.Diagnostic{},
}
if len(decls) != 0 {
parser.workerDiagnostics = getTestParseCommandLineWorkerDiagnostics(decls)
}
parser.optionsMap = GetNameMapFromList(parser.OptionsDeclarations())
parser.parseStrings(commandLine)
return &TestCommandLineParser{
Fs: fs,
WorkerDiagnostics: parser.workerDiagnostics,
FileNames: parser.fileNames,
Options: parser.options,
Errors: parser.errors,
}
}
type TestCommandLineParser struct {
Fs vfs.FS
WorkerDiagnostics *ParseCommandLineWorkerDiagnostics
FileNames []string
Options *collections.OrderedMap[string, any]
Errors []*ast.Diagnostic
}

View File

@@ -0,0 +1,58 @@
package tsoptions
import (
"strings"
"github.com/microsoft/typescript-go/internal/collections"
)
var (
CompilerNameMap = GetNameMapFromList(OptionsDeclarations)
BuildNameMap = GetNameMapFromList(BuildOpts)
WatchNameMap = GetNameMapFromList(OptionsForWatch)
)
func GetNameMapFromList(optDecls []*CommandLineOption) *NameMap {
optionsNames := collections.NewOrderedMapWithSizeHint[string, *CommandLineOption](len(optDecls))
shortOptionNames := map[string]string{}
for _, option := range optDecls {
optionsNames.Set(strings.ToLower(option.Name), option)
if option.ShortName != "" {
shortOptionNames[option.ShortName] = option.Name
}
}
return &NameMap{
optionsNames: optionsNames,
shortOptionNames: shortOptionNames,
}
}
type NameMap struct {
optionsNames *collections.OrderedMap[string, *CommandLineOption]
shortOptionNames map[string]string
}
func (nm *NameMap) Get(name string) *CommandLineOption {
return nm.optionsNames.GetOrZero(strings.ToLower(name))
}
func (nm *NameMap) GetFromShort(shortName string) *CommandLineOption {
// returns option only if shortName is a valid short option
name, ok := nm.shortOptionNames[shortName]
if !ok {
return nil
}
return nm.Get(name)
}
func (nm *NameMap) GetOptionDeclarationFromName(optionName string, allowShort bool) *CommandLineOption {
optionName = strings.ToLower(optionName)
// Try to translate short option names to their full equivalents.
if allowShort {
short := nm.shortOptionNames[optionName]
if short != "" {
optionName = short
}
}
return nm.Get(optionName)
}

View File

@@ -0,0 +1,45 @@
package tsoptions
import (
"sync"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/locale"
"github.com/microsoft/typescript-go/internal/tspath"
)
type ParsedBuildCommandLine struct {
BuildOptions *core.BuildOptions `json:"buildOptions"`
CompilerOptions *core.CompilerOptions `json:"compilerOptions"`
WatchOptions *core.WatchOptions `json:"watchOptions"`
Projects []string `json:"projects"`
Errors []*ast.Diagnostic `json:"errors"`
Raw any `json:"raw"`
comparePathsOptions tspath.ComparePathsOptions
resolvedProjectPaths []string
resolvedProjectPathsOnce sync.Once
locale locale.Locale
localeOnce sync.Once
}
func (p *ParsedBuildCommandLine) ResolvedProjectPaths() []string {
p.resolvedProjectPathsOnce.Do(func() {
p.resolvedProjectPaths = core.Map(p.Projects, func(project string) string {
return core.ResolveConfigFileNameOfProjectReference(
tspath.ResolvePath(p.comparePathsOptions.CurrentDirectory, project),
)
})
})
return p.resolvedProjectPaths
}
func (p *ParsedBuildCommandLine) Locale() locale.Locale {
p.localeOnce.Do(func() {
p.locale, _ = locale.Parse(p.CompilerOptions.Locale)
})
return p.locale
}

View File

@@ -0,0 +1,423 @@
package tsoptions
import (
"fmt"
"iter"
"slices"
"strings"
"sync"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/diagnostics"
"github.com/microsoft/typescript-go/internal/glob"
"github.com/microsoft/typescript-go/internal/locale"
"github.com/microsoft/typescript-go/internal/module"
"github.com/microsoft/typescript-go/internal/outputpaths"
"github.com/microsoft/typescript-go/internal/tspath"
"github.com/microsoft/typescript-go/internal/vfs"
"github.com/microsoft/typescript-go/internal/vfs/vfsmatch"
)
const (
fileGlobPattern = "*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,json}"
recursiveFileGlobPattern = "**/*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,json}"
)
type ParsedCommandLine struct {
ParsedConfig *core.ParsedOptions `json:"parsedConfig"`
ConfigFile *TsConfigSourceFile `json:"configFile"` // TsConfigSourceFile, used in Program and ExecuteCommandLine
Errors []*ast.Diagnostic `json:"errors"`
Raw any `json:"raw"`
CompileOnSave *bool `json:"compileOnSave"`
comparePathsOptions tspath.ComparePathsOptions
wildcardDirectoriesOnce sync.Once
wildcardDirectories map[string]bool
includeGlobsOnce sync.Once
includeGlobs []*glob.Glob
extraFileExtensions []FileExtensionInfo
sourceAndOutputMapsOnce sync.Once
sourceToProjectReference map[tspath.Path]*SourceOutputAndProjectReference
outputDtsToProjectReference map[tspath.Path]*SourceOutputAndProjectReference
commonSourceDirectory string
commonSourceDirectoryOnce sync.Once
resolvedProjectReferencePaths []string
resolvedProjectReferencePathsOnce sync.Once
literalFileNamesLen int
fileNamesByPath map[tspath.Path]string // maps file names to their paths, used for quick lookups
fileNamesByPathOnce sync.Once
locale locale.Locale
localeOnce sync.Once
}
func NewParsedCommandLine(
compilerOptions *core.CompilerOptions,
rootFileNames []string,
comparePathsOptions tspath.ComparePathsOptions,
) *ParsedCommandLine {
return &ParsedCommandLine{
ParsedConfig: &core.ParsedOptions{
CompilerOptions: compilerOptions,
FileNames: rootFileNames,
},
comparePathsOptions: comparePathsOptions,
}
}
type SourceOutputAndProjectReference struct {
Source string
OutputDts string
Resolved *ParsedCommandLine
}
var (
_ module.ResolvedProjectReference = (*ParsedCommandLine)(nil)
_ outputpaths.OutputPathsHost = (*ParsedCommandLine)(nil)
)
func (p *ParsedCommandLine) ConfigName() string {
if p == nil || p.ConfigFile == nil {
return ""
}
return p.ConfigFile.SourceFile.FileName()
}
func (p *ParsedCommandLine) SourceToProjectReference() map[tspath.Path]*SourceOutputAndProjectReference {
return p.sourceToProjectReference
}
func (p *ParsedCommandLine) OutputDtsToProjectReference() map[tspath.Path]*SourceOutputAndProjectReference {
return p.outputDtsToProjectReference
}
func (p *ParsedCommandLine) ParseInputOutputNames() {
p.sourceAndOutputMapsOnce.Do(func() {
sourceToOutput := map[tspath.Path]*SourceOutputAndProjectReference{}
outputDtsToSource := map[tspath.Path]*SourceOutputAndProjectReference{}
for outputDts, source := range p.getOutputDeclarationAndSourceFileNames() {
path := tspath.ToPath(source, p.GetCurrentDirectory(), p.UseCaseSensitiveFileNames())
projectReference := &SourceOutputAndProjectReference{
Source: source,
OutputDts: outputDts,
Resolved: p,
}
if outputDts != "" {
outputDtsToSource[tspath.ToPath(outputDts, p.GetCurrentDirectory(), p.UseCaseSensitiveFileNames())] = projectReference
}
sourceToOutput[path] = projectReference
}
p.outputDtsToProjectReference = outputDtsToSource
p.sourceToProjectReference = sourceToOutput
})
}
func (p *ParsedCommandLine) CommonSourceDirectory() string {
p.commonSourceDirectoryOnce.Do(func() {
files := func() []string {
return core.Filter(p.ParsedConfig.FileNames, func(file string) bool {
return !(p.ParsedConfig.CompilerOptions.NoEmitForJsFiles.IsTrue() && tspath.HasJSFileExtension(file)) && !tspath.IsDeclarationFileName(file)
})
}
p.commonSourceDirectory = outputpaths.GetCommonSourceDirectory(
p.ParsedConfig.CompilerOptions,
files,
p.GetCurrentDirectory(),
p.UseCaseSensitiveFileNames(),
p.checkSourceFilesBelongToPath,
)
})
return p.commonSourceDirectory
}
func (p *ParsedCommandLine) checkSourceFilesBelongToPath(sourceFiles []string, rootDirectory string) bool {
allFilesBelongToPath := true
for _, file := range sourceFiles {
absoluteSourceFilePath := tspath.GetCanonicalFileName(tspath.GetNormalizedAbsolutePath(file, p.GetCurrentDirectory()), p.UseCaseSensitiveFileNames())
if !tspath.ContainsPath(rootDirectory, file, p.comparePathsOptions) {
p.Errors = append(p.Errors, ast.NewCompilerDiagnostic(diagnostics.File_0_is_not_under_rootDir_1_rootDir_is_expected_to_contain_all_source_files, absoluteSourceFilePath, rootDirectory))
allFilesBelongToPath = false
}
}
return allFilesBelongToPath
}
func (p *ParsedCommandLine) GetCurrentDirectory() string {
return p.comparePathsOptions.CurrentDirectory
}
func (p *ParsedCommandLine) UseCaseSensitiveFileNames() bool {
return p.comparePathsOptions.UseCaseSensitiveFileNames
}
func (p *ParsedCommandLine) getOutputDeclarationAndSourceFileNames() iter.Seq2[string, string] {
return func(yield func(dtsName string, inputName string) bool) {
for _, fileName := range p.ParsedConfig.FileNames {
var outputDts string
if !tspath.IsDeclarationFileName(fileName) && !tspath.FileExtensionIs(fileName, tspath.ExtensionJson) {
outputDts = outputpaths.GetOutputDeclarationFileNameWorker(fileName, p.CompilerOptions(), p)
}
if !yield(outputDts, fileName) {
return
}
}
}
}
func (p *ParsedCommandLine) GetOutputFileNames() iter.Seq[string] {
return func(yield func(outputName string) bool) {
for _, fileName := range p.ParsedConfig.FileNames {
if tspath.IsDeclarationFileName(fileName) {
continue
}
jsFileName := outputpaths.GetOutputJSFileName(fileName, p.CompilerOptions(), p)
isJson := tspath.FileExtensionIs(fileName, tspath.ExtensionJson)
if jsFileName != "" {
if !yield(jsFileName) {
return
}
if !isJson {
sourceMap := outputpaths.GetSourceMapFilePath(jsFileName, p.CompilerOptions())
if sourceMap != "" {
if !yield(sourceMap) {
return
}
}
}
}
if isJson {
continue
}
if p.CompilerOptions().GetEmitDeclarations() {
dtsFileName := outputpaths.GetOutputDeclarationFileNameWorker(fileName, p.CompilerOptions(), p)
if dtsFileName != "" {
if !yield(dtsFileName) {
return
}
if p.CompilerOptions().GetAreDeclarationMapsEnabled() {
declarationMap := dtsFileName + ".map"
if !yield(declarationMap) {
return
}
}
}
}
}
}
}
func (p *ParsedCommandLine) GetBuildInfoFileName() string {
return outputpaths.GetBuildInfoFileName(p.CompilerOptions(), p.comparePathsOptions)
}
// WildcardDirectories returns the cached wildcard directories, initializing them if needed
func (p *ParsedCommandLine) WildcardDirectories() map[string]bool {
if p == nil {
return nil
}
p.wildcardDirectoriesOnce.Do(func() {
if p.wildcardDirectories == nil {
p.wildcardDirectories = getWildcardDirectories(
p.ConfigFile.configFileSpecs.validatedIncludeSpecs,
p.ConfigFile.configFileSpecs.validatedExcludeSpecs,
p.comparePathsOptions,
)
}
})
return p.wildcardDirectories
}
func (p *ParsedCommandLine) WildcardDirectoryGlobs() []*glob.Glob {
wildcardDirectories := p.WildcardDirectories()
if wildcardDirectories == nil {
return nil
}
p.includeGlobsOnce.Do(func() {
if p.includeGlobs == nil {
globs := make([]*glob.Glob, 0, len(wildcardDirectories))
for dir, recursive := range wildcardDirectories {
if parsed, err := glob.Parse(fmt.Sprintf("%s/%s", tspath.NormalizePath(dir), core.IfElse(recursive, recursiveFileGlobPattern, fileGlobPattern))); err == nil {
globs = append(globs, parsed)
}
}
p.includeGlobs = globs
}
})
return p.includeGlobs
}
// Normalized file names explicitly specified in `files`
func (p *ParsedCommandLine) LiteralFileNames() []string {
if p != nil && p.ConfigFile != nil {
return p.FileNames()[0:p.literalFileNamesLen]
}
return nil
}
func (p *ParsedCommandLine) SetParsedOptions(o *core.ParsedOptions) {
p.ParsedConfig = o
}
func (p *ParsedCommandLine) SetCompilerOptions(o *core.CompilerOptions) {
p.ParsedConfig.CompilerOptions = o
}
func (p *ParsedCommandLine) CompilerOptions() *core.CompilerOptions {
if p == nil {
return nil
}
return p.ParsedConfig.CompilerOptions
}
func (p *ParsedCommandLine) SetTypeAcquisition(o *core.TypeAcquisition) {
p.ParsedConfig.TypeAcquisition = o
}
func (p *ParsedCommandLine) TypeAcquisition() *core.TypeAcquisition {
return p.ParsedConfig.TypeAcquisition
}
// All file names matched by files, include, and exclude patterns
func (p *ParsedCommandLine) FileNames() []string {
return p.ParsedConfig.FileNames
}
func (p *ParsedCommandLine) FileNamesByPath() map[tspath.Path]string {
p.fileNamesByPathOnce.Do(func() {
p.fileNamesByPath = make(map[tspath.Path]string, len(p.ParsedConfig.FileNames))
for _, fileName := range p.ParsedConfig.FileNames {
path := tspath.ToPath(fileName, p.GetCurrentDirectory(), p.UseCaseSensitiveFileNames())
p.fileNamesByPath[path] = fileName
}
})
return p.fileNamesByPath
}
func (p *ParsedCommandLine) ProjectReferences() []*core.ProjectReference {
return p.ParsedConfig.ProjectReferences
}
func (p *ParsedCommandLine) ResolvedProjectReferencePaths() []string {
p.resolvedProjectReferencePathsOnce.Do(func() {
p.resolvedProjectReferencePaths = core.Map(p.ParsedConfig.ProjectReferences, core.ResolveProjectReferencePath)
})
return p.resolvedProjectReferencePaths
}
func (p *ParsedCommandLine) ExtendedSourceFiles() []string {
if p == nil || p.ConfigFile == nil {
return nil
}
return p.ConfigFile.ExtendedSourceFiles
}
func (p *ParsedCommandLine) GetConfigFileParsingDiagnostics() []*ast.Diagnostic {
if p.ConfigFile != nil {
// todo: !!! should be ConfigFile.ParseDiagnostics, check if they are the same
return slices.Concat(p.ConfigFile.SourceFile.Diagnostics(), p.Errors)
}
return p.Errors
}
// PossiblyMatchesFileName is a fast check to see if a file is currently included by a config
// or would be included if the file were to be created. It may return false positives.
func (p *ParsedCommandLine) PossiblyMatchesFileName(fileName string) bool {
path := tspath.ToPath(fileName, p.GetCurrentDirectory(), p.UseCaseSensitiveFileNames())
if _, ok := p.FileNamesByPath()[path]; ok {
return true
}
for _, include := range p.ConfigFile.configFileSpecs.validatedIncludeSpecs {
if !strings.ContainsAny(include, "*?") && !vfsmatch.IsImplicitGlob(include) {
includePath := tspath.ToPath(include, p.GetCurrentDirectory(), p.UseCaseSensitiveFileNames())
if includePath == path {
return true
}
}
}
if wildcardDirectoryGlobs := p.WildcardDirectoryGlobs(); len(wildcardDirectoryGlobs) > 0 {
for _, glob := range wildcardDirectoryGlobs {
if glob.Match(fileName) {
return true
}
}
}
return false
}
func (p *ParsedCommandLine) PossiblyMatchesDirectoryName(directoryPath tspath.Path) bool {
for wildcardDir, recursive := range p.WildcardDirectories() {
wildcardDirPath := tspath.ToPath(wildcardDir, p.GetCurrentDirectory(), p.UseCaseSensitiveFileNames())
if recursive {
if wildcardDirPath.ContainsPath(directoryPath) {
return true
}
} else {
if wildcardDirPath == directoryPath {
return true
}
}
}
return false
}
func (p *ParsedCommandLine) GetMatchedFileSpec(fileName string) string {
return p.ConfigFile.configFileSpecs.getMatchedFileSpec(fileName, p.comparePathsOptions)
}
func (p *ParsedCommandLine) GetMatchedIncludeSpec(fileName string) (string, bool) {
if len(p.ConfigFile.configFileSpecs.validatedIncludeSpecs) == 0 {
return "", false
}
if p.ConfigFile.configFileSpecs.isDefaultIncludeSpec {
return p.ConfigFile.configFileSpecs.validatedIncludeSpecs[0], true
}
return p.ConfigFile.configFileSpecs.getMatchedIncludeSpec(fileName, p.comparePathsOptions), false
}
func (p *ParsedCommandLine) ReloadFileNamesOfParsedCommandLine(fs vfs.FS) *ParsedCommandLine {
parsedConfig := *p.ParsedConfig
fileNames, literalFileNamesLen := getFileNamesFromConfigSpecs(
*p.ConfigFile.configFileSpecs,
p.GetCurrentDirectory(),
p.CompilerOptions(),
fs,
p.extraFileExtensions,
)
parsedConfig.FileNames = fileNames
parsedCommandLine := ParsedCommandLine{
ParsedConfig: &parsedConfig,
ConfigFile: p.ConfigFile,
Errors: p.Errors,
Raw: p.Raw,
CompileOnSave: p.CompileOnSave,
comparePathsOptions: p.comparePathsOptions,
wildcardDirectories: p.wildcardDirectories,
includeGlobs: p.includeGlobs,
extraFileExtensions: p.extraFileExtensions,
literalFileNamesLen: literalFileNamesLen,
}
return &parsedCommandLine
}
func (p *ParsedCommandLine) Locale() locale.Locale {
p.localeOnce.Do(func() {
p.locale, _ = locale.Parse(p.CompilerOptions().Locale)
})
return p.locale
}

View File

@@ -0,0 +1,168 @@
package tsoptions_test
import (
"slices"
"testing"
"github.com/microsoft/typescript-go/internal/tsoptions"
"github.com/microsoft/typescript-go/internal/tsoptions/tsoptionstest"
"github.com/microsoft/typescript-go/internal/vfs/vfstest"
"gotest.tools/v3/assert"
)
func TestParsedCommandLine(t *testing.T) {
t.Parallel()
t.Run("PossiblyMatchesFileName", func(t *testing.T) {
t.Parallel()
noFiles := map[string]string{}
noFilesFS := vfstest.FromMap(noFiles, true)
files := map[string]string{
"/dev/a.ts": "",
"/dev/a.d.ts": "",
"/dev/a.js": "",
"/dev/b.ts": "",
"/dev/b.js": "",
"/dev/c.d.ts": "",
"/dev/z/a.ts": "",
"/dev/z/abz.ts": "",
"/dev/z/aba.ts": "",
"/dev/z/b.ts": "",
"/dev/z/bbz.ts": "",
"/dev/z/bba.ts": "",
"/dev/x/a.ts": "",
"/dev/x/aa.ts": "",
"/dev/x/b.ts": "",
"/dev/x/y/a.ts": "",
"/dev/x/y/b.ts": "",
"/dev/js/a.js": "",
"/dev/js/b.js": "",
"/dev/js/d.min.js": "",
"/dev/js/ab.min.js": "",
"/ext/ext.ts": "",
"/ext/b/a..b.ts": "",
}
assertMatches := func(t *testing.T, parsedCommandLine *tsoptions.ParsedCommandLine, files map[string]string, matches []string) {
t.Helper()
for fileName := range files {
actual := parsedCommandLine.PossiblyMatchesFileName(fileName)
expected := slices.Contains(matches, fileName)
assert.Equal(t, actual, expected, "fileName: %s", fileName)
}
for _, fileName := range matches {
if _, ok := files[fileName]; !ok {
actual := parsedCommandLine.PossiblyMatchesFileName(fileName)
assert.Equal(t, actual, true, "fileName: %s", fileName)
}
}
}
t.Run("with literal file list", func(t *testing.T) {
t.Parallel()
t.Run("without exclude", func(t *testing.T) {
t.Parallel()
parsedCommandLine := tsoptionstest.GetParsedCommandLine(
t,
`{
"files": [
"a.ts",
"b.ts"
]
}`,
files,
"/dev",
/*useCaseSensitiveFileNames*/ true,
)
assertMatches(t, parsedCommandLine, files, []string{
"/dev/a.ts",
"/dev/b.ts",
})
})
t.Run("are not removed due to excludes", func(t *testing.T) {
t.Parallel()
parsedCommandLine := tsoptionstest.GetParsedCommandLine(
t,
`{
"files": [
"a.ts",
"b.ts"
],
"exclude": [
"b.ts"
]
}`,
files,
"/dev",
/*useCaseSensitiveFileNames*/ true,
)
assertMatches(t, parsedCommandLine, files, []string{
"/dev/a.ts",
"/dev/b.ts",
})
emptyParsedCommandLine := parsedCommandLine.ReloadFileNamesOfParsedCommandLine(noFilesFS)
assertMatches(t, emptyParsedCommandLine, noFiles, []string{
"/dev/a.ts",
"/dev/b.ts",
})
})
t.Run("duplicates", func(t *testing.T) {
t.Parallel()
parsedCommandLine := tsoptionstest.GetParsedCommandLine(
t,
`{
"files": [
"a.ts",
"a.ts",
"b.ts",
]
}`,
files,
"/dev",
/*useCaseSensitiveFileNames*/ true,
)
assert.DeepEqual(t, parsedCommandLine.LiteralFileNames(), []string{
"/dev/a.ts",
"/dev/b.ts",
})
})
})
t.Run("with literal include list", func(t *testing.T) {
t.Parallel()
t.Run("without exclude", func(t *testing.T) {
t.Parallel()
parsedCommandLine := tsoptionstest.GetParsedCommandLine(
t,
`{
"include": [
"a.ts",
"b.ts"
]
}`,
files,
"/dev",
/*useCaseSensitiveFileNames*/ true,
)
assertMatches(t, parsedCommandLine, files, []string{
"/dev/a.ts",
"/dev/b.ts",
})
emptyParsedCommandLine := parsedCommandLine.ReloadFileNamesOfParsedCommandLine(noFilesFS)
assertMatches(t, emptyParsedCommandLine, noFiles, []string{
"/dev/a.ts",
"/dev/b.ts",
})
})
})
})
}

View File

@@ -0,0 +1,660 @@
package tsoptions
import (
"reflect"
"strings"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/collections"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/diagnostics"
"github.com/microsoft/typescript-go/internal/tspath"
)
func ParseTristate(value any) core.Tristate {
if value == nil {
return core.TSUnknown
}
if v, ok := value.(core.Tristate); ok {
return v
}
if value == true {
return core.TSTrue
} else {
return core.TSFalse
}
}
func ParseStringArray(value any) []string {
if arr, ok := value.([]any); ok {
if arr == nil {
return nil
}
result := make([]string, 0, len(arr))
for _, v := range arr {
if str, ok := v.(string); ok {
result = append(result, str)
}
}
return result
}
return nil
}
func parseStringMap(value any) *collections.OrderedMap[string, []string] {
if m, ok := value.(*collections.OrderedMap[string, any]); ok {
result := collections.NewOrderedMapWithSizeHint[string, []string](m.Size())
for k, v := range m.Entries() {
result.Set(k, ParseStringArray(v))
}
return result
}
return nil
}
func ParseString(value any) string {
if str, ok := value.(string); ok {
return str
}
return ""
}
func parseNumber(value any) *int {
if num, ok := value.(int); ok {
return &num
}
if num, ok := value.(float64); ok {
n := int(num)
return &n
}
return nil
}
func parseProjectReference(json any) []*core.ProjectReference {
var result []*core.ProjectReference
if v, ok := json.(*collections.OrderedMap[string, any]); ok {
var reference core.ProjectReference
if v, ok := v.Get("path"); ok {
reference.Path = v.(string)
}
if v, ok := v.Get("circular"); ok {
reference.Circular = v.(bool)
}
result = append(result, &reference)
}
return result
}
func parseJsonToStringKey(json any) *collections.OrderedMap[string, any] {
result := collections.NewOrderedMapWithSizeHint[string, any](6)
if m, ok := json.(*collections.OrderedMap[string, any]); ok {
if v, ok := m.Get("include"); ok {
result.Set("include", v)
}
if v, ok := m.Get("exclude"); ok {
result.Set("exclude", v)
}
if v, ok := m.Get("files"); ok {
result.Set("files", v)
}
if v, ok := m.Get("references"); ok {
result.Set("references", v)
}
if v, ok := m.Get("extends"); ok {
if str, ok := v.(string); ok {
result.Set("extends", []any{str})
}
result.Set("extends", v)
}
if v, ok := m.Get("compilerOptions"); ok {
result.Set("compilerOptions", v)
}
if v, ok := m.Get("excludes"); ok {
result.Set("excludes", v)
}
if v, ok := m.Get("typeAcquisition"); ok {
result.Set("typeAcquisition", v)
}
}
return result
}
type optionParser interface {
ParseOption(key string, value any) []*ast.Diagnostic
UnknownOptionDiagnostic() *diagnostics.Message
UnknownDidYouMeanDiagnostic() *diagnostics.Message
}
type compilerOptionsParser struct {
*core.CompilerOptions
}
func (o *compilerOptionsParser) ParseOption(key string, value any) []*ast.Diagnostic {
return ParseCompilerOptions(key, value, o.CompilerOptions)
}
func (o *compilerOptionsParser) UnknownOptionDiagnostic() *diagnostics.Message {
return extraKeyDiagnostics("compilerOptions")
}
func (o *compilerOptionsParser) UnknownDidYouMeanDiagnostic() *diagnostics.Message {
return extraKeyDidYouMeanDiagnostics("compilerOptions")
}
type watchOptionsParser struct {
*core.WatchOptions
}
func (o *watchOptionsParser) ParseOption(key string, value any) []*ast.Diagnostic {
return ParseWatchOptions(key, value, o.WatchOptions)
}
func (o *watchOptionsParser) UnknownOptionDiagnostic() *diagnostics.Message {
return extraKeyDiagnostics("watchOptions")
}
func (o *watchOptionsParser) UnknownDidYouMeanDiagnostic() *diagnostics.Message {
return extraKeyDidYouMeanDiagnostics("watchOptions")
}
type typeAcquisitionParser struct {
*core.TypeAcquisition
}
func (o *typeAcquisitionParser) ParseOption(key string, value any) []*ast.Diagnostic {
return ParseTypeAcquisition(key, value, o.TypeAcquisition)
}
func (o *typeAcquisitionParser) UnknownOptionDiagnostic() *diagnostics.Message {
return extraKeyDiagnostics("typeAcquisition")
}
func (o *typeAcquisitionParser) UnknownDidYouMeanDiagnostic() *diagnostics.Message {
return extraKeyDidYouMeanDiagnostics("typeAcquisition")
}
type buildOptionsParser struct {
*core.BuildOptions
}
func (o *buildOptionsParser) ParseOption(key string, value any) []*ast.Diagnostic {
return ParseBuildOptions(key, value, o.BuildOptions)
}
func (o *buildOptionsParser) UnknownOptionDiagnostic() *diagnostics.Message {
return extraKeyDiagnostics("buildOptions")
}
func (o *buildOptionsParser) UnknownDidYouMeanDiagnostic() *diagnostics.Message {
return extraKeyDidYouMeanDiagnostics("buildOptions")
}
func ParseCompilerOptions(key string, value any, allOptions *core.CompilerOptions) []*ast.Diagnostic {
if value == nil {
return nil
}
if allOptions == nil {
return nil
}
parseCompilerOptions(key, value, allOptions)
return nil
}
func parseCompilerOptions(key string, value any, allOptions *core.CompilerOptions) (foundKey bool) {
option := CommandLineCompilerOptionsMap.Get(key)
if option != nil {
key = option.Name
}
switch key {
case "allowJs":
allOptions.AllowJs = ParseTristate(value)
case "allowImportingTsExtensions":
allOptions.AllowImportingTsExtensions = ParseTristate(value)
case "allowSyntheticDefaultImports":
allOptions.AllowSyntheticDefaultImports = ParseTristate(value)
case "allowNonTsExtensions":
allOptions.AllowNonTsExtensions = ParseTristate(value)
case "allowUmdGlobalAccess":
allOptions.AllowUmdGlobalAccess = ParseTristate(value)
case "allowUnreachableCode":
allOptions.AllowUnreachableCode = ParseTristate(value)
case "allowUnusedLabels":
allOptions.AllowUnusedLabels = ParseTristate(value)
case "allowArbitraryExtensions":
allOptions.AllowArbitraryExtensions = ParseTristate(value)
case "alwaysStrict":
allOptions.AlwaysStrict = ParseTristate(value)
case "assumeChangesOnlyAffectDirectDependencies":
allOptions.AssumeChangesOnlyAffectDirectDependencies = ParseTristate(value)
case "baseUrl":
allOptions.BaseUrl = ParseString(value)
case "build":
allOptions.Build = ParseTristate(value)
case "checkJs":
allOptions.CheckJs = ParseTristate(value)
case "customConditions":
allOptions.CustomConditions = ParseStringArray(value)
case "composite":
allOptions.Composite = ParseTristate(value)
case "declarationDir":
allOptions.DeclarationDir = ParseString(value)
case "deduplicatePackages":
allOptions.DeduplicatePackages = ParseTristate(value)
case "diagnostics":
allOptions.Diagnostics = ParseTristate(value)
case "disableSizeLimit":
allOptions.DisableSizeLimit = ParseTristate(value)
case "disableSourceOfProjectReferenceRedirect":
allOptions.DisableSourceOfProjectReferenceRedirect = ParseTristate(value)
case "disableSolutionSearching":
allOptions.DisableSolutionSearching = ParseTristate(value)
case "disableReferencedProjectLoad":
allOptions.DisableReferencedProjectLoad = ParseTristate(value)
case "declarationMap":
allOptions.DeclarationMap = ParseTristate(value)
case "declaration":
allOptions.Declaration = ParseTristate(value)
case "downlevelIteration":
allOptions.DownlevelIteration = ParseTristate(value)
case "erasableSyntaxOnly":
allOptions.ErasableSyntaxOnly = ParseTristate(value)
case "emitDeclarationOnly":
allOptions.EmitDeclarationOnly = ParseTristate(value)
case "extendedDiagnostics":
allOptions.ExtendedDiagnostics = ParseTristate(value)
case "emitDecoratorMetadata":
allOptions.EmitDecoratorMetadata = ParseTristate(value)
case "emitBOM":
allOptions.EmitBOM = ParseTristate(value)
case "esModuleInterop":
allOptions.ESModuleInterop = ParseTristate(value)
case "exactOptionalPropertyTypes":
allOptions.ExactOptionalPropertyTypes = ParseTristate(value)
case "explainFiles":
allOptions.ExplainFiles = ParseTristate(value)
case "experimentalDecorators":
allOptions.ExperimentalDecorators = ParseTristate(value)
case "forceConsistentCasingInFileNames":
allOptions.ForceConsistentCasingInFileNames = ParseTristate(value)
case "generateCpuProfile":
allOptions.GenerateCpuProfile = ParseString(value)
case "generateTrace":
allOptions.GenerateTrace = ParseString(value)
case "isolatedModules":
allOptions.IsolatedModules = ParseTristate(value)
case "ignoreConfig":
allOptions.IgnoreConfig = ParseTristate(value)
case "ignoreDeprecations":
allOptions.IgnoreDeprecations = ParseString(value)
case "importHelpers":
allOptions.ImportHelpers = ParseTristate(value)
case "incremental":
allOptions.Incremental = ParseTristate(value)
case "init":
allOptions.Init = ParseTristate(value)
case "inlineSourceMap":
allOptions.InlineSourceMap = ParseTristate(value)
case "inlineSources":
allOptions.InlineSources = ParseTristate(value)
case "isolatedDeclarations":
allOptions.IsolatedDeclarations = ParseTristate(value)
case "jsx":
allOptions.Jsx = floatOrInt32ToFlag[core.JsxEmit](value)
case "jsxFactory":
allOptions.JsxFactory = ParseString(value)
case "jsxFragmentFactory":
allOptions.JsxFragmentFactory = ParseString(value)
case "jsxImportSource":
allOptions.JsxImportSource = ParseString(value)
case "lib":
if _, ok := value.([]string); ok {
allOptions.Lib = value.([]string)
} else {
allOptions.Lib = ParseStringArray(value)
}
case "libReplacement":
allOptions.LibReplacement = ParseTristate(value)
case "listEmittedFiles":
allOptions.ListEmittedFiles = ParseTristate(value)
case "listFiles":
allOptions.ListFiles = ParseTristate(value)
case "listFilesOnly":
allOptions.ListFilesOnly = ParseTristate(value)
case "locale":
allOptions.Locale = ParseString(value)
case "mapRoot":
allOptions.MapRoot = ParseString(value)
case "module":
allOptions.Module = floatOrInt32ToFlag[core.ModuleKind](value)
case "moduleDetectionKind":
allOptions.ModuleDetection = floatOrInt32ToFlag[core.ModuleDetectionKind](value)
case "moduleResolution":
allOptions.ModuleResolution = floatOrInt32ToFlag[core.ModuleResolutionKind](value)
case "moduleSuffixes":
allOptions.ModuleSuffixes = ParseStringArray(value)
case "moduleDetection":
allOptions.ModuleDetection = floatOrInt32ToFlag[core.ModuleDetectionKind](value)
case "noCheck":
allOptions.NoCheck = ParseTristate(value)
case "noFallthroughCasesInSwitch":
allOptions.NoFallthroughCasesInSwitch = ParseTristate(value)
case "noEmitForJsFiles":
allOptions.NoEmitForJsFiles = ParseTristate(value)
case "noErrorTruncation":
allOptions.NoErrorTruncation = ParseTristate(value)
case "noImplicitAny":
allOptions.NoImplicitAny = ParseTristate(value)
case "noImplicitThis":
allOptions.NoImplicitThis = ParseTristate(value)
case "noLib":
allOptions.NoLib = ParseTristate(value)
case "noPropertyAccessFromIndexSignature":
allOptions.NoPropertyAccessFromIndexSignature = ParseTristate(value)
case "noUncheckedIndexedAccess":
allOptions.NoUncheckedIndexedAccess = ParseTristate(value)
case "noEmitHelpers":
allOptions.NoEmitHelpers = ParseTristate(value)
case "noEmitOnError":
allOptions.NoEmitOnError = ParseTristate(value)
case "noImplicitReturns":
allOptions.NoImplicitReturns = ParseTristate(value)
case "noUnusedLocals":
allOptions.NoUnusedLocals = ParseTristate(value)
case "noUnusedParameters":
allOptions.NoUnusedParameters = ParseTristate(value)
case "noImplicitOverride":
allOptions.NoImplicitOverride = ParseTristate(value)
case "noUncheckedSideEffectImports":
allOptions.NoUncheckedSideEffectImports = ParseTristate(value)
case "outFile":
allOptions.OutFile = ParseString(value)
case "noResolve":
allOptions.NoResolve = ParseTristate(value)
case "paths":
allOptions.Paths = parseStringMap(value)
case "preserveWatchOutput":
allOptions.PreserveWatchOutput = ParseTristate(value)
case "preserveConstEnums":
allOptions.PreserveConstEnums = ParseTristate(value)
case "preserveSymlinks":
allOptions.PreserveSymlinks = ParseTristate(value)
case "project":
allOptions.Project = ParseString(value)
case "pretty":
allOptions.Pretty = ParseTristate(value)
case "resolveJsonModule":
allOptions.ResolveJsonModule = ParseTristate(value)
case "resolvePackageJsonExports":
allOptions.ResolvePackageJsonExports = ParseTristate(value)
case "resolvePackageJsonImports":
allOptions.ResolvePackageJsonImports = ParseTristate(value)
case "reactNamespace":
allOptions.ReactNamespace = ParseString(value)
case "rewriteRelativeImportExtensions":
allOptions.RewriteRelativeImportExtensions = ParseTristate(value)
case "rootDir":
allOptions.RootDir = ParseString(value)
case "rootDirs":
allOptions.RootDirs = ParseStringArray(value)
case "removeComments":
allOptions.RemoveComments = ParseTristate(value)
case "stableTypeOrdering":
allOptions.StableTypeOrdering = ParseTristate(value)
case "strict":
allOptions.Strict = ParseTristate(value)
case "strictBindCallApply":
allOptions.StrictBindCallApply = ParseTristate(value)
case "strictBuiltinIteratorReturn":
allOptions.StrictBuiltinIteratorReturn = ParseTristate(value)
case "strictFunctionTypes":
allOptions.StrictFunctionTypes = ParseTristate(value)
case "strictNullChecks":
allOptions.StrictNullChecks = ParseTristate(value)
case "strictPropertyInitialization":
allOptions.StrictPropertyInitialization = ParseTristate(value)
case "skipDefaultLibCheck":
allOptions.SkipDefaultLibCheck = ParseTristate(value)
case "sourceMap":
allOptions.SourceMap = ParseTristate(value)
case "sourceRoot":
allOptions.SourceRoot = ParseString(value)
case "stripInternal":
allOptions.StripInternal = ParseTristate(value)
case "suppressOutputPathCheck":
allOptions.SuppressOutputPathCheck = ParseTristate(value)
case "target":
allOptions.Target = floatOrInt32ToFlag[core.ScriptTarget](value)
case "traceResolution":
allOptions.TraceResolution = ParseTristate(value)
case "tsBuildInfoFile":
allOptions.TsBuildInfoFile = ParseString(value)
case "typeRoots":
allOptions.TypeRoots = ParseStringArray(value)
case "types":
allOptions.Types = ParseStringArray(value)
case "useDefineForClassFields":
allOptions.UseDefineForClassFields = ParseTristate(value)
case "useUnknownInCatchVariables":
allOptions.UseUnknownInCatchVariables = ParseTristate(value)
case "verbatimModuleSyntax":
allOptions.VerbatimModuleSyntax = ParseTristate(value)
case "version":
allOptions.Version = ParseTristate(value)
case "help":
allOptions.Help = ParseTristate(value)
case "all":
allOptions.All = ParseTristate(value)
case "maxNodeModuleJsDepth":
allOptions.MaxNodeModuleJsDepth = parseNumber(value)
case "skipLibCheck":
allOptions.SkipLibCheck = ParseTristate(value)
case "noEmit":
allOptions.NoEmit = ParseTristate(value)
case "showConfig":
allOptions.ShowConfig = ParseTristate(value)
case "configFilePath":
allOptions.ConfigFilePath = ParseString(value)
case "noDtsResolution":
allOptions.NoDtsResolution = ParseTristate(value)
case "pathsBasePath":
allOptions.PathsBasePath = ParseString(value)
case "outDir":
allOptions.OutDir = ParseString(value)
case "newLine":
allOptions.NewLine = floatOrInt32ToFlag[core.NewLineKind](value)
case "watch":
allOptions.Watch = ParseTristate(value)
case "pprofDir":
allOptions.PprofDir = ParseString(value)
case "singleThreaded":
allOptions.SingleThreaded = ParseTristate(value)
case "quiet":
allOptions.Quiet = ParseTristate(value)
case "checkers":
allOptions.Checkers = parseNumber(value)
default:
// different than any key above
return false
}
return true
}
func floatOrInt32ToFlag[T ~int32](value any) T {
if v, ok := value.(T); ok {
return v
}
return T(value.(float64))
}
func ParseWatchOptions(key string, value any, allOptions *core.WatchOptions) []*ast.Diagnostic {
if allOptions == nil {
return nil
}
switch key {
case "watchInterval":
allOptions.Interval = parseNumber(value)
case "watchFile":
if value != nil {
allOptions.FileKind = value.(core.WatchFileKind)
}
case "watchDirectory":
if value != nil {
allOptions.DirectoryKind = value.(core.WatchDirectoryKind)
}
case "fallbackPolling":
if value != nil {
allOptions.FallbackPolling = value.(core.PollingKind)
}
case "synchronousWatchDirectory":
allOptions.SyncWatchDir = ParseTristate(value)
case "excludeDirectories":
allOptions.ExcludeDir = ParseStringArray(value)
case "excludeFiles":
allOptions.ExcludeFiles = ParseStringArray(value)
}
return nil
}
func ParseTypeAcquisition(key string, value any, allOptions *core.TypeAcquisition) []*ast.Diagnostic {
if value == nil {
return nil
}
if allOptions == nil {
return nil
}
switch key {
case "enable":
allOptions.Enable = ParseTristate(value)
case "include":
allOptions.Include = ParseStringArray(value)
case "exclude":
allOptions.Exclude = ParseStringArray(value)
case "disableFilenameBasedTypeAcquisition":
allOptions.DisableFilenameBasedTypeAcquisition = ParseTristate(value)
}
return nil
}
func ParseBuildOptions(key string, value any, allOptions *core.BuildOptions) []*ast.Diagnostic {
if value == nil {
return nil
}
if allOptions == nil {
return nil
}
option := BuildNameMap.Get(key)
if option != nil {
key = option.Name
}
switch key {
case "clean":
allOptions.Clean = ParseTristate(value)
case "dry":
allOptions.Dry = ParseTristate(value)
case "force":
allOptions.Force = ParseTristate(value)
case "builders":
allOptions.Builders = parseNumber(value)
case "stopBuildOnErrors":
allOptions.StopBuildOnErrors = ParseTristate(value)
case "verbose":
allOptions.Verbose = ParseTristate(value)
}
return nil
}
// mergeCompilerOptions merges the source compiler options into the target compiler options
// with optional awareness of explicitly set null values in the raw JSON.
// Fields in the source options will overwrite the corresponding fields in the target options,
// including when they are explicitly set to null in the raw configuration (if rawSource is provided).
func mergeCompilerOptions(targetOptions, sourceOptions *core.CompilerOptions, rawSource any) *core.CompilerOptions {
if sourceOptions == nil {
return targetOptions
}
// Collect explicitly null field names from raw JSON
var explicitNullFields collections.Set[string]
if rawSource != nil {
if rawMap, ok := rawSource.(*collections.OrderedMap[string, any]); ok && rawMap != nil {
// Options are nested under "compilerOptions" in both tsconfig.json and wrapped command line options
if compilerOptionsRaw, exists := rawMap.Get("compilerOptions"); exists {
if compilerOptionsMap, ok := compilerOptionsRaw.(*collections.OrderedMap[string, any]); ok {
for key, value := range compilerOptionsMap.Entries() {
if value == nil {
explicitNullFields.Add(key)
}
}
}
}
}
}
// Do the merge, handling explicit nulls during the normal merge
targetValue := reflect.ValueOf(targetOptions).Elem()
sourceValue := reflect.ValueOf(sourceOptions).Elem()
targetType := targetValue.Type()
for i := range targetValue.NumField() {
targetField := targetValue.Field(i)
sourceField := sourceValue.Field(i)
// Get the JSON field name for this struct field and check if it's explicitly null
if jsonTag := targetType.Field(i).Tag.Get("json"); jsonTag != "" {
if jsonFieldName, _, _ := strings.Cut(jsonTag, ","); jsonFieldName != "" && explicitNullFields.Has(jsonFieldName) {
targetField.SetZero()
continue
}
}
// Normal merge behavior: copy non-zero fields
if !sourceField.IsZero() {
targetField.Set(sourceField)
}
}
return targetOptions
}
func convertToOptionsWithAbsolutePaths(optionsBase *collections.OrderedMap[string, any], optionMap CommandLineOptionNameMap, cwd string) *collections.OrderedMap[string, any] {
// !!! convert to options with absolute paths was previously done with `CompilerOptions` object, but for ease of implementation, we do it pre-conversion.
// !!! Revisit this choice if/when refactoring when conversion is done in tsconfig parsing
if optionsBase == nil {
return nil
}
for o, v := range optionsBase.Entries() {
result, ok := ConvertOptionToAbsolutePath(o, v, optionMap, cwd)
if ok {
optionsBase.Set(o, result)
}
}
return optionsBase
}
func ConvertOptionToAbsolutePath(o string, v any, optionMap CommandLineOptionNameMap, cwd string) (any, bool) {
option := optionMap.Get(o)
if option == nil {
return nil, false
}
if option.Kind == "list" {
if option.Elements().IsFilePath {
if arr, ok := v.([]string); ok {
return core.Map(arr, func(item string) string {
return tspath.GetNormalizedAbsolutePath(item, cwd)
}), true
}
if arr, ok := v.([]any); ok {
return core.Map(arr, func(item any) any {
if s, isStr := item.(string); isStr {
return tspath.GetNormalizedAbsolutePath(s, cwd)
}
return item
}), true
}
}
} else if option.IsFilePath {
if value, ok := v.(string); ok {
return tspath.GetNormalizedAbsolutePath(value, cwd), true
}
}
return nil, false
}

View File

@@ -0,0 +1,36 @@
package tsoptions
import (
"reflect"
"strings"
"testing"
"github.com/microsoft/typescript-go/internal/core"
)
func TestParseCompilerOptionNoMissingFields(t *testing.T) {
t.Parallel()
var missingKeys []string
for _, field := range reflect.VisibleFields(reflect.TypeFor[core.CompilerOptions]()) {
if !field.IsExported() {
continue
}
keyName := field.Name
// use the JSON key from the tag, if present
// e.g. `json:"dog[,anythingelse]"` --> dog
if jsonTag, ok := field.Tag.Lookup("json"); ok {
keyName = strings.SplitN(jsonTag, ",", 2)[0]
}
val := reflect.Zero(field.Type).Interface()
co := core.CompilerOptions{}
found := parseCompilerOptions(keyName, val, &co)
if !found {
missingKeys = append(missingKeys, keyName)
}
}
if len(missingKeys) > 0 {
t.Errorf("The following keys are missing entries in the ParseCompilerOptions"+
" switch statement:\n%v", missingKeys)
}
}

View File

@@ -0,0 +1,389 @@
package tsoptions
import (
"reflect"
"github.com/microsoft/typescript-go/internal/collections"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/debug"
"github.com/microsoft/typescript-go/internal/diagnostics"
"github.com/microsoft/typescript-go/internal/tspath"
)
// computeFn wraps a typed getter method so it can be stored in an impliedOption's
// compute field (which has type func(*core.CompilerOptions) any).
func computeFn[T any](fn func(*core.CompilerOptions) T) func(*core.CompilerOptions) any {
return func(opts *core.CompilerOptions) any {
return fn(opts)
}
}
// impliedOption describes a compiler option whose effective value can be derived from
// other options. This mirrors TypeScript's computedOptions concept used in convertToTSConfig.
type impliedOption struct {
// name is the Go struct field name of the CompilerOptions field (e.g., "Module").
name string
// dependencies lists the Go struct field names that this option depends on.
dependencies []string
// compute returns the effective value of this option given compiler options.
compute func(opts *core.CompilerOptions) any
}
// impliedOptions lists the compiler options that may be implied by other options,
// mirroring TypeScript's computedOptions used in convertToTSConfig.
// Each compute function delegates directly to an existing core.CompilerOptions getter.
var impliedOptions = []impliedOption{
{name: "Module", dependencies: []string{"Target"}, compute: computeFn((*core.CompilerOptions).GetEmitModuleKind)},
{name: "ModuleResolution", dependencies: []string{"Module", "Target"}, compute: computeFn((*core.CompilerOptions).GetModuleResolutionKind)},
{name: "ModuleDetection", dependencies: []string{"Module", "Target"}, compute: computeFn((*core.CompilerOptions).GetEmitModuleDetectionKind)},
{name: "IsolatedModules", dependencies: []string{"VerbatimModuleSyntax"}, compute: computeFn((*core.CompilerOptions).GetIsolatedModules)},
{name: "PreserveConstEnums", dependencies: []string{"IsolatedModules", "VerbatimModuleSyntax"}, compute: computeFn((*core.CompilerOptions).ShouldPreserveConstEnums)},
{name: "Declaration", dependencies: []string{"Composite"}, compute: computeFn((*core.CompilerOptions).GetEmitDeclarations)},
{name: "DeclarationMap", dependencies: []string{"Declaration", "Composite"}, compute: computeFn((*core.CompilerOptions).GetAreDeclarationMapsEnabled)},
{name: "Incremental", dependencies: []string{"Composite"}, compute: computeFn((*core.CompilerOptions).IsIncremental)},
{name: "UseDefineForClassFields", dependencies: []string{"Target", "Module"}, compute: computeFn((*core.CompilerOptions).GetUseDefineForClassFields)},
{name: "ResolvePackageJsonExports", dependencies: []string{"ModuleResolution", "Module", "Target"}, compute: computeFn((*core.CompilerOptions).GetResolvePackageJsonExports)},
{name: "ResolvePackageJsonImports", dependencies: []string{"ModuleResolution", "ResolvePackageJsonExports", "Module", "Target"}, compute: computeFn((*core.CompilerOptions).GetResolvePackageJsonImports)},
{name: "ResolveJsonModule", dependencies: []string{"ModuleResolution", "Module", "Target"}, compute: computeFn((*core.CompilerOptions).GetResolveJsonModule)},
{name: "AllowJs", dependencies: []string{"CheckJs"}, compute: computeFn((*core.CompilerOptions).GetAllowJS)},
{name: "AllowImportingTsExtensions", dependencies: []string{"RewriteRelativeImportExtensions"}, compute: computeFn((*core.CompilerOptions).GetAllowImportingTsExtensions)},
}
// TSConfig represents the output structure for --showConfig
type TSConfig struct {
CompilerOptions *collections.OrderedMap[string, any] `json:"compilerOptions"`
References []any `json:"references,omitzero"`
Files []string `json:"files,omitzero"`
Include []string `json:"include,omitzero"`
Exclude []string `json:"exclude,omitzero"`
CompileOnSave *bool `json:"compileOnSave,omitzero"`
}
// ConvertToTSConfig generates a complete tsconfig representation for --showConfig output,
// matching the behavior of TypeScript's convertToTSConfig function.
func ConvertToTSConfig(configParseResult *ParsedCommandLine, configFileName string) *TSConfig {
if configFileName == "" {
configFileName = "tsconfig.json"
}
normalizedConfigPath := tspath.GetNormalizedAbsolutePath(configFileName, configParseResult.GetCurrentDirectory())
comparePathsOptions := tspath.ComparePathsOptions{
CurrentDirectory: configParseResult.GetCurrentDirectory(),
UseCaseSensitiveFileNames: configParseResult.UseCaseSensitiveFileNames(),
}
// Build the list of all resolved files as relative paths from the config file.
var files []string
for _, f := range configParseResult.FileNames() {
normalizedFilePath := tspath.GetNormalizedAbsolutePath(f, configParseResult.GetCurrentDirectory())
relativePath := tspath.GetRelativePathFromFile(normalizedConfigPath, normalizedFilePath, comparePathsOptions)
files = append(files, relativePath)
}
// Serialize compiler options
optionMap := serializeCompilerOptions(configParseResult.CompilerOptions(), normalizedConfigPath, comparePathsOptions)
// Remove command-line-only options from the output
for _, name := range []string{
"showConfig", "configFile", "configFilePath", "help", "init",
"listFilesOnly", "listEmittedFiles", "project", "build", "version",
} {
optionMap.Delete(name)
}
// Add implied compiler options (options that are derived from explicitly set options,
// such as moduleResolution implied by module, or useDefineForClassFields implied by target).
// This mirrors TypeScript's convertToTSConfig computedOptions logic.
addImpliedOptions(optionMap, configParseResult.CompilerOptions(), normalizedConfigPath, comparePathsOptions)
config := &TSConfig{
CompilerOptions: optionMap,
}
// Add references
if refs := configParseResult.ProjectReferences(); len(refs) > 0 {
var references []any
for _, r := range refs {
ref := &collections.OrderedMap[string, any]{}
ref.Set("path", r.OriginalPath)
if r.Circular {
ref.Set("circular", true)
}
references = append(references, ref)
}
config.References = references
}
// Add files
if len(files) > 0 {
config.Files = files
}
// Add include/exclude from configFileSpecs
if configParseResult.ConfigFile != nil && configParseResult.ConfigFile.configFileSpecs != nil {
specs := configParseResult.ConfigFile.configFileSpecs
include := filterSameAsDefaultInclude(specs.validatedIncludeSpecs)
if len(include) > 0 {
config.Include = include
}
config.Exclude = specs.validatedExcludeSpecs
}
// Add compileOnSave
if configParseResult.CompileOnSave != nil && *configParseResult.CompileOnSave {
t := true
config.CompileOnSave = &t
}
return config
}
// filterSameAsDefaultInclude returns nil if specs is the default include spec ["**/*"]
func filterSameAsDefaultInclude(specs []string) []string {
if len(specs) == 0 {
return nil
}
if len(specs) == 1 && specs[0] == defaultIncludeSpec {
return nil
}
return specs
}
// getNameOfCompilerOptionValue returns the string key for a given enum value by
// searching the option's enum map.
func getNameOfCompilerOptionValue(value any, enumMap *collections.OrderedMap[string, any]) string {
for k, v := range enumMap.Entries() {
if v == value {
return k
}
}
return ""
}
// serializeCompilerOptions converts CompilerOptions to an ordered map with
// string names as keys and serialized values (enums as strings, paths as
// relative paths, etc.) matching the output of tsc --showConfig.
func serializeCompilerOptions(options *core.CompilerOptions, configFilePath string, comparePathsOptions tspath.ComparePathsOptions) *collections.OrderedMap[string, any] {
result := collections.NewOrderedMapWithSizeHint[string, any](32)
configDir := tspath.GetDirectoryPath(configFilePath)
optionsValue := reflect.ValueOf(options).Elem()
optionsTypeInfo := reflect.TypeFor[core.CompilerOptions]()
for i := range optionsValue.NumField() {
field := optionsTypeInfo.Field(i)
if !field.IsExported() {
continue
}
optionDecl := CommandLineCompilerOptionsMap.Get(field.Name)
if optionDecl == nil {
continue
}
// Skip command-line-only and output formatting options
if optionDecl.Category == diagnostics.Command_line_Options || optionDecl.Category == diagnostics.Output_Formatting {
continue
}
fieldValue := optionsValue.Field(i)
// Skip zero values (unset options)
if fieldValue.IsZero() {
continue
}
name := optionDecl.Name
value := fieldValue.Interface()
enumMap := optionDecl.EnumMap()
if enumMap != nil {
// Enum option - convert numeric value to string name
serialized := serializeEnumValue(value, enumMap)
if serialized != "" {
result.Set(name, serialized)
}
continue
}
switch optionDecl.Kind {
case CommandLineOptionTypeListOrElement:
debug.Assert(false, "listOrElement option should not reach serialization")
case CommandLineOptionTypeList:
elem := optionDecl.Elements()
if elem != nil && elem.IsFilePath {
// List of file paths - make relative
if strs, ok := value.([]string); ok {
relPaths := make([]string, len(strs))
for j, s := range strs {
absPath := tspath.GetNormalizedAbsolutePath(s, configDir)
relPaths[j] = tspath.GetRelativePathFromFile(configFilePath, absPath, comparePathsOptions)
}
result.Set(name, relPaths)
continue
}
}
if elem != nil && elem.EnumMap() != nil {
// List of enum values (e.g., lib)
elemMap := elem.EnumMap()
if strs, ok := value.([]string); ok {
serialized := make([]string, 0, len(strs))
for _, s := range strs {
// lib values are already stored as the d.ts filename, need to find original key
found := getNameOfCompilerOptionValue(s, elemMap)
if found != "" {
serialized = append(serialized, found)
} else {
serialized = append(serialized, s)
}
}
result.Set(name, serialized)
continue
}
}
result.Set(name, value)
case CommandLineOptionTypeString:
if optionDecl.IsFilePath {
// File path option - make relative to config
if s, ok := value.(string); ok && s != "" {
absPath := tspath.GetNormalizedAbsolutePath(s, configDir)
result.Set(name, tspath.GetRelativePathFromFile(configFilePath, absPath, comparePathsOptions))
continue
}
}
result.Set(name, value)
case CommandLineOptionTypeBoolean:
if t, ok := value.(core.Tristate); ok {
if t.IsTrue() {
result.Set(name, true)
} else if t.IsFalse() {
result.Set(name, false)
}
} else {
result.Set(name, value)
}
case CommandLineOptionTypeNumber:
result.Set(name, value)
default:
result.Set(name, value)
}
}
return result
}
// serializeEnumValue converts an enum field value to its corresponding string key
// using the option's enum map. It handles int32-based enum types.
func serializeEnumValue(value any, enumMap *collections.OrderedMap[string, any]) string {
// The enum maps store values as core.ModuleKind, core.ScriptTarget, etc.
// But those are all int32 underneath. We need to compare by the underlying int32 value.
rv := reflect.ValueOf(value)
if rv.CanInt() {
intVal := rv.Int()
for k, v := range enumMap.Entries() {
ev := reflect.ValueOf(v)
if ev.CanInt() && ev.Int() == intVal {
return k
}
}
}
// Fallback: direct comparison
return getNameOfCompilerOptionValue(value, enumMap)
}
// addImpliedOptions adds compiler options that are implied by other explicitly-set options,
// mirroring TypeScript's convertToTSConfig behavior for computedOptions.
// For example, when module: nodenext is set, moduleResolution: nodenext is implied.
func addImpliedOptions(
optionMap *collections.OrderedMap[string, any],
options *core.CompilerOptions,
_ string,
_ tspath.ComparePathsOptions,
) {
// Build the set of explicitly provided option JSON names (e.g., "module", "target").
provided := make(map[string]bool, optionMap.Size())
for k := range optionMap.Keys() {
provided[k] = true
}
defaultOpts := &core.CompilerOptions{}
for _, entry := range impliedOptions {
// Get the option declaration for this implied option (using case-insensitive lookup).
optionDecl := CommandLineCompilerOptionsMap.Get(entry.name)
if optionDecl == nil {
continue
}
// Skip if this option is already explicitly provided.
if provided[optionDecl.Name] {
continue
}
// Check if any direct dependency is in the provided set.
// This mirrors TypeScript's optionDependsOn check.
if !anyDependencyProvided(entry.dependencies, provided) {
continue
}
// Compute the effective value with current options and the default value with empty options.
implied := entry.compute(options)
defaultVal := entry.compute(defaultOpts)
// If the implied value equals the default, this option doesn't add useful information.
if reflect.DeepEqual(implied, defaultVal) {
continue
}
// Serialize the implied value and add it to the option map.
serialized := serializeImpliedOptionValue(optionDecl, implied)
if serialized == nil {
continue
}
optionMap.Set(optionDecl.Name, serialized)
}
}
// anyDependencyProvided returns true if any of the given dependency names
// (using Go field names like "Target") corresponds to an option in the provided set.
func anyDependencyProvided(dependencies []string, provided map[string]bool) bool {
for _, dep := range dependencies {
depDecl := CommandLineCompilerOptionsMap.Get(dep)
if depDecl != nil && provided[depDecl.Name] {
return true
}
}
return false
}
// serializeImpliedOptionValue converts a computed implied option value to its serializable form.
// For enum options, it converts numeric values to their string names.
// For boolean options, it returns the bool directly.
func serializeImpliedOptionValue(optionDecl *CommandLineOption, value any) any {
if value == nil {
return nil
}
enumMap := optionDecl.EnumMap()
if enumMap != nil {
s := serializeEnumValue(value, enumMap)
if s != "" {
return s
}
return nil
}
switch v := value.(type) {
case bool:
return v
case core.Tristate:
if v.IsTrue() {
return true
} else if v.IsFalse() {
return false
}
return nil
}
return value
}

View File

@@ -0,0 +1,14 @@
package tsoptions_test
import (
"testing"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/testutil/baseline"
)
func TestMain(m *testing.M) {
core.ApplyDebugStackLimit()
defer baseline.Track()()
m.Run()
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,14 @@
package tsoptionstest
import (
"github.com/microsoft/typescript-go/internal/tsoptions"
"github.com/microsoft/typescript-go/internal/tspath"
"gotest.tools/v3/assert"
)
func GetParsedCommandLine(t assert.TestingT, jsonText string, files map[string]string, currentDirectory string, useCaseSensitiveFileNames bool) *tsoptions.ParsedCommandLine {
host := NewVFSParseConfigHost(files, currentDirectory, useCaseSensitiveFileNames)
configFileName := tspath.CombinePaths(currentDirectory, "tsconfig.json")
tsconfigSourceFile := tsoptions.NewTsconfigSourceFileFromFilePath(configFileName, tspath.ToPath(configFileName, currentDirectory, useCaseSensitiveFileNames), jsonText)
return tsoptions.ParseJsonSourceFileConfigFileContent(tsconfigSourceFile, host, currentDirectory, nil, nil, configFileName, nil, nil, nil)
}

View File

@@ -0,0 +1,41 @@
package tsoptionstest
import (
"github.com/microsoft/typescript-go/internal/tsoptions"
"github.com/microsoft/typescript-go/internal/tspath"
"github.com/microsoft/typescript-go/internal/vfs"
"github.com/microsoft/typescript-go/internal/vfs/vfstest"
)
func fixRoot(path string) string {
rootLength := tspath.GetRootLength(path)
if rootLength == 0 {
return path
}
if len(path) == rootLength {
return "."
}
return path[rootLength:]
}
type VfsParseConfigHost struct {
Vfs vfs.FS
CurrentDirectory string
}
var _ tsoptions.ParseConfigHost = (*VfsParseConfigHost)(nil)
func (h *VfsParseConfigHost) FS() vfs.FS {
return h.Vfs
}
func (h *VfsParseConfigHost) GetCurrentDirectory() string {
return h.CurrentDirectory
}
func NewVFSParseConfigHost(files map[string]string, currentDirectory string, useCaseSensitiveFileNames bool) *VfsParseConfigHost {
return &VfsParseConfigHost{
Vfs: vfstest.FromMap(files, useCaseSensitiveFileNames),
CurrentDirectory: currentDirectory,
}
}

View File

@@ -0,0 +1,134 @@
package tsoptions
import (
"strings"
"github.com/microsoft/typescript-go/internal/tspath"
"github.com/microsoft/typescript-go/internal/vfs/vfsmatch"
)
func getWildcardDirectories(include []string, exclude []string, comparePathsOptions tspath.ComparePathsOptions) map[string]bool {
// We watch a directory recursively if it contains a wildcard anywhere in a directory segment
// of the pattern:
//
// /a/b/**/d - Watch /a/b recursively to catch changes to any d in any subfolder recursively
// /a/b/*/d - Watch /a/b recursively to catch any d in any immediate subfolder, even if a new subfolder is added
// /a/b - Watch /a/b recursively to catch changes to anything in any recursive subfoler
//
// We watch a directory without recursion if it contains a wildcard in the file segment of
// the pattern:
//
// /a/b/* - Watch /a/b directly to catch any new file
// /a/b/a?z - Watch /a/b directly to catch any new file matching a?z
if len(include) == 0 {
return nil
}
excludeMatcher := vfsmatch.NewSpecMatcher(exclude, comparePathsOptions.CurrentDirectory, vfsmatch.UsageExclude, comparePathsOptions.UseCaseSensitiveFileNames)
wildcardDirectories := make(map[string]bool)
wildCardKeyToPath := make(map[string]string)
var recursiveKeys []string
for _, file := range include {
spec := tspath.NormalizePath(tspath.CombinePaths(comparePathsOptions.CurrentDirectory, file))
if excludeMatcher != nil && excludeMatcher.MatchString(spec) {
continue
}
match := getWildcardDirectoryFromSpec(spec, comparePathsOptions.UseCaseSensitiveFileNames)
if match != nil {
key := match.Key
path := match.Path
recursive := match.Recursive
existingPath, existsPath := wildCardKeyToPath[key]
var existingRecursive bool
if existsPath {
existingRecursive = wildcardDirectories[existingPath]
}
if !existsPath || (!existingRecursive && recursive) {
pathToUse := path
if existsPath {
pathToUse = existingPath
}
wildcardDirectories[pathToUse] = recursive
if !existsPath {
wildCardKeyToPath[key] = path
}
if recursive {
recursiveKeys = append(recursiveKeys, key)
}
}
}
// Remove any subpaths under an existing recursively watched directory
for path := range wildcardDirectories {
for _, recursiveKey := range recursiveKeys {
key := toCanonicalKey(path, comparePathsOptions.UseCaseSensitiveFileNames)
if key != recursiveKey && tspath.ContainsPath(recursiveKey, key, comparePathsOptions) {
delete(wildcardDirectories, path)
}
}
}
}
return wildcardDirectories
}
func toCanonicalKey(path string, useCaseSensitiveFileNames bool) string {
if useCaseSensitiveFileNames {
return path
}
return strings.ToLower(path)
}
// wildcardDirectoryMatch represents the result of a wildcard directory match
type wildcardDirectoryMatch struct {
Key string
Path string
Recursive bool
}
func getWildcardDirectoryFromSpec(spec string, useCaseSensitiveFileNames bool) *wildcardDirectoryMatch {
// Find the first occurrence of a wildcard character
firstWildcard := strings.IndexAny(spec, "*?")
if firstWildcard != -1 {
// Find the last directory separator before the wildcard
lastSepBeforeWildcard := strings.LastIndexByte(spec[:firstWildcard], tspath.DirectorySeparator)
if lastSepBeforeWildcard != -1 {
path := spec[:lastSepBeforeWildcard]
lastDirectorySeparatorIndex := strings.LastIndexByte(spec, tspath.DirectorySeparator)
// Determine if this should be watched recursively:
// recursive if the wildcard appears in a directory segment (not just the final file segment)
recursive := firstWildcard < lastDirectorySeparatorIndex
return &wildcardDirectoryMatch{
Key: toCanonicalKey(path, useCaseSensitiveFileNames),
Path: path,
Recursive: recursive,
}
}
}
if lastSepIndex := strings.LastIndexByte(spec, tspath.DirectorySeparator); lastSepIndex != -1 {
lastSegment := spec[lastSepIndex+1:]
if vfsmatch.IsImplicitGlob(lastSegment) {
path := tspath.RemoveTrailingDirectorySeparator(spec)
return &wildcardDirectoryMatch{
Key: toCanonicalKey(path, useCaseSensitiveFileNames),
Path: path,
Recursive: true,
}
}
}
return nil
}

View File

@@ -0,0 +1,85 @@
package tsoptions
import (
"testing"
"github.com/microsoft/typescript-go/internal/tspath"
"gotest.tools/v3/assert"
)
func TestGetWildcardDirectories_DotPrefixedIncludeWithDotDirExclude(t *testing.T) {
t.Parallel()
// https://github.com/microsoft/typescript-go/issues/3733
// "./"-prefixed include specs must be fully normalized before being tested
// against exclude patterns; otherwise the leftover literal "." path segment
// matches dot-directory excludes like "**/.*/", silently dropping every
// wildcard directory (and with them, root file watching for the config).
result := getWildcardDirectories(
[]string{"./app/**/*.ts", "./app/**/*.tsx"},
[]string{"**/node_modules", "**/.*/", "./build"},
tspath.ComparePathsOptions{
CurrentDirectory: "/home/projects/monorepo/apps/web",
UseCaseSensitiveFileNames: true,
},
)
assert.DeepEqual(t, result, map[string]bool{"/home/projects/monorepo/apps/web/app": true})
}
func TestGetWildcardDirectories_NonASCIICharacters(t *testing.T) {
t.Parallel()
tests := []struct {
name string
include []string
exclude []string
currentDirectory string
useCaseSensitiveFileNames bool
}{
{
name: "Norwegian character æ in path",
include: []string{"src/**/*.test.ts", "src/**/*.stories.ts", "src/**/*.mdx"},
exclude: []string{"node_modules"},
currentDirectory: "C:/Users/TobiasLægreid/dev/app/frontend/packages/react",
useCaseSensitiveFileNames: false,
},
{
name: "Japanese characters in path",
include: []string{"src/**/*.ts"},
exclude: []string{"テスト"},
currentDirectory: "/Users/ユーザー/プロジェクト",
useCaseSensitiveFileNames: true,
},
{
name: "Chinese characters in path",
include: []string{"源代码/**/*.js"},
exclude: []string{"节点模块"},
currentDirectory: "/home/用户/项目",
useCaseSensitiveFileNames: true,
},
{
name: "Various Unicode characters",
include: []string{"src/**/*.ts"},
exclude: []string{"node_modules"},
currentDirectory: "/Users/Müller/café/naïve/résumé",
useCaseSensitiveFileNames: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
comparePathsOptions := tspath.ComparePathsOptions{
CurrentDirectory: tt.currentDirectory,
UseCaseSensitiveFileNames: tt.useCaseSensitiveFileNames,
}
result := getWildcardDirectories(tt.include, tt.exclude, comparePathsOptions)
if result == nil {
t.Fatalf("expected non-nil result")
}
})
}
}