Eslint 自定义插件

6 篇文章 0 订阅
6 篇文章 0 订阅

Eslint 自定义插件

需求来源

在整理 import 的时候,发现 {} 中属性换行调整不到想要的结果。

例如:

// 编写的代码
import {A, B, C} from 'xxx'

// 期望格式化后的结果
import {
  A,
  B,
  C
} from 'xxx'

// object-curly-newline 规则的结果,我期望的是每一行一个属性
// 'object-curly-newline': [
//   'error',
//   {
//     ImportDeclaration: {
//       multiline: true,
//       minProperties: 3,
//     },
//   }
// ]
import {
  A, B, C
} from 'xxx'

于是打算自己实现一个 eslint plugin 来实现该功能。

思路

根据 eslint plugin 文档,创建项目结构,eslint 有自带的工具 yo,这里就没用使用,其实是一个道理。

├── README.md
├── build.js
├── jest.config.js
├── lib
│   ├── helper.js
│   ├── index.js
│   └── rules
│       └── newline.js
├── package-lock.json
├── package.json
└── tests
    └── newline.test.js

代码说明

package.json

比较核心的字段:
main: ./lib/index.js,对应commonjs引入方式的程序入口文件
exports: ./lib/index.js,默认导出的文件
scripts.build: 打包出较小的文件目录结构
scripts.test: 单元测试,包括了代码覆盖率
peerDependencies: 限制了 eslint 版本

{
  "name": "eslint-plugin-import-curly",
  "version": "1.0.1",
  "description": "import curly",
  "keywords": [
    "eslint",
    "eslintplugin",
    "eslint-plugin"
  ],
  "main": "./lib/index.js",
  "exports": "./lib/index.js",
  "scripts": {
    "build": "node ./build.js",
    "test": "jest --coverage",
    "fix": "eslint lib build.js --fix"
  },
  "devDependencies": {
    "@typescript-eslint/parser": "^6.18.1",
    "eslint": "^8.19.0",
    "eslint-doc-generator": "^1.0.0",
    "eslint-plugin-eslint-plugin": "^5.0.0",
    "eslint-plugin-jest": "^27.6.2",
    "fs-extra": "^11.2.0",
    "jest": "^29.7.0",
    "typescript": "^5.3.3"
  },
  "engines": {
    "node": "^14.17.0 || ^16.0.0 || >= 18.0.0"
  },
  "peerDependencies": {
    "eslint": ">=7"
  },
  "license": "ISC"
}

build.js

先看看如何打包,思路很简单,核心思路,拷贝 package.json README.md 和 lib 目录,package.json 去掉 devDependencies 中的所有依赖即可,
然后把这些文件放入到 build 中

'use strict'

const fs = require('fs')
const fse = require('fs-extra')
const { join } = require('path')

const buildDir = join(__dirname, 'build')

if (fs.existsSync(buildDir)) {
  fs.rmdirSync(buildDir, {recursive: true})
}
fs.mkdirSync(buildDir)

const packageJSON = JSON.parse(fs.readFileSync('./package.json', 'utf8'))
packageJSON.devDependencies = {}

fse.copySync('./lib', join(buildDir, 'lib'))
fs.writeFileSync(join(buildDir, 'package.json'), JSON.stringify(packageJSON, undefined, 2), 'utf8')
fs.copyFileSync('./README.md', join(buildDir, 'README.md'))

lib/index.js

正文开始,现在来看看 index.js 写了什么。

这文件也很简单,导入了一个包,然后导出了一个 rulesrules 里面包含的这是规则名称,即 newline

文档参考 Eslint plugin

"use strict";

const newline = require('./rules/newline')

module.exports.rules = {
  newline,
}

lib/rules/newline.js

现在来看看规则是如何实现的。

文档参考 Custom rules

