Skip to content

Commit cedfa2e

Browse files
wKozamgechev
authored andcommitted
fix(rule): handle NgModule with both contextual-lifecycle and contextual-decorators rules (#790)
1 parent 5629397 commit cedfa2e

File tree

4 files changed

+331
-4
lines changed

4 files changed

+331
-4
lines changed

src/contextualLifecycleRule.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
MetadataTypeKeys,
1515
MetadataTypes
1616
} from './util/utils';
17-
import { InjectableMetadata, PipeMetadata } from './angular';
17+
import { InjectableMetadata, ModuleMetadata, PipeMetadata } from './angular';
1818

1919
interface FailureParameters {
2020
readonly className: string;
@@ -56,6 +56,11 @@ class ContextualLifecycleWalker extends NgWalker {
5656
super.visitNgPipe(metadata);
5757
}
5858

59+
protected visitNgModule(metadata: ModuleMetadata) {
60+
this.validateDecorator(metadata, METADATA_TYPE_LIFECYCLE_MAPPER.NgModule);
61+
super.visitNgModule(metadata);
62+
}
63+
5964
private validateDecorator(metadata: PipeMetadata, allowedMethods: ReadonlySet<LifecycleMethodKeys>): void {
6065
const className = getClassName(metadata.controller)!;
6166

src/util/utils.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ export enum MetadataTypes {
6161
Component = 'Component',
6262
Directive = 'Directive',
6363
Injectable = 'Injectable',
64-
Pipe = 'Pipe'
64+
Pipe = 'Pipe',
65+
NgModule = 'NgModule'
6566
}
6667

6768
export type DecoratorKeys = keyof typeof Decorators;
@@ -84,13 +85,15 @@ export const METADATA_TYPE_DECORATOR_MAPPER: MetadataTypeDecoratorMapper = {
8485
Component: DECORATORS,
8586
Directive: DECORATORS,
8687
Injectable: new Set<DecoratorKeys>([]),
87-
Pipe: new Set<DecoratorKeys>([])
88+
Pipe: new Set<DecoratorKeys>([]),
89+
NgModule: new Set<DecoratorKeys>([])
8890
};
8991
export const METADATA_TYPE_LIFECYCLE_MAPPER: MetadataTypeLifecycleMapper = {
9092
Component: LIFECYCLE_METHODS,
9193
Directive: LIFECYCLE_METHODS,
9294
Injectable: new Set<LifecycleMethodKeys>([LifecycleMethods.ngOnDestroy]),
93-
Pipe: new Set<LifecycleMethodKeys>([LifecycleMethods.ngOnDestroy])
95+
Pipe: new Set<LifecycleMethodKeys>([LifecycleMethods.ngOnDestroy]),
96+
NgModule: new Set<LifecycleMethodKeys>([])
9497
};
9598

9699
export const getClassName = (node: Node): string | undefined => {

test/contextualDecoratorRule.spec.ts

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,163 @@ describe(ruleName, () => {
165165
});
166166
});
167167

168+
describe('NgModule', () => {
169+
it('should fail if a property is decorated with @ContentChild() decorator', () => {
170+
const source = `
171+
@NgModule()
172+
class Test {
173+
@ContentChild(Pane) pane: Pane;
174+
~~~~~~~~~~~~~~~~~~~
175+
}
176+
`;
177+
assertAnnotated({
178+
message: getFailureMessage({
179+
className: 'Test',
180+
decoratorName: Decorators.ContentChild,
181+
metadataType: MetadataTypes.NgModule
182+
}),
183+
ruleName,
184+
source
185+
});
186+
});
187+
188+
it('should fail if a property is decorated with @ContentChildren() decorator', () => {
189+
const source = `
190+
@NgModule()
191+
class Test {
192+
@ContentChildren(Pane, { descendants: true }) arbitraryNestedPanes: QueryList<Pane>;
193+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
194+
}
195+
`;
196+
assertAnnotated({
197+
message: getFailureMessage({
198+
className: 'Test',
199+
decoratorName: Decorators.ContentChildren,
200+
metadataType: MetadataTypes.NgModule
201+
}),
202+
ruleName,
203+
source
204+
});
205+
});
206+
207+
it('should fail if a property is decorated with @HostBinding() decorator', () => {
208+
const source = `
209+
@NgModule()
210+
class Test {
211+
@HostBinding('class.card-outline') private isCardOutline: boolean;
212+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
213+
}
214+
`;
215+
assertAnnotated({
216+
message: getFailureMessage({
217+
className: 'Test',
218+
decoratorName: Decorators.HostBinding,
219+
metadataType: MetadataTypes.NgModule
220+
}),
221+
ruleName,
222+
source
223+
});
224+
});
225+
226+
it('should fail if a method is decorated with @HostListener() decorator', () => {
227+
const source = `
228+
@NgModule()
229+
class Test {
230+
@HostListener('mouseover')
231+
~~~~~~~~~~~~~~~~~~~~~~~~~~
232+
mouseOver() {
233+
console.log('mouseOver');
234+
}
235+
}
236+
`;
237+
assertAnnotated({
238+
message: getFailureMessage({
239+
className: 'Test',
240+
decoratorName: Decorators.HostListener,
241+
metadataType: MetadataTypes.NgModule
242+
}),
243+
ruleName,
244+
source
245+
});
246+
});
247+
248+
it('should fail if a property is decorated with @Input() decorator', () => {
249+
const source = `
250+
@NgModule()
251+
class Test {
252+
@Input() label: string;
253+
~~~~~~~~
254+
}
255+
`;
256+
assertAnnotated({
257+
message: getFailureMessage({
258+
className: 'Test',
259+
decoratorName: Decorators.Input,
260+
metadataType: MetadataTypes.NgModule
261+
}),
262+
ruleName,
263+
source
264+
});
265+
});
266+
267+
it('should fail if a property is decorated with @Output() decorator', () => {
268+
const source = `
269+
@NgModule()
270+
class Test {
271+
@Output() emitter = new EventEmitter<void>();
272+
~~~~~~~~~
273+
}
274+
`;
275+
assertAnnotated({
276+
message: getFailureMessage({
277+
className: 'Test',
278+
decoratorName: Decorators.Output,
279+
metadataType: MetadataTypes.NgModule
280+
}),
281+
ruleName,
282+
source
283+
});
284+
});
285+
286+
it('should fail if a property is decorated with @ViewChild() decorator', () => {
287+
const source = `
288+
@NgModule()
289+
class Test {
290+
@ViewChild(Pane) pane: Pane;
291+
~~~~~~~~~~~~~~~~
292+
}
293+
`;
294+
assertAnnotated({
295+
message: getFailureMessage({
296+
className: 'Test',
297+
decoratorName: Decorators.ViewChild,
298+
metadataType: MetadataTypes.NgModule
299+
}),
300+
ruleName,
301+
source
302+
});
303+
});
304+
305+
it('should fail if a property is decorated with @ViewChildren() decorator', () => {
306+
const source = `
307+
@NgModule()
308+
class Test {
309+
@ViewChildren(Pane) panes: QueryList<Pane>;
310+
~~~~~~~~~~~~~~~~~~~
311+
}
312+
`;
313+
assertAnnotated({
314+
message: getFailureMessage({
315+
className: 'Test',
316+
decoratorName: Decorators.ViewChildren,
317+
metadataType: MetadataTypes.NgModule
318+
}),
319+
ruleName,
320+
source
321+
});
322+
});
323+
});
324+
168325
describe('Pipe', () => {
169326
it('should fail if a property is decorated with @ContentChild() decorator', () => {
170327
const source = `

test/contextualLifecycleRule.spec.ts

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,168 @@ describe(ruleName, () => {
150150
});
151151
});
152152

153+
describe('NgModule', () => {
154+
it('should fail if ngAfterContentChecked() method is present', () => {
155+
const source = `
156+
@NgModule()
157+
class Test {
158+
ngAfterContentChecked() { console.log('AfterContentChecked'); }
159+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
160+
}
161+
`;
162+
const message = getFailureMessage({
163+
className: 'Test',
164+
metadataType: MetadataTypes.NgModule,
165+
methodName: LifecycleMethods.ngAfterContentChecked
166+
});
167+
assertAnnotated({
168+
message,
169+
ruleName,
170+
source
171+
});
172+
});
173+
174+
it('should fail if ngAfterContentInit() method is present', () => {
175+
const source = `
176+
@NgModule()
177+
class Test {
178+
ngAfterContentInit() { console.log('AfterContentInit'); }
179+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
180+
}
181+
`;
182+
const message = getFailureMessage({
183+
className: 'Test',
184+
metadataType: MetadataTypes.NgModule,
185+
methodName: LifecycleMethods.ngAfterContentInit
186+
});
187+
assertAnnotated({
188+
message,
189+
ruleName,
190+
source
191+
});
192+
});
193+
194+
it('should fail if ngAfterViewChecked() method is present', () => {
195+
const source = `
196+
@NgModule()
197+
class Test {
198+
ngAfterViewChecked() { console.log('AfterViewChecked'); }
199+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
200+
}
201+
`;
202+
const message = getFailureMessage({
203+
className: 'Test',
204+
metadataType: MetadataTypes.NgModule,
205+
methodName: LifecycleMethods.ngAfterViewChecked
206+
});
207+
assertAnnotated({
208+
message,
209+
ruleName,
210+
source
211+
});
212+
});
213+
214+
it('should fail if ngAfterViewInit() method is present', () => {
215+
const source = `
216+
@NgModule()
217+
class Test {
218+
ngAfterViewInit() { console.log('AfterViewInit'); }
219+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
220+
}
221+
`;
222+
const message = getFailureMessage({
223+
className: 'Test',
224+
metadataType: MetadataTypes.NgModule,
225+
methodName: LifecycleMethods.ngAfterViewInit
226+
});
227+
assertAnnotated({
228+
message,
229+
ruleName,
230+
source
231+
});
232+
});
233+
234+
it('should fail if ngDoCheck() method is present', () => {
235+
const source = `
236+
@NgModule()
237+
class Test {
238+
ngDoCheck() { console.log('DoCheck'); }
239+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
240+
}
241+
`;
242+
const message = getFailureMessage({
243+
className: 'Test',
244+
metadataType: MetadataTypes.NgModule,
245+
methodName: LifecycleMethods.ngDoCheck
246+
});
247+
assertAnnotated({
248+
message,
249+
ruleName,
250+
source
251+
});
252+
});
253+
254+
it('should fail if ngOnChanges() method is present', () => {
255+
const source = `
256+
@NgModule()
257+
class Test {
258+
ngOnChanges() { console.log('OnChanges'); }
259+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
260+
}
261+
`;
262+
const message = getFailureMessage({
263+
className: 'Test',
264+
metadataType: MetadataTypes.NgModule,
265+
methodName: LifecycleMethods.ngOnChanges
266+
});
267+
assertAnnotated({
268+
message,
269+
ruleName,
270+
source
271+
});
272+
});
273+
274+
it('should fail if ngOnInit() method is present', () => {
275+
const source = `
276+
@NgModule()
277+
class Test {
278+
ngOnInit() { console.log('OnInit'); }
279+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
280+
}
281+
`;
282+
const message = getFailureMessage({
283+
className: 'Test',
284+
metadataType: MetadataTypes.NgModule,
285+
methodName: LifecycleMethods.ngOnInit
286+
});
287+
assertAnnotated({
288+
message,
289+
ruleName,
290+
source
291+
});
292+
});
293+
294+
it('should fail if ngOnDestroy() method is present', () => {
295+
const source = `
296+
@NgModule()
297+
class Test {
298+
ngOnDestroy() { console.log('OnDestroy'); }
299+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
300+
}
301+
`;
302+
const message = getFailureMessage({
303+
className: 'Test',
304+
metadataType: MetadataTypes.NgModule,
305+
methodName: LifecycleMethods.ngOnDestroy
306+
});
307+
assertAnnotated({
308+
message,
309+
ruleName,
310+
source
311+
});
312+
});
313+
});
314+
153315
describe('Pipe', () => {
154316
it('should fail if ngAfterContentChecked() method is present', () => {
155317
const source = `

0 commit comments

Comments
 (0)