Skip to content

Commit 167d318

Browse files
committed
Draft of configuration inheritance
1 parent 394dbbf commit 167d318

11 files changed

+374
-14
lines changed

Diff for: Jakefile.js

+1
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ var harnessSources = harnessCoreSources.concat([
155155
"moduleResolution.ts",
156156
"tsconfigParsing.ts",
157157
"commandLineParsing.ts",
158+
"configurationExtension.ts",
158159
"convertCompilerOptionsFromJson.ts",
159160
"convertTypingOptionsFromJson.ts",
160161
"tsserverProjectSystem.ts",

Diff for: src/compiler/commandLineParser.ts

+78-3
Original file line numberDiff line numberDiff line change
@@ -700,12 +700,54 @@ namespace ts {
700700
* @param basePath A root directory to resolve relative path entries in the config
701701
* file to. e.g. outDir
702702
*/
703-
export function parseJsonConfigFileContent(json: any, host: ParseConfigHost, basePath: string, existingOptions: CompilerOptions = {}, configFileName?: string): ParsedCommandLine {
703+
export function parseJsonConfigFileContent(json: any, host: ParseConfigHost, basePath: string, existingOptions: CompilerOptions = {}, configFileName?: string, resolutionStack: Path[] = []): ParsedCommandLine {
704704
const errors: Diagnostic[] = [];
705-
const compilerOptions: CompilerOptions = convertCompilerOptionsFromJsonWorker(json["compilerOptions"], basePath, errors, configFileName);
706-
const options = extend(existingOptions, compilerOptions);
705+
const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames);
706+
const resolvedPath = toPath(configFileName || "", basePath, getCanonicalFileName);
707+
if (resolutionStack.indexOf(resolvedPath) >= 0) {
708+
return {
709+
options: {},
710+
fileNames: [],
711+
typingOptions: {},
712+
raw: json,
713+
errors: [createCompilerDiagnostic(Diagnostics.Circularity_detected_while_resolving_configuration_Colon_0, [...resolutionStack, resolvedPath].join(" -> "))],
714+
wildcardDirectories: {}
715+
};
716+
}
717+
718+
let options: CompilerOptions = convertCompilerOptionsFromJsonWorker(json["compilerOptions"], basePath, errors, configFileName);
707719
const typingOptions: TypingOptions = convertTypingOptionsFromJsonWorker(json["typingOptions"], basePath, errors, configFileName);
708720

721+
if (json["extends"]) {
722+
let [include, exclude, files, baseOptions]: [string[], string[], string[], CompilerOptions] = [undefined, undefined, undefined, {}];
723+
if (typeof json["extends"] === "string") {
724+
[include, exclude, files, baseOptions] = (tryExtendsName(json["extends"]) || [include, exclude, files, baseOptions]);
725+
}
726+
else if (typeof json["extends"] === "object" && json["extends"].length) {
727+
for (const name of json["extends"]) {
728+
const [tempinclude, tempexclude, tempfiles, tempBase]: [string[], string[], string[], CompilerOptions] = (tryExtendsName(name) || [include, exclude, files, baseOptions]);
729+
include = tempinclude || include;
730+
exclude = tempexclude || exclude;
731+
files = tempfiles || files;
732+
baseOptions = assign({}, baseOptions, tempBase);
733+
}
734+
}
735+
else {
736+
errors.push(createCompilerDiagnostic(Diagnostics.Compiler_option_0_requires_a_value_of_type_1, "extends", "string or string[]"));
737+
}
738+
if (include && !json["include"]) {
739+
json["include"] = include;
740+
}
741+
if (exclude && !json["exclude"]) {
742+
json["exclude"] = exclude;
743+
}
744+
if (files && !json["files"]) {
745+
json["files"] = files;
746+
}
747+
options = assign({}, baseOptions, options);
748+
}
749+
750+
options = extend(existingOptions, options);
709751
options.configFilePath = configFileName;
710752

711753
const { fileNames, wildcardDirectories } = getFileNames(errors);
@@ -719,6 +761,39 @@ namespace ts {
719761
wildcardDirectories
720762
};
721763

764+
function tryExtendsName(extendedConfig: string): [string[], string[], string[], CompilerOptions] {
765+
// If the path isn't a rooted or relative path, don't try to resolve it (we reserve the right to special case module-id like paths in the future)
766+
if (!(isRootedDiskPath(extendedConfig) || startsWith(normalizeSlashes(extendedConfig), "./") || startsWith(normalizeSlashes(extendedConfig), "../"))) {
767+
errors.push(createCompilerDiagnostic(Diagnostics.The_path_in_an_extends_options_must_be_relative_or_rooted));
768+
return;
769+
}
770+
let extendedConfigPath = toPath(extendedConfig, basePath, getCanonicalFileName);
771+
if (!host.fileExists(extendedConfigPath) && !endsWith(extendedConfigPath, ".json")) {
772+
extendedConfigPath = `${extendedConfigPath}.json` as Path;
773+
if (!host.fileExists(extendedConfigPath)) {
774+
errors.push(createCompilerDiagnostic(Diagnostics.File_0_does_not_exist, extendedConfig));
775+
return;
776+
}
777+
}
778+
const extendedResult = readConfigFile(extendedConfigPath, path => host.readFile(path));
779+
if (extendedResult.error) {
780+
errors.push(extendedResult.error);
781+
return;
782+
}
783+
const extendedDirname = getDirectoryPath(extendedConfigPath);
784+
const relativeDifference = convertToRelativePath(extendedDirname, basePath, getCanonicalFileName);
785+
const updatePath: (path: string) => string = path => isRootedDiskPath(path) ? path : combinePaths(relativeDifference, path);
786+
// Merge configs (copy the resolution stack so it is never reused between branches in potential diamond-problem scenarios)
787+
const result = parseJsonConfigFileContent(extendedResult.config, host, extendedDirname, /*existingOptions*/undefined, getBaseFileName(extendedConfigPath), resolutionStack.concat([resolvedPath]));
788+
errors.push(...result.errors);
789+
const [include, exclude, files] = map(["include", "exclude", "files"], key => {
790+
if (!json[key] && extendedResult.config[key]) {
791+
return map(extendedResult.config[key], updatePath);
792+
}
793+
});
794+
return [include, exclude, files, result.options];
795+
}
796+
722797
function getFileNames(errors: Diagnostic[]): ExpandResult {
723798
let fileNames: string[];
724799
if (hasProperty(json, "files")) {

Diff for: src/compiler/core.ts

+29-1
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,20 @@ namespace ts {
171171
return result;
172172
}
173173

174+
export function mapObject<T, U>(object: Map<T>, f: (key: string, x: T) => [string, U]): Map<U> {
175+
let result: Map<U> = {};
176+
if (object) {
177+
result = {};
178+
for (const v of getKeys(object)) {
179+
const [key, value]: [string, U] = f(v, object[v]) || [undefined, undefined];
180+
if (key !== undefined) {
181+
result[key] = value;
182+
}
183+
}
184+
}
185+
return result;
186+
}
187+
174188
export function concatenate<T>(array1: T[], array2: T[]): T[] {
175189
if (!array2 || !array2.length) return array1;
176190
if (!array1 || !array1.length) return array2;
@@ -357,6 +371,20 @@ namespace ts {
357371
return result;
358372
}
359373

374+
export function assign<T1 extends Map<{}>, T2, T3>(t: T1, arg1: T2, arg2: T3): T1 & T2 & T3;
375+
export function assign<T1 extends Map<{}>, T2>(t: T1, arg1: T2): T1 & T2;
376+
export function assign<T1 extends Map<{}>>(t: T1, ...args: any[]): any;
377+
export function assign<T1 extends Map<{}>>(t: T1, ...args: any[]) {
378+
for (const arg of args) {
379+
for (const p of getKeys(arg)) {
380+
if (hasProperty(arg, p)) {
381+
t[p] = arg[p];
382+
}
383+
}
384+
}
385+
return t;
386+
}
387+
360388
export function forEachValue<T, U>(map: Map<T>, callback: (value: T) => U): U {
361389
let result: U;
362390
for (const id in map) {
@@ -941,7 +969,7 @@ namespace ts {
941969
* [^./] # matches everything up to the first . character (excluding directory seperators)
942970
* (\\.(?!min\\.js$))? # matches . characters but not if they are part of the .min.js file extension
943971
*/
944-
const singleAsteriskRegexFragmentFiles = "([^./]|(\\.(?!min\\.js$))?)*";
972+
const singleAsteriskRegexFragmentFiles = "([^./]|(\\.(?!min\\.js$))?)*";
945973
const singleAsteriskRegexFragmentOther = "[^/]*";
946974

947975
export function getRegularExpressionForWildcard(specs: string[], basePath: string, usage: "files" | "directories" | "exclude") {

Diff for: src/compiler/diagnosticMessages.json

+9
Original file line numberDiff line numberDiff line change
@@ -3031,5 +3031,14 @@
30313031
"Unknown typing option '{0}'.": {
30323032
"category": "Error",
30333033
"code": 17010
3034+
},
3035+
3036+
"Circularity detected while resolving configuration: {0}": {
3037+
"category": "Error",
3038+
"code": 18000
3039+
},
3040+
"The path in an 'extends' options must be relative or rooted.": {
3041+
"category": "Error",
3042+
"code": 18001
30343043
}
30353044
}

Diff for: src/compiler/types.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1693,6 +1693,8 @@ namespace ts {
16931693
* @param path The path to test.
16941694
*/
16951695
fileExists(path: string): boolean;
1696+
1697+
readFile(path: string): string;
16961698
}
16971699

16981700
export interface WriteFileCallback {

Diff for: src/harness/harness.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1540,7 +1540,8 @@ namespace Harness {
15401540
const parseConfigHost: ts.ParseConfigHost = {
15411541
useCaseSensitiveFileNames: false,
15421542
readDirectory: (name) => [],
1543-
fileExists: (name) => true
1543+
fileExists: (name) => true,
1544+
readFile: (name) => ts.forEach(testUnitData, data => data.name.toLowerCase() === name.toLowerCase() ? data.content : undefined)
15441545
};
15451546

15461547
// check if project has tsconfig.json in the list of files

Diff for: src/harness/projectsRunner.ts

+5
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@ class ProjectRunner extends RunnerBase {
222222
useCaseSensitiveFileNames: Harness.IO.useCaseSensitiveFileNames(),
223223
fileExists,
224224
readDirectory,
225+
readFile
225226
};
226227
const configParseResult = ts.parseJsonConfigFileContent(configObject, configParseHost, ts.getDirectoryPath(configFileName), compilerOptions);
227228
if (configParseResult.errors.length > 0) {
@@ -295,6 +296,10 @@ class ProjectRunner extends RunnerBase {
295296
return Harness.IO.fileExists(getFileNameInTheProjectTest(fileName));
296297
}
297298

299+
function readFile(fileName: string): string {
300+
return Harness.IO.readFile(getFileNameInTheProjectTest(fileName));
301+
}
302+
298303
function getSourceFileText(fileName: string): string {
299304
let text: string = undefined;
300305
try {

Diff for: src/harness/rwcRunner.ts

+1
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ namespace RWC {
7979
useCaseSensitiveFileNames: Harness.IO.useCaseSensitiveFileNames(),
8080
fileExists: Harness.IO.fileExists,
8181
readDirectory: Harness.IO.readDirectory,
82+
readFile: Harness.IO.readFile
8283
};
8384
const configParseResult = ts.parseJsonConfigFileContent(parsedTsconfigFileContents.config, configParseHost, ts.getDirectoryPath(tsconfigFile.path));
8485
fileNames = configParseResult.fileNames;

Diff for: src/harness/tsconfig.json

+1
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@
8686
"./unittests/moduleResolution.ts",
8787
"./unittests/tsconfigParsing.ts",
8888
"./unittests/commandLineParsing.ts",
89+
"./unittests/configurationExtension.ts",
8990
"./unittests/convertCompilerOptionsFromJson.ts",
9091
"./unittests/convertTypingOptionsFromJson.ts",
9192
"./unittests/tsserverProjectSystem.ts",

0 commit comments

Comments
 (0)