diff --git a/lib/utils.js b/lib/utils.js index 4ad42c24..da5faa74 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -118,55 +118,82 @@ function isTypeScriptRuleHelper(node) { * Helper for `getRuleInfo`. Handles ESM and TypeScript rules. */ function getRuleExportsESM(ast, scopeManager) { - return ast.body - .filter((statement) => - [ - 'ExportDefaultDeclaration', // export default rule; - 'TSExportAssignment', // export = rule; - ].includes(statement.type) - ) - .map((statement) => statement.declaration || statement.expression) - - .reduce((currentExports, node) => { - if (node.type === 'ObjectExpression') { - // Check `export default { create() {}, meta: {} }` - return collectInterestingProperties( - node.properties, - INTERESTING_RULE_KEYS - ); - } else if (isFunctionRule(node)) { - // Check `export default function(context) { return { ... }; }` - return { create: node, meta: null, isNewStyle: false }; - } else if (isTypeScriptRuleHelper(node)) { - // Check `export default someTypeScriptHelper({ create() {}, meta: {} }); - return collectInterestingProperties( - node.arguments[0].properties, - INTERESTING_RULE_KEYS - ); - } else if (node.type === 'Identifier') { - // Rule could be stored in a variable before being exported. - const possibleRule = findVariableValue(node, scopeManager); - if (possibleRule) { - if (possibleRule.type === 'ObjectExpression') { - // Check `const possibleRule = { ... }; export default possibleRule; - return collectInterestingProperties( - possibleRule.properties, - INTERESTING_RULE_KEYS - ); - } else if (isFunctionRule(possibleRule)) { - // Check `const possibleRule = function(context) { return { ... } }; export default possibleRule;` - return { create: possibleRule, meta: null, isNewStyle: false }; - } else if (isTypeScriptRuleHelper(possibleRule)) { - // Check `const possibleRule = someTypeScriptHelper({ ... }); export default possibleRule; - return collectInterestingProperties( - possibleRule.arguments[0].properties, - INTERESTING_RULE_KEYS - ); + const possibleNodes = []; + + for (const statement of ast.body) { + switch (statement.type) { + // export default rule; + case 'ExportDefaultDeclaration': { + possibleNodes.push(statement.declaration); + break; + } + // export = rule; + case 'TSExportAssignment': { + possibleNodes.push(statement.expression); + break; + } + // export const rule = { ... }; + // or export {rule}; + case 'ExportNamedDeclaration': { + for (const specifier of statement.specifiers) { + possibleNodes.push(specifier.local); + } + if (statement.declaration) { + if (statement.declaration.type === 'VariableDeclaration') { + for (const declarator of statement.declaration.declarations) { + if (declarator.init) { + possibleNodes.push(declarator.init); + } + } + } else { + possibleNodes.push(statement.declaration); } } + break; } - return currentExports; - }, {}); + } + } + + return possibleNodes.reduce((currentExports, node) => { + if (node.type === 'ObjectExpression') { + // Check `export default { create() {}, meta: {} }` + return collectInterestingProperties( + node.properties, + INTERESTING_RULE_KEYS + ); + } else if (isFunctionRule(node)) { + // Check `export default function(context) { return { ... }; }` + return { create: node, meta: null, isNewStyle: false }; + } else if (isTypeScriptRuleHelper(node)) { + // Check `export default someTypeScriptHelper({ create() {}, meta: {} }); + return collectInterestingProperties( + node.arguments[0].properties, + INTERESTING_RULE_KEYS + ); + } else if (node.type === 'Identifier') { + // Rule could be stored in a variable before being exported. + const possibleRule = findVariableValue(node, scopeManager); + if (possibleRule) { + if (possibleRule.type === 'ObjectExpression') { + // Check `const possibleRule = { ... }; export default possibleRule; + return collectInterestingProperties( + possibleRule.properties, + INTERESTING_RULE_KEYS + ); + } else if (isFunctionRule(possibleRule)) { + // Check `const possibleRule = function(context) { return { ... } }; export default possibleRule;` + return { create: possibleRule, meta: null, isNewStyle: false }; + } else if (isTypeScriptRuleHelper(possibleRule)) { + // Check `const possibleRule = someTypeScriptHelper({ ... }); export default possibleRule; + return collectInterestingProperties( + possibleRule.arguments[0].properties, + INTERESTING_RULE_KEYS + ); + } + } + } + return currentExports; + }, {}); } /** diff --git a/tests/lib/utils.js b/tests/lib/utils.js index fe771824..e90ecd6e 100644 --- a/tests/lib/utils.js +++ b/tests/lib/utils.js @@ -50,6 +50,12 @@ describe('utils', () => { 'module.exports = createESLintRule({ create() {}, meta: {} });', 'module.exports = util.createRule({ create() {}, meta: {} });', 'module.exports = ESLintUtils.RuleCreator(docsUrl)({ create() {}, meta: {} });', + + // Named export of a rule, only supported in ESM within this plugin + 'module.exports.rule = { create: function() {} };', + 'exports.rule = { create: function() {} };', + 'const rule = { create: function() {} }; module.exports.rule = rule;', + 'const rule = { create: function() {} }; exports.rule = rule;', ].forEach((noRuleCase) => { it(`returns null for ${noRuleCase}`, () => { const ast = espree.parse(noRuleCase, { ecmaVersion: 8, range: true }); @@ -65,15 +71,11 @@ describe('utils', () => { describe('the file does not have a valid rule (ESM)', () => { [ '', - 'export const foo = { create() {} }', 'export default { foo: {} }', 'const foo = {}; export default foo', 'const foo = 123; export default foo', 'const foo = function(){}; export default foo', - // Exports function but not default export. - 'export function foo (context) { return {}; }', - // Exports function but no object return inside function. 'export default function (context) { }', 'export default function (context) { return; }', @@ -209,6 +211,12 @@ describe('utils', () => { meta: { type: 'ObjectExpression' }, isNewStyle: true, }, + // No helper, exported variable. + 'export const rule = { create() {}, meta: {} };': { + create: { type: 'FunctionExpression' }, + meta: { type: 'ObjectExpression' }, + isNewStyle: true, + }, // no helper, variable with type. 'const rule: Rule.RuleModule = { create() {}, meta: {} }; export default rule;': { @@ -216,6 +224,33 @@ describe('utils', () => { meta: { type: 'ObjectExpression' }, isNewStyle: true, }, + // no helper, exported variable with type. + 'export const rule: Rule.RuleModule = { create() {}, meta: {} };': { + create: { type: 'FunctionExpression' }, + meta: { type: 'ObjectExpression' }, + isNewStyle: true, + }, + // no helper, exported reference with type. + 'const rule: Rule.RuleModule = { create() {}, meta: {} }; export {rule};': + { + create: { type: 'FunctionExpression' }, + meta: { type: 'ObjectExpression' }, + isNewStyle: true, + }, + // no helper, exported aliased reference with type. + 'const foo: Rule.RuleModule = { create() {}, meta: {} }; export {foo as rule};': + { + create: { type: 'FunctionExpression' }, + meta: { type: 'ObjectExpression' }, + isNewStyle: true, + }, + // no helper, exported variable with type in multiple declarations + 'export const foo = 5, rule: Rule.RuleModule = { create() {}, meta: {} };': + { + create: { type: 'FunctionExpression' }, + meta: { type: 'ObjectExpression' }, + isNewStyle: true, + }, // No helper, variable, `export =` syntax. 'const rule = { create() {}, meta: {} }; export = rule;': { create: { type: 'FunctionExpression' }, @@ -474,6 +509,16 @@ describe('utils', () => { meta: { type: 'ObjectExpression' }, isNewStyle: true, }, + 'export const rule = { create() {}, meta: {} };': { + create: { type: 'FunctionExpression' }, + meta: { type: 'ObjectExpression' }, + isNewStyle: true, + }, + 'const rule = { create() {}, meta: {} }; export {rule};': { + create: { type: 'FunctionExpression' }, + meta: { type: 'ObjectExpression' }, + isNewStyle: true, + }, // ESM (function style) 'export default function (context) { return {}; }': {