Skip to content

Commit

Permalink
Breaking: Support ESM rules
Browse files Browse the repository at this point in the history
  • Loading branch information
bmish committed Sep 24, 2021
1 parent 20235b6 commit e8f89bf
Show file tree
Hide file tree
Showing 13 changed files with 421 additions and 66 deletions.
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

0 comments on commit e8f89bf

Please sign in to comment.