From 55010edaa2aa1a82f519c7f3e743340c6a5692af Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 19 Feb 2025 13:51:16 -0800 Subject: [PATCH 1/3] Use ordered maps for options and users --- internal/collections/ordered_map.go | 28 ++++ internal/compiler/module/resolver.go | 14 +- internal/compiler/packagejson/cache.go | 12 +- internal/core/compileroptions.go | 209 ++++++++++++------------ internal/tsoptions/commandlineparser.go | 27 +-- internal/tsoptions/export_test.go | 5 +- internal/tsoptions/parsinghelpers.go | 75 ++++----- internal/tsoptions/tsconfigparsing.go | 74 ++++----- 8 files changed, 239 insertions(+), 205 deletions(-) diff --git a/internal/collections/ordered_map.go b/internal/collections/ordered_map.go index 9f7acde9f0..8bbc3fbd07 100644 --- a/internal/collections/ordered_map.go +++ b/internal/collections/ordered_map.go @@ -105,6 +105,10 @@ func (m *OrderedMap[K, V]) Delete(key K) (V, bool) { // A slice of the keys can be obtained by calling `slices.Collect`. func (m *OrderedMap[K, V]) Keys() iter.Seq[K] { return func(yield func(K) bool) { + if m == nil { + return + } + // We use a for loop here to ensure we enumerate new items added during iteration. //nolint:intrange for i := 0; i < len(m.keys); i++ { @@ -119,6 +123,10 @@ func (m *OrderedMap[K, V]) Keys() iter.Seq[K] { // A slice of the values can be obtained by calling `slices.Collect`. func (m *OrderedMap[K, V]) Values() iter.Seq[V] { return func(yield func(V) bool) { + if m == nil { + return + } + // We use a for loop here to ensure we enumerate new items added during iteration. //nolint:intrange for i := 0; i < len(m.keys); i++ { @@ -132,6 +140,10 @@ func (m *OrderedMap[K, V]) Values() iter.Seq[V] { // Entries returns an iterator over the key-value pairs in the map. func (m *OrderedMap[K, V]) Entries() iter.Seq2[K, V] { return func(yield func(K, V) bool) { + if m == nil { + return + } + // We use a for loop here to ensure we enumerate new items added during iteration. //nolint:intrange for i := 0; i < len(m.keys); i++ { @@ -153,11 +165,19 @@ func (m *OrderedMap[K, V]) Clear() { // Size returns the number of key-value pairs in the map. func (m *OrderedMap[K, V]) Size() int { + if m == nil { + return 0 + } + return len(m.keys) } // Clone returns a shallow copy of the map. func (m *OrderedMap[K, V]) Clone() *OrderedMap[K, V] { + if m == nil { + return nil + } + m2 := m.clone() return &m2 } @@ -169,6 +189,14 @@ func (m *OrderedMap[K, V]) clone() OrderedMap[K, V] { } } +func (m OrderedMap[K, V]) MarshalJSON() ([]byte, error) { + if len(m.mp) == 0 { + return []byte("{}"), nil + } + // TODO(jakebailey): implement proper sort order + return json.Marshal(m.mp) +} + func (m *OrderedMap[K, V]) UnmarshalJSON(data []byte) error { if string(data) == "null" { // By convention, to approximate the behavior of Unmarshal itself, diff --git a/internal/compiler/module/resolver.go b/internal/compiler/module/resolver.go index 4b378dcca8..d44b80938b 100644 --- a/internal/compiler/module/resolver.go +++ b/internal/compiler/module/resolver.go @@ -1023,7 +1023,7 @@ func (r *resolutionState) tryLoadModuleUsingOptionalResolutionSettings() *resolv } func (r *resolutionState) tryLoadModuleUsingPathsIfEligible() *resolved { - if len(r.compilerOptions.Paths) > 0 && !tspath.PathIsRelative(r.name) { + if r.compilerOptions.Paths.Size() > 0 && !tspath.PathIsRelative(r.name) { if r.resolver.traceEnabled() { r.resolver.host.Trace(diagnostics.X_paths_option_is_specified_looking_for_a_pattern_to_match_module_name_0.Format(r.name)) } @@ -1045,13 +1045,13 @@ func (r *resolutionState) tryLoadModuleUsingPathsIfEligible() *resolved { ) } -func (r *resolutionState) tryLoadModuleUsingPaths(extensions extensions, moduleName string, containingDirectory string, paths map[string][]string, pathPatterns parsedPatterns, loader resolutionKindSpecificLoader, onlyRecordFailures bool) *resolved { +func (r *resolutionState) tryLoadModuleUsingPaths(extensions extensions, moduleName string, containingDirectory string, paths *collections.OrderedMap[string, []string], pathPatterns parsedPatterns, loader resolutionKindSpecificLoader, onlyRecordFailures bool) *resolved { if matchedPattern := matchPatternOrExact(pathPatterns, moduleName); matchedPattern.IsValid() { matchedStar := matchedPattern.MatchedText(moduleName) if r.resolver.traceEnabled() { r.resolver.host.Trace(diagnostics.Module_name_0_matched_pattern_1.Format(moduleName, matchedPattern.Text)) } - for _, subst := range paths[matchedPattern.Text] { + for _, subst := range paths.GetOrZero(matchedPattern.Text) { path := strings.Replace(subst, "*", matchedStar, 1) candidate := tspath.NormalizePath(tspath.CombinePaths(containingDirectory, path)) if r.resolver.traceEnabled() { @@ -1688,7 +1688,7 @@ func moveToNextDirectorySeparatorIfAvailable(path string, prevSeparatorIndex int } func getPathsBasePath(options *core.CompilerOptions, currentDirectory string) string { - if len(options.Paths) == 0 { + if options.Paths.Size() == 0 { return "" } if options.PathsBasePath != "" { @@ -1702,12 +1702,12 @@ type parsedPatterns struct { patterns []core.Pattern } -func tryParsePatterns(paths map[string][]string) parsedPatterns { +func tryParsePatterns(paths *collections.OrderedMap[string, []string]) parsedPatterns { // !!! TS has a weakmap cache // We could store a cache on Resolver, but maybe we can wait and profile matchableStringSet := collections.OrderedSet[string]{} - patterns := make([]core.Pattern, 0, len(paths)) - for path := range paths { + patterns := make([]core.Pattern, 0, paths.Size()) + for path := range paths.Keys() { if pattern := core.TryParsePattern(path); pattern.IsValid() { if pattern.StarIndex == -1 { matchableStringSet.Add(path) diff --git a/internal/compiler/packagejson/cache.go b/internal/compiler/packagejson/cache.go index ef5dca8610..6c93fc99c7 100644 --- a/internal/compiler/packagejson/cache.go +++ b/internal/compiler/packagejson/cache.go @@ -70,33 +70,35 @@ func (p *PackageJson) GetVersionPaths(trace func(string)) VersionPaths { type VersionPaths struct { Version string pathsJSON *collections.OrderedMap[string, JSONValue] - paths map[string][]string + paths *collections.OrderedMap[string, []string] } func (v *VersionPaths) Exists() bool { return v != nil && v.Version != "" && v.pathsJSON != nil } -func (v *VersionPaths) GetPaths() map[string][]string { +func (v *VersionPaths) GetPaths() *collections.OrderedMap[string, []string] { if !v.Exists() { return nil } if v.paths != nil { return v.paths } - v.paths = make(map[string][]string, v.pathsJSON.Size()) + paths := collections.NewOrderedMapWithSizeHint[string, []string](v.pathsJSON.Size()) for key, value := range v.pathsJSON.Entries() { if value.Type != JSONValueTypeArray { continue } - v.paths[key] = make([]string, len(value.AsArray())) + slice := make([]string, len(value.AsArray())) for i, path := range value.AsArray() { if path.Type != JSONValueTypeString { continue } - v.paths[key][i] = path.Value.(string) + slice[i] = path.Value.(string) } + v.paths.Set(key, slice) } + v.paths = paths return v.paths } diff --git a/internal/core/compileroptions.go b/internal/core/compileroptions.go index edf1321754..eeffef595d 100644 --- a/internal/core/compileroptions.go +++ b/internal/core/compileroptions.go @@ -3,116 +3,117 @@ package core import ( "strings" + "github.com/microsoft/typescript-go/internal/collections" "github.com/microsoft/typescript-go/internal/tspath" ) //go:generate go run golang.org/x/tools/cmd/stringer -type=ModuleKind,ScriptTarget -output=compileroptions_stringer_generated.go type CompilerOptions struct { - AllowJs Tristate `json:"allowJs"` - AllowArbitraryExtensions Tristate `json:"allowArbitraryExtensions"` - AllowSyntheticDefaultImports Tristate `json:"allowSyntheticDefaultImports"` - AllowImportingTsExtensions Tristate `json:"allowImportingTsExtensions"` - AllowNonTsExtensions Tristate `json:"allowNonTsExtensions"` - AllowUmdGlobalAccess Tristate `json:"allowUmdGlobalAccess"` - AllowUnreachableCode Tristate `json:"allowUnreachableCode"` - AllowUnusedLabels Tristate `json:"allowUnusedLabels"` - AssumeChangesOnlyAffectDirectDependencies Tristate `json:"assumeChangesOnlyAffectDirectDependencies"` - AlwaysStrict Tristate `json:"alwaysStrict"` - BaseUrl string `json:"baseUrl"` - Build Tristate `json:"build"` - CheckJs Tristate `json:"checkJs"` - CustomConditions []string `json:"customConditions"` - Composite Tristate `json:"composite"` - EmitDeclarationOnly Tristate `json:"emitDeclarationOnly"` - EmitBOM Tristate `json:"emitBOM"` - EmitDecoratorMetadata Tristate `json:"emitDecoratorMetadata"` - DownlevelIteration Tristate `json:"downlevelIteration"` - Declaration Tristate `json:"declaration"` - DeclarationDir string `json:"declarationDir"` - DeclarationMap Tristate `json:"declarationMap"` - DisableSizeLimit Tristate `json:"disableSizeLimit"` - DisableSourceOfProjectReferenceRedirect Tristate `json:"disableSourceOfProjectReferenceRedirect"` - DisableSolutionSearching Tristate `json:"disableSolutionSearching"` - DisableReferencedProjectLoad Tristate `json:"disableReferencedProjectLoad"` - ESModuleInterop Tristate `json:"esModuleInterop"` - ExactOptionalPropertyTypes Tristate `json:"exactOptionalPropertyTypes"` - ExperimentalDecorators Tristate `json:"experimentalDecorators"` - ForceConsistentCasingInFileNames Tristate `json:"forceConsistentCasingInFileNames"` - IsolatedModules Tristate `json:"isolatedModules"` - IsolatedDeclarations Tristate `json:"isolatedDeclarations"` - IgnoreDeprecations string `json:"ignoreDeprecations"` - ImportHelpers Tristate `json:"importHelpers"` - InlineSourceMap Tristate `json:"inlineSourceMap"` - InlineSources Tristate `json:"inlineSources"` - Init Tristate `json:"init"` - Incremental Tristate `json:"incremental"` - Jsx JsxEmit `json:"jsx"` - JsxFactory string `json:"jsxFactory"` - JsxFragmentFactory string `json:"jsxFragmentFactory"` - JsxImportSource string `json:"jsxImportSource"` - KeyofStringsOnly Tristate `json:"keyofStringsOnly"` - Lib []string `json:"lib"` - Locale string `json:"locale"` - MapRoot string `json:"mapRoot"` - ModuleKind ModuleKind `json:"module"` - ModuleResolution ModuleResolutionKind `json:"moduleResolution"` - ModuleSuffixes []string `json:"moduleSuffixes"` - ModuleDetection ModuleDetectionKind `json:"moduleDetectionKind"` - NewLine NewLineKind `json:"newLine"` - NoEmit Tristate `json:"noEmit"` - NoCheck Tristate `json:"noCheck"` - NoErrorTruncation Tristate `json:"noErrorTruncation"` - NoFallthroughCasesInSwitch Tristate `json:"noFallthroughCasesInSwitch"` - NoImplicitAny Tristate `json:"noImplicitAny"` - NoImplicitThis Tristate `json:"noImplicitThis"` - NoImplicitReturns Tristate `json:"noImplicitReturns"` - NoEmitHelpers Tristate `json:"noEmitHelpers"` - NoLib Tristate `json:"noLib"` - NoPropertyAccessFromIndexSignature Tristate `json:"noPropertyAccessFromIndexSignature"` - NoUncheckedIndexedAccess Tristate `json:"noUncheckedIndexedAccess"` - NoEmitOnError Tristate `json:"noEmitOnError"` - NoUnusedLocals Tristate `json:"noUnusedLocals"` - NoUnusedParameters Tristate `json:"noUnusedParameters"` - NoResolve Tristate `json:"noResolve"` - NoImplicitOverride Tristate `json:"noImplicitOverride"` - NoUncheckedSideEffectImports Tristate `json:"noUncheckedSideEffectImports"` - Out string `json:"out"` - OutDir string `json:"outDir"` - OutFile string `json:"outFile"` - Paths map[string][]string `json:"paths"` - PreserveConstEnums Tristate `json:"preserveConstEnums"` - PreserveSymlinks Tristate `json:"preserveSymlinks"` - Project string `json:"project"` - ResolveJsonModule Tristate `json:"resolveJsonModule"` - ResolvePackageJsonExports Tristate `json:"resolvePackageJsonExports"` - ResolvePackageJsonImports Tristate `json:"resolvePackageJsonImports"` - RemoveComments Tristate `json:"removeComments"` - RewriteRelativeImportExtensions Tristate `json:"rewriteRelativeImportExtensions"` - ReactNamespace string `json:"reactNamespace"` - RootDir string `json:"rootDir"` - RootDirs []string `json:"rootDirs"` - SkipLibCheck Tristate `json:"skipLibCheck"` - Strict Tristate `json:"strict"` - StrictBindCallApply Tristate `json:"strictBindCallApply"` - StrictBuiltinIteratorReturn Tristate `json:"strictBuiltinIteratorReturn"` - StrictFunctionTypes Tristate `json:"strictFunctionTypes"` - StrictNullChecks Tristate `json:"strictNullChecks"` - StrictPropertyInitialization Tristate `json:"strictPropertyInitialization"` - StripInternal Tristate `json:"stripInternal"` - SkipDefaultLibCheck Tristate `json:"skipDefaultLibCheck"` - SourceMap Tristate `json:"sourceMap"` - SourceRoot string `json:"sourceRoot"` - SuppressOutputPathCheck Tristate `json:"suppressOutputPathCheck"` - Target ScriptTarget `json:"target"` - TraceResolution Tristate `json:"traceResolution"` - TsBuildInfoFile string `json:"tsBuildInfoFile"` - TypeRoots []string `json:"typeRoots"` - Types []string `json:"types"` - UseDefineForClassFields Tristate `json:"useDefineForClassFields"` - UseUnknownInCatchVariables Tristate `json:"useUnknownInCatchVariables"` - VerbatimModuleSyntax Tristate `json:"verbatimModuleSyntax"` - MaxNodeModuleJsDepth *int `json:"maxNodeModuleJsDepth"` + AllowJs Tristate `json:"allowJs"` + AllowArbitraryExtensions Tristate `json:"allowArbitraryExtensions"` + AllowSyntheticDefaultImports Tristate `json:"allowSyntheticDefaultImports"` + AllowImportingTsExtensions Tristate `json:"allowImportingTsExtensions"` + AllowNonTsExtensions Tristate `json:"allowNonTsExtensions"` + AllowUmdGlobalAccess Tristate `json:"allowUmdGlobalAccess"` + AllowUnreachableCode Tristate `json:"allowUnreachableCode"` + AllowUnusedLabels Tristate `json:"allowUnusedLabels"` + AssumeChangesOnlyAffectDirectDependencies Tristate `json:"assumeChangesOnlyAffectDirectDependencies"` + AlwaysStrict Tristate `json:"alwaysStrict"` + BaseUrl string `json:"baseUrl"` + Build Tristate `json:"build"` + CheckJs Tristate `json:"checkJs"` + CustomConditions []string `json:"customConditions"` + Composite Tristate `json:"composite"` + EmitDeclarationOnly Tristate `json:"emitDeclarationOnly"` + EmitBOM Tristate `json:"emitBOM"` + EmitDecoratorMetadata Tristate `json:"emitDecoratorMetadata"` + DownlevelIteration Tristate `json:"downlevelIteration"` + Declaration Tristate `json:"declaration"` + DeclarationDir string `json:"declarationDir"` + DeclarationMap Tristate `json:"declarationMap"` + DisableSizeLimit Tristate `json:"disableSizeLimit"` + DisableSourceOfProjectReferenceRedirect Tristate `json:"disableSourceOfProjectReferenceRedirect"` + DisableSolutionSearching Tristate `json:"disableSolutionSearching"` + DisableReferencedProjectLoad Tristate `json:"disableReferencedProjectLoad"` + ESModuleInterop Tristate `json:"esModuleInterop"` + ExactOptionalPropertyTypes Tristate `json:"exactOptionalPropertyTypes"` + ExperimentalDecorators Tristate `json:"experimentalDecorators"` + ForceConsistentCasingInFileNames Tristate `json:"forceConsistentCasingInFileNames"` + IsolatedModules Tristate `json:"isolatedModules"` + IsolatedDeclarations Tristate `json:"isolatedDeclarations"` + IgnoreDeprecations string `json:"ignoreDeprecations"` + ImportHelpers Tristate `json:"importHelpers"` + InlineSourceMap Tristate `json:"inlineSourceMap"` + InlineSources Tristate `json:"inlineSources"` + Init Tristate `json:"init"` + Incremental Tristate `json:"incremental"` + Jsx JsxEmit `json:"jsx"` + JsxFactory string `json:"jsxFactory"` + JsxFragmentFactory string `json:"jsxFragmentFactory"` + JsxImportSource string `json:"jsxImportSource"` + KeyofStringsOnly Tristate `json:"keyofStringsOnly"` + Lib []string `json:"lib"` + Locale string `json:"locale"` + MapRoot string `json:"mapRoot"` + ModuleKind ModuleKind `json:"module"` + ModuleResolution ModuleResolutionKind `json:"moduleResolution"` + ModuleSuffixes []string `json:"moduleSuffixes"` + ModuleDetection ModuleDetectionKind `json:"moduleDetectionKind"` + NewLine NewLineKind `json:"newLine"` + NoEmit Tristate `json:"noEmit"` + NoCheck Tristate `json:"noCheck"` + NoErrorTruncation Tristate `json:"noErrorTruncation"` + NoFallthroughCasesInSwitch Tristate `json:"noFallthroughCasesInSwitch"` + NoImplicitAny Tristate `json:"noImplicitAny"` + NoImplicitThis Tristate `json:"noImplicitThis"` + NoImplicitReturns Tristate `json:"noImplicitReturns"` + NoEmitHelpers Tristate `json:"noEmitHelpers"` + NoLib Tristate `json:"noLib"` + NoPropertyAccessFromIndexSignature Tristate `json:"noPropertyAccessFromIndexSignature"` + NoUncheckedIndexedAccess Tristate `json:"noUncheckedIndexedAccess"` + NoEmitOnError Tristate `json:"noEmitOnError"` + NoUnusedLocals Tristate `json:"noUnusedLocals"` + NoUnusedParameters Tristate `json:"noUnusedParameters"` + NoResolve Tristate `json:"noResolve"` + NoImplicitOverride Tristate `json:"noImplicitOverride"` + NoUncheckedSideEffectImports Tristate `json:"noUncheckedSideEffectImports"` + Out string `json:"out"` + OutDir string `json:"outDir"` + OutFile string `json:"outFile"` + Paths *collections.OrderedMap[string, []string] `json:"paths"` + PreserveConstEnums Tristate `json:"preserveConstEnums"` + PreserveSymlinks Tristate `json:"preserveSymlinks"` + Project string `json:"project"` + ResolveJsonModule Tristate `json:"resolveJsonModule"` + ResolvePackageJsonExports Tristate `json:"resolvePackageJsonExports"` + ResolvePackageJsonImports Tristate `json:"resolvePackageJsonImports"` + RemoveComments Tristate `json:"removeComments"` + RewriteRelativeImportExtensions Tristate `json:"rewriteRelativeImportExtensions"` + ReactNamespace string `json:"reactNamespace"` + RootDir string `json:"rootDir"` + RootDirs []string `json:"rootDirs"` + SkipLibCheck Tristate `json:"skipLibCheck"` + Strict Tristate `json:"strict"` + StrictBindCallApply Tristate `json:"strictBindCallApply"` + StrictBuiltinIteratorReturn Tristate `json:"strictBuiltinIteratorReturn"` + StrictFunctionTypes Tristate `json:"strictFunctionTypes"` + StrictNullChecks Tristate `json:"strictNullChecks"` + StrictPropertyInitialization Tristate `json:"strictPropertyInitialization"` + StripInternal Tristate `json:"stripInternal"` + SkipDefaultLibCheck Tristate `json:"skipDefaultLibCheck"` + SourceMap Tristate `json:"sourceMap"` + SourceRoot string `json:"sourceRoot"` + SuppressOutputPathCheck Tristate `json:"suppressOutputPathCheck"` + Target ScriptTarget `json:"target"` + TraceResolution Tristate `json:"traceResolution"` + TsBuildInfoFile string `json:"tsBuildInfoFile"` + TypeRoots []string `json:"typeRoots"` + Types []string `json:"types"` + UseDefineForClassFields Tristate `json:"useDefineForClassFields"` + UseUnknownInCatchVariables Tristate `json:"useUnknownInCatchVariables"` + VerbatimModuleSyntax Tristate `json:"verbatimModuleSyntax"` + MaxNodeModuleJsDepth *int `json:"maxNodeModuleJsDepth"` // Internal fields ConfigFilePath string `json:"configFilePath"` diff --git a/internal/tsoptions/commandlineparser.go b/internal/tsoptions/commandlineparser.go index f58a4b0855..90281bb3de 100644 --- a/internal/tsoptions/commandlineparser.go +++ b/internal/tsoptions/commandlineparser.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/microsoft/typescript-go/internal/ast" + "github.com/microsoft/typescript-go/internal/collections" "github.com/microsoft/typescript-go/internal/compiler/diagnostics" "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/stringutil" @@ -31,7 +32,7 @@ type commandLineParser struct { workerDiagnostics *ParseCommandLineWorkerDiagnostics optionsMap *NameMap fs vfs.FS - options map[string]any + options *collections.OrderedMap[string, any] fileNames []string errors []*ast.Diagnostic } @@ -69,7 +70,7 @@ func parseCommandLineWorker( fs: fs, workerDiagnostics: parseCommandLineWithDiagnostics, fileNames: []string{}, - options: map[string]any{}, + options: &collections.OrderedMap[string, any]{}, errors: []*ast.Diagnostic{}, } parser.optionsMap = GetNameMapFromList(parser.OptionsDeclarations()) @@ -184,11 +185,11 @@ func (p *commandLineParser) parseOptionValue( optValue = args[i] } if optValue == "null" { - p.options[opt.Name] = nil + p.options.Set(opt.Name, nil) i++ } else if opt.Kind == "boolean" { if optValue == "false" { - p.options[opt.Name] = false + p.options.Set(opt.Name, false) i++ } else { if optValue == "true" { @@ -211,12 +212,12 @@ func (p *commandLineParser) parseOptionValue( } p.errors = append(p.errors, ast.NewCompilerDiagnostic(diag, opt.Name, getCompilerOptionValueTypeString(opt))) if opt.Kind == "list" { - p.options[opt.Name] = []string{} + p.options.Set(opt.Name, []string{}) } else if opt.Kind == "enum" { p.errors = append(p.errors, createDiagnosticForInvalidEnumType(opt, nil, nil)) } } else { - p.options[opt.Name] = true + p.options.Set(opt.Name, true) } return i } @@ -226,7 +227,7 @@ func (p *commandLineParser) parseOptionValue( // todo: Make sure this parseInt matches JS parseInt num, e := strconv.ParseInt(args[i], 10, 0) if e == nil { - p.options[opt.Name] = num + p.options.Set(opt.Name, num) } i++ case "boolean": @@ -235,9 +236,9 @@ func (p *commandLineParser) parseOptionValue( // check next argument as boolean flag value if optValue == "false" { - p.options[opt.Name] = false + p.options.Set(opt.Name, false) } else { - p.options[opt.Name] = true + p.options.Set(opt.Name, true) } // try to consume next argument as value for boolean flag; do not consume argument if it is not "true" or "false" if optValue == "false" || optValue == "true" { @@ -246,14 +247,14 @@ func (p *commandLineParser) parseOptionValue( case "string": val, err := validateJsonOptionValue(opt, args[i], nil, nil) if err == nil { - p.options[opt.Name] = val + p.options.Set(opt.Name, val) } else { p.errors = append(p.errors, err...) } i++ case "list": result, err := p.parseListTypeOption(opt, args[i]) - p.options[opt.Name] = result + p.options.Set(opt.Name, result) p.errors = append(p.errors, err...) if len(result) > 0 || len(err) > 0 { i++ @@ -263,12 +264,12 @@ func (p *commandLineParser) parseOptionValue( panic("listOrElement not supported here") default: val, err := convertJsonOptionOfEnumType(opt, strings.TrimFunc(args[i], stringutil.IsWhiteSpaceLike), nil, nil) - p.options[opt.Name] = val + p.options.Set(opt.Name, val) p.errors = append(p.errors, err...) i++ } } else { - p.options[opt.Name] = nil + p.options.Set(opt.Name, nil) i++ } } diff --git a/internal/tsoptions/export_test.go b/internal/tsoptions/export_test.go index cc532a1e72..bc5d78e6c7 100644 --- a/internal/tsoptions/export_test.go +++ b/internal/tsoptions/export_test.go @@ -2,6 +2,7 @@ package tsoptions import ( "github.com/microsoft/typescript-go/internal/ast" + "github.com/microsoft/typescript-go/internal/collections" "github.com/microsoft/typescript-go/internal/vfs" ) @@ -21,7 +22,7 @@ func ParseCommandLineTestWorker( fs: fs, workerDiagnostics: CompilerOptionsDidYouMeanDiagnostics, fileNames: []string{}, - options: map[string]any{}, + options: &collections.OrderedMap[string, any]{}, errors: []*ast.Diagnostic{}, } if len(decls) != 0 { @@ -43,6 +44,6 @@ type TestCommandLineParser struct { Fs vfs.FS WorkerDiagnostics *ParseCommandLineWorkerDiagnostics FileNames []string - Options map[string]any + Options *collections.OrderedMap[string, any] Errors []*ast.Diagnostic } diff --git a/internal/tsoptions/parsinghelpers.go b/internal/tsoptions/parsinghelpers.go index 9208642906..7c8c00a77c 100644 --- a/internal/tsoptions/parsinghelpers.go +++ b/internal/tsoptions/parsinghelpers.go @@ -4,6 +4,7 @@ import ( "reflect" "github.com/microsoft/typescript-go/internal/ast" + "github.com/microsoft/typescript-go/internal/collections" "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/tspath" ) @@ -32,11 +33,11 @@ func parseStringArray(value any) []string { return nil } -func parseStringMap(value any) map[string][]string { - if m, ok := value.(map[string]any); ok { - result := make(map[string][]string) - for k, v := range m { - result[k] = parseStringArray(v) +func parseStringMap(value any) *collections.OrderedMap[string, []string] { + if m, ok := value.(*collections.OrderedMap[string, any]); ok { + result := collections.NewOrderedMapWithSizeHint[string, []string](m.Size()) + for k, v := range m.Entries() { + result.Set(k, parseStringArray(v)) } return result } @@ -59,15 +60,15 @@ func parseNumber(value any) *int { func parseProjectReference(json any) []core.ProjectReference { var result []core.ProjectReference - if v, ok := json.(map[string]any); ok { + if v, ok := json.(*collections.OrderedMap[string, any]); ok { var reference core.ProjectReference - if v, ok := v["path"]; ok { + if v, ok := v.Get("path"); ok { reference.Path = v.(string) } - if v, ok := v["originalPath"]; ok { + if v, ok := v.Get("originalPath"); ok { reference.OriginalPath = v.(string) } - if v, ok := v["circular"]; ok { + if v, ok := v.Get("circular"); ok { reference.Circular = v.(bool) } result = append(result, reference) @@ -75,51 +76,51 @@ func parseProjectReference(json any) []core.ProjectReference { return result } -func parseJsonToStringKey(json any) map[string]any { - result := make(map[string]any) - if m, ok := json.(map[string]any); ok { - if v, ok := m["include"]; ok { +func parseJsonToStringKey(json any) *collections.OrderedMap[string, any] { + result := collections.NewOrderedMapWithSizeHint[string, any](6) + if m, ok := json.(*collections.OrderedMap[string, any]); ok { + if v, ok := m.Get("include"); ok { if arr, ok := v.([]string); ok && len(arr) == 0 { - result["include"] = []any{} + result.Set("include", []any{}) } else { - result["include"] = v + result.Set("include", v) } } - if v, ok := m["exclude"]; ok { + if v, ok := m.Get("exclude"); ok { if arr, ok := v.([]string); ok && len(arr) == 0 { - result["exclude"] = []any{} + result.Set("exclude", []any{}) } else { - result["exclude"] = v + result.Set("exclude", v) } } - if v, ok := m["files"]; ok { + if v, ok := m.Get("files"); ok { if arr, ok := v.([]string); ok && len(arr) == 0 { - result["files"] = []any{} + result.Set("files", []any{}) } else { - result["files"] = v + result.Set("files", v) } } - if v, ok := m["references"]; ok { + if v, ok := m.Get("references"); ok { if arr, ok := v.([]string); ok && len(arr) == 0 { - result["references"] = []any{} + result.Set("references", []any{}) } else { - result["references"] = v + result.Set("references", v) } } - if v, ok := m["extends"]; ok { + if v, ok := m.Get("extends"); ok { if arr, ok := v.([]string); ok && len(arr) == 0 { - result["extends"] = []any{} + result.Set("extends", []any{}) } else if str, ok := v.(string); ok { - result["extends"] = []any{str} + result.Set("extends", []any{str}) } else { - result["extends"] = v + result.Set("extends", v) } } - if v, ok := m["compilerOptions"]; ok { - result["compilerOptions"] = v + if v, ok := m.Get("compilerOptions"); ok { + result.Set("compilerOptions", v) } - if v, ok := m["excludes"]; ok { - result["excludes"] = v + if v, ok := m.Get("excludes"); ok { + result.Set("excludes", v) } } return result @@ -436,25 +437,25 @@ func mergeCompilerOptions(targetOptions, sourceOptions *core.CompilerOptions) *c return targetOptions } -func convertToOptionsWithAbsolutePaths(optionsBase map[string]any, optionMap map[string]*CommandLineOption, cwd string) map[string]any { +func convertToOptionsWithAbsolutePaths(optionsBase *collections.OrderedMap[string, any], optionMap map[string]*CommandLineOption, cwd string) *collections.OrderedMap[string, any] { // !!! convert to options with absolute paths was previously done with `CompilerOptions` object, but for ease of implementation, we do it pre-conversion. // !!! Revisit this choice if/when refactoring when conversion is done in tsconfig parsing if optionsBase == nil { return nil } - for o, v := range optionsBase { + for o, v := range optionsBase.Entries() { option := optionMap[o] if option == nil || !option.isFilePath { continue } if option.Kind == "list" { if arr, ok := v.([]string); ok { - optionsBase[o] = core.Map(arr, func(item string) string { + optionsBase.Set(o, core.Map(arr, func(item string) string { return tspath.GetNormalizedAbsolutePath(item, cwd) - }) + })) } } else { - optionsBase[o] = tspath.GetNormalizedAbsolutePath(v.(string), cwd) + optionsBase.Set(o, tspath.GetNormalizedAbsolutePath(v.(string), cwd)) } } return optionsBase diff --git a/internal/tsoptions/tsconfigparsing.go b/internal/tsoptions/tsconfigparsing.go index 457506c896..b5badf32df 100644 --- a/internal/tsoptions/tsconfigparsing.go +++ b/internal/tsoptions/tsconfigparsing.go @@ -240,11 +240,13 @@ func convertConfigFileToObject( return convertToJson(sourceFile, firstObject, true /*returnValue*/, jsonConversionNotifier) } } - return make(map[string]any), errors + 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 { @@ -270,7 +272,7 @@ func isCompilerOptionsValue(option *CommandLineOption, value any) bool { return reflect.TypeOf(value).Kind() == reflect.Float64 } if option.Kind == "object" { - return reflect.TypeOf(value).Kind() == reflect.Map + return reflect.TypeOf(value) == orderedMapType } if option.Kind == "enum" && reflect.TypeOf(value).Kind() == reflect.String { _, ok := option.EnumMap().Get(strings.ToLower(value.(string))) @@ -522,8 +524,8 @@ func convertOptionsFromJson[O optionParser](optionsNameMap map[string]*CommandLi convertOption = convertJsonOption } var errors []*ast.Diagnostic - if _, ok := jsonOptions.(map[string]any); ok { - for key, value := range jsonOptions.(map[string]any) { + if _, ok := jsonOptions.(*collections.OrderedMap[string, any]); ok { + for key, value := range jsonOptions.(*collections.OrderedMap[string, any]).Entries() { opt, ok := optionsNameMap[key] commandLineOptionEnumMapVal := opt.EnumMap() if commandLineOptionEnumMapVal != nil { @@ -615,12 +617,10 @@ func convertObjectLiteralExpressionToJson( node *ast.ObjectLiteralExpression, objectOption *CommandLineOption, jsonConversionNotifier *jsonConversionNotifier, -) (map[string]any, []*ast.Diagnostic) { - var result map[string]any +) (*collections.OrderedMap[string, any], []*ast.Diagnostic) { + var result *collections.OrderedMap[string, any] if returnValue { - result = make(map[string]any) - } else { - result = nil + result = &collections.OrderedMap[string, any]{} } var errors []*ast.Diagnostic for _, element := range node.Properties.Nodes { @@ -649,7 +649,7 @@ func convertObjectLiteralExpressionToJson( errors = append(errors, err...) if keyText != "" { if returnValue { - result[keyText] = value + result.Set(keyText, value) } // Notify key value set, if user asked for it if jsonConversionNotifier != nil { @@ -781,23 +781,23 @@ func convertCompilerOptionsFromJsonWorker(jsonOptions any, basePath string, conf } func parseOwnConfigOfJson( - json map[string]any, + json *collections.OrderedMap[string, any], host ParseConfigHost, basePath string, configFileName string, ) (*parsedTsconfig, []*ast.Diagnostic) { var errors []*ast.Diagnostic - if json["excludes"] != nil { + if json.Has("excludes") { errors = append(errors, ast.NewCompilerDiagnostic(diagnostics.Unknown_option_excludes_Did_you_mean_exclude)) } - options, err := convertCompilerOptionsFromJsonWorker(json["compilerOptions"], basePath, configFileName) + options, err := convertCompilerOptionsFromJsonWorker(json.GetOrZero("compilerOptions"), basePath, configFileName) errors = append(errors, err...) // typeAcquisition := convertTypeAcquisitionFromJsonWorker(json.typeAcquisition, basePath, errors, configFileName) // watchOptions := convertWatchOptionsFromJsonWorker(json.watchOptions, basePath, errors) // json.compileOnSave = convertCompileOnSaveOptionFromJson(json, basePath, errors) var extendedConfigPath []string - if json["extends"] != nil && json["extends"] != "" { - extendedConfigPath, err = getExtendsConfigPathOrArray(json["extends"], host, basePath, configFileName, nil, nil, nil) + if extends := json.GetOrZero("extends"); extends != nil && extends != "" { + extendedConfigPath, err = getExtendsConfigPathOrArray(extends, host, basePath, configFileName, nil, nil, nil) errors = append(errors, err...) } parsedConfig := &parsedTsconfig{ @@ -877,7 +877,7 @@ func getExtendedConfig( // parseConfig just extracts options/include/exclude/files out of a config file. // It does not resolve the included files. func parseConfig( - json map[string]any, + json *collections.OrderedMap[string, any], sourceFile *TsConfigSourceFile, host ParseConfigHost, basePath string, @@ -891,7 +891,7 @@ func parseConfig( if slices.Contains(resolutionStack, resolvedPath) { var result *parsedTsconfig errors = append(errors, ast.NewCompilerDiagnostic(diagnostics.Circularity_detected_while_resolving_configuration_Colon_0)) - if len(json) == 0 { + if json.Size() == 0 { result = &parsedTsconfig{raw: json} } else { rawResult, err := convertToObject(sourceFile.SourceFile) @@ -924,12 +924,12 @@ func parseConfig( extendsRaw := extendedConfig.raw relativeDifference := "" setPropertyValue := func(propertyName string) { - if rawMap, ok := ownConfig.raw.(map[string]any); ok && rawMap[propertyName] != nil { + 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.(map[string]any); ok && rawMap[propertyName] != nil { - value := core.Map(rawMap[propertyName].([]any), func(path any) any { + if rawMap, ok := extendsRaw.(*collections.OrderedMap[string, any]); ok && rawMap.Has(propertyName) { + value := core.Map(rawMap.GetOrZero(propertyName).([]any), func(path any) any { if startsWithConfigDirTemplate(path) || tspath.IsRootedDiskPath(path.(string)) { return path.(string) } else { @@ -957,8 +957,8 @@ func parseConfig( setPropertyValue("include") setPropertyValue("exclude") setPropertyValue("files") - if extendedRawMap, ok := extendsRaw.(map[string]any); ok && extendedRawMap["compileOnSave"] != nil { - if compileOnSave, ok := extendedRawMap["compileOnSave"].(bool); ok { + if extendedRawMap, ok := extendsRaw.(*collections.OrderedMap[string, any]); ok && extendedRawMap.Has("compileOnSave") { + if compileOnSave, ok := extendedRawMap.GetOrZero("compileOnSave").(bool); ok { result.compileOnSave = compileOnSave } } @@ -980,16 +980,16 @@ func parseConfig( } } if result.include != nil { - ownConfig.raw.(map[string]any)["include"] = result.include + ownConfig.raw.(*collections.OrderedMap[string, any]).Set("include", result.include) } if result.exclude != nil { - ownConfig.raw.(map[string]any)["exclude"] = result.exclude + ownConfig.raw.(*collections.OrderedMap[string, any]).Set("exclude", result.exclude) } if result.files != nil { - ownConfig.raw.(map[string]any)["files"] = result.files + ownConfig.raw.(*collections.OrderedMap[string, any]).Set("files", result.files) } - if result.compileOnSave && ownConfig.raw.(map[string]any)["compileOnSave"] == nil { - ownConfig.raw.(map[string]any)["compileOnSave"] = result.compileOnSave + 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() { @@ -1018,7 +1018,7 @@ type propOfRaw struct { // 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 map[string]any, + json *collections.OrderedMap[string, any], sourceFile *TsConfigSourceFile, host ParseConfigHost, basePath string, @@ -1047,10 +1047,10 @@ func parseJsonConfigFileContentWorker( parsedConfig.options.ConfigFilePath = tspath.NormalizeSlashes(configFileName) } getPropFromRaw := func(prop string, validateElement func(value any) bool, elementTypeName string) propOfRaw { - value, exists := rawConfig[prop] + value, exists := rawConfig.Get(prop) if exists { if reflect.TypeOf(value).Kind() == reflect.Slice { - result := rawConfig[prop] + 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)) @@ -1064,14 +1064,14 @@ func parseJsonConfigFileContentWorker( } return propOfRaw{sliceValue: nil, wrongValue: "no-prop"} } - referencesOfRaw := getPropFromRaw("references", func(element any) bool { return reflect.TypeOf(element).Kind() == reflect.Map }, "object") + 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["extends"] + hasExtends := rawConfig.GetOrZero("extends") if fileSpecs.sliceValue != nil && len(fileSpecs.sliceValue) == 0 && hasZeroOrNoReferences && hasExtends == nil { if sourceFile != nil { var fileName string @@ -1169,7 +1169,7 @@ func parseJsonConfigFileContentWorker( getProjectReferences := func(basePath string) []core.ProjectReference { var projectReferences []core.ProjectReference = []core.ProjectReference{} - referencesOfRaw := getPropFromRaw("references", func(element any) bool { return reflect.TypeOf(element).Kind() == reflect.Map }, "object") + referencesOfRaw := getPropFromRaw("references", func(element any) bool { return reflect.TypeOf(element) == orderedMapType }, "object") if referencesOfRaw.sliceValue != nil { for _, reference := range referencesOfRaw.sliceValue { for _, ref := range parseProjectReference(reference) { @@ -1201,9 +1201,9 @@ func parseJsonConfigFileContentWorker( } } -func canJsonReportNoInputFiles(rawConfig map[string]any) bool { - _, filesExists := rawConfig["files"] - _, referencesExists := rawConfig["references"] +func canJsonReportNoInputFiles(rawConfig *collections.OrderedMap[string, any]) bool { + filesExists := rawConfig.Has("files") + referencesExists := rawConfig.Has("references") return !filesExists && !referencesExists } @@ -1343,7 +1343,7 @@ func handleOptionConfigDirTemplateSubstitution(options *core.CompilerOptions, ba // !!! don't hardcode this; use options declarations? - for _, v := range options.Paths { + for v := range options.Paths.Values() { substituteStringArrayWithConfigDirTemplate(v, basePath) } From 36c2754007569fce22ca65bc80786184cf0a7fff Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 19 Feb 2025 13:57:06 -0800 Subject: [PATCH 2/3] Implement MarshalJSON for OrderedMap --- internal/collections/ordered_map.go | 24 +++++++++++++++++-- ...er flags with input files in the middle.js | 2 +- .../Parse multiple library compiler flags .js | 2 +- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/internal/collections/ordered_map.go b/internal/collections/ordered_map.go index 8bbc3fbd07..3ac3abe726 100644 --- a/internal/collections/ordered_map.go +++ b/internal/collections/ordered_map.go @@ -193,8 +193,28 @@ func (m OrderedMap[K, V]) MarshalJSON() ([]byte, error) { if len(m.mp) == 0 { return []byte("{}"), nil } - // TODO(jakebailey): implement proper sort order - return json.Marshal(m.mp) + var buf bytes.Buffer + buf.WriteByte('{') + enc := json.NewEncoder(&buf) + enc.SetEscapeHTML(false) + + for i, k := range m.keys { + if i > 0 { + buf.WriteByte(',') + } + + if err := enc.Encode(k); err != nil { + return nil, err + } + + buf.WriteByte(':') + + if err := enc.Encode(m.mp[k]); err != nil { + return nil, err + } + } + buf.WriteByte('}') + return buf.Bytes(), nil } func (m *OrderedMap[K, V]) UnmarshalJSON(data []byte) error { diff --git a/testdata/baselines/reference/tsoptions/commandLineParsing/Parse multiple compiler flags with input files in the middle.js b/testdata/baselines/reference/tsoptions/commandLineParsing/Parse multiple compiler flags with input files in the middle.js index 060908a86e..d694954ad4 100644 --- a/testdata/baselines/reference/tsoptions/commandLineParsing/Parse multiple compiler flags with input files in the middle.js +++ b/testdata/baselines/reference/tsoptions/commandLineParsing/Parse multiple compiler flags with input files in the middle.js @@ -2,7 +2,7 @@ Args:: ["--module", "commonjs", "--target", "es5", "0.ts", "--lib", "es5,es2015.symbol.wellknown"] CompilerOptions:: -{"lib":["lib.es5.d.ts","lib.es2015.symbol.wellknown.d.ts"],"module":1,"target":1} +{"module":1,"target":1,"lib":["lib.es5.d.ts","lib.es2015.symbol.wellknown.d.ts"]} FileNames:: 0.ts diff --git a/testdata/baselines/reference/tsoptions/commandLineParsing/Parse multiple library compiler flags .js b/testdata/baselines/reference/tsoptions/commandLineParsing/Parse multiple library compiler flags .js index 9b6384d027..2042e46d1e 100644 --- a/testdata/baselines/reference/tsoptions/commandLineParsing/Parse multiple library compiler flags .js +++ b/testdata/baselines/reference/tsoptions/commandLineParsing/Parse multiple library compiler flags .js @@ -2,7 +2,7 @@ Args:: ["--module", "commonjs", "--target", "es5", "--lib", "es5", "0.ts", "--lib", "es2015.core, es2015.symbol.wellknown "] CompilerOptions:: -{"lib":["lib.es2015.core.d.ts","lib.es2015.symbol.wellknown.d.ts"],"module":1,"target":1} +{"module":1,"target":1,"lib":["lib.es2015.core.d.ts","lib.es2015.symbol.wellknown.d.ts"]} FileNames:: 0.ts From e1e4bda14b72562746b451c4a7edf5d8e62b8ae5 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 19 Feb 2025 15:23:47 -0800 Subject: [PATCH 3/3] Handle keys properly --- internal/collections/ordered_map.go | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/internal/collections/ordered_map.go b/internal/collections/ordered_map.go index 3ac3abe726..29f8af1335 100644 --- a/internal/collections/ordered_map.go +++ b/internal/collections/ordered_map.go @@ -2,6 +2,7 @@ package collections import ( "bytes" + "encoding" "encoding/json" "errors" "fmt" @@ -9,6 +10,7 @@ import ( "maps" "reflect" "slices" + "strconv" json2 "github.com/go-json-experiment/json" "github.com/go-json-experiment/json/jsontext" @@ -203,7 +205,12 @@ func (m OrderedMap[K, V]) MarshalJSON() ([]byte, error) { buf.WriteByte(',') } - if err := enc.Encode(k); err != nil { + keyString, err := resolveKeyName(reflect.ValueOf(k)) + if err != nil { + return nil, err + } + + if err := enc.Encode(keyString); err != nil { return nil, err } @@ -217,6 +224,26 @@ func (m OrderedMap[K, V]) MarshalJSON() ([]byte, error) { return buf.Bytes(), nil } +func resolveKeyName(k reflect.Value) (string, error) { + if k.Kind() == reflect.String { + return k.String(), nil + } + if tm, ok := k.Interface().(encoding.TextMarshaler); ok { + if k.Kind() == reflect.Pointer && k.IsNil() { + return "", nil + } + buf, err := tm.MarshalText() + return string(buf), err + } + switch k.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return strconv.FormatInt(k.Int(), 10), nil + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return strconv.FormatUint(k.Uint(), 10), nil + } + panic("unexpected map key type") +} + func (m *OrderedMap[K, V]) UnmarshalJSON(data []byte) error { if string(data) == "null" { // By convention, to approximate the behavior of Unmarshal itself,