Skip to content

Commit 58a8375

Browse files
committed
feat: add report generator
1 parent e87bd23 commit 58a8375

File tree

4 files changed

+155
-108
lines changed

4 files changed

+155
-108
lines changed

src/calculate-complexity.js

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import {readFile} from 'fs/promises'
2+
import ast from 'abstract-syntax-tree'
3+
4+
const typesAddingComplexity = [
5+
'IfStatement',
6+
'TryStatement',
7+
'CatchClause',
8+
'DoWhileStatement',
9+
'ForInStatement',
10+
'ForOfStatement',
11+
'ForStatement',
12+
'WhileStatement',
13+
'ConditionalExpression'
14+
]
15+
16+
function increasesComplexity(node) {
17+
const checTypesAddingComplexity = typesAddingComplexity.includes(node.type)
18+
const checkCaseStatements =
19+
node.type === 'SwitchCase' && node.consequent?.length > 0
20+
const checkConditionals =
21+
node.type === 'LogicalExpression' &&
22+
(node.operator === '||' || node.operator === '&&' || node.operator === '??')
23+
24+
if (checTypesAddingComplexity || checkCaseStatements || checkConditionals) {
25+
return [true, 1]
26+
}
27+
28+
return [false, 0]
29+
}
30+
31+
const resolveBody = {
32+
CatchClause: node => [node.handler.body.body],
33+
TryStatement: node => [node?.block?.body, node.handler.body.body],
34+
TryStatementHandler: node => [],
35+
LogicalExpression: node => [node.left, node.right],
36+
ForStatement: node => [node.body?.body],
37+
ForOfStatement: node => [node.body?.body],
38+
ForInStatement: node => [node.body?.body],
39+
SwitchStatement: node => node.cases,
40+
SwitchCase: node => [node.consequent],
41+
WhileStatement: node => [node.body?.body],
42+
IfStatement: node => [
43+
node.test,
44+
node?.consequent?.body,
45+
node?.alternate?.body
46+
],
47+
FunctionDeclaration: node => [node.declaration.body?.body],
48+
DoWhileStatement: node => [node.body?.body],
49+
BlockStatement: node => [node.body],
50+
VariableDeclaration: node => [node.declarations],
51+
VariableDeclarator: node => [node.init],
52+
ConditionalExpression: node => [
53+
[node.test],
54+
[node.consequent],
55+
[node.alternate]
56+
],
57+
ArrowFunctionExpression: node => node.body.body,
58+
ExpressionStatement: node => node.expression.arguments,
59+
ExportNamedDeclaration: node => [node.declaration],
60+
ExportDefaultDeclaration: node => [node.declaration]
61+
}
62+
63+
const getName = node => {
64+
return node.id.name
65+
}
66+
67+
function determineLogicalComplexity(body) {
68+
let complexity = 0
69+
const output = {}
70+
body.forEach(function cb(node) {
71+
if (!node) return
72+
if (node.type === 'FunctionDeclaration') {
73+
const old = complexity
74+
complexity = 1 // reset clock on each function
75+
node.body.body.forEach(cb)
76+
const name = getName(node)
77+
output[name] = complexity
78+
complexity = old
79+
} else {
80+
const resolvedBody = resolveBody[node.type]
81+
if (!resolvedBody) return
82+
const [shouldIncrease] = increasesComplexity(node)
83+
if (shouldIncrease) {
84+
complexity += 1
85+
}
86+
const nodeBody = resolvedBody(node)
87+
if (nodeBody) {
88+
nodeBody.forEach(cb)
89+
}
90+
}
91+
})
92+
93+
return output
94+
}
95+
96+
/**
97+
* Calculates the Cyclomatic Complexity of a given file.
98+
*
99+
* @param {string} filename
100+
* @returns {number} cyclomatic complexity
101+
*/
102+
export async function calculateComplexity(filename) {
103+
const file = await readFile(filename, 'utf-8')
104+
const tree = ast.parse(file)
105+
return determineLogicalComplexity(tree.body)
106+
}

src/index.test.js renamed to src/calculate-complexity.test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import {describe, it, beforeEach} from 'node:test'
1+
import {describe, it} from 'node:test'
22
import assert from 'node:assert/strict'
33
import {calculateComplexity} from './index.js'
44

