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 1 commit
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
58 changes: 33 additions & 25 deletions src/compiler/binder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,8 +298,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 @@ -326,34 +328,38 @@ namespace ts {
classifiableNames[name] = name;
}

if (symbol.flags & excludes) {
if (node.name) {
node.name.parent = node;
else if (symbol.flags & excludes) {
if (symbol.isDiscardable) {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: you may want to mention this change in the comment above

Copy link
Member Author

Choose a reason for hiding this comment

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

done

// 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 @@ -1967,7 +1973,7 @@ namespace ts {
}

function bindThisPropertyAssignment(node: BinaryExpression) {
// Declare a 'member' in case it turns out the container was an ES5 class or ES6 constructor
// Declare a 'member' if the container is an ES5 class or ES6 constructor
let assignee: Node;
if (container.kind === SyntaxKind.FunctionDeclaration || container.kind === SyntaxKind.FunctionExpression) {
assignee = container;
Expand All @@ -1980,7 +1986,9 @@ namespace ts {
}
assignee.symbol.members = assignee.symbol.members || {};
// 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);
// AND it can be overwritten by subsequent method declarations
let symbol = declareSymbol(assignee.symbol.members, assignee.symbol, node, SymbolFlags.Property, SymbolFlags.PropertyExcludes & ~SymbolFlags.Property);
symbol.isDiscardable = true;
}

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 @@ -2137,6 +2137,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 */ isDiscardable?: boolean;// True if a Javascript class property can be overwritten by a method
}

/* @internal */
Expand Down
32 changes: 31 additions & 1 deletion tests/baselines/reference/multipleDeclarations.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,22 @@ function C() {
}
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;


//// [output.js]
Expand All @@ -15,3 +30,18 @@ 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;
47 changes: 46 additions & 1 deletion tests/baselines/reference/multipleDeclarations.symbols
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,51 @@ C.prototype.m = function() {

this.nothing();
>this : Symbol(C, Decl(input.js, 0, 0))
}

class X {
>X : Symbol(X, Decl(input.js, 6, 1))

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

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

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

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

};
x.mistake;
>x.mistake : Symbol(X.mistake, Decl(input.js, 14, 5))
>x : Symbol(x, Decl(input.js, 18, 3))
>mistake : Symbol(X.mistake, Decl(input.js, 14, 5))

57 changes: 56 additions & 1 deletion tests/baselines/reference/multipleDeclarations.types
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,61 @@ C.prototype.m = function() {
>this.nothing : any
>this : { m: () => void; }
>nothing : any
}

class X {
>X : X

constructor() {
this.m = this.m.bind(this);
>this.m = this.m.bind(this) : any
>this.m : () => void
>this : this
>m : () => void
>this.m.bind(this) : any
>this.m.bind : (this: Function, thisArg: any, ...argArray: any[]) => any
>this.m : () => void
>this : this
>m : () => void
>bind : (this: Function, thisArg: any, ...argArray: any[]) => any
>this : this

this.mistake = 'frankly, complete nonsense';
>this.mistake = 'frankly, complete nonsense' : string
>this.mistake : () => void
>this : this
>mistake : () => void
>'frankly, complete nonsense' : string
}
m() {
>m : () => void
}
mistake() {
>mistake : () => void
}
}
let x = new X();
>x : X
>new X() : X
>X : typeof X

X.prototype.mistake = false;
>X.prototype.mistake = false : boolean
>X.prototype.mistake : () => void
>X.prototype : X
>X : typeof X
>prototype : X
>mistake : () => void
>false : boolean

x.m();
>x.m() : void
>x.m : () => void
>x : X
>m : () => void

};
x.mistake;
>x.mistake : () => void
>x : X
>mistake : () => void

17 changes: 16 additions & 1 deletion tests/cases/conformance/salsa/multipleDeclarations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,19 @@ function C() {
}
C.prototype.m = function() {
this.nothing();
};
}

Copy link
Member

Choose a reason for hiding this comment

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

Need a test that does this in the opposite order (with the constructor below the method declarations)

Copy link
Member Author

Choose a reason for hiding this comment

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

Done

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;