Skip to content

Commit 92bea77

Browse files
author
Andy Hanson
committed
Tsconfig inheritance: Do not resolve included files in an inherited tsconfig
1 parent c38d2a1 commit 92bea77

File tree

1 file changed

+110
-76
lines changed

1 file changed

+110
-76
lines changed

src/compiler/commandLineParser.ts

Lines changed: 110 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1088,53 +1088,36 @@ namespace ts {
10881088
* @param host Instance of ParseConfigHost used to enumerate files in folder.
10891089
* @param basePath A root directory to resolve relative path entries in the config
10901090
* file to. e.g. outDir
1091+
* @param resolutionStack Only present for backwards-compatibility. Should be empty.
10911092
*/
1092-
export function parseJsonConfigFileContent(json: any, host: ParseConfigHost, basePath: string, existingOptions: CompilerOptions = {}, configFileName?: string, resolutionStack: Path[] = [], extraFileExtensions: JsFileExtensionInfo[] = []): ParsedCommandLine {
1093+
export function parseJsonConfigFileContent(
1094+
json: any,
1095+
host: ParseConfigHost,
1096+
basePath: string,
1097+
existingOptions: CompilerOptions = {},
1098+
configFileName?: string,
1099+
resolutionStack: Path[] = [],
1100+
extraFileExtensions: JsFileExtensionInfo[] = [],
1101+
): ParsedCommandLine {
10931102
const errors: Diagnostic[] = [];
1094-
basePath = normalizeSlashes(basePath);
1095-
const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames);
1096-
const resolvedPath = toPath(configFileName || "", basePath, getCanonicalFileName);
1097-
if (resolutionStack.indexOf(resolvedPath) >= 0) {
1098-
return {
1099-
options: {},
1100-
fileNames: [],
1101-
typeAcquisition: {},
1102-
raw: json,
1103-
errors: [createCompilerDiagnostic(Diagnostics.Circularity_detected_while_resolving_configuration_Colon_0, [...resolutionStack, resolvedPath].join(" -> "))],
1104-
wildcardDirectories: {}
1105-
};
1106-
}
11071103

1108-
let options: CompilerOptions = convertCompilerOptionsFromJsonWorker(json["compilerOptions"], basePath, errors, configFileName);
1104+
let options = (() => {
1105+
const { include, exclude, files, options } = parseConfig(json, host, basePath, configFileName, resolutionStack, errors);
1106+
if (include) json.include = include;
1107+
if (exclude) json.exclude = exclude;
1108+
if (files) json.files = files;
1109+
return options;
1110+
})();
1111+
1112+
options = extend(existingOptions, options);
1113+
options.configFilePath = configFileName;
1114+
11091115
// typingOptions has been deprecated and is only supported for backward compatibility purposes.
11101116
// It should be removed in future releases - use typeAcquisition instead.
11111117
const jsonOptions = json["typeAcquisition"] || json["typingOptions"];
11121118
const typeAcquisition: TypeAcquisition = convertTypeAcquisitionFromJsonWorker(jsonOptions, basePath, errors, configFileName);
11131119

1114-
if (json["extends"]) {
1115-
let [include, exclude, files, baseOptions]: [string[], string[], string[], CompilerOptions] = [undefined, undefined, undefined, {}];
1116-
if (typeof json["extends"] === "string") {
1117-
[include, exclude, files, baseOptions] = (tryExtendsName(json["extends"]) || [include, exclude, files, baseOptions]);
1118-
}
1119-
else {
1120-
errors.push(createCompilerDiagnostic(Diagnostics.Compiler_option_0_requires_a_value_of_type_1, "extends", "string"));
1121-
}
1122-
if (include && !json["include"]) {
1123-
json["include"] = include;
1124-
}
1125-
if (exclude && !json["exclude"]) {
1126-
json["exclude"] = exclude;
1127-
}
1128-
if (files && !json["files"]) {
1129-
json["files"] = files;
1130-
}
1131-
options = assign({}, baseOptions, options);
1132-
}
1133-
1134-
options = extend(existingOptions, options);
1135-
options.configFilePath = configFileName;
1136-
1137-
const { fileNames, wildcardDirectories } = getFileNames(errors);
1120+
const { fileNames, wildcardDirectories } = getFileNames();
11381121
const compileOnSave = convertCompileOnSaveOptionFromJson(json, basePath, errors);
11391122

11401123
return {
@@ -1147,40 +1130,7 @@ namespace ts {
11471130
compileOnSave
11481131
};
11491132

1150-
function tryExtendsName(extendedConfig: string): [string[], string[], string[], CompilerOptions] {
1151-
// 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)
1152-
if (!(isRootedDiskPath(extendedConfig) || startsWith(normalizeSlashes(extendedConfig), "./") || startsWith(normalizeSlashes(extendedConfig), "../"))) {
1153-
errors.push(createCompilerDiagnostic(Diagnostics.A_path_in_an_extends_option_must_be_relative_or_rooted_but_0_is_not, extendedConfig));
1154-
return;
1155-
}
1156-
let extendedConfigPath = toPath(extendedConfig, basePath, getCanonicalFileName);
1157-
if (!host.fileExists(extendedConfigPath) && !endsWith(extendedConfigPath, ".json")) {
1158-
extendedConfigPath = `${extendedConfigPath}.json` as Path;
1159-
if (!host.fileExists(extendedConfigPath)) {
1160-
errors.push(createCompilerDiagnostic(Diagnostics.File_0_does_not_exist, extendedConfig));
1161-
return;
1162-
}
1163-
}
1164-
const extendedResult = readConfigFile(extendedConfigPath, path => host.readFile(path));
1165-
if (extendedResult.error) {
1166-
errors.push(extendedResult.error);
1167-
return;
1168-
}
1169-
const extendedDirname = getDirectoryPath(extendedConfigPath);
1170-
const relativeDifference = convertToRelativePath(extendedDirname, basePath, getCanonicalFileName);
1171-
const updatePath: (path: string) => string = path => isRootedDiskPath(path) ? path : combinePaths(relativeDifference, path);
1172-
// Merge configs (copy the resolution stack so it is never reused between branches in potential diamond-problem scenarios)
1173-
const result = parseJsonConfigFileContent(extendedResult.config, host, extendedDirname, /*existingOptions*/undefined, getBaseFileName(extendedConfigPath), resolutionStack.concat([resolvedPath]));
1174-
errors.push(...result.errors);
1175-
const [include, exclude, files] = map(["include", "exclude", "files"], key => {
1176-
if (!json[key] && extendedResult.config[key]) {
1177-
return map(extendedResult.config[key], updatePath);
1178-
}
1179-
});
1180-
return [include, exclude, files, result.options];
1181-
}
1182-
1183-
function getFileNames(errors: Diagnostic[]): ExpandResult {
1133+
function getFileNames(): ExpandResult {
11841134
let fileNames: string[];
11851135
if (hasProperty(json, "files")) {
11861136
if (isArray(json["files"])) {
@@ -1213,9 +1163,6 @@ namespace ts {
12131163
errors.push(createCompilerDiagnostic(Diagnostics.Compiler_option_0_requires_a_value_of_type_1, "exclude", "Array"));
12141164
}
12151165
}
1216-
else if (hasProperty(json, "excludes")) {
1217-
errors.push(createCompilerDiagnostic(Diagnostics.Unknown_option_excludes_Did_you_mean_exclude));
1218-
}
12191166
else {
12201167
// If no includes were specified, exclude common package folders and the outDir
12211168
excludeSpecs = includeSpecs ? [] : ["node_modules", "bower_components", "jspm_packages"];
@@ -1245,6 +1192,93 @@ namespace ts {
12451192
}
12461193
}
12471194

1195+
type ParsedTsconfig = { include?: string[], exclude?: string[], files?: string[], options: CompilerOptions };
1196+
1197+
/**
1198+
* This *just* extracts options/include/exclude/files out of a config file.
1199+
* It does *not* resolve the included files.
1200+
*/
1201+
function parseConfig(
1202+
json: any,
1203+
host: ParseConfigHost,
1204+
basePath: string,
1205+
configFileName: string,
1206+
resolutionStack: Path[] = [],
1207+
errors: Diagnostic[],
1208+
): ParsedTsconfig {
1209+
1210+
basePath = normalizeSlashes(basePath);
1211+
const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames);
1212+
const resolvedPath = toPath(configFileName || "", basePath, getCanonicalFileName);
1213+
1214+
if (resolutionStack.indexOf(resolvedPath) >= 0) {
1215+
errors.push(createCompilerDiagnostic(Diagnostics.Circularity_detected_while_resolving_configuration_Colon_0, [...resolutionStack, resolvedPath].join(" -> ")));
1216+
return { options: {} };
1217+
}
1218+
1219+
if (hasProperty(json, "excludes")) {
1220+
errors.push(createCompilerDiagnostic(Diagnostics.Unknown_option_excludes_Did_you_mean_exclude));
1221+
}
1222+
1223+
let options: CompilerOptions = convertCompilerOptionsFromJsonWorker(json.compilerOptions, basePath, errors, configFileName);
1224+
let include: string[] | undefined = json.include, exclude: string[] | undefined = json.exclude, files: string[] | undefined = json.files;
1225+
1226+
if (json.extends) {
1227+
// copy the resolution stack so it is never reused between branches in potential diamond-problem scenarios.
1228+
resolutionStack = resolutionStack.concat([resolvedPath]);
1229+
const base = getExtendedConfig(json.extends, host, basePath, getCanonicalFileName, resolutionStack, errors);
1230+
if (base) {
1231+
include = include || base.include;
1232+
exclude = exclude || base.exclude;
1233+
files = files || base.files;
1234+
options = assign({}, base.options, options);
1235+
}
1236+
}
1237+
1238+
return { include, exclude, files, options };
1239+
}
1240+
1241+
function getExtendedConfig(
1242+
extended: any, // Usually a string.
1243+
host: ts.ParseConfigHost,
1244+
basePath: string,
1245+
getCanonicalFileName: (fileName: string) => string,
1246+
resolutionStack: Path[],
1247+
errors: Diagnostic[],
1248+
): ParsedTsconfig | undefined {
1249+
if (typeof extended !== "string") {
1250+
errors.push(createCompilerDiagnostic(Diagnostics.Compiler_option_0_requires_a_value_of_type_1, "extends", "string"));
1251+
return undefined;
1252+
}
1253+
1254+
// 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)
1255+
if (!(isRootedDiskPath(extended) || startsWith(normalizeSlashes(extended), "./") || startsWith(normalizeSlashes(extended), "../"))) {
1256+
errors.push(createCompilerDiagnostic(Diagnostics.A_path_in_an_extends_option_must_be_relative_or_rooted_but_0_is_not, extended));
1257+
return undefined;
1258+
}
1259+
1260+
let extendedConfigPath = toPath(extended, basePath, getCanonicalFileName);
1261+
if (!host.fileExists(extendedConfigPath) && !endsWith(extendedConfigPath, ".json")) {
1262+
extendedConfigPath = extendedConfigPath + ".json" as Path;
1263+
if (!host.fileExists(extendedConfigPath)) {
1264+
errors.push(createCompilerDiagnostic(Diagnostics.File_0_does_not_exist, extended));
1265+
return undefined;
1266+
}
1267+
}
1268+
1269+
const extendedResult = readConfigFile(extendedConfigPath, path => host.readFile(path));
1270+
if (extendedResult.error) {
1271+
errors.push(extendedResult.error);
1272+
return undefined;
1273+
}
1274+
1275+
const extendedDirname = getDirectoryPath(extendedConfigPath);
1276+
const relativeDifference = convertToRelativePath(extendedDirname, basePath, getCanonicalFileName);
1277+
const updatePath: (path: string) => string = path => isRootedDiskPath(path) ? path : combinePaths(relativeDifference, path);
1278+
const { include, exclude, files, options } = parseConfig(extendedResult.config, host, extendedDirname, getBaseFileName(extendedConfigPath), resolutionStack, errors);
1279+
return { include: map(include, updatePath), exclude: map(exclude, updatePath), files: map(files, updatePath), options };
1280+
}
1281+
12481282
export function convertCompileOnSaveOptionFromJson(jsonOption: any, basePath: string, errors: Diagnostic[]): boolean {
12491283
if (!hasProperty(jsonOption, compileOnSaveCommandLineOption.name)) {
12501284
return false;

0 commit comments

Comments
 (0)