Skip to content

Commit 2f447ee

Browse files
committed
Merge pull request #5876 from RyanCavanaugh/javaScriptPrototypes
JavaScript prototype class inference
2 parents ef9f404 + 37f3ff8 commit 2f447ee

10 files changed

+308
-41
lines changed

Diff for: src/compiler/binder.ts

+62-7
Original file line numberDiff line numberDiff line change
@@ -222,8 +222,21 @@ namespace ts {
222222
case SyntaxKind.ExportAssignment:
223223
return (<ExportAssignment>node).isExportEquals ? "export=" : "default";
224224
case SyntaxKind.BinaryExpression:
225-
// Binary expression case is for JS module 'module.exports = expr'
226-
return "export=";
225+
switch (getSpecialPropertyAssignmentKind(node)) {
226+
case SpecialPropertyAssignmentKind.ModuleExports:
227+
// module.exports = ...
228+
return "export=";
229+
case SpecialPropertyAssignmentKind.ExportsProperty:
230+
case SpecialPropertyAssignmentKind.ThisProperty:
231+
// exports.x = ... or this.y = ...
232+
return ((node as BinaryExpression).left as PropertyAccessExpression).name.text;
233+
case SpecialPropertyAssignmentKind.PrototypeProperty:
234+
// className.prototype.methodName = ...
235+
return (((node as BinaryExpression).left as PropertyAccessExpression).expression as PropertyAccessExpression).name.text;
236+
}
237+
Debug.fail("Unknown binary declaration kind");
238+
break;
239+
227240
case SyntaxKind.FunctionDeclaration:
228241
case SyntaxKind.ClassDeclaration:
229242
return node.flags & NodeFlags.Default ? "default" : undefined;
@@ -1166,11 +1179,25 @@ namespace ts {
11661179
return checkStrictModeIdentifier(<Identifier>node);
11671180
case SyntaxKind.BinaryExpression:
11681181
if (isInJavaScriptFile(node)) {
1169-
if (isExportsPropertyAssignment(node)) {
1170-
bindExportsPropertyAssignment(<BinaryExpression>node);
1171-
}
1172-
else if (isModuleExportsAssignment(node)) {
1173-
bindModuleExportsAssignment(<BinaryExpression>node);
1182+
const specialKind = getSpecialPropertyAssignmentKind(node);
1183+
switch (specialKind) {
1184+
case SpecialPropertyAssignmentKind.ExportsProperty:
1185+
bindExportsPropertyAssignment(<BinaryExpression>node);
1186+
break;
1187+
case SpecialPropertyAssignmentKind.ModuleExports:
1188+
bindModuleExportsAssignment(<BinaryExpression>node);
1189+
break;
1190+
case SpecialPropertyAssignmentKind.PrototypeProperty:
1191+
bindPrototypePropertyAssignment(<BinaryExpression>node);
1192+
break;
1193+
case SpecialPropertyAssignmentKind.ThisProperty:
1194+
bindThisPropertyAssignment(<BinaryExpression>node);
1195+
break;
1196+
case SpecialPropertyAssignmentKind.None:
1197+
// Nothing to do
1198+
break;
1199+
default:
1200+
Debug.fail("Unknown special property assignment kind");
11741201
}
11751202
}
11761203
return checkStrictModeBinaryExpression(<BinaryExpression>node);
@@ -1351,6 +1378,34 @@ namespace ts {
13511378
bindExportAssignment(node);
13521379
}
13531380

1381+
function bindThisPropertyAssignment(node: BinaryExpression) {
1382+
// Declare a 'member' in case it turns out the container was an ES5 class
1383+
if (container.kind === SyntaxKind.FunctionExpression || container.kind === SyntaxKind.FunctionDeclaration) {
1384+
container.symbol.members = container.symbol.members || {};
1385+
declareSymbol(container.symbol.members, container.symbol, node, SymbolFlags.Property, SymbolFlags.PropertyExcludes);
1386+
}
1387+
}
1388+
1389+
function bindPrototypePropertyAssignment(node: BinaryExpression) {
1390+
// We saw a node of the form 'x.prototype.y = z'. Declare a 'member' y on x if x was a function.
1391+
1392+
// Look up the function in the local scope, since prototype assignments should
1393+
// follow the function declaration
1394+
const classId = <Identifier>(<PropertyAccessExpression>(<PropertyAccessExpression>node.left).expression).expression;
1395+
const funcSymbol = container.locals[classId.text];
1396+
if (!funcSymbol || !(funcSymbol.flags & SymbolFlags.Function)) {
1397+
return;
1398+
}
1399+
1400+
// Set up the members collection if it doesn't exist already
1401+
if (!funcSymbol.members) {
1402+
funcSymbol.members = {};
1403+
}
1404+
1405+
// Declare the method/property
1406+
declareSymbol(funcSymbol.members, funcSymbol, <PropertyAccessExpression>node.left, SymbolFlags.Property, SymbolFlags.PropertyExcludes);
1407+
}
1408+
13541409
function bindCallExpression(node: CallExpression) {
13551410
// We're only inspecting call expressions to detect CommonJS modules, so we can skip
13561411
// this check if we've already seen the module indicator

Diff for: src/compiler/checker.ts

+39-4
Original file line numberDiff line numberDiff line change
@@ -2743,9 +2743,13 @@ namespace ts {
27432743
if (declaration.kind === SyntaxKind.BinaryExpression) {
27442744
return links.type = checkExpression((<BinaryExpression>declaration).right);
27452745
}
2746-
// Handle exports.p = expr
27472746
if (declaration.kind === SyntaxKind.PropertyAccessExpression) {
2748-
return checkExpressionCached((<BinaryExpression>declaration.parent).right);
2747+
// Declarations only exist for property access expressions for certain
2748+
// special assignment kinds
2749+
if (declaration.parent.kind === SyntaxKind.BinaryExpression) {
2750+
// Handle exports.p = expr or this.p = expr or className.prototype.method = expr
2751+
return links.type = checkExpressionCached((<BinaryExpression>declaration.parent).right);
2752+
}
27492753
}
27502754
// Handle variable, parameter or property
27512755
if (!pushTypeResolution(symbol, TypeSystemPropertyName.Type)) {
@@ -7021,6 +7025,23 @@ namespace ts {
70217025
const symbol = getSymbolOfNode(container.parent);
70227026
return container.flags & NodeFlags.Static ? getTypeOfSymbol(symbol) : (<InterfaceType>getDeclaredTypeOfSymbol(symbol)).thisType;
70237027
}
7028+
7029+
// If this is a function in a JS file, it might be a class method. Check if it's the RHS
7030+
// of a x.prototype.y = function [name]() { .... }
7031+
if (isInJavaScriptFile(node) && container.kind === SyntaxKind.FunctionExpression) {
7032+
if (getSpecialPropertyAssignmentKind(container.parent) === SpecialPropertyAssignmentKind.PrototypeProperty) {
7033+
// Get the 'x' of 'x.prototype.y = f' (here, 'f' is 'container')
7034+
const className = (((container.parent as BinaryExpression) // x.protoype.y = f
7035+
.left as PropertyAccessExpression) // x.prototype.y
7036+
.expression as PropertyAccessExpression) // x.prototype
7037+
.expression; // x
7038+
const classSymbol = checkExpression(className).symbol;
7039+
if (classSymbol && classSymbol.members && (classSymbol.flags & SymbolFlags.Function)) {
7040+
return getInferredClassType(classSymbol);
7041+
}
7042+
}
7043+
}
7044+
70247045
return anyType;
70257046
}
70267047

@@ -9742,6 +9763,14 @@ namespace ts {
97429763
return links.resolvedSignature;
97439764
}
97449765

9766+
function getInferredClassType(symbol: Symbol) {
9767+
const links = getSymbolLinks(symbol);
9768+
if (!links.inferredClassType) {
9769+
links.inferredClassType = createAnonymousType(undefined, symbol.members, emptyArray, emptyArray, /*stringIndexType*/ undefined, /*numberIndexType*/ undefined);
9770+
}
9771+
return links.inferredClassType;
9772+
}
9773+
97459774
/**
97469775
* Syntactically and semantically checks a call or new expression.
97479776
* @param node The call/new expression to be checked.
@@ -9763,8 +9792,14 @@ namespace ts {
97639792
declaration.kind !== SyntaxKind.ConstructSignature &&
97649793
declaration.kind !== SyntaxKind.ConstructorType) {
97659794

9766-
// When resolved signature is a call signature (and not a construct signature) the result type is any
9767-
if (compilerOptions.noImplicitAny) {
9795+
// When resolved signature is a call signature (and not a construct signature) the result type is any, unless
9796+
// the declaring function had members created through 'x.prototype.y = expr' or 'this.y = expr' psuedodeclarations
9797+
// in a JS file
9798+
const funcSymbol = checkExpression(node.expression).symbol;
9799+
if (funcSymbol && funcSymbol.members && (funcSymbol.flags & SymbolFlags.Function)) {
9800+
return getInferredClassType(funcSymbol);
9801+
}
9802+
else if (compilerOptions.noImplicitAny) {
97689803
error(node, Diagnostics.new_expression_whose_target_lacks_a_construct_signature_implicitly_has_an_any_type);
97699804
}
97709805
return anyType;

Diff for: src/compiler/types.ts

+14
Original file line numberDiff line numberDiff line change
@@ -2017,6 +2017,7 @@ namespace ts {
20172017
type?: Type; // Type of value symbol
20182018
declaredType?: Type; // Type of class, interface, enum, type alias, or type parameter
20192019
typeParameters?: TypeParameter[]; // Type parameters of type alias (undefined if non-generic)
2020+
inferredClassType?: Type; // Type of an inferred ES5 class
20202021
instantiations?: Map<Type>; // Instantiations of generic type alias (undefined if non-generic)
20212022
mapper?: TypeMapper; // Type mapper for instantiation alias
20222023
referenced?: boolean; // True if alias symbol has been referenced as a value
@@ -2316,6 +2317,19 @@ namespace ts {
23162317
// It is optional because in contextual signature instantiation, nothing fails
23172318
}
23182319

2320+
/* @internal */
2321+
export const enum SpecialPropertyAssignmentKind {
2322+
None,
2323+
/// exports.name = expr
2324+
ExportsProperty,
2325+
/// module.exports = expr
2326+
ModuleExports,
2327+
/// className.prototype.name = expr
2328+
PrototypeProperty,
2329+
/// this.name = expr
2330+
ThisProperty
2331+
}
2332+
23192333
export interface DiagnosticMessage {
23202334
key: string;
23212335
category: DiagnosticCategory;

Diff for: src/compiler/utilities.ts

+33-26
Original file line numberDiff line numberDiff line change
@@ -1068,33 +1068,40 @@ namespace ts {
10681068
(<CallExpression>expression).arguments[0].kind === SyntaxKind.StringLiteral;
10691069
}
10701070

1071-
/**
1072-
* Returns true if the node is an assignment to a property on the identifier 'exports'.
1073-
* This function does not test if the node is in a JavaScript file or not.
1074-
*/
1075-
export function isExportsPropertyAssignment(expression: Node): boolean {
1076-
// of the form 'exports.name = expr' where 'name' and 'expr' are arbitrary
1077-
return isInJavaScriptFile(expression) &&
1078-
(expression.kind === SyntaxKind.BinaryExpression) &&
1079-
((<BinaryExpression>expression).operatorToken.kind === SyntaxKind.EqualsToken) &&
1080-
((<BinaryExpression>expression).left.kind === SyntaxKind.PropertyAccessExpression) &&
1081-
((<PropertyAccessExpression>(<BinaryExpression>expression).left).expression.kind === SyntaxKind.Identifier) &&
1082-
((<Identifier>((<PropertyAccessExpression>(<BinaryExpression>expression).left).expression)).text === "exports");
1083-
}
1071+
/// Given a BinaryExpression, returns SpecialPropertyAssignmentKind for the various kinds of property
1072+
/// assignments we treat as special in the binder
1073+
export function getSpecialPropertyAssignmentKind(expression: Node): SpecialPropertyAssignmentKind {
1074+
if (expression.kind !== SyntaxKind.BinaryExpression) {
1075+
return SpecialPropertyAssignmentKind.None;
1076+
}
1077+
const expr = <BinaryExpression>expression;
1078+
if (expr.operatorToken.kind !== SyntaxKind.EqualsToken || expr.left.kind !== SyntaxKind.PropertyAccessExpression) {
1079+
return SpecialPropertyAssignmentKind.None;
1080+
}
1081+
const lhs = <PropertyAccessExpression>expr.left;
1082+
if (lhs.expression.kind === SyntaxKind.Identifier) {
1083+
const lhsId = <Identifier>lhs.expression;
1084+
if (lhsId.text === "exports") {
1085+
// exports.name = expr
1086+
return SpecialPropertyAssignmentKind.ExportsProperty;
1087+
}
1088+
else if (lhsId.text === "module" && lhs.name.text === "exports") {
1089+
// module.exports = expr
1090+
return SpecialPropertyAssignmentKind.ModuleExports;
1091+
}
1092+
}
1093+
else if (lhs.expression.kind === SyntaxKind.ThisKeyword) {
1094+
return SpecialPropertyAssignmentKind.ThisProperty;
1095+
}
1096+
else if (lhs.expression.kind === SyntaxKind.PropertyAccessExpression) {
1097+
// chained dot, e.g. x.y.z = expr; this var is the 'x.y' part
1098+
const innerPropertyAccess = <PropertyAccessExpression>lhs.expression;
1099+
if (innerPropertyAccess.expression.kind === SyntaxKind.Identifier && innerPropertyAccess.name.text === "prototype") {
1100+
return SpecialPropertyAssignmentKind.PrototypeProperty;
1101+
}
1102+
}
10841103

1085-
/**
1086-
* Returns true if the node is an assignment to the property access expression 'module.exports'.
1087-
* This function does not test if the node is in a JavaScript file or not.
1088-
*/
1089-
export function isModuleExportsAssignment(expression: Node): boolean {
1090-
// of the form 'module.exports = expr' where 'expr' is arbitrary
1091-
return isInJavaScriptFile(expression) &&
1092-
(expression.kind === SyntaxKind.BinaryExpression) &&
1093-
((<BinaryExpression>expression).operatorToken.kind === SyntaxKind.EqualsToken) &&
1094-
((<BinaryExpression>expression).left.kind === SyntaxKind.PropertyAccessExpression) &&
1095-
((<PropertyAccessExpression>(<BinaryExpression>expression).left).expression.kind === SyntaxKind.Identifier) &&
1096-
((<Identifier>((<PropertyAccessExpression>(<BinaryExpression>expression).left).expression)).text === "module") &&
1097-
((<PropertyAccessExpression>(<BinaryExpression>expression).left).name.text === "exports");
1104+
return SpecialPropertyAssignmentKind.None;
10981105
}
10991106

11001107
export function getExternalModuleName(node: Node): Expression {

Diff for: src/harness/fourslash.ts

+18-4
Original file line numberDiff line numberDiff line change
@@ -1162,7 +1162,7 @@ namespace FourSlash {
11621162

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

11681168
public printErrorList() {
@@ -1204,12 +1204,26 @@ namespace FourSlash {
12041204

12051205
public printMemberListMembers() {
12061206
const members = this.getMemberListAtCaret();
1207-
Harness.IO.log(JSON.stringify(members));
1207+
this.printMembersOrCompletions(members);
12081208
}
12091209

12101210
public printCompletionListMembers() {
12111211
const completions = this.getCompletionListAtCaret();
1212-
Harness.IO.log(JSON.stringify(completions));
1212+
this.printMembersOrCompletions(completions);
1213+
}
1214+
1215+
private printMembersOrCompletions(info: ts.CompletionInfo) {
1216+
function pad(s: string, length: number) {
1217+
return s + new Array(length - s.length + 1).join(" ");
1218+
}
1219+
function max<T>(arr: T[], selector: (x: T) => number): number {
1220+
return arr.reduce((prev, x) => Math.max(prev, selector(x)), 0);
1221+
}
1222+
const longestNameLength = max(info.entries, m => m.name.length);
1223+
const longestKindLength = max(info.entries, m => m.kind.length);
1224+
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);
1225+
const membersString = info.entries.map(m => `${pad(m.name, longestNameLength)} ${pad(m.kind, longestKindLength)} ${m.kindModifiers}`).join("\n");
1226+
Harness.IO.log(membersString);
12131227
}
12141228

12151229
public printReferences() {
@@ -3287,4 +3301,4 @@ namespace FourSlashInterface {
32873301
};
32883302
}
32893303
}
3290-
}
3304+
}

Diff for: tests/cases/fourslash/javaScriptPrototype1.ts

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
///<reference path="fourslash.ts" />
2+
3+
// Assignments to the 'prototype' property of a function create a class
4+
5+
// @allowNonTsExtensions: true
6+
// @Filename: myMod.js
7+
//// function myCtor(x) {
8+
//// }
9+
//// myCtor.prototype.foo = function() { return 32 };
10+
//// myCtor.prototype.bar = function() { return '' };
11+
////
12+
//// var m = new myCtor(10);
13+
//// m/*1*/
14+
//// var a = m.foo;
15+
//// a/*2*/
16+
//// var b = a();
17+
//// b/*3*/
18+
//// var c = m.bar();
19+
//// c/*4*/
20+
21+
22+
// Members of the class instance
23+
goTo.marker('1');
24+
edit.insert('.');
25+
verify.memberListContains('foo', undefined, undefined, 'property');
26+
verify.memberListContains('bar', undefined, undefined, 'property');
27+
edit.backspace();
28+
29+
// Members of a class method (1)
30+
goTo.marker('2');
31+
edit.insert('.');
32+
verify.memberListContains('length', undefined, undefined, 'property');
33+
edit.backspace();
34+
35+
// Members of the invocation of a class method (1)
36+
goTo.marker('3');
37+
edit.insert('.');
38+
verify.memberListContains('toFixed', undefined, undefined, 'method');
39+
verify.not.memberListContains('substr', undefined, undefined, 'method');
40+
edit.backspace();
41+
42+
// Members of the invocation of a class method (2)
43+
goTo.marker('4');
44+
edit.insert('.');
45+
verify.memberListContains('substr', undefined, undefined, 'method');
46+
verify.not.memberListContains('toFixed', undefined, undefined, 'method');

Diff for: tests/cases/fourslash/javaScriptPrototype2.ts

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
///<reference path="fourslash.ts" />
2+
3+
// Assignments to 'this' in the constructorish body create
4+
// properties with those names
5+
6+
// @allowNonTsExtensions: true
7+
// @Filename: myMod.js
8+
//// function myCtor(x) {
9+
//// this.qua = 10;
10+
//// }
11+
//// myCtor.prototype.foo = function() { return 32 };
12+
//// myCtor.prototype.bar = function() { return '' };
13+
////
14+
//// var m = new myCtor(10);
15+
//// m/*1*/
16+
//// var x = m.qua;
17+
//// x/*2*/
18+
//// myCtor/*3*/
19+
20+
// Verify the instance property exists
21+
goTo.marker('1');
22+
edit.insert('.');
23+
verify.completionListContains('qua', undefined, undefined, 'property');
24+
edit.backspace();
25+
26+
// Verify the type of the instance property
27+
goTo.marker('2');
28+
edit.insert('.');
29+
verify.completionListContains('toFixed', undefined, undefined, 'method');
30+
31+
goTo.marker('3');
32+
edit.insert('.');
33+
// Make sure symbols don't leak out into the constructor
34+
verify.completionListContains('qua', undefined, undefined, 'warning');
35+
verify.completionListContains('foo', undefined, undefined, 'warning');
36+
verify.completionListContains('bar', undefined, undefined, 'warning');

0 commit comments

Comments
 (0)