Skip to content

Commit

Permalink
feat!: drop support for function-style rules and rules missing schemas
Browse files Browse the repository at this point in the history
BREAKING CHANGE

RFC-85
  • Loading branch information
bmish committed Dec 5, 2022
1 parent e6cb05a commit b3ff346
Show file tree
Hide file tree
Showing 25 changed files with 2,712 additions and 2,166 deletions.
2 changes: 1 addition & 1 deletion docs/src/developer-guide/working-with-rules-deprecated.md
Expand Up @@ -3,7 +3,7 @@ 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](./working-with-rules).
**Note:** This page covers the deprecated function-style rule format for ESLint <= 2.13.1. [This is the most recent rule format](./working-with-rules). This format has been removed as of ESLint 9.0.0.

Each rule in ESLint has two files named with its identifier (for example, `no-extra-semi`).

Expand Down
4 changes: 2 additions & 2 deletions docs/src/developer-guide/working-with-rules.md
Expand Up @@ -80,7 +80,7 @@ The source file for a rule exports an object with the following properties.

**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` (array) specifies the [options](#options-schemas) so ESLint can prevent invalid [rule configurations](../user-guide/configuring/rules#configuring-rules)
* `schema` (array) specifies the [options](#options-schemas) so ESLint can prevent invalid [rule configurations](../user-guide/configuring/rules#configuring-rules). Mandatory when a 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.

Expand Down Expand Up @@ -626,7 +626,7 @@ Please note that the following methods have been deprecated and will be removed

### Options Schemas

Rules may export 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 may export 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`. This property is mandatory when a rule has 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.

Expand Down
28 changes: 12 additions & 16 deletions lib/config/flat-config-helpers.js
Expand Up @@ -52,19 +52,19 @@ 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;
}

const SCHEMA_NO_OPTIONS = {
type: "array",
minItems: 0,
maxItems: 0
};

Object.freeze(SCHEMA_NO_OPTIONS);

