X Tutup
Skip to content

Commit 3b41774

Browse files
authored
Fix prototype call check for nullable types (#1230)
* Fix prototype call check for nullable types * Added extra test covering more types
1 parent 191c20b commit 3b41774

File tree

6 files changed

+95
-4
lines changed

6 files changed

+95
-4
lines changed

src/transformation/builtins/index.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
hasStandardLibrarySignature,
1010
isArrayType,
1111
isFunctionType,
12+
isNullableType,
1213
isNumberType,
1314
isStandardLibraryType,
1415
isStringType,
@@ -115,22 +116,34 @@ export function transformBuiltinCallExpression(
115116
}
116117
}
117118

118-
if (isStringType(context, ownerType) && hasStandardLibrarySignature(context, node)) {
119+
const isStringFunction =
120+
isStringType(context, ownerType) ||
121+
(expression.questionDotToken && isNullableType(context, ownerType, isStringType));
122+
if (isStringFunction && hasStandardLibrarySignature(context, node)) {
119123
if (isOptionalCall) return unsupportedOptionalCall();
120124
return transformStringPrototypeCall(context, node);
121125
}
122126

123-
if (isNumberType(context, ownerType) && hasStandardLibrarySignature(context, node)) {
127+
const isNumberFunction =
128+
isNumberType(context, ownerType) ||
129+
(expression.questionDotToken && isNullableType(context, ownerType, isNumberType));
130+
if (isNumberFunction && hasStandardLibrarySignature(context, node)) {
124131
if (isOptionalCall) return unsupportedOptionalCall();
125132
return transformNumberPrototypeCall(context, node);
126133
}
127134

128-
if (isArrayType(context, ownerType) && hasStandardLibrarySignature(context, node)) {
135+
const isArrayFunction =
136+
isArrayType(context, ownerType) ||
137+
(expression.questionDotToken && isNullableType(context, ownerType, isArrayType));
138+
if (isArrayFunction && hasStandardLibrarySignature(context, node)) {
129139
if (isOptionalCall) return unsupportedOptionalCall();
130140
return transformArrayPrototypeCall(context, node);
131141
}
132142

133-
if (isFunctionType(ownerType) && hasStandardLibrarySignature(context, node)) {
143+
const isFunctionFunction =
144+
isFunctionType(ownerType) ||
145+
(expression.questionDotToken && isNullableType(context, ownerType, (_, t) => isFunctionType(t)));
146+
if (isFunctionFunction && hasStandardLibrarySignature(context, node)) {
134147
if (isOptionalCall) return unsupportedOptionalCall();
135148
return transformFunctionPrototypeCall(context, node);
136149
}

src/transformation/utils/typescript/types.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ export function typeCanSatisfy(
6060
return false;
6161
}
6262

63+
export function isNullishType(context: TransformationContext, type: ts.Type): boolean {
64+
return isTypeWithFlags(context, type, ts.TypeFlags.Undefined | ts.TypeFlags.Null | ts.TypeFlags.VoidLike);
65+
}
66+
6367
export function isStringType(context: TransformationContext, type: ts.Type): boolean {
6468
return isTypeWithFlags(context, type, ts.TypeFlags.String | ts.TypeFlags.StringLike | ts.TypeFlags.StringLiteral);
6569
}
@@ -68,6 +72,17 @@ export function isNumberType(context: TransformationContext, type: ts.Type): boo
6872
return isTypeWithFlags(context, type, ts.TypeFlags.Number | ts.TypeFlags.NumberLike | ts.TypeFlags.NumberLiteral);
6973
}
7074

75+
export function isNullableType(
76+
context: TransformationContext,
77+
type: ts.Type,
78+
isType: (c: TransformationContext, t: ts.Type) => boolean
79+
): boolean {
80+
return (
81+
typeCanSatisfy(context, type, t => isType(context, t)) &&
82+
typeAlwaysSatisfies(context, type, t => isType(context, t) || isNullishType(context, t))
83+
);
84+
}
85+
7186
function isExplicitArrayType(context: TransformationContext, type: ts.Type): boolean {
7287
if (type.symbol) {
7388
const baseConstraint = context.checker.getBaseConstraintOfType(type);

test/unit/builtins/array.spec.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -700,3 +700,15 @@ test.each([
700700
])("trailing undefined or null are allowed in array literal (%p)", literal => {
701701
util.testExpression(literal).expectToHaveNoDiagnostics();
702702
});
703+
704+
// Issue #1218: https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1218
705+
test.each(["[1, 2, 3]", "undefined"])("prototype call on nullable array (%p)", value => {
706+
util.testFunction`
707+
function find(arr?: number[]) {
708+
return arr?.indexOf(2);
709+
}
710+
return find(${value});
711+
`
712+
.setOptions({ strictNullChecks: true })
713+
.expectToMatchJsResult();
714+
});

test/unit/builtins/numbers.spec.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,3 +143,15 @@ test.each([
143143
])("parseInt with base and trailing text (%p)", ({ numberString, base }) => {
144144
util.testExpression`parseInt("${numberString}", ${base})`.expectToMatchJsResult();
145145
});
146+
147+
// Issue #1218: https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1218
148+
test.each(["42", "undefined"])("prototype call on nullable number (%p)", value => {
149+
util.testFunction`
150+
function toString(n?: number) {
151+
return n?.toString();
152+
}
153+
return toString(${value});
154+
`
155+
.setOptions({ strictNullChecks: true })
156+
.expectToMatchJsResult();
157+
});

test/unit/builtins/string.spec.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,3 +367,30 @@ test("string intersected method", () => {
367367
return ({ abc: () => "a" } as Vector).abc();
368368
`.expectToMatchJsResult();
369369
});
370+
371+
// Issue #1218: https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1218
372+
test.each(['"foo"', "undefined"])("prototype call on nullable string (%p)", value => {
373+
util.testFunction`
374+
function toUpper(str?: string) {
375+
return str?.toUpperCase();
376+
}
377+
return toUpper(${value});
378+
`
379+
.setOptions({ strictNullChecks: true })
380+
.expectToMatchJsResult();
381+
});
382+
383+
// Issue #1218: https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1218
384+
test.each(["string | undefined", "string | null", "null | string", "null | undefined | string"])(
385+
"prototype call on nullable string type (%p)",
386+
type => {
387+
util.testFunction`
388+
function toUpper(str: ${type}) {
389+
return str?.toUpperCase();
390+
}
391+
return toUpper("foo");
392+
`
393+
.setOptions({ strictNullChecks: true })
394+
.expectToMatchJsResult();
395+
}
396+
);

test/unit/functions/functions.spec.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,18 @@ test("function apply without arguments should not lead to exception", () => {
181181
`.expectToMatchJsResult();
182182
});
183183

184+
// Issue #1218: https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1218
185+
test.each(["() => 4", "undefined"])("prototype call on nullable function (%p)", value => {
186+
util.testFunction`
187+
function call(f?: () => number) {
188+
return f?.apply(3);
189+
}
190+
return call(${value});
191+
`
192+
.setOptions({ strictNullChecks: true })
193+
.expectToMatchJsResult();
194+
});
195+
184196
test("Function call", () => {
185197
util.testFunction`
186198
const abc = function (this: { a: number }, a: string) { return this.a + a; }

0 commit comments

Comments
 (0)
X Tutup