From d98a9a0150b4b363b27c44eb336443b02581112f Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Mon, 14 May 2018 18:27:52 -0700 Subject: [PATCH 01/81] WIP --- src/compiler/tsbuild.ts | 69 ++++++++++++++++++++++++++++++++++++++ src/compiler/tsconfig.json | 3 +- 2 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 src/compiler/tsbuild.ts diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts new file mode 100644 index 00000000000..ee41584d1fe --- /dev/null +++ b/src/compiler/tsbuild.ts @@ -0,0 +1,69 @@ +namespace ts { +/* + interface BuildContext { + unchangedOutputs: FileMap; + } + + + + interface FileMap { + setValue(fileName: string, value: T): void; + getValue(fileName: string): T | never; + getValueOrUndefined(fileName: string): T | undefined; + getValueOrDefault(fileName: string, defaultValue: T): T; + tryGetValue(fileName: string): [false, undefined] | [true, T]; + } + + function createFileMap(): FileMap { + const lookup: { [key: string]: T } = Object.create(null); + + return { + setValue, + getValue, + getValueOrUndefined, + getValueOrDefault, + tryGetValue + } + + function setValue(fileName: string, value: T) { + lookup[normalizePath(fileName)] = value; + } + + function getValue(fileName: string): T | never { + const f = normalizePath(fileName); + if (f in lookup) { + return lookup[f]; + } else { + throw new Error(`No value corresponding to ${fileName} exists in this map`); + } + } + + function getValueOrUndefined(fileName: string): T | undefined { + const f = normalizePath(fileName); + if (f in lookup) { + return lookup[f]; + } else { + return undefined; + } + } + + function getValueOrDefault(fileName: string, defaultValue: T): T { + const f = normalizePath(fileName); + if (f in lookup) { + return lookup[f]; + } else { + return defaultValue; + } + } + + function tryGetValue(fileName: string): [false, undefined] | [true, T] { + const f = normalizePath(fileName); + if (f in lookup) { + return [true as true, lookup[f]]; + } else { + return [false as false, undefined]; + } + } + } + */ +} diff --git a/src/compiler/tsconfig.json b/src/compiler/tsconfig.json index 46e5384434a..0db027d555f 100644 --- a/src/compiler/tsconfig.json +++ b/src/compiler/tsconfig.json @@ -46,6 +46,7 @@ "resolutionCache.ts", "watch.ts", "commandLineParser.ts", - "tsc.ts" + "tsc.ts", + "tsbuild.ts" ] } From aeb562519952b6fb2c7d24955e3542ed278b6375 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Thu, 17 May 2018 16:39:15 -0700 Subject: [PATCH 02/81] WIP --- src/compiler/tsbuild.ts | 323 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 320 insertions(+), 3 deletions(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index ee41584d1fe..0c14778abe5 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -1,10 +1,125 @@ namespace ts { -/* + const MinimumDate = new Date(-8640000000000000); + const MaximumDate = new Date(8640000000000000); + + /** + * A BuildContext tracks what's going on during the course of a build. + * The primary thing we track here is which files were written to, + * but unchanged, because this enables fast downstream updates + */ interface BuildContext { - unchangedOutputs: FileMap; + /** + * Map from output file name to its pre-build timestamp + */ + unchangedOutputs: FileMap; + + /** + * Map from config file name to up-to-date status + */ + projectStatus: FileMap; } + enum BuildResultFlags { + None = 0, + /** + * No errors of any kind occurred during build + */ + Success = 1 << 0, + /** + * None of the .d.ts files emitted by this build were + * different from the existing files on disk + */ + DeclarationOutputUnchanged = 1 << 1, + + ConfigFileErrors = 1 << 2, + SyntaxErrors = 1 << 3, + TypeErrors = 1 << 4, + DeclarationEmitErrors = 1 << 5, + + AnyErrors = ConfigFileErrors | SyntaxErrors | TypeErrors | DeclarationEmitErrors + } + + enum UpToDateStatusType { + Unbuildable, + UpToDate, + /** + * The project appears out of date because its upstream inputs are newer than its outputs, + * but all of its outputs are actually newer than the previous identical outputs of its inputs. + * This means we can Pseudo-build (just touch timestamps), as if we had actually built this project. + */ + UpToDateWithUpstreamTypes, + OutputMissing, + OutOfDateWithSelf, + OutOfDateWithUpstream, + UpstreamOutOfDate + } + + type UpToDateStatus = + | StatusUnbuildable + | StatusUpToDate + | StatusOutputMissing + | StatusOutOfDateWithSelf + | StatusOutOfDateWithUpstream + | StatusUpstreamOutOfDate; + + /** + * The project can't be built at all in its current state. For example, + * its config file cannot be parsed, or it has a syntax error or missing file + */ + interface StatusUnbuildable { + type: UpToDateStatusType.Unbuildable; + reason: string; + } + + /** + * The project is up to date with respect to its inputs. + * We track what the newest input file is. + */ + interface StatusUpToDate { + type: UpToDateStatusType.UpToDate | UpToDateStatusType.UpToDateWithUpstreamTypes; + newestInputFileTime: Date; + newestDeclarationFileContentChangedTime: Date; + newestOutputFileTime: Date; + } + + /** + * One or more of the outputs of the project does not exist. + */ + interface StatusOutputMissing { + type: UpToDateStatusType.OutputMissing; + /** + * The name of the first output file that didn't exist + */ + missingOutputFileName: string; + } + + /** + * One or more of the project's outputs is older than its newest input. + */ + interface StatusOutOfDateWithSelf { + type: UpToDateStatusType.OutOfDateWithSelf; + outOfDateOutputFileName: string; + newerInputFileName: string; + } + + /** + * This project depends on an out-of-date project, so shouldn't be built yet + */ + interface StatusUpstreamOutOfDate { + type: UpToDateStatusType.UpstreamOutOfDate; + upstreamProjectName: string; + } + + /** + * One or more of the project's outputs is older than the newest output of + * an upstream project. + */ + interface StatusOutOfDateWithUpstream { + type: UpToDateStatusType.OutOfDateWithUpstream; + outOfDateOutputFileName: string; + newerProjectName: string; + } interface FileMap { setValue(fileName: string, value: T): void; @@ -14,6 +129,9 @@ namespace ts { tryGetValue(fileName: string): [false, undefined] | [true, T]; } + /** + * A FileMap maintains a normalized-key to value relationship + */ function createFileMap(): FileMap { const lookup: { [key: string]: T } = Object.create(null); @@ -65,5 +183,204 @@ namespace ts { } } } - */ + + function getOutputDeclarationFileName(inputFileName: string, configFile: ts.ParsedCommandLine) { + const relativePath = getRelativePathFromDirectory(rootDirOfOptions(configFile.options, configFile.options.configFilePath), inputFileName, true); + const outputPath = resolvePath(configFile.options.declarationDir || configFile.options.outDir || getDirectoryPath(configFile.options.configFilePath), relativePath); + return changeExtension(outputPath, ".d.ts"); + } + + function getOutputJavaScriptFileName(inputFileName: string, configFile: ts.ParsedCommandLine) { + // TODO handle JSX: Preserve + const relativePath = getRelativePathFromDirectory(rootDirOfOptions(configFile.options, configFile.options.configFilePath), inputFileName, true); + const outputPath = resolvePath(configFile.options.outDir || getDirectoryPath(configFile.options.configFilePath), relativePath); + return changeExtension(outputPath, (fileExtensionIs(inputFileName, ".tsx") && configFile.options.jsx === JsxEmit.Preserve) ? ".jsx" : ".js"); + } + + function getOutputFileNames(inputFileName: string, configFile: ts.ParsedCommandLine): ReadonlyArray { + if (configFile.options.outFile) { + return emptyArray; + } + + const outputs: string[] = []; + outputs.push(getOutputJavaScriptFileName(inputFileName, configFile)); + if (configFile.options.declaration) { + const dts = outputs.push(getOutputDeclarationFileName(inputFileName, configFile)); + if (configFile.options.declarationMap) { + outputs.push(dts + ".map"); + } + } + return outputs; + } + + function getOutFileOutputs(project: ts.ParsedCommandLine): ReadonlyArray { + Debug.assert(!!project.options.outFile, "outFile must be set"); + const outputs: string[] = []; + outputs.push(project.options.outFile); + if (project.options.declaration) { + const dts = outputs.push(changeExtension(project.options.outFile, ".d.ts")); + if (project.options.declarationMap) { + outputs.push(dts + ".map"); + } + } + return outputs; + } + + function rootDirOfOptions(opts: ts.CompilerOptions, configFileName: string) { + return opts.rootDir || path.dirname(configFileName); + } + + function createConfigFileCache(host: CompilerHost) { + const cache = createFileMap(); + const configParseHost = parseConfigHostFromCompilerHost(host); + + // TODO: Cache invalidation! + + function parseConfigFile(configFilePath: string) { + const sourceFile = host.getSourceFile(configFilePath, ScriptTarget.JSON) as JsonSourceFile; + const parsed = parseJsonSourceFileConfigFileContent(sourceFile, configParseHost, configFilePath); + cache.setValue(configFilePath, parsed); + return parsed; + } + + return { + parseConfigFile + } + } + + function newer(date1: Date, date2: Date): Date { + return date2 > date1 ? date2 : date1; + } + + function older(date1: Date, date2: Date): Date { + return date2 < date1 ? date2 : date1; + } + + function createSolutionBuilder(host: CompilerHost) { + const configFileCache = createConfigFileCache(host); + + function getUpToDateStatus(project: ParsedCommandLine, context: BuildContext): UpToDateStatus { + let newestInputFileName: string = '???'; + let newestInputFileTime = MinimumDate; + // Get timestamps of input files + for (const inputFile of project.fileNames) { + if (!host.fileExists(inputFile)) { + return { + type: UpToDateStatusType.Unbuildable, + reason: `${inputFile} does not exist` + }; + } + + const inputTime = sys.getModifiedTime(inputFile); + if (inputTime > newestInputFileTime) { + newestInputFileName = inputFile; + newestInputFileTime = inputTime; + } + } + + // Collect the expected outputs of this project + let outputs: ReadonlyArray; + if (project.options.outFile) { + outputs = getOutFileOutputs(project); + } + else { + outputs = []; + for (const inputFile of project.fileNames) { + (outputs as string[]).push(...getOutputFileNames(inputFile, project)); + } + } + + // Now see if all outputs are newer than the newest input + let oldestOutputFileName: string = "n/a"; + let oldestOutputFileTime: Date = MinimumDate; + let newestOutputFileTime: Date = MaximumDate; + let newestDeclarationFileContentChangedTime: Date = MinimumDate; + for (const output of outputs) { + // Output is missing + if (!host.fileExists(output)) { + return { + type: UpToDateStatusType.OutputMissing, + missingOutputFileName: output + }; + } + + const outputTime = sys.getModifiedTime(output); + // If an output is older than the newest input, we can stop checking + if (outputTime < newestInputFileTime) { + return { + type: UpToDateStatusType.OutOfDateWithSelf, + outOfDateOutputFileName: output, + newerInputFileName: newestInputFileName + }; + } + + if (outputTime < oldestOutputFileTime) { + oldestOutputFileTime = outputTime; + oldestOutputFileName = output; + } + newestOutputFileTime = older(newestOutputFileTime, outputTime); + + // Keep track of when the most recent time a .d.ts file was changed. + // In addition to file timestamps, we also keep track of when a .d.ts file + // had its file touched but not had its contents changed - this allows us + // to skip a downstream typecheck + if (fileExtensionIs(output, ".d.ts")) { + const unchangedTime = context.unchangedOutputs.getValueOrUndefined(output); + if (unchangedTime !== undefined) { + newestDeclarationFileContentChangedTime = newer(unchangedTime, newestDeclarationFileContentChangedTime); + } + else { + newestDeclarationFileContentChangedTime = newer(newestDeclarationFileContentChangedTime, sys.getModifiedTime(output)); + } + } + } + + let pseudoUpToDate = false; + // By here, we know the project is at least up-to-date with its own inputs. + // See if any of its upstream projects are newer than it + for (const ref of project.projectReferences) { + const refStatus = getUpToDateStatus(configFileCache.parseConfigFile(ref.path), context); + + // If the upstream project is out of date, then so are we (someone shouldn't have asked, though?) + if (refStatus.type !== UpToDateStatusType.UpToDate) { + return { + type: UpToDateStatusType.UpstreamOutOfDate, + upstreamProjectName: ref.path + } + } + + // If the upstream project's newest file is older than our oldest output, we + // can't be out of date because of it + if (refStatus.newestInputFileTime < oldestOutputFileTime) { + continue; + } + + // If the upstream project has only change .d.ts files, and we've built + // *after* those files, then we're "psuedo up to date" and eligible for a fast rebuild + if (refStatus.newestDeclarationFileContentChangedTime < oldestOutputFileTime) { + pseudoUpToDate = true; + continue; + } + + // We have an output older than an upstream output - we are out of date + return { + type: UpToDateStatusType.OutOfDateWithUpstream, + outOfDateOutputFileName: oldestOutputFileName, + newerProjectName: ref.path + }; + } + + // Up to date + return { + type: pseudoUpToDate ? UpToDateStatusType.UpToDateWithUpstreamTypes : UpToDateStatusType.UpToDate, + newestDeclarationFileContentChangedTime, + newestInputFileTime, + newestOutputFileTime + }; + } + + return { + getUpToDateStatus + } + } } From a4eb15635b15fc5062e010dc4e95d025e4a24843 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Thu, 17 May 2018 16:52:21 -0700 Subject: [PATCH 03/81] WIP, need to lint --- src/compiler/tsbuild.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index 0c14778abe5..e0929e355b2 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -260,6 +260,16 @@ namespace ts { const configFileCache = createConfigFileCache(host); function getUpToDateStatus(project: ParsedCommandLine, context: BuildContext): UpToDateStatus { + const prior = context.projectStatus.getValueOrUndefined(project.options.configFilePath); + if (prior !== undefined) { + return prior; + } + const actual = getUpToDateStatusWorker(project, context); + context.projectStatus.setValue(project.options.configFilePath, actual); + return actual; + } + + function getUpToDateStatusWorker(project: ParsedCommandLine, context: BuildContext): UpToDateStatus { let newestInputFileName: string = '???'; let newestInputFileTime = MinimumDate; // Get timestamps of input files @@ -381,6 +391,6 @@ namespace ts { return { getUpToDateStatus - } + }; } } From b2720adbc31bf7692db3a163f1f87be24af0eca6 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Mon, 21 May 2018 10:56:24 -0700 Subject: [PATCH 04/81] Lint --- src/compiler/tsbuild.ts | 51 ++++++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index e0929e355b2..40c23dc300d 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -1,6 +1,6 @@ namespace ts { - const MinimumDate = new Date(-8640000000000000); - const MaximumDate = new Date(8640000000000000); + const minimumDate = new Date(-8640000000000000); + const maximumDate = new Date(8640000000000000); /** * A BuildContext tracks what's going on during the course of a build. @@ -133,7 +133,8 @@ namespace ts { * A FileMap maintains a normalized-key to value relationship */ function createFileMap(): FileMap { - const lookup: { [key: string]: T } = Object.create(null); + // tslint:disable-next-line:no-null-keyword + const lookup: { [key: string]: T } = Object.create(/*prototype*/ null); return { setValue, @@ -141,7 +142,7 @@ namespace ts { getValueOrUndefined, getValueOrDefault, tryGetValue - } + }; function setValue(fileName: string, value: T) { lookup[normalizePath(fileName)] = value; @@ -151,7 +152,8 @@ namespace ts { const f = normalizePath(fileName); if (f in lookup) { return lookup[f]; - } else { + } + else { throw new Error(`No value corresponding to ${fileName} exists in this map`); } } @@ -160,7 +162,8 @@ namespace ts { const f = normalizePath(fileName); if (f in lookup) { return lookup[f]; - } else { + } + else { return undefined; } } @@ -169,7 +172,8 @@ namespace ts { const f = normalizePath(fileName); if (f in lookup) { return lookup[f]; - } else { + } + else { return defaultValue; } } @@ -178,26 +182,27 @@ namespace ts { const f = normalizePath(fileName); if (f in lookup) { return [true as true, lookup[f]]; - } else { + } + else { return [false as false, undefined]; } } } - function getOutputDeclarationFileName(inputFileName: string, configFile: ts.ParsedCommandLine) { - const relativePath = getRelativePathFromDirectory(rootDirOfOptions(configFile.options, configFile.options.configFilePath), inputFileName, true); + function getOutputDeclarationFileName(inputFileName: string, configFile: ParsedCommandLine) { + const relativePath = getRelativePathFromDirectory(rootDirOfOptions(configFile.options, configFile.options.configFilePath), inputFileName, /*ignoreCase*/ true); const outputPath = resolvePath(configFile.options.declarationDir || configFile.options.outDir || getDirectoryPath(configFile.options.configFilePath), relativePath); return changeExtension(outputPath, ".d.ts"); } - function getOutputJavaScriptFileName(inputFileName: string, configFile: ts.ParsedCommandLine) { + function getOutputJavaScriptFileName(inputFileName: string, configFile: ParsedCommandLine) { // TODO handle JSX: Preserve - const relativePath = getRelativePathFromDirectory(rootDirOfOptions(configFile.options, configFile.options.configFilePath), inputFileName, true); + const relativePath = getRelativePathFromDirectory(rootDirOfOptions(configFile.options, configFile.options.configFilePath), inputFileName, /*ignoreCase*/ true); const outputPath = resolvePath(configFile.options.outDir || getDirectoryPath(configFile.options.configFilePath), relativePath); return changeExtension(outputPath, (fileExtensionIs(inputFileName, ".tsx") && configFile.options.jsx === JsxEmit.Preserve) ? ".jsx" : ".js"); } - function getOutputFileNames(inputFileName: string, configFile: ts.ParsedCommandLine): ReadonlyArray { + function getOutputFileNames(inputFileName: string, configFile: ParsedCommandLine): ReadonlyArray { if (configFile.options.outFile) { return emptyArray; } @@ -213,7 +218,7 @@ namespace ts { return outputs; } - function getOutFileOutputs(project: ts.ParsedCommandLine): ReadonlyArray { + function getOutFileOutputs(project: ParsedCommandLine): ReadonlyArray { Debug.assert(!!project.options.outFile, "outFile must be set"); const outputs: string[] = []; outputs.push(project.options.outFile); @@ -226,7 +231,7 @@ namespace ts { return outputs; } - function rootDirOfOptions(opts: ts.CompilerOptions, configFileName: string) { + function rootDirOfOptions(opts: CompilerOptions, configFileName: string) { return opts.rootDir || path.dirname(configFileName); } @@ -245,7 +250,7 @@ namespace ts { return { parseConfigFile - } + }; } function newer(date1: Date, date2: Date): Date { @@ -270,8 +275,8 @@ namespace ts { } function getUpToDateStatusWorker(project: ParsedCommandLine, context: BuildContext): UpToDateStatus { - let newestInputFileName: string = '???'; - let newestInputFileTime = MinimumDate; + let newestInputFileName: string = undefined!; + let newestInputFileTime = minimumDate; // Get timestamps of input files for (const inputFile of project.fileNames) { if (!host.fileExists(inputFile)) { @@ -301,10 +306,10 @@ namespace ts { } // Now see if all outputs are newer than the newest input - let oldestOutputFileName: string = "n/a"; - let oldestOutputFileTime: Date = MinimumDate; - let newestOutputFileTime: Date = MaximumDate; - let newestDeclarationFileContentChangedTime: Date = MinimumDate; + let oldestOutputFileName: string = undefined!; + let oldestOutputFileTime: Date = minimumDate; + let newestOutputFileTime: Date = maximumDate; + let newestDeclarationFileContentChangedTime: Date = minimumDate; for (const output of outputs) { // Output is missing if (!host.fileExists(output)) { @@ -356,7 +361,7 @@ namespace ts { return { type: UpToDateStatusType.UpstreamOutOfDate, upstreamProjectName: ref.path - } + }; } // If the upstream project's newest file is older than our oldest output, we From b1dedf254037bd2cfcac2d5033495902aa793045 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Mon, 21 May 2018 13:05:14 -0700 Subject: [PATCH 05/81] WIP --- src/compiler/tsbuild.ts | 211 +++++++++++++++++++++++++++++++++++++++- src/compiler/types.ts | 2 + 2 files changed, 211 insertions(+), 2 deletions(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index 40c23dc300d..af04c67a977 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -19,6 +19,12 @@ namespace ts { projectStatus: FileMap; } + type Mapper = ReturnType; + interface DependencyGraph { + buildQueue: string[][]; + dependencyMap: Mapper; + } + enum BuildResultFlags { None = 0, @@ -189,6 +195,47 @@ namespace ts { } } + export function createDependencyMapper() { + const childToParents: { [key: string]: string[] } = {}; + const parentToChildren: { [key: string]: string[] } = {}; + const allKeys: string[] = []; + + function addReference(childConfigFileName: string, parentConfigFileName: string): void { + addEntry(childToParents, childConfigFileName, parentConfigFileName); + addEntry(parentToChildren, parentConfigFileName, childConfigFileName); + } + + function getReferencesTo(parentConfigFileName: string): string[] { + return parentToChildren[normalizePath(parentConfigFileName)] || []; + } + + function getReferencesOf(childConfigFileName: string): string[] { + return childToParents[normalizePath(childConfigFileName)] || []; + } + + function getKeys(): ReadonlyArray { + return allKeys; + } + + function addEntry(mapToAddTo: typeof childToParents | typeof parentToChildren, key: string, element: string) { + key = normalizePath(key); + element = normalizePath(element); + const arr = (mapToAddTo[key] = mapToAddTo[key] || []); + if (arr.indexOf(element) < 0) { + arr.push(element); + } + if (allKeys.indexOf(key) < 0) allKeys.push(key); + if (allKeys.indexOf(element) < 0) allKeys.push(element); + } + + return { + addReference, + getReferencesTo, + getReferencesOf, + getKeys + }; + } + function getOutputDeclarationFileName(inputFileName: string, configFile: ParsedCommandLine) { const relativePath = getRelativePathFromDirectory(rootDirOfOptions(configFile.options, configFile.options.configFilePath), inputFileName, /*ignoreCase*/ true); const outputPath = resolvePath(configFile.options.declarationDir || configFile.options.outDir || getDirectoryPath(configFile.options.configFilePath), relativePath); @@ -232,7 +279,7 @@ namespace ts { } function rootDirOfOptions(opts: CompilerOptions, configFileName: string) { - return opts.rootDir || path.dirname(configFileName); + return opts.rootDir || getDirectoryPath(configFileName); } function createConfigFileCache(host: CompilerHost) { @@ -261,7 +308,12 @@ namespace ts { return date2 < date1 ? date2 : date1; } + function isDeclarationFile(fileName: string) { + return fileExtensionIs(fileName, ".d.ts"); + } + function createSolutionBuilder(host: CompilerHost) { + const diagReporter = createDiagnosticReporter(sys, /*pretty*/true); const configFileCache = createConfigFileCache(host); function getUpToDateStatus(project: ParsedCommandLine, context: BuildContext): UpToDateStatus { @@ -394,8 +446,163 @@ namespace ts { }; } + function createDependencyGraph(roots: string[]): DependencyGraph { + // This is a list of list of projects that need to be built. + // The ordering here is "backwards", i.e. the first entry in the array is the last set of projects that need to be built; + // and the last entry is the first set of projects to be built. + // Each subarray is effectively unordered. + // We traverse the reference graph from each root, then "clean" the list by removing + // any entry that is duplicated to its right. + const buildQueue: string[][] = []; + const dependencyMap = createDependencyMapper(); + let buildQueuePosition = 0; + for (const root of roots) { + const config = configFileCache.parseConfigFile(root); + if (config === undefined) { + throw new Error(`Could not parse ${root}`); + } + enumerateReferences(normalizePath(root), config); + } + removeDuplicatesFromBuildQueue(buildQueue); + + return { + buildQueue, + dependencyMap + }; + + function enumerateReferences(fileName: string, root: ts.ParsedCommandLine): void { + const myBuildLevel = buildQueue[buildQueuePosition] = buildQueue[buildQueuePosition] || []; + if (myBuildLevel.indexOf(fileName) < 0) { + myBuildLevel.push(fileName); + } + + const refs = root.projectReferences; + if (refs === undefined) return; + buildQueuePosition++; + for (const ref of refs) { + dependencyMap.addReference(fileName, ref.path); + const resolvedRef = configFileCache.parseConfigFile(ref.path); + if (resolvedRef === undefined) continue; + enumerateReferences(normalizePath(ref.path), resolvedRef); + } + buildQueuePosition--; + } + + /** + * Removes entries from arrays which appear in later arrays. + * TODO: Use a lookup object to optimize this a bit? + */ + function removeDuplicatesFromBuildQueue(queue: string[][]): void { + // No need to check the last array + for (let i = 0; i < queue.length - 1; i++) { + queue[i] = queue[i].filter(fn => !occursAfter(fn, i + 1)); + } + + function occursAfter(s: string, start: number) { + for (let i = start; i < queue.length; i++) { + if (queue[i].indexOf(s) >= 0) return true; + } + return false; + } + } + } + + function buildSingleProject(proj: string, context: BuildContext) { + let resultFlags = BuildResultFlags.None; + resultFlags |= BuildResultFlags.DeclarationOutputUnchanged; + + const configFile = configFileCache.parseConfigFile(proj); + if (!configFile) { + // Failed to read the config file + resultFlags |= BuildResultFlags.ConfigFileErrors; + return resultFlags; + } + + if (configFile.fileNames.length === 0) { + // Nothing to build - must be a solution file, basically + return BuildResultFlags.None; + } + + const programOptions: CreateProgramOptions = { + projectReferences: configFile.projectReferences, + host: host, + rootNames: configFile.fileNames, + options: configFile.options + }; + const program = ts.createProgram(programOptions); + + // Don't emit anything in the presence of syntactic errors or options diagnostics + const syntaxDiagnostics = [...program.getOptionsDiagnostics(), ...program.getSyntacticDiagnostics()]; + if (syntaxDiagnostics.length) { + resultFlags |= BuildResultFlags.SyntaxErrors; + for (const diag of syntaxDiagnostics) { + diagReporter(diag); + } + return resultFlags; + } + + // Don't emit .d.ts if there are decl file errors + if (program.getCompilerOptions().declaration) { + const declDiagnostics = program.getDeclarationDiagnostics(); + if (declDiagnostics.length) { + resultFlags |= BuildResultFlags.DeclarationEmitErrors; + for (const diag of declDiagnostics) { + diagReporter(diag); + } + } + return resultFlags; + } + + const semanticDiagnostics = [...program.getSemanticDiagnostics()]; + if (semanticDiagnostics.length) { + resultFlags |= BuildResultFlags.TypeErrors; + for (const diag of semanticDiagnostics) { + diagReporter(diag); + } + return resultFlags; + } + + program.emit(undefined, (fileName, content, writeBom, onError) => { + let priorChangeTime: Date | undefined; + + if (isDeclarationFile(fileName) && host.fileExists(fileName)) { + if (host.readFile(fileName) === content) { + resultFlags &= ~BuildResultFlags.DeclarationOutputUnchanged; + priorChangeTime = host.getLastWriteTime && host.getLastWriteTime(fileName); + } + } + + host.writeFile(fileName, content, writeBom, onError, emptyArray); + if (priorChangeTime !== undefined) { + context.unchangedOutputs.setValue(fileName, priorChangeTime); + } + }); + + return resultFlags; + } + + function buildProjects(configFileNames: string[], context: BuildContext) { + // Establish what needs to be built + const graph = createDependencyGraph(configFileNames); + + const queue = graph.buildQueue; + while (queue.length > 0) { + const next = queue[0].pop()!; + + const result = buildSingleProject(next, context); + if (result & BuildResultFlags.AnyErrors) { + break; + } + + if (queue[0].length === 0) { + queue.pop(); + } + } + } + return { - getUpToDateStatus + getUpToDateStatus, + buildProjects }; } } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 4b2a7c080d2..3da916244d6 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -4768,6 +4768,8 @@ namespace ts { /* @internal */ hasInvalidatedResolution?: HasInvalidatedResolution; /* @internal */ hasChangedAutomaticTypeDirectiveNames?: boolean; createHash?(data: string): string; + + getLastWriteTime?(fileName: string): Date; } /* @internal */ From 7e0825a3e7adc9878834cd2e334efa2fa14f48af Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Mon, 21 May 2018 17:47:58 -0700 Subject: [PATCH 06/81] Clean, etc --- src/compiler/diagnosticMessages.json | 37 +++ src/compiler/program.ts | 7 +- src/compiler/sys.ts | 21 ++ src/compiler/tsbuild.ts | 343 ++++++++++++++++++++++----- src/compiler/tsc.ts | 4 + src/compiler/tsconfig.json | 2 +- src/compiler/types.ts | 4 +- 7 files changed, 350 insertions(+), 68 deletions(-) diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 2956faef0b3..1077db69ef7 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -3593,6 +3593,43 @@ "category": "Error", "code": 6309 }, + "Project '{0}' is out of date because oldest output '{1}' is older than newest input '{2}'": { + "category": "Message", + "code": 6350 + }, + "Project '{0}' is up to date because newest input '{1}' is older than oldest output '{2}'": { + "category": "Message", + "code": 6351 + }, + "Project '{0}' is out of date because output file '{1}' does not exist": { + "category": "Message", + "code": 6352 + }, + + "Project '{0}' is up to date with its upstream types": { + "category": "Message", + "code": 6353 + }, + "Sorted list of input projects: {0}": { + "category": "Message", + "code": 6354 + }, + "Would delete the following files:{0}": { + "category": "Message", + "code": 6355 + }, + "Would build project '{0}'": { + "category": "Message", + "code": 6356 + }, + "Building project '{0}'...": { + "category": "Message", + "code": 6357 + }, + "Updating output timestamps of project '{0}'...": { + "category": "Message", + "code": 6358 + }, "Variable '{0}' implicitly has an '{1}' type.": { "category": "Error", diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 365e948f749..5f8eb330d20 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -189,7 +189,10 @@ namespace ts { getEnvironmentVariable: name => sys.getEnvironmentVariable ? sys.getEnvironmentVariable(name) : "", getDirectories: (path: string) => sys.getDirectories(path), realpath, - readDirectory: (path, extensions, include, exclude, depth) => sys.readDirectory(path, extensions, include, exclude, depth) + readDirectory: (path, extensions, include, exclude, depth) => sys.readDirectory(path, extensions, include, exclude, depth), + getModifiedTime: path => sys.getModifiedTime(path), + setModifiedTime: (path, date) => sys.setModifiedTime(path, date), + deleteFile: path => sys.deleteFile(path) }; } @@ -2692,7 +2695,7 @@ namespace ts { /** * Returns the target config filename of a project reference */ - function resolveProjectReferencePath(host: CompilerHost, ref: ProjectReference): string | undefined { + export function resolveProjectReferencePath(host: CompilerHost, ref: ProjectReference): string | undefined { if (!host.fileExists(ref.path)) { return combinePaths(ref.path, "tsconfig.json"); } diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index 688400fc6d5..6993a3b438c 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -432,6 +432,7 @@ namespace ts { readFile(path: string, encoding?: string): string | undefined; getFileSize?(path: string): number; writeFile(path: string, data: string, writeByteOrderMark?: boolean): void; + /** * @pollingInterval - this parameter is used in polling-based watchers and ignored in watchers that * use native OS file watching @@ -447,6 +448,8 @@ namespace ts { getDirectories(path: string): string[]; readDirectory(path: string, extensions?: ReadonlyArray, exclude?: ReadonlyArray, include?: ReadonlyArray, depth?: number): string[]; getModifiedTime?(path: string): Date; + setModifiedTime?(path: string, time: Date): void; + deleteFile?(path: string): void; /** * This should be cryptographically secure. * A good implementation is node.js' `crypto.createHash`. (https://nodejs.org/api/crypto.html#crypto_crypto_createhash_algorithm) @@ -589,6 +592,8 @@ namespace ts { }, readDirectory, getModifiedTime, + setModifiedTime, + deleteFile, createHash: _crypto ? createMD5HashUsingNativeCrypto : generateDjb2Hash, getMemoryUsage() { if (global.gc) { @@ -1063,6 +1068,22 @@ namespace ts { } } + function setModifiedTime(path: string, time: Date) { + try { + _fs.utimesSync(path, time, time); + } + catch (e) { + } + } + + function deleteFile(path: string) { + try { + return _fs.unlinkSync(path); + } + catch (e) { + } + } + /** * djb2 hashing algorithm * http://www.cse.yorku.ca/~oz/hash.html diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index af04c67a977..b59e6ee4e95 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -8,6 +8,7 @@ namespace ts { * but unchanged, because this enables fast downstream updates */ interface BuildContext { + options: BuildOptions; /** * Map from output file name to its pre-build timestamp */ @@ -17,6 +18,8 @@ namespace ts { * Map from config file name to up-to-date status */ projectStatus: FileMap; + + verbose(diag: DiagnosticMessage, ...args: any[]): void; } type Mapper = ReturnType; @@ -25,6 +28,12 @@ namespace ts { dependencyMap: Mapper; } + interface BuildOptions { + dry: boolean; + force: boolean; + verbose: boolean; + } + enum BuildResultFlags { None = 0, @@ -51,7 +60,7 @@ namespace ts { UpToDate, /** * The project appears out of date because its upstream inputs are newer than its outputs, - * but all of its outputs are actually newer than the previous identical outputs of its inputs. + * but all of its outputs are actually newer than the previous identical outputs of its (.d.ts) inputs. * This means we can Pseudo-build (just touch timestamps), as if we had actually built this project. */ UpToDateWithUpstreamTypes, @@ -286,11 +295,12 @@ namespace ts { const cache = createFileMap(); const configParseHost = parseConfigHostFromCompilerHost(host); - // TODO: Cache invalidation! + // TODO: Cache invalidation under --watch! function parseConfigFile(configFilePath: string) { const sourceFile = host.getSourceFile(configFilePath, ScriptTarget.JSON) as JsonSourceFile; - const parsed = parseJsonSourceFileConfigFileContent(sourceFile, configParseHost, configFilePath); + const parsed = parseJsonSourceFileConfigFileContent(sourceFile, configParseHost, getDirectoryPath(configFilePath)); + parsed.options.configFilePath = configFilePath; cache.setValue(configFilePath, parsed); return parsed; } @@ -312,21 +322,109 @@ namespace ts { return fileExtensionIs(fileName, ".d.ts"); } - function createSolutionBuilder(host: CompilerHost) { + function createBuildContext(options: BuildOptions): BuildContext { + const verboseDiag = options.verbose && createDiagnosticReporter(sys, /*pretty*/ false); + return { + options, + projectStatus: createFileMap(), + unchangedOutputs: createFileMap(), + verbose: options.verbose ? (diag, ...args) => { + verboseDiag(createCompilerDiagnostic(diag, ...args)); + } : () => undefined + }; + } + + export function performBuild(args: string[]) { + const diagReporter = createDiagnosticReporter(sys, /*pretty*/true); + const host = createCompilerHost({}); + + let verbose = false; + let dry = false; + let force = false; + let clean = false; + + const projects: string[] = []; + for (let i = 0; i < args.length; i++) { + switch (args[i].toLowerCase()) { + case "-v": + case "--verbose": + verbose = true; + continue; + case "-d": + case "--dry": + dry = true; + continue; + case "-f": + case "--force": + force = true; + continue; + case "--clean": + clean = true; + continue; + } + // Not a flag, parse as filename + addProject(args[i]); + } + + if (projects.length === 0) { + // tsc -b invoked with no extra arguments; act as if invoked with "tsc -b ." + addProject("."); + } + + const context = createBuildContext({ verbose, dry, force }); + const builder = createSolutionBuilder(host, context); + if (clean) { + builder.cleanProjects(projects); + } + else { + builder.buildProjects(projects); + } + + function addProject(projectSpecification: string) { + const fileName = resolvePath(host.getCurrentDirectory(), projectSpecification); + const refPath = resolveProjectReferencePath(host, { path: fileName }); + if (!host.fileExists(refPath)) { + diagReporter(createCompilerDiagnostic(Diagnostics.File_0_does_not_exist, fileName)); + } + projects.push(refPath); + + } + } + + export function createSolutionBuilder(host: CompilerHost, context: BuildContext) { const diagReporter = createDiagnosticReporter(sys, /*pretty*/true); const configFileCache = createConfigFileCache(host); + return { + getUpToDateStatus, + buildProjects, + cleanProjects + }; + function getUpToDateStatus(project: ParsedCommandLine, context: BuildContext): UpToDateStatus { const prior = context.projectStatus.getValueOrUndefined(project.options.configFilePath); if (prior !== undefined) { return prior; } - const actual = getUpToDateStatusWorker(project, context); + const actual = getUpToDateStatusWorker(project); context.projectStatus.setValue(project.options.configFilePath, actual); return actual; } - function getUpToDateStatusWorker(project: ParsedCommandLine, context: BuildContext): UpToDateStatus { + function getAllProjectOutputs(project: ParsedCommandLine): ReadonlyArray { + if (project.options.outFile) { + return getOutFileOutputs(project); + } + else { + const outputs: string[] = []; + for (const inputFile of project.fileNames) { + (outputs as string[]).push(...getOutputFileNames(inputFile, project)); + } + return outputs; + } + } + + function getUpToDateStatusWorker(project: ParsedCommandLine): UpToDateStatus { let newestInputFileName: string = undefined!; let newestInputFileTime = minimumDate; // Get timestamps of input files @@ -338,7 +436,7 @@ namespace ts { }; } - const inputTime = sys.getModifiedTime(inputFile); + const inputTime = host.getModifiedTime(inputFile); if (inputTime > newestInputFileTime) { newestInputFileName = inputFile; newestInputFileTime = inputTime; @@ -346,21 +444,12 @@ namespace ts { } // Collect the expected outputs of this project - let outputs: ReadonlyArray; - if (project.options.outFile) { - outputs = getOutFileOutputs(project); - } - else { - outputs = []; - for (const inputFile of project.fileNames) { - (outputs as string[]).push(...getOutputFileNames(inputFile, project)); - } - } + const outputs = getAllProjectOutputs(project); // Now see if all outputs are newer than the newest input let oldestOutputFileName: string = undefined!; - let oldestOutputFileTime: Date = minimumDate; - let newestOutputFileTime: Date = maximumDate; + let oldestOutputFileTime: Date = maximumDate; + let newestOutputFileTime: Date = minimumDate; let newestDeclarationFileContentChangedTime: Date = minimumDate; for (const output of outputs) { // Output is missing @@ -371,7 +460,7 @@ namespace ts { }; } - const outputTime = sys.getModifiedTime(output); + const outputTime = host.getModifiedTime(output); // If an output is older than the newest input, we can stop checking if (outputTime < newestInputFileTime) { return { @@ -391,13 +480,13 @@ namespace ts { // In addition to file timestamps, we also keep track of when a .d.ts file // had its file touched but not had its contents changed - this allows us // to skip a downstream typecheck - if (fileExtensionIs(output, ".d.ts")) { + if (isDeclarationFile(output)) { const unchangedTime = context.unchangedOutputs.getValueOrUndefined(output); if (unchangedTime !== undefined) { newestDeclarationFileContentChangedTime = newer(unchangedTime, newestDeclarationFileContentChangedTime); } else { - newestDeclarationFileContentChangedTime = newer(newestDeclarationFileContentChangedTime, sys.getModifiedTime(output)); + newestDeclarationFileContentChangedTime = newer(newestDeclarationFileContentChangedTime, host.getModifiedTime(output)); } } } @@ -405,36 +494,40 @@ namespace ts { let pseudoUpToDate = false; // By here, we know the project is at least up-to-date with its own inputs. // See if any of its upstream projects are newer than it - for (const ref of project.projectReferences) { - const refStatus = getUpToDateStatus(configFileCache.parseConfigFile(ref.path), context); + if (project.projectReferences) { + for (const ref of project.projectReferences) { + const resolvedRef = resolveProjectReferencePath(host, ref); + const refStatus = getUpToDateStatus(configFileCache.parseConfigFile(resolvedRef), context); - // If the upstream project is out of date, then so are we (someone shouldn't have asked, though?) - if (refStatus.type !== UpToDateStatusType.UpToDate) { + // If the upstream project is out of date, then so are we (someone shouldn't have asked, though?) + if (refStatus.type !== UpToDateStatusType.UpToDate) { + return { + type: UpToDateStatusType.UpstreamOutOfDate, + upstreamProjectName: ref.path + }; + } + + // If the upstream project's newest file is older than our oldest output, we + // can't be out of date because of it + if (refStatus.newestInputFileTime < oldestOutputFileTime) { + continue; + } + + // If the upstream project has only change .d.ts files, and we've built + // *after* those files, then we're "psuedo up to date" and eligible for a fast rebuild + if (refStatus.newestDeclarationFileContentChangedTime < oldestOutputFileTime) { + pseudoUpToDate = true; + continue; + } + + // We have an output older than an upstream output - we are out of date + Debug.assert(oldestOutputFileName !== undefined, "Should have an oldest output filename here"); return { - type: UpToDateStatusType.UpstreamOutOfDate, - upstreamProjectName: ref.path + type: UpToDateStatusType.OutOfDateWithUpstream, + outOfDateOutputFileName: oldestOutputFileName, + newerProjectName: ref.path }; } - - // If the upstream project's newest file is older than our oldest output, we - // can't be out of date because of it - if (refStatus.newestInputFileTime < oldestOutputFileTime) { - continue; - } - - // If the upstream project has only change .d.ts files, and we've built - // *after* those files, then we're "psuedo up to date" and eligible for a fast rebuild - if (refStatus.newestDeclarationFileContentChangedTime < oldestOutputFileTime) { - pseudoUpToDate = true; - continue; - } - - // We have an output older than an upstream output - we are out of date - return { - type: UpToDateStatusType.OutOfDateWithUpstream, - outOfDateOutputFileName: oldestOutputFileName, - newerProjectName: ref.path - }; } // Up to date @@ -446,6 +539,7 @@ namespace ts { }; } + // TODO: Use the better algorithm function createDependencyGraph(roots: string[]): DependencyGraph { // This is a list of list of projects that need to be built. // The ordering here is "backwards", i.e. the first entry in the array is the last set of projects that need to be built; @@ -480,10 +574,11 @@ namespace ts { if (refs === undefined) return; buildQueuePosition++; for (const ref of refs) { - dependencyMap.addReference(fileName, ref.path); - const resolvedRef = configFileCache.parseConfigFile(ref.path); + const actualPath = resolveProjectReferencePath(host, ref); + dependencyMap.addReference(fileName, actualPath); + const resolvedRef = configFileCache.parseConfigFile(actualPath); if (resolvedRef === undefined) continue; - enumerateReferences(normalizePath(ref.path), resolvedRef); + enumerateReferences(normalizePath(actualPath), resolvedRef); } buildQueuePosition--; } @@ -507,7 +602,14 @@ namespace ts { } } - function buildSingleProject(proj: string, context: BuildContext) { + // TODO Accept parsedCommandLine + function buildSingleProject(proj: string) { + if (context.options.dry) { + diagReporter(createCompilerDiagnostic(Diagnostics.Would_build_project_0, proj)); + } + + context.verbose(Diagnostics.Building_project_0, proj); + let resultFlags = BuildResultFlags.None; resultFlags |= BuildResultFlags.DeclarationOutputUnchanged; @@ -549,8 +651,8 @@ namespace ts { for (const diag of declDiagnostics) { diagReporter(diag); } + return resultFlags; } - return resultFlags; } const semanticDiagnostics = [...program.getSemanticDiagnostics()]; @@ -562,47 +664,160 @@ namespace ts { return resultFlags; } + let newestDeclarationFileContentChangedTime = minimumDate; program.emit(undefined, (fileName, content, writeBom, onError) => { let priorChangeTime: Date | undefined; if (isDeclarationFile(fileName) && host.fileExists(fileName)) { if (host.readFile(fileName) === content) { + // Check for unchanged .d.ts files resultFlags &= ~BuildResultFlags.DeclarationOutputUnchanged; - priorChangeTime = host.getLastWriteTime && host.getLastWriteTime(fileName); + priorChangeTime = host.getModifiedTime && host.getModifiedTime(fileName); } } host.writeFile(fileName, content, writeBom, onError, emptyArray); if (priorChangeTime !== undefined) { + newestDeclarationFileContentChangedTime = newer(priorChangeTime, newestDeclarationFileContentChangedTime); context.unchangedOutputs.setValue(fileName, priorChangeTime); } }); + context.projectStatus.setValue(proj, { type: UpToDateStatusType.UpToDate, newestDeclarationFileContentChangedTime } as UpToDateStatus); + return resultFlags; } - function buildProjects(configFileNames: string[], context: BuildContext) { + function updateOutputTimestamps(proj: ParsedCommandLine) { + if (context.options.dry) { + diagReporter(createCompilerDiagnostic(Diagnostics.Would_build_project_0, proj.options.configFilePath)); + return; + } + + context.verbose(Diagnostics.Updating_output_timestamps_of_project_0, proj.options.configFilePath); + const now = new Date(); + const outputs = getAllProjectOutputs(proj); + let priorNewestUpdateTime = minimumDate; + for (const file of outputs) { + if (isDeclarationFile(file)) { + priorNewestUpdateTime = newer(priorNewestUpdateTime, host.getModifiedTime(file)); + } + host.setModifiedTime(file, now); + } + + context.projectStatus.setValue(proj.options.configFilePath, { type: UpToDateStatusType.UpToDate, newestDeclarationFileContentChangedTime: priorNewestUpdateTime } as UpToDateStatus); + } + + function cleanProjects(configFileNames: string[]) { + // Get the same graph for cleaning we'd use for building + const graph = createDependencyGraph(configFileNames); + + const fileReport: string[] = []; + for (const level of graph.buildQueue) { + for (const proj of level) { + const parsed = configFileCache.parseConfigFile(proj); + const outputs = getAllProjectOutputs(parsed); + for (const output of outputs) { + if (host.fileExists(output)) { + if (context.options.dry) { + fileReport.push(output); + } + else { + host.deleteFile(output); + } + } + } + } + } + + if (context.options.dry) { + diagReporter(createCompilerDiagnostic(Diagnostics.Would_delete_the_following_files_Colon_0, fileReport.map(f => `\r\n * ${f}`).join(""))); + } + } + + function buildProjects(configFileNames: string[]) { // Establish what needs to be built const graph = createDependencyGraph(configFileNames); const queue = graph.buildQueue; - while (queue.length > 0) { - const next = queue[0].pop()!; + reportBuildQueue(graph); - const result = buildSingleProject(next, context); + let next: string; + while (next = getNext()) { + const proj = configFileCache.parseConfigFile(next); + const status = getUpToDateStatus(proj, context); + reportProjectStatus(next, status); + + if (status.type === UpToDateStatusType.UpToDate && !context.options.force) { + // Up to date, skip + continue; + } + + if (status.type === UpToDateStatusType.UpToDateWithUpstreamTypes && !context.options.force) { + // Fake build + updateOutputTimestamps(proj); + continue; + } + + const result = buildSingleProject(next); if (result & BuildResultFlags.AnyErrors) { break; } + } - if (queue[0].length === 0) { - queue.pop(); + function getNext(): string | undefined { + if (queue.length === 0) { + return undefined; } + while (queue.length > 0) { + const last = queue[queue.length - 1]; + if (last.length === 0) { + queue.pop(); + continue; + } + return last.pop()!; + } + return undefined; } } - return { - getUpToDateStatus, - buildProjects - }; + function reportBuildQueue(graph: DependencyGraph) { + if (!context.options.verbose) return; + + const names: string[] = []; + for (const level of graph.buildQueue) { + for (const el of level) { + names.push(el); + } + } + names.reverse(); + context.verbose(Diagnostics.Sorted_list_of_input_projects_Colon_0, names.map(s => "\r\n * " + s).join("")); + } + + function reportProjectStatus(configFileName: string, status: UpToDateStatus) { + if (!context.options.verbose) return; + switch (status.type) { + case UpToDateStatusType.OutOfDateWithSelf: + context.verbose(Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, configFileName, status.outOfDateOutputFileName, status.newerInputFileName); + return; + case UpToDateStatusType.OutOfDateWithUpstream: + context.verbose(Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, configFileName, status.outOfDateOutputFileName, status.newerProjectName); + return; + case UpToDateStatusType.OutputMissing: + context.verbose(Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, configFileName, status.missingOutputFileName); + return; + case UpToDateStatusType.UpToDate: + context.verbose(Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, configFileName, status.newestDeclarationFileContentChangedTime as any, status.newestOutputFileTime); + return; + case UpToDateStatusType.UpToDateWithUpstreamTypes: + context.verbose(Diagnostics.Project_0_is_up_to_date_with_its_upstream_types, configFileName); + return; + case UpToDateStatusType.UpstreamOutOfDate: + context.verbose(Diagnostics.Project_0_is_up_to_date_with_its_upstream_types, configFileName); + return; + default: + throw new Error(`Invalid build status - ${UpToDateStatusType[status.type]}`); + } + } } } diff --git a/src/compiler/tsc.ts b/src/compiler/tsc.ts index 7617fc62cb6..51f3db7d616 100644 --- a/src/compiler/tsc.ts +++ b/src/compiler/tsc.ts @@ -47,6 +47,10 @@ namespace ts { } export function executeCommandLine(args: string[]): void { + if ((args[0].toLowerCase() === "--build") || (args[0].toLowerCase() === "-b")) { + return performBuild(args.slice(1)); + } + const commandLine = parseCommandLine(args); // Configuration file name (if any) diff --git a/src/compiler/tsconfig.json b/src/compiler/tsconfig.json index 0db027d555f..2bf59003dfd 100644 --- a/src/compiler/tsconfig.json +++ b/src/compiler/tsconfig.json @@ -46,7 +46,7 @@ "resolutionCache.ts", "watch.ts", "commandLineParser.ts", + "tsbuild.ts", "tsc.ts", - "tsbuild.ts" ] } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 3da916244d6..1275a8ef312 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -4769,7 +4769,9 @@ namespace ts { /* @internal */ hasChangedAutomaticTypeDirectiveNames?: boolean; createHash?(data: string): string; - getLastWriteTime?(fileName: string): Date; + getModifiedTime?(fileName: string): Date; + setModifiedTime?(fileName: string, date: Date): void; + deleteFile?(fileName: string): void; } /* @internal */ From 6d04378e905d7d55d51011b40d5b4c5b331f657c Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Mon, 21 May 2018 19:40:09 -0700 Subject: [PATCH 07/81] Testing WIP --- src/compiler/sys.ts | 2 + src/compiler/tsbuild.ts | 68 +++++++++++++-------- src/compiler/tsc.ts | 4 +- src/harness/fakes.ts | 12 ++++ src/harness/tsconfig.json | 1 + src/harness/unittests/tsbuild.ts | 71 ++++++++++++++++++++++ src/harness/vfs.ts | 20 +++++- tests/projects/sample1/core/index.ts | 2 + tests/projects/sample1/core/tsconfig.json | 3 + tests/projects/sample1/logic/index.ts | 4 ++ tests/projects/sample1/logic/tsconfig.json | 5 ++ tests/projects/sample1/tests/index.ts | 5 ++ tests/projects/sample1/tests/tsconfig.json | 6 ++ tests/projects/sample1/ui/index.ts | 5 ++ tests/projects/sample1/ui/tsconfig.json | 5 ++ 15 files changed, 184 insertions(+), 29 deletions(-) create mode 100644 src/harness/unittests/tsbuild.ts create mode 100644 tests/projects/sample1/core/index.ts create mode 100644 tests/projects/sample1/core/tsconfig.json create mode 100644 tests/projects/sample1/logic/index.ts create mode 100644 tests/projects/sample1/logic/tsconfig.json create mode 100644 tests/projects/sample1/tests/index.ts create mode 100644 tests/projects/sample1/tests/tsconfig.json create mode 100644 tests/projects/sample1/ui/index.ts create mode 100644 tests/projects/sample1/ui/tsconfig.json diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index 6993a3b438c..6b9c0e8710c 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -1073,6 +1073,7 @@ namespace ts { _fs.utimesSync(path, time, time); } catch (e) { + return; } } @@ -1081,6 +1082,7 @@ namespace ts { return _fs.unlinkSync(path); } catch (e) { + return; } } diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index b59e6ee4e95..d9c3eb4212d 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -7,7 +7,7 @@ namespace ts { * The primary thing we track here is which files were written to, * but unchanged, because this enables fast downstream updates */ - interface BuildContext { + export interface BuildContext { options: BuildOptions; /** * Map from output file name to its pre-build timestamp @@ -299,6 +299,9 @@ namespace ts { function parseConfigFile(configFilePath: string) { const sourceFile = host.getSourceFile(configFilePath, ScriptTarget.JSON) as JsonSourceFile; + if (sourceFile === undefined) { + return undefined; + } const parsed = parseJsonSourceFileConfigFileContent(sourceFile, configParseHost, getDirectoryPath(configFilePath)); parsed.options.configFilePath = configFilePath; cache.setValue(configFilePath, parsed); @@ -322,7 +325,7 @@ namespace ts { return fileExtensionIs(fileName, ".d.ts"); } - function createBuildContext(options: BuildOptions): BuildContext { + export function createBuildContext(options: BuildOptions): BuildContext { const verboseDiag = options.verbose && createDiagnosticReporter(sys, /*pretty*/ false); return { options, @@ -334,18 +337,15 @@ namespace ts { }; } - export function performBuild(args: string[]) { - const diagReporter = createDiagnosticReporter(sys, /*pretty*/true); - const host = createCompilerHost({}); - + export function performBuild(host: CompilerHost, reportDiagnostic: DiagnosticReporter, args: string[]) { let verbose = false; let dry = false; let force = false; let clean = false; const projects: string[] = []; - for (let i = 0; i < args.length; i++) { - switch (args[i].toLowerCase()) { + for (const arg of args) { + switch (arg.toLowerCase()) { case "-v": case "--verbose": verbose = true; @@ -363,7 +363,7 @@ namespace ts { continue; } // Not a flag, parse as filename - addProject(args[i]); + addProject(arg); } if (projects.length === 0) { @@ -372,7 +372,7 @@ namespace ts { } const context = createBuildContext({ verbose, dry, force }); - const builder = createSolutionBuilder(host, context); + const builder = createSolutionBuilder(host, reportDiagnostic, context); if (clean) { builder.cleanProjects(projects); } @@ -384,15 +384,14 @@ namespace ts { const fileName = resolvePath(host.getCurrentDirectory(), projectSpecification); const refPath = resolveProjectReferencePath(host, { path: fileName }); if (!host.fileExists(refPath)) { - diagReporter(createCompilerDiagnostic(Diagnostics.File_0_does_not_exist, fileName)); + reportDiagnostic(createCompilerDiagnostic(Diagnostics.File_0_does_not_exist, fileName)); } projects.push(refPath); } } - export function createSolutionBuilder(host: CompilerHost, context: BuildContext) { - const diagReporter = createDiagnosticReporter(sys, /*pretty*/true); + export function createSolutionBuilder(host: CompilerHost, reportDiagnostic: DiagnosticReporter, context: BuildContext) { const configFileCache = createConfigFileCache(host); return { @@ -418,7 +417,7 @@ namespace ts { else { const outputs: string[] = []; for (const inputFile of project.fileNames) { - (outputs as string[]).push(...getOutputFileNames(inputFile, project)); + outputs.push(...getOutputFileNames(inputFile, project)); } return outputs; } @@ -553,7 +552,8 @@ namespace ts { for (const root of roots) { const config = configFileCache.parseConfigFile(root); if (config === undefined) { - throw new Error(`Could not parse ${root}`); + reportDiagnostic(createCompilerDiagnostic(Diagnostics.File_0_does_not_exist, root)); + continue; } enumerateReferences(normalizePath(root), config); } @@ -564,7 +564,7 @@ namespace ts { dependencyMap }; - function enumerateReferences(fileName: string, root: ts.ParsedCommandLine): void { + function enumerateReferences(fileName: string, root: ParsedCommandLine): void { const myBuildLevel = buildQueue[buildQueuePosition] = buildQueue[buildQueuePosition] || []; if (myBuildLevel.indexOf(fileName) < 0) { myBuildLevel.push(fileName); @@ -605,7 +605,7 @@ namespace ts { // TODO Accept parsedCommandLine function buildSingleProject(proj: string) { if (context.options.dry) { - diagReporter(createCompilerDiagnostic(Diagnostics.Would_build_project_0, proj)); + reportDiagnostic(createCompilerDiagnostic(Diagnostics.Would_build_project_0, proj)); } context.verbose(Diagnostics.Building_project_0, proj); @@ -627,18 +627,18 @@ namespace ts { const programOptions: CreateProgramOptions = { projectReferences: configFile.projectReferences, - host: host, + host, rootNames: configFile.fileNames, options: configFile.options }; - const program = ts.createProgram(programOptions); + const program = createProgram(programOptions); // Don't emit anything in the presence of syntactic errors or options diagnostics const syntaxDiagnostics = [...program.getOptionsDiagnostics(), ...program.getSyntacticDiagnostics()]; if (syntaxDiagnostics.length) { resultFlags |= BuildResultFlags.SyntaxErrors; for (const diag of syntaxDiagnostics) { - diagReporter(diag); + reportDiagnostic(diag); } return resultFlags; } @@ -649,7 +649,7 @@ namespace ts { if (declDiagnostics.length) { resultFlags |= BuildResultFlags.DeclarationEmitErrors; for (const diag of declDiagnostics) { - diagReporter(diag); + reportDiagnostic(diag); } return resultFlags; } @@ -659,13 +659,13 @@ namespace ts { if (semanticDiagnostics.length) { resultFlags |= BuildResultFlags.TypeErrors; for (const diag of semanticDiagnostics) { - diagReporter(diag); + reportDiagnostic(diag); } return resultFlags; } let newestDeclarationFileContentChangedTime = minimumDate; - program.emit(undefined, (fileName, content, writeBom, onError) => { + program.emit(/*targetSourceFile*/ undefined, (fileName, content, writeBom, onError) => { let priorChangeTime: Date | undefined; if (isDeclarationFile(fileName) && host.fileExists(fileName)) { @@ -690,7 +690,7 @@ namespace ts { function updateOutputTimestamps(proj: ParsedCommandLine) { if (context.options.dry) { - diagReporter(createCompilerDiagnostic(Diagnostics.Would_build_project_0, proj.options.configFilePath)); + reportDiagnostic(createCompilerDiagnostic(Diagnostics.Would_build_project_0, proj.options.configFilePath)); return; } @@ -731,13 +731,29 @@ namespace ts { } if (context.options.dry) { - diagReporter(createCompilerDiagnostic(Diagnostics.Would_delete_the_following_files_Colon_0, fileReport.map(f => `\r\n * ${f}`).join(""))); + reportDiagnostic(createCompilerDiagnostic(Diagnostics.Would_delete_the_following_files_Colon_0, fileReport.map(f => `\r\n * ${f}`).join(""))); } } function buildProjects(configFileNames: string[]) { + const resolvedNames: string[] = []; + for (const name of configFileNames) { + let fullPath = resolvePath(host.getCurrentDirectory(), name); + if (host.fileExists(fullPath)) { + resolvedNames.push(fullPath); + continue; + } + fullPath = combinePaths(fullPath, "tsconfig.json"); + if (host.fileExists(fullPath)) { + resolvedNames.push(fullPath); + continue; + } + reportDiagnostic(createCompilerDiagnostic(Diagnostics.File_0_not_found, fullPath)); + return; + } + // Establish what needs to be built - const graph = createDependencyGraph(configFileNames); + const graph = createDependencyGraph(resolvedNames); const queue = graph.buildQueue; reportBuildQueue(graph); diff --git a/src/compiler/tsc.ts b/src/compiler/tsc.ts index 51f3db7d616..ba05092c11b 100644 --- a/src/compiler/tsc.ts +++ b/src/compiler/tsc.ts @@ -48,9 +48,9 @@ namespace ts { export function executeCommandLine(args: string[]): void { if ((args[0].toLowerCase() === "--build") || (args[0].toLowerCase() === "-b")) { - return performBuild(args.slice(1)); + return performBuild(createCompilerHost({}), createDiagnosticReporter(sys), args.slice(1)); } - + const commandLine = parseCommandLine(args); // Configuration file name (if any) diff --git a/src/harness/fakes.ts b/src/harness/fakes.ts index 4fdd30c940e..f5a2861d385 100644 --- a/src/harness/fakes.ts +++ b/src/harness/fakes.ts @@ -131,6 +131,10 @@ namespace fakes { return stats ? stats.mtime : undefined; } + public setModifiedTime(path: string, time: Date) { + this.vfs.utimesSync(path, time, time); + } + public createHash(data: string): string { return data; } @@ -252,6 +256,14 @@ namespace fakes { return this.sys.directoryExists(directoryName); } + public getModifiedTime(fileName: string) { + return this.sys.getModifiedTime(fileName); + } + + public setModifiedTime(fileName: string, time: Date) { + return this.sys.setModifiedTime(fileName, time); + } + public getDirectories(path: string): string[] { return this.sys.getDirectories(path); } diff --git a/src/harness/tsconfig.json b/src/harness/tsconfig.json index 4a95cd44928..ab857eb520d 100644 --- a/src/harness/tsconfig.json +++ b/src/harness/tsconfig.json @@ -52,6 +52,7 @@ "../compiler/builder.ts", "../compiler/resolutionCache.ts", "../compiler/watch.ts", + "../compiler/tsbuild.ts", "../compiler/commandLineParser.ts", "../services/types.ts", diff --git a/src/harness/unittests/tsbuild.ts b/src/harness/unittests/tsbuild.ts new file mode 100644 index 00000000000..5bd713f377f --- /dev/null +++ b/src/harness/unittests/tsbuild.ts @@ -0,0 +1,71 @@ +/// + +namespace ts { + let currentTime = 100; + const bfs = new vfs.FileSystem(/*ignoreCase*/ false, { time }); + const lastDiagnostics: Diagnostic[] = []; + const reportDiagnostic: DiagnosticReporter = diagnostic => lastDiagnostics.push(diagnostic); + + const sampleRoot = resolvePath(__dirname, "../../tests/projects/sample1"); + loadFsMirror(bfs, sampleRoot, "/src"); + bfs.mkdirpSync("/lib"); + bfs.writeFileSync("/lib/lib.d.ts", Harness.IO.readFile(combinePaths(Harness.libFolder, "lib.d.ts"))); + bfs.meta.set("defaultLibLocation", "/lib"); + bfs.makeReadonly(); + + describe("tsbuild tests", () => { + it("builds the referenced project", () => { + const fs = bfs.shadow(); + const host = new fakes.CompilerHost(fs); + const builder = createSolutionBuilder(host, reportDiagnostic, createBuildContext({ dry: false, force: false, verbose: false })); + + fs.chdir("/src/tests"); + fs.debugPrint(); + builder.buildProjects(["."]); + printDiagnostics(); + fs.debugPrint(); + assertDiagnosticMessages(Diagnostics.File_0_does_not_exist); + + tick(); + }); + }); + + function assertDiagnosticMessages(...expected: DiagnosticMessage[]) { + const actual = lastDiagnostics.slice(); + actual.sort((a, b) => b.code - a.code); + expected.sort((a, b) => b.code - a.code); + if (actual.length !== expected.length) { + assert.fail(actual, expected, `Diagnostic arrays did not match - expected ${actual.join(",")}, got ${expected.join(",")}`); + } + for (let i = 0; i < actual.length; i++) { + if (actual[i].code !== expected[i].code) { + assert.fail(actual[i].messageText, expected[i].message, "Mismatched error code"); + } + } + } + + export function printDiagnostics() { + const out = createDiagnosticReporter(sys); + for (const d of lastDiagnostics) { + out(d); + } + } + + function tick() { + currentTime += 10; + } + function time() { + return currentTime; + } + + function loadFsMirror(vfs: vfs.FileSystem, localRoot: string, virtualRoot: string) { + vfs.mkdirpSync(virtualRoot); + for (const path of Harness.IO.readDirectory(localRoot)) { + const file = getBaseFileName(path); + vfs.writeFileSync(virtualRoot + "/" + file, Harness.IO.readFile(localRoot + "/" + file)); + } + for (const dir of Harness.IO.getDirectories(localRoot)){ + loadFsMirror(vfs, localRoot + "/" + dir, virtualRoot + "/" + dir); + } + } +} \ No newline at end of file diff --git a/src/harness/vfs.ts b/src/harness/vfs.ts index 34a48f59091..f05a47ad64c 100644 --- a/src/harness/vfs.ts +++ b/src/harness/vfs.ts @@ -5,6 +5,11 @@ namespace vfs { */ export const builtFolder = "/.ts"; + /** + * Posix-style path to additional mountable folders (./tests/projects in this repo) + */ + export const projectsFolder = "/.projects"; + /** * Posix-style path to additional test libraries */ @@ -404,7 +409,18 @@ namespace vfs { } /** - * Get file status. + * Change file access times + * + * NOTE: do not rename this method as it is intended to align with the same named export of the "fs" module. + */ + public utimesSync(path: string, atime: Date, mtime: Date) { + const entry = this._walk(this._resolve(path)); + entry.node.atimeMs = +atime; + entry.node.mtimeMs = +mtime; + } + + /** + * Get file status. If `path` is a symbolic link, it is dereferenced. * * @link http://pubs.opengroup.org/onlinepubs/9699919799/functions/lstat.html * @@ -414,6 +430,7 @@ namespace vfs { return this._stat(this._walk(this._resolve(path), /*noFollow*/ true)); } + private _stat(entry: WalkResult) { const node = entry.node; if (!node) throw createIOError("ENOENT"); @@ -1282,6 +1299,7 @@ namespace vfs { files: { [builtFolder]: new Mount(vpath.resolve(host.getWorkspaceRoot(), "built/local"), resolver), [testLibFolder]: new Mount(vpath.resolve(host.getWorkspaceRoot(), "tests/lib"), resolver), + [projectsFolder]: new Mount(vpath.resolve(host.getWorkspaceRoot(), "tests/projects"), resolver), [srcFolder]: {} }, cwd: srcFolder, diff --git a/tests/projects/sample1/core/index.ts b/tests/projects/sample1/core/index.ts new file mode 100644 index 00000000000..9ade19f5e2e --- /dev/null +++ b/tests/projects/sample1/core/index.ts @@ -0,0 +1,2 @@ +export function leftPad(s: string, n: number) { return s + n; } +export function multiply(a: number, b: number) { return a * b; } diff --git a/tests/projects/sample1/core/tsconfig.json b/tests/projects/sample1/core/tsconfig.json new file mode 100644 index 00000000000..a514dfe8a03 --- /dev/null +++ b/tests/projects/sample1/core/tsconfig.json @@ -0,0 +1,3 @@ +{ + +} \ No newline at end of file diff --git a/tests/projects/sample1/logic/index.ts b/tests/projects/sample1/logic/index.ts new file mode 100644 index 00000000000..cccadd1718b --- /dev/null +++ b/tests/projects/sample1/logic/index.ts @@ -0,0 +1,4 @@ +import * as c from '../core'; +export function getSecondsInDay() { + return c.multiply(10, 15); +} diff --git a/tests/projects/sample1/logic/tsconfig.json b/tests/projects/sample1/logic/tsconfig.json new file mode 100644 index 00000000000..3c2056eec88 --- /dev/null +++ b/tests/projects/sample1/logic/tsconfig.json @@ -0,0 +1,5 @@ +{ + "references": [ + { "path": "../core" } + ] +} diff --git a/tests/projects/sample1/tests/index.ts b/tests/projects/sample1/tests/index.ts new file mode 100644 index 00000000000..e9a1ebde3e1 --- /dev/null +++ b/tests/projects/sample1/tests/index.ts @@ -0,0 +1,5 @@ +import * as c from '../core'; +import * as logic from '../logic'; + +c.leftPad("", 10); +logic.getSecondsInDay(); diff --git a/tests/projects/sample1/tests/tsconfig.json b/tests/projects/sample1/tests/tsconfig.json new file mode 100644 index 00000000000..dd1bf3bae6b --- /dev/null +++ b/tests/projects/sample1/tests/tsconfig.json @@ -0,0 +1,6 @@ +{ + "references": [ + { "path": "../core" }, + { "path": "../logic" } + ] +} \ No newline at end of file diff --git a/tests/projects/sample1/ui/index.ts b/tests/projects/sample1/ui/index.ts new file mode 100644 index 00000000000..9d7e7e3a89e --- /dev/null +++ b/tests/projects/sample1/ui/index.ts @@ -0,0 +1,5 @@ +import * as logic from '../logic'; + +export function run() { + console.log(logic.getSecondsInDay()); +} diff --git a/tests/projects/sample1/ui/tsconfig.json b/tests/projects/sample1/ui/tsconfig.json new file mode 100644 index 00000000000..45eff16d4d9 --- /dev/null +++ b/tests/projects/sample1/ui/tsconfig.json @@ -0,0 +1,5 @@ +{ + "references": [ + { "path": "../logic" } + ] +} From 76d2ba64df3e520a58cb58925d418c3e760793dc Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Mon, 21 May 2018 21:24:29 -0700 Subject: [PATCH 08/81] Testing WIP --- src/compiler/tsbuild.ts | 22 +++--- src/harness/unittests/tsbuild.ts | 88 ++++++++++++++++++---- tests/projects/sample1/core/index.ts | 1 + tests/projects/sample1/core/tsconfig.json | 5 +- tests/projects/sample1/logic/index.ts | 2 +- tests/projects/sample1/logic/tsconfig.json | 4 + tests/projects/sample1/tests/index.ts | 4 +- tests/projects/sample1/tests/tsconfig.json | 3 +- tests/projects/sample1/ui/tsconfig.json | 2 +- 9 files changed, 101 insertions(+), 30 deletions(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index d9c3eb4212d..34f013fa017 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -317,16 +317,12 @@ namespace ts { return date2 > date1 ? date2 : date1; } - function older(date1: Date, date2: Date): Date { - return date2 < date1 ? date2 : date1; - } - function isDeclarationFile(fileName: string) { return fileExtensionIs(fileName, ".d.ts"); } - export function createBuildContext(options: BuildOptions): BuildContext { - const verboseDiag = options.verbose && createDiagnosticReporter(sys, /*pretty*/ false); + export function createBuildContext(options: BuildOptions, reportDiagnostic: DiagnosticReporter): BuildContext { + const verboseDiag = options.verbose && reportDiagnostic; return { options, projectStatus: createFileMap(), @@ -371,8 +367,7 @@ namespace ts { addProject("."); } - const context = createBuildContext({ verbose, dry, force }); - const builder = createSolutionBuilder(host, reportDiagnostic, context); + const builder = createSolutionBuilder(host, reportDiagnostic, { verbose, dry, force }); if (clean) { builder.cleanProjects(projects); } @@ -391,8 +386,9 @@ namespace ts { } } - export function createSolutionBuilder(host: CompilerHost, reportDiagnostic: DiagnosticReporter, context: BuildContext) { + export function createSolutionBuilder(host: CompilerHost, reportDiagnostic: DiagnosticReporter, options: BuildOptions) { const configFileCache = createConfigFileCache(host); + let context: BuildContext = undefined!; return { getUpToDateStatus, @@ -473,7 +469,7 @@ namespace ts { oldestOutputFileTime = outputTime; oldestOutputFileName = output; } - newestOutputFileTime = older(newestOutputFileTime, outputTime); + newestOutputFileTime = newer(newestOutputFileTime, outputTime); // Keep track of when the most recent time a .d.ts file was changed. // In addition to file timestamps, we also keep track of when a .d.ts file @@ -709,6 +705,8 @@ namespace ts { } function cleanProjects(configFileNames: string[]) { + context = createBuildContext(options, reportDiagnostic); + // Get the same graph for cleaning we'd use for building const graph = createDependencyGraph(configFileNames); @@ -736,6 +734,8 @@ namespace ts { } function buildProjects(configFileNames: string[]) { + context = createBuildContext(options, reportDiagnostic); + const resolvedNames: string[] = []; for (const name of configFileNames) { let fullPath = resolvePath(host.getCurrentDirectory(), name); @@ -823,7 +823,7 @@ namespace ts { context.verbose(Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, configFileName, status.missingOutputFileName); return; case UpToDateStatusType.UpToDate: - context.verbose(Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, configFileName, status.newestDeclarationFileContentChangedTime as any, status.newestOutputFileTime); + context.verbose(Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, configFileName, status.newestInputFileTime, status.newestOutputFileTime); return; case UpToDateStatusType.UpToDateWithUpstreamTypes: context.verbose(Diagnostics.Project_0_is_up_to_date_with_its_upstream_types, configFileName); diff --git a/src/harness/unittests/tsbuild.ts b/src/harness/unittests/tsbuild.ts index 5bd713f377f..809d7bb1b0b 100644 --- a/src/harness/unittests/tsbuild.ts +++ b/src/harness/unittests/tsbuild.ts @@ -3,7 +3,7 @@ namespace ts { let currentTime = 100; const bfs = new vfs.FileSystem(/*ignoreCase*/ false, { time }); - const lastDiagnostics: Diagnostic[] = []; + let lastDiagnostics: Diagnostic[] = []; const reportDiagnostic: DiagnosticReporter = diagnostic => lastDiagnostics.push(diagnostic); const sampleRoot = resolvePath(__dirname, "../../tests/projects/sample1"); @@ -12,47 +12,109 @@ namespace ts { bfs.writeFileSync("/lib/lib.d.ts", Harness.IO.readFile(combinePaths(Harness.libFolder, "lib.d.ts"))); bfs.meta.set("defaultLibLocation", "/lib"); bfs.makeReadonly(); + tick(); describe("tsbuild tests", () => { - it("builds the referenced project", () => { + it("can build the sample project 'tests' without error", () => { const fs = bfs.shadow(); const host = new fakes.CompilerHost(fs); - const builder = createSolutionBuilder(host, reportDiagnostic, createBuildContext({ dry: false, force: false, verbose: false })); + const builder = createSolutionBuilder(host, reportDiagnostic, { dry: false, force: false, verbose: false }); fs.chdir("/src/tests"); - fs.debugPrint(); builder.buildProjects(["."]); - printDiagnostics(); - fs.debugPrint(); - assertDiagnosticMessages(Diagnostics.File_0_does_not_exist); + assertDiagnosticMessages(/*empty*/); + }); + it("can detect when and what to rebuild", () => { + const fs = bfs.shadow(); + const host = new fakes.CompilerHost(fs); + const builder = createSolutionBuilder(host, reportDiagnostic, { dry: false, force: false, verbose: true }); + + fs.chdir("/src/tests"); + builder.buildProjects(["."]); + assertDiagnosticMessages(Diagnostics.Sorted_list_of_input_projects_Colon_0, + Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, + Diagnostics.Building_project_0, + Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, + Diagnostics.Building_project_0, + Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, + Diagnostics.Building_project_0); tick(); + + // All three projects are up to date + clearDiagnostics(); + builder.buildProjects(["."]); + assertDiagnosticMessages(Diagnostics.Sorted_list_of_input_projects_Colon_0, + Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, + Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, + Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2); + tick(); + + // Update a file in the leaf node (tests), only it should rebuild the last one + clearDiagnostics(); + fs.writeFileSync("/src/tests/index.ts", "const m = 10;"); + builder.buildProjects(["."]); + + assertDiagnosticMessages(Diagnostics.Sorted_list_of_input_projects_Colon_0, + Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, + Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, + Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, + Diagnostics.Building_project_0); + tick(); + + // Update a file in the parent (without affecting types), should get fast downstream builds + clearDiagnostics(); + replaceText(fs, "/src/core/index.ts", "HELLO WORLD", "WELCOME PLANET"); + builder.buildProjects(["."]); + + assertDiagnosticMessages(Diagnostics.Sorted_list_of_input_projects_Colon_0, + Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, + Diagnostics.Building_project_0, + Diagnostics.Project_0_is_up_to_date_with_its_upstream_types, + Diagnostics.Updating_output_timestamps_of_project_0, + Diagnostics.Project_0_is_up_to_date_with_its_upstream_types, + Diagnostics.Updating_output_timestamps_of_project_0); }); }); + function replaceText(fs: vfs.FileSystem, path: string, oldText: string, newText: string) { + if (!fs.statSync(path).isFile()) { + throw new Error(`File ${path} does not exist`); + } + const old = fs.readFileSync(path, 'utf-8'); + if (old.indexOf(oldText) < 0) { + throw new Error(`Text "${oldText}" does not exist in file ${path}`); + } + const newContent = old.replace(oldText, newText); + fs.writeFileSync(path, newContent, 'utf-8'); + } + function assertDiagnosticMessages(...expected: DiagnosticMessage[]) { const actual = lastDiagnostics.slice(); - actual.sort((a, b) => b.code - a.code); - expected.sort((a, b) => b.code - a.code); if (actual.length !== expected.length) { - assert.fail(actual, expected, `Diagnostic arrays did not match - expected ${actual.join(",")}, got ${expected.join(",")}`); + assert.fail(actual, expected, `Diagnostic arrays did not match - expected\r\n${actual.map(a => " " + a.messageText).join("\r\n")}\r\ngot\r\n${expected.map(e => " " + e.message).join("\r\n")}`); } for (let i = 0; i < actual.length; i++) { if (actual[i].code !== expected[i].code) { - assert.fail(actual[i].messageText, expected[i].message, "Mismatched error code"); + assert.fail(actual[i].messageText, expected[i].message, `Mismatched error code - expected diagnostic ${i} "${actual[i].messageText}" to match ${expected[i].message}`); } } } - export function printDiagnostics() { + function clearDiagnostics() { + lastDiagnostics = []; + } + + export function printDiagnostics(header = "== Diagnostics ==") { const out = createDiagnosticReporter(sys); + sys.write(header + "\r\n"); for (const d of lastDiagnostics) { out(d); } } function tick() { - currentTime += 10; + currentTime += 100000; } function time() { return currentTime; diff --git a/tests/projects/sample1/core/index.ts b/tests/projects/sample1/core/index.ts index 9ade19f5e2e..529a7f549ec 100644 --- a/tests/projects/sample1/core/index.ts +++ b/tests/projects/sample1/core/index.ts @@ -1,2 +1,3 @@ +export const someString: string = "HELLO WORLD"; export function leftPad(s: string, n: number) { return s + n; } export function multiply(a: number, b: number) { return a * b; } diff --git a/tests/projects/sample1/core/tsconfig.json b/tests/projects/sample1/core/tsconfig.json index a514dfe8a03..b8332f5c476 100644 --- a/tests/projects/sample1/core/tsconfig.json +++ b/tests/projects/sample1/core/tsconfig.json @@ -1,3 +1,6 @@ { - + "compilerOptions": { + "composite": true, + "declaration": true + } } \ No newline at end of file diff --git a/tests/projects/sample1/logic/index.ts b/tests/projects/sample1/logic/index.ts index cccadd1718b..fd6b2106bb8 100644 --- a/tests/projects/sample1/logic/index.ts +++ b/tests/projects/sample1/logic/index.ts @@ -1,4 +1,4 @@ -import * as c from '../core'; +import * as c from '../core/index'; export function getSecondsInDay() { return c.multiply(10, 15); } diff --git a/tests/projects/sample1/logic/tsconfig.json b/tests/projects/sample1/logic/tsconfig.json index 3c2056eec88..a58b3a9f48e 100644 --- a/tests/projects/sample1/logic/tsconfig.json +++ b/tests/projects/sample1/logic/tsconfig.json @@ -1,4 +1,8 @@ { + "compilerOptions": { + "composite": true, + "declaration": true + }, "references": [ { "path": "../core" } ] diff --git a/tests/projects/sample1/tests/index.ts b/tests/projects/sample1/tests/index.ts index e9a1ebde3e1..f89dcd08a82 100644 --- a/tests/projects/sample1/tests/index.ts +++ b/tests/projects/sample1/tests/index.ts @@ -1,5 +1,5 @@ -import * as c from '../core'; -import * as logic from '../logic'; +import * as c from '../core/index'; +import * as logic from '../logic/index'; c.leftPad("", 10); logic.getSecondsInDay(); diff --git a/tests/projects/sample1/tests/tsconfig.json b/tests/projects/sample1/tests/tsconfig.json index dd1bf3bae6b..437d8ca6fb3 100644 --- a/tests/projects/sample1/tests/tsconfig.json +++ b/tests/projects/sample1/tests/tsconfig.json @@ -2,5 +2,6 @@ "references": [ { "path": "../core" }, { "path": "../logic" } - ] + ], + "files": ["index.ts"] } \ No newline at end of file diff --git a/tests/projects/sample1/ui/tsconfig.json b/tests/projects/sample1/ui/tsconfig.json index 45eff16d4d9..d843e35c549 100644 --- a/tests/projects/sample1/ui/tsconfig.json +++ b/tests/projects/sample1/ui/tsconfig.json @@ -1,5 +1,5 @@ { "references": [ - { "path": "../logic" } + { "path": "../logic/index" } ] } From 5a9a88320a4438a726b999c5d5ed0e77565c3437 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Tue, 22 May 2018 00:15:27 -0700 Subject: [PATCH 09/81] WIP --- src/compiler/tsbuild.ts | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index 34f013fa017..ac2462b2c07 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -388,15 +388,25 @@ namespace ts { export function createSolutionBuilder(host: CompilerHost, reportDiagnostic: DiagnosticReporter, options: BuildOptions) { const configFileCache = createConfigFileCache(host); - let context: BuildContext = undefined!; + let context = createBuildContext(options, reportDiagnostic); return { getUpToDateStatus, + getUpToDateStatusOfFile, buildProjects, - cleanProjects + cleanProjects, + resetBuildContext }; - function getUpToDateStatus(project: ParsedCommandLine, context: BuildContext): UpToDateStatus { + function resetBuildContext() { + context = createBuildContext(options, reportDiagnostic); + } + + function getUpToDateStatusOfFile(configFileName: string): UpToDateStatus { + return getUpToDateStatus(configFileCache.parseConfigFile(configFileName)); + } + + function getUpToDateStatus(project: ParsedCommandLine): UpToDateStatus { const prior = context.projectStatus.getValueOrUndefined(project.options.configFilePath); if (prior !== undefined) { return prior; @@ -705,8 +715,6 @@ namespace ts { } function cleanProjects(configFileNames: string[]) { - context = createBuildContext(options, reportDiagnostic); - // Get the same graph for cleaning we'd use for building const graph = createDependencyGraph(configFileNames); @@ -734,8 +742,6 @@ namespace ts { } function buildProjects(configFileNames: string[]) { - context = createBuildContext(options, reportDiagnostic); - const resolvedNames: string[] = []; for (const name of configFileNames) { let fullPath = resolvePath(host.getCurrentDirectory(), name); From 18f2baa88bdb1728cc5428285ec56d8eada4186e Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Tue, 22 May 2018 10:16:24 -0700 Subject: [PATCH 10/81] Fix errors --- src/compiler/tsbuild.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index ac2462b2c07..b157a1a4fe4 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -502,7 +502,7 @@ namespace ts { if (project.projectReferences) { for (const ref of project.projectReferences) { const resolvedRef = resolveProjectReferencePath(host, ref); - const refStatus = getUpToDateStatus(configFileCache.parseConfigFile(resolvedRef), context); + const refStatus = getUpToDateStatus(configFileCache.parseConfigFile(resolvedRef)); // If the upstream project is out of date, then so are we (someone shouldn't have asked, though?) if (refStatus.type !== UpToDateStatusType.UpToDate) { @@ -767,7 +767,7 @@ namespace ts { let next: string; while (next = getNext()) { const proj = configFileCache.parseConfigFile(next); - const status = getUpToDateStatus(proj, context); + const status = getUpToDateStatus(proj); reportProjectStatus(next, status); if (status.type === UpToDateStatusType.UpToDate && !context.options.force) { From fa07830ca9d970f6695b4aa210c9bace26be7993 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Tue, 22 May 2018 12:53:31 -0700 Subject: [PATCH 11/81] Passing tests --- src/compiler/diagnosticMessages.json | 4 +++ src/compiler/tsbuild.ts | 11 +++++-- src/harness/unittests/tsbuild.ts | 44 ++++++++++++++++++---------- 3 files changed, 40 insertions(+), 19 deletions(-) diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 1077db69ef7..8f1da78f1e9 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -3630,6 +3630,10 @@ "category": "Message", "code": 6358 }, + "Project '{0}' is up to date because it was previously built": { + "category": "Message", + "code": 6359 + }, "Variable '{0}' implicitly has an '{1}' type.": { "category": "Error", diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index b157a1a4fe4..10ed5280f71 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -514,13 +514,13 @@ namespace ts { // If the upstream project's newest file is older than our oldest output, we // can't be out of date because of it - if (refStatus.newestInputFileTime < oldestOutputFileTime) { + if (refStatus.newestInputFileTime <= oldestOutputFileTime) { continue; } // If the upstream project has only change .d.ts files, and we've built // *after* those files, then we're "psuedo up to date" and eligible for a fast rebuild - if (refStatus.newestDeclarationFileContentChangedTime < oldestOutputFileTime) { + if (refStatus.newestDeclarationFileContentChangedTime <= oldestOutputFileTime) { pseudoUpToDate = true; continue; } @@ -829,7 +829,12 @@ namespace ts { context.verbose(Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, configFileName, status.missingOutputFileName); return; case UpToDateStatusType.UpToDate: - context.verbose(Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, configFileName, status.newestInputFileTime, status.newestOutputFileTime); + if (status.newestInputFileTime !== undefined) { + context.verbose(Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, configFileName, status.newestInputFileTime, status.newestOutputFileTime); + } + else { + context.verbose(Diagnostics.Project_0_is_up_to_date_because_it_was_previously_built, configFileName); + } return; case UpToDateStatusType.UpToDateWithUpstreamTypes: context.verbose(Diagnostics.Project_0_is_up_to_date_with_its_upstream_types, configFileName); diff --git a/src/harness/unittests/tsbuild.ts b/src/harness/unittests/tsbuild.ts index 809d7bb1b0b..5ac4e19e769 100644 --- a/src/harness/unittests/tsbuild.ts +++ b/src/harness/unittests/tsbuild.ts @@ -1,5 +1,3 @@ -/// - namespace ts { let currentTime = 100; const bfs = new vfs.FileSystem(/*ignoreCase*/ false, { time }); @@ -14,8 +12,8 @@ namespace ts { bfs.makeReadonly(); tick(); - describe("tsbuild tests", () => { - it("can build the sample project 'tests' without error", () => { + describe("tsbuild - sanity check of clean build of 'sample1' project", () => { + it("can build the sample project 'sample1' without error", () => { const fs = bfs.shadow(); const host = new fakes.CompilerHost(fs); const builder = createSolutionBuilder(host, reportDiagnostic, { dry: false, force: false, verbose: false }); @@ -24,13 +22,17 @@ namespace ts { builder.buildProjects(["."]); assertDiagnosticMessages(/*empty*/); }); + }); - it("can detect when and what to rebuild", () => { - const fs = bfs.shadow(); - const host = new fakes.CompilerHost(fs); - const builder = createSolutionBuilder(host, reportDiagnostic, { dry: false, force: false, verbose: true }); + describe("tsbuild - can detect when and what to rebuild", () => { + const fs = bfs.shadow(); + const host = new fakes.CompilerHost(fs); + const builder = createSolutionBuilder(host, reportDiagnostic, { dry: false, force: false, verbose: true }); - fs.chdir("/src/tests"); + fs.chdir("/src/tests"); + + it("Builds the project", () => { + builder.resetBuildContext(); builder.buildProjects(["."]); assertDiagnosticMessages(Diagnostics.Sorted_list_of_input_projects_Colon_0, Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, @@ -40,19 +42,25 @@ namespace ts { Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, Diagnostics.Building_project_0); tick(); + }); - // All three projects are up to date + // All three projects are up to date + it("Detects that all projects are up to date", () => { clearDiagnostics(); + builder.resetBuildContext(); builder.buildProjects(["."]); assertDiagnosticMessages(Diagnostics.Sorted_list_of_input_projects_Colon_0, Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2); tick(); + }); - // Update a file in the leaf node (tests), only it should rebuild the last one + // Update a file in the leaf node (tests), only it should rebuild the last one + it("Only builds the leaf node project", () => { clearDiagnostics(); fs.writeFileSync("/src/tests/index.ts", "const m = 10;"); + builder.resetBuildContext(); builder.buildProjects(["."]); assertDiagnosticMessages(Diagnostics.Sorted_list_of_input_projects_Colon_0, @@ -61,10 +69,13 @@ namespace ts { Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, Diagnostics.Building_project_0); tick(); + }); - // Update a file in the parent (without affecting types), should get fast downstream builds + // Update a file in the parent (without affecting types), should get fast downstream builds + it("Detects type-only changes in upstream projects", () => { clearDiagnostics(); replaceText(fs, "/src/core/index.ts", "HELLO WORLD", "WELCOME PLANET"); + builder.resetBuildContext(); builder.buildProjects(["."]); assertDiagnosticMessages(Diagnostics.Sorted_list_of_input_projects_Colon_0, @@ -81,12 +92,12 @@ namespace ts { if (!fs.statSync(path).isFile()) { throw new Error(`File ${path} does not exist`); } - const old = fs.readFileSync(path, 'utf-8'); + const old = fs.readFileSync(path, "utf-8"); if (old.indexOf(oldText) < 0) { throw new Error(`Text "${oldText}" does not exist in file ${path}`); } const newContent = old.replace(oldText, newText); - fs.writeFileSync(path, newContent, 'utf-8'); + fs.writeFileSync(path, newContent, "utf-8"); } function assertDiagnosticMessages(...expected: DiagnosticMessage[]) { @@ -114,8 +125,9 @@ namespace ts { } function tick() { - currentTime += 100000; + currentTime += 60_000; } + function time() { return currentTime; } @@ -126,7 +138,7 @@ namespace ts { const file = getBaseFileName(path); vfs.writeFileSync(virtualRoot + "/" + file, Harness.IO.readFile(localRoot + "/" + file)); } - for (const dir of Harness.IO.getDirectories(localRoot)){ + for (const dir of Harness.IO.getDirectories(localRoot)) { loadFsMirror(vfs, localRoot + "/" + dir, virtualRoot + "/" + dir); } } From 5a664be278ed6213d7ff54a4515ca6a130f4a8eb Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Tue, 22 May 2018 13:34:52 -0700 Subject: [PATCH 12/81] Baseline accept --- tests/baselines/reference/api/tsserverlibrary.d.ts | 9 +++++++++ tests/baselines/reference/api/typescript.d.ts | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index c069dbf4d04..3a7fe516e59 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -2630,6 +2630,9 @@ declare namespace ts { resolveTypeReferenceDirectives?(typeReferenceDirectiveNames: string[], containingFile: string): (ResolvedTypeReferenceDirective | undefined)[]; getEnvironmentVariable?(name: string): string; createHash?(data: string): string; + getModifiedTime?(fileName: string): Date; + setModifiedTime?(fileName: string, date: Date): void; + deleteFile?(fileName: string): void; } interface SourceMapRange extends TextRange { source?: SourceMapSource; @@ -2979,6 +2982,8 @@ declare namespace ts { getDirectories(path: string): string[]; readDirectory(path: string, extensions?: ReadonlyArray, exclude?: ReadonlyArray, include?: ReadonlyArray, depth?: number): string[]; getModifiedTime?(path: string): Date; + setModifiedTime?(path: string, time: Date): void; + deleteFile?(path: string): void; /** * This should be cryptographically secure. * A good implementation is node.js' `crypto.createHash`. (https://nodejs.org/api/crypto.html#crypto_crypto_createhash_algorithm) @@ -4003,6 +4008,10 @@ declare namespace ts { */ function createProgram(createProgramOptions: CreateProgramOptions): Program; function createProgram(rootNames: ReadonlyArray, options: CompilerOptions, host?: CompilerHost, oldProgram?: Program, configFileParsingDiagnostics?: ReadonlyArray): Program; + /** + * Returns the target config filename of a project reference + */ + function resolveProjectReferencePath(host: CompilerHost, ref: ProjectReference): string | undefined; } declare namespace ts { interface EmitOutput { diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 4887de7fd2f..4fc382b5d2b 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -2630,6 +2630,9 @@ declare namespace ts { resolveTypeReferenceDirectives?(typeReferenceDirectiveNames: string[], containingFile: string): (ResolvedTypeReferenceDirective | undefined)[]; getEnvironmentVariable?(name: string): string; createHash?(data: string): string; + getModifiedTime?(fileName: string): Date; + setModifiedTime?(fileName: string, date: Date): void; + deleteFile?(fileName: string): void; } interface SourceMapRange extends TextRange { source?: SourceMapSource; @@ -2979,6 +2982,8 @@ declare namespace ts { getDirectories(path: string): string[]; readDirectory(path: string, extensions?: ReadonlyArray, exclude?: ReadonlyArray, include?: ReadonlyArray, depth?: number): string[]; getModifiedTime?(path: string): Date; + setModifiedTime?(path: string, time: Date): void; + deleteFile?(path: string): void; /** * This should be cryptographically secure. * A good implementation is node.js' `crypto.createHash`. (https://nodejs.org/api/crypto.html#crypto_crypto_createhash_algorithm) @@ -4003,6 +4008,10 @@ declare namespace ts { */ function createProgram(createProgramOptions: CreateProgramOptions): Program; function createProgram(rootNames: ReadonlyArray, options: CompilerOptions, host?: CompilerHost, oldProgram?: Program, configFileParsingDiagnostics?: ReadonlyArray): Program; + /** + * Returns the target config filename of a project reference + */ + function resolveProjectReferencePath(host: CompilerHost, ref: ProjectReference): string | undefined; } declare namespace ts { interface EmitOutput { From d4a56b910f2dfa6571937eda4f1d24b5144dcf6a Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Tue, 22 May 2018 18:10:03 -0700 Subject: [PATCH 13/81] WIP more tests --- src/harness/unittests/tsbuild.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/harness/unittests/tsbuild.ts b/src/harness/unittests/tsbuild.ts index 5ac4e19e769..e84fa403475 100644 --- a/src/harness/unittests/tsbuild.ts +++ b/src/harness/unittests/tsbuild.ts @@ -21,6 +21,12 @@ namespace ts { fs.chdir("/src/tests"); builder.buildProjects(["."]); assertDiagnosticMessages(/*empty*/); + + // Check for outputs. Not an exhaustive list + const expectedOutputs = ["/src/tests/index.js", "/src/core/index.js", "/src/core/index.d.ts"]; + for (const output of expectedOutputs) { + assert(fs.existsSync(output), `Expect file ${output} to exist`); + } }); }); From fc22b5b146a651fa018d9d68f0ed2bf6a9eac8e1 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Thu, 24 May 2018 10:59:07 -0700 Subject: [PATCH 14/81] WIP --- src/compiler/diagnosticMessages.json | 4 ++ src/compiler/tsbuild.ts | 52 +++++++++++----- src/harness/fakes.ts | 8 +++ src/harness/unittests/tsbuild.ts | 90 +++++++++++++++++++++++++++- 4 files changed, 138 insertions(+), 16 deletions(-) diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 8f1da78f1e9..bfd261802f4 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -3634,6 +3634,10 @@ "category": "Message", "code": 6359 }, + "Project '{0}' is up to date": { + "category": "Message", + "code": 6360 + }, "Variable '{0}' implicitly has an '{1}' type.": { "category": "Error", diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index 10ed5280f71..ec9e18899aa 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -608,10 +608,11 @@ namespace ts { } } - // TODO Accept parsedCommandLine + // TODO Accept parsedCommandLine instead? function buildSingleProject(proj: string) { if (context.options.dry) { reportDiagnostic(createCompilerDiagnostic(Diagnostics.Would_build_project_0, proj)); + return; } context.verbose(Diagnostics.Building_project_0, proj); @@ -714,34 +715,47 @@ namespace ts { context.projectStatus.setValue(proj.options.configFilePath, { type: UpToDateStatusType.UpToDate, newestDeclarationFileContentChangedTime: priorNewestUpdateTime } as UpToDateStatus); } - function cleanProjects(configFileNames: string[]) { - // Get the same graph for cleaning we'd use for building - const graph = createDependencyGraph(configFileNames); + function getFilesToClean(configFileNames: string[]): string[] | undefined { + const resolvedNames: string[] | undefined = resolveProjectNames(configFileNames); + if (resolvedNames === undefined) return; - const fileReport: string[] = []; + // Get the same graph for cleaning we'd use for building + const graph = createDependencyGraph(resolvedNames); + + const filesToDelete: string[] = []; for (const level of graph.buildQueue) { for (const proj of level) { const parsed = configFileCache.parseConfigFile(proj); const outputs = getAllProjectOutputs(parsed); for (const output of outputs) { if (host.fileExists(output)) { - if (context.options.dry) { - fileReport.push(output); - } - else { - host.deleteFile(output); - } + filesToDelete.push(output); } } } } + return filesToDelete; + } + + function cleanProjects(configFileNames: string[]) { + const filesToDelete = getFilesToClean(configFileNames); if (context.options.dry) { - reportDiagnostic(createCompilerDiagnostic(Diagnostics.Would_delete_the_following_files_Colon_0, fileReport.map(f => `\r\n * ${f}`).join(""))); + reportDiagnostic(createCompilerDiagnostic(Diagnostics.Would_delete_the_following_files_Colon_0, filesToDelete.map(f => `\r\n * ${f}`).join(""))); + } + else { + if (!host.deleteFile) { + throw new Error("Host does not support deleting files"); + } + + for (const output of filesToDelete) { + host.deleteFile(output); + } } } - function buildProjects(configFileNames: string[]) { + // TODO add branding to resolved filenames + function resolveProjectNames(configFileNames: string[]): string[] | undefined { const resolvedNames: string[] = []; for (const name of configFileNames) { let fullPath = resolvePath(host.getCurrentDirectory(), name); @@ -755,8 +769,14 @@ namespace ts { continue; } reportDiagnostic(createCompilerDiagnostic(Diagnostics.File_0_not_found, fullPath)); - return; + return undefined; } + return resolvedNames; + } + + function buildProjects(configFileNames: string[]) { + const resolvedNames: string[] | undefined = resolveProjectNames(configFileNames); + if (resolvedNames === undefined) return; // Establish what needs to be built const graph = createDependencyGraph(resolvedNames); @@ -772,6 +792,10 @@ namespace ts { if (status.type === UpToDateStatusType.UpToDate && !context.options.force) { // Up to date, skip + if (options.dry) { + // In a dry build, inform the user of this fact + reportDiagnostic(createCompilerDiagnostic(Diagnostics.Project_0_is_up_to_date)); + } continue; } diff --git a/src/harness/fakes.ts b/src/harness/fakes.ts index f5a2861d385..958629a0988 100644 --- a/src/harness/fakes.ts +++ b/src/harness/fakes.ts @@ -51,6 +51,10 @@ namespace fakes { this.vfs.writeFileSync(path, writeByteOrderMark ? utils.addUTF8ByteOrderMark(data) : data); } + public deleteFile(path: string) { + this.vfs.unlinkSync(path); + } + public fileExists(path: string) { const stats = this._getStats(path); return stats ? stats.isFile() : false; @@ -248,6 +252,10 @@ namespace fakes { return this.sys.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase(); } + public deleteFile(fileName: string) { + this.sys.deleteFile(fileName); + } + public fileExists(fileName: string): boolean { return this.sys.fileExists(fileName); } diff --git a/src/harness/unittests/tsbuild.ts b/src/harness/unittests/tsbuild.ts index e84fa403475..a761e154898 100644 --- a/src/harness/unittests/tsbuild.ts +++ b/src/harness/unittests/tsbuild.ts @@ -11,6 +11,9 @@ namespace ts { bfs.meta.set("defaultLibLocation", "/lib"); bfs.makeReadonly(); tick(); + const allExpectedOutputs = ["/src/tests/index.js", + "/src/core/index.js", "/src/core/index.d.ts", + "/src/logic/index.js", "/src/logic/index.d.ts"]; describe("tsbuild - sanity check of clean build of 'sample1' project", () => { it("can build the sample project 'sample1' without error", () => { @@ -23,13 +26,95 @@ namespace ts { assertDiagnosticMessages(/*empty*/); // Check for outputs. Not an exhaustive list - const expectedOutputs = ["/src/tests/index.js", "/src/core/index.js", "/src/core/index.d.ts"]; - for (const output of expectedOutputs) { + for (const output of allExpectedOutputs) { assert(fs.existsSync(output), `Expect file ${output} to exist`); } }); }); + describe("tsbuild - dry builds", () => { + it("doesn't write any files in a dry build", () => { + clearDiagnostics(); + const fs = bfs.shadow(); + const host = new fakes.CompilerHost(fs); + const builder = createSolutionBuilder(host, reportDiagnostic, { dry: true, force: false, verbose: false }); + fs.chdir("/src/tests"); + builder.buildProjects(["."]); + assertDiagnosticMessages(Diagnostics.Would_build_project_0, Diagnostics.Would_build_project_0, Diagnostics.Would_build_project_0); + + // Check for outputs to not be written. Not an exhaustive list + for (const output of allExpectedOutputs) { + assert(!fs.existsSync(output), `Expect file ${output} to not exist`); + } + }); + + it("indicates that it would skip builds during a dry build", () => { + clearDiagnostics(); + const fs = bfs.shadow(); + const host = new fakes.CompilerHost(fs); + + let builder = createSolutionBuilder(host, reportDiagnostic, { dry: false, force: false, verbose: false }); + fs.chdir("/src/tests"); + builder.buildProjects(["."]); + tick(); + + clearDiagnostics(); + builder = createSolutionBuilder(host, reportDiagnostic, { dry: true, force: false, verbose: false }); + builder.buildProjects(["."]); + assertDiagnosticMessages(Diagnostics.Project_0_is_up_to_date, Diagnostics.Project_0_is_up_to_date, Diagnostics.Project_0_is_up_to_date); + }); + }); + + describe("tsbuild - clean builds", () => { + it("removes all files it built", () => { + clearDiagnostics(); + const fs = bfs.shadow(); + const host = new fakes.CompilerHost(fs); + + const builder = createSolutionBuilder(host, reportDiagnostic, { dry: false, force: false, verbose: false }); + fs.chdir("/src/tests"); + builder.buildProjects(["."]); + // Verify they exist + for (const output of allExpectedOutputs) { + assert(fs.existsSync(output), `Expect file ${output} to exist`); + } + builder.cleanProjects(["."]); + // Verify they are gone + for (const output of allExpectedOutputs) { + assert(!fs.existsSync(output), `Expect file ${output} to not exist`); + } + // Subsequent clean shouldn't throw / etc + builder.cleanProjects(["."]); + }); + }); + + describe("tsbuild - force builds", () => { + it("always builds under --force", () => { + const fs = bfs.shadow(); + const host = new fakes.CompilerHost(fs); + + const builder = createSolutionBuilder(host, reportDiagnostic, { dry: false, force: true, verbose: false }); + fs.chdir("/src/tests"); + builder.buildProjects(["."]); + let currentTime = time(); + checkOutputTimestamps(currentTime); + + tick(); + Debug.assert(time() !== currentTime, "Time moves on"); + currentTime = time(); + builder.buildProjects(["."]); + checkOutputTimestamps(currentTime); + + function checkOutputTimestamps(expected: number) { + // Check timestamps + for (const output of allExpectedOutputs) { + const actual = fs.statSync(output).mtimeMs; + assert(actual === expected, `File ${output} has timestamp ${actual}, expected ${expected}`); + } + } + }); + }); + describe("tsbuild - can detect when and what to rebuild", () => { const fs = bfs.shadow(); const host = new fakes.CompilerHost(fs); @@ -38,6 +123,7 @@ namespace ts { fs.chdir("/src/tests"); it("Builds the project", () => { + clearDiagnostics(); builder.resetBuildContext(); builder.buildProjects(["."]); assertDiagnosticMessages(Diagnostics.Sorted_list_of_input_projects_Colon_0, From 8a883ae2047e915cb080eefa42dd2fbf3ea7f1c0 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Thu, 24 May 2018 12:38:33 -0700 Subject: [PATCH 15/81] Comments --- src/compiler/tsbuild.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index ec9e18899aa..406d9a3c2ff 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -252,7 +252,6 @@ namespace ts { } function getOutputJavaScriptFileName(inputFileName: string, configFile: ParsedCommandLine) { - // TODO handle JSX: Preserve const relativePath = getRelativePathFromDirectory(rootDirOfOptions(configFile.options, configFile.options.configFilePath), inputFileName, /*ignoreCase*/ true); const outputPath = resolvePath(configFile.options.outDir || getDirectoryPath(configFile.options.configFilePath), relativePath); return changeExtension(outputPath, (fileExtensionIs(inputFileName, ".tsx") && configFile.options.jsx === JsxEmit.Preserve) ? ".jsx" : ".js"); @@ -295,7 +294,7 @@ namespace ts { const cache = createFileMap(); const configParseHost = parseConfigHostFromCompilerHost(host); - // TODO: Cache invalidation under --watch! + // TODO: Cache invalidation under --watch function parseConfigFile(configFilePath: string) { const sourceFile = host.getSourceFile(configFilePath, ScriptTarget.JSON) as JsonSourceFile; @@ -591,7 +590,6 @@ namespace ts { /** * Removes entries from arrays which appear in later arrays. - * TODO: Use a lookup object to optimize this a bit? */ function removeDuplicatesFromBuildQueue(queue: string[][]): void { // No need to check the last array From 07812796d18cba1f54805d47c978bc77ea55a4f7 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Thu, 24 May 2018 12:46:28 -0700 Subject: [PATCH 16/81] Add resolution branding --- src/compiler/tsbuild.ts | 48 ++++++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index 406d9a3c2ff..b2ccdbd5f6c 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -1,4 +1,6 @@ namespace ts { + type ResolvedConfigFileName = string & { _isResolvedConfigFileName: never }; + const minimumDate = new Date(-8640000000000000); const maximumDate = new Date(8640000000000000); @@ -24,7 +26,7 @@ namespace ts { type Mapper = ReturnType; interface DependencyGraph { - buildQueue: string[][]; + buildQueue: ResolvedConfigFileName[][]; dependencyMap: Mapper; } @@ -296,7 +298,7 @@ namespace ts { // TODO: Cache invalidation under --watch - function parseConfigFile(configFilePath: string) { + function parseConfigFile(configFilePath: ResolvedConfigFileName) { const sourceFile = host.getSourceFile(configFilePath, ScriptTarget.JSON) as JsonSourceFile; if (sourceFile === undefined) { return undefined; @@ -401,7 +403,7 @@ namespace ts { context = createBuildContext(options, reportDiagnostic); } - function getUpToDateStatusOfFile(configFileName: string): UpToDateStatus { + function getUpToDateStatusOfFile(configFileName: ResolvedConfigFileName): UpToDateStatus { return getUpToDateStatus(configFileCache.parseConfigFile(configFileName)); } @@ -500,7 +502,7 @@ namespace ts { // See if any of its upstream projects are newer than it if (project.projectReferences) { for (const ref of project.projectReferences) { - const resolvedRef = resolveProjectReferencePath(host, ref); + const resolvedRef = resolveProjectReferencePath(host, ref) as ResolvedConfigFileName; const refStatus = getUpToDateStatus(configFileCache.parseConfigFile(resolvedRef)); // If the upstream project is out of date, then so are we (someone shouldn't have asked, though?) @@ -544,14 +546,14 @@ namespace ts { } // TODO: Use the better algorithm - function createDependencyGraph(roots: string[]): DependencyGraph { + function createDependencyGraph(roots: ResolvedConfigFileName[]): DependencyGraph { // This is a list of list of projects that need to be built. // The ordering here is "backwards", i.e. the first entry in the array is the last set of projects that need to be built; // and the last entry is the first set of projects to be built. // Each subarray is effectively unordered. // We traverse the reference graph from each root, then "clean" the list by removing // any entry that is duplicated to its right. - const buildQueue: string[][] = []; + const buildQueue: ResolvedConfigFileName[][] = []; const dependencyMap = createDependencyMapper(); let buildQueuePosition = 0; for (const root of roots) { @@ -560,7 +562,7 @@ namespace ts { reportDiagnostic(createCompilerDiagnostic(Diagnostics.File_0_does_not_exist, root)); continue; } - enumerateReferences(normalizePath(root), config); + enumerateReferences(normalizePath(root) as ResolvedConfigFileName, config); } removeDuplicatesFromBuildQueue(buildQueue); @@ -569,7 +571,7 @@ namespace ts { dependencyMap }; - function enumerateReferences(fileName: string, root: ParsedCommandLine): void { + function enumerateReferences(fileName: ResolvedConfigFileName, root: ParsedCommandLine): void { const myBuildLevel = buildQueue[buildQueuePosition] = buildQueue[buildQueuePosition] || []; if (myBuildLevel.indexOf(fileName) < 0) { myBuildLevel.push(fileName); @@ -579,11 +581,11 @@ namespace ts { if (refs === undefined) return; buildQueuePosition++; for (const ref of refs) { - const actualPath = resolveProjectReferencePath(host, ref); + const actualPath = resolveProjectReferencePath(host, ref) as ResolvedConfigFileName; dependencyMap.addReference(fileName, actualPath); const resolvedRef = configFileCache.parseConfigFile(actualPath); if (resolvedRef === undefined) continue; - enumerateReferences(normalizePath(actualPath), resolvedRef); + enumerateReferences(normalizePath(actualPath) as ResolvedConfigFileName, resolvedRef); } buildQueuePosition--; } @@ -607,7 +609,7 @@ namespace ts { } // TODO Accept parsedCommandLine instead? - function buildSingleProject(proj: string) { + function buildSingleProject(proj: ResolvedConfigFileName) { if (context.options.dry) { reportDiagnostic(createCompilerDiagnostic(Diagnostics.Would_build_project_0, proj)); return; @@ -713,8 +715,8 @@ namespace ts { context.projectStatus.setValue(proj.options.configFilePath, { type: UpToDateStatusType.UpToDate, newestDeclarationFileContentChangedTime: priorNewestUpdateTime } as UpToDateStatus); } - function getFilesToClean(configFileNames: string[]): string[] | undefined { - const resolvedNames: string[] | undefined = resolveProjectNames(configFileNames); + function getFilesToClean(configFileNames: ResolvedConfigFileName[]): string[] | undefined { + const resolvedNames: ResolvedConfigFileName[] | undefined = resolveProjectNames(configFileNames); if (resolvedNames === undefined) return; // Get the same graph for cleaning we'd use for building @@ -736,7 +738,10 @@ namespace ts { } function cleanProjects(configFileNames: string[]) { - const filesToDelete = getFilesToClean(configFileNames); + const resolvedNames: ResolvedConfigFileName[] | undefined = resolveProjectNames(configFileNames); + if (resolvedNames === undefined) return; + + const filesToDelete = getFilesToClean(resolvedNames); if (context.options.dry) { reportDiagnostic(createCompilerDiagnostic(Diagnostics.Would_delete_the_following_files_Colon_0, filesToDelete.map(f => `\r\n * ${f}`).join(""))); @@ -752,18 +757,17 @@ namespace ts { } } - // TODO add branding to resolved filenames - function resolveProjectNames(configFileNames: string[]): string[] | undefined { - const resolvedNames: string[] = []; + function resolveProjectNames(configFileNames: string[]): ResolvedConfigFileName[] | undefined { + const resolvedNames: ResolvedConfigFileName[] = []; for (const name of configFileNames) { let fullPath = resolvePath(host.getCurrentDirectory(), name); if (host.fileExists(fullPath)) { - resolvedNames.push(fullPath); + resolvedNames.push(fullPath as ResolvedConfigFileName); continue; } fullPath = combinePaths(fullPath, "tsconfig.json"); if (host.fileExists(fullPath)) { - resolvedNames.push(fullPath); + resolvedNames.push(fullPath as ResolvedConfigFileName); continue; } reportDiagnostic(createCompilerDiagnostic(Diagnostics.File_0_not_found, fullPath)); @@ -773,7 +777,7 @@ namespace ts { } function buildProjects(configFileNames: string[]) { - const resolvedNames: string[] | undefined = resolveProjectNames(configFileNames); + const resolvedNames: ResolvedConfigFileName[] | undefined = resolveProjectNames(configFileNames); if (resolvedNames === undefined) return; // Establish what needs to be built @@ -782,7 +786,7 @@ namespace ts { const queue = graph.buildQueue; reportBuildQueue(graph); - let next: string; + let next: ResolvedConfigFileName; while (next = getNext()) { const proj = configFileCache.parseConfigFile(next); const status = getUpToDateStatus(proj); @@ -809,7 +813,7 @@ namespace ts { } } - function getNext(): string | undefined { + function getNext(): ResolvedConfigFileName | undefined { if (queue.length === 0) { return undefined; } From d9bfbfe3bef15f01defc5b0db20e2f0be9660db0 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Thu, 24 May 2018 13:01:44 -0700 Subject: [PATCH 17/81] Fix tests run in parallel --- src/harness/unittests/tsbuild.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/harness/unittests/tsbuild.ts b/src/harness/unittests/tsbuild.ts index a761e154898..1596cc4ec19 100644 --- a/src/harness/unittests/tsbuild.ts +++ b/src/harness/unittests/tsbuild.ts @@ -21,6 +21,7 @@ namespace ts { const host = new fakes.CompilerHost(fs); const builder = createSolutionBuilder(host, reportDiagnostic, { dry: false, force: false, verbose: false }); + clearDiagnostics(); fs.chdir("/src/tests"); builder.buildProjects(["."]); assertDiagnosticMessages(/*empty*/); From 19ab8abbb9deb35ef65228c1347878a41fd8eb7d Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Thu, 24 May 2018 13:42:12 -0700 Subject: [PATCH 18/81] Fix strictNullChecks breaks --- src/compiler/program.ts | 6 +-- src/compiler/tsbuild.ts | 66 ++++++++++++++++++++++---------- src/harness/unittests/tsbuild.ts | 6 +-- src/harness/vfs.ts | 3 ++ 4 files changed, 54 insertions(+), 27 deletions(-) diff --git a/src/compiler/program.ts b/src/compiler/program.ts index b4b31226837..5b39c79aeac 100755 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -190,9 +190,9 @@ namespace ts { getDirectories: (path: string) => sys.getDirectories(path), realpath, readDirectory: (path, extensions, include, exclude, depth) => sys.readDirectory(path, extensions, include, exclude, depth), - getModifiedTime: path => sys.getModifiedTime(path), - setModifiedTime: (path, date) => sys.setModifiedTime(path, date), - deleteFile: path => sys.deleteFile(path) + getModifiedTime: sys.getModifiedTime && (path => sys.getModifiedTime!(path)), + setModifiedTime: sys.setModifiedTime && ((path, date) => sys.setModifiedTime!(path, date)), + deleteFile: sys.deleteFile && (path => sys.deleteFile!(path)) }; } diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index b2ccdbd5f6c..bbd8e68301f 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -248,14 +248,14 @@ namespace ts { } function getOutputDeclarationFileName(inputFileName: string, configFile: ParsedCommandLine) { - const relativePath = getRelativePathFromDirectory(rootDirOfOptions(configFile.options, configFile.options.configFilePath), inputFileName, /*ignoreCase*/ true); - const outputPath = resolvePath(configFile.options.declarationDir || configFile.options.outDir || getDirectoryPath(configFile.options.configFilePath), relativePath); + const relativePath = getRelativePathFromDirectory(rootDirOfOptions(configFile.options, configFile.options.configFilePath!), inputFileName, /*ignoreCase*/ true); + const outputPath = resolvePath(configFile.options.declarationDir || configFile.options.outDir || getDirectoryPath(configFile.options.configFilePath!), relativePath); return changeExtension(outputPath, ".d.ts"); } function getOutputJavaScriptFileName(inputFileName: string, configFile: ParsedCommandLine) { - const relativePath = getRelativePathFromDirectory(rootDirOfOptions(configFile.options, configFile.options.configFilePath), inputFileName, /*ignoreCase*/ true); - const outputPath = resolvePath(configFile.options.outDir || getDirectoryPath(configFile.options.configFilePath), relativePath); + const relativePath = getRelativePathFromDirectory(rootDirOfOptions(configFile.options, configFile.options.configFilePath!), inputFileName, /*ignoreCase*/ true); + const outputPath = resolvePath(configFile.options.outDir || getDirectoryPath(configFile.options.configFilePath!), relativePath); return changeExtension(outputPath, (fileExtensionIs(inputFileName, ".tsx") && configFile.options.jsx === JsxEmit.Preserve) ? ".jsx" : ".js"); } @@ -276,7 +276,9 @@ namespace ts { } function getOutFileOutputs(project: ParsedCommandLine): ReadonlyArray { - Debug.assert(!!project.options.outFile, "outFile must be set"); + if (!project.options.outFile) { + throw new Error("Assert - outFile must be set"); + } const outputs: string[] = []; outputs.push(project.options.outFile); if (project.options.declaration) { @@ -328,9 +330,7 @@ namespace ts { options, projectStatus: createFileMap(), unchangedOutputs: createFileMap(), - verbose: options.verbose ? (diag, ...args) => { - verboseDiag(createCompilerDiagnostic(diag, ...args)); - } : () => undefined + verbose: verboseDiag ? (diag, ...args) => verboseDiag(createCompilerDiagnostic(diag, ...args)) : () => undefined }; } @@ -379,6 +379,11 @@ namespace ts { function addProject(projectSpecification: string) { const fileName = resolvePath(host.getCurrentDirectory(), projectSpecification); const refPath = resolveProjectReferencePath(host, { path: fileName }); + if (!refPath) { + reportDiagnostic(createCompilerDiagnostic(Diagnostics.File_0_does_not_exist, projectSpecification)); + return; + } + if (!host.fileExists(refPath)) { reportDiagnostic(createCompilerDiagnostic(Diagnostics.File_0_does_not_exist, fileName)); } @@ -388,6 +393,10 @@ namespace ts { } export function createSolutionBuilder(host: CompilerHost, reportDiagnostic: DiagnosticReporter, options: BuildOptions) { + if (!host.getModifiedTime || !host.setModifiedTime) { + throw new Error("Host must support timestamp APIs"); + } + const configFileCache = createConfigFileCache(host); let context = createBuildContext(options, reportDiagnostic); @@ -407,13 +416,17 @@ namespace ts { return getUpToDateStatus(configFileCache.parseConfigFile(configFileName)); } - function getUpToDateStatus(project: ParsedCommandLine): UpToDateStatus { - const prior = context.projectStatus.getValueOrUndefined(project.options.configFilePath); + function getUpToDateStatus(project: ParsedCommandLine | undefined): UpToDateStatus { + if (project === undefined) { + return { type: UpToDateStatusType.Unbuildable, reason: "File deleted mid-build" }; + } + + const prior = context.projectStatus.getValueOrUndefined(project.options.configFilePath!); if (prior !== undefined) { return prior; } const actual = getUpToDateStatusWorker(project); - context.projectStatus.setValue(project.options.configFilePath, actual); + context.projectStatus.setValue(project.options.configFilePath!, actual); return actual; } @@ -442,7 +455,7 @@ namespace ts { }; } - const inputTime = host.getModifiedTime(inputFile); + const inputTime = host.getModifiedTime!(inputFile); if (inputTime > newestInputFileTime) { newestInputFileName = inputFile; newestInputFileTime = inputTime; @@ -466,7 +479,7 @@ namespace ts { }; } - const outputTime = host.getModifiedTime(output); + const outputTime = host.getModifiedTime!(output); // If an output is older than the newest input, we can stop checking if (outputTime < newestInputFileTime) { return { @@ -492,7 +505,7 @@ namespace ts { newestDeclarationFileContentChangedTime = newer(unchangedTime, newestDeclarationFileContentChangedTime); } else { - newestDeclarationFileContentChangedTime = newer(newestDeclarationFileContentChangedTime, host.getModifiedTime(output)); + newestDeclarationFileContentChangedTime = newer(newestDeclarationFileContentChangedTime, host.getModifiedTime!(output)); } } } @@ -609,10 +622,10 @@ namespace ts { } // TODO Accept parsedCommandLine instead? - function buildSingleProject(proj: ResolvedConfigFileName) { + function buildSingleProject(proj: ResolvedConfigFileName): BuildResultFlags { if (context.options.dry) { reportDiagnostic(createCompilerDiagnostic(Diagnostics.Would_build_project_0, proj)); - return; + return BuildResultFlags.Success; } context.verbose(Diagnostics.Building_project_0, proj); @@ -707,17 +720,17 @@ namespace ts { let priorNewestUpdateTime = minimumDate; for (const file of outputs) { if (isDeclarationFile(file)) { - priorNewestUpdateTime = newer(priorNewestUpdateTime, host.getModifiedTime(file)); + priorNewestUpdateTime = newer(priorNewestUpdateTime, host.getModifiedTime!(file)); } - host.setModifiedTime(file, now); + host.setModifiedTime!(file, now); } - context.projectStatus.setValue(proj.options.configFilePath, { type: UpToDateStatusType.UpToDate, newestDeclarationFileContentChangedTime: priorNewestUpdateTime } as UpToDateStatus); + context.projectStatus.setValue(proj.options.configFilePath!, { type: UpToDateStatusType.UpToDate, newestDeclarationFileContentChangedTime: priorNewestUpdateTime } as UpToDateStatus); } function getFilesToClean(configFileNames: ResolvedConfigFileName[]): string[] | undefined { const resolvedNames: ResolvedConfigFileName[] | undefined = resolveProjectNames(configFileNames); - if (resolvedNames === undefined) return; + if (resolvedNames === undefined) return undefined; // Get the same graph for cleaning we'd use for building const graph = createDependencyGraph(resolvedNames); @@ -726,6 +739,10 @@ namespace ts { for (const level of graph.buildQueue) { for (const proj of level) { const parsed = configFileCache.parseConfigFile(proj); + if (parsed === undefined) { + // File has gone missing; fine to ignore here + continue; + } const outputs = getAllProjectOutputs(parsed); for (const output of outputs) { if (host.fileExists(output)) { @@ -742,6 +759,9 @@ namespace ts { if (resolvedNames === undefined) return; const filesToDelete = getFilesToClean(resolvedNames); + if (filesToDelete === undefined) { + return; + } if (context.options.dry) { reportDiagnostic(createCompilerDiagnostic(Diagnostics.Would_delete_the_following_files_Colon_0, filesToDelete.map(f => `\r\n * ${f}`).join(""))); @@ -786,9 +806,13 @@ namespace ts { const queue = graph.buildQueue; reportBuildQueue(graph); - let next: ResolvedConfigFileName; + let next: ResolvedConfigFileName | undefined; while (next = getNext()) { const proj = configFileCache.parseConfigFile(next); + if (proj === undefined) { + break; + } + const status = getUpToDateStatus(proj); reportProjectStatus(next, status); diff --git a/src/harness/unittests/tsbuild.ts b/src/harness/unittests/tsbuild.ts index 1596cc4ec19..4570c7598e7 100644 --- a/src/harness/unittests/tsbuild.ts +++ b/src/harness/unittests/tsbuild.ts @@ -7,7 +7,7 @@ namespace ts { const sampleRoot = resolvePath(__dirname, "../../tests/projects/sample1"); loadFsMirror(bfs, sampleRoot, "/src"); bfs.mkdirpSync("/lib"); - bfs.writeFileSync("/lib/lib.d.ts", Harness.IO.readFile(combinePaths(Harness.libFolder, "lib.d.ts"))); + bfs.writeFileSync("/lib/lib.d.ts", Harness.IO.readFile(combinePaths(Harness.libFolder, "lib.d.ts"))!); bfs.meta.set("defaultLibLocation", "/lib"); bfs.makeReadonly(); tick(); @@ -196,7 +196,7 @@ namespace ts { function assertDiagnosticMessages(...expected: DiagnosticMessage[]) { const actual = lastDiagnostics.slice(); if (actual.length !== expected.length) { - assert.fail(actual, expected, `Diagnostic arrays did not match - expected\r\n${actual.map(a => " " + a.messageText).join("\r\n")}\r\ngot\r\n${expected.map(e => " " + e.message).join("\r\n")}`); + assert.fail(actual, expected, `Diagnostic arrays did not match - got\r\n${actual.map(a => " " + a.messageText).join("\r\n")}\r\nexpected\r\n${expected.map(e => " " + e.message).join("\r\n")}`); } for (let i = 0; i < actual.length; i++) { if (actual[i].code !== expected[i].code) { @@ -229,7 +229,7 @@ namespace ts { vfs.mkdirpSync(virtualRoot); for (const path of Harness.IO.readDirectory(localRoot)) { const file = getBaseFileName(path); - vfs.writeFileSync(virtualRoot + "/" + file, Harness.IO.readFile(localRoot + "/" + file)); + vfs.writeFileSync(virtualRoot + "/" + file, Harness.IO.readFile(localRoot + "/" + file)!); } for (const dir of Harness.IO.getDirectories(localRoot)) { loadFsMirror(vfs, localRoot + "/" + dir, virtualRoot + "/" + dir); diff --git a/src/harness/vfs.ts b/src/harness/vfs.ts index 09b2e39cd88..be8b6c3fed7 100644 --- a/src/harness/vfs.ts +++ b/src/harness/vfs.ts @@ -415,6 +415,9 @@ namespace vfs { */ public utimesSync(path: string, atime: Date, mtime: Date) { const entry = this._walk(this._resolve(path)); + if (!entry || !entry.node) { + throw createIOError("ENOENT"); + } entry.node.atimeMs = +atime; entry.node.mtimeMs = +mtime; } From cb8aa9b1a3b4d8abbb748d0bb3e733e9649d38bf Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Thu, 24 May 2018 16:21:06 -0700 Subject: [PATCH 19/81] Don't use invalid cached SourceFiles --- src/harness/fakes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/harness/fakes.ts b/src/harness/fakes.ts index ec731237db4..1bb358698a2 100644 --- a/src/harness/fakes.ts +++ b/src/harness/fakes.ts @@ -332,7 +332,7 @@ namespace fakes { if (cacheKey) { const meta = this.vfs.filemeta(canonicalFileName); const sourceFileFromMetadata = meta.get(cacheKey) as ts.SourceFile | undefined; - if (sourceFileFromMetadata) { + if (sourceFileFromMetadata && sourceFileFromMetadata.getFullText() === content) { this._sourceFiles.set(canonicalFileName, sourceFileFromMetadata); return sourceFileFromMetadata; } From 8ac795ba01f6457157f6bf4883a4c6a6c4ed9dee Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Thu, 24 May 2018 16:21:32 -0700 Subject: [PATCH 20/81] Correctly skip upstream-blocked projects --- src/compiler/diagnosticMessages.json | 8 +++ src/compiler/tsbuild.ts | 94 ++++++++++++++++++++-------- src/harness/unittests/tsbuild.ts | 25 ++++++++ 3 files changed, 100 insertions(+), 27 deletions(-) diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 40b32186d62..266233a7cdd 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -3644,6 +3644,14 @@ "category": "Message", "code": 6360 }, + "Skipping build of project '{0}' because its upstream project '{1}' has errors": { + "category": "Message", + "code": 6361 + }, + "Project '{0}' can't be built because it depends on a project with errors": { + "category": "Message", + "code": 6362 + }, "Variable '{0}' implicitly has an '{1}' type.": { "category": "Error", diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index bbd8e68301f..2b08474f019 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -69,7 +69,8 @@ namespace ts { OutputMissing, OutOfDateWithSelf, OutOfDateWithUpstream, - UpstreamOutOfDate + UpstreamOutOfDate, + UpstreamBlocked } type UpToDateStatus = @@ -78,7 +79,8 @@ namespace ts { | StatusOutputMissing | StatusOutOfDateWithSelf | StatusOutOfDateWithUpstream - | StatusUpstreamOutOfDate; + | StatusUpstreamOutOfDate + | StatusUpstreamBlocked; /** * The project can't be built at all in its current state. For example, @@ -128,6 +130,14 @@ namespace ts { upstreamProjectName: string; } + /** + * This project depends an upstream project with build errors + */ + interface StatusUpstreamBlocked { + type: UpToDateStatusType.UpstreamBlocked; + upstreamProjectName: string; + } + /** * One or more of the project's outputs is older than the newest output of * an upstream project. @@ -466,33 +476,33 @@ namespace ts { const outputs = getAllProjectOutputs(project); // Now see if all outputs are newer than the newest input - let oldestOutputFileName: string = undefined!; + let oldestOutputFileName: string | undefined; let oldestOutputFileTime: Date = maximumDate; let newestOutputFileTime: Date = minimumDate; let newestDeclarationFileContentChangedTime: Date = minimumDate; + let missingOutputFileName: string | undefined; + let isOutOfDateWithInputs = false; for (const output of outputs) { - // Output is missing + // Output is missing; can stop checking + // Don't immediately return because we can still be upstream-blocked, which is a higher-priority status if (!host.fileExists(output)) { - return { - type: UpToDateStatusType.OutputMissing, - missingOutputFileName: output - }; + missingOutputFileName = output; + break; } const outputTime = host.getModifiedTime!(output); - // If an output is older than the newest input, we can stop checking - if (outputTime < newestInputFileTime) { - return { - type: UpToDateStatusType.OutOfDateWithSelf, - outOfDateOutputFileName: output, - newerInputFileName: newestInputFileName - }; - } - if (outputTime < oldestOutputFileTime) { oldestOutputFileTime = outputTime; oldestOutputFileName = output; } + + // If an output is older than the newest input, we can stop checking + // Don't immediately return because we can still be upstream-blocked, which is a higher-priority status + if (outputTime < newestInputFileTime) { + isOutOfDateWithInputs = true; + break; + } + newestOutputFileTime = newer(newestOutputFileTime, outputTime); // Keep track of when the most recent time a .d.ts file was changed. @@ -511,13 +521,19 @@ namespace ts { } let pseudoUpToDate = false; - // By here, we know the project is at least up-to-date with its own inputs. - // See if any of its upstream projects are newer than it if (project.projectReferences) { for (const ref of project.projectReferences) { const resolvedRef = resolveProjectReferencePath(host, ref) as ResolvedConfigFileName; const refStatus = getUpToDateStatus(configFileCache.parseConfigFile(resolvedRef)); + // An upstream project is blocked + if (refStatus.type === UpToDateStatusType.Unbuildable) { + return { + type: UpToDateStatusType.UpstreamBlocked, + upstreamProjectName: ref.path + }; + } + // If the upstream project is out of date, then so are we (someone shouldn't have asked, though?) if (refStatus.type !== UpToDateStatusType.UpToDate) { return { @@ -543,12 +559,27 @@ namespace ts { Debug.assert(oldestOutputFileName !== undefined, "Should have an oldest output filename here"); return { type: UpToDateStatusType.OutOfDateWithUpstream, - outOfDateOutputFileName: oldestOutputFileName, + outOfDateOutputFileName: oldestOutputFileName!, newerProjectName: ref.path }; } } + if (missingOutputFileName !== undefined) { + return { + type: UpToDateStatusType.OutputMissing, + missingOutputFileName + }; + } + + if (isOutOfDateWithInputs) { + return { + type: UpToDateStatusType.OutOfDateWithSelf, + outOfDateOutputFileName: oldestOutputFileName!, + newerInputFileName: newestInputFileName + }; + } + // Up to date return { type: pseudoUpToDate ? UpToDateStatusType.UpToDateWithUpstreamTypes : UpToDateStatusType.UpToDate, @@ -637,6 +668,7 @@ namespace ts { if (!configFile) { // Failed to read the config file resultFlags |= BuildResultFlags.ConfigFileErrors; + context.projectStatus.setValue(proj, { type: UpToDateStatusType.Unbuildable, reason: "Config file errors" }); return resultFlags; } @@ -660,6 +692,7 @@ namespace ts { for (const diag of syntaxDiagnostics) { reportDiagnostic(diag); } + context.projectStatus.setValue(proj, { type: UpToDateStatusType.Unbuildable, reason: "Syntactic errors" }); return resultFlags; } @@ -671,6 +704,7 @@ namespace ts { for (const diag of declDiagnostics) { reportDiagnostic(diag); } + context.projectStatus.setValue(proj, { type: UpToDateStatusType.Unbuildable, reason: "Declaration file errors" }); return resultFlags; } } @@ -681,6 +715,7 @@ namespace ts { for (const diag of semanticDiagnostics) { reportDiagnostic(diag); } + context.projectStatus.setValue(proj, { type: UpToDateStatusType.Unbuildable, reason: "Semantic errors" }); return resultFlags; } @@ -704,7 +739,6 @@ namespace ts { }); context.projectStatus.setValue(proj, { type: UpToDateStatusType.UpToDate, newestDeclarationFileContentChangedTime } as UpToDateStatus); - return resultFlags; } @@ -812,15 +846,15 @@ namespace ts { if (proj === undefined) { break; } - const status = getUpToDateStatus(proj); reportProjectStatus(next, status); + const projName = proj.options.configFilePath; if (status.type === UpToDateStatusType.UpToDate && !context.options.force) { // Up to date, skip if (options.dry) { // In a dry build, inform the user of this fact - reportDiagnostic(createCompilerDiagnostic(Diagnostics.Project_0_is_up_to_date)); + reportDiagnostic(createCompilerDiagnostic(Diagnostics.Project_0_is_up_to_date, projName)); } continue; } @@ -831,10 +865,12 @@ namespace ts { continue; } - const result = buildSingleProject(next); - if (result & BuildResultFlags.AnyErrors) { - break; + if (status.type === UpToDateStatusType.UpstreamBlocked) { + context.verbose(Diagnostics.Skipping_build_of_project_0_because_its_upstream_project_1_has_errors, projName, status.upstreamProjectName); + continue; } + + buildSingleProject(next); } function getNext(): ResolvedConfigFileName | undefined { @@ -889,9 +925,13 @@ namespace ts { case UpToDateStatusType.UpToDateWithUpstreamTypes: context.verbose(Diagnostics.Project_0_is_up_to_date_with_its_upstream_types, configFileName); return; - case UpToDateStatusType.UpstreamOutOfDate: + case UpToDateStatusType.UpstreamOutOfDate: context.verbose(Diagnostics.Project_0_is_up_to_date_with_its_upstream_types, configFileName); return; + case UpToDateStatusType.UpstreamBlocked: + context.verbose(Diagnostics.Project_0_can_t_be_built_because_it_depends_on_a_project_with_errors, configFileName); + return; + default: throw new Error(`Invalid build status - ${UpToDateStatusType[status.type]}`); } diff --git a/src/harness/unittests/tsbuild.ts b/src/harness/unittests/tsbuild.ts index 4570c7598e7..f1f38bd2f9f 100644 --- a/src/harness/unittests/tsbuild.ts +++ b/src/harness/unittests/tsbuild.ts @@ -181,6 +181,31 @@ namespace ts { }); }); + describe("tsbuild - downstream-blocked compilations", () => { + it("won't build downstream projects if upstream projects have errors", () => { + const fs = bfs.shadow(); + const host = new fakes.CompilerHost(fs); + const builder = createSolutionBuilder(host, reportDiagnostic, { dry: false, force: false, verbose: true }); + + clearDiagnostics(); + + // Induce an error in the middle project + replaceText(fs, "/src/logic/index.ts", "c.multiply(10, 15)", `c.muitply()`); + fs.chdir("/src/tests"); + builder.buildProjects(["."]); + assertDiagnosticMessages( + Diagnostics.Sorted_list_of_input_projects_Colon_0, + Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, + Diagnostics.Building_project_0, + Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, + Diagnostics.Building_project_0, + Diagnostics.Property_0_does_not_exist_on_type_1, + Diagnostics.Project_0_can_t_be_built_because_it_depends_on_a_project_with_errors, + Diagnostics.Skipping_build_of_project_0_because_its_upstream_project_1_has_errors + ); + }); + }); + function replaceText(fs: vfs.FileSystem, path: string, oldText: string, newText: string) { if (!fs.statSync(path).isFile()) { throw new Error(`File ${path} does not exist`); From 1863d3fd48f986f851739ceb2e6d7561244c6c40 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Thu, 24 May 2018 17:48:45 -0700 Subject: [PATCH 21/81] Graph ordering test WIP --- src/compiler/tsbuild.ts | 201 ++++++++++++++++--------------- src/harness/unittests/tsbuild.ts | 42 +++++++ 2 files changed, 144 insertions(+), 99 deletions(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index 2b08474f019..4bd6c13914d 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -1,4 +1,9 @@ namespace ts { + /** + * Branded string for keeping track of when we've turned an ambiguous path + * specified like "./blah" to an absolute path to an actual + * tsconfig file, e.g. "/root/blah/tsconfig.json" + */ type ResolvedConfigFileName = string & { _isResolvedConfigFileName: never }; const minimumDate = new Date(-8640000000000000); @@ -6,8 +11,16 @@ namespace ts { /** * A BuildContext tracks what's going on during the course of a build. - * The primary thing we track here is which files were written to, - * but unchanged, because this enables fast downstream updates + * + * Callers may invoke any number of build requests within the same context; + * until the context is reset, each project will only be built at most once. + * + * Example: In a standard setup where project B depends on project A, and both are out of date, + * a failed build of A will result in A remaining out of date. When we try to build + * B, we should immediately bail instead of recomputing A's up-to-date status again. + * + * This also matters for performing fast (i.e. fake) downstream builds of projects + * when their upstream .d.ts files haven't changed content (but have newer timestamps) */ export interface BuildContext { options: BuildOptions; @@ -21,6 +34,9 @@ namespace ts { */ projectStatus: FileMap; + /** + * Issue a verbose diagnostic message. No-ops when options.verbose is false. + */ verbose(diag: DiagnosticMessage, ...args: any[]): void; } @@ -74,86 +90,86 @@ namespace ts { } type UpToDateStatus = - | StatusUnbuildable - | StatusUpToDate - | StatusOutputMissing - | StatusOutOfDateWithSelf - | StatusOutOfDateWithUpstream - | StatusUpstreamOutOfDate - | StatusUpstreamBlocked; + | Status.Unbuildable + | Status.UpToDate + | Status.OutputMissing + | Status.OutOfDateWithSelf + | Status.OutOfDateWithUpstream + | Status.UpstreamOutOfDate + | Status.UpstreamBlocked; - /** - * The project can't be built at all in its current state. For example, - * its config file cannot be parsed, or it has a syntax error or missing file - */ - interface StatusUnbuildable { - type: UpToDateStatusType.Unbuildable; - reason: string; - } - - /** - * The project is up to date with respect to its inputs. - * We track what the newest input file is. - */ - interface StatusUpToDate { - type: UpToDateStatusType.UpToDate | UpToDateStatusType.UpToDateWithUpstreamTypes; - newestInputFileTime: Date; - newestDeclarationFileContentChangedTime: Date; - newestOutputFileTime: Date; - } - - /** - * One or more of the outputs of the project does not exist. - */ - interface StatusOutputMissing { - type: UpToDateStatusType.OutputMissing; + namespace Status { /** - * The name of the first output file that didn't exist + * The project can't be built at all in its current state. For example, + * its config file cannot be parsed, or it has a syntax error or missing file */ - missingOutputFileName: string; - } + export interface Unbuildable { + type: UpToDateStatusType.Unbuildable; + reason: string; + } - /** - * One or more of the project's outputs is older than its newest input. - */ - interface StatusOutOfDateWithSelf { - type: UpToDateStatusType.OutOfDateWithSelf; - outOfDateOutputFileName: string; - newerInputFileName: string; - } + /** + * The project is up to date with respect to its inputs. + * We track what the newest input file is. + */ + export interface UpToDate { + type: UpToDateStatusType.UpToDate | UpToDateStatusType.UpToDateWithUpstreamTypes; + newestInputFileTime: Date; + newestDeclarationFileContentChangedTime: Date; + newestOutputFileTime: Date; + } - /** - * This project depends on an out-of-date project, so shouldn't be built yet - */ - interface StatusUpstreamOutOfDate { - type: UpToDateStatusType.UpstreamOutOfDate; - upstreamProjectName: string; - } + /** + * One or more of the outputs of the project does not exist. + */ + export interface OutputMissing { + type: UpToDateStatusType.OutputMissing; + /** + * The name of the first output file that didn't exist + */ + missingOutputFileName: string; + } - /** - * This project depends an upstream project with build errors - */ - interface StatusUpstreamBlocked { - type: UpToDateStatusType.UpstreamBlocked; - upstreamProjectName: string; - } + /** + * One or more of the project's outputs is older than its newest input. + */ + export interface OutOfDateWithSelf { + type: UpToDateStatusType.OutOfDateWithSelf; + outOfDateOutputFileName: string; + newerInputFileName: string; + } - /** - * One or more of the project's outputs is older than the newest output of - * an upstream project. - */ - interface StatusOutOfDateWithUpstream { - type: UpToDateStatusType.OutOfDateWithUpstream; - outOfDateOutputFileName: string; - newerProjectName: string; + /** + * This project depends on an out-of-date project, so shouldn't be built yet + */ + export interface UpstreamOutOfDate { + type: UpToDateStatusType.UpstreamOutOfDate; + upstreamProjectName: string; + } + + /** + * This project depends an upstream project with build errors + */ + export interface UpstreamBlocked { + type: UpToDateStatusType.UpstreamBlocked; + upstreamProjectName: string; + } + + /** + * One or more of the project's outputs is older than the newest output of + * an upstream project. + */ + export interface OutOfDateWithUpstream { + type: UpToDateStatusType.OutOfDateWithUpstream; + outOfDateOutputFileName: string; + newerProjectName: string; + } } interface FileMap { setValue(fileName: string, value: T): void; getValue(fileName: string): T | never; getValueOrUndefined(fileName: string): T | undefined; - getValueOrDefault(fileName: string, defaultValue: T): T; - tryGetValue(fileName: string): [false, undefined] | [true, T]; } /** @@ -167,8 +183,6 @@ namespace ts { setValue, getValue, getValueOrUndefined, - getValueOrDefault, - tryGetValue }; function setValue(fileName: string, value: T) { @@ -194,26 +208,6 @@ namespace ts { return undefined; } } - - function getValueOrDefault(fileName: string, defaultValue: T): T { - const f = normalizePath(fileName); - if (f in lookup) { - return lookup[f]; - } - else { - return defaultValue; - } - } - - function tryGetValue(fileName: string): [false, undefined] | [true, T] { - const f = normalizePath(fileName); - if (f in lookup) { - return [true as true, lookup[f]]; - } - else { - return [false as false, undefined]; - } - } } export function createDependencyMapper() { @@ -402,13 +396,13 @@ namespace ts { } } - export function createSolutionBuilder(host: CompilerHost, reportDiagnostic: DiagnosticReporter, options: BuildOptions) { + export function createSolutionBuilder(host: CompilerHost, reportDiagnostic: DiagnosticReporter, defaultOptions: BuildOptions) { if (!host.getModifiedTime || !host.setModifiedTime) { throw new Error("Host must support timestamp APIs"); } const configFileCache = createConfigFileCache(host); - let context = createBuildContext(options, reportDiagnostic); + let context = createBuildContext(defaultOptions, reportDiagnostic); return { getUpToDateStatus, @@ -418,8 +412,8 @@ namespace ts { resetBuildContext }; - function resetBuildContext() { - context = createBuildContext(options, reportDiagnostic); + function resetBuildContext(opts = defaultOptions) { + context = createBuildContext(opts, reportDiagnostic); } function getUpToDateStatusOfFile(configFileName: ResolvedConfigFileName): UpToDateStatus { @@ -798,7 +792,7 @@ namespace ts { } if (context.options.dry) { - reportDiagnostic(createCompilerDiagnostic(Diagnostics.Would_delete_the_following_files_Colon_0, filesToDelete.map(f => `\r\n * ${f}`).join(""))); + reportDiagnostic(createCompilerDiagnostic(Diagnostics.Would_delete_the_following_files_Colon_0, filesToDelete.map(f => `\r\n * ${f}`).join(""))); } else { if (!host.deleteFile) { @@ -852,7 +846,7 @@ namespace ts { const projName = proj.options.configFilePath; if (status.type === UpToDateStatusType.UpToDate && !context.options.force) { // Up to date, skip - if (options.dry) { + if (defaultOptions.dry) { // In a dry build, inform the user of this fact reportDiagnostic(createCompilerDiagnostic(Diagnostics.Project_0_is_up_to_date, projName)); } @@ -889,6 +883,9 @@ namespace ts { } } + /** + * Report the build ordering inferred from the current project graph if we're in verbose mode + */ function reportBuildQueue(graph: DependencyGraph) { if (!context.options.verbose) return; @@ -902,6 +899,9 @@ namespace ts { context.verbose(Diagnostics.Sorted_list_of_input_projects_Colon_0, names.map(s => "\r\n * " + s).join("")); } + /** + * Report the up-to-date status of a project if we're in verbose mode + */ function reportProjectStatus(configFileName: string, status: UpToDateStatus) { if (!context.options.verbose) return; switch (status.type) { @@ -931,9 +931,12 @@ namespace ts { case UpToDateStatusType.UpstreamBlocked: context.verbose(Diagnostics.Project_0_can_t_be_built_because_it_depends_on_a_project_with_errors, configFileName); return; - + case UpToDateStatusType.Unbuildable: + // TODO different error + context.verbose(Diagnostics.Project_0_can_t_be_built_because_it_depends_on_a_project_with_errors, configFileName); + return; default: - throw new Error(`Invalid build status - ${UpToDateStatusType[status.type]}`); + assertTypeIsNever(status); } } } diff --git a/src/harness/unittests/tsbuild.ts b/src/harness/unittests/tsbuild.ts index f1f38bd2f9f..4d2e5febdef 100644 --- a/src/harness/unittests/tsbuild.ts +++ b/src/harness/unittests/tsbuild.ts @@ -206,6 +206,48 @@ namespace ts { }); }); + describe("tsbuild - graph-ordering", () => { + it("orders the graph correctly", () => { + const fs = new vfs.FileSystem(false); + const host = new fakes.CompilerHost(fs); + const deps: [string, string][] = [ + ["A", "B"], + ["B", "C"], + ["A", "C"], + ["B", "D"], + ["C", "D"], + ["C", "E"], + ["F", "E"] + ]; + + writeProjects(fs, ["A", "B", "C", "D", "E", "F", "G"], deps); + const builder = createSolutionBuilder(host, reportDiagnostic, { dry: true, force: false, verbose: false }); + builder.buildProjects(["/project/A", "/project/G"]); + printDiagnostics(); + }); + + function writeProjects(fileSystem: vfs.FileSystem, projectNames: string[], deps: [string, string][]): string[] { + const projFileNames: string[] = []; + for (const dep of deps) { + if (projectNames.indexOf(dep[0]) < 0) throw new Error(`Invalid dependency - project ${dep[0]} does not exist`); + if (projectNames.indexOf(dep[1]) < 0) throw new Error(`Invalid dependency - project ${dep[1]} does not exist`); + } + for (const proj of projectNames) { + fileSystem.mkdirpSync(`/project/${proj}`); + fileSystem.writeFileSync(`/project/${proj}/${proj}.ts`, "export {}"); + const configFileName = `/project/${proj}/tsconfig.json`; + const configContent = JSON.stringify({ + compilerOptions: { composite: true }, + files: [`./${proj}.ts`], + references: deps.filter(d => d[0] === proj).map(d => ({ path: `../${d[1]}` })) + }, undefined, 2); + fileSystem.writeFileSync(configFileName, configContent); + projFileNames.push(configFileName); + } + return projFileNames; + } + }); + function replaceText(fs: vfs.FileSystem, path: string, oldText: string, newText: string) { if (!fs.statSync(path).isFile()) { throw new Error(`File ${path} does not exist`); From a7fcbcd3a446e77d52d899dd1f451924d9d62146 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Fri, 25 May 2018 16:06:33 -0700 Subject: [PATCH 22/81] Use better toposorting algorithm --- src/compiler/tsbuild.ts | 191 +++++++++++++++---------------- src/harness/unittests/tsbuild.ts | 58 +++++++--- 2 files changed, 132 insertions(+), 117 deletions(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index 4bd6c13914d..8e50800fff6 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -4,7 +4,7 @@ namespace ts { * specified like "./blah" to an absolute path to an actual * tsconfig file, e.g. "/root/blah/tsconfig.json" */ - type ResolvedConfigFileName = string & { _isResolvedConfigFileName: never }; + export type ResolvedConfigFileName = string & { _isResolvedConfigFileName: never }; const minimumDate = new Date(-8640000000000000); const maximumDate = new Date(8640000000000000); @@ -42,7 +42,7 @@ namespace ts { type Mapper = ReturnType; interface DependencyGraph { - buildQueue: ResolvedConfigFileName[][]; + buildQueue: ResolvedConfigFileName[]; dependencyMap: Mapper; } @@ -409,7 +409,8 @@ namespace ts { getUpToDateStatusOfFile, buildProjects, cleanProjects, - resetBuildContext + resetBuildContext, + getBuildGraph }; function resetBuildContext(opts = defaultOptions) { @@ -420,6 +421,13 @@ namespace ts { return getUpToDateStatus(configFileCache.parseConfigFile(configFileName)); } + function getBuildGraph(configFileNames: string[]) { + const resolvedNames: ResolvedConfigFileName[] | undefined = resolveProjectNames(configFileNames); + if (resolvedNames === undefined) return; + + return createDependencyGraph(resolvedNames); + } + function getUpToDateStatus(project: ParsedCommandLine | undefined): UpToDateStatus { if (project === undefined) { return { type: UpToDateStatusType.Unbuildable, reason: "File deleted mid-build" }; @@ -583,66 +591,62 @@ namespace ts { }; } - // TODO: Use the better algorithm - function createDependencyGraph(roots: ResolvedConfigFileName[]): DependencyGraph { - // This is a list of list of projects that need to be built. - // The ordering here is "backwards", i.e. the first entry in the array is the last set of projects that need to be built; - // and the last entry is the first set of projects to be built. - // Each subarray is effectively unordered. - // We traverse the reference graph from each root, then "clean" the list by removing - // any entry that is duplicated to its right. - const buildQueue: ResolvedConfigFileName[][] = []; - const dependencyMap = createDependencyMapper(); - let buildQueuePosition = 0; + function createDependencyGraph(roots: ResolvedConfigFileName[]): DependencyGraph | undefined { + const temporaryMarks: { [path: string]: true } = {}; + const permanentMarks: { [path: string]: true } = {}; + const circularityReportStack: string[] = []; + const buildOrder: ResolvedConfigFileName[] = []; + const graph = createDependencyMapper(); + + let hadError = false; + for (const root of roots) { - const config = configFileCache.parseConfigFile(root); - if (config === undefined) { - reportDiagnostic(createCompilerDiagnostic(Diagnostics.File_0_does_not_exist, root)); - continue; - } - enumerateReferences(normalizePath(root) as ResolvedConfigFileName, config); + visit(root); + } + + if (hadError) { + return undefined; } - removeDuplicatesFromBuildQueue(buildQueue); return { - buildQueue, - dependencyMap + buildQueue: buildOrder, + dependencyMap: graph }; - function enumerateReferences(fileName: ResolvedConfigFileName, root: ParsedCommandLine): void { - const myBuildLevel = buildQueue[buildQueuePosition] = buildQueue[buildQueuePosition] || []; - if (myBuildLevel.indexOf(fileName) < 0) { - myBuildLevel.push(fileName); - } - - const refs = root.projectReferences; - if (refs === undefined) return; - buildQueuePosition++; - for (const ref of refs) { - const actualPath = resolveProjectReferencePath(host, ref) as ResolvedConfigFileName; - dependencyMap.addReference(fileName, actualPath); - const resolvedRef = configFileCache.parseConfigFile(actualPath); - if (resolvedRef === undefined) continue; - enumerateReferences(normalizePath(actualPath) as ResolvedConfigFileName, resolvedRef); - } - buildQueuePosition--; - } - - /** - * Removes entries from arrays which appear in later arrays. - */ - function removeDuplicatesFromBuildQueue(queue: string[][]): void { - // No need to check the last array - for (let i = 0; i < queue.length - 1; i++) { - queue[i] = queue[i].filter(fn => !occursAfter(fn, i + 1)); - } - - function occursAfter(s: string, start: number) { - for (let i = start; i < queue.length; i++) { - if (queue[i].indexOf(s) >= 0) return true; + function visit(projPath: ResolvedConfigFileName, inCircularContext = false) { + // Already visited + if (permanentMarks[projPath]) return; + // Circular + if (temporaryMarks[projPath]) { + if (!inCircularContext) { + hadError = true; + reportDiagnostic(createCompilerDiagnostic(Diagnostics.Project_references_may_not_form_a_circular_graph_Cycle_detected_Colon_0, circularityReportStack.join("\r\n"))); + return; } - return false; } + + temporaryMarks[projPath] = true; + circularityReportStack.push(projPath); + const parsed = configFileCache.parseConfigFile(projPath); + if (parsed === undefined) { + hadError = true; + return; + } + if (parsed.projectReferences) { + for (const ref of parsed.projectReferences) { + const resolvedRefPath = resolveProjectName(ref.path); + if (resolvedRefPath === undefined) { + hadError = true; + break; + } + visit(resolvedRefPath, inCircularContext || ref.circular); + graph.addReference(projPath, resolvedRefPath); + } + } + + circularityReportStack.pop(); + permanentMarks[projPath] = true; + buildOrder.push(projPath); } } @@ -762,20 +766,19 @@ namespace ts { // Get the same graph for cleaning we'd use for building const graph = createDependencyGraph(resolvedNames); + if (graph === undefined) return undefined; const filesToDelete: string[] = []; - for (const level of graph.buildQueue) { - for (const proj of level) { - const parsed = configFileCache.parseConfigFile(proj); - if (parsed === undefined) { - // File has gone missing; fine to ignore here - continue; - } - const outputs = getAllProjectOutputs(parsed); - for (const output of outputs) { - if (host.fileExists(output)) { - filesToDelete.push(output); - } + for (const proj of graph.buildQueue) { + const parsed = configFileCache.parseConfigFile(proj); + if (parsed === undefined) { + // File has gone missing; fine to ignore here + continue; + } + const outputs = getAllProjectOutputs(parsed); + for (const output of outputs) { + if (host.fileExists(output)) { + filesToDelete.push(output); } } } @@ -805,21 +808,27 @@ namespace ts { } } + function resolveProjectName(name: string): ResolvedConfigFileName | undefined { + let fullPath = resolvePath(host.getCurrentDirectory(), name); + if (host.fileExists(fullPath)) { + return fullPath as ResolvedConfigFileName; + } + fullPath = combinePaths(fullPath, "tsconfig.json"); + if (host.fileExists(fullPath)) { + return fullPath as ResolvedConfigFileName; + } + reportDiagnostic(createCompilerDiagnostic(Diagnostics.File_0_not_found, fullPath)); + return undefined; + } + function resolveProjectNames(configFileNames: string[]): ResolvedConfigFileName[] | undefined { const resolvedNames: ResolvedConfigFileName[] = []; for (const name of configFileNames) { - let fullPath = resolvePath(host.getCurrentDirectory(), name); - if (host.fileExists(fullPath)) { - resolvedNames.push(fullPath as ResolvedConfigFileName); - continue; + const resolved = resolveProjectName(name); + if (resolved === undefined) { + return undefined; } - fullPath = combinePaths(fullPath, "tsconfig.json"); - if (host.fileExists(fullPath)) { - resolvedNames.push(fullPath as ResolvedConfigFileName); - continue; - } - reportDiagnostic(createCompilerDiagnostic(Diagnostics.File_0_not_found, fullPath)); - return undefined; + resolvedNames.push(resolved); } return resolvedNames; } @@ -830,12 +839,12 @@ namespace ts { // Establish what needs to be built const graph = createDependencyGraph(resolvedNames); + if (graph === undefined) return; const queue = graph.buildQueue; reportBuildQueue(graph); - let next: ResolvedConfigFileName | undefined; - while (next = getNext()) { + for (const next of queue) { const proj = configFileCache.parseConfigFile(next); if (proj === undefined) { break; @@ -866,21 +875,6 @@ namespace ts { buildSingleProject(next); } - - function getNext(): ResolvedConfigFileName | undefined { - if (queue.length === 0) { - return undefined; - } - while (queue.length > 0) { - const last = queue[queue.length - 1]; - if (last.length === 0) { - queue.pop(); - continue; - } - return last.pop()!; - } - return undefined; - } } /** @@ -890,12 +884,9 @@ namespace ts { if (!context.options.verbose) return; const names: string[] = []; - for (const level of graph.buildQueue) { - for (const el of level) { - names.push(el); - } + for (const name of graph.buildQueue) { + names.push(name); } - names.reverse(); context.verbose(Diagnostics.Sorted_list_of_input_projects_Colon_0, names.map(s => "\r\n * " + s).join("")); } diff --git a/src/harness/unittests/tsbuild.ts b/src/harness/unittests/tsbuild.ts index 4d2e5febdef..08818d6ac3f 100644 --- a/src/harness/unittests/tsbuild.ts +++ b/src/harness/unittests/tsbuild.ts @@ -207,25 +207,49 @@ namespace ts { }); describe("tsbuild - graph-ordering", () => { - it("orders the graph correctly", () => { - const fs = new vfs.FileSystem(false); - const host = new fakes.CompilerHost(fs); - const deps: [string, string][] = [ - ["A", "B"], - ["B", "C"], - ["A", "C"], - ["B", "D"], - ["C", "D"], - ["C", "E"], - ["F", "E"] - ]; + const fs = new vfs.FileSystem(false); + const host = new fakes.CompilerHost(fs); + const deps: [string, string][] = [ + ["A", "B"], + ["B", "C"], + ["A", "C"], + ["B", "D"], + ["C", "D"], + ["C", "E"], + ["F", "E"] + ]; - writeProjects(fs, ["A", "B", "C", "D", "E", "F", "G"], deps); - const builder = createSolutionBuilder(host, reportDiagnostic, { dry: true, force: false, verbose: false }); - builder.buildProjects(["/project/A", "/project/G"]); - printDiagnostics(); + writeProjects(fs, ["A", "B", "C", "D", "E", "F", "G"], deps); + + const builder = createSolutionBuilder(host, reportDiagnostic, { dry: true, force: false, verbose: false }); + + it("orders the graph correctly - specify two roots", () => { + checkGraphOrdering(["A", "G"], ["A", "B", "C", "D", "E", "G"]); }); + it("orders the graph correctly - multiple parts of the same graph in various orders", () => { + // TODO add cases here + }); + + function checkGraphOrdering(rootNames: string[], expectedBuildSet: string[]) { + const projFileNames = rootNames.map(getProjectFileName); + const graph = builder.getBuildGraph(projFileNames); + if (graph === undefined) throw new Error("Graph shouldn't be undefined"); + + assert.sameMembers(graph.buildQueue, expectedBuildSet.map(getProjectFileName)); + + for (const dep of deps) { + const child = getProjectFileName(dep[0]); + if (graph.buildQueue.indexOf(child) < 0) continue; + const parent = getProjectFileName(dep[1]); + assert.isAbove(graph.buildQueue.indexOf(child), graph.buildQueue.indexOf(parent), `Expecting child ${child} to be built after parent ${parent}`); + } + } + + function getProjectFileName(proj: string) { + return `/project/${proj}/tsconfig.json` as ResolvedConfigFileName; + } + function writeProjects(fileSystem: vfs.FileSystem, projectNames: string[], deps: [string, string][]): string[] { const projFileNames: string[] = []; for (const dep of deps) { @@ -235,7 +259,7 @@ namespace ts { for (const proj of projectNames) { fileSystem.mkdirpSync(`/project/${proj}`); fileSystem.writeFileSync(`/project/${proj}/${proj}.ts`, "export {}"); - const configFileName = `/project/${proj}/tsconfig.json`; + const configFileName = getProjectFileName(proj); const configContent = JSON.stringify({ compilerOptions: { composite: true }, files: [`./${proj}.ts`], From 70fa29b627af7192ac0a78b73e4ae242f8169a72 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Tue, 29 May 2018 10:00:54 -0700 Subject: [PATCH 23/81] Add graph ordering tests --- src/compiler/tsbuild.ts | 1 - src/harness/unittests/tsbuild.ts | 10 +++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index 8e50800fff6..978aa102c8a 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -650,7 +650,6 @@ namespace ts { } } - // TODO Accept parsedCommandLine instead? function buildSingleProject(proj: ResolvedConfigFileName): BuildResultFlags { if (context.options.dry) { reportDiagnostic(createCompilerDiagnostic(Diagnostics.Would_build_project_0, proj)); diff --git a/src/harness/unittests/tsbuild.ts b/src/harness/unittests/tsbuild.ts index 08818d6ac3f..ed1fd967952 100644 --- a/src/harness/unittests/tsbuild.ts +++ b/src/harness/unittests/tsbuild.ts @@ -228,7 +228,15 @@ namespace ts { }); it("orders the graph correctly - multiple parts of the same graph in various orders", () => { - // TODO add cases here + checkGraphOrdering(["A"], ["A", "B", "C", "D", "E"]); + checkGraphOrdering(["A", "C", "D"], ["A", "B", "C", "D", "E"]); + checkGraphOrdering(["D", "C", "A"], ["A", "B", "C", "D", "E"]); + }); + + it("orders the graph correctly - other orderings", () => { + checkGraphOrdering(["F"], ["F", "E"]); + checkGraphOrdering(["E"], ["E"]); + checkGraphOrdering(["F", "C", "A"], ["A", "B", "C", "D", "E", "F"]); }); function checkGraphOrdering(rootNames: string[], expectedBuildSet: string[]) { From 129f747ccc478881f67a89d677e7d8661bc48db5 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Tue, 29 May 2018 14:28:25 -0700 Subject: [PATCH 24/81] VFS fixes --- src/harness/vfs.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/harness/vfs.ts b/src/harness/vfs.ts index be8b6c3fed7..ba5905b8ea6 100644 --- a/src/harness/vfs.ts +++ b/src/harness/vfs.ts @@ -414,12 +414,16 @@ namespace vfs { * NOTE: do not rename this method as it is intended to align with the same named export of the "fs" module. */ public utimesSync(path: string, atime: Date, mtime: Date) { + if (this.isReadonly) throw createIOError("EROFS"); + if (!isFinite(+atime) || !isFinite(+mtime)) throw createIOError("EINVAL"); + const entry = this._walk(this._resolve(path)); if (!entry || !entry.node) { throw createIOError("ENOENT"); } entry.node.atimeMs = +atime; entry.node.mtimeMs = +mtime; + entry.node.ctimeMs = this.time(); } /** From f8c4301f14dad4ee4dd910231a67087a81212e33 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Wed, 30 May 2018 10:00:35 -0700 Subject: [PATCH 25/81] Add more errors; commandline help for --build; invalid flag combo detection --- src/compiler/commandLineParser.ts | 127 +++++++++++++++++++++++++ src/compiler/diagnosticMessages.json | 28 ++++++ src/compiler/tsbuild.ts | 64 +++++++++++++ src/compiler/tsc.ts | 137 +++------------------------ src/compiler/types.ts | 3 + 5 files changed, 236 insertions(+), 123 deletions(-) diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index 2f205f1e44e..fb0fcab4654 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -49,6 +49,14 @@ namespace ts { paramType: Diagnostics.FILE_OR_DIRECTORY, description: Diagnostics.Compile_the_project_given_the_path_to_its_configuration_file_or_to_a_folder_with_a_tsconfig_json, }, + { + name: "build", + type: "boolean", + shortName: "b", + showInSimplifiedHelpView: true, + category: Diagnostics.Command_line_Options, + description: Diagnostics.Build_one_or_more_projects_and_their_dependencies_if_out_of_date + }, { name: "pretty", type: "boolean", @@ -943,6 +951,125 @@ namespace ts { } + function getDiagnosticText(_message: DiagnosticMessage, ..._args: any[]): string { + const diagnostic = createCompilerDiagnostic.apply(undefined, arguments); + return diagnostic.messageText; + } + + /* @internal */ + export function printVersion() { + sys.write(getDiagnosticText(Diagnostics.Version_0, version) + sys.newLine); + } + + /* @internal */ + export function printHelp(optionsList: CommandLineOption[], syntaxPrefix = "") { + const output: string[] = []; + + // We want to align our "syntax" and "examples" commands to a certain margin. + const syntaxLength = getDiagnosticText(Diagnostics.Syntax_Colon_0, "").length; + const examplesLength = getDiagnosticText(Diagnostics.Examples_Colon_0, "").length; + let marginLength = Math.max(syntaxLength, examplesLength); + + // Build up the syntactic skeleton. + let syntax = makePadding(marginLength - syntaxLength); + syntax += `tsc ${syntaxPrefix}[${getDiagnosticText(Diagnostics.options)}] [${getDiagnosticText(Diagnostics.file)}...]`; + + output.push(getDiagnosticText(Diagnostics.Syntax_Colon_0, syntax)); + output.push(sys.newLine + sys.newLine); + + // Build up the list of examples. + const padding = makePadding(marginLength); + output.push(getDiagnosticText(Diagnostics.Examples_Colon_0, makePadding(marginLength - examplesLength) + "tsc hello.ts") + sys.newLine); + output.push(padding + "tsc --outFile file.js file.ts" + sys.newLine); + output.push(padding + "tsc @args.txt" + sys.newLine); + output.push(padding + "tsc --build tsconfig.json" + sys.newLine); + output.push(sys.newLine); + + output.push(getDiagnosticText(Diagnostics.Options_Colon) + sys.newLine); + + // We want our descriptions to align at the same column in our output, + // so we keep track of the longest option usage string. + marginLength = 0; + const usageColumn: string[] = []; // Things like "-d, --declaration" go in here. + const descriptionColumn: string[] = []; + + const optionsDescriptionMap = createMap(); // Map between option.description and list of option.type if it is a kind + + for (const option of optionsList) { + // If an option lacks a description, + // it is not officially supported. + if (!option.description) { + continue; + } + + let usageText = " "; + if (option.shortName) { + usageText += "-" + option.shortName; + usageText += getParamType(option); + usageText += ", "; + } + + usageText += "--" + option.name; + usageText += getParamType(option); + + usageColumn.push(usageText); + let description: string; + + if (option.name === "lib") { + description = getDiagnosticText(option.description); + const element = (option).element; + const typeMap = >element.type; + optionsDescriptionMap.set(description, arrayFrom(typeMap.keys()).map(key => `'${key}'`)); + } + else { + description = getDiagnosticText(option.description); + } + + descriptionColumn.push(description); + + // Set the new margin for the description column if necessary. + marginLength = Math.max(usageText.length, marginLength); + } + + // Special case that can't fit in the loop. + const usageText = " @<" + getDiagnosticText(Diagnostics.file) + ">"; + usageColumn.push(usageText); + descriptionColumn.push(getDiagnosticText(Diagnostics.Insert_command_line_options_and_files_from_a_file)); + marginLength = Math.max(usageText.length, marginLength); + + // Print out each row, aligning all the descriptions on the same column. + for (let i = 0; i < usageColumn.length; i++) { + const usage = usageColumn[i]; + const description = descriptionColumn[i]; + const kindsList = optionsDescriptionMap.get(description); + output.push(usage + makePadding(marginLength - usage.length + 2) + description + sys.newLine); + + if (kindsList) { + output.push(makePadding(marginLength + 4)); + for (const kind of kindsList) { + output.push(kind + " "); + } + output.push(sys.newLine); + } + } + + for (const line of output) { + sys.write(line); + } + return; + + function getParamType(option: CommandLineOption) { + if (option.paramType !== undefined) { + return " " + getDiagnosticText(option.paramType); + } + return ""; + } + + function makePadding(paddingLength: number): string { + return Array(paddingLength + 1).join(" "); + } + } + export type DiagnosticReporter = (diagnostic: Diagnostic) => void; /** * Reports config file diagnostics diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index d5d8c459ee0..f76b0a39fbd 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -3657,6 +3657,34 @@ "category": "Message", "code": 6362 }, + "Build one or more projects and their dependencies, if out-of-date": { + "category": "Message", + "code": 6363 + }, + "Delete the outputs of all projects": { + "category": "Message", + "code": 6364 + }, + "Enable verbose logging": { + "category": "Message", + "code": 6365 + }, + "Show what would be built (or deleted, if specified with --clean)": { + "category": "Message", + "code": 6366 + }, + "Build all projects, including those that appear to be up-to-date": { + "category": "Message", + "code": 6367 + }, + "Option '--build' must be the first command line argument.": { + "category": "Error", + "code": 6368 + }, + "Options '{0}' and '{1}' cannot be combined.": { + "category": "Error", + "code": 6369 + }, "Variable '{0}' implicitly has an '{1}' type.": { "category": "Error", diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index 978aa102c8a..665125b4dcc 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -338,11 +338,48 @@ namespace ts { }; } + const buildOpts: CommandLineOption[] = [ + { + name: "verbose", + shortName: "v", + category: Diagnostics.Command_line_Options, + description: Diagnostics.Enable_verbose_logging, + type: "boolean" + }, + { + name: "dry", + shortName: "d", + category: Diagnostics.Command_line_Options, + description: Diagnostics.Show_what_would_be_built_or_deleted_if_specified_with_clean, + type: "boolean" + }, + { + name: "force", + shortName: "f", + category: Diagnostics.Command_line_Options, + description: Diagnostics.Build_all_projects_including_those_that_appear_to_be_up_to_date, + type: "boolean" + }, + { + name: "clean", + category: Diagnostics.Command_line_Options, + description: Diagnostics.Delete_the_outputs_of_all_projects, + type: "boolean" + }, + { + name: "watch", + category: Diagnostics.Command_line_Options, + description: Diagnostics.Watch_input_files, + type: "boolean" + } + ]; + export function performBuild(host: CompilerHost, reportDiagnostic: DiagnosticReporter, args: string[]) { let verbose = false; let dry = false; let force = false; let clean = false; + let watch = false; const projects: string[] = []; for (const arg of args) { @@ -362,11 +399,38 @@ namespace ts { case "--clean": clean = true; continue; + case "--watch": + case "-w": + watch = true; + continue; + + case "--?": + case "-?": + case "--help": + return printHelp(buildOpts, "--build "); } // Not a flag, parse as filename addProject(arg); } + // Nonsensical combinations + if (clean && force) { + reportDiagnostic(createCompilerDiagnostic(Diagnostics.Options_0_and_1_cannot_be_combined, "clean", "force")); + return; + } + if (clean && verbose) { + reportDiagnostic(createCompilerDiagnostic(Diagnostics.Options_0_and_1_cannot_be_combined, "clean", "verbose")); + return; + } + if (clean && watch) { + reportDiagnostic(createCompilerDiagnostic(Diagnostics.Options_0_and_1_cannot_be_combined, "clean", "watch")); + return; + } + if (watch && dry) { + reportDiagnostic(createCompilerDiagnostic(Diagnostics.Options_0_and_1_cannot_be_combined, "watch", "dry")); + return; + } + if (projects.length === 0) { // tsc -b invoked with no extra arguments; act as if invoked with "tsc -b ." addProject("."); diff --git a/src/compiler/tsc.ts b/src/compiler/tsc.ts index 62abd834180..9800d2e768a 100644 --- a/src/compiler/tsc.ts +++ b/src/compiler/tsc.ts @@ -12,11 +12,6 @@ namespace ts { return count; } - function getDiagnosticText(_message: DiagnosticMessage, ..._args: any[]): string { - const diagnostic = createCompilerDiagnostic.apply(undefined, arguments); - return diagnostic.messageText; - } - let reportDiagnostic = createDiagnosticReporter(sys); function updateReportDiagnostic(options: CompilerOptions) { if (shouldBePretty(options)) { @@ -46,6 +41,13 @@ namespace ts { return s; } + function getOptionsForHelp(commandLine: ParsedCommandLine) { + // Sort our options by their names, (e.g. "--noImplicitAny" comes before "--watch") + return !!commandLine.options.all ? + sort(optionDeclarations, (a, b) => compareStringsCaseInsensitive(a.name, b.name)) : + filter(optionDeclarations.slice(), v => !!v.showInSimplifiedHelpView); + } + export function executeCommandLine(args: string[]): void { if ((args[0].toLowerCase() === "--build") || (args[0].toLowerCase() === "-b")) { return performBuild(createCompilerHost({}), createDiagnosticReporter(sys), args.slice(1)); @@ -53,6 +55,11 @@ namespace ts { const commandLine = parseCommandLine(args); + if (commandLine.options.build) { + reportDiagnostic(createCompilerDiagnostic(Diagnostics.Option_build_must_be_the_first_command_line_argument)); + return sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); + } + // Configuration file name (if any) let configFileName: string | undefined; if (commandLine.options.locale) { @@ -78,7 +85,7 @@ namespace ts { if (commandLine.options.help || commandLine.options.all) { printVersion(); - printHelp(!!commandLine.options.all); + printHelp(getOptionsForHelp(commandLine)); return sys.exit(ExitStatus.Success); } @@ -111,7 +118,7 @@ namespace ts { if (commandLine.fileNames.length === 0 && !configFileName) { printVersion(); - printHelp(!!commandLine.options.all); + printHelp(getOptionsForHelp(commandLine)); return sys.exit(ExitStatus.Success); } @@ -275,122 +282,6 @@ namespace ts { } } - function printVersion() { - sys.write(getDiagnosticText(Diagnostics.Version_0, version) + sys.newLine); - } - - function printHelp(showAllOptions: boolean) { - const output: string[] = []; - - // We want to align our "syntax" and "examples" commands to a certain margin. - const syntaxLength = getDiagnosticText(Diagnostics.Syntax_Colon_0, "").length; - const examplesLength = getDiagnosticText(Diagnostics.Examples_Colon_0, "").length; - let marginLength = Math.max(syntaxLength, examplesLength); - - // Build up the syntactic skeleton. - let syntax = makePadding(marginLength - syntaxLength); - syntax += "tsc [" + getDiagnosticText(Diagnostics.options) + "] [" + getDiagnosticText(Diagnostics.file) + " ...]"; - - output.push(getDiagnosticText(Diagnostics.Syntax_Colon_0, syntax)); - output.push(sys.newLine + sys.newLine); - - // Build up the list of examples. - const padding = makePadding(marginLength); - output.push(getDiagnosticText(Diagnostics.Examples_Colon_0, makePadding(marginLength - examplesLength) + "tsc hello.ts") + sys.newLine); - output.push(padding + "tsc --outFile file.js file.ts" + sys.newLine); - output.push(padding + "tsc @args.txt" + sys.newLine); - output.push(sys.newLine); - - output.push(getDiagnosticText(Diagnostics.Options_Colon) + sys.newLine); - - // Sort our options by their names, (e.g. "--noImplicitAny" comes before "--watch") - const optsList = showAllOptions ? - sort(optionDeclarations, (a, b) => compareStringsCaseInsensitive(a.name, b.name)) : - filter(optionDeclarations.slice(), v => !!v.showInSimplifiedHelpView); - - // We want our descriptions to align at the same column in our output, - // so we keep track of the longest option usage string. - marginLength = 0; - const usageColumn: string[] = []; // Things like "-d, --declaration" go in here. - const descriptionColumn: string[] = []; - - const optionsDescriptionMap = createMap(); // Map between option.description and list of option.type if it is a kind - - for (const option of optsList) { - // If an option lacks a description, - // it is not officially supported. - if (!option.description) { - continue; - } - - let usageText = " "; - if (option.shortName) { - usageText += "-" + option.shortName; - usageText += getParamType(option); - usageText += ", "; - } - - usageText += "--" + option.name; - usageText += getParamType(option); - - usageColumn.push(usageText); - let description: string; - - if (option.name === "lib") { - description = getDiagnosticText(option.description); - const element = (option).element; - const typeMap = >element.type; - optionsDescriptionMap.set(description, arrayFrom(typeMap.keys()).map(key => `'${key}'`)); - } - else { - description = getDiagnosticText(option.description); - } - - descriptionColumn.push(description); - - // Set the new margin for the description column if necessary. - marginLength = Math.max(usageText.length, marginLength); - } - - // Special case that can't fit in the loop. - const usageText = " @<" + getDiagnosticText(Diagnostics.file) + ">"; - usageColumn.push(usageText); - descriptionColumn.push(getDiagnosticText(Diagnostics.Insert_command_line_options_and_files_from_a_file)); - marginLength = Math.max(usageText.length, marginLength); - - // Print out each row, aligning all the descriptions on the same column. - for (let i = 0; i < usageColumn.length; i++) { - const usage = usageColumn[i]; - const description = descriptionColumn[i]; - const kindsList = optionsDescriptionMap.get(description); - output.push(usage + makePadding(marginLength - usage.length + 2) + description + sys.newLine); - - if (kindsList) { - output.push(makePadding(marginLength + 4)); - for (const kind of kindsList) { - output.push(kind + " "); - } - output.push(sys.newLine); - } - } - - for (const line of output) { - sys.write(line); - } - return; - - function getParamType(option: CommandLineOption) { - if (option.paramType !== undefined) { - return " " + getDiagnosticText(option.paramType); - } - return ""; - } - - function makePadding(paddingLength: number): string { - return Array(paddingLength + 1).join(" "); - } - } - function writeConfigFile(options: CompilerOptions, fileNames: string[]) { const currentDirectory = sys.getCurrentDirectory(); const file = normalizePath(combinePaths(currentDirectory, "tsconfig.json")); diff --git a/src/compiler/types.ts b/src/compiler/types.ts index fff1224ba96..be57e9c6443 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -4281,6 +4281,9 @@ namespace ts { allowUnusedLabels?: boolean; alwaysStrict?: boolean; // Always combine with strict property baseUrl?: string; + /** An error if set - this should only go through the -b pipeline and not actually be observed */ + /*@internal*/ + build?: boolean; charset?: string; checkJs?: boolean; /* @internal */ configFilePath?: string; From cd7a844a48c8d2bd31e5eb1df49cd0dce8a5a8d2 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Wed, 30 May 2018 10:12:51 -0700 Subject: [PATCH 26/81] We definitely have a type system --- src/compiler/tsbuild.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index 665125b4dcc..2a509a8c746 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -286,7 +286,8 @@ namespace ts { const outputs: string[] = []; outputs.push(project.options.outFile); if (project.options.declaration) { - const dts = outputs.push(changeExtension(project.options.outFile, ".d.ts")); + const dts = changeExtension(project.options.outFile, ".d.ts"); + outputs.push(dts); if (project.options.declarationMap) { outputs.push(dts + ".map"); } From 8adbf85e0e80a9044e325a06e2ba7b2c8a7d6562 Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Wed, 30 May 2018 17:53:26 -0700 Subject: [PATCH 27/81] Candidate sectional sourcemap emit implementation --- src/compiler/factory.ts | 7 +- src/compiler/program.ts | 4 +- src/compiler/sourcemap.ts | 115 ++++++++++++++++-- src/compiler/transformers/declarations.ts | 2 +- src/compiler/transformers/ts.ts | 2 +- src/compiler/types.ts | 3 + src/compiler/utilities.ts | 4 + .../reference/api/tsserverlibrary.d.ts | 8 +- tests/baselines/reference/api/typescript.d.ts | 8 +- 9 files changed, 133 insertions(+), 20 deletions(-) diff --git a/src/compiler/factory.ts b/src/compiler/factory.ts index 6085cc550c4..111be243769 100644 --- a/src/compiler/factory.ts +++ b/src/compiler/factory.ts @@ -2585,16 +2585,19 @@ namespace ts { return node; } - export function createUnparsedSourceFile(text: string): UnparsedSource { + export function createUnparsedSourceFile(text: string, map?: string): UnparsedSource { const node = createNode(SyntaxKind.UnparsedSource); node.text = text; + node.sourceMapText = map; return node; } - export function createInputFiles(javascript: string, declaration: string): InputFiles { + export function createInputFiles(javascript: string, declaration: string, javascriptMapText?: string, declarationMapText?: string): InputFiles { const node = createNode(SyntaxKind.InputFiles); node.javascriptText = javascript; + node.javascriptMapText = javascriptMapText; node.declarationText = declaration; + node.declarationMapText = declarationMapText; return node; } diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 4dfb9039ccf..2c8a702e139 100755 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -1192,8 +1192,10 @@ namespace ts { const dtsFilename = changeExtension(resolvedRefOpts.options.outFile, ".d.ts"); const js = host.readFile(resolvedRefOpts.options.outFile) || `/* Input file ${resolvedRefOpts.options.outFile} was missing */\r\n`; + const jsMap = host.readFile(resolvedRefOpts.options.outFile + ".map"); // TODO: try to read sourceMappingUrl comment from the js file const dts = host.readFile(dtsFilename) || `/* Input file ${dtsFilename} was missing */\r\n`; - const node = createInputFiles(js, dts); + const dtsMap = host.readFile(dtsFilename + ".map"); + const node = createInputFiles(js, dts, jsMap, dtsMap); nodes.push(node); } } diff --git a/src/compiler/sourcemap.ts b/src/compiler/sourcemap.ts index a274404f320..1f8723b9c80 100644 --- a/src/compiler/sourcemap.ts +++ b/src/compiler/sourcemap.ts @@ -99,6 +99,10 @@ namespace ts { let sourceMapDataList: SourceMapData[] | undefined; let disabled: boolean = !(compilerOptions.sourceMap || compilerOptions.inlineSourceMap); + let completedSections: SourceMapSectionDefinition[]; + let sectionStartLine: number; + let sectionStartColumn: number; + return { initialize, reset, @@ -146,6 +150,9 @@ namespace ts { lastEncodedNameIndex = 0; // Initialize source map data + completedSections = []; + sectionStartLine = 0; + sectionStartColumn = 0; sourceMapData = { sourceMapFilePath, jsSourceMappingURL: !compilerOptions.inlineSourceMap ? getBaseFileName(normalizeSlashes(sourceMapFilePath)) : undefined!, // TODO: GH#18217 @@ -214,6 +221,68 @@ namespace ts { lastEncodedNameIndex = undefined; sourceMapData = undefined!; sourceMapDataList = undefined!; + completedSections = undefined!; + sectionStartLine = undefined!; + sectionStartColumn = undefined!; + } + + interface SourceMapSection { + version: 3; + file: string; + sourceRoot?: string; + sources: string[]; + names?: string[]; + mappings: string; + sourcesContent?: string[]; + sections?: undefined; + } + + type SourceMapSectionDefinition = + | { offset: { line: number, column: number }, url: string } // Included for completeness + | { offset: { line: number, column: number }, map: SourceMap }; + + interface SectionalSourceMap { + version: 3; + file: string; + sections: SourceMapSectionDefinition[]; + } + + type SourceMap = SectionalSourceMap | SourceMapSection; + + function captureSection(): SourceMapSection { + return { + version: 3, + file: sourceMapData.sourceMapFile, + sourceRoot: sourceMapData.sourceMapSourceRoot, + sources: sourceMapData.sourceMapSources, + names: sourceMapData.sourceMapNames, + mappings: sourceMapData.sourceMapMappings, + sourcesContent: sourceMapData.sourceMapSourcesContent, + }; + } + + function resetSectionalData(): void { + sourceMapData.sourceMapSources = []; + sourceMapData.sourceMapNames = []; + sourceMapData.sourceMapMappings = ""; + sourceMapData.sourceMapSourcesContent = compilerOptions.inlineSources ? [] : undefined; + } + + function generateMap(): SourceMap { + if (completedSections.length) { + const last = { + offset: { line: sectionStartLine, column: sectionStartColumn }, + map: captureSection() + }; + return { + version: 3, + file: last.map.file, + sections: [...completedSections, last] + }; + } + else { + return captureSection(); + } } // Encoding for sourcemap span @@ -284,8 +353,8 @@ namespace ts { sourceLinePos.line++; sourceLinePos.character++; - const emittedLine = writer.getLine(); - const emittedColumn = writer.getColumn(); + const emittedLine = writer.getLine() - sectionStartLine; + const emittedColumn = emittedLine === 0 ? writer.getColumn() - sectionStartColumn : writer.getColumn(); // If this location wasn't recorded or the location in source is going backwards, record the span if (!lastRecordedSourceMapSpan || @@ -333,6 +402,38 @@ namespace ts { } if (node) { + if (isUnparsedSource(node) && node.sourceMapText !== undefined) { + if (lastRecordedSourceMapSpan && lastRecordedSourceMapSpan === lastEncodedSourceMapSpan) { // If we've recorded some spans, save them + completedSections.push({ offset: { line: sectionStartLine, column: sectionStartColumn }, map: captureSection() }); + resetSectionalData(); + } + const text = node.sourceMapText; + let parsed: {} | undefined; + try { + parsed = JSON.parse(text); + } + catch { + // empty + } + const offset = { line: writer.getLine(), column: writer.getColumn() }; + completedSections.push(parsed + ? { + offset, + map: parsed as SourceMap + } + : { + offset, + // This is just passes the buck on sourcemaps we don't really understand, instead of issuing an error (which would be difficult this late) + url: `data:application/json;charset=utf-8;base64,${base64encode(sys, text)}` + } + ); + const emitResult = emitCallback(hint, node); + sectionStartLine = writer.getLine(); + sectionStartColumn = writer.getColumn(); + lastRecordedSourceMapSpan = undefined!; + lastEncodedSourceMapSpan = undefined!; + return emitResult; + } const emitNode = node.emitNode; const emitFlags = emitNode && emitNode.flags || EmitFlags.None; const range = emitNode && emitNode.sourceMapRange; @@ -452,15 +553,7 @@ namespace ts { encodeLastRecordedSourceMapSpan(); - return JSON.stringify({ - version: 3, - file: sourceMapData.sourceMapFile, - sourceRoot: sourceMapData.sourceMapSourceRoot, - sources: sourceMapData.sourceMapSources, - names: sourceMapData.sourceMapNames, - mappings: sourceMapData.sourceMapMappings, - sourcesContent: sourceMapData.sourceMapSourcesContent, - }); + return JSON.stringify(generateMap()); } /** diff --git a/src/compiler/transformers/declarations.ts b/src/compiler/transformers/declarations.ts index a92448e5290..3469ef125a2 100644 --- a/src/compiler/transformers/declarations.ts +++ b/src/compiler/transformers/declarations.ts @@ -180,7 +180,7 @@ namespace ts { } ), mapDefined(node.prepends, prepend => { if (prepend.kind === SyntaxKind.InputFiles) { - return createUnparsedSourceFile(prepend.declarationText); + return createUnparsedSourceFile(prepend.declarationText, prepend.declarationMapText); } })); bundle.syntheticFileReferences = []; diff --git a/src/compiler/transformers/ts.ts b/src/compiler/transformers/ts.ts index cc244d92204..03bafc314c7 100644 --- a/src/compiler/transformers/ts.ts +++ b/src/compiler/transformers/ts.ts @@ -100,7 +100,7 @@ namespace ts { function transformBundle(node: Bundle) { return createBundle(node.sourceFiles.map(transformSourceFile), mapDefined(node.prepends, prepend => { if (prepend.kind === SyntaxKind.InputFiles) { - return createUnparsedSourceFile(prepend.javascriptText); + return createUnparsedSourceFile(prepend.javascriptText, prepend.javascriptMapText); } return prepend; })); diff --git a/src/compiler/types.ts b/src/compiler/types.ts index b13f4aea613..5275cdb7dfd 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -2652,12 +2652,15 @@ namespace ts { export interface InputFiles extends Node { kind: SyntaxKind.InputFiles; javascriptText: string; + javascriptMapText?: string; declarationText: string; + declarationMapText?: string; } export interface UnparsedSource extends Node { kind: SyntaxKind.UnparsedSource; text: string; + sourceMapText?: string; } export interface JsonSourceFile extends SourceFile { diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 426c0d8d1da..48289a43836 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -5454,6 +5454,10 @@ namespace ts { return node.kind === SyntaxKind.Bundle; } + export function isUnparsedSource(node: Node): node is UnparsedSource { + return node.kind === SyntaxKind.UnparsedSource; + } + // JSDoc export function isJSDocTypeExpression(node: Node): node is JSDocTypeExpression { diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index a6657b4d341..aa40d060bda 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -1669,11 +1669,14 @@ declare namespace ts { interface InputFiles extends Node { kind: SyntaxKind.InputFiles; javascriptText: string; + javascriptMapText?: string; declarationText: string; + declarationMapText?: string; } interface UnparsedSource extends Node { kind: SyntaxKind.UnparsedSource; text: string; + sourceMapText?: string; } interface JsonSourceFile extends SourceFile { statements: NodeArray; @@ -3371,6 +3374,7 @@ declare namespace ts { function isEnumMember(node: Node): node is EnumMember; function isSourceFile(node: Node): node is SourceFile; function isBundle(node: Node): node is Bundle; + function isUnparsedSource(node: Node): node is UnparsedSource; function isJSDocTypeExpression(node: Node): node is JSDocTypeExpression; function isJSDocAllType(node: JSDocAllType): node is JSDocAllType; function isJSDocUnknownType(node: Node): node is JSDocUnknownType; @@ -3826,8 +3830,8 @@ declare namespace ts { function createCommaList(elements: ReadonlyArray): CommaListExpression; function updateCommaList(node: CommaListExpression, elements: ReadonlyArray): CommaListExpression; function createBundle(sourceFiles: ReadonlyArray, prepends?: ReadonlyArray): Bundle; - function createUnparsedSourceFile(text: string): UnparsedSource; - function createInputFiles(javascript: string, declaration: string): InputFiles; + function createUnparsedSourceFile(text: string, map?: string): UnparsedSource; + function createInputFiles(javascript: string, declaration: string, javascriptMapText?: string, declarationMapText?: string): InputFiles; function updateBundle(node: Bundle, sourceFiles: ReadonlyArray, prepends?: ReadonlyArray): Bundle; function createImmediatelyInvokedFunctionExpression(statements: ReadonlyArray): CallExpression; function createImmediatelyInvokedFunctionExpression(statements: ReadonlyArray, param: ParameterDeclaration, paramValue: Expression): CallExpression; diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index d71bb5117ba..25f29c4f479 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -1669,11 +1669,14 @@ declare namespace ts { interface InputFiles extends Node { kind: SyntaxKind.InputFiles; javascriptText: string; + javascriptMapText?: string; declarationText: string; + declarationMapText?: string; } interface UnparsedSource extends Node { kind: SyntaxKind.UnparsedSource; text: string; + sourceMapText?: string; } interface JsonSourceFile extends SourceFile { statements: NodeArray; @@ -3371,6 +3374,7 @@ declare namespace ts { function isEnumMember(node: Node): node is EnumMember; function isSourceFile(node: Node): node is SourceFile; function isBundle(node: Node): node is Bundle; + function isUnparsedSource(node: Node): node is UnparsedSource; function isJSDocTypeExpression(node: Node): node is JSDocTypeExpression; function isJSDocAllType(node: JSDocAllType): node is JSDocAllType; function isJSDocUnknownType(node: Node): node is JSDocUnknownType; @@ -3826,8 +3830,8 @@ declare namespace ts { function createCommaList(elements: ReadonlyArray): CommaListExpression; function updateCommaList(node: CommaListExpression, elements: ReadonlyArray): CommaListExpression; function createBundle(sourceFiles: ReadonlyArray, prepends?: ReadonlyArray): Bundle; - function createUnparsedSourceFile(text: string): UnparsedSource; - function createInputFiles(javascript: string, declaration: string): InputFiles; + function createUnparsedSourceFile(text: string, map?: string): UnparsedSource; + function createInputFiles(javascript: string, declaration: string, javascriptMapText?: string, declarationMapText?: string): InputFiles; function updateBundle(node: Bundle, sourceFiles: ReadonlyArray, prepends?: ReadonlyArray): Bundle; function createImmediatelyInvokedFunctionExpression(statements: ReadonlyArray): CallExpression; function createImmediatelyInvokedFunctionExpression(statements: ReadonlyArray, param: ParameterDeclaration, paramValue: Expression): CallExpression; From eb1973a4fd129eb7deb5aad5a4f376bba396c21b Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Wed, 30 May 2018 19:29:47 -0700 Subject: [PATCH 28/81] Fixup sourcemap positions and text writer calculations --- src/compiler/emitter.ts | 2 +- src/compiler/sourcemap.ts | 35 ++++++++++--------- src/compiler/utilities.ts | 28 +++++++++------ .../baselines/reference/tsxErrorRecovery1.js | 2 +- .../tsxStatelessFunctionComponents3.js | 2 +- 5 files changed, 40 insertions(+), 29 deletions(-) diff --git a/src/compiler/emitter.ts b/src/compiler/emitter.ts index 5e53605decf..570dfc99653 100644 --- a/src/compiler/emitter.ts +++ b/src/compiler/emitter.ts @@ -1026,7 +1026,7 @@ namespace ts { // SyntaxKind.UnparsedSource function emitUnparsedSource(unparsed: UnparsedSource) { - write(unparsed.text); + writer.rawWrite(unparsed.text); } // diff --git a/src/compiler/sourcemap.ts b/src/compiler/sourcemap.ts index 1f8723b9c80..a93aedebf27 100644 --- a/src/compiler/sourcemap.ts +++ b/src/compiler/sourcemap.ts @@ -151,8 +151,8 @@ namespace ts { // Initialize source map data completedSections = []; - sectionStartLine = 0; - sectionStartColumn = 0; + sectionStartLine = 1; + sectionStartColumn = 1; sourceMapData = { sourceMapFilePath, jsSourceMappingURL: !compilerOptions.inlineSourceMap ? getBaseFileName(normalizeSlashes(sourceMapFilePath)) : undefined!, // TODO: GH#18217 @@ -270,14 +270,11 @@ namespace ts { function generateMap(): SourceMap { if (completedSections.length) { - const last = { - offset: { line: sectionStartLine, column: sectionStartColumn }, - map: captureSection() - }; + captureSectionalSpanIfNeeded(/*reset*/ false); return { version: 3, - file: last.map.file, - sections: [...completedSections, last] + file: sourceMapData.sourceMapFile, + sections: completedSections }; } else { @@ -353,8 +350,8 @@ namespace ts { sourceLinePos.line++; sourceLinePos.character++; - const emittedLine = writer.getLine() - sectionStartLine; - const emittedColumn = emittedLine === 0 ? writer.getColumn() - sectionStartColumn : writer.getColumn(); + const emittedLine = writer.getLine() - sectionStartLine + 1; + const emittedColumn = emittedLine === 0 ? (writer.getColumn() - sectionStartColumn + 1) : writer.getColumn(); // If this location wasn't recorded or the location in source is going backwards, record the span if (!lastRecordedSourceMapSpan || @@ -389,6 +386,15 @@ namespace ts { } } + function captureSectionalSpanIfNeeded(reset: boolean) { + if (lastRecordedSourceMapSpan && lastRecordedSourceMapSpan === lastEncodedSourceMapSpan) { // If we've recorded some spans, save them + completedSections.push({ offset: { line: sectionStartLine - 1, column: sectionStartColumn - 1 }, map: captureSection() }); + if (reset) { + resetSectionalData(); + } + } + } + /** * Emits a node with possible leading and trailing source maps. * @@ -403,10 +409,7 @@ namespace ts { if (node) { if (isUnparsedSource(node) && node.sourceMapText !== undefined) { - if (lastRecordedSourceMapSpan && lastRecordedSourceMapSpan === lastEncodedSourceMapSpan) { // If we've recorded some spans, save them - completedSections.push({ offset: { line: sectionStartLine, column: sectionStartColumn }, map: captureSection() }); - resetSectionalData(); - } + captureSectionalSpanIfNeeded(/*reset*/ true); const text = node.sourceMapText; let parsed: {} | undefined; try { @@ -415,7 +418,7 @@ namespace ts { catch { // empty } - const offset = { line: writer.getLine(), column: writer.getColumn() }; + const offset = { line: writer.getLine() - 1, column: writer.getColumn() - 1 }; completedSections.push(parsed ? { offset, @@ -431,7 +434,7 @@ namespace ts { sectionStartLine = writer.getLine(); sectionStartColumn = writer.getColumn(); lastRecordedSourceMapSpan = undefined!; - lastEncodedSourceMapSpan = undefined!; + lastEncodedSourceMapSpan = defaultLastEncodedSourceMapSpan; return emitResult; } const emitNode = node.emitNode; diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 48289a43836..70fafe8fb66 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -2825,13 +2825,26 @@ namespace ts { let lineCount: number; let linePos: number; + function updateLineCountAndPosFor(s: string) { + const lineStartsOfS = computeLineStarts(s); + if (lineStartsOfS.length > 1) { + lineCount = lineCount + lineStartsOfS.length - 1; + linePos = output.length - s.length + last(lineStartsOfS); + lineStart = (linePos - output.length) === 0; + } + else { + lineStart = false; + } + } + function write(s: string) { if (s && s.length) { if (lineStart) { - output += getIndentString(indent); + s = getIndentString(indent) + s; lineStart = false; } output += s; + updateLineCountAndPosFor(s); } } @@ -2845,21 +2858,14 @@ namespace ts { function rawWrite(s: string) { if (s !== undefined) { - if (lineStart) { - lineStart = false; - } output += s; + updateLineCountAndPosFor(s); } } function writeLiteral(s: string) { if (s && s.length) { write(s); - const lineStartsOfS = computeLineStarts(s); - if (lineStartsOfS.length > 1) { - lineCount = lineCount + lineStartsOfS.length - 1; - linePos = output.length - s.length + last(lineStartsOfS); - } } } @@ -2873,7 +2879,9 @@ namespace ts { } function writeTextOfNode(text: string, node: Node) { - write(getTextOfNodeFromSourceText(text, node)); + const s = getTextOfNodeFromSourceText(text, node); + write(s); + updateLineCountAndPosFor(s); } reset(); diff --git a/tests/baselines/reference/tsxErrorRecovery1.js b/tests/baselines/reference/tsxErrorRecovery1.js index 7abf1346c35..d91c464c6f9 100644 --- a/tests/baselines/reference/tsxErrorRecovery1.js +++ b/tests/baselines/reference/tsxErrorRecovery1.js @@ -14,5 +14,5 @@ function foo() { } // Shouldn't see any errors down here var y = {a} 1 }; -; + ; } diff --git a/tests/baselines/reference/tsxStatelessFunctionComponents3.js b/tests/baselines/reference/tsxStatelessFunctionComponents3.js index e7bfc980229..152c7e6ba4f 100644 --- a/tests/baselines/reference/tsxStatelessFunctionComponents3.js +++ b/tests/baselines/reference/tsxStatelessFunctionComponents3.js @@ -27,7 +27,7 @@ define(["require", "exports", "react"], function (require, exports, React) { // Should be OK var MainMenu = function (props) { return (

Main Menu

-
); }; + ); }; var App = function (_a) { var children = _a.children; return (
From d21c03ab9d9f280dc822875d8120824220da2c4d Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Thu, 31 May 2018 14:38:26 -0700 Subject: [PATCH 29/81] Invalidation + separated downstream builds --- src/compiler/diagnosticMessages.json | 4 + src/compiler/tsbuild.ts | 194 ++++++++++++++++++++++----- src/harness/unittests/tsbuild.ts | 96 ++++++++----- 3 files changed, 227 insertions(+), 67 deletions(-) diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index f76b0a39fbd..e9022bff0df 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -3685,6 +3685,10 @@ "category": "Error", "code": 6369 }, + "Skipping clean because not all projects could be located": { + "category": "Error", + "code": 6340 + }, "Variable '{0}' implicitly has an '{1}' type.": { "category": "Error", diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index 2a509a8c746..1f36f09393b 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -38,6 +38,10 @@ namespace ts { * Issue a verbose diagnostic message. No-ops when options.verbose is false. */ verbose(diag: DiagnosticMessage, ...args: any[]): void; + + invalidatedProjects: FileMap; + queuedProjects: FileMap; + missingRoots: Map; } type Mapper = ReturnType; @@ -73,7 +77,7 @@ namespace ts { AnyErrors = ConfigFileErrors | SyntaxErrors | TypeErrors | DeclarationEmitErrors } - enum UpToDateStatusType { + export enum UpToDateStatusType { Unbuildable, UpToDate, /** @@ -89,7 +93,7 @@ namespace ts { UpstreamBlocked } - type UpToDateStatus = + export type UpToDateStatus = | Status.Unbuildable | Status.UpToDate | Status.OutputMissing @@ -98,7 +102,7 @@ namespace ts { | Status.UpstreamOutOfDate | Status.UpstreamBlocked; - namespace Status { + export namespace Status { /** * The project can't be built at all in its current state. For example, * its config file cannot be parsed, or it has a syntax error or missing file @@ -170,6 +174,9 @@ namespace ts { setValue(fileName: string, value: T): void; getValue(fileName: string): T | never; getValueOrUndefined(fileName: string): T | undefined; + hasKey(fileName: string): boolean; + removeKey(fileName: string): void; + getKeys(): string[]; } /** @@ -183,8 +190,23 @@ namespace ts { setValue, getValue, getValueOrUndefined, + removeKey, + getKeys, + hasKey }; + function getKeys(): string[] { + return Object.keys(lookup); + } + + function hasKey(fileName: string) { + return normalizePath(fileName) in lookup; + } + + function removeKey(fileName: string) { + delete lookup[fileName]; + } + function setValue(fileName: string, value: T) { lookup[normalizePath(fileName)] = value; } @@ -211,30 +233,30 @@ namespace ts { } export function createDependencyMapper() { - const childToParents: { [key: string]: string[] } = {}; - const parentToChildren: { [key: string]: string[] } = {}; - const allKeys: string[] = []; + const childToParents: { [key: string]: ResolvedConfigFileName[] } = {}; + const parentToChildren: { [key: string]: ResolvedConfigFileName[] } = {}; + const allKeys: ResolvedConfigFileName[] = []; - function addReference(childConfigFileName: string, parentConfigFileName: string): void { + function addReference(childConfigFileName: ResolvedConfigFileName, parentConfigFileName: ResolvedConfigFileName): void { addEntry(childToParents, childConfigFileName, parentConfigFileName); addEntry(parentToChildren, parentConfigFileName, childConfigFileName); } - function getReferencesTo(parentConfigFileName: string): string[] { + function getReferencesTo(parentConfigFileName: ResolvedConfigFileName): ResolvedConfigFileName[] { return parentToChildren[normalizePath(parentConfigFileName)] || []; } - function getReferencesOf(childConfigFileName: string): string[] { + function getReferencesOf(childConfigFileName: ResolvedConfigFileName): ResolvedConfigFileName[] { return childToParents[normalizePath(childConfigFileName)] || []; } - function getKeys(): ReadonlyArray { + function getKeys(): ReadonlyArray { return allKeys; } - function addEntry(mapToAddTo: typeof childToParents | typeof parentToChildren, key: string, element: string) { - key = normalizePath(key); - element = normalizePath(element); + function addEntry(mapToAddTo: typeof childToParents | typeof parentToChildren, key: ResolvedConfigFileName, element: ResolvedConfigFileName) { + key = normalizePath(key) as ResolvedConfigFileName; + element = normalizePath(element) as ResolvedConfigFileName; const arr = (mapToAddTo[key] = mapToAddTo[key] || []); if (arr.indexOf(element) < 0) { arr.push(element); @@ -316,8 +338,13 @@ namespace ts { return parsed; } + function removeKey(configFilePath: ResolvedConfigFileName) { + cache.removeKey(configFilePath); + } + return { - parseConfigFile + parseConfigFile, + removeKey }; } @@ -331,11 +358,19 @@ namespace ts { export function createBuildContext(options: BuildOptions, reportDiagnostic: DiagnosticReporter): BuildContext { const verboseDiag = options.verbose && reportDiagnostic; + + const invalidatedProjects = createFileMap(); + const queuedProjects = createFileMap(); + const missingRoots = createMap(); + return { options, projectStatus: createFileMap(), unchangedOutputs: createFileMap(), - verbose: verboseDiag ? (diag, ...args) => verboseDiag(createCompilerDiagnostic(diag, ...args)) : () => undefined + verbose: verboseDiag ? (diag, ...args) => verboseDiag(createCompilerDiagnostic(diag, ...args)) : () => undefined, + invalidatedProjects, + missingRoots, + queuedProjects }; } @@ -437,12 +472,12 @@ namespace ts { addProject("."); } - const builder = createSolutionBuilder(host, reportDiagnostic, { verbose, dry, force }); + const builder = createSolutionBuilder(host, projects, reportDiagnostic, { verbose, dry, force }); if (clean) { - builder.cleanProjects(projects); + builder.cleanAllProjects(); } else { - builder.buildProjects(projects); + builder.buildAllProjects(); } function addProject(projectSpecification: string) { @@ -461,7 +496,11 @@ namespace ts { } } - export function createSolutionBuilder(host: CompilerHost, reportDiagnostic: DiagnosticReporter, defaultOptions: BuildOptions) { + /** + * A SolutionBuilder has an immutable set of rootNames that are the "entry point" projects, but + * can dynamically add/remove other projects based on changes on the rootNames' references + */ + export function createSolutionBuilder(host: CompilerHost, rootNames: ReadonlyArray, reportDiagnostic: DiagnosticReporter, defaultOptions: BuildOptions) { if (!host.getModifiedTime || !host.setModifiedTime) { throw new Error("Host must support timestamp APIs"); } @@ -470,12 +509,18 @@ namespace ts { let context = createBuildContext(defaultOptions, reportDiagnostic); return { + buildAllProjects, getUpToDateStatus, getUpToDateStatusOfFile, - buildProjects, - cleanProjects, + cleanAllProjects, resetBuildContext, - getBuildGraph + getBuildGraph, + + invalidateProject, + buildInvalidatedProjects, + buildDependentInvalidatedProjects, + + resolveProjectName }; function resetBuildContext(opts = defaultOptions) { @@ -486,13 +531,17 @@ namespace ts { return getUpToDateStatus(configFileCache.parseConfigFile(configFileName)); } - function getBuildGraph(configFileNames: string[]) { + function getBuildGraph(configFileNames: ReadonlyArray) { const resolvedNames: ResolvedConfigFileName[] | undefined = resolveProjectNames(configFileNames); - if (resolvedNames === undefined) return; + if (resolvedNames === undefined) return undefined; return createDependencyGraph(resolvedNames); } + function getGlobalDependencyGraph() { + return getBuildGraph(rootNames); + } + function getUpToDateStatus(project: ParsedCommandLine | undefined): UpToDateStatus { if (project === undefined) { return { type: UpToDateStatusType.Unbuildable, reason: "File deleted mid-build" }; @@ -507,6 +556,73 @@ namespace ts { return actual; } + function invalidateProject(configFileName: string) { + const resolved = resolveProjectName(configFileName); + if (resolved === undefined) { + // If this was a rootName, we need to track it as missing. + // Otherwise we can just ignore it and have it possibly surface as an error in any downstream projects, + // if they exist + + // TODO: do those things + return; + } + + configFileCache.removeKey(resolved); + context.invalidatedProjects.setValue(resolved, true); + context.projectStatus.removeKey(resolved); + + const graph = getGlobalDependencyGraph()!; + if (graph) { + queueBuildForDownstreamReferences(resolved); + } + + // Mark all downstream projects of this one needing to be built "later" + function queueBuildForDownstreamReferences(root: ResolvedConfigFileName) { + debugger; + const deps = graph.dependencyMap.getReferencesTo(root); + for (const ref of deps) { + // Can skip circular references + if (!context.queuedProjects.hasKey(ref)) { + context.queuedProjects.setValue(ref, true); + queueBuildForDownstreamReferences(ref); + } + } + } + } + + function buildInvalidatedProjects() { + buildSomeProjects(p => context.invalidatedProjects.hasKey(p)); + } + + function buildDependentInvalidatedProjects() { + buildSomeProjects(p => context.queuedProjects.hasKey(p)); + } + + function buildSomeProjects(predicate: (projName: ResolvedConfigFileName) => boolean) { + const resolvedNames: ResolvedConfigFileName[] | undefined = resolveProjectNames(rootNames); + if (resolvedNames === undefined) return; + + const graph = createDependencyGraph(resolvedNames)!; + for (const next of graph.buildQueue) { + if (!predicate(next)) continue; + + const resolved = resolveProjectName(next); + if (!resolved) continue; // ?? + const proj = configFileCache.parseConfigFile(resolved); + if (!proj) continue; // ? + + const status = getUpToDateStatus(proj); + reportProjectStatus(next, status); + + if (status.type === UpToDateStatusType.UpstreamBlocked) { + context.verbose(Diagnostics.Skipping_build_of_project_0_because_its_upstream_project_1_has_errors, resolved, status.upstreamProjectName); + continue; + } + + buildSingleProject(next); + } + } + function getAllProjectOutputs(project: ParsedCommandLine): ReadonlyArray { if (project.options.outFile) { return getOutFileOutputs(project); @@ -824,7 +940,7 @@ namespace ts { context.projectStatus.setValue(proj.options.configFilePath!, { type: UpToDateStatusType.UpToDate, newestDeclarationFileContentChangedTime: priorNewestUpdateTime } as UpToDateStatus); } - function getFilesToClean(configFileNames: ResolvedConfigFileName[]): string[] | undefined { + function getFilesToClean(configFileNames: ReadonlyArray): string[] | undefined { const resolvedNames: ResolvedConfigFileName[] | undefined = resolveProjectNames(configFileNames); if (resolvedNames === undefined) return undefined; @@ -849,12 +965,24 @@ namespace ts { return filesToDelete; } - function cleanProjects(configFileNames: string[]) { - const resolvedNames: ResolvedConfigFileName[] | undefined = resolveProjectNames(configFileNames); - if (resolvedNames === undefined) return; + function getAllProjectsInScope(): ReadonlyArray | undefined { + const resolvedNames = resolveProjectNames(rootNames); + if (resolvedNames === undefined) return undefined; + const graph = createDependencyGraph(resolvedNames); + if (graph === undefined) return undefined; + return graph.buildQueue; + } + + function cleanAllProjects() { + const resolvedNames: ReadonlyArray | undefined = getAllProjectsInScope(); + if (resolvedNames === undefined) { + reportDiagnostic(createCompilerDiagnostic(Diagnostics.Skipping_clean_because_not_all_projects_could_be_located)); + return; + } const filesToDelete = getFilesToClean(resolvedNames); if (filesToDelete === undefined) { + reportDiagnostic(createCompilerDiagnostic(Diagnostics.Skipping_clean_because_not_all_projects_could_be_located)); return; } @@ -885,7 +1013,7 @@ namespace ts { return undefined; } - function resolveProjectNames(configFileNames: string[]): ResolvedConfigFileName[] | undefined { + function resolveProjectNames(configFileNames: ReadonlyArray): ResolvedConfigFileName[] | undefined { const resolvedNames: ResolvedConfigFileName[] = []; for (const name of configFileNames) { const resolved = resolveProjectName(name); @@ -897,12 +1025,8 @@ namespace ts { return resolvedNames; } - function buildProjects(configFileNames: string[]) { - const resolvedNames: ResolvedConfigFileName[] | undefined = resolveProjectNames(configFileNames); - if (resolvedNames === undefined) return; - - // Establish what needs to be built - const graph = createDependencyGraph(resolvedNames); + function buildAllProjects() { + const graph = getGlobalDependencyGraph(); if (graph === undefined) return; const queue = graph.buildQueue; diff --git a/src/harness/unittests/tsbuild.ts b/src/harness/unittests/tsbuild.ts index ed1fd967952..46fee63a7c0 100644 --- a/src/harness/unittests/tsbuild.ts +++ b/src/harness/unittests/tsbuild.ts @@ -19,11 +19,10 @@ namespace ts { it("can build the sample project 'sample1' without error", () => { const fs = bfs.shadow(); const host = new fakes.CompilerHost(fs); - const builder = createSolutionBuilder(host, reportDiagnostic, { dry: false, force: false, verbose: false }); + const builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: false, force: false, verbose: false }); clearDiagnostics(); - fs.chdir("/src/tests"); - builder.buildProjects(["."]); + builder.buildAllProjects(); assertDiagnosticMessages(/*empty*/); // Check for outputs. Not an exhaustive list @@ -38,9 +37,8 @@ namespace ts { clearDiagnostics(); const fs = bfs.shadow(); const host = new fakes.CompilerHost(fs); - const builder = createSolutionBuilder(host, reportDiagnostic, { dry: true, force: false, verbose: false }); - fs.chdir("/src/tests"); - builder.buildProjects(["."]); + const builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: true, force: false, verbose: false }); + builder.buildAllProjects(); assertDiagnosticMessages(Diagnostics.Would_build_project_0, Diagnostics.Would_build_project_0, Diagnostics.Would_build_project_0); // Check for outputs to not be written. Not an exhaustive list @@ -54,14 +52,13 @@ namespace ts { const fs = bfs.shadow(); const host = new fakes.CompilerHost(fs); - let builder = createSolutionBuilder(host, reportDiagnostic, { dry: false, force: false, verbose: false }); - fs.chdir("/src/tests"); - builder.buildProjects(["."]); + let builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: false, force: false, verbose: false }); + builder.buildAllProjects(); tick(); clearDiagnostics(); - builder = createSolutionBuilder(host, reportDiagnostic, { dry: true, force: false, verbose: false }); - builder.buildProjects(["."]); + builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: true, force: false, verbose: false }); + builder.buildAllProjects(); assertDiagnosticMessages(Diagnostics.Project_0_is_up_to_date, Diagnostics.Project_0_is_up_to_date, Diagnostics.Project_0_is_up_to_date); }); }); @@ -72,20 +69,19 @@ namespace ts { const fs = bfs.shadow(); const host = new fakes.CompilerHost(fs); - const builder = createSolutionBuilder(host, reportDiagnostic, { dry: false, force: false, verbose: false }); - fs.chdir("/src/tests"); - builder.buildProjects(["."]); + const builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: false, force: false, verbose: false }); + builder.buildAllProjects(); // Verify they exist for (const output of allExpectedOutputs) { assert(fs.existsSync(output), `Expect file ${output} to exist`); } - builder.cleanProjects(["."]); + builder.cleanAllProjects(); // Verify they are gone for (const output of allExpectedOutputs) { assert(!fs.existsSync(output), `Expect file ${output} to not exist`); } // Subsequent clean shouldn't throw / etc - builder.cleanProjects(["."]); + builder.cleanAllProjects(); }); }); @@ -94,16 +90,15 @@ namespace ts { const fs = bfs.shadow(); const host = new fakes.CompilerHost(fs); - const builder = createSolutionBuilder(host, reportDiagnostic, { dry: false, force: true, verbose: false }); - fs.chdir("/src/tests"); - builder.buildProjects(["."]); + const builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: false, force: true, verbose: false }); + builder.buildAllProjects(); let currentTime = time(); checkOutputTimestamps(currentTime); tick(); Debug.assert(time() !== currentTime, "Time moves on"); currentTime = time(); - builder.buildProjects(["."]); + builder.buildAllProjects(); checkOutputTimestamps(currentTime); function checkOutputTimestamps(expected: number) { @@ -119,14 +114,12 @@ namespace ts { describe("tsbuild - can detect when and what to rebuild", () => { const fs = bfs.shadow(); const host = new fakes.CompilerHost(fs); - const builder = createSolutionBuilder(host, reportDiagnostic, { dry: false, force: false, verbose: true }); - - fs.chdir("/src/tests"); + const builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: false, force: false, verbose: true }); it("Builds the project", () => { clearDiagnostics(); builder.resetBuildContext(); - builder.buildProjects(["."]); + builder.buildAllProjects(); assertDiagnosticMessages(Diagnostics.Sorted_list_of_input_projects_Colon_0, Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, Diagnostics.Building_project_0, @@ -141,7 +134,7 @@ namespace ts { it("Detects that all projects are up to date", () => { clearDiagnostics(); builder.resetBuildContext(); - builder.buildProjects(["."]); + builder.buildAllProjects(); assertDiagnosticMessages(Diagnostics.Sorted_list_of_input_projects_Colon_0, Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, @@ -154,7 +147,7 @@ namespace ts { clearDiagnostics(); fs.writeFileSync("/src/tests/index.ts", "const m = 10;"); builder.resetBuildContext(); - builder.buildProjects(["."]); + builder.buildAllProjects(); assertDiagnosticMessages(Diagnostics.Sorted_list_of_input_projects_Colon_0, Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, @@ -169,7 +162,7 @@ namespace ts { clearDiagnostics(); replaceText(fs, "/src/core/index.ts", "HELLO WORLD", "WELCOME PLANET"); builder.resetBuildContext(); - builder.buildProjects(["."]); + builder.buildAllProjects(); assertDiagnosticMessages(Diagnostics.Sorted_list_of_input_projects_Colon_0, Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, @@ -185,14 +178,13 @@ namespace ts { it("won't build downstream projects if upstream projects have errors", () => { const fs = bfs.shadow(); const host = new fakes.CompilerHost(fs); - const builder = createSolutionBuilder(host, reportDiagnostic, { dry: false, force: false, verbose: true }); + const builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: false, force: false, verbose: true }); clearDiagnostics(); // Induce an error in the middle project replaceText(fs, "/src/logic/index.ts", "c.multiply(10, 15)", `c.muitply()`); - fs.chdir("/src/tests"); - builder.buildProjects(["."]); + builder.buildAllProjects(); assertDiagnosticMessages( Diagnostics.Sorted_list_of_input_projects_Colon_0, Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, @@ -221,8 +213,6 @@ namespace ts { writeProjects(fs, ["A", "B", "C", "D", "E", "F", "G"], deps); - const builder = createSolutionBuilder(host, reportDiagnostic, { dry: true, force: false, verbose: false }); - it("orders the graph correctly - specify two roots", () => { checkGraphOrdering(["A", "G"], ["A", "B", "C", "D", "E", "G"]); }); @@ -240,6 +230,8 @@ namespace ts { }); function checkGraphOrdering(rootNames: string[], expectedBuildSet: string[]) { + const builder = createSolutionBuilder(host, rootNames, reportDiagnostic, { dry: true, force: false, verbose: false }); + const projFileNames = rootNames.map(getProjectFileName); const graph = builder.getBuildGraph(projFileNames); if (graph === undefined) throw new Error("Graph shouldn't be undefined"); @@ -280,6 +272,39 @@ namespace ts { } }); + describe("tsbuild - project invalidation", () => { + it ("invalidates projects correctly", () => { + const fs = bfs.shadow(); + const host = new fakes.CompilerHost(fs); + const builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: false, force: false, verbose: false }); + + clearDiagnostics(); + builder.buildAllProjects(); + assertDiagnosticMessages(/*empty*/); + + // Update a timestamp in the middle project + tick(); + touch(fs, "/src/logic/index.ts"); + // Because we haven't reset the build context, the builder should assume there's nothing to do right now + const status = builder.getUpToDateStatusOfFile(builder.resolveProjectName("/src/logic")!); + assert.equal(status.type, UpToDateStatusType.UpToDate, "Project should be assumed to be up-to-date"); + + // Rebuild this project + tick(); + builder.invalidateProject("/src/logic"); + builder.buildInvalidatedProjects(); + // The file should be updated + assert.equal(fs.statSync("/src/logic/index.js").mtimeMs, time(), "JS file should have been rebuilt"); + assert.isBelow(fs.statSync("/src/tests/index.js").mtimeMs, time(), "Downstream JS file should *not* have been rebuilt"); + + // Build downstream projects should update 'tests', but not 'core' + tick(); + builder.buildDependentInvalidatedProjects(); + assert.equal(fs.statSync("/src/tests/index.js").mtimeMs, time(), "Downstream JS file should have been rebuilt"); + assert.isBelow(fs.statSync("/src/core/index.js").mtimeMs, time(), "Upstream JS file should not have been rebuilt"); + }); + }); + function replaceText(fs: vfs.FileSystem, path: string, oldText: string, newText: string) { if (!fs.statSync(path).isFile()) { throw new Error(`File ${path} does not exist`); @@ -324,6 +349,13 @@ namespace ts { return currentTime; } + function touch(fs: vfs.FileSystem, path: string) { + if (!fs.statSync(path).isFile()) { + throw new Error(`File ${path} does not exist`); + } + fs.utimesSync(path, new Date(time()), new Date(time())); + } + function loadFsMirror(vfs: vfs.FileSystem, localRoot: string, virtualRoot: string) { vfs.mkdirpSync(virtualRoot); for (const path of Harness.IO.readDirectory(localRoot)) { From 7a1de6142be4cb293f409e69ded0cdc581995f4e Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Fri, 1 Jun 2018 09:44:35 -0700 Subject: [PATCH 30/81] Scaffold watch mode --- src/compiler/tsbuild.ts | 34 ++++++++++++++++++++++++++++++---- src/compiler/tsc.ts | 2 +- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index 1f36f09393b..974edd6c60c 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -332,6 +332,7 @@ namespace ts { if (sourceFile === undefined) { return undefined; } + const parsed = parseJsonSourceFileConfigFileContent(sourceFile, configParseHost, getDirectoryPath(configFilePath)); parsed.options.configFilePath = configFilePath; cache.setValue(configFilePath, parsed); @@ -410,7 +411,7 @@ namespace ts { } ]; - export function performBuild(host: CompilerHost, reportDiagnostic: DiagnosticReporter, args: string[]) { + export function performBuild(host: CompilerHost, reportDiagnostic: DiagnosticReporter, args: string[], system?: System) { let verbose = false; let dry = false; let force = false; @@ -472,7 +473,7 @@ namespace ts { addProject("."); } - const builder = createSolutionBuilder(host, projects, reportDiagnostic, { verbose, dry, force }); + const builder = createSolutionBuilder(host, projects, reportDiagnostic, { verbose, dry, force }, system); if (clean) { builder.cleanAllProjects(); } @@ -480,6 +481,10 @@ namespace ts { builder.buildAllProjects(); } + if (watch) { + return builder.startWatching(); + } + function addProject(projectSpecification: string) { const fileName = resolvePath(host.getCurrentDirectory(), projectSpecification); const refPath = resolveProjectReferencePath(host, { path: fileName }); @@ -500,7 +505,7 @@ namespace ts { * A SolutionBuilder has an immutable set of rootNames that are the "entry point" projects, but * can dynamically add/remove other projects based on changes on the rootNames' references */ - export function createSolutionBuilder(host: CompilerHost, rootNames: ReadonlyArray, reportDiagnostic: DiagnosticReporter, defaultOptions: BuildOptions) { + export function createSolutionBuilder(host: CompilerHost, rootNames: ReadonlyArray, reportDiagnostic: DiagnosticReporter, defaultOptions: BuildOptions, system?: System) { if (!host.getModifiedTime || !host.setModifiedTime) { throw new Error("Host must support timestamp APIs"); } @@ -520,9 +525,30 @@ namespace ts { buildInvalidatedProjects, buildDependentInvalidatedProjects, - resolveProjectName + resolveProjectName, + + startWatching }; + function startWatching() { + if (!system) throw new Error("System host must be provided if using --watch"); + if (!system.watchFile || !system.watchDirectory || !system.setTimeout) throw new Error("System host must support watchFile / watchDirectory / setTimeout if using --watch"); + + const graph = getGlobalDependencyGraph()!; + for (const resolved of graph.buildQueue) { + const cfg = configFileCache.parseConfigFile(resolved); + if (cfg) { + for (const input of cfg.fileNames) { + system.watchFile(input, () => { + invalidateProject(resolved); + system.setTimeout!(buildInvalidatedProjects, 100); + system.setTimeout!(buildDependentInvalidatedProjects, 3000); + }); + } + } + } + } + function resetBuildContext(opts = defaultOptions) { context = createBuildContext(opts, reportDiagnostic); } diff --git a/src/compiler/tsc.ts b/src/compiler/tsc.ts index 9800d2e768a..744379aa696 100644 --- a/src/compiler/tsc.ts +++ b/src/compiler/tsc.ts @@ -50,7 +50,7 @@ namespace ts { export function executeCommandLine(args: string[]): void { if ((args[0].toLowerCase() === "--build") || (args[0].toLowerCase() === "-b")) { - return performBuild(createCompilerHost({}), createDiagnosticReporter(sys), args.slice(1)); + return performBuild(createCompilerHost({}), createDiagnosticReporter(sys), args.slice(1), sys); } const commandLine = parseCommandLine(args); From 7e9e29ce64218568ab7d33e6423838268238fc38 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Fri, 1 Jun 2018 13:12:57 -0700 Subject: [PATCH 31/81] Store + use the resolved path of sourceFiles in proj. ref. scenarios --- src/compiler/program.ts | 3 ++- src/compiler/types.ts | 1 + src/server/project.ts | 4 ++-- src/services/services.ts | 1 + 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 4dfb9039ccf..f797cac82e4 100755 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -987,7 +987,7 @@ namespace ts { for (const oldSourceFile of oldSourceFiles) { let newSourceFile = host.getSourceFileByPath - ? host.getSourceFileByPath(oldSourceFile.fileName, oldSourceFile.path, options.target!, /*onError*/ undefined, shouldCreateNewSourceFile) + ? host.getSourceFileByPath(oldSourceFile.fileName, oldSourceFile.resolvedPath || oldSourceFile.path, options.target!, /*onError*/ undefined, shouldCreateNewSourceFile) : host.getSourceFile(oldSourceFile.fileName, options.target!, /*onError*/ undefined, shouldCreateNewSourceFile); // TODO: GH#18217 if (!newSourceFile) { @@ -1991,6 +1991,7 @@ namespace ts { if (file) { sourceFilesFoundSearchingNodeModules.set(path, currentNodeModulesDepth > 0); file.path = path; + file.resolvedPath = toPath(fileName); if (host.useCaseSensitiveFileNames()) { const pathLowerCase = path.toLowerCase(); diff --git a/src/compiler/types.ts b/src/compiler/types.ts index a3cdb32df07..8d9f801c81e 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -2551,6 +2551,7 @@ namespace ts { fileName: string; /* @internal */ path: Path; text: string; + /* @internal */ resolvedPath: Path; /** * If two source files are for the same version of the same package, one will redirect to the other. diff --git a/src/server/project.ts b/src/server/project.ts index f6f68f66387..4986df27d98 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -629,8 +629,8 @@ namespace ts.server { return this.rootFiles; } return map(this.program.getSourceFiles(), sourceFile => { - const scriptInfo = this.projectService.getScriptInfoForPath(sourceFile.path); - Debug.assert(!!scriptInfo, "getScriptInfo", () => `scriptInfo for a file '${sourceFile.fileName}' Path: '${sourceFile.path}' is missing.`); + const scriptInfo = this.projectService.getScriptInfoForPath(sourceFile.resolvedPath || sourceFile.path); + Debug.assert(!!scriptInfo, "getScriptInfo", () => `scriptInfo for a file '${sourceFile.fileName}' Path: '${sourceFile.path}' / '${sourceFile.resolvedPath}' is missing.`); return scriptInfo!; }); } diff --git a/src/services/services.ts b/src/services/services.ts index dcea57b7159..b40f24112ef 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -540,6 +540,7 @@ namespace ts { public _declarationBrand: any; public fileName: string; public path: Path; + public resolvedPath: Path; public text: string; public scriptSnapshot: IScriptSnapshot; public lineMap: ReadonlyArray; From 17dc380ec8a5d007ba46d9fb7523d1da07317fc4 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Tue, 5 Jun 2018 13:19:41 -0700 Subject: [PATCH 32/81] Reorganize tsbuild unit test file --- src/harness/unittests/tsbuild.ts | 444 +++++++++++++++++-------------- 1 file changed, 239 insertions(+), 205 deletions(-) diff --git a/src/harness/unittests/tsbuild.ts b/src/harness/unittests/tsbuild.ts index 46fee63a7c0..48f568a65ac 100644 --- a/src/harness/unittests/tsbuild.ts +++ b/src/harness/unittests/tsbuild.ts @@ -1,202 +1,257 @@ namespace ts { let currentTime = 100; - const bfs = new vfs.FileSystem(/*ignoreCase*/ false, { time }); let lastDiagnostics: Diagnostic[] = []; const reportDiagnostic: DiagnosticReporter = diagnostic => lastDiagnostics.push(diagnostic); - const sampleRoot = resolvePath(__dirname, "../../tests/projects/sample1"); - loadFsMirror(bfs, sampleRoot, "/src"); - bfs.mkdirpSync("/lib"); - bfs.writeFileSync("/lib/lib.d.ts", Harness.IO.readFile(combinePaths(Harness.libFolder, "lib.d.ts"))!); - bfs.meta.set("defaultLibLocation", "/lib"); - bfs.makeReadonly(); - tick(); - const allExpectedOutputs = ["/src/tests/index.js", - "/src/core/index.js", "/src/core/index.d.ts", - "/src/logic/index.js", "/src/logic/index.d.ts"]; + namespace Sample1 { + tick(); + const projFs = loadProjectFromDisk("../../tests/projects/sample1"); - describe("tsbuild - sanity check of clean build of 'sample1' project", () => { - it("can build the sample project 'sample1' without error", () => { - const fs = bfs.shadow(); - const host = new fakes.CompilerHost(fs); - const builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: false, force: false, verbose: false }); + const allExpectedOutputs = ["/src/tests/index.js", + "/src/core/index.js", "/src/core/index.d.ts", + "/src/logic/index.js", "/src/logic/index.d.ts"]; - clearDiagnostics(); - builder.buildAllProjects(); - assertDiagnosticMessages(/*empty*/); + describe("tsbuild - sanity check of clean build of 'sample1' project", () => { + it("can build the sample project 'sample1' without error", () => { + const fs = projFs.shadow(); + const host = new fakes.CompilerHost(fs); + const builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: false, force: false, verbose: false }); - // Check for outputs. Not an exhaustive list - for (const output of allExpectedOutputs) { - assert(fs.existsSync(output), `Expect file ${output} to exist`); - } - }); - }); + clearDiagnostics(); + builder.buildAllProjects(); + assertDiagnosticMessages(/*empty*/); - describe("tsbuild - dry builds", () => { - it("doesn't write any files in a dry build", () => { - clearDiagnostics(); - const fs = bfs.shadow(); - const host = new fakes.CompilerHost(fs); - const builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: true, force: false, verbose: false }); - builder.buildAllProjects(); - assertDiagnosticMessages(Diagnostics.Would_build_project_0, Diagnostics.Would_build_project_0, Diagnostics.Would_build_project_0); - - // Check for outputs to not be written. Not an exhaustive list - for (const output of allExpectedOutputs) { - assert(!fs.existsSync(output), `Expect file ${output} to not exist`); - } - }); - - it("indicates that it would skip builds during a dry build", () => { - clearDiagnostics(); - const fs = bfs.shadow(); - const host = new fakes.CompilerHost(fs); - - let builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: false, force: false, verbose: false }); - builder.buildAllProjects(); - tick(); - - clearDiagnostics(); - builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: true, force: false, verbose: false }); - builder.buildAllProjects(); - assertDiagnosticMessages(Diagnostics.Project_0_is_up_to_date, Diagnostics.Project_0_is_up_to_date, Diagnostics.Project_0_is_up_to_date); - }); - }); - - describe("tsbuild - clean builds", () => { - it("removes all files it built", () => { - clearDiagnostics(); - const fs = bfs.shadow(); - const host = new fakes.CompilerHost(fs); - - const builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: false, force: false, verbose: false }); - builder.buildAllProjects(); - // Verify they exist - for (const output of allExpectedOutputs) { - assert(fs.existsSync(output), `Expect file ${output} to exist`); - } - builder.cleanAllProjects(); - // Verify they are gone - for (const output of allExpectedOutputs) { - assert(!fs.existsSync(output), `Expect file ${output} to not exist`); - } - // Subsequent clean shouldn't throw / etc - builder.cleanAllProjects(); - }); - }); - - describe("tsbuild - force builds", () => { - it("always builds under --force", () => { - const fs = bfs.shadow(); - const host = new fakes.CompilerHost(fs); - - const builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: false, force: true, verbose: false }); - builder.buildAllProjects(); - let currentTime = time(); - checkOutputTimestamps(currentTime); - - tick(); - Debug.assert(time() !== currentTime, "Time moves on"); - currentTime = time(); - builder.buildAllProjects(); - checkOutputTimestamps(currentTime); - - function checkOutputTimestamps(expected: number) { - // Check timestamps + // Check for outputs. Not an exhaustive list for (const output of allExpectedOutputs) { - const actual = fs.statSync(output).mtimeMs; - assert(actual === expected, `File ${output} has timestamp ${actual}, expected ${expected}`); + assert(fs.existsSync(output), `Expect file ${output} to exist`); } - } - }); - }); - - describe("tsbuild - can detect when and what to rebuild", () => { - const fs = bfs.shadow(); - const host = new fakes.CompilerHost(fs); - const builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: false, force: false, verbose: true }); - - it("Builds the project", () => { - clearDiagnostics(); - builder.resetBuildContext(); - builder.buildAllProjects(); - assertDiagnosticMessages(Diagnostics.Sorted_list_of_input_projects_Colon_0, - Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, - Diagnostics.Building_project_0, - Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, - Diagnostics.Building_project_0, - Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, - Diagnostics.Building_project_0); - tick(); + }); }); - // All three projects are up to date - it("Detects that all projects are up to date", () => { - clearDiagnostics(); - builder.resetBuildContext(); - builder.buildAllProjects(); - assertDiagnosticMessages(Diagnostics.Sorted_list_of_input_projects_Colon_0, - Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, - Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, - Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2); - tick(); + describe("tsbuild - dry builds", () => { + it("doesn't write any files in a dry build", () => { + clearDiagnostics(); + const fs = projFs.shadow(); + const host = new fakes.CompilerHost(fs); + const builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: true, force: false, verbose: false }); + builder.buildAllProjects(); + assertDiagnosticMessages(Diagnostics.Would_build_project_0, Diagnostics.Would_build_project_0, Diagnostics.Would_build_project_0); + + // Check for outputs to not be written. Not an exhaustive list + for (const output of allExpectedOutputs) { + assert(!fs.existsSync(output), `Expect file ${output} to not exist`); + } + }); + + it("indicates that it would skip builds during a dry build", () => { + clearDiagnostics(); + const fs = projFs.shadow(); + const host = new fakes.CompilerHost(fs); + + let builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: false, force: false, verbose: false }); + builder.buildAllProjects(); + tick(); + + clearDiagnostics(); + builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: true, force: false, verbose: false }); + builder.buildAllProjects(); + assertDiagnosticMessages(Diagnostics.Project_0_is_up_to_date, Diagnostics.Project_0_is_up_to_date, Diagnostics.Project_0_is_up_to_date); + }); }); - // Update a file in the leaf node (tests), only it should rebuild the last one - it("Only builds the leaf node project", () => { - clearDiagnostics(); - fs.writeFileSync("/src/tests/index.ts", "const m = 10;"); - builder.resetBuildContext(); - builder.buildAllProjects(); + describe("tsbuild - clean builds", () => { + it("removes all files it built", () => { + clearDiagnostics(); + const fs = projFs.shadow(); + const host = new fakes.CompilerHost(fs); - assertDiagnosticMessages(Diagnostics.Sorted_list_of_input_projects_Colon_0, - Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, - Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, - Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, - Diagnostics.Building_project_0); - tick(); + const builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: false, force: false, verbose: false }); + builder.buildAllProjects(); + // Verify they exist + for (const output of allExpectedOutputs) { + assert(fs.existsSync(output), `Expect file ${output} to exist`); + } + builder.cleanAllProjects(); + // Verify they are gone + for (const output of allExpectedOutputs) { + assert(!fs.existsSync(output), `Expect file ${output} to not exist`); + } + // Subsequent clean shouldn't throw / etc + builder.cleanAllProjects(); + }); }); - // Update a file in the parent (without affecting types), should get fast downstream builds - it("Detects type-only changes in upstream projects", () => { - clearDiagnostics(); - replaceText(fs, "/src/core/index.ts", "HELLO WORLD", "WELCOME PLANET"); - builder.resetBuildContext(); - builder.buildAllProjects(); + describe("tsbuild - force builds", () => { + it("always builds under --force", () => { + const fs = projFs.shadow(); + const host = new fakes.CompilerHost(fs); - assertDiagnosticMessages(Diagnostics.Sorted_list_of_input_projects_Colon_0, - Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, - Diagnostics.Building_project_0, - Diagnostics.Project_0_is_up_to_date_with_its_upstream_types, - Diagnostics.Updating_output_timestamps_of_project_0, - Diagnostics.Project_0_is_up_to_date_with_its_upstream_types, - Diagnostics.Updating_output_timestamps_of_project_0); + const builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: false, force: true, verbose: false }); + builder.buildAllProjects(); + let currentTime = time(); + checkOutputTimestamps(currentTime); + + tick(); + Debug.assert(time() !== currentTime, "Time moves on"); + currentTime = time(); + builder.buildAllProjects(); + checkOutputTimestamps(currentTime); + + function checkOutputTimestamps(expected: number) { + // Check timestamps + for (const output of allExpectedOutputs) { + const actual = fs.statSync(output).mtimeMs; + assert(actual === expected, `File ${output} has timestamp ${actual}, expected ${expected}`); + } + } + }); }); - }); - describe("tsbuild - downstream-blocked compilations", () => { - it("won't build downstream projects if upstream projects have errors", () => { - const fs = bfs.shadow(); + describe("tsbuild - can detect when and what to rebuild", () => { + const fs = projFs.shadow(); const host = new fakes.CompilerHost(fs); const builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: false, force: false, verbose: true }); - clearDiagnostics(); + it("Builds the project", () => { + clearDiagnostics(); + builder.resetBuildContext(); + builder.buildAllProjects(); + assertDiagnosticMessages(Diagnostics.Sorted_list_of_input_projects_Colon_0, + Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, + Diagnostics.Building_project_0, + Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, + Diagnostics.Building_project_0, + Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, + Diagnostics.Building_project_0); + tick(); + }); - // Induce an error in the middle project - replaceText(fs, "/src/logic/index.ts", "c.multiply(10, 15)", `c.muitply()`); - builder.buildAllProjects(); - assertDiagnosticMessages( - Diagnostics.Sorted_list_of_input_projects_Colon_0, - Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, - Diagnostics.Building_project_0, - Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, - Diagnostics.Building_project_0, - Diagnostics.Property_0_does_not_exist_on_type_1, - Diagnostics.Project_0_can_t_be_built_because_it_depends_on_a_project_with_errors, - Diagnostics.Skipping_build_of_project_0_because_its_upstream_project_1_has_errors - ); + // All three projects are up to date + it("Detects that all projects are up to date", () => { + clearDiagnostics(); + builder.resetBuildContext(); + builder.buildAllProjects(); + assertDiagnosticMessages(Diagnostics.Sorted_list_of_input_projects_Colon_0, + Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, + Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, + Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2); + tick(); + }); + + // Update a file in the leaf node (tests), only it should rebuild the last one + it("Only builds the leaf node project", () => { + clearDiagnostics(); + fs.writeFileSync("/src/tests/index.ts", "const m = 10;"); + builder.resetBuildContext(); + builder.buildAllProjects(); + + assertDiagnosticMessages(Diagnostics.Sorted_list_of_input_projects_Colon_0, + Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, + Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, + Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, + Diagnostics.Building_project_0); + tick(); + }); + + // Update a file in the parent (without affecting types), should get fast downstream builds + it("Detects type-only changes in upstream projects", () => { + clearDiagnostics(); + replaceText(fs, "/src/core/index.ts", "HELLO WORLD", "WELCOME PLANET"); + builder.resetBuildContext(); + builder.buildAllProjects(); + + assertDiagnosticMessages(Diagnostics.Sorted_list_of_input_projects_Colon_0, + Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, + Diagnostics.Building_project_0, + Diagnostics.Project_0_is_up_to_date_with_its_upstream_types, + Diagnostics.Updating_output_timestamps_of_project_0, + Diagnostics.Project_0_is_up_to_date_with_its_upstream_types, + Diagnostics.Updating_output_timestamps_of_project_0); + }); }); - }); + + describe("tsbuild - downstream-blocked compilations", () => { + it("won't build downstream projects if upstream projects have errors", () => { + const fs = projFs.shadow(); + const host = new fakes.CompilerHost(fs); + const builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: false, force: false, verbose: true }); + + clearDiagnostics(); + + // Induce an error in the middle project + replaceText(fs, "/src/logic/index.ts", "c.multiply(10, 15)", `c.muitply()`); + builder.buildAllProjects(); + assertDiagnosticMessages( + Diagnostics.Sorted_list_of_input_projects_Colon_0, + Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, + Diagnostics.Building_project_0, + Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, + Diagnostics.Building_project_0, + Diagnostics.Property_0_does_not_exist_on_type_1, + Diagnostics.Project_0_can_t_be_built_because_it_depends_on_a_project_with_errors, + Diagnostics.Skipping_build_of_project_0_because_its_upstream_project_1_has_errors + ); + }); + }); + + describe("tsbuild - project invalidation", () => { + it("invalidates projects correctly", () => { + const fs = projFs.shadow(); + const host = new fakes.CompilerHost(fs); + const builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: false, force: false, verbose: false }); + + clearDiagnostics(); + builder.buildAllProjects(); + assertDiagnosticMessages(/*empty*/); + + // Update a timestamp in the middle project + tick(); + touch(fs, "/src/logic/index.ts"); + // Because we haven't reset the build context, the builder should assume there's nothing to do right now + const status = builder.getUpToDateStatusOfFile(builder.resolveProjectName("/src/logic")!); + assert.equal(status.type, UpToDateStatusType.UpToDate, "Project should be assumed to be up-to-date"); + + // Rebuild this project + tick(); + builder.invalidateProject("/src/logic"); + builder.buildInvalidatedProjects(); + // The file should be updated + assert.equal(fs.statSync("/src/logic/index.js").mtimeMs, time(), "JS file should have been rebuilt"); + assert.isBelow(fs.statSync("/src/tests/index.js").mtimeMs, time(), "Downstream JS file should *not* have been rebuilt"); + + // Build downstream projects should update 'tests', but not 'core' + tick(); + builder.buildDependentInvalidatedProjects(); + assert.equal(fs.statSync("/src/tests/index.js").mtimeMs, time(), "Downstream JS file should have been rebuilt"); + assert.isBelow(fs.statSync("/src/core/index.js").mtimeMs, time(), "Upstream JS file should not have been rebuilt"); + }); + }); + } + + namespace OutFile { + const outFileFs = loadProjectFromDisk("../../tests/projects/outfile-concat"); + + describe("tsbuild - baseline sectioned sourcemaps", () => { + const fs = outFileFs.shadow(); + const host = new fakes.CompilerHost(fs); + const builder = createSolutionBuilder(host, ["/src/third"], reportDiagnostic, { dry: false, force: false, verbose: false }); + clearDiagnostics(); + builder.buildAllProjects(); + assertDiagnosticMessages(/*none*/); + + it("Generates files matching the baseline", () => { + Harness.Baseline.runBaseline("outfile-concat.js", () => { + return fs.readFileSync("/src/third/third-output.js", 'utf-8'); + }); + + Harness.Baseline.runBaseline("outfile-concat.js.map", () => { + return fs.readFileSync("/src/third/third-output.js.map", 'utf-8'); + }); + }); + }); + } + + void OutFile, Sample1; describe("tsbuild - graph-ordering", () => { const fs = new vfs.FileSystem(false); @@ -272,38 +327,6 @@ namespace ts { } }); - describe("tsbuild - project invalidation", () => { - it ("invalidates projects correctly", () => { - const fs = bfs.shadow(); - const host = new fakes.CompilerHost(fs); - const builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: false, force: false, verbose: false }); - - clearDiagnostics(); - builder.buildAllProjects(); - assertDiagnosticMessages(/*empty*/); - - // Update a timestamp in the middle project - tick(); - touch(fs, "/src/logic/index.ts"); - // Because we haven't reset the build context, the builder should assume there's nothing to do right now - const status = builder.getUpToDateStatusOfFile(builder.resolveProjectName("/src/logic")!); - assert.equal(status.type, UpToDateStatusType.UpToDate, "Project should be assumed to be up-to-date"); - - // Rebuild this project - tick(); - builder.invalidateProject("/src/logic"); - builder.buildInvalidatedProjects(); - // The file should be updated - assert.equal(fs.statSync("/src/logic/index.js").mtimeMs, time(), "JS file should have been rebuilt"); - assert.isBelow(fs.statSync("/src/tests/index.js").mtimeMs, time(), "Downstream JS file should *not* have been rebuilt"); - - // Build downstream projects should update 'tests', but not 'core' - tick(); - builder.buildDependentInvalidatedProjects(); - assert.equal(fs.statSync("/src/tests/index.js").mtimeMs, time(), "Downstream JS file should have been rebuilt"); - assert.isBelow(fs.statSync("/src/core/index.js").mtimeMs, time(), "Upstream JS file should not have been rebuilt"); - }); - }); function replaceText(fs: vfs.FileSystem, path: string, oldText: string, newText: string) { if (!fs.statSync(path).isFile()) { @@ -356,6 +379,17 @@ namespace ts { fs.utimesSync(path, new Date(time()), new Date(time())); } + function loadProjectFromDisk(root: string): vfs.FileSystem { + const fs = new vfs.FileSystem(/*ignoreCase*/ false, { time }); + const rootPath = resolvePath(__dirname, root); + loadFsMirror(fs, rootPath, "/src"); + fs.mkdirpSync("/lib"); + fs.writeFileSync("/lib/lib.d.ts", Harness.IO.readFile(combinePaths(Harness.libFolder, "lib.d.ts"))!); + fs.meta.set("defaultLibLocation", "/lib"); + fs.makeReadonly(); + return fs; + } + function loadFsMirror(vfs: vfs.FileSystem, localRoot: string, virtualRoot: string) { vfs.mkdirpSync(virtualRoot); for (const path of Harness.IO.readDirectory(localRoot)) { From 514a0d85a58821206171f561ba608990a3f4a130 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Tue, 5 Jun 2018 13:19:52 -0700 Subject: [PATCH 33/81] Add outfile-concat project refs test --- .../projects/outfile-concat/first/first_part1.ts | 11 +++++++++++ .../projects/outfile-concat/first/first_part2.ts | 1 + .../projects/outfile-concat/first/first_part3.ts | 3 +++ .../projects/outfile-concat/first/tsconfig.json | 14 ++++++++++++++ .../outfile-concat/second/second_part1.ts | 11 +++++++++++ .../outfile-concat/second/second_part2.ts | 5 +++++ .../projects/outfile-concat/second/tsconfig.json | 14 ++++++++++++++ .../projects/outfile-concat/third/third_part1.ts | 2 ++ .../projects/outfile-concat/third/tsconfig.json | 16 ++++++++++++++++ 9 files changed, 77 insertions(+) create mode 100644 tests/projects/outfile-concat/first/first_part1.ts create mode 100644 tests/projects/outfile-concat/first/first_part2.ts create mode 100644 tests/projects/outfile-concat/first/first_part3.ts create mode 100644 tests/projects/outfile-concat/first/tsconfig.json create mode 100644 tests/projects/outfile-concat/second/second_part1.ts create mode 100644 tests/projects/outfile-concat/second/second_part2.ts create mode 100644 tests/projects/outfile-concat/second/tsconfig.json create mode 100644 tests/projects/outfile-concat/third/third_part1.ts create mode 100644 tests/projects/outfile-concat/third/tsconfig.json diff --git a/tests/projects/outfile-concat/first/first_part1.ts b/tests/projects/outfile-concat/first/first_part1.ts new file mode 100644 index 00000000000..b8810033aaa --- /dev/null +++ b/tests/projects/outfile-concat/first/first_part1.ts @@ -0,0 +1,11 @@ +interface TheFirst { + none: any; +} + +const s = "Hello, world"; + +interface NoJsForHereEither { + none: any; +} + +console.log(s); diff --git a/tests/projects/outfile-concat/first/first_part2.ts b/tests/projects/outfile-concat/first/first_part2.ts new file mode 100644 index 00000000000..bd60d3eba9f --- /dev/null +++ b/tests/projects/outfile-concat/first/first_part2.ts @@ -0,0 +1 @@ +console.log(f()); diff --git a/tests/projects/outfile-concat/first/first_part3.ts b/tests/projects/outfile-concat/first/first_part3.ts new file mode 100644 index 00000000000..6f497fc490a --- /dev/null +++ b/tests/projects/outfile-concat/first/first_part3.ts @@ -0,0 +1,3 @@ +function f() { + return "JS does hoists"; +} \ No newline at end of file diff --git a/tests/projects/outfile-concat/first/tsconfig.json b/tests/projects/outfile-concat/first/tsconfig.json new file mode 100644 index 00000000000..75562e332f2 --- /dev/null +++ b/tests/projects/outfile-concat/first/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "es5", + "composite": true, + "removeComments": true, + "strict": false, + "sourceMap": true, + "declarationMap": true, + "declaration": true, + "outFile": "./first-output.js" + }, + "references": [ + ] +} diff --git a/tests/projects/outfile-concat/second/second_part1.ts b/tests/projects/outfile-concat/second/second_part1.ts new file mode 100644 index 00000000000..2b995fbe4a5 --- /dev/null +++ b/tests/projects/outfile-concat/second/second_part1.ts @@ -0,0 +1,11 @@ +namespace N { + // Comment text +} + +namespace N { + function f() { + console.log('testing'); + } + + f(); +} diff --git a/tests/projects/outfile-concat/second/second_part2.ts b/tests/projects/outfile-concat/second/second_part2.ts new file mode 100644 index 00000000000..b81737e8915 --- /dev/null +++ b/tests/projects/outfile-concat/second/second_part2.ts @@ -0,0 +1,5 @@ +class C { + doSomething() { + console.log("something got done"); + } +} diff --git a/tests/projects/outfile-concat/second/tsconfig.json b/tests/projects/outfile-concat/second/tsconfig.json new file mode 100644 index 00000000000..42d93ee56fb --- /dev/null +++ b/tests/projects/outfile-concat/second/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "es5", + "composite": true, + "removeComments": true, + "strict": false, + "sourceMap": true, + "declarationMap": true, + "declaration": true, + "outFile": "./second-output.js" + }, + "references": [ + ] +} diff --git a/tests/projects/outfile-concat/third/third_part1.ts b/tests/projects/outfile-concat/third/third_part1.ts new file mode 100644 index 00000000000..948688ae5ff --- /dev/null +++ b/tests/projects/outfile-concat/third/third_part1.ts @@ -0,0 +1,2 @@ +var c = new C(); +c.doSomething(); diff --git a/tests/projects/outfile-concat/third/tsconfig.json b/tests/projects/outfile-concat/third/tsconfig.json new file mode 100644 index 00000000000..195272b2d94 --- /dev/null +++ b/tests/projects/outfile-concat/third/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "es5", + "composite": true, + "removeComments": true, + "strict": false, + "sourceMap": true, + "declarationMap": true, + "declaration": true, + "outFile": "./third-output.js" + }, + "references": [ + { "path": "../first", "prepend": true }, + { "path": "../second", "prepend": true }, + ] +} From 394e29f9d12743ac21d3431b7be2eee13ae2afb3 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Tue, 5 Jun 2018 13:20:01 -0700 Subject: [PATCH 34/81] Accept baselines for sourcemap sections --- tests/baselines/reference/outfile-concat.js | 26 +++++++++++++++++++ .../baselines/reference/outfile-concat.js.map | 1 + 2 files changed, 27 insertions(+) create mode 100644 tests/baselines/reference/outfile-concat.js create mode 100644 tests/baselines/reference/outfile-concat.js.map diff --git a/tests/baselines/reference/outfile-concat.js b/tests/baselines/reference/outfile-concat.js new file mode 100644 index 00000000000..e3e5aa3d5f0 --- /dev/null +++ b/tests/baselines/reference/outfile-concat.js @@ -0,0 +1,26 @@ +var s = "Hello, world"; +console.log(s); +console.log(f()); +function f() { + return "JS does hoists"; +} +//# sourceMappingURL=first-output.js.map +var N; +(function (N) { + function f() { + console.log('testing'); + } + f(); +})(N || (N = {})); +var C = (function () { + function C() { + } + C.prototype.doSomething = function () { + console.log("something got done"); + }; + return C; +}()); +//# sourceMappingURL=second-output.js.map +var c = new C(); +c.doSomething(); +//# sourceMappingURL=third-output.js.map \ No newline at end of file diff --git a/tests/baselines/reference/outfile-concat.js.map b/tests/baselines/reference/outfile-concat.js.map new file mode 100644 index 00000000000..f8220832b2a --- /dev/null +++ b/tests/baselines/reference/outfile-concat.js.map @@ -0,0 +1 @@ +{"version":3,"file":"third-output.js","sections":[{"offset":{"line":0,"column":0},"map":{"version":3,"file":"first-output.js","sourceRoot":"","sources":["first_part1.ts","first_part2.ts","first_part3.ts"],"names":[],"mappings":"AAIA,IAAM,CAAC,GAAG,cAAc,CAAC;AAMzB,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;ACVf,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC;ACAjB;IACI,OAAO,gBAAgB,CAAC;AAC5B,CAAC"}},{"offset":{"line":7,"column":0},"map":{"version":3,"file":"second-output.js","sourceRoot":"","sources":["second_part1.ts","second_part2.ts"],"names":[],"mappings":"AAIA,IAAU,CAAC,CAMV;AAND,WAAU,CAAC;IACP;QACI,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IAC3B,CAAC;IAED,CAAC,EAAE,CAAC;AACR,CAAC,EANS,CAAC,KAAD,CAAC,QAMV;ACVD;IAAA;IAIA,CAAC;IAHG,uBAAW,GAAX;QACI,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAC;IACtC,CAAC;IACL,QAAC;AAAD,CAAC,AAJD,IAIC"}},{"offset":{"line":22,"column":41},"map":{"version":3,"file":"third-output.js","sourceRoot":"","sources":["third_part1.ts"],"names":[],"mappings":";AAAA,IAAI,CAAC,GAAG,IAAI,CAAC,EAAE,CAAC;AAChB,CAAC,CAAC,WAAW,EAAE,CAAC"}}]} \ No newline at end of file From e40778070ed380ce10ccf97f2eb031f8ab7e876d Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Tue, 5 Jun 2018 14:06:13 -0700 Subject: [PATCH 35/81] Complicate the paths of the project for better sourcemap testing --- src/harness/unittests/tsbuild.ts | 22 ++++++++--- src/harness/vfs.ts | 14 ++++--- .../reference/outfile-concat-fileListing.txt | 39 +++++++++++++++++++ tests/baselines/reference/third-output.js | 26 +++++++++++++ tests/baselines/reference/third-output.js.map | 1 + .../outfile-concat/first/tsconfig.json | 2 +- .../outfile-concat/second/tsconfig.json | 2 +- .../outfile-concat/third/tsconfig.json | 2 +- 8 files changed, 94 insertions(+), 14 deletions(-) create mode 100644 tests/baselines/reference/outfile-concat-fileListing.txt create mode 100644 tests/baselines/reference/third-output.js create mode 100644 tests/baselines/reference/third-output.js.map diff --git a/src/harness/unittests/tsbuild.ts b/src/harness/unittests/tsbuild.ts index 48f568a65ac..22f68fba866 100644 --- a/src/harness/unittests/tsbuild.ts +++ b/src/harness/unittests/tsbuild.ts @@ -239,13 +239,23 @@ namespace ts { builder.buildAllProjects(); assertDiagnosticMessages(/*none*/); - it("Generates files matching the baseline", () => { - Harness.Baseline.runBaseline("outfile-concat.js", () => { - return fs.readFileSync("/src/third/third-output.js", 'utf-8'); + const files = [ + "/src/third/thirdjs/output/third-output.js", + "/src/third/thirdjs/output/third-output.js.map" + ]; + + + for (const file of files) { + it(`Generates files matching the baseline - ${file}`, () => { + Harness.Baseline.runBaseline(getBaseFileName(file), () => { + return fs.readFileSync(file, 'utf-8'); + }); }); - - Harness.Baseline.runBaseline("outfile-concat.js.map", () => { - return fs.readFileSync("/src/third/third-output.js.map", 'utf-8'); + } + + it(`Generates files matching the baseline - file listing for outFile-concat`, () => { + Harness.Baseline.runBaseline("outfile-concat-fileListing.txt", () => { + return fs.getFileListing(); }); }); }); diff --git a/src/harness/vfs.ts b/src/harness/vfs.ts index ba5905b8ea6..186f3610dcd 100644 --- a/src/harness/vfs.ts +++ b/src/harness/vfs.ts @@ -353,10 +353,7 @@ namespace vfs { if (!result.node) this._mkdir(result); } - /** - * Print diagnostic information about the structure of the file system to the console. - */ - public debugPrint(): void { + public getFileListing(): string { let result = ""; const printLinks = (dirname: string | undefined, links: collections.SortedMap) => { const iterator = collections.getIterator(links); @@ -384,7 +381,14 @@ namespace vfs { } }; printLinks(/*dirname*/ undefined, this._getRootLinks()); - console.log(result); + return result; + } + + /** + * Print diagnostic information about the structure of the file system to the console. + */ + public debugPrint(): void { + console.log(this.getFileListing()); } // POSIX API (aligns with NodeJS "fs" module API) diff --git a/tests/baselines/reference/outfile-concat-fileListing.txt b/tests/baselines/reference/outfile-concat-fileListing.txt new file mode 100644 index 00000000000..3fca9338912 --- /dev/null +++ b/tests/baselines/reference/outfile-concat-fileListing.txt @@ -0,0 +1,39 @@ +*/ + /lib/ + /lib/lib.d.ts + /src/ + /src/2/ + /src/2/second-output.d.ts + /src/2/second-output.d.ts.map + /src/2/second-output.js + /src/2/second-output.js.map + /src/first/ + /src/first/bin/ + /src/first/bin/first-output.d.ts + /src/first/bin/first-output.d.ts.map + /src/first/bin/first-output.js + /src/first/bin/first-output.js.map + /src/first/first_part1.ts + /src/first/first_part2.ts + /src/first/first_part3.ts + /src/first/tsconfig.json + /src/first_part1.ts + /src/first_part2.ts + /src/first_part3.ts + /src/second/ + /src/second/second_part1.ts + /src/second/second_part2.ts + /src/second/tsconfig.json + /src/second_part1.ts + /src/second_part2.ts + /src/third/ + /src/third/third_part1.ts + /src/third/thirdjs/ + /src/third/thirdjs/output/ + /src/third/thirdjs/output/third-output.d.ts + /src/third/thirdjs/output/third-output.d.ts.map + /src/third/thirdjs/output/third-output.js + /src/third/thirdjs/output/third-output.js.map + /src/third/tsconfig.json + /src/third_part1.ts + /src/tsconfig.json \ No newline at end of file diff --git a/tests/baselines/reference/third-output.js b/tests/baselines/reference/third-output.js new file mode 100644 index 00000000000..e3e5aa3d5f0 --- /dev/null +++ b/tests/baselines/reference/third-output.js @@ -0,0 +1,26 @@ +var s = "Hello, world"; +console.log(s); +console.log(f()); +function f() { + return "JS does hoists"; +} +//# sourceMappingURL=first-output.js.map +var N; +(function (N) { + function f() { + console.log('testing'); + } + f(); +})(N || (N = {})); +var C = (function () { + function C() { + } + C.prototype.doSomething = function () { + console.log("something got done"); + }; + return C; +}()); +//# sourceMappingURL=second-output.js.map +var c = new C(); +c.doSomething(); +//# sourceMappingURL=third-output.js.map \ No newline at end of file diff --git a/tests/baselines/reference/third-output.js.map b/tests/baselines/reference/third-output.js.map new file mode 100644 index 00000000000..70b9ad69d53 --- /dev/null +++ b/tests/baselines/reference/third-output.js.map @@ -0,0 +1 @@ +{"version":3,"file":"third-output.js","sections":[{"offset":{"line":0,"column":0},"map":{"version":3,"file":"first-output.js","sourceRoot":"","sources":["../first_part1.ts","../first_part2.ts","../first_part3.ts"],"names":[],"mappings":"AAIA,IAAM,CAAC,GAAG,cAAc,CAAC;AAMzB,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;ACVf,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC;ACAjB;IACI,OAAO,gBAAgB,CAAC;AAC5B,CAAC"}},{"offset":{"line":7,"column":0},"map":{"version":3,"file":"second-output.js","sourceRoot":"","sources":["../second/second_part1.ts","../second/second_part2.ts"],"names":[],"mappings":"AAIA,IAAU,CAAC,CAMV;AAND,WAAU,CAAC;IACP;QACI,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IAC3B,CAAC;IAED,CAAC,EAAE,CAAC;AACR,CAAC,EANS,CAAC,KAAD,CAAC,QAMV;ACVD;IAAA;IAIA,CAAC;IAHG,uBAAW,GAAX;QACI,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAC;IACtC,CAAC;IACL,QAAC;AAAD,CAAC,AAJD,IAIC"}},{"offset":{"line":22,"column":41},"map":{"version":3,"file":"third-output.js","sourceRoot":"","sources":["../../third_part1.ts"],"names":[],"mappings":";AAAA,IAAI,CAAC,GAAG,IAAI,CAAC,EAAE,CAAC;AAChB,CAAC,CAAC,WAAW,EAAE,CAAC"}}]} \ No newline at end of file diff --git a/tests/projects/outfile-concat/first/tsconfig.json b/tests/projects/outfile-concat/first/tsconfig.json index 75562e332f2..8370f6512b8 100644 --- a/tests/projects/outfile-concat/first/tsconfig.json +++ b/tests/projects/outfile-concat/first/tsconfig.json @@ -7,7 +7,7 @@ "sourceMap": true, "declarationMap": true, "declaration": true, - "outFile": "./first-output.js" + "outFile": "./bin/first-output.js" }, "references": [ ] diff --git a/tests/projects/outfile-concat/second/tsconfig.json b/tests/projects/outfile-concat/second/tsconfig.json index 42d93ee56fb..d835cff6d66 100644 --- a/tests/projects/outfile-concat/second/tsconfig.json +++ b/tests/projects/outfile-concat/second/tsconfig.json @@ -7,7 +7,7 @@ "sourceMap": true, "declarationMap": true, "declaration": true, - "outFile": "./second-output.js" + "outFile": "../2/second-output.js" }, "references": [ ] diff --git a/tests/projects/outfile-concat/third/tsconfig.json b/tests/projects/outfile-concat/third/tsconfig.json index 195272b2d94..18c98608db1 100644 --- a/tests/projects/outfile-concat/third/tsconfig.json +++ b/tests/projects/outfile-concat/third/tsconfig.json @@ -7,7 +7,7 @@ "sourceMap": true, "declarationMap": true, "declaration": true, - "outFile": "./third-output.js" + "outFile": "./thirdjs/output/third-output.js" }, "references": [ { "path": "../first", "prepend": true }, From 449d60cdffdf43ef29f8202ff96da946b134bb05 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Tue, 5 Jun 2018 14:14:16 -0700 Subject: [PATCH 36/81] Don't crash on no args --- src/compiler/tsc.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/compiler/tsc.ts b/src/compiler/tsc.ts index 744379aa696..149439d9b67 100644 --- a/src/compiler/tsc.ts +++ b/src/compiler/tsc.ts @@ -49,7 +49,7 @@ namespace ts { } export function executeCommandLine(args: string[]): void { - if ((args[0].toLowerCase() === "--build") || (args[0].toLowerCase() === "-b")) { + if (args.length > 0 && ((args[0].toLowerCase() === "--build") || (args[0].toLowerCase() === "-b"))) { return performBuild(createCompilerHost({}), createDiagnosticReporter(sys), args.slice(1), sys); } From 5065c540cdb007265efbce50a0b07becba5b9769 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Tue, 5 Jun 2018 14:16:50 -0700 Subject: [PATCH 37/81] Tidy --- src/harness/unittests/tsbuild.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/harness/unittests/tsbuild.ts b/src/harness/unittests/tsbuild.ts index 22f68fba866..06b447a50e8 100644 --- a/src/harness/unittests/tsbuild.ts +++ b/src/harness/unittests/tsbuild.ts @@ -3,7 +3,7 @@ namespace ts { let lastDiagnostics: Diagnostic[] = []; const reportDiagnostic: DiagnosticReporter = diagnostic => lastDiagnostics.push(diagnostic); - namespace Sample1 { + export namespace Sample1 { tick(); const projFs = loadProjectFromDisk("../../tests/projects/sample1"); @@ -228,7 +228,7 @@ namespace ts { }); } - namespace OutFile { + export namespace OutFile { const outFileFs = loadProjectFromDisk("../../tests/projects/outfile-concat"); describe("tsbuild - baseline sectioned sourcemaps", () => { @@ -244,7 +244,6 @@ namespace ts { "/src/third/thirdjs/output/third-output.js.map" ]; - for (const file of files) { it(`Generates files matching the baseline - ${file}`, () => { Harness.Baseline.runBaseline(getBaseFileName(file), () => { @@ -261,8 +260,6 @@ namespace ts { }); } - void OutFile, Sample1; - describe("tsbuild - graph-ordering", () => { const fs = new vfs.FileSystem(false); const host = new fakes.CompilerHost(fs); From fad8f67093ef4d047e84bd05e7b203da817fd63b Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Tue, 5 Jun 2018 14:17:55 -0700 Subject: [PATCH 38/81] Lint --- src/harness/unittests/tsbuild.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/harness/unittests/tsbuild.ts b/src/harness/unittests/tsbuild.ts index 06b447a50e8..244f8c43fb0 100644 --- a/src/harness/unittests/tsbuild.ts +++ b/src/harness/unittests/tsbuild.ts @@ -199,18 +199,18 @@ namespace ts { const fs = projFs.shadow(); const host = new fakes.CompilerHost(fs); const builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: false, force: false, verbose: false }); - + clearDiagnostics(); builder.buildAllProjects(); assertDiagnosticMessages(/*empty*/); - + // Update a timestamp in the middle project tick(); touch(fs, "/src/logic/index.ts"); // Because we haven't reset the build context, the builder should assume there's nothing to do right now const status = builder.getUpToDateStatusOfFile(builder.resolveProjectName("/src/logic")!); assert.equal(status.type, UpToDateStatusType.UpToDate, "Project should be assumed to be up-to-date"); - + // Rebuild this project tick(); builder.invalidateProject("/src/logic"); @@ -218,7 +218,7 @@ namespace ts { // The file should be updated assert.equal(fs.statSync("/src/logic/index.js").mtimeMs, time(), "JS file should have been rebuilt"); assert.isBelow(fs.statSync("/src/tests/index.js").mtimeMs, time(), "Downstream JS file should *not* have been rebuilt"); - + // Build downstream projects should update 'tests', but not 'core' tick(); builder.buildDependentInvalidatedProjects(); @@ -247,7 +247,7 @@ namespace ts { for (const file of files) { it(`Generates files matching the baseline - ${file}`, () => { Harness.Baseline.runBaseline(getBaseFileName(file), () => { - return fs.readFileSync(file, 'utf-8'); + return fs.readFileSync(file, "utf-8"); }); }); } From 7ad9d57cc783f6eacb7924e1af0e8aa20d468573 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Tue, 5 Jun 2018 16:19:54 -0700 Subject: [PATCH 39/81] Include filename when reporting not found --- src/harness/vfs.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/harness/vfs.ts b/src/harness/vfs.ts index 186f3610dcd..0e16b0fe715 100644 --- a/src/harness/vfs.ts +++ b/src/harness/vfs.ts @@ -444,7 +444,7 @@ namespace vfs { private _stat(entry: WalkResult) { const node = entry.node; - if (!node) throw createIOError("ENOENT"); + if (!node) throw createIOError(`ENOENT`, entry.realpath); return new Stats( node.dev, node.ino, @@ -1155,8 +1155,8 @@ namespace vfs { EROFS: "file system is read-only" }); - export function createIOError(code: keyof typeof IOErrorMessages) { - const err: NodeJS.ErrnoException = new Error(`${code}: ${IOErrorMessages[code]}`); + export function createIOError(code: keyof typeof IOErrorMessages, details: string = "") { + const err: NodeJS.ErrnoException = new Error(`${code}: ${IOErrorMessages[code]} ${details}`); err.code = code; if (Error.captureStackTrace) Error.captureStackTrace(err, createIOError); return err; From 5111f4d5416eaf6d050b165981f2df0c24575f91 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Tue, 5 Jun 2018 16:20:07 -0700 Subject: [PATCH 40/81] Move proj ref logic to the right place --- src/compiler/program.ts | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 82e35c8182e..40a1bdcafff 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -618,25 +618,27 @@ namespace ts { // A parallel array to projectReferences storing the results of reading in the referenced tsconfig files const resolvedProjectReferences: (ResolvedProjectReference | undefined)[] | undefined = projectReferences ? [] : undefined; const projectReferenceRedirects: Map = createMap(); - if (projectReferences) { - for (const ref of projectReferences) { - const parsedRef = parseProjectReferenceConfigFile(ref); - resolvedProjectReferences!.push(parsedRef); - if (parsedRef) { - if (parsedRef.commandLine.options.outFile) { - const dtsOutfile = changeExtension(parsedRef.commandLine.options.outFile, ".d.ts"); - processSourceFile(dtsOutfile, /*isDefaultLib*/ false, /*ignoreNoDefaultLib*/ false, /*packageId*/ undefined); - } - addProjectReferenceRedirects(parsedRef.commandLine, projectReferenceRedirects); - } - } - } - + const shouldCreateNewSourceFile = shouldProgramCreateNewSourceFiles(oldProgram, options); const structuralIsReused = tryReuseStructureFromOldProgram(); if (structuralIsReused !== StructureIsReused.Completely) { processingDefaultLibFiles = []; processingOtherFiles = []; + + if (projectReferences) { + for (const ref of projectReferences) { + const parsedRef = parseProjectReferenceConfigFile(ref); + resolvedProjectReferences!.push(parsedRef); + if (parsedRef) { + if (parsedRef.commandLine.options.outFile) { + const dtsOutfile = changeExtension(parsedRef.commandLine.options.outFile, ".d.ts"); + processSourceFile(dtsOutfile, /*isDefaultLib*/ false, /*ignoreNoDefaultLib*/ false, /*packageId*/ undefined); + } + addProjectReferenceRedirects(parsedRef.commandLine, projectReferenceRedirects); + } + } + } + forEach(rootNames, name => processRootFile(name, /*isDefaultLib*/ false, /*ignoreNoDefaultLib*/ false)); // load type declarations specified via 'types' argument or implicitly from types/ and node_modules/@types folders From 11df004c2c6587b69e4c65565f6b61df7a503898 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Tue, 5 Jun 2018 16:20:20 -0700 Subject: [PATCH 41/81] Include all lib files in tsbuild harness --- src/harness/unittests/tsbuild.ts | 8 ++++++++ tests/baselines/reference/outfile-concat-fileListing.txt | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/src/harness/unittests/tsbuild.ts b/src/harness/unittests/tsbuild.ts index 244f8c43fb0..f076e0b34ab 100644 --- a/src/harness/unittests/tsbuild.ts +++ b/src/harness/unittests/tsbuild.ts @@ -391,6 +391,14 @@ namespace ts { const rootPath = resolvePath(__dirname, root); loadFsMirror(fs, rootPath, "/src"); fs.mkdirpSync("/lib"); + const libs = ["es5", "dom", "webworker.importscripts", "scripthost"]; + for (const lib of libs) { + const content = Harness.IO.readFile(combinePaths(Harness.libFolder, `lib.${lib}.d.ts`)); + if (content === undefined) { + throw new Error(`Failed to read lib ${lib}`); + } + fs.writeFileSync(`/lib/lib.${lib}.d.ts`, content); + } fs.writeFileSync("/lib/lib.d.ts", Harness.IO.readFile(combinePaths(Harness.libFolder, "lib.d.ts"))!); fs.meta.set("defaultLibLocation", "/lib"); fs.makeReadonly(); diff --git a/tests/baselines/reference/outfile-concat-fileListing.txt b/tests/baselines/reference/outfile-concat-fileListing.txt index 3fca9338912..fc6a1e7b28c 100644 --- a/tests/baselines/reference/outfile-concat-fileListing.txt +++ b/tests/baselines/reference/outfile-concat-fileListing.txt @@ -1,6 +1,10 @@ */ /lib/ /lib/lib.d.ts + /lib/lib.dom.d.ts + /lib/lib.es5.d.ts + /lib/lib.scripthost.d.ts + /lib/lib.webworker.importscripts.d.ts /src/ /src/2/ /src/2/second-output.d.ts From 856fc79ae75d194919df738f3d61194c1a0dcbfc Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Tue, 5 Jun 2018 16:28:42 -0700 Subject: [PATCH 42/81] Lint --- src/compiler/program.ts | 2 +- src/harness/vfs.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 40a1bdcafff..b08f966864a 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -618,7 +618,7 @@ namespace ts { // A parallel array to projectReferences storing the results of reading in the referenced tsconfig files const resolvedProjectReferences: (ResolvedProjectReference | undefined)[] | undefined = projectReferences ? [] : undefined; const projectReferenceRedirects: Map = createMap(); - + const shouldCreateNewSourceFile = shouldProgramCreateNewSourceFiles(oldProgram, options); const structuralIsReused = tryReuseStructureFromOldProgram(); if (structuralIsReused !== StructureIsReused.Completely) { diff --git a/src/harness/vfs.ts b/src/harness/vfs.ts index 0e16b0fe715..2c1c6f2dd54 100644 --- a/src/harness/vfs.ts +++ b/src/harness/vfs.ts @@ -1155,7 +1155,7 @@ namespace vfs { EROFS: "file system is read-only" }); - export function createIOError(code: keyof typeof IOErrorMessages, details: string = "") { + export function createIOError(code: keyof typeof IOErrorMessages, details = "") { const err: NodeJS.ErrnoException = new Error(`${code}: ${IOErrorMessages[code]} ${details}`); err.code = code; if (Error.captureStackTrace) Error.captureStackTrace(err, createIOError); From 291289f8c2abef4c23b2b9ecf366479c38d6c476 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Wed, 6 Jun 2018 13:03:03 -0700 Subject: [PATCH 43/81] Update messages --- src/compiler/diagnosticMessages.json | 20 ++++++++++---------- src/compiler/tsbuild.ts | 25 +++++++++++-------------- src/harness/unittests/tsbuild.ts | 20 ++++++++++---------- 3 files changed, 31 insertions(+), 34 deletions(-) diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 3aa9ea2b174..f9106b721e2 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -3633,19 +3633,19 @@ "code": 6352 }, - "Project '{0}' is up to date with its upstream types": { + "Project '{0}' is up to date with .d.ts files from its dependencies": { "category": "Message", "code": 6353 }, - "Sorted list of input projects: {0}": { + "Projects in this build: {0}": { "category": "Message", "code": 6354 }, - "Would delete the following files:{0}": { + "A non-dry build would delete the following files: {0}": { "category": "Message", "code": 6355 }, - "Would build project '{0}'": { + "A non-dry build would build project '{0}'": { "category": "Message", "code": 6356 }, @@ -3657,7 +3657,7 @@ "category": "Message", "code": 6358 }, - "Project '{0}' is up to date because it was previously built": { + "delete this - Project '{0}' is up to date because it was previously built": { "category": "Message", "code": 6359 }, @@ -3665,15 +3665,15 @@ "category": "Message", "code": 6360 }, - "Skipping build of project '{0}' because its upstream project '{1}' has errors": { + "Skipping build of project '{0}' because its dependency '{1}' has errors": { "category": "Message", "code": 6361 }, - "Project '{0}' can't be built because it depends on a project with errors": { + "Project '{0}' can't be built because its dependency '{1}' has errors": { "category": "Message", "code": 6362 }, - "Build one or more projects and their dependencies, if out-of-date": { + "Build one or more projects and their dependencies, if out of date": { "category": "Message", "code": 6363 }, @@ -3685,11 +3685,11 @@ "category": "Message", "code": 6365 }, - "Show what would be built (or deleted, if specified with --clean)": { + "Show what would be built (or deleted, if specified with '--clean')": { "category": "Message", "code": 6366 }, - "Build all projects, including those that appear to be up-to-date": { + "Build all projects, including those that appear to be up to date": { "category": "Message", "code": 6367 }, diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index 974edd6c60c..e91c23d02d4 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -641,7 +641,7 @@ namespace ts { reportProjectStatus(next, status); if (status.type === UpToDateStatusType.UpstreamBlocked) { - context.verbose(Diagnostics.Skipping_build_of_project_0_because_its_upstream_project_1_has_errors, resolved, status.upstreamProjectName); + context.verbose(Diagnostics.Skipping_build_of_project_0_because_its_dependency_1_has_errors, resolved, status.upstreamProjectName); continue; } @@ -859,7 +859,7 @@ namespace ts { function buildSingleProject(proj: ResolvedConfigFileName): BuildResultFlags { if (context.options.dry) { - reportDiagnostic(createCompilerDiagnostic(Diagnostics.Would_build_project_0, proj)); + reportDiagnostic(createCompilerDiagnostic(Diagnostics.A_non_dry_build_would_build_project_0, proj)); return BuildResultFlags.Success; } @@ -948,7 +948,7 @@ namespace ts { function updateOutputTimestamps(proj: ParsedCommandLine) { if (context.options.dry) { - reportDiagnostic(createCompilerDiagnostic(Diagnostics.Would_build_project_0, proj.options.configFilePath)); + reportDiagnostic(createCompilerDiagnostic(Diagnostics.A_non_dry_build_would_build_project_0, proj.options.configFilePath)); return; } @@ -1013,7 +1013,7 @@ namespace ts { } if (context.options.dry) { - reportDiagnostic(createCompilerDiagnostic(Diagnostics.Would_delete_the_following_files_Colon_0, filesToDelete.map(f => `\r\n * ${f}`).join(""))); + reportDiagnostic(createCompilerDiagnostic(Diagnostics.A_non_dry_build_would_delete_the_following_files_Colon_0, filesToDelete.map(f => `\r\n * ${f}`).join(""))); } else { if (!host.deleteFile) { @@ -1083,7 +1083,7 @@ namespace ts { } if (status.type === UpToDateStatusType.UpstreamBlocked) { - context.verbose(Diagnostics.Skipping_build_of_project_0_because_its_upstream_project_1_has_errors, projName, status.upstreamProjectName); + context.verbose(Diagnostics.Skipping_build_of_project_0_because_its_dependency_1_has_errors, projName, status.upstreamProjectName); continue; } @@ -1101,7 +1101,7 @@ namespace ts { for (const name of graph.buildQueue) { names.push(name); } - context.verbose(Diagnostics.Sorted_list_of_input_projects_Colon_0, names.map(s => "\r\n * " + s).join("")); + context.verbose(Diagnostics.Projects_in_this_build_Colon_0, names.map(s => "\r\n * " + s).join("")); } /** @@ -1123,22 +1123,19 @@ namespace ts { if (status.newestInputFileTime !== undefined) { context.verbose(Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, configFileName, status.newestInputFileTime, status.newestOutputFileTime); } - else { - context.verbose(Diagnostics.Project_0_is_up_to_date_because_it_was_previously_built, configFileName); - } + // Don't report anything for "up to date because it was already built" -- too verbose return; case UpToDateStatusType.UpToDateWithUpstreamTypes: - context.verbose(Diagnostics.Project_0_is_up_to_date_with_its_upstream_types, configFileName); + context.verbose(Diagnostics.Project_0_is_up_to_date_with_d_ts_files_from_its_dependencies, configFileName); return; case UpToDateStatusType.UpstreamOutOfDate: - context.verbose(Diagnostics.Project_0_is_up_to_date_with_its_upstream_types, configFileName); + context.verbose(Diagnostics.Project_0_is_up_to_date_with_d_ts_files_from_its_dependencies, configFileName); return; case UpToDateStatusType.UpstreamBlocked: - context.verbose(Diagnostics.Project_0_can_t_be_built_because_it_depends_on_a_project_with_errors, configFileName); + context.verbose(Diagnostics.Project_0_can_t_be_built_because_its_dependency_1_has_errors, configFileName, status.upstreamProjectName); return; case UpToDateStatusType.Unbuildable: - // TODO different error - context.verbose(Diagnostics.Project_0_can_t_be_built_because_it_depends_on_a_project_with_errors, configFileName); + context.verbose(Diagnostics.Failed_to_parse_file_0_Colon_1, configFileName, status.reason); return; default: assertTypeIsNever(status); diff --git a/src/harness/unittests/tsbuild.ts b/src/harness/unittests/tsbuild.ts index f076e0b34ab..ff80a5b96af 100644 --- a/src/harness/unittests/tsbuild.ts +++ b/src/harness/unittests/tsbuild.ts @@ -35,7 +35,7 @@ namespace ts { const host = new fakes.CompilerHost(fs); const builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: true, force: false, verbose: false }); builder.buildAllProjects(); - assertDiagnosticMessages(Diagnostics.Would_build_project_0, Diagnostics.Would_build_project_0, Diagnostics.Would_build_project_0); + assertDiagnosticMessages(Diagnostics.A_non_dry_build_would_build_project_0, Diagnostics.A_non_dry_build_would_build_project_0, Diagnostics.A_non_dry_build_would_build_project_0); // Check for outputs to not be written. Not an exhaustive list for (const output of allExpectedOutputs) { @@ -116,7 +116,7 @@ namespace ts { clearDiagnostics(); builder.resetBuildContext(); builder.buildAllProjects(); - assertDiagnosticMessages(Diagnostics.Sorted_list_of_input_projects_Colon_0, + assertDiagnosticMessages(Diagnostics.Projects_in_this_build_Colon_0, Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, Diagnostics.Building_project_0, Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, @@ -131,7 +131,7 @@ namespace ts { clearDiagnostics(); builder.resetBuildContext(); builder.buildAllProjects(); - assertDiagnosticMessages(Diagnostics.Sorted_list_of_input_projects_Colon_0, + assertDiagnosticMessages(Diagnostics.Projects_in_this_build_Colon_0, Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2); @@ -145,7 +145,7 @@ namespace ts { builder.resetBuildContext(); builder.buildAllProjects(); - assertDiagnosticMessages(Diagnostics.Sorted_list_of_input_projects_Colon_0, + assertDiagnosticMessages(Diagnostics.Projects_in_this_build_Colon_0, Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, @@ -160,12 +160,12 @@ namespace ts { builder.resetBuildContext(); builder.buildAllProjects(); - assertDiagnosticMessages(Diagnostics.Sorted_list_of_input_projects_Colon_0, + assertDiagnosticMessages(Diagnostics.Projects_in_this_build_Colon_0, Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, Diagnostics.Building_project_0, - Diagnostics.Project_0_is_up_to_date_with_its_upstream_types, + Diagnostics.Project_0_is_up_to_date_with_d_ts_files_from_its_dependencies, Diagnostics.Updating_output_timestamps_of_project_0, - Diagnostics.Project_0_is_up_to_date_with_its_upstream_types, + Diagnostics.Project_0_is_up_to_date_with_d_ts_files_from_its_dependencies, Diagnostics.Updating_output_timestamps_of_project_0); }); }); @@ -182,14 +182,14 @@ namespace ts { replaceText(fs, "/src/logic/index.ts", "c.multiply(10, 15)", `c.muitply()`); builder.buildAllProjects(); assertDiagnosticMessages( - Diagnostics.Sorted_list_of_input_projects_Colon_0, + Diagnostics.Projects_in_this_build_Colon_0, Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, Diagnostics.Building_project_0, Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, Diagnostics.Building_project_0, Diagnostics.Property_0_does_not_exist_on_type_1, - Diagnostics.Project_0_can_t_be_built_because_it_depends_on_a_project_with_errors, - Diagnostics.Skipping_build_of_project_0_because_its_upstream_project_1_has_errors + Diagnostics.Project_0_can_t_be_built_because_its_dependency_1_has_errors, + Diagnostics.Skipping_build_of_project_0_because_its_dependency_1_has_errors ); }); }); From b107849a3a2f67df1f2d620835825c94aac888bd Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Wed, 6 Jun 2018 13:47:59 -0700 Subject: [PATCH 44/81] Improve handling of container projects and issue relative filename messages --- src/compiler/diagnosticMessages.json | 44 ++++++----- src/compiler/tsbuild.ts | 106 ++++++++++++++++++++------- 2 files changed, 103 insertions(+), 47 deletions(-) diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index f9106b721e2..668f3b5a6f0 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -3632,78 +3632,82 @@ "category": "Message", "code": 6352 }, - - "Project '{0}' is up to date with .d.ts files from its dependencies": { + "Project '{0}' is out of date because its dependency '{1}' is out of date": { "category": "Message", "code": 6353 }, - "Projects in this build: {0}": { + + "Project '{0}' is up to date with .d.ts files from its dependencies": { "category": "Message", "code": 6354 }, - "A non-dry build would delete the following files: {0}": { + "Projects in this build: {0}": { "category": "Message", "code": 6355 }, - "A non-dry build would build project '{0}'": { + "A non-dry build would delete the following files: {0}": { "category": "Message", "code": 6356 }, - "Building project '{0}'...": { + "A non-dry build would build project '{0}'": { "category": "Message", "code": 6357 }, - "Updating output timestamps of project '{0}'...": { + "Building project '{0}'...": { "category": "Message", "code": 6358 }, - "delete this - Project '{0}' is up to date because it was previously built": { + "Updating output timestamps of project '{0}'...": { "category": "Message", "code": 6359 }, - "Project '{0}' is up to date": { + "delete this - Project '{0}' is up to date because it was previously built": { "category": "Message", "code": 6360 }, - "Skipping build of project '{0}' because its dependency '{1}' has errors": { + "Project '{0}' is up to date": { "category": "Message", "code": 6361 }, - "Project '{0}' can't be built because its dependency '{1}' has errors": { + "Skipping build of project '{0}' because its dependency '{1}' has errors": { "category": "Message", "code": 6362 }, - "Build one or more projects and their dependencies, if out of date": { + "Project '{0}' can't be built because its dependency '{1}' has errors": { "category": "Message", "code": 6363 }, - "Delete the outputs of all projects": { + "Build one or more projects and their dependencies, if out of date": { "category": "Message", "code": 6364 }, - "Enable verbose logging": { + "Delete the outputs of all projects": { "category": "Message", "code": 6365 }, - "Show what would be built (or deleted, if specified with '--clean')": { + "Enable verbose logging": { "category": "Message", "code": 6366 }, - "Build all projects, including those that appear to be up to date": { + "Show what would be built (or deleted, if specified with '--clean')": { "category": "Message", "code": 6367 }, - "Option '--build' must be the first command line argument.": { - "category": "Error", + "Build all projects, including those that appear to be up to date": { + "category": "Message", "code": 6368 }, - "Options '{0}' and '{1}' cannot be combined.": { + "Option '--build' must be the first command line argument.": { "category": "Error", "code": 6369 }, + "Options '{0}' and '{1}' cannot be combined.": { + "category": "Error", + "code": 6370 + }, "Skipping clean because not all projects could be located": { "category": "Error", - "code": 6340 + "code": 6371 }, "Variable '{0}' implicitly has an '{1}' type.": { diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index e91c23d02d4..db998d651c3 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -90,7 +90,12 @@ namespace ts { OutOfDateWithSelf, OutOfDateWithUpstream, UpstreamOutOfDate, - UpstreamBlocked + UpstreamBlocked, + + /** + * Projects with no outputs (i.e. "solution" files) + */ + ContainerOnly } export type UpToDateStatus = @@ -100,7 +105,8 @@ namespace ts { | Status.OutOfDateWithSelf | Status.OutOfDateWithUpstream | Status.UpstreamOutOfDate - | Status.UpstreamBlocked; + | Status.UpstreamBlocked + | Status.ContainerOnly; export namespace Status { /** @@ -112,6 +118,13 @@ namespace ts { reason: string; } + /** + * This project doesn't have any outputs, so "is it up to date" is a meaningless question. + */ + export interface ContainerOnly { + type: UpToDateStatusType.ContainerOnly; + } + /** * The project is up to date with respect to its inputs. * We track what the newest input file is. @@ -119,8 +132,11 @@ namespace ts { export interface UpToDate { type: UpToDateStatusType.UpToDate | UpToDateStatusType.UpToDateWithUpstreamTypes; newestInputFileTime: Date; + newestInputFileName: string; newestDeclarationFileContentChangedTime: Date; newestOutputFileTime: Date; + newestOutputFileName: string; + oldestOutputFileName: string; } /** @@ -684,12 +700,19 @@ namespace ts { // Collect the expected outputs of this project const outputs = getAllProjectOutputs(project); + if (outputs.length === 0) { + return { + type: UpToDateStatusType.ContainerOnly + }; + } + // Now see if all outputs are newer than the newest input - let oldestOutputFileName: string | undefined; - let oldestOutputFileTime: Date = maximumDate; - let newestOutputFileTime: Date = minimumDate; - let newestDeclarationFileContentChangedTime: Date = minimumDate; + let oldestOutputFileName = "(none)"; + let oldestOutputFileTime = maximumDate; + let newestOutputFileName = "(none)"; + let newestOutputFileTime = minimumDate; let missingOutputFileName: string | undefined; + let newestDeclarationFileContentChangedTime = minimumDate; let isOutOfDateWithInputs = false; for (const output of outputs) { // Output is missing; can stop checking @@ -712,7 +735,10 @@ namespace ts { break; } - newestOutputFileTime = newer(newestOutputFileTime, outputTime); + if (outputTime > newestOutputFileTime) { + newestOutputFileTime = outputTime; + newestOutputFileName = output; + } // Keep track of when the most recent time a .d.ts file was changed. // In addition to file timestamps, we also keep track of when a .d.ts file @@ -768,7 +794,7 @@ namespace ts { Debug.assert(oldestOutputFileName !== undefined, "Should have an oldest output filename here"); return { type: UpToDateStatusType.OutOfDateWithUpstream, - outOfDateOutputFileName: oldestOutputFileName!, + outOfDateOutputFileName: oldestOutputFileName, newerProjectName: ref.path }; } @@ -784,7 +810,7 @@ namespace ts { if (isOutOfDateWithInputs) { return { type: UpToDateStatusType.OutOfDateWithSelf, - outOfDateOutputFileName: oldestOutputFileName!, + outOfDateOutputFileName: oldestOutputFileName, newerInputFileName: newestInputFileName }; } @@ -794,7 +820,10 @@ namespace ts { type: pseudoUpToDate ? UpToDateStatusType.UpToDateWithUpstreamTypes : UpToDateStatusType.UpToDate, newestDeclarationFileContentChangedTime, newestInputFileTime, - newestOutputFileTime + newestOutputFileTime, + newestInputFileName, + newestOutputFileName, + oldestOutputFileName }; } @@ -1087,6 +1116,11 @@ namespace ts { continue; } + if (status.type === UpToDateStatusType.ContainerOnly) { + // Do nothing + continue; + } + buildSingleProject(next); } } @@ -1101,7 +1135,11 @@ namespace ts { for (const name of graph.buildQueue) { names.push(name); } - context.verbose(Diagnostics.Projects_in_this_build_Colon_0, names.map(s => "\r\n * " + s).join("")); + context.verbose(Diagnostics.Projects_in_this_build_Colon_0, names.map(s => "\r\n * " + relName(s)).join("")); + } + + function relName(path: string): string { + return convertToRelativePath(path, host.getCurrentDirectory(), f => host.getCanonicalFileName(f)); } /** @@ -1111,32 +1149,46 @@ namespace ts { if (!context.options.verbose) return; switch (status.type) { case UpToDateStatusType.OutOfDateWithSelf: - context.verbose(Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, configFileName, status.outOfDateOutputFileName, status.newerInputFileName); - return; + return context.verbose(Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, + relName(configFileName), + relName(status.outOfDateOutputFileName), + relName(status.newerInputFileName)); case UpToDateStatusType.OutOfDateWithUpstream: - context.verbose(Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, configFileName, status.outOfDateOutputFileName, status.newerProjectName); - return; + return context.verbose(Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, + relName(configFileName), + relName(status.outOfDateOutputFileName), + relName(status.newerProjectName)); case UpToDateStatusType.OutputMissing: - context.verbose(Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, configFileName, status.missingOutputFileName); - return; + return context.verbose(Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, + relName(configFileName), + relName(status.missingOutputFileName)); case UpToDateStatusType.UpToDate: if (status.newestInputFileTime !== undefined) { - context.verbose(Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, configFileName, status.newestInputFileTime, status.newestOutputFileTime); + return context.verbose(Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, + relName(configFileName), + relName(status.newestInputFileName), + relName(status.oldestOutputFileName)); } // Don't report anything for "up to date because it was already built" -- too verbose - return; + break; case UpToDateStatusType.UpToDateWithUpstreamTypes: - context.verbose(Diagnostics.Project_0_is_up_to_date_with_d_ts_files_from_its_dependencies, configFileName); - return; + return context.verbose(Diagnostics.Project_0_is_up_to_date_with_d_ts_files_from_its_dependencies, + relName(configFileName)); case UpToDateStatusType.UpstreamOutOfDate: - context.verbose(Diagnostics.Project_0_is_up_to_date_with_d_ts_files_from_its_dependencies, configFileName); - return; + return context.verbose(Diagnostics.Project_0_is_out_of_date_because_its_dependency_1_is_out_of_date, + relName(configFileName), + relName(status.upstreamProjectName)); case UpToDateStatusType.UpstreamBlocked: - context.verbose(Diagnostics.Project_0_can_t_be_built_because_its_dependency_1_has_errors, configFileName, status.upstreamProjectName); - return; + return context.verbose(Diagnostics.Project_0_can_t_be_built_because_its_dependency_1_has_errors, + relName(configFileName), + relName(status.upstreamProjectName)); case UpToDateStatusType.Unbuildable: - context.verbose(Diagnostics.Failed_to_parse_file_0_Colon_1, configFileName, status.reason); - return; + return context.verbose(Diagnostics.Failed_to_parse_file_0_Colon_1, + relName(configFileName), + status.reason); + case UpToDateStatusType.ContainerOnly: + // Don't report status on "solution" projects + break; default: assertTypeIsNever(status); } From 21a65f5dc03d619b58d98a9e2cb0387720f73d96 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Wed, 6 Jun 2018 16:47:15 -0700 Subject: [PATCH 45/81] Improved watch mode --- src/compiler/tsbuild.ts | 35 ++++++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index db998d651c3..c041540cdfe 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -341,8 +341,6 @@ namespace ts { const cache = createFileMap(); const configParseHost = parseConfigHostFromCompilerHost(host); - // TODO: Cache invalidation under --watch - function parseConfigFile(configFilePath: ResolvedConfigFileName) { const sourceFile = host.getSourceFile(configFilePath, ScriptTarget.JSON) as JsonSourceFile; if (sourceFile === undefined) { @@ -529,6 +527,8 @@ namespace ts { const configFileCache = createConfigFileCache(host); let context = createBuildContext(defaultOptions, reportDiagnostic); + const existingWatchersForWildcards = createMap(); + return { buildAllProjects, getUpToDateStatus, @@ -551,18 +551,43 @@ namespace ts { if (!system.watchFile || !system.watchDirectory || !system.setTimeout) throw new Error("System host must support watchFile / watchDirectory / setTimeout if using --watch"); const graph = getGlobalDependencyGraph()!; + if (!graph.buildQueue) { + // Everything is broken - we don't even know what to watch. Give up. + return; + } + for (const resolved of graph.buildQueue) { const cfg = configFileCache.parseConfigFile(resolved); if (cfg) { + // Watch this file + system.watchFile!(resolved, () => { + configFileCache.removeKey(resolved); + invalidateProjectAndScheduleBuilds(resolved); + }); + + // Update watchers for wildcard directories + if (cfg.configFileSpecs) { + updateWatchingWildcardDirectories(existingWatchersForWildcards, createMapFromTemplate(cfg.configFileSpecs.wildcardDirectories), (dir, flags) => { + return system.watchDirectory!(dir, () => { + invalidateProjectAndScheduleBuilds(resolved); + }, !!(flags & WatchDirectoryFlags.Recursive)); + }); + } + + // Watch input files for (const input of cfg.fileNames) { system.watchFile(input, () => { - invalidateProject(resolved); - system.setTimeout!(buildInvalidatedProjects, 100); - system.setTimeout!(buildDependentInvalidatedProjects, 3000); + invalidateProjectAndScheduleBuilds(resolved); }); } } } + + function invalidateProjectAndScheduleBuilds(resolved: ResolvedConfigFileName) { + invalidateProject(resolved); + system!.setTimeout!(buildInvalidatedProjects, 100); + system!.setTimeout!(buildDependentInvalidatedProjects, 3000); + } } function resetBuildContext(opts = defaultOptions) { From 7f2436ca35f070df351522bf5ae28db50e056e26 Mon Sep 17 00:00:00 2001 From: csigs Date: Thu, 7 Jun 2018 16:10:39 +0000 Subject: [PATCH 46/81] LEGO: check in for master to temporary branch. --- .../diagnosticMessages.generated.json.lcl | 15 +++++++++++++ .../diagnosticMessages.generated.json.lcl | 21 +++++++++++++++++++ .../diagnosticMessages.generated.json.lcl | 21 +++++++++++++++++++ 3 files changed, 57 insertions(+) diff --git a/src/loc/lcl/jpn/diagnosticMessages/diagnosticMessages.generated.json.lcl b/src/loc/lcl/jpn/diagnosticMessages/diagnosticMessages.generated.json.lcl index 4352021fac7..803851db20c 100644 --- a/src/loc/lcl/jpn/diagnosticMessages/diagnosticMessages.generated.json.lcl +++ b/src/loc/lcl/jpn/diagnosticMessages/diagnosticMessages.generated.json.lcl @@ -1935,6 +1935,18 @@ + + + + + + + + + + + + @@ -8712,6 +8724,9 @@ + + + diff --git a/src/loc/lcl/kor/diagnosticMessages/diagnosticMessages.generated.json.lcl b/src/loc/lcl/kor/diagnosticMessages/diagnosticMessages.generated.json.lcl index b0d68c1af91..591661b36f2 100644 --- a/src/loc/lcl/kor/diagnosticMessages/diagnosticMessages.generated.json.lcl +++ b/src/loc/lcl/kor/diagnosticMessages/diagnosticMessages.generated.json.lcl @@ -1935,6 +1935,18 @@ + + + + + + + + + + + + @@ -8709,6 +8721,15 @@ + + + + + + + + + diff --git a/src/loc/lcl/rus/diagnosticMessages/diagnosticMessages.generated.json.lcl b/src/loc/lcl/rus/diagnosticMessages/diagnosticMessages.generated.json.lcl index 9e0ffcb8f8f..282a3593711 100644 --- a/src/loc/lcl/rus/diagnosticMessages/diagnosticMessages.generated.json.lcl +++ b/src/loc/lcl/rus/diagnosticMessages/diagnosticMessages.generated.json.lcl @@ -1934,6 +1934,18 @@ + + + + + + + + + + + + @@ -8708,6 +8720,15 @@ + + + + + + + + + From 8147347e413b7ae7c38c1a74734f7d9508bd925f Mon Sep 17 00:00:00 2001 From: Mohamed Hegazy Date: Thu, 7 Jun 2018 10:20:11 -0700 Subject: [PATCH 47/81] Update Third Party Notice text --- ThirdPartyNoticeText.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ThirdPartyNoticeText.txt b/ThirdPartyNoticeText.txt index 5b3700bf382..baa89396c08 100644 --- a/ThirdPartyNoticeText.txt +++ b/ThirdPartyNoticeText.txt @@ -1,7 +1,6 @@ /*!----------------- TypeScript ThirdPartyNotices ------------------------------------------------------- -The TypeScript software is based on or incorporates material and code from the projects listed below (collectively "Third Party Code"). Microsoft is not the original author of the Third Party Code. The original copyright notice and the license, under which Microsoft received such Third Party Code, are set forth below. Such license and notices are provided for informational purposes only. Microsoft licenses the Third Party Code to you under the terms of the Apache 2.0 License. -All Third Party Code licensed by Microsoft under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +The TypeScript software is based on or incorporates material and code from the projects listed below (collectively "Third Party Code"). Microsoft is not the original author of the Third Party Code. The original copyright notice and the license, under which Microsoft received such Third Party Code, are set forth below. Such license and notices are provided for informational purposes only. MThe TypeScript software incorporates third party material from the projects listed below. The original copyright notice and the license under which Microsoft received such third party material are set forth below. Microsoft reserves all other rights not expressly granted, whether by implication, estoppel or otherwise. you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT. From d0ae03c4cc672e03687d11a9c1f805cdb36142e4 Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Thu, 7 Jun 2018 10:40:14 -0700 Subject: [PATCH 48/81] Handle import types in serializeType (#24701) --- src/compiler/transformers/ts.ts | 1 + .../reference/metadataImportType.errors.txt | 13 +++++++++ .../baselines/reference/metadataImportType.js | 28 +++++++++++++++++++ .../reference/metadataImportType.symbols | 8 ++++++ .../reference/metadataImportType.types | 11 ++++++++ tests/cases/compiler/metadataImportType.ts | 6 ++++ 6 files changed, 67 insertions(+) create mode 100644 tests/baselines/reference/metadataImportType.errors.txt create mode 100644 tests/baselines/reference/metadataImportType.js create mode 100644 tests/baselines/reference/metadataImportType.symbols create mode 100644 tests/baselines/reference/metadataImportType.types create mode 100644 tests/cases/compiler/metadataImportType.ts diff --git a/src/compiler/transformers/ts.ts b/src/compiler/transformers/ts.ts index 076645a14d8..0d29ac43ecf 100644 --- a/src/compiler/transformers/ts.ts +++ b/src/compiler/transformers/ts.ts @@ -1912,6 +1912,7 @@ namespace ts { case SyntaxKind.AnyKeyword: case SyntaxKind.UnknownKeyword: case SyntaxKind.ThisType: + case SyntaxKind.ImportType: break; default: diff --git a/tests/baselines/reference/metadataImportType.errors.txt b/tests/baselines/reference/metadataImportType.errors.txt new file mode 100644 index 00000000000..31824696a0b --- /dev/null +++ b/tests/baselines/reference/metadataImportType.errors.txt @@ -0,0 +1,13 @@ +tests/cases/compiler/metadataImportType.ts(2,6): error TS2304: Cannot find name 'test'. +tests/cases/compiler/metadataImportType.ts(3,8): error TS2307: Cannot find module './b'. + + +==== tests/cases/compiler/metadataImportType.ts (2 errors) ==== + export class A { + @test + ~~~~ +!!! error TS2304: Cannot find name 'test'. + b: import('./b').B + ~~~~~~~~~~~~~~~ +!!! error TS2307: Cannot find module './b'. + } \ No newline at end of file diff --git a/tests/baselines/reference/metadataImportType.js b/tests/baselines/reference/metadataImportType.js new file mode 100644 index 00000000000..9457a282582 --- /dev/null +++ b/tests/baselines/reference/metadataImportType.js @@ -0,0 +1,28 @@ +//// [metadataImportType.ts] +export class A { + @test + b: import('./b').B +} + +//// [metadataImportType.js] +"use strict"; +var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { + var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; + if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); + else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; + return c > 3 && r && Object.defineProperty(target, key, r), r; +}; +var __metadata = (this && this.__metadata) || function (k, v) { + if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); +}; +exports.__esModule = true; +var A = /** @class */ (function () { + function A() { + } + __decorate([ + test, + __metadata("design:type", Object) + ], A.prototype, "b"); + return A; +}()); +exports.A = A; diff --git a/tests/baselines/reference/metadataImportType.symbols b/tests/baselines/reference/metadataImportType.symbols new file mode 100644 index 00000000000..ce16caa805c --- /dev/null +++ b/tests/baselines/reference/metadataImportType.symbols @@ -0,0 +1,8 @@ +=== tests/cases/compiler/metadataImportType.ts === +export class A { +>A : Symbol(A, Decl(metadataImportType.ts, 0, 0)) + + @test + b: import('./b').B +>b : Symbol(A.b, Decl(metadataImportType.ts, 0, 16)) +} diff --git a/tests/baselines/reference/metadataImportType.types b/tests/baselines/reference/metadataImportType.types new file mode 100644 index 00000000000..05ba1bc9924 --- /dev/null +++ b/tests/baselines/reference/metadataImportType.types @@ -0,0 +1,11 @@ +=== tests/cases/compiler/metadataImportType.ts === +export class A { +>A : A + + @test +>test : any + + b: import('./b').B +>b : any +>B : No type information available! +} diff --git a/tests/cases/compiler/metadataImportType.ts b/tests/cases/compiler/metadataImportType.ts new file mode 100644 index 00000000000..531764f574a --- /dev/null +++ b/tests/cases/compiler/metadataImportType.ts @@ -0,0 +1,6 @@ +// @experimentalDecorators: true +// @emitDecoratorMetadata: true +export class A { + @test + b: import('./b').B +} \ No newline at end of file From acbda14450ca209f7d2304a800986186e1f04aaa Mon Sep 17 00:00:00 2001 From: krk Date: Thu, 7 Jun 2018 20:51:11 +0300 Subject: [PATCH 49/81] addMethodDeclaration codefix creates a generator function when target is child of a YieldExpression, resolves #24728 --- src/services/codefixes/helpers.ts | 6 ++++-- ...eFixAddMissingMember_generator_function.ts | 21 +++++++++++++++++++ ...AddMissingMember_non_generator_function.ts | 21 +++++++++++++++++++ 3 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 tests/cases/fourslash/codeFixAddMissingMember_generator_function.ts create mode 100644 tests/cases/fourslash/codeFixAddMissingMember_non_generator_function.ts diff --git a/src/services/codefixes/helpers.ts b/src/services/codefixes/helpers.ts index 5d033a82cb3..8de22cafe84 100644 --- a/src/services/codefixes/helpers.ts +++ b/src/services/codefixes/helpers.ts @@ -111,16 +111,18 @@ namespace ts.codefix { } export function createMethodFromCallExpression( - { typeArguments, arguments: args }: CallExpression, + { typeArguments, arguments: args, parent: parent }: CallExpression, methodName: string, inJs: boolean, makeStatic: boolean, preferences: UserPreferences, ): MethodDeclaration { + const asterisk = parent.kind === SyntaxKind.YieldExpression ? createToken(SyntaxKind.AsteriskToken) : undefined; + return createMethod( /*decorators*/ undefined, /*modifiers*/ makeStatic ? [createToken(SyntaxKind.StaticKeyword)] : undefined, - /*asteriskToken*/ undefined, + /*asteriskToken*/ asterisk, methodName, /*questionToken*/ undefined, /*typeParameters*/ inJs ? undefined : map(typeArguments, (_, i) => diff --git a/tests/cases/fourslash/codeFixAddMissingMember_generator_function.ts b/tests/cases/fourslash/codeFixAddMissingMember_generator_function.ts new file mode 100644 index 00000000000..6742cc43348 --- /dev/null +++ b/tests/cases/fourslash/codeFixAddMissingMember_generator_function.ts @@ -0,0 +1,21 @@ +/// + +////class C { +//// *method() { +//// yield* this.y(); +//// } +////} + +verify.codeFixAll({ + fixId: "addMissingMember", + fixAllDescription: "Add all missing members", + newFileContent: + `class C { + *method() { + yield* this.y(); + } + *y(): any { + throw new Error("Method not implemented."); + } +}`, +}); diff --git a/tests/cases/fourslash/codeFixAddMissingMember_non_generator_function.ts b/tests/cases/fourslash/codeFixAddMissingMember_non_generator_function.ts new file mode 100644 index 00000000000..a868646446a --- /dev/null +++ b/tests/cases/fourslash/codeFixAddMissingMember_non_generator_function.ts @@ -0,0 +1,21 @@ +/// + +////class C { +//// method() { +//// yield* this.y(); +//// } +////} + +verify.codeFixAll({ + fixId: "addMissingMember", + fixAllDescription: "Add all missing members", + newFileContent: + `class C { + method() { + yield* this.y(); + } + y(): any { + throw new Error("Method not implemented."); + } +}`, +}); From 5b92bdd88fc85c66bc1de599ce1ddebf1748f92d Mon Sep 17 00:00:00 2001 From: Klaus Meinhardt Date: Thu, 7 Jun 2018 20:35:47 +0200 Subject: [PATCH 50/81] checker: avoid useless closures --- src/compiler/checker.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index fe152f38556..8fea3750d56 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -2900,7 +2900,7 @@ namespace ts { function hasVisibleDeclarations(symbol: Symbol, shouldComputeAliasToMakeVisible: boolean): SymbolVisibilityResult | undefined { let aliasesToMakeVisible: LateVisibilityPaintedStatement[] | undefined; - if (forEach(symbol.declarations, declaration => !getIsDeclarationVisible(declaration))) { + if (!every(symbol.declarations, getIsDeclarationVisible)) { return undefined; } return { accessibility: SymbolAccessibility.Accessible, aliasesToMakeVisible }; @@ -5494,7 +5494,7 @@ namespace ts { // object types. function isValidBaseType(type: Type): type is BaseType { return !!(type.flags & (TypeFlags.Object | TypeFlags.NonPrimitive | TypeFlags.Any)) && !isGenericMappedType(type) || - !!(type.flags & TypeFlags.Intersection) && !some((type).types, t => !isValidBaseType(t)); + !!(type.flags & TypeFlags.Intersection) && every((type).types, isValidBaseType); } function resolveBaseTypesOfInterface(type: InterfaceType): void { @@ -10294,7 +10294,7 @@ namespace ts { return type.flags & TypeFlags.Object ? isEmptyResolvedType(resolveStructuredTypeMembers(type)) : type.flags & TypeFlags.NonPrimitive ? true : type.flags & TypeFlags.Union ? some((type).types, isEmptyObjectType) : - type.flags & TypeFlags.Intersection ? !some((type).types, t => !isEmptyObjectType(t)) : + type.flags & TypeFlags.Intersection ? every((type).types, isEmptyObjectType) : false; } @@ -11955,7 +11955,7 @@ namespace ts { function isLiteralType(type: Type): boolean { return type.flags & TypeFlags.Boolean ? true : - type.flags & TypeFlags.Union ? type.flags & TypeFlags.EnumLiteral ? true : !forEach((type).types, t => !isUnitType(t)) : + type.flags & TypeFlags.Union ? type.flags & TypeFlags.EnumLiteral ? true : every((type).types, isUnitType) : isUnitType(type); } @@ -16165,7 +16165,7 @@ namespace ts { return !!(type.flags & (TypeFlags.AnyOrUnknown | TypeFlags.NonPrimitive) || getFalsyFlags(type) & TypeFlags.DefinitelyFalsy && isValidSpreadType(removeDefinitelyFalsyTypes(type)) || type.flags & TypeFlags.Object && !isGenericMappedType(type) || - type.flags & TypeFlags.UnionOrIntersection && !forEach((type).types, t => !isValidSpreadType(t))); + type.flags & TypeFlags.UnionOrIntersection && every((type).types, isValidSpreadType)); } function checkJsxSelfClosingElement(node: JsxSelfClosingElement, checkMode: CheckMode | undefined): Type { @@ -20443,7 +20443,7 @@ namespace ts { if (propType.symbol && propType.symbol.flags & SymbolFlags.Class) { const name = prop.escapedName; const symbol = resolveName(prop.valueDeclaration, name, SymbolFlags.Type, undefined, name, /*isUse*/ false); - if (symbol && symbol.declarations.some(d => d.kind === SyntaxKind.JSDocTypedefTag)) { + if (symbol && symbol.declarations.some(isJSDocTypedefTag)) { grammarErrorOnNode(symbol.declarations[0], Diagnostics.Duplicate_identifier_0, unescapeLeadingUnderscores(name)); return grammarErrorOnNode(prop.valueDeclaration, Diagnostics.Duplicate_identifier_0, unescapeLeadingUnderscores(name)); } From 83c58a4fb5ea11f9b7dc13468300ef4a9c0d8565 Mon Sep 17 00:00:00 2001 From: Andy Date: Thu, 7 Jun 2018 12:05:47 -0700 Subject: [PATCH 51/81] Don't consider `x.` a new identifier location just because x has a number index signature (#24699) * Don't consider `x.` a new identifier location just because x has a number index signature * Update more tests --- src/services/completions.ts | 2 +- .../fourslash/completionListAfterStringLiteral1.ts | 10 +++++++--- tests/cases/fourslash/getJavaScriptCompletions12.ts | 2 +- tests/cases/fourslash/javaScriptClass1.ts | 2 +- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/services/completions.ts b/src/services/completions.ts index d83e86ab4ad..707e5fc0b01 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -1074,7 +1074,7 @@ namespace ts.Completions { } function addTypeProperties(type: Type): void { - isNewIdentifierLocation = hasIndexSignature(type); + isNewIdentifierLocation = !!type.getStringIndexType(); if (isUncheckedFile) { // In javascript files, for union types, we don't just get the members that diff --git a/tests/cases/fourslash/completionListAfterStringLiteral1.ts b/tests/cases/fourslash/completionListAfterStringLiteral1.ts index 533b428cdf5..9837f8a628c 100644 --- a/tests/cases/fourslash/completionListAfterStringLiteral1.ts +++ b/tests/cases/fourslash/completionListAfterStringLiteral1.ts @@ -2,6 +2,10 @@ ////"a"./**/ -goTo.marker(); -verify.not.completionListContains('alert'); -verify.completionListContains('charAt'); \ No newline at end of file +verify.completions({ + marker: "", + exact: [ + "toString", "charAt", "charCodeAt", "concat", "indexOf", "lastIndexOf", "localeCompare", "match", "replace", "search", "slice", + "split", "substring", "toLowerCase", "toLocaleLowerCase", "toUpperCase", "toLocaleUpperCase", "trim", "length", "substr", "valueOf", + ], +}); diff --git a/tests/cases/fourslash/getJavaScriptCompletions12.ts b/tests/cases/fourslash/getJavaScriptCompletions12.ts index df09b041e62..a819f26e250 100644 --- a/tests/cases/fourslash/getJavaScriptCompletions12.ts +++ b/tests/cases/fourslash/getJavaScriptCompletions12.ts @@ -24,7 +24,7 @@ ////var test1 = function(x) { return x./*4*/ }, test2 = function(a) { return a./*5*/ }; verify.completions( - { marker: "1", includes: { name: "charCodeAt", kind: "method" }, isNewIdentifierLocation: true }, + { marker: "1", includes: { name: "charCodeAt", kind: "method" } }, { marker: ["2", "3", "4"], includes: { name: "toExponential", kind: "method" } }, { marker: "5", includes: { name: "test1", kind: "warning" } }, ); diff --git a/tests/cases/fourslash/javaScriptClass1.ts b/tests/cases/fourslash/javaScriptClass1.ts index fbbb3c4880d..dd4ed33f718 100644 --- a/tests/cases/fourslash/javaScriptClass1.ts +++ b/tests/cases/fourslash/javaScriptClass1.ts @@ -22,7 +22,7 @@ edit.insert('.'); verify.completions({ exact: ["bar", "thing", "union", "Foo", "x"] }); edit.insert('bar.'); -verify.completions({ includes: ["substr"], isNewIdentifierLocation: true }); +verify.completions({ includes: ["substr"] }); edit.backspace('bar.'.length); edit.insert('union.'); From 0fefaf286df3efc3a54d3a429dde4ddb624a4443 Mon Sep 17 00:00:00 2001 From: Andy Date: Thu, 7 Jun 2018 12:10:48 -0700 Subject: [PATCH 52/81] moveToNewFile: Infer quote preference (#24652) --- src/services/codefixes/convertToEs6Module.ts | 56 +++++++++++-------- .../codefixes/fixInvalidImportSyntax.ts | 2 +- src/services/codefixes/importFixes.ts | 12 +--- src/services/codefixes/useDefaultImport.ts | 2 +- src/services/refactors/moveToNewFile.ts | 17 +++--- src/services/utilities.ts | 24 ++++++-- .../moveToNewFile_inferQuoteStyle.ts | 21 +++++++ 7 files changed, 85 insertions(+), 49 deletions(-) create mode 100644 tests/cases/fourslash/moveToNewFile_inferQuoteStyle.ts diff --git a/src/services/codefixes/convertToEs6Module.ts b/src/services/codefixes/convertToEs6Module.ts index c763d4da0c8..bd1e02845cd 100644 --- a/src/services/codefixes/convertToEs6Module.ts +++ b/src/services/codefixes/convertToEs6Module.ts @@ -5,10 +5,10 @@ namespace ts.codefix { getCodeActions(context) { const { sourceFile, program, preferences } = context; const changes = textChanges.ChangeTracker.with(context, changes => { - const moduleExportsChangedToDefault = convertFileToEs6Module(sourceFile, program.getTypeChecker(), changes, program.getCompilerOptions().target!, preferences); + const moduleExportsChangedToDefault = convertFileToEs6Module(sourceFile, program.getTypeChecker(), changes, program.getCompilerOptions().target!, getQuotePreference(sourceFile, preferences)); if (moduleExportsChangedToDefault) { for (const importingFile of program.getSourceFiles()) { - fixImportOfModuleExports(importingFile, sourceFile, changes, preferences); + fixImportOfModuleExports(importingFile, sourceFile, changes, getQuotePreference(importingFile, preferences)); } } }); @@ -17,7 +17,7 @@ namespace ts.codefix { }, }); - function fixImportOfModuleExports(importingFile: SourceFile, exportingFile: SourceFile, changes: textChanges.ChangeTracker, preferences: UserPreferences) { + function fixImportOfModuleExports(importingFile: SourceFile, exportingFile: SourceFile, changes: textChanges.ChangeTracker, quotePreference: QuotePreference) { for (const moduleSpecifier of importingFile.imports) { const imported = getResolvedModule(importingFile, moduleSpecifier.text); if (!imported || imported.resolvedFileName !== exportingFile.fileName) { @@ -27,7 +27,7 @@ namespace ts.codefix { const importNode = importFromModuleSpecifier(moduleSpecifier); switch (importNode.kind) { case SyntaxKind.ImportEqualsDeclaration: - changes.replaceNode(importingFile, importNode, makeImport(importNode.name, /*namedImports*/ undefined, moduleSpecifier, preferences)); + changes.replaceNode(importingFile, importNode, makeImport(importNode.name, /*namedImports*/ undefined, moduleSpecifier, quotePreference)); break; case SyntaxKind.CallExpression: if (isRequireCall(importNode, /*checkArgumentIsStringLiteralLike*/ false)) { @@ -39,13 +39,13 @@ namespace ts.codefix { } /** @returns Whether we converted a `module.exports =` to a default export. */ - function convertFileToEs6Module(sourceFile: SourceFile, checker: TypeChecker, changes: textChanges.ChangeTracker, target: ScriptTarget, preferences: UserPreferences): ModuleExportsChanged { + function convertFileToEs6Module(sourceFile: SourceFile, checker: TypeChecker, changes: textChanges.ChangeTracker, target: ScriptTarget, quotePreference: QuotePreference): ModuleExportsChanged { const identifiers: Identifiers = { original: collectFreeIdentifiers(sourceFile), additional: createMap() }; const exports = collectExportRenames(sourceFile, checker, identifiers); convertExportsAccesses(sourceFile, exports, changes); let moduleExportsChangedToDefault = false; for (const statement of sourceFile.statements) { - const moduleExportsChanged = convertStatement(sourceFile, statement, checker, changes, identifiers, target, exports, preferences); + const moduleExportsChanged = convertStatement(sourceFile, statement, checker, changes, identifiers, target, exports, quotePreference); moduleExportsChangedToDefault = moduleExportsChangedToDefault || moduleExportsChanged; } return moduleExportsChangedToDefault; @@ -98,10 +98,10 @@ namespace ts.codefix { /** Whether `module.exports =` was changed to `export default` */ type ModuleExportsChanged = boolean; - function convertStatement(sourceFile: SourceFile, statement: Statement, checker: TypeChecker, changes: textChanges.ChangeTracker, identifiers: Identifiers, target: ScriptTarget, exports: ExportRenames, preferences: UserPreferences): ModuleExportsChanged { + function convertStatement(sourceFile: SourceFile, statement: Statement, checker: TypeChecker, changes: textChanges.ChangeTracker, identifiers: Identifiers, target: ScriptTarget, exports: ExportRenames, quotePreference: QuotePreference): ModuleExportsChanged { switch (statement.kind) { case SyntaxKind.VariableStatement: - convertVariableStatement(sourceFile, statement as VariableStatement, changes, checker, identifiers, target, preferences); + convertVariableStatement(sourceFile, statement as VariableStatement, changes, checker, identifiers, target, quotePreference); return false; case SyntaxKind.ExpressionStatement: { const { expression } = statement as ExpressionStatement; @@ -109,7 +109,7 @@ namespace ts.codefix { case SyntaxKind.CallExpression: { if (isRequireCall(expression, /*checkArgumentIsStringLiteralLike*/ true)) { // For side-effecting require() call, just make a side-effecting import. - changes.replaceNode(sourceFile, statement, makeImport(/*name*/ undefined, /*namedImports*/ undefined, expression.arguments[0], preferences)); + changes.replaceNode(sourceFile, statement, makeImport(/*name*/ undefined, /*namedImports*/ undefined, expression.arguments[0], quotePreference)); } return false; } @@ -125,7 +125,15 @@ namespace ts.codefix { } } - function convertVariableStatement(sourceFile: SourceFile, statement: VariableStatement, changes: textChanges.ChangeTracker, checker: TypeChecker, identifiers: Identifiers, target: ScriptTarget, preferences: UserPreferences): void { + function convertVariableStatement( + sourceFile: SourceFile, + statement: VariableStatement, + changes: textChanges.ChangeTracker, + checker: TypeChecker, + identifiers: Identifiers, + target: ScriptTarget, + quotePreference: QuotePreference, + ): void { const { declarationList } = statement; let foundImport = false; const newNodes = flatMap(declarationList.declarations, decl => { @@ -138,11 +146,11 @@ namespace ts.codefix { } else if (isRequireCall(initializer, /*checkArgumentIsStringLiteralLike*/ true)) { foundImport = true; - return convertSingleImport(sourceFile, name, initializer.arguments[0], changes, checker, identifiers, target, preferences); + return convertSingleImport(sourceFile, name, initializer.arguments[0], changes, checker, identifiers, target, quotePreference); } else if (isPropertyAccessExpression(initializer) && isRequireCall(initializer.expression, /*checkArgumentIsStringLiteralLike*/ true)) { foundImport = true; - return convertPropertyAccessImport(name, initializer.name.text, initializer.expression.arguments[0], identifiers, preferences); + return convertPropertyAccessImport(name, initializer.name.text, initializer.expression.arguments[0], identifiers, quotePreference); } } // Move it out to its own variable statement. (This will not be used if `!foundImport`) @@ -155,20 +163,20 @@ namespace ts.codefix { } /** Converts `const name = require("moduleSpecifier").propertyName` */ - function convertPropertyAccessImport(name: BindingName, propertyName: string, moduleSpecifier: StringLiteralLike, identifiers: Identifiers, preferences: UserPreferences): ReadonlyArray { + function convertPropertyAccessImport(name: BindingName, propertyName: string, moduleSpecifier: StringLiteralLike, identifiers: Identifiers, quotePreference: QuotePreference): ReadonlyArray { switch (name.kind) { case SyntaxKind.ObjectBindingPattern: case SyntaxKind.ArrayBindingPattern: { // `const [a, b] = require("c").d` --> `import { d } from "c"; const [a, b] = d;` const tmp = makeUniqueName(propertyName, identifiers); return [ - makeSingleImport(tmp, propertyName, moduleSpecifier, preferences), + makeSingleImport(tmp, propertyName, moduleSpecifier, quotePreference), makeConst(/*modifiers*/ undefined, name, createIdentifier(tmp)), ]; } case SyntaxKind.Identifier: // `const a = require("b").c` --> `import { c as a } from "./b"; - return [makeSingleImport(name.text, propertyName, moduleSpecifier, preferences)]; + return [makeSingleImport(name.text, propertyName, moduleSpecifier, quotePreference)]; default: return Debug.assertNever(name); } @@ -340,7 +348,7 @@ namespace ts.codefix { checker: TypeChecker, identifiers: Identifiers, target: ScriptTarget, - preferences: UserPreferences, + quotePreference: QuotePreference, ): ReadonlyArray { switch (name.kind) { case SyntaxKind.ObjectBindingPattern: { @@ -349,7 +357,7 @@ namespace ts.codefix { ? undefined : makeImportSpecifier(e.propertyName && (e.propertyName as Identifier).text, e.name.text)); // tslint:disable-line no-unnecessary-type-assertion (TODO: GH#18217) if (importSpecifiers) { - return [makeImport(/*name*/ undefined, importSpecifiers, moduleSpecifier, preferences)]; + return [makeImport(/*name*/ undefined, importSpecifiers, moduleSpecifier, quotePreference)]; } } // falls through -- object destructuring has an interesting pattern and must be a variable declaration @@ -360,12 +368,12 @@ namespace ts.codefix { */ const tmp = makeUniqueName(moduleSpecifierToValidIdentifier(moduleSpecifier.text, target), identifiers); return [ - makeImport(createIdentifier(tmp), /*namedImports*/ undefined, moduleSpecifier, preferences), + makeImport(createIdentifier(tmp), /*namedImports*/ undefined, moduleSpecifier, quotePreference), makeConst(/*modifiers*/ undefined, getSynthesizedDeepClone(name), createIdentifier(tmp)), ]; } case SyntaxKind.Identifier: - return convertSingleIdentifierImport(file, name, moduleSpecifier, changes, checker, identifiers, preferences); + return convertSingleIdentifierImport(file, name, moduleSpecifier, changes, checker, identifiers, quotePreference); default: return Debug.assertNever(name); } @@ -375,7 +383,7 @@ namespace ts.codefix { * Convert `import x = require("x").` * Also converts uses like `x.y()` to `y()` and uses a named import. */ - function convertSingleIdentifierImport(file: SourceFile, name: Identifier, moduleSpecifier: StringLiteralLike, changes: textChanges.ChangeTracker, checker: TypeChecker, identifiers: Identifiers, preferences: UserPreferences): ReadonlyArray { + function convertSingleIdentifierImport(file: SourceFile, name: Identifier, moduleSpecifier: StringLiteralLike, changes: textChanges.ChangeTracker, checker: TypeChecker, identifiers: Identifiers, quotePreference: QuotePreference): ReadonlyArray { const nameSymbol = checker.getSymbolAtLocation(name); // Maps from module property name to name actually used. (The same if there isn't shadowing.) const namedBindingsNames = createMap(); @@ -410,7 +418,7 @@ namespace ts.codefix { // If it was unused, ensure that we at least import *something*. needDefaultImport = true; } - return [makeImport(needDefaultImport ? getSynthesizedDeepClone(name) : undefined, namedBindings, moduleSpecifier, preferences)]; + return [makeImport(needDefaultImport ? getSynthesizedDeepClone(name) : undefined, namedBindings, moduleSpecifier, quotePreference)]; } // Identifiers helpers @@ -488,10 +496,10 @@ namespace ts.codefix { getSynthesizedDeepClones(cls.members)); } - function makeSingleImport(localName: string, propertyName: string, moduleSpecifier: StringLiteralLike, preferences: UserPreferences): ImportDeclaration { + function makeSingleImport(localName: string, propertyName: string, moduleSpecifier: StringLiteralLike, quotePreference: QuotePreference): ImportDeclaration { return propertyName === "default" - ? makeImport(createIdentifier(localName), /*namedImports*/ undefined, moduleSpecifier, preferences) - : makeImport(/*name*/ undefined, [makeImportSpecifier(propertyName, localName)], moduleSpecifier, preferences); + ? makeImport(createIdentifier(localName), /*namedImports*/ undefined, moduleSpecifier, quotePreference) + : makeImport(/*name*/ undefined, [makeImportSpecifier(propertyName, localName)], moduleSpecifier, quotePreference); } function makeImportSpecifier(propertyName: string | undefined, name: string): ImportSpecifier { diff --git a/src/services/codefixes/fixInvalidImportSyntax.ts b/src/services/codefixes/fixInvalidImportSyntax.ts index 728a8b5663c..cfdc19e257d 100644 --- a/src/services/codefixes/fixInvalidImportSyntax.ts +++ b/src/services/codefixes/fixInvalidImportSyntax.ts @@ -28,7 +28,7 @@ namespace ts.codefix { const variations: CodeFixAction[] = []; // import Bluebird from "bluebird"; - variations.push(createAction(context, sourceFile, node, makeImport(namespace.name, /*namedImports*/ undefined, node.moduleSpecifier, context.preferences))); + variations.push(createAction(context, sourceFile, node, makeImport(namespace.name, /*namedImports*/ undefined, node.moduleSpecifier, getQuotePreference(sourceFile, context.preferences)))); if (getEmitModuleKind(opts) === ModuleKind.CommonJS) { // import Bluebird = require("bluebird"); diff --git a/src/services/codefixes/importFixes.ts b/src/services/codefixes/importFixes.ts index ecd2393418b..66402068fa4 100644 --- a/src/services/codefixes/importFixes.ts +++ b/src/services/codefixes/importFixes.ts @@ -197,7 +197,7 @@ namespace ts.codefix { const lastImportDeclaration = findLast(sourceFile.statements, isAnyImportSyntax); const moduleSpecifierWithoutQuotes = stripQuotes(moduleSpecifier); - const quotedModuleSpecifier = createLiteral(moduleSpecifierWithoutQuotes, shouldUseSingleQuote(sourceFile, preferences)); + const quotedModuleSpecifier = makeStringLiteral(moduleSpecifierWithoutQuotes, getQuotePreference(sourceFile, preferences)); const importDecl = importKind !== ImportKind.Equals ? createImportDeclaration( /*decorators*/ undefined, @@ -225,16 +225,6 @@ namespace ts.codefix { return createCodeAction(Diagnostics.Import_0_from_module_1, [symbolName, moduleSpecifierWithoutQuotes], changes); } - function shouldUseSingleQuote(sourceFile: SourceFile, preferences: UserPreferences): boolean { - if (preferences.quotePreference) { - return preferences.quotePreference === "single"; - } - else { - const firstModuleSpecifier = firstOrUndefined(sourceFile.imports); - return !!firstModuleSpecifier && !isStringDoubleQuoted(firstModuleSpecifier, sourceFile); - } - } - function createImportClauseOfKind(kind: ImportKind.Default | ImportKind.Named | ImportKind.Namespace, symbolName: string) { const id = createIdentifier(symbolName); switch (kind) { diff --git a/src/services/codefixes/useDefaultImport.ts b/src/services/codefixes/useDefaultImport.ts index 34e3d40e514..36aa0bb1697 100644 --- a/src/services/codefixes/useDefaultImport.ts +++ b/src/services/codefixes/useDefaultImport.ts @@ -37,6 +37,6 @@ namespace ts.codefix { } function doChange(changes: textChanges.ChangeTracker, sourceFile: SourceFile, info: Info, preferences: UserPreferences): void { - changes.replaceNode(sourceFile, info.importNode, makeImport(info.name, /*namedImports*/ undefined, info.moduleSpecifier, preferences)); + changes.replaceNode(sourceFile, info.importNode, makeImport(info.name, /*namedImports*/ undefined, info.moduleSpecifier, getQuotePreference(sourceFile, preferences))); } } diff --git a/src/services/refactors/moveToNewFile.ts b/src/services/refactors/moveToNewFile.ts index c1f1ec1fd5e..0c6aaa61b65 100644 --- a/src/services/refactors/moveToNewFile.ts +++ b/src/services/refactors/moveToNewFile.ts @@ -118,7 +118,8 @@ namespace ts.refactor { } const useEs6ModuleSyntax = !!oldFile.externalModuleIndicator; - const importsFromNewFile = createOldFileImportsFromNewFile(usage.oldFileImportsFromNewFile, newModuleName, useEs6ModuleSyntax, preferences); + const quotePreference = getQuotePreference(oldFile, preferences); + const importsFromNewFile = createOldFileImportsFromNewFile(usage.oldFileImportsFromNewFile, newModuleName, useEs6ModuleSyntax, quotePreference); if (importsFromNewFile) { changes.insertNodeBefore(oldFile, oldFile.statements[0], importsFromNewFile, /*blankLineBetween*/ true); } @@ -129,7 +130,7 @@ namespace ts.refactor { updateImportsInOtherFiles(changes, program, oldFile, usage.movedSymbols, newModuleName); return [ - ...getNewFileImportsAndAddExportInOldFile(oldFile, usage.oldImportsNeededByNewFile, usage.newFileImportsFromOldFile, changes, checker, useEs6ModuleSyntax, preferences), + ...getNewFileImportsAndAddExportInOldFile(oldFile, usage.oldImportsNeededByNewFile, usage.newFileImportsFromOldFile, changes, checker, useEs6ModuleSyntax, quotePreference), ...addExports(oldFile, toMove.all, usage.oldFileImportsFromNewFile, useEs6ModuleSyntax), ]; } @@ -268,7 +269,7 @@ namespace ts.refactor { | ImportEqualsDeclaration | VariableStatement; - function createOldFileImportsFromNewFile(newFileNeedExport: ReadonlySymbolSet, newFileNameWithExtension: string, useEs6Imports: boolean, preferences: UserPreferences): Statement | undefined { + function createOldFileImportsFromNewFile(newFileNeedExport: ReadonlySymbolSet, newFileNameWithExtension: string, useEs6Imports: boolean, quotePreference: QuotePreference): Statement | undefined { let defaultImport: Identifier | undefined; const imports: string[] = []; newFileNeedExport.forEach(symbol => { @@ -279,14 +280,14 @@ namespace ts.refactor { imports.push(symbol.name); } }); - return makeImportOrRequire(defaultImport, imports, newFileNameWithExtension, useEs6Imports, preferences); + return makeImportOrRequire(defaultImport, imports, newFileNameWithExtension, useEs6Imports, quotePreference); } - function makeImportOrRequire(defaultImport: Identifier | undefined, imports: ReadonlyArray, path: string, useEs6Imports: boolean, preferences: UserPreferences): Statement | undefined { + function makeImportOrRequire(defaultImport: Identifier | undefined, imports: ReadonlyArray, path: string, useEs6Imports: boolean, quotePreference: QuotePreference): Statement | undefined { path = ensurePathIsNonModuleName(path); if (useEs6Imports) { const specifiers = imports.map(i => createImportSpecifier(/*propertyName*/ undefined, createIdentifier(i))); - return makeImportIfNecessary(defaultImport, specifiers, path, preferences); + return makeImportIfNecessary(defaultImport, specifiers, path, quotePreference); } else { Debug.assert(!defaultImport); // If there's a default export, it should have been an es6 module. @@ -392,7 +393,7 @@ namespace ts.refactor { changes: textChanges.ChangeTracker, checker: TypeChecker, useEs6ModuleSyntax: boolean, - preferences: UserPreferences, + quotePreference: QuotePreference, ): ReadonlyArray { const copiedOldImports: SupportedImportStatement[] = []; for (const oldStatement of oldFile.statements) { @@ -424,7 +425,7 @@ namespace ts.refactor { } }); - append(copiedOldImports, makeImportOrRequire(oldFileDefault, oldFileNamedImports, removeFileExtension(getBaseFileName(oldFile.fileName)), useEs6ModuleSyntax, preferences)); + append(copiedOldImports, makeImportOrRequire(oldFileDefault, oldFileNamedImports, removeFileExtension(getBaseFileName(oldFile.fileName)), useEs6ModuleSyntax, quotePreference)); return copiedOldImports; } diff --git a/src/services/utilities.ts b/src/services/utilities.ts index 786de97f20e..efc1488d6a4 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -1257,18 +1257,34 @@ namespace ts { return createGetCanonicalFileName(hostUsesCaseSensitiveFileNames(host)); } - export function makeImportIfNecessary(defaultImport: Identifier | undefined, namedImports: ReadonlyArray | undefined, moduleSpecifier: string, preferences: UserPreferences): ImportDeclaration | undefined { - return defaultImport || namedImports && namedImports.length ? makeImport(defaultImport, namedImports, moduleSpecifier, preferences) : undefined; + export function makeImportIfNecessary(defaultImport: Identifier | undefined, namedImports: ReadonlyArray | undefined, moduleSpecifier: string, quotePreference: QuotePreference): ImportDeclaration | undefined { + return defaultImport || namedImports && namedImports.length ? makeImport(defaultImport, namedImports, moduleSpecifier, quotePreference) : undefined; } - export function makeImport(defaultImport: Identifier | undefined, namedImports: ReadonlyArray | undefined, moduleSpecifier: string | Expression, preferences: UserPreferences): ImportDeclaration { + export function makeImport(defaultImport: Identifier | undefined, namedImports: ReadonlyArray | undefined, moduleSpecifier: string | Expression, quotePreference: QuotePreference): ImportDeclaration { return createImportDeclaration( /*decorators*/ undefined, /*modifiers*/ undefined, defaultImport || namedImports ? createImportClause(defaultImport, namedImports && namedImports.length ? createNamedImports(namedImports) : undefined) : undefined, - typeof moduleSpecifier === "string" ? createLiteral(moduleSpecifier, preferences.quotePreference === "single") : moduleSpecifier); + typeof moduleSpecifier === "string" ? makeStringLiteral(moduleSpecifier, quotePreference) : moduleSpecifier); + } + + export function makeStringLiteral(text: string, quotePreference: QuotePreference): StringLiteral { + return createLiteral(text, quotePreference === QuotePreference.Single); + } + + export const enum QuotePreference { Single, Double } + + export function getQuotePreference(sourceFile: SourceFile, preferences: UserPreferences): QuotePreference { + if (preferences.quotePreference) { + return preferences.quotePreference === "single" ? QuotePreference.Single : QuotePreference.Double; + } + else { + const firstModuleSpecifier = firstOrUndefined(sourceFile.imports); + return !!firstModuleSpecifier && !isStringDoubleQuoted(firstModuleSpecifier, sourceFile) ? QuotePreference.Single : QuotePreference.Double; + } } export function symbolNameNoDefault(symbol: Symbol): string | undefined { diff --git a/tests/cases/fourslash/moveToNewFile_inferQuoteStyle.ts b/tests/cases/fourslash/moveToNewFile_inferQuoteStyle.ts new file mode 100644 index 00000000000..1e7b4347a87 --- /dev/null +++ b/tests/cases/fourslash/moveToNewFile_inferQuoteStyle.ts @@ -0,0 +1,21 @@ +/// + +// @Filename: /a.ts +////import 'unrelated'; +//// +////[|const x = 0;|] +////x; + +verify.moveToNewFile({ + newFileContents: { + "/a.ts": +`import { x } from './x'; + +import 'unrelated'; + +x;`, + + "/x.ts": +`export const x = 0;`, + }, +}); From 87217018b83fd0ccfc3079940b9777f1d64beb35 Mon Sep 17 00:00:00 2001 From: Andy Date: Thu, 7 Jun 2018 12:43:57 -0700 Subject: [PATCH 53/81] Add 'nameSpan' property to NavigationTree (#24698) --- src/harness/fourslash.ts | 1 + src/server/client.ts | 1 + src/server/protocol.ts | 1 + src/server/session.ts | 1 + src/services/navigationBar.ts | 14 ++++++++------ src/services/types.ts | 1 + tests/baselines/reference/api/tsserverlibrary.d.ts | 2 ++ tests/baselines/reference/api/typescript.d.ts | 1 + .../fourslash/navigationBarInitializerSpans.ts | 8 +++++--- 9 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/harness/fourslash.ts b/src/harness/fourslash.ts index 2ace3df3b42..532ce5e0488 100644 --- a/src/harness/fourslash.ts +++ b/src/harness/fourslash.ts @@ -2869,6 +2869,7 @@ Actual: ${stringify(fullActual)}`); function replacer(key: string, value: any) { switch (key) { case "spans": + case "nameSpan": return options && options.checkSpans ? value : undefined; case "start": case "length": diff --git a/src/server/client.ts b/src/server/client.ts index e797cadb8ca..4cd29266027 100644 --- a/src/server/client.ts +++ b/src/server/client.ts @@ -451,6 +451,7 @@ namespace ts.server { kind: tree.kind, kindModifiers: tree.kindModifiers, spans: tree.spans.map(span => this.decodeSpan(span, fileName, lineMap)), + nameSpan: tree.nameSpan && this.decodeSpan(tree.nameSpan, fileName, lineMap), childItems: map(tree.childItems, item => this.decodeNavigationTree(item, fileName, lineMap)) }; } diff --git a/src/server/protocol.ts b/src/server/protocol.ts index be1a6247fb0..3082dbae6aa 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -2555,6 +2555,7 @@ namespace ts.server.protocol { kind: ScriptElementKind; kindModifiers: string; spans: TextSpan[]; + nameSpan: TextSpan | undefined; childItems?: NavigationTree[]; } diff --git a/src/server/session.ts b/src/server/session.ts index 3a368587d4b..a2d8b5fcf87 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -1507,6 +1507,7 @@ namespace ts.server { kind: tree.kind, kindModifiers: tree.kindModifiers, spans: tree.spans.map(span => this.toLocationTextSpan(span, scriptInfo)), + nameSpan: tree.nameSpan && this.toLocationTextSpan(tree.nameSpan, scriptInfo), childItems: map(tree.childItems, item => this.toLocationNavigationTree(item, scriptInfo)) }; } diff --git a/src/services/navigationBar.ts b/src/services/navigationBar.ts index 0ce64d786e1..f381be9b7d1 100644 --- a/src/services/navigationBar.ts +++ b/src/services/navigationBar.ts @@ -36,6 +36,7 @@ namespace ts.NavigationBar { */ interface NavigationBarNode { node: Node; + name: DeclarationName | undefined; additionalNodes: Node[] | undefined; parent: NavigationBarNode | undefined; // Present for all but root node children: NavigationBarNode[] | undefined; @@ -91,7 +92,7 @@ namespace ts.NavigationBar { function rootNavigationBarNode(sourceFile: SourceFile): NavigationBarNode { Debug.assert(!parentsStack.length); - const root: NavigationBarNode = { node: sourceFile, additionalNodes: undefined, parent: undefined, children: undefined, indent: 0 }; + const root: NavigationBarNode = { node: sourceFile, name: undefined, additionalNodes: undefined, parent: undefined, children: undefined, indent: 0 }; parent = root; for (const statement of sourceFile.statements) { addChildrenRecursively(statement); @@ -108,6 +109,7 @@ namespace ts.NavigationBar { function emptyNavigationBarNode(node: Node): NavigationBarNode { return { node, + name: isDeclaration(node) || isExpression(node) ? getNameOfDeclaration(node) : undefined, additionalNodes: undefined, parent, children: undefined, @@ -420,12 +422,11 @@ namespace ts.NavigationBar { } } - function getItemName(node: Node): string { + function getItemName(node: Node, name: Node | undefined): string { if (node.kind === SyntaxKind.ModuleDeclaration) { return getModuleName(node); } - const name = getNameOfDeclaration(node); if (name) { const text = nodeText(name); if (text.length > 0) { @@ -534,17 +535,18 @@ namespace ts.NavigationBar { function convertToTree(n: NavigationBarNode): NavigationTree { return { - text: getItemName(n.node), + text: getItemName(n.node, n.name), kind: getNodeKind(n.node), kindModifiers: getModifiers(n.node), spans: getSpans(n), + nameSpan: n.name && getNodeSpan(n.name), childItems: map(n.children, convertToTree) }; } function convertToTopLevelItem(n: NavigationBarNode): NavigationBarItem { return { - text: getItemName(n.node), + text: getItemName(n.node, n.name), kind: getNodeKind(n.node), kindModifiers: getModifiers(n.node), spans: getSpans(n), @@ -556,7 +558,7 @@ namespace ts.NavigationBar { function convertToChildItem(n: NavigationBarNode): NavigationBarItem { return { - text: getItemName(n.node), + text: getItemName(n.node, n.name), kind: getNodeKind(n.node), kindModifiers: getNodeModifiers(n.node), spans: getSpans(n), diff --git a/src/services/types.ts b/src/services/types.ts index 4ac439b56cc..9ae51a4ea61 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -425,6 +425,7 @@ namespace ts { * There will be more than one if this is the result of merging. */ spans: TextSpan[]; + nameSpan: TextSpan | undefined; /** Present if non-empty */ childItems?: NavigationTree[]; } diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 45e32195e79..ef1cfa9cd85 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -4657,6 +4657,7 @@ declare namespace ts { * There will be more than one if this is the result of merging. */ spans: TextSpan[]; + nameSpan: TextSpan | undefined; /** Present if non-empty */ childItems?: NavigationTree[]; } @@ -7532,6 +7533,7 @@ declare namespace ts.server.protocol { kind: ScriptElementKind; kindModifiers: string; spans: TextSpan[]; + nameSpan: TextSpan | undefined; childItems?: NavigationTree[]; } type TelemetryEventName = "telemetry"; diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index ac272a91094..c055400e628 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -4657,6 +4657,7 @@ declare namespace ts { * There will be more than one if this is the result of merging. */ spans: TextSpan[]; + nameSpan: TextSpan | undefined; /** Present if non-empty */ childItems?: NavigationTree[]; } diff --git a/tests/cases/fourslash/navigationBarInitializerSpans.ts b/tests/cases/fourslash/navigationBarInitializerSpans.ts index 67752c85577..7b044db9c4c 100644 --- a/tests/cases/fourslash/navigationBarInitializerSpans.ts +++ b/tests/cases/fourslash/navigationBarInitializerSpans.ts @@ -1,9 +1,9 @@ /// -////const [|x = () => 0|]; -////const f = [|function f() {}|]; +////const [|[|x|] = () => 0|]; +////const f = [|function [|f|]() {}|]; -const [s0, s1] = test.spans(); +const [s0, s0Name, s1, s1Name] = test.spans(); const sGlobal = { start: 0, length: 45 }; verify.navigationTree({ @@ -15,11 +15,13 @@ verify.navigationTree({ text: "f", kind: "function", spans: [s1], + nameSpan: s1Name, }, { text: "x", kind: "const", spans: [s0], + nameSpan: s0Name, }, ] }, { checkSpans: true }); From 2b4569c04ff2e973469c1b3be882698834a2c9e5 Mon Sep 17 00:00:00 2001 From: Ron Buckton Date: Thu, 7 Jun 2018 13:34:16 -0700 Subject: [PATCH 54/81] Fix prologue order in async function --- src/compiler/transformers/es2015.ts | 21 +++--- .../unittests/evaluation/asyncArrow.ts | 18 +++++ .../reference/asyncArrowFunction11_es5.js | 71 +++++++++++++++++++ .../asyncArrowFunction11_es5.symbols | 22 ++++++ .../reference/asyncArrowFunction11_es5.types | 27 +++++++ .../asyncArrowFunction11_es5.ts | 10 +++ 6 files changed, 156 insertions(+), 13 deletions(-) create mode 100644 src/harness/unittests/evaluation/asyncArrow.ts create mode 100644 tests/baselines/reference/asyncArrowFunction11_es5.js create mode 100644 tests/baselines/reference/asyncArrowFunction11_es5.symbols create mode 100644 tests/baselines/reference/asyncArrowFunction11_es5.types create mode 100644 tests/cases/conformance/async/es5/asyncArrowFunction/asyncArrowFunction11_es5.ts diff --git a/src/compiler/transformers/es2015.ts b/src/compiler/transformers/es2015.ts index b05cabcd85a..e757c5d09d3 100644 --- a/src/compiler/transformers/es2015.ts +++ b/src/compiler/transformers/es2015.ts @@ -1832,6 +1832,7 @@ namespace ts { let statementsLocation: TextRange; let closeBraceLocation: TextRange | undefined; + const leadingStatements: Statement[] = []; const statements: Statement[] = []; const body = node.body!; let statementOffset: number | undefined; @@ -1840,21 +1841,16 @@ namespace ts { if (isBlock(body)) { // ensureUseStrict is false because no new prologue-directive should be added. // addStandardPrologue will put already-existing directives at the beginning of the target statement-array - statementOffset = addStandardPrologue(statements, body.statements, /*ensureUseStrict*/ false); + statementOffset = addStandardPrologue(leadingStatements, body.statements, /*ensureUseStrict*/ false); } - addCaptureThisForNodeIfNeeded(statements, node); - addDefaultValueAssignmentsIfNeeded(statements, node); - addRestParameterIfNeeded(statements, node, /*inConstructorWithSynthesizedSuper*/ false); - - // If we added any generated statements, this must be a multi-line block. - if (!multiLine && statements.length > 0) { - multiLine = true; - } + addCaptureThisForNodeIfNeeded(leadingStatements, node); + addDefaultValueAssignmentsIfNeeded(leadingStatements, node); + addRestParameterIfNeeded(leadingStatements, node, /*inConstructorWithSynthesizedSuper*/ false); if (isBlock(body)) { // addCustomPrologue puts already-existing directives at the beginning of the target statement-array - statementOffset = addCustomPrologue(statements, body.statements, statementOffset, visitor); + statementOffset = addCustomPrologue(leadingStatements, body.statements, statementOffset, visitor); statementsLocation = body.statements; addRange(statements, visitNodes(body.statements, visitor, isStatement, statementOffset)); @@ -1897,15 +1893,14 @@ namespace ts { const lexicalEnvironment = context.endLexicalEnvironment(); prependStatements(statements, lexicalEnvironment); - prependCaptureNewTargetIfNeeded(statements, node, /*copyOnWrite*/ false); // If we added any final generated statements, this must be a multi-line block - if (!multiLine && lexicalEnvironment && lexicalEnvironment.length) { + if (some(leadingStatements) || some(lexicalEnvironment)) { multiLine = true; } - const block = createBlock(setTextRange(createNodeArray(statements), statementsLocation), multiLine); + const block = createBlock(setTextRange(createNodeArray([...leadingStatements, ...statements]), statementsLocation), multiLine); setTextRange(block, node.body); if (!multiLine && singleLine) { setEmitFlags(block, EmitFlags.SingleLine); diff --git a/src/harness/unittests/evaluation/asyncArrow.ts b/src/harness/unittests/evaluation/asyncArrow.ts new file mode 100644 index 00000000000..994fe8a84be --- /dev/null +++ b/src/harness/unittests/evaluation/asyncArrow.ts @@ -0,0 +1,18 @@ +describe("asyncArrowEvaluation", () => { + // https://github.com/Microsoft/TypeScript/issues/24722 + it("this capture (es5)", async () => { + const result = evaluator.evaluateTypeScript(` + export class A { + b = async (...args: any[]) => { + await Promise.resolve(); + output.push({ ["a"]: () => this }); // computed property name after 'await' triggers case + }; + } + export const output: any[] = []; + export async function main() { + await new A().b(); + }`); + await result.main(); + assert.instanceOf(result.output[0].a(), result.A); + }); +}); \ No newline at end of file diff --git a/tests/baselines/reference/asyncArrowFunction11_es5.js b/tests/baselines/reference/asyncArrowFunction11_es5.js new file mode 100644 index 00000000000..013941c96ca --- /dev/null +++ b/tests/baselines/reference/asyncArrowFunction11_es5.js @@ -0,0 +1,71 @@ +//// [asyncArrowFunction11_es5.ts] +// https://github.com/Microsoft/TypeScript/issues/24722 +class A { + b = async (...args: any[]) => { + await Promise.resolve(); + const obj = { ["a"]: () => this }; // computed property name after `await` triggers case + }; +} + +//// [asyncArrowFunction11_es5.js] +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; + return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (_) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +// https://github.com/Microsoft/TypeScript/issues/24722 +var A = /** @class */ (function () { + function A() { + var _this = this; + this.b = function () { + var args = []; + for (var _i = 0; _i < arguments.length; _i++) { + args[_i] = arguments[_i]; + } + return __awaiter(_this, void 0, void 0, function () { + var _a, obj; + var _this = this; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: return [4 /*yield*/, Promise.resolve()]; + case 1: + _b.sent(); + obj = (_a = {}, _a["a"] = function () { return _this; }, _a); + return [2 /*return*/]; + } + }); + }); + }; + } + return A; +}()); diff --git a/tests/baselines/reference/asyncArrowFunction11_es5.symbols b/tests/baselines/reference/asyncArrowFunction11_es5.symbols new file mode 100644 index 00000000000..aff484d5927 --- /dev/null +++ b/tests/baselines/reference/asyncArrowFunction11_es5.symbols @@ -0,0 +1,22 @@ +=== tests/cases/conformance/async/es5/asyncArrowFunction/asyncArrowFunction11_es5.ts === +// https://github.com/Microsoft/TypeScript/issues/24722 +class A { +>A : Symbol(A, Decl(asyncArrowFunction11_es5.ts, 0, 0)) + + b = async (...args: any[]) => { +>b : Symbol(A.b, Decl(asyncArrowFunction11_es5.ts, 1, 9)) +>args : Symbol(args, Decl(asyncArrowFunction11_es5.ts, 2, 15)) + + await Promise.resolve(); +>Promise.resolve : Symbol(PromiseConstructor.resolve, Decl(lib.es2015.promise.d.ts, --, --), Decl(lib.es2015.promise.d.ts, --, --)) +>Promise : Symbol(Promise, Decl(lib.es5.d.ts, --, --), Decl(lib.es2015.iterable.d.ts, --, --), Decl(lib.es2015.promise.d.ts, --, --), Decl(lib.es2015.symbol.wellknown.d.ts, --, --), Decl(lib.es2018.promise.d.ts, --, --)) +>resolve : Symbol(PromiseConstructor.resolve, Decl(lib.es2015.promise.d.ts, --, --), Decl(lib.es2015.promise.d.ts, --, --)) + + const obj = { ["a"]: () => this }; // computed property name after `await` triggers case +>obj : Symbol(obj, Decl(asyncArrowFunction11_es5.ts, 4, 13)) +>["a"] : Symbol(["a"], Decl(asyncArrowFunction11_es5.ts, 4, 21)) +>"a" : Symbol(["a"], Decl(asyncArrowFunction11_es5.ts, 4, 21)) +>this : Symbol(A, Decl(asyncArrowFunction11_es5.ts, 0, 0)) + + }; +} diff --git a/tests/baselines/reference/asyncArrowFunction11_es5.types b/tests/baselines/reference/asyncArrowFunction11_es5.types new file mode 100644 index 00000000000..70eea5e2f2a --- /dev/null +++ b/tests/baselines/reference/asyncArrowFunction11_es5.types @@ -0,0 +1,27 @@ +=== tests/cases/conformance/async/es5/asyncArrowFunction/asyncArrowFunction11_es5.ts === +// https://github.com/Microsoft/TypeScript/issues/24722 +class A { +>A : A + + b = async (...args: any[]) => { +>b : (...args: any[]) => Promise +>async (...args: any[]) => { await Promise.resolve(); const obj = { ["a"]: () => this }; // computed property name after `await` triggers case } : (...args: any[]) => Promise +>args : any[] + + await Promise.resolve(); +>await Promise.resolve() : void +>Promise.resolve() : Promise +>Promise.resolve : { (value: T | PromiseLike): Promise; (): Promise; } +>Promise : PromiseConstructor +>resolve : { (value: T | PromiseLike): Promise; (): Promise; } + + const obj = { ["a"]: () => this }; // computed property name after `await` triggers case +>obj : { ["a"]: () => this; } +>{ ["a"]: () => this } : { ["a"]: () => this; } +>["a"] : () => this +>"a" : "a" +>() => this : () => this +>this : this + + }; +} diff --git a/tests/cases/conformance/async/es5/asyncArrowFunction/asyncArrowFunction11_es5.ts b/tests/cases/conformance/async/es5/asyncArrowFunction/asyncArrowFunction11_es5.ts new file mode 100644 index 00000000000..98630114be9 --- /dev/null +++ b/tests/cases/conformance/async/es5/asyncArrowFunction/asyncArrowFunction11_es5.ts @@ -0,0 +1,10 @@ +// @target: es5 +// @lib: esnext, dom +// @downlevelIteration: true +// https://github.com/Microsoft/TypeScript/issues/24722 +class A { + b = async (...args: any[]) => { + await Promise.resolve(); + const obj = { ["a"]: () => this }; // computed property name after `await` triggers case + }; +} \ No newline at end of file From 1c6ff9c0b6f20c0889fee96d16e3213d90117121 Mon Sep 17 00:00:00 2001 From: Mohamed Hegazy Date: Thu, 7 Jun 2018 13:47:20 -0700 Subject: [PATCH 55/81] Update header --- ThirdPartyNoticeText.txt | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/ThirdPartyNoticeText.txt b/ThirdPartyNoticeText.txt index baa89396c08..acda89ef37a 100644 --- a/ThirdPartyNoticeText.txt +++ b/ThirdPartyNoticeText.txt @@ -1,11 +1,7 @@ /*!----------------- TypeScript ThirdPartyNotices ------------------------------------------------------- -The TypeScript software is based on or incorporates material and code from the projects listed below (collectively "Third Party Code"). Microsoft is not the original author of the Third Party Code. The original copyright notice and the license, under which Microsoft received such Third Party Code, are set forth below. Such license and notices are provided for informational purposes only. MThe TypeScript software incorporates third party material from the projects listed below. The original copyright notice and the license under which Microsoft received such third party material are set forth below. Microsoft reserves all other rights not expressly granted, whether by implication, estoppel or otherwise. you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +The TypeScript software incorporates third party material from the projects listed below. The original copyright notice and the license under which Microsoft received such third party material are set forth below. Microsoft reserves all other rights not expressly granted, whether by implication, estoppel or otherwise. -THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT. - -See the Apache Version 2.0 License for specific language governing permissions and -limitations under the License. --------------------------------------------- Third Party Code Components -------------------------------------------- From 16e01174b70d1ad2977b799ed949a5046ae6124a Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 7 Jun 2018 13:36:24 -0700 Subject: [PATCH 56/81] Do not watch folders like "c:/users/username", "c:/users/username/folderAtRoot" Fixes Microsoft/vscode#51139 --- src/compiler/resolutionCache.ts | 37 +++++++++++++------ .../unittests/tsserverProjectSystem.ts | 22 ++++++++--- 2 files changed, 42 insertions(+), 17 deletions(-) diff --git a/src/compiler/resolutionCache.ts b/src/compiler/resolutionCache.ts index b4c428d5e5a..3f848cfbc29 100644 --- a/src/compiler/resolutionCache.ts +++ b/src/compiler/resolutionCache.ts @@ -349,8 +349,32 @@ namespace ts { return endsWith(dirPath, "/node_modules/@types"); } - function isDirectoryAtleastAtLevelFromFSRoot(dirPath: Path, minLevels: number) { - for (let searchIndex = getRootLength(dirPath); minLevels > 0; minLevels--) { + /** + * Filter out paths like + * "/", "/user", "/user/username", "/user/username/folderAtRoot", + * "c:/", "c:/users", "c:/users/username", "c:/users/username/folderAtRoot", "c:/folderAtRoot" + * @param dirPath + */ + function canWatchDirectory(dirPath: Path) { + const rootLength = getRootLength(dirPath); + if (dirPath.length === rootLength) { + // Ignore "/", "c:/" + return false; + } + + const nextDirectorySeparator = dirPath.indexOf(directorySeparator, rootLength); + if (nextDirectorySeparator === -1) { + // ignore "/user", "c:/users" or "c:/folderAtRoot" + return false; + } + + if (dirPath.charCodeAt(0) !== CharacterCodes.slash && + dirPath.substr(rootLength, nextDirectorySeparator).search(/users/i) === -1) { + // Paths like c:/folderAtRoot/subFolder are allowed + return true; + } + + for (let searchIndex = nextDirectorySeparator + 1, searchLevels = 2; searchLevels > 0; searchLevels--) { searchIndex = dirPath.indexOf(directorySeparator, searchIndex) + 1; if (searchIndex === 0) { // Folder isnt at expected minimun levels @@ -360,15 +384,6 @@ namespace ts { return true; } - function canWatchDirectory(dirPath: Path) { - return isDirectoryAtleastAtLevelFromFSRoot(dirPath, - // When root is "/" do not watch directories like: - // "/", "/user", "/user/username", "/user/username/folderAtRoot" - // When root is "c:/" do not watch directories like: - // "c:/", "c:/folderAtRoot" - dirPath.charCodeAt(0) === CharacterCodes.slash ? 3 : 1); - } - function filterFSRootDirectoriesToWatch(watchPath: DirectoryOfFailedLookupWatch, dirPath: Path): DirectoryOfFailedLookupWatch { if (!canWatchDirectory(dirPath)) { watchPath.ignore = true; diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index aa1f4143fc8..da5da0e50e4 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -7501,8 +7501,8 @@ namespace ts.projectSystem { }); describe("tsserverProjectSystem Watched recursive directories with windows style file system", () => { - function verifyWatchedDirectories(useProjectAtRoot: boolean) { - const root = useProjectAtRoot ? "c:/" : "c:/myfolder/allproject/"; + function verifyWatchedDirectories(rootedPath: string, useProjectAtRoot: boolean) { + const root = useProjectAtRoot ? rootedPath : `${rootedPath}myfolder/allproject/`; const configFile: File = { path: root + "project/tsconfig.json", content: "{}" @@ -7531,12 +7531,22 @@ namespace ts.projectSystem { ].concat(useProjectAtRoot ? [] : [root + nodeModulesAtTypes]), /*recursive*/ true); } - it("When project is in rootFolder", () => { - verifyWatchedDirectories(/*useProjectAtRoot*/ true); + function verifyRootedDirectoryWatch(rootedPath: string) { + it("When project is in rootFolder of style c:/", () => { + verifyWatchedDirectories(rootedPath, /*useProjectAtRoot*/ true); + }); + + it("When files at some folder other than root", () => { + verifyWatchedDirectories(rootedPath, /*useProjectAtRoot*/ false); + }); + } + + describe("for rootFolder of style c:/", () => { + verifyRootedDirectoryWatch("c:/"); }); - it("When files at some folder other than root", () => { - verifyWatchedDirectories(/*useProjectAtRoot*/ false); + describe("for rootFolder of style c:/users/username", () => { + verifyRootedDirectoryWatch("c:/users/username/"); }); }); From fde6f22408881bd3e0008938cbed2ddf5be9766d Mon Sep 17 00:00:00 2001 From: Andy Date: Thu, 7 Jun 2018 14:28:06 -0700 Subject: [PATCH 57/81] Fix bug: In newFileChanges, setParentNodes in new source file (#24765) --- src/services/textChanges.ts | 2 +- tests/cases/fourslash/moveToNewFile_jsx.ts | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 tests/cases/fourslash/moveToNewFile_jsx.ts diff --git a/src/services/textChanges.ts b/src/services/textChanges.ts index 02c29b0568d..c37969f19c2 100644 --- a/src/services/textChanges.ts +++ b/src/services/textChanges.ts @@ -735,7 +735,7 @@ namespace ts.textChanges { export function newFileChanges(oldFile: SourceFile, fileName: string, statements: ReadonlyArray, newLineCharacter: string, formatContext: formatting.FormatContext): FileTextChanges { // TODO: this emits the file, parses it back, then formats it that -- may be a less roundabout way to do this const nonFormattedText = statements.map(s => getNonformattedText(s, oldFile, newLineCharacter).text).join(newLineCharacter); - const sourceFile = createSourceFile(fileName, nonFormattedText, ScriptTarget.ESNext); + const sourceFile = createSourceFile(fileName, nonFormattedText, ScriptTarget.ESNext, /*setParentNodes*/ true); const changes = formatting.formatDocument(sourceFile, formatContext); const text = applyChanges(nonFormattedText, changes); return { fileName, textChanges: [createTextChange(createTextSpan(0, 0), text)], isNewFile: true }; diff --git a/tests/cases/fourslash/moveToNewFile_jsx.ts b/tests/cases/fourslash/moveToNewFile_jsx.ts new file mode 100644 index 00000000000..b969794af30 --- /dev/null +++ b/tests/cases/fourslash/moveToNewFile_jsx.ts @@ -0,0 +1,14 @@ +/// + +// @Filename: /a.tsx +////[|
a
;|] + +verify.moveToNewFile({ + newFileContents: { + "/a.tsx": +``, + + "/newFile.tsx": +`
a
;`, + } +}); From 399ae514067a9fc5d664650a2f5896071d32f08b Mon Sep 17 00:00:00 2001 From: Andy Date: Thu, 7 Jun 2018 14:30:19 -0700 Subject: [PATCH 58/81] Support 'tsconfig.json' when converting TextChanges to CodeEdits (#24667) * Support 'tsconfig.json' when converting TextChanges to CodeEdits * Create Project#getSourceFileOrConfigFile to use instead --- .../unittests/tsserverProjectSystem.ts | 65 ++++++++++++++++++- src/server/project.ts | 6 ++ src/server/session.ts | 53 +++++++++------ .../reference/api/tsserverlibrary.d.ts | 2 - 4 files changed, 102 insertions(+), 24 deletions(-) diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index aa1f4143fc8..a8592f04055 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -466,7 +466,7 @@ namespace ts.projectSystem { return newRequest; } - export function openFilesForSession(files: File[], session: server.Session) { + export function openFilesForSession(files: ReadonlyArray, session: server.Session) { for (const file of files) { const request = makeSessionRequest(CommandNames.Open, { file: file.path }); session.executeCommand(request); @@ -6192,6 +6192,69 @@ namespace ts.projectSystem { renameLocation: { line: 2, offset: 3 }, }); }); + + it("handles text changes in tsconfig.json", () => { + const aTs = { + path: "/a.ts", + content: "export const a = 0;", + }; + const tsconfig = { + path: "/tsconfig.json", + content: '{ "files": ["./a.ts"] }', + }; + + const session = createSession(createServerHost([aTs, tsconfig])); + openFilesForSession([aTs], session); + + const response1 = session.executeCommandSeq({ + command: server.protocol.CommandTypes.GetEditsForRefactor, + arguments: { + refactor: "Move to a new file", + action: "Move to a new file", + file: "/a.ts", + startLine: 1, + startOffset: 1, + endLine: 1, + endOffset: 20, + }, + }).response; + assert.deepEqual(response1, { + edits: [ + { + fileName: "/a.ts", + textChanges: [ + { + start: { line: 1, offset: 1 }, + end: { line: 1, offset: 20 }, + newText: "", + }, + ], + }, + { + fileName: "/tsconfig.json", + textChanges: [ + { + start: { line: 1, offset: 21 }, + end: { line: 1, offset: 21 }, + newText: ", \"./a.1.ts\"", + }, + ], + }, + { + fileName: "/a.1.ts", + textChanges: [ + { + start: { line: 0, offset: 0 }, + end: { line: 0, offset: 0 }, + newText: "export const a = 0;", + }, + ], + } + ], + renameFilename: undefined, + renameLocation: undefined, + }); + }); }); describe("tsserverProjectSystem CachingFileSystemInformation", () => { diff --git a/src/server/project.ts b/src/server/project.ts index f6f68f66387..155569690b2 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -550,6 +550,12 @@ namespace ts.server { return this.program.getSourceFileByPath(path); } + /* @internal */ + getSourceFileOrConfigFile(path: Path): SourceFile | undefined { + const options = this.program.getCompilerOptions(); + return path === options.configFilePath ? options.configFile : this.getSourceFile(path); + } + close() { if (this.program) { // if we have a program - release all files that are enlisted in program but arent root diff --git a/src/server/session.ts b/src/server/session.ts index a2d8b5fcf87..d24e2addd33 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -1771,20 +1771,10 @@ namespace ts.server { } private mapTextChangesToCodeEdits(project: Project, textChanges: ReadonlyArray): protocol.FileCodeEdits[] { - return textChanges.map(change => this.mapTextChangesToCodeEditsUsingScriptinfo(change, project.getScriptInfoForNormalizedPath(toNormalizedPath(change.fileName))!)); - } - - private mapTextChangesToCodeEditsUsingScriptinfo(textChanges: FileTextChanges, scriptInfo: ScriptInfo | undefined): protocol.FileCodeEdits { - Debug.assert(!!textChanges.isNewFile === !scriptInfo); - if (scriptInfo) { - return { - fileName: textChanges.fileName, - textChanges: textChanges.textChanges.map(textChange => this.convertTextChangeToCodeEdit(textChange, scriptInfo)) - }; - } - else { - return this.convertNewFileTextChangeToCodeEdit(textChanges); - } + return textChanges.map(change => { + const path = normalizedPathToPath(toNormalizedPath(change.fileName), this.host.getCurrentDirectory(), fileName => this.getCanonicalFileName(fileName)); + return mapTextChangesToCodeEdits(change, project.getSourceFileOrConfigFile(path)); + }); } private convertTextChangeToCodeEdit(change: TextChange, scriptInfo: ScriptInfo): protocol.CodeEdit { @@ -1795,13 +1785,6 @@ namespace ts.server { }; } - private convertNewFileTextChangeToCodeEdit(textChanges: FileTextChanges): protocol.FileCodeEdits { - Debug.assert(textChanges.textChanges.length === 1); - const change = first(textChanges.textChanges); - Debug.assert(change.span.start === 0 && change.span.length === 0); - return { fileName: textChanges.fileName, textChanges: [{ start: { line: 0, offset: 0 }, end: { line: 0, offset: 0 }, newText: change.newText }] }; - } - private getBraceMatching(args: protocol.FileLocationRequestArgs, simplifiedResult: boolean): protocol.TextSpan[] | TextSpan[] | undefined { const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args); const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file)!; @@ -2280,6 +2263,34 @@ namespace ts.server { } } + function mapTextChangesToCodeEdits(textChanges: FileTextChanges, sourceFile: SourceFile | undefined): protocol.FileCodeEdits { + Debug.assert(!!textChanges.isNewFile === !sourceFile); + if (sourceFile) { + return { + fileName: textChanges.fileName, + textChanges: textChanges.textChanges.map(textChange => convertTextChangeToCodeEdit(textChange, sourceFile)), + }; + } + else { + return convertNewFileTextChangeToCodeEdit(textChanges); + } + } + + function convertTextChangeToCodeEdit(change: TextChange, sourceFile: SourceFile): protocol.CodeEdit { + return { + start: convertToLocation(sourceFile.getLineAndCharacterOfPosition(change.span.start)), + end: convertToLocation(sourceFile.getLineAndCharacterOfPosition(change.span.start + change.span.length)), + newText: change.newText ? change.newText : "", + }; + } + + function convertNewFileTextChangeToCodeEdit(textChanges: FileTextChanges): protocol.FileCodeEdits { + Debug.assert(textChanges.textChanges.length === 1); + const change = first(textChanges.textChanges); + Debug.assert(change.span.start === 0 && change.span.length === 0); + return { fileName: textChanges.fileName, textChanges: [{ start: { line: 0, offset: 0 }, end: { line: 0, offset: 0 }, newText: change.newText }] }; + } + export interface HandlerResponse { response?: {}; responseRequired?: boolean; diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index ef1cfa9cd85..ba6ffcb80ac 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -8564,9 +8564,7 @@ declare namespace ts.server { private mapCodeAction; private mapCodeFixAction; private mapTextChangesToCodeEdits; - private mapTextChangesToCodeEditsUsingScriptinfo; private convertTextChangeToCodeEdit; - private convertNewFileTextChangeToCodeEdit; private getBraceMatching; private getDiagnosticsForProject; getCanonicalFileName(fileName: string): string; From 33d08932596cf49f45958e370329d1852758298a Mon Sep 17 00:00:00 2001 From: Andy Date: Thu, 7 Jun 2018 15:03:19 -0700 Subject: [PATCH 59/81] Add completions from literal contextual types (#24674) * Add completions from literal contextual types * Remove getTypesOfUnion * undo baseline changes --- src/services/completions.ts | 69 +++++++++++++------- src/services/services.ts | 2 +- tests/cases/fourslash/completionsLiterals.ts | 12 ++++ 3 files changed, 60 insertions(+), 23 deletions(-) create mode 100644 tests/cases/fourslash/completionsLiterals.ts diff --git a/src/services/completions.ts b/src/services/completions.ts index 707e5fc0b01..e31ac4e81f3 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -101,7 +101,7 @@ namespace ts.Completions { } function completionInfoFromData(sourceFile: SourceFile, typeChecker: TypeChecker, compilerOptions: CompilerOptions, log: Log, completionData: CompletionData, preferences: UserPreferences): CompletionInfo | undefined { - const { symbols, completionKind, isInSnippetScope, isNewIdentifierLocation, location, propertyAccessToConvert, keywordFilters, symbolToOriginInfoMap, recommendedCompletion, isJsxInitializer } = completionData; + const { symbols, completionKind, isInSnippetScope, isNewIdentifierLocation, location, propertyAccessToConvert, keywordFilters, literals, symbolToOriginInfoMap, recommendedCompletion, isJsxInitializer } = completionData; if (sourceFile.languageVariant === LanguageVariant.JSX && location && location.parent && isJsxClosingElement(location.parent)) { // In the TypeScript JSX element, if such element is not defined. When users query for completion at closing tag, @@ -143,6 +143,10 @@ namespace ts.Completions { addRange(entries, getKeywordCompletions(keywordFilters)); } + for (const literal of literals) { + entries.push(createCompletionEntryForLiteral(literal)); + } + return { isGlobalCompletion: isInSnippetScope, isMemberCompletion, isNewIdentifierLocation, entries }; } @@ -184,6 +188,11 @@ namespace ts.Completions { }); } + const completionNameForLiteral = JSON.stringify; + function createCompletionEntryForLiteral(literal: string | number): CompletionEntry { + return { name: completionNameForLiteral(literal), kind: ScriptElementKind.string, kindModifiers: ScriptElementKindModifier.none, sortText: "0" }; + } + function createCompletionEntry( symbol: Symbol, location: Node | undefined, @@ -372,7 +381,7 @@ namespace ts.Completions { case SyntaxKind.LiteralType: switch (node.parent.parent.kind) { case SyntaxKind.TypeReference: - return { kind: StringLiteralCompletionKind.Types, types: getStringLiteralTypes(typeChecker.getTypeArgumentConstraint(node.parent as LiteralTypeNode), typeChecker), isNewIdentifier: false }; + return { kind: StringLiteralCompletionKind.Types, types: getStringLiteralTypes(typeChecker.getTypeArgumentConstraint(node.parent as LiteralTypeNode)), isNewIdentifier: false }; case SyntaxKind.IndexedAccessType: // Get all apparent property names // i.e. interface Foo { @@ -448,7 +457,7 @@ namespace ts.Completions { function fromContextualType(): StringLiteralCompletion { // Get completion for string literal from string literal type // i.e. var x: "hi" | "hello" = "/*completion position*/" - return { kind: StringLiteralCompletionKind.Types, types: getStringLiteralTypes(getContextualTypeFromParent(node, typeChecker), typeChecker), isNewIdentifier: false }; + return { kind: StringLiteralCompletionKind.Types, types: getStringLiteralTypes(getContextualTypeFromParent(node, typeChecker)), isNewIdentifier: false }; } } @@ -462,7 +471,7 @@ namespace ts.Completions { if (!candidate.hasRestParameter && argumentInfo.argumentCount > candidate.parameters.length) return; const type = checker.getParameterType(candidate, argumentInfo.argumentIndex); isNewIdentifier = isNewIdentifier || !!(type.flags & TypeFlags.String); - return getStringLiteralTypes(type, checker, uniques); + return getStringLiteralTypes(type, uniques); }); return { kind: StringLiteralCompletionKind.Types, types, isNewIdentifier }; @@ -472,11 +481,11 @@ namespace ts.Completions { return type && { kind: StringLiteralCompletionKind.Properties, symbols: type.getApparentProperties(), hasIndexSignature: hasIndexSignature(type) }; } - function getStringLiteralTypes(type: Type | undefined, typeChecker: TypeChecker, uniques = createMap()): ReadonlyArray { + function getStringLiteralTypes(type: Type | undefined, uniques = createMap()): ReadonlyArray { if (!type) return emptyArray; type = skipConstraint(type); return type.isUnion() - ? flatMap(type.types, t => getStringLiteralTypes(t, typeChecker, uniques)) + ? flatMap(type.types, t => getStringLiteralTypes(t, uniques)) : type.isStringLiteral() && !(type.flags & TypeFlags.EnumLiteral) && addToSeen(uniques, type.value) ? [type] : emptyArray; @@ -491,7 +500,7 @@ namespace ts.Completions { readonly isJsxInitializer: IsJsxInitializer; } function getSymbolCompletionFromEntryId(program: Program, log: Log, sourceFile: SourceFile, position: number, entryId: CompletionEntryIdentifier, - ): SymbolCompletion | { type: "request", request: Request } | { type: "none" } { + ): SymbolCompletion | { type: "request", request: Request } | { type: "literal", literal: string | number } | { type: "none" } { const compilerOptions = program.getCompilerOptions(); const completionData = getCompletionData(program, log, sourceFile, isUncheckedFile(sourceFile, compilerOptions), position, { includeCompletionsForModuleExports: true, includeCompletionsWithInsertText: true }, entryId); if (!completionData) { @@ -501,7 +510,10 @@ namespace ts.Completions { return { type: "request", request: completionData }; } - const { symbols, location, completionKind, symbolToOriginInfoMap, previousToken, isJsxInitializer } = completionData; + const { symbols, literals, location, completionKind, symbolToOriginInfoMap, previousToken, isJsxInitializer } = completionData; + + const literal = find(literals, l => completionNameForLiteral(l) === entryId.name); + if (literal !== undefined) return { type: "literal", literal }; // Find the symbol with the matching entry name. // We don't need to perform character checks here because we're only comparing the @@ -574,12 +586,22 @@ namespace ts.Completions { const { codeActions, sourceDisplay } = getCompletionEntryCodeActionsAndSourceDisplay(symbolToOriginInfoMap, symbol, program, typeChecker, host, compilerOptions, sourceFile, previousToken, formatContext, getCanonicalFileName, program.getSourceFiles(), preferences); return createCompletionDetailsForSymbol(symbol, typeChecker, sourceFile, location!, cancellationToken, codeActions, sourceDisplay); // TODO: GH#18217 } + case "literal": { + const { literal } = symbolCompletion; + return createSimpleDetails(completionNameForLiteral(literal), ScriptElementKind.string, typeof literal === "string" ? SymbolDisplayPartKind.stringLiteral : SymbolDisplayPartKind.numericLiteral); + } case "none": // Didn't find a symbol with this name. See if we can find a keyword instead. - return allKeywordsCompletions().some(c => c.name === name) ? createCompletionDetails(name, ScriptElementKindModifier.none, ScriptElementKind.keyword, [displayPart(name, SymbolDisplayPartKind.keyword)]) : undefined; + return allKeywordsCompletions().some(c => c.name === name) ? createSimpleDetails(name, ScriptElementKind.keyword, SymbolDisplayPartKind.keyword) : undefined; + default: + Debug.assertNever(symbolCompletion); } } + function createSimpleDetails(name: string, kind: ScriptElementKind, kind2: SymbolDisplayPartKind): CompletionEntryDetails { + return createCompletionDetails(name, ScriptElementKindModifier.none, kind, [displayPart(name, kind2)]); + } + function createCompletionDetailsForSymbol(symbol: Symbol, checker: TypeChecker, sourceFile: SourceFile, location: Node, cancellationToken: CancellationToken, codeActions?: CodeAction[], sourceDisplay?: SymbolDisplayPart[]): CompletionEntryDetails { const { displayParts, documentation, symbolKind, tags } = checker.runWithCancellationToken(cancellationToken, checker => @@ -669,6 +691,7 @@ namespace ts.Completions { readonly isNewIdentifierLocation: boolean; readonly location: Node | undefined; readonly keywordFilters: KeywordCompletionFilters; + readonly literals: ReadonlyArray; readonly symbolToOriginInfoMap: SymbolOriginInfoMap; readonly recommendedCompletion: Symbol | undefined; readonly previousToken: Node | undefined; @@ -685,23 +708,22 @@ namespace ts.Completions { None, } - function getRecommendedCompletion(currentToken: Node, position: number, sourceFile: SourceFile, checker: TypeChecker): Symbol | undefined { - const contextualType = getContextualType(currentToken, position, sourceFile, checker); + function getRecommendedCompletion(previousToken: Node, contextualType: Type, checker: TypeChecker): Symbol | undefined { // For a union, return the first one with a recommended completion. return firstDefined(contextualType && (contextualType.isUnion() ? contextualType.types : [contextualType]), type => { const symbol = type && type.symbol; // Don't include make a recommended completion for an abstract class return symbol && (symbol.flags & (SymbolFlags.EnumMember | SymbolFlags.Enum | SymbolFlags.Class) && !isAbstractConstructorSymbol(symbol)) - ? getFirstSymbolInChain(symbol, currentToken, checker) + ? getFirstSymbolInChain(symbol, previousToken, checker) : undefined; }); } - function getContextualType(currentToken: Node, position: number, sourceFile: SourceFile, checker: TypeChecker): Type | undefined { - const { parent } = currentToken; - switch (currentToken.kind) { + function getContextualType(previousToken: Node, position: number, sourceFile: SourceFile, checker: TypeChecker): Type | undefined { + const { parent } = previousToken; + switch (previousToken.kind) { case SyntaxKind.Identifier: - return getContextualTypeFromParent(currentToken as Identifier, checker); + return getContextualTypeFromParent(previousToken as Identifier, checker); case SyntaxKind.EqualsToken: switch (parent.kind) { case SyntaxKind.VariableDeclaration: @@ -720,14 +742,14 @@ namespace ts.Completions { case SyntaxKind.OpenBraceToken: return isJsxExpression(parent) && parent.parent.kind !== SyntaxKind.JsxElement ? checker.getContextualTypeForJsxAttribute(parent.parent) : undefined; default: - const argInfo = SignatureHelp.getArgumentInfoForCompletions(currentToken, position, sourceFile); + const argInfo = SignatureHelp.getArgumentInfoForCompletions(previousToken, position, sourceFile); return argInfo // At `,`, treat this as the next argument after the comma. - ? checker.getContextualTypeForArgumentAtIndex(argInfo.invocation, argInfo.argumentIndex + (currentToken.kind === SyntaxKind.CommaToken ? 1 : 0)) - : isEqualityOperatorKind(currentToken.kind) && isBinaryExpression(parent) && isEqualityOperatorKind(parent.operatorToken.kind) + ? checker.getContextualTypeForArgumentAtIndex(argInfo.invocation, argInfo.argumentIndex + (previousToken.kind === SyntaxKind.CommaToken ? 1 : 0)) + : isEqualityOperatorKind(previousToken.kind) && isBinaryExpression(parent) && isEqualityOperatorKind(parent.operatorToken.kind) // completion at `x ===/**/` should be for the right side ? checker.getTypeAtLocation(parent.left) - : checker.getContextualType(currentToken as Expression); + : checker.getContextualType(previousToken as Expression); } } @@ -1005,8 +1027,11 @@ namespace ts.Completions { log("getCompletionData: Semantic work: " + (timestamp() - semanticStart)); - const recommendedCompletion = previousToken && getRecommendedCompletion(previousToken, position, sourceFile, typeChecker); - return { kind: CompletionDataKind.Data, symbols, completionKind, isInSnippetScope, propertyAccessToConvert, isNewIdentifierLocation, location, keywordFilters, symbolToOriginInfoMap, recommendedCompletion, previousToken, isJsxInitializer }; + const contextualType = previousToken && getContextualType(previousToken, position, sourceFile, typeChecker); + const literals = mapDefined(contextualType && (contextualType.isUnion() ? contextualType.types : [contextualType]), t => t.isLiteral() ? t.value : undefined); + + const recommendedCompletion = previousToken && contextualType && getRecommendedCompletion(previousToken, contextualType, typeChecker); + return { kind: CompletionDataKind.Data, symbols, completionKind, isInSnippetScope, propertyAccessToConvert, isNewIdentifierLocation, location, keywordFilters, literals, symbolToOriginInfoMap, recommendedCompletion, previousToken, isJsxInitializer }; type JSDocTagWithTypeExpression = JSDocParameterTag | JSDocPropertyTag | JSDocReturnTag | JSDocTypeTag | JSDocTypedefTag; diff --git a/src/services/services.ts b/src/services/services.ts index 0f05a542885..7d88d334b38 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -426,7 +426,7 @@ namespace ts { return !!(this.flags & TypeFlags.UnionOrIntersection); } isLiteral(): this is LiteralType { - return !!(this.flags & TypeFlags.Literal); + return !!(this.flags & TypeFlags.StringOrNumberLiteral); } isStringLiteral(): this is StringLiteralType { return !!(this.flags & TypeFlags.StringLiteral); diff --git a/tests/cases/fourslash/completionsLiterals.ts b/tests/cases/fourslash/completionsLiterals.ts new file mode 100644 index 00000000000..475d3dd247e --- /dev/null +++ b/tests/cases/fourslash/completionsLiterals.ts @@ -0,0 +1,12 @@ +/// + +////const x: 0 | "one" = /**/; + +verify.completions({ + marker: "", + includes: [ + { name: "0", kind: "string", text: "0" }, + { name: '"one"', kind: "string", text: '"one"' }, + ], + isNewIdentifierLocation: true, +}); From 48dedacf3bcbc7f718d5999850b5ab1f0d8597c9 Mon Sep 17 00:00:00 2001 From: Andy Date: Thu, 7 Jun 2018 15:03:38 -0700 Subject: [PATCH 60/81] fixStrictClassInitialization: Don't provide a default for `string` or `number` (#24767) * fixStrictClassInitialization: Don't provide a default for `string` or `number` * Update baselines --- src/compiler/checker.ts | 6 ++++-- src/compiler/types.ts | 2 ++ .../codefixes/fixStrictClassInitialization.ts | 10 ++-------- .../reference/checkJsxChildrenProperty3.types | 4 ++-- .../reference/checkJsxChildrenProperty4.types | 4 ++-- .../codeFixClassPropertyInitialization.ts | 14 +++++++------- .../codeFixClassPropertyInitialization3.ts | 4 ++-- .../codeFixClassPropertyInitialization4.ts | 15 --------------- .../codeFixClassPropertyInitialization8.ts | 4 ++-- .../codeFixClassPropertyInitialization_all_3.ts | 16 ++++++++-------- 10 files changed, 31 insertions(+), 48 deletions(-) delete mode 100644 tests/cases/fourslash/codeFixClassPropertyInitialization4.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 8fea3750d56..a847e68e1af 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -282,6 +282,8 @@ namespace ts { createPromiseType, createArrayType, getBooleanType: () => booleanType, + getFalseType: () => falseType, + getTrueType: () => trueType, getVoidType: () => voidType, getUndefinedType: () => undefinedType, getNullType: () => nullType, @@ -374,9 +376,9 @@ namespace ts { const nullWideningType = strictNullChecks ? nullType : createIntrinsicType(TypeFlags.Null | TypeFlags.ContainsWideningType, "null"); const stringType = createIntrinsicType(TypeFlags.String, "string"); const numberType = createIntrinsicType(TypeFlags.Number, "number"); - const trueType = createIntrinsicType(TypeFlags.BooleanLiteral, "true"); const falseType = createIntrinsicType(TypeFlags.BooleanLiteral, "false"); - const booleanType = createBooleanType([trueType, falseType]); + const trueType = createIntrinsicType(TypeFlags.BooleanLiteral, "true"); + const booleanType = createBooleanType([falseType, trueType]); const esSymbolType = createIntrinsicType(TypeFlags.ESSymbol, "symbol"); const voidType = createIntrinsicType(TypeFlags.Void, "void"); const neverType = createIntrinsicType(TypeFlags.Never, "never"); diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 2b493921f89..726947111be 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -3016,6 +3016,8 @@ namespace ts { /* @internal */ getStringType(): Type; /* @internal */ getNumberType(): Type; /* @internal */ getBooleanType(): Type; + /* @internal */ getFalseType(): Type; + /* @internal */ getTrueType(): Type; /* @internal */ getVoidType(): Type; /* @internal */ getUndefinedType(): Type; /* @internal */ getNullType(): Type; diff --git a/src/services/codefixes/fixStrictClassInitialization.ts b/src/services/codefixes/fixStrictClassInitialization.ts index 40a26da204a..278d1e2d3f6 100644 --- a/src/services/codefixes/fixStrictClassInitialization.ts +++ b/src/services/codefixes/fixStrictClassInitialization.ts @@ -109,14 +109,8 @@ namespace ts.codefix { } function getDefaultValueFromType (checker: TypeChecker, type: Type): Expression | undefined { - if (type.flags & TypeFlags.String) { - return createLiteral(""); - } - else if (type.flags & TypeFlags.Number) { - return createNumericLiteral("0"); - } - else if (type.flags & TypeFlags.Boolean) { - return createFalse(); + if (type.flags & TypeFlags.BooleanLiteral) { + return type === checker.getFalseType() ? createFalse() : createTrue(); } else if (type.isLiteral()) { return createLiteral(type.value); diff --git a/tests/baselines/reference/checkJsxChildrenProperty3.types b/tests/baselines/reference/checkJsxChildrenProperty3.types index 7fbb91bee00..84764f07b7a 100644 --- a/tests/baselines/reference/checkJsxChildrenProperty3.types +++ b/tests/baselines/reference/checkJsxChildrenProperty3.types @@ -38,11 +38,11 @@ class FetchUser extends React.Component { ? this.props.children(this.state.result) >this.props.children(this.state.result) : JSX.Element ->this.props.children : ((user: IUser) => JSX.Element) | (((user: IUser) => JSX.Element) & string) | (((user: IUser) => JSX.Element) & number) | (((user: IUser) => JSX.Element) & true) | (((user: IUser) => JSX.Element) & false) | (((user: IUser) => JSX.Element) & React.ReactElement) | (((user: IUser) => JSX.Element) & (string | number | boolean | any[] | React.ReactElement)[]) +>this.props.children : ((user: IUser) => JSX.Element) | (((user: IUser) => JSX.Element) & string) | (((user: IUser) => JSX.Element) & number) | (((user: IUser) => JSX.Element) & false) | (((user: IUser) => JSX.Element) & true) | (((user: IUser) => JSX.Element) & React.ReactElement) | (((user: IUser) => JSX.Element) & (string | number | boolean | any[] | React.ReactElement)[]) >this.props : IFetchUserProps & { children?: React.ReactNode; } >this : this >props : IFetchUserProps & { children?: React.ReactNode; } ->children : ((user: IUser) => JSX.Element) | (((user: IUser) => JSX.Element) & string) | (((user: IUser) => JSX.Element) & number) | (((user: IUser) => JSX.Element) & true) | (((user: IUser) => JSX.Element) & false) | (((user: IUser) => JSX.Element) & React.ReactElement) | (((user: IUser) => JSX.Element) & (string | number | boolean | any[] | React.ReactElement)[]) +>children : ((user: IUser) => JSX.Element) | (((user: IUser) => JSX.Element) & string) | (((user: IUser) => JSX.Element) & number) | (((user: IUser) => JSX.Element) & false) | (((user: IUser) => JSX.Element) & true) | (((user: IUser) => JSX.Element) & React.ReactElement) | (((user: IUser) => JSX.Element) & (string | number | boolean | any[] | React.ReactElement)[]) >this.state.result : any >this.state : any >this : this diff --git a/tests/baselines/reference/checkJsxChildrenProperty4.types b/tests/baselines/reference/checkJsxChildrenProperty4.types index 6e4c04aa231..6e2ff7fe121 100644 --- a/tests/baselines/reference/checkJsxChildrenProperty4.types +++ b/tests/baselines/reference/checkJsxChildrenProperty4.types @@ -38,11 +38,11 @@ class FetchUser extends React.Component { ? this.props.children(this.state.result) >this.props.children(this.state.result) : JSX.Element ->this.props.children : ((user: IUser) => JSX.Element) | (((user: IUser) => JSX.Element) & string) | (((user: IUser) => JSX.Element) & number) | (((user: IUser) => JSX.Element) & true) | (((user: IUser) => JSX.Element) & false) | (((user: IUser) => JSX.Element) & React.ReactElement) | (((user: IUser) => JSX.Element) & (string | number | boolean | any[] | React.ReactElement)[]) +>this.props.children : ((user: IUser) => JSX.Element) | (((user: IUser) => JSX.Element) & string) | (((user: IUser) => JSX.Element) & number) | (((user: IUser) => JSX.Element) & false) | (((user: IUser) => JSX.Element) & true) | (((user: IUser) => JSX.Element) & React.ReactElement) | (((user: IUser) => JSX.Element) & (string | number | boolean | any[] | React.ReactElement)[]) >this.props : IFetchUserProps & { children?: React.ReactNode; } >this : this >props : IFetchUserProps & { children?: React.ReactNode; } ->children : ((user: IUser) => JSX.Element) | (((user: IUser) => JSX.Element) & string) | (((user: IUser) => JSX.Element) & number) | (((user: IUser) => JSX.Element) & true) | (((user: IUser) => JSX.Element) & false) | (((user: IUser) => JSX.Element) & React.ReactElement) | (((user: IUser) => JSX.Element) & (string | number | boolean | any[] | React.ReactElement)[]) +>children : ((user: IUser) => JSX.Element) | (((user: IUser) => JSX.Element) & string) | (((user: IUser) => JSX.Element) & number) | (((user: IUser) => JSX.Element) & false) | (((user: IUser) => JSX.Element) & true) | (((user: IUser) => JSX.Element) & React.ReactElement) | (((user: IUser) => JSX.Element) & (string | number | boolean | any[] | React.ReactElement)[]) >this.state.result : any >this.state : any >this : this diff --git a/tests/cases/fourslash/codeFixClassPropertyInitialization.ts b/tests/cases/fourslash/codeFixClassPropertyInitialization.ts index 6b7cbbc2705..fd1317ccaeb 100644 --- a/tests/cases/fourslash/codeFixClassPropertyInitialization.ts +++ b/tests/cases/fourslash/codeFixClassPropertyInitialization.ts @@ -12,15 +12,15 @@ //// //// class T { //// -//// a: string; +//// a: boolean; //// -//// static b: string; +//// static b: boolean; //// -//// private c: string; +//// private c: boolean; //// //// d: number | undefined; //// -//// e: string | number; +//// e: string | boolean; //// //// f: 1; //// @@ -46,9 +46,9 @@ function fixes(name: string, type: string, options: { isPrivate?: boolean, noIni } verify.codeFixAvailable([ - ...fixes("a", "string"), - ...fixes("c", "string", { isPrivate: true }), - ...fixes("e", "string | number"), + ...fixes("a", "boolean"), + ...fixes("c", "boolean", { isPrivate: true }), + ...fixes("e", "string | boolean"), ...fixes("f", "1"), ...fixes("g", '"123" | "456"'), ...fixes("h", "boolean"), diff --git a/tests/cases/fourslash/codeFixClassPropertyInitialization3.ts b/tests/cases/fourslash/codeFixClassPropertyInitialization3.ts index 329c6107ac7..8b76da581fe 100644 --- a/tests/cases/fourslash/codeFixClassPropertyInitialization3.ts +++ b/tests/cases/fourslash/codeFixClassPropertyInitialization3.ts @@ -3,13 +3,13 @@ // @strict: true //// class T { -//// a: string; +//// a: boolean; //// } verify.codeFix({ description: `Add initializer to property 'a'`, newFileContent: `class T { - a: string = ""; + a: boolean = false; }`, index: 2 }) \ No newline at end of file diff --git a/tests/cases/fourslash/codeFixClassPropertyInitialization4.ts b/tests/cases/fourslash/codeFixClassPropertyInitialization4.ts deleted file mode 100644 index 17a363e15b3..00000000000 --- a/tests/cases/fourslash/codeFixClassPropertyInitialization4.ts +++ /dev/null @@ -1,15 +0,0 @@ -/// - -// @strict: true - -//// class T { -//// a: number; -//// } - -verify.codeFix({ - description: `Add initializer to property 'a'`, - newFileContent: `class T { - a: number = 0; -}`, - index: 2 -}) \ No newline at end of file diff --git a/tests/cases/fourslash/codeFixClassPropertyInitialization8.ts b/tests/cases/fourslash/codeFixClassPropertyInitialization8.ts index 5c1f7873c16..8af1dd263dd 100644 --- a/tests/cases/fourslash/codeFixClassPropertyInitialization8.ts +++ b/tests/cases/fourslash/codeFixClassPropertyInitialization8.ts @@ -3,13 +3,13 @@ // @strict: true //// class T { -//// a: string | number; +//// a: string | boolean; //// } verify.codeFix({ description: `Add initializer to property 'a'`, newFileContent: `class T { - a: string | number = ""; + a: string | boolean = false; }`, index: 2 }) \ No newline at end of file diff --git a/tests/cases/fourslash/codeFixClassPropertyInitialization_all_3.ts b/tests/cases/fourslash/codeFixClassPropertyInitialization_all_3.ts index 5931c6bd977..29a9fe19b15 100644 --- a/tests/cases/fourslash/codeFixClassPropertyInitialization_all_3.ts +++ b/tests/cases/fourslash/codeFixClassPropertyInitialization_all_3.ts @@ -12,15 +12,15 @@ //// //// class T { //// -//// a: string; +//// a: boolean; //// -//// static b: string; +//// static b: boolean; //// -//// private c: string; +//// private c: boolean; //// //// d: number | undefined; //// -//// e: string | number; +//// e: string | boolean; //// //// f: 1; //// @@ -50,15 +50,15 @@ class Foo {} class T { - a: string = ""; + a: boolean = false; - static b: string; + static b: boolean; - private c: string = ""; + private c: boolean = false; d: number | undefined; - e: string | number = ""; + e: string | boolean = false; f: 1 = 1; From 18e4ca62ed33a2f62b8b015da832fb5a32af526b Mon Sep 17 00:00:00 2001 From: csigs Date: Thu, 7 Jun 2018 22:10:59 +0000 Subject: [PATCH 61/81] LEGO: check in for master to temporary branch. --- .../diagnosticMessages.generated.json.lcl | 21 +++++++++++++++++++ .../diagnosticMessages.generated.json.lcl | 21 +++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/src/loc/lcl/fra/diagnosticMessages/diagnosticMessages.generated.json.lcl b/src/loc/lcl/fra/diagnosticMessages/diagnosticMessages.generated.json.lcl index 01118e162e7..d13446474d0 100644 --- a/src/loc/lcl/fra/diagnosticMessages/diagnosticMessages.generated.json.lcl +++ b/src/loc/lcl/fra/diagnosticMessages/diagnosticMessages.generated.json.lcl @@ -1947,6 +1947,18 @@
+ + + + + + + + + + + + @@ -8721,6 +8733,15 @@ + + + + + + + + + diff --git a/src/loc/lcl/ptb/diagnosticMessages/diagnosticMessages.generated.json.lcl b/src/loc/lcl/ptb/diagnosticMessages/diagnosticMessages.generated.json.lcl index 5c86135cbf9..bdabbd3cf7d 100644 --- a/src/loc/lcl/ptb/diagnosticMessages/diagnosticMessages.generated.json.lcl +++ b/src/loc/lcl/ptb/diagnosticMessages/diagnosticMessages.generated.json.lcl @@ -1928,6 +1928,18 @@ + + + + + + + + + + + + @@ -8699,6 +8711,15 @@ + + + + + + + + + From 3822e3e4ed0e3524ffde41a85742df7cdc93cd72 Mon Sep 17 00:00:00 2001 From: Andy Date: Thu, 7 Jun 2018 15:45:03 -0700 Subject: [PATCH 62/81] tryGetModuleNameAsNodeModule: Ignore file extension (#24774) --- src/compiler/moduleSpecifiers.ts | 2 +- .../fourslash/importNameCodeFixNewImportNodeModules7.ts | 9 +-------- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/src/compiler/moduleSpecifiers.ts b/src/compiler/moduleSpecifiers.ts index 50daa81d60c..6da0fc61e63 100644 --- a/src/compiler/moduleSpecifiers.ts +++ b/src/compiler/moduleSpecifiers.ts @@ -248,7 +248,7 @@ namespace ts.moduleSpecifiers { const mainFileRelative = packageJsonContent.typings || packageJsonContent.types || packageJsonContent.main; if (mainFileRelative) { const mainExportFile = toPath(mainFileRelative, packageRootPath, getCanonicalFileName); - if (mainExportFile === getCanonicalFileName(path)) { + if (removeFileExtension(mainExportFile) === removeFileExtension(getCanonicalFileName(path))) { return packageRootPath; } } diff --git a/tests/cases/fourslash/importNameCodeFixNewImportNodeModules7.ts b/tests/cases/fourslash/importNameCodeFixNewImportNodeModules7.ts index 9032018a7f5..beaaad57f19 100644 --- a/tests/cases/fourslash/importNameCodeFixNewImportNodeModules7.ts +++ b/tests/cases/fourslash/importNameCodeFixNewImportNodeModules7.ts @@ -15,15 +15,8 @@ // @Filename: node_modules/package-name/package.json //// { "main": "bin/lib/libfile.js" } - -// In this case, importing the module by its package name: -// import { f1 } from 'package-name' -// could in theory work, however the resulting code compiles with a module resolution error -// since bin/lib/libfile.d.ts isn't declared under "typings" in package.json -// Therefore just import the module by its qualified path - verify.importFixAtPosition([ -`import { f1 } from "package-name/bin/lib/libfile"; +`import { f1 } from "package-name"; f1('');` ]); \ No newline at end of file From bceb08b36faaa357d7680d35a91991aa6b5c3975 Mon Sep 17 00:00:00 2001 From: Steven Date: Tue, 5 Jun 2018 20:30:07 -0400 Subject: [PATCH 63/81] build: add check for lib size --- Gulpfile.js | 10 +++++++++- scripts/build/get-dir-size.js | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 scripts/build/get-dir-size.js diff --git a/Gulpfile.js b/Gulpfile.js index 52744e12740..0c8b4e119cb 100644 --- a/Gulpfile.js +++ b/Gulpfile.js @@ -12,6 +12,7 @@ const clone = require("gulp-clone"); const newer = require("gulp-newer"); const tsc = require("gulp-typescript"); const tsc_oop = require("./scripts/build/gulp-typescript-oop"); +const { getDirSize } = require("./scripts/build/get-dir-size"); const insert = require("gulp-insert"); const sourcemaps = require("gulp-sourcemaps"); const Q = require("q"); @@ -588,7 +589,14 @@ gulp.task("VerifyLKG", /*help*/ false, [], () => { gulp.task("LKGInternal", /*help*/ false, ["lib", "local"]); gulp.task("LKG", "Makes a new LKG out of the built js files", ["clean", "dontUseDebugMode"], () => { - return runSequence("LKGInternal", "VerifyLKG"); + const lib = "./lib"; + const sizeBefore = getDirSize(lib); + const seq = runSequence("LKGInternal", "VerifyLKG"); + const sizeAfter = getDirSize(lib); + if (sizeAfter > (sizeBefore * 1.10)) { + throw new Error("The lib folder increased by 10% or more. This likely indicates a bug."); + } + return seq; }); diff --git a/scripts/build/get-dir-size.js b/scripts/build/get-dir-size.js new file mode 100644 index 00000000000..1b36e4cdb11 --- /dev/null +++ b/scripts/build/get-dir-size.js @@ -0,0 +1,32 @@ +// @ts-check +const { lstatSync, readdirSync } = require("fs"); +const { join } = require("path"); +const { promisify } = require("util"); +const execFile = promisify(require("child_process").execFile); + +/** + * Find the size of a directory recursively. + * Symbolic links are counted once (same inode). + * @param {string} root + * @param {Set} seen + * @returns {number} bytes + */ +function getDirSize(root, seen = new Set()) { + const stats = lstatSync(root); + + if (seen.has(stats.ino)) { + return 0; + } + + seen.add(stats.ino); + + if (!stats.isDirectory()) { + return stats.size; + } + + return readdirSync(root) + .map(file => getDirSize(join(root, file), seen)) + .reduce((acc, num) => acc + num, 0); +} + +exports.getDirSize = getDirSize; From f89273a31f737a371223f80dbbec1c95cb4f0a4a Mon Sep 17 00:00:00 2001 From: Steven Date: Thu, 7 Jun 2018 08:41:21 -0400 Subject: [PATCH 64/81] Remove unused promisify --- scripts/build/get-dir-size.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/scripts/build/get-dir-size.js b/scripts/build/get-dir-size.js index 1b36e4cdb11..d02c638a03a 100644 --- a/scripts/build/get-dir-size.js +++ b/scripts/build/get-dir-size.js @@ -1,8 +1,6 @@ // @ts-check const { lstatSync, readdirSync } = require("fs"); const { join } = require("path"); -const { promisify } = require("util"); -const execFile = promisify(require("child_process").execFile); /** * Find the size of a directory recursively. From 38a46b754d71fbff6a06254f8b56dd1915bfe690 Mon Sep 17 00:00:00 2001 From: Steven Date: Thu, 7 Jun 2018 08:42:32 -0400 Subject: [PATCH 65/81] Rename get-dir-size.js to getDirSize.js --- scripts/build/{get-dir-size.js => getDirSize.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename scripts/build/{get-dir-size.js => getDirSize.js} (100%) diff --git a/scripts/build/get-dir-size.js b/scripts/build/getDirSize.js similarity index 100% rename from scripts/build/get-dir-size.js rename to scripts/build/getDirSize.js From 1e0c22453b0e8175de3a1801401ed7aa4e470d2c Mon Sep 17 00:00:00 2001 From: Steven Date: Thu, 7 Jun 2018 08:42:57 -0400 Subject: [PATCH 66/81] Rename get-dir-size to getDirSize --- Gulpfile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gulpfile.js b/Gulpfile.js index 0c8b4e119cb..7d021d9f360 100644 --- a/Gulpfile.js +++ b/Gulpfile.js @@ -12,7 +12,7 @@ const clone = require("gulp-clone"); const newer = require("gulp-newer"); const tsc = require("gulp-typescript"); const tsc_oop = require("./scripts/build/gulp-typescript-oop"); -const { getDirSize } = require("./scripts/build/get-dir-size"); +const { getDirSize } = require("./scripts/build/getDirSize"); const insert = require("gulp-insert"); const sourcemaps = require("gulp-sourcemaps"); const Q = require("q"); From 06f411c4de1c0f1875560af8db58679e9e743c79 Mon Sep 17 00:00:00 2001 From: Steven Date: Thu, 7 Jun 2018 20:52:22 -0400 Subject: [PATCH 67/81] Change getDirSize to default export --- Gulpfile.js | 7 +++---- scripts/build/getDirSize.js | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Gulpfile.js b/Gulpfile.js index 7d021d9f360..db7bb76d34b 100644 --- a/Gulpfile.js +++ b/Gulpfile.js @@ -12,7 +12,7 @@ const clone = require("gulp-clone"); const newer = require("gulp-newer"); const tsc = require("gulp-typescript"); const tsc_oop = require("./scripts/build/gulp-typescript-oop"); -const { getDirSize } = require("./scripts/build/getDirSize"); +const getDirSize = require("./scripts/build/getDirSize"); const insert = require("gulp-insert"); const sourcemaps = require("gulp-sourcemaps"); const Q = require("q"); @@ -589,10 +589,9 @@ gulp.task("VerifyLKG", /*help*/ false, [], () => { gulp.task("LKGInternal", /*help*/ false, ["lib", "local"]); gulp.task("LKG", "Makes a new LKG out of the built js files", ["clean", "dontUseDebugMode"], () => { - const lib = "./lib"; - const sizeBefore = getDirSize(lib); + const sizeBefore = getDirSize(lkgDirectory); const seq = runSequence("LKGInternal", "VerifyLKG"); - const sizeAfter = getDirSize(lib); + const sizeAfter = getDirSize(lkgDirectory); if (sizeAfter > (sizeBefore * 1.10)) { throw new Error("The lib folder increased by 10% or more. This likely indicates a bug."); } diff --git a/scripts/build/getDirSize.js b/scripts/build/getDirSize.js index d02c638a03a..278c4e7f009 100644 --- a/scripts/build/getDirSize.js +++ b/scripts/build/getDirSize.js @@ -27,4 +27,4 @@ function getDirSize(root, seen = new Set()) { .reduce((acc, num) => acc + num, 0); } -exports.getDirSize = getDirSize; +module.exports = getDirSize; From 3cd802510ce8b90af94f6ab2e7047d2185d43668 Mon Sep 17 00:00:00 2001 From: Steven Date: Thu, 7 Jun 2018 20:53:44 -0400 Subject: [PATCH 68/81] Add getDirSize checks to Jakefile LKG --- Jakefile.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Jakefile.js b/Jakefile.js index 55729eecaad..fed1bf3e7fa 100644 --- a/Jakefile.js +++ b/Jakefile.js @@ -8,6 +8,7 @@ var path = require("path"); var child_process = require("child_process"); var fold = require("travis-fold"); var ts = require("./lib/typescript"); +const getDirSize = require("./scripts/build/getDirSize"); // Variables var compilerDirectory = "src/compiler/"; @@ -643,6 +644,7 @@ task("generate-spec", [specMd]); // Makes a new LKG. This target does not build anything, but errors if not all the outputs are present in the built/local directory desc("Makes a new LKG out of the built js files"); task("LKG", ["clean", "release", "local"].concat(libraryTargets), function () { + const sizeBefore = getDirSize(LKGDirectory); var expectedFiles = [tscFile, servicesFile, serverFile, nodePackageFile, nodeDefinitionsFile, standaloneDefinitionsFile, tsserverLibraryFile, tsserverLibraryDefinitionFile, cancellationTokenFile, typingsInstallerFile, buildProtocolDts, watchGuardFile]. concat(libraryTargets). concat(localizationTargets); @@ -658,10 +660,11 @@ task("LKG", ["clean", "release", "local"].concat(libraryTargets), function () { for (i in expectedFiles) { jake.cpR(expectedFiles[i], LKGDirectory); } - //var resourceDirectories = fs.readdirSync(builtLocalResourcesDirectory).map(function(p) { return path.join(builtLocalResourcesDirectory, p); }); - //resourceDirectories.map(function(d) { - // jake.cpR(d, LKGResourcesDirectory); - //}); + + const sizeAfter = getDirSize(LKGDirectory); + if (sizeAfter > (sizeBefore * 1.10)) { + throw new Error("The lib folder increased by 10% or more. This likely indicates a bug."); + } }); // Test directory From 8b034e6cd89fe51360b7316cad166f1ee712345e Mon Sep 17 00:00:00 2001 From: Steven Date: Thu, 7 Jun 2018 20:58:29 -0400 Subject: [PATCH 69/81] Modernize syntax by using arrow funcs --- Jakefile.js | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/Jakefile.js b/Jakefile.js index fed1bf3e7fa..a6483355c73 100644 --- a/Jakefile.js +++ b/Jakefile.js @@ -643,23 +643,19 @@ task("generate-spec", [specMd]); // Makes a new LKG. This target does not build anything, but errors if not all the outputs are present in the built/local directory desc("Makes a new LKG out of the built js files"); -task("LKG", ["clean", "release", "local"].concat(libraryTargets), function () { +task("LKG", ["clean", "release", "local"].concat(libraryTargets), () => { const sizeBefore = getDirSize(LKGDirectory); var expectedFiles = [tscFile, servicesFile, serverFile, nodePackageFile, nodeDefinitionsFile, standaloneDefinitionsFile, tsserverLibraryFile, tsserverLibraryDefinitionFile, cancellationTokenFile, typingsInstallerFile, buildProtocolDts, watchGuardFile]. concat(libraryTargets). concat(localizationTargets); - var missingFiles = expectedFiles.filter(function (f) { - return !fs.existsSync(f); - }); + var missingFiles = expectedFiles.filter(f => !fs.existsSync(f)); if (missingFiles.length > 0) { fail(new Error("Cannot replace the LKG unless all built targets are present in directory " + builtLocalDirectory + ". The following files are missing:\n" + missingFiles.join("\n"))); } // Copy all the targets into the LKG directory jake.mkdirP(LKGDirectory); - for (i in expectedFiles) { - jake.cpR(expectedFiles[i], LKGDirectory); - } + expectedFiles.forEach(f => jake.cpR(f, LKGDirectory)); const sizeAfter = getDirSize(LKGDirectory); if (sizeAfter > (sizeBefore * 1.10)) { From 75df424a6d8271205c5c729916898749a945c643 Mon Sep 17 00:00:00 2001 From: csigs Date: Fri, 8 Jun 2018 04:10:38 +0000 Subject: [PATCH 70/81] LEGO: check in for master to temporary branch. --- .../diagnosticMessages.generated.json.lcl | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/loc/lcl/csy/diagnosticMessages/diagnosticMessages.generated.json.lcl b/src/loc/lcl/csy/diagnosticMessages/diagnosticMessages.generated.json.lcl index d2c010d954d..1d250c68944 100644 --- a/src/loc/lcl/csy/diagnosticMessages/diagnosticMessages.generated.json.lcl +++ b/src/loc/lcl/csy/diagnosticMessages/diagnosticMessages.generated.json.lcl @@ -1947,12 +1947,18 @@ + + + + + + From d7a0619009c8cfe4d5e9d1161786fac7e7e8b396 Mon Sep 17 00:00:00 2001 From: krk Date: Fri, 8 Jun 2018 14:49:11 +0300 Subject: [PATCH 71/81] Inlined asterisk token creation. --- src/services/codefixes/helpers.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/services/codefixes/helpers.ts b/src/services/codefixes/helpers.ts index 8de22cafe84..58e0d2bebbf 100644 --- a/src/services/codefixes/helpers.ts +++ b/src/services/codefixes/helpers.ts @@ -117,12 +117,10 @@ namespace ts.codefix { makeStatic: boolean, preferences: UserPreferences, ): MethodDeclaration { - const asterisk = parent.kind === SyntaxKind.YieldExpression ? createToken(SyntaxKind.AsteriskToken) : undefined; - return createMethod( /*decorators*/ undefined, /*modifiers*/ makeStatic ? [createToken(SyntaxKind.StaticKeyword)] : undefined, - /*asteriskToken*/ asterisk, + /*asteriskToken*/ isYieldExpression(parent) ? createToken(SyntaxKind.AsteriskToken) : undefined, methodName, /*questionToken*/ undefined, /*typeParameters*/ inJs ? undefined : map(typeArguments, (_, i) => From 855c3a6d4fb2f81061711f52c1b01b6f2295eeee Mon Sep 17 00:00:00 2001 From: Andy Date: Fri, 8 Jun 2018 10:39:01 -0700 Subject: [PATCH 72/81] fixUnusedIdentifier: Delete trailing comma in array binding pattern (#24800) --- src/services/codefixes/fixUnusedIdentifier.ts | 15 ++++++--------- ...ixUnusedIdentifier_destructure_partlyUnused.ts | 4 ++-- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/services/codefixes/fixUnusedIdentifier.ts b/src/services/codefixes/fixUnusedIdentifier.ts index c97902e7713..ebe65197c7f 100644 --- a/src/services/codefixes/fixUnusedIdentifier.ts +++ b/src/services/codefixes/fixUnusedIdentifier.ts @@ -228,15 +228,12 @@ namespace ts.codefix { case SyntaxKind.BindingElement: { const pattern = (parent as BindingElement).parent; - switch (pattern.kind) { - case SyntaxKind.ArrayBindingPattern: - changes.deleteNode(sourceFile, parent); // Don't delete ',' - break; - case SyntaxKind.ObjectBindingPattern: - changes.deleteNodeInList(sourceFile, parent); - break; - default: - return Debug.assertNever(pattern); + const preserveComma = pattern.kind === SyntaxKind.ArrayBindingPattern && parent !== last(pattern.elements); + if (preserveComma) { + changes.deleteNode(sourceFile, parent); + } + else { + changes.deleteNodeInList(sourceFile, parent); } break; } diff --git a/tests/cases/fourslash/codeFixUnusedIdentifier_destructure_partlyUnused.ts b/tests/cases/fourslash/codeFixUnusedIdentifier_destructure_partlyUnused.ts index 6ed5973d400..e0783706085 100644 --- a/tests/cases/fourslash/codeFixUnusedIdentifier_destructure_partlyUnused.ts +++ b/tests/cases/fourslash/codeFixUnusedIdentifier_destructure_partlyUnused.ts @@ -57,7 +57,7 @@ verify.codeFixAll({ x; z; } { - const [x,] = o; + const [x] = o; x; } { @@ -65,7 +65,7 @@ verify.codeFixAll({ y; } { - const [, y,] = o; + const [, y] = o; y; } { From f17fe8713e05756babc72720e091bbefc83f5135 Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Fri, 8 Jun 2018 10:54:18 -0700 Subject: [PATCH 73/81] Actually forward sourcemaps to gulp-typescript doesnt disable sourcemaps (#24766) --- scripts/build/gulp-typescript-oop.js | 2 +- scripts/build/main.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/build/gulp-typescript-oop.js b/scripts/build/gulp-typescript-oop.js index d4b494e6435..78d09c70874 100644 --- a/scripts/build/gulp-typescript-oop.js +++ b/scripts/build/gulp-typescript-oop.js @@ -31,7 +31,7 @@ function createProject(tsConfigFileName, settings, options) { read() {}, /** @param {*} file */ write(file, encoding, callback) { - proc.send({ method: "write", params: { path: file.path, cwd: file.cwd, base: file.base }}); + proc.send({ method: "write", params: { path: file.path, cwd: file.cwd, base: file.base, sourceMap: file.sourceMap }}); callback(); }, final(callback) { diff --git a/scripts/build/main.js b/scripts/build/main.js index 3dcec4880d7..70a46adca8e 100644 --- a/scripts/build/main.js +++ b/scripts/build/main.js @@ -72,6 +72,7 @@ process.on("message", ({ method, params }) => { base: params.base }); file.contents = fs.readFileSync(file.path); + if (params.sourceMap) file.sourceMap = params.sourceMap; inputStream.push(/** @type {*} */(file)); } else if (method === "final") { From 4240d9dc0f8de1e272c060f11140de35cfbc349c Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Fri, 8 Jun 2018 13:11:30 -0700 Subject: [PATCH 74/81] always visit them all (#24802) --- src/compiler/checker.ts | 6 +++++- .../importNotElidedWhenNotFound.errors.txt | 19 ++++++++++++++++-- .../reference/importNotElidedWhenNotFound.js | 20 ++++++++++++++++++- .../importNotElidedWhenNotFound.symbols | 18 +++++++++++++++++ .../importNotElidedWhenNotFound.types | 20 +++++++++++++++++++ .../compiler/importNotElidedWhenNotFound.ts | 10 +++++++++- 6 files changed, 88 insertions(+), 5 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index a847e68e1af..ce1ee54edcd 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -18620,7 +18620,7 @@ namespace ts { if (node.expression.kind === SyntaxKind.SuperKeyword) { const superType = checkSuperExpression(node.expression); if (isTypeAny(superType)) { - forEach(node.arguments, checkExpression); // Still visit arguments so they get marked for visibility, etc + forEach(node.arguments, checkExpresionNoReturn); // Still visit arguments so they get marked for visibility, etc return anySignature; } if (superType !== errorType) { @@ -20781,6 +20781,10 @@ namespace ts { return type; } + function checkExpresionNoReturn(node: Expression) { + checkExpression(node); + } + // Checks an expression and returns its type. The contextualMapper parameter serves two purposes: When // contextualMapper is not undefined and not equal to the identityMapper function object it indicates that the // expression is being inferentially typed (section 4.15.2 in spec) and provides the type mapper to use in diff --git a/tests/baselines/reference/importNotElidedWhenNotFound.errors.txt b/tests/baselines/reference/importNotElidedWhenNotFound.errors.txt index 51e1fe9b44d..b8245c0081d 100644 --- a/tests/baselines/reference/importNotElidedWhenNotFound.errors.txt +++ b/tests/baselines/reference/importNotElidedWhenNotFound.errors.txt @@ -1,8 +1,10 @@ tests/cases/compiler/importNotElidedWhenNotFound.ts(1,15): error TS2307: Cannot find module 'file'. tests/cases/compiler/importNotElidedWhenNotFound.ts(2,15): error TS2307: Cannot find module 'other_file'. +tests/cases/compiler/importNotElidedWhenNotFound.ts(10,16): error TS2307: Cannot find module 'file2'. +tests/cases/compiler/importNotElidedWhenNotFound.ts(11,16): error TS2307: Cannot find module 'file3'. -==== tests/cases/compiler/importNotElidedWhenNotFound.ts (2 errors) ==== +==== tests/cases/compiler/importNotElidedWhenNotFound.ts (4 errors) ==== import X from 'file'; ~~~~~~ !!! error TS2307: Cannot find module 'file'. @@ -14,4 +16,17 @@ tests/cases/compiler/importNotElidedWhenNotFound.ts(2,15): error TS2307: Cannot constructor() { super(X); } - } \ No newline at end of file + } + + import X2 from 'file2'; + ~~~~~~~ +!!! error TS2307: Cannot find module 'file2'. + import X3 from 'file3'; + ~~~~~~~ +!!! error TS2307: Cannot find module 'file3'. + class Q extends Z { + constructor() { + super(X2, X3); + } + } + \ No newline at end of file diff --git a/tests/baselines/reference/importNotElidedWhenNotFound.js b/tests/baselines/reference/importNotElidedWhenNotFound.js index 303e5df8f22..9eef51f4052 100644 --- a/tests/baselines/reference/importNotElidedWhenNotFound.js +++ b/tests/baselines/reference/importNotElidedWhenNotFound.js @@ -6,7 +6,16 @@ class Y extends Z { constructor() { super(X); } -} +} + +import X2 from 'file2'; +import X3 from 'file3'; +class Q extends Z { + constructor() { + super(X2, X3); + } +} + //// [importNotElidedWhenNotFound.js] "use strict"; @@ -30,3 +39,12 @@ var Y = /** @class */ (function (_super) { } return Y; }(other_file_1["default"])); +var file2_1 = require("file2"); +var file3_1 = require("file3"); +var Q = /** @class */ (function (_super) { + __extends(Q, _super); + function Q() { + return _super.call(this, file2_1["default"], file3_1["default"]) || this; + } + return Q; +}(other_file_1["default"])); diff --git a/tests/baselines/reference/importNotElidedWhenNotFound.symbols b/tests/baselines/reference/importNotElidedWhenNotFound.symbols index 37558d6be3d..c920eefb44e 100644 --- a/tests/baselines/reference/importNotElidedWhenNotFound.symbols +++ b/tests/baselines/reference/importNotElidedWhenNotFound.symbols @@ -14,3 +14,21 @@ class Y extends Z { >X : Symbol(X, Decl(importNotElidedWhenNotFound.ts, 0, 6)) } } + +import X2 from 'file2'; +>X2 : Symbol(X2, Decl(importNotElidedWhenNotFound.ts, 9, 6)) + +import X3 from 'file3'; +>X3 : Symbol(X3, Decl(importNotElidedWhenNotFound.ts, 10, 6)) + +class Q extends Z { +>Q : Symbol(Q, Decl(importNotElidedWhenNotFound.ts, 10, 23)) +>Z : Symbol(Z, Decl(importNotElidedWhenNotFound.ts, 1, 6)) + + constructor() { + super(X2, X3); +>X2 : Symbol(X2, Decl(importNotElidedWhenNotFound.ts, 9, 6)) +>X3 : Symbol(X3, Decl(importNotElidedWhenNotFound.ts, 10, 6)) + } +} + diff --git a/tests/baselines/reference/importNotElidedWhenNotFound.types b/tests/baselines/reference/importNotElidedWhenNotFound.types index 02304b6e2d2..ca29d981944 100644 --- a/tests/baselines/reference/importNotElidedWhenNotFound.types +++ b/tests/baselines/reference/importNotElidedWhenNotFound.types @@ -16,3 +16,23 @@ class Y extends Z { >X : any } } + +import X2 from 'file2'; +>X2 : any + +import X3 from 'file3'; +>X3 : any + +class Q extends Z { +>Q : Q +>Z : any + + constructor() { + super(X2, X3); +>super(X2, X3) : void +>super : any +>X2 : any +>X3 : any + } +} + diff --git a/tests/cases/compiler/importNotElidedWhenNotFound.ts b/tests/cases/compiler/importNotElidedWhenNotFound.ts index 2ff7cd64d0d..7781dc9d430 100644 --- a/tests/cases/compiler/importNotElidedWhenNotFound.ts +++ b/tests/cases/compiler/importNotElidedWhenNotFound.ts @@ -5,4 +5,12 @@ class Y extends Z { constructor() { super(X); } -} \ No newline at end of file +} + +import X2 from 'file2'; +import X3 from 'file3'; +class Q extends Z { + constructor() { + super(X2, X3); + } +} From 5d70d9223c9cc46e1f5ed15da94c0fe6178b59ab Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 8 Jun 2018 13:57:25 -0700 Subject: [PATCH 75/81] Always resolve the first identifier of computed property name to get the symbol and track it Fixes #24798 --- src/compiler/checker.ts | 10 ++-- ...eclarationEmitWithDefaultAsComputedName.js | 46 ++++++++++++++++ ...ationEmitWithDefaultAsComputedName.symbols | 45 ++++++++++++++++ ...arationEmitWithDefaultAsComputedName.types | 50 ++++++++++++++++++ ...clarationEmitWithDefaultAsComputedName2.js | 46 ++++++++++++++++ ...tionEmitWithDefaultAsComputedName2.symbols | 47 +++++++++++++++++ ...rationEmitWithDefaultAsComputedName2.types | 52 +++++++++++++++++++ ...eclarationEmitWithDefaultAsComputedName.ts | 19 +++++++ ...clarationEmitWithDefaultAsComputedName2.ts | 19 +++++++ 9 files changed, 331 insertions(+), 3 deletions(-) create mode 100644 tests/baselines/reference/declarationEmitWithDefaultAsComputedName.js create mode 100644 tests/baselines/reference/declarationEmitWithDefaultAsComputedName.symbols create mode 100644 tests/baselines/reference/declarationEmitWithDefaultAsComputedName.types create mode 100644 tests/baselines/reference/declarationEmitWithDefaultAsComputedName2.js create mode 100644 tests/baselines/reference/declarationEmitWithDefaultAsComputedName2.symbols create mode 100644 tests/baselines/reference/declarationEmitWithDefaultAsComputedName2.types create mode 100644 tests/cases/compiler/declarationEmitWithDefaultAsComputedName.ts create mode 100644 tests/cases/compiler/declarationEmitWithDefaultAsComputedName2.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index a847e68e1af..b7e6123a19e 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -3533,9 +3533,13 @@ namespace ts { context.enclosingDeclaration = undefined; if (getCheckFlags(propertySymbol) & CheckFlags.Late) { const decl = first(propertySymbol.declarations); - const name = hasLateBindableName(decl) && resolveEntityName(decl.name.expression, SymbolFlags.Value); - if (name && context.tracker.trackSymbol) { - context.tracker.trackSymbol(name, saveEnclosingDeclaration, SymbolFlags.Value); + if (context.tracker.trackSymbol && hasLateBindableName(decl)) { + // get symbol of the first identifier of the entityName + const firstIdentifier = getFirstIdentifier(decl.name.expression); + const name = resolveName(firstIdentifier, firstIdentifier.escapedText, SymbolFlags.Value | SymbolFlags.ExportValue, /*nodeNotFoundErrorMessage*/ undefined, /*nameArg*/ undefined, /*isUse*/ true); + if (name) { + context.tracker.trackSymbol(name, saveEnclosingDeclaration, SymbolFlags.Value); + } } } const propertyName = symbolToName(propertySymbol, context, SymbolFlags.Value, /*expectsIdentifier*/ true); diff --git a/tests/baselines/reference/declarationEmitWithDefaultAsComputedName.js b/tests/baselines/reference/declarationEmitWithDefaultAsComputedName.js new file mode 100644 index 00000000000..ae45c09438f --- /dev/null +++ b/tests/baselines/reference/declarationEmitWithDefaultAsComputedName.js @@ -0,0 +1,46 @@ +//// [tests/cases/compiler/declarationEmitWithDefaultAsComputedName.ts] //// + +//// [other.ts] +type Experiment = { + name: Name; +}; +declare const createExperiment: ( + options: Experiment +) => Experiment; +export default createExperiment({ + name: "foo" +}); + +//// [main.ts] +import other from "./other"; +export const obj = { + [other.name]: 1, +}; + +//// [other.js] +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.default = createExperiment({ + name: "foo" +}); +//// [main.js] +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +var _a; +var other_1 = require("./other"); +exports.obj = (_a = {}, + _a[other_1.default.name] = 1, + _a); + + +//// [other.d.ts] +declare type Experiment = { + name: Name; +}; +declare const _default: Experiment<"foo">; +export default _default; +//// [main.d.ts] +import other from "./other"; +export declare const obj: { + [other.name]: number; +}; diff --git a/tests/baselines/reference/declarationEmitWithDefaultAsComputedName.symbols b/tests/baselines/reference/declarationEmitWithDefaultAsComputedName.symbols new file mode 100644 index 00000000000..50d91d4c603 --- /dev/null +++ b/tests/baselines/reference/declarationEmitWithDefaultAsComputedName.symbols @@ -0,0 +1,45 @@ +=== tests/cases/compiler/other.ts === +type Experiment = { +>Experiment : Symbol(Experiment, Decl(other.ts, 0, 0)) +>Name : Symbol(Name, Decl(other.ts, 0, 16)) + + name: Name; +>name : Symbol(name, Decl(other.ts, 0, 25)) +>Name : Symbol(Name, Decl(other.ts, 0, 16)) + +}; +declare const createExperiment: ( +>createExperiment : Symbol(createExperiment, Decl(other.ts, 3, 13)) +>Name : Symbol(Name, Decl(other.ts, 3, 33)) + + options: Experiment +>options : Symbol(options, Decl(other.ts, 3, 54)) +>Experiment : Symbol(Experiment, Decl(other.ts, 0, 0)) +>Name : Symbol(Name, Decl(other.ts, 3, 33)) + +) => Experiment; +>Experiment : Symbol(Experiment, Decl(other.ts, 0, 0)) +>Name : Symbol(Name, Decl(other.ts, 3, 33)) + +export default createExperiment({ +>createExperiment : Symbol(createExperiment, Decl(other.ts, 3, 13)) + + name: "foo" +>name : Symbol(name, Decl(other.ts, 6, 33)) + +}); + +=== tests/cases/compiler/main.ts === +import other from "./other"; +>other : Symbol(other, Decl(main.ts, 0, 6)) + +export const obj = { +>obj : Symbol(obj, Decl(main.ts, 1, 12)) + + [other.name]: 1, +>[other.name] : Symbol([other.name], Decl(main.ts, 1, 20)) +>other.name : Symbol(name, Decl(other.ts, 0, 25)) +>other : Symbol(other, Decl(main.ts, 0, 6)) +>name : Symbol(name, Decl(other.ts, 0, 25)) + +}; diff --git a/tests/baselines/reference/declarationEmitWithDefaultAsComputedName.types b/tests/baselines/reference/declarationEmitWithDefaultAsComputedName.types new file mode 100644 index 00000000000..ef11f7c2f59 --- /dev/null +++ b/tests/baselines/reference/declarationEmitWithDefaultAsComputedName.types @@ -0,0 +1,50 @@ +=== tests/cases/compiler/other.ts === +type Experiment = { +>Experiment : Experiment +>Name : Name + + name: Name; +>name : Name +>Name : Name + +}; +declare const createExperiment: ( +>createExperiment : (options: Experiment) => Experiment +>Name : Name + + options: Experiment +>options : Experiment +>Experiment : Experiment +>Name : Name + +) => Experiment; +>Experiment : Experiment +>Name : Name + +export default createExperiment({ +>createExperiment({ name: "foo"}) : Experiment<"foo"> +>createExperiment : (options: Experiment) => Experiment +>{ name: "foo"} : { name: "foo"; } + + name: "foo" +>name : "foo" +>"foo" : "foo" + +}); + +=== tests/cases/compiler/main.ts === +import other from "./other"; +>other : { name: "foo"; } + +export const obj = { +>obj : { [other.name]: number; } +>{ [other.name]: 1,} : { [other.name]: number; } + + [other.name]: 1, +>[other.name] : number +>other.name : "foo" +>other : { name: "foo"; } +>name : "foo" +>1 : 1 + +}; diff --git a/tests/baselines/reference/declarationEmitWithDefaultAsComputedName2.js b/tests/baselines/reference/declarationEmitWithDefaultAsComputedName2.js new file mode 100644 index 00000000000..ddf89f2d57d --- /dev/null +++ b/tests/baselines/reference/declarationEmitWithDefaultAsComputedName2.js @@ -0,0 +1,46 @@ +//// [tests/cases/compiler/declarationEmitWithDefaultAsComputedName2.ts] //// + +//// [other.ts] +type Experiment = { + name: Name; +}; +declare const createExperiment: ( + options: Experiment +) => Experiment; +export default createExperiment({ + name: "foo" +}); + +//// [main.ts] +import * as other2 from "./other"; +export const obj = { + [other2.default.name]: 1 +}; + +//// [other.js] +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.default = createExperiment({ + name: "foo" +}); +//// [main.js] +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +var _a; +var other2 = require("./other"); +exports.obj = (_a = {}, + _a[other2.default.name] = 1, + _a); + + +//// [other.d.ts] +declare type Experiment = { + name: Name; +}; +declare const _default: Experiment<"foo">; +export default _default; +//// [main.d.ts] +import * as other2 from "./other"; +export declare const obj: { + [other2.default.name]: number; +}; diff --git a/tests/baselines/reference/declarationEmitWithDefaultAsComputedName2.symbols b/tests/baselines/reference/declarationEmitWithDefaultAsComputedName2.symbols new file mode 100644 index 00000000000..3f4be4d8b43 --- /dev/null +++ b/tests/baselines/reference/declarationEmitWithDefaultAsComputedName2.symbols @@ -0,0 +1,47 @@ +=== tests/cases/compiler/other.ts === +type Experiment = { +>Experiment : Symbol(Experiment, Decl(other.ts, 0, 0)) +>Name : Symbol(Name, Decl(other.ts, 0, 16)) + + name: Name; +>name : Symbol(name, Decl(other.ts, 0, 25)) +>Name : Symbol(Name, Decl(other.ts, 0, 16)) + +}; +declare const createExperiment: ( +>createExperiment : Symbol(createExperiment, Decl(other.ts, 3, 13)) +>Name : Symbol(Name, Decl(other.ts, 3, 33)) + + options: Experiment +>options : Symbol(options, Decl(other.ts, 3, 54)) +>Experiment : Symbol(Experiment, Decl(other.ts, 0, 0)) +>Name : Symbol(Name, Decl(other.ts, 3, 33)) + +) => Experiment; +>Experiment : Symbol(Experiment, Decl(other.ts, 0, 0)) +>Name : Symbol(Name, Decl(other.ts, 3, 33)) + +export default createExperiment({ +>createExperiment : Symbol(createExperiment, Decl(other.ts, 3, 13)) + + name: "foo" +>name : Symbol(name, Decl(other.ts, 6, 33)) + +}); + +=== tests/cases/compiler/main.ts === +import * as other2 from "./other"; +>other2 : Symbol(other2, Decl(main.ts, 0, 6)) + +export const obj = { +>obj : Symbol(obj, Decl(main.ts, 1, 12)) + + [other2.default.name]: 1 +>[other2.default.name] : Symbol([other2.default.name], Decl(main.ts, 1, 20)) +>other2.default.name : Symbol(name, Decl(other.ts, 0, 25)) +>other2.default : Symbol(other2.default, Decl(other.ts, 5, 22)) +>other2 : Symbol(other2, Decl(main.ts, 0, 6)) +>default : Symbol(other2.default, Decl(other.ts, 5, 22)) +>name : Symbol(name, Decl(other.ts, 0, 25)) + +}; diff --git a/tests/baselines/reference/declarationEmitWithDefaultAsComputedName2.types b/tests/baselines/reference/declarationEmitWithDefaultAsComputedName2.types new file mode 100644 index 00000000000..a507034378b --- /dev/null +++ b/tests/baselines/reference/declarationEmitWithDefaultAsComputedName2.types @@ -0,0 +1,52 @@ +=== tests/cases/compiler/other.ts === +type Experiment = { +>Experiment : Experiment +>Name : Name + + name: Name; +>name : Name +>Name : Name + +}; +declare const createExperiment: ( +>createExperiment : (options: Experiment) => Experiment +>Name : Name + + options: Experiment +>options : Experiment +>Experiment : Experiment +>Name : Name + +) => Experiment; +>Experiment : Experiment +>Name : Name + +export default createExperiment({ +>createExperiment({ name: "foo"}) : Experiment<"foo"> +>createExperiment : (options: Experiment) => Experiment +>{ name: "foo"} : { name: "foo"; } + + name: "foo" +>name : "foo" +>"foo" : "foo" + +}); + +=== tests/cases/compiler/main.ts === +import * as other2 from "./other"; +>other2 : typeof other2 + +export const obj = { +>obj : { [other2.default.name]: number; } +>{ [other2.default.name]: 1} : { [other2.default.name]: number; } + + [other2.default.name]: 1 +>[other2.default.name] : number +>other2.default.name : "foo" +>other2.default : { name: "foo"; } +>other2 : typeof other2 +>default : { name: "foo"; } +>name : "foo" +>1 : 1 + +}; diff --git a/tests/cases/compiler/declarationEmitWithDefaultAsComputedName.ts b/tests/cases/compiler/declarationEmitWithDefaultAsComputedName.ts new file mode 100644 index 00000000000..868bad44847 --- /dev/null +++ b/tests/cases/compiler/declarationEmitWithDefaultAsComputedName.ts @@ -0,0 +1,19 @@ +// @declaration: true +// @target: es5 + +// @filename: other.ts +type Experiment = { + name: Name; +}; +declare const createExperiment: ( + options: Experiment +) => Experiment; +export default createExperiment({ + name: "foo" +}); + +// @filename: main.ts +import other from "./other"; +export const obj = { + [other.name]: 1, +}; \ No newline at end of file diff --git a/tests/cases/compiler/declarationEmitWithDefaultAsComputedName2.ts b/tests/cases/compiler/declarationEmitWithDefaultAsComputedName2.ts new file mode 100644 index 00000000000..8feeaa2fa61 --- /dev/null +++ b/tests/cases/compiler/declarationEmitWithDefaultAsComputedName2.ts @@ -0,0 +1,19 @@ +// @declaration: true +// @target: es5 + +// @filename: other.ts +type Experiment = { + name: Name; +}; +declare const createExperiment: ( + options: Experiment +) => Experiment; +export default createExperiment({ + name: "foo" +}); + +// @filename: main.ts +import * as other2 from "./other"; +export const obj = { + [other2.default.name]: 1 +}; \ No newline at end of file From 394da3e46d38486db346de70f0d2ba1297439889 Mon Sep 17 00:00:00 2001 From: csigs Date: Fri, 8 Jun 2018 22:10:46 +0000 Subject: [PATCH 76/81] LEGO: check in for master to temporary branch. --- .../diagnosticMessages.generated.json.lcl | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/loc/lcl/ita/diagnosticMessages/diagnosticMessages.generated.json.lcl b/src/loc/lcl/ita/diagnosticMessages/diagnosticMessages.generated.json.lcl index f542f06a4e4..5c8c80e51d2 100644 --- a/src/loc/lcl/ita/diagnosticMessages/diagnosticMessages.generated.json.lcl +++ b/src/loc/lcl/ita/diagnosticMessages/diagnosticMessages.generated.json.lcl @@ -1935,6 +1935,24 @@ + + + + + + + + + + + + + + + + + + From e821d613a18fe4801a45cccfe7f0432f6cdf205f Mon Sep 17 00:00:00 2001 From: Andy Date: Fri, 8 Jun 2018 15:20:33 -0700 Subject: [PATCH 77/81] fixUnusedIdentifier: Remove unused writes (#24805) --- src/services/codefixes/fixUnusedIdentifier.ts | 10 ++++++++ .../codeFixUnusedIdentifier_deleteWrite.ts | 24 +++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 tests/cases/fourslash/codeFixUnusedIdentifier_deleteWrite.ts diff --git a/src/services/codefixes/fixUnusedIdentifier.ts b/src/services/codefixes/fixUnusedIdentifier.ts index ebe65197c7f..719e3a316f7 100644 --- a/src/services/codefixes/fixUnusedIdentifier.ts +++ b/src/services/codefixes/fixUnusedIdentifier.ts @@ -152,6 +152,7 @@ namespace ts.codefix { switch (token.kind) { case SyntaxKind.Identifier: tryDeleteIdentifier(changes, sourceFile, token, deletedAncestors, checker, isFixAll); + deleteAssignments(changes, sourceFile, token as Identifier, checker); break; case SyntaxKind.PropertyDeclaration: case SyntaxKind.NamespaceImport: @@ -163,6 +164,15 @@ namespace ts.codefix { } } + function deleteAssignments(changes: textChanges.ChangeTracker, sourceFile: SourceFile, token: Identifier, checker: TypeChecker) { + FindAllReferences.Core.eachSymbolReferenceInFile(token, checker, sourceFile, (ref: Node) => { + if (ref.parent.kind === SyntaxKind.PropertyAccessExpression) ref = ref.parent; + if (ref.parent.kind === SyntaxKind.BinaryExpression && ref.parent.parent.kind === SyntaxKind.ExpressionStatement) { + changes.deleteNode(sourceFile, ref.parent.parent); + } + }); + } + function tryDeleteDefault(changes: textChanges.ChangeTracker, sourceFile: SourceFile, token: Node, deletedAncestors: NodeSet | undefined): void { if (isDeclarationName(token)) { if (deletedAncestors) deletedAncestors.add(token.parent); diff --git a/tests/cases/fourslash/codeFixUnusedIdentifier_deleteWrite.ts b/tests/cases/fourslash/codeFixUnusedIdentifier_deleteWrite.ts new file mode 100644 index 00000000000..61ff1211c7c --- /dev/null +++ b/tests/cases/fourslash/codeFixUnusedIdentifier_deleteWrite.ts @@ -0,0 +1,24 @@ +/// + +// @noLib: true +// @noUnusedLocals: true + +////let x = 0; +////x = 1; +//// +////export class C { +//// private p: number; +//// +//// m() { this.p = 0; } +////} + +verify.codeFixAll({ + fixId: "unusedIdentifier_delete", + fixAllDescription: "Delete all unused declarations", + newFileContent: +` +export class C { + + m() { } +}`, +}); From 04187bde8de24f1913099ce04cf22dbb8d20e59c Mon Sep 17 00:00:00 2001 From: Andy Date: Fri, 8 Jun 2018 15:56:56 -0700 Subject: [PATCH 78/81] fixStrictClassInitialization: Support array initializer (#24810) --- src/compiler/types.ts | 2 +- src/services/codefixes/fixStrictClassInitialization.ts | 3 +++ .../fourslash/codeFixClassPropertyInitialization_all_3.ts | 4 ++++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 726947111be..32c6faea37c 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -3056,12 +3056,12 @@ namespace ts { /* @internal */ getSymbolCount(): number; /* @internal */ getTypeCount(): number; + /* @internal */ isArrayLikeType(type: Type): boolean; /** * For a union, will include a property if it's defined in *any* of the member types. * So for `{ a } | { b }`, this will include both `a` and `b`. * Does not include properties of primitive types. */ - /* @internal */ isArrayLikeType(type: Type): boolean; /* @internal */ getAllPossiblePropertiesOfTypes(type: ReadonlyArray): Symbol[]; /* @internal */ resolveName(name: string, location: Node, meaning: SymbolFlags, excludeGlobals: boolean): Symbol | undefined; /* @internal */ getJsxNamespace(location?: Node): string; diff --git a/src/services/codefixes/fixStrictClassInitialization.ts b/src/services/codefixes/fixStrictClassInitialization.ts index 278d1e2d3f6..f52ebadc851 100644 --- a/src/services/codefixes/fixStrictClassInitialization.ts +++ b/src/services/codefixes/fixStrictClassInitialization.ts @@ -127,6 +127,9 @@ namespace ts.codefix { return createNew(createIdentifier(type.symbol.name), /*typeArguments*/ undefined, /*argumentsArray*/ undefined); } + else if (checker.isArrayLikeType(type)) { + return createArrayLiteral(); + } return undefined; } } diff --git a/tests/cases/fourslash/codeFixClassPropertyInitialization_all_3.ts b/tests/cases/fourslash/codeFixClassPropertyInitialization_all_3.ts index 29a9fe19b15..08584a01f91 100644 --- a/tests/cases/fourslash/codeFixClassPropertyInitialization_all_3.ts +++ b/tests/cases/fourslash/codeFixClassPropertyInitialization_all_3.ts @@ -35,6 +35,8 @@ //// k: AT; //// //// l: Foo; +//// +//// m: number[]; //// } verify.codeFixAll({ @@ -73,5 +75,7 @@ class T { k: AT = new AT; l: Foo = new Foo; + + m: number[] = []; }` }); \ No newline at end of file From 1de2f839f2f39f8fb6c185a2f4cb5be9173c2e34 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Fri, 8 Jun 2018 17:43:16 -0700 Subject: [PATCH 79/81] PR fixups --- src/compiler/tsbuild.ts | 174 +++++++++++++++---------------- src/compiler/tsc.ts | 10 +- src/harness/unittests/tsbuild.ts | 29 ++++-- 3 files changed, 110 insertions(+), 103 deletions(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index c041540cdfe..606fc1a27f6 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -9,6 +9,13 @@ namespace ts { const minimumDate = new Date(-8640000000000000); const maximumDate = new Date(8640000000000000); + export interface BuildHost { + verbose(diag: DiagnosticMessage, ...args: string[]): void; + error(diag: DiagnosticMessage, ...args: string[]): void; + errorDiagnostic(diag: Diagnostic): void; + message(diag: DiagnosticMessage, ...args: string[]): void; + } + /** * A BuildContext tracks what's going on during the course of a build. * @@ -34,11 +41,6 @@ namespace ts { */ projectStatus: FileMap; - /** - * Issue a verbose diagnostic message. No-ops when options.verbose is false. - */ - verbose(diag: DiagnosticMessage, ...args: any[]): void; - invalidatedProjects: FileMap; queuedProjects: FileMap; missingRoots: Map; @@ -371,9 +373,7 @@ namespace ts { return fileExtensionIs(fileName, ".d.ts"); } - export function createBuildContext(options: BuildOptions, reportDiagnostic: DiagnosticReporter): BuildContext { - const verboseDiag = options.verbose && reportDiagnostic; - + export function createBuildContext(options: BuildOptions): BuildContext { const invalidatedProjects = createFileMap(); const queuedProjects = createFileMap(); const missingRoots = createMap(); @@ -382,7 +382,6 @@ namespace ts { options, projectStatus: createFileMap(), unchangedOutputs: createFileMap(), - verbose: verboseDiag ? (diag, ...args) => verboseDiag(createCompilerDiagnostic(diag, ...args)) : () => undefined, invalidatedProjects, missingRoots, queuedProjects @@ -425,7 +424,7 @@ namespace ts { } ]; - export function performBuild(host: CompilerHost, reportDiagnostic: DiagnosticReporter, args: string[], system?: System) { + export function performBuild(args: string[], compilerHost: CompilerHost, buildHost: BuildHost, system?: System) { let verbose = false; let dry = false; let force = false; @@ -466,20 +465,16 @@ namespace ts { // Nonsensical combinations if (clean && force) { - reportDiagnostic(createCompilerDiagnostic(Diagnostics.Options_0_and_1_cannot_be_combined, "clean", "force")); - return; + return buildHost.error(Diagnostics.Options_0_and_1_cannot_be_combined, "clean", "force"); } if (clean && verbose) { - reportDiagnostic(createCompilerDiagnostic(Diagnostics.Options_0_and_1_cannot_be_combined, "clean", "verbose")); - return; + return buildHost.error(Diagnostics.Options_0_and_1_cannot_be_combined, "clean", "verbose"); } if (clean && watch) { - reportDiagnostic(createCompilerDiagnostic(Diagnostics.Options_0_and_1_cannot_be_combined, "clean", "watch")); - return; + return buildHost.error(Diagnostics.Options_0_and_1_cannot_be_combined, "clean", "watch"); } if (watch && dry) { - reportDiagnostic(createCompilerDiagnostic(Diagnostics.Options_0_and_1_cannot_be_combined, "watch", "dry")); - return; + return buildHost.error(Diagnostics.Options_0_and_1_cannot_be_combined, "watch", "dry"); } if (projects.length === 0) { @@ -487,7 +482,7 @@ namespace ts { addProject("."); } - const builder = createSolutionBuilder(host, projects, reportDiagnostic, { verbose, dry, force }, system); + const builder = createSolutionBuilder(compilerHost, buildHost, projects, { dry, force, verbose }, system); if (clean) { builder.cleanAllProjects(); } @@ -500,15 +495,14 @@ namespace ts { } function addProject(projectSpecification: string) { - const fileName = resolvePath(host.getCurrentDirectory(), projectSpecification); - const refPath = resolveProjectReferencePath(host, { path: fileName }); + const fileName = resolvePath(compilerHost.getCurrentDirectory(), projectSpecification); + const refPath = resolveProjectReferencePath(compilerHost, { path: fileName }); if (!refPath) { - reportDiagnostic(createCompilerDiagnostic(Diagnostics.File_0_does_not_exist, projectSpecification)); - return; + return buildHost.error(Diagnostics.File_0_does_not_exist, projectSpecification); } - if (!host.fileExists(refPath)) { - reportDiagnostic(createCompilerDiagnostic(Diagnostics.File_0_does_not_exist, fileName)); + if (!compilerHost.fileExists(refPath)) { + return buildHost.error(Diagnostics.File_0_does_not_exist, fileName); } projects.push(refPath); @@ -519,13 +513,13 @@ namespace ts { * A SolutionBuilder has an immutable set of rootNames that are the "entry point" projects, but * can dynamically add/remove other projects based on changes on the rootNames' references */ - export function createSolutionBuilder(host: CompilerHost, rootNames: ReadonlyArray, reportDiagnostic: DiagnosticReporter, defaultOptions: BuildOptions, system?: System) { - if (!host.getModifiedTime || !host.setModifiedTime) { + export function createSolutionBuilder(compilerHost: CompilerHost, buildHost: BuildHost, rootNames: ReadonlyArray, defaultOptions: BuildOptions, system?: System) { + if (!compilerHost.getModifiedTime || !compilerHost.setModifiedTime) { throw new Error("Host must support timestamp APIs"); } - const configFileCache = createConfigFileCache(host); - let context = createBuildContext(defaultOptions, reportDiagnostic); + const configFileCache = createConfigFileCache(compilerHost); + let context = createBuildContext(defaultOptions); const existingWatchersForWildcards = createMap(); @@ -560,7 +554,7 @@ namespace ts { const cfg = configFileCache.parseConfigFile(resolved); if (cfg) { // Watch this file - system.watchFile!(resolved, () => { + system.watchFile(resolved, () => { configFileCache.removeKey(resolved); invalidateProjectAndScheduleBuilds(resolved); }); @@ -573,7 +567,7 @@ namespace ts { }, !!(flags & WatchDirectoryFlags.Recursive)); }); } - + // Watch input files for (const input of cfg.fileNames) { system.watchFile(input, () => { @@ -591,7 +585,7 @@ namespace ts { } function resetBuildContext(opts = defaultOptions) { - context = createBuildContext(opts, reportDiagnostic); + context = createBuildContext(opts); } function getUpToDateStatusOfFile(configFileName: ResolvedConfigFileName): UpToDateStatus { @@ -679,10 +673,10 @@ namespace ts { if (!proj) continue; // ? const status = getUpToDateStatus(proj); - reportProjectStatus(next, status); + verboseReportProjectStatus(next, status); if (status.type === UpToDateStatusType.UpstreamBlocked) { - context.verbose(Diagnostics.Skipping_build_of_project_0_because_its_dependency_1_has_errors, resolved, status.upstreamProjectName); + if (context.options.verbose) buildHost.verbose(Diagnostics.Skipping_build_of_project_0_because_its_dependency_1_has_errors, resolved, status.upstreamProjectName); continue; } @@ -708,14 +702,14 @@ namespace ts { let newestInputFileTime = minimumDate; // Get timestamps of input files for (const inputFile of project.fileNames) { - if (!host.fileExists(inputFile)) { + if (!compilerHost.fileExists(inputFile)) { return { type: UpToDateStatusType.Unbuildable, reason: `${inputFile} does not exist` }; } - const inputTime = host.getModifiedTime!(inputFile); + const inputTime = compilerHost.getModifiedTime!(inputFile); if (inputTime > newestInputFileTime) { newestInputFileName = inputFile; newestInputFileTime = inputTime; @@ -742,12 +736,12 @@ namespace ts { for (const output of outputs) { // Output is missing; can stop checking // Don't immediately return because we can still be upstream-blocked, which is a higher-priority status - if (!host.fileExists(output)) { + if (!compilerHost.fileExists(output)) { missingOutputFileName = output; break; } - const outputTime = host.getModifiedTime!(output); + const outputTime = compilerHost.getModifiedTime!(output); if (outputTime < oldestOutputFileTime) { oldestOutputFileTime = outputTime; oldestOutputFileName = output; @@ -775,7 +769,7 @@ namespace ts { newestDeclarationFileContentChangedTime = newer(unchangedTime, newestDeclarationFileContentChangedTime); } else { - newestDeclarationFileContentChangedTime = newer(newestDeclarationFileContentChangedTime, host.getModifiedTime!(output)); + newestDeclarationFileContentChangedTime = newer(newestDeclarationFileContentChangedTime, compilerHost.getModifiedTime!(output)); } } } @@ -783,7 +777,7 @@ namespace ts { let pseudoUpToDate = false; if (project.projectReferences) { for (const ref of project.projectReferences) { - const resolvedRef = resolveProjectReferencePath(host, ref) as ResolvedConfigFileName; + const resolvedRef = resolveProjectReferencePath(compilerHost, ref) as ResolvedConfigFileName; const refStatus = getUpToDateStatus(configFileCache.parseConfigFile(resolvedRef)); // An upstream project is blocked @@ -881,7 +875,7 @@ namespace ts { if (temporaryMarks[projPath]) { if (!inCircularContext) { hadError = true; - reportDiagnostic(createCompilerDiagnostic(Diagnostics.Project_references_may_not_form_a_circular_graph_Cycle_detected_Colon_0, circularityReportStack.join("\r\n"))); + buildHost.error(Diagnostics.Project_references_may_not_form_a_circular_graph_Cycle_detected_Colon_0, circularityReportStack.join("\r\n")); return; } } @@ -913,11 +907,11 @@ namespace ts { function buildSingleProject(proj: ResolvedConfigFileName): BuildResultFlags { if (context.options.dry) { - reportDiagnostic(createCompilerDiagnostic(Diagnostics.A_non_dry_build_would_build_project_0, proj)); + buildHost.message(Diagnostics.A_non_dry_build_would_build_project_0, proj); return BuildResultFlags.Success; } - context.verbose(Diagnostics.Building_project_0, proj); + if (context.options.verbose) buildHost.verbose(Diagnostics.Building_project_0, proj); let resultFlags = BuildResultFlags.None; resultFlags |= BuildResultFlags.DeclarationOutputUnchanged; @@ -937,7 +931,7 @@ namespace ts { const programOptions: CreateProgramOptions = { projectReferences: configFile.projectReferences, - host, + host: compilerHost, rootNames: configFile.fileNames, options: configFile.options }; @@ -948,7 +942,7 @@ namespace ts { if (syntaxDiagnostics.length) { resultFlags |= BuildResultFlags.SyntaxErrors; for (const diag of syntaxDiagnostics) { - reportDiagnostic(diag); + buildHost.errorDiagnostic(diag); } context.projectStatus.setValue(proj, { type: UpToDateStatusType.Unbuildable, reason: "Syntactic errors" }); return resultFlags; @@ -960,18 +954,19 @@ namespace ts { if (declDiagnostics.length) { resultFlags |= BuildResultFlags.DeclarationEmitErrors; for (const diag of declDiagnostics) { - reportDiagnostic(diag); + buildHost.errorDiagnostic(diag); } context.projectStatus.setValue(proj, { type: UpToDateStatusType.Unbuildable, reason: "Declaration file errors" }); return resultFlags; } } - const semanticDiagnostics = [...program.getSemanticDiagnostics()]; + // Same as above but now for semantic diagnostics + const semanticDiagnostics = program.getSemanticDiagnostics(); if (semanticDiagnostics.length) { resultFlags |= BuildResultFlags.TypeErrors; for (const diag of semanticDiagnostics) { - reportDiagnostic(diag); + buildHost.errorDiagnostic(diag); } context.projectStatus.setValue(proj, { type: UpToDateStatusType.Unbuildable, reason: "Semantic errors" }); return resultFlags; @@ -981,15 +976,15 @@ namespace ts { program.emit(/*targetSourceFile*/ undefined, (fileName, content, writeBom, onError) => { let priorChangeTime: Date | undefined; - if (isDeclarationFile(fileName) && host.fileExists(fileName)) { - if (host.readFile(fileName) === content) { + if (isDeclarationFile(fileName) && compilerHost.fileExists(fileName)) { + if (compilerHost.readFile(fileName) === content) { // Check for unchanged .d.ts files resultFlags &= ~BuildResultFlags.DeclarationOutputUnchanged; - priorChangeTime = host.getModifiedTime && host.getModifiedTime(fileName); + priorChangeTime = compilerHost.getModifiedTime && compilerHost.getModifiedTime(fileName); } } - host.writeFile(fileName, content, writeBom, onError, emptyArray); + compilerHost.writeFile(fileName, content, writeBom, onError, emptyArray); if (priorChangeTime !== undefined) { newestDeclarationFileContentChangedTime = newer(priorChangeTime, newestDeclarationFileContentChangedTime); context.unchangedOutputs.setValue(fileName, priorChangeTime); @@ -1002,19 +997,18 @@ namespace ts { function updateOutputTimestamps(proj: ParsedCommandLine) { if (context.options.dry) { - reportDiagnostic(createCompilerDiagnostic(Diagnostics.A_non_dry_build_would_build_project_0, proj.options.configFilePath)); - return; + return buildHost.message(Diagnostics.A_non_dry_build_would_build_project_0, proj.options.configFilePath!); } - context.verbose(Diagnostics.Updating_output_timestamps_of_project_0, proj.options.configFilePath); + if (context.options.verbose) buildHost.verbose(Diagnostics.Updating_output_timestamps_of_project_0, proj.options.configFilePath!); const now = new Date(); const outputs = getAllProjectOutputs(proj); let priorNewestUpdateTime = minimumDate; for (const file of outputs) { if (isDeclarationFile(file)) { - priorNewestUpdateTime = newer(priorNewestUpdateTime, host.getModifiedTime!(file)); + priorNewestUpdateTime = newer(priorNewestUpdateTime, compilerHost.getModifiedTime!(file)); } - host.setModifiedTime!(file, now); + compilerHost.setModifiedTime!(file, now); } context.projectStatus.setValue(proj.options.configFilePath!, { type: UpToDateStatusType.UpToDate, newestDeclarationFileContentChangedTime: priorNewestUpdateTime } as UpToDateStatus); @@ -1037,7 +1031,7 @@ namespace ts { } const outputs = getAllProjectOutputs(parsed); for (const output of outputs) { - if (host.fileExists(output)) { + if (compilerHost.fileExists(output)) { filesToDelete.push(output); } } @@ -1056,40 +1050,38 @@ namespace ts { function cleanAllProjects() { const resolvedNames: ReadonlyArray | undefined = getAllProjectsInScope(); if (resolvedNames === undefined) { - reportDiagnostic(createCompilerDiagnostic(Diagnostics.Skipping_clean_because_not_all_projects_could_be_located)); - return; + return buildHost.message(Diagnostics.Skipping_clean_because_not_all_projects_could_be_located); } const filesToDelete = getFilesToClean(resolvedNames); if (filesToDelete === undefined) { - reportDiagnostic(createCompilerDiagnostic(Diagnostics.Skipping_clean_because_not_all_projects_could_be_located)); - return; + return buildHost.message(Diagnostics.Skipping_clean_because_not_all_projects_could_be_located); } if (context.options.dry) { - reportDiagnostic(createCompilerDiagnostic(Diagnostics.A_non_dry_build_would_delete_the_following_files_Colon_0, filesToDelete.map(f => `\r\n * ${f}`).join(""))); + return buildHost.message(Diagnostics.A_non_dry_build_would_delete_the_following_files_Colon_0, filesToDelete.map(f => `\r\n * ${f}`).join("")); } - else { - if (!host.deleteFile) { - throw new Error("Host does not support deleting files"); - } - for (const output of filesToDelete) { - host.deleteFile(output); - } + // Do this check later to allow --clean --dry to function even if the host can't delete files + if (!compilerHost.deleteFile) { + throw new Error("Host does not support deleting files"); + } + + for (const output of filesToDelete) { + compilerHost.deleteFile(output); } } function resolveProjectName(name: string): ResolvedConfigFileName | undefined { - let fullPath = resolvePath(host.getCurrentDirectory(), name); - if (host.fileExists(fullPath)) { + const fullPath = resolvePath(compilerHost.getCurrentDirectory(), name); + if (compilerHost.fileExists(fullPath)) { return fullPath as ResolvedConfigFileName; } - fullPath = combinePaths(fullPath, "tsconfig.json"); - if (host.fileExists(fullPath)) { - return fullPath as ResolvedConfigFileName; + const fullPathWithTsconfig = combinePaths(fullPath, "tsconfig.json"); + if (compilerHost.fileExists(fullPathWithTsconfig)) { + return fullPathWithTsconfig as ResolvedConfigFileName; } - reportDiagnostic(createCompilerDiagnostic(Diagnostics.File_0_not_found, fullPath)); + buildHost.error(Diagnostics.File_0_not_found, relName(fullPath)); return undefined; } @@ -1118,14 +1110,14 @@ namespace ts { break; } const status = getUpToDateStatus(proj); - reportProjectStatus(next, status); + verboseReportProjectStatus(next, status); - const projName = proj.options.configFilePath; + const projName = proj.options.configFilePath!; if (status.type === UpToDateStatusType.UpToDate && !context.options.force) { // Up to date, skip if (defaultOptions.dry) { // In a dry build, inform the user of this fact - reportDiagnostic(createCompilerDiagnostic(Diagnostics.Project_0_is_up_to_date, projName)); + buildHost.message(Diagnostics.Project_0_is_up_to_date, projName); } continue; } @@ -1137,7 +1129,7 @@ namespace ts { } if (status.type === UpToDateStatusType.UpstreamBlocked) { - context.verbose(Diagnostics.Skipping_build_of_project_0_because_its_dependency_1_has_errors, projName, status.upstreamProjectName); + if (context.options.verbose) buildHost.verbose(Diagnostics.Skipping_build_of_project_0_because_its_dependency_1_has_errors, projName, status.upstreamProjectName); continue; } @@ -1160,36 +1152,36 @@ namespace ts { for (const name of graph.buildQueue) { names.push(name); } - context.verbose(Diagnostics.Projects_in_this_build_Colon_0, names.map(s => "\r\n * " + relName(s)).join("")); + if (context.options.verbose) buildHost.verbose(Diagnostics.Projects_in_this_build_Colon_0, names.map(s => "\r\n * " + relName(s)).join("")); } function relName(path: string): string { - return convertToRelativePath(path, host.getCurrentDirectory(), f => host.getCanonicalFileName(f)); + return convertToRelativePath(path, compilerHost.getCurrentDirectory(), f => compilerHost.getCanonicalFileName(f)); } /** * Report the up-to-date status of a project if we're in verbose mode */ - function reportProjectStatus(configFileName: string, status: UpToDateStatus) { + function verboseReportProjectStatus(configFileName: string, status: UpToDateStatus) { if (!context.options.verbose) return; switch (status.type) { case UpToDateStatusType.OutOfDateWithSelf: - return context.verbose(Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, + return buildHost.verbose(Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, relName(configFileName), relName(status.outOfDateOutputFileName), relName(status.newerInputFileName)); case UpToDateStatusType.OutOfDateWithUpstream: - return context.verbose(Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, + return buildHost.verbose(Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, relName(configFileName), relName(status.outOfDateOutputFileName), relName(status.newerProjectName)); case UpToDateStatusType.OutputMissing: - return context.verbose(Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, + return buildHost.verbose(Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, relName(configFileName), relName(status.missingOutputFileName)); case UpToDateStatusType.UpToDate: if (status.newestInputFileTime !== undefined) { - return context.verbose(Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, + return buildHost.verbose(Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, relName(configFileName), relName(status.newestInputFileName), relName(status.oldestOutputFileName)); @@ -1197,18 +1189,18 @@ namespace ts { // Don't report anything for "up to date because it was already built" -- too verbose break; case UpToDateStatusType.UpToDateWithUpstreamTypes: - return context.verbose(Diagnostics.Project_0_is_up_to_date_with_d_ts_files_from_its_dependencies, + return buildHost.verbose(Diagnostics.Project_0_is_up_to_date_with_d_ts_files_from_its_dependencies, relName(configFileName)); case UpToDateStatusType.UpstreamOutOfDate: - return context.verbose(Diagnostics.Project_0_is_out_of_date_because_its_dependency_1_is_out_of_date, + return buildHost.verbose(Diagnostics.Project_0_is_out_of_date_because_its_dependency_1_is_out_of_date, relName(configFileName), relName(status.upstreamProjectName)); case UpToDateStatusType.UpstreamBlocked: - return context.verbose(Diagnostics.Project_0_can_t_be_built_because_its_dependency_1_has_errors, + return buildHost.verbose(Diagnostics.Project_0_can_t_be_built_because_its_dependency_1_has_errors, relName(configFileName), relName(status.upstreamProjectName)); case UpToDateStatusType.Unbuildable: - return context.verbose(Diagnostics.Failed_to_parse_file_0_Colon_1, + return buildHost.verbose(Diagnostics.Failed_to_parse_file_0_Colon_1, relName(configFileName), status.reason); case UpToDateStatusType.ContainerOnly: diff --git a/src/compiler/tsc.ts b/src/compiler/tsc.ts index 149439d9b67..93834a6ea54 100644 --- a/src/compiler/tsc.ts +++ b/src/compiler/tsc.ts @@ -50,7 +50,15 @@ namespace ts { export function executeCommandLine(args: string[]): void { if (args.length > 0 && ((args[0].toLowerCase() === "--build") || (args[0].toLowerCase() === "-b"))) { - return performBuild(createCompilerHost({}), createDiagnosticReporter(sys), args.slice(1), sys); + const reportDiag = createDiagnosticReporter(sys, /*pretty*/ true); + const report = (message: DiagnosticMessage, ...args: string[]) => reportDiag(createCompilerDiagnostic(message, ...args)); + const buildHost: BuildHost = { + error: report, + verbose: report, + message: report, + errorDiagnostic: d => reportDiag(d) + }; + return performBuild(args.slice(1), createCompilerHost({}), buildHost, sys); } const commandLine = parseCommandLine(args); diff --git a/src/harness/unittests/tsbuild.ts b/src/harness/unittests/tsbuild.ts index ff80a5b96af..9d093f61e09 100644 --- a/src/harness/unittests/tsbuild.ts +++ b/src/harness/unittests/tsbuild.ts @@ -2,6 +2,13 @@ namespace ts { let currentTime = 100; let lastDiagnostics: Diagnostic[] = []; const reportDiagnostic: DiagnosticReporter = diagnostic => lastDiagnostics.push(diagnostic); + const report = (message: DiagnosticMessage, ...args: string[]) => reportDiagnostic(createCompilerDiagnostic(message, ...args)); + const buildHost: BuildHost = { + error: report, + verbose: report, + message: report, + errorDiagnostic: d => reportDiagnostic(d) + }; export namespace Sample1 { tick(); @@ -15,7 +22,7 @@ namespace ts { it("can build the sample project 'sample1' without error", () => { const fs = projFs.shadow(); const host = new fakes.CompilerHost(fs); - const builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: false, force: false, verbose: false }); + const builder = createSolutionBuilder(host, buildHost, ["/src/tests"], { dry: false, force: false, verbose: false }); clearDiagnostics(); builder.buildAllProjects(); @@ -33,7 +40,7 @@ namespace ts { clearDiagnostics(); const fs = projFs.shadow(); const host = new fakes.CompilerHost(fs); - const builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: true, force: false, verbose: false }); + const builder = createSolutionBuilder(host, buildHost, ["/src/tests"], { dry: true, force: false, verbose: false }); builder.buildAllProjects(); assertDiagnosticMessages(Diagnostics.A_non_dry_build_would_build_project_0, Diagnostics.A_non_dry_build_would_build_project_0, Diagnostics.A_non_dry_build_would_build_project_0); @@ -48,12 +55,12 @@ namespace ts { const fs = projFs.shadow(); const host = new fakes.CompilerHost(fs); - let builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: false, force: false, verbose: false }); + let builder = createSolutionBuilder(host, buildHost, ["/src/tests"], { dry: false, force: false, verbose: false }); builder.buildAllProjects(); tick(); clearDiagnostics(); - builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: true, force: false, verbose: false }); + builder = createSolutionBuilder(host, buildHost, ["/src/tests"], { dry: true, force: false, verbose: false }); builder.buildAllProjects(); assertDiagnosticMessages(Diagnostics.Project_0_is_up_to_date, Diagnostics.Project_0_is_up_to_date, Diagnostics.Project_0_is_up_to_date); }); @@ -65,7 +72,7 @@ namespace ts { const fs = projFs.shadow(); const host = new fakes.CompilerHost(fs); - const builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: false, force: false, verbose: false }); + const builder = createSolutionBuilder(host, buildHost, ["/src/tests"], { dry: false, force: false, verbose: false }); builder.buildAllProjects(); // Verify they exist for (const output of allExpectedOutputs) { @@ -86,7 +93,7 @@ namespace ts { const fs = projFs.shadow(); const host = new fakes.CompilerHost(fs); - const builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: false, force: true, verbose: false }); + const builder = createSolutionBuilder(host, buildHost, ["/src/tests"], { dry: false, force: true, verbose: false }); builder.buildAllProjects(); let currentTime = time(); checkOutputTimestamps(currentTime); @@ -110,7 +117,7 @@ namespace ts { describe("tsbuild - can detect when and what to rebuild", () => { const fs = projFs.shadow(); const host = new fakes.CompilerHost(fs); - const builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: false, force: false, verbose: true }); + const builder = createSolutionBuilder(host, buildHost, ["/src/tests"], { dry: false, force: false, verbose: true }); it("Builds the project", () => { clearDiagnostics(); @@ -174,7 +181,7 @@ namespace ts { it("won't build downstream projects if upstream projects have errors", () => { const fs = projFs.shadow(); const host = new fakes.CompilerHost(fs); - const builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: false, force: false, verbose: true }); + const builder = createSolutionBuilder(host, buildHost, ["/src/tests"], { dry: false, force: false, verbose: true }); clearDiagnostics(); @@ -198,7 +205,7 @@ namespace ts { it("invalidates projects correctly", () => { const fs = projFs.shadow(); const host = new fakes.CompilerHost(fs); - const builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: false, force: false, verbose: false }); + const builder = createSolutionBuilder(host, buildHost, ["/src/tests"], { dry: false, force: false, verbose: false }); clearDiagnostics(); builder.buildAllProjects(); @@ -234,7 +241,7 @@ namespace ts { describe("tsbuild - baseline sectioned sourcemaps", () => { const fs = outFileFs.shadow(); const host = new fakes.CompilerHost(fs); - const builder = createSolutionBuilder(host, ["/src/third"], reportDiagnostic, { dry: false, force: false, verbose: false }); + const builder = createSolutionBuilder(host, buildHost, ["/src/third"], { dry: false, force: false, verbose: false }); clearDiagnostics(); builder.buildAllProjects(); assertDiagnosticMessages(/*none*/); @@ -292,7 +299,7 @@ namespace ts { }); function checkGraphOrdering(rootNames: string[], expectedBuildSet: string[]) { - const builder = createSolutionBuilder(host, rootNames, reportDiagnostic, { dry: true, force: false, verbose: false }); + const builder = createSolutionBuilder(host, buildHost, rootNames, { dry: true, force: false, verbose: false }); const projFileNames = rootNames.map(getProjectFileName); const graph = builder.getBuildGraph(projFileNames); From b97bc8e07112d43f624eb31c38c9001415fe5ee0 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Sat, 9 Jun 2018 09:15:17 -0700 Subject: [PATCH 80/81] Use native map --- src/compiler/tsbuild.ts | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index 606fc1a27f6..e13166d3099 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -202,7 +202,7 @@ namespace ts { */ function createFileMap(): FileMap { // tslint:disable-next-line:no-null-keyword - const lookup: { [key: string]: T } = Object.create(/*prototype*/ null); + const lookup = createMap(); return { setValue, @@ -218,21 +218,21 @@ namespace ts { } function hasKey(fileName: string) { - return normalizePath(fileName) in lookup; + return lookup.has(normalizePath(fileName)); } function removeKey(fileName: string) { - delete lookup[fileName]; + lookup.delete(normalizePath(fileName)); } function setValue(fileName: string, value: T) { - lookup[normalizePath(fileName)] = value; + lookup.set(normalizePath(fileName), value); } function getValue(fileName: string): T | never { const f = normalizePath(fileName); - if (f in lookup) { - return lookup[f]; + if (lookup.has(f)) { + return lookup.get(f)!; } else { throw new Error(`No value corresponding to ${fileName} exists in this map`); @@ -241,12 +241,7 @@ namespace ts { function getValueOrUndefined(fileName: string): T | undefined { const f = normalizePath(fileName); - if (f in lookup) { - return lookup[f]; - } - else { - return undefined; - } + return lookup.get(f); } } From 0f626fdcb3026452fe1ca333085c2b720add2ee6 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Sat, 9 Jun 2018 09:43:25 -0700 Subject: [PATCH 81/81] Last round PR comments --- src/compiler/tsbuild.ts | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index e13166d3099..79306cc5d06 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -246,9 +246,9 @@ namespace ts { } export function createDependencyMapper() { - const childToParents: { [key: string]: ResolvedConfigFileName[] } = {}; - const parentToChildren: { [key: string]: ResolvedConfigFileName[] } = {}; - const allKeys: ResolvedConfigFileName[] = []; + const childToParents = createFileMap(); + const parentToChildren = createFileMap(); + const allKeys = createFileMap(); function addReference(childConfigFileName: ResolvedConfigFileName, parentConfigFileName: ResolvedConfigFileName): void { addEntry(childToParents, childConfigFileName, parentConfigFileName); @@ -256,26 +256,29 @@ namespace ts { } function getReferencesTo(parentConfigFileName: ResolvedConfigFileName): ResolvedConfigFileName[] { - return parentToChildren[normalizePath(parentConfigFileName)] || []; + return parentToChildren.getValueOrUndefined(parentConfigFileName) || []; } function getReferencesOf(childConfigFileName: ResolvedConfigFileName): ResolvedConfigFileName[] { - return childToParents[normalizePath(childConfigFileName)] || []; + return childToParents.getValueOrUndefined(childConfigFileName) || []; } function getKeys(): ReadonlyArray { - return allKeys; + return allKeys.getKeys() as ResolvedConfigFileName[]; } function addEntry(mapToAddTo: typeof childToParents | typeof parentToChildren, key: ResolvedConfigFileName, element: ResolvedConfigFileName) { key = normalizePath(key) as ResolvedConfigFileName; element = normalizePath(element) as ResolvedConfigFileName; - const arr = (mapToAddTo[key] = mapToAddTo[key] || []); + let arr = mapToAddTo.getValueOrUndefined(key); + if (arr === undefined) { + mapToAddTo.setValue(key, arr = []); + } if (arr.indexOf(element) < 0) { arr.push(element); } - if (allKeys.indexOf(key) < 0) allKeys.push(key); - if (allKeys.indexOf(element) < 0) allKeys.push(element); + allKeys.setValue(key, true); + allKeys.setValue(element, true); } return { @@ -289,13 +292,13 @@ namespace ts { function getOutputDeclarationFileName(inputFileName: string, configFile: ParsedCommandLine) { const relativePath = getRelativePathFromDirectory(rootDirOfOptions(configFile.options, configFile.options.configFilePath!), inputFileName, /*ignoreCase*/ true); const outputPath = resolvePath(configFile.options.declarationDir || configFile.options.outDir || getDirectoryPath(configFile.options.configFilePath!), relativePath); - return changeExtension(outputPath, ".d.ts"); + return changeExtension(outputPath, Extension.Dts); } function getOutputJavaScriptFileName(inputFileName: string, configFile: ParsedCommandLine) { const relativePath = getRelativePathFromDirectory(rootDirOfOptions(configFile.options, configFile.options.configFilePath!), inputFileName, /*ignoreCase*/ true); const outputPath = resolvePath(configFile.options.outDir || getDirectoryPath(configFile.options.configFilePath!), relativePath); - return changeExtension(outputPath, (fileExtensionIs(inputFileName, ".tsx") && configFile.options.jsx === JsxEmit.Preserve) ? ".jsx" : ".js"); + return changeExtension(outputPath, (fileExtensionIs(inputFileName, Extension.Tsx) && configFile.options.jsx === JsxEmit.Preserve) ? Extension.Jsx : Extension.Js); } function getOutputFileNames(inputFileName: string, configFile: ParsedCommandLine): ReadonlyArray { @@ -316,12 +319,12 @@ namespace ts { function getOutFileOutputs(project: ParsedCommandLine): ReadonlyArray { if (!project.options.outFile) { - throw new Error("Assert - outFile must be set"); + return Debug.fail("outFile must be set"); } const outputs: string[] = []; outputs.push(project.options.outFile); if (project.options.declaration) { - const dts = changeExtension(project.options.outFile, ".d.ts"); + const dts = changeExtension(project.options.outFile, Extension.Dts); outputs.push(dts); if (project.options.declarationMap) { outputs.push(dts + ".map"); @@ -365,7 +368,7 @@ namespace ts { } function isDeclarationFile(fileName: string) { - return fileExtensionIs(fileName, ".d.ts"); + return fileExtensionIs(fileName, Extension.Dts); } export function createBuildContext(options: BuildOptions): BuildContext {