summaryrefslogtreecommitdiffstats
path: root/python/mozlint/mozlint/result.py
diff options
context:
space:
mode:
Diffstat (limited to 'python/mozlint/mozlint/result.py')
-rw-r--r--python/mozlint/mozlint/result.py163
1 files changed, 163 insertions, 0 deletions
diff --git a/python/mozlint/mozlint/result.py b/python/mozlint/mozlint/result.py
new file mode 100644
index 0000000000..01b04afee6
--- /dev/null
+++ b/python/mozlint/mozlint/result.py
@@ -0,0 +1,163 @@
+# 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)