Skip to content

Commit

Permalink
cEP-0030: Next Generation Action System
Browse files Browse the repository at this point in the history
Closes #181
  • Loading branch information
akshatkarani committed Jun 26, 2019
1 parent 93c7cce commit 0e76d9e
Showing 1 changed file with 389 additions and 0 deletions.
389 changes: 389 additions & 0 deletions cEP-0030.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,389 @@
# 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](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. `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:
# 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. 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. 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 message, a 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 a argument.

```python

yield Result(self,
message,
actions=[EditCommitMessageAction(),
AddNewlineAction()])

```

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

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

```

## 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.

0 comments on commit 0e76d9e

Please sign in to comment.