diff --git a/eslint-internal-rules/no-invalid-meta-default-options.js b/eslint-internal-rules/no-invalid-meta-default-options.js new file mode 100644 index 000000000..919d8fa56 --- /dev/null +++ b/eslint-internal-rules/no-invalid-meta-default-options.js @@ -0,0 +1,156 @@ +/** + * @fileoverview Internal rule to enforce valid default options. + * @author Flo Edelmann + */ + +'use strict' + +const Ajv = require('ajv') +const metaSchema = require('ajv/lib/refs/json-schema-draft-04.json') + +// from https://github.com/eslint/eslint/blob/main/lib/shared/ajv.js +const ajv = new Ajv({ + meta: false, + useDefaults: true, + validateSchema: false, + missingRefs: 'ignore', + verbose: true, + schemaId: 'auto' +}) +ajv.addMetaSchema(metaSchema) +ajv._opts.defaultMeta = metaSchema.id + +// from https://github.com/eslint/eslint/blob/main/lib/config/flat-config-helpers.js +const noOptionsSchema = Object.freeze({ + type: 'array', + minItems: 0, + maxItems: 0 +}) +function getRuleOptionsSchema(schema) { + if (schema === false || typeof schema !== 'object' || schema === null) { + return null + } + + if (!Array.isArray(schema)) { + return schema + } + + if (schema.length === 0) { + return { ...noOptionsSchema } + } + + return { + type: 'array', + items: schema, + minItems: 0, + maxItems: schema.length + } +} + +/** + * @param {RuleContext} context + * @param {ASTNode} node + * @returns {any} + */ +function getNodeValue(context, node) { + try { + // eslint-disable-next-line no-eval + return eval(context.getSourceCode().getText(node)) + } catch (error) { + return undefined + } +} + +/** + * Gets the property of the Object node passed in that has the name specified. + * + * @param {string} propertyName Name of the property to return. + * @param {ASTNode} node The ObjectExpression node. + * @returns {ASTNode} The Property node or null if not found. + */ +function getPropertyFromObject(propertyName, node) { + if (node && node.type === 'ObjectExpression') { + for (const property of node.properties) { + if (property.type === 'Property' && property.key.name === propertyName) { + return property + } + } + } + return null +} + +module.exports = { + meta: { + type: 'problem', + docs: { + description: 'enforce correct use of `meta` property in core rules', + categories: ['Internal'] + }, + schema: [], + messages: { + defaultOptionsNotMatchingSchema: + 'Default options do not match the schema.' + } + }, + + create(context) { + /** @type {ASTNode} */ + let exportsNode + + return { + AssignmentExpression(node) { + if ( + node.left && + node.right && + node.left.type === 'MemberExpression' && + node.left.object.name === 'module' && + node.left.property.name === 'exports' + ) { + exportsNode = node.right + } + }, + + 'Program:exit'() { + const metaProperty = getPropertyFromObject('meta', exportsNode) + if (!metaProperty) { + return + } + + const metaSchema = getPropertyFromObject('schema', metaProperty.value) + const metaDefaultOptions = getPropertyFromObject( + 'defaultOptions', + metaProperty.value + ) + + if ( + !metaSchema || + !metaDefaultOptions || + metaDefaultOptions.value.type !== 'ArrayExpression' + ) { + return + } + + const defaultOptions = getNodeValue(context, metaDefaultOptions.value) + const schema = getNodeValue(context, metaSchema.value) + + if (!defaultOptions || !schema) { + return + } + + let validate + try { + validate = ajv.compile(getRuleOptionsSchema(schema)) + } catch (error) { + return + } + + if (!validate(defaultOptions)) { + context.report({ + node: metaDefaultOptions.value, + messageId: 'defaultOptionsNotMatchingSchema' + }) + } + } + } + } +} diff --git a/eslint.config.js b/eslint.config.js index 73c3ba2a4..925c9577c 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -35,6 +35,7 @@ module.exports = [ internal: { rules: { 'no-invalid-meta': require('./eslint-internal-rules/no-invalid-meta'), + 'no-invalid-meta-default-options': require('./eslint-internal-rules/no-invalid-meta-default-options'), 'no-invalid-meta-docs-categories': require('./eslint-internal-rules/no-invalid-meta-docs-categories'), 'require-eslint-community': require('./eslint-internal-rules/require-eslint-community') } @@ -45,7 +46,6 @@ module.exports = [ // turn off some rules from shared configs in all files { rules: { - 'eslint-plugin/require-meta-default-options': 'off', // TODO: enable when all rules have defaultOptions 'eslint-plugin/require-meta-docs-recommended': 'off', // use `categories` instead 'eslint-plugin/require-meta-schema-description': 'off', @@ -225,6 +225,7 @@ module.exports = [ { pattern: 'https://eslint.vuejs.org/rules/{{name}}.html' } ], 'internal/no-invalid-meta': 'error', + 'internal/no-invalid-meta-default-options': 'error', 'internal/no-invalid-meta-docs-categories': 'error' } }, @@ -233,6 +234,7 @@ module.exports = [ rules: { 'eslint-plugin/require-meta-docs-url': 'off', 'internal/no-invalid-meta': 'error', + 'internal/no-invalid-meta-default-options': 'error', 'internal/no-invalid-meta-docs-categories': 'error' } }, diff --git a/lib/rules/attribute-hyphenation.js b/lib/rules/attribute-hyphenation.js index 65d096cd4..95b001f2f 100644 --- a/lib/rules/attribute-hyphenation.js +++ b/lib/rules/attribute-hyphenation.js @@ -68,6 +68,13 @@ module.exports = { additionalProperties: false } ], + defaultOptions: [ + 'always', + { + ignore: [], + ignoreTags: [] + } + ], messages: { mustBeHyphenated: "Attribute '{{text}}' must be hyphenated.", cannotBeHyphenated: "Attribute '{{text}}' can't be hyphenated." diff --git a/lib/rules/attributes-order.js b/lib/rules/attributes-order.js index 50c9b452c..c51ccb4b8 100644 --- a/lib/rules/attributes-order.js +++ b/lib/rules/attributes-order.js @@ -455,6 +455,24 @@ module.exports = { additionalProperties: false } ], + defaultOptions: [ + { + order: [ + ATTRS.DEFINITION, + ATTRS.LIST_RENDERING, + ATTRS.CONDITIONALS, + ATTRS.RENDER_MODIFIERS, + ATTRS.GLOBAL, + [ATTRS.UNIQUE, ATTRS.SLOT], + ATTRS.TWO_WAY_BINDING, + ATTRS.OTHER_DIRECTIVES, + [ATTRS.ATTR_DYNAMIC, ATTRS.ATTR_STATIC, ATTRS.ATTR_SHORTHAND_BOOL], + ATTRS.EVENTS, + ATTRS.CONTENT + ], + alphabetical: false + } + ], messages: { expectedOrder: `Attribute "{{currentNode}}" should go before "{{prevNode}}".` } diff --git a/lib/rules/block-lang.js b/lib/rules/block-lang.js index 2190b17cb..483a887a7 100644 --- a/lib/rules/block-lang.js +++ b/lib/rules/block-lang.js @@ -151,6 +151,13 @@ module.exports = { additionalProperties: false } ], + defaultOptions: [ + { + script: { allowNoLang: true }, + template: { allowNoLang: true }, + style: { allowNoLang: true } + } + ], messages: { expected: "Only {{allows}} can be used for the 'lang' attribute of '<{{tag}}>'.", diff --git a/lib/rules/block-order.js b/lib/rules/block-order.js index 03b3d4a5f..6d93309f2 100644 --- a/lib/rules/block-order.js +++ b/lib/rules/block-order.js @@ -61,6 +61,11 @@ module.exports = { additionalProperties: false } ], + defaultOptions: [ + { + order: [['script', 'template'], 'style'] + } + ], messages: { unexpected: "'<{{elementName}}{{elementAttributes}}>' should be above '<{{firstUnorderedName}}{{firstUnorderedAttributes}}>' on line {{line}}." diff --git a/lib/rules/block-tag-newline.js b/lib/rules/block-tag-newline.js index 22fb12870..fc7f45a7b 100644 --- a/lib/rules/block-tag-newline.js +++ b/lib/rules/block-tag-newline.js @@ -71,6 +71,14 @@ module.exports = { additionalProperties: false } ], + defaultOptions: [ + { + singleline: 'consistent', + multiline: 'always', + maxEmptyLines: 0, + blocks: {} + } + ], messages: { unexpectedOpeningLinebreak: "There should be no line break after '<{{tag}}>'.", diff --git a/lib/rules/comment-directive.js b/lib/rules/comment-directive.js index 655e222bd..d6a6b1bb5 100644 --- a/lib/rules/comment-directive.js +++ b/lib/rules/comment-directive.js @@ -289,6 +289,7 @@ module.exports = { additionalProperties: false } ], + defaultOptions: [{ reportUnusedDisableDirectives: false }], messages: { disableBlock: '--block {{key}}', enableBlock: '++block', diff --git a/lib/rules/component-api-style.js b/lib/rules/component-api-style.js index 550eebfed..c925305a8 100644 --- a/lib/rules/component-api-style.js +++ b/lib/rules/component-api-style.js @@ -216,6 +216,7 @@ module.exports = { minItems: 1 } ], + defaultOptions: [['script-setup', 'composition']], messages: { disallowScriptSetup: '`