From d473edd4af343978c8c13c524d89bb4c79941aea Mon Sep 17 00:00:00 2001 From: Tetsuro Yoshikawa Date: Sat, 23 Jul 2022 08:11:27 +0900 Subject: [PATCH] feat: add CyclomaticComplexity Analyzer. --- .../Adapter/ComplexityCountableNode.ts | 4 + .../CyclomaticComplexity/Calculator.ts | 45 +++++++++++ .../ComplexityIncrement.ts | 13 +++ .../CyclomaticComplexity.ts | 13 +++ .../__tests__/Calculator.test.ts | 80 +++++++++++++++++++ .../__tests__/CyclomaticComplexity.test.ts | 20 +++++ 6 files changed, 175 insertions(+) create mode 100644 src/Analyzer/CodeMetricsCalculator/CyclomaticComplexity/Adapter/ComplexityCountableNode.ts create mode 100644 src/Analyzer/CodeMetricsCalculator/CyclomaticComplexity/Calculator.ts create mode 100644 src/Analyzer/CodeMetricsCalculator/CyclomaticComplexity/ComplexityIncrement.ts create mode 100644 src/Analyzer/CodeMetricsCalculator/CyclomaticComplexity/CyclomaticComplexity.ts create mode 100644 src/Analyzer/CodeMetricsCalculator/CyclomaticComplexity/__tests__/Calculator.test.ts create mode 100644 src/Analyzer/CodeMetricsCalculator/CyclomaticComplexity/__tests__/CyclomaticComplexity.test.ts diff --git a/src/Analyzer/CodeMetricsCalculator/CyclomaticComplexity/Adapter/ComplexityCountableNode.ts b/src/Analyzer/CodeMetricsCalculator/CyclomaticComplexity/Adapter/ComplexityCountableNode.ts new file mode 100644 index 0000000..b41f72a --- /dev/null +++ b/src/Analyzer/CodeMetricsCalculator/CyclomaticComplexity/Adapter/ComplexityCountableNode.ts @@ -0,0 +1,4 @@ +export interface ComplexityCountableNode { + isIncrement(): boolean; + getChildren(): ComplexityCountableNode[]; +} diff --git a/src/Analyzer/CodeMetricsCalculator/CyclomaticComplexity/Calculator.ts b/src/Analyzer/CodeMetricsCalculator/CyclomaticComplexity/Calculator.ts new file mode 100644 index 0000000..84a8feb --- /dev/null +++ b/src/Analyzer/CodeMetricsCalculator/CyclomaticComplexity/Calculator.ts @@ -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 + ) {} + + 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))); + } +} diff --git a/src/Analyzer/CodeMetricsCalculator/CyclomaticComplexity/ComplexityIncrement.ts b/src/Analyzer/CodeMetricsCalculator/CyclomaticComplexity/ComplexityIncrement.ts new file mode 100644 index 0000000..64caf8f --- /dev/null +++ b/src/Analyzer/CodeMetricsCalculator/CyclomaticComplexity/ComplexityIncrement.ts @@ -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; + } +} diff --git a/src/Analyzer/CodeMetricsCalculator/CyclomaticComplexity/CyclomaticComplexity.ts b/src/Analyzer/CodeMetricsCalculator/CyclomaticComplexity/CyclomaticComplexity.ts new file mode 100644 index 0000000..358ffc5 --- /dev/null +++ b/src/Analyzer/CodeMetricsCalculator/CyclomaticComplexity/CyclomaticComplexity.ts @@ -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); + } +} diff --git a/src/Analyzer/CodeMetricsCalculator/CyclomaticComplexity/__tests__/Calculator.test.ts b/src/Analyzer/CodeMetricsCalculator/CyclomaticComplexity/__tests__/Calculator.test.ts new file mode 100644 index 0000000..ff3fccd --- /dev/null +++ b/src/Analyzer/CodeMetricsCalculator/CyclomaticComplexity/__tests__/Calculator.test.ts @@ -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); + }); + }); +}); diff --git a/src/Analyzer/CodeMetricsCalculator/CyclomaticComplexity/__tests__/CyclomaticComplexity.test.ts b/src/Analyzer/CodeMetricsCalculator/CyclomaticComplexity/__tests__/CyclomaticComplexity.test.ts new file mode 100644 index 0000000..812548c --- /dev/null +++ b/src/Analyzer/CodeMetricsCalculator/CyclomaticComplexity/__tests__/CyclomaticComplexity.test.ts @@ -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: () => [], +});