Skip to content

Commit

Permalink
v1.0.92
Browse files Browse the repository at this point in the history
Add JSDoc rule
  • Loading branch information
eliottvincent committed Jul 10, 2024
1 parent 58195a7 commit 038d8c1
Show file tree
Hide file tree
Showing 7 changed files with 232 additions and 4 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ Each item has emojis denoting:
| [crisp/jsdoc-check-optional-params](https://github.com/crisp-oss/eslint-plugin-crisp/blob/master/rules/jsdoc-check-optional-params.js) | Requires optional parameters to be surrounded by brackets | | 🟢 |
| [crisp/jsdoc-enforce-access](https://github.com/crisp-oss/eslint-plugin-crisp/blob/master/rules/jsdoc-enforce-access.js) | Requires one of `@public`, `@private`, or `@protected` for functions | | 🟢 |
| [crisp/jsdoc-enforce-classdesc](https://github.com/crisp-oss/eslint-plugin-crisp/blob/master/rules/jsdoc-enforce-classdesc.js) | Ensures JSDoc for class headers to include a non-empty `@classdesc` | 🟠 | 🟢 |
| [crisp/jsdoc-require-description-uppercase](https://github.com/crisp-oss/eslint-plugin-crisp/blob/master/rules/jsdoc-require-description-uppercase.js) | Requires descriptions to start with an uppercase character | 🟠 | 🟢 |

#### General Vue rules

Expand Down
1 change: 1 addition & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ module.exports = {
"jsdoc-check-optional-params": require("./rules/jsdoc-check-optional-params"),
"jsdoc-enforce-access": require("./rules/jsdoc-enforce-access"),
"jsdoc-enforce-classdesc": require("./rules/jsdoc-enforce-classdesc"),
"jsdoc-require-description-uppercase": require("./rules/jsdoc-require-description-uppercase"),
"methods-naming": require("./rules/methods-naming"),
"methods-ordering": require("./rules/methods-ordering"),
"multiline-comment-end-backslash": require("./rules/multiline-comment-end-backslash"),
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "eslint-plugin-crisp",
"version": "1.0.91",
"version": "1.0.92",
"description": "Custom ESLint Rules for Crisp",
"author": "Crisp IM SAS",
"main": "index.js",
Expand Down
1 change: 1 addition & 0 deletions recommended-vue.js
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ module.exports = {
"crisp/jsdoc-check-optional-params": "error",
"crisp/jsdoc-enforce-access": "error",
"crisp/jsdoc-enforce-classdesc": "error",
"crisp/jsdoc-require-description-uppercase": "error",

// General Vue rules
"vue/attributes-order": [
Expand Down
3 changes: 2 additions & 1 deletion recommended.js
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ module.exports = {
// Crisp JSDoc rules
"crisp/jsdoc-align-params": "error",
"crisp/jsdoc-check-indentation": "error",
"crisp/jsdoc-enforce-classdesc": "error"
"crisp/jsdoc-enforce-classdesc": "error",
"crisp/jsdoc-require-description-uppercase": "error"
}
}
224 changes: 224 additions & 0 deletions rules/jsdoc-require-description-uppercase.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
// Based on https://github.com/gajus/eslint-plugin-jsdoc/blob/main/src/rules/requireDescriptionCompleteSentence.js
// Removed handling of sentences, punctuation, abbreviations. We just need to check that the description starts with an uppercase character.

const { default: iterateJsdoc } = require("eslint-plugin-jsdoc/dist/iterateJsdoc");
const escapeStringRegexp = require("escape-string-regexp");

const otherDescriptiveTags = new Set([
// 'copyright' and 'see' might be good addition, but as the former may be
// sensitive text, and the latter may have just a link, they are not
// included by default
"summary", "file", "fileoverview", "overview", "classdesc", "todo",
"deprecated", "throws", "exception", "yields", "yield",
]);

/**
* @param {string} text
* @returns {string[]}
*/
const extractParagraphs = (text) => {
return text.split(/(?<![;:])\n\n+/u);
};

/**
* @param {string} str
* @returns {boolean}
*/
const isCapitalized = (str) => {
return str[0] === str[0].toUpperCase();
};

/**
* @param {string} str
* @returns {boolean}
*/
const isTable = (str) => {
return str.charAt(0) === "|";
};

/**
* @param {string} str
* @returns {string}
*/
const capitalize = (str) => {
return str.charAt(0).toUpperCase() + str.slice(1);
};

/**
* @param {string} description
* @param {import('../iterateJsdoc.js').Report} reportOrig
* @param {import('eslint').Rule.Node} jsdocNode
* @param {import('eslint').SourceCode} sourceCode
* @param {import('comment-parser').Spec|{
* line: import('../iterateJsdoc.js').Integer
* }} tag
* @returns {boolean}
*/
const validateDescription = (
description, reportOrig, jsdocNode, sourceCode, tag,
) => {
if (!description || (/^\n+$/u).test(description)) {
return false;
}

const descriptionNoHeadings = description.replaceAll(/^\s*#[^\n]*(\n|$)/gm, "");

const paragraphs = extractParagraphs(descriptionNoHeadings).filter(Boolean);

return paragraphs.some((paragraph, parIdx) => {
const sentences = [paragraph];

const fix = /** @type {import('eslint').Rule.ReportFixer} */ (fixer) => {
let text = sourceCode.getText(jsdocNode);

for (const sentence of sentences.filter((sentence_) => {
return !(/^\s*$/u).test(sentence_) && !isCapitalized(sentence_) &&
!isTable(sentence_);
})) {
const beginning = sentence.split("\n")[0];

if ("tag" in tag && tag.tag) {
const reg = new RegExp(`(@${escapeStringRegexp(tag.tag)}.*)${escapeStringRegexp(beginning)}`, "u");

text = text.replace(reg, (_$0, $1) => {
return $1 + capitalize(beginning);
});
} else {
text = text.replace(new RegExp("((?:[.?!]|\\*|\\})\\s*)" + escapeStringRegexp(beginning), "u"), "$1" + capitalize(beginning));
}
}

return fixer.replaceText(jsdocNode, text);
};

/**
* @param {string} msg
* @param {import('eslint').Rule.ReportFixer | null | undefined} fixer
* @param {{
* line?: number | undefined;
* column?: number | undefined;
* } | (import('comment-parser').Spec & {
* line?: number | undefined;
* column?: number | undefined;
* })} tagObj
* @returns {void}
*/
const report = (msg, fixer, tagObj) => {
if ("line" in tagObj) {
/**
* @type {{
* line: number;
* }}
*/ (tagObj).line += parIdx * 2;
} else {
/** @type {import('comment-parser').Spec} */ (
tagObj
).source[0].number += parIdx * 2;
}

// Avoid errors if old column doesn't exist here
tagObj.column = 0;
reportOrig(msg, fixer, tagObj);
};

if (sentences.some((sentence) => {
return !(/^\s*$/u).test(sentence) && !isCapitalized(sentence) && !isTable(sentence);
})) {
report("Sentences should start with an uppercase character.", fix, tag);
}

return false;
});
};

module.exports = iterateJsdoc(({
sourceCode,
context,
jsdoc,
report,
jsdocNode,
utils,
}) => {
let {
description,
} = utils.getDescription();

const indices = [
...description.matchAll(/```[\s\S]*```/gu),
].map((match) => {
const {
index,
} = match;
const [
{
length,
},
] = match;
return {
index,
length,
};
}).reverse();

for (const {
index,
length,
} of indices) {
description = description.slice(0, index) +
description.slice(/** @type {import('../iterateJsdoc.js').Integer} */ (
index
) + length);
}

if (validateDescription(description, report, jsdocNode, sourceCode, {
line: jsdoc.source[0].number + 1,
})) {
return;
}

utils.forEachPreferredTag("description", (matchingJsdocTag) => {
const desc = `${matchingJsdocTag.name} ${utils.getTagDescription(matchingJsdocTag)}`.trim();
validateDescription(desc, report, jsdocNode, sourceCode, matchingJsdocTag);
}, true);

const {
tagsWithNames,
} = utils.getTagsByType(jsdoc.tags);
const tagsWithoutNames = utils.filterTags(({
tag: tagName,
}) => {
return otherDescriptiveTags.has(tagName) ||
utils.hasOptionTag(tagName) && !tagsWithNames.some(({
tag,
}) => {
// If user accidentally adds tags with names (or like `returns`
// get parsed as having names), do not add to this list
return tag === tagName;
});
});

tagsWithNames.some((tag) => {
const desc = /** @type {string} */ (
utils.getTagDescription(tag)
).replace(/^- /u, "").trimEnd();

return validateDescription(desc, report, jsdocNode, sourceCode, tag);
});

tagsWithoutNames.some((tag) => {
const desc = `${tag.name} ${utils.getTagDescription(tag)}`.trim();

return validateDescription(desc, report, jsdocNode, sourceCode, tag);
});
}, {
iterateAllJsdocs: true,
meta: {
docs: {
description: "Requires that block description, explicit `@description`, and `@param`/`@returns` tag descriptions are written in complete sentences.",
url: "https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/require-description-complete-sentence.md#repos-sticky-header",
},
fixable: "code",
schema: [],
type: "suggestion",
},
});

0 comments on commit 038d8c1

Please sign in to comment.