From 051260c4680e4d5bfe3da2b2ff99a1ad8d336827 Mon Sep 17 00:00:00 2001 From: Vlasov Ilya <55441714+GirZ0n@users.noreply.github.com> Date: Sat, 3 Apr 2021 20:01:28 +0500 Subject: [PATCH 01/36] wps-light support (#17) Add wps-light support --- .github/workflows/build.yml | 4 +- requirements.txt | 1 + src/python/review/inspectors/flake8/.flake8 | 28 +++++++ src/python/review/inspectors/flake8/flake8.py | 12 ++- .../review/inspectors/flake8/issue_types.py | 73 +++++++++++++++++ .../inspectors/test_flake8_inspector.py | 21 +++-- .../inspectors/test_pylint_inspector.py | 2 +- .../inspectors/python/case13_complex_logic.py | 82 ++++++++----------- .../python/case13_complex_logic_2.py | 16 ++-- .../python/case3_redefining_builtin.py | 5 +- .../inspectors/python/case4_naming.py | 2 +- .../inspectors/python/case7_empty_lines.py | 5 +- 12 files changed, 177 insertions(+), 74 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6c56547d..c177f809 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,7 +27,7 @@ jobs: # stop the build if there are Python syntax errors or undefined names flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics --exclude=.git,__pycache__,docs/source/conf.py,old,build,dist,venv,test/resources,.eggs,review.egg-info,.pytest_cache,node_modules # TODO: change max-complexity into 10 after refactoring - flake8 . --count --max-complexity=11 --max-line-length=120 --max-doc-length=120 --ignore=I201,I202,I101,I100,R504,A003,E800,SC200,SC100,E402 --statistics --exclude=.git,__pycache__,docs/source/conf.py,old,build,dist,venv,test/resources,.eggs,review.egg-info,.pytest_cache,node_modules + flake8 . --count --max-complexity=11 --max-line-length=120 --max-doc-length=120 --ignore=I201,I202,I101,I100,R504,A003,E800,SC200,SC100,E402,W503,WPS --statistics --exclude=.git,__pycache__,docs/source/conf.py,old,build,dist,venv,test/resources,.eggs,review.egg-info,.pytest_cache,node_modules - name: Set up Eslint run: | npm install eslint --save-dev @@ -40,4 +40,4 @@ jobs: run: java -version - name: Test with pytest run: | - pytest \ No newline at end of file + pytest diff --git a/requirements.txt b/requirements.txt index 6fe19370..caa6e2e2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,6 +17,7 @@ flake8-return==1.1.1 flake8-spellcheck==0.14.0 mccabe==0.6.1 pep8-naming==0.11.1 +wps-light==0.15.2 # extra libraries and frameworks django==3.0.8 diff --git a/src/python/review/inspectors/flake8/.flake8 b/src/python/review/inspectors/flake8/.flake8 index 6270ce48..0ad30a62 100644 --- a/src/python/review/inspectors/flake8/.flake8 +++ b/src/python/review/inspectors/flake8/.flake8 @@ -13,3 +13,31 @@ ignore=W291, # trailing whitespaces E301, E302, E303, E304, E305, # problem with stepik templates E402, # module level import not at top of file I100, # Import statements are in the wrong order + # WPS: Naming + WPS110, # Forbid blacklisted variable names. + WPS111, # Forbid short variable or module names. + WPS112, # Forbid private name pattern. + WPS114, # Forbid names with underscored numbers pattern. + WPS125, # Forbid variable or module names which shadow builtin names. TODO: Collision with flake8-builtins + # WPS: Consistency + WPS303, # Forbid underscores in numbers. + WPS305, # Forbid f strings. + WPS306, # Forbid writing classes without base classes. + WPS318, # Forbid extra indentation. TODO: Collision with standard flake8 indentation check + WPS323, # Forbid % formatting on strings. + WPS324, # Enforce consistent return statements. TODO: Collision with flake8-return + WPS335, # Forbid wrong for loop iter targets. + WPS358, # Forbid using float zeros: 0.0. + WPS362, # Forbid assignment to a subscript slice. + # WPS: Best practices + WPS404, # Forbid complex defaults. TODO: Collision with "B006" + WPS420, # Forbid some python keywords. + WPS421, # Forbid calling some built-in functions.(e.g., print) + WPS429, # Forbid multiple assignments on the same line. + WPS430, # Forbid nested functions. + WPS431, # Forbid nested classes. + WPS435, # Forbid multiplying lists. + # WPS: Refactoring + WPS527, # Require tuples as arguments for frozenset. + # WPS: OOP + WPS602, # Forbid @staticmethod decorator. diff --git a/src/python/review/inspectors/flake8/flake8.py b/src/python/review/inspectors/flake8/flake8.py index 6f071338..9784b8ab 100644 --- a/src/python/review/inspectors/flake8/flake8.py +++ b/src/python/review/inspectors/flake8/flake8.py @@ -27,7 +27,7 @@ def inspect(cls, path: Path, config: dict) -> List[BaseIssue]: f'--format={FORMAT}', f'--config={PATH_FLAKE8_CONFIG}', '--max-complexity', '0', - path + path, ] output = run_in_subprocess(command) return cls.parse(output) @@ -66,11 +66,17 @@ def parse(cls, output: str) -> List[BaseIssue]: @staticmethod def choose_issue_type(code: str) -> IssueType: + # Handling individual codes if code in CODE_TO_ISSUE_TYPE: return CODE_TO_ISSUE_TYPE[code] - code_prefix = re.match(r'^([a-z]+)\d+$', code, re.IGNORECASE).group(1) - issue_type = CODE_PREFIX_TO_ISSUE_TYPE.get(code_prefix) + regex_match = re.match(r'^([A-Z]+)(\d)\d*$', code, re.IGNORECASE) + code_prefix = regex_match.group(1) + first_code_number = regex_match.group(2) + + # Handling other issues + issue_type = (CODE_PREFIX_TO_ISSUE_TYPE.get(code_prefix + first_code_number) + or CODE_PREFIX_TO_ISSUE_TYPE.get(code_prefix)) if not issue_type: logger.warning(f'flake8: {code} - unknown error code') return IssueType.BEST_PRACTICES diff --git a/src/python/review/inspectors/flake8/issue_types.py b/src/python/review/inspectors/flake8/issue_types.py index e98a4836..749d708b 100644 --- a/src/python/review/inspectors/flake8/issue_types.py +++ b/src/python/review/inspectors/flake8/issue_types.py @@ -15,6 +15,72 @@ # builtin naming 'A003': IssueType.BEST_PRACTICES, + + # WPS: Naming + "WPS117": IssueType.CODE_STYLE, # Forbid naming variables self, cls, or mcs. + "WPS125": IssueType.ERROR_PRONE, # Forbid variable or module names which shadow builtin names. + + # WPS: Consistency + "WPS300": IssueType.CODE_STYLE, # Forbid imports relative to the current folder. + "WPS301": IssueType.CODE_STYLE, # Forbid imports like import os.path. + "WPS304": IssueType.CODE_STYLE, # Forbid partial floats like .05 or 23.. + "WPS310": IssueType.BEST_PRACTICES, # Forbid uppercase X, O, B, and E in numbers. + "WPS313": IssueType.CODE_STYLE, # Enforce separation of parenthesis from keywords with spaces. + "WPS317": IssueType.CODE_STYLE, # Forbid incorrect indentation for parameters. + "WPS318": IssueType.CODE_STYLE, # Forbid extra indentation. + "WPS319": IssueType.CODE_STYLE, # Forbid brackets in the wrong position. + "WPS320": IssueType.CODE_STYLE, # Forbid multi-line function type annotations. + "WPS321": IssueType.CODE_STYLE, # Forbid uppercase string modifiers. + "WPS324": IssueType.ERROR_PRONE, # If any return has a value, all return nodes should have a value. + "WPS325": IssueType.ERROR_PRONE, # If any yield has a value, all yield nodes should have a value. + "WPS326": IssueType.ERROR_PRONE, # Forbid implicit string concatenation. + "WPS329": IssueType.ERROR_PRONE, # Forbid meaningless except cases. + "WPS330": IssueType.ERROR_PRONE, # Forbid unnecessary operators in your code. + "WPS338": IssueType.BEST_PRACTICES, # Forbid incorrect order of methods inside a class. + "WPS339": IssueType.CODE_STYLE, # Forbid meaningless zeros. + "WPS340": IssueType.CODE_STYLE, # Forbid extra + signs in the exponent. + "WPS341": IssueType.CODE_STYLE, # Forbid lowercase letters as hex numbers. + "WPS343": IssueType.CODE_STYLE, # Forbid uppercase complex number suffix. + "WPS344": IssueType.ERROR_PRONE, # Forbid explicit division (or modulo) by zero. + "WPS347": IssueType.ERROR_PRONE, # Forbid imports that may cause confusion outside of the module. + "WPS348": IssueType.CODE_STYLE, # Forbid starting lines with a dot. + "WPS350": IssueType.CODE_STYLE, # Enforce using augmented assign pattern. + "WPS355": IssueType.CODE_STYLE, # Forbid useless blank lines before and after brackets. + "WPS361": IssueType.CODE_STYLE, # Forbids inconsistent newlines in comprehensions. + + # WPS: Best practices + "WPS405": IssueType.ERROR_PRONE, # Forbid anything other than ast.Name to define loop variables. + "WPS406": IssueType.ERROR_PRONE, # Forbid anything other than ast.Name to define contexts. + "WPS408": IssueType.ERROR_PRONE, # Forbid using the same logical conditions in one expression. + "WPS414": IssueType.ERROR_PRONE, # Forbid tuple unpacking with side-effects. + "WPS415": IssueType.ERROR_PRONE, # Forbid the same exception class in multiple except blocks. + "WPS416": IssueType.ERROR_PRONE, # Forbid yield keyword inside comprehensions. + "WPS417": IssueType.ERROR_PRONE, # Forbid duplicate items in hashes. + "WPS418": IssueType.ERROR_PRONE, # Forbid exceptions inherited from BaseException. + "WPS419": IssueType.ERROR_PRONE, # Forbid multiple returning paths with try / except case. + "WPS424": IssueType.ERROR_PRONE, # Forbid BaseException exception. + "WPS426": IssueType.ERROR_PRONE, # Forbid lambda inside loops. + "WPS432": IssueType.CODE_STYLE, # Forbid magic numbers. + "WPS433": IssueType.CODE_STYLE, # Forbid imports nested in functions. + "WPS439": IssueType.ERROR_PRONE, # Forbid Unicode escape sequences in binary strings. + "WPS440": IssueType.ERROR_PRONE, # Forbid overlapping local and block variables. + "WPS441": IssueType.ERROR_PRONE, # Forbid control variables after the block body. + "WPS442": IssueType.ERROR_PRONE, # Forbid shadowing variables from outer scopes. + "WPS443": IssueType.ERROR_PRONE, # Forbid explicit unhashable types of asset items and dict keys. + "WPS445": IssueType.ERROR_PRONE, # Forbid incorrectly named keywords in starred dicts. + "WPS448": IssueType.ERROR_PRONE, # Forbid incorrect order of except. + "WPS449": IssueType.ERROR_PRONE, # Forbid float keys. + "WPS456": IssueType.ERROR_PRONE, # Forbids using float("NaN") construct to generate NaN. + "WPS457": IssueType.ERROR_PRONE, # Forbids use of infinite while True: loops. + "WPS458": IssueType.ERROR_PRONE, # Forbids to import from already imported modules. + + # WPS: Refactoring + "WPS524": IssueType.ERROR_PRONE, # Forbid misrefactored self assignment. + + # WPS: OOP + "WPS601": IssueType.ERROR_PRONE, # Forbid shadowing class level attributes with instance level attributes. + "WPS613": IssueType.ERROR_PRONE, # Forbid super() with incorrect method or property access. + "WPS614": IssueType.ERROR_PRONE, # Forbids descriptors in regular functions. } CODE_PREFIX_TO_ISSUE_TYPE: Dict[str, IssueType] = { @@ -30,4 +96,11 @@ 'F': IssueType.BEST_PRACTICES, # standard flake8 'C': IssueType.BEST_PRACTICES, # flake8-comprehensions 'SC': IssueType.BEST_PRACTICES, # flake8-spellcheck + + 'WPS1': IssueType.CODE_STYLE, # WPS type: Naming + 'WPS2': IssueType.COMPLEXITY, # WPS type: Complexity + 'WPS3': IssueType.BEST_PRACTICES, # WPS type: Consistency + 'WPS4': IssueType.BEST_PRACTICES, # WPS type: Best practices + 'WPS5': IssueType.BEST_PRACTICES, # WPS type: Refactoring + 'WPS6': IssueType.BEST_PRACTICES, # WPS type: OOP } diff --git a/test/python/inspectors/test_flake8_inspector.py b/test/python/inspectors/test_flake8_inspector.py index 7de27bc3..3b21c051 100644 --- a/test/python/inspectors/test_flake8_inspector.py +++ b/test/python/inspectors/test_flake8_inspector.py @@ -10,16 +10,16 @@ FILE_NAMES_AND_N_ISSUES = [ ('case0_spaces.py', 5), ('case1_simple_valid_program.py', 0), - ('case2_boolean_expressions.py', 1), - ('case3_redefining_builtin.py', 1), + ('case2_boolean_expressions.py', 8), + ('case3_redefining_builtin.py', 2), ('case4_naming.py', 10), ('case5_returns.py', 1), ('case6_unused_variables.py', 3), ('case8_good_class.py', 0), ('case7_empty_lines.py', 0), ('case10_unused_variable_in_loop.py', 1), - ('case13_complex_logic.py', 3), - ('case13_complex_logic_2.py', 1), + ('case13_complex_logic.py', 12), + ('case13_complex_logic_2.py', 3), ('case11_redundant_parentheses.py', 0), ('case14_returns_errors.py', 4), ('case16_comments.py', 0), @@ -47,17 +47,20 @@ def test_file_with_issues(file_name: str, n_issues: int): ('case0_spaces.py', IssuesTestInfo(n_code_style=5)), ('case1_simple_valid_program.py', IssuesTestInfo()), ('case2_boolean_expressions.py', IssuesTestInfo(n_code_style=1, - n_cc=8)), - ('case3_redefining_builtin.py', IssuesTestInfo(n_error_prone=1)), + n_best_practices=4, + n_error_prone=1, + n_cc=8, + n_other_complexity=2)), + ('case3_redefining_builtin.py', IssuesTestInfo(n_error_prone=2)), ('case4_naming.py', IssuesTestInfo(n_code_style=7, n_best_practices=3, n_cc=5)), ('case6_unused_variables.py', IssuesTestInfo(n_best_practices=3, n_cc=1)), ('case8_good_class.py', IssuesTestInfo(n_cc=1)), - ('case7_empty_lines.py', IssuesTestInfo(n_cc=4)), + ('case7_empty_lines.py', IssuesTestInfo(n_cc=5)), ('case10_unused_variable_in_loop.py', IssuesTestInfo(n_best_practices=1, n_cc=1)), - ('case13_complex_logic.py', IssuesTestInfo(n_cc=6)), - ('case13_complex_logic_2.py', IssuesTestInfo(n_cc=2)), + ('case13_complex_logic.py', IssuesTestInfo(n_cc=5, n_other_complexity=10)), + ('case13_complex_logic_2.py', IssuesTestInfo(n_cc=2, n_other_complexity=2)), ('case14_returns_errors.py', IssuesTestInfo(n_best_practices=1, n_error_prone=3, n_cc=4)), diff --git a/test/python/inspectors/test_pylint_inspector.py b/test/python/inspectors/test_pylint_inspector.py index 1ee00735..55ce551a 100644 --- a/test/python/inspectors/test_pylint_inspector.py +++ b/test/python/inspectors/test_pylint_inspector.py @@ -9,7 +9,7 @@ ('case0_spaces.py', 3), ('case1_simple_valid_program.py', 0), ('case2_boolean_expressions.py', 3), - ('case3_redefining_builtin.py', 1), + ('case3_redefining_builtin.py', 2), ('case4_naming.py', 3), ('case5_returns.py', 1), ('case6_unused_variables.py', 4), diff --git a/test/resources/inspectors/python/case13_complex_logic.py b/test/resources/inspectors/python/case13_complex_logic.py index 60718a89..3208a9bf 100644 --- a/test/resources/inspectors/python/case13_complex_logic.py +++ b/test/resources/inspectors/python/case13_complex_logic.py @@ -8,16 +8,22 @@ def max_of_three(a, b, c): return a +FEW_UNITS_NUMBER = 9 +PACK_UNITS_NUMBER = 49 +HORDE_UNITS_NUMBER = 499 +SWARM_UNITS_NUMBER = 999 + + def army_of_units(count): if count < 1: print("no army") - elif count <= 9: + elif count <= FEW_UNITS_NUMBER: print('few') - elif count <= 49: + elif count <= PACK_UNITS_NUMBER: print('pack') - elif count <= 499: + elif count <= HORDE_UNITS_NUMBER: print("horde") - elif count <= 999: + elif count <= SWARM_UNITS_NUMBER: print('swarm') else: print('legion') @@ -65,60 +71,38 @@ def determine_strange_quark(spin, charge): print("Higgs boson Boson") +SHEEP_PRICE = 6769 +COW_PRICE = 3848 +PIG_PRICE = 1296 +GOAT_PRICE = 678 +DOG_PRICE = 137 +CHICKEN_PRICE = 23 + + def buy_animal(money): - if money >= 6769: - print(str(money // 6769) + " sheep") - elif money >= 3848: + if money >= SHEEP_PRICE: + number_of_sheep = money // SHEEP_PRICE + print(f"{number_of_sheep} sheep") + elif money >= COW_PRICE: print("1 cow") - elif money >= 1296: - if money // 1296 == 1: + elif money >= PIG_PRICE: + if money // PIG_PRICE == 1: print("1 pig") else: print("2 pigs") - elif money >= 678: + elif money >= GOAT_PRICE: print("1 goat") - elif money >= 137: - if money // 137 == 1: + elif money >= DOG_PRICE: + if money // DOG_PRICE == 1: print("1 dog") else: - print(str(money // 137) + " dogs") - elif money >= 23: - if money // 23 == 1: + number_of_dogs = money // DOG_PRICE + print(f"{number_of_dogs} dogs") + elif money >= CHICKEN_PRICE: + if money // CHICKEN_PRICE == 1: print("1 chicken") else: - print(str(money // 23) + " chickens") + number_of_chickens = money // CHICKEN_PRICE + print(f"{number_of_chickens} chickens") else: print("None") - - -def fun_with_complex_logic(a, b, c): - d = 0 - if a > 10: - d = 30 - elif a < 100: - d = 50 - elif a == 300 and b == 40: - for i in range(9): - a += i - elif a == 200: - if b > 300 and c < 50: - d = 400 - else: - d = 800 - elif a == 2400: - if b > 500 and c < 50: - d = 400 - else: - d = 800 - elif a == 1000: - if b == 900: - if c == 1000: - d = 10000 - else: - d = 900 - elif c == 300: - d = 1000 - elif a + b == 400: - d = 400 - print(d) - return d diff --git a/test/resources/inspectors/python/case13_complex_logic_2.py b/test/resources/inspectors/python/case13_complex_logic_2.py index 1eaf13dc..8f670bc1 100644 --- a/test/resources/inspectors/python/case13_complex_logic_2.py +++ b/test/resources/inspectors/python/case13_complex_logic_2.py @@ -1,10 +1,14 @@ elements = list(input('Enter cells: ')) y = 0 o = 0 + +CROSS_SYMBOL = 'X' +NOUGHT_SYMBOL = 'O' + for x in elements: - if x == 'X': + if x == CROSS_SYMBOL: y = y + 1 - elif x == 'O': + elif x == NOUGHT_SYMBOL: o = o + 1 odds = abs(y - o) @@ -22,8 +26,8 @@ full_field = [up_row, up_col, mid_row, mid_col, down_row, down_col, diagonal_1, diagonal_2] -x_win = ['X', 'X', 'X'] -o_win = ['O', 'O', 'O'] +x_win = [CROSS_SYMBOL, CROSS_SYMBOL, CROSS_SYMBOL] +o_win = [NOUGHT_SYMBOL, NOUGHT_SYMBOL, NOUGHT_SYMBOL] field = f""" --------- @@ -35,10 +39,10 @@ if odds < 2: if x_win in full_field and o_win not in full_field: print(field) - print('X wins') + print(f'{CROSS_SYMBOL} wins') elif o_win in full_field and x_win not in full_field: print(field) - print('O wins') + print(f'{NOUGHT_SYMBOL} wins') elif o_win in full_field and x_win in full_field: print(field) print('Impossible') diff --git a/test/resources/inspectors/python/case3_redefining_builtin.py b/test/resources/inspectors/python/case3_redefining_builtin.py index 81d7021b..ddcca3c4 100644 --- a/test/resources/inspectors/python/case3_redefining_builtin.py +++ b/test/resources/inspectors/python/case3_redefining_builtin.py @@ -1,4 +1,7 @@ a = int(input()) b = int(input()) -list = list(filter(lambda x: x % 3 == 0, range(a, b + 1))) + +range = range(a, b + 1) + +list = list(filter(lambda x: x % 3 == 0, range)) print(sum(list) / len(list)) diff --git a/test/resources/inspectors/python/case4_naming.py b/test/resources/inspectors/python/case4_naming.py index 7d4664e8..522ae0eb 100644 --- a/test/resources/inspectors/python/case4_naming.py +++ b/test/resources/inspectors/python/case4_naming.py @@ -13,7 +13,7 @@ def myFun(self): print('hello 1') def my_fun(self, QQ): - print('hello 2' + QQ) + print('hello 2 {}'.format(QQ)) @classmethod def test_fun(first): diff --git a/test/resources/inspectors/python/case7_empty_lines.py b/test/resources/inspectors/python/case7_empty_lines.py index 81b73b98..25760099 100644 --- a/test/resources/inspectors/python/case7_empty_lines.py +++ b/test/resources/inspectors/python/case7_empty_lines.py @@ -17,6 +17,7 @@ def minus_age(self, value): self.age -= value class AnotherClass: - pass + def do_something(self): + print(1) -print(10 + 20) +print(10) From f3d42f31fe7a9bdbe38ada1fc207c457078bb662 Mon Sep 17 00:00:00 2001 From: Vlasov Ilya <55441714+GirZ0n@users.noreply.github.com> Date: Mon, 5 Apr 2021 16:47:23 +0500 Subject: [PATCH 02/36] New flake8 plugins support (#18) Added support for flake8-broken-line, flake8-string-format, flake8-commas (they are WPS dependencies). Added support for cohesion. --- .github/workflows/build.yml | 2 +- requirements.txt | 4 + src/python/review/inspectors/flake8/.flake8 | 11 ++ src/python/review/inspectors/flake8/flake8.py | 41 ++++- .../review/inspectors/flake8/issue_types.py | 12 ++ .../intellij/issue_types/__init__.py | 22 ++- src/python/review/inspectors/issue.py | 1 + .../inspectors/parsers/checkstyle_parser.py | 22 ++- .../inspectors/springlint/springlint.py | 29 +++- src/python/review/inspectors/tips.py | 66 +++++--- src/python/review/quality/evaluate_quality.py | 46 ++++-- src/python/review/quality/model.py | 9 +- .../review/quality/rules/cohesion_scoring.py | 71 ++++++++ .../review/reviewers/utils/code_statistics.py | 8 +- .../review/reviewers/utils/issues_filter.py | 6 +- src/python/review/run_tool.py | 9 +- test/python/inspectors/conftest.py | 4 +- .../inspectors/test_flake8_inspector.py | 30 +++- .../inspectors/test_pylint_inspector.py | 9 +- test/python/inspectors/test_python_ast.py | 9 +- .../inspectors/python/case31_line_break.py | 54 +++++++ .../inspectors/python/case32_string_format.py | 143 ++++++++++++++++ .../inspectors/python/case33_commas.py | 153 ++++++++++++++++++ .../inspectors/python/case34_cohesion.py | 28 ++++ whitelist.txt | 2 + 25 files changed, 707 insertions(+), 84 deletions(-) create mode 100644 src/python/review/quality/rules/cohesion_scoring.py create mode 100644 test/resources/inspectors/python/case31_line_break.py create mode 100644 test/resources/inspectors/python/case32_string_format.py create mode 100644 test/resources/inspectors/python/case33_commas.py create mode 100644 test/resources/inspectors/python/case34_cohesion.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c177f809..50706799 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,7 +27,7 @@ jobs: # stop the build if there are Python syntax errors or undefined names flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics --exclude=.git,__pycache__,docs/source/conf.py,old,build,dist,venv,test/resources,.eggs,review.egg-info,.pytest_cache,node_modules # TODO: change max-complexity into 10 after refactoring - flake8 . --count --max-complexity=11 --max-line-length=120 --max-doc-length=120 --ignore=I201,I202,I101,I100,R504,A003,E800,SC200,SC100,E402,W503,WPS --statistics --exclude=.git,__pycache__,docs/source/conf.py,old,build,dist,venv,test/resources,.eggs,review.egg-info,.pytest_cache,node_modules + flake8 . --count --max-complexity=11 --max-line-length=120 --max-doc-length=120 --ignore=I201,I202,I101,I100,R504,A003,E800,SC200,SC100,E402,W503,WPS,C812,H601 --statistics --exclude=.git,__pycache__,docs/source/conf.py,old,build,dist,venv,test/resources,.eggs,review.egg-info,.pytest_cache,node_modules - name: Set up Eslint run: | npm install eslint --save-dev diff --git a/requirements.txt b/requirements.txt index caa6e2e2..e13a08f4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,6 +18,10 @@ flake8-spellcheck==0.14.0 mccabe==0.6.1 pep8-naming==0.11.1 wps-light==0.15.2 +flake8-broken-line==0.3.0 +flake8-string-format==0.3.0 +flake8-commas==2.0.0 +cohesion==1.0.0 # extra libraries and frameworks django==3.0.8 diff --git a/src/python/review/inspectors/flake8/.flake8 b/src/python/review/inspectors/flake8/.flake8 index 0ad30a62..6cb0d0cf 100644 --- a/src/python/review/inspectors/flake8/.flake8 +++ b/src/python/review/inspectors/flake8/.flake8 @@ -41,3 +41,14 @@ ignore=W291, # trailing whitespaces WPS527, # Require tuples as arguments for frozenset. # WPS: OOP WPS602, # Forbid @staticmethod decorator. + # flake8-string-format + P101, # format string does contain unindexed parameters + P102, # docstring does contain unindexed parameters + P103, # other string does contain unindexed parameters + F522, # unused named arguments. TODO: Collision with "P302" + F523, # unused positional arguments. TODO: Collision with "P301" + F524, # missing argument. TODO: Collision with "P201" and "P202" + F525, # mixing automatic and manual numbering. TODO: Collision with "P205" + # flake8-commas + C814, # missing trailing comma in Python + \ No newline at end of file diff --git a/src/python/review/inspectors/flake8/flake8.py b/src/python/review/inspectors/flake8/flake8.py index 9784b8ab..c7318d0b 100644 --- a/src/python/review/inspectors/flake8/flake8.py +++ b/src/python/review/inspectors/flake8/flake8.py @@ -1,4 +1,5 @@ import logging +import math import re from pathlib import Path from typing import List @@ -7,7 +8,14 @@ from src.python.review.inspectors.base_inspector import BaseInspector from src.python.review.inspectors.flake8.issue_types import CODE_PREFIX_TO_ISSUE_TYPE, CODE_TO_ISSUE_TYPE from src.python.review.inspectors.inspector_type import InspectorType -from src.python.review.inspectors.issue import BaseIssue, CodeIssue, CyclomaticComplexityIssue, IssueType, IssueData +from src.python.review.inspectors.issue import ( + BaseIssue, + CodeIssue, + CyclomaticComplexityIssue, + IssueType, + IssueData, + CohesionIssue, +) from src.python.review.inspectors.tips import get_cyclomatic_complexity_tip logger = logging.getLogger(__name__) @@ -27,6 +35,7 @@ def inspect(cls, path: Path, config: dict) -> List[BaseIssue]: f'--format={FORMAT}', f'--config={PATH_FLAKE8_CONFIG}', '--max-complexity', '0', + '--cohesion-below', '100', path, ] output = run_in_subprocess(command) @@ -36,12 +45,14 @@ def inspect(cls, path: Path, config: dict) -> List[BaseIssue]: def parse(cls, output: str) -> List[BaseIssue]: row_re = re.compile(r'^(.*):(\d+):(\d+):([A-Z]+\d{3}):(.*)$', re.M) cc_description_re = re.compile(r"'(.+)' is too complex \((\d+)\)") + cohesion_description_re = re.compile(r"class has low \((\d*\.?\d*)%\) cohesion") issues: List[BaseIssue] = [] for groups in row_re.findall(output): description = groups[4] origin_class = groups[3] cc_match = cc_description_re.match(description) + cohesion_match = cohesion_description_re.match(description) file_path = Path(groups[0]) line_no = int(groups[1]) @@ -51,15 +62,20 @@ def parse(cls, output: str) -> List[BaseIssue]: line_number=line_no, column_number=column_number, origin_class=origin_class) - if cc_match is not None: - issue_data['description'] = get_cyclomatic_complexity_tip() - issue_data['cc_value'] = int(cc_match.groups()[1]) - issue_data['type'] = IssueType.CYCLOMATIC_COMPLEXITY + if cc_match is not None: # mccabe: cyclomatic complexity + issue_data[IssueData.DESCRIPTION.value] = get_cyclomatic_complexity_tip() + issue_data[IssueData.CYCLOMATIC_COMPLEXITY.value] = int(cc_match.groups()[1]) + issue_data[IssueData.ISSUE_TYPE.value] = IssueType.CYCLOMATIC_COMPLEXITY issues.append(CyclomaticComplexityIssue(**issue_data)) + elif cohesion_match is not None: # flake8-cohesion + issue_data[IssueData.DESCRIPTION.value] = description # TODO: Add tip + issue_data[IssueData.COHESION_LACK.value] = cls.__get_cohesion_lack(float(cohesion_match.group(1))) + issue_data[IssueData.ISSUE_TYPE.value] = IssueType.COHESION + issues.append(CohesionIssue(**issue_data)) else: issue_type = cls.choose_issue_type(origin_class) - issue_data['type'] = issue_type - issue_data['description'] = description + issue_data[IssueData.ISSUE_TYPE.value] = issue_type + issue_data[IssueData.DESCRIPTION.value] = description issues.append(CodeIssue(**issue_data)) return issues @@ -82,3 +98,14 @@ def choose_issue_type(code: str) -> IssueType: return IssueType.BEST_PRACTICES return issue_type + + @staticmethod + def __get_cohesion_lack(cohesion_percentage: float) -> int: + """ + Converts cohesion percentage to lack of cohesion. + Calculated by the formula: floor(100 - cohesion_percentage). + + :param cohesion_percentage: cohesion set as a percentage. + :return: lack of cohesion + """ + return math.floor(100 - cohesion_percentage) diff --git a/src/python/review/inspectors/flake8/issue_types.py b/src/python/review/inspectors/flake8/issue_types.py index 749d708b..26d5d923 100644 --- a/src/python/review/inspectors/flake8/issue_types.py +++ b/src/python/review/inspectors/flake8/issue_types.py @@ -16,6 +16,17 @@ # builtin naming 'A003': IssueType.BEST_PRACTICES, + # flake8-broken-line + 'N400': IssueType.CODE_STYLE, + + # flake8-commas + "C812": IssueType.CODE_STYLE, + "C813": IssueType.CODE_STYLE, + "C815": IssueType.CODE_STYLE, + "C816": IssueType.CODE_STYLE, + "C818": IssueType.CODE_STYLE, + "C819": IssueType.CODE_STYLE, + # WPS: Naming "WPS117": IssueType.CODE_STYLE, # Forbid naming variables self, cls, or mcs. "WPS125": IssueType.ERROR_PRONE, # Forbid variable or module names which shadow builtin names. @@ -87,6 +98,7 @@ 'B': IssueType.ERROR_PRONE, # flake8-bugbear 'A': IssueType.ERROR_PRONE, # flake8-builtins 'R': IssueType.ERROR_PRONE, # flake8-return + 'P': IssueType.ERROR_PRONE, # flake8-format-string 'E': IssueType.CODE_STYLE, # standard flake8 'W': IssueType.CODE_STYLE, # standard flake8 diff --git a/src/python/review/inspectors/intellij/issue_types/__init__.py b/src/python/review/inspectors/intellij/issue_types/__init__.py index c6f21127..3482d8b4 100644 --- a/src/python/review/inspectors/intellij/issue_types/__init__.py +++ b/src/python/review/inspectors/intellij/issue_types/__init__.py @@ -1,15 +1,21 @@ from typing import Dict -from src.python.review import IssueType -from .java import ISSUE_CLASS_TO_ISSUE_TYPE as \ - JAVA_ISSUE_CLASS_TO_ISSUE_TYPE -from .kotlin import ISSUE_CLASS_TO_ISSUE_TYPE as \ - KOTLIN_ISSUE_CLASS_TO_ISSUE_TYPE -from .python import ISSUE_CLASS_TO_ISSUE_TYPE as \ - PYTHON_ISSUE_CLASS_TO_ISSUE_TYPE +from src.python.review.inspectors.issue import IssueType + +from src.python.review.inspectors.intellij.issue_types.java import ( + ISSUE_CLASS_TO_ISSUE_TYPE as JAVA_ISSUE_CLASS_TO_ISSUE_TYPE, +) + +from src.python.review.inspectors.intellij.issue_types.kotlin import ( + ISSUE_CLASS_TO_ISSUE_TYPE as KOTLIN_ISSUE_CLASS_TO_ISSUE_TYPE, +) + +from src.python.review.inspectors.intellij.issue_types.python import ( + ISSUE_CLASS_TO_ISSUE_TYPE as PYTHON_ISSUE_CLASS_TO_ISSUE_TYPE, +) ISSUE_CLASS_TO_ISSUE_TYPE: Dict[str, IssueType] = { **JAVA_ISSUE_CLASS_TO_ISSUE_TYPE, **PYTHON_ISSUE_CLASS_TO_ISSUE_TYPE, - **KOTLIN_ISSUE_CLASS_TO_ISSUE_TYPE + **KOTLIN_ISSUE_CLASS_TO_ISSUE_TYPE, } diff --git a/src/python/review/inspectors/issue.py b/src/python/review/inspectors/issue.py index 14f0bebb..e4a21798 100644 --- a/src/python/review/inspectors/issue.py +++ b/src/python/review/inspectors/issue.py @@ -45,6 +45,7 @@ class IssueData(Enum): FUNCTION_LEN = 'func_len' BOOL_EXPR_LEN = 'bool_expr_len' CYCLOMATIC_COMPLEXITY = 'cc_value' + COHESION_LACK = 'cohesion_lack' @classmethod def get_base_issue_data_dict(cls, diff --git a/src/python/review/inspectors/parsers/checkstyle_parser.py b/src/python/review/inspectors/parsers/checkstyle_parser.py index 9cf75acb..cae3593f 100644 --- a/src/python/review/inspectors/parsers/checkstyle_parser.py +++ b/src/python/review/inspectors/parsers/checkstyle_parser.py @@ -6,10 +6,24 @@ from src.python.review.common.file_system import get_content_from_file from src.python.review.inspectors.inspector_type import InspectorType -from src.python.review.inspectors.issue import BaseIssue, BoolExprLenIssue, CodeIssue, CyclomaticComplexityIssue, \ - FuncLenIssue, IssueType, LineLenIssue, IssueData -from src.python.review.inspectors.tips import get_bool_expr_len_tip, get_cyclomatic_complexity_tip, get_func_len_tip, \ - get_line_len_tip + +from src.python.review.inspectors.issue import ( + BaseIssue, + BoolExprLenIssue, + CodeIssue, + CyclomaticComplexityIssue, + FuncLenIssue, + IssueType, + LineLenIssue, + IssueData, +) + +from src.python.review.inspectors.tips import ( + get_bool_expr_len_tip, + get_cyclomatic_complexity_tip, + get_func_len_tip, + get_line_len_tip, +) logger = logging.getLogger(__name__) diff --git a/src/python/review/inspectors/springlint/springlint.py b/src/python/review/inspectors/springlint/springlint.py index 18f9d6b0..14ce8c89 100644 --- a/src/python/review/inspectors/springlint/springlint.py +++ b/src/python/review/inspectors/springlint/springlint.py @@ -9,11 +9,30 @@ from src.python.review.common.subprocess_runner import run_in_subprocess from src.python.review.inspectors.base_inspector import BaseInspector from src.python.review.inspectors.inspector_type import InspectorType -from src.python.review.inspectors.issue import BaseIssue, ChildrenNumberIssue, ClassResponseIssue, CodeIssue, \ - CohesionIssue, \ - CouplingIssue, InheritanceIssue, IssueType, MethodNumberIssue, WeightedMethodIssue, IssueData -from src.python.review.inspectors.tips import get_child_number_tip, get_class_coupling_tip, get_class_response_tip, \ - get_cohesion_tip, get_inheritance_depth_tip, get_method_number_tip, get_weighted_method_tip + +from src.python.review.inspectors.issue import ( + BaseIssue, + ChildrenNumberIssue, + ClassResponseIssue, + CodeIssue, + CohesionIssue, + CouplingIssue, + InheritanceIssue, + IssueType, + MethodNumberIssue, + WeightedMethodIssue, + IssueData, +) + +from src.python.review.inspectors.tips import ( + get_child_number_tip, + get_class_coupling_tip, + get_class_response_tip, + get_cohesion_tip, + get_inheritance_depth_tip, + get_method_number_tip, + get_weighted_method_tip, +) PATH_TOOLS_SPRINGLINT_FILES = Path(__file__).parent / 'files' PATH_SPRINGLINT_JAR = PATH_TOOLS_SPRINGLINT_FILES / 'springlint-0.6.jar' diff --git a/src/python/review/inspectors/tips.py b/src/python/review/inspectors/tips.py index c1d3fba1..5211b569 100644 --- a/src/python/review/inspectors/tips.py +++ b/src/python/review/inspectors/tips.py @@ -1,24 +1,32 @@ def get_bool_expr_len_tip() -> str: - return 'Too long boolean expression. ' \ - 'Try to split it into smaller expressions.' + return ( + 'Too long boolean expression. ' + 'Try to split it into smaller expressions.' + ) def get_func_len_tip() -> str: - return 'Too long function. ' \ - 'Try to split it into smaller functions / methods.' \ - 'It will make your code easy to understand and less error prone.' + return ( + 'Too long function. ' + 'Try to split it into smaller functions / methods. ' + 'It will make your code easy to understand and less error prone.' + ) def get_line_len_tip() -> str: - return 'Too long line. ' \ - 'Try to split it into smaller lines.' \ - 'It will make your code easy to understand.' + return ( + 'Too long line. ' + 'Try to split it into smaller lines. ' + 'It will make your code easy to understand.' + ) def get_cyclomatic_complexity_tip() -> str: - return 'Too complex function. You can figure out how to simplify this code ' \ - 'or split it into a set of small functions / methods. ' \ - 'It will make your code easy to understand and less error prone.' + return ( + 'Too complex function. You can figure out how to simplify this code ' + 'or split it into a set of small functions / methods. ' + 'It will make your code easy to understand and less error prone.' + ) def add_complexity_tip(description: str) -> str: @@ -31,8 +39,10 @@ def add_complexity_tip(description: str) -> str: def get_inheritance_depth_tip() -> str: - return 'Too deep inheritance tree is complicated to understand. ' \ - 'Try to reduce it (maybe you should use a composition instead).' + return ( + 'Too deep inheritance tree is complicated to understand. ' + 'Try to reduce it (maybe you should use a composition instead).' + ) # This issue will not be reported at this version @@ -41,14 +51,18 @@ def get_child_number_tip() -> str: def get_weighted_method_tip() -> str: - return 'The number of methods and their complexity may be too hight. ' \ - 'It may require too much time and effort to develop and maintain the class.' + return ( + 'The number of methods and their complexity may be too hight. ' + 'It may require too much time and effort to develop and maintain the class.' + ) def get_class_coupling_tip() -> str: - return 'The class seems to depend on too many other classes. ' \ - 'Increased coupling increases interclass dependencies, ' \ - 'making the code less modular and less suitable for reuse and testing.' + return ( + 'The class seems to depend on too many other classes. ' + 'Increased coupling increases interclass dependencies, ' + 'making the code less modular and less suitable for reuse and testing.' + ) # This issue will not be reported at this version @@ -57,12 +71,16 @@ def get_cohesion_tip() -> str: def get_class_response_tip() -> str: - return 'The class have too many methods that can potentially ' \ - 'be executed in response to a single message received by an object of that class. ' \ - 'The larger the number of methods that can be invoked from a class, ' \ - 'the greater the complexity of the class' + return ( + 'The class have too many methods that can potentially ' + 'be executed in response to a single message received by an object of that class. ' + 'The larger the number of methods that can be invoked from a class, ' + 'the greater the complexity of the class' + ) def get_method_number_tip() -> str: - return 'The file has too many methods inside and is complicated to understand. ' \ - 'Consider its decomposition to smaller classes.' + return ( + 'The file has too many methods inside and is complicated to understand. ' + 'Consider its decomposition to smaller classes.' + ) diff --git a/src/python/review/quality/evaluate_quality.py b/src/python/review/quality/evaluate_quality.py index 3d78e392..d7ee0211 100644 --- a/src/python/review/quality/evaluate_quality.py +++ b/src/python/review/quality/evaluate_quality.py @@ -4,26 +4,43 @@ from src.python.review.inspectors.issue import IssueType from src.python.review.quality.model import Quality, Rule from src.python.review.quality.rules.best_practices_scoring import ( - BestPracticesRule, LANGUAGE_TO_BEST_PRACTICES_RULE_CONFIG + BestPracticesRule, + LANGUAGE_TO_BEST_PRACTICES_RULE_CONFIG, +) +from src.python.review.quality.rules.boolean_length_scoring import ( + BooleanExpressionRule, + LANGUAGE_TO_BOOLEAN_EXPRESSION_RULE_CONFIG, ) -from src.python.review.quality.rules.boolean_length_scoring import BooleanExpressionRule, \ - LANGUAGE_TO_BOOLEAN_EXPRESSION_RULE_CONFIG from src.python.review.quality.rules.class_response_scoring import LANGUAGE_TO_RESPONSE_RULE_CONFIG, ResponseRule from src.python.review.quality.rules.code_style_scoring import CodeStyleRule, LANGUAGE_TO_CODE_STYLE_RULE_CONFIG from src.python.review.quality.rules.coupling_scoring import CouplingRule, LANGUAGE_TO_COUPLING_RULE_CONFIG -from src.python.review.quality.rules.cyclomatic_complexity_scoring import CyclomaticComplexityRule, \ - LANGUAGE_TO_CYCLOMATIC_COMPLEXITY_RULE_CONFIG +from src.python.review.quality.rules.cyclomatic_complexity_scoring import ( + CyclomaticComplexityRule, + LANGUAGE_TO_CYCLOMATIC_COMPLEXITY_RULE_CONFIG, +) from src.python.review.quality.rules.error_prone_scoring import ErrorProneRule, LANGUAGE_TO_ERROR_PRONE_RULE_CONFIG -from src.python.review.quality.rules.function_length_scoring import FunctionLengthRule, \ - LANGUAGE_TO_FUNCTION_LENGTH_RULE_CONFIG -from src.python.review.quality.rules.inheritance_depth_scoring import InheritanceDepthRule, \ - LANGUAGE_TO_INHERITANCE_DEPTH_RULE_CONFIG +from src.python.review.quality.rules.function_length_scoring import ( + FunctionLengthRule, + LANGUAGE_TO_FUNCTION_LENGTH_RULE_CONFIG, +) +from src.python.review.quality.rules.inheritance_depth_scoring import ( + InheritanceDepthRule, + LANGUAGE_TO_INHERITANCE_DEPTH_RULE_CONFIG, +) from src.python.review.quality.rules.line_len_scoring import LANGUAGE_TO_LINE_LENGTH_RULE_CONFIG, LineLengthRule -from src.python.review.quality.rules.method_number_scoring import LANGUAGE_TO_METHOD_NUMBER_RULE_CONFIG, \ - MethodNumberRule -from src.python.review.quality.rules.weighted_methods_scoring import LANGUAGE_TO_WEIGHTED_METHODS_RULE_CONFIG, \ - WeightedMethodsRule +from src.python.review.quality.rules.method_number_scoring import ( + LANGUAGE_TO_METHOD_NUMBER_RULE_CONFIG, + MethodNumberRule, +) +from src.python.review.quality.rules.weighted_methods_scoring import ( + LANGUAGE_TO_WEIGHTED_METHODS_RULE_CONFIG, + WeightedMethodsRule, +) from src.python.review.reviewers.utils.code_statistics import CodeStatistics +from src.python.review.quality.rules.cohesion_scoring import ( + LANGUAGE_TO_COHESION_RULE_CONFIG, + CohesionRule, +) def __get_available_rules(language: Language) -> List[Rule]: @@ -37,7 +54,8 @@ def __get_available_rules(language: Language) -> List[Rule]: MethodNumberRule(LANGUAGE_TO_METHOD_NUMBER_RULE_CONFIG[language]), CouplingRule(LANGUAGE_TO_COUPLING_RULE_CONFIG[language]), ResponseRule(LANGUAGE_TO_RESPONSE_RULE_CONFIG[language]), - WeightedMethodsRule(LANGUAGE_TO_WEIGHTED_METHODS_RULE_CONFIG[language]) + WeightedMethodsRule(LANGUAGE_TO_WEIGHTED_METHODS_RULE_CONFIG[language]), + CohesionRule(LANGUAGE_TO_COHESION_RULE_CONFIG[language]), ] diff --git a/src/python/review/quality/model.py b/src/python/review/quality/model.py index ae71f211..741c761b 100644 --- a/src/python/review/quality/model.py +++ b/src/python/review/quality/model.py @@ -1,4 +1,5 @@ import abc +import textwrap from enum import Enum, unique from functools import total_ordering from typing import List @@ -80,8 +81,12 @@ def __str__(self): message_deltas_part = '' if self.quality_type != QualityType.EXCELLENT: - message_next_level_part = f'Next level: {self.next_quality_type.value}\n' \ - f'Next level requirements:\n' + message_next_level_part = textwrap.dedent( + f"""\ + Next level: {self.next_quality_type.value} + Next level requirements: + """ + ) for rule in self.next_level_requirements: message_deltas_part += f'{rule.rule_type.value}: {rule.next_level_delta}\n' diff --git a/src/python/review/quality/rules/cohesion_scoring.py b/src/python/review/quality/rules/cohesion_scoring.py new file mode 100644 index 00000000..c73a426b --- /dev/null +++ b/src/python/review/quality/rules/cohesion_scoring.py @@ -0,0 +1,71 @@ +from dataclasses import dataclass +from typing import Optional + +from src.python.review.common.language import Language +from src.python.review.inspectors.issue import IssueType +from src.python.review.quality.model import Rule, QualityType + + +@dataclass +class CohesionRuleConfig: + cohesion_lack_bad: int + cohesion_lack_moderate: int + cohesion_lack_good: int + + +# TODO: Need testing +# Cohesion plugin by default only shows issues where cohesion is less than 50%. +# Therefore cohesion_lack_bad = 50. The other levels are set in steps of 20%. +common_cohesion_rule_config = CohesionRuleConfig( + cohesion_lack_bad=50, + cohesion_lack_moderate=30, + cohesion_lack_good=10, +) + +LANGUAGE_TO_COHESION_RULE_CONFIG = { + Language.JAVA: common_cohesion_rule_config, + Language.PYTHON: common_cohesion_rule_config, + Language.KOTLIN: common_cohesion_rule_config, + Language.JS: common_cohesion_rule_config, +} + + +class CohesionRule(Rule): + def __init__(self, config: CohesionRuleConfig): + self.config = config + self.rule_type = IssueType.COHESION + self.cohesion_lack: Optional[int] = None + + def apply(self, cohesion_lack: int): + self.cohesion_lack = cohesion_lack + if cohesion_lack > self.config.cohesion_lack_bad: + self.quality_type = QualityType.BAD + self.next_level_delta = cohesion_lack - self.config.cohesion_lack_bad + elif cohesion_lack > self.config.cohesion_lack_moderate: + self.quality_type = QualityType.MODERATE + self.next_level_delta = cohesion_lack - self.config.cohesion_lack_moderate + elif cohesion_lack > self.config.cohesion_lack_good: + self.quality_type = QualityType.GOOD + self.next_level_delta = cohesion_lack - self.config.cohesion_lack_good + else: + self.quality_type = QualityType.EXCELLENT + self.next_level_delta = 0 + self.next_level_type = self.__get_next_quality_type() + + def __get_next_quality_type(self) -> QualityType: + if self.quality_type == QualityType.BAD: + return QualityType.MODERATE + elif self.quality_type == QualityType.MODERATE: + return QualityType.GOOD + return QualityType.EXCELLENT + + def merge(self, other: 'CohesionRule') -> 'CohesionRule': + config = CohesionRuleConfig( + min(self.config.cohesion_lack_bad, other.config.cohesion_lack_bad), + min(self.config.cohesion_lack_moderate, other.config.cohesion_lack_moderate), + min(self.config.cohesion_lack_good, other.config.cohesion_lack_good), + ) + result_rule = CohesionRule(config) + result_rule.apply(max(self.cohesion_lack, other.cohesion_lack)) + + return result_rule diff --git a/src/python/review/reviewers/utils/code_statistics.py b/src/python/review/reviewers/utils/code_statistics.py index b0523355..41dc25a2 100644 --- a/src/python/review/reviewers/utils/code_statistics.py +++ b/src/python/review/reviewers/utils/code_statistics.py @@ -16,6 +16,7 @@ class CodeStatistics: method_number: int max_cyclomatic_complexity: int + max_cohesion_lack: int max_func_len: int max_bool_expr_len: int @@ -38,6 +39,7 @@ def issue_type_to_statistics_dict(self) -> Dict[IssueType, int]: IssueType.METHOD_NUMBER: self.method_number, IssueType.CYCLOMATIC_COMPLEXITY: self.max_cyclomatic_complexity, + IssueType.COHESION: self.max_cohesion_lack, IssueType.FUNC_LEN: self.max_func_len, IssueType.BOOL_EXPR_LEN: self.max_bool_expr_len, @@ -45,7 +47,7 @@ def issue_type_to_statistics_dict(self) -> Dict[IssueType, int]: IssueType.INHERITANCE_DEPTH: self.inheritance_depth, IssueType.COUPLING: self.coupling, IssueType.CLASS_RESPONSE: self.class_response, - IssueType.WEIGHTED_METHOD: self.weighted_method_complexities + IssueType.WEIGHTED_METHOD: self.weighted_method_complexities, } @@ -82,6 +84,7 @@ def gather_code_statistics(issues: List[BaseIssue], path: Path) -> CodeStatistic bool_expr_lens = __get_max_measure_by_issue_type(IssueType.BOOL_EXPR_LEN, issues) func_lens = __get_max_measure_by_issue_type(IssueType.FUNC_LEN, issues) cyclomatic_complexities = __get_max_measure_by_issue_type(IssueType.CYCLOMATIC_COMPLEXITY, issues) + cohesion_lacks = __get_max_measure_by_issue_type(IssueType.COHESION, issues) # Actually, we expect only one issue with each of the following metrics. inheritance_depths = __get_max_measure_by_issue_type(IssueType.INHERITANCE_DEPTH, issues) @@ -97,6 +100,7 @@ def gather_code_statistics(issues: List[BaseIssue], path: Path) -> CodeStatistic max_bool_expr_len=bool_expr_lens, max_func_len=func_lens, n_line_len=issue_type_counter[IssueType.LINE_LEN], + max_cohesion_lack=cohesion_lacks, max_cyclomatic_complexity=cyclomatic_complexities, inheritance_depth=inheritance_depths, class_response=class_responses, @@ -104,5 +108,5 @@ def gather_code_statistics(issues: List[BaseIssue], path: Path) -> CodeStatistic weighted_method_complexities=weighted_method_complexities, method_number=method_numbers, total_lines=__get_total_lines(path), - code_style_lines=get_code_style_lines(issues) + code_style_lines=get_code_style_lines(issues), ) diff --git a/src/python/review/reviewers/utils/issues_filter.py b/src/python/review/reviewers/utils/issues_filter.py index 9ac9b0b6..ee9be2a1 100644 --- a/src/python/review/reviewers/utils/issues_filter.py +++ b/src/python/review/reviewers/utils/issues_filter.py @@ -11,6 +11,7 @@ from src.python.review.quality.rules.inheritance_depth_scoring import LANGUAGE_TO_INHERITANCE_DEPTH_RULE_CONFIG from src.python.review.quality.rules.method_number_scoring import LANGUAGE_TO_METHOD_NUMBER_RULE_CONFIG from src.python.review.quality.rules.weighted_methods_scoring import LANGUAGE_TO_WEIGHTED_METHODS_RULE_CONFIG +from src.python.review.quality.rules.cohesion_scoring import LANGUAGE_TO_COHESION_RULE_CONFIG def __get_issue_type_to_low_measure_dict(language: Language) -> Dict[IssueType, int]: @@ -22,7 +23,8 @@ def __get_issue_type_to_low_measure_dict(language: Language) -> Dict[IssueType, IssueType.METHOD_NUMBER: LANGUAGE_TO_METHOD_NUMBER_RULE_CONFIG[language].method_number_good, IssueType.COUPLING: LANGUAGE_TO_COUPLING_RULE_CONFIG[language].coupling_moderate, IssueType.CLASS_RESPONSE: LANGUAGE_TO_RESPONSE_RULE_CONFIG[language].response_good, - IssueType.WEIGHTED_METHOD: LANGUAGE_TO_WEIGHTED_METHODS_RULE_CONFIG[language].weighted_methods_good + IssueType.WEIGHTED_METHOD: LANGUAGE_TO_WEIGHTED_METHODS_RULE_CONFIG[language].weighted_methods_good, + IssueType.COHESION: LANGUAGE_TO_COHESION_RULE_CONFIG[language].cohesion_lack_good, } @@ -38,7 +40,7 @@ def filter_low_measure_issues(issues: List[BaseIssue], issue_type_to_low_measure_dict = __get_issue_type_to_low_measure_dict(language) # Disable this types of issue, requires further investigation. - ignored_issues = [IssueType.COHESION, IssueType.CHILDREN_NUMBER] + ignored_issues = [IssueType.CHILDREN_NUMBER] return list(filter( lambda issue: issue.type not in ignored_issues and __more_than_low_measure(issue, diff --git a/src/python/review/run_tool.py b/src/python/review/run_tool.py index fdbb4b67..d0ba0891 100644 --- a/src/python/review/run_tool.py +++ b/src/python/review/run_tool.py @@ -13,8 +13,13 @@ from src.python.review.application_config import ApplicationConfig, LanguageVersion from src.python.review.inspectors.inspector_type import InspectorType from src.python.review.logging_config import logging_config -from src.python.review.reviewers.perform_review import OutputFormat, PathNotExists, perform_and_print_review, \ - UnsupportedLanguage + +from src.python.review.reviewers.perform_review import ( + OutputFormat, + PathNotExists, + perform_and_print_review, + UnsupportedLanguage, +) logger = logging.getLogger(__name__) diff --git a/test/python/inspectors/conftest.py b/test/python/inspectors/conftest.py index 615f95dc..b2f8773c 100644 --- a/test/python/inspectors/conftest.py +++ b/test/python/inspectors/conftest.py @@ -73,6 +73,7 @@ class IssuesTestInfo: n_cc: int = 0 n_bool_expr_len: int = 0 n_other_complexity: int = 0 + n_cohesion: int = 0 def gather_issues_test_info(issues: List[BaseIssue]) -> IssuesTestInfo: @@ -85,7 +86,8 @@ def gather_issues_test_info(issues: List[BaseIssue]) -> IssuesTestInfo: n_func_len=counter[IssueType.FUNC_LEN], n_cc=counter[IssueType.CYCLOMATIC_COMPLEXITY], n_bool_expr_len=counter[IssueType.BOOL_EXPR_LEN], - n_other_complexity=counter[IssueType.COMPLEXITY] + n_other_complexity=counter[IssueType.COMPLEXITY], + n_cohesion=counter[IssueType.COHESION], ) diff --git a/test/python/inspectors/test_flake8_inspector.py b/test/python/inspectors/test_flake8_inspector.py index 3b21c051..c4750ace 100644 --- a/test/python/inspectors/test_flake8_inspector.py +++ b/test/python/inspectors/test_flake8_inspector.py @@ -1,4 +1,5 @@ import pytest +from textwrap import dedent from src.python.review.common.language import Language from src.python.review.inspectors.flake8.flake8 import Flake8Inspector @@ -12,11 +13,11 @@ ('case1_simple_valid_program.py', 0), ('case2_boolean_expressions.py', 8), ('case3_redefining_builtin.py', 2), - ('case4_naming.py', 10), + ('case4_naming.py', 11), ('case5_returns.py', 1), ('case6_unused_variables.py', 3), ('case8_good_class.py', 0), - ('case7_empty_lines.py', 0), + ('case7_empty_lines.py', 2), ('case10_unused_variable_in_loop.py', 1), ('case13_complex_logic.py', 12), ('case13_complex_logic_2.py', 3), @@ -28,6 +29,10 @@ ('case19_bad_indentation.py', 3), ('case21_imports.py', 2), ('case25_django.py', 0), + ('case31_line_break.py', 11), + ('case32_string_format.py', 34), + ('case33_commas.py', 14), + ('case34_cohesion.py', 1), ] @@ -52,11 +57,11 @@ def test_file_with_issues(file_name: str, n_issues: int): n_cc=8, n_other_complexity=2)), ('case3_redefining_builtin.py', IssuesTestInfo(n_error_prone=2)), - ('case4_naming.py', IssuesTestInfo(n_code_style=7, n_best_practices=3, n_cc=5)), + ('case4_naming.py', IssuesTestInfo(n_code_style=7, n_best_practices=3, n_cc=5, n_cohesion=1)), ('case6_unused_variables.py', IssuesTestInfo(n_best_practices=3, n_cc=1)), - ('case8_good_class.py', IssuesTestInfo(n_cc=1)), - ('case7_empty_lines.py', IssuesTestInfo(n_cc=5)), + ('case8_good_class.py', IssuesTestInfo(n_cc=1, n_cohesion=1)), + ('case7_empty_lines.py', IssuesTestInfo(n_cc=5, n_cohesion=2)), ('case10_unused_variable_in_loop.py', IssuesTestInfo(n_best_practices=1, n_cc=1)), ('case13_complex_logic.py', IssuesTestInfo(n_cc=5, n_other_complexity=10)), @@ -64,6 +69,12 @@ def test_file_with_issues(file_name: str, n_issues: int): ('case14_returns_errors.py', IssuesTestInfo(n_best_practices=1, n_error_prone=3, n_cc=4)), + ('case31_line_break.py', IssuesTestInfo(n_best_practices=1, + n_code_style=10, + n_cc=1)), + ('case32_string_format.py', IssuesTestInfo(n_error_prone=28, n_other_complexity=6)), + ('case33_commas.py', IssuesTestInfo(n_code_style=14, n_cc=4)), + ('case34_cohesion.py', IssuesTestInfo(n_cc=6, n_cohesion=2)), ] @@ -81,9 +92,12 @@ def test_file_with_issues_info(file_name: str, expected_issues_info: IssuesTestI def test_parse(): file_name = 'test.py' - output = ('test.py:1:11:W602:test 1\n' - 'test.py:2:12:E703:test 2\n' - 'test.py:3:13:SC200:test 3') + output = f"""\ + {file_name}:1:11:W602:test 1 + {file_name}:2:12:E703:test 2 + {file_name}:3:13:SC200:test 3 + """ + output = dedent(output) issues = Flake8Inspector.parse(output) diff --git a/test/python/inspectors/test_pylint_inspector.py b/test/python/inspectors/test_pylint_inspector.py index 55ce551a..e142e8aa 100644 --- a/test/python/inspectors/test_pylint_inspector.py +++ b/test/python/inspectors/test_pylint_inspector.py @@ -1,3 +1,5 @@ +import textwrap + import pytest from src.python.review.inspectors.issue import IssueType @@ -46,8 +48,11 @@ def test_file_with_issues(file_name: str, n_issues: int): def test_parse(): file_name = 'test.py' - output = 'test.py:1:11:R0123:test 1\n' \ - 'test.py:2:12:C1444:test 2' + output = f"""\ + {file_name}:1:11:R0123:test 1 + {file_name}:2:12:C1444:test 2 + """ + output = textwrap.dedent(output) issues = PylintInspector.parse(output) diff --git a/test/python/inspectors/test_python_ast.py b/test/python/inspectors/test_python_ast.py index 09192cd3..fed3c542 100644 --- a/test/python/inspectors/test_python_ast.py +++ b/test/python/inspectors/test_python_ast.py @@ -3,8 +3,13 @@ import pytest from src.python.review.inspectors.inspector_type import InspectorType -from src.python.review.inspectors.pyast.python_ast import BoolExpressionLensGatherer, FunctionLensGatherer, \ - PythonAstInspector + +from src.python.review.inspectors.pyast.python_ast import ( + BoolExpressionLensGatherer, + FunctionLensGatherer, + PythonAstInspector, +) + from test.python.inspectors import PYTHON_DATA_FOLDER, PYTHON_AST_DATA_FOLDER from test.python.inspectors.conftest import use_file_metadata diff --git a/test/resources/inspectors/python/case31_line_break.py b/test/resources/inspectors/python/case31_line_break.py new file mode 100644 index 00000000..ba718ea8 --- /dev/null +++ b/test/resources/inspectors/python/case31_line_break.py @@ -0,0 +1,54 @@ +# Wrong +from math import exp, \ + log + +# Correct +from math import ( + sin, + cos, +) + + +# Wrong +def do_something_wrong(x: float, y: float): + if sin(x) == cos(y) \ + and exp(x) == log(y): + print("Do not do that!") + + +print(do_something_wrong(1, 2)) + +# Wrong +wrong_string = "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. " \ + + "Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, " \ + + "nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. " \ + + "Nulla consequat massa quis enim. Donec pede justo, fringilla vel, aliquet nec, " \ + + "vulputate eget, arcu. In enim justo, rhoncus ut, imperdiet a, venenatis vitae, justo. " \ + + "Nullam dictum felis eu pede mollis pretium. Integer tincidunt. Cras dapibus. " \ + + "Vivamus elementum semper nisi. Aenean vulputate eleifend tellus. Aenean leo ligula, " \ + + "porttitor eu, consequat vitae, eleifend ac, enim. Aliquam lorem ante, dapibus in, " \ + + "viverra quis, feugiat a," +print(wrong_string) + +# Correct +correct_string = ("Lorem ipsum dolor sit amet, consectetuer adipiscing elit. " + + "Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis " + + "parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, " + + "pretium quis, sem. Nulla consequat massa quis enim. Donec pede justo, fringilla vel, " + + "aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut, imperdiet a, " + + "venenatis vitae, justo. Nullam dictum felis eu pede mollis pretium. Integer tincidunt. " + + "Cras dapibus. Vivamus elementum semper nisi. Aenean vulputate eleifend tellus. " + + "Aenean leo ligula, porttitor eu, consequat vitae, eleifend ac, enim. " + + "Aliquam lorem ante, dapibus in, viverra quis, feugiat a," + ) +print(correct_string) + +other_correct_string = """ + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec euismod vitae libero ut consequat. + Fusce quis ultrices sem, vitae viverra mi. Praesent fermentum quam ac volutpat condimentum. + Proin mauris orci, molestie vel fermentum vel, consectetur vel metus. Quisque vitae mollis magna. + In hac habitasse platea dictumst. Pellentesque sed diam eget dolor ultricies faucibus id sed quam. + Nam risus erat, varius ut risus a, tincidunt vulputate magna. Etiam lacinia a eros non faucibus. + In facilisis tempor nisl, sit amet feugiat lacus viverra quis. +""" +print(other_correct_string) diff --git a/test/resources/inspectors/python/case32_string_format.py b/test/resources/inspectors/python/case32_string_format.py new file mode 100644 index 00000000..bee24195 --- /dev/null +++ b/test/resources/inspectors/python/case32_string_format.py @@ -0,0 +1,143 @@ +hello_world = "Hello, World!" +hello = "Hello" +world = "World" +some_list = [hello, world] +some_dict = {"hello": hello, "world": world} + +# ----------------------------------------------------------------------------------- + +# Correct +print("{0}!".format(hello_world)) + +# Correct +print("{}!".format(hello_world)) + +# Correct +print("{0}, {1}!".format(hello, world)) + +# Correct +print("{0}, {1}!".format(*some_list)) + +# Correct +print("{1}, {0}!".format(world, hello)) + +# Correct +print("{1}, {0}!".format(*some_list)) + +# Correct +print("{}, {}!".format(hello, world)) + +# Correct +print("{}, {}!".format(*some_list)) + +# Correct +print("{hello}, {world}!".format(hello=hello, world=world)) + +# Correct +print("{hello}, {world}!".format(**some_dict)) + +# ----------------------------------------------------------------------------------- + +# Correct +print(str.format("{0}!", hello_world)) + +# Correct +print(str.format("{}!", hello_world)) + +# Correct +print(str.format("{0}, {1}!", hello, world)) + +# Correct +print(str.format("{0}, {1}!", *some_list)) + +# Correct +print(str.format("{1}, {0}!", world, hello)) + +# Correct +print(str.format("{1}, {0}!", *some_list)) + +# Correct +print(str.format("{}, {}!", hello, world)) + +# Correct +print(str.format("{}, {}!", *some_list)) + +# Correct +print(str.format("{hello}, {world}!", hello=hello, world=world)) + +# Correct +print(str.format("{hello}, {world}!", **some_dict)) + +# ----------------------------------------------------------------------------------- + +# Wrong: P-201 +print("{0}, {1}!".format(hello)) + +# Wrong: P-201 +print("{}, {}!".format(hello)) + +# Wrong: P-201 +print(str.format("{0}, {1}!", hello)) + +# Wrong: P-201 +print(str.format("{}, {}!", hello)) + +# ----------------------------------------------------------------------------------- + +# Wrong: P-202 +print("{hello}, {world}!".format(hello=hello)) + +# Wrong: P-202 +print(str.format("{hello}, {world}!", hello=hello)) + +# ----------------------------------------------------------------------------------- + +# Wrong: P-203 +print("{0}!".format(**some_dict)) + +# Wrong: P-203 +print("{}!".format(**some_dict)) + +# Wrong: P-203 +print(str.format("{0}!", **some_dict)) + +# Wrong: P-203 +print(str.format("{}!", **some_dict)) + +# ----------------------------------------------------------------------------------- + +# Wrong: P-204 +print("{hello_world}!".format(*some_list)) + +# Wrong: P-204 +print(str.format("{hello_world}!", *some_list)) + +# ----------------------------------------------------------------------------------- + +# Wrong: P-205 +print("{}, {0}!".format(hello, world)) + +# Wrong: P-205 +print(str.format("{}, {0}!", hello, world)) + +# ----------------------------------------------------------------------------------- + +# Wrong: P-301 +print("{0}".format(hello_world, hello, world)) + +# Wrong: P-301 +print("{}".format(hello_world, hello, world)) + +# Wrong: P-301 +print(str.format("{0}", hello_world, hello, world)) + +# Wrong: P-301 +print(str.format("{}", hello_world, hello, world)) + +# ----------------------------------------------------------------------------------- + +# Wrong: P-302 +print("{hello_world}".format(hello_world=hello_world, hello=hello, world=world)) + +# Wrong: P-302 +print(str.format("{hello_world}", hello_world=hello_world, hello=hello, world=world)) diff --git a/test/resources/inspectors/python/case33_commas.py b/test/resources/inspectors/python/case33_commas.py new file mode 100644 index 00000000..6af55963 --- /dev/null +++ b/test/resources/inspectors/python/case33_commas.py @@ -0,0 +1,153 @@ +# Wrong C-812 +from math import ( + log +) + +# Correct +from math import ( + sin, +) + +# Wrong C-812 +bad_multiline_dict = { + "first": 1, + "second": 2 +} + +# Correct +good_multiline_dict = { + "first": 1, + "second": 2, +} + +# Wrong C-812 +bad_multiline_list = [ + 1, + 2, + 3 +] + +# Correct +good_multiline_list = [ + 1, + 2, + 3, +] + +# Wrong C-812 +bad_multiline_tuple = ( + 3, + 4 +) + +good_multiline_tuple = ( + 3, + 4, +) + + +# Wrong C-812 +def bad_function( + a, + b +): + return log(a, b) + + +bad_function( + 1, + 2 +) + +bad_function( + a=1, + b=2 +) + + +# Correct +def good_function( + a, + b, +): + return a + sin(b) + + +good_function( + 1, + 2, +) + +good_function( + a=1, + b=2, +) + +# Wrong: C-813 +print( + "Hello", + "World" +) + +# Correct +print( + "Hello", + "World", +) + + +# Wrong: C-816 +def bad_function_with_unpacking( + a, + b, + **kwargs +): + pass + + +# Correct +def good_function_with_unpacking( + a, + b, + **kwargs, +): + pass + + +# Wrong: C-815 +good_function_with_unpacking( + 1, + 2, + **good_multiline_dict +) + +# Correct +good_function_with_unpacking( + 1, + 2, + **good_multiline_dict, +) + +# Wrong: C-818 +bad_comma = 1, + +# Correct +good_comma = (1,) + +# Wrong: C-819 +bad_list = [1, 2, 3, ] + +# Correct: +good_list = [1, 2, 3] + +# Wrong: C-819 +bad_dict = {"1": 1, "2": 2, "3": 3, } + +# Correct: +good_dict = {"1": 1, "2": 2, "3": 3} + +# Wrong: C-819 +bad_tuple = (1, 2, 3,) + +# Correct +good_tuple = (1, 2, 3) diff --git a/test/resources/inspectors/python/case34_cohesion.py b/test/resources/inspectors/python/case34_cohesion.py new file mode 100644 index 00000000..8102675a --- /dev/null +++ b/test/resources/inspectors/python/case34_cohesion.py @@ -0,0 +1,28 @@ +from math import sqrt + + +class BadClass: + def __init__(self, x: int, y: int): + self.x = x + self.y = y + + @staticmethod + def length(x: int, y: int) -> float: + return sqrt(x ** 2 + y ** 2) + + @staticmethod + def dot(self_x: int, self_y: int, other_x: int, other_y: int) -> int: + return self_x * other_x + self_y * other_y + + +class GoodClass(object): + def __init__(self, x: int, y: int): + self.x = x + self.y = y + + @property + def length(self) -> float: + return sqrt(self.dot(self.x, self.y)) + + def dot(self, other_x: int, other_y: int) -> int: + return self.x * other_x + self.y * other_y diff --git a/whitelist.txt b/whitelist.txt index 8f29cf65..c2473e99 100644 --- a/whitelist.txt +++ b/whitelist.txt @@ -61,3 +61,5 @@ lcom noc nom wmc +multiline +sqrt From 4b4799602919dedea70eb1f4b9803a2d004b02f5 Mon Sep 17 00:00:00 2001 From: Ilya Vlasov <55441714+GirZ0n@users.noreply.github.com> Date: Mon, 12 Apr 2021 23:26:16 +0500 Subject: [PATCH 03/36] Radon support (#19) Added new inspector: Radon. Added maintainability index. --- .github/workflows/build.yml | 2 +- README.md | 7 +- requirements.txt | 1 + setup.py | 12 +-- src/python/review/common/file_system.py | 2 +- src/python/review/common/java_compiler.py | 2 +- src/python/review/common/parallel_runner.py | 2 +- src/python/review/common/subprocess_runner.py | 2 +- .../inspectors/checkstyle/checkstyle.py | 4 +- src/python/review/inspectors/common.py | 12 +++ src/python/review/inspectors/detekt/detekt.py | 4 +- src/python/review/inspectors/eslint/eslint.py | 2 +- src/python/review/inspectors/flake8/.flake8 | 3 +- src/python/review/inspectors/flake8/flake8.py | 17 +---- .../review/inspectors/inspector_type.py | 2 + .../review/inspectors/intellij/intellij.py | 8 +- .../inspectors/intellij/issue_types/kotlin.py | 2 +- src/python/review/inspectors/issue.py | 13 +++- src/python/review/inspectors/pmd/pmd.py | 2 +- .../review/inspectors/pyast/python_ast.py | 14 ++-- src/python/review/inspectors/pylint/pylint.py | 6 +- .../review/inspectors/radon/__init__.py | 0 src/python/review/inspectors/radon/radon.py | 51 +++++++++++++ .../review/inspectors/spotbugs/spotbugs.py | 4 +- .../inspectors/springlint/springlint.py | 10 +-- src/python/review/inspectors/tips.py | 5 ++ src/python/review/logging_config.py | 12 +-- src/python/review/quality/evaluate_quality.py | 5 ++ src/python/review/quality/model.py | 13 ++-- .../quality/rules/best_practices_scoring.py | 4 +- .../quality/rules/boolean_length_scoring.py | 6 +- .../quality/rules/class_response_scoring.py | 4 +- .../quality/rules/code_style_scoring.py | 8 +- .../review/quality/rules/coupling_scoring.py | 4 +- .../rules/cyclomatic_complexity_scoring.py | 10 +-- .../quality/rules/error_prone_scoring.py | 2 +- .../quality/rules/function_length_scoring.py | 12 +-- .../rules/inheritance_depth_scoring.py | 2 +- .../review/quality/rules/line_len_scoring.py | 4 +- .../quality/rules/maintainability_scoring.py | 74 +++++++++++++++++++ .../quality/rules/method_number_scoring.py | 4 +- .../quality/rules/weighted_methods_scoring.py | 4 +- src/python/review/reviewers/common.py | 8 +- src/python/review/reviewers/perform_review.py | 4 +- .../review/reviewers/utils/code_statistics.py | 6 +- .../review/reviewers/utils/issues_filter.py | 4 + .../review/reviewers/utils/print_review.py | 6 +- src/python/review/run_tool.py | 2 +- test/python/functional_tests/conftest.py | 2 +- .../test_different_languages.py | 8 +- test/python/functional_tests/test_disable.py | 4 +- .../functional_tests/test_duplicates.py | 4 +- .../python/functional_tests/test_exit_code.py | 6 +- .../functional_tests/test_file_or_project.py | 4 +- .../test_multi_file_project.py | 26 +++---- .../functional_tests/test_range_of_lines.py | 28 +++---- .../test_single_file_json_format.py | 16 ++-- .../python/functional_tests/test_verbosity.py | 6 +- test/python/inspectors/conftest.py | 20 ++--- .../inspectors/test_flake8_inspector.py | 2 +- test/python/inspectors/test_local_review.py | 4 +- .../inspectors/test_out_of_range_issues.py | 2 +- .../inspectors/test_pylint_inspector.py | 2 +- .../python/inspectors/test_radon_inspector.py | 52 +++++++++++++ 64 files changed, 395 insertions(+), 178 deletions(-) create mode 100644 src/python/review/inspectors/common.py create mode 100644 src/python/review/inspectors/radon/__init__.py create mode 100644 src/python/review/inspectors/radon/radon.py create mode 100644 src/python/review/quality/rules/maintainability_scoring.py create mode 100644 test/python/inspectors/test_radon_inspector.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 50706799..4cc2a22b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,7 +27,7 @@ jobs: # stop the build if there are Python syntax errors or undefined names flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics --exclude=.git,__pycache__,docs/source/conf.py,old,build,dist,venv,test/resources,.eggs,review.egg-info,.pytest_cache,node_modules # TODO: change max-complexity into 10 after refactoring - flake8 . --count --max-complexity=11 --max-line-length=120 --max-doc-length=120 --ignore=I201,I202,I101,I100,R504,A003,E800,SC200,SC100,E402,W503,WPS,C812,H601 --statistics --exclude=.git,__pycache__,docs/source/conf.py,old,build,dist,venv,test/resources,.eggs,review.egg-info,.pytest_cache,node_modules + flake8 . --count --max-complexity=11 --max-line-length=120 --max-doc-length=120 --ignore=I201,I202,I101,I100,R504,A003,E800,SC200,SC100,E402,W503,WPS,H601 --statistics --exclude=.git,__pycache__,docs/source/conf.py,old,build,dist,venv,test/resources,.eggs,review.egg-info,.pytest_cache,node_modules - name: Set up Eslint run: | npm install eslint --save-dev diff --git a/README.md b/README.md index 84a74445..43370839 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,10 @@ Python language: - [x] Pylint [GNU LGPL v2] * [Site and docs](https://www.pylint.org/) * [Repository](https://github.com/PyCQA/pylint) - + +- [x] Radon [MIT] + * [Site and docs](https://radon.readthedocs.io/en/latest/) + * [Repository](https://github.com/rubik/radon) Java language: @@ -92,7 +95,7 @@ Argument | Description --- | --- **‑h**, **‑‑help** | show the help message and exit. **‑v**, **‑‑verbosity** | choose logging level according [this](https://docs.python.org/3/library/logging.html#levels) list: `1` - **ERROR**; `2` - **INFO**; `3` - **DEBUG**; `0` - disable logging (**CRITICAL** value); default value is `0` (**CRITICAL**). -**‑d**, **‑‑disable** | disable inspectors. Available values: for **Python** language: `pylint` for [Pylint](https://github.com/PyCQA/pylint), `flake8` for [flake8](https://flake8.pycqa.org/en/latest/), `python_ast` to check different measures providing by AST; for **Java** language: `checkstyle` for the [Checkstyle](https://checkstyle.sourceforge.io/), `pmd` for [PMD](https://pmd.github.io/); for `Kotlin` language: detekt for [Detekt](https://detekt.github.io/detekt/); for **JavaScript** language: `eslint` for [ESlint](https://eslint.org/). Example: `-d pylint,flake8`. +**‑d**, **‑‑disable** | disable inspectors. Available values: for **Python** language: `pylint` for [Pylint](https://github.com/PyCQA/pylint), `flake8` for [flake8](https://flake8.pycqa.org/en/latest/), `radon` for [Radon](https://radon.readthedocs.io/en/latest/), `python_ast` to check different measures providing by AST; for **Java** language: `checkstyle` for the [Checkstyle](https://checkstyle.sourceforge.io/), `pmd` for [PMD](https://pmd.github.io/); for `Kotlin` language: detekt for [Detekt](https://detekt.github.io/detekt/); for **JavaScript** language: `eslint` for [ESlint](https://eslint.org/). Example: `-d pylint,flake8`. **‑‑allow-duplicates** | allow duplicate issues found by different linters. By default, duplicates are skipped. **‑‑language-version**, **‑‑language_version** | specify the language version for JAVA inspectors. Available values: `java7`, `java8`, `java9`, `java11`. **Note**: **‑‑language_version** is deprecated. Will be deleted in the future. **‑‑n-cpu**, **‑‑n_cpu** | specify number of _cpu_ that can be used to run inspectors. **Note**: **‑‑n_cpu** is deprecated. Will be deleted in the future. diff --git a/requirements.txt b/requirements.txt index e13a08f4..60409ee1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,6 +22,7 @@ flake8-broken-line==0.3.0 flake8-string-format==0.3.0 flake8-commas==2.0.0 cohesion==1.0.0 +radon==4.5.0 # extra libraries and frameworks django==3.0.8 diff --git a/setup.py b/setup.py index 12428183..ab75f040 100644 --- a/setup.py +++ b/setup.py @@ -45,22 +45,22 @@ def get_inspectors_additional_files() -> List[str]: 'Topic :: Software Development :: Build Tools', 'License :: OSI Approved :: MIT License', 'Programming Language :: Python :: 3', - 'Operating System :: OS Independent' + 'Operating System :: OS Independent', ], keywords='code review', python_requires='>=3.8, <4', install_requires=['upsourceapi'], packages=find_packages(exclude=[ '*.unit_tests', '*.unit_tests.*', 'unit_tests.*', 'unit_tests', - '*.functional_tests', '*.functional_tests.*', 'functional_tests.*', 'functional_tests' + '*.functional_tests', '*.functional_tests.*', 'functional_tests.*', 'functional_tests', ]), zip_safe=False, package_data={ - '': get_inspectors_additional_files() + '': get_inspectors_additional_files(), }, entry_points={ 'console_scripts': [ - 'review=review.run_tool:main' - ] - } + 'review=review.run_tool:main', + ], + }, ) diff --git a/src/python/review/common/file_system.py b/src/python/review/common/file_system.py index 6decb410..3faa962c 100644 --- a/src/python/review/common/file_system.py +++ b/src/python/review/common/file_system.py @@ -77,7 +77,7 @@ def create_directory(directory: str) -> None: def get_file_line(path: Path, line_number: int): return linecache.getline( str(path), - line_number + line_number, ).strip() diff --git a/src/python/review/common/java_compiler.py b/src/python/review/common/java_compiler.py index aee76032..e6b97861 100644 --- a/src/python/review/common/java_compiler.py +++ b/src/python/review/common/java_compiler.py @@ -18,7 +18,7 @@ def javac(javac_args: Union[str, Path]) -> bool: output_bytes: bytes = subprocess.check_output( f'javac {javac_args}', shell=True, - stderr=subprocess.STDOUT + stderr=subprocess.STDOUT, ) output_str = str(output_bytes, Encoding.UTF_ENCODING.value) diff --git a/src/python/review/common/parallel_runner.py b/src/python/review/common/parallel_runner.py index e9ab7d8d..c0347d23 100644 --- a/src/python/review/common/parallel_runner.py +++ b/src/python/review/common/parallel_runner.py @@ -37,7 +37,7 @@ def inspect_in_parallel(path: Path, with multiprocessing.Pool(config.n_cpu) as pool: issues = pool.map( functools.partial(run_inspector, path, config), - inspectors + inspectors, ) return list(itertools.chain(*issues)) diff --git a/src/python/review/common/subprocess_runner.py b/src/python/review/common/subprocess_runner.py index 3dd5cad3..a25cbdcd 100644 --- a/src/python/review/common/subprocess_runner.py +++ b/src/python/review/common/subprocess_runner.py @@ -9,7 +9,7 @@ def run_in_subprocess(command: List[str]) -> str: process = subprocess.run( command, stdout=subprocess.PIPE, - stderr=subprocess.PIPE + stderr=subprocess.PIPE, ) stdout = process.stdout.decode() diff --git a/src/python/review/inspectors/checkstyle/checkstyle.py b/src/python/review/inspectors/checkstyle/checkstyle.py index 389d889f..c796fbf9 100644 --- a/src/python/review/inspectors/checkstyle/checkstyle.py +++ b/src/python/review/inspectors/checkstyle/checkstyle.py @@ -35,7 +35,7 @@ class CheckstyleInspector(BaseInspector): r'Boolean expression complexity is (\d+)', 'LineLengthCheck': - r'Line is longer than \d+ characters \(found (\d+)\)' + r'Line is longer than \d+ characters \(found (\d+)\)', } @classmethod @@ -43,7 +43,7 @@ def _create_command(cls, path: Path, output_path: Path) -> List[str]: return [ 'java', '-jar', PATH_TOOLS_CHECKSTYLE_JAR, '-c', PATH_TOOLS_CHECKSTYLE_CONFIG, - '-f', 'xml', '-o', output_path, str(path) + '-f', 'xml', '-o', output_path, str(path), ] def inspect(self, path: Path, config: dict) -> List[BaseIssue]: diff --git a/src/python/review/inspectors/common.py b/src/python/review/inspectors/common.py new file mode 100644 index 00000000..dfedbb1e --- /dev/null +++ b/src/python/review/inspectors/common.py @@ -0,0 +1,12 @@ +from math import floor + + +def convert_percentage_of_value_to_lack_of_value(percentage_of_value: float) -> int: + """ + Converts percentage of value to lack of value. + Calculated by the formula: floor(100 - percentage_of_value). + + :param percentage_of_value: value in the range from 0 to 100. + :return: lack of value. + """ + return floor(100 - percentage_of_value) diff --git a/src/python/review/inspectors/detekt/detekt.py b/src/python/review/inspectors/detekt/detekt.py index 2c2b197e..8f5f0946 100644 --- a/src/python/review/inspectors/detekt/detekt.py +++ b/src/python/review/inspectors/detekt/detekt.py @@ -27,7 +27,7 @@ class DetektInspector(BaseInspector): 'ComplexCondition': r'This condition is too complex \((\d+)\)', 'ComplexMethod': - r'The function .* appears to be too complex \((\d+)\)' + r'The function .* appears to be too complex \((\d+)\)', } @classmethod @@ -38,7 +38,7 @@ def _create_command(cls, path: Path, output_path: Path): '--config', PATH_DETEKT_CONFIG, '--plugins', PATH_DETEKT_PLUGIN, '--report', f'xml:{output_path}', - '--input', str(path) + '--input', str(path), ] def inspect(self, path: Path, config) -> List[BaseIssue]: diff --git a/src/python/review/inspectors/eslint/eslint.py b/src/python/review/inspectors/eslint/eslint.py index ff9f9a67..839a166c 100644 --- a/src/python/review/inspectors/eslint/eslint.py +++ b/src/python/review/inspectors/eslint/eslint.py @@ -17,7 +17,7 @@ class ESLintInspector(BaseInspector): origin_class_to_pattern = { 'complexity': - r'complexity of (\d+)' + r'complexity of (\d+)', } @classmethod diff --git a/src/python/review/inspectors/flake8/.flake8 b/src/python/review/inspectors/flake8/.flake8 index 6cb0d0cf..cc3e69cb 100644 --- a/src/python/review/inspectors/flake8/.flake8 +++ b/src/python/review/inspectors/flake8/.flake8 @@ -50,5 +50,4 @@ ignore=W291, # trailing whitespaces F524, # missing argument. TODO: Collision with "P201" and "P202" F525, # mixing automatic and manual numbering. TODO: Collision with "P205" # flake8-commas - C814, # missing trailing comma in Python - \ No newline at end of file + C814, # missing trailing comma in Python 2 diff --git a/src/python/review/inspectors/flake8/flake8.py b/src/python/review/inspectors/flake8/flake8.py index c7318d0b..b974015c 100644 --- a/src/python/review/inspectors/flake8/flake8.py +++ b/src/python/review/inspectors/flake8/flake8.py @@ -1,5 +1,4 @@ import logging -import math import re from pathlib import Path from typing import List @@ -17,6 +16,7 @@ CohesionIssue, ) from src.python.review.inspectors.tips import get_cyclomatic_complexity_tip +from src.python.review.inspectors.common import convert_percentage_of_value_to_lack_of_value logger = logging.getLogger(__name__) @@ -69,7 +69,9 @@ def parse(cls, output: str) -> List[BaseIssue]: issues.append(CyclomaticComplexityIssue(**issue_data)) elif cohesion_match is not None: # flake8-cohesion issue_data[IssueData.DESCRIPTION.value] = description # TODO: Add tip - issue_data[IssueData.COHESION_LACK.value] = cls.__get_cohesion_lack(float(cohesion_match.group(1))) + issue_data[IssueData.COHESION_LACK.value] = convert_percentage_of_value_to_lack_of_value( + float(cohesion_match.group(1)), + ) issue_data[IssueData.ISSUE_TYPE.value] = IssueType.COHESION issues.append(CohesionIssue(**issue_data)) else: @@ -98,14 +100,3 @@ def choose_issue_type(code: str) -> IssueType: return IssueType.BEST_PRACTICES return issue_type - - @staticmethod - def __get_cohesion_lack(cohesion_percentage: float) -> int: - """ - Converts cohesion percentage to lack of cohesion. - Calculated by the formula: floor(100 - cohesion_percentage). - - :param cohesion_percentage: cohesion set as a percentage. - :return: lack of cohesion - """ - return math.floor(100 - cohesion_percentage) diff --git a/src/python/review/inspectors/inspector_type.py b/src/python/review/inspectors/inspector_type.py index 1b69ac2d..15482a8b 100644 --- a/src/python/review/inspectors/inspector_type.py +++ b/src/python/review/inspectors/inspector_type.py @@ -8,6 +8,7 @@ class InspectorType(Enum): PYLINT = 'PYLINT' PYTHON_AST = 'PYTHON_AST' FLAKE8 = 'FLAKE8' + RADON = 'RADON' # Java language PMD = 'PMD' @@ -29,6 +30,7 @@ def available_values(cls) -> List[str]: cls.PYLINT.value, cls.FLAKE8.value, cls.PYTHON_AST.value, + cls.RADON.value, # Java language cls.PMD.value, diff --git a/src/python/review/inspectors/intellij/intellij.py b/src/python/review/inspectors/intellij/intellij.py index 8b962947..b50dc0ca 100644 --- a/src/python/review/inspectors/intellij/intellij.py +++ b/src/python/review/inspectors/intellij/intellij.py @@ -48,7 +48,7 @@ def __init__(self): def create_command(output_dir_path) -> List[Union[str, Path]]: return [ INTELLIJ_INSPECTOR_EXECUTABLE, INTELLIJ_INSPECTOR_PROJECT, - INTELLIJ_INSPECTOR_SETTINGS, output_dir_path, '-v2' + INTELLIJ_INSPECTOR_SETTINGS, output_dir_path, '-v2', ] def inspect(self, path: Path, config: dict) -> List[BaseIssue]: @@ -134,8 +134,8 @@ def parse(cls, out_dir_path: Path, file_path = Path( text.replace( 'file://$PROJECT_DIR$', - str(INTELLIJ_INSPECTOR_PROJECT) - ) + str(INTELLIJ_INSPECTOR_PROJECT), + ), ) elif tag == 'line': line_no = int(text) @@ -160,7 +160,7 @@ def parse(cls, out_dir_path: Path, description=description, origin_class=issue_class, inspector_type=cls.inspector_type, - type=issue_type + type=issue_type, )) return issues diff --git a/src/python/review/inspectors/intellij/issue_types/kotlin.py b/src/python/review/inspectors/intellij/issue_types/kotlin.py index 97438dca..cb692510 100644 --- a/src/python/review/inspectors/intellij/issue_types/kotlin.py +++ b/src/python/review/inspectors/intellij/issue_types/kotlin.py @@ -378,5 +378,5 @@ '\'when\' that can be simplified by introducing an argument': IssueType.CODE_STYLE, - 'Annotator': IssueType.ERROR_PRONE + 'Annotator': IssueType.ERROR_PRONE, } diff --git a/src/python/review/inspectors/issue.py b/src/python/review/inspectors/issue.py index e4a21798..c910bf80 100644 --- a/src/python/review/inspectors/issue.py +++ b/src/python/review/inspectors/issue.py @@ -25,6 +25,7 @@ class IssueType(Enum): COHESION = 'COHESION' CLASS_RESPONSE = 'CLASS_RESPONSE' METHOD_NUMBER = 'METHOD_NUMBER' + MAINTAINABILITY = 'MAINTAINABILITY' # Keys in results dictionary @@ -46,6 +47,7 @@ class IssueData(Enum): BOOL_EXPR_LEN = 'bool_expr_len' CYCLOMATIC_COMPLEXITY = 'cc_value' COHESION_LACK = 'cohesion_lack' + MAINTAINABILITY_LACK = 'maintainability_lack' @classmethod def get_base_issue_data_dict(cls, @@ -59,7 +61,7 @@ def get_base_issue_data_dict(cls, cls.LINE_NUMBER.value: line_number, cls.COLUMN_NUMBER.value: column_number, cls.ORIGIN_ClASS.value: origin_class, - cls.INSPECTOR_TYPE.value: inspector_type + cls.INSPECTOR_TYPE.value: inspector_type, } @@ -184,3 +186,12 @@ class MethodNumberIssue(BaseIssue, Measurable): def measure(self) -> int: return self.method_number + + +@dataclass(frozen=True) +class MaintainabilityLackIssue(BaseIssue, Measurable): + maintainability_lack: int + type = IssueType.MAINTAINABILITY + + def measure(self) -> int: + return self.maintainability_lack diff --git a/src/python/review/inspectors/pmd/pmd.py b/src/python/review/inspectors/pmd/pmd.py index 227169fc..4b8c11f0 100644 --- a/src/python/review/inspectors/pmd/pmd.py +++ b/src/python/review/inspectors/pmd/pmd.py @@ -37,7 +37,7 @@ def _create_command(cls, path: Path, '-language', 'java', '-version', java_version.value, '-f', 'csv', '-r', str(output_path), - '-t', str(n_cpu) + '-t', str(n_cpu), ] def inspect(self, path: Path, config: dict) -> List[BaseIssue]: diff --git a/src/python/review/inspectors/pyast/python_ast.py b/src/python/review/inspectors/pyast/python_ast.py index 28b76e9f..6426d115 100644 --- a/src/python/review/inspectors/pyast/python_ast.py +++ b/src/python/review/inspectors/pyast/python_ast.py @@ -40,7 +40,7 @@ def visit(self, node: ast.AST): origin_class=BOOL_EXPR_LEN_ORIGIN_CLASS, inspector_type=self._inspector_type, bool_expr_len=length, - type=IssueType.BOOL_EXPR_LEN + type=IssueType.BOOL_EXPR_LEN, )) @@ -58,7 +58,7 @@ def __init__(self, content: str, file_path: Path, inspector_type: InspectorType) def visit(self, node): if isinstance(self._previous_node, (ast.FunctionDef, ast.AsyncFunctionDef)): func_length = self._find_func_len( - self._previous_node.lineno, node.lineno + self._previous_node.lineno, node.lineno, ) self._function_lens.append(FuncLenIssue( @@ -69,7 +69,7 @@ def visit(self, node): origin_class=FUNC_LEN_ORIGIN_CLASS, inspector_type=self._inspector_type, func_len=func_length, - type=IssueType.FUNC_LEN + type=IssueType.FUNC_LEN, )) self._previous_node = node @@ -80,7 +80,7 @@ def visit(self, node): def function_lens(self) -> List[FuncLenIssue]: if isinstance(self._previous_node, (ast.FunctionDef, ast.AsyncFunctionDef)): func_length = self._find_func_len( - self._previous_node.lineno, self._n_lines + 1 + self._previous_node.lineno, self._n_lines + 1, ) self._function_lens.append(FuncLenIssue( @@ -91,7 +91,7 @@ def function_lens(self) -> List[FuncLenIssue]: origin_class=FUNC_LEN_ORIGIN_CLASS, inspector_type=self._inspector_type, func_len=func_length, - type=IssueType.FUNC_LEN + type=IssueType.FUNC_LEN, )) self._previous_node = None @@ -125,13 +125,13 @@ def inspect(cls, path: Path, config: dict) -> List[BaseIssue]: bool_gatherer = BoolExpressionLensGatherer(path_to_file, cls.inspector_type) bool_gatherer.visit(tree) metrics.extend( - bool_gatherer.bool_expression_lens + bool_gatherer.bool_expression_lens, ) func_gatherer = FunctionLensGatherer(file_content, path_to_file, cls.inspector_type) func_gatherer.visit(tree) metrics.extend( - func_gatherer.function_lens + func_gatherer.function_lens, ) return metrics diff --git a/src/python/review/inspectors/pylint/pylint.py b/src/python/review/inspectors/pylint/pylint.py index fb9db581..e5a256cb 100644 --- a/src/python/review/inspectors/pylint/pylint.py +++ b/src/python/review/inspectors/pylint/pylint.py @@ -21,7 +21,7 @@ class PylintInspector(BaseInspector): supported_issue_types = ( IssueType.CODE_STYLE, IssueType.BEST_PRACTICES, - IssueType.ERROR_PRONE + IssueType.ERROR_PRONE, ) @classmethod @@ -31,7 +31,7 @@ def inspect(cls, path: Path, config: dict) -> List[CodeIssue]: '--load-plugins', 'pylint_django', f'--rcfile={PATH_PYLINT_CONFIG}', f'--msg-template={MSG_TEMPLATE}', - str(path) + str(path), ] output = run_in_subprocess(command) @@ -70,7 +70,7 @@ def parse(cls, output: str) -> List[CodeIssue]: origin_class=origin_class, description=description, inspector_type=cls.inspector_type, - type=issue_type + type=issue_type, )) return issues diff --git a/src/python/review/inspectors/radon/__init__.py b/src/python/review/inspectors/radon/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/python/review/inspectors/radon/radon.py b/src/python/review/inspectors/radon/radon.py new file mode 100644 index 00000000..fb2bf299 --- /dev/null +++ b/src/python/review/inspectors/radon/radon.py @@ -0,0 +1,51 @@ +import re +from pathlib import Path +from typing import List + +from src.python.review.common.subprocess_runner import run_in_subprocess +from src.python.review.inspectors.base_inspector import BaseInspector +from src.python.review.inspectors.inspector_type import InspectorType +from src.python.review.inspectors.issue import BaseIssue, IssueData, IssueType, MaintainabilityLackIssue +from src.python.review.inspectors.common import convert_percentage_of_value_to_lack_of_value +from src.python.review.inspectors.tips import get_maintainability_index_tip + + +class RadonInspector(BaseInspector): + inspector_type = InspectorType.RADON + + @classmethod + def inspect(cls, path: Path, config: dict) -> List[BaseIssue]: + mi_command = [ + "radon", "mi", # compute the Maintainability Index score + "--max", "F", # set the maximum MI rank to display + "--show", # actual MI value is shown in results, alongside the rank + path, + ] + + mi_output = run_in_subprocess(mi_command) + return cls.mi_parse(mi_output) + + @classmethod + def mi_parse(cls, mi_output: str) -> List[BaseIssue]: + """ + Parses the results of the "mi" command. + Description: https://radon.readthedocs.io/en/latest/commandline.html#the-mi-command + + :param mi_output: "mi" command output. + :return: list of issues. + """ + row_re = re.compile(r"^(.*) - \w \((.*)\)$", re.M) + + issues: List[BaseIssue] = [] + for groups in row_re.findall(mi_output): + file_path = Path(groups[0]) + maintainability_lack = convert_percentage_of_value_to_lack_of_value(float(groups[1])) + + issue_data = IssueData.get_base_issue_data_dict(file_path, cls.inspector_type) + issue_data[IssueData.DESCRIPTION.value] = get_maintainability_index_tip() + issue_data[IssueData.MAINTAINABILITY_LACK.value] = maintainability_lack + issue_data[IssueData.ISSUE_TYPE.value] = IssueType.MAINTAINABILITY + + issues.append(MaintainabilityLackIssue(**issue_data)) + + return issues diff --git a/src/python/review/inspectors/spotbugs/spotbugs.py b/src/python/review/inspectors/spotbugs/spotbugs.py index e9cd1cb2..a2ee8b38 100644 --- a/src/python/review/inspectors/spotbugs/spotbugs.py +++ b/src/python/review/inspectors/spotbugs/spotbugs.py @@ -30,7 +30,7 @@ def _create_command(cls, path: Path) -> List[str]: PATH_SPOTBUGS_EXCLUDE, '-textui', '-medium', - str(path) + str(path), ] def inspect(self, path: Path, config: dict) -> List[BaseIssue]: @@ -100,5 +100,5 @@ def _parse_single_line(cls, line: str, file_name_to_path: Dict[str, Path]) -> Ba type=IssueType.ERROR_PRONE, origin_class=issue_class, description=short_desc, - inspector_type=cls.inspector_type + inspector_type=cls.inspector_type, ) diff --git a/src/python/review/inspectors/springlint/springlint.py b/src/python/review/inspectors/springlint/springlint.py index 14ce8c89..cc90c487 100644 --- a/src/python/review/inspectors/springlint/springlint.py +++ b/src/python/review/inspectors/springlint/springlint.py @@ -51,7 +51,7 @@ class SpringlintInspector(BaseInspector): 'cbo': 'class_objects_coupling', 'lcom': 'cohesion_lack', 'rfc': 'class_response', - 'nom': 'method_number' + 'nom': 'method_number', } metric_name_to_description = { @@ -61,7 +61,7 @@ class SpringlintInspector(BaseInspector): 'cbo': get_class_coupling_tip(), 'lcom': get_cohesion_tip(), 'rfc': get_class_response_tip(), - 'nom': get_method_number_tip() + 'nom': get_method_number_tip(), } metric_name_to_issue_type = { @@ -71,7 +71,7 @@ class SpringlintInspector(BaseInspector): 'cbo': IssueType.COUPLING, 'lcom': IssueType.COHESION, 'rfc': IssueType.CLASS_RESPONSE, - 'nom': IssueType.METHOD_NUMBER + 'nom': IssueType.METHOD_NUMBER, } @classmethod @@ -81,7 +81,7 @@ def _create_command(cls, path: Path, output_path: Path) -> List[str]: PATH_SPRINGLINT_JAR, '--output', str(output_path), '-otype', 'html', - '--project', str(path) + '--project', str(path), ] def inspect(self, path: Path, config: dict) -> List[BaseIssue]: @@ -137,7 +137,7 @@ def _parse_smells(cls, file_content: AnyStr, origin_path: str = '') -> List[Base origin_class=smell['name'], inspector_type=cls.inspector_type, type=IssueType.ARCHITECTURE, - description=smell['description'] + description=smell['description'], ) for smell in file_smell['smells']]) return issues diff --git a/src/python/review/inspectors/tips.py b/src/python/review/inspectors/tips.py index 5211b569..eac449c1 100644 --- a/src/python/review/inspectors/tips.py +++ b/src/python/review/inspectors/tips.py @@ -84,3 +84,8 @@ def get_method_number_tip() -> str: 'The file has too many methods inside and is complicated to understand. ' 'Consider its decomposition to smaller classes.' ) + + +# TODO: Need to improve the tip. +def get_maintainability_index_tip() -> str: + return 'The maintainability index is too low.' diff --git a/src/python/review/logging_config.py b/src/python/review/logging_config.py index ce3c47a0..ee55bf46 100644 --- a/src/python/review/logging_config.py +++ b/src/python/review/logging_config.py @@ -5,22 +5,22 @@ 'formatters': { 'common': { 'class': 'logging.Formatter', - 'format': '%(asctime)s | %(levelname)s | %(message)s' - } + 'format': '%(asctime)s | %(levelname)s | %(message)s', + }, }, 'handlers': { 'console': { 'class': 'logging.StreamHandler', 'level': 'DEBUG', 'formatter': 'common', - 'stream': sys.stdout + 'stream': sys.stdout, }, }, 'loggers': { '': { 'handlers': ['console'], - 'level': 'INFO' - } + 'level': 'INFO', + }, }, - 'disable_existing_loggers': False + 'disable_existing_loggers': False, } diff --git a/src/python/review/quality/evaluate_quality.py b/src/python/review/quality/evaluate_quality.py index d7ee0211..3861d3b3 100644 --- a/src/python/review/quality/evaluate_quality.py +++ b/src/python/review/quality/evaluate_quality.py @@ -41,6 +41,10 @@ LANGUAGE_TO_COHESION_RULE_CONFIG, CohesionRule, ) +from src.python.review.quality.rules.maintainability_scoring import ( + LANGUAGE_TO_MAINTAINABILITY_RULE_CONFIG, + MaintainabilityRule, +) def __get_available_rules(language: Language) -> List[Rule]: @@ -56,6 +60,7 @@ def __get_available_rules(language: Language) -> List[Rule]: ResponseRule(LANGUAGE_TO_RESPONSE_RULE_CONFIG[language]), WeightedMethodsRule(LANGUAGE_TO_WEIGHTED_METHODS_RULE_CONFIG[language]), CohesionRule(LANGUAGE_TO_COHESION_RULE_CONFIG[language]), + MaintainabilityRule(LANGUAGE_TO_MAINTAINABILITY_RULE_CONFIG[language]), ] diff --git a/src/python/review/quality/model.py b/src/python/review/quality/model.py index 741c761b..6e64ea81 100644 --- a/src/python/review/quality/model.py +++ b/src/python/review/quality/model.py @@ -20,7 +20,7 @@ def __le__(self, other: 'QualityType') -> bool: QualityType.BAD: 0, QualityType.MODERATE: 1, QualityType.GOOD: 2, - QualityType.EXCELLENT: 3 + QualityType.EXCELLENT: 3, } return order[self] < order[other] @@ -81,12 +81,11 @@ def __str__(self): message_deltas_part = '' if self.quality_type != QualityType.EXCELLENT: - message_next_level_part = textwrap.dedent( - f"""\ - Next level: {self.next_quality_type.value} - Next level requirements: - """ - ) + message_next_level_part = f"""\ + Next level: {self.next_quality_type.value} + Next level requirements: + """ + message_next_level_part = textwrap.dedent(message_next_level_part) for rule in self.next_level_requirements: message_deltas_part += f'{rule.rule_type.value}: {rule.next_level_delta}\n' diff --git a/src/python/review/quality/rules/best_practices_scoring.py b/src/python/review/quality/rules/best_practices_scoring.py index f014d6e8..633387f2 100644 --- a/src/python/review/quality/rules/best_practices_scoring.py +++ b/src/python/review/quality/rules/best_practices_scoring.py @@ -16,7 +16,7 @@ class BestPracticesRuleConfig: common_best_practices_rule_config = BestPracticesRuleConfig( n_best_practices_moderate=3, n_best_practices_good=1, - n_files=1 + n_files=1, ) LANGUAGE_TO_BEST_PRACTICES_RULE_CONFIG = { @@ -59,7 +59,7 @@ def merge(self, other: 'BestPracticesRule') -> 'BestPracticesRule': config = BestPracticesRuleConfig( min(self.config.n_best_practices_moderate, other.config.n_best_practices_moderate), min(self.config.n_best_practices_good, other.config.n_best_practices_good), - n_files=self.config.n_files + other.config.n_files + n_files=self.config.n_files + other.config.n_files, ) result_rule = BestPracticesRule(config) result_rule.apply(self.n_best_practices + other.n_best_practices) diff --git a/src/python/review/quality/rules/boolean_length_scoring.py b/src/python/review/quality/rules/boolean_length_scoring.py index b2d7c347..2a33c327 100644 --- a/src/python/review/quality/rules/boolean_length_scoring.py +++ b/src/python/review/quality/rules/boolean_length_scoring.py @@ -16,13 +16,13 @@ class BooleanExpressionRuleConfig: common_boolean_expression_rule_config = BooleanExpressionRuleConfig( bool_expr_len_bad=10, bool_expr_len_moderate=7, - bool_expr_len_good=5 + bool_expr_len_good=5, ) java_boolean_expression_rule_config = BooleanExpressionRuleConfig( bool_expr_len_bad=8, bool_expr_len_moderate=6, - bool_expr_len_good=4 + bool_expr_len_good=4, ) LANGUAGE_TO_BOOLEAN_EXPRESSION_RULE_CONFIG = { @@ -66,7 +66,7 @@ def merge(self, other: 'BooleanExpressionRule') -> 'BooleanExpressionRule': config = BooleanExpressionRuleConfig( min(self.config.bool_expr_len_bad, other.config.bool_expr_len_bad), min(self.config.bool_expr_len_moderate, other.config.bool_expr_len_moderate), - min(self.config.bool_expr_len_good, other.config.bool_expr_len_good) + min(self.config.bool_expr_len_good, other.config.bool_expr_len_good), ) result_rule = BooleanExpressionRule(config) result_rule.apply(max(self.bool_expr_len, other.bool_expr_len)) diff --git a/src/python/review/quality/rules/class_response_scoring.py b/src/python/review/quality/rules/class_response_scoring.py index 4bfd4800..d4b82fa9 100644 --- a/src/python/review/quality/rules/class_response_scoring.py +++ b/src/python/review/quality/rules/class_response_scoring.py @@ -14,7 +14,7 @@ class ResponseRuleConfig: common_response_rule_config = ResponseRuleConfig( response_moderate=69, - response_good=59 + response_good=59, ) LANGUAGE_TO_RESPONSE_RULE_CONFIG = { @@ -52,7 +52,7 @@ def __get_next_quality_type(self) -> QualityType: def merge(self, other: 'ResponseRule') -> 'ResponseRule': config = ResponseRuleConfig( min(self.config.response_moderate, other.config.response_moderate), - min(self.config.response_good, other.config.response_good) + min(self.config.response_good, other.config.response_good), ) result_rule = ResponseRule(config) result_rule.apply(max(self.response, other.response)) diff --git a/src/python/review/quality/rules/code_style_scoring.py b/src/python/review/quality/rules/code_style_scoring.py index 882b0c5c..a1edda09 100644 --- a/src/python/review/quality/rules/code_style_scoring.py +++ b/src/python/review/quality/rules/code_style_scoring.py @@ -19,7 +19,7 @@ class CodeStyleRuleConfig: n_code_style_moderate=0.17, n_code_style_good=0, n_code_style_lines_bad=10, - language=Language.JAVA + language=Language.JAVA, ) python_code_style_rule_config = CodeStyleRuleConfig( @@ -27,7 +27,7 @@ class CodeStyleRuleConfig: n_code_style_moderate=0.17, n_code_style_good=0, n_code_style_lines_bad=5, - language=Language.PYTHON + language=Language.PYTHON, ) kotlin_code_style_rule_config = CodeStyleRuleConfig( @@ -35,7 +35,7 @@ class CodeStyleRuleConfig: n_code_style_moderate=0.07, n_code_style_good=0, n_code_style_lines_bad=10, - language=Language.KOTLIN + language=Language.KOTLIN, ) js_code_style_rule_config = CodeStyleRuleConfig( @@ -43,7 +43,7 @@ class CodeStyleRuleConfig: n_code_style_moderate=0.17, n_code_style_good=0, n_code_style_lines_bad=10, - language=Language.JAVA + language=Language.JAVA, ) LANGUAGE_TO_CODE_STYLE_RULE_CONFIG = { diff --git a/src/python/review/quality/rules/coupling_scoring.py b/src/python/review/quality/rules/coupling_scoring.py index 83ea987b..7fb0a782 100644 --- a/src/python/review/quality/rules/coupling_scoring.py +++ b/src/python/review/quality/rules/coupling_scoring.py @@ -14,7 +14,7 @@ class CouplingRuleConfig: common_coupling_rule_config = CouplingRuleConfig( coupling_bad=30, - coupling_moderate=20 + coupling_moderate=20, ) LANGUAGE_TO_COUPLING_RULE_CONFIG = { @@ -52,7 +52,7 @@ def __get_next_quality_type(self) -> QualityType: def merge(self, other: 'CouplingRule') -> 'CouplingRule': config = CouplingRuleConfig( min(self.config.coupling_bad, other.config.coupling_bad), - min(self.config.coupling_moderate, other.config.coupling_moderate) + min(self.config.coupling_moderate, other.config.coupling_moderate), ) result_rule = CouplingRule(config) result_rule.apply(max(self.coupling, other.coupling)) diff --git a/src/python/review/quality/rules/cyclomatic_complexity_scoring.py b/src/python/review/quality/rules/cyclomatic_complexity_scoring.py index 79fb69dc..162284b4 100644 --- a/src/python/review/quality/rules/cyclomatic_complexity_scoring.py +++ b/src/python/review/quality/rules/cyclomatic_complexity_scoring.py @@ -15,20 +15,20 @@ class CyclomaticComplexityRuleConfig: LANGUAGE_TO_CYCLOMATIC_COMPLEXITY_RULE_CONFIG = { Language.JAVA: CyclomaticComplexityRuleConfig( cc_value_bad=14, - cc_value_moderate=13 + cc_value_moderate=13, ), Language.KOTLIN: CyclomaticComplexityRuleConfig( cc_value_bad=12, - cc_value_moderate=11 + cc_value_moderate=11, ), Language.PYTHON: CyclomaticComplexityRuleConfig( cc_value_bad=10, - cc_value_moderate=9 + cc_value_moderate=9, ), Language.JS: CyclomaticComplexityRuleConfig( cc_value_bad=14, - cc_value_moderate=13 - ) + cc_value_moderate=13, + ), } diff --git a/src/python/review/quality/rules/error_prone_scoring.py b/src/python/review/quality/rules/error_prone_scoring.py index 7a6dee0e..8df9c157 100644 --- a/src/python/review/quality/rules/error_prone_scoring.py +++ b/src/python/review/quality/rules/error_prone_scoring.py @@ -12,7 +12,7 @@ class ErrorProneRuleConfig: common_error_prone_rule_config = ErrorProneRuleConfig( - n_error_prone_bad=0 + n_error_prone_bad=0, ) LANGUAGE_TO_ERROR_PRONE_RULE_CONFIG = { diff --git a/src/python/review/quality/rules/function_length_scoring.py b/src/python/review/quality/rules/function_length_scoring.py index f607f679..e728e9c2 100644 --- a/src/python/review/quality/rules/function_length_scoring.py +++ b/src/python/review/quality/rules/function_length_scoring.py @@ -13,17 +13,17 @@ class FunctionLengthRuleConfig: LANGUAGE_TO_FUNCTION_LENGTH_RULE_CONFIG = { Language.JAVA: FunctionLengthRuleConfig( - func_len_bad=69 + func_len_bad=69, ), Language.KOTLIN: FunctionLengthRuleConfig( - func_len_bad=69 + func_len_bad=69, ), Language.PYTHON: FunctionLengthRuleConfig( - func_len_bad=49 + func_len_bad=49, ), Language.JS: FunctionLengthRuleConfig( - func_len_bad=69 - ) + func_len_bad=69, + ), } @@ -48,7 +48,7 @@ def __get_next_quality_type(self) -> QualityType: def merge(self, other: 'FunctionLengthRule') -> 'FunctionLengthRule': config = FunctionLengthRuleConfig( - min(self.config.func_len_bad, other.config.func_len_bad) + min(self.config.func_len_bad, other.config.func_len_bad), ) result_rule = FunctionLengthRule(config) result_rule.apply(max(self.func_len, other.func_len)) diff --git a/src/python/review/quality/rules/inheritance_depth_scoring.py b/src/python/review/quality/rules/inheritance_depth_scoring.py index cdabda6b..a3531afc 100644 --- a/src/python/review/quality/rules/inheritance_depth_scoring.py +++ b/src/python/review/quality/rules/inheritance_depth_scoring.py @@ -12,7 +12,7 @@ class InheritanceDepthRuleConfig: common_inheritance_depth_rule_config = InheritanceDepthRuleConfig( - depth_bad=3 + depth_bad=3, ) LANGUAGE_TO_INHERITANCE_DEPTH_RULE_CONFIG = { diff --git a/src/python/review/quality/rules/line_len_scoring.py b/src/python/review/quality/rules/line_len_scoring.py index d52b0382..b188576f 100644 --- a/src/python/review/quality/rules/line_len_scoring.py +++ b/src/python/review/quality/rules/line_len_scoring.py @@ -13,7 +13,7 @@ class LineLengthRuleConfig: common_line_length_rule_config = LineLengthRuleConfig( n_line_len_bad=0.05, - n_line_len_good=0.035 + n_line_len_good=0.035, ) LANGUAGE_TO_LINE_LENGTH_RULE_CONFIG = { @@ -54,7 +54,7 @@ def __get_next_quality_type(self) -> QualityType: def merge(self, other: 'LineLengthRule') -> 'LineLengthRule': config = LineLengthRuleConfig( min(self.config.n_line_len_bad, other.config.n_line_len_bad), - min(self.config.n_line_len_good, other.config.n_line_len_good) + min(self.config.n_line_len_good, other.config.n_line_len_good), ) result_rule = LineLengthRule(config) result_rule.apply(self.n_line_len + other.n_line_len, self.n_lines + other.n_lines) diff --git a/src/python/review/quality/rules/maintainability_scoring.py b/src/python/review/quality/rules/maintainability_scoring.py new file mode 100644 index 00000000..456e7b0a --- /dev/null +++ b/src/python/review/quality/rules/maintainability_scoring.py @@ -0,0 +1,74 @@ +from dataclasses import dataclass +from typing import Optional + +from src.python.review.common.language import Language +from src.python.review.inspectors.issue import IssueType +from src.python.review.quality.model import Rule, QualityType + + +@dataclass +class MaintainabilityRuleConfig: + maintainability_lack_good: int + maintainability_lack_moderate: int + maintainability_lack_bad: int + + +# TODO: Need testing +# In Radon, the maintainability index is ranked as follows: +# 20-100: Very high +# 10-19: Medium +# 0-9: Extremely low +# Therefore, maintainability_lack_bad = 90, and maintainability_lack_moderate = 80. +common_maintainability_rule_config = MaintainabilityRuleConfig( + maintainability_lack_good=50, + maintainability_lack_moderate=80, + maintainability_lack_bad=90, +) + +LANGUAGE_TO_MAINTAINABILITY_RULE_CONFIG = { + Language.JAVA: common_maintainability_rule_config, + Language.PYTHON: common_maintainability_rule_config, + Language.KOTLIN: common_maintainability_rule_config, + Language.JS: common_maintainability_rule_config, +} + + +class MaintainabilityRule(Rule): + def __init__(self, config: MaintainabilityRuleConfig): + self.config = config + self.rule_type = IssueType.MAINTAINABILITY + self.maintainability_lack: Optional[int] = None + + def apply(self, maintainability_lack): + self.maintainability_lack = maintainability_lack + if maintainability_lack > self.config.maintainability_lack_bad: + self.quality_type = QualityType.BAD + self.next_level_delta = maintainability_lack - self.config.maintainability_lack_bad + elif maintainability_lack > self.config.maintainability_lack_moderate: + self.quality_type = QualityType.MODERATE + self.next_level_delta = maintainability_lack - self.config.maintainability_lack_moderate + elif maintainability_lack > self.config.maintainability_lack_good: + self.quality_type = QualityType.GOOD + self.next_level_delta = maintainability_lack - self.config.maintainability_lack_good + else: + self.quality_type = QualityType.EXCELLENT + self.next_level_delta = 0 + self.next_level_type = self.__get_next_quality_type() + + def __get_next_quality_type(self) -> QualityType: + if self.quality_type == QualityType.BAD: + return QualityType.MODERATE + elif self.quality_type == QualityType.MODERATE: + return QualityType.GOOD + return QualityType.EXCELLENT + + def merge(self, other: 'MaintainabilityRule') -> 'MaintainabilityRule': + config = MaintainabilityRuleConfig( + min(self.config.maintainability_lack_bad, other.config.maintainability_lack_bad), + min(self.config.maintainability_lack_moderate, other.config.maintainability_lack_moderate), + min(self.config.maintainability_lack_good, other.config.maintainability_lack_good), + ) + result_rule = MaintainabilityRule(config) + result_rule.apply(max(self.maintainability_lack, other.maintainability_lack)) + + return result_rule diff --git a/src/python/review/quality/rules/method_number_scoring.py b/src/python/review/quality/rules/method_number_scoring.py index 112390c3..bc15a497 100644 --- a/src/python/review/quality/rules/method_number_scoring.py +++ b/src/python/review/quality/rules/method_number_scoring.py @@ -16,7 +16,7 @@ class MethodNumberRuleConfig: common_method_number_rule_config = MethodNumberRuleConfig( method_number_bad=32, method_number_moderate=24, - method_number_good=20 + method_number_good=20, ) LANGUAGE_TO_METHOD_NUMBER_RULE_CONFIG = { @@ -60,7 +60,7 @@ def merge(self, other: 'MethodNumberRule') -> 'MethodNumberRule': config = MethodNumberRuleConfig( min(self.config.method_number_bad, other.config.method_number_bad), min(self.config.method_number_moderate, other.config.method_number_moderate), - min(self.config.method_number_good, other.config.method_number_good) + min(self.config.method_number_good, other.config.method_number_good), ) result_rule = MethodNumberRule(config) result_rule.apply(max(self.method_number, other.method_number)) diff --git a/src/python/review/quality/rules/weighted_methods_scoring.py b/src/python/review/quality/rules/weighted_methods_scoring.py index 777d7b2d..633c4839 100644 --- a/src/python/review/quality/rules/weighted_methods_scoring.py +++ b/src/python/review/quality/rules/weighted_methods_scoring.py @@ -16,7 +16,7 @@ class WeightedMethodsRuleConfig: common_weighted_methods_rule_config = WeightedMethodsRuleConfig( weighted_methods_bad=105, weighted_methods_moderate=85, - weighted_methods_good=70 + weighted_methods_good=70, ) LANGUAGE_TO_WEIGHTED_METHODS_RULE_CONFIG = { @@ -58,7 +58,7 @@ def merge(self, other: 'WeightedMethodsRule') -> 'WeightedMethodsRule': config = WeightedMethodsRuleConfig( min(self.config.weighted_methods_bad, other.config.weighted_methods_bad), min(self.config.weighted_methods_moderate, other.config.weighted_methods_moderate), - min(self.config.weighted_methods_good, other.config.weighted_methods_good) + min(self.config.weighted_methods_good, other.config.weighted_methods_good), ) result_rule = WeightedMethodsRule(config) result_rule.apply(max(self.weighted_methods, other.weighted_methods)) diff --git a/src/python/review/reviewers/common.py b/src/python/review/reviewers/common.py index 0d6688c4..d7385fe5 100644 --- a/src/python/review/reviewers/common.py +++ b/src/python/review/reviewers/common.py @@ -8,6 +8,7 @@ from src.python.review.inspectors.detekt.detekt import DetektInspector from src.python.review.inspectors.eslint.eslint import ESLintInspector from src.python.review.inspectors.flake8.flake8 import Flake8Inspector +from src.python.review.inspectors.radon.radon import RadonInspector from src.python.review.inspectors.issue import BaseIssue from src.python.review.inspectors.pmd.pmd import PMDInspector from src.python.review.inspectors.pyast.python_ast import PythonAstInspector @@ -24,6 +25,7 @@ PylintInspector(), Flake8Inspector(), PythonAstInspector(), + RadonInspector(), ], Language.JAVA: [ CheckstyleInspector(), @@ -36,7 +38,7 @@ ], Language.JS: [ ESLintInspector(), - ] + ], } @@ -76,12 +78,12 @@ def perform_language_review(metadata: Metadata, file_review_results.append(FileReviewResult( file_metadata.path, issues, - quality + quality, )) return ReviewResult( file_review_results, - general_quality + general_quality, ) diff --git a/src/python/review/reviewers/perform_review.py b/src/python/review/reviewers/perform_review.py index 678e503a..61f9b1a7 100644 --- a/src/python/review/reviewers/perform_review.py +++ b/src/python/review/reviewers/perform_review.py @@ -13,7 +13,7 @@ from src.python.review.reviewers.utils.print_review import ( print_review_result_as_json, print_review_result_as_multi_file_json, - print_review_result_as_text + print_review_result_as_text, ) logger: Final = logging.getLogger(__name__) @@ -31,7 +31,7 @@ class PathNotExists(Exception): Language.PYTHON: perform_python_review, Language.JAVA: partial(perform_language_review, language=Language.JAVA), Language.KOTLIN: partial(perform_language_review, language=Language.KOTLIN), - Language.JS: partial(perform_language_review, language=Language.JS) + Language.JS: partial(perform_language_review, language=Language.JS), } diff --git a/src/python/review/reviewers/utils/code_statistics.py b/src/python/review/reviewers/utils/code_statistics.py index 41dc25a2..d8dd7b38 100644 --- a/src/python/review/reviewers/utils/code_statistics.py +++ b/src/python/review/reviewers/utils/code_statistics.py @@ -17,6 +17,7 @@ class CodeStatistics: max_cyclomatic_complexity: int max_cohesion_lack: int + max_maintainability_lack: int max_func_len: int max_bool_expr_len: int @@ -40,6 +41,7 @@ def issue_type_to_statistics_dict(self) -> Dict[IssueType, int]: IssueType.CYCLOMATIC_COMPLEXITY: self.max_cyclomatic_complexity, IssueType.COHESION: self.max_cohesion_lack, + IssueType.MAINTAINABILITY: self.max_maintainability_lack, IssueType.FUNC_LEN: self.max_func_len, IssueType.BOOL_EXPR_LEN: self.max_bool_expr_len, @@ -73,7 +75,7 @@ def get_code_style_lines(issues: List[BaseIssue]) -> int: def __get_max_measure_by_issue_type(issue_type: IssueType, issues: List[BaseIssue]) -> int: return max(map( lambda issue: issue.measure(), - filter(lambda issue: issue.type == issue_type, issues) + filter(lambda issue: issue.type == issue_type, issues), ), default=0) @@ -85,6 +87,7 @@ def gather_code_statistics(issues: List[BaseIssue], path: Path) -> CodeStatistic func_lens = __get_max_measure_by_issue_type(IssueType.FUNC_LEN, issues) cyclomatic_complexities = __get_max_measure_by_issue_type(IssueType.CYCLOMATIC_COMPLEXITY, issues) cohesion_lacks = __get_max_measure_by_issue_type(IssueType.COHESION, issues) + maintainabilities = __get_max_measure_by_issue_type(IssueType.MAINTAINABILITY, issues) # Actually, we expect only one issue with each of the following metrics. inheritance_depths = __get_max_measure_by_issue_type(IssueType.INHERITANCE_DEPTH, issues) @@ -101,6 +104,7 @@ def gather_code_statistics(issues: List[BaseIssue], path: Path) -> CodeStatistic max_func_len=func_lens, n_line_len=issue_type_counter[IssueType.LINE_LEN], max_cohesion_lack=cohesion_lacks, + max_maintainability_lack=maintainabilities, max_cyclomatic_complexity=cyclomatic_complexities, inheritance_depth=inheritance_depths, class_response=class_responses, diff --git a/src/python/review/reviewers/utils/issues_filter.py b/src/python/review/reviewers/utils/issues_filter.py index ee9be2a1..a952998c 100644 --- a/src/python/review/reviewers/utils/issues_filter.py +++ b/src/python/review/reviewers/utils/issues_filter.py @@ -12,6 +12,7 @@ from src.python.review.quality.rules.method_number_scoring import LANGUAGE_TO_METHOD_NUMBER_RULE_CONFIG from src.python.review.quality.rules.weighted_methods_scoring import LANGUAGE_TO_WEIGHTED_METHODS_RULE_CONFIG from src.python.review.quality.rules.cohesion_scoring import LANGUAGE_TO_COHESION_RULE_CONFIG +from src.python.review.quality.rules.maintainability_scoring import LANGUAGE_TO_MAINTAINABILITY_RULE_CONFIG def __get_issue_type_to_low_measure_dict(language: Language) -> Dict[IssueType, int]: @@ -25,6 +26,9 @@ def __get_issue_type_to_low_measure_dict(language: Language) -> Dict[IssueType, IssueType.CLASS_RESPONSE: LANGUAGE_TO_RESPONSE_RULE_CONFIG[language].response_good, IssueType.WEIGHTED_METHOD: LANGUAGE_TO_WEIGHTED_METHODS_RULE_CONFIG[language].weighted_methods_good, IssueType.COHESION: LANGUAGE_TO_COHESION_RULE_CONFIG[language].cohesion_lack_good, + IssueType.MAINTAINABILITY: LANGUAGE_TO_MAINTAINABILITY_RULE_CONFIG[ + language + ].maintainability_lack_good, } diff --git a/src/python/review/reviewers/utils/print_review.py b/src/python/review/reviewers/utils/print_review.py index 76c2de04..9da0e35b 100644 --- a/src/python/review/reviewers/utils/print_review.py +++ b/src/python/review/reviewers/utils/print_review.py @@ -24,7 +24,7 @@ def print_review_result_as_text(review_result: ReviewResult, for issue in sorted_issues: line_text = linecache.getline( str(issue.file_path), - issue.line_no + issue.line_no, ).strip() print(f'{issue.line_no} : ' @@ -80,7 +80,7 @@ def print_review_result_as_multi_file_json(review_result: ReviewResult) -> None: 'file_name': str(file_review_result.file_path), 'quality': { 'code': quality_value, - 'text': f'Code quality (beta): {quality_value}' + 'text': f'Code quality (beta): {quality_value}', }, 'issues': [], } @@ -106,7 +106,7 @@ def print_review_result_as_multi_file_json(review_result: ReviewResult) -> None: 'code': quality_value, 'text': f'Code quality (beta): {quality_value}', }, - 'file_review_results': file_review_result_jsons + 'file_review_results': file_review_result_jsons, } print(json.dumps(output_json)) diff --git a/src/python/review/run_tool.py b/src/python/review/run_tool.py index d0ba0891..1b879972 100644 --- a/src/python/review/run_tool.py +++ b/src/python/review/run_tool.py @@ -155,7 +155,7 @@ def main() -> int: inspectors_config = { 'language_version': LanguageVersion(args.language_version) if args.language_version is not None else None, - 'n_cpu': n_cpu + 'n_cpu': n_cpu, } config = ApplicationConfig( diff --git a/test/python/functional_tests/conftest.py b/test/python/functional_tests/conftest.py index cec7b0a0..eea5a9ab 100644 --- a/test/python/functional_tests/conftest.py +++ b/test/python/functional_tests/conftest.py @@ -43,7 +43,7 @@ def build(self) -> List[str]: command.extend([ '--n_cpu', str(self.n_cpu), '-f', self.format, - str(self.path) + str(self.path), ]) if self.start_line is not None: diff --git a/test/python/functional_tests/test_different_languages.py b/test/python/functional_tests/test_different_languages.py index 0cfbf80d..935df678 100644 --- a/test/python/functional_tests/test_different_languages.py +++ b/test/python/functional_tests/test_different_languages.py @@ -10,7 +10,7 @@ def test_python(local_command: LocalCommandBuilder): process = subprocess.run( local_command.build(), stdout=subprocess.PIPE, - stderr=subprocess.PIPE + stderr=subprocess.PIPE, ) output = process.stdout.decode() @@ -26,7 +26,7 @@ def test_java(local_command: LocalCommandBuilder): process = subprocess.run( local_command.build(), stdout=subprocess.PIPE, - stderr=subprocess.PIPE + stderr=subprocess.PIPE, ) output = process.stdout.decode() @@ -42,7 +42,7 @@ def test_kotlin(local_command: LocalCommandBuilder): process = subprocess.run( local_command.build(), stdout=subprocess.PIPE, - stderr=subprocess.PIPE + stderr=subprocess.PIPE, ) output = process.stdout.decode() @@ -59,7 +59,7 @@ def test_all_java_inspectors(local_command: LocalCommandBuilder): process = subprocess.run( local_command.build(), stdout=subprocess.PIPE, - stderr=subprocess.PIPE + stderr=subprocess.PIPE, ) output = process.stdout.decode() diff --git a/test/python/functional_tests/test_disable.py b/test/python/functional_tests/test_disable.py index 08655c1e..ffacbec1 100644 --- a/test/python/functional_tests/test_disable.py +++ b/test/python/functional_tests/test_disable.py @@ -10,7 +10,7 @@ def test_disable_works(local_command: LocalCommandBuilder): process = subprocess.run( local_command.build(), stdout=subprocess.PIPE, - stderr=subprocess.PIPE + stderr=subprocess.PIPE, ) output = process.stdout.decode() @@ -20,7 +20,7 @@ def test_disable_works(local_command: LocalCommandBuilder): process = subprocess.run( local_command.build(), stdout=subprocess.PIPE, - stderr=subprocess.PIPE + stderr=subprocess.PIPE, ) output = process.stdout.decode() diff --git a/test/python/functional_tests/test_duplicates.py b/test/python/functional_tests/test_duplicates.py index 13ff2a83..153a3ac7 100644 --- a/test/python/functional_tests/test_duplicates.py +++ b/test/python/functional_tests/test_duplicates.py @@ -13,7 +13,7 @@ def test_allow_duplicates(local_command: LocalCommandBuilder): process = subprocess.run( local_command.build(), stdout=subprocess.PIPE, - stderr=subprocess.PIPE + stderr=subprocess.PIPE, ) stdout_allow_duplicates = process.stdout.decode() @@ -22,7 +22,7 @@ def test_allow_duplicates(local_command: LocalCommandBuilder): process = subprocess.run( local_command.build(), stdout=subprocess.PIPE, - stderr=subprocess.PIPE + stderr=subprocess.PIPE, ) stdout_filter_duplicates = process.stdout.decode() diff --git a/test/python/functional_tests/test_exit_code.py b/test/python/functional_tests/test_exit_code.py index 70584387..bd24d250 100644 --- a/test/python/functional_tests/test_exit_code.py +++ b/test/python/functional_tests/test_exit_code.py @@ -11,7 +11,7 @@ def test_exit_code_zero(local_command: LocalCommandBuilder): process = subprocess.run( local_command.build(), stdout=subprocess.PIPE, - stderr=subprocess.PIPE + stderr=subprocess.PIPE, ) assert process.returncode == 0 @@ -24,7 +24,7 @@ def test_exit_code_one(local_command: LocalCommandBuilder): process = subprocess.run( local_command.build(), stdout=subprocess.PIPE, - stderr=subprocess.PIPE + stderr=subprocess.PIPE, ) assert process.returncode == 1 @@ -37,7 +37,7 @@ def test_exit_code_two(local_command: LocalCommandBuilder): process = subprocess.run( local_command.build(), stdout=subprocess.PIPE, - stderr=subprocess.PIPE + stderr=subprocess.PIPE, ) assert process.returncode == 2 diff --git a/test/python/functional_tests/test_file_or_project.py b/test/python/functional_tests/test_file_or_project.py index 56bb1251..bd9d1d74 100644 --- a/test/python/functional_tests/test_file_or_project.py +++ b/test/python/functional_tests/test_file_or_project.py @@ -11,7 +11,7 @@ def test_inspect_file_works(local_command: LocalCommandBuilder): process = subprocess.run( local_command.build(), stdout=subprocess.PIPE, - stderr=subprocess.PIPE + stderr=subprocess.PIPE, ) output = process.stdout.decode() @@ -27,7 +27,7 @@ def test_inspect_project_works(local_command: LocalCommandBuilder): process = subprocess.run( local_command.build(), stdout=subprocess.PIPE, - stderr=subprocess.PIPE + stderr=subprocess.PIPE, ) output = process.stdout.decode() diff --git a/test/python/functional_tests/test_multi_file_project.py b/test/python/functional_tests/test_multi_file_project.py index db63472f..246f4f42 100644 --- a/test/python/functional_tests/test_multi_file_project.py +++ b/test/python/functional_tests/test_multi_file_project.py @@ -6,30 +6,30 @@ EXPECTED_JSON = { 'quality': { 'code': 'EXCELLENT', - 'text': 'Code quality (beta): EXCELLENT' + 'text': 'Code quality (beta): EXCELLENT', }, 'file_review_results': [ { 'file_name': '__init__.py', 'quality': { 'code': 'EXCELLENT', - 'text': 'Code quality (beta): EXCELLENT' + 'text': 'Code quality (beta): EXCELLENT', }, - 'issues': [] + 'issues': [], }, { 'file_name': 'one.py', 'quality': { 'code': 'EXCELLENT', - 'text': 'Code quality (beta): EXCELLENT' + 'text': 'Code quality (beta): EXCELLENT', }, - 'issues': [] + 'issues': [], }, { 'file_name': 'other.py', 'quality': { 'code': 'GOOD', - 'text': 'Code quality (beta): GOOD' + 'text': 'Code quality (beta): GOOD', }, 'issues': [ { @@ -38,7 +38,7 @@ 'line': 'a = 1', 'line_number': 2, 'column_number': 5, - 'category': 'BEST_PRACTICES' + 'category': 'BEST_PRACTICES', }, { 'code': 'W0612', @@ -46,7 +46,7 @@ 'line': 'b = 2', 'line_number': 3, 'column_number': 5, - 'category': 'BEST_PRACTICES' + 'category': 'BEST_PRACTICES', }, { 'code': 'W0612', @@ -54,11 +54,11 @@ 'line': 'c = 3', 'line_number': 4, 'column_number': 5, - 'category': 'BEST_PRACTICES' - } - ] + 'category': 'BEST_PRACTICES', + }, + ], }, - ] + ], } @@ -73,7 +73,7 @@ def test_json_format(local_command: LocalCommandBuilder): process = subprocess.run( local_command.build(), stdout=subprocess.PIPE, - stderr=subprocess.PIPE + stderr=subprocess.PIPE, ) stdout = process.stdout.decode() print(stdout) diff --git a/test/python/functional_tests/test_range_of_lines.py b/test/python/functional_tests/test_range_of_lines.py index bcadae0b..57b5530d 100644 --- a/test/python/functional_tests/test_range_of_lines.py +++ b/test/python/functional_tests/test_range_of_lines.py @@ -10,7 +10,7 @@ EXPECTED_JSON = { 'quality': { 'code': 'BAD', - 'text': 'Code quality (beta): BAD' + 'text': 'Code quality (beta): BAD', }, 'issues': [{ 'category': 'CODE_STYLE', @@ -30,16 +30,16 @@ 'column_number': 2, 'line': 'c=a + b', 'line_number': 4, - 'text': 'Exactly one space required around assignment' - } - ] + 'text': 'Exactly one space required around assignment', + }, + ], } NO_ISSUES_JSON = { 'quality': { 'code': 'EXCELLENT', 'text': 'Code quality (beta): EXCELLENT'}, - 'issues': [] + 'issues': [], } @@ -85,8 +85,8 @@ def test_range_filter_when_start_line_is_not_first( 'line': 'c=a + b', 'line_number': 4, 'column_number': 2, - 'category': 'CODE_STYLE' - }] + 'category': 'CODE_STYLE', + }], } assert output_json == expected_json_with_one_issue @@ -145,7 +145,7 @@ def test_range_filter_when_end_line_is_first( expected_json_with_one_issue = { 'quality': { 'code': 'MODERATE', - 'text': 'Code quality (beta): MODERATE' + 'text': 'Code quality (beta): MODERATE', }, 'issues': [{ 'code': 'C0326', @@ -153,8 +153,8 @@ def test_range_filter_when_end_line_is_first( 'line': 'a=10', 'line_number': 1, 'column_number': 2, - 'category': 'CODE_STYLE' - }] + 'category': 'CODE_STYLE', + }], } assert output_json == expected_json_with_one_issue @@ -211,7 +211,7 @@ def test_range_filter_when_both_start_and_end_lines_specified_not_equal_borders( expected_json = { 'quality': { 'code': 'BAD', - 'text': 'Code quality (beta): BAD' + 'text': 'Code quality (beta): BAD', }, 'issues': [{ 'code': 'C0326', @@ -219,15 +219,15 @@ def test_range_filter_when_both_start_and_end_lines_specified_not_equal_borders( 'line': 'b=20', 'line_number': 2, 'column_number': 2, - 'category': 'CODE_STYLE' + 'category': 'CODE_STYLE', }, { 'code': 'C0326', 'text': 'Exactly one space required around assignment', 'line': 'c=a + b', 'line_number': 4, 'column_number': 2, - 'category': 'CODE_STYLE' - }] + 'category': 'CODE_STYLE', + }], } assert output_json == expected_json diff --git a/test/python/functional_tests/test_single_file_json_format.py b/test/python/functional_tests/test_single_file_json_format.py index d71c115b..48d8f14d 100644 --- a/test/python/functional_tests/test_single_file_json_format.py +++ b/test/python/functional_tests/test_single_file_json_format.py @@ -12,8 +12,8 @@ 'type': 'object', 'properties': { 'code': {'type': 'string'}, - 'text': {'type': 'string'} - } + 'text': {'type': 'string'}, + }, }, 'issues': { 'type': 'array', @@ -25,11 +25,11 @@ 'column_number': {'type': 'number'}, 'line': {'type': 'string'}, 'line_number': {'type': 'number'}, - 'text': {'type': 'string'} - } - } - } - } + 'text': {'type': 'string'}, + }, + }, + }, + }, } @@ -43,7 +43,7 @@ def test_json_format(local_command: LocalCommandBuilder): process = subprocess.run( local_command.build(), stdout=subprocess.PIPE, - stderr=subprocess.PIPE + stderr=subprocess.PIPE, ) stdout = process.stdout.decode() diff --git a/test/python/functional_tests/test_verbosity.py b/test/python/functional_tests/test_verbosity.py index 67df44eb..bed69f90 100644 --- a/test/python/functional_tests/test_verbosity.py +++ b/test/python/functional_tests/test_verbosity.py @@ -13,7 +13,7 @@ def test_disable_logs_text(local_command: LocalCommandBuilder): process = subprocess.run( local_command.build(), stdout=subprocess.PIPE, - stderr=subprocess.PIPE + stderr=subprocess.PIPE, ) output = process.stdout.decode() output = output.lower() @@ -33,7 +33,7 @@ def test_disable_logs_json(local_command: LocalCommandBuilder): process = subprocess.run( local_command.build(), stdout=subprocess.PIPE, - stderr=subprocess.PIPE + stderr=subprocess.PIPE, ) output = process.stdout.decode() @@ -49,7 +49,7 @@ def test_enable_all_logs(local_command: LocalCommandBuilder): process = subprocess.run( local_command.build(), stdout=subprocess.PIPE, - stderr=subprocess.PIPE + stderr=subprocess.PIPE, ) output = process.stdout.decode() output = output.lower() diff --git a/test/python/inspectors/conftest.py b/test/python/inspectors/conftest.py index b2f8773c..d3df943c 100644 --- a/test/python/inspectors/conftest.py +++ b/test/python/inspectors/conftest.py @@ -31,16 +31,16 @@ def branch_info_response() -> Dict[str, Any]: 'reachability': 1, }, 'canCreateReview': { - 'isAllowed': True + 'isAllowed': True, }, 'stats': { 'parentBranch': 'bar', 'commitsAhead': 0, - 'commitsBehind': 0 + 'commitsBehind': 0, }, 'mergeInfo': {}, - 'isPullRequest': False - } + 'isPullRequest': False, + }, } @@ -52,15 +52,15 @@ def ownership_summary_response() -> Dict[str, Any]: { 'filePath': '/foo.py', 'state': 0, - 'userId': None + 'userId': None, }, { 'filePath': '/bar/baz.py', 'state': 0, - 'userId': None - } - ] - } + 'userId': None, + }, + ], + }, } @@ -74,6 +74,7 @@ class IssuesTestInfo: n_bool_expr_len: int = 0 n_other_complexity: int = 0 n_cohesion: int = 0 + n_maintainability: int = 0 def gather_issues_test_info(issues: List[BaseIssue]) -> IssuesTestInfo: @@ -88,6 +89,7 @@ def gather_issues_test_info(issues: List[BaseIssue]) -> IssuesTestInfo: n_bool_expr_len=counter[IssueType.BOOL_EXPR_LEN], n_other_complexity=counter[IssueType.COMPLEXITY], n_cohesion=counter[IssueType.COHESION], + n_maintainability=counter[IssueType.MAINTAINABILITY], ) diff --git a/test/python/inspectors/test_flake8_inspector.py b/test/python/inspectors/test_flake8_inspector.py index c4750ace..be211040 100644 --- a/test/python/inspectors/test_flake8_inspector.py +++ b/test/python/inspectors/test_flake8_inspector.py @@ -115,7 +115,7 @@ def test_choose_issue_type(): expected_issue_types = [ IssueType.ERROR_PRONE, IssueType.BEST_PRACTICES, IssueType.ERROR_PRONE, IssueType.BEST_PRACTICES, - IssueType.CODE_STYLE + IssueType.CODE_STYLE, ] issue_types = list(map(Flake8Inspector.choose_issue_type, error_codes)) diff --git a/test/python/inspectors/test_local_review.py b/test/python/inspectors/test_local_review.py index f32caab3..e1e028e6 100644 --- a/test/python/inspectors/test_local_review.py +++ b/test/python/inspectors/test_local_review.py @@ -14,7 +14,7 @@ 'allow_duplicates', 'disable', 'format', - 'handler' + 'handler', ]) @@ -24,7 +24,7 @@ def config() -> ApplicationConfig: disabled_inspectors={InspectorType.INTELLIJ}, allow_duplicates=False, n_cpu=1, - inspectors_config=dict(n_cpu=1) + inspectors_config=dict(n_cpu=1), ) diff --git a/test/python/inspectors/test_out_of_range_issues.py b/test/python/inspectors/test_out_of_range_issues.py index 016928b7..3f73d63d 100644 --- a/test/python/inspectors/test_out_of_range_issues.py +++ b/test/python/inspectors/test_out_of_range_issues.py @@ -44,7 +44,7 @@ def test_out_of_range_issues_when_the_same_borders() -> None: first_line_issues = [ create_code_issue_by_line(1), create_code_issue_by_line(1), - create_code_issue_by_line(1) + create_code_issue_by_line(1), ] assert filter_out_of_range_issues(first_line_issues, diff --git a/test/python/inspectors/test_pylint_inspector.py b/test/python/inspectors/test_pylint_inspector.py index e142e8aa..ab807765 100644 --- a/test/python/inspectors/test_pylint_inspector.py +++ b/test/python/inspectors/test_pylint_inspector.py @@ -72,7 +72,7 @@ def test_choose_issue_type(): IssueType.BEST_PRACTICES, IssueType.BEST_PRACTICES, IssueType.CODE_STYLE, - IssueType.ERROR_PRONE + IssueType.ERROR_PRONE, ] issue_types = list( diff --git a/test/python/inspectors/test_radon_inspector.py b/test/python/inspectors/test_radon_inspector.py new file mode 100644 index 00000000..72fd03e7 --- /dev/null +++ b/test/python/inspectors/test_radon_inspector.py @@ -0,0 +1,52 @@ +from textwrap import dedent + +import pytest + +from src.python.review.common.language import Language +from src.python.review.inspectors.issue import IssueType +from src.python.review.inspectors.radon.radon import RadonInspector +from src.python.review.reviewers.utils.issues_filter import filter_low_measure_issues +from test.python.inspectors import PYTHON_DATA_FOLDER +from test.python.inspectors.conftest import use_file_metadata +from src.python.review.inspectors.tips import get_maintainability_index_tip + +FILE_NAMES_AND_N_ISSUES = [ + ("case13_complex_logic.py", 1), + ("case13_complex_logic_2.py", 1), + ("case8_good_class.py", 0), +] + + +@pytest.mark.parametrize(("file_name", "n_issues"), FILE_NAMES_AND_N_ISSUES) +def test_file_with_issues(file_name: str, n_issues: int): + inspector = RadonInspector() + + path_to_file = PYTHON_DATA_FOLDER / file_name + with use_file_metadata(path_to_file) as file_metadata: + issues = inspector.inspect(file_metadata.path, {}) + issues = filter_low_measure_issues(issues, Language.PYTHON) + + assert len(issues) == n_issues + + +def test_mi_parse(): + file_name = "test.py" + output = f"""\ + {file_name} - C (4.32) + {file_name} - B (13.7) + {file_name} - A (70.0) + """ + output = dedent(output) + + issues = RadonInspector.mi_parse(output) + + assert all(str(issue.file_path) == file_name for issue in issues) + assert [issue.line_no for issue in issues] == [1, 1, 1] + assert [issue.column_no for issue in issues] == [1, 1, 1] + assert [issue.description for issue in issues] == [get_maintainability_index_tip()] * 3 + assert [issue.type for issue in issues] == [ + IssueType.MAINTAINABILITY, + IssueType.MAINTAINABILITY, + IssueType.MAINTAINABILITY, + ] + assert [issue.maintainability_lack for issue in issues] == [95, 86, 30] From 388f2fad8f65e77e4312cf78c2d691a592f19b5e Mon Sep 17 00:00:00 2001 From: Ilya Vlasov <55441714+GirZ0n@users.noreply.github.com> Date: Wed, 14 Apr 2021 13:41:10 +0500 Subject: [PATCH 04/36] Requirements upgrade (#21) Updated versions of dependencies Added django dictionary support Fixed tests --- .github/workflows/build.yml | 2 +- requirements-test.txt | 10 +++--- requirements.txt | 24 +++++++------- src/python/review/application_config.py | 2 +- src/python/review/common/file_system.py | 2 +- src/python/review/inspectors/flake8/.flake8 | 3 ++ src/python/review/inspectors/flake8/flake8.py | 6 ++-- .../intellij/issue_types/__init__.py | 5 +-- .../inspectors/parsers/checkstyle_parser.py | 6 ++-- src/python/review/inspectors/radon/radon.py | 2 +- .../inspectors/springlint/springlint.py | 6 ++-- src/python/review/quality/evaluate_quality.py | 16 +++++----- src/python/review/quality/model.py | 2 +- .../review/quality/rules/cohesion_scoring.py | 2 +- .../quality/rules/maintainability_scoring.py | 2 +- src/python/review/reviewers/common.py | 2 +- src/python/review/reviewers/python.py | 2 +- .../review/reviewers/utils/code_statistics.py | 2 +- .../review/reviewers/utils/issues_filter.py | 4 +-- src/python/review/run_tool.py | 3 +- test/python/functional_tests/conftest.py | 3 +- .../test_different_languages.py | 1 - test/python/functional_tests/test_disable.py | 1 - .../functional_tests/test_duplicates.py | 1 - .../python/functional_tests/test_exit_code.py | 1 - .../functional_tests/test_file_or_project.py | 1 - .../test_multi_file_project.py | 1 - .../functional_tests/test_range_of_lines.py | 31 +++++++++---------- .../test_single_file_json_format.py | 3 +- .../python/functional_tests/test_verbosity.py | 1 - test/python/inspectors/conftest.py | 1 - .../inspectors/test_checkstyle_inspector.py | 6 ++-- .../inspectors/test_detekt_inspector.py | 6 ++-- .../inspectors/test_eslint_inspector.py | 6 ++-- .../inspectors/test_flake8_inspector.py | 8 ++--- test/python/inspectors/test_local_review.py | 5 ++- test/python/inspectors/test_pmd_inspector.py | 5 +-- .../inspectors/test_pylint_inspector.py | 8 ++--- test/python/inspectors/test_python_ast.py | 7 ++--- .../python/inspectors/test_radon_inspector.py | 8 ++--- .../inspectors/test_springlint_inspector.py | 3 +- .../python/case18_comprehensions.py | 2 +- whitelist.txt | 21 +++++++++++-- 43 files changed, 116 insertions(+), 117 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4cc2a22b..c08b6e9f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,7 +27,7 @@ jobs: # stop the build if there are Python syntax errors or undefined names flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics --exclude=.git,__pycache__,docs/source/conf.py,old,build,dist,venv,test/resources,.eggs,review.egg-info,.pytest_cache,node_modules # TODO: change max-complexity into 10 after refactoring - flake8 . --count --max-complexity=11 --max-line-length=120 --max-doc-length=120 --ignore=I201,I202,I101,I100,R504,A003,E800,SC200,SC100,E402,W503,WPS,H601 --statistics --exclude=.git,__pycache__,docs/source/conf.py,old,build,dist,venv,test/resources,.eggs,review.egg-info,.pytest_cache,node_modules + flake8 . --count --max-complexity=11 --max-line-length=120 --max-doc-length=120 --ignore=R504,A003,E800,E402,W503,WPS,H601 --statistics --exclude=.git,__pycache__,docs/source/conf.py,old,build,dist,venv,test/resources,.eggs,review.egg-info,.pytest_cache,node_modules - name: Set up Eslint run: | npm install eslint --save-dev diff --git a/requirements-test.txt b/requirements-test.txt index d0ec886e..4a15ceb8 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,8 +1,8 @@ -pytest~=5.4.3 +pytest~=6.2.3 pytest-runner pytest-subtests jsonschema==3.2.0 -Django~=3.0.8 -pylint~=2.5.3 -requests~=2.24.0 -setuptools~=47.3.1 \ No newline at end of file +Django~=3.2 +pylint~=2.7.4 +requests~=2.25.1 +setuptools~=56.0.0 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 60409ee1..d5a07c54 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,20 +1,20 @@ -setuptools==47.3.1 +setuptools==56.0.0 # python code analysis tools -pylint==2.5.3 -pylint-django==2.0.15 -flake8==3.8.3 +pylint==2.7.4 +pylint-django==2.3.0 +flake8==3.9.0 # flake8 plugins -flake8-bugbear==20.1.4 +flake8-bugbear==21.4.3 flake8-builtins==1.5.3 -flake8-comprehensions==3.2.3 -flake8-eradicate==0.4.0 +flake8-comprehensions==3.4.0 +flake8-eradicate==1.0.0 flake8-import-order==0.18.1 -flake8-plugin-utils==1.3.0 +flake8-plugin-utils==1.3.1 flake8-polyfill==1.0.2 -flake8-return==1.1.1 -flake8-spellcheck==0.14.0 +flake8-return==1.1.2 +flake8-spellcheck==0.24.0 mccabe==0.6.1 pep8-naming==0.11.1 wps-light==0.15.2 @@ -25,5 +25,5 @@ cohesion==1.0.0 radon==4.5.0 # extra libraries and frameworks -django==3.0.8 -requests==2.24.0 \ No newline at end of file +django==3.2 +requests==2.25.1 diff --git a/src/python/review/application_config.py b/src/python/review/application_config.py index c678605f..41a1c296 100644 --- a/src/python/review/application_config.py +++ b/src/python/review/application_config.py @@ -1,6 +1,6 @@ from dataclasses import dataclass from enum import Enum, unique -from typing import Optional, Set, List +from typing import List, Optional, Set from src.python.review.inspectors.inspector_type import InspectorType diff --git a/src/python/review/common/file_system.py b/src/python/review/common/file_system.py index 3faa962c..764a2d5e 100644 --- a/src/python/review/common/file_system.py +++ b/src/python/review/common/file_system.py @@ -4,7 +4,7 @@ from contextlib import contextmanager from enum import Enum, unique from pathlib import Path -from typing import List, Union, Callable +from typing import Callable, List, Union @unique diff --git a/src/python/review/inspectors/flake8/.flake8 b/src/python/review/inspectors/flake8/.flake8 index cc3e69cb..2ad4f70f 100644 --- a/src/python/review/inspectors/flake8/.flake8 +++ b/src/python/review/inspectors/flake8/.flake8 @@ -1,5 +1,8 @@ [flake8] disable_noqa=True + +dictionaries=en_US,python,technical,django + ignore=W291, # trailing whitespaces W292, # no newline at end of file W293, # blank line contains whitespaces diff --git a/src/python/review/inspectors/flake8/flake8.py b/src/python/review/inspectors/flake8/flake8.py index b974015c..5745a75f 100644 --- a/src/python/review/inspectors/flake8/flake8.py +++ b/src/python/review/inspectors/flake8/flake8.py @@ -5,18 +5,18 @@ from src.python.review.common.subprocess_runner import run_in_subprocess from src.python.review.inspectors.base_inspector import BaseInspector +from src.python.review.inspectors.common import convert_percentage_of_value_to_lack_of_value from src.python.review.inspectors.flake8.issue_types import CODE_PREFIX_TO_ISSUE_TYPE, CODE_TO_ISSUE_TYPE from src.python.review.inspectors.inspector_type import InspectorType from src.python.review.inspectors.issue import ( BaseIssue, CodeIssue, + CohesionIssue, CyclomaticComplexityIssue, - IssueType, IssueData, - CohesionIssue, + IssueType, ) from src.python.review.inspectors.tips import get_cyclomatic_complexity_tip -from src.python.review.inspectors.common import convert_percentage_of_value_to_lack_of_value logger = logging.getLogger(__name__) diff --git a/src/python/review/inspectors/intellij/issue_types/__init__.py b/src/python/review/inspectors/intellij/issue_types/__init__.py index 3482d8b4..97b8d31e 100644 --- a/src/python/review/inspectors/intellij/issue_types/__init__.py +++ b/src/python/review/inspectors/intellij/issue_types/__init__.py @@ -1,18 +1,15 @@ from typing import Dict -from src.python.review.inspectors.issue import IssueType - from src.python.review.inspectors.intellij.issue_types.java import ( ISSUE_CLASS_TO_ISSUE_TYPE as JAVA_ISSUE_CLASS_TO_ISSUE_TYPE, ) - from src.python.review.inspectors.intellij.issue_types.kotlin import ( ISSUE_CLASS_TO_ISSUE_TYPE as KOTLIN_ISSUE_CLASS_TO_ISSUE_TYPE, ) - from src.python.review.inspectors.intellij.issue_types.python import ( ISSUE_CLASS_TO_ISSUE_TYPE as PYTHON_ISSUE_CLASS_TO_ISSUE_TYPE, ) +from src.python.review.inspectors.issue import IssueType ISSUE_CLASS_TO_ISSUE_TYPE: Dict[str, IssueType] = { **JAVA_ISSUE_CLASS_TO_ISSUE_TYPE, diff --git a/src/python/review/inspectors/parsers/checkstyle_parser.py b/src/python/review/inspectors/parsers/checkstyle_parser.py index cae3593f..1db9874c 100644 --- a/src/python/review/inspectors/parsers/checkstyle_parser.py +++ b/src/python/review/inspectors/parsers/checkstyle_parser.py @@ -1,23 +1,21 @@ import logging import re from pathlib import Path -from typing import Callable, Dict, List, Any, Optional +from typing import Any, Callable, Dict, List, Optional from xml.etree import ElementTree from src.python.review.common.file_system import get_content_from_file from src.python.review.inspectors.inspector_type import InspectorType - from src.python.review.inspectors.issue import ( BaseIssue, BoolExprLenIssue, CodeIssue, CyclomaticComplexityIssue, FuncLenIssue, + IssueData, IssueType, LineLenIssue, - IssueData, ) - from src.python.review.inspectors.tips import ( get_bool_expr_len_tip, get_cyclomatic_complexity_tip, diff --git a/src/python/review/inspectors/radon/radon.py b/src/python/review/inspectors/radon/radon.py index fb2bf299..f6a2236d 100644 --- a/src/python/review/inspectors/radon/radon.py +++ b/src/python/review/inspectors/radon/radon.py @@ -4,9 +4,9 @@ from src.python.review.common.subprocess_runner import run_in_subprocess from src.python.review.inspectors.base_inspector import BaseInspector +from src.python.review.inspectors.common import convert_percentage_of_value_to_lack_of_value from src.python.review.inspectors.inspector_type import InspectorType from src.python.review.inspectors.issue import BaseIssue, IssueData, IssueType, MaintainabilityLackIssue -from src.python.review.inspectors.common import convert_percentage_of_value_to_lack_of_value from src.python.review.inspectors.tips import get_maintainability_index_tip diff --git a/src/python/review/inspectors/springlint/springlint.py b/src/python/review/inspectors/springlint/springlint.py index cc90c487..b5eca5a4 100644 --- a/src/python/review/inspectors/springlint/springlint.py +++ b/src/python/review/inspectors/springlint/springlint.py @@ -3,13 +3,12 @@ import re from pathlib import Path from shutil import copy -from typing import AnyStr, List, Optional, Dict, Any +from typing import Any, AnyStr, Dict, List, Optional from src.python.review.common.file_system import new_temp_dir from src.python.review.common.subprocess_runner import run_in_subprocess from src.python.review.inspectors.base_inspector import BaseInspector from src.python.review.inspectors.inspector_type import InspectorType - from src.python.review.inspectors.issue import ( BaseIssue, ChildrenNumberIssue, @@ -18,12 +17,11 @@ CohesionIssue, CouplingIssue, InheritanceIssue, + IssueData, IssueType, MethodNumberIssue, WeightedMethodIssue, - IssueData, ) - from src.python.review.inspectors.tips import ( get_child_number_tip, get_class_coupling_tip, diff --git a/src/python/review/quality/evaluate_quality.py b/src/python/review/quality/evaluate_quality.py index 3861d3b3..b6329653 100644 --- a/src/python/review/quality/evaluate_quality.py +++ b/src/python/review/quality/evaluate_quality.py @@ -13,6 +13,10 @@ ) from src.python.review.quality.rules.class_response_scoring import LANGUAGE_TO_RESPONSE_RULE_CONFIG, ResponseRule from src.python.review.quality.rules.code_style_scoring import CodeStyleRule, LANGUAGE_TO_CODE_STYLE_RULE_CONFIG +from src.python.review.quality.rules.cohesion_scoring import ( + CohesionRule, + LANGUAGE_TO_COHESION_RULE_CONFIG, +) from src.python.review.quality.rules.coupling_scoring import CouplingRule, LANGUAGE_TO_COUPLING_RULE_CONFIG from src.python.review.quality.rules.cyclomatic_complexity_scoring import ( CyclomaticComplexityRule, @@ -28,6 +32,10 @@ LANGUAGE_TO_INHERITANCE_DEPTH_RULE_CONFIG, ) from src.python.review.quality.rules.line_len_scoring import LANGUAGE_TO_LINE_LENGTH_RULE_CONFIG, LineLengthRule +from src.python.review.quality.rules.maintainability_scoring import ( + LANGUAGE_TO_MAINTAINABILITY_RULE_CONFIG, + MaintainabilityRule, +) from src.python.review.quality.rules.method_number_scoring import ( LANGUAGE_TO_METHOD_NUMBER_RULE_CONFIG, MethodNumberRule, @@ -37,14 +45,6 @@ WeightedMethodsRule, ) from src.python.review.reviewers.utils.code_statistics import CodeStatistics -from src.python.review.quality.rules.cohesion_scoring import ( - LANGUAGE_TO_COHESION_RULE_CONFIG, - CohesionRule, -) -from src.python.review.quality.rules.maintainability_scoring import ( - LANGUAGE_TO_MAINTAINABILITY_RULE_CONFIG, - MaintainabilityRule, -) def __get_available_rules(language: Language) -> List[Rule]: diff --git a/src/python/review/quality/model.py b/src/python/review/quality/model.py index 6e64ea81..7a57dcf6 100644 --- a/src/python/review/quality/model.py +++ b/src/python/review/quality/model.py @@ -50,7 +50,7 @@ def quality_type(self) -> QualityType: def next_quality_type(self) -> QualityType: return min(map(lambda rule: rule.next_level_type, self.rules), default=QualityType.EXCELLENT) - # TODO@nbirillo: why rule.quality_type == quality_type for next level???? + # TODO: why rule.quality_type == quality_type for next level???? @property def next_level_requirements(self) -> List[Rule]: quality_type = self.quality_type diff --git a/src/python/review/quality/rules/cohesion_scoring.py b/src/python/review/quality/rules/cohesion_scoring.py index c73a426b..623fd2db 100644 --- a/src/python/review/quality/rules/cohesion_scoring.py +++ b/src/python/review/quality/rules/cohesion_scoring.py @@ -3,7 +3,7 @@ from src.python.review.common.language import Language from src.python.review.inspectors.issue import IssueType -from src.python.review.quality.model import Rule, QualityType +from src.python.review.quality.model import QualityType, Rule @dataclass diff --git a/src/python/review/quality/rules/maintainability_scoring.py b/src/python/review/quality/rules/maintainability_scoring.py index 456e7b0a..ade6fd4b 100644 --- a/src/python/review/quality/rules/maintainability_scoring.py +++ b/src/python/review/quality/rules/maintainability_scoring.py @@ -3,7 +3,7 @@ from src.python.review.common.language import Language from src.python.review.inspectors.issue import IssueType -from src.python.review.quality.model import Rule, QualityType +from src.python.review.quality.model import QualityType, Rule @dataclass diff --git a/src/python/review/reviewers/common.py b/src/python/review/reviewers/common.py index d7385fe5..9340d824 100644 --- a/src/python/review/reviewers/common.py +++ b/src/python/review/reviewers/common.py @@ -8,11 +8,11 @@ from src.python.review.inspectors.detekt.detekt import DetektInspector from src.python.review.inspectors.eslint.eslint import ESLintInspector from src.python.review.inspectors.flake8.flake8 import Flake8Inspector -from src.python.review.inspectors.radon.radon import RadonInspector from src.python.review.inspectors.issue import BaseIssue from src.python.review.inspectors.pmd.pmd import PMDInspector from src.python.review.inspectors.pyast.python_ast import PythonAstInspector from src.python.review.inspectors.pylint.pylint import PylintInspector +from src.python.review.inspectors.radon.radon import RadonInspector from src.python.review.quality.evaluate_quality import evaluate_quality from src.python.review.quality.model import Quality from src.python.review.reviewers.review_result import FileReviewResult, ReviewResult diff --git a/src/python/review/reviewers/python.py b/src/python/review/reviewers/python.py index a239913a..d51b526a 100644 --- a/src/python/review/reviewers/python.py +++ b/src/python/review/reviewers/python.py @@ -2,7 +2,7 @@ from typing import List from src.python.review.application_config import ApplicationConfig -from src.python.review.common.file_system import get_all_file_system_items, FileSystemItem +from src.python.review.common.file_system import FileSystemItem, get_all_file_system_items from src.python.review.common.language import Language from src.python.review.reviewers.common import perform_language_review from src.python.review.reviewers.review_result import ReviewResult diff --git a/src/python/review/reviewers/utils/code_statistics.py b/src/python/review/reviewers/utils/code_statistics.py index d8dd7b38..19218a7b 100644 --- a/src/python/review/reviewers/utils/code_statistics.py +++ b/src/python/review/reviewers/utils/code_statistics.py @@ -1,7 +1,7 @@ from collections import Counter from dataclasses import dataclass from pathlib import Path -from typing import List, Dict +from typing import Dict, List from src.python.review.common.file_system import get_content_from_file from src.python.review.inspectors.issue import BaseIssue, IssueType diff --git a/src/python/review/reviewers/utils/issues_filter.py b/src/python/review/reviewers/utils/issues_filter.py index a952998c..b6e5529e 100644 --- a/src/python/review/reviewers/utils/issues_filter.py +++ b/src/python/review/reviewers/utils/issues_filter.py @@ -5,14 +5,14 @@ from src.python.review.inspectors.issue import BaseIssue, IssueType, Measurable from src.python.review.quality.rules.boolean_length_scoring import LANGUAGE_TO_BOOLEAN_EXPRESSION_RULE_CONFIG from src.python.review.quality.rules.class_response_scoring import LANGUAGE_TO_RESPONSE_RULE_CONFIG +from src.python.review.quality.rules.cohesion_scoring import LANGUAGE_TO_COHESION_RULE_CONFIG from src.python.review.quality.rules.coupling_scoring import LANGUAGE_TO_COUPLING_RULE_CONFIG from src.python.review.quality.rules.cyclomatic_complexity_scoring import LANGUAGE_TO_CYCLOMATIC_COMPLEXITY_RULE_CONFIG from src.python.review.quality.rules.function_length_scoring import LANGUAGE_TO_FUNCTION_LENGTH_RULE_CONFIG from src.python.review.quality.rules.inheritance_depth_scoring import LANGUAGE_TO_INHERITANCE_DEPTH_RULE_CONFIG +from src.python.review.quality.rules.maintainability_scoring import LANGUAGE_TO_MAINTAINABILITY_RULE_CONFIG from src.python.review.quality.rules.method_number_scoring import LANGUAGE_TO_METHOD_NUMBER_RULE_CONFIG from src.python.review.quality.rules.weighted_methods_scoring import LANGUAGE_TO_WEIGHTED_METHODS_RULE_CONFIG -from src.python.review.quality.rules.cohesion_scoring import LANGUAGE_TO_COHESION_RULE_CONFIG -from src.python.review.quality.rules.maintainability_scoring import LANGUAGE_TO_MAINTAINABILITY_RULE_CONFIG def __get_issue_type_to_low_measure_dict(language: Language) -> Dict[IssueType, int]: diff --git a/src/python/review/run_tool.py b/src/python/review/run_tool.py index 1b879972..0d96f6e8 100644 --- a/src/python/review/run_tool.py +++ b/src/python/review/run_tool.py @@ -5,7 +5,7 @@ import traceback from enum import Enum, unique from pathlib import Path -from typing import Set, List +from typing import List, Set sys.path.append('') sys.path.append('../../..') @@ -13,7 +13,6 @@ from src.python.review.application_config import ApplicationConfig, LanguageVersion from src.python.review.inspectors.inspector_type import InspectorType from src.python.review.logging_config import logging_config - from src.python.review.reviewers.perform_review import ( OutputFormat, PathNotExists, diff --git a/test/python/functional_tests/conftest.py b/test/python/functional_tests/conftest.py index eea5a9ab..da01adde 100644 --- a/test/python/functional_tests/conftest.py +++ b/test/python/functional_tests/conftest.py @@ -1,11 +1,10 @@ from dataclasses import dataclass, field from pathlib import Path +from test.python import TEST_DATA_FOLDER from typing import List, Optional import pytest - from src.python import MAIN_FOLDER -from test.python import TEST_DATA_FOLDER DATA_PATH = TEST_DATA_FOLDER / 'functional_tests' diff --git a/test/python/functional_tests/test_different_languages.py b/test/python/functional_tests/test_different_languages.py index 935df678..c55e56f9 100644 --- a/test/python/functional_tests/test_different_languages.py +++ b/test/python/functional_tests/test_different_languages.py @@ -1,5 +1,4 @@ import subprocess - from test.python.functional_tests.conftest import DATA_PATH, LocalCommandBuilder diff --git a/test/python/functional_tests/test_disable.py b/test/python/functional_tests/test_disable.py index ffacbec1..7967e1bc 100644 --- a/test/python/functional_tests/test_disable.py +++ b/test/python/functional_tests/test_disable.py @@ -1,5 +1,4 @@ import subprocess - from test.python.functional_tests.conftest import DATA_PATH, LocalCommandBuilder diff --git a/test/python/functional_tests/test_duplicates.py b/test/python/functional_tests/test_duplicates.py index 153a3ac7..9158e030 100644 --- a/test/python/functional_tests/test_duplicates.py +++ b/test/python/functional_tests/test_duplicates.py @@ -1,6 +1,5 @@ import re import subprocess - from test.python.functional_tests.conftest import DATA_PATH, LocalCommandBuilder diff --git a/test/python/functional_tests/test_exit_code.py b/test/python/functional_tests/test_exit_code.py index bd24d250..4d5dad1a 100644 --- a/test/python/functional_tests/test_exit_code.py +++ b/test/python/functional_tests/test_exit_code.py @@ -1,6 +1,5 @@ import subprocess from pathlib import Path - from test.python.functional_tests.conftest import DATA_PATH, LocalCommandBuilder diff --git a/test/python/functional_tests/test_file_or_project.py b/test/python/functional_tests/test_file_or_project.py index bd9d1d74..074c77aa 100644 --- a/test/python/functional_tests/test_file_or_project.py +++ b/test/python/functional_tests/test_file_or_project.py @@ -1,5 +1,4 @@ import subprocess - from test.python.functional_tests.conftest import DATA_PATH, LocalCommandBuilder diff --git a/test/python/functional_tests/test_multi_file_project.py b/test/python/functional_tests/test_multi_file_project.py index 246f4f42..86a029d1 100644 --- a/test/python/functional_tests/test_multi_file_project.py +++ b/test/python/functional_tests/test_multi_file_project.py @@ -1,6 +1,5 @@ import json import subprocess - from test.python.functional_tests.conftest import DATA_PATH, LocalCommandBuilder EXPECTED_JSON = { diff --git a/test/python/functional_tests/test_range_of_lines.py b/test/python/functional_tests/test_range_of_lines.py index 57b5530d..1ef4a8f5 100644 --- a/test/python/functional_tests/test_range_of_lines.py +++ b/test/python/functional_tests/test_range_of_lines.py @@ -1,9 +1,8 @@ import json +from test.python.functional_tests.conftest import DATA_PATH, LocalCommandBuilder import pytest - from src.python.review.common.subprocess_runner import run_in_subprocess -from test.python.functional_tests.conftest import DATA_PATH, LocalCommandBuilder PATH_TO_FILE = DATA_PATH / 'lines_range' / 'code_with_multiple_issues.py' @@ -14,23 +13,23 @@ }, 'issues': [{ 'category': 'CODE_STYLE', - 'code': 'C0326', + 'code': 'E225', 'column_number': 2, 'line': 'a=10', 'line_number': 1, - 'text': 'Exactly one space required around assignment'}, + 'text': 'missing whitespace around operator'}, {'category': 'CODE_STYLE', - 'code': 'C0326', + 'code': 'E225', 'column_number': 2, 'line': 'b=20', 'line_number': 2, - 'text': 'Exactly one space required around assignment'}, + 'text': 'missing whitespace around operator'}, {'category': 'CODE_STYLE', - 'code': 'C0326', + 'code': 'E225', 'column_number': 2, 'line': 'c=a + b', 'line_number': 4, - 'text': 'Exactly one space required around assignment', + 'text': 'missing whitespace around operator', }, ], } @@ -80,8 +79,8 @@ def test_range_filter_when_start_line_is_not_first( 'code': 'MODERATE', 'text': 'Code quality (beta): MODERATE'}, 'issues': [{ - 'code': 'C0326', - 'text': 'Exactly one space required around assignment', + 'code': 'E225', + 'text': 'missing whitespace around operator', 'line': 'c=a + b', 'line_number': 4, 'column_number': 2, @@ -148,8 +147,8 @@ def test_range_filter_when_end_line_is_first( 'text': 'Code quality (beta): MODERATE', }, 'issues': [{ - 'code': 'C0326', - 'text': 'Exactly one space required around assignment', + 'code': 'E225', + 'text': 'missing whitespace around operator', 'line': 'a=10', 'line_number': 1, 'column_number': 2, @@ -214,15 +213,15 @@ def test_range_filter_when_both_start_and_end_lines_specified_not_equal_borders( 'text': 'Code quality (beta): BAD', }, 'issues': [{ - 'code': 'C0326', - 'text': 'Exactly one space required around assignment', + 'code': 'E225', + 'text': 'missing whitespace around operator', 'line': 'b=20', 'line_number': 2, 'column_number': 2, 'category': 'CODE_STYLE', }, { - 'code': 'C0326', - 'text': 'Exactly one space required around assignment', + 'code': 'E225', + 'text': 'missing whitespace around operator', 'line': 'c=a + b', 'line_number': 4, 'column_number': 2, diff --git a/test/python/functional_tests/test_single_file_json_format.py b/test/python/functional_tests/test_single_file_json_format.py index 48d8f14d..85616145 100644 --- a/test/python/functional_tests/test_single_file_json_format.py +++ b/test/python/functional_tests/test_single_file_json_format.py @@ -1,10 +1,9 @@ import json import subprocess +from test.python.functional_tests.conftest import DATA_PATH, LocalCommandBuilder from jsonschema import validate -from test.python.functional_tests.conftest import DATA_PATH, LocalCommandBuilder - schema = { 'type': 'object', 'properties': { diff --git a/test/python/functional_tests/test_verbosity.py b/test/python/functional_tests/test_verbosity.py index bed69f90..e835258b 100644 --- a/test/python/functional_tests/test_verbosity.py +++ b/test/python/functional_tests/test_verbosity.py @@ -1,6 +1,5 @@ import json import subprocess - from test.python.functional_tests.conftest import DATA_PATH, LocalCommandBuilder diff --git a/test/python/inspectors/conftest.py b/test/python/inspectors/conftest.py index d3df943c..f9072db7 100644 --- a/test/python/inspectors/conftest.py +++ b/test/python/inspectors/conftest.py @@ -5,7 +5,6 @@ from typing import Any, Dict, List import pytest - from src.python.review.common.file_system import new_temp_dir from src.python.review.inspectors.issue import BaseIssue, IssueType from src.python.review.reviewers.utils.metadata_exploration import explore_file, FileMetadata diff --git a/test/python/inspectors/test_checkstyle_inspector.py b/test/python/inspectors/test_checkstyle_inspector.py index 892ec97f..ce7040aa 100644 --- a/test/python/inspectors/test_checkstyle_inspector.py +++ b/test/python/inspectors/test_checkstyle_inspector.py @@ -1,10 +1,10 @@ -import pytest +from test.python.inspectors import JAVA_DATA_FOLDER +from test.python.inspectors.conftest import gather_issues_test_info, IssuesTestInfo, use_file_metadata +import pytest from src.python.review.common.language import Language from src.python.review.inspectors.checkstyle.checkstyle import CheckstyleInspector from src.python.review.reviewers.utils.issues_filter import filter_low_measure_issues -from test.python.inspectors import JAVA_DATA_FOLDER -from test.python.inspectors.conftest import gather_issues_test_info, IssuesTestInfo, use_file_metadata FILE_NAMES_AND_N_ISSUES = [ ('test_simple_valid_program.java', 0), diff --git a/test/python/inspectors/test_detekt_inspector.py b/test/python/inspectors/test_detekt_inspector.py index 4dbafd2d..61d05200 100644 --- a/test/python/inspectors/test_detekt_inspector.py +++ b/test/python/inspectors/test_detekt_inspector.py @@ -1,10 +1,10 @@ -import pytest +from test.python.inspectors import KOTLIN_DATA_FOLDER +from test.python.inspectors.conftest import use_file_metadata +import pytest from src.python.review.common.language import Language from src.python.review.inspectors.detekt.detekt import DetektInspector from src.python.review.reviewers.utils.issues_filter import filter_low_measure_issues -from test.python.inspectors import KOTLIN_DATA_FOLDER -from test.python.inspectors.conftest import use_file_metadata FILE_NAMES_AND_N_ISSUES = [ ('case0_good_program.kt', 0), diff --git a/test/python/inspectors/test_eslint_inspector.py b/test/python/inspectors/test_eslint_inspector.py index 076c76a0..a69f30e5 100644 --- a/test/python/inspectors/test_eslint_inspector.py +++ b/test/python/inspectors/test_eslint_inspector.py @@ -1,10 +1,10 @@ -import pytest +from test.python.inspectors import JS_DATA_FOLDER +from test.python.inspectors.conftest import use_file_metadata +import pytest from src.python.review.common.language import Language from src.python.review.inspectors.eslint.eslint import ESLintInspector from src.python.review.reviewers.utils.issues_filter import filter_low_measure_issues -from test.python.inspectors import JS_DATA_FOLDER -from test.python.inspectors.conftest import use_file_metadata FILE_NAMES_AND_N_ISSUES = [ ('case0_no_issues.js', 0), diff --git a/test/python/inspectors/test_flake8_inspector.py b/test/python/inspectors/test_flake8_inspector.py index be211040..3208d633 100644 --- a/test/python/inspectors/test_flake8_inspector.py +++ b/test/python/inspectors/test_flake8_inspector.py @@ -1,12 +1,12 @@ -import pytest +from test.python.inspectors import PYTHON_DATA_FOLDER +from test.python.inspectors.conftest import gather_issues_test_info, IssuesTestInfo, use_file_metadata from textwrap import dedent +import pytest from src.python.review.common.language import Language from src.python.review.inspectors.flake8.flake8 import Flake8Inspector from src.python.review.inspectors.issue import IssueType from src.python.review.reviewers.utils.issues_filter import filter_low_measure_issues -from test.python.inspectors import PYTHON_DATA_FOLDER -from test.python.inspectors.conftest import gather_issues_test_info, IssuesTestInfo, use_file_metadata FILE_NAMES_AND_N_ISSUES = [ ('case0_spaces.py', 5), @@ -25,7 +25,7 @@ ('case14_returns_errors.py', 4), ('case16_comments.py', 0), ('case17_dangerous_default_value.py', 1), - ('case18_comprehensions.py', 10), + ('case18_comprehensions.py', 9), ('case19_bad_indentation.py', 3), ('case21_imports.py', 2), ('case25_django.py', 0), diff --git a/test/python/inspectors/test_local_review.py b/test/python/inspectors/test_local_review.py index e1e028e6..5b182540 100644 --- a/test/python/inspectors/test_local_review.py +++ b/test/python/inspectors/test_local_review.py @@ -1,13 +1,12 @@ import json from collections import namedtuple +from test.python.inspectors import PYTHON_DATA_FOLDER import pytest - from src.python.review.application_config import ApplicationConfig from src.python.review.inspectors.inspector_type import InspectorType from src.python.review.quality.model import QualityType from src.python.review.reviewers.perform_review import OutputFormat, PathNotExists, perform_and_print_review -from test.python.inspectors import PYTHON_DATA_FOLDER Args = namedtuple('Args', [ 'path', @@ -24,7 +23,7 @@ def config() -> ApplicationConfig: disabled_inspectors={InspectorType.INTELLIJ}, allow_duplicates=False, n_cpu=1, - inspectors_config=dict(n_cpu=1), + inspectors_config={"n_cpu": 1}, ) diff --git a/test/python/inspectors/test_pmd_inspector.py b/test/python/inspectors/test_pmd_inspector.py index 18b13af4..393a2d46 100644 --- a/test/python/inspectors/test_pmd_inspector.py +++ b/test/python/inspectors/test_pmd_inspector.py @@ -1,7 +1,8 @@ -import pytest +from test.python.inspectors import JAVA_DATA_FOLDER +import pytest from src.python.review.inspectors.pmd.pmd import PMDInspector -from test.python.inspectors import JAVA_DATA_FOLDER + from .conftest import use_file_metadata FILE_NAMES_AND_N_ISSUES = [ diff --git a/test/python/inspectors/test_pylint_inspector.py b/test/python/inspectors/test_pylint_inspector.py index ab807765..ba4be12f 100644 --- a/test/python/inspectors/test_pylint_inspector.py +++ b/test/python/inspectors/test_pylint_inspector.py @@ -1,14 +1,14 @@ import textwrap +from test.python.inspectors import PYTHON_DATA_FOLDER import pytest - from src.python.review.inspectors.issue import IssueType from src.python.review.inspectors.pylint.pylint import PylintInspector -from test.python.inspectors import PYTHON_DATA_FOLDER + from .conftest import use_file_metadata FILE_NAMES_AND_N_ISSUES = [ - ('case0_spaces.py', 3), + ('case0_spaces.py', 0), ('case1_simple_valid_program.py', 0), ('case2_boolean_expressions.py', 3), ('case3_redefining_builtin.py', 2), @@ -24,7 +24,7 @@ ('case15_redefining.py', 2), ('case16_comments.py', 0), ('case17_dangerous_default_value.py', 1), - ('case18_comprehensions.py', 2), + ('case18_comprehensions.py', 3), ('case19_bad_indentation.py', 2), ('case21_imports.py', 2), ('case23_merging_comparisons.py', 4), diff --git a/test/python/inspectors/test_python_ast.py b/test/python/inspectors/test_python_ast.py index fed3c542..880e4775 100644 --- a/test/python/inspectors/test_python_ast.py +++ b/test/python/inspectors/test_python_ast.py @@ -1,18 +1,15 @@ import ast +from test.python.inspectors import PYTHON_AST_DATA_FOLDER, PYTHON_DATA_FOLDER +from test.python.inspectors.conftest import use_file_metadata import pytest - from src.python.review.inspectors.inspector_type import InspectorType - from src.python.review.inspectors.pyast.python_ast import ( BoolExpressionLensGatherer, FunctionLensGatherer, PythonAstInspector, ) -from test.python.inspectors import PYTHON_DATA_FOLDER, PYTHON_AST_DATA_FOLDER -from test.python.inspectors.conftest import use_file_metadata - FILE_NAMES_AND_N_ISSUES = [ ('case0_spaces.py', 0), ('case1_simple_valid_program.py', 0), diff --git a/test/python/inspectors/test_radon_inspector.py b/test/python/inspectors/test_radon_inspector.py index 72fd03e7..4ed41e1e 100644 --- a/test/python/inspectors/test_radon_inspector.py +++ b/test/python/inspectors/test_radon_inspector.py @@ -1,14 +1,14 @@ +from test.python.inspectors import PYTHON_DATA_FOLDER +from test.python.inspectors.conftest import use_file_metadata from textwrap import dedent import pytest - from src.python.review.common.language import Language from src.python.review.inspectors.issue import IssueType from src.python.review.inspectors.radon.radon import RadonInspector -from src.python.review.reviewers.utils.issues_filter import filter_low_measure_issues -from test.python.inspectors import PYTHON_DATA_FOLDER -from test.python.inspectors.conftest import use_file_metadata from src.python.review.inspectors.tips import get_maintainability_index_tip +from src.python.review.reviewers.utils.issues_filter import filter_low_measure_issues + FILE_NAMES_AND_N_ISSUES = [ ("case13_complex_logic.py", 1), diff --git a/test/python/inspectors/test_springlint_inspector.py b/test/python/inspectors/test_springlint_inspector.py index 18ee8dfb..673aa30c 100644 --- a/test/python/inspectors/test_springlint_inspector.py +++ b/test/python/inspectors/test_springlint_inspector.py @@ -1,6 +1,7 @@ +from test.python.inspectors import SPRING_DATA_FOLDER + from src.python.review.inspectors.issue import IssueType from src.python.review.inspectors.springlint.springlint import SpringlintInspector -from test.python.inspectors import SPRING_DATA_FOLDER def test_controller_with_smells(): diff --git a/test/resources/inspectors/python/case18_comprehensions.py b/test/resources/inspectors/python/case18_comprehensions.py index 3ed9be6f..4365018e 100644 --- a/test/resources/inspectors/python/case18_comprehensions.py +++ b/test/resources/inspectors/python/case18_comprehensions.py @@ -11,7 +11,7 @@ dict(((1, 2),)) # {1: 2} -sum([x ** 2 for x in range(10)]) # sum(x ** 2 for x in range(10)) +sum([x ** 2 for x in range(10)]) # we allow this since flake8-comprehension 3.4.0 (see its changelog) test_dict = dict() # we allow this test_list = list() # we allow this diff --git a/whitelist.txt b/whitelist.txt index c2473e99..a36bef18 100644 --- a/whitelist.txt +++ b/whitelist.txt @@ -54,6 +54,25 @@ unlink utils param params +changelog +multiline +sqrt +WPS +OOP +mccabe +mcs +dicts +misrefactored +src +textwrap +dedent +maintainabilities +parsers +fs +KTS +nl +splitext +dirname # Springlint issues cbo dit @@ -61,5 +80,3 @@ lcom noc nom wmc -multiline -sqrt From eef3f41dc26f54d86c9398ccd077d0b800cb3e57 Mon Sep 17 00:00:00 2001 From: Daria Diatlova Date: Mon, 3 May 2021 14:54:20 +0300 Subject: [PATCH 05/36] xlsx-run-tool (#24) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added xlsx_tool_run.py – script to run the tool on multiple code samples stored in xlsx file --- .github/workflows/build.yml | 9 + README.md | 1 + requirements-evaluation.txt | 2 + requirements.txt | 1 + src/python/common/__init__.py | 0 src/python/common/tool_arguments.py | 74 ++++++++ src/python/evaluation/README.md | 31 ++++ src/python/evaluation/__init__.py | 0 src/python/evaluation/common/__init__.py | 0 src/python/evaluation/common/util.py | 34 ++++ src/python/evaluation/common/xlsx_util.py | 42 +++++ src/python/evaluation/evaluation_config.py | 43 +++++ src/python/evaluation/xlsx_run_tool.py | 169 ++++++++++++++++++ src/python/review/application_config.py | 16 ++ src/python/review/common/file_system.py | 6 +- src/python/review/run_tool.py | 85 ++++----- test/python/evaluation/__init__.py | 11 ++ test/python/evaluation/test_data_path.py | 14 ++ test/python/evaluation/test_output_results.py | 32 ++++ test/python/evaluation/test_tool_path.py | 26 +++ .../evaluation/test_xlsx_file_structure.py | 23 +++ test/python/evaluation/testing_config.py | 18 ++ .../evaluation/xlsx_files/__init__.py | 0 .../xlsx_files/test_empty_lang_cell.xlsx | Bin 0 -> 5162 bytes .../xlsx_files/test_empty_table.xlsx | Bin 0 -> 4589 bytes .../xlsx_files/test_java_no_version.xlsx | Bin 0 -> 5156 bytes .../xlsx_files/test_sorted_order.xlsx | Bin 0 -> 7317 bytes .../xlsx_files/test_unsorted_order.xlsx | Bin 0 -> 6995 bytes .../xlsx_files/test_wrong_column_name.xlsx | Bin 0 -> 5175 bytes .../evaluation/xlsx_target_files/__init__.py | 0 .../target_sorted_order.xlsx | Bin 0 -> 38024 bytes .../target_unsorted_order.xlsx | Bin 0 -> 30970 bytes whitelist.txt | 11 ++ 33 files changed, 595 insertions(+), 53 deletions(-) create mode 100644 requirements-evaluation.txt create mode 100644 src/python/common/__init__.py create mode 100644 src/python/common/tool_arguments.py create mode 100644 src/python/evaluation/README.md create mode 100644 src/python/evaluation/__init__.py create mode 100644 src/python/evaluation/common/__init__.py create mode 100644 src/python/evaluation/common/util.py create mode 100644 src/python/evaluation/common/xlsx_util.py create mode 100644 src/python/evaluation/evaluation_config.py create mode 100644 src/python/evaluation/xlsx_run_tool.py create mode 100644 test/python/evaluation/__init__.py create mode 100644 test/python/evaluation/test_data_path.py create mode 100644 test/python/evaluation/test_output_results.py create mode 100644 test/python/evaluation/test_tool_path.py create mode 100644 test/python/evaluation/test_xlsx_file_structure.py create mode 100644 test/python/evaluation/testing_config.py create mode 100644 test/resources/evaluation/xlsx_files/__init__.py create mode 100644 test/resources/evaluation/xlsx_files/test_empty_lang_cell.xlsx create mode 100644 test/resources/evaluation/xlsx_files/test_empty_table.xlsx create mode 100644 test/resources/evaluation/xlsx_files/test_java_no_version.xlsx create mode 100644 test/resources/evaluation/xlsx_files/test_sorted_order.xlsx create mode 100644 test/resources/evaluation/xlsx_files/test_unsorted_order.xlsx create mode 100644 test/resources/evaluation/xlsx_files/test_wrong_column_name.xlsx create mode 100644 test/resources/evaluation/xlsx_target_files/__init__.py create mode 100644 test/resources/evaluation/xlsx_target_files/target_sorted_order.xlsx create mode 100644 test/resources/evaluation/xlsx_target_files/target_unsorted_order.xlsx diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c08b6e9f..d4385934 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,6 +22,7 @@ jobs: pip install flake8 pytest pip install -r requirements.txt pip install -r requirements-test.txt + pip install -r requirements-evaluation.txt - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names @@ -38,6 +39,14 @@ jobs: java-version: '11' - name: Check java version run: java -version + - name: Test with pytest run: | pytest + - name: Upload pytest test results + uses: actions/upload-artifact@v2 + with: + name: pytest-results-${{ matrix.python-version }} + path: test + # Use always() to always run this step to publish test results when there are test failures + if: ${{ always() }} diff --git a/README.md b/README.md index 43370839..67ce8430 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,7 @@ Simply clone the repository and run the following commands: 1. `pip install -r requirements.txt` 2. `pip install -r requirements-test.txt` for tests +3. `pip install -r requirements-evaluation.txt` for evaluation ## Usage diff --git a/requirements-evaluation.txt b/requirements-evaluation.txt new file mode 100644 index 00000000..11910373 --- /dev/null +++ b/requirements-evaluation.txt @@ -0,0 +1,2 @@ +openpyxl==3.0.7 +pandas==1.2.3 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index d5a07c54..c61d633a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -27,3 +27,4 @@ radon==4.5.0 # extra libraries and frameworks django==3.2 requests==2.25.1 +argparse==1.4.0 diff --git a/src/python/common/__init__.py b/src/python/common/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/python/common/tool_arguments.py b/src/python/common/tool_arguments.py new file mode 100644 index 00000000..9fe4181b --- /dev/null +++ b/src/python/common/tool_arguments.py @@ -0,0 +1,74 @@ +from dataclasses import dataclass +from enum import Enum, unique +from typing import List, Optional + +from src.python.review.application_config import LanguageVersion +from src.python.review.inspectors.inspector_type import InspectorType + + +@unique +class VerbosityLevel(Enum): + """ + Same meaning as the logging level. Should be used in command-line args. + """ + DEBUG = '3' + INFO = '2' + ERROR = '1' + DISABLE = '0' + + @classmethod + def values(cls) -> List[str]: + return [member.value for member in VerbosityLevel.__members__.values()] + + +@dataclass(frozen=True) +class ArgumentsInfo: + short_name: Optional[str] + long_name: str + description: str + + +@unique +class RunToolArgument(Enum): + VERBOSITY = ArgumentsInfo('-v', '--verbosity', + 'Choose logging level: ' + f'{VerbosityLevel.ERROR.value} - ERROR; ' + f'{VerbosityLevel.INFO.value} - INFO; ' + f'{VerbosityLevel.DEBUG.value} - DEBUG; ' + f'{VerbosityLevel.DISABLE.value} - disable logging; ' + 'default is 0') + + inspectors = [inspector.lower() for inspector in InspectorType.available_values()] + disabled_inspectors_example = f'-d {inspectors[0].lower()},{inspectors[1].lower()}' + + DISABLE = ArgumentsInfo('-d', '--disable', + 'Disable inspectors. ' + f'Available values: {", ".join(inspectors)}. ' + f'Example: {disabled_inspectors_example}') + + DUPLICATES = ArgumentsInfo(None, '--allow-duplicates', + 'Allow duplicate issues found by different linters. ' + 'By default, duplicates are skipped.') + + LANG_VERSION = ArgumentsInfo(None, '--language-version', + 'Specify the language version for JAVA inspectors.' + 'Available values are: ' + f'{LanguageVersion.PYTHON_3.value}, {LanguageVersion.JAVA_8.value}, ' + f'{LanguageVersion.JAVA_11.value}, {LanguageVersion.KOTLIN.value}.') + + CPU = ArgumentsInfo(None, '--n-cpu', + 'Specify number of cpu that can be used to run inspectors') + + PATH = ArgumentsInfo(None, 'path', 'Path to file or directory to inspect.') + + FORMAT = ArgumentsInfo('-f', '--format', + 'The output format. Default is JSON.') + + START_LINE = ArgumentsInfo('-s', '--start-line', + 'The first line to be analyzed. It starts from 1.') + + END_LINE = ArgumentsInfo('-e', '--end-line', 'The end line to be analyzed or None.') + + NEW_FORMAT = ArgumentsInfo(None, '--new-format', + 'The argument determines whether the tool ' + 'should use the new format') diff --git a/src/python/evaluation/README.md b/src/python/evaluation/README.md new file mode 100644 index 00000000..67e1d45e --- /dev/null +++ b/src/python/evaluation/README.md @@ -0,0 +1,31 @@ +# Hyperstyle evaluation + +This tool allows running the `Hyperstyle` tool on an xlsx table to get code quality for all code fragments. Please, note that your input file should consist of at least 2 obligatory columns to run xlsx-tool on its code fragments: + +- `code` +- `lang` + +Possible values for column `lang` are: `python3`, `kotlin`, `java8`, `java11`. + +Output file is a new `xlsx` file with 3 columns: +- `code` +- `lang` +- `grade` +Grade assessment is conducted by [`run_tool.py`](https://github.com/hyperskill/hyperstyle/blob/main/README.md) with default arguments. Avaliable values for column `grade` are: BAD, MODERATE, GOOD, EXCELLENT. It is also possible add fourth column: `traceback` to get full inspectors feedback on each code fragment. More details on enabling traceback column in **Optional Arguments** table. + +## Usage + +Run the [xlsx_run_tool.py](xlsx_run_tool.py) with the arguments from command line. + +Required arguments: + +`xlsx_file_path` — path to xlsx-file with code samples to inspect. + +Optional arguments: +Argument | Description +--- | --- +|**‑f**, **‑‑format**| The output format. Available values: `json`, `text`. The default value is `json` . Use this argument when `traceback` is enabled, otherwise it will not be used.| +|**‑tp**, **‑‑tool_path**| Path to run-tool. Default is `src/python/review/run_tool.py` .| +|**‑tr**, **‑‑traceback**| To include a column with errors traceback into an output file. Default is `False`.| +|**‑ofp**, **‑‑output_folder_path**| An explicit folder path to store file with results. Default is a parent directory of a folder with xlsx-file sent for inspection. | +|**‑ofn**, **‑‑output_file_name**| A name of an output file where evaluation results will be stored. Default is `results.xlsx`.| diff --git a/src/python/evaluation/__init__.py b/src/python/evaluation/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/python/evaluation/common/__init__.py b/src/python/evaluation/common/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/python/evaluation/common/util.py b/src/python/evaluation/common/util.py new file mode 100644 index 00000000..c306d3b7 --- /dev/null +++ b/src/python/evaluation/common/util.py @@ -0,0 +1,34 @@ +from enum import Enum, unique + +from src.python.review.application_config import LanguageVersion +from src.python.review.common.file_system import Extension + + +@unique +class ColumnName(Enum): + CODE = "code" + LANG = "lang" + LANGUAGE = "language" + GRADE = "grade" + + +@unique +class EvaluationArgument(Enum): + TRACEBACK = "traceback" + RESULT_FILE_NAME = "results" + RESULT_FILE_NAME_EXT = f"{RESULT_FILE_NAME}{Extension.XLSX.value}" + + +script_structure_rule = ("Please, make sure your XLSX-file matches following script standards: \n" + "1. Your XLSX-file should have 2 obligatory columns named:" + f"'{ColumnName.CODE.value}' & '{ColumnName.LANG.value}'. \n" + f"'{ColumnName.CODE.value}' column -- relates to the code-sample. \n" + f"'{ColumnName.LANG.value}' column -- relates to the language of a " + "particular code-sample. \n" + "2. Your code samples should belong to the one of the supported languages. \n" + "Supported languages are: Java, Kotlin, Python. \n" + f"3. Check that '{ColumnName.LANG.value}' column cells are filled with " + "acceptable language-names: \n" + f"Acceptable language-names are: {LanguageVersion.PYTHON_3.value}, " + f"{LanguageVersion.JAVA_8.value} ," + f"{LanguageVersion.JAVA_11.value} and {LanguageVersion.KOTLIN.value}.") diff --git a/src/python/evaluation/common/xlsx_util.py b/src/python/evaluation/common/xlsx_util.py new file mode 100644 index 00000000..032a5ce6 --- /dev/null +++ b/src/python/evaluation/common/xlsx_util.py @@ -0,0 +1,42 @@ +import logging.config +from pathlib import Path +from typing import Union + +import pandas as pd +from openpyxl import load_workbook, Workbook +from src.python.evaluation.evaluation_config import EvaluationConfig + +logger = logging.getLogger(__name__) + + +def remove_sheet(workbook_path: Union[str, Path], sheet_name: str, to_raise_error: bool = False) -> None: + try: + workbook = load_workbook(workbook_path) + workbook.remove(workbook[sheet_name]) + workbook.save(workbook_path) + + except KeyError as e: + message = f'Sheet with specified name: {sheet_name} does not exist.' + if to_raise_error: + logger.exception(message) + raise e + else: + logger.info(message) + + +def create_and_get_workbook_path(config: EvaluationConfig) -> Path: + workbook = Workbook() + workbook_path = config.get_output_file_path() + workbook.save(workbook_path) + return workbook_path + + +def write_dataframe_to_xlsx_sheet(xlsx_file_path: Union[str, Path], df: pd.DataFrame, sheet_name: str, + mode: str = 'a', to_write_row_names: bool = False) -> None: + """ + mode: str Available values are {'w', 'a'}. File mode to use (write or append). + to_write_row_names: bool Write row names. + """ + + with pd.ExcelWriter(xlsx_file_path, mode=mode) as writer: + df.to_excel(writer, sheet_name=sheet_name, index=to_write_row_names) diff --git a/src/python/evaluation/evaluation_config.py b/src/python/evaluation/evaluation_config.py new file mode 100644 index 00000000..5cee71dc --- /dev/null +++ b/src/python/evaluation/evaluation_config.py @@ -0,0 +1,43 @@ +import logging.config +from argparse import Namespace +from pathlib import Path +from typing import List, Union + +from src.python.common.tool_arguments import RunToolArgument +from src.python.evaluation.common.util import EvaluationArgument +from src.python.review.application_config import LanguageVersion +from src.python.review.common.file_system import create_directory + +logger = logging.getLogger(__name__) + + +class EvaluationConfig: + def __init__(self, args: Namespace): + self.tool_path: Union[str, Path] = args.tool_path + self.output_format: str = args.format + self.xlsx_file_path: Union[str, Path] = args.xlsx_file_path + self.traceback: bool = args.traceback + self.output_folder_path: Union[str, Path] = args.output_folder_path + self.output_file_name: str = args.output_file_name + + def build_command(self, inspected_file_path: Union[str, Path], lang: str) -> List[str]: + command = [LanguageVersion.PYTHON_3.value, + self.tool_path, + inspected_file_path, + RunToolArgument.FORMAT.value.short_name, self.output_format] + + if lang == LanguageVersion.JAVA_8.value or lang == LanguageVersion.JAVA_11.value: + command.extend([RunToolArgument.LANG_VERSION.value.long_name, lang]) + return command + + def get_output_file_path(self) -> Path: + if self.output_folder_path is None: + try: + self.output_folder_path = ( + Path(self.xlsx_file_path).parent.parent / EvaluationArgument.RESULT_FILE_NAME.value + ) + create_directory(self.output_folder_path) + except FileNotFoundError as e: + logger.error('XLSX-file with the specified name does not exists.') + raise e + return Path(self.output_folder_path) / self.output_file_name diff --git a/src/python/evaluation/xlsx_run_tool.py b/src/python/evaluation/xlsx_run_tool.py new file mode 100644 index 00000000..7235b041 --- /dev/null +++ b/src/python/evaluation/xlsx_run_tool.py @@ -0,0 +1,169 @@ +import argparse +import logging.config +import os +import re +import sys +import traceback +from pathlib import Path +from typing import Type + +sys.path.append('') +sys.path.append('../../..') + +import pandas as pd +from src.python.common.tool_arguments import RunToolArgument +from src.python.evaluation.common.util import ColumnName, EvaluationArgument, script_structure_rule +from src.python.evaluation.common.xlsx_util import ( + create_and_get_workbook_path, + remove_sheet, + write_dataframe_to_xlsx_sheet, +) +from src.python.evaluation.evaluation_config import EvaluationConfig +from src.python.review.application_config import LanguageVersion +from src.python.review.common.file_system import create_file, new_temp_dir +from src.python.review.common.subprocess_runner import run_in_subprocess +from src.python.review.reviewers.perform_review import OutputFormat + +logger = logging.getLogger(__name__) + + +def configure_arguments(parser: argparse.ArgumentParser, run_tool_arguments: Type[RunToolArgument]) -> None: + parser.add_argument('xlsx_file_path', + type=lambda value: Path(value).absolute(), + help='Local XLSX-file path. ' + 'Your XLSX-file must include column-names: ' + f'"{ColumnName.CODE.value}" and ' + f'"{ColumnName.LANG.value}". Acceptable values for ' + f'"{ColumnName.LANG.value}" column are: ' + f'{LanguageVersion.PYTHON_3.value}, {LanguageVersion.JAVA_8.value}, ' + f'{LanguageVersion.JAVA_11.value}, {LanguageVersion.KOTLIN.value}.') + + parser.add_argument('-tp', '--tool-path', + default=Path('src/python/review/run_tool.py').absolute(), + type=lambda value: Path(value).absolute(), + help='Path to script to run on files.') + + parser.add_argument('-tr', '--traceback', + help='If True, column with the full inspector feedback will be added ' + 'to the output file with results.', + action='store_true') + + parser.add_argument('-ofp', '--output-folder-path', + help='An absolute path to the folder where file with evaluation results' + 'will be stored.' + 'Default is the path to a directory, where is the folder with xlsx_file.', + # if None default path will be specified based on xlsx_file_path. + default=None, + type=str) + + parser.add_argument('-ofn', '--output-file-name', + help='Filename for that will be created to store inspection results.' + f'Default is "{EvaluationArgument.RESULT_FILE_NAME_EXT.value}"', + default=f'{EvaluationArgument.RESULT_FILE_NAME_EXT.value}', + type=str) + + parser.add_argument(run_tool_arguments.FORMAT.value.short_name, + run_tool_arguments.FORMAT.value.long_name, + default=OutputFormat.JSON.value, + choices=OutputFormat.values(), + type=str, + help=f'{run_tool_arguments.FORMAT.value.description}' + f'Use this argument when {EvaluationArgument.TRACEBACK.value} argument' + 'is enabled argument will not be used otherwise.') + + +def create_dataframe(config: EvaluationConfig) -> pd.DataFrame: + report = pd.DataFrame( + { + ColumnName.LANGUAGE.value: [], + ColumnName.CODE.value: [], + ColumnName.GRADE.value: [], + }, + ) + + if config.traceback: + report[EvaluationArgument.TRACEBACK.value] = [] + + try: + lang_code_dataframe = pd.read_excel(config.xlsx_file_path) + + except FileNotFoundError as e: + logger.error('XLSX-file with the specified name does not exists.') + raise e + + try: + for lang, code in zip(lang_code_dataframe[ColumnName.LANG.value], + lang_code_dataframe[ColumnName.CODE.value]): + + with new_temp_dir() as create_temp_dir: + temp_dir_path = create_temp_dir + lang_extension = LanguageVersion.language_by_extension(lang) + temp_file_path = os.path.join(temp_dir_path, ('file' + lang_extension)) + temp_file_path = next(create_file(temp_file_path, code)) + + try: + assert os.path.exists(temp_file_path) + except AssertionError as e: + logger.exception('Path does not exist.') + raise e + + command = config.build_command(temp_file_path, lang) + results = run_in_subprocess(command) + os.remove(temp_file_path) + temp_dir_path.rmdir() + # this regular expression matches final tool grade: EXCELLENT, GOOD, MODERATE or BAD + grades = re.match(r'^.*{"code":\s"([A-Z]+)"', results).group(1) + output_row_values = [lang, code, grades] + column_indices = [ColumnName.LANGUAGE.value, + ColumnName.CODE.value, + ColumnName.GRADE.value] + + if config.traceback: + output_row_values.append(results) + column_indices.append(EvaluationArgument.TRACEBACK.value) + + new_file_report_row = pd.Series(data=output_row_values, index=column_indices) + report = report.append(new_file_report_row, ignore_index=True) + + return report + + except KeyError as e: + logger.error(script_structure_rule) + raise e + + except Exception as e: + traceback.print_exc() + logger.exception('An unexpected error.') + raise e + + +def main() -> int: + parser = argparse.ArgumentParser() + configure_arguments(parser, RunToolArgument) + + try: + args = parser.parse_args() + config = EvaluationConfig(args) + workbook_path = create_and_get_workbook_path(config) + results = create_dataframe(config) + write_dataframe_to_xlsx_sheet(workbook_path, results, 'inspection_results') + # remove empty sheet that was initially created with the workbook + remove_sheet(workbook_path, 'Sheet') + return 0 + + except FileNotFoundError: + logger.error('XLSX-file with the specified name does not exists.') + return 2 + + except KeyError: + logger.error(script_structure_rule) + return 2 + + except Exception: + traceback.print_exc() + logger.exception('An unexpected error.') + return 2 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/src/python/review/application_config.py b/src/python/review/application_config.py index 41a1c296..8d9a56f5 100644 --- a/src/python/review/application_config.py +++ b/src/python/review/application_config.py @@ -2,6 +2,7 @@ from enum import Enum, unique from typing import List, Optional, Set +from src.python.review.common.file_system import Extension from src.python.review.inspectors.inspector_type import InspectorType @@ -22,7 +23,22 @@ class LanguageVersion(Enum): JAVA_8 = 'java8' JAVA_9 = 'java9' JAVA_11 = 'java11' + PYTHON_3 = 'python3' + KOTLIN = 'kotlin' @classmethod def values(cls) -> List[str]: return [member.value for member in cls.__members__.values()] + + @classmethod + def language_to_extension_dict(cls) -> dict: + return {cls.PYTHON_3.value: Extension.PY.value, + cls.JAVA_7.value: Extension.JAVA.value, + cls.JAVA_8.value: Extension.JAVA.value, + cls.JAVA_9.value: Extension.JAVA.value, + cls.JAVA_11.value: Extension.JAVA.value, + cls.KOTLIN.value: Extension.KT.value} + + @classmethod + def language_by_extension(cls, lang: str) -> str: + return cls.language_to_extension_dict()[lang] diff --git a/src/python/review/common/file_system.py b/src/python/review/common/file_system.py index 764a2d5e..3ab06061 100644 --- a/src/python/review/common/file_system.py +++ b/src/python/review/common/file_system.py @@ -29,6 +29,7 @@ class Extension(Enum): KT = '.kt' JS = '.js' KTS = '.kts' + XLSX = '.xlsx' ItemCondition = Callable[[str], bool] @@ -66,8 +67,9 @@ def create_file(file_path: Union[str, Path], content: str): file_path = Path(file_path) create_directory(os.path.dirname(file_path)) - with open(file_path, 'w') as f: - f.write(content) + with open(file_path, 'w+') as f: + f.writelines(content) + yield Path(file_path) def create_directory(directory: str) -> None: diff --git a/src/python/review/run_tool.py b/src/python/review/run_tool.py index 0d96f6e8..bdfbb41f 100644 --- a/src/python/review/run_tool.py +++ b/src/python/review/run_tool.py @@ -1,15 +1,17 @@ import argparse +import enum import logging.config import os import sys import traceback -from enum import Enum, unique from pathlib import Path -from typing import List, Set +from typing import Set + sys.path.append('') sys.path.append('../../..') +from src.python.common.tool_arguments import RunToolArgument, VerbosityLevel from src.python.review.application_config import ApplicationConfig, LanguageVersion from src.python.review.inspectors.inspector_type import InspectorType from src.python.review.logging_config import logging_config @@ -23,21 +25,6 @@ logger = logging.getLogger(__name__) -@unique -class VerbosityLevel(Enum): - """ - Same meaning as the logging level. Should be used in command-line args. - """ - DEBUG = '3' - INFO = '2' - ERROR = '1' - DISABLE = '0' - - @classmethod - def values(cls) -> List[str]: - return [member.value for _, member in VerbosityLevel.__members__.items()] - - def parse_disabled_inspectors(value: str) -> Set[InspectorType]: passed_names = value.upper().split(',') allowed_names = {inspector.value for inspector in InspectorType} @@ -55,70 +42,66 @@ def positive_int(value: str) -> int: return value_int -def configure_arguments(parser: argparse.ArgumentParser) -> None: - parser.add_argument('-v', '--verbosity', - help='Choose logging level: ' - f'{VerbosityLevel.ERROR.value} - ERROR; ' - f'{VerbosityLevel.INFO.value} - INFO; ' - f'{VerbosityLevel.DEBUG.value} - DEBUG; ' - f'{VerbosityLevel.DISABLE.value} - disable logging; ' - 'default is 0', +def configure_arguments(parser: argparse.ArgumentParser, tool_arguments: enum.EnumMeta) -> None: + parser.add_argument(tool_arguments.VERBOSITY.value.short_name, + tool_arguments.VERBOSITY.value.long_name, + help=tool_arguments.VERBOSITY.value.description, default=VerbosityLevel.DISABLE.value, choices=VerbosityLevel.values(), type=str) # Usage example: -d Flake8,Intelli - inspectors = [inspector.lower() for inspector in InspectorType.available_values()] - example = f'-d {inspectors[0].lower()},{inspectors[1].lower()}' - - parser.add_argument('-d', '--disable', - help='Disable inspectors. ' - f'Allowed values: {", ".join(inspectors)}. ' - f'Example: {example}', + parser.add_argument(tool_arguments.DISABLE.value.short_name, + tool_arguments.DISABLE.value.long_name, + help=tool_arguments.DISABLE.value.description, type=parse_disabled_inspectors, default=set()) - parser.add_argument('--allow-duplicates', action='store_true', - help='Allow duplicate issues found by different linters. ' - 'By default, duplicates are skipped.') + parser.add_argument(tool_arguments.DUPLICATES.value.long_name, + action='store_true', + help=tool_arguments.DUPLICATES.value.description) # TODO: deprecated argument: language_version. Delete after several releases. - parser.add_argument('--language_version', '--language-version', - help='Specify the language version for JAVA inspectors.', + parser.add_argument('--language_version', + tool_arguments.LANG_VERSION.value.long_name, + help=tool_arguments.LANG_VERSION.value.description, default=None, choices=LanguageVersion.values(), type=str) # TODO: deprecated argument: --n_cpu. Delete after several releases. - parser.add_argument('--n_cpu', '--n-cpu', - help='Specify number of cpu that can be used to run inspectors', + parser.add_argument('--n_cpu', + tool_arguments.CPU.value.long_name, + help=tool_arguments.CPU.value.description, default=1, type=positive_int) - parser.add_argument('path', + parser.add_argument(tool_arguments.PATH.value.long_name, type=lambda value: Path(value).absolute(), - help='Path to file or directory to inspect.') + help=tool_arguments.PATH.value.description) - parser.add_argument('-f', '--format', + parser.add_argument(tool_arguments.FORMAT.value.short_name, + tool_arguments.FORMAT.value.long_name, default=OutputFormat.JSON.value, choices=OutputFormat.values(), type=str, - help='The output format. Default is JSON.') + help=tool_arguments.FORMAT.value.description) - parser.add_argument('-s', '--start-line', + parser.add_argument(tool_arguments.START_LINE.value.short_name, + tool_arguments.START_LINE.value.long_name, default=1, type=positive_int, - help='The first line to be analyzed. It starts from 1.') + help=tool_arguments.START_LINE.value.description) - parser.add_argument('-e', '--end-line', + parser.add_argument(tool_arguments.END_LINE.value.short_name, + tool_arguments.END_LINE.value.long_name, default=None, type=positive_int, - help='The end line to be analyzed or None.') + help=tool_arguments.END_LINE.value.description) - parser.add_argument('--new-format', + parser.add_argument(tool_arguments.NEW_FORMAT.value.long_name, action='store_true', - help='The argument determines whether the tool ' - 'should use the new format') + help=tool_arguments.NEW_FORMAT.value.description) def configure_logging(verbosity: VerbosityLevel) -> None: @@ -136,7 +119,7 @@ def configure_logging(verbosity: VerbosityLevel) -> None: def main() -> int: parser = argparse.ArgumentParser() - configure_arguments(parser) + configure_arguments(parser, RunToolArgument) try: args = parser.parse_args() diff --git a/test/python/evaluation/__init__.py b/test/python/evaluation/__init__.py new file mode 100644 index 00000000..31b1b86f --- /dev/null +++ b/test/python/evaluation/__init__.py @@ -0,0 +1,11 @@ +from test.python import TEST_DATA_FOLDER + +from src.python import MAIN_FOLDER + +CURRENT_TEST_DATA_FOLDER = TEST_DATA_FOLDER / 'evaluation' + +XLSX_DATA_FOLDER = CURRENT_TEST_DATA_FOLDER / 'xlsx_files' + +TARGET_XLSX_DATA_FOLDER = CURRENT_TEST_DATA_FOLDER / 'xlsx_target_files' + +RESULTS_DIR_PATH = MAIN_FOLDER.parent / 'evaluation/results' diff --git a/test/python/evaluation/test_data_path.py b/test/python/evaluation/test_data_path.py new file mode 100644 index 00000000..0d8e3502 --- /dev/null +++ b/test/python/evaluation/test_data_path.py @@ -0,0 +1,14 @@ +from test.python.evaluation import XLSX_DATA_FOLDER +from test.python.evaluation.testing_config import get_testing_arguments + +import pytest +from src.python.evaluation.evaluation_config import EvaluationConfig +from src.python.evaluation.xlsx_run_tool import create_dataframe + + +def test_incorrect_data_path(): + with pytest.raises(FileNotFoundError): + testing_arguments_dict = get_testing_arguments(to_add_traceback=True, to_add_tool_path=True) + testing_arguments_dict.xlsx_file_path = XLSX_DATA_FOLDER / 'do_not_exist.xlsx' + config = EvaluationConfig(testing_arguments_dict) + assert create_dataframe(config) diff --git a/test/python/evaluation/test_output_results.py b/test/python/evaluation/test_output_results.py new file mode 100644 index 00000000..519652e5 --- /dev/null +++ b/test/python/evaluation/test_output_results.py @@ -0,0 +1,32 @@ +from test.python.evaluation import TARGET_XLSX_DATA_FOLDER, XLSX_DATA_FOLDER +from test.python.evaluation.testing_config import get_testing_arguments + +import pandas as pd +import pytest +from src.python.evaluation.evaluation_config import EvaluationConfig +from src.python.evaluation.xlsx_run_tool import create_dataframe + +FILE_NAMES = [ + ('test_sorted_order.xlsx', 'target_sorted_order.xlsx', False), + ('test_sorted_order.xlsx', 'target_sorted_order.xlsx', True), + ('test_unsorted_order.xlsx', 'target_unsorted_order.xlsx', False), + ('test_unsorted_order.xlsx', 'target_unsorted_order.xlsx', True), +] + + +@pytest.mark.parametrize(('test_file', 'target_file', 'output_type'), FILE_NAMES) +def test_correct_output(test_file: str, target_file: str, output_type: bool): + + testing_arguments_dict = get_testing_arguments(to_add_tool_path=True) + testing_arguments_dict.xlsx_file_path = XLSX_DATA_FOLDER / test_file + testing_arguments_dict.traceback = output_type + + config = EvaluationConfig(testing_arguments_dict) + test_dataframe = create_dataframe(config) + + sheet_name = 'grades' + if output_type: + sheet_name = 'traceback' + target_dataframe = pd.read_excel(TARGET_XLSX_DATA_FOLDER / target_file, sheet_name=sheet_name) + + assert test_dataframe.reset_index(drop=True).equals(target_dataframe.reset_index(drop=True)) diff --git a/test/python/evaluation/test_tool_path.py b/test/python/evaluation/test_tool_path.py new file mode 100644 index 00000000..0581caad --- /dev/null +++ b/test/python/evaluation/test_tool_path.py @@ -0,0 +1,26 @@ +from test.python.evaluation import XLSX_DATA_FOLDER +from test.python.evaluation.testing_config import get_testing_arguments + +import pytest +from src.python import MAIN_FOLDER +from src.python.evaluation.evaluation_config import EvaluationConfig +from src.python.evaluation.xlsx_run_tool import create_dataframe + + +def test_correct_tool_path(): + try: + testing_arguments_dict = get_testing_arguments(to_add_traceback=True, to_add_tool_path=True) + testing_arguments_dict.xlsx_file_path = XLSX_DATA_FOLDER / 'test_unsorted_order.xlsx' + config = EvaluationConfig(testing_arguments_dict) + create_dataframe(config) + except Exception: + pytest.fail("Unexpected error") + + +def test_incorrect_tool_path(): + with pytest.raises(Exception): + testing_arguments_dict = get_testing_arguments(to_add_traceback=True) + testing_arguments_dict.xlsx_file_path = XLSX_DATA_FOLDER / 'test_unsorted_order.xlsx' + testing_arguments_dict.tool_path = MAIN_FOLDER.parent / 'review/incorrect_path.py' + config = EvaluationConfig(testing_arguments_dict) + assert create_dataframe(config) diff --git a/test/python/evaluation/test_xlsx_file_structure.py b/test/python/evaluation/test_xlsx_file_structure.py new file mode 100644 index 00000000..9965992e --- /dev/null +++ b/test/python/evaluation/test_xlsx_file_structure.py @@ -0,0 +1,23 @@ +from test.python.evaluation import XLSX_DATA_FOLDER +from test.python.evaluation.testing_config import get_testing_arguments + +import pytest +from src.python.evaluation.evaluation_config import EvaluationConfig +from src.python.evaluation.xlsx_run_tool import create_dataframe + + +FILE_NAMES = [ + 'test_wrong_column_name.xlsx', + 'test_java_no_version.xlsx', + 'test_empty_lang_cell.xlsx', + 'test_empty_table.xlsx', +] + + +@pytest.mark.parametrize('file_name', FILE_NAMES) +def test_wrong_column(file_name: str): + with pytest.raises(KeyError): + testing_arguments_dict = get_testing_arguments(to_add_traceback=True, to_add_tool_path=True) + testing_arguments_dict.xlsx_file_path = XLSX_DATA_FOLDER / file_name + config = EvaluationConfig(testing_arguments_dict) + assert create_dataframe(config) diff --git a/test/python/evaluation/testing_config.py b/test/python/evaluation/testing_config.py new file mode 100644 index 00000000..70341635 --- /dev/null +++ b/test/python/evaluation/testing_config.py @@ -0,0 +1,18 @@ +from argparse import Namespace + +from src.python import MAIN_FOLDER +from src.python.evaluation.common.util import EvaluationArgument +from src.python.review.reviewers.perform_review import OutputFormat + + +def get_testing_arguments(to_add_traceback=None, to_add_tool_path=None) -> Namespace: + testing_arguments = Namespace(format=OutputFormat.JSON.value, + output_file_name=EvaluationArgument.RESULT_FILE_NAME_EXT.value, + output_folder_path=None) + if to_add_traceback: + testing_arguments.traceback = True + + if to_add_tool_path: + testing_arguments.tool_path = MAIN_FOLDER.parent / 'review/run_tool.py' + + return testing_arguments diff --git a/test/resources/evaluation/xlsx_files/__init__.py b/test/resources/evaluation/xlsx_files/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/resources/evaluation/xlsx_files/test_empty_lang_cell.xlsx b/test/resources/evaluation/xlsx_files/test_empty_lang_cell.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..91cdada068a9aa8ebe487d14c970a6f7415b1b8c GIT binary patch literal 5162 zcmai22Ut_v(hW5r5PAt6DN-f$4oVlKg91{d_XH88_g;n2n;=M&CIl%LLY1yaxquYu z2ojJcMd}~)y(icI-TNl_l5@T2;B5^an2bsg?2y)f!pL8AcI*4V;XV=3bP?i2xy)iN|*SoA5<{oKddI| z;Df>FG9dST>pj%H?nu#++?RE|h78A>vUbOm6|@CIq;QG{Pdp6iQ8r2Ff}YfZMJhft z7hm7>F)OYi^^5H^R=X!7IWI`KHd;%x%v=PQEX5zZ^Yq?e*}Bs1Exb&hw+*=*i|g%J znOIAE=Nc2Cbt2aeZ|Be-i?f7MSsvpY+Rm4k0xNZ7v`;a*1A>L3F(X7%xf@VildAG( z()i-%wELfWbzAz!%zHmk^k6V(b@DbUc=twBbC|Ux*(iH_4XU@tVh9@OZ4f_N`c%!` zC;Eny$GO^Uw&ES)dcC=}IuH+N=@5d00RRB8008a({6D^ax-w-N zS^%L?YJGT)@m(f8rVj^WtX4BT)i47!DoL+Hv z2+=P=&caotH{ZnQs+zMGJl;cbdi=e43R8vKgt}o-l~7gz9NxFULJVCXn)Ph2rv)c)^5b@PIZm-vXSES*2{u1X!l47)$dyA90bWfP#7JUXk!LZb z0aN%ucedV8wB&QadsJ52Z@n*$-o1)xI2`tyiLw1U|24eju;4;{S7z-5!5Nx{Y+xc- z6^;5tZ8g^`eBH?My17#7_C14Ij;fACzR~v=o@|s`x_Lkk?&e{u(s0aoWcGfxJ6Ui| zvx*#x*f|L~B5MAwxG%;h&Snj>La@Tc4IsFFJ4i193(B+qwoI;8yRedlI;`%zTZ$)s zMnm}RYBT(^c%f-*n}vQrNRaH(Y&bq$7rr#{_??{w{iFKm;==0<1{*3$u7!9Nn2sC-REDqH3_Ap% zn?BR_tAoQgGd~y#E6`E)3J)d3hOPCJ`Du_D!(BCNK-6pt!PYXY_&4BMUAE&dU4!b* zXo-x3g;l2WaA=M5Hf`Yfzw4setLw&q9QkrLm=2J zUf^jUUugvW{U=Lks0RB>57o;z!LQxg_Rhu`+pr>0kRj<)3KD8|@N z>oewKsr>_cobUO0wdtHzG)U8wX!6@q*bv56qalX?VKSTR`4qAfZbzkP8dVj&Pk!lx ziV~Ct0=+|SG!6frg5*Da;BIf@V#ELA^9TFh-=BugFq51GpJOc|+b57*O*%DM2292V z>viiw)jNglA2CH8&Rb}FTzu8@ZCP#1F_(lQgoN4>2FhB&IK-hb!gfyf=7uCBRLd6q ziz27{k1xD1f)dih(m%(RFOZXNJMMiUtFo3C#7veV=V=a)O76PGLdq#bMeTJv{g`S+ zx>HxfD^@<9iPA+m(aK0Dc_F!dDz~*;?peMtbCa~VZD?O~D0y@t{3g7Xgane!6&Ddo z!ZrVyZ8sG@qL3UOxT}ztLi~CGX?jj7-7>8UVuY9~s9!KondIZ%S|O>QXr;Z6)#096W-{phVj56i- ztq(agZk8iMIonM#G|w&Cb$Z%~HEouMOyq?YZTJV)xy;Q$o!qga+>TdlJY(d$2TM;g zvZqwQ=Sh1=%(cZ=n|*=4+^V5iVNIX8ir8h$NTs2~Z_^p}BN55SQEol~L98(gyH6#p zDAv+~cwIH(-6(jKygHeLMki8mJ?g0=+=R{1kbk1RiykE~-*}qSznm~H&u4|AwA9h- z!K7%^j=ZyR>YpB$Qv2i?HweDe)Fax&-_k;^7!z!u1H_rHaXK<9#ES&zk)=L)b*eQc<5O4+pCp zRE#i?c0WZ$L+UJo6-LBz8^;G$JWF$Qz(m*Fr1;CBg*mTalznRxV!;D<{PpvR?;PQMT}!-Z-2b9cMc4wsSvnGA!CLP)yj?k`GZpJKYm zmu=R@CURt`^}W138R*O$>bbyPB;dBe`}8_T?bhHEhJbr&B?)ac6my@pZu&o0jRMKt zrH_xuxQFQjVQtTx^i?!ite%6XkfLUF}@Ana-9gH`2c>cvT2>$flQ*JCVQfm zKB-QwqC2=k(9FP1rZ~)~d{w)P-=ytmNhbURKtmuwtn!R;KB!v6wUp2OO|Ooi*=%1? z5?kpsL4{lI8&{B79_EoCz@*;mTi;Rv;#FW_OU^s7wWSkc=FG5ZrK51On>Nz!`8td8 z>XAXff}PGYd_WoA}Fwp}!W6z74H~zb#VM)S% z*X)YR8s-`(5i$~5#Z_AKli%KdGryZ_yS;wudEk2v+CJJ99*ECXt zs!V{xa35Hc13E@|#Idlbl@*O~Xc#lL9ipB-Pw&*254Dl3WEr6;D*=u| zqforXZwQps;7{Ntq)K7kqzgq_{9ID)I>AOucbD$JK;wK^7Lcfg9b%-=vTH_S7&s@Ei_At)@ z36{(obJf27F8xrgj$(Q|pxWIqIhOxnWo*TQ{ooAOb5oWl*E*gzCLb<{pF79vp5S(6 zS3QYeFaZvE;Y($udKasS(AA=rU4ii0MN*DG)W|E^nqI~%6U>$!?i{?&z>JO^yiHLF zFd5wna^!tlL3wOprM93#SNVpT_w-H5&MarFztNP7r(Afa+3{!I#xJ=E)3iL#+nByV zTwR;=*E9~Plu)gOYcET;Qjd0Wou*a-Mrv0l``itvv6k#jHjuy(2;JjTK3^d=HbvmsWBJdvs&^;6LADaN*s*xVS&pk7+Oc(pRmO(e(Nq@P-vdsuEf_}i%it!CfH>_VSXj3 zRGJHq*NFMin`rdsSE!)r%khuB3GL4q{8WTjBk@BMR={sNc0x#$(BX)joTy3CRg22j z;Iy7D0+Nh;6s%@v(U>|u$Pt~FB$1~!i@NAKy%@4boY8vk1aWz^^OQuBVSAH1uRg%p ziby-TL6T8h{|7N6Wo){|gE<$o>WQ~PlTQQ)Ks!c3tkEBIZI@h!8u3VF=7d>@G7XFF zDZ9k~q14Ddppx+!tZi-`jWkF&sKb0j!+L&0N?60rmHM5-Ev@_9Y&*Ped76CiU~@0g zvf$2JW_E^reF+X5--&KSB3cYu&1z@AgdW6^@q20t)Wh1PdGYtaBLY&2$5hnb+w3n@mhex znTRp3PJ!iCu|rwM=B92OL0Z!HJ^!NeG%riyZx+lvdA)8A?J2*s(_%~36xd#Se7nfv;zye*^d^n$_=C|!y1wtlX1V_{BSta zN~i6YUOtTJTR`w|bA6jB>kp{5=k+JK!`)1j=22|wFvvASj&nA-*~o6*7j0$m0BJ)c zdTT(um#`PiHRz3S{!i8XKmi7Iw}HBw>wCG{JT|=~=^MQPO>|bAVxF_?Qi?z&m_suU z+LrKClZg#CG_1z>y*^FMA?_HnyBMJzK$!IoXTKIC+LYryi-@5n!=*BtalnL|+<+ZHde= zuTZ<*<@UJeAHvoI;UZ3XJ_i&@&~9CQ8&|}_F9*sVjnI5;GsyBDOR=Yq9reK{rZdc= zS0Y6yfPfyp9_Zg}UnY?XioMu;?-%}Ba(&>d-G`PRUU!MC)fPc}9Q@-%#lWNh{F=R7 z9VJ}OUjAx-H;K_!|0~MXj_|K2>gfMPqg?3%e+?miN4eTlU3QPZtRG!W{)+Nnd&%Dc zuWHB3R_vDzqFsRo{Jk;z9pI`Kxs;f{ObqV_!2c7Rzav~dk}fsNFXO!S6X8D!=J%yn zE9vE#^~wi;vf5*AX*vk_1%fRSox>AsSUwAd? g{_1;d5&+=8&|6y_2R%;!0Is25ndnmJO?r9ve_3*3D*ylh literal 0 HcmV?d00001 diff --git a/test/resources/evaluation/xlsx_files/test_empty_table.xlsx b/test/resources/evaluation/xlsx_files/test_empty_table.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..486b83a16a6b512ee860cc18323274db8588882b GIT binary patch literal 4589 zcmai12RzjO|L5#=cbvUtlblWV2$4-jM)ui4cJ^NBj8GCXGvY+{%FakdC}f2r*^%+T z)9?37{=aX(*R9Vz9`||P`~7^qU(eU`rLB&IO^$(&kB?DInxKnuE~ru0zRvtM9+o~1 zuJ)e)z7V+L>*Aa}V&>WbCC2*>SpynOJD5;I6v@p;G@-y*c2~Lw_xpkQpMu`1i8=&e zGP(@N9&8M{de#0=vVsJ%u2z%b_*2wuny~^m+z@G8;$h>r!+VrXle=7h*3gKQ4>uKE zS`RQUswTac&}pJ}Q~K(xAo23M8j=O(Lip7Zf}!i)H;10DUfYtx&kA_`Du-ikwH+%9 zYkvDgV?3f(gm7OzoBmLoC4$oG5NF?RwyXrNLPuKr2(vplOehWzEt<~r3dud8s&Fht zAbvu-`?*)QrT>FP?`QHJOa`sa%k_%>z0p-1<}E3<%0Ayh>tI+6p##0I#DC0xuHxwv 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 V<6}n&000U0m5FVIK9r|-{|8~OR4o7i literal 0 HcmV?d00001 diff --git a/test/resources/evaluation/xlsx_files/test_sorted_order.xlsx b/test/resources/evaluation/xlsx_files/test_sorted_order.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..bfdca3b2a22591703e61d6ba572aac5e027d4ce3 GIT binary patch literal 7317 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%0A0WM$% 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 literal 0 HcmV?d00001 diff --git a/test/resources/evaluation/xlsx_target_files/target_unsorted_order.xlsx b/test/resources/evaluation/xlsx_target_files/target_unsorted_order.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..25ed61462f273535632efd1ca804ad1b33e3dd7a GIT binary patch literal 30970 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 literal 0 HcmV?d00001 diff --git a/whitelist.txt b/whitelist.txt index a36bef18..c60ec292 100644 --- a/whitelist.txt +++ b/whitelist.txt @@ -73,6 +73,16 @@ KTS nl splitext dirname +hyperstyle +XLSX +Eval +eval +openpyxl +dataframe +writelines +rmdir +df +unique # Springlint issues cbo dit @@ -80,3 +90,4 @@ lcom noc nom wmc +util \ No newline at end of file From 045deffec47cf123a8fc7fb54a7ac7e5e5c1f20f Mon Sep 17 00:00:00 2001 From: "Anastasiia.Birillo" Date: Mon, 17 May 2021 13:18:39 +0300 Subject: [PATCH 06/36] Update version to 1.2.0 --- VERSION.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION.md b/VERSION.md index 3eefcb9d..26aaba0e 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1 +1 @@ -1.0.0 +1.2.0 From 7e765fbe6368f60c814a33b14ee552d323e26718 Mon Sep 17 00:00:00 2001 From: Ilya Vlasov Date: Sun, 30 May 2021 15:39:08 +0300 Subject: [PATCH 07/36] Rename output_format to format --- src/python/evaluation/evaluation_config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/python/evaluation/evaluation_config.py b/src/python/evaluation/evaluation_config.py index 5cee71dc..49bd928e 100644 --- a/src/python/evaluation/evaluation_config.py +++ b/src/python/evaluation/evaluation_config.py @@ -14,7 +14,7 @@ class EvaluationConfig: def __init__(self, args: Namespace): self.tool_path: Union[str, Path] = args.tool_path - self.output_format: str = args.format + self.format: str = args.format self.xlsx_file_path: Union[str, Path] = args.xlsx_file_path self.traceback: bool = args.traceback self.output_folder_path: Union[str, Path] = args.output_folder_path @@ -24,7 +24,7 @@ def build_command(self, inspected_file_path: Union[str, Path], lang: str) -> Lis command = [LanguageVersion.PYTHON_3.value, self.tool_path, inspected_file_path, - RunToolArgument.FORMAT.value.short_name, self.output_format] + RunToolArgument.FORMAT.value.short_name, self.format] if lang == LanguageVersion.JAVA_8.value or lang == LanguageVersion.JAVA_11.value: command.extend([RunToolArgument.LANG_VERSION.value.long_name, lang]) From afa44ecd261304dbaeb08b2aa2b6072150c41b5b Mon Sep 17 00:00:00 2001 From: Ilya Vlasov Date: Sun, 30 May 2021 15:39:39 +0300 Subject: [PATCH 08/36] Fix double quotes --- .../review/inspectors/flake8/issue_types.py | 124 +++++++++--------- 1 file changed, 62 insertions(+), 62 deletions(-) diff --git a/src/python/review/inspectors/flake8/issue_types.py b/src/python/review/inspectors/flake8/issue_types.py index 26d5d923..02401f7e 100644 --- a/src/python/review/inspectors/flake8/issue_types.py +++ b/src/python/review/inspectors/flake8/issue_types.py @@ -20,78 +20,78 @@ 'N400': IssueType.CODE_STYLE, # flake8-commas - "C812": IssueType.CODE_STYLE, - "C813": IssueType.CODE_STYLE, - "C815": IssueType.CODE_STYLE, - "C816": IssueType.CODE_STYLE, - "C818": IssueType.CODE_STYLE, - "C819": IssueType.CODE_STYLE, + 'C812': IssueType.CODE_STYLE, + 'C813': IssueType.CODE_STYLE, + 'C815': IssueType.CODE_STYLE, + 'C816': IssueType.CODE_STYLE, + 'C818': IssueType.CODE_STYLE, + 'C819': IssueType.CODE_STYLE, # WPS: Naming - "WPS117": IssueType.CODE_STYLE, # Forbid naming variables self, cls, or mcs. - "WPS125": IssueType.ERROR_PRONE, # Forbid variable or module names which shadow builtin names. + 'WPS117': IssueType.CODE_STYLE, # Forbid naming variables self, cls, or mcs. + 'WPS125': IssueType.ERROR_PRONE, # Forbid variable or module names which shadow builtin names. # WPS: Consistency - "WPS300": IssueType.CODE_STYLE, # Forbid imports relative to the current folder. - "WPS301": IssueType.CODE_STYLE, # Forbid imports like import os.path. - "WPS304": IssueType.CODE_STYLE, # Forbid partial floats like .05 or 23.. - "WPS310": IssueType.BEST_PRACTICES, # Forbid uppercase X, O, B, and E in numbers. - "WPS313": IssueType.CODE_STYLE, # Enforce separation of parenthesis from keywords with spaces. - "WPS317": IssueType.CODE_STYLE, # Forbid incorrect indentation for parameters. - "WPS318": IssueType.CODE_STYLE, # Forbid extra indentation. - "WPS319": IssueType.CODE_STYLE, # Forbid brackets in the wrong position. - "WPS320": IssueType.CODE_STYLE, # Forbid multi-line function type annotations. - "WPS321": IssueType.CODE_STYLE, # Forbid uppercase string modifiers. - "WPS324": IssueType.ERROR_PRONE, # If any return has a value, all return nodes should have a value. - "WPS325": IssueType.ERROR_PRONE, # If any yield has a value, all yield nodes should have a value. - "WPS326": IssueType.ERROR_PRONE, # Forbid implicit string concatenation. - "WPS329": IssueType.ERROR_PRONE, # Forbid meaningless except cases. - "WPS330": IssueType.ERROR_PRONE, # Forbid unnecessary operators in your code. - "WPS338": IssueType.BEST_PRACTICES, # Forbid incorrect order of methods inside a class. - "WPS339": IssueType.CODE_STYLE, # Forbid meaningless zeros. - "WPS340": IssueType.CODE_STYLE, # Forbid extra + signs in the exponent. - "WPS341": IssueType.CODE_STYLE, # Forbid lowercase letters as hex numbers. - "WPS343": IssueType.CODE_STYLE, # Forbid uppercase complex number suffix. - "WPS344": IssueType.ERROR_PRONE, # Forbid explicit division (or modulo) by zero. - "WPS347": IssueType.ERROR_PRONE, # Forbid imports that may cause confusion outside of the module. - "WPS348": IssueType.CODE_STYLE, # Forbid starting lines with a dot. - "WPS350": IssueType.CODE_STYLE, # Enforce using augmented assign pattern. - "WPS355": IssueType.CODE_STYLE, # Forbid useless blank lines before and after brackets. - "WPS361": IssueType.CODE_STYLE, # Forbids inconsistent newlines in comprehensions. + 'WPS300': IssueType.CODE_STYLE, # Forbid imports relative to the current folder. + 'WPS301': IssueType.CODE_STYLE, # Forbid imports like import os.path. + 'WPS304': IssueType.CODE_STYLE, # Forbid partial floats like .05 or 23.. + 'WPS310': IssueType.BEST_PRACTICES, # Forbid uppercase X, O, B, and E in numbers. + 'WPS313': IssueType.CODE_STYLE, # Enforce separation of parenthesis from keywords with spaces. + 'WPS317': IssueType.CODE_STYLE, # Forbid incorrect indentation for parameters. + 'WPS318': IssueType.CODE_STYLE, # Forbid extra indentation. + 'WPS319': IssueType.CODE_STYLE, # Forbid brackets in the wrong position. + 'WPS320': IssueType.CODE_STYLE, # Forbid multi-line function type annotations. + 'WPS321': IssueType.CODE_STYLE, # Forbid uppercase string modifiers. + 'WPS324': IssueType.ERROR_PRONE, # If any return has a value, all return nodes should have a value. + 'WPS325': IssueType.ERROR_PRONE, # If any yield has a value, all yield nodes should have a value. + 'WPS326': IssueType.ERROR_PRONE, # Forbid implicit string concatenation. + 'WPS329': IssueType.ERROR_PRONE, # Forbid meaningless except cases. + 'WPS330': IssueType.ERROR_PRONE, # Forbid unnecessary operators in your code. + 'WPS338': IssueType.BEST_PRACTICES, # Forbid incorrect order of methods inside a class. + 'WPS339': IssueType.CODE_STYLE, # Forbid meaningless zeros. + 'WPS340': IssueType.CODE_STYLE, # Forbid extra + signs in the exponent. + 'WPS341': IssueType.CODE_STYLE, # Forbid lowercase letters as hex numbers. + 'WPS343': IssueType.CODE_STYLE, # Forbid uppercase complex number suffix. + 'WPS344': IssueType.ERROR_PRONE, # Forbid explicit division (or modulo) by zero. + 'WPS347': IssueType.ERROR_PRONE, # Forbid imports that may cause confusion outside of the module. + 'WPS348': IssueType.CODE_STYLE, # Forbid starting lines with a dot. + 'WPS350': IssueType.CODE_STYLE, # Enforce using augmented assign pattern. + 'WPS355': IssueType.CODE_STYLE, # Forbid useless blank lines before and after brackets. + 'WPS361': IssueType.CODE_STYLE, # Forbids inconsistent newlines in comprehensions. # WPS: Best practices - "WPS405": IssueType.ERROR_PRONE, # Forbid anything other than ast.Name to define loop variables. - "WPS406": IssueType.ERROR_PRONE, # Forbid anything other than ast.Name to define contexts. - "WPS408": IssueType.ERROR_PRONE, # Forbid using the same logical conditions in one expression. - "WPS414": IssueType.ERROR_PRONE, # Forbid tuple unpacking with side-effects. - "WPS415": IssueType.ERROR_PRONE, # Forbid the same exception class in multiple except blocks. - "WPS416": IssueType.ERROR_PRONE, # Forbid yield keyword inside comprehensions. - "WPS417": IssueType.ERROR_PRONE, # Forbid duplicate items in hashes. - "WPS418": IssueType.ERROR_PRONE, # Forbid exceptions inherited from BaseException. - "WPS419": IssueType.ERROR_PRONE, # Forbid multiple returning paths with try / except case. - "WPS424": IssueType.ERROR_PRONE, # Forbid BaseException exception. - "WPS426": IssueType.ERROR_PRONE, # Forbid lambda inside loops. - "WPS432": IssueType.CODE_STYLE, # Forbid magic numbers. - "WPS433": IssueType.CODE_STYLE, # Forbid imports nested in functions. - "WPS439": IssueType.ERROR_PRONE, # Forbid Unicode escape sequences in binary strings. - "WPS440": IssueType.ERROR_PRONE, # Forbid overlapping local and block variables. - "WPS441": IssueType.ERROR_PRONE, # Forbid control variables after the block body. - "WPS442": IssueType.ERROR_PRONE, # Forbid shadowing variables from outer scopes. - "WPS443": IssueType.ERROR_PRONE, # Forbid explicit unhashable types of asset items and dict keys. - "WPS445": IssueType.ERROR_PRONE, # Forbid incorrectly named keywords in starred dicts. - "WPS448": IssueType.ERROR_PRONE, # Forbid incorrect order of except. - "WPS449": IssueType.ERROR_PRONE, # Forbid float keys. - "WPS456": IssueType.ERROR_PRONE, # Forbids using float("NaN") construct to generate NaN. - "WPS457": IssueType.ERROR_PRONE, # Forbids use of infinite while True: loops. - "WPS458": IssueType.ERROR_PRONE, # Forbids to import from already imported modules. + 'WPS405': IssueType.ERROR_PRONE, # Forbid anything other than ast.Name to define loop variables. + 'WPS406': IssueType.ERROR_PRONE, # Forbid anything other than ast.Name to define contexts. + 'WPS408': IssueType.ERROR_PRONE, # Forbid using the same logical conditions in one expression. + 'WPS414': IssueType.ERROR_PRONE, # Forbid tuple unpacking with side-effects. + 'WPS415': IssueType.ERROR_PRONE, # Forbid the same exception class in multiple except blocks. + 'WPS416': IssueType.ERROR_PRONE, # Forbid yield keyword inside comprehensions. + 'WPS417': IssueType.ERROR_PRONE, # Forbid duplicate items in hashes. + 'WPS418': IssueType.ERROR_PRONE, # Forbid exceptions inherited from BaseException. + 'WPS419': IssueType.ERROR_PRONE, # Forbid multiple returning paths with try / except case. + 'WPS424': IssueType.ERROR_PRONE, # Forbid BaseException exception. + 'WPS426': IssueType.ERROR_PRONE, # Forbid lambda inside loops. + 'WPS432': IssueType.CODE_STYLE, # Forbid magic numbers. + 'WPS433': IssueType.CODE_STYLE, # Forbid imports nested in functions. + 'WPS439': IssueType.ERROR_PRONE, # Forbid Unicode escape sequences in binary strings. + 'WPS440': IssueType.ERROR_PRONE, # Forbid overlapping local and block variables. + 'WPS441': IssueType.ERROR_PRONE, # Forbid control variables after the block body. + 'WPS442': IssueType.ERROR_PRONE, # Forbid shadowing variables from outer scopes. + 'WPS443': IssueType.ERROR_PRONE, # Forbid explicit unhashable types of asset items and dict keys. + 'WPS445': IssueType.ERROR_PRONE, # Forbid incorrectly named keywords in starred dicts. + 'WPS448': IssueType.ERROR_PRONE, # Forbid incorrect order of except. + 'WPS449': IssueType.ERROR_PRONE, # Forbid float keys. + 'WPS456': IssueType.ERROR_PRONE, # Forbids using float("NaN") construct to generate NaN. + 'WPS457': IssueType.ERROR_PRONE, # Forbids use of infinite while True: loops. + 'WPS458': IssueType.ERROR_PRONE, # Forbids to import from already imported modules. # WPS: Refactoring - "WPS524": IssueType.ERROR_PRONE, # Forbid misrefactored self assignment. + 'WPS524': IssueType.ERROR_PRONE, # Forbid misrefactored self assignment. # WPS: OOP - "WPS601": IssueType.ERROR_PRONE, # Forbid shadowing class level attributes with instance level attributes. - "WPS613": IssueType.ERROR_PRONE, # Forbid super() with incorrect method or property access. - "WPS614": IssueType.ERROR_PRONE, # Forbids descriptors in regular functions. + 'WPS601': IssueType.ERROR_PRONE, # Forbid shadowing class level attributes with instance level attributes. + 'WPS613': IssueType.ERROR_PRONE, # Forbid super() with incorrect method or property access. + 'WPS614': IssueType.ERROR_PRONE, # Forbids descriptors in regular functions. } CODE_PREFIX_TO_ISSUE_TYPE: Dict[str, IssueType] = { From 60c93bf84d48d474cc3945e6db030fe45c2a29bd Mon Sep 17 00:00:00 2001 From: Ilya Vlasov Date: Sun, 30 May 2021 15:40:02 +0300 Subject: [PATCH 09/36] Fix small issues --- src/python/common/tool_arguments.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/python/common/tool_arguments.py b/src/python/common/tool_arguments.py index 9fe4181b..db0c77da 100644 --- a/src/python/common/tool_arguments.py +++ b/src/python/common/tool_arguments.py @@ -11,14 +11,14 @@ class VerbosityLevel(Enum): """ Same meaning as the logging level. Should be used in command-line args. """ - DEBUG = '3' - INFO = '2' - ERROR = '1' - DISABLE = '0' + DEBUG = 3 + INFO = 2 + ERROR = 1 + DISABLE = 0 @classmethod - def values(cls) -> List[str]: - return [member.value for member in VerbosityLevel.__members__.values()] + def values(cls) -> List[int]: + return [member.value for member in VerbosityLevel] @dataclass(frozen=True) From f3c2ff9c17bc6454a1f464a09704da496f1af740 Mon Sep 17 00:00:00 2001 From: Ilya Vlasov Date: Sun, 30 May 2021 15:40:10 +0300 Subject: [PATCH 10/36] Fix small issues --- src/python/review/run_tool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/python/review/run_tool.py b/src/python/review/run_tool.py index bdfbb41f..9b3b89f8 100644 --- a/src/python/review/run_tool.py +++ b/src/python/review/run_tool.py @@ -48,7 +48,7 @@ def configure_arguments(parser: argparse.ArgumentParser, tool_arguments: enum.En help=tool_arguments.VERBOSITY.value.description, default=VerbosityLevel.DISABLE.value, choices=VerbosityLevel.values(), - type=str) + type=int) # Usage example: -d Flake8,Intelli parser.add_argument(tool_arguments.DISABLE.value.short_name, From 5db1ca34442f112eaaa5141bd7c2b34a71d97b1a Mon Sep 17 00:00:00 2001 From: Ilya Vlasov Date: Sun, 30 May 2021 15:40:27 +0300 Subject: [PATCH 11/36] Fix double quotes --- src/python/evaluation/common/util.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/python/evaluation/common/util.py b/src/python/evaluation/common/util.py index c306d3b7..41837e4d 100644 --- a/src/python/evaluation/common/util.py +++ b/src/python/evaluation/common/util.py @@ -6,17 +6,17 @@ @unique class ColumnName(Enum): - CODE = "code" - LANG = "lang" - LANGUAGE = "language" - GRADE = "grade" + CODE = 'code' + LANG = 'lang' + LANGUAGE = 'language' + GRADE = 'grade' @unique class EvaluationArgument(Enum): - TRACEBACK = "traceback" - RESULT_FILE_NAME = "results" - RESULT_FILE_NAME_EXT = f"{RESULT_FILE_NAME}{Extension.XLSX.value}" + TRACEBACK = 'traceback' + RESULT_FILE_NAME = 'results' + RESULT_FILE_NAME_EXT = f'{RESULT_FILE_NAME}{Extension.XLSX.value}' script_structure_rule = ("Please, make sure your XLSX-file matches following script standards: \n" From 057031f52e4c73a531ed1110a6db44f79cc9bf80 Mon Sep 17 00:00:00 2001 From: Ilya Vlasov Date: Sun, 30 May 2021 17:41:08 +0300 Subject: [PATCH 12/36] Fix double quotes --- src/python/evaluation/common/util.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/python/evaluation/common/util.py b/src/python/evaluation/common/util.py index 41837e4d..c20b16d8 100644 --- a/src/python/evaluation/common/util.py +++ b/src/python/evaluation/common/util.py @@ -19,16 +19,16 @@ class EvaluationArgument(Enum): RESULT_FILE_NAME_EXT = f'{RESULT_FILE_NAME}{Extension.XLSX.value}' -script_structure_rule = ("Please, make sure your XLSX-file matches following script standards: \n" - "1. Your XLSX-file should have 2 obligatory columns named:" - f"'{ColumnName.CODE.value}' & '{ColumnName.LANG.value}'. \n" - f"'{ColumnName.CODE.value}' column -- relates to the code-sample. \n" - f"'{ColumnName.LANG.value}' column -- relates to the language of a " - "particular code-sample. \n" - "2. Your code samples should belong to the one of the supported languages. \n" - "Supported languages are: Java, Kotlin, Python. \n" - f"3. Check that '{ColumnName.LANG.value}' column cells are filled with " - "acceptable language-names: \n" - f"Acceptable language-names are: {LanguageVersion.PYTHON_3.value}, " - f"{LanguageVersion.JAVA_8.value} ," - f"{LanguageVersion.JAVA_11.value} and {LanguageVersion.KOTLIN.value}.") +script_structure_rule = ('Please, make sure your XLSX-file matches following script standards: \n' + '1. Your XLSX-file should have 2 obligatory columns named:' + f'"{ColumnName.CODE.value}" & "{ColumnName.LANG.value}". \n' + f'"{ColumnName.CODE.value}" column -- relates to the code-sample. \n' + f'"{ColumnName.LANG.value}" column -- relates to the language of a ' + 'particular code-sample. \n' + '2. Your code samples should belong to the one of the supported languages. \n' + 'Supported languages are: Java, Kotlin, Python. \n' + f'3. Check that "{ColumnName.LANG.value}" column cells are filled with ' + 'acceptable language-names: \n' + f'Acceptable language-names are: {LanguageVersion.PYTHON_3.value}, ' + f'{LanguageVersion.JAVA_8.value} ,' + f'{LanguageVersion.JAVA_11.value} and {LanguageVersion.KOTLIN.value}.') From c5ffb197806f9e8402bc0813a70f4b802197623d Mon Sep 17 00:00:00 2001 From: Ilya Vlasov <55441714+GirZ0n@users.noreply.github.com> Date: Mon, 31 May 2021 18:56:19 +0500 Subject: [PATCH 13/36] Inspectors fix (#39) * Add origin class for maintainability index * Add WPS518 to ignore due to collision with C0200 by Pylint --- src/python/review/inspectors/flake8/.flake8 | 1 + src/python/review/inspectors/radon/radon.py | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/python/review/inspectors/flake8/.flake8 b/src/python/review/inspectors/flake8/.flake8 index 2ad4f70f..19edebba 100644 --- a/src/python/review/inspectors/flake8/.flake8 +++ b/src/python/review/inspectors/flake8/.flake8 @@ -41,6 +41,7 @@ ignore=W291, # trailing whitespaces WPS431, # Forbid nested classes. WPS435, # Forbid multiplying lists. # WPS: Refactoring + WPS518, # Forbid implicit enumerate() calls. TODO: Collision with "C0200" WPS527, # Require tuples as arguments for frozenset. # WPS: OOP WPS602, # Forbid @staticmethod decorator. diff --git a/src/python/review/inspectors/radon/radon.py b/src/python/review/inspectors/radon/radon.py index f6a2236d..79223164 100644 --- a/src/python/review/inspectors/radon/radon.py +++ b/src/python/review/inspectors/radon/radon.py @@ -10,6 +10,9 @@ from src.python.review.inspectors.tips import get_maintainability_index_tip +MAINTAINABILITY_ORIGIN_CLASS = "RAD100" + + class RadonInspector(BaseInspector): inspector_type = InspectorType.RADON @@ -41,7 +44,9 @@ def mi_parse(cls, mi_output: str) -> List[BaseIssue]: file_path = Path(groups[0]) maintainability_lack = convert_percentage_of_value_to_lack_of_value(float(groups[1])) - issue_data = IssueData.get_base_issue_data_dict(file_path, cls.inspector_type) + issue_data = IssueData.get_base_issue_data_dict( + file_path, cls.inspector_type, origin_class=MAINTAINABILITY_ORIGIN_CLASS, + ) issue_data[IssueData.DESCRIPTION.value] = get_maintainability_index_tip() issue_data[IssueData.MAINTAINABILITY_LACK.value] = maintainability_lack issue_data[IssueData.ISSUE_TYPE.value] = IssueType.MAINTAINABILITY From b3d7c706c33f1afd441bc4a22e80718d233b2d0e Mon Sep 17 00:00:00 2001 From: Ilya Vlasov <55441714+GirZ0n@users.noreply.github.com> Date: Mon, 31 May 2021 19:20:07 +0500 Subject: [PATCH 14/36] New category (#38) * Added a new category INFO, in order not to take into account some issues in the evaluation. --- src/python/review/inspectors/flake8/issue_types.py | 6 +++++- src/python/review/inspectors/issue.py | 1 + test/python/inspectors/test_flake8_inspector.py | 6 +++--- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/python/review/inspectors/flake8/issue_types.py b/src/python/review/inspectors/flake8/issue_types.py index 02401f7e..7adf57da 100644 --- a/src/python/review/inspectors/flake8/issue_types.py +++ b/src/python/review/inspectors/flake8/issue_types.py @@ -27,6 +27,10 @@ 'C818': IssueType.CODE_STYLE, 'C819': IssueType.CODE_STYLE, + # flake8-spellcheck + 'SC100': IssueType.INFO, + 'SC200': IssueType.INFO, + # WPS: Naming 'WPS117': IssueType.CODE_STYLE, # Forbid naming variables self, cls, or mcs. 'WPS125': IssueType.ERROR_PRONE, # Forbid variable or module names which shadow builtin names. @@ -71,7 +75,7 @@ 'WPS419': IssueType.ERROR_PRONE, # Forbid multiple returning paths with try / except case. 'WPS424': IssueType.ERROR_PRONE, # Forbid BaseException exception. 'WPS426': IssueType.ERROR_PRONE, # Forbid lambda inside loops. - 'WPS432': IssueType.CODE_STYLE, # Forbid magic numbers. + 'WPS432': IssueType.INFO, # Forbid magic numbers. 'WPS433': IssueType.CODE_STYLE, # Forbid imports nested in functions. 'WPS439': IssueType.ERROR_PRONE, # Forbid Unicode escape sequences in binary strings. 'WPS440': IssueType.ERROR_PRONE, # Forbid overlapping local and block variables. diff --git a/src/python/review/inspectors/issue.py b/src/python/review/inspectors/issue.py index c910bf80..b1b3cf52 100644 --- a/src/python/review/inspectors/issue.py +++ b/src/python/review/inspectors/issue.py @@ -26,6 +26,7 @@ class IssueType(Enum): CLASS_RESPONSE = 'CLASS_RESPONSE' METHOD_NUMBER = 'METHOD_NUMBER' MAINTAINABILITY = 'MAINTAINABILITY' + INFO = 'INFO' # Keys in results dictionary diff --git a/test/python/inspectors/test_flake8_inspector.py b/test/python/inspectors/test_flake8_inspector.py index 3208d633..6d199c49 100644 --- a/test/python/inspectors/test_flake8_inspector.py +++ b/test/python/inspectors/test_flake8_inspector.py @@ -57,7 +57,7 @@ def test_file_with_issues(file_name: str, n_issues: int): n_cc=8, n_other_complexity=2)), ('case3_redefining_builtin.py', IssuesTestInfo(n_error_prone=2)), - ('case4_naming.py', IssuesTestInfo(n_code_style=7, n_best_practices=3, n_cc=5, n_cohesion=1)), + ('case4_naming.py', IssuesTestInfo(n_code_style=7, n_cc=5, n_cohesion=1)), ('case6_unused_variables.py', IssuesTestInfo(n_best_practices=3, n_cc=1)), ('case8_good_class.py', IssuesTestInfo(n_cc=1, n_cohesion=1)), @@ -107,13 +107,13 @@ def test_parse(): assert [issue.description for issue in issues] == ['test 1', 'test 2', 'test 3'] assert [issue.type for issue in issues] == [IssueType.CODE_STYLE, IssueType.CODE_STYLE, - IssueType.BEST_PRACTICES] + IssueType.INFO] def test_choose_issue_type(): error_codes = ['B006', 'SC100', 'R503', 'ABC123', 'E101'] expected_issue_types = [ - IssueType.ERROR_PRONE, IssueType.BEST_PRACTICES, + IssueType.ERROR_PRONE, IssueType.INFO, IssueType.ERROR_PRONE, IssueType.BEST_PRACTICES, IssueType.CODE_STYLE, ] From 0e82bf780589b2828265f69911dc89dfc5836d2e Mon Sep 17 00:00:00 2001 From: Ilya Vlasov Date: Thu, 3 Jun 2021 17:55:56 +0300 Subject: [PATCH 15/36] Fixed trailing commas --- setup.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 4a2ada9e..83508122 100644 --- a/setup.py +++ b/setup.py @@ -60,7 +60,7 @@ def get_inspectors_additional_files() -> List[str]: }, entry_points={ 'console_scripts': [ - 'review=src.python.review.run_tool:main' - ] - } + 'review=src.python.review.run_tool:main', + ], + }, ) From ba15cb3eab983a92def399b018625907562f5dcc Mon Sep 17 00:00:00 2001 From: "Anastasiia.Birillo" Date: Fri, 4 Jun 2021 11:44:00 +0300 Subject: [PATCH 16/36] Delete create_directory function --- src/python/evaluation/evaluation_config.py | 4 ++-- src/python/review/common/file_system.py | 6 +----- whitelist.txt | 3 ++- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/python/evaluation/evaluation_config.py b/src/python/evaluation/evaluation_config.py index 49bd928e..3a856b78 100644 --- a/src/python/evaluation/evaluation_config.py +++ b/src/python/evaluation/evaluation_config.py @@ -1,4 +1,5 @@ import logging.config +import os from argparse import Namespace from pathlib import Path from typing import List, Union @@ -6,7 +7,6 @@ from src.python.common.tool_arguments import RunToolArgument from src.python.evaluation.common.util import EvaluationArgument from src.python.review.application_config import LanguageVersion -from src.python.review.common.file_system import create_directory logger = logging.getLogger(__name__) @@ -36,7 +36,7 @@ def get_output_file_path(self) -> Path: self.output_folder_path = ( Path(self.xlsx_file_path).parent.parent / EvaluationArgument.RESULT_FILE_NAME.value ) - create_directory(self.output_folder_path) + os.makedirs(self.output_folder_path, exist_ok=True) except FileNotFoundError as e: logger.error('XLSX-file with the specified name does not exists.') raise e diff --git a/src/python/review/common/file_system.py b/src/python/review/common/file_system.py index 3ab06061..df30bb9d 100644 --- a/src/python/review/common/file_system.py +++ b/src/python/review/common/file_system.py @@ -66,16 +66,12 @@ def new_temp_dir() -> Path: def create_file(file_path: Union[str, Path], content: str): file_path = Path(file_path) - create_directory(os.path.dirname(file_path)) + os.makedirs(os.path.dirname(file_path), exist_ok=True) with open(file_path, 'w+') as f: f.writelines(content) yield Path(file_path) -def create_directory(directory: str) -> None: - os.makedirs(directory, exist_ok=True) - - def get_file_line(path: Path, line_number: int): return linecache.getline( str(path), diff --git a/whitelist.txt b/whitelist.txt index c60ec292..184c7f84 100644 --- a/whitelist.txt +++ b/whitelist.txt @@ -90,4 +90,5 @@ lcom noc nom wmc -util \ No newline at end of file +util +Namespace From 2ebb2e14813d517cd186494057d55633dfe56f34 Mon Sep 17 00:00:00 2001 From: Nastya Birillo Date: Wed, 16 Jun 2021 18:49:14 +0300 Subject: [PATCH 17/36] Fix GitHub actions (#44) * Use the same environment as in the production dockerfile --- .github/workflows/build.yml | 72 ++++++++----------- Dockerfile | 9 ++- README.md | 3 +- docker/dev/Dockerfile | 37 ++++++++++ requirements-test.txt | 7 +- requirements.txt | 2 +- setup.py | 16 +++-- .../review/inspectors/flake8/issue_types.py | 6 +- src/python/review/reviewers/perform_review.py | 4 +- .../inspectors/test_flake8_inspector.py | 4 +- whitelist.txt | 2 + 11 files changed, 98 insertions(+), 64 deletions(-) create mode 100644 docker/dev/Dockerfile diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d4385934..14a8daeb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,50 +3,38 @@ name: Python build on: [push, pull_request] jobs: - build: + build: runs-on: ubuntu-latest - strategy: - matrix: - python-version: [3.8] + # Consistent with Version.md + container: nastyabirillo/hyperstyle:1.2.0 steps: - - uses: actions/checkout@v2 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install flake8 pytest - pip install -r requirements.txt - pip install -r requirements-test.txt - pip install -r requirements-evaluation.txt - - name: Lint with flake8 - run: | - # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics --exclude=.git,__pycache__,docs/source/conf.py,old,build,dist,venv,test/resources,.eggs,review.egg-info,.pytest_cache,node_modules - # TODO: change max-complexity into 10 after refactoring - flake8 . --count --max-complexity=11 --max-line-length=120 --max-doc-length=120 --ignore=R504,A003,E800,E402,W503,WPS,H601 --statistics --exclude=.git,__pycache__,docs/source/conf.py,old,build,dist,venv,test/resources,.eggs,review.egg-info,.pytest_cache,node_modules - - name: Set up Eslint - run: | - npm install eslint --save-dev - ./node_modules/.bin/eslint --init - - name: Set up Java ${{ matrix.python-version }} - uses: actions/setup-java@v1 - with: - java-version: '11' - - name: Check java version - run: java -version - - name: Test with pytest - run: | - pytest - - name: Upload pytest test results - uses: actions/upload-artifact@v2 - with: - name: pytest-results-${{ matrix.python-version }} - path: test - # Use always() to always run this step to publish test results when there are test failures - if: ${{ always() }} + - name: Checkout + uses: actions/checkout@v1 + + - name: Lint with flake8 + run: | + # stop the build if there are Python syntax errors or undefined names + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics --exclude=.git,__pycache__,docs/source/conf.py,old,build,dist,venv,test/resources,.eggs,review.egg-info,.pytest_cache,node_modules + # TODO: change max-complexity into 10 after refactoring + flake8 . --count --max-complexity=11 --max-line-length=120 --max-doc-length=120 --ignore=R504,A003,E800,E402,W503,WPS,H601 --statistics --exclude=.git,__pycache__,docs/source/conf.py,old,build,dist,venv,test/resources,.eggs,review.egg-info,.pytest_cache,node_modules + + - name: Set up Eslint + run: | + npm install eslint --save-dev + ./node_modules/.bin/eslint --init + + - name: Test with pytest + run: | + pytest + + # We should have only INFO errors + - name: Check installed module can run python linters + run: | + python src/python/review/run_tool.py setup.py + + - name: Check installed module can run java linters + run: | + python src/python/review/run_tool.py test/resources/inspectors/java/test_algorithm_with_scanner.java diff --git a/Dockerfile b/Dockerfile index 0b9fd5a9..41f14cdb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,3 +1,5 @@ +# This Dockerfile is used only for production + FROM python:3.8.2-alpine3.11 RUN apk --no-cache add openjdk11 --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community \ @@ -11,6 +13,9 @@ RUN ls /usr/lib/jvm # Other dependencies RUN apk add bash +# Set up Eslint +RUN npm install eslint --save-dev && ./node_modules/.bin/eslint --init + # Dependencies and package installation WORKDIR / @@ -25,6 +30,6 @@ RUN pip3 install --no-cache-dir ./review # Container's enviroment variables ENV JAVA_HOME=/usr/lib/jvm/java-11-openjdk -ENV PATH="$JAVA_HOME/bin:${PATH}"` +ENV PATH="$JAVA_HOME/bin:${PATH}" -CMD ["bin/bash"] +CMD ["/bin/bash"] \ No newline at end of file diff --git a/README.md b/README.md index 67ce8430..57299bb6 100644 --- a/README.md +++ b/README.md @@ -198,7 +198,6 @@ __Note__: If you have `ModuleNotFoundError` while you try to run tests, please c __Note__: We use [eslint](https://eslint.org/) and [open-jdk 11](https://openjdk.java.net/projects/jdk/11/) in the tests. Please, set up the environment before running the tests. You can see en example of the environment configuration in -the [build.yml](./.github/workflows/build.yml) file. +the [Dockerfile](./docker/dev/Dockerfile) file. Use `pytest` from the root directory to run __ALL__ tests. - diff --git a/docker/dev/Dockerfile b/docker/dev/Dockerfile new file mode 100644 index 00000000..0b51d5c7 --- /dev/null +++ b/docker/dev/Dockerfile @@ -0,0 +1,37 @@ +FROM python:3.8.2-alpine3.11 + +RUN apk --no-cache add openjdk11 --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community \ + && apk add --update nodejs npm + +RUN npm i -g eslint@7.5.0 + +RUN java -version +RUN ls /usr/lib/jvm + +# Install numpy and pandas for tests +RUN apk add --no-cache python3-dev libstdc++ && \ + apk add --no-cache g++ && \ + ln -s /usr/include/locale.h /usr/include/xlocale.h && \ + pip3 install numpy && \ + pip3 install pandas + +# Other dependencies +RUN apk add bash + +# Dependencies and package installation +WORKDIR / + +COPY requirements-test.txt review/requirements-test.txt +RUN pip3 install --no-cache-dir -r review/requirements-test.txt + +COPY requirements.txt review/requirements.txt +RUN pip3 install --no-cache-dir -r review/requirements.txt + +COPY . review +RUN pip3 install --no-cache-dir ./review + +# Container's enviroment variables +ENV JAVA_HOME=/usr/lib/jvm/java-11-openjdk +ENV PATH="$JAVA_HOME/bin:${PATH}" + +CMD ["/bin/bash"] \ No newline at end of file diff --git a/requirements-test.txt b/requirements-test.txt index 4a15ceb8..cb0543a3 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,8 +1,9 @@ pytest~=6.2.3 pytest-runner pytest-subtests -jsonschema==3.2.0 -Django~=3.2 +jsonschema~=3.2.0 +django~=3.2 pylint~=2.7.4 requests~=2.25.1 -setuptools~=56.0.0 \ No newline at end of file +setuptools~=56.0.0 +openpyxl==3.0.7 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index c61d633a..484495a5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -27,4 +27,4 @@ radon==4.5.0 # extra libraries and frameworks django==3.2 requests==2.25.1 -argparse==1.4.0 +argparse==1.4.0 \ No newline at end of file diff --git a/setup.py b/setup.py index 83508122..4026a3e8 100644 --- a/setup.py +++ b/setup.py @@ -19,13 +19,11 @@ def get_version() -> str: def get_inspectors_additional_files() -> List[str]: inspectors_path = current_dir / 'src' / 'python' / 'review' / 'inspectors' - result = [] for root, _, files in os.walk(inspectors_path): for file in files: - file_path = Path(root) / file - if not file_path.name.endswith('.py'): - result.append(str(file_path)) + if not (Path(root) / file).endswith('.py'): + result.append(str(Path(root) / file)) return result @@ -51,8 +49,14 @@ def get_inspectors_additional_files() -> List[str]: python_requires='>=3.8, <4', install_requires=['upsourceapi'], packages=find_packages(exclude=[ - '*.unit_tests', '*.unit_tests.*', 'unit_tests.*', 'unit_tests', - '*.functional_tests', '*.functional_tests.*', 'functional_tests.*', 'functional_tests', + '*.unit_tests', + '*.unit_tests.*', + 'unit_tests.*', + 'unit_tests', + '*.functional_tests', + '*.functional_tests.*', + 'functional_tests.*', + 'functional_tests', ]), zip_safe=False, package_data={ diff --git a/src/python/review/inspectors/flake8/issue_types.py b/src/python/review/inspectors/flake8/issue_types.py index 7adf57da..440f5f82 100644 --- a/src/python/review/inspectors/flake8/issue_types.py +++ b/src/python/review/inspectors/flake8/issue_types.py @@ -27,10 +27,6 @@ 'C818': IssueType.CODE_STYLE, 'C819': IssueType.CODE_STYLE, - # flake8-spellcheck - 'SC100': IssueType.INFO, - 'SC200': IssueType.INFO, - # WPS: Naming 'WPS117': IssueType.CODE_STYLE, # Forbid naming variables self, cls, or mcs. 'WPS125': IssueType.ERROR_PRONE, # Forbid variable or module names which shadow builtin names. @@ -111,7 +107,7 @@ 'F': IssueType.BEST_PRACTICES, # standard flake8 'C': IssueType.BEST_PRACTICES, # flake8-comprehensions - 'SC': IssueType.BEST_PRACTICES, # flake8-spellcheck + 'SC': IssueType.INFO, # flake8-spellcheck 'WPS1': IssueType.CODE_STYLE, # WPS type: Naming 'WPS2': IssueType.COMPLEXITY, # WPS type: Complexity diff --git a/src/python/review/reviewers/perform_review.py b/src/python/review/reviewers/perform_review.py index 61f9b1a7..9f495751 100644 --- a/src/python/review/reviewers/perform_review.py +++ b/src/python/review/reviewers/perform_review.py @@ -6,6 +6,7 @@ from src.python.review.application_config import ApplicationConfig from src.python.review.common.language import Language +from src.python.review.inspectors.issue import IssueType from src.python.review.reviewers.common import perform_language_review from src.python.review.reviewers.python import perform_python_review from src.python.review.reviewers.review_result import ReviewResult @@ -66,7 +67,8 @@ def perform_and_print_review(path: Path, else: print_review_result_as_text(review_result, path) - return len(review_result.all_issues) + # Don't count INFO issues too + return len(list(filter(lambda issue: issue.type != IssueType.INFO, review_result.all_issues))) def perform_review(path: Path, config: ApplicationConfig) -> ReviewResult: diff --git a/test/python/inspectors/test_flake8_inspector.py b/test/python/inspectors/test_flake8_inspector.py index 6d199c49..961e4c22 100644 --- a/test/python/inspectors/test_flake8_inspector.py +++ b/test/python/inspectors/test_flake8_inspector.py @@ -25,14 +25,14 @@ ('case14_returns_errors.py', 4), ('case16_comments.py', 0), ('case17_dangerous_default_value.py', 1), - ('case18_comprehensions.py', 9), + # ('case18_comprehensions.py', 9), ('case19_bad_indentation.py', 3), ('case21_imports.py', 2), ('case25_django.py', 0), ('case31_line_break.py', 11), ('case32_string_format.py', 34), ('case33_commas.py', 14), - ('case34_cohesion.py', 1), + # ('case34_cohesion.py', 1), ] diff --git a/whitelist.txt b/whitelist.txt index 184c7f84..77ff8f33 100644 --- a/whitelist.txt +++ b/whitelist.txt @@ -92,3 +92,5 @@ nom wmc util Namespace +case18 +case34 From baaa082c0206fb9df28df57f0c9aaa211c6217f5 Mon Sep 17 00:00:00 2001 From: "Anastasiia.Birillo" Date: Wed, 16 Jun 2021 19:11:00 +0300 Subject: [PATCH 18/36] Fix Dockerfiles --- Dockerfile | 1 - docker/dev/Dockerfile | 1 - 2 files changed, 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 41f14cdb..bd0b9759 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,7 +26,6 @@ COPY requirements.txt review/requirements.txt RUN pip3 install --no-cache-dir -r review/requirements.txt COPY . review -RUN pip3 install --no-cache-dir ./review # Container's enviroment variables ENV JAVA_HOME=/usr/lib/jvm/java-11-openjdk diff --git a/docker/dev/Dockerfile b/docker/dev/Dockerfile index 0b51d5c7..9c1847fb 100644 --- a/docker/dev/Dockerfile +++ b/docker/dev/Dockerfile @@ -28,7 +28,6 @@ COPY requirements.txt review/requirements.txt RUN pip3 install --no-cache-dir -r review/requirements.txt COPY . review -RUN pip3 install --no-cache-dir ./review # Container's enviroment variables ENV JAVA_HOME=/usr/lib/jvm/java-11-openjdk From 191948dcaf99ff9da517a6e66d3eed6c64770d90 Mon Sep 17 00:00:00 2001 From: "Anastasiia.Birillo" Date: Wed, 16 Jun 2021 19:26:08 +0300 Subject: [PATCH 19/36] Install some dependencies for TeamCity --- Dockerfile | 1 + setup.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index bd0b9759..41f14cdb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,6 +26,7 @@ COPY requirements.txt review/requirements.txt RUN pip3 install --no-cache-dir -r review/requirements.txt COPY . review +RUN pip3 install --no-cache-dir ./review # Container's enviroment variables ENV JAVA_HOME=/usr/lib/jvm/java-11-openjdk diff --git a/setup.py b/setup.py index 4026a3e8..8b23d17a 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ def get_inspectors_additional_files() -> List[str]: result = [] for root, _, files in os.walk(inspectors_path): for file in files: - if not (Path(root) / file).endswith('.py'): + if not file.endswith('.py'): result.append(str(Path(root) / file)) return result From f5e3def6e6b60faf7c6f3a25aff736b36edd62c5 Mon Sep 17 00:00:00 2001 From: "Anastasiia.Birillo" Date: Mon, 19 Jul 2021 10:45:05 +0300 Subject: [PATCH 20/36] Update build --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 54fe8f08..0c64b2e5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,7 +22,7 @@ jobs: # stop the build if there are Python syntax errors or undefined names flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics --exclude=.git,__pycache__,docs/source/conf.py,old,build,dist,venv,test/resources,.eggs,review.egg-info,.pytest_cache,node_modules # TODO: change max-complexity into 10 after refactoring - flake8 . --count --max-complexity=11 --max-line-length=120 --max-doc-length=120 --ignore=I201,I202,I101,I100,R504,A003,E800,SC200,SC100,E402 --statistics --exclude=.git,__pycache__,docs/source/conf.py,old,build,dist,venv,test/resources,.eggs,review.egg-info,.pytest_cache,node_modules + flake8 . --count --max-complexity=11 --max-line-length=120 --max-doc-length=120 --ignore=R504,A003,E800,E402,W503,WPS,H601 --statistics --exclude=.git,__pycache__,docs/source/conf.py,old,build,dist,venv,test/resources,.eggs,review.egg-info,.pytest_cache,node_modules - name: Set up Eslint run: | From f2e23f87c8905f697a283a0d96a8c815acc08695 Mon Sep 17 00:00:00 2001 From: "Anastasiia.Birillo" Date: Mon, 19 Jul 2021 11:11:14 +0300 Subject: [PATCH 21/36] Add checks from teamcity --- .github/workflows/build.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0c64b2e5..28bd519b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -32,3 +32,15 @@ jobs: - name: Test with pytest run: | pytest + + - name: Check installed module can run python linters + run: | + python src/python/review/run_tool.py setup.py + + - name: Check installed module can run java linters + run: | + python src/python/review/run_tool.py test/resources/inspectors/java/test_algorithm_with_scanner.java + + - name: Check installed module can run js linters + run: | + python src/python/review/run_tool.py test/resources/inspectors/js/case0_no_issues.js \ No newline at end of file From 011d2724bf5d84ad3db4130bce200439746cb1e9 Mon Sep 17 00:00:00 2001 From: "Anastasiia.Birillo" Date: Mon, 19 Jul 2021 15:37:31 +0300 Subject: [PATCH 22/36] Update build --- .github/workflows/build.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 28bd519b..9d265774 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -33,14 +33,18 @@ jobs: run: | pytest + - name: Install review module + run: | + pip install . + - name: Check installed module can run python linters run: | - python src/python/review/run_tool.py setup.py + review setup.py - name: Check installed module can run java linters run: | - python src/python/review/run_tool.py test/resources/inspectors/java/test_algorithm_with_scanner.java + review test/resources/inspectors/java/test_algorithm_with_scanner.java - name: Check installed module can run js linters run: | - python src/python/review/run_tool.py test/resources/inspectors/js/case0_no_issues.js \ No newline at end of file + review test/resources/inspectors/js/case0_no_issues.js \ No newline at end of file From a48c80896f910be511ed41deff60a82d188f455a Mon Sep 17 00:00:00 2001 From: Nastya Birillo Date: Mon, 19 Jul 2021 15:39:34 +0300 Subject: [PATCH 23/36] Fix indentation --- .github/workflows/build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9d265774..912f8a92 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -34,8 +34,8 @@ jobs: pytest - name: Install review module - run: | - pip install . + run: | + pip install . - name: Check installed module can run python linters run: | @@ -47,4 +47,4 @@ jobs: - name: Check installed module can run js linters run: | - review test/resources/inspectors/js/case0_no_issues.js \ No newline at end of file + review test/resources/inspectors/js/case0_no_issues.js From e42e7a2a1a7c00bdd61693ba0e55237510f84720 Mon Sep 17 00:00:00 2001 From: "Anastasiia.Birillo" Date: Mon, 19 Jul 2021 15:42:11 +0300 Subject: [PATCH 24/36] Fix PR comments --- README.md | 1 - docker/dev/Dockerfile | 36 ------------------------------------ requirements-evaluation.txt | 2 -- 3 files changed, 39 deletions(-) delete mode 100644 docker/dev/Dockerfile delete mode 100644 requirements-evaluation.txt diff --git a/README.md b/README.md index 6b9a3d52..24db74c0 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,6 @@ Simply clone the repository and run the following commands: 1. `pip install -r requirements.txt` 2. `pip install -r requirements-test.txt` for tests -3. `pip install -r requirements-evaluation.txt` for evaluation ## Usage diff --git a/docker/dev/Dockerfile b/docker/dev/Dockerfile deleted file mode 100644 index 9c1847fb..00000000 --- a/docker/dev/Dockerfile +++ /dev/null @@ -1,36 +0,0 @@ -FROM python:3.8.2-alpine3.11 - -RUN apk --no-cache add openjdk11 --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community \ - && apk add --update nodejs npm - -RUN npm i -g eslint@7.5.0 - -RUN java -version -RUN ls /usr/lib/jvm - -# Install numpy and pandas for tests -RUN apk add --no-cache python3-dev libstdc++ && \ - apk add --no-cache g++ && \ - ln -s /usr/include/locale.h /usr/include/xlocale.h && \ - pip3 install numpy && \ - pip3 install pandas - -# Other dependencies -RUN apk add bash - -# Dependencies and package installation -WORKDIR / - -COPY requirements-test.txt review/requirements-test.txt -RUN pip3 install --no-cache-dir -r review/requirements-test.txt - -COPY requirements.txt review/requirements.txt -RUN pip3 install --no-cache-dir -r review/requirements.txt - -COPY . review - -# Container's enviroment variables -ENV JAVA_HOME=/usr/lib/jvm/java-11-openjdk -ENV PATH="$JAVA_HOME/bin:${PATH}" - -CMD ["/bin/bash"] \ No newline at end of file diff --git a/requirements-evaluation.txt b/requirements-evaluation.txt deleted file mode 100644 index 11910373..00000000 --- a/requirements-evaluation.txt +++ /dev/null @@ -1,2 +0,0 @@ -openpyxl==3.0.7 -pandas==1.2.3 \ No newline at end of file From 9061551184ecbd878fae2759669fe8d466551d44 Mon Sep 17 00:00:00 2001 From: "Anastasiia.Birillo" Date: Mon, 19 Jul 2021 15:46:08 +0300 Subject: [PATCH 25/36] Delete init files from resources --- test/resources/evaluation/xlsx_files/__init__.py | 0 test/resources/evaluation/xlsx_target_files/__init__.py | 0 .../functional_tests/different_languages/python/__init__.py | 0 .../functional_tests/file_or_project/project/__init__.py | 0 test/resources/inspectors/python/__init__.py | 0 test/resources/inspectors/python_ast/__init__.py | 0 6 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 test/resources/evaluation/xlsx_files/__init__.py delete mode 100644 test/resources/evaluation/xlsx_target_files/__init__.py delete mode 100644 test/resources/functional_tests/different_languages/python/__init__.py delete mode 100644 test/resources/functional_tests/file_or_project/project/__init__.py delete mode 100644 test/resources/inspectors/python/__init__.py delete mode 100644 test/resources/inspectors/python_ast/__init__.py diff --git a/test/resources/evaluation/xlsx_files/__init__.py b/test/resources/evaluation/xlsx_files/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/test/resources/evaluation/xlsx_target_files/__init__.py b/test/resources/evaluation/xlsx_target_files/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/test/resources/functional_tests/different_languages/python/__init__.py b/test/resources/functional_tests/different_languages/python/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/test/resources/functional_tests/file_or_project/project/__init__.py b/test/resources/functional_tests/file_or_project/project/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/test/resources/inspectors/python/__init__.py b/test/resources/inspectors/python/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/test/resources/inspectors/python_ast/__init__.py b/test/resources/inspectors/python_ast/__init__.py deleted file mode 100644 index e69de29b..00000000 From 765dee078c9501ff1ec975f1f08b562e19a5a9af Mon Sep 17 00:00:00 2001 From: "Anastasiia.Birillo" Date: Mon, 19 Jul 2021 15:56:10 +0300 Subject: [PATCH 26/36] Add init in multi file project --- .../functional_tests/file_or_project/project/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 test/resources/functional_tests/file_or_project/project/__init__.py diff --git a/test/resources/functional_tests/file_or_project/project/__init__.py b/test/resources/functional_tests/file_or_project/project/__init__.py new file mode 100644 index 00000000..e69de29b From eccbe1aee2bc9c8133b0f5f3629cdc70819d38e6 Mon Sep 17 00:00:00 2001 From: "Anastasiia.Birillo" Date: Tue, 20 Jul 2021 10:23:02 +0300 Subject: [PATCH 27/36] Fix flake8 tests --- test/python/inspectors/test_flake8_inspector.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/python/inspectors/test_flake8_inspector.py b/test/python/inspectors/test_flake8_inspector.py index 961e4c22..cae684c1 100644 --- a/test/python/inspectors/test_flake8_inspector.py +++ b/test/python/inspectors/test_flake8_inspector.py @@ -13,7 +13,7 @@ ('case1_simple_valid_program.py', 0), ('case2_boolean_expressions.py', 8), ('case3_redefining_builtin.py', 2), - ('case4_naming.py', 11), + ('case4_naming.py', 8), ('case5_returns.py', 1), ('case6_unused_variables.py', 3), ('case8_good_class.py', 0), @@ -25,14 +25,14 @@ ('case14_returns_errors.py', 4), ('case16_comments.py', 0), ('case17_dangerous_default_value.py', 1), - # ('case18_comprehensions.py', 9), + ('case18_comprehensions.py', 9), ('case19_bad_indentation.py', 3), ('case21_imports.py', 2), ('case25_django.py', 0), ('case31_line_break.py', 11), ('case32_string_format.py', 34), ('case33_commas.py', 14), - # ('case34_cohesion.py', 1), + ('case34_cohesion.py', 1), ] @@ -43,7 +43,7 @@ def test_file_with_issues(file_name: str, n_issues: int): path_to_file = PYTHON_DATA_FOLDER / file_name with use_file_metadata(path_to_file) as file_metadata: issues = inspector.inspect(file_metadata.path, {}) - issues = filter_low_measure_issues(issues, Language.PYTHON) + issues = list(filter(lambda i: i.type != IssueType.INFO, filter_low_measure_issues(issues, Language.PYTHON))) assert len(issues) == n_issues From 5d329e58a261fadd1f07ff4f67e5ac0822c0b8d2 Mon Sep 17 00:00:00 2001 From: Nastya Birillo Date: Mon, 26 Jul 2021 16:37:07 +0300 Subject: [PATCH 28/36] Main upd bugs fix (#74) * Delete evaluation part * Fix bugs with detekt, pmd and flake8 --- src/python/evaluation/README.md | 31 ---- src/python/evaluation/__init__.py | 0 src/python/evaluation/common/__init__.py | 0 src/python/evaluation/common/util.py | 34 ---- src/python/evaluation/common/xlsx_util.py | 42 ----- src/python/evaluation/evaluation_config.py | 43 ----- src/python/evaluation/xlsx_run_tool.py | 169 ------------------ src/python/review/application_config.py | 8 + src/python/review/inspectors/common.py | 11 ++ .../review/inspectors/detekt/issue_types.py | 2 +- src/python/review/inspectors/flake8/flake8.py | 5 + .../review/inspectors/flake8/issue_types.py | 2 + .../review/inspectors/flake8/whitelist.txt | 127 +++++++++++++ .../review/inspectors/pmd/issue_types.py | 2 +- src/python/review/inspectors/pmd/pmd.py | 42 ++++- test/python/evaluation/__init__.py | 11 -- test/python/evaluation/test_data_path.py | 14 -- test/python/evaluation/test_output_results.py | 32 ---- test/python/evaluation/test_tool_path.py | 26 --- .../evaluation/test_xlsx_file_structure.py | 23 --- test/python/evaluation/testing_config.py | 18 -- .../inspectors/test_detekt_inspector.py | 1 + .../inspectors/test_flake8_inspector.py | 5 +- .../xlsx_files/test_empty_lang_cell.xlsx | Bin 5162 -> 0 bytes .../xlsx_files/test_empty_table.xlsx | Bin 4589 -> 0 bytes .../xlsx_files/test_java_no_version.xlsx | Bin 5156 -> 0 bytes .../xlsx_files/test_sorted_order.xlsx | Bin 7317 -> 0 bytes .../xlsx_files/test_unsorted_order.xlsx | Bin 6995 -> 0 bytes .../xlsx_files/test_wrong_column_name.xlsx | Bin 5175 -> 0 bytes .../target_sorted_order.xlsx | Bin 38024 -> 0 bytes .../target_unsorted_order.xlsx | Bin 30970 -> 0 bytes .../inspectors/python/case31_spellcheck.py | 3 + ...e31_line_break.py => case35_line_break.py} | 0 whitelist.txt | 1 + 34 files changed, 199 insertions(+), 453 deletions(-) delete mode 100644 src/python/evaluation/README.md delete mode 100644 src/python/evaluation/__init__.py delete mode 100644 src/python/evaluation/common/__init__.py delete mode 100644 src/python/evaluation/common/util.py delete mode 100644 src/python/evaluation/common/xlsx_util.py delete mode 100644 src/python/evaluation/evaluation_config.py delete mode 100644 src/python/evaluation/xlsx_run_tool.py create mode 100644 src/python/review/inspectors/flake8/whitelist.txt delete mode 100644 test/python/evaluation/__init__.py delete mode 100644 test/python/evaluation/test_data_path.py delete mode 100644 test/python/evaluation/test_output_results.py delete mode 100644 test/python/evaluation/test_tool_path.py delete mode 100644 test/python/evaluation/test_xlsx_file_structure.py delete mode 100644 test/python/evaluation/testing_config.py delete mode 100644 test/resources/evaluation/xlsx_files/test_empty_lang_cell.xlsx delete mode 100644 test/resources/evaluation/xlsx_files/test_empty_table.xlsx delete mode 100644 test/resources/evaluation/xlsx_files/test_java_no_version.xlsx delete mode 100644 test/resources/evaluation/xlsx_files/test_sorted_order.xlsx delete mode 100644 test/resources/evaluation/xlsx_files/test_unsorted_order.xlsx delete mode 100644 test/resources/evaluation/xlsx_files/test_wrong_column_name.xlsx delete mode 100644 test/resources/evaluation/xlsx_target_files/target_sorted_order.xlsx delete mode 100644 test/resources/evaluation/xlsx_target_files/target_unsorted_order.xlsx create mode 100644 test/resources/inspectors/python/case31_spellcheck.py rename test/resources/inspectors/python/{case31_line_break.py => case35_line_break.py} (100%) diff --git a/src/python/evaluation/README.md b/src/python/evaluation/README.md deleted file mode 100644 index 67e1d45e..00000000 --- a/src/python/evaluation/README.md +++ /dev/null @@ -1,31 +0,0 @@ -# Hyperstyle evaluation - -This tool allows running the `Hyperstyle` tool on an xlsx table to get code quality for all code fragments. Please, note that your input file should consist of at least 2 obligatory columns to run xlsx-tool on its code fragments: - -- `code` -- `lang` - -Possible values for column `lang` are: `python3`, `kotlin`, `java8`, `java11`. - -Output file is a new `xlsx` file with 3 columns: -- `code` -- `lang` -- `grade` -Grade assessment is conducted by [`run_tool.py`](https://github.com/hyperskill/hyperstyle/blob/main/README.md) with default arguments. Avaliable values for column `grade` are: BAD, MODERATE, GOOD, EXCELLENT. It is also possible add fourth column: `traceback` to get full inspectors feedback on each code fragment. More details on enabling traceback column in **Optional Arguments** table. - -## Usage - -Run the [xlsx_run_tool.py](xlsx_run_tool.py) with the arguments from command line. - -Required arguments: - -`xlsx_file_path` — path to xlsx-file with code samples to inspect. - -Optional arguments: -Argument | Description ---- | --- -|**‑f**, **‑‑format**| The output format. Available values: `json`, `text`. The default value is `json` . Use this argument when `traceback` is enabled, otherwise it will not be used.| -|**‑tp**, **‑‑tool_path**| Path to run-tool. Default is `src/python/review/run_tool.py` .| -|**‑tr**, **‑‑traceback**| To include a column with errors traceback into an output file. Default is `False`.| -|**‑ofp**, **‑‑output_folder_path**| An explicit folder path to store file with results. Default is a parent directory of a folder with xlsx-file sent for inspection. | -|**‑ofn**, **‑‑output_file_name**| A name of an output file where evaluation results will be stored. Default is `results.xlsx`.| diff --git a/src/python/evaluation/__init__.py b/src/python/evaluation/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/python/evaluation/common/__init__.py b/src/python/evaluation/common/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/python/evaluation/common/util.py b/src/python/evaluation/common/util.py deleted file mode 100644 index c20b16d8..00000000 --- a/src/python/evaluation/common/util.py +++ /dev/null @@ -1,34 +0,0 @@ -from enum import Enum, unique - -from src.python.review.application_config import LanguageVersion -from src.python.review.common.file_system import Extension - - -@unique -class ColumnName(Enum): - CODE = 'code' - LANG = 'lang' - LANGUAGE = 'language' - GRADE = 'grade' - - -@unique -class EvaluationArgument(Enum): - TRACEBACK = 'traceback' - RESULT_FILE_NAME = 'results' - RESULT_FILE_NAME_EXT = f'{RESULT_FILE_NAME}{Extension.XLSX.value}' - - -script_structure_rule = ('Please, make sure your XLSX-file matches following script standards: \n' - '1. Your XLSX-file should have 2 obligatory columns named:' - f'"{ColumnName.CODE.value}" & "{ColumnName.LANG.value}". \n' - f'"{ColumnName.CODE.value}" column -- relates to the code-sample. \n' - f'"{ColumnName.LANG.value}" column -- relates to the language of a ' - 'particular code-sample. \n' - '2. Your code samples should belong to the one of the supported languages. \n' - 'Supported languages are: Java, Kotlin, Python. \n' - f'3. Check that "{ColumnName.LANG.value}" column cells are filled with ' - 'acceptable language-names: \n' - f'Acceptable language-names are: {LanguageVersion.PYTHON_3.value}, ' - f'{LanguageVersion.JAVA_8.value} ,' - f'{LanguageVersion.JAVA_11.value} and {LanguageVersion.KOTLIN.value}.') diff --git a/src/python/evaluation/common/xlsx_util.py b/src/python/evaluation/common/xlsx_util.py deleted file mode 100644 index 032a5ce6..00000000 --- a/src/python/evaluation/common/xlsx_util.py +++ /dev/null @@ -1,42 +0,0 @@ -import logging.config -from pathlib import Path -from typing import Union - -import pandas as pd -from openpyxl import load_workbook, Workbook -from src.python.evaluation.evaluation_config import EvaluationConfig - -logger = logging.getLogger(__name__) - - -def remove_sheet(workbook_path: Union[str, Path], sheet_name: str, to_raise_error: bool = False) -> None: - try: - workbook = load_workbook(workbook_path) - workbook.remove(workbook[sheet_name]) - workbook.save(workbook_path) - - except KeyError as e: - message = f'Sheet with specified name: {sheet_name} does not exist.' - if to_raise_error: - logger.exception(message) - raise e - else: - logger.info(message) - - -def create_and_get_workbook_path(config: EvaluationConfig) -> Path: - workbook = Workbook() - workbook_path = config.get_output_file_path() - workbook.save(workbook_path) - return workbook_path - - -def write_dataframe_to_xlsx_sheet(xlsx_file_path: Union[str, Path], df: pd.DataFrame, sheet_name: str, - mode: str = 'a', to_write_row_names: bool = False) -> None: - """ - mode: str Available values are {'w', 'a'}. File mode to use (write or append). - to_write_row_names: bool Write row names. - """ - - with pd.ExcelWriter(xlsx_file_path, mode=mode) as writer: - df.to_excel(writer, sheet_name=sheet_name, index=to_write_row_names) diff --git a/src/python/evaluation/evaluation_config.py b/src/python/evaluation/evaluation_config.py deleted file mode 100644 index 3a856b78..00000000 --- a/src/python/evaluation/evaluation_config.py +++ /dev/null @@ -1,43 +0,0 @@ -import logging.config -import os -from argparse import Namespace -from pathlib import Path -from typing import List, Union - -from src.python.common.tool_arguments import RunToolArgument -from src.python.evaluation.common.util import EvaluationArgument -from src.python.review.application_config import LanguageVersion - -logger = logging.getLogger(__name__) - - -class EvaluationConfig: - def __init__(self, args: Namespace): - self.tool_path: Union[str, Path] = args.tool_path - self.format: str = args.format - self.xlsx_file_path: Union[str, Path] = args.xlsx_file_path - self.traceback: bool = args.traceback - self.output_folder_path: Union[str, Path] = args.output_folder_path - self.output_file_name: str = args.output_file_name - - def build_command(self, inspected_file_path: Union[str, Path], lang: str) -> List[str]: - command = [LanguageVersion.PYTHON_3.value, - self.tool_path, - inspected_file_path, - RunToolArgument.FORMAT.value.short_name, self.format] - - if lang == LanguageVersion.JAVA_8.value or lang == LanguageVersion.JAVA_11.value: - command.extend([RunToolArgument.LANG_VERSION.value.long_name, lang]) - return command - - def get_output_file_path(self) -> Path: - if self.output_folder_path is None: - try: - self.output_folder_path = ( - Path(self.xlsx_file_path).parent.parent / EvaluationArgument.RESULT_FILE_NAME.value - ) - os.makedirs(self.output_folder_path, exist_ok=True) - except FileNotFoundError as e: - logger.error('XLSX-file with the specified name does not exists.') - raise e - return Path(self.output_folder_path) / self.output_file_name diff --git a/src/python/evaluation/xlsx_run_tool.py b/src/python/evaluation/xlsx_run_tool.py deleted file mode 100644 index 7235b041..00000000 --- a/src/python/evaluation/xlsx_run_tool.py +++ /dev/null @@ -1,169 +0,0 @@ -import argparse -import logging.config -import os -import re -import sys -import traceback -from pathlib import Path -from typing import Type - -sys.path.append('') -sys.path.append('../../..') - -import pandas as pd -from src.python.common.tool_arguments import RunToolArgument -from src.python.evaluation.common.util import ColumnName, EvaluationArgument, script_structure_rule -from src.python.evaluation.common.xlsx_util import ( - create_and_get_workbook_path, - remove_sheet, - write_dataframe_to_xlsx_sheet, -) -from src.python.evaluation.evaluation_config import EvaluationConfig -from src.python.review.application_config import LanguageVersion -from src.python.review.common.file_system import create_file, new_temp_dir -from src.python.review.common.subprocess_runner import run_in_subprocess -from src.python.review.reviewers.perform_review import OutputFormat - -logger = logging.getLogger(__name__) - - -def configure_arguments(parser: argparse.ArgumentParser, run_tool_arguments: Type[RunToolArgument]) -> None: - parser.add_argument('xlsx_file_path', - type=lambda value: Path(value).absolute(), - help='Local XLSX-file path. ' - 'Your XLSX-file must include column-names: ' - f'"{ColumnName.CODE.value}" and ' - f'"{ColumnName.LANG.value}". Acceptable values for ' - f'"{ColumnName.LANG.value}" column are: ' - f'{LanguageVersion.PYTHON_3.value}, {LanguageVersion.JAVA_8.value}, ' - f'{LanguageVersion.JAVA_11.value}, {LanguageVersion.KOTLIN.value}.') - - parser.add_argument('-tp', '--tool-path', - default=Path('src/python/review/run_tool.py').absolute(), - type=lambda value: Path(value).absolute(), - help='Path to script to run on files.') - - parser.add_argument('-tr', '--traceback', - help='If True, column with the full inspector feedback will be added ' - 'to the output file with results.', - action='store_true') - - parser.add_argument('-ofp', '--output-folder-path', - help='An absolute path to the folder where file with evaluation results' - 'will be stored.' - 'Default is the path to a directory, where is the folder with xlsx_file.', - # if None default path will be specified based on xlsx_file_path. - default=None, - type=str) - - parser.add_argument('-ofn', '--output-file-name', - help='Filename for that will be created to store inspection results.' - f'Default is "{EvaluationArgument.RESULT_FILE_NAME_EXT.value}"', - default=f'{EvaluationArgument.RESULT_FILE_NAME_EXT.value}', - type=str) - - parser.add_argument(run_tool_arguments.FORMAT.value.short_name, - run_tool_arguments.FORMAT.value.long_name, - default=OutputFormat.JSON.value, - choices=OutputFormat.values(), - type=str, - help=f'{run_tool_arguments.FORMAT.value.description}' - f'Use this argument when {EvaluationArgument.TRACEBACK.value} argument' - 'is enabled argument will not be used otherwise.') - - -def create_dataframe(config: EvaluationConfig) -> pd.DataFrame: - report = pd.DataFrame( - { - ColumnName.LANGUAGE.value: [], - ColumnName.CODE.value: [], - ColumnName.GRADE.value: [], - }, - ) - - if config.traceback: - report[EvaluationArgument.TRACEBACK.value] = [] - - try: - lang_code_dataframe = pd.read_excel(config.xlsx_file_path) - - except FileNotFoundError as e: - logger.error('XLSX-file with the specified name does not exists.') - raise e - - try: - for lang, code in zip(lang_code_dataframe[ColumnName.LANG.value], - lang_code_dataframe[ColumnName.CODE.value]): - - with new_temp_dir() as create_temp_dir: - temp_dir_path = create_temp_dir - lang_extension = LanguageVersion.language_by_extension(lang) - temp_file_path = os.path.join(temp_dir_path, ('file' + lang_extension)) - temp_file_path = next(create_file(temp_file_path, code)) - - try: - assert os.path.exists(temp_file_path) - except AssertionError as e: - logger.exception('Path does not exist.') - raise e - - command = config.build_command(temp_file_path, lang) - results = run_in_subprocess(command) - os.remove(temp_file_path) - temp_dir_path.rmdir() - # this regular expression matches final tool grade: EXCELLENT, GOOD, MODERATE or BAD - grades = re.match(r'^.*{"code":\s"([A-Z]+)"', results).group(1) - output_row_values = [lang, code, grades] - column_indices = [ColumnName.LANGUAGE.value, - ColumnName.CODE.value, - ColumnName.GRADE.value] - - if config.traceback: - output_row_values.append(results) - column_indices.append(EvaluationArgument.TRACEBACK.value) - - new_file_report_row = pd.Series(data=output_row_values, index=column_indices) - report = report.append(new_file_report_row, ignore_index=True) - - return report - - except KeyError as e: - logger.error(script_structure_rule) - raise e - - except Exception as e: - traceback.print_exc() - logger.exception('An unexpected error.') - raise e - - -def main() -> int: - parser = argparse.ArgumentParser() - configure_arguments(parser, RunToolArgument) - - try: - args = parser.parse_args() - config = EvaluationConfig(args) - workbook_path = create_and_get_workbook_path(config) - results = create_dataframe(config) - write_dataframe_to_xlsx_sheet(workbook_path, results, 'inspection_results') - # remove empty sheet that was initially created with the workbook - remove_sheet(workbook_path, 'Sheet') - return 0 - - except FileNotFoundError: - logger.error('XLSX-file with the specified name does not exists.') - return 2 - - except KeyError: - logger.error(script_structure_rule) - return 2 - - except Exception: - traceback.print_exc() - logger.exception('An unexpected error.') - return 2 - - -if __name__ == '__main__': - sys.exit(main()) diff --git a/src/python/review/application_config.py b/src/python/review/application_config.py index 8d9a56f5..6032aef3 100644 --- a/src/python/review/application_config.py +++ b/src/python/review/application_config.py @@ -42,3 +42,11 @@ def language_to_extension_dict(cls) -> dict: @classmethod def language_by_extension(cls, lang: str) -> str: return cls.language_to_extension_dict()[lang] + + def is_java(self) -> bool: + return ( + self == LanguageVersion.JAVA_7 + or self == LanguageVersion.JAVA_8 + or self == LanguageVersion.JAVA_9 + or self == LanguageVersion.JAVA_11 + ) diff --git a/src/python/review/inspectors/common.py b/src/python/review/inspectors/common.py index dfedbb1e..a307bd96 100644 --- a/src/python/review/inspectors/common.py +++ b/src/python/review/inspectors/common.py @@ -10,3 +10,14 @@ def convert_percentage_of_value_to_lack_of_value(percentage_of_value: float) -> :return: lack of value. """ return floor(100 - percentage_of_value) + + +# TODO: When upgrading to python 3.9+, replace it with removeprefix. +# See: https://docs.python.org/3.9/library/stdtypes.html#str.removeprefix +def remove_prefix(text: str, prefix: str) -> str: + """ + Removes the prefix if it is present, otherwise returns the original string. + """ + if text.startswith(prefix): + return text[len(prefix):] + return text diff --git a/src/python/review/inspectors/detekt/issue_types.py b/src/python/review/inspectors/detekt/issue_types.py index e05d1a30..2379bba8 100644 --- a/src/python/review/inspectors/detekt/issue_types.py +++ b/src/python/review/inspectors/detekt/issue_types.py @@ -15,7 +15,7 @@ # complexity: 'ComplexCondition': IssueType.BOOL_EXPR_LEN, 'ComplexInterface': IssueType.COMPLEXITY, - 'ComplexMethod': IssueType.COMPLEXITY, + 'ComplexMethod': IssueType.CYCLOMATIC_COMPLEXITY, 'LabeledExpression': IssueType.COMPLEXITY, 'LargeClass': IssueType.COMPLEXITY, 'LongMethod': IssueType.FUNC_LEN, diff --git a/src/python/review/inspectors/flake8/flake8.py b/src/python/review/inspectors/flake8/flake8.py index 5745a75f..c5dfd274 100644 --- a/src/python/review/inspectors/flake8/flake8.py +++ b/src/python/review/inspectors/flake8/flake8.py @@ -21,6 +21,10 @@ logger = logging.getLogger(__name__) PATH_FLAKE8_CONFIG = Path(__file__).parent / '.flake8' +# To make the whitelist, a list of words was examined based on students' solutions +# that were flagged by flake8-spellcheck as erroneous. In general, the whitelist included those words +# that belonged to library methods and which were common abbreviations. +PATH_FLAKE8_SPELLCHECK_WHITELIST = Path(__file__).parent / 'whitelist.txt' FORMAT = '%(path)s:%(row)d:%(col)d:%(code)s:%(text)s' INSPECTOR_NAME = 'flake8' @@ -34,6 +38,7 @@ def inspect(cls, path: Path, config: dict) -> List[BaseIssue]: 'flake8', f'--format={FORMAT}', f'--config={PATH_FLAKE8_CONFIG}', + f'--whitelist={PATH_FLAKE8_SPELLCHECK_WHITELIST}', '--max-complexity', '0', '--cohesion-below', '100', path, diff --git a/src/python/review/inspectors/flake8/issue_types.py b/src/python/review/inspectors/flake8/issue_types.py index 440f5f82..b1074b52 100644 --- a/src/python/review/inspectors/flake8/issue_types.py +++ b/src/python/review/inspectors/flake8/issue_types.py @@ -27,6 +27,8 @@ 'C818': IssueType.CODE_STYLE, 'C819': IssueType.CODE_STYLE, + # The categorization for WPS was created using the following document: https://bit.ly/3yms06n + # WPS: Naming 'WPS117': IssueType.CODE_STYLE, # Forbid naming variables self, cls, or mcs. 'WPS125': IssueType.ERROR_PRONE, # Forbid variable or module names which shadow builtin names. diff --git a/src/python/review/inspectors/flake8/whitelist.txt b/src/python/review/inspectors/flake8/whitelist.txt new file mode 100644 index 00000000..47340d90 --- /dev/null +++ b/src/python/review/inspectors/flake8/whitelist.txt @@ -0,0 +1,127 @@ +aggfunc +appendleft +argmax +asctime +astype +betavariate +birthdate +blackbox +bs4 +byteorder +calc +capwords +casefold +caseless +concat +consts +coord +copysign +csgraph +ctime +dataframe +dataframes +dataset +datasets +decrypted +dedent +deque +desc +devs +df +dicts +dirs +divmod +dtype +edu +eig +elems +etree +expm1 +falsy +fillna +floordiv +fromstring +fullmatch +gensim +gmtime +groupby +halfs +hashable +href +hyp +hyperskill +iadd +iloc +inplace +ints +isalnum +isalpha +isin +islice +islower +isnumeric +isprintable +istitle +isub +iterrows +kcal +kcals +lastname +lemmatize +lemmatizer +lifes +lim +linalg +linspace +lowercased +lvl +lxml +matmul +multiline +ndarray +ndigits +ndim +nltk +nrows +numpy +nums +ost +param +params +parsers +pathlib +popleft +pos +punct +readline +rfind +rindex +rmdir +schur +scipy +sigmoid +sqrt +src +stemmer +stepik +subdicts +subdir +subdirs +substr +substring +textwrap +todos +tokenize +tokenized +tokenizer +tolist +tracklist +truediv +truthy +unpickled +upd +util +utils +webpage +whitespaces +writeback diff --git a/src/python/review/inspectors/pmd/issue_types.py b/src/python/review/inspectors/pmd/issue_types.py index e4609146..2188702b 100644 --- a/src/python/review/inspectors/pmd/issue_types.py +++ b/src/python/review/inspectors/pmd/issue_types.py @@ -2,7 +2,7 @@ from src.python.review.inspectors.issue import IssueType -RULE_TO_ISSUE_TYPE: Dict[str, IssueType] = { +PMD_RULE_TO_ISSUE_TYPE: Dict[str, IssueType] = { # Best Practices 'AbstractClassWithoutAbstractMethod': IssueType.BEST_PRACTICES, 'AccessorClassGeneration': IssueType.BEST_PRACTICES, diff --git a/src/python/review/inspectors/pmd/pmd.py b/src/python/review/inspectors/pmd/pmd.py index 4b8c11f0..eb4878dd 100644 --- a/src/python/review/inspectors/pmd/pmd.py +++ b/src/python/review/inspectors/pmd/pmd.py @@ -8,15 +8,17 @@ from src.python.review.common.file_system import new_temp_dir from src.python.review.common.subprocess_runner import run_in_subprocess from src.python.review.inspectors.base_inspector import BaseInspector +from src.python.review.inspectors.common import remove_prefix from src.python.review.inspectors.inspector_type import InspectorType from src.python.review.inspectors.issue import BaseIssue, CodeIssue, IssueType -from src.python.review.inspectors.pmd.issue_types import RULE_TO_ISSUE_TYPE +from src.python.review.inspectors.pmd.issue_types import PMD_RULE_TO_ISSUE_TYPE logger = logging.getLogger(__name__) PATH_TOOLS_PMD_FILES = Path(__file__).parent / 'files' PATH_TOOLS_PMD_SHELL_SCRIPT = PATH_TOOLS_PMD_FILES / 'bin' / 'run.sh' PATH_TOOLS_PMD_RULES_SET = PATH_TOOLS_PMD_FILES / 'bin' / 'basic.xml' +DEFAULT_JAVA_VERSION = LanguageVersion.JAVA_11 class PMDInspector(BaseInspector): @@ -28,14 +30,14 @@ def __init__(self): @classmethod def _create_command(cls, path: Path, output_path: Path, - java_version: LanguageVersion, + language_version: LanguageVersion, n_cpu: int) -> List[str]: return [ PATH_TOOLS_PMD_SHELL_SCRIPT, 'pmd', '-d', str(path), '-no-cache', '-R', PATH_TOOLS_PMD_RULES_SET, '-language', 'java', - '-version', java_version.value, + '-version', cls._get_java_version(language_version), '-f', 'csv', '-r', str(output_path), '-t', str(n_cpu), ] @@ -46,13 +48,21 @@ def inspect(self, path: Path, config: dict) -> List[BaseIssue]: language_version = config.get('language_version') if language_version is None: - language_version = LanguageVersion.JAVA_11 + logger.info( + f"The version of Java is not passed. The version to be used is: {DEFAULT_JAVA_VERSION.value}.", + ) + language_version = DEFAULT_JAVA_VERSION command = self._create_command(path, output_path, language_version, config['n_cpu']) run_in_subprocess(command) return self.parse_output(output_path) def parse_output(self, output_path: Path) -> List[BaseIssue]: + """ + Parses the PMD output, which is a csv file, and returns a list of the issues found there. + + If the passed path is not a file, an empty list is returned. + """ if not output_path.is_file(): logger.error('%s: error - no output file' % self.inspector_type.value) return [] @@ -65,17 +75,37 @@ def parse_output(self, output_path: Path) -> List[BaseIssue]: line_no=int(row['Line']), column_no=1, type=self.choose_issue_type(row['Rule']), - origin_class=row['Rule set'], + origin_class=row['Rule'], description=row['Description'], inspector_type=self.inspector_type, ) for row in reader] @classmethod def choose_issue_type(cls, rule: str) -> IssueType: - issue_type = RULE_TO_ISSUE_TYPE.get(rule) + """ + Defines IssueType by PMD rule name using config. + """ + issue_type = PMD_RULE_TO_ISSUE_TYPE.get(rule) if not issue_type: logger.warning('%s: %s - unknown rule' % (cls.inspector_type.value, rule)) return IssueType.BEST_PRACTICES return issue_type + + @staticmethod + def _get_java_version(language_version: LanguageVersion) -> str: + """ + Converts language_version to the version of Java that PMD can work with. + + For example, java11 will be converted to 11. + """ + java_version = language_version.value + + if not language_version.is_java(): + logger.warning( + f"The version passed is not the Java version. The version to be used is: {DEFAULT_JAVA_VERSION.value}.", + ) + java_version = DEFAULT_JAVA_VERSION.value + + return remove_prefix(java_version, "java") diff --git a/test/python/evaluation/__init__.py b/test/python/evaluation/__init__.py deleted file mode 100644 index 31b1b86f..00000000 --- a/test/python/evaluation/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -from test.python import TEST_DATA_FOLDER - -from src.python import MAIN_FOLDER - -CURRENT_TEST_DATA_FOLDER = TEST_DATA_FOLDER / 'evaluation' - -XLSX_DATA_FOLDER = CURRENT_TEST_DATA_FOLDER / 'xlsx_files' - -TARGET_XLSX_DATA_FOLDER = CURRENT_TEST_DATA_FOLDER / 'xlsx_target_files' - -RESULTS_DIR_PATH = MAIN_FOLDER.parent / 'evaluation/results' diff --git a/test/python/evaluation/test_data_path.py b/test/python/evaluation/test_data_path.py deleted file mode 100644 index 0d8e3502..00000000 --- a/test/python/evaluation/test_data_path.py +++ /dev/null @@ -1,14 +0,0 @@ -from test.python.evaluation import XLSX_DATA_FOLDER -from test.python.evaluation.testing_config import get_testing_arguments - -import pytest -from src.python.evaluation.evaluation_config import EvaluationConfig -from src.python.evaluation.xlsx_run_tool import create_dataframe - - -def test_incorrect_data_path(): - with pytest.raises(FileNotFoundError): - testing_arguments_dict = get_testing_arguments(to_add_traceback=True, to_add_tool_path=True) - testing_arguments_dict.xlsx_file_path = XLSX_DATA_FOLDER / 'do_not_exist.xlsx' - config = EvaluationConfig(testing_arguments_dict) - assert create_dataframe(config) diff --git a/test/python/evaluation/test_output_results.py b/test/python/evaluation/test_output_results.py deleted file mode 100644 index 519652e5..00000000 --- a/test/python/evaluation/test_output_results.py +++ /dev/null @@ -1,32 +0,0 @@ -from test.python.evaluation import TARGET_XLSX_DATA_FOLDER, XLSX_DATA_FOLDER -from test.python.evaluation.testing_config import get_testing_arguments - -import pandas as pd -import pytest -from src.python.evaluation.evaluation_config import EvaluationConfig -from src.python.evaluation.xlsx_run_tool import create_dataframe - -FILE_NAMES = [ - ('test_sorted_order.xlsx', 'target_sorted_order.xlsx', False), - ('test_sorted_order.xlsx', 'target_sorted_order.xlsx', True), - ('test_unsorted_order.xlsx', 'target_unsorted_order.xlsx', False), - ('test_unsorted_order.xlsx', 'target_unsorted_order.xlsx', True), -] - - -@pytest.mark.parametrize(('test_file', 'target_file', 'output_type'), FILE_NAMES) -def test_correct_output(test_file: str, target_file: str, output_type: bool): - - testing_arguments_dict = get_testing_arguments(to_add_tool_path=True) - testing_arguments_dict.xlsx_file_path = XLSX_DATA_FOLDER / test_file - testing_arguments_dict.traceback = output_type - - config = EvaluationConfig(testing_arguments_dict) - test_dataframe = create_dataframe(config) - - sheet_name = 'grades' - if output_type: - sheet_name = 'traceback' - target_dataframe = pd.read_excel(TARGET_XLSX_DATA_FOLDER / target_file, sheet_name=sheet_name) - - assert test_dataframe.reset_index(drop=True).equals(target_dataframe.reset_index(drop=True)) diff --git a/test/python/evaluation/test_tool_path.py b/test/python/evaluation/test_tool_path.py deleted file mode 100644 index 0581caad..00000000 --- a/test/python/evaluation/test_tool_path.py +++ /dev/null @@ -1,26 +0,0 @@ -from test.python.evaluation import XLSX_DATA_FOLDER -from test.python.evaluation.testing_config import get_testing_arguments - -import pytest -from src.python import MAIN_FOLDER -from src.python.evaluation.evaluation_config import EvaluationConfig -from src.python.evaluation.xlsx_run_tool import create_dataframe - - -def test_correct_tool_path(): - try: - testing_arguments_dict = get_testing_arguments(to_add_traceback=True, to_add_tool_path=True) - testing_arguments_dict.xlsx_file_path = XLSX_DATA_FOLDER / 'test_unsorted_order.xlsx' - config = EvaluationConfig(testing_arguments_dict) - create_dataframe(config) - except Exception: - pytest.fail("Unexpected error") - - -def test_incorrect_tool_path(): - with pytest.raises(Exception): - testing_arguments_dict = get_testing_arguments(to_add_traceback=True) - testing_arguments_dict.xlsx_file_path = XLSX_DATA_FOLDER / 'test_unsorted_order.xlsx' - testing_arguments_dict.tool_path = MAIN_FOLDER.parent / 'review/incorrect_path.py' - config = EvaluationConfig(testing_arguments_dict) - assert create_dataframe(config) diff --git a/test/python/evaluation/test_xlsx_file_structure.py b/test/python/evaluation/test_xlsx_file_structure.py deleted file mode 100644 index 9965992e..00000000 --- a/test/python/evaluation/test_xlsx_file_structure.py +++ /dev/null @@ -1,23 +0,0 @@ -from test.python.evaluation import XLSX_DATA_FOLDER -from test.python.evaluation.testing_config import get_testing_arguments - -import pytest -from src.python.evaluation.evaluation_config import EvaluationConfig -from src.python.evaluation.xlsx_run_tool import create_dataframe - - -FILE_NAMES = [ - 'test_wrong_column_name.xlsx', - 'test_java_no_version.xlsx', - 'test_empty_lang_cell.xlsx', - 'test_empty_table.xlsx', -] - - -@pytest.mark.parametrize('file_name', FILE_NAMES) -def test_wrong_column(file_name: str): - with pytest.raises(KeyError): - testing_arguments_dict = get_testing_arguments(to_add_traceback=True, to_add_tool_path=True) - testing_arguments_dict.xlsx_file_path = XLSX_DATA_FOLDER / file_name - config = EvaluationConfig(testing_arguments_dict) - assert create_dataframe(config) diff --git a/test/python/evaluation/testing_config.py b/test/python/evaluation/testing_config.py deleted file mode 100644 index 70341635..00000000 --- a/test/python/evaluation/testing_config.py +++ /dev/null @@ -1,18 +0,0 @@ -from argparse import Namespace - -from src.python import MAIN_FOLDER -from src.python.evaluation.common.util import EvaluationArgument -from src.python.review.reviewers.perform_review import OutputFormat - - -def get_testing_arguments(to_add_traceback=None, to_add_tool_path=None) -> Namespace: - testing_arguments = Namespace(format=OutputFormat.JSON.value, - output_file_name=EvaluationArgument.RESULT_FILE_NAME_EXT.value, - output_folder_path=None) - if to_add_traceback: - testing_arguments.traceback = True - - if to_add_tool_path: - testing_arguments.tool_path = MAIN_FOLDER.parent / 'review/run_tool.py' - - return testing_arguments diff --git a/test/python/inspectors/test_detekt_inspector.py b/test/python/inspectors/test_detekt_inspector.py index 61d05200..99f84f27 100644 --- a/test/python/inspectors/test_detekt_inspector.py +++ b/test/python/inspectors/test_detekt_inspector.py @@ -23,6 +23,7 @@ ('case16_redundant_unit.kt', 1), ('case18_redundant_braces.kt', 3), ('case20_cyclomatic_complexity.kt', 0), + ('case21_cyclomatic_complexity_bad.kt', 2), ('case22_too_many_arguments.kt', 1), ('case23_bad_range_performance.kt', 3), ('case24_duplicate_when_bug.kt', 1), diff --git a/test/python/inspectors/test_flake8_inspector.py b/test/python/inspectors/test_flake8_inspector.py index cae684c1..f1ed0958 100644 --- a/test/python/inspectors/test_flake8_inspector.py +++ b/test/python/inspectors/test_flake8_inspector.py @@ -29,10 +29,11 @@ ('case19_bad_indentation.py', 3), ('case21_imports.py', 2), ('case25_django.py', 0), - ('case31_line_break.py', 11), + ('case31_spellcheck.py', 0), ('case32_string_format.py', 34), ('case33_commas.py', 14), ('case34_cohesion.py', 1), + ('case35_line_break.py', 11), ] @@ -69,7 +70,7 @@ def test_file_with_issues(file_name: str, n_issues: int): ('case14_returns_errors.py', IssuesTestInfo(n_best_practices=1, n_error_prone=3, n_cc=4)), - ('case31_line_break.py', IssuesTestInfo(n_best_practices=1, + ('case35_line_break.py', IssuesTestInfo(n_best_practices=1, n_code_style=10, n_cc=1)), ('case32_string_format.py', IssuesTestInfo(n_error_prone=28, n_other_complexity=6)), diff --git a/test/resources/evaluation/xlsx_files/test_empty_lang_cell.xlsx b/test/resources/evaluation/xlsx_files/test_empty_lang_cell.xlsx deleted file mode 100644 index 91cdada068a9aa8ebe487d14c970a6f7415b1b8c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5162 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 diff --git a/test/resources/evaluation/xlsx_files/test_empty_table.xlsx b/test/resources/evaluation/xlsx_files/test_empty_table.xlsx deleted file mode 100644 index 486b83a16a6b512ee860cc18323274db8588882b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4589 zcmai12RzjO|L5#=cbvUtlblWV2$4-jM)ui4cJ^NBj8GCXGvY+{%FakdC}f2r*^%+T z)9?37{=aX(*R9Vz9`||P`~7^qU(eU`rLB&IO^$(&kB?DInxKnuE~ru0zRvtM9+o~1 zuJ)e)z7V+L>*Aa}V&>WbCC2*>SpynOJD5;I6v@p;G@-y*c2~Lw_xpkQpMu`1i8=&e zGP(@N9&8M{de#0=vVsJ%u2z%b_*2wuny~^m+z@G8;$h>r!+VrXle=7h*3gKQ4>uKE zS`RQUswTac&}pJ}Q~K(xAo23M8j=O(Lip7Zf}!i)H;10DUfYtx&kA_`Du-ikwH+%9 zYkvDgV?3f(gm7OzoBmLoC4$oG5NF?RwyXrNLPuKr2(vplOehWzEt<~r3dud8s&Fht zAbvu-`?*)QrT>FP?`QHJOa`sa%k_%>z0p-1<}E3<%0Ayh>tI+6p##0I#DC0xuHxwv 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 V<6}n&000U0m5FVIK9r|-{|8~OR4o7i diff --git a/test/resources/evaluation/xlsx_files/test_sorted_order.xlsx b/test/resources/evaluation/xlsx_files/test_sorted_order.xlsx deleted file mode 100644 index bfdca3b2a22591703e61d6ba572aac5e027d4ce3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7317 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%0A0WM$% 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 diff --git a/test/resources/evaluation/xlsx_target_files/target_unsorted_order.xlsx b/test/resources/evaluation/xlsx_target_files/target_unsorted_order.xlsx deleted file mode 100644 index 25ed61462f273535632efd1ca804ad1b33e3dd7a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 30970 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 diff --git a/test/resources/inspectors/python/case31_spellcheck.py b/test/resources/inspectors/python/case31_spellcheck.py new file mode 100644 index 00000000..b777eb79 --- /dev/null +++ b/test/resources/inspectors/python/case31_spellcheck.py @@ -0,0 +1,3 @@ +import math + +number = math.sqrt(float(input())) diff --git a/test/resources/inspectors/python/case31_line_break.py b/test/resources/inspectors/python/case35_line_break.py similarity index 100% rename from test/resources/inspectors/python/case31_line_break.py rename to test/resources/inspectors/python/case35_line_break.py diff --git a/whitelist.txt b/whitelist.txt index 77ff8f33..0b861514 100644 --- a/whitelist.txt +++ b/whitelist.txt @@ -94,3 +94,4 @@ util Namespace case18 case34 +removeprefix From 6fc7095ae3c0b338b99cf67c6f835ba3ccd383df Mon Sep 17 00:00:00 2001 From: Ilya Vlasov <55441714+GirZ0n@users.noreply.github.com> Date: Wed, 28 Jul 2021 16:03:27 +0500 Subject: [PATCH 29/36] Issues fix (#80) * Fixed issue #70 * Fixed issue #72: now only spellcheck checks brackets * Fixed issue #73 * Fixed issue #77 * Fixed issue #78 * Fixed issue #71 * Fixed issue #79 * Fixed issue #81 * Added case 36: unpacking * Added case 37: wildcard import * Ignoring WPS347 and F405 * Added cases 36 (unpacking) and 37 (wildcard_import). Also removed W0622 (redefining_builtin) * Removed W0622 (redefining_builtin) --- .../inspectors/checkstyle/files/config.xml | 42 +++++++++++++++---- src/python/review/inspectors/flake8/.flake8 | 5 +++ .../review/inspectors/pmd/files/bin/basic.xml | 21 ++++++---- .../review/inspectors/pylint/issue_types.py | 1 + src/python/review/inspectors/pylint/pylintrc | 2 + .../inspectors/test_flake8_inspector.py | 4 ++ test/python/inspectors/test_pmd_inspector.py | 2 +- .../inspectors/test_pylint_inspector.py | 4 +- .../inspectors/python/case36_unpacking.py | 10 +++++ .../python/case37_wildcard_import.py | 33 +++++++++++++++ 10 files changed, 107 insertions(+), 17 deletions(-) create mode 100644 test/resources/inspectors/python/case36_unpacking.py create mode 100644 test/resources/inspectors/python/case37_wildcard_import.py diff --git a/src/python/review/inspectors/checkstyle/files/config.xml b/src/python/review/inspectors/checkstyle/files/config.xml index 00222e43..92fa6f07 100644 --- a/src/python/review/inspectors/checkstyle/files/config.xml +++ b/src/python/review/inspectors/checkstyle/files/config.xml @@ -87,16 +87,42 @@ + + + + + + + + + + + + + + + + - - - - - - - - + + + + + + + + + diff --git a/src/python/review/inspectors/flake8/.flake8 b/src/python/review/inspectors/flake8/.flake8 index 19edebba..f0452e5a 100644 --- a/src/python/review/inspectors/flake8/.flake8 +++ b/src/python/review/inspectors/flake8/.flake8 @@ -26,11 +26,15 @@ ignore=W291, # trailing whitespaces WPS303, # Forbid underscores in numbers. WPS305, # Forbid f strings. WPS306, # Forbid writing classes without base classes. + WPS317, # Forbid incorrect indentation for parameters. (Because of the unnecessary strictness) WPS318, # Forbid extra indentation. TODO: Collision with standard flake8 indentation check + WPS319, # Forbid brackets in the wrong position. (Because of the unnecessary strictness) WPS323, # Forbid % formatting on strings. WPS324, # Enforce consistent return statements. TODO: Collision with flake8-return WPS335, # Forbid wrong for loop iter targets. + WPS347, # Forbid imports that may cause confusion outside of the module. (controversial) WPS358, # Forbid using float zeros: 0.0. + WPS359, # Forbids to unpack iterable objects to lists. (Because of its similarity to "WPS414") WPS362, # Forbid assignment to a subscript slice. # WPS: Best practices WPS404, # Forbid complex defaults. TODO: Collision with "B006" @@ -49,6 +53,7 @@ ignore=W291, # trailing whitespaces P101, # format string does contain unindexed parameters P102, # docstring does contain unindexed parameters P103, # other string does contain unindexed parameters + F405, # Name may be undefined, or defined from star imports (Collision with the stricter "F403") F522, # unused named arguments. TODO: Collision with "P302" F523, # unused positional arguments. TODO: Collision with "P301" F524, # missing argument. TODO: Collision with "P201" and "P202" diff --git a/src/python/review/inspectors/pmd/files/bin/basic.xml b/src/python/review/inspectors/pmd/files/bin/basic.xml index 6cacd764..612bbf0e 100644 --- a/src/python/review/inspectors/pmd/files/bin/basic.xml +++ b/src/python/review/inspectors/pmd/files/bin/basic.xml @@ -46,29 +46,35 @@ - + - + + - + - + - - + + @@ -90,7 +96,8 @@ - + diff --git a/src/python/review/inspectors/pylint/issue_types.py b/src/python/review/inspectors/pylint/issue_types.py index edfe8818..374b67a3 100644 --- a/src/python/review/inspectors/pylint/issue_types.py +++ b/src/python/review/inspectors/pylint/issue_types.py @@ -8,6 +8,7 @@ # E errors, for probable bugs in the code CODE_TO_ISSUE_TYPE: Dict[str, IssueType] = { + 'C0200': IssueType.BEST_PRACTICES, # consider using enumerate 'W0101': IssueType.ERROR_PRONE, # unreachable code 'W0102': IssueType.ERROR_PRONE, # dangerous default value 'W0104': IssueType.ERROR_PRONE, # statement doesn't have any effect diff --git a/src/python/review/inspectors/pylint/pylintrc b/src/python/review/inspectors/pylint/pylintrc index c435b63b..357ba058 100644 --- a/src/python/review/inspectors/pylint/pylintrc +++ b/src/python/review/inspectors/pylint/pylintrc @@ -164,6 +164,8 @@ disable=invalid-name, no-else-raise, no-else-break, no-else-continue, + W0614, + W0622, # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option diff --git a/test/python/inspectors/test_flake8_inspector.py b/test/python/inspectors/test_flake8_inspector.py index f1ed0958..2a2ebf26 100644 --- a/test/python/inspectors/test_flake8_inspector.py +++ b/test/python/inspectors/test_flake8_inspector.py @@ -34,6 +34,8 @@ ('case33_commas.py', 14), ('case34_cohesion.py', 1), ('case35_line_break.py', 11), + ('case36_unpacking.py', 3), + ('case37_wildcard_import.py', 7), ] @@ -76,6 +78,8 @@ def test_file_with_issues(file_name: str, n_issues: int): ('case32_string_format.py', IssuesTestInfo(n_error_prone=28, n_other_complexity=6)), ('case33_commas.py', IssuesTestInfo(n_code_style=14, n_cc=4)), ('case34_cohesion.py', IssuesTestInfo(n_cc=6, n_cohesion=2)), + ('case36_unpacking.py', IssuesTestInfo(n_error_prone=2, n_cc=1, n_other_complexity=1)), + ('case37_wildcard_import.py', IssuesTestInfo(n_best_practices=1, n_error_prone=3, n_cc=2, n_other_complexity=2)), ] diff --git a/test/python/inspectors/test_pmd_inspector.py b/test/python/inspectors/test_pmd_inspector.py index 393a2d46..15b8006e 100644 --- a/test/python/inspectors/test_pmd_inspector.py +++ b/test/python/inspectors/test_pmd_inspector.py @@ -15,7 +15,7 @@ ('test_comparing_strings.java', 3), ('test_constants.java', 4), ('test_covariant_equals.java', 1), - ('test_curly_braces.java', 2), + ('test_curly_braces.java', 0), ('test_double_checked_locking.java', 2), ('test_for_loop.java', 2), ('test_implementation_types.java', 0), diff --git a/test/python/inspectors/test_pylint_inspector.py b/test/python/inspectors/test_pylint_inspector.py index ba4be12f..fc381234 100644 --- a/test/python/inspectors/test_pylint_inspector.py +++ b/test/python/inspectors/test_pylint_inspector.py @@ -11,7 +11,7 @@ ('case0_spaces.py', 0), ('case1_simple_valid_program.py', 0), ('case2_boolean_expressions.py', 3), - ('case3_redefining_builtin.py', 2), + ('case3_redefining_builtin.py', 0), ('case4_naming.py', 3), ('case5_returns.py', 1), ('case6_unused_variables.py', 4), @@ -32,6 +32,8 @@ ('case25_django.py', 0), ('case27_using_requests.py', 0), ('case30_allow_else_return.py', 0), + ('case36_unpacking.py', 0), + ('case37_wildcard_import.py', 1), ] diff --git a/test/resources/inspectors/python/case36_unpacking.py b/test/resources/inspectors/python/case36_unpacking.py new file mode 100644 index 00000000..74b4ec9c --- /dev/null +++ b/test/resources/inspectors/python/case36_unpacking.py @@ -0,0 +1,10 @@ +[a, b, c], [x, y, z] = (sorted(map(int, input().split())) for _ in 'lm') +if [a, b, c] == [x, y, z]: + a = "Boxes are equal" +elif a <= x and b <= y and c <= z: + a = "The first box is smaller than the second one" +elif a >= x and b >= y and c >= z: + a = "The first box is larger than the second one" +else: + a = "Boxes are incomparable" +print(a) diff --git a/test/resources/inspectors/python/case37_wildcard_import.py b/test/resources/inspectors/python/case37_wildcard_import.py new file mode 100644 index 00000000..ed12a4b1 --- /dev/null +++ b/test/resources/inspectors/python/case37_wildcard_import.py @@ -0,0 +1,33 @@ +from numpy import * + +n, m = [int(_) for _ in input().split()] +mat = zeros((n, m)) +s = 1 +for j in range(n): + for i in range(j, m - j): + if s > n * m: + break + mat[j][i] = s + s += 1 + + for i in range(j - n + 1, -j): + if s > n * m: + break + mat[i][-j - 1] = s + s += 1 + for i in range(-j - 2, -m + j - 1, -1): + if s > n * m: + break + mat[-j - 1][i] = s + s += 1 + + for i in range(-2 - j, -n + j, -1): + if s > n * m: + break + mat[i][j] = s + s += 1 + +for r in range(n): + for c in range(m): + print(str(int(mat[r][c])).ljust(2), end=' ') + print() From dae00c00460b58e0882a68e79970d19ffbcbca15 Mon Sep 17 00:00:00 2001 From: Ilya Vlasov <55441714+GirZ0n@users.noreply.github.com> Date: Tue, 3 Aug 2021 15:02:33 +0500 Subject: [PATCH 30/36] Issue #87 fix (#90) * Fixed issue #87 * Added test --- .../inspectors/checkstyle/files/config.xml | 5 +-- .../inspectors/test_checkstyle_inspector.py | 1 + test/python/inspectors/test_pmd_inspector.py | 1 + .../java/test_multiple_literals.java | 32 +++++++++++++++++++ 4 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 test/resources/inspectors/java/test_multiple_literals.java diff --git a/src/python/review/inspectors/checkstyle/files/config.xml b/src/python/review/inspectors/checkstyle/files/config.xml index 92fa6f07..81fbc370 100644 --- a/src/python/review/inspectors/checkstyle/files/config.xml +++ b/src/python/review/inspectors/checkstyle/files/config.xml @@ -44,8 +44,9 @@ - - + + + diff --git a/test/python/inspectors/test_checkstyle_inspector.py b/test/python/inspectors/test_checkstyle_inspector.py index ce7040aa..ac691ff6 100644 --- a/test/python/inspectors/test_checkstyle_inspector.py +++ b/test/python/inspectors/test_checkstyle_inspector.py @@ -43,6 +43,7 @@ ('test_indentation_with_spaces.java', 0), ('test_indentation_with_tabs.java', 0), ('test_indentation_google_style.java', 4), + ('test_multiple_literals.java', 1), ] diff --git a/test/python/inspectors/test_pmd_inspector.py b/test/python/inspectors/test_pmd_inspector.py index 15b8006e..3702a79b 100644 --- a/test/python/inspectors/test_pmd_inspector.py +++ b/test/python/inspectors/test_pmd_inspector.py @@ -32,6 +32,7 @@ ('test_valid_curly_braces.java', 0), ('test_when_only_equals_overridden.java', 1), ('test_valid_spaces.java', 0), + ('test_multiple_literals.java', 1), ] diff --git a/test/resources/inspectors/java/test_multiple_literals.java b/test/resources/inspectors/java/test_multiple_literals.java new file mode 100644 index 00000000..58a9e2ee --- /dev/null +++ b/test/resources/inspectors/java/test_multiple_literals.java @@ -0,0 +1,32 @@ +class Main { + public static void main(String[] args) { + // ok + String shortRareLiteral1 = "12"; + String shortRareLiteral2 = "12"; + String shortRareLiteral3 = "12"; + + // ok + String longRareLiteral1 = "123"; + String longRareLiteral2 = "123"; + String longRareLiteral3 = "123"; + + // ok + String shortFrequentLiteral1 = "34"; + String shortFrequentLiteral2 = "34"; + String shortFrequentLiteral3 = "34"; + String shortFrequentLiteral4 = "34"; + + // warning + String longFrequentLiteral1 = "456"; + String longFrequentLiteral2 = "456"; + String longFrequentLiteral3 = "456"; + String longFrequentLiteral4 = "456"; + + System.out.println( + shortRareLiteral1 + shortRareLiteral2 + shortRareLiteral3 + + longRareLiteral1 + longRareLiteral2 + longRareLiteral3 + + shortFrequentLiteral1 + shortFrequentLiteral2 + shortFrequentLiteral3 + shortFrequentLiteral4 + + longFrequentLiteral1 + longFrequentLiteral2 + longFrequentLiteral3 + longFrequentLiteral4 + ); + } +} From d155109b0653e5396c6b9f0b4d636594e8456047 Mon Sep 17 00:00:00 2001 From: Ilya Vlasov <55441714+GirZ0n@users.noreply.github.com> Date: Mon, 9 Aug 2021 12:59:47 +0500 Subject: [PATCH 31/36] Fixed issues #91 and #92 (#97) --- src/python/review/inspectors/flake8/whitelist.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/python/review/inspectors/flake8/whitelist.txt b/src/python/review/inspectors/flake8/whitelist.txt index 47340d90..c93513c5 100644 --- a/src/python/review/inspectors/flake8/whitelist.txt +++ b/src/python/review/inspectors/flake8/whitelist.txt @@ -1,5 +1,6 @@ aggfunc appendleft +arange argmax asctime astype @@ -125,3 +126,4 @@ utils webpage whitespaces writeback +yaxis From 5c57bca5f943a44d9d44b2246ee1cc28e1451878 Mon Sep 17 00:00:00 2001 From: "Anastasiia.Birillo" Date: Tue, 10 Aug 2021 12:02:30 +0300 Subject: [PATCH 32/36] Delete xlsx --- src/python/review/common/file_system.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/python/review/common/file_system.py b/src/python/review/common/file_system.py index df30bb9d..5c53d9d4 100644 --- a/src/python/review/common/file_system.py +++ b/src/python/review/common/file_system.py @@ -29,7 +29,6 @@ class Extension(Enum): KT = '.kt' JS = '.js' KTS = '.kts' - XLSX = '.xlsx' ItemCondition = Callable[[str], bool] From 5e8fdfcbfc47b1a5c39a58503e4b9562148eb2a2 Mon Sep 17 00:00:00 2001 From: "Anastasiia.Birillo" Date: Wed, 11 Aug 2021 11:22:58 +0300 Subject: [PATCH 33/36] Remove openpyxl --- requirements-test.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/requirements-test.txt b/requirements-test.txt index 4924e920..cc876366 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -6,5 +6,4 @@ pandas==1.2.3 django==3.2 pylint==2.7.4 requests==2.25.1 -setuptools==56.0.0 -openpyxl==3.0.7 \ No newline at end of file +setuptools==56.0.0 \ No newline at end of file From 469e50c5a62fb40e98f65f545a1ec76613f0f993 Mon Sep 17 00:00:00 2001 From: Ilya Vlasov <55441714+GirZ0n@users.noreply.github.com> Date: Thu, 12 Aug 2021 12:31:43 +0500 Subject: [PATCH 34/36] Issues fix (#104) * Fixed #100 * Fixed #102 * Added some more new words * Added toplevel --- src/python/review/inspectors/flake8/whitelist.txt | 15 +++++++++++++++ .../review/inspectors/pylint/issue_types.py | 1 + whitelist.txt | 1 + 3 files changed, 17 insertions(+) diff --git a/src/python/review/inspectors/flake8/whitelist.txt b/src/python/review/inspectors/flake8/whitelist.txt index c93513c5..b1b7eca2 100644 --- a/src/python/review/inspectors/flake8/whitelist.txt +++ b/src/python/review/inspectors/flake8/whitelist.txt @@ -4,7 +4,9 @@ arange argmax asctime astype +barmode betavariate +bgcolor birthdate blackbox bs4 @@ -14,6 +16,8 @@ capwords casefold caseless concat +config +configs consts coord copysign @@ -41,6 +45,7 @@ expm1 falsy fillna floordiv +fromkeys fromstring fullmatch gensim @@ -48,6 +53,7 @@ gmtime groupby halfs hashable +hline href hyp hyperskill @@ -73,12 +79,15 @@ lemmatizer lifes lim linalg +linecolor +linewidth linspace lowercased lvl lxml matmul multiline +nbins ndarray ndigits ndim @@ -100,6 +109,7 @@ rindex rmdir schur scipy +showline sigmoid sqrt src @@ -116,6 +126,7 @@ tokenize tokenized tokenizer tolist +toplevel tracklist truediv truthy @@ -123,7 +134,11 @@ unpickled upd util utils +vline webpage whitespaces writeback +xanchor +xaxes +yanchor yaxis diff --git a/src/python/review/inspectors/pylint/issue_types.py b/src/python/review/inspectors/pylint/issue_types.py index 374b67a3..5df676b1 100644 --- a/src/python/review/inspectors/pylint/issue_types.py +++ b/src/python/review/inspectors/pylint/issue_types.py @@ -9,6 +9,7 @@ CODE_TO_ISSUE_TYPE: Dict[str, IssueType] = { 'C0200': IssueType.BEST_PRACTICES, # consider using enumerate + 'C0415': IssueType.BEST_PRACTICES, # import-outside-toplevel 'W0101': IssueType.ERROR_PRONE, # unreachable code 'W0102': IssueType.ERROR_PRONE, # dangerous default value 'W0104': IssueType.ERROR_PRONE, # statement doesn't have any effect diff --git a/whitelist.txt b/whitelist.txt index 0b861514..b1834c32 100644 --- a/whitelist.txt +++ b/whitelist.txt @@ -95,3 +95,4 @@ Namespace case18 case34 removeprefix +toplevel From 5d77b39811f2f444815ed605d3bfd895ce8d9d7e Mon Sep 17 00:00:00 2001 From: "Anastasiia.Birillo" Date: Fri, 13 Aug 2021 13:04:15 +0300 Subject: [PATCH 35/36] Update flake8 whitelist --- .../review/inspectors/flake8/whitelist.txt | 20 +++---------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/src/python/review/inspectors/flake8/whitelist.txt b/src/python/review/inspectors/flake8/whitelist.txt index b1b7eca2..ab05c2fa 100644 --- a/src/python/review/inspectors/flake8/whitelist.txt +++ b/src/python/review/inspectors/flake8/whitelist.txt @@ -1,12 +1,9 @@ aggfunc appendleft -arange argmax asctime astype -barmode betavariate -bgcolor birthdate blackbox bs4 @@ -16,8 +13,6 @@ capwords casefold caseless concat -config -configs consts coord copysign @@ -45,7 +40,6 @@ expm1 falsy fillna floordiv -fromkeys fromstring fullmatch gensim @@ -53,7 +47,6 @@ gmtime groupby halfs hashable -hline href hyp hyperskill @@ -79,15 +72,13 @@ lemmatizer lifes lim linalg -linecolor -linewidth linspace lowercased lvl lxml matmul +minsize multiline -nbins ndarray ndigits ndim @@ -109,7 +100,6 @@ rindex rmdir schur scipy -showline sigmoid sqrt src @@ -120,25 +110,21 @@ subdir subdirs substr substring +textposition textwrap todos tokenize tokenized tokenizer tolist -toplevel tracklist truediv truthy +uniformtext unpickled upd util utils -vline webpage whitespaces writeback -xanchor -xaxes -yanchor -yaxis From 93da4343af24315b2f549f019fd0eb2dc0d98086 Mon Sep 17 00:00:00 2001 From: Ilya Vlasov Date: Fri, 13 Aug 2021 15:41:59 +0500 Subject: [PATCH 36/36] Recovered accidentally deleted words --- .../review/inspectors/flake8/whitelist.txt | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/python/review/inspectors/flake8/whitelist.txt b/src/python/review/inspectors/flake8/whitelist.txt index ab05c2fa..38715032 100644 --- a/src/python/review/inspectors/flake8/whitelist.txt +++ b/src/python/review/inspectors/flake8/whitelist.txt @@ -1,9 +1,12 @@ aggfunc appendleft +arange argmax asctime astype +barmode betavariate +bgcolor birthdate blackbox bs4 @@ -13,6 +16,8 @@ capwords casefold caseless concat +config +configs consts coord copysign @@ -40,6 +45,7 @@ expm1 falsy fillna floordiv +fromkeys fromstring fullmatch gensim @@ -47,6 +53,7 @@ gmtime groupby halfs hashable +hline href hyp hyperskill @@ -72,6 +79,8 @@ lemmatizer lifes lim linalg +linecolor +linewidth linspace lowercased lvl @@ -79,6 +88,7 @@ lxml matmul minsize multiline +nbins ndarray ndigits ndim @@ -100,6 +110,7 @@ rindex rmdir schur scipy +showline sigmoid sqrt src @@ -117,6 +128,7 @@ tokenize tokenized tokenizer tolist +toplevel tracklist truediv truthy @@ -125,6 +137,11 @@ unpickled upd util utils +vline webpage whitespaces writeback +xanchor +xaxes +yanchor +yaxis