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 18, 2019
1 parent 93c7cce commit f96a556
Showing 1 changed file with 355 additions and 0 deletions.
355 changes: 355 additions & 0 deletions cEP-0030.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,355 @@
# 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. 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.

0 comments on commit f96a556

Please sign in to comment.