Skip to content

Commit 71660ae

Browse files
sneasmgechev
authored andcommittedAug 2, 2018
feat(rule): add pipe-prefix rule (#693)
* feat(rule): add pipe-prefix rule * docs(pipe-naming): mention pipe-prefix in pipe-naming deprecation * test(pipe-prefix): fix assertion error
1 parent a9d74be commit 71660ae

File tree

4 files changed

+204
-1
lines changed

4 files changed

+204
-1
lines changed
 

‎src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export { Rule as NoTemplateCallExpressionRule } from './noTemplateCallExpression
2424
export { Rule as NoUnusedCssRule } from './noUnusedCssRule';
2525
export { Rule as PipeImpureRule } from './pipeImpureRule';
2626
export { Rule as PipeNamingRule } from './pipeNamingRule';
27+
export { Rule as PipePrefixRule } from './pipePrefixRule';
2728
export { Rule as PreferInlineDecorator } from './preferInlineDecoratorRule';
2829
export { Rule as PreferOutputReadonlyRule } from './preferOutputReadonlyRule';
2930
export { Rule as TemplateConditionalComplexityRule } from './templateConditionalComplexityRule';

‎src/pipeNamingRule.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const OPTION_KEBAB_CASE = 'kebab-case';
1111

1212
export class Rule extends Lint.Rules.AbstractRule {
1313
static readonly metadata: Lint.IRuleMetadata = {
14-
deprecationMessage: `You can name your pipes only ${OPTION_CAMEL_CASE}. If you try to use snake-case then your application will not compile.`,
14+
deprecationMessage: `You can name your pipes only ${OPTION_CAMEL_CASE}. If you try to use snake-case then your application will not compile. For prefix validation use pipe-prefix rule.`,
1515
description: 'Enforce consistent case and prefix for pipes.',
1616
optionExamples: [
1717
[true, OPTION_CAMEL_CASE, 'myPrefix'],

‎src/pipePrefixRule.ts

+98
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { sprintf } from 'sprintf-js';
2+
import * as Lint from 'tslint';
3+
import * as ts from 'typescript';
4+
import { NgWalker } from './angular/ngWalker';
5+
import { SelectorValidator } from './util/selectorValidator';
6+
import { getDecoratorArgument } from './util/utils';
7+
8+
export class Rule extends Lint.Rules.AbstractRule {
9+
static readonly metadata: Lint.IRuleMetadata = {
10+
description: 'Enforce consistent prefix for pipes.',
11+
optionExamples: [[true, 'myPrefix'], [true, 'myPrefix', 'myOtherPrefix']],
12+
options: {
13+
items: [
14+
{
15+
type: 'string'
16+
}
17+
],
18+
minLength: 1,
19+
type: 'array'
20+
},
21+
optionsDescription: Lint.Utils.dedent`
22+
* The list of arguments are supported prefixes (given as strings).
23+
`,
24+
rationale: 'Consistent conventions make it easy to quickly identify and reference assets of different types.',
25+
ruleName: 'pipe-prefix',
26+
type: 'style',
27+
typescriptOnly: true
28+
};
29+
30+
static FAILURE_STRING = `The name of the Pipe decorator of class %s should start with prefix %s, however its value is "%s"`;
31+
32+
prefix: string;
33+
private prefixChecker: Function;
34+
35+
constructor(options: Lint.IOptions) {
36+
super(options);
37+
38+
let args = options.ruleArguments;
39+
if (!(args instanceof Array)) {
40+
args = [args];
41+
}
42+
this.prefix = args.join(',');
43+
let prefixExpression = args.join('|');
44+
this.prefixChecker = SelectorValidator.prefix(prefixExpression, 'camelCase');
45+
}
46+
47+
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
48+
return this.applyWithWalker(new ClassMetadataWalker(sourceFile, this));
49+
}
50+
51+
isEnabled(): boolean {
52+
const {
53+
metadata: {
54+
options: { minLength }
55+
}
56+
} = Rule;
57+
const { length } = this.ruleArguments;
58+
59+
return super.isEnabled() && length >= minLength;
60+
}
61+
62+
validatePrefix(prefix: string): boolean {
63+
return this.prefixChecker(prefix);
64+
}
65+
}
66+
67+
export class ClassMetadataWalker extends NgWalker {
68+
constructor(sourceFile: ts.SourceFile, private rule: Rule) {
69+
super(sourceFile, rule.getOptions());
70+
}
71+
72+
protected visitNgPipe(controller: ts.ClassDeclaration, decorator: ts.Decorator) {
73+
let className = controller.name!.text;
74+
this.validateProperties(className, decorator);
75+
super.visitNgPipe(controller, decorator);
76+
}
77+
78+
private validateProperties(className: string, pipe: ts.Decorator) {
79+
const argument = getDecoratorArgument(pipe)!;
80+
81+
argument.properties
82+
.filter(p => p.name && ts.isIdentifier(p.name) && p.name.text === 'name')
83+
.forEach(this.validateProperty.bind(this, className));
84+
}
85+
86+
private validateProperty(className: string, property: ts.Node) {
87+
const initializer = ts.isPropertyAssignment(property) ? property.initializer : undefined;
88+
89+
if (initializer && ts.isStringLiteral(initializer)) {
90+
const propName = initializer.text;
91+
const isValid = this.rule.validatePrefix(propName);
92+
93+
if (!isValid) {
94+
this.addFailureAtNode(property, sprintf(Rule.FAILURE_STRING, className, this.rule.prefix, propName));
95+
}
96+
}
97+
}
98+
}

‎test/pipePrefixRule.spec.ts

+104
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { assertSuccess, assertAnnotated } from './testHelper';
2+
3+
describe('pipe-prefix', () => {
4+
describe('invalid pipe name', () => {
5+
it('should fail when Pipe has no prefix ng', () => {
6+
let source = `
7+
@Pipe({
8+
name: 'foo-bar'
9+
~~~~~~~~~~~~~~~
10+
})
11+
class Test {}
12+
`;
13+
assertAnnotated({
14+
ruleName: 'pipe-prefix',
15+
message: 'The name of the Pipe decorator of class Test should start with prefix ng, however its value is "foo-bar"',
16+
source,
17+
options: ['ng']
18+
});
19+
});
20+
21+
it('should fail when Pipe has no prefix applying multiple prefixes', () => {
22+
let source = `
23+
@Pipe({
24+
name: 'foo-bar'
25+
~~~~~~~~~~~~~~~
26+
})
27+
class Test {}
28+
`;
29+
assertAnnotated({
30+
ruleName: 'pipe-prefix',
31+
message: 'The name of the Pipe decorator of class Test should start' + ' with prefix ng,mg,sg, however its value is "foo-bar"',
32+
source,
33+
options: ['ng', 'mg', 'sg']
34+
});
35+
});
36+
});
37+
38+
describe('empty pipe', () => {
39+
it('should not fail when @Pipe not invoked', () => {
40+
let source = `
41+
@Pipe
42+
class Test {}
43+
`;
44+
assertSuccess('pipe-prefix', source, ['ng']);
45+
});
46+
});
47+
48+
describe('pipe with name as variable', () => {
49+
it('should ignore the rule when the name is a variable', () => {
50+
const source = `
51+
export function mockPipe(name: string): any {
52+
@Pipe({ name })
53+
class MockPipe implements PipeTransform {
54+
transform(input: any): any {
55+
return input;
56+
}
57+
}
58+
return MockPipe;
59+
}
60+
`;
61+
assertSuccess('pipe-prefix', source, ['ng']);
62+
});
63+
});
64+
65+
describe('valid pipe name', () => {
66+
it('should succeed with prefix ng in @Pipe', () => {
67+
let source = `
68+
@Pipe({
69+
name: 'ngBarFoo'
70+
})
71+
class Test {}
72+
`;
73+
assertSuccess('pipe-prefix', source, ['ng']);
74+
});
75+
76+
it('should succeed with multiple prefixes in @Pipe', () => {
77+
let source = `
78+
@Pipe({
79+
name: 'ngBarFoo'
80+
})
81+
class Test {}
82+
`;
83+
assertSuccess('pipe-prefix', source, ['ng', 'sg', 'mg']);
84+
});
85+
86+
it('should succeed when the class is not a Pipe', () => {
87+
let source = `
88+
class Test {}
89+
`;
90+
assertSuccess('pipe-prefix', source, ['ng']);
91+
});
92+
93+
it('should do nothing if the name of the pipe is not a literal', () => {
94+
let source = `
95+
const pipeName = 'fooBar';
96+
@Pipe({
97+
name: pipeName
98+
})
99+
class Test {}
100+
`;
101+
assertSuccess('pipe-prefix', source, ['ng']);
102+
});
103+
});
104+
});

0 commit comments

Comments
 (0)
Please sign in to comment.