Skip to content

Commit ae68f6b

Browse files
authored
fix: detect rules exported using a variable (#233)
1 parent 6c58c91 commit ae68f6b

File tree

3 files changed

+82
-17
lines changed

3 files changed

+82
-17
lines changed

Diff for: lib/utils.js

+43-17
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,32 @@ function isFunctionRule (node) {
8282
);
8383
}
8484

85+
/**
86+
* Check if the given node is a function call representing a known TypeScript rule creator format.
87+
* @param {Node} node
88+
* @returns {boolean}
89+
*/
90+
function isTypeScriptRuleHelper (node) {
91+
return (
92+
node.type === 'CallExpression' &&
93+
node.arguments.length === 1 &&
94+
node.arguments[0].type === 'ObjectExpression' &&
95+
// Check various TypeScript rule helper formats.
96+
(
97+
// createESLintRule({ ... })
98+
node.callee.type === 'Identifier' ||
99+
// util.createRule({ ... })
100+
(node.callee.type === 'MemberExpression' && node.callee.object.type === 'Identifier' && node.callee.property.type === 'Identifier') ||
101+
// ESLintUtils.RuleCreator(docsUrl)({ ... })
102+
(node.callee.type === 'CallExpression' && node.callee.callee.type === 'MemberExpression' && node.callee.callee.object.type === 'Identifier' && node.callee.callee.property.type === 'Identifier')
103+
)
104+
);
105+
}
106+
85107
/**
86108
* Helper for `getRuleInfo`. Handles ESM and TypeScript rules.
87109
*/
88-
function getRuleExportsESM (ast) {
110+
function getRuleExportsESM (ast, scopeManager) {
89111
return ast.body
90112
.filter(statement => statement.type === 'ExportDefaultDeclaration')
91113
.map(statement => statement.declaration)
@@ -97,22 +119,20 @@ function getRuleExportsESM (ast) {
97119
} else if (isFunctionRule(node)) {
98120
// Check `export default function(context) { return { ... }; }`
99121
return { create: node, meta: null, isNewStyle: false };
100-
} else if (
101-
node.type === 'CallExpression' &&
102-
node.arguments.length === 1 &&
103-
node.arguments[0].type === 'ObjectExpression' &&
104-
// Check various TypeScript rule helper formats.
105-
(
106-
// createESLintRule({ ... })
107-
node.callee.type === 'Identifier' ||
108-
// util.createRule({ ... })
109-
(node.callee.type === 'MemberExpression' && node.callee.object.type === 'Identifier' && node.callee.property.type === 'Identifier') ||
110-
// ESLintUtils.RuleCreator(docsUrl)({ ... })
111-
(node.callee.type === 'CallExpression' && node.callee.callee.type === 'MemberExpression' && node.callee.callee.object.type === 'Identifier' && node.callee.callee.property.type === 'Identifier')
112-
)
113-
) {
122+
} else if (isTypeScriptRuleHelper(node)) {
114123
// Check `export default someTypeScriptHelper({ create() {}, meta: {} });
115124
return collectInterestingProperties(node.arguments[0].properties, INTERESTING_RULE_KEYS);
125+
} else if (node.type === 'Identifier') {
126+
const possibleRule = findVariableValue(node, scopeManager);
127+
if (possibleRule) {
128+
if (possibleRule.type === 'ObjectExpression') {
129+
// Check `const possibleRule = { ... }; export default possibleRule;
130+
return collectInterestingProperties(possibleRule.properties, INTERESTING_RULE_KEYS);
131+
} else if (isTypeScriptRuleHelper(possibleRule)) {
132+
// Check `const possibleRule = someTypeScriptHelper({ ... }); export default possibleRule;
133+
return collectInterestingProperties(possibleRule.arguments[0].properties, INTERESTING_RULE_KEYS);
134+
}
135+
}
116136
}
117137
return currentExports;
118138
}, {});
@@ -121,7 +141,7 @@ function getRuleExportsESM (ast) {
121141
/**
122142
* Helper for `getRuleInfo`. Handles CJS rules.
123143
*/
124-
function getRuleExportsCJS (ast) {
144+
function getRuleExportsCJS (ast, scopeManager) {
125145
let exportsVarOverridden = false;
126146
let exportsIsFunction = false;
127147
return ast.body
@@ -145,6 +165,12 @@ function getRuleExportsCJS (ast) {
145165
// Check `module.exports = { create: function () {}, meta: {} }`
146166

147167
return collectInterestingProperties(node.right.properties, INTERESTING_RULE_KEYS);
168+
} else if (node.right.type === 'Identifier') {
169+
const possibleRule = findVariableValue(node.right, scopeManager);
170+
if (possibleRule && possibleRule.type === 'ObjectExpression') {
171+
// Check `const possibleRule = { ... }; module.exports = possibleRule;
172+
return collectInterestingProperties(possibleRule.properties, INTERESTING_RULE_KEYS);
173+
}
148174
}
149175
return {};
150176
} else if (
@@ -218,7 +244,7 @@ module.exports = {
218244
from the file, the return value will be `null`.
219245
*/
220246
getRuleInfo ({ ast, scopeManager }) {
221-
const exportNodes = ast.sourceType === 'module' ? getRuleExportsESM(ast) : getRuleExportsCJS(ast);
247+
const exportNodes = ast.sourceType === 'module' ? getRuleExportsESM(ast, scopeManager) : getRuleExportsCJS(ast, scopeManager);
222248

223249
const createExists = Object.prototype.hasOwnProperty.call(exportNodes, 'create');
224250
if (!createExists) {

Diff for: tests/lib/rules/require-meta-schema.js

+14
Original file line numberDiff line numberDiff line change
@@ -370,5 +370,19 @@ schema: [] },
370370
{ messageId: 'missing', type: 'ObjectExpression', suggestions: [] },
371371
],
372372
},
373+
{
374+
// `rule`, `create`, and `meta` as variable.
375+
code: `
376+
const meta = {};
377+
const create = function create(context) { const options = context.options; }
378+
const rule = { meta, create };
379+
module.exports = rule;
380+
`,
381+
output: null,
382+
errors: [
383+
{ messageId: 'foundOptionsUsage', type: 'ObjectExpression', suggestions: [] },
384+
{ messageId: 'missing', type: 'ObjectExpression', suggestions: [] },
385+
],
386+
},
373387
],
374388
});

