├── .gitignore ├── .travis.yml ├── tests ├── types │ ├── tsconfig.json │ └── tests.ts └── lib │ └── rule-composer.js ├── .eslintrc.yml ├── types.d.ts ├── LICENSE.md ├── CHANGELOG.md ├── package.json ├── README.md └── lib └── rule-composer.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '4' 4 | - '6' 5 | - '8' 6 | - '9' 7 | -------------------------------------------------------------------------------- /tests/types/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "noEmit": true, 5 | "target": "es2015" 6 | } 7 | } -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | extends: 2 | - airbnb-base 3 | - plugin:node/recommended 4 | 5 | parserOptions: 6 | sourceType: script 7 | 8 | plugins: 9 | - node 10 | 11 | env: 12 | node: true 13 | 14 | rules: 15 | comma-dangle: [error, always-multiline] 16 | function-paren-newline: [error, consistent] 17 | max-len: off 18 | prefer-rest-params: off 19 | prefer-spread: off 20 | no-restricted-syntax: off 21 | prefer-destructuring: off 22 | -------------------------------------------------------------------------------- /tests/types/tests.ts: -------------------------------------------------------------------------------- 1 | import * as ruleComposer from '../..'; 2 | 3 | let ruleModule: import('eslint').Rule.RuleModule; 4 | 5 | ruleComposer.mapReports(ruleModule, (problem, metadata) => { 6 | problem.data; 7 | problem.loc.start; 8 | problem.loc.end; 9 | problem.message.substr(0); 10 | problem.messageId.substr(0); 11 | 12 | problem.fix = void 0; 13 | problem.fix = () => null; 14 | problem.fix = fixer => fixer.remove(problem.node); 15 | problem.fix = fixer => [fixer.remove(problem.node)]; 16 | 17 | metadata.filename.substr(0); 18 | metadata.options[0]; 19 | metadata.settings; 20 | metadata.sourceCode.getText(problem.node); 21 | 22 | return problem; 23 | }); 24 | 25 | ruleComposer.filterReports(ruleModule, (problem, metadata) => false); 26 | 27 | ruleComposer.joinReports([ruleModule, ruleModule]); 28 | -------------------------------------------------------------------------------- /types.d.ts: -------------------------------------------------------------------------------- 1 | import * as estree from 'estree'; 2 | import * as eslint from 'eslint'; 3 | 4 | interface Problem { 5 | node: estree.Node 6 | message: string 7 | messageId: string | null 8 | data: object | null 9 | loc: eslint.AST.SourceLocation 10 | fix?(fixer: eslint.Rule.RuleFixer): null | eslint.Rule.Fix | eslint.Rule.Fix[]; 11 | } 12 | 13 | interface Metadata { 14 | sourceCode: eslint.SourceCode 15 | settings?: object 16 | options: any[] 17 | filename: string 18 | } 19 | 20 | interface Predicate { 21 | (problem: Problem, metadata: Metadata): T 22 | } 23 | 24 | export function mapReports( 25 | rule: eslint.Rule.RuleModule, 26 | iteratee: Predicate 27 | ): eslint.Rule.RuleModule; 28 | 29 | export function filterReports( 30 | rule: eslint.Rule.RuleModule, 31 | predicate: Predicate 32 | ): eslint.Rule.RuleModule; 33 | 34 | export function joinReports( 35 | rules: eslint.Rule.RuleModule[] 36 | ): eslint.Rule.RuleModule; 37 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright © 2017 Teddy Katz 5 | 6 | Permission is hereby granted, free of charge, to any person 7 | obtaining a copy of this software and associated documentation 8 | files (the “Software”), to deal in the Software without 9 | restriction, including without limitation the rights to use, 10 | copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the 12 | Software is furnished to do so, subject to the following 13 | conditions: 14 | 15 | The above copyright notice and this permission notice shall be 16 | included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 20 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 22 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 23 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 25 | OTHER DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v0.3.0 (2018-04-16) 4 | 5 | * Update: Add filename property to metadata ([#3](https://github.com/not-an-aardvark/eslint-rule-composer/issues/3)) ([c6982df](https://github.com/not-an-aardvark/eslint-rule-composer/commit/c6982df862ffd9f2f7595d05d407eb7b5f9e83f9)) 6 | 7 | ## v0.2.0 (2018-04-14) 8 | 9 | * Update: Add a reference to context settings and options ([#1](https://github.com/not-an-aardvark/eslint-rule-composer/issues/1)) ([e7312ba](https://github.com/not-an-aardvark/eslint-rule-composer/commit/e7312bae50399f7576220649a52b8bbb4d4083c2)) 10 | * Build: set up Travis CI ([7e43e8c](https://github.com/not-an-aardvark/eslint-rule-composer/commit/7e43e8c05f667b0335f8ca7505acf831e1616070)) 11 | 12 | ## v0.1.1 (2018-03-14) 13 | 14 | * Chore: set up release script ([2ce3403](https://github.com/not-an-aardvark/eslint-rule-composer/commit/2ce3403d9cade255f904a3f8b9135076fa0937f1)) 15 | * Update: support rules that use messageIds ([861137c](https://github.com/not-an-aardvark/eslint-rule-composer/commit/861137cd9080c6a9f9e1dfe5a5e0fa03f81bf5ec)) 16 | * Docs: fix formatting in readme ([bad652b](https://github.com/not-an-aardvark/eslint-rule-composer/commit/bad652b05f6470e2155df02746acfa85a45fb4ab)) 17 | 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-rule-composer", 3 | "version": "0.3.0", 4 | "description": "A utility for composing ESLint rules from other ESLint rules", 5 | "main": "lib/rule-composer.js", 6 | "types": "types.d.ts", 7 | "files": [ 8 | "lib/", 9 | "types.d.ts" 10 | ], 11 | "scripts": { 12 | "lint": "eslint lib/ tests/", 13 | "test": "npm run lint && mocha tests/**/*.js && tsc -p ./tests/types", 14 | "generate-release": "node-release-script" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/not-an-aardvark/eslint-rule-composer.git" 19 | }, 20 | "keywords": [ 21 | "eslint" 22 | ], 23 | "author": "Teddy Katz", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/not-an-aardvark/eslint-rule-composer/issues" 27 | }, 28 | "homepage": "https://github.com/not-an-aardvark/eslint-rule-composer#readme", 29 | "devDependencies": { 30 | "@not-an-aardvark/node-release-script": "^0.1.0", 31 | "@types/eslint": "^4.16.6", 32 | "@types/estree": "0.0.39", 33 | "chai": "^4.1.2", 34 | "eslint": "^4.7.1", 35 | "eslint-config-airbnb-base": "^12.0.0", 36 | "eslint-plugin-import": "^2.7.0", 37 | "eslint-plugin-node": "^5.1.1", 38 | "mocha": "^3.5.3", 39 | "typescript": "^3.3.3" 40 | }, 41 | "engines": { 42 | "node": ">=4.0.0" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # eslint-rule-composer 2 | 3 | This is a utility that allows you to build [ESLint](https://eslint.org/) rules out of other ESLint rules. 4 | 5 | ## Installation 6 | 7 | ``` 8 | npm install eslint-rule-composer --save 9 | ``` 10 | 11 | Requires Node 4 or later. 12 | 13 | If you're using TypeScript, it's recommended to install `@types/eslint`: 14 | 15 | ``` 16 | npm install -D @types/eslint 17 | ``` 18 | 19 | ## Examples 20 | 21 | The following example creates a modified version of the [`no-unused-expressions`](https://eslint.org/docs/rules/no-unused-expressions) rule which does not report lines starting with `expect`. 22 | 23 | ```js 24 | const ruleComposer = require('eslint-rule-composer'); 25 | const eslint = require('eslint'); 26 | const noUnusedExpressionsRule = new eslint.Linter().getRules().get('no-unused-expressions'); 27 | 28 | module.exports = ruleComposer.filterReports( 29 | noUnusedExpressionsRule, 30 | (problem, metadata) => metadata.sourceCode.getFirstToken(problem.node).value !== 'expect' 31 | ); 32 | ``` 33 | 34 | The following example creates a modified version of the [`semi`](https://eslint.org/docs/rules/semi) rule which reports missing semicolons after experimental class properties: 35 | 36 | ```js 37 | const ruleComposer = require('eslint-rule-composer'); 38 | const eslint = require('eslint'); 39 | const semiRule = new eslint.Linter().getRules().get('semi'); 40 | 41 | module.exports = ruleComposer.joinReports([ 42 | semiRule, 43 | context => ({ 44 | ClassProperty(node) { 45 | if (context.getSourceCode().getLastToken(node).value !== ';') { 46 | context.report({ node, message: 'Missing semicolon.' }) 47 | } 48 | } 49 | }) 50 | ]); 51 | ``` 52 | 53 | You can access rule's options and [shared settings](https://eslint.org/docs/user-guide/configuring#adding-shared-settings) from the current ESLint configuration. The following example creates a modified version of the [`no-unused-expressions`](https://eslint.org/docs/rules/no-unused-expressions) rule which accepts a list of exceptions. 54 | 55 | ```js 56 | 57 | /* 58 | rule configuration: 59 | 60 | { 61 | "custom-no-unused-expressions": ["error", { 62 | "whitelist": ["expect", "test"] 63 | }] 64 | } 65 | */ 66 | 67 | const ruleComposer = require('eslint-rule-composer'); 68 | const eslint = require('eslint'); 69 | const noUnusedExpressionsRule = new eslint.Linter().getRules().get('no-unused-expressions'); 70 | 71 | module.exports = ruleComposer.filterReports( 72 | noUnusedExpressionsRule, 73 | (problem, metadata) => { 74 | const firstToken = metadata.sourceCode.getFirstToken(problem.node); 75 | const whitelist = metadata.options[0].whitelist; 76 | return whitelist.includes(value) === false 77 | } 78 | ); 79 | ``` 80 | 81 | ## API 82 | 83 | ### `ruleComposer.filterReports(rule, predicate)` and `ruleComposer.mapReports(rule, predicate)` 84 | 85 | Both of these functions accept two arguments: `rule` (an ESLint rule object) and `predicate` (a function) 86 | 87 | `filterReports(rule, predicate)` returns a new rule such that whenever the original rule would have reported a problem, the new rule will report a problem only if `predicate` returns true for that problem. 88 | `mapReports(rule, predicate)` returns a new rule such that whenever the original rule would have reported a problem, the new rule reports the result of calling `predicate` on the problem. 89 | 90 | In both cases, `predicate` is called with two arguments: `problem` and `metadata`. 91 | 92 | * `problem` is a normalized representation of a problem reported by the original rule. This has the following schema: 93 | 94 | ``` 95 | { 96 | node: ASTNode | null, 97 | message: string, 98 | messageId: string | null, 99 | data: Object | null, 100 | loc: { 101 | start: { line: number, column: number }, 102 | end: { line: number, column: number } | null 103 | }, 104 | fix: Function 105 | } 106 | ``` 107 | 108 | Note that the `messageId` and `data` properties will only be present if the original rule reported a problem using [Message IDs](https://eslint.org/docs/developer-guide/working-with-rules#messageids), otherwise they will be null. 109 | 110 | When returning a descriptor with `mapReports`, the `messageId` property on the returned descriptor will be used to generate the new message. To modify a report message directly for a rule that uses message IDs, ensure that the `predicate` function returns an object without a `messageId` property. 111 | * `metadata` is an object containing information about the source text that was linted. This has the following properties: 112 | * `sourceCode`: a [`SourceCode`](https://eslint.org/docs/developer-guide/working-with-rules#contextgetsourcecode) instance corresponding to the linted text. 113 | * `settings`: linter instance's [shared settings](https://eslint.org/docs/user-guide/configuring#adding-shared-settings) 114 | * `options`: rule's [configuration options](https://eslint.org/docs/developer-guide/working-with-rules#contextoptions) 115 | * `filename`: corresponding filename for the linted text. 116 | 117 | ### `ruleComposer.joinReports(rules)` 118 | 119 | Given an array of ESLint rule objects, `joinReports` returns a new rule that will report all of the problems from any of the rules in the array. The options provided to the new rule will also be provided to all of the rules in the array. 120 | 121 | ### Getting a reference to an ESLint rule 122 | 123 | To get a reference to an ESLint core rule, you can use ESLint's [public API](https://eslint.org/docs/developer-guide/nodejs-api) like this: 124 | 125 | ```js 126 | // get a reference to the 'semi' rule 127 | 128 | const eslint = require('eslint'); 129 | const semiRule = new eslint.Linter().getRules().get('semi'); 130 | ``` 131 | 132 | To get a reference to a rule from a plugin, you can do this: 133 | 134 | ```js 135 | // get a reference to the 'react/boolean-prop-naming' rule 136 | const booleanPropNamingRule = require('eslint-plugin-react').rules['boolean-prop-naming']; 137 | ``` 138 | 139 | You can also create your own rules (see the [rule documentation](https://eslint.org/docs/developer-guide/working-with-rules)): 140 | 141 | ```js 142 | const myCustomRule = { 143 | create(context) { 144 | return { 145 | DebuggerStatement(node) { 146 | context.report({ node, message: 'Do not use debugger statements.' }); 147 | } 148 | } 149 | } 150 | }; 151 | ``` 152 | 153 | ## License 154 | 155 | MIT License 156 | -------------------------------------------------------------------------------- /lib/rule-composer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Translates a multi-argument context.report() call into a single object argument call 5 | * @param {...*} arguments A list of arguments passed to `context.report` 6 | * @returns {MessageDescriptor} A normalized object containing report information 7 | */ 8 | function normalizeMultiArgReportCall() { 9 | // If there is one argument, it is considered to be a new-style call already. 10 | if (arguments.length === 1) { 11 | return arguments[0]; 12 | } 13 | 14 | // If the second argument is a string, the arguments are interpreted as [node, message, data, fix]. 15 | if (typeof arguments[1] === 'string') { 16 | return { 17 | node: arguments[0], 18 | message: arguments[1], 19 | data: arguments[2], 20 | fix: arguments[3], 21 | }; 22 | } 23 | 24 | // Otherwise, the arguments are interpreted as [node, loc, message, data, fix]. 25 | return { 26 | node: arguments[0], 27 | loc: arguments[1], 28 | message: arguments[2], 29 | data: arguments[3], 30 | fix: arguments[4], 31 | }; 32 | } 33 | 34 | /** 35 | * Normalizes a MessageDescriptor to always have a `loc` with `start` and `end` properties 36 | * @param {MessageDescriptor} descriptor A descriptor for the report from a rule. 37 | * @returns {{start: Location, end: (Location|null)}} An updated location that infers the `start` and `end` properties 38 | * from the `node` of the original descriptor, or infers the `start` from the `loc` of the original descriptor. 39 | */ 40 | function normalizeReportLoc(descriptor) { 41 | if (descriptor.loc) { 42 | if (descriptor.loc.start) { 43 | return descriptor.loc; 44 | } 45 | return { start: descriptor.loc, end: null }; 46 | } 47 | return descriptor.node.loc; 48 | } 49 | 50 | 51 | /** 52 | * Interpolates data placeholders in report messages 53 | * @param {MessageDescriptor} descriptor The report message descriptor. 54 | * @param {Object} messageIds Message IDs from rule metadata 55 | * @returns {{message: string, data: Object}} The interpolated message and data for the descriptor 56 | */ 57 | function normalizeMessagePlaceholders(descriptor, messageIds) { 58 | const message = typeof descriptor.messageId === 'string' ? messageIds[descriptor.messageId] : descriptor.message; 59 | if (!descriptor.data) { 60 | return { 61 | message, 62 | data: typeof descriptor.messageId === 'string' ? {} : null, 63 | }; 64 | } 65 | 66 | const normalizedData = Object.create(null); 67 | const interpolatedMessage = message.replace(/\{\{\s*([^{}]+?)\s*\}\}/g, (fullMatch, term) => { 68 | if (term in descriptor.data) { 69 | normalizedData[term] = descriptor.data[term]; 70 | return descriptor.data[term]; 71 | } 72 | 73 | return fullMatch; 74 | }); 75 | 76 | return { 77 | message: interpolatedMessage, 78 | data: Object.freeze(normalizedData), 79 | }; 80 | } 81 | 82 | function getRuleMeta(rule) { 83 | return typeof rule === 'object' && rule.meta && typeof rule.meta === 'object' 84 | ? rule.meta 85 | : {}; 86 | } 87 | 88 | function getMessageIds(rule) { 89 | const meta = getRuleMeta(rule); 90 | return meta.messages && typeof rule.meta.messages === 'object' 91 | ? meta.messages 92 | : {}; 93 | } 94 | 95 | function getReportNormalizer(rule) { 96 | const messageIds = getMessageIds(rule); 97 | 98 | return function normalizeReport() { 99 | const descriptor = normalizeMultiArgReportCall.apply(null, arguments); 100 | const interpolatedMessageAndData = normalizeMessagePlaceholders(descriptor, messageIds); 101 | 102 | return { 103 | node: descriptor.node, 104 | message: interpolatedMessageAndData.message, 105 | messageId: typeof descriptor.messageId === 'string' ? descriptor.messageId : null, 106 | data: typeof descriptor.messageId === 'string' ? interpolatedMessageAndData.data : null, 107 | loc: normalizeReportLoc(descriptor), 108 | fix: descriptor.fix, 109 | }; 110 | }; 111 | } 112 | 113 | function getRuleCreateFunc(rule) { 114 | return typeof rule === 'function' ? rule : rule.create; 115 | } 116 | 117 | function removeMessageIfMessageIdPresent(reportDescriptor) { 118 | const newDescriptor = Object.assign({}, reportDescriptor); 119 | 120 | if (typeof reportDescriptor.messageId === 'string' && typeof reportDescriptor.message === 'string') { 121 | delete newDescriptor.message; 122 | } 123 | 124 | return newDescriptor; 125 | } 126 | 127 | module.exports = Object.freeze({ 128 | filterReports(rule, predicate) { 129 | return Object.freeze({ 130 | create(context) { 131 | const filename = context.getFilename(); 132 | const sourceCode = context.getSourceCode(); 133 | const settings = context.settings; 134 | const options = context.options; 135 | return getRuleCreateFunc(rule)( 136 | Object.freeze( 137 | Object.create( 138 | context, 139 | { 140 | report: { 141 | enumerable: true, 142 | value() { 143 | const reportDescriptor = getReportNormalizer(rule).apply(null, arguments); 144 | if (predicate(reportDescriptor, { 145 | sourceCode, settings, options, filename, 146 | })) { 147 | context.report(removeMessageIfMessageIdPresent(reportDescriptor)); 148 | } 149 | }, 150 | }, 151 | } 152 | ) 153 | ) 154 | ); 155 | }, 156 | schema: rule.schema, 157 | meta: getRuleMeta(rule), 158 | }); 159 | }, 160 | mapReports(rule, iteratee) { 161 | return Object.freeze({ 162 | create(context) { 163 | const filename = context.getFilename(); 164 | const sourceCode = context.getSourceCode(); 165 | const settings = context.settings; 166 | const options = context.options; 167 | return getRuleCreateFunc(rule)( 168 | Object.freeze( 169 | Object.create( 170 | context, 171 | { 172 | report: { 173 | enumerable: true, 174 | value() { 175 | context.report( 176 | removeMessageIfMessageIdPresent( 177 | iteratee( 178 | getReportNormalizer(rule).apply(null, arguments), 179 | { 180 | sourceCode, settings, options, filename, 181 | } 182 | ) 183 | ) 184 | ); 185 | }, 186 | }, 187 | } 188 | ) 189 | ) 190 | ); 191 | }, 192 | schema: rule.schema, 193 | meta: getRuleMeta(rule), 194 | }); 195 | }, 196 | joinReports(rules) { 197 | return Object.freeze({ 198 | create(context) { 199 | return rules 200 | .map(rule => getRuleCreateFunc(rule)(context)) 201 | .reduce( 202 | (allListeners, ruleListeners) => 203 | Object.keys(ruleListeners).reduce( 204 | (combinedListeners, key) => { 205 | const currentListener = combinedListeners[key]; 206 | const ruleListener = ruleListeners[key]; 207 | if (currentListener) { 208 | return Object.assign({}, combinedListeners, { 209 | [key]() { 210 | currentListener.apply(null, arguments); 211 | ruleListener.apply(null, arguments); 212 | }, 213 | }); 214 | } 215 | return Object.assign({}, combinedListeners, { [key]: ruleListener }); 216 | }, 217 | allListeners 218 | ), 219 | Object.create(null) 220 | ); 221 | }, 222 | meta: Object.freeze({ 223 | messages: Object.assign.apply( 224 | null, 225 | [Object.create(null)].concat(rules.map(getMessageIds)) 226 | ), 227 | fixable: 'code', 228 | }), 229 | }); 230 | }, 231 | }); 232 | -------------------------------------------------------------------------------- /tests/lib/rule-composer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const eslint = require('eslint'); 4 | const assert = require('chai').assert; 5 | const ruleComposer = require('../..'); 6 | 7 | const RuleTester = eslint.RuleTester; 8 | const ruleTester = new RuleTester(); 9 | const coreRules = new eslint.Linter().getRules(); 10 | 11 | ruleTester.run( 12 | 'filterReports', 13 | ruleComposer.filterReports(coreRules.get('no-undef'), descriptor => descriptor.node && descriptor.node.name !== 'foo'), 14 | { 15 | valid: [ 16 | 'foo;', 17 | 'var bar; bar;', 18 | ], 19 | invalid: [ 20 | { 21 | code: 'bar;', 22 | errors: [{ line: 1, column: 1 }], 23 | }, 24 | { 25 | code: 'foo; bar;', 26 | errors: [{ line: 1, column: 6 }], 27 | }, 28 | { 29 | code: 'bar; foo;', 30 | errors: [{ line: 1, column: 1 }], 31 | }, 32 | ], 33 | } 34 | ); 35 | 36 | // test with settings 37 | 38 | ruleTester.run( 39 | 'filterReports - with settings', 40 | ruleComposer.filterReports(coreRules.get('no-undef'), (descriptor, m) => ( 41 | descriptor.node && m.settings.tokenWhitelist.indexOf(descriptor.node.name) === -1 42 | )), 43 | { 44 | valid: [ 45 | { 46 | code: 'foo;', 47 | settings: { tokenWhitelist: ['foo'] }, 48 | }, 49 | { 50 | code: 'var bar; bar;', 51 | settings: { tokenWhitelist: ['foo'] }, 52 | }, 53 | ], 54 | invalid: [ 55 | { 56 | code: 'bar;', 57 | errors: [{ line: 1, column: 1 }], 58 | settings: { tokenWhitelist: ['foo'] }, 59 | }, 60 | { 61 | code: 'foo; bar;', 62 | errors: [{ line: 1, column: 6 }], 63 | settings: { tokenWhitelist: ['foo'] }, 64 | }, 65 | { 66 | code: 'bar; foo;', 67 | errors: [{ line: 1, column: 1 }], 68 | settings: { tokenWhitelist: ['foo'] }, 69 | }, 70 | ], 71 | } 72 | ); 73 | 74 | const ruleWithOptions = ruleComposer.filterReports(coreRules.get('no-undef'), (descriptor, m) => ( 75 | descriptor.node && m.options[0].tokenWhitelist.indexOf(descriptor.node.name) === -1 76 | )); 77 | 78 | // overwrite schema to allow custom options... 79 | ruleWithOptions.meta.schema[0].additionalProperties = true; 80 | 81 | ruleTester.run( 82 | 'filterReports - with options', 83 | ruleWithOptions, 84 | { 85 | valid: [ 86 | { 87 | code: 'foo;', 88 | options: [{ tokenWhitelist: ['foo'] }], 89 | }, 90 | { 91 | code: 'var bar; bar;', 92 | options: [{ tokenWhitelist: ['foo'] }], 93 | }, 94 | ], 95 | invalid: [ 96 | { 97 | code: 'bar;', 98 | errors: [{ line: 1, column: 1 }], 99 | options: [{ tokenWhitelist: ['foo'] }], 100 | }, 101 | { 102 | code: 'foo; bar;', 103 | errors: [{ line: 1, column: 6 }], 104 | options: [{ tokenWhitelist: ['foo'] }], 105 | }, 106 | { 107 | code: 'bar; foo;', 108 | errors: [{ line: 1, column: 1 }], 109 | options: [{ tokenWhitelist: ['foo'] }], 110 | }, 111 | ], 112 | } 113 | ); 114 | 115 | ruleTester.run( 116 | 'filterReports with filename', 117 | ruleComposer.filterReports(coreRules.get('no-undef'), (descriptor, metadata) => { 118 | assert.strictEqual(metadata.filename, 'index.js'); 119 | return descriptor.node && descriptor.node.name !== 'foo'; 120 | }), 121 | { 122 | valid: [ 123 | { 124 | code: 'foo', 125 | filename: 'index.js', 126 | }, 127 | { 128 | code: 'var bar; bar;', 129 | filename: 'index.js', 130 | }, 131 | ], 132 | invalid: [ 133 | { 134 | code: 'bar;', 135 | errors: [{ line: 1, column: 1 }], 136 | filename: 'index.js', 137 | }, 138 | ], 139 | } 140 | ); 141 | 142 | ruleTester.run( 143 | 'joinReports', 144 | ruleComposer.joinReports([ 145 | context => ({ Program: node => context.report(node, 'foo') }), 146 | context => ({ 'Program:exit': node => context.report(node, 'bar') }), 147 | { create: context => ({ 'Program:exit': node => context.report(node, 'baz') }) }, 148 | ]), 149 | { 150 | valid: [], 151 | invalid: [ 152 | { 153 | code: 'a', 154 | errors: [ 155 | { type: 'Program', message: 'foo' }, 156 | { type: 'Program', message: 'bar' }, 157 | { type: 'Program', message: 'baz' }, 158 | ], 159 | }, 160 | ], 161 | } 162 | ); 163 | 164 | ruleTester.run( 165 | 'mapReports', 166 | ruleComposer.mapReports( 167 | context => ({ Program: node => context.report({ node, message: 'foo' }) }), 168 | descriptor => Object.assign({}, descriptor, { message: descriptor.message.toUpperCase() }) 169 | ), 170 | { 171 | valid: [], 172 | invalid: [ 173 | { 174 | code: 'a', 175 | errors: [ 176 | { type: 'Program', message: 'FOO' }, 177 | ], 178 | }, 179 | ], 180 | } 181 | ); 182 | 183 | // test with settings 184 | 185 | ruleTester.run( 186 | 'mapReports - with settings', 187 | ruleComposer.mapReports( 188 | context => ({ Program: node => context.report({ node, message: 'foo' }) }), 189 | (descriptor, m) => Object.assign({}, descriptor, { message: descriptor.message[m.settings.method]() }) 190 | ), 191 | { 192 | valid: [], 193 | invalid: [ 194 | { 195 | code: 'a', 196 | errors: [ 197 | { type: 'Program', message: 'FOO' }, 198 | ], 199 | settings: { method: 'toUpperCase' }, 200 | }, 201 | ], 202 | } 203 | ); 204 | 205 | // test with settings 206 | 207 | ruleTester.run( 208 | 'mapReports - with options', 209 | ruleComposer.mapReports( 210 | context => ({ Program: node => context.report({ node, message: 'foo' }) }), 211 | (descriptor, m) => Object.assign({}, descriptor, { message: descriptor.message[m.options[0].method]() }) 212 | ), 213 | { 214 | valid: [], 215 | invalid: [ 216 | { 217 | code: 'a', 218 | errors: [ 219 | { type: 'Program', message: 'FOO' }, 220 | ], 221 | options: [{ method: 'toUpperCase' }], 222 | }, 223 | ], 224 | } 225 | ); 226 | 227 | ruleTester.run( 228 | 'mapReports with filename', 229 | ruleComposer.mapReports( 230 | context => ({ Program: node => context.report({ node, message: 'foo' }) }), 231 | (descriptor, metadata) => Object.assign({}, descriptor, { message: metadata.filename }) 232 | ), 233 | { 234 | valid: [], 235 | invalid: [ 236 | { 237 | code: 'a', 238 | filename: 'test.js', 239 | errors: [ 240 | { type: 'Program', message: 'test.js' }, 241 | ], 242 | }, 243 | ], 244 | } 245 | ); 246 | 247 | ruleTester.run( 248 | 'checking the first token of the report', 249 | ruleComposer.filterReports( 250 | coreRules.get('no-unused-expressions'), 251 | (problem, metadata) => metadata.sourceCode.getFirstToken(problem.node).value !== 'expect' 252 | ), 253 | { 254 | valid: [ 255 | 'expect(foo).to.be.true;', 256 | 'expect;', 257 | ], 258 | invalid: [ 259 | { 260 | code: 'foo;', 261 | errors: 1, 262 | }, 263 | ], 264 | } 265 | ); 266 | 267 | ruleTester.run( 268 | 'composing rules that use messageId', 269 | ruleComposer.filterReports( 270 | { 271 | meta: { 272 | messages: { 273 | foo: 'Foo error.', 274 | bar: 'Bar error.', 275 | baz: 'Baz error {{myData}}.', 276 | }, 277 | }, 278 | create(context) { 279 | return { 280 | Program(node) { 281 | context.report({ node, messageId: 'foo' }); 282 | context.report({ node, messageId: 'bar' }); 283 | context.report({ node, messageId: 'baz', data: { myData: 'BAZ', otherData: 'blah' } }); 284 | context.report({ node, message: 'Not message id {{aa}}', data: { aa: 'foo' } }); 285 | }, 286 | }; 287 | }, 288 | }, 289 | (problem) => { 290 | if (problem.messageId === 'baz') { 291 | assert.strictEqual(problem.message, 'Baz error BAZ.'); 292 | assert.deepEqual(problem.data, { myData: 'BAZ' }); 293 | } else if (problem.messageId === 'foo') { 294 | assert.strictEqual(problem.message, 'Foo error.'); 295 | assert.deepEqual(problem.data, {}); 296 | } else if (problem.messageId === 'bar') { 297 | assert.strictEqual(problem.message, 'Bar error.'); 298 | assert.deepEqual(problem.data, {}); 299 | } else if (problem.messageId === null) { 300 | assert.strictEqual(problem.message, 'Not message id foo'); 301 | assert.strictEqual(problem.data, null); 302 | } else { 303 | assert.fail('Unexpected reported problem'); 304 | } 305 | 306 | return problem.message === 'Foo error.' || problem.messageId === 'baz'; 307 | } 308 | ), 309 | { 310 | valid: [], 311 | invalid: [ 312 | { 313 | code: 'x', 314 | errors: [ 315 | { type: 'Program', message: 'Foo error.' }, 316 | { type: 'Program', message: 'Baz error BAZ.' }, 317 | ], 318 | }, 319 | ], 320 | } 321 | ); 322 | --------------------------------------------------------------------------------