From a348e246e7f90fa128045dfbd40ff226e2184430 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Tue, 19 Dec 2017 16:19:13 -0800 Subject: [PATCH] Add 'project' option --- src/compiler/commandLineParser.ts | 6 ++ src/compiler/core.ts | 7 +++ src/compiler/diagnosticMessages.json | 26 ++++++--- src/compiler/program.ts | 43 ++++++++++----- src/harness/unittests/projectReferences.ts | 64 ++++++++++++---------- 5 files changed, 95 insertions(+), 51 deletions(-) diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index 44f599d24ea..4fe4202c8bd 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -217,6 +217,12 @@ namespace ts { category: Diagnostics.Basic_Options, description: Diagnostics.Specify_the_root_directory_of_input_files_Use_to_control_the_output_directory_structure_with_outDir, }, + { + name: "project", + type: "boolean", + category: Diagnostics.Basic_Options, + description: Diagnostics.Enable_project_compilation, + }, { name: "removeComments", type: "boolean", diff --git a/src/compiler/core.ts b/src/compiler/core.ts index a79a3751db6..15b4b69269a 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -2019,6 +2019,13 @@ namespace ts { : moduleKind === ModuleKind.System; } + export function getEmitDeclarations(compilerOptions: CompilerOptions) { + if (compilerOptions.project) { + return true; + } + return compilerOptions.declaration; + } + export type StrictOptionName = "noImplicitAny" | "noImplicitThis" | "strictNullChecks" | "strictFunctionTypes" | "strictPropertyInitialization" | "alwaysStrict"; export function getStrictOptionValue(compilerOptions: CompilerOptions, flag: StrictOptionName): boolean { diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index f977f8129c4..329e3fb80ac 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -3413,25 +3413,33 @@ "code": 6186 }, - "Project references may not form a circular graph. Cycle detected: {0}": { - "category": "Error", - "code": 6300 - }, "Projects to reference": { "category": "Message", "code": 6301 }, - "Referenced project '{0}' must have 'declaration': true": { - "category": "Error", + "Enable project compilation": { + "category": "Message", "code": 6302 }, - "Referenced project '{0}' must have an explicit 'rootDir' setting": { + "Project references may not form a circular graph. Cycle detected: {0}": { "category": "Error", "code": 6303 }, - "Output file '{0}' has not been built from source file '{1}'": { + "Projects may not disable declaration emit.": { "category": "Error", - "code": 6404 + "code": 6304 + }, + "Output file '{0}' has not been built from source file '{1}'.": { + "category": "Error", + "code": 6305 + }, + "Referenced project '{0}' must have setting \"project\": true.": { + "category": "Error", + "code": 6306 + }, + "File '{0}' is not in project file list. Projects must list all files or use an 'include' pattern.": { + "category": "Error", + "code": 6307 }, "Enable strict checking of property initialization in classes.": { diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 88afad4288b..43e8faec331 100755 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -612,6 +612,17 @@ namespace ts { Debug.assert(!!missingFilePaths); + // List of collected files is complete; validate exhautiveness if this is a project with a file list + if (options.project && rootNames.length < files.length) { + const normalizedRootNames = rootNames.map(r => normalizePath(r)); + const sourceFiles = files.filter(f => !f.isDeclarationFile).map(f => normalizePath(f.path)); + for (const file of sourceFiles) { + if (normalizedRootNames.every(r => r !== file)) { + programDiagnostics.add(createCompilerDiagnostic(Diagnostics.File_0_is_not_in_project_file_list_Projects_must_list_all_files_or_use_an_include_pattern, file)); + } + } + } + // unconditionally set moduleResolutionCache to undefined to avoid unnecessary leaks moduleResolutionCache = undefined; @@ -674,7 +685,11 @@ namespace ts { function getCommonSourceDirectory() { if (commonSourceDirectory === undefined) { const emittedFiles = filter(files, file => sourceFileMayBeEmitted(file, options, isSourceFileFromExternalLibrary)); - if (options.rootDir && checkSourceFilesBelongToPath(emittedFiles, options.rootDir)) { + if (options.project) { + // Project compilations never infer their root from the input source paths + commonSourceDirectory = getNormalizedAbsolutePath(options.rootDir || ".", currentDirectory); + } + else if (options.rootDir && checkSourceFilesBelongToPath(emittedFiles, options.rootDir)) { // If a rootDir is specified and is valid use it as the commonSourceDirectory commonSourceDirectory = getNormalizedAbsolutePath(options.rootDir, currentDirectory); } @@ -1101,7 +1116,7 @@ namespace ts { // otherwise, using options specified in '--lib' instead of '--target' default library file const equalityComparer = host.useCaseSensitiveFileNames() ? equateStringsCaseSensitive : equateStringsCaseInsensitive; if (!options.lib) { - return equalityComparer(file.fileName, getDefaultLibraryFileName()); + return equalityComparer(file.fileName, getDefaultLibraryFileName()); } else { return forEach(options.lib, libFileName => equalityComparer(file.fileName, combinePaths(defaultLibraryPath, libFileName))); @@ -2065,10 +2080,9 @@ namespace ts { const result = createMap(); walkProjectReferenceGraph(host, rootOptions, createMapping); - function createMapping(_resolvedFile: string, referencedProject: CompilerOptions) { - // No rootDir in target set; this will be an error later on in the process - if (referencedProject.rootDir === undefined) return; - result.set(referencedProject.rootDir, referencedProject.outDir); + function createMapping(resolvedFile: string, referencedProject: CompilerOptions) { + const rootDir = normalizePath(referencedProject.rootDir || getDirectoryPath(resolvedFile)); + result.set(rootDir, referencedProject.outDir); // If this project uses outFile, add the outFile to our compilation if (referencedProject.outFile) { const outFile = combinePaths(referencedProject.outDir, referencedProject.outFile); @@ -2080,10 +2094,8 @@ namespace ts { function checkProjectReferenceGraph() { // Checks the following conditions: - // * Any referenced project has declaration: true - // * Any referenced project has an explicit rootDir + // * Any referenced project has project: true // * No circularities exist - // * TODO No project root is a subfolder of any other project root const illegalRefs = createMap(); const cycleName: string[] = [options.configFilePath || host.getCurrentDirectory()]; @@ -2100,11 +2112,8 @@ namespace ts { Debug.fail("Options cannot be undefined"); return; } - if (!opts.declaration) { - createDiagnosticForOptionName(Diagnostics.Referenced_project_0_must_have_declaration_Colon_true, fileName); - } - if (!opts.rootDir) { - createDiagnosticForOptionName(Diagnostics.Referenced_project_0_must_have_an_explicit_rootDir_setting, fileName); + if (!opts.project) { + createDiagnosticForOptionName(Diagnostics.Referenced_project_0_must_have_setting_project_Colon_true, fileName); } illegalRefs.set(normalizedPath, true); cycleName.push(normalizedPath); @@ -2146,6 +2155,12 @@ namespace ts { createDiagnosticForOptionName(Diagnostics.Option_paths_cannot_be_used_without_specifying_baseUrl_option, "paths"); } + if (options.project) { + if (options.declaration === false) { + createDiagnosticForOptionName(Diagnostics.Projects_may_not_disable_declaration_emit, "declaration"); + } + } + if (options.paths) { for (const key in options.paths) { if (!hasProperty(options.paths, key)) { diff --git a/src/harness/unittests/projectReferences.ts b/src/harness/unittests/projectReferences.ts index dc225a314c8..456b5254482 100644 --- a/src/harness/unittests/projectReferences.ts +++ b/src/harness/unittests/projectReferences.ts @@ -4,9 +4,10 @@ namespace ts { interface TestProjectSpecification { configFileName?: string; - references: string[]; + references?: string[]; files: { [fileName: string]: string }; outputFiles?: { [fileName: string]: string }; + config?: object; options?: Partial; } interface TestSpecification { @@ -52,11 +53,11 @@ namespace ts { const options = { compilerOptions: { references: sp.references.map(r => ({ path: r })), - declaration: true, - rootDir: ".", + project: true, outDir: "bin", ...sp.options - } + }, + ...sp.config }; const configContent = JSON.stringify(options); const outDir = options.compilerOptions.outDir; @@ -123,35 +124,42 @@ namespace ts { * Validate that we enforce the basic settings constraints for referenced projects */ describe("project-references constraint checking for settings", () => { - const spec: TestSpecification = { - "/primary": { - files: { "/primary/a.ts": emptyModule }, - references: ["../secondary"] - }, - "/secondary": { - files: { "/secondary/b.ts": moduleImporting("../primary/a") }, - references: [], - options: { - declaration: false - } - } - }; it("errors when declaration = false", () => { - testProjectReferences(spec, "/primary/tsconfig.json", program => { - const errs = program.getOptionsDiagnostics(); - assertHasError("Reports an error about the wrong decl setting", errs, Diagnostics.Referenced_project_0_must_have_declaration_Colon_true); - }); - }); + const spec: TestSpecification = { + "/primary": { + files: { "/primary/a.ts": emptyModule }, + references: [], + options: { + declaration: false + } + } + }; - it("errors when rootDir is not set", () => { - spec["/secondary"].options.declaration = true; - spec["/secondary"].options.rootDir = undefined; testProjectReferences(spec, "/primary/tsconfig.json", program => { const errs = program.getOptionsDiagnostics(); - assertHasError("Reports an error about the wrong decl setting", errs, Diagnostics.Referenced_project_0_must_have_an_explicit_rootDir_setting); + assertHasError("Reports an error about the wrong decl setting", errs, Diagnostics.Projects_may_not_disable_declaration_emit); + }); + }); + // * TODO Projects must list all files or none + it("errors when the file list is not exhaustive", () => { + const spec: TestSpecification = { + "/primary": { + files: { + "/primary/a.ts": "import * as b from './b'", + "/primary/b.ts": "export {}" + }, + references: [], + config: { + files: ["a.ts"] + } + } + }; + + testProjectReferences(spec, "/primary/tsconfig.json", program => { + const errs = program.getOptionsDiagnostics(); + assertHasError("Reports an error about b.ts not being in the list", errs, Diagnostics.File_0_is_not_in_project_file_list_Projects_must_list_all_files_or_use_an_include_pattern); }); }); - // * TODO No project root is a subfolder of any other project root }); /** @@ -190,7 +198,7 @@ namespace ts { it("redirects to the output .d.ts file", () => { const spec: TestSpecification = { "/alpha": { - files: { "/alpha/a.ts": "export const m: number;" }, + files: { "/alpha/a.ts": "export const m: number = 3;" }, references: [], outputFiles: { "a.d.ts": emptyModule } },