-
Notifications
You must be signed in to change notification settings - Fork 22
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(explorer): calculate code statistics
- Loading branch information
1 parent
2596cfe
commit 84d171b
Showing
15 changed files
with
837 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
125 changes: 125 additions & 0 deletions
125
discopop_explorer/utilities/statistics/collect_statistics.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
# This file is part of the DiscoPoP software (http://www.discopop.tu-darmstadt.de) | ||
# | ||
# Copyright (c) 2020, Technische Universitaet Darmstadt, Germany | ||
# | ||
# This software may be modified and distributed under the terms of | ||
# the 3-Clause BSD License. See the LICENSE file in the package base | ||
# directory for details. | ||
|
||
from __future__ import annotations | ||
from typing import TYPE_CHECKING | ||
import logging | ||
|
||
from discopop_explorer.utilities.statistics.cyclomatic_complexity.boxplot import get_cyclomatic_complexities_for_boxplot | ||
from discopop_explorer.utilities.statistics.cyclomatic_complexity.total import get_summed_cyclomatic_complexity | ||
from discopop_explorer.utilities.statistics.maximum_call_path_depth import get_maximum_call_path_depth | ||
from discopop_explorer.utilities.statistics.num_function_calls import get_suggestion_num_function_calls | ||
from discopop_explorer.utilities.statistics.output_statistics import ( | ||
output_aggregated_suggestion_statistics, | ||
output_code_statistics, | ||
output_suggestion_statistics, | ||
) | ||
from discopop_explorer.utilities.statistics.suggestion_call_path_depths import get_suggestion_call_path_depths | ||
from discopop_explorer.utilities.statistics.suggestion_cyclomatic_complexity import ( | ||
get_suggestion_summed_cyclomatic_complexity_from_calls, | ||
) | ||
from discopop_explorer.utilities.statistics.suggestion_lines_of_code import ( | ||
get_suggestion_immediate_lines_of_code, | ||
get_suggestion_lines_of_code_including_calls, | ||
) | ||
|
||
if TYPE_CHECKING: | ||
from discopop_explorer.discopop_explorer import ExplorerArguments | ||
from discopop_library.result_classes.DetectionResult import DetectionResult | ||
|
||
logger = logging.getLogger("statistics") | ||
|
||
|
||
def collect_statistics(arguments: ExplorerArguments, res: DetectionResult) -> None: | ||
logger.info("Collecting code statistics...") | ||
maximum_call_path_depth = get_maximum_call_path_depth(res.pet) | ||
logger.debug("--> maximum_call_path_depth: " + str(maximum_call_path_depth)) | ||
suggestion_call_path_depths = get_suggestion_call_path_depths(res) | ||
logger.debug( | ||
"--> suggestion_call_path_depths: " | ||
+ str([str(key) + " => " + str(suggestion_call_path_depths[key]) for key in suggestion_call_path_depths]) | ||
) | ||
suggestion_num_function_calls = get_suggestion_num_function_calls(res) | ||
logger.debug( | ||
"--> suggestion_num_function_calls: " | ||
+ str([str(key) + " => " + str(suggestion_num_function_calls[key]) for key in suggestion_num_function_calls]) | ||
) | ||
suggestion_immediate_lines_of_code = get_suggestion_immediate_lines_of_code(res) | ||
logger.debug( | ||
"--> suggestion_immediate_lines_of_code: " | ||
+ str( | ||
[ | ||
str(key) + " => " + str(suggestion_immediate_lines_of_code[key]) | ||
for key in suggestion_immediate_lines_of_code | ||
] | ||
) | ||
) | ||
|
||
suggestion_lines_of_code_including_calls = get_suggestion_lines_of_code_including_calls(res) | ||
logger.debug( | ||
"--> suggestion_lines_of_code_including_calls: " | ||
+ str( | ||
[ | ||
str(key) + " => " + str(suggestion_lines_of_code_including_calls[key]) | ||
for key in suggestion_lines_of_code_including_calls | ||
] | ||
) | ||
) | ||
|
||
summed_cyclomatic_complexity = get_summed_cyclomatic_complexity(arguments, res) | ||
logger.debug("--> summed_cyclomatic_complexity = " + str(summed_cyclomatic_complexity)) | ||
|
||
cc_min, cc_max, cc_avg, cc_lower_quart, cc_upper_quart = get_cyclomatic_complexities_for_boxplot(arguments, res) | ||
logger.debug("--> cc_min: " + str(cc_min)) | ||
logger.debug("--> cc_max: " + str(cc_max)) | ||
logger.debug("--> cc_avg: " + str(cc_avg)) | ||
logger.debug("--> cc_lower_quart: " + str(cc_lower_quart)) | ||
logger.debug("--> cc_upper_quart: " + str(cc_upper_quart)) | ||
|
||
suggestion_summed_cyclomatic_complexity_from_calls = get_suggestion_summed_cyclomatic_complexity_from_calls( | ||
arguments, res | ||
) | ||
logger.debug( | ||
"--> suggestion_summed_cyclomatic_complexity_from_calls: " | ||
+ str( | ||
[ | ||
str(key) + " => " + str(suggestion_summed_cyclomatic_complexity_from_calls[key]) | ||
for key in suggestion_summed_cyclomatic_complexity_from_calls | ||
] | ||
) | ||
) | ||
|
||
# output statistics to file | ||
output_code_statistics( | ||
arguments, | ||
maximum_call_path_depth, | ||
summed_cyclomatic_complexity, | ||
cc_min, | ||
cc_max, | ||
cc_avg, | ||
cc_lower_quart, | ||
cc_upper_quart, | ||
) | ||
|
||
output_suggestion_statistics( | ||
arguments, | ||
suggestion_call_path_depths, | ||
suggestion_num_function_calls, | ||
suggestion_immediate_lines_of_code, | ||
suggestion_lines_of_code_including_calls, | ||
suggestion_summed_cyclomatic_complexity_from_calls, | ||
) | ||
|
||
output_aggregated_suggestion_statistics( | ||
arguments, | ||
suggestion_call_path_depths, | ||
suggestion_num_function_calls, | ||
suggestion_immediate_lines_of_code, | ||
suggestion_lines_of_code_including_calls, | ||
suggestion_summed_cyclomatic_complexity_from_calls, | ||
) |
63 changes: 63 additions & 0 deletions
63
discopop_explorer/utilities/statistics/cyclomatic_complexity/boxplot.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
# This file is part of the DiscoPoP software (http://www.discopop.tu-darmstadt.de) | ||
# | ||
# Copyright (c) 2020, Technische Universitaet Darmstadt, Germany | ||
# | ||
# This software may be modified and distributed under the terms of | ||
# the 3-Clause BSD License. See the LICENSE file in the package base | ||
# directory for details. | ||
|
||
from __future__ import annotations | ||
from typing import TYPE_CHECKING, Dict, List, Tuple | ||
|
||
if TYPE_CHECKING: | ||
from discopop_explorer.discopop_explorer import ExplorerArguments | ||
from discopop_library.result_classes.DetectionResult import DetectionResult | ||
|
||
# define aliases for readability | ||
from subprocess import check_output | ||
from discopop_library.PathManagement.PathManagement import load_file_mapping | ||
|
||
|
||
MIN = int | ||
MAX = int | ||
AVG = int | ||
LOWER_QUART = int | ||
UPPER_QUART = int | ||
|
||
|
||
def get_cyclomatic_complexities_for_boxplot( | ||
arguments: ExplorerArguments, res: DetectionResult | ||
) -> Tuple[MIN, MAX, AVG, LOWER_QUART, UPPER_QUART]: | ||
file_mapping = load_file_mapping(arguments.file_mapping_file) | ||
# get summed cyclomatic complexity for all functions in all files | ||
cmd = ["pmccabe", "-C"] | ||
for file_id in file_mapping: | ||
file_path = file_mapping[file_id] | ||
cmd.append(str(file_path)) | ||
out = check_output(cmd).decode("utf-8") | ||
|
||
# unpack and store results temporarily | ||
cyclomatic_complexities: List[int] = [] | ||
for line in out.split("\n"): | ||
split_line = line.split("\t") | ||
if len(split_line) < 9: | ||
continue | ||
cyclomatic_complexity = split_line[1] | ||
# file_path = split_line[7] | ||
# function_name = split_line[8] | ||
|
||
cyclomatic_complexities.append(int(cyclomatic_complexity)) | ||
|
||
# calculate statistics | ||
cc_min: MIN = min(cyclomatic_complexities) | ||
cc_max: MAX = max(cyclomatic_complexities) | ||
cc_avg: AVG = int(sum(cyclomatic_complexities) / len(cyclomatic_complexities)) | ||
sorted_cyclomatic_complexities = sorted(cyclomatic_complexities) | ||
lower_quartile_idx = int((len(sorted_cyclomatic_complexities) + 1) * 1 / 4) | ||
upper_quartile_idx = int((len(sorted_cyclomatic_complexities) + 1) * 3 / 4) | ||
lower_quartile = sorted_cyclomatic_complexities[lower_quartile_idx] | ||
if len(sorted_cyclomatic_complexities) == 1: | ||
upper_quartile_idx = 0 | ||
upper_quartile = sorted_cyclomatic_complexities[upper_quartile_idx] | ||
|
||
return cc_min, cc_max, cc_avg, lower_quartile, upper_quartile |
60 changes: 60 additions & 0 deletions
60
discopop_explorer/utilities/statistics/cyclomatic_complexity/cc_dictionary.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
# This file is part of the DiscoPoP software (http://www.discopop.tu-darmstadt.de) | ||
# | ||
# Copyright (c) 2020, Technische Universitaet Darmstadt, Germany | ||
# | ||
# This software may be modified and distributed under the terms of | ||
# the 3-Clause BSD License. See the LICENSE file in the package base | ||
# directory for details. | ||
|
||
from __future__ import annotations | ||
from subprocess import check_output | ||
from typing import TYPE_CHECKING, Dict, Optional | ||
|
||
from discopop_library.PathManagement.PathManagement import load_file_mapping | ||
|
||
if TYPE_CHECKING: | ||
from discopop_explorer.discopop_explorer import ExplorerArguments | ||
from discopop_library.result_classes.DetectionResult import DetectionResult | ||
|
||
|
||
FILE_ID = int | ||
FILEPATH = str | ||
FUNC_NAME = str | ||
CYC_COMP = int | ||
|
||
CC_DICT = Dict[FILE_ID, Dict[FUNC_NAME, CYC_COMP]] | ||
|
||
|
||
def get_cyclomatic_complexity_dictionary(arguments: ExplorerArguments, res: DetectionResult) -> CC_DICT: | ||
file_mapping = load_file_mapping(arguments.file_mapping_file) | ||
# get summed cyclomatic complexity for all functions in all files | ||
cmd = ["pmccabe", "-C"] | ||
for file_id in file_mapping: | ||
file_path = file_mapping[file_id] | ||
cmd.append(str(file_path)) | ||
out = check_output(cmd).decode("utf-8") | ||
|
||
# unpack and repack results | ||
cc_dict: Dict[FILE_ID, Dict[FUNC_NAME, CYC_COMP]] = dict() | ||
for line in out.split("\n"): | ||
split_line = line.split("\t") | ||
if len(split_line) < 9: | ||
continue | ||
cyclomatic_complexity: CYC_COMP = int(split_line[1]) | ||
file_path_2: FILEPATH = split_line[7] | ||
# get file_id | ||
file_id_2: Optional[int] = None | ||
for file_id in file_mapping: | ||
if str(file_mapping[file_id]) == file_path_2: | ||
file_id_2 = file_id | ||
|
||
if file_id_2 is None: | ||
continue | ||
|
||
func_name: FUNC_NAME = split_line[8] | ||
|
||
if file_id_2 not in cc_dict: | ||
cc_dict[file_id_2] = dict() | ||
cc_dict[file_id_2][func_name] = cyclomatic_complexity | ||
|
||
return cc_dict |
52 changes: 52 additions & 0 deletions
52
discopop_explorer/utilities/statistics/cyclomatic_complexity/subtree.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
# This file is part of the DiscoPoP software (http://www.discopop.tu-darmstadt.de) | ||
# | ||
# Copyright (c) 2020, Technische Universitaet Darmstadt, Germany | ||
# | ||
# This software may be modified and distributed under the terms of | ||
# the 3-Clause BSD License. See the LICENSE file in the package base | ||
# directory for details. | ||
|
||
from __future__ import annotations | ||
|
||
from typing import TYPE_CHECKING, List, cast | ||
|
||
from discopop_explorer.enums.EdgeType import EdgeType | ||
from discopop_explorer.functions.PEGraph.queries.edges import out_edges | ||
|
||
if TYPE_CHECKING: | ||
from discopop_explorer.discopop_explorer import ExplorerArguments | ||
from discopop_library.result_classes.DetectionResult import DetectionResult | ||
from discopop_explorer.utilities.statistics.cyclomatic_complexity.cc_dictionary import CC_DICT | ||
from discopop_explorer.aliases.NodeID import NodeID | ||
|
||
from discopop_explorer.classes.PEGraph.FunctionNode import FunctionNode | ||
from discopop_explorer.functions.PEGraph.queries.subtree import subtree_of_type | ||
|
||
|
||
def get_subtree_cyclomatic_complexity_from_calls( | ||
arguments: ExplorerArguments, res: DetectionResult, cc_dict: CC_DICT, root_node_id: NodeID | ||
) -> int: | ||
subtree = subtree_of_type(res.pet, res.pet.node_at(root_node_id)) | ||
# collect called functions | ||
called_functions: List[FunctionNode] = [] | ||
for node in subtree: | ||
out_call_edges = out_edges(res.pet, node.id, EdgeType.CALLSNODE) | ||
for _, target, _ in out_call_edges: | ||
called_functions.append(cast(FunctionNode, res.pet.node_at(target))) | ||
# identify cyclomatic complexities for called functions by matching against cc_dict entries | ||
# due to overloading or name extensions (i.e. get_a vs. get_a_and_b), select the shortest function name that matches | ||
cyclomatic_complexities: List[int] = [] | ||
for func in called_functions: | ||
if func.file_id not in cc_dict: | ||
continue | ||
candidates: List[str] = [] | ||
for clean_func_name in cc_dict[func.file_id]: | ||
if clean_func_name in func.name: | ||
candidates.append(clean_func_name) | ||
sorted_candidates = sorted(candidates, key=lambda x: len(x)) | ||
if len(sorted_candidates) > 0: | ||
best_match = sorted_candidates[0] | ||
cyclomatic_complexities.append(cc_dict[func.file_id][best_match]) | ||
|
||
# sum cyclomatic complexities of called functions | ||
return sum(cyclomatic_complexities) |
32 changes: 32 additions & 0 deletions
32
discopop_explorer/utilities/statistics/cyclomatic_complexity/total.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
# This file is part of the DiscoPoP software (http://www.discopop.tu-darmstadt.de) | ||
# | ||
# Copyright (c) 2020, Technische Universitaet Darmstadt, Germany | ||
# | ||
# This software may be modified and distributed under the terms of | ||
# the 3-Clause BSD License. See the LICENSE file in the package base | ||
# directory for details. | ||
|
||
from __future__ import annotations | ||
from typing import TYPE_CHECKING | ||
|
||
if TYPE_CHECKING: | ||
from discopop_explorer.discopop_explorer import ExplorerArguments | ||
from discopop_library.result_classes.DetectionResult import DetectionResult | ||
from discopop_library.PathManagement.PathManagement import load_file_mapping | ||
|
||
|
||
from subprocess import check_output | ||
|
||
|
||
def get_summed_cyclomatic_complexity(arguments: ExplorerArguments, res: DetectionResult) -> int: | ||
"""calculate the total cyclomatic complexity""" | ||
file_mapping = load_file_mapping(arguments.file_mapping_file) | ||
# get summed cyclomatic complexity for all functions in all files | ||
cmd = ["pmccabe", "-T"] | ||
for file_id in file_mapping: | ||
file_path = file_mapping[file_id] | ||
cmd.append(str(file_path)) | ||
out = check_output(cmd).decode("utf-8") | ||
summed_cyclomatic_complexity = int(out.split("\t")[1]) | ||
|
||
return summed_cyclomatic_complexity |
Oops, something went wrong.