diff --git a/cEP-0030.md b/cEP-0030.md new file mode 100644 index 000000000..61b12da1d --- /dev/null +++ b/cEP-0030.md @@ -0,0 +1,355 @@ +# 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 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 action from predefined set of actionscan 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 changing the current action system so that bears +can define their own actions. + + +# Implementation + +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 `Result.actions` is added to list of predefined actions when user is asked +for a 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 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 +`Result.actions` to 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 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, +`Processing` module needs to be modified. +2. Definition of `ACTIONS` needs to be modified to contain action +objects instead of action class. + +```python + +ACTIONS = [DoNothingAction(), + ApplyPatchAction(), + PrintDebugMessageAction(), + ShowPatchAction(), + IgnoreResultAction(), + ShowAppliedPatchesAction(), + GeneratePatchesAction()] + +``` + +3. `autoapply_actions` needs to be modified to enable user to +autoapply bear defined actions. + +```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 a 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: + 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 + +``` + +4. `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 + +``` + +5. Auto applying actions specific to bears is same as auto-applying +predefined actions. User just need to add +`default_actions = BearName: ActionName` in coafile. + +## 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 action for bear user needs to implement `is_applicable` +and `apply` method with correct logic. User can also add a `init` method to +pass the necessary data. +3. It may be the case that `is_appicable` method need to be implemented in +such a way that once action is applied is should return False. For this user +can use `file_diff_dict` to know if action has been applied or not. + +### AddNewlineAction for GitCommitBear + +1. `AddNewlineAction` is an action specific to `GitCommitBear`. Whenever +`GitCommitBear` detects that there is no newline between shorlog and +body of the commit message it will yield a `Result` and pass +`AddNewlineAction` as a argument. + +```python + +yield Result(self, + message, + actions=[AddNewlineAction(message, shortlog, body)]) + +``` + +2. Implemtation 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 + + @staticmethod + def is_applicable(result, + original_file_dict, + file_diff_dict, + applied_actions=()): + return 'AddNewlineAction' not in file_diff_dict + + def apply(self, result, original_file_dict, file_diff_dict, **kwargs): + """ + Add (N)ewline + """ + new_commit_message = '{}\n\n{}'.format(self.shortlog, self.body) + command = 'git commit --amend -m "{}"'.format(new_commit_message) + stdout, err = run_shell_command(command) + # When action is applied a new element is added to + # `file_diff_dict`. This is used in `is_applicable` to know + # if the action was applied or not. + file_diff_dict['AddNewlineAction'] = True + return file_diff_dict + +``` + +3. Some other ideas for actions which can be implemented for GitCommitBear +are: +`OpenEditorAction` - Opens up a editor to edit your commit message. +`FixLinelengthAction` - Fixes that line length of shortlog are body if greater +that specified limit. + +## Bears with Multiple Patches + +1. To support bears with multiple patches a new action `PatchAction` +is implemented. Corresponding to each patch provided for a problem we +have a instance of `PatchAction` in `result.actions`. +2. `PatchAction` is implemented such that it implicitly includes +`ShowPatchAction` and `ApplyPatchAction` for that patch. +3. Initially only `ShowPatchAction` of one patch is applied. +If user wants to apply this patch the `ApplyPatchAction` of that patch +is applied. If the user asks for another patch then `ShowPatchAction` +corresponding to that patch is applied.