Skip to content

Commit 4a70782

Browse files
committed
scripts: ci: Add CI bindings style checker
Implement a check in the CI pipeline to enforce that property names in device tree bindings do not contain underscores. Signed-off-by: James Roy <rruuaanng@outlook.com> Signed-off-by: Martí Bolívar <marti.bolivar@nordicsemi.no>
1 parent b2f82f7 commit 4a70782

File tree

2 files changed

+97
-31
lines changed

2 files changed

+97
-31
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# This file is a YAML allowlist of property names that are allowed to
2+
# bypass the underscore check in bindings. These properties are exempt
3+
# from the rule that requires using '-' instead of '_'.
4+
#
5+
# The file content can be as shown below:
6+
# - propname1
7+
# - propname2
8+
# - ...
9+
10+
- mmc-hs200-1_8v
11+
- mmc-hs400-1_8v

scripts/ci/check_compliance.py

Lines changed: 86 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import shutil
2222
import textwrap
2323
import unidiff
24+
import yaml
2425

2526
from yamllint import config, linter
2627

@@ -35,6 +36,28 @@
3536
import list_boards
3637
import list_hardware
3738

39+
sys.path.insert(0, str(Path(__file__).resolve().parents[2]
40+
/ "scripts" / "dts" / "python-devicetree" / "src"))
41+
from devicetree import edtlib
42+
43+
44+
# Let the user run this script as ./scripts/ci/check_compliance.py without
45+
# making them set ZEPHYR_BASE.
46+
ZEPHYR_BASE = Path(os.environ.get('ZEPHYR_BASE'))
47+
if not ZEPHYR_BASE:
48+
ZEPHYR_BASE = Path(__file__).resolve().parents[2]
49+
# Propagate this decision to child processes.
50+
os.environ['ZEPHYR_BASE'] = str(ZEPHYR_BASE)
51+
52+
# Initialize the property names allowlist
53+
BINDINGS_PROPERTIES_AL = None
54+
with open(Path(__file__).parents[1] / 'bindings_properties_allowlist.yaml') as f:
55+
allowlist = yaml.safe_load(f.read())
56+
if allowlist is not None:
57+
BINDINGS_PROPERTIES_AL = set(allowlist)
58+
else:
59+
BINDINGS_PROPERTIES_AL = set()
60+
3861
logger = None
3962

4063
def git(*args, cwd=None, ignore_non_zero=False):
@@ -318,7 +341,7 @@ def run(self):
318341

319342
vendor_prefixes = {"others"}
320343
# add vendor prefixes from the main zephyr repo
321-
vendor_prefixes |= get_vendor_prefixes(ZEPHYR_BASE / "dts" / "bindings" / "vendor-prefixes.txt", self.error)
344+
vendor_prefixes |= get_vendor_prefixes(Path(ZEPHYR_BASE) , self.error)
322345

323346
# add vendor prefixes from the current repo
324347
dts_roots = get_module_setting_root('dts', path / "zephyr" / "module.yml")
@@ -380,32 +403,75 @@ class DevicetreeBindingsCheck(ComplianceTest):
380403
doc = "See https://docs.zephyrproject.org/latest/build/dts/bindings.html for more details."
381404

382405
def run(self, full=True):
383-
dts_bindings = self.parse_dt_bindings()
384-
385-
for dts_binding in dts_bindings:
386-
self.required_false_check(dts_binding)
406+
bindings_diff, bindings = self.get_yaml_bindings()
387407

388-
def parse_dt_bindings(self):
408+
# If no bindings are changed, skip this check.
409+
try:
410+
subprocess.check_call(['git', 'diff', '--quiet', COMMIT_RANGE]
411+
+ bindings_diff)
412+
nodiff = True
413+
except subprocess.CalledProcessError:
414+
nodiff = False
415+
if nodiff:
416+
self.skip('no changes to bindings were made')
417+
418+
for binding in bindings:
419+
self.check(binding, self.check_yaml_property_name)
420+
self.check(binding, self.required_false_check)
421+
422+
@staticmethod
423+
def check(binding, callback):
424+
while binding is not None:
425+
callback(binding)
426+
binding = binding.child_binding
427+
428+
def get_yaml_bindings(self):
389429
"""
390-
Returns a list of dts/bindings/**/*.yaml files
430+
Returns a list of 'dts/bindings/**/*.yaml'
391431
"""
432+
from glob import glob
433+
BINDINGS_PATH = 'dts/bindings/'
434+
bindings_diff_dir, bindings = set(), []
392435

393-
dt_bindings = []
394-
for file_name in get_files(filter="d"):
395-
if 'dts/bindings/' in file_name and file_name.endswith('.yaml'):
396-
dt_bindings.append(file_name)
436+
for file_name in get_files(filter='d'):
437+
if BINDINGS_PATH in file_name:
438+
p = file_name.partition(BINDINGS_PATH)
439+
bindings_diff_dir.add(os.path.join(p[0], p[1]))
397440

398-
return dt_bindings
441+
for path in bindings_diff_dir:
442+
yamls = glob(f'{os.fspath(path)}/**/*.yaml', recursive=True)
443+
bindings.extend(yamls)
399444

400-
def required_false_check(self, dts_binding):
401-
with open(dts_binding) as file:
402-
for line_number, line in enumerate(file, 1):
403-
if 'required: false' in line:
404-
self.fmtd_failure(
405-
'warning', 'Devicetree Bindings', dts_binding,
406-
line_number, col=None,
407-
desc="'required: false' is redundant, please remove")
445+
bindings = edtlib.bindings_from_paths(bindings, ignore_errors=True)
446+
return list(bindings_diff_dir), bindings
447+
448+
def check_yaml_property_name(self, binding):
449+
"""
450+
Checks if the property names in the binding file contain underscores.
451+
"""
452+
for prop_name in binding.prop2specs:
453+
if '_' in prop_name and prop_name not in BINDINGS_PROPERTIES_AL:
454+
better_prop = prop_name.replace('_', '-')
455+
print(f"Required: In '{binding.path}', "
456+
f"the property '{prop_name}' "
457+
f"should be renamed to '{better_prop}'.")
458+
self.failure(
459+
f"{binding.path}: property '{prop_name}' contains underscores.\n"
460+
f"\tUse '{better_prop}' instead unless this property name is from Linux.\n"
461+
"Or another authoritative upstream source of bindings for "
462+
f"compatible '{binding.compatible}'.\n"
463+
"\tHint: update 'bindings_properties_allowlist.yaml' if you need to "
464+
"override this check for this property."
465+
)
408466

467+
def required_false_check(self, binding):
468+
raw_props = binding.raw.get('properties', {})
469+
for prop_name, raw_prop in raw_props.items():
470+
if raw_prop.get('required') is False:
471+
self.failure(
472+
f'{binding.path}: property "{prop_name}": '
473+
"'required: false' is redundant, please remove"
474+
)
409475

410476
class KconfigCheck(ComplianceTest):
411477
"""
@@ -1990,17 +2056,6 @@ def _main(args):
19902056
# The "real" main(), which is wrapped to catch exceptions and report them
19912057
# to GitHub. Returns the number of test failures.
19922058

1993-
global ZEPHYR_BASE
1994-
ZEPHYR_BASE = os.environ.get('ZEPHYR_BASE')
1995-
if not ZEPHYR_BASE:
1996-
# Let the user run this script as ./scripts/ci/check_compliance.py without
1997-
# making them set ZEPHYR_BASE.
1998-
ZEPHYR_BASE = str(Path(__file__).resolve().parents[2])
1999-
2000-
# Propagate this decision to child processes.
2001-
os.environ['ZEPHYR_BASE'] = ZEPHYR_BASE
2002-
ZEPHYR_BASE = Path(ZEPHYR_BASE)
2003-
20042059
# The absolute path of the top-level git directory. Initialize it here so
20052060
# that issues running Git can be reported to GitHub.
20062061
global GIT_TOP

0 commit comments

Comments
 (0)