Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Apr 19, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 46 additions & 26 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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);
}
}
}
Expand Down Expand Up @@ -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));
}

/**
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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>();
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -22155,8 +22173,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)));
}
Expand Down Expand Up @@ -23530,20 +23549,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.
Expand Down Expand Up @@ -23604,7 +23624,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);
Expand Down Expand Up @@ -26836,7 +26856,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 {
Expand Down
4 changes: 2 additions & 2 deletions src/compiler/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
31 changes: 31 additions & 0 deletions tests/baselines/reference/jsdocTemplateClass.errors.txt
Original file line number Diff line number Diff line change
@@ -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'.

54 changes: 54 additions & 0 deletions tests/baselines/reference/jsdocTemplateClass.symbols
Original file line number Diff line number Diff line change
@@ -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))

62 changes: 62 additions & 0 deletions tests/baselines/reference/jsdocTemplateClass.types
Original file line number Diff line number Diff line change
@@ -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

Original file line number Diff line number Diff line change
@@ -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'.

Loading