Skip to content

#952: Report NPath complexity by method#1246

Open
Beeeeeeee-c wants to merge 1 commit into
cqfn:masterfrom
Beeeeeeee-c:fix-issue-952-npath-report
Open

#952: Report NPath complexity by method#1246
Beeeeeeee-c wants to merge 1 commit into
cqfn:masterfrom
Beeeeeeee-c:fix-issue-952-npath-report

Conversation

@Beeeeeeee-c

@Beeeeeeee-c Beeeeeeee-c commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features

    • NPath complexity can now be calculated even when Maven is unavailable.
    • Added per-method NPath reporting, including class name, method name, and complexity.
  • Bug Fixes

    • Improved NPath behavior in environments without Maven so metric checks still work.
    • Updated test expectations to handle both Maven and non-Maven setups consistently.
  • Tests

    • Enabled several previously skipped or expected-to-fail tests to run normally.
    • Added coverage for NPath reporting output and related pipeline behavior.

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a fallback path in the NPath metric to compute complexity without Maven when mvn is unavailable, introduces a report() method returning per-method complexity data, and re-enables several previously skipped/xfailed tests with updated assertions.

Changes

Maven-free NPath metric and reporting

Layer / File(s) Summary
Report data shape and Maven fallback entry point
aibolit/metrics/npath/main.py
Adds NPathMethodReport TypedDict and an early shutil.which('mvn') check in value() that routes to a new _value_without_maven() method building the AST and computing complexity directly.
Per-method report aggregation
aibolit/metrics/npath/main.py
Refactors MvnFreeNPathMetric.value() to sum complexities from a new report() method iterating class subtrees and methods to build NPathMethodReport entries.
NPath test updates for mvn-conditional behavior and report() coverage
test/metrics/npath/test_all_types.py
Updates existing assertions to branch on mvn availability, and adds new tests plus a _report helper covering single-class and multi-class report() output.

Re-enabling skipped and xfailed tests

Layer / File(s) Summary
Metric AST-compatibility filtering
test/config/test_config.py
Removes pytest import and @pytest.mark.xfail, and filters metrics via metrics_exclude before running AST-parameter assertions.
Re-enabled recommend pipeline and stats tests
test/recommend/test_recommend_pipeline.py, test/stats/test_stats.py
Removes skip imports and @skip decorators so previously skipped tests run, and updates the expected MD5 checksum in the recommend pipeline test.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Suggested reviewers: yegor256, ivanovmg

Poem

A rabbit hopped where mvn was gone,
Found a path without it, and carried on,
Reports of methods, class by class,
Skipped tests woken, let them pass,
Complexity counted, hop by hop! 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: reporting NPath complexity by method.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (1)
test/config/test_config.py (1)

9-19: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Stale docstring/TODO after scope change.

