Skip to content
Draft
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion internal/compiler/host.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
2 changes: 1 addition & 1 deletion internal/execute/build/host.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 6 additions & 1 deletion internal/execute/tsc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion internal/project/configfileregistrybuilder.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
43 changes: 43 additions & 0 deletions internal/tsoptions/commandlineparser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand Down
13 changes: 12 additions & 1 deletion internal/tsoptions/parsinghelpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -540,11 +540,15 @@ func mergeCompilerOptions(targetOptions, sourceOptions *core.CompilerOptions, ra
if sourceOptions == nil {
return targetOptions
}
if targetOptions == nil {
targetOptions = &core.CompilerOptions{}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot The target should never be nil. It does not make sense to merge into nothing. The caller must be wrong here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit 5a52fe3. Instead of checking for nil in mergeCompilerOptions, I now initialize options to an empty struct when a circularity error is detected in parseConfig, ensuring targetOptions is never nil.


// 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() {
Expand All @@ -553,6 +557,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)
}
}
}
}
}
Expand Down
51 changes: 47 additions & 4 deletions internal/tsoptions/tsconfigparsing.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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,
Expand All @@ -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 {
Expand Down Expand Up @@ -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) {
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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-/// <reference no-default-lib=\"true\"/>\ninterface Boolean {}\ninterface Function {}\ninterface CallableFunction {}\ninterface NewableFunction {}\ninterface IArguments {}\ninterface Number { toExponential: any; }\ninterface Object {}\ninterface RegExp {}\ninterface String { charAt: any; }\ninterface Array<T> { length: number; [n: number]: T; }\ninterface ReadonlyArray<T> {}\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-/// <reference no-default-lib=\"true\"/>\ninterface Boolean {}\ninterface Function {}\ninterface CallableFunction {}\ninterface NewableFunction {}\ninterface IArguments {}\ninterface Number { toExponential: any; }\ninterface Object {}\ninterface RegExp {}\ninterface String { charAt: any; }\ninterface Array<T> { length: number; [n: number]: T; }\ninterface ReadonlyArray<T> {}\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-/// <reference no-default-lib=\"true\"/>\ninterface Boolean {}\ninterface Function {}\ninterface CallableFunction {}\ninterface NewableFunction {}\ninterface IArguments {}\ninterface Number { toExponential: any; }\ninterface Object {}\ninterface RegExp {}\ninterface String { charAt: any; }\ninterface Array<T> { length: number; [n: number]: T; }\ninterface ReadonlyArray<T> {}\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-/// <reference no-default-lib=\"true\"/>\ninterface Boolean {}\ninterface Function {}\ninterface CallableFunction {}\ninterface NewableFunction {}\ninterface IArguments {}\ninterface Number { toExponential: any; }\ninterface Object {}\ninterface RegExp {}\ninterface String { charAt: any; }\ninterface Array<T> { length: number; [n: number]: T; }\ninterface ReadonlyArray<T> {}\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