Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 64 additions & 2 deletions aibolit/patterns/loop_outsider/loop_outsider.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
# SPDX-FileCopyrightText: Copyright (c) 2019-2025 Aibolit
# SPDX-License-Identifier: MIT
from typing import List


from typing import List, Set
from aibolit.ast_framework import AST, ASTNodeType
from aibolit.types_decl import LineNumber
from aibolit.utils.ast_builder import build_ast


class LoopOutsider:
Expand All @@ -17,4 +21,62 @@ def value(self, filename) -> List[LineNumber]:
"""
Returns the line number of loop outsiders found in file.
"""
return []
res = []
ast = AST.build_from_javalang(build_ast(filename))
loop_types = [ASTNodeType.WHILE_STATEMENT, ASTNodeType.FOR_STATEMENT,
ASTNodeType.DO_STATEMENT]

Comment thread
coderabbitai[bot] marked this conversation as resolved.
for loop_type in loop_types:
for loop_statement in ast.get_proxy_nodes(loop_type):
subtree = ast.get_subtree(loop_statement)
var_changes = self._find_variable_changes(subtree)
loop_vars_declarations = (
self._find_loop_variable_declarations(subtree,
loop_statement,
loop_type))

# Check if affected variables are not declared in loop_vars
for node in var_changes:
if node.member not in loop_vars_declarations:
res.append(node.line)

Comment on lines +40 to +42

@coderabbitai coderabbitai Bot Jun 8, 2025

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Guard against nodes without .member and avoid duplicates

ASSIGNMENT.expression may be a MemberReference or a ReferenceType; not all have a .member attribute.
Additionally, appending directly to a list yields duplicates.

-                    if node.member not in loop_vars_declarations:
-                        res.append(node.line)
+                    var_name = getattr(node, "member", None)
+                    if var_name and var_name not in loop_vars_declarations:
+                        res.add(node.line)

Couple with res: set[int] = set() at the top and replace the final return sorted(res).

📝 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
if node.member not in loop_vars_declarations:
res.append(node.line)
var_name = getattr(node, "member", None)
if var_name and var_name not in loop_vars_declarations:
res.add(node.line)
🤖 Prompt for AI Agents
In aibolit/patterns/loop_outsider/loop_outsider.py around lines 69 to 71, the
code assumes all nodes have a .member attribute, which can cause errors if they
don't, and appends line numbers directly to a list causing duplicates. Fix this
by first checking if the node has a .member attribute before accessing it, and
change res to a set to avoid duplicates. At the top of the function, initialize
res as a set of integers, and before returning, convert res to a sorted list.

@ivanovmg ivanovmg Jun 9, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Instead of using getattr as suggested by the bot, I suppose would be better to explicitly check that node.node_type == ASTNodeType.MEMBER_REFERNCE, because the reflection adds another level of complexity.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@ivanovmg You're absolutely right! That's a much better approach. Explicitly checking the node type is more precise and safer than using getattr. Only MEMBER_REFERENCE nodes are guaranteed to have the .member attribute, so filtering by node type is the correct solution.

Here's the improved code implementing your suggestion while also addressing the duplicate issue:

def value(self, filename) -> List[LineNumber]:
    """
    Returns the line number of loop outsiders found in file.
    """
    res = set()
    with open(filename, 'r', encoding='utf-8') as f:
        content = f.read()
    javalang_parsed = parse.parse(content)
    ast = AST.build_from_javalang(javalang_parsed)
    loop_types = [ASTNodeType.WHILE_STATEMENT, ASTNodeType.FOR_STATEMENT,
                  ASTNodeType.DO_STATEMENT]

    for loop_type in loop_types:
        for loop_statement in ast.get_proxy_nodes(loop_type):
            subtree = ast.get_subtree(loop_statement)
            var_changes = self._find_variable_changes(subtree)
            loop_vars_declarations = (
                self._find_loop_variable_declarations(subtree,
                                                      loop_statement,
                                                      loop_type))

            # Check if affected variables are not declared in loop_vars
            for node in var_changes:
                if node.node_type == ASTNodeType.MEMBER_REFERENCE:
                    if node.member not in loop_vars_declarations:
                        res.add(node.line)

    return sorted(res)

Your approach is cleaner and more type-safe than the defensive getattr pattern. It also makes the intent clearer - we only want to process member reference nodes since those represent variable access patterns.

@ivanovmg ivanovmg Jun 9, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@SinbadTheSailor2005 can you comment on this? Is it a viable recommendation? If so, then maybe introduce this check as well: if node.node_type == ASTNodeType.MEMBER_REFERENCE.

