JavaScript class inference from prototype property assignment

This commit is contained in:
Ryan Cavanaugh
2015-10-26 15:42:25 -07:00
parent eda6eca3c7
commit b31b45f584
6 changed files with 129 additions and 14 deletions
+32
View File
@@ -622,6 +622,7 @@ namespace ts {
function bindAnonymousDeclaration(node: Declaration, symbolFlags: SymbolFlags, name: string) {
let symbol = createSymbol(symbolFlags, name);
addDeclarationToSymbol(symbol, node, symbolFlags);
return symbol;
}
function bindBlockScopedDeclaration(node: Declaration, symbolFlags: SymbolFlags, symbolExcludes: SymbolFlags) {
@@ -867,6 +868,9 @@ namespace ts {
else if (isModuleExportsAssignment(node)) {
bindModuleExportsAssignment(<BinaryExpression>node);
}
else if (isPrototypePropertyAssignment(node)) {
bindPrototypePropertyAssignment(node);
}
}
return checkStrictModeBinaryExpression(<BinaryExpression>node);
case SyntaxKind.CatchClause:
@@ -1034,6 +1038,34 @@ namespace ts {
bindExportAssignment(node);
}
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
// adds a member 'y' to the result of that constructor function
// Get 'x', the class
let classId = <Identifier>(<PropertyAccessExpression>(<PropertyAccessExpression>node.left).expression).expression;
// Look up the function in the local scope, since prototype assignments should immediately
// follow the function declaration
let funcSymbol = container.locals[classId.text];
if (!funcSymbol) {
return;
}
// The function is now a constructor rather than a normal function
if (!funcSymbol.inferredConstructor) {
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);
declareSymbol(prototypeSymbol.members, prototypeSymbol, <PropertyAccessExpression>node.left, SymbolFlags.Method | SymbolFlags.Property, SymbolFlags.None);
}
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
+29 -14
View File
@@ -122,8 +122,8 @@ namespace ts {
let noConstraintType = createAnonymousType(undefined, emptySymbols, emptyArray, emptyArray, undefined, undefined);
let anySignature = createSignature(undefined, undefined, emptyArray, anyType, undefined, 0, false, false);
let unknownSignature = createSignature(undefined, undefined, emptyArray, unknownType, undefined, 0, false, false);
let anySignature = createSignature(undefined, undefined, emptyArray, undefined, anyType, undefined, 0, false, false);
let unknownSignature = createSignature(undefined, undefined, emptyArray, undefined, unknownType, undefined, 0, false, false);
let globals: SymbolTable = {};
@@ -3247,12 +3247,13 @@ namespace ts {
resolveObjectTypeMembers(type, source, typeParameters, typeArguments);
}
function createSignature(declaration: SignatureDeclaration, typeParameters: TypeParameter[], parameters: Symbol[],
function createSignature(declaration: SignatureDeclaration, typeParameters: TypeParameter[], parameters: Symbol[], kind: SignatureKind,
resolvedReturnType: Type, typePredicate: TypePredicate, minArgumentCount: number, hasRestParameter: boolean, hasStringLiterals: boolean): Signature {
let sig = new Signature(checker);
sig.declaration = declaration;
sig.typeParameters = typeParameters;
sig.parameters = parameters;
sig.kind = kind;
sig.resolvedReturnType = resolvedReturnType;
sig.typePredicate = typePredicate;
sig.minArgumentCount = minArgumentCount;
@@ -3262,13 +3263,13 @@ namespace ts {
}
function cloneSignature(sig: Signature): Signature {
return createSignature(sig.declaration, sig.typeParameters, sig.parameters, sig.resolvedReturnType, sig.typePredicate,
return createSignature(sig.declaration, sig.typeParameters, sig.parameters, sig.kind, sig.resolvedReturnType, sig.typePredicate,
sig.minArgumentCount, sig.hasRestParameter, sig.hasStringLiterals);
}
function getDefaultConstructSignatures(classType: InterfaceType): Signature[] {
if (!getBaseTypes(classType).length) {
return [createSignature(undefined, classType.localTypeParameters, emptyArray, classType, undefined, 0, false, false)];
return [createSignature(undefined, classType.localTypeParameters, emptyArray, SignatureKind.Construct, classType, undefined, 0, false, false)];
}
let baseConstructorType = getBaseConstructorTypeOfClass(classType);
let baseSignatures = getSignaturesOfType(baseConstructorType, SignatureKind.Construct);
@@ -3788,7 +3789,26 @@ namespace ts {
}
}
links.resolvedSignature = createSignature(declaration, typeParameters, parameters, returnType, typePredicate,
let kind: SignatureKind;
switch (declaration.kind) {
case SyntaxKind.Constructor:
case SyntaxKind.ConstructSignature:
case SyntaxKind.ConstructorType:
kind = SignatureKind.Construct;
break;
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);
}
else {
kind = SignatureKind.Call;
}
break;
}
links.resolvedSignature = createSignature(declaration, typeParameters, parameters, kind, returnType, typePredicate,
minArgumentCount, hasRestParameter(declaration), hasStringLiterals);
}
return links.resolvedSignature;
@@ -3905,7 +3925,7 @@ namespace ts {
// object type literal or interface (using the new keyword). Each way of declaring a constructor
// will result in a different declaration kind.
if (!signature.isolatedSignatureType) {
let isConstructor = signature.declaration.kind === SyntaxKind.Constructor || signature.declaration.kind === SyntaxKind.ConstructSignature;
let isConstructor = signature.kind === SignatureKind.Construct;
let type = <ResolvedType>createObjectType(TypeFlags.Anonymous | TypeFlags.FromSignature);
type.members = emptySymbols;
type.properties = emptyArray;
@@ -4611,6 +4631,7 @@ namespace ts {
}
let result = createSignature(signature.declaration, freshTypeParameters,
instantiateList(signature.parameters, mapper, instantiateSymbol),
signature.kind,
instantiateType(signature.resolvedReturnType, mapper),
freshTypePredicate,
signature.minArgumentCount, signature.hasRestParameter, signature.hasStringLiterals);
@@ -9359,13 +9380,7 @@ namespace ts {
return voidType;
}
if (node.kind === SyntaxKind.NewExpression) {
let declaration = signature.declaration;
if (declaration &&
declaration.kind !== SyntaxKind.Constructor &&
declaration.kind !== SyntaxKind.ConstructSignature &&
declaration.kind !== SyntaxKind.ConstructorType) {
if (signature.kind === SignatureKind.Call) {
// When resolved signature is a call signature (and not a construct signature) the result type is any
if (compilerOptions.noImplicitAny) {
error(node, Diagnostics.new_expression_whose_target_lacks_a_construct_signature_implicitly_has_an_any_type);
+2
View File
@@ -1714,6 +1714,7 @@ namespace ts {
/* @internal */ parent?: Symbol; // Parent symbol
/* @internal */ exportSymbol?: Symbol; // Exported symbol associated with this symbol
/* @internal */ constEnumOnlyModule?: boolean; // True if module contains only const enums or other modules with only const enums
/* @internal */ inferredConstructor?: boolean; // A function promoted to constructor as the result of a prototype property assignment
}
/* @internal */
@@ -1958,6 +1959,7 @@ namespace ts {
declaration: SignatureDeclaration; // Originating declaration
typeParameters: TypeParameter[]; // Type parameters (undefined if non-generic)
parameters: Symbol[]; // Parameters
kind: SignatureKind; // Call or Construct
typePredicate?: TypePredicate; // Type predicate
/* @internal */
resolvedReturnType: Type; // Resolved return type
+29
View File
@@ -1086,6 +1086,35 @@ namespace ts {
((<PropertyAccessExpression>(<BinaryExpression>expression).left).name.text === "exports");
}
/**
* Returns true if this expression is an assignment to the given named property
*/
function isAssignmentToProperty(expression: Node, name?: string): expression is BinaryExpression {
return (expression.kind === SyntaxKind.BinaryExpression) &&
((<BinaryExpression>expression).operatorToken.kind === SyntaxKind.EqualsToken) &&
isNamedPropertyAccess((<BinaryExpression>expression).left, name);
}
/**
* Returns true if this expression is a PropertyAccessExpression where the property name is the provided name
*/
function isNamedPropertyAccess(expression: Node, name?: string): expression is PropertyAccessExpression {
return expression.kind === SyntaxKind.PropertyAccessExpression &&
(!name || (<PropertyAccessExpression>expression).name.text === name);
}
/**
* Returns true if the node is an assignment in the form 'id1.prototype.id2 = expr' where id1 and id2
* are any identifier.
* This function does not test if the node is in a JavaScript file or not.
*/
export function isPrototypePropertyAssignment(expression: Node): expression is BinaryExpression {
return isAssignmentToProperty(expression) &&
isNamedPropertyAccess(expression.left) &&
isNamedPropertyAccess((<PropertyAccessExpression>expression.left).expression, "prototype") &&
(<PropertyAccessExpression>(<PropertyAccessExpression>expression.left).expression).expression.kind === SyntaxKind.Identifier;
}
export function getExternalModuleName(node: Node): Expression {
if (node.kind === SyntaxKind.ImportDeclaration) {
return (<ImportDeclaration>node).moduleSpecifier;
+1
View File
@@ -735,6 +735,7 @@ namespace ts {
declaration: SignatureDeclaration;
typeParameters: TypeParameter[];
parameters: Symbol[];
kind: SignatureKind;
resolvedReturnType: Type;
minArgumentCount: number;
hasRestParameter: boolean;
@@ -0,0 +1,36 @@
///<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 x = m.foo();
//// x/*2*/
//// var y = m.bar();
//// y/*3*/
goTo.marker('1');
edit.insert('.');
verify.memberListContains('foo', undefined, undefined, 'method');
edit.insert('foo');
edit.backspace();
edit.backspace();
goTo.marker('2');
edit.insert('.');
verify.memberListContains('toFixed', undefined, undefined, 'method');
verify.not.memberListContains('substr', undefined, undefined, 'method');
edit.backspace();
goTo.marker('3');
edit.insert('.');
verify.memberListContains('substr', undefined, undefined, 'method');
verify.not.memberListContains('toFixed', undefined, undefined, 'method');