Skip to content

JavaScript prototype class inference #5876

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 15 commits into from
Dec 14, 2015
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
69 changes: 62 additions & 7 deletions src/compiler/binder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,8 +222,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 @@ -1166,11 +1179,25 @@ namespace ts {
return checkStrictModeIdentifier(<Identifier>node);
case SyntaxKind.BinaryExpression:
if (isInJavaScriptFile(node)) {
if (isExportsPropertyAssignment(node)) {
bindExportsPropertyAssignment(<BinaryExpression>node);
}
else if (isModuleExportsAssignment(node)) {
bindModuleExportsAssignment(<BinaryExpression>node);
const 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;
case SpecialPropertyAssignmentKind.None:
// Nothing to do
break;
default:
Debug.fail("Unknown special property assignment kind");
}
}
return checkStrictModeBinaryExpression(<BinaryExpression>node);
Expand Down Expand Up @@ -1339,6 +1366,34 @@ namespace ts {
bindExportAssignment(node);
}

function bindThisPropertyAssignment(node: BinaryExpression) {
// Declare a 'member' in case it turns out the container was an ES5 class
if (container.kind === SyntaxKind.FunctionExpression || container.kind === SyntaxKind.FunctionDeclaration) {
container.symbol.members = container.symbol.members || {};
declareSymbol(container.symbol.members, container.symbol, node, SymbolFlags.Property, SymbolFlags.PropertyExcludes);
}
}

function bindPrototypePropertyAssignment(node: BinaryExpression) {
// We saw a node of the form 'x.prototype.y = z'. Declare a 'member' y on x if x was a function.

// Look up the function in the local scope, since prototype assignments should
// follow the function declaration
const classId = <Identifier>(<PropertyAccessExpression>(<PropertyAccessExpression>node.left).expression).expression;
const funcSymbol = container.locals[classId.text];
Copy link
Member

Choose a reason for hiding this comment

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

This doesn't guarantee that the prototype assignment immediately follows. Also, what if classId denotes a non-function, such as a variable or an interface declaration? I'm thinking you should instead check that the prototype assignment is immediately contained in a statement list and that the immediately preceding statement is a function declaration.

if (!funcSymbol || !(funcSymbol.flags & SymbolFlags.Function)) {
return;
}

// Set up the members collection if it doesn't exist already
if (!funcSymbol.members) {
funcSymbol.members = {};
}

// Declare the method/property
declareSymbol(funcSymbol.members, funcSymbol, <PropertyAccessExpression>node.left, SymbolFlags.Property, SymbolFlags.PropertyExcludes);
}

function bindCallExpression(node: CallExpression) {
// We're only inspecting call expressions to detect CommonJS modules, so we can skip
// this check if we've already seen the module indicator
Expand Down
43 changes: 39 additions & 4 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2664,9 +2664,13 @@ 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);
// Declarations only exist for property access expressions for certain
// special assignment kinds
if (declaration.parent.kind === SyntaxKind.BinaryExpression) {
// Handle exports.p = expr or this.p = expr or className.prototype.method = expr
return links.type = checkExpressionCached((<BinaryExpression>declaration.parent).right);
}
}
// Handle variable, parameter or property
if (!pushTypeResolution(symbol, TypeSystemPropertyName.Type)) {
Expand Down Expand Up @@ -6888,6 +6892,23 @@ namespace ts {
const symbol = getSymbolOfNode(container.parent);
return container.flags & NodeFlags.Static ? getTypeOfSymbol(symbol) : (<InterfaceType>getDeclaredTypeOfSymbol(symbol)).thisType;
}

// If this is a function in a JS file, it might be a class method. Check if it's the RHS
// of a x.prototype.y = function [name]() { .... }
if (isInJavaScriptFile(node) && container.kind === SyntaxKind.FunctionExpression) {
if (getSpecialPropertyAssignmentKind(container.parent) === SpecialPropertyAssignmentKind.PrototypeProperty) {
// Get the 'x' of 'x.prototype.y = f' (here, 'f' is 'container')
const className = (((container.parent as BinaryExpression) // x.protoype.y = f
.left as PropertyAccessExpression) // x.prototype.y
.expression as PropertyAccessExpression) // x.prototype
.expression; // x
const classSymbol = checkExpression(className).symbol;
if (classSymbol && classSymbol.members && (classSymbol.flags & SymbolFlags.Function)) {
return getInferredClassType(classSymbol);
}
}
}

return anyType;
}

Expand Down Expand Up @@ -9609,6 +9630,14 @@ namespace ts {
return links.resolvedSignature;
}

function getInferredClassType(symbol: Symbol) {
const links = getSymbolLinks(symbol);
if (!links.inferredClassType) {
links.inferredClassType = createAnonymousType(undefined, symbol.members, emptyArray, emptyArray, /*stringIndexType*/ undefined, /*numberIndexType*/ undefined);
}
return links.inferredClassType;
}