The TODO still says "Once all metrics meet requirements, remove the decorator." but the decorator is already removed and the test now permanently narrows its scope via metrics_exclude rather than covering all metrics. The comment should be updated to reflect that excluded metrics are intentionally not verified going forward, otherwise the docstring misleads future readers about the test's actual coverage.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/config/test_config.py` around lines 9 - 19, Update the docstring/TODO in
test_each_metric_in_config_accepts_ast to match the current test behavior: it no
longer removes a decorator after all metrics comply, and it now intentionally
uses metrics_exclude to narrow coverage. Clarify that excluded metrics are
intentionally not verified by this test going forward, and remove the outdated
wording about removing the decorator. Refer to
test_each_metric_in_config_accepts_ast and metrics_exclude when revising the
text.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@aibolit/metrics/npath/main.py`:
- Around line 67-73: _`_value_without_maven()` should preserve the same input
handling as `value()` and not reject directory inputs._ Update the input
validation in `_value_without_maven` to accept both `os.path.isdir(self.input)`
and `os.path.isfile(self.input)`, matching the behavior in `NPathMetric.value`.
Then make sure the AST construction path can handle either case consistently
before calling `MvnFreeNPathMetric.value()`.
- Around line 110-122: The report generation in report() and the aggregate in
value() are skipping methods declared inside nested classes because
AST.subtrees(ASTNodeType.CLASS_DECLARATION) and class_declaration.methods only
cover direct class methods. Update the traversal in NPathCalculator so nested
class methods are either included intentionally or explicitly excluded with the
correct behavior, and add a regression test covering an inner class to verify
the expected result.

In `@test/config/test_config.py`:
- Around line 21-27: The test setup is incorrectly filtering metrics using
metrics_exclude, which can hide valid AST-compatible metrics like
NumberMethods/M8. Update the logic in the test_config setup to select metrics
based on AST compatibility instead of reusing metrics_exclude, and reference the
existing ast_compatible_metrics loop in test_config.py to either iterate over
all metrics or maintain a separate list of AST-incompatible metrics.

In `@test/metrics/npath/test_all_types.py`:
- Around line 16-24: The no-mvn branch in testIncorrectFormat uses a blanket
Exception assertion, which is too loose. Tighten the pytest.raises call to match
the specific message raised by NPathMetric.value/_value_without_maven, similar
to how the mvn branch matches PMDException. Use the known “File ... does not
exist” message pattern so the test verifies the intended failure path without
accepting unrelated exceptions.

---

Nitpick comments:
In `@test/config/test_config.py`:
- Around line 9-19: Update the docstring/TODO in
test_each_metric_in_config_accepts_ast to match the current test behavior: it no
longer removes a decorator after all metrics comply, and it now intentionally
uses metrics_exclude to narrow coverage. Clarify that excluded metrics are
intentionally not verified by this test going forward, and remove the outdated
wording about removing the decorator. Refer to
test_each_metric_in_config_accepts_ast and metrics_exclude when revising the
text.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b42d55ee-8d5f-4e4c-b59d-9c05ea106c9c

📥 Commits

Reviewing files that changed from the base of the PR and between 0b0555f and c34c7ef.

⛔ Files ignored due to path filters (1)
  • test/stats/results_test.csv is excluded by !**/*.csv
📒 Files selected for processing (5)
  • aibolit/metrics/npath/main.py
  • test/config/test_config.py
  • test/metrics/npath/test_all_types.py
  • test/recommend/test_recommend_pipeline.py
  • test/stats/test_stats.py

Comment on lines +67 to +73
def _value_without_maven(self):
if not os.path.isfile(self.input):
raise Exception(' '.join(['File', self.input, 'does not exist']))
ast = AST.build_from_javalang(build_ast(self.input))
complexity = MvnFreeNPathMetric(ast).value()
return {'data': [{'file': self.input, 'complexity': complexity}], 'errors': []}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

_value_without_maven() drops directory-input support that the mvn path provides.

The mvn path (value(), lines 43-48) supports both os.path.isdir(self.input) and os.path.isfile(self.input). _value_without_maven() only checks os.path.isfile, so a directory input that previously worked (with mvn installed) will now raise 'File ... does not exist' whenever mvn is unavailable — same input, different outcome depending on the environment.

🐛 Sketch of a fix supporting directory input
 def _value_without_maven(self):
-    if not os.path.isfile(self.input):
-        raise Exception(' '.join(['File', self.input, 'does not exist']))
-    ast = AST.build_from_javalang(build_ast(self.input))
-    complexity = MvnFreeNPathMetric(ast).value()
-    return {'data': [{'file': self.input, 'complexity': complexity}], 'errors': []}
+    if os.path.isfile(self.input):
+        files = [self.input]
+    elif os.path.isdir(self.input):
+        files = [
+            os.path.join(dirpath, name)
+            for dirpath, _, names in os.walk(self.input)
+            for name in names if name.endswith('.java')
+        ]
+    else:
+        raise Exception(' '.join(['File', self.input, 'does not exist']))
+    data = []
+    for f in files:
+        ast = AST.build_from_javalang(build_ast(f))
+        complexity = MvnFreeNPathMetric(ast).value()
+        data.append({'file': f, 'complexity': complexity})
+    return {'data': data, 'errors': []}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def _value_without_maven(self):
if not os.path.isfile(self.input):
raise Exception(' '.join(['File', self.input, 'does not exist']))
ast = AST.build_from_javalang(build_ast(self.input))
complexity = MvnFreeNPathMetric(ast).value()
return {'data': [{'file': self.input, 'complexity': complexity}], 'errors': []}
def _value_without_maven(self):
if os.path.isfile(self.input):
files = [self.input]
elif os.path.isdir(self.input):
files = [
os.path.join(dirpath, name)
for dirpath, _, names in os.walk(self.input)
for name in names if name.endswith('.java')
]
else:
raise Exception(' '.join(['File', self.input, 'does not exist']))
data = []
for f in files:
ast = AST.build_from_javalang(build_ast(f))
complexity = MvnFreeNPathMetric(ast).value()
data.append({'file': f, 'complexity': complexity})
return {'data': data, 'errors': []}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@aibolit/metrics/npath/main.py` around lines 67 - 73,
_`_value_without_maven()` should preserve the same input handling as `value()`
and not reject directory inputs._ Update the input validation in
`_value_without_maven` to accept both `os.path.isdir(self.input)` and
`os.path.isfile(self.input)`, matching the behavior in `NPathMetric.value`. Then
make sure the AST construction path can handle either case consistently before
calling `MvnFreeNPathMetric.value()`.

