Skip to content

Commit c0b9ce7

Browse files
author
abacus_fixer
committed
Add cyclomatic complexity rule; raise file_too_long weight to 2
Extends tools/03_code_analysis/code_quality_score.py with two changes: - New `high_cyclomatic_complexity` rule: counts if/for/while/switch/case/ &&/|| per function body (McCabe complexity). Threshold 10, -1 per extra point, capped at 30 per file. Identifies functions that should be split. - `file_too_long` weight raised from -1 to -2 per 50-line block beyond 500 lines, reflecting the higher maintenance cost of very large files. Implementation: - `find_function_bodies()` locates function definitions with `{...}` bodies, reusing the prefix/reject logic from find_long_function_signatures so that function calls, lambdas, macros, and function-pointer typedefs are excluded. - `find_high_complexity_functions()` walks each body and counts control-flow keywords via CYCLO_KEYWORDS_RE. - Cyclomatic complexity follows McCabe: `else if` counts as two `if`, `switch` + each `case` count separately, `&&`/`||` each add 1. Scan results on source/ (1652 files, excluding test/ dirs): - Average score: 79.1 (was 82.1) - Passing rate (>=60): 1355/1652 = 82.1% - high_cyclomatic_complexity triggered: 594 functions - file_too_long triggered: 167 files Top offenders identified by the new rule: - source_hamilt/module_xc/xc_grad.cpp:28 `gradcorr` (complexity 145) - source_lcao/force_stress_lcao.cpp:69 `getForceStress` (103) - source_lcao/module_deepks/lcao_deepks_iface.cpp:63 `out_deepks_labels` (94) - source_io/module_ctrl/ctrl_scf_lcao.cpp:82 `ctrl_scf_lcao` (68) - source_estate/module_charge/charge.cpp:245 `atomic_rho` (60)
1 parent c15fa10 commit c0b9ce7

1 file changed

Lines changed: 147 additions & 2 deletions

File tree

tools/03_code_analysis/code_quality_score.py

Lines changed: 147 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,11 @@
2424
- `friend` keyword exposing internals: -1 per occurrence (cap 5)
2525
- unpaired `new` without matching `delete`: -1 per occurrence (cap 5)
2626
- local variable shadowing a member variable: -1 per occurrence (cap 5)
27-
- file longer than 500 lines: -1 per additional 50-line block
27+
- file longer than 500 lines: -2 per additional 50-line block
2828
- each `new` keyword usage: -1 per occurrence (no cap)
2929
- function with more than 7 parameters: -1 per extra param (cap 30 per file)
30+
- function cyclomatic complexity > 10 (if/for/while/switch/case/&&/||):
31+
-1 per extra point (cap 30 per file)
3032
3133
Usage:
3234
python3 code_quality_score.py source/source_base
@@ -88,9 +90,10 @@
8890
"friend_keyword": 1,
8991
"unpaired_new_delete": 1,
9092
"member_local_name_conflict": 1,
91-
"file_too_long": 1,
93+
"file_too_long": 2,
9294
"raw_new_keyword": 1,
9395
"too_many_parameters": 1,
96+
"high_cyclomatic_complexity": 1,
9497
}
9598

9699
CAPS = {
@@ -106,13 +109,15 @@
106109
"unpaired_new_delete": 5,
107110
"member_local_name_conflict": 5,
108111
"too_many_parameters": 30,
112+
"high_cyclomatic_complexity": 30,
109113
}
110114

111115
FUNCTION_LENGTH_THRESHOLD = 50
112116
FUNCTION_LENGTH_STEP = 50
113117
FILE_LENGTH_THRESHOLD = 500
114118
FILE_LENGTH_STEP = 50
115119
FUNCTION_PARAM_THRESHOLD = 7
120+
CYCLO_THRESHOLD = 10
116121
LINE_LENGTH_LIMIT = 120
117122
FILENAME_LENGTH_LIMIT = 20
118123
PASS_THRESHOLD = 60
@@ -159,6 +164,7 @@
159164
}
160165
FUNC_NAME_RE = re.compile(r"\b([A-Za-z_]\w*)\s*\(")
161166
QUALIFIER_RE = re.compile(r"\b(?:const|override|final|noexcept)\b")
167+
CYCLO_KEYWORDS_RE = re.compile(r"\b(?:if|for|while|switch|case)\b|&&|\|\|")
162168

163169

