Skip to content

Commit

Permalink
Merge pull request #5876 from RyanCavanaugh/javaScriptPrototypes
Browse files Browse the repository at this point in the history
JavaScript prototype class inference
  • Loading branch information
RyanCavanaugh committed Dec 14, 2015
2 parents ef9f404 + 37f3ff8 commit 2f447ee
Show file tree
Hide file tree
Showing 10 changed files with 308 additions and 41 deletions.
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 @@ -1351,6 +1378,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];
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 @@ -2743,9 +2743,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 @@ -7021,6 +7025,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 @@ -9742,6 +9763,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 @@ -9763,8 +9792,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 @@ -2017,6 +2017,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 @@ -2316,6 +2317,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 @@ -1068,33 +1068,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

0 comments on commit 2f447ee

Please sign in to comment.