import uuid
from os.path import relpath
from coala_utils.decorators import (
enforce_signature, generate_ordering, generate_repr, get_public_members)
from coalib.bearlib.aspects import aspectbase
from coalib.results.RESULT_SEVERITY import RESULT_SEVERITY
from coalib.results.SourceRange import SourceRange
# Omit additional info, debug message and diffs for brevity
[docs]@generate_repr(('id', hex),
'origin',
'affected_code',
('severity', RESULT_SEVERITY.reverse.get),
'confidence',
'message',
('aspect', lambda aspect: type(aspect).__qualname__),
'applied_actions')
@generate_ordering('affected_code',
'severity',
'confidence',
'origin',
'message_base',
'message_arguments',
'aspect',
'additional_info',
'diffs',
'debug_msg',
'applied_actions')
class Result:
"""
A result is anything that has an origin and a message.
Optionally it might affect a file.
Result messages can also have arguments. The message is python
style formatted with these arguments.
>>> r = Result('origin','{arg1} and {arg2}', \
message_arguments={'arg1': 'foo', 'arg2': 'bar'})
>>> r.message
'foo and bar'
Message arguments may be changed later. The result message
will also reflect these changes.
>>> r.message_arguments = {'arg1': 'spam', 'arg2': 'eggs'}
>>> r.message
'spam and eggs'
"""
@enforce_signature
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 = {}):
"""
:param origin:
Class name or creator object of this object.
:param message:
Base message to show with this result.
:param affected_code:
A tuple of ``SourceRange`` objects pointing to related positions
in the source code.
:param severity:
Severity of this result.
:param additional_info:
A long description holding additional information about the issue
and/or how to fix it. You can use this like a manual entry for a
category of issues.
:param debug_msg:
A message which may help the user find out why this result was
yielded.
:param diffs:
A dictionary with filename as key and ``Diff`` object
associated with it as value.
:param confidence:
A number between 0 and 100 describing the likelihood of this result
being a real issue.
:param aspect:
An aspectclass instance which this result is associated to.
Note that this should be a leaf of the aspect tree!
(If you have a node, spend some time figuring out which of
the leafs exactly your result belongs to.)
:param message_arguments:
Arguments to be provided to the base message.
:param applied_actions:
A dictionary that contains the result, file_dict, file_diff_dict and
the section for an action.
:raises ValueError:
Raised when confidence is not between 0 and 100.
:raises KeyError:
Raised when message_base can not be formatted with
message_arguments.
"""
origin = origin or ''
if not isinstance(origin, str):
origin = origin.__class__.__name__
if severity not in RESULT_SEVERITY.reverse:
raise ValueError('severity is not a valid RESULT_SEVERITY')
self.origin = origin
self.message_base = message
self.message_arguments = message_arguments
self.applied_actions = applied_actions
if message_arguments:
self.message_base.format(**self.message_arguments)
self.debug_msg = debug_msg
self.additional_info = additional_info
# Sorting is important for tuple comparison
self.affected_code = tuple(sorted(affected_code))
self.severity = severity
if confidence < 0 or confidence > 100:
raise ValueError('Value of confidence should be between 0 and 100.')
self.confidence = confidence
self.diffs = diffs
self.id = uuid.uuid4().int
self.aspect = aspect
if self.aspect and not self.additional_info:
self.additional_info = '{} {}'.format(
aspect.docs.importance_reason, aspect.docs.fix_suggestions)
@property
def message(self):
if not self.message_arguments:
return self.message_base
return self.message_base.format(**self.message_arguments)
@message.setter
def message(self, value: str):
self.message_base = value
[docs] def set_applied_actions(self, applied_actions):
self.applied_actions = applied_actions
[docs] def get_applied_actions(self):
return self.applied_actions
[docs] @classmethod
@enforce_signature
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 = {}):
"""
Creates a result with only one SourceRange with the given start and end
locations.
:param origin:
Class name or creator object of this object.
:param message:
Base message to show with this result.
:param message_arguments:
Arguments to be provided to the base message
:param file:
The related file.
:param line:
The first related line in the file. (First line is 1)
:param column:
The column indicating the first character. (First character is 1)
:param end_line:
The last related line in the file.
:param end_column:
The column indicating the last character.
:param severity:
Severity of this result.
:param additional_info:
A long description holding additional information about the issue
and/or how to fix it. You can use this like a manual entry for a
category of issues.
:param debug_msg:
A message which may help the user find out why this result was
yielded.
:param diffs:
A dictionary with filename as key and ``Diff`` object
associated with it as value.
:param confidence:
A number between 0 and 100 describing the likelihood of this result
being a real issue.
:param aspect:
An Aspect object which this result is associated to. Note that this
should be a leaf of the aspect tree! (If you have a node, spend
some time figuring out which of the leafs exactly your result
belongs to.)
"""
range = SourceRange.from_values(file,
line,
column,
end_line,
end_column)
return cls(origin=origin,
message=message,
affected_code=(range,),
severity=severity,
additional_info=additional_info,
debug_msg=debug_msg,
diffs=diffs,
confidence=confidence,
aspect=aspect,
message_arguments=message_arguments)
[docs] def to_string_dict(self):
"""
Makes a dictionary which has all keys and values as strings and
contains all the data that the base Result has.
FIXME: diffs are not serialized ATM.
FIXME: Only the first SourceRange of affected_code is serialized. If
there are more, this data is currently missing.
:return: Dictionary with keys and values as string.
"""
retval = {}
members = ['id',
'additional_info',
'debug_msg',
'message',
'message_base',
'message_arguments',
'origin',
'confidence']
for member in members:
value = getattr(self, member)
retval[member] = '' if value is None else str(value)
retval['severity'] = str(RESULT_SEVERITY.reverse.get(
self.severity, ''))
if len(self.affected_code) > 0:
retval['file'] = self.affected_code[0].file
line = self.affected_code[0].start.line
retval['line_nr'] = '' if line is None else str(line)
else:
retval['file'], retval['line_nr'] = '', ''
return retval
[docs] @enforce_signature
def apply(self, file_dict: dict):
"""
Applies all contained diffs to the given file_dict. This operation will
be done in-place.
:param file_dict: A dictionary containing all files with filename as
key and all lines a value. Will be modified.
"""
for filename, diff in self.diffs.items():
file_dict[filename] = diff.modified
def __add__(self, other):
"""
Joins those patches to one patch.
:param other: The other patch.
"""
assert isinstance(self.diffs, dict)
assert isinstance(other.diffs, dict)
for filename in other.diffs:
if filename in self.diffs:
self.diffs[filename] += other.diffs[filename]
else:
self.diffs[filename] = other.diffs[filename]
return self
[docs] def overlaps(self, ranges):
"""
Determines if the result overlaps with source ranges provided.
:param ranges: A list SourceRange objects to check for overlap.
:return: True if the ranges overlap with the result.
"""
if isinstance(ranges, SourceRange):
ranges = [ranges]
for range in ranges:
for self_range in self.affected_code:
if range.overlaps(self_range):
return True
return False
[docs] def location_repr(self):
"""
Retrieves a string, that briefly represents
the affected code of the result.
:return: A string containing all of the affected files
separated by a comma.
"""
if not self.affected_code:
return 'the whole project'
# Set important to exclude duplicate file names
range_paths = set(sourcerange.file
for sourcerange in self.affected_code)
return ', '.join(repr(relpath(range_path))
for range_path in sorted(range_paths))
def __json__(self, use_relpath=False):
_dict = get_public_members(self)
if use_relpath and _dict['diffs']:
_dict['diffs'] = {relpath(file): diff
for file, diff in _dict['diffs'].items()}
_dict['aspect'] = type(self.aspect).__qualname__
return _dict