Add getEffectiveConstructSignatures (#27561)

* Add helpers that understand constructor functions

* getEffectiveConstructSignatures gets construct signatures from type, and
  call signatures from constructor functions if there are no construct
  signatures.
* getEffectiveConstructSignatureReturnType gets the "JS Class type" for
  constructor functions, and the return type of signatures for all other
  declarations.

This is a first step toward making constructor functions have construct
signatures instead of call signatures, which will also contribute to
fixing instantiation of generic constructor functions, which is basically
broken right now.

Note that the baselines *improve* but, because of the previously
mentioned generic problem, are still not correct. Construct signatures
for constructor functions and generic constructor functions turns out to
be an intertwined problem.

* Correct correct originalBaseType

And, for now, return anyType for generic constructor functions used as
base types. Don't give an incorrect error based on the function's return
type, which is usually void.

* Add error examples to tests

* Add construct signatures instead of getEffective* functions

* Fix typo in baseline

* Remove pesky newline

I thought I got rid of it!

* Test of constructor tag on object literal method

It doesn't work, and shouldn't in the future, because it's a runtime
error.
This commit is contained in:
Nathan Shively-Sanders 2018-10-15 12:47:57 -07:00 committed by GitHub
parent bb275b999c
commit c184184713
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 141 additions and 53 deletions

View file

@ -5622,9 +5622,6 @@ namespace ts {
function getConstructorsForTypeArguments(type: Type, typeArgumentNodes: ReadonlyArray<TypeNode> | undefined, location: Node): ReadonlyArray<Signature> {
const typeArgCount = length(typeArgumentNodes);
const isJavascript = isInJSFile(location);
if (isJSConstructorType(type) && !typeArgCount) {
return getSignaturesOfType(type, SignatureKind.Call);
}
return filter(getSignaturesOfType(type, SignatureKind.Construct),
sig => (isJavascript || typeArgCount >= getMinTypeArgumentCount(sig.typeParameters)) && typeArgCount <= length(sig.typeParameters));
}
@ -5706,7 +5703,9 @@ namespace ts {
const baseTypeNode = getBaseTypeNodeOfClass(type)!;
const typeArgs = typeArgumentsFromTypeReferenceNode(baseTypeNode);
let baseType: Type;
const originalBaseType = baseConstructorType && baseConstructorType.symbol ? getDeclaredTypeOfSymbol(baseConstructorType.symbol) : undefined;
const originalBaseType = isJSConstructorType(baseConstructorType) ? baseConstructorType :
baseConstructorType.symbol ? getDeclaredTypeOfSymbol(baseConstructorType.symbol) :
undefined;
if (baseConstructorType.symbol && baseConstructorType.symbol.flags & SymbolFlags.Class &&
areAllOuterTypeParametersApplied(originalBaseType!)) {
// When base constructor type is a class with no captured type arguments we know that the constructors all have the same type parameters as the
@ -5717,8 +5716,8 @@ namespace ts {
else if (baseConstructorType.flags & TypeFlags.Any) {
baseType = baseConstructorType;
}
else if (isJSConstructorType(baseConstructorType) && !baseTypeNode.typeArguments) {
baseType = getJSClassType(baseConstructorType.symbol) || anyType;
else if (isJSConstructorType(baseConstructorType)) {
baseType = !baseTypeNode.typeArguments && getJSClassType(baseConstructorType.symbol) || anyType;
}
else {
// The class derives from a "class-like" constructor function, check that we have at least one construct signature
@ -6730,6 +6729,7 @@ namespace ts {
// will never be observed because a qualified name can't reference signatures.
if (symbol.flags & (SymbolFlags.Function | SymbolFlags.Method)) {
type.callSignatures = getSignaturesOfSymbol(symbol);
type.constructSignatures = filter(type.callSignatures, sig => isJSConstructor(sig.declaration));
}
// And likewise for construct signatures for classes
if (symbol.flags & SymbolFlags.Class) {
@ -7866,6 +7866,7 @@ namespace ts {
let type = signature.target ? instantiateType(getReturnTypeOfSignature(signature.target), signature.mapper!) :
signature.unionSignatures ? getUnionType(map(signature.unionSignatures, getReturnTypeOfSignature), UnionReduction.Subtype) :
getReturnTypeFromAnnotation(signature.declaration!) ||
isJSConstructor(signature.declaration) && getJSClassType(getSymbolOfNode(signature.declaration!)) ||
(nodeIsMissing((<FunctionLikeDeclaration>signature.declaration).body) ? anyType : getReturnTypeFromBody(<FunctionLikeDeclaration>signature.declaration));
if (!popTypeResolution()) {
if (signature.declaration) {
@ -15451,12 +15452,9 @@ namespace ts {
}
if (!targetType) {
let constructSignatures = getSignaturesOfType(rightType, SignatureKind.Construct);
if (constructSignatures.length === 0) {
constructSignatures = filter(getSignaturesOfType(rightType, SignatureKind.Call), sig => isJSConstructor(sig.declaration));
}
const constructSignatures = getSignaturesOfType(rightType, SignatureKind.Construct);
targetType = constructSignatures.length ?
getUnionType(map(constructSignatures, signature => isJSConstructor(signature.declaration) && getJSClassType(getSymbolOfNode(signature.declaration!)) || getReturnTypeOfSignature(getErasedSignature(signature)))) :
getUnionType(map(constructSignatures, signature => getReturnTypeOfSignature(getErasedSignature(signature)))) :
emptyObjectType;
}
@ -20039,12 +20037,12 @@ namespace ts {
// Function interface, since they have none by default. This is a bit of a leap of faith
// that the user will not add any.
const callSignatures = getSignaturesOfType(apparentType, SignatureKind.Call);
const constructSignatures = getSignaturesOfType(apparentType, SignatureKind.Construct);
const numConstructSignatures = getSignaturesOfType(apparentType, SignatureKind.Construct).length;
// TS 1.0 Spec: 4.12
// In an untyped function call no TypeArgs are permitted, Args can be any argument list, no contextual
// types are provided for the argument expressions, and the result is always of type Any.
if (isUntypedFunctionCall(funcType, apparentType, callSignatures.length, constructSignatures.length)) {
if (isUntypedFunctionCall(funcType, apparentType, callSignatures.length, numConstructSignatures)) {
// The unknownType indicates that an error already occurred (and was reported). No
// need to report another error in this case.
if (funcType !== errorType && node.typeArguments) {
@ -20056,7 +20054,7 @@ namespace ts {
// TypeScript employs overload resolution in typed function calls in order to support functions
// with multiple call signatures.
if (!callSignatures.length) {
if (constructSignatures.length) {
if (numConstructSignatures) {
error(node, Diagnostics.Value_of_type_0_is_not_callable_Did_you_mean_to_include_new, typeToString(funcType));
}
else {
@ -20272,9 +20270,9 @@ namespace ts {
}
const callSignatures = getSignaturesOfType(apparentType, SignatureKind.Call);
const constructSignatures = getSignaturesOfType(apparentType, SignatureKind.Construct);
const numConstructSignatures = getSignaturesOfType(apparentType, SignatureKind.Construct).length;
if (isUntypedFunctionCall(tagType, apparentType, callSignatures.length, constructSignatures.length)) {
if (isUntypedFunctionCall(tagType, apparentType, callSignatures.length, numConstructSignatures)) {
return resolveUntypedCall(node);
}
@ -20322,8 +20320,8 @@ namespace ts {
}
const callSignatures = getSignaturesOfType(apparentType, SignatureKind.Call);
const constructSignatures = getSignaturesOfType(apparentType, SignatureKind.Construct);
if (isUntypedFunctionCall(funcType, apparentType, callSignatures.length, constructSignatures.length)) {
const numConstructSignatures = getSignaturesOfType(apparentType, SignatureKind.Construct).length;
if (isUntypedFunctionCall(funcType, apparentType, callSignatures.length, numConstructSignatures)) {
return resolveUntypedCall(node);
}

View file

@ -2,9 +2,8 @@ tests/cases/conformance/salsa/first.js(23,9): error TS2554: Expected 1 arguments
tests/cases/conformance/salsa/first.js(31,5): error TS2416: Property 'load' in type 'Sql' is not assignable to the same property in base type 'Wagon'.
Type '(files: string[], format: "csv" | "json" | "xmlolololol") => void' is not assignable to type '(supplies?: any[]) => void'.
tests/cases/conformance/salsa/first.js(47,24): error TS2507: Type '(numberEaten: number) => void' is not a constructor function type.
tests/cases/conformance/salsa/generic.js(8,15): error TS2508: No base constructor has the specified number of type arguments.
tests/cases/conformance/salsa/generic.js(11,21): error TS2339: Property 'flavour' does not exist on type 'Chowder'.
tests/cases/conformance/salsa/generic.js(18,9): error TS2339: Property 'flavour' does not exist on type 'Chowder'.
tests/cases/conformance/salsa/generic.js(19,19): error TS2554: Expected 1 arguments, but got 0.
tests/cases/conformance/salsa/generic.js(20,32): error TS2345: Argument of type '0' is not assignable to parameter of type '{ claim: "ignorant" | "malicious"; }'.
tests/cases/conformance/salsa/second.ts(8,25): error TS2507: Type '(numberEaten: number) => void' is not a constructor function type.
tests/cases/conformance/salsa/second.ts(14,7): error TS2417: Class static side 'typeof Conestoga' incorrectly extends base class static side 'typeof Wagon'.
Types of property 'circle' are incompatible.
@ -115,7 +114,7 @@ tests/cases/conformance/salsa/second.ts(17,15): error TS2345: Argument of type '
c.drunkOO
c.numberOxen
==== tests/cases/conformance/salsa/generic.js (3 errors) ====
==== tests/cases/conformance/salsa/generic.js (2 errors) ====
/**
* @template T
* @param {T} flavour
@ -124,21 +123,22 @@ tests/cases/conformance/salsa/second.ts(17,15): error TS2345: Argument of type '
this.flavour = flavour
}
/** @extends {Soup<{ claim: "ignorant" | "malicious" }>} */
~~~~
!!! error TS2508: No base constructor has the specified number of type arguments.
class Chowder extends Soup {
log() {
return this.flavour
~~~~~~~
!!! error TS2339: Property 'flavour' does not exist on type 'Chowder'.
}
}
var soup = new Soup(1);
soup.flavour
var chowder = new Chowder();
var chowder = new Chowder({ claim: "ignorant" });
chowder.flavour.claim
~~~~~~~
!!! error TS2339: Property 'flavour' does not exist on type 'Chowder'.
var errorNoArgs = new Chowder();
~~~~~~~~~~~~~
!!! error TS2554: Expected 1 arguments, but got 0.
!!! related TS6210 tests/cases/conformance/salsa/generic.js:5:15: An argument for 'flavour' was not provided.
var errorArgType = new Chowder(0);
~
!!! error TS2345: Argument of type '0' is not assignable to parameter of type '{ claim: "ignorant" | "malicious"; }'.

View file

@ -218,11 +218,20 @@ soup.flavour
>soup : Symbol(soup, Decl(generic.js, 14, 3))
>flavour : Symbol(Soup.flavour, Decl(generic.js, 4, 24))
var chowder = new Chowder();
var chowder = new Chowder({ claim: "ignorant" });
>chowder : Symbol(chowder, Decl(generic.js, 16, 3))
>Chowder : Symbol(Chowder, Decl(generic.js, 6, 1))
>claim : Symbol(claim, Decl(generic.js, 16, 27))
chowder.flavour.claim
>chowder : Symbol(chowder, Decl(generic.js, 16, 3))
var errorNoArgs = new Chowder();
>errorNoArgs : Symbol(errorNoArgs, Decl(generic.js, 18, 3))
>Chowder : Symbol(Chowder, Decl(generic.js, 6, 1))
var errorArgType = new Chowder(0);
>errorArgType : Symbol(errorArgType, Decl(generic.js, 19, 3))
>Chowder : Symbol(Chowder, Decl(generic.js, 6, 1))

View file

@ -268,16 +268,30 @@ soup.flavour
>soup : typeof Soup
>flavour : number
var chowder = new Chowder();
>chowder : Chowder
>new Chowder() : Chowder
var chowder = new Chowder({ claim: "ignorant" });
>chowder : any
>new Chowder({ claim: "ignorant" }) : any
>Chowder : typeof Chowder
>{ claim: "ignorant" } : { claim: "ignorant"; }
>claim : "ignorant"
>"ignorant" : "ignorant"
chowder.flavour.claim
>chowder.flavour.claim : any
>chowder.flavour : any
>chowder : Chowder
>chowder : any
>flavour : any
>claim : any
var errorNoArgs = new Chowder();
>errorNoArgs : any
>new Chowder() : any
>Chowder : typeof Chowder
var errorArgType = new Chowder(0);
>errorArgType : any
>new Chowder(0) : any
>Chowder : typeof Chowder
>0 : 0

View file

@ -25,7 +25,7 @@ tests/cases/conformance/salsa/index.js(55,13): error TS2554: Expected 1 argument
if (!(this instanceof C3)) return new C3();
};
const c3_v1 = C3();
const c3_v1 = C3(); // error: @class tag requires 'new'
~~~~
!!! error TS2348: Value of type 'typeof C3' is not callable. Did you mean to include 'new'?
const c3_v2 = new C3();
@ -35,7 +35,7 @@ tests/cases/conformance/salsa/index.js(55,13): error TS2554: Expected 1 argument
if (!(this instanceof C4)) return new C4();
};
const c4_v1 = C4();
const c4_v1 = C4(); // error: @class tag requires 'new'
~~~~
!!! error TS2348: Value of type 'typeof C4' is not callable. Did you mean to include 'new'?
const c4_v2 = new C4();

View file

@ -49,7 +49,7 @@ function C3() {
};
const c3_v1 = C3();
const c3_v1 = C3(); // error: @class tag requires 'new'
>c3_v1 : Symbol(c3_v1, Decl(index.js, 21, 5))
>C3 : Symbol(C3, Decl(index.js, 14, 23))
@ -68,7 +68,7 @@ var C4 = function () {
};
const c4_v1 = C4();
const c4_v1 = C4(); // error: @class tag requires 'new'
>c4_v1 : Symbol(c4_v1, Decl(index.js, 29, 5))
>C4 : Symbol(C4, Decl(index.js, 25, 3))

View file

@ -76,9 +76,9 @@ function C3() {
};
const c3_v1 = C3();
>c3_v1 : C3
>C3() : C3
const c3_v1 = C3(); // error: @class tag requires 'new'
>c3_v1 : any
>C3() : any
>C3 : typeof C3
const c3_v2 = new C3();
@ -102,9 +102,9 @@ var C4 = function () {
};
const c4_v1 = C4();
>c4_v1 : C4
>C4() : C4
const c4_v1 = C4(); // error: @class tag requires 'new'
>c4_v1 : any
>C4() : any
>C4 : typeof C4
const c4_v2 = new C4();

View file

@ -0,0 +1,15 @@
tests/cases/conformance/jsdoc/example.js(3,16): error TS2339: Property 'bar' does not exist on type '{ Foo(): void; }'.
tests/cases/conformance/jsdoc/example.js(5,2): error TS7009: 'new' expression, whose target lacks a construct signature, implicitly has an 'any' type.
==== tests/cases/conformance/jsdoc/example.js (2 errors) ====
const obj = {
/** @constructor */
Foo() { this.bar = "bar" }
~~~
!!! error TS2339: Property 'bar' does not exist on type '{ Foo(): void; }'.
};
(new obj.Foo()).bar
~~~~~~~~~~~~~
!!! error TS7009: 'new' expression, whose target lacks a construct signature, implicitly has an 'any' type.

View file

@ -0,0 +1,16 @@
=== tests/cases/conformance/jsdoc/example.js ===
const obj = {
>obj : Symbol(obj, Decl(example.js, 0, 5))
/** @constructor */
Foo() { this.bar = "bar" }
>Foo : Symbol(Foo, Decl(example.js, 0, 13))
>this : Symbol(obj, Decl(example.js, 0, 11))
>bar : Symbol(bar, Decl(example.js, 2, 9))
};
(new obj.Foo()).bar
>obj.Foo : Symbol(Foo, Decl(example.js, 0, 13))
>obj : Symbol(obj, Decl(example.js, 0, 5))
>Foo : Symbol(Foo, Decl(example.js, 0, 13))

View file

@ -0,0 +1,24 @@
=== tests/cases/conformance/jsdoc/example.js ===
const obj = {
>obj : { Foo(): void; }
>{ /** @constructor */ Foo() { this.bar = "bar" }} : { Foo(): void; }
/** @constructor */
Foo() { this.bar = "bar" }
>Foo : () => void
>this.bar = "bar" : "bar"
>this.bar : any
>this : { Foo(): void; }
>bar : any
>"bar" : "bar"
};
(new obj.Foo()).bar
>(new obj.Foo()).bar : any
>(new obj.Foo()) : any
>new obj.Foo() : any
>obj.Foo : () => void
>obj : { Foo(): void; }
>Foo : () => void
>bar : any

View file

@ -1,7 +1,7 @@
=== tests/cases/conformance/salsa/module.js ===
var Outer = function(element, config) {};
>Outer : { (element: any, config: any): void; Pos(line: any, ch: any): void; }
>function(element, config) {} : { (element: any, config: any): void; Pos(line: any, ch: any): void; }
>Outer : { (element: any, config: any): void; Pos(line: any, ch: any): Pos; }
>function(element, config) {} : { (element: any, config: any): void; Pos(line: any, ch: any): Pos; }
>element : any
>config : any
@ -10,7 +10,7 @@ var Outer = function(element, config) {};
Outer.Pos = function (line, ch) {};
>Outer.Pos = function (line, ch) {} : typeof Pos
>Outer.Pos : typeof Pos
>Outer : { (element: any, config: any): void; Pos(line: any, ch: any): void; }
>Outer : { (element: any, config: any): void; Pos(line: any, ch: any): Pos; }
>Pos : typeof Pos
>function (line, ch) {} : typeof Pos
>line : any
@ -21,7 +21,7 @@ Outer.Pos.prototype.line;
>Outer.Pos.prototype.line : any
>Outer.Pos.prototype : any
>Outer.Pos : typeof Pos
>Outer : { (element: any, config: any): void; Pos(line: any, ch: any): void; }
>Outer : { (element: any, config: any): void; Pos(line: any, ch: any): Pos; }
>Pos : typeof Pos
>prototype : any
>line : any
@ -30,7 +30,7 @@ var pos = new Outer.Pos(1, 'x');
>pos : Pos
>new Outer.Pos(1, 'x') : Pos
>Outer.Pos : typeof Pos
>Outer : { (element: any, config: any): void; Pos(line: any, ch: any): void; }
>Outer : { (element: any, config: any): void; Pos(line: any, ch: any): Pos; }
>Pos : typeof Pos
>1 : 1
>'x' : "x"

View file

@ -0,0 +1,10 @@
// @noEmit: true
// @Filename: example.js
// @allowJs: true
// @checkJs: true
// @noImplicitAny: true
const obj = {
/** @constructor */
Foo() { this.bar = "bar" }
};
(new obj.Foo()).bar

View file

@ -99,6 +99,8 @@ class Chowder extends Soup {
var soup = new Soup(1);
soup.flavour
var chowder = new Chowder();
var chowder = new Chowder({ claim: "ignorant" });
chowder.flavour.claim
var errorNoArgs = new Chowder();
var errorArgType = new Chowder(0);

View file

@ -24,7 +24,7 @@ function C3() {
if (!(this instanceof C3)) return new C3();
};
const c3_v1 = C3();
const c3_v1 = C3(); // error: @class tag requires 'new'
const c3_v2 = new C3();
/** @class */
@ -32,7 +32,7 @@ var C4 = function () {
if (!(this instanceof C4)) return new C4();
};
const c4_v1 = C4();
const c4_v1 = C4(); // error: @class tag requires 'new'
const c4_v2 = new C4();
var c5_v1;

View file

@ -14,4 +14,4 @@
////}
////var p = new Pers/**/on();
goTo.marker();
verify.quickInfoIs("function Person(name: string, age: number): void", "Represents a person\na b multiline test");
verify.quickInfoIs("function Person(name: string, age: number): Person", "Represents a person\na b multiline test");