Skip to content

Commit 95e3e29

Browse files
authored
Merge pull request #25 from TrilonIO/feat/enforce-custom-provider-type
Feat/enforce custom provider type
2 parents 753a86d + 02c2f7b commit 95e3e29

File tree

8 files changed

+555
-6
lines changed

8 files changed

+555
-6
lines changed

.node-version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
v20
1+
v20.11.1

.nvmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
./.node-version

README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,12 @@ The "recommended" preset contains the rules listed below. If you need custom con
4646

4747
## Rules
4848

49-
| Rule | Description | Recommended |
49+
| Rule | Description | Type |
5050
| ------------------------------------------------------------------------------------ | -------------------------------------------------------------- | ----------- |
51-
| [`@trilon/enforce-close-testing-module`](docs/rules/enforce-close-testing-module.md) | Ensures NestJS testing modules are closed properly after tests ||
52-
| [`@trilon/check-inject-decorator`](docs/rules/check-inject-decorator.md) | Detects incorrect usage of `@Inject(TOKEN)` decorator ||
53-
| [`@trilon/detect-circular-reference`](docs/rules/detect-circular-reference.md) | Detects usage of `forwardRef()` method ||
51+
| [`@trilon/enforce-close-testing-module`](docs/rules/enforce-close-testing-module.md) | Ensures NestJS testing modules are closed properly after tests | Recommended ✅ |
52+
| [`@trilon/check-inject-decorator`](docs/rules/check-inject-decorator.md) | Detects incorrect usage of `@Inject(TOKEN)` decorator | Recommended ✅ |
53+
| [`@trilon/detect-circular-reference`](docs/rules/detect-circular-reference.md) | Detects usage of `forwardRef()` method | Recommended ✅ |
54+
| [`@trilon/enforce-custom-provider-type`](docs/rules/enforce-custom-provider-type.md) | Enforces a styleguide for provider types | Strict ⚠️ |
5455
---
5556

