eslint-formatter-gitlab 2.2.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.
Files changed (4) hide show
  1. package/README.md +15 -12
  2. package/index.js +227 -43
  3. package/package.json +22 -21
  4. package/CHANGELOG.md +0 -56
package/README.md CHANGED
@@ -1,10 +1,12 @@
1
1
  # ESLint Formatter for GitLab
2
2
 
3
- > Show ESLint results directly in the [GitLab code quality] results
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 GitLab Bronze or Starter 11.5 and at least ESLint 5.
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:14-alpine
27
+ image: node:18-alpine
24
28
  script:
25
29
  - npm ci
26
30
  - npx eslint --format gitlab .
@@ -34,17 +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 [Merge Request !1] of `eslint-formatter-gitlab` itself.
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. In order to still allow some way of
42
- configuration, options are passed using environment variables.
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.
43
50
 
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
- | `ESLINT_FORMATTER` | The ESLint formatter to use for the console output. This defaults to stylish, the default ESLint formatter. |
51
+ # License
48
52
 
49
- [gitlab code quality]: https://docs.gitlab.com/ee/user/project/merge_requests/code_quality.html
50
- [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,18 +1,89 @@
1
- const { createHash } = require('crypto');
2
- const { existsSync, lstatSync, mkdirSync, readFileSync, writeFileSync } = require('fs');
3
- const { dirname, join, relative, resolve } = require('path');
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
+ */
4
31
 
5
- const { CLIEngine } = require('eslint');
6
- const yaml = require('js-yaml');
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');
60
+
61
+ const chalk = require('chalk');
62
+ const yaml = require('yaml');
7
63
 
8
64
  const {
9
65
  CI_CONFIG_PATH = '.gitlab-ci.yml',
10
66
  CI_JOB_NAME,
11
67
  CI_PROJECT_DIR = process.cwd(),
68
+ CI_PROJECT_URL,
69
+ CI_COMMIT_SHORT_SHA,
12
70
  ESLINT_CODE_QUALITY_REPORT,
13
- ESLINT_FORMATTER,
71
+ GITLAB_CI,
72
+ NODE_ENV,
14
73
  } = process.env;
15
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
+
16
87
  /**
17
88
  * @returns {string} The output path of the code quality artifact.
18
89
  */
@@ -27,9 +98,11 @@ function getOutputPath() {
27
98
  ' Please manually provide a path via the ESLINT_CODE_QUALITY_REPORT variable.',
28
99
  );
29
100
  }
30
- const jobs = yaml.load(readFileSync(configPath, 'utf-8'));
31
- const { artifacts } = jobs[CI_JOB_NAME];
32
- const location = artifacts && artifacts.reports && artifacts.reports.codequality;
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;
33
106
  const msg = `Expected ${CI_JOB_NAME}.artifacts.reports.codequality to be one exact path`;
