1777 lines
70 KiB
Go
1777 lines
70 KiB
Go
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 <file>.ts and
|
|
// <file>.d.ts (or <file>.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
|
|
}
|