From 3324169ba76fbdf5530b4538477f8ae7e7fb2a0a Mon Sep 17 00:00:00 2001 From: Rafael Santana Date: Tue, 26 Mar 2019 23:12:44 -0300 Subject: [PATCH 1/4] feat: create objectKeys helper --- src/util/object-keys.ts | 1 + 1 file changed, 1 insertion(+) create mode 100644 src/util/object-keys.ts diff --git a/src/util/object-keys.ts b/src/util/object-keys.ts new file mode 100644 index 000000000..faeb007d8 --- /dev/null +++ b/src/util/object-keys.ts @@ -0,0 +1 @@ +export const objectKeys = Object.keys as (o: T) => ReadonlyArray>; From 66672e101059d8c9bddeff748272ab5adf3eb216 Mon Sep 17 00:00:00 2001 From: Rafael Santana Date: Tue, 26 Mar 2019 23:13:10 -0300 Subject: [PATCH 2/4] fix: adjust getDecoratorName function --- src/util/utils.ts | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/util/utils.ts b/src/util/utils.ts index 3387927c8..202812f7f 100644 --- a/src/util/utils.ts +++ b/src/util/utils.ts @@ -1,4 +1,5 @@ import { + BooleanLiteral, ClassDeclaration, createNodeArray, Decorator, @@ -19,7 +20,6 @@ import { ObjectLiteralExpression, SourceFile, StringLiteral, - BooleanLiteral, SyntaxKind } from 'typescript'; import { getDeclaredMethods } from './classDeclarationUtils'; @@ -136,10 +136,17 @@ export const getDecoratorPropertyInitializer = (decorator: Decorator, name: stri return property.initializer; }; -export const getDecoratorName = (decorator: Decorator): string | undefined => - isCallExpression(decorator.expression) && isIdentifier(decorator.expression.expression) - ? decorator.expression.expression.text - : undefined; +export const getDecoratorName = (decorator: Decorator): string | undefined => { + const { expression } = decorator; + + if (isIdentifier(expression)) return expression.text; + + if (isCallExpression(expression) && isIdentifier(expression.expression)) { + return expression.expression.text; + } + + return undefined; +}; export const getNextToLastParentNode = (node: Node): Node => { let currentNode = node; @@ -203,12 +210,12 @@ export const kebabToCamelCase = (value: string) => value.replace(/-[a-zA-Z]/g, x export const isSameLine = (sourceFile: SourceFile, pos1: number, pos2: number): boolean => getLineAndCharacterOfPosition(sourceFile, pos1).line === getLineAndCharacterOfPosition(sourceFile, pos2).line; -export const isStringLiteralLike = (node: Node): node is StringLiteral | NoSubstitutionTemplateLiteral => - isStringLiteral(node) || isNoSubstitutionTemplateLiteral(node); - export const isBooleanLiteralLike = (node: Node): node is BooleanLiteral => node.kind === SyntaxKind.FalseKeyword || node.kind === SyntaxKind.TrueKeyword; +export const isStringLiteralLike = (node: Node): node is StringLiteral | NoSubstitutionTemplateLiteral => + isStringLiteral(node) || isNoSubstitutionTemplateLiteral(node); + export const maybeNodeArray = (nodes: NodeArray): ReadonlyArray => nodes || []; // Regex below matches any Unicode word and compatible with ES5. In ES2018 the same result From 253af22fdab8cb89a3571e94cc2f3469989376bf Mon Sep 17 00:00:00 2001 From: Rafael Santana Date: Tue, 26 Mar 2019 23:14:53 -0300 Subject: [PATCH 3/4] refactor(rule): minor refactor for no-attribute-decorator --- src/noAttributeDecoratorRule.ts | 37 ++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/src/noAttributeDecoratorRule.ts b/src/noAttributeDecoratorRule.ts index b56ed6d4b..70131579c 100644 --- a/src/noAttributeDecoratorRule.ts +++ b/src/noAttributeDecoratorRule.ts @@ -2,6 +2,7 @@ import { IRuleMetadata, RuleFailure, WalkContext } from 'tslint'; import { AbstractRule } from 'tslint/lib/rules'; import { ConstructorDeclaration, + createNodeArray, Decorator, forEachChild, isConstructorDeclaration, @@ -11,48 +12,50 @@ import { } from 'typescript'; import { getDecoratorName } from './util/utils'; +const ATTRIBUTE = 'Attribute'; + export class Rule extends AbstractRule { static readonly metadata: IRuleMetadata = { - description: 'Disallows usage of @Attribute decorator.', + description: `Disallows usage of @${ATTRIBUTE} decorator.`, options: null, optionsDescription: 'Not configurable.', - rationale: '@Attribute is considered bad practice. Use @Input instead.', ruleName: 'no-attribute-decorator', type: 'functionality', typescriptOnly: true }; - static readonly FAILURE_STRING = '@Attribute is considered bad practice. Use @Input instead.'; + static readonly FAILURE_STRING = `@${ATTRIBUTE} is considered bad practice. Use @Input instead.`; apply(sourceFile: SourceFile): RuleFailure[] { return this.applyWithFunction(sourceFile, walk); } } -const isAttributeDecorator = (decorator: Decorator): boolean => getDecoratorName(decorator) === 'Attribute'; - -const validateConstructor = (context: WalkContext, node: ConstructorDeclaration): void => - node.parameters.forEach(parameter => validateParameter(context, parameter)); +const callbackHandler = (walkContext: WalkContext, node: Node): void => { + if (isConstructorDeclaration(node)) validateConstructor(walkContext, node); +}; -const validateDecorator = (context: WalkContext, decorator: Decorator): void => { - if (!isAttributeDecorator(decorator)) return; +const isAttributeDecorator = (decorator: Decorator): boolean => getDecoratorName(decorator) === ATTRIBUTE; - context.addFailureAtNode(decorator, Rule.FAILURE_STRING); +const validateConstructor = (walkContext: WalkContext, node: ConstructorDeclaration): void => { + node.parameters.forEach(parameter => validateParameter(walkContext, parameter)); }; -const validateParameter = (context: WalkContext, parameter: ParameterDeclaration): void => { - const { decorators } = parameter; +const validateDecorator = (walkContext: WalkContext, decorator: Decorator): void => { + if (!isAttributeDecorator(decorator)) return; - if (!decorators) return; + walkContext.addFailureAtNode(decorator, Rule.FAILURE_STRING); +}; - decorators.forEach(decorator => validateDecorator(context, decorator)); +const validateParameter = (walkContext: WalkContext, node: ParameterDeclaration): void => { + createNodeArray(node.decorators).forEach(decorator => validateDecorator(walkContext, decorator)); }; -const walk = (context: WalkContext): void => { - const { sourceFile } = context; +const walk = (walkContext: WalkContext): void => { + const { sourceFile } = walkContext; const callback = (node: Node): void => { - if (isConstructorDeclaration(node)) validateConstructor(context, node); + callbackHandler(walkContext, node); forEachChild(node, callback); }; From bc9c6a3f5c4d871ac94875a596786e5c8c6405e5 Mon Sep 17 00:00:00 2001 From: Rafael Santana Date: Tue, 26 Mar 2019 23:15:25 -0300 Subject: [PATCH 4/4] refactor(rule): rework prefer-inline-decorator to accept options --- src/preferInlineDecoratorRule.ts | 295 ++- test/preferInlineDecoratorRule.spec.ts | 3027 +++++++++++++++++++++++- 2 files changed, 3166 insertions(+), 156 deletions(-) diff --git a/src/preferInlineDecoratorRule.ts b/src/preferInlineDecoratorRule.ts index 1c0a6c103..f6dafeddd 100644 --- a/src/preferInlineDecoratorRule.ts +++ b/src/preferInlineDecoratorRule.ts @@ -1,81 +1,286 @@ -import { IOptions, IRuleMetadata, Replacement, RuleFailure } from 'tslint/lib'; +import { IRuleMetadata, RuleFailure, WalkContext } from 'tslint/lib'; import { AbstractRule } from 'tslint/lib/rules'; -import { Decorator, isPropertyDeclaration, SourceFile } from 'typescript'; -import { NgWalker } from './angular/ngWalker'; +import { dedent } from 'tslint/lib/utils'; +import { + createNodeArray, + Decorator, + forEachChild, + GetAccessorDeclaration, + isGetAccessorDeclaration, + isMethodDeclaration, + isParameter, + isParameterPropertyDeclaration, + isPropertyDeclaration, + isSetAccessorDeclaration, + MethodDeclaration, + Node, + ParameterDeclaration, + ParameterPropertyDeclaration, + PropertyDeclaration, + SetAccessorDeclaration, + SourceFile +} from 'typescript'; +import { isNotNullOrUndefined } from './util/is-not-null-or-undefined'; +import { objectKeys } from './util/object-keys'; import { Decorators, getDecoratorName, isSameLine } from './util/utils'; +const OPTION_GETTERS = 'getters'; +const OPTION_METHODS = 'methods'; +const OPTION_PARAMETER_PROPERTIES = 'parameter-properties'; +const OPTION_PARAMETERS = 'parameters'; +const OPTION_PROPERTIES = 'properties'; +const OPTION_SETTERS = 'setters'; + +const OPTION_SAFELIST = 'safelist'; + +const OPTION_SCHEMA_VALUE = { + oneOf: [ + { + type: 'boolean' + }, + { + properties: { + items: { + type: 'string' + }, + type: 'array', + uniqueItems: true + }, + type: 'object' + } + ] +}; + +type OptionKeys = + | typeof OPTION_GETTERS + | typeof OPTION_METHODS + | typeof OPTION_PARAMETER_PROPERTIES + | typeof OPTION_PARAMETERS + | typeof OPTION_PROPERTIES + | typeof OPTION_SETTERS; + +type Safelist = Record>; + +type OptionValue = boolean | Safelist; + +type OptionDictionary = Record; + +type Declaration = + | GetAccessorDeclaration + | MethodDeclaration + | ParameterDeclaration + | ParameterPropertyDeclaration + | PropertyDeclaration + | SetAccessorDeclaration; + +const DEFAULT_OPTIONS: OptionDictionary = { + [OPTION_GETTERS]: true, + [OPTION_METHODS]: true, + [OPTION_PARAMETER_PROPERTIES]: true, + [OPTION_PARAMETERS]: true, + [OPTION_PROPERTIES]: true, + [OPTION_SETTERS]: true +}; + +const STYLE_GUIDE_LINK = 'https://angular.io/guide/styleguide#style-05-12'; + export class Rule extends AbstractRule { static readonly metadata: IRuleMetadata = { - description: 'Ensures that decorators are on the same line as the property/method it decorates.', - descriptionDetails: 'See more at https://angular.io/guide/styleguide#style-05-12.', - hasFix: true, - optionExamples: [true, [true, Decorators.HostListener], [true, Decorators.Input, 'MyCustomDecorator']], - options: { - items: [ + description: 'Ensures that declarations are on the same line as its decorator(s).', + descriptionDetails: `See more at ${STYLE_GUIDE_LINK}.`, + optionExamples: [ + true, + [true, { [OPTION_METHODS]: false }], + [ + true, { - type: 'string' + [OPTION_GETTERS]: { + [OPTION_SAFELIST]: [Decorators.Input] + }, + [OPTION_METHODS]: true, + [OPTION_PARAMETER_PROPERTIES]: false, + [OPTION_PARAMETERS]: false, + [OPTION_PROPERTIES]: { + [OPTION_SAFELIST]: [Decorators.Output, 'MyCustomDecorator'] + }, + [OPTION_SETTERS]: true } - ], - type: 'array' + ] + ], + options: { + additionalProperties: false, + properties: { + [OPTION_GETTERS]: OPTION_SCHEMA_VALUE, + [OPTION_METHODS]: OPTION_SCHEMA_VALUE, + [OPTION_PARAMETER_PROPERTIES]: OPTION_SCHEMA_VALUE, + [OPTION_PARAMETERS]: OPTION_SCHEMA_VALUE, + [OPTION_PROPERTIES]: OPTION_SCHEMA_VALUE, + [OPTION_SETTERS]: OPTION_SCHEMA_VALUE + }, + type: 'object' }, - optionsDescription: 'A list of blacklisted decorators.', - rationale: 'Placing the decorator on the same line usually makes for shorter code and still easily identifies the property/method.', + optionsDescription: dedent` + An optional object with optional \`${OPTION_GETTERS}\`, \`${OPTION_METHODS}\`, \`${OPTION_PARAMETER_PROPERTIES}\`, \`${OPTION_PARAMETERS}\`, \`${OPTION_PROPERTIES}\` and \`${OPTION_SETTERS}\` properties. + + The properties can be specifed as booleans or as objects with the property \`${OPTION_SAFELIST}\` containing the names of the decorators that should be ignored. Note that if a declaration is decorated with multiple decorators and at least one of them is present in \`${OPTION_SAFELIST}\`, this declaration is ignored. + + * \`${OPTION_GETTERS}\` - requires that ${OPTION_GETTERS} are on the same line as its decorator(s). Defaults to \`true\`. + * \`${OPTION_METHODS}\` - requires that ${OPTION_METHODS} are on the same line as its decorator(s). Defaults to \`true\`. + * \`${OPTION_PARAMETER_PROPERTIES}\` - requires that parameter properties are on the same line as its decorator(s). Defaults to \`true\`. + * \`${OPTION_PARAMETERS}\` - requires that ${OPTION_PARAMETERS} are on the same line as its decorator(s). Defaults to \`true\`. + * \`${OPTION_PROPERTIES}\` - requires that ${OPTION_PROPERTIES} are on the same line as its decorator(s). Defaults to \`true\`. + * \`${OPTION_SETTERS}\` - requires that ${OPTION_SETTERS} are on the same line as its decorator(s). Defaults to \`true\`. + `, + rationale: 'Placing the decorator on the same line usually makes for shorter code and still easily identifies the declarations.', ruleName: 'prefer-inline-decorator', type: 'style', typescriptOnly: true }; - static readonly FAILURE_STRING = 'Consider placing decorators on the same line as the property/method it decorates'; + static readonly FAILURE_STRING = `Place declarations on the same line as its decorator(s) (${STYLE_GUIDE_LINK})`; apply(sourceFile: SourceFile): RuleFailure[] { - const walker = new Walker(sourceFile, this.getOptions()); + const options: OptionDictionary = { + ...DEFAULT_OPTIONS, + ...this.ruleArguments[0] + }; - return this.applyWithWalker(walker); + return this.applyWithFunction(sourceFile, walk, options); } isEnabled(): boolean { - return super.isEnabled() && this.ruleArguments.every(ruleArgument => !!(typeof ruleArgument === 'string' && ruleArgument.trim())); + return super.isEnabled() && this.areOptionsValid(); } -} -class Walker extends NgWalker { - private readonly blacklistedDecorators: ReadonlySet; + private areOptionsValid(): boolean { + const { length: ruleArgumentsLength } = this.ruleArguments; - constructor(source: SourceFile, options: IOptions) { - super(source, options); - this.blacklistedDecorators = new Set(options.ruleArguments); - } + if (ruleArgumentsLength === 0) return true; + + if (ruleArgumentsLength > 1) return false; - protected visitMethodDecorator(decorator: Decorator): void { - this.validateDecorator(decorator); - super.visitMethodDecorator(decorator); + const { + metadata: { options: ruleOptions } + } = Rule; + const [ruleArgument] = this.ruleArguments as ReadonlyArray; + const ruleArgumentsKeys = objectKeys(ruleArgument); + const propertiesKeys = objectKeys(ruleOptions.properties as OptionDictionary); + + return ( + ruleArgumentsKeys.every(argumentKey => propertiesKeys.indexOf(argumentKey) !== -1) && + ruleArgumentsKeys + .map(argumentKey => ruleArgument[argumentKey]) + .every(argumentValue => { + if (typeof argumentValue === 'boolean') return true; + + if (!argumentValue || typeof argumentValue !== 'object') return false; + + const argumentValueKeys = objectKeys(argumentValue); + + if (argumentValueKeys.length !== 1) return false; + + const safelist = argumentValue[argumentValueKeys[0]]; + + return Array.isArray(safelist) && safelist.length > 0; + }) + ); } +} + +const callbackHandler = (walkContext: WalkContext, node: Node): void => { + const { + options: { getters, methods, [OPTION_PARAMETER_PROPERTIES]: parameterProperties, parameters, properties, setters } + } = walkContext; - protected visitPropertyDecorator(decorator: Decorator): void { - this.validateDecorator(decorator); - super.visitPropertyDecorator(decorator); + if (getters && isGetAccessorDeclaration(node)) { + validateGetAccessorDeclaration(walkContext, node); + } else if (methods && isMethodDeclaration(node)) { + validateMethodDeclaration(walkContext, node); + } else if (parameters && isParameter(node) && !isParameterPropertyDeclaration(node)) { + validateParameterDeclaration(walkContext, node); + } else if (parameterProperties && isParameterPropertyDeclaration(node)) { + validateParameterPropertyDeclaration(walkContext, node); + } else if (properties && isPropertyDeclaration(node)) { + validatePropertyDeclaration(walkContext, node); + } else if (setters && isSetAccessorDeclaration(node)) { + validateSetAccessorDeclaration(walkContext, node); } +}; - private validateDecorator(decorator: Decorator): void { - const decoratorName = getDecoratorName(decorator); +const canIgnoreDecorator = (walkContext: WalkContext, decoratorName: string, optionKey: OptionKeys): boolean => { + const { + options: { [optionKey]: optionValue } + } = walkContext; - if (!decoratorName) return; + return optionValue && typeof optionValue === 'object' && optionValue.safelist.indexOf(decoratorName) !== -1; +}; - const isDecoratorBlacklisted = this.blacklistedDecorators.has(decoratorName); +const hasAnyIgnoredDecorator = ( + walkContext: WalkContext, + decorators: ReadonlyArray, + optionKey: OptionKeys +): boolean => { + const nonIgnoredDecoratorNames = decorators + .map(getDecoratorName) + .filter(isNotNullOrUndefined) + .filter(decoratorName => !canIgnoreDecorator(walkContext, decoratorName, optionKey)); - if (isDecoratorBlacklisted) return; + return decorators.length !== nonIgnoredDecoratorNames.length; +}; - const decoratorStartPos = decorator.getStart(); - const { parent: property } = decorator; +const validateDecorators = ( + walkContext: WalkContext, + decorators: ReadonlyArray, + declaration: Declaration, + optionKey: OptionKeys +): void => { + if (decorators.length === 0 || hasAnyIgnoredDecorator(walkContext, decorators, optionKey)) return; - if (!property || !isPropertyDeclaration(property)) return; + const [firstDecorator] = decorators; + const firstDecoratorStartPos = firstDecorator.getStart(); + const declarationStartPos = declaration.name.getStart(); - const propertyStartPos = property.name.getStart(); + if (isSameLine(walkContext.sourceFile, firstDecoratorStartPos, declarationStartPos)) return; - if (isSameLine(this.getSourceFile(), decoratorStartPos, propertyStartPos)) return; + walkContext.addFailureAt(firstDecoratorStartPos, declaration.getWidth(), Rule.FAILURE_STRING); +}; - const fix = Replacement.deleteFromTo(decorator.getEnd(), propertyStartPos - 1); +const validateDeclaration = (walkContext: WalkContext, declaration: Declaration, optionKey: OptionKeys): void => { + validateDecorators(walkContext, createNodeArray(declaration.decorators), declaration, optionKey); +}; - this.addFailureAt(decoratorStartPos, property.getWidth(), Rule.FAILURE_STRING, fix); - } -} +const validateGetAccessorDeclaration = (walkContext: WalkContext, node: GetAccessorDeclaration): void => { + validateDeclaration(walkContext, node, OPTION_GETTERS); +}; + +const validateMethodDeclaration = (walkContext: WalkContext, node: MethodDeclaration): void => { + validateDeclaration(walkContext, node, OPTION_METHODS); +}; + +const validateParameterDeclaration = (walkContext: WalkContext, node: ParameterDeclaration): void => { + validateDeclaration(walkContext, node, OPTION_PARAMETERS); +}; + +const validateParameterPropertyDeclaration = (walkContext: WalkContext, node: ParameterDeclaration): void => { + validateDeclaration(walkContext, node, OPTION_PARAMETER_PROPERTIES); +}; + +const validatePropertyDeclaration = (walkContext: WalkContext, node: PropertyDeclaration): void => { + validateDeclaration(walkContext, node, OPTION_PROPERTIES); +}; + +const validateSetAccessorDeclaration = (walkContext: WalkContext, node: SetAccessorDeclaration): void => { + validateDeclaration(walkContext, node, OPTION_SETTERS); +}; + +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/preferInlineDecoratorRule.spec.ts b/test/preferInlineDecoratorRule.spec.ts index df3d48da3..5f198d97b 100644 --- a/test/preferInlineDecoratorRule.spec.ts +++ b/test/preferInlineDecoratorRule.spec.ts @@ -1,7 +1,4 @@ -import { expect } from 'chai'; -import { Replacement } from 'tslint/lib'; import { Rule } from '../src/preferInlineDecoratorRule'; -import { Decorators } from '../src/util/utils'; import { assertAnnotated, assertFailures, assertSuccess } from './testHelper'; const { @@ -11,14 +8,21 @@ const { describe(ruleName, () => { describe('failure', () => { - describe('common cases', () => { - it('should fail if a property is not on the same line as its decorator', () => { + describe('getters', () => { + it('should fail if a getter is not on the same line as its decorator', () => { const source = ` + @Component({ + selector: 'app-test', + template: '

Hey!

' + }) class Test { - @Input('test') - ~~~~~~~~~~~~~~ - testVar: string; - ~~~~~~~~~~~~~~~~ + @Input() + ~~~~~~~~ + get test(): string { + return this._test; + } + ~ + private _test: string; } `; assertAnnotated({ @@ -28,176 +32,2977 @@ describe(ruleName, () => { }); }); - it('should fail if multiple properties are not on the same line as their decorators', () => { + it('should fail if a getter is not on the same line as its decorators', () => { const source = ` - class Test { - @Input('test1') - testVar1: string; - @MyCustomDecorator() - testVar2: string; + function getValue(@Parse + type: string): void { + getInnerValue(type); } - `; - assertFailures(ruleName, source, [ - { - endPosition: { - character: 29, - line: 3 - }, - message: FAILURE_STRING, - startPosition: { - character: 12, - line: 2 + + @Component({ + selector: 'app-test', + template: '

Hey!

' + }) + class Test { + @CustomGetter + ~~~~~~~~~~~~~ + @Input() + get test(): string { + return this._test; } - }, - { - endPosition: { - character: 29, - line: 5 - }, - message: FAILURE_STRING, - startPosition: { - character: 12, - line: 4 + ~ + set test(value: string) { + this._test = value; + } + private _test: string; + + @CustomInput() get test1(): string { + return this._test1; + } + private _test1: string; + + constructor(@CustomDecorator @Attribute('label') + private label: string) {} + + makeSomething(): void { + this.makeInternalDoSomething(); + } + + private makeInternalDoSomething(): void {} + + @MyCustomDecorator @HostListener('change') veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLong(): void { + getValue('very long click'); + } + + @MyCustomDecorator + @HostListener('click') + handleClick(): void { + getValue('click'); } } - ]); + `; + assertAnnotated({ + message: FAILURE_STRING, + options: [ + { + methods: false, + 'parameter-properties': false, + parameters: false, + setters: false + } + ], + ruleName, + source + }); }); - }); - describe('blacklist', () => { - it('should fail if a property is not on the same line as its decorator, which is not blacklisted', () => { + it('should fail if a getter is not on the same line as its decorators, which are not on the safelist', () => { const source = ` + function getValue(@Parse + type: string): void { + getInnerValue(type); + } + + @Component({ + selector: 'app-test', + template: '

Hey!

' + }) class Test { - @Input('test') - ~~~~~~~~~~~~~~ - testVar: string; - ~~~~~~~~~~~~~~~~ + @CustomGetter + ~~~~~~~~~~~~~ + @Input() + ~~~~~~~~ + get test(): string { + return this._test; + } + ~ + set test(value: string) { + this._test = value; + } + private _test: string; + + @CustomInput() + get test1(): string { + return this._test1; + } + private _test1: string; + + constructor(@CustomDecorator @Attribute('label') + private label: string) {} + + makeSomething(): void { + this.makeInternalDoSomething(); + } + + private makeInternalDoSomething(): void {} + + @MyCustomDecorator @HostListener('change') veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLong(): void { + getValue('very long click'); + } + + @MyCustomDecorator + @HostListener('click') + handleClick(): void { + getValue('click'); + } + + @CmpDecorator + @MyCustomDecorator + handleCustomClick(): void { + getValue('custom click'); + } } `; assertAnnotated({ message: FAILURE_STRING, - options: [Decorators.Output], + options: [ + { + getters: { + safelist: ['CustomInput'] + }, + methods: false, + 'parameter-properties': false, + parameters: false, + setters: false + } + ], ruleName, source }); }); - }); - }); - describe('success', () => { - describe('common cases', () => { - it('should succeed if a property is on the same line as its decorator', () => { + it('should fail if multiple getters are not on the same line as their decorators', () => { const source = ` + function getValue(@Parse + type: string): void { + getInnerValue(type); + } + + @Component({ + selector: 'app-test', + template: '

Hey!

' + }) class Test { - @Input('test') testVar: string; + @Input() + get test(): string { + return this._test; + } + set test(value: string) { + this._test = value; + } + private _test: string; + + @CustomInput() + get test1(): string { + return this._test1; + } + private _test1: string; + + @Input() set test2(value: string) { + this._test2 = value; + } + private _test2: string; + + @Output() + private readonly testChange = new EventEmitter(); + + @Output() private readonly test1Change = new EventEmitter(); + + constructor(@CustomDecorator @Attribute('label') + private label: string, @Inject(Engine) engine: Engine, + @Attribute('test') + testParam: number) {} + + makeSomething(): void { + this.makeInternalDoSomething(); + } + + private makeInternalDoSomething(): void {} + + methodWithParamDecorators(@Parse + param1: string, @Parse param2: number): void { + + } + + @MyCustomDecorator @HostListener('change') veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLong(): void { + getValue('very long click'); + } + + @MyCustomDecorator + @HostListener('click') + handleClick(): void { + getValue('click'); + } + + @CmpDecorator + @CustomCmpDecorator + handleCustomClick(): void { + getValue('custom click'); + } + + @NgDecorator() + @Codelyzer + handleCustomClick2(): void { + getValue('custom click 2'); + } } `; - assertSuccess(ruleName, source); + assertFailures( + ruleName, + source, + [ + { + endPosition: { + character: 13, + line: 14 + }, + message: FAILURE_STRING, + startPosition: { + character: 12, + line: 11 + } + }, + { + endPosition: { + character: 13, + line: 23 + }, + message: FAILURE_STRING, + startPosition: { + character: 12, + line: 20 + } + } + ], + [ + { + methods: false, + 'parameter-properties': false, + parameters: false, + properties: false, + setters: false + } + ] + ); }); + }); - it('should succeed if multiple properties are on the same line as their decorators', () => { + describe('methods', () => { + it('should fail if a method is not on the same line as its decorator', () => { const source = ` + @Component({ + selector: 'app-test', + template: '

Hey!

' + }) class Test { - @Input('test1') testVar1: string; - @MyCustomDecorator() testVar2: string; + @HostListener('click') + ~~~~~~~~~~~~~~~~~~~~~~ + handleClick(): void { + doSomething('click'); + } + ~ } `; - assertSuccess(ruleName, source); + assertAnnotated({ + message: FAILURE_STRING, + ruleName, + source + }); }); - it('should succeed if a property starts on the same line as its decorator and ends on the next line', () => { + it('should fail if a method is not on the same line as its decorators', () => { const source = ` + function getValue(@Parse + type: string): void { + getInnerValue(type); + } + + @Component({ + selector: 'app-test', + template: '

Hey!

' + }) class Test { - @Input('test') testVar: string = - veryVeryVeryVeryVeryVeryVeryLongDefaultVariable; + @Input() + set test(value: string) { + this._test = value; + } + get test(): string { + return this._test; + } + private _test: string; + + @CustomInput() get test1(): string { + return this._test1; + } + private _test1: string; + + constructor(@CustomDecorator @Attribute('label') + private label: string) {} + + makeSomething(): void { + this.makeInternalDoSomething(); + } + + private makeInternalDoSomething(): void {} + + @MyCustomDecorator @HostListener('change') veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLong(): void { + getValue('very long click'); + } + + @MyCustomDecorator + ~~~~~~~~~~~~~~~~~~ + @HostListener('click') + handleClick(): void { + getValue('click'); + } + ~ } `; - assertSuccess(ruleName, source); + assertAnnotated({ + message: FAILURE_STRING, + options: [ + { + getters: false, + 'parameter-properties': false, + parameters: false, + setters: false + } + ], + ruleName, + source + }); }); - }); - describe('blacklist', () => { - it('should succeed if a property is not on the same line as its decorator, which is blacklisted', () => { + it('should fail if a method is not on the same line as its decorators, which are not on the safelist', () => { const source = ` + function getValue(@Parse + type: string): void { + getInnerValue(type); + } + + @Component({ + selector: 'app-test', + template: '

Hey!

' + }) class Test { - @Output() - test = new EventEmitter(); + @Input() + set test(value: string) { + this._test = value; + } + get test(): string { + return this._test; + } + private _test: string; + + @CustomInput() get test1(): string { + return this._test1; + } + private _test1: string; + + constructor(@CustomDecorator @Attribute('label') + private label: string) {} + + makeSomething(): void { + this.makeInternalDoSomething(); + } + + private makeInternalDoSomething(): void {} + + @MyCustomDecorator @HostListener('change') veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLong(): void { + getValue('very long click'); + } + + @MyCustomDecorator + ~~~~~~~~~~~~~~~~~~ + @HostListener('click') + handleClick(): void { + getValue('click'); + } + ~ + + @CmpDecorator + @MyCustomDecorator + handleCustomClick(): void { + getValue('custom click'); + } } `; - assertSuccess(ruleName, source, [Decorators.Output]); + assertAnnotated({ + message: FAILURE_STRING, + options: [ + { + getters: false, + methods: { + safelist: ['CmpDecorator'] + }, + 'parameter-properties': false, + parameters: false, + setters: false + } + ], + ruleName, + source + }); }); - }); - describe('special cases', () => { - it('should succeed if getter accessor starts on the same line as its decorator and ends on the next line', () => { + it('should fail if multiple methods are not on the same line as their decorators', () => { const source = ` + function getValue(@Parse + type: string): void { + getInnerValue(type); + } + + @Component({ + selector: 'app-test', + template: '

Hey!

' + }) class Test { - @Input() get test(): string { + @Input() + set test(value: string) { + this._test = value; + } + get test(): string { return this._test; } private _test: string; + + @CustomInput() get test1(): string { + return this._test1; + } + private _test1: string; + + @CustomInput() set test2(value: string) { + this._test2 = value; + } + private _test2: string; + + @Output() + private readonly testChange = new EventEmitter(); + + @Output() private readonly test1Change = new EventEmitter(); + + constructor(@CustomDecorator @Attribute('label') + private label: string, @Inject(Engine) engine: Engine, + @Attribute('test') + testParam: number) {} + + makeSomething(): void { + this.makeInternalDoSomething(); + } + + private makeInternalDoSomething(): void {} + + methodWithParamDecorators(@Parse + param1: string, @Parse param2: number): void { + + } + + @MyCustomDecorator @HostListener('change') veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLong(): void { + getValue('very long click'); + } + + @MyCustomDecorator + @HostListener('click') + handleClick(): void { + getValue('click'); + } + + @CmpDecorator @CustomCmpDecorator handleCustomClick(): void { + getValue('custom click'); + } + + @NgDecorator() + @Codelyzer + handleCustomClick2(): void { + getValue('custom click 2'); + } } `; - assertSuccess(ruleName, source); + assertFailures( + ruleName, + source, + [ + { + endPosition: { + character: 13, + line: 59 + }, + message: FAILURE_STRING, + startPosition: { + character: 12, + line: 55 + } + }, + { + endPosition: { + character: 13, + line: 69 + }, + message: FAILURE_STRING, + startPosition: { + character: 12, + line: 65 + } + } + ], + [ + { + getters: false, + 'parameter-properties': false, + parameters: false, + properties: false, + setters: false + } + ] + ); }); + }); - it('should succeed if setter accessor starts on the same line as its decorator and ends on the next line', () => { + describe('parameter-properties', () => { + it('should fail if a parameter property is not on the same line as its decorator', () => { const source = ` + @Component({ + selector: 'app-test', + template: '

Hey!

' + }) class Test { - @Input() set test(value: string) { - this._test = value; - } - private _test: string; + constructor(@Attribute('label') + ~~~~~~~~~~~~~~~~~~~ + private label: string) {} + ~ } `; - assertSuccess(ruleName, source); + assertAnnotated({ + message: FAILURE_STRING, + ruleName, + source + }); }); - it('should succeed if getters/setters accessors are not on the same line as their decorators', () => { + it('should fail if a parameter property is not on the same line as its decorators', () => { const source = ` + function getValue(@Parse + type: string): void { + getInnerValue(type); + } + + @Component({ + selector: 'app-test', + template: '

Hey!

' + }) class Test { @Input() - get test(): string { - return this._test; - } set test(value: string) { this._test = value; } + get test(): string { + return this._test; + } private _test: string; + + @CustomInput() get test1(): string { + return this._test1; + } + private _test1: string; + + constructor(@CustomDecorator @Attribute('label') + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + private label: string) {} + ~ + + makeSomething(): void { + this.makeInternalDoSomething(); + } + + private makeInternalDoSomething(): void {} + + @MyCustomDecorator @HostListener('change') veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLong(): void { + getValue('very long click'); + } + + @MyCustomDecorator + @HostListener('click') + handleClick(): void { + getValue('click'); + } } `; - assertSuccess(ruleName, source); + assertAnnotated({ + message: FAILURE_STRING, + options: [ + { + getters: false, + methods: false, + parameters: false, + setters: false + } + ], + ruleName, + source + }); }); - }); - }); - describe('replacements', () => { - it('should fail and apply proper replacements if a property is not on the same line as its decorator', () => { - const source = ` - class Test { - @Output() - ~~~~~~~~~ - test = new EventEmitter(); - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - } - `; - const failures = assertAnnotated({ - message: FAILURE_STRING, - ruleName, - source - }); - - if (!Array.isArray(failures)) return; - - const replacement = Replacement.applyFixes(source, failures.map(x => x.getFix()!)); - const expectedSource = ` - class Test { - @Output() test = new EventEmitter(); - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - } - `; - - expect(replacement).to.eq(expectedSource); + it('should fail if a parameter property is not on the same line as its decorators, which are not on the safelist', () => { + const source = ` + function getValue(@Parse + type: string): void { + getInnerValue(type); + } + + @Component({ + selector: 'app-test', + template: '

Hey!

' + }) + class Test { + @Input() + set test(value: string) { + this._test = value; + } + get test(): string { + return this._test; + } + private _test: string; + + @CustomInput() get test1(): string { + return this._test1; + } + private _test1: string; + + constructor(@CustomDecorator @Attribute('label') + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + private label: string, @Inject(Engine) engine: Engine, + ~ + @NgConstructor('test') + testParam: number) {} + + makeSomething(): void { + this.makeInternalDoSomething(); + } + + private makeInternalDoSomething(): void {} + + @MyCustomDecorator @HostListener('change') veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLong(): void { + getValue('very long click'); + } + + @MyCustomDecorator + @HostListener('click') + handleClick(): void { + getValue('click'); + } + + @CmpDecorator + @MyCustomDecorator + handleCustomClick(): void { + getValue('custom click'); + } + } + `; + assertAnnotated({ + message: FAILURE_STRING, + options: [ + { + getters: false, + methods: false, + 'parameter-properties': { + safelist: ['NgConstructor'] + }, + parameters: false, + setters: false + } + ], + ruleName, + source + }); + }); + + it('should fail if multiple parameter properties are not on the same line as their decorators', () => { + const source = ` + function getValue(@Parse + type: string): void { + getInnerValue(type); + } + + @Component({ + selector: 'app-test', + template: '

Hey!

' + }) + class Test { + @Input() + set test(value: string) { + this._test = value; + } + get test(): string { + return this._test; + } + private _test: string; + + @CustomInput() get test1(): string { + return this._test1; + } + private _test1: string; + + @CustomInput() set test2(value: string) { + this._test2 = value; + } + private _test2: string; + + @Output() + private readonly testChange = new EventEmitter(); + + @Output() private readonly test1Change = new EventEmitter(); + + constructor(@CustomDecorator @Attribute('label') + private label: string, @Inject(Engine) engine: Engine, + @Attribute('test') + testParam: number) {} + + makeSomething(): void { + this.makeInternalDoSomething(); + } + + private makeInternalDoSomething(): void {} + + methodWithParamDecorators(@Parse + param1: string, @Parse param2: number): void { + + } + + @MyCustomDecorator @HostListener('change') veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLong(): void { + getValue('very long click'); + } + + @MyCustomDecorator + @HostListener('click') + handleClick(): void { + getValue('click'); + } + + @CmpDecorator @CustomCmpDecorator handleCustomClick(): void { + getValue('custom click'); + } + + @NgDecorator() + @Codelyzer + handleCustomClick2(): void { + getValue('custom click 2'); + } + } + `; + assertFailures( + ruleName, + source, + [ + { + endPosition: { + character: 37, + line: 36 + }, + message: FAILURE_STRING, + startPosition: { + character: 24, + line: 35 + } + }, + { + endPosition: { + character: 40, + line: 37 + }, + message: FAILURE_STRING, + startPosition: { + character: 39, + line: 36 + } + } + ], + [ + { + getters: false, + methods: false, + parameters: false, + properties: false, + setters: false + } + ] + ); + }); + }); + + describe('parameters', () => { + it('should fail if a parameter is not on the same line as its decorator', () => { + const source = ` + function getValue(@Parse + ~~~~~~ + type: string): void { + ~ + getInnerValue(type); + } + `; + assertAnnotated({ + message: FAILURE_STRING, + ruleName, + source + }); + }); + + it('should fail if a parameter is not on the same line as its decorators', () => { + const source = ` + function getValue(@Parse + ~~~~~~ + type: string): void { + ~ + getInnerValue(type); + } + + @Component({ + selector: 'app-test', + template: '

Hey!

' + }) + class Test { + @Input() + set test(value: string) { + this._test = value; + } + get test(): string { + return this._test; + } + private _test: string; + + @CustomInput() get test1(): string { + return this._test1; + } + private _test1: string; + + constructor(@CustomDecorator @Attribute('label') + private label: string) {} + + makeSomething(): void { + this.makeInternalDoSomething(); + } + + private makeInternalDoSomething(): void {} + + @MyCustomDecorator @HostListener('change') veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLong(): void { + getValue('very long click'); + } + + @MyCustomDecorator + @HostListener('click') + handleClick(): void { + getValue('click'); + } + } + `; + assertAnnotated({ + message: FAILURE_STRING, + options: [ + { + getters: false, + methods: false, + 'parameter-properties': false, + setters: false + } + ], + ruleName, + source + }); + }); + + it('should fail if a parameter is not on the same line as its decorators, which are not on the safelist', () => { + const source = ` + function getValue(@Parse @Attr + ~~~~~~~~~~~~ + type: string): void { + ~ + getInnerValue(type); + } + + @Component({ + selector: 'app-test', + template: '

Hey!

' + }) + class Test { + @Input() + set test(value: string) { + this._test = value; + } + get test(): string { + return this._test; + } + private _test: string; + + @CustomInput() get test1(): string { + return this._test1; + } + private _test1: string; + + constructor(@CustomDecorator @Attribute('label') + private label: string) {} + + makeSomething(): void { + this.makeInternalDoSomething(); + } + + private makeInternalDoSomething(): void {} + + methodWithParamDecorators(@ParseStr + param1: string, @Parse param2: number): void { + + } + + @MyCustomDecorator @HostListener('change') veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLong(): void { + getValue('very long click'); + } + + @MyCustomDecorator + @HostListener('click') + handleClick(): void { + getValue('click'); + } + + @CmpDecorator + @MyCustomDecorator + handleCustomClick(): void { + getValue('custom click'); + } + } + `; + assertAnnotated({ + message: FAILURE_STRING, + options: [ + { + getters: false, + methods: false, + 'parameter-properties': false, + parameters: { + safelist: ['ParseStr'] + }, + setters: false + } + ], + ruleName, + source + }); + }); + + it('should fail if multiple parameters are not on the same line as their decorators', () => { + const source = ` + function getValue(@Parse + type: string): void { + getInnerValue(type); + } + + @Component({ + selector: 'app-test', + template: '

Hey!

' + }) + class Test { + @Input() + set test(value: string) { + this._test = value; + } + get test(): string { + return this._test; + } + private _test: string; + + @CustomInput() get test1(): string { + return this._test1; + } + private _test1: string; + + @CustomInput() set test2(value: string) { + this._test2 = value; + } + private _test2: string; + + @Output() + private readonly testChange = new EventEmitter(); + + @Output() private readonly test1Change = new EventEmitter(); + + constructor(@CustomDecorator @Attribute('label') + private label: string, @Inject(Engine) engine: Engine, + @Attribute('test') testParam: number) {} + + makeSomething(): void { + this.makeInternalDoSomething(); + } + + private makeInternalDoSomething(): void {} + + methodWithParamDecorators(@Parse + param1: string, @Parse param2: number): void { + + } + + @MyCustomDecorator @HostListener('change') veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLong(): void { + getValue('very long click'); + } + + @MyCustomDecorator + @HostListener('click') + handleClick(): void { + getValue('click'); + } + + @CmpDecorator @CustomCmpDecorator handleCustomClick(): void { + getValue('custom click'); + } + + @NgDecorator() + @Codelyzer + handleCustomClick2(): void { + getValue('custom click 2'); + } + } + `; + assertFailures( + ruleName, + source, + [ + { + endPosition: { + character: 24, + line: 2 + }, + message: FAILURE_STRING, + startPosition: { + character: 28, + line: 1 + } + }, + { + endPosition: { + character: 28, + line: 46 + }, + message: FAILURE_STRING, + startPosition: { + character: 38, + line: 45 + } + } + ], + [ + { + getters: false, + methods: false, + 'parameter-properties': false, + properties: false, + setters: false + } + ] + ); + }); + }); + + describe('properties', () => { + it('should fail if a property is not on the same line as its decorator', () => { + const source = ` + @Component({ + selector: 'app-test', + template: '

Hey!

' + }) + class Test { + @Output() + ~~~~~~~~~ + private readonly testChange = new EventEmitter(); + ~ + + @Output() private readonly test1Change = new EventEmitter(); + } + `; + assertAnnotated({ + message: FAILURE_STRING, + ruleName, + source + }); + }); + + it('should fail if a property is not on the same line as its decorators', () => { + const source = ` + function getValue(@Parse + type: string): void { + getInnerValue(type); + } + + @Component({ + selector: 'app-test', + template: '

Hey!

' + }) + class Test { + @Input() + set test(value: string) { + this._test = value; + } + get test(): string { + return this._test; + } + private _test: string; + + @CustomInput() get test1(): string { + return this._test1; + } + private _test1: string; + + @CustomOutput + ~~~~~~~~~~~~~ + @Output() + private readonly testChange = new EventEmitter(); + ~ + + @Output() private readonly test1Change = new EventEmitter(); + + constructor(@CustomDecorator @Attribute('label') + private label: string) {} + + makeSomething(): void { + this.makeInternalDoSomething(); + } + + private makeInternalDoSomething(): void {} + + @MyCustomDecorator @HostListener('change') veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLong(): void { + getValue('very long click'); + } + + @MyCustomDecorator + @HostListener('click') + handleClick(): void { + getValue('click'); + } + } + `; + assertAnnotated({ + message: FAILURE_STRING, + options: [ + { + getters: false, + methods: false, + 'parameter-properties': false, + parameters: false, + setters: false + } + ], + ruleName, + source + }); + }); + + it('should fail if a property is not on the same line as its decorators, which are not on the safelist', () => { + const source = ` + function getValue(@Parse + type: string): void { + getInnerValue(type); + } + + @Component({ + selector: 'app-test', + template: '

Hey!

' + }) + class Test { + @Input() + set test(value: string) { + this._test = value; + } + get test(): string { + return this._test; + } + private _test: string; + + @CustomInput() get test1(): string { + return this._test1; + } + private _test1: string; + + @CustomOutput + ~~~~~~~~~~~~~ + @Output() + private readonly testChange = new EventEmitter(); + ~ + + @MyOutput() + private readonly test1Change = new EventEmitter(); + + constructor(@CustomDecorator @Attribute('label') + private label: string) {} + + makeSomething(): void { + this.makeInternalDoSomething(); + } + + private makeInternalDoSomething(): void {} + + @MyCustomDecorator @HostListener('change') veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLong(): void { + getValue('very long click'); + } + + @MyCustomDecorator + @HostListener('click') + handleClick(): void { + getValue('click'); + } + + @CmpDecorator + @MyCustomDecorator + handleCustomClick(): void { + getValue('custom click'); + } + } + `; + assertAnnotated({ + message: FAILURE_STRING, + options: [ + { + getters: false, + methods: false, + 'parameter-properties': false, + parameters: false, + properties: { + safelist: ['MyOutput'] + }, + setters: false + } + ], + ruleName, + source + }); + }); + + it('should fail if multiple properties are not on the same line as their decorators', () => { + const source = ` + function getValue(@Parse + type: string): void { + getInnerValue(type); + } + + @Component({ + selector: 'app-test', + template: '

Hey!

' + }) + class Test { + @Input() + set test(value: string) { + this._test = value; + } + get test(): string { + return this._test; + } + private _test: string; + + @CustomInput() get test1(): string { + return this._test1; + } + private _test1: string; + + @CustomInput() set test2(value: string) { + this._test2 = value; + } + private _test2: string; + + @CustomOutput + @Output() + private readonly testChange = new EventEmitter(); + + @MyOutput @Output() + private readonly test1Change = new EventEmitter(); + + constructor(@CustomDecorator @Attribute('label') + private label: string, @Inject(Engine) engine: Engine, + @Attribute('test') + testParam: number) {} + + makeSomething(): void { + this.makeInternalDoSomething(); + } + + private makeInternalDoSomething(): void {} + + methodWithParamDecorators(@Parse + param1: string, @Parse param2: number): void { + + } + + @MyCustomDecorator @HostListener('change') veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLong(): void { + getValue('very long click'); + } + + @MyCustomDecorator + @HostListener('click') + handleClick(): void { + getValue('click'); + } + + @CmpDecorator @CustomCmpDecorator handleCustomClick(): void { + getValue('custom click'); + } + + @NgDecorator() + @Codelyzer + handleCustomClick2(): void { + getValue('custom click 2'); + } + } + `; + assertFailures( + ruleName, + source, + [ + { + endPosition: { + character: 69, + line: 32 + }, + message: FAILURE_STRING, + startPosition: { + character: 12, + line: 30 + } + }, + { + endPosition: { + character: 70, + line: 35 + }, + message: FAILURE_STRING, + startPosition: { + character: 12, + line: 34 + } + } + ], + [ + { + getters: false, + methods: false, + 'parameter-properties': false, + parameters: false, + setters: false + } + ] + ); + }); + }); + + describe('setters', () => { + it('should fail if a setter is not on the same line as its decorator', () => { + const source = ` + @Component({ + selector: 'app-test', + template: '

Hey!

' + }) + class Test { + @Input() + ~~~~~~~~ + set test(value: string) { + this._test = value; + } + ~ + private _test: string; + } + `; + assertAnnotated({ + message: FAILURE_STRING, + ruleName, + source + }); + }); + + it('should fail if a setter is not on the same line as its decorators', () => { + const source = ` + function getValue(@Parse + type: string): void { + getInnerValue(type); + } + + @Component({ + selector: 'app-test', + template: '

Hey!

' + }) + class Test { + @CustomSetter + ~~~~~~~~~~~~~ + @Input() + set test(value: string) { + this._test = value; + } + ~ + get test(): string { + return this._test; + } + private _test: string; + + @CustomInput() get test1(): string { + return this._test1; + } + private _test1: string; + + constructor(@CustomDecorator @Attribute('label') + private label: string) {} + + makeSomething(): void { + this.makeInternalDoSomething(); + } + + private makeInternalDoSomething(): void {} + + @MyCustomDecorator @HostListener('change') veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLong(): void { + getValue('very long click'); + } + + @MyCustomDecorator + @HostListener('click') + handleClick(): void { + getValue('click'); + } + } + `; + assertAnnotated({ + message: FAILURE_STRING, + options: [ + { + getters: false, + methods: false, + 'parameter-properties': false, + parameters: false + } + ], + ruleName, + source + }); + }); + + it('should fail if a setter is not on the same line as its decorators, which are not on the safelist', () => { + const source = ` + function getValue(@Parse + type: string): void { + getInnerValue(type); + } + + @Component({ + selector: 'app-test', + template: '

Hey!

' + }) + class Test { + @CustomSetter + ~~~~~~~~~~~~~ + @Input() + get test(): string { + return this._test; + } + ~ + set test(value: string) { + this._test = value; + } + private _test: string; + + @CustomInput() + set test(value: string) { + this._test1 = value; + } + + @Input() get test1(): string { + return this._test1; + } + + private _test1: string; + + constructor(@CustomDecorator @Attribute('label') + private label: string) {} + + makeSomething(): void { + this.makeInternalDoSomething(); + } + + private makeInternalDoSomething(): void {} + + @MyCustomDecorator @HostListener('change') veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLong(): void { + getValue('very long click'); + } + + @MyCustomDecorator + @HostListener('click') + handleClick(): void { + getValue('click'); + } + + @CmpDecorator + @MyCustomDecorator + handleCustomClick(): void { + getValue('custom click'); + } + } + `; + assertAnnotated({ + message: FAILURE_STRING, + options: [ + { + methods: false, + 'parameter-properties': false, + parameters: false, + setters: { + safelist: ['CustomInput'] + } + } + ], + ruleName, + source + }); + }); + + it('should fail if multiple setters are not on the same line as their decorators', () => { + const source = ` + function getValue(@Parse + type: string): void { + getInnerValue(type); + } + + @Component({ + selector: 'app-test', + template: '

Hey!

' + }) + class Test { + @CustomSetter + @Input() + set test(value: string) { + this._test = value; + } + get test(): string { + return this._test; + } + private _test: string; + + @CustomInput() + set test1(value: string) { + this._test1 = value; + } + private _test1: string; + + @Input() set test2(value: string) { + this._test2 = value; + } + private _test2: string; + + @Output() + private readonly testChange = new EventEmitter(); + + @Output() private readonly test1Change = new EventEmitter(); + + constructor(@CustomDecorator @Attribute('label') + private label: string, @Inject(Engine) engine: Engine, + @Attribute('test') + testParam: number) {} + + makeSomething(): void { + this.makeInternalDoSomething(); + } + + private makeInternalDoSomething(): void {} + + methodWithParamDecorators(@Parse + param1: string, @Parse param2: number): void { + + } + + @MyCustomDecorator @HostListener('change') veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLong(): void { + getValue('very long click'); + } + + @MyCustomDecorator + @HostListener('click') + handleClick(): void { + getValue('click'); + } + + @CmpDecorator + @CustomCmpDecorator + handleCustomClick(): void { + getValue('custom click'); + } + + @NgDecorator() + @Codelyzer + handleCustomClick2(): void { + getValue('custom click 2'); + } + } + `; + assertFailures( + ruleName, + source, + [ + { + endPosition: { + character: 13, + line: 15 + }, + message: FAILURE_STRING, + startPosition: { + character: 12, + line: 11 + } + }, + { + endPosition: { + character: 13, + line: 24 + }, + message: FAILURE_STRING, + startPosition: { + character: 12, + line: 21 + } + } + ], + [ + { + getters: false, + methods: false, + 'parameter-properties': false, + parameters: false, + properties: false + } + ] + ); + }); + }); + }); + + describe('success', () => { + describe('getters', () => { + it('should succeed if a getter is on the same line as its decorator', () => { + const source = ` + @Component({ + selector: 'app-test', + template: '

Hey!

' + }) + class Test { + @Input() get test(): string { + return this._test; + } + private _test: string; + } + `; + assertSuccess(ruleName, source); + }); + + it('should succeed if a getter is on the same line as its decorators', () => { + const source = ` + function getValue(@Parse + type: string): void { + getInnerValue(type); + } + + @Component({ + selector: 'app-test', + template: '

Hey!

' + }) + class Test { + @CustomGetter @Input() get test(): string { + return this._test; + } + set test(value: string) { + this._test = value; + } + private _test: string; + + @CustomInput() get test1(): string { + return this._test1; + } + private _test1: string; + + constructor(@CustomDecorator @Attribute('label') + private label: string) {} + + makeSomething(): void { + this.makeInternalDoSomething(); + } + + private makeInternalDoSomething(): void {} + + @MyCustomDecorator @HostListener('change') veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLong(): void { + getValue('very long click'); + } + + @MyCustomDecorator + @HostListener('click') + handleClick(): void { + getValue('click'); + } + } + `; + assertSuccess(ruleName, source, [ + { + methods: false, + 'parameter-properties': false, + parameters: false, + setters: false + } + ]); + }); + + it('should succeed if a getter is not on the same line as its decorators, which are on the safelist', () => { + const source = ` + function getValue(@Parse + type: string): void { + getInnerValue(type); + } + + @Component({ + selector: 'app-test', + template: '

Hey!

' + }) + class Test { + @CustomGetter + @Input() + get test(): string { + return this._test; + } + set test(value: string) { + this._test = value; + } + private _test: string; + + @CustomInput() + get test1(): string { + return this._test1; + } + private _test1: string; + + constructor(@CustomDecorator @Attribute('label') + private label: string) {} + + makeSomething(): void { + this.makeInternalDoSomething(); + } + + private makeInternalDoSomething(): void {} + + @MyCustomDecorator @HostListener('change') veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLong(): void { + getValue('very long click'); + } + + @MyCustomDecorator + @HostListener('click') + handleClick(): void { + getValue('click'); + } + + @CmpDecorator + @MyCustomDecorator + handleCustomClick(): void { + getValue('custom click'); + } + } + `; + assertSuccess(ruleName, source, [ + { + getters: { + safelist: ['CustomGetter', 'CustomInput'] + }, + methods: false, + 'parameter-properties': false, + parameters: false, + setters: false + } + ]); + }); + + it('should succeed if multiple getters are on the same line as their decorators', () => { + const source = ` + function getValue(@Parse + type: string): void { + getInnerValue(type); + } + + @Component({ + selector: 'app-test', + template: '

Hey!

' + }) + class Test { + @Input() get test(): string { + return this._test; + } + set test(value: string) { + this._test = value; + } + private _test: string; + + @CustomInput() get test1(): string { + return this._test1; + } + private _test1: string; + + @Input() set test2(value: string) { + this._test2 = value; + } + private _test2: string; + + @Output() + private readonly testChange = new EventEmitter(); + + @Output() private readonly test1Change = new EventEmitter(); + + constructor(@CustomDecorator @Attribute('label') + private label: string, @Inject(Engine) engine: Engine, + @Attribute('test') + testParam: number) {} + + makeSomething(): void { + this.makeInternalDoSomething(); + } + + private makeInternalDoSomething(): void {} + + methodWithParamDecorators(@Parse + param1: string, @Parse param2: number): void { + + } + + @MyCustomDecorator @HostListener('change') veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLong(): void { + getValue('very long click'); + } + + @MyCustomDecorator + @HostListener('click') + handleClick(): void { + getValue('click'); + } + + @CmpDecorator + @CustomCmpDecorator + handleCustomClick(): void { + getValue('custom click'); + } + + @NgDecorator() + @Codelyzer + handleCustomClick2(): void { + getValue('custom click 2'); + } + } + `; + assertSuccess(ruleName, source, [ + { + methods: false, + 'parameter-properties': false, + parameters: false, + properties: false, + setters: false + } + ]); + }); + }); + + describe('methods', () => { + it('should succeed if a method is on the same line as its decorator', () => { + const source = ` + @Component({ + selector: 'app-test', + template: '

Hey!

' + }) + class Test { + @HostListener('click') handleClick(): void { + doSomething('click'); + } + } + `; + assertSuccess(ruleName, source); + }); + + it('should succeed if a method is on the same line as its decorators', () => { + const source = ` + function getValue(@Parse + type: string): void { + getInnerValue(type); + } + + @Component({ + selector: 'app-test', + template: '

Hey!

' + }) + class Test { + @Input() + set test(value: string) { + this._test = value; + } + get test(): string { + return this._test; + } + private _test: string; + + @CustomInput() get test1(): string { + return this._test1; + } + private _test1: string; + + constructor(@CustomDecorator @Attribute('label') + private label: string) {} + + makeSomething(): void { + this.makeInternalDoSomething(); + } + + private makeInternalDoSomething(): void {} + + @MyCustomDecorator @HostListener('change') veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLong(): void { + getValue('very long click'); + } + + @MyCustomDecorator @HostListener('click') handleClick(): void { + getValue('click'); + } + } + `; + assertSuccess(ruleName, source, [ + { + getters: false, + 'parameter-properties': false, + parameters: false, + setters: false + } + ]); + }); + + it('should succeed if a method is on the same line as its decorators, which are on the safelist', () => { + const source = ` + function getValue(@Parse + type: string): void { + getInnerValue(type); + } + + @Component({ + selector: 'app-test', + template: '

Hey!

' + }) + class Test { + @Input() + set test(value: string) { + this._test = value; + } + get test(): string { + return this._test; + } + private _test: string; + + @CustomInput() get test1(): string { + return this._test1; + } + private _test1: string; + + constructor(@CustomDecorator @Attribute('label') + private label: string) {} + + makeSomething(): void { + this.makeInternalDoSomething(); + } + + private makeInternalDoSomething(): void {} + + @MyCustomDecorator @HostListener('change') veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLong(): void { + getValue('very long click'); + } + + @MyCustomDecorator @HostListener('click') handleClick(): void { + getValue('click'); + } + + @CmpDecorator + @MyCustomDecorator + handleCustomClick(): void { + getValue('custom click'); + } + } + `; + assertSuccess(ruleName, source, [ + { + getters: false, + methods: { + safelist: ['CmpDecorator'] + }, + 'parameter-properties': false, + parameters: false, + setters: false + } + ]); + }); + + it('should succeed if multiple methods are on the same line as their decorators', () => { + const source = ` + function getValue(@Parse + type: string): void { + getInnerValue(type); + } + + @Component({ + selector: 'app-test', + template: '

Hey!

' + }) + class Test { + @Input() + set test(value: string) { + this._test = value; + } + get test(): string { + return this._test; + } + private _test: string; + + @CustomInput() get test1(): string { + return this._test1; + } + private _test1: string; + + @CustomInput() set test2(value: string) { + this._test2 = value; + } + private _test2: string; + + @Output() + private readonly testChange = new EventEmitter(); + + @Output() private readonly test1Change = new EventEmitter(); + + constructor(@CustomDecorator @Attribute('label') + private label: string, @Inject(Engine) engine: Engine, + @Attribute('test') + testParam: number) {} + + makeSomething(): void { + this.makeInternalDoSomething(); + } + + private makeInternalDoSomething(): void {} + + methodWithParamDecorators(@Parse + param1: string, @Parse param2: number): void { + + } + + @MyCustomDecorator @HostListener('change') veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLong(): void { + getValue('very long click'); + } + + @MyCustomDecorator @HostListener('click') handleClick(): void { + getValue('click'); + } + + @CmpDecorator @CustomCmpDecorator handleCustomClick(): void { + getValue('custom click'); + } + + @NgDecorator() @Codelyzer handleCustomClick2(): void { + getValue('custom click 2'); + } + } + `; + assertSuccess(ruleName, source, [ + { + getters: false, + 'parameter-properties': false, + parameters: false, + properties: false, + setters: false + } + ]); + }); + }); + + describe('parameter-properties', () => { + it('should succeed if a parameter property is on the same line as its decorator', () => { + const source = ` + @Component({ + selector: 'app-test', + template: '

Hey!

' + }) + class Test { + constructor(@Attribute('label') private label: string) {} + } + `; + assertSuccess(ruleName, source); + }); + + it('should succeed if a parameter property is on the same line as its decorators', () => { + const source = ` + function getValue(@Parse + type: string): void { + getInnerValue(type); + } + + @Component({ + selector: 'app-test', + template: '

Hey!

' + }) + class Test { + @Input() + set test(value: string) { + this._test = value; + } + get test(): string { + return this._test; + } + private _test: string; + + @CustomInput() get test1(): string { + return this._test1; + } + private _test1: string; + + constructor(@CustomDecorator @Attribute('label') private label: string) {} + + makeSomething(): void { + this.makeInternalDoSomething(); + } + + private makeInternalDoSomething(): void {} + + @MyCustomDecorator @HostListener('change') veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLong(): void { + getValue('very long click'); + } + + @MyCustomDecorator + @HostListener('click') + handleClick(): void { + getValue('click'); + } + } + `; + assertSuccess(ruleName, source, [ + { + getters: false, + methods: false, + parameters: false, + setters: false + } + ]); + }); + + it('should succeed if a parameter property is on the same line as its decorators, which are on the safelist', () => { + const source = ` + function getValue(@Parse + type: string): void { + getInnerValue(type); + } + + @Component({ + selector: 'app-test', + template: '

Hey!

' + }) + class Test { + @Input() + set test(value: string) { + this._test = value; + } + get test(): string { + return this._test; + } + private _test: string; + + @CustomInput() get test1(): string { + return this._test1; + } + private _test1: string; + + constructor(@CustomDecorator @Attribute('label') private label: string, + @Inject(Engine) engine: Engine, + @NgConstructor('test') + testParam: number) {} + + makeSomething(): void { + this.makeInternalDoSomething(); + } + + private makeInternalDoSomething(): void {} + + @MyCustomDecorator @HostListener('change') veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLong(): void { + getValue('very long click'); + } + + @MyCustomDecorator + @HostListener('click') + handleClick(): void { + getValue('click'); + } + + @CmpDecorator + @MyCustomDecorator + handleCustomClick(): void { + getValue('custom click'); + } + } + `; + assertSuccess(ruleName, source, [ + { + getters: false, + methods: false, + 'parameter-properties': { + safelist: ['NgConstructor'] + }, + parameters: false, + setters: false + } + ]); + }); + + it('should succeed if multiple parameter properties are on the same line as their decorators', () => { + const source = ` + function getValue(@Parse + type: string): void { + getInnerValue(type); + } + + @Component({ + selector: 'app-test', + template: '

Hey!

' + }) + class Test { + @Input() + set test(value: string) { + this._test = value; + } + get test(): string { + return this._test; + } + private _test: string; + + @CustomInput() get test1(): string { + return this._test1; + } + private _test1: string; + + @CustomInput() set test2(value: string) { + this._test2 = value; + } + private _test2: string; + + @Output() + private readonly testChange = new EventEmitter(); + + @Output() private readonly test1Change = new EventEmitter(); + + constructor(@CustomDecorator @Attribute('label') private label: string, + @Inject(Engine) engine: Engine, + @Attribute('test') testParam: number) {} + + makeSomething(): void { + this.makeInternalDoSomething(); + } + + private makeInternalDoSomething(): void {} + + methodWithParamDecorators(@Parse + param1: string, @Parse param2: number): void { + + } + + @MyCustomDecorator @HostListener('change') veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLong(): void { + getValue('very long click'); + } + + @MyCustomDecorator + @HostListener('click') + handleClick(): void { + getValue('click'); + } + + @CmpDecorator @CustomCmpDecorator handleCustomClick(): void { + getValue('custom click'); + } + + @NgDecorator() + @Codelyzer + handleCustomClick2(): void { + getValue('custom click 2'); + } + } + `; + assertSuccess(ruleName, source, [ + { + getters: false, + methods: false, + parameters: false, + properties: false, + setters: false + } + ]); + }); + }); + + describe('parameters', () => { + it('should succeed if a parameter is on the same line as its decorator', () => { + const source = ` + function getValue(@Parse type: string): void { + getInnerValue(type); + } + `; + assertSuccess(ruleName, source); + }); + + it('should succeed if a parameter is on the same line as its decorators', () => { + const source = ` + function getValue(@Parse type: string): void { + getInnerValue(type); + } + + @Component({ + selector: 'app-test', + template: '

Hey!

' + }) + class Test { + @Input() + set test(value: string) { + this._test = value; + } + get test(): string { + return this._test; + } + private _test: string; + + @CustomInput() get test1(): string { + return this._test1; + } + private _test1: string; + + constructor(@CustomDecorator @Attribute('label') + private label: string) {} + + makeSomething(): void { + this.makeInternalDoSomething(); + } + + private makeInternalDoSomething(): void {} + + @MyCustomDecorator @HostListener('change') veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLong(): void { + getValue('very long click'); + } + + @MyCustomDecorator + @HostListener('click') + handleClick(): void { + getValue('click'); + } + } + `; + assertSuccess(ruleName, source, [ + { + getters: false, + methods: false, + 'parameter-properties': false, + setters: false + } + ]); + }); + + it('should succeed if a parameter is on the same line as its decorators, which are on the safelist', () => { + const source = ` + function getValue(@Parse @Attr type: string): void { + getInnerValue(type); + } + + @Component({ + selector: 'app-test', + template: '

Hey!

' + }) + class Test { + @Input() + set test(value: string) { + this._test = value; + } + get test(): string { + return this._test; + } + private _test: string; + + @CustomInput() get test1(): string { + return this._test1; + } + private _test1: string; + + constructor(@CustomDecorator @Attribute('label') + private label: string) {} + + makeSomething(): void { + this.makeInternalDoSomething(); + } + + private makeInternalDoSomething(): void {} + + methodWithParamDecorators(@ParseStr + param1: string, @Parse param2: number): void { + + } + + @MyCustomDecorator @HostListener('change') veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLong(): void { + getValue('very long click'); + } + + @MyCustomDecorator + @HostListener('click') + handleClick(): void { + getValue('click'); + } + + @CmpDecorator + @MyCustomDecorator + handleCustomClick(): void { + getValue('custom click'); + } + } + `; + assertSuccess(ruleName, source, [ + { + getters: false, + methods: false, + 'parameter-properties': false, + parameters: { + safelist: ['ParseStr'] + }, + setters: false + } + ]); + }); + + it('should succeed if multiple parameters are on the same line as their decorators', () => { + const source = ` + function getValue(@Parse type: string): void { + getInnerValue(type); + } + + @Component({ + selector: 'app-test', + template: '

Hey!

' + }) + class Test { + @Input() + set test(value: string) { + this._test = value; + } + get test(): string { + return this._test; + } + private _test: string; + + @CustomInput() get test1(): string { + return this._test1; + } + private _test1: string; + + @CustomInput() set test2(value: string) { + this._test2 = value; + } + private _test2: string; + + @Output() + private readonly testChange = new EventEmitter(); + + @Output() private readonly test1Change = new EventEmitter(); + + constructor(@CustomDecorator @Attribute('label') + private label: string, @Inject(Engine) engine: Engine, + @Attribute('test') testParam: number) {} + + makeSomething(): void { + this.makeInternalDoSomething(); + } + + private makeInternalDoSomething(): void {} + + methodWithParamDecorators(@Parse param1: string, @Parse param2: number): void { + + } + + @MyCustomDecorator @HostListener('change') veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLong(): void { + getValue('very long click'); + } + + @MyCustomDecorator + @HostListener('click') + handleClick(): void { + getValue('click'); + } + + @CmpDecorator @CustomCmpDecorator handleCustomClick(): void { + getValue('custom click'); + } + + @NgDecorator() + @Codelyzer + handleCustomClick2(): void { + getValue('custom click 2'); + } + } + `; + assertSuccess(ruleName, source, [ + { + getters: false, + methods: false, + 'parameter-properties': false, + properties: false, + setters: false + } + ]); + }); + }); + + describe('properties', () => { + it('should succeed if a property is on the same line as its decorator', () => { + const source = ` + @Component({ + selector: 'app-test', + template: '

Hey!

' + }) + class Test { + @Output() private readonly testChange = new EventEmitter(); + + @Output() private readonly test1Change = new EventEmitter(); + } + `; + assertSuccess(ruleName, source); + }); + + it('should succeed if a property is on the same line as its decorators', () => { + const source = ` + function getValue(@Parse + type: string): void { + getInnerValue(type); + } + + @Component({ + selector: 'app-test', + template: '

Hey!

' + }) + class Test { + @Input() + set test(value: string) { + this._test = value; + } + get test(): string { + return this._test; + } + private _test: string; + + @CustomInput() get test1(): string { + return this._test1; + } + private _test1: string; + + @CustomOutput @Output() private readonly testChange = new EventEmitter(); + + @Output() private readonly test1Change = new EventEmitter(); + + constructor(@CustomDecorator @Attribute('label') + private label: string) {} + + makeSomething(): void { + this.makeInternalDoSomething(); + } + + private makeInternalDoSomething(): void {} + + @MyCustomDecorator @HostListener('change') veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLong(): void { + getValue('very long click'); + } + + @MyCustomDecorator + @HostListener('click') + handleClick(): void { + getValue('click'); + } + } + `; + assertSuccess(ruleName, source, [ + { + getters: false, + methods: false, + 'parameter-properties': false, + parameters: false, + setters: false + } + ]); + }); + + it('should succeed if a property is on the same line as its decorators, which are on the safelist', () => { + const source = ` + function getValue(@Parse + type: string): void { + getInnerValue(type); + } + + @Component({ + selector: 'app-test', + template: '

Hey!

' + }) + class Test { + @Input() + set test(value: string) { + this._test = value; + } + get test(): string { + return this._test; + } + private _test: string; + + @CustomInput() get test1(): string { + return this._test1; + } + private _test1: string; + + @CustomOutput @Output() private readonly testChange = new EventEmitter(); + + @MyOutput() + private readonly test1Change = new EventEmitter(); + + constructor(@CustomDecorator @Attribute('label') + private label: string) {} + + makeSomething(): void { + this.makeInternalDoSomething(); + } + + private makeInternalDoSomething(): void {} + + @MyCustomDecorator @HostListener('change') veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLong(): void { + getValue('very long click'); + } + + @MyCustomDecorator + @HostListener('click') + handleClick(): void { + getValue('click'); + } + + @CmpDecorator + @MyCustomDecorator + handleCustomClick(): void { + getValue('custom click'); + } + } + `; + assertSuccess(ruleName, source, [ + { + getters: false, + methods: false, + 'parameter-properties': false, + parameters: false, + properties: { + safelist: ['MyOutput'] + }, + setters: false + } + ]); + }); + + it('should succeed if multiple properties are on the same line as their decorators', () => { + const source = ` + function getValue(@Parse + type: string): void { + getInnerValue(type); + } + + @Component({ + selector: 'app-test', + template: '

Hey!

' + }) + class Test { + @Input() + set test(value: string) { + this._test = value; + } + get test(): string { + return this._test; + } + private _test: string; + + @CustomInput() get test1(): string { + return this._test1; + } + private _test1: string; + + @CustomInput() set test2(value: string) { + this._test2 = value; + } + private _test2: string; + + @CustomOutput @Output() private readonly testChange = new EventEmitter(); + + @MyOutput @Output() private readonly test1Change = new EventEmitter(); + + constructor(@CustomDecorator @Attribute('label') + private label: string, @Inject(Engine) engine: Engine, + @Attribute('test') + testParam: number) {} + + makeSomething(): void { + this.makeInternalDoSomething(); + } + + private makeInternalDoSomething(): void {} + + methodWithParamDecorators(@Parse + param1: string, @Parse param2: number): void { + + } + + @MyCustomDecorator @HostListener('change') veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLong(): void { + getValue('very long click'); + } + + @MyCustomDecorator + @HostListener('click') + handleClick(): void { + getValue('click'); + } + + @CmpDecorator @CustomCmpDecorator handleCustomClick(): void { + getValue('custom click'); + } + + @NgDecorator() + @Codelyzer + handleCustomClick2(): void { + getValue('custom click 2'); + } + } + `; + assertSuccess(ruleName, source, [ + { + getters: false, + methods: false, + 'parameter-properties': false, + parameters: false, + setters: false + } + ]); + }); + }); + + describe('setters', () => { + it('should succeed if a setter is on the same line as its decorator', () => { + const source = ` + @Component({ + selector: 'app-test', + template: '

Hey!

' + }) + class Test { + @Input() set test(value: string) { + this._test = value; + } + private _test: string; + } + `; + assertSuccess(ruleName, source); + }); + + it('should succeed if a setter is on the same line as its decorators', () => { + const source = ` + function getValue(@Parse + type: string): void { + getInnerValue(type); + } + + @Component({ + selector: 'app-test', + template: '

Hey!

' + }) + class Test { + @CustomSetter @Input() set test(value: string) { + this._test = value; + } + get test(): string { + return this._test; + } + private _test: string; + + @CustomInput() get test1(): string { + return this._test1; + } + private _test1: string; + + constructor(@CustomDecorator @Attribute('label') + private label: string) {} + + makeSomething(): void { + this.makeInternalDoSomething(); + } + + private makeInternalDoSomething(): void {} + + @MyCustomDecorator @HostListener('change') veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLong(): void { + getValue('very long click'); + } + + @MyCustomDecorator + @HostListener('click') + handleClick(): void { + getValue('click'); + } + } + `; + assertSuccess(ruleName, source, [ + { + getters: false, + methods: false, + 'parameter-properties': false, + parameters: false + } + ]); + }); + + it('should succeed if a setter is on the same line as its decorators, which are on the safelist', () => { + const source = ` + function getValue(@Parse + type: string): void { + getInnerValue(type); + } + + @Component({ + selector: 'app-test', + template: '

Hey!

' + }) + class Test { + @CustomSetter @Input() get test(): string { + return this._test; + } + set test(value: string) { + this._test = value; + } + private _test: string; + + @CustomInput() + set test(value: string) { + this._test1 = value; + } + + @Input() get test1(): string { + return this._test1; + } + + private _test1: string; + + constructor(@CustomDecorator @Attribute('label') + private label: string) {} + + makeSomething(): void { + this.makeInternalDoSomething(); + } + + private makeInternalDoSomething(): void {} + + @MyCustomDecorator @HostListener('change') veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLong(): void { + getValue('very long click'); + } + + @MyCustomDecorator + @HostListener('click') + handleClick(): void { + getValue('click'); + } + + @CmpDecorator + @MyCustomDecorator + handleCustomClick(): void { + getValue('custom click'); + } + } + `; + assertSuccess(ruleName, source, [ + { + methods: false, + 'parameter-properties': false, + parameters: false, + setters: { + safelist: ['CustomInput'] + } + } + ]); + }); + + it('should succeed if multiple setters are on the same line as their decorators', () => { + const source = ` + function getValue(@Parse + type: string): void { + getInnerValue(type); + } + + @Component({ + selector: 'app-test', + template: '

Hey!

' + }) + class Test { + @CustomSetter @Input() set test(value: string) { + this._test = value; + } + get test(): string { + return this._test; + } + private _test: string; + + @CustomInput() set test1(value: string) { + this._test1 = value; + } + private _test1: string; + + @Input() set test2(value: string) { + this._test2 = value; + } + private _test2: string; + + @Output() + private readonly testChange = new EventEmitter(); + + @Output() private readonly test1Change = new EventEmitter(); + + constructor(@CustomDecorator @Attribute('label') + private label: string, @Inject(Engine) engine: Engine, + @Attribute('test') + testParam: number) {} + + makeSomething(): void { + this.makeInternalDoSomething(); + } + + private makeInternalDoSomething(): void {} + + methodWithParamDecorators(@Parse + param1: string, @Parse param2: number): void { + + } + + @MyCustomDecorator @HostListener('change') veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLong(): void { + getValue('very long click'); + } + + @MyCustomDecorator + @HostListener('click') + handleClick(): void { + getValue('click'); + } + + @CmpDecorator + @CustomCmpDecorator + handleCustomClick(): void { + getValue('custom click'); + } + + @NgDecorator() + @Codelyzer + handleCustomClick2(): void { + getValue('custom click 2'); + } + } + `; + assertSuccess(ruleName, source, [ + { + getters: false, + methods: false, + 'parameter-properties': false, + parameters: false, + properties: false + } + ]); + }); }); }); });