diff --git a/src/compiler/binder.ts b/src/compiler/binder.ts index 7561783d1398b..eedcd456b84cd 100644 --- a/src/compiler/binder.ts +++ b/src/compiler/binder.ts @@ -222,8 +222,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; @@ -1166,11 +1179,25 @@ namespace ts { return checkStrictModeIdentifier(node); case SyntaxKind.BinaryExpression: if (isInJavaScriptFile(node)) { - if (isExportsPropertyAssignment(node)) { - bindExportsPropertyAssignment(node); - } - else if (isModuleExportsAssignment(node)) { - bindModuleExportsAssignment(node); + const 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; + case SpecialPropertyAssignmentKind.None: + // Nothing to do + break; + default: + Debug.fail("Unknown special property assignment kind"); } } return checkStrictModeBinaryExpression(node); @@ -1339,6 +1366,34 @@ namespace ts { bindExportAssignment(node); } + function bindThisPropertyAssignment(node: BinaryExpression) { + // Declare a 'member' in case it turns out the container was an ES5 class + if (container.kind === SyntaxKind.FunctionExpression || container.kind === SyntaxKind.FunctionDeclaration) { + container.symbol.members = container.symbol.members || {}; + declareSymbol(container.symbol.members, container.symbol, node, SymbolFlags.Property, SymbolFlags.PropertyExcludes); + } + } + + function bindPrototypePropertyAssignment(node: BinaryExpression) { + // We saw a node of the form 'x.prototype.y = z'. Declare a 'member' y on x if x was a function. + + // Look up the function in the local scope, since prototype assignments should + // follow the function declaration + const classId = ((node.left).expression).expression; + const funcSymbol = container.locals[classId.text]; + if (!funcSymbol || !(funcSymbol.flags & SymbolFlags.Function)) { + return; + } + + // Set up the members collection if it doesn't exist already + if (!funcSymbol.members) { + funcSymbol.members = {}; + } + + // Declare the method/property + declareSymbol(funcSymbol.members, funcSymbol, node.left, SymbolFlags.Property, SymbolFlags.PropertyExcludes); + } + function bindCallExpression(node: CallExpression) { // We're only inspecting call expressions to detect CommonJS modules, so we can skip // this check if we've already seen the module indicator diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 6d69c45dbce98..5bfa474422446 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -2664,9 +2664,13 @@ 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); + // Declarations only exist for property access expressions for certain + // special assignment kinds + if (declaration.parent.kind === SyntaxKind.BinaryExpression) { + // Handle exports.p = expr or this.p = expr or className.prototype.method = expr + return links.type = checkExpressionCached((declaration.parent).right); + } } // Handle variable, parameter or property if (!pushTypeResolution(symbol, TypeSystemPropertyName.Type)) { @@ -6888,6 +6892,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 && classSymbol.members && (classSymbol.flags & SymbolFlags.Function)) { + return getInferredClassType(classSymbol); + } + } + } + return anyType; } @@ -9609,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. @@ -9630,8 +9659,14 @@ namespace ts { declaration.kind !== SyntaxKind.ConstructSignature && declaration.kind !== SyntaxKind.ConstructorType) { - // When resolved signature is a call signature (and not a construct signature) the result type is any - if (compilerOptions.noImplicitAny) { + // When resolved signature is a call signature (and not a construct signature) the result type is any, unless + // the declaring function had members created through 'x.prototype.y = expr' or 'this.y = expr' psuedodeclarations + // in a JS file + const funcSymbol = checkExpression(node.expression).symbol; + if (funcSymbol && funcSymbol.members && (funcSymbol.flags & SymbolFlags.Function)) { + return getInferredClassType(funcSymbol); + } + else if (compilerOptions.noImplicitAny) { error(node, Diagnostics.new_expression_whose_target_lacks_a_construct_signature_implicitly_has_an_any_type); } return anyType; diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 9411437c9815f..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 @@ -2290,6 +2291,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 95bf4ff7fa32b..6730f38a84896 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -1063,33 +1063,40 @@ namespace ts { (expression).arguments[0].kind === SyntaxKind.StringLiteral; } - /** - * Returns true if the node is an assignment to a property on the identifier 'exports'. - * This function does not test if the node is in a JavaScript file or not. - */ - export function isExportsPropertyAssignment(expression: Node): boolean { - // of the form 'exports.name = expr' where 'name' and 'expr' are arbitrary - return isInJavaScriptFile(expression) && - (expression.kind === SyntaxKind.BinaryExpression) && - ((expression).operatorToken.kind === SyntaxKind.EqualsToken) && - ((expression).left.kind === SyntaxKind.PropertyAccessExpression) && - (((expression).left).expression.kind === SyntaxKind.Identifier) && - (((((expression).left).expression)).text === "exports"); - } + /// Given a BinaryExpression, returns SpecialPropertyAssignmentKind for the various kinds of property + /// assignments we treat as special in the binder + export function getSpecialPropertyAssignmentKind(expression: Node): SpecialPropertyAssignmentKind { + if (expression.kind !== SyntaxKind.BinaryExpression) { + return SpecialPropertyAssignmentKind.None; + } + const expr = 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 + const innerPropertyAccess = lhs.expression; + if (innerPropertyAccess.expression.kind === SyntaxKind.Identifier && innerPropertyAccess.name.text === "prototype") { + return SpecialPropertyAssignmentKind.PrototypeProperty; + } + } - /** - * Returns true if the node is an assignment to the property access expression 'module.exports'. - * This function does not test if the node is in a JavaScript file or not. - */ - export function isModuleExportsAssignment(expression: Node): boolean { - // of the form 'module.exports = expr' where 'expr' is arbitrary - return isInJavaScriptFile(expression) && - (expression.kind === SyntaxKind.BinaryExpression) && - ((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"); + return SpecialPropertyAssignmentKind.None; } export function getExternalModuleName(node: Node): Expression { diff --git a/src/harness/fourslash.ts b/src/harness/fourslash.ts index 9d7293457816b..24f3319285b22 100644 --- a/src/harness/fourslash.ts +++ b/src/harness/fourslash.ts @@ -1162,7 +1162,7 @@ namespace FourSlash { public printCurrentQuickInfo() { const quickInfo = this.languageService.getQuickInfoAtPosition(this.activeFile.fileName, this.currentCaretPosition); - Harness.IO.log(JSON.stringify(quickInfo)); + Harness.IO.log("Quick Info: " + quickInfo.displayParts.map(part => part.text).join("")); } public printErrorList() { @@ -1204,12 +1204,26 @@ namespace FourSlash { public printMemberListMembers() { const members = this.getMemberListAtCaret(); - Harness.IO.log(JSON.stringify(members)); + this.printMembersOrCompletions(members); } public printCompletionListMembers() { const completions = this.getCompletionListAtCaret(); - Harness.IO.log(JSON.stringify(completions)); + this.printMembersOrCompletions(completions); + } + + private printMembersOrCompletions(info: ts.CompletionInfo) { + function pad(s: string, length: number) { + return s + new Array(length - s.length + 1).join(" "); + } + function max(arr: T[], selector: (x: T) => number): number { + return arr.reduce((prev, x) => Math.max(prev, selector(x)), 0); + } + const longestNameLength = max(info.entries, m => m.name.length); + const longestKindLength = max(info.entries, m => m.kind.length); + info.entries.sort((m, n) => m.sortText > n.sortText ? 1 : m.sortText < n.sortText ? -1 : m.name > n.name ? 1 : m.name < n.name ? -1 : 0); + const membersString = info.entries.map(m => `${pad(m.name, longestNameLength)} ${pad(m.kind, longestKindLength)} ${m.kindModifiers}`).join("\n"); + Harness.IO.log(membersString); } public printReferences() { @@ -3287,4 +3301,4 @@ namespace FourSlashInterface { }; } } -} +} diff --git a/tests/cases/fourslash/javaScriptPrototype1.ts b/tests/cases/fourslash/javaScriptPrototype1.ts new file mode 100644 index 0000000000000..c9c454cfb2c7d --- /dev/null +++ b/tests/cases/fourslash/javaScriptPrototype1.ts @@ -0,0 +1,46 @@ +/// + +// Assignments to the 'prototype' property of a function create a class + +// @allowNonTsExtensions: true +// @Filename: myMod.js +//// function myCtor(x) { +//// } +//// myCtor.prototype.foo = function() { return 32 }; +//// myCtor.prototype.bar = function() { return '' }; +//// +//// var m = new myCtor(10); +//// m/*1*/ +//// var a = m.foo; +//// a/*2*/ +//// var b = a(); +//// b/*3*/ +//// var c = m.bar(); +//// c/*4*/ + + +// Members of the class instance +goTo.marker('1'); +edit.insert('.'); +verify.memberListContains('foo', undefined, undefined, 'property'); +verify.memberListContains('bar', undefined, undefined, 'property'); +edit.backspace(); + +// Members of a class method (1) +goTo.marker('2'); +edit.insert('.'); +verify.memberListContains('length', undefined, undefined, 'property'); +edit.backspace(); + +// Members of the invocation of a class method (1) +goTo.marker('3'); +edit.insert('.'); +verify.memberListContains('toFixed', undefined, undefined, 'method'); +verify.not.memberListContains('substr', undefined, undefined, 'method'); +edit.backspace(); + +// Members of the invocation of a class method (2) +goTo.marker('4'); +edit.insert('.'); +verify.memberListContains('substr', undefined, undefined, 'method'); +verify.not.memberListContains('toFixed', undefined, undefined, 'method'); 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..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/javaScriptPrototype4.ts b/tests/cases/fourslash/javaScriptPrototype4.ts new file mode 100644 index 0000000000000..81cb5fe378424 --- /dev/null +++ b/tests/cases/fourslash/javaScriptPrototype4.ts @@ -0,0 +1,21 @@ +/// + +// Check for any odd symbol leakage + +// @allowNonTsExtensions: true +// @Filename: myMod.js +//// function myCtor(x) { +//// this.qua = 10; +//// } +//// myCtor.prototype.foo = function() { return 32 }; +//// myCtor.prototype.bar = function() { return '' }; +//// +//// myCtor/*1*/ + +goTo.marker('1'); +edit.insert('.'); + +// Check members of the function +verify.completionListContains('foo', undefined, undefined, 'warning'); +verify.completionListContains('bar', undefined, undefined, 'warning'); +verify.completionListContains('qua', undefined, undefined, 'warning'); 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');