├── .eslintignore
├── .gitignore
├── docs
├── git-log.png
├── vscode.png
├── git-blame.png
├── demo1-after.png
├── demo1-before.png
└── commit-author.png
├── tsconfig.build.json
├── demo
├── demo4.vue
├── demoFolder
│ └── demo3.ts
├── demo1.ts
└── demo2.js
├── script
├── dev.js
├── dist.js
├── demoExpect
│ ├── demo3.txt
│ ├── demo2.txt
│ └── demo1.txt
└── test.js
├── src
├── index.ts
├── features
│ ├── fixByAuth.ts
│ └── fixNormal.ts
├── utils
│ └── utils.ts
└── libs
│ └── eslilnt-autofix-api.ts
├── .eslintrc-vue.json
├── .eslintrc.json
├── tsconfig.json
├── LICENSE
├── package.json
└── README.md
/.eslintignore:
--------------------------------------------------------------------------------
1 | script/*
2 | demo/*
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
--------------------------------------------------------------------------------
/docs/git-log.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlloyTeam/AlloyLint/HEAD/docs/git-log.png
--------------------------------------------------------------------------------
/docs/vscode.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlloyTeam/AlloyLint/HEAD/docs/vscode.png
--------------------------------------------------------------------------------
/docs/git-blame.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlloyTeam/AlloyLint/HEAD/docs/git-blame.png
--------------------------------------------------------------------------------
/docs/demo1-after.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlloyTeam/AlloyLint/HEAD/docs/demo1-after.png
--------------------------------------------------------------------------------
/docs/demo1-before.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlloyTeam/AlloyLint/HEAD/docs/demo1-before.png
--------------------------------------------------------------------------------
/docs/commit-author.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlloyTeam/AlloyLint/HEAD/docs/commit-author.png
--------------------------------------------------------------------------------
/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | // 当前项目的编译文件
2 | {
3 | "extends": "./tsconfig.json",
4 | "include": [
5 | "./src/**/*"
6 | ],
7 | }
--------------------------------------------------------------------------------
/demo/demo4.vue:
--------------------------------------------------------------------------------
1 | import Vue from "vue"
2 | import App from "./Hello";
3 |
4 | Vue.config.productionTip=false;
5 |
6 | new Vue({el: "#app", template: "",
7 | components: { App }
8 | });
--------------------------------------------------------------------------------
/script/dev.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @file 本地开发编译
3 | * @author sigmaliu
4 | */
5 |
6 | const path = require('path');
7 | const fs = require('fs');
8 | const { buildFunc } = require('./dist');
9 |
10 | buildFunc();
11 |
12 | fs.watch(path.resolve(__dirname, '../src'), { recursive: true }, () => {
13 | try {
14 | buildFunc();
15 | }
16 | catch(err) {
17 | console.log(err);
18 | }
19 | })
--------------------------------------------------------------------------------
/demo/demoFolder/demo3.ts:
--------------------------------------------------------------------------------
1 | (function() {
2 | if (false)
3 | {
4 | return 1;
5 | } else {
6 | return 2;
7 | }
8 | })()
9 |
10 | const a = {
11 | "b": 1,
12 | "c": 2,
13 | d: 3
14 | }
15 |
16 | const b = {
17 | "b": 1,
18 | "c": 2,
19 | d: 3,
20 | }
21 |
22 | let x = this.canvas.excel.selectData.active.xIndex;
23 | let y = this.canvas.excel.selectData.active.yIndex;
24 | let _isEditing = this.canvas.isEditing;
25 | let sheetInfo = {}
26 |
27 | if (false) {
28 |
29 | }
--------------------------------------------------------------------------------
/demo/demo1.ts:
--------------------------------------------------------------------------------
1 | document.documentElement.style['position'] = 'fixed';
2 |
3 | var ua = navigator.userAgent.toLowerCase();
4 |
5 | //auth2
6 | // auth2
7 |
8 | var dbName = 'dbName'
9 |
10 | const storeName = 'storeName';
11 |
12 | console.log('创建[ ' + dbName + ' / ' + storeName + ' ]成功');
13 |
14 | if(true)
15 | {
16 | console.log()
17 | } else { console.log()}
18 |
19 | var userAgent = {
20 | ua: ua
21 | }
22 |
23 | const a = 1;
24 | var b = 2;
25 | const c = 3;
26 | let d = 4;
27 |
28 | let e = {a: a, b: b, c: c, d:d};
29 |
30 | let f,
31 | g,
32 | h;
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | /**
4 | * @file eslint 修复文件
5 | * @author sigmaliu
6 | */
7 |
8 | import program from 'commander';
9 | import fixFilesNormal from './features/fixNormal';
10 | import fixFilesByAuth from './features/fixByAuth';
11 |
12 | (() => {
13 | // 解析参数
14 | program
15 | .option('-a, --author', 'apply eslint autofix but keep last author info in git blame');
16 |
17 | program.parse(process.argv);
18 |
19 | if (program.author) {
20 | fixFilesByAuth(program.args);
21 |
22 | return;
23 | }
24 |
25 | fixFilesNormal(program.args);
26 | })();
27 |
--------------------------------------------------------------------------------
/.eslintrc-vue.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "plugin:vue/base"
4 | ],
5 | "env": {
6 | "browser": true,
7 | "commonjs": true,
8 | "es6": true
9 | },
10 | "parser": "babel-eslint",
11 | "plugins": [
12 | "html"
13 | ],
14 | "parserOptions": {
15 | "sourceType": "module",
16 | "ecmaVersion": 7
17 | },
18 | "rules": {
19 | "no-console": "off",
20 | "no-restricted-syntax": "off",
21 | "no-plusplus": "off",
22 | "no-continue": "off",
23 | "indent": ["error", 4],
24 | "max-len": ["error", 140]
25 | }
26 | }
--------------------------------------------------------------------------------
/demo/demo2.js:
--------------------------------------------------------------------------------
1 | if (false) {
2 | return 1;
3 | }
4 | else {
5 | return 2;
6 | }
7 |
8 | var getImgAndWidth = function(data, dprrecord, callback) {
9 | var canvas = document.createElement('canvas');
10 | var ctx = canvas.getContext('2d');
11 | var img = new Image();
12 | }
13 |
14 | sheetId = sheetId ? sheetId : core.window.SpreadsheetApp.spreadsheet.getActiveSheetId();
15 |
16 | let rowData = sheet.data.rowData;
17 |
18 | //debugger;
19 |
20 | let options = {};
21 |
22 | let { sheetIndex, autoSwitch = true, sheetId, sheetName, sheetType } = options;
23 |
24 | var useless;
25 |
26 | if (true) {
27 | return 1;
28 | } else {
29 | return 2;
30 | }
--------------------------------------------------------------------------------
/script/dist.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @file 编译脚本
3 | * @author sigmaliu
4 | */
5 |
6 | const fs = require('fs-extra');
7 | const path = require('path');
8 | const execSync = require('child_process').execSync;
9 |
10 | const buildFunc = () => {
11 | fs.removeSync(path.resolve(__dirname, '../dist'));
12 |
13 | try {
14 | execSync('tsc -b tsconfig.build.json', {
15 | cwd: path.resolve(__dirname, '../'),
16 | });
17 | }
18 | catch(err) {
19 | console.log(err.stdout.toString());
20 | }
21 |
22 | console.log(`[alloylint] ${new Date().toLocaleTimeString()}: compiler done`);
23 | }
24 |
25 | if (require.main === module) {
26 | buildFunc();
27 | }
28 |
29 | module.exports = {
30 | buildFunc,
31 | }
32 |
33 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "airbnb-typescript/base"
4 | ],
5 | "parser": "@typescript-eslint/parser",
6 | "parserOptions": {
7 | "sourceType": "module",
8 | "ecmaVersion": 6,
9 | "ecmaFeatures": {},
10 | "project": "./tsconfig.json"
11 | },
12 | "env": {
13 | "browser": true,
14 | "mocha": true,
15 | "node": true
16 | },
17 | "globals": {},
18 | "rules": {
19 | "@typescript-eslint/ban-ts-ignore": "off",
20 | "no-console": "off",
21 | "no-restricted-syntax": "off",
22 | "no-plusplus": "off",
23 | "no-continue": "off",
24 | "@typescript-eslint/indent": ["error", 4],
25 | "max-len": ["error", 140]
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowSyntheticDefaultImports": true,
4 | "target": "es5",
5 | "noImplicitAny": true,
6 | "strictNullChecks": true,
7 | "noImplicitThis": true,
8 | "moduleResolution": "node",
9 | "outDir": "dist",
10 | "removeComments": false,
11 | "esModuleInterop": true,
12 | "forceConsistentCasingInFileNames": true,
13 | "baseUrl": ".",
14 | "declaration": true,
15 | "skipLibCheck": true,
16 | "lib": ["ES2015", "ES2016"]
17 | },
18 | "include": [
19 | "./src/**/*",
20 | // 因为当前脚本会查找 tsconfig.json 文件,要把 demo 文件夹加进去,但是编译又不要的
21 | "./demo/**/*"
22 | ],
23 | "exclude": [
24 | "node_modules"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (C) 2017-2020 AlloyTeam
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/script/demoExpect/demo3.txt:
--------------------------------------------------------------------------------
1 | 03df835c (auth3 2020-01-11 17:54:50 +0800 1) (function() {
2 | 03df835c (auth3 2020-01-11 17:54:50 +0800 2) if (false)
3 | 03df835c (auth3 2020-01-11 17:54:50 +0800 3) {
4 | 03df835c (auth3 2020-01-11 17:54:50 +0800 4) return 1;
5 | 03df835c (auth3 2020-01-11 17:54:50 +0800 5) } else {
6 | 03df835c (auth3 2020-01-11 17:54:50 +0800 6) return 2;
7 | 03df835c (auth3 2020-01-11 17:54:50 +0800 7) }
8 | 03df835c (auth3 2020-01-11 17:54:50 +0800 8) })()
9 | 03df835c (auth3 2020-01-11 17:54:50 +0800 9)
10 | 03df835c (auth3 2020-01-11 17:54:50 +0800 10) const a = {
11 | 03df835c (auth3 2020-01-11 17:54:50 +0800 11) "b": 1,
12 | 03df835c (auth3 2020-01-11 17:54:50 +0800 12) "c": 2,
13 | 03df835c (auth3 2020-01-11 17:54:50 +0800 13) d: 3
14 | 03df835c (auth3 2020-01-11 17:54:50 +0800 14) }
15 | 03df835c (auth3 2020-01-11 17:54:50 +0800 15)
16 | 03df835c (auth3 2020-01-11 17:54:50 +0800 16) const b = {
17 | 03df835c (auth3 2020-01-11 17:54:50 +0800 17) "b": 1,
18 | 03df835c (auth3 2020-01-11 17:54:50 +0800 18) "c": 2,
19 | 03df835c (auth3 2020-01-11 17:54:50 +0800 19) d: 3,
20 | 56463988 (auth1 2020-04-23 22:08:30 +0800 20) };
21 | 254bdad9 (auth1 2020-01-11 17:59:02 +0800 21)
22 | 56463988 (auth1 2020-04-23 22:08:30 +0800 22) const x = this.canvas.excel.selectData.active.xIndex;
23 | 56463988 (auth1 2020-04-23 22:08:30 +0800 23) const y = this.canvas.excel.selectData.active.yIndex;
24 | 56463988 (auth1 2020-04-23 22:08:30 +0800 24) const _isEditing = this.canvas.isEditing;
25 | 56463988 (auth1 2020-04-23 22:08:30 +0800 25) const sheetInfo = {};
26 | 254bdad9 (auth1 2020-01-11 17:59:02 +0800 26)
27 | 254bdad9 (auth1 2020-01-11 17:59:02 +0800 27) if (false) {
28 | 56463988 (auth1 2020-04-23 22:08:30 +0800 28)
29 | 03df835c (auth3 2020-01-11 17:54:50 +0800 29) }
--------------------------------------------------------------------------------
/script/demoExpect/demo2.txt:
--------------------------------------------------------------------------------
1 | 03df835c (auth3 2020-01-11 17:54:50 +0800 1) if (false) {
2 | 03df835c (auth3 2020-01-11 17:54:50 +0800 2) return 1;
3 | 03df835c (auth3 2020-01-11 17:54:50 +0800 3) }
4 | 29714b2d (auth3 2020-04-23 22:08:34 +0800 4)
5 | e5311587 (auth3 2020-04-23 22:08:35 +0800 5) return 2;
6 | 29714b2d (auth3 2020-04-23 22:08:34 +0800 6)
7 | 03df835c (auth3 2020-01-11 17:54:50 +0800 7)
8 | dad674f0 (auth2 2020-04-23 22:08:32 +0800 8) const getImgAndWidth = function (data, dprrecord, callback) {
9 | 443a1b32 (auth2 2020-04-23 22:08:33 +0800 9) const canvas = document.createElement('canvas');
10 | 443a1b32 (auth2 2020-04-23 22:08:33 +0800 10) const ctx = canvas.getContext('2d');
11 | 443a1b32 (auth2 2020-04-23 22:08:33 +0800 11) const img = new Image();
12 | e5311587 (auth3 2020-04-23 22:08:35 +0800 12) };
13 | 03df835c (auth3 2020-01-11 17:54:50 +0800 13)
14 | 56463988 (auth1 2020-04-23 22:08:30 +0800 14) sheetId = sheetId || core.window.SpreadsheetApp.spreadsheet.getActiveSheetId();
15 | 254bdad9 (auth1 2020-01-11 17:59:02 +0800 15)
16 | 08b0c05a (auth1 2020-04-23 22:08:31 +0800 16) const { rowData } = sheet.data;
17 | 254bdad9 (auth1 2020-01-11 17:59:02 +0800 17)
18 | 56463988 (auth1 2020-04-23 22:08:30 +0800 18) // debugger;
19 | 254bdad9 (auth1 2020-01-11 17:59:02 +0800 19)
20 | e5311587 (auth3 2020-04-23 22:08:35 +0800 20) const options = {};
21 | 03df835c (auth3 2020-01-11 17:54:50 +0800 21)
22 | e5311587 (auth3 2020-04-23 22:08:35 +0800 22) let {
23 | ca905a6c (auth3 2020-04-23 22:08:36 +0800 23) sheetIndex, autoSwitch = true, sheetId, sheetName, sheetType,
24 | e5311587 (auth3 2020-04-23 22:08:35 +0800 24) } = options;
25 | 03df835c (auth3 2020-01-11 17:54:50 +0800 25)
26 | e5311587 (auth3 2020-04-23 22:08:35 +0800 26) let useless;
27 | 03df835c (auth3 2020-01-11 17:54:50 +0800 27)
28 | 03df835c (auth3 2020-01-11 17:54:50 +0800 28) if (true) {
29 | 03df835c (auth3 2020-01-11 17:54:50 +0800 29) return 1;
30 | e5311587 (auth3 2020-04-23 22:08:35 +0800 30) }
31 | e5311587 (auth3 2020-04-23 22:08:35 +0800 31) return 2;
--------------------------------------------------------------------------------
/script/demoExpect/demo1.txt:
--------------------------------------------------------------------------------
1 | 56463988 (auth1 2020-04-23 22:08:30 +0800 1) document.documentElement.style.position = 'fixed';
2 | 9df4cce3 (auth1 2020-01-11 17:36:55 +0800 2)
3 | 08b0c05a (auth1 2020-04-23 22:08:31 +0800 3) const ua = navigator.userAgent.toLowerCase();
4 | 9df4cce3 (auth1 2020-01-11 17:36:55 +0800 4)
5 | a0417c15 (auth2 2020-04-23 22:08:31 +0800 5) // auth2
6 | 5eed95f7 (auth2 2020-01-11 17:43:51 +0800 6) // auth2
7 | 5eed95f7 (auth2 2020-01-11 17:43:51 +0800 7)
8 | dad674f0 (auth2 2020-04-23 22:08:32 +0800 8) const dbName = 'dbName';
9 | 5eed95f7 (auth2 2020-01-11 17:43:51 +0800 9)
10 | 5eed95f7 (auth2 2020-01-11 17:43:51 +0800 10) const storeName = 'storeName';
11 | 5eed95f7 (auth2 2020-01-11 17:43:51 +0800 11)
12 | 443a1b32 (auth2 2020-04-23 22:08:33 +0800 12) console.log(`创建[ ${dbName} / ${storeName} ]成功`);
13 | 5eed95f7 (auth2 2020-01-11 17:43:51 +0800 13)
14 | 29714b2d (auth3 2020-04-23 22:08:34 +0800 14) if (true) {
15 | 29714b2d (auth3 2020-04-23 22:08:34 +0800 15) console.log();
16 | 65ad1c5d (auth3 2020-04-23 22:08:34 +0800 16) } else { console.log(); }
17 | 96432cc1 (auth3 2020-01-18 17:42:18 +0800 17)
18 | 08b0c05a (auth1 2020-04-23 22:08:31 +0800 18) const userAgent = {
19 | 08b0c05a (auth1 2020-04-23 22:08:31 +0800 19) ua,
20 | a0417c15 (auth2 2020-04-23 22:08:31 +0800 20) };
21 | 5eed95f7 (auth2 2020-01-11 17:43:51 +0800 21)
22 | 5eed95f7 (auth2 2020-01-11 17:43:51 +0800 22) const a = 1;
23 | dad674f0 (auth2 2020-04-23 22:08:32 +0800 23) const b = 2;
24 | 5eed95f7 (auth2 2020-01-11 17:43:51 +0800 24) const c = 3;
25 | a0417c15 (auth2 2020-04-23 22:08:31 +0800 25) const d = 4;
26 | 5eed95f7 (auth2 2020-01-11 17:43:51 +0800 26)
27 | 65ad1c5d (auth3 2020-04-23 22:08:34 +0800 27) const e = {
28 | e5311587 (auth3 2020-04-23 22:08:35 +0800 28) a, b, c, d,
29 | e5311587 (auth3 2020-04-23 22:08:35 +0800 29) };
30 | 03df835c (auth3 2020-01-11 17:54:50 +0800 30)
31 | 29714b2d (auth3 2020-04-23 22:08:34 +0800 31) let f;
32 | 65ad1c5d (auth3 2020-04-23 22:08:34 +0800 32) let g;
33 | 65ad1c5d (auth3 2020-04-23 22:08:34 +0800 33) let h;
--------------------------------------------------------------------------------
/src/features/fixByAuth.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @file 运用 eslint 修复文件,但是保留最后修改人信息
3 | * @author sigmaliu
4 | */
5 |
6 | import { execSync } from 'child_process';
7 | import AutoFixAPI from '../libs/eslilnt-autofix-api';
8 | import {
9 | unknown, fileFilter, getAuthAndEmailMapping, log, globToFiles,
10 | } from '../utils/utils';
11 |
12 | let authEmailMapping: {
13 | [author: string]: string;
14 | } = {};
15 |
16 | export default function fixFilesByAuth(args: string[]) {
17 | const autoFixAPI = new AutoFixAPI();
18 |
19 | (async () => {
20 | let files = await globToFiles(args);
21 |
22 | files = fileFilter(files);
23 |
24 | while (true) {
25 | // 所有文件的报告
26 | const { report, auther } = autoFixAPI.getESLintReportByAuth(files);
27 |
28 | if (!report) {
29 | break;
30 | }
31 |
32 | if (!(report.fixableErrorCount + report.fixableWarningCount > 0)) {
33 | break;
34 | }
35 |
36 | const { results } = report;
37 |
38 | const nextTasks = results.filter((res: any) => (res.fixableErrorCount + res.fixableWarningCount) > 0);
39 | // 更新下次需要修复的文件
40 | const nextFiles = nextTasks.map((task: any) => task.filePath);
41 |
42 | if (!nextTasks.length) {
43 | break;
44 | }
45 |
46 | autoFixAPI.applyAutoFixByReport(report);
47 |
48 | let email = authEmailMapping[auther] || '';
49 |
50 | // 当前映射里没有 email 信息
51 | if (!email) {
52 | for (const nextFile of nextFiles) {
53 | authEmailMapping = Object.assign(authEmailMapping, getAuthAndEmailMapping(nextFile));
54 |
55 | email = authEmailMapping[auther];
56 |
57 | if (email) {
58 | break;
59 | }
60 | }
61 | }
62 |
63 | const fileAdd = `git add ${files.join(' ')}`;
64 |
65 | let fileCommit = `git commit -m "style: autofix for ${auther} by alloylint" --author="${auther} ${email}"`;
66 |
67 | if (auther === unknown) {
68 | fileCommit = 'git commit -m "style: autofix by alloylint"';
69 | }
70 |
71 | execSync(`${fileAdd} && ${fileCommit}`);
72 |
73 | files = nextFiles;
74 | }
75 |
76 | log('autofix done');
77 | })();
78 | }
79 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "alloylint",
3 | "version": "0.1.2",
4 | "description": "apply eslint autofix but keep last author info in git blame",
5 | "contributors": [
6 | "sigmaliu "
7 | ],
8 | "main": "./dist/index.js",
9 | "bin": {
10 | "alloylint": "./dist/index.js"
11 | },
12 | "keywords": [
13 | "alloy",
14 | "alloyteam",
15 | "front-end",
16 | "javascript",
17 | "typscript",
18 | "eslint",
19 | "eslint autofix",
20 | "autofix",
21 | "style",
22 | "blame",
23 | "history",
24 | "fix"
25 | ],
26 | "homepage": "https://github.com/AlloyTeam/AlloyLint",
27 | "repository": {
28 | "type": "git",
29 | "url": "https://github.com/AlloyTeam/AlloyLint.git"
30 | },
31 | "bugs": {
32 | "url": "https://github.com/AlloyTeam/AlloyLint/issues"
33 | },
34 | "scripts": {
35 | "dist": "node ./script/dist.js",
36 | "dev": "node ./script/dev.js",
37 | "test": "node ./script/test.js",
38 | "prepublishOnly": "npm run dist",
39 | "demo:fix": "cross-env MODE=demo ts-node ./src 'demo/**/*{js,ts}'",
40 | "demo:fixAuth": "ross-env MODE=demo ts-node ./src -a 'demo/**/*{js,ts}'",
41 | "demo:fixVue": "cross-env MODE=demo TYPE=vue ts-node ./src 'demo/**/*.vue'",
42 | "demo:fixVueAuth": "cross-env MODE=demo ts-node ./src -a 'demo/**/*.vue'"
43 | },
44 | "husky": {
45 | "hooks": {
46 | "pre-commit": "lint-staged"
47 | }
48 | },
49 | "lint-staged": {
50 | "{src,script}/**/*.{js,ts}": [
51 | "ts-node ./src",
52 | "git add"
53 | ]
54 | },
55 | "author": "sigmaliu ",
56 | "license": "MIT",
57 | "dependencies": {
58 | "@types/eslint": "^6.1.3",
59 | "@types/node": "^13.1.2",
60 | "commander": "^4.0.1",
61 | "globby": "^10.0.1",
62 | "eslint": "^6.8.0",
63 | "fs-extra": "^8.1.0"
64 | },
65 | "files": [
66 | "dist/*",
67 | "docs/*"
68 | ],
69 | "devDependencies": {
70 | "@typescript-eslint/eslint-plugin": "^2.18.0",
71 | "@typescript-eslint/parser": "^2.18.0",
72 | "babel-eslint": "^10.1.0",
73 | "cross-env": "^7.0.0",
74 | "eslint-config-airbnb-typescript": "^6.3.1",
75 | "eslint-config-alloy": "^3.5.0",
76 | "eslint-plugin-html": "^6.0.2",
77 | "eslint-plugin-import": "^2.20.2",
78 | "eslint-plugin-react": "^7.17.0",
79 | "eslint-plugin-standard": "^4.0.1",
80 | "eslint-plugin-vue": "^6.2.2",
81 | "husky": "^3.1.0",
82 | "lint-staged": "^9.5.0",
83 | "ts-node": "^8.10.1",
84 | "typescript": "^3.7.5"
85 | },
86 | "engines": {
87 | "node": ">=0.10.0",
88 | "npm": ">=1.2.10"
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/script/test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @file 测试脚本
3 | * @author sigmaliu
4 | */
5 |
6 | const fs = require('fs');
7 | const path = require('path');
8 | const execSync = require('child_process').execSync;
9 | const globby = require('globby');
10 | const {
11 | buildFunc
12 | } = require('./dist');
13 |
14 | const rootDir = path.resolve(__dirname, '../');
15 | const binPath = path.resolve(__dirname, '../dist/index.js');
16 |
17 | // 记录当前的 commit
18 | let currentCommit = execSync('git log | head -n 1', {
19 | cwd: rootDir,
20 | });
21 | currentCommit = currentCommit.toString().split(' ').slice(1).join(' ').trim();
22 |
23 | // 编译
24 | buildFunc();
25 |
26 | (async () => {
27 | let extensions = ['js', 'jsx', 'ts', 'tsx', 'mjs'];
28 | let files = await globby("demo/**/*{js,ts}", {
29 | expandDirectories: {
30 | files: ['*'],
31 | extensions,
32 | },
33 | });
34 |
35 | // 运行脚本
36 | execSync(`node ${binPath} -a "demo/**/*"`, {
37 | cwd: rootDir,
38 | });
39 |
40 | for (let file of files) {
41 | const fileName = path.basename(file);
42 | const extName = path.extname(fileName);
43 | const expectPath = path.resolve(__dirname, `./demoExpect/${fileName.slice(0, -extName.length)}.txt`);
44 |
45 | const expectBlame = fs.readFileSync(expectPath, 'utf8');
46 |
47 | const expectAuth = parseBlameToGetAuth(expectBlame);
48 |
49 | const afterFixAuth = getAuthByEachLine(file);
50 |
51 | const lines = Object.keys(afterFixAuth);
52 |
53 | const fixMatchExpect = afterFixAuth.every((value, index) => value === expectAuth[index]);
54 |
55 | const expectMatchFix = expectAuth.every((value, index) => value === afterFixAuth[index]);
56 |
57 | if (fixMatchExpect && expectMatchFix) {
58 | console.log(`${fileName} test pass`);
59 | }
60 | else {
61 | console.log(`${fileName} test fail`);
62 | }
63 | }
64 |
65 | // 恢复到之前的 commit
66 | execSync(`git reset --hard ${currentCommit}`, {
67 | cwd: process.cwd(),
68 | });
69 | })()
70 |
71 | /**
72 | * 获取文件的每行作者
73 | * @param {string} filePath
74 | */
75 | function getAuthByEachLine(filePath) {
76 | const gitBlame = execSync(`git blame ${filePath}`);
77 |
78 | return parseBlameToGetAuth(gitBlame.toString());
79 | }
80 |
81 | /**
82 | * 解析 git blame 的结果,获取每行的作者信息
83 | * @param {string} gitBlame
84 | */
85 | function parseBlameToGetAuth(gitBlame) {
86 | const lineAuthMapping = [];
87 | const lines = gitBlame.split('\n');
88 | const len = lines.length;
89 |
90 | for (let i = 0; i < len; i++) {
91 | const line = lines[i];
92 |
93 | const matches = line.match(/^\w+\s+\((.*?)\s+\d+-\d+-\d+\s+.*?(\d+)\)/);
94 |
95 | if (matches) {
96 | const auth = matches[1];
97 | const codeLine = parseInt(matches[2], 10);
98 |
99 | lineAuthMapping[codeLine] = auth;
100 | }
101 | }
102 |
103 | return lineAuthMapping;
104 | }
--------------------------------------------------------------------------------
/src/features/fixNormal.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @file 只是修复文件
3 | * @author sigmaliu
4 | */
5 |
6 | import AutoFixAPI from '../libs/eslilnt-autofix-api';
7 | import { log, globToFiles } from '../utils/utils';
8 |
9 | const cpuNums = require('os').cpus().length;
10 | const cluster = require('cluster');
11 |
12 | type TInfoCount = {
13 | errorCount: number;
14 | warningCount: number;
15 | fixableErrorCount: number;
16 | fixableWarningCount: number;
17 | };
18 |
19 | export default function fixFilesNormal(args: string[]) {
20 | const autoFixAPI = new AutoFixAPI();
21 |
22 | (async () => {
23 | if (cluster.isMaster) {
24 | const files = await globToFiles(args);
25 | const fileNums = files.length;
26 | let workerExitCount = 0;
27 | const infoCount: TInfoCount = {
28 | errorCount: 0,
29 | warningCount: 0,
30 | fixableErrorCount: 0,
31 | fixableWarningCount: 0,
32 | };
33 |
34 | log('start collecting information');
35 |
36 | // 如果才一个文件,就不用起 worker
37 | if (fileNums < 2) {
38 | autoFixAPI.applyAutoFixToFiles(files);
39 |
40 | log('autofix done');
41 | } else {
42 | const workerNums = Math.min(fileNums, cpuNums);
43 | const remain = fileNums % workerNums;
44 | const eachLen = (fileNums - remain) / workerNums;
45 |
46 | for (let i = 0; i < workerNums; i++) {
47 | const worker = cluster.fork();
48 | const fileStart = (i * eachLen) + (Math.min(i, remain));
49 | const fileEnd = fileStart + eachLen + (i < remain ? 1 : 0);
50 |
51 | worker.send({ fileParts: files.slice(fileStart, fileEnd) });
52 |
53 | worker.on('exit', () => {
54 | workerExitCount++;
55 |
56 | if (workerExitCount === workerNums) {
57 | log(JSON.stringify(infoCount, null, 4));
58 |
59 | log('autofix done');
60 |
61 | process.exit(0);
62 | }
63 | });
64 |
65 | worker.on('message', (info: {infoCount: TInfoCount}) => {
66 | if (info && info.infoCount) {
67 | const {
68 | errorCount, warningCount, fixableErrorCount, fixableWarningCount,
69 | } = info.infoCount;
70 | infoCount.errorCount += errorCount;
71 | infoCount.warningCount += warningCount;
72 | infoCount.fixableErrorCount += fixableErrorCount;
73 | infoCount.fixableWarningCount += fixableWarningCount;
74 | }
75 | });
76 | }
77 | }
78 | } else if (cluster.isWorker) {
79 | process.on('message', (task) => {
80 | if (task && task.fileParts) {
81 | autoFixAPI.applyAutoFixToFiles(task.fileParts);
82 |
83 | process.exit(0);
84 | }
85 | });
86 | }
87 | })();
88 | }
89 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # [AlloyLint](https://github.com/AlloyTeam/AlloyLint)
2 | [](https://github.com/AlloyTeam)
3 | [](https://github.com/AlloyTeam/AlloyLint/blob/master/tsconfig.json/LICENSE)
4 |
5 | ESLint 自动修复拓展插件,自动保留最后修改人
6 |
7 | ## 📎 为什么有这个插件?
8 |
9 | 基于两个痛点:
10 |
11 | - 运行 `ESLint --fix` 后,git 提交记录变成了运行修复命令的人员,为后续的修改追踪带来很大的麻烦
12 | - 在 `lint-staged` 里使用 `ESLint --fix`,如果有无法修复的问题,将会报错,阻断后面的提交流程
13 |
14 | ## 🔬 原理
15 |
16 | - 使用 `ESLint CLIEngine` 提供的 Node API 收集文件信息
17 | - 通过 `CLIEngine` 的 `fix` 参数定制输出报告
18 | - `git blame` 获取行和修改人信息映射
19 | - `git commit --author` 指定提交人员
20 |
21 |
22 | ## 📢 使用者指南
23 |
24 | ### 一、全局安装
25 |
26 | 如果要对存量代码做修复,一般全局安装
27 |
28 | ```sh
29 | npm install alloylint -g
30 | ```
31 |
32 | ### 二、本地安装
33 |
34 | 如果要对项目后面的代码做约束和修复,选择本地安装
35 |
36 | ```sh
37 | npm install alloylint -D
38 | ```
39 |
40 | ### 三、在 npm script 使用
41 |
42 | ```sh
43 | "lint": "alloylint file.js"
44 | ```
45 |
46 | 支持 glob 模式:
47 |
48 | ```sh
49 | "lint": "alloylint './src/**/*'"
50 | ```
51 |
52 | 更多 glob 模式的写法,可以参考 [minimatch](https://github.com/isaacs/minimatch#usage)
53 |
54 | ### 四、配合 lint-staged 使用
55 |
56 | 在 `lint-staged` 使用 AlloyLint 可以在提交代码的时候自动运行自动修复,尽可能地修复可修复问题。
57 |
58 | 1. 安装 `husky` 和 `lint-staged`
59 |
60 | ```sh
61 | npm install husky lint-staged -D
62 | ```
63 |
64 | 2. 添加以下内容到 `package.json` 里
65 |
66 | ```json
67 | "husky": {
68 | "hooks": {
69 | "pre-commit": "lint-staged"
70 | }
71 | },
72 | "lint-staged": {
73 | "*.{js,ts}": [
74 | "alloylint",
75 | "git add"
76 | ]
77 | }
78 | ```
79 |
80 | ### 五、保留最后修改人
81 |
82 | 添加 `-a` 或者 `--author` 参数
83 |
84 | ```sh
85 | alloylint -a "./src/**/*"
86 | ```
87 |
88 | ------
89 |
90 | ## 🍗 效果
91 |
92 | **假设原来有这样一段代码**
93 |
94 | 
95 |
96 | **在运行了 AlloyLint 后,可以看到 14 - 16 行,27 - 29 行即使有行列变动,依然能完整保留最后修改人的信息**
97 |
98 | 
99 |
100 | **git blame 工具仍维持这原修改人**
101 |
102 | 
103 |
104 | **编辑器里看到的也是经过我们处理过的最后修改人**
105 |
106 | 
107 |
108 | **git log 上看到的也都是按每个修改人去做的自动修复记录**
109 |
110 | 
111 |
112 | **在 git blame --line-porcelain 能同时看到修改人和提交人**
113 |
114 | 
115 |
116 | ## 📂 目录和文件介绍
117 |
118 | ```
119 | .
120 | ├── demo 测试文件
121 | ├── dist 编译后文件
122 | ├── docs 文档资源
123 | ├── script 编译和测试脚本
124 | ├── src 源码
125 | ```
126 |
127 | ## 🔧 贡献者指南
128 |
129 | ### 开发
130 |
131 | 1. 安装依赖
132 | ```sh
133 | npm i
134 | ```
135 |
136 |
137 | 2. 开发环境
138 |
139 | ```sh
140 | npm run dev
141 | ```
142 |
143 | ### 测试
144 |
145 | 1. 运行命令
146 |
147 | ```sh
148 | npm run demo:fixAuth
149 | ```
150 |
151 | 2. 调试自定文件目录
152 |
153 | ```sh
154 | npx ts-node ./src [file-path]
155 | ```
156 |
157 |
158 | ### 自举
159 |
160 | 1. AlloyLint 代码在 Commit 后会自动编译,并用编译后脚本格式化变动的代码文件,因此开发者不用关心这部分
161 |
162 |
163 | ## 💊 Issues
164 |
165 | 有什么好的建议或者遇到问题,请不吝到 [Issues](https://github.com/AlloyTeam/AlloyLint/issues) 提问题和讨论
166 |
--------------------------------------------------------------------------------
/src/utils/utils.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @file 工具函数
3 | * @author sigmaliu
4 | */
5 |
6 | import fs from 'fs';
7 | import path from 'path';
8 | import globby from 'globby';
9 | import { execSync } from 'child_process';
10 |
11 | export const cwd = process.cwd();
12 |
13 | export const unknown = 'alloylint-unknown';
14 |
15 | export const BOM = '\uFEFF';
16 |
17 | /**
18 | * 发生错误,打印信息,退出脚本
19 | * @param params 参数列表
20 | */
21 | export function occurError(...params: any[]) {
22 | console.error('[alloylint]', ...params);
23 |
24 | process.exit(0);
25 | }
26 |
27 | /**
28 | * 打印信息
29 | * @param params 参数列表
30 | */
31 | export function log(...params: any[]) {
32 | console.error('[alloylint]', ...params);
33 | }
34 |
35 | /**
36 | * 过滤掉一些文件
37 | * @param files 文件数组
38 | */
39 | export function fileFilter(files: string[]) {
40 | const result = [];
41 |
42 | for (const file of files) {
43 | let contents = '';
44 |
45 | try {
46 | contents = fs.readFileSync(file, 'utf8');
47 | } catch (err) {
48 | continue;
49 | }
50 |
51 | const lines = contents.split(/\r?\n/);
52 | const len = lines.length;
53 | let isMini = false;
54 |
55 | for (let j = 0; j < len; j++) {
56 | const line = lines[j];
57 |
58 | const width = line.length;
59 |
60 | // 本来就是压缩的文件
61 | if (width > 1000) {
62 | isMini = true;
63 |
64 | break;
65 | }
66 | }
67 |
68 | if (!isMini) {
69 | result.push(file);
70 | }
71 | }
72 |
73 | return result;
74 | }
75 |
76 | /**
77 | * glob 转化成文件列表
78 | */
79 | export async function globToFiles(args: string[]) :Promise {
80 | let extensions = ['js', 'jsx', 'ts', 'tsx', 'mjs', 'vue'];
81 |
82 | // @ts-ignore
83 | let files = await globby(args, {
84 | expandDirectories: {
85 | files: ['*'],
86 | extensions,
87 | },
88 | ignore: ['**/node_modules/**'],
89 | });
90 |
91 | extensions = extensions.map((e) => `.${e}`);
92 |
93 | files = files.filter((file: string) => extensions.includes(path.extname(file)));
94 |
95 | files = files.map((file: string) => path.resolve(cwd, file));
96 |
97 | files = fileFilter(files);
98 |
99 | const filesSet = new Set(files);
100 |
101 | files = Array.from(filesSet);
102 |
103 | files = files.map((file: string) => fs.realpathSync(file));
104 |
105 | if (!files.length) {
106 | occurError('No file to be fixed');
107 | }
108 |
109 | return files;
110 | }
111 |
112 | /**
113 | * 501e975e36b73b8015ce73151ea67c6e4967af5f 1 1 1
114 | * author auth1
115 | * author-mail
116 | * author-time 1578735415
117 | * author-tz +0800
118 | * committer sigmaliu
119 | * committer-mail
120 | * committer-time 1578735415
121 | * committer-tz +0800
122 | * summary auth1
123 | * filename demo/demo1.ts
124 | * document.documentElement.style['position'] = 'fixed';
125 | */
126 | export function getAuthAndEmailByBlame(gitBlame: string | string[]) {
127 | const lines = Array.isArray(gitBlame) ? gitBlame : gitBlame.split('\n');
128 | let auth = '';
129 | let email = '';
130 |
131 | for (const line of lines) {
132 | const each = line.split(' ');
133 |
134 | if (each[0] === 'author') {
135 | auth = each.slice(1).join(' ').trim();
136 | }
137 |
138 | if (each[0] === 'author-mail') {
139 | email = each.slice(1).join(' ').trim();
140 | }
141 | }
142 |
143 | return {
144 | auth,
145 | email,
146 | };
147 | }
148 |
149 | /**
150 | * 获取每个文件的所有行数对应的修改人
151 | * @param filePath
152 | */
153 | export function getAuthByEachLine(filePath: string) {
154 | const lineAuthMapping: string[] = [];
155 | const gitBlame = execSync(`git blame ${filePath}`);
156 |
157 | const lines = gitBlame.toString().split('\n');
158 | const len = lines.length;
159 |
160 | for (let i = 0; i < len; i++) {
161 | const line = lines[i];
162 |
163 | const matches = line.match(/^\w+\s+\((.*?)\s+\d+-\d+-\d+\s+.*?(\d+)\)/);
164 |
165 | if (matches) {
166 | const auth = matches[1];
167 | const codeLine = parseInt(matches[2], 10);
168 |
169 | lineAuthMapping[codeLine] = auth;
170 | }
171 | }
172 |
173 | return lineAuthMapping;
174 | }
175 |
176 | /**
177 | * 获取每个文件出现的作者对应的邮箱
178 | * @param filePath
179 | */
180 | export function getAuthAndEmailMapping(filePath: string) {
181 | const authEmailMapping: {[auth: string]: string} = {};
182 |
183 | const gitBlame = execSync(`git blame ${filePath} --line-porcelain`).toString();
184 |
185 | const lines = gitBlame.split('\n');
186 | const len = lines.length;
187 |
188 | for (let i = 0; i < len; i += 13) {
189 | const { auth, email } = getAuthAndEmailByBlame(lines.slice(i, i + 13));
190 |
191 | authEmailMapping[auth] = email;
192 | }
193 |
194 | return authEmailMapping;
195 | }
196 |
--------------------------------------------------------------------------------
/src/libs/eslilnt-autofix-api.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @file 用 eslint 修复文件的 api
3 | * @author sigmaliu
4 | */
5 |
6 | import fs from 'fs';
7 | import path from 'path';
8 | import { CLIEngine } from 'eslint';
9 | import cluster from 'cluster';
10 | import {
11 | occurError, log, cwd, getAuthByEachLine, BOM, unknown,
12 | } from '../utils/utils';
13 |
14 | interface IMessage {
15 | column: number; // - 出错的列。
16 | line: number; // - 出错的行。
17 | message: string; // - 应该被输出的信息。
18 | ruleId: string; // - 触发该消息的规则的 ID (如果 fatal 为true则此值为null)。
19 | severity: number; // - 根据你的配置,值为1或2。
20 | endColumn: number; // - 错误发生的范围的结束列 (如果不是范围,这个属性会被省略)。
21 | endLine?: number; // - 错误发生的范围的结束行 (如果不是范围,这个属性会被省略)。
22 | fix?: { // - 描述修复问
23 | range: [number, number]; // 要做替换的源码的位置,开始位置和结束位置
24 | text: string; // 要用来替换源码的内容
25 | };
26 | }
27 |
28 | const IS_DEMO = process.env.MODE === 'demo';
29 | const IS_VUE = process.env.TYPE === 'vue';
30 |
31 | export default class AutoFixAPI {
32 | /**
33 | * 修复文件
34 | * @param files 文件列表
35 | */
36 | public applyAutoFixToFiles = (files: string[]) => {
37 | try {
38 | const baseConfig = this.getBaseConfig();
39 |
40 | // @ts-ignore
41 | const cli = new CLIEngine(baseConfig);
42 |
43 | const report = cli.executeOnFiles(files);
44 |
45 | const {
46 | errorCount, warningCount, fixableErrorCount, fixableWarningCount,
47 | } = report;
48 |
49 | const infoCount = {
50 | errorCount,
51 | warningCount,
52 | fixableErrorCount,
53 | fixableWarningCount,
54 | };
55 |
56 | if (cluster.isMaster) {
57 | log(JSON.stringify(infoCount, null, 4));
58 | } else {
59 | process.send!({ infoCount });
60 | }
61 |
62 | CLIEngine.outputFixes(report);
63 | } catch (err) {
64 | occurError(err);
65 | }
66 | };
67 |
68 | /**
69 | * 生成某个最后修改人所涉及到的文件和代码的修复报告
70 | * @param files 文件列表
71 | */
72 | public getESLintReportByAuth = (files: string[]) => {
73 | // CLIEngine 的基本配置,和其他修复 API 通用的
74 | const baseConfig = this.getBaseConfig();
75 | // 统计每个文件可被修复的问题数
76 | let [fixableErrorCount, fixableWarningCount] = [0, 0];
77 | // 统计所有文件总计的可被修复的问题数
78 | let [totalErrorCount, totalWarningCount, totalFixableErrorCount, totalFixableWarningCount] = [0, 0, 0, 0];
79 | // 重写每个文件结果的 messages 数组
80 | let messages: IMessage[] = [];
81 | // 记录上次解析的文件
82 | let lastFile = '';
83 | // 当前解析的文件
84 | let currentFile = '';
85 | // 记录当前解析的文件的每一行所对应的修改人
86 | let authByEachLine: string[] = [];
87 | // 当前解析文件的源码
88 | let sourceCode = '';
89 | // 当次生成报告所要修复的最后修改人
90 | let filterAuth = '';
91 | // 当前文件是否含有 BOM 开头
92 | let bom = sourceCode.startsWith(BOM) ? BOM : '';
93 | // 移除源码的 BOM 开头
94 | sourceCode = bom ? sourceCode.slice(1) : sourceCode;
95 | // 已经解析到源码的哪个位置
96 | let lastPos = Number.NEGATIVE_INFINITY;
97 | // 对源码做修改后的内容
98 | let output = bom;
99 |
100 | /**
101 | * 根据 fix 信息对源码做修改
102 | * @param fix 要对源码做修改的信息
103 | */
104 | const attemptFix = (fix: { range: [number, number]; text: string }) => {
105 | const start = fix.range[0];
106 | const end = fix.range[1];
107 |
108 | // 非法范围信息
109 | if (lastPos >= start || start > end) {
110 | return false;
111 | }
112 |
113 | // 移除 BOM
114 | if ((start < 0 && end >= 0) || (start === 0 && fix.text.startsWith(BOM))) {
115 | output = '';
116 | }
117 |
118 | // 根据 fix 信息修复代码
119 | output += sourceCode.slice(Math.max(0, lastPos), Math.max(0, start));
120 | output += fix.text;
121 | lastPos = end;
122 |
123 | sourceCode = output;
124 | output = '';
125 |
126 | return true;
127 | };
128 |
129 | // @ts-ignore eslint 的提示是错的,fix 可以是 boolean,也可以是 () => boolean
130 | baseConfig.fix = (info: IMessage) => {
131 | messages.push(info);
132 |
133 | const { fix, line } = info;
134 |
135 | // 不可修复,直接返回
136 | if (!fix) {
137 | return false;
138 | }
139 |
140 | let shouldFix = false;
141 |
142 | // 处理的文件变化了
143 | if (lastFile !== currentFile) {
144 | lastFile = currentFile;
145 |
146 | // 生成未修复前的文件每一行的最后修改人
147 | authByEachLine = getAuthByEachLine(currentFile);
148 |
149 | // 读取文件内容
150 | sourceCode = fs.readFileSync(currentFile, 'utf8');
151 |
152 | // 更新记录当前文件的基本信息
153 | bom = sourceCode.startsWith(BOM) ? BOM : '';
154 | sourceCode = bom ? sourceCode.slice(1) : sourceCode;
155 | lastPos = Number.NEGATIVE_INFINITY;
156 | output = bom;
157 | }
158 |
159 | const [codeStart, codeEnd] = fix.range;
160 |
161 | // 获取将要被改变的源码信息
162 | const changedCode = sourceCode.slice(codeStart, codeEnd);
163 |
164 | const originLineCount = fix.text.split('\r?\n').length;
165 | const changeLineCount = changedCode.split('\r?\n').length;
166 |
167 | const changeAuth = authByEachLine[line] || unknown;
168 |
169 | // 行数有变动,更新 authByEachLine
170 | if (originLineCount !== changeLineCount) {
171 | authByEachLine.splice(line, originLineCount, ...(new Array(changeLineCount).fill(changeAuth)));
172 | }
173 |
174 | // 第一次找到了当次要做修复的作者, 要包含当次 fix 信息
175 | if (!filterAuth) {
176 | filterAuth = changeAuth;
177 |
178 | shouldFix = true;
179 | } else if (changeAuth === filterAuth) {
180 | // 符合当次修复的作者,要包含当次 fix 信息
181 | shouldFix = true;
182 | }
183 |
184 | if (shouldFix) {
185 | // 运用修复,更新文件源码
186 | shouldFix = attemptFix(fix);
187 | }
188 |
189 | // 确实被修复了,更新统计信息
190 | if (shouldFix) {
191 | if (info.severity === 2) {
192 | fixableErrorCount += 1;
193 | } else if (info.severity === 1) {
194 | fixableWarningCount += 1;
195 | }
196 | }
197 |
198 | return shouldFix;
199 | };
200 |
201 | const report: any = {
202 | results: [],
203 | };
204 |
205 | // @ts-ignore
206 | const cli = new CLIEngine(baseConfig);
207 |
208 | for (const file of files) {
209 | // 为了让 fix 函数取得当前处理的文件名的
210 | currentFile = file;
211 |
212 | // 重置统计每个文件的变量
213 | messages = [];
214 | fixableErrorCount = 0;
215 | fixableWarningCount = 0;
216 |
217 | const result = cli.executeOnFiles([file]).results[0];
218 |
219 | // executeOnFiles 生成的 messages 没有 fix 信息,所以这里替换掉
220 | result.messages = messages;
221 |
222 | // 更新每个文件的统计信息
223 | result.fixableErrorCount = fixableErrorCount;
224 | result.fixableWarningCount = fixableWarningCount;
225 |
226 | // 更新整个报告的统计信息
227 | totalErrorCount += result.errorCount;
228 | totalWarningCount += result.warningCount;
229 | totalFixableErrorCount += fixableErrorCount;
230 | totalFixableWarningCount += fixableWarningCount;
231 |
232 | report.results.push(result);
233 | }
234 |
235 | report.errorCount = totalErrorCount;
236 | report.warningCount = totalWarningCount;
237 | report.fixableErrorCount = totalFixableErrorCount;
238 | report.fixableWarningCount = totalFixableWarningCount;
239 |
240 | return {
241 | report,
242 | auther: filterAuth,
243 | };
244 | };
245 |
246 | /**
247 | * 根据 report 运用修复
248 | * @param report
249 | */
250 | applyAutoFixByReport = (report: any) => {
251 | CLIEngine.outputFixes(report);
252 | };
253 |
254 | /**
255 | * 初始化
256 | */
257 | private getBaseConfig = () => {
258 | const ignoreFile = this.getFile('.eslintignore.json', '.eslintignore');
259 | let config = this.getFile('.eslintrc.json', '.eslintrc.js');
260 |
261 | if (IS_DEMO && IS_VUE) {
262 | config = this.getFile('.eslintrc-vue.json');
263 | }
264 |
265 | if (!config) {
266 | occurError('can not find .eslintrc.json or .eslintrc.js');
267 | }
268 |
269 | let ignoreConfig = {
270 | ignore: false,
271 | ignorePath: '',
272 | };
273 |
274 | if (ignoreFile && !IS_DEMO) {
275 | ignoreConfig = {
276 | ignore: true,
277 | ignorePath: ignoreFile,
278 | };
279 | }
280 |
281 | return {
282 | ...ignoreConfig,
283 | useEslintrc: true,
284 | configFile: config,
285 | fix: true,
286 | // @ts-ignore
287 | fixTypes: ['problem', 'suggestion', 'layout'],
288 | rules: {
289 | '@typescript-eslint/prefer-string-starts-ends-with': 0, // autofix不能修复这条规则,会修出问题
290 | '@typescript-eslint/no-non-null-assertion': 0,
291 | '@typescript-eslint/no-unnecessary-type-assertion': 0,
292 | },
293 | };
294 | };
295 |
296 | /**
297 | * 备选文件
298 | * @param prefer
299 | * @param backup
300 | */
301 | private getFile = (prefer: string, backup: string = '') => {
302 | const prePath = path.resolve(cwd, prefer);
303 | const backPath = path.resolve(cwd, backup);
304 |
305 | if (fs.existsSync(prePath)) {
306 | return prePath;
307 | }
308 | if (backup && fs.existsSync(backPath)) {
309 | return backPath;
310 | }
311 |
312 | return null;
313 | };
314 | }
315 |
--------------------------------------------------------------------------------