Original comment from the bot: #816 (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.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

return sorted(res)

def _find_variable_changes(self, ast: AST) -> Set:
"""Find all variables that are modified in the code."""
var_changes = set()

# Find variables affected by increment/decrement operations
for node in ast.get_proxy_nodes(ASTNodeType.MEMBER_REFERENCE):
if self._variable_is_affected(node):
var_changes.add(node)

# Find variables affected by assignments
for node in ast.get_proxy_nodes(ASTNodeType.ASSIGNMENT):
var_changes.add(node.expressionl)
return var_changes
Comment on lines +55 to +57

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

Guard against assignments whose LHS isn’t a MemberReference

assignment.expressionl can be an ArraySelector, This, etc.
Dereferencing .member later will raise AttributeError.

-        for node in ast.get_proxy_nodes(ASTNodeType.ASSIGNMENT):
-            var_changes.add(node.expressionl)
+        for assign in ast.get_proxy_nodes(ASTNodeType.ASSIGNMENT):
+            lhs = assign.expressionl
+            if lhs.node_type == ASTNodeType.MEMBER_REFERENCE:
+                var_changes.add(lhs)
📝 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
for node in ast.get_proxy_nodes(ASTNodeType.ASSIGNMENT):
var_changes.add(node.expressionl)
return var_changes
for assign in ast.get_proxy_nodes(ASTNodeType.ASSIGNMENT):
lhs = assign.expressionl
if lhs.node_type == ASTNodeType.MEMBER_REFERENCE:
var_changes.add(lhs)
return var_changes
🤖 Prompt for AI Agents
In aibolit/patterns/loop_outsider/loop_outsider.py around lines 56 to 58, the
code adds node.expressionl to var_changes without checking its type, but
expressionl can be various types like ArraySelector or This, not just
MemberReference. To fix this, add a type check to ensure node.expressionl is an
instance of MemberReference before adding it to var_changes, preventing
AttributeError when accessing .member later.


def _find_loop_variable_declarations(self, ast: AST, loop_statement,
loop_type) -> Set:
"""Find all variable declarations within the loop scope."""
loop_vars_declarations = set()
subtree = ast.get_subtree(loop_statement)

# Find local variable declarations
Comment on lines +59 to +65

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

Do not call get_subtree on a node that is external to the passed AST

ast already represents the subtree for the current loop.
Calling ast.get_subtree(loop_statement) again will raise networkx.exception.NetworkXError because loop_statement belongs to the original graph.

-        subtree = ast.get_subtree(loop_statement)
+        subtree = ast  # `ast` is already the loop-local subtree
📝 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 _find_loop_variable_declarations(self, ast: AST, loop_statement,
loop_type) -> Set:
"""Find all variable declarations within the loop scope."""
loop_vars_declarations = set()
subtree = ast.get_subtree(loop_statement)
# Find local variable declarations
def _find_loop_variable_declarations(self, ast: AST, loop_statement,
loop_type) -> Set:
"""Find all variable declarations within the loop scope."""
loop_vars_declarations = set()
- subtree = ast.get_subtree(loop_statement)
+ subtree = ast # `ast` is already the loop-local subtree
# Find local variable declarations
🤖 Prompt for AI Agents
In aibolit/patterns/loop_outsider/loop_outsider.py around lines 60 to 66, remove
the call to ast.get_subtree(loop_statement) because ast already represents the
subtree for the current loop. Using get_subtree on loop_statement, which belongs
to the original AST, causes a NetworkXError. Instead, operate directly on ast
without extracting a subtree again.

for node in subtree.get_proxy_nodes(
ASTNodeType.LOCAL_VARIABLE_DECLARATION):
loop_vars_declarations.add(node.names[-1])

# For for-loops, also check variable declarations in the loop header
if loop_type == ASTNodeType.FOR_STATEMENT:
for node_for in subtree.get_proxy_nodes(
ASTNodeType.VARIABLE_DECLARATION):
loop_vars_declarations.add(node_for.names[-1])

Comment on lines +66 to +75

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Collect all declared names, not just the last one

LOCAL_VARIABLE_DECLARATION.names is a list of VariableDeclarator; grabbing [-1]
misses earlier declarators in a multi-declaration statement (int a, b;) and
returns the node object rather than the string identifier.

-            loop_vars_declarations.add(node.names[-1])
+            for declarator in node.names:
+                loop_vars_declarations.add(declarator)

Apply the same pattern to the VARIABLE_DECLARATION branch.

