From b31b45f584282175281a04c4e465115e4dde6c6a Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Mon, 26 Oct 2015 15:42:25 -0700 Subject: [PATCH 01/13] 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 d5ab7dd11ca9c..bfef27a9102f3 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 ac5056bf769a4..24da884a33087 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 5e8de9a3fb411..5232209e7d198 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 9eff4370ec22a..05184e29f58c8 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 30d993cb6b634..2c2bbb8e73e88 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 0000000000000..0dec8d721d6f4 --- /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'); From bc3d95c0a41c833802d0939ecda081a167565b02 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Tue, 27 Oct 2015 17:17:31 -0700 Subject: [PATCH 02/13] JS class members as methods --- src/compiler/binder.ts | 2 +- src/compiler/checker.ts | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/compiler/binder.ts b/src/compiler/binder.ts index bfef27a9102f3..85882c495b680 100644 --- a/src/compiler/binder.ts +++ b/src/compiler/binder.ts @@ -1063,7 +1063,7 @@ namespace ts { // 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); + declareSymbol(prototypeSymbol.members, prototypeSymbol, node.left, SymbolFlags.Method, SymbolFlags.None); } function bindCallExpression(node: CallExpression) { diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 24da884a33087..f0bdd48afa7a7 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -3843,6 +3843,11 @@ namespace ts { } } result.push(getSignatureFromDeclaration(node)); + break; + + case SyntaxKind.PropertyAccessExpression: + // Class inference from ClassName.prototype.methodName = expr + return getSignaturesOfType(checkExpressionCached((node.parent).right), SignatureKind.Call); } } return result; From 3b7213116d324585dc73548dc2289f8513b1884c Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Fri, 30 Oct 2015 12:34:56 -0700 Subject: [PATCH 03/13] Inference from JavaScript `prototype` property assignments --- src/compiler/binder.ts | 58 ++++++++++++++----- src/compiler/checker.ts | 31 +++++++--- src/compiler/types.ts | 13 +++++ src/compiler/utilities.ts | 36 ++++++++++++ tests/cases/fourslash/javaScriptPrototype1.ts | 26 ++++++--- tests/cases/fourslash/javaScriptPrototype2.ts | 36 ++++++++++++ tests/cases/fourslash/javaScriptPrototype3.ts | 40 +++++++++++++ tests/cases/fourslash/javaScriptPrototype4.ts | 36 ++++++++++++ 8 files changed, 247 insertions(+), 29 deletions(-) create mode 100644 tests/cases/fourslash/javaScriptPrototype2.ts create mode 100644 tests/cases/fourslash/javaScriptPrototype3.ts create mode 100644 tests/cases/fourslash/javaScriptPrototype4.ts diff --git a/src/compiler/binder.ts b/src/compiler/binder.ts index 85882c495b680..9bd51f0c7633b 100644 --- a/src/compiler/binder.ts +++ b/src/compiler/binder.ts @@ -167,8 +167,21 @@ namespace ts { case SyntaxKind.ExportAssignment: return (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; @@ -862,14 +875,20 @@ namespace ts { return checkStrictModeIdentifier(node); case SyntaxKind.BinaryExpression: if (isJavaScriptFile) { - if (isExportsPropertyAssignment(node)) { - bindExportsPropertyAssignment(node); - } - else if (isModuleExportsAssignment(node)) { - bindModuleExportsAssignment(node); - } - else if (isPrototypePropertyAssignment(node)) { - bindPrototypePropertyAssignment(node); + let specialKind = getSpecialPropertyAssignmentKind(node); + switch (specialKind) { + case SpecialPropertyAssignmentKind.ExportsProperty: + bindExportsPropertyAssignment(node); + break; + case SpecialPropertyAssignmentKind.ModuleExports: + bindModuleExportsAssignment(node); + break; + case SpecialPropertyAssignmentKind.PrototypeProperty: + bindPrototypePropertyAssignment(node); + break; + case SpecialPropertyAssignmentKind.ThisProperty: + bindThisPropertyAssignment(node); + break; } } return checkStrictModeBinaryExpression(node); @@ -1038,6 +1057,13 @@ namespace ts { bindExportAssignment(node); } + function bindThisPropertyAssignment(node: BinaryExpression) { + if (container.kind === SyntaxKind.FunctionExpression || container.kind === SyntaxKind.FunctionDeclaration) { + container.symbol.members = container.symbol.members || {}; + declareClassMember(node, SymbolFlags.Property, SymbolFlags.PropertyExcludes); + } + } + 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 @@ -1054,16 +1080,20 @@ namespace ts { // The function is now a constructor rather than a normal function if (!funcSymbol.inferredConstructor) { - funcSymbol.flags = (funcSymbol.flags | SymbolFlags.Class) & ~SymbolFlags.Function; + declareSymbol(container.locals, funcSymbol, funcSymbol.valueDeclaration, SymbolFlags.Class, SymbolFlags.None); + // 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); + // Declare the 'prototype' member of the function + let prototypeSymbol = declareSymbol(funcSymbol.exports, funcSymbol, (node.left).expression, SymbolFlags.ObjectLiteral | SymbolFlags.Property, SymbolFlags.None); + + // Declare the property on the prototype symbol declareSymbol(prototypeSymbol.members, prototypeSymbol, node.left, SymbolFlags.Method, SymbolFlags.None); + // and on the class type + declareSymbol(funcSymbol.members, funcSymbol, node.left, SymbolFlags.Method, SymbolFlags.PropertyExcludes); } function bindCallExpression(node: CallExpression) { diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index f0bdd48afa7a7..fb7040a96ab16 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -2576,9 +2576,16 @@ namespace ts { if (declaration.kind === SyntaxKind.BinaryExpression) { return links.type = checkExpression((declaration).right); } - // Handle exports.p = expr if (declaration.kind === SyntaxKind.PropertyAccessExpression) { - return checkExpressionCached((declaration.parent).right); + if (declaration.parent.kind === SyntaxKind.BinaryExpression) { + // Handle exports.p = expr or this.p = expr or className.prototype.method = expr + return links.type = checkExpression((declaration.parent).right); + } + else { + // Declaration for className.prototype in inferred JS class + let type = createAnonymousType(symbol, symbol.members, emptyArray, emptyArray, undefined, undefined); + return links.type = type; + } } // Handle variable, parameter or property if (!pushTypeResolution(symbol, TypeSystemPropertyName.Type)) { @@ -3799,8 +3806,15 @@ namespace ts { 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); + let members = createSymbolTable(emptyArray); + // Collect methods declared with className.protoype.methodName = ... + let proto = declaration.symbol.exports["prototype"]; + if (proto) { + mergeSymbolTable(members, proto.members); + } + // Collect properties defined in the constructor by this.propName = ... + mergeSymbolTable(members, declaration.symbol.members); + returnType = createAnonymousType(declaration.symbol, members, emptyArray, emptyArray, undefined, undefined); } else { kind = SignatureKind.Call; @@ -3846,8 +3860,8 @@ namespace ts { break; case SyntaxKind.PropertyAccessExpression: - // Class inference from ClassName.prototype.methodName = expr - return getSignaturesOfType(checkExpressionCached((node.parent).right), SignatureKind.Call); + result = getSignaturesOfType(checkExpressionCached((node.parent).right), SignatureKind.Call); + break; } } return result; @@ -6957,7 +6971,10 @@ namespace ts { let operator = binaryExpression.operatorToken.kind; if (operator >= SyntaxKind.FirstAssignment && operator <= SyntaxKind.LastAssignment) { // In an assignment expression, the right operand is contextually typed by the type of the left operand. - if (node === binaryExpression.right) { + // In JS files where a special assignment is taking place, don't contextually type the RHS to avoid + // incorrectly assuming a circular 'any' (the type of the LHS is determined by the RHS) + if (node === binaryExpression.right && + !(node.parserContextFlags & ParserContextFlags.JavaScriptFile && getSpecialPropertyAssignmentKind(binaryExpression))) { return checkExpression(binaryExpression.left); } } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 5232209e7d198..00375f8a574d4 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -2013,6 +2013,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; diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 05184e29f58c8..cb72e87bb9402 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -1057,6 +1057,42 @@ namespace ts { (expression).arguments[0].kind === SyntaxKind.StringLiteral; } + /// 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 = expression; + if (expr.operatorToken.kind !== SyntaxKind.EqualsToken || expr.left.kind !== SyntaxKind.PropertyAccessExpression) { + return SpecialPropertyAssignmentKind.None; + } + const lhs = expr.left; + if (lhs.expression.kind === SyntaxKind.Identifier) { + const lhsId = 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 + let innerPropertyAccess = lhs.expression; + if (innerPropertyAccess.expression.kind === SyntaxKind.Identifier && innerPropertyAccess.name.text === "prototype") { + return SpecialPropertyAssignmentKind.PrototypeProperty; + } + } + + return SpecialPropertyAssignmentKind.None; + } + /** * 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. diff --git a/tests/cases/fourslash/javaScriptPrototype1.ts b/tests/cases/fourslash/javaScriptPrototype1.ts index 0dec8d721d6f4..0473d18aae873 100644 --- a/tests/cases/fourslash/javaScriptPrototype1.ts +++ b/tests/cases/fourslash/javaScriptPrototype1.ts @@ -11,26 +11,36 @@ //// //// var m = new myCtor(10); //// m/*1*/ -//// var x = m.foo(); -//// x/*2*/ -//// var y = m.bar(); -//// y/*3*/ +//// 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, 'method'); -edit.insert('foo'); - -edit.backspace(); +verify.memberListContains('bar', undefined, undefined, 'method'); 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(); -goTo.marker('3'); +// 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'); diff --git a/tests/cases/fourslash/javaScriptPrototype2.ts b/tests/cases/fourslash/javaScriptPrototype2.ts new file mode 100644 index 0000000000000..ab6afff3d2ce7 --- /dev/null +++ b/tests/cases/fourslash/javaScriptPrototype2.ts @@ -0,0 +1,36 @@ +/// + +// 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'); diff --git a/tests/cases/fourslash/javaScriptPrototype3.ts b/tests/cases/fourslash/javaScriptPrototype3.ts new file mode 100644 index 0000000000000..900a39ddce9eb --- /dev/null +++ b/tests/cases/fourslash/javaScriptPrototype3.ts @@ -0,0 +1,40 @@ +/// + +// ES6 classes can extend from JS classes + +// @allowNonTsExtensions: true +// @Filename: myMod.js +//// function myCtor(x) { +//// this.qua = 10; +//// } +//// myCtor.prototype.foo = function() { return 32 }; +//// myCtor.prototype.bar = function() { return '' }; +//// +//// class MyClass extends myCtor { +//// fn() { +//// this/*1*/ +//// let y = super.foo(); +//// y; +//// } +//// } +//// var n = new MyClass(3); +//// n/*2*/; + +goTo.marker('1'); +edit.insert('.'); +// Current class method +verify.completionListContains('fn', undefined, undefined, 'method'); +// Base class method +verify.completionListContains('foo', undefined, undefined, 'method'); +// Base class instance property +verify.completionListContains('qua', undefined, undefined, 'property'); +edit.backspace(); + +// Derived class instance from outside the class +goTo.marker('2'); +edit.insert('.'); +verify.completionListContains('fn', undefined, undefined, 'method'); +// Base class method +verify.completionListContains('foo', undefined, undefined, 'method'); +// Base class instance property +verify.completionListContains('qua', undefined, undefined, 'property'); diff --git a/tests/cases/fourslash/javaScriptPrototype4.ts b/tests/cases/fourslash/javaScriptPrototype4.ts new file mode 100644 index 0000000000000..78eab19733a49 --- /dev/null +++ b/tests/cases/fourslash/javaScriptPrototype4.ts @@ -0,0 +1,36 @@ +/// + +// 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('prototype', undefined, undefined, 'property'); +verify.completionListContains('foo', undefined, undefined, 'warning'); +verify.completionListContains('bar', undefined, undefined, 'warning'); +verify.completionListContains('qua', undefined, undefined, 'warning'); + +// Check members of function.prototype +edit.insert('prototype.'); +debugger; +debug.printMemberListMembers(); +verify.completionListContains('foo', undefined, undefined, 'method'); +verify.completionListContains('bar', undefined, undefined, 'method'); +verify.completionListContains('qua', undefined, undefined, 'warning'); +verify.completionListContains('prototype', undefined, undefined, 'warning'); + +// debug.printErrorList(); +// debug.printCurrentQuickInfo(); +// edit.insert('.'); +// verify.completionListContains('toFixed', undefined, undefined, 'method'); From a3a5c1619d6371cd9b093f71ab83abc1d485d856 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Fri, 30 Oct 2015 12:35:20 -0700 Subject: [PATCH 04/13] Human-readable fourslash debug output for completion lists / quickinfo --- src/harness/fourslash.ts | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/harness/fourslash.ts b/src/harness/fourslash.ts index dd2dde6989c28..8564714801a33 100644 --- a/src/harness/fourslash.ts +++ b/src/harness/fourslash.ts @@ -1265,7 +1265,7 @@ namespace FourSlash { public printCurrentQuickInfo() { let 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() { @@ -1307,12 +1307,26 @@ namespace FourSlash { public printMemberListMembers() { let members = this.getMemberListAtCaret(); - Harness.IO.log(JSON.stringify(members)); + this.printMembersOrCompletions(members); } public printCompletionListMembers() { let 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(arr: T[], selector: (x: T) => number): number { + return arr.reduce((prev, x) => Math.max(prev, selector(x)), 0); + } + let longestNameLength = max(info.entries, m => m.name.length); + let 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); + let membersString = info.entries.map(m => `${pad(m.name, longestNameLength)} ${pad(m.kind, longestKindLength)} ${m.kindModifiers}`).join("\n"); + Harness.IO.log(membersString); } public printReferences() { From eaeeb1f7624f9be5287544659debdbda40df8824 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Mon, 9 Nov 2015 15:19:41 -0800 Subject: [PATCH 05/13] Fix TC --- tests/cases/fourslash/javaScriptPrototype4.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/cases/fourslash/javaScriptPrototype4.ts b/tests/cases/fourslash/javaScriptPrototype4.ts index 78eab19733a49..f78bf52438bdf 100644 --- a/tests/cases/fourslash/javaScriptPrototype4.ts +++ b/tests/cases/fourslash/javaScriptPrototype4.ts @@ -23,14 +23,9 @@ verify.completionListContains('qua', undefined, undefined, 'warning'); // Check members of function.prototype edit.insert('prototype.'); -debugger; debug.printMemberListMembers(); verify.completionListContains('foo', undefined, undefined, 'method'); verify.completionListContains('bar', undefined, undefined, 'method'); verify.completionListContains('qua', undefined, undefined, 'warning'); verify.completionListContains('prototype', undefined, undefined, 'warning'); -// debug.printErrorList(); -// debug.printCurrentQuickInfo(); -// edit.insert('.'); -// verify.completionListContains('toFixed', undefined, undefined, 'method'); From fb83ee0a3036687115f505d621a8022eaca95e07 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Fri, 20 Nov 2015 10:59:13 -0800 Subject: [PATCH 06/13] WIP --- src/compiler/binder.ts | 20 ++++--- src/compiler/checker.ts | 5 +- src/compiler/utilities.ts | 58 ------------------- tests/cases/fourslash/javaScriptPrototype4.ts | 1 - 4 files changed, 16 insertions(+), 68 deletions(-) diff --git a/src/compiler/binder.ts b/src/compiler/binder.ts index 9bd51f0c7633b..f91cf10774946 100644 --- a/src/compiler/binder.ts +++ b/src/compiler/binder.ts @@ -635,7 +635,6 @@ 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) { @@ -889,6 +888,11 @@ namespace ts { case SpecialPropertyAssignmentKind.ThisProperty: bindThisPropertyAssignment(node); break; + case SpecialPropertyAssignmentKind.None: + // Nothing to do + break; + default: + Debug.fail("Unknown special property assignment kind"); } } return checkStrictModeBinaryExpression(node); @@ -1080,19 +1084,19 @@ namespace ts { // The function is now a constructor rather than a normal function if (!funcSymbol.inferredConstructor) { + // Have the binder set up all the related class symbols for us declareSymbol(container.locals, funcSymbol, funcSymbol.valueDeclaration, SymbolFlags.Class, SymbolFlags.None); - // funcSymbol.flags = (funcSymbol.flags | SymbolFlags.Class) & ~SymbolFlags.Function; - funcSymbol.members = funcSymbol.members || {}; + // funcSymbol.members = funcSymbol.members || {}; funcSymbol.members["__constructor"] = funcSymbol; funcSymbol.inferredConstructor = true; } - // Declare the 'prototype' member of the function - let prototypeSymbol = declareSymbol(funcSymbol.exports, funcSymbol, (node.left).expression, SymbolFlags.ObjectLiteral | SymbolFlags.Property, SymbolFlags.None); + // Get the exports of the class so we can add the method to it + let funcExports = declareSymbol(funcSymbol.exports, funcSymbol, (node.left).expression, SymbolFlags.ObjectLiteral | SymbolFlags.Property, SymbolFlags.None); - // Declare the property on the prototype symbol - declareSymbol(prototypeSymbol.members, prototypeSymbol, node.left, SymbolFlags.Method, SymbolFlags.None); - // and on the class type + // Declare the method + declareSymbol(funcExports.members, funcExports, node.left, SymbolFlags.Method, SymbolFlags.None); + // and on the members of the function so it appears in 'prototype' declareSymbol(funcSymbol.members, funcSymbol, node.left, SymbolFlags.Method, SymbolFlags.PropertyExcludes); } diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index fb7040a96ab16..18a878ae91be2 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -2577,9 +2577,11 @@ namespace ts { return links.type = checkExpression((declaration).right); } if (declaration.kind === SyntaxKind.PropertyAccessExpression) { + // 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 = checkExpression((declaration.parent).right); + return links.type = checkExpressionCached((declaration.parent).right); } else { // Declaration for className.prototype in inferred JS class @@ -3860,6 +3862,7 @@ namespace ts { break; case SyntaxKind.PropertyAccessExpression: + // Inferred class method result = getSignaturesOfType(checkExpressionCached((node.parent).right), SignatureKind.Call); break; } diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index cb72e87bb9402..603f5a3d93c97 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -1093,64 +1093,6 @@ namespace ts { return SpecialPropertyAssignmentKind.None; } - /** - * 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) && - ((expression).operatorToken.kind === SyntaxKind.EqualsToken) && - ((expression).left.kind === SyntaxKind.PropertyAccessExpression) && - (((expression).left).expression.kind === SyntaxKind.Identifier) && - (((((expression).left).expression)).text === "exports"); - } - - /** - * 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) && - ((expression).operatorToken.kind === SyntaxKind.EqualsToken) && - ((expression).left.kind === SyntaxKind.PropertyAccessExpression) && - (((expression).left).expression.kind === SyntaxKind.Identifier) && - (((((expression).left).expression)).text === "module") && - (((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/tests/cases/fourslash/javaScriptPrototype4.ts b/tests/cases/fourslash/javaScriptPrototype4.ts index f78bf52438bdf..3ed723740b489 100644 --- a/tests/cases/fourslash/javaScriptPrototype4.ts +++ b/tests/cases/fourslash/javaScriptPrototype4.ts @@ -23,7 +23,6 @@ verify.completionListContains('qua', undefined, undefined, 'warning'); // Check members of function.prototype edit.insert('prototype.'); -debug.printMemberListMembers(); verify.completionListContains('foo', undefined, undefined, 'method'); verify.completionListContains('bar', undefined, undefined, 'method'); verify.completionListContains('qua', undefined, undefined, 'warning'); From c4b0b62bfc694928ed79858d8e9bcb8f2790bb05 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Tue, 1 Dec 2015 15:06:53 -0800 Subject: [PATCH 07/13] Merge fixup --- src/compiler/binder.ts | 10 +++++----- src/compiler/checker.ts | 21 ++++++++++++++------- src/compiler/utilities.ts | 2 +- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/compiler/binder.ts b/src/compiler/binder.ts index 7f0fd2fb3fa50..86fcc809d5850 100644 --- a/src/compiler/binder.ts +++ b/src/compiler/binder.ts @@ -1178,8 +1178,8 @@ namespace ts { case SyntaxKind.Identifier: return checkStrictModeIdentifier(node); case SyntaxKind.BinaryExpression: - if (isJavaScriptFile) { - let specialKind = getSpecialPropertyAssignmentKind(node); + if (isInJavaScriptFile(node)) { + const specialKind = getSpecialPropertyAssignmentKind(node); switch (specialKind) { case SpecialPropertyAssignmentKind.ExportsProperty: bindExportsPropertyAssignment(node); @@ -1378,11 +1378,11 @@ namespace ts { // 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; + const 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]; + const funcSymbol = container.locals[classId.text]; if (!funcSymbol) { return; } @@ -1397,7 +1397,7 @@ namespace ts { } // Get the exports of the class so we can add the method to it - let funcExports = declareSymbol(funcSymbol.exports, funcSymbol, (node.left).expression, SymbolFlags.ObjectLiteral | SymbolFlags.Property, SymbolFlags.None); + const funcExports = declareSymbol(funcSymbol.exports, funcSymbol, (node.left).expression, SymbolFlags.ObjectLiteral | SymbolFlags.Property, SymbolFlags.None); // Declare the method declareSymbol(funcExports.members, funcExports, node.left, SymbolFlags.Method, SymbolFlags.None); diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 29e31a84a230f..94c13fd1f3f77 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -123,8 +123,8 @@ namespace ts { const noConstraintType = createAnonymousType(undefined, emptySymbols, emptyArray, emptyArray, undefined, undefined); - const anySignature = createSignature(undefined, undefined, emptyArray, undefined, anyType, undefined, 0, false, false); - const unknownSignature = createSignature(undefined, undefined, emptyArray, undefined, unknownType, undefined, 0, false, false); + const anySignature = createSignature(undefined, undefined, emptyArray, undefined, anyType, undefined, 0, /*hasRestParameter*/ false, /*hasStringLiterals*/ false); + const unknownSignature = createSignature(undefined, undefined, emptyArray, undefined, unknownType, undefined, 0, /*hasRestParameter*/ false, /*hasStringLiterals*/ false); const globals: SymbolTable = {}; @@ -2661,7 +2661,7 @@ namespace ts { } else { // Declaration for className.prototype in inferred JS class - let type = createAnonymousType(symbol, symbol.members, emptyArray, emptyArray, undefined, undefined); + const type = createAnonymousType(symbol, symbol.members, emptyArray, emptyArray, undefined, undefined); return links.type = type; } } @@ -3930,9 +3930,9 @@ namespace ts { default: if (declaration.symbol.inferredConstructor) { kind = SignatureKind.Construct; - let members = createSymbolTable(emptyArray); + const members = createSymbolTable(emptyArray); // Collect methods declared with className.protoype.methodName = ... - let proto = declaration.symbol.exports["prototype"]; + const proto = declaration.symbol.exports["prototype"]; if (proto) { mergeSymbolTable(members, proto.members); } @@ -3954,7 +3954,7 @@ namespace ts { function getSignaturesOfSymbol(symbol: Symbol): Signature[] { if (!symbol) return emptyArray; - const result: Signature[] = []; + let result: Signature[] = []; for (let i = 0, len = symbol.declarations.length; i < len; i++) { const node = symbol.declarations[i]; switch (node.kind) { @@ -6841,7 +6841,14 @@ namespace ts { let container: Node; if (symbol.flags & SymbolFlags.Class) { // get parent of class declaration - container = getClassLikeDeclarationOfSymbol(symbol).parent; + const classDeclaration = getClassLikeDeclarationOfSymbol(symbol); + if (classDeclaration) { + container = classDeclaration.parent; + } + else { + // JS-inferred class; do nothing + return; + } } else { // nesting structure: diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 553c5bbeff03b..cffc1e90a0601 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -1090,7 +1090,7 @@ namespace ts { } else if (lhs.expression.kind === SyntaxKind.PropertyAccessExpression) { // chained dot, e.g. x.y.z = expr; this var is the 'x.y' part - let innerPropertyAccess = lhs.expression; + const innerPropertyAccess = lhs.expression; if (innerPropertyAccess.expression.kind === SyntaxKind.Identifier && innerPropertyAccess.name.text === "prototype") { return SpecialPropertyAssignmentKind.PrototypeProperty; } From fabc43d0d401795531f7597eae70d209d0bb0e33 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Fri, 4 Dec 2015 14:11:56 -0800 Subject: [PATCH 08/13] JS Prototypes WIP --- src/compiler/binder.ts | 33 +++++++++++---------------------- src/compiler/types.ts | 1 - 2 files changed, 11 insertions(+), 23 deletions(-) diff --git a/src/compiler/binder.ts b/src/compiler/binder.ts index 86fcc809d5850..eedcd456b84cd 100644 --- a/src/compiler/binder.ts +++ b/src/compiler/binder.ts @@ -1367,42 +1367,31 @@ namespace ts { } 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 || {}; - declareClassMember(node, SymbolFlags.Property, SymbolFlags.PropertyExcludes); + 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'. - // 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 - const classId = ((node.left).expression).expression; + // 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 immediately + // Look up the function in the local scope, since prototype assignments should // follow the function declaration + const classId = ((node.left).expression).expression; const funcSymbol = container.locals[classId.text]; - if (!funcSymbol) { + if (!funcSymbol || !(funcSymbol.flags & SymbolFlags.Function)) { return; } - // The function is now a constructor rather than a normal function - if (!funcSymbol.inferredConstructor) { - // Have the binder set up all the related class symbols for us - declareSymbol(container.locals, funcSymbol, funcSymbol.valueDeclaration, SymbolFlags.Class, SymbolFlags.None); - // funcSymbol.members = funcSymbol.members || {}; - funcSymbol.members["__constructor"] = funcSymbol; - funcSymbol.inferredConstructor = true; + // Set up the members collection if it doesn't exist already + if (!funcSymbol.members) { + funcSymbol.members = {}; } - // Get the exports of the class so we can add the method to it - const funcExports = declareSymbol(funcSymbol.exports, funcSymbol, (node.left).expression, SymbolFlags.ObjectLiteral | SymbolFlags.Property, SymbolFlags.None); - - // Declare the method - declareSymbol(funcExports.members, funcExports, node.left, SymbolFlags.Method, SymbolFlags.None); - // and on the members of the function so it appears in 'prototype' - declareSymbol(funcSymbol.members, funcSymbol, node.left, SymbolFlags.Method, SymbolFlags.PropertyExcludes); + // Declare the method/property + declareSymbol(funcSymbol.members, funcSymbol, node.left, SymbolFlags.Property, SymbolFlags.PropertyExcludes); } function bindCallExpression(node: CallExpression) { diff --git a/src/compiler/types.ts b/src/compiler/types.ts index daec64c0dac07..16e5e4b57feb4 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -1985,7 +1985,6 @@ 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 */ From fcd00a59d2b213bf469ed891e95ca9ab0d6c73a4 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Fri, 4 Dec 2015 14:58:32 -0800 Subject: [PATCH 09/13] Simplified JS prototype class inference --- src/compiler/checker.ts | 82 ++++++------------- src/compiler/types.ts | 1 - src/services/services.ts | 1 - tests/cases/fourslash/javaScriptPrototype1.ts | 4 +- tests/cases/fourslash/javaScriptPrototype3.ts | 40 --------- tests/cases/fourslash/javaScriptPrototype4.ts | 9 -- 6 files changed, 27 insertions(+), 110 deletions(-) delete mode 100644 tests/cases/fourslash/javaScriptPrototype3.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 22b45926caded..dc2d6649013c2 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -123,8 +123,8 @@ namespace ts { const noConstraintType = createAnonymousType(undefined, emptySymbols, emptyArray, emptyArray, undefined, undefined); - const anySignature = createSignature(undefined, undefined, emptyArray, undefined, anyType, undefined, 0, /*hasRestParameter*/ false, /*hasStringLiterals*/ false); - const unknownSignature = createSignature(undefined, undefined, emptyArray, undefined, unknownType, undefined, 0, /*hasRestParameter*/ false, /*hasStringLiterals*/ false); + const anySignature = createSignature(undefined, undefined, emptyArray, anyType, undefined, 0, /*hasRestParameter*/ false, /*hasStringLiterals*/ false); + const unknownSignature = createSignature(undefined, undefined, emptyArray, unknownType, undefined, 0, /*hasRestParameter*/ false, /*hasStringLiterals*/ false); const globals: SymbolTable = {}; @@ -3374,13 +3374,12 @@ namespace ts { resolveObjectTypeMembers(type, source, typeParameters, typeArguments); } - function createSignature(declaration: SignatureDeclaration, typeParameters: TypeParameter[], parameters: Symbol[], kind: SignatureKind, + function createSignature(declaration: SignatureDeclaration, typeParameters: TypeParameter[], parameters: Symbol[], resolvedReturnType: Type, typePredicate: TypePredicate, minArgumentCount: number, hasRestParameter: boolean, hasStringLiterals: boolean): Signature { const 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; @@ -3390,13 +3389,13 @@ namespace ts { } function cloneSignature(sig: Signature): Signature { - return createSignature(sig.declaration, sig.typeParameters, sig.parameters, sig.kind, sig.resolvedReturnType, sig.typePredicate, + return createSignature(sig.declaration, sig.typeParameters, sig.parameters, sig.resolvedReturnType, sig.typePredicate, sig.minArgumentCount, sig.hasRestParameter, sig.hasStringLiterals); } function getDefaultConstructSignatures(classType: InterfaceType): Signature[] { if (!hasClassBaseType(classType)) { - return [createSignature(undefined, classType.localTypeParameters, emptyArray, SignatureKind.Construct, classType, undefined, 0, /*hasRestParameter*/ false, /*hasStringLiterals*/ false)]; + return [createSignature(undefined, classType.localTypeParameters, emptyArray, classType, undefined, 0, /*hasRestParameter*/ false, /*hasStringLiterals*/ false)]; } const baseConstructorType = getBaseConstructorTypeOfClass(classType); const baseSignatures = getSignaturesOfType(baseConstructorType, SignatureKind.Construct); @@ -3932,33 +3931,7 @@ namespace ts { } } - 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; - const members = createSymbolTable(emptyArray); - // Collect methods declared with className.protoype.methodName = ... - const proto = declaration.symbol.exports["prototype"]; - if (proto) { - mergeSymbolTable(members, proto.members); - } - // Collect properties defined in the constructor by this.propName = ... - mergeSymbolTable(members, declaration.symbol.members); - returnType = createAnonymousType(declaration.symbol, members, emptyArray, emptyArray, undefined, undefined); - } - else { - kind = SignatureKind.Call; - } - break; - } - - links.resolvedSignature = createSignature(declaration, typeParameters, parameters, kind, returnType, typePredicate, + links.resolvedSignature = createSignature(declaration, typeParameters, parameters, returnType, typePredicate, minArgumentCount, hasRestParameter(declaration), hasStringLiterals); } return links.resolvedSignature; @@ -3966,7 +3939,7 @@ namespace ts { function getSignaturesOfSymbol(symbol: Symbol): Signature[] { if (!symbol) return emptyArray; - let result: Signature[] = []; + const result: Signature[] = []; for (let i = 0, len = symbol.declarations.length; i < len; i++) { const node = symbol.declarations[i]; switch (node.kind) { @@ -3993,12 +3966,6 @@ namespace ts { } } result.push(getSignatureFromDeclaration(node)); - break; - - case SyntaxKind.PropertyAccessExpression: - // Inferred class method - result = getSignaturesOfType(checkExpressionCached((node.parent).right), SignatureKind.Call); - break; } } return result; @@ -4081,7 +4048,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) { - const isConstructor = signature.kind === SignatureKind.Construct; + const isConstructor = signature.declaration.kind === SyntaxKind.Constructor || signature.declaration.kind === SyntaxKind.ConstructSignature; const type = createObjectType(TypeFlags.Anonymous | TypeFlags.FromSignature); type.members = emptySymbols; type.properties = emptyArray; @@ -4784,7 +4751,6 @@ namespace ts { } const result = createSignature(signature.declaration, freshTypeParameters, instantiateList(signature.parameters, mapper, instantiateSymbol), - signature.kind, instantiateType(signature.resolvedReturnType, mapper), freshTypePredicate, signature.minArgumentCount, signature.hasRestParameter, signature.hasStringLiterals); @@ -6838,14 +6804,7 @@ namespace ts { let container: Node; if (symbol.flags & SymbolFlags.Class) { // get parent of class declaration - const classDeclaration = getClassLikeDeclarationOfSymbol(symbol); - if (classDeclaration) { - container = classDeclaration.parent; - } - else { - // JS-inferred class; do nothing - return; - } + container = getClassLikeDeclarationOfSymbol(symbol).parent; } else { // nesting structure: @@ -7189,10 +7148,7 @@ namespace ts { const operator = binaryExpression.operatorToken.kind; if (operator >= SyntaxKind.FirstAssignment && operator <= SyntaxKind.LastAssignment) { // In an assignment expression, the right operand is contextually typed by the type of the left operand. - // In JS files where a special assignment is taking place, don't contextually type the RHS to avoid - // incorrectly assuming a circular 'any' (the type of the LHS is determined by the RHS) - if (node === binaryExpression.right && - !(node.parserContextFlags & ParserContextFlags.JavaScriptFile && getSpecialPropertyAssignmentKind(binaryExpression))) { + if (node === binaryExpression.right) { return checkExpression(binaryExpression.left); } } @@ -9676,9 +9632,21 @@ namespace ts { return voidType; } if (node.kind === SyntaxKind.NewExpression) { - 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) { + const declaration = signature.declaration; + + if (declaration && + declaration.kind !== SyntaxKind.Constructor && + 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, 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 createAnonymousType(undefined, funcSymbol.members, emptyArray, emptyArray, /*stringIndexType*/ undefined, /*numberIndexType*/ undefined); + } + else if (compilerOptions.noImplicitAny) { error(node, Diagnostics.new_expression_whose_target_lacks_a_construct_signature_implicitly_has_an_any_type); } return anyType; diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 169ce4acfda80..34e862768d0e6 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -2237,7 +2237,6 @@ 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/services/services.ts b/src/services/services.ts index e145406ac18b7..4e0a0265b9193 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -741,7 +741,6 @@ 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 index 0473d18aae873..c9c454cfb2c7d 100644 --- a/tests/cases/fourslash/javaScriptPrototype1.ts +++ b/tests/cases/fourslash/javaScriptPrototype1.ts @@ -22,8 +22,8 @@ // Members of the class instance goTo.marker('1'); edit.insert('.'); -verify.memberListContains('foo', undefined, undefined, 'method'); -verify.memberListContains('bar', undefined, undefined, 'method'); +verify.memberListContains('foo', undefined, undefined, 'property'); +verify.memberListContains('bar', undefined, undefined, 'property'); edit.backspace(); // Members of a class method (1) diff --git a/tests/cases/fourslash/javaScriptPrototype3.ts b/tests/cases/fourslash/javaScriptPrototype3.ts deleted file mode 100644 index 900a39ddce9eb..0000000000000 --- a/tests/cases/fourslash/javaScriptPrototype3.ts +++ /dev/null @@ -1,40 +0,0 @@ -/// - -// ES6 classes can extend from JS classes - -// @allowNonTsExtensions: true -// @Filename: myMod.js -//// function myCtor(x) { -//// this.qua = 10; -//// } -//// myCtor.prototype.foo = function() { return 32 }; -//// myCtor.prototype.bar = function() { return '' }; -//// -//// class MyClass extends myCtor { -//// fn() { -//// this/*1*/ -//// let y = super.foo(); -//// y; -//// } -//// } -//// var n = new MyClass(3); -//// n/*2*/; - -goTo.marker('1'); -edit.insert('.'); -// Current class method -verify.completionListContains('fn', undefined, undefined, 'method'); -// Base class method -verify.completionListContains('foo', undefined, undefined, 'method'); -// Base class instance property -verify.completionListContains('qua', undefined, undefined, 'property'); -edit.backspace(); - -// Derived class instance from outside the class -goTo.marker('2'); -edit.insert('.'); -verify.completionListContains('fn', undefined, undefined, 'method'); -// Base class method -verify.completionListContains('foo', undefined, undefined, 'method'); -// Base class instance property -verify.completionListContains('qua', undefined, undefined, 'property'); diff --git a/tests/cases/fourslash/javaScriptPrototype4.ts b/tests/cases/fourslash/javaScriptPrototype4.ts index 3ed723740b489..81cb5fe378424 100644 --- a/tests/cases/fourslash/javaScriptPrototype4.ts +++ b/tests/cases/fourslash/javaScriptPrototype4.ts @@ -16,15 +16,6 @@ goTo.marker('1'); edit.insert('.'); // Check members of the function -verify.completionListContains('prototype', undefined, undefined, 'property'); verify.completionListContains('foo', undefined, undefined, 'warning'); verify.completionListContains('bar', undefined, undefined, 'warning'); verify.completionListContains('qua', undefined, undefined, 'warning'); - -// Check members of function.prototype -edit.insert('prototype.'); -verify.completionListContains('foo', undefined, undefined, 'method'); -verify.completionListContains('bar', undefined, undefined, 'method'); -verify.completionListContains('qua', undefined, undefined, 'warning'); -verify.completionListContains('prototype', undefined, undefined, 'warning'); - From c97dffff3b2590e92311fccf2bf3744599972370 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Mon, 7 Dec 2015 11:55:30 -0800 Subject: [PATCH 10/13] Support 'this' in inferred method bodies --- src/compiler/checker.ts | 17 ++++++++++++++++ tests/cases/fourslash/javaScriptPrototype3.ts | 20 +++++++++++++++++++ tests/cases/fourslash/javaScriptPrototype5.ts | 19 ++++++++++++++++++ 3 files changed, 56 insertions(+) create mode 100644 tests/cases/fourslash/javaScriptPrototype3.ts create mode 100644 tests/cases/fourslash/javaScriptPrototype5.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index dc2d6649013c2..79f514e786475 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -6897,6 +6897,23 @@ namespace ts { const symbol = getSymbolOfNode(container.parent); return container.flags & NodeFlags.Static ? getTypeOfSymbol(symbol) : (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.members) { + return createAnonymousType(undefined, classSymbol.members, emptyArray, emptyArray, /*stringIndexType*/ undefined, /*numberIndexType*/ undefined); + } + } + } + return anyType; } diff --git a/tests/cases/fourslash/javaScriptPrototype3.ts b/tests/cases/fourslash/javaScriptPrototype3.ts new file mode 100644 index 0000000000000..7b3f63eca57f1 --- /dev/null +++ b/tests/cases/fourslash/javaScriptPrototype3.ts @@ -0,0 +1,20 @@ +/// + +// 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'); diff --git a/tests/cases/fourslash/javaScriptPrototype5.ts b/tests/cases/fourslash/javaScriptPrototype5.ts new file mode 100644 index 0000000000000..a0125e4751213 --- /dev/null +++ b/tests/cases/fourslash/javaScriptPrototype5.ts @@ -0,0 +1,19 @@ +/// + +// 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'); From 50892acfd855b0a0ae42a4e2f2186fd11948753f Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Wed, 9 Dec 2015 11:16:57 -0800 Subject: [PATCH 11/13] Address CR feedback --- src/compiler/checker.ts | 17 ++++++++++------- src/compiler/types.ts | 1 + 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 79f514e786475..5e8fd45510e07 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -2671,11 +2671,6 @@ namespace ts { // Handle exports.p = expr or this.p = expr or className.prototype.method = expr return links.type = checkExpressionCached((declaration.parent).right); } - else { - // Declaration for className.prototype in inferred JS class - const type = createAnonymousType(symbol, symbol.members, emptyArray, emptyArray, undefined, undefined); - return links.type = type; - } } // Handle variable, parameter or property if (!pushTypeResolution(symbol, TypeSystemPropertyName.Type)) { @@ -6908,7 +6903,7 @@ namespace ts { .expression as PropertyAccessExpression) // x.prototype .expression; // x const classSymbol = checkExpression(className).symbol; - if (classSymbol.members) { + if (classSymbol && classSymbol.members) { return createAnonymousType(undefined, classSymbol.members, emptyArray, emptyArray, /*stringIndexType*/ undefined, /*numberIndexType*/ undefined); } } @@ -9635,6 +9630,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. @@ -9661,7 +9664,7 @@ namespace ts { // in a JS file const funcSymbol = checkExpression(node.expression).symbol; if (funcSymbol && funcSymbol.members && (funcSymbol.flags & SymbolFlags.Function)) { - return createAnonymousType(undefined, funcSymbol.members, emptyArray, emptyArray, /*stringIndexType*/ undefined, /*numberIndexType*/ undefined); + return getInferredClassType(funcSymbol); } else 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 34e862768d0e6..2e557ad09a460 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -1997,6 +1997,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; // 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 From fcfc424b491bc139a02a1e4b09c0fb92f932b631 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Wed, 9 Dec 2015 11:18:40 -0800 Subject: [PATCH 12/13] One more --- src/compiler/checker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 5e8fd45510e07..7e2bf85c59b22 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -6904,7 +6904,7 @@ namespace ts { .expression; // x const classSymbol = checkExpression(className).symbol; if (classSymbol && classSymbol.members) { - return createAnonymousType(undefined, classSymbol.members, emptyArray, emptyArray, /*stringIndexType*/ undefined, /*numberIndexType*/ undefined); + return getInferredClassType(classSymbol); } } } From 37f3ff8d05efc6b3717bc7ead63bc746d509c3ee Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Mon, 14 Dec 2015 11:32:06 -0800 Subject: [PATCH 13/13] Check for function flag on class symbol --- src/compiler/checker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 7e2bf85c59b22..5bfa474422446 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -6903,7 +6903,7 @@ namespace ts { .expression as PropertyAccessExpression) // x.prototype .expression; // x const classSymbol = checkExpression(className).symbol; - if (classSymbol && classSymbol.members) { + if (classSymbol && classSymbol.members && (classSymbol.flags & SymbolFlags.Function)) { return getInferredClassType(classSymbol); } }