Skip to content

Commit e8f89bf

Browse files
committed
Breaking: Support ESM rules
1 parent 20235b6 commit e8f89bf

13 files changed

+421
-66
lines changed

lib/rules/prefer-object-rule.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ module.exports = {
4646
// note - we intentionally don't worry about formatting here, as otherwise we have
4747
// to indent the function correctly
4848

49-
if (ruleInfo.create.type === 'FunctionExpression') {
49+
if (ruleInfo.create.type === 'FunctionExpression' || ruleInfo.create.type === 'FunctionDeclaration') {
5050
const openParenToken = sourceCode.getFirstToken(
5151
ruleInfo.create,
5252
token => token.type === 'Punctuator' && token.value === '('

lib/utils.js

+88-59
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,93 @@ function isRuleTesterConstruction (node) {
8383
);
8484
}
8585

86-
module.exports = {
86+
const INTERESTING_RULE_KEYS = new Set(['create', 'meta']);
87+
88+
/**
89+
* Helper for `getRuleInfo`. Handles ESM rules.
90+
*/
91+
function getRuleExportsESM (ast) {
92+
return ast.body
93+
.filter(statement => statement.type === 'ExportDefaultDeclaration')
94+
.map(statement => statement.declaration)
95+
// eslint-disable-next-line unicorn/prefer-object-from-entries
96+
.reduce((currentExports, node) => {
97+
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+
}, {});
106+
} else if (isNormalFunctionExpression(node)) {
107+
return { create: node, meta: null, isNewStyle: false };
108+
}
109+
return currentExports;
110+
}, {});
111+
}
112+
113+
/**
114+
* Helper for `getRuleInfo`. Handles CJS rules.
115+
*/
116+
function getRuleExportsCJS (ast) {
117+
let exportsVarOverridden = false;
118+
let exportsIsFunction = false;
119+
return ast.body
120+
.filter(statement => statement.type === 'ExpressionStatement')
121+
.map(statement => statement.expression)
122+
.filter(expression => expression.type === 'AssignmentExpression')
123+
.filter(expression => expression.left.type === 'MemberExpression')
124+
// eslint-disable-next-line unicorn/prefer-object-from-entries
125+
.reduce((currentExports, node) => {
126+
if (
127+
node.left.object.type === 'Identifier' && node.left.object.name === 'module' &&
128+
node.left.property.type === 'Identifier' && node.left.property.name === 'exports'
129+
) {
130+
exportsVarOverridden = true;
131+
if (isNormalFunctionExpression(node.right)) {
132+
// Check `module.exports = function () {}`
133+
134+
exportsIsFunction = true;
135+
return { create: node.right, meta: null, isNewStyle: false };
136+
} else if (node.right.type === 'ObjectExpression') {
137+
// Check `module.exports = { create: function () {}, meta: {} }`
138+
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+
}, {});
147+
}
148+
return {};
149+
} else if (
150+
!exportsIsFunction &&
151+
node.left.object.type === 'MemberExpression' &&
152+
node.left.object.object.type === 'Identifier' && node.left.object.object.name === 'module' &&
153+
node.left.object.property.type === 'Identifier' && node.left.object.property.name === 'exports' &&
154+
node.left.property.type === 'Identifier' && INTERESTING_RULE_KEYS.has(node.left.property.name)
155+
) {
156+
// Check `module.exports.create = () => {}`
87157

158+
currentExports[node.left.property.name] = node.right;
159+
} else if (
160+
!exportsVarOverridden &&
161+
node.left.object.type === 'Identifier' && node.left.object.name === 'exports' &&
162+
node.left.property.type === 'Identifier' && INTERESTING_RULE_KEYS.has(node.left.property.name)
163+
) {
164+
// Check `exports.create = () => {}`
165+
166+
currentExports[node.left.property.name] = node.right;
167+
}
168+
return currentExports;
169+
}, {});
170+
}
171+
172+
module.exports = {
88173
/**
89174
* Performs static analysis on an AST to try to determine the final value of `module.exports`.
90175
* @param {{ast: ASTNode, scopeManager?: ScopeManager}} sourceCode The object contains `Program` AST node, and optional `scopeManager`
@@ -94,63 +179,7 @@ module.exports = {
94179
from the file, the return value will be `null`.
95180
*/
96181
getRuleInfo ({ ast, scopeManager }) {
97-
const INTERESTING_KEYS = new Set(['create', 'meta']);
98-
let exportsVarOverridden = false;
99-
let exportsIsFunction = false;
100-
101-
const exportNodes = ast.body
102-
.filter(statement => statement.type === 'ExpressionStatement')
103-
.map(statement => statement.expression)
104-
.filter(expression => expression.type === 'AssignmentExpression')
105-
.filter(expression => expression.left.type === 'MemberExpression')
106-
// eslint-disable-next-line unicorn/prefer-object-from-entries
107-
.reduce((currentExports, node) => {
108-
if (
109-
node.left.object.type === 'Identifier' && node.left.object.name === 'module' &&
110-
node.left.property.type === 'Identifier' && node.left.property.name === 'exports'
111-
) {
112-
exportsVarOverridden = true;
113-
114-
if (isNormalFunctionExpression(node.right)) {
115-
// Check `module.exports = function () {}`
116-
117-
exportsIsFunction = true;
118-
return { create: node.right, meta: null };
119-
} else if (node.right.type === 'ObjectExpression') {
120-
// Check `module.exports = { create: function () {}, meta: {} }`
121-
122-
exportsIsFunction = false;
123-
// eslint-disable-next-line unicorn/prefer-object-from-entries
124-
return node.right.properties.reduce((parsedProps, prop) => {
125-
const keyValue = module.exports.getKeyName(prop);
126-
if (INTERESTING_KEYS.has(keyValue)) {
127-
parsedProps[keyValue] = prop.value;
128-
}
129-
return parsedProps;
130-
}, {});
131-
}
132-
return {};
133-
} else if (
134-
!exportsIsFunction &&
135-
node.left.object.type === 'MemberExpression' &&
136-
node.left.object.object.type === 'Identifier' && node.left.object.object.name === 'module' &&
137-
node.left.object.property.type === 'Identifier' && node.left.object.property.name === 'exports' &&
138-
node.left.property.type === 'Identifier' && INTERESTING_KEYS.has(node.left.property.name)
139-
) {
140-
// Check `module.exports.create = () => {}`
141-
142-
currentExports[node.left.property.name] = node.right;
143-
} else if (
144-
!exportsVarOverridden &&
145-
node.left.object.type === 'Identifier' && node.left.object.name === 'exports' &&
146-
node.left.property.type === 'Identifier' && INTERESTING_KEYS.has(node.left.property.name)
147-
) {
148-
// Check `exports.create = () => {}`
149-
150-
currentExports[node.left.property.name] = node.right;
151-
}
152-
return currentExports;
153-
}, {});
182+
const exportNodes = ast.sourceType === 'module' ? getRuleExportsESM(ast) : getRuleExportsCJS(ast);
154183

155184
const createExists = Object.prototype.hasOwnProperty.call(exportNodes, 'create');
156185
if (!createExists) {
@@ -164,7 +193,7 @@ module.exports = {
164193
return null;
165194
}
166195

167-
return Object.assign({ isNewStyle: !exportsIsFunction, meta: null }, exportNodes);
196+
return Object.assign({ isNewStyle: true, meta: null }, exportNodes);
168197
},
169198

170199
/**

tests/lib/rules/meta-property-ordering.js

+34
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,16 @@ ruleTester.run('test-case-property-ordering', rule, {
2424
create() {},
2525
};`,
2626

27+
{
28+
// ESM
29+
code: `
30+
export default {
31+
meta: {type, docs, fixable, schema, messages},
32+
create() {},
33+
};`,
34+
parserOptions: { sourceType: 'module' },
35+
},
36+
2737
`
2838
module.exports = {
2939
meta: {docs, schema, messages},
@@ -85,6 +95,30 @@ ruleTester.run('test-case-property-ordering', rule, {
8595
};`,
8696
errors: [{ messageId: 'inconsistentOrder', data: { order: ['type', 'docs', 'fixable'].join(', ') } }],
8797
},
98+
{
99+
// ESM
100+
code: `
101+
export default {
102+
meta: {
103+
docs,
104+
fixable,
105+
type: 'problem',
106+
},
107+
create() {},
108+
};`,
109+
110+
output: `
111+
export default {
112+
meta: {
113+
type: 'problem',
114+
docs,
115+
fixable,
116+
},
117+
create() {},
118+
};`,
119+
parserOptions: { sourceType: 'module' },
120+
errors: [{ messageId: 'inconsistentOrder', data: { order: ['type', 'docs', 'fixable'].join(', ') } }],
121+
},
88122
{
89123
code: `
90124
module.exports = {

tests/lib/rules/prefer-message-ids.js

+23
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,17 @@ ruleTester.run('prefer-message-ids', rule, {
2929
}
3030
};
3131
`,
32+
{
33+
// ESM
34+
code: `
35+
export default {
36+
create(context) {
37+
context.report({ node, messageId: 'foo' });
38+
}
39+
};
40+
`,
41+
parserOptions: { sourceType: 'module' },
42+
},
3243
`
3344
module.exports = {
3445
create(context) {
@@ -91,6 +102,18 @@ ruleTester.run('prefer-message-ids', rule, {
91102
`,
92103
errors: [{ messageId: 'foundMessage', type: 'Property' }],
93104
},
105+
{
106+
// ESM
107+
code: `
108+
export default {
109+
create(context) {
110+
context.report({ node, message: 'foo' });
111+
}
112+
};
113+
`,
114+
parserOptions: { sourceType: 'module' },
115+
errors: [{ messageId: 'foundMessage', type: 'Property' }],
116+
},
94117
{
95118
// With message in variable.
96119
code: `

tests/lib/rules/prefer-object-rule.js

+43-5
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@
1111
const rule = require('../../../lib/rules/prefer-object-rule');
1212
const RuleTester = require('eslint').RuleTester;
1313

14-
const ERROR = { messageId: 'preferObject', line: 2, column: 26 };
15-
1614
// ------------------------------------------------------------------------------
1715
// Tests
1816
// ------------------------------------------------------------------------------
@@ -63,6 +61,18 @@ ruleTester.run('prefer-object-rule', rule, {
6361
};
6462
module.exports = rule;
6563
`,
64+
65+
{
66+
// ESM
67+
code: `
68+
export default {
69+
create(context) {
70+
return { Program() { context.report() } };
71+
},
72+
};
73+
`,
74+
parserOptions: { sourceType: 'module' },
75+
},
6676
],
6777

6878
invalid: [
@@ -77,7 +87,7 @@ ruleTester.run('prefer-object-rule', rule, {
7787
return { Program() { context.report() } };
7888
}};
7989
`,
80-
errors: [ERROR],
90+
errors: [{ messageId: 'preferObject', line: 2, column: 26 }],
8191
},
8292
{
8393
code: `
@@ -90,7 +100,7 @@ ruleTester.run('prefer-object-rule', rule, {
90100
return { Program() { context.report() } };
91101
}};
92102
`,
93-
errors: [ERROR],
103+
errors: [{ messageId: 'preferObject', line: 2, column: 26 }],
94104
},
95105
{
96106
code: `
@@ -103,7 +113,35 @@ ruleTester.run('prefer-object-rule', rule, {
103113
return { Program() { context.report() } };
104114
}};
105115
`,
106-
errors: [ERROR],
116+
errors: [{ messageId: 'preferObject', line: 2, column: 26 }],
117+
},
118+
119+
// ESM
120+
{
121+
code: `
122+
export default function (context) {
123+
return { Program() { context.report() } };
124+
};
125+
`,
126+
output: `
127+
export default {create(context) {
128+
return { Program() { context.report() } };
129+
}};
130+
`,
131+
parserOptions: { sourceType: 'module' },
132+
errors: [{ messageId: 'preferObject', line: 2, column: 24 }],
133+
},
134+
{
135+
code: 'export default function create() {};',
136+
output: 'export default {create() {}};',
137+
parserOptions: { sourceType: 'module' },
138+
errors: [{ messageId: 'preferObject', line: 1, column: 16 }],
139+
},
140+
{
141+
code: 'export default () => {};',
142+
output: 'export default {create: () => {}};',
143+
parserOptions: { sourceType: 'module' },
144+
errors: [{ messageId: 'preferObject', line: 1, column: 16 }],
107145
},
108146
],
109147
});

tests/lib/rules/report-message-format.js

+24
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,18 @@ ruleTester.run('report-message-format', rule, {
3333
`,
3434
options: ['foo'],
3535
},
36+
{
37+
// ESM
38+
code: `
39+
export default {
40+
create(context) {
41+
context.report(node, 'foo');
42+
}
43+
};
44+
`,
45+
options: ['foo'],
46+
parserOptions: { sourceType: 'module' },
47+
},
3648
{
3749
// With message as variable.
3850
code: `
@@ -164,6 +176,18 @@ ruleTester.run('report-message-format', rule, {
164176
`,
165177
options: ['foo'],
166178
},
179+
{
180+
// ESM
181+
code: `
182+
export default {
183+
create(context) {
184+
context.report(node, 'bar');
185+
}
186+
};
187+
`,
188+
options: ['foo'],
189+
parserOptions: { sourceType: 'module' },
190+
},
167191
{
168192
// With message as variable.
169193
code: `

0 commit comments

Comments
 (0)