Skip to content

Commit

Permalink
Fix name resolution in typedef and allow defaults for template tags
Browse files Browse the repository at this point in the history
  • Loading branch information
rbuckton committed Aug 17, 2021
1 parent 339ad92 commit 3d9187f
Show file tree
Hide file tree
Showing 12 changed files with 462 additions and 6 deletions.
2 changes: 1 addition & 1 deletion src/compiler/binder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3388,7 +3388,7 @@ namespace ts {

function bindTypeParameter(node: TypeParameterDeclaration) {
if (isJSDocTemplateTag(node.parent)) {
const container = find((node.parent.parent as JSDoc).tags!, isJSDocTypeAlias) || getHostSignatureFromJSDoc(node.parent); // TODO: GH#18217
const container = getEffectiveContainerForJSDocTemplateTag(node.parent);
if (container) {
if (!container.locals) {
container.locals = createSymbolTable();
Expand Down
9 changes: 6 additions & 3 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2045,7 +2045,9 @@ namespace ts {
lastSelfReferenceLocation = location;
}
lastLocation = location;
location = location.parent;
location = isJSDocTemplateTag(location) ?
getEffectiveContainerForJSDocTemplateTag(location) || location.parent :
location.parent;
}

// We just climbed up parents looking for the name, meaning that we started in a descendant node of `lastLocation`.
Expand Down Expand Up @@ -12836,7 +12838,7 @@ namespace ts {

function getParentSymbolOfTypeParameter(typeParameter: TypeParameter): Symbol | undefined {
const tp = getDeclarationOfKind<TypeParameterDeclaration>(typeParameter.symbol, SyntaxKind.TypeParameter)!;
const host = isJSDocTemplateTag(tp.parent) ? getHostSignatureFromJSDoc(tp.parent) : tp.parent;
const host = isJSDocTemplateTag(tp.parent) ? getEffectiveContainerForJSDocTemplateTag(tp.parent) : tp.parent;
return host && getSymbolOfNode(host);
}

Expand Down Expand Up @@ -33458,7 +33460,7 @@ namespace ts {
}
}

checkTypeParameters(node.typeParameters);
checkTypeParameters(getEffectiveTypeParameterDeclarations(node));

forEach(node.parameters, checkParameter);

Expand Down Expand Up @@ -35089,6 +35091,7 @@ namespace ts {
checkTypeNameIsReserved(node.name, Diagnostics.Type_alias_name_cannot_be_0);
}
checkSourceElement(node.typeExpression);
checkTypeParameters(getEffectiveTypeParameterDeclarations(node));
}

function checkJSDocTemplateTag(node: JSDocTemplateTag): void {
Expand Down
20 changes: 18 additions & 2 deletions src/compiler/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8053,6 +8053,22 @@ namespace ts {
return token() === SyntaxKind.OpenBraceToken ? parseJSDocTypeExpression() : undefined;
}

function parseBracketNameInTemplateTag(): { name: Identifier, defaultType?: TypeNode } {
const isBracketed = parseOptionalJsdoc(SyntaxKind.OpenBracketToken);
if (isBracketed) {
skipWhitespace();
}
const name = parseJSDocIdentifierName(Diagnostics.Unexpected_token_A_type_parameter_name_was_expected_without_curly_braces);
let defaultType: TypeNode | undefined;
if (isBracketed) {
skipWhitespace();
parseExpected(SyntaxKind.EqualsToken);
defaultType = doInsideOfContext(NodeFlags.JSDoc, parseJSDocType);
parseExpected(SyntaxKind.CloseBracketToken);
}
return { name, defaultType };
}

function parseBracketNameInPropertyAndParamTag(): { name: EntityName, isBracketed: boolean } {
// Looking for something like '[foo]', 'foo', '[foo.bar]' or 'foo.bar'
const isBracketed = parseOptionalJsdoc(SyntaxKind.OpenBracketToken);
Expand Down Expand Up @@ -8441,11 +8457,11 @@ namespace ts {

function parseTemplateTagTypeParameter() {
const typeParameterPos = getNodePos();
const name = parseJSDocIdentifierName(Diagnostics.Unexpected_token_A_type_parameter_name_was_expected_without_curly_braces);
const { name, defaultType } = parseBracketNameInTemplateTag();
if (nodeIsMissing(name)) {
return undefined;
}
return finishNode(factory.createTypeParameterDeclaration(name, /*constraint*/ undefined, /*defaultType*/ undefined), typeParameterPos);
return finishNode(factory.createTypeParameterDeclaration(name, /*constraint*/ undefined, defaultType), typeParameterPos);
}

function parseTemplateTagTypeParameters() {
Expand Down
12 changes: 12 additions & 0 deletions src/compiler/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2703,6 +2703,18 @@ namespace ts {
return parameter && parameter.symbol;
}

export function getEffectiveContainerForJSDocTemplateTag(node: JSDocTemplateTag) {
if (isJSDoc(node.parent) && node.parent.tags) {
// A @template tag belongs to any @typedef, @callback, or @enum tags in the same comment block, if they exist.
const typeAlias = find(node.parent.tags, isJSDocTypeAlias);
if (typeAlias) {
return typeAlias;
}
}
// otherwise it belongs to the host it annotates
return getHostSignatureFromJSDoc(node);
}

export function getHostSignatureFromJSDoc(node: Node): SignatureDeclaration | undefined {
const host = getEffectiveJSDocHost(node);
return host && isFunctionLike(host) ? host : undefined;
Expand Down
99 changes: 99 additions & 0 deletions tests/baselines/reference/jsdocTemplateTagDefault.errors.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
tests/cases/conformance/jsdoc/file.js(9,20): error TS2322: Type 'number' is not assignable to type 'string'.
tests/cases/conformance/jsdoc/file.js(22,34): error TS1005: '=' expected.
tests/cases/conformance/jsdoc/file.js(27,35): error TS1110: Type expected.
tests/cases/conformance/jsdoc/file.js(33,14): error TS2706: Required type parameters may not follow optional type parameters.
tests/cases/conformance/jsdoc/file.js(39,14): error TS2706: Required type parameters may not follow optional type parameters.
tests/cases/conformance/jsdoc/file.js(44,17): error TS2744: Type parameter defaults can only reference previously declared type parameters.
tests/cases/conformance/jsdoc/file.js(59,14): error TS2706: Required type parameters may not follow optional type parameters.
tests/cases/conformance/jsdoc/file.js(66,17): error TS2744: Type parameter defaults can only reference previously declared type parameters.


==== tests/cases/conformance/jsdoc/file.js (8 errors) ====
/**
* @template {string | number} [T=string] - ok: defaults are permitted
* @typedef {[T]} A
*/

/** @type {A} */ // ok, default for `T` in `A` is `string`
const aDefault1 = [""];
/** @type {A} */ // error: `number` is not assignable to string`
const aDefault2 = [0];
~
!!! error TS2322: Type 'number' is not assignable to type 'string'.
/** @type {A<string>} */ // ok, `T` is provided for `A`
const aString = [""];
/** @type {A<number>} */ // ok, `T` is provided for `A`
const aNumber = [0];

/**
* @template T
* @template [U=T] - ok: default can reference earlier type parameter
* @typedef {[T, U]} B
*/

/**
* @template {string | number} [T] - error: default requires an `=type`
~
!!! error TS1005: '=' expected.
* @typedef {[T]} C
*/

/**
* @template {string | number} [T=] - error: default requires a `type`
~
!!! error TS1110: Type expected.
* @typedef {[T]} D
*/

/**
* @template {string | number} [T=string]
* @template U - error: Required type parameters cannot follow optional type parameters
~
!!! error TS2706: Required type parameters may not follow optional type parameters.
* @typedef {[T, U]} E
*/

/**
* @template {string | number} [T=string]
* @template U - error: Required type parameters cannot follow optional type parameters
~
!!! error TS2706: Required type parameters may not follow optional type parameters.
* @typedef {[T, U]} F
*/

/**
* @template [T=U] - error: Type parameter defaults can only reference previously declared type parameters.
~
!!! error TS2744: Type parameter defaults can only reference previously declared type parameters.
* @template [U=T]
* @typedef {[T, U]} G
*/

/**
* @template T
* @template [U=T] - ok: default can reference earlier type parameter
* @param {T} a
* @param {U} b
*/
function f1(a, b) {}

/**
* @template {string | number} [T=string]
* @template U - error: Required type parameters cannot follow optional type parameters
~
!!! error TS2706: Required type parameters may not follow optional type parameters.
* @param {T} a
* @param {U} b
*/
function f2(a, b) {}

/**
* @template [T=U] - error: Type parameter defaults can only reference previously declared type parameters.
~
!!! error TS2744: Type parameter defaults can only reference previously declared type parameters.
* @template [U=T]
* @param {T} a
* @param {U} b
*/
function f3(a, b) {}

89 changes: 89 additions & 0 deletions tests/baselines/reference/jsdocTemplateTagDefault.symbols
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
=== tests/cases/conformance/jsdoc/file.js ===
/**
* @template {string | number} [T=string] - ok: defaults are permitted
* @typedef {[T]} A
*/

/** @type {A} */ // ok, default for `T` in `A` is `string`
const aDefault1 = [""];
>aDefault1 : Symbol(aDefault1, Decl(file.js, 6, 5))

/** @type {A} */ // error: `number` is not assignable to string`
const aDefault2 = [0];
>aDefault2 : Symbol(aDefault2, Decl(file.js, 8, 5))

/** @type {A<string>} */ // ok, `T` is provided for `A`
const aString = [""];
>aString : Symbol(aString, Decl(file.js, 10, 5))

/** @type {A<number>} */ // ok, `T` is provided for `A`
const aNumber = [0];
>aNumber : Symbol(aNumber, Decl(file.js, 12, 5))

/**
* @template T
* @template [U=T] - ok: default can reference earlier type parameter
* @typedef {[T, U]} B
*/

/**
* @template {string | number} [T] - error: default requires an `=type`
* @typedef {[T]} C
*/

/**
* @template {string | number} [T=] - error: default requires a `type`
* @typedef {[T]} D
*/

/**
* @template {string | number} [T=string]
* @template U - error: Required type parameters cannot follow optional type parameters
* @typedef {[T, U]} E
*/

/**
* @template {string | number} [T=string]
* @template U - error: Required type parameters cannot follow optional type parameters
* @typedef {[T, U]} F
*/

/**
* @template [T=U] - error: Type parameter defaults can only reference previously declared type parameters.
* @template [U=T]
* @typedef {[T, U]} G
*/

/**
* @template T
* @template [U=T] - ok: default can reference earlier type parameter
* @param {T} a
* @param {U} b
*/
function f1(a, b) {}
>f1 : Symbol(f1, Decl(file.js, 12, 20))
>a : Symbol(a, Decl(file.js, 54, 12))
>b : Symbol(b, Decl(file.js, 54, 14))

/**
* @template {string | number} [T=string]
* @template U - error: Required type parameters cannot follow optional type parameters
* @param {T} a
* @param {U} b
*/
function f2(a, b) {}
>f2 : Symbol(f2, Decl(file.js, 54, 20))
>a : Symbol(a, Decl(file.js, 62, 12))
>b : Symbol(b, Decl(file.js, 62, 14))

/**
* @template [T=U] - error: Type parameter defaults can only reference previously declared type parameters.
* @template [U=T]
* @param {T} a
* @param {U} b
*/
function f3(a, b) {}
>f3 : Symbol(f3, Decl(file.js, 62, 20))
>a : Symbol(a, Decl(file.js, 70, 12))
>b : Symbol(b, Decl(file.js, 70, 14))

97 changes: 97 additions & 0 deletions tests/baselines/reference/jsdocTemplateTagDefault.types
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
=== tests/cases/conformance/jsdoc/file.js ===
/**
* @template {string | number} [T=string] - ok: defaults are permitted
* @typedef {[T]} A
*/

/** @type {A} */ // ok, default for `T` in `A` is `string`
const aDefault1 = [""];
>aDefault1 : A<string>
>[""] : [string]
>"" : ""

/** @type {A} */ // error: `number` is not assignable to string`
const aDefault2 = [0];
>aDefault2 : A<string>
>[0] : [number]
>0 : 0

/** @type {A<string>} */ // ok, `T` is provided for `A`
const aString = [""];
>aString : A<string>
>[""] : [string]
>"" : ""

/** @type {A<number>} */ // ok, `T` is provided for `A`
const aNumber = [0];
>aNumber : A<number>
>[0] : [number]
>0 : 0

/**
* @template T
* @template [U=T] - ok: default can reference earlier type parameter
* @typedef {[T, U]} B
*/

/**
* @template {string | number} [T] - error: default requires an `=type`
* @typedef {[T]} C
*/

/**
* @template {string | number} [T=] - error: default requires a `type`
* @typedef {[T]} D
*/

/**
* @template {string | number} [T=string]
* @template U - error: Required type parameters cannot follow optional type parameters
* @typedef {[T, U]} E
*/

/**
* @template {string | number} [T=string]
* @template U - error: Required type parameters cannot follow optional type parameters
* @typedef {[T, U]} F
*/

/**
* @template [T=U] - error: Type parameter defaults can only reference previously declared type parameters.
* @template [U=T]
* @typedef {[T, U]} G
*/

/**
* @template T
* @template [U=T] - ok: default can reference earlier type parameter
* @param {T} a
* @param {U} b
*/
function f1(a, b) {}
>f1 : <T, U = T>(a: T, b: U) => void
>a : T
>b : U

/**
* @template {string | number} [T=string]
* @template U - error: Required type parameters cannot follow optional type parameters
* @param {T} a
* @param {U} b
*/
function f2(a, b) {}
>f2 : <T extends string | number = string, U>(a: T, b: U) => void
>a : T
>b : U

/**
* @template [T=U] - error: Type parameter defaults can only reference previously declared type parameters.
* @template [U=T]
* @param {T} a
* @param {U} b
*/
function f3(a, b) {}
>f3 : <T = U, U = T>(a: T, b: U) => void
>a : T
>b : U

Loading

0 comments on commit 3d9187f

Please sign in to comment.