module.exports = {
  meta: {
    // 该规则主要关注空格、分号、逗号和括号,程序的所有部分决定代码的外观而不是代码的执行方式。这些规则适用于 AST 中未指定的部分代码。
    type: 'layout',
    // whitespace: 可以修复代码中的空白字符问题,例如缩进不一致、换行不规范、空格过多或过少等。
    fixable: 'whitespace',
    // 传入参数的规则校验
    schema: [
      {
        type: 'object',
        properties: {
          count: {
            type: 'number',
          },
        },
        additionalProperties: false,
      },
    ],
    // 规则文档
    docs: {
      url: 'xxxxx'
    },
    // key: reportId, value: 错误消息提示
    messages: {
      'import-curly-newline': 'Run autofix to import curly newline!',
    },
  },
  // 返回一个对象,其中包含 ESLint 在遍历 JavaScript 代码的抽象语法树(ESTree 定义的 AST)时调用到 “visit” 节点的方法
  // AST 结构可以在网站 https://astexplorer.net/ 上查看
  create: (context) => {
    // 初始化默认值,超过了三个属性则换行
    const option = getDefaultValues({ ...context.options[0] }, { count: 3 })

    return {
      // 遍历 import 语句的回调方法,node 则为当前 import 节点
      ImportDeclaration: node => {
        // 保留花括号中的属性,去掉 default 的导入: import A, {B, C} from 'xxx',即去掉 A
        const importSpecifiers = node.specifiers.filter(v => v.type === 'ImportSpecifier')
        if (!(importSpecifiers.length && option.count)) {
          return
        }
        if (importSpecifiers.length < option.count) {
          return
        }
        // 判断和处理代码
        handleFailedImport(context, node, importSpecifiers, 'import-curly-newline')
      },
    }
  }
}

function handleFailedImport(context, node, importSpecifiers, messageId) {
  const sourceCode = context.getSourceCode()

  const {
    // 左花括号节点
    bracesLeft,
    // 后花括号节点
    bracesRight,
    // 最后一个属性是否是逗号结尾
    isHavaLastComma
  } = getBracesTokens(sourceCode.getTokens(node))

  if (
    bracesLeft &&
    bracesRight &&
    importSpecifiers &&
    // 判断是否是每个属性都在单独的一行
    isImportNewline(importSpecifiers, bracesLeft.loc.start.line, bracesRight.loc.start.line)
  ) {
    // 报告错误
    reportFailedImport(
      context,
      [bracesLeft.range[0], bracesRight.range[1]],
      messageId,
      // 修复后的代码
      fixFailedImportCurlyNewline(sourceCode.getText(node), isHavaLastComma)
    )
  }
}

// true 表示代码不满足规则
function isImportNewline(nodes, bracesLeftLine, bracesRightLine) {
  // 初始化上一行为左花括号的行数
  let lastNodeLine = bracesLeftLine
  for (const node of nodes) {
    // 判断同一个节点是否在同一行 A as AA, A \n as AA
    if (node.loc.start.line !== node.loc.end.line) {
      return true
    }
    // 判断上一个节点和该节点是否在同一行
    if (lastNodeLine === node.loc.start.line) {
      return true
    } else {
      lastNodeLine = node.loc.start.line
    }
  }
  // 判断最后一个节点是否和右花括号在 同一行
  return nodes[nodes.length - 1].loc.start.line === bracesRightLine
}

// 修复代码
function fixFailedImportCurlyNewline(codeStr, isHavaLastComma) {
  // 判断结束符
  const lineOff = /\r\n/.test(codeStr) ? '\r\n' : (/\r/.test(codeStr) ? '\r' : '\n')

  // 思路是把花括号中的内容,先去掉换行符,然后按照逗号分成数组,过滤掉空行,最后在每个节点前加两个空格
  // TODO 这里固定了加两个空格,应该做兼容,暂时没有想到有什么好的方案
  const params = codeStr.substring(codeStr.indexOf('{') + 1, codeStr.indexOf('}'))
    .replace(/\n/g, '')
    .replace(/\r/g, '')
    .split(',')
    .filter(v => v.trim())
    .map(v => `  ${v.trim()}`)

  // 如果最后有逗号,添加一个空节点
  if (isHavaLastComma) {
    params.push('')
  }

  // 拼接修复后的代码即可
  return `{${lineOff}${params.join(`,${lineOff}`)}${isHavaLastComma ? '}' : `${lineOff}}`}`
}

lib/helper.js

相对通用的方法

