UNPKG

@fimbul/wotan

Version:

Pluggable TypeScript and JavaScript linter

536 lines 23.9 kB
"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