diff --git a/scripts/labels/checker_labels.py b/scripts/labels/checker_labels.py index 682f6687bb..0c1f9ea004 100644 --- a/scripts/labels/checker_labels.py +++ b/scripts/labels/checker_labels.py @@ -7,9 +7,10 @@ # ------------------------------------------------------------------------- """Provides I/O with the configuration files that describe checker labels.""" from collections import deque +from enum import Enum, auto as Enumerator import json import pathlib -from typing import Dict, List, Optional, cast +from typing import Callable, Dict, List, Optional, Set, cast from codechecker_common.checker_labels import split_label_kv @@ -22,6 +23,28 @@ Labels = Dict[str, Dict[str, str]] +K_LabelToolSkipDirective = "label-tool-skip" + + +class SkipDirectiveRespectStyle(Enum): + """ + Do not respect the directive. + """ + NoAction = Enumerator() + + """ + Fetch the list of the relevant skip directives automatically, and respect + it. + """ + AutomaticYes = Enumerator() + + """ + Respect only the skip list passed directly with the style argument, and + do not perform automatic fetching. + """ + AsPassed = Enumerator() + + def _load_json(path: pathlib.Path) -> Dict: try: with path.open("r") as file: @@ -59,22 +82,77 @@ def _save_json(path: pathlib.Path, data: Dict): raise +def _project_labels_by_key( + label_cfg: _ConfigFileLabels, + key: str, + value_predicate: Optional[Callable[[str], bool]] = None +) -> _ConfigFileLabels: + """ + Projects the `label_cfg` to a mapping of ``Checker -> List[T]``, in which + only the **values** of labels with the specified `key` are kept, and all + other labels are ignored. + + If `value_predicate` is set, in addition to the `key` matching, will only + keep values that satisfy the given predicate. + """ + return { + checker: [label_v + for label in labels + for label_k, label_v in (split_label_kv(label),) + if label_k == key + and (not value_predicate or value_predicate(label_v))] + for checker, labels in label_cfg.items()} + + class MultipleLabelsError(Exception): """ Raised by `get_checker_labels` if multiple labels exist for the same key. """ def __init__(self, key): - super().__init__("Multiple labels with key: %s", key) + super().__init__("Multiple labels with key: %s" % key) self.key = key -def get_checker_labels(analyser: str, path: pathlib.Path, key: str) \ - -> SingleLabels: +def get_checkers_with_ignore_of_key(path: pathlib.Path, + key: str) -> Set[str]: + """ + Loads the checker config label file available at `path` and filters it for + the list of checkers that are set to ignore/skip labels of the specified + `key`, i.e., a ``label-tool-skip:KEY`` exists for `key`'s value amongst the + checker's labels. + """ + try: + label_cfg = cast(_ConfigFileLabels, _load_json(path)["labels"]) + except KeyError: + error("'%s' is not a label config file", path) + raise + + labels_skip_of_key = _project_labels_by_key( + label_cfg, K_LabelToolSkipDirective, + lambda skip: skip == key) + return {checker + for checker, labels in labels_skip_of_key.items() + if len(labels)} + + +def get_checker_labels( + analyser: str, + path: pathlib.Path, + key: str, + skip_directive_handling: SkipDirectiveRespectStyle = + SkipDirectiveRespectStyle.AutomaticYes, + checkers_to_skip: Optional[Set[str]] = None +) -> SingleLabels: """ Loads and filters the checker config label file available at `path` for the `key` label. Raises `MultipleLabelsError` if there is at least two labels with the same `key`. + + Labels of a particular "type" for which a skip directive + (``label-tool-skip:KEY``, e.g., ``label-tool-skip:severity``) exists will + not appear, as-if the label did not even exist, depending on + `skip_directive_handling`'s value. """ try: label_cfg = cast(_ConfigFileLabels, _load_json(path)["labels"]) @@ -82,33 +160,47 @@ def get_checker_labels(analyser: str, path: pathlib.Path, key: str) \ error("'%s' is not a label config file", path) raise + if skip_directive_handling == SkipDirectiveRespectStyle.NoAction or \ + checkers_to_skip is None: + checkers_to_skip = set() + elif skip_directive_handling == SkipDirectiveRespectStyle.AutomaticYes: + checkers_to_skip = get_checkers_with_ignore_of_key(path, key) filtered_labels = { - checker: [label_v - for label in labels - for label_k, label_v in (split_label_kv(label),) - if label_k == key] - for checker, labels in label_cfg.items()} + checker: labels + for checker, labels in _project_labels_by_key(label_cfg, key).items() + if checker not in checkers_to_skip} if OutputSettings.trace(): deque((trace("No '%s:' label found for '%s/%s'", key, analyser, checker) for checker, labels in filtered_labels.items() - if not labels), maxlen=0) + if not labels and checker not in checkers_to_skip), maxlen=0) - if any(len(labels) > 1 for labels in filtered_labels.values()): + if any(len(labels) > 1 for labels in filtered_labels.values() if labels): raise MultipleLabelsError(key) + return {checker: labels[0] if labels else None for checker, labels in filtered_labels.items()} -def update_checker_labels(analyser: str, - path: pathlib.Path, - key: str, - updates: SingleLabels): +def update_checker_labels( + analyser: str, + path: pathlib.Path, + key: str, + updates: SingleLabels, + skip_directive_handling: SkipDirectiveRespectStyle = + SkipDirectiveRespectStyle.AutomaticYes, + checkers_to_skip: Optional[Set[str]] = None +): """ Loads a checker config label file available at `path` and updates the `key` labels based on the `updates` structure, overwriting or adding the existing label (or raising `MultipleLabelsError` if it is not unique which one to overwrite), then writes the resulting data structure back to `path`. + + Labels of a particular "type" for which a skip directive + (``label-tool-skip:KEY``, e.g., ``label-tool-skip:severity``) exists will + not be written or updated in the config file, even if the value was present + in `updates`, depending on `skip_directive_handling`'s value. """ try: config = _load_json(path) @@ -117,17 +209,25 @@ def update_checker_labels(analyser: str, error("'%s's '%s' is not a label config file", analyser, path) raise + if skip_directive_handling == SkipDirectiveRespectStyle.NoAction or \ + checkers_to_skip is None: + checkers_to_skip = set() + elif skip_directive_handling == SkipDirectiveRespectStyle.AutomaticYes: + checkers_to_skip = get_checkers_with_ignore_of_key(path, key) label_indices = { checker: [index for index, label in enumerate(labels) if split_label_kv(label)[0] == key] for checker, labels in label_cfg.items() - } + if checker not in checkers_to_skip} if any(len(indices) > 1 for indices in label_indices.values()): raise MultipleLabelsError(key) label_indices = {checker: indices[0] if len(indices) == 1 else None for checker, indices in label_indices.items()} for checker, new_label in updates.items(): + if checker in checkers_to_skip: + continue + try: checker_labels = label_cfg[checker] except KeyError: diff --git a/scripts/labels/doc_url/generate_tool/__main__.py b/scripts/labels/doc_url/generate_tool/__main__.py index 0fcec43ef9..abfecb6f02 100755 --- a/scripts/labels/doc_url/generate_tool/__main__.py +++ b/scripts/labels/doc_url/generate_tool/__main__.py @@ -15,8 +15,8 @@ from tabulate import tabulate -from ...checker_labels import SingleLabels, get_checker_labels, \ - update_checker_labels +from ...checker_labels import SingleLabels, SkipDirectiveRespectStyle, \ + get_checker_labels, get_checkers_with_ignore_of_key, update_checker_labels from ...codechecker import default_checker_label_dir from ...exception import EngineError from ...output import Settings as GlobalOutputSettings, \ @@ -65,6 +65,9 @@ epilogue: str = "" +K_DocUrl: str = "doc_url" + + def args(parser: Optional[argparse.ArgumentParser]) -> argparse.ArgumentParser: if not parser: parser = argparse.ArgumentParser( @@ -211,8 +214,11 @@ def main(args: argparse.Namespace) -> Optional[int]: analyser, path) try: - labels = get_checker_labels(analyser, path, "doc_url") - pass + checkers_to_skip = get_checkers_with_ignore_of_key( + path, K_DocUrl) + labels = get_checker_labels(analyser, path, K_DocUrl, + SkipDirectiveRespectStyle.AsPassed, + checkers_to_skip) except Exception: import traceback traceback.print_exc() @@ -240,6 +246,7 @@ def main(args: argparse.Namespace) -> Optional[int]: analyser, generator_class, labels, + checkers_to_skip ) statistics.append(statistic) rc = int(tool.ReturnFlags(rc) | status) @@ -269,7 +276,10 @@ def main(args: argparse.Namespace) -> Optional[int]: analyser, path) try: - update_checker_labels(analyser, path, "doc_url", urls) + update_checker_labels( + analyser, path, K_DocUrl, urls, + SkipDirectiveRespectStyle.AsPassed, + checkers_to_skip) except Exception: import traceback traceback.print_exc() diff --git a/scripts/labels/doc_url/generate_tool/tool.py b/scripts/labels/doc_url/generate_tool/tool.py index 9be7a43205..d47ffba8ac 100644 --- a/scripts/labels/doc_url/generate_tool/tool.py +++ b/scripts/labels/doc_url/generate_tool/tool.py @@ -9,7 +9,7 @@ from collections import deque from enum import IntFlag, auto as Enumerator import sys -from typing import List, NamedTuple, Optional, Tuple, Type, cast +from typing import List, NamedTuple, Optional, Set, Tuple, Type, cast from ...checker_labels import SingleLabels from ...output import Settings as GlobalOutputSettings, log, coloured, emoji @@ -26,7 +26,8 @@ class Statistics(NamedTuple): Analyser: str Generator: str Checkers: int - Skipped: Optional[int] + Generator_Skipped: Optional[int] + Directive_Skipped: Optional[int] Missing: Optional[int] OK: Optional[int] Updated: Optional[int] @@ -53,13 +54,17 @@ class ReturnFlags(IntFlag): RemainsMissing = Enumerator() -def run_generator(generator: Base, urls: SingleLabels) \ - -> Tuple[List[str], SingleLabels, SingleLabels, List[str]]: +def run_generator(generator: Base, urls: SingleLabels, + checkers_to_skip: Set[str]) \ + -> Tuple[List[str], SingleLabels, SingleLabels, List[str], + List[str], List[str]]: analyser = generator.analyser ok: List[str] = list() updated: SingleLabels = dict() new: SingleLabels = dict() gone: List[str] = list() + generator_skip: List[str] = list() + directive_skip: List[str] = list() generation_result: SingleLabels = dict(generator.generate()) for checker in sorted(urls.keys() | generation_result.keys()): @@ -70,6 +75,16 @@ def run_generator(generator: Base, urls: SingleLabels) \ analyser, checker, coloured("SKIP", "light_magenta"), file=sys.stderr) + generator_skip.append(checker) + continue + if checker in checkers_to_skip: + if GlobalOutputSettings.trace(): + log("%s%s/%s: %s", + emoji(":stop_sign: "), + analyser, checker, + coloured("DIRECTIVE-SKIP", "light_magenta"), + file=sys.stderr) + directive_skip.append(checker) continue existing_url, new_url = \ @@ -117,7 +132,7 @@ def run_generator(generator: Base, urls: SingleLabels) \ existing_url, file=sys.stdout) - return ok, updated, new, gone + return ok, updated, new, gone, generator_skip, directive_skip def print_generation(analyser: str, @@ -206,18 +221,19 @@ def print_missing(analyser: str, maxlen=0) -def execute(analyser: str, generator_class: Type, labels: SingleLabels) \ +def execute(analyser: str, generator_class: Type, labels: SingleLabels, + checkers_to_skip: Set[str]) \ -> Tuple[ReturnFlags, SingleLabels, Statistics]: """ Runs one instance of the generation for a specific analyser. """ status = cast(ReturnFlags, 0) - generator = generator_class(analyser) missing = [checker for checker in labels if not labels[checker]] stats = Statistics(Analyser=analyser, Generator=generator_class.kind, Checkers=len(labels), - Skipped=None, + Generator_Skipped=None, + Directive_Skipped=None, Missing=len(missing) if missing else None, OK=None, Updated=None, @@ -227,7 +243,10 @@ def execute(analyser: str, generator_class: Type, labels: SingleLabels) \ Not_Found=len(missing) if missing else None, ) urls: SingleLabels = dict() - ok, updated, new, gone = run_generator(generator_class(analyser), labels) + ok, updated, new, gone, generator_skip, directive_skip = \ + run_generator(generator_class(analyser), + labels, + checkers_to_skip) print_generation(analyser, labels, ok, updated, new) urls.update(updated) urls.update(new) @@ -235,16 +254,19 @@ def execute(analyser: str, generator_class: Type, labels: SingleLabels) \ ok = set(ok) new = set(new) gone = set(gone) - to_skip = {checker for checker - in (labels.keys() | ok | new | gone) - if generator.skip(checker)} + generator_skip = set(generator_skip) + directive_skip = set(directive_skip) + any_skip = generator_skip | directive_skip print_gone(analyser, {checker: labels[checker] - for checker in gone - to_skip}) + for checker in gone - any_skip}) remaining_missing = [checker for checker - in labels.keys() - ok - updated.keys() - to_skip] + in labels.keys() - ok - updated.keys() - any_skip] print_missing(analyser, remaining_missing) - stats = stats._replace(Skipped=len(to_skip) if to_skip else None, + stats = stats._replace(Generator_Skipped=len(generator_skip) + if generator_skip else None, + Directive_Skipped=len(directive_skip) + if directive_skip else None, OK=len(ok) if ok else None, Updated=len(updated) if updated else None, Gone=len(gone) if gone else None, diff --git a/scripts/labels/doc_url/verify_tool/__main__.py b/scripts/labels/doc_url/verify_tool/__main__.py index 8359d3ae6a..c36e4abd21 100755 --- a/scripts/labels/doc_url/verify_tool/__main__.py +++ b/scripts/labels/doc_url/verify_tool/__main__.py @@ -19,8 +19,8 @@ from codechecker_common.compatibility.multiprocessing import cpu_count from codechecker_common.util import clamp -from ...checker_labels import SingleLabels, get_checker_labels, \ - update_checker_labels +from ...checker_labels import SingleLabels, SkipDirectiveRespectStyle, \ + get_checker_labels, get_checkers_with_ignore_of_key, update_checker_labels from ...codechecker import default_checker_label_dir from ...exception import EngineError from ...output import Settings as GlobalOutputSettings, \ @@ -72,6 +72,9 @@ epilogue: str = "" +K_DocUrl: str = "doc_url" + + def args(parser: Optional[argparse.ArgumentParser]) -> argparse.ArgumentParser: if not parser: parser = argparse.ArgumentParser( @@ -285,7 +288,18 @@ def main(args: argparse.Namespace) -> Optional[int]: analyser, path) try: - labels = get_checker_labels(analyser, path, "doc_url") + checkers_to_skip = get_checkers_with_ignore_of_key( + path, K_DocUrl) + labels = get_checker_labels(analyser, path, K_DocUrl, + SkipDirectiveRespectStyle.AsPassed, + checkers_to_skip) + + # Note: get_checker_labels() will *IGNORE* the checkers present + # in checkers_to_skip. + # This results in the checkers with "label-tool-skip:doc_url" + # label staying invisible during the execution of this tool, + # hence the called executing functions do not need to get this + # data structure. except Exception: import traceback traceback.print_exc() @@ -352,7 +366,10 @@ def main(args: argparse.Namespace) -> Optional[int]: analyser, path) try: - update_checker_labels(analyser, path, "doc_url", fixes) + update_checker_labels( + analyser, path, K_DocUrl, fixes, + SkipDirectiveRespectStyle.AsPassed, + checkers_to_skip) except Exception: import traceback traceback.print_exc() diff --git a/scripts/labels/doc_url/verify_tool/tool.py b/scripts/labels/doc_url/verify_tool/tool.py index 3943791417..28e8725a14 100644 --- a/scripts/labels/doc_url/verify_tool/tool.py +++ b/scripts/labels/doc_url/verify_tool/tool.py @@ -25,7 +25,7 @@ class Statistics(NamedTuple): Analyser: str Checkers: int Verifier: str - Skipped: Optional[int] + Verifier_Skipped: Optional[int] Reset: Optional[int] Missing: Optional[int] Verified: Optional[int] @@ -67,7 +67,7 @@ def execute(analyser: str, stats = Statistics(Analyser=analyser, Checkers=len(labels), Verifier=verifier_class.kind, - Skipped=None, + Verifier_Skipped=None, Reset=None, Missing=None, Verified=None, @@ -93,7 +93,7 @@ def execute(analyser: str, report.print_verifications(analyser, labels, ok, not_ok, missing) urls_to_save.update({checker: labels[checker] for checker in ok}) verified = len(labels) - skip - len(missing) - stats = stats._replace(Skipped=skip if skip else None, + stats = stats._replace(Verifier_Skipped=skip if skip else None, Missing=len(missing) if missing else None, Verified=verified if verified else None, OK=len(ok) if ok else None, diff --git a/scripts/labels/severity/generate_tool/__main__.py b/scripts/labels/severity/generate_tool/__main__.py index b21faff79d..4cc693d9c1 100755 --- a/scripts/labels/severity/generate_tool/__main__.py +++ b/scripts/labels/severity/generate_tool/__main__.py @@ -15,8 +15,8 @@ from tabulate import tabulate -from ...checker_labels import SingleLabels, get_checker_labels, \ - update_checker_labels +from ...checker_labels import SingleLabels, SkipDirectiveRespectStyle, \ + get_checker_labels, get_checkers_with_ignore_of_key, update_checker_labels from ...codechecker import default_checker_label_dir from ...exception import EngineError from ...output import Settings as GlobalOutputSettings, \ @@ -67,6 +67,9 @@ epilogue: str = "" +K_Severity: str = "severity" + + def args(parser: Optional[argparse.ArgumentParser]) -> argparse.ArgumentParser: if not parser: parser = argparse.ArgumentParser( @@ -213,8 +216,11 @@ def main(args: argparse.Namespace) -> Optional[int]: analyser, path) try: - labels = get_checker_labels(analyser, path, "severity") - pass + checkers_to_skip = get_checkers_with_ignore_of_key( + path, K_Severity) + labels = get_checker_labels(analyser, path, K_Severity, + SkipDirectiveRespectStyle.AsPassed, + checkers_to_skip) except Exception: import traceback traceback.print_exc() @@ -222,8 +228,8 @@ def main(args: argparse.Namespace) -> Optional[int]: error("Failed to obtain checker labels for '%s'!", analyser) continue - geners = list(analyser_selection.select_generator(analyser)) - if not geners: + generators = list(analyser_selection.select_generator(analyser)) + if not generators: log("%sSkipped '%s', no generator implementation!", emoji(":no_littering: "), analyser) @@ -231,7 +237,7 @@ def main(args: argparse.Namespace) -> Optional[int]: severities: SingleLabels = dict() conflicts: Set[str] = set() - for generator_class in geners: + for generator_class in generators: log("%sGenerating '%s' as '%s' (%s)...", emoji(":thought_balloon: "), analyser, @@ -242,6 +248,7 @@ def main(args: argparse.Namespace) -> Optional[int]: analyser, generator_class, labels, + checkers_to_skip, ) statistics.append(statistic) rc = int(tool.ReturnFlags(rc) | status) @@ -271,8 +278,10 @@ def main(args: argparse.Namespace) -> Optional[int]: analyser, path) try: - update_checker_labels(analyser, path, "severity", - severities) + update_checker_labels( + analyser, path, K_Severity, severities, + SkipDirectiveRespectStyle.AsPassed, + checkers_to_skip) except Exception: import traceback traceback.print_exc() diff --git a/scripts/labels/severity/generate_tool/tool.py b/scripts/labels/severity/generate_tool/tool.py index eb9e1cc5f3..88803be670 100644 --- a/scripts/labels/severity/generate_tool/tool.py +++ b/scripts/labels/severity/generate_tool/tool.py @@ -9,7 +9,7 @@ from collections import deque from enum import IntFlag, auto as Enumerator import sys -from typing import List, NamedTuple, Optional, Tuple, Type, cast +from typing import List, NamedTuple, Optional, Set, Tuple, Type, cast from ...checker_labels import SingleLabels from ...output import Settings as GlobalOutputSettings, log, coloured, emoji @@ -26,7 +26,8 @@ class Statistics(NamedTuple): Analyser: str Generator: str Checkers: int - Skipped: Optional[int] + Generator_Skipped: Optional[int] + Directive_Skipped: Optional[int] Missing: Optional[int] OK: Optional[int] Updated: Optional[int] @@ -53,13 +54,17 @@ class ReturnFlags(IntFlag): RemainsMissing = Enumerator() -def run_generator(generator: Base, severities: SingleLabels) \ - -> Tuple[List[str], SingleLabels, SingleLabels, List[str]]: +def run_generator(generator: Base, severities: SingleLabels, + checkers_to_skip: Set[str]) \ + -> Tuple[List[str], SingleLabels, SingleLabels, List[str], + List[str], List[str]]: analyser = generator.analyser ok: List[str] = list() updated: SingleLabels = dict() new: SingleLabels = dict() gone: List[str] = list() + generator_skip: List[str] = list() + directive_skip: List[str] = list() generation_result: SingleLabels = dict(generator.generate()) for checker in sorted(severities.keys() | generation_result.keys()): @@ -70,6 +75,16 @@ def run_generator(generator: Base, severities: SingleLabels) \ analyser, checker, coloured("SKIP", "light_magenta"), file=sys.stderr) + generator_skip.append(checker) + continue + if checker in checkers_to_skip: + if GlobalOutputSettings.trace(): + log("%s%s/%s: %s", + emoji(":stop_sign: "), + analyser, checker, + coloured("DIRECTIVE-SKIP", "light_magenta"), + file=sys.stderr) + directive_skip.append(checker) continue existing_severity, new_severity = \ @@ -117,7 +132,7 @@ def run_generator(generator: Base, severities: SingleLabels) \ existing_severity, file=sys.stdout) - return ok, updated, new, gone + return ok, updated, new, gone, generator_skip, directive_skip def print_generation(analyser: str, @@ -207,8 +222,12 @@ def print_missing(analyser: str, maxlen=0) -def execute(analyser: str, generator_class: Type, labels: SingleLabels) \ - -> Tuple[ReturnFlags, SingleLabels, Statistics]: +def execute( + analyser: str, + generator_class: Type, + labels: SingleLabels, + checkers_to_skip: Set[str] +) -> Tuple[ReturnFlags, SingleLabels, Statistics]: """ Runs one instance of the generation for a specific analyser. """ @@ -218,7 +237,8 @@ def execute(analyser: str, generator_class: Type, labels: SingleLabels) \ stats = Statistics(Analyser=analyser, Generator=generator_class.kind, Checkers=len(labels), - Skipped=None, + Generator_Skipped=None, + Directive_Skipped=None, Missing=len(missing) if missing else None, OK=None, Updated=None, @@ -228,7 +248,10 @@ def execute(analyser: str, generator_class: Type, labels: SingleLabels) \ Not_Found=len(missing) if missing else None, ) severities: SingleLabels = dict() - ok, updated, new, gone = run_generator(generator_class(analyser), labels) + ok, updated, new, gone, generator_skip, directive_skip = \ + run_generator(generator_class(analyser), + labels, + checkers_to_skip) print_generation(analyser, labels, ok, updated, new) severities.update(updated) severities.update(new) @@ -236,16 +259,20 @@ def execute(analyser: str, generator_class: Type, labels: SingleLabels) \ ok = set(ok) new = set(new) gone = set(gone) - to_skip = {checker for checker - in (labels.keys() | ok | new | gone) - if generator.skip(checker)} + generator_skip = set(generator_skip) + directive_skip = set(directive_skip) + any_skip = generator_skip | directive_skip print_gone(analyser, {checker: labels[checker] - for checker in gone - to_skip}) + for checker in gone - any_skip}) remaining_missing = [checker for checker - in labels.keys() - ok - updated.keys() - to_skip] + in labels.keys() - ok - updated.keys() - + any_skip] print_missing(analyser, remaining_missing) - stats = stats._replace(Skipped=len(to_skip) if to_skip else None, + stats = stats._replace(Generator_Skipped=len(generator_skip) + if generator_skip else None, + Directive_Skipped=len(directive_skip) + if directive_skip else None, OK=len(ok) if ok else None, Updated=len(updated) if updated else None, Gone=len(gone) if gone else None,