UNPKG

23.9 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3exports.ProgramStateFactory = void 0;
4const tslib_1 = require("tslib");
5const inversify_1 = require("inversify");
6const ts = require("typescript");
7const dependency_resolver_1 = require("./dependency-resolver");
8const utils_1 = require("../utils");
9const bind_decorator_1 = require("bind-decorator");
10const ymir_1 = require("@fimbul/ymir");
11const debug = require("debug");
12const tsutils_1 = require("tsutils");
13const path = require("path");
14const log = debug('wotan:programState');
15let ProgramStateFactory = class ProgramStateFactory {
16 constructor(resolverFactory, statePersistence, contentId) {
17 this.resolverFactory = resolverFactory;
18 this.statePersistence = statePersistence;
19 this.contentId = contentId;
20 }
21 create(program, host, tsconfigPath) {
22 return new ProgramStateImpl(host, program, this.resolverFactory.create(host, program), this.statePersistence, this.contentId, tsconfigPath);
23 }
24};
25ProgramStateFactory = tslib_1.__decorate([
26 inversify_1.injectable(),
27 tslib_1.__metadata("design:paramtypes", [dependency_resolver_1.DependencyResolverFactory,
28 ymir_1.StatePersistence,
29 ymir_1.ContentId])
30], ProgramStateFactory);
31exports.ProgramStateFactory = ProgramStateFactory;
32const STATE_VERSION = 1;
33const oldStateSymbol = Symbol('oldState');
34class ProgramStateImpl {
35 constructor(host, program, resolver, statePersistence, contentId, project) {
36 this.host = host;
37 this.program = program;
38 this.resolver = resolver;
39 this.statePersistence = statePersistence;
40 this.contentId = contentId;
41 this.project = project;
42 this.projectDirectory = utils_1.unixifyPath(path.dirname(this.project));
43 this.caseSensitive = this.host.useCaseSensitiveFileNames();
44 this.canonicalProjectDirectory = this.caseSensitive ? this.projectDirectory : this.projectDirectory.toLowerCase();
45 this.optionsHash = computeCompilerOptionsHash(this.program.getCompilerOptions(), this.projectDirectory);
46 this.assumeChangesOnlyAffectDirectDependencies = tsutils_1.isCompilerOptionEnabled(this.program.getCompilerOptions(), 'assumeChangesOnlyAffectDirectDependencies');
47 this.contentIds = new Map();
48 this.fileResults = new Map();
49 this.relativePathNames = new Map();
50 this.recheckOldState = true;
51 // TODO this can be removed once ProjectHost correctly reflects applied fixed in readFile
52 this.contentIdHost = {
53 readFile: (f) => { var _a; return (_a = this.program.getSourceFile(f)) === null || _a === void 0 ? void 0 : _a.text; },
54 };
55 const oldState = this.statePersistence.loadState(project);
56 if ((oldState === null || oldState === void 0 ? void 0 : oldState.v) !== STATE_VERSION || oldState.ts !== ts.version || oldState.options !== this.optionsHash) {
57 this[oldStateSymbol] = undefined;
58 this.dependenciesUpToDate = new Uint8Array(0);
59 }
60 else {
61 this[oldStateSymbol] = this.remapFileNames(oldState);
62 this.dependenciesUpToDate = new Uint8Array(oldState.files.length);
63 }
64 }
65 /** get old state if global files didn't change */
66 tryReuseOldState() {
67 const oldState = this[oldStateSymbol];
68 if (oldState === undefined || !this.recheckOldState)
69 return oldState;
70 const filesAffectingGlobalScope = this.resolver.getFilesAffectingGlobalScope();
71 if (oldState.global.length !== filesAffectingGlobalScope.length)
72 return this[oldStateSymbol] = undefined;
73 const globalFilesWithId = this.sortById(filesAffectingGlobalScope);
74 for (let i = 0; i < globalFilesWithId.length; ++i) {
75 const index = oldState.global[i];
76 if (globalFilesWithId[i].id !== oldState.files[index].id ||
77 !this.assumeChangesOnlyAffectDirectDependencies &&
78 !this.fileDependenciesUpToDate(globalFilesWithId[i].fileName, index, oldState))
79 return this[oldStateSymbol] = undefined;
80 }
81 this.recheckOldState = false;
82 return oldState;
83 }
84 update(program, updatedFile) {
85 this.program = program;
86 this.resolver.update(program, updatedFile);
87 this.contentIds.delete(updatedFile);
88 this.recheckOldState = true;
89 this.dependenciesUpToDate.fill(0 /* Unknown */);
90 }
91 getContentId(file) {
92 return utils_1.resolveCachedResult(this.contentIds, file, this.computeContentId);
93 }
94 computeContentId(file) {
95 return this.contentId.forFile(file, this.contentIdHost);
96 }
97 getRelativePath(fileName) {
98 return utils_1.resolveCachedResult(this.relativePathNames, fileName, this.makeRelativePath);
99 }
100 makeRelativePath(fileName) {
101 return utils_1.unixifyPath(path.relative(this.canonicalProjectDirectory, this.caseSensitive ? fileName : fileName.toLowerCase()));
102 }
103 getUpToDateResult(fileName, configHash) {
104 const oldState = this.tryReuseOldState();
105 if (oldState === undefined)
106 return;
107 const index = this.lookupFileIndex(fileName, oldState);
108 if (index === undefined)
109 return;
110 const old = oldState.files[index];
111 if (old.result === undefined ||
112 old.config !== configHash ||
113 old.id !== this.getContentId(fileName) ||
114 !this.fileDependenciesUpToDate(fileName, index, oldState))
115 return;
116 log('reusing state for %s', fileName);
117 return old.result;
118 }
119 setFileResult(fileName, configHash, result) {
120 if (!this.isFileUpToDate(fileName)) {
121 log('File %s is outdated, merging current state into old state', fileName);
122 // we need to create a state where the file is up-to-date
123 // so we replace the old state with the current state
124 // this includes all results from old state that are still up-to-date and all file results if they are still valid
125 const newState = this[oldStateSymbol] = this.aggregate();
126 this.recheckOldState = false;
127 this.fileResults = new Map();
128 this.dependenciesUpToDate = new Uint8Array(newState.files.length).fill(2 /* Ok */);
129 }
130 this.fileResults.set(fileName, { result, config: configHash });
131 }
132 isFileUpToDate(fileName) {
133 const oldState = this.tryReuseOldState();
134 if (oldState === undefined)
135 return false;
136 const index = this.lookupFileIndex(fileName, oldState);
137 if (index === undefined || oldState.files[index].id !== this.getContentId(fileName))
138 return false;
139 switch (this.dependenciesUpToDate[index]) {
140 case 0 /* Unknown */:
141 return this.fileDependenciesUpToDate(fileName, index, oldState);
142 case 2 /* Ok */:
143 return true;
144 case 1 /* Outdated */:
145 return false;
146 }
147 }
148 fileDependenciesUpToDate(fileName, index, oldState) {
149 // File names that are waiting to be processed, each iteration of the loop processes one file
150 const fileNameQueue = [fileName];
151 // For each entry in `fileNameQueue` this holds the index of that file in `oldState.files`
152 const indexQueue = [index];
153 // If a file is waiting for its children to be processed, it is moved from `indexQueue` to `parents`
154 const parents = [];
155 // For each entry in `parents` this holds the number of children that still need to be processed for that file
156 const childCounts = [];
157 // For each entry in `parents` this holds the index in of the earliest circular dependency in `parents`.
158 // For example, a value of `[Number.MAX_SAFE_INTEGER, 0]` means that `parents[1]` has a dependency on `parents[0]` (the root file).
159 const circularDependenciesQueue = [];
160 // If a file has a circular on one of its parents, it is moved from `indexQueue` to the current cycle
161 // or creates a new cycle if its parent is not already in a cycle.
162 const cycles = [];
163 while (true) {
164 index = indexQueue.pop();
165 fileName = fileNameQueue.pop();
166 processFile: {
167 switch (this.dependenciesUpToDate[index]) {
168 case 1 /* Outdated */:
169 return markAsOutdated(parents, index, cycles, this.dependenciesUpToDate);
170 case 2 /* Ok */:
171 break processFile;
172 }
173 for (const cycle of cycles) {
174 if (cycle.has(index)) {
175 // we already know this is a circular dependency, skip this one and simply mark the parent as circular
176 setCircularDependency(parents, circularDependenciesQueue, index, cycles, findCircularDependencyOfCycle(parents, circularDependenciesQueue, cycle));
177 break processFile;
178 }
179 }
180 let earliestCircularDependency = Number.MAX_SAFE_INTEGER;
181 let childCount = 0;
182 const old = oldState.files[index];
183 const dependencies = this.resolver.getDependencies(fileName);
184 const keys = old.dependencies === undefined ? utils_1.emptyArray : Object.keys(old.dependencies);
185 if (dependencies.size !== keys.length)
186 return markAsOutdated(parents, index, cycles, this.dependenciesUpToDate);
187 for (const key of keys) {
188 let newDeps = dependencies.get(key);
189 const oldDeps = old.dependencies[key];
190 if (oldDeps === null) {
191 if (newDeps !== null)
192 return markAsOutdated(parents, index, cycles, this.dependenciesUpToDate);
193 continue;
194 }
195 if (newDeps === null)
196 return markAsOutdated(parents, index, cycles, this.dependenciesUpToDate);
197 newDeps = Array.from(new Set(newDeps));
198 if (newDeps.length !== oldDeps.length)
199 return markAsOutdated(parents, index, cycles, this.dependenciesUpToDate);
200 const newDepsWithId = this.sortById(newDeps);
201 for (let i = 0; i < newDepsWithId.length; ++i) {
202 const oldDepState = oldState.files[oldDeps[i]];
203 if (newDepsWithId[i].id !== oldDepState.id)
204 return markAsOutdated(parents, index, cycles, this.dependenciesUpToDate);
205 if (!this.assumeChangesOnlyAffectDirectDependencies && fileName !== newDepsWithId[i].fileName) {
206 const indexInQueue = parents.indexOf(oldDeps[i]);
207 if (indexInQueue === -1) {
208 // no circular dependency
209 fileNameQueue.push(newDepsWithId[i].fileName);
210 indexQueue.push(oldDeps[i]);
211 ++childCount;
212 }
213 else if (indexInQueue < earliestCircularDependency) {
214 earliestCircularDependency = indexInQueue;
215 }
216 }
217 }
218 }
219 if (earliestCircularDependency !== Number.MAX_SAFE_INTEGER) {
220 earliestCircularDependency =
221 setCircularDependency(parents, circularDependenciesQueue, index, cycles, earliestCircularDependency);
222 }
223 else if (childCount === 0) {
224 this.dependenciesUpToDate[index] = 2 /* Ok */;
225 }
226 if (childCount !== 0) {
227 parents.push(index);
228 childCounts.push(childCount);
229 circularDependenciesQueue.push(earliestCircularDependency);
230 continue;
231 }
232 }
233 // we only get here for files with no children to process
234 if (parents.length === 0)
235 return true; // only happens if the initial file has no dependencies or they are all already known as Ok
236 while (--childCounts[childCounts.length - 1] === 0) {
237 index = parents.pop();
238 childCounts.pop();
239 const earliestCircularDependency = circularDependenciesQueue.pop();
240 if (earliestCircularDependency >= parents.length) {
241 this.dependenciesUpToDate[index] = 2 /* Ok */;
242 if (earliestCircularDependency !== Number.MAX_SAFE_INTEGER)
243 for (const dep of cycles.pop()) // cycle ends here
244 // update result for files that had a circular dependency on this one
245 this.dependenciesUpToDate[dep] = 2 /* Ok */;
246 }
247 if (parents.length === 0)
248 return true;
249 }
250 }
251 }
252 save() {
253 if (this.fileResults.size === 0)
254 return; // nothing to save
255 const oldState = this[oldStateSymbol];
256 if (oldState !== undefined && this.dependenciesUpToDate.every((v) => v === 2 /* Ok */)) {
257 // state is still good, only update results
258 const files = oldState.files.slice();
259 for (const [fileName, result] of this.fileResults) {
260 const index = this.lookupFileIndex(fileName, oldState);
261 files[index] = { ...files[index], ...result };
262 }
263 this.statePersistence.saveState(this.project, {
264 ...oldState,
265 files,
266 });
267 }
268 else {
269 this.statePersistence.saveState(this.project, this.aggregate());
270 }
271 }
272 aggregate() {
273 const additionalFiles = new Set();
274 const oldState = this.tryReuseOldState();
275 const sourceFiles = this.program.getSourceFiles();
276 const lookup = {};
277 const mapToIndex = ({ fileName }) => {
278 const relativeName = this.getRelativePath(fileName);
279 let index = lookup[relativeName];
280 if (index === undefined) {
281 index = sourceFiles.length + additionalFiles.size;
282 additionalFiles.add(fileName);
283 lookup[relativeName] = index;
284 }
285 return index;
286 };
287 const mapDependencies = (dependencies) => {
288 if (dependencies.size === 0)
289 return;
290 const result = {};
291 for (const [key, f] of dependencies)
292 result[key] = f === null
293 ? null
294 : this.sortById(Array.from(new Set(f))).map(mapToIndex);
295 return result;
296 };
297 const files = [];
298 for (let i = 0; i < sourceFiles.length; ++i)
299 lookup[this.getRelativePath(sourceFiles[i].fileName)] = i;
300 for (const file of sourceFiles) {
301 let results = this.fileResults.get(file.fileName);
302 if (results === undefined && oldState !== undefined) {
303 const index = this.lookupFileIndex(file.fileName, oldState);
304 if (index !== undefined) {
305 const old = oldState.files[index];
306 if (old.result !== undefined)
307 results = old;
308 }
309 }
310 if (results !== undefined && !this.isFileUpToDate(file.fileName)) {
311 log('Discarding outdated results for %s', file.fileName);
312 results = undefined;
313 }
314 files.push({
315 ...results,
316 id: this.getContentId(file.fileName),
317 dependencies: mapDependencies(this.resolver.getDependencies(file.fileName)),
318 });
319 }
320 for (const additional of additionalFiles)
321 files.push({ id: this.getContentId(additional) });
322 return {
323 files,
324 lookup,
325 v: STATE_VERSION,
326 ts: ts.version,
327 cs: this.caseSensitive,
328 global: this.sortById(this.resolver.getFilesAffectingGlobalScope()).map(mapToIndex),
329 options: this.optionsHash,
330 };
331 }
332 sortById(fileNames) {
333 return fileNames
334 .map((f) => ({ fileName: f, id: this.getContentId(f) }))
335 .sort(compareId);
336 }
337 lookupFileIndex(fileName, oldState) {
338 fileName = this.getRelativePath(fileName);
339 if (!oldState.cs && this.caseSensitive)
340 fileName = fileName.toLowerCase();
341 return oldState.lookup[fileName];
342 }
343 remapFileNames(oldState) {
344 // only need to remap if oldState is case sensitive and current host is case insensitive
345 if (!oldState.cs || this.caseSensitive)
346 return oldState;
347 const lookup = {};
348 for (const [key, value] of Object.entries(oldState.lookup))
349 lookup[key.toLowerCase()] = value;
350 return { ...oldState, lookup, cs: false };
351 }
352}
353tslib_1.__decorate([
354 bind_decorator_1.default,
355 tslib_1.__metadata("design:type", Function),
356 tslib_1.__metadata("design:paramtypes", [String]),
357 tslib_1.__metadata("design:returntype", void 0)
358], ProgramStateImpl.prototype, "computeContentId", null);
359tslib_1.__decorate([
360 bind_decorator_1.default,
361 tslib_1.__metadata("design:type", Function),
362 tslib_1.__metadata("design:paramtypes", [String]),
363 tslib_1.__metadata("design:returntype", void 0)
364], ProgramStateImpl.prototype, "makeRelativePath", null);
365function findCircularDependencyOfCycle(parents, circularDependencies, cycle) {
366 for (let i = 0; i < parents.length; ++i) {
367 const dep = circularDependencies[i];
368 if (dep !== Number.MAX_SAFE_INTEGER && cycle.has(parents[i]))
369 return dep;
370 }
371 /* istanbul ignore next */
372 throw new Error('should never happen');
373}
374function setCircularDependency(parents, circularDependencies, self, cycles, earliestCircularDependency) {
375 let cyclesToMerge = 0;
376 for (let i = circularDependencies.length - 1, inCycle = false; i >= earliestCircularDependency; --i) {
377 const dep = circularDependencies[i];
378 if (dep === Number.MAX_SAFE_INTEGER) {
379 inCycle = false;
380 }
381 else {
382 if (!inCycle) {
383 ++cyclesToMerge;
384 inCycle = true;
385 }
386 if (dep === i) {
387 inCycle = false; // if cycle ends here, the next parent might start a new one
388 }
389 else if (dep <= earliestCircularDependency) {
390 earliestCircularDependency = dep;
391 break;
392 }
393 }
394 }
395 let targetCycle;
396 if (cyclesToMerge === 0) {
397 targetCycle = new Set();
398 cycles.push(targetCycle);
399 }
400 else {
401 targetCycle = cycles[cycles.length - cyclesToMerge];
402 while (--cyclesToMerge)
403 for (const d of cycles.pop())
404 targetCycle.add(d);
405 }
406 targetCycle.add(self);
407 for (let i = circularDependencies.length - 1; i >= earliestCircularDependency; --i) {
408 targetCycle.add(parents[i]);
409 circularDependencies[i] = earliestCircularDependency;
410 }
411 return earliestCircularDependency;
412}
413function markAsOutdated(parents, index, cycles, results) {
414 results[index] = 1 /* Outdated */;
415 for (index of parents)
416 results[index] = 1 /* Outdated */;
417 for (const cycle of cycles)
418 for (index of cycle)
419 results[index] = 1 /* Outdated */;
420 return false;
421}
422function compareId(a, b) {
423 return +(a.id >= b.id) - +(a.id <= b.id);
424}
425const compilerOptionKinds = {
426 allowJs: 1 /* Value */,
427 allowSyntheticDefaultImports: 1 /* Value */,
428 allowUmdGlobalAccess: 1 /* Value */,
429 allowUnreachableCode: 1 /* Value */,
430 allowUnusedLabels: 1 /* Value */,
431 alwaysStrict: 1 /* Value */,
432 assumeChangesOnlyAffectDirectDependencies: 1 /* Value */,
433 baseUrl: 2 /* Path */,
434 charset: 1 /* Value */,
435 checkJs: 1 /* Value */,
436 composite: 1 /* Value */,
437 declaration: 1 /* Value */,
438 declarationDir: 2 /* Path */,
439 declarationMap: 1 /* Value */,
440 disableReferencedProjectLoad: 0 /* Ignore */,
441 disableSizeLimit: 1 /* Value */,
442 disableSourceOfProjectReferenceRedirect: 1 /* Value */,
443 disableSolutionSearching: 0 /* Ignore */,
444 downlevelIteration: 1 /* Value */,
445 emitBOM: 1 /* Value */,
446 emitDeclarationOnly: 1 /* Value */,
447 emitDecoratorMetadata: 1 /* Value */,
448 esModuleInterop: 1 /* Value */,
449 experimentalDecorators: 1 /* Value */,
450 forceConsistentCasingInFileNames: 1 /* Value */,
451 importHelpers: 1 /* Value */,
452 importsNotUsedAsValues: 1 /* Value */,
453 incremental: 1 /* Value */,
454 inlineSourceMap: 1 /* Value */,
455 inlineSources: 1 /* Value */,
456 isolatedModules: 1 /* Value */,
457 jsx: 1 /* Value */,
458 jsxFactory: 1 /* Value */,
459 jsxFragmentFactory: 1 /* Value */,
460 jsxImportSource: 1 /* Value */,
461 keyofStringsOnly: 1 /* Value */,
462 lib: 1 /* Value */,
463 locale: 1 /* Value */,
464 mapRoot: 1 /* Value */,
465 maxNodeModuleJsDepth: 1 /* Value */,
466 module: 1 /* Value */,
467 moduleResolution: 1 /* Value */,
468 newLine: 1 /* Value */,
469 noEmit: 1 /* Value */,
470 noEmitHelpers: 1 /* Value */,
471 noEmitOnError: 1 /* Value */,
472 noErrorTruncation: 1 /* Value */,
473 noFallthroughCasesInSwitch: 1 /* Value */,
474 noImplicitAny: 1 /* Value */,
475 noImplicitReturns: 1 /* Value */,
476 noImplicitThis: 1 /* Value */,
477 noImplicitUseStrict: 1 /* Value */,
478 noLib: 1 /* Value */,
479 noPropertyAccessFromIndexSignature: 1 /* Value */,
480 noResolve: 1 /* Value */,
481 noStrictGenericChecks: 1 /* Value */,
482 noUncheckedIndexedAccess: 1 /* Value */,
483 noUnusedLocals: 1 /* Value */,
484 noUnusedParameters: 1 /* Value */,
485 out: 1 /* Value */,
486 outDir: 2 /* Path */,
487 outFile: 2 /* Path */,
488 paths: 1 /* Value */,
489 pathsBasePath: 2 /* Path */,
490 preserveConstEnums: 1 /* Value */,
491 preserveSymlinks: 1 /* Value */,
492 project: 0 /* Ignore */,
493 reactNamespace: 1 /* Value */,
494 removeComments: 1 /* Value */,
495 resolveJsonModule: 1 /* Value */,
496 rootDir: 2 /* Path */,
497 rootDirs: 3 /* PathArray */,
498 skipDefaultLibCheck: 1 /* Value */,
499 skipLibCheck: 1 /* Value */,
500 sourceMap: 1 /* Value */,
501 sourceRoot: 1 /* Value */,
502 strict: 1 /* Value */,
503 strictBindCallApply: 1 /* Value */,
504 strictFunctionTypes: 1 /* Value */,
505 strictNullChecks: 1 /* Value */,
506 strictPropertyInitialization: 1 /* Value */,
507 stripInternal: 1 /* Value */,
508 suppressExcessPropertyErrors: 1 /* Value */,
509 suppressImplicitAnyIndexErrors: 1 /* Value */,
510 target: 1 /* Value */,
511 traceResolution: 1 /* Value */,
512 tsBuildInfoFile: 0 /* Ignore */,
513 typeRoots: 3 /* PathArray */,
514 types: 1 /* Value */,
515 useDefineForClassFields: 1 /* Value */,
516};
517function computeCompilerOptionsHash(options, relativeTo) {
518 const obj = {};
519 for (const key of Object.keys(options).sort()) {
520 switch (compilerOptionKinds[key]) {
521 case 1 /* Value */:
522 obj[key] = options[key];
523 break;
524 case 2 /* Path */:
525 obj[key] = makeRelativePath(options[key]);
526 break;
527 case 3 /* PathArray */:
528 obj[key] = options[key].map(makeRelativePath);
529 }
530 }
531 return '' + utils_1.djb2(JSON.stringify(obj));
532 function makeRelativePath(p) {
533 return utils_1.unixifyPath(path.relative(relativeTo, p));
534 }
535}
536//# sourceMappingURL=program-state.js.map
\No newline at end of file