Skip to content

Commit fe3dac2

Browse files
committed
Breaking: Support ESM rules
1 parent 30bb8e2 commit fe3dac2

13 files changed

+405
-64
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

+84-57
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,89 @@ function isRuleTesterConstruction (node) {
8383
);
8484
}
8585

86-
module.exports = {
86+
const INTERESTING_RULE_KEYS = new Set(['create', 'meta']);
8787

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+
.reduce((currentExports, node) => {
96+
if (node.type === 'ObjectExpression') {
97+
return node.properties.reduce((parsedProps, prop) => {
98+
const keyValue = module.exports.getKeyName(prop);
99+
if (INTERESTING_RULE_KEYS.has(keyValue)) {
100+
parsedProps[keyValue] = prop.value;
101+
}
102+
return parsedProps;
103+
}, {});
104+
} else if (isNormalFunctionExpression(node)) {
105+
return { create: node, meta: null, isNewStyle: false };
106+
}
107+
return currentExports;
108+
}, {});
109+
}
110+
111+
/**
112+
* Helper for `getRuleInfo`. Handles CJS rules.
113+
*/
114+
function getRuleExportsCJS (ast) {
115+
let exportsVarOverridden = false;
116+
let exportsIsFunction = false;
117+
return ast.body
118+
.filter(statement => statement.type === 'ExpressionStatement')
119+
.map(statement => statement.expression)
120+
.filter(expression => expression.type === 'AssignmentExpression')
121+
.filter(expression => expression.left.type === 'MemberExpression')
122+
.reduce((currentExports, node) => {
123+
if (
124+
node.left.object.type === 'Identifier' && node.left.object.name === 'module' &&
125+
node.left.property.type === 'Identifier' && node.left.property.name === 'exports'
126+
) {
127+
exportsVarOverridden = true;
128+
if (isNormalFunctionExpression(node.right)) {
129+
// Check `module.exports = function () {}`
130+
131+
exportsIsFunction = true;
132+
return { create: node.right, meta: null, isNewStyle: false };
133+
} else if (node.right.type === 'ObjectExpression') {
134+
// Check `module.exports = { create: function () {}, meta: {} }`
135+
136+
return node.right.properties.reduce((parsedProps, prop) => {
137+
const keyValue = module.exports.getKeyName(prop);
138+
if (INTERESTING_RULE_KEYS.has(keyValue)) {
139+
parsedProps[keyValue] = prop.value;
140+
}
141+
return parsedProps;
142+
}, {});
143+
}
144+
return {};
145+
} else if (
146+
!exportsIsFunction &&
147+
node.left.object.type === 'MemberExpression' &&
148+
node.left.object.object.type === 'Identifier' && node.left.object.object.name === 'module' &&
149+
node.left.object.property.type === 'Identifier' && node.left.object.property.name === 'exports' &&
150+
node.left.property.type === 'Identifier' && INTERESTING_RULE_KEYS.has(node.left.property.name)
151+
) {
152+
// Check `module.exports.create = () => {}`
153+
154+
currentExports[node.left.property.name] = node.right;
155+
} else if (
156+
!exportsVarOverridden &&
157+
node.left.object.type === 'Identifier' && node.left.object.name === 'exports' &&
158+
node.left.property.type === 'Identifier' && INTERESTING_RULE_KEYS.has(node.left.property.name)
159+
) {
160+
// Check `exports.create = () => {}`
161+
162+
currentExports[node.left.property.name] = node.right;
163+
}
164+
return currentExports;
165+
}, {});
166+
}
167+
168+
module.exports = {
88169
/**
89170
* Performs static analysis on an AST to try to determine the final value of `module.exports`.
90171
* @param {{ast: ASTNode, scopeManager?: ScopeManager}} sourceCode The object contains `Program` AST node, and optional `scopeManager`
@@ -94,61 +175,7 @@ module.exports = {
94175
from the file, the return value will be `null`.
95176
*/
96177
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-
.reduce((currentExports, node) => {
107-
if (
108-
node.left.object.type === 'Identifier' && node.left.object.name === 'module' &&
109-
node.left.property.type === 'Identifier' && node.left.property.name === 'exports'
110-
) {
111-
exportsVarOverridden = true;
112-
113-
if (isNormalFunctionExpression(node.right)) {
114-
// Check `module.exports = function () {}`
115-
116-
exportsIsFunction = true;
117-
return { create: node.right, meta: null };
118-
} else if (node.right.type === 'ObjectExpression') {
119-
// Check `module.exports = { create: function () {}, meta: {} }`
120-
121-
exportsIsFunction = false;
122-
return node.right.properties.reduce((parsedProps, prop) => {
123-
const keyValue = module.exports.getKeyName(prop);
124-
if (INTERESTING_KEYS.has(keyValue)) {
125-
parsedProps[keyValue] = prop.value;
126-
}
127-
return parsedProps;
128-
}, {});
129-
}
130-
return {};
131-
} else if (
132-
!exportsIsFunction &&
133-
node.left.object.type === 'MemberExpression' &&
134-
node.left.object.object.type === 'Identifier' && node.left.object.object.name === 'module' &&
135-
node.left.object.property.type === 'Identifier' && node.left.object.property.name === 'exports' &&
136-
node.left.property.type === 'Identifier' && INTERESTING_KEYS.has(node.left.property.name)
137-
) {
138-
// Check `module.exports.create = () => {}`
139-
140-
currentExports[node.left.property.name] = node.right;
141-
} else if (
142-
!exportsVarOverridden &&
143-
node.left.object.type === 'Identifier' && node.left.object.name === 'exports' &&
144-
node.left.property.type === 'Identifier' && INTERESTING_KEYS.has(node.left.property.name)
145-
) {
146-
// Check `exports.create = () => {}`
147-
148-
currentExports[node.left.property.name] = node.right;
149-
}
150-
return currentExports;
151-
}, {});
178+
const exportNodes = ast.sourceType === 'module' ? getRuleExportsESM(ast) : getRuleExportsCJS(ast);
152179

153180
const createExists = Object.prototype.hasOwnProperty.call(exportNodes, 'create');
154181
if (!createExists) {
@@ -162,7 +189,7 @@ module.exports = {
162189
return null;
163190
}
164191

165-
return Object.assign({ isNewStyle: !exportsIsFunction, meta: null }, exportNodes);
192+
return Object.assign({ isNewStyle: true, meta: null }, exportNodes);
166193
},
167194

168195
/**

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)