diff --git a/src/index.ts b/src/index.ts index 749228e29..04b91ab92 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,6 +21,7 @@ export { Rule as NoOutputRenameRule } from './noOutputRenameRule'; export { Rule as NoUnusedCssRule } from './noUnusedCssRule'; export { Rule as PipeImpureRule } from './pipeImpureRule'; export { Rule as PipeNamingRule } from './pipeNamingRule'; +export { Rule as PreferOutputReadonlyRule } from './preferOutputReadonlyRule'; export { Rule as TemplateConditionalComplexityRule } from './templateConditionalComplexityRule'; export { Rule as TemplateCyclomaticComplexityRule } from './templateCyclomaticComplexityRule'; export { Rule as TemplatesNoNegatedAsync } from './templatesNoNegatedAsyncRule'; diff --git a/src/preferOutputReadonlyRule.ts b/src/preferOutputReadonlyRule.ts new file mode 100644 index 000000000..6934775c4 --- /dev/null +++ b/src/preferOutputReadonlyRule.ts @@ -0,0 +1,34 @@ +import * as Lint from 'tslint'; +import * as ts from 'typescript'; +import { NgWalker } from './angular/ngWalker'; + +export class Rule extends Lint.Rules.AbstractRule { + public static metadata: Lint.IRuleMetadata = { + description: 'Prefer to declare `@Output` as readonly since they are not supposed to be reassigned.', + options: null, + optionsDescription: 'Not configurable.', + rationale: '', + ruleName: 'prefer-output-readonly', + type: 'maintainability', + typescriptOnly: true, + }; + + static FAILURE_STRING = 'Prefer to declare `@Output` as readonly since they are not supposed to be reassigned'; + + public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { + return this.applyWithWalker(new OutputMetadataWalker(sourceFile, this.getOptions())); + } +} + +export class OutputMetadataWalker extends NgWalker { + visitNgOutput(property: ts.PropertyDeclaration, output: ts.Decorator, args: string[]) { + if (property.modifiers && property.modifiers.some(m => m.kind === ts.SyntaxKind.ReadonlyKeyword)) { + return; + } + + const className = (property.parent as ts.PropertyAccessExpression).name.getText(); + const memberName = property.name.getText(); + this.addFailureAtNode(property.name, Rule.FAILURE_STRING); + super.visitNgOutput(property, output, args); + } +} diff --git a/test/preferOutputReadonlyRule.spec.ts b/test/preferOutputReadonlyRule.spec.ts new file mode 100644 index 000000000..892e67ec9 --- /dev/null +++ b/test/preferOutputReadonlyRule.spec.ts @@ -0,0 +1,32 @@ +import { assertAnnotated, assertSuccess } from './testHelper'; + +const ruleName = 'prefer-output-readonly'; + +describe(ruleName, () => { + describe('failure', () => { + it('should fail when an @Output is not readonly', () => { + const source = ` + class Test { + @Output() testEmitter = new EventEmitter(); + ~~~~~~~~~~~ + } + `; + assertAnnotated({ + ruleName, + message: 'Prefer to declare `@Output` as readonly since they are not supposed to be reassigned', + source + }); + }); + }); + + describe('success', () => { + it('should pass when an @Output is readonly', () => { + const source = ` + class Test { + @Output() readonly testEmitter = new EventEmitter(); + } + `; + assertSuccess(ruleName, source); + }); + }); +});