From d5fa8da257443f59898d9257b1a9993695c7e51d Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Fri, 5 Jul 2024 19:49:16 +0800 Subject: [PATCH] feat: new rule `convert-to-jsdoc-comments`; fixes #1002 --- .README/rules/convert-to-jsdoc-comments.md | 62 +++ docs/rules/convert-to-jsdoc-comments.md | 179 +++++++++ package.json | 2 +- pnpm-lock.yaml | 17 +- src/bin/generateRule.js | 2 +- src/index.js | 3 + src/rules/convertToJsdocComments.js | 213 ++++++++++ .../assertions/convertToJsdocComments.js | 369 ++++++++++++++++++ test/rules/ruleNames.json | 1 + 9 files changed, 844 insertions(+), 4 deletions(-) create mode 100644 .README/rules/convert-to-jsdoc-comments.md create mode 100644 docs/rules/convert-to-jsdoc-comments.md create mode 100644 src/rules/convertToJsdocComments.js create mode 100644 test/rules/assertions/convertToJsdocComments.js diff --git a/.README/rules/convert-to-jsdoc-comments.md b/.README/rules/convert-to-jsdoc-comments.md new file mode 100644 index 000000000..4b9874a4b --- /dev/null +++ b/.README/rules/convert-to-jsdoc-comments.md @@ -0,0 +1,62 @@ +# `convert-to-jsdoc-comments` + +Converts single line or non-JSDoc, multiline comments into JSDoc comments. + +## Options + +### `enableFixer` + +Set to `false` to disable fixing. + +### `lineOrBlockStyle` + +What style of comments to which to apply JSDoc conversion. + +- `block` - Applies to block-style comments (`/* ... */`) +- `line` - Applies to line-style comments (`// ...`) +- `both` - Applies to both block and line-style comments + +Defaults to `both`. + +### `enforceJsdocLineStyle` + +What policy to enforce on the conversion of non-JSDoc comments without +line breaks. (Non-JSDoc (mulitline) comments with line breaks will always +be converted to `multi` style JSDoc comments.) + +- `multi` - Convert to multi-line style +```js +/** + * Some text + */ +``` +- `single` - Convert to single-line style +```js +/** Some text */ +``` + +Defaults to `multi`. + +### `allowedPrefixes` + +An array of prefixes to allow at the beginning of a comment. + +Defaults to `['@ts-', 'istanbul ', 'c8 ', 'v8 ', 'eslint', 'prettier-']`. + +Supplying your own value overrides the defaults. + +||| +|---|---| +|Context|`ArrowFunctionExpression`, `FunctionDeclaration`, `FunctionExpression`| +|Tags|(N/A)| +|Recommended|false| +|Settings|`minLines`, `maxLines`| +|Options|`enableFixer`, `enforceJsdocLineStyle`, `lineOrBlockStyle`| + +## Failing examples + + + +## Passing examples + + diff --git a/docs/rules/convert-to-jsdoc-comments.md b/docs/rules/convert-to-jsdoc-comments.md new file mode 100644 index 000000000..330721e40 --- /dev/null +++ b/docs/rules/convert-to-jsdoc-comments.md @@ -0,0 +1,179 @@ + + +# convert-to-jsdoc-comments + +Converts single line or non-JSDoc, multiline comments into JSDoc comments. + + + +## Options + + + +### enableFixer + +Set to `false` to disable fixing. + + + +### lineOrBlockStyle + +What style of comments to which to apply JSDoc conversion. + +- `block` - Applies to block-style comments (`/* ... */`) +- `line` - Applies to line-style comments (`// ...`) +- `both` - Applies to both block and line-style comments + +Defaults to `both`. + + + +### enforceJsdocLineStyle + +What policy to enforce on the conversion of non-JSDoc comments without +line breaks. (Non-JSDoc (mulitline) comments with line breaks will always +be converted to `multi` style JSDoc comments.) + +- `multi` - Convert to multi-line style +```js +/** + * Some text + */ +``` +- `single` - Convert to single-line style +```js +/** Some text */ +``` + +Defaults to `multi`. + + + +### allowedPrefixes + +An array of prefixes to allow at the beginning of a comment. + +Defaults to `['@ts-', 'istanbul ', 'c8 ', 'v8 ', 'eslint', 'prettier-']`. + +Supplying your own value overrides the defaults. + +||| +|---|---| +|Context|`ArrowFunctionExpression`, `FunctionDeclaration`, `FunctionExpression`| +|Tags|(N/A)| +|Recommended|false| +|Settings|`minLines`, `maxLines`| +|Options|`enableFixer`, `enforceJsdocLineStyle`, `lineOrBlockStyle`| + + + +## Failing examples + +The following patterns are considered problems: + +````js +// A single line comment +function quux () {} +// "jsdoc/convert-to-jsdoc-comments": ["error"|"warn", {"enforceJsdocLineStyle":"single"}] +// Message: Line comments should be JSDoc-style. + +// A single line comment +function quux () {} +// "jsdoc/convert-to-jsdoc-comments": ["error"|"warn", {"contexts":[{"context":"FunctionDeclaration","inlineCommentBlock":true}]}] +// Message: Line comments should be JSDoc-style. + +// A single line comment +function quux () {} +// "jsdoc/convert-to-jsdoc-comments": ["error"|"warn", {"enableFixer":false,"enforceJsdocLineStyle":"single"}] +// Message: Line comments should be JSDoc-style. + +// A single line comment +function quux () {} +// "jsdoc/convert-to-jsdoc-comments": ["error"|"warn", {"lineOrBlockStyle":"line","enforceJsdocLineStyle":"single"}] +// Message: Line comments should be JSDoc-style. + +/* A single line comment */ +function quux () {} +// "jsdoc/convert-to-jsdoc-comments": ["error"|"warn", {"enforceJsdocLineStyle":"single"}] +// Message: Block comments should be JSDoc-style. + +/* A single line comment */ +function quux () {} +// "jsdoc/convert-to-jsdoc-comments": ["error"|"warn", {"lineOrBlockStyle":"block","enforceJsdocLineStyle":"single"}] +// Message: Block comments should be JSDoc-style. + +// A single line comment +function quux () {} +// "jsdoc/convert-to-jsdoc-comments": ["error"|"warn", {"enforceJsdocLineStyle":"multi"}] +// Message: Line comments should be JSDoc-style. + +// A single line comment +function quux () {} +// Message: Line comments should be JSDoc-style. + +/* A single line comment */ +function quux () {} +// "jsdoc/convert-to-jsdoc-comments": ["error"|"warn", {"enforceJsdocLineStyle":"multi"}] +// Message: Block comments should be JSDoc-style. + +// Single line comment +function quux() { + +} +// Settings: {"jsdoc":{"structuredTags":{"see":{"name":false,"required":["name"]}}}} +// Message: Cannot add "name" to `require` with the tag's `name` set to `false` + +/* Entity to represent a user in the system. */ +@Entity('users', getVal()) +export class User { +} +// "jsdoc/convert-to-jsdoc-comments": ["error"|"warn", {"contexts":["ClassDeclaration"]}] +// Message: Block comments should be JSDoc-style. + +/* A single line comment */ function quux () {} +// Settings: {"jsdoc":{"minLines":0,"maxLines":0}} +// "jsdoc/convert-to-jsdoc-comments": ["error"|"warn", {"enforceJsdocLineStyle":"single"}] +// Message: Block comments should be JSDoc-style. +```` + + + + + +## Passing examples + +The following patterns are not considered problems: + +````js +/** A single line comment */ +function quux () {} +// "jsdoc/convert-to-jsdoc-comments": ["error"|"warn", {"enforceJsdocLineStyle":"single"}] + +/** A single line comment */ +function quux () {} +// "jsdoc/convert-to-jsdoc-comments": ["error"|"warn", {"enforceJsdocLineStyle":"multi"}] + +/** A single line comment */ +function quux () {} +// "jsdoc/convert-to-jsdoc-comments": ["error"|"warn", {"lineOrBlockStyle":"line"}] + +/** A single line comment */ +function quux () {} +// "jsdoc/convert-to-jsdoc-comments": ["error"|"warn", {"lineOrBlockStyle":"block"}] + +/* A single line comment */ +function quux () {} +// "jsdoc/convert-to-jsdoc-comments": ["error"|"warn", {"lineOrBlockStyle":"line","enforceJsdocLineStyle":"single"}] + +// A single line comment +function quux () {} +// "jsdoc/convert-to-jsdoc-comments": ["error"|"warn", {"lineOrBlockStyle":"block","enforceJsdocLineStyle":"single"}] + +// @ts-expect-error +function quux () {} + +// @custom-something +function quux () {} +// "jsdoc/convert-to-jsdoc-comments": ["error"|"warn", {"allowedPrefixes":["@custom-"]}] +```` + diff --git a/package.json b/package.json index 57ac8f39f..f6b0ddae6 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "url": "http://gajus.com" }, "dependencies": { - "@es-joy/jsdoccomment": "~0.43.1", + "@es-joy/jsdoccomment": "~0.44.0", "are-docs-informative": "^0.0.2", "comment-parser": "1.4.1", "debug": "^4.3.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4706cb98c..ca6713633 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: dependencies: '@es-joy/jsdoccomment': - specifier: ~0.43.1 - version: 0.43.1 + specifier: ~0.44.0 + version: 0.44.0 are-docs-informative: specifier: ^0.0.2 version: 0.0.2 @@ -957,6 +957,10 @@ packages: resolution: {integrity: sha512-I238eDtOolvCuvtxrnqtlBaw0BwdQuYqK7eA6XIonicMdOOOb75mqdIzkGDUbS04+1Di007rgm9snFRNeVrOog==} engines: {node: '>=16'} + '@es-joy/jsdoccomment@0.44.0': + resolution: {integrity: sha512-2KR2uvAhfrZeVJKBhQ5UU0LK7n9NU4RHs2B0bIjLkieZXsrkCMqWCJhwwSJ67qHoqjy4jj0+3qjl5SM2MnNmEg==} + engines: {node: '>=16'} + '@eslint-community/eslint-utils@4.4.0': resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -6653,6 +6657,15 @@ snapshots: esquery: 1.5.0 jsdoc-type-pratt-parser: 4.0.0 + '@es-joy/jsdoccomment@0.44.0': + dependencies: + '@types/eslint': 8.56.10 + '@types/estree': 1.0.5 + '@typescript-eslint/types': 7.14.1 + comment-parser: 1.4.1 + esquery: 1.5.0 + jsdoc-type-pratt-parser: 4.0.0 + '@eslint-community/eslint-utils@4.4.0(eslint@8.56.0)': dependencies: eslint: 8.56.0 diff --git a/src/bin/generateRule.js b/src/bin/generateRule.js index 7216f0ebe..2fd8e1cd4 100644 --- a/src/bin/generateRule.js +++ b/src/bin/generateRule.js @@ -121,7 +121,7 @@ export default iterateJsdoc(({ await fs.writeFile(ruleTestPath, ruleTestTemplate); } - const ruleReadmeTemplate = `### \`${ruleName}\` + const ruleReadmeTemplate = `# \`${ruleName}\` ||| |---|---| diff --git a/src/index.js b/src/index.js index 3ffec00a7..535ed0b62 100644 --- a/src/index.js +++ b/src/index.js @@ -9,6 +9,7 @@ import checkSyntax from './rules/checkSyntax.js'; import checkTagNames from './rules/checkTagNames.js'; import checkTypes from './rules/checkTypes.js'; import checkValues from './rules/checkValues.js'; +import convertToJsdocComments from './rules/convertToJsdocComments.js'; import emptyTags from './rules/emptyTags.js'; import implementsOnClasses from './rules/implementsOnClasses.js'; import importsAsDependencies from './rules/importsAsDependencies.js'; @@ -81,6 +82,7 @@ const index = { 'check-tag-names': checkTagNames, 'check-types': checkTypes, 'check-values': checkValues, + 'convert-to-jsdoc-comments': convertToJsdocComments, 'empty-tags': emptyTags, 'implements-on-classes': implementsOnClasses, 'imports-as-dependencies': importsAsDependencies, @@ -153,6 +155,7 @@ const createRecommendedRuleset = (warnOrError, flatName) => { 'jsdoc/check-tag-names': warnOrError, 'jsdoc/check-types': warnOrError, 'jsdoc/check-values': warnOrError, + 'jsdoc/convert-to-jsdoc-comments': 'off', 'jsdoc/empty-tags': warnOrError, 'jsdoc/implements-on-classes': warnOrError, 'jsdoc/imports-as-dependencies': 'off', diff --git a/src/rules/convertToJsdocComments.js b/src/rules/convertToJsdocComments.js new file mode 100644 index 000000000..111a59c26 --- /dev/null +++ b/src/rules/convertToJsdocComments.js @@ -0,0 +1,213 @@ +import iterateJsdoc from '../iterateJsdoc.js'; +import { + getSettings, +} from '../iterateJsdoc.js'; +import jsdocUtils from '../jsdocUtils.js'; +import { + getNonJsdocComment, + getDecorator, + getReducedASTNode, +} from '@es-joy/jsdoccomment'; + +/** @type {import('eslint').Rule.RuleModule} */ +export default { + create (context) { + /* c8 ignore next -- Fallback to deprecated method */ + const { + sourceCode = context.getSourceCode(), + } = context; + const settings = getSettings(context); + if (!settings) { + return {}; + } + + const { + contexts = settings.contexts || [], + enableFixer = true, + enforceJsdocLineStyle = 'multi', + lineOrBlockStyle = 'both', + allowedPrefixes = ['@ts-', 'istanbul ', 'c8 ', 'v8 ', 'eslint', 'prettier-'] + } = context.options[0] ?? {}; + + /** + * @type {import('../iterateJsdoc.js').CheckJsdoc} + */ + const checkNonJsdoc = (_info, _handler, node) => { + const comment = getNonJsdocComment(sourceCode, node, settings); + + if ( + !comment || + /** @type {string[]} */ + (allowedPrefixes).some((prefix) => { + return comment.value.trimStart().startsWith(prefix); + }) + ) { + return; + } + + const fix = /** @type {import('eslint').Rule.ReportFixer} */ (fixer) => { + // Default to one line break if the `minLines`/`maxLines` settings allow + const lines = settings.minLines === 0 && settings.maxLines >= 1 ? 1 : settings.minLines; + /** @type {import('eslint').Rule.Node|import('@typescript-eslint/types').TSESTree.Decorator} */ + let baseNode = getReducedASTNode(node, sourceCode); + + const decorator = getDecorator(baseNode); + if (decorator) { + baseNode = decorator; + } + + const indent = jsdocUtils.getIndent({ + text: sourceCode.getText( + /** @type {import('eslint').Rule.Node} */ (baseNode), + /** @type {import('eslint').AST.SourceLocation} */ + ( + /** @type {import('eslint').Rule.Node} */ (baseNode).loc + ).start.column, + ), + }); + + const { + inlineCommentBlock, + } = + /** + * @type {{ + * context: string, + * inlineCommentBlock: boolean, + * minLineCount: import('../iterateJsdoc.js').Integer + * }[]} + */ (contexts).find((contxt) => { + if (typeof contxt === 'string') { + return false; + } + + const { + context: ctxt, + } = contxt; + return ctxt === node.type; + }) || {}; + const insertion = ( + inlineCommentBlock || enforceJsdocLineStyle === 'single' + ? `/** ${comment.value.trim()} ` + : `/**\n${indent}*${comment.value.trimEnd()}\n${indent}` + ) + + `*/${'\n'.repeat((lines || 1) - 1)}`; + + return fixer.replaceText( + /** @type {import('eslint').AST.Token} */ + (comment), + insertion, + ); + }; + + /** + * @param {string} messageId + */ + const report = (messageId) => { + const loc = { + end: { + column: 0, + /* c8 ignore next 2 -- Guard */ + // @ts-expect-error Ok + line: (comment.loc?.start?.line ?? 1), + }, + start: { + column: 0, + /* c8 ignore next 2 -- Guard */ + // @ts-expect-error Ok + line: (comment.loc?.start?.line ?? 1) + }, + }; + + context.report({ + fix: enableFixer ? fix : null, + loc, + messageId, + node, + }); + }; + + if (comment.type === 'Block') { + if (lineOrBlockStyle === 'line') { + return; + } + report('blockCommentsJsdocStyle'); + return; + } + + if (comment.type === 'Line') { + if (lineOrBlockStyle === 'block') { + return; + } + report('lineCommentsJsdocStyle'); + } + }; + + return { + ...jsdocUtils.getContextObject( + jsdocUtils.enforcedContexts(context, true, settings), + checkNonJsdoc, + ) + }; + }, + meta: { + fixable: 'code', + + messages: { + blockCommentsJsdocStyle: 'Block comments should be JSDoc-style.', + lineCommentsJsdocStyle: 'Line comments should be JSDoc-style.', + }, + + docs: { + description: 'Converts non-JSDoc comments preceding nodes into JSDoc ones', + url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/convert-to-jsdoc-comments.md#repos-sticky-header', + }, + schema: [ + { + additionalProperties: false, + properties: { + allowedPrefixes: { + type: 'array', + items: { + type: 'string' + } + }, + enableFixer: { + type: 'boolean' + }, + enforceJsdocLineStyle: { + type: 'string', + enum: ['multi', 'single'] + }, + lineOrBlockStyle: { + type: 'string', + enum: ['block', 'line', 'both'] + }, + contexts: { + items: { + anyOf: [ + { + type: 'string', + }, + { + additionalProperties: false, + properties: { + context: { + type: 'string', + }, + inlineCommentBlock: { + type: 'boolean', + }, + }, + type: 'object', + }, + ], + }, + type: 'array', + }, + }, + type: 'object', + }, + ], + type: 'suggestion', + }, +}; diff --git a/test/rules/assertions/convertToJsdocComments.js b/test/rules/assertions/convertToJsdocComments.js new file mode 100644 index 000000000..b92663817 --- /dev/null +++ b/test/rules/assertions/convertToJsdocComments.js @@ -0,0 +1,369 @@ +import {parser as typescriptEslintParser} from 'typescript-eslint'; + +export default { + invalid: [ + { + code: ` + // A single line comment + function quux () {} + `, + errors: [ + { + line: 2, + message: 'Line comments should be JSDoc-style.', + }, + ], + options: [ + { + enforceJsdocLineStyle: 'single' + } + ], + output: ` + /** A single line comment */ + function quux () {} + ` + }, + { + code: ` + // A single line comment + function quux () {} + `, + errors: [ + { + line: 2, + message: 'Line comments should be JSDoc-style.', + }, + ], + options: [ + { + contexts: [ + { + context: 'FunctionDeclaration', + inlineCommentBlock: true + } + ] + } + ], + output: ` + /** A single line comment */ + function quux () {} + ` + }, + { + code: ` + // A single line comment + function quux () {} + `, + errors: [ + { + line: 2, + message: 'Line comments should be JSDoc-style.', + }, + ], + options: [ + { + enableFixer: false, + enforceJsdocLineStyle: 'single' + } + ], + }, + { + code: ` + // A single line comment + function quux () {} + `, + errors: [ + { + line: 2, + message: 'Line comments should be JSDoc-style.', + }, + ], + options: [ + { + lineOrBlockStyle: 'line', + enforceJsdocLineStyle: 'single' + } + ], + output: ` + /** A single line comment */ + function quux () {} + ` + }, + { + code: ` + /* A single line comment */ + function quux () {} + `, + errors: [ + { + line: 2, + message: 'Block comments should be JSDoc-style.', + }, + ], + options: [ + { + enforceJsdocLineStyle: 'single' + } + ], + output: ` + /** A single line comment */ + function quux () {} + `, + }, + { + code: ` + /* A single line comment */ + function quux () {} + `, + errors: [ + { + line: 2, + message: 'Block comments should be JSDoc-style.', + }, + ], + options: [ + { + lineOrBlockStyle: 'block', + enforceJsdocLineStyle: 'single' + } + ], + output: ` + /** A single line comment */ + function quux () {} + `, + }, + { + code: ` + // A single line comment + function quux () {} + `, + errors: [ + { + line: 2, + message: 'Line comments should be JSDoc-style.', + }, + ], + options: [ + { + enforceJsdocLineStyle: 'multi' + } + ], + output: ` + /** + * A single line comment + */ + function quux () {} + `, + }, + { + code: ` + // A single line comment + function quux () {} + `, + errors: [ + { + line: 2, + message: 'Line comments should be JSDoc-style.', + }, + ], + output: ` + /** + * A single line comment + */ + function quux () {} + `, + }, + { + code: ` + /* A single line comment */ + function quux () {} + `, + errors: [ + { + line: 2, + message: 'Block comments should be JSDoc-style.', + }, + ], + options: [ + { + enforceJsdocLineStyle: 'multi' + } + ], + output: ` + /** + * A single line comment + */ + function quux () {} + `, + }, + { + code: ` + // Single line comment + function quux() { + + } + `, + errors: [ + { + line: 1, + message: 'Cannot add "name" to `require` with the tag\'s `name` set to `false`', + }, + ], + settings: { + jsdoc: { + structuredTags: { + see: { + name: false, + required: [ + 'name', + ], + }, + }, + }, + }, + }, + { + code: ` + /* Entity to represent a user in the system. */ + @Entity('users', getVal()) + export class User { + } + `, + errors: [ + { + line: 2, + message: 'Block comments should be JSDoc-style.', + }, + ], + options: [ + { + contexts: ['ClassDeclaration'] + } + ], + output: ` + /** + * Entity to represent a user in the system. + */ + @Entity('users', getVal()) + export class User { + } + `, + languageOptions: { + parser: typescriptEslintParser, + sourceType: 'module', + }, + }, + { + code: ` + /* A single line comment */ function quux () {} + `, + errors: [ + { + line: 2, + message: 'Block comments should be JSDoc-style.', + }, + ], + options: [ + { + enforceJsdocLineStyle: 'single' + } + ], + settings: { + jsdoc: { + minLines: 0, + maxLines: 0, + }, + }, + output: ` + /** A single line comment */ function quux () {} + ` + }, + ], + valid: [ + { + code: ` + /** A single line comment */ + function quux () {} + `, + options: [ + { + enforceJsdocLineStyle: 'single' + } + ], + }, + { + code: ` + /** A single line comment */ + function quux () {} + `, + options: [ + { + enforceJsdocLineStyle: 'multi' + } + ], + }, + { + code: ` + /** A single line comment */ + function quux () {} + `, + options: [ + { + lineOrBlockStyle: 'line', + } + ], + }, + { + code: ` + /** A single line comment */ + function quux () {} + `, + options: [ + { + lineOrBlockStyle: 'block', + } + ], + }, + { + code: ` + /* A single line comment */ + function quux () {} + `, + options: [ + { + lineOrBlockStyle: 'line', + enforceJsdocLineStyle: 'single' + } + ], + }, + { + code: ` + // A single line comment + function quux () {} + `, + options: [ + { + lineOrBlockStyle: 'block', + enforceJsdocLineStyle: 'single' + } + ], + }, + { + code: ` + // @ts-expect-error + function quux () {} + `, + }, + { + code: ` + // @custom-something + function quux () {} + `, + options: [ + { + allowedPrefixes: ['@custom-'] + } + ], + }, + ], +}; diff --git a/test/rules/ruleNames.json b/test/rules/ruleNames.json index 24038af51..15418c0d8 100644 --- a/test/rules/ruleNames.json +++ b/test/rules/ruleNames.json @@ -10,6 +10,7 @@ "check-tag-names", "check-types", "check-values", + "convert-to-jsdoc-comments", "empty-tags", "implements-on-classes", "imports-as-dependencies",