// 获取左右花括号的位置,判断最后是否有逗号
function getBracesTokens(tokens) {
  let bracesLeft
  let bracesRight
  let isHavaLastComma = false
  tokens.forEach((v, i) => {
    if (v.type === 'Punctuator' && v.value === '{') {
      bracesLeft = v
    }
    if (v.type === 'Punctuator' && v.value === '}') {
      if (i > 0 && tokens[i - 1].value === ',') {
        isHavaLastComma = true
      }
      bracesRight = v
    }
  })
  return {
    bracesLeft,
    bracesRight,
    isHavaLastComma,
  }
}

// 报告错误代码,并且提示修复
function reportFailedImport(context, ranges, messageId, fixedCode) {
  const sourceCode = context.getSourceCode()

  context.report({
    messageId,
    loc: {
      start: sourceCode.getLocFromIndex(ranges[0]),
      end: sourceCode.getLocFromIndex(ranges[1])
    },
    fix: fixer => fixer.replaceTextRange(ranges, fixedCode)
  })
}

// 生成默认参数
function getDefaultValues(options, defaultValues) {
  for (const key of Object.keys(defaultValues)) {
    if (options[key] === undefined) {
      options[key] = defaultValues[key]
    }
  }
  return options
}

使用方法

先安装该插件

npm install eslint-plugin-import-curly --save-dev

.eslintrc 文件中配置该插件,去掉 eslint-plugin-

{
  "plugins": ["import-curly"]
}

规则:import-curly/newline

用于格式化换行操作,import 花括号中属性默认超过了 3 个,则每个属性都换一行。

name类型默认值描述
countnumber3如果花括号中的属性超过了 3 个,则会触发该规则
{
  "rules": {
    "import-curly/newline": "error"
  }
}

或者

{
  "rules": {
    "import-curly/newline": [
      "error",
      {
        "count": 4
      }
    ]
  }
}

case:

// invalid
import {
  A as AA,B, C} from 'C'

// ||
// \/

// valid
import {
  A as AA,
  B,
  C
} from 'C'

规则:import-curly/sort-params

用于 import 花括号中的属性进行排序

name类型默认值可选值描述
typeLocationstringignoreignore,first,lastignore: 忽略 ts 中 type 的位置,first: type 的位置在最前方, last: type 的位置在最后放
orderBystringalphabeticalOrderalphabeticalOrder,letterNumberalphabeticalOrder: 按照字母排序, letterNumber: 按照字数排序。如果按照字数排序,相同字数的会按照字母排序
sortBystringaecaec,descaec: 增序, desc: 降序
ignoreCasebooleantruetrue: 忽略大小写,false: 不忽略大小写
{
  "rules": {
    "import-curly/sort-params": "error"
  }
}

或者

{
  "rules": {
    "import-curly/sort-params": [
      "error",
      {
        "typeLocation": "first",
        "orderBy": "letterNumber",
        "sortBy": "desc",
        "ignoreCase": false
      }
    ]
  }
}
默认规则
{
  "rules": {
    "import-curly/sort-params": "error",
    "import-curly/sort-params": [
      "error",
      {
        "typeLocation": "ignore",
        "orderBy": "alphabeticalOrder",
        "sortBy": "aec",
        "ignoreCase": true
      }
    ]
  }
}

有效的样例

import type {a,b,c,d} from 'a'
import {A,B,C,D} from 'a'
import type {a,B,c,D} from 'a'
import {A,b,c,D} from 'a'
import {type a,b,type c,d} from 'a'
import {a,type b,c,default as d} from 'a'

错误的样例

// invalid
import {type A,type c,d,B} from 'a'
// ||
// \/
// valid
import {type A,B,type c,d} from 'a'


// invalid, ignore type, compare with 'default'
import {c,default as d,type b,a} from 'a'
// ||
// \/
// valid
import {a,type b,c,default as d} from 'a'
type 在最前方
{
  "rules": {
    "import-curly/sort-params": [
      "error",
      {
        "typeLocation": "first"
      }
    ],
    "import-curly/sort-params": [
      "error",
      {
        "typeLocation": "ignore",
        "orderBy": "alphabeticalOrder",
        "sortBy": "aec",
        "ignoreCase": true
      }
    ]
  }
}

有效的样例

// type first
import {type B,type c,A, d} from 'a'
import {type b,type d,a,c} from 'a'

