Skip to content

Breaking: Support ESM rules #177

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Sep 26, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/rules/prefer-object-rule.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ module.exports = {
// note - we intentionally don't worry about formatting here, as otherwise we have
// to indent the function correctly

if (ruleInfo.create.type === 'FunctionExpression') {
if (ruleInfo.create.type === 'FunctionExpression' || ruleInfo.create.type === 'FunctionDeclaration') {
const openParenToken = sourceCode.getFirstToken(
ruleInfo.create,
token => token.type === 'Punctuator' && token.value === '('
Expand Down
147 changes: 88 additions & 59 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,93 @@ function isRuleTesterConstruction (node) {
);
}

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

/**
* Helper for `getRuleInfo`. Handles ESM rules.
*/
function getRuleExportsESM (ast) {
return ast.body
.filter(statement => statement.type === 'ExportDefaultDeclaration')
.map(statement => statement.declaration)
// eslint-disable-next-line unicorn/prefer-object-from-entries
.reduce((currentExports, node) => {
if (node.type === 'ObjectExpression') {
// eslint-disable-next-line unicorn/prefer-object-from-entries
return node.properties.reduce((parsedProps, prop) => {
const keyValue = module.exports.getKeyName(prop);
if (INTERESTING_RULE_KEYS.has(keyValue)) {
parsedProps[keyValue] = prop.value;
}
return parsedProps;
}, {});
} else if (isNormalFunctionExpression(node)) {
return { create: node, meta: null, isNewStyle: false };
}
return currentExports;
}, {});
}

/**
* Helper for `getRuleInfo`. Handles CJS rules.
*/
function getRuleExportsCJS (ast) {
let exportsVarOverridden = false;
let exportsIsFunction = false;
return ast.body
.filter(statement => statement.type === 'ExpressionStatement')
.map(statement => statement.expression)
.filter(expression => expression.type === 'AssignmentExpression')
.filter(expression => expression.left.type === 'MemberExpression')
// eslint-disable-next-line unicorn/prefer-object-from-entries
.reduce((currentExports, node) => {
if (
node.left.object.type === 'Identifier' && node.left.object.name === 'module' &&
node.left.property.type === 'Identifier' && node.left.property.name === 'exports'
) {
exportsVarOverridden = true;
if (isNormalFunctionExpression(node.right)) {
// Check `module.exports = function () {}`

exportsIsFunction = true;
return { create: node.right, meta: null, isNewStyle: false };
} else if (node.right.type === 'ObjectExpression') {
// Check `module.exports = { create: function () {}, meta: {} }`

// eslint-disable-next-line unicorn/prefer-object-from-entries
return node.right.properties.reduce((parsedProps, prop) => {
const keyValue = module.exports.getKeyName(prop);
if (INTERESTING_RULE_KEYS.has(keyValue)) {
parsedProps[keyValue] = prop.value;
}
return parsedProps;
}, {});
}
return {};
} else if (
!exportsIsFunction &&
node.left.object.type === 'MemberExpression' &&
node.left.object.object.type === 'Identifier' && node.left.object.object.name === 'module' &&
node.left.object.property.type === 'Identifier' && node.left.object.property.name === 'exports' &&
node.left.property.type === 'Identifier' && INTERESTING_RULE_KEYS.has(node.left.property.name)
) {
// Check `module.exports.create = () => {}`

currentExports[node.left.property.name] = node.right;
} else if (
!exportsVarOverridden &&
node.left.object.type === 'Identifier' && node.left.object.name === 'exports' &&
node.left.property.type === 'Identifier' && INTERESTING_RULE_KEYS.has(node.left.property.name)
) {
// Check `exports.create = () => {}`

currentExports[node.left.property.name] = node.right;
}
return currentExports;
}, {});
}

module.exports = {
/**
* Performs static analysis on an AST to try to determine the final value of `module.exports`.
* @param {{ast: ASTNode, scopeManager?: ScopeManager}} sourceCode The object contains `Program` AST node, and optional `scopeManager`
Expand All @@ -94,63 +179,7 @@ module.exports = {
from the file, the return value will be `null`.
*/
getRuleInfo ({ ast, scopeManager }) {
const INTERESTING_KEYS = new Set(['create', 'meta']);
let exportsVarOverridden = false;
let exportsIsFunction = false;

const exportNodes = ast.body
.filter(statement => statement.type === 'ExpressionStatement')
.map(statement => statement.expression)
.filter(expression => expression.type === 'AssignmentExpression')
.filter(expression => expression.left.type === 'MemberExpression')
// eslint-disable-next-line unicorn/prefer-object-from-entries
.reduce((currentExports, node) => {
if (
node.left.object.type === 'Identifier' && node.left.object.name === 'module' &&
node.left.property.type === 'Identifier' && node.left.property.name === 'exports'
) {
exportsVarOverridden = true;

if (isNormalFunctionExpression(node.right)) {
// Check `module.exports = function () {}`

exportsIsFunction = true;
return { create: node.right, meta: null };
} else if (node.right.type === 'ObjectExpression') {
// Check `module.exports = { create: function () {}, meta: {} }`

exportsIsFunction = false;
// eslint-disable-next-line unicorn/prefer-object-from-entries
return node.right.properties.reduce((parsedProps, prop) => {
const keyValue = module.exports.getKeyName(prop);
if (INTERESTING_KEYS.has(keyValue)) {
parsedProps[keyValue] = prop.value;
}
return parsedProps;
}, {});
}
return {};
} else if (
!exportsIsFunction &&
node.left.object.type === 'MemberExpression' &&
node.left.object.object.type === 'Identifier' && node.left.object.object.name === 'module' &&
node.left.object.property.type === 'Identifier' && node.left.object.property.name === 'exports' &&
node.left.property.type === 'Identifier' && INTERESTING_KEYS.has(node.left.property.name)
) {
// Check `module.exports.create = () => {}`

currentExports[node.left.property.name] = node.right;
} else if (
!exportsVarOverridden &&
node.left.object.type === 'Identifier' && node.left.object.name === 'exports' &&
node.left.property.type === 'Identifier' && INTERESTING_KEYS.has(node.left.property.name)
) {
// Check `exports.create = () => {}`

currentExports[node.left.property.name] = node.right;
}
return currentExports;
}, {});
const exportNodes = ast.sourceType === 'module' ? getRuleExportsESM(ast) : getRuleExportsCJS(ast);

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

return Object.assign({ isNewStyle: !exportsIsFunction, meta: null }, exportNodes);
return Object.assign({ isNewStyle: true, meta: null }, exportNodes);
},

/**
Expand Down
34 changes: 34 additions & 0 deletions tests/lib/rules/meta-property-ordering.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@ ruleTester.run('test-case-property-ordering', rule, {
create() {},
};`,

{
// ESM
code: `
export default {
meta: {type, docs, fixable, schema, messages},
create() {},
};`,
parserOptions: { sourceType: 'module' },
},

`
module.exports = {
meta: {docs, schema, messages},
Expand Down Expand Up @@ -85,6 +95,30 @@ ruleTester.run('test-case-property-ordering', rule, {
};`,
errors: [{ messageId: 'inconsistentOrder', data: { order: ['type', 'docs', 'fixable'].join(', ') } }],
},
{
// ESM
code: `
export default {
meta: {
docs,
fixable,
type: 'problem',
},
create() {},
};`,

output: `
export default {
meta: {
type: 'problem',
docs,
fixable,
},
create() {},
};`,
parserOptions: { sourceType: 'module' },
errors: [{ messageId: 'inconsistentOrder', data: { order: ['type', 'docs', 'fixable'].join(', ') } }],
},
{
code: `
module.exports = {
Expand Down
23 changes: 23 additions & 0 deletions tests/lib/rules/prefer-message-ids.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,17 @@ ruleTester.run('prefer-message-ids', rule, {
}
};
`,
{
// ESM
code: `
export default {
create(context) {
context.report({ node, messageId: 'foo' });
}
};
`,
parserOptions: { sourceType: 'module' },
},
`
module.exports = {
create(context) {
Expand Down Expand Up @@ -91,6 +102,18 @@ ruleTester.run('prefer-message-ids', rule, {
`,
errors: [{ messageId: 'foundMessage', type: 'Property' }],
},
{
// ESM
code: `
export default {
create(context) {
context.report({ node, message: 'foo' });
}
};
`,
parserOptions: { sourceType: 'module' },
errors: [{ messageId: 'foundMessage', type: 'Property' }],
},
{
// With message in variable.
code: `
Expand Down
48 changes: 43 additions & 5 deletions tests/lib/rules/prefer-object-rule.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@
const rule = require('../../../lib/rules/prefer-object-rule');
const RuleTester = require('eslint').RuleTester;

const ERROR = { messageId: 'preferObject', line: 2, column: 26 };

// ------------------------------------------------------------------------------
// Tests
// ------------------------------------------------------------------------------
Expand Down Expand Up @@ -63,6 +61,18 @@ ruleTester.run('prefer-object-rule', rule, {
};
module.exports = rule;
`,

{
// ESM
code: `
export default {
create(context) {
return { Program() { context.report() } };
},
};
`,
parserOptions: { sourceType: 'module' },
},
],

invalid: [
Expand All @@ -77,7 +87,7 @@ ruleTester.run('prefer-object-rule', rule, {
return { Program() { context.report() } };
}};
`,
errors: [ERROR],
errors: [{ messageId: 'preferObject', line: 2, column: 26 }],
},
{
code: `
Expand All @@ -90,7 +100,7 @@ ruleTester.run('prefer-object-rule', rule, {
return { Program() { context.report() } };
}};
`,
errors: [ERROR],
errors: [{ messageId: 'preferObject', line: 2, column: 26 }],
},
{
code: `
Expand All @@ -103,7 +113,35 @@ ruleTester.run('prefer-object-rule', rule, {
return { Program() { context.report() } };
}};
`,
errors: [ERROR],
errors: [{ messageId: 'preferObject', line: 2, column: 26 }],
},

// ESM
{
code: `
export default function (context) {
return { Program() { context.report() } };
};
`,
output: `
export default {create(context) {
return { Program() { context.report() } };
}};
`,
parserOptions: { sourceType: 'module' },
errors: [{ messageId: 'preferObject', line: 2, column: 24 }],
},
{
code: 'export default function create() {};',
output: 'export default {create() {}};',
parserOptions: { sourceType: 'module' },
errors: [{ messageId: 'preferObject', line: 1, column: 16 }],
},
{
code: 'export default () => {};',
output: 'export default {create: () => {}};',
parserOptions: { sourceType: 'module' },
errors: [{ messageId: 'preferObject', line: 1, column: 16 }],
},
],
});
24 changes: 24 additions & 0 deletions tests/lib/rules/report-message-format.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,18 @@ ruleTester.run('report-message-format', rule, {
`,
options: ['foo'],
},
{
// ESM
code: `
export default {
create(context) {
context.report(node, 'foo');
}
};
`,
options: ['foo'],
parserOptions: { sourceType: 'module' },
},
{
// With message as variable.
code: `
Expand Down Expand Up @@ -164,6 +176,18 @@ ruleTester.run('report-message-format', rule, {
`,
options: ['foo'],
},
{
// ESM
code: `
export default {
create(context) {
context.report(node, 'bar');
}
};
`,
options: ['foo'],
parserOptions: { sourceType: 'module' },
},
{
// With message as variable.
code: `
Expand Down
Loading