Skip to content

Commit

Permalink
Merge pull request #281 from timocov/fix134-ns
Browse files Browse the repository at this point in the history
Added wrapping `import/export * as Ns` with a namespace
  • Loading branch information
timocov authored Dec 27, 2023
2 parents 2b4d91b + 2328187 commit f0f0c21
Show file tree
Hide file tree
Showing 42 changed files with 776 additions and 99 deletions.
258 changes: 210 additions & 48 deletions src/bundle-generator.ts

Large diffs are not rendered by default.

83 changes: 72 additions & 11 deletions src/collisions-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import * as ts from 'typescript';

import {
getActualSymbol,
getClosestModuleLikeNode,
getDeclarationNameSymbol,
getDeclarationsForSymbol,
} from './helpers/typescript';
import { verboseLog } from './logger';

Expand Down Expand Up @@ -64,6 +66,8 @@ export class CollisionsResolver {
* 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})
*/
public resolveReferencedIdentifier(referencedIdentifier: ts.Identifier): string | null {
const identifierSymbol = getDeclarationNameSymbol(referencedIdentifier, this.typeChecker);
Expand All @@ -73,22 +77,55 @@ export class CollisionsResolver {
return null;
}

const namesForSymbol = this.namesForSymbol(identifierSymbol);
const identifierText = referencedIdentifier.getText();
if (namesForSymbol.has(identifierText)) {
// if the set of already registered names contains the one that is requested then lets use it
return identifierText;
// 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
const symbolScopePath = this.getNodeScope(getDeclarationsForSymbol(identifierSymbol)[0]);

// 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;
}

const namesArray = Array.from(namesForSymbol);
for (const name of namesArray) {
// attempt to find a generated name first to provide identifiers close to the original code as much as possible
if (name.startsWith(`${identifierText}$`)) {
return name;
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;
}

return namesArray[0] || null;
const newIdentifierParts = [
...symbolScopePath.map((symbol: ts.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('.');
}

/**
Expand Down Expand Up @@ -138,11 +175,35 @@ export class CollisionsResolver {
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('.');
}

/**
* 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)]`
*/
private getNodeScope(node: ts.Node): ts.Symbol[] {
const scopeIdentifiersPath: ts.Symbol[] = [];

let currentNode: ts.Node = getClosestModuleLikeNode(node);
while (ts.isModuleDeclaration(currentNode) && ts.isIdentifier(currentNode.name)) {
const nameSymbol = getDeclarationNameSymbol(currentNode.name, this.typeChecker);
if (nameSymbol === null) {
throw new Error(`Cannot find symbol for identifier '${currentNode.name.getText()}'`);
}

scopeIdentifiersPath.push(nameSymbol);
currentNode = getClosestModuleLikeNode(currentNode.parent);
}

return scopeIdentifiersPath.reverse();
}

private registerSymbol(identifierSymbol: ts.Symbol, preferredName: string): string | null {
if (!renamingSupportedSymbols.some((flag: ts.SymbolFlags) => identifierSymbol.flags & flag)) {
// if a symbol for something else that we don't support yet - skip
Expand Down
68 changes: 48 additions & 20 deletions src/generate-output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface OutputParams extends OutputHelpers {
imports: Map<string, ModuleImportsSet>;
statements: readonly ts.Statement[];
renamedExports: Map<string, string>;
wrappedNamespaces: Map<string, Map<string, string>>;
}

export interface NeedStripDefaultKeywordResult {
Expand Down Expand Up @@ -77,10 +78,25 @@ export function generateOutput(params: OutputParams, options: OutputOptions = {}
resultOutput += `\n\n${statementsTextToString(statements)}`;
}

if (params.wrappedNamespaces.size !== 0) {
resultOutput += `\n\n${
Array.from(params.wrappedNamespaces.entries())
.map(([namespaceName, exportedNames]: [string, Map<string, string>]) => {
return `declare namespace ${namespaceName} {\n\texport { ${
Array.from(exportedNames.entries())
.map(([exportedName, localName]: [string, string]) => renamedExportValue(exportedName, localName))
.sort()
.join(', ')
} };\n}`;
})
.join('\n')
}`;
}

if (params.renamedExports.size !== 0) {
resultOutput += `\n\nexport {\n\t${
Array.from(params.renamedExports.entries())
.map(([exportedName, localName]: [string, string]) => exportedName !== localName ? `${localName} as ${exportedName}` : exportedName)
.map(([exportedName, localName]: [string, string]) => renamedExportValue(exportedName, localName))
.sort()
.join(',\n\t')
},\n};`;
Expand All @@ -107,6 +123,14 @@ function statementsTextToString(statements: StatementText[]): string {
return spacesToTabs(prettifyStatementsText(statementsText));
}

function renamedExportValue(exportedName: string, localName: string): string {
return exportedName !== localName ? `${localName} as ${exportedName}` : exportedName;
}

function renamedImportValue(importedName: string, localName: string): string {
return importedName !== localName ? `${importedName} as ${localName}` : importedName;
}

function prettifyStatementsText(statementsText: string): string {
const sourceFile = ts.createSourceFile('output.d.ts', statementsText, ts.ScriptTarget.Latest, false, ts.ScriptKind.TS);
const printer = ts.createPrinter(
Expand Down Expand Up @@ -148,25 +172,18 @@ function getStatementText(statement: ts.Statement, includeSortingValue: boolean,
}

if (needResolveIdentifiers) {
if (ts.isPropertyAccessExpression(node) || ts.isQualifiedName(node)) {
const resolvedName = helpers.resolveIdentifierName(node as ts.PropertyAccessEntityNameExpression | ts.QualifiedName);
if (ts.isPropertyAccessExpression(node)) {
const resolvedName = helpers.resolveIdentifierName(node as ts.PropertyAccessEntityNameExpression);
if (resolvedName !== null && resolvedName !== node.getText()) {
const identifiers = resolvedName.split('.');

let result: ts.PropertyAccessExpression | ts.QualifiedName | ts.Identifier = ts.factory.createIdentifier(identifiers[0]);
let result: ts.PropertyAccessExpression | ts.Identifier = ts.factory.createIdentifier(identifiers[0]);

for (let index = 1; index < identifiers.length; index += 1) {
if (ts.isQualifiedName(node)) {
result = ts.factory.createQualifiedName(
result as ts.QualifiedName,
ts.factory.createIdentifier(identifiers[index])
);
} else {
result = ts.factory.createPropertyAccessExpression(
result as ts.PropertyAccessExpression,
ts.factory.createIdentifier(identifiers[index])
);
}
result = ts.factory.createPropertyAccessExpression(
result as ts.PropertyAccessExpression,
ts.factory.createIdentifier(identifiers[index])
);
}

return result;
Expand All @@ -175,15 +192,26 @@ function getStatementText(statement: ts.Statement, includeSortingValue: boolean,
return node;
}

if (ts.isIdentifier(node)) {
// QualifiedName and PropertyAccessExpression are handled above already
if (ts.isQualifiedName(node.parent) || ts.isPropertyAccessExpression(node.parent)) {
if (ts.isIdentifier(node) || ts.isQualifiedName(node)) {
// QualifiedName and PropertyAccessExpression are handled separately
if (ts.isIdentifier(node) && (ts.isQualifiedName(node.parent) || ts.isPropertyAccessExpression(node.parent))) {
return node;
}

const resolvedName = helpers.resolveIdentifierName(node);
if (resolvedName !== null && resolvedName !== node.getText()) {
return ts.factory.createIdentifier(resolvedName);
const identifiers = resolvedName.split('.');

let result: ts.QualifiedName | ts.Identifier = ts.factory.createIdentifier(identifiers[0]);

for (let index = 1; index < identifiers.length; index += 1) {
result = ts.factory.createQualifiedName(
result,
ts.factory.createIdentifier(identifiers[index])
);
}

return result;
}

return node;
Expand Down Expand Up @@ -285,7 +313,7 @@ function generateImports(libraryName: string, imports: ModuleImportsSet): string
if (imports.namedImports.size !== 0) {
result.push(`import { ${
Array.from(imports.namedImports.entries())
.map(([localName, importedName]: [string, string]) => localName !== importedName ? `${importedName} as ${localName}` : importedName)
.map(([localName, importedName]: [string, string]) => renamedImportValue(importedName, localName))
.sort()
.join(', ')
} } ${fromEnding}`);
Expand Down
57 changes: 56 additions & 1 deletion src/helpers/typescript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ export const enum ExportType {
export interface SourceFileExport {
exportedName: string;
symbol: ts.Symbol;
originalSymbol: ts.Symbol;
type: ExportType;
}

Expand All @@ -162,6 +163,7 @@ export function getExportsForSourceFile(typeChecker: ts.TypeChecker, sourceFileS
return [
{
symbol,
originalSymbol: commonJsExport,
type: ExportType.CommonJS,
exportedName: '',
},
Expand All @@ -171,7 +173,7 @@ export function getExportsForSourceFile(typeChecker: ts.TypeChecker, sourceFileS

const result = typeChecker
.getExportsOfModule(sourceFileSymbol)
.map<SourceFileExport>((symbol: ts.Symbol) => ({ symbol, exportedName: symbol.name, type: ExportType.ES6Named }));
.map<SourceFileExport>((symbol: ts.Symbol) => ({ symbol, originalSymbol: symbol, exportedName: symbol.name, type: ExportType.ES6Named }));

if (sourceFileSymbol.exports !== undefined) {
const defaultExportSymbol = sourceFileSymbol.exports.get(ts.InternalSymbolName.Default);
Expand All @@ -184,6 +186,7 @@ export function getExportsForSourceFile(typeChecker: ts.TypeChecker, sourceFileS
// but let's add it to be sure add if there is no such export
result.push({
symbol: defaultExportSymbol,
originalSymbol: defaultExportSymbol,
type: ExportType.ES6Default,
exportedName: 'default',
});
Expand Down Expand Up @@ -497,6 +500,15 @@ export function getRootSourceFile(program: ts.Program, rootFileName: string): ts
return sourceFile;
}

export function getNodeOwnSymbol(node: ts.Node, typeChecker: ts.TypeChecker): ts.Symbol {
const nodeSymbol = typeChecker.getSymbolAtLocation(node);
if (nodeSymbol === undefined) {
throw new Error(`Cannot find symbol for node "${node.getText()}" in "${node.parent.getText()}" from "${node.getSourceFile().fileName}"`);
}

return nodeSymbol;
}

export function getNodeSymbol(node: ts.Node, typeChecker: ts.TypeChecker): ts.Symbol | null {
if (ts.isSourceFile(node)) {
const fileSymbol = typeChecker.getSymbolAtLocation(node);
Expand Down Expand Up @@ -604,3 +616,46 @@ export function getImportModuleName(imp: ts.ImportEqualsDeclaration | ts.ImportD

return null;
}

/**
* Returns a symbol that an {@link exportElement} reference to.
*
* For example, for given `export { Value }` it returns a declaration of `Value` whatever it is (import statement, interface declaration, etc).
*/
export function getExportReferencedSymbol(exportElement: ts.ExportSpecifier, typeChecker: ts.TypeChecker): ts.Symbol {
return exportElement.propertyName !== undefined
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
? typeChecker.getSymbolAtLocation(exportElement.propertyName)!
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
: typeChecker.getImmediateAliasedSymbol(typeChecker.getSymbolAtLocation(exportElement.name)!)!
;
}

export function getSymbolExportStarDeclaration(symbol: ts.Symbol): ts.ExportDeclaration {
if (symbol.escapedName !== ts.InternalSymbolName.ExportStar) {
throw new Error(`Only ExportStar symbol can have export star declaration, but got ${symbol.escapedName}`);
}

// this means that an export contains `export * from 'module'` statement
const exportStarDeclaration = getDeclarationsForSymbol(symbol).find(ts.isExportDeclaration);
if (exportStarDeclaration === undefined || exportStarDeclaration.moduleSpecifier === undefined) {
throw new Error(`Cannot find export declaration for ${symbol.getName()} symbol`);
}

return exportStarDeclaration;
}

export function getDeclarationsForExportedValues(exp: ts.ExportAssignment | ts.ExportDeclaration, typeChecker: ts.TypeChecker): ts.Declaration[] {
const nodeForSymbol = ts.isExportAssignment(exp) ? exp.expression : exp.moduleSpecifier;
if (nodeForSymbol === undefined) {
return [];
}

const symbolForExpression = typeChecker.getSymbolAtLocation(nodeForSymbol);
if (symbolForExpression === undefined) {
return [];
}

const symbol = getActualSymbol(symbolForExpression, typeChecker);
return getDeclarationsForSymbol(symbol);
}
Loading

0 comments on commit f0f0c21

Please sign in to comment.