diff --git a/.README/rules/multiline-blocks.md b/.README/rules/multiline-blocks.md index 6cbdb182a..9d71e323d 100644 --- a/.README/rules/multiline-blocks.md +++ b/.README/rules/multiline-blocks.md @@ -38,6 +38,13 @@ line will be reported. (Text preceding a newline is not reported.) If this is `true`, any single line blocks will be reported, except those which are whitelisted in `singleLineTags`. +### `requireSingleLineUnderCount` (defaults to `null`) + +If this number is set, it indicates a minimum line width for a single line of +JSDoc content spread over a multi-line comment block. If a line is under the +minimum length, it will be reported so as to enforce single line JSDoc blocks +for such cases. + ### `singleLineTags` (defaults to `['lends', 'type']`) An array of tags which can nevertheless be allowed as single line blocks when @@ -96,7 +103,7 @@ cannot be reliably added after the tag either). |Tags|Any (though `singleLineTags` and `multilineTags` control the application)| |Recommended|true| |Settings|| -|Options|`allowMultipleTags`, `minimumLengthForMultiline`, `multilineTags`, `noFinalLineText`, `noMultilineBlocks`, `noSingleLineBlocks`, `noZeroLineText`, `singleLineTags`| +|Options|`allowMultipleTags`, `minimumLengthForMultiline`, `multilineTags`, `noFinalLineText`, `noMultilineBlocks`, `noSingleLineBlocks`, `noZeroLineText`, `requireSingleLineUnderCount`, `singleLineTags`| ## Failing examples diff --git a/docs/rules/multiline-blocks.md b/docs/rules/multiline-blocks.md index 7913017ef..9c3a23072 100644 --- a/docs/rules/multiline-blocks.md +++ b/docs/rules/multiline-blocks.md @@ -50,6 +50,15 @@ line will be reported. (Text preceding a newline is not reported.) If this is `true`, any single line blocks will be reported, except those which are whitelisted in `singleLineTags`. + + +### requireSingleLineUnderCount (defaults to null) + +If this number is set, it indicates a minimum line width for a single line of +JSDoc content spread over a multi-line comment block. If a line is under the +minimum length, it will be reported so as to enforce single line JSDoc blocks +for such cases. + ### singleLineTags (defaults to ['lends', 'type']) @@ -120,7 +129,7 @@ cannot be reliably added after the tag either). |Tags|Any (though `singleLineTags` and `multilineTags` control the application)| |Recommended|true| |Settings|| -|Options|`allowMultipleTags`, `minimumLengthForMultiline`, `multilineTags`, `noFinalLineText`, `noMultilineBlocks`, `noSingleLineBlocks`, `noZeroLineText`, `singleLineTags`| +|Options|`allowMultipleTags`, `minimumLengthForMultiline`, `multilineTags`, `noFinalLineText`, `noMultilineBlocks`, `noSingleLineBlocks`, `noZeroLineText`, `requireSingleLineUnderCount`, `singleLineTags`| @@ -283,6 +292,38 @@ The following patterns are considered problems: * Description */ // "jsdoc/multiline-blocks": ["error"|"warn", {"noFinalLineText":true}] // Message: Should have no text on the final line (before the `*/`). + +/** + * Description too short + */ +// "jsdoc/multiline-blocks": ["error"|"warn", {"requireSingleLineUnderCount":80}] +// Message: Description is too short to be multi-line. + +/** Description too short + */ +// "jsdoc/multiline-blocks": ["error"|"warn", {"requireSingleLineUnderCount":80}] +// Message: Description is too short to be multi-line. + +/** + * Description too short */ +// "jsdoc/multiline-blocks": ["error"|"warn", {"requireSingleLineUnderCount":80}] +// Message: Description is too short to be multi-line. + +/** + * @someTag {someType} Description too short + */ +// "jsdoc/multiline-blocks": ["error"|"warn", {"requireSingleLineUnderCount":80}] +// Message: Description is too short to be multi-line. + +/** @someTag {someType} Description too short + */ +// "jsdoc/multiline-blocks": ["error"|"warn", {"requireSingleLineUnderCount":80}] +// Message: Description is too short to be multi-line. + +/** + * @someTag {someType} Description too short */ +// "jsdoc/multiline-blocks": ["error"|"warn", {"requireSingleLineUnderCount":80}] +// Message: Description is too short to be multi-line. ```` @@ -394,5 +435,39 @@ The following patterns are not considered problems: /** @someTag with Description */ // "jsdoc/multiline-blocks": ["error"|"warn", {"noFinalLineText":true}] + +/** + * This description here is very much long enough, I'd say, wouldn't you? + */ +// "jsdoc/multiline-blocks": ["error"|"warn", {"requireSingleLineUnderCount":80}] + +/** + * This description here is + * on multiple lines. + */ +// "jsdoc/multiline-blocks": ["error"|"warn", {"requireSingleLineUnderCount":80}] + +/** This description here is on a single line, so it doesn't matter if it goes over. */ +// "jsdoc/multiline-blocks": ["error"|"warn", {"requireSingleLineUnderCount":80}] + +/** + * @someTag {someType} This description here is very much long enough, I'd say, wouldn't you? + */ +// "jsdoc/multiline-blocks": ["error"|"warn", {"requireSingleLineUnderCount":80}] + +/** + * @someTag {someType} This description here is + * on multiple lines. + */ +// "jsdoc/multiline-blocks": ["error"|"warn", {"requireSingleLineUnderCount":80}] + +/** @someTag {someTag} This description here is on a single line, so it doesn't matter if it goes over. */ +// "jsdoc/multiline-blocks": ["error"|"warn", {"requireSingleLineUnderCount":80}] + +/** + * Description short but has... + * @someTag + */ +// "jsdoc/multiline-blocks": ["error"|"warn", {"requireSingleLineUnderCount":80}] ```` diff --git a/src/rules/multilineBlocks.js b/src/rules/multilineBlocks.js index 29079110d..223dece31 100644 --- a/src/rules/multilineBlocks.js +++ b/src/rules/multilineBlocks.js @@ -1,5 +1,151 @@ import iterateJsdoc from '../iterateJsdoc.js'; +/** + * @param {import('@es-joy/jsdoccomment').JsdocBlockWithInline} jsdoc + * @param {import('../iterateJsdoc.js').Utils} utils + * @param {number} requireSingleLineUnderCount + */ +const checkForShortTags = (jsdoc, utils, requireSingleLineUnderCount) => { + if (!requireSingleLineUnderCount || !jsdoc.tags.length) { + return false; + } + + let lastLineWithTag = 0; + let exceedsLength = false; + let hasDesc = false; + const tagLines = jsdoc.source.reduce((acc, { + tokens: { + delimiter, + description: desc, + name, + postDelimiter, + postName, + postTag, + postType, + start, + tag, + type, + }, + }, idx) => { + if (tag.length) { + lastLineWithTag = idx; + if ( + start.length + delimiter.length + postDelimiter.length + + type.length + postType.length + name.length + postName.length + + tag.length + postTag.length + desc.length < + requireSingleLineUnderCount + ) { + exceedsLength = true; + } + + return acc + 1; + } else if (desc.length) { + hasDesc = true; + return acc; + } + + return acc; + }, 0); + // Could be tagLines > 1 + if (!hasDesc && exceedsLength && tagLines === 1) { + const fixer = () => { + const tokens = jsdoc.source[lastLineWithTag].tokens; + jsdoc.source = [ + { + number: 0, + source: '', + tokens: utils.seedTokens({ + delimiter: '/**', + description: tokens.description.trimEnd() + ' ', + end: '*/', + name: tokens.name, + postDelimiter: ' ', + postName: tokens.postName, + postTag: tokens.postTag, + postType: tokens.postType, + start: jsdoc.source[0].tokens.start, + tag: tokens.tag, + type: tokens.type, + }), + }, + ]; + }; + + utils.reportJSDoc( + 'Description is too short to be multi-line.', + null, + fixer, + ); + return true; + } + + return false; +}; + +/** + * @param {import('@es-joy/jsdoccomment').JsdocBlockWithInline} jsdoc + * @param {import('../iterateJsdoc.js').Utils} utils + * @param {number} requireSingleLineUnderCount + */ +const checkForShortDescriptions = (jsdoc, utils, requireSingleLineUnderCount) => { + if (!requireSingleLineUnderCount || jsdoc.tags.length) { + return false; + } + + let lastLineWithDesc = 0; + let exceedsLength = false; + const descLines = jsdoc.source.reduce((acc, { + tokens: { + delimiter, + description: desc, + postDelimiter, + start, + }, + }, idx) => { + if (desc.length) { + lastLineWithDesc = idx; + if ( + start.length + delimiter.length + postDelimiter.length + desc.length < + requireSingleLineUnderCount + ) { + exceedsLength = true; + } + + return acc + 1; + } + + return acc; + }, 0); + // Could be descLines > 1 + if (exceedsLength && descLines === 1) { + const fixer = () => { + const desc = jsdoc.source[lastLineWithDesc].tokens.description; + jsdoc.source = [ + { + number: 0, + source: '', + tokens: utils.seedTokens({ + delimiter: '/**', + description: desc.trimEnd() + ' ', + end: '*/', + postDelimiter: ' ', + start: jsdoc.source[0].tokens.start, + }), + }, + ]; + }; + + utils.reportJSDoc( + 'Description is too short to be multi-line.', + null, + fixer, + ); + return true; + } + + return false; +}; + export default iterateJsdoc(({ context, jsdoc, @@ -15,6 +161,7 @@ export default iterateJsdoc(({ noMultilineBlocks = false, noSingleLineBlocks = false, noZeroLineText = true, + requireSingleLineUnderCount = null, singleLineTags = [ 'lends', 'type', ], @@ -62,6 +209,16 @@ export default iterateJsdoc(({ return; } + if (checkForShortDescriptions(jsdoc, utils, requireSingleLineUnderCount) + ) { + return; + } + + if (checkForShortTags(jsdoc, utils, requireSingleLineUnderCount) + ) { + return; + } + const lineChecks = () => { if ( noZeroLineText && @@ -318,6 +475,9 @@ export default iterateJsdoc(({ noZeroLineText: { type: 'boolean', }, + requireSingleLineUnderCount: { + type: 'number', + }, singleLineTags: { items: { type: 'string', diff --git a/test/rules/assertions/multilineBlocks.js b/test/rules/assertions/multilineBlocks.js index eae189420..cd7288c4e 100644 --- a/test/rules/assertions/multilineBlocks.js +++ b/test/rules/assertions/multilineBlocks.js @@ -619,6 +619,128 @@ export default /** @type {import('../index.js').TestCases} */ ({ */ `, }, + { + code: ` + /** + * Description too short + */ + `, + errors: [ + { + line: 2, + message: 'Description is too short to be multi-line.', + }, + ], + options: [ + { + requireSingleLineUnderCount: 80, + }, + ], + output: ` + /** Description too short */ + `, + }, + { + code: ` + /** Description too short + */ + `, + errors: [ + { + line: 2, + message: 'Description is too short to be multi-line.', + }, + ], + options: [ + { + requireSingleLineUnderCount: 80, + }, + ], + output: ` + /** Description too short */ + `, + }, + { + code: ` + /** + * Description too short */ + `, + errors: [ + { + line: 2, + message: 'Description is too short to be multi-line.', + }, + ], + options: [ + { + requireSingleLineUnderCount: 80, + }, + ], + output: ` + /** Description too short */ + `, + }, + { + code: ` + /** + * @someTag {someType} Description too short + */ + `, + errors: [ + { + line: 2, + message: 'Description is too short to be multi-line.', + }, + ], + options: [ + { + requireSingleLineUnderCount: 80, + }, + ], + output: ` + /** @someTag {someType} Description too short */ + `, + }, + { + code: ` + /** @someTag {someType} Description too short + */ + `, + errors: [ + { + line: 2, + message: 'Description is too short to be multi-line.', + }, + ], + options: [ + { + requireSingleLineUnderCount: 80, + }, + ], + output: ` + /** @someTag {someType} Description too short */ + `, + }, + { + code: ` + /** + * @someTag {someType} Description too short */ + `, + errors: [ + { + line: 2, + message: 'Description is too short to be multi-line.', + }, + ], + options: [ + { + requireSingleLineUnderCount: 80, + }, + ], + output: ` + /** @someTag {someType} Description too short */ + `, + }, ], valid: [ { @@ -895,5 +1017,88 @@ export default /** @type {import('../index.js').TestCases} */ ({ }, ], }, + { + code: ` + /** + * This description here is very much long enough, I'd say, wouldn't you? + */ +`, + options: [ + { + requireSingleLineUnderCount: 80, + }, + ], + }, + { + code: ` + /** + * This description here is + * on multiple lines. + */ +`, + options: [ + { + requireSingleLineUnderCount: 80, + }, + ], + }, + { + code: ` + /** This description here is on a single line, so it doesn't matter if it goes over. */ +`, + options: [ + { + requireSingleLineUnderCount: 80, + }, + ], + }, + { + code: ` + /** + * @someTag {someType} This description here is very much long enough, I'd say, wouldn't you? + */ +`, + options: [ + { + requireSingleLineUnderCount: 80, + }, + ], + }, + { + code: ` + /** + * @someTag {someType} This description here is + * on multiple lines. + */ +`, + options: [ + { + requireSingleLineUnderCount: 80, + }, + ], + }, + { + code: ` + /** @someTag {someTag} This description here is on a single line, so it doesn't matter if it goes over. */ +`, + options: [ + { + requireSingleLineUnderCount: 80, + }, + ], + }, + { + code: ` + /** + * Description short but has... + * @someTag + */ + `, + options: [ + { + requireSingleLineUnderCount: 80, + }, + ], + }, ], });