Skip to content

Commit

Permalink
Cyclomatic Complexity Rule (palantir#1464)
Browse files Browse the repository at this point in the history
  • Loading branch information
bencoveney authored and jkillian committed Sep 14, 2016
1 parent e048ca4 commit bfe06b2
Show file tree
Hide file tree
Showing 10 changed files with 703 additions and 0 deletions.
1 change: 1 addition & 0 deletions src/configs/latest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

export const rules = {
"adjacent-overload-signatures": true,
"cyclomatic-complexity": false,
"no-unsafe-finally": true,
"object-literal-key-quotes": [true, "as-needed"],
"object-literal-shorthand": true,
Expand Down
211 changes: 211 additions & 0 deletions src/rules/cyclomaticComplexityRule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
/**
* @license
* Copyright 2016 Palantir Technologies, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import * as Lint from "../lint";
import * as ts from "typescript";

export class Rule extends Lint.Rules.AbstractRule {

public static DEFAULT_THRESHOLD = 20;
public static MINIMUM_THRESHOLD = 2;

/* tslint:disable:object-literal-sort-keys */
public static metadata: Lint.IRuleMetadata = {
ruleName: "cyclomatic-complexity",
description: "Enforces a threshold of cyclomatic complexity.",
descriptionDetails: Lint.Utils.dedent`
Cyclomatic complexity is assessed for each function of any type. A starting value of 1
is assigned and this value is then incremented for every statement which can branch the
control flow within the function. The following statements and expressions contribute
to cyclomatic complexity:
* \`catch\`
* \`if\` and \`? :\`
* \`||\` and \`&&\` due to short-circuit evaluation
* \`for\`, \`for in\` and \`for of\` loops
* \`while\` and \`do while\` loops`,
rationale: Lint.Utils.dedent`
Cyclomatic complexity is a code metric which indicates the level of complexity in a
function. High cyclomatic complexity indicates confusing code which may be prone to
errors or difficult to modify.`,
optionsDescription: Lint.Utils.dedent`
An optional upper limit for cyclomatic complexity can be specified. If no limit option
is provided a default value of $(Rule.DEFAULT_THRESHOLD) will be used.`,
options: {
type: "number",
minimum: "$(Rule.MINIMUM_THRESHOLD)",
},
optionExamples: ["true", "[true, 20]"],
type: "maintainability",
};
/* tslint:enable:object-literal-sort-keys */

public static ANONYMOUS_FAILURE_STRING = (expected: number, actual: number) =>
`The function has a cyclomatic complexity of ${actual} which is higher than the threshold of ${expected}`;
public static NAMED_FAILURE_STRING = (expected: number, actual: number, name: string) =>
`The function ${name} has a cyclomatic complexity of ${actual} which is higher than the threshold of ${expected}`;

public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
return this.applyWithWalker(new CyclomaticComplexityWalker(sourceFile, this.getOptions(), this.threshold));
}

public isEnabled(): boolean {
// Disable the rule if the option is provided but non-numeric or less than the minimum.
const isThresholdValid = typeof this.threshold === "number" && this.threshold >= Rule.MINIMUM_THRESHOLD;
return super.isEnabled() && isThresholdValid;
}

private get threshold(): number {
return this.getOptions().ruleArguments[0] || Rule.DEFAULT_THRESHOLD;
}
}

class CyclomaticComplexityWalker extends Lint.RuleWalker {

private functions: number[] = [];

public constructor(sourceFile: ts.SourceFile, options: Lint.IOptions, private threshold: number) {
super(sourceFile, options);
}

protected visitArrowFunction(node: ts.FunctionLikeDeclaration) {
this.startFunction();
super.visitArrowFunction(node);
this.endFunction(node);
}

protected visitBinaryExpression(node: ts.BinaryExpression) {
switch (node.operatorToken.kind) {
case ts.SyntaxKind.BarBarToken:
case ts.SyntaxKind.AmpersandAmpersandToken:
this.incrementComplexity();
break;
default:
break;
}
super.visitBinaryExpression(node);
}

protected visitCaseClause(node: ts.CaseClause) {
this.incrementComplexity();
super.visitCaseClause(node);
}

protected visitCatchClause(node: ts.CatchClause) {
this.incrementComplexity();
super.visitCatchClause(node);
}

protected visitConditionalExpression(node: ts.ConditionalExpression) {
this.incrementComplexity();
super.visitConditionalExpression(node);
}

public visitConstructorDeclaration(node: ts.ConstructorDeclaration) {
this.startFunction();
super.visitConstructorDeclaration(node);
this.endFunction(node);
}

protected visitDoStatement(node: ts.DoStatement) {
this.incrementComplexity();
super.visitDoStatement(node);
}

protected visitForStatement(node: ts.ForStatement) {
this.incrementComplexity();
super.visitForStatement(node);
}

protected visitForInStatement(node: ts.ForInStatement) {
this.incrementComplexity();
super.visitForInStatement(node);
}

protected visitForOfStatement(node: ts.ForOfStatement) {
this.incrementComplexity();
super.visitForOfStatement(node);
}

protected visitFunctionDeclaration(node: ts.FunctionDeclaration) {
this.startFunction();
super.visitFunctionDeclaration(node);
this.endFunction(node);
}

protected visitFunctionExpression(node: ts.FunctionExpression) {
this.startFunction();
super.visitFunctionExpression(node);
this.endFunction(node);
}

protected visitGetAccessor(node: ts.AccessorDeclaration) {
this.startFunction();
super.visitGetAccessor(node);
this.endFunction(node);
}

protected visitIfStatement(node: ts.IfStatement) {
this.incrementComplexity();
super.visitIfStatement(node);
}

protected visitMethodDeclaration(node: ts.MethodDeclaration) {
this.startFunction();
super.visitMethodDeclaration(node);
this.endFunction(node);
}

protected visitSetAccessor(node: ts.AccessorDeclaration) {
this.startFunction();
super.visitSetAccessor(node);
this.endFunction(node);
}

protected visitWhileStatement(node: ts.WhileStatement) {
this.incrementComplexity();
super.visitWhileStatement(node);
}

private startFunction() {
// Push an initial complexity value to the stack for the new function.
this.functions.push(1);
}

private endFunction(node: ts.FunctionLikeDeclaration) {
const complexity = this.functions.pop();

// Check for a violation.
if (complexity > this.threshold) {
let failureString: string;

// Attempt to find a name for the function.
if (node.name && node.name.kind === ts.SyntaxKind.Identifier) {
failureString = Rule.NAMED_FAILURE_STRING(this.threshold, complexity, (node.name as ts.Identifier).text);
} else {
failureString = Rule.ANONYMOUS_FAILURE_STRING(this.threshold, complexity);
}

this.addFailure(this.createFailure(node.getStart(), node.getWidth(), failureString));
}
}

private incrementComplexity() {
if (this.functions.length) {
this.functions[this.functions.length - 1]++;
}
}
}
1 change: 1 addition & 0 deletions src/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
"rules/classNameRule.ts",
"rules/commentFormatRule.ts",
"rules/curlyRule.ts",
"rules/cyclomaticComplexityRule.ts",
"rules/eoflineRule.ts",
"rules/fileHeaderRule.ts",
"rules/forinRule.ts",
Expand Down
68 changes: 68 additions & 0 deletions test/rules/cyclomatic-complexity/defaultThreshold/test.ts.lint
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Check that a default threshold is used if none is specified.

