diff --git a/packages/jsii-rosetta/jest.config.mjs b/packages/jsii-rosetta/jest.config.mjs index 9637f6c9ad..2aebf0431f 100644 --- a/packages/jsii-rosetta/jest.config.mjs +++ b/packages/jsii-rosetta/jest.config.mjs @@ -4,4 +4,5 @@ import { default as defaultConfig, overriddenConfig } from '../../jest.config.mj export default overriddenConfig({ setupFiles: [createRequire(import.meta.url).resolve('./jestsetup.js')], testTimeout: process.env.CI === 'true' ? 30_000 : defaultConfig.testTimeout, + watchPathIgnorePatterns: ['(\\.d)?\\.tsx?$'], }); diff --git a/packages/jsii-rosetta/lib/languages/csharp.ts b/packages/jsii-rosetta/lib/languages/csharp.ts index acca98b23b..010903164e 100644 --- a/packages/jsii-rosetta/lib/languages/csharp.ts +++ b/packages/jsii-rosetta/lib/languages/csharp.ts @@ -140,7 +140,7 @@ export class CSharpVisitor extends DefaultVisitor { const namespace = fmap(importStatement.moduleSymbol, findDotnetName) ?? guessedNamespace; if (importStatement.imports.import === 'full') { - this.dropPropertyAccesses.add(importStatement.imports.alias); + this.dropPropertyAccesses.add(importStatement.imports.sourceName); this.alreadyImportedNamespaces.add(namespace); return new OTree([`using ${namespace};`], [], { canBreakLine: true }); } @@ -563,34 +563,34 @@ export class CSharpVisitor extends DefaultVisitor { } public variableDeclaration(node: ts.VariableDeclaration, renderer: CSharpRenderer): OTree { - let fallback = 'var'; - if (node.type) { - fallback = node.type.getText(); - } + let typeOrVar = 'var'; + const fallback = node.type?.getText() ?? 'var'; const type = - (node.type && renderer.typeOfType(node.type)) || + (node.type && renderer.typeOfType(node.type)) ?? (node.initializer && renderer.typeOfExpression(node.initializer)); - let renderedType = this.renderType(node, type, false, fallback, renderer); - if (renderedType === 'object') { - renderedType = 'var'; + const varType = this.renderType(node, type, false, fallback, renderer); + // If there is an initializer, and the value isn't "IDictionary<...", we always use var, as this is the + // recommendation from Roslyn. + if (varType !== 'object' && (varType.startsWith('IDictionary<') || node.initializer == null)) { + typeOrVar = varType; } if (!node.initializer) { - return new OTree([renderedType, ' ', renderer.convert(node.name), ';'], []); + return new OTree([typeOrVar, ' ', renderer.convert(node.name), ';']); } return new OTree( [ - renderedType, + typeOrVar, ' ', renderer.convert(node.name), ' = ', renderer.updateContext({ preferObjectLiteralAsStruct: false }).convert(node.initializer), ';', ], - [], + undefined, { canBreakLine: true }, ); } @@ -715,7 +715,7 @@ function findDotnetName(jsiiSymbol: JsiiSymbol): string | undefined { } } - return `${recurse(namespaceName(fqn))}.${simpleName(jsiiSymbol.fqn)}`; + return `${recurse(namespaceName(fqn))}.${ucFirst(simpleName(jsiiSymbol.fqn))}`; } } diff --git a/packages/jsii-rosetta/lib/languages/default.ts b/packages/jsii-rosetta/lib/languages/default.ts index 022475f563..15c989006f 100644 --- a/packages/jsii-rosetta/lib/languages/default.ts +++ b/packages/jsii-rosetta/lib/languages/default.ts @@ -4,8 +4,10 @@ import { analyzeObjectLiteral, ObjectLiteralStruct } from '../jsii/jsii-types'; import { isNamedLikeStruct, isJsiiProtocolType } from '../jsii/jsii-utils'; import { OTree, NO_SYNTAX } from '../o-tree'; import { AstRenderer, AstHandler, nimpl, CommentSyntax } from '../renderer'; +import { SubmoduleReference } from '../submodule-reference'; import { voidExpressionString } from '../typescript/ast-utils'; import { ImportStatement } from '../typescript/imports'; +import { inferredTypeOfExpression } from '../typescript/types'; import { TargetLanguage } from '.'; @@ -98,7 +100,11 @@ export abstract class DefaultVisitor implements AstHandler { return this.notImplemented(node, context); } - public propertyAccessExpression(node: ts.PropertyAccessExpression, context: AstRenderer): OTree { + public propertyAccessExpression( + node: ts.PropertyAccessExpression, + context: AstRenderer, + _submoduleReference: SubmoduleReference | undefined, + ): OTree { return new OTree([context.convert(node.expression), '.', context.convert(node.name)]); } @@ -169,7 +175,7 @@ export abstract class DefaultVisitor implements AstHandler { : false, ); - const inferredType = context.inferredTypeOfExpression(node); + const inferredType = inferredTypeOfExpression(context.typeChecker, node); if ((inferredType && isJsiiProtocolType(context.typeChecker, inferredType)) || anyMembersFunctions) { context.report( node, diff --git a/packages/jsii-rosetta/lib/languages/go.ts b/packages/jsii-rosetta/lib/languages/go.ts index 9fcb50632d..a012bd85a6 100644 --- a/packages/jsii-rosetta/lib/languages/go.ts +++ b/packages/jsii-rosetta/lib/languages/go.ts @@ -4,8 +4,10 @@ import { AssertionError } from 'assert'; import * as ts from 'typescript'; import { analyzeObjectLiteral, determineJsiiType, JsiiType, ObjectLiteralStruct } from '../jsii/jsii-types'; +import { lookupJsiiSymbolFromNode } from '../jsii/jsii-utils'; import { OTree } from '../o-tree'; import { AstRenderer } from '../renderer'; +import { SubmoduleReference } from '../submodule-reference'; import { isExported, isPublic, isPrivate, isReadOnly, isStatic } from '../typescript/ast-utils'; import { analyzeImportDeclaration, ImportStatement } from '../typescript/imports'; import { @@ -228,6 +230,15 @@ export class GoVisitor extends DefaultVisitor { className: ucFirst(expr.name.text), classNamespace: renderer.updateContext({ isExported: false }).convert(expr.expression), }; + } else if ( + ts.isPropertyAccessExpression(expr.expression) && + renderer.submoduleReferences.has(expr.expression) + ) { + const submodule = renderer.submoduleReferences.get(expr.expression)!; + return { + className: ucFirst(expr.name.text), + classNamespace: renderer.updateContext({ isExported: false }).convert(submodule.lastNode), + }; } renderer.reportUnsupported(expr.expression, TargetLanguage.GO); return { @@ -277,15 +288,17 @@ export class GoVisitor extends DefaultVisitor { ? JSON.stringify(node.name.text) : this.goName(node.name.text, renderer, renderer.typeChecker.getSymbolAtLocation(node.name)) : renderer.convert(node.name); - // Struct member values are always pointers... return new OTree( [ key, ': ', renderer .updateContext({ - wrapPtr: renderer.currentContext.isStruct || renderer.currentContext.inMapLiteral, + // Reset isExported, as this was intended for the key name translation... + isExported: undefined, + // Struct member values are always pointers... isPtr: renderer.currentContext.isStruct, + wrapPtr: renderer.currentContext.isStruct || renderer.currentContext.inMapLiteral, }) .convert(node.initializer), ], @@ -389,13 +402,18 @@ export class GoVisitor extends DefaultVisitor { structType: ObjectLiteralStruct, renderer: GoRenderer, ): OTree { + const isExported = structType.kind === 'struct'; return new OTree( [ '&', - this.goName(structType.type.symbol.name, renderer.updateContext({ isPtr: false }), structType.type.symbol), + this.goName( + structType.type.symbol.name, + renderer.updateContext({ isExported, isPtr: false }), + structType.type.symbol, + ), '{', ], - renderer.updateContext({ isStruct: true }).convertAll(node.properties), + renderer.updateContext({ isExported, isStruct: true }).convertAll(node.properties), { suffix: '}', separator: ',', @@ -444,16 +462,30 @@ export class GoVisitor extends DefaultVisitor { return new OTree(['fmt.Println(', renderedArgs, ')']); } - public propertyAccessExpression(node: ts.PropertyAccessExpression, renderer: GoRenderer): OTree { + public propertyAccessExpression( + node: ts.PropertyAccessExpression, + renderer: GoRenderer, + submoduleReference?: SubmoduleReference, + ): OTree { + if (submoduleReference != null) { + return new OTree([ + renderer + .updateContext({ isExported: false, isPtr: false, wrapPtr: false }) + .convert(submoduleReference.lastNode), + ]); + } + const expressionType = typeOfExpression(renderer.typeChecker, node.expression); const valueSymbol = renderer.typeChecker.getSymbolAtLocation(node.name); - const isClassStaticMember = + const isStaticMember = valueSymbol?.valueDeclaration != null && isStatic(valueSymbol.valueDeclaration); + const isClassStaticPropertyAccess = + isStaticMember && expressionType?.symbol?.valueDeclaration != null && ts.isClassDeclaration(expressionType.symbol.valueDeclaration) && - valueSymbol?.valueDeclaration != null && - ts.isPropertyDeclaration(valueSymbol.valueDeclaration) && - isStatic(valueSymbol.valueDeclaration); + (ts.isPropertyDeclaration(valueSymbol.valueDeclaration) || ts.isAccessor(valueSymbol.valueDeclaration)); + const isClassStaticMethodAccess = + isStaticMember && !isClassStaticPropertyAccess && ts.isMethodDeclaration(valueSymbol.valueDeclaration); // When the expression has an unknown type (unresolved symbol), and has an upper-case first // letter, we assume it's a type name... In such cases, what comes after can be considered a @@ -463,18 +495,32 @@ export class GoVisitor extends DefaultVisitor { expressionType.symbol == null && /(?:\.|^)[A-Z][^.]*$/.exec(node.expression.getText(node.expression.getSourceFile())) != null; - const isEnum = + // Whether the node is an enum member reference. + const isEnumMember = expressionType?.symbol?.valueDeclaration != null && ts.isEnumDeclaration(expressionType.symbol.valueDeclaration); - const delimiter = isEnum || isClassStaticMember || expressionLooksLikeTypeReference ? '_' : '.'; + const jsiiSymbol = lookupJsiiSymbolFromNode(renderer.typeChecker, node.name); + const isExportedTypeName = jsiiSymbol != null && jsiiSymbol.symbolType !== 'module'; + + const delimiter = + isEnumMember || isClassStaticPropertyAccess || isClassStaticMethodAccess || expressionLooksLikeTypeReference + ? '_' + : '.'; return new OTree([ renderer.convert(node.expression), delimiter, renderer - .updateContext({ isExported: isClassStaticMember || expressionLooksLikeTypeReference || isEnum }) + .updateContext({ + isExported: + isClassStaticPropertyAccess || + isClassStaticMethodAccess || + expressionLooksLikeTypeReference || + isEnumMember || + isExportedTypeName, + }) .convert(node.name), - ...(isClassStaticMember + ...(isClassStaticPropertyAccess ? ['()'] : // If the parent's not a call-like expression, and it's an inferred static property access, we need to put call // parentheses at the end, as static properties are accessed via synthetic readers. @@ -578,8 +624,13 @@ export class GoVisitor extends DefaultVisitor { return wrapPtrExpression(renderer.typeChecker, node, output); } - public stringLiteral(node: ts.StringLiteral, renderer: GoRenderer): OTree { - const text = JSON.stringify(node.text); + public stringLiteral(node: ts.StringLiteral | ts.NoSubstitutionTemplateLiteral, renderer: GoRenderer): OTree { + // Go supports backtick-delimited multi-line string literals, similar/same as JavaScript no-substitution templates. + // We only use this trick if the literal includes actual new line characters (otherwise it just looks weird in go). + const text = + ts.isNoSubstitutionTemplateLiteral(node) && /[\n\r]/m.test(node.text) + ? node.getText(node.getSourceFile()) + : JSON.stringify(node.text); return new OTree([`${renderer.currentContext.wrapPtr ? jsiiStr(text) : text}`]); } @@ -801,25 +852,30 @@ export class GoVisitor extends DefaultVisitor { public importStatement(node: ImportStatement, renderer: AstRenderer): OTree { const packageName = node.moduleSymbol?.sourceAssembly?.packageJson.jsii?.targets?.go?.packageName ?? - this.goName(node.packageName, renderer, undefined); + node.packageName + // Special case namespaced npm package names, so they are mangled the same way pacmak does. + .replace(/@([a-z0-9_-]+)\/([a-z0-9_-])/, '$1$2') + .split('/') + .map((txt) => this.goName(txt, renderer, undefined)) + .filter((txt) => txt !== '') + .join('/'); const moduleName = node.moduleSymbol?.sourceAssembly?.packageJson.jsii?.targets?.go?.moduleName ? `${node.moduleSymbol.sourceAssembly.packageJson.jsii.targets.go.moduleName}/${packageName}` : `github.com/aws-samples/dummy/${packageName}`; if (node.imports.import === 'full') { - return new OTree( - ['import ', this.goName(node.imports.alias, renderer, undefined), ' "', moduleName, '"'], - undefined, - { canBreakLine: true }, - ); + // We don't emit the alias if it matches the last path segment (conventionally this is the package name) + const maybeAlias = node.imports.alias ? `${this.goName(node.imports.alias, renderer, undefined)} ` : ''; + + return new OTree([`import ${maybeAlias}${JSON.stringify(moduleName)}`], undefined, { canBreakLine: true }); } if (node.imports.elements.length === 0) { // This is a blank import (for side-effects only) - return new OTree(['import _ "', moduleName, '"'], undefined, { canBreakLine: true }); + return new OTree([`import _ ${JSON.stringify(moduleName)}`], undefined, { canBreakLine: true }); } - return new OTree(['import "', moduleName, '"'], undefined, { canBreakLine: true }); + return new OTree([`import ${JSON.stringify(moduleName)}`], undefined, { canBreakLine: true }); } public variableDeclaration(node: ts.VariableDeclaration, renderer: AstRenderer): OTree { diff --git a/packages/jsii-rosetta/lib/languages/java.ts b/packages/jsii-rosetta/lib/languages/java.ts index 778735731c..bfd14bb0fb 100644 --- a/packages/jsii-rosetta/lib/languages/java.ts +++ b/packages/jsii-rosetta/lib/languages/java.ts @@ -6,6 +6,7 @@ import { jsiiTargetParameter } from '../jsii/packages'; import { TargetLanguage } from '../languages/target-language'; import { OTree, NO_SYNTAX } from '../o-tree'; import { AstRenderer } from '../renderer'; +import { SubmoduleReference } from '../submodule-reference'; import { isReadOnly, matchAst, nodeOfType, quoteStringLiteral, visibility } from '../typescript/ast-utils'; import { ImportStatement } from '../typescript/imports'; import { isEnumAccess, isStaticReadonlyAccess, determineReturnType } from '../typescript/types'; @@ -121,9 +122,8 @@ export class JavaVisitor extends DefaultVisitor { public importStatement(importStatement: ImportStatement): OTree { const guessedNamespace = guessJavaNamespaceName(importStatement.packageName); - if (importStatement.imports.import === 'full') { - this.dropPropertyAccesses.add(importStatement.imports.alias); + this.dropPropertyAccesses.add(importStatement.imports.sourceName); const namespace = fmap(importStatement.moduleSymbol, findJavaName) ?? guessedNamespace; return new OTree([`import ${namespace}.*;`], [], { canBreakLine: true }); @@ -132,7 +132,14 @@ export class JavaVisitor extends DefaultVisitor { const imports = importStatement.imports.elements.map((e) => { const fqn = fmap(e.importedSymbol, findJavaName) ?? `${guessedNamespace}.${e.sourceName}`; - return e.importedSymbol?.symbolType === 'module' ? `import ${fqn}.*;` : `import ${fqn};`; + // If there is no imported symbol, we check if there is anything looking like a type name in + // the source name (that is, any segment that starts with an upper case letter), and if none + // is found, assume this refers to a namespace/module. + return (e.importedSymbol?.symbolType == null && + !e.sourceName.split('.').some((segment) => /^[A-Z]/.test(segment))) || + e.importedSymbol?.symbolType === 'module' + ? `import ${fqn}.*;` + : `import ${fqn};`; }); const localNames = importStatement.imports.elements @@ -548,8 +555,17 @@ export class JavaVisitor extends DefaultVisitor { : this.singlePropertyInJavaScriptObjectLiteralToFluentSetters(node.name, node.name, renderer); } - public propertyAccessExpression(node: ts.PropertyAccessExpression, renderer: JavaRenderer): OTree { + public propertyAccessExpression( + node: ts.PropertyAccessExpression, + renderer: JavaRenderer, + submoduleRef: SubmoduleReference | undefined, + ): OTree { const rightHandSide = renderer.convert(node.name); + // If a submodule access, then just render the name, we emitted a * import of the expression segment already. + if (submoduleRef != null) { + return rightHandSide; + } + let parts: Array; const leftHandSide = renderer.textOf(node.expression); @@ -860,7 +876,10 @@ function findJavaName(jsiiSymbol: JsiiSymbol): string | undefined { } } - return `${recurse(namespaceName(fqn))}.${simpleName(jsiiSymbol.fqn)}`; + const ns = namespaceName(fqn); + const nsJavaName = recurse(ns); + const leaf = simpleName(fqn); + return `${nsJavaName}.${leaf}`; } } diff --git a/packages/jsii-rosetta/lib/languages/python.ts b/packages/jsii-rosetta/lib/languages/python.ts index c4e1c02579..a9b16a2269 100644 --- a/packages/jsii-rosetta/lib/languages/python.ts +++ b/packages/jsii-rosetta/lib/languages/python.ts @@ -15,6 +15,7 @@ import { jsiiTargetParameter } from '../jsii/packages'; import { TargetLanguage } from '../languages/target-language'; import { NO_SYNTAX, OTree, renderTree } from '../o-tree'; import { AstRenderer, nimpl, CommentSyntax } from '../renderer'; +import { SubmoduleReference } from '../submodule-reference'; import { matchAst, nodeOfType, @@ -168,12 +169,13 @@ export class PythonVisitor extends DefaultVisitor { if (node.imports.import === 'full') { const moduleName = fmap(node.moduleSymbol, findPythonName) ?? guessPythonPackageName(node.packageName); + const importName = node.imports.alias ?? node.imports.sourceName; this.addImport({ importedFqn: node.moduleSymbol?.fqn ?? node.packageName, - importName: node.imports.alias, + importName, }); - return new OTree([`import ${moduleName} as ${mangleIdentifier(node.imports.alias)}`], [], { + return new OTree([`import ${moduleName} as ${mangleIdentifier(importName)}`], [], { canBreakLine: true, }); } @@ -325,7 +327,11 @@ export class PythonVisitor extends DefaultVisitor { ); } - public propertyAccessExpression(node: ts.PropertyAccessExpression, context: PythonVisitorContext) { + public propertyAccessExpression( + node: ts.PropertyAccessExpression, + context: PythonVisitorContext, + submoduleReference: SubmoduleReference | undefined, + ) { const fullText = context.textOf(node); if (fullText in BUILTIN_FUNCTIONS) { return new OTree([BUILTIN_FUNCTIONS[fullText]]); @@ -339,7 +345,11 @@ export class PythonVisitor extends DefaultVisitor { return context.convert(node.name); } - return super.propertyAccessExpression(node, context); + if (submoduleReference != null) { + return context.convert(submoduleReference.lastNode); + } + + return super.propertyAccessExpression(node, context, submoduleReference); } public parameterDeclaration(node: ts.ParameterDeclaration, context: PythonVisitorContext): OTree { diff --git a/packages/jsii-rosetta/lib/languages/record-references.ts b/packages/jsii-rosetta/lib/languages/record-references.ts index d53750845c..801c470393 100644 --- a/packages/jsii-rosetta/lib/languages/record-references.ts +++ b/packages/jsii-rosetta/lib/languages/record-references.ts @@ -4,6 +4,7 @@ import { lookupJsiiSymbol } from '../jsii/jsii-utils'; import { TargetLanguage } from '../languages/target-language'; import { OTree, NO_SYNTAX } from '../o-tree'; import { AstRenderer } from '../renderer'; +import { SubmoduleReference } from '../submodule-reference'; import { Spans } from '../typescript/visible-spans'; import { DefaultVisitor } from './default'; @@ -18,7 +19,7 @@ type RecordReferencesRenderer = AstRenderer; export class RecordReferencesVisitor extends DefaultVisitor { public static readonly VERSION = '2'; - public readonly language = TargetLanguage.PYTHON; // Doesn't matter, but we need it to use the visitor infra :( + public readonly language = TargetLanguage.VISUALIZE; public readonly defaultContext = {}; private readonly references = new Set(); @@ -63,7 +64,11 @@ export class RecordReferencesVisitor extends DefaultVisitor { + public readonly language = TargetLanguage.VISUALIZE; public readonly defaultContext: void = undefined; public constructor(private readonly includeHandlerNames?: boolean) {} diff --git a/packages/jsii-rosetta/lib/renderer.ts b/packages/jsii-rosetta/lib/renderer.ts index 42e6075dc6..137cd4fefb 100644 --- a/packages/jsii-rosetta/lib/renderer.ts +++ b/packages/jsii-rosetta/lib/renderer.ts @@ -2,6 +2,7 @@ import * as ts from 'typescript'; import { TargetLanguage } from './languages'; import { NO_SYNTAX, OTree, UnknownSyntax, Span } from './o-tree'; +import { SubmoduleReference, SubmoduleReferenceMap } from './submodule-reference'; import { commentRangeFromTextRange, extractMaskingVoidExpression, @@ -30,6 +31,7 @@ export class AstRenderer { public readonly typeChecker: ts.TypeChecker, private readonly handler: AstHandler, private readonly options: AstRendererOptions = {}, + public readonly submoduleReferences: SubmoduleReferenceMap = new Map(), ) { this.currentContext = handler.defaultContext; } @@ -236,155 +238,122 @@ export class AstRenderer { * Dispatch node to handler */ private dispatch(tree: ts.Node): OTree { - // Special nodes - if (ts.isEmptyStatement(tree)) { - // Additional semicolon where it doesn't belong. - return NO_SYNTAX; - } - const visitor = this.handler; - // Nodes with meaning - if (ts.isSourceFile(tree)) { - return visitor.sourceFile(tree, this); - } - if (ts.isImportEqualsDeclaration(tree)) { - return visitor.importStatement(analyzeImportEquals(tree, this), this); - } - if (ts.isImportDeclaration(tree)) { - return visitor.importStatement(analyzeImportDeclaration(tree, this), this); - } - if (ts.isStringLiteral(tree) || ts.isNoSubstitutionTemplateLiteral(tree)) { - return visitor.stringLiteral(tree, this); - } - if (ts.isNumericLiteral(tree)) { - return visitor.numericLiteral(tree, this); - } - if (ts.isFunctionDeclaration(tree)) { - return visitor.functionDeclaration(tree, this); - } - if (ts.isIdentifier(tree)) { - return visitor.identifier(tree, this); - } - if (ts.isBlock(tree)) { - return visitor.block(tree, this); - } - if (ts.isParameter(tree)) { - return visitor.parameterDeclaration(tree, this); - } - if (ts.isReturnStatement(tree)) { - return visitor.returnStatement(tree, this); - } - if (ts.isBinaryExpression(tree)) { - return visitor.binaryExpression(tree, this); - } - if (ts.isIfStatement(tree)) { - return visitor.ifStatement(tree, this); - } - if (ts.isPropertyAccessExpression(tree)) { - return visitor.propertyAccessExpression(tree, this); - } - if (ts.isAwaitExpression(tree)) { - return visitor.awaitExpression(tree, this); - } - if (ts.isCallExpression(tree)) { - return visitor.callExpression(tree, this); - } - if (ts.isExpressionStatement(tree)) { - return visitor.expressionStatement(tree, this); - } - if (ts.isToken(tree)) { - return visitor.token(tree, this); - } - if (ts.isObjectLiteralExpression(tree)) { - return visitor.objectLiteralExpression(tree, this); - } - if (ts.isNewExpression(tree)) { - return visitor.newExpression(tree, this); - } - if (ts.isPropertyAssignment(tree)) { - return visitor.propertyAssignment(tree, this); - } - if (ts.isVariableStatement(tree)) { - return visitor.variableStatement(tree, this); - } - if (ts.isVariableDeclarationList(tree)) { - return visitor.variableDeclarationList(tree, this); - } - if (ts.isVariableDeclaration(tree)) { - return visitor.variableDeclaration(tree, this); + // Using a switch on tree.kind + forced down-casting, because this is significantly faster than + // doing a cascade of `if` statements with the `ts.is` functions, since `tree.kind` is + // effectively integers, and this switch statement is hence optimizable to a jump table. This is + // a VERY significant enhancement to the debugging experience, too. + switch (tree.kind) { + case ts.SyntaxKind.EmptyStatement: + // Additional semicolon where it doesn't belong. + return NO_SYNTAX; + case ts.SyntaxKind.SourceFile: + return visitor.sourceFile(tree as ts.SourceFile, this); + case ts.SyntaxKind.ImportEqualsDeclaration: + return visitor.importStatement(analyzeImportEquals(tree as ts.ImportEqualsDeclaration, this), this); + case ts.SyntaxKind.ImportDeclaration: + return new OTree( + [], + analyzeImportDeclaration(tree as ts.ImportDeclaration, this, this.submoduleReferences).map((import_) => + visitor.importStatement(import_, this), + ), + { canBreakLine: true, separator: '\n' }, + ); + case ts.SyntaxKind.StringLiteral: + case ts.SyntaxKind.NoSubstitutionTemplateLiteral: + return visitor.stringLiteral(tree as ts.StringLiteral | ts.NoSubstitutionTemplateLiteral, this); + case ts.SyntaxKind.NumericLiteral: + return visitor.numericLiteral(tree as ts.NumericLiteral, this); + case ts.SyntaxKind.FunctionDeclaration: + return visitor.functionDeclaration(tree as ts.FunctionDeclaration, this); + case ts.SyntaxKind.Identifier: + return visitor.identifier(tree as ts.Identifier, this); + case ts.SyntaxKind.Block: + return visitor.block(tree as ts.Block, this); + case ts.SyntaxKind.Parameter: + return visitor.parameterDeclaration(tree as ts.ParameterDeclaration, this); + case ts.SyntaxKind.ReturnStatement: + return visitor.returnStatement(tree as ts.ReturnStatement, this); + case ts.SyntaxKind.BinaryExpression: + return visitor.binaryExpression(tree as ts.BinaryExpression, this); + case ts.SyntaxKind.IfStatement: + return visitor.ifStatement(tree as ts.IfStatement, this); + case ts.SyntaxKind.PropertyAccessExpression: + const submoduleReference = this.submoduleReferences?.get(tree as ts.PropertyAccessExpression); + return visitor.propertyAccessExpression(tree as ts.PropertyAccessExpression, this, submoduleReference); + case ts.SyntaxKind.AwaitExpression: + return visitor.awaitExpression(tree as ts.AwaitExpression, this); + case ts.SyntaxKind.CallExpression: + return visitor.callExpression(tree as ts.CallExpression, this); + case ts.SyntaxKind.ExpressionStatement: + return visitor.expressionStatement(tree as ts.ExpressionStatement, this); + case ts.SyntaxKind.ObjectLiteralExpression: + return visitor.objectLiteralExpression(tree as ts.ObjectLiteralExpression, this); + case ts.SyntaxKind.NewExpression: + return visitor.newExpression(tree as ts.NewExpression, this); + case ts.SyntaxKind.PropertyAssignment: + return visitor.propertyAssignment(tree as ts.PropertyAssignment, this); + case ts.SyntaxKind.VariableStatement: + return visitor.variableStatement(tree as ts.VariableStatement, this); + case ts.SyntaxKind.VariableDeclarationList: + return visitor.variableDeclarationList(tree as ts.VariableDeclarationList, this); + case ts.SyntaxKind.VariableDeclaration: + return visitor.variableDeclaration(tree as ts.VariableDeclaration, this); + case ts.SyntaxKind.ArrayLiteralExpression: + return visitor.arrayLiteralExpression(tree as ts.ArrayLiteralExpression, this); + case ts.SyntaxKind.ShorthandPropertyAssignment: + return visitor.shorthandPropertyAssignment(tree as ts.ShorthandPropertyAssignment, this); + case ts.SyntaxKind.ForOfStatement: + return visitor.forOfStatement(tree as ts.ForOfStatement, this); + case ts.SyntaxKind.ClassDeclaration: + return visitor.classDeclaration(tree as ts.ClassDeclaration, this); + case ts.SyntaxKind.Constructor: + return visitor.constructorDeclaration(tree as ts.ConstructorDeclaration, this); + case ts.SyntaxKind.PropertyDeclaration: + return visitor.propertyDeclaration(tree as ts.PropertyDeclaration, this); + case ts.SyntaxKind.ComputedPropertyName: + return visitor.computedPropertyName((tree as ts.ComputedPropertyName).expression, this); + case ts.SyntaxKind.MethodDeclaration: + return visitor.methodDeclaration(tree as ts.MethodDeclaration, this); + case ts.SyntaxKind.InterfaceDeclaration: + return visitor.interfaceDeclaration(tree as ts.InterfaceDeclaration, this); + case ts.SyntaxKind.PropertySignature: + return visitor.propertySignature(tree as ts.PropertySignature, this); + case ts.SyntaxKind.MethodSignature: + return visitor.methodSignature(tree as ts.MethodSignature, this); + case ts.SyntaxKind.AsExpression: + return visitor.asExpression(tree as ts.AsExpression, this); + case ts.SyntaxKind.PrefixUnaryExpression: + return visitor.prefixUnaryExpression(tree as ts.PrefixUnaryExpression, this); + case ts.SyntaxKind.SpreadAssignment: + if (this.textOf(tree) === '...') { + return visitor.ellipsis(tree as ts.SpreadAssignment, this); + } + return visitor.spreadAssignment(tree as ts.SpreadAssignment, this); + case ts.SyntaxKind.SpreadElement: + if (this.textOf(tree) === '...') { + return visitor.ellipsis(tree as ts.SpreadElement, this); + } + return visitor.spreadElement(tree as ts.SpreadElement, this); + case ts.SyntaxKind.ElementAccessExpression: + return visitor.elementAccessExpression(tree as ts.ElementAccessExpression, this); + case ts.SyntaxKind.TemplateExpression: + return visitor.templateExpression(tree as ts.TemplateExpression, this); + case ts.SyntaxKind.NonNullExpression: + return visitor.nonNullExpression(tree as ts.NonNullExpression, this); + case ts.SyntaxKind.ParenthesizedExpression: + return visitor.parenthesizedExpression(tree as ts.ParenthesizedExpression, this); + case ts.SyntaxKind.VoidExpression: + return visitor.maskingVoidExpression(tree as ts.VoidExpression, this); + case ts.SyntaxKind.JSDocComment: + return visitor.jsDoc(tree as ts.JSDoc, this); + default: + if (ts.isToken(tree)) { + return visitor.token(tree, this); + } + this.reportUnsupported(tree, undefined); } - if (ts.isJSDoc(tree)) { - return visitor.jsDoc(tree, this); - } - if (ts.isArrayLiteralExpression(tree)) { - return visitor.arrayLiteralExpression(tree, this); - } - if (ts.isShorthandPropertyAssignment(tree)) { - return visitor.shorthandPropertyAssignment(tree, this); - } - if (ts.isForOfStatement(tree)) { - return visitor.forOfStatement(tree, this); - } - if (ts.isClassDeclaration(tree)) { - return visitor.classDeclaration(tree, this); - } - if (ts.isConstructorDeclaration(tree)) { - return visitor.constructorDeclaration(tree, this); - } - if (ts.isPropertyDeclaration(tree)) { - return visitor.propertyDeclaration(tree, this); - } - if (ts.isComputedPropertyName(tree)) { - return visitor.computedPropertyName(tree.expression, this); - } - if (ts.isMethodDeclaration(tree)) { - return visitor.methodDeclaration(tree, this); - } - if (ts.isInterfaceDeclaration(tree)) { - return visitor.interfaceDeclaration(tree, this); - } - if (ts.isPropertySignature(tree)) { - return visitor.propertySignature(tree, this); - } - if (ts.isMethodSignature(tree)) { - return visitor.methodSignature(tree, this); - } - if (ts.isAsExpression(tree)) { - return visitor.asExpression(tree, this); - } - if (ts.isPrefixUnaryExpression(tree)) { - return visitor.prefixUnaryExpression(tree, this); - } - if (ts.isSpreadAssignment(tree)) { - if (this.textOf(tree) === '...') { - return visitor.ellipsis(tree, this); - } - return visitor.spreadAssignment(tree, this); - } - if (ts.isSpreadElement(tree)) { - if (this.textOf(tree) === '...') { - return visitor.ellipsis(tree, this); - } - return visitor.spreadElement(tree, this); - } - if (ts.isElementAccessExpression(tree)) { - return visitor.elementAccessExpression(tree, this); - } - if (ts.isTemplateExpression(tree)) { - return visitor.templateExpression(tree, this); - } - if (ts.isNonNullExpression(tree)) { - return visitor.nonNullExpression(tree, this); - } - if (ts.isParenthesizedExpression(tree)) { - return visitor.parenthesizedExpression(tree, this); - } - if (ts.isVoidExpression(tree)) { - return visitor.maskingVoidExpression(tree, this); - } - - this.reportUnsupported(tree, undefined); if (this.options.bestEffort !== false) { // When doing best-effort conversion and we don't understand the node type, just return the complete text of it as-is @@ -457,6 +426,8 @@ export class AstRenderer { * of AST node. */ export interface AstHandler { + readonly language: TargetLanguage; + readonly defaultContext: C; readonly indentChar?: ' ' | '\t'; mergeContext(old: C, update: Partial): C; @@ -473,7 +444,11 @@ export interface AstHandler { returnStatement(node: ts.ReturnStatement, context: AstRenderer): OTree; binaryExpression(node: ts.BinaryExpression, context: AstRenderer): OTree; ifStatement(node: ts.IfStatement, context: AstRenderer): OTree; - propertyAccessExpression(node: ts.PropertyAccessExpression, context: AstRenderer): OTree; + propertyAccessExpression( + node: ts.PropertyAccessExpression, + context: AstRenderer, + submoduleReference: SubmoduleReference | undefined, + ): OTree; awaitExpression(node: ts.AwaitExpression, context: AstRenderer): OTree; callExpression(node: ts.CallExpression, context: AstRenderer): OTree; expressionStatement(node: ts.ExpressionStatement, context: AstRenderer): OTree; diff --git a/packages/jsii-rosetta/lib/submodule-reference.ts b/packages/jsii-rosetta/lib/submodule-reference.ts new file mode 100644 index 0000000000..9ce5203a4d --- /dev/null +++ b/packages/jsii-rosetta/lib/submodule-reference.ts @@ -0,0 +1,187 @@ +import * as ts from 'typescript'; + +export type SubmoduleReferenceMap = ReadonlyMap< + ts.PropertyAccessExpression | ts.LeftHandSideExpression | ts.Identifier | ts.PrivateIdentifier, + SubmoduleReference +>; + +export class SubmoduleReference { + public static inSourceFile(sourceFile: ts.SourceFile, typeChecker: ts.TypeChecker): SubmoduleReferenceMap { + const importDeclarations = sourceFile.statements + .filter((stmt) => ts.isImportDeclaration(stmt)) + .flatMap((stmt) => importedSymbolsFrom(stmt as ts.ImportDeclaration, sourceFile, typeChecker)); + + return SubmoduleReference.inNode(sourceFile, typeChecker, new Set(importDeclarations)); + } + + private static inNode( + node: ts.Node, + typeChecker: ts.TypeChecker, + importDeclarations: ReadonlySet, + map = new Map< + ts.PropertyAccessExpression | ts.LeftHandSideExpression | ts.Identifier | ts.PrivateIdentifier, + SubmoduleReference + >(), + ): Map< + ts.PropertyAccessExpression | ts.LeftHandSideExpression | ts.Identifier | ts.PrivateIdentifier, + SubmoduleReference + > { + if (ts.isPropertyAccessExpression(node)) { + const [head, ...tail] = propertyPath(node); + const symbol = typeChecker.getSymbolAtLocation(head.name); + if (symbol && importDeclarations.has(symbol)) { + // This is a reference within an imported namespace, so we need to record that... + const firstNonNamespace = tail.findIndex((item) => !isLikelyNamespace(item.name, typeChecker)); + if (firstNonNamespace < 0) { + map.set(node.expression, new SubmoduleReference(symbol, node.expression, [])); + } else { + const tailEnd = tail[firstNonNamespace].expression; + const path = tail.slice(0, firstNonNamespace).map((item) => item.name); + map.set(tailEnd, new SubmoduleReference(symbol, tailEnd, path)); + } + } + + return map; + } + + // Faster than ||-ing a bung of if statements to avoid traversing uninteresting nodes... + switch (node.kind) { + case ts.SyntaxKind.ImportDeclaration: + case ts.SyntaxKind.ExportDeclaration: + break; + default: + for (const child of node.getChildren()) { + map = SubmoduleReference.inNode(child, typeChecker, importDeclarations, map); + } + } + + return map; + } + + private constructor( + public readonly root: ts.Symbol, + public readonly submoduleChain: ts.LeftHandSideExpression | ts.Identifier | ts.PrivateIdentifier, + public readonly path: readonly ts.Node[], + ) {} + + public get lastNode(): ts.Node { + if (this.path.length === 0) { + const node = this.root.valueDeclaration ?? this.root.declarations[0]; + return ts.isNamespaceImport(node) || ts.isImportSpecifier(node) ? node.name : node; + } + return this.path[this.path.length - 1]; + } + + public toString(): string { + return `${this.constructor.name} item.getText(item.getSourceFile())), + )}>`; + } +} + +/** + * Determines what symbols are imported by the given TypeScript import + * delcaration, in the context of the specified file, using the provided type + * checker. + * + * @param decl an import declaration. + * @param sourceFile the source file that contains the import declaration. + * @param typeChecker a TypeChecker instance valid for the provided source file. + * + * @returns the (possibly empty) list of symbols imported by this declaration. + */ +function importedSymbolsFrom( + decl: ts.ImportDeclaration, + sourceFile: ts.SourceFile, + typeChecker: ts.TypeChecker, +): ts.Symbol[] { + const { importClause } = decl; + + if (importClause == null) { + // This is a "for side effects" import, which isn't relevant for our business here... + return []; + } + + const { name, namedBindings } = importClause; + const imports = new Array(); + + if (name != null) { + const symbol = typeChecker.getSymbolAtLocation(name); + if (symbol == null) { + throw new Error(`No symbol was defined for node ${name.getText(sourceFile)}`); + } + imports.push(symbol); + } + if (namedBindings != null) { + if (ts.isNamespaceImport(namedBindings)) { + const { name } = namedBindings; + const symbol = typeChecker.getSymbolAtLocation(name); + if (symbol == null) { + throw new Error(`No symbol was defined for node ${name.getText(sourceFile)}`); + } + imports.push(symbol); + } else { + for (const specifier of namedBindings.elements) { + const { name } = specifier; + const symbol = typeChecker.getSymbolAtLocation(name); + if (symbol == null) { + throw new Error(`No symbol was defined for node ${name.getText(sourceFile)}`); + } + imports.push(symbol); + } + } + } + + return imports; +} + +interface PathEntry { + readonly name: ts.Identifier | ts.PrivateIdentifier | ts.LeftHandSideExpression; + readonly expression: ts.LeftHandSideExpression; +} + +function propertyPath(node: ts.PropertyAccessExpression): readonly PathEntry[] { + const { expression, name } = node; + if (!ts.isPropertyAccessExpression(expression)) { + return [ + { name: expression, expression }, + { name, expression }, + ]; + } + return [...propertyPath(expression), { name, expression }]; +} + +/** + * A heuristic to determine whether the provided node likely refers to some + * namespace. + * + * @param node the node to be checked. + * @param typeChecker a type checker that can obtain symbols for this node. + * + * @returns true if the node likely refers to a namespace name. + */ +function isLikelyNamespace(node: ts.Node, typeChecker: ts.TypeChecker): boolean { + if (!ts.isIdentifier(node)) { + return false; + } + + // If the identifier was bound to a symbol, we can inspect the declarations of + // it to validate they are all module or namespace declarations. + const symbol = typeChecker.getSymbolAtLocation(node); + if (symbol != null) { + return ( + symbol.declarations.length > 0 && + symbol.declarations.every( + (decl) => ts.isModuleDeclaration(decl) || ts.isNamespaceExport(decl) || ts.isNamespaceImport(decl), + ) + ); + } + + // We understand this is likely a namespace if the name does not start with + // upper-case letter. + return !startsWithUpperCase(node.text); +} + +function startsWithUpperCase(text: string): boolean { + return text.length > 0 && text[0] === text[0].toUpperCase(); +} diff --git a/packages/jsii-rosetta/lib/translate.ts b/packages/jsii-rosetta/lib/translate.ts index 74dfe27b16..8ce88f226b 100644 --- a/packages/jsii-rosetta/lib/translate.ts +++ b/packages/jsii-rosetta/lib/translate.ts @@ -3,10 +3,12 @@ import { inspect } from 'util'; import { TARGET_LANGUAGES, TargetLanguage } from './languages'; import { RecordReferencesVisitor } from './languages/record-references'; +import { supportsTransitiveSubmoduleAccess } from './languages/target-language'; import * as logging from './logging'; import { renderTree } from './o-tree'; import { AstRenderer, AstHandler, AstRendererOptions } from './renderer'; import { TypeScriptSnippet, completeSource, SnippetParameters, formatLocation } from './snippet'; +import { SubmoduleReference, SubmoduleReferenceMap } from './submodule-reference'; import { snippetKey } from './tablets/key'; import { ORIGINAL_SNIPPET_KEY } from './tablets/schema'; import { TranslatedSnippet } from './tablets/tablets'; @@ -50,10 +52,14 @@ export class Translator { const translator = this.translatorFor(snip); const translations = mkDict( - languages.map((lang) => { + languages.flatMap((lang, idx, languages) => { + if (languages.slice(0, idx).includes(lang)) { + // This language was duplicated in the request... we'll skip that here... + return []; + } const languageConverterFactory = TARGET_LANGUAGES[lang]; const translated = translator.renderUsing(languageConverterFactory.createVisitor()); - return [lang, { source: translated, version: languageConverterFactory.version }] as const; + return [[lang, { source: translated, version: languageConverterFactory.version }] as const]; }), ); @@ -155,6 +161,7 @@ export class SnippetTranslator { private readonly visibleSpans: Spans; private readonly compilation!: CompilationResult; private readonly tryCompile: boolean; + private readonly submoduleReferences: SubmoduleReferenceMap; public constructor(snippet: TypeScriptSnippet, private readonly options: SnippetTranslatorOptions = {}) { const compiler = options.compiler ?? new TypeScriptCompiler(); @@ -171,6 +178,12 @@ export class SnippetTranslator { // Respect '/// !hide' and '/// !show' directives this.visibleSpans = Spans.visibleSpansFromSource(source); + // Find submodule references on explicit imports + this.submoduleReferences = SubmoduleReference.inSourceFile( + this.compilation.rootFile, + this.compilation.program.getTypeChecker(), + ); + // This makes it about 5x slower, so only do it on demand // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing this.tryCompile = (options.includeCompilerDiagnostics || snippet.strict) ?? false; @@ -225,6 +238,8 @@ export class SnippetTranslator { this.compilation.program.getTypeChecker(), visitor, this.options, + // If we support transitive submodule access, don't provide a submodule reference map. + supportsTransitiveSubmoduleAccess(visitor.language) ? undefined : this.submoduleReferences, ); const converted = converter.convert(this.compilation.rootFile); this.translateDiagnostics.push(...filterVisibleDiagnostics(converter.diagnostics, this.visibleSpans)); @@ -243,6 +258,7 @@ export class SnippetTranslator { this.compilation.program.getTypeChecker(), visitor, this.options, + this.submoduleReferences, ); converter.convert(this.compilation.rootFile); return visitor.fqnsReferenced(); diff --git a/packages/jsii-rosetta/lib/typescript/imports.ts b/packages/jsii-rosetta/lib/typescript/imports.ts index 830bf29388..0210a62fc4 100644 --- a/packages/jsii-rosetta/lib/typescript/imports.ts +++ b/packages/jsii-rosetta/lib/typescript/imports.ts @@ -2,6 +2,7 @@ import * as ts from 'typescript'; import { JsiiSymbol, parentSymbol, lookupJsiiSymbolFromNode } from '../jsii/jsii-utils'; import { AstRenderer } from '../renderer'; +import { SubmoduleReferenceMap } from '../submodule-reference'; import { fmap } from '../util'; import { allOfType, matchAst, nodeOfType, stringFromLiteral } from './ast-utils'; @@ -17,7 +18,16 @@ export interface ImportStatement { export type FullImport = { readonly import: 'full'; - readonly alias: string; + /** + * The name of the namespace prefix in the source code. Used to strip the + * prefix in certain languages (e.g: Java). + */ + readonly sourceName: string; + /** + * The name under which this module is imported. Undefined if the module is + * not aliased (could be the case for namepsace/submodule imports). + */ + readonly alias?: string; }; export type SelectiveImport = { @@ -42,15 +52,27 @@ export function analyzeImportEquals(node: ts.ImportEqualsDeclaration, context: A moduleName = stringFromLiteral(bindings.ref.expression); }); + const sourceName = context.textOf(node.name); + return { node, packageName: moduleName, moduleSymbol: lookupJsiiSymbolFromNode(context.typeChecker, node.name), - imports: { import: 'full', alias: context.textOf(node.name) }, + imports: { import: 'full', alias: sourceName, sourceName }, }; } -export function analyzeImportDeclaration(node: ts.ImportDeclaration, context: AstRenderer): ImportStatement { +export function analyzeImportDeclaration(node: ts.ImportDeclaration, context: AstRenderer): ImportStatement; +export function analyzeImportDeclaration( + node: ts.ImportDeclaration, + context: AstRenderer, + submoduleReferences: SubmoduleReferenceMap, +): ImportStatement[]; +export function analyzeImportDeclaration( + node: ts.ImportDeclaration, + context: AstRenderer, + submoduleReferences?: SubmoduleReferenceMap, +): ImportStatement | ImportStatement[] { const packageName = stringFromLiteral(node.moduleSpecifier); const starBindings = matchAst( @@ -62,15 +84,54 @@ export function analyzeImportDeclaration(node: ts.ImportDeclaration, context: As ); if (starBindings) { - return { + const sourceName = context.textOf(starBindings.namespace.name); + const bareImport: ImportStatement = { node, packageName, moduleSymbol: lookupJsiiSymbolFromNode(context.typeChecker, starBindings.namespace.name), imports: { import: 'full', - alias: context.textOf(starBindings.namespace.name), + alias: sourceName, + sourceName, }, }; + if (submoduleReferences == null) { + return bareImport; + } + + const rootSymbol = context.typeChecker.getSymbolAtLocation(starBindings.namespace.name); + const refs = rootSymbol && Array.from(submoduleReferences.values()).filter((ref) => ref.root === rootSymbol); + // No submodule reference, or only 1 where the path is empty (this is used to signal the use of the bare import so it's not erased) + if (refs == null || refs.length === 0 || (refs.length === 1 && refs[0].path.length === 0)) { + return [bareImport]; + } + + return refs.flatMap(({ lastNode, path, root, submoduleChain }, idx, array): ImportStatement[] => { + if ( + array + .slice(0, idx) + .some( + (other) => other.root === root && context.textOf(other.submoduleChain) === context.textOf(submoduleChain), + ) + ) { + // This would be a duplicate, so we're skipping it + return []; + } + + const moduleSymbol = lookupJsiiSymbolFromNode(context.typeChecker, lastNode); + return [ + { + node, + packageName: [packageName, ...path.map((node) => context.textOf(node))].join('/'), + moduleSymbol, + imports: { + import: 'full', + alias: undefined, // No alias exists in the source text for this... + sourceName: context.textOf(submoduleChain), + }, + }, + ]; + }); } const namedBindings = matchAst( @@ -84,32 +145,77 @@ export function analyzeImportDeclaration(node: ts.ImportDeclaration, context: As ), ); - const elements: ImportBinding[] = []; - if (namedBindings) { - elements.push( - ...namedBindings.specifiers.map((spec) => { - // regular import { name }, renamed import { propertyName, name } - if (spec.propertyName) { - // Renamed import - return { - sourceName: context.textOf(spec.propertyName), - alias: context.textOf(spec.name), - importedSymbol: lookupJsiiSymbolFromNode(context.typeChecker, spec.propertyName), - } as ImportBinding; + const extraImports = new Array(); + const elements: ImportBinding[] = (namedBindings?.specifiers ?? []).flatMap( + ({ name, propertyName }): ImportBinding[] => { + // regular import { name } + // renamed import { propertyName as name } + const directBinding = { + sourceName: context.textOf(propertyName ?? name), + alias: propertyName && context.textOf(name), + importedSymbol: lookupJsiiSymbolFromNode(context.typeChecker, propertyName ?? name), + } as const; + + if (submoduleReferences != null) { + const symbol = context.typeChecker.getSymbolAtLocation(name); + let omitDirectBinding = false; + for (const match of Array.from(submoduleReferences.values()).filter((ref) => ref.root === symbol)) { + if (match.path.length === 0) { + // This is a namespace binding that is used as-is (not via a transitive path). It needs to be preserved. + omitDirectBinding = false; + continue; + } + const subPackageName = [packageName, ...match.path.map((node) => node.getText(node.getSourceFile()))].join( + '/', + ); + const importedSymbol = lookupJsiiSymbolFromNode(context.typeChecker, match.lastNode); + const moduleSymbol = fmap(importedSymbol, parentSymbol); + const importStatement = + extraImports.find((stmt) => { + if (moduleSymbol != null) { + return stmt.moduleSymbol === moduleSymbol; + } + return stmt.packageName === subPackageName; + }) ?? + extraImports[ + extraImports.push({ + moduleSymbol, + node: match.lastNode, + packageName: subPackageName, + imports: { import: 'selective', elements: [] }, + }) - 1 + ]; + + (importStatement.imports as SelectiveImport).elements.push({ + sourceName: context.textOf(match.submoduleChain), + importedSymbol, + }); } + if (omitDirectBinding) { + return []; + } + } + + return [directBinding]; + }, + ); - return { - sourceName: context.textOf(spec.name), - importedSymbol: lookupJsiiSymbolFromNode(context.typeChecker, spec.name), - }; - }), - ); + if (submoduleReferences == null) { + return { + node, + packageName, + imports: { import: 'selective', elements }, + moduleSymbol: fmap(elements?.[0]?.importedSymbol, parentSymbol), + }; } - return { - node, - packageName, - imports: { import: 'selective', elements }, - moduleSymbol: fmap(elements?.[0]?.importedSymbol, parentSymbol), - }; + return [ + { + node, + packageName, + imports: { import: 'selective', elements }, + moduleSymbol: fmap(elements?.[0]?.importedSymbol, parentSymbol), + }, + ...extraImports, + ]; } diff --git a/packages/jsii-rosetta/package.json b/packages/jsii-rosetta/package.json index ae13b80527..8379504608 100644 --- a/packages/jsii-rosetta/package.json +++ b/packages/jsii-rosetta/package.json @@ -21,6 +21,7 @@ "@types/workerpool": "^6.1.1", "@types/semver": "^7.3.13", "jsii-build-tools": "0.0.0", + "jsii-calc": "3.20.120", "memory-streams": "^0.1.3", "mock-fs": "^5.2.0" }, diff --git a/packages/jsii-rosetta/test/commands/transliterate.test.ts b/packages/jsii-rosetta/test/commands/transliterate.test.ts index a59d11bafa..def2c33d9d 100644 --- a/packages/jsii-rosetta/test/commands/transliterate.test.ts +++ b/packages/jsii-rosetta/test/commands/transliterate.test.ts @@ -149,7 +149,7 @@ export class ClassName implements IInterface { "markdown": "# README \`\`\`csharp - IInterface object = new ClassName("this", 1337, new ClassNameProps { Foo = "bar" }); + var object = new ClassName("this", 1337, new ClassNameProps { Foo = "bar" }); object.Property = EnumType.OPTION_A; object.MethodCall(); diff --git a/packages/jsii-rosetta/test/jsii-imports.test.ts b/packages/jsii-rosetta/test/jsii-imports.test.ts index d388b79e70..7f70f34390 100644 --- a/packages/jsii-rosetta/test/jsii-imports.test.ts +++ b/packages/jsii-rosetta/test/jsii-imports.test.ts @@ -242,7 +242,7 @@ describe('no submodule', () => { // eslint-disable-next-line prettier/prettier expectTranslation(trans, TargetLanguage.CSHARP, [ 'using Example.Test.Demo;', - 'MyEnum x = MyEnum.OPTION_A;', + 'var x = MyEnum.OPTION_A;', ]); }); }); @@ -276,7 +276,7 @@ describe('no submodule', () => { // eslint-disable-next-line prettier/prettier expectTranslation(trans, TargetLanguage.CSHARP, [ 'using Example.Test.Demo;', - 'MyEnum x = MyEnum.OPTION_A;', + 'var x = MyEnum.OPTION_A;', ]); }); }); @@ -482,7 +482,7 @@ const DEFAULT_JAVA_CODE = [ // The implementation part of the CSharp code is always the same const DEFAULT_CSHARP_CODE = [ - 'MyClass obj = new MyClass("value", new MyClassProps {', + 'var obj = new MyClass("value", new MyClassProps {', ' MyStruct = new MyStruct {', ' Value = "v"', ' }', diff --git a/packages/jsii-rosetta/test/translations/calls/shorthand_property.cs b/packages/jsii-rosetta/test/translations/calls/shorthand_property.cs index 595427e1b6..7fdbfa6ba0 100644 --- a/packages/jsii-rosetta/test/translations/calls/shorthand_property.cs +++ b/packages/jsii-rosetta/test/translations/calls/shorthand_property.cs @@ -1,2 +1,2 @@ -string foo = "hello"; -CallFunction(new Struct { Foo = foo }); \ No newline at end of file +var foo = "hello"; +CallFunction(new Struct { Foo = foo }); diff --git a/packages/jsii-rosetta/test/translations/calls/will_type_deep_structs_directly_if_type_info_is_available.go b/packages/jsii-rosetta/test/translations/calls/will_type_deep_structs_directly_if_type_info_is_available.go index db753ca420..9a870cd954 100644 --- a/packages/jsii-rosetta/test/translations/calls/will_type_deep_structs_directly_if_type_info_is_available.go +++ b/packages/jsii-rosetta/test/translations/calls/will_type_deep_structs_directly_if_type_info_is_available.go @@ -16,9 +16,9 @@ func foo(x *f64, outer *outerStruct) { } foo(jsii.Number(25), &outerStruct{ - foo: jsii.Number(3), - deeper: &deeperStruct{ - a: jsii.Number(1), - b: jsii.Number(2), + Foo: jsii.Number(3), + Deeper: &deeperStruct{ + A: jsii.Number(1), + B: jsii.Number(2), }, }) diff --git a/packages/jsii-rosetta/test/translations/expressions/await.cs b/packages/jsii-rosetta/test/translations/expressions/await.cs index 24cfb3b9c0..8f613704ed 100644 --- a/packages/jsii-rosetta/test/translations/expressions/await.cs +++ b/packages/jsii-rosetta/test/translations/expressions/await.cs @@ -1 +1 @@ -int x = Future(); \ No newline at end of file +var x = Future(); diff --git a/packages/jsii-rosetta/test/translations/expressions/backtick_string_w_o_substitutions.cs b/packages/jsii-rosetta/test/translations/expressions/backtick_string_w_o_substitutions.cs index 9c2fb840a5..496ef56f7b 100644 --- a/packages/jsii-rosetta/test/translations/expressions/backtick_string_w_o_substitutions.cs +++ b/packages/jsii-rosetta/test/translations/expressions/backtick_string_w_o_substitutions.cs @@ -1 +1 @@ -string x = "some string"; \ No newline at end of file +var x = "some string"; diff --git a/packages/jsii-rosetta/test/translations/expressions/computed_key.cs b/packages/jsii-rosetta/test/translations/expressions/computed_key.cs index c967d3aa6b..60e9dac39d 100644 --- a/packages/jsii-rosetta/test/translations/expressions/computed_key.cs +++ b/packages/jsii-rosetta/test/translations/expressions/computed_key.cs @@ -1,4 +1,4 @@ -string y = "WHY?"; +var y = "WHY?"; IDictionary x = new Dictionary { { $"key-{y}", "value" } }; IDictionary z = new Dictionary { { y, true } }; diff --git a/packages/jsii-rosetta/test/translations/expressions/string_interpolation.cs b/packages/jsii-rosetta/test/translations/expressions/string_interpolation.cs index 0a9840046f..08e4c2a4e7 100644 --- a/packages/jsii-rosetta/test/translations/expressions/string_interpolation.cs +++ b/packages/jsii-rosetta/test/translations/expressions/string_interpolation.cs @@ -1,5 +1,5 @@ -string x = "world"; -string y = "well"; +var x = "world"; +var y = "well"; Console.WriteLine($"Hello, {x}, it works {y}!"); // And now a multi-line expression diff --git a/packages/jsii-rosetta/test/translations/expressions/string_literal.cs b/packages/jsii-rosetta/test/translations/expressions/string_literal.cs index 64c5ebc69d..187b415102 100644 --- a/packages/jsii-rosetta/test/translations/expressions/string_literal.cs +++ b/packages/jsii-rosetta/test/translations/expressions/string_literal.cs @@ -1,5 +1,5 @@ -string literal = @" -This si a multiline string literal. +var literal = @" +This is a multiline string literal. ""It's cool!"". diff --git a/packages/jsii-rosetta/test/translations/expressions/string_literal.go b/packages/jsii-rosetta/test/translations/expressions/string_literal.go index 4ff71f0a21..07b11a356d 100644 --- a/packages/jsii-rosetta/test/translations/expressions/string_literal.go +++ b/packages/jsii-rosetta/test/translations/expressions/string_literal.go @@ -1 +1,9 @@ -literal := "\nThis si a multiline string literal.\n\n\"It's cool!\".\n\nYEAH BABY!!\n\nLitteral \\n right here (not a newline!)\n" +literal := ` +This is a multiline string literal. + +"It's cool!". + +YEAH BABY!! + +Litteral \\n right here (not a newline!) +` diff --git a/packages/jsii-rosetta/test/translations/expressions/string_literal.java b/packages/jsii-rosetta/test/translations/expressions/string_literal.java index d81e09e1e9..a9a03175b7 100644 --- a/packages/jsii-rosetta/test/translations/expressions/string_literal.java +++ b/packages/jsii-rosetta/test/translations/expressions/string_literal.java @@ -1 +1 @@ -String literal = "\nThis si a multiline string literal.\n\n\"It's cool!\".\n\nYEAH BABY!!\n\nLitteral \\n right here (not a newline!)\n"; +String literal = "\nThis is a multiline string literal.\n\n\"It's cool!\".\n\nYEAH BABY!!\n\nLitteral \\n right here (not a newline!)\n"; diff --git a/packages/jsii-rosetta/test/translations/expressions/string_literal.py b/packages/jsii-rosetta/test/translations/expressions/string_literal.py index b9f43b8966..49745aeaf0 100644 --- a/packages/jsii-rosetta/test/translations/expressions/string_literal.py +++ b/packages/jsii-rosetta/test/translations/expressions/string_literal.py @@ -1,5 +1,5 @@ literal = """ -This si a multiline string literal. +This is a multiline string literal. "It's cool!". diff --git a/packages/jsii-rosetta/test/translations/expressions/string_literal.ts b/packages/jsii-rosetta/test/translations/expressions/string_literal.ts index ebff58a601..20f2c0894c 100644 --- a/packages/jsii-rosetta/test/translations/expressions/string_literal.ts +++ b/packages/jsii-rosetta/test/translations/expressions/string_literal.ts @@ -1,5 +1,5 @@ const literal = ` -This si a multiline string literal. +This is a multiline string literal. "It's cool!". diff --git a/packages/jsii-rosetta/test/translations/expressions/struct_assignment.cs b/packages/jsii-rosetta/test/translations/expressions/struct_assignment.cs index 975d97e10f..ad3ae95492 100644 --- a/packages/jsii-rosetta/test/translations/expressions/struct_assignment.cs +++ b/packages/jsii-rosetta/test/translations/expressions/struct_assignment.cs @@ -3,4 +3,4 @@ class Test public string Key { get; set; } } -Test x = new Test { Key = "value" }; +var x = new Test { Key = "value" }; diff --git a/packages/jsii-rosetta/test/translations/imports/selective_import.java b/packages/jsii-rosetta/test/translations/imports/selective_import.java index 3412911fec..333742a4f7 100644 --- a/packages/jsii-rosetta/test/translations/imports/selective_import.java +++ b/packages/jsii-rosetta/test/translations/imports/selective_import.java @@ -1,6 +1,6 @@ -import scope.some.module.one; +import scope.some.module.one.*; import scope.some.module.Two; -import scope.some.module.someThree; -import scope.some.module.four; +import scope.some.module.someThree.*; +import scope.some.module.four.*; new Two(); renamed(); diff --git a/packages/jsii-rosetta/test/translations/imports/submodule-import.cs b/packages/jsii-rosetta/test/translations/imports/submodule-import.cs new file mode 100644 index 0000000000..b83d38c184 --- /dev/null +++ b/packages/jsii-rosetta/test/translations/imports/submodule-import.cs @@ -0,0 +1,15 @@ +using Amazon.JSII.Tests.CalculatorNamespace; +using Amazon.JSII.Tests.CalculatorNamespace.HomonymousForwardReferences; +using Gen.Providers.Aws; + +// Access without existing type information +var awsKmsKeyExamplekms = new Kms.KmsKey(this, "examplekms", new Struct { + DeletionWindowInDays = 7, + Description = "KMS key 1" +}); + +// Accesses two distinct points of the submodule hierarchy +var myClass = new Submodule.MyClass(new SomeStruct { Prop = Submodule.Child.SomeEnum.SOME }); + +// Access via a renamed import +Foo.Consumer.Consume(new ConsumerProps { Homonymous = new Homonymous { StringProperty = "yes" } }); diff --git a/packages/jsii-rosetta/test/translations/imports/submodule-import.go b/packages/jsii-rosetta/test/translations/imports/submodule-import.go new file mode 100644 index 0000000000..4d09940446 --- /dev/null +++ b/packages/jsii-rosetta/test/translations/imports/submodule-import.go @@ -0,0 +1,23 @@ +import "github.com/aws/jsii/jsii-calc/go/jsiicalc/submodule" +import "github.com/aws/jsii/jsii-calc/go/jsiicalc/submodule/child" +import "github.com/aws/jsii/jsii-calc/go/jsiicalc" +import "github.com/aws/jsii/jsii-calc/go/jsiicalc/foo" +import "github.com/aws-samples/dummy/gen/providers/aws/kms" + +// Access without existing type information +awsKmsKeyExamplekms := kms.NewKmsKey(this, jsii.String("examplekms"), map[string]interface{}{ + "deletionWindowInDays": jsii.Number(7), + "description": jsii.String("KMS key 1"), +}) + +// Accesses two distinct points of the submodule hierarchy +myClass := submodule.NewMyClass(&SomeStruct{ + Prop: child.SomeEnum_SOME, +}) + +// Access via a renamed import +foo.Consumer_Consume(&ConsumerProps{ + Homonymous: &Homonymous{ + StringProperty: jsii.String("yes"), + }, +}) diff --git a/packages/jsii-rosetta/test/translations/imports/submodule-import.java b/packages/jsii-rosetta/test/translations/imports/submodule-import.java new file mode 100644 index 0000000000..e0c1acae67 --- /dev/null +++ b/packages/jsii-rosetta/test/translations/imports/submodule-import.java @@ -0,0 +1,17 @@ +import software.amazon.jsii.tests.calculator.submodule.*; +import software.amazon.jsii.tests.calculator.submodule.child.*; +import software.amazon.jsii.tests.calculator.homonymousForwardReferences.*; +import software.amazon.jsii.tests.calculator.homonymousForwardReferences.foo.*; +import gen.providers.aws.kms.*; + +// Access without existing type information +Object awsKmsKeyExamplekms = KmsKey.Builder.create(this, "examplekms") + .deletionWindowInDays(7) + .description("KMS key 1") + .build(); + +// Accesses two distinct points of the submodule hierarchy +MyClass myClass = MyClass.Builder.create().prop(SomeEnum.SOME).build(); + +// Access via a renamed import +Consumer.consume(ConsumerProps.builder().homonymous(Homonymous.builder().stringProperty("yes").build()).build()); diff --git a/packages/jsii-rosetta/test/translations/imports/submodule-import.py b/packages/jsii-rosetta/test/translations/imports/submodule-import.py new file mode 100644 index 0000000000..2ff0a54a0e --- /dev/null +++ b/packages/jsii-rosetta/test/translations/imports/submodule-import.py @@ -0,0 +1,15 @@ +import jsii_calc as calc +from jsii_calc import homonymous_forward_references as ns +import ...gen.providers.aws as aws + +# Access without existing type information +aws_kms_key_examplekms = aws.kms.KmsKey(self, "examplekms", + deletion_window_in_days=7, + description="KMS key 1" +) + +# Accesses two distinct points of the submodule hierarchy +my_class = calc.submodule.MyClass(prop=calc.submodule.child.SomeEnum.SOME) + +# Access via a renamed import +ns.foo.Consumer.consume(homonymous=ns.foo.Homonymous(string_property="yes")) diff --git a/packages/jsii-rosetta/test/translations/imports/submodule-import.ts b/packages/jsii-rosetta/test/translations/imports/submodule-import.ts new file mode 100644 index 0000000000..b552e577af --- /dev/null +++ b/packages/jsii-rosetta/test/translations/imports/submodule-import.ts @@ -0,0 +1,15 @@ +import * as calc from 'jsii-calc'; +import { homonymousForwardReferences as ns } from 'jsii-calc'; +import * as aws from './.gen/providers/aws'; + +// Access without existing type information +const awsKmsKeyExamplekms = new aws.kms.KmsKey(this, 'examplekms', { + deletionWindowInDays: 7, + description: 'KMS key 1', +}); + +// Accesses two distinct points of the submodule hierarchy +const myClass = new calc.submodule.MyClass({ prop: calc.submodule.child.SomeEnum.SOME }); + +// Access via a renamed import +ns.foo.Consumer.consume({ homonymous: { stringProperty: 'yes' } }); diff --git a/packages/jsii-rosetta/test/translations/statements/statements_and_newlines.cs b/packages/jsii-rosetta/test/translations/statements/statements_and_newlines.cs index 9f6d62ccdf..a2040eded2 100644 --- a/packages/jsii-rosetta/test/translations/statements/statements_and_newlines.cs +++ b/packages/jsii-rosetta/test/translations/statements/statements_and_newlines.cs @@ -1,6 +1,6 @@ public int DoThing() { - int x = 1; // x seems to be equal to 1 + var x = 1; // x seems to be equal to 1 return x + 1; } @@ -15,12 +15,12 @@ public boolean DoThing2(int x) public int DoThing3() { - int x = 1; + var x = 1; return x + 1; } public void DoThing4() { - int x = 1; + var x = 1; x = 85; -} \ No newline at end of file +} diff --git a/packages/jsii-rosetta/test/translations/structs/infer_struct_from_union.go b/packages/jsii-rosetta/test/translations/structs/infer_struct_from_union.go index 890a35ba1c..8c07048685 100644 --- a/packages/jsii-rosetta/test/translations/structs/infer_struct_from_union.go +++ b/packages/jsii-rosetta/test/translations/structs/infer_struct_from_union.go @@ -1,7 +1,7 @@ takes(&myProps{ - struct_: &someStruct{ - enabled: jsii.Boolean(false), - option: jsii.String("option"), + Struct: &someStruct{ + Enabled: jsii.Boolean(false), + Option: jsii.String("option"), }, }) diff --git a/packages/jsii-rosetta/test/translations/structs/optional_known_struct.go b/packages/jsii-rosetta/test/translations/structs/optional_known_struct.go index d2260cdfa3..5ba6138412 100644 --- a/packages/jsii-rosetta/test/translations/structs/optional_known_struct.go +++ b/packages/jsii-rosetta/test/translations/structs/optional_known_struct.go @@ -1,3 +1,3 @@ NewVpc(this, jsii.String("Something"), &vpcProps{ - argument: jsii.Number(5), + Argument: jsii.Number(5), }) diff --git a/packages/jsii-rosetta/test/translations/structs/struct_starting_with_i.go b/packages/jsii-rosetta/test/translations/structs/struct_starting_with_i.go index 9a74d11250..e3a653acb2 100644 --- a/packages/jsii-rosetta/test/translations/structs/struct_starting_with_i.go +++ b/packages/jsii-rosetta/test/translations/structs/struct_starting_with_i.go @@ -1,3 +1,3 @@ NewIntegration(this, jsii.String("Something"), &integrationOptions{ - argument: jsii.Number(5), + Argument: jsii.Number(5), }) diff --git a/packages/jsii-rosetta/test/translations/structs/var_new_class_known_struct.cs b/packages/jsii-rosetta/test/translations/structs/var_new_class_known_struct.cs index c449a530d6..dac70b4fcc 100644 --- a/packages/jsii-rosetta/test/translations/structs/var_new_class_known_struct.cs +++ b/packages/jsii-rosetta/test/translations/structs/var_new_class_known_struct.cs @@ -1,3 +1,3 @@ -Vpc vpc = new Vpc(this, "Something", new VpcProps { +var vpc = new Vpc(this, "Something", new VpcProps { Argument = 5 }); diff --git a/packages/jsii-rosetta/test/translations/structs/var_new_class_known_struct.go b/packages/jsii-rosetta/test/translations/structs/var_new_class_known_struct.go index b1fa2fbdf2..61ccdb2311 100644 --- a/packages/jsii-rosetta/test/translations/structs/var_new_class_known_struct.go +++ b/packages/jsii-rosetta/test/translations/structs/var_new_class_known_struct.go @@ -1,3 +1,3 @@ vpc := NewVpc(this, jsii.String("Something"), &vpcProps{ - argument: jsii.Number(5), + Argument: jsii.Number(5), })