@fimbul/wotan
Version:
Pluggable TypeScript and JavaScript linter
262 lines • 12.1 kB
JavaScript
"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