From b31b45f584282175281a04c4e465115e4dde6c6a Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Mon, 26 Oct 2015 15:42:25 -0700 Subject: [PATCH] JavaScript class inference from prototype property assignment --- src/compiler/binder.ts | 32 ++++++++++++++ src/compiler/checker.ts | 43 +++++++++++++------ src/compiler/types.ts | 2 + src/compiler/utilities.ts | 29 +++++++++++++ src/services/services.ts | 1 + tests/cases/fourslash/javaScriptPrototype1.ts | 36 ++++++++++++++++ 6 files changed, 129 insertions(+), 14 deletions(-) create mode 100644 tests/cases/fourslash/javaScriptPrototype1.ts diff --git a/src/compiler/binder.ts b/src/compiler/binder.ts index d5ab7dd11ca..bfef27a9102 100644 --- a/src/compiler/binder.ts +++ b/src/compiler/binder.ts @@ -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(node); } + else if (isPrototypePropertyAssignment(node)) { + bindPrototypePropertyAssignment(node); + } } return checkStrictModeBinaryExpression(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 = ((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 = (node.left).name; + let prototypeSymbol = declareSymbol(funcSymbol.members, funcSymbol, (node.left).expression, SymbolFlags.HasMembers, SymbolFlags.None); + declareSymbol(prototypeSymbol.members, prototypeSymbol, 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 diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index ac5056bf769..24da884a330 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -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 = 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); diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 5e8de9a3fb4..5232209e7d1 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -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 diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 9eff4370ec2..05184e29f58 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -1086,6 +1086,35 @@ namespace ts { (((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) && + ((expression).operatorToken.kind === SyntaxKind.EqualsToken) && + isNamedPropertyAccess((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 || (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((expression.left).expression, "prototype") && + ((expression.left).expression).expression.kind === SyntaxKind.Identifier; + } + export function getExternalModuleName(node: Node): Expression { if (node.kind === SyntaxKind.ImportDeclaration) { return (node).moduleSpecifier; diff --git a/src/services/services.ts b/src/services/services.ts index 30d993cb6b6..2c2bbb8e73e 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -735,6 +735,7 @@ namespace ts { declaration: SignatureDeclaration; typeParameters: TypeParameter[]; parameters: Symbol[]; + kind: SignatureKind; resolvedReturnType: Type; minArgumentCount: number; hasRestParameter: boolean; diff --git a/tests/cases/fourslash/javaScriptPrototype1.ts b/tests/cases/fourslash/javaScriptPrototype1.ts new file mode 100644 index 00000000000..0dec8d721d6 --- /dev/null +++ b/tests/cases/fourslash/javaScriptPrototype1.ts @@ -0,0 +1,36 @@ +/// + +// 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');