Skip to content

Commit 8fbfa3e

Browse files
Copilotjakebailey
andauthored
Fix tsconfig null override for extended configurations (#1313)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: jakebailey <5341706+jakebailey@users.noreply.github.com>
1 parent dbe69bd commit 8fbfa3e

13 files changed

+622
-9
lines changed

internal/tsoptions/parsinghelpers.go

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package tsoptions
22

33
import (
44
"reflect"
5+
"strings"
56

67
"github.com/microsoft/typescript-go/internal/ast"
78
"github.com/microsoft/typescript-go/internal/collections"
@@ -488,25 +489,54 @@ func ParseTypeAcquisition(key string, value any, allOptions *core.TypeAcquisitio
488489
return nil
489490
}
490491

491-
// mergeCompilerOptions merges the source compiler options into the target compiler options.
492-
// Fields in the source options will overwrite the corresponding fields in the target options.
493-
func mergeCompilerOptions(targetOptions, sourceOptions *core.CompilerOptions) *core.CompilerOptions {
492+
// mergeCompilerOptions merges the source compiler options into the target compiler options
493+
// with optional awareness of explicitly set null values in the raw JSON.
494+
// Fields in the source options will overwrite the corresponding fields in the target options,
495+
// including when they are explicitly set to null in the raw configuration (if rawSource is provided).
496+
func mergeCompilerOptions(targetOptions, sourceOptions *core.CompilerOptions, rawSource any) *core.CompilerOptions {
494497
if sourceOptions == nil {
495498
return targetOptions
496499
}
497500

501+
// Collect explicitly null field names from raw JSON
502+
var explicitNullFields collections.Set[string]
503+
if rawSource != nil {
504+
if rawMap, ok := rawSource.(*collections.OrderedMap[string, any]); ok {
505+
if compilerOptionsRaw, exists := rawMap.Get("compilerOptions"); exists {
506+
if compilerOptionsMap, ok := compilerOptionsRaw.(*collections.OrderedMap[string, any]); ok {
507+
for key, value := range compilerOptionsMap.Entries() {
508+
if value == nil {
509+
explicitNullFields.Add(key)
510+
}
511+
}
512+
}
513+
}
514+
}
515+
}
516+
517+
// Do the merge, handling explicit nulls during the normal merge
498518
targetValue := reflect.ValueOf(targetOptions).Elem()
499519
sourceValue := reflect.ValueOf(sourceOptions).Elem()
520+
targetType := targetValue.Type()
500521

501522
for i := range targetValue.NumField() {
502523
targetField := targetValue.Field(i)
503524
sourceField := sourceValue.Field(i)
504-
if sourceField.IsZero() {
505-
continue
506-
} else {
525+
526+
// Get the JSON field name for this struct field and check if it's explicitly null
527+
if jsonTag := targetType.Field(i).Tag.Get("json"); jsonTag != "" {
528+
if jsonFieldName, _, _ := strings.Cut(jsonTag, ","); jsonFieldName != "" && explicitNullFields.Has(jsonFieldName) {
529+
targetField.SetZero()
530+
continue
531+
}
532+
}
533+
534+
// Normal merge behavior: copy non-zero fields
535+
if !sourceField.IsZero() {
507536
targetField.Set(sourceField)
508537
}
509538
}
539+
510540
return targetOptions
511541
}
512542

internal/tsoptions/tsconfigparsing.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1040,7 +1040,7 @@ func parseConfig(
10401040
result.compileOnSave = compileOnSave
10411041
}
10421042
}
1043-
mergeCompilerOptions(result.options, extendedConfig.options)
1043+
mergeCompilerOptions(result.options, extendedConfig.options, extendsRaw)
10441044
}
10451045
}
10461046

@@ -1074,7 +1074,7 @@ func parseConfig(
10741074
sourceFile.ExtendedSourceFiles = append(sourceFile.ExtendedSourceFiles, extendedSourceFile)
10751075
}
10761076
}
1077-
ownConfig.options = mergeCompilerOptions(result.options, ownConfig.options)
1077+
ownConfig.options = mergeCompilerOptions(result.options, ownConfig.options, ownConfig.raw)
10781078
// ownConfig.watchOptions = ownConfig.watchOptions && result.watchOptions ?
10791079
// assignWatchOptions(result, ownConfig.watchOptions) :
10801080
// ownConfig.watchOptions || result.watchOptions;
@@ -1118,7 +1118,7 @@ func parseJsonConfigFileContentWorker(
11181118
var errors []*ast.Diagnostic
11191119
resolutionStackString := []string{}
11201120
parsedConfig, errors := parseConfig(json, sourceFile, host, basePath, configFileName, resolutionStackString, extendedConfigCache)
1121-
mergeCompilerOptions(parsedConfig.options, existingOptions)
1121+
mergeCompilerOptions(parsedConfig.options, existingOptions, nil)
11221122
handleOptionConfigDirTemplateSubstitution(parsedConfig.options, basePathForFileNames)
11231123
rawConfig := parseJsonToStringKey(parsedConfig.raw)
11241124
if configFileName != "" && parsedConfig.options != nil {

internal/tsoptions/tsconfigparsing_test.go

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -583,6 +583,155 @@ export {}`,
583583
},
584584
}},
585585
},
586+
{
587+
title: "null overrides in extended tsconfig - array fields",
588+
noSubmoduleBaseline: true,
589+
input: []testConfig{{
590+
jsonText: `{
591+
"extends": "./tsconfig-base.json",
592+
"compilerOptions": {
593+
"types": null,
594+
"lib": null,
595+
"typeRoots": null
596+
}
597+
}`,
598+
configFileName: "tsconfig.json",
599+
basePath: "/",
600+
allFileList: map[string]string{
601+
"/tsconfig-base.json": `{
602+
"compilerOptions": {
603+
"types": ["node", "@types/jest"],
604+
"lib": ["es2020", "dom"],
605+
"typeRoots": ["./types", "./node_modules/@types"]
606+
}
607+
}`,
608+
"/app.ts": "",
609+
},
610+
}},
611+
},
612+
{
613+
title: "null overrides in extended tsconfig - string fields",
614+
noSubmoduleBaseline: true,
615+
input: []testConfig{{
616+
jsonText: `{
617+
"extends": "./tsconfig-base.json",
618+
"compilerOptions": {
619+
"outDir": null,
620+
"baseUrl": null,
621+
"rootDir": null
622+
}
623+
}`,
624+
configFileName: "tsconfig.json",
625+
basePath: "/",
626+
allFileList: map[string]string{
627+
"/tsconfig-base.json": `{
628+
"compilerOptions": {
629+
"outDir": "./dist",
630+
"baseUrl": "./src",
631+
"rootDir": "./src"
632+
}
633+
}`,
634+
"/app.ts": "",
635+
},
636+
}},
637+
},
638+
{
639+
title: "null overrides in extended tsconfig - mixed field types",
640+
noSubmoduleBaseline: true,
641+
input: []testConfig{{
642+
jsonText: `{
643+
"extends": "./tsconfig-base.json",
644+
"compilerOptions": {
645+
"types": null,
646+
"outDir": null,
647+
"strict": false,
648+
"lib": ["es2022"],
649+
"allowJs": null
650+
}
651+
}`,
652+
configFileName: "tsconfig.json",
653+
basePath: "/",
654+
allFileList: map[string]string{
655+
"/tsconfig-base.json": `{
656+
"compilerOptions": {
657+
"types": ["node"],
658+
"lib": ["es2020", "dom"],
659+
"outDir": "./dist",
660+
"strict": true,
661+
"allowJs": true,
662+
"target": "es2020"
663+
}
664+
}`,
665+
"/app.ts": "",
666+
},
667+
}},
668+
},
669+
{
670+
title: "null overrides with multiple extends levels",
671+
noSubmoduleBaseline: true,
672+
input: []testConfig{{
673+
jsonText: `{
674+
"extends": "./tsconfig-middle.json",
675+
"compilerOptions": {
676+
"types": null,
677+
"lib": null
678+
}
679+
}`,
680+
configFileName: "tsconfig.json",
681+
basePath: "/",
682+
allFileList: map[string]string{
683+
"/tsconfig-middle.json": `{
684+
"extends": "./tsconfig-base.json",
685+
"compilerOptions": {
686+
"types": ["jest"],
687+
"outDir": "./build"
688+
}
689+
}`,
690+
"/tsconfig-base.json": `{
691+
"compilerOptions": {
692+
"types": ["node"],
693+
"lib": ["es2020"],
694+
"outDir": "./dist",
695+
"strict": true
696+
}
697+
}`,
698+
"/app.ts": "",
699+
},
700+
}},
701+
},
702+
{
703+
title: "null overrides in middle level of extends chain",
704+
noSubmoduleBaseline: true,
705+
input: []testConfig{{
706+
jsonText: `{
707+
"extends": "./tsconfig-middle.json",
708+
"compilerOptions": {
709+
"outDir": "./final"
710+
}
711+
}`,
712+
configFileName: "tsconfig.json",
713+
basePath: "/",
714+
allFileList: map[string]string{
715+
"/tsconfig-middle.json": `{
716+
"extends": "./tsconfig-base.json",
717+
"compilerOptions": {
718+
"types": null,
719+
"lib": null,
720+
"outDir": "./middle"
721+
}
722+
}`,
723+
"/tsconfig-base.json": `{
724+
"compilerOptions": {
725+
"types": ["node"],
726+
"lib": ["es2020"],
727+
"outDir": "./base",
728+
"strict": true
729+
}
730+
}`,
731+
"/app.ts": "",
732+
},
733+
}},
734+
},
586735
}
587736

588737
var tsconfigWithExtends = `{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
Fs::
2+
//// [/app.ts]
3+
4+
5+
//// [/tsconfig-base.json]
6+
{
7+
"compilerOptions": {
8+
"types": ["node", "@types/jest"],
9+
"lib": ["es2020", "dom"],
10+
"typeRoots": ["./types", "./node_modules/@types"]
11+
}
12+
}
13+
14+
//// [/tsconfig.json]
15+
{
16+
"extends": "./tsconfig-base.json",
17+
"compilerOptions": {
18+
"types": null,
19+
"lib": null,
20+
"typeRoots": null
21+
}
22+
}
23+
24+
25+
configFileName:: tsconfig.json
26+
CompilerOptions::
27+
{
28+
"configFilePath": "/tsconfig.json"
29+
}
30+
31+
TypeAcquisition::
32+
{}
33+
34+
FileNames::
35+
/app.ts
36+
Errors::
37+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
Fs::
2+
//// [/app.ts]
3+
4+
5+
//// [/tsconfig-base.json]
6+
{
7+
"compilerOptions": {
8+
"types": ["node", "@types/jest"],
9+
"lib": ["es2020", "dom"],
10+
"typeRoots": ["./types", "./node_modules/@types"]
11+
}
12+
}
13+
14+
//// [/tsconfig.json]
15+
{
16+
"extends": "./tsconfig-base.json",
17+
"compilerOptions": {
18+
"types": null,
19+
"lib": null,
20+
"typeRoots": null
21+
}
22+
}
23+
24+
25+
configFileName:: tsconfig.json
26+
CompilerOptions::
27+
{
28+
"configFilePath": "/tsconfig.json"
29+
}
30+
31+
TypeAcquisition::
32+
{}
33+
34+
FileNames::
35+
/app.ts
36+
Errors::
37+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
Fs::
2+
//// [/app.ts]
3+
4+
5+
//// [/tsconfig-base.json]
6+
{
7+
"compilerOptions": {
8+
"types": ["node"],
9+
"lib": ["es2020", "dom"],
10+
"outDir": "./dist",
11+
"strict": true,
12+
"allowJs": true,
13+
"target": "es2020"
14+
}
15+
}
16+
17+
//// [/tsconfig.json]
18+
{
19+
"extends": "./tsconfig-base.json",
20+
"compilerOptions": {
21+
"types": null,
22+
"outDir": null,
23+
"strict": false,
24+
"lib": ["es2022"],
25+
"allowJs": null
26+
}
27+
}
28+
29+
30+
configFileName:: tsconfig.json
31+
CompilerOptions::
32+
{
33+
"lib": [
34+
"lib.es2022.d.ts"
35+
],
36+
"strict": false,
37+
"target": 7,
38+
"configFilePath": "/tsconfig.json"
39+
}
40+
41+
TypeAcquisition::
42+
{}
43+
44+
FileNames::
45+
/app.ts
46+
Errors::
47+

0 commit comments

Comments
 (0)