Skip to content

Add a python script to test the code quality of ABACUS codes - #7843

Open
mohanchen wants to merge 19 commits into
deepmodeling:developfrom
mohanchen:2026-08-20
Open

Add a python script to test the code quality of ABACUS codes#7843
mohanchen wants to merge 19 commits into
deepmodeling:developfrom
mohanchen:2026-08-20

Conversation

@mohanchen

Copy link
Copy Markdown
Collaborator

Add a python script to test the code quality of ABACUS codes

abacus_fixer added 3 commits August 20, 2026 22:27
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)
@mohanchen
mohanchen requested a review from Critsium-xy August 20, 2026 14:53
@mohanchen mohanchen added the Compile & CICD & Docs & Dependencies Issues related to compiling ABACUS label Aug 20, 2026
abacus_fixer and others added 16 commits August 21, 2026 07:49
… 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.
@mohanchen
mohanchen requested a review from AsTonyshment August 24, 2026 01:22
@mohanchen mohanchen added the Tests/Examples Issues/PR related to unit tests and integrate tests label Aug 24, 2026

@AsTonyshment AsTonyshment left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +890 to +894
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))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +853 to +856
# we need a `{` body (not `;` declaration, not `= 0` pure virtual)
if pos >= n or stripped[pos] != "{":
i = j + 1
continue

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +509 to +513
if c == "'":
in_char = True
out.append("'")
i += 1
continue

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +767 to +771
# 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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +618 to +624
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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +1013 to +1014
# apply brace counting for this line
depth = prev_depth + line.count("{") - line.count("}")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(?![.:])")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +139 to 142
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();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might suggest slightly changing the rule for public members in structs. ABACUS 开源项目 C++ 代码规范 says:

Image

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Compile & CICD & Docs & Dependencies Issues related to compiling ABACUS Tests/Examples Issues/PR related to unit tests and integrate tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants