Repository: hyperskill/hyperstyle (language: python)

Issue:
Use only four main categories for issues that were found
Separate issues into four categories: best practice, error-prone, code style and complexity since now we have more categories. All available categories can be found [here](https://github.com/hyperskill/hyperstyle/blob/main/src/python/review/inspectors/issue.py#L11).


Discussion / hints:

Fix patch:
diff --git a/README.md b/README.md
index 4833b3a6..c2bfc4a2 100644
--- a/README.md
+++ b/README.md
@@ -105,6 +105,7 @@ Argument | Description
 **&#8209;e**, **&#8209;&#8209;end-line**  |  the end line to be analyzed. The default value is `None`, which meant to handle file by the end.
 **&#8209;&#8209;new-format**              |  the argument determines whether the tool should use the _new format_. _New format_ means separating the result by the files to allow getting quality and observed issues for each file separately. The default value is `False`.
 **&#8209;&#8209;history**                 |  JSON string with a list of issues for each language. For each issue its class and quantity are specified. Example: `--history "{\"python\": [{\"origin_class\": \"SC200\", \"number\": 20}, {\"origin_class\": \"WPS314\", \"number\": 3}]}"`
+**&#8209;&#8209;with&#8209;all&#8209;categories** | Without this flag, all issues will be categorized into 5 main categories: `CODE_STYLE`, `BEST_PRACTICES`, `ERROR_PRONE`, `COMPLEXITY`, `INFO`.
 
 The output examples:
 
diff --git a/src/python/common/tool_arguments.py b/src/python/common/tool_arguments.py
index 84439f6a..5911933a 100644
--- a/src/python/common/tool_arguments.py
+++ b/src/python/common/tool_arguments.py
@@ -78,6 +78,10 @@ class RunToolArgument(Enum):
                             'Json string, which contains lists of issues in the previous submissions '
                             'for other tasks for one user.')
 
+    WITH_ALL_CATEGORIES = ArgumentsInfo(None, '--with-all-categories',
+                                        'Without this flag, all issues will be categorized into 5 main categories: '
+                                        'CODE_STYLE, BEST_PRACTICES, ERROR_PRONE, COMPLEXITY, INFO.')
+
     SOLUTIONS_FILE_PATH = ArgumentsInfo(None, 'solutions_file_path',
                                         'Local XLSX-file or CSV-file path. '
                                         'Your file must include column-names: '
diff --git a/src/python/review/application_config.py b/src/python/review/application_config.py
index 995b0bc0..eb6109a1 100644
--- a/src/python/review/application_config.py
+++ b/src/python/review/application_config.py
@@ -12,6 +12,7 @@ class ApplicationConfig:
     allow_duplicates: bool
     n_cpu: int
     inspectors_config: dict
+    with_all_categories: bool
     start_line: int = 1
     end_line: Optional[int] = None
     new_format: bool = False
diff --git a/src/python/review/inspectors/issue.py b/src/python/review/inspectors/issue.py
index 3122378d..3ff25255 100644
--- a/src/python/review/inspectors/issue.py
+++ b/src/python/review/inspectors/issue.py
@@ -10,30 +10,47 @@
 
 @unique
 class IssueType(Enum):
+    # Code style issues
     CODE_STYLE = 'CODE_STYLE'
+    LINE_LEN = 'LINE_LEN'
+
+    # Best practice issues
     BEST_PRACTICES = 'BEST_PRACTICES'
-    ERROR_PRONE = 'ERROR_PRONE'
     FUNC_LEN = 'FUNC_LEN'
-    LINE_LEN = 'LINE_LEN'
-    CYCLOMATIC_COMPLEXITY = 'CYCLOMATIC_COMPLEXITY'
     BOOL_EXPR_LEN = 'BOOL_EXPR_LEN'
+    CLASS_RESPONSE = 'CLASS_RESPONSE'
+    METHOD_NUMBER = 'METHOD_NUMBER'
+
+    # Error-prone issues
+    ERROR_PRONE = 'ERROR_PRONE'
+
+    # Code complexity issues
     COMPLEXITY = 'COMPLEXITY'
-    ARCHITECTURE = 'ARCHITECTURE'
+    CYCLOMATIC_COMPLEXITY = 'CYCLOMATIC_COMPLEXITY'
     INHERITANCE_DEPTH = 'INHERITANCE_DEPTH'
     CHILDREN_NUMBER = 'CHILDREN_NUMBER'
     WEIGHTED_METHOD = 'WEIGHTED_METHOD'
     COUPLING = 'COUPLING'
     COHESION = 'COHESION'
-    CLASS_RESPONSE = 'CLASS_RESPONSE'
-    METHOD_NUMBER = 'METHOD_NUMBER'
     MAINTAINABILITY = 'MAINTAINABILITY'
+
+    # Info issues
     INFO = 'INFO'
 
+    # Others
     UNDEFINED = 'UNDEFINED'
+    ARCHITECTURE = 'ARCHITECTURE'  # TODO: Distribute into one of the main types
 
     def __str__(self) -> str:
         return ' '.join(self.value.lower().split('_'))
 
+    def to_main_type(self) -> 'IssueType':
+        """
+        Converts the issue type to main issue type.
+        Main issue types: CODE_STYLE, BEST_PRACTICES, ERROR_PRONE, COMPLEXITY, INFO.
+        """
+        return get_main_category_by_issue_type(self)
+
 
 ISSUE_TYPE_TO_MAIN_CATEGORY = {
     # CODE_STYLE
@@ -60,6 +77,9 @@ def __str__(self) -> str:
     IssueType.CHILDREN_NUMBER: IssueType.COMPLEXITY,
     IssueType.INHERITANCE_DEPTH: IssueType.COMPLEXITY,
     IssueType.ARCHITECTURE: IssueType.COMPLEXITY,
+
+    # INFO
+    IssueType.INFO: IssueType.INFO,
 }
 
 
diff --git a/src/python/review/quality/penalty.py b/src/python/review/quality/penalty.py
index b1334682..ff4a6e13 100644
--- a/src/python/review/quality/penalty.py
+++ b/src/python/review/quality/penalty.py
@@ -25,6 +25,8 @@
     IssueType.MAINTAINABILITY: 0.3,
     IssueType.METHOD_NUMBER: 0.2,
     IssueType.WEIGHTED_METHOD: 0.2,
+    IssueType.UNDEFINED: 0,
+    IssueType.INFO: 0,
 }
 
 