5657
# Trilon Consulting
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
---
2+
description: 'Enforces a styleguide for provider types'
3+
---
4+
5+
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.
6+
7+
## Options
8+
9+
This rule accepts an object with the "prefer" property, which is an array containing one or more of the following values:
10+
11+
- `value`: Enforces the use of value providers.
12+
- `factory`: Enforces the use of factory providers.
13+
- `class`: Enforces the use of class providers.
14+
- `existing`: Enforces the use of existing providers.
15+
16+
17+
### Example of Options
18+
19+
```json
20+
"rules": {
21+
"@trilon/enforce-custom-provider-type": [
22+
"warn", {
23+
"prefer": ["factory", "value"]
24+
}
25+
]
26+
}
27+
```
28+
29+
## Examples
30+
Considering the options above, the following examples will show how the rule behaves when the `prefer` option is set to `factory`.
31+
32+
### ❌ Incorrect
33+
34+
```ts
35+
const customValueProvider: Provider = {
36+
provide: 'TOKEN',
37+
useExisting: 'some-value' // ⚠️ provider is not of type ["factory", "value"]
38+
}
39+
40+
const customClassProvider: Provider = {
41+
provide: AbstractClass,
42+
useClass: SomeClass // ⚠️ provider is not of type ["factory", "value"]
43+
}
44+
```
45+
46+
### ✅ Correct
47+
48+
const factoryProvider: Provider = {
49+
provide: 'TOKEN',
50+
useFactory: () => 'some-value'
51+
}
52+
53+
## When Not To Use It
54+
55+
If you don't want to enforce a particular style of provider, you can disable this rule.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"LICENSE"
1313
],
1414
"engines": {
15-
"node": ">=20.9.0",
15+
"node": ">=18.*.*",
1616
"yarn": ">=4.0.2"
1717
},
1818
"scripts": {

src/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import enforceCloseTestingModuleRule from './rules/enforce-close-testing-module.rule';
22
import checkInjectDecoratorRule from './rules/check-inject-decorator.rule';
33
import detectCircularReferenceRule from './rules/detect-circular-reference.rule';
4+
import enforceCustomProviderTypeRule from './rules/enforce-custom-provider-type.rule';
45
// TODO: we should type this as ESLint.Plugin but there's a type incompatibilities with the utils package
56
const plugin = {
67
configs: {
@@ -11,11 +12,20 @@ const plugin = {
1112
'@trilon/detect-circular-reference': 'warn',
1213
},
1314
},
15+
strict: {
16+
rules: {
17+
'@trilon/enforce-close-testing-module': 'error',
18+
'@trilon/check-inject-decorator': 'error',
19+
'@trilon/detect-circular-reference': 'error',
20+
'@trilon/enforce-custom-provider-type': 'error',
21+
},
22+
},
1423
},
1524
rules: {
1625
'enforce-close-testing-module': enforceCloseTestingModuleRule,
1726
'check-inject-decorator': checkInjectDecoratorRule,
1827
'detect-circular-reference': detectCircularReferenceRule,
28+
'@trilon/enforce-custom-provider-type': enforceCustomProviderTypeRule,
1929
},
2030
};
2131

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import {
2+
ASTUtils,
3+
AST_NODE_TYPES,
4+
ESLintUtils,
5+
type TSESTree,
6+
} from '@typescript-eslint/utils';
7+
8+
const createRule = ESLintUtils.RuleCreator(
9+
(name) => `https://eslint.org/docs/latest/rules/${name}`
10+
);
11+
12+
type ProviderType = 'class' | 'factory' | 'value' | 'existing' | 'unknown';
13+
14+
export type Options = [
15+
{
16+
prefer: ProviderType[];
17+
},
18+
];
19+
20+
const defaultOptions: Options = [
21+
{
22+
prefer: [],
23+
},
24+
];
25+
26+
export type MessageIds = 'providerTypeMismatch';
27+
28+
export default createRule<Options, MessageIds>({
29+
name: 'enforce-custom-provider-type',
30+
meta: {
31+
type: 'suggestion',
32+
docs: {
33+
description: 'Ensure that custom providers are of the preferred type',
34+
},
35+
fixable: undefined,
36+
schema: [
37+
{
38+
type: 'object',
39+
properties: {
40+
prefer: {
41+
type: 'array',
42+
items: {
43+
type: 'string',
44+
enum: ['class', 'factory', 'value', 'existing'],
45+
},
46+
},
47+
},
48+
},
49+
],
50+
messages: {
51+
providerTypeMismatch: 'Provider is not of type {{ preferred }}',
52+
},
53+
},
54+
defaultOptions,
55+
create(context) {
56+
const options = context.options[0] || defaultOptions[0];
57+
const preferredTypes = options.prefer;
58+
const providerTypesImported: string[] = [];
59+
return {
60+
'ImportDeclaration[source.value="@nestjs/common"]': (
61+
node: TSESTree.ImportDeclaration
62+
) => {
63+
const specifiers = node.specifiers;
64+
65+
const isImportSpecifier = (
66+
node: TSESTree.ImportClause
67+
): node is TSESTree.ImportSpecifier =>
68+
node.type === AST_NODE_TYPES.ImportSpecifier;
69+
70+
const isProviderImport = (spec: TSESTree.ImportSpecifier) =>
71+
[
72+
'Provider',
73+
'ClassProvider',
74+
'FactoryProvider',
75+
'ValueProvider',
76+
].includes(spec.imported.name);
77+
78+
specifiers
79+
.filter(isImportSpecifier)
80+
.filter(isProviderImport)
81+
.forEach((spec) =>
82+
providerTypesImported.push(spec.local.name ?? spec.imported.name)
83+
);
84+
},
85+
86+
'Property[key.name="providers"] > ArrayExpression > ObjectExpression': (
87+
node: TSESTree.ObjectExpression
88+
) => {
89+
for (const property of node.properties) {
90+
if (property.type === AST_NODE_TYPES.Property) {
91+
const providerType = providerTypeOfProperty(property);
92+
93+
if (
94+
providerType &&
95+
!preferredTypes.includes(providerType) &&
96+
preferredTypes.length > 0
97+
) {
98+
context.report({
99+
node: property,
100+
messageId: 'providerTypeMismatch',
101+
data: {
102+
preferred: preferredTypes,
103+
},
104+
});
105+
}
106+
}
107+
}
108+
},
109+
110+
'Identifier[typeAnnotation.typeAnnotation.type="TSTypeReference"]': (
111+
node: TSESTree.Identifier
112+
) => {
113+
const typeName = (
114+
node.typeAnnotation?.typeAnnotation as TSESTree.TSTypeReference
115+
).typeName;
116+
117+
if (
118+
ASTUtils.isIdentifier(typeName) &&
119+
providerTypesImported.includes(typeName.name) &&
120+
preferredTypes.length > 0
121+
) {
122+
const providerType = providerTypeOfIdentifier(node);
123+
if (providerType && !preferredTypes.includes(providerType)) {
124+
context.report({
125+
node,
126+
messageId: 'providerTypeMismatch',
127+
data: {
128+
preferred: preferredTypes,
129+
},
130+
});
131+
}
132+
}
133+
},
134+
};
135+
},
136+
});
137+
138+
function providerTypeOfIdentifier(
139+
node: TSESTree.Identifier
140+
): ProviderType | undefined {
141+
const parent = node.parent;
142+
143+
if (ASTUtils.isVariableDeclarator(parent)) {
144+
const init = parent.init;
145+
let type: ProviderType | undefined;
146+
if (init?.type === AST_NODE_TYPES.ObjectExpression) {
147+
const properties = init.properties;
148+
for (const property of properties) {
149+
if (property.type === AST_NODE_TYPES.Property) {
150+
type = providerTypeOfProperty(property);
151+
}
152+
}
153+
}
154+
155+
return type;
156+
}
157+
}
158+
159+
function providerTypeOfProperty(
160+
node: TSESTree.Property
161+
): ProviderType | undefined {
162+
const propertyKey = (node.key as TSESTree.Identifier)?.name;
163+
return propertyKey === 'useClass'
164+
? 'class'
165+
: propertyKey === 'useFactory'
166+
? 'factory'
167+
: propertyKey === 'useValue'
168+
? 'value'
169+
: propertyKey === 'useExisting'
170+
? 'existing'
171+
: undefined;
172+
};

0 commit comments

Comments
 (0)