eslint-formatter-gitlab 3.0.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -18
- package/index.js +145 -51
- package/package.json +18 -18
package/README.md
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
# ESLint Formatter for GitLab
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Show ESLint results directly in the
|
|
4
|
+
[GitLab code quality](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality.html)
|
|
5
|
+
results
|
|
4
6
|
|
|
5
7
|
## Requirements
|
|
6
8
|
|
|
7
|
-
This requires at least
|
|
9
|
+
This package requires at least Node.js 14 and ESLint 5.
|
|
8
10
|
|
|
9
11
|
## Installation
|
|
10
12
|
|
|
@@ -14,13 +16,15 @@ Install `eslint` and `eslint-formatter-gitlab` using your package manager.
|
|
|
14
16
|
npm install --save-dev eslint eslint-formatter-gitlab
|
|
15
17
|
```
|
|
16
18
|
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
17
21
|
Define a GitLab job to run `eslint`.
|
|
18
22
|
|
|
19
23
|
_.gitlab-ci.yml_:
|
|
20
24
|
|
|
21
25
|
```yaml
|
|
22
26
|
eslint:
|
|
23
|
-
image: node:
|
|
27
|
+
image: node:18-alpine
|
|
24
28
|
script:
|
|
25
29
|
- npm ci
|
|
26
30
|
- npx eslint --format gitlab .
|
|
@@ -34,23 +38,16 @@ code quality report based on the GitLab configuration file.
|
|
|
34
38
|
|
|
35
39
|
## Example
|
|
36
40
|
|
|
37
|
-
An example of the results can be seen in
|
|
41
|
+
An example of the results can be seen in
|
|
42
|
+
[Merge Request !1](https://gitlab.com/remcohaszing/eslint-formatter-gitlab/merge_requests/1) of
|
|
43
|
+
`eslint-formatter-gitlab` itself.
|
|
38
44
|
|
|
39
45
|
## Configuration Options
|
|
40
46
|
|
|
41
|
-
ESLint formatters don’t take any configuration options.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
| Environment Variable | Description |
|
|
45
|
-
| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
46
|
-
| `ESLINT_CODE_QUALITY_REPORT` | The location to store the code quality report. By default it will detect the location of the codequality artifact defined in the GitLab CI configuration file. |
|
|
47
|
-
|
|
48
|
-
## Upgrading
|
|
49
|
-
|
|
50
|
-
### to v3
|
|
47
|
+
ESLint formatters don’t take any configuration options. `eslint-formatter-gitlab` uses GitLab
|
|
48
|
+
environment variables to configure the output. In addition, the environment variable
|
|
49
|
+
`ESLINT_CODE_QUALITY_REPORT` is used to override the location to store the code quality report.
|
|
51
50
|
|
|
52
|
-
|
|
53
|
-
always uses a builtin formatter.
|
|
51
|
+
# License
|
|
54
52
|
|
|
55
|
-
[
|
|
56
|
-
[merge request !1]: https://gitlab.com/remcohaszing/eslint-formatter-gitlab/merge_requests/1
|
|
53
|
+
[MIT](LICENSE.md) @ [Remco Haszing](https://gitlab.com/remcohaszing)
|
package/index.js
CHANGED
|
@@ -1,11 +1,65 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {import('eslint').ESLint.LintResult} LintResult
|
|
3
|
+
* @typedef {import('eslint').ESLint.LintResultData} LintResultData
|
|
4
|
+
* @typedef {import('eslint').Linter.LintMessage} LintMessage
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {object} GitLabReports
|
|
9
|
+
* @property {string} [codequality]
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {object} GitLabArtifacts
|
|
14
|
+
* @property {GitLabReports} [reports]
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @typedef {object} GitLabJob
|
|
19
|
+
* @property {GitLabArtifacts} [artifacts]
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {Record<string, GitLabJob>} GitLabCI
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @typedef {object} CodeClimateLines
|
|
28
|
+
* @property {number} begin
|
|
29
|
+
* @property {number} end
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @typedef {object} CodeClimateContents
|
|
34
|
+
* @property {string} body
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @typedef {object} CodeClimateLocation
|
|
39
|
+
* https://github.com/codeclimate/platform/blob/master/spec/analyzers/SPEC.md#locations
|
|
40
|
+
* @property {string} path
|
|
41
|
+
* @property {CodeClimateLines} lines
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @typedef {object} CodeClimateIssue
|
|
46
|
+
* https://github.com/codeclimate/platform/blob/master/spec/analyzers/SPEC.md#issues
|
|
47
|
+
* @property {'issue'} type
|
|
48
|
+
* @property {string} check_name
|
|
49
|
+
* @property {string} description
|
|
50
|
+
* @property {CodeClimateContents} [contents]
|
|
51
|
+
* @property {'info' | 'minor' | 'major' | 'critical' | 'blocker'} severity
|
|
52
|
+
* @property {string} [fingerprint]
|
|
53
|
+
* @property {CodeClimateLocation} location
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
const { createHash } = require('node:crypto');
|
|
57
|
+
const { existsSync, lstatSync, mkdirSync, readFileSync, writeFileSync } = require('node:fs');
|
|
58
|
+
const { EOL } = require('node:os');
|
|
59
|
+
const { dirname, join, relative, resolve } = require('node:path');
|
|
5
60
|
|
|
6
|
-
// eslint-disable-next-line unicorn/import-style
|
|
7
61
|
const chalk = require('chalk');
|
|
8
|
-
const yaml = require('
|
|
62
|
+
const yaml = require('yaml');
|
|
9
63
|
|
|
10
64
|
const {
|
|
11
65
|
CI_CONFIG_PATH = '.gitlab-ci.yml',
|
|
@@ -18,6 +72,18 @@ const {
|
|
|
18
72
|
NODE_ENV,
|
|
19
73
|
} = process.env;
|
|
20
74
|
|
|
75
|
+
/**
|
|
76
|
+
* @type {yaml.CollectionTag}
|
|
77
|
+
*/
|
|
78
|
+
const reference = {
|
|
79
|
+
tag: '!reference',
|
|
80
|
+
collection: 'seq',
|
|
81
|
+
default: false,
|
|
82
|
+
resolve() {
|
|
83
|
+
// We only allow the syntax. We don’t actually resolve the reference.
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
|
|
21
87
|
/**
|
|
22
88
|
* @returns {string} The output path of the code quality artifact.
|
|
23
89
|
*/
|
|
@@ -32,9 +98,11 @@ function getOutputPath() {
|
|
|
32
98
|
' Please manually provide a path via the ESLINT_CODE_QUALITY_REPORT variable.',
|
|
33
99
|
);
|
|
34
100
|
}
|
|
35
|
-
const jobs =
|
|
36
|
-
|
|
37
|
-
|
|
101
|
+
const jobs = /** @type {GitLabCI} */ (
|
|
102
|
+
yaml.parse(readFileSync(configPath, 'utf8'), { version: '1.1', customTags: [reference] })
|
|
103
|
+
);
|
|
104
|
+
const { artifacts } = jobs[/** @type {string} */ (CI_JOB_NAME)];
|
|
105
|
+
const location = artifacts?.reports?.codequality;
|
|
38
106
|
const msg = `Expected ${CI_JOB_NAME}.artifacts.reports.codequality to be one exact path`;
|
|
39
107
|
if (!location) {
|
|
40
108
|
throw new Error(`${msg}, but no value was found.`);
|
|
@@ -46,8 +114,8 @@ function getOutputPath() {
|
|
|
46
114
|
}
|
|
47
115
|
|
|
48
116
|
/**
|
|
49
|
-
* @param {string} filePath
|
|
50
|
-
* @param {
|
|
117
|
+
* @param {string} filePath The path to the linted file.
|
|
118
|
+
* @param {LintMessage} message The ESLint report message.
|
|
51
119
|
* @returns {string} The fingerprint for the ESLint report message.
|
|
52
120
|
*/
|
|
53
121
|
function createFingerprint(filePath, message) {
|
|
@@ -61,16 +129,21 @@ function createFingerprint(filePath, message) {
|
|
|
61
129
|
}
|
|
62
130
|
|
|
63
131
|
/**
|
|
64
|
-
* @param {
|
|
65
|
-
* @
|
|
132
|
+
* @param {LintResult[]} results The ESLint report results.
|
|
133
|
+
* @param {LintResultData} data The ESLint report result data.
|
|
134
|
+
* @returns {CodeClimateIssue[]} The ESLint messages in the form of a GitLab code quality report.
|
|
66
135
|
*/
|
|
67
|
-
function convert(results) {
|
|
136
|
+
function convert(results, data) {
|
|
137
|
+
/** @type {CodeClimateIssue[]} */
|
|
68
138
|
const messages = [];
|
|
69
139
|
for (const result of results) {
|
|
70
140
|
for (const message of result.messages) {
|
|
71
141
|
const relativePath = relative(CI_PROJECT_DIR, result.filePath);
|
|
72
|
-
|
|
73
|
-
|
|
142
|
+
|
|
143
|
+
/** @type {CodeClimateIssue} */
|
|
144
|
+
const issue = {
|
|
145
|
+
type: 'issue',
|
|
146
|
+
check_name: message.ruleId ?? '',
|
|
74
147
|
description: message.message,
|
|
75
148
|
severity: message.severity === 2 ? 'major' : 'minor',
|
|
76
149
|
fingerprint: createFingerprint(relativePath, message),
|
|
@@ -78,16 +151,31 @@ function convert(results) {
|
|
|
78
151
|
path: relativePath,
|
|
79
152
|
lines: {
|
|
80
153
|
begin: message.line,
|
|
154
|
+
end: message.endLine ?? message.line,
|
|
81
155
|
},
|
|
82
156
|
},
|
|
83
|
-
}
|
|
157
|
+
};
|
|
158
|
+
const docs = message.ruleId ? data.rulesMeta[message.ruleId]?.docs : undefined;
|
|
159
|
+
if (docs) {
|
|
160
|
+
let body = docs.description || '';
|
|
161
|
+
if (docs.url) {
|
|
162
|
+
if (body) {
|
|
163
|
+
body += '\n\n';
|
|
164
|
+
}
|
|
165
|
+
body += `[${message.ruleId}](${docs.url})`;
|
|
166
|
+
}
|
|
167
|
+
if (body) {
|
|
168
|
+
issue.contents = { body };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
messages.push(issue);
|
|
84
172
|
}
|
|
85
173
|
}
|
|
86
174
|
return messages;
|
|
87
175
|
}
|
|
88
176
|
|
|
89
177
|
/**
|
|
90
|
-
* @param {
|
|
178
|
+
* @param {LintMessage} message The ESLint report message.
|
|
91
179
|
* @returns {boolean} `true` if the message is at error level, `false` if it represents a warning
|
|
92
180
|
*/
|
|
93
181
|
function messageIsLevelError(message) {
|
|
@@ -95,34 +183,18 @@ function messageIsLevelError(message) {
|
|
|
95
183
|
}
|
|
96
184
|
|
|
97
185
|
/**
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
186
|
+
* Make a text singular or plural based on the count.
|
|
187
|
+
*
|
|
188
|
+
* @param {number} count The count of the data.
|
|
189
|
+
* @param {string} text The text to make singular or plural.
|
|
190
|
+
* @returns {string} The formatted text.
|
|
101
191
|
*/
|
|
102
|
-
function
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
for (const result of results) {
|
|
106
|
-
for (const message of result.messages) {
|
|
107
|
-
const isError = messageIsLevelError(message);
|
|
108
|
-
stats.errors += isError ? 1 : 0;
|
|
109
|
-
stats.warnings += isError ? 0 : 1;
|
|
110
|
-
stats.maxRuleIdLength = message.ruleId
|
|
111
|
-
? Math.max(stats.maxRuleIdLength, message.ruleId.length)
|
|
112
|
-
: stats.maxRuleIdLength;
|
|
113
|
-
stats.maxMsgLength = Math.max(stats.maxMsgLength, message.message.length);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
stats.total = stats.warnings + stats.errors;
|
|
118
|
-
|
|
119
|
-
return stats;
|
|
192
|
+
function plural(count, text) {
|
|
193
|
+
return `${count} ${text}${count === 1 ? '' : 's'}`;
|
|
120
194
|
}
|
|
121
195
|
|
|
122
|
-
const plural = (count, text) => `${count} ${text}${count === 1 ? '' : 's'}`;
|
|
123
|
-
|
|
124
196
|
/**
|
|
125
|
-
* @param {
|
|
197
|
+
* @param {LintResult[]} results The ESLint report results.
|
|
126
198
|
* @returns {string} The ESLint messages converted to a format
|
|
127
199
|
* suitable as output in GitLab CI job logs.
|
|
128
200
|
*/
|
|
@@ -133,12 +205,28 @@ function gitlabConsoleFormatter(results) {
|
|
|
133
205
|
|
|
134
206
|
const lines = [''];
|
|
135
207
|
|
|
208
|
+
/** @type {string | undefined} */
|
|
136
209
|
let gitLabBaseURL;
|
|
137
210
|
if (CI_PROJECT_URL && CI_COMMIT_SHORT_SHA) {
|
|
138
211
|
gitLabBaseURL = `${CI_PROJECT_URL}/-/blob/${CI_COMMIT_SHORT_SHA}/`;
|
|
139
212
|
}
|
|
140
213
|
|
|
141
|
-
|
|
214
|
+
let errors = 0;
|
|
215
|
+
let warnings = 0;
|
|
216
|
+
let maxRuleIdLength = 0;
|
|
217
|
+
let maxMsgLength = 0;
|
|
218
|
+
|
|
219
|
+
for (const result of results) {
|
|
220
|
+
for (const message of result.messages) {
|
|
221
|
+
const isError = messageIsLevelError(message);
|
|
222
|
+
errors += isError ? 1 : 0;
|
|
223
|
+
warnings += isError ? 0 : 1;
|
|
224
|
+
maxRuleIdLength = message.ruleId
|
|
225
|
+
? Math.max(maxRuleIdLength, message.ruleId.length)
|
|
226
|
+
: maxRuleIdLength;
|
|
227
|
+
maxMsgLength = Math.max(maxMsgLength, message.message.length);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
142
230
|
|
|
143
231
|
for (const result of results) {
|
|
144
232
|
const { filePath, messages } = result;
|
|
@@ -147,8 +235,8 @@ function gitlabConsoleFormatter(results) {
|
|
|
147
235
|
for (const message of messages) {
|
|
148
236
|
let line;
|
|
149
237
|
line = messageIsLevelError(message) ? labelError : labelWarn;
|
|
150
|
-
line += String(message.ruleId
|
|
151
|
-
line += message.message.padEnd(
|
|
238
|
+
line += String(message.ruleId || '').padEnd(maxRuleIdLength + 2);
|
|
239
|
+
line += message.message.padEnd(maxMsgLength + 2);
|
|
152
240
|
|
|
153
241
|
if (gitLabBaseURL) {
|
|
154
242
|
// Create link to referenced file in GitLab
|
|
@@ -162,9 +250,10 @@ function gitlabConsoleFormatter(results) {
|
|
|
162
250
|
}
|
|
163
251
|
}
|
|
164
252
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
253
|
+
const total = warnings + errors;
|
|
254
|
+
if (total > 0) {
|
|
255
|
+
const details = `(${plural(errors, 'error')}, ${plural(warnings, 'warning')})`;
|
|
256
|
+
lines.push('', `${chalk.red('✖')} ${plural(total, 'problem')} ${details}`);
|
|
168
257
|
} else {
|
|
169
258
|
lines.push(`${chalk.green('✔')} No problems found`);
|
|
170
259
|
}
|
|
@@ -173,16 +262,21 @@ function gitlabConsoleFormatter(results) {
|
|
|
173
262
|
return lines.join(EOL);
|
|
174
263
|
}
|
|
175
264
|
|
|
176
|
-
|
|
265
|
+
/**
|
|
266
|
+
* @param {LintResult[]} results The ESLint report results.
|
|
267
|
+
* @param {LintResultData} data The ESLint report result data.
|
|
268
|
+
*/
|
|
269
|
+
module.exports = (results, data) => {
|
|
270
|
+
/* istanbul ignore next */
|
|
177
271
|
if (GITLAB_CI === 'true' && NODE_ENV !== 'test') {
|
|
178
272
|
chalk.level = 1;
|
|
179
273
|
}
|
|
180
274
|
if (CI_JOB_NAME || ESLINT_CODE_QUALITY_REPORT) {
|
|
181
|
-
const
|
|
275
|
+
const issues = convert(results, data);
|
|
182
276
|
const outputPath = ESLINT_CODE_QUALITY_REPORT || getOutputPath();
|
|
183
277
|
const dir = dirname(outputPath);
|
|
184
278
|
mkdirSync(dir, { recursive: true });
|
|
185
|
-
writeFileSync(outputPath, JSON.stringify(
|
|
279
|
+
writeFileSync(outputPath, JSON.stringify(issues, null, 2));
|
|
186
280
|
}
|
|
187
281
|
|
|
188
282
|
return gitlabConsoleFormatter(results);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-formatter-gitlab",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"description": "Show ESLint results directly in the GitLab code quality results",
|
|
5
5
|
"author": "Remco Haszing <remcohaszing@gmail.com>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -9,14 +9,16 @@
|
|
|
9
9
|
"type": "git",
|
|
10
10
|
"url": "https://gitlab.com/remcohaszing/eslint-formatter-gitlab.git"
|
|
11
11
|
},
|
|
12
|
+
"funding": "https://github.com/sponsors/remcohaszing",
|
|
12
13
|
"bugs": {
|
|
13
14
|
"url": "https://gitlab.com/remcohaszing/eslint-formatter-gitlab.git/issues"
|
|
14
15
|
},
|
|
16
|
+
"exports": "./index.js",
|
|
15
17
|
"files": [
|
|
16
18
|
"index.js"
|
|
17
19
|
],
|
|
18
20
|
"scripts": {
|
|
19
|
-
"test": "jest"
|
|
21
|
+
"test": "jest --coverage"
|
|
20
22
|
},
|
|
21
23
|
"keywords": [
|
|
22
24
|
"eslint",
|
|
@@ -25,30 +27,28 @@
|
|
|
25
27
|
"gitlab",
|
|
26
28
|
"gitlab-ci"
|
|
27
29
|
],
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=14.0.0"
|
|
32
|
+
},
|
|
28
33
|
"dependencies": {
|
|
29
34
|
"chalk": "^4.0.0",
|
|
30
|
-
"
|
|
35
|
+
"yaml": "^2.0.0"
|
|
31
36
|
},
|
|
32
37
|
"peerDependencies": {
|
|
33
38
|
"eslint": "^5 || ^6 || ^7 || ^8"
|
|
34
39
|
},
|
|
35
40
|
"devDependencies": {
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"eslint
|
|
40
|
-
"eslint-
|
|
41
|
-
"eslint-plugin-jest": "^
|
|
41
|
+
"@types/eslint": "^8.0.0",
|
|
42
|
+
"@types/jest": "^29.0.0",
|
|
43
|
+
"@types/node": "^18.0.0",
|
|
44
|
+
"eslint": "^8.0.0",
|
|
45
|
+
"eslint-config-remcohaszing": "^7.0.0",
|
|
46
|
+
"eslint-plugin-jest": "^27.0.0",
|
|
42
47
|
"eslint-plugin-jest-formatting": "^3.0.0",
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"eslint-plugin-node": "^11.0.0",
|
|
46
|
-
"eslint-plugin-prettier": "^4.0.0",
|
|
47
|
-
"eslint-plugin-sort-destructure-keys": "^1.0.0",
|
|
48
|
-
"eslint-plugin-unicorn": "^36.0.0",
|
|
49
|
-
"jest": "^27.0.0",
|
|
50
|
-
"jest-junit": "^13.0.0",
|
|
48
|
+
"jest": "^29.0.0",
|
|
49
|
+
"jest-junit": "^14.0.0",
|
|
51
50
|
"memfs": "^3.0.0",
|
|
52
|
-
"prettier": "^2.0.0"
|
|
51
|
+
"prettier": "^2.0.0",
|
|
52
|
+
"typescript": "^4.0.0"
|
|
53
53
|
}
|
|
54
54
|
}
|