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.
Files changed (3) hide show
  1. package/README.md +15 -18
  2. package/index.js +145 -51
  3. package/package.json +18 -18
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,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 [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.
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
- - Support for the environment variable `ESLINT_FORMATTER` has been removed, console output now
53
- always uses a builtin formatter.
51
+ # License
54
52
 
55
- [gitlab code quality]: https://docs.gitlab.com/ee/user/project/merge_requests/code_quality.html
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
- const { createHash } = require('crypto');
2
- const { existsSync, lstatSync, mkdirSync, readFileSync, writeFileSync } = require('fs');
3
- const { EOL } = require('os');
4
- 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
+ */
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('js-yaml');
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 = yaml.load(readFileSync(configPath, 'utf-8'));
36
- const { artifacts } = jobs[CI_JOB_NAME];
37
- 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;
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 - The path to the linted file.
50
- * @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.
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 {object[]} results - The ESLint report results.
65
- * @returns {object[]} The ESLint messages in the form of a GitLab code quality report.
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
- // https://github.com/codeclimate/spec/blob/master/SPEC.md#data-types
73
- messages.push({
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 {object} message - The ESLint report message.
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
- * @param {object[]} results - The ESLint report results.
99
- * @returns {object} Statistics about the number of problems at various levels
100
- * and length of contained description strings.
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 calculateResultsStats(results) {
103
- const stats = { total: 0, errors: 0, warnings: 0, maxRuleIdLength: 0, maxMsgLength: 0 };
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 {object[]} results - The ESLint report results.
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
- const stats = calculateResultsStats(results);
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 ? message.ruleId : '').padEnd(stats.maxRuleIdLength + 2);
151
- line += message.message.padEnd(stats.maxMsgLength + 2);
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
- if (stats.total > 0) {
166
- const details = `(${plural(stats.errors, 'error')}, ${plural(stats.warnings, 'warning')})`;
167
- lines.push('', `${chalk.red('')} ${plural(stats.total, 'problem')} ${details}`);
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
- module.exports = (results) => {
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 data = convert(results);
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(data, null, 2));
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.0.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,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
- "js-yaml": "^4.0.0"
35
+ "yaml": "^2.0.0"
31
36
  },
32
37
  "peerDependencies": {
33
38
  "eslint": "^5 || ^6 || ^7 || ^8"
34
39
  },
35
40
  "devDependencies": {
36
- "codecov": "^3.0.0",
37
- "eslint": "^7.0.0",
38
- "eslint-config-remcohaszing": "^3.0.0",
39
- "eslint-plugin-eslint-comments": "^3.0.0",
40
- "eslint-plugin-import": "^2.0.0",
41
- "eslint-plugin-jest": "^24.0.0",
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
- "eslint-plugin-jsdoc": "^36.0.0",
44
- "eslint-plugin-markdown": "^2.0.0",
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
  }