164170
@dataclass
@@ -521,6 +527,123 @@ def find_long_function_signatures(
521527
return findings
522528

523529

530+
def find_function_bodies(content: str) -> List[Tuple[int, str, int, int]]:
531+
"""Find function definitions (with body), not just declarations.
532+
533+
Returns list of (signature_line_no, function_name, body_start_pos,
534+
body_end_pos) where positions are absolute offsets in stripped content.
535+
Reuses the prefix/reject logic from find_long_function_signatures.
536+
"""
537+
stripped = strip_comments(content)
538+
n = len(stripped)
539+
bodies: List[Tuple[int, str, int, int]] = []
540+
541+
i = 0
542+
while i < n:
543+
m = FUNC_NAME_RE.search(stripped, i)
544+
if not m:
545+
break
546+
name = m.group(1)
547+
if name in NON_FUNCTION_KEYWORDS:
548+
i = m.end()
549+
continue
550+
551+
line_start = stripped.rfind("\n", 0, m.start()) + 1
552+
prefix = stripped[line_start:m.start()]
553+
prefix_stripped = prefix.rstrip()
554+
prefix_lstripped = prefix.lstrip()
555+
556+
if prefix_lstripped.startswith("#"):
557+
i = m.end()
558+
continue
559+
if prefix_stripped.endswith("]"):
560+
i = m.end()
561+
continue
562+
if prefix_stripped.endswith(".") or prefix_stripped.endswith("->"):
563+
i = m.end()
564+
continue
565+
if prefix_stripped.endswith("=") and not prefix_stripped.endswith("=="):
566+
i = m.end()
567+
continue
568+
if "typedef" in prefix_stripped:
569+
i = m.end()
570+
continue
571+
572+
paren_open = m.end() - 1
573+
depth = 1
574+
j = paren_open + 1
575+
while j < n and depth > 0:
576+
c = stripped[j]
577+
if c == "(":
578+
depth += 1
579+
elif c == ")":
580+
depth -= 1
581+
if depth == 0:
582+
break
583+
j += 1
584+
if depth != 0:
585+
i = m.end()
586+
continue
587+
588+
# find what comes after `)`: skip whitespace and qualifiers
589+
pos = j + 1
590+
while pos < n and stripped[pos] in " \t\n":
591+
pos += 1
592+
while True:
593+
mq = QUALIFIER_RE.match(stripped, pos)
594+
if not mq:
595+
break
596+
pos = mq.end()
597+
while pos < n and stripped[pos] in " \t\n":
598+
pos += 1
599+
600+
# we need a `{` body (not `;` declaration, not `= 0` pure virtual)
601+
if pos >= n or stripped[pos] != "{":
602+
i = j + 1
603+
continue
604+
605+
# match braces to find body end
606+
body_open = pos
607+
depth = 1
608+
body_close = body_open + 1
609+
while body_close < n and depth > 0:
610+
c = stripped[body_close]
611+
if c == "{":
612+
depth += 1
613+
elif c == "}":
614+
depth -= 1
615+
if depth == 0:
616+
break
617+
body_close += 1
618+
if depth != 0:
619+
i = j + 1
620+
continue
621+
622+
sig_line_no = stripped[:paren_open].count("\n") + 1
623+
bodies.append((sig_line_no, name, body_open, body_close))
624+
i = body_close + 1
625+
626+
return bodies
627+
628+
629+
def find_high_complexity_functions(
630+
content: str, threshold: int
631+
) -> List[Tuple[int, str, int]]:
632+
"""Find functions whose cyclomatic complexity exceeds threshold.
633+
634+
Cyclomatic complexity counts: if, for, while, switch, case, &&, ||.
635+
Returns list of (line_no, function_name, complexity).
636+
"""
637+
stripped = strip_comments(content)
638+
findings: List[Tuple[int, str, int]] = []
639+
for sig_line, name, body_open, body_close in find_function_bodies(content):
640+
body = stripped[body_open + 1:body_close]
641+
complexity = len(CYCLO_KEYWORDS_RE.findall(body))
642+
if complexity > threshold:
643+
findings.append((sig_line, name, complexity))
644+
return findings
645+
646+
524647
def analyze_class_blocks(content: str) -> Tuple[List[Finding], List[Finding], List[Finding]]:
525648
"""Analyze class/struct blocks for public member variables, long member
526649
functions, and member/local name conflicts.
@@ -762,6 +885,28 @@ def append_capped(rule: str, count: int) -> None:
762885
deduction=per_deduction,
763886
))
764887

888+
# high cyclomatic complexity rule (per-function, capped across the file)
889+
high_cyclo_funcs = find_high_complexity_functions(content, CYCLO_THRESHOLD)
890+
cap_cyclo = CAPS.get("high_cyclomatic_complexity")
891+
running_cyclo_deduction = 0
892+
for line_no, fname, cyclo in high_cyclo_funcs:
893+
excess = cyclo - CYCLO_THRESHOLD
894+
per_deduction = excess * WEIGHTS["high_cyclomatic_complexity"]
895+
if cap_cyclo is not None and running_cyclo_deduction + per_deduction > cap_cyclo:
896+
per_deduction = max(0, cap_cyclo - running_cyclo_deduction)
897+
if per_deduction == 0:
898+
break
899+
running_cyclo_deduction += per_deduction
900+
findings.append(Finding(
901+
rule="high_cyclomatic_complexity",
902+
line=line_no,
903+
reason=(
904+
f"function '{fname}' has cyclomatic complexity {cyclo} "
905+
f"(exceeds {CYCLO_THRESHOLD} by {excess})"
906+
),
907+
deduction=per_deduction,
908+
))
909+
765910
# class-based rules
766911
pub_findings, long_func_findings, conflict_findings = analyze_class_blocks(content)
767912
findings.extend(pub_findings)

0 commit comments

Comments
 (0)