Skip to content

Commit e75318f

Browse files
author
abacus_fixer
committed
Add post-C++11 rule (-80); expand test dirs; fix string-literal false matches
This commit extends tools/03_code_analysis/code_quality_score.py with one new scoring rule, expands the test-file exclusion list, and fixes a critical class of false positives for keyword-based rules. 1. New rule: post_cpp11_feature (-80, one-shot per file) The ABACUS project keeps a C++11 baseline (see AGENTS.md § Required Baseline rule 7). Any newer syntax is a compilation risk on older compilers, so a one-shot -80 deduction is applied when any of the following high-confidence, low-false-positive patterns is seen: C++14 std::make_unique<T>(...) digit separator in numeric literals (1'000'000) C++17 if constexpr (...) structured binding auto [a, b] = ...; fold expressions (args + ...), (... + args), etc. std::optional<T>, std::variant<T,U>, std::any [[nodiscard]], [[maybe_unused]] attributes C++20 concept / requires / consteval / constinit coroutine keywords: co_await, co_yield, co_return std::span<T>, std::ranges::*, std::format(...) C++23 std::expected<T,E>, std::print(...), std::println(...) Detection uses a list of (label, compiled_regex) pairs defined in POST_CPP11_PATTERNS. A single Finding is emitted per file listing all distinct features and their line numbers so the report is actionable. 2. Test directory exclusion: add "test_serial" to SKIP_DIRS The exclusion set previously contained {test, tests, test_parallel, unit_test, unittest} but missed test_serial/ under source_io and source_base; nine files leaked into score summaries. Now skipped. 3. False-positive fix: introduce strip_strings() helper strip_comments() erases comments but preserves string literals on purpose (brace-matching parsers later rely on the real quote boundaries). That meant keyword-based rules (e.g. the C++20 requires regex) matched ordinary words inside user-facing strings such as WARNING_QUIT("... eigensolver requires replicated ..."). The new strip_strings() function walks through content character by character, tracks "... " and '...' modes, and replaces every character inside quotes with a space (newlines are preserved so line numbers stay correct). find_post_cpp11_features() now runs on strip_strings(strip_comments(content)) — the double pass eliminates string-literal false matches while still catching real keywords. 4. Results on source/ (1643 files, excluding test dirs): - Avg score: 79.1 (previous scan w/ buggy version: 78.6) - Pass rate (>=60): 1348/1643 = 82.0% - post_cpp11_feature triggered on exactly 1 file after the fix: source/source_hsolver/diago_pexsi.cpp -> std::make_unique (C++14) The previous 16 files flagged as "requires (C++20)" were all string-literal false matches and are now correctly cleared.
1 parent c0b9ce7 commit e75318f

1 file changed

Lines changed: 215 additions & 1 deletion

File tree

tools/03_code_analysis/code_quality_score.py

Lines changed: 215 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@
2929
- function with more than 7 parameters: -1 per extra param (cap 30 per file)
3030
- function cyclomatic complexity > 10 (if/for/while/switch/case/&&/||):
3131
-1 per extra point (cap 30 per file)
32+
- post-C++11 feature usage: -80 per file (one-shot); detects high-confidence
33+
C++14/17/20/23 tokens such as `std::make_unique`, `if constexpr`,
34+
`[[nodiscard]]`, `auto [...]` structured bindings, `concept`,
35+
`requires`, `consteval`, `co_await`, `std::optional`, `std::variant`,
36+
`std::any`, `std::span`, `std::expected`, `std::format`, etc.
3237
3338
Usage:
3439
python3 code_quality_score.py source/source_base
@@ -56,7 +61,7 @@
5661
".git", "build", "__pycache__", "node_modules", ".cache",
5762
"third_party", "thirdparty", ".vscode", ".idea", ".trae-cn",
5863
"Dependencies",
59-
"test", "tests", "test_parallel", "unit_test", "unittest",
64+
"test", "tests", "test_serial", "test_parallel", "unit_test", "unittest",
6065
}
6166

6267
CAPS = {
@@ -94,6 +99,7 @@
9499
"raw_new_keyword": 1,
95100
"too_many_parameters": 1,
96101
"high_cyclomatic_complexity": 1,
102+
"post_cpp11_feature": 80,
97103
}
98104

