Skip to content

Commit

Permalink
fix(: preserve newlines in tag description comparisons; fixes gajus#692
Browse files Browse the repository at this point in the history
  • Loading branch information
brettz9 committed Apr 28, 2021
1 parent fb6bc0b commit 39ff85a
Show file tree
Hide file tree
Showing 12 changed files with 151 additions and 26 deletions.
31 changes: 31 additions & 0 deletions README.md
Expand Up @@ -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}.
*/
Expand Down Expand Up @@ -9208,6 +9233,12 @@ function quux () {
function quux () {
}
/** @description something */
function quux () {
}
// "jsdoc/require-description": ["error"|"warn", {"descriptionStyle":"tag"}]
````
Expand Down
28 changes: 28 additions & 0 deletions src/iterateJsdoc.js
Expand Up @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions src/rules/checkExamples.js
Expand Up @@ -263,7 +263,7 @@ export default iterateJsdoc(({
return;
}
checkSource({
source: `(${tag.description})`,
source: `(${utils.getTagDescription(tag)})`,
targetTagName,
...filenameInfo,
});
Expand Down Expand Up @@ -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())) {
Expand Down
14 changes: 7 additions & 7 deletions src/rules/checkValues.js
Expand Up @@ -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}.`,
Expand All @@ -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}.`,
Expand All @@ -40,15 +40,15 @@ export default iterateJsdoc(({
);
} else if (!semver.valid(version)) {
report(
`Invalid JSDoc @${targetTagName}: "${jsdocParameter.description}".`,
`Invalid JSDoc @${targetTagName}: "${utils.getTagDescription(jsdocParameter)}".`,
null,
jsdocParameter,
);
}
});
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(
Expand Down Expand Up @@ -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}.`,
Expand All @@ -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,
);
Expand Down
6 changes: 3 additions & 3 deletions src/rules/matchDescription.js
Expand Up @@ -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);
}
Expand All @@ -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);
});
Expand Down
2 changes: 1 addition & 1 deletion src/rules/requireDescription.js
Expand Up @@ -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.`);
}
});
Expand Down
6 changes: 3 additions & 3 deletions src/rules/requireDescriptionCompleteSentence.js
Expand Up @@ -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);

Expand All @@ -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);
});
Expand Down
2 changes: 1 addition & 1 deletion src/rules/requireExample.js
Expand Up @@ -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.`);
Expand Down
11 changes: 6 additions & 5 deletions src/rules/requireHyphenBeforeParamDescription.js
Expand Up @@ -12,19 +12,20 @@ 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) => {
const lineIndex = jsdocTag.line;
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]
Expand All @@ -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);
Expand Down
8 changes: 4 additions & 4 deletions src/rules/validTypes.js
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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);
Expand Down
52 changes: 52 additions & 0 deletions test/rules/assertions/matchDescription.js
Expand Up @@ -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: `
/**
Expand Down
13 changes: 13 additions & 0 deletions test/rules/assertions/requireDescription.js
Expand Up @@ -851,5 +851,18 @@ export default {
}
`,
},
{
code: `
/** @description something */
function quux () {
}
`,
options: [
{
descriptionStyle: 'tag',
},
],
},
],
};

0 comments on commit 39ff85a

Please sign in to comment.