Skip to content

Commit 547bd6f

Browse files
committed
[feat] Guideline statistics
The new `Guideline statistics` tab on the Statistics page can list all rules for the selected guidelines. The user can select multiple guidelines (but currently the only one is `sei-cert` and it is the default). The table can show the checker statistics that are related to the specified guideline rule. Rules may connect to more than one checker or may not have any checker. The checker statistics are calculated for runs that are selected (or for all runs if no run selected) in the report filter. It can show guideline name, guideline rule, checker name, checker severity, checker status, number of closed and outstanding reports. The status informs the user about how many runs the given checker was enabled or disabled. Closed and outstanding report counts depend on review and detection status. New config dir was created to store guideline files. Each yaml file represents a guideline an contains its rules. The `Guidelines` class can parse the yamls. We can reach the guideline data via `getGuidelineRules` API endpoint that can return a list of `Rules`.
1 parent 2032d18 commit 547bd6f

File tree

28 files changed

+1584
-28
lines changed

28 files changed

+1584
-28
lines changed

analyzer/codechecker_analyzer/analyzer_context.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from codechecker_analyzer.arg import analyzer_binary
2020
from codechecker_common import logger
2121
from codechecker_common.checker_labels import CheckerLabels
22+
from codechecker_common.guidelines import Guidelines
2223
from codechecker_common.singleton import Singleton
2324
from codechecker_common.util import load_json
2425
from pathlib import Path
@@ -52,13 +53,17 @@ def __init__(self):
5253
if 'CC_TEST_LABELS_DIR' in os.environ:
5354
labels_dir = os.environ['CC_TEST_LABELS_DIR']
5455

56+
guidelines_dir = os.path.join(self._data_files_dir_path,
57+
'config', 'guidelines')
58+
5559
cfg_dict = self.__get_package_config()
5660
self.env_vars = cfg_dict['environment_variables']
5761

5862
lcfg_dict = self.__get_package_layout()
5963
self.pckg_layout = lcfg_dict['runtime']
6064

6165
self._checker_labels = CheckerLabels(labels_dir)
66+
self._guidelines = Guidelines(guidelines_dir)
6267
self.__package_version = None
6368
self.__package_build_date = None
6469
self.__package_git_hash = None
@@ -370,6 +375,10 @@ def checker_plugin(self):
370375
def checker_labels(self):
371376
return self._checker_labels
372377

378+
@property
379+
def guideline(self):
380+
return self._guidelines
381+
373382

