Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(component-change-detection): add change detection strategy rule
Closes #135
- Loading branch information
1 parent
0ed079f
commit 40792ac
Showing
3 changed files
with
163 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
import { Utils } from 'tslint/lib'; | ||
import * as Lint from 'tslint'; | ||
import * as ts from 'typescript'; | ||
import { getDecoratorArgument, getDecoratorName } from './util/utils'; | ||
import { sprintf } from 'sprintf-js'; | ||
import { NgWalker } from './angular'; | ||
|
||
const OPTION_DEFAULT = 'Default'; | ||
const OPTION_ON_PUSH = 'OnPush'; | ||
|
||
export type ChangeDetectionType = 'Default' | 'OnPush'; | ||
|
||
export class Rule extends Lint.Rules.AbstractRule { | ||
public static metadata: Lint.IRuleMetadata = { | ||
ruleName: 'component-change-detection', | ||
type: 'functionality', | ||
description: 'Enforce the preferred component change detection type (Default or OnPush).', | ||
descriptionDetails: Utils.dedent` | ||
See more at https://angular.io/api/core/ChangeDetectionStrategy | ||
`, | ||
optionExamples: [[true, OPTION_DEFAULT], [true, OPTION_ON_PUSH]], | ||
options: { | ||
items: [ | ||
{ | ||
enum: [OPTION_DEFAULT, OPTION_ON_PUSH] | ||
} | ||
], | ||
maxLength: 1, | ||
minLength: 1, | ||
type: 'array' | ||
}, | ||
optionsDescription: Utils.dedent` | ||
Options accepts one obligatory item as an array: | ||
1. \`${OPTION_DEFAULT}\` or \`${OPTION_ON_PUSH}\` to force components to use that type of change detection | ||
`, | ||
rationale: Utils.dedent` | ||
By using OnPush for change detection, Angular will only run a change detection cycle when that | ||
components inputs or outputs change | ||
`, | ||
typescriptOnly: true | ||
}; | ||
|
||
static CHANGE_DETECTION_INVALID_FAILURE = 'The changeDetection value of the component "%s" should be set to ChangeDetectionStrategy.%s'; | ||
|
||
type: ChangeDetectionType; | ||
|
||
constructor(options: Lint.IOptions) { | ||
super(options); | ||
const args = this.getOptions().ruleArguments; | ||
this.type = args[0]; | ||
} | ||
|
||
isEnabled(): boolean { | ||
const { | ||
metadata: { | ||
options: { maxLength, minLength } | ||
} | ||
} = Rule; | ||
const { length } = this.ruleArguments; | ||
|
||
return super.isEnabled() && length >= minLength && length <= maxLength; | ||
} | ||
|
||
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { | ||
return this.applyWithWalker(new ComponentChangeDetectionValidatorWalker(sourceFile, this)); | ||
} | ||
} | ||
|
||
export class ComponentChangeDetectionValidatorWalker extends NgWalker { | ||
constructor(sourceFile: ts.SourceFile, private rule: Rule) { | ||
super(sourceFile, rule.getOptions()); | ||
} | ||
|
||
visitClassDeclaration(node: ts.ClassDeclaration) { | ||
ts.createNodeArray(node.decorators).forEach(this.validateDecorator.bind(this, node.name!.text)); | ||
super.visitClassDeclaration(node); | ||
} | ||
|
||
private validateDecorator(className: string, decorator: ts.Decorator) { | ||
const argument = getDecoratorArgument(decorator)!; | ||
const name = getDecoratorName(decorator); | ||
|
||
// Do not run component rules for directives | ||
if (name === 'Component') { | ||
this.validateChangeDetection(className, decorator, argument); | ||
} | ||
} | ||
|
||
private validateChangeDetection(className: string, decorator: ts.Decorator, arg: ts.Node) { | ||
if (!ts.isObjectLiteralExpression(arg)) { | ||
return; | ||
} | ||
|
||
const changeDetectionAssignment = arg.properties | ||
.filter(prop => ts.isPropertyAssignment(prop) && this.validateProperty(prop)) | ||
.map(prop => (ts.isPropertyAssignment(prop) ? prop.initializer : undefined)) | ||
.filter(Boolean)[0] as ts.PropertyAccessExpression; | ||
|
||
if (!changeDetectionAssignment) { | ||
this.addFailureAtNode(decorator, sprintf(Rule.CHANGE_DETECTION_INVALID_FAILURE, className, this.rule.type)); | ||
} else { | ||
const changeDetectionValue = changeDetectionAssignment!.name.escapedText as string; | ||
|
||
if (this.rule.type !== changeDetectionValue) { | ||
this.addFailureAtNode(changeDetectionAssignment, sprintf(Rule.CHANGE_DETECTION_INVALID_FAILURE, className, this.rule.type)); | ||
} | ||
} | ||
} | ||
|
||
private validateProperty(p: ts.PropertyAssignment): boolean { | ||
return ts.isIdentifier(p.name) && p.name.text === 'changeDetection'; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import { assertSuccess, assertAnnotated } from './testHelper'; | ||
|
||
describe('component-change-detection', () => { | ||
describe('invalid component change detection', () => { | ||
it('should fail when component used without preferred change detection type', () => { | ||
let source = ` | ||
@Component({ | ||
changeDetection: ChangeDetectionStrategy.Default | ||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
}) | ||
class Test {} | ||
`; | ||
assertAnnotated({ | ||
ruleName: 'component-change-detection', | ||
message: 'The changeDetection value of the component "Test" should be set to ChangeDetectionStrategy.OnPush', | ||
source, | ||
options: ['OnPush'] | ||
}); | ||
}); | ||
|
||
it('should fail when component change detection is not set', () => { | ||
let source = ` | ||
@Component({ | ||
~~~~~~~~~~~~ | ||
selector: 'foo' | ||
}) | ||
~~ | ||
class Test {}`; | ||
assertAnnotated({ | ||
ruleName: 'component-change-detection', | ||
message: 'The changeDetection value of the component "Test" should be set to ChangeDetectionStrategy.OnPush', | ||
source, | ||
options: ['OnPush'] | ||
}); | ||
}); | ||
}); | ||
|
||
describe('valid component selector', () => { | ||
it('should succeed when a valid change detection strategy is set on @Component', () => { | ||
let source = ` | ||
@Component({ | ||
changeDetection: ChangeDetectionStrategy.OnPush | ||
}) | ||
class Test {} | ||
`; | ||
assertSuccess('component-change-detection', source, ['OnPush']); | ||
}); | ||
}); | ||
}); |