diff --git a/lib/rules/prefer-object-rule.js b/lib/rules/prefer-object-rule.js index 72b9920e..8873ec0f 100644 --- a/lib/rules/prefer-object-rule.js +++ b/lib/rules/prefer-object-rule.js @@ -46,7 +46,7 @@ module.exports = { // note - we intentionally don't worry about formatting here, as otherwise we have // to indent the function correctly - if (ruleInfo.create.type === 'FunctionExpression') { + if (ruleInfo.create.type === 'FunctionExpression' || ruleInfo.create.type === 'FunctionDeclaration') { const openParenToken = sourceCode.getFirstToken( ruleInfo.create, token => token.type === 'Punctuator' && token.value === '(' diff --git a/lib/utils.js b/lib/utils.js index 024360bc..f07d4991 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -83,8 +83,93 @@ function isRuleTesterConstruction (node) { ); } -module.exports = { +const INTERESTING_RULE_KEYS = new Set(['create', 'meta']); + +/** + * Helper for `getRuleInfo`. Handles ESM rules. + */ +function getRuleExportsESM (ast) { + return ast.body + .filter(statement => statement.type === 'ExportDefaultDeclaration') + .map(statement => statement.declaration) + // eslint-disable-next-line unicorn/prefer-object-from-entries + .reduce((currentExports, node) => { + if (node.type === 'ObjectExpression') { + // eslint-disable-next-line unicorn/prefer-object-from-entries + return node.properties.reduce((parsedProps, prop) => { + const keyValue = module.exports.getKeyName(prop); + if (INTERESTING_RULE_KEYS.has(keyValue)) { + parsedProps[keyValue] = prop.value; + } + return parsedProps; + }, {}); + } else if (isNormalFunctionExpression(node)) { + return { create: node, meta: null, isNewStyle: false }; + } + return currentExports; + }, {}); +} + +/** + * Helper for `getRuleInfo`. Handles CJS rules. + */ +function getRuleExportsCJS (ast) { + let exportsVarOverridden = false; + let exportsIsFunction = false; + return ast.body + .filter(statement => statement.type === 'ExpressionStatement') + .map(statement => statement.expression) + .filter(expression => expression.type === 'AssignmentExpression') + .filter(expression => expression.left.type === 'MemberExpression') + // eslint-disable-next-line unicorn/prefer-object-from-entries + .reduce((currentExports, node) => { + if ( + node.left.object.type === 'Identifier' && node.left.object.name === 'module' && + node.left.property.type === 'Identifier' && node.left.property.name === 'exports' + ) { + exportsVarOverridden = true; + if (isNormalFunctionExpression(node.right)) { + // Check `module.exports = function () {}` + + exportsIsFunction = true; + return { create: node.right, meta: null, isNewStyle: false }; + } else if (node.right.type === 'ObjectExpression') { + // Check `module.exports = { create: function () {}, meta: {} }` + + // eslint-disable-next-line unicorn/prefer-object-from-entries + return node.right.properties.reduce((parsedProps, prop) => { + const keyValue = module.exports.getKeyName(prop); + if (INTERESTING_RULE_KEYS.has(keyValue)) { + parsedProps[keyValue] = prop.value; + } + return parsedProps; + }, {}); + } + return {}; + } else if ( + !exportsIsFunction && + node.left.object.type === 'MemberExpression' && + node.left.object.object.type === 'Identifier' && node.left.object.object.name === 'module' && + node.left.object.property.type === 'Identifier' && node.left.object.property.name === 'exports' && + node.left.property.type === 'Identifier' && INTERESTING_RULE_KEYS.has(node.left.property.name) + ) { + // Check `module.exports.create = () => {}` + currentExports[node.left.property.name] = node.right; + } else if ( + !exportsVarOverridden && + node.left.object.type === 'Identifier' && node.left.object.name === 'exports' && + node.left.property.type === 'Identifier' && INTERESTING_RULE_KEYS.has(node.left.property.name) + ) { + // Check `exports.create = () => {}` + + currentExports[node.left.property.name] = node.right; + } + return currentExports; + }, {}); +} + +module.exports = { /** * Performs static analysis on an AST to try to determine the final value of `module.exports`. * @param {{ast: ASTNode, scopeManager?: ScopeManager}} sourceCode The object contains `Program` AST node, and optional `scopeManager` @@ -94,63 +179,7 @@ module.exports = { from the file, the return value will be `null`. */ getRuleInfo ({ ast, scopeManager }) { - const INTERESTING_KEYS = new Set(['create', 'meta']); - let exportsVarOverridden = false; - let exportsIsFunction = false; - - const exportNodes = ast.body - .filter(statement => statement.type === 'ExpressionStatement') - .map(statement => statement.expression) - .filter(expression => expression.type === 'AssignmentExpression') - .filter(expression => expression.left.type === 'MemberExpression') - // eslint-disable-next-line unicorn/prefer-object-from-entries - .reduce((currentExports, node) => { - if ( - node.left.object.type === 'Identifier' && node.left.object.name === 'module' && - node.left.property.type === 'Identifier' && node.left.property.name === 'exports' - ) { - exportsVarOverridden = true; - - if (isNormalFunctionExpression(node.right)) { - // Check `module.exports = function () {}` - - exportsIsFunction = true; - return { create: node.right, meta: null }; - } else if (node.right.type === 'ObjectExpression') { - // Check `module.exports = { create: function () {}, meta: {} }` - - exportsIsFunction = false; - // eslint-disable-next-line unicorn/prefer-object-from-entries - return node.right.properties.reduce((parsedProps, prop) => { - const keyValue = module.exports.getKeyName(prop); - if (INTERESTING_KEYS.has(keyValue)) { - parsedProps[keyValue] = prop.value; - } - return parsedProps; - }, {}); - } - return {}; - } else if ( - !exportsIsFunction && - node.left.object.type === 'MemberExpression' && - node.left.object.object.type === 'Identifier' && node.left.object.object.name === 'module' && - node.left.object.property.type === 'Identifier' && node.left.object.property.name === 'exports' && - node.left.property.type === 'Identifier' && INTERESTING_KEYS.has(node.left.property.name) - ) { - // Check `module.exports.create = () => {}` - - currentExports[node.left.property.name] = node.right; - } else if ( - !exportsVarOverridden && - node.left.object.type === 'Identifier' && node.left.object.name === 'exports' && - node.left.property.type === 'Identifier' && INTERESTING_KEYS.has(node.left.property.name) - ) { - // Check `exports.create = () => {}` - - currentExports[node.left.property.name] = node.right; - } - return currentExports; - }, {}); + const exportNodes = ast.sourceType === 'module' ? getRuleExportsESM(ast) : getRuleExportsCJS(ast); const createExists = Object.prototype.hasOwnProperty.call(exportNodes, 'create'); if (!createExists) { @@ -164,7 +193,7 @@ module.exports = { return null; } - return Object.assign({ isNewStyle: !exportsIsFunction, meta: null }, exportNodes); + return Object.assign({ isNewStyle: true, meta: null }, exportNodes); }, /** diff --git a/tests/lib/rules/meta-property-ordering.js b/tests/lib/rules/meta-property-ordering.js index 3d5f0947..bf545e80 100644 --- a/tests/lib/rules/meta-property-ordering.js +++ b/tests/lib/rules/meta-property-ordering.js @@ -24,6 +24,16 @@ ruleTester.run('test-case-property-ordering', rule, { create() {}, };`, + { + // ESM + code: ` + export default { + meta: {type, docs, fixable, schema, messages}, + create() {}, + };`, + parserOptions: { sourceType: 'module' }, + }, + ` module.exports = { meta: {docs, schema, messages}, @@ -85,6 +95,30 @@ ruleTester.run('test-case-property-ordering', rule, { };`, errors: [{ messageId: 'inconsistentOrder', data: { order: ['type', 'docs', 'fixable'].join(', ') } }], }, + { + // ESM + code: ` + export default { + meta: { + docs, + fixable, + type: 'problem', + }, + create() {}, + };`, + + output: ` + export default { + meta: { + type: 'problem', + docs, + fixable, + }, + create() {}, + };`, + parserOptions: { sourceType: 'module' }, + errors: [{ messageId: 'inconsistentOrder', data: { order: ['type', 'docs', 'fixable'].join(', ') } }], + }, { code: ` module.exports = { diff --git a/tests/lib/rules/prefer-message-ids.js b/tests/lib/rules/prefer-message-ids.js index cfabcef4..2dc53436 100644 --- a/tests/lib/rules/prefer-message-ids.js +++ b/tests/lib/rules/prefer-message-ids.js @@ -29,6 +29,17 @@ ruleTester.run('prefer-message-ids', rule, { } }; `, + { + // ESM + code: ` + export default { + create(context) { + context.report({ node, messageId: 'foo' }); + } + }; + `, + parserOptions: { sourceType: 'module' }, + }, ` module.exports = { create(context) { @@ -91,6 +102,18 @@ ruleTester.run('prefer-message-ids', rule, { `, errors: [{ messageId: 'foundMessage', type: 'Property' }], }, + { + // ESM + code: ` + export default { + create(context) { + context.report({ node, message: 'foo' }); + } + }; + `, + parserOptions: { sourceType: 'module' }, + errors: [{ messageId: 'foundMessage', type: 'Property' }], + }, { // With message in variable. code: ` diff --git a/tests/lib/rules/prefer-object-rule.js b/tests/lib/rules/prefer-object-rule.js index 1e48ec2d..77cc6f1e 100644 --- a/tests/lib/rules/prefer-object-rule.js +++ b/tests/lib/rules/prefer-object-rule.js @@ -11,8 +11,6 @@ const rule = require('../../../lib/rules/prefer-object-rule'); const RuleTester = require('eslint').RuleTester; -const ERROR = { messageId: 'preferObject', line: 2, column: 26 }; - // ------------------------------------------------------------------------------ // Tests // ------------------------------------------------------------------------------ @@ -63,6 +61,18 @@ ruleTester.run('prefer-object-rule', rule, { }; module.exports = rule; `, + + { + // ESM + code: ` + export default { + create(context) { + return { Program() { context.report() } }; + }, + }; + `, + parserOptions: { sourceType: 'module' }, + }, ], invalid: [ @@ -77,7 +87,7 @@ ruleTester.run('prefer-object-rule', rule, { return { Program() { context.report() } }; }}; `, - errors: [ERROR], + errors: [{ messageId: 'preferObject', line: 2, column: 26 }], }, { code: ` @@ -90,7 +100,7 @@ ruleTester.run('prefer-object-rule', rule, { return { Program() { context.report() } }; }}; `, - errors: [ERROR], + errors: [{ messageId: 'preferObject', line: 2, column: 26 }], }, { code: ` @@ -103,7 +113,35 @@ ruleTester.run('prefer-object-rule', rule, { return { Program() { context.report() } }; }}; `, - errors: [ERROR], + errors: [{ messageId: 'preferObject', line: 2, column: 26 }], + }, + + // ESM + { + code: ` + export default function (context) { + return { Program() { context.report() } }; + }; + `, + output: ` + export default {create(context) { + return { Program() { context.report() } }; + }}; + `, + parserOptions: { sourceType: 'module' }, + errors: [{ messageId: 'preferObject', line: 2, column: 24 }], + }, + { + code: 'export default function create() {};', + output: 'export default {create() {}};', + parserOptions: { sourceType: 'module' }, + errors: [{ messageId: 'preferObject', line: 1, column: 16 }], + }, + { + code: 'export default () => {};', + output: 'export default {create: () => {}};', + parserOptions: { sourceType: 'module' }, + errors: [{ messageId: 'preferObject', line: 1, column: 16 }], }, ], }); diff --git a/tests/lib/rules/report-message-format.js b/tests/lib/rules/report-message-format.js index d93543d2..0dff15ac 100644 --- a/tests/lib/rules/report-message-format.js +++ b/tests/lib/rules/report-message-format.js @@ -33,6 +33,18 @@ ruleTester.run('report-message-format', rule, { `, options: ['foo'], }, + { + // ESM + code: ` + export default { + create(context) { + context.report(node, 'foo'); + } + }; + `, + options: ['foo'], + parserOptions: { sourceType: 'module' }, + }, { // With message as variable. code: ` @@ -164,6 +176,18 @@ ruleTester.run('report-message-format', rule, { `, options: ['foo'], }, + { + // ESM + code: ` + export default { + create(context) { + context.report(node, 'bar'); + } + }; + `, + options: ['foo'], + parserOptions: { sourceType: 'module' }, + }, { // With message as variable. code: ` diff --git a/tests/lib/rules/require-meta-docs-description.js b/tests/lib/rules/require-meta-docs-description.js index e8c67109..6c9aceba 100644 --- a/tests/lib/rules/require-meta-docs-description.js +++ b/tests/lib/rules/require-meta-docs-description.js @@ -21,6 +21,16 @@ ruleTester.run('require-meta-docs-description', rule, { create(context) {} }; `, + { + // ESM + code: ` + export default { + meta: { docs: { description: 'disallow unused variables' } }, + create(context) {} + }; + `, + parserOptions: { sourceType: 'module' }, + }, ` module.exports = { meta: { docs: { description: 'enforce a maximum line length' } }, @@ -104,6 +114,18 @@ ruleTester.run('require-meta-docs-description', rule, { output: null, errors: [{ messageId: 'missing', type: 'ObjectExpression' }], }, + { + // ESM + code: ` + export default { + meta: {}, + create(context) {} + }; + `, + output: null, + parserOptions: { sourceType: 'module' }, + errors: [{ messageId: 'missing', type: 'ObjectExpression' }], + }, { code: ` module.exports = { diff --git a/tests/lib/rules/require-meta-docs-url.js b/tests/lib/rules/require-meta-docs-url.js index b7195aef..0f755940 100644 --- a/tests/lib/rules/require-meta-docs-url.js +++ b/tests/lib/rules/require-meta-docs-url.js @@ -66,6 +66,20 @@ tester.run('require-meta-docs-url', rule, { pattern: 'path/to/{{name}}.md', }], }, + { + // ESM + filename: 'test-rule', + code: ` + export default { + meta: {docs: {url: "path/to/test-rule.md"}}, + create() {} + } + `, + options: [{ + pattern: 'path/to/{{name}}.md', + }], + parserOptions: { sourceType: 'module' }, + }, { // `url` in variable. filename: 'test-rule', @@ -537,6 +551,31 @@ url: "plugin-name/test.md" }], errors: [{ messageId: 'missing', type: 'ObjectExpression' }], }, + { + // ESM + filename: 'test.js', + code: ` + export default { + meta: {}, + create() {} + } + `, + output: ` + export default { + meta: { +docs: { +url: "plugin-name/test.md" +} +}, + create() {} + } + `, + options: [{ + pattern: 'plugin-name/{{ name }}.md', + }], + parserOptions: { sourceType: 'module' }, + errors: [{ messageId: 'missing', type: 'ObjectExpression' }], + }, { filename: 'test.js', code: ` diff --git a/tests/lib/rules/require-meta-fixable.js b/tests/lib/rules/require-meta-fixable.js index ce4761b9..417cf42d 100644 --- a/tests/lib/rules/require-meta-fixable.js +++ b/tests/lib/rules/require-meta-fixable.js @@ -34,6 +34,18 @@ ruleTester.run('require-meta-fixable', rule, { } }; `, + { + // ESM + code: ` + export default { + meta: { fixable: 'code' }, + create(context) { + context.report({node, message, fix: foo}); + } + }; + `, + parserOptions: { sourceType: 'module' }, + }, // Value in variable. ` const fixable = 'code'; @@ -183,6 +195,17 @@ ruleTester.run('require-meta-fixable', rule, { `, errors: [{ messageId: 'missing', type: 'ObjectExpression' }], }, + { + // ESM + code: ` + export default { + meta: {}, + create(context) { context.report({node, message, fix: foo}); } + }; + `, + parserOptions: { sourceType: 'module' }, + errors: [{ messageId: 'missing', type: 'ObjectExpression' }], + }, { code: ` module.exports = { diff --git a/tests/lib/rules/require-meta-has-suggestions.js b/tests/lib/rules/require-meta-has-suggestions.js index 0695e343..6218a4a7 100644 --- a/tests/lib/rules/require-meta-has-suggestions.js +++ b/tests/lib/rules/require-meta-has-suggestions.js @@ -112,6 +112,18 @@ ruleTester.run('require-meta-has-suggestions', rule, { } }; `, + { + // ESM: Provides suggestions, has hasSuggestions property. + code: ` + export default { + meta: { hasSuggestions: true }, + create(context) { + context.report({node, message, suggest: [{}]}); + } + }; + `, + parserOptions: { sourceType: 'module' }, + }, // Provides suggestions, has hasSuggestions property (as variable). ` const hasSuggestions = true; @@ -186,6 +198,17 @@ ruleTester.run('require-meta-has-suggestions', rule, { output: null, errors: [{ messageId: 'shouldBeSuggestable', type: 'FunctionExpression', line: 3, column: 17, endLine: 3, endColumn: 78 }], }, + { + // ESM: Reports suggestions, no meta object, violation should be on `create` function. + code: ` + export default { + create(context) { context.report({node, message, suggest: [{}]}); } + }; + `, + output: null, + parserOptions: { sourceType: 'module' }, + errors: [{ messageId: 'shouldBeSuggestable', type: 'FunctionExpression', line: 3, column: 17, endLine: 3, endColumn: 78 }], + }, { // Reports suggestions, no hasSuggestions property, violation should be on `meta` object, empty meta object. code: ` diff --git a/tests/lib/rules/require-meta-schema.js b/tests/lib/rules/require-meta-schema.js index 7450ea75..07a0e5b0 100644 --- a/tests/lib/rules/require-meta-schema.js +++ b/tests/lib/rules/require-meta-schema.js @@ -53,6 +53,16 @@ ruleTester.run('require-meta-schema', rule, { create(context) { const options = foo.options; } }; `, + { + // ESM + code: ` + export default { + meta: { schema: { "enum": ["always", "never"] } }, + create(context) { const options = context.options; } + }; + `, + parserOptions: { sourceType: 'module' }, + }, ` const schema = []; module.exports = { @@ -144,6 +154,36 @@ schema: [] module.exports = { meta: { schema: [] +}, + create(context) {} + }; + `, + }, + ], + }, + ], + }, + { + // ESM + code: ` + export default { + meta: {}, + create(context) {} + }; + `, + output: null, + parserOptions: { sourceType: 'module' }, + errors: [ + { + messageId: 'missing', + type: 'ObjectExpression', + suggestions: [ + { + messageId: 'addEmptySchema', + output: ` + export default { + meta: { +schema: [] }, create(context) {} }; diff --git a/tests/lib/rules/require-meta-type.js b/tests/lib/rules/require-meta-type.js index 25ca0992..33b118c1 100644 --- a/tests/lib/rules/require-meta-type.js +++ b/tests/lib/rules/require-meta-type.js @@ -25,6 +25,16 @@ ruleTester.run('require-meta-type', rule, { create(context) {} }; `, + { + // ESM + code: ` + export default { + meta: { type: 'problem' }, + create(context) {} + }; + `, + parserOptions: { sourceType: 'module' }, + }, ` module.exports = { meta: { type: 'suggestion' }, @@ -81,6 +91,17 @@ ruleTester.run('require-meta-type', rule, { `, errors: [{ messageId: 'missing', type: 'ObjectExpression' }], }, + { + // ESM + code: ` + export default { + meta: {}, + create(context) {} + }; + `, + parserOptions: { sourceType: 'module' }, + errors: [{ messageId: 'missing', type: 'ObjectExpression' }], + }, { code: ` function create(context) {} diff --git a/tests/lib/utils.js b/tests/lib/utils.js index 8e28a935..ac97bd33 100644 --- a/tests/lib/utils.js +++ b/tests/lib/utils.js @@ -33,8 +33,23 @@ describe('utils', () => { }); }); + describe('the file does not have a valid rule (ESM)', () => { + [ + '', + 'export const foo = { create() {} }', + 'export default { foo: {} }', + 'const foo = {}; export default foo', + ].forEach(noRuleCase => { + it(`returns null for ${noRuleCase}`, () => { + const ast = espree.parse(noRuleCase, { ecmaVersion: 8, range: true, sourceType: 'module' }); + assert.isNull(utils.getRuleInfo({ ast }), 'Expected no rule to be found'); + }); + }); + }); + describe('the file has a valid rule', () => { const CASES = { + // CJS 'module.exports = { create: function foo() {} };': { create: { type: 'FunctionExpression', id: { name: 'foo' } }, // (This property will actually contain the AST node.) meta: null, @@ -110,11 +125,35 @@ describe('utils', () => { meta: null, isNewStyle: false, }, + + // ESM (object style) + 'export default { create() {} }': { + create: { type: 'FunctionExpression' }, + meta: null, + isNewStyle: true, + }, + 'export default { create() {}, meta: {} }': { + create: { type: 'FunctionExpression' }, + meta: { type: 'ObjectExpression' }, + isNewStyle: true, + }, + + // ESM (function style) + 'export default function () {}': { + create: { type: 'FunctionDeclaration' }, + meta: null, + isNewStyle: false, + }, + 'export default () => {}': { + create: { type: 'ArrowFunctionExpression' }, + meta: null, + isNewStyle: false, + }, }; Object.keys(CASES).forEach(ruleSource => { it(ruleSource, () => { - const ast = espree.parse(ruleSource, { ecmaVersion: 6, range: true }); + const ast = espree.parse(ruleSource, { ecmaVersion: 6, range: true, sourceType: ruleSource.startsWith('export default') ? 'module' : 'script' }); const ruleInfo = utils.getRuleInfo({ ast }); assert( lodash.isMatch(ruleInfo, CASES[ruleSource]),