Skip to content

Commit

Permalink
feat: Add new no-meta-schema-default rule (#503)
Browse files Browse the repository at this point in the history
* Add new `no-meta-schema-default` rule

* Disable rule locally

* Compare `type` rather than `key`

Co-authored-by: 唯然 <[email protected]>

---------

Co-authored-by: 唯然 <[email protected]>
  • Loading branch information
FloEdelmann and aladdin-add authored Dec 18, 2024
1 parent 8de68ec commit dbd96b2
Show file tree
Hide file tree
Showing 5 changed files with 449 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ module.exports = [
| [meta-property-ordering](docs/rules/meta-property-ordering.md) | enforce the order of meta properties | | 🔧 | | |
| [no-deprecated-context-methods](docs/rules/no-deprecated-context-methods.md) | disallow usage of deprecated methods on rule context objects || 🔧 | | |
| [no-deprecated-report-api](docs/rules/no-deprecated-report-api.md) | disallow the version of `context.report()` with multiple arguments || 🔧 | | |
| [no-meta-schema-default](docs/rules/no-meta-schema-default.md) | disallow rules `meta.schema` properties to include defaults | | | | |
| [no-missing-message-ids](docs/rules/no-missing-message-ids.md) | disallow `messageId`s that are missing from `meta.messages` || | | |
| [no-missing-placeholders](docs/rules/no-missing-placeholders.md) | disallow missing placeholders in rule report messages || | | |
| [no-property-in-node](docs/rules/no-property-in-node.md) | disallow using `in` to narrow node types instead of looking at properties | | | | 💭 |
Expand Down
83 changes: 83 additions & 0 deletions docs/rules/no-meta-schema-default.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Disallow rules `meta.schema` properties to include defaults (`eslint-plugin/no-meta-schema-default`)

<!-- end auto-generated rule header -->

Since ESLint v9.15.0, rules' default options are supported using `meta.defaultOptions`. Additionally defining them using the `default` property in `meta.schema` is confusing, error-prone, and can be ambiguous for complex schemas.

## Rule Details

This rule disallows the `default` property in rules' `meta.schema`.

Examples of **incorrect** code for this rule:

```js
/* eslint eslint-plugin/no-meta-schema-default: error */

module.exports = {
meta: {
schema: [
{
elements: { type: 'string' },
type: 'array',
default: [],
},
],
},
create() {},
};

module.exports = {
meta: {
schema: {
type: 'object',
properties: {
foo: { type: 'string', default: 'bar' },
baz: { type: 'number', default: 42 },
},
},
},
create() {},
};
```

Examples of **correct** code for this rule:

```js
/* eslint eslint-plugin/no-meta-schema-default: error */

module.exports = {
meta: {
schema: [
{
elements: { type: 'string' },
type: 'array',
},
],
defaultOptions: [[]],
},
create() {},
};

module.exports = {
meta: {
schema: {
type: 'object',
properties: {
foo: { type: 'string' },
baz: { type: 'number' },
},
},
defaultOptions: [{ foo: 'bar', baz: 42 }],
},
create() {},
};
```

## When Not To Use It

When using [`eslint-doc-generator`](https://github.com/bmish/eslint-doc-generator) to generate documentation for your rules, you may want to disable this rule to include the `default` property in the generated documentation. This is because `eslint-doc-generator` does not yet support `meta.defaultOptions`, see [bmish/eslint-doc-generator#513](https://github.com/bmish/eslint-doc-generator/issues/513).

## Further Reading

- [ESLint rule docs: Option Defaults](https://eslint.org/docs/latest/extend/custom-rules#option-defaults)
- [RFC introducing `meta.defaultOptions`](https://github.com/eslint/rfcs/blob/main/designs/2023-rule-options-defaults/README.md)
1 change: 1 addition & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ module.exports = [
plugins: eslintPluginConfig.plugins,
rules: {
...eslintPluginConfig.rules,
'eslint-plugin/no-meta-schema-default': 'off', // TODO: enable once https://github.com/bmish/eslint-doc-generator/issues/513 is fixed and released
'eslint-plugin/report-message-format': ['error', '^[^a-z].*.$'],
'eslint-plugin/require-meta-docs-url': [
'error',
Expand Down
113 changes: 113 additions & 0 deletions lib/rules/no-meta-schema-default.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
'use strict';

const { getStaticValue } = require('@eslint-community/eslint-utils');
const utils = require('../utils');

// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------

/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
type: 'suggestion',
docs: {
description:
'disallow rules `meta.schema` properties to include defaults',
category: 'Rules',
recommended: false,
url: 'https://github.com/eslint-community/eslint-plugin-eslint-plugin/tree/HEAD/docs/rules/no-meta-schema-default.md',
},
schema: [],
messages: {
foundDefault: 'Disallowed default value in schema.',
},
},

create(context) {
const sourceCode = context.sourceCode || context.getSourceCode(); // TODO: just use context.sourceCode when dropping eslint < v9
const { scopeManager } = sourceCode;
const ruleInfo = utils.getRuleInfo(sourceCode);
if (!ruleInfo) {
return {};
}

const schemaNode = utils.getMetaSchemaNode(ruleInfo.meta, scopeManager);
if (!schemaNode) {
return {};
}

const schemaProperty = utils.getMetaSchemaNodeProperty(
schemaNode,
scopeManager,
);

if (schemaProperty?.type === 'ObjectExpression') {
checkSchemaElement(schemaProperty, true);
} else if (schemaProperty?.type === 'ArrayExpression') {
for (const element of schemaProperty.elements) {
checkSchemaElement(element, true);
}
}

return {};

function checkSchemaElement(node) {
if (node.type !== 'ObjectExpression') {
return;
}

for (const { type, key, value } of node.properties) {
if (type !== 'Property') {
continue;
}
const staticKey =
key.type === 'Identifier' ? { value: key.name } : getStaticValue(key);
if (!staticKey?.value) {
continue;
}

switch (key.name ?? key.value) {
case 'allOf':
case 'anyOf':
case 'oneOf': {
if (value.type === 'ArrayExpression') {
for (const element of value.elements) {
checkSchemaElement(element);
}
}

break;
}

case 'properties': {
if (Array.isArray(value.properties)) {
for (const property of value.properties) {
if (property.value?.type === 'ObjectExpression') {
checkSchemaElement(property.value);
}
}
}

break;
}

case 'elements': {
checkSchemaElement(value);

break;
}

case 'default': {
context.report({
messageId: 'foundDefault',
node: key,
});

break;
}
}
}
}
},
};
Loading

0 comments on commit dbd96b2

Please sign in to comment.