From 061b85b69e73900c212af06afa74d64a74a936e6 Mon Sep 17 00:00:00 2001 From: Thiago Martins Date: Mon, 5 Feb 2024 10:04:05 -0300 Subject: [PATCH] feat: ensure the provider type is factory --- .../enforce-custom-provider-type.rule.ts | 72 ++++++++++++++++++- .../enforce-custom-provider-type.rule.spec.ts | 22 +++++- 2 files changed, 91 insertions(+), 3 deletions(-) diff --git a/src/rules/enforce-custom-provider-type.rule.ts b/src/rules/enforce-custom-provider-type.rule.ts index b094e86..3c68019 100644 --- a/src/rules/enforce-custom-provider-type.rule.ts +++ b/src/rules/enforce-custom-provider-type.rule.ts @@ -1,4 +1,9 @@ -import { ESLintUtils } from '@typescript-eslint/utils'; +import { + ASTUtils, + AST_NODE_TYPES, + ESLintUtils, + type TSESTree, +} from '@typescript-eslint/utils'; const createRule = ESLintUtils.RuleCreator( (name) => `https://eslint.org/docs/latest/rules/${name}` @@ -45,8 +50,71 @@ export default createRule({ }, defaultOptions, create(context) { + const options = context.options[0] || defaultOptions[0]; + const preferredType = options.prefer; return { - 'Program:exit': (node) => {}, + 'Identifier[typeAnnotation.typeAnnotation.type="TSTypeReference"]': ( + node: TSESTree.Identifier + ) => { + const typeName = ( + node.typeAnnotation?.typeAnnotation as TSESTree.TSTypeReference + ).typeName; + + if (ASTUtils.isIdentifier(typeName) && typeName.name === 'Provider') { + const providerType = getProviderType(node); + if (providerType && providerType !== preferredType) { + context.report({ + node, + messageId: 'providerTypeMismatch', + data: { + preferred: preferredType, + }, + }); + } + } + }, }; }, }); + +function getProviderType(node: TSESTree.Identifier): ProviderType | undefined { + const parent = node.parent; + + if (ASTUtils.isVariableDeclarator(parent)) { + const init = parent.init; + let type: ProviderType | undefined; + if (init?.type === AST_NODE_TYPES.ObjectExpression) { + const properties = init.properties; + for (const property of properties) { + if ( + property.type === AST_NODE_TYPES.Property && + ASTUtils.isIdentifier(property.key) && + property.key.name === 'useFactory' + ) { + type = 'factory'; + break; + } + + if ( + property.type === AST_NODE_TYPES.Property && + ASTUtils.isIdentifier(property.key) && + property.key.name === 'useClass' + ) { + type = 'class'; + break; + } + + if ( + property.type === AST_NODE_TYPES.Property && + ASTUtils.isIdentifier(property.key) && + property.key.name === 'useValue' + ) { + type = 'value'; + break; + } + } + } + + return type; + } +} diff --git a/tests/rules/enforce-custom-provider-type.rule.spec.ts b/tests/rules/enforce-custom-provider-type.rule.spec.ts index 109ae70..9e1b27b 100644 --- a/tests/rules/enforce-custom-provider-type.rule.spec.ts +++ b/tests/rules/enforce-custom-provider-type.rule.spec.ts @@ -33,5 +33,25 @@ ruleTester.run('enforce-custom-provider-type', enforceCustomProviderTypeRule, { ], }, ], - invalid: [], + invalid: [ + { + code: ` + import { Provider } from '@nestjs/common'; + const customValueProvider: Provider = { + provide: 'TOKEN', + useValue: 'some-value' // ⚠️ provider is not of type "factory" + } + `, + options: [ + { + prefer: 'factory', + }, + ], + errors: [ + { + messageId: 'providerTypeMismatch', + }, + ], + }, + ], });