diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index bc4180f8310..eb8adb84072 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -5124,7 +5124,9 @@ namespace ts { const declaration = find(symbol.declarations, d => d.kind === SyntaxKind.JSDocTypedefTag || d.kind === SyntaxKind.TypeAliasDeclaration); - let type = getTypeFromTypeNode(declaration.kind === SyntaxKind.JSDocTypedefTag ? declaration.typeExpression : declaration.type); + const typeNode = declaration.kind === SyntaxKind.JSDocTypedefTag ? declaration.typeExpression : declaration.type; + // If typeNode is missing, we will error in checkJSDocTypedefTag. + let type = typeNode ? getTypeFromTypeNode(typeNode) : unknownType; if (popTypeResolution()) { const typeParameters = getLocalTypeParametersOfClassOrInterfaceOrTypeAlias(symbol); @@ -19779,11 +19781,11 @@ namespace ts { } } - function checkJSDoc(node: FunctionDeclaration | MethodDeclaration) { - if (!isInJavaScriptFile(node)) { - return; + function checkJSDocTypedefTag(node: JSDocTypedefTag) { + if (!node.typeExpression) { + // If the node had `@property` tags, `typeExpression` would have been set to the first property tag. + error(node.name, Diagnostics.JSDoc_typedef_tag_should_either_have_a_type_annotation_or_be_followed_by_property_or_member_tags); } - forEach(node.jsDoc, checkSourceElement); } function checkJSDocComment(node: JSDoc) { @@ -19795,7 +19797,6 @@ namespace ts { } function checkFunctionOrMethodDeclaration(node: FunctionDeclaration | MethodDeclaration): void { - checkJSDoc(node); checkDecorators(node); checkSignatureDeclaration(node); const functionFlags = getFunctionFlags(node); @@ -22429,6 +22430,12 @@ namespace ts { return; } + if (isInJavaScriptFile(node) && (node as JSDocContainer).jsDoc) { + for (const jsdoc of (node as JSDocContainer).jsDoc) { + checkJSDocComment(jsdoc); + } + } + const kind = node.kind; if (cancellationToken) { // Only bother checking on a few construct kinds. We don't want to be excessively @@ -22483,6 +22490,8 @@ namespace ts { case SyntaxKind.ParenthesizedType: case SyntaxKind.TypeOperator: return checkSourceElement((node).type); + case SyntaxKind.JSDocTypedefTag: + return checkJSDocTypedefTag(node as JSDocTypedefTag); case SyntaxKind.JSDocComment: return checkJSDocComment(node as JSDoc); case SyntaxKind.JSDocParameterTag: diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 662e87d3159..2a8b74d64bd 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -3507,6 +3507,10 @@ "category": "Error", "code": 8020 }, + "JSDoc '@typedef' tag should either have a type annotation or be followed by '@property' or '@member' tags.": { + "category": "Error", + "code": 8021 + }, "Only identifiers/qualified-names with optional type arguments are currently supported in a class 'extends' clause.": { "category": "Error", "code": 9002 diff --git a/tests/baselines/reference/jsdocTypedefMissingType.errors.txt b/tests/baselines/reference/jsdocTypedefMissingType.errors.txt new file mode 100644 index 00000000000..b5b29a47ed9 --- /dev/null +++ b/tests/baselines/reference/jsdocTypedefMissingType.errors.txt @@ -0,0 +1,23 @@ +/a.js(2,14): error TS8021: JSDoc '@typedef' tag should either have a type annotation or be followed by '@property' or '@member' tags. +/a.js(12,11): error TS1005: '{' expected. + + +==== /a.js (2 errors) ==== + // Bad: missing a type + /** @typedef T */ + ~ +!!! error TS8021: JSDoc '@typedef' tag should either have a type annotation or be followed by '@property' or '@member' tags. + + const t = 0; + + // OK: missing a type, but have property tags. + /** + * @typedef Person + * @property {string} name + */ + + /** @type Person */ + ~~~~~~ +!!! error TS1005: '{' expected. + const person = { name: "" }; + \ No newline at end of file diff --git a/tests/baselines/reference/jsdocTypedefMissingType.symbols b/tests/baselines/reference/jsdocTypedefMissingType.symbols new file mode 100644 index 00000000000..1722beccfbc --- /dev/null +++ b/tests/baselines/reference/jsdocTypedefMissingType.symbols @@ -0,0 +1,18 @@ +=== /a.js === +// Bad: missing a type +/** @typedef T */ + +const t = 0; +>t : Symbol(t, Decl(a.js, 3, 5)) + +// OK: missing a type, but have property tags. +/** + * @typedef Person + * @property {string} name + */ + +/** @type Person */ +const person = { name: "" }; +>person : Symbol(person, Decl(a.js, 12, 5)) +>name : Symbol(name, Decl(a.js, 12, 16)) + diff --git a/tests/baselines/reference/jsdocTypedefMissingType.types b/tests/baselines/reference/jsdocTypedefMissingType.types new file mode 100644 index 00000000000..4a3f796ccb8 --- /dev/null +++ b/tests/baselines/reference/jsdocTypedefMissingType.types @@ -0,0 +1,21 @@ +=== /a.js === +// Bad: missing a type +/** @typedef T */ + +const t = 0; +>t : 0 +>0 : 0 + +// OK: missing a type, but have property tags. +/** + * @typedef Person + * @property {string} name + */ + +/** @type Person */ +const person = { name: "" }; +>person : { [x: string]: any; name: string; } +>{ name: "" } : { [x: string]: any; name: string; } +>name : string +>"" : "" + diff --git a/tests/cases/compiler/jsdocTypedefMissingType.ts b/tests/cases/compiler/jsdocTypedefMissingType.ts new file mode 100644 index 00000000000..b4fe9cee170 --- /dev/null +++ b/tests/cases/compiler/jsdocTypedefMissingType.ts @@ -0,0 +1,18 @@ +// @allowJs: true +// @checkJs: true +// @noEmit: true + +// @Filename: /a.js +// Bad: missing a type +/** @typedef T */ + +const t = 0; + +// OK: missing a type, but have property tags. +/** + * @typedef Person + * @property {string} name + */ + +/** @type Person */ +const person = { name: "" }; diff --git a/tests/cases/fourslash/quickInfoJsdocTypedefMissingType.ts b/tests/cases/fourslash/quickInfoJsdocTypedefMissingType.ts new file mode 100644 index 00000000000..a1d6cc74891 --- /dev/null +++ b/tests/cases/fourslash/quickInfoJsdocTypedefMissingType.ts @@ -0,0 +1,11 @@ +/// + +// @allowJs: true + +// @Filename: /a.js +/////** +//// * @typedef /**/A +//// */ +////var x; + +verify.quickInfoAt("", "type A = any");