From 39ff85ab7d2a526211b2501c9ed9dfdc6f9ccb1f Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Wed, 17 Feb 2021 08:50:45 +0800 Subject: [PATCH] fix(: preserve newlines in tag description comparisons; fixes #692 --- README.md | 31 +++++++++++ src/iterateJsdoc.js | 28 ++++++++++ src/rules/checkExamples.js | 4 +- src/rules/checkValues.js | 14 ++--- src/rules/matchDescription.js | 6 +-- src/rules/requireDescription.js | 2 +- .../requireDescriptionCompleteSentence.js | 6 +-- src/rules/requireExample.js | 2 +- .../requireHyphenBeforeParamDescription.js | 11 ++-- src/rules/validTypes.js | 8 +-- test/rules/assertions/matchDescription.js | 52 +++++++++++++++++++ test/rules/assertions/requireDescription.js | 13 +++++ 12 files changed, 151 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 6a39ebd65..8d12b0b67 100644 --- a/README.md +++ b/README.md @@ -6574,6 +6574,31 @@ function quux () { } // "jsdoc/match-description": ["error"|"warn", {"tags":{"description":true}}] +/** + * @description Foo + * bar. + * @param + */ +function quux () { + +} +// "jsdoc/match-description": ["error"|"warn", {"tags":{"description":true}}] + +/** @description Foo bar. */ +function quux () { + +} +// "jsdoc/match-description": ["error"|"warn", {"tags":{"description":true}}] + +/** + * @description Foo + * bar. + */ +function quux () { + +} +// "jsdoc/match-description": ["error"|"warn", {"tags":{"description":true}}] + /** * Foo. {@see Math.sin}. */ @@ -9208,6 +9233,12 @@ function quux () { function quux () { } + +/** @description something */ +function quux () { + +} +// "jsdoc/require-description": ["error"|"warn", {"descriptionStyle":"tag"}] ```` diff --git a/src/iterateJsdoc.js b/src/iterateJsdoc.js index 36147c07e..a841ae2bc 100644 --- a/src/iterateJsdoc.js +++ b/src/iterateJsdoc.js @@ -216,6 +216,34 @@ const getUtils = ( return jsdocUtils.getRegexFromString(str, requiredFlags); }; + utils.getTagDescription = (tg) => { + const descriptions = []; + tg.source.some(({ + tokens: {end, postDelimiter, tag, postTag, name, type, description}, + }) => { + const desc = ( + tag && postTag || + !tag && !name && !type && postDelimiter || '' + + // Remove space + ).slice(1) + + (description || ''); + + if (end) { + if (desc) { + descriptions.push(desc); + } + + return true; + } + descriptions.push(desc); + + return false; + }); + + return descriptions.join('\n'); + }; + utils.getDescription = () => { const descriptions = []; let lastDescriptionLine; diff --git a/src/rules/checkExamples.js b/src/rules/checkExamples.js index de766e058..826fafe05 100644 --- a/src/rules/checkExamples.js +++ b/src/rules/checkExamples.js @@ -263,7 +263,7 @@ export default iterateJsdoc(({ return; } checkSource({ - source: `(${tag.description})`, + source: `(${utils.getTagDescription(tag)})`, targetTagName, ...filenameInfo, }); @@ -304,7 +304,7 @@ export default iterateJsdoc(({ const matchingFilenameInfo = getFilenameInfo(matchingFileName); utils.forEachPreferredTag('example', (tag, targetTagName) => { - let source = tag.source[0].tokens.postTag.slice(1) + tag.description; + let source = utils.getTagDescription(tag); const match = source.match(hasCaptionRegex); if (captionRequired && (!match || !match[1].trim())) { diff --git a/src/rules/checkValues.js b/src/rules/checkValues.js index 0ab11fadf..c419077d3 100644 --- a/src/rules/checkValues.js +++ b/src/rules/checkValues.js @@ -15,7 +15,7 @@ export default iterateJsdoc(({ } = options; utils.forEachPreferredTag('version', (jsdocParameter, targetTagName) => { - const version = jsdocParameter.description.trim(); + const version = utils.getTagDescription(jsdocParameter).trim(); if (!version) { report( `Missing JSDoc @${targetTagName}.`, @@ -24,14 +24,14 @@ export default iterateJsdoc(({ ); } else if (!semver.valid(version)) { report( - `Invalid JSDoc @${targetTagName}: "${jsdocParameter.description}".`, + `Invalid JSDoc @${targetTagName}: "${utils.getTagDescription(jsdocParameter)}".`, null, jsdocParameter, ); } }); utils.forEachPreferredTag('since', (jsdocParameter, targetTagName) => { - const version = jsdocParameter.description.trim(); + const version = utils.getTagDescription(jsdocParameter).trim(); if (!version) { report( `Missing JSDoc @${targetTagName}.`, @@ -40,7 +40,7 @@ export default iterateJsdoc(({ ); } else if (!semver.valid(version)) { report( - `Invalid JSDoc @${targetTagName}: "${jsdocParameter.description}".`, + `Invalid JSDoc @${targetTagName}: "${utils.getTagDescription(jsdocParameter)}".`, null, jsdocParameter, ); @@ -48,7 +48,7 @@ export default iterateJsdoc(({ }); utils.forEachPreferredTag('license', (jsdocParameter, targetTagName) => { const licenseRegex = utils.getRegexFromString(licensePattern, 'g'); - const match = jsdocParameter.description.match(licenseRegex); + const match = utils.getTagDescription(jsdocParameter).match(licenseRegex); const license = match && match[1] || match[0]; if (!license.trim()) { report( @@ -78,7 +78,7 @@ export default iterateJsdoc(({ }); utils.forEachPreferredTag('author', (jsdocParameter, targetTagName) => { - const author = jsdocParameter.description.trim(); + const author = utils.getTagDescription(jsdocParameter).trim(); if (!author) { report( `Missing JSDoc @${targetTagName}.`, @@ -87,7 +87,7 @@ export default iterateJsdoc(({ ); } else if (allowedAuthors && !allowedAuthors.includes(author)) { report( - `Invalid JSDoc @${targetTagName}: "${jsdocParameter.description}"; expected one of ${allowedAuthors.join(', ')}.`, + `Invalid JSDoc @${targetTagName}: "${utils.getTagDescription(jsdocParameter)}"; expected one of ${allowedAuthors.join(', ')}.`, null, jsdocParameter, ); diff --git a/src/rules/matchDescription.js b/src/rules/matchDescription.js index 493d43ccc..e897db8f9 100644 --- a/src/rules/matchDescription.js +++ b/src/rules/matchDescription.js @@ -58,7 +58,7 @@ export default iterateJsdoc(({ }; utils.forEachPreferredTag('description', (matchingJsdocTag, targetTagName) => { - const description = (matchingJsdocTag.name + ' ' + matchingJsdocTag.description).trim(); + const description = (matchingJsdocTag.name + ' ' + utils.getTagDescription(matchingJsdocTag)).trim(); if (hasOptionTag(targetTagName)) { validateDescription(description, matchingJsdocTag); } @@ -70,13 +70,13 @@ export default iterateJsdoc(({ const {tagsWithNames, tagsWithoutNames} = utils.getTagsByType(whitelistedTags); tagsWithNames.some((tag) => { - const description = _.trimStart(tag.description, '- '); + const description = _.trimStart(utils.getTagDescription(tag), '- ').trim(); return validateDescription(description, tag); }); tagsWithoutNames.some((tag) => { - const description = (tag.name + ' ' + tag.description).trim(); + const description = (tag.name + ' ' + utils.getTagDescription(tag)).trim(); return validateDescription(description, tag); }); diff --git a/src/rules/requireDescription.js b/src/rules/requireDescription.js index 214f52770..3dc20b6d0 100644 --- a/src/rules/requireDescription.js +++ b/src/rules/requireDescription.js @@ -70,7 +70,7 @@ export default iterateJsdoc(({ } functionExamples.forEach((example) => { - if (!checkDescription(`${example.name} ${example.description}`)) { + if (!checkDescription(`${example.name} ${utils.getTagDescription(example)}`)) { report(`Missing JSDoc @${targetTagName} description.`); } }); diff --git a/src/rules/requireDescriptionCompleteSentence.js b/src/rules/requireDescriptionCompleteSentence.js index ea0b3efc4..bf59c12c4 100644 --- a/src/rules/requireDescriptionCompleteSentence.js +++ b/src/rules/requireDescriptionCompleteSentence.js @@ -175,7 +175,7 @@ export default iterateJsdoc(({ } utils.forEachPreferredTag('description', (matchingJsdocTag) => { - const desc = `${matchingJsdocTag.name} ${matchingJsdocTag.description}`.trim(); + const desc = `${matchingJsdocTag.name} ${utils.getTagDescription(matchingJsdocTag)}`.trim(); validateDescription(desc, report, jsdocNode, abbreviationsRegex, sourceCode, matchingJsdocTag, newlineBeforeCapsAssumesBadSentenceEnd); }, true); @@ -190,13 +190,13 @@ export default iterateJsdoc(({ }); tagsWithNames.some((tag) => { - const desc = _.trimStart(tag.description, '- ').trimEnd(); + const desc = _.trimStart(utils.getTagDescription(tag), '- ').trimEnd(); return validateDescription(desc, report, jsdocNode, abbreviationsRegex, sourceCode, tag, newlineBeforeCapsAssumesBadSentenceEnd); }); tagsWithoutNames.some((tag) => { - const desc = `${tag.name} ${tag.description}`.trim(); + const desc = `${tag.name} ${utils.getTagDescription(tag)}`.trim(); return validateDescription(desc, report, jsdocNode, abbreviationsRegex, sourceCode, tag, newlineBeforeCapsAssumesBadSentenceEnd); }); diff --git a/src/rules/requireExample.js b/src/rules/requireExample.js index 5fea5b72a..c328d6da8 100644 --- a/src/rules/requireExample.js +++ b/src/rules/requireExample.js @@ -36,7 +36,7 @@ export default iterateJsdoc(({ } functionExamples.forEach((example) => { - const exampleContent = _.compact(`${example.name} ${example.description}`.trim().split('\n')); + const exampleContent = _.compact(`${example.name} ${utils.getTagDescription(example)}`.trim().split('\n')); if (!exampleContent.length) { report(`Missing JSDoc @${targetTagName} description.`); diff --git a/src/rules/requireHyphenBeforeParamDescription.js b/src/rules/requireHyphenBeforeParamDescription.js index eff16286f..79d964024 100644 --- a/src/rules/requireHyphenBeforeParamDescription.js +++ b/src/rules/requireHyphenBeforeParamDescription.js @@ -12,11 +12,12 @@ export default iterateJsdoc(({ const checkHyphens = (jsdocTag, targetTagName, circumstance = mainCircumstance) => { const always = !circumstance || circumstance === 'always'; - if (!jsdocTag.description.trim()) { + const desc = utils.getTagDescription(jsdocTag); + if (!desc.trim()) { return; } - const startsWithHyphen = (/^\s*-/u).test(jsdocTag.description); + const startsWithHyphen = (/^\s*-/u).test(desc); if (always) { if (!startsWithHyphen) { report(`There must be a hyphen before @${targetTagName} description.`, (fixer) => { @@ -24,7 +25,7 @@ export default iterateJsdoc(({ const sourceLines = sourceCode.getText(jsdocNode).split('\n'); // Get start index of description, accounting for multi-line descriptions - const description = jsdocTag.description.split('\n')[0]; + const description = desc.split('\n')[0]; const descriptionIndex = sourceLines[lineIndex].lastIndexOf(description); const replacementLine = sourceLines[lineIndex] @@ -37,11 +38,11 @@ export default iterateJsdoc(({ } } else if (startsWithHyphen) { report(`There must be no hyphen before @${targetTagName} description.`, (fixer) => { - const [unwantedPart] = /^\s*-\s*/u.exec(jsdocTag.description); + const [unwantedPart] = /^\s*-\s*/u.exec(desc); const replacement = sourceCode .getText(jsdocNode) - .replace(jsdocTag.description, jsdocTag.description.slice(unwantedPart.length)); + .replace(desc, desc.slice(unwantedPart.length)); return fixer.replaceText(jsdocNode, replacement); }, jsdocTag); diff --git a/src/rules/validTypes.js b/src/rules/validTypes.js index dc7c9e2d4..b4daf942e 100644 --- a/src/rules/validTypes.js +++ b/src/rules/validTypes.js @@ -83,10 +83,10 @@ export default iterateJsdoc(({ }; if (tag.tag === 'borrows') { - const thisNamepath = tag.description.replace(asExpression, '').trim(); + const thisNamepath = utils.getTagDescription(tag).replace(asExpression, '').trim(); - if (!asExpression.test(tag.description) || !thisNamepath) { - report(`@borrows must have an "as" expression. Found "${tag.description}"`, null, tag); + if (!asExpression.test(utils.getTagDescription(tag)) || !thisNamepath) { + report(`@borrows must have an "as" expression. Found "${utils.getTagDescription(tag)}"`, null, tag); return; } @@ -134,7 +134,7 @@ export default iterateJsdoc(({ 'param', 'arg', 'argument', 'property', 'prop', ].includes(tag.tag) && - (tag.tag !== 'see' || !tag.description.includes('{@link')) + (tag.tag !== 'see' || !utils.getTagDescription(tag).includes('{@link')) ) { const modeInfo = tagMustHaveNamePosition === true ? '' : ` in "${mode}" mode`; report(`Tag @${tag.tag} must have a name/namepath${modeInfo}.`, null, tag); diff --git a/test/rules/assertions/matchDescription.js b/test/rules/assertions/matchDescription.js index 2db178e5b..85ac34012 100644 --- a/test/rules/assertions/matchDescription.js +++ b/test/rules/assertions/matchDescription.js @@ -973,6 +973,58 @@ export default { }, ], }, + { + code: ` + /** + * @description Foo + * bar. + * @param + */ + function quux () { + + } + `, + options: [ + { + tags: { + description: true, + }, + }, + ], + }, + { + code: ` + /** @description Foo bar. */ + function quux () { + + } + `, + options: [ + { + tags: { + description: true, + }, + }, + ], + }, + { + code: ` + /** + * @description Foo + * bar. + */ + function quux () { + + } + `, + options: [ + { + tags: { + description: true, + }, + }, + ], + }, { code: ` /** diff --git a/test/rules/assertions/requireDescription.js b/test/rules/assertions/requireDescription.js index f3f030f80..4ce6d0b63 100644 --- a/test/rules/assertions/requireDescription.js +++ b/test/rules/assertions/requireDescription.js @@ -851,5 +851,18 @@ export default { } `, }, + { + code: ` + /** @description something */ + function quux () { + + } + `, + options: [ + { + descriptionStyle: 'tag', + }, + ], + }, ], };