Js constructor function fixes (#22721)

* Do not add undefined for this assignments in functions

* Test:constructor functions with --strict

* First draft -- works, but needs a stricter check added

* Update baselines

* Make undefined-skip stricter and more efficient

Symbol-based now instead of syntactic

* Exclude prototype function assignments

* Add explanatory comment
This commit is contained in:
Nathan Shively-Sanders 2018-03-20 11:24:09 -07:00 committed by GitHub
parent ab8233c5d3
commit 1074819be3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 245 additions and 16 deletions

View File

@ -4259,7 +4259,14 @@ namespace ts {
}
if (isPropertyAccessExpression(expression.left) && expression.left.expression.kind === SyntaxKind.ThisKeyword) {
if (getThisContainer(expression, /*includeArrowFunctions*/ false).kind === SyntaxKind.Constructor) {
const thisContainer = getThisContainer(expression, /*includeArrowFunctions*/ false);
const isPrototypeProperty = isBinaryExpression(thisContainer.parent) &&
getSpecialPropertyAssignmentKind(thisContainer.parent) === SpecialPropertyAssignmentKind.PrototypeProperty;
// Properties defined in a constructor (or javascript constructor function) don't get undefined added.
// Function expressions that are assigned to the prototype count as methods.
if (thisContainer.kind === SyntaxKind.Constructor ||
thisContainer.kind === SyntaxKind.FunctionDeclaration ||
(thisContainer.kind === SyntaxKind.FunctionExpression && !isPrototypeProperty)) {
definedInConstructor = true;
}
else {
@ -18566,7 +18573,7 @@ namespace ts {
return true;
}
/** NOTE: Return value of `[]` means a different thing than `undefined`. `[]` means return `void`, `undefined` means return `never`. */
/** NOTE: Return value of `[]` means a different thing than `undefined`. `[]` means func returns `void`, `undefined` means it returns `never`. */
function checkAndAggregateReturnExpressionTypes(func: FunctionLikeDeclaration, checkMode: CheckMode): Type[] | undefined {
const functionFlags = getFunctionFlags(func);
const aggregatedTypes: Type[] = [];
@ -18595,7 +18602,9 @@ namespace ts {
if (aggregatedTypes.length === 0 && !hasReturnWithNoExpression && (hasReturnOfTypeNever || mayReturnNever(func))) {
return undefined;
}
if (strictNullChecks && aggregatedTypes.length && hasReturnWithNoExpression) {
if (strictNullChecks && aggregatedTypes.length && hasReturnWithNoExpression &&
!(isJavaScriptConstructor(func) && aggregatedTypes.some(t => t.symbol === func.symbol))) {
// Javascript "callable constructors", containing eg `if (!(this instanceof A)) return new A()` should not add undefined
pushIfUnique(aggregatedTypes, undefinedType);
}
return aggregatedTypes;

View File

@ -0,0 +1,28 @@
tests/cases/conformance/salsa/a.js(9,1): error TS2322: Type 'undefined' is not assignable to type 'number'.
==== tests/cases/conformance/salsa/a.js (1 errors) ====
/** @param {number} x */
function C(x) {
this.x = x
}
C.prototype.m = function() {
this.y = 12
}
var c = new C(1)
c.x = undefined // should error
~~~
!!! error TS2322: Type 'undefined' is not assignable to type 'number'.
c.y = undefined // ok
/** @param {number} x */
function A(x) {
if (!(this instanceof A)) {
return new A(x)
}
this.x = x
}
var k = A(1)
var j = new A(2)
k.x === j.x

View File

@ -0,0 +1,69 @@
=== tests/cases/conformance/salsa/a.js ===
/** @param {number} x */
function C(x) {
>C : Symbol(C, Decl(a.js, 0, 0))
>x : Symbol(x, Decl(a.js, 1, 11))
this.x = x
>x : Symbol(C.x, Decl(a.js, 1, 15))
>x : Symbol(x, Decl(a.js, 1, 11))
}
C.prototype.m = function() {
>C.prototype : Symbol(C.m, Decl(a.js, 3, 1))
>C : Symbol(C, Decl(a.js, 0, 0))
>prototype : Symbol(Function.prototype, Decl(lib.d.ts, --, --))
>m : Symbol(C.m, Decl(a.js, 3, 1))
this.y = 12
>this.y : Symbol(C.y, Decl(a.js, 4, 28))
>this : Symbol(C, Decl(a.js, 0, 0))
>y : Symbol(C.y, Decl(a.js, 4, 28))
}
var c = new C(1)
>c : Symbol(c, Decl(a.js, 7, 3))
>C : Symbol(C, Decl(a.js, 0, 0))
c.x = undefined // should error
>c.x : Symbol(C.x, Decl(a.js, 1, 15))
>c : Symbol(c, Decl(a.js, 7, 3))
>x : Symbol(C.x, Decl(a.js, 1, 15))
>undefined : Symbol(undefined)
c.y = undefined // ok
>c.y : Symbol(C.y, Decl(a.js, 4, 28))
>c : Symbol(c, Decl(a.js, 7, 3))
>y : Symbol(C.y, Decl(a.js, 4, 28))
>undefined : Symbol(undefined)
/** @param {number} x */
function A(x) {
>A : Symbol(A, Decl(a.js, 9, 15))
>x : Symbol(x, Decl(a.js, 12, 11))
if (!(this instanceof A)) {
>A : Symbol(A, Decl(a.js, 9, 15))
return new A(x)
>A : Symbol(A, Decl(a.js, 9, 15))
>x : Symbol(x, Decl(a.js, 12, 11))
}
this.x = x
>x : Symbol(A.x, Decl(a.js, 15, 5))
>x : Symbol(x, Decl(a.js, 12, 11))
}
var k = A(1)
>k : Symbol(k, Decl(a.js, 18, 3))
>A : Symbol(A, Decl(a.js, 9, 15))
var j = new A(2)
>j : Symbol(j, Decl(a.js, 19, 3))
>A : Symbol(A, Decl(a.js, 9, 15))
k.x === j.x
>k.x : Symbol(A.x, Decl(a.js, 15, 5))
>k : Symbol(k, Decl(a.js, 18, 3))
>x : Symbol(A.x, Decl(a.js, 15, 5))
>j.x : Symbol(A.x, Decl(a.js, 15, 5))
>j : Symbol(j, Decl(a.js, 19, 3))
>x : Symbol(A.x, Decl(a.js, 15, 5))

View File

@ -0,0 +1,94 @@
=== tests/cases/conformance/salsa/a.js ===
/** @param {number} x */
function C(x) {
>C : (x: number) => void
>x : number
this.x = x
>this.x = x : number
>this.x : any
>this : any
>x : any
>x : number
}
C.prototype.m = function() {
>C.prototype.m = function() { this.y = 12} : () => void
>C.prototype.m : any
>C.prototype : any
>C : (x: number) => void
>prototype : any
>m : any
>function() { this.y = 12} : () => void
this.y = 12
>this.y = 12 : 12
>this.y : number | undefined
>this : { x: number; m: () => void; y: number | undefined; }
>y : number | undefined
>12 : 12
}
var c = new C(1)
>c : { x: number; m: () => void; y: number | undefined; }
>new C(1) : { x: number; m: () => void; y: number | undefined; }
>C : (x: number) => void
>1 : 1
c.x = undefined // should error
>c.x = undefined : undefined
>c.x : number
>c : { x: number; m: () => void; y: number | undefined; }
>x : number
>undefined : undefined
c.y = undefined // ok
>c.y = undefined : undefined
>c.y : number | undefined
>c : { x: number; m: () => void; y: number | undefined; }
>y : number | undefined
>undefined : undefined
/** @param {number} x */
function A(x) {
>A : (x: number) => typeof A
>x : number
if (!(this instanceof A)) {
>!(this instanceof A) : boolean
>(this instanceof A) : boolean
>this instanceof A : boolean
>this : any
>A : (x: number) => typeof A
return new A(x)
>new A(x) : { x: number; }
>A : (x: number) => typeof A
>x : number
}
this.x = x
>this.x = x : number
>this.x : any
>this : any
>x : any
>x : number
}
var k = A(1)
>k : { x: number; }
>A(1) : { x: number; }
>A : (x: number) => typeof A
>1 : 1
var j = new A(2)
>j : { x: number; }
>new A(2) : { x: number; }
>A : (x: number) => typeof A
>2 : 2
k.x === j.x
>k.x === j.x : boolean
>k.x : number
>k : { x: number; }
>x : number
>j.x : number
>j : { x: number; }
>x : number

View File

@ -7,10 +7,10 @@ tests/cases/conformance/jsdoc/b.js(51,17): error TS2352: Type 'SomeDerived' cann
Property 'q' is missing in type 'SomeDerived'.
tests/cases/conformance/jsdoc/b.js(52,17): error TS2352: Type 'SomeBase' cannot be converted to type 'SomeOther'.
Property 'q' is missing in type 'SomeBase'.
tests/cases/conformance/jsdoc/b.js(58,1): error TS2322: Type '{ p: string | number | undefined; }' is not assignable to type 'SomeBase'.
tests/cases/conformance/jsdoc/b.js(58,1): error TS2322: Type '{ p: string | number; }' is not assignable to type 'SomeBase'.
Types of property 'p' are incompatible.
Type 'string | number | undefined' is not assignable to type 'number'.
Type 'undefined' is not assignable to type 'number'.
Type 'string | number' is not assignable to type 'number'.
Type 'string' is not assignable to type 'number'.
tests/cases/conformance/jsdoc/b.js(66,8): error TS2352: Type 'boolean' cannot be converted to type 'string | number'.
tests/cases/conformance/jsdoc/b.js(66,15): error TS2304: Cannot find name 'numOrStr'.
tests/cases/conformance/jsdoc/b.js(66,24): error TS1005: '}' expected.
@ -97,10 +97,10 @@ tests/cases/conformance/jsdoc/b.js(67,8): error TS2454: Variable 'numOrStr' is u
someBase = someFakeClass; // Error
~~~~~~~~
!!! error TS2322: Type '{ p: string | number | undefined; }' is not assignable to type 'SomeBase'.
!!! error TS2322: Type '{ p: string | number; }' is not assignable to type 'SomeBase'.
!!! error TS2322: Types of property 'p' are incompatible.
!!! error TS2322: Type 'string | number | undefined' is not assignable to type 'number'.
!!! error TS2322: Type 'undefined' is not assignable to type 'number'.
!!! error TS2322: Type 'string | number' is not assignable to type 'number'.
!!! error TS2322: Type 'string' is not assignable to type 'number'.
someBase = /** @type {SomeBase} */(someFakeClass);
// Type assertion cannot be a type-predicate type

View File

@ -108,8 +108,8 @@ var someOther = new SomeOther();
>SomeOther : typeof SomeOther
var someFakeClass = new SomeFakeClass();
>someFakeClass : { p: string | number | undefined; }
>new SomeFakeClass() : { p: string | number | undefined; }
>someFakeClass : { p: string | number; }
>new SomeFakeClass() : { p: string | number; }
>SomeFakeClass : () => void
someBase = /** @type {SomeBase} */(someDerived);
@ -168,24 +168,24 @@ someOther = /** @type {SomeOther} */(someOther);
someFakeClass = someBase;
>someFakeClass = someBase : SomeBase
>someFakeClass : { p: string | number | undefined; }
>someFakeClass : { p: string | number; }
>someBase : SomeBase
someFakeClass = someDerived;
>someFakeClass = someDerived : SomeDerived
>someFakeClass : { p: string | number | undefined; }
>someFakeClass : { p: string | number; }
>someDerived : SomeDerived
someBase = someFakeClass; // Error
>someBase = someFakeClass : { p: string | number | undefined; }
>someBase = someFakeClass : { p: string | number; }
>someBase : SomeBase
>someFakeClass : { p: string | number | undefined; }
>someFakeClass : { p: string | number; }
someBase = /** @type {SomeBase} */(someFakeClass);
>someBase = /** @type {SomeBase} */(someFakeClass) : SomeBase
>someBase : SomeBase
>(someFakeClass) : SomeBase
>someFakeClass : { p: string | number | undefined; }
>someFakeClass : { p: string | number; }
// Type assertion cannot be a type-predicate type
/** @type {number | string} */

View File

@ -0,0 +1,29 @@
// @noEmit: true
// @allowJs: true
// @checkJs: true
// @strict: true
// @noImplicitThis: false
// @Filename: a.js
/** @param {number} x */
function C(x) {
this.x = x
}
C.prototype.m = function() {
this.y = 12
}
var c = new C(1)
c.x = undefined // should error
c.y = undefined // ok
/** @param {number} x */
function A(x) {
if (!(this instanceof A)) {
return new A(x)
}
this.x = x
}
var k = A(1)
var j = new A(2)
k.x === j.x