-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #25 from TrilonIO/feat/enforce-custom-provider-type
Feat/enforce custom provider type
- Loading branch information
Showing
8 changed files
with
555 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
v20 | ||
v20.11.1 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
./.node-version |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
--- | ||
description: 'Enforces a styleguide for provider types' | ||
--- | ||
|
||
Large teams can have the desire to limit or enforce a particular style of creating [custom providers](https://docs.nestjs.com/fundamentals/custom-providers); e.g. banning request-scoped providers to avoid potential circular dependencies, or [preferring factory providers over value providers to significantly increase performance](https://github.com/nestjs/nest/pull/12753). This rule enforces a particular type of provider to be used. | ||
|
||
## Options | ||
|
||
This rule accepts an object with the "prefer" property, which is an array containing one or more of the following values: | ||
|
||
- `value`: Enforces the use of value providers. | ||
- `factory`: Enforces the use of factory providers. | ||
- `class`: Enforces the use of class providers. | ||
- `existing`: Enforces the use of existing providers. | ||
|
||
|
||
### Example of Options | ||
|
||
```json | ||
"rules": { | ||
"@trilon/enforce-custom-provider-type": [ | ||
"warn", { | ||
"prefer": ["factory", "value"] | ||
} | ||
] | ||
} | ||
``` | ||
|
||
## Examples | ||
Considering the options above, the following examples will show how the rule behaves when the `prefer` option is set to `factory`. | ||
|
||
### ❌ Incorrect | ||
|
||
```ts | ||
const customValueProvider: Provider = { | ||
provide: 'TOKEN', | ||
useExisting: 'some-value' // ⚠️ provider is not of type ["factory", "value"] | ||
} | ||
|
||
const customClassProvider: Provider = { | ||
provide: AbstractClass, | ||
useClass: SomeClass // ⚠️ provider is not of type ["factory", "value"] | ||
} | ||
``` | ||
|
||
### ✅ Correct | ||
|
||
const factoryProvider: Provider = { | ||
provide: 'TOKEN', | ||
useFactory: () => 'some-value' | ||
} | ||
|
||
## When Not To Use It | ||
|
||
If you don't want to enforce a particular style of provider, you can disable this rule. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,7 +12,7 @@ | |
"LICENSE" | ||
], | ||
"engines": { | ||
"node": ">=20.9.0", | ||
"node": ">=18.*.*", | ||
"yarn": ">=4.0.2" | ||
}, | ||
"scripts": { | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,172 @@ | ||
import { | ||
ASTUtils, | ||
AST_NODE_TYPES, | ||
ESLintUtils, | ||
type TSESTree, | ||
} from '@typescript-eslint/utils'; | ||
|
||
const createRule = ESLintUtils.RuleCreator( | ||
(name) => `https://eslint.org/docs/latest/rules/${name}` | ||
); | ||
|
||
type ProviderType = 'class' | 'factory' | 'value' | 'existing' | 'unknown'; | ||
|
||
export type Options = [ | ||
{ | ||
prefer: ProviderType[]; | ||
}, | ||
]; | ||
|
||
const defaultOptions: Options = [ | ||
{ | ||
prefer: [], | ||
}, | ||
]; | ||
|
||
export type MessageIds = 'providerTypeMismatch'; | ||
|
||
export default createRule<Options, MessageIds>({ | ||
name: 'enforce-custom-provider-type', | ||
meta: { | ||
type: 'suggestion', | ||
docs: { | ||
description: 'Ensure that custom providers are of the preferred type', | ||
}, | ||
fixable: undefined, | ||
schema: [ | ||
{ | ||
type: 'object', | ||
properties: { | ||
prefer: { | ||
type: 'array', | ||
items: { | ||
type: 'string', | ||
enum: ['class', 'factory', 'value', 'existing'], | ||
}, | ||
}, | ||
}, | ||
}, | ||
], | ||
messages: { | ||
providerTypeMismatch: 'Provider is not of type {{ preferred }}', | ||
}, | ||
}, | ||
defaultOptions, | ||
create(context) { | ||
const options = context.options[0] || defaultOptions[0]; | ||
const preferredTypes = options.prefer; | ||
const providerTypesImported: string[] = []; | ||
return { | ||
'ImportDeclaration[source.value="@nestjs/common"]': ( | ||
node: TSESTree.ImportDeclaration | ||
) => { | ||
const specifiers = node.specifiers; | ||
|
||
const isImportSpecifier = ( | ||
node: TSESTree.ImportClause | ||
): node is TSESTree.ImportSpecifier => | ||
node.type === AST_NODE_TYPES.ImportSpecifier; | ||
|
||
const isProviderImport = (spec: TSESTree.ImportSpecifier) => | ||
[ | ||
'Provider', | ||
'ClassProvider', | ||
'FactoryProvider', | ||
'ValueProvider', | ||
].includes(spec.imported.name); | ||
|
||
specifiers | ||
.filter(isImportSpecifier) | ||
.filter(isProviderImport) | ||
.forEach((spec) => | ||
providerTypesImported.push(spec.local.name ?? spec.imported.name) | ||
); | ||
}, | ||
|
||
'Property[key.name="providers"] > ArrayExpression > ObjectExpression': ( | ||
node: TSESTree.ObjectExpression | ||
) => { | ||
for (const property of node.properties) { | ||
if (property.type === AST_NODE_TYPES.Property) { | ||
const providerType = providerTypeOfProperty(property); | ||
|
||
if ( | ||
providerType && | ||
!preferredTypes.includes(providerType) && | ||
preferredTypes.length > 0 | ||
) { | ||
context.report({ | ||
node: property, | ||
messageId: 'providerTypeMismatch', | ||
data: { | ||
preferred: preferredTypes, | ||
}, | ||
}); | ||
} | ||
} | ||
} | ||
}, | ||
|
||
'Identifier[typeAnnotation.typeAnnotation.type="TSTypeReference"]': ( | ||
node: TSESTree.Identifier | ||
) => { | ||
const typeName = ( | ||
node.typeAnnotation?.typeAnnotation as TSESTree.TSTypeReference | ||
).typeName; | ||
|
||
if ( | ||
ASTUtils.isIdentifier(typeName) && | ||
providerTypesImported.includes(typeName.name) && | ||
preferredTypes.length > 0 | ||
) { | ||
const providerType = providerTypeOfIdentifier(node); | ||
if (providerType && !preferredTypes.includes(providerType)) { | ||
context.report({ | ||
node, | ||
messageId: 'providerTypeMismatch', | ||
data: { | ||
preferred: preferredTypes, | ||
}, | ||
}); | ||
} | ||
} | ||
}, | ||
}; | ||
}, | ||
}); | ||
|
||
function providerTypeOfIdentifier( | ||
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) { | ||
type = providerTypeOfProperty(property); | ||
} | ||
} | ||
} | ||
|
||
return type; | ||
} | ||
} | ||
|
||
function providerTypeOfProperty( | ||
node: TSESTree.Property | ||
): ProviderType | undefined { | ||
const propertyKey = (node.key as TSESTree.Identifier)?.name; | ||
return propertyKey === 'useClass' | ||
? 'class' | ||
: propertyKey === 'useFactory' | ||
? 'factory' | ||
: propertyKey === 'useValue' | ||
? 'value' | ||
: propertyKey === 'useExisting' | ||
? 'existing' | ||
: undefined; | ||
}; |
Oops, something went wrong.