dts-bundle-generator
Version:
DTS Bundle Generator
216 lines (215 loc) • 12 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.CollisionsResolver = void 0;
const ts = require("typescript");
const typescript_1 = require("./helpers/typescript");
const logger_1 = require("./logger");
const renamingSupportedSymbols = [
ts.SymbolFlags.Alias,
ts.SymbolFlags.Variable,
ts.SymbolFlags.Class,
ts.SymbolFlags.Enum,
ts.SymbolFlags.Function,
ts.SymbolFlags.Interface,
ts.SymbolFlags.NamespaceModule,
ts.SymbolFlags.TypeAlias,
ts.SymbolFlags.ValueModule,
];
/**
* A class that holds information about top-level scoped names and allows to get collision-free names in one occurred.
*/
class CollisionsResolver {
constructor(typeChecker) {
this.collisionsMap = new Map();
this.generatedNames = new Map();
this.typeChecker = typeChecker;
}
/**
* Adds (or "registers") a top-level {@link identifier} (which takes a top-level scope name to use).
*/
addTopLevelIdentifier(identifier) {
const symbol = (0, typescript_1.getDeclarationNameSymbol)(identifier, this.typeChecker);
if (symbol === null) {
throw new Error(`Something went wrong - cannot find a symbol for top-level identifier ${identifier.getText()} (from ${identifier.parent.parent.getText()})`);
}
const newLocalName = this.registerSymbol(symbol, identifier.getText());
if (newLocalName === null) {
throw new Error(`Something went wrong - a symbol ${symbol.name} for top-level identifier ${identifier.getText()} cannot be renamed`);
}
return newLocalName;
}
/**
* Returns a set of all already registered names for a given {@link symbol}.
*/
namesForSymbol(symbol) {
return this.generatedNames.get((0, typescript_1.getActualSymbol)(symbol, this.typeChecker)) || new Set();
}
/**
* Resolves given {@link referencedIdentifier} to a name.
* It assumes that a symbol for this identifier has been registered before by calling {@link addTopLevelIdentifier} method.
* Otherwise it will return `null`.
*
* Note that a returned value might be of a different type of the identifier (e.g. {@link ts.QualifiedName} for a given {@link ts.Identifier})
*/
resolveReferencedIdentifier(referencedIdentifier) {
const identifierSymbol = (0, typescript_1.getDeclarationNameSymbol)(referencedIdentifier, this.typeChecker);
if (identifierSymbol === null) {
// that's fine if an identifier doesn't have a symbol
// it could be in cases like for `prop` in `declare function func({ prop: prop3 }?: InterfaceName): TypeName;`
return null;
}
const symbolScopePath = this.getSymbolScope(identifierSymbol);
// this scope defines where the current identifier is located
const currentIdentifierScope = this.getNodeScope(referencedIdentifier);
if (symbolScopePath.length > 0 && currentIdentifierScope.length > 0 && symbolScopePath[0] === currentIdentifierScope[0]) {
// if a referenced symbol is declared in the same scope where it is located
// then just return its reference as is without any modification
// also note that in this method we're working with identifiers only (i.e. it cannot be a qualified name)
return referencedIdentifier.getText();
}
const topLevelIdentifierSymbol = symbolScopePath.length === 0 ? identifierSymbol : symbolScopePath[0];
const namesForTopLevelSymbol = this.namesForSymbol(topLevelIdentifierSymbol);
if (namesForTopLevelSymbol.size === 0) {
return null;
}
let topLevelName = symbolScopePath.length === 0 ? referencedIdentifier.getText() : topLevelIdentifierSymbol.getName();
if (!namesForTopLevelSymbol.has(topLevelName)) {
// if the set of already registered names does not contain the one that is requested
const topLevelNamesArray = Array.from(namesForTopLevelSymbol);
// lets find more suitable name for a top level symbol
let suitableTopLevelName = topLevelNamesArray[0];
for (const name of topLevelNamesArray) {
// attempt to find a generated name first to provide identifiers close to the original code as much as possible
if (name.startsWith(`${topLevelName}$`)) {
suitableTopLevelName = name;
break;
}
}
topLevelName = suitableTopLevelName;
}
const newIdentifierParts = [
...symbolScopePath.map((symbol) => symbol.getName()),
referencedIdentifier.getText(),
];
// we don't need to rename any symbol but top level only as only it can collide with other symbols
newIdentifierParts[0] = topLevelName;
return newIdentifierParts.join('.');
}
/**
* Similar to {@link resolveReferencedIdentifier}, but works with qualified names (Ns.Ns1.Interface).
* The main point of this resolver is that it might change the first part of the qualifier only (as it drives uniqueness of a name).
*/
resolveReferencedQualifiedName(referencedIdentifier) {
let topLevelIdentifier = referencedIdentifier;
if (ts.isQualifiedName(topLevelIdentifier) || ts.isPropertyAccessExpression(topLevelIdentifier)) {
let leftmostIdentifier = ts.isQualifiedName(topLevelIdentifier) ? topLevelIdentifier.left : topLevelIdentifier.expression;
while (ts.isQualifiedName(leftmostIdentifier) || ts.isPropertyAccessExpression(leftmostIdentifier)) {
leftmostIdentifier = ts.isQualifiedName(leftmostIdentifier) ? leftmostIdentifier.left : leftmostIdentifier.expression;
}
topLevelIdentifier = leftmostIdentifier;
}
const topLevelName = this.resolveReferencedIdentifier(topLevelIdentifier);
if (topLevelName === null) {
// that's fine if we don't have a name for this top-level symbol
// it simply means that this symbol type might not be supported for renaming
// at this point the top-level identifier isn't registered yet
// but it is possible that the full qualified name is registered so we can use its replacement instead
// it is possible in cases where you use `import * as nsName` for internal modules
// so `nsName.Interface` will be resolved to `Interface` (or any other name that `Interface` was registered with)
const identifierSymbol = (0, typescript_1.getDeclarationNameSymbol)(referencedIdentifier, this.typeChecker);
if (identifierSymbol === null) {
// that's fine if an identifier doesn't have a symbol
// it could be in cases like for `prop` in `declare function func({ prop: prop3 }?: InterfaceName): TypeName;`
return null;
}
const namesForSymbol = this.namesForSymbol(identifierSymbol);
if (namesForSymbol.size !== 0) {
// if the set of already registered names contains the one that is requested then lets use it
return Array.from(namesForSymbol)[0];
}
// if it is not registered - just skip it
return null;
}
// for nodes that we have to import we need to add an imported value to the collisions map
// so it will not overlap with other imports/inlined nodes
const identifierParts = referencedIdentifier.getText().split('.');
// update top level part as it could get renamed above
// note that `topLevelName` might be a qualified name (e.g. with `.` in the name)
// but this is fine as we join with `.` below anyway
// but it is worth it to mention here ¯\_(ツ)_/¯
identifierParts[0] = topLevelName;
return identifierParts.join('.');
}
getSymbolScope(identifierSymbol) {
const identifierDeclarations = (0, typescript_1.getDeclarationsForSymbol)(identifierSymbol);
// not all symbols have declarations, e.g. `globalThis` or `undefined` (not type but value e.g. in `typeof undefined`)
// they are "fake" symbols that exist only at the compiler level (see checker.ts file in in the compiler or `globals.set()` calls)
if (identifierDeclarations.length === 0) {
return [];
}
// we assume that all symbols for a given identifier will be in the same scope (i.e. defined in the same namespaces-chain)
// so we can use any declaration to find that scope as they all will have the same scope
return this.getNodeScope(identifierDeclarations[0]);
}
/**
* Returns a node's scope where it is located in terms of namespaces/modules.
* E.g. A scope for `Opt` in `declare module foo { type Opt = number; }` is `[Symbol(foo)]`
*/
getNodeScope(node) {
const scopeIdentifiersPath = [];
let currentNode = (0, typescript_1.getClosestModuleLikeNode)(node);
while (ts.isModuleDeclaration(currentNode) && ts.isIdentifier(currentNode.name)) {
const nameSymbol = (0, typescript_1.getDeclarationNameSymbol)(currentNode.name, this.typeChecker);
if (nameSymbol === null) {
throw new Error(`Cannot find symbol for identifier '${currentNode.name.getText()}'`);
}
scopeIdentifiersPath.push(nameSymbol);
currentNode = (0, typescript_1.getClosestModuleLikeNode)(currentNode.parent);
}
return scopeIdentifiersPath.reverse();
}
registerSymbol(identifierSymbol, preferredName) {
if (!renamingSupportedSymbols.some((flag) => identifierSymbol.flags & flag)) {
// if a symbol for something else that we don't support yet - skip
(0, logger_1.verboseLog)(`Symbol ${identifierSymbol.name} cannot be renamed because its flag (${identifierSymbol.flags}) isn't supported`);
return null;
}
if (identifierSymbol.flags & ts.SymbolFlags.NamespaceModule && identifierSymbol.escapedName === ts.InternalSymbolName.Global) {
// no need to rename `declare global` namespaces
return null;
}
let symbolName = preferredName;
if (symbolName === 'default') {
// this is special case as an identifier cannot be named `default` because of es6 syntax
// so lets fallback to some valid name
symbolName = '_default';
}
const collisionsKey = symbolName;
let collisionSymbols = this.collisionsMap.get(collisionsKey);
if (collisionSymbols === undefined) {
collisionSymbols = new Map();
this.collisionsMap.set(collisionsKey, collisionSymbols);
}
const storedSymbolName = collisionSymbols.get(identifierSymbol);
if (storedSymbolName !== undefined) {
return storedSymbolName;
}
let nameIndex = collisionSymbols.size;
let newName = collisionSymbols.size === 0 ? symbolName : `${symbolName}$${nameIndex}`;
let resolvedGlobalSymbol = (0, typescript_1.resolveGlobalName)(this.typeChecker, newName);
while (resolvedGlobalSymbol !== undefined && resolvedGlobalSymbol !== identifierSymbol) {
nameIndex += 1;
newName = `${symbolName}$${nameIndex}`;
resolvedGlobalSymbol = (0, typescript_1.resolveGlobalName)(this.typeChecker, newName);
}
collisionSymbols.set(identifierSymbol, newName);
let symbolNames = this.generatedNames.get(identifierSymbol);
if (symbolNames === undefined) {
symbolNames = new Set();
this.generatedNames.set(identifierSymbol, symbolNames);
}
symbolNames.add(newName);
return newName;
}
}
exports.CollisionsResolver = CollisionsResolver;
;