Skip to content
Open
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
82 changes: 79 additions & 3 deletions src/rules/__tests__/no-export.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ ruleTester.run('no-export', rule, {
valid: [
'describe("a test", () => { expect(1).toBe(1); })',
'window.location = "valid"',
'module.somethingElse = "foo";',
'export const myThing = "valid"',
'export default function () {}',
'module.exports = function(){}',
Expand All @@ -26,6 +25,69 @@ ruleTester.run('no-export', rule, {
expect(1).toBe(1);
});
`,
dedent`
const module = {};
module.somethingElse = 'foo';
test('a test', () => {
expect(1).toBe(1);
});
`,
dedent`
module.somethingElse = 'foo';
test('a test', () => {
expect(1).toBe(1);
});
`,
dedent`
const exports = {};
exports.foo = function () {};
test('a test', () => {
expect(1).toBe(1);
});
`,
dedent`
const exports = 'foo';
exports = function () {};
test('a test', () => {
expect(1).toBe(1);
});
`,
dedent`
function getModule() {
return module;
}

getModule().exports = function () {};

test('a test', () => {
expect(1).toBe(1);
});
`,
dedent`
const exports = 'exports';
module[exports] = function () {};
test('a test', () => {
expect(1).toBe(1);
});
`,
{
code: dedent`
exports.foo = function () {};
test('a test', () => {
expect(1).toBe(1);
});
`,
parserOptions: { sourceType: 'script' },
},
{
code: dedent`
exports = function () {};
test('a test', () => {
expect(1).toBe(1);
});
`,
parserOptions: { sourceType: 'script' },
},
],
invalid: [
{
Expand Down Expand Up @@ -77,6 +139,16 @@ ruleTester.run('no-export', rule, {
parserOptions: { sourceType: 'module' },
errors: [{ endColumn: 24, column: 1, messageId: 'unexpectedExport' }],
},
{
code: dedent`
module.export.invalid = function () {};

test('a test', () => {
expect(1).toBe(1);
});
`,
errors: [{ endColumn: 22, column: 1, messageId: 'unexpectedExport' }],
},
{
code: 'module.exports["invalid"] = function() {}; test("a test", () => { expect(1).toBe(1);});',
errors: [{ endColumn: 26, column: 1, messageId: 'unexpectedExport' }],
Expand All @@ -86,8 +158,12 @@ ruleTester.run('no-export', rule, {
errors: [{ endColumn: 15, column: 1, messageId: 'unexpectedExport' }],
},
{
code: 'module.export.invalid = function() {}; ; test("a test", () => { expect(1).toBe(1);});',
errors: [{ endColumn: 22, column: 1, messageId: 'unexpectedExport' }],
code: 'module["exports"] = function() {}; test("a test", () => { expect(1).toBe(1);});',
errors: [{ endColumn: 18, column: 1, messageId: 'unexpectedExport' }],
},
{
code: 'module.exports.foo.bar = function() {}; test("a test", () => { expect(1).toBe(1);});',
errors: [{ endColumn: 23, column: 1, messageId: 'unexpectedExport' }],
},
{
code: dedent`
Expand Down
81 changes: 57 additions & 24 deletions src/rules/no-export.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,58 @@
import { AST_NODE_TYPES, type TSESTree } from '@typescript-eslint/utils';
import {
AST_NODE_TYPES,
type TSESLint,
type TSESTree,
} from '@typescript-eslint/utils';
import { createRule, isTypeOfJestFnCall, resolveScope } from './utils';

const isGlobalModuleIdentifier = (
node: TSESTree.Identifier,
context: TSESLint.RuleContext<string, unknown[]>,
): boolean =>
node.name === 'module' &&
resolveScope(context.sourceCode.getScope(node), 'module') === null;

type MemberExpressionWithIdentifierObject = TSESTree.MemberExpression & {
object: TSESTree.Identifier;
};

const getModuleMemberExpressionRoot = (
member: TSESTree.MemberExpression,
): MemberExpressionWithIdentifierObject | null => {
let current = member;

while (current.object.type === AST_NODE_TYPES.MemberExpression) {
current = current.object;
}

return current.object.type === AST_NODE_TYPES.Identifier
? (current as MemberExpressionWithIdentifierObject)
: null;
};

const isCommonJsExportAssignment = (
node: TSESTree.AssignmentExpression,
context: TSESLint.RuleContext<string, unknown[]>,
): boolean => {
const { left } = node;

if (left.type !== AST_NODE_TYPES.MemberExpression) {
return false;
}

const root = getModuleMemberExpressionRoot(left);

return (
root !== null &&
isGlobalModuleIdentifier(root.object, context) &&
((!root.computed &&
root.property.type === AST_NODE_TYPES.Identifier &&
/^exports?$/u.test(root.property.name)) ||
(root.property.type === AST_NODE_TYPES.Literal &&
root.property.value === 'exports'))
);
};

export default createRule({
name: __filename,
meta: {
Expand All @@ -19,7 +71,7 @@ export default createRule({
| TSESTree.ExportNamedDeclaration
| TSESTree.ExportDefaultDeclaration
| TSESTree.TSExportAssignment
| TSESTree.MemberExpression
| TSESTree.AssignmentExpression['left']
> = [];
let hasTestCase = false;

Expand All @@ -45,28 +97,9 @@ export default createRule({
) {
exportNodes.push(node);
},
'AssignmentExpression > MemberExpression'(
node: TSESTree.MemberExpression,
) {
let { object, property } = node;

if (object.type === AST_NODE_TYPES.MemberExpression) {
({ object, property } = object);
}

if (
object.type !== AST_NODE_TYPES.Identifier ||
object.name !== 'module' ||
resolveScope(context.sourceCode.getScope(object), 'module') !== null
) {
return;
}

if (
property.type === AST_NODE_TYPES.Identifier &&
/^exports?$/u.test(property.name)
) {
exportNodes.push(node);
AssignmentExpression(node: TSESTree.AssignmentExpression) {
if (isCommonJsExportAssignment(node, context)) {
exportNodes.push(node.left);
}
},
};
Expand Down
Loading