-
Notifications
You must be signed in to change notification settings - Fork 51
#161: Fix class variable and tests #1229
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 5 commits
4014272
546de9e
8e96527
9d43a78
9c081dc
a4abb7a
d445e99
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,6 +12,7 @@ | |
| from bs4 import BeautifulSoup | ||
|
|
||
| from aibolit.ast_framework import AST, ASTNode, ASTNodeType | ||
| from aibolit.utils.ast_builder import build_ast | ||
|
|
||
|
|
||
| class NPathMetric(): | ||
|
|
@@ -27,6 +28,8 @@ def value(self, showoutput=False): | |
|
|
||
| if len(self.input) == 0: | ||
| raise ValueError('Empty file for analysis') | ||
| if shutil.which('mvn') is None: | ||
| return self._value_without_maven() | ||
| try: | ||
| root = os.path.join(tempfile.gettempdir(), uuid.uuid4().hex) | ||
| dirName = os.path.join(root, 'src/main/java') | ||
|
|
@@ -55,6 +58,19 @@ def value(self, showoutput=False): | |
| finally: | ||
| shutil.rmtree(root) | ||
|
|
||
| def _value_without_maven(self): | ||
| if not os.path.isfile(self.input): | ||
| raise Exception(' '.join(['File', self.input, 'does not exist'])) | ||
| try: | ||
| ast = AST.build_from_javalang(build_ast(self.input)) | ||
| except Exception as exception: | ||
| raise Exception(f'PMDException: {exception}') from exception | ||
| complexity = MvnFreeNPathMetric(ast).value() | ||
| return { | ||
| 'data': [{'file': self.input, 'complexity': complexity}], | ||
| 'errors': [], | ||
| } | ||
|
|
||
|
Comment on lines
+61
to
+73
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major 🧩 Analysis chain🏁 Script executed: #!/bin/bash
rg -nP --type=py -C3 'NPathMetric\s*\('Repository: cqfn/aibolit Length of output: 150 Fallback rejects directory inputs that the Maven path accepts. The Add directory handling to the fallback path to match the Maven behavior. aibolit/metrics/npath/main.py Lines: 61-73 Original code🤖 Prompt for AI Agents |
||
| def __parseFile(self, root): | ||
| result = {'data': [], 'errors': []} | ||
| content = [] | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,25 +1,135 @@ | ||
| # SPDX-FileCopyrightText: Copyright (c) 2019-2026 Aibolit | ||
| # SPDX-License-Identifier: MIT | ||
| import os | ||
| from dataclasses import dataclass | ||
| from typing import Any | ||
|
|
||
| from aibolit.ast_framework import AST, ASTNode, ASTNodeType | ||
| from aibolit.utils.ast_builder import build_ast | ||
|
|
||
| class BidirectIndex: | ||
|
|
||
| def __init__(self): | ||
| pass | ||
| @dataclass | ||
| class _VariableState: | ||
| line: int | ||
| increments: int = 0 | ||
| decrements: int = 0 | ||
|
|
||
|
|
||
| def value(self, filename: str | os.PathLike): | ||
| class BidirectIndex: | ||
|
|
||
| def value(self, filename: str | os.PathLike | AST): | ||
| """ | ||
| Finds if a variable is being incremented and decremented within the same method | ||
|
|
||
| :param filename: filename to be analyzed | ||
| :param filename: filename or AST to be analyzed | ||
| :return: list of LineNumber with the variable declaration lines | ||
|
|
||
| @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 | ||
| """ | ||
| return [] | ||
| ast = self._ast(filename) | ||
| lines: list[int] = [] | ||
| for method in ast.proxy_nodes(ASTNodeType.METHOD_DECLARATION): | ||
| lines.extend(self._bidirect_lines_in_method(method)) | ||
| return sorted(lines) | ||
|
|
||
| def _ast(self, source: str | os.PathLike | AST) -> AST: | ||
| if isinstance(source, AST): | ||
| return source | ||
| return AST.build_from_javalang(build_ast(source)) | ||
|
|
||
| def _bidirect_lines_in_method(self, method: ASTNode) -> list[int]: | ||
| states: list[_VariableState] = [] | ||
| scopes: list[dict[str, _VariableState]] = [{}] | ||
|
|
||
| def lookup(name: str) -> _VariableState | None: | ||
| for scope in reversed(scopes): | ||
| if name in scope: | ||
| return scope[name] | ||
| return None | ||
|
|
||
| def candidate(name: str, line: int) -> _VariableState: | ||
| state = lookup(name) | ||
| if state is None: | ||
| state = _VariableState(line) | ||
| scopes[-1][name] = state | ||
| states.append(state) | ||
| return state | ||
|
|
||
| def declare(node: ASTNode) -> None: | ||
| for name in node.names: | ||
| state = _VariableState(node.line) | ||
| scopes[-1][name] = state | ||
| states.append(state) | ||
|
|
||
| def member_name(node: ASTNode) -> str | None: | ||
| if node.node_type == ASTNodeType.MEMBER_REFERENCE and node.qualifier is None: | ||
| return node.member | ||
| return None | ||
|
|
||
| def mark_unary(node: ASTNode) -> None: | ||
| name = member_name(node) | ||
| if name is None: | ||
| return | ||
| operators = set(node.prefix_operators + node.postfix_operators) | ||
| state = candidate(name, node.line) | ||
| if '++' in operators: | ||
| state.increments += 1 | ||
| if '--' in operators: | ||
| state.decrements += 1 | ||
|
|
||
| def mark_assignment(node: ASTNode) -> None: | ||
| name = member_name(node.expressionl) | ||
| if name is None: | ||
| return | ||
| state = candidate(name, node.line) | ||
| if node.type == '+=': | ||
| state.increments += 1 | ||
| elif node.type == '-=': | ||
| state.decrements += 1 | ||
| elif node.type == '=': | ||
| mark_self_update(state, name, node.value) | ||
|
|
||
| def mark_self_update(state: _VariableState, name: str, value: ASTNode) -> None: | ||
| if value.node_type != ASTNodeType.BINARY_OPERATION: | ||
| return | ||
| if value.operator == '+' and ( | ||
| member_name(value.operandl) == name or member_name(value.operandr) == name | ||
| ): | ||
| state.increments += 1 | ||
| elif value.operator == '-' and member_name(value.operandl) == name: | ||
| state.decrements += 1 | ||
|
|
||
| def walk(node_or_nodes: Any) -> None: | ||
| if isinstance(node_or_nodes, list): | ||
| for node in node_or_nodes: | ||
| walk(node) | ||
| return | ||
|
|
||
| node = node_or_nodes | ||
| creates_scope = node.node_type in { | ||
| ASTNodeType.BLOCK_STATEMENT, | ||
| ASTNodeType.FOR_STATEMENT, | ||
| } | ||
| if creates_scope: | ||
| scopes.append({}) | ||
| try: | ||
| if node.node_type in { | ||
| ASTNodeType.LOCAL_VARIABLE_DECLARATION, | ||
| ASTNodeType.VARIABLE_DECLARATION, | ||
| }: | ||
| declare(node) | ||
| elif node.node_type == ASTNodeType.MEMBER_REFERENCE: | ||
| mark_unary(node) | ||
| elif node.node_type == ASTNodeType.ASSIGNMENT: | ||
| mark_assignment(node) | ||
|
|
||
| for child in node.children: | ||
| walk(child) | ||
| finally: | ||
| if creates_scope: | ||
| scopes.pop() | ||
|
|
||
| walk(method.body) | ||
| return [ | ||
| state.line | ||
| for state in states | ||
| if state.increments > 0 and state.decrements > 0 | ||
| ] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| # SPDX-FileCopyrightText: Copyright (c) 2019-2026 Aibolit | ||
| # SPDX-License-Identifier: MIT |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| # SPDX-FileCopyrightText: Copyright (c) 2019-2026 Aibolit | ||
| # SPDX-License-Identifier: MIT | ||
|
|
||
| from typing import List | ||
|
|
||
| from aibolit.ast_framework import AST, ASTNodeType | ||
|
|
||
|
|
||
| class ClassInheritance: | ||
| """ | ||
| Find classes that use implementation inheritance via the `extends` keyword. | ||
| """ | ||
|
|
||
| def value(self, ast: AST) -> List[int]: | ||
| lines: List[int] = [] | ||
| for class_declaration in ast.proxy_nodes(ASTNodeType.CLASS_DECLARATION): | ||
| if class_declaration.extends is not None: | ||
| lines.append(class_declaration.line) | ||
| return lines |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| # SPDX-FileCopyrightText: Copyright (c) 2019-2026 Aibolit | ||
| # SPDX-License-Identifier: MIT | ||
|
|
||
| from typing import List | ||
|
|
||
| from aibolit.ast_framework import AST, ASTNodeType | ||
|
|
||
|
|
||
| class ClassVariable: | ||
| """ | ||
| Find local variables declared with the same concrete class type they instantiate. | ||
| """ | ||
|
|
||
| def value(self, ast: AST) -> List[int]: | ||
| lines: List[int] = [] | ||
| for variable_declaration in ast.proxy_nodes(ASTNodeType.LOCAL_VARIABLE_DECLARATION): | ||
| if variable_declaration.type.node_type != ASTNodeType.REFERENCE_TYPE: | ||
| continue | ||
|
|
||
| declared_type = self._get_type_name(variable_declaration.type) | ||
| for declarator in variable_declaration.declarators: | ||
| initializer = declarator.initializer | ||
| if initializer is None or initializer.node_type != ASTNodeType.CLASS_CREATOR: | ||
| continue | ||
|
|
||
| if self._get_type_name(initializer.type) == declared_type: | ||
| lines.append(variable_declaration.line) | ||
| break | ||
| return lines | ||
|
|
||
| @staticmethod | ||
| def _get_type_name(type_node) -> str: | ||
| parts = [type_node.name] | ||
| sub_type = type_node.sub_type | ||
| while sub_type is not None: | ||
| parts.append(sub_type.name) | ||
| sub_type = sub_type.sub_type | ||
| return '.'.join(parts) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Add a
P35section to keep the pattern catalog consistent with registry.aibolit/config.pyregistersP35(Class variable) at Line 204, butPATTERNS.mdcurrently has noP35entry. This leaves the public pattern dictionary incomplete.🤖 Prompt for AI Agents