eslint-docgen 0.5.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-docgen",
3
- "version": "0.5.1",
3
+ "version": "0.7.0",
4
4
  "description": "Automatically generate ESLint plugin documentation from rule metadata and test cases.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -25,7 +25,7 @@
25
25
  "dependencies": {
26
26
  "chalk": "^4.1.2",
27
27
  "ejs": "^3.1.6",
28
- "eslint": ">=5.0.0",
28
+ "eslint": ">=8.0.0",
29
29
  "import-fresh": "^3.3.0",
30
30
  "jsonschema": "^1.4.0",
31
31
  "merge-options": "^3.0.4",
@@ -36,9 +36,9 @@
36
36
  "upath": "^2.0.1"
37
37
  },
38
38
  "devDependencies": {
39
- "eslint-config-wikimedia": "^0.20.0",
39
+ "eslint-config-wikimedia": "^0.22.1",
40
40
  "eslint-plugin-node": "^11.1.0",
41
- "mocha": "^9.1.3",
41
+ "mocha": "^9.2.0",
42
42
  "nyc": "^15.1.0"
43
43
  }
44
44
  }
@@ -2,6 +2,13 @@
2
2
 
3
3
  const pluralize = require( 'pluralize' );
4
4
  const path = require( 'upath' );
5
+ // Mapping from extension to language name for markdown
6
+ // Default language is 'js'
7
+ const languageFromExtension = {
8
+ '.html': 'html',
9
+ '.ts': 'ts',
10
+ '.vue': 'vue'
11
+ };
5
12
 
6
13
  /**
7
14
  * Format a list of items with natural language
@@ -30,7 +37,7 @@ function mdLink( target, label ) {
30
37
  return '[' + label + '](' + target + ')';
31
38
  }
32
39
 
33
- function buildDocsFromTests(
40
+ async function buildDocsFromTests(
34
41
  name, ruleMeta, tests, configMap, config, globalTemplates, loadRuleTemplate, testerConfig
35
42
  ) {
36
43
 
@@ -60,28 +67,31 @@ function buildDocsFromTests(
60
67
  }
61
68
 
62
69
  /**
63
- *
64
- * @param {string|Object} test
65
- * @return {string|undefined} The base file name, or undefined if not set
70
+ * @param {string|Object} test Test case
71
+ * @return {string|null} The base file name, or null if not set
66
72
  */
67
73
  function getFilename( test ) {
68
74
  if ( typeof test === 'string' || test.filename === undefined ) {
69
- // Either the test is just a code snippet, or its an object but with
70
- // no file name set
71
- return undefined;
75
+ // Either the test is just a code snippet, or no file name is set
76
+ return null;
72
77
  }
73
- // We don't need the full file path, just the last bit with the name and extension
74
78
  return path.basename( test.filename );
75
79
  }
76
80
 
