In JS, class supports @template tag for declaring type parameters (#23511)

* Support @template as a class type parameter

Still need to do the following:
1. Correctly get jsdoc host in predicate.
2. Make this work for constructor functions too.
3. Scan rest of codebase for other usages of the type parameters
property that should be calls to getEffectiveTypeParameterDeclarations.
4. Rename tp to something more readable, like typar or ts'.

* Use jsdoc host declaration to find container

* Longer names for type parameters

* Fix renaming operation

* Update fourslash test

* Support @template for JS constructors

* Look for both outer and tag type parameters

* Improve naming to improve code clarity
This commit is contained in:
Nathan Shively-Sanders
2018-04-19 15:58:43 -07:00
committed by GitHub
parent 84b12910e8
commit 8d969a23cb
11 changed files with 413 additions and 31 deletions

View File

@@ -1554,7 +1554,8 @@ namespace ts {
function isTypeParameterSymbolDeclaredInContainer(symbol: Symbol, container: Node) {
for (const decl of symbol.declarations) {
if (decl.kind === SyntaxKind.TypeParameter && decl.parent === container) {
const parent = isJSDocTemplateTag(decl.parent) ? getJSDocHost(decl.parent) : decl.parent;
if (decl.kind === SyntaxKind.TypeParameter && parent === container) {
return true;
}
}
@@ -2060,10 +2061,10 @@ namespace ts {
let symbol: Symbol;
if (name.kind === SyntaxKind.Identifier) {
const message = meaning === namespaceMeaning ? Diagnostics.Cannot_find_namespace_0 : Diagnostics.Cannot_find_name_0;
symbol = resolveName(location || name, name.escapedText, meaning, ignoreErrors ? undefined : message, name, /*isUse*/ true);
const symbolFromJSPrototype = isInJavaScriptFile(name) && resolveEntityNameFromJSPrototype(name, meaning);
symbol = resolveName(location || name, name.escapedText, meaning, ignoreErrors || symbolFromJSPrototype ? undefined : message, name, /*isUse*/ true);
if (!symbol) {
return undefined;
return symbolFromJSPrototype;
}
}
else if (name.kind === SyntaxKind.QualifiedName || name.kind === SyntaxKind.PropertyAccessExpression) {
@@ -2114,6 +2115,18 @@ namespace ts {
return (symbol.flags & meaning) || dontResolveAlias ? symbol : resolveAlias(symbol);
}
function resolveEntityNameFromJSPrototype(name: Identifier, meaning: SymbolFlags) {
if (isJSDocTypeReference(name.parent) && isJSDocTag(name.parent.parent.parent)) {
const host = getJSDocHost(name.parent.parent.parent as JSDocTag);
if (isExpressionStatement(host) &&
isBinaryExpression(host.expression) &&
getSpecialPropertyAssignmentKind(host.expression) === SpecialPropertyAssignmentKind.PrototypeProperty) {
const secondaryLocation = getSymbolOfNode(host.expression.left).parent.valueDeclaration;
return resolveName(secondaryLocation, name.escapedText, meaning, /*nameNotFoundMessage*/ undefined, name, /*isUse*/ true);
}
}
}
function resolveExternalModuleName(location: Node, moduleReferenceExpression: Expression): Symbol {
return resolveExternalModuleNameWorker(location, moduleReferenceExpression, Diagnostics.Cannot_find_module_0);
}
@@ -4897,8 +4910,7 @@ namespace ts {
// in-place and returns the same array.
function appendTypeParameters(typeParameters: TypeParameter[], declarations: ReadonlyArray<TypeParameterDeclaration>): TypeParameter[] {
for (const declaration of declarations) {
const tp = getDeclaredTypeOfTypeParameter(getSymbolOfNode(declaration));
typeParameters = appendIfUnique(typeParameters, tp);
typeParameters = appendIfUnique(typeParameters, getDeclaredTypeOfTypeParameter(getSymbolOfNode(declaration)));
}
return typeParameters;
}
@@ -4958,8 +4970,9 @@ namespace ts {
if (node.kind === SyntaxKind.InterfaceDeclaration || node.kind === SyntaxKind.ClassDeclaration ||
node.kind === SyntaxKind.ClassExpression || node.kind === SyntaxKind.TypeAliasDeclaration) {
const declaration = <InterfaceDeclaration | TypeAliasDeclaration>node;
if (declaration.typeParameters) {
result = appendTypeParameters(result, declaration.typeParameters);
const typeParameters = getEffectiveTypeParameterDeclarations(declaration);
if (typeParameters) {
result = appendTypeParameters(result, typeParameters);
}
}
}
@@ -5455,9 +5468,10 @@ namespace ts {
*/
function isThislessFunctionLikeDeclaration(node: FunctionLikeDeclaration): boolean {
const returnType = getEffectiveReturnTypeNode(node);
const typeParameters = getEffectiveTypeParameterDeclarations(node);
return (node.kind === SyntaxKind.Constructor || (returnType && isThislessType(returnType))) &&
node.parameters.every(isThislessVariableLikeDeclaration) &&
(!node.typeParameters || node.typeParameters.every(isThislessTypeParameter));
(!typeParameters || typeParameters.every(isThislessTypeParameter));
}
/**
@@ -6735,8 +6749,7 @@ namespace ts {
function getTypeParametersFromDeclaration(declaration: DeclarationWithTypeParameters): TypeParameter[] {
let result: TypeParameter[];
forEach(getEffectiveTypeParameterDeclarations(declaration), node => {
const tp = getDeclaredTypeOfTypeParameter(node.symbol);
result = appendIfUnique(result, tp);
result = appendIfUnique(result, getDeclaredTypeOfTypeParameter(node.symbol));
});
return result;
}
@@ -7547,7 +7560,7 @@ namespace ts {
return constraints ? getSubstitutionType(typeVariable, getIntersectionType(append(constraints, typeVariable))) : typeVariable;
}
function isJSDocTypeReference(node: NodeWithTypeArguments): node is TypeReferenceNode {
function isJSDocTypeReference(node: Node): node is TypeReferenceNode {
return node.flags & NodeFlags.JSDoc && node.kind === SyntaxKind.TypeReference;
}
@@ -9170,10 +9183,15 @@ namespace ts {
// aren't the right hand side of a generic type alias declaration we optimize by reducing the
// set of type parameters to those that are possibly referenced in the literal.
const declaration = symbol.declarations[0];
const outerTypeParameters = getOuterTypeParameters(declaration, /*includeThisTypes*/ true) || emptyArray;
let outerTypeParameters = getOuterTypeParameters(declaration, /*includeThisTypes*/ true);
if (isJavaScriptConstructor(declaration)) {
const templateTagParameters = getTypeParametersFromDeclaration(declaration as DeclarationWithTypeParameters);
outerTypeParameters = addRange(outerTypeParameters, templateTagParameters);
}
typeParameters = outerTypeParameters || emptyArray;
typeParameters = symbol.flags & SymbolFlags.TypeLiteral && !target.aliasTypeArguments ?
filter(outerTypeParameters, tp => isTypeParameterPossiblyReferenced(tp, declaration)) :
outerTypeParameters;
filter(typeParameters, tp => isTypeParameterPossiblyReferenced(tp, declaration)) :
typeParameters;
links.outerTypeParameters = typeParameters;
if (typeParameters.length) {
links.instantiations = createMap<Type>();
@@ -18533,7 +18551,7 @@ namespace ts {
}
const type = funcSymbol && getJavaScriptClassType(funcSymbol);
if (type) {
return type;
return signature.target ? instantiateType(type, signature.mapper) : type;
}
if (noImplicitAny) {
error(node, Diagnostics.new_expression_whose_target_lacks_a_construct_signature_implicitly_has_an_any_type);
@@ -22161,8 +22179,9 @@ namespace ts {
): void {
// Only report errors on the last declaration for the type parameter container;
// this ensures that all uses have been accounted for.
if (!(node.flags & NodeFlags.Ambient) && node.typeParameters && last(getSymbolOfNode(node)!.declarations) === node) {
for (const typeParameter of node.typeParameters) {
const typeParameters = getEffectiveTypeParameterDeclarations(node);
if (!(node.flags & NodeFlags.Ambient) && typeParameters && last(getSymbolOfNode(node)!.declarations) === node) {
for (const typeParameter of typeParameters) {
if (!(getMergedSymbol(typeParameter.symbol).isReferenced & SymbolFlags.TypeParameter) && !isIdentifierThatStartsWithUnderScore(typeParameter.name)) {
addDiagnostic(UnusedKind.Parameter, createDiagnosticForNode(typeParameter.name, Diagnostics._0_is_declared_but_its_value_is_never_read, symbolName(typeParameter.symbol)));
}
@@ -23536,20 +23555,21 @@ namespace ts {
}
}
function areTypeParametersIdentical(declarations: ReadonlyArray<ClassDeclaration | InterfaceDeclaration>, typeParameters: TypeParameter[]) {
const maxTypeArgumentCount = length(typeParameters);
const minTypeArgumentCount = getMinTypeArgumentCount(typeParameters);
function areTypeParametersIdentical(declarations: ReadonlyArray<ClassDeclaration | InterfaceDeclaration>, targetParameters: TypeParameter[]) {
const maxTypeArgumentCount = length(targetParameters);
const minTypeArgumentCount = getMinTypeArgumentCount(targetParameters);
for (const declaration of declarations) {
// If this declaration has too few or too many type parameters, we report an error
const numTypeParameters = length(declaration.typeParameters);
const sourceParameters = getEffectiveTypeParameterDeclarations(declaration);
const numTypeParameters = length(sourceParameters);
if (numTypeParameters < minTypeArgumentCount || numTypeParameters > maxTypeArgumentCount) {
return false;
}
for (let i = 0; i < numTypeParameters; i++) {
const source = declaration.typeParameters[i];
const target = typeParameters[i];
const source = sourceParameters[i];
const target = targetParameters[i];
// If the type parameter node does not have the same as the resolved type
// parameter at this position, we report an error.
@@ -23610,7 +23630,7 @@ namespace ts {
checkCollisionWithRequireExportsInGeneratedCode(node, node.name);
checkCollisionWithGlobalPromiseInGeneratedCode(node, node.name);
}
checkTypeParameters(node.typeParameters);
checkTypeParameters(getEffectiveTypeParameterDeclarations(node));
checkExportsOnMergedDeclarations(node);
const symbol = getSymbolOfNode(node);
const type = <InterfaceType>getDeclaredTypeOfSymbol(symbol);
@@ -26846,7 +26866,7 @@ namespace ts {
function checkGrammarClassLikeDeclaration(node: ClassLikeDeclaration): boolean {
const file = getSourceFileOfNode(node);
return checkGrammarClassDeclarationHeritageClauses(node) || checkGrammarTypeParameterList(node.typeParameters, file);
return checkGrammarClassDeclarationHeritageClauses(node) || checkGrammarTypeParameterList(getEffectiveTypeParameterDeclarations(node), file);
}
function checkGrammarArrowFunction(node: Node, file: SourceFile): boolean {

View File

@@ -3055,11 +3055,11 @@ namespace ts {
* Gets the effective type parameters. If the node was parsed in a
* JavaScript file, gets the type parameters from the `@template` tag from JSDoc.
*/
export function getEffectiveTypeParameterDeclarations(node: DeclarationWithTypeParameters): ReadonlyArray<TypeParameterDeclaration> | undefined {
export function getEffectiveTypeParameterDeclarations(node: DeclarationWithTypeParameters) {
return node.typeParameters || (isInJavaScriptFile(node) ? getJSDocTypeParameterDeclarations(node) : undefined);
}
export function getJSDocTypeParameterDeclarations(node: DeclarationWithTypeParameters): ReadonlyArray<TypeParameterDeclaration> {
export function getJSDocTypeParameterDeclarations(node: DeclarationWithTypeParameters) {
const templateTag = getJSDocTemplateTag(node);
return templateTag && templateTag.typeParameters;
}

View File

@@ -0,0 +1,31 @@
tests/cases/conformance/jsdoc/templateTagOnClasses.js(24,1): error TS2322: Type 'boolean' is not assignable to type 'number'.
==== tests/cases/conformance/jsdoc/templateTagOnClasses.js (1 errors) ====
/**
* @template {T}
* @typedef {(t: T) => T} Id
*/
class Foo {
/** @typedef {(t: T) => T} Id2 */
/** @param {T} x */
constructor (x) {
this.a = x
}
/**
*
* @param {T} x
* @param {Id} y
* @param {Id2} alpha
* @return {T}
*/
foo(x, y, alpha) {
return alpha(y(x))
}
}
var f = new Foo(1)
var g = new Foo(false)
f.a = g.a
~~~
!!! error TS2322: Type 'boolean' is not assignable to type 'number'.

View File

@@ -0,0 +1,54 @@
=== tests/cases/conformance/jsdoc/templateTagOnClasses.js ===
/**
* @template {T}
* @typedef {(t: T) => T} Id
*/
class Foo {
>Foo : Symbol(Foo, Decl(templateTagOnClasses.js, 0, 0))
/** @typedef {(t: T) => T} Id2 */
/** @param {T} x */
constructor (x) {
>x : Symbol(x, Decl(templateTagOnClasses.js, 7, 17))
this.a = x
>this.a : Symbol(Foo.a, Decl(templateTagOnClasses.js, 7, 21))
>this : Symbol(Foo, Decl(templateTagOnClasses.js, 0, 0))
>a : Symbol(Foo.a, Decl(templateTagOnClasses.js, 7, 21))
>x : Symbol(x, Decl(templateTagOnClasses.js, 7, 17))
}
/**
*
* @param {T} x
* @param {Id} y
* @param {Id2} alpha
* @return {T}
*/
foo(x, y, alpha) {
>foo : Symbol(Foo.foo, Decl(templateTagOnClasses.js, 9, 5))
>x : Symbol(x, Decl(templateTagOnClasses.js, 17, 8))
>y : Symbol(y, Decl(templateTagOnClasses.js, 17, 10))
>alpha : Symbol(alpha, Decl(templateTagOnClasses.js, 17, 13))
return alpha(y(x))
>alpha : Symbol(alpha, Decl(templateTagOnClasses.js, 17, 13))
>y : Symbol(y, Decl(templateTagOnClasses.js, 17, 10))
>x : Symbol(x, Decl(templateTagOnClasses.js, 17, 8))
}
}
var f = new Foo(1)
>f : Symbol(f, Decl(templateTagOnClasses.js, 21, 3))
>Foo : Symbol(Foo, Decl(templateTagOnClasses.js, 0, 0))
var g = new Foo(false)
>g : Symbol(g, Decl(templateTagOnClasses.js, 22, 3))
>Foo : Symbol(Foo, Decl(templateTagOnClasses.js, 0, 0))
f.a = g.a
>f.a : Symbol(Foo.a, Decl(templateTagOnClasses.js, 7, 21))
>f : Symbol(f, Decl(templateTagOnClasses.js, 21, 3))
>a : Symbol(Foo.a, Decl(templateTagOnClasses.js, 7, 21))
>g.a : Symbol(Foo.a, Decl(templateTagOnClasses.js, 7, 21))
>g : Symbol(g, Decl(templateTagOnClasses.js, 22, 3))
>a : Symbol(Foo.a, Decl(templateTagOnClasses.js, 7, 21))

View File

@@ -0,0 +1,62 @@
=== tests/cases/conformance/jsdoc/templateTagOnClasses.js ===
/**
* @template {T}
* @typedef {(t: T) => T} Id
*/
class Foo {
>Foo : Foo<T>
/** @typedef {(t: T) => T} Id2 */
/** @param {T} x */
constructor (x) {
>x : T
this.a = x
>this.a = x : T
>this.a : T
>this : this
>a : T
>x : T
}
/**
*
* @param {T} x
* @param {Id} y
* @param {Id2} alpha
* @return {T}
*/
foo(x, y, alpha) {
>foo : (x: T, y: (t: T) => T, alpha: (t: T) => T) => T
>x : T
>y : (t: T) => T
>alpha : (t: T) => T
return alpha(y(x))
>alpha(y(x)) : T
>alpha : (t: T) => T
>y(x) : T
>y : (t: T) => T
>x : T
}
}
var f = new Foo(1)
>f : Foo<number>
>new Foo(1) : Foo<number>
>Foo : typeof Foo
>1 : 1
var g = new Foo(false)
>g : Foo<boolean>
>new Foo(false) : Foo<boolean>
>Foo : typeof Foo
>false : false
f.a = g.a
>f.a = g.a : boolean
>f.a : number
>f : Foo<number>
>a : number
>g.a : boolean
>g : Foo<boolean>
>a : boolean

View File

@@ -0,0 +1,28 @@
tests/cases/conformance/jsdoc/templateTagOnConstructorFunctions.js(21,1): error TS2322: Type 'false' is not assignable to type 'number'.
==== tests/cases/conformance/jsdoc/templateTagOnConstructorFunctions.js (1 errors) ====
/**
* @template {T}
* @typedef {(t: T) => T} Id
* @param {T} t
*/
function Zet(t) {
/** @type {T} */
this.u
this.t = t
}
/**
* @param {T} v
* @param {Id} id
*/
Zet.prototype.add = function(v, id) {
this.u = v || this.t
return id(this.u)
}
var z = new Zet(1)
z.t = 2
z.u = false
~~~
!!! error TS2322: Type 'false' is not assignable to type 'number'.

View File

@@ -0,0 +1,57 @@
=== tests/cases/conformance/jsdoc/templateTagOnConstructorFunctions.js ===
/**
* @template {T}
* @typedef {(t: T) => T} Id
* @param {T} t
*/
function Zet(t) {
>Zet : Symbol(Zet, Decl(templateTagOnConstructorFunctions.js, 0, 0))
>t : Symbol(t, Decl(templateTagOnConstructorFunctions.js, 5, 13))
/** @type {T} */
this.u
this.t = t
>t : Symbol(Zet.t, Decl(templateTagOnConstructorFunctions.js, 7, 10))
>t : Symbol(t, Decl(templateTagOnConstructorFunctions.js, 5, 13))
}
/**
* @param {T} v
* @param {Id} id
*/
Zet.prototype.add = function(v, id) {
>Zet.prototype : Symbol(Zet.add, Decl(templateTagOnConstructorFunctions.js, 9, 1))
>Zet : Symbol(Zet, Decl(templateTagOnConstructorFunctions.js, 0, 0))
>prototype : Symbol(Function.prototype, Decl(lib.d.ts, --, --))
>add : Symbol(Zet.add, Decl(templateTagOnConstructorFunctions.js, 9, 1))
>v : Symbol(v, Decl(templateTagOnConstructorFunctions.js, 14, 29))
>id : Symbol(id, Decl(templateTagOnConstructorFunctions.js, 14, 31))
this.u = v || this.t
>this.u : Symbol(Zet.u, Decl(templateTagOnConstructorFunctions.js, 5, 17), Decl(templateTagOnConstructorFunctions.js, 14, 37))
>this : Symbol(Zet, Decl(templateTagOnConstructorFunctions.js, 0, 0))
>u : Symbol(Zet.u, Decl(templateTagOnConstructorFunctions.js, 5, 17), Decl(templateTagOnConstructorFunctions.js, 14, 37))
>v : Symbol(v, Decl(templateTagOnConstructorFunctions.js, 14, 29))
>this.t : Symbol(Zet.t, Decl(templateTagOnConstructorFunctions.js, 7, 10))
>this : Symbol(Zet, Decl(templateTagOnConstructorFunctions.js, 0, 0))
>t : Symbol(Zet.t, Decl(templateTagOnConstructorFunctions.js, 7, 10))
return id(this.u)
>id : Symbol(id, Decl(templateTagOnConstructorFunctions.js, 14, 31))
>this.u : Symbol(Zet.u, Decl(templateTagOnConstructorFunctions.js, 5, 17), Decl(templateTagOnConstructorFunctions.js, 14, 37))
>this : Symbol(Zet, Decl(templateTagOnConstructorFunctions.js, 0, 0))
>u : Symbol(Zet.u, Decl(templateTagOnConstructorFunctions.js, 5, 17), Decl(templateTagOnConstructorFunctions.js, 14, 37))
}
var z = new Zet(1)
>z : Symbol(z, Decl(templateTagOnConstructorFunctions.js, 18, 3))
>Zet : Symbol(Zet, Decl(templateTagOnConstructorFunctions.js, 0, 0))
z.t = 2
>z.t : Symbol(Zet.t, Decl(templateTagOnConstructorFunctions.js, 7, 10))
>z : Symbol(z, Decl(templateTagOnConstructorFunctions.js, 18, 3))
>t : Symbol(Zet.t, Decl(templateTagOnConstructorFunctions.js, 7, 10))
z.u = false
>z.u : Symbol(Zet.u, Decl(templateTagOnConstructorFunctions.js, 5, 17), Decl(templateTagOnConstructorFunctions.js, 14, 37))
>z : Symbol(z, Decl(templateTagOnConstructorFunctions.js, 18, 3))
>u : Symbol(Zet.u, Decl(templateTagOnConstructorFunctions.js, 5, 17), Decl(templateTagOnConstructorFunctions.js, 14, 37))

View File

@@ -0,0 +1,76 @@
=== tests/cases/conformance/jsdoc/templateTagOnConstructorFunctions.js ===
/**
* @template {T}
* @typedef {(t: T) => T} Id
* @param {T} t
*/
function Zet(t) {
>Zet : typeof Zet
>t : T
/** @type {T} */
this.u
>this.u : any
>this : any
>u : any
this.t = t
>this.t = t : T
>this.t : any
>this : any
>t : any
>t : T
}
/**
* @param {T} v
* @param {Id} id
*/
Zet.prototype.add = function(v, id) {
>Zet.prototype.add = function(v, id) { this.u = v || this.t return id(this.u)} : (v: T, id: (t: T) => T) => T
>Zet.prototype.add : any
>Zet.prototype : any
>Zet : typeof Zet
>prototype : any
>add : any
>function(v, id) { this.u = v || this.t return id(this.u)} : (v: T, id: (t: T) => T) => T
>v : T
>id : (t: T) => T
this.u = v || this.t
>this.u = v || this.t : T
>this.u : T
>this : Zet
>u : T
>v || this.t : T
>v : T
>this.t : T
>this : Zet
>t : T
return id(this.u)
>id(this.u) : T
>id : (t: T) => T
>this.u : T
>this : Zet
>u : T
}
var z = new Zet(1)
>z : typeof Zet
>new Zet(1) : typeof Zet
>Zet : typeof Zet
>1 : 1
z.t = 2
>z.t = 2 : 2
>z.t : number
>z : typeof Zet
>t : number
>2 : 2
z.u = false
>z.u = false : false
>z.u : number
>z : typeof Zet
>u : number
>false : false

View File

@@ -0,0 +1,29 @@
// @allowJs: true
// @checkJs: true
// @noEmit: true
// @Filename: templateTagOnClasses.js
/**
* @template {T}
* @typedef {(t: T) => T} Id
*/
class Foo {
/** @typedef {(t: T) => T} Id2 */
/** @param {T} x */
constructor (x) {
this.a = x
}
/**
*
* @param {T} x
* @param {Id} y
* @param {Id2} alpha
* @return {T}
*/
foo(x, y, alpha) {
return alpha(y(x))
}
}
var f = new Foo(1)
var g = new Foo(false)
f.a = g.a

View File

@@ -0,0 +1,26 @@
// @allowJs: true
// @checkJs: true
// @noEmit: true
// @Filename: templateTagOnConstructorFunctions.js
/**
* @template {T}
* @typedef {(t: T) => T} Id
* @param {T} t
*/
function Zet(t) {
/** @type {T} */
this.u
this.t = t
}
/**
* @param {T} v
* @param {Id} id
*/
Zet.prototype.add = function(v, id) {
this.u = v || this.t
return id(this.u)
}
var z = new Zet(1)
z.t = 2
z.u = false

View File

@@ -3,15 +3,14 @@
// @allowJs: true
// @Filename: /a.js
// TODO: https://github.com/Microsoft/TypeScript/issues/16411
// Both uses of T should be referenced.
/////** @template [|{| "isWriteAccess": true, "isDefinition": true |}T|] */
////class C {
//// constructor() {
//// /** @type {T} */
//// /** @type {[|T|]} */
//// this.x = null;
//// }
////}
verify.singleReferenceGroup("(type parameter) T in C");
verify.singleReferenceGroup("(type parameter) T in C<T>", test.ranges());