374383
def get_context():
375384
try:
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# -------------------------------------------------------------------------
2+
#
3+
# Part of the CodeChecker project, under the Apache License v2.0 with
4+
# LLVM Exceptions. See LICENSE for license information.
5+
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
#
7+
# -------------------------------------------------------------------------
8+
9+
"""Tests for Guidelines class."""
10+
11+
12+
import yaml
13+
import os
14+
import tempfile
15+
import unittest
16+
17+
from codechecker_common.guidelines import Guidelines
18+
19+
20+
class TestGuidelines(unittest.TestCase):
21+
def setUp(self) -> None:
22+
self.guidelines_dir = tempfile.TemporaryDirectory()
23+
self.initialize_guidelines_dir()
24+
25+
def tearDown(self) -> None:
26+
self.guidelines_dir.cleanup()
27+
28+
def initialize_guidelines_dir(self):
29+
guidelines = {
30+
"guideline": "sei-cert",
31+
"guideline_title": "SEI CERT Coding Standard",
32+
"rules": [
33+
{
34+
"rule_id": "con50-cpp",
35+
"rule_url": "https://wiki.sei.cmu.edu/confluence/display"
36+
"/cplusplus/CON50-CPP.+Do+not+destroy+a+mutex"
37+
"+while+it+is+locked",
38+
"rule_title": ""
39+
},
40+
{
41+
"rule_id": "con51-cpp",
42+
"rule_url": "https://wiki.sei.cmu.edu/confluence/display"
43+
"/cplusplus/CON51-CPP.+Ensure+actively+held+"
44+
"locks+are+released+on+exceptional+conditions",
45+
"rule_title": ""
46+
},
47+
{
48+
"rule_id": "con52-cpp",
49+
"rule_url": "https://wiki.sei.cmu.edu/confluence/display"
50+
"/cplusplus/CON52-CPP.+Prevent+data+races+when"
51+
"+accessing+bit-fields+from+multiple+threads",
52+
"rule_title": ""
53+
},
54+
{
55+
"rule_id": "con53-cpp",
56+
"rule_url": "https://wiki.sei.cmu.edu/confluence/display"
57+
"/cplusplus/CON53-CPP.+Avoid+deadlock+by+"
58+
"locking+in+a+predefined+order",
59+
"rule_title": ""
60+
},
61+
]
62+
}
63+
64+
with open(os.path.join(self.guidelines_dir.name, 'sei-cert.yaml'),
65+
'w', encoding='utf-8') as fp:
66+
yaml.safe_dump(guidelines, fp, default_flow_style=False)
67+
68+
def test_guidelines(self):
69+
g = Guidelines(self.guidelines_dir.name)
70+
71+
self.assertNotEqual(len(g.rules_of_guideline("sei-cert")), 0)
72+
73+
self.assertEqual(
74+
sorted(g.rules_of_guideline("sei-cert").keys()),
75+
["con50-cpp", "con51-cpp", "con52-cpp", "con53-cpp"])
76+
77+
self.assertEqual(
78+
g.rules_of_guideline("sei-cert"),
79+
{
80+
"con50-cpp": {
81+
"rule_url": "https://wiki.sei.cmu.edu/confluence/display"
82+
"/cplusplus/CON50-CPP.+Do+not+destroy+a+mutex"
83+
"+while+it+is+locked",
84+
"rule_title": ""
85+
},
86+
"con51-cpp": {
87+
"rule_url": "https://wiki.sei.cmu.edu/confluence/display"
88+
"/cplusplus/CON51-CPP.+Ensure+actively+held+"
89+
"locks+are+released+on+exceptional+conditions",
90+
"rule_title": ""
91+
},
92+
"con52-cpp": {
93+
"rule_url": "https://wiki.sei.cmu.edu/confluence/display"
94+
"/cplusplus/CON52-CPP.+Prevent+data+races+when"
95+
"+accessing+bit-fields+from+multiple+threads",
96+
"rule_title": ""
97+
},
98+
"con53-cpp": {
99+
"rule_url": "https://wiki.sei.cmu.edu/confluence/display"
100+
"/cplusplus/CON53-CPP.+Avoid+deadlock+by+"
101+
"locking+in+a+predefined+order",
102+
"rule_title": ""
103+
},
104+
})

