Skip to content

Commit 93c3aae

Browse files
committed
added metrics
1 parent 9a171b3 commit 93c3aae

File tree

4 files changed

+259
-0
lines changed

4 files changed

+259
-0
lines changed
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { describe, test, expect } from "vitest";
2+
import { CMetricsAnalyzer } from "./index.js";
3+
import { getCFilesMap, cFilesFolder } from "../testFiles/index.js";
4+
import path from "path";
5+
6+
describe("CMetricsAnalyzer", () => {
7+
const analyzer = new CMetricsAnalyzer();
8+
const files = getCFilesMap();
9+
10+
const analyzeFile = (filePath: string) => {
11+
const absolutePath = path.join(cFilesFolder, filePath);
12+
const file = files.get(absolutePath);
13+
if (!file) {
14+
throw new Error(`File not found: ${absolutePath}`);
15+
}
16+
return analyzer.analyzeNode(file.rootNode);
17+
};
18+
19+
test("burgers.c", () => {
20+
const metrics = analyzeFile("burgers.c");
21+
expect(metrics.characterCount >= 1485).toBe(true);
22+
expect(metrics.codeCharacterCount < 1485).toBe(true);
23+
expect(metrics.linesCount >= 53).toBe(true);
24+
expect(metrics.codeLinesCount < 53).toBe(true);
25+
expect(metrics.cyclomaticComplexity >= 6).toBe(true);
26+
});
27+
28+
test("burgers.h", () => {
29+
const metrics = analyzeFile("burgers.h");
30+
expect(metrics.characterCount >= 1267).toBe(true);
31+
expect(metrics.codeCharacterCount < 1267).toBe(true);
32+
expect(metrics.linesCount >= 71).toBe(true);
33+
expect(metrics.codeLinesCount < 71).toBe(true);
34+
expect(metrics.cyclomaticComplexity).toBe(1);
35+
});
36+
37+
test("crashcases.h", () => {
38+
const metrics = analyzeFile("crashcases.h");
39+
expect(metrics.characterCount >= 956).toBe(true);
40+
expect(metrics.codeCharacterCount < 956).toBe(true);
41+
expect(metrics.linesCount >= 33).toBe(true);
42+
expect(metrics.codeLinesCount < 33).toBe(true);
43+
expect(metrics.cyclomaticComplexity).toBe(0);
44+
});
45+
});
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import { C_COMPLEXITY_QUERY, C_COMMENT_QUERY } from "./queries.js";
2+
import { CComplexityMetrics, CommentSpan, CodeCounts } from "./types.js";
3+
import Parser from "tree-sitter";
4+
5+
export class CMetricsAnalyzer {
6+
/**
7+
* Calculates metrics for a C symbol.
8+
* @param node - The syntax node to analyze.
9+
* @returns An object containing the complexity metrics.
10+
*/
11+
public analyzeNode(node: Parser.SyntaxNode): CComplexityMetrics {
12+
if (node.type === "preproc_function_def") {
13+
node = node.childForFieldName("value");
14+
}
15+
const complexityCount = this.getComplexityCount(node);
16+
const linesCount = node.endPosition.row - node.startPosition.row + 1;
17+
const codeCounts = this.getCodeCounts(node);
18+
const codeLinesCount = codeCounts.lines;
19+
const characterCount = node.endIndex - node.startIndex;
20+
const codeCharacterCount = codeCounts.characters;
21+
22+
return {
23+
cyclomaticComplexity: complexityCount,
24+
linesCount,
25+
codeLinesCount,
26+
characterCount,
27+
codeCharacterCount,
28+
};
29+
}
30+
31+
private getComplexityCount(node: Parser.SyntaxNode): number {
32+
const complexityMatches = C_COMPLEXITY_QUERY.captures(node);
33+
return complexityMatches.length;
34+
}
35+
36+
/**
37+
* Finds comments in the given node and returns their spans.
38+
* @param node - The AST node to analyze
39+
* @returns An object containing pure comment lines and comment spans
40+
*/
41+
private findComments(
42+
node: Parser.SyntaxNode,
43+
lines: string[],
44+
): {
45+
pureCommentLines: Set<number>;
46+
commentSpans: CommentSpan[];
47+
} {
48+
const pureCommentLines = new Set<number>();
49+
const commentSpans: CommentSpan[] = [];
50+
51+
const commentCaptures = C_COMMENT_QUERY.captures(node);
52+
53+
for (const capture of commentCaptures) {
54+
const commentNode = capture.node;
55+
56+
// Record the comment span for character counting
57+
commentSpans.push({
58+
start: {
59+
row: commentNode.startPosition.row,
60+
column: commentNode.startPosition.column,
61+
},
62+
end: {
63+
row: commentNode.endPosition.row,
64+
column: commentNode.endPosition.column,
65+
},
66+
});
67+
68+
// Check if the comment starts at the beginning of the line (ignoring whitespace)
69+
const lineIdx = commentNode.startPosition.row - node.startPosition.row;
70+
if (lineIdx >= 0 && lineIdx < lines.length) {
71+
const lineText = lines[lineIdx];
72+
const textBeforeComment = lineText.substring(
73+
0,
74+
commentNode.startPosition.column,
75+
);
76+
77+
// If there's only whitespace before the comment, it's a pure comment line
78+
if (textBeforeComment.trim().length === 0) {
79+
for (
80+
let line = commentNode.startPosition.row;
81+
line <= commentNode.endPosition.row;
82+
line++
83+
) {
84+
pureCommentLines.add(line);
85+
}
86+
}
87+
}
88+
}
89+
90+
return { pureCommentLines, commentSpans };
91+
}
92+
93+
/**
94+
* Finds all empty lines in a node
95+
*
96+
* @param node The syntax node to analyze
97+
* @param lines The lines of text in the node
98+
* @returns Set of line numbers that are empty
99+
*/
100+
private findEmptyLines(
101+
node: Parser.SyntaxNode,
102+
lines: string[],
103+
): Set<number> {
104+
const emptyLines = new Set<number>();
105+
for (let i = 0; i < lines.length; i++) {
106+
const lineIndex = node.startPosition.row + i;
107+
if (lines[i].trim().length === 0) {
108+
emptyLines.add(lineIndex);
109+
}
110+
}
111+
112+
return emptyLines;
113+
}
114+
115+
private getCodeCounts(node: Parser.SyntaxNode): CodeCounts {
116+
const lines = node.text.split(/\r?\n/);
117+
const linesCount = lines.length;
118+
// Find comments and their spans
119+
const { pureCommentLines, commentSpans } = this.findComments(node, lines);
120+
121+
// Find empty lines
122+
const emptyLines = this.findEmptyLines(node, lines);
123+
124+
// Calculate code lines
125+
const nonCodeLines = new Set([...pureCommentLines, ...emptyLines]);
126+
const codeLinesCount = linesCount - nonCodeLines.size;
127+
128+
let codeCharCount = 0;
129+
130+
// Process each line individually
131+
for (let i = 0; i < lines.length; i++) {
132+
const lineIndex = node.startPosition.row + i;
133+
const line = lines[i];
134+
135+
// Skip empty lines and pure comment lines
136+
if (emptyLines.has(lineIndex) || pureCommentLines.has(lineIndex)) {
137+
continue;
138+
}
139+
140+
// Process line for code characters
141+
let lineText = line;
142+
143+
// Remove comment content from the line if present
144+
for (const span of commentSpans) {
145+
if (span.start.row === lineIndex) {
146+
// Comment starts on this line
147+
lineText = lineText.substring(0, span.start.column);
148+
}
149+
}
150+
151+
// Count normalized code characters (trim excessive whitespace)
152+
const normalizedText = lineText.trim().replace(/\s+/g, " ");
153+
codeCharCount += normalizedText.length;
154+
}
155+
156+
return {
157+
lines: codeLinesCount,
158+
characters: codeCharCount,
159+
};
160+
}
161+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import Parser from "tree-sitter";
2+
import { cParser } from "../../../helpers/treeSitter/parsers.js";
3+
4+
// Tree-sitter query to find complexity-related nodes
5+
export const C_COMPLEXITY_QUERY = new Parser.Query(
6+
cParser.getLanguage(),
7+
`
8+
(if_statement) @complexity
9+
(while_statement) @complexity
10+
(for_statement) @complexity
11+
(do_statement) @complexity
12+
(case_statement) @complexity
13+
(conditional_expression) @complexity
14+
(preproc_ifdef) @complexity
15+
`,
16+
);
17+
18+
export const C_COMMENT_QUERY = new Parser.Query(
19+
cParser.getLanguage(),
20+
`
21+
(comment) @comment
22+
`,
23+
);
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/**
2+
* Interface for code volumes
3+
*/
4+
export interface CodeCounts {
5+
/** Number of lines of code */
6+
lines: number;
7+
/** Number of characters of code */
8+
characters: number;
9+
}
10+
11+
export interface CommentSpan {
12+
start: { row: number; column: number };
13+
end: { row: number; column: number };
14+
}
15+
16+
/**
17+
* Represents complexity metrics for a C symbol
18+
*/
19+
export interface CComplexityMetrics {
20+
/** Cyclomatic complexity (McCabe complexity) */
21+
cyclomaticComplexity: number;
22+
/** Code lines (not including whitespace or comments) */
23+
codeLinesCount: number;
24+
/** Total lines (including whitespace and comments) */
25+
linesCount: number;
26+
/** Characters of actual code (excluding comments and excessive whitespace) */
27+
codeCharacterCount: number;
28+
/** Total characters in the entire symbol */
29+
characterCount: number;
30+
}

0 commit comments

Comments
 (0)