From f713067814b2b7244bcf04a88c8ce230a57234e8 Mon Sep 17 00:00:00 2001 From: Evgeny Maslov Date: Tue, 26 May 2020 14:22:05 +0300 Subject: [PATCH] Reorganized console output --- README.md | 85 ++++++++++++++++++++- aibolit/__main__.py | 92 +++++++++++++++++++---- test/recommend/test_recommend_pipeline.py | 48 +++++++++++- 3 files changed, 207 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 785c05ca..265bfb89 100644 --- a/README.md +++ b/README.md @@ -33,8 +33,89 @@ It will run recommendation function for the model (model is located in [aibolit/ The model finds a pattern which contribution is the largest to the Cyclomatic Complexity. If anything is found, you will see all recommendations for the mentioned patterns. You can see the list of all patterns in [Patterns.md](https://github.com/yegor256/aibolit/blob/master/PATTERNS.md). -The output of recommendation will be saved to the `out.xml` file into the current directory. -You can change the output file, using the `--output` parameter. +The output of recommendation will be redirected to the stdout. +If the program has the `0` exit code, it means that all analyzed files do not have any issues. +If the program has the `1` exit code, it means that at least 1 analyzed file has an issue. +If the program has the `2` exit code, it means that program crash occurred. + +You can change the format, using the `--format` parameter. The default parameter is `--format=text`. +```bash +$ aibolit recommend --folder src/java --format=text --full +``` + +It will show the text, where all data are sorted by pattern's importance in descending order and grouped by a pattern name: + +``` +Show all patterns +Filename /mnt/d/src/java/Configuration.java: +Score for file: 127.67642529949538 +Some issues found +line 294: Null check (P13) +line 391: Null check (P13) +line 235: Non final attribute (P12) +line 3840: Var in the middle (P21) +line 3844: Var in the middle (P21) +line 3848: Var in the middle (P21) +line 2411: Null Assignment (P28) +Filename /mnt/d/src/java/ErrorExample.java: +Error when calculating patterns: Can't count P1 metric: +Filename /mnt/d/src/java/MavenSlice.java: +Your code is perfect in aibolit's opinion +Total score: 127.67642529949538 + +``` + +You can also choose xml format. It will have the same format as `text` mode, but xml will be created: + +```xml + + 127.67642529949538 + + + + /mnt/d/src/java/Configuration.java + Some issues found + 127.67642529949538 + + +
Null check
+ + 294 + 391 + +
+ +
Non final attribute
+ + 235 + +
+ +
Var in the middle
+ + 235 + +
+ +
Null Assignment
+ + 2411 + +
+
+
+ + /mnt/d/src/java/ErrorExample.java + Error when calculating patterns: Can't count P1 metric: + + + /mnt/d/src/java/MavenSlice.java + Your code is perfect in aibolit's opinion + +
+
+ +``` Model is automatically installed with *aibolit* package, but you can also try your own model diff --git a/aibolit/__main__.py b/aibolit/__main__.py index f9a4f4ff..b01f1bab 100644 --- a/aibolit/__main__.py +++ b/aibolit/__main__.py @@ -39,6 +39,7 @@ from pathlib import Path import pickle from aibolit.model.model import TwoFoldRankingModel, Dataset # type: ignore # noqa: F401 +from sys import stdout dir_path = os.path.dirname(os.path.realpath(__file__)) @@ -225,7 +226,6 @@ def run_recommend_for_file(file: str, args): :param args: different command line arguments :return: dict with code lines, filename and pattern name """ - print('Analyzing {}'.format(file)) java_file = str(Path(os.getcwd(), file)) input_params, code_lines_dict, error_string = calculate_patterns_and_metrics(java_file) results_list, importances = inference(input_params, code_lines_dict, args) @@ -322,13 +322,48 @@ def get_exit_code(results): return exit_code +def create_text(results, full_report): + importances_for_all_classes = [] + buffer = [] + if not full_report: + buffer.append('Show pattern with the largest contribution to Cognitive Complexity') + else: + buffer.append('Show all patterns') + for result_for_file in results: + filename = result_for_file.get('filename') + buffer.append('Filename {}: '.format(filename)) + results = result_for_file.get('results') + errors_string = result_for_file.get('error_string') + if not results and not errors_string: + output_string = 'Your code is perfect in aibolit\'s opinion' + buffer.append(output_string) + elif not results and errors_string: + output_string = 'Error when calculating patterns: {}'.format(str(errors_string)) + buffer.append(output_string) + else: + output_string = 'Some issues found' + score = result_for_file['importances'] + importances_for_all_classes.append(score) + buffer.append('Score for file: {}'.format(score)) + buffer.append(output_string) + for pattern_item in result_for_file['results']: + code = pattern_item.get('pattern_code') + if code: + pattern_name_str = pattern_item.get('pattern_name') + buffer.append('line {}: {} ({})'.format(pattern_item.get('code_line'), pattern_name_str, code)) + if importances_for_all_classes: + buffer.append('Total score: {}'.format(np.mean(importances_for_all_classes))) + + return buffer + + def recommend(): """Run recommendation pipeline.""" parser = argparse.ArgumentParser( description='Get recommendations for Java code', usage=''' - aibolit recommend < --folder | --filenames > [--output] [--model_file] [--threshold] [--full] + aibolit recommend < --folder | --filenames > [--output] [--model_file] [--threshold] [--full] [--format] ''') group_exclusive = parser.add_mutually_exclusive_group(required=True) @@ -344,12 +379,6 @@ def recommend(): nargs="*", default=False ) - parser.add_argument( - '--output', - help='output of xml file where all results will be saved, default is out.xml of the current directory', - default=False - ) - parser.add_argument( '--model_file', help='''file where pretrained model is located, the default path is located @@ -368,6 +397,11 @@ def recommend(): default=False, action='store_true' ) + parser.add_argument( + '--format', + default='text', + help='text (by default) or xml. Usage: --format=xml' + ) args = parser.parse_args(sys.argv[2:]) @@ -382,18 +416,44 @@ def recommend(): results = list(run_thread(files, args)) - if args.output: - filename = args.output - else: - filename = 'out.xml' + if args.format: + new_results = format_converter_for_pattern(results) + if args.format == 'text': + text = create_text(new_results, args) + print('\n'.join(text)) + elif args.format == 'xml': + root = create_xml_tree(results, args.full) + tree = root.getroottree() + tree.write(stdout.buffer, pretty_print=True) + else: + raise Exception('Unknown format') - root = create_xml_tree(results, args.full) - tree = root.getroottree() - tree.write(filename, pretty_print=True) exit_code = get_exit_code(results) return exit_code +def format_converter_for_pattern(results): + """Reformat data where data are sorted by patterns importance + (it is already sorted in the input). + Then lines are sorted in ascending order.""" + + def flatten(l): + return [item for sublist in l for item in sublist] + + for file in results: + items = file.get('results') + if items: + new_items = flatten([ + [{'pattern_code': x['pattern_code'], + 'pattern_name': x['pattern_name'], + 'code_line': line, + } for line in sorted(x['code_lines'])] for x in items + ]) + file['results'] = new_items + + return results + + def version(): """ Parses arguments and shows current version of program. @@ -429,6 +489,8 @@ def main(): } exit_code = run_parse_args(commands) except Exception: + import traceback + traceback.print_exc() sys.exit(2) else: sys.exit(exit_code) diff --git a/test/recommend/test_recommend_pipeline.py b/test/recommend/test_recommend_pipeline.py index c86e1377..6da72492 100644 --- a/test/recommend/test_recommend_pipeline.py +++ b/test/recommend/test_recommend_pipeline.py @@ -28,7 +28,8 @@ from aibolit.config import Config from lxml import etree -from aibolit.__main__ import list_dir, calculate_patterns_and_metrics, create_xml_tree +from aibolit.__main__ import list_dir, calculate_patterns_and_metrics, \ + create_xml_tree, create_text, format_converter_for_pattern class TestRecommendPipeline(TestCase): @@ -38,6 +39,38 @@ def __init__(self, *args, **kwargs): self.cur_file_dir = Path(os.path.realpath(__file__)).parent self.config = Config.get_patterns_config() + def __create_mock_input(self): + patterns = [x['code'] for x in self.config['patterns']] + item = { + 'filename': '1.java', + 'results': [ + {'pattern_code': 'P23', + 'pattern_name': 'Some patterns name', + 'code_lines': [1, 2, 4] + } + ], + 'importances': sum([0.1 + x for x in range(len(patterns))]) + } + another_item = { + 'filename': 'hdd/home/jardani_jovonovich/John_wick.java', + 'results': [ + {'pattern_code': 'P2', + 'pattern_name': 'Somebody please get this man a gun', + 'code_lines': [10, 100, 15000]}, + {'pattern_code': 'P4', + 'pattern_name': 'New item', + 'code_lines': [5, 6]} + ], + 'importances': sum([0.1 + 2 * x for x in range(len(patterns))]) + } + error_file = { + 'error_string': "Error occured", + 'filename': 'hdd/home/Error.java', + 'results': [] + } + mock_input = [item, another_item, error_file] + return mock_input + def test_calculate_patterns_and_metrics(self): file = Path(self.cur_file_dir, 'folder/LottieImageAsset.java') calculate_patterns_and_metrics(file) @@ -93,3 +126,16 @@ def test_xml_empty_resutls(self): xml_string = create_xml_tree([], True) md5_hash = md5(etree.tostring(xml_string)) self.assertEqual(md5_hash.hexdigest(), '7d55be99025f9d9bba410bdbd2c42cee') + + def test_text_format(self): + mock_input = self.__create_mock_input() + new_mock = format_converter_for_pattern(mock_input) + text = create_text(new_mock, full_report=True) + md5_hash = md5('\n'.join(text).encode('utf-8')) + self.assertEqual(md5_hash.hexdigest(), 'e59a6eced350dc1320dffc2b99dcfecd') + + def test_empty_text_format(self): + new_mock = format_converter_for_pattern([]) + text = create_text(new_mock, full_report=True) + md5_hash = md5('\n'.join(text).encode('utf-8')) + self.assertEqual(md5_hash.hexdigest(), 'bc22beda46ca18267a677eb32361a2aa')