Skip to content

Commit e068475

Browse files
committed
Refactor so that builder handles only source files and program
1 parent ef5935b commit e068475

3 files changed

Lines changed: 164 additions & 118 deletions

File tree

src/server/builder.ts

Lines changed: 124 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -3,126 +3,101 @@
33
/// <reference path="session.ts" />
44

55
namespace ts.server {
6-
export function shouldEmitFile(scriptInfo: ScriptInfo) {
7-
return !scriptInfo.hasMixedContent;
8-
}
9-
106
export interface Builder {
117
/**
128
* This is the callback when file infos in the builder are updated
139
*/
14-
onProjectUpdateGraph(): void;
15-
getFilesAffectedBy(scriptInfo: ScriptInfo): string[];
16-
/**
17-
* @returns {boolean} whether the emit was conducted or not
18-
*/
19-
emitFile(scriptInfo: ScriptInfo, writeFile: (path: string, data: string, writeByteOrderMark?: boolean) => void): boolean;
10+
onProgramUpdateGraph(program: Program): void;
11+
getFilesAffectedBy(program: Program, path: Path): string[];
12+
emitFile(program: Program, path: Path): EmitOutput;
2013
clear(): void;
2114
}
2215

2316
interface EmitHandler {
24-
addScriptInfo(scriptInfo: ScriptInfo): void;
17+
addScriptInfo(program: Program, sourceFile: SourceFile): void;
2518
removeScriptInfo(path: Path): void;
26-
updateScriptInfo(scriptInfo: ScriptInfo): void;
19+
updateScriptInfo(program: Program, sourceFile: SourceFile): void;
2720
/**
2821
* Gets the files affected by the script info which has updated shape from the known one
2922
*/
30-
getFilesAffectedByUpdatedShape(scriptInfo: ScriptInfo, singleFileResult: string[]): string[];
23+
getFilesAffectedByUpdatedShape(program: Program, sourceFile: SourceFile, singleFileResult: string[]): string[];
3124
}
3225

33-
export function createBuilder(project: Project): Builder {
26+
export function createBuilder(
27+
getCanonicalFileName: (fileName: string) => string,
28+
getEmitOutput: (program: Program, sourceFile: SourceFile, emitOnlyDtsFiles?: boolean) => EmitOutput,
29+
computeHash: (data: string) => string,
30+
shouldEmitFile: (sourceFile: SourceFile) => boolean
31+
): Builder {
3432
let isModuleEmit: boolean | undefined;
35-
let projectVersionForDependencyGraph: string;
3633
// Last checked shape signature for the file info
3734
let fileInfos: Map<string>;
3835
let emitHandler: EmitHandler;
3936
return {
40-
onProjectUpdateGraph,
37+
onProgramUpdateGraph,
4138
getFilesAffectedBy,
4239
emitFile,
4340
clear
4441
};
4542

46-
function createProjectGraph() {
47-
const currentIsModuleEmit = project.getCompilerOptions().module !== ModuleKind.None;
43+
function createProgramGraph(program: Program) {
44+
const currentIsModuleEmit = program.getCompilerOptions().module !== ModuleKind.None;
4845
if (isModuleEmit !== currentIsModuleEmit) {
4946
isModuleEmit = currentIsModuleEmit;
5047
emitHandler = isModuleEmit ? getModuleEmitHandler() : getNonModuleEmitHandler();
5148
fileInfos = undefined;
5249
}
5350

5451
fileInfos = mutateExistingMap(
55-
fileInfos, arrayToMap(project.getScriptInfos(), info => info.path),
56-
(_path, info) => {
57-
emitHandler.addScriptInfo(info);
52+
fileInfos, arrayToMap(program.getSourceFiles(), sourceFile => sourceFile.path),
53+
(_path, sourceFile) => {
54+
emitHandler.addScriptInfo(program, sourceFile);
5855
return "";
5956
},
6057
(path: Path, _value) => emitHandler.removeScriptInfo(path),
6158
/*isSameValue*/ undefined,
6259
/*OnDeleteExistingMismatchValue*/ undefined,
63-
(_prevValue, scriptInfo) => emitHandler.updateScriptInfo(scriptInfo)
60+
(_prevValue, sourceFile) => emitHandler.updateScriptInfo(program, sourceFile)
6461
);
65-
projectVersionForDependencyGraph = project.getProjectVersion();
6662
}
6763

68-
function ensureFileInfos() {
64+
function ensureProgramGraph(program: Program) {
6965
if (!emitHandler) {
70-
createProjectGraph();
66+
createProgramGraph(program);
7167
}
72-
Debug.assert(projectVersionForDependencyGraph === project.getProjectVersion());
7368
}
7469

75-
function onProjectUpdateGraph() {
70+
function onProgramUpdateGraph(program: Program) {
7671
if (emitHandler) {
77-
createProjectGraph();
72+
createProgramGraph(program);
7873
}
7974
}
8075

81-
function getFilesAffectedBy(scriptInfo: ScriptInfo): string[] {
82-
ensureFileInfos();
76+
function getFilesAffectedBy(program: Program, path: Path): string[] {
77+
ensureProgramGraph(program);
8378

84-
const singleFileResult = scriptInfo.hasMixedContent ? [] : [scriptInfo.fileName];
85-
const path = scriptInfo.path;
86-
if (!fileInfos || !fileInfos.has(path) || !updateShapeSignature(scriptInfo)) {
79+
const sourceFile = program.getSourceFile(path);
80+
const singleFileResult = sourceFile && shouldEmitFile(sourceFile) ? [sourceFile.fileName] : [];
81+
if (!fileInfos || !fileInfos.has(path) || !updateShapeSignature(program, sourceFile)) {
8782
return singleFileResult;
8883
}
8984

90-
return emitHandler.getFilesAffectedByUpdatedShape(scriptInfo, singleFileResult);
85+
return emitHandler.getFilesAffectedByUpdatedShape(program, sourceFile, singleFileResult);
9186
}
9287

93-
/**
94-
* @returns {boolean} whether the emit was conducted or not
95-
*/
96-
function emitFile(scriptInfo: ScriptInfo, writeFile: (path: string, data: string, writeByteOrderMark?: boolean) => void): boolean {
97-
ensureFileInfos();
98-
if (!fileInfos || !fileInfos.has(scriptInfo.path)) {
99-
return false;
88+
function emitFile(program: Program, path: Path): EmitOutput {
89+
ensureProgramGraph(program);
90+
if (!fileInfos || !fileInfos.has(path)) {
91+
return { outputFiles: [], emitSkipped: true };
10092
}
10193

102-
const { emitSkipped, outputFiles } = project.getFileEmitOutput(scriptInfo, /*emitOnlyDtsFiles*/ false);
103-
if (!emitSkipped) {
104-
const projectRootPath = project.getProjectRootPath();
105-
for (const outputFile of outputFiles) {
106-
const outputFileAbsoluteFileName = getNormalizedAbsolutePath(outputFile.name, projectRootPath ? projectRootPath : getDirectoryPath(scriptInfo.fileName));
107-
writeFile(outputFileAbsoluteFileName, outputFile.text, outputFile.writeByteOrderMark);
108-
}
109-
}
110-
return !emitSkipped;
94+
return getEmitOutput(program, program.getSourceFileByPath(path), /*emitOnlyDtsFiles*/ false);
11195
}
11296

11397
function clear() {
11498
isModuleEmit = undefined;
11599
emitHandler = undefined;
116100
fileInfos = undefined;
117-
projectVersionForDependencyGraph = undefined;
118-
}
119-
120-
function getSourceFile(path: Path) {
121-
return project.getSourceFile(path);
122-
}
123-
124-
function getScriptInfo(path: Path) {
125-
return project.projectService.getScriptInfoForPath(path);
126101
}
127102

128103
function isExternalModuleOrHasOnlyAmbientExternalModules(sourceFile: SourceFile) {
@@ -147,12 +122,8 @@ namespace ts.server {
147122
/**
148123
* @return {boolean} indicates if the shape signature has changed since last update.
149124
*/
150-
function updateShapeSignature(scriptInfo: ScriptInfo) {
151-
const path = scriptInfo.path;
152-
const sourceFile = getSourceFile(path);
153-
if (!sourceFile) {
154-
return true;
155-
}
125+
function updateShapeSignature(program: Program, sourceFile: SourceFile) {
126+
const path = sourceFile.path;
156127

157128
const prevSignature = fileInfos.get(path);
158129
let latestSignature = prevSignature;
@@ -161,7 +132,7 @@ namespace ts.server {
161132
fileInfos.set(path, latestSignature);
162133
}
163134
else {
164-
const emitOutput = project.getFileEmitOutput(scriptInfo, /*emitOnlyDtsFiles*/ true);
135+
const emitOutput = getEmitOutput(program, sourceFile, /*emitOnlyDtsFiles*/ true);
165136
if (emitOutput.outputFiles && emitOutput.outputFiles.length > 0) {
166137
latestSignature = computeHash(emitOutput.outputFiles[0].text);
167138
fileInfos.set(path, latestSignature);
@@ -171,8 +142,67 @@ namespace ts.server {
171142
return !prevSignature || latestSignature !== prevSignature;
172143
}
173144

174-
function computeHash(text: string) {
175-
return project.projectService.host.createHash(text);
145+
/**
146+
* Gets the referenced files for a file from the program
147+
* @param program
148+
* @param path
149+
*/
150+
function getReferencedFiles(program: Program, sourceFile: SourceFile): Map<true> {
151+
const referencedFiles = createMap<true>();
152+
// We need to use a set here since the code can contain the same import twice,
153+
// but that will only be one dependency.
154+
// To avoid invernal conversion, the key of the referencedFiles map must be of type Path
155+
if (sourceFile.imports && sourceFile.imports.length > 0) {
156+
const checker: TypeChecker = program.getTypeChecker();
157+
for (const importName of sourceFile.imports) {
158+
const symbol = checker.getSymbolAtLocation(importName);
159+
if (symbol && symbol.declarations && symbol.declarations[0]) {
160+
const declarationSourceFile = symbol.declarations[0].getSourceFile();
161+
if (declarationSourceFile) {
162+
referencedFiles.set(declarationSourceFile.path, true);
163+
}
164+
}
165+
}
166+
}
167+
168+
const sourceFileDirectory = getDirectoryPath(sourceFile.path);
169+
// Handle triple slash references
170+
if (sourceFile.referencedFiles && sourceFile.referencedFiles.length > 0) {
171+
for (const referencedFile of sourceFile.referencedFiles) {
172+
const referencedPath = toPath(referencedFile.fileName, sourceFileDirectory, getCanonicalFileName);
173+
referencedFiles.set(referencedPath, true);
174+
}
175+
}
176+
177+
// Handle type reference directives
178+
if (sourceFile.resolvedTypeReferenceDirectiveNames) {
179+
sourceFile.resolvedTypeReferenceDirectiveNames.forEach((resolvedTypeReferenceDirective) => {
180+
if (!resolvedTypeReferenceDirective) {
181+
return;
182+
}
183+
184+
const fileName = resolvedTypeReferenceDirective.resolvedFileName;
185+
const typeFilePath = toPath(fileName, sourceFileDirectory, getCanonicalFileName);
186+
referencedFiles.set(typeFilePath, true);
187+
});
188+
}
189+
190+
return referencedFiles;
191+
}
192+
193+
/**
194+
* Gets all the emittable files from the program
195+
*/
196+
function getAllEmittableFiles(program: Program) {
197+
const defaultLibraryFileName = getDefaultLibFileName(program.getCompilerOptions());
198+
const sourceFiles = program.getSourceFiles();
199+
const result: string[] = [];
200+
for (const sourceFile of sourceFiles) {
201+
if (getBaseFileName(sourceFile.fileName) !== defaultLibraryFileName && shouldEmitFile(sourceFile)) {
202+
result.push(sourceFile.fileName);
203+
}
204+
}
205+
return result;
176206
}
177207

178208
function noop() { }
@@ -185,14 +215,14 @@ namespace ts.server {
185215
getFilesAffectedByUpdatedShape
186216
};
187217

188-
function getFilesAffectedByUpdatedShape(_scriptInfo: ScriptInfo, singleFileResult: string[]): string[] {
189-
const options = project.getCompilerOptions();
218+
function getFilesAffectedByUpdatedShape(program: Program, _sourceFile: SourceFile, singleFileResult: string[]): string[] {
219+
const options = program.getCompilerOptions();
190220
// If `--out` or `--outFile` is specified, any new emit will result in re-emitting the entire project,
191221
// so returning the file itself is good enough.
192222
if (options && (options.out || options.outFile)) {
193223
return singleFileResult;
194224
}
195-
return project.getAllEmittableFiles();
225+
return getAllEmittableFiles(program);
196226
}
197227
}
198228

@@ -207,47 +237,47 @@ namespace ts.server {
207237
getFilesAffectedByUpdatedShape
208238
};
209239

210-
function setReferences(path: Path, latestVersion: string, existingMap: Map<true>) {
211-
existingMap = mutateExistingMapWithNewSet<true>(existingMap, project.getReferencedFiles(path),
240+
function setReferences(program: Program, sourceFile: SourceFile, existingMap: Map<true>) {
241+
const path = sourceFile.path;
242+
existingMap = mutateExistingMapWithNewSet<true>(
243+
existingMap,
244+
getReferencedFiles(program, sourceFile),
212245
// Creating new Reference: Also add referenced by
213246
key => { referencedBy.add(key, path); return true; },
214247
// Remove existing reference
215248
(key, _existingValue) => { referencedBy.remove(key, path); }
216249
);
217250
references.set(path, existingMap);
218-
scriptVersionForReferences.set(path, latestVersion);
251+
scriptVersionForReferences.set(path, sourceFile.version);
219252
}
220253

221-
function addScriptInfo(info: ScriptInfo) {
222-
setReferences(info.path, info.getLatestVersion(), undefined);
254+
function addScriptInfo(program: Program, sourceFile: SourceFile) {
255+
setReferences(program, sourceFile, undefined);
223256
}
224257

225258
function removeScriptInfo(path: Path) {
226259
references.delete(path);
227260
scriptVersionForReferences.delete(path);
228261
}
229262

230-
function updateScriptInfo(scriptInfo: ScriptInfo) {
231-
const path = scriptInfo.path;
263+
function updateScriptInfo(program: Program, sourceFile: SourceFile) {
264+
const path = sourceFile.path;
232265
const lastUpdatedVersion = scriptVersionForReferences.get(path);
233-
const latestVersion = scriptInfo.getLatestVersion();
234-
if (lastUpdatedVersion !== latestVersion) {
235-
setReferences(path, latestVersion, references.get(path));
266+
if (lastUpdatedVersion !== sourceFile.version) {
267+
setReferences(program, sourceFile, references.get(path));
236268
}
237269
}
238270

239271
function getReferencedByPaths(path: Path) {
240272
return referencedBy.get(path) || [];
241273
}
242274

243-
function getFilesAffectedByUpdatedShape(scriptInfo: ScriptInfo, singleFileResult: string[]): string[] {
244-
const path = scriptInfo.path;
245-
const sourceFile = getSourceFile(path);
275+
function getFilesAffectedByUpdatedShape(program: Program, sourceFile: SourceFile, singleFileResult: string[]): string[] {
246276
if (!isExternalModuleOrHasOnlyAmbientExternalModules(sourceFile)) {
247-
return project.getAllEmittableFiles();
277+
return getAllEmittableFiles(program);
248278
}
249279

250-
const options = project.getCompilerOptions();
280+
const options = program.getCompilerOptions();
251281
if (options && (options.isolatedModules || options.out || options.outFile)) {
252282
return singleFileResult;
253283
}
@@ -256,22 +286,23 @@ namespace ts.server {
256286
// Because if so, its own referencedBy files need to be saved as well to make the
257287
// emitting result consistent with files on disk.
258288

259-
const fileNamesMap = createMap<NormalizedPath>();
260-
const setFileName = (path: Path, scriptInfo: ScriptInfo) => {
261-
fileNamesMap.set(path, scriptInfo && shouldEmitFile(scriptInfo) ? scriptInfo.fileName : undefined);
289+
const fileNamesMap = createMap<string>();
290+
const setFileName = (path: Path, sourceFile: SourceFile) => {
291+
fileNamesMap.set(path, sourceFile && shouldEmitFile(sourceFile) ? sourceFile.fileName : undefined);
262292
};
263293

264294
// Start with the paths this file was referenced by
265-
setFileName(path, scriptInfo);
295+
const path = sourceFile.path;
296+
setFileName(path, sourceFile);
266297
const queue = getReferencedByPaths(path).slice();
267298
while (queue.length > 0) {
268299
const currentPath = queue.pop();
269300
if (!fileNamesMap.has(currentPath)) {
270-
const currentScriptInfo = getScriptInfo(currentPath);
271-
if (currentScriptInfo && updateShapeSignature(currentScriptInfo)) {
301+
const currentSourceFile = program.getSourceFileByPath(currentPath);
302+
if (currentSourceFile && updateShapeSignature(program, currentSourceFile)) {
272303
queue.push(...getReferencedByPaths(currentPath));
273304
}
274-
setFileName(currentPath, currentScriptInfo);
305+
setFileName(currentPath, currentSourceFile);
275306
}
276307
}
277308

0 commit comments

Comments
 (0)