Skip to content

Commit

Permalink
Draft of configuration inheritance
Browse files Browse the repository at this point in the history
  • Loading branch information
weswigham committed Jul 29, 2016
1 parent 394dbbf commit 167d318
Show file tree
Hide file tree
Showing 11 changed files with 374 additions and 14 deletions.
1 change: 1 addition & 0 deletions Jakefile.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ var harnessSources = harnessCoreSources.concat([
"moduleResolution.ts",
"tsconfigParsing.ts",
"commandLineParsing.ts",
"configurationExtension.ts",
"convertCompilerOptionsFromJson.ts",
"convertTypingOptionsFromJson.ts",
"tsserverProjectSystem.ts",
Expand Down
81 changes: 78 additions & 3 deletions src/compiler/commandLineParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -700,12 +700,54 @@ namespace ts {
* @param basePath A root directory to resolve relative path entries in the config
* file to. e.g. outDir
*/
export function parseJsonConfigFileContent(json: any, host: ParseConfigHost, basePath: string, existingOptions: CompilerOptions = {}, configFileName?: string): ParsedCommandLine {
export function parseJsonConfigFileContent(json: any, host: ParseConfigHost, basePath: string, existingOptions: CompilerOptions = {}, configFileName?: string, resolutionStack: Path[] = []): ParsedCommandLine {
const errors: Diagnostic[] = [];
const compilerOptions: CompilerOptions = convertCompilerOptionsFromJsonWorker(json["compilerOptions"], basePath, errors, configFileName);
const options = extend(existingOptions, compilerOptions);
const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames);
const resolvedPath = toPath(configFileName || "", basePath, getCanonicalFileName);
if (resolutionStack.indexOf(resolvedPath) >= 0) {
return {
options: {},
fileNames: [],
typingOptions: {},
raw: json,
errors: [createCompilerDiagnostic(Diagnostics.Circularity_detected_while_resolving_configuration_Colon_0, [...resolutionStack, resolvedPath].join(" -> "))],
wildcardDirectories: {}
};
}

let options: CompilerOptions = convertCompilerOptionsFromJsonWorker(json["compilerOptions"], basePath, errors, configFileName);
const typingOptions: TypingOptions = convertTypingOptionsFromJsonWorker(json["typingOptions"], basePath, errors, configFileName);

if (json["extends"]) {
let [include, exclude, files, baseOptions]: [string[], string[], string[], CompilerOptions] = [undefined, undefined, undefined, {}];
if (typeof json["extends"] === "string") {
[include, exclude, files, baseOptions] = (tryExtendsName(json["extends"]) || [include, exclude, files, baseOptions]);
}
else if (typeof json["extends"] === "object" && json["extends"].length) {
for (const name of json["extends"]) {
const [tempinclude, tempexclude, tempfiles, tempBase]: [string[], string[], string[], CompilerOptions] = (tryExtendsName(name) || [include, exclude, files, baseOptions]);
include = tempinclude || include;
exclude = tempexclude || exclude;
files = tempfiles || files;
baseOptions = assign({}, baseOptions, tempBase);
}
}
else {
errors.push(createCompilerDiagnostic(Diagnostics.Compiler_option_0_requires_a_value_of_type_1, "extends", "string or string[]"));
}
if (include && !json["include"]) {
json["include"] = include;
}
if (exclude && !json["exclude"]) {
json["exclude"] = exclude;
}
if (files && !json["files"]) {
json["files"] = files;
}
options = assign({}, baseOptions, options);
}

options = extend(existingOptions, options);
options.configFilePath = configFileName;

const { fileNames, wildcardDirectories } = getFileNames(errors);
Expand All @@ -719,6 +761,39 @@ namespace ts {
wildcardDirectories
};

function tryExtendsName(extendedConfig: string): [string[], string[], string[], CompilerOptions] {
// 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)
if (!(isRootedDiskPath(extendedConfig) || startsWith(normalizeSlashes(extendedConfig), "./") || startsWith(normalizeSlashes(extendedConfig), "../"))) {
errors.push(createCompilerDiagnostic(Diagnostics.The_path_in_an_extends_options_must_be_relative_or_rooted));
return;
}
let extendedConfigPath = toPath(extendedConfig, basePath, getCanonicalFileName);
if (!host.fileExists(extendedConfigPath) && !endsWith(extendedConfigPath, ".json")) {
extendedConfigPath = `${extendedConfigPath}.json` as Path;
if (!host.fileExists(extendedConfigPath)) {
errors.push(createCompilerDiagnostic(Diagnostics.File_0_does_not_exist, extendedConfig));
return;
}
}
const extendedResult = readConfigFile(extendedConfigPath, path => host.readFile(path));
if (extendedResult.error) {
errors.push(extendedResult.error);
return;
}
const extendedDirname = getDirectoryPath(extendedConfigPath);
const relativeDifference = convertToRelativePath(extendedDirname, basePath, getCanonicalFileName);
const updatePath: (path: string) => string = path => isRootedDiskPath(path) ? path : combinePaths(relativeDifference, path);
// Merge configs (copy the resolution stack so it is never reused between branches in potential diamond-problem scenarios)
const result = parseJsonConfigFileContent(extendedResult.config, host, extendedDirname, /*existingOptions*/undefined, getBaseFileName(extendedConfigPath), resolutionStack.concat([resolvedPath]));
errors.push(...result.errors);
const [include, exclude, files] = map(["include", "exclude", "files"], key => {
if (!json[key] && extendedResult.config[key]) {
return map(extendedResult.config[key], updatePath);
}
});
return [include, exclude, files, result.options];
}

function getFileNames(errors: Diagnostic[]): ExpandResult {
let fileNames: string[];
if (hasProperty(json, "files")) {
Expand Down
30 changes: 29 additions & 1 deletion src/compiler/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,20 @@ namespace ts {
return result;
}

export function mapObject<T, U>(object: Map<T>, f: (key: string, x: T) => [string, U]): Map<U> {
let result: Map<U> = {};
if (object) {
result = {};
for (const v of getKeys(object)) {
const [key, value]: [string, U] = f(v, object[v]) || [undefined, undefined];
if (key !== undefined) {
result[key] = value;
}
}
}
return result;
}

export function concatenate<T>(array1: T[], array2: T[]): T[] {
if (!array2 || !array2.length) return array1;
if (!array1 || !array1.length) return array2;
Expand Down Expand Up @@ -357,6 +371,20 @@ namespace ts {
return result;
}

export function assign<T1 extends Map<{}>, T2, T3>(t: T1, arg1: T2, arg2: T3): T1 & T2 & T3;
export function assign<T1 extends Map<{}>, T2>(t: T1, arg1: T2): T1 & T2;
export function assign<T1 extends Map<{}>>(t: T1, ...args: any[]): any;
export function assign<T1 extends Map<{}>>(t: T1, ...args: any[]) {
for (const arg of args) {
for (const p of getKeys(arg)) {
if (hasProperty(arg, p)) {
t[p] = arg[p];
}
}
}
return t;
}

export function forEachValue<T, U>(map: Map<T>, callback: (value: T) => U): U {
let result: U;
for (const id in map) {
Expand Down Expand Up @@ -941,7 +969,7 @@ namespace ts {
* [^./] # matches everything up to the first . character (excluding directory seperators)
* (\\.(?!min\\.js$))? # matches . characters but not if they are part of the .min.js file extension
*/
const singleAsteriskRegexFragmentFiles = "([^./]|(\\.(?!min\\.js$))?)*";
const singleAsteriskRegexFragmentFiles = "([^./]|(\\.(?!min\\.js$))?)*";
const singleAsteriskRegexFragmentOther = "[^/]*";

export function getRegularExpressionForWildcard(specs: string[], basePath: string, usage: "files" | "directories" | "exclude") {
Expand Down
9 changes: 9 additions & 0 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -3031,5 +3031,14 @@
"Unknown typing option '{0}'.": {
"category": "Error",
"code": 17010
},

"Circularity detected while resolving configuration: {0}": {
"category": "Error",
"code": 18000
},
"The path in an 'extends' options must be relative or rooted.": {
"category": "Error",
"code": 18001
}
}
2 changes: 2 additions & 0 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1693,6 +1693,8 @@ namespace ts {
* @param path The path to test.
*/
fileExists(path: string): boolean;

readFile(path: string): string;
}

export interface WriteFileCallback {
Expand Down
3 changes: 2 additions & 1 deletion src/harness/harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1540,7 +1540,8 @@ namespace Harness {
const parseConfigHost: ts.ParseConfigHost = {
useCaseSensitiveFileNames: false,
readDirectory: (name) => [],
fileExists: (name) => true
fileExists: (name) => true,
readFile: (name) => ts.forEach(testUnitData, data => data.name.toLowerCase() === name.toLowerCase() ? data.content : undefined)
};

// check if project has tsconfig.json in the list of files
Expand Down
5 changes: 5 additions & 0 deletions src/harness/projectsRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ class ProjectRunner extends RunnerBase {
useCaseSensitiveFileNames: Harness.IO.useCaseSensitiveFileNames(),
fileExists,
readDirectory,
readFile
};
const configParseResult = ts.parseJsonConfigFileContent(configObject, configParseHost, ts.getDirectoryPath(configFileName), compilerOptions);
if (configParseResult.errors.length > 0) {
Expand Down Expand Up @@ -295,6 +296,10 @@ class ProjectRunner extends RunnerBase {
return Harness.IO.fileExists(getFileNameInTheProjectTest(fileName));
}

function readFile(fileName: string): string {
return Harness.IO.readFile(getFileNameInTheProjectTest(fileName));
}

function getSourceFileText(fileName: string): string {
let text: string = undefined;
try {
Expand Down
1 change: 1 addition & 0 deletions src/harness/rwcRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ namespace RWC {
useCaseSensitiveFileNames: Harness.IO.useCaseSensitiveFileNames(),
fileExists: Harness.IO.fileExists,
readDirectory: Harness.IO.readDirectory,
readFile: Harness.IO.readFile
};
const configParseResult = ts.parseJsonConfigFileContent(parsedTsconfigFileContents.config, configParseHost, ts.getDirectoryPath(tsconfigFile.path));
fileNames = configParseResult.fileNames;
Expand Down
1 change: 1 addition & 0 deletions src/harness/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
"./unittests/moduleResolution.ts",
"./unittests/tsconfigParsing.ts",
"./unittests/commandLineParsing.ts",
"./unittests/configurationExtension.ts",
"./unittests/convertCompilerOptionsFromJson.ts",
"./unittests/convertTypingOptionsFromJson.ts",
"./unittests/tsserverProjectSystem.ts",
Expand Down
Loading

0 comments on commit 167d318

Please sign in to comment.