1 | "use strict";
|
2 | Object.defineProperty(exports, "__esModule", { value: true });
|
3 | exports.ProgramStateFactory = void 0;
|
4 | const tslib_1 = require("tslib");
|
5 | const inversify_1 = require("inversify");
|
6 | const ts = require("typescript");
|
7 | const dependency_resolver_1 = require("./dependency-resolver");
|
8 | const utils_1 = require("../utils");
|
9 | const bind_decorator_1 = require("bind-decorator");
|
10 | const ymir_1 = require("@fimbul/ymir");
|
11 | const debug = require("debug");
|
12 | const tsutils_1 = require("tsutils");
|
13 | const path = require("path");
|
14 | const log = debug('wotan:programState');
|
15 | let 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 | };
|
25 | ProgramStateFactory = 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);
|
31 | exports.ProgramStateFactory = ProgramStateFactory;
|
32 | const STATE_VERSION = 1;
|
33 | const oldStateSymbol = Symbol('oldState');
|
34 | class 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 |
|
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 |
|
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 );
|
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 |
|
123 |
|
124 |
|
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 );
|
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 :
|
141 | return this.fileDependenciesUpToDate(fileName, index, oldState);
|
142 | case 2 :
|
143 | return true;
|
144 | case 1 :
|
145 | return false;
|
146 | }
|
147 | }
|
148 | fileDependenciesUpToDate(fileName, index, oldState) {
|
149 |
|
150 | const fileNameQueue = [fileName];
|
151 |
|
152 | const indexQueue = [index];
|
153 |
|
154 | const parents = [];
|
155 |
|
156 | const childCounts = [];
|
157 |
|
158 |
|
159 | const circularDependenciesQueue = [];
|
160 |
|
161 |
|
162 | const cycles = [];
|
163 | while (true) {
|
164 | index = indexQueue.pop();
|
165 | fileName = fileNameQueue.pop();
|
166 | processFile: {
|
167 | switch (this.dependenciesUpToDate[index]) {
|
168 | case 1 :
|
169 | return markAsOutdated(parents, index, cycles, this.dependenciesUpToDate);
|
170 | case 2 :
|
171 | break processFile;
|
172 | }
|
173 | for (const cycle of cycles) {
|
174 | if (cycle.has(index)) {
|
175 |
|
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 |
|
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 ;
|
225 | }
|
226 | if (childCount !== 0) {
|
227 | parents.push(index);
|
228 | childCounts.push(childCount);
|
229 | circularDependenciesQueue.push(earliestCircularDependency);
|
230 | continue;
|
231 | }
|
232 | }
|
233 |
|
234 | if (parents.length === 0)
|
235 | return true;
|
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 ;
|
242 | if (earliestCircularDependency !== Number.MAX_SAFE_INTEGER)
|
243 | for (const dep of cycles.pop())
|
244 |
|
245 | this.dependenciesUpToDate[dep] = 2 ;
|
246 | }
|
247 | if (parents.length === 0)
|
248 | return true;
|
249 | }
|
250 | }
|
251 | }
|
252 | save() {
|
253 | if (this.fileResults.size === 0)
|
254 | return;
|
255 | const oldState = this[oldStateSymbol];
|
256 | if (oldState !== undefined && this.dependenciesUpToDate.every((v) => v === 2 )) {
|
257 |
|
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 |
|
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 | }
|
353 | tslib_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);
|
359 | tslib_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);
|
365 | function 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 |
|
372 | throw new Error('should never happen');
|
373 | }
|
374 | function 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;
|
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 | }
|
413 | function markAsOutdated(parents, index, cycles, results) {
|
414 | results[index] = 1 ;
|
415 | for (index of parents)
|
416 | results[index] = 1 ;
|
417 | for (const cycle of cycles)
|
418 | for (index of cycle)
|
419 | results[index] = 1 ;
|
420 | return false;
|
421 | }
|
422 | function compareId(a, b) {
|
423 | return +(a.id >= b.id) - +(a.id <= b.id);
|
424 | }
|
425 | const compilerOptionKinds = {
|
426 | allowJs: 1 ,
|
427 | allowSyntheticDefaultImports: 1 ,
|
428 | allowUmdGlobalAccess: 1 ,
|
429 | allowUnreachableCode: 1 ,
|
430 | allowUnusedLabels: 1 ,
|
431 | alwaysStrict: 1 ,
|
432 | assumeChangesOnlyAffectDirectDependencies: 1 ,
|
433 | baseUrl: 2 ,
|
434 | charset: 1 ,
|
435 | checkJs: 1 ,
|
436 | composite: 1 ,
|
437 | declaration: 1 ,
|
438 | declarationDir: 2 ,
|
439 | declarationMap: 1 ,
|
440 | disableReferencedProjectLoad: 0 ,
|
441 | disableSizeLimit: 1 ,
|
442 | disableSourceOfProjectReferenceRedirect: 1 ,
|
443 | disableSolutionSearching: 0 ,
|
444 | downlevelIteration: 1 ,
|
445 | emitBOM: 1 ,
|
446 | emitDeclarationOnly: 1 ,
|
447 | emitDecoratorMetadata: 1 ,
|
448 | esModuleInterop: 1 ,
|
449 | experimentalDecorators: 1 ,
|
450 | forceConsistentCasingInFileNames: 1 ,
|
451 | importHelpers: 1 ,
|
452 | importsNotUsedAsValues: 1 ,
|
453 | incremental: 1 ,
|
454 | inlineSourceMap: 1 ,
|
455 | inlineSources: 1 ,
|
456 | isolatedModules: 1 ,
|
457 | jsx: 1 ,
|
458 | jsxFactory: 1 ,
|
459 | jsxFragmentFactory: 1 ,
|
460 | jsxImportSource: 1 ,
|
461 | keyofStringsOnly: 1 ,
|
462 | lib: 1 ,
|
463 | locale: 1 ,
|
464 | mapRoot: 1 ,
|
465 | maxNodeModuleJsDepth: 1 ,
|
466 | module: 1 ,
|
467 | moduleResolution: 1 ,
|
468 | newLine: 1 ,
|
469 | noEmit: 1 ,
|
470 | noEmitHelpers: 1 ,
|
471 | noEmitOnError: 1 ,
|
472 | noErrorTruncation: 1 ,
|
473 | noFallthroughCasesInSwitch: 1 ,
|
474 | noImplicitAny: 1 ,
|
475 | noImplicitReturns: 1 ,
|
476 | noImplicitThis: 1 ,
|
477 | noImplicitUseStrict: 1 ,
|
478 | noLib: 1 ,
|
479 | noPropertyAccessFromIndexSignature: 1 ,
|
480 | noResolve: 1 ,
|
481 | noStrictGenericChecks: 1 ,
|
482 | noUncheckedIndexedAccess: 1 ,
|
483 | noUnusedLocals: 1 ,
|
484 | noUnusedParameters: 1 ,
|
485 | out: 1 ,
|
486 | outDir: 2 ,
|
487 | outFile: 2 ,
|
488 | paths: 1 ,
|
489 | pathsBasePath: 2 ,
|
490 | preserveConstEnums: 1 ,
|
491 | preserveSymlinks: 1 ,
|
492 | project: 0 ,
|
493 | reactNamespace: 1 ,
|
494 | removeComments: 1 ,
|
495 | resolveJsonModule: 1 ,
|
496 | rootDir: 2 ,
|
497 | rootDirs: 3 ,
|
498 | skipDefaultLibCheck: 1 ,
|
499 | skipLibCheck: 1 ,
|
500 | sourceMap: 1 ,
|
501 | sourceRoot: 1 ,
|
502 | strict: 1 ,
|
503 | strictBindCallApply: 1 ,
|
504 | strictFunctionTypes: 1 ,
|
505 | strictNullChecks: 1 ,
|
506 | strictPropertyInitialization: 1 ,
|
507 | stripInternal: 1 ,
|
508 | suppressExcessPropertyErrors: 1 ,
|
509 | suppressImplicitAnyIndexErrors: 1 ,
|
510 | target: 1 ,
|
511 | traceResolution: 1 ,
|
512 | tsBuildInfoFile: 0 ,
|
513 | typeRoots: 3 ,
|
514 | types: 1 ,
|
515 | useDefineForClassFields: 1 ,
|
516 | };
|
517 | function computeCompilerOptionsHash(options, relativeTo) {
|
518 | const obj = {};
|
519 | for (const key of Object.keys(options).sort()) {
|
520 | switch (compilerOptionKinds[key]) {
|
521 | case 1 :
|
522 | obj[key] = options[key];
|
523 | break;
|
524 | case 2 :
|
525 | obj[key] = makeRelativePath(options[key]);
|
526 | break;
|
527 | case 3 :
|
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 |
|
\ | No newline at end of file |