Diff for: tests/lib/utils.js

+25
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ describe('utils', () => {
1616
'',
1717
'module.exports;',
1818
'module.exports = foo;',
19+
'const foo = {}; module.exports = foo;',
20+
'const foo = function() { return {}; }; module.exports = foo;',
21+
'const foo = 123; module.exports = foo;',
1922
'module.boop = function(context) { return {};};',
2023
'exports = function(context) { return {};};',
2124
'module.exports = function* (context) { return {}; };',
@@ -62,6 +65,7 @@ describe('utils', () => {
6265
'export const foo = { create() {} }',
6366
'export default { foo: {} }',
6467
'const foo = {}; export default foo',
68+
'const foo = 123; export default foo',
6569

6670
// Exports function but not default export.
6771
'export function foo (context) { return {}; }',
@@ -102,6 +106,7 @@ describe('utils', () => {
102106
'export default foo<Options, MessageIds>(123);',
103107
'export default foo.bar<Options, MessageIds>(123);',
104108
'export default foo.bar()<Options, MessageIds>(123);',
109+
'const notRule = foo(); export default notRule;',
105110
].forEach(noRuleCase => {
106111
it(`returns null for ${noRuleCase}`, () => {
107112
const ast = typescriptEslintParser.parse(noRuleCase, { ecmaVersion: 8, range: true, sourceType: 'module' });
@@ -149,6 +154,11 @@ describe('utils', () => {
149154
meta: { type: 'ObjectExpression' },
150155
isNewStyle: true,
151156
},
157+
'const rule = createESLintRule({ create() {}, meta: {} }); export default rule;': {
158+
create: { type: 'FunctionExpression' },
159+
meta: { type: 'ObjectExpression' },
160+
isNewStyle: true,
161+
},
152162

153163
// Util function with "{} as const".
154164
'export default createESLintRule({ create() {}, meta: {} as const });': {
@@ -292,6 +302,11 @@ describe('utils', () => {
292302
meta: { type: 'ObjectExpression' },
293303
isNewStyle: true,
294304
},
305+
'const rule = { create() {}, meta: {} }; module.exports = rule;': {
306+
create: { type: 'FunctionExpression' },
307+
meta: { type: 'ObjectExpression' },
308+
isNewStyle: true,
309+
},
295310
};
296311

297312
Object.keys(CASES).forEach(ruleSource => {
@@ -330,6 +345,16 @@ describe('utils', () => {
330345
meta: { type: 'ObjectExpression' },
331346
isNewStyle: true,
332347
},
348+
'const rule = { create() {}, meta: {} }; export default rule;': {
349+
create: { type: 'FunctionExpression' },
350+
meta: { type: 'ObjectExpression' },
351+
isNewStyle: true,
352+
},
353+
'const create = function() {}; const meta = {}; const rule = { create, meta }; export default rule;': {
354+
create: { type: 'FunctionExpression' },
355+
meta: { type: 'ObjectExpression' },
356+
isNewStyle: true,
357+
},
333358

334359
// ESM (function style)
335360
'export default function (context) { return {}; }': {

0 commit comments

Comments
 (0)