package tsoptions import ( "strconv" "strings" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/collections" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/core" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/diagnostics" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/stringutil" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/tspath" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/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 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()) optionsWithAbsolutePaths := convertToOptionsWithAbsolutePaths(parser.options, 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()) 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, 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, ) *commandLineParser { parser := &commandLineParser{ fs: fs, 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, nil) } 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) { 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 || text == "" { // !!! 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" { if diag == nil { diag = p.workerDiagnostics.OptionTypeMismatchDiagnostic } 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 { p.options.Set(opt.Name, num) } 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)} }