package tsoptions import ( "fmt" "reflect" "regexp" "slices" "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/debug" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/diagnostics" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/jsnum" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/module" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/parser" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/tspath" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/vfs" "github.com/dlclark/regexp2" ) type extendsResult struct { options *core.CompilerOptions // watchOptions compiler.WatchOptions watchOptionsCopied bool include []any exclude []any files []any compileOnSave bool extendedSourceFiles collections.Set[string] } var compilerOptionsDeclaration = &CommandLineOption{ Name: "compilerOptions", Kind: CommandLineOptionTypeObject, ElementOptions: CommandLineCompilerOptionsMap, } var compileOnSaveCommandLineOption = &CommandLineOption{ Name: "compileOnSave", Kind: CommandLineOptionTypeBoolean, DefaultValueDescription: false, } var extendsOptionDeclaration = &CommandLineOption{ Name: "extends", Kind: CommandLineOptionTypeListOrElement, Category: diagnostics.File_Management, ElementOptions: commandLineOptionsToMap([]*CommandLineOption{ {Name: "extends", Kind: CommandLineOptionTypeString}, }), } var tsconfigRootOptionsMap = &CommandLineOption{ Name: "undefined", // should never be needed since this is root Kind: CommandLineOptionTypeObject, ElementOptions: commandLineOptionsToMap([]*CommandLineOption{ compilerOptionsDeclaration, // watchOptionsDeclaration, typeAcquisitionDeclaration, extendsOptionDeclaration, { Name: "references", Kind: CommandLineOptionTypeList, // should be a list of projectReference // Category: diagnostics.Projects, }, { Name: "files", Kind: CommandLineOptionTypeList, // Category: diagnostics.File_Management, }, { Name: "include", Kind: CommandLineOptionTypeList, // Category: diagnostics.File_Management, // DefaultValueDescription: diagnostics.if_files_is_specified_otherwise_Asterisk_Asterisk_Slash_Asterisk, }, { Name: "exclude", Kind: CommandLineOptionTypeList, // Category: diagnostics.File_Management, // DefaultValueDescription: diagnostics.Node_modules_bower_components_jspm_packages_plus_the_value_of_outDir_if_one_is_specified, }, compileOnSaveCommandLineOption, }), } type configFileSpecs struct { filesSpecs any // Present to report errors (user specified specs), validatedIncludeSpecs are used for file name matching includeSpecs any // Present to report errors (user specified specs), validatedExcludeSpecs are used for file name matching excludeSpecs any validatedFilesSpec []string validatedIncludeSpecs []string validatedExcludeSpecs []string validatedFilesSpecBeforeSubstitution []string validatedIncludeSpecsBeforeSubstitution []string isDefaultIncludeSpec bool } func (c *configFileSpecs) matchesExclude(fileName string, comparePathsOptions tspath.ComparePathsOptions) bool { if len(c.validatedExcludeSpecs) == 0 { return false } excludePattern := vfs.GetRegularExpressionForWildcard(c.validatedExcludeSpecs, comparePathsOptions.CurrentDirectory, "exclude") excludeRegex := vfs.GetRegexFromPattern(excludePattern, comparePathsOptions.UseCaseSensitiveFileNames) if match, err := excludeRegex.MatchString(fileName); err == nil && match { return true } if !tspath.HasExtension(fileName) { if match, err := excludeRegex.MatchString(tspath.EnsureTrailingDirectorySeparator(fileName)); err == nil && match { return true } } return false } func (c *configFileSpecs) getMatchedIncludeSpec(fileName string, comparePathsOptions tspath.ComparePathsOptions) string { if len(c.validatedIncludeSpecs) == 0 { return "" } for index, spec := range c.validatedIncludeSpecs { includePattern := vfs.GetPatternFromSpec(spec, comparePathsOptions.CurrentDirectory, "files") if includePattern != "" { includeRegex := vfs.GetRegexFromPattern(includePattern, comparePathsOptions.UseCaseSensitiveFileNames) if match, err := includeRegex.MatchString(fileName); err == nil && match { return c.validatedIncludeSpecsBeforeSubstitution[index] } } } return "" } func (c *configFileSpecs) getMatchedFileSpec(fileName string, comparePathsOptions tspath.ComparePathsOptions) string { if len(c.validatedFilesSpec) == 0 { return "" } filePath := tspath.ToPath(fileName, comparePathsOptions.CurrentDirectory, comparePathsOptions.UseCaseSensitiveFileNames) for index, spec := range c.validatedFilesSpec { if tspath.ToPath(spec, comparePathsOptions.CurrentDirectory, comparePathsOptions.UseCaseSensitiveFileNames) == filePath { return c.validatedFilesSpecBeforeSubstitution[index] } } return "" } type FileExtensionInfo struct { Extension string IsMixedContent bool ScriptKind core.ScriptKind } type ExtendedConfigCache interface { GetExtendedConfig(fileName string, path tspath.Path, parse func() *ExtendedConfigCacheEntry) *ExtendedConfigCacheEntry } type ExtendedConfigCacheEntry struct { extendedResult *TsConfigSourceFile extendedConfig *parsedTsconfig errors []*ast.Diagnostic } type parsedTsconfig struct { raw any options *core.CompilerOptions // watchOptions *core.WatchOptions typeAcquisition *core.TypeAcquisition // Note that the case of the config path has not yet been normalized, as no files have been imported into the project yet extendedConfigPath any } func parseOwnConfigOfJsonSourceFile( sourceFile *ast.SourceFile, host ParseConfigHost, basePath string, configFileName string, ) (*parsedTsconfig, []*ast.Diagnostic) { compilerOptions := getDefaultCompilerOptions(configFileName) typeAcquisition := getDefaultTypeAcquisition(configFileName) // var watchOptions *compiler.WatchOptions var extendedConfigPath any var rootCompilerOptions []*ast.PropertyName var errors []*ast.Diagnostic onPropertySet := func( keyText string, value any, propertyAssignment *ast.PropertyAssignment, parentOption *CommandLineOption, // TsConfigOnlyOption, option *CommandLineOption, ) (any, []*ast.Diagnostic) { // Ensure value is verified except for extends which is handled in its own way for error reporting var propertySetErrors []*ast.Diagnostic if option != nil && option != extendsOptionDeclaration { value, propertySetErrors = convertJsonOption(option, value, basePath, propertyAssignment, propertyAssignment.Initializer, sourceFile) } if parentOption != nil && parentOption.Name != "undefined" && value != nil { if option != nil && option.Name != "" { var parseDiagnostics []*ast.Diagnostic switch parentOption.Name { case "compilerOptions": parseDiagnostics = ParseCompilerOptions(option.Name, value, compilerOptions) case "typeAcquisition": parseDiagnostics = ParseTypeAcquisition(option.Name, value, typeAcquisition) } propertySetErrors = append(propertySetErrors, parseDiagnostics...) } else if keyText != "" && extraKeyDiagnostics(parentOption.Name) != nil { unknownNameDiag := extraKeyDiagnostics(parentOption.Name) if parentOption.ElementOptions != nil { // !!! TODO: support suggestion propertySetErrors = append(propertySetErrors, createUnknownOptionError( keyText, unknownNameDiag, "", /*unknownOptionErrorText*/ propertyAssignment.Name(), sourceFile, nil, /*alternateMode*/ )) } else { // errors = append(errors, ast.NewCompilerDiagnostic(diagnostics.Unknown_compiler_option_0_Did_you_mean_1, keyText, core.FindKey(parentOption.ElementOptions, keyText))) } } } else if parentOption == tsconfigRootOptionsMap { if option == extendsOptionDeclaration { configPath, err := getExtendsConfigPathOrArray(value, host, basePath, configFileName, propertyAssignment, propertyAssignment.Initializer, sourceFile) extendedConfigPath = configPath propertySetErrors = append(propertySetErrors, err...) } else if option == nil { if keyText == "excludes" { propertySetErrors = append(propertySetErrors, CreateDiagnosticForNodeInSourceFile(sourceFile, propertyAssignment.Name(), diagnostics.Unknown_option_excludes_Did_you_mean_exclude)) } if core.Find(OptionsDeclarations, func(option *CommandLineOption) bool { return option.Name == keyText }) != nil { rootCompilerOptions = append(rootCompilerOptions, propertyAssignment.Name()) } } } return value, propertySetErrors } json, err := convertConfigFileToObject( sourceFile, &jsonConversionNotifier{ tsconfigRootOptionsMap, onPropertySet, }, ) errors = append(errors, err...) // if len(rootCompilerOptions) != 0 && json != nil && json.CompilerOptions != nil { // errors = append(errors, ast.NewDiagnostic(sourceFile, rootCompilerOptions[0], diagnostics.X_0_should_be_set_inside_the_compilerOptions_object_of_the_config_json_file)) // } return &parsedTsconfig{ raw: json, options: compilerOptions, // watchOptions: watchOptions, typeAcquisition: typeAcquisition, extendedConfigPath: extendedConfigPath, }, errors } type TsConfigSourceFile struct { ExtendedSourceFiles []string configFileSpecs *configFileSpecs SourceFile *ast.SourceFile } func tsconfigToSourceFile(tsconfigSourceFile *TsConfigSourceFile) *ast.SourceFile { if tsconfigSourceFile == nil { return nil } return tsconfigSourceFile.SourceFile } func NewTsconfigSourceFileFromFilePath(configFileName string, configPath tspath.Path, configSourceText string) *TsConfigSourceFile { sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{ FileName: configFileName, Path: configPath, }, configSourceText, core.ScriptKindJSON) return &TsConfigSourceFile{ SourceFile: sourceFile, } } type jsonConversionNotifier struct { rootOptions *CommandLineOption onPropertySet func(keyText string, value any, propertyAssignment *ast.PropertyAssignment, parentOption *CommandLineOption, option *CommandLineOption) (any, []*ast.Diagnostic) } func convertConfigFileToObject( sourceFile *ast.SourceFile, jsonConversionNotifier *jsonConversionNotifier, ) (any, []*ast.Diagnostic) { var rootExpression *ast.Expression if len(sourceFile.Statements.Nodes) > 0 { rootExpression = sourceFile.Statements.Nodes[0].AsExpressionStatement().Expression } if rootExpression != nil && rootExpression.Kind != ast.KindObjectLiteralExpression { baseFileName := "tsconfig.json" if tspath.GetBaseFileName(sourceFile.FileName()) == "jsconfig.json" { baseFileName = "jsconfig.json" } errors := []*ast.Diagnostic{ast.NewCompilerDiagnostic(diagnostics.The_root_value_of_a_0_file_must_be_an_object, baseFileName)} // Last-ditch error recovery. Somewhat useful because the JSON parser will recover from some parse errors by // synthesizing a top-level array literal expression. There's a reasonable chance the first element of that // array is a well-formed configuration object, made into an array element by stray characters. if ast.IsArrayLiteralExpression(rootExpression) { firstObject := core.Find(rootExpression.AsArrayLiteralExpression().Elements.Nodes, ast.IsObjectLiteralExpression) if firstObject != nil { return convertToJson(sourceFile, firstObject, true /*returnValue*/, jsonConversionNotifier) } } return &collections.OrderedMap[string, any]{}, errors } return convertToJson(sourceFile, rootExpression, true, jsonConversionNotifier) } var orderedMapType = reflect.TypeFor[*collections.OrderedMap[string, any]]() func isCompilerOptionsValue(option *CommandLineOption, value any) bool { if option != nil { if value == nil { return !option.DisallowNullOrUndefined() } if option.Kind == "list" { return reflect.TypeOf(value).Kind() == reflect.Slice } if option.Kind == "listOrElement" { if reflect.TypeOf(value).Kind() == reflect.Slice { return true } else { return isCompilerOptionsValue(option.Elements(), value) } } if option.Kind == "string" { return reflect.TypeOf(value).Kind() == reflect.String } if option.Kind == "boolean" { return reflect.TypeOf(value).Kind() == reflect.Bool } if option.Kind == "number" { return reflect.TypeOf(value).Kind() == reflect.Float64 } if option.Kind == "object" { return reflect.TypeOf(value) == orderedMapType } if option.Kind == "enum" && reflect.TypeOf(value).Kind() == reflect.String { return true } } return false } func validateJsonOptionValue( opt *CommandLineOption, val any, valueExpression *ast.Expression, sourceFile *ast.SourceFile, ) (any, []*ast.Diagnostic) { if val == nil { return nil, nil } errors := []*ast.Diagnostic{} if opt.extraValidation { diag := specToDiagnostic(val.(string), false) if diag != nil { errors = append(errors, CreateDiagnosticForNodeInSourceFileOrCompilerDiagnostic(sourceFile, valueExpression, diag)) return nil, errors } } return val, nil } func convertJsonOptionOfListType( option *CommandLineOption, values any, basePath string, propertyAssignment *ast.PropertyAssignment, valueExpression *ast.Node, sourceFile *ast.SourceFile, ) ([]any, []*ast.Diagnostic) { var expression *ast.Node var errors []*ast.Diagnostic if values, ok := values.([]any); ok { mappedValues := core.MapIndex(values, func(v any, index int) any { if valueExpression != nil { expression = valueExpression.AsArrayLiteralExpression().Elements.Nodes[index] } result, err := convertJsonOption(option.Elements(), v, basePath, propertyAssignment, expression, sourceFile) errors = append(errors, err...) return result }) filteredValues := mappedValues if !option.listPreserveFalsyValues { filteredValues = core.Filter(mappedValues, func(v any) bool { return (v != nil && v != false && v != 0 && v != "") }) } return filteredValues, errors } return nil, errors } const configDirTemplate = "${configDir}" func startsWithConfigDirTemplate(value any) bool { str, ok := value.(string) if !ok { return false } return strings.HasPrefix(strings.ToLower(str), strings.ToLower(configDirTemplate)) } func normalizeNonListOptionValue(option *CommandLineOption, basePath string, value any) any { if option.IsFilePath { value = tspath.NormalizeSlashes(value.(string)) if !startsWithConfigDirTemplate(value) { value = tspath.GetNormalizedAbsolutePath(value.(string), basePath) } if value == "" { value = "." } } return value } func convertJsonOption( opt *CommandLineOption, value any, basePath string, propertyAssignment *ast.PropertyAssignment, valueExpression *ast.Expression, sourceFile *ast.SourceFile, ) (any, []*ast.Diagnostic) { if opt.IsCommandLineOnly { var nodeValue *ast.Node if propertyAssignment != nil { nodeValue = propertyAssignment.Name() } if sourceFile == nil && nodeValue == nil { return nil, []*ast.Diagnostic{ast.NewCompilerDiagnostic(diagnostics.Option_0_can_only_be_specified_on_command_line, opt.Name)} } else { return nil, []*ast.Diagnostic{CreateDiagnosticForNodeInSourceFileOrCompilerDiagnostic(sourceFile, nodeValue, diagnostics.Option_0_can_only_be_specified_on_command_line, opt.Name)} } } if isCompilerOptionsValue(opt, value) { switch opt.Kind { case CommandLineOptionTypeList: return convertJsonOptionOfListType(opt, value, basePath, propertyAssignment, valueExpression, sourceFile) // as ArrayLiteralExpression | undefined case CommandLineOptionTypeListOrElement: if reflect.TypeOf(value).Kind() == reflect.Slice { return convertJsonOptionOfListType(opt, value, basePath, propertyAssignment, valueExpression, sourceFile) } else { return convertJsonOption(opt.Elements(), value, basePath, propertyAssignment, valueExpression, sourceFile) } case CommandLineOptionTypeEnum: return convertJsonOptionOfEnumType(opt, value.(string), valueExpression, sourceFile) } validatedValue, errors := validateJsonOptionValue(opt, value, valueExpression, sourceFile) if len(errors) > 0 || validatedValue == nil { return validatedValue, errors } else { return normalizeNonListOptionValue(opt, basePath, validatedValue), errors } } else { return nil, []*ast.Diagnostic{CreateDiagnosticForNodeInSourceFileOrCompilerDiagnostic(sourceFile, valueExpression, diagnostics.Compiler_option_0_requires_a_value_of_type_1, opt.Name, getCompilerOptionValueTypeString(opt))} } } func getExtendsConfigPathOrArray( value CompilerOptionsValue, host ParseConfigHost, basePath string, configFileName string, propertyAssignment *ast.PropertyAssignment, valueExpression *ast.Expression, sourceFile *ast.SourceFile, ) ([]string, []*ast.Diagnostic) { var extendedConfigPathArray []string newBase := basePath if configFileName != "" { newBase = directoryOfCombinedPath(configFileName, basePath) } if reflect.TypeOf(value).Kind() == reflect.String { val, err := getExtendsConfigPath(value.(string), host, newBase, valueExpression, sourceFile) if val != "" { extendedConfigPathArray = append(extendedConfigPathArray, val) } return extendedConfigPathArray, err } var errors []*ast.Diagnostic if reflect.TypeOf(value).Kind() == reflect.Slice { for index, fileName := range value.([]any) { var expression *ast.Expression = nil if valueExpression != nil { expression = valueExpression.AsArrayLiteralExpression().Elements.Nodes[index] } if reflect.TypeOf(fileName).Kind() == reflect.String { val, err := getExtendsConfigPath(fileName.(string), host, newBase, expression, sourceFile) if val != "" { extendedConfigPathArray = append(extendedConfigPathArray, val) } errors = append(errors, err...) } else { _, err := convertJsonOption(extendsOptionDeclaration.Elements(), value, basePath, propertyAssignment, expression, sourceFile) errors = append(errors, err...) } } } else { _, errors = convertJsonOption(extendsOptionDeclaration, value, basePath, propertyAssignment, valueExpression, sourceFile) } return extendedConfigPathArray, errors } func getExtendsConfigPath( extendedConfig string, host ParseConfigHost, basePath string, valueExpression *ast.Expression, sourceFile *ast.SourceFile, ) (string, []*ast.Diagnostic) { extendedConfig = tspath.NormalizeSlashes(extendedConfig) var errors []*ast.Diagnostic var errorFile *ast.SourceFile if sourceFile != nil { errorFile = sourceFile } if tspath.IsRootedDiskPath(extendedConfig) || strings.HasPrefix(extendedConfig, "./") || strings.HasPrefix(extendedConfig, "../") { extendedConfigPath := tspath.GetNormalizedAbsolutePath(extendedConfig, basePath) if !host.FS().FileExists(extendedConfigPath) && !strings.HasSuffix(extendedConfigPath, tspath.ExtensionJson) { extendedConfigPath = extendedConfigPath + tspath.ExtensionJson if !host.FS().FileExists(extendedConfigPath) { errors = append(errors, CreateDiagnosticForNodeInSourceFileOrCompilerDiagnostic(errorFile, valueExpression, diagnostics.File_0_not_found, extendedConfig)) return "", errors } } return extendedConfigPath, errors } // If the path isn't a rooted or relative path, resolve like a module resolverHost := &resolverHost{host} if resolved := module.ResolveConfig(extendedConfig, tspath.CombinePaths(basePath, "tsconfig.json"), resolverHost); resolved.IsResolved() { return resolved.ResolvedFileName, errors } if extendedConfig == "" { errors = append(errors, CreateDiagnosticForNodeInSourceFileOrCompilerDiagnostic(errorFile, valueExpression, diagnostics.Compiler_option_0_cannot_be_given_an_empty_string, "extends")) } else { errors = append(errors, CreateDiagnosticForNodeInSourceFileOrCompilerDiagnostic(errorFile, valueExpression, diagnostics.File_0_not_found, extendedConfig)) } return "", errors } type tsConfigOptions struct { prop map[string][]string references []*core.ProjectReference notDefined string } type CommandLineOptionNameMap map[string]*CommandLineOption func (m CommandLineOptionNameMap) Get(name string) *CommandLineOption { opt, ok := m[name] if !ok { opt, _ = m[strings.ToLower(name)] } return opt } func commandLineOptionsToMap(compilerOptions []*CommandLineOption) CommandLineOptionNameMap { result := make(map[string]*CommandLineOption, len(compilerOptions)*2) for i := range compilerOptions { result[compilerOptions[i].Name] = compilerOptions[i] result[strings.ToLower(compilerOptions[i].Name)] = compilerOptions[i] } return result } var CommandLineCompilerOptionsMap CommandLineOptionNameMap = commandLineOptionsToMap(OptionsDeclarations) func convertMapToOptions[O optionParser](compilerOptions *collections.OrderedMap[string, any], result O) O { // this assumes any `key`, `value` pair in `options` will have `value` already be the correct type. this function should no error handling for key, value := range compilerOptions.Entries() { result.ParseOption(key, value) } return result } func convertOptionsFromJson[O optionParser](optionsNameMap CommandLineOptionNameMap, jsonOptions any, basePath string, result O) (O, []*ast.Diagnostic) { if jsonOptions == nil { return result, nil } jsonMap, ok := jsonOptions.(*collections.OrderedMap[string, any]) if !ok { // !!! probably should be an error return result, nil } var errors []*ast.Diagnostic for key, value := range jsonMap.Entries() { opt := optionsNameMap.Get(key) if opt == nil { // !!! TODO?: support suggestion errors = append(errors, createUnknownOptionError(key, result.UnknownOptionDiagnostic(), "", nil, nil, nil)) continue } commandLineOptionEnumMapVal := opt.EnumMap() if commandLineOptionEnumMapVal != nil { val, ok := commandLineOptionEnumMapVal.Get(strings.ToLower(value.(string))) if ok { errors = result.ParseOption(key, val) } } else { convertJson, err := convertJsonOption(opt, value, basePath, nil, nil, nil) errors = append(errors, err...) compilerOptionsErr := result.ParseOption(key, convertJson) errors = append(errors, compilerOptionsErr...) } } return result, errors } func convertArrayLiteralExpressionToJson( sourceFile *ast.SourceFile, elements []*ast.Expression, elementOption *CommandLineOption, returnValue bool, ) (any, []*ast.Diagnostic) { if !returnValue { for _, element := range elements { convertPropertyValueToJson(sourceFile, element, elementOption, returnValue, nil) } return nil, nil } // Filter out invalid values if len(elements) == 0 { // Always return an empty array, even if elements is nil. // The parser will produce nil slices instead of allocating empty ones. return []any{}, nil } var errors []*ast.Diagnostic var value []any for _, element := range elements { convertedValue, err := convertPropertyValueToJson(sourceFile, element, elementOption, returnValue, nil) errors = append(errors, err...) if convertedValue != nil { value = append(value, convertedValue) } } return value, errors } func directoryOfCombinedPath(fileName string, basePath string) string { // Use the `getNormalizedAbsolutePath` function to avoid canonicalizing the path, as it must remain noncanonical // until consistent casing errors are reported return tspath.GetDirectoryPath(tspath.GetNormalizedAbsolutePath(fileName, basePath)) } // ParseConfigFileTextToJson parses the text of the tsconfig.json file // fileName is the path to the config file // jsonText is the text of the config file func ParseConfigFileTextToJson(fileName string, path tspath.Path, jsonText string) (any, []*ast.Diagnostic) { jsonSourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{ FileName: fileName, Path: path, }, jsonText, core.ScriptKindJSON) config, errors := convertConfigFileToObject(jsonSourceFile /*jsonConversionNotifier*/, nil) if len(jsonSourceFile.Diagnostics()) > 0 { errors = []*ast.Diagnostic{jsonSourceFile.Diagnostics()[0]} } return config, errors } type ParseConfigHost interface { FS() vfs.FS GetCurrentDirectory() string } type resolverHost struct { ParseConfigHost } func (r *resolverHost) Trace(msg string) {} func ParseJsonSourceFileConfigFileContent( sourceFile *TsConfigSourceFile, host ParseConfigHost, basePath string, existingOptions *core.CompilerOptions, configFileName string, resolutionStack []tspath.Path, extraFileExtensions []FileExtensionInfo, extendedConfigCache ExtendedConfigCache, ) *ParsedCommandLine { // tracing?.push(tracing.Phase.Parse, "parseJsonSourceFileConfigFileContent", { path: sourceFile.fileName }); result := parseJsonConfigFileContentWorker(nil /*json*/, sourceFile, host, basePath, existingOptions, configFileName, resolutionStack, extraFileExtensions, extendedConfigCache) // tracing?.pop(); return result } func convertObjectLiteralExpressionToJson( sourceFile *ast.SourceFile, returnValue bool, node *ast.ObjectLiteralExpression, objectOption *CommandLineOption, jsonConversionNotifier *jsonConversionNotifier, ) (*collections.OrderedMap[string, any], []*ast.Diagnostic) { var result *collections.OrderedMap[string, any] if returnValue { result = &collections.OrderedMap[string, any]{} } var errors []*ast.Diagnostic for _, element := range node.Properties.Nodes { if element.Kind != ast.KindPropertyAssignment { errors = append(errors, ast.NewDiagnostic(sourceFile, element.Loc, diagnostics.Property_assignment_expected)) continue } // !!! // if ast.IsQuestionToken(element) { // errors = append(errors, ast.NewDiagnostic(sourceFile, element.Loc, diagnostics.Property_assignment_expected)) // } if element.Name() != nil && !isDoubleQuotedString(element.Name()) { errors = append(errors, ast.NewDiagnostic(sourceFile, element.Loc, diagnostics.String_literal_with_double_quotes_expected)) } textOfKey := "" if !ast.IsComputedNonLiteralName(element.Name()) { textOfKey, _ = ast.TryGetTextOfPropertyName(element.Name()) } keyText := textOfKey var option *CommandLineOption = nil if keyText != "" && objectOption != nil && objectOption.ElementOptions != nil { option = objectOption.ElementOptions.Get(keyText) } value, err := convertPropertyValueToJson(sourceFile, element.AsPropertyAssignment().Initializer, option, returnValue, jsonConversionNotifier) errors = append(errors, err...) if keyText != "" { if returnValue { result.Set(keyText, value) } // Notify key value set, if user asked for it if jsonConversionNotifier != nil { _, err := jsonConversionNotifier.onPropertySet(keyText, value, element.AsPropertyAssignment(), objectOption, option) errors = append(errors, err...) } } } return result, errors } // convertToJson converts the json syntax tree into the json value and report errors // This returns the json value (apart from checking errors) only if returnValue provided is true. // Otherwise it just checks the errors and returns undefined func convertToJson( sourceFile *ast.SourceFile, rootExpression *ast.Expression, returnValue bool, jsonConversionNotifier *jsonConversionNotifier, ) (any, []*ast.Diagnostic) { if rootExpression == nil { if returnValue { return struct{}{}, nil } else { return nil, nil } } var rootOptions *CommandLineOption if jsonConversionNotifier != nil { rootOptions = jsonConversionNotifier.rootOptions } return convertPropertyValueToJson(sourceFile, rootExpression, rootOptions, returnValue, jsonConversionNotifier) } func isDoubleQuotedString(node *ast.Node) bool { return ast.IsStringLiteral(node) } func convertPropertyValueToJson(sourceFile *ast.SourceFile, valueExpression *ast.Expression, option *CommandLineOption, returnValue bool, jsonConversionNotifier *jsonConversionNotifier) (any, []*ast.Diagnostic) { switch valueExpression.Kind { case ast.KindTrueKeyword: return true, nil case ast.KindFalseKeyword: return false, nil case ast.KindNullKeyword: // todo: how to manage null return nil, nil case ast.KindStringLiteral: if !isDoubleQuotedString(valueExpression) { return valueExpression.AsStringLiteral().Text, []*ast.Diagnostic{ast.NewDiagnostic(sourceFile, valueExpression.Loc, diagnostics.String_literal_with_double_quotes_expected)} } return valueExpression.AsStringLiteral().Text, nil case ast.KindNumericLiteral: return float64(jsnum.FromString(valueExpression.AsNumericLiteral().Text)), nil case ast.KindPrefixUnaryExpression: if valueExpression.AsPrefixUnaryExpression().Operator != ast.KindMinusToken || valueExpression.AsPrefixUnaryExpression().Operand.Kind != ast.KindNumericLiteral { break // not valid JSON syntax } return float64(-jsnum.FromString(valueExpression.AsPrefixUnaryExpression().Operand.AsNumericLiteral().Text)), nil case ast.KindObjectLiteralExpression: objectLiteralExpression := valueExpression.AsObjectLiteralExpression() // Currently having element option declaration in the tsconfig with type "object" // determines if it needs onSetValidOptionKeyValueInParent callback or not // At moment there are only "compilerOptions", "typeAcquisition" and "typingOptions" // that satisfies it and need it to modify options set in them (for normalizing file paths) // vs what we set in the json // If need arises, we can modify this interface and callbacks as needed return convertObjectLiteralExpressionToJson(sourceFile, returnValue, objectLiteralExpression, option, jsonConversionNotifier) case ast.KindArrayLiteralExpression: result, errors := convertArrayLiteralExpressionToJson( sourceFile, valueExpression.AsArrayLiteralExpression().Elements.Nodes, option, returnValue, ) return result, errors } // Not in expected format var errors []*ast.Diagnostic if option != nil { errors = []*ast.Diagnostic{ast.NewDiagnostic(sourceFile, valueExpression.Loc, diagnostics.Compiler_option_0_requires_a_value_of_type_1, option.Name, getCompilerOptionValueTypeString(option))} } else { errors = []*ast.Diagnostic{ast.NewDiagnostic(sourceFile, valueExpression.Loc, diagnostics.Property_value_can_only_be_string_literal_numeric_literal_true_false_null_object_literal_or_array_literal)} } return nil, errors } // ParseJsonConfigFileContent parses the contents of a config file (tsconfig.json). // jsonNode: The contents of the config file to parse // host: Instance of ParseConfigHost used to enumerate files in folder. // basePath: A root directory to resolve relative path entries in the config file to. e.g. outDir func ParseJsonConfigFileContent(json any, host ParseConfigHost, basePath string, existingOptions *core.CompilerOptions, configFileName string, resolutionStack []tspath.Path, extraFileExtensions []FileExtensionInfo, extendedConfigCache ExtendedConfigCache) *ParsedCommandLine { result := parseJsonConfigFileContentWorker(parseJsonToStringKey(json), nil /*sourceFile*/, host, basePath, existingOptions, configFileName, resolutionStack, extraFileExtensions, extendedConfigCache) return result } // convertToObject converts the json syntax tree into the json value func convertToObject(sourceFile *ast.SourceFile) (any, []*ast.Diagnostic) { var rootExpression *ast.Expression if len(sourceFile.Statements.Nodes) != 0 { rootExpression = sourceFile.Statements.Nodes[0].AsExpressionStatement().Expression } return convertToJson(sourceFile, rootExpression, true /*returnValue*/, nil /*jsonConversionNotifier*/) } func getDefaultCompilerOptions(configFileName string) *core.CompilerOptions { options := &core.CompilerOptions{} if configFileName != "" && tspath.GetBaseFileName(configFileName) == "jsconfig.json" { depth := 2 options = &core.CompilerOptions{ AllowJs: core.TSTrue, MaxNodeModuleJsDepth: &depth, AllowSyntheticDefaultImports: core.TSTrue, SkipLibCheck: core.TSTrue, NoEmit: core.TSTrue, } } return options } func getDefaultTypeAcquisition(configFileName string) *core.TypeAcquisition { options := &core.TypeAcquisition{} if configFileName != "" && tspath.GetBaseFileName(configFileName) == "jsconfig.json" { options.Enable = core.TSTrue } return options } func convertCompilerOptionsFromJsonWorker(jsonOptions any, basePath string, configFileName string) (*core.CompilerOptions, []*ast.Diagnostic) { options := getDefaultCompilerOptions(configFileName) _, errors := convertOptionsFromJson(CommandLineCompilerOptionsMap, jsonOptions, basePath, &compilerOptionsParser{options}) if configFileName != "" { options.ConfigFilePath = tspath.NormalizeSlashes(configFileName) } return options, errors } func convertTypeAcquisitionFromJsonWorker(jsonOptions any, basePath string, configFileName string) (*core.TypeAcquisition, []*ast.Diagnostic) { options := getDefaultTypeAcquisition(configFileName) _, errors := convertOptionsFromJson(typeAcquisitionDeclaration.ElementOptions, jsonOptions, basePath, &typeAcquisitionParser{options}) return options, errors } func parseOwnConfigOfJson( json *collections.OrderedMap[string, any], host ParseConfigHost, basePath string, configFileName string, ) (*parsedTsconfig, []*ast.Diagnostic) { var errors []*ast.Diagnostic if json.Has("excludes") { errors = append(errors, ast.NewCompilerDiagnostic(diagnostics.Unknown_option_excludes_Did_you_mean_exclude)) } options, err := convertCompilerOptionsFromJsonWorker(json.GetOrZero("compilerOptions"), basePath, configFileName) typeAcquisition, err2 := convertTypeAcquisitionFromJsonWorker(json.GetOrZero("typeAcquisition"), basePath, configFileName) errors = append(append(errors, err...), err2...) // watchOptions := convertWatchOptionsFromJsonWorker(json.watchOptions, basePath, errors) // json.compileOnSave = convertCompileOnSaveOptionFromJson(json, basePath, errors) var extendedConfigPath []string if extends := json.GetOrZero("extends"); extends != nil && extends != "" { extendedConfigPath, err = getExtendsConfigPathOrArray(extends, host, basePath, configFileName, nil, nil, nil) errors = append(errors, err...) } parsedConfig := &parsedTsconfig{ raw: json, options: options, typeAcquisition: typeAcquisition, extendedConfigPath: extendedConfigPath, } return parsedConfig, errors } func readJsonConfigFile(fileName string, path tspath.Path, readFile func(fileName string) (string, bool)) (*TsConfigSourceFile, []*ast.Diagnostic) { text, diagnostic := tryReadFile(fileName, readFile, []*ast.Diagnostic{}) if text != "" { return &TsConfigSourceFile{ SourceFile: parser.ParseSourceFile(ast.SourceFileParseOptions{ FileName: fileName, Path: path, }, text, core.ScriptKindJSON), }, diagnostic } else { file := &TsConfigSourceFile{ SourceFile: (&ast.NodeFactory{}).NewSourceFile(ast.SourceFileParseOptions{FileName: fileName, Path: path}, "", nil, (&ast.NodeFactory{}).NewToken(ast.KindEndOfFile)).AsSourceFile(), } file.SourceFile.SetDiagnostics(diagnostic) return file, diagnostic } } func getExtendedConfig( sourceFile *TsConfigSourceFile, extendedConfigFileName string, host ParseConfigHost, resolutionStack []string, extendedConfigCache ExtendedConfigCache, result *extendsResult, ) (*parsedTsconfig, []*ast.Diagnostic) { var errors []*ast.Diagnostic extendedConfigPath := tspath.ToPath(extendedConfigFileName, host.GetCurrentDirectory(), host.FS().UseCaseSensitiveFileNames()) parse := func() *ExtendedConfigCacheEntry { var extendedConfig *parsedTsconfig var entryErrors []*ast.Diagnostic extendedResult, err := readJsonConfigFile(extendedConfigFileName, extendedConfigPath, host.FS().ReadFile) entryErrors = append(entryErrors, err...) if len(extendedResult.SourceFile.Diagnostics()) == 0 { extendedConfig, err = parseConfig(nil, extendedResult, host, tspath.GetDirectoryPath(extendedConfigFileName), tspath.GetBaseFileName(extendedConfigFileName), resolutionStack, extendedConfigCache) entryErrors = append(entryErrors, err...) } return &ExtendedConfigCacheEntry{ extendedResult: extendedResult, extendedConfig: extendedConfig, errors: entryErrors, } } var cacheEntry *ExtendedConfigCacheEntry if extendedConfigCache != nil { cacheEntry = extendedConfigCache.GetExtendedConfig(extendedConfigFileName, extendedConfigPath, parse) } else { cacheEntry = parse() } if len(cacheEntry.errors) > 0 { errors = append(errors, cacheEntry.errors...) } if cacheEntry.extendedResult != nil { if sourceFile != nil { result.extendedSourceFiles.Add(cacheEntry.extendedResult.SourceFile.FileName()) for _, extendedSourceFile := range cacheEntry.extendedResult.ExtendedSourceFiles { result.extendedSourceFiles.Add(extendedSourceFile) } } } return cacheEntry.extendedConfig, errors } // parseConfig just extracts options/include/exclude/files out of a config file. // It does not resolve the included files. func parseConfig( json *collections.OrderedMap[string, any], sourceFile *TsConfigSourceFile, host ParseConfigHost, basePath string, configFileName string, resolutionStack []string, extendedConfigCache ExtendedConfigCache, ) (*parsedTsconfig, []*ast.Diagnostic) { basePath = tspath.NormalizeSlashes(basePath) resolvedPath := tspath.GetNormalizedAbsolutePath(configFileName, basePath) var errors []*ast.Diagnostic if slices.Contains(resolutionStack, resolvedPath) { var result *parsedTsconfig errors = append(errors, ast.NewCompilerDiagnostic(diagnostics.Circularity_detected_while_resolving_configuration_Colon_0)) if json.Size() == 0 { result = &parsedTsconfig{raw: json} } else { rawResult, err := convertToObject(sourceFile.SourceFile) errors = append(errors, err...) result = &parsedTsconfig{raw: rawResult} } return result, errors } var ownConfig *parsedTsconfig var err []*ast.Diagnostic if json != nil { ownConfig, err = parseOwnConfigOfJson(json, host, basePath, configFileName) } else { ownConfig, err = parseOwnConfigOfJsonSourceFile(tsconfigToSourceFile(sourceFile), host, basePath, configFileName) } errors = append(errors, err...) if ownConfig.options != nil && ownConfig.options.Paths != nil { // If we end up needing to resolve relative paths from 'paths' relative to // the config file location, we'll need to know where that config file was. // Since 'paths' can be inherited from an extended config in another directory, // we wouldn't know which directory to use unless we store it here. ownConfig.options.PathsBasePath = basePath } applyExtendedConfig := func(result *extendsResult, extendedConfigPath string) { extendedConfig, extendedErrors := getExtendedConfig(sourceFile, extendedConfigPath, host, resolutionStack, extendedConfigCache, result) errors = append(errors, extendedErrors...) if extendedConfig != nil && extendedConfig.options != nil { extendsRaw := extendedConfig.raw relativeDifference := "" setPropertyValue := func(propertyName string) { if rawMap, ok := ownConfig.raw.(*collections.OrderedMap[string, any]); ok && rawMap.Has(propertyName) { return } if propertyName == "include" || propertyName == "exclude" || propertyName == "files" { if rawMap, ok := extendsRaw.(*collections.OrderedMap[string, any]); ok && rawMap.Has(propertyName) { if slice, _ := rawMap.GetOrZero(propertyName).([]any); slice != nil { value := core.Map(slice, func(path any) any { if startsWithConfigDirTemplate(path) || tspath.IsRootedDiskPath(path.(string)) { return path.(string) } else { if relativeDifference == "" { t := tspath.ComparePathsOptions{ UseCaseSensitiveFileNames: host.FS().UseCaseSensitiveFileNames(), CurrentDirectory: basePath, } relativeDifference = tspath.ConvertToRelativePath(tspath.GetDirectoryPath(extendedConfigPath), t) } return tspath.CombinePaths(relativeDifference, path.(string)) } }) if propertyName == "include" { result.include = value } else if propertyName == "exclude" { result.exclude = value } else if propertyName == "files" { result.files = value } } } } } setPropertyValue("include") setPropertyValue("exclude") setPropertyValue("files") if extendedRawMap, ok := extendsRaw.(*collections.OrderedMap[string, any]); ok && extendedRawMap.Has("compileOnSave") { if compileOnSave, ok := extendedRawMap.GetOrZero("compileOnSave").(bool); ok { result.compileOnSave = compileOnSave } } mergeCompilerOptions(result.options, extendedConfig.options, extendsRaw) } } if ownConfig.extendedConfigPath != nil { // copy the resolution stack so it is never reused between branches in potential diamond-problem scenarios. resolutionStack = append(resolutionStack, resolvedPath) var result *extendsResult = &extendsResult{ options: &core.CompilerOptions{}, } if reflect.TypeOf(ownConfig.extendedConfigPath).Kind() == reflect.String { applyExtendedConfig(result, ownConfig.extendedConfigPath.(string)) } else if configPath, ok := ownConfig.extendedConfigPath.([]string); ok { for _, extendedConfigPath := range configPath { applyExtendedConfig(result, extendedConfigPath) } } if result.include != nil { ownConfig.raw.(*collections.OrderedMap[string, any]).Set("include", result.include) } if result.exclude != nil { ownConfig.raw.(*collections.OrderedMap[string, any]).Set("exclude", result.exclude) } if result.files != nil { ownConfig.raw.(*collections.OrderedMap[string, any]).Set("files", result.files) } if result.compileOnSave && !ownConfig.raw.(*collections.OrderedMap[string, any]).Has("compileOnSave") { ownConfig.raw.(*collections.OrderedMap[string, any]).Set("compileOnSave", result.compileOnSave) } if sourceFile != nil { for extendedSourceFile := range result.extendedSourceFiles.Keys() { sourceFile.ExtendedSourceFiles = append(sourceFile.ExtendedSourceFiles, extendedSourceFile) } } ownConfig.options = mergeCompilerOptions(result.options, ownConfig.options, ownConfig.raw) // ownConfig.watchOptions = ownConfig.watchOptions && result.watchOptions ? // assignWatchOptions(result, ownConfig.watchOptions) : // ownConfig.watchOptions || result.watchOptions; } return ownConfig, errors } const defaultIncludeSpec = "**/*" type propOfRaw struct { sliceValue []any wrongValue string } // parseJsonConfigFileContentWorker parses the contents of a config file from json or json source file (tsconfig.json). // json: The contents of the config file to parse // sourceFile: sourceFile corresponding to the Json // host: Instance of ParseConfigHost used to enumerate files in folder. // basePath: A root directory to resolve relative path entries in the config file to. e.g. outDir // resolutionStack: Only present for backwards-compatibility. Should be empty. func parseJsonConfigFileContentWorker( json *collections.OrderedMap[string, any], sourceFile *TsConfigSourceFile, host ParseConfigHost, basePath string, existingOptions *core.CompilerOptions, configFileName string, resolutionStack []tspath.Path, extraFileExtensions []FileExtensionInfo, extendedConfigCache ExtendedConfigCache, ) *ParsedCommandLine { debug.Assert((json == nil && sourceFile != nil) || (json != nil && sourceFile == nil)) basePathForFileNames := "" if configFileName != "" { basePathForFileNames = tspath.NormalizePath(directoryOfCombinedPath(configFileName, basePath)) } else { basePathForFileNames = tspath.NormalizePath(basePath) } var errors []*ast.Diagnostic resolutionStackString := []string{} parsedConfig, errors := parseConfig(json, sourceFile, host, basePath, configFileName, resolutionStackString, extendedConfigCache) mergeCompilerOptions(parsedConfig.options, existingOptions, nil) handleOptionConfigDirTemplateSubstitution(parsedConfig.options, basePathForFileNames) rawConfig := parseJsonToStringKey(parsedConfig.raw) if configFileName != "" && parsedConfig.options != nil { parsedConfig.options.ConfigFilePath = tspath.NormalizeSlashes(configFileName) } getPropFromRaw := func(prop string, validateElement func(value any) bool, elementTypeName string) propOfRaw { value, exists := rawConfig.Get(prop) if exists && value != nil { if reflect.TypeOf(value).Kind() == reflect.Slice { result := rawConfig.GetOrZero(prop) if _, ok := result.([]any); ok { if sourceFile == nil && !core.Every(result.([]any), validateElement) { errors = append(errors, ast.NewCompilerDiagnostic(diagnostics.Compiler_option_0_requires_a_value_of_type_1, prop, elementTypeName)) } } return propOfRaw{sliceValue: result.([]any)} } else if sourceFile == nil { errors = append(errors, ast.NewCompilerDiagnostic(diagnostics.Compiler_option_0_requires_a_value_of_type_1, prop, "Array")) return propOfRaw{sliceValue: nil, wrongValue: "not-array"} } } return propOfRaw{sliceValue: nil, wrongValue: "no-prop"} } referencesOfRaw := getPropFromRaw("references", func(element any) bool { return reflect.TypeOf(element) == orderedMapType }, "object") fileSpecs := getPropFromRaw("files", func(element any) bool { return reflect.TypeOf(element).Kind() == reflect.String }, "string") if fileSpecs.sliceValue != nil || fileSpecs.wrongValue == "" { hasZeroOrNoReferences := false if referencesOfRaw.wrongValue == "no-prop" || referencesOfRaw.wrongValue == "not-array" || len(referencesOfRaw.sliceValue) == 0 { hasZeroOrNoReferences = true } hasExtends := rawConfig.GetOrZero("extends") if fileSpecs.sliceValue != nil && len(fileSpecs.sliceValue) == 0 && hasZeroOrNoReferences && hasExtends == nil { if sourceFile != nil { var fileName string if configFileName != "" { fileName = configFileName } else { fileName = "tsconfig.json" } diagnosticMessage := diagnostics.The_files_list_in_config_file_0_is_empty nodeValue := ForEachTsConfigPropArray(sourceFile.SourceFile, "files", func(property *ast.PropertyAssignment) *ast.Node { return property.Initializer }) errors = append(errors, CreateDiagnosticForNodeInSourceFile(sourceFile.SourceFile, nodeValue, diagnosticMessage, fileName)) } else { errors = append(errors, ast.NewCompilerDiagnostic(diagnostics.The_files_list_in_config_file_0_is_empty, configFileName)) } } } includeSpecs := getPropFromRaw("include", func(element any) bool { return reflect.TypeOf(element).Kind() == reflect.String }, "string") excludeSpecs := getPropFromRaw("exclude", func(element any) bool { return reflect.TypeOf(element).Kind() == reflect.String }, "string") isDefaultIncludeSpec := false if excludeSpecs.wrongValue == "no-prop" && parsedConfig.options != nil { outDir := parsedConfig.options.OutDir declarationDir := parsedConfig.options.DeclarationDir if outDir != "" || declarationDir != "" { var values []any if outDir != "" { values = append(values, outDir) } if declarationDir != "" { values = append(values, declarationDir) } excludeSpecs = propOfRaw{sliceValue: values} } } if fileSpecs.sliceValue == nil && includeSpecs.sliceValue == nil { includeSpecs = propOfRaw{sliceValue: []any{defaultIncludeSpec}} isDefaultIncludeSpec = true } var validatedIncludeSpecs []string var validatedIncludeSpecsBeforeSubstitution []string var validatedExcludeSpecs []string var validatedFilesSpec []string var validatedFilesSpecBeforeSubstitution []string // The exclude spec list is converted into a regular expression, which allows us to quickly // test whether a file or directory should be excluded before recursively traversing the // file system. if includeSpecs.sliceValue != nil { var err []*ast.Diagnostic validatedIncludeSpecsBeforeSubstitution, err = validateSpecs(includeSpecs.sliceValue, true /*disallowTrailingRecursion*/, tsconfigToSourceFile(sourceFile), "include") errors = append(errors, err...) if validatedIncludeSpecs = getSubstitutedStringArrayWithConfigDirTemplate(validatedIncludeSpecsBeforeSubstitution, basePathForFileNames); validatedIncludeSpecs == nil { validatedIncludeSpecs = validatedIncludeSpecsBeforeSubstitution } } if excludeSpecs.sliceValue != nil { var err []*ast.Diagnostic validatedExcludeSpecs, err = validateSpecs(excludeSpecs.sliceValue, false /*disallowTrailingRecursion*/, tsconfigToSourceFile(sourceFile), "exclude") errors = append(errors, err...) if validatedExcludeSpecsWithSubstitution := getSubstitutedStringArrayWithConfigDirTemplate(validatedExcludeSpecs, basePathForFileNames); validatedExcludeSpecsWithSubstitution != nil { validatedExcludeSpecs = validatedExcludeSpecsWithSubstitution } } if fileSpecs.sliceValue != nil { fileSpecs := core.Filter(fileSpecs.sliceValue, func(spec any) bool { return reflect.TypeOf(spec).Kind() == reflect.String }) for _, spec := range fileSpecs { if spec, ok := spec.(string); ok { validatedFilesSpecBeforeSubstitution = append(validatedFilesSpecBeforeSubstitution, spec) } } if validatedFilesSpec = getSubstitutedStringArrayWithConfigDirTemplate(validatedFilesSpecBeforeSubstitution, basePathForFileNames); validatedFilesSpec == nil { validatedFilesSpec = validatedFilesSpecBeforeSubstitution } } configFileSpecs := configFileSpecs{ fileSpecs.sliceValue, includeSpecs.sliceValue, excludeSpecs.sliceValue, validatedFilesSpec, validatedIncludeSpecs, validatedExcludeSpecs, validatedFilesSpecBeforeSubstitution, validatedIncludeSpecsBeforeSubstitution, isDefaultIncludeSpec, } if sourceFile != nil { sourceFile.configFileSpecs = &configFileSpecs } getFileNames := func(basePath string) ([]string, int) { parsedConfigOptions := parsedConfig.options fileNames, literalFileNamesLen := getFileNamesFromConfigSpecs(configFileSpecs, basePath, parsedConfigOptions, host.FS(), extraFileExtensions) if shouldReportNoInputFiles(fileNames, canJsonReportNoInputFiles(rawConfig), resolutionStack) { includeSpecs := configFileSpecs.includeSpecs excludeSpecs := configFileSpecs.excludeSpecs if includeSpecs == nil { includeSpecs = []string{} } if excludeSpecs == nil { excludeSpecs = []string{} } errors = append(errors, ast.NewCompilerDiagnostic(diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2, configFileName, core.Must(core.StringifyJson(includeSpecs, "", "")), core.Must(core.StringifyJson(excludeSpecs, "", "")))) } return fileNames, literalFileNamesLen } getProjectReferences := func(basePath string) []*core.ProjectReference { var projectReferences []*core.ProjectReference newReferencesOfRaw := getPropFromRaw("references", func(element any) bool { return reflect.TypeOf(element) == orderedMapType }, "object") if newReferencesOfRaw.sliceValue != nil { projectReferences = []*core.ProjectReference{} for _, reference := range newReferencesOfRaw.sliceValue { for _, ref := range parseProjectReference(reference) { if reflect.TypeOf(ref.Path).Kind() != reflect.String { if sourceFile == nil { errors = append(errors, ast.NewCompilerDiagnostic(diagnostics.Compiler_option_0_requires_a_value_of_type_1, "reference.path", "string")) } } else { projectReferences = append(projectReferences, &core.ProjectReference{ Path: tspath.GetNormalizedAbsolutePath(ref.Path, basePath), OriginalPath: ref.Path, Circular: ref.Circular, }) } } } } return projectReferences } fileNames, literalFileNamesLen := getFileNames(basePathForFileNames) return &ParsedCommandLine{ ParsedConfig: &core.ParsedOptions{ CompilerOptions: parsedConfig.options, TypeAcquisition: parsedConfig.typeAcquisition, // WatchOptions: nil, FileNames: fileNames, ProjectReferences: getProjectReferences(basePathForFileNames), }, ConfigFile: sourceFile, Raw: parsedConfig.raw, Errors: errors, extraFileExtensions: extraFileExtensions, comparePathsOptions: tspath.ComparePathsOptions{ UseCaseSensitiveFileNames: host.FS().UseCaseSensitiveFileNames(), CurrentDirectory: basePathForFileNames, }, literalFileNamesLen: literalFileNamesLen, } } func canJsonReportNoInputFiles(rawConfig *collections.OrderedMap[string, any]) bool { filesExists := rawConfig.Has("files") referencesExists := rawConfig.Has("references") return !filesExists && !referencesExists } func shouldReportNoInputFiles(fileNames []string, canJsonReportNoInputFiles bool, resolutionStack []tspath.Path) bool { return len(fileNames) == 0 && canJsonReportNoInputFiles && len(resolutionStack) == 0 } func validateSpecs(specs any, disallowTrailingRecursion bool, jsonSourceFile *ast.SourceFile, specKey string) ([]string, []*ast.Diagnostic) { createDiagnostic := func(message *diagnostics.Message, spec string) *ast.Diagnostic { element := GetTsConfigPropArrayElementValue(jsonSourceFile, specKey, spec) return CreateDiagnosticForNodeInSourceFileOrCompilerDiagnostic(jsonSourceFile, element.AsNode(), message, spec) } var errors []*ast.Diagnostic var finalSpecs []string for _, spec := range specs.([]any) { if reflect.TypeOf(spec).Kind() != reflect.String { continue } diag := specToDiagnostic(spec.(string), disallowTrailingRecursion) if diag != nil { errors = append(errors, createDiagnostic(diag, spec.(string))) } else { finalSpecs = append(finalSpecs, spec.(string)) } } return finalSpecs, errors } func specToDiagnostic(spec string, disallowTrailingRecursion bool) *diagnostics.Message { if disallowTrailingRecursion { if ok, _ := regexp.MatchString(invalidTrailingRecursionPattern, spec); ok { return diagnostics.File_specification_cannot_end_in_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0 } } else if invalidDotDotAfterRecursiveWildcard(spec) { return diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0 } return nil } func invalidDotDotAfterRecursiveWildcard(s string) bool { // We used to use the regex /(^|\/)\*\*\/(.*\/)?\.\.($|\/)/ to check for this case, but // in v8, that has polynomial performance because the recursive wildcard match - **/ - // can be matched in many arbitrary positions when multiple are present, resulting // in bad backtracking (and we don't care which is matched - just that some /.. segment // comes after some **/ segment). var wildcardIndex int if strings.HasPrefix(s, "**/") { wildcardIndex = 0 } else { wildcardIndex = strings.Index(s, "/**/") } if wildcardIndex == -1 { return false } var lastDotIndex int if strings.HasSuffix(s, "/..") { lastDotIndex = len(s) } else { lastDotIndex = strings.LastIndex(s, "/../") } return lastDotIndex > wildcardIndex } // Tests for a path that ends in a recursive directory wildcard. // // Matches **, \**, **\, and \**\, but not a**b. // NOTE: used \ in place of / above to avoid issues with multiline comments. // // Breakdown: // // (^|\/) # matches either the beginning of the string or a directory separator. // \*\* # matches the recursive directory wildcard "**". // \/?$ # matches an optional trailing directory separator at the end of the string. const invalidTrailingRecursionPattern = `(?:^|\/)\*\*\/?$` func GetTsConfigPropArrayElementValue(tsConfigSourceFile *ast.SourceFile, propKey string, elementValue string) *ast.StringLiteral { callback := GetCallbackForFindingPropertyAssignmentByValue(elementValue) return ForEachTsConfigPropArray(tsConfigSourceFile, propKey, func(property *ast.PropertyAssignment) *ast.StringLiteral { if value := callback(property); value != nil { return value.AsStringLiteral() } return nil }) } func ForEachTsConfigPropArray[T any](tsConfigSourceFile *ast.SourceFile, propKey string, callback func(property *ast.PropertyAssignment) *T) *T { if tsConfigSourceFile != nil { return ForEachPropertyAssignment(getTsConfigObjectLiteralExpression(tsConfigSourceFile), propKey, callback) } return nil } func CreateDiagnosticAtReferenceSyntax(config *ParsedCommandLine, index int, message *diagnostics.Message, args ...any) *ast.Diagnostic { return ForEachTsConfigPropArray(config.ConfigFile.SourceFile, "references", func(property *ast.PropertyAssignment) *ast.Diagnostic { if ast.IsArrayLiteralExpression(property.Initializer) { value := property.Initializer.AsArrayLiteralExpression().Elements.Nodes if len(value) > index { return CreateDiagnosticForNodeInSourceFile(config.ConfigFile.SourceFile, value[index], message, args...) } } return nil }) } func GetCallbackForFindingPropertyAssignmentByValue(value string) func(property *ast.PropertyAssignment) *ast.Node { return func(property *ast.PropertyAssignment) *ast.Node { if ast.IsArrayLiteralExpression(property.Initializer) { return core.Find(property.Initializer.AsArrayLiteralExpression().Elements.Nodes, func(element *ast.Node) bool { return ast.IsStringLiteral(element) && element.AsStringLiteral().Text == value }) } return nil } } func GetOptionsSyntaxByArrayElementValue(objectLiteral *ast.ObjectLiteralExpression, propKey string, elementValue string) *ast.Node { return ForEachPropertyAssignment(objectLiteral, propKey, GetCallbackForFindingPropertyAssignmentByValue(elementValue)) } func ForEachPropertyAssignment[T any](objectLiteral *ast.ObjectLiteralExpression, key string, callback func(property *ast.PropertyAssignment) *T, key2 ...string) *T { if objectLiteral != nil { for _, property := range objectLiteral.Properties.Nodes { if !ast.IsPropertyAssignment(property) { continue } if propName, ok := ast.TryGetTextOfPropertyName(property.Name()); ok { if propName == key || (len(key2) > 0 && key2[0] == propName) { return callback(property.AsPropertyAssignment()) } } } } return nil } func getTsConfigObjectLiteralExpression(tsConfigSourceFile *ast.SourceFile) *ast.ObjectLiteralExpression { if tsConfigSourceFile != nil && tsConfigSourceFile.Statements != nil && len(tsConfigSourceFile.Statements.Nodes) > 0 { expression := tsConfigSourceFile.Statements.Nodes[0].AsExpressionStatement().Expression return expression.AsObjectLiteralExpression() } return nil } func getSubstitutedPathWithConfigDirTemplate(value string, basePath string) string { return tspath.GetNormalizedAbsolutePath(strings.Replace(value, configDirTemplate, "./", 1), basePath) } func getSubstitutedStringArrayWithConfigDirTemplate(list []string, basePath string) []string { var result []string for i, element := range list { if startsWithConfigDirTemplate(element) { if result == nil { result = slices.Clone(list) } result[i] = getSubstitutedPathWithConfigDirTemplate(element, basePath) } } if result != nil { return result } return nil } func handleOptionConfigDirTemplateSubstitution(compilerOptions *core.CompilerOptions, basePath string) { if compilerOptions == nil { return } // !!! don't hardcode this; use options declarations? for k, v := range compilerOptions.Paths.Entries() { if substitution := getSubstitutedStringArrayWithConfigDirTemplate(v, basePath); substitution != nil { compilerOptions.Paths.Set(k, substitution) } } if rootDirs := getSubstitutedStringArrayWithConfigDirTemplate(compilerOptions.RootDirs, basePath); rootDirs != nil { compilerOptions.RootDirs = rootDirs } if typeRoots := getSubstitutedStringArrayWithConfigDirTemplate(compilerOptions.TypeRoots, basePath); typeRoots != nil { compilerOptions.TypeRoots = typeRoots } if startsWithConfigDirTemplate(compilerOptions.GenerateCpuProfile) { compilerOptions.GenerateCpuProfile = getSubstitutedPathWithConfigDirTemplate(compilerOptions.GenerateCpuProfile, basePath) } if startsWithConfigDirTemplate(compilerOptions.GenerateTrace) { compilerOptions.GenerateTrace = getSubstitutedPathWithConfigDirTemplate(compilerOptions.GenerateTrace, basePath) } if startsWithConfigDirTemplate(compilerOptions.OutFile) { compilerOptions.OutFile = getSubstitutedPathWithConfigDirTemplate(compilerOptions.OutFile, basePath) } if startsWithConfigDirTemplate(compilerOptions.OutDir) { compilerOptions.OutDir = getSubstitutedPathWithConfigDirTemplate(compilerOptions.OutDir, basePath) } if startsWithConfigDirTemplate(compilerOptions.RootDir) { compilerOptions.RootDir = getSubstitutedPathWithConfigDirTemplate(compilerOptions.RootDir, basePath) } if startsWithConfigDirTemplate(compilerOptions.TsBuildInfoFile) { compilerOptions.TsBuildInfoFile = getSubstitutedPathWithConfigDirTemplate(compilerOptions.TsBuildInfoFile, basePath) } if startsWithConfigDirTemplate(compilerOptions.BaseUrl) { compilerOptions.BaseUrl = getSubstitutedPathWithConfigDirTemplate(compilerOptions.BaseUrl, basePath) } if startsWithConfigDirTemplate(compilerOptions.DeclarationDir) { compilerOptions.DeclarationDir = getSubstitutedPathWithConfigDirTemplate(compilerOptions.DeclarationDir, basePath) } } // hasFileWithHigherPriorityExtension determines whether a literal or wildcard file has already been included that has a higher extension priority. // file is the path to the file. func hasFileWithHigherPriorityExtension(file string, extensions [][]string, hasFile func(fileName string) bool) bool { var extensionGroup []string for _, group := range extensions { if tspath.FileExtensionIsOneOf(file, group) { extensionGroup = append(extensionGroup, group...) } } if len(extensionGroup) == 0 { return false } for _, ext := range extensionGroup { // d.ts files match with .ts extension and with case sensitive sorting the file order for same files with ts tsx and dts extension is // d.ts, .ts, .tsx in that order so we need to handle tsx and dts of same same name case here and in remove files with same extensions // So dont match .d.ts files with .ts extension if tspath.FileExtensionIs(file, ext) && (ext != tspath.ExtensionTs || !tspath.FileExtensionIs(file, tspath.ExtensionDts)) { return false } if hasFile(tspath.ChangeExtension(file, ext)) { if ext == tspath.ExtensionDts && (tspath.FileExtensionIs(file, tspath.ExtensionJs) || tspath.FileExtensionIs(file, tspath.ExtensionJsx)) { // LEGACY BEHAVIOR: An off-by-one bug somewhere in the extension priority system for wildcard module loading allowed declaration // files to be loaded alongside their js(x) counterparts. We regard this as generally undesirable, but retain the behavior to // prevent breakage. continue } return true } } return false } // Removes files included via wildcard expansion with a lower extension priority that have already been included. // file is the path to the file. func removeWildcardFilesWithLowerPriorityExtension(file string, wildcardFiles *collections.OrderedMap[string, string], extensions [][]string, keyMapper func(value string) string) { var extensionGroup []string for _, group := range extensions { if tspath.FileExtensionIsOneOf(file, group) { extensionGroup = append(extensionGroup, group...) } } if extensionGroup == nil { return } for i := len(extensionGroup) - 1; i >= 0; i-- { ext := extensionGroup[i] if tspath.FileExtensionIs(file, ext) { return } lowerPriorityPath := keyMapper(tspath.ChangeExtension(file, ext)) wildcardFiles.Delete(lowerPriorityPath) } } // getFileNamesFromConfigSpecs gets the file names from the provided config file specs that contain, files, include, exclude and // other properties needed to resolve the file names // configFileSpecs is the config file specs extracted with file names to include, wildcards to include/exclude and other details // basePath is the base path for any relative file specifications. // options is the Compiler options. // host is the host used to resolve files and directories. // extraFileExtensions optionally file extra file extension information from host func getFileNamesFromConfigSpecs( configFileSpecs configFileSpecs, basePath string, // considering this is the current directory options *core.CompilerOptions, host vfs.FS, extraFileExtensions []FileExtensionInfo, ) ([]string, int) { extraFileExtensions = []FileExtensionInfo{} basePath = tspath.NormalizePath(basePath) keyMappper := func(value string) string { return tspath.GetCanonicalFileName(value, host.UseCaseSensitiveFileNames()) } // Literal file names (provided via the "files" array in tsconfig.json) are stored in a // file map with a possibly case insensitive key. We use this map later when when including // wildcard paths. var literalFileMap collections.OrderedMap[string, string] // Wildcard paths (provided via the "includes" array in tsconfig.json) are stored in a // file map with a possibly case insensitive key. We use this map to store paths matched // via wildcard, and to handle extension priority. var wildcardFileMap collections.OrderedMap[string, string] // Wildcard paths of json files (provided via the "includes" array in tsconfig.json) are stored in a // file map with a possibly case insensitive key. We use this map to store paths matched // via wildcard of *.json kind var wildCardJsonFileMap collections.OrderedMap[string, string] validatedFilesSpec := configFileSpecs.validatedFilesSpec validatedIncludeSpecs := configFileSpecs.validatedIncludeSpecs validatedExcludeSpecs := configFileSpecs.validatedExcludeSpecs // Rather than re-query this for each file and filespec, we query the supported extensions // once and store it on the expansion context. supportedExtensions := GetSupportedExtensions(options, extraFileExtensions) supportedExtensionsWithJsonIfResolveJsonModule := GetSupportedExtensionsWithJsonIfResolveJsonModule(options, supportedExtensions) // Literal files are always included verbatim. An "include" or "exclude" specification cannot // remove a literal file. for _, fileName := range validatedFilesSpec { file := tspath.GetNormalizedAbsolutePath(fileName, basePath) literalFileMap.Set(keyMappper(fileName), file) } var jsonOnlyIncludeRegexes []*regexp2.Regexp if len(validatedIncludeSpecs) > 0 { files := vfs.ReadDirectory(host, basePath, basePath, core.Flatten(supportedExtensionsWithJsonIfResolveJsonModule), validatedExcludeSpecs, validatedIncludeSpecs, nil) for _, file := range files { if tspath.FileExtensionIs(file, tspath.ExtensionJson) { if jsonOnlyIncludeRegexes == nil { includes := core.Filter(validatedIncludeSpecs, func(include string) bool { return strings.HasSuffix(include, tspath.ExtensionJson) }) includeFilePatterns := core.Map(vfs.GetRegularExpressionsForWildcards(includes, basePath, "files"), func(pattern string) string { return fmt.Sprintf("^%s$", pattern) }) if includeFilePatterns != nil { jsonOnlyIncludeRegexes = core.Map(includeFilePatterns, func(pattern string) *regexp2.Regexp { return vfs.GetRegexFromPattern(pattern, host.UseCaseSensitiveFileNames()) }) } else { jsonOnlyIncludeRegexes = nil } } includeIndex := core.FindIndex(jsonOnlyIncludeRegexes, func(re *regexp2.Regexp) bool { return core.Must(re.MatchString(file)) }) if includeIndex != -1 { key := keyMappper(file) if !literalFileMap.Has(key) && !wildCardJsonFileMap.Has(key) { wildCardJsonFileMap.Set(key, file) } } continue } // If we have already included a literal or wildcard path with a // higher priority extension, we should skip this file. // // This handles cases where we may encounter both .ts and // .d.ts (or .js if "allowJs" is enabled) in the same // directory when they are compilation outputs. if hasFileWithHigherPriorityExtension(file, supportedExtensions, func(fileName string) bool { canonicalFileName := keyMappper(fileName) return literalFileMap.Has(canonicalFileName) || wildcardFileMap.Has(canonicalFileName) }) { continue } // We may have included a wildcard path with a lower priority // extension due to the user-defined order of entries in the // "include" array. If there is a lower priority extension in the // same directory, we should remove it. removeWildcardFilesWithLowerPriorityExtension(file, &wildcardFileMap, supportedExtensions, keyMappper) key := keyMappper(file) if !literalFileMap.Has(key) && !wildcardFileMap.Has(key) { wildcardFileMap.Set(key, file) } } } files := make([]string, 0, literalFileMap.Size()+wildcardFileMap.Size()+wildCardJsonFileMap.Size()) for file := range literalFileMap.Values() { files = append(files, file) } for file := range wildcardFileMap.Values() { files = append(files, file) } for file := range wildCardJsonFileMap.Values() { files = append(files, file) } return files, literalFileMap.Size() } func GetSupportedExtensions(compilerOptions *core.CompilerOptions, extraFileExtensions []FileExtensionInfo) [][]string { needJSExtensions := compilerOptions.GetAllowJS() if len(extraFileExtensions) == 0 { if needJSExtensions { return tspath.AllSupportedExtensions } else { return tspath.SupportedTSExtensions } } var builtins [][]string if needJSExtensions { builtins = tspath.AllSupportedExtensions } else { builtins = tspath.SupportedTSExtensions } flatBuiltins := core.Flatten(builtins) var result [][]string for _, x := range extraFileExtensions { if x.ScriptKind == core.ScriptKindDeferred || (needJSExtensions && (x.ScriptKind == core.ScriptKindJS || x.ScriptKind == core.ScriptKindJSX)) && !slices.Contains(flatBuiltins, x.Extension) { result = append(result, []string{x.Extension}) } } extensions := slices.Concat(builtins, result) return extensions } func GetSupportedExtensionsWithJsonIfResolveJsonModule(compilerOptions *core.CompilerOptions, supportedExtensions [][]string) [][]string { if compilerOptions == nil || !compilerOptions.GetResolveJsonModule() { return supportedExtensions } if core.Same(supportedExtensions, tspath.AllSupportedExtensions) { return tspath.AllSupportedExtensionsWithJson } if core.Same(supportedExtensions, tspath.SupportedTSExtensions) { return tspath.SupportedTSExtensionsWithJson } return slices.Concat(supportedExtensions, [][]string{{tspath.ExtensionJson}}) } // Reads the config file and reports errors. func GetParsedCommandLineOfConfigFile( configFileName string, options *core.CompilerOptions, sys ParseConfigHost, extendedConfigCache ExtendedConfigCache, ) (*ParsedCommandLine, []*ast.Diagnostic) { configFileName = tspath.GetNormalizedAbsolutePath(configFileName, sys.GetCurrentDirectory()) return GetParsedCommandLineOfConfigFilePath(configFileName, tspath.ToPath(configFileName, sys.GetCurrentDirectory(), sys.FS().UseCaseSensitiveFileNames()), options, sys, extendedConfigCache) } func GetParsedCommandLineOfConfigFilePath( configFileName string, path tspath.Path, options *core.CompilerOptions, sys ParseConfigHost, extendedConfigCache ExtendedConfigCache, ) (*ParsedCommandLine, []*ast.Diagnostic) { errors := []*ast.Diagnostic{} configFileText, errors := tryReadFile(configFileName, sys.FS().ReadFile, errors) if len(errors) > 0 { // these are unrecoverable errors--exit to report them as diagnostics return nil, errors } tsConfigSourceFile := NewTsconfigSourceFileFromFilePath(configFileName, path, configFileText) // tsConfigSourceFile.resolvedPath = tsConfigSourceFile.FileName() // tsConfigSourceFile.originalFileName = tsConfigSourceFile.FileName() return ParseJsonSourceFileConfigFileContent( tsConfigSourceFile, sys, tspath.GetDirectoryPath(configFileName), options, configFileName, nil, nil, extendedConfigCache, ), nil }