99105
CAPS = {
@@ -166,6 +172,103 @@
166172
QUALIFIER_RE = re.compile(r"\b(?:const|override|final|noexcept)\b")
167173
CYCLO_KEYWORDS_RE = re.compile(r"\b(?:if|for|while|switch|case)\b|&&|\|\|")
168174

175+
# Post-C++11 features with low false-positive rates.
176+
# Each entry is (label, compiled_regex) — the label appears in the finding.
177+
POST_CPP11_PATTERNS: List[Tuple[str, "re.Pattern[str]"]] = [
178+
# C++14
179+
(
180+
"std::make_unique",
181+
re.compile(r"\bstd::make_unique\s*<"),
182+
),
183+
(
184+
"digit-separator in numeric literal (C++14)",
185+
re.compile(r"(?<!')\b\d[\d']*'[\d']*\b"),
186+
),
187+
# C++17
188+
(
189+
"if constexpr (C++17)",
190+
re.compile(r"\bif\s+constexpr\b"),
191+
),
192+
(
193+
"structured binding auto [...] (C++17)",
194+
re.compile(r"\bauto\s*&?\s*\[[^\[\]]+\]\s*="),
195+
),
196+
(
197+
"fold expression (C++17)",
198+
re.compile(
199+
r"\(\s*[A-Za-z_]\w*\s*(?:\+\+|\&\&|\|\||[+\-*/%^&|<>]=?|[.<>=])"
200+
r"\s*\.\.\.\s*\)"
201+
r"|\(\s*\.\.\.\s*(?:\+\+|\&\&|\|\||[+\-*/%^&|<>]=?|[.<>=])"
202+
r"\s*[A-Za-z_]\w*\s*\)"
203+
r"|\(\s*[A-Za-z_]\w*\s*(?:\+\+|\&\&|\|\||[+\-*/%^&|<>]=?|[.<>=])"
204+
r"\s*\.\.\.\s*(?:\+\+|\&\&|\|\||[+\-*/%^&|<>]=?|[.<>=])"
205+
r"\s*[^,)]+\s*\)"
206+
),
207+
),
208+
(
209+
"std::optional (C++17)",
210+
re.compile(r"\bstd::optional\b"),
211+
),
212+
(
213+
"std::variant (C++17)",
214+
re.compile(r"\bstd::variant\b"),
215+
),
216+
(
217+
"std::any (C++17)",
218+
re.compile(r"\bstd::any\b"),
219+
),
220+
(
221+
"[[nodiscard]] (C++17)",
222+
re.compile(r"\[\[nodiscard\b"),
223+
),
224+
(
225+
"[[maybe_unused]] (C++17)",
226+
re.compile(r"\[\[maybe_unused\b"),
227+
),
228+
# C++20
229+
(
230+
"concept (C++20)",
231+
re.compile(r"\bconcept\b"),
232+
),
233+
(
234+
"requires (C++20)",
235+
re.compile(r"\brequires\b"),
236+
),
237+
(
238+
"consteval (C++20)",
239+
re.compile(r"\bconsteval\b"),
240+
),
241+
(
242+
"constinit (C++20)",
243+
re.compile(r"\bconstinit\b"),
244+
),
245+
(
246+
"coroutine co_await / co_yield / co_return (C++20)",
247+
re.compile(r"\bco_(?:await|yield|return)\b"),
248+
),
249+
(
250+
"std::span (C++20)",
251+
re.compile(r"\bstd::span\b"),
252+
),
253+
(
254+
"std::ranges (C++20)",
255+
re.compile(r"\bstd::ranges::"),
256+
),
257+
(
258+
"std::format (C++20)",
259+
re.compile(r"\bstd::format\b"),
260+
),
261+
# C++23
262+
(
263+
"std::expected (C++23)",
264+
re.compile(r"\bstd::expected\b"),
265+
),
266+
(
267+
"std::print / std::println (C++23)",
268+
re.compile(r"\bstd::print(?:ln)?\s*\("),
269+
),
270+
]
271+
169272

170273
@dataclass
171274
class Finding:
@@ -285,6 +388,77 @@ def strip_comments(content: str) -> str:
285388
return "".join(out)
286389

287390

391+
def strip_strings(content: str) -> str:
392+
"""Return content with string and character literals erased to spaces,
393+
preserving line numbers.
394+
395+
The result still has the same number of characters and lines, but no
396+
letter/word survives inside "..." or '...'. This prevents false matches
397+
on keywords that happen to appear inside string literals.
398+
"""
399+
out = []
400+
i = 0
401+
n = len(content)
402+
in_string = False
403+
in_char = False
404+
while i < n:
405+
c = content[i]
406+
if in_string:
407+
if c == "\\" and i + 1 < n:
408+
# keep the escape sequence length but blank it out
409+
out.append(" ")
410+
if content[i + 1] == "\n":
411+
out.append("\n")
412+
else:
413+
out.append(" ")
414+
i += 2
415+
continue
416+
if c == '"':
417+
in_string = False
418+
out.append('"') # keep the boundary marker (safest for line numbers)
419+
i += 1
420+
continue
421+
if c == "\n":
422+
out.append("\n")
423+
else:
424+
out.append(" ")
425+
i += 1
426+
continue
427+
if in_char:
428+
if c == "\\" and i + 1 < n:
429+
out.append(" ")
430+
if content[i + 1] == "\n":
431+
out.append("\n")
432+
else:
433+
out.append(" ")
434+
i += 2
435+
continue
436+
if c == "'":
437+
in_char = False
438+
out.append("'")
439+
i += 1
440+
continue
441+
if c == "\n":
442+
out.append("\n")
443+
else:
444+
out.append(" ")
445+
i += 1
446+
continue
447+
if c == '"':
448+
in_string = True
449+
out.append('"')
450+
i += 1
451+
continue
452+
if c == "'":
453+
in_char = True
454+
out.append("'")
455+
i += 1
456+
continue
457+
out.append(c)
458+
i += 1
459+
return "".join(out)
460+
461+
288462
def find_class_blocks(code: str) -> List[Tuple[int, int, str, str]]:
289463
"""Find top-level class/struct blocks. Returns list of
290464
(start_line_1indexed, end_line_1indexed, kind, name).
@@ -644,6 +818,30 @@ def find_high_complexity_functions(
644818
return findings
645819

646820

821+
def find_post_cpp11_features(
822+
content: str,
823+
) -> List[Tuple[int, str]]:
824+
"""Find the first occurrence of each post-C++11 feature.
825+
826+
Uses the patterns in POST_CPP11_PATTERNS against content with both
827+
comments AND string literals blanked out. String blanking prevents
828+
false matches on keywords embedded in user-facing messages (e.g.
829+
`"X requires Y"` in a WARNING_QUIT call).
830+
831+
Returns a list of (line_no, feature_label) — one entry per distinct
832+
detected feature so the finding message is informative (the deduction
833+
is one-shot -80 per file regardless of how many features hit).
834+
"""
835+
stripped = strip_strings(strip_comments(content))
836+
hits: List[Tuple[int, str]] = []
837+
for label, regex in POST_CPP11_PATTERNS:
838+
m = regex.search(stripped)
839+
if m:
840+
line_no = stripped[:m.start()].count("\n") + 1
841+
hits.append((line_no, label))
842+
return hits
843+
844+
647845
def analyze_class_blocks(content: str) -> Tuple[List[Finding], List[Finding], List[Finding]]:
648846
"""Analyze class/struct blocks for public member variables, long member
649847
functions, and member/local name conflicts.
@@ -907,6 +1105,22 @@ def append_capped(rule: str, count: int) -> None:
9071105
deduction=per_deduction,
9081106
))
9091107

1108+
# post-C++11 feature rule: one-shot -80, lists all distinct features
1109+
post_cpp11_hits = find_post_cpp11_features(content)
1110+
if post_cpp11_hits:
1111+
feature_list = ", ".join(
1112+
f"'{label}' (line {ln})" for ln, label in post_cpp11_hits
1113+
)
1114+
findings.append(Finding(
1115+
rule="post_cpp11_feature",
1116+
line=post_cpp11_hits[0][0],
1117+
reason=(
1118+
f"uses post-C++11 feature(s): {feature_list} "
1119+
f"(repo baseline is C++11)"
1120+
),
1121+
deduction=WEIGHTS["post_cpp11_feature"],
1122+
))
1123+
9101124
# class-based rules
9111125
pub_findings, long_func_findings, conflict_findings = analyze_class_blocks(content)
9121126
findings.extend(pub_findings)

0 commit comments

Comments
 (0)