kittenipc/kitcom/internal/tsgo/tsoptions/parsinghelpers.go
2025-10-15 10:12:44 +03:00

621 lines
19 KiB
Go

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