eslint-formatter-gitlab 5.1.0 → 6.0.1
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 +43 -13
- package/lib/eslint-formatter-gitlab.js +173 -0
- package/package.json +18 -14
- package/types/eslint-formatter-gitlab.d.ts +12 -0
- package/types/eslint-formatter-gitlab.d.ts.map +1 -0
- package/index.d.ts +0 -11
- package/index.d.ts.map +0 -1
- package/index.js +0 -277
package/README.md
CHANGED
|
@@ -1,22 +1,26 @@
|
|
|
1
1
|
# ESLint Formatter for GitLab
|
|
2
2
|
|
|
3
|
+
[](https://gitlab.com/remcohaszing/eslint-formatter-gitlab/-/pipelines)
|
|
4
|
+
[](https://gitlab.com/remcohaszing/eslint-formatter-gitlab/-/pipelines)
|
|
5
|
+
[](https://github.com/sponsors/remcohaszing)
|
|
6
|
+
[](https://www.npmjs.com/package/eslint-formatter-gitlab)
|
|
7
|
+
[](https://www.npmjs.com/package/eslint-formatter-gitlab)
|
|
8
|
+
|
|
9
|
+
<img alt="" height="256" src="https://gitlab.com/remcohaszing/eslint-formatter-gitlab/-/avatar">
|
|
10
|
+
|
|
3
11
|
Show ESLint results directly in the
|
|
4
|
-
[GitLab code quality](https://docs.gitlab.com/ee/
|
|
5
|
-
results.
|
|
12
|
+
[GitLab code quality](https://docs.gitlab.com/ee/ci/testing/code_quality.html) results.
|
|
6
13
|
|
|
7
14
|
## Table of Contents
|
|
8
15
|
|
|
9
|
-
- [Requirements](#requirements)
|
|
10
16
|
- [Installation](#installation)
|
|
11
17
|
- [Usage](#usage)
|
|
18
|
+
- [Programmatic usage](#programmatic-usage)
|
|
12
19
|
- [Example](#example)
|
|
13
20
|
- [Configuration](#configuration)
|
|
21
|
+
- [Compatibility](#compatibility)
|
|
14
22
|
- [License](#license)
|
|
15
23
|
|
|
16
|
-
## Requirements
|
|
17
|
-
|
|
18
|
-
This package requires at least Node.js 18 and ESLint 5.
|
|
19
|
-
|
|
20
24
|
## Installation
|
|
21
25
|
|
|
22
26
|
Install `eslint` and `eslint-formatter-gitlab` using your package manager.
|
|
@@ -29,21 +33,35 @@ npm install --save-dev eslint eslint-formatter-gitlab
|
|
|
29
33
|
|
|
30
34
|
Define a GitLab job to run `eslint`.
|
|
31
35
|
|
|
32
|
-
|
|
36
|
+
`.gitlab-ci.yml`:
|
|
33
37
|
|
|
34
38
|
```yaml
|
|
35
39
|
eslint:
|
|
36
|
-
image: node:
|
|
40
|
+
image: node:22-alpine
|
|
37
41
|
script:
|
|
38
42
|
- npm ci
|
|
39
|
-
- npx eslint --format gitlab
|
|
43
|
+
- npx eslint --format gitlab
|
|
40
44
|
artifacts:
|
|
41
45
|
reports:
|
|
42
46
|
codequality: gl-codequality.json
|
|
43
47
|
```
|
|
44
48
|
|
|
45
49
|
The formatter automatically detects a GitLab CI environment. It detects where to output the code
|
|
46
|
-
quality report based on the GitLab configuration file.
|
|
50
|
+
quality report based on the GitLab configuration file. It also prints ESLint issues to the GitLab
|
|
51
|
+
job console with links.
|
|
52
|
+
|
|
53
|
+
### Programmatic usage
|
|
54
|
+
|
|
55
|
+
The formatter can be used programmatically using ESLint.
|
|
56
|
+
|
|
57
|
+
```js
|
|
58
|
+
import { ESLint } from 'eslint'
|
|
59
|
+
|
|
60
|
+
const eslint = new ESLint()
|
|
61
|
+
const formatter = await eslint.loadFormatter('gitlab')
|
|
62
|
+
const results = await eslint.lintFiles([])
|
|
63
|
+
const formatted = await formatter.format(results)
|
|
64
|
+
```
|
|
47
65
|
|
|
48
66
|
## Example
|
|
49
67
|
|
|
@@ -55,8 +73,20 @@ An example of the results can be seen in
|
|
|
55
73
|
|
|
56
74
|
ESLint formatters don’t take any configuration options. `eslint-formatter-gitlab` uses GitLab’s
|
|
57
75
|
[predefined environment variables](https://docs.gitlab.com/ee/ci/variables/predefined_variables.html)
|
|
58
|
-
to configure the output.
|
|
59
|
-
|
|
76
|
+
to configure the output. The following predefined environment variables are used:
|
|
77
|
+
|
|
78
|
+
- `CI_COMMIT_SHORT_SHA` to generate a link in the console output.
|
|
79
|
+
- `CI_CONFIG_PATH` to determine the GitLab CI configuration file to use. (Default: `.gitlab-ci.yml`)
|
|
80
|
+
- `CI_JOB_NAME` to determine which job configuration to read the code quality report path from.
|
|
81
|
+
- `CI_PROJECT_DIR` To determine relative paths. (Default: current working directory)
|
|
82
|
+
- `CI_PROJECT_URL` to generate a link in the console output.
|
|
83
|
+
|
|
84
|
+
In addition, the environment variable `ESLINT_CODE_QUALITY_REPORT` is used to override the location
|
|
85
|
+
to store the code quality report.
|
|
86
|
+
|
|
87
|
+
## Compatibility
|
|
88
|
+
|
|
89
|
+
This package is compatible with Node.js 20 or greater and ESLint 9 or greater.
|
|
60
90
|
|
|
61
91
|
## License
|
|
62
92
|
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import { ESLint } from 'eslint'
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
6
|
+
import { dirname, join, relative, resolve } from 'node:path'
|
|
7
|
+
import { styleText } from 'node:util'
|
|
8
|
+
|
|
9
|
+
import { toCodeClimate } from 'eslint-formatter-codeclimate'
|
|
10
|
+
import yaml from 'yaml'
|
|
11
|
+
|
|
12
|
+
/** @type {yaml.CollectionTag} */
|
|
13
|
+
const reference = {
|
|
14
|
+
tag: '!reference',
|
|
15
|
+
collection: 'seq',
|
|
16
|
+
default: false,
|
|
17
|
+
resolve() {
|
|
18
|
+
// We only allow the syntax. We don’t actually resolve the reference.
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {string} projectDir
|
|
24
|
+
* The GitLab project directory.
|
|
25
|
+
* @param {string | undefined} jobName
|
|
26
|
+
* The GitLab CI job name.
|
|
27
|
+
* @returns {Promise<string>}
|
|
28
|
+
* The output path of the code quality artifact.
|
|
29
|
+
*/
|
|
30
|
+
async function getOutputPath(projectDir, jobName) {
|
|
31
|
+
const configPath = join(projectDir, process.env.CI_CONFIG_PATH ?? '.gitlab-ci.yml')
|
|
32
|
+
// GitlabCI allows a custom configuration path which can be a URL or a path relative to another
|
|
33
|
+
// project. In these cases CI_CONFIG_PATH is empty and we'll have to require the user provide
|
|
34
|
+
// ESLINT_CODE_QUALITY_REPORT.
|
|
35
|
+
let configContents
|
|
36
|
+
try {
|
|
37
|
+
configContents = await readFile(configPath, 'utf8')
|
|
38
|
+
} catch {
|
|
39
|
+
throw new Error(
|
|
40
|
+
'Could not resolve .gitlab-ci.yml to automatically detect report artifact path.' +
|
|
41
|
+
' Please manually provide a path via the ESLINT_CODE_QUALITY_REPORT variable.'
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
const doc = yaml.parseDocument(configContents, {
|
|
45
|
+
version: '1.1',
|
|
46
|
+
customTags: [reference]
|
|
47
|
+
})
|
|
48
|
+
const path = [jobName, 'artifacts', 'reports', 'codequality']
|
|
49
|
+
const location = doc.getIn(path)
|
|
50
|
+
if (typeof location !== 'string' || !location) {
|
|
51
|
+
throw new TypeError(
|
|
52
|
+
`Expected ${path.join('.')} to be one exact path, got: ${JSON.stringify(location)}`
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
return resolve(projectDir, location)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Make a text singular or plural based on the count.
|
|
60
|
+
*
|
|
61
|
+
* @param {number} count
|
|
62
|
+
* The count of the data.
|
|
63
|
+
* @param {string} text
|
|
64
|
+
* The text to make singular or plural.
|
|
65
|
+
* @returns {string}
|
|
66
|
+
* The formatted text.
|
|
67
|
+
*/
|
|
68
|
+
function plural(count, text) {
|
|
69
|
+
return `${count} ${text}${count === 1 ? '' : 's'}`
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @param {ESLint.LintResult[]} results
|
|
74
|
+
* The ESLint report results.
|
|
75
|
+
* @param {string} projectDir
|
|
76
|
+
* The GitLab project directory.
|
|
77
|
+
* @returns {string}
|
|
78
|
+
* The ESLint messages converted to a format suitable as output in GitLab CI job logs.
|
|
79
|
+
*/
|
|
80
|
+
function gitlabConsoleFormatter(results, projectDir) {
|
|
81
|
+
// Severity labels manually padded to have equal lengths and end with spaces
|
|
82
|
+
const labelFatal = `${styleText('magenta', 'fatal')} `
|
|
83
|
+
const labelError = `${styleText('red', 'error')} `
|
|
84
|
+
const labelWarn = `${styleText('yellow', 'warn')} `
|
|
85
|
+
|
|
86
|
+
const lines = ['']
|
|
87
|
+
|
|
88
|
+
/** @type {string | undefined} */
|
|
89
|
+
let gitLabBaseURL
|
|
90
|
+
const projectUrl = process.env.CI_PROJECT_URL
|
|
91
|
+
const commitSha = process.env.CI_COMMIT_SHORT_SHA
|
|
92
|
+
if (projectUrl && commitSha) {
|
|
93
|
+
gitLabBaseURL = `${projectUrl}/-/blob/${commitSha}/`
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let fatal = 0
|
|
97
|
+
let errors = 0
|
|
98
|
+
let warnings = 0
|
|
99
|
+
let maxRuleIdLength = 0
|
|
100
|
+
let maxMsgLength = 0
|
|
101
|
+
|
|
102
|
+
for (const result of results) {
|
|
103
|
+
fatal += result.fatalErrorCount
|
|
104
|
+
errors += result.errorCount - result.fatalErrorCount
|
|
105
|
+
warnings += result.warningCount
|
|
106
|
+
for (const message of result.messages) {
|
|
107
|
+
if (message.ruleId) {
|
|
108
|
+
maxRuleIdLength = Math.max(maxRuleIdLength, message.ruleId.length)
|
|
109
|
+
}
|
|
110
|
+
maxMsgLength = Math.max(maxMsgLength, message.message.length)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
for (const result of results) {
|
|
115
|
+
const { filePath, messages } = result
|
|
116
|
+
const repoFilePath = relative(projectDir, filePath)
|
|
117
|
+
|
|
118
|
+
for (const message of messages) {
|
|
119
|
+
let line = message.fatal ? labelFatal : message.severity === 1 ? labelWarn : labelError
|
|
120
|
+
line += String(message.ruleId || '').padEnd(maxRuleIdLength + 2)
|
|
121
|
+
line += message.message.padEnd(maxMsgLength + 2)
|
|
122
|
+
|
|
123
|
+
if (gitLabBaseURL) {
|
|
124
|
+
// Create link to referenced file in GitLab
|
|
125
|
+
let anchor = `#L${message.line}`
|
|
126
|
+
if (message.endLine != null && message.endLine !== message.line) {
|
|
127
|
+
anchor += `-${message.endLine}`
|
|
128
|
+
}
|
|
129
|
+
line += styleText('blue', `${gitLabBaseURL}${repoFilePath}${anchor}`)
|
|
130
|
+
} else {
|
|
131
|
+
line += `${filePath}:${message.line}:${message.column}`
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
lines.push(line)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const total = warnings + errors + fatal
|
|
139
|
+
if (total > 0) {
|
|
140
|
+
const details = `(${fatal} fatal, ${plural(errors, 'error')}, ${plural(warnings, 'warning')})`
|
|
141
|
+
lines.push('', `${styleText('red', '✖')} ${plural(total, 'problem')} ${details}`)
|
|
142
|
+
} else {
|
|
143
|
+
lines.push(`${styleText('green', '✔')} No problems found`)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
lines.push('')
|
|
147
|
+
return lines.join('\n')
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* @param {ESLint.LintResult[]} results
|
|
152
|
+
* The ESLint report results.
|
|
153
|
+
* @param {ESLint.LintResultData} data
|
|
154
|
+
* The ESLint report result data.
|
|
155
|
+
* @returns {Promise<string>}
|
|
156
|
+
* The ESLint output to print to the console.
|
|
157
|
+
*/
|
|
158
|
+
async function eslintFormatterGitLab(results, data) {
|
|
159
|
+
let outputPath = process.env.ESLINT_CODE_QUALITY_REPORT
|
|
160
|
+
const projectDir = process.env.CI_PROJECT_DIR ?? data.cwd
|
|
161
|
+
const jobName = process.env.CI_JOB_NAME
|
|
162
|
+
if (jobName || outputPath) {
|
|
163
|
+
const issues = toCodeClimate(results, data.rulesMeta, projectDir)
|
|
164
|
+
outputPath ||= await getOutputPath(projectDir, jobName)
|
|
165
|
+
const dir = dirname(outputPath)
|
|
166
|
+
await mkdir(dir, { recursive: true })
|
|
167
|
+
await writeFile(outputPath, `${JSON.stringify(issues, null, 2)}\n`)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return gitlabConsoleFormatter(results, projectDir)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export default eslintFormatterGitLab
|
package/package.json
CHANGED
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-formatter-gitlab",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.1",
|
|
4
4
|
"description": "Show ESLint results directly in the GitLab code quality results",
|
|
5
|
+
"type": "module",
|
|
5
6
|
"author": "Remco Haszing <remcohaszing@gmail.com>",
|
|
6
7
|
"license": "MIT",
|
|
7
8
|
"homepage": "https://gitlab.com/remcohaszing/eslint-formatter-gitlab#readme",
|
|
8
9
|
"repository": "gitlab:remcohaszing/eslint-formatter-gitlab",
|
|
9
10
|
"funding": "https://github.com/sponsors/remcohaszing",
|
|
10
11
|
"bugs": "https://gitlab.com/remcohaszing/eslint-formatter-gitlab/-/issues",
|
|
11
|
-
"
|
|
12
|
+
"main": "./lib/eslint-formatter-gitlab.js",
|
|
13
|
+
"types": "./types/eslint-formatter-gitlab.d.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
"types": "./types/eslint-formatter-gitlab.d.ts",
|
|
16
|
+
"default": "./lib/eslint-formatter-gitlab.js"
|
|
17
|
+
},
|
|
12
18
|
"files": [
|
|
13
|
-
"
|
|
19
|
+
"lib",
|
|
20
|
+
"types"
|
|
14
21
|
],
|
|
15
22
|
"scripts": {
|
|
16
23
|
"prepack": "tsc --build",
|
|
17
|
-
"test": "c8 node --test --test-reporter
|
|
24
|
+
"test": "c8 node --test --test-reporter junit --test-reporter-destination=junit.xml --test-reporter spec --test-reporter-destination stdout"
|
|
18
25
|
},
|
|
19
26
|
"keywords": [
|
|
20
27
|
"eslint",
|
|
@@ -24,23 +31,20 @@
|
|
|
24
31
|
"gitlab-ci"
|
|
25
32
|
],
|
|
26
33
|
"dependencies": {
|
|
27
|
-
"
|
|
34
|
+
"eslint-formatter-codeclimate": "^1.0.0",
|
|
28
35
|
"yaml": "^2.0.0"
|
|
29
36
|
},
|
|
30
37
|
"peerDependencies": {
|
|
31
|
-
"eslint": ">=
|
|
38
|
+
"eslint": ">=9"
|
|
32
39
|
},
|
|
33
40
|
"devDependencies": {
|
|
34
|
-
"@
|
|
35
|
-
"@types/
|
|
36
|
-
"
|
|
37
|
-
"c8": "^8.0.0",
|
|
41
|
+
"@remcohaszing/eslint": "^11.0.0",
|
|
42
|
+
"@types/node": "^22.0.0",
|
|
43
|
+
"c8": "^10.0.0",
|
|
38
44
|
"codeclimate-types": "^0.3.0",
|
|
39
|
-
"eslint": "^8.0.0",
|
|
40
|
-
"eslint-config-remcohaszing": "^10.0.0",
|
|
41
45
|
"prettier": "^3.0.0",
|
|
42
|
-
"remark-cli": "^
|
|
43
|
-
"remark-preset-remcohaszing": "^
|
|
46
|
+
"remark-cli": "^12.0.0",
|
|
47
|
+
"remark-preset-remcohaszing": "^3.0.0",
|
|
44
48
|
"typescript": "^5.0.0"
|
|
45
49
|
}
|
|
46
50
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export default eslintFormatterGitLab;
|
|
2
|
+
/**
|
|
3
|
+
* @param {ESLint.LintResult[]} results
|
|
4
|
+
* The ESLint report results.
|
|
5
|
+
* @param {ESLint.LintResultData} data
|
|
6
|
+
* The ESLint report result data.
|
|
7
|
+
* @returns {Promise<string>}
|
|
8
|
+
* The ESLint output to print to the console.
|
|
9
|
+
*/
|
|
10
|
+
declare function eslintFormatterGitLab(results: ESLint.LintResult[], data: ESLint.LintResultData): Promise<string>;
|
|
11
|
+
import type { ESLint } from 'eslint';
|
|
12
|
+
//# sourceMappingURL=eslint-formatter-gitlab.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"eslint-formatter-gitlab.d.ts","sourceRoot":"","sources":["../lib/eslint-formatter-gitlab.js"],"names":[],"mappings":";AAqJA;;;;;;;GAOG;AACH,gDAPW,iBAAiB,EAAE,QAEnB,qBAAqB,GAEnB,OAAO,CAAC,MAAM,CAAC,CAgB3B;4BAzK0B,QAAQ"}
|
package/index.d.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
export = eslintFormatterGitLab;
|
|
2
|
-
/**
|
|
3
|
-
* @param {import('eslint').ESLint.LintResult[]} results
|
|
4
|
-
* The ESLint report results.
|
|
5
|
-
* @param {import('eslint').ESLint.LintResultData} data
|
|
6
|
-
* The ESLint report result data.
|
|
7
|
-
* @returns {string}
|
|
8
|
-
* The ESLint output to print to the console.
|
|
9
|
-
*/
|
|
10
|
-
declare function eslintFormatterGitLab(results: import('eslint').ESLint.LintResult[], data: import('eslint').ESLint.LintResultData): string;
|
|
11
|
-
//# sourceMappingURL=index.d.ts.map
|
package/index.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.js"],"names":[],"mappings":";AA0PA;;;;;;;GAOG;AACH,gDAPW,OAAO,QAAQ,EAAE,MAAM,CAAC,UAAU,EAAE,QAEpC,OAAO,QAAQ,EAAE,MAAM,CAAC,cAAc,GAEpC,MAAM,CAmBlB"}
|
package/index.js
DELETED
|
@@ -1,277 +0,0 @@
|
|
|
1
|
-
const { createHash } = require('node:crypto')
|
|
2
|
-
const { existsSync, lstatSync, mkdirSync, readFileSync, writeFileSync } = require('node:fs')
|
|
3
|
-
const { EOL } = require('node:os')
|
|
4
|
-
const { dirname, join, relative, resolve } = require('node:path')
|
|
5
|
-
|
|
6
|
-
const chalk = require('chalk')
|
|
7
|
-
const yaml = require('yaml')
|
|
8
|
-
|
|
9
|
-
const {
|
|
10
|
-
CI_COMMIT_SHORT_SHA,
|
|
11
|
-
CI_CONFIG_PATH = '.gitlab-ci.yml',
|
|
12
|
-
CI_JOB_NAME,
|
|
13
|
-
CI_PROJECT_DIR = process.cwd(),
|
|
14
|
-
CI_PROJECT_URL,
|
|
15
|
-
ESLINT_CODE_QUALITY_REPORT,
|
|
16
|
-
GITLAB_CI
|
|
17
|
-
} = process.env
|
|
18
|
-
|
|
19
|
-
/** @type {yaml.CollectionTag} */
|
|
20
|
-
const reference = {
|
|
21
|
-
tag: '!reference',
|
|
22
|
-
collection: 'seq',
|
|
23
|
-
default: false,
|
|
24
|
-
resolve() {
|
|
25
|
-
// We only allow the syntax. We don’t actually resolve the reference.
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* @returns {string}
|
|
31
|
-
* The output path of the code quality artifact.
|
|
32
|
-
*/
|
|
33
|
-
function getOutputPath() {
|
|
34
|
-
const configPath = join(CI_PROJECT_DIR, CI_CONFIG_PATH)
|
|
35
|
-
// GitlabCI allows a custom configuration path which can be a URL or a path relative to another
|
|
36
|
-
// project. In these cases CI_CONFIG_PATH is empty and we'll have to require the user provide
|
|
37
|
-
// ESLINT_CODE_QUALITY_REPORT.
|
|
38
|
-
if (!existsSync(configPath) || !lstatSync(configPath).isFile()) {
|
|
39
|
-
throw new Error(
|
|
40
|
-
'Could not resolve .gitlab-ci.yml to automatically detect report artifact path.' +
|
|
41
|
-
' Please manually provide a path via the ESLINT_CODE_QUALITY_REPORT variable.'
|
|
42
|
-
)
|
|
43
|
-
}
|
|
44
|
-
const doc = yaml.parseDocument(readFileSync(configPath, 'utf8'), {
|
|
45
|
-
version: '1.1',
|
|
46
|
-
customTags: [reference]
|
|
47
|
-
})
|
|
48
|
-
const path = [CI_JOB_NAME, 'artifacts', 'reports', 'codequality']
|
|
49
|
-
const location = doc.getIn(path)
|
|
50
|
-
if (typeof location !== 'string' || !location) {
|
|
51
|
-
throw new TypeError(
|
|
52
|
-
`Expected ${path.join('.')} to be one exact path, got: ${JSON.stringify(location)}`
|
|
53
|
-
)
|
|
54
|
-
}
|
|
55
|
-
return resolve(CI_PROJECT_DIR, location)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* @param {string} filePath
|
|
60
|
-
* The path to the linted file.
|
|
61
|
-
* @param {import('eslint').Linter.LintMessage} message
|
|
62
|
-
* The ESLint report message.
|
|
63
|
-
* @param {Set<string>} hashes
|
|
64
|
-
* Hashes already encountered. Used to avoid duplicate hashes
|
|
65
|
-
* @returns {string}
|
|
66
|
-
* The fingerprint for the ESLint report message.
|
|
67
|
-
*/
|
|
68
|
-
function createFingerprint(filePath, message, hashes) {
|
|
69
|
-
const md5 = createHash('md5')
|
|
70
|
-
md5.update(filePath)
|
|
71
|
-
if (message.ruleId) {
|
|
72
|
-
md5.update(message.ruleId)
|
|
73
|
-
}
|
|
74
|
-
md5.update(message.message)
|
|
75
|
-
|
|
76
|
-
// Create copy of hash since md5.digest() will finalize it, not allowing us to .update() again
|
|
77
|
-
let md5Tmp = md5.copy()
|
|
78
|
-
let hash = md5Tmp.digest('hex')
|
|
79
|
-
|
|
80
|
-
while (hashes.has(hash)) {
|
|
81
|
-
// Hash collision. This happens if we encounter the same ESLint message in one file
|
|
82
|
-
// multiple times. Keep generating new hashes until we get a unique one.
|
|
83
|
-
md5.update(hash)
|
|
84
|
-
|
|
85
|
-
md5Tmp = md5.copy()
|
|
86
|
-
hash = md5Tmp.digest('hex')
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
hashes.add(hash)
|
|
90
|
-
return hash
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* @param {import('eslint').ESLint.LintResult[]} results
|
|
95
|
-
* The ESLint report results.
|
|
96
|
-
* @param {import('eslint').ESLint.LintResultData} data
|
|
97
|
-
* The ESLint report result data.
|
|
98
|
-
* @returns {import('codeclimate-types').Issue[]}
|
|
99
|
-
* The ESLint messages in the form of a GitLab code quality report.
|
|
100
|
-
*/
|
|
101
|
-
function convert(results, data) {
|
|
102
|
-
/** @type {import('codeclimate-types').Issue[]} */
|
|
103
|
-
const messages = []
|
|
104
|
-
|
|
105
|
-
/** @type {Set<string>} */
|
|
106
|
-
const hashes = new Set()
|
|
107
|
-
|
|
108
|
-
for (const result of results) {
|
|
109
|
-
const relativePath = relative(CI_PROJECT_DIR, result.filePath)
|
|
110
|
-
|
|
111
|
-
for (const message of result.messages) {
|
|
112
|
-
/** @type {import('codeclimate-types').Issue} */
|
|
113
|
-
const issue = {
|
|
114
|
-
type: 'issue',
|
|
115
|
-
categories: ['Style'],
|
|
116
|
-
check_name: message.ruleId ?? '',
|
|
117
|
-
description: message.message,
|
|
118
|
-
severity: message.fatal ? 'critical' : message.severity === 2 ? 'major' : 'minor',
|
|
119
|
-
fingerprint: createFingerprint(relativePath, message, hashes),
|
|
120
|
-
location: {
|
|
121
|
-
path: relativePath,
|
|
122
|
-
lines: {
|
|
123
|
-
begin: message.line,
|
|
124
|
-
end: message.endLine ?? message.line
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
messages.push(issue)
|
|
129
|
-
|
|
130
|
-
if (!message.ruleId) {
|
|
131
|
-
continue
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
if (!data.rulesMeta[message.ruleId]) {
|
|
135
|
-
continue
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const { docs, type } = data.rulesMeta[message.ruleId]
|
|
139
|
-
if (type === 'problem') {
|
|
140
|
-
issue.categories.unshift('Bug Risk')
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
if (!docs) {
|
|
144
|
-
continue
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
let body = docs.description || ''
|
|
148
|
-
if (docs.url) {
|
|
149
|
-
if (body) {
|
|
150
|
-
body += '\n\n'
|
|
151
|
-
}
|
|
152
|
-
body += `[${message.ruleId}](${docs.url})`
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
if (body) {
|
|
156
|
-
issue.content = { body }
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
return messages
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Make a text singular or plural based on the count.
|
|
165
|
-
*
|
|
166
|
-
* @param {number} count
|
|
167
|
-
* The count of the data.
|
|
168
|
-
* @param {string} text
|
|
169
|
-
* The text to make singular or plural.
|
|
170
|
-
* @returns {string}
|
|
171
|
-
* The formatted text.
|
|
172
|
-
*/
|
|
173
|
-
function plural(count, text) {
|
|
174
|
-
return `${count} ${text}${count === 1 ? '' : 's'}`
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* @param {import('eslint').ESLint.LintResult[]} results
|
|
179
|
-
* The ESLint report results.
|
|
180
|
-
* @returns {string}
|
|
181
|
-
* The ESLint messages converted to a format suitable as output in GitLab CI job logs.
|
|
182
|
-
*/
|
|
183
|
-
function gitlabConsoleFormatter(results) {
|
|
184
|
-
// Severity labels manually padded to have equal lengths and end with spaces
|
|
185
|
-
const labelFatal = `${chalk.magenta('fatal')} `
|
|
186
|
-
const labelError = `${chalk.red('error')} `
|
|
187
|
-
const labelWarn = `${chalk.yellow('warn')} `
|
|
188
|
-
|
|
189
|
-
const lines = ['']
|
|
190
|
-
|
|
191
|
-
/** @type {string | undefined} */
|
|
192
|
-
let gitLabBaseURL
|
|
193
|
-
if (CI_PROJECT_URL && CI_COMMIT_SHORT_SHA) {
|
|
194
|
-
gitLabBaseURL = `${CI_PROJECT_URL}/-/blob/${CI_COMMIT_SHORT_SHA}/`
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
let fatal = 0
|
|
198
|
-
let errors = 0
|
|
199
|
-
let warnings = 0
|
|
200
|
-
let maxRuleIdLength = 0
|
|
201
|
-
let maxMsgLength = 0
|
|
202
|
-
|
|
203
|
-
for (const result of results) {
|
|
204
|
-
fatal += result.fatalErrorCount
|
|
205
|
-
errors += result.errorCount - result.fatalErrorCount
|
|
206
|
-
warnings += result.warningCount
|
|
207
|
-
for (const message of result.messages) {
|
|
208
|
-
if (message.ruleId) {
|
|
209
|
-
maxRuleIdLength = Math.max(maxRuleIdLength, message.ruleId.length)
|
|
210
|
-
}
|
|
211
|
-
maxMsgLength = Math.max(maxMsgLength, message.message.length)
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
for (const result of results) {
|
|
216
|
-
const { filePath, messages } = result
|
|
217
|
-
const repoFilePath = relative(CI_PROJECT_DIR, filePath)
|
|
218
|
-
|
|
219
|
-
for (const message of messages) {
|
|
220
|
-
let line = message.fatal ? labelFatal : message.severity === 1 ? labelWarn : labelError
|
|
221
|
-
line += String(message.ruleId || '').padEnd(maxRuleIdLength + 2)
|
|
222
|
-
line += message.message.padEnd(maxMsgLength + 2)
|
|
223
|
-
|
|
224
|
-
if (gitLabBaseURL) {
|
|
225
|
-
// Create link to referenced file in GitLab
|
|
226
|
-
let anchor = `#L${message.line}`
|
|
227
|
-
if (message.endLine != null && message.endLine !== message.line) {
|
|
228
|
-
anchor += `-${message.endLine}`
|
|
229
|
-
}
|
|
230
|
-
line += chalk.blue(`${gitLabBaseURL}${repoFilePath}${anchor}`)
|
|
231
|
-
} else {
|
|
232
|
-
line += `${filePath}:${message.line}:${message.column}`
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
lines.push(line)
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const total = warnings + errors + fatal
|
|
240
|
-
if (total > 0) {
|
|
241
|
-
const details = `(${fatal} fatal, ${plural(errors, 'error')}, ${plural(warnings, 'warning')})`
|
|
242
|
-
lines.push('', `${chalk.red('✖')} ${plural(total, 'problem')} ${details}`)
|
|
243
|
-
} else {
|
|
244
|
-
lines.push(`${chalk.green('✔')} No problems found`)
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
lines.push('')
|
|
248
|
-
return lines.join(EOL)
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
/**
|
|
252
|
-
* @param {import('eslint').ESLint.LintResult[]} results
|
|
253
|
-
* The ESLint report results.
|
|
254
|
-
* @param {import('eslint').ESLint.LintResultData} data
|
|
255
|
-
* The ESLint report result data.
|
|
256
|
-
* @returns {string}
|
|
257
|
-
* The ESLint output to print to the console.
|
|
258
|
-
*/
|
|
259
|
-
function eslintFormatterGitLab(results, data) {
|
|
260
|
-
/* c8 ignore start */
|
|
261
|
-
if (GITLAB_CI === 'true') {
|
|
262
|
-
chalk.level = 1
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
/* c8 ignore stop */
|
|
266
|
-
if (CI_JOB_NAME || ESLINT_CODE_QUALITY_REPORT) {
|
|
267
|
-
const issues = convert(results, data)
|
|
268
|
-
const outputPath = ESLINT_CODE_QUALITY_REPORT || getOutputPath()
|
|
269
|
-
const dir = dirname(outputPath)
|
|
270
|
-
mkdirSync(dir, { recursive: true })
|
|
271
|
-
writeFileSync(outputPath, JSON.stringify(issues, null, 2))
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
return gitlabConsoleFormatter(results)
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
module.exports = eslintFormatterGitLab
|