/**
* Syntactically and semantically checks a call or new expression.
* @param node The call/new expression to be checked.
Expand All @@ -9630,8 +9659,14 @@ namespace ts {
declaration.kind !== SyntaxKind.ConstructSignature &&
declaration.kind !== SyntaxKind.ConstructorType) {

// When resolved signature is a call signature (and not a construct signature) the result type is any
if (compilerOptions.noImplicitAny) {
// When resolved signature is a call signature (and not a construct signature) the result type is any, unless
// the declaring function had members created through 'x.prototype.y = expr' or 'this.y = expr' psuedodeclarations
// in a JS file
const funcSymbol = checkExpression(node.expression).symbol;
if (funcSymbol && funcSymbol.members && (funcSymbol.flags & SymbolFlags.Function)) {
return getInferredClassType(funcSymbol);
}
else if (compilerOptions.noImplicitAny) {
error(node, Diagnostics.new_expression_whose_target_lacks_a_construct_signature_implicitly_has_an_any_type);
}
return anyType;
Expand Down
14 changes: 14 additions & 0 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1997,6 +1997,7 @@ namespace ts {
type?: Type; // Type of value symbol
declaredType?: Type; // Type of class, interface, enum, type alias, or type parameter
typeParameters?: TypeParameter[]; // Type parameters of type alias (undefined if non-generic)
inferredClassType?: Type; // Type of an inferred ES5 class
instantiations?: Map<Type>; // Instantiations of generic type alias (undefined if non-generic)
mapper?: TypeMapper; // Type mapper for instantiation alias
referenced?: boolean; // True if alias symbol has been referenced as a value
Expand Down Expand Up @@ -2290,6 +2291,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
59 changes: 33 additions & 26 deletions src/compiler/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1063,33 +1063,40 @@ namespace ts {
(<CallExpression>expression).arguments[0].kind === SyntaxKind.StringLiteral;
}

/**
* 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.
*/
export function isExportsPropertyAssignment(expression: Node): boolean {
// of the form 'exports.name = expr' where 'name' and 'expr' are arbitrary
return isInJavaScriptFile(expression) &&
(expression.kind === SyntaxKind.BinaryExpression) &&
((<BinaryExpression>expression).operatorToken.kind === SyntaxKind.EqualsToken) &&
((<BinaryExpression>expression).left.kind === SyntaxKind.PropertyAccessExpression) &&
((<PropertyAccessExpression>(<BinaryExpression>expression).left).expression.kind === SyntaxKind.Identifier) &&
((<Identifier>((<PropertyAccessExpression>(<BinaryExpression>expression).left).expression)).text === "exports");
}
/// 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
const innerPropertyAccess = <PropertyAccessExpression>lhs.expression;
if (innerPropertyAccess.expression.kind === SyntaxKind.Identifier && innerPropertyAccess.name.text === "prototype") {
return SpecialPropertyAssignmentKind.PrototypeProperty;
}
}

/**
* Returns true if the node is an assignment to the property access expression 'module.exports'.
* This function does not test if the node is in a JavaScript file or not.
*/
export function isModuleExportsAssignment(expression: Node): boolean {
// of the form 'module.exports = expr' where 'expr' is arbitrary
return isInJavaScriptFile(expression) &&
(expression.kind === SyntaxKind.BinaryExpression) &&
((<BinaryExpression>expression).operatorToken.kind === SyntaxKind.EqualsToken) &&
((<BinaryExpression>expression).left.kind === SyntaxKind.PropertyAccessExpression) &&
((<PropertyAccessExpression>(<BinaryExpression>expression).left).expression.kind === SyntaxKind.Identifier) &&
((<Identifier>((<PropertyAccessExpression>(<BinaryExpression>expression).left).expression)).text === "module") &&
((<PropertyAccessExpression>(<BinaryExpression>expression).left).name.text === "exports");
return SpecialPropertyAssignmentKind.None;
}

export function getExternalModuleName(node: Node): Expression {
Expand Down
22 changes: 18 additions & 4 deletions src/harness/fourslash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1162,7 +1162,7 @@ namespace FourSlash {

public printCurrentQuickInfo() {
const quickInfo = this.languageService.getQuickInfoAtPosition(this.activeFile.fileName, this.currentCaretPosition);
Harness.IO.log(JSON.stringify(quickInfo));
Harness.IO.log("Quick Info: " + quickInfo.displayParts.map(part => part.text).join(""));
}

public printErrorList() {
Expand Down Expand Up @@ -1204,12 +1204,26 @@ namespace FourSlash {

public printMemberListMembers() {
const members = this.getMemberListAtCaret();
Harness.IO.log(JSON.stringify(members));
this.printMembersOrCompletions(members);
}

public printCompletionListMembers() {
const completions = this.getCompletionListAtCaret();
Harness.IO.log(JSON.stringify(completions));
this.printMembersOrCompletions(completions);
}

private printMembersOrCompletions(info: ts.CompletionInfo) {
function pad(s: string, length: number) {
return s + new Array(length - s.length + 1).join(" ");
}
function max<T>(arr: T[], selector: (x: T) => number): number {
return arr.reduce((prev, x) => Math.max(prev, selector(x)), 0);
}
const longestNameLength = max(info.entries, m => m.name.length);
const longestKindLength = max(info.entries, m => m.kind.length);
info.entries.sort((m, n) => m.sortText > n.sortText ? 1 : m.sortText < n.sortText ? -1 : m.name > n.name ? 1 : m.name < n.name ? -1 : 0);
const membersString = info.entries.map(m => `${pad(m.name, longestNameLength)} ${pad(m.kind, longestKindLength)} ${m.kindModifiers}`).join("\n");
Harness.IO.log(membersString);
}

public printReferences() {
Expand Down Expand Up @@ -3287,4 +3301,4 @@ namespace FourSlashInterface {
};
}
}
}
}
46 changes: 46 additions & 0 deletions tests/cases/fourslash/javaScriptPrototype1.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
///<reference path="fourslash.ts" />

// Assignments to the 'prototype' property of a function create a class

// @allowNonTsExtensions: true
// @Filename: myMod.js
//// function myCtor(x) {
//// }
//// myCtor.prototype.foo = function() { return 32 };
//// myCtor.prototype.bar = function() { return '' };
////
//// var m = new myCtor(10);
//// m/*1*/
//// 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, 'property');
verify.memberListContains('bar', undefined, undefined, 'property');
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();

// 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');
Loading