Add a python script to test the code quality of ABACUS codes - #7843
Add a python script to test the code quality of ABACUS codes#7843mohanchen wants to merge 19 commits into
Conversation
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)
… 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.
AsTonyshment
left a comment
There was a problem hiding this comment.
Keeping the source compatible with C++11 is a reasonable goal, especially for older HPC systems (even though the current CMake configuration uses at least C++14 when ENABLE_PEXSI, ENABLE_LIBRI, or BUILD_TESTING is enabled, when USE_CUDA is used with CUDA Toolkit < 13, or when 1.5.0 <= Torch_VERSION < 2.1.0; it uses at least C++17 when USE_CUDA is used with CUDA Toolkit >= 13, or when Torch_VERSION >= 2.1.0).
However, my coding agent tested the parser with many small, valid C++11 examples and found several reproducible false positives and missed cases. I have described these cases, together with a few suggestions of my own about the related C++ changes and scoring rules, in the inline comments. I hope they are helpful :)
| i = 0 | ||
| n = len(code) | ||
| while i < n: | ||
| m = CLASS_OPEN_RE.search(code, i) |
There was a problem hiding this comment.
This also matches class inside a template parameter list. Reproducer:
template <class T>
void f()
{
int x = 0;
}find_class_blocks() returns [(1, 5, "class", "T")], treating the function body as class T. Please distinguish class definitions from template parameters.
Furthermore, this also matches scoped enums. Reproducer:
enum class Kind
{
first,
second
};find_class_blocks() returns a class block named Kind. Please exclude enum class before running class-member checks.
| stripped = strip_comments(content) | ||
| findings: List[Tuple[int, str, int]] = [] | ||
| for sig_line, name, body_open, body_close in find_function_bodies(content): | ||
| body = stripped[body_open + 1:body_close] | ||
| complexity = len(CYCLO_KEYWORDS_RE.findall(body)) |
There was a problem hiding this comment.
Strings are still present in stripped, so words in messages count as control flow. Reproducer:
void f()
{
const char* text = "if if if if if if if if if if if";
}The function is reported with complexity 11. Please remove string and character literals before applying CYCLO_KEYWORDS_RE.
| # we need a `{` body (not `;` declaration, not `= 0` pure virtual) | ||
| if pos >= n or stripped[pos] != "{": | ||
| i = j + 1 | ||
| continue |
There was a problem hiding this comment.
This only accepts qualifiers followed directly by {, so valid C++11 trailing-return functions are missed. Reproducer:
auto f(int x) -> int
{
if (x > 0)
{
return 1;
}
return 0;
}find_function_bodies() returns an empty list. Please handle -> return_type before looking for the body.
Furthermore, constructor initializer lists are not handled. Reproducer:
A::A(int x) : x_(x)
{
if (x > 0)
{
x_ = 0;
}
}The parser reports a function named x_ instead of constructor A::A. Please parse or skip the initializer list before finding the body.
| if c == "'": | ||
| in_char = True | ||
| out.append("'") | ||
| i += 1 | ||
| continue |
There was a problem hiding this comment.
Every ' starts character-literal mode here, including a digit separator. Reproducer:
int value = 1'000'000;find_post_cpp11_features() returns no match, so the advertised C++14 digit-separator rule does not work. Please distinguish numeric separators from character literals.
| # for `;` and `=`, require a return-type prefix (else it looks like a | ||
| # function call). For `{`, allow empty prefix (constructor/destructor). | ||
| if after[k] in ";=" and not prefix_stripped: | ||
| i = j + 1 | ||
| continue |
There was a problem hiding this comment.
A non-empty prefix is not enough to identify a declaration. Reproducer:
int f(int a, int b, int c, int d, int e, int f, int g, int h);
int g()
{
return f(1, 2, 3, 4, 5, 6, 7, 8);
}find_long_function_signatures() reports f twice. The second match is only a call in a return statement.
| if s.startswith(bad_prefixes): | ||
| return False | ||
| # skip macro-like lines (all caps with parens already excluded above) | ||
| # require at least one identifier character | ||
| if not re.search(r"[A-Za-z_]", s): | ||
| return False | ||
| return True |
There was a problem hiding this comment.
A continued type alias is accepted as a member variable. Reproducer:
struct A
{
using value_type
= std::vector<int>;
};The result is public member in struct A: = std::vector<int>;. The same false positive occurs in sto_tool.h; please account for multi-line using declarations.
| # apply brace counting for this line | ||
| depth = prev_depth + line.count("{") - line.count("}") |
There was a problem hiding this comment.
Counting braces on lines that still contain strings breaks the class depth. Reproducer:
class A
{
public:
void f()
{
const char* text = "{";
}
int value;
};analyze_class_blocks() does not find public member value. Please remove literals before counting braces.
|
|
||
| CHINESE_RE = re.compile("[\u4e00-\u9fff]") | ||
| USING_NS_STD_RE = re.compile(r"\busing\s+namespace\s+std\b") | ||
| UPPERCASE_CONST_RE = re.compile(r"(?<![.:])\b[A-Z][A-Z0-9_]{2,}\b(?![.:])") |
There was a problem hiding this comment.
The lookbehind excludes x.UPPER and Type::UPPER, but not ptr->UPPER. Reproducer:
value = ptr->UPPER_MEMBER;UPPERCASE_CONST_RE matches UPPER_MEMBER, so equivalent member accesses receive different scores. Please exclude -> member access as well.
| std::unique_ptr<T> one(new T(1.0)); | ||
| std::unique_ptr<T> zero(new T(0.0)); | ||
| const T *one_ = one.get(); | ||
| const T *zero_ = zero.get(); |
There was a problem hiding this comment.
one and zero are only read by GEMM, so I think they do not need heap allocation. Using
const T one(1.0);
const T zero(0.0);and then passing &one and &zero would be much simpler and would remain fully C++11-compatible.
| the parameter list spans lines. | ||
| - #include of .hpp implementation header: -2 per occurrence | ||
| (cap 5) (AGENTS.md §3, §4) | ||
| - each public member variable in a class/struct: -1 |
There was a problem hiding this comment.
I might suggest slightly changing the rule for public members in structs. ABACUS 开源项目 C++ 代码规范 says:
So I think most public data members (default behavior of struct) are intended design of these data-only structs. In my own opinion, it would be more useful to flag member functions in structs instead of their public data members.
Add a python script to test the code quality of ABACUS codes