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 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 the options used (`null` if no options are used).
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-3.5' => null,
174
- 'deprecated-3.3' => [ { allowAjaxEvents: true } ]
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.7.1",
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 && nyc mocha tests/",
20
- "report": "nyc report --reporter=html"
22
+ "test": "eslint . --cache && c8 mocha tests/",
23
+ "report": "c8 report --reporter=html"
21
24
  },
22
25
  "engines": {
23
- "node": ">=12.0.0"
26
+ "node": ">=20.0.0"
24
27
  },
25
28
  "dependencies": {
26
29
  "chalk": "^4.1.2",
27
- "ejs": "^3.1.8",
28
- "eslint": ">=8.0.0",
29
- "import-fresh": "^3.3.0",
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": "^1.0.4",
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
- "eslint-config-wikimedia": "^0.22.1",
40
- "eslint-plugin-node": "^11.1.0",
41
- "mocha": "^9.2.0",
42
- "nyc": "^15.1.0"
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( ( acc, cur, i, arr ) =>
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( ( test ) =>
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( ( acc, code ) =>
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
- 0 );
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( function ( test, i ) {
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( ( a, b ) => {
275
- return a.key === b.key ?
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;
@@ -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" },
@@ -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 { Linter, ESLint } = require( 'eslint' );
4
- const linter = new Linter();
5
- const eslint = new ESLint();
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
- const config = await eslint.calculateConfigForFile( require( './main' ) );
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
- // Optimization: Only use fixable rules
14
- const fixableRules = {};
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
- fixableRules[ ruleName ] = config.rules[ ruleName ];
39
+ rules[ ruleName ] = config.rules[ ruleName ];
20
40
  }
21
41
  }
22
42
  } );
23
- config.rules = fixableRules;
24
43
 
25
- return config;
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 Config
138
+ * @param {Object|Object[]} testerConfig RuleTester config
33
139
  * @return {string} Fixed code
34
140
  */
35
141
  async function lintFix( code, testerConfig ) {
36
- const mergedConfig = mergeOptions( await getConfig(), testerConfig );
142
+ const config = await getConfig();
37
143
 
38
- // TODO
39
- // istanbul ignore next
40
- if ( typeof mergedConfig.parser === 'string' ) {
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 Config
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( ( line, i ) =>
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
- return {
40
- config: config,
41
- configPath: 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;
@@ -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
- compiled( data, null, function ( template, includeData ) {
36
- const mergedData = Object.assign( {}, data, includeData );
37
- if ( !hasOwn.call( templateStrings, template ) ) {
38
- throw new Error( 'Template `' + template + '` not found in template `' + filename + '`' );
39
- }
40
- return globalTemplates[ template ]( mergedData );
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
@@ -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 main = require( packagePath( './package' ) ).main || 'index.js';
5
6
 
6
7
  module.exports = packagePath( main );
package/src/naming.js CHANGED
@@ -14,17 +14,19 @@
14
14
  */
15
15
  function getShorthandName( fullname, prefix ) {
16
16
  if ( fullname[ 0 ] === '@' ) {
17
- let matchResult = new RegExp( `^(@[^/]+)/${prefix}$`, 'u' ).exec( fullname );
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
- matchResult = new RegExp( `^(@[^/]+)/${prefix}-(.+)$`, 'u' ).exec( fullname );
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
 
@@ -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( packagePath( 'node_modules/eslint' ) ).RuleTester;
4
+ const ESLintRuleTester = require( './eslint' )().RuleTester;
6
5
  const inDocMode = !!process.env.DOCGEN;
7
6
 
8
7
  /**
@@ -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' )().config.pluginName;
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
- const rules = configs[ name ].rules || {};
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
- Array.isArray( rules[ fullName ] ) ? rules[ fullName ].slice( 1 ) : null
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 } ) %>.
@@ -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
- let config, configPath;
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 configValidator = require( './validate-config' );
14
+ const validateConfig = require( './validate-config' );
20
15
  function assertValidConfig( maybeValidConfig, configSource ) {
21
- const configErrors = configValidator( maybeValidConfig );
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( ( message ) =>
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
  }