Merge pull request #5876 from RyanCavanaugh/javaScriptPrototypes
JavaScript prototype class inference
This commit is contained in:
commit
2f447ee005
10 changed files with 308 additions and 41 deletions
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
|
|
@ -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)) {
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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() {
|
||||
|
@ -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() {
|
||||
|
@ -3287,4 +3301,4 @@ namespace FourSlashInterface {
|
|||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
46
tests/cases/fourslash/javaScriptPrototype1.ts
Normal file
46
tests/cases/fourslash/javaScriptPrototype1.ts
Normal file
|
@ -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
tests/cases/fourslash/javaScriptPrototype2.ts
Normal file
36
tests/cases/fourslash/javaScriptPrototype2.ts
Normal file
|
@ -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');
|
20
tests/cases/fourslash/javaScriptPrototype3.ts
Normal file
20
tests/cases/fourslash/javaScriptPrototype3.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
///<reference path="fourslash.ts" />
|
||||
|
||||
// Inside an inferred method body, the type of 'this' is the class type
|
||||
|
||||
// @allowNonTsExtensions: true
|
||||
// @Filename: myMod.js
|
||||
//// function myCtor(x) {
|
||||
//// this.qua = 10;
|
||||
//// }
|
||||
//// myCtor.prototype.foo = function() { return this/**/; };
|
||||
//// myCtor.prototype.bar = function() { return '' };
|
||||
////
|
||||
|
||||
goTo.marker();
|
||||
edit.insert('.');
|
||||
|
||||
// Check members of the function
|
||||
verify.completionListContains('foo', undefined, undefined, 'property');
|
||||
verify.completionListContains('bar', undefined, undefined, 'property');
|
||||
verify.completionListContains('qua', undefined, undefined, 'property');
|
21
tests/cases/fourslash/javaScriptPrototype4.ts
Normal file
21
tests/cases/fourslash/javaScriptPrototype4.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
///<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('foo', undefined, undefined, 'warning');
|
||||
verify.completionListContains('bar', undefined, undefined, 'warning');
|
||||
verify.completionListContains('qua', undefined, undefined, 'warning');
|
19
tests/cases/fourslash/javaScriptPrototype5.ts
Normal file
19
tests/cases/fourslash/javaScriptPrototype5.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
///<reference path="fourslash.ts" />
|
||||
|
||||
// No prototype assignments are needed to enable class inference
|
||||
|
||||
// @allowNonTsExtensions: true
|
||||
// @Filename: myMod.js
|
||||
//// function myCtor() {
|
||||
//// this.foo = 'hello';
|
||||
//// this.bar = 10;
|
||||
//// }
|
||||
//// let x = new myCtor();
|
||||
//// x/**/
|
||||
|
||||
goTo.marker();
|
||||
edit.insert('.');
|
||||
|
||||
// Check members of the function
|
||||
verify.completionListContains('foo', undefined, undefined, 'property');
|
||||
verify.completionListContains('bar', undefined, undefined, 'property');
|
Loading…
Reference in a new issue