@fimbul/wotan
Version:
Pluggable TypeScript and JavaScript linter
536 lines • 23.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ProgramStateFactory = void 0;
const tslib_1 = require("tslib");
const inversify_1 = require("inversify");
const ts = require("typescript");
const dependency_resolver_1 = require("./dependency-resolver");
const utils_1 = require("../utils");
const bind_decorator_1 = require("bind-decorator");
const ymir_1 = require("@fimbul/ymir");
const debug = require("debug");
const tsutils_1 = require("tsutils");
const path = require("path");
const log = debug('wotan:programState');
let ProgramStateFactory = class ProgramStateFactory {
constructor(resolverFactory, statePersistence, contentId) {
this.resolverFactory = resolverFactory;
this.statePersistence = statePersistence;
this.contentId = contentId;
}
create(program, host, tsconfigPath) {
return new ProgramStateImpl(host, program, this.resolverFactory.create(host, program), this.statePersistence, this.contentId, tsconfigPath);
}
};
ProgramStateFactory = tslib_1.__decorate([
inversify_1.injectable(),
tslib_1.__metadata("design:paramtypes", [dependency_resolver_1.DependencyResolverFactory,
ymir_1.StatePersistence,
ymir_1.ContentId])
], ProgramStateFactory);
exports.ProgramStateFactory = ProgramStateFactory;
const STATE_VERSION = 1;
const oldStateSymbol = Symbol('oldState');
class ProgramStateImpl {
constructor(host, program, resolver, statePersistence, contentId, project) {
this.host = host;
this.program = program;
this.resolver = resolver;
this.statePersistence = statePersistence;
this.contentId = contentId;
this.project = project;
this.projectDirectory = utils_1.unixifyPath(path.dirname(this.project));
this.caseSensitive = this.host.useCaseSensitiveFileNames();
this.canonicalProjectDirectory = this.caseSensitive ? this.projectDirectory : this.projectDirectory.toLowerCase();
this.optionsHash = computeCompilerOptionsHash(this.program.getCompilerOptions(), this.projectDirectory);
this.assumeChangesOnlyAffectDirectDependencies = tsutils_1.isCompilerOptionEnabled(this.program.getCompilerOptions(), 'assumeChangesOnlyAffectDirectDependencies');
this.contentIds = new Map();
this.fileResults = new Map();
this.relativePathNames = new Map();
this.recheckOldState = true;
// TODO this can be removed once ProjectHost correctly reflects applied fixed in readFile
this.contentIdHost = {
readFile: (f) => { var _a; return (_a = this.program.getSourceFile(f)) === null || _a === void 0 ? void 0 : _a.text; },
};
const oldState = this.statePersistence.loadState(project);
if ((oldState === null || oldState === void 0 ? void 0 : oldState.v) !== STATE_VERSION || oldState.ts !== ts.version || oldState.options !== this.optionsHash) {
this[oldStateSymbol] = undefined;
this.dependenciesUpToDate = new Uint8Array(0);
}
else {
this[oldStateSymbol] = this.remapFileNames(oldState);
this.dependenciesUpToDate = new Uint8Array(oldState.files.length);
}
}
/** get old state if global files didn't change */
tryReuseOldState() {
const oldState = this[oldStateSymbol];
if (oldState === undefined || !this.recheckOldState)
return oldState;
const filesAffectingGlobalScope = this.resolver.getFilesAffectingGlobalScope();
if (oldState.global.length !== filesAffectingGlobalScope.length)
return this[oldStateSymbol] = undefined;
const globalFilesWithId = this.sortById(filesAffectingGlobalScope);
for (let i = 0; i < globalFilesWithId.length; ++i) {
const index = oldState.global[i];
if (globalFilesWithId[i].id !== oldState.files[index].id ||
!this.assumeChangesOnlyAffectDirectDependencies &&
!this.fileDependenciesUpToDate(globalFilesWithId[i].fileName, index, oldState))
return this[oldStateSymbol] = undefined;
}
this.recheckOldState = false;
return oldState;
}
update(program, updatedFile) {
this.program = program;
this.resolver.update(program, updatedFile);
this.contentIds.delete(updatedFile);
this.recheckOldState = true;
this.dependenciesUpToDate.fill(0 /* Unknown */);
}
getContentId(file) {
return utils_1.resolveCachedResult(this.contentIds, file, this.computeContentId);
}
computeContentId(file) {
return this.contentId.forFile(file, this.contentIdHost);
}
getRelativePath(fileName) {
return utils_1.resolveCachedResult(this.relativePathNames, fileName, this.makeRelativePath);
}
makeRelativePath(fileName) {
return utils_1.unixifyPath(path.relative(this.canonicalProjectDirectory, this.caseSensitive ? fileName : fileName.toLowerCase()));
}
getUpToDateResult(fileName, configHash) {
const oldState = this.tryReuseOldState();
if (oldState === undefined)
return;
const index = this.lookupFileIndex(fileName, oldState);
if (index === undefined)
return;
const old = oldState.files[index];
if (old.result === undefined ||
old.config !== configHash ||
old.id !== this.getContentId(fileName) ||
!this.fileDependenciesUpToDate(fileName, index, oldState))
return;
log('reusing state for %s', fileName);
return old.result;
}
setFileResult(fileName, configHash, result) {
if (!this.isFileUpToDate(fileName)) {
log('File %s is outdated, merging current state into old state', fileName);
// we need to create a state where the file is up-to-date
// so we replace the old state with the current state
// this includes all results from old state that are still up-to-date and all file results if they are still valid
const newState = this[oldStateSymbol] = this.aggregate();
this.recheckOldState = false;
this.fileResults = new Map();
this.dependenciesUpToDate = new Uint8Array(newState.files.length).fill(2 /* Ok */);
}
this.fileResults.set(fileName, { result, config: configHash });
}
isFileUpToDate(fileName) {
const oldState = this.tryReuseOldState();
if (oldState === undefined)
return false;
const index = this.lookupFileIndex(fileName, oldState);
if (index === undefined || oldState.files[index].id !== this.getContentId(fileName))
return false;
switch (this.dependenciesUpToDate[index]) {
case 0 /* Unknown */:
return this.fileDependenciesUpToDate(fileName, index, oldState);
case 2 /* Ok */:
return true;
case 1 /* Outdated */:
return false;
}
}
fileDependenciesUpToDate(fileName, index, oldState) {
// File names that are waiting to be processed, each iteration of the loop processes one file
const fileNameQueue = [fileName];
// For each entry in `fileNameQueue` this holds the index of that file in `oldState.files`
const indexQueue = [index];
// If a file is waiting for its children to be processed, it is moved from `indexQueue` to `parents`
const parents = [];
// For each entry in `parents` this holds the number of children that still need to be processed for that file
const childCounts = [];
// For each entry in `parents` this holds the index in of the earliest circular dependency in `parents`.
// For example, a value of `[Number.MAX_SAFE_INTEGER, 0]` means that `parents[1]` has a dependency on `parents[0]` (the root file).
const circularDependenciesQueue = [];
// If a file has a circular on one of its parents, it is moved from `indexQueue` to the current cycle
// or creates a new cycle if its parent is not already in a cycle.
const cycles = [];
while (true) {
index = indexQueue.pop();
fileName = fileNameQueue.pop();
processFile: {
switch (this.dependenciesUpToDate[index]) {
case 1 /* Outdated */:
return markAsOutdated(parents, index, cycles, this.dependenciesUpToDate);
case 2 /* Ok */:
break processFile;
}
for (const cycle of cycles) {
if (cycle.has(index)) {
// we already know this is a circular dependency, skip this one and simply mark the parent as circular
setCircularDependency(parents, circularDependenciesQueue, index, cycles, findCircularDependencyOfCycle(parents, circularDependenciesQueue, cycle));
break processFile;
}
}
let earliestCircularDependency = Number.MAX_SAFE_INTEGER;
let childCount = 0;
const old = oldState.files[index];
const dependencies = this.resolver.getDependencies(fileName);
const keys = old.dependencies === undefined ? utils_1.emptyArray : Object.keys(old.dependencies);
if (dependencies.size !== keys.length)
return markAsOutdated(parents, index, cycles, this.dependenciesUpToDate);
for (const key of keys) {
let newDeps = dependencies.get(key);
const oldDeps = old.dependencies[key];
if (oldDeps === null) {
if (newDeps !== null)
return markAsOutdated(parents, index, cycles, this.dependenciesUpToDate);
continue;
}
if (newDeps === null)
return markAsOutdated(parents, index, cycles, this.dependenciesUpToDate);
newDeps = Array.from(new Set(newDeps));
if (newDeps.length !== oldDeps.length)
return markAsOutdated(parents, index, cycles, this.dependenciesUpToDate);
const newDepsWithId = this.sortById(newDeps);
for (let i = 0; i < newDepsWithId.length; ++i) {
const oldDepState = oldState.files[oldDeps[i]];
if (newDepsWithId[i].id !== oldDepState.id)
return markAsOutdated(parents, index, cycles, this.dependenciesUpToDate);
if (!this.assumeChangesOnlyAffectDirectDependencies && fileName !== newDepsWithId[i].fileName) {
const indexInQueue = parents.indexOf(oldDeps[i]);
if (indexInQueue === -1) {
// no circular dependency
fileNameQueue.push(newDepsWithId[i].fileName);
indexQueue.push(oldDeps[i]);
++childCount;
}
else if (indexInQueue < earliestCircularDependency) {
earliestCircularDependency = indexInQueue;
}
}
}
}
if (earliestCircularDependency !== Number.MAX_SAFE_INTEGER) {
earliestCircularDependency =
setCircularDependency(parents, circularDependenciesQueue, index, cycles, earliestCircularDependency);
}
else if (childCount === 0) {
this.dependenciesUpToDate[index] = 2 /* Ok */;
}
if (childCount !== 0) {
parents.push(index);
childCounts.push(childCount);
circularDependenciesQueue.push(earliestCircularDependency);
continue;
}
}
// we only get here for files with no children to process
if (parents.length === 0)
return true; // only happens if the initial file has no dependencies or they are all already known as Ok
while (--childCounts[childCounts.length - 1] === 0) {
index = parents.pop();
childCounts.pop();
const earliestCircularDependency = circularDependenciesQueue.pop();
if (earliestCircularDependency >= parents.length) {
this.dependenciesUpToDate[index] = 2 /* Ok */;
if (earliestCircularDependency !== Number.MAX_SAFE_INTEGER)
for (const dep of cycles.pop()) // cycle ends here
// update result for files that had a circular dependency on this one
this.dependenciesUpToDate[dep] = 2 /* Ok */;
}
if (parents.length === 0)
return true;
}
}
}
save() {
if (this.fileResults.size === 0)
return; // nothing to save
const oldState = this[oldStateSymbol];
if (oldState !== undefined && this.dependenciesUpToDate.every((v) => v === 2 /* Ok */)) {
// state is still good, only update results
const files = oldState.files.slice();
for (const [fileName, result] of this.fileResults) {
const index = this.lookupFileIndex(fileName, oldState);
files[index] = { ...files[index], ...result };
}
this.statePersistence.saveState(this.project, {
...oldState,
files,
});
}
else {
this.statePersistence.saveState(this.project, this.aggregate());
}
}
aggregate() {
const additionalFiles = new Set();
const oldState = this.tryReuseOldState();
const sourceFiles = this.program.getSourceFiles();
const lookup = {};
const mapToIndex = ({ fileName }) => {
const relativeName = this.getRelativePath(fileName);
let index = lookup[relativeName];
if (index === undefined) {
index = sourceFiles.length + additionalFiles.size;
additionalFiles.add(fileName);
lookup[relativeName] = index;
}
return index;
};
const mapDependencies = (dependencies) => {
if (dependencies.size === 0)
return;
const result = {};
for (const [key, f] of dependencies)
result[key] = f === null
? null
: this.sortById(Array.from(new Set(f))).map(mapToIndex);
return result;
};
const files = [];
for (let i = 0; i < sourceFiles.length; ++i)
lookup[this.getRelativePath(sourceFiles[i].fileName)] = i;
for (const file of sourceFiles) {
let results = this.fileResults.get(file.fileName);
if (results === undefined && oldState !== undefined) {
const index = this.lookupFileIndex(file.fileName, oldState);
if (index !== undefined) {
const old = oldState.files[index];
if (old.result !== undefined)
results = old;
}
}
if (results !== undefined && !this.isFileUpToDate(file.fileName)) {
log('Discarding outdated results for %s', file.fileName);
results = undefined;
}
files.push({
...results,
id: this.getContentId(file.fileName),
dependencies: mapDependencies(this.resolver.getDependencies(file.fileName)),
});
}
for (const additional of additionalFiles)
files.push({ id: this.getContentId(additional) });
return {
files,
lookup,
v: STATE_VERSION,
ts: ts.version,
cs: this.caseSensitive,
global: this.sortById(this.resolver.getFilesAffectingGlobalScope()).map(mapToIndex),
options: this.optionsHash,
};
}
sortById(fileNames) {
return fileNames
.map((f) => ({ fileName: f, id: this.getContentId(f) }))
.sort(compareId);
}
lookupFileIndex(fileName, oldState) {
fileName = this.getRelativePath(fileName);
if (!oldState.cs && this.caseSensitive)
fileName = fileName.toLowerCase();
return oldState.lookup[fileName];
}
remapFileNames(oldState) {
// only need to remap if oldState is case sensitive and current host is case insensitive
if (!oldState.cs || this.caseSensitive)
return oldState;
const lookup = {};
for (const [key, value] of Object.entries(oldState.lookup))
lookup[key.toLowerCase()] = value;
return { ...oldState, lookup, cs: false };
}
}
tslib_1.__decorate([
bind_decorator_1.default,
tslib_1.__metadata("design:type", Function),
tslib_1.__metadata("design:paramtypes", [String]),
tslib_1.__metadata("design:returntype", void 0)
], ProgramStateImpl.prototype, "computeContentId", null);
tslib_1.__decorate([
bind_decorator_1.default,
tslib_1.__metadata("design:type", Function),
tslib_1.__metadata("design:paramtypes", [String]),
tslib_1.__metadata("design:returntype", void 0)
], ProgramStateImpl.prototype, "makeRelativePath", null);
function findCircularDependencyOfCycle(parents, circularDependencies, cycle) {
for (let i = 0; i < parents.length; ++i) {
const dep = circularDependencies[i];
if (dep !== Number.MAX_SAFE_INTEGER && cycle.has(parents[i]))
return dep;
}
/* istanbul ignore next */
throw new Error('should never happen');
}
function setCircularDependency(parents, circularDependencies, self, cycles, earliestCircularDependency) {
let cyclesToMerge = 0;
for (let i = circularDependencies.length - 1, inCycle = false; i >= earliestCircularDependency; --i) {
const dep = circularDependencies[i];
if (dep === Number.MAX_SAFE_INTEGER) {
inCycle = false;
}
else {
if (!inCycle) {
++cyclesToMerge;
inCycle = true;
}
if (dep === i) {
inCycle = false; // if cycle ends here, the next parent might start a new one
}
else if (dep <= earliestCircularDependency) {
earliestCircularDependency = dep;
break;
}
}
}
let targetCycle;
if (cyclesToMerge === 0) {
targetCycle = new Set();
cycles.push(targetCycle);
}
else {
targetCycle = cycles[cycles.length - cyclesToMerge];
while (--cyclesToMerge)
for (const d of cycles.pop())
targetCycle.add(d);
}
targetCycle.add(self);
for (let i = circularDependencies.length - 1; i >= earliestCircularDependency; --i) {
targetCycle.add(parents[i]);
circularDependencies[i] = earliestCircularDependency;
}
return earliestCircularDependency;
}
function markAsOutdated(parents, index, cycles, results) {
results[index] = 1 /* Outdated */;
for (index of parents)
results[index] = 1 /* Outdated */;
for (const cycle of cycles)
for (index of cycle)
results[index] = 1 /* Outdated */;
return false;
}
function compareId(a, b) {
return +(a.id >= b.id) - +(a.id <= b.id);
}
const compilerOptionKinds = {
allowJs: 1 /* Value */,
allowSyntheticDefaultImports: 1 /* Value */,
allowUmdGlobalAccess: 1 /* Value */,
allowUnreachableCode: 1 /* Value */,
allowUnusedLabels: 1 /* Value */,
alwaysStrict: 1 /* Value */,
assumeChangesOnlyAffectDirectDependencies: 1 /* Value */,
baseUrl: 2 /* Path */,
charset: 1 /* Value */,
checkJs: 1 /* Value */,
composite: 1 /* Value */,
declaration: 1 /* Value */,
declarationDir: 2 /* Path */,
declarationMap: 1 /* Value */,
disableReferencedProjectLoad: 0 /* Ignore */,
disableSizeLimit: 1 /* Value */,
disableSourceOfProjectReferenceRedirect: 1 /* Value */,
disableSolutionSearching: 0 /* Ignore */,
downlevelIteration: 1 /* Value */,
emitBOM: 1 /* Value */,
emitDeclarationOnly: 1 /* Value */,
emitDecoratorMetadata: 1 /* Value */,
esModuleInterop: 1 /* Value */,
experimentalDecorators: 1 /* Value */,
forceConsistentCasingInFileNames: 1 /* Value */,
importHelpers: 1 /* Value */,
importsNotUsedAsValues: 1 /* Value */,
incremental: 1 /* Value */,
inlineSourceMap: 1 /* Value */,
inlineSources: 1 /* Value */,
isolatedModules: 1 /* Value */,
jsx: 1 /* Value */,
jsxFactory: 1 /* Value */,
jsxFragmentFactory: 1 /* Value */,
jsxImportSource: 1 /* Value */,
keyofStringsOnly: 1 /* Value */,
lib: 1 /* Value */,
locale: 1 /* Value */,
mapRoot: 1 /* Value */,
maxNodeModuleJsDepth: 1 /* Value */,
module: 1 /* Value */,
moduleResolution: 1 /* Value */,
newLine: 1 /* Value */,
noEmit: 1 /* Value */,
noEmitHelpers: 1 /* Value */,
noEmitOnError: 1 /* Value */,
noErrorTruncation: 1 /* Value */,
noFallthroughCasesInSwitch: 1 /* Value */,
noImplicitAny: 1 /* Value */,
noImplicitReturns: 1 /* Value */,
noImplicitThis: 1 /* Value */,
noImplicitUseStrict: 1 /* Value */,
noLib: 1 /* Value */,
noPropertyAccessFromIndexSignature: 1 /* Value */,
noResolve: 1 /* Value */,
noStrictGenericChecks: 1 /* Value */,
noUncheckedIndexedAccess: 1 /* Value */,
noUnusedLocals: 1 /* Value */,
noUnusedParameters: 1 /* Value */,
out: 1 /* Value */,
outDir: 2 /* Path */,
outFile: 2 /* Path */,
paths: 1 /* Value */,
pathsBasePath: 2 /* Path */,
preserveConstEnums: 1 /* Value */,
preserveSymlinks: 1 /* Value */,
project: 0 /* Ignore */,
reactNamespace: 1 /* Value */,
removeComments: 1 /* Value */,
resolveJsonModule: 1 /* Value */,
rootDir: 2 /* Path */,
rootDirs: 3 /* PathArray */,
skipDefaultLibCheck: 1 /* Value */,
skipLibCheck: 1 /* Value */,
sourceMap: 1 /* Value */,
sourceRoot: 1 /* Value */,
strict: 1 /* Value */,
strictBindCallApply: 1 /* Value */,
strictFunctionTypes: 1 /* Value */,
strictNullChecks: 1 /* Value */,
strictPropertyInitialization: 1 /* Value */,
stripInternal: 1 /* Value */,
suppressExcessPropertyErrors: 1 /* Value */,
suppressImplicitAnyIndexErrors: 1 /* Value */,
target: 1 /* Value */,
traceResolution: 1 /* Value */,
tsBuildInfoFile: 0 /* Ignore */,
typeRoots: 3 /* PathArray */,
types: 1 /* Value */,
useDefineForClassFields: 1 /* Value */,
};
function computeCompilerOptionsHash(options, relativeTo) {
const obj = {};
for (const key of Object.keys(options).sort()) {
switch (compilerOptionKinds[key]) {
case 1 /* Value */:
obj[key] = options[key];
break;
case 2 /* Path */:
obj[key] = makeRelativePath(options[key]);
break;
case 3 /* PathArray */:
obj[key] = options[key].map(makeRelativePath);
}
}
return '' + utils_1.djb2(JSON.stringify(obj));
function makeRelativePath(p) {
return utils_1.unixifyPath(path.relative(relativeTo, p));
}
}
//# sourceMappingURL=program-state.js.map