├── .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 ` 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 | 52 | 55 | 56 | 59 | 62 | ``` 63 | 64 | ✔️ Examples of **correct** code: 65 | ```vue 66 | 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 --------------------------------------------------------------------------------