77
- function buildRuleDetails( testList, valid, showFixes ) {
81
+ async function buildRuleDetails( testList, isValid, showFixes ) {
82
+ testList = testList.filter( ( test ) =>
83
+ test.docgen === undefined ?
84
+ !config.excludeExamplesByDefault :
85
+ test.docgen
86
+ );
87
+
78
88
  let fixedCode, fixedOutput, maxCodeLength;
79
89
  const testsByOptions = {};
80
90
 
81
91
  const codeList = testList.map( getCode );
82
92
  if ( config.fixCodeExamples ) {
83
93
  const fix = require( './fix' );
84
- fixedCode = fix.batchLintFix( codeList, testerConfig );
94
+ fixedCode = await fix.batchLintFix( codeList, testerConfig );
85
95
  } else {
86
96
  fixedCode = codeList;
87
97
  }
@@ -100,7 +110,7 @@ function buildDocsFromTests(
100
110
  const outputList = testList.map( ( test ) => test.output );
101
111
  if ( config.fixCodeExamples ) {
102
112
  const fix = require( './fix' );
103
- fixedOutput = fix.batchLintFix( outputList, testerConfig );
113
+ fixedOutput = await fix.batchLintFix( outputList, testerConfig );
104
114
  } else {
105
115
  fixedOutput = outputList;
106
116
  }
@@ -111,39 +121,37 @@ function buildDocsFromTests(
111
121
  const codeSet = {};
112
122
  let previousMultiLine = false;
113
123
  testList.forEach( function ( test, i ) {
114
- if ( !config.excludeExamplesByDefault && test.noDoc ) {
115
- messages.push( {
116
- type: 'warn',
117
- text: '`noDoc: true` is deprecated. Use `docgen: false` instead.'
118
- } );
119
- return;
124
+ const filename = getFilename( test );
125
+ let lang = 'js';
126
+ // Switch to Vue if we are showing file names and its a Vue file.
127
+ // optionsAndSettings.filename is only set if it should be shown
128
+ // TODO should we add other languages too?
129
+ if ( filename ) {
130
+ const ext = path.extname( filename );
131
+ if ( ext in languageFromExtension ) {
132
+ lang = languageFromExtension[ ext ];
133
+ }
120
134
  }
121
135
 
122
- const docgen = test.docgen === undefined ?
123
- !config.excludeExamplesByDefault :
124
- test.docgen;
125
-
126
- if ( !docgen ) {
127
- return;
128
- }
136
+ // Keys are in reverse sort order, e.g. examples with options
137
+ // are shown before examples with settings
138
+ const optionsAndSettings = {
139
+ // Always separate tests by language for syntax highlighting
140
+ lang: lang,
141
+ settings: test.settings,
142
+ options: test.options,
143
+ // Only group by filename if filenames are shown
144
+ filename: config.showFilenames && filename
145
+ };
146
+ const hashObject = {};
147
+ // Ensure examples without a specific key are shown first
148
+ Object.keys( optionsAndSettings ).forEach( ( key ) => {
149
+ hashObject[ key ] = optionsAndSettings[ key ] ?
150
+ [ '1', optionsAndSettings[ key ] ] :
151
+ [ '0', optionsAndSettings[ key ] ];
152
+ } );
129
153
 
130
- let optionsAndSettings;
131
- // Only include the filename if it should be shown
132
- // Don't create an object if there are no options or settings and the file name is not
133
- // set, so that those examples are sorted to the top of the docs
134
- if (
135
- test.options || test.settings ||
136
- ( config.showFilenames && getFilename( test ) )
137
- ) {
138
- optionsAndSettings = {
139
- options: test.options,
140
- settings: test.settings
141
- };
142
- if ( config.showFilenames ) {
143
- optionsAndSettings.filename = getFilename( test );
144
- }
145
- }
146
- const hash = optionsAndSettings ? JSON.stringify( optionsAndSettings ) : '';
154
+ const hash = JSON.stringify( hashObject );
147
155
 
148
156
  codeSet[ hash ] = codeSet[ hash ] || {};
149
157
 
@@ -168,9 +176,8 @@ function buildDocsFromTests(
168
176
  };
169
177
 
170
178
  if ( showFixes && test.output ) {
171
- const output = fixedOutput[ i ];
172
179
  const codeLines = code.split( '\n' );
173
- const outputLines = output.split( '\n' );
180
+ const outputLines = fixedOutput[ i ].split( '\n' );
174
181
  const maxLines = Math.max( codeLines.length, outputLines.length );
175
182
  const exampleLines = [];
176
183
  for ( let l = 0; l < maxLines; l++ ) {
@@ -188,7 +195,8 @@ function buildDocsFromTests(
188
195
  example = code;
189
196
  multiLine = code.split( '\n' ).length > 1;
190
197
  }
191
- if ( multiLine || previousMultiLine ) {
198
+ // Put linebreaks around multi-line examples
199
+ if ( testsByOptions[ hash ].tests.length && ( multiLine || previousMultiLine ) ) {
192
200
  example = '\n' + example;
193
201
  }
194
202
  testsByOptions[ hash ].tests.push( example );
@@ -207,18 +215,19 @@ function buildDocsFromTests(
207
215
  if ( config.fixCodeExamples ) {
208
216
  const fix = require( './fix' );
209
217
  // Fixes whitespace in block comment. Too expensive for such a small fix?
210
- comments = fix.batchLintFix( comments, testerConfig );
218
+ comments = await fix.batchLintFix( comments, testerConfig );
211
219
  }
212
220
  }
213
221
 
214
222
  return Object.keys( testsByOptions ).map( ( key, i ) => {
215
223
  const section = testsByOptions[ key ];
216
224
  const optionsAndSettings = section.optionsAndSettings;
225
+ const lang = optionsAndSettings && optionsAndSettings.lang;
217
226
  const options = optionsAndSettings && optionsAndSettings.options;
218
227
  const settings = optionsAndSettings && optionsAndSettings.settings;
219
228
  const filename = optionsAndSettings && optionsAndSettings.filename;
220
229
 
221
- let examples = '```js\n';
230
+ let examples = '```' + lang + '\n';
222
231
  if ( config.showConfigComments ) {
223
232
  examples += comments[ i ] + '\n';
224
233
  }
@@ -227,7 +236,7 @@ function buildDocsFromTests(
227
236
 
228
237
  return {
229
238
  key: key,
230
- valid: valid,
239
+ valid: isValid,
231
240
  options: options ? JSON.stringify( options ) : '',
232
241
  settings: settings ? JSON.stringify( settings ) : '',
233
242
  filename: filename || '',
@@ -259,8 +268,8 @@ function buildDocsFromTests(
259
268
  } );
260
269
  }
261
270
 
262
- const invalid = buildRuleDetails( tests.invalid, false );
263
- const valid = buildRuleDetails( tests.valid, true );
271
+ const invalid = await buildRuleDetails( tests.invalid, false );
272
+ const valid = await buildRuleDetails( tests.valid, true );
264
273
 
265
274
  const validInvalid = invalid.concat( valid ).sort( ( a, b ) => {
266
275
  return a.key === b.key ?
@@ -270,7 +279,7 @@ function buildDocsFromTests(
270
279
 
271
280
  let fixed = [];
272
281
  if ( ruleMeta.fixable && config.showFixExamples ) {
273
- fixed = buildRuleDetails(
282
+ fixed = await buildRuleDetails(
274
283
  tests.invalid.filter( ( test ) => !!test.output ),
275
284
  false,
276
285
  true
package/src/fix.js CHANGED
@@ -1,23 +1,29 @@
1
1
  'use strict';
2
2
 
3
- const eslint = require( 'eslint' );
4
- const linter = new eslint.Linter();
5
- const cli = new eslint.CLIEngine();
6
- const config = cli.getConfigForFile( require( './main' ) );
7
- const rules = cli.getRules();
3
+ const { Linter, ESLint } = require( 'eslint' );
4
+ const linter = new Linter();
5
+ const eslint = new ESLint();
8
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
9
 
10
- // Optimization: Only use fixable rules
11
- const fixableRules = {};
12
- Object.keys( config.rules ).forEach( function ( ruleName ) {
13
- if ( rules.has( ruleName ) ) {
14
- const rule = rules.get( ruleName );
15
- if ( rule.meta.fixable ) {
16
- fixableRules[ ruleName ] = config.rules[ ruleName ];
10
+ async function getConfig() {
11
+ const config = await eslint.calculateConfigForFile( require( './main' ) );
12
+
13
+ // Optimization: Only use fixable rules
14
+ const fixableRules = {};
15
+ Object.keys( config.rules ).forEach( function ( ruleName ) {
16
+ if ( builtinRules.has( ruleName ) ) {
17
+ const rule = builtinRules.get( ruleName );
18
+ if ( rule.meta.fixable ) {
19
+ fixableRules[ ruleName ] = config.rules[ ruleName ];
20
+ }
17
21
  }
18
- }
19
- } );
20
- config.rules = fixableRules;
22
+ } );
23
+ config.rules = fixableRules;
24
+
25
+ return config;
26
+ }
21
27
 
22
28
  /**
23
29
  * Lint and fix some code
@@ -26,8 +32,8 @@ config.rules = fixableRules;
26
32
  * @param {Object} testerConfig Config
27
33
  * @return {string} Fixed code
28
34
  */
29
- function lintFix( code, testerConfig ) {
30
- const mergedConfig = mergeOptions( config, testerConfig );
35
+ async function lintFix( code, testerConfig ) {
36
+ const mergedConfig = mergeOptions( await getConfig(), testerConfig );
31
37
 
32
38
  // TODO
33
39
  // istanbul ignore next
@@ -55,11 +61,12 @@ function lintFix( code, testerConfig ) {
55
61
  * @param {Object} testerConfig Config
56
62
  * @return {string[]} Fixed code list
57
63
  */
58
- function batchLintFix( codeList, testerConfig ) {
64
+ async function batchLintFix( codeList, testerConfig ) {
59
65
  const separator = '\n/* - */\n';
60
66
  const codeBlock = codeList.join( ';' + separator );
61
67
  // Add an extra semicolon to avoid syntax error
62
- return lintFix( codeBlock, testerConfig ).split( separator ).map( ( code ) => code.trim() );
68
+ const fixed = await lintFix( codeBlock, testerConfig );
69
+ return fixed.split( separator ).map( ( code ) => code.trim() );
63
70
  }
64
71
 
65
72
  module.exports = {
@@ -3,37 +3,26 @@
3
3
  const packagePath = require( './package-path' );
4
4
  // Use plugin's version of ESLint
5
5
  const ESLintRuleTester = require( packagePath( 'node_modules/eslint' ) ).RuleTester;
6
- const docMode = !!process.env.DOCGEN || process.argv.includes( '--doc' );
7
-
8
- if ( process.argv.includes( '--doc' ) ) {
9
- const formatter = require( './formatter' );
10
- console.log(
11
- formatter.warn( '--doc is deprecated, use the DOCGEN=1 environment variable instead.' )
12
- );
13
- }
6
+ const inDocMode = !!process.env.DOCGEN;
14
7
 
15
8
  /**
16
9
  * Extends ESLint's RuleTester to also build documentation
17
10
  */
18
11
  class RuleTester extends ESLintRuleTester {
19
12
  run( name, rule, tests ) {
20
- if ( docMode ) {
13
+ if ( inDocMode ) {
21
14
  RuleTester.it( name, ( done ) => {
22
15
  const writeDocsFromTests = require( './write-docs-from-tests' );
23
16
  writeDocsFromTests( name, rule, tests, this.testerConfig, done );
24
17
  } );
25
18
  } else {
26
- // Filter out invalid property "noDoc"
19
+ // Filter out invalid property "docgen"
27
20
  // (used in documentation building mode).
28
21
  tests.valid.forEach( ( test ) => {
29
22
  delete test.docgen;
30
- // Deprecated
31
- delete test.noDoc;
32
23
  } );
33
24
  tests.invalid.forEach( ( test ) => {
34
25
  delete test.docgen;
35
- // Deprecated
36
- delete test.noDoc;
37
26
  } );
38
27
 
39
28
  // Filter out invalid top level property "docgenConfig"
@@ -1,6 +1,5 @@
1
1
  'use strict';
2
2
 
3
- /* eslint-disable no-process-exit */
4
3
  const fs = require( 'fs' );
5
4
  const mkdirp = require( 'mkdirp' );
6
5
  const path = require( 'upath' );
@@ -14,20 +13,14 @@ let config, configPath;
14
13
  try {
15
14
  ( { config, configPath } = getConfig() );
16
15
  } catch ( e ) {
17
- console.log( formatter.heading( 'eslint-docgen' ) );
18
- console.log( formatter.error( e.message ) );
19
- console.log();
20
- process.exit( 1 );
16
+ throw new Error( [ formatter.heading( 'eslint-docgen' ), formatter.error( e.message ) ].join( '\n' ) );
21
17
  }
22
18
 
23
19
  const configValidator = require( './validate-config' );
24
20
  function assertValidConfig( maybeValidConfig, configSource ) {
25
21
  const configErrors = configValidator( maybeValidConfig );
26
22
  if ( configErrors.length ) {
27
- console.log( formatter.heading( configSource ) );
28
- configErrors.forEach( ( error ) => console.log( formatter.error( error ) ) );
29
- console.log();
30
- process.exit( 1 );
23
+ throw new Error( [ formatter.heading( configSource ), ...configErrors.map( formatter.error ) ].join( '\n' ) );
31
24
  }
32
25
  }
33
26
 
@@ -42,7 +35,7 @@ if ( config.globalTemplatePath ) {
42
35
  }
43
36
  const { globalTemplates, loadRuleTemplate } = loadTemplates( templatePaths );
44
37
 
45
- function writeDocsFromTests( name, rule, tests, testerConfig, done ) {
38
+ async function writeDocsFromTests( name, rule, tests, testerConfig, done ) {
46
39
  // If the tests have a `docgenConfig` property, this overrides the global configuration
47
40
  let configForRule = config;
48
41
  if ( tests.docgenConfig !== undefined ) {
@@ -53,25 +46,17 @@ function writeDocsFromTests( name, rule, tests, testerConfig, done ) {
53
46
  const outputPath = packagePath( configForRule.docPath.replace( '{name}', name ) );
54
47
  const ruleWithConfig = rulesWithConfig.get( name );
55
48
  if ( !ruleWithConfig ) {
56
- console.log();
57
- console.log( formatter.heading( outputPath ) );
58
- console.log( formatter.error( 'Rule not found.' ) );
59
- console.log();
60
- process.exit( 1 );
49
+ throw new Error( [ formatter.heading( outputPath ), formatter.error( 'Rule not found.' ) ].join( '\n' ) );
61
50
  }
62
51
  const configMap = rulesWithConfig.get( name ).configMap;
63
52
  let output, messages;
64
53
  try {
65
- ( { output, messages } = buildDocsFromTests(
54
+ ( { output, messages } = await buildDocsFromTests(
66
55
  name, rule.meta, tests, configMap, configForRule,
67
56
  globalTemplates, loadRuleTemplate, testerConfig
68
57
  ) );
69
58
  } catch ( e ) {
70
- console.log();
71
- console.log( formatter.heading( outputPath ) );
72
- console.log( formatter.error( e.message ) );
73
- console.log();
74
- process.exit( 1 );
59
+ throw new Error( [ formatter.heading( outputPath ), formatter.error( e.message ) ].join( '\n' ) );
75
60
  }
76
61
 
77
62
  const outputDir = path.dirname( outputPath );
@@ -92,8 +77,10 @@ function writeDocsFromTests( name, rule, tests, testerConfig, done ) {
92
77
  console.log();
93
78
  }
94
79
 
95
- if ( messages.some( ( message ) => message.type === 'error' ) ) {
96
- process.exit( 1 );
80
+ const errors = messages.filter( ( message ) => message.type === 'error' );
81
+
82
+ if ( errors.length ) {
83
+ throw new Error( errors.map( formatter.error ).join( '\n' ) );
97
84
  }
98
85
 
99
86
  done();