Skip to content
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
Prev Previous commit
Next Next commit
Inference from JavaScript prototype property assignments
  • Loading branch information
RyanCavanaugh committed Oct 30, 2015
commit 3b7213116d324585dc73548dc2289f8513b1884c
58 changes: 44 additions & 14 deletions src/compiler/binder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,8 +167,21 @@ namespace ts {
case SyntaxKind.ExportAssignment:
return (<ExportAssignment>node).isExportEquals ? "export=" : "default";
case SyntaxKind.BinaryExpression:
// Binary expression case is for JS module 'module.exports = expr'
return "export=";
switch (getSpecialPropertyAssignmentKind(node)) {
case SpecialPropertyAssignmentKind.ModuleExports:
// module.exports = ...
return "export=";
case SpecialPropertyAssignmentKind.ExportsProperty:
case SpecialPropertyAssignmentKind.ThisProperty:
// exports.x = ... or this.y = ...
return ((node as BinaryExpression).left as PropertyAccessExpression).name.text;
case SpecialPropertyAssignmentKind.PrototypeProperty:
// className.prototype.methodName = ...
return (((node as BinaryExpression).left as PropertyAccessExpression).expression as PropertyAccessExpression).name.text;
}
Debug.fail("Unknown binary declaration kind");
break;

case SyntaxKind.FunctionDeclaration:
case SyntaxKind.ClassDeclaration:
return node.flags & NodeFlags.Default ? "default" : undefined;
Expand Down Expand Up @@ -862,14 +875,20 @@ namespace ts {
return checkStrictModeIdentifier(<Identifier>node);
case SyntaxKind.BinaryExpression:
if (isJavaScriptFile) {
if (isExportsPropertyAssignment(node)) {
bindExportsPropertyAssignment(<BinaryExpression>node);
}
else if (isModuleExportsAssignment(node)) {
bindModuleExportsAssignment(<BinaryExpression>node);
}
else if (isPrototypePropertyAssignment(node)) {
bindPrototypePropertyAssignment(node);
let specialKind = getSpecialPropertyAssignmentKind(node);
switch (specialKind) {
case SpecialPropertyAssignmentKind.ExportsProperty:
bindExportsPropertyAssignment(<BinaryExpression>node);
break;
case SpecialPropertyAssignmentKind.ModuleExports:
bindModuleExportsAssignment(<BinaryExpression>node);
break;
case SpecialPropertyAssignmentKind.PrototypeProperty:
bindPrototypePropertyAssignment(<BinaryExpression>node);
break;
case SpecialPropertyAssignmentKind.ThisProperty:
bindThisPropertyAssignment(<BinaryExpression>node);
break;
}
}
return checkStrictModeBinaryExpression(<BinaryExpression>node);
Expand Down Expand Up @@ -1038,6 +1057,13 @@ namespace ts {
bindExportAssignment(node);
}

function bindThisPropertyAssignment(node: BinaryExpression) {
if (container.kind === SyntaxKind.FunctionExpression || container.kind === SyntaxKind.FunctionDeclaration) {
container.symbol.members = container.symbol.members || {};
declareClassMember(node, SymbolFlags.Property, SymbolFlags.PropertyExcludes);
}
}

function bindPrototypePropertyAssignment(node: BinaryExpression) {
// We saw a node of the form 'x.prototype.y = z'.
// This does two things: turns 'x' into a constructor function, and
Expand All @@ -1054,16 +1080,20 @@ namespace ts {

// The function is now a constructor rather than a normal function
if (!funcSymbol.inferredConstructor) {
funcSymbol.flags = (funcSymbol.flags | SymbolFlags.Class) & ~SymbolFlags.Function;
declareSymbol(container.locals, funcSymbol, funcSymbol.valueDeclaration, SymbolFlags.Class, SymbolFlags.None);
// funcSymbol.flags = (funcSymbol.flags | SymbolFlags.Class) & ~SymbolFlags.Function;
funcSymbol.members = funcSymbol.members || {};
funcSymbol.members["__constructor"] = funcSymbol;
funcSymbol.inferredConstructor = true;
}

// Get 'y', the property name, and add it to the type of the class
let propertyName = (<PropertyAccessExpression>node.left).name;
let prototypeSymbol = declareSymbol(funcSymbol.members, funcSymbol, <PropertyAccessExpression>(<PropertyAccessExpression>node.left).expression, SymbolFlags.HasMembers, SymbolFlags.None);
// Declare the 'prototype' member of the function
let prototypeSymbol = declareSymbol(funcSymbol.exports, funcSymbol, <PropertyAccessExpression>(<PropertyAccessExpression>node.left).expression, SymbolFlags.ObjectLiteral | SymbolFlags.Property, SymbolFlags.None);

// Declare the property on the prototype symbol
declareSymbol(prototypeSymbol.members, prototypeSymbol, <PropertyAccessExpression>node.left, SymbolFlags.Method, SymbolFlags.None);
// and on the class type
declareSymbol(funcSymbol.members, funcSymbol, <PropertyAccessExpression>node.left, SymbolFlags.Method, SymbolFlags.PropertyExcludes);
}

function bindCallExpression(node: CallExpression) {
Expand Down
31 changes: 24 additions & 7 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2576,9 +2576,16 @@ namespace ts {
if (declaration.kind === SyntaxKind.BinaryExpression) {
return links.type = checkExpression((<BinaryExpression>declaration).right);
}
// Handle exports.p = expr
if (declaration.kind === SyntaxKind.PropertyAccessExpression) {
return checkExpressionCached((<BinaryExpression>declaration.parent).right);
if (declaration.parent.kind === SyntaxKind.BinaryExpression) {
// Handle exports.p = expr or this.p = expr or className.prototype.method = expr
return links.type = checkExpression((<BinaryExpression>declaration.parent).right);
}
else {
// Declaration for className.prototype in inferred JS class
let type = createAnonymousType(symbol, symbol.members, emptyArray, emptyArray, undefined, undefined);
return links.type = type;
}
}
// Handle variable, parameter or property
if (!pushTypeResolution(symbol, TypeSystemPropertyName.Type)) {
Expand Down Expand Up @@ -3799,8 +3806,15 @@ namespace ts {
default:
if (declaration.symbol.inferredConstructor) {
kind = SignatureKind.Construct;
let proto = declaration.symbol.members["prototype"];
returnType = createAnonymousType(createSymbol(SymbolFlags.None, "__jsClass"), proto.members, emptyArray, emptyArray, undefined, undefined);
let members = createSymbolTable(emptyArray);
// Collect methods declared with className.protoype.methodName = ...
let proto = declaration.symbol.exports["prototype"];
if (proto) {
mergeSymbolTable(members, proto.members);
}
// Collect properties defined in the constructor by this.propName = ...
mergeSymbolTable(members, declaration.symbol.members);
returnType = createAnonymousType(declaration.symbol, members, emptyArray, emptyArray, undefined, undefined);
}
else {
kind = SignatureKind.Call;
Expand Down Expand Up @@ -3846,8 +3860,8 @@ namespace ts {
break;

case SyntaxKind.PropertyAccessExpression:
// Class inference from ClassName.prototype.methodName = expr
return getSignaturesOfType(checkExpressionCached((<BinaryExpression>node.parent).right), SignatureKind.Call);
result = getSignaturesOfType(checkExpressionCached((<BinaryExpression>node.parent).right), SignatureKind.Call);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm concerned with using checkExpression here. It might recursively get you back to this same spot. We're just supposed to create a signature from a declaration here.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Move to getTypeOf[...]

break;
}
}
return result;
Expand Down Expand Up @@ -6957,7 +6971,10 @@ namespace ts {
let operator = binaryExpression.operatorToken.kind;
if (operator >= SyntaxKind.FirstAssignment && operator <= SyntaxKind.LastAssignment) {
// In an assignment expression, the right operand is contextually typed by the type of the left operand.
if (node === binaryExpression.right) {
// In JS files where a special assignment is taking place, don't contextually type the RHS to avoid
// incorrectly assuming a circular 'any' (the type of the LHS is determined by the RHS)
if (node === binaryExpression.right &&
!(node.parserContextFlags & ParserContextFlags.JavaScriptFile && getSpecialPropertyAssignmentKind(binaryExpression))) {
return checkExpression(binaryExpression.left);
}
}
Expand Down
13 changes: 13 additions & 0 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2013,6 +2013,19 @@ namespace ts {
// It is optional because in contextual signature instantiation, nothing fails
}

/* @internal */
export const enum SpecialPropertyAssignmentKind {
None,
/// exports.name = expr
ExportsProperty,
/// module.exports = expr
ModuleExports,
/// className.prototype.name = expr
PrototypeProperty,
/// this.name = expr
ThisProperty
}

export interface DiagnosticMessage {
key: string;
category: DiagnosticCategory;
Expand Down
36 changes: 36 additions & 0 deletions src/compiler/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1057,6 +1057,42 @@ namespace ts {
(<CallExpression>expression).arguments[0].kind === SyntaxKind.StringLiteral;
}

/// Given a BinaryExpression, returns SpecialPropertyAssignmentKind for the various kinds of property
/// assignments we treat as special in the binder
export function getSpecialPropertyAssignmentKind(expression: Node): SpecialPropertyAssignmentKind {
if (expression.kind !== SyntaxKind.BinaryExpression) {
return SpecialPropertyAssignmentKind.None;
}
const expr = <BinaryExpression>expression;
if (expr.operatorToken.kind !== SyntaxKind.EqualsToken || expr.left.kind !== SyntaxKind.PropertyAccessExpression) {
return SpecialPropertyAssignmentKind.None;
}
const lhs = <PropertyAccessExpression>expr.left;
if (lhs.expression.kind === SyntaxKind.Identifier) {
const lhsId = <Identifier>lhs.expression;
if (lhsId.text === "exports") {
// exports.name = expr
return SpecialPropertyAssignmentKind.ExportsProperty;
}
else if (lhsId.text === "module" && lhs.name.text === "exports") {
// module.exports = expr
return SpecialPropertyAssignmentKind.ModuleExports;
}
}
else if (lhs.expression.kind === SyntaxKind.ThisKeyword) {
return SpecialPropertyAssignmentKind.ThisProperty;
}
else if (lhs.expression.kind === SyntaxKind.PropertyAccessExpression) {
// chained dot, e.g. x.y.z = expr; this var is the 'x.y' part
let innerPropertyAccess = <PropertyAccessExpression>lhs.expression;
if (innerPropertyAccess.expression.kind === SyntaxKind.Identifier && innerPropertyAccess.name.text === "prototype") {
return SpecialPropertyAssignmentKind.PrototypeProperty;
}
}

return SpecialPropertyAssignmentKind.None;
}

/**
* Returns true if the node is an assignment to a property on the identifier 'exports'.
* This function does not test if the node is in a JavaScript file or not.
Expand Down
26 changes: 18 additions & 8 deletions tests/cases/fourslash/javaScriptPrototype1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,26 +11,36 @@
////
//// var m = new myCtor(10);
//// m/*1*/
//// var x = m.foo();
//// x/*2*/
//// var y = m.bar();
//// y/*3*/
//// var a = m.foo;
//// a/*2*/
//// var b = a();
//// b/*3*/
//// var c = m.bar();
//// c/*4*/


// Members of the class instance
goTo.marker('1');
edit.insert('.');
verify.memberListContains('foo', undefined, undefined, 'method');
edit.insert('foo');

edit.backspace();
verify.memberListContains('bar', undefined, undefined, 'method');
edit.backspace();

// Members of a class method (1)
goTo.marker('2');
edit.insert('.');
verify.memberListContains('length', undefined, undefined, 'property');
edit.backspace();

// Members of the invocation of a class method (1)
goTo.marker('3');
edit.insert('.');
verify.memberListContains('toFixed', undefined, undefined, 'method');
verify.not.memberListContains('substr', undefined, undefined, 'method');
edit.backspace();

goTo.marker('3');
// Members of the invocation of a class method (2)
goTo.marker('4');
edit.insert('.');
verify.memberListContains('substr', undefined, undefined, 'method');
verify.not.memberListContains('toFixed', undefined, undefined, 'method');
36 changes: 36 additions & 0 deletions tests/cases/fourslash/javaScriptPrototype2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
///<reference path="fourslash.ts" />

// Assignments to 'this' in the constructorish body create
// properties with those names

// @allowNonTsExtensions: true
// @Filename: myMod.js
//// function myCtor(x) {
//// this.qua = 10;
//// }
//// myCtor.prototype.foo = function() { return 32 };
//// myCtor.prototype.bar = function() { return '' };
////
//// var m = new myCtor(10);
//// m/*1*/
//// var x = m.qua;
//// x/*2*/
//// myCtor/*3*/

// Verify the instance property exists
goTo.marker('1');
edit.insert('.');
verify.completionListContains('qua', undefined, undefined, 'property');
edit.backspace();

// Verify the type of the instance property
goTo.marker('2');
edit.insert('.');
verify.completionListContains('toFixed', undefined, undefined, 'method');

goTo.marker('3');
edit.insert('.');
// Make sure symbols don't leak out into the constructor
verify.completionListContains('qua', undefined, undefined, 'warning');
verify.completionListContains('foo', undefined, undefined, 'warning');
verify.completionListContains('bar', undefined, undefined, 'warning');
40 changes: 40 additions & 0 deletions tests/cases/fourslash/javaScriptPrototype3.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
///<reference path="fourslash.ts" />

// ES6 classes can extend from JS classes

// @allowNonTsExtensions: true
// @Filename: myMod.js
//// function myCtor(x) {
//// this.qua = 10;
//// }
//// myCtor.prototype.foo = function() { return 32 };
//// myCtor.prototype.bar = function() { return '' };
////
//// class MyClass extends myCtor {
//// fn() {
//// this/*1*/
//// let y = super.foo();
//// y;
//// }
//// }
//// var n = new MyClass(3);
//// n/*2*/;

goTo.marker('1');
edit.insert('.');
// Current class method
verify.completionListContains('fn', undefined, undefined, 'method');
// Base class method
verify.completionListContains('foo', undefined, undefined, 'method');
// Base class instance property
verify.completionListContains('qua', undefined, undefined, 'property');
edit.backspace();

// Derived class instance from outside the class
goTo.marker('2');
edit.insert('.');
verify.completionListContains('fn', undefined, undefined, 'method');
// Base class method
verify.completionListContains('foo', undefined, undefined, 'method');
// Base class instance property
verify.completionListContains('qua', undefined, undefined, 'property');
36 changes: 36 additions & 0 deletions tests/cases/fourslash/javaScriptPrototype4.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
///<reference path="fourslash.ts" />

// Check for any odd symbol leakage

// @allowNonTsExtensions: true
// @Filename: myMod.js
//// function myCtor(x) {
//// this.qua = 10;
//// }
//// myCtor.prototype.foo = function() { return 32 };
//// myCtor.prototype.bar = function() { return '' };
////
//// myCtor/*1*/

goTo.marker('1');
edit.insert('.');

// Check members of the function
verify.completionListContains('prototype', undefined, undefined, 'property');
verify.completionListContains('foo', undefined, undefined, 'warning');
verify.completionListContains('bar', undefined, undefined, 'warning');
verify.completionListContains('qua', undefined, undefined, 'warning');

// Check members of function.prototype
edit.insert('prototype.');
debugger;
debug.printMemberListMembers();
verify.completionListContains('foo', undefined, undefined, 'method');
verify.completionListContains('bar', undefined, undefined, 'method');
verify.completionListContains('qua', undefined, undefined, 'warning');
verify.completionListContains('prototype', undefined, undefined, 'warning');

// debug.printErrorList();
// debug.printCurrentQuickInfo();
// edit.insert('.');
// verify.completionListContains('toFixed', undefined, undefined, 'method');