From 2377940ed37c952e0a528b9e69e1b152ff2770df Mon Sep 17 00:00:00 2001 From: akshatkarani Date: Sat, 11 May 2019 20:42:47 +0530 Subject: [PATCH 1/2] cEP-0030: Next Generation Action System Closes https://github.com/coala/cEPs/issues/181 --- cEP-0030.md | 526 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 526 insertions(+) create mode 100644 cEP-0030.md diff --git a/cEP-0030.md b/cEP-0030.md new file mode 100644 index 000000000..ec12a949a --- /dev/null +++ b/cEP-0030.md @@ -0,0 +1,526 @@ +# Next Generation Action System + +| Metadata | | +| -------- | -----------------------------------------------| +| cEP | 30 | +| Version | 1.0 | +| Title | Next Generation Action System | +| Authors | Akshat Karani | +| Status | Proposed | +| Type | Process | + +# Abstract + +This cEP describes the details about Next Generation Action System which +will allow bears to define their own actions as a part of +[GSoC'19 project](https://summerofcode.withgoogle.com/projects/#5450946933424128). + +# Introduction + +Bears run some analysis on a piece of code and output is in the form of +a `Result` object. Then some action from a predefined set of actions +is applied to that `Result` object. This system is a bit restrictive +as only an action from predefined set of actions can be taken. +If there is a system which will support bears defining their +own actions, then it will make bears more useful. +This project is about modifying the current action system so that bears +can define their own actions. +This project also aims to implement a new action which will help in +supporting for bears to provide multiple patches for the affected code. + + +# Support for bears to define their own actions + +The idea is to add a new attribute to `Result` class which will be a list of +action instances defined by origin of the Result. When bears yield a Result, +they can pass optional argument defining actions. +Then actions in `result.actions` are added to the list of predefined actions +when the user is asked for an action to apply. + +## Changing the `Result` class + +1. The first step is to facilitate bears defining their own actions. For this +the `__init__` and `from_values` method of the Result class are to be changed. +While yielding Result object a new optional parameter `actions` can be passed. +This is a list of actions that are specific to the bear. + +```python + +class Result: + + # A new parameter `actions` is added + def __init__(self, + origin, + message: str, + affected_code: (tuple, list) = (), + severity: int = RESULT_SEVERITY.NORMAL, + additional_info: str = '', + debug_msg='', + diffs: (dict, None) = None, + confidence: int = 100, + aspect: (aspectbase, None) = None, + message_arguments: dict = {}, + applied_actions: dict = {}, + actions: list = []): + + # A new attribute `actions` is added + self.actions = actions + + # A new parameter `actions` is added + def from_values(cls, + origin, + message: str, + file: str, + line: (int, None) = None, + column: (int, None) = None, + end_line: (int, None) = None, + end_column: (int, None) = None, + severity: int = RESULT_SEVERITY.NORMAL, + additional_info: str = '', + debug_msg='', + diffs: (dict, None) = None, + confidence: int = 100, + aspect: (aspectbase, None) = None, + message_arguments: dict = {}, + actions: list = []): + + # Passing an optional argument `actions` + return cls(origin=origin, + message=message, + affected_code=(source_range,), + severity=severity, + additional_info=additional_info, + debug_msg=debug_msg, + diffs=diffs, + confidence=confidence, + aspect=aspect, + message_arguments=message_arguments, + actions=actions) + +``` + +## Modifying `ConsoleInteraction` module + +1. `ConsoleInteraction` module needs to be modified to add the actions in +`result.actions` to the list of predefined actions when asking user +for an action to apply +2. For this `acquire_actions_and_apply` function needs to be modified. + +```python + +def acquire_actions_and_apply(console_printer, + section, + file_diff_dict, + result, + file_dict, + cli_actions=None, + apply_single=False): + cli_actions = CLI_ACTIONS if cli_actions is None else cli_actions + failed_actions = set() + applied_actions = {} + + while True: + action_dict = {} + metadata_list = [] + # Only change is adding result.actions here + for action in list(cli_actions) + result.actions: + # All the applicable actions from cli_actions and result.actions + # are appended to `metadata_list`. + # Then user if asked for an action from `metadata_list`. + if action.is_applicable(result, + file_dict, + file_diff_dict, + tuple(applied_actions.keys())) is True: + metadata = action.get_metadata() + action_dict[metadata.name] = action + metadata_list.append(metadata) + + if not metadata_list: + return + +``` + +## Modifying `Processing` module + +1. To allow user to autoapply the actions defined by the bears, some changes +are to be made in the `Processing` module. +2. `autoapply_actions` needs to be modified. `result.actions` from all the +results are collected in `bear_actions` and it is passed to +`get_default_actions` function. After `get_default_actions` returns actions +to autoapply we loop over the results and to apply these actions wherever +applicable. + +```python +def autoapply_actions(results, + file_dict, + file_diff_dict, + section, + log_printer=None): + + bear_actions = [] + # bear defined actions from all the results are added to `bear_actions`. + for result in results: + bear_actions += result.actions + + # `bear_actions` is passed as an argument to `get_default_actions` function. + default_actions, invalid_actions = get_default_actions(section, + bear_actions) + no_autoapply_warn = bool(section.get('no_autoapply_warn', False)) + for bearname, actionname in invalid_actions.items(): + logging.warning('Selected default action {!r} for bear {!r} does not ' + 'exist. Ignoring action.'.format(actionname, bearname)) + + if len(default_actions) == 0: + return results + + not_processed_results = [] + for result in results: + try: + action = default_actions[result.origin] + except KeyError: + for bear_glob in default_actions: + if fnmatch(result.origin, bear_glob): + action = default_actions[bear_glob] + break + else: + not_processed_results.append(result) + continue + + # This condition checks that if action is in bear_actions which means + # that default action is one defined by a bear, then action must be in + # result.actions because then only that action can be applied to that + # result. + if action not in bear_actions or action in result.actions: + applicable = action.is_applicable(result, + file_dict, + file_diff_dict) + if applicable is not True: + if not no_autoapply_warn: + logging.warning('{}: {}'.format(result.origin, applicable)) + not_processed_results.append(result) + continue + + try: + # If action is in `ACTIONS` then action is class. + # Otherwise if action is in `bear_actions` then action is + # object. + if action in ACTIONS: + action = action() + action.apply_from_section(result, + file_dict, + file_diff_dict, + section) + logging.info('Applied {!r} on {} from {!r}.'.format( + action.get_metadata().name, + result.location_repr(), + result.origin)) + except Exception as ex: + not_processed_results.append(result) + log_exception( + 'Failed to execute action {!r} with error: {}.'.format( + action.get_metadata().name, ex), + ex) + logging.debug('-> for result ' + repr(result) + '.') + # Otherwise this result is added to the list of not processed results. + else: + not_processed_results.append(result) + + return not_processed_results + +``` + +3. `get_default_action` function needs to be modified to get default +actions from `bears_actions` also. + +```python + +# A new parameter `bear_actions` is added. +def get_default_actions(section, bear_actions): + try: + default_actions = dict(section['default_actions']) + except IndexError: + return {}, {} + + # `action_dict` now contains all the actions from `ACTIONS` as well as + # bear_actions. + # bears_actions contain action objects, to be consistent with this + # `ACTIONS` was changed to contain action objects. + action_dict = {action.get_metadata().name: action + for action in ACTIONS + bear_actions} + invalid_action_set = default_actions.values() - action_dict.keys() + invalid_actions = {} + if len(invalid_action_set) != 0: + invalid_actions = { + bear: action + for bear, action in default_actions.items() + if action in invalid_action_set} + for invalid in invalid_actions.keys(): + del default_actions[invalid] + + actions = {bearname: action_dict[action_name] + for bearname, action_name in default_actions.items()} + return actions, invalid_actions + +``` + +4. Auto applying actions specific to bears is same as auto-applying +predefined actions. Users just need to add +`default_actions = SomeBear: SomeBearAction` in coafile to autoapply +`SomeBearAction` on a result whose origin is `SomeBear`. + +## Writing bear specific actions + +1. The above changes will now allow bears to define their own actions and +user can apply these actions interactively or by default. +2. While writing any bear specific actions user must implement +`is_applicable` and `apply` method with correct logic. User can also add a +`init` method to pass the necessary data if required. +3. Some ideas for actions which can be implemented for GitCommitBear +are: +`EditCommitMessageAction` - Allows to edit commit message. +`AddNewlineAction` - Adds a newline between shortlog and body of message. +`FixLinelengthAction` - Fixes the line length of shortlog are body if greater +that specified limit. + +### EditCommitMessageAction for GitCommitBear + +1. `EditCommitMessageAction` is an action specific to `GitCommitBear`. +On applying this action, an editor will open up in which user +can edit the commit message of the HEAD commit. +2. Implementation of `EditCommitMessageAction` + +```python + +import subprocess +from coalib.results.result_actions.ResultAction import ResultAction + + +def git(*args): + return subprocess.check_call(['git'] + list(args)) + + +class EditCommitMessageAction(ResultAction): + + SUCCESS_MESSAGE = 'Commit message edited successfully.' + + @staticmethod + def is_applicable(result, + original_file_dict, + file_diff_dict, + applied_actions=()): + return True + + def apply(self, result, original_file_dict, file_diff_dict): + """ + Edit (C)ommit Message [Note: This may rewrite your commit history] + """ + git('commit', '-o', '--amend') + return file_diff_dict + +``` + +### AddNewlineAction for GitCommitBear + +1. `AddNewlineAction` is an action specific to `GitCommitBear`. Whenever +`GitCommitBear` detects that there is no newline between shortlog and +body of the commit message it will yield a `Result` and pass +`AddNewlineAction` as an argument. + +```python + +yield Result(self, + message, + actions=[EditCommitMessageAction(), + AddNewlineAction()]) + +``` + +2. Implementation of `AddNewlineAction` + +```python + +from coalib.misc.Shell import run_shell_command +from coalib.results.result_actions.ResultAction import ResultAction + + +class AddNewlineAction(ResultAction): + + SUCCESS_MESSAGE = 'New Line added successfully.' + + def __init__(self, shortlog, body): + self.shortlog = shortlog + self.body = body + + def is_applicable(self, + result, + original_file_dict, + file_diff_dict, + applied_actions=()): + # When `EditCommitMessageAction` or `AddNewlineAction` is + # applied once, then we need to retrieve commit message once + # again and check if action is still applicable or not. + new_message, _ = run_shell_command('git log -1 --pretty=%B') + new_message = new_message.rstrip('\n') + pos = new_message.find('\n') + self.shortlog = new_message[:pos] if pos != -1 else new_message + self.body = new_message[pos+1:] if pos != -1 else '' + if self.body[0] != '\n': + return True + else: + return False + + def apply(self, result, original_file_dict, file_diff_dict, **kwargs): + """ + Add New(L)ine [Note: This may rewrite your commit history] + """ + new_commit_message = '{}\n\n{}'.format(self.shortlog, self.body) + command = 'git commit -o --amend -m "{}"'.format(new_commit_message) + stdout, err = run_shell_command(command) + return file_diff_dict + +``` + +# Support for bears to provide multiple patches + +Currently bears suggest the patches in form of `diffs`, to facilitate bears +suggesting multiple patches we add a new attribute to `Result` class, +`alternate_diffs`. It is a list of alternate patches suggested by the bear. +For each alternate patch we add an `AlternatePatchAction` to list of +actions. + +## Changing the `Result` class + +1. `init` method and `from_values` method are modified and a new parameter, +`alternate_diffs` is added. It is a list of dictionaries. + +```python + +class Result: + + # A new parameter `alternate_diffs` is added + def __init__(self, + origin, + message: str, + affected_code: (tuple, list) = (), + severity: int = RESULT_SEVERITY.NORMAL, + additional_info: str = '', + debug_msg='', + diffs: (dict, None) = None, + confidence: int = 100, + aspect: (aspectbase, None) = None, + message_arguments: dict = {}, + applied_actions: dict = {}, + actions: list = [], + alternate_diffs: (list,None) = None): + + # A new attribute `alternate_diffs` is added + self.alternate_diffs = alternate_diffs + + # A new parameter `alternate_diffs` is added + def from_values(cls, + origin, + message: str, + file: str, + line: (int, None) = None, + column: (int, None) = None, + end_line: (int, None) = None, + end_column: (int, None) = None, + severity: int = RESULT_SEVERITY.NORMAL, + additional_info: str = '', + debug_msg='', + diffs: (dict, None) = None, + confidence: int = 100, + aspect: (aspectbase, None) = None, + message_arguments: dict = {}, + actions: list = [], + alternate_diffs: (list,None) = None): + + # Passing an optional argument `alternate_diffs` + return cls(origin=origin, + message=message, + affected_code=(source_range,), + severity=severity, + additional_info=additional_info, + debug_msg=debug_msg, + diffs=diffs, + confidence=confidence, + aspect=aspect, + message_arguments=message_arguments, + actions=actions, + alternate_diffs=alternate_diffs) + +``` + +## Implementing `AlternatePatchAction` + +1. `AlternatePatchAction` object holds the alternate patch corresponding to +it in a `diffs` attribute. +2. The basic idea is to swaps the values of `result.diffs` and `self.diffs` +and then apply `ShowPatchAction`. This will show the alternate patch to the +user. After this user chooses `ApplyPatchAction` then changes made are +corresponding to the alternate patch. + +```python + +class AlternatePatchAction(ResultAction): + + SUCCESS_MESSAGE = 'Displayed patch successfully.' + + def __init__(self, diffs): + self.diffs = diffs + + def is_applicable(self, + result: Result, + original_file_dict, + file_diff_dict, + applied_actions=()): + return 'ApplyPatchAction' not in applied_actions + + def apply(self, + result, + original_file_dict, + file_diff_dict, + no_color: bool = False): + self.diffs, result.diffs = result.diffs, self.diffs + return ShowPatchAction().apply(result, + original_file_dict, + file_diff_dict, + no_color=no_color) + +``` + +## Modifying `ConsoleInteraction` module + +1. A new function `get_alternate_patch_action` is defined. It takes a result +object as a parameter and returns a tuple of `AlternatePatchAction` instances, +each corresponding to an alternative patch. + +```python + +def get_alternate_patch_actions(result): + """ + Returns a tuple of AlternatePatchAction instances, each corresponding + to an alternate_diff in result.alternate_diffs + """ + alternate_patch_actions = [] + if result.alternate_diffs is not None: + for alternate_diff in result.alternate_diffs: + alternate_patch_actions.append( + AlternatePatchAction(alternate_diff)) + return tuple(alternate_patch_actions) + +``` + +2. This function is called in `acquire_actions_and_apply` method. The tuple +returned by `get_alternate_patch_actions` is added to the tuple of +`cli_actions`. + +```python + +def acquire_actions_and_apply(..) + cli_actions = CLI_ACTIONS if cli_actions is None else cli_actions + cli_actions += get_alternate_patch_actions(result) + +``` + +These changes will now allow bears to suggest multiple patches. \ No newline at end of file From b8a2d2b74e50bbcbf812ace6f0009a1e2e4da11c Mon Sep 17 00:00:00 2001 From: Abhinav Kaushlya Date: Sun, 6 Jun 2021 10:30:41 +0530 Subject: [PATCH 2/2] Add row for cEP-0030 to README.md --- CODEOWNERS | 2 +- README.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index 997b0016c..6ecc62423 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1,2 +1,2 @@ # noqa -*.md @RJ722 @kriti21 @virresh @yzgyyang @bekicot @nemaniarjun @Vamshi99 @ksdme @palash25 @nvzard @MacBox7 @binore @anctartica @sks444 @RaiVaibhav @sangamcse @li-boxuan @ishanSrt @pareksha @abhishalya @bkhanale @PrajwalM2212 @pilgrim2308 +*.md @RJ722 @kriti21 @virresh @yzgyyang @bekicot @nemaniarjun @Vamshi99 @ksdme @palash25 @nvzard @MacBox7 @binore @anctartica @sks444 @RaiVaibhav @sangamcse @li-boxuan @ishanSrt @pareksha @abhishalya @bkhanale @PrajwalM2212 @pilgrim2308 @akshatkarani diff --git a/README.md b/README.md index 46ced6b7b..bed025fea 100644 --- a/README.md +++ b/README.md @@ -26,4 +26,5 @@ in the `cEP-0000.md` document. | [cEP-0025](cEP-0025.md) | Integrate pyflakes AST into coala | This cEP describes the details of the process of integrating the pyflakes-enhanced-AST into coala as part of [GSoC'18 project](https://summerofcode.withgoogle.com/projects/#5549789140221952). | | [cEP-0027](cEP-0027.md) | coala Bears Testing API | This cEP describes the implementation process of `BaseTestHelper` class and `GlobalBearTestHelper` class to improve testing API of coala as a part of the [GSoC'18 project](https://summerofcode.withgoogle.com/projects/#6625036551585792). | | [cEP-0029](cEP-0029.md) | Support TOML as a configuration format | This cEP describes the implementation process how support for TOML will be implemented and integrated with the existing configuration system as a part of the [GSoC'19 project](https://summerofcode.withgoogle.com/projects/#6388671438127104). | +| [cEP-0030](cEP-0030.md) | Next Generation Action System | This cEP describes the details about Next Generation Action System which will allow bears to define their own actions as a part of [GSoC'19 project](https://summerofcode.withgoogle.com/projects/#5450946933424128). | | [cEP-0036](cEP-0036.md) | Gitmate for coala | This cEP describes the improvements to IGitt and Gitmate, which include upgrading dependencies, making it functional, changes to plugins, etc. as a part of the [GSoC'21 project](https://summerofcode.withgoogle.com/projects/#6263057774280704). |