明确问题

首先明确「只校验 git diff 内容的 eslint 校验逻辑」的含义,那就是只校验存在于 git diff 的文件内容,也就是被改动过的文件内容,同一文件其他地方即使存在 esilnt 问题也不校验提示

从最直观的角度来讲,我们可能会想输入入手,考虑如何将 git diff 的内容喂给 eslint 作校验,这样看起来就满足了需求。

不过,这有一个显而易见的问题

就是 eslint 的许多校验逻辑是需要上下文的,例如 unused-var 没有上下文,就无法判断是否没有被使用过。而我们处于变更的文件内容是不一定有上下文的,大概率只有零散的片段。

缺乏上下文会让 eslint 无法正确执行。

因此我们可以换一个思路,并不是在对输入动手脚,而是对输出作处理

具体实现

做法并不困难,eslint 支持基于 eslint-plugin 自定义 processor 参照 https://eslint.org/docs/developer-guide/working-with-plugins#processors-in-plugins

其中 postprocess 这个 processor 会在执行完 eslint 校验之后调用,此时我们就可以获得 eslint 中所有的校验结果,同时也包括报错的文件,行数以及具体内容。

因此,只要结合 git diff 获取到文件变更的上下边界,判断报错行数是否在这个边界之内,就可以对报错信息做一层过滤。只保留报错行数在边界范围之内的报错。其最终输出的结果正好和我们的要求是一致的。

因此,原理说明之后,实现并不困难

首先是通过 git diff 获取上下边界

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
const getDiffFiles = () => {
const stdout = execSync(`git diff --stat`, {
encoding: 'utf-8'
})
const row = stdout.trim().split('\n')
row.pop()
const files = row.map(str => {
const [filePath] = str.trim().split(' ')
return filePath
})
return files
}

const getFileChangeStatus = () => {
const files = getDiffFiles()
const matchFileName = (str) => {
for (const file of files) {
if (str.includes(file)) {
return file
}
}
return ''
}

const stdout = execSync(`git diff --unified=0 --ignore-all-space`, {
encoding: 'utf-8'
})

const changeLogs = stdout
.split("@@")

let indexArray = []
let startIndex = []
let endIndex = []
let fileNameIndex = matchFileName(changeLogs[0])
const indexMap = {}
for (let i = 1; i < changeLogs.length; i += 2) {
indexArray = changeLogs[i].trim().split(" ")
const fileName = changeLogs[i - 1].includes('diff --git')
? matchFileName(changeLogs[i - 1])
: ''
if (fileName !== '' && fileName !== fileNameIndex) {
indexMap[fileNameIndex] = {
startIndex,
endIndex,
fileName: fileNameIndex,
}
startIndex = []
endIndex = []
fileNameIndex = fileName
}

let start = 0, end = 0
let startArray = []
if (indexArray.length > 1) {
startArray = indexArray[1].split(",").map(v => parseInt(v))
}

if (startArray.length > 1) {
start = parseInt(startArray[0])
end = parseInt(startArray[0]) + parseInt(startArray[1])
} else {
start = parseInt(startArray[0])
end = start + 1
}
startIndex.push(start)
endIndex.push(end)
}

if (!indexMap[fileNameIndex]) {
indexMap[fileNameIndex] = {
startIndex,
endIndex,
fileName: fileNameIndex,
}
}
return indexMap
}

接下来,则是在 postprocess 过滤报错信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
const diffMap = {} // 一次 eslint 调用过程中,git diff 不太可能会改变,通过一个外部变量保存 diff 信息,以免多次计算
module.exports.processor = {
supportsAutofix: true,

postprocess: function (messages, filename) {

if (!Object.keys(diffMap).length) {
diffMap = getFileChangeStatus()
}

const relativePath = relative(process.cwd(), filename)

if (!Object.keys(diffMap).includes(relativePath)) {
return []
}


const {startIndex, endIndex} = diffMap[relativePath]

const filterMessages = messages[0].filter(message => {
const firstStart = startIndex[0]
const lastEnd = endIndex[endIndex.length - 1]
// 对于小于的一个开始边界和大于最后一个结束边界的报错直接跳出
if (message.line < firstStart || message.line > lastEnd) {
return false
}
return startIndex.some((start, index) => {
const end = endIndex[index]
return message.line <= start && message.line <= end
})

})

return [].concat(filterMessages);
},
}

这样,就实现了只校验 git diff 的 eslint 工具

放弃

这 demo 看起来还颇为可行,但仔细想想却有许多问题

  1. 能校验在 git diff 之内的文件,也就只能校验 git diff 之内的文件。

    这个时候如果随手新建一个文件,如果没有被加入到 git 当中,是无法被处理到的,

    同样的,各种 ide save to formatter 的格式化工具,由于输出就没有问题,自然也不会报错或者自动 fix。

    当然,这里也有一个简单的解决方法,那就是另外保留一份没用使用这个插件的配置,在项目中按需使用

  2. 无法在 ci 中使用

    另一个显而易见的问题,这个功能依赖于 git diff 输出边界。前提是存在可以 diif 的内容,在本地时,diff 的显然是 add 而未 commit 的文件。 然而,在 ci 中,我们已经 commit 了文件,git diff 并不存在对应的内容。自然也就无法输出 lint 结果

    当然,我们这个时候可以考虑通过指定当前分支和特定分支的对比获取一个确定的 diff 内容,这要求 plugin 具有更高的可配置性,而且如何选择和那个分支对比也是一个问题。

  3. 文件内风格割裂的问题

    我们使用 elinst 一定程度上还是希望达到代码风格的统一。然而使用这样一个基于部分内容的解决方案,却会带来同一个文件上代码风格的分裂。例如,在一个全是 4 空格的旧代码文件当中,突然出现部分经过编辑与格式化的代码为 2 空格。这难道不也是一个代码风格的割裂么,这和我们使用 eslint 的目标背道而驰

结语

本文介绍了一个基于 eslint plugin 和 git diff 的部分内容 lint&formatter 的方案,也介绍了最终放弃这个方案的理由。

当然,作为一个对 eslint plugin 的学习小 demo 倒也还行