Skip to content

Commit 656816f

Browse files
mohammedzamakhanmgechev
authored andcommitted
feat(rule): add use-injectable-provided-in (#814)
1 parent 3b82574 commit 656816f

File tree

5 files changed

+89
-2
lines changed

5 files changed

+89
-2
lines changed

src/angular/metadata.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,5 +55,5 @@ export class ModuleMetadata {
5555
}
5656

5757
export class InjectableMetadata {
58-
constructor(readonly controller: ts.ClassDeclaration, readonly decorator: ts.Decorator) {}
58+
constructor(readonly controller: ts.ClassDeclaration, readonly decorator: ts.Decorator, readonly providedIn?: string | ts.Expression) {}
5959
}

src/angular/metadataReader.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,9 @@ export class MetadataReader {
107107
}
108108

109109
protected readInjectableMetadata(d: ts.ClassDeclaration, dec: ts.Decorator): DirectiveMetadata {
110-
return new InjectableMetadata(d, dec);
110+
const providedInExpression = getDecoratorPropertyInitializer(dec, 'providedIn');
111+
112+
return new InjectableMetadata(d, dec, providedInExpression);
111113
}
112114

113115
protected readComponentMetadata(d: ts.ClassDeclaration, dec: ts.Decorator): ComponentMetadata {

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export { Rule as TemplateNoNegatedAsyncRule } from './templateNoNegatedAsyncRule
4747
export { Rule as TemplateUseTrackByFunctionRule } from './templateUseTrackByFunctionRule';
4848
export { Rule as UseComponentSelectorRule } from './useComponentSelectorRule';
4949
export { Rule as UseComponentViewEncapsulationRule } from './useComponentViewEncapsulationRule';
50+
export { Rule as UseInjectableProvidedInRule } from './useInjectableProvidedInRule';
5051
export { Rule as UseLifecycleInterfaceRule } from './useLifecycleInterfaceRule';
5152
export { Rule as UsePipeDecoratorRule } from './usePipeDecoratorRule';
5253
export { Rule as UsePipeTransformInterfaceRule } from './usePipeTransformInterfaceRule';

src/useInjectableProvidedInRule.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { IRuleMetadata, RuleFailure } from 'tslint';
2+
import { AbstractRule } from 'tslint/lib/rules';
3+
import { SourceFile } from 'typescript';
4+
import { InjectableMetadata } from './angular';
5+
import { NgWalker } from './angular/ngWalker';
6+
7+
export class Rule extends AbstractRule {
8+
static readonly metadata: IRuleMetadata = {
9+
description: "Enforces classes decorated with @Injectable to use the 'providedIn' property.",
10+
options: null,
11+
optionsDescription: 'Not configurable.',
12+
rationale: "Using the 'providedIn' property makes classes decorated with @Injectable tree shakeable.",
13+
ruleName: 'use-injectable-provided-in',
14+
type: 'functionality',
15+
typescriptOnly: true
16+
};
17+
18+
static readonly FAILURE_STRING = "Classes decorated with @Injectable should use the 'providedIn' property";
19+
20+
apply(sourceFile: SourceFile): RuleFailure[] {
21+
const walker = new Walker(sourceFile, this.getOptions());
22+
23+
return this.applyWithWalker(walker);
24+
}
25+
}
26+
27+
class Walker extends NgWalker {
28+
protected visitNgInjectable(metadata: InjectableMetadata): void {
29+
this.validateInjectable(metadata);
30+
super.visitNgInjectable(metadata);
31+
}
32+
33+
private validateInjectable(metadata: InjectableMetadata): void {
34+
if (metadata.providedIn) return;
35+
36+
this.addFailureAtNode(metadata.decorator, Rule.FAILURE_STRING);
37+
}
38+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { Rule } from '../src/useInjectableProvidedInRule';
2+
import { assertAnnotated, assertSuccess } from './testHelper';
3+
4+
const {
5+
metadata: { ruleName },
6+
FAILURE_STRING
7+
} = Rule;
8+
9+
describe(ruleName, () => {
10+
describe('failures', () => {
11+
it('should fail if providedIn property is not set', () => {
12+
const source = `
13+
@Injectable()
14+
~~~~~~~~~~~~~
15+
class Test {}
16+
`;
17+
assertAnnotated({
18+
message: FAILURE_STRING,
19+
ruleName,
20+
source
21+
});
22+
});
23+
});
24+
25+
describe('success', () => {
26+
it('should succeed if providedIn property is set to a literal string', () => {
27+
const source = `
28+
@Injectable({
29+
providedIn: 'root'
30+
})
31+
class Test {}
32+
`;
33+
assertSuccess(ruleName, source);
34+
});
35+
36+
it('should succeed if providedIn property is set to a module', () => {
37+
const source = `
38+
@Injectable({
39+
providedIn: SomeModule
40+
})
41+
class Test {}
42+
`;
43+
assertSuccess(ruleName, source);
44+
});
45+
});
46+
});

0 commit comments

Comments
 (0)