diff --git a/packages/jsii-rosetta/lib/jsii/jsii-utils.ts b/packages/jsii-rosetta/lib/jsii/jsii-utils.ts index 40ad0aa827..05d363b05d 100644 --- a/packages/jsii-rosetta/lib/jsii/jsii-utils.ts +++ b/packages/jsii-rosetta/lib/jsii/jsii-utils.ts @@ -15,7 +15,7 @@ export function isStructType(type: ts.Type) { ); } -function hasFlag(flags: A, test: A) { +export function hasFlag(flags: A, test: A) { // tslint:disable-next-line:no-bitwise return (flags & test) !== 0; } diff --git a/packages/jsii-rosetta/lib/languages/default.ts b/packages/jsii-rosetta/lib/languages/default.ts index 36c79ce586..ed93de511b 100644 --- a/packages/jsii-rosetta/lib/languages/default.ts +++ b/packages/jsii-rosetta/lib/languages/default.ts @@ -1,6 +1,6 @@ import * as ts from 'typescript'; -import { isStructInterface, isStructType } from '../jsii/jsii-utils'; +import { hasFlag, isStructInterface, isStructType } from '../jsii/jsii-utils'; import { OTree, NO_SYNTAX } from '../o-tree'; import { AstRenderer, AstHandler, nimpl, CommentSyntax } from '../renderer'; import { voidExpressionString } from '../typescript/ast-utils'; @@ -132,7 +132,21 @@ export abstract class DefaultVisitor implements AstHandler { public objectLiteralExpression(node: ts.ObjectLiteralExpression, context: AstRenderer): OTree { const type = typeWithoutUndefinedUnion(context.inferredTypeOfExpression(node)); - const isUnknownType = !type || !type.symbol; + let isUnknownType = !type; + if (type && hasFlag(type.flags, ts.TypeFlags.Any)) { + // The type checker by itself won't tell us the difference between an `any` that + // was literally declared as a type in the code, vs an `any` it assumes because it + // can't find a function's type declaration. + // + // Search for the function's declaration and only if we can't find it, + // the type is actually unknown (otherwise it's a literal 'any'). + const call = findEnclosingCallExpression(node); + const signature = call ? context.typeChecker.getResolvedSignature(call) : undefined; + if (!signature?.declaration) { + isUnknownType = true; + } + } + const isKnownStruct = type && isStructType(type); if (isUnknownType) { @@ -317,3 +331,14 @@ const UNARY_OPS: { [op in ts.PrefixUnaryOperator]: string } = { [ts.SyntaxKind.TildeToken]: '~', [ts.SyntaxKind.ExclamationToken]: '~', }; + +function findEnclosingCallExpression(node?: ts.Node): ts.CallLikeExpression | undefined { + while (node) { + if (ts.isCallLikeExpression(node)) { + return node; + } + node = node.parent; + } + + return undefined; +} diff --git a/packages/jsii-rosetta/lib/typescript/types.ts b/packages/jsii-rosetta/lib/typescript/types.ts index 1bc1d76188..a74a6f1fa7 100644 --- a/packages/jsii-rosetta/lib/typescript/types.ts +++ b/packages/jsii-rosetta/lib/typescript/types.ts @@ -90,9 +90,24 @@ export function inferMapElementType( elements: ts.NodeArray, renderer: AstRenderer, ): ts.Type | undefined { - return typeIfSame( - elements.map((el) => (ts.isPropertyAssignment(el) ? renderer.typeOfExpression(el.initializer) : undefined)), - ); + const nodes = elements.map(elementValueNode).filter(isDefined); + const types = nodes.map((x) => renderer.typeOfExpression(x)); + + return types.every((t) => isSameType(types[0], t)) ? types[0] : undefined; + + function elementValueNode(el: ts.ObjectLiteralElementLike): ts.Expression | undefined { + if (ts.isPropertyAssignment(el)) { + return el.initializer; + } + if (ts.isShorthandPropertyAssignment(el)) { + return el.name; + } + return undefined; + } +} + +function isSameType(a: ts.Type, b: ts.Type) { + return a.flags === b.flags && a.symbol?.name === b.symbol?.name; } function typeIfSame(types: Array): ts.Type | undefined { @@ -120,3 +135,7 @@ export function arrayElementType(type: ts.Type): ts.Type | undefined { } return undefined; } + +function isDefined(x: A): x is NonNullable { + return x !== undefined; +} diff --git a/packages/jsii-rosetta/test/translations/statements/vararg_any_call.cs b/packages/jsii-rosetta/test/translations/statements/vararg_any_call.cs index 67563261ff..6d653a93e7 100644 --- a/packages/jsii-rosetta/test/translations/statements/vararg_any_call.cs +++ b/packages/jsii-rosetta/test/translations/statements/vararg_any_call.cs @@ -2,6 +2,6 @@ public void Test(Array _args) { } -Test(new Struct { Key = "Value", Also = 1337 }); +Test(new Dictionary { { "Key", "Value" }, { "also", 1337 } }); -Test(new Struct { Key = "Value" }, new Struct { Also = 1337 }); +Test(new Dictionary { { "Key", "Value" } }, new Dictionary { { "also", 1337 } }); diff --git a/packages/jsii-rosetta/test/translations/structs/any_type_never_a_struct.cs b/packages/jsii-rosetta/test/translations/structs/any_type_never_a_struct.cs new file mode 100644 index 0000000000..f153ccb3c3 --- /dev/null +++ b/packages/jsii-rosetta/test/translations/structs/any_type_never_a_struct.cs @@ -0,0 +1,3 @@ +FunctionThatTakesAnAny(new Dictionary { + { "argument", 5 } +}); \ No newline at end of file diff --git a/packages/jsii-rosetta/test/translations/structs/any_type_never_a_struct.java b/packages/jsii-rosetta/test/translations/structs/any_type_never_a_struct.java new file mode 100644 index 0000000000..5393bf1b9a --- /dev/null +++ b/packages/jsii-rosetta/test/translations/structs/any_type_never_a_struct.java @@ -0,0 +1,2 @@ +functionThatTakesAnAny(Map.of( + "argument", 5)); \ No newline at end of file diff --git a/packages/jsii-rosetta/test/translations/structs/any_type_never_a_struct.py b/packages/jsii-rosetta/test/translations/structs/any_type_never_a_struct.py new file mode 100644 index 0000000000..bab97133f8 --- /dev/null +++ b/packages/jsii-rosetta/test/translations/structs/any_type_never_a_struct.py @@ -0,0 +1,3 @@ +function_that_takes_an_any({ + "argument": 5 +}) \ No newline at end of file diff --git a/packages/jsii-rosetta/test/translations/structs/any_type_never_a_struct.ts b/packages/jsii-rosetta/test/translations/structs/any_type_never_a_struct.ts new file mode 100644 index 0000000000..91dca55b13 --- /dev/null +++ b/packages/jsii-rosetta/test/translations/structs/any_type_never_a_struct.ts @@ -0,0 +1,6 @@ +/// !hide +function functionThatTakesAnAny(opts: any) { } +/// !show +functionThatTakesAnAny({ + argument: 5 +}); \ No newline at end of file