From 8dc845e007f092398f832af76aca500593b477fe Mon Sep 17 00:00:00 2001 From: Perryvw Date: Sun, 13 Mar 2022 20:07:57 +0100 Subject: [PATCH 1/2] Fix prototype call check for nullable types --- src/transformation/builtins/index.ts | 21 ++++++++++++++++---- src/transformation/utils/typescript/types.ts | 15 ++++++++++++++ test/unit/builtins/array.spec.ts | 12 +++++++++++ test/unit/builtins/numbers.spec.ts | 12 +++++++++++ test/unit/builtins/string.spec.ts | 12 +++++++++++ test/unit/functions/functions.spec.ts | 12 +++++++++++ 6 files changed, 80 insertions(+), 4 deletions(-) diff --git a/src/transformation/builtins/index.ts b/src/transformation/builtins/index.ts index b56486425..94c5cb44f 100644 --- a/src/transformation/builtins/index.ts +++ b/src/transformation/builtins/index.ts @@ -9,6 +9,7 @@ import { hasStandardLibrarySignature, isArrayType, isFunctionType, + isNullableType, isNumberType, isStandardLibraryType, isStringType, @@ -115,22 +116,34 @@ export function transformBuiltinCallExpression( } } - if (isStringType(context, ownerType) && hasStandardLibrarySignature(context, node)) { + const isStringFunction = + isStringType(context, ownerType) || + (expression.questionDotToken && isNullableType(context, ownerType, isStringType)); + if (isStringFunction && hasStandardLibrarySignature(context, node)) { if (isOptionalCall) return unsupportedOptionalCall(); return transformStringPrototypeCall(context, node); } - if (isNumberType(context, ownerType) && hasStandardLibrarySignature(context, node)) { + const isNumberFunction = + isNumberType(context, ownerType) || + (expression.questionDotToken && isNullableType(context, ownerType, isNumberType)); + if (isNumberFunction && hasStandardLibrarySignature(context, node)) { if (isOptionalCall) return unsupportedOptionalCall(); return transformNumberPrototypeCall(context, node); } - if (isArrayType(context, ownerType) && hasStandardLibrarySignature(context, node)) { + const isArrayFunction = + isArrayType(context, ownerType) || + (expression.questionDotToken && isNullableType(context, ownerType, isArrayType)); + if (isArrayFunction && hasStandardLibrarySignature(context, node)) { if (isOptionalCall) return unsupportedOptionalCall(); return transformArrayPrototypeCall(context, node); } - if (isFunctionType(ownerType) && hasStandardLibrarySignature(context, node)) { + const isFunctionFunction = + isFunctionType(ownerType) || + (expression.questionDotToken && isNullableType(context, ownerType, (_, t) => isFunctionType(t))); + if (isFunctionFunction && hasStandardLibrarySignature(context, node)) { if (isOptionalCall) return unsupportedOptionalCall(); return transformFunctionPrototypeCall(context, node); } diff --git a/src/transformation/utils/typescript/types.ts b/src/transformation/utils/typescript/types.ts index cb1af0824..4be744f7e 100644 --- a/src/transformation/utils/typescript/types.ts +++ b/src/transformation/utils/typescript/types.ts @@ -60,6 +60,10 @@ export function typeCanSatisfy( return false; } +export function isNullishType(context: TransformationContext, type: ts.Type): boolean { + return isTypeWithFlags(context, type, ts.TypeFlags.Undefined | ts.TypeFlags.Null | ts.TypeFlags.VoidLike); +} + export function isStringType(context: TransformationContext, type: ts.Type): boolean { return isTypeWithFlags(context, type, ts.TypeFlags.String | ts.TypeFlags.StringLike | ts.TypeFlags.StringLiteral); } @@ -68,6 +72,17 @@ export function isNumberType(context: TransformationContext, type: ts.Type): boo return isTypeWithFlags(context, type, ts.TypeFlags.Number | ts.TypeFlags.NumberLike | ts.TypeFlags.NumberLiteral); } +export function isNullableType( + context: TransformationContext, + type: ts.Type, + isType: (c: TransformationContext, t: ts.Type) => boolean +): boolean { + return ( + typeCanSatisfy(context, type, t => isType(context, t)) && + typeAlwaysSatisfies(context, type, t => isType(context, t) || isNullishType(context, t)) + ); +} + function isExplicitArrayType(context: TransformationContext, type: ts.Type): boolean { if (type.symbol) { const baseConstraint = context.checker.getBaseConstraintOfType(type); diff --git a/test/unit/builtins/array.spec.ts b/test/unit/builtins/array.spec.ts index aae630b78..eaeddf08e 100644 --- a/test/unit/builtins/array.spec.ts +++ b/test/unit/builtins/array.spec.ts @@ -700,3 +700,15 @@ test.each([ ])("trailing undefined or null are allowed in array literal (%p)", literal => { util.testExpression(literal).expectToHaveNoDiagnostics(); }); + +// Issue #1218: https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1218 +test.each(["[1, 2, 3]", "undefined"])("prototype call on nullable array (%p)", value => { + util.testFunction` + function find(arr?: number[]) { + return arr?.indexOf(2); + } + return find(${value}); + ` + .setOptions({ strictNullChecks: true }) + .expectToMatchJsResult(); +}); diff --git a/test/unit/builtins/numbers.spec.ts b/test/unit/builtins/numbers.spec.ts index ad9ae0c79..ab7e332a9 100644 --- a/test/unit/builtins/numbers.spec.ts +++ b/test/unit/builtins/numbers.spec.ts @@ -143,3 +143,15 @@ test.each([ ])("parseInt with base and trailing text (%p)", ({ numberString, base }) => { util.testExpression`parseInt("${numberString}", ${base})`.expectToMatchJsResult(); }); + +// Issue #1218: https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1218 +test.each(["42", "undefined"])("prototype call on nullable number (%p)", value => { + util.testFunction` + function toString(n?: number) { + return n?.toString(); + } + return toString(${value}); + ` + .setOptions({ strictNullChecks: true }) + .expectToMatchJsResult(); +}); diff --git a/test/unit/builtins/string.spec.ts b/test/unit/builtins/string.spec.ts index 70d7e2463..a924ece4f 100644 --- a/test/unit/builtins/string.spec.ts +++ b/test/unit/builtins/string.spec.ts @@ -367,3 +367,15 @@ test("string intersected method", () => { return ({ abc: () => "a" } as Vector).abc(); `.expectToMatchJsResult(); }); + +// Issue #1218: https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1218 +test.each(['"foo"', "undefined"])("prototype call on nullable string (%p)", value => { + util.testFunction` + function toUpper(str?: string) { + return str?.toUpperCase(); + } + return toUpper(${value}); + ` + .setOptions({ strictNullChecks: true }) + .expectToMatchJsResult(); +}); diff --git a/test/unit/functions/functions.spec.ts b/test/unit/functions/functions.spec.ts index b5fc4bdfd..c5a488460 100644 --- a/test/unit/functions/functions.spec.ts +++ b/test/unit/functions/functions.spec.ts @@ -181,6 +181,18 @@ test("function apply without arguments should not lead to exception", () => { `.expectToMatchJsResult(); }); +// Issue #1218: https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1218 +test.each(["() => 4", "undefined"])("prototype call on nullable function (%p)", value => { + util.testFunction` + function call(f?: () => number) { + return f?.apply(3); + } + return call(${value}); + ` + .setOptions({ strictNullChecks: true }) + .expectToMatchJsResult(); +}); + test("Function call", () => { util.testFunction` const abc = function (this: { a: number }, a: string) { return this.a + a; } From 8403960f387ed6de50b549dbe65bd5453db104cb Mon Sep 17 00:00:00 2001 From: Perryvw Date: Mon, 14 Mar 2022 20:36:22 +0100 Subject: [PATCH 2/2] Added extra test covering more types --- test/unit/builtins/string.spec.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test/unit/builtins/string.spec.ts b/test/unit/builtins/string.spec.ts index a924ece4f..9bb7fbebf 100644 --- a/test/unit/builtins/string.spec.ts +++ b/test/unit/builtins/string.spec.ts @@ -379,3 +379,18 @@ test.each(['"foo"', "undefined"])("prototype call on nullable string (%p)", valu .setOptions({ strictNullChecks: true }) .expectToMatchJsResult(); }); + +// Issue #1218: https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1218 +test.each(["string | undefined", "string | null", "null | string", "null | undefined | string"])( + "prototype call on nullable string type (%p)", + type => { + util.testFunction` + function toUpper(str: ${type}) { + return str?.toUpperCase(); + } + return toUpper("foo"); + ` + .setOptions({ strictNullChecks: true }) + .expectToMatchJsResult(); + } +);