From f107fdcc71cce241d9f8ca35903fffbc2d8adc70 Mon Sep 17 00:00:00 2001 From: Yevhenii Kachanov Date: Mon, 30 Jun 2025 15:14:20 +0200 Subject: [PATCH 01/16] Implement bidirect index pattern tests is working add docs for code --- .../patterns/bidirect_index/bidirect_index.py | 163 ++++++++++++++++-- .../bidirect_index/test_bidirect_index.py | 2 - 2 files changed, 152 insertions(+), 13 deletions(-) diff --git a/aibolit/patterns/bidirect_index/bidirect_index.py b/aibolit/patterns/bidirect_index/bidirect_index.py index 05eb63c29..1cbe7a286 100644 --- a/aibolit/patterns/bidirect_index/bidirect_index.py +++ b/aibolit/patterns/bidirect_index/bidirect_index.py @@ -1,25 +1,166 @@ # SPDX-FileCopyrightText: Copyright (c) 2019-2025 Aibolit # SPDX-License-Identifier: MIT import os +import re class BidirectIndex: + """ + This class analyzes Java source code to find line numbers where a variable + is used as a bidirectional index. + A bidirectional index is defined as a variable that: + - is assigned a value (with or without type declaration) within a method or block, + - is incremented and decremented somewhere in its scope, + - increments/decrements inside a for-loop with a local variable of the same name + are ignored (to avoid "fake" cases). + + The typical use-case: detect patterns like `i = 0; ... ++i; ... --i;` + in Java code, while ignoring manipulations of `i` inside + loops where `i` is a local loop variable (e.g., `for (int i = 0; ...) { ... }`). + + Usage: + idx = BidirectIndex() + result = idx.value("MyClass.java") + # result is a list of line numbers matching the described pattern + """ def __init__(self): pass - def value(self, filename: str | os.PathLike): + @staticmethod + def value(filename: str | os.PathLike): """ - Finds if a variable is being incremented and decremented within the same method + Analyze the given Java file and return a sorted list of line numbers where a variable + is used as a bidirectional index as per the definition above. - :param filename: filename to be analyzed - :return: list of LineNumber with the variable declaration lines + Args: + filename: Path to the Java source file. - @todo #139:30min Implement bidirect index pattern - If the same numeric variable is incremented and decremented within the same method, - it's a pattern. A numeric variable should either be always growing or decreasing. - Bi-directional index is confusing. The method must return a list with the line numbers - of the variables that match this pattern. After implementation, activate tests in - test_bidirect_index.py + Returns: + List[int]: Sorted list of line numbers where bidirectional indices are found. """ - return [] + with open(filename, encoding="utf-8") as f: + lines = f.readlines() + + result = [] + + def methods(): + """ + Find the start and end line indices (0-based) for each method in the file. + + Returns: + List[Tuple[int, int]]: Each tuple is (start_line_idx, end_line_idx) for a method. + """ + res = [] + brace = 0 + mstart = None + for idx, line in enumerate(lines): + # Detect method start by pattern: 'void methodName(...) {' + if re.search(r"\bvoid\b\s+\w+\s*\(.*\)\s*{", line): + if mstart is None: + mstart = idx + brace = 0 + brace += line.count("{") + brace -= line.count("}") + if mstart is not None and brace == 0: + res.append((mstart, idx)) + mstart = None + return res + + def analyze_block(start, end): + """ + Recursively analyze a block of code between line indices `start` and `end` (inclusive) + for bidirectional index variables. + + Args: + start (int): Index of the first line of the block. + end (int): Index of the last line of the block. + """ + i = start + while i <= end: + line = lines[i] + # 1. Variable declaration with type (e.g., 'int i = 0;') + typed_decl = re.match(r'\s*(int|long|byte|short)\s+(\w+)\s*=', line) + # 2. Variable assignment without type (e.g., 'i = 0;') + # if not already matched as typed + untyped_decl = re.match(r'^\s*(\w+)\s*=\s*[^;]+;', line) \ + if not typed_decl else None + var = None + if typed_decl: + var = typed_decl.group(2) + elif untyped_decl: + var = untyped_decl.group(1) + if var: + # Determine the lifetime of the variable (until next redeclaration or block end) + j = i + 1 + while j <= end: + l = lines[j] + # Variable shadowing: stop at a new declaration with the same name + if re.match(r'\s*(int|long|byte|short)\s+' + re.escape(var) + r'\s*=', l): + break + j += 1 + + # Identify ranges of for-loops that have a local variable with the same name + for_blocks = [] + k = i + 1 + while k < j: + l = lines[k] + for_decl = re.match(r'\s*for\s*\(\s*int\s+' + re.escape(var) + r'\s*=', l) + if for_decl: + # Find the boundaries of the for-loop block + brace = l.count('{') - l.count('}') + bstart = k + k += 1 + while k < j and brace > 0: + brace += lines[k].count('{') - lines[k].count('}') + k += 1 + for_blocks.append((bstart, k - 1)) + continue + k += 1 + + # Count increments/decrements outside any for-block with local var + inc_outside = 0 + dec_outside = 0 + k = i + 1 + while k < j: + # Skip lines inside for-blocks with local var + in_for = any(bstart <= k <= bend for (bstart, bend) in for_blocks) + if not in_for: + l = lines[k] + # Increment patterns: ++var, var++, var += 1, var = var + 1 + if re.search( + r'(\+\+' + re.escape(var) + r'|' + re.escape(var) + r'\+\+|' + + re.escape(var) + r'\s*\+=\s*1\b|' + + re.escape(var) + r'\s*=\s*' + re.escape(var) + r'\s*\+\s*1\b)', + l): + inc_outside += 1 + # Decrement patterns: --var, var--, var -= 1, var = var - 1 + if re.search(r'(--' + re.escape(var) + r'|' + re.escape(var) + r'--|' + + re.escape(var) + r'\s*-=\s*1\b|' + + re.escape(var) + r'\s*=\s*' + re.escape( + var) + r'\s*-\s*1\b)', l): + dec_outside += 1 + k += 1 + + # If both increment and decrement are found, + # record the line number of declaration/assignment (1-based) + if inc_outside and dec_outside: + result.append(i + 1) + # Recursively analyze inner code blocks + if '{' in line: + brace, block_start = 1, i + i += 1 + while i <= end: + brace += lines[i].count("{") + brace -= lines[i].count("}") + if brace == 0: + analyze_block(block_start + 1, i - 1) + break + i += 1 + else: + i += 1 + + # For each method, analyze its body + for m_start, m_end in methods(): + analyze_block(m_start + 1, m_end) + return sorted(result) diff --git a/test/patterns/bidirect_index/test_bidirect_index.py b/test/patterns/bidirect_index/test_bidirect_index.py index b9ed7c933..c39794d85 100644 --- a/test/patterns/bidirect_index/test_bidirect_index.py +++ b/test/patterns/bidirect_index/test_bidirect_index.py @@ -4,11 +4,9 @@ import os.path from pathlib import Path from unittest import TestCase -import unittest from aibolit.patterns.bidirect_index.bidirect_index import BidirectIndex -@unittest.skip('Not implemented') class BidirectIndexTestCase(TestCase): dir_path = Path(os.path.realpath(__file__)).parent From 519d2ddaff47537999270a599887fb9e875c3ac4 Mon Sep 17 00:00:00 2001 From: Yevhenii Kachanov Date: Mon, 30 Jun 2025 15:23:48 +0200 Subject: [PATCH 02/16] rework class for reworking too-many-statements issue --- .../patterns/bidirect_index/bidirect_index.py | 221 +++++++++--------- 1 file changed, 104 insertions(+), 117 deletions(-) diff --git a/aibolit/patterns/bidirect_index/bidirect_index.py b/aibolit/patterns/bidirect_index/bidirect_index.py index 1cbe7a286..52044844b 100644 --- a/aibolit/patterns/bidirect_index/bidirect_index.py +++ b/aibolit/patterns/bidirect_index/bidirect_index.py @@ -43,124 +43,111 @@ def value(filename: str | os.PathLike): lines = f.readlines() result = [] + for m_start, m_end in BidirectIndex.find_methods(lines): + BidirectIndex.analyze_block(lines, m_start + 1, m_end, result) + return sorted(result) - def methods(): - """ - Find the start and end line indices (0-based) for each method in the file. - - Returns: - List[Tuple[int, int]]: Each tuple is (start_line_idx, end_line_idx) for a method. - """ - res = [] - brace = 0 - mstart = None - for idx, line in enumerate(lines): - # Detect method start by pattern: 'void methodName(...) {' - if re.search(r"\bvoid\b\s+\w+\s*\(.*\)\s*{", line): - if mstart is None: - mstart = idx - brace = 0 - brace += line.count("{") - brace -= line.count("}") - if mstart is not None and brace == 0: - res.append((mstart, idx)) - mstart = None - return res - - def analyze_block(start, end): - """ - Recursively analyze a block of code between line indices `start` and `end` (inclusive) - for bidirectional index variables. - - Args: - start (int): Index of the first line of the block. - end (int): Index of the last line of the block. - """ - i = start - while i <= end: - line = lines[i] - # 1. Variable declaration with type (e.g., 'int i = 0;') - typed_decl = re.match(r'\s*(int|long|byte|short)\s+(\w+)\s*=', line) - # 2. Variable assignment without type (e.g., 'i = 0;') - # if not already matched as typed - untyped_decl = re.match(r'^\s*(\w+)\s*=\s*[^;]+;', line) \ - if not typed_decl else None - var = None - if typed_decl: - var = typed_decl.group(2) - elif untyped_decl: - var = untyped_decl.group(1) - if var: - # Determine the lifetime of the variable (until next redeclaration or block end) - j = i + 1 - while j <= end: - l = lines[j] - # Variable shadowing: stop at a new declaration with the same name - if re.match(r'\s*(int|long|byte|short)\s+' + re.escape(var) + r'\s*=', l): - break - j += 1 - - # Identify ranges of for-loops that have a local variable with the same name - for_blocks = [] - k = i + 1 - while k < j: - l = lines[k] - for_decl = re.match(r'\s*for\s*\(\s*int\s+' + re.escape(var) + r'\s*=', l) - if for_decl: - # Find the boundaries of the for-loop block - brace = l.count('{') - l.count('}') - bstart = k - k += 1 - while k < j and brace > 0: - brace += lines[k].count('{') - lines[k].count('}') - k += 1 - for_blocks.append((bstart, k - 1)) - continue - k += 1 - - # Count increments/decrements outside any for-block with local var - inc_outside = 0 - dec_outside = 0 - k = i + 1 - while k < j: - # Skip lines inside for-blocks with local var - in_for = any(bstart <= k <= bend for (bstart, bend) in for_blocks) - if not in_for: - l = lines[k] - # Increment patterns: ++var, var++, var += 1, var = var + 1 - if re.search( - r'(\+\+' + re.escape(var) + r'|' + re.escape(var) + r'\+\+|' + - re.escape(var) + r'\s*\+=\s*1\b|' + - re.escape(var) + r'\s*=\s*' + re.escape(var) + r'\s*\+\s*1\b)', - l): - inc_outside += 1 - # Decrement patterns: --var, var--, var -= 1, var = var - 1 - if re.search(r'(--' + re.escape(var) + r'|' + re.escape(var) + r'--|' + - re.escape(var) + r'\s*-=\s*1\b|' + - re.escape(var) + r'\s*=\s*' + re.escape( - var) + r'\s*-\s*1\b)', l): - dec_outside += 1 - k += 1 + @staticmethod + def find_methods(lines): + """ + Find the start and end line indices (0-based) for each Java method in the file. + """ + res = [] + brace = 0 + mstart = None + for idx, line in enumerate(lines): + if re.search(r"\bvoid\b\s+\w+\s*\(.*\)\s*{", line): + if mstart is None: + mstart = idx + brace = 0 + brace += line.count("{") + brace -= line.count("}") + if mstart is not None and brace == 0: + res.append((mstart, idx)) + mstart = None + return res - # If both increment and decrement are found, - # record the line number of declaration/assignment (1-based) - if inc_outside and dec_outside: - result.append(i + 1) - # Recursively analyze inner code blocks - if '{' in line: - brace, block_start = 1, i - i += 1 - while i <= end: - brace += lines[i].count("{") - brace -= lines[i].count("}") - if brace == 0: - analyze_block(block_start + 1, i - 1) - break - i += 1 - else: + @staticmethod + def analyze_block(lines, start, end, result): + """ + Recursively analyze a block of code between 'start' and 'end' (inclusive). + """ + i = start + while i <= end: + line = lines[i] + typed_decl = re.match(r'\s*(int|long|byte|short)\s+(\w+)\s*=', line) + untyped_decl = re.match(r'^\s*(\w+)\s*=\s*[^;]+;', line) \ + if not typed_decl else None + var = typed_decl.group(2) if typed_decl else (untyped_decl.group(1) + if untyped_decl else None) + if var: + j = i + 1 + while j <= end: + l = lines[j] + if re.match(r'\s*(int|long|byte|short)\s+' + re.escape(var) + r'\s*=', l): + break + j += 1 + for_blocks = BidirectIndex.find_for_blocks(lines, var, i + 1, j) + inc_outside, dec_outside = ( + BidirectIndex.count_inc_dec(lines, var, for_blocks, i + 1, j)) + if inc_outside and dec_outside: + result.append(i + 1) + if '{' in line: + brace, block_start = 1, i + i += 1 + while i <= end: + brace += lines[i].count("{") + brace -= lines[i].count("}") + if brace == 0: + BidirectIndex.analyze_block(lines, block_start + 1, i - 1, result) + break i += 1 + else: + i += 1 - # For each method, analyze its body - for m_start, m_end in methods(): - analyze_block(m_start + 1, m_end) - return sorted(result) + @staticmethod + def find_for_blocks(lines, var, start, end): + """ + Find all ranges of for-loops that declare a local variable with the given name. + """ + for_blocks = [] + k = start + while k < end: + l = lines[k] + for_decl = re.match(r'\s*for\s*\(\s*int\s+' + re.escape(var) + r'\s*=', l) + if for_decl: + brace = l.count('{') - l.count('}') + bstart = k + k += 1 + while k < end and brace > 0: + brace += lines[k].count('{') - lines[k].count('}') + k += 1 + for_blocks.append((bstart, k - 1)) + continue + k += 1 + return for_blocks + + @staticmethod + def count_inc_dec(lines, var, for_blocks, start, end): + """ + Count increments and decrements of variable `var` outside any for-blocks. + """ + inc_outside = 0 + dec_outside = 0 + k = start + while k < end: + in_for = any(bstart <= k <= bend for (bstart, bend) in for_blocks) + if not in_for: + l = lines[k] + if re.search( + r'(\+\+' + re.escape(var) + r'|' + re.escape(var) + r'\+\+|' + + re.escape(var) + r'\s*\+=\s*1\b|' + + re.escape(var) + r'\s*=\s*' + re.escape(var) + r'\s*\+\s*1\b)', l): + inc_outside += 1 + if re.search( + r'(--' + re.escape(var) + r'|' + re.escape(var) + r'--|' + + re.escape(var) + r'\s*-=\s*1\b|' + + re.escape(var) + r'\s*=\s*' + re.escape(var) + r'\s*-\s*1\b)', l): + dec_outside += 1 + k += 1 + return inc_outside, dec_outside From 2fac967db94c847e4ac057591ea398dd617addae Mon Sep 17 00:00:00 2001 From: Yevhenii Kachanov Date: Mon, 30 Jun 2025 15:35:42 +0200 Subject: [PATCH 03/16] resolve issues from flake8 ruff --- .../patterns/bidirect_index/bidirect_index.py | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/aibolit/patterns/bidirect_index/bidirect_index.py b/aibolit/patterns/bidirect_index/bidirect_index.py index 52044844b..9a946d2cf 100644 --- a/aibolit/patterns/bidirect_index/bidirect_index.py +++ b/aibolit/patterns/bidirect_index/bidirect_index.py @@ -20,7 +20,7 @@ class BidirectIndex: Usage: idx = BidirectIndex() - result = idx.value("MyClass.java") + result = idx.value('MyClass.java') # result is a list of line numbers matching the described pattern """ @@ -39,7 +39,7 @@ def value(filename: str | os.PathLike): Returns: List[int]: Sorted list of line numbers where bidirectional indices are found. """ - with open(filename, encoding="utf-8") as f: + with open(filename, encoding='utf-8') as f: lines = f.readlines() result = [] @@ -56,12 +56,12 @@ def find_methods(lines): brace = 0 mstart = None for idx, line in enumerate(lines): - if re.search(r"\bvoid\b\s+\w+\s*\(.*\)\s*{", line): + if re.search(r'\bvoid\b\s+\w+\s*\(.*\)\s*{', line): if mstart is None: mstart = idx brace = 0 - brace += line.count("{") - brace -= line.count("}") + brace += line.count('{') + brace -= line.count('}') if mstart is not None and brace == 0: res.append((mstart, idx)) mstart = None @@ -83,8 +83,8 @@ def analyze_block(lines, start, end, result): if var: j = i + 1 while j <= end: - l = lines[j] - if re.match(r'\s*(int|long|byte|short)\s+' + re.escape(var) + r'\s*=', l): + line_ = lines[j] + if re.match(r'\s*(int|long|byte|short)\s+' + re.escape(var) + r'\s*=', line_): break j += 1 for_blocks = BidirectIndex.find_for_blocks(lines, var, i + 1, j) @@ -96,8 +96,8 @@ def analyze_block(lines, start, end, result): brace, block_start = 1, i i += 1 while i <= end: - brace += lines[i].count("{") - brace -= lines[i].count("}") + brace += lines[i].count('{') + brace -= lines[i].count('}') if brace == 0: BidirectIndex.analyze_block(lines, block_start + 1, i - 1, result) break @@ -113,10 +113,10 @@ def find_for_blocks(lines, var, start, end): for_blocks = [] k = start while k < end: - l = lines[k] - for_decl = re.match(r'\s*for\s*\(\s*int\s+' + re.escape(var) + r'\s*=', l) + line_ = lines[k] + for_decl = re.match(r'\s*for\s*\(\s*int\s+' + re.escape(var) + r'\s*=', line_) if for_decl: - brace = l.count('{') - l.count('}') + brace = line_.count('{') - line_.count('}') bstart = k k += 1 while k < end and brace > 0: @@ -138,16 +138,16 @@ def count_inc_dec(lines, var, for_blocks, start, end): while k < end: in_for = any(bstart <= k <= bend for (bstart, bend) in for_blocks) if not in_for: - l = lines[k] + line_ = lines[k] if re.search( r'(\+\+' + re.escape(var) + r'|' + re.escape(var) + r'\+\+|' + re.escape(var) + r'\s*\+=\s*1\b|' + - re.escape(var) + r'\s*=\s*' + re.escape(var) + r'\s*\+\s*1\b)', l): + re.escape(var) + r'\s*=\s*' + re.escape(var) + r'\s*\+\s*1\b)', line_): inc_outside += 1 if re.search( r'(--' + re.escape(var) + r'|' + re.escape(var) + r'--|' + re.escape(var) + r'\s*-=\s*1\b|' + - re.escape(var) + r'\s*=\s*' + re.escape(var) + r'\s*-\s*1\b)', l): + re.escape(var) + r'\s*=\s*' + re.escape(var) + r'\s*-\s*1\b)', line_): dec_outside += 1 k += 1 return inc_outside, dec_outside From 9f93a1ef05b3db90f7a36fc850d42b5cebfe366f Mon Sep 17 00:00:00 2001 From: Yevhenii Kachanov Date: Mon, 30 Jun 2025 16:01:31 +0200 Subject: [PATCH 04/16] add catching error for file not found case. update doc; update pattern for searching; --- .../patterns/bidirect_index/bidirect_index.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/aibolit/patterns/bidirect_index/bidirect_index.py b/aibolit/patterns/bidirect_index/bidirect_index.py index 9a946d2cf..057847b0b 100644 --- a/aibolit/patterns/bidirect_index/bidirect_index.py +++ b/aibolit/patterns/bidirect_index/bidirect_index.py @@ -39,10 +39,13 @@ def value(filename: str | os.PathLike): Returns: List[int]: Sorted list of line numbers where bidirectional indices are found. """ - with open(filename, encoding='utf-8') as f: - lines = f.readlines() + try: + with open(filename, encoding='utf-8') as f: + lines = f.readlines() + except (FileNotFoundError, IOError) as e: + raise ValueError(f'Failed to read file {filename}: {e}') - result = [] + result: list[int] = [] for m_start, m_end in BidirectIndex.find_methods(lines): BidirectIndex.analyze_block(lines, m_start + 1, m_end, result) return sorted(result) @@ -51,12 +54,20 @@ def value(filename: str | os.PathLike): def find_methods(lines): """ Find the start and end line indices (0-based) for each Java method in the file. + + Args: + lines (list[str]): The lines of the Java source file to analyze. + + Returns: + list[tuple[int, int]]: Each tuple is (start_line_idx, end_line_idx) for a method. """ res = [] brace = 0 mstart = None for idx, line in enumerate(lines): - if re.search(r'\bvoid\b\s+\w+\s*\(.*\)\s*{', line): + if (re.search( + r'(public|private|protected|static|\s)*([\w<>\[\]]+)\s+\w+\s*\([^)]*\)\s*{', + line)): if mstart is None: mstart = idx brace = 0 From 252cbbb55698abfc6399fe1385940a2b0bfbf52f Mon Sep 17 00:00:00 2001 From: Yevhenii Kachanov Date: Thu, 3 Jul 2025 15:34:30 +0200 Subject: [PATCH 05/16] indent "are ignored (to avoid "fake" cases)." --- aibolit/patterns/bidirect_index/bidirect_index.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aibolit/patterns/bidirect_index/bidirect_index.py b/aibolit/patterns/bidirect_index/bidirect_index.py index 057847b0b..0e243f685 100644 --- a/aibolit/patterns/bidirect_index/bidirect_index.py +++ b/aibolit/patterns/bidirect_index/bidirect_index.py @@ -12,7 +12,7 @@ class BidirectIndex: - is assigned a value (with or without type declaration) within a method or block, - is incremented and decremented somewhere in its scope, - increments/decrements inside a for-loop with a local variable of the same name - are ignored (to avoid "fake" cases). + are ignored (to avoid "fake" cases). The typical use-case: detect patterns like `i = 0; ... ++i; ... --i;` in Java code, while ignoring manipulations of `i` inside From cb2a04c38c659c312f5c17485f96583d39972922 Mon Sep 17 00:00:00 2001 From: Yevhenii Kachanov Date: Thu, 3 Jul 2025 18:25:52 +0200 Subject: [PATCH 06/16] rework to AST tests and pattern also add new rows the enum class --- aibolit/ast_framework/ast_node_type.py | 3 ++ .../patterns/bidirect_index/bidirect_index.py | 17 +++------ .../bidirect_index/test_bidirect_index.py | 38 +++++++++++-------- 3 files changed, 30 insertions(+), 28 deletions(-) diff --git a/aibolit/ast_framework/ast_node_type.py b/aibolit/ast_framework/ast_node_type.py index 36678c2bf..ed85f0b46 100644 --- a/aibolit/ast_framework/ast_node_type.py +++ b/aibolit/ast_framework/ast_node_type.py @@ -13,6 +13,7 @@ class ASTNodeType(Enum): ARRAY_SELECTOR = auto() ASSERT_STATEMENT = auto() ASSIGNMENT = auto() + AUGMENTED_ASSIGNMENT = auto() BASIC_TYPE = auto() BINARY_OPERATION = auto() BLOCK_STATEMENT = auto() @@ -44,6 +45,7 @@ class ASTNodeType(Enum): FOR_CONTROL = auto() FOR_STATEMENT = auto() FORMAL_PARAMETER = auto() + IDENTIFIER = auto() IF_STATEMENT = auto() IMPORT = auto() INFERRED_FORMAL_PARAMETER = auto() @@ -80,6 +82,7 @@ class ASTNodeType(Enum): TYPE_ARGUMENT = auto() TYPE_DECLARATION = auto() TYPE_PARAMETER = auto() + UNARY_OPERATION = auto() UNKNOWN = auto() # Custom type, used only in parsing javalang AST VARIABLE_DECLARATION = auto() VARIABLE_DECLARATOR = auto() diff --git a/aibolit/patterns/bidirect_index/bidirect_index.py b/aibolit/patterns/bidirect_index/bidirect_index.py index 0e243f685..0b404c6c4 100644 --- a/aibolit/patterns/bidirect_index/bidirect_index.py +++ b/aibolit/patterns/bidirect_index/bidirect_index.py @@ -1,6 +1,5 @@ # SPDX-FileCopyrightText: Copyright (c) 2019-2025 Aibolit # SPDX-License-Identifier: MIT -import os import re @@ -20,31 +19,25 @@ class BidirectIndex: Usage: idx = BidirectIndex() - result = idx.value('MyClass.java') + lines = open("MyClass.java", encoding="utf-8").readlines() + result = idx.value(lines) # result is a list of line numbers matching the described pattern """ def __init__(self): pass - @staticmethod - def value(filename: str | os.PathLike): + def value(self, lines): """ - Analyze the given Java file and return a sorted list of line numbers where a variable + Analyze the given Java file lines and return a sorted list of line numbers where a variable is used as a bidirectional index as per the definition above. Args: - filename: Path to the Java source file. + lines (list[str]): Lines of the Java source file. Returns: List[int]: Sorted list of line numbers where bidirectional indices are found. """ - try: - with open(filename, encoding='utf-8') as f: - lines = f.readlines() - except (FileNotFoundError, IOError) as e: - raise ValueError(f'Failed to read file {filename}: {e}') - result: list[int] = [] for m_start, m_end in BidirectIndex.find_methods(lines): BidirectIndex.analyze_block(lines, m_start + 1, m_end, result) diff --git a/test/patterns/bidirect_index/test_bidirect_index.py b/test/patterns/bidirect_index/test_bidirect_index.py index c39794d85..343491cff 100644 --- a/test/patterns/bidirect_index/test_bidirect_index.py +++ b/test/patterns/bidirect_index/test_bidirect_index.py @@ -1,67 +1,73 @@ -# SPDX-FileCopyrightText: Copyright (c) 2019-2025 Aibolit -# SPDX-License-Identifier: MIT - import os.path from pathlib import Path from unittest import TestCase + from aibolit.patterns.bidirect_index.bidirect_index import BidirectIndex class BidirectIndexTestCase(TestCase): dir_path = Path(os.path.realpath(__file__)).parent + def _lines_from_file(self, filename): + with open(filename, encoding='utf-8') as f: + return f.readlines() + def test_bidirect_index_increase_decrease(self): + lines = self._lines_from_file(Path(self.dir_path, 'BidirectIndexIncreaseDecrease.java')) self.assertEqual( - BidirectIndex().value(Path(self.dir_path, 'BidirectIndexIncreaseDecrease.java')), + BidirectIndex().value(lines), [6], 'Could not find bidirect index when index increased and then decreased', ) def test_bidirect_index_decrease_increase(self): + lines = self._lines_from_file(Path(self.dir_path, 'BidirectIndexDecreaseIncrease.java')) self.assertEqual( - BidirectIndex().value(Path(self.dir_path, 'BidirectIndexDecreaseIncrease.java')), + BidirectIndex().value(lines), [6], 'Could not find bidirect index when index decreased and then increased', ) def test_bidirect_index_increase_decrease_assignment(self): + lines = self._lines_from_file( + Path(self.dir_path, 'BidirectIndexIncreaseDecreaseAssignment.java')) self.assertEqual( - BidirectIndex().value( - Path(self.dir_path, 'BidirectIndexIncreaseDecreaseAssignment.java') - ), + BidirectIndex().value(lines), [6], 'Could not find bidirect index when index increased and then decreased with assignment', ) def test_bidirect_index_increase_assignment_decrease(self): + lines = self._lines_from_file( + Path(self.dir_path, 'BidirectIndexIncreaseAssignmentDecrease.java')) self.assertEqual( - BidirectIndex().value( - Path(self.dir_path, 'BidirectIndexIncreaseAssignmentDecrease.java') - ), + BidirectIndex().value(lines), [6], 'Could not find bidirect index when index increased with assignment and then decreased', ) def test_bidirect_index_increase_assignment_decrease_assignment(self): + lines = self._lines_from_file( + Path(self.dir_path, 'BidirectIndexIncreaseAssignmentDecreaseAssignment.java')) self.assertEqual( - BidirectIndex().value( - Path(self.dir_path, 'BidirectIndexIncreaseAssignmentDecreaseAssignment.java') - ), + BidirectIndex().value(lines), [6], 'Could not find bidirect index when index increased with assignment ' 'and then decreased with assignment', ) def test_bidirect_index_hidden_scope_true(self): + lines = self._lines_from_file(Path(self.dir_path, 'BidirectIndexHiddenScope.java')) self.assertEqual( - BidirectIndex().value(Path(self.dir_path, 'BidirectIndexHiddenScope.java')), + BidirectIndex().value(lines), [], 'Could not find bidirec index when scope is hidden', ) def test_bidirect_index_outsider(self): + lines = self._lines_from_file(Path(self.dir_path, 'BidirectIndexOutsider.java')) self.assertEqual( - BidirectIndex().value(Path(self.dir_path, 'BidirectIndexOutsider.java')), + BidirectIndex().value(lines), [13], 'Could not find bidirec index when index is ot of loop', ) From 73dbaa57498e33543727e9dc7d5af98a7524ffa1 Mon Sep 17 00:00:00 2001 From: Yevhenii Kachanov Date: Sun, 6 Jul 2025 12:14:27 +0200 Subject: [PATCH 07/16] add Copyright --- test/patterns/bidirect_index/test_bidirect_index.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/patterns/bidirect_index/test_bidirect_index.py b/test/patterns/bidirect_index/test_bidirect_index.py index 343491cff..ce723f5a7 100644 --- a/test/patterns/bidirect_index/test_bidirect_index.py +++ b/test/patterns/bidirect_index/test_bidirect_index.py @@ -1,3 +1,5 @@ +# SPDX-FileCopyrightText: Copyright (c) 2019-2025 Aibolit +# SPDX-License-Identifier: MIT import os.path from pathlib import Path from unittest import TestCase From 3be30f2d4c35d178bb68265f24606759b425680e Mon Sep 17 00:00:00 2001 From: Yevhenii Kachanov Date: Mon, 7 Jul 2025 11:31:35 +0200 Subject: [PATCH 08/16] rework all staticMethods change doc --- .../patterns/bidirect_index/bidirect_index.py | 230 +++++++++--------- 1 file changed, 115 insertions(+), 115 deletions(-) diff --git a/aibolit/patterns/bidirect_index/bidirect_index.py b/aibolit/patterns/bidirect_index/bidirect_index.py index 0b404c6c4..2283b7d3b 100644 --- a/aibolit/patterns/bidirect_index/bidirect_index.py +++ b/aibolit/patterns/bidirect_index/bidirect_index.py @@ -3,6 +3,119 @@ import re +def find_methods(lines): + """ + Find the start and end line indices (0-based) for each Java method in the file. + + Args: + lines (list[str]): The lines of the Java source file to analyze. + + Returns: + list[tuple[int, int]]: Each tuple is (start_line_idx, end_line_idx) for a method. + """ + res = [] + brace = 0 + mstart = None + for idx, line in enumerate(lines): + if (re.search( + r'(public|private|protected|static|\s)*([\w<>\[\]]+)\s+\w+\s*\([^)]*\)\s*{', + line)): + if mstart is None: + mstart = idx + brace = 0 + brace += line.count('{') + brace -= line.count('}') + if mstart is not None and brace == 0: + res.append((mstart, idx)) + mstart = None + return res + + +def analyze_block(lines, start, end, result): + """ + Recursively analyze a block of code between 'start' and 'end' (inclusive). + """ + i = start + while i <= end: + line = lines[i] + typed_decl = re.match(r'\s*(int|long|byte|short)\s+(\w+)\s*=', line) + untyped_decl = re.match(r'^\s*(\w+)\s*=\s*[^;]+;', line) \ + if not typed_decl else None + var = typed_decl.group(2) if typed_decl else (untyped_decl.group(1) + if untyped_decl else None) + if var: + j = i + 1 + while j <= end: + line_ = lines[j] + if re.match(r'\s*(int|long|byte|short)\s+' + re.escape(var) + r'\s*=', line_): + break + j += 1 + for_blocks = find_for_blocks(lines, var, i + 1, j) + inc_outside, dec_outside = ( + count_inc_dec(lines, var, for_blocks, i + 1, j)) + if inc_outside and dec_outside: + result.append(i + 1) + if '{' in line: + brace, block_start = 1, i + i += 1 + while i <= end: + brace += lines[i].count('{') + brace -= lines[i].count('}') + if brace == 0: + analyze_block(lines, block_start + 1, i - 1, result) + break + i += 1 + else: + i += 1 + + +def find_for_blocks(lines, var, start, end): + """ + Find all ranges of for-loops that declare a local variable with the given name. + """ + for_blocks = [] + k = start + while k < end: + line_ = lines[k] + for_decl = re.match(r'\s*for\s*\(\s*int\s+' + re.escape(var) + r'\s*=', line_) + if for_decl: + brace = line_.count('{') - line_.count('}') + bstart = k + k += 1 + while k < end and brace > 0: + brace += lines[k].count('{') - lines[k].count('}') + k += 1 + for_blocks.append((bstart, k - 1)) + continue + k += 1 + return for_blocks + + +def count_inc_dec(lines, var, for_blocks, start, end): + """ + Count increments and decrements of variable `var` outside any for-blocks. + """ + inc_outside = 0 + dec_outside = 0 + k = start + while k < end: + in_for = any(bstart <= k <= bend for (bstart, bend) in for_blocks) + if not in_for: + line_ = lines[k] + if re.search( + r'(\+\+' + re.escape(var) + r'|' + re.escape(var) + r'\+\+|' + + re.escape(var) + r'\s*\+=\s*1\b|' + + re.escape(var) + r'\s*=\s*' + re.escape(var) + r'\s*\+\s*1\b)', line_): + inc_outside += 1 + if re.search( + r'(--' + re.escape(var) + r'|' + re.escape(var) + r'--|' + + re.escape(var) + r'\s*-=\s*1\b|' + + re.escape(var) + r'\s*=\s*' + re.escape(var) + r'\s*-\s*1\b)', line_): + dec_outside += 1 + k += 1 + return inc_outside, dec_outside + + class BidirectIndex: """ This class analyzes Java source code to find line numbers where a variable @@ -39,119 +152,6 @@ def value(self, lines): List[int]: Sorted list of line numbers where bidirectional indices are found. """ result: list[int] = [] - for m_start, m_end in BidirectIndex.find_methods(lines): - BidirectIndex.analyze_block(lines, m_start + 1, m_end, result) + for m_start, m_end in find_methods(lines): + analyze_block(lines, m_start + 1, m_end, result) return sorted(result) - - @staticmethod - def find_methods(lines): - """ - Find the start and end line indices (0-based) for each Java method in the file. - - Args: - lines (list[str]): The lines of the Java source file to analyze. - - Returns: - list[tuple[int, int]]: Each tuple is (start_line_idx, end_line_idx) for a method. - """ - res = [] - brace = 0 - mstart = None - for idx, line in enumerate(lines): - if (re.search( - r'(public|private|protected|static|\s)*([\w<>\[\]]+)\s+\w+\s*\([^)]*\)\s*{', - line)): - if mstart is None: - mstart = idx - brace = 0 - brace += line.count('{') - brace -= line.count('}') - if mstart is not None and brace == 0: - res.append((mstart, idx)) - mstart = None - return res - - @staticmethod - def analyze_block(lines, start, end, result): - """ - Recursively analyze a block of code between 'start' and 'end' (inclusive). - """ - i = start - while i <= end: - line = lines[i] - typed_decl = re.match(r'\s*(int|long|byte|short)\s+(\w+)\s*=', line) - untyped_decl = re.match(r'^\s*(\w+)\s*=\s*[^;]+;', line) \ - if not typed_decl else None - var = typed_decl.group(2) if typed_decl else (untyped_decl.group(1) - if untyped_decl else None) - if var: - j = i + 1 - while j <= end: - line_ = lines[j] - if re.match(r'\s*(int|long|byte|short)\s+' + re.escape(var) + r'\s*=', line_): - break - j += 1 - for_blocks = BidirectIndex.find_for_blocks(lines, var, i + 1, j) - inc_outside, dec_outside = ( - BidirectIndex.count_inc_dec(lines, var, for_blocks, i + 1, j)) - if inc_outside and dec_outside: - result.append(i + 1) - if '{' in line: - brace, block_start = 1, i - i += 1 - while i <= end: - brace += lines[i].count('{') - brace -= lines[i].count('}') - if brace == 0: - BidirectIndex.analyze_block(lines, block_start + 1, i - 1, result) - break - i += 1 - else: - i += 1 - - @staticmethod - def find_for_blocks(lines, var, start, end): - """ - Find all ranges of for-loops that declare a local variable with the given name. - """ - for_blocks = [] - k = start - while k < end: - line_ = lines[k] - for_decl = re.match(r'\s*for\s*\(\s*int\s+' + re.escape(var) + r'\s*=', line_) - if for_decl: - brace = line_.count('{') - line_.count('}') - bstart = k - k += 1 - while k < end and brace > 0: - brace += lines[k].count('{') - lines[k].count('}') - k += 1 - for_blocks.append((bstart, k - 1)) - continue - k += 1 - return for_blocks - - @staticmethod - def count_inc_dec(lines, var, for_blocks, start, end): - """ - Count increments and decrements of variable `var` outside any for-blocks. - """ - inc_outside = 0 - dec_outside = 0 - k = start - while k < end: - in_for = any(bstart <= k <= bend for (bstart, bend) in for_blocks) - if not in_for: - line_ = lines[k] - if re.search( - r'(\+\+' + re.escape(var) + r'|' + re.escape(var) + r'\+\+|' + - re.escape(var) + r'\s*\+=\s*1\b|' + - re.escape(var) + r'\s*=\s*' + re.escape(var) + r'\s*\+\s*1\b)', line_): - inc_outside += 1 - if re.search( - r'(--' + re.escape(var) + r'|' + re.escape(var) + r'--|' + - re.escape(var) + r'\s*-=\s*1\b|' + - re.escape(var) + r'\s*=\s*' + re.escape(var) + r'\s*-\s*1\b)', line_): - dec_outside += 1 - k += 1 - return inc_outside, dec_outside From 4a02d3cff496cc64d72d12ab124b54766037bec7 Mon Sep 17 00:00:00 2001 From: Yevhenii Kachanov Date: Tue, 15 Jul 2025 16:30:14 +0200 Subject: [PATCH 09/16] rework tests and pattern value method to (self, filename: str | os.PathLike) -> list[LineNumber] --- .../patterns/bidirect_index/bidirect_index.py | 39 +++++++++---------- .../bidirect_index/test_bidirect_index.py | 35 +++++++---------- 2 files changed, 33 insertions(+), 41 deletions(-) diff --git a/aibolit/patterns/bidirect_index/bidirect_index.py b/aibolit/patterns/bidirect_index/bidirect_index.py index 2283b7d3b..246e47f3a 100644 --- a/aibolit/patterns/bidirect_index/bidirect_index.py +++ b/aibolit/patterns/bidirect_index/bidirect_index.py @@ -1,7 +1,10 @@ # SPDX-FileCopyrightText: Copyright (c) 2019-2025 Aibolit # SPDX-License-Identifier: MIT +import os import re +from aibolit.types_decl import LineNumber + def find_methods(lines): """ @@ -118,40 +121,36 @@ def count_inc_dec(lines, var, for_blocks, start, end): class BidirectIndex: """ - This class analyzes Java source code to find line numbers where a variable - is used as a bidirectional index. - A bidirectional index is defined as a variable that: - - is assigned a value (with or without type declaration) within a method or block, - - is incremented and decremented somewhere in its scope, - - increments/decrements inside a for-loop with a local variable of the same name - are ignored (to avoid "fake" cases). - - The typical use-case: detect patterns like `i = 0; ... ++i; ... --i;` - in Java code, while ignoring manipulations of `i` inside - loops where `i` is a local loop variable (e.g., `for (int i = 0; ...) { ... }`). + Analyze Java source code to find line numbers where a variable is used as a bidirectional index. + + A bidirectional index is a variable that: + - is assigned a value (with or without type declaration) within a method or block, + - is incremented and decremented somewhere in its scope, + - increments/decrements inside a for-loop with a local variable of the same name are ignored. Usage: idx = BidirectIndex() - lines = open("MyClass.java", encoding="utf-8").readlines() - result = idx.value(lines) - # result is a list of line numbers matching the described pattern + result = idx.value("MyClass.java") """ def __init__(self): pass - def value(self, lines): + def value(self, filename: str | os.PathLike) -> list[LineNumber]: """ - Analyze the given Java file lines and return a sorted list of line numbers where a variable - is used as a bidirectional index as per the definition above. + Analyze the given Java file and return a sorted list of line numbers where a variable + is used as a bidirectional index. Args: - lines (list[str]): Lines of the Java source file. + filename (str | os.PathLike): Path to the Java source file. Returns: - List[int]: Sorted list of line numbers where bidirectional indices are found. + list[LineNumber]: Sorted list of line numbers where bidirectional indices are found. """ + + with open(filename, encoding='utf-8') as f: + lines = f.readlines() result: list[int] = [] for m_start, m_end in find_methods(lines): analyze_block(lines, m_start + 1, m_end, result) - return sorted(result) + return [LineNumber(n) for n in sorted(result)] diff --git a/test/patterns/bidirect_index/test_bidirect_index.py b/test/patterns/bidirect_index/test_bidirect_index.py index ce723f5a7..56d4d5cfe 100644 --- a/test/patterns/bidirect_index/test_bidirect_index.py +++ b/test/patterns/bidirect_index/test_bidirect_index.py @@ -10,66 +10,59 @@ class BidirectIndexTestCase(TestCase): dir_path = Path(os.path.realpath(__file__)).parent - def _lines_from_file(self, filename): - with open(filename, encoding='utf-8') as f: - return f.readlines() - def test_bidirect_index_increase_decrease(self): - lines = self._lines_from_file(Path(self.dir_path, 'BidirectIndexIncreaseDecrease.java')) + file_path = Path(self.dir_path, 'BidirectIndexIncreaseDecrease.java') self.assertEqual( - BidirectIndex().value(lines), + BidirectIndex().value(file_path), [6], 'Could not find bidirect index when index increased and then decreased', ) def test_bidirect_index_decrease_increase(self): - lines = self._lines_from_file(Path(self.dir_path, 'BidirectIndexDecreaseIncrease.java')) + file_path = Path(self.dir_path, 'BidirectIndexDecreaseIncrease.java') self.assertEqual( - BidirectIndex().value(lines), + BidirectIndex().value(file_path), [6], 'Could not find bidirect index when index decreased and then increased', ) def test_bidirect_index_increase_decrease_assignment(self): - lines = self._lines_from_file( - Path(self.dir_path, 'BidirectIndexIncreaseDecreaseAssignment.java')) + file_path = Path(self.dir_path, 'BidirectIndexIncreaseDecreaseAssignment.java') self.assertEqual( - BidirectIndex().value(lines), + BidirectIndex().value(file_path), [6], 'Could not find bidirect index when index increased and then decreased with assignment', ) def test_bidirect_index_increase_assignment_decrease(self): - lines = self._lines_from_file( - Path(self.dir_path, 'BidirectIndexIncreaseAssignmentDecrease.java')) + file_path = Path(self.dir_path, 'BidirectIndexIncreaseAssignmentDecrease.java') self.assertEqual( - BidirectIndex().value(lines), + BidirectIndex().value(file_path), [6], 'Could not find bidirect index when index increased with assignment and then decreased', ) def test_bidirect_index_increase_assignment_decrease_assignment(self): - lines = self._lines_from_file( - Path(self.dir_path, 'BidirectIndexIncreaseAssignmentDecreaseAssignment.java')) + file_path = Path(self.dir_path, 'BidirectIndexIncreaseAssignmentDecreaseAssignment.java') self.assertEqual( - BidirectIndex().value(lines), + BidirectIndex().value(file_path), [6], 'Could not find bidirect index when index increased with assignment ' 'and then decreased with assignment', ) def test_bidirect_index_hidden_scope_true(self): - lines = self._lines_from_file(Path(self.dir_path, 'BidirectIndexHiddenScope.java')) + file_path = Path(self.dir_path, 'BidirectIndexHiddenScope.java') self.assertEqual( - BidirectIndex().value(lines), + BidirectIndex().value(file_path), [], 'Could not find bidirec index when scope is hidden', ) def test_bidirect_index_outsider(self): - lines = self._lines_from_file(Path(self.dir_path, 'BidirectIndexOutsider.java')) + file_path = Path(self.dir_path, 'BidirectIndexOutsider.java') self.assertEqual( - BidirectIndex().value(lines), + BidirectIndex().value(file_path), [13], 'Could not find bidirec index when index is ot of loop', ) From bbc8cede5fc745be3b85d0b0ddb74e18c999e164 Mon Sep 17 00:00:00 2001 From: Yevhenii Kachanov Date: Tue, 15 Jul 2025 17:01:02 +0200 Subject: [PATCH 10/16] refactoring code by note: Update method calls to use instance methods. The calls to standalone functions should be updated to use instance methods after converting them. --- .../patterns/bidirect_index/bidirect_index.py | 227 +++++++++--------- 1 file changed, 111 insertions(+), 116 deletions(-) diff --git a/aibolit/patterns/bidirect_index/bidirect_index.py b/aibolit/patterns/bidirect_index/bidirect_index.py index 246e47f3a..a1a4db45f 100644 --- a/aibolit/patterns/bidirect_index/bidirect_index.py +++ b/aibolit/patterns/bidirect_index/bidirect_index.py @@ -6,119 +6,6 @@ from aibolit.types_decl import LineNumber -def find_methods(lines): - """ - Find the start and end line indices (0-based) for each Java method in the file. - - Args: - lines (list[str]): The lines of the Java source file to analyze. - - Returns: - list[tuple[int, int]]: Each tuple is (start_line_idx, end_line_idx) for a method. - """ - res = [] - brace = 0 - mstart = None - for idx, line in enumerate(lines): - if (re.search( - r'(public|private|protected|static|\s)*([\w<>\[\]]+)\s+\w+\s*\([^)]*\)\s*{', - line)): - if mstart is None: - mstart = idx - brace = 0 - brace += line.count('{') - brace -= line.count('}') - if mstart is not None and brace == 0: - res.append((mstart, idx)) - mstart = None - return res - - -def analyze_block(lines, start, end, result): - """ - Recursively analyze a block of code between 'start' and 'end' (inclusive). - """ - i = start - while i <= end: - line = lines[i] - typed_decl = re.match(r'\s*(int|long|byte|short)\s+(\w+)\s*=', line) - untyped_decl = re.match(r'^\s*(\w+)\s*=\s*[^;]+;', line) \ - if not typed_decl else None - var = typed_decl.group(2) if typed_decl else (untyped_decl.group(1) - if untyped_decl else None) - if var: - j = i + 1 - while j <= end: - line_ = lines[j] - if re.match(r'\s*(int|long|byte|short)\s+' + re.escape(var) + r'\s*=', line_): - break - j += 1 - for_blocks = find_for_blocks(lines, var, i + 1, j) - inc_outside, dec_outside = ( - count_inc_dec(lines, var, for_blocks, i + 1, j)) - if inc_outside and dec_outside: - result.append(i + 1) - if '{' in line: - brace, block_start = 1, i - i += 1 - while i <= end: - brace += lines[i].count('{') - brace -= lines[i].count('}') - if brace == 0: - analyze_block(lines, block_start + 1, i - 1, result) - break - i += 1 - else: - i += 1 - - -def find_for_blocks(lines, var, start, end): - """ - Find all ranges of for-loops that declare a local variable with the given name. - """ - for_blocks = [] - k = start - while k < end: - line_ = lines[k] - for_decl = re.match(r'\s*for\s*\(\s*int\s+' + re.escape(var) + r'\s*=', line_) - if for_decl: - brace = line_.count('{') - line_.count('}') - bstart = k - k += 1 - while k < end and brace > 0: - brace += lines[k].count('{') - lines[k].count('}') - k += 1 - for_blocks.append((bstart, k - 1)) - continue - k += 1 - return for_blocks - - -def count_inc_dec(lines, var, for_blocks, start, end): - """ - Count increments and decrements of variable `var` outside any for-blocks. - """ - inc_outside = 0 - dec_outside = 0 - k = start - while k < end: - in_for = any(bstart <= k <= bend for (bstart, bend) in for_blocks) - if not in_for: - line_ = lines[k] - if re.search( - r'(\+\+' + re.escape(var) + r'|' + re.escape(var) + r'\+\+|' + - re.escape(var) + r'\s*\+=\s*1\b|' + - re.escape(var) + r'\s*=\s*' + re.escape(var) + r'\s*\+\s*1\b)', line_): - inc_outside += 1 - if re.search( - r'(--' + re.escape(var) + r'|' + re.escape(var) + r'--|' + - re.escape(var) + r'\s*-=\s*1\b|' + - re.escape(var) + r'\s*=\s*' + re.escape(var) + r'\s*-\s*1\b)', line_): - dec_outside += 1 - k += 1 - return inc_outside, dec_outside - - class BidirectIndex: """ Analyze Java source code to find line numbers where a variable is used as a bidirectional index. @@ -136,6 +23,115 @@ class BidirectIndex: def __init__(self): pass + def _find_methods(self, lines): + """ + Find the start and end line indices (0-based) for each Java method in the file. + + Args: + lines (list[str]): The lines of the Java source file to analyze. + + Returns: + list[tuple[int, int]]: Each tuple is (start_line_idx, end_line_idx) for a method. + """ + res = [] + brace = 0 + mstart = None + for idx, line in enumerate(lines): + if (re.search( + r'(public|private|protected|static|\s)*([\w<>\[\]]+)\s+\w+\s*\([^)]*\)\s*{', + line)): + if mstart is None: + mstart = idx + brace = 0 + brace += line.count('{') + brace -= line.count('}') + if mstart is not None and brace == 0: + res.append((mstart, idx)) + mstart = None + return res + + def _analyze_block(self, lines, start, end, result): + """ + Recursively analyze a block of code between 'start' and 'end' (inclusive). + """ + i = start + while i <= end: + line = lines[i] + typed_decl = re.match(r'\s*(int|long|byte|short)\s+(\w+)\s*=', line) + untyped_decl = re.match(r'^\s*(\w+)\s*=\s*[^;]+;', line) \ + if not typed_decl else None + var = typed_decl.group(2) if typed_decl else (untyped_decl.group(1) + if untyped_decl else None) + if var: + j = i + 1 + while j <= end: + line_ = lines[j] + if re.match(r'\s*(int|long|byte|short)\s+' + re.escape(var) + r'\s*=', line_): + break + j += 1 + inc_outside, dec_outside = ( + self._count_inc_dec(lines, var, i + 1, j)) + if inc_outside and dec_outside: + result.append(i + 1) + if '{' in line: + brace, block_start = 1, i + i += 1 + while i <= end: + brace += lines[i].count('{') + brace -= lines[i].count('}') + if brace == 0: + self._analyze_block(lines, block_start + 1, i - 1, result) + break + i += 1 + else: + i += 1 + + def _find_for_blocks(self, lines, var, start, end): + """ + Find all ranges of for-loops that declare a local variable with the given name. + """ + for_blocks = [] + k = start + while k < end: + line_ = lines[k] + for_decl = re.match(r'\s*for\s*\(\s*int\s+' + re.escape(var) + r'\s*=', line_) + if for_decl: + brace = line_.count('{') - line_.count('}') + bstart = k + k += 1 + while k < end and brace > 0: + brace += lines[k].count('{') - lines[k].count('}') + k += 1 + for_blocks.append((bstart, k - 1)) + continue + k += 1 + return for_blocks + + def _count_inc_dec(self, lines, var, start, end): + """ + Count increments and decrements of variable `var` outside any for-blocks. + """ + for_blocks = self._find_for_blocks(lines, var, start, end) + inc_outside = 0 + dec_outside = 0 + k = start + while k < end: + in_for = any(bstart <= k <= bend for (bstart, bend) in for_blocks) + if not in_for: + line_ = lines[k] + if re.search( + r'(\+\+' + re.escape(var) + r'|' + re.escape(var) + r'\+\+|' + + re.escape(var) + r'\s*\+=\s*1\b|' + + re.escape(var) + r'\s*=\s*' + re.escape(var) + r'\s*\+\s*1\b)', line_): + inc_outside += 1 + if re.search( + r'(--' + re.escape(var) + r'|' + re.escape(var) + r'--|' + + re.escape(var) + r'\s*-=\s*1\b|' + + re.escape(var) + r'\s*=\s*' + re.escape(var) + r'\s*-\s*1\b)', line_): + dec_outside += 1 + k += 1 + return inc_outside, dec_outside + def value(self, filename: str | os.PathLike) -> list[LineNumber]: """ Analyze the given Java file and return a sorted list of line numbers where a variable @@ -147,10 +143,9 @@ def value(self, filename: str | os.PathLike) -> list[LineNumber]: Returns: list[LineNumber]: Sorted list of line numbers where bidirectional indices are found. """ - with open(filename, encoding='utf-8') as f: lines = f.readlines() result: list[int] = [] - for m_start, m_end in find_methods(lines): - analyze_block(lines, m_start + 1, m_end, result) + for m_start, m_end in self._find_methods(lines): + self._analyze_block(lines, m_start + 1, m_end, result) return [LineNumber(n) for n in sorted(result)] From cdce2a0e9310432b7ec11829d3b5ecafdd477b5b Mon Sep 17 00:00:00 2001 From: Yevhenii Kachanov Date: Tue, 15 Jul 2025 17:12:58 +0200 Subject: [PATCH 11/16] add improvements --- aibolit/patterns/bidirect_index/bidirect_index.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/aibolit/patterns/bidirect_index/bidirect_index.py b/aibolit/patterns/bidirect_index/bidirect_index.py index a1a4db45f..bd848d98e 100644 --- a/aibolit/patterns/bidirect_index/bidirect_index.py +++ b/aibolit/patterns/bidirect_index/bidirect_index.py @@ -58,7 +58,8 @@ def _analyze_block(self, lines, start, end, result): while i <= end: line = lines[i] typed_decl = re.match(r'\s*(int|long|byte|short)\s+(\w+)\s*=', line) - untyped_decl = re.match(r'^\s*(\w+)\s*=\s*[^;]+;', line) \ + # Only match simple reassignments, not method calls or field access + untyped_decl = re.match(r'^\s*(\w+)\s*=\s*[^=;]+;', line) \ if not typed_decl else None var = typed_decl.group(2) if typed_decl else (untyped_decl.group(1) if untyped_decl else None) @@ -66,6 +67,7 @@ def _analyze_block(self, lines, start, end, result): j = i + 1 while j <= end: line_ = lines[j] + # Check for both typed declarations and simple reassignments if re.match(r'\s*(int|long|byte|short)\s+' + re.escape(var) + r'\s*=', line_): break j += 1 @@ -94,7 +96,8 @@ def _find_for_blocks(self, lines, var, start, end): k = start while k < end: line_ = lines[k] - for_decl = re.match(r'\s*for\s*\(\s*int\s+' + re.escape(var) + r'\s*=', line_) + for_decl = re.match( + r'\s*for\s*\(\s*(int|long|byte|short)\s+' + re.escape(var) + r'\s*=', line_) if for_decl: brace = line_.count('{') - line_.count('}') bstart = k From 5fb34f96833095066a7402d41a70d517bdd94102 Mon Sep 17 00:00:00 2001 From: Yevhenii Kachanov Date: Tue, 15 Jul 2025 18:44:24 +0200 Subject: [PATCH 12/16] resolve params runtime error. --- aibolit/patterns/bidirect_index/bidirect_index.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aibolit/patterns/bidirect_index/bidirect_index.py b/aibolit/patterns/bidirect_index/bidirect_index.py index bd848d98e..e76b09127 100644 --- a/aibolit/patterns/bidirect_index/bidirect_index.py +++ b/aibolit/patterns/bidirect_index/bidirect_index.py @@ -72,7 +72,7 @@ def _analyze_block(self, lines, start, end, result): break j += 1 inc_outside, dec_outside = ( - self._count_inc_dec(lines, var, i + 1, j)) + self._count_inc_dec(lines, var, i + 1, j - 1)) if inc_outside and dec_outside: result.append(i + 1) if '{' in line: From 0bea3f9db8a497707af2871f2e07a7082383ed1e Mon Sep 17 00:00:00 2001 From: Yevhenii Kachanov Date: Sat, 26 Jul 2025 10:55:15 +0200 Subject: [PATCH 13/16] remove refactoring of tests --- .../bidirect_index/test_bidirect_index.py | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/test/patterns/bidirect_index/test_bidirect_index.py b/test/patterns/bidirect_index/test_bidirect_index.py index 56d4d5cfe..3dfec4256 100644 --- a/test/patterns/bidirect_index/test_bidirect_index.py +++ b/test/patterns/bidirect_index/test_bidirect_index.py @@ -11,58 +11,57 @@ class BidirectIndexTestCase(TestCase): dir_path = Path(os.path.realpath(__file__)).parent def test_bidirect_index_increase_decrease(self): - file_path = Path(self.dir_path, 'BidirectIndexIncreaseDecrease.java') self.assertEqual( - BidirectIndex().value(file_path), + BidirectIndex().value(Path(self.dir_path, 'BidirectIndexIncreaseDecrease.java')), [6], 'Could not find bidirect index when index increased and then decreased', ) def test_bidirect_index_decrease_increase(self): - file_path = Path(self.dir_path, 'BidirectIndexDecreaseIncrease.java') self.assertEqual( - BidirectIndex().value(file_path), + BidirectIndex().value(Path(self.dir_path, 'BidirectIndexDecreaseIncrease.java')), [6], 'Could not find bidirect index when index decreased and then increased', ) def test_bidirect_index_increase_decrease_assignment(self): - file_path = Path(self.dir_path, 'BidirectIndexIncreaseDecreaseAssignment.java') self.assertEqual( - BidirectIndex().value(file_path), + BidirectIndex().value( + Path(self.dir_path, 'BidirectIndexIncreaseDecreaseAssignment.java') + ), [6], 'Could not find bidirect index when index increased and then decreased with assignment', ) def test_bidirect_index_increase_assignment_decrease(self): - file_path = Path(self.dir_path, 'BidirectIndexIncreaseAssignmentDecrease.java') self.assertEqual( - BidirectIndex().value(file_path), + BidirectIndex().value( + Path(self.dir_path, 'BidirectIndexIncreaseAssignmentDecrease.java') + ), [6], 'Could not find bidirect index when index increased with assignment and then decreased', ) def test_bidirect_index_increase_assignment_decrease_assignment(self): - file_path = Path(self.dir_path, 'BidirectIndexIncreaseAssignmentDecreaseAssignment.java') self.assertEqual( - BidirectIndex().value(file_path), + BidirectIndex().value( + Path(self.dir_path, 'BidirectIndexIncreaseAssignmentDecreaseAssignment.java') + ), [6], 'Could not find bidirect index when index increased with assignment ' 'and then decreased with assignment', ) def test_bidirect_index_hidden_scope_true(self): - file_path = Path(self.dir_path, 'BidirectIndexHiddenScope.java') self.assertEqual( - BidirectIndex().value(file_path), + BidirectIndex().value(Path(self.dir_path, 'BidirectIndexHiddenScope.java')), [], 'Could not find bidirec index when scope is hidden', ) def test_bidirect_index_outsider(self): - file_path = Path(self.dir_path, 'BidirectIndexOutsider.java') self.assertEqual( - BidirectIndex().value(file_path), + BidirectIndex().value(Path(self.dir_path, 'BidirectIndexOutsider.java')), [13], 'Could not find bidirec index when index is ot of loop', ) From 5df7b193580cc0285e898ddcb0ceac3d50bae0e1 Mon Sep 17 00:00:00 2001 From: Yevhenii Kachanov Date: Sat, 26 Jul 2025 11:06:53 +0200 Subject: [PATCH 14/16] refactoring regex added re.VERBOSE for regex update doc string --- .../patterns/bidirect_index/bidirect_index.py | 100 +++++++++++------- 1 file changed, 60 insertions(+), 40 deletions(-) diff --git a/aibolit/patterns/bidirect_index/bidirect_index.py b/aibolit/patterns/bidirect_index/bidirect_index.py index e76b09127..c22c82be1 100644 --- a/aibolit/patterns/bidirect_index/bidirect_index.py +++ b/aibolit/patterns/bidirect_index/bidirect_index.py @@ -20,8 +20,23 @@ class BidirectIndex: result = idx.value("MyClass.java") """ - def __init__(self): - pass + def value(self, filename: str | os.PathLike) -> list[LineNumber]: + """ + Analyze the given Java file and return a sorted list of line numbers where a variable + is used as a bidirectional index. + + Args: + filename (str | os.PathLike): Path to the Java source file. + + Returns: + list[LineNumber]: Sorted list of line numbers where bidirectional indices are found. + """ + with open(filename, encoding='utf-8') as f: + lines = f.readlines() + result: list[int] = [] + for m_start, m_end in self._find_methods(lines): + self._analyze_block(lines, m_start + 1, m_end, result) + return [LineNumber(n) for n in sorted(result)] def _find_methods(self, lines): """ @@ -36,10 +51,18 @@ def _find_methods(self, lines): res = [] brace = 0 mstart = None + # Regular expression to find method declaration in Java + method_pattern = re.compile(r""" + (public|private|protected|static|\s)* # access modifiers and static + ([\w<>\[\]]+) # # return type + \s+ # space + \w+ # method name + \s* # possible spaces + \( [^)]* \) # parameters in brackets + \s* \{ # opening curly brace + """, re.VERBOSE) for idx, line in enumerate(lines): - if (re.search( - r'(public|private|protected|static|\s)*([\w<>\[\]]+)\s+\w+\s*\([^)]*\)\s*{', - line)): + if method_pattern.search(line): if mstart is None: mstart = idx brace = 0 @@ -57,18 +80,21 @@ def _analyze_block(self, lines, start, end, result): i = start while i <= end: line = lines[i] - typed_decl = re.match(r'\s*(int|long|byte|short)\s+(\w+)\s*=', line) - # Only match simple reassignments, not method calls or field access - untyped_decl = re.match(r'^\s*(\w+)\s*=\s*[^=;]+;', line) \ - if not typed_decl else None + # Finding a variable declaration with type + typed_decl = re.match( + r'\s*(int|long|byte|short)\s+(\w+)\s*=', line) + # Finding a simple assignment without a type + untyped_decl = re.match( + r'^\s*(\w+)\s*=\s*[^=;]+;', line) if not typed_decl else None var = typed_decl.group(2) if typed_decl else (untyped_decl.group(1) if untyped_decl else None) if var: j = i + 1 while j <= end: line_ = lines[j] - # Check for both typed declarations and simple reassignments - if re.match(r'\s*(int|long|byte|short)\s+' + re.escape(var) + r'\s*=', line_): + # Check for re-declaration of variable with type + if re.match( + r'\s*(int|long|byte|short)\s+' + re.escape(var) + r'\s*=', line_): break j += 1 inc_outside, dec_outside = ( @@ -94,11 +120,16 @@ def _find_for_blocks(self, lines, var, start, end): """ for_blocks = [] k = start + # Regular expression to find variable declaration in for + for_pattern = re.compile(r""" + \s*for\s* # keyword for + \( # opening parenthesis + \s*(int|long|byte|short) # variable type + \s+""" + re.escape(var) + r"""\s*= # variable name and = + """, re.VERBOSE) while k < end: line_ = lines[k] - for_decl = re.match( - r'\s*for\s*\(\s*(int|long|byte|short)\s+' + re.escape(var) + r'\s*=', line_) - if for_decl: + if for_pattern.match(line_): brace = line_.count('{') - line_.count('}') bstart = k k += 1 @@ -118,37 +149,26 @@ def _count_inc_dec(self, lines, var, start, end): inc_outside = 0 dec_outside = 0 k = start + # Regular expressions for increment and decrement + inc_pattern = re.compile(r""" + (\+\+""" + re.escape(var) + r"""| # ++var + """ + re.escape(var) + r"""\+\+| # var++ + """ + re.escape(var) + r"""\s*\+=\s*1\b| # var += 1 + """ + re.escape(var) + r"""\s*=\s*""" + re.escape(var) + r"""\s*\+\s*1\b) # var = var + 1 + """, re.VERBOSE) + dec_pattern = re.compile(r""" + (--""" + re.escape(var) + r"""| # --var + """ + re.escape(var) + r"""--| # var-- + """ + re.escape(var) + r"""\s*-=\s*1\b| # var -= 1 + """ + re.escape(var) + r"""\s*=\s*""" + re.escape(var) + r"""\s*-\s*1\b) # var = var - 1 + """, re.VERBOSE) while k < end: in_for = any(bstart <= k <= bend for (bstart, bend) in for_blocks) if not in_for: line_ = lines[k] - if re.search( - r'(\+\+' + re.escape(var) + r'|' + re.escape(var) + r'\+\+|' + - re.escape(var) + r'\s*\+=\s*1\b|' + - re.escape(var) + r'\s*=\s*' + re.escape(var) + r'\s*\+\s*1\b)', line_): + if inc_pattern.search(line_): inc_outside += 1 - if re.search( - r'(--' + re.escape(var) + r'|' + re.escape(var) + r'--|' + - re.escape(var) + r'\s*-=\s*1\b|' + - re.escape(var) + r'\s*=\s*' + re.escape(var) + r'\s*-\s*1\b)', line_): + if dec_pattern.search(line_): dec_outside += 1 k += 1 return inc_outside, dec_outside - - def value(self, filename: str | os.PathLike) -> list[LineNumber]: - """ - Analyze the given Java file and return a sorted list of line numbers where a variable - is used as a bidirectional index. - - Args: - filename (str | os.PathLike): Path to the Java source file. - - Returns: - list[LineNumber]: Sorted list of line numbers where bidirectional indices are found. - """ - with open(filename, encoding='utf-8') as f: - lines = f.readlines() - result: list[int] = [] - for m_start, m_end in self._find_methods(lines): - self._analyze_block(lines, m_start + 1, m_end, result) - return [LineNumber(n) for n in sorted(result)] From ff28b9a3d8a7e106bca6af5f87cb346a072f414a Mon Sep 17 00:00:00 2001 From: Yevhenii Kachanov Date: Sat, 26 Jul 2025 11:09:32 +0200 Subject: [PATCH 15/16] fix ruff and pylint issues --- .../patterns/bidirect_index/bidirect_index.py | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/aibolit/patterns/bidirect_index/bidirect_index.py b/aibolit/patterns/bidirect_index/bidirect_index.py index c22c82be1..42e21051d 100644 --- a/aibolit/patterns/bidirect_index/bidirect_index.py +++ b/aibolit/patterns/bidirect_index/bidirect_index.py @@ -52,7 +52,7 @@ def _find_methods(self, lines): brace = 0 mstart = None # Regular expression to find method declaration in Java - method_pattern = re.compile(r""" + method_pattern = re.compile(r''' (public|private|protected|static|\s)* # access modifiers and static ([\w<>\[\]]+) # # return type \s+ # space @@ -60,7 +60,7 @@ def _find_methods(self, lines): \s* # possible spaces \( [^)]* \) # parameters in brackets \s* \{ # opening curly brace - """, re.VERBOSE) + ''', re.VERBOSE) for idx, line in enumerate(lines): if method_pattern.search(line): if mstart is None: @@ -121,12 +121,12 @@ def _find_for_blocks(self, lines, var, start, end): for_blocks = [] k = start # Regular expression to find variable declaration in for - for_pattern = re.compile(r""" + for_pattern = re.compile(r''' \s*for\s* # keyword for \( # opening parenthesis \s*(int|long|byte|short) # variable type - \s+""" + re.escape(var) + r"""\s*= # variable name and = - """, re.VERBOSE) + \s+''' + re.escape(var) + r'''\s*= # variable name and = + ''', re.VERBOSE) while k < end: line_ = lines[k] if for_pattern.match(line_): @@ -150,18 +150,18 @@ def _count_inc_dec(self, lines, var, start, end): dec_outside = 0 k = start # Regular expressions for increment and decrement - inc_pattern = re.compile(r""" - (\+\+""" + re.escape(var) + r"""| # ++var - """ + re.escape(var) + r"""\+\+| # var++ - """ + re.escape(var) + r"""\s*\+=\s*1\b| # var += 1 - """ + re.escape(var) + r"""\s*=\s*""" + re.escape(var) + r"""\s*\+\s*1\b) # var = var + 1 - """, re.VERBOSE) - dec_pattern = re.compile(r""" - (--""" + re.escape(var) + r"""| # --var - """ + re.escape(var) + r"""--| # var-- - """ + re.escape(var) + r"""\s*-=\s*1\b| # var -= 1 - """ + re.escape(var) + r"""\s*=\s*""" + re.escape(var) + r"""\s*-\s*1\b) # var = var - 1 - """, re.VERBOSE) + inc_pattern = re.compile(r''' + (\+\+''' + re.escape(var) + r'''| # ++var + ''' + re.escape(var) + r'''\+\+| # var++ + ''' + re.escape(var) + r'''\s*\+=\s*1\b| # var += 1 + ''' + re.escape(var) + r'''\s*=\s*''' + re.escape(var) + r'''\s*\+\s*1\b)# var = var + 1 + ''', re.VERBOSE) + dec_pattern = re.compile(r''' + (--''' + re.escape(var) + r'''| # --var + ''' + re.escape(var) + r'''--| # var-- + ''' + re.escape(var) + r'''\s*-=\s*1\b| # var -= 1 + ''' + re.escape(var) + r'''\s*=\s*''' + re.escape(var) + r'''\s*-\s*1\b)# var = var - 1 + ''', re.VERBOSE) while k < end: in_for = any(bstart <= k <= bend for (bstart, bend) in for_blocks) if not in_for: From 1872c259b2708f041e61fa40c8c08f5c92f3e92d Mon Sep 17 00:00:00 2001 From: Yevhenii Kachanov Date: Sat, 26 Jul 2025 11:18:53 +0200 Subject: [PATCH 16/16] removed "#" symbol --- aibolit/patterns/bidirect_index/bidirect_index.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aibolit/patterns/bidirect_index/bidirect_index.py b/aibolit/patterns/bidirect_index/bidirect_index.py index 42e21051d..d45dc0a36 100644 --- a/aibolit/patterns/bidirect_index/bidirect_index.py +++ b/aibolit/patterns/bidirect_index/bidirect_index.py @@ -54,7 +54,7 @@ def _find_methods(self, lines): # Regular expression to find method declaration in Java method_pattern = re.compile(r''' (public|private|protected|static|\s)* # access modifiers and static - ([\w<>\[\]]+) # # return type + ([\w<>\[\]]+) # return type \s+ # space \w+ # method name \s* # possible spaces @@ -92,7 +92,7 @@ def _analyze_block(self, lines, start, end, result): j = i + 1 while j <= end: line_ = lines[j] - # Check for re-declaration of variable with type + # Check for both typed declarations and simple reassignments if re.match( r'\s*(int|long|byte|short)\s+' + re.escape(var) + r'\s*=', line_): break