Skip to content

Allow JS multiple declarations of ctor properties #10123

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
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
89 changes: 47 additions & 42 deletions src/compiler/binder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -299,8 +299,10 @@ namespace ts {
const name = isDefaultExport && parent ? "default" : getDeclarationName(node);

let symbol: Symbol;
if (name !== undefined) {

if (name === undefined) {
symbol = createSymbol(SymbolFlags.None, "__missing");
}
else {
// Check and see if the symbol table already has a symbol with this name. If not,
// create a new symbol with this name and add it to the table. Note that we don't
// give the new symbol any flags *yet*. This ensures that it will not conflict
Expand All @@ -312,6 +314,11 @@ namespace ts {
// declaration we have for this symbol, and then create a new symbol for this
// declaration.
//
// Note that when properties declared in Javascript constructors
// (marked by isReplaceableByMethod) conflict with another symbol, the property loses.
// Always. This allows the common Javascript pattern of overwriting a prototype method
// with an bound instance method of the same type: `this.method = this.method.bind(this)`
//
// If we created a new symbol, either because we didn't have a symbol with this name
// in the symbol table, or we conflicted with an existing symbol, then just add this
// node as the sole declaration of the new symbol.
Expand All @@ -326,33 +333,37 @@ namespace ts {
}

if (symbol.flags & excludes) {
if (node.name) {
node.name.parent = node;
if (symbol.isReplaceableByMethod) {
// Javascript constructor-declared symbols can be discarded in favor of
// prototype symbols like methods.
symbol = symbolTable[name] = createSymbol(SymbolFlags.None, name);
}
else {
if (node.name) {
node.name.parent = node;
}

// Report errors every position with duplicate declaration
// Report errors on previous encountered declarations
let message = symbol.flags & SymbolFlags.BlockScopedVariable
? Diagnostics.Cannot_redeclare_block_scoped_variable_0
: Diagnostics.Duplicate_identifier_0;
// Report errors every position with duplicate declaration
// Report errors on previous encountered declarations
let message = symbol.flags & SymbolFlags.BlockScopedVariable
? Diagnostics.Cannot_redeclare_block_scoped_variable_0
: Diagnostics.Duplicate_identifier_0;

forEach(symbol.declarations, declaration => {
if (declaration.flags & NodeFlags.Default) {
message = Diagnostics.A_module_cannot_have_multiple_default_exports;
}
});
forEach(symbol.declarations, declaration => {
if (declaration.flags & NodeFlags.Default) {
message = Diagnostics.A_module_cannot_have_multiple_default_exports;
}
});

forEach(symbol.declarations, declaration => {
file.bindDiagnostics.push(createDiagnosticForNode(declaration.name || declaration, message, getDisplayName(declaration)));
});
file.bindDiagnostics.push(createDiagnosticForNode(node.name || node, message, getDisplayName(node)));
forEach(symbol.declarations, declaration => {
file.bindDiagnostics.push(createDiagnosticForNode(declaration.name || declaration, message, getDisplayName(declaration)));
});
file.bindDiagnostics.push(createDiagnosticForNode(node.name || node, message, getDisplayName(node)));

symbol = createSymbol(SymbolFlags.None, name);
symbol = createSymbol(SymbolFlags.None, name);
}
}
}
else {
symbol = createSymbol(SymbolFlags.None, "__missing");
}

addDeclarationToSymbol(symbol, node, includes);
symbol.parent = parent;
Expand Down Expand Up @@ -1966,31 +1977,25 @@ namespace ts {
}

function bindThisPropertyAssignment(node: BinaryExpression) {
// Declare a 'member' in case it turns out the container was an ES5 class or ES6 constructor
let assignee: Node;
Debug.assert(isInJavaScriptFile(node));
// Declare a 'member' if the container is an ES5 class or ES6 constructor
if (container.kind === SyntaxKind.FunctionDeclaration || container.kind === SyntaxKind.FunctionExpression) {
assignee = container;
container.symbol.members = container.symbol.members || createMap<Symbol>();
// It's acceptable for multiple 'this' assignments of the same identifier to occur
declareSymbol(container.symbol.members, container.symbol, node, SymbolFlags.Property, SymbolFlags.PropertyExcludes & ~SymbolFlags.Property);
}
else if (container.kind === SyntaxKind.Constructor) {
if (isInJavaScriptFile(node)) {
// this.foo assignment in a JavaScript class
// Bind this property to the containing class
const saveContainer = container;
container = container.parent;
bindPropertyOrMethodOrAccessor(node, SymbolFlags.Property, SymbolFlags.None);
container = saveContainer;
return;
// this.foo assignment in a JavaScript class
// Bind this property to the containing class
const saveContainer = container;
container = container.parent;
const symbol = bindPropertyOrMethodOrAccessor(node, SymbolFlags.Property, SymbolFlags.None);
if (symbol) {
// constructor-declared symbols can be overwritten by subsequent method declarations
(symbol as Symbol).isReplaceableByMethod = true;
}
else {
assignee = container.parent;
}
}
else {
return;
container = saveContainer;
}
assignee.symbol.members = assignee.symbol.members || createMap<Symbol>();
// It's acceptable for multiple 'this' assignments of the same identifier to occur
declareSymbol(assignee.symbol.members, assignee.symbol, node, SymbolFlags.Property, SymbolFlags.PropertyExcludes & ~SymbolFlags.Property);
}

function bindPrototypePropertyAssignment(node: BinaryExpression) {
Expand Down
1 change: 1 addition & 0 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2157,6 +2157,7 @@ namespace ts {
/* @internal */ exportSymbol?: Symbol; // Exported symbol associated with this symbol
/* @internal */ constEnumOnlyModule?: boolean; // True if module contains only const enums or other modules with only const enums
/* @internal */ isReferenced?: boolean; // True if the symbol is referenced elsewhere
/* @internal */ isReplaceableByMethod?: boolean; // Can this Javascript class property be replaced by a method symbol?
/* @internal */ isAssigned?: boolean; // True if the symbol is a parameter with assignments
}

Expand Down
61 changes: 59 additions & 2 deletions tests/baselines/reference/multipleDeclarations.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,38 @@
//// [input.js]

function C() {
this.m = null;
}
C.prototype.m = function() {
this.nothing();
};
}
class X {
constructor() {
this.m = this.m.bind(this);
this.mistake = 'frankly, complete nonsense';
}
m() {
}
mistake() {
}
}
let x = new X();
X.prototype.mistake = false;
x.m();
x.mistake;
class Y {
mistake() {
}
m() {
}
constructor() {
this.m = this.m.bind(this);
this.mistake = 'even more nonsense';
}
}
Y.prototype.mistake = true;
let y = new Y();
y.m();
y.mistake();


//// [output.js]
Expand All @@ -15,3 +42,33 @@ function C() {
C.prototype.m = function () {
this.nothing();
};
var X = (function () {
function X() {
this.m = this.m.bind(this);
this.mistake = 'frankly, complete nonsense';
}
X.prototype.m = function () {
};
X.prototype.mistake = function () {
};
return X;
}());
var x = new X();
X.prototype.mistake = false;
x.m();
x.mistake;
var Y = (function () {
function Y() {
this.m = this.m.bind(this);
this.mistake = 'even more nonsense';
}
Y.prototype.mistake = function () {
};
Y.prototype.m = function () {
};
return Y;
}());
Y.prototype.mistake = true;
var y = new Y();
y.m();
y.mistake();
97 changes: 92 additions & 5 deletions tests/baselines/reference/multipleDeclarations.symbols
Original file line number Diff line number Diff line change
@@ -1,19 +1,106 @@
=== tests/cases/conformance/salsa/input.js ===

function C() {
>C : Symbol(C, Decl(input.js, 0, 0))

this.m = null;
>m : Symbol(C.m, Decl(input.js, 1, 14), Decl(input.js, 3, 1))
>m : Symbol(C.m, Decl(input.js, 0, 14), Decl(input.js, 2, 1))
}
C.prototype.m = function() {
>C.prototype : Symbol(C.m, Decl(input.js, 1, 14), Decl(input.js, 3, 1))
>C.prototype : Symbol(C.m, Decl(input.js, 0, 14), Decl(input.js, 2, 1))
>C : Symbol(C, Decl(input.js, 0, 0))
>prototype : Symbol(Function.prototype, Decl(lib.d.ts, --, --))
>m : Symbol(C.m, Decl(input.js, 1, 14), Decl(input.js, 3, 1))
>m : Symbol(C.m, Decl(input.js, 0, 14), Decl(input.js, 2, 1))

this.nothing();
>this : Symbol(C, Decl(input.js, 0, 0))
}
class X {
>X : Symbol(X, Decl(input.js, 5, 1))

constructor() {
this.m = this.m.bind(this);
>this.m : Symbol(X.m, Decl(input.js, 10, 5))
>this : Symbol(X, Decl(input.js, 5, 1))
>m : Symbol(X.m, Decl(input.js, 7, 19))
>this.m.bind : Symbol(Function.bind, Decl(lib.d.ts, --, --))
>this.m : Symbol(X.m, Decl(input.js, 10, 5))
>this : Symbol(X, Decl(input.js, 5, 1))
>m : Symbol(X.m, Decl(input.js, 10, 5))
>bind : Symbol(Function.bind, Decl(lib.d.ts, --, --))
>this : Symbol(X, Decl(input.js, 5, 1))

this.mistake = 'frankly, complete nonsense';
>this.mistake : Symbol(X.mistake, Decl(input.js, 12, 5))
>this : Symbol(X, Decl(input.js, 5, 1))
>mistake : Symbol(X.mistake, Decl(input.js, 8, 35))
}
m() {
>m : Symbol(X.m, Decl(input.js, 10, 5))
}
mistake() {
>mistake : Symbol(X.mistake, Decl(input.js, 12, 5))
}
}
let x = new X();
>x : Symbol(x, Decl(input.js, 16, 3))
>X : Symbol(X, Decl(input.js, 5, 1))

X.prototype.mistake = false;
>X.prototype.mistake : Symbol(X.mistake, Decl(input.js, 12, 5))
>X : Symbol(X, Decl(input.js, 5, 1))
>prototype : Symbol(X.prototype)

x.m();
>x.m : Symbol(X.m, Decl(input.js, 10, 5))
>x : Symbol(x, Decl(input.js, 16, 3))
>m : Symbol(X.m, Decl(input.js, 10, 5))

x.mistake;
>x.mistake : Symbol(X.mistake, Decl(input.js, 12, 5))
>x : Symbol(x, Decl(input.js, 16, 3))
>mistake : Symbol(X.mistake, Decl(input.js, 12, 5))

class Y {
>Y : Symbol(Y, Decl(input.js, 19, 10))

mistake() {
>mistake : Symbol(Y.mistake, Decl(input.js, 20, 9), Decl(input.js, 26, 35))
}
m() {
>m : Symbol(Y.m, Decl(input.js, 22, 5), Decl(input.js, 25, 19))
}
constructor() {
this.m = this.m.bind(this);
>this.m : Symbol(Y.m, Decl(input.js, 22, 5), Decl(input.js, 25, 19))
>this : Symbol(Y, Decl(input.js, 19, 10))
>m : Symbol(Y.m, Decl(input.js, 22, 5), Decl(input.js, 25, 19))
>this.m : Symbol(Y.m, Decl(input.js, 22, 5), Decl(input.js, 25, 19))
>this : Symbol(Y, Decl(input.js, 19, 10))
>m : Symbol(Y.m, Decl(input.js, 22, 5), Decl(input.js, 25, 19))
>this : Symbol(Y, Decl(input.js, 19, 10))

this.mistake = 'even more nonsense';
>this.mistake : Symbol(Y.mistake, Decl(input.js, 20, 9), Decl(input.js, 26, 35))
>this : Symbol(Y, Decl(input.js, 19, 10))
>mistake : Symbol(Y.mistake, Decl(input.js, 20, 9), Decl(input.js, 26, 35))
}
}
Y.prototype.mistake = true;
>Y.prototype.mistake : Symbol(Y.mistake, Decl(input.js, 20, 9), Decl(input.js, 26, 35))
>Y : Symbol(Y, Decl(input.js, 19, 10))
>prototype : Symbol(Y.prototype)

let y = new Y();
>y : Symbol(y, Decl(input.js, 31, 3))
>Y : Symbol(Y, Decl(input.js, 19, 10))

y.m();
>y.m : Symbol(Y.m, Decl(input.js, 22, 5), Decl(input.js, 25, 19))
>y : Symbol(y, Decl(input.js, 31, 3))
>m : Symbol(Y.m, Decl(input.js, 22, 5), Decl(input.js, 25, 19))

};
y.mistake();
>y.mistake : Symbol(Y.mistake, Decl(input.js, 20, 9), Decl(input.js, 26, 35))
>y : Symbol(y, Decl(input.js, 31, 3))
>mistake : Symbol(Y.mistake, Decl(input.js, 20, 9), Decl(input.js, 26, 35))

Loading