Skip to content

Commit 7f1ae1f

Browse files
committed
Breaking: Add support for TypeScript rules
1 parent df662c0 commit 7f1ae1f

File tree

2 files changed

+63
-16
lines changed

2 files changed

+63
-16
lines changed

lib/utils.js

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,23 @@ function isRuleTesterConstruction (node) {
8585

8686
const INTERESTING_RULE_KEYS = new Set(['create', 'meta']);
8787

88+
/**
89+
* Collect properties from an object that have interesting key names into a new object
90+
* @param {Node[]} properties
91+
* @param {Set<String>} interestingKeys
92+
* @returns Object
93+
*/
94+
function collectInterestingProperties (properties, interestingKeys) {
95+
// eslint-disable-next-line unicorn/prefer-object-from-entries
96+
return properties.reduce((parsedProps, prop) => {
97+
const keyValue = module.exports.getKeyName(prop);
98+
if (interestingKeys.has(keyValue)) {
99+
parsedProps[keyValue] = prop.value;
100+
}
101+
return parsedProps;
102+
}, {});
103+
}
104+
88105
/**
89106
* Helper for `getRuleInfo`. Handles ESM rules.
90107
*/
@@ -95,16 +112,27 @@ function getRuleExportsESM (ast) {
95112
// eslint-disable-next-line unicorn/prefer-object-from-entries
96113
.reduce((currentExports, node) => {
97114
if (node.type === 'ObjectExpression') {
98-
// eslint-disable-next-line unicorn/prefer-object-from-entries
99-
return node.properties.reduce((parsedProps, prop) => {
100-
const keyValue = module.exports.getKeyName(prop);
101-
if (INTERESTING_RULE_KEYS.has(keyValue)) {
102-
parsedProps[keyValue] = prop.value;
103-
}
104-
return parsedProps;
105-
}, {});
115+
// Check `export default { create() {}, meta: {} }`
116+
return collectInterestingProperties(node.properties, INTERESTING_RULE_KEYS);
106117
} else if (isNormalFunctionExpression(node)) {
118+
// Check `export default function() {}`
107119
return { create: node, meta: null, isNewStyle: false };
120+
} else if (
121+
node.type === 'CallExpression' &&
122+
node.arguments.length === 1 &&
123+
node.arguments[0].type === 'ObjectExpression' &&
124+
// Check various TypeScript rule helper formats.
125+
(
126+
// createESLintRule({ ... })
127+
node.callee.type === 'Identifier' ||
128+
// util.createRule({ ... })
129+
(node.callee.type === 'MemberExpression' && node.callee.object.type === 'Identifier' && node.callee.property.type === 'Identifier') ||
130+
// ESLintUtils.RuleCreator(docsUrl)({ ... })
131+
(node.callee.type === 'CallExpression' && node.callee.callee.type === 'MemberExpression' && node.callee.callee.object.type === 'Identifier' && node.callee.callee.property.type === 'Identifier')
132+
)
133+
) {
134+
// Check `export default someTypeScriptHelper({ create() {}, meta: {} });
135+
return collectInterestingProperties(node.arguments[0].properties, INTERESTING_RULE_KEYS);
108136
}
109137
return currentExports;
110138
}, {});
@@ -136,14 +164,7 @@ function getRuleExportsCJS (ast) {
136164
} else if (node.right.type === 'ObjectExpression') {
137165
// Check `module.exports = { create: function () {}, meta: {} }`
138166

139-
// eslint-disable-next-line unicorn/prefer-object-from-entries
140-
return node.right.properties.reduce((parsedProps, prop) => {
141-
const keyValue = module.exports.getKeyName(prop);
142-
if (INTERESTING_RULE_KEYS.has(keyValue)) {
143-
parsedProps[keyValue] = prop.value;
144-
}
145-
return parsedProps;
146-
}, {});
167+
return collectInterestingProperties(node.right.properties, INTERESTING_RULE_KEYS);
147168
}
148169
return {};
149170
} else if (

tests/lib/utils.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ describe('utils', () => {
3939
'export const foo = { create() {} }',
4040
'export default { foo: {} }',
4141
'const foo = {}; export default foo',
42+
'export default foo()({ create() {}, meta: {} });',
43+
'export default foo().bar({ create() {}, meta: {} });',
44+
'export default foo.bar.baz({ create() {}, meta: {} });',
45+
'export default foo(123);',
46+
'export default foo.bar(123);',
47+
'export default foo.bar()(123);',
4248
].forEach(noRuleCase => {
4349
it(`returns null for ${noRuleCase}`, () => {
4450
const ast = espree.parse(noRuleCase, { ecmaVersion: 8, range: true, sourceType: 'module' });
@@ -149,6 +155,26 @@ describe('utils', () => {
149155
meta: null,
150156
isNewStyle: false,
151157
},
158+
159+
// ESM + TypeScript helper
160+
// Util function only
161+
'export default createESLintRule({ create() {}, meta: {} })': {
162+
create: { type: 'FunctionExpression' },
163+
meta: { type: 'ObjectExpression' },
164+
isNewStyle: true,
165+
},
166+
// Util function from util object
167+
'export default util.createRule({ create() {}, meta: {} });': {
168+
create: { type: 'FunctionExpression' },
169+
meta: { type: 'ObjectExpression' },
170+
isNewStyle: true,
171+
},
172+
// Util function from util object with additional doc URL argument
173+
'export default ESLintUtils.RuleCreator(docsUrl)({ create() {}, meta: {} });': {
174+
create: { type: 'FunctionExpression' },
175+
meta: { type: 'ObjectExpression' },
176+
isNewStyle: true,
177+
},
152178
};
153179

154180
Object.keys(CASES).forEach(ruleSource => {

0 commit comments

Comments
 (0)