/**
* Gets a complete options schema for a rule.
* @param {{create: Function, schema: (Array|null)}} rule A new-style rule object
Expand All @@ -76,7 +76,7 @@ function getRuleOptionsSchema(rule) {
return null;
}

const schema = rule.schema || rule.meta && rule.meta.schema;
const schema = rule.meta && rule.meta.schema;

if (Array.isArray(schema)) {
if (schema.length) {
Expand All @@ -87,16 +87,12 @@ function getRuleOptionsSchema(rule) {
maxItems: schema.length
};
}
return {
type: "array",
minItems: 0,
maxItems: 0
};
return SCHEMA_NO_OPTIONS;

}

// Given a full schema, leave it alone
return schema || null;
return schema || SCHEMA_NO_OPTIONS;
}


Expand Down
4 changes: 2 additions & 2 deletions lib/linter/linter.js
Expand Up @@ -1946,7 +1946,7 @@ 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} ruleModule The rule module
* @returns {void}
*/
defineRule(ruleId, ruleModule) {
Expand All @@ -1956,7 +1956,7 @@ class Linter {

/**
* Defines many new linting rules.
* @param {Record<string, Function | Rule>} rulesToDefine map from unique rule identifier to rule
* @param {Record<string, Rule>} rulesToDefine map from unique rule identifier to rule
* @returns {void}
*/
defineRules(rulesToDefine) {
Expand Down
22 changes: 6 additions & 16 deletions lib/linter/rules.js
Expand Up @@ -12,20 +12,6 @@

const builtInRules = require("../rules");

//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------

/**
* 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;
}

//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
Expand All @@ -41,11 +27,15 @@ class Rules {
/**
* Registers a rule module for rule id in storage.
* @param {string} ruleId Rule id (file name).
* @param {Function} ruleModule Rule handler.
* @param {RuleModule} ruleModule Rule handler.
* @throws {TypeError} If the rule is using the deprecated function-style instead of object-style.
* @returns {void}
*/
define(ruleId, ruleModule) {
this._rules[ruleId] = normalizeRule(ruleModule);
if (typeof ruleModule === "function") {
throw new TypeError(`Function-style rules are no longer supported. The rule "${ruleId}" needs to be switched to an object-style rule.`);
}
this._rules[ruleId] = ruleModule;
}

/**
Expand Down
24 changes: 13 additions & 11 deletions lib/rule-tester/flat-rule-tester.js
Expand Up @@ -447,7 +447,7 @@ class FlatRuleTester {
/**
* Adds a new rule test to execute.
* @param {string} ruleName The name of the rule to run.
* @param {Function} rule The rule to test.
* @param {RuleModule} rule The rule to test.
* @param {{
* valid: (ValidTestCase | string)[],
* invalid: InvalidTestCase[]
Expand Down Expand Up @@ -516,7 +516,7 @@ class FlatRuleTester {

// freezeDeeply(context.languageOptions);

return (typeof rule === "function" ? rule : rule.create)(context);
return rule.create(context);
}
})
}
Expand Down Expand Up @@ -619,15 +619,17 @@ class FlatRuleTester {
plugins: {
"rule-tester": {
rules: {
"validate-ast"() {
return {
Program(node) {
beforeAST = cloneDeeplyExcludesParent(node);
},
"Program:exit"(node) {
afterAST = node;
}
};
"validate-ast": {
create() {
return {
Program(node) {
beforeAST = cloneDeeplyExcludesParent(node);
},
"Program:exit"(node) {
afterAST = node;
}
};
}
}
}
}
Expand Down
63 changes: 20 additions & 43 deletions lib/rule-tester/rule-tester.js
Expand Up @@ -305,36 +305,6 @@ function getCommentsDeprecation() {
);
}

/**
* 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/developer-guide/working-with-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/developer-guide/working-with-rules#options-schemas`,
"DeprecationWarning"
);
}
}

//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
Expand Down Expand Up @@ -509,7 +479,7 @@ class RuleTester {
/**
* Define a rule for one particular run of tests.
* @param {string} name The name of the rule to define.
* @param {Function} rule The rule definition.
* @param {RuleModule} rule The rule definition.
* @returns {void}
*/
defineRule(name, rule) {
Expand All @@ -519,7 +489,7 @@ class RuleTester {
/**
* Adds a new rule test to execute.
* @param {string} ruleName The name of the rule to run.
* @param {Function} rule The rule to test.
* @param {RuleModule} rule The rule to test.
* @param {{
* valid: (ValidTestCase | string)[],
* invalid: InvalidTestCase[]
Expand Down Expand Up @@ -552,7 +522,9 @@ class RuleTester {
}

if (typeof rule === "function") {
emitLegacyRuleAPIWarning(ruleName);
throw new Error(
`"${ruleName}" rule is using the deprecated function-style format. Please use object-style format: https://eslint.org/docs/developer-guide/working-with-rules`
);
}

linter.defineRule(ruleName, Object.assign({}, rule, {
Expand All @@ -563,7 +535,7 @@ class RuleTester {
freezeDeeply(context.settings);
freezeDeeply(context.parserOptions);

return (typeof rule === "function" ? rule : rule.create)(context);
return rule.create(context);
}
}));

Expand Down Expand Up @@ -613,12 +585,13 @@ class RuleTester {
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);
throw new Error(
`"${ruleName}" rule has options but is missing the "meta.schema" property. Please add a schema: https://eslint.org/docs/developer-guide/working-with-rules#options-schemas`
);
}
config.rules[ruleName] = [1].concat(item.options);
} else {
Expand All @@ -632,14 +605,18 @@ class RuleTester {
* The goal is to check whether or not AST was modified when
* running the rule under test.
*/
linter.defineRule("rule-tester/validate-ast", () => ({
Program(node) {
beforeAST = cloneDeeplyExcludesParent(node);
},
"Program:exit"(node) {
afterAST = node;
linter.defineRule("rule-tester/validate-ast", {
create() {
return {
Program(node) {
beforeAST = cloneDeeplyExcludesParent(node);
},
"Program:exit"(node) {
afterAST = node;
}
};
}
}));
});

if (typeof config.parser === "string") {
assert(path.isAbsolute(config.parser), "Parsers provided as strings to RuleTester must be absolute paths");
Expand Down
18 changes: 11 additions & 7 deletions lib/shared/config-validator.js
Expand Up @@ -47,6 +47,14 @@ const severityMap = {
off: 0
};

const SCHEMA_NO_OPTIONS = {
type: "array",
minItems: 0,
maxItems: 0
};

Object.freeze(SCHEMA_NO_OPTIONS);

/**
* Gets a complete options schema for a rule.
* @param {{create: Function, schema: (Array|null)}} rule A new-style rule object
Expand All @@ -57,7 +65,7 @@ function getRuleOptionsSchema(rule) {
return null;
}

const schema = rule.schema || rule.meta && rule.meta.schema;
const schema = rule.meta && rule.meta.schema;

// Given a tuple of schemas, insert warning level at the beginning
if (Array.isArray(schema)) {
Expand All @@ -69,16 +77,12 @@ function getRuleOptionsSchema(rule) {
maxItems: schema.length
};
}
return {
type: "array",
minItems: 0,
maxItems: 0
};
return SCHEMA_NO_OPTIONS;

}

// Given a full schema, leave it alone
return schema || null;
return schema || SCHEMA_NO_OPTIONS;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion lib/shared/types.js
Expand Up @@ -164,7 +164,7 @@ module.exports = {};
* @property {Record<string, ConfigData>} [configs] The definition of plugin configs.
* @property {Record<string, Environment>} [environments] The definition of plugin environments.
* @property {Record<string, Processor>} [processors] The definition of plugin processors.
* @property {Record<string, Function | Rule>} [rules] The definition of plugin rules.
* @property {Record<string, Rule>} [rules] The definition of plugin rules.
*/

/**
Expand Down
24 changes: 13 additions & 11 deletions tests/fixtures/rules/custom-rule.js
@@ -1,15 +1,17 @@
module.exports = function(context) {
module.exports = {
meta: {
schema: []
},
create(context) {

"use strict";
"use strict";

return {
"Identifier": function(node) {
if (node.name === "foo") {
context.report(node, "Identifier cannot be named 'foo'.");
return {
"Identifier": function(node) {
if (node.name === "foo") {
context.report(node, "Identifier cannot be named 'foo'.");
}
}
}
};

};
}
};

module.exports.schema = [];

0 comments on commit b3ff346

Please sign in to comment.