Skip to content

Commit

Permalink
fix: detect rules exported using a variable (#233)
Browse files Browse the repository at this point in the history
  • Loading branch information
bmish committed Dec 8, 2021
1 parent 6c58c91 commit ae68f6b
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 17 deletions.
60 changes: 43 additions & 17 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,32 @@ function isFunctionRule (node) {
);
}

/**
* Check if the given node is a function call representing a known TypeScript rule creator format.
* @param {Node} node
* @returns {boolean}
*/
function isTypeScriptRuleHelper (node) {
return (
node.type === 'CallExpression' &&
node.arguments.length === 1 &&
node.arguments[0].type === 'ObjectExpression' &&
// Check various TypeScript rule helper formats.
(
// createESLintRule({ ... })
node.callee.type === 'Identifier' ||
// util.createRule({ ... })
(node.callee.type === 'MemberExpression' && node.callee.object.type === 'Identifier' && node.callee.property.type === 'Identifier') ||
// ESLintUtils.RuleCreator(docsUrl)({ ... })
(node.callee.type === 'CallExpression' && node.callee.callee.type === 'MemberExpression' && node.callee.callee.object.type === 'Identifier' && node.callee.callee.property.type === 'Identifier')
)
);
}

/**
* Helper for `getRuleInfo`. Handles ESM and TypeScript rules.
*/
function getRuleExportsESM (ast) {
function getRuleExportsESM (ast, scopeManager) {
return ast.body
.filter(statement => statement.type === 'ExportDefaultDeclaration')
.map(statement => statement.declaration)
Expand All @@ -97,22 +119,20 @@ function getRuleExportsESM (ast) {
} else if (isFunctionRule(node)) {
// Check `export default function(context) { return { ... }; }`
return { create: node, meta: null, isNewStyle: false };
} else if (
node.type === 'CallExpression' &&
node.arguments.length === 1 &&
node.arguments[0].type === 'ObjectExpression' &&
// Check various TypeScript rule helper formats.
(
// createESLintRule({ ... })
node.callee.type === 'Identifier' ||
// util.createRule({ ... })
(node.callee.type === 'MemberExpression' && node.callee.object.type === 'Identifier' && node.callee.property.type === 'Identifier') ||
// ESLintUtils.RuleCreator(docsUrl)({ ... })
(node.callee.type === 'CallExpression' && node.callee.callee.type === 'MemberExpression' && node.callee.callee.object.type === 'Identifier' && node.callee.callee.property.type === 'Identifier')
)
) {
} else if (isTypeScriptRuleHelper(node)) {
// Check `export default someTypeScriptHelper({ create() {}, meta: {} });
return collectInterestingProperties(node.arguments[0].properties, INTERESTING_RULE_KEYS);
} else if (node.type === 'Identifier') {
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 (isTypeScriptRuleHelper(possibleRule)) {
// Check `const possibleRule = someTypeScriptHelper({ ... }); export default possibleRule;
return collectInterestingProperties(possibleRule.arguments[0].properties, INTERESTING_RULE_KEYS);
}
}
}
return currentExports;
}, {});
Expand All @@ -121,7 +141,7 @@ function getRuleExportsESM (ast) {
/**
* Helper for `getRuleInfo`. Handles CJS rules.
*/
function getRuleExportsCJS (ast) {
function getRuleExportsCJS (ast, scopeManager) {
let exportsVarOverridden = false;
let exportsIsFunction = false;
return ast.body
Expand All @@ -145,6 +165,12 @@ function getRuleExportsCJS (ast) {
// Check `module.exports = { create: function () {}, meta: {} }`

return collectInterestingProperties(node.right.properties, INTERESTING_RULE_KEYS);
} else if (node.right.type === 'Identifier') {
const possibleRule = findVariableValue(node.right, scopeManager);
if (possibleRule && possibleRule.type === 'ObjectExpression') {
// Check `const possibleRule = { ... }; module.exports = possibleRule;
return collectInterestingProperties(possibleRule.properties, INTERESTING_RULE_KEYS);
}
}
return {};
} else if (
Expand Down Expand Up @@ -218,7 +244,7 @@ module.exports = {
from the file, the return value will be `null`.
*/
getRuleInfo ({ ast, scopeManager }) {
const exportNodes = ast.sourceType === 'module' ? getRuleExportsESM(ast) : getRuleExportsCJS(ast);
const exportNodes = ast.sourceType === 'module' ? getRuleExportsESM(ast, scopeManager) : getRuleExportsCJS(ast, scopeManager);

const createExists = Object.prototype.hasOwnProperty.call(exportNodes, 'create');
if (!createExists) {
Expand Down
14 changes: 14 additions & 0 deletions tests/lib/rules/require-meta-schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -370,5 +370,19 @@ schema: [] },
{ messageId: 'missing', type: 'ObjectExpression', suggestions: [] },
],
},
{
// `rule`, `create`, and `meta` as variable.
code: `
const meta = {};
const create = function create(context) { const options = context.options; }
const rule = { meta, create };
module.exports = rule;
`,
output: null,
errors: [
{ messageId: 'foundOptionsUsage', type: 'ObjectExpression', suggestions: [] },
{ messageId: 'missing', type: 'ObjectExpression', suggestions: [] },
],
},
],
});
25 changes: 25 additions & 0 deletions tests/lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ describe('utils', () => {
'',
'module.exports;',
'module.exports = foo;',
'const foo = {}; module.exports = foo;',
'const foo = function() { return {}; }; module.exports = foo;',
'const foo = 123; module.exports = foo;',
'module.boop = function(context) { return {};};',
'exports = function(context) { return {};};',
'module.exports = function* (context) { return {}; };',
Expand Down Expand Up @@ -62,6 +65,7 @@ describe('utils', () => {
'export const foo = { create() {} }',
'export default { foo: {} }',
'const foo = {}; export default foo',
'const foo = 123; export default foo',

// Exports function but not default export.
'export function foo (context) { return {}; }',
Expand Down Expand Up @@ -102,6 +106,7 @@ describe('utils', () => {
'export default foo<Options, MessageIds>(123);',
'export default foo.bar<Options, MessageIds>(123);',
'export default foo.bar()<Options, MessageIds>(123);',
'const notRule = foo(); export default notRule;',
].forEach(noRuleCase => {
it(`returns null for ${noRuleCase}`, () => {
const ast = typescriptEslintParser.parse(noRuleCase, { ecmaVersion: 8, range: true, sourceType: 'module' });
Expand Down Expand Up @@ -149,6 +154,11 @@ describe('utils', () => {
meta: { type: 'ObjectExpression' },
isNewStyle: true,
},
'const rule = createESLintRule({ create() {}, meta: {} }); export default rule;': {
create: { type: 'FunctionExpression' },
meta: { type: 'ObjectExpression' },
isNewStyle: true,
},

// Util function with "{} as const".
'export default createESLintRule({ create() {}, meta: {} as const });': {
Expand Down Expand Up @@ -292,6 +302,11 @@ describe('utils', () => {
meta: { type: 'ObjectExpression' },
isNewStyle: true,
},
'const rule = { create() {}, meta: {} }; module.exports = rule;': {
create: { type: 'FunctionExpression' },
meta: { type: 'ObjectExpression' },
isNewStyle: true,
},
};

Object.keys(CASES).forEach(ruleSource => {
Expand Down Expand Up @@ -330,6 +345,16 @@ describe('utils', () => {
meta: { type: 'ObjectExpression' },
isNewStyle: true,
},
'const rule = { create() {}, meta: {} }; export default rule;': {
create: { type: 'FunctionExpression' },
meta: { type: 'ObjectExpression' },
isNewStyle: true,
},
'const create = function() {}; const meta = {}; const rule = { create, meta }; export default rule;': {
create: { type: 'FunctionExpression' },
meta: { type: 'ObjectExpression' },
isNewStyle: true,
},

// ESM (function style)
'export default function (context) { return {}; }': {
Expand Down

0 comments on commit ae68f6b

Please sign in to comment.