From d46be35b613d6da8f6eb636e8b5ec3533dfdb629 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Wed, 10 Jul 2024 09:58:29 +0800 Subject: [PATCH] feat(`require-template`): add rule; fixes part of #1120 --- .README/README.md | 2 + .README/rules/require-template.md | 54 ++++++ .github/workflows/feature.yaml | 1 + .ncurc.cjs | 2 +- README.md | 2 + docs/rules/require-template.md | 147 ++++++++++++++++ package.json | 4 +- pnpm-lock.yaml | 215 ++++++++++++++++------- src/bin/generateDocs.js | 4 +- src/index.js | 3 + src/rules/requireTemplate.js | 119 +++++++++++++ test/rules/assertions/requireTemplate.js | 209 ++++++++++++++++++++++ test/rules/ruleNames.json | 1 + 13 files changed, 692 insertions(+), 71 deletions(-) create mode 100644 .README/rules/require-template.md create mode 100644 docs/rules/require-template.md create mode 100644 src/rules/requireTemplate.js create mode 100644 test/rules/assertions/requireTemplate.js diff --git a/.README/README.md b/.README/README.md index dd634b97d..b555190bc 100644 --- a/.README/README.md +++ b/.README/README.md @@ -236,6 +236,7 @@ non-default-recommended fixer). |:heavy_check_mark:|:wrench:|[check-tag-names](./docs/rules/check-tag-names.md#readme)|Reports invalid jsdoc (block) tag names| |:heavy_check_mark:|:wrench:|[check-types](./docs/rules/check-types.md#readme)|Reports types deemed invalid (customizable and with defaults, for preventing and/or recommending replacements)| |:heavy_check_mark:||[check-values](./docs/rules/check-values.md#readme)|Checks for expected content within some miscellaneous tags (`@version`, `@since`, `@license`, `@author`)| +| || [convert-to-jsdoc-comments](./docs/rules/convert-to-jsdoc-comments.md#readme) | Converts line and block comments preceding or following specified nodes into JSDoc comments| |:heavy_check_mark:|:wrench:|[empty-tags](./docs/rules/empty-tags.md#readme)|Checks tags that are expected to be empty (e.g., `@abstract` or `@async`), reporting if they have content| |:heavy_check_mark:||[implements-on-classes](./docs/rules/implements-on-classes.md#readme)|Prohibits use of `@implements` on non-constructor functions (to enforce the tag only being used on classes/constructors)| |||[informative-docs](./docs/rules/informative-docs.md#readme)|Reports on JSDoc texts that serve only to restate their attached name.| @@ -270,6 +271,7 @@ non-default-recommended fixer). |:heavy_check_mark:||[require-returns-check](./docs/rules/require-returns-check.md#readme)|Requires a return statement be present in a function body if a `@returns` tag is specified in the jsdoc comment block (and reports if multiple `@returns` tags are present).| |:heavy_check_mark:||[require-returns-description](./docs/rules/require-returns-description.md#readme)|Requires that the `@returns` tag has a `description` value (not including `void`/`undefined` type returns).| |:heavy_check_mark: (off in TS)||[require-returns-type](./docs/rules/require-returns-type.md#readme)|Requires that `@returns` tag has a type value (in curly brackets).| +| || [require-template](./docs/rules/require-template.md#readme) | Requires `@template` tags be present when type parameters are used.| |||[require-throws](./docs/rules/require-throws.md#readme)|Requires that throw statements are documented| |:heavy_check_mark:||[require-yields](./docs/rules/require-yields.md#readme)|Requires that yields are documented| |:heavy_check_mark:||[require-yields-check](./docs/rules/require-yields-check.md#readme)|Ensures that if a `@yields` is present that a `yield` (or `yield` with a value) is present in the function body (or that if a `@next` is present that there is a `yield` with a return value present)| diff --git a/.README/rules/require-template.md b/.README/rules/require-template.md new file mode 100644 index 000000000..73bff6b8d --- /dev/null +++ b/.README/rules/require-template.md @@ -0,0 +1,54 @@ +# `require-template` + +Checks to see that `@template` tags are present for any detected type +parameters. + +Currently checks `TSTypeAliasDeclaration` such as: + +```ts +export type Pairs = [D, V | undefined]; +``` + +or + +```js +/** + * @typedef {[D, V | undefined]} Pairs + */ +``` + +Note that in the latter TypeScript-flavor JavaScript example, there is no way +for us to firmly distinguish between `D` and `V` as type parameters or as some +other identifiers, so we use an algorithm that any single capital letters +are assumed to be templates. + +## Options + +### `requireSeparateTemplates` + +Requires that each template have its own separate line, i.e., preventing +templates of this format: + +```js +/** + * @template T, U, V + */ +``` + +Defaults to `false`. + +||| +|---|---| +|Context|everywhere| +|Tags|`template`| +|Recommended|true| +|Settings|| +|Options|`requireSeparateTemplates`| + +## Failing examples + + + +## Passing examples + + diff --git a/.github/workflows/feature.yaml b/.github/workflows/feature.yaml index db380e999..e9a9b961f 100644 --- a/.github/workflows/feature.yaml +++ b/.github/workflows/feature.yaml @@ -42,6 +42,7 @@ jobs: node_js_version: - '18' - '20' + - '22' build: runs-on: ubuntu-latest name: Build diff --git a/.ncurc.cjs b/.ncurc.cjs index 503643a43..203f17551 100644 --- a/.ncurc.cjs +++ b/.ncurc.cjs @@ -2,7 +2,7 @@ module.exports = { reject: [ - // Todo: When package converted to ESM only + // Todo: When our package converted to ESM only 'escape-string-regexp', // todo[engine:node@>=20]: Can reenable diff --git a/README.md b/README.md index 5c4192ac2..600e3625d 100644 --- a/README.md +++ b/README.md @@ -263,6 +263,7 @@ non-default-recommended fixer). |:heavy_check_mark:|:wrench:|[check-tag-names](./docs/rules/check-tag-names.md#readme)|Reports invalid jsdoc (block) tag names| |:heavy_check_mark:|:wrench:|[check-types](./docs/rules/check-types.md#readme)|Reports types deemed invalid (customizable and with defaults, for preventing and/or recommending replacements)| |:heavy_check_mark:||[check-values](./docs/rules/check-values.md#readme)|Checks for expected content within some miscellaneous tags (`@version`, `@since`, `@license`, `@author`)| +| || [convert-to-jsdoc-comments](./docs/rules/convert-to-jsdoc-comments.md#readme) | Converts line and block comments preceding or following specified nodes into JSDoc comments| |:heavy_check_mark:|:wrench:|[empty-tags](./docs/rules/empty-tags.md#readme)|Checks tags that are expected to be empty (e.g., `@abstract` or `@async`), reporting if they have content| |:heavy_check_mark:||[implements-on-classes](./docs/rules/implements-on-classes.md#readme)|Prohibits use of `@implements` on non-constructor functions (to enforce the tag only being used on classes/constructors)| |||[informative-docs](./docs/rules/informative-docs.md#readme)|Reports on JSDoc texts that serve only to restate their attached name.| @@ -297,6 +298,7 @@ non-default-recommended fixer). |:heavy_check_mark:||[require-returns-check](./docs/rules/require-returns-check.md#readme)|Requires a return statement be present in a function body if a `@returns` tag is specified in the jsdoc comment block (and reports if multiple `@returns` tags are present).| |:heavy_check_mark:||[require-returns-description](./docs/rules/require-returns-description.md#readme)|Requires that the `@returns` tag has a `description` value (not including `void`/`undefined` type returns).| |:heavy_check_mark: (off in TS)||[require-returns-type](./docs/rules/require-returns-type.md#readme)|Requires that `@returns` tag has a type value (in curly brackets).| +| || [require-template](./docs/rules/require-template.md#readme) | Requires `@template` tags be present when type parameters are used.| |||[require-throws](./docs/rules/require-throws.md#readme)|Requires that throw statements are documented| |:heavy_check_mark:||[require-yields](./docs/rules/require-yields.md#readme)|Requires that yields are documented| |:heavy_check_mark:||[require-yields-check](./docs/rules/require-yields-check.md#readme)|Ensures that if a `@yields` is present that a `yield` (or `yield` with a value) is present in the function body (or that if a `@next` is present that there is a `yield` with a return value present)| diff --git a/docs/rules/require-template.md b/docs/rules/require-template.md new file mode 100644 index 000000000..8bef1adfb --- /dev/null +++ b/docs/rules/require-template.md @@ -0,0 +1,147 @@ + + +# require-template + +Checks to see that `@template` tags are present for any detected type +parameters. + +Currently checks `TSTypeAliasDeclaration` such as: + +```ts +export type Pairs = [D, V | undefined]; +``` + +or + +```js +/** + * @typedef {[D, V | undefined]} Pairs + */ +``` + +Note that in the latter TypeScript-flavor JavaScript example, there is no way +for us to firmly distinguish between `D` and `V` as type parameters or as some +other identifiers, so we use an algorithm that any single capital letters +are assumed to be templates. + + + +## Options + + + +### requireSeparateTemplates + +Requires that each template have its own separate line, i.e., preventing +templates of this format: + +```js +/** + * @template T, U, V + */ +``` + +Defaults to `false`. + +||| +|---|---| +|Context|everywhere| +|Tags|`template`| +|Recommended|true| +|Settings|| +|Options|`requireSeparateTemplates`| + + + +## Failing examples + +The following patterns are considered problems: + +````js +/** + * + */ +type Pairs = [D, V | undefined]; +// Message: Missing @template D + +/** + * + */ +export type Pairs = [D, V | undefined]; +// Message: Missing @template D + +/** + * @typedef {[D, V | undefined]} Pairs + */ +// Message: Missing @template D + +/** + * @typedef {[D, V | undefined]} Pairs + */ +// Settings: {"jsdoc":{"mode":"permissive"}} +// Message: Missing @template D + +/** + * @template D, U + */ +export type Extras = [D, U, V | undefined]; +// Message: Missing @template V + +/** + * @template D, U + * @typedef {[D, U, V | undefined]} Extras + */ +// Message: Missing @template V + +/** + * @template D, V + */ +export type Pairs = [D, V | undefined]; +// "jsdoc/require-template": ["error"|"warn", {"requireSeparateTemplates":true}] +// Message: Missing separate @template for V + +/** + * @template D, V + * @typedef {[D, V | undefined]} Pairs + */ +// "jsdoc/require-template": ["error"|"warn", {"requireSeparateTemplates":true}] +// Message: Missing separate @template for V +```` + + + + + +## Passing examples + +The following patterns are not considered problems: + +````js +/** + * @template D + * @template V + */ +export type Pairs = [D, V | undefined]; + +/** + * @template D + * @template V + * @typedef {[D, V | undefined]} Pairs + */ + +/** + * @template D, U, V + */ +export type Extras = [D, U, V | undefined]; + +/** + * @template D, U, V + * @typedef {[D, U, V | undefined]} Extras + */ + +/** + * @typedef {[D, U, V | undefined]} Extras + * @typedef {[D, U, V | undefined]} Extras + */ +```` + diff --git a/package.json b/package.json index 27bf8a661..2fc6d945c 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "url": "http://gajus.com" }, "dependencies": { - "@es-joy/jsdoccomment": "~0.45.0", + "@es-joy/jsdoccomment": "~0.46.0", "are-docs-informative": "^0.0.2", "comment-parser": "1.4.1", "debug": "^4.3.5", @@ -54,7 +54,7 @@ "eslint": "9.6.0", "eslint-config-canonical": "~43.0.13", "espree": "^10.1.0", - "gitdown": "^3.1.5", + "gitdown": "^4.0.0", "glob": "^10.4.2", "globals": "^15.8.0", "husky": "^9.0.11", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cebb6297b..dece4c064 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: dependencies: '@es-joy/jsdoccomment': - specifier: ~0.45.0 - version: 0.45.0 + specifier: ~0.46.0 + version: 0.46.0 are-docs-informative: specifier: ^0.0.2 version: 0.0.2 @@ -148,8 +148,8 @@ importers: specifier: ^10.1.0 version: 10.1.0 gitdown: - specifier: ^3.1.5 - version: 3.1.5 + specifier: ^4.0.0 + version: 4.0.0(re2@1.20.9) glob: specifier: ^10.4.2 version: 10.4.2 @@ -952,8 +952,8 @@ packages: resolution: {integrity: sha512-I238eDtOolvCuvtxrnqtlBaw0BwdQuYqK7eA6XIonicMdOOOb75mqdIzkGDUbS04+1Di007rgm9snFRNeVrOog==} engines: {node: '>=16'} - '@es-joy/jsdoccomment@0.45.0': - resolution: {integrity: sha512-U8T5eXLkP78Sr12rR91494GhlEgp8jqs7OaUHbdUffADxU1JQmKjZm5uSyAEGv2oolDMJ+wce7yylfnnwOevtA==} + '@es-joy/jsdoccomment@0.46.0': + resolution: {integrity: sha512-C3Axuq1xd/9VqFZpW4YAzOx5O9q/LP46uIQy/iNDpHG3fmPa6TBtvfglMCs3RBiBxAIi0Go97r8+jvTt55XMyQ==} engines: {node: '>=16'} '@eslint-community/eslint-utils@4.4.0': @@ -2015,6 +2015,10 @@ packages: resolution: {integrity: sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==} engines: {node: '>=6'} + clone-regexp@3.0.0: + resolution: {integrity: sha512-ujdnoq2Kxb8s3ItNBtnYeXdm07FcU0u8ARAT1lQ2YdMwQC+cdiXX8KoqMVuglztILivceTtp4ivqGSmEmhBUJw==} + engines: {node: '>=12'} + collection-visit@1.0.0: resolution: {integrity: sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==} engines: {node: '>=0.10.0'} @@ -2854,9 +2858,9 @@ packages: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} - filesize@6.4.0: - resolution: {integrity: sha512-mjFIpOHC4jbfcTfoh4rkWpI31mF7viw9ikj/JyLoKzqlwG/YsefKfvYlYhdYdg/9mtK2z1AzgN/0LvVQ3zdlSQ==} - engines: {node: '>= 0.4.0'} + filesize@10.1.4: + resolution: {integrity: sha512-ryBwPIIeErmxgPnm6cbESAzXjuEFubs+yKYLBZvg3CaiNcmkJChoOGcBSrZ6IwkMwPABwPpVXE6IlNdGJJrvEg==} + engines: {node: '>= 10.4.0'} fill-range@4.0.0: resolution: {integrity: sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==} @@ -2960,6 +2964,10 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + function-timeout@0.1.1: + resolution: {integrity: sha512-0NVVC0TaP7dSTvn1yMiy6d6Q8gifzbvQafO46RtLG/kHJUBNd+pVRGOBoK44wNBvtSPUJRfdVvkFdD3p0xvyZg==} + engines: {node: '>=14.16'} + function-timeout@1.0.1: resolution: {integrity: sha512-6yPMImFFuaMPNaTMTBuolA8EanHJWF5Vju0NHpObRURT105J6x1Mf2a7J4P7Sqk2xDxv24N5L0RatEhTBhNmdA==} engines: {node: '>=18'} @@ -3025,9 +3033,9 @@ packages: get-tsconfig@4.7.2: resolution: {integrity: sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==} - get-urls@10.0.1: - resolution: {integrity: sha512-5usUMF4FE0cTS7yUuRvBsB4tSs5u8KFn7v01+IDGL/18tuoADu5ehthqw+y2EHnJsWhsp1443K5P+C/XWZQ04g==} - engines: {node: '>=10.12.0'} + get-urls@12.1.0: + resolution: {integrity: sha512-qHO+QmPiI1bEw0Y/m+WMAAx/UoEEXLZwEx0DVaKMtlHNrKbMeV960LryIpd+E2Ykb9XkVHmVtpbCsmul3GhR0g==} + engines: {node: '>=16'} get-value@2.0.6: resolution: {integrity: sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==} @@ -3039,9 +3047,9 @@ packages: git-log-parser@1.2.0: resolution: {integrity: sha512-rnCVNfkTL8tdNryFuaY0fYiBWEBcgF748O6ZI61rslBvr2o7U65c2/6npCRqH40vuAhtgtDiqLTJjBVdrejCzA==} - gitdown@3.1.5: - resolution: {integrity: sha512-nvdK4qp8yQdzbdHgnEUuC0ubfAvv27fHNpsX9/0FLJvAJk687zGkptRNvls4U5UNYMKdunIL84QR+qQKyHMDaw==} - engines: {node: '>=10'} + gitdown@4.0.0: + resolution: {integrity: sha512-yzkDLVzsYkM86p+a3XtW2gdBqs3Jw0cn20Fb/pAaiag5JNzApj7xDM+kcYiFZef/CoIuEr8D+hR1wzgky9ZLYg==} + engines: {node: '>=18'} hasBin: true gitinfo@2.4.0: @@ -3065,6 +3073,10 @@ packages: engines: {node: '>=16 || 14 >=14.18'} hasBin: true + glob@10.4.5: + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + hasBin: true + glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Glob versions prior to v9 are no longer supported @@ -3506,6 +3518,10 @@ packages: resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} engines: {node: '>= 0.4'} + is-regexp@3.1.0: + resolution: {integrity: sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA==} + engines: {node: '>=12'} + is-set@2.0.2: resolution: {integrity: sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==} @@ -3890,9 +3906,9 @@ packages: engines: {node: '>= 18'} hasBin: true - marked@2.1.3: - resolution: {integrity: sha512-/Q+7MGzaETqifOMWYEA7HVMaZb4XbcRfaOzcSsHZEith83KGlvaSG33u0SKu89Mj5h+T8V2hM+8O45Qc5XTgwA==} - engines: {node: '>= 10'} + marked@13.0.2: + resolution: {integrity: sha512-J6CPjP8pS5sgrRqxVRvkCIkZ6MFdRIjDkwUwgJ9nL2fbmM6qGQeB2C16hi8Cc9BOzj6xXzy0jyi0iPIfnMHYzA==} + engines: {node: '>= 18'} hasBin: true meow@13.2.0: @@ -4111,10 +4127,6 @@ packages: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} - normalize-url@5.3.1: - resolution: {integrity: sha512-K1c7+vaAP+Yh5bOGmA10PGPpp+6h7WZrl7GwqKhUflBc9flU9pzG27DDeB9+iuhZkE3BJZOcgN1P/2sS5pqrWw==} - engines: {node: '>=10'} - normalize-url@8.0.0: resolution: {integrity: sha512-uVFpKhj5MheNBJRTiMZ9pE/7hD1QTeEvugSJW/OmLzAp78PB5O6adfMNTvmfKhXBkvCzC+rqifWcVYpGFwTjnw==} engines: {node: '>=14.16'} @@ -5117,6 +5129,10 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + super-regex@0.2.0: + resolution: {integrity: sha512-WZzIx3rC1CvbMDloLsVw0lkZVKJWbrkJ0k1ghKFmcnPrW1+jWbgTkTEWVtD9lMdmI4jZEz40+naBxl1dCUhXXw==} + engines: {node: '>=14.16'} + super-regex@1.0.0: resolution: {integrity: sha512-CY8u7DtbvucKuquCmOFEKhr9Besln7n9uN8eFbwcoGYWXOMW07u2o8njWaiXt11ylS3qoGF55pILjRmPlbodyg==} engines: {node: '>=18'} @@ -5420,10 +5436,14 @@ packages: resolution: {integrity: sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - url-regex-safe@2.1.0: - resolution: {integrity: sha512-nfrmOb62+kN4JJRkY/w9QzZeuc63/ddIFfqS9ntok0UYsJW3C5J/jGvYZecwk8V68og2zWA7fcVn4GUXIi5qbg==} - engines: {node: '>= 10.12.0'} - deprecated: Please upgrade to url-regex-safe@v3.0.0+ AND install re2 as an additional dependency in your project via `npm install re2` or `yarn add re2`. + url-regex-safe@4.0.0: + resolution: {integrity: sha512-BrnFCWKNFrFnRzKD66NtJqQepfJrUHNPvPxE5y5NSAhXBb4OlobQjt7907Jm4ItPiXaeX+dDWMkcnOd4jR9N8A==} + engines: {node: '>= 14'} + peerDependencies: + re2: ^1.20.1 + peerDependenciesMeta: + re2: + optional: true url-regexp@1.0.2: resolution: {integrity: sha512-Tt0N/yu3iNSCqZ7wJ6AxTtF/QSemtfzLH+astikB0CR/u/7X132VaBdiNEXbiAGiU+LXsIpyB2Hqz8OY4zw8MA==} @@ -6650,10 +6670,8 @@ snapshots: esquery: 1.6.0 jsdoc-type-pratt-parser: 4.0.0 - '@es-joy/jsdoccomment@0.45.0': + '@es-joy/jsdoccomment@0.46.0': dependencies: - '@types/eslint': 8.56.10 - '@types/estree': 1.0.5 comment-parser: 1.4.1 esquery: 1.6.0 jsdoc-type-pratt-parser: 4.0.0 @@ -7007,10 +7025,12 @@ snapshots: socks-proxy-agent: 8.0.2 transitivePeerDependencies: - supports-color + optional: true '@npmcli/fs@3.1.0': dependencies: semver: 7.6.2 + optional: true '@octokit/auth-token@5.1.0': {} @@ -7555,7 +7575,8 @@ snapshots: fast-url-parser: 1.1.3 tslib: 2.6.2 - abbrev@2.0.0: {} + abbrev@2.0.0: + optional: true acorn-globals@1.0.9: dependencies: @@ -7585,6 +7606,7 @@ snapshots: dependencies: clean-stack: 2.2.0 indent-string: 4.0.0 + optional: true aggregate-error@5.0.0: dependencies: @@ -7917,6 +7939,7 @@ snapshots: ssri: 10.0.5 tar: 6.2.0 unique-filename: 3.0.0 + optional: true cache-base@1.0.1: dependencies: @@ -7999,7 +8022,8 @@ snapshots: optionalDependencies: fsevents: 2.3.3 - chownr@2.0.0: {} + chownr@2.0.0: + optional: true ci-info@4.0.0: {} @@ -8014,7 +8038,8 @@ snapshots: dependencies: escape-string-regexp: 1.0.5 - clean-stack@2.2.0: {} + clean-stack@2.2.0: + optional: true clean-stack@5.2.0: dependencies: @@ -8068,6 +8093,10 @@ snapshots: kind-of: 6.0.3 shallow-clone: 3.0.1 + clone-regexp@3.0.0: + dependencies: + is-regexp: 3.1.0 + collection-visit@1.0.0: dependencies: map-visit: 1.0.0 @@ -8416,7 +8445,8 @@ snapshots: env-paths@2.2.1: {} - err-code@2.0.3: {} + err-code@2.0.3: + optional: true error-ex@1.3.2: dependencies: @@ -9151,7 +9181,8 @@ snapshots: transitivePeerDependencies: - supports-color - exponential-backoff@3.1.1: {} + exponential-backoff@3.1.1: + optional: true extend-shallow@2.0.1: dependencies: @@ -9231,7 +9262,7 @@ snapshots: dependencies: flat-cache: 4.0.1 - filesize@6.4.0: {} + filesize@10.1.4: {} fill-range@4.0.0: dependencies: @@ -9327,10 +9358,12 @@ snapshots: fs-minipass@2.1.0: dependencies: minipass: 3.3.6 + optional: true fs-minipass@3.0.3: dependencies: minipass: 7.1.2 + optional: true fs-readdir-recursive@1.1.0: {} @@ -9341,6 +9374,8 @@ snapshots: function-bind@1.1.2: {} + function-timeout@0.1.1: {} + function-timeout@1.0.1: {} function.prototype.name@1.1.6: @@ -9397,12 +9432,13 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 - get-urls@10.0.1: + get-urls@12.1.0(re2@1.20.9): dependencies: - normalize-url: 5.3.1 - url-regex-safe: 2.1.0 + normalize-url: 8.0.0 + super-regex: 0.2.0 + url-regex-safe: 4.0.0(re2@1.20.9) transitivePeerDependencies: - - supports-color + - re2 get-value@2.0.6: {} @@ -9419,23 +9455,23 @@ snapshots: through2: 2.0.5 traverse: 0.6.8 - gitdown@3.1.5: + gitdown@4.0.0(re2@1.20.9): dependencies: bluebird: 3.7.2 deadlink: 1.1.3 - filesize: 6.4.0 - get-urls: 10.0.1 + filesize: 10.1.4 + get-urls: 12.1.0(re2@1.20.9) gitinfo: 2.4.0 - glob: 7.2.3 + glob: 10.4.5 jsonfile: 6.1.0 lodash: 4.17.21 markdown-contents: 1.0.11 - marked: 2.1.3 + marked: 13.0.2 moment: 2.30.1 stack-trace: 0.0.10 - yargs: 16.2.0 + yargs: 17.7.2 transitivePeerDependencies: - - supports-color + - re2 gitinfo@2.4.0: dependencies: @@ -9467,6 +9503,15 @@ snapshots: package-json-from-dist: 1.0.0 path-scurry: 1.11.1 + glob@10.4.5: + dependencies: + foreground-child: 3.1.1 + jackspeak: 3.1.2 + minimatch: 9.0.4 + minipass: 7.1.2 + package-json-from-dist: 1.0.0 + path-scurry: 1.11.1 + glob@7.2.3: dependencies: fs.realpath: 1.0.0 @@ -9645,7 +9690,8 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 - http-cache-semantics@4.1.1: {} + http-cache-semantics@4.1.1: + optional: true http-proxy-agent@7.0.0: dependencies: @@ -9720,7 +9766,8 @@ snapshots: ini@1.3.8: {} - install-artifact-from-github@1.3.5: {} + install-artifact-from-github@1.3.5: + optional: true internal-slot@1.0.7: dependencies: @@ -9735,7 +9782,8 @@ snapshots: ip-regex@4.3.0: {} - ip@2.0.1: {} + ip@2.0.1: + optional: true is-accessor-descriptor@1.0.1: dependencies: @@ -9845,7 +9893,8 @@ snapshots: dependencies: js-types: 1.0.0 - is-lambda@1.0.1: {} + is-lambda@1.0.1: + optional: true is-map@2.0.2: {} @@ -9893,6 +9942,8 @@ snapshots: call-bind: 1.0.7 has-tostringtag: 1.0.2 + is-regexp@3.1.0: {} + is-set@2.0.2: {} is-shared-array-buffer@1.0.3: @@ -9948,7 +9999,8 @@ snapshots: isexe@2.0.0: {} - isexe@3.1.1: {} + isexe@3.1.1: + optional: true isobject@2.1.0: dependencies: @@ -10276,6 +10328,7 @@ snapshots: ssri: 10.0.5 transitivePeerDependencies: - supports-color + optional: true map-cache@0.2.2: {} @@ -10300,7 +10353,7 @@ snapshots: marked@12.0.1: {} - marked@2.1.3: {} + marked@13.0.2: {} meow@13.2.0: {} @@ -10380,6 +10433,7 @@ snapshots: minipass-collect@2.0.1: dependencies: minipass: 7.1.2 + optional: true minipass-fetch@3.0.4: dependencies: @@ -10388,24 +10442,30 @@ snapshots: minizlib: 2.1.2 optionalDependencies: encoding: 0.1.13 + optional: true minipass-flush@1.0.5: dependencies: minipass: 3.3.6 + optional: true minipass-pipeline@1.2.4: dependencies: minipass: 3.3.6 + optional: true minipass-sized@1.0.3: dependencies: minipass: 3.3.6 + optional: true minipass@3.3.6: dependencies: yallist: 4.0.0 + optional: true - minipass@5.0.0: {} + minipass@5.0.0: + optional: true minipass@7.1.2: {} @@ -10413,13 +10473,15 @@ snapshots: dependencies: minipass: 3.3.6 yallist: 4.0.0 + optional: true mixin-deep@1.3.2: dependencies: for-in: 1.0.2 is-extendable: 1.0.1 - mkdirp@1.0.4: {} + mkdirp@1.0.4: + optional: true mocha@10.6.0: dependencies: @@ -10458,7 +10520,8 @@ snapshots: object-assign: 4.1.1 thenify-all: 1.6.0 - nan@2.18.0: {} + nan@2.18.0: + optional: true nanomatch@1.2.13: dependencies: @@ -10480,7 +10543,8 @@ snapshots: natural-compare@1.4.0: {} - negotiator@0.6.3: {} + negotiator@0.6.3: + optional: true neo-async@2.6.2: {} @@ -10520,12 +10584,14 @@ snapshots: which: 4.0.0 transitivePeerDependencies: - supports-color + optional: true node-releases@2.0.14: {} nopt@7.2.0: dependencies: abbrev: 2.0.0 + optional: true normalize-package-data@2.5.0: dependencies: @@ -10547,8 +10613,6 @@ snapshots: normalize-path@3.0.0: {} - normalize-url@5.3.1: {} - normalize-url@8.0.0: {} npm-path@2.0.4: @@ -10730,6 +10794,7 @@ snapshots: p-map@4.0.0: dependencies: aggregate-error: 3.1.0 + optional: true p-map@7.0.1: {} @@ -10856,7 +10921,8 @@ snapshots: dependencies: parse-ms: 4.0.0 - proc-log@3.0.0: {} + proc-log@3.0.0: + optional: true process-nextick-args@2.0.1: {} @@ -10864,6 +10930,7 @@ snapshots: dependencies: err-code: 2.0.3 retry: 0.12.0 + optional: true prop-types@15.8.1: dependencies: @@ -10920,6 +10987,7 @@ snapshots: node-gyp: 10.0.1 transitivePeerDependencies: - supports-color + optional: true react-is@16.13.1: {} @@ -11115,7 +11183,8 @@ snapshots: ret@0.1.15: {} - retry@0.12.0: {} + retry@0.12.0: + optional: true reusify@1.0.4: {} @@ -11305,7 +11374,8 @@ snapshots: ansi-styles: 6.2.1 is-fullwidth-code-point: 5.0.0 - smart-buffer@4.2.0: {} + smart-buffer@4.2.0: + optional: true snapdragon-node@2.1.1: dependencies: @@ -11337,11 +11407,13 @@ snapshots: socks: 2.7.1 transitivePeerDependencies: - supports-color + optional: true socks@2.7.1: dependencies: ip: 2.0.1 smart-buffer: 4.2.0 + optional: true source-map-resolve@0.5.3: dependencies: @@ -11411,6 +11483,7 @@ snapshots: ssri@10.0.5: dependencies: minipass: 7.1.2 + optional: true stack-trace@0.0.10: {} @@ -11529,6 +11602,12 @@ snapshots: strip-json-comments@3.1.1: {} + super-regex@0.2.0: + dependencies: + clone-regexp: 3.0.0 + function-timeout: 0.1.1 + time-span: 5.1.0 + super-regex@1.0.0: dependencies: function-timeout: 1.0.1 @@ -11579,6 +11658,7 @@ snapshots: minizlib: 2.1.2 mkdirp: 1.0.4 yallist: 4.0.0 + optional: true temp-dir@3.0.0: {} @@ -11793,10 +11873,12 @@ snapshots: unique-filename@3.0.0: dependencies: unique-slug: 4.0.0 + optional: true unique-slug@4.0.0: dependencies: imurmurhash: 0.1.4 + optional: true unique-string@3.0.0: dependencies: @@ -11829,13 +11911,12 @@ snapshots: url-join@5.0.0: {} - url-regex-safe@2.1.0: + url-regex-safe@4.0.0(re2@1.20.9): dependencies: ip-regex: 4.3.0 - re2: 1.20.9 tlds: 1.248.0 - transitivePeerDependencies: - - supports-color + optionalDependencies: + re2: 1.20.9 url-regexp@1.0.2: {} @@ -11950,6 +12031,7 @@ snapshots: which@4.0.0: dependencies: isexe: 3.1.1 + optional: true word-wrap@1.2.5: {} @@ -12003,7 +12085,8 @@ snapshots: yallist@3.1.1: {} - yallist@4.0.0: {} + yallist@4.0.0: + optional: true yaml-eslint-parser@1.2.3: dependencies: diff --git a/src/bin/generateDocs.js b/src/bin/generateDocs.js index 1e66ba602..a771b6139 100644 --- a/src/bin/generateDocs.js +++ b/src/bin/generateDocs.js @@ -149,8 +149,8 @@ const generateDocs = async () => { ); }), ...otherPaths, - ].map((docPath) => { - const gitdown = Gitdown.readFile(docPath); + ].map(async (docPath) => { + const gitdown = await Gitdown.readFile(docPath); gitdown.setConfig({ gitinfo: { diff --git a/src/index.js b/src/index.js index 535ed0b62..a6a6bae64 100644 --- a/src/index.js +++ b/src/index.js @@ -45,6 +45,7 @@ import requireReturns from './rules/requireReturns.js'; import requireReturnsCheck from './rules/requireReturnsCheck.js'; import requireReturnsDescription from './rules/requireReturnsDescription.js'; import requireReturnsType from './rules/requireReturnsType.js'; +import requireTemplate from './rules/requireTemplate.js'; import requireThrows from './rules/requireThrows.js'; import requireYields from './rules/requireYields.js'; import requireYieldsCheck from './rules/requireYieldsCheck.js'; @@ -118,6 +119,7 @@ const index = { 'require-returns-check': requireReturnsCheck, 'require-returns-description': requireReturnsDescription, 'require-returns-type': requireReturnsType, + 'require-template': requireTemplate, 'require-throws': requireThrows, 'require-yields': requireYields, 'require-yields-check': requireYieldsCheck, @@ -191,6 +193,7 @@ const createRecommendedRuleset = (warnOrError, flatName) => { 'jsdoc/require-returns-check': warnOrError, 'jsdoc/require-returns-description': warnOrError, 'jsdoc/require-returns-type': warnOrError, + 'jsdoc/require-template': 'off', 'jsdoc/require-throws': 'off', 'jsdoc/require-yields': warnOrError, 'jsdoc/require-yields-check': warnOrError, diff --git a/src/rules/requireTemplate.js b/src/rules/requireTemplate.js new file mode 100644 index 000000000..015902eb0 --- /dev/null +++ b/src/rules/requireTemplate.js @@ -0,0 +1,119 @@ +import { + parse as parseType, + traverse, + tryParse as tryParseType, +} from '@es-joy/jsdoccomment'; +import iterateJsdoc from '../iterateJsdoc.js'; + +export default iterateJsdoc(({ + context, + utils, + node, + settings, + report, +}) => { + const { + requireSeparateTemplates = false, + } = context.options[0] || {}; + + const { + mode + } = settings; + + const usedNames = new Set(); + const templateTags = utils.getTags('template'); + const templateNames = templateTags.flatMap(({name}) => { + return name.split(/,\s*/); + }); + + for (const tag of templateTags) { + const {name} = tag; + const names = name.split(/,\s*/); + if (requireSeparateTemplates && names.length > 1) { + report(`Missing separate @template for ${names[1]}`, null, tag); + } + } + + /** + * @param {import('@typescript-eslint/types').TSESTree.TSTypeAliasDeclaration} aliasDeclaration + */ + const checkParameters = (aliasDeclaration) => { + /* c8 ignore next -- Guard */ + const {params} = aliasDeclaration.typeParameters ?? {params: []}; + for (const {name: {name}} of params) { + usedNames.add(name); + } + for (const usedName of usedNames) { + if (!templateNames.includes(usedName)) { + report(`Missing @template ${usedName}`); + } + } + }; + + const handleTypeAliases = () => { + const nde = /** @type {import('@typescript-eslint/types').TSESTree.Node} */ ( + node + ); + if (!nde) { + return; + } + switch (nde.type) { + case 'ExportNamedDeclaration': + if (nde.declaration?.type === 'TSTypeAliasDeclaration') { + checkParameters(nde.declaration); + } + break; + case 'TSTypeAliasDeclaration': + checkParameters(nde); + break; + } + }; + + const typedefTags = utils.getTags('typedef'); + if (!typedefTags.length || typedefTags.length >= 2) { + handleTypeAliases(); + return; + } + + const potentialType = typedefTags[0].type; + const parsedType = mode === 'permissive' ? + tryParseType(/** @type {string} */ (potentialType)) : + parseType(/** @type {string} */ (potentialType), mode) + + traverse(parsedType, (nde) => { + const { + type, + value, + } = /** @type {import('jsdoc-type-pratt-parser').NameResult} */ (nde); + if (type === 'JsdocTypeName' && (/^[A-Z]$/).test(value)) { + usedNames.add(value); + } + }); + + // Could check against whitelist/blacklist + for (const usedName of usedNames) { + if (!templateNames.includes(usedName)) { + report(`Missing @template ${usedName}`, null, typedefTags[0]); + } + } +}, { + iterateAllJsdocs: true, + meta: { + docs: { + description: 'Requires template tags for each generic type parameter', + url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/require-template.md#repos-sticky-header', + }, + schema: [ + { + additionalProperties: false, + properties: { + requireSeparateTemplates: { + type: 'boolean' + } + }, + type: 'object', + }, + ], + type: 'suggestion', + }, +}); diff --git a/test/rules/assertions/requireTemplate.js b/test/rules/assertions/requireTemplate.js new file mode 100644 index 000000000..6bb092b45 --- /dev/null +++ b/test/rules/assertions/requireTemplate.js @@ -0,0 +1,209 @@ +import {parser as typescriptEslintParser} from 'typescript-eslint'; + +export default { + invalid: [ + { + code: ` + /** + * + */ + type Pairs = [D, V | undefined]; + `, + errors: [ + { + line: 2, + message: 'Missing @template D', + }, + { + line: 2, + message: 'Missing @template V', + }, + ], + languageOptions: { + parser: typescriptEslintParser + }, + }, + { + code: ` + /** + * + */ + export type Pairs = [D, V | undefined]; + `, + errors: [ + { + line: 2, + message: 'Missing @template D', + }, + { + line: 2, + message: 'Missing @template V', + }, + ], + languageOptions: { + parser: typescriptEslintParser + }, + }, + { + code: ` + /** + * @typedef {[D, V | undefined]} Pairs + */ + `, + errors: [ + { + line: 3, + message: 'Missing @template D', + }, + { + line: 3, + message: 'Missing @template V', + }, + ], + }, + { + code: ` + /** + * @typedef {[D, V | undefined]} Pairs + */ + `, + errors: [ + { + line: 3, + message: 'Missing @template D', + }, + { + line: 3, + message: 'Missing @template V', + }, + ], + settings: { + jsdoc: { + mode: 'permissive', + }, + }, + }, + { + code: ` + /** + * @template D, U + */ + export type Extras = [D, U, V | undefined]; + `, + errors: [ + { + line: 2, + message: 'Missing @template V', + }, + ], + languageOptions: { + parser: typescriptEslintParser + }, + }, + { + code: ` + /** + * @template D, U + * @typedef {[D, U, V | undefined]} Extras + */ + `, + errors: [ + { + line: 4, + message: 'Missing @template V', + }, + ], + }, + { + code: ` + /** + * @template D, V + */ + export type Pairs = [D, V | undefined]; + `, + errors: [ + { + line: 3, + message: 'Missing separate @template for V', + }, + ], + languageOptions: { + parser: typescriptEslintParser + }, + options: [ + { + requireSeparateTemplates: true, + } + ], + }, + { + code: ` + /** + * @template D, V + * @typedef {[D, V | undefined]} Pairs + */ + `, + errors: [ + { + line: 3, + message: 'Missing separate @template for V', + }, + ], + options: [ + { + requireSeparateTemplates: true, + } + ], + }, + ], + valid: [ + { + code: ` + /** + * @template D + * @template V + */ + export type Pairs = [D, V | undefined]; + `, + languageOptions: { + parser: typescriptEslintParser + }, + }, + { + code: ` + /** + * @template D + * @template V + * @typedef {[D, V | undefined]} Pairs + */ + `, + }, + { + code: ` + /** + * @template D, U, V + */ + export type Extras = [D, U, V | undefined]; + `, + languageOptions: { + parser: typescriptEslintParser + }, + }, + { + code: ` + /** + * @template D, U, V + * @typedef {[D, U, V | undefined]} Extras + */ + `, + }, + { + code: ` + /** + * @typedef {[D, U, V | undefined]} Extras + * @typedef {[D, U, V | undefined]} Extras + */ + `, + }, + ], +}; diff --git a/test/rules/ruleNames.json b/test/rules/ruleNames.json index 15418c0d8..c158f48e4 100644 --- a/test/rules/ruleNames.json +++ b/test/rules/ruleNames.json @@ -46,6 +46,7 @@ "require-returns-check", "require-returns-description", "require-returns-type", + "require-template", "require-throws", "require-yields", "require-yields-check",