UNPKG

@persagy2/eslint-plugin

Version:

一个适用于 vue3.x、typescript 项目的通用eslint预设插件

618 lines (602 loc) 24 kB
// @ts-nocheck /** * @persagy2/eslint-plugin@0.2.0-dev.1 * * Copyright (c) 2023 halo951 <https://github.com/halo951> * Released under MIT License * * @build Mon Dec 04 2023 10:15:20 GMT+0800 (中国标准时间) * @author halo951<https://github.com/halo951> * @license MIT */ import { rules as rules$1 } from '@typescript-eslint/eslint-plugin'; import { resolveConfigFile, resolveConfig } from 'prettier'; import { fileURLToPath } from 'node:url'; import np from 'node:path'; const NAMING_CONVENTION = { kebabCase: /^([a-z]+-?)([a-z0-9]+-)*[a-z0-9]+$/, pascalCase: /^([A-Z][a-z0-9]*)*$/, flatCase: /^[a-z0-9]+$/, camelCase: /^([a-z]+){1}([A-Z][a-z0-9]*)*$/, snakeCase: /^([a-z]+_?)([a-z0-9]+_)*[a-z0-9]+$/, screamingSnakeCase: /^([A-Z]+_?)([A-Z0-9]+_)*[A-Z0-9]+$/ }; const date = /* @__PURE__ */ new Date(); const day = ["\u65E5", "\u4E00", "\u4E8C", "\u4E09", "\u56DB", "\u4E94", "\u516D", "\u65E5"]; const time = `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}(\u5468${day[date.getDay()]}) ${date.getHours()}:${date.getMinutes()}`; class RequiredCommentHandler { /** 上下文 */ context; /** 源码 */ sc; /** rule options */ options; constructor(context, options) { this.context = context; this.options = options; this.sc = context.getSourceCode(); } /** 检查是否存在单行 block comment */ checkSimpleBlockComment(node, messageId) { const beforeComment = this.sc.getCommentsBefore(node); if (beforeComment.length > 0) return; this.context.report({ node, messageId, suggest: [ { messageId, fix(fixer) { let nbsp = new Array(node.loc?.start.column).fill(" ").join(""); return fixer.insertTextBefore(node, "/** TODO */\n" + nbsp); } } ] }); } /** 增加前缀注释示例模板 * * @description 如果有注释内容, 则将注释内容进行整合 */ appendExampleCommentForNullComment(fixer, node) { const task = []; const declaration = node.declaration; let comments = ["/**"]; comments.push(` * TODO <\u63CF\u8FF0\u4FE1\u606F>`); comments.push(" * "); if (node.superClass) { const superClass = node.superClass; comments.push(`* @extends ${superClass.name}`); } if (declaration) { const type = node.declaration.type; switch (type) { case "TSEnumDeclaration": comments.push(` * @enum`); break; case "TSInterfaceDeclaration": comments.push(` * @interface`); if (node.declaration.extends) { const extendsStr = node.declaration.extends.map((e) => e.expression?.name).filter((n) => !!n).join(","); comments.push(`* @extends ${extendsStr}`); } break; } } comments.push(" * @author <author@persagy.com>"); comments.push(" * @date " + time); comments.push(" */"); task.push(fixer.insertTextBefore(node, comments.join("\n") + "\n")); return task; } /** 将已存在的夯筑是转化成块注释 */ convertLineCommentToBlockComment(fixer, node, needDate) { const task = []; const beforeComment = this.sc.getCommentsBefore(node); const commentMessage = beforeComment.map((n) => n.value.trim()); if (needDate && !commentMessage.find((c) => c.includes("@date"))) { commentMessage.push("\n"); commentMessage.push(`@date ${time}`); } const commentContent = commentMessage.map((c) => ` * ${c.trim()}`).join("\n"); for (const c of beforeComment) { if (c.range) task.push(fixer.removeRange(c.range)); } task.push(fixer.insertTextBefore(node, `/** ${commentContent} */ `)); return task; } /** 需要增加前置注释 */ shouldHasBeforeCommnet(node, messageId) { const beforeComment = this.sc.getCommentsBefore(node); if (beforeComment.length === 0) { this.context.report({ node, messageId, suggest: [ { messageId, fix: (fixer) => this.appendExampleCommentForNullComment(fixer, node) } ] }); return false; } return true; } /** 需要使用块注释风格 */ shouldUsedBlockComment(node, messageId) { const beforeComment = this.sc.getCommentsBefore(node); const hasLineCommnet = !!beforeComment.find( (c) => c.type === "Line" && !c.value.includes("eslint-disable") ); if (!hasLineCommnet) return; this.context.report({ node, messageId, suggest: [{ messageId, fix: (fixer) => this.convertLineCommentToBlockComment(fixer, node, true) }] }); } /** 常量需要包含注释 */ constantShouldHasComment(node) { const isVar = node.parent.type === "VariableDeclarator"; const isConstantNameStyle = NAMING_CONVENTION.screamingSnakeCase.test(node.name); if (isVar && isConstantNameStyle) { const checkNode = node.parent.parent.parent.type === "ExportNamedDeclaration" ? node.parent.parent.parent : node.parent.parent; const beforeComment = this.sc.getCommentsBefore(checkNode); const hasComment = !!beforeComment.find((c) => !c.value.includes("eslint-disable")); if (hasComment) return; this.context.report({ node, messageId: `CONSTANT_NEED_HAS_COMMENT` }); } } /** 检查方法定义 */ checkFunuctionDefined(node) { let body; switch (node.type) { case "ArrowFunctionExpression": if (/Expression/.test(node.body.type)) { return; } body = node.body; break; case "FunctionDeclaration": body = node.body; break; case "MethodDefinition": body = node.value.body; break; } const filterLength = 3; const maxCommnet = Math.floor(filterLength / 2); const c = this.sc.getCommentsInside(node).length; const s = body.body.length; if (s > filterLength && c < maxCommnet) { this.context.report({ node, messageId: "METHOD_COMMENT_LINE_LENGTH", data: { filterLength, maxCommnet } }); } } } const requiredComment = { // meta 的规则说明 // https://cn.eslint.org/docs/developer-guide/working-with-rules#rule-basics meta: { type: "layout", docs: { description: "\u5F3A\u5236\u8981\u6C42\u5199\u6CE8\u91CA", recommended: true }, fixable: "whitespace", hasSuggestions: true, schema: [], messages: { EXPORT_NEED_HAS_COMMENT: `\u5BFC\u51FA\u58F0\u660E\u9700\u8981\u589E\u52A0\u524D\u7F00\u6CE8\u91CA (block comment \u98CE\u683C)`, EXPORT_NEED_USED_EFFECTIVE_BLOCK_COMMENT: `\u5BFC\u51FA\u58F0\u660E\u5EFA\u8BAE\u4F7F\u7528\u5BF9\u7F16\u8F91\u5668\u66F4\u52A0\u53CB\u597D\u7684 block comment \u98CE\u683C`, CLASS_NEED_HAS_COMMENT: `\u7C7B\u58F0\u660E\u9700\u8981\u589E\u52A0\u524D\u7F00\u6CE8\u91CA (block comment \u98CE\u683C)`, CLASS_NEED_USED_EFFECTIVE_BLOCK_COMMENT: `\u7C7B\u58F0\u660E\u5EFA\u8BAE\u4F7F\u7528\u5BF9\u7F16\u8F91\u5668\u66F4\u52A0\u53CB\u597D\u7684 block comment \u98CE\u683C`, CONSTANT_NEED_HAS_COMMENT: `\u5E38\u91CF\u5B9A\u4E49\u9700\u8981\u589E\u52A0\u524D\u7F00\u6CE8\u91CA, \u5EFA\u8BAE\u4F7F\u7528 'block comment'`, ENUM_SHOULD_HAS_COMMENT_OR_VALUE: `\u679A\u4E3E\u9009\u9879\u9700\u8981\u6709\u6CE8\u91CA\u8BF4\u660E\u6216\u663E\u5F0F\u58F0\u660E\u679A\u4E3E\u503C`, INTERFACE_SHOULD_HAS_COMMENT: `\u63A5\u53E3\u5C5E\u6027\u9700\u8981\u6709\u6CE8\u91CA\u8BF4\u660E`, ABSTRACT_PROPERTY_NEED_COMMENT: "\u62BD\u8C61\u5C5E\u6027\u9700\u8981\u6709\u6CE8\u91CA\u8BF4\u660E", ABSTRACT_METHOD_NEED_COMMENT: "\u62BD\u8C61\u65B9\u6CD5\u9700\u8981\u6709\u6CE8\u91CA\u8BF4\u660E", STATIC_NEED_COMMENT: "\u9759\u6001\u8BED\u6CD5\u5757\u9700\u8981\u6709\u6CE8\u91CA\u8BF4\u660E", PROPERTY_MANY_WORD_NEED_COMMENT: "\u5F53\u5C5E\u6027\u547D\u540D\u7531\u591A\u4E2A\u5355\u8BCD\u6784\u6210\u65F6, \u9700\u8981\u6709\u6CE8\u91CA\u8BF4\u660E", NEED_COMMENT: "\u9700\u8981\u6CE8\u91CA", METHOD_COMMENT_LINE_LENGTH: "\u5F53\u65B9\u6CD5\u4E2D, \u5B9A\u4E49\u4E86\u8D85\u8FC7{{ filterLength }}\u4E2A\u8BED\u6CD5\u5757\u65F6, \u65B9\u6CD5\u5185\u6CE8\u91CA\u6761\u6570\u4E0D\u5F97\u5C11\u4E8E{{ maxCommnet }}\u6761" } }, create(context) { const options = { export: false, class: false, constant: true, enum: false, interface: true, method: true, prop: false }; const h = new RequiredCommentHandler(context, options); return { /** export default 必须包含注释 */ ExportDefaultDeclaration(node) { let next = false; if (!options.export) return; next = h.shouldHasBeforeCommnet(node, "EXPORT_NEED_HAS_COMMENT"); if (next) h.shouldUsedBlockComment(node, "EXPORT_NEED_USED_EFFECTIVE_BLOCK_COMMENT"); }, /** export name 的对象必须包含注释 */ ExportNamedDeclaration(node) { let next = false; if (!options.export) return; next = h.shouldHasBeforeCommnet(node, "EXPORT_NEED_HAS_COMMENT"); if (next) h.shouldUsedBlockComment(node, "EXPORT_NEED_USED_EFFECTIVE_BLOCK_COMMENT"); }, /** 类声明必须包含注释 * * - 如果, 类声明的 parent 节点 是 export default or export name, 那么忽略类注释 */ ClassDeclaration(node) { let next = false; if (!options.class) return; if (["ExportDefaultDeclaration", "ExportNamedDeclaration"].includes(node.parent.type)) { return; } next = h.shouldHasBeforeCommnet(node, "CLASS_NEED_HAS_COMMENT"); if (next) h.shouldUsedBlockComment(node, "CLASS_NEED_USED_EFFECTIVE_BLOCK_COMMENT"); }, /** 通过标识符过滤器, 获取常量声明的定义 */ Identifier(node) { if (!options.constant) return; h.constantShouldHasComment(node); }, /** ts 枚举声明 */ TSEnumMember(node) { if (!options.enum) return; h.checkSimpleBlockComment(node, "ENUM_SHOULD_HAS_COMMENT_OR_VALUE"); }, /** ts 接口声明 */ TSInterfaceBody(body) { if (!options.interface) return; const list = body.body; for (const prop of list) { let node = prop; h.checkSimpleBlockComment(node, "INTERFACE_SHOULD_HAS_COMMENT"); } }, /** 属性定义 */ Property(node) { if (!options.prop) return; const key = node.key; const name = key?.name ?? ""; const wordLen = name.split(/[A-Z]/).length; if (wordLen > 1) { h.checkSimpleBlockComment(node, "PROPERTY_MANY_WORD_NEED_COMMENT"); } }, /** 类成员属性 * @description only ts */ PropertyDefinition(node) { if (!options.class) return; const key = node.key; const name = key?.name ?? ""; const wordLen = name.split(/[A-Z]/).length; if (wordLen > 1) { h.checkSimpleBlockComment(node, "PROPERTY_MANY_WORD_NEED_COMMENT"); } }, /** 静态语法块 * @description only ts */ StaticBlock(node) { if (!options.class) return; h.checkSimpleBlockComment(node, "STATIC_NEED_COMMENT"); }, /** 抽象属性 * @description only ts */ TSAbstractPropertyDefinition(node) { if (!options.class) return; h.checkSimpleBlockComment(node, "ABSTRACT_PROPERTY_NEED_COMMENT"); }, /** 抽象方法 * @description only ts */ TSAbstractMethodDefinition(node) { if (!options.class) return; h.checkSimpleBlockComment(node, "ABSTRACT_METHOD_NEED_COMMENT"); }, /** 方法声明器 * * @description * - 如果方法名有多个单词构成, 则需要方法名需要注释 * - 检查方法圈复杂度, 判断是否需要内容注释 */ FunctionDeclaration(node) { if (!options.method) return; h.checkFunuctionDefined(node); }, /** 成员变量定义 * * @description * - 如果方法名有多个单词构成, 则需要方法名需要注释 * - 检查方法圈复杂度, 判断是否需要内容注释 */ MethodDefinition(node) { if (!options.method) return; if (["constructor", "set"].includes(node.kind)) return; h.checkFunuctionDefined(node); }, /** 箭头函数 */ ArrowFunctionExpression(node) { if (!options.method) return; h.checkFunuctionDefined(node); } }; } }; const rules = { /** 要求指定语法快必须包含注释 */ "required-comment": requiredComment }; const AnyParser = { parseForESLint(text, _options) { return { ast: { sourceType: "script", type: "Program", body: [], tokens: [], comments: [], range: [0, text.length], loc: { start: { line: 1, column: 0 }, end: { line: text.split(/\n/).length + 1, column: 0 } } } }; } }; const parser = { extends: ["plugin:vue/base"], plugins: ["@persagy2", "@typescript-eslint", "vue", "prettier"], ignorePatterns: ["*.lock", "*.jpg", "*.png", "*.font", "*.log"], parserOptions: { parser: { js: "espree", jsx: "espree", ts: require.resolve("@typescript-eslint/parser"), tsx: require.resolve("@typescript-eslint/parser"), vue: require.resolve("vue-eslint-parser"), md: AnyParser, markdown: AnyParser, json: AnyParser, json5: AnyParser, svg: AnyParser, less: AnyParser, scss: AnyParser, sass: AnyParser, css: AnyParser }, extraFileExtensions: [".vue"], ecmaFeatures: { jsx: true } }, env: { es2022: true, node: true, browser: true, commonjs: true }, rules: {} }; const isVue = await (async () => { try { return !!await import('./chunks/vue.runtime.esm-bundler.mjs'); } catch (error) { return false; } })(); const writeRules = (list) => { const rules = {}; const write = (level, name, options) => { let r; const isTsRule = /^@typescript-eslint\/.+?/.test(name); const isVueRule = /^vue\/.+?/.test(name); if (isVueRule && !isVue) return; if (isTsRule) { r = name.replace("@typescript-eslint/", ""); if (rules$1[r]?.meta?.docs?.requiresTypeChecking) return; if (rules[r]) delete rules[r]; } if (isVueRule) { r = name.replace("vue/", ""); if (rules[r]) rules[r]; } if (options) { rules[name] = [level]; } else { rules[name] = [level, options]; } }; for (const { level, name, options } of list) { write(level, name, options); } return rules; }; const createConfig = () => { const conf = { extends: [], rules: {} }; let level = "error"; const list = []; return { /** 继承哪个规则的配置 */ extends(parent) { conf.extends.push(parent); return this; }, /** 指示下面导入的规则的等级 */ level(value) { level = value; return this; }, /** 添加规则 */ rule(name, options) { list.push({ level, name, options }); return this; }, /** 导出配置 */ config() { conf.rules = writeRules(list); return conf; } }; }; const basic = createConfig().extends("plugin:@persagy2/parser").level("error").rule("no-eval").rule("@typescript-eslint/no-implied-eval").rule("no-undef").rule("no-loss-of-precision").rule("no-delete-var").rule("no-invalid-regexp").rule("no-useless-backreference").rule("no-invalid-this").rule("no-constructor-return").rule("constructor-super").rule("getter-return").rule("no-setter-return").rule("no-this-before-super").rule("unicorn/no-invalid-remove-event-listener").rule("no-irregular-whitespace").rule("no-shadow-restricted-names").rule("use-isnan").rule("valid-typeof").rule("no-unsafe-finally").rule("no-unsafe-negation").rule("no-useless-escape").rule("no-control-regex").rule("no-empty-character-class").rule("no-redeclare").rule("no-dupe-args").rule("no-dupe-class-members").rule("no-dupe-keys").rule("no-class-assign").rule("no-const-assign").rule("no-func-assign").rule("no-import-assign").rule("@typescript-eslint/ban-ts-comment").rule("@typescript-eslint/adjacent-overload-signatures").rule("@typescript-eslint/no-loss-of-precision").rule("@typescript-eslint/no-invalid-this").rule("@typescript-eslint/no-non-null-assertion").rule("@typescript-eslint/no-redeclare").rule("@typescript-eslint/no-dupe-class-members").rule("vue/comment-directive").rule("vue/jsx-uses-vars").rule("vue/valid-attribute-name").rule("vue/valid-define-emits").rule("vue/valid-define-props").rule("vue/valid-next-tick").rule("vue/valid-template-root").rule("vue/valid-v-bind").rule("vue/valid-v-cloak").rule("vue/valid-v-else-if").rule("vue/valid-v-else").rule("vue/valid-v-for").rule("vue/valid-v-html").rule("vue/valid-v-if").rule("vue/valid-v-model").rule("vue/valid-v-on").rule("vue/valid-v-once").rule("vue/valid-v-pre").rule("vue/valid-v-show").rule("vue/valid-v-slot").rule("vue/valid-v-text").rule("vue/valid-v-is").rule("vue/valid-v-memo").rule("vue/no-parsing-error").rule("vue/no-template-key").rule("vue/no-export-in-script-setup").rule("vue/no-setup-props-destructure").rule("vue/no-expose-after-await").rule("vue/no-lifecycle-after-await").rule("vue/no-watch-after-await").rule("vue/no-reserved-component-names").rule("vue/no-reserved-keys").rule("vue/no-dupe-keys").rule("vue/no-mutating-props").rule("vue/no-reserved-props").rule("vue/no-async-in-computed-properties").rule("vue/no-computed-properties-in-data").config(); const __filename = fileURLToPath(import.meta.url); const root = np.resolve(np.dirname(__filename), ".."); const DEFAULT_CONFIG = { // ! 注意: 由于 eslint-plugin-prettier 识别不出来时, 会默认给一个 babel 的 parser, 所以这里覆盖掉这个配置 // ! 让 prettier 自己去识别 parser: void 0, /** 在语句末尾打印分号 */ semi: false, /** 优先单引号 */ singleQuote: true, /** 单行 120字节代码 */ printWidth: 120, /** tab 长度 */ tabWidth: 4, /** 多行数组、对象结尾不附加逗号 */ trailingComma: "none", /** 文件结尾空行 */ endOfLine: "auto" }; const resolvePrettierConfig = async () => { const configPath = await resolveConfigFile(root); if (!configPath) { return DEFAULT_CONFIG; } else { const config = await resolveConfig(configPath); return { ...DEFAULT_CONFIG, ...config }; } }; const prettierConfig = await resolvePrettierConfig(); const style = createConfig().extends("plugin:@persagy2/parser").level("warn").rule("prettier/prettier", prettierConfig).rule("spaced-comment").rule("lines-around-comment", { beforeBlockComment: true }).rule("sort-imports", { ignoreCase: true, ignoreDeclarationSort: true, memberSyntaxSortOrder: ["none", "all", "single", "multiple"], allowSeparatedGroups: true }).rule("vue/prefer-import-from-vue").rule("prefer-arrow-callback").rule("one-var", "never").rule("no-unneeded-ternary").rule("@typescript-eslint/prefer-optional-chain").rule("yoda").rule("@typescript-eslint/no-array-constructor").rule("@typescript-eslint/array-type").rule("vue/html-end-tags").rule("vue/attribute-hyphenation").rule("vue/component-definition-name-casing", "PascalCase").config(); const deprecated = createConfig().extends("plugin:@persagy2/parser").level("error").rule("vue/no-deprecated-destroyed-lifecycle").rule("vue/no-deprecated-dollar-listeners-api").rule("vue/no-deprecated-dollar-scopedslots-api").rule("vue/no-deprecated-events-api").rule("vue/no-deprecated-filter").rule("vue/no-deprecated-functional-template").rule("vue/no-deprecated-html-element-is").rule("vue/no-deprecated-inline-template").rule("vue/no-deprecated-props-default-this").rule("vue/no-deprecated-router-link-tag-prop").rule("vue/no-deprecated-v-on-native-modifier").rule("vue/no-deprecated-vue-config-keycodes").rule("vue/no-deprecated-v-bind-sync").rule("vue/no-deprecated-v-is").rule("vue/no-deprecated-v-on-number-modifiers").rule("vue/no-deprecated-data-object-declaration").config(); const comment = createConfig().extends("plugin:@persagy2/parser").level("warn").rule("@persagy2/required-comment").config(); const complexity = createConfig().extends("plugin:@persagy2/parser").level("warn").rule("max-statements-per-line", { max: 6 }).rule("max-nested-callbacks", { max: 4 }).rule("complexity", { max: 12 }).rule("max-depth", { max: 6 }).rule("max-params", { max: 6 }).rule("no-sequences").rule("no-array-constructor").rule("no-cond-assign").rule("no-multi-assign").rule("no-return-assign").rule("vue/no-use-computed-property-like-method").rule("vue/no-side-effects-in-computed-properties").config(); const normal = createConfig().extends("plugin:@persagy2/parser").extends("plugin:@persagy2/basic").extends("plugin:@persagy2/deprecated").extends("plugin:@persagy2/style").level("warn").rule("no-empty").rule("no-empty-pattern").rule("no-empty-function").rule("no-loop-func").rule("@typescript-eslint/no-loop-func").rule("no-case-declarations").rule("default-case", { commentPattern: "skip" }).rule("no-warning-comments", { terms: ["todo", "TODO"], location: "anywhere" }).rule("vue/no-ref-as-operand").rule("vue/prop-name-casing").rule("@typescript-eslint/no-misused-promises", { checksVoidReturn: false }).rule("@typescript-eslint/no-misused-new").rule("no-promise-executor-return").rule("no-unreachable").rule("no-unused-vars").rule("@typescript-eslint/no-unused-vars").rule("vue/no-unused-vars").rule("no-unused-private-class-members").rule("@typescript-eslint/no-empty-function").rule("vue/eqeqeq").rule("no-async-promise-executor").config(); const strict = createConfig().extends("plugin:@persagy2/normal").extends("plugin:@persagy2/comment").extends("plugin:@persagy2/complexity").config(); const index = { /** 特定后缀的文件处理器 */ processors: { ".d.ts": require("@typescript-eslint/parser"), ".vue": require("eslint-plugin-vue/lib/processor") }, /** 框架内置的自定义规则 (规则扩展) */ rules, /** 可选规则模板 */ configs: { /** 解析器配置 */ parser, /** 语义语法、编码安全性检查 (一般不需要启用, 包含完整的语法语义规则校验, 可能会拖慢lint速度) */ basic, /** 编码风格检查 */ style, /** 废弃语法特性检查 (vue3.x) */ deprecated, /** 注释完善性检查 */ comment, /** 编码复杂度检查 */ complexity, /** 标准(完整)规则 * * @包含 * 1. 语义语法、编码安全性检查 * 2. 废弃语法特性检查 (vue3.x) * 3. 编码风格检查 * 4. 编码复杂度 */ normal, /** * 严格(完整)规则 * * @description 在标准规则的基础上, 增加编码复杂度、注释规范检查 */ strict } }; const createEslintConfig = (options) => { const { strategy, disable, dev } = options; if (disable) { return { root: true, extends: ["plugin:@persagy2/parser"] }; } else if (strategy === "style" || dev) { return { root: true, /** 配置规则等级 */ extends: ["plugin:@persagy2/style"] }; } else if (strategy === "normal") { return { root: true, /** 配置规则等级 */ extends: ["plugin:@persagy2/normal"] }; } else if (strategy === "strict") { return { root: true, /** 配置规则等级 */ extends: ["plugin:@persagy2/strict"] }; } }; export { createEslintConfig, index as default };