错误的样例

// invalid, compare with 'default'
import {c,a,default as d,type b} from 'a'
// ||
// \/
// valid
import {type b,a,c,default as d} from 'a'
按照字数排序
{
  "rules": {
    "import-curly/sort-params": [
      "error",
      {
        "orderBy": "letterNumber"
      }
    ],
    "import-curly/sort-params": [
      "error",
      {
        "typeLocation": "ignore",
        "orderBy": "letterNumber",
        "sortBy": "aec",
        "ignoreCase": true
      }
    ]
  }
}

有效的样例

import {a, ba, bb, ccc} from 'a'
import {a, ba, bb, bbb} from 'a'

错误的样例

// invalid bb ba letterNumber is the same, should order by alpha
import {a, bb, ba} from 'a'
// ||
// \/
// valid
import {a, ba, bb} from 'a'
按照字母降序排序
{
  "rules": {
    "import-curly/sort-params": [
      "error",
      {
        "sortBy": "desc"
      }
    ],
    "import-curly/sort-params": [
      "error",
      {
        "typeLocation": "ignore",
        "orderBy": "alphabeticalOrder",
        "sortBy": "desc",
        "ignoreCase": true
      }
    ]
  }
}

有效的样例

import {d, c, b, a} from 'a'
按照字数降序排序
{
  "rules": {
    "import-curly/sort-params": [
      "error",
      {
        "orderBy": "letterNumber",
        "sortBy": "desc"
      }
    ],
    "import-curly/sort-params": [
      "error",
      {
        "typeLocation": "ignore",
        "orderBy": "letterNumber",
        "sortBy": "desc",
        "ignoreCase": true
      }
    ]
  }
}

有效的样例

import {aaa, cc, aa, d} from 'a'
不忽略大小写
{
  "rules": {
    "import-curly/sort-params": [
      "error",
      {
        "ignoreCase": false
      }
    ],
    "import-curly/sort-params": [
      "error",
      {
        "typeLocation": "ignore",
        "orderBy": "alphabeticalOrder",
        "sortBy": "aec",
        "ignoreCase": false
      }
    ]
  }
}

有效的样例

import {A, B, C, a, b, c} from 'a'
import {A, B, C, a, b, c, default as d} from 'a'

总结

如果大佬有什么现有的更好的方案,欢迎讨论
附上项目地址: eslint-plugin-import-curly

  • 23
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要在VSCode中安装并配置ESLint件,请按照以下步骤进行操作: 1. 打开VSCode,点击左侧的扩展图标(四个方块)或按下快捷键`Ctrl+Shift+X`来打开扩展面板。 2. 在搜索栏中输入"ESLint"并按下回车键,找到ESLint件并点击安装按钮进行安装。 3. 安装完成后,点击"重新加载"按钮来重新加载VSCode。 4. 接下来,你需要在你的项目中安装ESLint。在终端中进入你的项目根目录,并执行以下命令: ``` npm install eslint --save-dev ``` 或者如果你使用的是Yarn: ``` yarn add eslint --dev ``` 5. 安装完成后,在终端中执行以下命令来初始化ESLint配置文件: ``` npx eslint --init ``` 这将引导你完成一个交互式的配置过程。你可以根据自己的需求选择一些配置选项,例如选择ESLint的规则(Airbnb、Standard等),或者手动配置自定义规则。 6. 完成配置后,ESLint将会生成一个`.eslintrc`文件,该文件存放着ESLint的规则配置。 7. 回到VSCode,点击左下角的设置图标(齿轮图标),选择"首选项"->"设置"来打开用户设置。 8. 在用户设置中搜索"eslint.autoFixOnSave"并勾选该选项,这将在保存文件时自动修复ESLint错误。 9. 在用户设置中搜索"eslint.validate",找到"ESLint: Validate"选项,并在其值中添加以下内容: ``` "javascript", "javascriptreact", "typescript", "typescriptreact" ``` 这将使ESLint对JavaScript、JavaScript React、TypeScript和TypeScript React文件进行验证。 现在,你已经成功安装并配置了ESLint件。在保存文件时,ESLint将会自动运行,并根据配置的规则进行验证和修复。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值