Skip to content

Commit

Permalink
[gui] Show checker labels in GUI
Browse files Browse the repository at this point in the history
When clicking on a checker name in the GUI, we present the belonging
checker labels. This also contains the documentation which now comes
from the label file.
  • Loading branch information
bruntib committed Nov 3, 2021
1 parent 4be1eca commit aa72dc0
Show file tree
Hide file tree
Showing 29 changed files with 1,328 additions and 693 deletions.
7 changes: 5 additions & 2 deletions analyzer/codechecker_analyzer/cmd/checkers.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,10 @@ def __guideline_to_label(
"--label guideline:sei-cert" and "--guideline sei-cert:str38-c" is the same
as "--label sei-cert:str38-c".
"""
guidelines = cl.occurring_values('guideline', args.analyzers)
guidelines = []
for analyzer in args.analyzers:
guidelines.extend(cl.occurring_values('guideline', analyzer))

if args.guideline in guidelines:
return f'guideline:{args.guideline}'
else:
Expand Down Expand Up @@ -580,7 +583,7 @@ def __print_checkers(args: argparse.Namespace, cl: CheckerLabels):
result = []
for analyzer in args.analyzers:
if labels:
checkers = cl.checkers_by_labels(labels, [analyzer])
checkers = cl.checkers_by_labels(labels, analyzer)
result.extend(
filter(lambda x: x[1] in checkers, checker_info[analyzer]))
else:
Expand Down
48 changes: 48 additions & 0 deletions analyzer/tests/unit/test_checker_labels.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,16 @@ def test_checker_labels(self):
cl.label_of_checker('globalChecker', 'profile'),
['security'])

self.assertEqual(
cl.label_of_checker(
'bugprone-undelegated-constructor', 'severity', 'clang-tidy'),
'MEDIUM')

self.assertEqual(
cl.label_of_checker(
'bugprone-undelegated-constructor', 'severity', 'clangsa'),
'UNSPECIFIED')

self.assertEqual(
sorted(cl.labels_of_checker('globalChecker')),
sorted([
Expand All @@ -169,3 +179,41 @@ def test_checker_labels(self):
self.assertEqual(
sorted(cl.occurring_values('profile')),
sorted(['default', 'extreme', 'security', 'sensitive']))

self.assertEqual(
cl.severity('bugprone-undelegated-constructor'),
'MEDIUM')

self.assertEqual(
cl.severity('bugprone-undelegated-constructor', 'clang-tidy'),
'MEDIUM')

self.assertEqual(
cl.severity('bugprone-undelegated-constructor', 'clangsa'),
'UNSPECIFIED')

self.assertEqual(
cl.get_description('profile'), {
'default': 'Default documentation',
'sensitive': 'Sensitive documentation',
'extreme': 'Extreme documentation'})

self.assertEqual(
sorted(cl.checkers()),
sorted([
'globalChecker',
'core.DivideZero',
'core.NonNullParamChecker',
'core.builtin.NoReturnFunctions',
'globalChecker',
'bugprone-undelegated-constructor',
'google-objc-global-variable-declaration',
'cert-err34-c']))

self.assertEqual(
sorted(cl.checkers('clang-tidy')),
sorted([
'globalChecker',
'bugprone-undelegated-constructor',
'google-objc-global-variable-declaration',
'cert-err34-c']))
99 changes: 56 additions & 43 deletions codechecker_common/checker_labels.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import os
from collections import defaultdict
from itertools import chain
from typing import Dict, Iterable, List, Tuple, Union
from typing import Any, DefaultDict, Dict, Iterable, List, Optional, Set, \
Tuple, Union, cast
from codechecker_common.util import load_json_or_empty


# TODO: Most of the methods of this class get an optional analyzer name. If
# None is given to these functions then labels of any analyzer's checkers is
# taken into account. This the union of all analyzers' checkers is a bad
# approach because different analyzers may theoretically have checkers with the
# same name. Fortunately this is not the case with the current analyzers'
# checkers, so now it works properly.
class CheckerLabels:
# These labels should be unique by each analyzer. If this uniqueness is
# violated then an error is thrown during label JSON file parse. The
Expand All @@ -17,7 +23,7 @@ def __init__(self, checker_labels_dir: str):
raise NotADirectoryError(
f'{checker_labels_dir} is not a directory.')

label_json_files = os.listdir(
label_json_files: Iterable[str] = os.listdir(
os.path.join(checker_labels_dir, 'analyzers'))

self.__descriptions = {}
Expand All @@ -36,7 +42,7 @@ def __init__(self, checker_labels_dir: str):
def __union_label_files(
self,
label_files: Iterable[str]
) -> Dict[str, Dict[str, List[str]]]:
) -> Dict[str, DefaultDict[str, List[str]]]:
"""
This function creates an union object of the given label files. The
resulting object maps analyzers to the collection of their checkers
Expand Down Expand Up @@ -80,7 +86,7 @@ def __get_label_key_value(self, key_value: str) -> Tuple[str, str]:

return key_value[:pos].strip(), key_value[pos + 1:].strip()

def __check_json_format(self, data: dict) -> bool:
def __check_json_format(self, data: dict):
"""
Check the format of checker labels' JSON config file, i.e. this file
must contain specific values with specific types. For example the
Expand Down Expand Up @@ -125,34 +131,44 @@ def is_unique(labels: Iterable[str], label: str):
f'Label "severity" should be unique for checker '
'{checker}.')

def __get_analyzer_data(self, analyzers: Iterable[str]):
for analyzer, checkers in self.__data.items():
if analyzers is None or analyzer in analyzers:
yield analyzer, checkers
def __get_analyzer_data(
self,
analyzer: Optional[str] = None
) -> Iterable[Tuple[str, Any]]:
"""
Most functions of this class require an analyzer name which determines
a checker name specifically. If no analyzer is given then all of them
is taken into account (for backward-compatibility reasons). This helper
function yields either an analyzer's label data or all analyzer's
label data.
"""
for a, c in self.__data.items():
if analyzer is None or a == analyzer:
yield a, c

def checkers_by_labels(
self,
filter_labels: Iterable[str],
analyzers: Iterable[str] = None
analyzer: Optional[str] = None
) -> List[str]:
"""
Returns a list of checkers that have at least one of the specified
labels.
filter_labels -- A string list which contains labels with specified
values. E.g. ['profile:default', 'severity:high'].
analyzers -- A list of analyzers among which checkers are searched.
By default all analyzers are searched.
analyzer -- An optional analyzer name of which checkers are searched.
By default all analyzers are searched.
"""
collection = []

filter_labels = set(map(self.__get_label_key_value, filter_labels))
label_set = set(map(self.__get_label_key_value, filter_labels))

for _, checkers in self.__get_analyzer_data(analyzers):
for _, checkers in self.__get_analyzer_data(analyzer):
for checker, labels in checkers.items():
labels = set(map(self.__get_label_key_value, labels))

if labels.intersection(filter_labels):
if labels.intersection(label_set):
collection.append(checker)

return collection
Expand All @@ -161,7 +177,7 @@ def label_of_checker(
self,
checker: str,
label: str,
analyzers: Iterable[str] = None
analyzer: Optional[str] = None
) -> Union[str, List[str]]:
"""
If a label has unique constraint then this function retuns the value
Expand All @@ -172,51 +188,48 @@ def label_of_checker(
are also searched. For example "clang-diagnostic" in the config file
matches "clang-diagnostic-unused-argument".
"""
labels = chain.from_iterable([])

for analyzer, _ in self.labels_of_checker(checker, analyzers):
if analyzers is None or analyzer in analyzers:
labels = chain(labels, filter(
lambda lab: lab[0] == label,
self.labels_of_checker(checker)))
labels = (
value
for key, value in self.labels_of_checker(checker, analyzer)
if key == label)

if label in CheckerLabels.UNIQUE_LABELS:
try:
return next(labels)[1]
except Exception:
return next(labels)
except StopIteration:
return CheckerLabels.UNIQUE_LABELS[label]

# TODO set() is used for uniqueing results in case a checker name is
# provided by multiple analyzers. This will be unnecessary when we
# cover this case properly.
return list(set(map(lambda value: value[1], labels)))
return list(set(labels))

def severity(self, checker: str, analyzers: Iterable[str] = None) -> str:
def severity(self, checker: str, analyzer: Optional[str] = None) -> str:
"""
Shorthand for the following call:
checker_labels.label_of_checker(checker, 'severity', analyzers)
checker_labels.label_of_checker(checker, 'severity', analyzer)
"""
return self.label_of_checker(checker, 'severity', analyzers)
return cast(str, self.label_of_checker(checker, 'severity', analyzer))

def labels_of_checker(
self,
checker: str,
analyzers: Iterable[str] = None
) -> List[str]:
analyzer: Optional[str] = None
) -> List[Tuple[str, str]]:
"""
Return the list of labels of a checker. The list contains (label,
value) pairs. If the checker name is not found in the label config file
then its prefixes are also searched. For example "clang-diagnostic" in
the config file matches "clang-diagnostic-unused-argument".
"""
labels = []
labels: List[Tuple[str, str]] = []

for _, checkers in self.__get_analyzer_data(analyzers):
c = checker
for _, checkers in self.__get_analyzer_data(analyzer):
c: Optional[str] = checker

if c not in checkers:
c = next(filter(
lambda c: checker.startswith(c),
lambda c: checker.startswith(cast(str, c)),
iter(checkers.keys())), None)

labels.extend(
Expand All @@ -227,30 +240,30 @@ def labels_of_checker(
# cover this case properly.
return list(set(labels))

def get_description(self, label: str) -> Dict[str, str]:
def get_description(self, label: str) -> Optional[Dict[str, str]]:
"""
Returns the descriptions of the given label's values.
"""
return self.__descriptions.get(label)

def checkers(self, analyzers: Iterable[str] = None) -> List[str]:
def checkers(self, analyzer: Optional[str] = None) -> List[str]:
"""
Return the list of available checkers.
"""
collection = []

for _, checkers in self.__get_analyzer_data(analyzers):
for _, checkers in self.__get_analyzer_data(analyzer):
collection.extend(checkers.keys())

return collection

def labels(self, analyzers: Iterable[str] = None) -> List[str]:
def labels(self, analyzer: Optional[str] = None) -> List[str]:
"""
Returns a list of occurring labels.
"""
collection = set()
collection: Set[str] = set()

for _, checkers in self.__get_analyzer_data(analyzers):
for _, checkers in self.__get_analyzer_data(analyzer):
for labels in checkers.values():
collection.update(map(
lambda x: self.__get_label_key_value(x)[0], labels))
Expand All @@ -260,15 +273,15 @@ def labels(self, analyzers: Iterable[str] = None) -> List[str]:
def occurring_values(
self,
label: str,
analyzers: Iterable[str] = None
analyzer: Optional[str] = None
) -> List[str]:
"""
Return the list of values belonging to the given label which were used
for at least one checker.
"""
values = set()

for _, checkers in self.__get_analyzer_data(analyzers):
for _, checkers in self.__get_analyzer_data(analyzer):
for labels in checkers.values():
for lab, value in map(self.__get_label_key_value, labels):
if lab == label:
Expand Down
Loading

0 comments on commit aa72dc0

Please sign in to comment.