diff --git a/src/compiler/binder.ts b/src/compiler/binder.ts index 6957be484f35a..dc0ff55b89aa0 100644 --- a/src/compiler/binder.ts +++ b/src/compiler/binder.ts @@ -1478,6 +1478,14 @@ namespace ts { // It's acceptable for multiple 'this' assignments of the same identifier to occur declareSymbol(container.symbol.members, container.symbol, node, SymbolFlags.Property, SymbolFlags.PropertyExcludes & ~SymbolFlags.Property); } + else if (container.kind === SyntaxKind.Constructor && isInJavaScriptFile(node)) { + // this.foo assignment in a JavaScript class + // Bind this property to the containing class + const saveContainer = container; + container = container.parent; + bindPropertyOrMethodOrAccessor(node, SymbolFlags.Property, SymbolFlags.None); + container = saveContainer; + } } function bindPrototypePropertyAssignment(node: BinaryExpression) { diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 27b0839720559..49efcc7dc0b61 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -2937,6 +2937,14 @@ namespace ts { } // Handle module.exports = expr if (declaration.kind === SyntaxKind.BinaryExpression) { + // Use JS Doc type if present on parent expression statement + if (declaration.flags & NodeFlags.JavaScriptFile) { + const typeTag = getJSDocTypeTag(declaration.parent); + if (typeTag && typeTag.typeExpression) { + return links.type = getTypeFromTypeNode(typeTag.typeExpression.type); + } + } + return links.type = getUnionType(map(symbol.declarations, (decl: BinaryExpression) => checkExpressionCached(decl.right))); } if (declaration.kind === SyntaxKind.PropertyAccessExpression) { @@ -8037,6 +8045,11 @@ namespace ts { const binaryExpression = node.parent; const operator = binaryExpression.operatorToken.kind; if (operator >= SyntaxKind.FirstAssignment && operator <= SyntaxKind.LastAssignment) { + // Don't do this for special property assignments to avoid circularity + if (getSpecialPropertyAssignmentKind(binaryExpression) !== SpecialPropertyAssignmentKind.None) { + return undefined; + } + // In an assignment expression, the right operand is contextually typed by the type of the left operand. if (node === binaryExpression.right) { return checkExpression(binaryExpression.left); diff --git a/tests/cases/fourslash/javaScriptClass1.ts b/tests/cases/fourslash/javaScriptClass1.ts new file mode 100644 index 0000000000000..19f7a39b6150d --- /dev/null +++ b/tests/cases/fourslash/javaScriptClass1.ts @@ -0,0 +1,31 @@ +/// + +// Classes have their shape inferred from assignments +// to properties of 'this' in the constructor + +// @allowNonTsExtensions: true +// @Filename: Foo.js +//// class Foo { +//// constructor() { +//// this.bar = 'world'; +//// this.thing = () => 0; +//// this.union = 'foo'; +//// this.union = 100; +//// } +//// } +//// var x = new Foo(); +//// x/**/ + + +goTo.marker(); +edit.insert('.'); +verify.completionListContains("bar", /*displayText*/ undefined, /*documentation*/ undefined, "property"); +verify.completionListContains("thing", /*displayText*/ undefined, /*documentation*/ undefined, "property"); +verify.completionListContains("union", /*displayText*/ undefined, /*documentation*/ undefined, "property"); + +edit.insert('bar.'); +verify.completionListContains("substr", /*displayText*/ undefined, /*documentation*/ undefined, "method"); +edit.backspace('bar.'.length); + +edit.insert('union.'); +verify.completionListContains("toString", /*displayText*/ undefined, /*documentation*/ undefined, "method"); \ No newline at end of file diff --git a/tests/cases/fourslash/javaScriptClass2.ts b/tests/cases/fourslash/javaScriptClass2.ts new file mode 100644 index 0000000000000..d6daa320c5a87 --- /dev/null +++ b/tests/cases/fourslash/javaScriptClass2.ts @@ -0,0 +1,22 @@ +/// + +// In an inferred class, we can rename successfully + +// @allowNonTsExtensions: true +// @Filename: Foo.js +//// class Foo { +//// constructor() { +//// this.[|union|] = 'foo'; +//// this./*1*/[|union|] = 100; +//// } +//// method() { return this./*2*/[|union|]; } +//// } +//// var x = new Foo(); +//// x./*3*/[|union|]; + +goTo.marker('1'); +verify.renameLocations(/*findInStrings*/false, /*findInComments*/false); +goTo.marker('2'); +verify.renameLocations(/*findInStrings*/false, /*findInComments*/false); +goTo.marker('3'); +verify.renameLocations(/*findInStrings*/false, /*findInComments*/false); diff --git a/tests/cases/fourslash/javaScriptClass3.ts b/tests/cases/fourslash/javaScriptClass3.ts new file mode 100644 index 0000000000000..47004d53b044f --- /dev/null +++ b/tests/cases/fourslash/javaScriptClass3.ts @@ -0,0 +1,24 @@ +/// + +// In an inferred class, we can to-to-def successfully + +// @allowNonTsExtensions: true +// @Filename: Foo.js +//// class Foo { +//// constructor() { +//// /*dst1*/this.alpha = 10; +//// /*dst2*/this.beta = 'gamma'; +//// } +//// method() { return this.alpha; } +//// } +//// var x = new Foo(); +//// x.alpha/*src1*/; +//// x.beta/*src2*/; + +goTo.marker('src1'); +goTo.definition(); +verify.caretAtMarker('dst1'); + +goTo.marker('src2'); +goTo.definition(); +verify.caretAtMarker('dst2'); diff --git a/tests/cases/fourslash/javaScriptClass4.ts b/tests/cases/fourslash/javaScriptClass4.ts new file mode 100644 index 0000000000000..1148d5f320eb2 --- /dev/null +++ b/tests/cases/fourslash/javaScriptClass4.ts @@ -0,0 +1,22 @@ +/// + +// Classes have their shape inferred from assignments +// to properties of 'this' in the constructor + +// @allowNonTsExtensions: true +// @Filename: Foo.js +//// class Foo { +//// constructor() { +//// /** +//// * @type {string} +//// */ +//// this.baz = null; +//// } +//// } +//// var x = new Foo(); +//// x/**/ + +goTo.marker(); +edit.insert('.baz.'); +verify.completionListContains("substr", /*displayText*/ undefined, /*documentation*/ undefined, "method"); +