Skip to content

Latest commit

 

History

History
526 lines (425 loc) · 18.8 KB

cEP-0030.md

File metadata and controls

526 lines (425 loc) · 18.8 KB

Next Generation Action System

Metadata
cEP 30
Version 1.0
Title Next Generation Action System
Authors Akshat Karani mailto:[email protected]
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.

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.
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.
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.
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
  1. get_default_action function needs to be modified to get default actions from bears_actions also.
# 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
  1. 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
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.
yield Result(self,
             message,
             actions=[EditCommitMessageAction(),
                      AddNewlineAction()])
  1. Implementation of AddNewlineAction
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.
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.
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.
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)
  1. 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.
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.