Comment on lines +110 to +122
return sum(method['complexity'] for method in self.report())

def report(self) -> list[NPathMethodReport]:
return [
{
'class_name': class_declaration.name,
'method_name': method.name,
'complexity': self._method_npath(method),
}
for class_ast in self.ast.subtrees(ASTNodeType.CLASS_DECLARATION)
for class_declaration in [class_ast.root()]
for method in class_declaration.methods
]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Locate the `methods` computed field definition used by ASTNode for CLASS_DECLARATION
rg -n "'methods'" aibolit/ast_framework -C5
ast-grep run --pattern 'CLASS_DECLARATION: {$$$}' --lang python aibolit/ast_framework

Repository: cqfn/aibolit

Length of output: 2309


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the AST/computed-fields implementation and existing NPath tests.
git ls-files 'aibolit/ast_framework/**' 'aibolit/metrics/npath/**' 'tests/**' | sed -n '1,200p'

echo '--- standard_fields.py ---'
cat -n aibolit/ast_framework/computed_fields_catalog/standard_fields.py | sed -n '1,140p'

echo '--- AST subtree implementation ---'
rg -n "def subtrees|class AST|subtrees\(" aibolit/ast_framework -C 3

echo '--- NPath metric and tests ---'
rg -n "class TestMvnFreeNPathMetric|NPath|inner class|nested class|class .*class" aibolit/metrics/npath tests -C 3

Repository: cqfn/aibolit

Length of output: 10798


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- AST.subtrees implementation ---'
cat -n aibolit/ast_framework/ast.py | sed -n '60,130p'

echo '--- ASTNodeType and class declaration handling ---'
cat -n aibolit/ast_framework/ast_node_type.py | sed -n '1,120p'
cat -n aibolit/ast_framework/ast_node.py | sed -n '1,220p'

echo '--- NPathMetric / MvnFreeNPathMetric around report() ---'
cat -n aibolit/metrics/npath/main.py | sed -n '100,150p'

echo '--- tests mentioning MvnFreeNPathMetric or inner classes ---'
rg -n "MvnFreeNPathMetric|TestMvnFreeNPathMetric|inner class|nested class|class .*class" aibolit tests -C 3

Repository: cqfn/aibolit

Length of output: 20529


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- nodes_filter_factory ---'
cat -n aibolit/ast_framework/computed_fields_catalog/nodes_filter.py | sed -n '1,220p'

echo '--- any tests / fixtures for nested classes in the repo ---'
rg -n "nested class|inner class|CLASS_DECLARATION" aibolit -g '*test*' -g '*Test*' -C 2

echo '--- references to class_declaration.methods ---'
rg -n "\.methods\b" aibolit -C 2

Repository: cqfn/aibolit

Length of output: 1459


