Skip to content

Commit 5e34f41

Browse files
rafaelss95mgechev
authored andcommitted
fix(rule): some cases not being reported by no-output-rename rule (#614)
1 parent af52912 commit 5e34f41

File tree

3 files changed

+101
-47
lines changed

3 files changed

+101
-47
lines changed

src/noInputRenameRule.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,10 @@ export const getFailureMessage = (className: string, propertyName: string): stri
3030
};
3131

3232
export class InputMetadataWalker extends NgWalker {
33-
private directiveSelectors: DirectiveMetadata['selector'][];
33+
private directiveSelectors: ReadonlySet<DirectiveMetadata['selector']>;
3434

3535
protected visitNgDirective(metadata: DirectiveMetadata): void {
36-
this.directiveSelectors = (metadata.selector || '').replace(/[\[\]\s]/g, '').split(',');
36+
this.directiveSelectors = new Set((metadata.selector || '').replace(/[\[\]\s]/g, '').split(','));
3737
super.visitNgDirective(metadata);
3838
}
3939

@@ -43,7 +43,7 @@ export class InputMetadataWalker extends NgWalker {
4343
}
4444

4545
private canPropertyBeAliased(propertyAlias: string, propertyName: string): boolean {
46-
return !!(this.directiveSelectors && this.directiveSelectors.indexOf(propertyAlias) !== -1 && propertyAlias !== propertyName);
46+
return !!(this.directiveSelectors && this.directiveSelectors.has(propertyAlias) && propertyAlias !== propertyName);
4747
}
4848

4949
private validateInput(property: ts.PropertyDeclaration, input: ts.Decorator, args: string[]) {

src/noOutputRenameRule.ts

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,55 @@
1-
import * as Lint from 'tslint';
2-
import * as ts from 'typescript';
3-
import { sprintf } from 'sprintf-js';
1+
import { IRuleMetadata, RuleFailure, Rules } from 'tslint/lib';
2+
import { Decorator, PropertyDeclaration, SourceFile } from 'typescript/lib/typescript';
3+
import { DirectiveMetadata } from './angular/metadata';
44
import { NgWalker } from './angular/ngWalker';
55

6-
export class Rule extends Lint.Rules.AbstractRule {
7-
public static metadata: Lint.IRuleMetadata = {
8-
ruleName: 'no-output-rename',
9-
type: 'maintainability',
6+
export class Rule extends Rules.AbstractRule {
7+
static readonly metadata: IRuleMetadata = {
108
description: 'Disallows renaming directive outputs by providing a string to the decorator.',
119
descriptionDetails: 'See more at https://angular.io/styleguide#style-05-13.',
12-
rationale: 'Two names for the same property (one private, one public) is inherently confusing.',
1310
options: null,
1411
optionsDescription: 'Not configurable.',
12+
rationale: 'Two names for the same property (one private, one public) is inherently confusing.',
13+
ruleName: 'no-output-rename',
14+
type: 'maintainability',
1515
typescriptOnly: true
1616
};
1717

18-
static FAILURE_STRING: string = 'In the class "%s", the directive output ' +
19-
'property "%s" should not be renamed.' +
20-
'Please, consider the following use "@Output() %s = new EventEmitter();"';
18+
static readonly FAILURE_STRING = '@Outputs should not be renamed';
2119

22-
public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
20+
apply(sourceFile: SourceFile): RuleFailure[] {
2321
return this.applyWithWalker(new OutputMetadataWalker(sourceFile, this.getOptions()));
2422
}
2523
}
2624

25+
export const getFailureMessage = (): string => {
26+
return Rule.FAILURE_STRING;
27+
};
28+
2729
export class OutputMetadataWalker extends NgWalker {
28-
protected visitNgOutput(property: ts.PropertyDeclaration, output: ts.Decorator, args: string[]) {
29-
let className = (<any>property).parent.name.text;
30-
let memberName = (<any>property.name).text;
31-
if (args.length !== 0 && memberName !== args[0]) {
32-
let failureConfig: string[] = [className, memberName, memberName];
33-
failureConfig.unshift(Rule.FAILURE_STRING);
34-
this.addFailure(this.createFailure(property.getStart(), property.getWidth(), sprintf.apply(this, failureConfig)));
35-
}
30+
private directiveSelectors: ReadonlySet<DirectiveMetadata['selector']>;
3631

32+
visitNgDirective(metadata: DirectiveMetadata): void {
33+
this.directiveSelectors = new Set((metadata.selector || '').replace(/[\[\]\s]/g, '').split(','));
34+
super.visitNgDirective(metadata);
35+
}
36+
37+
protected visitNgOutput(property: PropertyDeclaration, output: Decorator, args: string[]) {
38+
this.validateOutput(property, output, args);
3739
super.visitNgOutput(property, output, args);
3840
}
41+
42+
private canPropertyBeAliased(propertyAlias: string, propertyName: string): boolean {
43+
return !!(this.directiveSelectors && this.directiveSelectors.has(propertyAlias) && propertyAlias !== propertyName);
44+
}
45+
46+
private validateOutput(property: PropertyDeclaration, output: Decorator, args: string[]) {
47+
const propertyName = property.name.getText();
48+
49+
if (args.length === 0 || this.canPropertyBeAliased(args[0], propertyName)) {
50+
return;
51+
}
52+
53+
this.addFailureAtNode(property, getFailureMessage());
54+
}
3955
}

test/noOutputRenameRule.spec.ts

Lines changed: 62 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,79 @@
1-
import { assertSuccess, assertAnnotated } from './testHelper';
1+
import { getFailureMessage, Rule } from '../src/noOutputRenameRule';
2+
import { assertAnnotated, assertSuccess } from './testHelper';
23

3-
describe('no-output-rename', () => {
4-
describe('invalid directive output property', () => {
5-
it('should fail, when a directive output property is renamed', () => {
6-
let source = `
7-
class ButtonComponent {
8-
@Output('changeEvent') change = new EventEmitter<any>();
9-
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4+
const {
5+
metadata: { ruleName }
6+
} = Rule;
7+
8+
describe(ruleName, () => {
9+
describe('failure', () => {
10+
it('should fail when a directive output property is renamed', () => {
11+
const source = `
12+
@Component({
13+
template: 'test'
14+
})
15+
class TestComponent {
16+
@Output('onChange') change = new EventEmitter<void>();
17+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1018
}
1119
`;
1220
assertAnnotated({
13-
ruleName: 'no-output-rename',
14-
message:
15-
'In the class "ButtonComponent", the directive output property "change" should not be renamed.' +
16-
'Please, consider the following use "@Output() change = new EventEmitter();"',
21+
message: getFailureMessage(),
22+
ruleName,
1723
source
1824
});
1925
});
20-
});
2126

22-
describe('valid directive output property', () => {
23-
it('should succeed, when a directive output property is properly used', () => {
24-
let source = `
25-
class ButtonComponent {
26-
@Output() change = new EventEmitter<any>();
27+
it('should fail when a directive output property is renamed and its name is strictly equal to the property', () => {
28+
const source = `
29+
@Component({
30+
template: 'test'
31+
})
32+
class TestComponent {
33+
@Output('change') change = new EventEmitter<void>();
34+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
2735
}
2836
`;
29-
assertSuccess('no-output-rename', source);
37+
assertAnnotated({ message: getFailureMessage(), ruleName, source });
3038
});
3139

32-
it('should succeed, when a directive output property rename is the same as the property name', () => {
33-
let source = `
34-
class ButtonComponent {
35-
@Output('change') change = new EventEmitter<any>();
40+
it("should fail when the directive's selector matches exactly both property name and the alias", () => {
41+
const source = `
42+
@Directive({
43+
selector: '[test], foo'
44+
})
45+
class TestDirective {
46+
@Output('test') test = new EventEmitter<void>();
47+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3648
}
3749
`;
38-
assertSuccess('no-output-rename', source);
50+
assertAnnotated({ message: getFailureMessage(), ruleName, source });
51+
});
52+
});
53+
54+
describe('success', () => {
55+
it('should succeed when a directive output property is properly used', () => {
56+
const source = `
57+
@Component({
58+
template: 'test'
59+
})
60+
class TestComponent {
61+
@Output() change = new EventEmitter<void>();
62+
}
63+
`;
64+
assertSuccess(ruleName, source);
65+
});
66+
67+
it("should succeed when the directive's selector is also an output property", () => {
68+
const source = `
69+
@Directive({
70+
selector: '[foo], test'
71+
})
72+
class TestDirective {
73+
@Output('foo') bar = new EventEmitter<void>();
74+
}
75+
`;
76+
assertSuccess(ruleName, source);
3977
});
4078
});
4179
});

0 commit comments

Comments
 (0)