diff --git a/src/python/review/reviewers/perform_review.py b/src/python/review/reviewers/perform_review.py
index 9f495751..fb44c9a0 100644
--- a/src/python/review/reviewers/perform_review.py
+++ b/src/python/review/reviewers/perform_review.py
@@ -61,11 +61,11 @@ def perform_and_print_review(path: Path,
 
     if OutputFormat.JSON == output_format:
         if config.new_format:
-            print_review_result_as_multi_file_json(review_result)
+            print_review_result_as_multi_file_json(review_result, config)
         else:
-            print_review_result_as_json(review_result)
+            print_review_result_as_json(review_result, config)
     else:
-        print_review_result_as_text(review_result, path)
+        print_review_result_as_text(review_result, path, config)
 
     # Don't count INFO issues too
     return len(list(filter(lambda issue: issue.type != IssueType.INFO, review_result.all_issues)))
diff --git a/src/python/review/reviewers/utils/print_review.py b/src/python/review/reviewers/utils/print_review.py
index 617eb548..66a5f2a3 100644
--- a/src/python/review/reviewers/utils/print_review.py
+++ b/src/python/review/reviewers/utils/print_review.py
@@ -5,6 +5,7 @@
 from typing import Any, Dict, List
 
 from src.python.evaluation.inspectors.common.statistics import PenaltyIssue
+from src.python.review.application_config import ApplicationConfig
 from src.python.review.common.file_system import get_file_line
 from src.python.review.inspectors.inspector_type import InspectorType
 from src.python.review.inspectors.issue import BaseIssue, IssueType
@@ -12,7 +13,8 @@
 
 
 def print_review_result_as_text(review_result: ReviewResult,
-                                path: Path) -> None:
+                                path: Path,
+                                config: ApplicationConfig) -> None:
     heading = f'\nReview of {str(path)} ({len(review_result.all_issues)} violations)'
     print(heading)
 
@@ -32,9 +34,13 @@ def print_review_result_as_text(review_result: ReviewResult,
                     issue.line_no,
                 ).strip()
 
