diff --git a/internal/compiler/host.go b/internal/compiler/host.go index 68f3cf620a..5d523a9ab3 100644 --- a/internal/compiler/host.go +++ b/internal/compiler/host.go @@ -83,6 +83,6 @@ func (h *compilerHost) GetSourceFile(opts ast.SourceFileParseOptions) *ast.Sourc } func (h *compilerHost) GetResolvedProjectReference(fileName string, path tspath.Path) *tsoptions.ParsedCommandLine { - commandLine, _ := tsoptions.GetParsedCommandLineOfConfigFilePath(fileName, path, nil, h, h.extendedConfigCache) + commandLine, _ := tsoptions.GetParsedCommandLineOfConfigFilePath(fileName, path, nil, nil /*optionsRaw*/, h, h.extendedConfigCache) return commandLine } diff --git a/internal/execute/build/host.go b/internal/execute/build/host.go index 91f50aa59c..6c21169155 100644 --- a/internal/execute/build/host.go +++ b/internal/execute/build/host.go @@ -59,7 +59,7 @@ func (h *host) GetSourceFile(opts ast.SourceFileParseOptions) *ast.SourceFile { func (h *host) GetResolvedProjectReference(fileName string, path tspath.Path) *tsoptions.ParsedCommandLine { return h.resolvedReferences.loadOrStoreNew(path, func(path tspath.Path) *tsoptions.ParsedCommandLine { configStart := h.orchestrator.opts.Sys.Now() - commandLine, _ := tsoptions.GetParsedCommandLineOfConfigFilePath(fileName, path, h.orchestrator.opts.Command.CompilerOptions, h, &h.extendedConfigCache) + commandLine, _ := tsoptions.GetParsedCommandLineOfConfigFilePath(fileName, path, h.orchestrator.opts.Command.CompilerOptions, nil /*optionsRaw*/, h, &h.extendedConfigCache) configTime := h.orchestrator.opts.Sys.Now().Sub(configStart) h.configTimes.Store(path, configTime) return commandLine diff --git a/internal/execute/tsc.go b/internal/execute/tsc.go index 65bf8d43bb..d4b40f0435 100644 --- a/internal/execute/tsc.go +++ b/internal/execute/tsc.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/microsoft/typescript-go/internal/ast" + "github.com/microsoft/typescript-go/internal/collections" "github.com/microsoft/typescript-go/internal/compiler" "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/diagnostics" @@ -173,7 +174,11 @@ func tscCompilation(sys tsc.System, commandLine *tsoptions.ParsedCommandLine, te var compileTimes tsc.CompileTimes if configFileName != "" { configStart := sys.Now() - configParseResult, errors := tsoptions.GetParsedCommandLineOfConfigFile(configFileName, compilerOptionsFromCommandLine, sys, extendedConfigCache) + var commandLineRaw *collections.OrderedMap[string, any] + if raw, ok := commandLine.Raw.(*collections.OrderedMap[string, any]); ok { + commandLineRaw = raw + } + configParseResult, errors := tsoptions.GetParsedCommandLineOfConfigFileWithRaw(configFileName, compilerOptionsFromCommandLine, commandLineRaw, sys, extendedConfigCache) compileTimes.ConfigTime = sys.Now().Sub(configStart) if len(errors) != 0 { // these are unrecoverable errors--exit to report them as diagnostics diff --git a/internal/project/configfileregistrybuilder.go b/internal/project/configfileregistrybuilder.go index a4b5a7ff6d..9d154a4193 100644 --- a/internal/project/configfileregistrybuilder.go +++ b/internal/project/configfileregistrybuilder.go @@ -106,7 +106,7 @@ func (c *configFileRegistryBuilder) reloadIfNeeded(entry *configFileEntry, fileN entry.commandLine = entry.commandLine.ReloadFileNamesOfParsedCommandLine(c.fs.fs) case PendingReloadFull: logger.Log("Loading config file: " + fileName) - entry.commandLine, _ = tsoptions.GetParsedCommandLineOfConfigFilePath(fileName, path, nil, c, c) + entry.commandLine, _ = tsoptions.GetParsedCommandLineOfConfigFilePath(fileName, path, nil, nil /*optionsRaw*/, c, c) c.updateExtendingConfigs(path, entry.commandLine, entry.commandLine) c.updateRootFilesWatch(fileName, entry) logger.Log("Finished loading config file") diff --git a/internal/tsoptions/commandlineparser_test.go b/internal/tsoptions/commandlineparser_test.go index 9caf562d17..6634d66a4f 100644 --- a/internal/tsoptions/commandlineparser_test.go +++ b/internal/tsoptions/commandlineparser_test.go @@ -8,6 +8,7 @@ import ( "github.com/go-json-experiment/json" "github.com/google/go-cmp/cmp/cmpopts" + "github.com/microsoft/typescript-go/internal/collections" "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/diagnostics" "github.com/microsoft/typescript-go/internal/diagnosticwriter" @@ -86,6 +87,48 @@ func TestCommandLineParseResult(t *testing.T) { } } +func TestCustomConditionsNullOverride(t *testing.T) { + t.Parallel() + + files := map[string]string{ + "/project/tsconfig.json": `{ + "compilerOptions": { + "customConditions": ["condition1", "condition2"] + } +}`, + "/project/index.ts": `console.log("Hello, World!");`, + } + + host := tsoptionstest.NewVFSParseConfigHost(files, "/project", true) + + // Parse command line with --customConditions null + cmdLine := tsoptions.ParseCommandLine([]string{"--project", "/project", "--customConditions", "null"}, host) + + // Check that the raw options contain null for customConditions + if rawMap, ok := cmdLine.Raw.(*collections.OrderedMap[string, any]); ok { + customConditionsRaw, exists := rawMap.Get("customConditions") + assert.Assert(t, exists, "customConditions should exist in raw options") + assert.Assert(t, customConditionsRaw == nil, "customConditions should be nil in raw options, got: %v", customConditionsRaw) + } else { + t.Fatal("Raw options should be an OrderedMap") + } + + // Now parse the config file with the command line options + parsedConfig, errors := tsoptions.GetParsedCommandLineOfConfigFileWithRaw( + "/project/tsconfig.json", + cmdLine.CompilerOptions(), + cmdLine.Raw.(*collections.OrderedMap[string, any]), + host, + nil, + ) + + assert.Assert(t, len(errors) == 0, "Should not have errors: %v", errors) + + // Check that customConditions is nil (overridden by command line) + customConditions := parsedConfig.CompilerOptions().CustomConditions + assert.Assert(t, customConditions == nil, "customConditions should be nil after override, got: %v", customConditions) +} + func TestParseCommandLineVerifyNull(t *testing.T) { t.Parallel() repo.SkipIfNoTypeScriptSubmodule(t) diff --git a/internal/tsoptions/parsinghelpers.go b/internal/tsoptions/parsinghelpers.go index bf16518be2..71d7bb8ed0 100644 --- a/internal/tsoptions/parsinghelpers.go +++ b/internal/tsoptions/parsinghelpers.go @@ -544,7 +544,8 @@ func mergeCompilerOptions(targetOptions, sourceOptions *core.CompilerOptions, ra // 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 rawMap, ok := rawSource.(*collections.OrderedMap[string, any]); ok && rawMap != nil { + // For tsconfig.json, options are nested under "compilerOptions" if compilerOptionsRaw, exists := rawMap.Get("compilerOptions"); exists { if compilerOptionsMap, ok := compilerOptionsRaw.(*collections.OrderedMap[string, any]); ok { for key, value := range compilerOptionsMap.Entries() { @@ -553,6 +554,13 @@ func mergeCompilerOptions(targetOptions, sourceOptions *core.CompilerOptions, ra } } } + } else { + // For command line options, the map IS the options directly + for key, value := range rawMap.Entries() { + if value == nil { + explicitNullFields.Add(key) + } + } } } } diff --git a/internal/tsoptions/tsconfigparsing.go b/internal/tsoptions/tsconfigparsing.go index 9ccef5af8d..fb5a3c2598 100644 --- a/internal/tsoptions/tsconfigparsing.go +++ b/internal/tsoptions/tsconfigparsing.go @@ -691,7 +691,24 @@ func ParseJsonSourceFileConfigFileContent( extendedConfigCache ExtendedConfigCache, ) *ParsedCommandLine { // tracing?.push(tracing.Phase.Parse, "parseJsonSourceFileConfigFileContent", { path: sourceFile.fileName }); - result := parseJsonConfigFileContentWorker(nil /*json*/, sourceFile, host, basePath, existingOptions, configFileName, resolutionStack, extraFileExtensions, extendedConfigCache) + result := parseJsonConfigFileContentWorker(nil /*json*/, sourceFile, host, basePath, existingOptions, nil /*existingOptionsRaw*/, configFileName, resolutionStack, extraFileExtensions, extendedConfigCache) + // tracing?.pop(); + return result +} + +func ParseJsonSourceFileConfigFileContentWithRaw( + sourceFile *TsConfigSourceFile, + host ParseConfigHost, + basePath string, + existingOptions *core.CompilerOptions, + existingOptionsRaw *collections.OrderedMap[string, any], + 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, existingOptionsRaw, configFileName, resolutionStack, extraFileExtensions, extendedConfigCache) // tracing?.pop(); return result } @@ -829,7 +846,7 @@ func convertPropertyValueToJson(sourceFile *ast.SourceFile, valueExpression *ast // 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) + result := parseJsonConfigFileContentWorker(parseJsonToStringKey(json), nil /*sourceFile*/, host, basePath, existingOptions, nil /*existingOptionsRaw*/, configFileName, resolutionStack, extraFileExtensions, extendedConfigCache) return result } @@ -994,11 +1011,11 @@ func parseConfig( var result *parsedTsconfig errors = append(errors, ast.NewCompilerDiagnostic(diagnostics.Circularity_detected_while_resolving_configuration_Colon_0)) if json.Size() == 0 { - result = &parsedTsconfig{raw: json} + result = &parsedTsconfig{raw: json, options: &core.CompilerOptions{}} } else { rawResult, err := convertToObject(sourceFile.SourceFile) errors = append(errors, err...) - result = &parsedTsconfig{raw: rawResult} + result = &parsedTsconfig{raw: rawResult, options: &core.CompilerOptions{}} } return result, errors } @@ -1127,6 +1144,7 @@ func parseJsonConfigFileContentWorker( host ParseConfigHost, basePath string, existingOptions *core.CompilerOptions, + existingOptionsRaw *collections.OrderedMap[string, any], configFileName string, resolutionStack []tspath.Path, extraFileExtensions []FileExtensionInfo, @@ -1144,7 +1162,7 @@ func parseJsonConfigFileContentWorker( var errors []*ast.Diagnostic resolutionStackString := []string{} parsedConfig, errors := parseConfig(json, sourceFile, host, basePath, configFileName, resolutionStackString, extendedConfigCache) - mergeCompilerOptions(parsedConfig.options, existingOptions, nil) + mergeCompilerOptions(parsedConfig.options, existingOptions, existingOptionsRaw) handleOptionConfigDirTemplateSubstitution(parsedConfig.options, basePathForFileNames) rawConfig := parseJsonToStringKey(parsedConfig.raw) if configFileName != "" && parsedConfig.options != nil { @@ -1743,13 +1761,25 @@ func GetParsedCommandLineOfConfigFile( 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) + return GetParsedCommandLineOfConfigFilePath(configFileName, tspath.ToPath(configFileName, sys.GetCurrentDirectory(), sys.FS().UseCaseSensitiveFileNames()), options, nil /*optionsRaw*/, sys, extendedConfigCache) +} + +func GetParsedCommandLineOfConfigFileWithRaw( + configFileName string, + options *core.CompilerOptions, + optionsRaw *collections.OrderedMap[string, any], + 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, optionsRaw, sys, extendedConfigCache) } func GetParsedCommandLineOfConfigFilePath( configFileName string, path tspath.Path, options *core.CompilerOptions, + optionsRaw *collections.OrderedMap[string, any], sys ParseConfigHost, extendedConfigCache ExtendedConfigCache, ) (*ParsedCommandLine, []*ast.Diagnostic) { @@ -1763,6 +1793,19 @@ func GetParsedCommandLineOfConfigFilePath( tsConfigSourceFile := NewTsconfigSourceFileFromFilePath(configFileName, path, configFileText) // tsConfigSourceFile.resolvedPath = tsConfigSourceFile.FileName() // tsConfigSourceFile.originalFileName = tsConfigSourceFile.FileName() + if optionsRaw != nil { + return ParseJsonSourceFileConfigFileContentWithRaw( + tsConfigSourceFile, + sys, + tspath.GetDirectoryPath(configFileName), + options, + optionsRaw, + configFileName, + nil, + nil, + extendedConfigCache, + ), nil + } return ParseJsonSourceFileConfigFileContent( tsConfigSourceFile, sys, diff --git a/testdata/baselines/reference/tsc/composite/when-setting-composite-null-on-command-line.js b/testdata/baselines/reference/tsc/composite/when-setting-composite-null-on-command-line.js index 285f20672a..c16f2e1dd8 100644 --- a/testdata/baselines/reference/tsc/composite/when-setting-composite-null-on-command-line.js +++ b/testdata/baselines/reference/tsc/composite/when-setting-composite-null-on-command-line.js @@ -41,69 +41,10 @@ interface Symbol { readonly [Symbol.toStringTag]: string; } declare const console: { log(msg: any): void; }; -//// [/home/src/workspaces/project/src/main.d.ts] *new* -export declare const x = 10; - //// [/home/src/workspaces/project/src/main.js] *new* "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.x = void 0; exports.x = 10; -//// [/home/src/workspaces/project/tsconfig.tsbuildinfo] *new* -{"version":"FakeTSVersion","root":[2],"fileNames":["lib.d.ts","./src/main.ts"],"fileInfos":[{"version":"8859c12c614ce56ba9a18e58384a198f-/// \ninterface Boolean {}\ninterface Function {}\ninterface CallableFunction {}\ninterface NewableFunction {}\ninterface IArguments {}\ninterface Number { toExponential: any; }\ninterface Object {}\ninterface RegExp {}\ninterface String { charAt: any; }\ninterface Array { length: number; [n: number]: T; }\ninterface ReadonlyArray {}\ninterface SymbolConstructor {\n (desc?: string | number): symbol;\n for(name: string): symbol;\n readonly toStringTag: symbol;\n}\ndeclare var Symbol: SymbolConstructor;\ninterface Symbol {\n readonly [Symbol.toStringTag]: string;\n}\ndeclare const console: { log(msg: any): void; };","affectsGlobalScope":true,"impliedNodeFormat":1},{"version":"28e8748a7acd58f4f59388926e914f86-export const x = 10;","signature":"f9b4154a9a5944099ecf197d4519d083-export declare const x = 10;\n","impliedNodeFormat":1}],"options":{"composite":true,"module":1,"target":1},"latestChangedDtsFile":"./src/main.d.ts"} -//// [/home/src/workspaces/project/tsconfig.tsbuildinfo.readable.baseline.txt] *new* -{ - "version": "FakeTSVersion", - "root": [ - { - "files": [ - "./src/main.ts" - ], - "original": 2 - } - ], - "fileNames": [ - "lib.d.ts", - "./src/main.ts" - ], - "fileInfos": [ - { - "fileName": "lib.d.ts", - "version": "8859c12c614ce56ba9a18e58384a198f-/// \ninterface Boolean {}\ninterface Function {}\ninterface CallableFunction {}\ninterface NewableFunction {}\ninterface IArguments {}\ninterface Number { toExponential: any; }\ninterface Object {}\ninterface RegExp {}\ninterface String { charAt: any; }\ninterface Array { length: number; [n: number]: T; }\ninterface ReadonlyArray {}\ninterface SymbolConstructor {\n (desc?: string | number): symbol;\n for(name: string): symbol;\n readonly toStringTag: symbol;\n}\ndeclare var Symbol: SymbolConstructor;\ninterface Symbol {\n readonly [Symbol.toStringTag]: string;\n}\ndeclare const console: { log(msg: any): void; };", - "signature": "8859c12c614ce56ba9a18e58384a198f-/// \ninterface Boolean {}\ninterface Function {}\ninterface CallableFunction {}\ninterface NewableFunction {}\ninterface IArguments {}\ninterface Number { toExponential: any; }\ninterface Object {}\ninterface RegExp {}\ninterface String { charAt: any; }\ninterface Array { length: number; [n: number]: T; }\ninterface ReadonlyArray {}\ninterface SymbolConstructor {\n (desc?: string | number): symbol;\n for(name: string): symbol;\n readonly toStringTag: symbol;\n}\ndeclare var Symbol: SymbolConstructor;\ninterface Symbol {\n readonly [Symbol.toStringTag]: string;\n}\ndeclare const console: { log(msg: any): void; };", - "affectsGlobalScope": true, - "impliedNodeFormat": "CommonJS", - "original": { - "version": "8859c12c614ce56ba9a18e58384a198f-/// \ninterface Boolean {}\ninterface Function {}\ninterface CallableFunction {}\ninterface NewableFunction {}\ninterface IArguments {}\ninterface Number { toExponential: any; }\ninterface Object {}\ninterface RegExp {}\ninterface String { charAt: any; }\ninterface Array { length: number; [n: number]: T; }\ninterface ReadonlyArray {}\ninterface SymbolConstructor {\n (desc?: string | number): symbol;\n for(name: string): symbol;\n readonly toStringTag: symbol;\n}\ndeclare var Symbol: SymbolConstructor;\ninterface Symbol {\n readonly [Symbol.toStringTag]: string;\n}\ndeclare const console: { log(msg: any): void; };", - "affectsGlobalScope": true, - "impliedNodeFormat": 1 - } - }, - { - "fileName": "./src/main.ts", - "version": "28e8748a7acd58f4f59388926e914f86-export const x = 10;", - "signature": "f9b4154a9a5944099ecf197d4519d083-export declare const x = 10;\n", - "impliedNodeFormat": "CommonJS", - "original": { - "version": "28e8748a7acd58f4f59388926e914f86-export const x = 10;", - "signature": "f9b4154a9a5944099ecf197d4519d083-export declare const x = 10;\n", - "impliedNodeFormat": 1 - } - } - ], - "options": { - "composite": true, - "module": 1, - "target": 1 - }, - "latestChangedDtsFile": "./src/main.d.ts", - "size": 1123 -} -tsconfig.json:: -SemanticDiagnostics:: -*refresh* /home/src/tslibs/TS/Lib/lib.d.ts -*refresh* /home/src/workspaces/project/src/main.ts -Signatures:: -(stored at emit) /home/src/workspaces/project/src/main.ts