codechecker_common/guidelines.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
# -------------------------------------------------------------------------
2+
#
3+
# Part of the CodeChecker project, under the Apache License v2.0 with
4+
# LLVM Exceptions. See LICENSE for license information.
5+
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
#
7+
# -------------------------------------------------------------------------
8+
import os
9+
from typing import Any, DefaultDict, Dict, Iterable, List
10+
from collections import defaultdict
11+
12+
from codechecker_common.util import load_yaml
13+
from codechecker_common.logger import get_logger
14+
15+
LOG = get_logger('system')
16+
17+
18+
class Guidelines:
19+
def __init__(self, guidelines_dir: str):
20+
if not os.path.isdir(guidelines_dir):
21+
raise NotADirectoryError(
22+
f'{guidelines_dir} is not a directory.')
23+
24+
guideline_yaml_files = map(
25+
lambda f: os.path.join(guidelines_dir, f),
26+
os.listdir(guidelines_dir))
27+
28+
self.__all_rules = self.__union_guideline_files(guideline_yaml_files)
29+
30+
def __check_guideline_format(self, guideline_data: dict):
31+
"""
32+
Check the format of a guideline, It must contain specific values with
33+
specific types. In case of any format error a ValueError exception is
34+
thrown with the description of the wrong format.
35+
"""
36+
37+
if "guideline" not in guideline_data \
38+
or not isinstance(guideline_data["guideline"], str):
39+
raise ValueError(
40+
"The 'guideline' field must exist and be a string.")
41+
42+
if "guideline_title" not in guideline_data \
43+
or not isinstance(guideline_data["guideline_title"], str):
44+
raise ValueError(
45+
"The 'guideline_title' field must exist and be a string.")
46+
47+
rules = guideline_data.get("rules")
48+
if not isinstance(rules, list) \
49+
or not all(map(lambda r: isinstance(r, dict), rules)):
50+
raise ValueError(
51+
"The 'rules' field must exist and be a list of dictionaris.")
52+
53+
if any(map(lambda rule: "rule_id" not in rule
54+
or not isinstance(rule["rule_id"], str), rules)):
55+
raise ValueError(
56+
"All rules must have 'rule_id' that is a string.")
57+
58+
def __union_guideline_files(
59+
self,
60+
guideline_files: Iterable[str]
61+
) -> DefaultDict[str, Dict[str, Dict[str, str]]]:
62+
"""
63+
This function creates a union object of the given guideline files. The
64+
resulting object maps guidelines to the collection of their rules.
65+
E.g.:
66+
{
67+
"guideline1": {
68+
"rule_id1": {
69+
"rule_url": ...
70+
"title": ...
71+
},
72+
"rule_id2": {
73+
...
74+
}
75+
],
76+
"guideline2": {
77+
...
78+
},
79+
}
80+
"""
81+
all_rules: DefaultDict[
82+
str, Dict[str, Dict[str, str]]] = defaultdict(dict)
83+
84+
for guideline_file in guideline_files:
85+
guideline_data = load_yaml(guideline_file)
86+
87+
try:
88+
self.__check_guideline_format(guideline_data)
89+
90+
guideline_name = guideline_data["guideline"]
91+
rules = guideline_data["rules"]
92+
all_rules[guideline_name] = {rule.pop("rule_id"): rule
93+
for rule in rules}
94+
except ValueError as ex:
95+
LOG.warning("%s does not have a correct guideline format.",
96+
guideline_file)
97+
LOG.warning(ex)
98+
99+
return all_rules
100+
101+
def rules_of_guideline(
102+
self,
103+
guideline_name: str,
104+
) -> List[Any]:
105+
"""
106+
Return the list of rules of a guideline.
107+
"""
108+
109+
guideline_rules = self.__all_rules[guideline_name]
110+
111+
return guideline_rules
112+
113+
def all_guideline_rules(
114+
self
115+
) -> DefaultDict[str, Dict[str, Dict[str, str]]]:
116+
return self.__all_rules

codechecker_common/util.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"""
1111
import itertools
1212
import json
13+
import yaml
1314
import os
1415
from typing import TextIO
1516

@@ -89,6 +90,32 @@ def load_json(path: str, default=None, lock=False, display_warning=True):
8990
return ret
9091

9192

93+
def load_yaml(path: str):
94+
"""
95+
Load the contents of the given file as a YAML and return it's value.
96+
"""
97+
98+
try:
99+
with open(path, "r", encoding="utf-8") as f:
100+
return yaml.safe_load(f)
101+
except OSError as ex:
102+
LOG.warning("Failed to open YAML file: %s", path)
103+
LOG.warning(ex)
104+
return None
105+
except yaml.YAMLError as ex:
106+
LOG.warning("Failed to parse YAML file: %s", path)
107+
LOG.warning(ex)
108+
return None
109+
except ValueError as ex:
110+
LOG.warning("%s is not a valid YAML file.", path)
111+
LOG.warning(ex)
112+
return None
113+
except TypeError as ex:
114+
LOG.warning("Failed to process YAML file: %s", path)
115+
LOG.warning(ex)
116+
return None
117+
118+
92119
def get_linef(fp: TextIO, line_no: int) -> str:
93120
"""'fp' should be (readable) file object.
94121
Return the line content at line_no or an empty line

0 commit comments

Comments
 (0)