diff --git a/src/catkin_lint/checks/manifest.py b/src/catkin_lint/checks/manifest.py index 262e0dc..fa4b0d3 100644 --- a/src/catkin_lint/checks/manifest.py +++ b/src/catkin_lint/checks/manifest.py @@ -99,13 +99,7 @@ def on_final(info): if node.tag is ET.Comment: args = (node.text or "").split() if args and args[0] == "catkin_lint:" and args[1] in ["ignore_once", "ignore", "report"]: - msg_ids = set([a.upper() for a in args[2:]]) - if args[1] == "ignore": - info.ignore_message_ids |= msg_ids - if args[1] == "report": - info.ignore_message_ids -= msg_ids - if args[1] == "ignore_once": - info.ignore_message_ids_once |= msg_ids + info.linter._handle_pragma(info, args[1:]) else: relevant_deps = info.exec_dep dep_type = exec_dep_type @@ -119,7 +113,7 @@ def on_final(info): pkg = mo.group(1) if pkg is not None and pkg != info.manifest.name and info.env.get_package_type(pkg) == PackageType.CATKIN and pkg not in relevant_deps and pkg not in essential_packages: info.report(WARNING, "LAUNCH_DEPEND", type=dep_type, pkg=pkg, file_location=(src_filename, node.sourceline or 0)) - info.ignore_message_ids_once.clear() + info.ignore_messages_once.clear() except (ET.Error, ValueError) as err: info.report(WARNING, "PARSE_ERROR", msg=str(err), file_location=(src_filename, 0)) diff --git a/src/catkin_lint/linter.py b/src/catkin_lint/linter.py index 2dde4f4..be87911 100644 --- a/src/catkin_lint/linter.py +++ b/src/catkin_lint/linter.py @@ -30,6 +30,7 @@ # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +import operator import os import posixpath import re @@ -37,6 +38,7 @@ import string from fnmatch import fnmatch from copy import copy +from collections import defaultdict from .cmake import ParserContext, argparse as cmake_argparse, CMakeSyntaxError from .diagnostics import msg, add_user_defined_msg from .util import abspath @@ -46,6 +48,14 @@ NOTICE = 2 +def re_fullmatch(a, b): + return re.fullmatch(a, b) + + +def not_re_fullmatch(a, b): + return not re.fullmatch(a, b) + + class Message(object): def __init__(self, package, file_name, line, level, msg_id, text, description): @@ -83,7 +93,7 @@ class PathConstants(object): class LintInfo(object): - def __init__(self, env): + def __init__(self, env, linter=None): self.env = env self.path = None self.subdir = "" @@ -92,8 +102,8 @@ def __init__(self, env): self.manifest = None self.file = "" self.line = 0 - self.ignore_message_ids = set() - self.ignore_message_ids_once = set() + self.ignore_messages = defaultdict(list) + self.ignore_messages_once = defaultdict(list) self.command_loc = {} self.commands = set() self.find_packages = set() @@ -109,15 +119,54 @@ def __init__(self, env): self.ignored_messages = [] self.generated_files = set([""]) self.message_level_override = {} + self.linter = linter + + def parse_ignore_filter(self, s): + results = defaultdict(list) + for mo in re.finditer(r"(?P[a-z0-9_-]+)(:?\[(?P[^\]]*)\])?", s, re.IGNORECASE): + msg_id = mo.group("ID").upper() + expr = mo.group("EXPR") + if expr: + clauses = [] + for clause in expr.split(","): + clause = clause.strip() + if "!=" in clause: + clauses.append((operator.ne, *[s.strip() for s in clause.split("!=", 1)])) + elif "!~" in clause: + clauses.append((not_re_fullmatch, *[s.strip() for s in clause.split("!~", 1)])) + elif "=" in clause: + clauses.append((operator.eq, *[s.strip() for s in clause.split("=", 1)])) + elif "~" in clause: + clauses.append((re_fullmatch, *[s.strip() for s in clause.split("~", 1)])) + else: + clauses.append((lambda x, y: False, None, None)) + results[msg_id].append(clauses) + else: + results[msg_id].append(True) + return results def report(self, level, msg_id, **kwargs): + def _matches_ignore_filter(expr): + if isinstance(expr, bool): + return expr + for clauses in expr: + if isinstance(clauses, bool) and clauses == True: + return True + for match in clauses: + if not match[0](match[2], kwargs.get(match[1], None)): + break + else: + return True + return False + file_name, line = self.file, self.line loc = kwargs.get("file_location", None) if loc: file_name, line = loc del kwargs["file_location"] msg_id, text, description = msg(msg_id, **kwargs) - if msg_id in self.ignore_message_ids or msg_id in self.ignore_message_ids_once: + + if _matches_ignore_filter(self.ignore_messages.get(msg_id, False)) or _matches_ignore_filter(self.ignore_messages_once.get(msg_id, False)): self.ignored_messages.append(Message( package=self.manifest.name, file_name=file_name, @@ -465,11 +514,21 @@ def _handle_list(self, info, args): def _handle_pragma(self, info, args): pragma = args.pop(0) if pragma == "ignore": - info.ignore_message_ids |= set([a.upper() for a in args]) + msgs = info.parse_ignore_filter(" ".join(args)) + print(msgs) + for key, value in msgs.items(): + info.ignore_messages[key] += value if pragma == "report": - info.ignore_message_ids -= set([a.upper() for a in args]) + msgs = info.parse_ignore_filter(" ".join(args)) + for key, value in msgs.items(): + if True in value: + info.ignore_messages[key].clear() + else: + info.ignore_messages[key] = [entry for entry in info.ignore_messages[key] if entry not in value] if pragma == "ignore_once": - info.ignore_message_ids_once |= set([a.upper() for a in args]) + msgs = info.parse_ignore_filter(" ".join(args)) + for key, value in msgs.items(): + info.ignore_messages_once[key] += value if pragma == "skip": self._ctx.skip_block() @@ -653,11 +712,11 @@ def _parse_file(self, info, filename): self.execute_hook(info, cmd, args) info.commands.add(cmd) info.command_loc[cmd] = info.current_location() - info.ignore_message_ids_once.clear() + info.ignore_messages_once.clear() finally: info.file = save_file info.line = save_line - info.ignore_message_ids_once.clear() + info.ignore_messages_once.clear() self._ctx = save_ctx KEYWORD_TO_SEVERITY = {"error": ERROR, "warning": WARNING, "notice": NOTICE} @@ -667,10 +726,10 @@ def _get_overrides(self, info, section): val = section[opt].lower().strip() opt = opt.upper() if val == "ignore": - info.ignore_message_ids.add(opt) + info.ignore_messages[opt].append(True) elif val == "default": info.message_level_override.pop(opt, None) - info.ignore_message_ids.discard(opt) + info.ignore_messages[opt].clear() else: severity = self.KEYWORD_TO_SEVERITY.get(val, None) if severity is not None: @@ -678,7 +737,7 @@ def _get_overrides(self, info, section): def lint(self, path, manifest, info=None, config=None): if info is None: - info = LintInfo(self.env) + info = LintInfo(self.env, linter=self) if config is not None: if "*" in config: self._get_overrides(info, config["*"]) diff --git a/test/helper.py b/test/helper.py index 423bfb5..835fe68 100644 --- a/test/helper.py +++ b/test/helper.py @@ -156,7 +156,7 @@ def get_cmakelist(filename): linter._read_file = get_cmakelist if checks is not None: linter.require(checks) - info = LintInfo(env) + info = LintInfo(env, linter=linter) linter.lint(os.path.normpath(package_path), manifest, info=info) if not indentation: linter.messages = [m for m in linter.messages if m.id != "INDENTATION"] diff --git a/test/test_linter.py b/test/test_linter.py index bd1640a..f0285f7 100644 --- a/test/test_linter.py +++ b/test/test_linter.py @@ -99,6 +99,54 @@ def test_pragma(self): CATKIN_PACKAGE() """, checks=cc.all) self.assertEqual([], result) + result = mock_lint(env, pkg, + """ + #catkin_lint: ignore cmd_case[cmd=CATKIN_PACKAGE] + project(mock) + find_package(catkin REQUIRED) + CATKIN_PACKAGE() + """, checks=cc.all) + self.assertEqual([], result) + result = mock_lint(env, pkg, + """ + #catkin_lint: ignore cmd_case[cmd!=PROJECT] + project(mock) + find_package(catkin REQUIRED) + CATKIN_PACKAGE() + """, checks=cc.all) + self.assertEqual([], result) + result = mock_lint(env, pkg, + """ + #catkin_lint: ignore cmd_case[cmd!=CATKIN_PACKAGE] + project(mock) + find_package(catkin REQUIRED) + CATKIN_PACKAGE() + """, checks=cc.all) + self.assertEqual(["CMD_CASE"], result) + result = mock_lint(env, pkg, + """ + #catkin_lint: ignore cmd_case[cmd~(?i)catkin_package] + project(mock) + find_package(catkin REQUIRED) + CATKIN_PACKAGE() + """, checks=cc.all) + self.assertEqual([], result) + result = mock_lint(env, pkg, + """ + #catkin_lint: ignore cmd_case[cmd!~(?i)catkin_package] + project(mock) + find_package(catkin REQUIRED) + CATKIN_PACKAGE() + """, checks=cc.all) + self.assertEqual(["CMD_CASE"], result) + result = mock_lint(env, pkg, + """ + #catkin_lint: ignore cmd_case[cmd=PROJECT] + project(mock) + find_package(catkin REQUIRED) + CATKIN_PACKAGE() + """, checks=cc.all) + self.assertEqual(["CMD_CASE"], result) result = mock_lint(env, pkg, """ #catkin_lint: ignore cmd_case