Nested class methods are skipped here. AST.subtrees(ASTNodeType.CLASS_DECLARATION) only returns the outer class subtree, and methods only filters direct METHOD_DECLARATION children from each class body. Any method declared inside an inner class is omitted from both report() and value(). Add a test for a nested class to lock the intended behavior in.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@aibolit/metrics/npath/main.py` around lines 110 - 122, The report generation
in report() and the aggregate in value() are skipping methods declared inside
nested classes because AST.subtrees(ASTNodeType.CLASS_DECLARATION) and
class_declaration.methods only cover direct class methods. Update the traversal
in NPathCalculator so nested class methods are either included intentionally or
explicitly excluded with the correct behavior, and add a regression test
covering an inner class to verify the expected result.

Comment on lines +21 to +27
excluded_metric_codes = set(patterns_config['metrics_exclude'])
ast_compatible_metrics = [
metric_config
for metric_config in patterns_config['metrics']
if metric_config['code'] not in excluded_metric_codes
]
for metric_config in ast_compatible_metrics:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect config.py context around metrics_exclude and the metric classes it lists
rg -n -B5 -A2 "metrics_exclude" aibolit/config.py

Repository: cqfn/aibolit

Length of output: 652


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== test/config/test_config.py ==\n'
cat -n test/config/test_config.py | sed -n '1,220p'

printf '\n== aibolit/config.py ==\n'
cat -n aibolit/config.py | sed -n '1,320p'

printf '\n== Search for metrics_exclude usage ==\n'
rg -n "metrics_exclude|ast: AST|AST-annotated|accepts an ast" -S aibolit test

Repository: cqfn/aibolit

Length of output: 26724


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Excluded metric implementations ==\n'
for f in \
  aibolit/metrics/entropy/entropy.py \
  aibolit/metrics/spaces/SpaceCounter.py \
  aibolit/metrics/lcom4/lcom4.py \
  aibolit/metrics/number_variables/numVariables.py \
  aibolit/metrics/NumberMethods/NumberMethods.py
do
  printf '\n-- %s --\n' "$f"
  cat -n "$f" | sed -n '1,260p'
done

Repository: cqfn/aibolit

Length of output: 6155


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== __main__.py relevant sections ==\n'
cat -n aibolit/__main__.py | sed -n '260,360p'

printf '\n== ml_pipeline.py relevant section ==\n'
cat -n aibolit/ml_pipeline/ml_pipeline.py | sed -n '60,120p'

printf '\n== model.py relevant section ==\n'
cat -n aibolit/model/model.py | sed -n '1,120p'

Repository: cqfn/aibolit

Length of output: 12816


Filter the test by AST compatibility, not metrics_exclude. metrics_exclude is used for pipeline/model feature selection and already includes M8 (NumberMethods), which does accept ast: AST. This test will skip valid metrics and can miss regressions; iterate over all metrics or keep a separate AST-incompatible list instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/config/test_config.py` around lines 21 - 27, The test setup is
incorrectly filtering metrics using metrics_exclude, which can hide valid
AST-compatible metrics like NumberMethods/M8. Update the logic in the
test_config setup to select metrics based on AST compatibility instead of
reusing metrics_exclude, and reference the existing ast_compatible_metrics loop
in test_config.py to either iterate over all metrics or maintain a separate list
of AST-incompatible metrics.

Comment on lines 16 to +24
def testIncorrectFormat():
with pytest.raises(Exception, match='PMDException'):
file = 'test/metrics/npath/ooo.java'
metric = NPathMetric(file)
metric.value(True)
file = 'test/metrics/npath/ooo.java'
metric = NPathMetric(file)
if shutil.which('mvn') is None:
with pytest.raises(Exception):
metric.value(True)
else:
with pytest.raises(Exception, match='PMDException'):
metric.value(True)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Blind Exception assertion flagged by static analysis (B017).

Since _value_without_maven() (main.py) also raises a plain Exception with a known message ('File ... does not exist'), the no-mvn branch could match on that message the same way the mvn branch matches 'PMDException', tightening the assertion.

🧪 Suggested tightening
     if shutil.which('mvn') is None:
-        with pytest.raises(Exception):
+        with pytest.raises(Exception, match='does not exist'):
             metric.value(True)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def testIncorrectFormat():
with pytest.raises(Exception, match='PMDException'):
file = 'test/metrics/npath/ooo.java'
metric = NPathMetric(file)
metric.value(True)
file = 'test/metrics/npath/ooo.java'
metric = NPathMetric(file)
if shutil.which('mvn') is None:
with pytest.raises(Exception):
metric.value(True)
else:
with pytest.raises(Exception, match='PMDException'):
metric.value(True)
def testIncorrectFormat():
file = 'test/metrics/npath/ooo.java'
metric = NPathMetric(file)
if shutil.which('mvn') is None:
with pytest.raises(Exception, match='does not exist'):
metric.value(True)
else:
with pytest.raises(Exception, match='PMDException'):
metric.value(True)
🧰 Tools
🪛 Ruff (0.15.20)

[warning] 20-20: Do not assert blind exception: Exception

(B017)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/metrics/npath/test_all_types.py` around lines 16 - 24, The no-mvn branch
in testIncorrectFormat uses a blanket Exception assertion, which is too loose.
Tighten the pytest.raises call to match the specific message raised by
NPathMetric.value/_value_without_maven, similar to how the mvn branch matches
PMDException. Use the known “File ... does not exist” message pattern so the
test verifies the intended failure path without accepting unrelated exceptions.

Source: Linters/SAST tools

@yegor256

Copy link
Copy Markdown
Member

@Beeeeeeee-c conflicts here

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants