From 1541599ea0417ef062c7734c113302d081042a71 Mon Sep 17 00:00:00 2001 From: Nathan Shively-Sanders <293473+sandersn@users.noreply.github.com> Date: Thu, 26 Apr 2018 08:14:22 -0700 Subject: [PATCH] Check base type for special property declarations (#23671) If the base type has a property by that name, add it to the list constructor types to make it as authoritative as special assignments found in the constructor. --- src/compiler/checker.ts | 25 ++++++++++- ...erringClassMembersFromAssignments3.symbols | 25 +++++++++++ ...nferringClassMembersFromAssignments3.types | 29 +++++++++++++ ...erringClassMembersFromAssignments4.symbols | 28 +++++++++++++ ...nferringClassMembersFromAssignments4.types | 32 ++++++++++++++ ...erringClassMembersFromAssignments5.symbols | 37 ++++++++++++++++ ...nferringClassMembersFromAssignments5.types | 42 +++++++++++++++++++ .../inferringClassMembersFromAssignments3.ts | 16 +++++++ .../inferringClassMembersFromAssignments4.ts | 17 ++++++++ .../inferringClassMembersFromAssignments5.ts | 21 ++++++++++ 10 files changed, 270 insertions(+), 2 deletions(-) create mode 100644 tests/baselines/reference/inferringClassMembersFromAssignments3.symbols create mode 100644 tests/baselines/reference/inferringClassMembersFromAssignments3.types create mode 100644 tests/baselines/reference/inferringClassMembersFromAssignments4.symbols create mode 100644 tests/baselines/reference/inferringClassMembersFromAssignments4.types create mode 100644 tests/baselines/reference/inferringClassMembersFromAssignments5.symbols create mode 100644 tests/baselines/reference/inferringClassMembersFromAssignments5.types create mode 100644 tests/cases/conformance/salsa/inferringClassMembersFromAssignments3.ts create mode 100644 tests/cases/conformance/salsa/inferringClassMembersFromAssignments4.ts create mode 100644 tests/cases/conformance/salsa/inferringClassMembersFromAssignments5.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 2c4999e516f..fa485b68a92 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -4430,7 +4430,7 @@ namespace ts { const special = getSpecialPropertyAssignmentKind(expression); if (special === SpecialPropertyAssignmentKind.ThisProperty) { const thisContainer = getThisContainer(expression, /*includeArrowFunctions*/ false); - // Properties defined in a constructor (or javascript constructor function) don't get undefined added. + // Properties defined in a constructor (or base constructor, or javascript constructor function) don't get undefined added. // Function expressions that are assigned to the prototype count as methods. declarationInConstructor = thisContainer.kind === SyntaxKind.Constructor || thisContainer.kind === SyntaxKind.FunctionDeclaration || @@ -4500,7 +4500,14 @@ namespace ts { } let type = jsDocType; if (!type) { - // use only the constructor types unless only null | undefined (including widening variants) were assigned there + // use only the constructor types unless they were only assigned null | undefined (including widening variants) + if (definedInMethod) { + const propType = getTypeOfSpecialPropertyOfBaseType(symbol); + if (propType) { + (constructorTypes || (constructorTypes = [])).push(propType); + definedInConstructor = true; + } + } const sourceTypes = some(constructorTypes, t => !!(t.flags & ~(TypeFlags.Nullable | TypeFlags.ContainsWideningType))) ? constructorTypes : types; type = getUnionType(sourceTypes, UnionReduction.Subtype); } @@ -4514,6 +4521,20 @@ namespace ts { return widened; } + /** check for definition in base class if any declaration is in a class */ + function getTypeOfSpecialPropertyOfBaseType(specialProperty: Symbol) { + const parentDeclaration = forEach(specialProperty.declarations, d => { + const parent = getThisContainer(d, /*includeArrowFunctions*/ false).parent; + return isClassLike(parent) && parent; + }); + if (parentDeclaration) { + const classType = getDeclaredTypeOfSymbol(getSymbolOfNode(parentDeclaration)) as InterfaceType; + const baseClassType = classType && getBaseTypes(classType)[0]; + if (baseClassType) { + return getTypeOfPropertyOfType(baseClassType, specialProperty.escapedName); + } + } + } // Return the type implied by a binding pattern element. This is the type of the initializer of the element if // one is present. Otherwise, if the element is itself a binding pattern, it is the type implied by the binding diff --git a/tests/baselines/reference/inferringClassMembersFromAssignments3.symbols b/tests/baselines/reference/inferringClassMembersFromAssignments3.symbols new file mode 100644 index 00000000000..b4006600dbe --- /dev/null +++ b/tests/baselines/reference/inferringClassMembersFromAssignments3.symbols @@ -0,0 +1,25 @@ +=== tests/cases/conformance/salsa/a.js === +class Base { +>Base : Symbol(Base, Decl(a.js, 0, 0)) + + constructor() { + this.p = 1 +>this.p : Symbol(Base.p, Decl(a.js, 1, 19)) +>this : Symbol(Base, Decl(a.js, 0, 0)) +>p : Symbol(Base.p, Decl(a.js, 1, 19)) + } +} +class Derived extends Base { +>Derived : Symbol(Derived, Decl(a.js, 4, 1)) +>Base : Symbol(Base, Decl(a.js, 0, 0)) + + m() { +>m : Symbol(Derived.m, Decl(a.js, 5, 28)) + + this.p = 1 +>this.p : Symbol(Derived.p, Decl(a.js, 6, 9)) +>this : Symbol(Derived, Decl(a.js, 4, 1)) +>p : Symbol(Derived.p, Decl(a.js, 6, 9)) + } +} + diff --git a/tests/baselines/reference/inferringClassMembersFromAssignments3.types b/tests/baselines/reference/inferringClassMembersFromAssignments3.types new file mode 100644 index 00000000000..60d545b2832 --- /dev/null +++ b/tests/baselines/reference/inferringClassMembersFromAssignments3.types @@ -0,0 +1,29 @@ +=== tests/cases/conformance/salsa/a.js === +class Base { +>Base : Base + + constructor() { + this.p = 1 +>this.p = 1 : 1 +>this.p : number +>this : this +>p : number +>1 : 1 + } +} +class Derived extends Base { +>Derived : Derived +>Base : Base + + m() { +>m : () => void + + this.p = 1 +>this.p = 1 : 1 +>this.p : number +>this : this +>p : number +>1 : 1 + } +} + diff --git a/tests/baselines/reference/inferringClassMembersFromAssignments4.symbols b/tests/baselines/reference/inferringClassMembersFromAssignments4.symbols new file mode 100644 index 00000000000..d0d438c3ed3 --- /dev/null +++ b/tests/baselines/reference/inferringClassMembersFromAssignments4.symbols @@ -0,0 +1,28 @@ +=== tests/cases/conformance/salsa/a.js === +class Base { +>Base : Symbol(Base, Decl(a.js, 0, 0)) + + m() { +>m : Symbol(Base.m, Decl(a.js, 0, 12)) + + this.p = 1 +>this.p : Symbol(Base.p, Decl(a.js, 1, 9)) +>this : Symbol(Base, Decl(a.js, 0, 0)) +>p : Symbol(Base.p, Decl(a.js, 1, 9)) + } +} +class Derived extends Base { +>Derived : Symbol(Derived, Decl(a.js, 4, 1)) +>Base : Symbol(Base, Decl(a.js, 0, 0)) + + m() { +>m : Symbol(Derived.m, Decl(a.js, 5, 28)) + + // should be OK, and p should have type number | undefined from its base + this.p = 1 +>this.p : Symbol(Derived.p, Decl(a.js, 6, 9)) +>this : Symbol(Derived, Decl(a.js, 4, 1)) +>p : Symbol(Derived.p, Decl(a.js, 6, 9)) + } +} + diff --git a/tests/baselines/reference/inferringClassMembersFromAssignments4.types b/tests/baselines/reference/inferringClassMembersFromAssignments4.types new file mode 100644 index 00000000000..f20602d188e --- /dev/null +++ b/tests/baselines/reference/inferringClassMembersFromAssignments4.types @@ -0,0 +1,32 @@ +=== tests/cases/conformance/salsa/a.js === +class Base { +>Base : Base + + m() { +>m : () => void + + this.p = 1 +>this.p = 1 : 1 +>this.p : number | undefined +>this : this +>p : number | undefined +>1 : 1 + } +} +class Derived extends Base { +>Derived : Derived +>Base : Base + + m() { +>m : () => void + + // should be OK, and p should have type number | undefined from its base + this.p = 1 +>this.p = 1 : 1 +>this.p : number | undefined +>this : this +>p : number | undefined +>1 : 1 + } +} + diff --git a/tests/baselines/reference/inferringClassMembersFromAssignments5.symbols b/tests/baselines/reference/inferringClassMembersFromAssignments5.symbols new file mode 100644 index 00000000000..5c46300d770 --- /dev/null +++ b/tests/baselines/reference/inferringClassMembersFromAssignments5.symbols @@ -0,0 +1,37 @@ +=== tests/cases/conformance/salsa/a.js === +class Base { +>Base : Symbol(Base, Decl(a.js, 0, 0)) + + m() { +>m : Symbol(Base.m, Decl(a.js, 0, 12)) + + this.p = 1 +>this.p : Symbol(Base.p, Decl(a.js, 1, 9)) +>this : Symbol(Base, Decl(a.js, 0, 0)) +>p : Symbol(Base.p, Decl(a.js, 1, 9)) + } +} +class Derived extends Base { +>Derived : Symbol(Derived, Decl(a.js, 4, 1)) +>Base : Symbol(Base, Decl(a.js, 0, 0)) + + constructor() { + super(); +>super : Symbol(Base, Decl(a.js, 0, 0)) + + // should be OK, and p should have type number from this assignment + this.p = 1 +>this.p : Symbol(Derived.p, Decl(a.js, 7, 16)) +>this : Symbol(Derived, Decl(a.js, 4, 1)) +>p : Symbol(Derived.p, Decl(a.js, 7, 16)) + } + test() { +>test : Symbol(Derived.test, Decl(a.js, 10, 5)) + + return this.p +>this.p : Symbol(Derived.p, Decl(a.js, 7, 16)) +>this : Symbol(Derived, Decl(a.js, 4, 1)) +>p : Symbol(Derived.p, Decl(a.js, 7, 16)) + } +} + diff --git a/tests/baselines/reference/inferringClassMembersFromAssignments5.types b/tests/baselines/reference/inferringClassMembersFromAssignments5.types new file mode 100644 index 00000000000..fc95b82e5fd --- /dev/null +++ b/tests/baselines/reference/inferringClassMembersFromAssignments5.types @@ -0,0 +1,42 @@ +=== tests/cases/conformance/salsa/a.js === +class Base { +>Base : Base + + m() { +>m : () => void + + this.p = 1 +>this.p = 1 : 1 +>this.p : number | undefined +>this : this +>p : number | undefined +>1 : 1 + } +} +class Derived extends Base { +>Derived : Derived +>Base : Base + + constructor() { + super(); +>super() : void +>super : typeof Base + + // should be OK, and p should have type number from this assignment + this.p = 1 +>this.p = 1 : 1 +>this.p : number +>this : this +>p : number +>1 : 1 + } + test() { +>test : () => number + + return this.p +>this.p : number +>this : this +>p : number + } +} + diff --git a/tests/cases/conformance/salsa/inferringClassMembersFromAssignments3.ts b/tests/cases/conformance/salsa/inferringClassMembersFromAssignments3.ts new file mode 100644 index 00000000000..800bd368dba --- /dev/null +++ b/tests/cases/conformance/salsa/inferringClassMembersFromAssignments3.ts @@ -0,0 +1,16 @@ +// @noEmit: true +// @allowJs: true +// @checkJs: true +// @noImplicitAny: true +// @strictNullChecks: true +// @Filename: a.js +class Base { + constructor() { + this.p = 1 + } +} +class Derived extends Base { + m() { + this.p = 1 + } +} diff --git a/tests/cases/conformance/salsa/inferringClassMembersFromAssignments4.ts b/tests/cases/conformance/salsa/inferringClassMembersFromAssignments4.ts new file mode 100644 index 00000000000..ba443bf8595 --- /dev/null +++ b/tests/cases/conformance/salsa/inferringClassMembersFromAssignments4.ts @@ -0,0 +1,17 @@ +// @noEmit: true +// @allowJs: true +// @checkJs: true +// @noImplicitAny: true +// @strictNullChecks: true +// @Filename: a.js +class Base { + m() { + this.p = 1 + } +} +class Derived extends Base { + m() { + // should be OK, and p should have type number | undefined from its base + this.p = 1 + } +} diff --git a/tests/cases/conformance/salsa/inferringClassMembersFromAssignments5.ts b/tests/cases/conformance/salsa/inferringClassMembersFromAssignments5.ts new file mode 100644 index 00000000000..5044ea396eb --- /dev/null +++ b/tests/cases/conformance/salsa/inferringClassMembersFromAssignments5.ts @@ -0,0 +1,21 @@ +// @noEmit: true +// @allowJs: true +// @checkJs: true +// @noImplicitAny: true +// @strictNullChecks: true +// @Filename: a.js +class Base { + m() { + this.p = 1 + } +} +class Derived extends Base { + constructor() { + super(); + // should be OK, and p should have type number from this assignment + this.p = 1 + } + test() { + return this.p + } +}