├── .editorconfig
├── .eslintignore
├── .eslintrc.json
├── .github
└── workflows
│ └── nodejs.yml
├── .gitignore
├── .mocharc.json
├── Changelog.md
├── LICENSE
├── README.md
├── package-lock.json
├── package.json
├── src
├── build-docs-from-tests.js
├── config-schema.json
├── default-config.js
├── fix.js
├── formatter.js
├── get-config.js
├── index.js
├── load-templates.js
├── main.js
├── naming.js
├── package-path.js
├── rule-tester.js
├── rules-with-config.js
├── templates
│ ├── deprecated.ejs
│ ├── exampleFixed.ejs
│ ├── exampleInvalid.ejs
│ ├── exampleValid.ejs
│ ├── examples.ejs
│ ├── fixable.ejs
│ ├── inConfig.ejs
│ ├── inConfigs.ejs
│ ├── index.ejs
│ ├── optionsAndSettings.ejs
│ └── resources.ejs
├── validate-config.js
└── write-docs-from-tests.js
└── tests
├── .eslintrc.json
├── build-docs-from-tests.js
├── cases
├── config-comments.md
├── file-names.md
├── no-fix-code-examples.md
├── no-show-fixes.md
├── not-fixable.md
├── plugin-missing-config
│ └── package.json
├── plugin-scoped
│ └── package.json
├── plugin
│ ├── .eslintdocgenrc.json
│ ├── package.json
│ ├── ruleTemplates
│ │ └── my-rule.ejs
│ └── src
│ │ ├── .eslintrc.json
│ │ └── main.js
├── rule-template-path.md
├── scoped-plugin.md
├── simple-rule.md
├── syntax-lang.md
└── templates
│ └── test.ejs
├── formatter.js
├── get-config.js
├── init.js
├── load-templates.js
├── rules-with-config.js
├── test-utils.js
├── validate-config.js
└── write-docs-from-tests.js
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = tab
5 | indent_size = 4
6 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | # Test plugin contains fake rules
2 | tests/cases/plugin
3 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "wikimedia",
4 | "wikimedia/node",
5 | "wikimedia/language/es2018",
6 | "plugin:node/recommended"
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/.github/workflows/nodejs.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
3 |
4 | name: Node.js CI
5 |
6 | on:
7 | push:
8 | branches: [ master ]
9 | pull_request:
10 | branches: [ master ]
11 |
12 | jobs:
13 | build:
14 |
15 | runs-on: ${{ matrix.os }}
16 |
17 | strategy:
18 | matrix:
19 | node-version: [16.x, 18.x, 20.x]
20 | os: [ubuntu-latest, windows-latest]
21 |
22 | steps:
23 | - name: Turn off autocrlf for Windows testing
24 | run: |
25 | git config --global core.autocrlf false
26 | - uses: actions/checkout@v2
27 | - name: Use Node.js ${{ matrix.node-version }}
28 | uses: actions/setup-node@v1
29 | with:
30 | node-version: ${{ matrix.node-version }}
31 | - run: npm ci
32 | - run: npm run build --if-present
33 | - run: npm test
34 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | coverage/
3 | .nyc_output/
4 | .eslintcache
5 |
--------------------------------------------------------------------------------
/.mocharc.json:
--------------------------------------------------------------------------------
1 | {
2 | "file": "./tests/init.js",
3 | "timeout": 5000
4 | }
5 |
--------------------------------------------------------------------------------
/Changelog.md:
--------------------------------------------------------------------------------
1 | # eslint-docgen release history
2 |
3 | ## v0.7.1
4 | * Fix: lintFix: Check for custom parser in mergedConfig (#132)
5 | * Code: Security updates from `npm audit fix`
6 |
7 | ## v0.7.0
8 | * Breaking change: Upgrade to ESLint 8
9 |
10 | ## v0.6.2
11 | * Fix: Use throw instead of process.exit (#127) (Justin Poehnelt)
12 | * Code: Update eslint-config-wikimedia to 0.21.0
13 |
14 | ## v0.6.1
15 | * Fix: Filter out docgen:false tests before parsing (#120)
16 |
17 | ## v0.6.0
18 | * Breaking change: Drop deprecated 'noDoc' option and '--doc' CLI flag (#115)
19 | * New: build-docs-from-tests: add Vue syntax highlighting (#105) (DannyS712)
20 | * New: Add TypeScript syntax highlighting
21 | * New: Add HTML syntax highlighting
22 | * New: Separate syntax highlighting support from showFilenames (#114)
23 | * Fix: Don't put an empty line before the first multi-line examples in a set (#117)
24 | * Code: Cleanup code for detecting highlight language (#113)
25 |
26 | ## v0.5.1
27 | * Breaking change: Change the default icon for rules which are included in a preset (#109)
28 | * Breaking change: Add generated-file-warning comment to the top of MD files (#107)
29 | * Breaking change: Drop support for Node 10 (#110)
30 | * New: Add configuration to show file names for test cases (#101) (DannyS712)
31 | * New: Add per-rule config overrides (DannyS7123)
32 | * Docs: Various README fixes
33 | * Code: Update dependencies
34 |
35 | ## v0.4.5
36 | * Code: Update dependencies
37 |
38 | ## v0.4.4
39 |
40 | * Code: Ensure config.docPath exists before writing output path. (Raine Revere)
41 | * Code: Calculate outputDir from outputPath
42 | * Code: Fix reporting of writeFile error messages
43 | * Code: Output formatted error when rule not found (#92)
44 | * Docs: Fix config param heading level in README (#91)
45 |
46 | ## v0.4.3
47 |
48 | * Code: Run one test 'it' per rule
49 |
50 | ## v0.4.2
51 |
52 | * New: Introduce excludeExamplesByDefault config
53 | * Code: Remove some whitespace in config-schema
54 | * Code: Add a 'report' script
55 |
56 | ## v0.4.1
57 |
58 | * Code: Update mocha to 8.0.1
59 | * Code: Replace path with upath for windows compatibility (#83)
60 | * Code: Use DOCGEN environment variable instead of --doc flag
61 | * Code: Add an editorconfig to make the tabs a reasonable width on GitHub (Jed Fox)
62 | * Code: Allow package.main to be empty
63 | * Code: Support scoped plugin names
64 | * Docs: Add more method documentation
65 | * Docs: Fix external links to examples
66 | * Docs: Add Migration section to README and move some pieces around
67 |
68 | ## v0.4.0
69 |
70 | * Breaking change: New: Add fix message to introduction (#49)
71 | * Breaking change: New: Format long lists correctly ("a, b, and c")
72 | * Breaking change: New: Sort examples by options *then* valid/invalid (#48)
73 | * New: Refactor rulesData to rulesWithConfig and expose (#16)
74 | * New: Add ruleTemplatePath config (#14)
75 | * New: Add showFixExamples config (#49)
76 | * Code: Update eslint-config-wikimedia to 0.16.1 and use /mocha
77 | * Code: Use pkg-dir
78 | * Code: Create a JSON-schema to validate configs (#63)
79 | * Code: Update various dependencies
80 | * Tests: Separate config tests into get/validate
81 | * Tests: Increase timeout
82 | * Tests: Separate buildDocsFromTests cases
83 | * Docs: Fix example docpath
84 |
85 | ## v0.3.1
86 |
87 | * Fix: Use plugin's version of ESLint
88 |
89 | ## v0.3.0
90 |
91 | * Breaking change: New: Support multi-line test cases (#34)
92 | * New: Support multiple lines in formatter
93 | * New: Add min/maxExamples to config (#29)
94 | * New: Use chalk to do pretty output of warnings/errors (#38)
95 | * Code: Bump node dependency to 10 (#44)
96 | * Code: Use strict mode
97 | * Code: Use eslint-plugin-node/recommended
98 | * Code: Removed fixed length truncation from rules-data.js
99 | * Tests: Add tests for most utils (#39)
100 | * Tests: Increase code coverage to 100%
101 | * Docs: Add "plugin" to description
102 |
103 | ## v0.2.0
104 |
105 | * Breaking change: Refactor templates. Users with existing template overrides will need to rewrite them.
106 | * Release: Update ejs to 3.1.3
107 |
108 | ## v0.1.0
109 |
110 | * Initial release.
111 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Wikimedia Foundation
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # eslint-docgen
2 | Automatically generate ESLint plugin documentation from rule metadata and test cases.
3 |
4 | ## ⬇️ Installation
5 |
6 | ```sh
7 | npm install eslint-docgen --save-dev
8 | ```
9 |
10 | ## 🛠️ Setup
11 |
12 | Replace all uses of `RuleTester` with the version from this package:
13 | ```js
14 | // Old:
15 | const RuleTester = require( 'eslint' ).RuleTester;
16 | // New:
17 | const RuleTester = require( 'eslint-docgen' ).RuleTester;
18 | ```
19 |
20 | Create a configuration file as described in [*Configuration*](#%EF%B8%8F-configuration), setting `docPath` and preferably `rulePath` and `testPath`.
21 |
22 | ## 📖 Usage
23 | To build your documentation, run your rule tests with the `DOCGEN` environment variable set in the command line, e.g.
24 | ```sh
25 | DOCGEN=1 mocha tests/rules/
26 | ```
27 |
28 | You could add this to your package.json to make it available as `npm run doc`, e.g.
29 | ```jsonc
30 | {
31 | //
32 | "scripts": {
33 | //
34 | "doc": "rm -rf docs/rules && DOCGEN=1 mochan tests/rules/"
35 | }
36 | //
37 | }
38 | ```
39 |
40 | Documentation will be built using **rule metadata** and **test data** passed to `RuleTester`:
41 |
42 | #### `rule.meta.docs.description`
43 | Used as the description of the rule in the documentation.
44 |
45 | #### `rule.meta.docs.deprecated` / `rule.meta.docs.replacedBy`
46 | Used to show a deprecation warning in the documentation, optionally with links to replacement rule(s).
47 |
48 | #### `tests.valid`/`tests.invalid` from `RuleTester#run`
49 | Will generate code blocks showing examples of valid/invalid usage. Blocks will be grouped by unique `options`/`settings` configurations. Fixable rules with `output` will generate a separate block showing the before and after.
50 |
51 | By default all test cases will be included in the examples. To **exclude** specific test cases from these code blocks use the `docgen: false` option:
52 | ```js
53 | {
54 | code: 'App.method();',
55 | docgen: false
56 | }
57 | ```
58 |
59 | If you have `excludeExamplesByDefault` set to `true` in your config, you can **include** specific test cases in these code blocks by using the `docgen: true` option:
60 | ```js
61 | {
62 | code: 'App.method();',
63 | docgen: true
64 | }
65 | ```
66 |
67 |
68 | ## 🤖 Migration
69 | To migrate an existing plugin with manually built documentation you can use the following process:
70 |
71 | 1. Follow the steps in [*Installation*](#%EF%B8%8F-installation) and [*Setup*](#%EF%B8%8F-setup).
72 | 2. Move your existing documentation to a new folder, e.g. `docs/template/MYRULE.md` and in your `.eslintdocgenrc` set `ruleTemplatePath` to this new folder, e.g. `"docs/template/{name}.md`". Optionally you can rename these files to `.ejs`.
73 | 3. Run the generator (as described in [*Usage*](#-usage)) to confirm that it copies your old documentation (now your templates) back to the original documentation path.
74 | 4. Start switching out manually written sections of your templates with include blocks such as those found in [`index.ejs`](src/templates/index.ejs).
75 |
76 | ## ⚙️ Configuration
77 |
78 | Configuration for all rules in a project is controlled by creating a JSON/JavaScript file called `.eslintdocgenrc.json`/`.eslintdocgenrc.js` in your project root:
79 |
80 | #### JSON
81 | ```jsonc
82 | {
83 | "docPath": "docs/rules/{name}.md",
84 | // ...
85 | }
86 | ```
87 |
88 | #### JavaScript
89 | ```js
90 | module.exports = {
91 | docPath: 'docs/rules/{name}.md',
92 | // ...
93 | };
94 | ```
95 |
96 | #### Overriding
97 |
98 | The project-wide rules configuration can be overridden for individual rules by adding a `docgenConfig` property to the tests object passed to RuleTester.run(). All configuration options that are supported project-wide can be changed.
99 |
100 | ### Options
101 |
102 | The following config options are available:
103 |
104 | #### `docPath` (*required*)
105 | The path to store rule documentation files, with `{name}` as a placeholder for the rule name, e.g. `"docs/rules/{name}.md"` or `"rules/{name}/README.md"`.
106 |
107 | #### `rulePath`
108 | The path where the rule is defined, only required if `ruleLink` is `true`. Same format as `docPath`.
109 |
110 | #### `testPath`
111 | The path where the rule's tests are defined, only required if `testPath` is `true`. Same format as `docPath`.
112 |
113 | #### `ruleTemplatePath`
114 | When defined, will try to use a rule specific template instead of [`index.ejs`](src/templates/index.ejs), e.g. `"docs/templates/{name}.ejs"`. Same format as `docPath`.
115 |
116 | #### `globalTemplatePath`
117 | When defined, templates in this path will override the global templates defined in [`src/templates`](src/templates).
118 |
119 | #### `docLink` (default `false`)
120 | Add a link to the documentation source in the "Resources" section.
121 |
122 | #### `ruleLink` (default `true`)
123 | Add a link to the rule source in the "Resources" section. Requires `rulePath` to be defined.
124 |
125 | #### `testLink` (default `true`)
126 | Add a link to the rule's test source in the "Resources" section. Requires `testPath` to be defined.
127 |
128 | #### `pluginName` (default from package name)
129 | The name of your plugin as used in directives, e.g. `plugin:pluginName/rule`. Defaults to the name in `package.json` with `eslint-plugin-` stripped.
130 |
131 | #### `fixCodeExamples` (default `true`)
132 | Fix code examples using the ESLint configuration used for your `main` script.
133 |
134 | #### `showConfigComments` (default `false`)
135 | Shows config comments at the top of code examples:
136 | ```js
137 | /* eslint myPlugin/rule: "error" */
138 | // Test cases
139 | ```
140 |
141 | #### `showFixExamples` (default `true`)
142 | Show examples of how code is fixed by the rule.
143 |
144 | #### `showFilenames` (default: `false`)
145 | Show the relevant file name for test cases.
146 |
147 | #### `excludeExamplesByDefault` (default `false`)
148 | Exclude tests from being used as examples by default. When this is `true` users must set `docgen: true` on any test they want to be included in examples.
149 |
150 | #### `minExamples` (default `['warn', 2]`)
151 | Minimum examples per rule. Tuple where first value is one of `'warn'` or `'error'`, and the second value is the minimum number of examples required. Use `null` for no minimum.
152 |
153 | #### `maxExamples` (default `['warn', 50]`)
154 | Maximum examples per rule. Tuple where first value is one of `'warn'` or `'error'`, and the second value is the maximum number of examples allowed. Use `null` for no maximum.
155 |
156 | #### `tabWidth` (default `4`)
157 | Number of spaces to convert tabs to in code examples. Tabs in examples are always converted to spaces so their widths can be determined reliably for alignment.
158 |
159 | ## 🔍 Rules index
160 |
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).
162 |
163 | Note that the rule names do not include the plugin prefix.
164 |
165 | Example:
166 | ```js
167 | require( 'eslint-docgen' ).rulesWithConfig.get( 'no-event-shorthand' );
168 | // Outputs:
169 | {
170 | meta: [Object],
171 | create: [Function],
172 | configMap: Map {
173 | 'deprecated-3.5' => null,
174 | 'deprecated-3.3' => [ { allowAjaxEvents: true } ]
175 | }
176 | }
177 | ```
178 |
179 | ## ✏️ Examples
180 | * [Rule in eslint-plugin-no-jquery](https://github.com/wikimedia/eslint-plugin-no-jquery/blob/master/docs/rules/no-error-shorthand.md)
181 | * [Rule in eslint-plugin-mediawiki](https://github.com/wikimedia/eslint-plugin-mediawiki/blob/master/docs/rules/valid-package-file-require.md)
182 | * [Sample test case output](tests/cases/simple-rule.md)
183 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "eslint-docgen",
3 | "version": "0.7.1",
4 | "description": "Automatically generate ESLint plugin documentation from rule metadata and test cases.",
5 | "repository": {
6 | "type": "git",
7 | "url": "git+https://github.com/wikimedia/eslint-docgen.git"
8 | },
9 | "license": "MIT",
10 | "keywords": [
11 | "eslint",
12 | "documentation"
13 | ],
14 | "main": "src/index.js",
15 | "files": [
16 | "src"
17 | ],
18 | "scripts": {
19 | "test": "eslint . --cache && nyc mocha tests/",
20 | "report": "nyc report --reporter=html"
21 | },
22 | "engines": {
23 | "node": ">=16.0.0"
24 | },
25 | "dependencies": {
26 | "chalk": "^4.1.2",
27 | "ejs": "^3.1.10",
28 | "eslint": ">=8.0.0",
29 | "import-fresh": "^3.3.0",
30 | "jsonschema": "^1.4.1",
31 | "merge-options": "^3.0.4",
32 | "mkdirp": "^3.0.1",
33 | "pkg-dir": "^5.0.0",
34 | "pluralize": "^8.0.0",
35 | "simple-mock": "^0.8.0",
36 | "upath": "^2.0.1"
37 | },
38 | "devDependencies": {
39 | "eslint-config-wikimedia": "^0.27.0",
40 | "eslint-plugin-node": "^11.1.0",
41 | "mocha": "^10.4.0",
42 | "nyc": "^15.1.0"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/build-docs-from-tests.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const pluralize = require( 'pluralize' );
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 | };
12 |
13 | /**
14 | * Format a list of items with natural language
15 | *
16 | * Equivalent to:
17 | * new Intl.ListFormat( 'en', { type: 'conjunction' } );
18 | * However Intl.ListFormat not always available
19 | *
20 | * @param {string[]} list List of strings
21 | * @return {string} Concatenated list
22 | */
23 | function listFormatter( list ) {
24 | return list.reduce( ( acc, cur, i, arr ) =>
25 | acc + ( i === 0 ? '' : ( ( i === arr.length - 1 ) ? ' and ' : ', ' ) ) + cur,
26 | '' );
27 | }
28 |
29 | /**
30 | * Make a markdown link from a target and label
31 | *
32 | * @param {string} target Link target
33 | * @param {string} label Link label
34 | * @return {string} Markdown link
35 | */
36 | function mdLink( target, label ) {
37 | return '[' + label + '](' + target + ')';
38 | }
39 |
40 | async function buildDocsFromTests(
41 | name, ruleMeta, tests, configMap, config, globalTemplates, loadRuleTemplate, testerConfig
42 | ) {
43 |
44 | const messages = [];
45 | const docs = ruleMeta.docs || {};
46 |
47 | /**
48 | * Replace tabs with spaces.
49 | *
50 | * We can't reply on browsers to render tabs at at consistent width.
51 | *
52 | * @param {string} code
53 | * @return {string}
54 | */
55 | function fixTabs( code ) {
56 | return code.replace( /\t/g, ' '.repeat( config.tabWidth ) );
57 | }
58 |
59 | /**
60 | * Get code from test case
61 | *
62 | * @param {string|Object} test Test case
63 | * @return {string} Test case's code
64 | */
65 | function getCode( test ) {
66 | return typeof test === 'string' ? test : test.code;
67 | }
68 |
69 | /**
70 | * @param {string|Object} test Test case
71 | * @return {string|null} The base file name, or null if not set
72 | */
73 | function getFilename( test ) {
74 | if ( typeof test === 'string' || test.filename === undefined ) {
75 | // Either the test is just a code snippet, or no file name is set
76 | return null;
77 | }
78 | return path.basename( test.filename );
79 | }
80 |
81 | async function buildRuleDetails( testList, isValid, showFixes ) {
82 | testList = testList.filter( ( test ) =>
83 | test.docgen === undefined ?
84 | !config.excludeExamplesByDefault :
85 | test.docgen
86 | );
87 |
88 | let fixedCode, fixedOutput, maxCodeLength;
89 | const testsByOptions = {};
90 |
91 | const codeList = testList.map( getCode );
92 | if ( config.fixCodeExamples ) {
93 | const fix = require( './fix' );
94 | fixedCode = await fix.batchLintFix( codeList, testerConfig );
95 | } else {
96 | fixedCode = codeList;
97 | }
98 |
99 | fixedCode = fixedCode.map( fixTabs );
100 |
101 | if ( showFixes ) {
102 | // Calculate maxCodeLength for alignment
103 | maxCodeLength = fixedCode.reduce( ( acc, code ) =>
104 | code.split( '\n' ).reduce(
105 | ( lineAcc, line ) => Math.max( lineAcc, line.length ),
106 | acc
107 | ),
108 | 0 );
109 |
110 | const outputList = testList.map( ( test ) => test.output );
111 | if ( config.fixCodeExamples ) {
112 | const fix = require( './fix' );
113 | fixedOutput = await fix.batchLintFix( outputList, testerConfig );
114 | } else {
115 | fixedOutput = outputList;
116 | }
117 |
118 | fixedOutput = fixedOutput.map( fixTabs );
119 | }
120 |
121 | const codeSet = {};
122 | let previousMultiLine = false;
123 | testList.forEach( function ( test, i ) {
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 | }
134 | }
135 |
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 | } );
153 |
154 | const hash = JSON.stringify( hashObject );
155 |
156 | codeSet[ hash ] = codeSet[ hash ] || {};
157 |
158 | let example = '';
159 | let multiLine = false;
160 | const code = fixedCode[ i ];
161 | if ( !showFixes && Object.prototype.hasOwnProperty.call( codeSet[ hash ], code ) ) {
162 | messages.push( {
163 | type: 'warn',
164 | text: 'Duplicate code example found, examples can be hidden with `docgen: false`:\n' +
165 | getCode( testList[ codeSet[ hash ][ code ] ] ) + '\n' +
166 | getCode( testList[ i ] )
167 | } );
168 | }
169 |
170 | codeSet[ hash ][ code ] = i;
171 |
172 | testsByOptions[ hash ] = testsByOptions[ hash ] ||
173 | {
174 | tests: [],
175 | optionsAndSettings: optionsAndSettings
176 | };
177 |
178 | if ( showFixes && test.output ) {
179 | const codeLines = code.split( '\n' );
180 | const outputLines = fixedOutput[ i ].split( '\n' );
181 | const maxLines = Math.max( codeLines.length, outputLines.length );
182 | const exampleLines = [];
183 | for ( let l = 0; l < maxLines; l++ ) {
184 | const codeLine = codeLines[ l ] || '';
185 | const outputLine = outputLines[ l ] || '';
186 | exampleLines.push(
187 | codeLine + ' '.repeat( Math.max( 0, maxCodeLength - codeLine.length ) ) +
188 | ' /* → */' +
189 | ( outputLine ? ' ' + outputLine : '' )
190 | );
191 | }
192 | example = exampleLines.join( '\n' );
193 | multiLine = maxLines > 1;
194 | } else {
195 | example = code;
196 | multiLine = code.split( '\n' ).length > 1;
197 | }
198 | // Put linebreaks around multi-line examples
199 | if ( testsByOptions[ hash ].tests.length && ( multiLine || previousMultiLine ) ) {
200 | example = '\n' + example;
201 | }
202 | testsByOptions[ hash ].tests.push( example );
203 | previousMultiLine = multiLine;
204 | } );
205 |
206 | let comments = {};
207 | if ( config.showConfigComments ) {
208 | comments = Object.keys( testsByOptions ).map( ( key ) => {
209 | const optionsAndSettings = testsByOptions[ key ].optionsAndSettings;
210 | const value = optionsAndSettings && optionsAndSettings.options ?
211 | [ 'error', optionsAndSettings.options ] :
212 | 'error';
213 | return '/*eslint ' + config.pluginName + '/' + name + ': ' + JSON.stringify( value ) + '*/';
214 | } );
215 | if ( config.fixCodeExamples ) {
216 | const fix = require( './fix' );
217 | // Fixes whitespace in block comment. Too expensive for such a small fix?
218 | comments = await fix.batchLintFix( comments, testerConfig );
219 | }
220 | }
221 |
222 | return Object.keys( testsByOptions ).map( ( key, i ) => {
223 | const section = testsByOptions[ key ];
224 | const optionsAndSettings = section.optionsAndSettings;
225 | const lang = optionsAndSettings && optionsAndSettings.lang;
226 | const options = optionsAndSettings && optionsAndSettings.options;
227 | const settings = optionsAndSettings && optionsAndSettings.settings;
228 | const filename = optionsAndSettings && optionsAndSettings.filename;
229 |
230 | let examples = '```' + lang + '\n';
231 | if ( config.showConfigComments ) {
232 | examples += comments[ i ] + '\n';
233 | }
234 | examples += section.tests.join( '\n' );
235 | examples += '\n```';
236 |
237 | return {
238 | key: key,
239 | valid: isValid,
240 | options: options ? JSON.stringify( options ) : '',
241 | settings: settings ? JSON.stringify( settings ) : '',
242 | filename: filename || '',
243 | examples: examples,
244 | testCount: section.tests.length
245 | };
246 | } );
247 | }
248 |
249 | if ( !docs.description ) {
250 | messages.push( { type: 'warn', text: 'No description found in rule metadata' } );
251 | }
252 |
253 | let replacedByLinks = '';
254 | if ( docs.deprecated && docs.replacedBy ) {
255 | replacedByLinks = listFormatter(
256 | docs.replacedBy.map( ( ruleName ) => mdLink( ruleName + '.md', '`' + ruleName + '`' ) )
257 | );
258 | }
259 |
260 | let inConfigs = [];
261 | if ( configMap ) {
262 | inConfigs = Array.from( configMap.keys() ).map( ( configName ) => {
263 | const options = configMap.get( configName );
264 | return {
265 | config: configName,
266 | options: options && Object.keys( options[ 0 ] ).length ? JSON.stringify( options ) : ''
267 | };
268 | } );
269 | }
270 |
271 | const invalid = await buildRuleDetails( tests.invalid, false );
272 | const valid = await buildRuleDetails( tests.valid, true );
273 |
274 | const validInvalid = invalid.concat( valid ).sort( ( a, b ) => {
275 | return a.key === b.key ?
276 | ( a.valid < b.valid ? -1 : 1 ) :
277 | ( a.key < b.key ? -1 : 1 );
278 | } );
279 |
280 | let fixed = [];
281 | if ( ruleMeta.fixable && config.showFixExamples ) {
282 | fixed = await buildRuleDetails(
283 | tests.invalid.filter( ( test ) => !!test.output ),
284 | false,
285 | true
286 | );
287 | }
288 |
289 | const exampleCount = invalid.concat( valid ).concat( fixed )
290 | .reduce( ( acc, section ) => acc + section.testCount, 0 );
291 |
292 | if ( config.minExamples && exampleCount < config.minExamples[ 1 ] ) {
293 | messages.push( {
294 | type: config.minExamples[ 0 ],
295 | text: exampleCount + ' ' + pluralize( 'example', exampleCount ) + ' found, expected at least ' + config.minExamples[ 1 ] + '.',
296 | label: 'config.minExamples'
297 | } );
298 | }
299 | if ( config.maxExamples && exampleCount > config.maxExamples[ 1 ] ) {
300 | messages.push( {
301 | type: config.maxExamples[ 0 ],
302 | text: exampleCount + ' ' + pluralize( 'example', exampleCount ) + ' found, expected fewer than ' + config.maxExamples[ 1 ] + '.',
303 | label: 'config.maxExamples'
304 | } );
305 | }
306 |
307 | /**
308 | * Code path with substituted rule name
309 | *
310 | * @param {string} pattern Code path with {name} as placeholder for the rule name
311 | * @param {string} ruleName Rule name
312 | * @return {string}
313 | */
314 | function codeLink( pattern, ruleName ) {
315 | const filePath = pattern.replace( '{name}', ruleName );
316 | return path.join( '/', filePath );
317 | }
318 |
319 | let output;
320 | let index = globalTemplates.index;
321 |
322 | if ( config.ruleTemplatePath ) {
323 | const fs = require( 'fs' );
324 | const packagePath = require( './package-path' );
325 | const ruleTemplatePath = packagePath( config.ruleTemplatePath.replace( '{name}', name ) );
326 | // eslint-disable-next-line security/detect-non-literal-fs-filename
327 | if ( fs.existsSync( ruleTemplatePath ) ) {
328 | index = loadRuleTemplate( ruleTemplatePath );
329 | }
330 | }
331 |
332 | try {
333 | output = index( {
334 | // root
335 | description: docs.description,
336 | title: name,
337 | // deprecated
338 | deprecated: docs.deprecated,
339 | replacedByLinks: replacedByLinks,
340 | // inConfigs
341 | inConfigs: inConfigs,
342 | pluginName: config.pluginName,
343 | // fixable
344 | fixable: !!ruleMeta.fixable,
345 | // examples
346 | fixed: fixed,
347 | validInvalid: validInvalid,
348 | invalid: invalid,
349 | valid: valid,
350 | // resources
351 | linkDoc: config.docLink ? codeLink( config.docPath, name ) : '',
352 | linkRule: config.ruleLink ? codeLink( config.rulePath, name ) : '',
353 | linkTest: config.testLink ? codeLink( config.testPath, name ) : ''
354 | } ).replace( /\n{3,}/g, '\n\n' ).trim() + '\n';
355 | } catch ( e ) {
356 | // TODO: Test template errors
357 | // istanbul ignore next
358 | messages.push( { type: 'error', text: e } );
359 | }
360 |
361 | return {
362 | output: output,
363 | messages: messages
364 | };
365 | }
366 |
367 | module.exports = buildDocsFromTests;
368 |
--------------------------------------------------------------------------------
/src/config-schema.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "object",
3 | "properties": {
4 | "pluginName": { "type": "string" },
5 | "fixCodeExamples": { "type": "boolean" },
6 | "showConfigComments": { "type": "boolean" },
7 | "showFixExamples": { "type": "boolean" },
8 | "showFilenames": { "type": "boolean" },
9 | "tabWidth": {
10 | "type": "integer",
11 | "minimum": 0
12 | },
13 | "docPath": { "$ref": "#/definitions/path" },
14 | "rulePath": { "$ref": "#/definitions/nullablePath" },
15 | "testPath": { "$ref": "#/definitions/nullablePath" },
16 | "ruleTemplatePath": { "$ref": "#/definitions/nullablePath" },
17 | "globalTemplatePath": {
18 | "type": [ "string", "null" ]
19 | },
20 | "templatePath": {
21 | "type": "null",
22 | "message": "must be renamed to \"globalTemplatePath\""
23 | },
24 | "docLink": { "type": "boolean" },
25 | "ruleLink": { "type": "boolean" },
26 | "testLink": { "type": "boolean" },
27 | "excludeExamplesByDefault": { "type": "boolean" },
28 | "minExamples": { "$ref": "#/definitions/minMaxExamples" },
29 | "maxExamples": { "$ref": "#/definitions/minMaxExamples" }
30 | },
31 | "allOf": [
32 | {
33 | "anyOf": [
34 | {
35 | "properties": {
36 | "ruleLink": { "const": false }
37 | }
38 | },
39 | {
40 | "not": {
41 | "properties": {
42 | "rulePath": { "const": null }
43 | }
44 | }
45 | }
46 | ],
47 | "message": "does not have rulePath when ruleLink is true"
48 | },
49 | {
50 | "anyOf": [
51 | {
52 | "properties": {
53 | "testLink": { "const": false }
54 | }
55 | },
56 | {
57 | "not": {
58 | "properties": {
59 | "testPath": { "const": null }
60 | }
61 | }
62 | }
63 | ],
64 | "message": "does not have testPath when testLink is true"
65 | }
66 | ],
67 | "definitions": {
68 | "path": {
69 | "type": "string",
70 | "pattern": "\\{name\\}",
71 | "message": "must contain \"{name}\""
72 | },
73 | "nullablePath": {
74 | "anyOf": [
75 | { "$ref": "#/definitions/path" },
76 | { "type": "null" }
77 | ],
78 | "message": "must contain \"{name}\" or be null"
79 | },
80 | "minMaxExamples": {
81 | "anyOf": [
82 | {
83 | "type": "array",
84 | "items": [
85 | {
86 | "enum": [ "warn", "error" ]
87 | },
88 | {
89 | "type": "integer",
90 | "minimum": 0
91 | }
92 | ]
93 | },
94 | {
95 | "type": "null"
96 | }
97 | ],
98 | "message": "must be a tuple containing \"warn\"/\"error\" and a positive integer, or be null"
99 | }
100 | },
101 | "additionalProperties": false
102 | }
103 |
--------------------------------------------------------------------------------
/src/default-config.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const packagePath = require( './package-path' );
4 | // eslint-disable-next-line security/detect-non-literal-require
5 | const packageName = require( packagePath( './package' ) ).name;
6 | const naming = require( './naming' );
7 | const pluginName = naming.getShorthandName( packageName, 'eslint-plugin' );
8 |
9 | module.exports = {
10 | pluginName: pluginName,
11 | fixCodeExamples: true,
12 | showConfigComments: false,
13 | showFilenames: false,
14 | showFixExamples: true,
15 | tabWidth: 4,
16 | docPath: null,
17 | rulePath: null,
18 | testPath: null,
19 | ruleTemplatePath: null,
20 | globalTemplatePath: null,
21 | docLink: false,
22 | ruleLink: true,
23 | testLink: true,
24 | excludeExamplesByDefault: false,
25 | minExamples: [ 'warn', 2 ],
26 | maxExamples: [ 'warn', 50 ]
27 | };
28 |
--------------------------------------------------------------------------------
/src/fix.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const { Linter, ESLint } = require( 'eslint' );
4 | const linter = new Linter();
5 | const eslint = new ESLint();
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 |
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 | }
21 | }
22 | } );
23 | config.rules = fixableRules;
24 |
25 | return config;
26 | }
27 |
28 | /**
29 | * Lint and fix some code
30 | *
31 | * @param {string} code Code
32 | * @param {Object} testerConfig Config
33 | * @return {string} Fixed code
34 | */
35 | async function lintFix( code, testerConfig ) {
36 | const mergedConfig = mergeOptions( await getConfig(), testerConfig );
37 |
38 | // TODO
39 | // istanbul ignore next
40 | if ( typeof mergedConfig.parser === 'string' ) {
41 | // eslint-disable-next-line security/detect-non-literal-require
42 | linter.defineParser( mergedConfig.parser, require( mergedConfig.parser ) );
43 | }
44 |
45 | const result = linter.verifyAndFix( code, mergedConfig );
46 | const err = result.messages.find( ( message ) => message.fatal );
47 | // TODO
48 | // istanbul ignore next
49 | if ( err ) {
50 | const line = code.split( '\n' )[ err.line - 1 ];
51 | throw new Error( err.message + ':\n' + line );
52 | }
53 | return result.output;
54 | }
55 |
56 | /**
57 | * Lint and fix a collection of code snippets
58 | *
59 | * Concatenates the code snippets into one code block to improve performance
60 | *
61 | * @param {string[]} codeList Code list
62 | * @param {Object} testerConfig Config
63 | * @return {string[]} Fixed code list
64 | */
65 | async function batchLintFix( codeList, testerConfig ) {
66 | const separator = '\n/* - */\n';
67 | const codeBlock = codeList.join( ';' + separator );
68 | // Add an extra semicolon to avoid syntax error
69 | const fixed = await lintFix( codeBlock, testerConfig );
70 | return fixed.split( separator ).map( ( code ) => code.trim() );
71 | }
72 |
73 | module.exports = {
74 | lintFix: lintFix,
75 | batchLintFix: batchLintFix
76 | };
77 |
--------------------------------------------------------------------------------
/src/formatter.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const chalk = require( 'chalk' );
4 |
5 | /**
6 | * Add a dim "label" suffix
7 | *
8 | * @param {string} label Label
9 | * @return {string}
10 | */
11 | function labelSuffix( label ) {
12 | return label ? ' ' + chalk.dim( label ) : '';
13 | }
14 |
15 | /**
16 | * Indent every line except the first one by a fixed amount
17 | *
18 | * @param {string} text Text to indent
19 | * @param {number} indent Characters to indent by
20 | * @return {string}
21 | */
22 | function indentAfterFirst( text, indent ) {
23 | return text.split( '\n' ).map( ( line, i ) =>
24 | i ? ' '.repeat( indent ) + line : line
25 | ).join( '\n' );
26 | }
27 |
28 | module.exports = {
29 | /**
30 | * Format a message as a warning
31 | *
32 | * @param {string} msg Message
33 | * @param {string} label Label suffix
34 | * @return {string}
35 | */
36 | warn: ( msg, label ) => ' ' + chalk.yellow( 'warning' ) + ' ' + indentAfterFirst( msg, 11 ) + labelSuffix( label ),
37 | /**
38 | * Format a message as an error
39 | *
40 | * @param {string} msg Message
41 | * @param {string} label Label suffix
42 | * @return {string}
43 | */
44 | error: ( msg, label ) => ' ' + chalk.red( 'error' ) + ' ' + indentAfterFirst( msg, 9 ) + labelSuffix( label ),
45 | /**
46 | * Format a message as a heading
47 | *
48 | * @param {string} msg Message
49 | * @return {string}
50 | */
51 | heading: ( msg ) => chalk.underline( msg )
52 | };
53 |
--------------------------------------------------------------------------------
/src/get-config.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const fs = require( 'fs' );
4 | const packagePath = require( './package-path' );
5 | const importFresh = require( 'import-fresh' );
6 |
7 | const configFilenames = [
8 | '.eslintdocgenrc.js',
9 | '.eslintdocgenrc.json'
10 | ];
11 |
12 | /**
13 | * Get the eslintdocgenrc config from the current working directory
14 | *
15 | * Missing values are backfilled with the defaults
16 | *
17 | * @return {Object} Config
18 | */
19 | function getConfig() {
20 | let config, configPath;
21 |
22 | configFilenames.some( ( configFilename ) => {
23 | configPath = packagePath( configFilename );
24 |
25 | // eslint-disable-next-line security/detect-non-literal-fs-filename
26 | if ( fs.existsSync( configPath ) ) {
27 | config = importFresh( configPath );
28 | return true;
29 | }
30 | return false;
31 | } );
32 |
33 | if ( !config ) {
34 | throw new Error( '.eslintdocgenrc not found' );
35 | }
36 |
37 | const defaultConfig = require( './default-config.js' );
38 | config = Object.assign( {}, defaultConfig, config );
39 |
40 | return {
41 | config: config,
42 | configPath: configPath
43 | };
44 | }
45 |
46 | module.exports = getConfig;
47 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = {
4 | RuleTester: require( './rule-tester' ),
5 | rulesWithConfig: require( './rules-with-config' )
6 | };
7 |
--------------------------------------------------------------------------------
/src/load-templates.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const fs = require( 'fs' );
4 | const path = require( 'upath' );
5 | const ejs = require( 'ejs' );
6 |
7 | function loadTemplatesFromPath( dirPath ) {
8 | // eslint-disable-next-line security/detect-non-literal-fs-filename
9 | const files = fs.readdirSync( dirPath );
10 | const templates = {};
11 | files.forEach( ( filename ) => {
12 | templates[ path.parse( filename ).name ] =
13 | // eslint-disable-next-line security/detect-non-literal-fs-filename
14 | fs.readFileSync( path.join( dirPath, filename ) ).toString();
15 | } );
16 | return templates;
17 | }
18 |
19 | /**
20 | * Load templates from a list of paths
21 | *
22 | * @param {string[]} dirPaths Paths
23 | * @return {Object} Keyed object with compiled EJS templates (globalTemplates),
24 | * and a function to load per-rule templates (loadRuleTemplate)
25 | */
26 | function loadTemplates( dirPaths ) {
27 | const templateStrings = {};
28 | const globalTemplates = {};
29 | dirPaths.forEach( ( dirPath ) => {
30 | Object.assign( templateStrings, loadTemplatesFromPath( dirPath ) );
31 | } );
32 | const hasOwn = Object.prototype.hasOwnProperty;
33 |
34 | function compile( string, filename ) {
35 | const compiled = ejs.compile( string, { client: true } );
36 | return ( data ) =>
37 | compiled( data, null, function ( template, includeData ) {
38 | const mergedData = Object.assign( {}, data, includeData );
39 | if ( !hasOwn.call( templateStrings, template ) ) {
40 | throw new Error( 'Template `' + template + '` not found in template `' + filename + '`' );
41 | }
42 | return globalTemplates[ template ]( mergedData );
43 | } );
44 | }
45 |
46 | Object.keys( templateStrings ).forEach( ( filename ) => {
47 | globalTemplates[ filename ] = compile( templateStrings[ filename ], filename );
48 | } );
49 |
50 | function loadRuleTemplate( ruleTemplatePath ) {
51 | return compile(
52 | // eslint-disable-next-line security/detect-non-literal-fs-filename
53 | fs.readFileSync( ruleTemplatePath ).toString()
54 | );
55 | }
56 |
57 | return { globalTemplates, loadRuleTemplate };
58 | }
59 |
60 | module.exports = loadTemplates;
61 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const packagePath = require( './package-path' );
4 | // eslint-disable-next-line security/detect-non-literal-require
5 | const main = require( packagePath( './package' ) ).main || 'index.js';
6 |
7 | module.exports = packagePath( main );
8 |
--------------------------------------------------------------------------------
/src/naming.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // istanbul ignore file
4 | // Method is tested upstream
5 |
6 | /**
7 | * Removes the prefix from a fullname.
8 | *
9 | * Copied from https://github.com/eslint/eslint/blob/master/lib/shared/naming.js
10 | *
11 | * @param {string} fullname The term which may have the prefix.
12 | * @param {string} prefix The prefix to remove.
13 | * @return {string} The term without prefix.
14 | */
15 | function getShorthandName( fullname, prefix ) {
16 | if ( fullname[ 0 ] === '@' ) {
17 | // eslint-disable-next-line security/detect-non-literal-regexp
18 | let matchResult = new RegExp( `^(@[^/]+)/${ prefix }$`, 'u' ).exec( fullname );
19 |
20 | if ( matchResult ) {
21 | return matchResult[ 1 ];
22 | }
23 |
24 | // eslint-disable-next-line security/detect-non-literal-regexp
25 | matchResult = new RegExp( `^(@[^/]+)/${ prefix }-(.+)$`, 'u' ).exec( fullname );
26 | if ( matchResult ) {
27 | return `${ matchResult[ 1 ] }/${ matchResult[ 2 ] }`;
28 | }
29 | } else if ( fullname.startsWith( `${ prefix }-` ) ) {
30 | return fullname.slice( prefix.length + 1 );
31 | }
32 |
33 | return fullname;
34 | }
35 |
36 | module.exports = {
37 | getShorthandName: getShorthandName
38 | };
39 |
--------------------------------------------------------------------------------
/src/package-path.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const pkgDir = require( 'pkg-dir' );
4 | const path = require( 'upath' );
5 | const dir = pkgDir.sync();
6 |
7 | function packagePath( p ) {
8 | return path.join( dir, p );
9 | }
10 |
11 | module.exports = packagePath;
12 |
--------------------------------------------------------------------------------
/src/rule-tester.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const packagePath = require( './package-path' );
4 | // Use plugin's version of ESLint
5 | // eslint-disable-next-line security/detect-non-literal-require
6 | const ESLintRuleTester = require( packagePath( 'node_modules/eslint' ) ).RuleTester;
7 | const inDocMode = !!process.env.DOCGEN;
8 |
9 | /**
10 | * Extends ESLint's RuleTester to also build documentation
11 | */
12 | class RuleTester extends ESLintRuleTester {
13 | run( name, rule, tests ) {
14 | if ( inDocMode ) {
15 | RuleTester.it( name, ( done ) => {
16 | const writeDocsFromTests = require( './write-docs-from-tests' );
17 | writeDocsFromTests( name, rule, tests, this.testerConfig, done );
18 | } );
19 | } else {
20 | // Filter out invalid property "docgen"
21 | // (used in documentation building mode).
22 | tests.valid.forEach( ( test ) => {
23 | delete test.docgen;
24 | } );
25 | tests.invalid.forEach( ( test ) => {
26 | delete test.docgen;
27 | } );
28 |
29 | // Filter out invalid top level property "docgenConfig"
30 | // (used in documentation building mode).
31 | delete tests.docgenConfig;
32 |
33 | return super.run.call( this, name, rule, tests );
34 | }
35 | }
36 | }
37 |
38 | module.exports = RuleTester;
39 |
--------------------------------------------------------------------------------
/src/rules-with-config.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // eslint-disable-next-line security/detect-non-literal-require
4 | const main = require( require( './main' ) );
5 | const pluginName = require( './get-config' )().config.pluginName;
6 | const configs = main.configs;
7 | const rulesWithConfig = new Map( Object.entries( main.rules ) );
8 |
9 | // Add a configMap property to the rulesWithConfig
10 | rulesWithConfig.forEach( ( rule, name ) => {
11 | rulesWithConfig.get( name ).configMap = new Map();
12 | } );
13 |
14 | // Iterate over configs to add config data to map
15 | for ( const name in configs ) {
16 | const rules = configs[ name ].rules || {};
17 | for ( const fullName in rules ) {
18 | // Configs use the full rule name with the plugin prefix, so remove this
19 | const shortName = fullName.slice( pluginName.length + 1 );
20 | rulesWithConfig.get( shortName ).configMap.set(
21 | name,
22 | Array.isArray( rules[ fullName ] ) ? rules[ fullName ].slice( 1 ) : null
23 | );
24 | }
25 | }
26 |
27 | module.exports = rulesWithConfig;
28 |
--------------------------------------------------------------------------------
/src/templates/deprecated.ejs:
--------------------------------------------------------------------------------
1 | <% if ( deprecated ) { %>
2 | ⚠️ This rule is deprecated.<% if ( replacedByLinks ) { %> Use <%- replacedByLinks %> instead.<% } %>
3 | <% } %>
--------------------------------------------------------------------------------
/src/templates/exampleFixed.ejs:
--------------------------------------------------------------------------------
1 | 🔧 Examples of code **fixed** by this rule<%- include( 'optionsAndSettings', { data: section } ) %>:
--------------------------------------------------------------------------------
/src/templates/exampleInvalid.ejs:
--------------------------------------------------------------------------------
1 | ❌ Examples of **incorrect** code<%- include( 'optionsAndSettings', { data: section } ) %>:
--------------------------------------------------------------------------------
/src/templates/exampleValid.ejs:
--------------------------------------------------------------------------------
1 | ✔️ Examples of **correct** code<%- include( 'optionsAndSettings', { data: section } ) %>:
--------------------------------------------------------------------------------
/src/templates/examples.ejs:
--------------------------------------------------------------------------------
1 | <% validInvalid.forEach( ( section ) => { %>
2 | <%- include( section.valid ? 'exampleValid' : 'exampleInvalid', { section: section } ) %>
3 | <%- section.examples %>
4 | <% } ) %>
5 |
6 | <% fixed.forEach( ( section ) => { %>
7 | <%- include( 'exampleFixed', { section: section } ) %>
8 | <%- section.examples %>
9 | <% } ) %>
--------------------------------------------------------------------------------
/src/templates/fixable.ejs:
--------------------------------------------------------------------------------
1 | <% if ( fixable ) { %>
2 | 🔧 The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
3 | <% } %>
--------------------------------------------------------------------------------
/src/templates/inConfig.ejs:
--------------------------------------------------------------------------------
1 | 📋 This rule is enabled in `plugin:<%- pluginName %>/<%- inConfig.config %>`<%- include( 'optionsAndSettings', { data: inConfig } ) %>.
--------------------------------------------------------------------------------
/src/templates/inConfigs.ejs:
--------------------------------------------------------------------------------
1 | <% inConfigs.forEach( ( inConfig ) => { %>
2 | <%- include( 'inConfig', { inConfig: inConfig } ) %>
3 | <% } ) %>
--------------------------------------------------------------------------------
/src/templates/index.ejs:
--------------------------------------------------------------------------------
1 | [//]: # (This file is generated by eslint-docgen. Do not edit it directly.)
2 |
3 | # <%- title %>
4 |
5 | <%- description %>
6 |
7 | <%- include( 'deprecated' ) %>
8 |
9 | <%- include( 'inConfigs' ) %>
10 |
11 | <%- include( 'fixable' ) %>
12 |
13 | ## Rule details
14 |
15 | <%- include( 'examples' ) %>
16 |
17 | <%- include( 'resources' ) %>
18 |
--------------------------------------------------------------------------------
/src/templates/optionsAndSettings.ejs:
--------------------------------------------------------------------------------
1 | <% if ( data.options || data.settings ) { _%>
2 | with<% if ( data.options ) { %> `<%- data.options %>` options<% } _%>
3 | <% if ( data.settings ) { %><% if ( data.options ) { %> and<% } %> `<%- data.settings %>` settings<% } _%>
4 | <% } _%>
5 | <% if ( data.filename ) { _%>
6 | for a file named `<%- data.filename %>`<% } %>
--------------------------------------------------------------------------------
/src/templates/resources.ejs:
--------------------------------------------------------------------------------
1 | <% if ( linkRule || linkTest || linkDoc ) { %>
2 | ## Resources
3 |
4 | <% if ( linkRule ) { %>
5 | * [Rule source](<%- linkRule %>)<% } -%>
6 | <% if ( linkTest ) { %>
7 | * [Test source](<%- linkTest %>)<% } -%>
8 | <% if ( linkDoc ) { %>
9 | * [Documentation source](<%- linkDoc %>)<% } -%>
10 | <% } %>
--------------------------------------------------------------------------------
/src/validate-config.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Validator = require( 'jsonschema' ).Validator;
4 | const v = new Validator();
5 | const schema = require( './config-schema' );
6 |
7 | /**
8 | * Validate a config against the schema
9 | *
10 | * @param {Object} config Config
11 | * @return {string[]} A list of errors found
12 | */
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 );
16 |
17 | return errors;
18 | }
19 |
20 | module.exports = validateConfig;
21 |
--------------------------------------------------------------------------------
/src/write-docs-from-tests.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const fs = require( 'fs' );
4 | const mkdirp = require( 'mkdirp' );
5 | const path = require( 'upath' );
6 | const buildDocsFromTests = require( './build-docs-from-tests' );
7 |
8 | const formatter = require( './formatter' );
9 | const rulesWithConfig = require( './rules-with-config' );
10 |
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 | }
18 |
19 | const configValidator = require( './validate-config' );
20 | function assertValidConfig( maybeValidConfig, configSource ) {
21 | const configErrors = configValidator( maybeValidConfig );
22 | if ( configErrors.length ) {
23 | throw new Error( [ formatter.heading( configSource ), ...configErrors.map( formatter.error ) ].join( '\n' ) );
24 | }
25 | }
26 |
27 | assertValidConfig( config, configPath );
28 |
29 | const packagePath = require( './package-path' );
30 |
31 | const loadTemplates = require( './load-templates' );
32 | const templatePaths = [ path.join( __dirname, 'templates' ) ];
33 | if ( config.globalTemplatePath ) {
34 | templatePaths.push( packagePath( config.globalTemplatePath ) );
35 | }
36 | const { globalTemplates, loadRuleTemplate } = loadTemplates( templatePaths );
37 |
38 | async function writeDocsFromTests( name, rule, tests, testerConfig, done ) {
39 | // If the tests have a `docgenConfig` property, this overrides the global configuration
40 | let configForRule = config;
41 | if ( tests.docgenConfig !== undefined ) {
42 | configForRule = Object.assign( {}, config, tests.docgenConfig );
43 | assertValidConfig( configForRule, 'Rule specific config for ' + name );
44 | delete tests.docgenConfig;
45 | }
46 | const outputPath = packagePath( configForRule.docPath.replace( '{name}', name ) );
47 | const ruleWithConfig = rulesWithConfig.get( name );
48 | if ( !ruleWithConfig ) {
49 | throw new Error( [ formatter.heading( outputPath ), formatter.error( 'Rule not found.' ) ].join( '\n' ) );
50 | }
51 | const configMap = rulesWithConfig.get( name ).configMap;
52 | let output, messages;
53 | try {
54 | ( { output, messages } = await buildDocsFromTests(
55 | name, rule.meta, tests, configMap, configForRule,
56 | globalTemplates, loadRuleTemplate, testerConfig
57 | ) );
58 | } catch ( e ) {
59 | throw new Error( [ formatter.heading( outputPath ), formatter.error( e.message ) ].join( '\n' ) );
60 | }
61 |
62 | const outputDir = path.dirname( outputPath );
63 | mkdirp( outputDir ).then( () => {
64 | // eslint-disable-next-line security/detect-non-literal-fs-filename
65 | fs.writeFile(
66 | outputPath,
67 | output,
68 | ( err ) => {
69 | if ( err ) {
70 | messages.push( { type: 'error', text: err.toString() } );
71 | }
72 | if ( messages.length ) {
73 | console.log();
74 | console.log( formatter.heading( outputPath ) );
75 | messages.forEach( ( message ) =>
76 | console.log( formatter[ message.type ]( message.text, message.label ) )
77 | );
78 | console.log();
79 | }
80 |
81 | const errors = messages.filter( ( message ) => message.type === 'error' );
82 |
83 | if ( errors.length ) {
84 | throw new Error( errors.map( formatter.error ).join( '\n' ) );
85 | }
86 |
87 | done();
88 | }
89 | );
90 | } );
91 | }
92 |
93 | module.exports = writeDocsFromTests;
94 |
--------------------------------------------------------------------------------
/tests/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "wikimedia/mocha"
3 | }
4 |
--------------------------------------------------------------------------------
/tests/build-docs-from-tests.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const assert = require( 'assert' );
4 | const fs = require( 'fs' );
5 | const path = require( 'upath' );
6 | const testUtils = require( './test-utils' );
7 |
8 | /**
9 | * Returns the contents that would be appropriate for an overall
10 | * .vue file, with the scriptContents being contained between
11 | * tags.
12 | *
13 | * @param {string} scriptContents
14 | * @return {string}
15 | */
16 | function makeVueFileContent( scriptContents ) {
17 | return `
18 | Placeholder...
19 |
20 | `;
23 | }
24 |
25 | /* eslint-disable mocha/no-setup-in-describe */
26 |
27 | describe( 'buildDocsFromTests', () => {
28 | const htmlFilename = path.resolve( __dirname, '../sandbox/test.html' );
29 | const jsFilename = path.resolve( __dirname, '../sandbox/test.js' );
30 | const tsFilename = path.resolve( __dirname, '../sandbox/test.ts' );
31 | const vueFilename = path.resolve( __dirname, '../sandbox/test.vue' );
32 |
33 | const noDesc = { type: 'warn', text: 'No description found in rule metadata' };
34 | const cases = [
35 | {
36 | description: 'simple-rule.md: Basic features with default settings',
37 | name: 'my-rule',
38 | ruleMeta: {
39 | docs: {
40 | description: 'My rule enforces a thing',
41 | deprecated: true,
42 | replacedBy: [ 'my-new-rule', 'my-other-rule', 'third-rule' ]
43 | },
44 | fixable: 'code'
45 | },
46 | tests: {
47 | valid: [
48 | 'var x = "123"',
49 | 'var y = "45678"',
50 | 'var z = { a: 3, ...b }',
51 | {
52 | code: 'var z1 = "123"',
53 | options: [ { myOption: true } ]
54 | },
55 | {
56 | code: 'var z2 = "123"',
57 | options: [ { myOption: true } ]
58 | },
59 | {
60 | code: 'var x = "1,23"',
61 | settings: [ { lang: 'fr' } ]
62 | },
63 | {
64 | code: 'var z1 = "1,23"',
65 | options: [ { myOption: true } ],
66 | settings: [ { lang: 'fr' } ]
67 | }
68 | ],
69 | invalid: [
70 | {
71 | code: 'var x = "1.23"',
72 | output: 'var x = "123"'
73 | },
74 | {
75 | code: 'var y = "4.5678"',
76 | output: 'var y = "45678"'
77 | },
78 | {
79 | code: 'multi\n.line\n.case',
80 | output: 'Multi\n.Line.Case;'
81 | },
82 | {
83 | code: 'multi.line.case',
84 | output: 'Multi\n.Line\n.Case;'
85 | },
86 | {
87 | code: 'singleAfterMulti;',
88 | output: 'SingleAfterMulti;'
89 | },
90 | {
91 | code: 'var z1 = "1.23"',
92 | options: [ { myOption: true } ],
93 | output: 'var z1 = "123"'
94 | },
95 | {
96 | code: 'var z2 = "1.23"',
97 | options: [ { myOption: true } ],
98 | output: 'var z2 = "123"'
99 | },
100 | {
101 | // Parser error shouldn't be a problem if docgen is false
102 | code: 'parser:::error',
103 | docgen: false
104 | }
105 | ]
106 | },
107 | testerConfig: {
108 | parserOptions: { ecmaVersion: 2019 }
109 | },
110 | configMap: new Map( Object.entries( {
111 | recommended: [ { myOption: true } ],
112 | strict: null
113 | } ) ),
114 | expected: 'cases/simple-rule.md'
115 | },
116 | {
117 | description: 'syntax-lang.md: Different syntax languages',
118 | name: 'syntax-lang',
119 | ruleMeta: {
120 | docs: {
121 | description: 'Syntax language set from filename extension, fixCodeExamples:false'
122 | }
123 | },
124 | tests: {
125 | valid: [
126 | 'var x = 123;',
127 | 'var y = 456;',
128 | {
129 | code: 'var jsX = 123;',
130 | filename: jsFilename
131 | },
132 | {
133 | code: 'var jsY = 456;',
134 | filename: jsFilename
135 | },
136 | {
137 | code: makeVueFileContent( 'var vueZ = 789;' ),
138 | filename: vueFilename
139 | },
140 | {
141 | code: 'function tsF(x: T): T { return x; }',
142 | filename: tsFilename
143 | },
144 | {
145 | code: '',
146 | filename: htmlFilename
147 | }
148 | ],
149 | invalid: [
150 | {
151 | code: 'var tsX = 123;',
152 | filename: jsFilename
153 | },
154 | {
155 | code: 'var tsY = 456;',
156 | filename: jsFilename
157 | },
158 | {
159 | code: makeVueFileContent( 'var jsZ = 789;' ),
160 | filename: vueFilename
161 | },
162 | {
163 | code: makeVueFileContent( 'var tsZ = 789;' ),
164 | filename: vueFilename
165 | },
166 | {
167 | code: 'function jsF(x: T): T { return x; }',
168 | filename: tsFilename
169 | },
170 | {
171 | code: '',
172 | filename: htmlFilename
173 | }
174 | ]
175 | },
176 | config: {
177 | fixCodeExamples: false
178 | },
179 | expected: 'cases/syntax-lang.md'
180 | },
181 | {
182 | description: 'file-names.md: Show filenames',
183 | name: 'file-names',
184 | ruleMeta: {
185 | docs: {
186 | description: 'showFilenames: true'
187 | }
188 | },
189 | tests: {
190 | valid: [
191 | 'var x = 123;',
192 | 'var y = 456;',
193 | {
194 | code: 'var jsX = 123;',
195 | filename: jsFilename
196 | },
197 | {
198 | code: 'var jsY = 456;',
199 | filename: jsFilename
200 | }
201 | ],
202 | invalid: [
203 | 'var vueX = 123;',
204 | 'var vueY = 456;',
205 | {
206 | code: 'var tsX = 123;',
207 | filename: jsFilename
208 | },
209 | {
210 | code: 'var tsY = 456;',
211 | filename: jsFilename
212 | }
213 | ]
214 | },
215 | config: {
216 | showFilenames: true
217 | },
218 | expected: 'cases/file-names.md'
219 | },
220 | {
221 | description: 'no-fix-code-examples.md: No description, rule with `docgen: false`, fixCodeExamples:false, showConfigComments:true',
222 | ruleMeta: {
223 | fixable: 'code'
224 | },
225 | tests: {
226 | valid: [
227 | 'var x="123"',
228 | 'var y="45678"',
229 | {
230 | code: 'var z="not-included-in-docs"',
231 | docgen: false
232 | }
233 | ],
234 | invalid: [
235 | {
236 | code: 'var x="1.23"',
237 | options: [ { myOption: true } ],
238 | output: 'var x="123"'
239 | },
240 | {
241 | code: 'var y="4.5678"',
242 | options: [ { myOption: true } ]
243 | // `output` missing, not allowed in ESLint >= 7
244 | }
245 | ]
246 | },
247 | config: {
248 | showConfigComments: true,
249 | fixCodeExamples: false,
250 | minExamples: [ 'warn', 1 ],
251 | maxExamples: [ 'error', 3 ]
252 | },
253 | messages: [
254 | noDesc,
255 | {
256 | type: 'error',
257 | text: '5 examples found, expected fewer than 3.',
258 | label: 'config.maxExamples'
259 | }
260 | ],
261 | expected: 'cases/no-fix-code-examples.md'
262 | },
263 | {
264 | description: 'no-fix-code-examples.md: No description, rules with `docgen: true`, excludeExamplesByDefault: true, fixCodeExamples:false, showConfigComments:true',
265 | ruleMeta: {
266 | fixable: 'code'
267 | },
268 | tests: {
269 | valid: [
270 | {
271 | code: 'var x="123"',
272 | docgen: true
273 | },
274 | {
275 | code: 'var y="45678"',
276 | docgen: true
277 | },
278 | 'var z="not-included-in-docs"'
279 | ],
280 | invalid: [
281 | {
282 | code: 'var x="1.23"',
283 | options: [ { myOption: true } ],
284 | output: 'var x="123"',
285 | docgen: true
286 | },
287 | {
288 | code: 'var y="4.5678"',
289 | options: [ { myOption: true } ],
290 | // `output` missing, not allowed in ESLint >= 7
291 | docgen: true
292 | }
293 | ]
294 | },
295 | config: {
296 | excludeExamplesByDefault: true,
297 | showConfigComments: true,
298 | fixCodeExamples: false
299 | },
300 | messages: [
301 | noDesc
302 | ],
303 | expected: 'cases/no-fix-code-examples.md'
304 | },
305 | {
306 | description: 'config-comments.md: No valid cases, fixCodeExamples:true, showConfigComments:true',
307 | ruleMeta: {
308 | fixable: 'code'
309 | },
310 | tests: {
311 | valid: [],
312 | invalid: [
313 | {
314 | code: 'var x="1.23"',
315 | output: 'var x="123"'
316 | },
317 | // Duplicate example after lint-fix
318 | {
319 | code: 'var x = "1.23"',
320 | output: 'var x = "123"'
321 | },
322 | // Different options, not a duplicate
323 | {
324 | code: 'var x = "1.23"',
325 | options: [ { myOption: true } ],
326 | output: 'var x = "123"'
327 | }
328 | ]
329 | },
330 | config: {
331 | showConfigComments: true,
332 | fixCodeExamples: true
333 | },
334 | messages: [
335 | noDesc,
336 | {
337 | type: 'warn',
338 | text: 'Duplicate code example found, examples can be hidden with `docgen: false`:\nvar x="1.23"\nvar x = "1.23"'
339 | }
340 | ],
341 | expected: 'cases/config-comments.md'
342 | },
343 | {
344 | description: 'not-fixable.md: Rule not fixable, only docLink',
345 | tests: {
346 | valid: [],
347 | invalid: [
348 | {
349 | code: 'var x="1.23"',
350 | // Rule is not fixable, so output is ignored
351 | output: 'var x="123"'
352 | }
353 | ]
354 | },
355 | config: {
356 | docLink: true,
357 | ruleLink: false,
358 | testLink: false
359 | },
360 | messages: [
361 | noDesc,
362 | {
363 | type: 'warn',
364 | text: '1 example found, expected at least 2.',
365 | label: 'config.minExamples'
366 | }
367 | ],
368 | expected: 'cases/not-fixable.md'
369 | },
370 | {
371 | description: 'not-show-fixes.md: Don\'t show fixes',
372 | ruleMeta: {
373 | fixable: 'code'
374 | },
375 | tests: {
376 | valid: [
377 | 'var x="4.56"'
378 | ],
379 | invalid: [
380 | {
381 | code: 'var x="1.23"',
382 | output: 'var x="123"'
383 | }
384 | ]
385 | },
386 | config: {
387 | showFixExamples: false,
388 | ruleTemplatePath: 'invalid/path/finds/no/templates/{name}.ejs'
389 | },
390 | messages: [ noDesc ],
391 | expected: 'cases/no-show-fixes.md'
392 | },
393 | {
394 | description: 'ruleTemplatePath',
395 | ruleMeta: {
396 | fixable: 'code'
397 | },
398 | tests: {
399 | valid: [
400 | 'var x="4.56"'
401 | ],
402 | invalid: [
403 | 'var x="1.23"'
404 | ]
405 | },
406 | config: {
407 | ruleTemplatePath: 'ruleTemplates/{name}.ejs'
408 | },
409 | messages: [ noDesc ],
410 | expected: 'cases/rule-template-path.md'
411 | },
412 | {
413 | description: 'scoped plugin',
414 | tests: {
415 | valid: [
416 | 'var x="4.56"'
417 | ],
418 | invalid: [
419 | 'var x="1.23"'
420 | ]
421 | },
422 | config: {
423 | showConfigComments: true
424 | },
425 | messages: [ noDesc ],
426 | cwd: 'cases/plugin-scoped',
427 | expected: 'cases/scoped-plugin.md'
428 | }
429 | ];
430 |
431 | cases.forEach( ( caseItem ) => {
432 | it( caseItem.description, async () => {
433 | testUtils.mockCwd( caseItem.cwd || 'cases/plugin/src' );
434 |
435 | const buildDocsFromTests = require( '../src/build-docs-from-tests' );
436 | const loadTemplates = require( '../src/load-templates' );
437 | const { globalTemplates, loadRuleTemplate } = loadTemplates( [ path.join( __dirname, '../src/templates' ) ] );
438 | const defaultConfig = require( '../src/default-config' );
439 |
440 | defaultConfig.docPath = 'docs/{name}.md';
441 | defaultConfig.rulePath = 'rules/{name}.js';
442 | defaultConfig.testPath = 'tests/{name}.js';
443 |
444 | function loadCase( filename ) {
445 | // eslint-disable-next-line security/detect-non-literal-fs-filename
446 | return fs.readFileSync( path.join( __dirname, filename ) ).toString();
447 | }
448 |
449 | const { output, messages } = await buildDocsFromTests(
450 | caseItem.name || 'my-rule',
451 | caseItem.ruleMeta || {},
452 | caseItem.tests,
453 | caseItem.configMap,
454 | Object.assign( {}, defaultConfig, caseItem.config ),
455 | caseItem.templates || globalTemplates,
456 | loadRuleTemplate,
457 | caseItem.testerConfig
458 | );
459 |
460 | assert.strictEqual( output, loadCase( caseItem.expected ), 'output' );
461 | assert.deepEqual( messages, caseItem.messages || [], 'messages' );
462 | } );
463 | } );
464 | } );
465 |
--------------------------------------------------------------------------------
/tests/cases/config-comments.md:
--------------------------------------------------------------------------------
1 | [//]: # (This file is generated by eslint-docgen. Do not edit it directly.)
2 |
3 | # my-rule
4 |
5 | 🔧 The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
6 |
7 | ## Rule details
8 |
9 | ❌ Examples of **incorrect** code:
10 | ```js
11 | /* eslint my-plugin/my-rule: "error" */
12 | var x = '1.23';
13 | var x = '1.23';
14 | ```
15 |
16 | ❌ Examples of **incorrect** code with `[{"myOption":true}]` options:
17 | ```js
18 | /* eslint my-plugin/my-rule: ["error",[{"myOption":true}]] */
19 | var x = '1.23';
20 | ```
21 |
22 | 🔧 Examples of code **fixed** by this rule:
23 | ```js
24 | /* eslint my-plugin/my-rule: "error" */
25 | var x = '1.23'; /* → */ var x = '123';
26 | var x = '1.23'; /* → */ var x = '123';
27 | ```
28 |
29 | 🔧 Examples of code **fixed** by this rule with `[{"myOption":true}]` options:
30 | ```js
31 | /* eslint my-plugin/my-rule: ["error",[{"myOption":true}]] */
32 | var x = '1.23'; /* → */ var x = '123';
33 | ```
34 |
35 | ## Resources
36 |
37 | * [Rule source](/rules/my-rule.js)
38 | * [Test source](/tests/my-rule.js)
39 |
--------------------------------------------------------------------------------
/tests/cases/file-names.md:
--------------------------------------------------------------------------------
1 | [//]: # (This file is generated by eslint-docgen. Do not edit it directly.)
2 |
3 | # file-names
4 |
5 | showFilenames: true
6 |
7 | ## Rule details
8 |
9 | ❌ Examples of **incorrect** code:
10 | ```js
11 | var vueX = 123;
12 | var vueY = 456;
13 | ```
14 |
15 | ✔️ Examples of **correct** code:
16 | ```js
17 | var x = 123;
18 | var y = 456;
19 | ```
20 |
21 | ❌ Examples of **incorrect** code for a file named `test.js`:
22 | ```js
23 | var tsX = 123;
24 | var tsY = 456;
25 | ```
26 |
27 | ✔️ Examples of **correct** code for a file named `test.js`:
28 | ```js
29 | var jsX = 123;
30 | var jsY = 456;
31 | ```
32 |
33 | ## Resources
34 |
35 | * [Rule source](/rules/file-names.js)
36 | * [Test source](/tests/file-names.js)
37 |
--------------------------------------------------------------------------------
/tests/cases/no-fix-code-examples.md:
--------------------------------------------------------------------------------
1 | [//]: # (This file is generated by eslint-docgen. Do not edit it directly.)
2 |
3 | # my-rule
4 |
5 | 🔧 The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
6 |
7 | ## Rule details
8 |
9 | ✔️ Examples of **correct** code:
10 | ```js
11 | /*eslint my-plugin/my-rule: "error"*/
12 | var x="123"
13 | var y="45678"
14 | ```
15 |
16 | ❌ Examples of **incorrect** code with `[{"myOption":true}]` options:
17 | ```js
18 | /*eslint my-plugin/my-rule: ["error",[{"myOption":true}]]*/
19 | var x="1.23"
20 | var y="4.5678"
21 | ```
22 |
23 | 🔧 Examples of code **fixed** by this rule with `[{"myOption":true}]` options:
24 | ```js
25 | /*eslint my-plugin/my-rule: ["error",[{"myOption":true}]]*/
26 | var x="1.23" /* → */ var x="123"
27 | ```
28 |
29 | ## Resources
30 |
31 | * [Rule source](/rules/my-rule.js)
32 | * [Test source](/tests/my-rule.js)
33 |
--------------------------------------------------------------------------------
/tests/cases/no-show-fixes.md:
--------------------------------------------------------------------------------
1 | [//]: # (This file is generated by eslint-docgen. Do not edit it directly.)
2 |
3 | # my-rule
4 |
5 | 🔧 The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
6 |
7 | ## Rule details
8 |
9 | ❌ Examples of **incorrect** code:
10 | ```js
11 | var x = '1.23';
12 | ```
13 |
14 | ✔️ Examples of **correct** code:
15 | ```js
16 | var x = '4.56';
17 | ```
18 |
19 | ## Resources
20 |
21 | * [Rule source](/rules/my-rule.js)
22 | * [Test source](/tests/my-rule.js)
23 |
--------------------------------------------------------------------------------
/tests/cases/not-fixable.md:
--------------------------------------------------------------------------------
1 | [//]: # (This file is generated by eslint-docgen. Do not edit it directly.)
2 |
3 | # my-rule
4 |
5 | ## Rule details
6 |
7 | ❌ Examples of **incorrect** code:
8 | ```js
9 | var x = '1.23';
10 | ```
11 |
12 | ## Resources
13 |
14 | * [Documentation source](/docs/my-rule.md)
15 |
--------------------------------------------------------------------------------
/tests/cases/plugin-missing-config/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "eslint-plugin-missing-config"
3 | }
4 |
--------------------------------------------------------------------------------
/tests/cases/plugin-scoped/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@user/eslint-plugin-scoped"
3 | }
4 |
--------------------------------------------------------------------------------
/tests/cases/plugin/.eslintdocgenrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "docPath": "docs/{name}/README.md",
3 | "ruleLink": false,
4 | "testLink": false
5 | }
6 |
--------------------------------------------------------------------------------
/tests/cases/plugin/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "eslint-plugin-my-plugin",
3 | "main": "src/main.js"
4 | }
--------------------------------------------------------------------------------
/tests/cases/plugin/ruleTemplates/my-rule.ejs:
--------------------------------------------------------------------------------
1 | == My rule ==
2 |
3 | Custom template for my rule.
4 |
5 | ## Rule details
6 |
7 | <%- include( 'examples' ) %>
8 |
9 | ## When not to use use
10 |
11 | You may not need this rule if you don't use ES6.
12 |
13 | <%- include( 'resources' ) %>
14 |
15 |
--------------------------------------------------------------------------------
/tests/cases/plugin/src/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "rules": {
4 | "indent": [ "error", 4 ],
5 | "no-console": "error",
6 | "no-extra-semi": "error",
7 | "semi": "error",
8 | "quotes": [ "error", "single" ],
9 | "some-plugin-rule": "error",
10 | "space-infix-ops": [ "error" ],
11 | "spaced-comment": [ "error", "always", {
12 | "exceptions": [ "*", "!" ],
13 | "block": { "balanced": true }
14 | } ]
15 | }
16 | }
--------------------------------------------------------------------------------
/tests/cases/plugin/src/main.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | rules: {
3 | 'recommended-rule': { meta: { docs: 'Recommended' } },
4 | 'my-rule': { meta: { docs: 'My' } },
5 | },
6 | configs: {
7 | recommended: {
8 | rules: {
9 | 'my-plugin/recommended-rule': [ 'error', { myOption: true } ]
10 | }
11 | },
12 | strict: {
13 | rules: {
14 | 'my-plugin/recommended-rule': 'error'
15 | }
16 | },
17 | all: {
18 | extends: [ 'my-plugin/strict', 'my-plugin/recommended' ]
19 | }
20 | }
21 | };
22 |
--------------------------------------------------------------------------------
/tests/cases/rule-template-path.md:
--------------------------------------------------------------------------------
1 | == My rule ==
2 |
3 | Custom template for my rule.
4 |
5 | ## Rule details
6 |
7 | ❌ Examples of **incorrect** code:
8 | ```js
9 | var x = '1.23';
10 | ```
11 |
12 | ✔️ Examples of **correct** code:
13 | ```js
14 | var x = '4.56';
15 | ```
16 |
17 | ## When not to use use
18 |
19 | You may not need this rule if you don't use ES6.
20 |
21 | ## Resources
22 |
23 | * [Rule source](/rules/my-rule.js)
24 | * [Test source](/tests/my-rule.js)
25 |
--------------------------------------------------------------------------------
/tests/cases/scoped-plugin.md:
--------------------------------------------------------------------------------
1 | [//]: # (This file is generated by eslint-docgen. Do not edit it directly.)
2 |
3 | # my-rule
4 |
5 | ## Rule details
6 |
7 | ❌ Examples of **incorrect** code:
8 | ```js
9 | /* eslint @user/scoped/my-rule: "error" */
10 | const x = '1.23';
11 | ```
12 |
13 | ✔️ Examples of **correct** code:
14 | ```js
15 | /* eslint @user/scoped/my-rule: "error" */
16 | const x = '4.56';
17 | ```
18 |
19 | ## Resources
20 |
21 | * [Rule source](/rules/my-rule.js)
22 | * [Test source](/tests/my-rule.js)
23 |
--------------------------------------------------------------------------------
/tests/cases/simple-rule.md:
--------------------------------------------------------------------------------
1 | [//]: # (This file is generated by eslint-docgen. Do not edit it directly.)
2 |
3 | # my-rule
4 |
5 | My rule enforces a thing
6 |
7 | ⚠️ This rule is deprecated. Use [`my-new-rule`](my-new-rule.md), [`my-other-rule`](my-other-rule.md) and [`third-rule`](third-rule.md) instead.
8 |
9 | 📋 This rule is enabled in `plugin:my-plugin/recommended` with `[{"myOption":true}]` options.
10 |
11 | 📋 This rule is enabled in `plugin:my-plugin/strict`.
12 |
13 | 🔧 The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
14 |
15 | ## Rule details
16 |
17 | ❌ Examples of **incorrect** code:
18 | ```js
19 | var x = '1.23';
20 | var y = '4.5678';
21 |
22 | multi
23 | .line
24 | .case;
25 |
26 | multi.line.case;
27 | singleAfterMulti;
28 | ```
29 |
30 | ✔️ Examples of **correct** code:
31 | ```js
32 | var x = '123';
33 | var y = '45678';
34 | var z = { a: 3, ...b };
35 | ```
36 |
37 | ❌ Examples of **incorrect** code with `[{"myOption":true}]` options:
38 | ```js
39 | var z1 = '1.23';
40 | var z2 = '1.23';
41 | ```
42 |
43 | ✔️ Examples of **correct** code with `[{"myOption":true}]` options:
44 | ```js
45 | var z1 = '123';
46 | var z2 = '123';
47 | ```
48 |
49 | ✔️ Examples of **correct** code with `[{"lang":"fr"}]` settings:
50 | ```js
51 | var x = '1,23';
52 | ```
53 |
54 | ✔️ Examples of **correct** code with `[{"myOption":true}]` options and `[{"lang":"fr"}]` settings:
55 | ```js
56 | var z1 = '1,23';
57 | ```
58 |
59 | 🔧 Examples of code **fixed** by this rule:
60 | ```js
61 | var x = '1.23'; /* → */ var x = '123';
62 | var y = '4.5678'; /* → */ var y = '45678';
63 |
64 | multi /* → */ Multi
65 | .line /* → */ .Line.Case;
66 | .case; /* → */
67 |
68 | multi.line.case; /* → */ Multi
69 | /* → */ .Line
70 | /* → */ .Case;
71 |
72 | singleAfterMulti; /* → */ SingleAfterMulti;
73 | ```
74 |
75 | 🔧 Examples of code **fixed** by this rule with `[{"myOption":true}]` options:
76 | ```js
77 | var z1 = '1.23'; /* → */ var z1 = '123';
78 | var z2 = '1.23'; /* → */ var z2 = '123';
79 | ```
80 |
81 | ## Resources
82 |
83 | * [Rule source](/rules/my-rule.js)
84 | * [Test source](/tests/my-rule.js)
85 |
--------------------------------------------------------------------------------
/tests/cases/syntax-lang.md:
--------------------------------------------------------------------------------
1 | [//]: # (This file is generated by eslint-docgen. Do not edit it directly.)
2 |
3 | # syntax-lang
4 |
5 | Syntax language set from filename extension, fixCodeExamples:false
6 |
7 | ## Rule details
8 |
9 | ❌ Examples of **incorrect** code:
10 | ```html
11 |
14 | ```
15 |
16 | ✔️ Examples of **correct** code:
17 | ```html
18 |
21 | ```
22 |
23 | ❌ Examples of **incorrect** code:
24 | ```js
25 | var tsX = 123;
26 | var tsY = 456;
27 | ```
28 |
29 | ✔️ Examples of **correct** code:
30 | ```js
31 | var x = 123;
32 | var y = 456;
33 | var jsX = 123;
34 | var jsY = 456;
35 | ```
36 |
37 | ❌ Examples of **incorrect** code:
38 | ```ts
39 | function jsF(x: T): T { return x; }
40 | ```
41 |
42 | ✔️ Examples of **correct** code:
43 | ```ts
44 | function tsF(x: T): T { return x; }
45 | ```
46 |
47 | ❌ Examples of **incorrect** code:
48 | ```vue
49 |
50 | Placeholder...
51 |
52 |
55 |
56 |
57 | Placeholder...
58 |
59 |
62 | ```
63 |
64 | ✔️ Examples of **correct** code:
65 | ```vue
66 |
67 | Placeholder...
68 |
69 |
72 | ```
73 |
74 | ## Resources
75 |
76 | * [Rule source](/rules/syntax-lang.js)
77 | * [Test source](/tests/syntax-lang.js)
78 |
--------------------------------------------------------------------------------
/tests/cases/templates/test.ejs:
--------------------------------------------------------------------------------
1 | <%- include( 'doesNotExist' ) %>
--------------------------------------------------------------------------------
/tests/formatter.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const assert = require( 'assert' );
4 |
5 | describe( 'formatter', () => {
6 | it( 'formatter', () => {
7 | const formatter = require( '../src/formatter' );
8 | assert.ok( formatter.warn( 'foo' ).match( /warning.*foo/ ) );
9 | assert.ok( formatter.error( 'bar' ).match( /error.*bar/ ) );
10 | assert.ok( formatter.heading( 'baz' ).match( /baz/ ) );
11 | assert.ok( formatter.warn( 'quux', 'whee' ).match( /warning.*quux.*whee/ ) );
12 | assert.ok( formatter.error( 'whee', 'quux' ).match( /error.*whee.*quux/ ) );
13 | assert.ok( formatter.warn( 'foo\nbar' ).match( /warning.*foo\n {11}bar/ ) );
14 | } );
15 | } );
16 |
--------------------------------------------------------------------------------
/tests/get-config.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const assert = require( 'assert' );
4 | const testUtils = require( './test-utils' );
5 |
6 | describe( 'getConfig', () => {
7 | it( 'simple config', () => {
8 | testUtils.mockCwd( 'cases/plugin/src' );
9 | const { config } = require( '../src/get-config.js' )();
10 | assert.deepEqual(
11 | config,
12 | {
13 | pluginName: 'my-plugin',
14 | fixCodeExamples: true,
15 | showConfigComments: false,
16 | showFilenames: false,
17 | showFixExamples: true,
18 | tabWidth: 4,
19 | docPath: 'docs/{name}/README.md',
20 | rulePath: null,
21 | testPath: null,
22 | ruleTemplatePath: null,
23 | globalTemplatePath: null,
24 | docLink: false,
25 | ruleLink: false,
26 | testLink: false,
27 | excludeExamplesByDefault: false,
28 | minExamples: [ 'warn', 2 ],
29 | maxExamples: [ 'warn', 50 ]
30 | }
31 | );
32 | } );
33 |
34 | it( 'missing config', () => {
35 | testUtils.mockCwd( 'cases/plugin-missing-config' );
36 | const getConfig = require( '../src/get-config.js' );
37 | assert.throws( () => {
38 | getConfig();
39 | }, { message: /.eslintdocgenrc not found/ } );
40 | } );
41 | } );
42 |
--------------------------------------------------------------------------------
/tests/init.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const simple = require( 'simple-mock' );
4 |
5 | // eslint-disable-next-line mocha/no-top-level-hooks
6 | afterEach( () => {
7 | // Un-mock
8 | simple.restore();
9 | // Clear require cache
10 | Object.keys( require.cache ).forEach( ( key ) => {
11 | delete require.cache[ key ];
12 | } );
13 | } );
14 |
--------------------------------------------------------------------------------
/tests/load-templates.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const assert = require( 'assert' );
4 | const path = require( 'upath' );
5 |
6 | describe( 'loadTemplates', () => {
7 | it( 'Use invalid include', () => {
8 | const loadTemplates = require( '../src/load-templates' );
9 | const { globalTemplates } = loadTemplates( [ path.join( __dirname, 'cases/templates' ) ] );
10 | assert.throws( () => {
11 | globalTemplates.test();
12 | }, { message: /Template `doesNotExist` not found in template `test`/ } );
13 | } );
14 | } );
15 |
--------------------------------------------------------------------------------
/tests/rules-with-config.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const assert = require( 'assert' );
4 | const testUtils = require( './test-utils' );
5 |
6 | describe( 'rulesWithConfig', () => {
7 | it( 'rulesWithConfig', () => {
8 | testUtils.mockCwd( 'cases/plugin/src' );
9 | const rulesWithConfig = require( '../src/rules-with-config.js' );
10 | const expected = new Map( Object.entries( {
11 | 'recommended-rule': {
12 | meta: { docs: 'Recommended' },
13 | configMap: new Map( Object.entries( {
14 | recommended: [ { myOption: true } ],
15 | strict: null
16 | } ) )
17 | },
18 | 'my-rule': {
19 | meta: { docs: 'My' },
20 | configMap: new Map()
21 | }
22 | } ) );
23 | assert.deepEqual(
24 | rulesWithConfig,
25 | expected
26 | );
27 | } );
28 | } );
29 |
--------------------------------------------------------------------------------
/tests/test-utils.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const path = require( 'upath' );
4 | const simple = require( 'simple-mock' );
5 |
6 | function mockCwd( relativePath ) {
7 | simple.mock( process, 'cwd' ).returnWith( path.join( __dirname, relativePath ) );
8 | }
9 |
10 | module.exports = {
11 | mockCwd: mockCwd
12 | };
13 |
--------------------------------------------------------------------------------
/tests/validate-config.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const assert = require( 'assert' );
4 | const validateConfig = require( '../src/validate-config.js' );
5 |
6 | describe( 'validateConfig', () => {
7 | it( 'default config with valid paths', () => {
8 | const defaultConfig = require( '../src/default-config.js' );
9 | const result = validateConfig( Object.assign( {}, defaultConfig, {
10 | docPath: 'docs/rules/{name}.md',
11 | rulePath: 'rules/{name}.md',
12 | testPath: 'tests/rules/{name}.md'
13 | } ) );
14 | assert.deepEqual(
15 | result,
16 | []
17 | );
18 | } );
19 |
20 | it( 'various bad config values', () => {
21 | assert.deepEqual(
22 | validateConfig( {
23 | pluginName: false,
24 | fixCodeExamples: 3,
25 | showConfigComments: 'string',
26 | showFixExamples: {},
27 | tabWidth: -1.5,
28 | docPath: 'no-name-param',
29 | rulePath: '',
30 | testPath: '',
31 | templatePath: 'foo',
32 | ruleTemplatePath: 'no-name-{param}',
33 | globalTemplatePath: false,
34 | docLink: '',
35 | ruleLink: 'true',
36 | testLink: 'true',
37 | excludeExamplesByDefault: [],
38 | minExamples: [ 'info', 3 ],
39 | maxExamples: [ 'error', -2 ],
40 | additionalProperty: 'foo'
41 | } ),
42 | [
43 | 'instance.pluginName is not of a type(s) string',
44 | 'instance.fixCodeExamples is not of a type(s) boolean',
45 | 'instance.showConfigComments is not of a type(s) boolean',
46 | 'instance.showFixExamples is not of a type(s) boolean',
47 | 'instance.tabWidth is not of a type(s) integer',
48 | 'instance.tabWidth must be greater than or equal to 0',
49 | 'instance.docPath must contain "{name}"',
50 | 'instance.rulePath must contain "{name}" or be null',
51 | 'instance.testPath must contain "{name}" or be null',
52 | 'instance.ruleTemplatePath must contain "{name}" or be null',
53 | 'instance.globalTemplatePath is not of a type(s) string,null',
54 | 'instance.templatePath must be renamed to "globalTemplatePath"',
55 | 'instance.docLink is not of a type(s) boolean',
56 | 'instance.ruleLink is not of a type(s) boolean',
57 | 'instance.testLink is not of a type(s) boolean',
58 | 'instance.excludeExamplesByDefault is not of a type(s) boolean',
59 | 'instance.minExamples must be a tuple containing "warn"/"error" and a positive integer, or be null',
60 | 'instance.maxExamples must be a tuple containing "warn"/"error" and a positive integer, or be null',
61 | 'instance is not allowed to have the additional property "additionalProperty"'
62 | ]
63 | );
64 | } );
65 |
66 | it( 'ruleLink but no rulePath', () => {
67 | const defaultConfig = require( '../src/default-config.js' );
68 | const result = validateConfig( Object.assign( {}, defaultConfig, {
69 | docPath: 'docs/{name}.md',
70 | ruleLink: true,
71 | testLink: false
72 | } ) );
73 | assert.deepEqual( result, [
74 | 'instance does not match allOf schema [subschema 0] with 1 error[s]:',
75 | 'instance does not have rulePath when ruleLink is true'
76 | ] );
77 | } );
78 |
79 | it( 'testLink but no testPath', () => {
80 | const defaultConfig = require( '../src/default-config.js' );
81 | const result = validateConfig( Object.assign( {}, defaultConfig, {
82 | docPath: 'docs/{name}.md',
83 | ruleLink: false,
84 | testLink: true
85 | } ) );
86 | assert.deepEqual( result, [
87 | 'instance does not match allOf schema [subschema 1] with 1 error[s]:',
88 | 'instance does not have testPath when testLink is true'
89 | ] );
90 | } );
91 | } );
92 |
--------------------------------------------------------------------------------
/tests/write-docs-from-tests.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wikimedia/eslint-docgen/2c912af651cf37a3a23f9fb73b0224ff2af23069/tests/write-docs-from-tests.js
--------------------------------------------------------------------------------