From 322133000e47aabf9426ebf5315520c63f3483d8 Mon Sep 17 00:00:00 2001 From: William KOZA Date: Sun, 11 Feb 2018 21:02:41 +0100 Subject: [PATCH] feat(rule): cyclomatic complexity rule (#512) * feat(rule): add templateCyclomaticComplexityRule (#509) * feat(rule): review + rebase --- src/angular/config.ts | 2 + src/index.ts | 1 + src/templateCyclomaticComplexityRule.ts | 75 +++++++++++++++++ test/templateCyclomaticComplexityRule.spec.ts | 84 +++++++++++++++++++ 4 files changed, 162 insertions(+) create mode 100644 src/templateCyclomaticComplexityRule.ts create mode 100644 test/templateCyclomaticComplexityRule.spec.ts diff --git a/src/angular/config.ts b/src/angular/config.ts index a2a6553e8..3487eff75 100644 --- a/src/angular/config.ts +++ b/src/angular/config.ts @@ -69,6 +69,8 @@ export const Config: Config = { { selector: '[ngIf]', exportAs: 'ngIf', inputs: ['ngIf'] }, { selector: '[ngFor][ngForOf]', exportAs: 'ngFor', inputs: ['ngForTemplate', 'ngForOf'] }, { selector: '[ngSwitch]', exportAs: 'ngSwitch', inputs: ['ngSwitch'] }, + { selector: '[ngSwitchCase]', exportAs: 'ngSwitchCase', inputs: ['ngSwitchCase'] }, + { selector: '[ngSwitchDefault]', exportAs: 'ngSwitchDefault', inputs: ['ngSwitchDefault'] }, // @angular/material { selector: 'mat-autocomplete', exportAs: 'matAutocomplete' }, diff --git a/src/index.ts b/src/index.ts index e0b2a5fa1..de9ec9c29 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,6 +7,7 @@ export { Rule as DecoratorNotAllowedRule } from './decoratorNotAllowedRule'; export { Rule as DirectiveClassSuffixRule } from './directiveClassSuffixRule'; export { Rule as DirectiveSelectorRule } from './directiveSelectorRule'; export { Rule as I18nRule } from './i18nRule'; +export { Rule as TemplateCyclomaticComplexityRule } from './templateCyclomaticComplexityRule'; export { Rule as TemplateConditionalComplexityRule } from './templateConditionalComplexityRule'; export { Rule as ImportDestructuringSpacingRule } from './importDestructuringSpacingRule'; export { Rule as NoAttributeParameterDecoratorRule } from './noAttributeParameterDecoratorRule'; diff --git a/src/templateCyclomaticComplexityRule.ts b/src/templateCyclomaticComplexityRule.ts new file mode 100644 index 000000000..b6ce2b6ba --- /dev/null +++ b/src/templateCyclomaticComplexityRule.ts @@ -0,0 +1,75 @@ +import * as Lint from 'tslint'; +import * as ts from 'typescript'; +import * as ast from '@angular/compiler'; +import { sprintf } from 'sprintf-js'; +import { BasicTemplateAstVisitor } from './angular/templates/basicTemplateAstVisitor'; +import { NgWalker } from './angular/ngWalker'; + +export class Rule extends Lint.Rules.AbstractRule { + public static metadata: Lint.IRuleMetadata = { + ruleName: 'template-cyclomatic-complexity', + type: 'functionality', + // tslint:disable-next-line:max-line-length + description: 'Checks cyclomatic complexity against a specified limit. It is a quantitative measure of the number of linearly independent paths through a program\'s source code', + rationale: 'Cyclomatic complexity over some threshold indicates that the logic should be moved outside the template.', + options: { + type: 'array', + items: { + type: 'string' + }, + minLength: 0, + maxLength: 2, + }, + optionExamples: [ + 'true', + '[true, 6]' + ], + optionsDescription: 'Determine the maximum number of the cyclomatic complexity.', + typescriptOnly: true, + hasFix: false + }; + + // tslint:disable-next-line:max-line-length + static COMPLEXITY_FAILURE_STRING = 'The cyclomatic complexity exceeded the defined limit (cost \'%s\'). Your template should be refactored.'; + + static COMPLEXITY_MAX = 5; + + public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { + + return this.applyWithWalker( + new NgWalker(sourceFile, + this.getOptions(), { + templateVisitorCtrl: TemplateConditionalComplexityVisitor, + })); + } +} + +class TemplateConditionalComplexityVisitor extends BasicTemplateAstVisitor { + + complexity = 0; + + visitDirectiveProperty(prop: ast.BoundDirectivePropertyAst, context: BasicTemplateAstVisitor): any { + if (prop.sourceSpan) { + const directive = (prop.sourceSpan).toString(); + + if (directive.startsWith('*ngFor') || directive.startsWith('*ngIf') || + directive.startsWith('*ngSwitchCase') || directive.startsWith('*ngSwitchDefault')) { + this.complexity++; + } + } + + const options = this.getOptions(); + const complexityMax: number = options.length ? options[0] : Rule.COMPLEXITY_MAX; + + if (this.complexity > complexityMax) { + const span = prop.sourceSpan; + let failureConfig: string[] = [String(Rule.COMPLEXITY_MAX)]; + failureConfig.unshift(Rule.COMPLEXITY_FAILURE_STRING); + this.addFailure(this.createFailure(span.start.offset, span.end.offset - span.start.offset, + sprintf.apply(this, failureConfig)) + ); + } + + super.visitDirectiveProperty(prop, context); + } +} diff --git a/test/templateCyclomaticComplexityRule.spec.ts b/test/templateCyclomaticComplexityRule.spec.ts new file mode 100644 index 000000000..bd15d1a1a --- /dev/null +++ b/test/templateCyclomaticComplexityRule.spec.ts @@ -0,0 +1,84 @@ +// tslint:disable:max-line-length +import { assertSuccess, assertAnnotated } from './testHelper'; +import { Replacement } from 'tslint'; +import { expect } from 'chai'; + +describe('cyclomatic complexity', () => { + describe('success', () => { + it('should work with a lower level of complexity', () => { + let source = ` + @Component({ + template: \` +
+
  • + {{ person.name }} +
    + + + +
    +
  • +
    + \` + }) + class Bar {} + `; + assertSuccess('template-cyclomatic-complexity', source); + }); + + it('should work with a higher level of complexity', () => { + let source = ` + @Component({ + template: \` +
    +
  • +
    {{ person.name }}
    +
    + + + + +
    +
  • +
    + \` + }) + class Bar {} + `; + assertSuccess('template-cyclomatic-complexity', source, [7]); + }); + + }); + + + describe('failure', () => { + it('should fail with a higher level of complexity', () => { + let source = ` + @Component({ + template: \` +
    +
  • +
    {{ person.name }}
    +
    + + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~ + +
    +
  • +
    + \` + }) + class Bar {} + `; + assertAnnotated({ + ruleName: 'template-cyclomatic-complexity', + message: 'The cyclomatic complexity exceeded the defined limit (cost \'5\'). Your template should be refactored.', + source + }); + }); + + }); + +});