From efb82d04b3c84587225085c0678a4a714e852edb Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Sat, 24 Sep 2022 09:37:10 -0700 Subject: [PATCH] Add JSDoc eslint rule See the next commit for a more fleshed-out description. --- .eslintrc.json | 1 + scripts/eslint/rules/jsdoc-format.cjs | 112 ++++++++++++++++++++++++++ 2 files changed, 113 insertions(+) create mode 100644 scripts/eslint/rules/jsdoc-format.cjs diff --git a/.eslintrc.json b/.eslintrc.json index c3ce2174a7739..f78f3250020cf 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -87,6 +87,7 @@ "local/simple-indent": "error", "local/debug-assert": "error", "local/no-keywords": "error", + "local/jsdoc-format": "error", // eslint-plugin-import "import/no-extraneous-dependencies": ["error", { "optionalDependencies": false }], diff --git a/scripts/eslint/rules/jsdoc-format.cjs b/scripts/eslint/rules/jsdoc-format.cjs new file mode 100644 index 0000000000000..9971c9945f83e --- /dev/null +++ b/scripts/eslint/rules/jsdoc-format.cjs @@ -0,0 +1,112 @@ +const { TSESTree } = require("@typescript-eslint/utils"); +const { createRule } = require("./utils.cjs"); + +module.exports = createRule({ + name: "jsdoc-format", + meta: { + docs: { + description: ``, + recommended: "error", + }, + messages: { + internalCommentInNonJSDocError: `@internal should not appear in non-JSDoc comment for declaration.`, + internalCommentNotLastError: `@internal should only appear in final JSDoc comment for declaration.`, + multipleJSDocError: `Declaration has multiple JSDoc comments.`, + internalCommentOnParameterProperty: `@internal cannot appear on a JSDoc comment; use a declared property and an assignment in the constructor instead.`, + }, + schema: [], + type: "problem", + }, + defaultOptions: [], + + create(context) { + const sourceCode = context.getSourceCode(); + const atInternal = "@internal"; + const jsdocStart = "/**"; + + /** @type {(c: TSESTree.Comment, indexInComment: number) => TSESTree.SourceLocation} */ + const getAtInternalLoc = (c, indexInComment) => { + const line = c.loc.start.line; + return { + start: { + line, + column: c.loc.start.column + indexInComment, + }, + end: { + line, + column: c.loc.start.column + indexInComment + atInternal.length, + }, + }; + }; + + /** @type {(c: TSESTree.Comment) => TSESTree.SourceLocation} */ + const getJSDocStartLoc = (c) => { + return { + start: c.loc.start, + end: { + line: c.loc.start.line, + column: c.loc.start.column + jsdocStart.length, + }, + }; + }; + + /** @type {(node: TSESTree.Node) => void} */ + const checkJSDocFormat = (node) => { + const blockComments = sourceCode.getCommentsBefore(node).filter(c => c.type === "Block"); + if (blockComments.length === 0) { + return; + } + + const last = blockComments.length - 1; + let seenJSDoc = false; + for (let i = 0; i < blockComments.length; i++) { + const c = blockComments[i]; + const rawComment = sourceCode.getText(c); + + const isJSDoc = rawComment.startsWith(jsdocStart); + if (isJSDoc && seenJSDoc) { + context.report({ messageId: "multipleJSDocError", node: c, loc: getJSDocStartLoc(c) }); + } + seenJSDoc = seenJSDoc || isJSDoc; + + const indexInComment = rawComment.indexOf(atInternal); + if (indexInComment === -1) { + continue; + } + + if (!isJSDoc) { + context.report({ messageId: "internalCommentInNonJSDocError", node: c, loc: getAtInternalLoc(c, indexInComment) }); + } + else if (i !== last) { + context.report({ messageId: "internalCommentNotLastError", node: c, loc: getAtInternalLoc(c, indexInComment) }); + } + else if (node.type === "TSParameterProperty") { + context.report({ messageId: "internalCommentOnParameterProperty", node: c, loc: getAtInternalLoc(c, indexInComment) }); + } + + // TODO(jakebailey): check duplicate @internal? + } + }; + + return { + ClassDeclaration: checkJSDocFormat, + FunctionDeclaration: checkJSDocFormat, + TSEnumDeclaration: checkJSDocFormat, + TSModuleDeclaration: checkJSDocFormat, + VariableDeclaration: checkJSDocFormat, + TSInterfaceDeclaration: checkJSDocFormat, + TSTypeAliasDeclaration: checkJSDocFormat, + TSCallSignatureDeclaration: checkJSDocFormat, + ExportAllDeclaration: checkJSDocFormat, + ExportNamedDeclaration: checkJSDocFormat, + TSImportEqualsDeclaration: checkJSDocFormat, + TSNamespaceExportDeclaration: checkJSDocFormat, + TSConstructSignatureDeclaration: checkJSDocFormat, + ExportDefaultDeclaration: checkJSDocFormat, + TSPropertySignature: checkJSDocFormat, + TSIndexSignature: checkJSDocFormat, + TSMethodSignature: checkJSDocFormat, + TSParameterProperty: checkJSDocFormat, + }; + }, +});