# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
import os
from collections
import defaultdict
from itertools
import chain
from json
import JSONEncoder
import attr
import mozpack.path
as mozpath
class ResultSummary(object):
"""Represents overall result state from an entire lint run."""
root =
None
def __init__(self, root, fail_on_warnings=
True):
self.fail_on_warnings = fail_on_warnings
self.reset()
# Store the repository root folder to be able to build
# Issues relative paths to that folder
if ResultSummary.root
is None:
ResultSummary.root = mozpath.normpath(root)
def reset(self):
self.issues = defaultdict(list)
self.failed_run = set()
self.failed_setup = set()
self.suppressed_warnings = defaultdict(int)
self.fixed = 0
def has_issues_failure(self):
"""Returns true in case issues were detected during the lint run. Do not
consider warning issues
in case `self.fail_on_warnings`
is set to
False.
"""
if self.fail_on_warnings
is False:
return any(
result.level !=
"warning" for result
in chain(*self.issues.values())
)
return len(self.issues) >= 1
@property
def returncode(self):
if self.has_issues_failure()
or self.failed:
return 1
return 0
@property
def failed(self):
return self.failed_setup | self.failed_run
@property
def total_issues(self):
return sum([len(v)
for v
in self.issues.values()])
@property
def total_suppressed_warnings(self):
return sum(self.suppressed_warnings.values())
@property
def total_fixed(self):
return self.fixed
def update(self, other):
"""Merge results from another ResultSummary into this one."""
for path, obj
in other.issues.items():
self.issues[path].extend(obj)
self.failed_run |= other.failed_run
self.failed_setup |= other.failed_setup
self.fixed += other.fixed
for k, v
in other.suppressed_warnings.items():
self.suppressed_warnings[k] += v
@attr.s(slots=
True, kw_only=
True)
class Issue(object):
"""Represents a single lint issue and its related metadata.
:param linter: name of the linter that flagged this error
:param path: path to the file containing the error
:param message: text describing the error
:param lineno: line number that contains the error
:param column: column containing the error
:param level: severity of the error, either
'warning' or 'error' (default
'error')
:param hint: suggestion
for fixing the error (optional)
:param source: source code context of the error (optional)
:param rule: name of the rule that was violated (optional)
:param lineoffset: denotes an error spans multiple lines, of the form
(<lineno offset>, <num lines>) (optional)
:param diff: a diff describing the changes that need to be made to the code
"""
linter = attr.ib()
path = attr.ib()
lineno = attr.ib(
default=
None, converter=
lambda lineno: int(lineno)
if lineno
else 0
)
column = attr.ib(
default=
None, converter=
lambda column: int(column)
if column
else column
)
message = attr.ib()
hint = attr.ib(default=
None)
source = attr.ib(default=
None)
level = attr.ib(default=
None, converter=
lambda level: level
or "error")
rule = attr.ib(default=
None)
lineoffset = attr.ib(default=
None)
diff = attr.ib(default=
None)
relpath = attr.ib(init=
False, default=
None)
def __attrs_post_init__(self):
root = ResultSummary.root
assert root
is not None,
"Missing ResultSummary.root"
if os.path.isabs(self.path):
self.path = mozpath.normpath(self.path)
self.relpath = mozpath.relpath(self.path, root)
else:
self.relpath = mozpath.normpath(self.path)
self.path = mozpath.join(root, self.path)
class IssueEncoder(JSONEncoder):
"""Class for encoding :class:`~result.Issue` to json.
Usage:
.. code-block:: python
json.dumps(results, cls=IssueEncoder)
"""
def default(self, o):
if isinstance(o, Issue):
return attr.asdict(o)
return JSONEncoder.default(self, o)
def from_config(config, **kwargs):
"""Create a :class:`~result.Issue` from a linter config.
Convenience method that pulls defaults
from a linter
config
and forwards them.
:param config: linter config
as defined
in a .yml file
:param kwargs: same
as :
class:`~result.Issue`
:returns: :
class:`~result.Issue` object
"""
args = {}
for arg
in attr.fields(Issue):
if arg.init:
args[arg.name] = kwargs.get(arg.name, config.get(arg.name))
if not args[
"linter"]:
args[
"linter"] = config.get(
"name")
if not args[
"message"]:
args[
"message"] = config.get(
"description")
return Issue(**args)