Skip to content

Commit 7e0dd65

Browse files
committed
Merge pull request #3 from mgechev/property-directive
Property directive
2 parents c61a059 + ff778d4 commit 7e0dd65

10 files changed

+193
-21
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
node_modules
22
typings
33
dist
4-
4+
.idea

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ Below you can find a recommended configuration which is based on the [Angular 2
1717
"host-parameter-decorator": true,
1818
"input-parameter-decorator": true,
1919
"output-parameter-decorator": true,
20-
"attribute-parameter-decorator":true
20+
"attribute-parameter-decorator":true,
21+
"input-property-directive":true,
22+
"output-property-directive":true
2123
}
2224
```
2325

@@ -33,8 +35,8 @@ Below you can find a recommended configuration which is based on the [Angular 2
3335
- [x] Use `@Output` instead of `outputs` decorator property.
3436
- [x] Use `@HostListeners` and `@HostBindings` instead of `host` decorator property.
3537
- [ ] Do not use `nativeElement` injected with `ElementRef`.
36-
- [ ] Do not rename inputs.
37-
- [ ] Do not rename outputs.
38+
- [x] Do not rename inputs.
39+
- [x] Do not rename outputs.
3840
- [ ] Externalize template above *n* lines of code.
3941
- [x] Do not use the `@Attribute` decorator.
4042
- [ ] Do not use `forwardRef`.
Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,19 @@
11
import * as Lint from 'tslint/lib/lint';
22
import * as ts from 'typescript';
33
import {ConstructorRule} from "./parameterConstructorBase";
4-
5-
const attributeValidator = (parameter:ts.ParameterDeclaration)=>{
6-
let isValid = true;
7-
if (parameter.decorators) {
8-
parameter.decorators.forEach((decorator:ts.Decorator)=> {
9-
let baseExpr = <any>decorator.expression || {};
10-
let expr = baseExpr.expression || {};
11-
let name = expr.text;
12-
if (name==='Attribute') {
13-
isValid = false
14-
}
15-
});
16-
}
17-
return isValid;
18-
};
4+
import {decoratorValidator} from "./util/decoratorValidator";
195

206
const FAILURE_STRING = 'In the constructor of class "%s", the parameter "%s" uses the @Attribute decorator, ' +
217
'which is considered as a bad practice. Please, consider construction of type "@Input() %s: string"';
228

9+
const attributeCondition = (name)=>{
10+
return (name=='Attribute')
11+
};
12+
2313
export class Rule extends ConstructorRule {
2414

2515
constructor(ruleName:string, value:any, disabledIntervals:Lint.IDisabledInterval[]) {
26-
super(ruleName, value, disabledIntervals,attributeValidator,FAILURE_STRING);
16+
super(ruleName, value, disabledIntervals,decoratorValidator(attributeCondition),FAILURE_STRING);
2717
}
2818

2919
}

src/inputPropertyDirectiveRule.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import * as Lint from 'tslint/lib/lint';
2+
import * as ts from 'typescript';
3+
import {ClassParameterRule} from "./propertyClassBase";
4+
import {decoratorValidator} from './util/decoratorValidator';
5+
6+
const FAILURE_STRING = 'In the class "%s", the directive input property "%s" should not be renamed.' +
7+
'Please, consider the following use "@Input() %s: string"';
8+
9+
const renameInputCondition = (name, arg)=> {
10+
return (name === 'Input' && arg);
11+
};
12+
13+
export class Rule extends ClassParameterRule {
14+
15+
constructor(ruleName:string, value:any, disabledIntervals:Lint.IDisabledInterval[]) {
16+
super(ruleName, value, disabledIntervals, decoratorValidator(renameInputCondition), FAILURE_STRING);
17+
}
18+
19+
}

src/outputPropertyDirectiveRule.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import * as Lint from 'tslint/lib/lint';
2+
import * as ts from 'typescript';
3+
import {ClassParameterRule} from "./propertyClassBase";
4+
import {decoratorValidator} from './util/decoratorValidator';
5+
6+
const FAILURE_STRING = 'In the class "%s", the directive output property "%s" should not be renamed.' +
7+
'Please, consider the following use "@Output() %s = new EventEmitter();"';
8+
9+
const renameOutputCondition = (name, arg)=> {
10+
return (name === 'Output' && arg);
11+
};
12+
13+
export class Rule extends ClassParameterRule {
14+
15+
constructor(ruleName:string, value:any, disabledIntervals:Lint.IDisabledInterval[]) {
16+
super(ruleName, value, disabledIntervals, decoratorValidator(renameOutputCondition), FAILURE_STRING);
17+
}
18+
19+
}

src/propertyClassBase.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import * as Lint from 'tslint/lib/lint';
2+
import * as ts from 'typescript';
3+
import {sprintf} from 'sprintf-js';
4+
5+
export class ClassParameterRule extends Lint.Rules.AbstractRule {
6+
7+
constructor(ruleName:string, value:any, disabledIntervals:Lint.IDisabledInterval[], private validator:Function,
8+
private errorMessage:string) {
9+
super(ruleName, value, disabledIntervals);
10+
}
11+
12+
public apply(sourceFile:ts.SourceFile):Lint.RuleFailure[] {
13+
let documentRegistry = ts.createDocumentRegistry();
14+
let languageServiceHost = Lint.createLanguageServiceHost('file.ts', sourceFile.getFullText());
15+
let languageService:ts.LanguageService = ts.createLanguageService(languageServiceHost, documentRegistry);
16+
return this.applyWithWalker(
17+
new ClassMetadataWalker(
18+
sourceFile,
19+
languageService,
20+
this));
21+
}
22+
23+
public validate(parameter:ts.ParameterDeclaration) {
24+
return this.validator(parameter);
25+
}
26+
27+
public getFailureString(failureConfig) {
28+
return sprintf(this.errorMessage, failureConfig.className, failureConfig.memberName, failureConfig.memberName);
29+
}
30+
}
31+
32+
export class ClassMetadataWalker extends Lint.RuleWalker {
33+
34+
private languageService:ts.LanguageService;
35+
private typeChecker:ts.TypeChecker;
36+
37+
constructor(sourceFile:ts.SourceFile, languageService:ts.LanguageService, private rule:ClassParameterRule) {
38+
super(sourceFile, rule.getOptions());
39+
this.languageService = languageService;
40+
this.typeChecker = languageService.getProgram().getTypeChecker();
41+
}
42+
43+
visitClassDeclaration(node:ts.ClassDeclaration) {
44+
(node.members || []).forEach(this.validateClassMember.bind(this, node.name.text));
45+
super.visitClassDeclaration(node);
46+
}
47+
48+
validateClassMember(className:string, member) {
49+
if (!this.rule.validate(member)) {
50+
let memberName = member.name.text;
51+
this.addFailure(
52+
this.createFailure(
53+
member.getStart(),
54+
member.getWidth(),
55+
this.rule.getFailureString({className, memberName})));
56+
}
57+
}
58+
}

src/util/decoratorValidator.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
export const decoratorValidator = (condition)=> {
2+
return function (element) {
3+
let isValid:boolean = true;
4+
if (element.decorators) {
5+
element.decorators.forEach((decorator)=> {
6+
let baseExpr = <any>decorator.expression || {};
7+
let expr = baseExpr.expression || {};
8+
let name = expr.text;
9+
let args = baseExpr.arguments || [];
10+
let arg = args[0];
11+
if (condition(name, arg)) {
12+
isValid = false;
13+
}
14+
})
15+
}
16+
return isValid;
17+
}
18+
};

test/attributeParameterDecoratorRule.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ describe('attribute-parameter-decorator', () => {
1010
this.label = label;
1111
}
1212
}`;
13-
assertFailure('decorator-attribute', source, {
13+
assertFailure('attribute-parameter-decorator', source, {
1414
message: 'In the constructor of class "ButtonComponent", the parameter "label" uses the @Attribute decorator, ' +
1515
'which is considered as a bad practice. Please, consider construction of type "@Input() label: string"',
1616
startPosition: {
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import {assertFailure, assertSuccess} from './testHelper';
2+
3+
describe('input-property-directive', () => {
4+
describe('invalid directive input property', () => {
5+
it(`should fail, when a directive input property is renamed`, () => {
6+
let source = `
7+
class ButtonComponent {
8+
@Input('labelAttribute') label: string;
9+
}`;
10+
assertFailure('input-property-directive', source, {
11+
message: 'In the class "ButtonComponent", the directive input property "label" should not be renamed.' +
12+
'Please, consider the following use "@Input() label: string"',
13+
startPosition: {
14+
line: 2,
15+
character: 16
16+
},
17+
endPosition: {
18+
line: 2,
19+
character: 55
20+
}
21+
});
22+
});
23+
});
24+
describe('valid directive input property', () => {
25+
it('should succeed, when a directive input property is properly used', () => {
26+
let source = `
27+
class ButtonComponent {
28+
@Input() label: string;
29+
}`;
30+
assertSuccess('input-property-directive', source);
31+
});
32+
});
33+
});
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import {assertFailure, assertSuccess} from './testHelper';
2+
3+
describe('output-property-directive', () => {
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+
}`;
10+
assertFailure('output-property-directive', source, {
11+
message: 'In the class "ButtonComponent", the directive output property "change" should not be renamed.'+
12+
'Please, consider the following use "@Output() change = new EventEmitter();"',
13+
startPosition: {
14+
line: 2,
15+
character: 16
16+
},
17+
endPosition: {
18+
line: 2,
19+
character: 72
20+
}
21+
});
22+
});
23+
});
24+
describe('valid directive input property', () => {
25+
it('should succeed, when a directive output property is properly used', () => {
26+
let source = `
27+
class ButtonComponent {
28+
@Output() change = new EventEmitter<any>();
29+
}`;
30+
assertSuccess('output-property-directive', source);
31+
});
32+
});
33+
});

0 commit comments

Comments
 (0)