eslint-docgen 0.7.1 → 0.8.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 +12 -3
- package/package.json +18 -13
- package/src/build-docs-from-tests.js +21 -15
- package/src/build-readme.js +108 -0
- package/src/config-schema.json +2 -0
- package/src/default-config.js +3 -0
- package/src/eslint.js +20 -0
- package/src/fix.js +124 -21
- package/src/formatter.js +2 -2
- package/src/get-config.js +11 -5
- package/src/load-templates.js +10 -8
- package/src/main.js +1 -0
- package/src/naming.js +6 -4
- package/src/rule-tester.js +1 -2
- package/src/rules-with-config.js +55 -3
- package/src/templates/inConfig.ejs +1 -1
- package/src/validate-config.js +1 -2
- package/src/write-docs-from-tests.js +7 -13
package/README.md
CHANGED
|
@@ -116,6 +116,14 @@ When defined, will try to use a rule specific template instead of [`index.ejs`](
|
|
|
116
116
|
#### `globalTemplatePath`
|
|
117
117
|
When defined, templates in this path will override the global templates defined in [`src/templates`](src/templates).
|
|
118
118
|
|
|
119
|
+
#### `readmeTemplate` (default `'README.md.template'`)
|
|
120
|
+
File path containing a README template, with placeholders for a generated rules list (`<!-- rules -->`), and optionally deprecated rules (`<!-- deprecated -->`).
|
|
121
|
+
|
|
122
|
+
Invoke by running `eslint-docgen-build-readme`.
|
|
123
|
+
|
|
124
|
+
#### `readmeTarget` (default `'README.md'`)
|
|
125
|
+
File path to write generated README to, after compiling `readmeTemplate`.
|
|
126
|
+
|
|
119
127
|
#### `docLink` (default `false`)
|
|
120
128
|
Add a link to the documentation source in the "Resources" section.
|
|
121
129
|
|
|
@@ -158,7 +166,7 @@ Number of spaces to convert tabs to in code examples. Tabs in examples are alway
|
|
|
158
166
|
|
|
159
167
|
## 🔍 Rules index
|
|
160
168
|
|
|
161
|
-
To assist with building an index of your rules, for example to put in a root README, this package exports `rulesWithConfig`. The value is a Map much like the one returned by [Linter#getRules](https://eslint.org/docs/developer-guide/nodejs-api#linter-getrules) but each rule has an additional `configMap` property that describes which configs include the rule and
|
|
169
|
+
To assist with building an index of your rules, for example to put in a root README, this package exports `rulesWithConfig`. The value is a Map much like the one returned by [Linter#getRules](https://eslint.org/docs/developer-guide/nodejs-api#linter-getrules) but each rule has an additional `configMap` property that describes which configs include the rule, the severity (normalised to a string) and options used (`null` if none are used).
|
|
162
170
|
|
|
163
171
|
Note that the rule names do not include the plugin prefix.
|
|
164
172
|
|
|
@@ -170,8 +178,9 @@ require( 'eslint-docgen' ).rulesWithConfig.get( 'no-event-shorthand' );
|
|
|
170
178
|
meta: [Object],
|
|
171
179
|
create: [Function],
|
|
172
180
|
configMap: Map {
|
|
173
|
-
'deprecated-
|
|
174
|
-
'deprecated-3.
|
|
181
|
+
'deprecated-4.0' => { severity: 'error', options: null },
|
|
182
|
+
'deprecated-3.5' => { severity: 'warn', options: null },
|
|
183
|
+
'deprecated-3.3' => { severity: 'warn', options: [ { allowAjaxEvents: true } ] }
|
|
175
184
|
}
|
|
176
185
|
}
|
|
177
186
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-docgen",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "Automatically generate ESLint plugin documentation from rule metadata and test cases.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -12,33 +12,38 @@
|
|
|
12
12
|
"documentation"
|
|
13
13
|
],
|
|
14
14
|
"main": "src/index.js",
|
|
15
|
+
"bin": {
|
|
16
|
+
"eslint-docgen-build-readme": "src/build-readme.js"
|
|
17
|
+
},
|
|
15
18
|
"files": [
|
|
16
19
|
"src"
|
|
17
20
|
],
|
|
18
21
|
"scripts": {
|
|
19
|
-
"test": "eslint . --cache &&
|
|
20
|
-
"report": "
|
|
22
|
+
"test": "eslint . --cache && c8 mocha tests/",
|
|
23
|
+
"report": "c8 report --reporter=html"
|
|
21
24
|
},
|
|
22
25
|
"engines": {
|
|
23
|
-
"node": ">=
|
|
26
|
+
"node": ">=20.0.0"
|
|
24
27
|
},
|
|
25
28
|
"dependencies": {
|
|
26
29
|
"chalk": "^4.1.2",
|
|
27
|
-
"ejs": "^
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"jsonschema": "^1.4.1",
|
|
30
|
+
"ejs": "^4.0.1",
|
|
31
|
+
"import-fresh": "^3.3.1",
|
|
32
|
+
"jsonschema": "^1.5.0",
|
|
31
33
|
"merge-options": "^3.0.4",
|
|
32
|
-
"mkdirp": "^
|
|
34
|
+
"mkdirp": "^3.0.1",
|
|
33
35
|
"pkg-dir": "^5.0.0",
|
|
34
36
|
"pluralize": "^8.0.0",
|
|
35
37
|
"simple-mock": "^0.8.0",
|
|
36
38
|
"upath": "^2.0.1"
|
|
37
39
|
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"eslint": ">=8.0.0"
|
|
42
|
+
},
|
|
38
43
|
"devDependencies": {
|
|
39
|
-
"
|
|
40
|
-
"eslint
|
|
41
|
-
"
|
|
42
|
-
"
|
|
44
|
+
"c8": "^12.0.0",
|
|
45
|
+
"eslint": "^8.57.1",
|
|
46
|
+
"eslint-config-wikimedia": "^0.32.3",
|
|
47
|
+
"mocha": "^12.0.3"
|
|
43
48
|
}
|
|
44
49
|
}
|
|
@@ -21,9 +21,10 @@ const languageFromExtension = {
|
|
|
21
21
|
* @return {string} Concatenated list
|
|
22
22
|
*/
|
|
23
23
|
function listFormatter( list ) {
|
|
24
|
-
return list.reduce(
|
|
25
|
-
acc + ( i === 0 ? '' : ( ( i === arr.length - 1 ) ? ' and ' : ', ' ) ) + cur,
|
|
26
|
-
|
|
24
|
+
return list.reduce(
|
|
25
|
+
( acc, cur, i, arr ) => acc + ( i === 0 ? '' : ( ( i === arr.length - 1 ) ? ' and ' : ', ' ) ) + cur,
|
|
26
|
+
''
|
|
27
|
+
);
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
/**
|
|
@@ -79,8 +80,8 @@ async function buildDocsFromTests(
|
|
|
79
80
|
}
|
|
80
81
|
|
|
81
82
|
async function buildRuleDetails( testList, isValid, showFixes ) {
|
|
82
|
-
testList = testList.filter(
|
|
83
|
-
test.docgen === undefined ?
|
|
83
|
+
testList = testList.filter(
|
|
84
|
+
( test ) => test.docgen === undefined ?
|
|
84
85
|
!config.excludeExamplesByDefault :
|
|
85
86
|
test.docgen
|
|
86
87
|
);
|
|
@@ -100,12 +101,13 @@ async function buildDocsFromTests(
|
|
|
100
101
|
|
|
101
102
|
if ( showFixes ) {
|
|
102
103
|
// Calculate maxCodeLength for alignment
|
|
103
|
-
maxCodeLength = fixedCode.reduce(
|
|
104
|
-
code.split( '\n' ).reduce(
|
|
104
|
+
maxCodeLength = fixedCode.reduce(
|
|
105
|
+
( acc, code ) => code.split( '\n' ).reduce(
|
|
105
106
|
( lineAcc, line ) => Math.max( lineAcc, line.length ),
|
|
106
107
|
acc
|
|
107
108
|
),
|
|
108
|
-
|
|
109
|
+
0
|
|
110
|
+
);
|
|
109
111
|
|
|
110
112
|
const outputList = testList.map( ( test ) => test.output );
|
|
111
113
|
if ( config.fixCodeExamples ) {
|
|
@@ -120,7 +122,7 @@ async function buildDocsFromTests(
|
|
|
120
122
|
|
|
121
123
|
const codeSet = {};
|
|
122
124
|
let previousMultiLine = false;
|
|
123
|
-
testList.forEach(
|
|
125
|
+
testList.forEach( ( test, i ) => {
|
|
124
126
|
const filename = getFilename( test );
|
|
125
127
|
let lang = 'js';
|
|
126
128
|
// Switch to Vue if we are showing file names and its a Vue file.
|
|
@@ -260,10 +262,12 @@ async function buildDocsFromTests(
|
|
|
260
262
|
let inConfigs = [];
|
|
261
263
|
if ( configMap ) {
|
|
262
264
|
inConfigs = Array.from( configMap.keys() ).map( ( configName ) => {
|
|
263
|
-
const options = configMap.get( configName );
|
|
265
|
+
const { options, severity } = configMap.get( configName );
|
|
266
|
+
|
|
264
267
|
return {
|
|
265
268
|
config: configName,
|
|
266
|
-
options: options && Object.keys( options[ 0 ] ).length ? JSON.stringify( options ) : ''
|
|
269
|
+
options: options && Object.keys( options[ 0 ] ).length ? JSON.stringify( options ) : '',
|
|
270
|
+
severity: severity
|
|
267
271
|
};
|
|
268
272
|
} );
|
|
269
273
|
}
|
|
@@ -271,11 +275,12 @@ async function buildDocsFromTests(
|
|
|
271
275
|
const invalid = await buildRuleDetails( tests.invalid, false );
|
|
272
276
|
const valid = await buildRuleDetails( tests.valid, true );
|
|
273
277
|
|
|
274
|
-
const validInvalid = invalid.concat( valid ).sort(
|
|
275
|
-
|
|
278
|
+
const validInvalid = invalid.concat( valid ).sort(
|
|
279
|
+
// sort by key, then value
|
|
280
|
+
( a, b ) => a.key === b.key ?
|
|
276
281
|
( a.valid < b.valid ? -1 : 1 ) :
|
|
277
|
-
( a.key < b.key ? -1 : 1 )
|
|
278
|
-
|
|
282
|
+
( a.key < b.key ? -1 : 1 )
|
|
283
|
+
);
|
|
279
284
|
|
|
280
285
|
let fixed = [];
|
|
281
286
|
if ( ruleMeta.fixable && config.showFixExamples ) {
|
|
@@ -323,6 +328,7 @@ async function buildDocsFromTests(
|
|
|
323
328
|
const fs = require( 'fs' );
|
|
324
329
|
const packagePath = require( './package-path' );
|
|
325
330
|
const ruleTemplatePath = packagePath( config.ruleTemplatePath.replace( '{name}', name ) );
|
|
331
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename
|
|
326
332
|
if ( fs.existsSync( ruleTemplatePath ) ) {
|
|
327
333
|
index = loadRuleTemplate( ruleTemplatePath );
|
|
328
334
|
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const configRename = ( name ) => name.replace( 'deprecated-', '' );
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Build the README contents from a template
|
|
8
|
+
*
|
|
9
|
+
* @param {Object} config Resolved eslint-docgen config
|
|
10
|
+
* @param {Map} rulesWithConfig Map of rule names to rule data (meta and configMap)
|
|
11
|
+
* @param {string} template README template containing `<!-- rules -->` and
|
|
12
|
+
* `<!-- deprecated -->` placeholders
|
|
13
|
+
* @return {string} README contents
|
|
14
|
+
*/
|
|
15
|
+
function buildReadme( config, rulesWithConfig, template ) {
|
|
16
|
+
function getRules( deprecated ) {
|
|
17
|
+
return Array.from( rulesWithConfig.keys() ).map( ( rule ) => {
|
|
18
|
+
const fullRule = config.pluginName + '/' + rule;
|
|
19
|
+
const ruleData = rulesWithConfig.get( rule );
|
|
20
|
+
const docs = ruleData.meta.docs;
|
|
21
|
+
const configs = Array.from( ruleData.configMap.keys() ).map( ( name ) => {
|
|
22
|
+
const { options, severity } = ruleData.configMap.get( name );
|
|
23
|
+
// TODO: Create util to compare options to defaults
|
|
24
|
+
return {
|
|
25
|
+
name: configRename( name ),
|
|
26
|
+
hasOptions: !!( options && Object.keys( options[ 0 ] ).length ),
|
|
27
|
+
severity
|
|
28
|
+
};
|
|
29
|
+
} );
|
|
30
|
+
// Sort configs by name - sort numerical values by version number
|
|
31
|
+
configs.sort( ( a, b ) => {
|
|
32
|
+
if ( a.name.includes( '.' ) === -1 ) {
|
|
33
|
+
return a.name.localeCompare( b.name );
|
|
34
|
+
}
|
|
35
|
+
const aVersion = a.name.split( '.' ).map( ( n ) => parseInt( n, 10 ) );
|
|
36
|
+
const bVersion = b.name.split( '.' ).map( ( n ) => parseInt( n, 10 ) );
|
|
37
|
+
for ( let i = 0; i < Math.max( aVersion.length, bVersion.length ); i++ ) {
|
|
38
|
+
if ( aVersion[ i ] !== bVersion[ i ] ) {
|
|
39
|
+
return ( aVersion[ i ] || 0 ) - ( bVersion[ i ] || 0 );
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return 0;
|
|
43
|
+
} );
|
|
44
|
+
if ( !!docs.deprecated === deprecated ) {
|
|
45
|
+
return '* [`' + fullRule + '`](' + config.docPath.replace( '{name}', rule ) + ')' +
|
|
46
|
+
(
|
|
47
|
+
ruleData.meta.schema.length ?
|
|
48
|
+
' ⚙️' :
|
|
49
|
+
''
|
|
50
|
+
) +
|
|
51
|
+
(
|
|
52
|
+
ruleData.meta.fixable ?
|
|
53
|
+
' 🔧' :
|
|
54
|
+
''
|
|
55
|
+
) +
|
|
56
|
+
(
|
|
57
|
+
docs.replacedBy ?
|
|
58
|
+
' (use [`' + config.pluginName + '/' + docs.replacedBy + '`](' + config.docPath.replace( '{name}', docs.replacedBy ) + '))' :
|
|
59
|
+
''
|
|
60
|
+
) +
|
|
61
|
+
(
|
|
62
|
+
ruleData.configMap.size ?
|
|
63
|
+
' ' +
|
|
64
|
+
Array.from( configs ).map(
|
|
65
|
+
( c ) => ( c.severity === 'error' ? '**' : '' ) +
|
|
66
|
+
'`' + c.name + ( c.hasOptions ? '†' : '' ) + '`' +
|
|
67
|
+
( c.severity === 'error' ? '**' : '' ) ).join( ', ' ) :
|
|
68
|
+
''
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
} ).filter( ( rule ) => rule ).join( '\n' );
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return `<!-- This file is built by eslint-docgen-build-readme. Do not edit it directly; edit ${ config.readmeTemplate } instead. -->\n` +
|
|
76
|
+
template
|
|
77
|
+
.replace( '<!-- rules -->', getRules( false ) )
|
|
78
|
+
.replace( '<!-- deprecated -->', getRules( true ) );
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/* istanbul ignore next */
|
|
82
|
+
function main() {
|
|
83
|
+
const fs = require( 'fs' );
|
|
84
|
+
const rulesWithConfig = require( './rules-with-config' );
|
|
85
|
+
const getConfig = require( './get-config' );
|
|
86
|
+
|
|
87
|
+
const config = getConfig();
|
|
88
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename
|
|
89
|
+
const template = fs.readFileSync( config.readmeTemplate, { encoding: 'UTF8' } );
|
|
90
|
+
|
|
91
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename
|
|
92
|
+
fs.writeFile(
|
|
93
|
+
config.readmeTarget,
|
|
94
|
+
buildReadme( config, rulesWithConfig, template ),
|
|
95
|
+
( err ) => {
|
|
96
|
+
if ( err ) {
|
|
97
|
+
throw err;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/* istanbul ignore next */
|
|
104
|
+
if ( require.main === module ) {
|
|
105
|
+
main();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
module.exports = buildReadme;
|
package/src/config-schema.json
CHANGED
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
"type": "null",
|
|
22
22
|
"message": "must be renamed to \"globalTemplatePath\""
|
|
23
23
|
},
|
|
24
|
+
"readmeTemplate": { "type": "string" },
|
|
25
|
+
"readmeTarget": { "type": "string" },
|
|
24
26
|
"docLink": { "type": "boolean" },
|
|
25
27
|
"ruleLink": { "type": "boolean" },
|
|
26
28
|
"testLink": { "type": "boolean" },
|
package/src/default-config.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const packagePath = require( './package-path' );
|
|
4
|
+
// eslint-disable-next-line security/detect-non-literal-require
|
|
4
5
|
const packageName = require( packagePath( './package' ) ).name;
|
|
5
6
|
const naming = require( './naming' );
|
|
6
7
|
const pluginName = naming.getShorthandName( packageName, 'eslint-plugin' );
|
|
@@ -17,6 +18,8 @@ module.exports = {
|
|
|
17
18
|
testPath: null,
|
|
18
19
|
ruleTemplatePath: null,
|
|
19
20
|
globalTemplatePath: null,
|
|
21
|
+
readmeTemplate: 'README.md.template',
|
|
22
|
+
readmeTarget: 'README.md',
|
|
20
23
|
docLink: false,
|
|
21
24
|
ruleLink: true,
|
|
22
25
|
testLink: true,
|
package/src/eslint.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const packagePath = require( './package-path' );
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Load a module from the plugin's copy of ESLint
|
|
7
|
+
*
|
|
8
|
+
* Docs should be built with the same version of ESLint, and the same config
|
|
9
|
+
* system, as the plugin's tests are run with.
|
|
10
|
+
*
|
|
11
|
+
* @param {string} [subpath] Module path within the ESLint package, e.g. 'use-at-your-own-risk'
|
|
12
|
+
* @return {Object} Module exports
|
|
13
|
+
*/
|
|
14
|
+
function requireESLint( subpath ) {
|
|
15
|
+
const name = subpath ? 'eslint/' + subpath : 'eslint';
|
|
16
|
+
// eslint-disable-next-line security/detect-non-literal-require
|
|
17
|
+
return require( require.resolve( name, { paths: [ packagePath( '.' ) ] } ) );
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module.exports = requireESLint;
|
package/src/fix.js
CHANGED
|
@@ -1,47 +1,150 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
const
|
|
3
|
+
const requireESLint = require( './eslint' );
|
|
4
|
+
const { Linter, ESLint, loadESLint } = requireESLint();
|
|
5
|
+
const { builtinRules } = requireESLint( 'use-at-your-own-risk' );
|
|
6
6
|
const mergeOptions = require( 'merge-options' );
|
|
7
|
-
// eslint-disable-next-line node/no-missing-require
|
|
8
|
-
const { builtinRules } = require( 'eslint/use-at-your-own-risk' );
|
|
9
7
|
|
|
8
|
+
/**
|
|
9
|
+
* ESLint 9+'s RuleTester only supports flat config, so test cases and the
|
|
10
|
+
* tester config are in flat config format.
|
|
11
|
+
*/
|
|
12
|
+
const isFlatRuleTester = parseInt( Linter.version, 10 ) >= 9;
|
|
13
|
+
|
|
14
|
+
const linter = new Linter( { configType: isFlatRuleTester ? 'flat' : 'eslintrc' } );
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Get the plugin's ESLint config for its main file, normalized to a format
|
|
18
|
+
* which doesn't depend on the config system used
|
|
19
|
+
*
|
|
20
|
+
* Only fixable core rules are kept, as plugin rules would need the plugin to
|
|
21
|
+
* be loaded, and only fixes are needed.
|
|
22
|
+
*
|
|
23
|
+
* @return {Promise<Object>} Normalized config
|
|
24
|
+
*/
|
|
10
25
|
async function getConfig() {
|
|
11
|
-
|
|
26
|
+
// loadESLint (ESLint 8.57+) returns the class for the config system used
|
|
27
|
+
// by the plugin, i.e. depending on whether it has an eslint.config.js file
|
|
28
|
+
const ESLintClass = loadESLint ? await loadESLint() : ESLint;
|
|
29
|
+
const eslint = new ESLintClass();
|
|
30
|
+
const config = await eslint.calculateConfigForFile( require( './main' ) ) || {};
|
|
31
|
+
const isFlatConfig = ESLintClass.configType === 'flat';
|
|
32
|
+
const languageOptions = ( isFlatConfig ? config.languageOptions : config.parserOptions ) || {};
|
|
12
33
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
Object.keys( config.rules ).forEach( function ( ruleName ) {
|
|
34
|
+
const rules = {};
|
|
35
|
+
Object.keys( config.rules || {} ).forEach( ( ruleName ) => {
|
|
16
36
|
if ( builtinRules.has( ruleName ) ) {
|
|
17
37
|
const rule = builtinRules.get( ruleName );
|
|
18
38
|
if ( rule.meta.fixable ) {
|
|
19
|
-
|
|
39
|
+
rules[ ruleName ] = config.rules[ ruleName ];
|
|
20
40
|
}
|
|
21
41
|
}
|
|
22
42
|
} );
|
|
23
|
-
config.rules = fixableRules;
|
|
24
43
|
|
|
25
|
-
return
|
|
44
|
+
return {
|
|
45
|
+
rules,
|
|
46
|
+
settings: config.settings,
|
|
47
|
+
// eslintrc only, can set the ecmaVersion, e.g. "es2017"
|
|
48
|
+
env: config.env,
|
|
49
|
+
// Parser name/path (eslintrc) or object (flat config)
|
|
50
|
+
parser: isFlatConfig ? languageOptions.parser : config.parser,
|
|
51
|
+
parserOptions: isFlatConfig ? languageOptions.parserOptions : config.parserOptions,
|
|
52
|
+
ecmaVersion: languageOptions.ecmaVersion,
|
|
53
|
+
sourceType: languageOptions.sourceType
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get a flat config array, for linting with ESLint 9+
|
|
59
|
+
*
|
|
60
|
+
* @param {Object} config Normalized config, from #getConfig
|
|
61
|
+
* @param {Object|Object[]} testerConfig RuleTester config
|
|
62
|
+
* @return {Object[]} Flat config array
|
|
63
|
+
*/
|
|
64
|
+
function getFlatConfig( config, testerConfig ) {
|
|
65
|
+
const baseConfig = {
|
|
66
|
+
rules: config.rules,
|
|
67
|
+
languageOptions: {}
|
|
68
|
+
};
|
|
69
|
+
if ( config.settings ) {
|
|
70
|
+
baseConfig.settings = config.settings;
|
|
71
|
+
}
|
|
72
|
+
[ 'ecmaVersion', 'sourceType', 'parserOptions' ].forEach( ( key ) => {
|
|
73
|
+
if ( config[ key ] !== undefined ) {
|
|
74
|
+
baseConfig.languageOptions[ key ] = config[ key ];
|
|
75
|
+
}
|
|
76
|
+
} );
|
|
77
|
+
if ( config.parser ) {
|
|
78
|
+
baseConfig.languageOptions.parser = typeof config.parser === 'string' ?
|
|
79
|
+
// eslint-disable-next-line security/detect-non-literal-require
|
|
80
|
+
require( config.parser ) :
|
|
81
|
+
config.parser;
|
|
82
|
+
}
|
|
83
|
+
// The RuleTester config may include rules which aren't loaded here,
|
|
84
|
+
// e.g. "rule-tester/validate-ast", and rules aren't needed for fixing.
|
|
85
|
+
const testerConfigs = [].concat( testerConfig || [] ).map( ( testerConfigItem ) => {
|
|
86
|
+
const copy = Object.assign( {}, testerConfigItem );
|
|
87
|
+
delete copy.rules;
|
|
88
|
+
return copy;
|
|
89
|
+
} );
|
|
90
|
+
return [ baseConfig, ...testerConfigs ];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Get an eslintrc config, for linting with ESLint 8
|
|
95
|
+
*
|
|
96
|
+
* @param {Object} config Normalized config, from #getConfig
|
|
97
|
+
* @param {Object} testerConfig RuleTester config
|
|
98
|
+
* @return {Object} eslintrc config
|
|
99
|
+
*/
|
|
100
|
+
function getEslintrcConfig( config, testerConfig ) {
|
|
101
|
+
const baseConfig = {
|
|
102
|
+
rules: config.rules,
|
|
103
|
+
parserOptions: Object.assign( {}, config.parserOptions )
|
|
104
|
+
};
|
|
105
|
+
if ( config.settings ) {
|
|
106
|
+
baseConfig.settings = config.settings;
|
|
107
|
+
}
|
|
108
|
+
if ( config.env ) {
|
|
109
|
+
baseConfig.env = config.env;
|
|
110
|
+
}
|
|
111
|
+
[ 'ecmaVersion', 'sourceType' ].forEach( ( key ) => {
|
|
112
|
+
if ( config[ key ] !== undefined ) {
|
|
113
|
+
baseConfig.parserOptions[ key ] = config[ key ];
|
|
114
|
+
}
|
|
115
|
+
} );
|
|
116
|
+
if ( config.parser ) {
|
|
117
|
+
baseConfig.parser = config.parser;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const mergedConfig = mergeOptions( baseConfig, testerConfig );
|
|
121
|
+
|
|
122
|
+
if ( mergedConfig.parser && typeof mergedConfig.parser !== 'string' ) {
|
|
123
|
+
// A parser object, from an ESLint 8.57 flat config
|
|
124
|
+
linter.defineParser( 'eslint-docgen-parser', mergedConfig.parser );
|
|
125
|
+
mergedConfig.parser = 'eslint-docgen-parser';
|
|
126
|
+
} else if ( typeof mergedConfig.parser === 'string' ) {
|
|
127
|
+
// eslint-disable-next-line security/detect-non-literal-require
|
|
128
|
+
linter.defineParser( mergedConfig.parser, require( mergedConfig.parser ) );
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return mergedConfig;
|
|
26
132
|
}
|
|
27
133
|
|
|
28
134
|
/**
|
|
29
135
|
* Lint and fix some code
|
|
30
136
|
*
|
|
31
137
|
* @param {string} code Code
|
|
32
|
-
* @param {Object} testerConfig
|
|
138
|
+
* @param {Object|Object[]} testerConfig RuleTester config
|
|
33
139
|
* @return {string} Fixed code
|
|
34
140
|
*/
|
|
35
141
|
async function lintFix( code, testerConfig ) {
|
|
36
|
-
const
|
|
142
|
+
const config = await getConfig();
|
|
37
143
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
linter.defineParser( mergedConfig.parser, require( mergedConfig.parser ) );
|
|
42
|
-
}
|
|
144
|
+
const result = isFlatRuleTester ?
|
|
145
|
+
linter.verifyAndFix( code, getFlatConfig( config, testerConfig ), { filename: 'example.js' } ) :
|
|
146
|
+
linter.verifyAndFix( code, getEslintrcConfig( config, testerConfig ) );
|
|
43
147
|
|
|
44
|
-
const result = linter.verifyAndFix( code, mergedConfig );
|
|
45
148
|
const err = result.messages.find( ( message ) => message.fatal );
|
|
46
149
|
// TODO
|
|
47
150
|
// istanbul ignore next
|
|
@@ -58,7 +161,7 @@ async function lintFix( code, testerConfig ) {
|
|
|
58
161
|
* Concatenates the code snippets into one code block to improve performance
|
|
59
162
|
*
|
|
60
163
|
* @param {string[]} codeList Code list
|
|
61
|
-
* @param {Object} testerConfig
|
|
164
|
+
* @param {Object|Object[]} testerConfig RuleTester config
|
|
62
165
|
* @return {string[]} Fixed code list
|
|
63
166
|
*/
|
|
64
167
|
async function batchLintFix( codeList, testerConfig ) {
|
package/src/formatter.js
CHANGED
|
@@ -20,8 +20,8 @@ function labelSuffix( label ) {
|
|
|
20
20
|
* @return {string}
|
|
21
21
|
*/
|
|
22
22
|
function indentAfterFirst( text, indent ) {
|
|
23
|
-
return text.split( '\n' ).map(
|
|
24
|
-
i ? ' '.repeat( indent ) + line : line
|
|
23
|
+
return text.split( '\n' ).map(
|
|
24
|
+
( line, i ) => i ? ' '.repeat( indent ) + line : line
|
|
25
25
|
).join( '\n' );
|
|
26
26
|
}
|
|
27
27
|
|
package/src/get-config.js
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
const fs = require( 'fs' );
|
|
4
4
|
const packagePath = require( './package-path' );
|
|
5
5
|
const importFresh = require( 'import-fresh' );
|
|
6
|
+
const validateConfig = require( './validate-config' );
|
|
7
|
+
const formatter = require( './formatter' );
|
|
6
8
|
|
|
7
9
|
const configFilenames = [
|
|
8
10
|
'.eslintdocgenrc.js',
|
|
@@ -15,6 +17,7 @@ const configFilenames = [
|
|
|
15
17
|
* Missing values are backfilled with the defaults
|
|
16
18
|
*
|
|
17
19
|
* @return {Object} Config
|
|
20
|
+
* @throws {Error} If no config file is found, or if the config is invalid
|
|
18
21
|
*/
|
|
19
22
|
function getConfig() {
|
|
20
23
|
let config, configPath;
|
|
@@ -22,6 +25,7 @@ function getConfig() {
|
|
|
22
25
|
configFilenames.some( ( configFilename ) => {
|
|
23
26
|
configPath = packagePath( configFilename );
|
|
24
27
|
|
|
28
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename
|
|
25
29
|
if ( fs.existsSync( configPath ) ) {
|
|
26
30
|
config = importFresh( configPath );
|
|
27
31
|
return true;
|
|
@@ -30,16 +34,18 @@ function getConfig() {
|
|
|
30
34
|
} );
|
|
31
35
|
|
|
32
36
|
if ( !config ) {
|
|
33
|
-
throw new Error( '.eslintdocgenrc not found' );
|
|
37
|
+
throw new Error( [ formatter.heading( 'eslint-docgen' ), formatter.error( '.eslintdocgenrc not found' ) ].join( '\n' ) );
|
|
34
38
|
}
|
|
35
39
|
|
|
36
40
|
const defaultConfig = require( './default-config.js' );
|
|
37
41
|
config = Object.assign( {}, defaultConfig, config );
|
|
38
42
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
configPath
|
|
42
|
-
}
|
|
43
|
+
const configErrors = validateConfig( config );
|
|
44
|
+
if ( configErrors.length ) {
|
|
45
|
+
throw new Error( [ formatter.heading( configPath ), ...configErrors.map( formatter.error ) ].join( '\n' ) );
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return config;
|
|
43
49
|
}
|
|
44
50
|
|
|
45
51
|
module.exports = getConfig;
|
package/src/load-templates.js
CHANGED
|
@@ -5,10 +5,12 @@ const path = require( 'upath' );
|
|
|
5
5
|
const ejs = require( 'ejs' );
|
|
6
6
|
|
|
7
7
|
function loadTemplatesFromPath( dirPath ) {
|
|
8
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename
|
|
8
9
|
const files = fs.readdirSync( dirPath );
|
|
9
10
|
const templates = {};
|
|
10
11
|
files.forEach( ( filename ) => {
|
|
11
12
|
templates[ path.parse( filename ).name ] =
|
|
13
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename
|
|
12
14
|
fs.readFileSync( path.join( dirPath, filename ) ).toString();
|
|
13
15
|
} );
|
|
14
16
|
return templates;
|
|
@@ -31,14 +33,13 @@ function loadTemplates( dirPaths ) {
|
|
|
31
33
|
|
|
32
34
|
function compile( string, filename ) {
|
|
33
35
|
const compiled = ejs.compile( string, { client: true } );
|
|
34
|
-
return ( data ) =>
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
} );
|
|
36
|
+
return ( data ) => compiled( data, null, ( template, includeData ) => {
|
|
37
|
+
const mergedData = Object.assign( {}, data, includeData );
|
|
38
|
+
if ( !hasOwn.call( templateStrings, template ) ) {
|
|
39
|
+
throw new Error( 'Template `' + template + '` not found in template `' + filename + '`' );
|
|
40
|
+
}
|
|
41
|
+
return globalTemplates[ template ]( mergedData );
|
|
42
|
+
} );
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
Object.keys( templateStrings ).forEach( ( filename ) => {
|
|
@@ -47,6 +48,7 @@ function loadTemplates( dirPaths ) {
|
|
|
47
48
|
|
|
48
49
|
function loadRuleTemplate( ruleTemplatePath ) {
|
|
49
50
|
return compile(
|
|
51
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename
|
|
50
52
|
fs.readFileSync( ruleTemplatePath ).toString()
|
|
51
53
|
);
|
|
52
54
|
}
|
package/src/main.js
CHANGED
package/src/naming.js
CHANGED
|
@@ -14,17 +14,19 @@
|
|
|
14
14
|
*/
|
|
15
15
|
function getShorthandName( fullname, prefix ) {
|
|
16
16
|
if ( fullname[ 0 ] === '@' ) {
|
|
17
|
-
|
|
17
|
+
// eslint-disable-next-line security/detect-non-literal-regexp
|
|
18
|
+
let matchResult = new RegExp( `^(@[^/]+)/${ prefix }$`, 'u' ).exec( fullname );
|
|
18
19
|
|
|
19
20
|
if ( matchResult ) {
|
|
20
21
|
return matchResult[ 1 ];
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
|
|
24
|
+
// eslint-disable-next-line security/detect-non-literal-regexp
|
|
25
|
+
matchResult = new RegExp( `^(@[^/]+)/${ prefix }-(.+)$`, 'u' ).exec( fullname );
|
|
24
26
|
if ( matchResult ) {
|
|
25
|
-
return `${matchResult[ 1 ]}/${matchResult[ 2 ]}`;
|
|
27
|
+
return `${ matchResult[ 1 ] }/${ matchResult[ 2 ] }`;
|
|
26
28
|
}
|
|
27
|
-
} else if ( fullname.startsWith( `${prefix}-` ) ) {
|
|
29
|
+
} else if ( fullname.startsWith( `${ prefix }-` ) ) {
|
|
28
30
|
return fullname.slice( prefix.length + 1 );
|
|
29
31
|
}
|
|
30
32
|
|
package/src/rule-tester.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const packagePath = require( './package-path' );
|
|
4
3
|
// Use plugin's version of ESLint
|
|
5
|
-
const ESLintRuleTester = require(
|
|
4
|
+
const ESLintRuleTester = require( './eslint' )().RuleTester;
|
|
6
5
|
const inDocMode = !!process.env.DOCGEN;
|
|
7
6
|
|
|
8
7
|
/**
|
package/src/rules-with-config.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
// eslint-disable-next-line security/detect-non-literal-require
|
|
3
4
|
const main = require( require( './main' ) );
|
|
4
|
-
const pluginName = require( './get-config' )().
|
|
5
|
+
const pluginName = require( './get-config' )().pluginName;
|
|
5
6
|
const configs = main.configs;
|
|
6
7
|
const rulesWithConfig = new Map( Object.entries( main.rules ) );
|
|
7
8
|
|
|
@@ -10,15 +11,66 @@ rulesWithConfig.forEach( ( rule, name ) => {
|
|
|
10
11
|
rulesWithConfig.get( name ).configMap = new Map();
|
|
11
12
|
} );
|
|
12
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Check if a config is a duplicate of another config in a different format
|
|
16
|
+
*
|
|
17
|
+
* Plugins which support both eslintrc and flat config often export each config
|
|
18
|
+
* twice, e.g. `recommended` and `flat/recommended`, or `recommended` and
|
|
19
|
+
* `legacy-recommended`. Only list these once.
|
|
20
|
+
*
|
|
21
|
+
* @param {string} name Config name
|
|
22
|
+
* @return {boolean}
|
|
23
|
+
*/
|
|
24
|
+
function isDuplicateConfig( name ) {
|
|
25
|
+
const match = name.match( /^(?:flat\/|legacy-)(.*)$/ );
|
|
26
|
+
return !!match && Object.prototype.hasOwnProperty.call( configs, match[ 1 ] );
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get the rules set by a config
|
|
31
|
+
*
|
|
32
|
+
* @param {Object|Object[]} config eslintrc config, flat config, or flat config array
|
|
33
|
+
* @return {Object} Rules
|
|
34
|
+
*/
|
|
35
|
+
function getRules( config ) {
|
|
36
|
+
return Array.isArray( config ) ?
|
|
37
|
+
Object.assign( {}, ...config.map( getRules ) ) :
|
|
38
|
+
config.rules || {};
|
|
39
|
+
}
|
|
40
|
+
|
|
13
41
|
// Iterate over configs to add config data to map
|
|
14
42
|
for ( const name in configs ) {
|
|
15
|
-
|
|
43
|
+
if ( isDuplicateConfig( name ) ) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const rules = getRules( configs[ name ] );
|
|
16
47
|
for ( const fullName in rules ) {
|
|
48
|
+
// Flat config arrays can include rules from other plugins
|
|
49
|
+
if ( !fullName.startsWith( pluginName + '/' ) ) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
17
52
|
// Configs use the full rule name with the plugin prefix, so remove this
|
|
18
53
|
const shortName = fullName.slice( pluginName.length + 1 );
|
|
54
|
+
let options, rawSeverity;
|
|
55
|
+
|
|
56
|
+
if ( Array.isArray( rules[ fullName ] ) ) {
|
|
57
|
+
options = rules[ fullName ].slice( 1 );
|
|
58
|
+
rawSeverity = rules[ fullName ][ 0 ];
|
|
59
|
+
} else {
|
|
60
|
+
options = null;
|
|
61
|
+
rawSeverity = rules[ fullName ];
|
|
62
|
+
}
|
|
63
|
+
const severityMap = [ 'off', 'warn', 'error' ];
|
|
64
|
+
const severity = typeof rawSeverity === 'number' ?
|
|
65
|
+
severityMap[ rawSeverity ] || 'error' :
|
|
66
|
+
rawSeverity;
|
|
67
|
+
|
|
19
68
|
rulesWithConfig.get( shortName ).configMap.set(
|
|
20
69
|
name,
|
|
21
|
-
|
|
70
|
+
{
|
|
71
|
+
severity,
|
|
72
|
+
options
|
|
73
|
+
}
|
|
22
74
|
);
|
|
23
75
|
}
|
|
24
76
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
📋 This rule is enabled in `plugin:<%- pluginName %>/<%- inConfig.config %>`<%- include( 'optionsAndSettings', { data: inConfig } ) %>.
|
|
1
|
+
📋 This rule is enabled as <%- inConfig.severity === 'warn' ? 'a warning' : 'an error' %> in `plugin:<%- pluginName %>/<%- inConfig.config %>`<%- include( 'optionsAndSettings', { data: inConfig } ) %>.
|
package/src/validate-config.js
CHANGED
|
@@ -11,8 +11,7 @@ const schema = require( './config-schema' );
|
|
|
11
11
|
* @return {string[]} A list of errors found
|
|
12
12
|
*/
|
|
13
13
|
function validateConfig( config ) {
|
|
14
|
-
const errors = v.validate( config, schema ).errors.map( ( e ) =>
|
|
15
|
-
e.schema.message ? e.property + ' ' + e.schema.message : e.stack );
|
|
14
|
+
const errors = v.validate( config, schema ).errors.map( ( e ) => e.schema.message ? e.property + ' ' + e.schema.message : e.stack );
|
|
16
15
|
|
|
17
16
|
return errors;
|
|
18
17
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const fs = require( 'fs' );
|
|
4
|
-
const mkdirp = require( 'mkdirp' );
|
|
4
|
+
const { mkdirp } = require( 'mkdirp' );
|
|
5
5
|
const path = require( 'upath' );
|
|
6
6
|
const buildDocsFromTests = require( './build-docs-from-tests' );
|
|
7
7
|
|
|
@@ -9,23 +9,16 @@ const formatter = require( './formatter' );
|
|
|
9
9
|
const rulesWithConfig = require( './rules-with-config' );
|
|
10
10
|
|
|
11
11
|
const getConfig = require( './get-config' );
|
|
12
|
-
|
|
13
|
-
try {
|
|
14
|
-
( { config, configPath } = getConfig() );
|
|
15
|
-
} catch ( e ) {
|
|
16
|
-
throw new Error( [ formatter.heading( 'eslint-docgen' ), formatter.error( e.message ) ].join( '\n' ) );
|
|
17
|
-
}
|
|
12
|
+
const config = getConfig();
|
|
18
13
|
|
|
19
|
-
const
|
|
14
|
+
const validateConfig = require( './validate-config' );
|
|
20
15
|
function assertValidConfig( maybeValidConfig, configSource ) {
|
|
21
|
-
const configErrors =
|
|
16
|
+
const configErrors = validateConfig( maybeValidConfig );
|
|
22
17
|
if ( configErrors.length ) {
|
|
23
18
|
throw new Error( [ formatter.heading( configSource ), ...configErrors.map( formatter.error ) ].join( '\n' ) );
|
|
24
19
|
}
|
|
25
20
|
}
|
|
26
21
|
|
|
27
|
-
assertValidConfig( config, configPath );
|
|
28
|
-
|
|
29
22
|
const packagePath = require( './package-path' );
|
|
30
23
|
|
|
31
24
|
const loadTemplates = require( './load-templates' );
|
|
@@ -61,6 +54,7 @@ async function writeDocsFromTests( name, rule, tests, testerConfig, done ) {
|
|
|
61
54
|
|
|
62
55
|
const outputDir = path.dirname( outputPath );
|
|
63
56
|
mkdirp( outputDir ).then( () => {
|
|
57
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename
|
|
64
58
|
fs.writeFile(
|
|
65
59
|
outputPath,
|
|
66
60
|
output,
|
|
@@ -71,8 +65,8 @@ async function writeDocsFromTests( name, rule, tests, testerConfig, done ) {
|
|
|
71
65
|
if ( messages.length ) {
|
|
72
66
|
console.log();
|
|
73
67
|
console.log( formatter.heading( outputPath ) );
|
|
74
|
-
messages.forEach(
|
|
75
|
-
console.log( formatter[ message.type ]( message.text, message.label ) )
|
|
68
|
+
messages.forEach(
|
|
69
|
+
( message ) => console.log( formatter[ message.type ]( message.text, message.label ) )
|
|
76
70
|
);
|
|
77
71
|
console.log();
|
|
78
72
|
}
|