Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: drop support for function-style rules and rules missing schemas #16614

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
7 changes: 5 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 @@ -533,6 +533,7 @@ The `quotes` rule in this example has one option, `"double"` (the `error` is the

```js
module.exports = {
meta: { schema: [/* ... */] },
create: function(context) {
var isDouble = (context.options[0] === "double");

Expand Down Expand Up @@ -626,7 +627,9 @@ 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 `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`.

Providing a schema is mandatory when a rule has options. However, it is possible to opt-out of providing a schema using `schema: false`, but doing so is discouraged as it increases the chance of bugs and mistakes. Rules without options can simply omit the schema property or use `schema: []`, both of which prevent any options from being passed.

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
39 changes: 23 additions & 16 deletions lib/config/flat-config-helpers.js
Expand Up @@ -52,31 +52,42 @@ 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
* @returns {Object} JSON Schema for the rule's options.
* @throws {Error} An error if the schema is a common no-op.
*/
function getRuleOptionsSchema(rule) {

if (!rule) {
return null;
}

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

// Check if the rule opted-out of specifying a schema.
if (schema === false) {
return null;
}

// Check for no-op schema.
if (typeof schema === "object" && !Array.isArray(schema) && Object.keys(schema).length === 0) {
throw new Error("`schema: {}` is a no-op. For rules with options, please fill in a complete schema. For rules without options, please omit `schema` or use `schema: []`.");
}

if (Array.isArray(schema)) {
if (schema.length) {
Expand All @@ -87,16 +98,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(`"${ruleId}" rule is using the deprecated function-style format. Please use object-style format: https://eslint.org/docs/developer-guide/working-with-rules`);
}
this._rules[ruleId] = ruleModule;
}

/**
Expand Down
4 changes: 2 additions & 2 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
45 changes: 9 additions & 36 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 Down
2 changes: 1 addition & 1 deletion lib/rules/no-constructor-return.js
Expand Up @@ -20,7 +20,7 @@ module.exports = {
url: "https://eslint.org/docs/rules/no-constructor-return"
},

schema: {},
schema: [],

fixable: null,

Expand Down
29 changes: 22 additions & 7 deletions lib/shared/config-validator.js
Expand Up @@ -47,17 +47,36 @@ 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
* @returns {Object} JSON Schema for the rule's options.
* @throws {Error} An error if the schema is a common no-op.
*/
function getRuleOptionsSchema(rule) {
if (!rule) {
return null;
}

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

// Check if the rule opted-out of specifying a schema.
if (schema === false) {
return null;
}

// Check for no-op schema.
if (typeof schema === "object" && !Array.isArray(schema) && Object.keys(schema).length === 0) {
throw new Error("`schema: {}` is a no-op. For rules with options, please fill in a complete schema. For rules without options, please omit `schema` or use `schema: []`.");
}

// Given a tuple of schemas, insert warning level at the beginning
if (Array.isArray(schema)) {
Expand All @@ -69,16 +88,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
6 changes: 2 additions & 4 deletions tests/lib/linter/rules.js
Expand Up @@ -32,16 +32,14 @@ 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", () => {
it("throws when using deprecated function-style rule format", () => {

/**
* A rule that does nothing
* @returns {void}
*/
function rule() {}
rule.schema = [];
rules.define("foo", rule);
assert.deepStrictEqual(rules.get("foo"), { create: rule, schema: [] });
assert.throws(() => rules.define("foo", rule), "\"foo\" rule is using the deprecated function-style format. Please use object-style format: https://eslint.org/docs/developer-guide/working-with-rules");
});

it("should return the rule as-is if it was defined as an object with a create() method", () => {
Expand Down
24 changes: 0 additions & 24 deletions tests/lib/rule-tester/flat-rule-tester.js
Expand Up @@ -1701,30 +1701,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)", () => {
Expand Down