Skip to content

Commit 5a419a2

Browse files
authored
In tsc --b mode detect if root file of compilation is not root any more (#52417)
1 parent d63d081 commit 5a419a2

File tree

489 files changed

+20291
-2890
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

489 files changed

+20291
-2890
lines changed

src/compiler/builder.ts

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import {
3737
emitSkippedWithNoDiagnostics,
3838
emptyArray,
3939
ensurePathIsNonModuleName,
40+
FileIncludeKind,
4041
filterSemanticDiagnostics,
4142
forEach,
4243
forEachEntry,
@@ -911,9 +912,17 @@ export type ProgramBuildInfoEmitSignature = ProgramBuildInfoFileId | [fileId: Pr
911912
*/
912913
export type ProgramMultiFileEmitBuildInfoFileInfo = string | ProgramMultiFileEmitBuildInfoBuilderStateFileInfo;
913914
/** @internal */
915+
export type ProgramBuildInfoRootStartEnd = [start: ProgramBuildInfoFileId, end: ProgramBuildInfoFileId];
916+
/**
917+
* Either start and end of FileId for consecutive fileIds to be included as root or single fileId that is root
918+
* @internal
919+
*/
920+
export type ProgramBuildInfoRoot = ProgramBuildInfoRootStartEnd | ProgramBuildInfoFileId;
921+
/** @internal */
914922
export interface ProgramMultiFileEmitBuildInfo {
915923
fileNames: readonly string[];
916924
fileInfos: readonly ProgramMultiFileEmitBuildInfoFileInfo[];
925+
root: readonly ProgramBuildInfoRoot[];
917926
options: CompilerOptions | undefined;
918927
fileIdsList: readonly (readonly ProgramBuildInfoFileId[])[] | undefined;
919928
referencedMap: ProgramBuildInfoReferencedMap | undefined;
@@ -942,6 +951,7 @@ export type ProgramBuildInfoBundlePendingEmit = BuilderFileEmit | false;
942951
export interface ProgramBundleEmitBuildInfo {
943952
fileNames: readonly string[];
944953
fileInfos: readonly ProgramBundleEmitBuildInfoFileInfo[];
954+
root: readonly ProgramBuildInfoRoot[];
945955
options: CompilerOptions | undefined;
946956
outSignature: EmitSignature | undefined;
947957
latestChangedDtsFile: string | undefined;
@@ -966,19 +976,22 @@ function getBuildInfo(state: BuilderProgramState, bundle: BundleBuildInfo | unde
966976
const latestChangedDtsFile = state.latestChangedDtsFile ? relativeToBuildInfoEnsuringAbsolutePath(state.latestChangedDtsFile) : undefined;
967977
const fileNames: string[] = [];
968978
const fileNameToFileId = new Map<string, ProgramBuildInfoFileId>();
979+
const root: ProgramBuildInfoRoot[] = [];
969980
if (outFile(state.compilerOptions)) {
970981
// Copy all fileInfo, version and impliedFormat
971982
// Affects global scope and signature doesnt matter because with --out they arent calculated or needed to determine upto date ness
972983
const fileInfos = arrayFrom(state.fileInfos.entries(), ([key, value]): ProgramBundleEmitBuildInfoFileInfo => {
973984
// Ensure fileId
974-
toFileId(key);
985+
const fileId = toFileId(key);
986+
tryAddRoot(key, fileId);
975987
return value.impliedFormat ?
976988
{ version: value.version, impliedFormat: value.impliedFormat, signature: undefined, affectsGlobalScope: undefined } :
977989
value.version;
978990
});
979991
const program: ProgramBundleEmitBuildInfo = {
980992
fileNames,
981993
fileInfos,
994+
root,
982995
options: convertToProgramBuildInfoCompilerOptions(state.compilerOptions),
983996
outSignature: state.outSignature,
984997
latestChangedDtsFile,
@@ -1005,6 +1018,7 @@ function getBuildInfo(state: BuilderProgramState, bundle: BundleBuildInfo | unde
10051018
const fileInfos = arrayFrom(state.fileInfos.entries(), ([key, value]): ProgramMultiFileEmitBuildInfoFileInfo => {
10061019
// Ensure fileId
10071020
const fileId = toFileId(key);
1021+
tryAddRoot(key, fileId);
10081022
Debug.assert(fileNames[fileId - 1] === relativeToBuildInfo(key));
10091023
const oldSignature = state.oldSignatures?.get(key);
10101024
const actualSignature = oldSignature !== undefined ? oldSignature || undefined : value.signature;
@@ -1100,6 +1114,7 @@ function getBuildInfo(state: BuilderProgramState, bundle: BundleBuildInfo | unde
11001114
const program: ProgramMultiFileEmitBuildInfo = {
11011115
fileNames,
11021116
fileInfos,
1117+
root,
11031118
options: convertToProgramBuildInfoCompilerOptions(state.compilerOptions),
11041119
fileIdsList,
11051120
referencedMap,
@@ -1140,6 +1155,25 @@ function getBuildInfo(state: BuilderProgramState, bundle: BundleBuildInfo | unde
11401155
return fileIdListId;
11411156
}
11421157

1158+
function tryAddRoot(path: Path, fileId: ProgramBuildInfoFileId) {
1159+
const file = state.program!.getSourceFile(path)!;
1160+
if (!state.program!.getFileIncludeReasons().get(file.path)!.some(r => r.kind === FileIncludeKind.RootFile)) return;
1161+
// First fileId as is
1162+
if (!root.length) return root.push(fileId);
1163+
const last = root[root.length - 1];
1164+
const isLastStartEnd = isArray(last);
1165+
// If its [..., last = [start, end = fileId - 1]], update last to [start, fileId]
1166+
if (isLastStartEnd && last[1] === fileId - 1) return last[1] = fileId;
1167+
// If its [..., last = [start, end !== fileId - 1]] or [last] or [..., last !== fileId - 1], push the fileId
1168+
if (isLastStartEnd || root.length === 1 || last !== fileId - 1) return root.push(fileId);
1169+
const lastButOne = root[root.length - 2];
1170+
// If [..., lastButOne = [start, end], lastFileId] or [..., lastButOne !== lastFileId - 1, lastFileId], push the fileId
1171+
if (!isNumber(lastButOne) || lastButOne !== last - 1) return root.push(fileId);
1172+
// Convert lastButOne as [lastButOne, fileId]
1173+
root[root.length - 2] = [lastButOne, fileId];
1174+
return root.length = root.length - 1;
1175+
}
1176+
11431177
/**
11441178
* @param optionKey key of CommandLineOption to use to determine if the option should be serialized in tsbuildinfo
11451179
*/
@@ -1776,16 +1810,32 @@ export function getBuildInfoFileVersionMap(
17761810
program: ProgramBuildInfo,
17771811
buildInfoPath: string,
17781812
host: Pick<ReadBuildProgramHost, "useCaseSensitiveFileNames" | "getCurrentDirectory">
1779-
): Map<Path, string> {
1813+
) {
17801814
const buildInfoDirectory = getDirectoryPath(getNormalizedAbsolutePath(buildInfoPath, host.getCurrentDirectory()));
17811815
const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames());
17821816
const fileInfos = new Map<Path, string>();
1817+
let rootIndex = 0;
1818+
const roots: Path[] = [];
17831819
program.fileInfos.forEach((fileInfo, index) => {
17841820
const path = toPath(program.fileNames[index], buildInfoDirectory, getCanonicalFileName);
17851821
const version = isString(fileInfo) ? fileInfo : fileInfo.version;
17861822
fileInfos.set(path, version);
1823+
if (rootIndex < program.root.length) {
1824+
const current = program.root[rootIndex];
1825+
const fileId = (index + 1) as ProgramBuildInfoFileId;
1826+
if (isArray(current)) {
1827+
if (current[0] <= fileId && fileId <= current[1]) {
1828+
roots.push(path);
1829+
if (current[1] === fileId) rootIndex++;
1830+
}
1831+
}
1832+
else if (current === fileId) {
1833+
roots.push(path);
1834+
rootIndex++;
1835+
}
1836+
}
17871837
});
1788-
return fileInfos;
1838+
return { fileInfos, roots };
17891839
}
17901840

17911841
/** @internal */

src/compiler/diagnosticMessages.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5562,6 +5562,10 @@
55625562
"category": "Message",
55635563
"code": 6411
55645564
},
5565+
"Project '{0}' is out of date because buildinfo file '{1}' indicates that file '{2}' was root file of compilation but not any more.": {
5566+
"category": "Message",
5567+
"code": 6412
5568+
},
55655569

55665570
"The expected type comes from property '{0}' which is declared here on type '{1}'": {
55675571
"category": "Message",

src/compiler/tsbuild.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
combinePaths,
33
Extension,
44
fileExtensionIs,
5+
Path,
56
ResolvedConfigFileName,
67
} from "./_namespaces/ts";
78

@@ -28,6 +29,7 @@ export enum UpToDateStatusType {
2829
OutOfDateWithUpstream,
2930
OutOfDateBuildInfo,
3031
OutOfDateOptions,
32+
OutOfDateRoots,
3133
UpstreamOutOfDate,
3234
UpstreamBlocked,
3335
ComputingUpstream,
@@ -51,6 +53,7 @@ export type UpToDateStatus =
5153
| Status.OutOfDateWithSelf
5254
| Status.OutOfDateWithUpstream
5355
| Status.OutOfDateBuildInfo
56+
| Status.OutOfDateRoots
5457
| Status.UpstreamOutOfDate
5558
| Status.UpstreamBlocked
5659
| Status.ComputingUpstream
@@ -127,10 +130,16 @@ export namespace Status {
127130
* Buildinfo indicates that build is out of date
128131
*/
129132
export interface OutOfDateBuildInfo {
130-
type: UpToDateStatusType.OutOfDateBuildInfo | UpToDateStatusType.OutOfDateOptions,
133+
type: UpToDateStatusType.OutOfDateBuildInfo | UpToDateStatusType.OutOfDateOptions;
131134
buildInfoFile: string;
132135
}
133136

137+
export interface OutOfDateRoots {
138+
type: UpToDateStatusType.OutOfDateRoots,
139+
buildInfoFile: string;
140+
inputFile: Path;
141+
}
142+
134143
/**
135144
* This project depends on an out-of-date project, so shouldn't be built yet
136145
*/

src/compiler/tsbuildPublic.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1729,7 +1729,7 @@ function getUpToDateStatusWorker<T extends BuilderProgram>(state: SolutionBuilde
17291729
let oldestOutputFileTime = maximumDate;
17301730
let buildInfoTime: Date | undefined;
17311731
let buildInfoProgram: ProgramBuildInfo | undefined;
1732-
let buildInfoVersionMap: Map<Path, string> | undefined;
1732+
let buildInfoVersionMap: ReturnType<typeof getBuildInfoFileVersionMap> | undefined;
17331733
if (buildInfoPath) {
17341734
const buildInfoCacheEntry = getBuildInfoCacheEntry(state, buildInfoPath, resolvedPath);
17351735
buildInfoTime = buildInfoCacheEntry?.modifiedTime || ts.getModifiedTime(host, buildInfoPath);
@@ -1798,6 +1798,7 @@ function getUpToDateStatusWorker<T extends BuilderProgram>(state: SolutionBuilde
17981798
let newestInputFileTime = minimumDate;
17991799
/** True if input file has changed timestamp but text is not changed, we can then do only timestamp updates on output to make it look up-to-date later */
18001800
let pseudoInputUpToDate = false;
1801+
const seenRoots = new Set<Path>();
18011802
// Get timestamps of input files
18021803
for (const inputFile of project.fileNames) {
18031804
const inputTime = getModifiedTime(state, inputFile);
@@ -1815,7 +1816,7 @@ function getUpToDateStatusWorker<T extends BuilderProgram>(state: SolutionBuilde
18151816
if (buildInfoProgram) {
18161817
// Read files and see if they are same, read is anyways cached
18171818
if (!buildInfoVersionMap) buildInfoVersionMap = getBuildInfoFileVersionMap(buildInfoProgram, buildInfoPath!, host);
1818-
version = buildInfoVersionMap.get(toPath(state, inputFile));
1819+
version = buildInfoVersionMap.fileInfos.get(toPath(state, inputFile));
18191820
const text = version ? state.readFileWithCache(inputFile) : undefined;
18201821
currentVersion = text !== undefined ? getSourceFileVersionAsHashFromText(host, text) : undefined;
18211822
if (version && version === currentVersion) pseudoInputUpToDate = true;
@@ -1834,6 +1835,22 @@ function getUpToDateStatusWorker<T extends BuilderProgram>(state: SolutionBuilde
18341835
newestInputFileName = inputFile;
18351836
newestInputFileTime = inputTime;
18361837
}
1838+
1839+
if (buildInfoProgram) seenRoots.add(toPath(state, inputFile));
1840+
}
1841+
1842+
if (buildInfoProgram) {
1843+
if (!buildInfoVersionMap) buildInfoVersionMap = getBuildInfoFileVersionMap(buildInfoProgram, buildInfoPath!, host);
1844+
for (const existingRoot of buildInfoVersionMap.roots) {
1845+
if (!seenRoots.has(existingRoot)) {
1846+
// File was root file when project was built but its not any more
1847+
return {
1848+
type: UpToDateStatusType.OutOfDateRoots,
1849+
buildInfoFile: buildInfoPath!,
1850+
inputFile: existingRoot,
1851+
};
1852+
}
1853+
}
18371854
}
18381855

18391856
// Now see if all outputs are newer than the newest input
@@ -2561,6 +2578,14 @@ function reportUpToDateStatus<T extends BuilderProgram>(state: SolutionBuilderSt
25612578
relName(state, configFileName),
25622579
relName(state, status.buildInfoFile)
25632580
);
2581+
case UpToDateStatusType.OutOfDateRoots:
2582+
return reportStatus(
2583+
state,
2584+
Diagnostics.Project_0_is_out_of_date_because_buildinfo_file_1_indicates_that_file_2_was_root_file_of_compilation_but_not_any_more,
2585+
relName(state, configFileName),
2586+
relName(state, status.buildInfoFile),
2587+
relName(state, status.inputFile),
2588+
);
25642589
case UpToDateStatusType.UpToDate:
25652590
if (status.newestInputFileTime !== undefined) {
25662591
return reportStatus(

src/testRunner/tests.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ import "./unittests/tsbuild/demo";
7171
import "./unittests/tsbuild/emitDeclarationOnly";
7272
import "./unittests/tsbuild/emptyFiles";
7373
import "./unittests/tsbuild/exitCodeOnBogusFile";
74+
import "./unittests/tsbuild/fileDelete";
7475
import "./unittests/tsbuild/graphOrdering";
7576
import "./unittests/tsbuild/inferredTypeFromTransitiveModule";
7677
import "./unittests/tsbuild/javascriptProjectEmit";
@@ -84,6 +85,7 @@ import "./unittests/tsbuild/outputPaths";
8485
import "./unittests/tsbuild/publicApi";
8586
import "./unittests/tsbuild/referencesWithRootDirInParent";
8687
import "./unittests/tsbuild/resolveJsonModule";
88+
import "./unittests/tsbuild/roots";
8789
import "./unittests/tsbuild/sample";
8890
import "./unittests/tsbuild/transitiveReferences";
8991
import "./unittests/tsbuildWatch/configFileErrors";
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import * as ts from "../../_namespaces/ts";
2+
import {
3+
dedent
4+
} from "../../_namespaces/Utils";
5+
import {
6+
compilerOptionsToConfigJson,
7+
loadProjectFromFiles,
8+
verifyTsc,
9+
} from "../tsc/helpers";
10+
11+
describe("unittests:: tsbuild:: fileDelete::", () => {
12+
function fs(childOptions: ts.CompilerOptions, mainOptions?: ts.CompilerOptions) {
13+
return loadProjectFromFiles({
14+
"/src/child/child.ts": dedent`
15+
import { child2 } from "../child/child2";
16+
export function child() {
17+
child2();
18+
}
19+
`,
20+
"/src/child/child2.ts": dedent`
21+
export function child2() {
22+
}
23+
`,
24+
"/src/child/tsconfig.json": JSON.stringify({
25+
compilerOptions: compilerOptionsToConfigJson(childOptions),
26+
}),
27+
...(mainOptions ? {
28+
"/src/main/main.ts": dedent`
29+
import { child } from "${childOptions.outFile ? "child" : "../child/child"}";
30+
export function main() {
31+
child();
32+
}
33+
`,
34+
"/src/main/tsconfig.json": JSON.stringify({
35+
compilerOptions: compilerOptionsToConfigJson(mainOptions),
36+
references: [{ path: "../child" }],
37+
}),
38+
} : {}),
39+
});
40+
}
41+
42+
verifyTsc({
43+
scenario: "fileDelete",
44+
subScenario: `detects deleted file`,
45+
commandLineArgs: ["--b", "/src/main/tsconfig.json", "-v", "--traceResolution", "--explainFiles"],
46+
fs: () => fs({ composite: true }, { composite: true }),
47+
edits: [{
48+
caption: "delete child2 file",
49+
edit: fs => {
50+
fs.rimrafSync("/src/child/child2.ts");
51+
fs.rimrafSync("/src/child/child2.js");
52+
fs.rimrafSync("/src/child/child2.d.ts");
53+
},
54+
discrepancyExplanation: () => [
55+
"Clean build will not have latestChangedDtsFile as there was no emit and emitSignatures as undefined for files",
56+
"Incremental will store the past latestChangedDtsFile and emitSignatures",
57+
]
58+
}],
59+
});
60+
61+
verifyTsc({
62+
scenario: "fileDelete",
63+
subScenario: `detects deleted file with outFile`,
64+
commandLineArgs: ["--b", "/src/main/tsconfig.json", "-v", "--traceResolution", "--explainFiles"],
65+
fs: () => fs({ composite: true, outFile: "../childResult.js", module: ts.ModuleKind.AMD }, { composite: true, outFile: "../mainResult.js", module: ts.ModuleKind.AMD }),
66+
edits: [{
67+
caption: "delete child2 file",
68+
edit: fs => fs.rimrafSync("/src/child/child2.ts")
69+
}],
70+
});
71+
72+
verifyTsc({
73+
scenario: "fileDelete",
74+
subScenario: `deleted file without composite`,
75+
commandLineArgs: ["--b", "/src/child/tsconfig.json", "-v", "--traceResolution", "--explainFiles"],
76+
fs: () => fs({}),
77+
edits: [{
78+
caption: "delete child2 file",
79+
edit: fs => {
80+
fs.rimrafSync("/src/child/child2.ts");
81+
fs.rimrafSync("/src/child/child2.js");
82+
}
83+
}],
84+
});
85+
86+
verifyTsc({
87+
scenario: "fileDelete",
88+
subScenario: `deleted file with outFile without composite`,
89+
commandLineArgs: ["--b", "/src/child/tsconfig.json", "-v", "--traceResolution", "--explainFiles"],
90+
fs: () => fs({ outFile: "../childResult.js", module: ts.ModuleKind.AMD }),
91+
edits: [{
92+
caption: "delete child2 file",
93+
edit: fs => fs.rimrafSync("/src/child/child2.ts"),
94+
}],
95+
});
96+
});

0 commit comments

Comments
 (0)