Skip to content

Correctly track thisContainer for this-property-assignments in JS nested containers #22779

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Mar 22, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 14 additions & 11 deletions src/compiler/binder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ namespace ts {
let languageVersion: ScriptTarget;
let parent: Node;
let container: Node;
let containerContainer: Node; // Container one level up
let thisParentContainer: Node; // Container one level up
let blockScopeContainer: Node;
let lastContainer: Node;
let seenThisKeyword: boolean;
Expand Down Expand Up @@ -186,7 +186,7 @@ namespace ts {
languageVersion = undefined;
parent = undefined;
container = undefined;
containerContainer = undefined;
thisParentContainer = undefined;
blockScopeContainer = undefined;
lastContainer = undefined;
seenThisKeyword = false;
Expand Down Expand Up @@ -479,11 +479,9 @@ namespace ts {
// and block-container. Then after we pop out of processing the children, we restore
// these saved values.
const saveContainer = container;
const saveContainerContainer = containerContainer;
const saveThisParentContainer = thisParentContainer;
const savedBlockScopeContainer = blockScopeContainer;

containerContainer = container;

// Depending on what kind of node this is, we may have to adjust the current container
// and block-container. If the current node is a container, then it is automatically
// considered the current block-container as well. Also, for containers that we know
Expand All @@ -502,6 +500,9 @@ namespace ts {
// for it. We must clear this so we don't accidentally move any stale data forward from
// a previous compilation.
if (containerFlags & ContainerFlags.IsContainer) {
if (node.kind !== SyntaxKind.ArrowFunction) {
thisParentContainer = container;
}
container = blockScopeContainer = node;
if (containerFlags & ContainerFlags.HasLocals) {
container.locals = createSymbolTable();
Expand Down Expand Up @@ -571,7 +572,7 @@ namespace ts {
}

container = saveContainer;
containerContainer = saveContainerContainer;
thisParentContainer = saveThisParentContainer;
blockScopeContainer = savedBlockScopeContainer;
}

Expand Down Expand Up @@ -2338,14 +2339,16 @@ namespace ts {
if (isBinaryExpression(thisContainer.parent) && thisContainer.parent.operatorToken.kind === SyntaxKind.EqualsToken) {
const l = thisContainer.parent.left;
if (isPropertyAccessEntityNameExpression(l) && isPrototypeAccess(l.expression)) {
constructorSymbol = getJSInitializerSymbolFromName(l.expression.expression, containerContainer);
constructorSymbol = getJSInitializerSymbolFromName(l.expression.expression, thisParentContainer);
}
}

// Declare a 'member' if the container is an ES5 class or ES6 constructor
constructorSymbol.members = constructorSymbol.members || createSymbolTable();
// It's acceptable for multiple 'this' assignments of the same identifier to occur
declareSymbol(constructorSymbol.members, constructorSymbol, node, SymbolFlags.Property, SymbolFlags.PropertyExcludes & ~SymbolFlags.Property);
if (constructorSymbol) {
// Declare a 'member' if the container is an ES5 class or ES6 constructor
constructorSymbol.members = constructorSymbol.members || createSymbolTable();
// It's acceptable for multiple 'this' assignments of the same identifier to occur
declareSymbol(constructorSymbol.members, constructorSymbol, node, SymbolFlags.Property, SymbolFlags.PropertyExcludes & ~SymbolFlags.Property);
}
break;

case SyntaxKind.Constructor:
Expand Down
35 changes: 35 additions & 0 deletions tests/baselines/reference/typeFromPropertyAssignment20.symbols
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
=== tests/cases/conformance/salsa/bluebird.js ===
!function outer (f) { return f }(
>outer : Symbol(outer, Decl(bluebird.js, 0, 1))
>f : Symbol(f, Decl(bluebird.js, 0, 17))
>f : Symbol(f, Decl(bluebird.js, 0, 17))

function inner () {
>inner : Symbol(inner, Decl(bluebird.js, 0, 33))

function Async() {
>Async : Symbol(Async, Decl(bluebird.js, 1, 23))

this._trampolineEnabled = true;
>_trampolineEnabled : Symbol(Async._trampolineEnabled, Decl(bluebird.js, 2, 26), Decl(bluebird.js, 7, 20))
}

Async.prototype.disableTrampolineIfNecessary = function dtin(b) {
>Async.prototype : Symbol(Async.disableTrampolineIfNecessary, Decl(bluebird.js, 4, 9))
>Async : Symbol(Async, Decl(bluebird.js, 1, 23))
>prototype : Symbol(Function.prototype, Decl(lib.d.ts, --, --))
>disableTrampolineIfNecessary : Symbol(Async.disableTrampolineIfNecessary, Decl(bluebird.js, 4, 9))
>dtin : Symbol(dtin, Decl(bluebird.js, 6, 54))
>b : Symbol(b, Decl(bluebird.js, 6, 69))

if (b) {
>b : Symbol(b, Decl(bluebird.js, 6, 69))

this._trampolineEnabled = false;
>this._trampolineEnabled : Symbol(Async._trampolineEnabled, Decl(bluebird.js, 2, 26), Decl(bluebird.js, 7, 20))
>this : Symbol(Async, Decl(bluebird.js, 1, 23))
>_trampolineEnabled : Symbol(Async._trampolineEnabled, Decl(bluebird.js, 2, 26), Decl(bluebird.js, 7, 20))
}
};
})

48 changes: 48 additions & 0 deletions tests/baselines/reference/typeFromPropertyAssignment20.types
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
=== tests/cases/conformance/salsa/bluebird.js ===
!function outer (f) { return f }(
>!function outer (f) { return f }( function inner () { function Async() { this._trampolineEnabled = true; } Async.prototype.disableTrampolineIfNecessary = function dtin(b) { if (b) { this._trampolineEnabled = false; } }; }) : boolean
>function outer (f) { return f }( function inner () { function Async() { this._trampolineEnabled = true; } Async.prototype.disableTrampolineIfNecessary = function dtin(b) { if (b) { this._trampolineEnabled = false; } }; }) : () => void
>function outer (f) { return f } : (f: () => void) => () => void
>outer : (f: () => void) => () => void
>f : () => void
>f : () => void

function inner () {
>function inner () { function Async() { this._trampolineEnabled = true; } Async.prototype.disableTrampolineIfNecessary = function dtin(b) { if (b) { this._trampolineEnabled = false; } }; } : () => void
>inner : () => void

function Async() {
>Async : () => void

this._trampolineEnabled = true;
>this._trampolineEnabled = true : true
>this._trampolineEnabled : any
>this : any
>_trampolineEnabled : any
>true : true
}

Async.prototype.disableTrampolineIfNecessary = function dtin(b) {
>Async.prototype.disableTrampolineIfNecessary = function dtin(b) { if (b) { this._trampolineEnabled = false; } } : (b: any) => void
>Async.prototype.disableTrampolineIfNecessary : any
>Async.prototype : any
>Async : () => void
>prototype : any
>disableTrampolineIfNecessary : any
>function dtin(b) { if (b) { this._trampolineEnabled = false; } } : (b: any) => void
>dtin : (b: any) => void
>b : any

if (b) {
>b : any

this._trampolineEnabled = false;
>this._trampolineEnabled = false : false
>this._trampolineEnabled : boolean
>this : { _trampolineEnabled: boolean; disableTrampolineIfNecessary: (b: any) => void; }
>_trampolineEnabled : boolean
>false : false
}
};
})

14 changes: 14 additions & 0 deletions tests/baselines/reference/typeFromPropertyAssignment21.errors.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
tests/cases/conformance/salsa/chrome-devtools-DOMExtension.js(2,17): error TS2339: Property 'removeChildren' does not exist on type 'Event'.
tests/cases/conformance/salsa/chrome-devtools-DOMExtension.js(3,10): error TS2339: Property 'textContent' does not exist on type 'Event'.


==== tests/cases/conformance/salsa/chrome-devtools-DOMExtension.js (2 errors) ====
// Extend that DOM! (doesn't work, but shouldn't crash)
Event.prototype.removeChildren = function () {
~~~~~~~~~~~~~~
!!! error TS2339: Property 'removeChildren' does not exist on type 'Event'.
this.textContent = 'nope, not going to happen'
~~~~~~~~~~~
!!! error TS2339: Property 'textContent' does not exist on type 'Event'.
}

11 changes: 11 additions & 0 deletions tests/baselines/reference/typeFromPropertyAssignment21.symbols
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
=== tests/cases/conformance/salsa/chrome-devtools-DOMExtension.js ===
// Extend that DOM! (doesn't work, but shouldn't crash)
Event.prototype.removeChildren = function () {
>Event.prototype : Symbol(prototype, Decl(lib.dom.d.ts, --, --))
>Event : Symbol(Event, Decl(lib.dom.d.ts, --, --), Decl(lib.dom.d.ts, --, --))
>prototype : Symbol(prototype, Decl(lib.dom.d.ts, --, --))

this.textContent = 'nope, not going to happen'
>this : Symbol(Event, Decl(lib.dom.d.ts, --, --), Decl(lib.dom.d.ts, --, --))
}

19 changes: 19 additions & 0 deletions tests/baselines/reference/typeFromPropertyAssignment21.types
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
=== tests/cases/conformance/salsa/chrome-devtools-DOMExtension.js ===
// Extend that DOM! (doesn't work, but shouldn't crash)
Event.prototype.removeChildren = function () {
>Event.prototype.removeChildren = function () { this.textContent = 'nope, not going to happen'} : () => void
>Event.prototype.removeChildren : any
>Event.prototype : Event
>Event : { new (typeArg: string, eventInitDict?: EventInit): Event; prototype: Event; readonly AT_TARGET: number; readonly BUBBLING_PHASE: number; readonly CAPTURING_PHASE: number; readonly NONE: number; }
>prototype : Event
>removeChildren : any
>function () { this.textContent = 'nope, not going to happen'} : () => void

this.textContent = 'nope, not going to happen'
>this.textContent = 'nope, not going to happen' : "nope, not going to happen"
>this.textContent : any
>this : Event
>textContent : any
>'nope, not going to happen' : "nope, not going to happen"
}

21 changes: 21 additions & 0 deletions tests/baselines/reference/typeFromPropertyAssignment22.errors.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
tests/cases/conformance/salsa/npm-install.js(12,1): error TS2322: Type 'string | number' is not assignable to type 'number | undefined'.
Type 'string' is not assignable to type 'number | undefined'.


==== tests/cases/conformance/salsa/npm-install.js (1 errors) ====
function Installer () {
this.args = 0
}
Installer.prototype.loadArgMetadata = function (next) {
// ArrowFunction isn't treated as a this-container
(args) => {
this.args = 'hi'
this.newProperty = 1
}
}
var i = new Installer()
i.newProperty = i.args // error, number | string =/> number | undefined
~~~~~~~~~~~~~
!!! error TS2322: Type 'string | number' is not assignable to type 'number | undefined'.
!!! error TS2322: Type 'string' is not assignable to type 'number | undefined'.

41 changes: 41 additions & 0 deletions tests/baselines/reference/typeFromPropertyAssignment22.symbols
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
=== tests/cases/conformance/salsa/npm-install.js ===
function Installer () {
>Installer : Symbol(Installer, Decl(npm-install.js, 0, 0))

this.args = 0
>args : Symbol(Installer.args, Decl(npm-install.js, 0, 23), Decl(npm-install.js, 5, 15))
}
Installer.prototype.loadArgMetadata = function (next) {
>Installer.prototype : Symbol(Installer.loadArgMetadata, Decl(npm-install.js, 2, 1))
>Installer : Symbol(Installer, Decl(npm-install.js, 0, 0))
>prototype : Symbol(Function.prototype, Decl(lib.d.ts, --, --))
>loadArgMetadata : Symbol(Installer.loadArgMetadata, Decl(npm-install.js, 2, 1))
>next : Symbol(next, Decl(npm-install.js, 3, 48))

// ArrowFunction isn't treated as a this-container
(args) => {
>args : Symbol(args, Decl(npm-install.js, 5, 5))

this.args = 'hi'
>this.args : Symbol(Installer.args, Decl(npm-install.js, 0, 23), Decl(npm-install.js, 5, 15))
>this : Symbol(Installer, Decl(npm-install.js, 0, 0))
>args : Symbol(Installer.args, Decl(npm-install.js, 0, 23), Decl(npm-install.js, 5, 15))

this.newProperty = 1
>this.newProperty : Symbol(Installer.newProperty, Decl(npm-install.js, 6, 24))
>this : Symbol(Installer, Decl(npm-install.js, 0, 0))
>newProperty : Symbol(Installer.newProperty, Decl(npm-install.js, 6, 24))
}
}
var i = new Installer()
>i : Symbol(i, Decl(npm-install.js, 10, 3))
>Installer : Symbol(Installer, Decl(npm-install.js, 0, 0))

i.newProperty = i.args // error, number | string =/> number | undefined
>i.newProperty : Symbol(Installer.newProperty, Decl(npm-install.js, 6, 24))
>i : Symbol(i, Decl(npm-install.js, 10, 3))
>newProperty : Symbol(Installer.newProperty, Decl(npm-install.js, 6, 24))
>i.args : Symbol(Installer.args, Decl(npm-install.js, 0, 23), Decl(npm-install.js, 5, 15))
>i : Symbol(i, Decl(npm-install.js, 10, 3))
>args : Symbol(Installer.args, Decl(npm-install.js, 0, 23), Decl(npm-install.js, 5, 15))

55 changes: 55 additions & 0 deletions tests/baselines/reference/typeFromPropertyAssignment22.types
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
=== tests/cases/conformance/salsa/npm-install.js ===
function Installer () {
>Installer : () => void

this.args = 0
>this.args = 0 : 0
>this.args : any
>this : any
>args : any
>0 : 0
}
Installer.prototype.loadArgMetadata = function (next) {
>Installer.prototype.loadArgMetadata = function (next) { // ArrowFunction isn't treated as a this-container (args) => { this.args = 'hi' this.newProperty = 1 }} : (next: any) => void
>Installer.prototype.loadArgMetadata : any
>Installer.prototype : any
>Installer : () => void
>prototype : any
>loadArgMetadata : any
>function (next) { // ArrowFunction isn't treated as a this-container (args) => { this.args = 'hi' this.newProperty = 1 }} : (next: any) => void
>next : any

// ArrowFunction isn't treated as a this-container
(args) => {
>(args) => { this.args = 'hi' this.newProperty = 1 } : (args: any) => void
>args : any

this.args = 'hi'
>this.args = 'hi' : "hi"
>this.args : string | number
>this : { args: string | number; loadArgMetadata: (next: any) => void; newProperty: number | undefined; }
>args : string | number
>'hi' : "hi"

this.newProperty = 1
>this.newProperty = 1 : 1
>this.newProperty : number | undefined
>this : { args: string | number; loadArgMetadata: (next: any) => void; newProperty: number | undefined; }
>newProperty : number | undefined
>1 : 1
}
}
var i = new Installer()
>i : { args: string | number; loadArgMetadata: (next: any) => void; newProperty: number | undefined; }
>new Installer() : { args: string | number; loadArgMetadata: (next: any) => void; newProperty: number | undefined; }
>Installer : () => void

i.newProperty = i.args // error, number | string =/> number | undefined
>i.newProperty = i.args : string | number
>i.newProperty : number | undefined
>i : { args: string | number; loadArgMetadata: (next: any) => void; newProperty: number | undefined; }
>newProperty : number | undefined
>i.args : string | number
>i : { args: string | number; loadArgMetadata: (next: any) => void; newProperty: number | undefined; }
>args : string | number

17 changes: 17 additions & 0 deletions tests/cases/conformance/salsa/typeFromPropertyAssignment20.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// @allowJs: true
// @checkJs: true
// @noEmit: true
// @Filename: bluebird.js

!function outer (f) { return f }(
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this be made simpler? Why are we applying identity and then negating it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(I'm doing some digging to find out why all these pieces appear to be required.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's a simpler repro that fails on batch compilation:

function Async() {
    this._trampolineEnabled = true;
}

Async.prototype.disableTrampolineIfNecessary = function dtin(b) {
    if (b) {
        this._trampolineEnabled = false;
    }
};

I was testing on the language service, and I can't get this repro to crash consistently like I can the more complex one -- you just have to request diagnostics for that one.

function inner () {
function Async() {
this._trampolineEnabled = true;
}

Async.prototype.disableTrampolineIfNecessary = function dtin(b) {
if (b) {
this._trampolineEnabled = false;
}
};
})
10 changes: 10 additions & 0 deletions tests/cases/conformance/salsa/typeFromPropertyAssignment21.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// @allowJs: true
// @checkJs: true
// @noEmit: true
// @lib: dom, es5
// @Filename: chrome-devtools-DOMExtension.js

// Extend that DOM! (doesn't work, but shouldn't crash)
Event.prototype.removeChildren = function () {
this.textContent = 'nope, not going to happen'
}
17 changes: 17 additions & 0 deletions tests/cases/conformance/salsa/typeFromPropertyAssignment22.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// @allowJs: true
// @checkJs: true
// @noEmit: true
// @strictNullChecks: true
// @Filename: npm-install.js
function Installer () {
this.args = 0
}
Installer.prototype.loadArgMetadata = function (next) {
// ArrowFunction isn't treated as a this-container
(args) => {
this.args = 'hi'
this.newProperty = 1
}
}
var i = new Installer()
i.newProperty = i.args // error, number | string =/> number | undefined