34
107
  if (!location) {
35
108
  throw new Error(`${msg}, but no value was found.`);
@@ -41,8 +114,8 @@ function getOutputPath() {
41
114
  }
42
115
 
43
116
  /**
44
- * @param {string} filePath - The path to the linted file.
45
- * @param {object} message - The ESLint report message.
117
+ * @param {string} filePath The path to the linted file.
118
+ * @param {LintMessage} message The ESLint report message.
46
119
  * @returns {string} The fingerprint for the ESLint report message.
47
120
  */
48
121
  function createFingerprint(filePath, message) {
@@ -56,44 +129,155 @@ function createFingerprint(filePath, message) {
56
129
  }
57
130
 
58
131
  /**
59
- * @param {object[]} results - The ESLint report results.
60
- * @returns {object[]} The ESLint messages in the form of a GitLab code quality report.
61
- */
62
- function convert(results) {
63
- return results.reduce(
64
- (acc, result) => [
65
- ...acc,
66
- ...result.messages.map((message) => {
67
- const relativePath = relative(CI_PROJECT_DIR, result.filePath);
68
- // https://github.com/codeclimate/spec/blob/master/SPEC.md#data-types
69
- return {
70
- description: message.message,
71
- severity: message.severity === 2 ? 'major' : 'minor',
72
- fingerprint: createFingerprint(relativePath, message),
73
- location: {
74
- path: relativePath,
75
- lines: {
76
- begin: message.line,
77
- },
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.
135
+ */
136
+ function convert(results, data) {
137
+ /** @type {CodeClimateIssue[]} */
138
+ const messages = [];
139
+ for (const result of results) {
140
+ for (const message of result.messages) {
141
+ const relativePath = relative(CI_PROJECT_DIR, result.filePath);
142
+
143
+ /** @type {CodeClimateIssue} */
144
+ const issue = {
145
+ type: 'issue',
146
+ check_name: message.ruleId ?? '',
147
+ description: message.message,
148
+ severity: message.severity === 2 ? 'major' : 'minor',
149
+ fingerprint: createFingerprint(relativePath, message),
150
+ location: {
151
+ path: relativePath,
152
+ lines: {
153
+ begin: message.line,
154
+ end: message.endLine ?? message.line,
78
155
  },
79
- };
80
- }),
81
- ],
82
- [],
83
- );
156
+ },
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);
172
+ }
173
+ }
174
+ return messages;
84
175
  }
85
176
 
86
- module.exports = (results) => {
177
+ /**
178
+ * @param {LintMessage} message The ESLint report message.
179
+ * @returns {boolean} `true` if the message is at error level, `false` if it represents a warning
180
+ */
181
+ function messageIsLevelError(message) {
182
+ return message.fatal || message.severity === 2;
183
+ }
184
+
185
+ /**
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.
191
+ */
192
+ function plural(count, text) {
193
+ return `${count} ${text}${count === 1 ? '' : 's'}`;
194
+ }
195
+
196
+ /**
197
+ * @param {LintResult[]} results The ESLint report results.
198
+ * @returns {string} The ESLint messages converted to a format
199
+ * suitable as output in GitLab CI job logs.
200
+ */
201
+ function gitlabConsoleFormatter(results) {
202
+ // Severity labels manually padded to have equal lengths and end with spaces
203
+ const labelError = `${chalk.red('error')} `;
204
+ const labelWarn = `${chalk.yellow('warn')} `;
205
+
206
+ const lines = [''];
207
+
208
+ /** @type {string | undefined} */
209
+ let gitLabBaseURL;
210
+ if (CI_PROJECT_URL && CI_COMMIT_SHORT_SHA) {
211
+ gitLabBaseURL = `${CI_PROJECT_URL}/-/blob/${CI_COMMIT_SHORT_SHA}/`;
212
+ }
213
+
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
+ }
230
+
231
+ for (const result of results) {
232
+ const { filePath, messages } = result;
233
+ const repoFilePath = relative(CI_PROJECT_DIR, filePath);
234
+
235
+ for (const message of messages) {
236
+ let line;
237
+ line = messageIsLevelError(message) ? labelError : labelWarn;
238
+ line += String(message.ruleId || '').padEnd(maxRuleIdLength + 2);
239
+ line += message.message.padEnd(maxMsgLength + 2);
240
+
241
+ if (gitLabBaseURL) {
242
+ // Create link to referenced file in GitLab
243
+ const anchor = message.line === undefined ? '' : `#L${message.line}`;
244
+ line += chalk.blue(`${gitLabBaseURL}${repoFilePath}${anchor}`);
245
+ } else {
246
+ line += `${filePath}:${message.line || 0}:${message.column || 0}`;
247
+ }
248
+
249
+ lines.push(line);
250
+ }
251
+ }
252
+
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}`);
257
+ } else {
258
+ lines.push(`${chalk.green('✔')} No problems found`);
259
+ }
260
+
261
+ lines.push('');
262
+ return lines.join(EOL);
263
+ }
264
+
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 */
271
+ if (GITLAB_CI === 'true' && NODE_ENV !== 'test') {
272
+ chalk.level = 1;
273
+ }
87
274
  if (CI_JOB_NAME || ESLINT_CODE_QUALITY_REPORT) {
88
- const data = convert(results);
275
+ const issues = convert(results, data);
89
276
  const outputPath = ESLINT_CODE_QUALITY_REPORT || getOutputPath();
90
277
  const dir = dirname(outputPath);
91
278
  mkdirSync(dir, { recursive: true });
92
- writeFileSync(outputPath, JSON.stringify(data, null, 2));
279
+ writeFileSync(outputPath, JSON.stringify(issues, null, 2));
93
280
  }
94
- let formatter = CLIEngine.getFormatter(ESLINT_FORMATTER);
95
- if (formatter === module.exports) {
96
- formatter = CLIEngine.getFormatter();
97
- }
98
- return formatter(results);
281
+
282
+ return gitlabConsoleFormatter(results);
99
283
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-formatter-gitlab",
3
- "version": "2.2.0",
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,29 +27,28 @@
25
27
  "gitlab",
26
28
  "gitlab-ci"
27
29
  ],
30
+ "engines": {
31
+ "node": ">=14.0.0"
32
+ },
28
33
  "dependencies": {
29
- "js-yaml": "^4.0.0"
34
+ "chalk": "^4.0.0",
35
+ "yaml": "^2.0.0"
30
36
  },
31
37
  "peerDependencies": {
32
- "eslint": "^5 || ^6 || ^7"
38
+ "eslint": "^5 || ^6 || ^7 || ^8"
33
39
  },
34
40
  "devDependencies": {
35
- "codecov": "^3.8.1",
36
- "eslint": "^7.18.0",
37
- "eslint-config-remcohaszing": "^3.1.0",
38
- "eslint-plugin-eslint-comments": "^3.2.0",
39
- "eslint-plugin-import": "^2.22.1",
40
- "eslint-plugin-jest": "^24.1.3",
41
- "eslint-plugin-jest-formatting": "^2.0.1",
42
- "eslint-plugin-jsdoc": "^31.4.0",
43
- "eslint-plugin-markdown": "^2.0.0-rc.1",
44
- "eslint-plugin-node": "^11.1.0",
45
- "eslint-plugin-prettier": "^3.3.1",
46
- "eslint-plugin-sort-destructure-keys": "^1.3.5",
47
- "eslint-plugin-unicorn": "^27.0.0",
48
- "jest": "^26.6.3",
49
- "jest-junit": "^12.0.0",
50
- "memfs": "^3.2.0",
51
- "prettier": "^2.2.1"
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",
47
+ "eslint-plugin-jest-formatting": "^3.0.0",
48
+ "jest": "^29.0.0",
49
+ "jest-junit": "^14.0.0",
50
+ "memfs": "^3.0.0",
51
+ "prettier": "^2.0.0",
52
+ "typescript": "^4.0.0"
52
53
  }
53
54
  }
package/CHANGELOG.md DELETED
@@ -1,56 +0,0 @@
1
- # Changelog
2
-
3
- All notable changes to this project will be documented in this file.
4
-
5
- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project
6
- adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
-
8
- ## [Unreleased]
9
-
10
- ## [2.2.0] - 2021-01-27
11
-
12
- ### Changed
13
-
14
- - Updated `ja-yaml`.
15
-
16
- ## [2.2.0] - 2020-12-12
17
-
18
- ### Added
19
-
20
- - Added severity to output.
21
-
22
- ## [2.0.0] - 2020-04-14
23
-
24
- ### Changed
25
-
26
- - Support ESLint 7.
27
-
28
- ### Fixed
29
-
30
- - Fix infinite recursion when `ESLINT_CODE_QUALITY_REPORT` refers to `eslint-formatter-gitlab`
31
- itself.
32
-
33
- ## [1.1.0] - 2019-08-08
34
-
35
- ### Changed
36
-
37
- - Support ESLint 6.
38
-
39
- ## [1.0.2] - 2018-12-13
40
-
41
- ### Fixes
42
-
43
- - Fix automated release process.
44
-
45
- ## [1.0.1] - 2018-12-13
46
-
47
- ### Added
48
-
49
- - Tests.
50
- - Link to example merge request.
51
-
52
- ## [1.0.0] - 2018-11-29
53
-
54
- ### Added
55
-
56
- - Initial release.