Skip to content

Commit

Permalink
feat(script): Support label-tool-skip directive labels
Browse files Browse the repository at this point in the history
In some cases it might be deemed necessary that a label (e.g., a higher
or lower `severity`) is more appropriate than what the tool would
otherwise auto-generate.
In such cases, a `label-tool-skip:LABEL_KEY` (e.g.,
`label-tool-skip:severity`, as applied in
commit 04d27ab) can indicate to stamp
the fact that a developer overruled the tools' decisions.

Prior to this commit, the tooling actually disregarded this information,
but now I added the necessary handling to the existing aspects.
  • Loading branch information
whisperity committed Jul 9, 2024
1 parent 716d70e commit 962f449
Show file tree
Hide file tree
Showing 7 changed files with 252 additions and 68 deletions.
132 changes: 116 additions & 16 deletions scripts/labels/checker_labels.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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:
Expand Down Expand Up @@ -59,56 +82,125 @@ 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"])
except KeyError:
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)
Expand All @@ -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:
Expand Down
20 changes: 15 additions & 5 deletions scripts/labels/doc_url/generate_tool/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, \
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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()
Expand Down
52 changes: 37 additions & 15 deletions scripts/labels/doc_url/generate_tool/tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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]
Expand All @@ -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()):
Expand All @@ -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 = \
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -227,24 +243,30 @@ 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)

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,
Expand Down
Loading

0 comments on commit 962f449

Please sign in to comment.