📝 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
for node in subtree.get_proxy_nodes(
ASTNodeType.LOCAL_VARIABLE_DECLARATION):
loop_vars_declarations.add(node.names[-1])
# For for-loops, also check variable declarations in the loop header
if loop_type == ASTNodeType.FOR_STATEMENT:
for node_for in subtree.get_proxy_nodes(
ASTNodeType.VARIABLE_DECLARATION):
loop_vars_declarations.add(node_for.names[-1])
for node in subtree.get_proxy_nodes(
ASTNodeType.LOCAL_VARIABLE_DECLARATION):
for declarator in node.names:
loop_vars_declarations.add(declarator)
# For for-loops, also check variable declarations in the loop header
if loop_type == ASTNodeType.FOR_STATEMENT:
for node_for in subtree.get_proxy_nodes(
ASTNodeType.VARIABLE_DECLARATION):
for declarator in node_for.names:
loop_vars_declarations.add(declarator)
🤖 Prompt for AI Agents
In aibolit/patterns/loop_outsider/loop_outsider.py lines 67 to 76, the code only
adds the last declared variable name from the names list, which misses earlier
declared variables and adds node objects instead of string identifiers. Update
both the LOCAL_VARIABLE_DECLARATION and VARIABLE_DECLARATION branches to iterate
over all items in the names list and add each variable's string identifier to
loop_vars_declarations.

return loop_vars_declarations

def _variable_is_affected(self, node):
return ('--' in node.prefix_operators or '--' in
node.postfix_operators or
'++' in node.prefix_operators or '++' in
node.postfix_operators)
2 changes: 1 addition & 1 deletion test/patterns/loop_outsider/LoopOutsiderAddAndInWhile.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ public void loopOutsiderAddAndInWhile() {
x += 1; // here
}
}
}
}
12 changes: 12 additions & 0 deletions test/patterns/loop_outsider/NoLoopOutsider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// SPDX-FileCopyrightText: Copyright (c) 2019-2025 Aibolit
// SPDX-License-Identifier: MIT

public class NoLoopOutsider {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thank you for these new tests!

@SinbadTheSailor2005 SinbadTheSailor2005 Jun 9, 2025

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

@ivanovmg , look.
The check

 if node.node_type == ASTNodeType.MEMBER_REFERENCE 

is unneccesary, since var_changes list constains objects of ASTNodeType.MEMBER_REFERENCE.

Look the code snippets, where the algorithm fills the var_changes list

        # Find variables affected by assignments
        for node in ast.get_proxy_nodes(ASTNodeType.ASSIGNMENT):
            var_changes.add(node.expressionl)
 for node in ast.get_proxy_nodes(ASTNodeType.MEMBER_REFERENCE):
            if self._variable_is_affected(node):
                var_changes.add(node) # addding MEMBER_REFERENCE
    # Find variables affected by assignments
    for node in ast.get_proxy_nodes(ASTNodeType.ASSIGNMENT):
        var_changes.add(node.expressionl) # expressionl is  ASTNodeType.MEMBER_REFERENCE too!

To conclue, all objects in var_changes are gurantee to have .member propertie

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ok then. Thank you!

public void noLoopOutsider() {
int x = 0;
Integer y = 0;
while (true) {

}
}
}
11 changes: 11 additions & 0 deletions test/patterns/loop_outsider/NoLoopOutsiderFakeIncrementing.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// SPDX-FileCopyrightText: Copyright (c) 2019-2025 Aibolit
// SPDX-License-Identifier: MIT

public class NoLoopOutsiderFakeIncrementing{
public void noLoopOutsiderFakeIncrementing() {
int x = 0;
while (true) {
}
x++;
}
}
42 changes: 25 additions & 17 deletions test/patterns/loop_outsider/test_loop_outsider.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,11 @@
# SPDX-License-Identifier: MIT

import os
import unittest
from pathlib import Path
from unittest import TestCase
from aibolit.patterns.loop_outsider.loop_outsider import LoopOutsider