5-
describe('CyclomaticJS', () => {
5+
describe('calculateComplexity', () => {
66
it('simple-source', async () => {
77
const complexity = await calculateComplexity('./examples/simple-source.js')
88
assert.deepEqual(complexity, {

src/index.js

Lines changed: 2 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -1,106 +1,2 @@
1-
import {readFile} from 'fs/promises'
2-
import ast from 'abstract-syntax-tree'
3-
4-
const typesAddingComplexity = [
5-
'IfStatement',
6-
'TryStatement',
7-
'CatchClause',
8-
'DoWhileStatement',
9-
'ForInStatement',
10-
'ForOfStatement',
11-
'ForStatement',
12-
'WhileStatement',
13-
'ConditionalExpression'
14-
]
15-
16-
function increasesComplexity(node) {
17-
const checTypesAddingComplexity = typesAddingComplexity.includes(node.type)
18-
const checkCaseStatements =
19-
node.type === 'SwitchCase' && node.consequent?.length > 0
20-
const checkConditionals =
21-
node.type === 'LogicalExpression' &&
22-
(node.operator === '||' || node.operator === '&&' || node.operator === '??')
23-
24-
if (checTypesAddingComplexity || checkCaseStatements || checkConditionals) {
25-
return [true, 1]
26-
}
27-
28-
return [false, 0]
29-
}
30-
31-
const resolveBody = {
32-
CatchClause: node => [node.handler.body.body],
33-
TryStatement: node => [node?.block?.body, node.handler.body.body],
34-
TryStatementHandler: node => [],
35-
LogicalExpression: node => [node.left, node.right],
36-
ForStatement: node => [node.body?.body],
37-
ForOfStatement: node => [node.body?.body],
38-
ForInStatement: node => [node.body?.body],
39-
SwitchStatement: node => node.cases,
40-
SwitchCase: node => [node.consequent],
41-
WhileStatement: node => [node.body?.body],
42-
IfStatement: node => [
43-
node.test,
44-
node?.consequent?.body,
45-
node?.alternate?.body
46-
],
47-
FunctionDeclaration: node => [node.declaration.body?.body],
48-
DoWhileStatement: node => [node.body?.body],
49-
BlockStatement: node => [node.body],
50-
VariableDeclaration: node => [node.declarations],
51-
VariableDeclarator: node => [node.init],
52-
ConditionalExpression: node => [
53-
[node.test],
54-
[node.consequent],
55-
[node.alternate]
56-
],
57-
ArrowFunctionExpression: node => node.body.body,
58-
ExpressionStatement: node => node.expression.arguments,
59-
ExportNamedDeclaration: node => [node.declaration],
60-
ExportDefaultDeclaration: node => [node.declaration]
61-
}
62-
63-
const getName = node => {
64-
return node.id.name
65-
}
66-
67-
function determineLogicalComplexity(body) {
68-
let complexity = 0
69-
const output = {}
70-
body.forEach(function cb(node) {
71-
if (!node) return
72-
if (node.type === 'FunctionDeclaration') {
73-
const old = complexity
74-
complexity = 1 // reset clock on each function
75-
node.body.body.forEach(cb)
76-
const name = getName(node)
77-
output[name] = complexity
78-
complexity = old
79-
} else {
80-
const resolvedBody = resolveBody[node.type]
81-
if (!resolvedBody) return
82-
const [shouldIncrease] = increasesComplexity(node)
83-
if (shouldIncrease) {
84-
complexity += 1
85-
}
86-
const nodeBody = resolvedBody(node)
87-
if (nodeBody) {
88-
nodeBody.forEach(cb)
89-
}
90-
}
91-
})
92-
93-
return output
94-
}
95-
96-
/**
97-
* Calculates the Cyclomatic Complexity of a given file.
98-
*
99-
* @param {string} filename
100-
* @returns {number} cyclomatic complexity
101-
*/
102-
export async function calculateComplexity(filename) {
103-
const file = await readFile(filename, 'utf-8')
104-
const tree = ast.parse(file)
105-
return determineLogicalComplexity(tree.body)
106-
}
1+
export * from './calculate-complexity'
2+
export * from './report-generator'

src/report-generator.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import {readdir} from 'fs/promises'
2+
import {calculateComplexity} from './calculate-complexity.js'
3+
4+
export async function getSourceFile(folder, includedType, excludedType) {
5+
let filePaths = []
6+
// get contents for folder
7+
const paths = await readdir(folder, {withFileTypes: true})
8+
// check if item is a directory
9+
10+
for (const path of paths) {
11+
const filePath = `${folder}/${path.name}`
12+
13+
if (path.isDirectory()) {
14+
if (path.name.match(/.*node_modules.*|.git|.github/)) continue
15+
16+
const recursePaths = await getSourceFile(
17+
`${folder}/${path.name}`,
18+
includedType,
19+
excludedType
20+
)
21+
filePaths = filePaths.concat(recursePaths)
22+
} else {
23+
if (filePath.match(includedType) && !filePath.match(excludedType))
24+
filePaths.push(filePath)
25+
}
26+
}
27+
return filePaths
28+
}
29+
30+
export async function generateComplexityReport(directory) {
31+
/**
32+
* Find all files in a subfolder
33+
* find all javascript
34+
*/
35+
const include = /\.js$/
36+
const exclude = /\__mocks__|.test.js|Test.js/
37+
const sourceFiles = await getSourceFile(directory, include, exclude)
38+
const analyzedFiles = await Promise.all(
39+
sourceFiles.map(async file => ({
40+
file,
41+
report: await calculateComplexity(file)
42+
}))
43+
)
44+
console.log(JSON.stringify(analyzedFiles, undefined, 2))
45+
}

0 commit comments

Comments
 (0)