diff --git a/docs/src/extend/custom-rules-deprecated.md b/docs/src/extend/custom-rules-deprecated.md index 49faa4f13c7..8e94b6dd206 100644 --- a/docs/src/extend/custom-rules-deprecated.md +++ b/docs/src/extend/custom-rules-deprecated.md @@ -3,578 +3,6 @@ title: Working with Rules (Deprecated) --- -**Note:** This page covers the deprecated rule format for ESLint <= 2.13.1. [This is the most recent rule format](./custom-rules). +As of ESLint v9.0.0, the function-style rule format that was current in ESLint <= 2.13.1 is no longer supported. -Each rule in ESLint has two files named with its identifier (for example, `no-extra-semi`). - -* in the `lib/rules` directory: a source file (for example, `no-extra-semi.js`) -* in the `tests/lib/rules` directory: a test file (for example, `no-extra-semi.js`) - -**Important:** If you submit a **core** rule to the ESLint repository, you **must** follow some conventions explained below. - -Here is the basic format of the source file for a rule: - -```js -/** - * @fileoverview Rule to disallow unnecessary semicolons - * @author Nicholas C. Zakas - */ - -"use strict"; - -//------------------------------------------------------------------------------ -// Rule Definition -//------------------------------------------------------------------------------ - -module.exports = function(context) { - return { - // callback functions - }; -}; - -module.exports.schema = []; // no options -``` - -## Rule Basics - -`schema` (array) specifies the [options](#options-schemas) so ESLint can prevent invalid [rule configurations](../use/configure/rules) - -`create` (function) returns an object with methods that ESLint calls to "visit" nodes while traversing the abstract syntax tree (AST as defined by [ESTree](https://github.com/estree/estree)) of JavaScript code: - -* if a key is a node type, ESLint calls that **visitor** function while going **down** the tree -* if a key is a node type plus `:exit`, ESLint calls that **visitor** function while going **up** the tree -* if a key is an event name, ESLint calls that **handler** function for [code path analysis](code-path-analysis) - -A rule can use the current node and its surrounding tree to report or fix problems. - -Here are methods for the [array-callback-return](../rules/array-callback-return) rule: - -```js -function checkLastSegment (node) { - // report problem for function if last code path segment is reachable -} - -module.exports = function(context) { - // declare the state of the rule - return { - ReturnStatement: function(node) { - // at a ReturnStatement node while going down - }, - // at a function expression node while going up: - "FunctionExpression:exit": checkLastSegment, - "ArrowFunctionExpression:exit": checkLastSegment, - onCodePathStart: function (codePath, node) { - // at the start of analyzing a code path - }, - onCodePathEnd: function(codePath, node) { - // at the end of analyzing a code path - } - }; -}; -``` - -## The Context Object - -The `context` object contains additional functionality that is helpful for rules to do their jobs. As the name implies, the `context` object contains information that is relevant to the context of the rule. The `context` object has the following properties: - -* `parserOptions` - the parser options configured for this run (more details [here](../use/configure/language-options#specifying-parser-options)). -* `id` - the rule ID. -* `options` - an array of rule options. -* `settings` - the `settings` from configuration. -* `parserPath` - the full path to the `parser` from configuration. - -Additionally, the `context` object has the following methods: - -* `getAncestors()` - returns an array of ancestor nodes based on the current traversal. -* `getDeclaredVariables(node)` - returns the declared variables on the given node. -* `getFilename()` - returns the filename associated with the source. -* `getScope()` - returns the current scope. -* `getSourceCode()` - returns a `SourceCode` object that you can use to work with the source that was passed to ESLint -* `markVariableAsUsed(name)` - marks the named variable in scope as used. This affects the [no-unused-vars](../rules/no-unused-vars) rule. -* `report(descriptor)` - reports a problem in the code. - -**Deprecated:** The following methods on the `context` object are deprecated. Please use the corresponding methods on `SourceCode` instead: - -* `getAllComments()` - returns an array of all comments in the source. Use `sourceCode.getAllComments()` instead. -* `getComments(node)` - returns the leading and trailing comments arrays for the given node. Use `sourceCode.getComments(node)` instead. -* `getFirstToken(node)` - returns the first token representing the given node. Use `sourceCode.getFirstToken(node)` instead. -* `getFirstTokens(node, count)` - returns the first `count` tokens representing the given node. Use `sourceCode.getFirstTokens(node, count)` instead. -* `getJSDocComment(node)` - returns the JSDoc comment for a given node or `null` if there is none. Use `sourceCode.getJSDocComment(node)` instead. -* `getLastToken(node)` - returns the last token representing the given node. Use `sourceCode.getLastToken(node)` instead. -* `getLastTokens(node, count)` - returns the last `count` tokens representing the given node. Use `sourceCode.getLastTokens(node, count)` instead. -* `getNodeByRangeIndex(index)` - returns the deepest node in the AST containing the given source index. Use `sourceCode.getNodeByRangeIndex(index)` instead. -* `getSource(node)` - returns the source code for the given node. Omit `node` to get the whole source. Use `sourceCode.getText(node)` instead. -* `getSourceLines()` - returns the entire source code split into an array of string lines. Use `sourceCode.lines` instead. -* `getTokenAfter(nodeOrToken)` - returns the first token after the given node or token. Use `sourceCode.getTokenAfter(nodeOrToken)` instead. -* `getTokenBefore(nodeOrToken)` - returns the first token before the given node or token. Use `sourceCode.getTokenBefore(nodeOrToken)` instead. -* `getTokenByRangeStart(index)` - returns the token whose range starts at the given index in the source. Use `sourceCode.getTokenByRangeStart(index)` instead. -* `getTokens(node)` - returns all tokens for the given node. Use `sourceCode.getTokens(node)` instead. -* `getTokensAfter(nodeOrToken, count)` - returns `count` tokens after the given node or token. Use `sourceCode.getTokensAfter(nodeOrToken, count)` instead. -* `getTokensBefore(nodeOrToken, count)` - returns `count` tokens before the given node or token. Use `sourceCode.getTokensBefore(nodeOrToken, count)` instead. -* `getTokensBetween(node1, node2)` - returns the tokens between two nodes. Use `sourceCode.getTokensBetween(node1, node2)` instead. -* `report(node, [location], message)` - reports a problem in the code. - -### context.report() - -The main method you'll use is `context.report()`, which publishes a warning or error (depending on the configuration being used). This method accepts a single argument, which is an object containing the following properties: - -* `message` - the problem message. -* `node` - (optional) the AST node related to the problem. If present and `loc` is not specified, then the starting location of the node is used as the location of the problem. -* `loc` - (optional) an object specifying the location of the problem. If both `loc` and `node` are specified, then the location is used from `loc` instead of `node`. - * `line` - the 1-based line number at which the problem occurred. - * `column` - the 0-based column number at which the problem occurred. -* `data` - (optional) placeholder data for `message`. -* `fix` - (optional) a function that applies a fix to resolve the problem. - -Note that at least one of `node` or `loc` is required. - -The simplest example is to use just `node` and `message`: - -```js -context.report({ - node: node, - message: "Unexpected identifier" -}); -``` - -The node contains all of the information necessary to figure out the line and column number of the offending text as well the source text representing the node. - -You can also use placeholders in the message and provide `data`: - -```js -{% raw %} -context.report({ - node: node, - message: "Unexpected identifier: {{ identifier }}", - data: { - identifier: node.name - } -}); -{% endraw %} -``` - -Note that leading and trailing whitespace is optional in message parameters. - -The node contains all of the information necessary to figure out the line and column number of the offending text as well the source text representing the node. - -### Applying Fixes - -If you'd like ESLint to attempt to fix the problem you're reporting, you can do so by specifying the `fix` function when using `context.report()`. The `fix` function receives a single argument, a `fixer` object, that you can use to apply a fix. For example: - -```js -context.report({ - node: node, - message: "Missing semicolon". - fix: function(fixer) { - return fixer.insertTextAfter(node, ";"); - } -}); -``` - -Here, the `fix()` function is used to insert a semicolon after the node. Note that the fix is not immediately applied and may not be applied at all if there are conflicts with other fixes. If the fix cannot be applied, then the problem message is reported as usual; if the fix can be applied, then the problem message is not reported. - -The `fixer` object has the following methods: - -* `insertTextAfter(nodeOrToken, text)` - inserts text after the given node or token -* `insertTextAfterRange(range, text)` - inserts text after the given range -* `insertTextBefore(nodeOrToken, text)` - inserts text before the given node or token -* `insertTextBeforeRange(range, text)` - inserts text before the given range -* `remove(nodeOrToken)` - removes the given node or token -* `removeRange(range)` - removes text in the given range -* `replaceText(nodeOrToken, text)` - replaces the text in the given node or token -* `replaceTextRange(range, text)` - replaces the text in the given range - -Best practices for fixes: - -1. Make fixes that are as small as possible. Anything more than a single character is risky and could prevent other, simpler fixes from being made. -1. Only make one fix per message. This is enforced because you must return the result of the fixer operation from `fix()`. -1. Fixes should not introduce clashes with other rules. You can accidentally introduce a new problem that won't be reported until ESLint is run again. Another good reason to make as small a fix as possible. - -### context.options - -Some rules require options in order to function correctly. These options appear in configuration (`.eslintrc`, command line, or in comments). For example: - -```json -{ - "quotes": [2, "double"] -} -``` - -The `quotes` rule in this example has one option, `"double"` (the `2` is the error level). You can retrieve the options for a rule by using `context.options`, which is an array containing every configured option for the rule. In this case, `context.options[0]` would contain `"double"`: - -```js -module.exports = function(context) { - - var isDouble = (context.options[0] === "double"); - - // ... -} -``` - -Since `context.options` is just an array, you can use it to determine how many options have been passed as well as retrieving the actual options themselves. Keep in mind that the error level is not part of `context.options`, as the error level cannot be known or modified from inside a rule. - -When using options, make sure that your rule has some logic defaults in case the options are not provided. - -### context.getSourceCode() - -The `SourceCode` object is the main object for getting more information about the source code being linted. You can retrieve the `SourceCode` object at any time by using the `getSourceCode()` method: - -```js -module.exports = function(context) { - - var sourceCode = context.getSourceCode(); - - // ... -} -``` - -Once you have an instance of `SourceCode`, you can use the methods on it to work with the code: - -* `getAllComments()` - returns an array of all comments in the source. -* `getComments(node)` - returns the leading and trailing comments arrays for the given node. -* `getFirstToken(node)` - returns the first token representing the given node. -* `getFirstTokens(node, count)` - returns the first `count` tokens representing the given node. -* `getJSDocComment(node)` - returns the JSDoc comment for a given node or `null` if there is none. -* `getLastToken(node)` - returns the last token representing the given node. -* `getLastTokens(node, count)` - returns the last `count` tokens representing the given node. -* `getNodeByRangeIndex(index)` - returns the deepest node in the AST containing the given source index. -* `isSpaceBetweenTokens(first, second)` - returns true if there is a whitespace character between the two tokens. -* `getText(node)` - returns the source code for the given node. Omit `node` to get the whole source. -* `getTokenAfter(nodeOrToken)` - returns the first token after the given node or token. -* `getTokenBefore(nodeOrToken)` - returns the first token before the given node or token. -* `getTokenByRangeStart(index)` - returns the token whose range starts at the given index in the source. -* `getTokens(node)` - returns all tokens for the given node. -* `getTokensAfter(nodeOrToken, count)` - returns `count` tokens after the given node or token. -* `getTokensBefore(nodeOrToken, count)` - returns `count` tokens before the given node or token. -* `getTokensBetween(node1, node2)` - returns the tokens between two nodes. - -There are also some properties you can access: - -* `hasBOM` - the flag to indicate whether or not the source code has Unicode BOM. -* `text` - the full text of the code being linted. Unicode BOM has been stripped from this text. -* `ast` - the `Program` node of the AST for the code being linted. -* `lines` - an array of lines, split according to the specification's definition of line breaks. - -You should use a `SourceCode` object whenever you need to get more information about the code being linted. - -### Options Schemas - -Rules may export a `schema` property, which is a [JSON schema](http://json-schema.org/) format description of a rule's options which will be used by ESLint to validate configuration options and prevent invalid or unexpected inputs before they are passed to the rule in `context.options`. - -There are two formats for a rule's exported `schema`. The first is a full JSON Schema object describing all possible options the rule accepts, including the rule's error level as the first argument and any optional arguments thereafter. - -However, to simplify schema creation, rules may also export an array of schemas for each optional positional argument, and ESLint will automatically validate the required error level first. For example, the `yoda` rule accepts a primary mode argument, as well as an extra options object with named properties. - -```js -// "yoda": [2, "never", { "exceptRange": true }] -module.exports.schema = [ - { - "enum": ["always", "never"] - }, - { - "type": "object", - "properties": { - "exceptRange": { - "type": "boolean" - } - }, - "additionalProperties": false - } -]; -``` - -In the preceding example, the error level is assumed to be the first argument. It is followed by the first optional argument, a string which may be either `"always"` or `"never"`. The final optional argument is an object, which may have a Boolean property named `exceptRange`. - -To learn more about JSON Schema, we recommend looking at some [examples](http://json-schema.org/examples.html) to start, and also reading [Understanding JSON Schema](http://spacetelescope.github.io/understanding-json-schema/) (a free ebook). - -### Getting the Source - -If your rule needs to get the actual JavaScript source to work with, then use the `sourceCode.getText()` method. This method works as follows: - -```js - -// get all source -var source = sourceCode.getText(); - -// get source for just this AST node -var nodeSource = sourceCode.getText(node); - -// get source for AST node plus previous two characters -var nodeSourceWithPrev = sourceCode.getText(node, 2); - -// get source for AST node plus following two characters -var nodeSourceWithFollowing = sourceCode.getText(node, 0, 2); -``` - -In this way, you can look for patterns in the JavaScript text itself when the AST isn't providing the appropriate data (such as location of commas, semicolons, parentheses, etc.). - -### Accessing comments - -If you need to access comments for a specific node you can use `sourceCode.getComments(node)`: - -```js -// the "comments" variable has a "leading" and "trailing" property containing -// its leading and trailing comments, respectively -var comments = sourceCode.getComments(node); -``` - -Keep in mind that comments are technically not a part of the AST and are only attached to it on demand, i.e. when you call `getComments()`. - -**Note:** One of the libraries adds AST node properties for comments - do not use these properties. Always use `sourceCode.getComments()` as this is the only guaranteed API for accessing comments (we will likely change how comments are handled later). - -### Accessing Code Paths - -ESLint analyzes code paths while traversing AST. -You can access that code path objects with five events related to code paths. - -[details here](code-path-analysis) - -## Rule Unit Tests - -Each rule must have a set of unit tests submitted with it to be accepted. The test file is named the same as the source file but lives in `tests/lib/`. For example, if your rule source file is `lib/rules/foo.js` then your test file should be `tests/lib/rules/foo.js`. - -For your rule, be sure to test: - -1. All instances that should be flagged as warnings. -1. At least one pattern that should **not** be flagged as a warning. - -The basic pattern for a rule unit test file is: - -```js -/** - * @fileoverview Tests for no-with rule. - * @author Nicholas C. Zakas - */ - -"use strict"; - -//------------------------------------------------------------------------------ -// Requirements -//------------------------------------------------------------------------------ - -var rule = require("../../../lib/rules/no-with"), - RuleTester = require("../../../lib/testers/rule-tester"); - -//------------------------------------------------------------------------------ -// Tests -//------------------------------------------------------------------------------ - -var ruleTester = new RuleTester(); -ruleTester.run("no-with", rule, { - valid: [ - "foo.bar()" - ], - invalid: [ - { - code: "with(foo) { bar() }", - errors: [{ message: "Unexpected use of 'with' statement.", type: "WithStatement"}] - } - ] -}); -``` - -Be sure to replace the value of `"no-with"` with your rule's ID. There are plenty of examples in the `tests/lib/rules/` directory. - -### Valid Code - -Each valid case can be either a string or an object. The object form is used when you need to specify additional global variables or arguments for the rule. For example, the following defines `window` as a global variable for code that should not trigger the rule being tested: - -```js -valid: [ - { - code: "window.alert()", - globals: [ "window" ] - } -] -``` - -You can also pass options to the rule (if it accepts them). These arguments are equivalent to how people can configure rules in their `.eslintrc` file. For example: - -```js -valid: [ - { - code: "var msg = 'Hello';", - options: [ "single" ] - } -] -``` - -The `options` property must be an array of options. This gets passed through to `context.options` in the rule. - -### Invalid Code - -Each invalid case must be an object containing the code to test and at least one message that is produced by the rule. The `errors` key specifies an array of objects, each containing a message (your rule may trigger multiple messages for the same code). You should also specify the type of AST node you expect to receive back using the `type` key. The AST node should represent the actual spot in the code where there is a problem. For example: - -```js -invalid: [ - { - code: "function doSomething() { var f; if (true) { var build = true; } f = build; }", - errors: [ - { message: "build used outside of binding context.", type: "Identifier" } - ] - } -] -``` - -In this case, the message is specific to the variable being used and the AST node type is `Identifier`. - -Similar to the valid cases, you can also specify `options` to be passed to the rule: - -```js -invalid: [ - { - code: "function doSomething() { var f; if (true) { var build = true; } f = build; }", - options: [ "double" ], - errors: [ - { message: "build used outside of binding context.", type: "Identifier" } - ] - } -] -``` - -For simpler cases where the only thing that really matters is the error message, you can also specify any `errors` as strings. You can also have some strings and some objects, if you like. - -```js -invalid: [ - { - code: "'single quotes'", - options: ["double"], - errors: ["Strings must use doublequote."] - } -] -``` - -### Specifying Parser Options - -Some tests require that a certain parser configuration must be used. This can be specified in test specifications via the `parserOptions` setting. - -For example, to set `ecmaVersion` to 6 (in order to use constructs like `for ... of`): - -```js -valid: [ - { - code: "for (x of a) doSomething();", - parserOptions: { ecmaVersion: 6 } - } -] -``` - -If you are working with ES6 modules: - -```js -valid: [ - { - code: "export default function () {};", - parserOptions: { ecmaVersion: 6, sourceType: "module" } - } -] -``` - -For non-version specific features such as JSX: - -```js -valid: [ - { - code: "var foo =
{bar}
", - parserOptions: { ecmaFeatures: { jsx: true } } - } -] -``` - -The options available and the expected syntax for `parserOptions` is the same as those used in [configuration](../use/configure/language-options#specifying-parser-options). - -### Write Several Tests - -Provide as many unit tests as possible. Your pull request will never be turned down for having too many tests submitted with it! - -## Performance Testing - -To keep the linting process efficient and unobtrusive, it is useful to verify the performance impact of new rules or modifications to existing rules. - -### Overall Performance - -The `npm run perf` command gives a high-level overview of ESLint running time with default rules (`eslint:recommended`) enabled. - -```bash -$ git checkout main -Switched to branch 'main' - -$ npm run perf -CPU Speed is 2200 with multiplier 7500000 -Performance Run #1: 1394.689313ms -Performance Run #2: 1423.295351ms -Performance Run #3: 1385.09515ms -Performance Run #4: 1382.406982ms -Performance Run #5: 1409.68566ms -Performance budget ok: 1394.689313ms (limit: 3409.090909090909ms) - -$ git checkout my-rule-branch -Switched to branch 'my-rule-branch' - -$ npm run perf -CPU Speed is 2200 with multiplier 7500000 -Performance Run #1: 1443.736547ms -Performance Run #2: 1419.193291ms -Performance Run #3: 1436.018228ms -Performance Run #4: 1473.605485ms -Performance Run #5: 1457.455283ms -Performance budget ok: 1443.736547ms (limit: 3409.090909090909ms) -``` - -### Per-rule Performance - -ESLint has a built-in method to track performance of individual rules. Setting the `TIMING` environment variable will trigger the display, upon linting completion, of the ten longest-running rules, along with their individual running time and relative performance impact as a percentage of total rule processing time. - -```bash -$ TIMING=1 eslint lib -Rule | Time (ms) | Relative -:-----------------------|----------:|--------: -no-multi-spaces | 52.472 | 6.1% -camelcase | 48.684 | 5.7% -no-irregular-whitespace | 43.847 | 5.1% -valid-jsdoc | 40.346 | 4.7% -handle-callback-err | 39.153 | 4.6% -space-infix-ops | 35.444 | 4.1% -no-undefined | 25.693 | 3.0% -no-shadow | 22.759 | 2.7% -no-empty-class | 21.976 | 2.6% -semi | 19.359 | 2.3% -``` - -To test one rule explicitly, combine the `--no-eslintrc`, and `--rule` options: - -```bash -$ TIMING=1 eslint --no-eslintrc --rule "quotes: [2, 'double']" lib -Rule | Time (ms) | Relative -:------|----------:|--------: -quotes | 18.066 | 100.0% -``` - -## Rule Naming Conventions - -The rule naming conventions for ESLint are fairly simple: - -* If your rule is disallowing something, prefix it with `no-` such as `no-eval` for disallowing `eval()` and `no-debugger` for disallowing `debugger`. -* If your rule is enforcing the inclusion of something, use a short name without a special prefix. -* Keep your rule names as short as possible, use abbreviations where appropriate, and no more than four words. -* Use dashes between words. - -## Rule Acceptance Criteria - -Because rules are highly personal (and therefore very contentious), accepted rules should: - -* Not be library-specific. -* Demonstrate a possible issue that can be resolved by rewriting the code. -* Be general enough so as to apply for a large number of developers. -* Not be the opposite of an existing rule. -* Not overlap with an existing rule. - -## Runtime Rules - -The thing that makes ESLint different from other linters is the ability to define custom rules at runtime. This is perfect for rules that are specific to your project or company and wouldn't make sense for ESLint to ship with. With runtime rules, you don't have to wait for the next version of ESLint or be disappointed that your rule isn't general enough to apply to the larger JavaScript community, just write your rules and include them at runtime. - -Runtime rules are written in the same format as all other rules. Create your rule as you would any other and then follow these steps: - -1. Place all of your runtime rules in the same directory (i.e., `eslint_rules`). -2. Create a [configuration file](../use/configure/) and specify your rule ID error level under the `rules` key. Your rule will not run unless it has a value of `1` or `2` in the configuration file. -3. Run the [command line interface](../use/command-line-interface) using the `--rulesdir` option to specify the location of your runtime rules. +[This is the most recent rule format](./custom-rules). diff --git a/docs/src/extend/custom-rules.md b/docs/src/extend/custom-rules.md index bc7d936f28f..2c3577f00e3 100644 --- a/docs/src/extend/custom-rules.md +++ b/docs/src/extend/custom-rules.md @@ -10,8 +10,6 @@ eleventyNavigation: You can create custom rules to use with ESLint. You might want to create a custom rule if the [core rules](../rules/) do not cover your use case. -**Note:** This page covers the most recent rule format for ESLint >= 3.0.0. There is also a [deprecated rule format](./custom-rules-deprecated). - Here's the basic format of a custom rule: ```js @@ -60,7 +58,7 @@ The source file for a rule exports an object with the following properties. Both **Important:** the `hasSuggestions` property is mandatory for rules that provide suggestions. If this property isn't set to `true`, ESLint will throw an error whenever the rule attempts to produce a suggestion. Omit the `hasSuggestions` property if the rule does not provide suggestions. -* `schema`: (`object | array`) Specifies the [options](#options-schemas) so ESLint can prevent invalid [rule configurations](../use/configure/rules). +* `schema`: (`object | array | false`) Specifies the [options](#options-schemas) so ESLint can prevent invalid [rule configurations](../use/configure/rules). Mandatory when the rule has options. * `deprecated`: (`boolean`) Indicates whether the rule has been deprecated. You may omit the `deprecated` property if the rule has not been deprecated. @@ -482,6 +480,13 @@ The `quotes` rule in this example has one option, `"double"` (the `error` is the ```js module.exports = { + meta: { + schema: [ + { + enum: ["single", "double", "backtick"] + } + ] + }, create: function(context) { var isDouble = (context.options[0] === "double"); @@ -494,6 +499,8 @@ Since `context.options` is just an array, you can use it to determine how many o When using options, make sure that your rule has some logical defaults in case the options are not provided. +Rules with options must specify a [schema](#options-schemas). + ### Accessing the Source Code The `SourceCode` object is the main object for getting more information about the source code being linted. You can retrieve the `SourceCode` object at any time by using the `context.sourceCode` property: @@ -612,9 +619,11 @@ You can also access comments through many of `sourceCode`'s methods using the `i ### Options Schemas -Rules may specify a `schema` property, which is a [JSON Schema](https://json-schema.org/) format description of a rule's options which will be used by ESLint to validate configuration options and prevent invalid or unexpected inputs before they are passed to the rule in `context.options`. +Rules with options must specify a `meta.schema` property, which is a [JSON Schema](https://json-schema.org/) format description of a rule's options which will be used by ESLint to validate configuration options and prevent invalid or unexpected inputs before they are passed to the rule in `context.options`. + +If your rule has options, it is strongly recommended that you specify a schema for options validation. However, it is possible to opt-out of options validation by setting `schema: false`, but doing so is discouraged as it increases the chance of bugs and mistakes. -Note: Prior to ESLint v9.0.0, rules without a schema are passed their options directly from the config without any validation. In ESLint v9.0.0 and later, rules without schemas will throw errors when options are passed. See the [Require schemas and object-style rules](https://github.com/eslint/rfcs/blob/main/designs/2021-schema-object-rules/README.md) RFC for further details. +For rules that don't specify a `meta.schema` property, ESLint throws errors when any options are passed. If your rule doesn't have options, do not set `schema: false`, but simply omit the schema property or use `schema: []`, both of which prevent any options from being passed. When validating a rule's config, there are five steps: diff --git a/lib/config/flat-config-helpers.js b/lib/config/flat-config-helpers.js index e00c56434cd..0280255932c 100644 --- a/lib/config/flat-config-helpers.js +++ b/lib/config/flat-config-helpers.js @@ -5,6 +5,23 @@ "use strict"; +//------------------------------------------------------------------------------ +// Typedefs +//------------------------------------------------------------------------------ + +/** @typedef {import("../shared/types").Rule} Rule */ + +//------------------------------------------------------------------------------ +// Private Members +//------------------------------------------------------------------------------ + +// JSON schema that disallows passing any options +const noOptionsSchema = Object.freeze({ + type: "array", + minItems: 0, + maxItems: 0 +}); + //----------------------------------------------------------------------------- // Functions //----------------------------------------------------------------------------- @@ -52,32 +69,39 @@ function getRuleFromConfig(ruleId, config) { const { pluginName, ruleName } = parseRuleId(ruleId); const plugin = config.plugins && config.plugins[pluginName]; - let rule = plugin && plugin.rules && plugin.rules[ruleName]; - - - // normalize function rules into objects - if (rule && typeof rule === "function") { - rule = { - create: rule - }; - } + const rule = plugin && plugin.rules && plugin.rules[ruleName]; return rule; } /** * Gets a complete options schema for a rule. - * @param {{create: Function, schema: (Array|null)}} rule A new-style rule object - * @returns {Object} JSON Schema for the rule's options. + * @param {Rule} rule A rule object + * @throws {TypeError} If `meta.schema` is specified but is not an array, object or `false`. + * @returns {Object|null} JSON Schema for the rule's options. `null` if `meta.schema` is `false`. */ function getRuleOptionsSchema(rule) { - if (!rule) { + if (!rule.meta) { + return { ...noOptionsSchema }; // default if `meta.schema` is not specified + } + + const schema = rule.meta.schema; + + if (typeof schema === "undefined") { + return { ...noOptionsSchema }; // default if `meta.schema` is not specified + } + + // `schema:false` is an allowed explicit opt-out of options validation for the rule + if (schema === false) { return null; } - const schema = rule.schema || rule.meta && rule.meta.schema; + if (typeof schema !== "object" || schema === null) { + throw new TypeError("Rule's `meta.schema` must be an array or object"); + } + // ESLint-specific array form needs to be converted into a valid JSON Schema definition if (Array.isArray(schema)) { if (schema.length) { return { @@ -87,16 +111,13 @@ function getRuleOptionsSchema(rule) { maxItems: schema.length }; } - return { - type: "array", - minItems: 0, - maxItems: 0 - }; + // `schema:[]` is an explicit way to specify that the rule does not accept any options + return { ...noOptionsSchema }; } - // Given a full schema, leave it alone - return schema || null; + // `schema:` is assumed to be a valid JSON Schema definition + return schema; } diff --git a/lib/config/rule-validator.js b/lib/config/rule-validator.js index eee5b40bd07..a087718c9b4 100644 --- a/lib/config/rule-validator.js +++ b/lib/config/rule-validator.js @@ -66,6 +66,25 @@ function throwRuleNotFoundError({ pluginName, ruleName }, config) { throw new TypeError(errorMessage); } +/** + * The error type when a rule has an invalid `meta.schema`. + */ +class InvalidRuleOptionsSchemaError extends Error { + + /** + * Creates a new instance. + * @param {string} ruleId Id of the rule that has an invalid `meta.schema`. + * @param {Error} processingError Error caught while processing the `meta.schema`. + */ + constructor(ruleId, processingError) { + super( + `Error while processing options validation schema of rule '${ruleId}': ${processingError.message}`, + { cause: processingError } + ); + this.code = "ESLINT_INVALID_RULE_OPTIONS_SCHEMA"; + } +} + //----------------------------------------------------------------------------- // Exports //----------------------------------------------------------------------------- @@ -130,10 +149,14 @@ class RuleValidator { // Precompile and cache validator the first time if (!this.validators.has(rule)) { - const schema = getRuleOptionsSchema(rule); - - if (schema) { - this.validators.set(rule, ajv.compile(schema)); + try { + const schema = getRuleOptionsSchema(rule); + + if (schema) { + this.validators.set(rule, ajv.compile(schema)); + } + } catch (err) { + throw new InvalidRuleOptionsSchemaError(ruleId, err); } } diff --git a/lib/linter/linter.js b/lib/linter/linter.js index 55396b1ad75..bfb08736cba 100644 --- a/lib/linter/linter.js +++ b/lib/linter/linter.js @@ -439,6 +439,15 @@ function getDirectiveComments(sourceCode, ruleMapper, warnInlineConfig) { try { validator.validateRuleOptions(rule, name, ruleValue); } catch (err) { + + /* + * If the rule has invalid `meta.schema`, throw the error because + * this is not an invalid inline configuration but an invalid rule. + */ + if (err.code === "ESLINT_INVALID_RULE_OPTIONS_SCHEMA") { + throw err; + } + problems.push(createLintingProblem({ ruleId: name, message: err.message, @@ -885,12 +894,18 @@ function parse(text, languageOptions, filePath) { /** * Runs a rule, and gets its listeners - * @param {Rule} rule A normalized rule with a `create` method + * @param {Rule} rule A rule object * @param {Context} ruleContext The context that should be passed to the rule + * @throws {TypeError} If `rule` is not an object with a `create` method * @throws {any} Any error during the rule's `create` * @returns {Object} A map of selector listeners provided by the rule */ function createRuleListeners(rule, ruleContext) { + + if (!rule || typeof rule !== "object" || typeof rule.create !== "function") { + throw new TypeError(`Error while loading rule '${ruleContext.id}': Rule must be an object with a \`create\` method`); + } + try { return rule.create(ruleContext); } catch (ex) { @@ -1648,6 +1663,14 @@ class Linter { mergedInlineConfig.rules[ruleId] = ruleValue; } catch (err) { + /* + * If the rule has invalid `meta.schema`, throw the error because + * this is not an invalid inline configuration but an invalid rule. + */ + if (err.code === "ESLINT_INVALID_RULE_OPTIONS_SCHEMA") { + throw err; + } + let baseMessage = err.message.slice( err.message.startsWith("Key \"rules\":") ? err.message.indexOf(":", 12) + 1 @@ -1941,17 +1964,17 @@ class Linter { /** * Defines a new linting rule. * @param {string} ruleId A unique rule identifier - * @param {Function | Rule} ruleModule Function from context to object mapping AST node types to event handlers + * @param {Rule} rule A rule object * @returns {void} */ - defineRule(ruleId, ruleModule) { + defineRule(ruleId, rule) { assertEslintrcConfig(this); - internalSlotsMap.get(this).ruleMap.define(ruleId, ruleModule); + internalSlotsMap.get(this).ruleMap.define(ruleId, rule); } /** * Defines many new linting rules. - * @param {Record} rulesToDefine map from unique rule identifier to rule + * @param {Record} rulesToDefine map from unique rule identifier to rule * @returns {void} */ defineRules(rulesToDefine) { diff --git a/lib/linter/rules.js b/lib/linter/rules.js index 647bab68784..bb8e36823bd 100644 --- a/lib/linter/rules.js +++ b/lib/linter/rules.js @@ -13,18 +13,10 @@ const builtInRules = require("../rules"); //------------------------------------------------------------------------------ -// Helpers +// Typedefs //------------------------------------------------------------------------------ -/** - * Normalizes a rule module to the new-style API - * @param {(Function|{create: Function})} rule A rule object, which can either be a function - * ("old-style") or an object with a `create` method ("new-style") - * @returns {{create: Function}} A new-style rule. - */ -function normalizeRule(rule) { - return typeof rule === "function" ? Object.assign({ create: rule }, rule) : rule; -} +/** @typedef {import("../shared/types").Rule} Rule */ //------------------------------------------------------------------------------ // Public Interface @@ -41,18 +33,17 @@ class Rules { /** * Registers a rule module for rule id in storage. * @param {string} ruleId Rule id (file name). - * @param {Function} ruleModule Rule handler. + * @param {Rule} rule Rule object. * @returns {void} */ - define(ruleId, ruleModule) { - this._rules[ruleId] = normalizeRule(ruleModule); + define(ruleId, rule) { + this._rules[ruleId] = rule; } /** * Access rule handler by id (file name). * @param {string} ruleId Rule id (file name). - * @returns {{create: Function, schema: JsonSchema[]}} - * A rule. This is normalized to always have the new-style shape with a `create` method. + * @returns {Rule} Rule object. */ get(ruleId) { if (typeof this._rules[ruleId] === "string") { diff --git a/lib/rule-tester/flat-rule-tester.js b/lib/rule-tester/flat-rule-tester.js index 0a127c749a0..9f52110e60a 100644 --- a/lib/rule-tester/flat-rule-tester.js +++ b/lib/rule-tester/flat-rule-tester.js @@ -317,6 +317,14 @@ function throwForbiddenMethodError(methodName, prototype) { }; } +const metaSchemaDescription = ` +\t- If the rule has options, set \`meta.schema\` to an array or non-empty object to enable options validation. +\t- If the rule doesn't have options, omit \`meta.schema\` to enforce that no options can be passed to the rule. +\t- You can also set \`meta.schema\` to \`false\` to opt-out of options validation (not recommended). + +\thttps://eslint.org/docs/latest/extend/custom-rules#options-schemas +`; + //------------------------------------------------------------------------------ // Public Interface //------------------------------------------------------------------------------ @@ -490,13 +498,13 @@ class FlatRuleTester { /** * Adds a new rule test to execute. * @param {string} ruleName The name of the rule to run. - * @param {Function | Rule} rule The rule to test. + * @param {Rule} rule The rule to test. * @param {{ * valid: (ValidTestCase | string)[], * invalid: InvalidTestCase[] * }} test The collection of tests to run. - * @throws {TypeError|Error} If non-object `test`, or if a required - * scenario of the given type is missing. + * @throws {TypeError|Error} If `rule` is not an object with a `create` method, + * or if non-object `test`, or if a required scenario of the given type is missing. * @returns {void} */ run(ruleName, rule, test) { @@ -507,6 +515,10 @@ class FlatRuleTester { linter = this.linter, ruleId = `rule-to-test/${ruleName}`; + if (!rule || typeof rule !== "object" || typeof rule.create !== "function") { + throw new TypeError("Rule must be an object with a `create` method"); + } + if (!test || typeof test !== "object") { throw new TypeError(`Test Scenarios for rule ${ruleName} : Could not find test scenario object`); } @@ -560,7 +572,7 @@ class FlatRuleTester { // freezeDeeply(context.languageOptions); - return (typeof rule === "function" ? rule : rule.create)(context); + return rule.create(context); } }) } @@ -652,7 +664,31 @@ class FlatRuleTester { } }); - const schema = getRuleOptionsSchema(rule); + let schema; + + try { + schema = getRuleOptionsSchema(rule); + } catch (err) { + err.message += metaSchemaDescription; + throw err; + } + + /* + * Check and throw an error if the schema is an empty object (`schema:{}`), because such schema + * doesn't validate or enforce anything and is therefore considered a possible error. If the intent + * was to skip options validation, `schema:false` should be set instead (explicit opt-out). + * + * For this purpose, a schema object is considered empty if it doesn't have any own enumerable string-keyed + * properties. While `ajv.compile()` does use enumerable properties from the prototype chain as well, + * it caches compiled schemas by serializing only own enumerable properties, so it's generally not a good idea + * to use inherited properties in schemas because schemas that differ only in inherited properties would end up + * having the same cache entry that would be correct for only one of them. + * + * At this point, `schema` can only be an object or `null`. + */ + if (schema && Object.keys(schema).length === 0) { + throw new Error(`\`schema: {}\` is a no-op${metaSchemaDescription}`); + } /* * Setup AST getters. diff --git a/lib/rule-tester/rule-tester.js b/lib/rule-tester/rule-tester.js index 9718135da07..37730af88df 100644 --- a/lib/rule-tester/rule-tester.js +++ b/lib/rule-tester/rule-tester.js @@ -314,36 +314,6 @@ function throwForbiddenMethodError(methodName) { }; } -/** - * Emit a deprecation warning if function-style format is being used. - * @param {string} ruleName Name of the rule. - * @returns {void} - */ -function emitLegacyRuleAPIWarning(ruleName) { - if (!emitLegacyRuleAPIWarning[`warned-${ruleName}`]) { - emitLegacyRuleAPIWarning[`warned-${ruleName}`] = true; - process.emitWarning( - `"${ruleName}" rule is using the deprecated function-style format and will stop working in ESLint v9. Please use object-style format: https://eslint.org/docs/latest/extend/custom-rules`, - "DeprecationWarning" - ); - } -} - -/** - * Emit a deprecation warning if rule has options but is missing the "meta.schema" property - * @param {string} ruleName Name of the rule. - * @returns {void} - */ -function emitMissingSchemaWarning(ruleName) { - if (!emitMissingSchemaWarning[`warned-${ruleName}`]) { - emitMissingSchemaWarning[`warned-${ruleName}`] = true; - process.emitWarning( - `"${ruleName}" rule has options but is missing the "meta.schema" property and will stop working in ESLint v9. Please add a schema: https://eslint.org/docs/latest/extend/custom-rules#options-schemas`, - "DeprecationWarning" - ); - } -} - /** * Emit a deprecation warning if rule uses CodePath#currentSegments. * @param {string} ruleName Name of the rule. @@ -359,6 +329,13 @@ function emitCodePathCurrentSegmentsWarning(ruleName) { } } +const metaSchemaDescription = ` +\t- If the rule has options, set \`meta.schema\` to an array or non-empty object to enable options validation. +\t- If the rule doesn't have options, omit \`meta.schema\` to enforce that no options can be passed to the rule. +\t- You can also set \`meta.schema\` to \`false\` to opt-out of options validation (not recommended). +\thttps://eslint.org/docs/latest/extend/custom-rules#options-schemas +`; + //------------------------------------------------------------------------------ // Public Interface //------------------------------------------------------------------------------ @@ -533,26 +510,23 @@ class RuleTester { /** * Define a rule for one particular run of tests. * @param {string} name The name of the rule to define. - * @param {Function | Rule} rule The rule definition. + * @param {Rule} rule The rule definition. * @returns {void} */ defineRule(name, rule) { - if (typeof rule === "function") { - emitLegacyRuleAPIWarning(name); - } this.rules[name] = rule; } /** * Adds a new rule test to execute. * @param {string} ruleName The name of the rule to run. - * @param {Function | Rule} rule The rule to test. + * @param {Rule} rule The rule to test. * @param {{ * valid: (ValidTestCase | string)[], * invalid: InvalidTestCase[] * }} test The collection of tests to run. - * @throws {TypeError|Error} If non-object `test`, or if a required - * scenario of the given type is missing. + * @throws {TypeError|Error} If `rule` is not an object with a `create` method, + * or if non-object `test`, or if a required scenario of the given type is missing. * @returns {void} */ run(ruleName, rule, test) { @@ -562,6 +536,10 @@ class RuleTester { scenarioErrors = [], linter = this.linter; + if (!rule || typeof rule !== "object" || typeof rule.create !== "function") { + throw new TypeError("Rule must be an object with a `create` method"); + } + if (!test || typeof test !== "object") { throw new TypeError(`Test Scenarios for rule ${ruleName} : Could not find test scenario object`); } @@ -578,10 +556,6 @@ class RuleTester { ].concat(scenarioErrors).join("\n")); } - if (typeof rule === "function") { - emitLegacyRuleAPIWarning(ruleName); - } - linter.defineRule(ruleName, Object.assign({}, rule, { // Create a wrapper rule that freezes the `context` properties. @@ -590,7 +564,7 @@ class RuleTester { freezeDeeply(context.settings); freezeDeeply(context.parserOptions); - return (typeof rule === "function" ? rule : rule.create)(context); + return rule.create(context); } })); @@ -638,21 +612,36 @@ class RuleTester { if (hasOwnProperty(item, "options")) { assert(Array.isArray(item.options), "options must be an array"); - if ( - item.options.length > 0 && - typeof rule === "object" && - ( - !rule.meta || (rule.meta && (typeof rule.meta.schema === "undefined" || rule.meta.schema === null)) - ) - ) { - emitMissingSchemaWarning(ruleName); - } config.rules[ruleName] = [1].concat(item.options); } else { config.rules[ruleName] = 1; } - const schema = getRuleOptionsSchema(rule); + let schema; + + try { + schema = getRuleOptionsSchema(rule); + } catch (err) { + err.message += metaSchemaDescription; + throw err; + } + + /* + * Check and throw an error if the schema is an empty object (`schema:{}`), because such schema + * doesn't validate or enforce anything and is therefore considered a possible error. If the intent + * was to skip options validation, `schema:false` should be set instead (explicit opt-out). + * + * For this purpose, a schema object is considered empty if it doesn't have any own enumerable string-keyed + * properties. While `ajv.compile()` does use enumerable properties from the prototype chain as well, + * it caches compiled schemas by serializing only own enumerable properties, so it's generally not a good idea + * to use inherited properties in schemas because schemas that differ only in inherited properties would end up + * having the same cache entry that would be correct for only one of them. + * + * At this point, `schema` can only be an object or `null`. + */ + if (schema && Object.keys(schema).length === 0) { + throw new Error(`\`schema: {}\` is a no-op${metaSchemaDescription}`); + } /* * Setup AST getters. diff --git a/lib/rules/no-constructor-return.js b/lib/rules/no-constructor-return.js index d7d98939b9a..075ec918571 100644 --- a/lib/rules/no-constructor-return.js +++ b/lib/rules/no-constructor-return.js @@ -20,7 +20,7 @@ module.exports = { url: "https://eslint.org/docs/latest/rules/no-constructor-return" }, - schema: {}, + schema: [], fixable: null, diff --git a/lib/shared/config-validator.js b/lib/shared/config-validator.js index 47353ac4814..8ff1f44874c 100644 --- a/lib/shared/config-validator.js +++ b/lib/shared/config-validator.js @@ -17,6 +17,23 @@ "use strict"; +//------------------------------------------------------------------------------ +// Typedefs +//------------------------------------------------------------------------------ + +/** @typedef {import("../shared/types").Rule} Rule */ + +//------------------------------------------------------------------------------ +// Private Members +//------------------------------------------------------------------------------ + +// JSON schema that disallows passing any options +const noOptionsSchema = Object.freeze({ + type: "array", + minItems: 0, + maxItems: 0 +}); + //------------------------------------------------------------------------------ // Requirements //------------------------------------------------------------------------------ @@ -49,17 +66,36 @@ const severityMap = { /** * Gets a complete options schema for a rule. - * @param {{create: Function, schema: (Array|null)}} rule A new-style rule object - * @returns {Object} JSON Schema for the rule's options. + * @param {Rule} rule A rule object + * @throws {TypeError} If `meta.schema` is specified but is not an array, object or `false`. + * @returns {Object|null} JSON Schema for the rule's options. + * `null` if rule wasn't passed or its `meta.schema` is `false`. */ function getRuleOptionsSchema(rule) { if (!rule) { return null; } - const schema = rule.schema || rule.meta && rule.meta.schema; + if (!rule.meta) { + return { ...noOptionsSchema }; // default if `meta.schema` is not specified + } + + const schema = rule.meta.schema; + + if (typeof schema === "undefined") { + return { ...noOptionsSchema }; // default if `meta.schema` is not specified + } + + // `schema:false` is an allowed explicit opt-out of options validation for the rule + if (schema === false) { + return null; + } + + if (typeof schema !== "object" || schema === null) { + throw new TypeError("Rule's `meta.schema` must be an array or object"); + } - // Given a tuple of schemas, insert warning level at the beginning + // ESLint-specific array form needs to be converted into a valid JSON Schema definition if (Array.isArray(schema)) { if (schema.length) { return { @@ -69,16 +105,13 @@ function getRuleOptionsSchema(rule) { maxItems: schema.length }; } - return { - type: "array", - minItems: 0, - maxItems: 0 - }; + // `schema:[]` is an explicit way to specify that the rule does not accept any options + return { ...noOptionsSchema }; } - // Given a full schema, leave it alone - return schema || null; + // `schema:` is assumed to be a valid JSON Schema definition + return schema; } /** diff --git a/lib/shared/types.js b/lib/shared/types.js index e3a40bc986b..15666d1c23f 100644 --- a/lib/shared/types.js +++ b/lib/shared/types.js @@ -168,7 +168,7 @@ module.exports = {}; * @property {Record} [configs] The definition of plugin configs. * @property {Record} [environments] The definition of plugin environments. * @property {Record} [processors] The definition of plugin processors. - * @property {Record} [rules] The definition of plugin rules. + * @property {Record} [rules] The definition of plugin rules. */ /** diff --git a/package.json b/package.json index 788d2bb7493..74b522fe0c9 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", + "@eslint/eslintrc": "^3.0.0", "@eslint/js": "8.56.0", "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", diff --git a/tests/fixtures/plugins/node_modules/eslint-plugin-schema-array.js b/tests/fixtures/plugins/node_modules/eslint-plugin-schema-array.js new file mode 100644 index 00000000000..f149940ff0f --- /dev/null +++ b/tests/fixtures/plugins/node_modules/eslint-plugin-schema-array.js @@ -0,0 +1,26 @@ +module.exports = { + rules: { + rule1: { + meta: { + schema: [ + { + type: "string" + } + ] + }, + create(context) { + return { + Program(node) { + const [option] = context.options; + + if (option) { + context.report({ node, message: `Option '${option}' was passed` }); + } else { + context.report({ node, message: `No options were passed` }); + } + } + }; + } + } + }, +}; diff --git a/tests/fixtures/plugins/node_modules/eslint-plugin-schema-empty-array.js b/tests/fixtures/plugins/node_modules/eslint-plugin-schema-empty-array.js new file mode 100644 index 00000000000..e3cd7269395 --- /dev/null +++ b/tests/fixtures/plugins/node_modules/eslint-plugin-schema-empty-array.js @@ -0,0 +1,16 @@ +module.exports = { + rules: { + rule1: { + meta: { + schema: [] + }, + create(context) { + return { + Program(node) { + context.report({ node, message: `Hello` }); + } + }; + } + } + }, +}; diff --git a/tests/fixtures/plugins/node_modules/eslint-plugin-schema-false b/tests/fixtures/plugins/node_modules/eslint-plugin-schema-false new file mode 100644 index 00000000000..02fae974564 --- /dev/null +++ b/tests/fixtures/plugins/node_modules/eslint-plugin-schema-false @@ -0,0 +1,22 @@ +module.exports = { + rules: { + rule1: { + meta: { + schema: false + }, + create(context) { + return { + Program(node) { + const [option] = context.options; + + if (option) { + context.report({ node, message: `Option '${option}' was passed` }); + } else { + context.report({ node, message: `No options were passed` }); + } + } + }; + } + } + }, +}; diff --git a/tests/fixtures/plugins/node_modules/eslint-plugin-schema-invalid.js b/tests/fixtures/plugins/node_modules/eslint-plugin-schema-invalid.js new file mode 100644 index 00000000000..1de7d2e9d2a --- /dev/null +++ b/tests/fixtures/plugins/node_modules/eslint-plugin-schema-invalid.js @@ -0,0 +1,12 @@ +module.exports = { + rules: { + rule1: { + meta: { + schema: { minItems: [] } + }, + create() { + return {}; + } + } + }, +}; diff --git a/tests/fixtures/plugins/node_modules/eslint-plugin-schema-missing.js b/tests/fixtures/plugins/node_modules/eslint-plugin-schema-missing.js new file mode 100644 index 00000000000..4b492d8e082 --- /dev/null +++ b/tests/fixtures/plugins/node_modules/eslint-plugin-schema-missing.js @@ -0,0 +1,13 @@ +module.exports = { + rules: { + rule1: { + create(context) { + return { + Program(node) { + context.report({ node, message: `Hello` }); + } + }; + } + } + }, +}; diff --git a/tests/fixtures/plugins/node_modules/eslint-plugin-schema-null.js b/tests/fixtures/plugins/node_modules/eslint-plugin-schema-null.js new file mode 100644 index 00000000000..eb26e85b189 --- /dev/null +++ b/tests/fixtures/plugins/node_modules/eslint-plugin-schema-null.js @@ -0,0 +1,13 @@ + +module.exports = { + rules: { + rule1: { + meta: { + schema: null + }, + create() { + return {}; + } + } + }, +}; diff --git a/tests/fixtures/plugins/node_modules/eslint-plugin-schema-object.js b/tests/fixtures/plugins/node_modules/eslint-plugin-schema-object.js new file mode 100644 index 00000000000..02bf5b14bde --- /dev/null +++ b/tests/fixtures/plugins/node_modules/eslint-plugin-schema-object.js @@ -0,0 +1,29 @@ +module.exports = { + rules: { + rule1: { + meta: { + schema: { + type: "array", + items: [ + { + type: "string" + } + ] + } + }, + create(context) { + return { + Program(node) { + const [option] = context.options; + + if (option) { + context.report({ node, message: `Option '${option}' was passed` }); + } else { + context.report({ node, message: `No options were passed` }); + } + } + }; + } + } + }, +}; diff --git a/tests/fixtures/plugins/node_modules/eslint-plugin-schema-true.js b/tests/fixtures/plugins/node_modules/eslint-plugin-schema-true.js new file mode 100644 index 00000000000..b8254775a17 --- /dev/null +++ b/tests/fixtures/plugins/node_modules/eslint-plugin-schema-true.js @@ -0,0 +1,12 @@ +module.exports = { + rules: { + rule1: { + meta: { + schema: true + }, + create() { + return {}; + } + } + }, +}; diff --git a/tests/fixtures/plugins/node_modules/eslint-plugin-with-function-style-rules.js b/tests/fixtures/plugins/node_modules/eslint-plugin-with-function-style-rules.js new file mode 100644 index 00000000000..0802e4210c4 --- /dev/null +++ b/tests/fixtures/plugins/node_modules/eslint-plugin-with-function-style-rules.js @@ -0,0 +1,12 @@ +module.exports = { + rules: { + rule1: function (context) { + context.report({ + node: context.getSourceCode().ast, + message: "Rule report from plugin", + }); + + return {}; + }, + }, +}; diff --git a/tests/fixtures/rules/dir1/no-strings.js b/tests/fixtures/rules/dir1/no-strings.js index 1f566ac06d9..4997bbbea58 100644 --- a/tests/fixtures/rules/dir1/no-strings.js +++ b/tests/fixtures/rules/dir1/no-strings.js @@ -1,14 +1,15 @@ "use strict"; -module.exports = function(context) { +module.exports = { + create(context) { + return { - return { + "Literal": function(node) { + if (typeof node.value === 'string') { + context.report(node, "String!"); + } - "Literal": function(node) { - if (typeof node.value === 'string') { - context.report(node, "String!"); } - - } - }; + }; + } }; diff --git a/tests/fixtures/rules/dir2/no-literals.js b/tests/fixtures/rules/dir2/no-literals.js index fdaa2d08cba..5dfbcb869b3 100644 --- a/tests/fixtures/rules/dir2/no-literals.js +++ b/tests/fixtures/rules/dir2/no-literals.js @@ -1,11 +1,12 @@ "use strict"; -module.exports = function(context) { +module.exports = { + create(context) { + return { - return { - - "Literal": function(node) { - context.report(node, "Literal!"); - } - }; + "Literal": function(node) { + context.report(node, "Literal!"); + } + }; + } }; diff --git a/tests/fixtures/rules/function-style/no-strings.js b/tests/fixtures/rules/function-style/no-strings.js new file mode 100644 index 00000000000..1f566ac06d9 --- /dev/null +++ b/tests/fixtures/rules/function-style/no-strings.js @@ -0,0 +1,14 @@ +"use strict"; + +module.exports = function(context) { + + return { + + "Literal": function(node) { + if (typeof node.value === 'string') { + context.report(node, "String!"); + } + + } + }; +}; diff --git a/tests/fixtures/rules/wrong/custom-rule.js b/tests/fixtures/rules/wrong/custom-rule.js index 9cc26c9ecf2..6ea49963d04 100644 --- a/tests/fixtures/rules/wrong/custom-rule.js +++ b/tests/fixtures/rules/wrong/custom-rule.js @@ -1,3 +1,5 @@ -module.exports = function() { - throw new Error("Boom!"); +module.exports = { + create() { + throw new Error("Boom!"); + } }; diff --git a/tests/lib/cli-engine/cli-engine.js b/tests/lib/cli-engine/cli-engine.js index ad737f3fc57..4b8ee3f4d58 100644 --- a/tests/lib/cli-engine/cli-engine.js +++ b/tests/lib/cli-engine/cli-engine.js @@ -3959,11 +3959,15 @@ describe("CLIEngine", () => { cwd: rootPath, files: { "internal-rules/test.js": ` - module.exports = context => ({ - ExpressionStatement(node) { - context.report({ node, message: "ok" }) - } - }) + module.exports = { + create(context) { + return { + ExpressionStatement(node) { + context.report({ node, message: "ok" }); + }, + }; + }, + }; `, ".eslintrc.json": { root: true, diff --git a/tests/lib/config/flat-config-array.js b/tests/lib/config/flat-config-array.js index 6bc5a92278d..54df478b929 100644 --- a/tests/lib/config/flat-config-array.js +++ b/tests/lib/config/flat-config-array.js @@ -1774,6 +1774,212 @@ describe("FlatConfigArray", () => { ], /Value \[\] should NOT have fewer than 1 items/u); }); + [null, true, 0, 1, "", "always", () => {}].forEach(schema => { + it(`should error with a message that contains the rule name when a configured rule has invalid \`meta.schema\` (${schema})`, async () => { + + await assertInvalidConfig([ + { + plugins: { + foo: { + rules: { + bar: { + meta: { + schema + } + } + } + } + }, + rules: { + "foo/bar": "error" + } + } + ], "Error while processing options validation schema of rule 'foo/bar': Rule's `meta.schema` must be an array or object"); + }); + }); + + it("should error with a message that contains the rule name when a configured rule has invalid `meta.schema` (invalid JSON Schema definition)", async () => { + + await assertInvalidConfig([ + { + plugins: { + foo: { + rules: { + bar: { + meta: { + schema: { minItems: [] } + } + } + } + } + }, + rules: { + "foo/bar": "error" + } + } + ], "Error while processing options validation schema of rule 'foo/bar': minItems must be number"); + }); + + it("should allow rules with `schema:false` to have any configurations", async () => { + + const configs = new FlatConfigArray([ + { + plugins: { + foo: { + rules: { + bar: { + meta: { + schema: false + }, + create() { + return {}; + } + }, + baz: { + meta: { + schema: false + }, + create() { + return {}; + } + } + } + } + } + }, + { + rules: { + "foo/bar": "error", + "foo/baz": ["error", "always"] + } + } + ]); + + await configs.normalize(); + + // does not throw + const config = configs.getConfig("foo.js"); + + assert.deepStrictEqual(config.rules, { + "foo/bar": [2], + "foo/baz": [2, "always"] + }); + }); + + it("should allow rules without `meta` to be configured without options", async () => { + + const configs = new FlatConfigArray([ + { + plugins: { + foo: { + rules: { + bar: { + create() { + return {}; + } + } + } + } + } + }, + { + rules: { + "foo/bar": "error" + } + } + ]); + + await configs.normalize(); + + // does not throw + const config = configs.getConfig("foo.js"); + + assert.deepStrictEqual(config.rules, { + "foo/bar": [2] + }); + }); + + it("should allow rules without `meta.schema` to be configured without options", async () => { + + const configs = new FlatConfigArray([ + { + plugins: { + foo: { + rules: { + meta: {}, + bar: { + create() { + return {}; + } + } + } + } + } + }, + { + rules: { + "foo/bar": "error" + } + } + ]); + + await configs.normalize(); + + // does not throw + const config = configs.getConfig("foo.js"); + + assert.deepStrictEqual(config.rules, { + "foo/bar": [2] + }); + }); + + it("should throw if a rule without `meta` is configured with an option", async () => { + await assertInvalidConfig([ + { + plugins: { + foo: { + rules: { + bar: { + create() { + return {}; + } + } + } + } + } + }, + { + rules: { + "foo/bar": ["error", "always"] + } + } + ], /should NOT have more than 0 items/u); + }); + + it("should throw if a rule without `meta.schema` is configured with an option", async () => { + await assertInvalidConfig([ + { + plugins: { + foo: { + rules: { + bar: { + meta: {}, + create() { + return {}; + } + } + } + } + } + }, + { + rules: { + "foo/bar": ["error", "always"] + } + } + ], /should NOT have more than 0 items/u); + }); + it("should merge two objects", () => assertMergedResult([ { rules: { diff --git a/tests/lib/config/flat-config-helpers.js b/tests/lib/config/flat-config-helpers.js index 004fb82b13c..e7076d045ce 100644 --- a/tests/lib/config/flat-config-helpers.js +++ b/tests/lib/config/flat-config-helpers.js @@ -11,7 +11,8 @@ const { parseRuleId, - getRuleFromConfig + getRuleFromConfig, + getRuleOptionsSchema } = require("../../../lib/config/flat-config-helpers"); const assert = require("chai").assert; @@ -99,4 +100,93 @@ describe("Config Helpers", () => { }); }); + describe("getRuleOptionsSchema", () => { + const noOptionsSchema = { + type: "array", + minItems: 0, + maxItems: 0 + }; + + it("should return schema that doesn't accept options if rule doesn't have `meta`", () => { + const rule = {}; + const result = getRuleOptionsSchema(rule); + + assert.deepStrictEqual(result, noOptionsSchema); + }); + + it("should return schema that doesn't accept options if rule doesn't have `meta.schema`", () => { + const rule = { meta: {} }; + const result = getRuleOptionsSchema(rule); + + assert.deepStrictEqual(result, noOptionsSchema); + }); + + it("should return schema that doesn't accept options if `meta.schema` is `undefined`", () => { + const rule = { meta: { schema: void 0 } }; + const result = getRuleOptionsSchema(rule); + + assert.deepStrictEqual(result, noOptionsSchema); + }); + + it("should return schema that doesn't accept options if `meta.schema` is `[]`", () => { + const rule = { meta: { schema: [] } }; + const result = getRuleOptionsSchema(rule); + + assert.deepStrictEqual(result, noOptionsSchema); + }); + + it("should return JSON Schema definition object if `meta.schema` is in the array form", () => { + const firstOption = { enum: ["always", "never"] }; + const rule = { meta: { schema: [firstOption] } }; + const result = getRuleOptionsSchema(rule); + + assert.deepStrictEqual( + result, + { + type: "array", + items: [firstOption], + minItems: 0, + maxItems: 1 + } + ); + }); + + it("should return `meta.schema` as is if `meta.schema` is an object", () => { + const schema = { + type: "array", + items: [{ + enum: ["always", "never"] + }] + }; + const rule = { meta: { schema } }; + const result = getRuleOptionsSchema(rule); + + assert.deepStrictEqual(result, schema); + }); + + it("should return `null` if `meta.schema` is `false`", () => { + const rule = { meta: { schema: false } }; + const result = getRuleOptionsSchema(rule); + + assert.strictEqual(result, null); + }); + + [null, true, 0, 1, "", "always", () => {}].forEach(schema => { + it(`should throw an error if \`meta.schema\` is ${typeof schema} ${schema}`, () => { + const rule = { meta: { schema } }; + + assert.throws(() => { + getRuleOptionsSchema(rule); + }, "Rule's `meta.schema` must be an array or object"); + }); + }); + + it("should ignore top-level `schema` property", () => { + const rule = { schema: { enum: ["always", "never"] } }; + const result = getRuleOptionsSchema(rule); + + assert.deepStrictEqual(result, noOptionsSchema); + }); + }); + }); diff --git a/tests/lib/eslint/eslint.js b/tests/lib/eslint/eslint.js index e7ff4208d05..cde34e27623 100644 --- a/tests/lib/eslint/eslint.js +++ b/tests/lib/eslint/eslint.js @@ -1673,6 +1673,23 @@ describe("ESLint", () => { }, /Error while loading rule 'custom-rule'/u); }); + it("should throw an error when loading a function-style custom rule", async () => { + eslint = new ESLint({ + ignore: false, + useEslintrc: false, + rulePaths: [getFixturePath("rules", "function-style")], + overrideConfig: { + rules: { + "no-strings": "error" + } + } + }); + + await assert.rejects(async () => { + await eslint.lintFiles([getFixturePath("rules", "test", "test-custom-rule.js")]); + }, /Error while loading rule 'no-strings': Rule must be an object with a `create` method/u); + }); + it("should return one message when a custom rule matches a file", async () => { eslint = new ESLint({ ignore: false, @@ -2362,6 +2379,24 @@ describe("ESLint", () => { assert.strictEqual(results[0].messages[0].ruleId, "test/example-rule"); }); + it("should throw an error when executing with a function-style rule from a preloaded plugin", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + useEslintrc: false, + overrideConfig: { + plugins: ["test"], + rules: { "test/example-rule": 1 } + }, + plugins: { + "eslint-plugin-test": { rules: { "example-rule": () => ({}) } } + } + }); + + await assert.rejects(async () => { + await eslint.lintFiles([fs.realpathSync(getFixturePath("rules", "test", "test-custom-rule.js"))]); + }, /Error while loading rule 'test\/example-rule': Rule must be an object with a `create` method/u); + }); + it("should return two messages when executing with `baseConfig` that extends preloaded plugin config", async () => { eslint = new ESLint({ cwd: path.join(fixtureDir, ".."), @@ -2408,6 +2443,277 @@ describe("ESLint", () => { assert.strictEqual(results[0].messages[0].ruleId, "with-rules/rule1"); assert.strictEqual(results[0].messages[0].message, "Rule report from plugin"); }); + + it("should throw an error when executing with a function-style rule from a plugin", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, "plugins"), + baseConfig: { + plugins: ["with-function-style-rules"], + rules: { "with-function-style-rules/rule1": "error" } + }, + useEslintrc: false + }); + + await assert.rejects(async () => { + await eslint.lintFiles([fs.realpathSync(getFixturePath("rules", "test", "test-custom-rule.js"))]); + }, /Error while loading rule 'with-function-style-rules\/rule1': Rule must be an object with a `create` method/u); + }); + + it("should throw an error when executing with a rule with `schema:true` from a plugin", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, "plugins"), + baseConfig: { + plugins: ["schema-true"], + rules: { "schema-true/rule1": "error" } + }, + useEslintrc: false + }); + + await assert.rejects(async () => { + await eslint.lintFiles([fs.realpathSync(getFixturePath("rules", "test", "test-custom-rule.js"))]); + }, /Error while processing options validation schema of rule 'schema-true\/rule1': Rule's `meta.schema` must be an array or object/u); + }); + + it("should throw an error when executing with a rule with `schema:null` from a plugin", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, "plugins"), + baseConfig: { + plugins: ["schema-null"], + rules: { "schema-null/rule1": "error" } + }, + useEslintrc: false + }); + + await assert.rejects(async () => { + await eslint.lintFiles([fs.realpathSync(getFixturePath("rules", "test", "test-custom-rule.js"))]); + }, /Error while processing options validation schema of rule 'schema-null\/rule1': Rule's `meta.schema` must be an array or object/u); + }); + + it("should throw an error when executing with a rule with invalid JSON schema type from a plugin", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, "plugins"), + baseConfig: { + plugins: ["schema-invalid"], + rules: { "schema-invalid/rule1": "error" } + }, + useEslintrc: false + }); + + await assert.rejects(async () => { + await eslint.lintFiles([fs.realpathSync(getFixturePath("rules", "test", "test-custom-rule.js"))]); + }, /Error while processing options validation schema of rule 'schema-invalid\/rule1': minItems must be number/u); + }); + + it("should succesfully execute with a rule with `schema:false` from a plugin when no options were passed", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, "plugins"), + baseConfig: { + plugins: ["schema-false"], + rules: { "schema-false/rule1": "error" } + }, + useEslintrc: false + }); + + const [result] = await eslint.lintFiles([fs.realpathSync(getFixturePath("rules", "test", "test-custom-rule.js"))]); + + assert.strictEqual(result.messages.length, 1); + assert.strictEqual(result.messages[0].ruleId, "schema-false/rule1"); + assert.strictEqual(result.messages[0].message, "No options were passed"); + }); + + it("should succesfully execute with a rule with `schema:false` from a plugin when an option is passed", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, "plugins"), + baseConfig: { + plugins: ["schema-false"], + rules: { "schema-false/rule1": ["error", "always"] } + }, + useEslintrc: false + }); + + const [result] = await eslint.lintFiles([fs.realpathSync(getFixturePath("rules", "test", "test-custom-rule.js"))]); + + assert.strictEqual(result.messages.length, 1); + assert.strictEqual(result.messages[0].ruleId, "schema-false/rule1"); + assert.strictEqual(result.messages[0].message, "Option 'always' was passed"); + }); + + it("should succesfully execute with a rule with `schema:[]` from a plugin when no options were passed", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, "plugins"), + baseConfig: { + plugins: ["schema-empty-array"], + rules: { "schema-empty-array/rule1": "error" } + }, + useEslintrc: false + }); + + const [result] = await eslint.lintFiles([fs.realpathSync(getFixturePath("rules", "test", "test-custom-rule.js"))]); + + assert.strictEqual(result.messages.length, 1); + assert.strictEqual(result.messages[0].ruleId, "schema-empty-array/rule1"); + assert.strictEqual(result.messages[0].message, "Hello"); + }); + + it("should throw when executing with a rule with `schema:[]` from a plugin when an option is passed", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, "plugins"), + baseConfig: { + plugins: ["schema-empty-array"], + rules: { "schema-empty-array/rule1": ["error", "always"] } + }, + useEslintrc: false + }); + + await assert.rejects(async () => { + await eslint.lintFiles([fs.realpathSync(getFixturePath("rules", "test", "test-custom-rule.js"))]); + }, /Configuration for rule "schema-empty-array\/rule1" is invalid.*should NOT have more than 0 items/us); + }); + + it("should succesfully execute with a rule with no schema from a plugin when no options were passed", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, "plugins"), + baseConfig: { + plugins: ["schema-missing"], + rules: { "schema-missing/rule1": "error" } + }, + useEslintrc: false + }); + + const [result] = await eslint.lintFiles([fs.realpathSync(getFixturePath("rules", "test", "test-custom-rule.js"))]); + + assert.strictEqual(result.messages.length, 1); + assert.strictEqual(result.messages[0].ruleId, "schema-missing/rule1"); + assert.strictEqual(result.messages[0].message, "Hello"); + }); + + it("should throw when executing with a rule with no schema from a plugin when an option is passed", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, "plugins"), + baseConfig: { + plugins: ["schema-missing"], + rules: { "schema-missing/rule1": ["error", "always"] } + }, + useEslintrc: false + }); + + await assert.rejects(async () => { + await eslint.lintFiles([fs.realpathSync(getFixturePath("rules", "test", "test-custom-rule.js"))]); + }, /Configuration for rule "schema-missing\/rule1" is invalid.*should NOT have more than 0 items/us); + }); + + it("should succesfully execute with a rule with an array schema from a plugin when no options were passed", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, "plugins"), + baseConfig: { + plugins: ["schema-array"], + rules: { "schema-array/rule1": "error" } + }, + useEslintrc: false + }); + + const [result] = await eslint.lintFiles([fs.realpathSync(getFixturePath("rules", "test", "test-custom-rule.js"))]); + + assert.strictEqual(result.messages.length, 1); + assert.strictEqual(result.messages[0].ruleId, "schema-array/rule1"); + assert.strictEqual(result.messages[0].message, "No options were passed"); + }); + + it("should succesfully execute with a rule with an array schema from a plugin when a correct option was passed", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, "plugins"), + baseConfig: { + plugins: ["schema-array"], + rules: { "schema-array/rule1": ["error", "always"] } + }, + useEslintrc: false + }); + + const [result] = await eslint.lintFiles([fs.realpathSync(getFixturePath("rules", "test", "test-custom-rule.js"))]); + + assert.strictEqual(result.messages.length, 1); + assert.strictEqual(result.messages[0].ruleId, "schema-array/rule1"); + assert.strictEqual(result.messages[0].message, "Option 'always' was passed"); + }); + + it("should throw when executing with a rule with an array schema from a plugin when an incorrect option was passed", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, "plugins"), + baseConfig: { + plugins: ["schema-array"], + rules: { "schema-array/rule1": ["error", 5] } + }, + useEslintrc: false + }); + + await assert.rejects(async () => { + await eslint.lintFiles([fs.realpathSync(getFixturePath("rules", "test", "test-custom-rule.js"))]); + }, /Configuration for rule "schema-array\/rule1" is invalid.*Value 5 should be string/us); + }); + + it("should throw when executing with a rule with an array schema from a plugin when an extra option was passed", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, "plugins"), + baseConfig: { + plugins: ["schema-array"], + rules: { "schema-array/rule1": ["error", "always", "never"] } + }, + useEslintrc: false + }); + + await assert.rejects(async () => { + await eslint.lintFiles([fs.realpathSync(getFixturePath("rules", "test", "test-custom-rule.js"))]); + }, /Configuration for rule "schema-array\/rule1" is invalid.*should NOT have more than 1 items/us); + }); + + it("should succesfully execute with a rule with an object schema from a plugin when no options were passed", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, "plugins"), + baseConfig: { + plugins: ["schema-object"], + rules: { "schema-object/rule1": "error" } + }, + useEslintrc: false + }); + + const [result] = await eslint.lintFiles([fs.realpathSync(getFixturePath("rules", "test", "test-custom-rule.js"))]); + + assert.strictEqual(result.messages.length, 1); + assert.strictEqual(result.messages[0].ruleId, "schema-object/rule1"); + assert.strictEqual(result.messages[0].message, "No options were passed"); + }); + + it("should succesfully execute with a rule with an object schema from a plugin when a correct option was passed", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, "plugins"), + baseConfig: { + plugins: ["schema-object"], + rules: { "schema-object/rule1": ["error", "always"] } + }, + useEslintrc: false + }); + + const [result] = await eslint.lintFiles([fs.realpathSync(getFixturePath("rules", "test", "test-custom-rule.js"))]); + + assert.strictEqual(result.messages.length, 1); + assert.strictEqual(result.messages[0].ruleId, "schema-object/rule1"); + assert.strictEqual(result.messages[0].message, "Option 'always' was passed"); + }); + + it("should throw when executing with a rule with an object schema from a plugin when an incorrect option was passed", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, "plugins"), + baseConfig: { + plugins: ["schema-object"], + rules: { "schema-object/rule1": ["error", 5] } + }, + useEslintrc: false + }); + + await assert.rejects(async () => { + await eslint.lintFiles([fs.realpathSync(getFixturePath("rules", "test", "test-custom-rule.js"))]); + }, /Configuration for rule "schema-object\/rule1" is invalid.*Value 5 should be string/us); + }); }); describe("cache", () => { @@ -4216,11 +4522,15 @@ describe("ESLint", () => { cwd: rootPath, files: { "internal-rules/test.js": ` - module.exports = context => ({ - ExpressionStatement(node) { - context.report({ node, message: "ok" }) - } - }) + module.exports = { + create(context) { + return { + ExpressionStatement(node) { + context.report({ node, message: "ok" }); + }, + }; + }, + }; `, ".eslintrc.json": { root: true, diff --git a/tests/lib/linter/linter.js b/tests/lib/linter/linter.js index eddc1387f6f..941a714b719 100644 --- a/tests/lib/linter/linter.js +++ b/tests/lib/linter/linter.js @@ -319,6 +319,68 @@ describe("Linter", () => { sinon.assert.calledOnce(spyBinaryExpression); }); + it("should throw an error if a rule is a function", () => { + + /** + * Legacy-format rule (a function instead of an object with `create` method). + * @param {RuleContext} context The ESLint rule context object. + * @returns {Object} Listeners. + */ + function functionStyleRule(context) { + return { + Program(node) { + context.report({ node, message: "bad" }); + } + }; + } + + linter.defineRule("function-style-rule", functionStyleRule); + + assert.throws( + () => linter.verify("foo", { rules: { "function-style-rule": "error" } }), + TypeError, + "Error while loading rule 'function-style-rule': Rule must be an object with a `create` method" + ); + }); + + it("should throw an error if a rule is an object without 'create' method", () => { + const rule = { + create_(context) { + return { + Program(node) { + context.report({ node, message: "bad" }); + } + }; + } + }; + + linter.defineRule("object-rule-without-create", rule); + + assert.throws( + () => linter.verify("foo", { rules: { "object-rule-without-create": "error" } }), + TypeError, + "Error while loading rule 'object-rule-without-create': Rule must be an object with a `create` method" + ); + }); + + it("should throw an error if a rule with invalid `meta.schema` is enabled in a configuration comment", () => { + const rule = { + meta: { + schema: true + }, + create() { + return {}; + } + }; + + linter.defineRule("rule-with-invalid-schema", rule); + + assert.throws( + () => linter.verify("/* eslint rule-with-invalid-schema: 2 */"), + "Error while processing options validation schema of rule 'rule-with-invalid-schema': Rule's `meta.schema` must be an array or object" + ); + }); + it("should throw an error if a rule reports a problem without a message", () => { linter.defineRule("invalid-report", { create: context => ({ @@ -8987,6 +9049,123 @@ describe("Linter with FlatConfigArray", () => { assert.strictEqual(suppressedMessages.length, 0); }); + it("should throw an error if a rule is a function", () => { + + /** + * Legacy-format rule (a function instead of an object with `create` method). + * @param {RuleContext} context The ESLint rule context object. + * @returns {Object} Listeners. + */ + function functionStyleRule(context) { + return { + Program(node) { + context.report({ node, message: "bad" }); + } + }; + } + + const config = { + plugins: { + test: { + rules: { + "function-style-rule": functionStyleRule + } + } + }, + rules: { "test/function-style-rule": "error" } + }; + + assert.throws( + () => linter.verify("foo", config), + TypeError, + "Error while loading rule 'test/function-style-rule': Rule must be an object with a `create` method" + ); + }); + + it("should throw an error if a rule is an object without 'create' method", () => { + const rule = { + create_(context) { + return { + Program(node) { + context.report({ node, message: "bad" }); + } + }; + } + }; + + const config = { + plugins: { + test: { + rules: { + "object-rule-without-create": rule + } + } + }, + rules: { "test/object-rule-without-create": "error" } + }; + + assert.throws( + () => linter.verify("foo", config), + TypeError, + "Error while loading rule 'test/object-rule-without-create': Rule must be an object with a `create` method" + ); + }); + + it("should throw an error if a rule with invalid `meta.schema` is enabled in the configuration", () => { + const config = [ + { + plugins: { + test: { + rules: { + "rule-with-invalid-schema": { + meta: { + schema: true + }, + create() { + return {}; + } + } + } + } + } + }, + { + rules: { "test/rule-with-invalid-schema": "error" } + } + ]; + + assert.throws( + () => linter.verify("foo", config), + "Error while processing options validation schema of rule 'test/rule-with-invalid-schema': Rule's `meta.schema` must be an array or object" + ); + }); + + it("should throw an error if a rule with invalid `meta.schema` is enabled in a configuration comment", () => { + const config = [ + { + plugins: { + test: { + rules: { + "rule-with-invalid-schema": { + meta: { + schema: true + }, + create() { + return {}; + } + } + } + } + } + } + ]; + + assert.throws( + () => linter.verify("/* eslint test/rule-with-invalid-schema: 2 */", config), + "Error while processing options validation schema of rule 'test/rule-with-invalid-schema': Rule's `meta.schema` must be an array or object" + ); + }); + it("should throw an error if a rule reports a problem without a message", () => { const config = { diff --git a/tests/lib/linter/rules.js b/tests/lib/linter/rules.js index 7ae12289842..0c4b5a26d4e 100644 --- a/tests/lib/linter/rules.js +++ b/tests/lib/linter/rules.js @@ -32,18 +32,6 @@ describe("rules", () => { assert.ok(rules.get(ruleId)); }); - it("should return the rule as an object with a create() method if the rule was defined as a function", () => { - - /** - * A rule that does nothing - * @returns {void} - */ - function rule() {} - rule.schema = []; - rules.define("foo", rule); - assert.deepStrictEqual(rules.get("foo"), { create: rule, schema: [] }); - }); - it("should return the rule as-is if it was defined as an object with a create() method", () => { const rule = { create() {} }; diff --git a/tests/lib/rule-tester/flat-rule-tester.js b/tests/lib/rule-tester/flat-rule-tester.js index 5f47ce1fe4b..0e7db8b8b6a 100644 --- a/tests/lib/rule-tester/flat-rule-tester.js +++ b/tests/lib/rule-tester/flat-rule-tester.js @@ -1326,6 +1326,29 @@ describe("FlatRuleTester", () => { }); + it("should throw an error with the original message and an additional description if rule has `meta.schema` of an invalid type", () => { + const rule = { + meta: { + schema: true + }, + create(context) { + return { + Program(node) { + context.report({ node, message: "bad" }); + } + }; + } + }; + + assert.throws(() => { + ruleTester.run("rule-with-invalid-schema-type", rule, { + valid: [], + invalid: [ + { code: "var foo = bar;", errors: 1 } + ] + }); + }, /Rule's `meta.schema` must be an array or object.*set `meta.schema` to an array or non-empty object to enable options validation/us); + }); it("should prevent invalid options schemas", () => { assert.throws(() => { @@ -1342,6 +1365,92 @@ describe("FlatRuleTester", () => { }); + it("should throw an error if rule schema is `{}`", () => { + const rule = { + meta: { + schema: {} + }, + create(context) { + return { + Program(node) { + context.report({ node, message: "bad" }); + } + }; + } + }; + + assert.throws(() => { + ruleTester.run("rule-with-empty-object-schema", rule, { + valid: [], + invalid: [ + { code: "var foo = bar;", errors: 1 } + ] + }); + }, /`schema: \{\}` is a no-op.*set `meta.schema` to an array or non-empty object to enable options validation/us); + }); + + it("should throw an error if rule schema has only non-enumerable properties", () => { + const rule = { + meta: { + schema: Object.create(null, { + type: { + value: "array", + enumerable: false + }, + items: { + value: [{ enum: ["foo"] }], + enumerable: false + } + }) + }, + create(context) { + return { + Program(node) { + context.report({ node, message: "bad" }); + } + }; + } + }; + + assert.throws(() => { + ruleTester.run("rule-with-empty-object-schema", rule, { + valid: [], + invalid: [ + { code: "var foo = bar;", errors: 1 } + ] + }); + }, /`schema: \{\}` is a no-op.*set `meta.schema` to an array or non-empty object to enable options validation/us); + }); + + it("should throw an error if rule schema has only inherited enumerable properties", () => { + const rule = { + meta: { + schema: { + __proto__: { + type: "array", + items: [{ enum: ["foo"] }] + } + } + }, + create(context) { + return { + Program(node) { + context.report({ node, message: "bad" }); + } + }; + } + }; + + assert.throws(() => { + ruleTester.run("rule-with-empty-object-schema", rule, { + valid: [], + invalid: [ + { code: "var foo = bar;", errors: 1 } + ] + }); + }, /`schema: \{\}` is a no-op.*set `meta.schema` to an array or non-empty object to enable options validation/us); + }); + it("should prevent schema violations in options", () => { assert.throws(() => { ruleTester.run("no-schema-violation", require("../../fixtures/testers/rule-tester/no-schema-violation"), { @@ -1638,6 +1747,52 @@ describe("FlatRuleTester", () => { }, "Use node.range[0] instead of node.start"); }); + it("should throw an error if rule is a function", () => { + + /** + * Legacy-format rule (a function instead of an object with `create` method). + * @param {RuleContext} context The ESLint rule context object. + * @returns {Object} Listeners. + */ + function functionStyleRule(context) { + return { + Program(node) { + context.report({ node, message: "bad" }); + } + }; + } + + assert.throws(() => { + ruleTester.run("function-style-rule", functionStyleRule, { + valid: [], + invalid: [ + { code: "var foo = bar;", errors: 1 } + ] + }); + }, "Rule must be an object with a `create` method"); + }); + + it("should throw an error if rule is an object without 'create' method", () => { + const rule = { + create_(context) { + return { + Program(node) { + context.report({ node, message: "bad" }); + } + }; + } + }; + + assert.throws(() => { + ruleTester.run("object-rule-without-create", rule, { + valid: [], + invalid: [ + { code: "var foo = bar;", errors: 1 } + ] + }); + }, "Rule must be an object with a `create` method"); + }); + it("should throw an error if no test scenarios given", () => { assert.throws(() => { ruleTester.run("foo", require("../../fixtures/testers/rule-tester/modify-ast-at-last")); @@ -1782,30 +1937,6 @@ describe("FlatRuleTester", () => { }); }, /Fixable rules must set the `meta\.fixable` property/u); }); - it("should throw an error if a legacy-format rule produces fixes", () => { - - /** - * Legacy-format rule (a function instead of an object with `create` method). - * @param {RuleContext} context The ESLint rule context object. - * @returns {Object} Listeners. - */ - function replaceProgramWith5Rule(context) { - return { - Program(node) { - context.report({ node, message: "bad", fix: fixer => fixer.replaceText(node, "5") }); - } - }; - } - - assert.throws(() => { - ruleTester.run("replaceProgramWith5", replaceProgramWith5Rule, { - valid: [], - invalid: [ - { code: "var foo = bar;", output: "5", errors: 1 } - ] - }); - }, /Fixable rules must set the `meta\.fixable` property/u); - }); describe("suggestions", () => { it("should pass with valid suggestions (tested using desc)", () => { diff --git a/tests/lib/rule-tester/rule-tester.js b/tests/lib/rule-tester/rule-tester.js index 5af50dd7dea..7a52910cc6a 100644 --- a/tests/lib/rule-tester/rule-tester.js +++ b/tests/lib/rule-tester/rule-tester.js @@ -1243,6 +1243,30 @@ describe("RuleTester", () => { }); }); + it("should throw an error with the original message and an additional description if rule has `meta.schema` of an invalid type", () => { + const rule = { + meta: { + schema: true + }, + create(context) { + return { + Program(node) { + context.report({ node, message: "bad" }); + } + }; + } + }; + + assert.throws(() => { + ruleTester.run("rule-with-invalid-schema-type", rule, { + valid: [], + invalid: [ + { code: "var foo = bar;", errors: 1 } + ] + }); + }, /Rule's `meta.schema` must be an array or object.*set `meta.schema` to an array or non-empty object to enable options validation/us); + }); + it("should prevent invalid options schemas", () => { assert.throws(() => { ruleTester.run("no-invalid-schema", require("../../fixtures/testers/rule-tester/no-invalid-schema"), { @@ -1258,6 +1282,92 @@ describe("RuleTester", () => { }); + it("should throw an error if rule schema is `{}`", () => { + const rule = { + meta: { + schema: {} + }, + create(context) { + return { + Program(node) { + context.report({ node, message: "bad" }); + } + }; + } + }; + + assert.throws(() => { + ruleTester.run("rule-with-empty-object-schema", rule, { + valid: [], + invalid: [ + { code: "var foo = bar;", errors: 1 } + ] + }); + }, /`schema: \{\}` is a no-op.*set `meta.schema` to an array or non-empty object to enable options validation/us); + }); + + it("should throw an error if rule schema has only non-enumerable properties", () => { + const rule = { + meta: { + schema: Object.create(null, { + type: { + value: "array", + enumerable: false + }, + items: { + value: [{ enum: ["foo"] }], + enumerable: false + } + }) + }, + create(context) { + return { + Program(node) { + context.report({ node, message: "bad" }); + } + }; + } + }; + + assert.throws(() => { + ruleTester.run("rule-with-empty-object-schema", rule, { + valid: [], + invalid: [ + { code: "var foo = bar;", errors: 1 } + ] + }); + }, /`schema: \{\}` is a no-op.*set `meta.schema` to an array or non-empty object to enable options validation/us); + }); + + it("should throw an error if rule schema has only inherited enumerable properties", () => { + const rule = { + meta: { + schema: { + __proto__: { + type: "array", + items: [{ enum: ["foo"] }] + } + } + }, + create(context) { + return { + Program(node) { + context.report({ node, message: "bad" }); + } + }; + } + }; + + assert.throws(() => { + ruleTester.run("rule-with-empty-object-schema", rule, { + valid: [], + invalid: [ + { code: "var foo = bar;", errors: 1 } + ] + }); + }, /`schema: \{\}` is a no-op.*set `meta.schema` to an array or non-empty object to enable options validation/us); + }); + it("should prevent schema violations in options", () => { assert.throws(() => { ruleTester.run("no-schema-violation", require("../../fixtures/testers/rule-tester/no-schema-violation"), { @@ -1579,6 +1689,52 @@ describe("RuleTester", () => { }, "Use node.range[0] instead of node.start"); }); + it("should throw an error if rule is a function", () => { + + /** + * Legacy-format rule (a function instead of an object with `create` method). + * @param {RuleContext} context The ESLint rule context object. + * @returns {Object} Listeners. + */ + function functionStyleRule(context) { + return { + Program(node) { + context.report({ node, message: "bad" }); + } + }; + } + + assert.throws(() => { + ruleTester.run("function-style-rule", functionStyleRule, { + valid: [], + invalid: [ + { code: "var foo = bar;", errors: 1 } + ] + }); + }, "Rule must be an object with a `create` method"); + }); + + it("should throw an error if rule is an object without 'create' method", () => { + const rule = { + create_(context) { + return { + Program(node) { + context.report({ node, message: "bad" }); + } + }; + } + }; + + assert.throws(() => { + ruleTester.run("object-rule-without-create", rule, { + valid: [], + invalid: [ + { code: "var foo = bar;", errors: 1 } + ] + }); + }, "Rule must be an object with a `create` method"); + }); + it("should throw an error if no test scenarios given", () => { assert.throws(() => { ruleTester.run("foo", require("../../fixtures/testers/rule-tester/modify-ast-at-last")); @@ -2234,27 +2390,6 @@ describe("RuleTester", () => { describe("deprecations", () => { let processStub; - const ruleWithNoSchema = { - meta: { - type: "suggestion" - }, - create(context) { - return { - Program(node) { - context.report({ node, message: "bad" }); - } - }; - } - }; - const ruleWithNoMeta = { - create(context) { - return { - Program(node) { - context.report({ node, message: "bad" }); - } - }; - } - }; beforeEach(() => { processStub = sinon.stub(process, "emitWarning"); @@ -2264,232 +2399,6 @@ describe("RuleTester", () => { processStub.restore(); }); - it("should log a deprecation warning when using the legacy function-style API for rule", () => { - - /** - * Legacy-format rule (a function instead of an object with `create` method). - * @param {RuleContext} context The ESLint rule context object. - * @returns {Object} Listeners. - */ - function functionStyleRule(context) { - return { - Program(node) { - context.report({ node, message: "bad" }); - } - }; - } - - ruleTester.run("function-style-rule", functionStyleRule, { - valid: [], - invalid: [ - { code: "var foo = bar;", errors: 1 } - ] - }); - - assert.strictEqual(processStub.callCount, 1, "calls `process.emitWarning()` once"); - assert.deepStrictEqual( - processStub.getCall(0).args, - [ - "\"function-style-rule\" rule is using the deprecated function-style format and will stop working in ESLint v9. Please use object-style format: https://eslint.org/docs/latest/extend/custom-rules", - "DeprecationWarning" - ] - ); - }); - - it("should log a deprecation warning when meta is not defined for the rule", () => { - ruleTester.run("rule-with-no-meta-1", ruleWithNoMeta, { - valid: [], - invalid: [ - { code: "var foo = bar;", options: [{ foo: true }], errors: 1 } - ] - }); - - assert.strictEqual(processStub.callCount, 1, "calls `process.emitWarning()` once"); - assert.deepStrictEqual( - processStub.getCall(0).args, - [ - "\"rule-with-no-meta-1\" rule has options but is missing the \"meta.schema\" property and will stop working in ESLint v9. Please add a schema: https://eslint.org/docs/latest/extend/custom-rules#options-schemas", - "DeprecationWarning" - ] - ); - }); - - it("should log a deprecation warning when schema is not defined for the rule", () => { - ruleTester.run("rule-with-no-schema-1", ruleWithNoSchema, { - valid: [], - invalid: [ - { code: "var foo = bar;", options: [{ foo: true }], errors: 1 } - ] - }); - - assert.strictEqual(processStub.callCount, 1, "calls `process.emitWarning()` once"); - assert.deepStrictEqual( - processStub.getCall(0).args, - [ - "\"rule-with-no-schema-1\" rule has options but is missing the \"meta.schema\" property and will stop working in ESLint v9. Please add a schema: https://eslint.org/docs/latest/extend/custom-rules#options-schemas", - "DeprecationWarning" - ] - ); - }); - - it("should log a deprecation warning when schema is `undefined`", () => { - const ruleWithUndefinedSchema = { - meta: { - type: "problem", - // eslint-disable-next-line no-undefined -- intentionally added for test case - schema: undefined - }, - create(context) { - return { - Program(node) { - context.report({ node, message: "bad" }); - } - }; - } - }; - - ruleTester.run("rule-with-undefined-schema", ruleWithUndefinedSchema, { - valid: [], - invalid: [ - { code: "var foo = bar;", options: [{ foo: true }], errors: 1 } - ] - }); - - assert.strictEqual(processStub.callCount, 1, "calls `process.emitWarning()` once"); - assert.deepStrictEqual( - processStub.getCall(0).args, - [ - "\"rule-with-undefined-schema\" rule has options but is missing the \"meta.schema\" property and will stop working in ESLint v9. Please add a schema: https://eslint.org/docs/latest/extend/custom-rules#options-schemas", - "DeprecationWarning" - ] - ); - }); - - it("should log a deprecation warning when schema is `null`", () => { - const ruleWithNullSchema = { - meta: { - type: "problem", - schema: null - }, - create(context) { - return { - Program(node) { - context.report({ node, message: "bad" }); - } - }; - } - }; - - ruleTester.run("rule-with-null-schema", ruleWithNullSchema, { - valid: [], - invalid: [ - { code: "var foo = bar;", options: [{ foo: true }], errors: 1 } - ] - }); - - assert.strictEqual(processStub.callCount, 1, "calls `process.emitWarning()` once"); - assert.deepStrictEqual( - processStub.getCall(0).args, - [ - "\"rule-with-null-schema\" rule has options but is missing the \"meta.schema\" property and will stop working in ESLint v9. Please add a schema: https://eslint.org/docs/latest/extend/custom-rules#options-schemas", - "DeprecationWarning" - ] - ); - }); - - it("should not log a deprecation warning when schema is an empty array", () => { - const ruleWithEmptySchema = { - meta: { - type: "suggestion", - schema: [] - }, - create(context) { - return { - Program(node) { - context.report({ node, message: "bad" }); - } - }; - } - }; - - ruleTester.run("rule-with-no-options", ruleWithEmptySchema, { - valid: [], - invalid: [{ code: "var foo = bar;", errors: 1 }] - }); - - assert.strictEqual(processStub.callCount, 0, "never calls `process.emitWarning()`"); - }); - - it("When the rule is an object-style rule, the legacy rule API warning is not emitted", () => { - ruleTester.run("rule-with-no-schema-2", ruleWithNoSchema, { - valid: [], - invalid: [ - { code: "var foo = bar;", errors: 1 } - ] - }); - - assert.strictEqual(processStub.callCount, 0, "never calls `process.emitWarning()`"); - }); - - it("When the rule has meta.schema and there are test cases with options, the missing schema warning is not emitted", () => { - const ruleWithSchema = { - meta: { - type: "suggestion", - schema: [{ - type: "boolean" - }] - }, - create(context) { - return { - Program(node) { - context.report({ node, message: "bad" }); - } - }; - } - }; - - ruleTester.run("rule-with-schema", ruleWithSchema, { - valid: [], - invalid: [ - { code: "var foo = bar;", options: [true], errors: 1 } - ] - }); - - assert.strictEqual(processStub.callCount, 0, "never calls `process.emitWarning()`"); - }); - - it("When the rule does not have meta, but there are no test cases with options, the missing schema warning is not emitted", () => { - ruleTester.run("rule-with-no-meta-2", ruleWithNoMeta, { - valid: [], - invalid: [ - { code: "var foo = bar;", errors: 1 } - ] - }); - - assert.strictEqual(processStub.callCount, 0, "never calls `process.emitWarning()`"); - }); - - it("When the rule has meta without meta.schema, but there are no test cases with options, the missing schema warning is not emitted", () => { - ruleTester.run("rule-with-no-schema-3", ruleWithNoSchema, { - valid: [], - invalid: [ - { code: "var foo = bar;", errors: 1 } - ] - }); - - assert.strictEqual(processStub.callCount, 0, "never calls `process.emitWarning()`"); - }); - it("When the rule has meta without meta.schema, and some test cases have options property but it's an empty array, the missing schema warning is not emitted", () => { - ruleTester.run("rule-with-no-schema-4", ruleWithNoSchema, { - valid: [], - invalid: [ - { code: "var foo = bar;", options: [], errors: 1 } - ] - }); - - assert.strictEqual(processStub.callCount, 0, "never calls `process.emitWarning()`"); - }); - it("should emit a deprecation warning when CodePath#currentSegments is accessed", () => { const useCurrentSegmentsRule = { diff --git a/tests/lib/shared/config-validator.js b/tests/lib/shared/config-validator.js index 5788824b929..fc7cb64e349 100644 --- a/tests/lib/shared/config-validator.js +++ b/tests/lib/shared/config-validator.js @@ -413,6 +413,12 @@ describe("Validator", () => { describe("getRuleOptionsSchema", () => { + const noOptionsSchema = { + type: "array", + minItems: 0, + maxItems: 0 + }; + it("should return null for a missing rule", () => { assert.strictEqual(validator.getRuleOptionsSchema(ruleMapper("non-existent-rule")), null); }); @@ -424,6 +430,87 @@ describe("Validator", () => { }); }); + it("should return schema that doesn't accept options if rule doesn't have `meta`", () => { + const rule = {}; + const result = validator.getRuleOptionsSchema(rule); + + assert.deepStrictEqual(result, noOptionsSchema); + }); + + it("should return schema that doesn't accept options if rule doesn't have `meta.schema`", () => { + const rule = { meta: {} }; + const result = validator.getRuleOptionsSchema(rule); + + assert.deepStrictEqual(result, noOptionsSchema); + }); + + it("should return schema that doesn't accept options if `meta.schema` is `undefined`", () => { + const rule = { meta: { schema: void 0 } }; + const result = validator.getRuleOptionsSchema(rule); + + assert.deepStrictEqual(result, noOptionsSchema); + }); + + it("should return schema that doesn't accept options if `meta.schema` is `[]`", () => { + const rule = { meta: { schema: [] } }; + const result = validator.getRuleOptionsSchema(rule); + + assert.deepStrictEqual(result, noOptionsSchema); + }); + + it("should return JSON Schema definition object if `meta.schema` is in the array form", () => { + const firstOption = { enum: ["always", "never"] }; + const rule = { meta: { schema: [firstOption] } }; + const result = validator.getRuleOptionsSchema(rule); + + assert.deepStrictEqual( + result, + { + type: "array", + items: [firstOption], + minItems: 0, + maxItems: 1 + } + ); + }); + + it("should return `meta.schema` as is if `meta.schema` is an object", () => { + const schema = { + type: "array", + items: [{ + enum: ["always", "never"] + }] + }; + const rule = { meta: { schema } }; + const result = validator.getRuleOptionsSchema(rule); + + assert.deepStrictEqual(result, schema); + }); + + it("should return `null` if `meta.schema` is `false`", () => { + const rule = { meta: { schema: false } }; + const result = validator.getRuleOptionsSchema(rule); + + assert.strictEqual(result, null); + }); + + [null, true, 0, 1, "", "always", () => {}].forEach(schema => { + it(`should throw an error if \`meta.schema\` is ${typeof schema} ${schema}`, () => { + const rule = { meta: { schema } }; + + assert.throws(() => { + validator.getRuleOptionsSchema(rule); + }, "Rule's `meta.schema` must be an array or object"); + }); + }); + + it("should ignore top-level `schema` property", () => { + const rule = { schema: { enum: ["always", "never"] } }; + const result = validator.getRuleOptionsSchema(rule); + + assert.deepStrictEqual(result, noOptionsSchema); + }); + }); describe("validateRuleOptions", () => { diff --git a/tests/tools/config-rule.js b/tests/tools/config-rule.js index c91f464c20b..2404a2147ab 100644 --- a/tests/tools/config-rule.js +++ b/tests/tools/config-rule.js @@ -304,7 +304,7 @@ describe("ConfigRule", () => { it("should allow to ignore deprecated rules", () => { const expectedRules = Array.from(builtInRules.entries()) .filter(([, rule]) => { - const isDeprecated = (typeof rule === "function") ? rule.deprecated : rule.meta.deprecated; + const isDeprecated = rule.meta.deprecated; return !isDeprecated; }) diff --git a/tools/config-rule.js b/tools/config-rule.js index 91e7eaef5a4..88cd88123af 100644 --- a/tools/config-rule.js +++ b/tools/config-rule.js @@ -293,8 +293,8 @@ function generateConfigsFromSchema(schema) { */ function createCoreRuleConfigs(noDeprecated = false) { return Array.from(builtInRules).reduce((accumulator, [id, rule]) => { - const schema = (typeof rule === "function") ? rule.schema : rule.meta.schema; - const isDeprecated = (typeof rule === "function") ? rule.deprecated : rule.meta.deprecated; + const schema = rule.meta.schema; + const isDeprecated = rule.meta.deprecated; if (noDeprecated && isDeprecated) { return accumulator;