@unittest.skip("Not implemented")
class LoopOutsiderTestCase(TestCase):
"""
@todo #138:30min Continue to implement loop_outsider
Expand All @@ -21,104 +19,114 @@ class LoopOutsiderTestCase(TestCase):
def test_find_loop_outsider_assignment_in_while(self):
self.assertEqual(
LoopOutsider().value(Path(self.dir_path, "LoopOutsiderAssignmentInWhile.java")),
[5],
[8],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I noticed that the test suite does not have a single test for a good piece of code, where the pattern is not matched.
I suggest that you add this test (here, or an extra puzzle).

"Could not find loop outsider assignment in while loop",
)

def test_find_loop_outsider_prefix_increment_in_while(self):
self.assertEqual(
LoopOutsider().value(Path(self.dir_path, "LoopOutsiderPrefixIncrementInWhile.java")),
[5],
[8],
"Could not find loop outsider with prefix increment in while loop",
)

def test_find_loop_outsider_postfix_increment_in_while(self):
self.assertEqual(
LoopOutsider().value(Path(self.dir_path, "LoopOutsiderPostfixIncrementInWhile.java")),
[5],
[8],
"Could not find loop outsider with postfix increment in while loop",
)

def test_find_loop_outsider_prefix_decrement_in_while(self):
self.assertEqual(
LoopOutsider().value(Path(self.dir_path, "LoopOutsiderPrefixDecrementInWhile.java")),
[5],
[8],
"Could not find loop outsider with prefix decrement in while loop",
)

def test_find_loop_outsider_postfix_decrement_in_while(self):
self.assertEqual(
LoopOutsider().value(Path(self.dir_path, "LoopOutsiderPostfixDecrementInWhile.java")),
[5],
[8],
"Could not find loop outsider with postfix decrement in while loop",
)

def test_find_loop_outsider_add_and_in_while(self):
self.assertEqual(
LoopOutsider().value(Path(self.dir_path, "LoopOutsiderAddAndInWhile.java")),
[5],
[8],
"Could not find loop outsider with add AND in while loop",
)

def test_find_loop_outsider_subtract_and_in_while(self):
self.assertEqual(
LoopOutsider().value(Path(self.dir_path, "LoopOutsiderSubtractAndInWhile.java")),
[5],
[8],
"Could not find loop outsider with subtract AND in while loop",
)

def test_find_loop_outsider_multiply_and_in_while(self):
self.assertEqual(
LoopOutsider().value(Path(self.dir_path, "LoopOutsiderMultiplyAndInWhile.java")),
[5],
[8],
"Could not find loop outsider with multiply AND in while loop",
)

def test_find_loop_outsider_divide_and_in_while(self):
self.assertEqual(
LoopOutsider().value(Path(self.dir_path, "LoopOutsiderDivideAndInWhile.java")),
[5],
[8],
"Could not find loop outsider with divide AND in while loop",
)

def test_find_loop_outsider_modulus_and_in_while(self):
self.assertEqual(
LoopOutsider().value(Path(self.dir_path, "LoopOutsiderModulusAndInWhile.java")),
[5],
[8],
"Could not find loop outsider with modulus AND in while loop",
)

def test_find_loop_outsider_left_shift_and_in_while(self):
self.assertEqual(
LoopOutsider().value(Path(self.dir_path, "LoopOutsiderLeftShiftAndInWhile.java")),
[5],
[8],
"Could not find loop outsider with left shift AND in while loop",
)

def test_find_loop_outsider_right_shift_and_in_while(self):
self.assertEqual(
LoopOutsider().value(Path(self.dir_path, "LoopOutsiderRightShiftAndInWhile.java")),
[5],
[8],
"Could not find loop outsider with right shift AND in while loop",
)

def test_find_loop_outsider_bitwise_and_in_while(self):
self.assertEqual(
LoopOutsider().value(Path(self.dir_path, "LoopOutsiderBitwiseAndInWhile.java")),
[5],
[8],
"Could not find loop outsider with bitwise AND in while loop",
)

def test_find_loop_outsider_bitwise_inclusive_or_in_while(self):
self.assertEqual(
LoopOutsider().value(Path(self.dir_path, "LoopOutsiderBitwiseInclusiveOrInWhile.java")),
[5],
[8],
"Could not find loop outsider with bitwise inclusive OR in while loop",
)

def test_find_loop_outsider_bitwise_exclusive_or_in_while(self):
self.assertEqual(
LoopOutsider().value(Path(self.dir_path, "LoopOutsiderBitwiseExclusiveOrInWhile.java")),
[5],
[8],
"Could not find loop outsider with bitwise exclusive OR in while loop",
)

def test_should_not_detect_loop_outsider_when_variable_never_modified_outside_loop(self):
self.assertEqual(LoopOutsider().value(Path(self.dir_path,
"NoLoopOutsider.java")), [],
"Found unexisted loop outsider")

def test_should_not_detect_loop_outsider_when_incrementing_outside_loop(self):
self.assertEqual(LoopOutsider().value(Path(self.dir_path,
"NoLoopOutsiderFakeIncrementing.java")), [],
"Found unexisted loop outsider")
Loading