UNPKG

@fimbul/wotan

Version:

Pluggable TypeScript and JavaScript linter

262 lines 12.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.DependencyResolverFactory = void 0; const tslib_1 = require("tslib"); const inversify_1 = require("inversify"); const ts = require("typescript"); const tsutils_1 = require("tsutils"); const utils_1 = require("../utils"); const bind_decorator_1 = require("bind-decorator"); const path = require("path"); let DependencyResolverFactory = class DependencyResolverFactory { create(host, program) { return new DependencyResolverImpl(host, program); } }; DependencyResolverFactory = tslib_1.__decorate([ inversify_1.injectable() ], DependencyResolverFactory); exports.DependencyResolverFactory = DependencyResolverFactory; class DependencyResolverImpl { constructor(host, program) { var _a, _b; this.host = host; this.program = program; this.dependencies = new Map(); this.fileToProjectReference = undefined; this.fileMetadata = new Map(); this.compilerOptions = this.program.getCompilerOptions(); this.useSourceOfProjectReferenceRedirect = ((_b = (_a = this.host).useSourceOfProjectReferenceRedirect) === null || _b === void 0 ? void 0 : _b.call(_a)) === true && !this.compilerOptions.disableSourceOfProjectReferenceRedirect; this.state = undefined; } update(program, updatedFile) { this.state = undefined; this.dependencies.delete(updatedFile); this.fileMetadata.delete(updatedFile); this.program = program; } buildState() { const affectsGlobalScope = []; const ambientModules = new Map(); const patternAmbientModules = new Map(); const moduleAugmentationsTemp = new Map(); for (const file of this.program.getSourceFiles()) { const meta = this.getFileMetaData(file.fileName); if (meta.affectsGlobalScope) affectsGlobalScope.push(file.fileName); for (const ambientModule of meta.ambientModules) { const map = meta.isExternalModule ? moduleAugmentationsTemp : ambientModule.includes('*') ? patternAmbientModules : ambientModules; addToList(map, ambientModule, file.fileName); } } const moduleAugmentations = new Map(); for (const [module, files] of moduleAugmentationsTemp) { // if an ambient module with the same identifier exists, the augmentation always applies to that const ambientModuleAffectingFiles = ambientModules.get(module); if (ambientModuleAffectingFiles !== undefined) { ambientModuleAffectingFiles.push(...files); continue; } for (const file of files) { const resolved = this.getExternalReferences(file).get(module); // if an augmentation's identifier can be resolved from the declaring file, the augmentation applies to the resolved path if (resolved != null) { // tslint:disable-line:triple-equals addToList(moduleAugmentations, resolved, file); } else { // if a pattern ambient module matches the augmented identifier, the augmentation applies to that const matchingPattern = getBestMatchingPattern(module, patternAmbientModules.keys()); if (matchingPattern !== undefined) addToList(patternAmbientModules, matchingPattern, file); } } } return { affectsGlobalScope, ambientModules, moduleAugmentations, patternAmbientModules, }; } getFilesAffectingGlobalScope() { var _a; return ((_a = this.state) !== null && _a !== void 0 ? _a : (this.state = this.buildState())).affectsGlobalScope; } getDependencies(file) { var _a; const result = new Map(); if (this.program.getSourceFile(file) === undefined) return result; (_a = this.state) !== null && _a !== void 0 ? _a : (this.state = this.buildState()); { const augmentations = this.state.moduleAugmentations.get(file); if (augmentations !== undefined) result.set('\0', augmentations); } for (const [identifier, resolved] of this.getExternalReferences(file)) { const filesAffectingAmbientModule = this.state.ambientModules.get(identifier); if (filesAffectingAmbientModule !== undefined) { result.set(identifier, filesAffectingAmbientModule); } else if (resolved !== null) { const list = [resolved]; const augmentations = this.state.moduleAugmentations.get(resolved); if (augmentations !== undefined) list.push(...augmentations); result.set(identifier, list); } else { const pattern = getBestMatchingPattern(identifier, this.state.patternAmbientModules.keys()); if (pattern !== undefined) { result.set(identifier, this.state.patternAmbientModules.get(pattern)); } else { result.set(identifier, null); } } } const meta = this.fileMetadata.get(file); if (!meta.isExternalModule) for (const ambientModule of meta.ambientModules) result.set(ambientModule, this.state[ambientModule.includes('*') ? 'patternAmbientModules' : 'ambientModules'].get(ambientModule)); return result; } getFileMetaData(fileName) { return utils_1.resolveCachedResult(this.fileMetadata, fileName, this.collectMetaDataForFile); } getExternalReferences(fileName) { return utils_1.resolveCachedResult(this.dependencies, fileName, this.collectExternalReferences); } collectMetaDataForFile(fileName) { return collectFileMetadata(this.program.getSourceFile(fileName)); } collectExternalReferences(fileName) { var _a; // TODO add tslib if importHelpers is enabled const sourceFile = this.program.getSourceFile(fileName); const references = new Set(tsutils_1.findImports(sourceFile, tsutils_1.ImportKind.All, false).map(({ text }) => text)); if (ts.isExternalModule(sourceFile)) for (const augmentation of this.getFileMetaData(fileName).ambientModules) references.add(augmentation); const result = new Map(); if (references.size === 0) return result; (_a = this.fileToProjectReference) !== null && _a !== void 0 ? _a : (this.fileToProjectReference = createProjectReferenceMap(this.program.getResolvedProjectReferences())); const arr = Array.from(references); const resolved = this.host.resolveModuleNames(arr, fileName, undefined, this.fileToProjectReference.get(fileName), this.compilerOptions); for (let i = 0; i < resolved.length; ++i) { const current = resolved[i]; if (current === undefined) { result.set(arr[i], null); } else { const projectReference = this.useSourceOfProjectReferenceRedirect ? this.fileToProjectReference.get(current.resolvedFileName) : undefined; if (projectReference === undefined) { result.set(arr[i], current.resolvedFileName); } else if (projectReference.commandLine.options.outFile) { // with outFile the files must be global anyway, so we don't care about the exact file result.set(arr[i], projectReference.commandLine.fileNames[0]); } else { result.set(arr[i], getSourceOfProjectReferenceRedirect(current.resolvedFileName, projectReference)); } } } return result; } } 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) ], DependencyResolverImpl.prototype, "collectMetaDataForFile", 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", Map) ], DependencyResolverImpl.prototype, "collectExternalReferences", null); function getBestMatchingPattern(moduleName, patternAmbientModules) { // TypeScript uses the pattern with the longest matching prefix let longestMatchLength = -1; let longestMatch; for (const pattern of patternAmbientModules) { if (moduleName.length < pattern.length - 1) continue; // compare length without the wildcard first, to avoid false positives like 'foo' matching 'foo*oo' const index = pattern.indexOf('*'); if (index > longestMatchLength && moduleName.startsWith(pattern.substring(0, index)) && moduleName.endsWith(pattern.substring(index + 1))) { longestMatchLength = index; longestMatch = pattern; } } return longestMatch; } function createProjectReferenceMap(references) { const result = new Map(); for (const ref of utils_1.iterateProjectReferences(references)) for (const file of utils_1.getOutputFileNamesOfProjectReference(ref)) result.set(file, ref); return result; } function addToList(map, key, value) { const arr = map.get(key); if (arr === undefined) { map.set(key, [value]); } else { arr.push(value); } } function collectFileMetadata(sourceFile) { let affectsGlobalScope; const ambientModules = new Set(); const isExternalModule = ts.isExternalModule(sourceFile); for (const statement of sourceFile.statements) { if (statement.flags & ts.NodeFlags.GlobalAugmentation) { affectsGlobalScope = true; } else if (tsutils_1.isModuleDeclaration(statement) && statement.name.kind === ts.SyntaxKind.StringLiteral) { ambientModules.add(statement.name.text); if (!isExternalModule && !affectsGlobalScope && statement.body !== undefined) { // search for global augmentations in ambient module blocks for (const s of statement.body.statements) { if (s.flags & ts.NodeFlags.GlobalAugmentation) { affectsGlobalScope = true; break; } } } } else if (tsutils_1.isNamespaceExportDeclaration(statement)) { affectsGlobalScope = true; } else if (affectsGlobalScope === undefined) { // files that only consist of ambient modules do not affect global scope affectsGlobalScope = !isExternalModule; } } return { ambientModules, isExternalModule, affectsGlobalScope: affectsGlobalScope === true }; } function getSourceOfProjectReferenceRedirect(outputFileName, ref) { const options = ref.commandLine.options; const projectDirectory = path.dirname(ref.sourceFile.fileName); const origin = utils_1.unixifyPath(path.resolve(options.rootDir || projectDirectory, path.relative(options.declarationDir || options.outDir || /* istanbul ignore next */ projectDirectory, outputFileName.slice(0, -5)))); for (const extension of ['.ts', '.tsx', '.js', '.jsx']) { const name = origin + extension; if (ref.commandLine.fileNames.includes(name)) return name; } /* istanbul ignore next */ return outputFileName; // should never happen } //# sourceMappingURL=dependency-resolver.js.map