Files
kjol/tools/tsgo/internal/tsoptions/showconfig.go
2026-07-09 16:50:43 -04:00

390 lines
14 KiB
Go

package tsoptions
import (
"reflect"
"github.com/microsoft/typescript-go/internal/collections"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/debug"
"github.com/microsoft/typescript-go/internal/diagnostics"
"github.com/microsoft/typescript-go/internal/tspath"
)
// computeFn wraps a typed getter method so it can be stored in an impliedOption's
// compute field (which has type func(*core.CompilerOptions) any).
func computeFn[T any](fn func(*core.CompilerOptions) T) func(*core.CompilerOptions) any {
return func(opts *core.CompilerOptions) any {
return fn(opts)
}
}
// impliedOption describes a compiler option whose effective value can be derived from
// other options. This mirrors TypeScript's computedOptions concept used in convertToTSConfig.
type impliedOption struct {
// name is the Go struct field name of the CompilerOptions field (e.g., "Module").
name string
// dependencies lists the Go struct field names that this option depends on.
dependencies []string
// compute returns the effective value of this option given compiler options.
compute func(opts *core.CompilerOptions) any
}
// impliedOptions lists the compiler options that may be implied by other options,
// mirroring TypeScript's computedOptions used in convertToTSConfig.
// Each compute function delegates directly to an existing core.CompilerOptions getter.
var impliedOptions = []impliedOption{
{name: "Module", dependencies: []string{"Target"}, compute: computeFn((*core.CompilerOptions).GetEmitModuleKind)},
{name: "ModuleResolution", dependencies: []string{"Module", "Target"}, compute: computeFn((*core.CompilerOptions).GetModuleResolutionKind)},
{name: "ModuleDetection", dependencies: []string{"Module", "Target"}, compute: computeFn((*core.CompilerOptions).GetEmitModuleDetectionKind)},
{name: "IsolatedModules", dependencies: []string{"VerbatimModuleSyntax"}, compute: computeFn((*core.CompilerOptions).GetIsolatedModules)},
{name: "PreserveConstEnums", dependencies: []string{"IsolatedModules", "VerbatimModuleSyntax"}, compute: computeFn((*core.CompilerOptions).ShouldPreserveConstEnums)},
{name: "Declaration", dependencies: []string{"Composite"}, compute: computeFn((*core.CompilerOptions).GetEmitDeclarations)},
{name: "DeclarationMap", dependencies: []string{"Declaration", "Composite"}, compute: computeFn((*core.CompilerOptions).GetAreDeclarationMapsEnabled)},
{name: "Incremental", dependencies: []string{"Composite"}, compute: computeFn((*core.CompilerOptions).IsIncremental)},
{name: "UseDefineForClassFields", dependencies: []string{"Target", "Module"}, compute: computeFn((*core.CompilerOptions).GetUseDefineForClassFields)},
{name: "ResolvePackageJsonExports", dependencies: []string{"ModuleResolution", "Module", "Target"}, compute: computeFn((*core.CompilerOptions).GetResolvePackageJsonExports)},
{name: "ResolvePackageJsonImports", dependencies: []string{"ModuleResolution", "ResolvePackageJsonExports", "Module", "Target"}, compute: computeFn((*core.CompilerOptions).GetResolvePackageJsonImports)},
{name: "ResolveJsonModule", dependencies: []string{"ModuleResolution", "Module", "Target"}, compute: computeFn((*core.CompilerOptions).GetResolveJsonModule)},
{name: "AllowJs", dependencies: []string{"CheckJs"}, compute: computeFn((*core.CompilerOptions).GetAllowJS)},
{name: "AllowImportingTsExtensions", dependencies: []string{"RewriteRelativeImportExtensions"}, compute: computeFn((*core.CompilerOptions).GetAllowImportingTsExtensions)},
}
// TSConfig represents the output structure for --showConfig
type TSConfig struct {
CompilerOptions *collections.OrderedMap[string, any] `json:"compilerOptions"`
References []any `json:"references,omitzero"`
Files []string `json:"files,omitzero"`
Include []string `json:"include,omitzero"`
Exclude []string `json:"exclude,omitzero"`
CompileOnSave *bool `json:"compileOnSave,omitzero"`
}
// ConvertToTSConfig generates a complete tsconfig representation for --showConfig output,
// matching the behavior of TypeScript's convertToTSConfig function.
func ConvertToTSConfig(configParseResult *ParsedCommandLine, configFileName string) *TSConfig {
if configFileName == "" {
configFileName = "tsconfig.json"
}
normalizedConfigPath := tspath.GetNormalizedAbsolutePath(configFileName, configParseResult.GetCurrentDirectory())
comparePathsOptions := tspath.ComparePathsOptions{
CurrentDirectory: configParseResult.GetCurrentDirectory(),
UseCaseSensitiveFileNames: configParseResult.UseCaseSensitiveFileNames(),
}
// Build the list of all resolved files as relative paths from the config file.
var files []string
for _, f := range configParseResult.FileNames() {
normalizedFilePath := tspath.GetNormalizedAbsolutePath(f, configParseResult.GetCurrentDirectory())
relativePath := tspath.GetRelativePathFromFile(normalizedConfigPath, normalizedFilePath, comparePathsOptions)
files = append(files, relativePath)
}
// Serialize compiler options
optionMap := serializeCompilerOptions(configParseResult.CompilerOptions(), normalizedConfigPath, comparePathsOptions)
// Remove command-line-only options from the output
for _, name := range []string{
"showConfig", "configFile", "configFilePath", "help", "init",
"listFilesOnly", "listEmittedFiles", "project", "build", "version",
} {
optionMap.Delete(name)
}
// Add implied compiler options (options that are derived from explicitly set options,
// such as moduleResolution implied by module, or useDefineForClassFields implied by target).
// This mirrors TypeScript's convertToTSConfig computedOptions logic.
addImpliedOptions(optionMap, configParseResult.CompilerOptions(), normalizedConfigPath, comparePathsOptions)
config := &TSConfig{
CompilerOptions: optionMap,
}
// Add references
if refs := configParseResult.ProjectReferences(); len(refs) > 0 {
var references []any
for _, r := range refs {
ref := &collections.OrderedMap[string, any]{}
ref.Set("path", r.OriginalPath)
if r.Circular {
ref.Set("circular", true)
}
references = append(references, ref)
}
config.References = references
}
// Add files
if len(files) > 0 {
config.Files = files
}
// Add include/exclude from configFileSpecs
if configParseResult.ConfigFile != nil && configParseResult.ConfigFile.configFileSpecs != nil {
specs := configParseResult.ConfigFile.configFileSpecs
include := filterSameAsDefaultInclude(specs.validatedIncludeSpecs)
if len(include) > 0 {
config.Include = include
}
config.Exclude = specs.validatedExcludeSpecs
}
// Add compileOnSave
if configParseResult.CompileOnSave != nil && *configParseResult.CompileOnSave {
t := true
config.CompileOnSave = &t
}
return config
}
// filterSameAsDefaultInclude returns nil if specs is the default include spec ["**/*"]
func filterSameAsDefaultInclude(specs []string) []string {
if len(specs) == 0 {
return nil
}
if len(specs) == 1 && specs[0] == defaultIncludeSpec {
return nil
}
return specs
}
// getNameOfCompilerOptionValue returns the string key for a given enum value by
// searching the option's enum map.
func getNameOfCompilerOptionValue(value any, enumMap *collections.OrderedMap[string, any]) string {
for k, v := range enumMap.Entries() {
if v == value {
return k
}
}
return ""
}
// serializeCompilerOptions converts CompilerOptions to an ordered map with
// string names as keys and serialized values (enums as strings, paths as
// relative paths, etc.) matching the output of tsc --showConfig.
func serializeCompilerOptions(options *core.CompilerOptions, configFilePath string, comparePathsOptions tspath.ComparePathsOptions) *collections.OrderedMap[string, any] {
result := collections.NewOrderedMapWithSizeHint[string, any](32)
configDir := tspath.GetDirectoryPath(configFilePath)
optionsValue := reflect.ValueOf(options).Elem()
optionsTypeInfo := reflect.TypeFor[core.CompilerOptions]()
for i := range optionsValue.NumField() {
field := optionsTypeInfo.Field(i)
if !field.IsExported() {
continue
}
optionDecl := CommandLineCompilerOptionsMap.Get(field.Name)
if optionDecl == nil {
continue
}
// Skip command-line-only and output formatting options
if optionDecl.Category == diagnostics.Command_line_Options || optionDecl.Category == diagnostics.Output_Formatting {
continue
}
fieldValue := optionsValue.Field(i)
// Skip zero values (unset options)
if fieldValue.IsZero() {
continue
}
name := optionDecl.Name
value := fieldValue.Interface()
enumMap := optionDecl.EnumMap()
if enumMap != nil {
// Enum option - convert numeric value to string name
serialized := serializeEnumValue(value, enumMap)
if serialized != "" {
result.Set(name, serialized)
}
continue
}
switch optionDecl.Kind {
case CommandLineOptionTypeListOrElement:
debug.Assert(false, "listOrElement option should not reach serialization")
case CommandLineOptionTypeList:
elem := optionDecl.Elements()
if elem != nil && elem.IsFilePath {
// List of file paths - make relative
if strs, ok := value.([]string); ok {
relPaths := make([]string, len(strs))
for j, s := range strs {
absPath := tspath.GetNormalizedAbsolutePath(s, configDir)
relPaths[j] = tspath.GetRelativePathFromFile(configFilePath, absPath, comparePathsOptions)
}
result.Set(name, relPaths)
continue
}
}
if elem != nil && elem.EnumMap() != nil {
// List of enum values (e.g., lib)
elemMap := elem.EnumMap()
if strs, ok := value.([]string); ok {
serialized := make([]string, 0, len(strs))
for _, s := range strs {
// lib values are already stored as the d.ts filename, need to find original key
found := getNameOfCompilerOptionValue(s, elemMap)
if found != "" {
serialized = append(serialized, found)
} else {
serialized = append(serialized, s)
}
}
result.Set(name, serialized)
continue
}
}
result.Set(name, value)
case CommandLineOptionTypeString:
if optionDecl.IsFilePath {
// File path option - make relative to config
if s, ok := value.(string); ok && s != "" {
absPath := tspath.GetNormalizedAbsolutePath(s, configDir)
result.Set(name, tspath.GetRelativePathFromFile(configFilePath, absPath, comparePathsOptions))
continue
}
}
result.Set(name, value)
case CommandLineOptionTypeBoolean:
if t, ok := value.(core.Tristate); ok {
if t.IsTrue() {
result.Set(name, true)
} else if t.IsFalse() {
result.Set(name, false)
}
} else {
result.Set(name, value)
}
case CommandLineOptionTypeNumber:
result.Set(name, value)
default:
result.Set(name, value)
}
}
return result
}
// serializeEnumValue converts an enum field value to its corresponding string key
// using the option's enum map. It handles int32-based enum types.
func serializeEnumValue(value any, enumMap *collections.OrderedMap[string, any]) string {
// The enum maps store values as core.ModuleKind, core.ScriptTarget, etc.
// But those are all int32 underneath. We need to compare by the underlying int32 value.
rv := reflect.ValueOf(value)
if rv.CanInt() {
intVal := rv.Int()
for k, v := range enumMap.Entries() {
ev := reflect.ValueOf(v)
if ev.CanInt() && ev.Int() == intVal {
return k
}
}
}
// Fallback: direct comparison
return getNameOfCompilerOptionValue(value, enumMap)
}
// addImpliedOptions adds compiler options that are implied by other explicitly-set options,
// mirroring TypeScript's convertToTSConfig behavior for computedOptions.
// For example, when module: nodenext is set, moduleResolution: nodenext is implied.
func addImpliedOptions(
optionMap *collections.OrderedMap[string, any],
options *core.CompilerOptions,
_ string,
_ tspath.ComparePathsOptions,
) {
// Build the set of explicitly provided option JSON names (e.g., "module", "target").
provided := make(map[string]bool, optionMap.Size())
for k := range optionMap.Keys() {
provided[k] = true
}
defaultOpts := &core.CompilerOptions{}
for _, entry := range impliedOptions {
// Get the option declaration for this implied option (using case-insensitive lookup).
optionDecl := CommandLineCompilerOptionsMap.Get(entry.name)
if optionDecl == nil {
continue
}
// Skip if this option is already explicitly provided.
if provided[optionDecl.Name] {
continue
}
// Check if any direct dependency is in the provided set.
// This mirrors TypeScript's optionDependsOn check.
if !anyDependencyProvided(entry.dependencies, provided) {
continue
}
// Compute the effective value with current options and the default value with empty options.
implied := entry.compute(options)
defaultVal := entry.compute(defaultOpts)
// If the implied value equals the default, this option doesn't add useful information.
if reflect.DeepEqual(implied, defaultVal) {
continue
}
// Serialize the implied value and add it to the option map.
serialized := serializeImpliedOptionValue(optionDecl, implied)
if serialized == nil {
continue
}
optionMap.Set(optionDecl.Name, serialized)
}
}
// anyDependencyProvided returns true if any of the given dependency names
// (using Go field names like "Target") corresponds to an option in the provided set.
func anyDependencyProvided(dependencies []string, provided map[string]bool) bool {
for _, dep := range dependencies {
depDecl := CommandLineCompilerOptionsMap.Get(dep)
if depDecl != nil && provided[depDecl.Name] {
return true
}
}
return false
}
// serializeImpliedOptionValue converts a computed implied option value to its serializable form.
// For enum options, it converts numeric values to their string names.
// For boolean options, it returns the bool directly.
func serializeImpliedOptionValue(optionDecl *CommandLineOption, value any) any {
if value == nil {
return nil
}
enumMap := optionDecl.EnumMap()
if enumMap != nil {
s := serializeEnumValue(value, enumMap)
if s != "" {
return s
}
return nil
}
switch v := value.(type) {
case bool:
return v
case core.Tristate:
if v.IsTrue() {
return true
} else if v.IsFalse() {
return false
}
return nil
}
return value
}