function validThresholdPass() {
const condition1 = true ? "true" : "false";
const condition2 = true ? "true" : "false";
const condition3 = true ? "true" : "false";
const condition4 = true ? "true" : "false";
const condition5 = true ? "true" : "false";
const condition6 = true ? "true" : "false";
const condition7 = true ? "true" : "false";
const condition8 = true ? "true" : "false";
const condition9 = true ? "true" : "false";
const condition10 = true ? "true" : "false";
const condition11 = true ? "true" : "false";
const condition12 = true ? "true" : "false";
const condition13 = true ? "true" : "false";
const condition14 = true ? "true" : "false";
const condition15 = true ? "true" : "false";
const condition16 = true ? "true" : "false";
const condition17 = true ? "true" : "false";
const condition18 = true ? "true" : "false";
const condition19 = true ? "true" : "false";
}

function validThresholdFail() {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
const condition1 = true ? "true" : "false";
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
const condition2 = true ? "true" : "false";
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
const condition3 = true ? "true" : "false";
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
const condition4 = true ? "true" : "false";
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
const condition5 = true ? "true" : "false";
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
const condition6 = true ? "true" : "false";
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
const condition7 = true ? "true" : "false";
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
const condition8 = true ? "true" : "false";
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
const condition9 = true ? "true" : "false";
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
const condition10 = true ? "true" : "false";
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
const condition11 = true ? "true" : "false";
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
const condition12 = true ? "true" : "false";
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
const condition13 = true ? "true" : "false";
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
const condition14 = true ? "true" : "false";
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
const condition15 = true ? "true" : "false";
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
const condition16 = true ? "true" : "false";
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
const condition17 = true ? "true" : "false";
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
const condition18 = true ? "true" : "false";
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
const condition19 = true ? "true" : "false";
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
const condition20 = true ? "true" : "false";
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
}
~ [The function validThresholdFail has a cyclomatic complexity of 21 which is higher than the threshold of 20]
5 changes: 5 additions & 0 deletions test/rules/cyclomatic-complexity/defaultThreshold/tslint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"rules": {
"cyclomatic-complexity": [true]
}
}
25 changes: 25 additions & 0 deletions test/rules/cyclomatic-complexity/invalidThreshold/test.ts.lint
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Check that rule is not applied for invalid config values by checking a
// function with complexity higher than the dafault threshold.

function invalidThreshold() {
const condition1 = true ? "true" : "false";
const condition2 = true ? "true" : "false";
const condition3 = true ? "true" : "false";
const condition4 = true ? "true" : "false";
const condition5 = true ? "true" : "false";
const condition6 = true ? "true" : "false";
const condition7 = true ? "true" : "false";
const condition8 = true ? "true" : "false";
const condition9 = true ? "true" : "false";
const condition10 = true ? "true" : "false";
const condition11 = true ? "true" : "false";
const condition12 = true ? "true" : "false";
const condition13 = true ? "true" : "false";
const condition14 = true ? "true" : "false";
const condition15 = true ? "true" : "false";
const condition16 = true ? "true" : "false";
const condition17 = true ? "true" : "false";
const condition18 = true ? "true" : "false";
const condition19 = true ? "true" : "false";
const condition20 = true ? "true" : "false";
}
5 changes: 5 additions & 0 deletions test/rules/cyclomatic-complexity/invalidThreshold/tslint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"rules": {
"cyclomatic-complexity": [true, -5]
}
}
Loading

0 comments on commit bfe06b2

Please sign in to comment.