diff --git a/src/contextualDecoratorRule.ts b/src/contextualDecoratorRule.ts index 9497449ce..7a50d56a9 100644 --- a/src/contextualDecoratorRule.ts +++ b/src/contextualDecoratorRule.ts @@ -1,14 +1,27 @@ import { sprintf } from 'sprintf-js'; -import { IRuleMetadata, RuleFailure } from 'tslint'; +import { IRuleMetadata, RuleFailure, WalkContext } from 'tslint'; import { AbstractRule } from 'tslint/lib/rules'; import { dedent } from 'tslint/lib/utils'; -import { createNodeArray, Decorator, isClassDeclaration, SourceFile } from 'typescript'; -import { NgWalker } from './angular/ngWalker'; +import { + AccessorDeclaration, + createNodeArray, + Decorator, + forEachChild, + isAccessor, + isMethodDeclaration, + isParameterPropertyDeclaration, + isPropertyDeclaration, + MethodDeclaration, + Node, + ParameterPropertyDeclaration, + PropertyDeclaration, + SourceFile +} from 'typescript'; +import { isNotNullOrUndefined } from './util/isNotNullOrUndefined'; import { ANGULAR_CLASS_DECORATOR_MAPPER, AngularClassDecoratorKeys, AngularClassDecorators, - AngularInnerClassDecoratorKeys, AngularInnerClassDecorators, getDecoratorName, getNextToLastParentNode, @@ -17,77 +30,83 @@ import { } from './util/utils'; interface FailureParameters { - readonly className: string; readonly classDecoratorName: AngularClassDecoratorKeys; - readonly innerClassDecoratorName: AngularInnerClassDecoratorKeys; } -export const getFailureMessage = (failureParameters: FailureParameters): string => - sprintf( - Rule.FAILURE_STRING, - failureParameters.innerClassDecoratorName, - failureParameters.className, - failureParameters.classDecoratorName - ); +type DeclarationLike = AccessorDeclaration | MethodDeclaration | ParameterPropertyDeclaration | PropertyDeclaration; + +export const getFailureMessage = (failureParameters: FailureParameters): string => { + return sprintf(Rule.FAILURE_STRING, failureParameters.classDecoratorName); +}; export class Rule extends AbstractRule { static readonly metadata: IRuleMetadata = { - description: 'Ensures that classes use allowed decorator in its body.', + description: 'Ensures that classes use contextual decorators in its body.', options: null, optionsDescription: 'Not configurable.', rationale: dedent` - Some decorators can only be used in certain class types. - For example, an @${AngularInnerClassDecorators.Input} should not be used - in an @${AngularClassDecorators.Injectable} class. + Some decorators should only be used in certain class types. For example, + the decorator @${AngularInnerClassDecorators.Input}() should + not be used in a class decorated with @${AngularClassDecorators.Injectable}(). `, ruleName: 'contextual-decorator', type: 'functionality', typescriptOnly: true }; - static readonly FAILURE_STRING = 'The decorator "%s" is not allowed for class "%s" because it is decorated with "%s"'; + static readonly FAILURE_STRING = 'Decorator out of context for "@%s()"'; apply(sourceFile: SourceFile): RuleFailure[] { - const walker = new Walker(sourceFile, this.getOptions()); - - return this.applyWithWalker(walker); + return this.applyWithFunction(sourceFile, walk); } } -class Walker extends NgWalker { - protected visitMethodDecorator(decorator: Decorator): void { - this.validateDecorator(decorator); - super.visitMethodDecorator(decorator); - } +const callbackHandler = (walkContext: WalkContext, node: Node): void => { + if (isDeclarationLike(node)) validateDeclaration(walkContext, node); +}; - protected visitPropertyDecorator(decorator: Decorator): void { - this.validateDecorator(decorator); - super.visitPropertyDecorator(decorator); - } +const getClassDecoratorName = (klass: Node): AngularClassDecoratorKeys | undefined => { + return createNodeArray(klass.decorators) + .map(getDecoratorName) + .filter(isNotNullOrUndefined) + .find(isAngularClassDecorator); +}; - private validateDecorator(decorator: Decorator): void { - const klass = getNextToLastParentNode(decorator); +const isDeclarationLike = (node: Node): node is DeclarationLike => { + return isAccessor(node) || isMethodDeclaration(node) || isParameterPropertyDeclaration(node) || isPropertyDeclaration(node); +}; - if (!isClassDeclaration(klass) || !klass.name) return; +const validateDeclaration = (walkContext: WalkContext, node: DeclarationLike): void => { + const klass = getNextToLastParentNode(node); + const classDecoratorName = getClassDecoratorName(klass); - const classDecoratorName = createNodeArray(klass.decorators) - .map(x => x.expression.getText()) - .map(x => x.replace(/[^a-zA-Z]/g, '')) - .find(isAngularClassDecorator); + if (!classDecoratorName) return; - if (!classDecoratorName) return; + createNodeArray(node.decorators).forEach(decorator => validateDecorator(walkContext, decorator, classDecoratorName)); +}; - const innerClassDecoratorName = getDecoratorName(decorator); +const validateDecorator = (walkContext: WalkContext, node: Decorator, classDecoratorName: AngularClassDecoratorKeys): void => { + const decoratorName = getDecoratorName(node); - if (!innerClassDecoratorName || !isAngularInnerClassDecorator(innerClassDecoratorName)) return; + if (!decoratorName || !isAngularInnerClassDecorator(decoratorName)) return; - const allowedDecorators = ANGULAR_CLASS_DECORATOR_MAPPER.get(classDecoratorName); + const allowedDecorators = ANGULAR_CLASS_DECORATOR_MAPPER.get(classDecoratorName); - if (!allowedDecorators || allowedDecorators.has(innerClassDecoratorName)) return; + if (!allowedDecorators || allowedDecorators.has(decoratorName)) return; - const className = klass.name.getText(); - const failure = getFailureMessage({ classDecoratorName, className, innerClassDecoratorName }); + const failure = getFailureMessage({ classDecoratorName }); - this.addFailureAtNode(decorator, failure); - } -} + walkContext.addFailureAtNode(node, failure); +}; + +const walk = (walkContext: WalkContext): void => { + const { sourceFile } = walkContext; + + const callback = (node: Node): void => { + callbackHandler(walkContext, node); + + forEachChild(node, callback); + }; + + forEachChild(sourceFile, callback); +}; diff --git a/test/contextualDecoratorRule.spec.ts b/test/contextualDecoratorRule.spec.ts index 32ab9c798..5f6c36c8f 100644 --- a/test/contextualDecoratorRule.spec.ts +++ b/test/contextualDecoratorRule.spec.ts @@ -1,6 +1,6 @@ import { getFailureMessage, Rule } from '../src/contextualDecoratorRule'; -import { AngularClassDecorators, AngularInnerClassDecorators } from '../src/util/utils'; -import { assertAnnotated, assertSuccess } from './testHelper'; +import { AngularClassDecorators } from '../src/util/utils'; +import { assertAnnotated, assertMultipleAnnotated, assertSuccess } from './testHelper'; const { metadata: { ruleName } @@ -9,472 +9,1033 @@ const { describe(ruleName, () => { describe('failure', () => { describe('Injectable', () => { - it('should fail if a property is decorated with @ContentChild() decorator', () => { - const source = ` - @Injectable() - class Test { - @ContentChild(Pane) pane: Pane; - ~~~~~~~~~~~~~~~~~~~ - } - `; - assertAnnotated({ - message: getFailureMessage({ - classDecoratorName: AngularClassDecorators.Injectable, - className: 'Test', - innerClassDecoratorName: AngularInnerClassDecorators.ContentChild - }), - ruleName, - source + describe('accessors', () => { + it('should fail if getter accessor is decorated with @Input() decorator', () => { + const source = ` + @Injectable({ + providedIn: 'root' + }) + class Test { + @Input() + ~~~~~~~~ + get label(): string { + return this._label; + } + set label(value: string) { + this._label = value; + } + private _label: string; + } + `; + assertAnnotated({ + message: getFailureMessage({ + classDecoratorName: AngularClassDecorators.Injectable + }), + ruleName, + source + }); }); - }); - it('should fail if a property is decorated with @ContentChildren() decorator', () => { - const source = ` - @Injectable() - class Test { - @ContentChildren(Pane, { descendants: true }) arbitraryNestedPanes: QueryList; - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - } - `; - assertAnnotated({ - message: getFailureMessage({ - classDecoratorName: AngularClassDecorators.Injectable, - className: 'Test', - innerClassDecoratorName: AngularInnerClassDecorators.ContentChildren - }), - ruleName, - source + it('should fail if setter accessor is decorated with @Input() decorator', () => { + const source = ` + @Injectable({ + providedIn: 'root' + }) + class Test { + @Input() + ~~~~~~~~ + set label(value: string) { + this._label = value; + } + get label(): string { + return this._label; + } + private _label: string; + } + `; + assertAnnotated({ + message: getFailureMessage({ + classDecoratorName: AngularClassDecorators.Injectable + }), + ruleName, + source + }); }); - }); - it('should fail if a property is decorated with @HostBinding() decorator', () => { - const source = ` - @Injectable() - class Test { - @HostBinding('class.card-outline') private isCardOutline: boolean; - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - } - `; - assertAnnotated({ - message: getFailureMessage({ - classDecoratorName: AngularClassDecorators.Injectable, - className: 'Test', - innerClassDecoratorName: AngularInnerClassDecorators.HostBinding - }), - ruleName, - source + it('should fail if setter accessor is decorated with @ViewChild() decorator', () => { + const source = ` + @Injectable({ + providedIn: 'root' + }) + class Test { + @ViewChild(Pane) + ~~~~~~~~~~~~~~~~ + set label(value: Pane) { + doSomething(); + } + } + `; + assertAnnotated({ + message: getFailureMessage({ + classDecoratorName: AngularClassDecorators.Injectable + }), + ruleName, + source + }); }); }); - it('should fail if a method is decorated with @HostListener() decorator', () => { - const source = ` - @Injectable() - class Test { - @HostListener('mouseover') - ~~~~~~~~~~~~~~~~~~~~~~~~~~ - mouseOver() { - console.log('mouseOver'); + describe('methods', () => { + it('should fail if a method is decorated with @HostListener() decorator', () => { + const source = ` + @Injectable({ + providedIn: 'root' + }) + class Test { + @HostListener('mouseover') + ~~~~~~~~~~~~~~~~~~~~~~~~~~ + mouseOver() { + this.doSomething(); + } } - } - `; - assertAnnotated({ - message: getFailureMessage({ - classDecoratorName: AngularClassDecorators.Injectable, - className: 'Test', - innerClassDecoratorName: AngularInnerClassDecorators.HostListener - }), - ruleName, - source + `; + assertAnnotated({ + message: getFailureMessage({ + classDecoratorName: AngularClassDecorators.Injectable + }), + ruleName, + source + }); }); }); - it('should fail if a property is decorated with @Input() decorator', () => { - const source = ` - @Injectable() - class Test { - @Input() label: string; - ~~~~~~~~ - } - `; - assertAnnotated({ - message: getFailureMessage({ - classDecoratorName: AngularClassDecorators.Injectable, - className: 'Test', - innerClassDecoratorName: AngularInnerClassDecorators.Input - }), - ruleName, - source + describe('parameter properties', () => { + it('should fail if a parameter property is decorated with @Attribute() decorator', () => { + const source = ` + @Injectable({ + providedIn: 'root' + }) + class Test { + constructor( + @Attribute('test') private readonly test: string + ~~~~~~~~~~~~~~~~~~ + ) {} + } + `; + assertAnnotated({ + message: getFailureMessage({ + classDecoratorName: AngularClassDecorators.Injectable + }), + ruleName, + source + }); }); }); - it('should fail if a property is decorated with @Output() decorator', () => { - const source = ` - @Injectable() - class Test { - @Output() emitter = new EventEmitter(); - ~~~~~~~~~ - } - `; - assertAnnotated({ - message: getFailureMessage({ - classDecoratorName: AngularClassDecorators.Injectable, - className: 'Test', - innerClassDecoratorName: AngularInnerClassDecorators.Output - }), - ruleName, - source + describe('properties', () => { + it('should fail if a property is decorated with @ContentChild() decorator', () => { + const source = ` + @Injectable({ + providedIn: 'root' + }) + class Test { + @ContentChild(Pane) pane: Pane; + ~~~~~~~~~~~~~~~~~~~ + } + `; + assertAnnotated({ + message: getFailureMessage({ + classDecoratorName: AngularClassDecorators.Injectable + }), + ruleName, + source + }); }); - }); - it('should fail if a property is decorated with @ViewChild() decorator', () => { - const source = ` - @Injectable() - class Test { - @ViewChild(Pane) pane: Pane; - ~~~~~~~~~~~~~~~~ - } - `; - assertAnnotated({ - message: getFailureMessage({ - classDecoratorName: AngularClassDecorators.Injectable, - className: 'Test', - innerClassDecoratorName: AngularInnerClassDecorators.ViewChild - }), - ruleName, - source + it('should fail if a property is decorated with @ContentChildren() decorator', () => { + const source = ` + @Injectable({ + providedIn: 'root' + }) + class Test { + @ContentChildren(Pane, { descendants: true }) arbitraryNestedPanes: QueryList; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + } + `; + assertAnnotated({ + message: getFailureMessage({ + classDecoratorName: AngularClassDecorators.Injectable + }), + ruleName, + source + }); }); - }); - it('should fail if a property is decorated with @ViewChildren() decorator', () => { - const source = ` - @Injectable() - class Test { - @ViewChildren(Pane) panes: QueryList; - ~~~~~~~~~~~~~~~~~~~ - } - `; - assertAnnotated({ - message: getFailureMessage({ - classDecoratorName: AngularClassDecorators.Injectable, - className: 'Test', - innerClassDecoratorName: AngularInnerClassDecorators.ViewChildren - }), - ruleName, - source + it('should fail if a property is decorated with @HostBinding() decorator', () => { + const source = ` + @Injectable({ + providedIn: 'root' + }) + class Test { + @HostBinding('class.card-outline') private isCardOutline: boolean; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + } + `; + assertAnnotated({ + message: getFailureMessage({ + classDecoratorName: AngularClassDecorators.Injectable + }), + ruleName, + source + }); }); - }); - }); - describe('NgModule', () => { - it('should fail if a property is decorated with @ContentChild() decorator', () => { - const source = ` - @NgModule() - class Test { - @ContentChild(Pane) pane: Pane; - ~~~~~~~~~~~~~~~~~~~ - } - `; - assertAnnotated({ - message: getFailureMessage({ - classDecoratorName: AngularClassDecorators.NgModule, - className: 'Test', - innerClassDecoratorName: AngularInnerClassDecorators.ContentChild - }), - ruleName, - source + it('should fail if a property is decorated with @Input() decorator', () => { + const source = ` + @Injectable({ + providedIn: 'root' + }) + class Test { + @Input() label: string; + ~~~~~~~~ + } + `; + assertAnnotated({ + message: getFailureMessage({ + classDecoratorName: AngularClassDecorators.Injectable + }), + ruleName, + source + }); }); - }); - it('should fail if a property is decorated with @ContentChildren() decorator', () => { - const source = ` - @NgModule() - class Test { - @ContentChildren(Pane, { descendants: true }) arbitraryNestedPanes: QueryList; - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - } - `; - assertAnnotated({ - message: getFailureMessage({ - classDecoratorName: AngularClassDecorators.NgModule, - className: 'Test', - innerClassDecoratorName: AngularInnerClassDecorators.ContentChildren - }), - ruleName, - source + it('should fail if a property is decorated with @Output() decorator', () => { + const source = ` + @Injectable({ + providedIn: 'root' + }) + class Test { + @Output() emitter = new EventEmitter(); + ~~~~~~~~~ + } + `; + assertAnnotated({ + message: getFailureMessage({ + classDecoratorName: AngularClassDecorators.Injectable + }), + ruleName, + source + }); }); - }); - it('should fail if a property is decorated with @HostBinding() decorator', () => { - const source = ` - @NgModule() - class Test { - @HostBinding('class.card-outline') private isCardOutline: boolean; - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - } - `; - assertAnnotated({ - message: getFailureMessage({ - classDecoratorName: AngularClassDecorators.NgModule, - className: 'Test', - innerClassDecoratorName: AngularInnerClassDecorators.HostBinding - }), - ruleName, - source + it('should fail if a property is decorated with @ViewChild() decorator', () => { + const source = ` + @Injectable({ + providedIn: 'root' + }) + class Test { + @ViewChild(Pane) pane: Pane; + ~~~~~~~~~~~~~~~~ + } + `; + assertAnnotated({ + message: getFailureMessage({ + classDecoratorName: AngularClassDecorators.Injectable + }), + ruleName, + source + }); }); - }); - it('should fail if a method is decorated with @HostListener() decorator', () => { - const source = ` - @NgModule() - class Test { - @HostListener('mouseover') - ~~~~~~~~~~~~~~~~~~~~~~~~~~ - mouseOver() { - console.log('mouseOver'); + it('should fail if a property is decorated with @ViewChildren() decorator', () => { + const source = ` + @Injectable({ + providedIn: 'root' + }) + class Test { + @ViewChildren(Pane) panes: QueryList; + ~~~~~~~~~~~~~~~~~~~ } - } - `; - assertAnnotated({ - message: getFailureMessage({ - classDecoratorName: AngularClassDecorators.NgModule, - className: 'Test', - innerClassDecoratorName: AngularInnerClassDecorators.HostListener - }), - ruleName, - source + `; + assertAnnotated({ + message: getFailureMessage({ + classDecoratorName: AngularClassDecorators.Injectable + }), + ruleName, + source + }); }); }); - it('should fail if a property is decorated with @Input() decorator', () => { - const source = ` - @NgModule() - class Test { - @Input() label: string; - ~~~~~~~~ - } - `; - assertAnnotated({ - message: getFailureMessage({ - classDecoratorName: AngularClassDecorators.NgModule, - className: 'Test', - innerClassDecoratorName: AngularInnerClassDecorators.Input - }), - ruleName, - source - }); - }); + describe('multiple declarations', () => { + it('should fail if declarations are decorated with non allowed decorator', () => { + const source = ` + @Injectable({ + providedIn: 'root' + }) + class Test { + @ContentChild(Pane) pane: Pane; + ~~~~~~~~~~~~~~~~~~~ - it('should fail if a property is decorated with @Output() decorator', () => { - const source = ` - @NgModule() - class Test { - @Output() emitter = new EventEmitter(); - ~~~~~~~~~ - } - `; - assertAnnotated({ - message: getFailureMessage({ - classDecoratorName: AngularClassDecorators.NgModule, - className: 'Test', - innerClassDecoratorName: AngularInnerClassDecorators.Output - }), - ruleName, - source - }); - }); + @Input() + ^^^^^^^^ + get label(): string { + return this._label; + } + set label(value: string) { + this._label = value; + } + private _label: string; - it('should fail if a property is decorated with @ViewChild() decorator', () => { - const source = ` - @NgModule() - class Test { - @ViewChild(Pane) pane: Pane; - ~~~~~~~~~~~~~~~~ - } - `; - assertAnnotated({ - message: getFailureMessage({ - classDecoratorName: AngularClassDecorators.NgModule, - className: 'Test', - innerClassDecoratorName: AngularInnerClassDecorators.ViewChild - }), - ruleName, - source - }); - }); + @ViewChild(Pane) + ################ + set label(value: Pane) { + doSomething(); + } - it('should fail if a property is decorated with @ViewChildren() decorator', () => { - const source = ` - @NgModule() - class Test { - @ViewChildren(Pane) panes: QueryList; - ~~~~~~~~~~~~~~~~~~~ - } - `; - assertAnnotated({ - message: getFailureMessage({ - classDecoratorName: AngularClassDecorators.NgModule, - className: 'Test', - innerClassDecoratorName: AngularInnerClassDecorators.ViewChildren - }), - ruleName, - source + get type(): string { + return this._type; + } + set type(value: string) { + this._type = value; + } + private _type: string; + + private prop: string | undefined; + + constructor( + @Attribute('test') private readonly test: string, + %%%%%%%%%%%%%%%%%% + @Inject(LOCALE_ID) localeId: string + ) {} + + @HostListener('mouseover') + ¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶ + mouseOver() { + this.doSomething(); + } + + clickHandler(): void {} + } + `; + const msg = getFailureMessage({ + classDecoratorName: AngularClassDecorators.Injectable + }); + const failures = [ + { + char: '~', + msg + }, + { + char: '^', + msg + }, + { + char: '#', + msg + }, + { + char: '%', + msg + }, + { + char: '¶', + msg + } + ]; + assertMultipleAnnotated({ + failures, + ruleName, + source + }); }); }); }); - describe('Pipe', () => { - it('should fail if a property is decorated with @ContentChild() decorator', () => { - const source = ` - @Pipe() - class Test { - @ContentChild(Pane) pane: Pane; - ~~~~~~~~~~~~~~~~~~~ - } - `; - assertAnnotated({ - message: getFailureMessage({ - classDecoratorName: AngularClassDecorators.Pipe, - className: 'Test', - innerClassDecoratorName: AngularInnerClassDecorators.ContentChild - }), - ruleName, - source + describe('NgModule', () => { + describe('accessors', () => { + it('should fail if getter accessor is decorated with @Input() decorator', () => { + const source = ` + @NgModule({ + providers: [] + }) + class Test { + @Input() + ~~~~~~~~ + get label(): string { + return this._label; + } + set label(value: string) { + this._label = value; + } + private _label: string; + } + `; + assertAnnotated({ + message: getFailureMessage({ + classDecoratorName: AngularClassDecorators.NgModule + }), + ruleName, + source + }); }); - }); - it('should fail if a property is decorated with @ContentChildren() decorator', () => { - const source = ` - @Pipe() - class Test { - @ContentChildren(Pane, { descendants: true }) arbitraryNestedPanes: QueryList; - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - } - `; - assertAnnotated({ - message: getFailureMessage({ - classDecoratorName: AngularClassDecorators.Pipe, - className: 'Test', - innerClassDecoratorName: AngularInnerClassDecorators.ContentChildren - }), - ruleName, - source + it('should fail if setter accessor is decorated with @Input() decorator', () => { + const source = ` + @NgModule({ + providers: [] + }) + class Test { + @Input() + ~~~~~~~~ + set label(value: string) { + this._label = value; + } + get label(): string { + return this._label; + } + private _label: string; + } + `; + assertAnnotated({ + message: getFailureMessage({ + classDecoratorName: AngularClassDecorators.NgModule + }), + ruleName, + source + }); }); - }); - it('should fail if a property is decorated with @HostBinding() decorator', () => { - const source = ` - @Pipe() - class Test { - @HostBinding('class.card-outline') private isCardOutline: boolean; - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - } - `; - assertAnnotated({ - message: getFailureMessage({ - classDecoratorName: AngularClassDecorators.Pipe, - className: 'Test', - innerClassDecoratorName: AngularInnerClassDecorators.HostBinding - }), - ruleName, - source + it('should fail if setter accessor is decorated with @ViewChild() decorator', () => { + const source = ` + @NgModule({ + providers: [] + }) + class Test { + @ViewChild(Pane) + ~~~~~~~~~~~~~~~~ + set label(value: Pane) { + doSomething(); + } + } + `; + assertAnnotated({ + message: getFailureMessage({ + classDecoratorName: AngularClassDecorators.NgModule + }), + ruleName, + source + }); }); }); - it('should fail if a method is decorated with @HostListener() decorator', () => { - const source = ` - @Pipe() - class Test { - @HostListener('mouseover') - ~~~~~~~~~~~~~~~~~~~~~~~~~~ - mouseOver() { - console.log('mouseOver'); + describe('methods', () => { + it('should fail if a method is decorated with @HostListener() decorator', () => { + const source = ` + @NgModule({ + providers: [] + }) + class Test { + @HostListener('mouseover') + ~~~~~~~~~~~~~~~~~~~~~~~~~~ + mouseOver() { + this.doSomething(); + } } - } - `; - assertAnnotated({ - message: getFailureMessage({ - classDecoratorName: AngularClassDecorators.Pipe, - className: 'Test', - innerClassDecoratorName: AngularInnerClassDecorators.HostListener - }), - ruleName, - source + `; + assertAnnotated({ + message: getFailureMessage({ + classDecoratorName: AngularClassDecorators.NgModule + }), + ruleName, + source + }); }); }); - it('should fail if a property is decorated with @Input() decorator', () => { - const source = ` - @Pipe() - class Test { - @Input() label: string; - ~~~~~~~~ - } - `; - assertAnnotated({ - message: getFailureMessage({ - classDecoratorName: AngularClassDecorators.Pipe, - className: 'Test', - innerClassDecoratorName: AngularInnerClassDecorators.Input - }), - ruleName, - source + describe('parameter properties', () => { + it('should fail if a parameter property is decorated with @Attribute() decorator', () => { + const source = ` + @NgModule({ + providers: [] + }) + class Test { + constructor( + @Attribute('test') private readonly test: string + ~~~~~~~~~~~~~~~~~~ + ) {} + } + `; + assertAnnotated({ + message: getFailureMessage({ + classDecoratorName: AngularClassDecorators.NgModule + }), + ruleName, + source + }); }); }); - it('should fail if a property is decorated with @Output() decorator', () => { - const source = ` - @Pipe() - class Test { - @Output() emitter = new EventEmitter(); - ~~~~~~~~~ - } - `; - assertAnnotated({ - message: getFailureMessage({ - classDecoratorName: AngularClassDecorators.Pipe, - className: 'Test', - innerClassDecoratorName: AngularInnerClassDecorators.Output - }), - ruleName, - source - }); + describe('properties', () => { + it('should fail if a property is decorated with @ContentChild() decorator', () => { + const source = ` + @NgModule({ + providers: [] + }) + class Test { + @ContentChild(Pane) pane: Pane; + ~~~~~~~~~~~~~~~~~~~ + } + `; + assertAnnotated({ + message: getFailureMessage({ + classDecoratorName: AngularClassDecorators.NgModule + }), + ruleName, + source + }); + }); + + it('should fail if a property is decorated with @ContentChildren() decorator', () => { + const source = ` + @NgModule({ + providers: [] + }) + class Test { + @ContentChildren(Pane, { descendants: true }) arbitraryNestedPanes: QueryList; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + } + `; + assertAnnotated({ + message: getFailureMessage({ + classDecoratorName: AngularClassDecorators.NgModule + }), + ruleName, + source + }); + }); + + it('should fail if a property is decorated with @HostBinding() decorator', () => { + const source = ` + @NgModule({ + providers: [] + }) + class Test { + @HostBinding('class.card-outline') private isCardOutline: boolean; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + } + `; + assertAnnotated({ + message: getFailureMessage({ + classDecoratorName: AngularClassDecorators.NgModule + }), + ruleName, + source + }); + }); + + it('should fail if a property is decorated with @Input() decorator', () => { + const source = ` + @NgModule({ + providers: [] + }) + class Test { + @Input() label: string; + ~~~~~~~~ + } + `; + assertAnnotated({ + message: getFailureMessage({ + classDecoratorName: AngularClassDecorators.NgModule + }), + ruleName, + source + }); + }); + + it('should fail if a property is decorated with @Output() decorator', () => { + const source = ` + @NgModule({ + providers: [] + }) + class Test { + @Output() emitter = new EventEmitter(); + ~~~~~~~~~ + } + `; + assertAnnotated({ + message: getFailureMessage({ + classDecoratorName: AngularClassDecorators.NgModule + }), + ruleName, + source + }); + }); + + it('should fail if a property is decorated with @ViewChild() decorator', () => { + const source = ` + @NgModule({ + providers: [] + }) + class Test { + @ViewChild(Pane) pane: Pane; + ~~~~~~~~~~~~~~~~ + } + `; + assertAnnotated({ + message: getFailureMessage({ + classDecoratorName: AngularClassDecorators.NgModule + }), + ruleName, + source + }); + }); + + it('should fail if a property is decorated with @ViewChildren() decorator', () => { + const source = ` + @NgModule({ + providers: [] + }) + class Test { + @ViewChildren(Pane) panes: QueryList; + ~~~~~~~~~~~~~~~~~~~ + } + `; + assertAnnotated({ + message: getFailureMessage({ + classDecoratorName: AngularClassDecorators.NgModule + }), + ruleName, + source + }); + }); }); - it('should fail if a property is decorated with @ViewChild() decorator', () => { - const source = ` - @Pipe() - class Test { - @ViewChild(Pane) pane: Pane; - ~~~~~~~~~~~~~~~~ - } - `; - assertAnnotated({ - message: getFailureMessage({ - classDecoratorName: AngularClassDecorators.Pipe, - className: 'Test', - innerClassDecoratorName: AngularInnerClassDecorators.ViewChild - }), - ruleName, - source + describe('multiple declarations', () => { + it('should fail if declarations are decorated with non allowed decorator', () => { + const source = ` + @NgModule({ + providers: [] + }) + class Test { + @ContentChild(Pane) pane: Pane; + ~~~~~~~~~~~~~~~~~~~ + + @Input() + ^^^^^^^^ + get label(): string { + return this._label; + } + set label(value: string) { + this._label = value; + } + private _label: string; + + @ViewChild(Pane) + ################ + set label(value: Pane) { + doSomething(); + } + + get type(): string { + return this._type; + } + set type(value: string) { + this._type = value; + } + private _type: string; + + private prop: string | undefined; + + constructor( + @Attribute('test') private readonly test: string, + %%%%%%%%%%%%%%%%%% + @Inject(LOCALE_ID) localeId: string + ) {} + + @HostListener('mouseover') + ¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶ + mouseOver() { + this.doSomething(); + } + + clickHandler(): void {} + } + `; + const msg = getFailureMessage({ + classDecoratorName: AngularClassDecorators.NgModule + }); + const failures = [ + { + char: '~', + msg + }, + { + char: '^', + msg + }, + { + char: '#', + msg + }, + { + char: '%', + msg + }, + { + char: '¶', + msg + } + ]; + assertMultipleAnnotated({ + failures, + ruleName, + source + }); }); }); + }); - it('should fail if a property is decorated with @ViewChildren() decorator', () => { - const source = ` - @Pipe() - class Test { - @ViewChildren(Pane) panes: QueryList; - ~~~~~~~~~~~~~~~~~~~ - } - `; - assertAnnotated({ - message: getFailureMessage({ - classDecoratorName: AngularClassDecorators.Pipe, - className: 'Test', - innerClassDecoratorName: AngularInnerClassDecorators.ViewChildren - }), - ruleName, - source + describe('Pipe', () => { + describe('accessors', () => { + it('should fail if getter accessor is decorated with @Input() decorator', () => { + const source = ` + @Pipe({ + name: 'test' + }) + class Test { + @Input() + ~~~~~~~~ + get label(): string { + return this._label; + } + set label(value: string) { + this._label = value; + } + private _label: string; + } + `; + assertAnnotated({ + message: getFailureMessage({ + classDecoratorName: AngularClassDecorators.Pipe + }), + ruleName, + source + }); + }); + + it('should fail if setter accessor is decorated with @Input() decorator', () => { + const source = ` + @Pipe({ + name: 'test' + }) + class Test { + @Input() + ~~~~~~~~ + set label(value: string) { + this._label = value; + } + get label(): string { + return this._label; + } + private _label: string; + } + `; + assertAnnotated({ + message: getFailureMessage({ + classDecoratorName: AngularClassDecorators.Pipe + }), + ruleName, + source + }); + }); + + it('should fail if setter accessor is decorated with @ViewChild() decorator', () => { + const source = ` + @Pipe({ + name: 'test' + }) + class Test { + @ViewChild(Pane) + ~~~~~~~~~~~~~~~~ + set label(value: Pane) { + doSomething(); + } + } + `; + assertAnnotated({ + message: getFailureMessage({ + classDecoratorName: AngularClassDecorators.Pipe + }), + ruleName, + source + }); + }); + }); + + describe('methods', () => { + it('should fail if a method is decorated with @HostListener() decorator', () => { + const source = ` + @Pipe({ + name: 'test' + }) + class Test { + @HostListener('mouseover') + ~~~~~~~~~~~~~~~~~~~~~~~~~~ + mouseOver() { + this.doSomething(); + } + } + `; + assertAnnotated({ + message: getFailureMessage({ + classDecoratorName: AngularClassDecorators.Pipe + }), + ruleName, + source + }); + }); + }); + + describe('parameter properties', () => { + it('should fail if a parameter property is decorated with @Attribute() decorator', () => { + const source = ` + @Pipe({ + name: 'test' + }) + class Test { + constructor( + @Attribute('test') private readonly test: string + ~~~~~~~~~~~~~~~~~~ + ) {} + } + `; + assertAnnotated({ + message: getFailureMessage({ + classDecoratorName: AngularClassDecorators.Pipe + }), + ruleName, + source + }); + }); + }); + + describe('properties', () => { + it('should fail if a property is decorated with @ContentChild() decorator', () => { + const source = ` + @Pipe({ + name: 'test' + }) + class Test { + @ContentChild(Pane) pane: Pane; + ~~~~~~~~~~~~~~~~~~~ + } + `; + assertAnnotated({ + message: getFailureMessage({ + classDecoratorName: AngularClassDecorators.Pipe + }), + ruleName, + source + }); + }); + + it('should fail if a property is decorated with @ContentChildren() decorator', () => { + const source = ` + @Pipe({ + name: 'test' + }) + class Test { + @ContentChildren(Pane, { descendants: true }) arbitraryNestedPanes: QueryList; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + } + `; + assertAnnotated({ + message: getFailureMessage({ + classDecoratorName: AngularClassDecorators.Pipe + }), + ruleName, + source + }); + }); + + it('should fail if a property is decorated with @HostBinding() decorator', () => { + const source = ` + @Pipe({ + name: 'test' + }) + class Test { + @HostBinding('class.card-outline') private isCardOutline: boolean; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + } + `; + assertAnnotated({ + message: getFailureMessage({ + classDecoratorName: AngularClassDecorators.Pipe + }), + ruleName, + source + }); + }); + + it('should fail if a property is decorated with @Input() decorator', () => { + const source = ` + @Pipe({ + name: 'test' + }) + class Test { + @Input() label: string; + ~~~~~~~~ + } + `; + assertAnnotated({ + message: getFailureMessage({ + classDecoratorName: AngularClassDecorators.Pipe + }), + ruleName, + source + }); + }); + + it('should fail if a property is decorated with @Output() decorator', () => { + const source = ` + @Pipe({ + name: 'test' + }) + class Test { + @Output() emitter = new EventEmitter(); + ~~~~~~~~~ + } + `; + assertAnnotated({ + message: getFailureMessage({ + classDecoratorName: AngularClassDecorators.Pipe + }), + ruleName, + source + }); + }); + + it('should fail if a property is decorated with @ViewChild() decorator', () => { + const source = ` + @Pipe({ + name: 'test' + }) + class Test { + @ViewChild(Pane) pane: Pane; + ~~~~~~~~~~~~~~~~ + } + `; + assertAnnotated({ + message: getFailureMessage({ + classDecoratorName: AngularClassDecorators.Pipe + }), + ruleName, + source + }); + }); + + it('should fail if a property is decorated with @ViewChildren() decorator', () => { + const source = ` + @Pipe({ + name: 'test' + }) + class Test { + @ViewChildren(Pane) panes: QueryList; + ~~~~~~~~~~~~~~~~~~~ + } + `; + assertAnnotated({ + message: getFailureMessage({ + classDecoratorName: AngularClassDecorators.Pipe + }), + ruleName, + source + }); + }); + }); + + describe('multiple declarations', () => { + it('should fail if declarations are decorated with non allowed decorator', () => { + const source = ` + @Pipe({ + name: 'test' + }) + class Test { + @ContentChild(Pane) pane: Pane; + ~~~~~~~~~~~~~~~~~~~ + + @Input() + ^^^^^^^^ + get label(): string { + return this._label; + } + set label(value: string) { + this._label = value; + } + private _label: string; + + @ViewChild(Pane) + ################ + set label(value: Pane) { + doSomething(); + } + + get type(): string { + return this._type; + } + set type(value: string) { + this._type = value; + } + private _type: string; + + private prop: string | undefined; + + constructor( + @Attribute('test') private readonly test: string, + %%%%%%%%%%%%%%%%%% + @Inject(LOCALE_ID) localeId: string + ) {} + + @HostListener('mouseover') + ¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶ + mouseOver() { + this.doSomething(); + } + + clickHandler(): void {} + } + `; + const msg = getFailureMessage({ + classDecoratorName: AngularClassDecorators.Pipe + }); + const failures = [ + { + char: '~', + msg + }, + { + char: '^', + msg + }, + { + char: '#', + msg + }, + { + char: '%', + msg + }, + { + char: '¶', + msg + } + ]; + assertMultipleAnnotated({ + failures, + ruleName, + source + }); }); }); }); @@ -482,21 +1043,23 @@ describe(ruleName, () => { describe('multiple decorators per file', () => { it('should fail if contains @Directive and @Pipe decorators and the @Pipe contains a not allowed decorator', () => { const source = ` - @Directive() + @Directive({ + selector: 'test' + }) class TestDirective { @Input() label: string; } - @Pipe() + @Pipe({ + name: 'test' + }) class Test { @Input() label: string; ~~~~~~~~ } `; const message = getFailureMessage({ - classDecoratorName: AngularClassDecorators.Pipe, - className: 'Test', - innerClassDecoratorName: AngularInnerClassDecorators.Input + classDecoratorName: AngularClassDecorators.Pipe }); assertAnnotated({ message, @@ -509,178 +1072,898 @@ describe(ruleName, () => { describe('success', () => { describe('Component', () => { - it('should succeed if a property is decorated with @ContentChild() decorator', () => { - const source = ` - @Component() - class Test { - @ContentChild(Pane) pane: Pane; - } - `; - assertSuccess(ruleName, source); - }); + describe('accessors', () => { + it('should succeed if getter accessor is decorated with @Input() decorator', () => { + const source = ` + @Component({ + template: 'Hi!' + }) + class Test { + @Input() + get label(): string { + return this._label; + } + set label(value: string) { + this._label = value; + } + private _label: string; + } + `; + assertSuccess(ruleName, source); + }); - it('should succeed if a property is decorated with @ContentChildren() decorator', () => { - const source = ` - @Component() - class Test { - @ContentChildren(Pane, { descendants: true }) arbitraryNestedPanes: QueryList; - } - `; - assertSuccess(ruleName, source); - }); + it('should succeed if setter accessor is decorated with @Input() decorator', () => { + const source = ` + @Component({ + template: 'Hi!' + }) + class Test { + @Input() + set label(value: string) { + this._label = value; + } + get label(): string { + return this._label; + } + private _label: string; + } + `; + assertSuccess(ruleName, source); + }); - it('should succeed if a property is decorated with @HostBinding() decorator', () => { - const source = ` - @Component() - class Test { - @HostBinding('class.card-outline') private isCardOutline: boolean; - } - `; - assertSuccess(ruleName, source); + it('should succeed if setter accessor is decorated with @ViewChild() decorator', () => { + const source = ` + @Component({ + template: 'Hi!' + }) + class Test { + @ViewChild(Pane) + set label(value: Pane) { + doSomething(); + } + } + `; + assertSuccess(ruleName, source); + }); }); - it('should succeed if a method is decorated with @HostListener() decorator', () => { - const source = ` - @Component() - class Test { - @HostListener('mouseover') - mouseOver() { - console.log('mouseOver'); + describe('methods', () => { + it('should succeed if a method is decorated with @HostListener() decorator', () => { + const source = ` + @Component({ + template: 'Hi!' + }) + class Test { + @HostListener('mouseover') + mouseOver() { + this.doSomething(); + } } - } - `; - assertSuccess(ruleName, source); + `; + assertSuccess(ruleName, source); + }); }); - it('should succeed if a property is decorated with @Input() decorator', () => { - const source = ` - @Component() - class Test { - @Input() label: string; - } - `; - assertSuccess(ruleName, source); - }); + describe('parameter properties', () => { + it('should succeed if a parameter property is decorated with @Host() decorator', () => { + const source = ` + @Component({ + template: 'Hi!' + }) + class Test { + constructor( + @Host() private readonly host: DynamicHost + ) {} + } + `; + assertSuccess(ruleName, source); + }); - it('should succeed if a property is decorated with @Output() decorator', () => { - const source = ` - @Component() - class Test { - @Output() emitter = new EventEmitter(); - } - `; - assertSuccess(ruleName, source); - }); + it('should succeed if a parameter property is decorated with @Inject() decorator', () => { + const source = ` + @Component({ + template: 'Hi!' + }) + class Test { + constructor( + @Inject(LOCALE_ID) private readonly localeId: string + ) {} + } + `; + assertSuccess(ruleName, source); + }); - it('should succeed if a property is decorated with @ViewChild() decorator', () => { - const source = ` - @Component() - class Test { - @ViewChild(Pane) - set pane(value: Pane) { - console.log('panel setter called'); + it('should succeed if a parameter property is decorated with @Optional() decorator', () => { + const source = ` + @Component({ + template: 'Hi!' + }) + class Test { + constructor( + @Optional() testBase: TestBase, + ) {} } - } - `; - assertSuccess(ruleName, source); - }); + `; + assertSuccess(ruleName, source); + }); - it('should succeed if a property is decorated with @ViewChildren() decorator', () => { - const source = ` - @Component() - class Test { - @ViewChildren(Pane) panes: QueryList; - } - `; - assertSuccess(ruleName, source); - }); - }); + it('should succeed if a parameter property is decorated with @Self() decorator', () => { + const source = ` + @Component({ + template: 'Hi!' + }) + class Test { + constructor( + @Self() public readonly test: Test, + ) {} + } + `; + assertSuccess(ruleName, source); + }); - describe('Directive', () => { - it('should succeed if a property is decorated with @ContentChild() decorator', () => { - const source = ` - @Directive() - class Test { - @ContentChild(Pane) pane: Pane; - } - `; - assertSuccess(ruleName, source); + it('should succeed if a parameter property is decorated with @SkipSelf() decorator', () => { + const source = ` + @Component({ + template: 'Hi!' + }) + class Test { + constructor( + @SkipSelf() protected readonly parentTest: ParentTest + ) {} + } + `; + assertSuccess(ruleName, source); + }); }); - it('should succeed if a property is decorated with @ContentChildren() decorator', () => { - const source = ` - @Directive() - class Test { - @ContentChildren(Pane, { descendants: true }) arbitraryNestedPanes: QueryList; - } - `; - assertSuccess(ruleName, source); + describe('properties', () => { + it('should succeed if a property is decorated with @ContentChild() decorator', () => { + const source = ` + @Component({ + template: 'Hi!' + }) + class Test { + @ContentChild(Pane) pane: Pane; + } + `; + assertSuccess(ruleName, source); + }); + + it('should succeed if a property is decorated with @ContentChildren() decorator', () => { + const source = ` + @Component({ + template: 'Hi!' + }) + class Test { + @ContentChildren(Pane, { descendants: true }) arbitraryNestedPanes: QueryList; + } + `; + assertSuccess(ruleName, source); + }); + + it('should succeed if a property is decorated with @HostBinding() decorator', () => { + const source = ` + @Component({ + template: 'Hi!' + }) + class Test { + @HostBinding('class.card-outline') private isCardOutline: boolean; + } + `; + assertSuccess(ruleName, source); + }); + + it('should succeed if a property is decorated with @Input() decorator', () => { + const source = ` + @Component({ + template: 'Hi!' + }) + class Test { + @Input() label: string; + } + `; + assertSuccess(ruleName, source); + }); + + it('should succeed if a property is decorated with @Output() decorator', () => { + const source = ` + @Component({ + template: 'Hi!' + }) + class Test { + @Output() emitter = new EventEmitter(); + } + `; + assertSuccess(ruleName, source); + }); + + it('should succeed if a property is decorated with @ViewChild() decorator', () => { + const source = ` + @Component({ + template: 'Hi!' + }) + class Test { + @ViewChild(Pane) + set pane(value: Pane) { + console.log('panel setter called'); + } + } + `; + assertSuccess(ruleName, source); + }); + + it('should succeed if a property is decorated with @ViewChildren() decorator', () => { + const source = ` + @Component({ + template: 'Hi!' + }) + class Test { + @ViewChildren(Pane) panes: QueryList; + } + `; + assertSuccess(ruleName, source); + }); }); - it('should succeed if a property is decorated with @HostBinding() decorator', () => { - const source = ` - @Directive() - class Test { - @HostBinding('class.card-outline') private isCardOutline: boolean; - } - `; - assertSuccess(ruleName, source); + describe('multiple declarations', () => { + it('should succeed if declarations are decorated with allowed decorators', () => { + const source = ` + @Component({ + template: 'Hi!' + }) + class Test { + @ContentChild(Pane) pane: Pane; + + @Input() + get label(): string { + return this._label; + } + set label(value: string) { + this._label = value; + } + private _label: string; + + @ViewChild(Pane) + set label(value: Pane) { + doSomething(); + } + + get type(): string { + return this._type; + } + set type(value: string) { + this._type = value; + } + private _type: string; + + private prop: string | undefined; + + constructor( + @Attribute('test') private readonly test: string, + @Host() @Optional() private readonly host: DynamicHost, + @Inject(LOCALE_ID) private readonly localeId: string, + @Inject(TEST_BASE) @Optional() testBase: TestBase, + @Optional() @Self() public readonly test: Test, + @Optional() @SkipSelf() protected readonly parentTest: ParentTest + ) {} + + @HostListener('mouseover') + mouseOver(): void { + this.doSomething(); + } + + clickHandler(): void {} + } + `; + assertSuccess(ruleName, source); + }); }); + }); - it('should succeed if a method is decorated with @HostListener() decorator', () => { - const source = ` - @Directive() - class Test { - @HostListener('mouseover') - mouseOver() { - console.log('mouseOver'); + describe('Directive', () => { + describe('accessors', () => { + it('should succeed if getter accessor is decorated with @Input() decorator', () => { + const source = ` + @Directive({ + selector: 'test' + }) + class Test { + @Input() + get label(): string { + return this._label; + } + set label(value: string) { + this._label = value; + } + private _label: string; } - } - `; - assertSuccess(ruleName, source); + `; + assertSuccess(ruleName, source); + }); + + it('should succeed if setter accessor is decorated with @Input() decorator', () => { + const source = ` + @Directive({ + selector: 'test' + }) + class Test { + @Input() + set label(value: string) { + this._label = value; + } + get label(): string { + return this._label; + } + private _label: string; + } + `; + assertSuccess(ruleName, source); + }); + + it('should succeed if setter accessor is decorated with @ViewChild() decorator', () => { + const source = ` + @Directive({ + selector: 'test' + }) + class Test { + @ViewChild(Pane) + set label(value: Pane) { + doSomething(); + } + } + `; + assertSuccess(ruleName, source); + }); }); - it('should succeed if a property is decorated with @Input() decorator', () => { - const source = ` - @Directive() - class Test { - @Input() label: string; - } - `; - assertSuccess(ruleName, source); + describe('methods', () => { + it('should succeed if a method is decorated with @HostListener() decorator', () => { + const source = ` + @Directive({ + selector: 'test' + }) + class Test { + @HostListener('mouseover') + mouseOver() { + this.doSomething(); + } + } + `; + assertSuccess(ruleName, source); + }); }); - it('should succeed if a property is decorated with @Output() decorator', () => { - const source = ` - @Directive() - class Test { - @Output() emitter = new EventEmitter(); - } - `; - assertSuccess(ruleName, source); + describe('parameter properties', () => { + it('should succeed if a parameter property is decorated with @Host() decorator', () => { + const source = ` + @Directive({ + selector: 'test' + }) + class Test { + constructor( + @Host() private readonly host: DynamicHost + ) {} + } + `; + assertSuccess(ruleName, source); + }); + + it('should succeed if a parameter property is decorated with @Inject() decorator', () => { + const source = ` + @Directive({ + selector: 'test' + }) + class Test { + constructor( + @Inject(LOCALE_ID) private readonly localeId: string + ) {} + } + `; + assertSuccess(ruleName, source); + }); + + it('should succeed if a parameter property is decorated with @Optional() decorator', () => { + const source = ` + @Directive({ + selector: 'test' + }) + class Test { + constructor( + @Optional() testBase: TestBase, + ) {} + } + `; + assertSuccess(ruleName, source); + }); + + it('should succeed if a parameter property is decorated with @Self() decorator', () => { + const source = ` + @Directive({ + selector: 'test' + }) + class Test { + constructor( + @Self() public readonly test: Test, + ) {} + } + `; + assertSuccess(ruleName, source); + }); + + it('should succeed if a parameter property is decorated with @SkipSelf() decorator', () => { + const source = ` + @Directive({ + selector: 'test' + }) + class Test { + constructor( + @SkipSelf() protected readonly parentTest: ParentTest + ) {} + } + `; + assertSuccess(ruleName, source); + }); }); - it('should succeed if a property is decorated with @ViewChild() decorator', () => { - const source = ` - @Directive() - class Test { - @ViewChild(Pane) - set pane(value: Pane) { - console.log('panel setter called'); + describe('properties', () => { + it('should succeed if a property is decorated with @ContentChild() decorator', () => { + const source = ` + @Directive({ + selector: 'test' + }) + class Test { + @ContentChild(Pane) pane: Pane; } - } - `; - assertSuccess(ruleName, source); + `; + assertSuccess(ruleName, source); + }); + + it('should succeed if a property is decorated with @ContentChildren() decorator', () => { + const source = ` + @Directive({ + selector: 'test' + }) + class Test { + @ContentChildren(Pane, { descendants: true }) arbitraryNestedPanes: QueryList; + } + `; + assertSuccess(ruleName, source); + }); + + it('should succeed if a property is decorated with @HostBinding() decorator', () => { + const source = ` + @Directive({ + selector: 'test' + }) + class Test { + @HostBinding('class.card-outline') private isCardOutline: boolean; + } + `; + assertSuccess(ruleName, source); + }); + + it('should succeed if a property is decorated with @Input() decorator', () => { + const source = ` + @Directive({ + selector: 'test' + }) + class Test { + @Input() label: string; + } + `; + assertSuccess(ruleName, source); + }); + + it('should succeed if a property is decorated with @Output() decorator', () => { + const source = ` + @Directive({ + selector: 'test' + }) + class Test { + @Output() emitter = new EventEmitter(); + } + `; + assertSuccess(ruleName, source); + }); + + it('should succeed if a property is decorated with @ViewChild() decorator', () => { + const source = ` + @Directive({ + selector: 'test' + }) + class Test { + @ViewChild(Pane) + set pane(value: Pane) { + console.log('panel setter called'); + } + } + `; + assertSuccess(ruleName, source); + }); + + it('should succeed if a property is decorated with @ViewChildren() decorator', () => { + const source = ` + @Directive({ + selector: 'test' + }) + class Test { + @ViewChildren(Pane) panes: QueryList; + } + `; + assertSuccess(ruleName, source); + }); }); - it('should succeed if a property is decorated with @ViewChildren() decorator', () => { - const source = ` - @Directive() - class Test { - @ViewChildren(Pane) panes: QueryList; - } - `; - assertSuccess(ruleName, source); + describe('multiple declarations', () => { + it('should succeed if declarations are decorated with allowed decorators', () => { + const source = ` + @Directive({ + selector: 'test' + }) + class Test { + @ContentChild(Pane) pane: Pane; + + @Input() + get label(): string { + return this._label; + } + set label(value: string) { + this._label = value; + } + private _label: string; + + @ViewChild(Pane) + set label(value: Pane) { + doSomething(); + } + + get type(): string { + return this._type; + } + set type(value: string) { + this._type = value; + } + private _type: string; + + private prop: string | undefined; + + constructor( + @Attribute('test') private readonly test: string, + @Host() @Optional() private readonly host: DynamicHost, + @Inject(LOCALE_ID) private readonly localeId: string, + @Inject(TEST_BASE) @Optional() testBase: TestBase, + @Optional() @Self() public readonly test: Test, + @Optional() @SkipSelf() protected readonly parentTest: ParentTest + ) {} + + @HostListener('mouseover') + mouseOver(): void { + this.doSomething(); + } + + clickHandler(): void {} + } + `; + assertSuccess(ruleName, source); + }); + }); + }); + + describe('Injectable', () => { + describe('parameter properties', () => { + it('should succeed if a parameter property is decorated with @Host() decorator', () => { + const source = ` + @Injectable({ + providedIn: 'root' + }) + class Test { + constructor( + @Host() private readonly host: DynamicHost + ) {} + } + `; + assertSuccess(ruleName, source); + }); + + it('should succeed if a parameter property is decorated with @Inject() decorator', () => { + const source = ` + @Injectable({ + providedIn: 'root' + }) + class Test { + constructor( + @Inject(LOCALE_ID) private readonly localeId: string + ) {} + } + `; + assertSuccess(ruleName, source); + }); + + it('should succeed if a parameter property is decorated with @Optional() decorator', () => { + const source = ` + @Injectable({ + providedIn: 'root' + }) + class Test { + constructor( + @Optional() testBase: TestBase, + ) {} + } + `; + assertSuccess(ruleName, source); + }); + + it('should succeed if a parameter property is decorated with @Self() decorator', () => { + const source = ` + @Injectable({ + providedIn: 'root' + }) + class Test { + constructor( + @Self() public readonly test: Test, + ) {} + } + `; + assertSuccess(ruleName, source); + }); + + it('should succeed if a parameter property is decorated with @SkipSelf() decorator', () => { + const source = ` + @Injectable({ + providedIn: 'root' + }) + class Test { + constructor( + @SkipSelf() protected readonly parentTest: ParentTest + ) {} + } + `; + assertSuccess(ruleName, source); + }); + }); + + describe('multiple declarations', () => { + it('should succeed if declarations are decorated with allowed decorators', () => { + const source = ` + @Injectable({ + providedIn: 'root' + }) + class Test { + get type(): string { + return this._type; + } + set type(value: string) { + this._type = value; + } + private _type: string; + + private prop: string | undefined; + + constructor( + @Host() @Optional() private readonly host: DynamicHost, + @Inject(LOCALE_ID) private readonly localeId: string, + @Inject(TEST_BASE) @Optional() testBase: TestBase, + @Optional() @Self() public readonly test: Test, + @Optional() @SkipSelf() protected readonly parentTest: ParentTest + ) {} + + clickHandler(): void {} + } + `; + assertSuccess(ruleName, source); + }); + }); + }); + + describe('NgModule', () => { + describe('parameter properties', () => { + it('should succeed if a parameter property is decorated with @Host() decorator', () => { + const source = ` + @NgModule({ + providers: [] + }) + class Test { + constructor( + @Host() private readonly host: DynamicHost + ) {} + } + `; + assertSuccess(ruleName, source); + }); + + it('should succeed if a parameter property is decorated with @Inject() decorator', () => { + const source = ` + @NgModule({ + providers: [] + }) + class Test { + constructor( + @Inject(LOCALE_ID) private readonly localeId: string + ) {} + } + `; + assertSuccess(ruleName, source); + }); + + it('should succeed if a parameter property is decorated with @Optional() decorator', () => { + const source = ` + @NgModule({ + providers: [] + }) + class Test { + constructor( + @Optional() testBase: TestBase, + ) {} + } + `; + assertSuccess(ruleName, source); + }); + + it('should succeed if a parameter property is decorated with @Self() decorator', () => { + const source = ` + @NgModule({ + providers: [] + }) + class Test { + constructor( + @Self() public readonly test: Test, + ) {} + } + `; + assertSuccess(ruleName, source); + }); + + it('should succeed if a parameter property is decorated with @SkipSelf() decorator', () => { + const source = ` + @NgModule({ + providers: [] + }) + class Test { + constructor( + @SkipSelf() protected readonly parentTest: ParentTest + ) {} + } + `; + assertSuccess(ruleName, source); + }); + }); + + describe('multiple declarations', () => { + it('should succeed if declarations are decorated with allowed decorators', () => { + const source = ` + @NgModule({ + providers: [] + }) + class Test { + get type(): string { + return this._type; + } + set type(value: string) { + this._type = value; + } + private _type: string; + + private prop: string | undefined; + + constructor( + @Host() @Optional() private readonly host: DynamicHost, + @Inject(LOCALE_ID) private readonly localeId: string, + @Inject(TEST_BASE) @Optional() testBase: TestBase, + @Optional() @Self() public readonly test: Test, + @Optional() @SkipSelf() protected readonly parentTest: ParentTest + ) {} + + clickHandler(): void {} + } + `; + assertSuccess(ruleName, source); + }); + }); + }); + + describe('Pipe', () => { + describe('parameter properties', () => { + it('should succeed if a parameter property is decorated with @Host() decorator', () => { + const source = ` + @Pipe({ + name: 'test' + }) + class Test { + constructor( + @Host() private readonly host: DynamicHost + ) {} + } + `; + assertSuccess(ruleName, source); + }); + + it('should succeed if a parameter property is decorated with @Inject() decorator', () => { + const source = ` + @Pipe({ + name: 'test' + }) + class Test { + constructor( + @Inject(LOCALE_ID) private readonly localeId: string + ) {} + } + `; + assertSuccess(ruleName, source); + }); + + it('should succeed if a parameter property is decorated with @Optional() decorator', () => { + const source = ` + @Pipe({ + name: 'test' + }) + class Test { + constructor( + @Optional() testBase: TestBase, + ) {} + } + `; + assertSuccess(ruleName, source); + }); + + it('should succeed if a parameter property is decorated with @Self() decorator', () => { + const source = ` + @Pipe({ + name: 'test' + }) + class Test { + constructor( + @Self() public readonly test: Test, + ) {} + } + `; + assertSuccess(ruleName, source); + }); + + it('should succeed if a parameter property is decorated with @SkipSelf() decorator', () => { + const source = ` + @Pipe({ + name: 'test' + }) + class Test { + constructor( + @SkipSelf() protected readonly parentTest: ParentTest + ) {} + } + `; + assertSuccess(ruleName, source); + }); + }); + + describe('multiple declarations', () => { + it('should succeed if declarations are decorated with allowed decorators', () => { + const source = ` + @Pipe({ + name: 'test' + }) + class Test { + get type(): string { + return this._type; + } + set type(value: string) { + this._type = value; + } + private _type: string; + + private prop: string | undefined; + + constructor( + @Host() @Optional() private readonly host: DynamicHost, + @Inject(LOCALE_ID) private readonly localeId: string, + @Inject(TEST_BASE) @Optional() testBase: TestBase, + @Optional() @Self() public readonly test: Test, + @Optional() @SkipSelf() protected readonly parentTest: ParentTest + ) {} + + clickHandler(): void {} + } + `; + assertSuccess(ruleName, source); + }); }); });