Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
102 changes: 102 additions & 0 deletions src/rules/detect-circular-reference.rule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import {
AST_NODE_TYPES,
ESLintUtils,
type TSESTree,
} from '@typescript-eslint/utils';

const createRule = ESLintUtils.RuleCreator(
(name) => `https://eslint.org/docs/latest/rules/${name}`
);

export type MessageIds =
| 'serviceCircularDependency'
| 'moduleCircularDependency';

const defaultOptions: unknown[] = [];

export default createRule<unknown[], MessageIds>({
name: 'detect-circular-reference',
meta: {
type: 'problem',
docs: {
description:
'Warns about circular dependencies with forwardRef() function',
recommended: 'recommended',
},
fixable: undefined,
schema: [], // no options
messages: {
serviceCircularDependency: '⚠️ Circular-dependency detected',
moduleCircularDependency: '⚠️ Circular-dependency detected',
},
},
defaultOptions,
create(context) {
let forwardRefName: string = 'forwardRef';
return {
'ImportDeclaration > ImportSpecifier[imported.name="forwardRef"]': (
node: TSESTree.ImportSpecifier & {
parent: TSESTree.ImportDeclaration;
imported: TSESTree.Identifier & {
source: TSESTree.Literal;
};
}
) => {
if (node.parent?.source.value === '@nestjs/common') {
forwardRefName = node.local.name;
}
},

'CallExpression > Identifier': (
node: TSESTree.Identifier & {
parent: TSESTree.CallExpression;
}
) => {
if (node.name !== forwardRefName) {
return;
}

if (isNodeWithinImportsArray(node.parent, forwardRefName)) {
return context.report({
messageId: 'moduleCircularDependency',
node,
loc: node.loc,
});
}

return context.report({
messageId: 'serviceCircularDependency',
node,
loc: node.loc,
});
},
};
},
});

function isNodeWithinImportsArray(
node: TSESTree.CallExpression,
forwardRefName: string
): boolean {
return !!(
node.parent?.type === AST_NODE_TYPES.ArrayExpression &&
node.parent?.elements.find((element) =>
isForwardRefExpression(element, forwardRefName)
)
);
}

function isForwardRefExpression(
node: TSESTree.Expression | null | TSESTree.SpreadElement,
forwardRefName: string
): node is TSESTree.CallExpression & {
callee: TSESTree.Identifier & {
name: string;
};
} {
return (
node?.type === AST_NODE_TYPES.CallExpression &&
node?.callee.type === AST_NODE_TYPES.Identifier &&
node?.callee.name === forwardRefName
);
}
95 changes: 95 additions & 0 deletions tests/rules/detect-circular-reference.rule.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { RuleTester } from '@typescript-eslint/rule-tester';
import detectCircularReferenceRule from '../../src/rules/detect-circular-reference.rule';

// This test required changes to the tsconfig file to allow importing from the rule-tester package.
// See https://github.com/typescript-eslint/typescript-eslint/issues/7284

const ruleTester = new RuleTester({
parserOptions: {
project: './tsconfig.json',
},
parser: '@typescript-eslint/parser',
defaultFilenames: {
// We need to specify a filename that will be used by the rule parser.
// Since the test process starts at the root of the project, we need to point to the sub folder containing it.
ts: './tests/rules/file.ts',
tsx: '',
},
});

ruleTester.run('detect-circular-reference', detectCircularReferenceRule, {
valid: [
{
code: `
import { forwardRef } from '@nestjs/common';
import { CommonService } from './common.service';
@Injectable()
export class CatsService {
constructor(
private commonService: CommonService,
) {}
}
`,
},
{
code: `
@Module({
imports: [CatsModule],
})
export class CommonModule {}
`,
},
],
invalid: [
{
code: `
import { forwardRef } from '@nestjs/common';

@Injectable()
export class CatsService {
constructor(
@Inject(forwardRef(() => CommonService)) // ⚠️ Circular-dependency detected
private commonService: CommonService,
) {}
}
`,
errors: [
{
messageId: 'serviceCircularDependency',
},
],
},
{
code: `
import { forwardRef as renamedForwardRef } from '@nestjs/common';

@Injectable()
export class CatsService {
constructor(
@Inject(renamedForwardRef(() => CommonService)) // ⚠️ Circular-dependency detected
private commonService: CommonService,
) {}
}
`,
errors: [
{
messageId: 'serviceCircularDependency',
},
],
},
{
code: `
import { forwardRef } from '@nestjs/common'
@Module({
imports: [forwardRef(() => CatsModule)], // ⚠️ Circular-dependency detected
})
export class CommonModule {}
`,
errors: [
{
messageId: 'moduleCircularDependency',
},
],
},
],
});