Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
46 changes: 18 additions & 28 deletions aibolit/metrics/external_methods_called/external_methods_called.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
# SPDX-FileCopyrightText: Copyright (c) 2019-2026 Aibolit
# SPDX-License-Identifier: MIT
import itertools
import os
from dataclasses import dataclass
from typing import Iterator
from typing import Set

from aibolit.ast_framework import AST, ASTNode, ASTNodeType
from aibolit.ast_framework import AST, ASTNodeType
from aibolit.utils.ast_builder import build_ast


Expand All @@ -14,11 +12,10 @@ class ExternalMethodsCalled:
Measure the number of external methods called by the class.
"""

def __init__(self):
pass

def value(self, filepath: str | os.PathLike):
return ExternalMethodsCalledCount(AST.build_from_javalang(build_ast(filepath))).total()
def value(self, ast: AST) -> int:
if not isinstance(ast, AST):
ast = AST.build_from_javalang(build_ast(ast))
return ExternalMethodsCalledCount(ast).total()


@dataclass(frozen=True)
Expand All @@ -28,23 +25,16 @@ class ExternalMethodsCalledCount:
def total(self) -> int:
return len(set(self._external_method_names_called()))

def _external_method_names_called(self) -> Iterator[str]:
for node in self._parent_method_invocation_nodes():
for first, second in itertools.pairwise(node.children):
if self._member_reference_followed_by_method_invocation(first, second):
yield second.member

def _parent_method_invocation_nodes(self) -> Iterator[ASTNode]:
for node in self.ast.proxy_nodes(ASTNodeType.METHOD_INVOCATION):
if (parent := node.parent) is not None:
yield parent

def _member_reference_followed_by_method_invocation(
self,
first: ASTNode,
second: ASTNode,
) -> bool:
return (
first.node_type == ASTNodeType.MEMBER_REFERENCE and
second.node_type == ASTNodeType.METHOD_INVOCATION
def _external_method_names_called(self) -> Set[str]:
local_methods = self._local_method_names()
return set(
node.member
for node in self.ast.proxy_nodes(ASTNodeType.METHOD_INVOCATION)
if node.member not in local_methods
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.

def _local_method_names(self) -> Set[str]:
return set(
node.name
for node in self.ast.proxy_nodes(ASTNodeType.METHOD_DECLARATION)
)
13 changes: 13 additions & 0 deletions test/metrics/external_methods_called/LocalMethodCall.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// SPDX-FileCopyrightText: Copyright (c) 2019-2026 Aibolit
// SPDX-License-Identifier: MIT

public class LocalMethodCall {

public String value() {
return "value";
}

public String call() {
return this.value();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,40 +5,55 @@
from pathlib import Path
from unittest import TestCase

from aibolit.ast_framework import AST
from aibolit.metrics.external_methods_called.external_methods_called import ExternalMethodsCalled
from aibolit.utils.ast_builder import build_ast


class ExternalMethodsCalledTest(TestCase):
dir_path = Path(os.path.realpath(__file__)).parent

def test_no_external_method_calls(self):
self.assertEqual(
ExternalMethodsCalled().value(Path(self.dir_path, 'NoExternalMethodCalls.java')),
ExternalMethodsCalled().value(self._ast('NoExternalMethodCalls.java')),
0,
'Could not calculate how many external methods are called when none'
)

def test_external_method_calls(self):
self.assertEqual(
ExternalMethodsCalled().value(Path(self.dir_path, 'ExternalMethodCalls.java')),
ExternalMethodsCalled().value(self._ast('ExternalMethodCalls.java')),
2,
'Could not calculate how many external method are called when they exist'
)

def test_external_method_calls_from_filepath(self):
self.assertEqual(
ExternalMethodsCalled().value(Path(self.dir_path, 'ExternalMethodCalls.java')),
2,
'Could not calculate external methods from a file path'
)

def test_inner_class_external_method_calls(self):
self.assertEqual(
ExternalMethodsCalled().value(
Path(self.dir_path, 'InnerClassExternalMethodCalls.java')
),
ExternalMethodsCalled().value(self._ast('InnerClassExternalMethodCalls.java')),
1,
'Could not calculate how many external methods called when they exist in inner class'
)

def test_double_external_method_calls(self):
self.assertEqual(
ExternalMethodsCalled().value(
Path(self.dir_path, 'DoubleExternalMethodCalls.java')
),
ExternalMethodsCalled().value(self._ast('DoubleExternalMethodCalls.java')),
1,
'Could not calculate external methods called when they are called more than once'
)

def test_local_method_call_is_not_external(self):
self.assertEqual(
ExternalMethodsCalled().value(self._ast('LocalMethodCall.java')),
0,
'Could not ignore local method calls'
)

def _ast(self, filename):
return AST.build_from_javalang(build_ast(Path(self.dir_path, filename)))
Loading