+                issue_type = issue.type
+                if not config.with_all_categories:
+                    issue_type = issue_type.to_main_type()
+
                 print(f'{issue.line_no} : '
                       f'{issue.column_no} : '
-                      f'{issue.type.value} : '
+                      f'{issue_type.value} : '
                       f'{issue.inspector_type.value} : '
                       f'{issue.origin_class} : '
                       f'{issue.description} : '
@@ -48,7 +54,7 @@ def print_review_result_as_text(review_result: ReviewResult,
     print(review_result.general_quality, end='')
 
 
-def print_review_result_as_json(review_result: ReviewResult) -> None:
+def print_review_result_as_json(review_result: ReviewResult, config: ApplicationConfig) -> None:
     issues = review_result.all_issues
 
     issues.sort(key=lambda issue: issue.line_no)
@@ -65,12 +71,12 @@ def print_review_result_as_json(review_result: ReviewResult) -> None:
         if quality_with_penalty != quality_without_penalty:
             influence_on_penalty = review_result.general_punisher.get_issue_influence_on_penalty(issue.origin_class)
 
-        output_json['issues'].append(convert_issue_to_json(issue, influence_on_penalty))
+        output_json['issues'].append(convert_issue_to_json(issue, config, influence_on_penalty))
 
     print(json.dumps(output_json))
 
 
-def print_review_result_as_multi_file_json(review_result: ReviewResult) -> None:
+def print_review_result_as_multi_file_json(review_result: ReviewResult, config: ApplicationConfig) -> None:
     file_review_result_jsons = []
 
     review_result.file_review_results.sort(key=lambda result: result.file_path)
@@ -94,7 +100,7 @@ def print_review_result_as_multi_file_json(review_result: ReviewResult) -> None:
             if quality_with_penalty != quality_without_penalty:
                 influence_on_penalty = file_review_result.punisher.get_issue_influence_on_penalty(issue.origin_class)
 
-            file_review_result_json['issues'].append(convert_issue_to_json(issue, influence_on_penalty))
+            file_review_result_json['issues'].append(convert_issue_to_json(issue, config, influence_on_penalty))
 
     quality_without_penalty = review_result.general_quality.quality_type
     quality_with_penalty = review_result.general_punisher.get_quality_with_penalty(quality_without_penalty)
@@ -121,16 +127,20 @@ class IssueJsonFields(Enum):
     INFLUENCE_ON_PENALTY = 'influence_on_penalty'
 
 
-def convert_issue_to_json(issue: BaseIssue, influence_on_penalty: int = 0) -> Dict[str, Any]:
+def convert_issue_to_json(issue: BaseIssue, config: ApplicationConfig, influence_on_penalty: int = 0) -> Dict[str, Any]:
     line_text = get_file_line(issue.file_path, issue.line_no)
 
+    issue_type = issue.type
+    if not config.with_all_categories:
+        issue_type = issue_type.to_main_type()
+
     return {
         IssueJsonFields.CODE.value: issue.origin_class,
         IssueJsonFields.TEXT.value: issue.description,
         IssueJsonFields.LINE.value: line_text,
         IssueJsonFields.LINE_NUMBER.value: issue.line_no,
         IssueJsonFields.COLUMN_NUMBER.value: issue.column_no,
-        IssueJsonFields.CATEGORY.value: issue.type.value,
+        IssueJsonFields.CATEGORY.value: issue_type.value,
         IssueJsonFields.INFLUENCE_ON_PENALTY.value: influence_on_penalty,
     }
 
diff --git a/src/python/review/run_tool.py b/src/python/review/run_tool.py
index caddaed7..0230b05d 100644
--- a/src/python/review/run_tool.py
+++ b/src/python/review/run_tool.py
@@ -106,6 +106,10 @@ def configure_arguments(parser: argparse.ArgumentParser) -> None:
                         help=RunToolArgument.HISTORY.value.description,
                         type=str)
 
+    parser.add_argument(RunToolArgument.WITH_ALL_CATEGORIES.value.long_name,
+                        help=RunToolArgument.WITH_ALL_CATEGORIES.value.description,
+                        action='store_true')
+
 
 def configure_logging(verbosity: VerbosityLevel) -> None:
     if verbosity is VerbosityLevel.ERROR:
@@ -150,6 +154,7 @@ def main() -> int:
             end_line=args.end_line,
             new_format=args.new_format,
             history=args.history,
+            with_all_categories=args.with_all_categories,
         )
 
         n_issues = perform_and_print_review(args.path, OutputFormat(args.format), config)


Test patch:
diff --git a/test/python/inspectors/test_local_review.py b/test/python/inspectors/test_local_review.py
index 5b182540..e7bf3b8b 100644
--- a/test/python/inspectors/test_local_review.py
+++ b/test/python/inspectors/test_local_review.py
@@ -24,6 +24,7 @@ def config() -> ApplicationConfig:
         allow_duplicates=False,
         n_cpu=1,
         inspectors_config={"n_cpu": 1},
+        with_all_categories=False,
     )