Skip to content

Commit

Permalink
feat: add CyclomaticComplexity Analyzer.
Browse files Browse the repository at this point in the history
  • Loading branch information
ytetsuro committed Aug 14, 2022
1 parent f2c6c13 commit d473edd
Show file tree
Hide file tree
Showing 6 changed files with 175 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface ComplexityCountableNode {
isIncrement(): boolean;
getChildren(): ComplexityCountableNode[];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { ComplexityCountableNode } from './Adapter/ComplexityCountableNode';
import { ComplexityIncrement } from './ComplexityIncrement';
import { CyclomaticComplexity } from './CyclomaticComplexity';
import { CalculatorForAST } from '../../FromASTNode/CalculatorForAST';
import { MethodAnalyzer } from '../../FromASTNode/MethodAnalyzer';
import { Metrics } from '../../Metrics/Metrics';
import { ASTNodeSource } from '../../FromASTNode/ASTNodeSource';
import { inject, injectable } from 'inversify';
import { Types } from '../../../types/Types';
import { Converter } from '../../Adapter/Converter';

@injectable()
export class Calculator implements CalculatorForAST {
constructor(
@inject(MethodAnalyzer) private readonly analyzer: MethodAnalyzer,
@inject(Types.cognitiveComplexityConverter)
private readonly converter: Converter<ComplexityCountableNode>
) {}

analyze(astNodes: ASTNodeSource[]) {
return this.analyzer
.analyze(astNodes)
.map(({ astNode, ...other }) => ({
...other,
countableNode: this.converter.convert(astNode),
}))
.map((row) => new Metrics(row.file, row.codePoints, this.calculate(row.countableNode)));
}

calculate(node: ComplexityCountableNode): CyclomaticComplexity[] {
const complexities = this.extractComplexity(node);

return [new CyclomaticComplexity(complexities)];
}

private extractComplexity(node: ComplexityCountableNode): ComplexityIncrement[] {
const result: ComplexityIncrement[] = [];

if (node.isIncrement()) {
result.push(new ComplexityIncrement(node));
}

return result.concat(...node.getChildren().map((row) => this.extractComplexity(row)));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { ComplexityCountableNode } from './Adapter/ComplexityCountableNode';

export class ComplexityIncrement {
public readonly complexity: number;

constructor(complexityCountableNode: ComplexityCountableNode) {
this.complexity = complexityCountableNode.isIncrement() ? 1 : 0;
}

valueOf() {
return this.complexity;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { MetricsType } from '../../Metrics/MetricsType';
import { MetricsValue } from '../../Metrics/MetricsValue';
import { ComplexityIncrement } from './ComplexityIncrement';

export class CyclomaticComplexity implements MetricsValue {
public readonly type = MetricsType.CognitiveComplexity;

constructor(private readonly complexities: ComplexityIncrement[]) {}

valueOf(): number {
return this.complexities.reduce((prev, next) => Number(prev) + Number(next), 0);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { Calculator } from '../Calculator';
import { ComplexityCountableNode } from '../../../../TestHelpers/ComplexityCountableNode';
import { MethodAnalyzer } from '../../../FromASTNode/MethodAnalyzer';
import { ASTNodeExtractor } from '../../../ASTNodeExtractor';
import { ASTNode } from '../../../../TestHelpers/ASTNode';
import { CyclomaticComplexity } from '../CyclomaticComplexity';

describe('Cyclomatic Complexity Calculator', () => {
describe('should increment when incrementable node.', () => {
const createCountableNodeSeed: (ast: ASTNode) => any = (ast: ASTNode) => ({
DSL: ast.getName(),
children: ast.getChildren().map((ast) => createCountableNodeSeed(ast)),
});
const calculator = new Calculator(new MethodAnalyzer(new ASTNodeExtractor()), {
convert: (ast: ASTNode) => new ComplexityCountableNode(createCountableNodeSeed(ast)),
});

it('should returns 1 when incrementable node.', () => {
const actual = calculator.analyze([
{
astNode: new ASTNode(':root:0:0', {
'C:DummyClass:0:20': {
'M:I:1:9': {},
},
}),
file: {
fullPath: '/tmp/dummy.ts',
relativePath: 'dummy.ts',
extension: 'ts',
},
},
]);

expect(Number(actual[0].getMetricsByMetricsValue(CyclomaticComplexity))).toBe(1);
});

it('should returns 1 when childNode is incrementable node.', () => {
const actual = calculator.analyze([
{
astNode: new ASTNode(':root:0:0', {
'C:DummyClass:0:20': {
'M::1:9': {
':I:2:3': {},
},
},
}),
file: {
fullPath: '/tmp/dummy.ts',
relativePath: 'dummy.ts',
extension: 'ts',
},
},
]);

expect(Number(actual[0].getMetricsByMetricsValue(CyclomaticComplexity))).toBe(1);
});

it('should return 2 when has tow incrementable node.', () => {
const actual = calculator.analyze([
{
astNode: new ASTNode(':root:0:0', {
'C:DummyClass:0:20': {
'M:N:1:9': {
':I:2:3': {},
':I:4:5': {},
},
},
}),
file: {
fullPath: '/tmp/dummy.ts',
relativePath: 'dummy.ts',
extension: 'ts',
},
},
]);

expect(Number(actual[0].getMetricsByMetricsValue(CyclomaticComplexity))).toBe(2);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { CyclomaticComplexity } from '../CyclomaticComplexity';
import { ComplexityIncrement } from '../ComplexityIncrement';

describe('Complexity Store Class', () => {
describe('.valueOf()', () => {
it('return sum complexities', () => {
const complexityStore = new CyclomaticComplexity([
new ComplexityIncrement(createDummyNode('I')),
new ComplexityIncrement(createDummyNode('I')),
]);

expect(complexityStore.valueOf()).toStrictEqual(2);
});
});
});

const createDummyNode = (DSL: string) => ({
isIncrement: () => DSL.includes('I'),
getChildren: () => [],
});

0 comments on commit d473edd

Please sign in to comment.