1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
|
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0-only
#
# Check DTS coding style on YAML binding examples and on
# .dts/.dtsi/.dtso source files. Enforces rules from
# Documentation/devicetree/bindings/dts-coding-style.rst.
#
# Two modes:
# --mode=relaxed (default)
# Only rules that produce zero warnings on the current tree.
# Suitable for dt_binding_check.
# --mode=strict
# All rules. Required for new submissions.
#
# Two input types (auto-detected by file extension):
# *.yaml -- DT binding; check each example block
# *.dts/*.dtsi/*.dtso -- DTS source; whole file is one block
#
# Rules are declared in a registry (see RULES below); each rule is
# tagged with the lowest mode that runs it. Promoting a rule from
# 'strict' to 'relaxed' is a one-line change.
import argparse
import re
import sys
from enum import Enum, auto
import ruamel.yaml
# ---------------------------------------------------------------------------
# Line classification
# ---------------------------------------------------------------------------
class LineType(Enum):
BLANK = auto()
COMMENT = auto() # // ... or /* ... */ on one line
COMMENT_START = auto() # /* without closing */
COMMENT_BODY = auto() # inside a multi-line comment
COMMENT_END = auto() # closing */
PREPROCESSOR = auto() # #include / #define / #ifdef / ...
NODE_OPEN = auto() # something { (with optional label/name/addr)
NODE_CLOSE = auto() # };
PROPERTY = auto() # name = value; or name;
CONTINUATION = auto() # continuation of a multi-line property
re_cpp_directive = re.compile(
r'^#\s*(include|define|undef|ifdef|ifndef|if|else|elif|endif|'
r'pragma|error|warning)\b')
# label: name@addr { -- label and addr optional; name can be "/"
# Per the DT spec a node name may start with a digit (e.g. 1wire@...).
# The address part is captured loosely (any non-space, non-brace run) so
# malformed addresses (e.g. memory@0x1000) still reach
# check_unit_address_format() instead of silently bypassing the check.
re_node_header = re.compile(
r'^(?:([a-zA-Z_][a-zA-Z0-9_]*):\s*)?'
r'([a-zA-Z0-9][a-zA-Z0-9,._+-]*|/)'
r'(?:@([^\s{]+))?'
r'\s*\{$')
re_ref_node = re.compile(
r'^&([a-zA-Z_][a-zA-Z0-9_]*)\s*\{$')
def is_preprocessor(stripped):
"""Tell C preprocessor directives apart from DTS '#'-prefixed props."""
return re_cpp_directive.match(stripped) is not None
class DtsLine:
__slots__ = ('lineno', 'raw', 'linetype', 'indent_str', 'stripped',
'prop_name', 'continuations',
'node_name', 'node_addr', 'label', 'ref_name', 'depth',
'closures')
def __init__(self, lineno, raw, linetype, indent_str, stripped):
self.lineno = lineno # 1-based within the block
self.raw = raw
self.linetype = linetype
self.indent_str = indent_str # leading whitespace as-is
self.stripped = stripped
self.prop_name = None
self.continuations = []
self.node_name = None
self.node_addr = None
self.label = None
self.ref_name = None
self.depth = 0 # filled in by classify_lines
self.closures = 1 # count of '}' on a NODE_CLOSE line
def _split_code(text):
"""Return (code, opens_block) for a leading-stripped line: the
code portion with // and /* */ comments removed (string literals
kept verbatim), and whether a /* */ block comment is left open.
The code portion is right-stripped so the endswith() checks in
classify_lines see code only, not a trailing comment or blanks."""
out = []
i = 0
n = len(text)
while i < n:
c = text[i]
if c == '"':
j = i + 1
while j < n:
if text[j] == '\\':
j += 2
continue
if text[j] == '"':
j += 1
break
j += 1
out.append(text[i:j])
i = j
continue
if c == '/' and i + 1 < n and text[i + 1] == '/':
break
if c == '/' and i + 1 < n and text[i + 1] == '*':
end = text.find('*/', i + 2)
if end < 0:
return (''.join(out).rstrip(), True)
i = end + 2
continue
out.append(c)
i += 1
return (''.join(out).rstrip(), False)
re_only_closures = re.compile(r'(?:\}\s*;?\s*)+$')
def classify_lines(text):
"""Return a list of DtsLine. Tracks { } depth and groups
continuation lines onto their leading PROPERTY line."""
out = []
in_block_comment = False
in_cpp_macro = False
prev_complete = True
depth = 0
# Split preserving the indent string verbatim
re_lead = re.compile(r'^([ \t]*)(.*)$')
for i, raw in enumerate(text.split('\n'), start=1):
m = re_lead.match(raw)
indent_str = m.group(1)
stripped = m.group(2)
# Continuation of a multi-line C preprocessor directive: the
# previous PREPROCESSOR line ended with a '\\' line splice, so
# this line is part of the same macro. Treat it as
# PREPROCESSOR until the splice chain ends (no trailing '\\'
# or a blank line).
if in_cpp_macro:
dl = DtsLine(i, raw, LineType.PREPROCESSOR,
indent_str, stripped)
dl.depth = depth
out.append(dl)
in_cpp_macro = (bool(stripped) and
stripped.rstrip().endswith('\\'))
continue
if not stripped:
dl = DtsLine(i, raw, LineType.BLANK, '', '')
dl.depth = depth
out.append(dl)
continue
if in_block_comment:
ltype = (LineType.COMMENT_END if '*/' in stripped
else LineType.COMMENT_BODY)
if ltype == LineType.COMMENT_END:
in_block_comment = False
dl = DtsLine(i, raw, ltype, indent_str, stripped)
dl.depth = depth
out.append(dl)
continue
if stripped.startswith('#') and is_preprocessor(stripped):
dl = DtsLine(i, raw, LineType.PREPROCESSOR,
indent_str, stripped)
dl.depth = depth
out.append(dl)
prev_complete = True
in_cpp_macro = stripped.rstrip().endswith('\\')
continue
# Strip comments first so all later structural checks see code
# only. An unclosed /* sets in_block_comment for the next line.
code, opens_block = _split_code(stripped)
if opens_block:
in_block_comment = True
# Pure-comment line: nothing left after stripping. Classify as
# COMMENT_START (carries to next line) or COMMENT, and skip the
# structural classification entirely.
if not code:
ltype = LineType.COMMENT_START if opens_block else LineType.COMMENT
dl = DtsLine(i, raw, ltype, indent_str, stripped)
dl.depth = depth
out.append(dl)
continue
if not prev_complete:
dl = DtsLine(i, raw, LineType.CONTINUATION, indent_str, code)
dl.depth = depth
out.append(dl)
prev_complete = (code.endswith(';') or
code.endswith('{') or
code.endswith('};'))
continue
# NODE_CLOSE: the canonical form is "}" or "};" alone. A line
# that is nothing but closures (e.g. "}; };") is still treated
# as NODE_CLOSE for depth tracking, but the multi-closure case
# is flagged separately by check_node_close_alone via
# dl.closures.
if re_only_closures.match(code):
closures = code.count('}')
depth = max(depth - closures, 0)
dl = DtsLine(i, raw, LineType.NODE_CLOSE, indent_str, code)
dl.depth = depth
dl.closures = closures
out.append(dl)
prev_complete = True
continue
if code.endswith('{'):
dl = DtsLine(i, raw, LineType.NODE_OPEN, indent_str, code)
parse_node_header(dl)
dl.depth = depth
out.append(dl)
depth += 1
prev_complete = True
continue
# Property (or first line of a multi-line property).
dl = DtsLine(i, raw, LineType.PROPERTY, indent_str, code)
parse_property_name(dl)
dl.depth = depth
out.append(dl)
prev_complete = code.endswith(';')
# Group continuation lines onto their leading PROPERTY.
last_prop = None
grouped = []
for dl in out:
if dl.linetype == LineType.CONTINUATION and last_prop is not None:
last_prop.continuations.append(dl)
continue
if dl.linetype == LineType.PROPERTY:
last_prop = dl
elif dl.linetype != LineType.BLANK and \
dl.linetype not in (LineType.COMMENT, LineType.COMMENT_BODY,
LineType.COMMENT_END,
LineType.COMMENT_START):
last_prop = None
grouped.append(dl)
return grouped
def parse_node_header(dl):
m = re_node_header.match(dl.stripped)
if m:
dl.label = m.group(1)
dl.node_name = m.group(2)
dl.node_addr = m.group(3)
return
m = re_ref_node.match(dl.stripped)
if m:
dl.ref_name = m.group(1)
def parse_property_name(dl):
m = re.match(r'^([a-zA-Z0-9#][a-zA-Z0-9,._+#-]*)\s*[=;]', dl.stripped)
if m:
dl.prop_name = m.group(1)
def collect_labels_and_refs(text):
"""Return (defined_labels, referenced_labels) found anywhere outside
/* */ comments and string literals. Labels named fake_intc* (injected
by dt-extract-example) are skipped."""
# Strip block comments first so labels inside them don't count
stripped = re.sub(r'/\*.*?\*/', '', text, flags=re.DOTALL)
# Strip line comments
stripped = re.sub(r'//[^\n]*', '', stripped)
# Strip string literals so words inside quotes (e.g. "Error: foo")
# are not picked up as label definitions or &-references.
stripped = re.sub(r'"(?:[^"\\]|\\.)*"', '""', stripped)
defined = set()
referenced = set()
# A label precedes a node header; the next non-space token may start
# with a letter (foo, &ref), a digit (1wire), or '/' (root node).
for m in re.finditer(
r'(?:^|[\s{])([a-zA-Z_][a-zA-Z0-9_]*):\s*[a-zA-Z0-9/&]',
stripped):
name = m.group(1)
if not name.startswith('fake_intc'):
defined.add(name)
for m in re.finditer(r'&([a-zA-Z_][a-zA-Z0-9_]*)', stripped):
referenced.add(m.group(1))
return defined, referenced
# ---------------------------------------------------------------------------
# Rule registry
# ---------------------------------------------------------------------------
class Ctx:
"""Context passed to each rule check. Carries the parsed lines,
raw text, mode, and indent kind."""
def __init__(self, lines, text, mode, indent_kind):
self.lines = lines
self.text = text
self.mode = mode # 'relaxed' or 'strict'
self.indent_kind = indent_kind # 'spaces' or 'tab'
class Rule:
__slots__ = ('name', 'mode', 'description', 'check', 'applies_to')
def __init__(self, name, mode, description, check,
applies_to=('yaml', 'dts', 'dtsi', 'dtso')):
self.name = name
self.mode = mode # 'relaxed' or 'strict'
self.description = description
self.check = check
self.applies_to = applies_to # input types this rule covers
# --- individual rule check functions --------------------------------------
def check_trailing_whitespace(ctx):
for dl in ctx.lines:
if dl.raw != dl.raw.rstrip():
yield (dl.lineno, 'trailing whitespace')
def check_tab_in_dts(ctx):
"""Reject literal tabs in DTS lines when input is YAML.
For YAML examples, indent and content must use spaces. Tabs inside
a #define value are tolerated (those are CPP macros, not DTS).
For .dts files, this rule does not apply -- tabs are required.
"""
if ctx.indent_kind != 'spaces':
return
for dl in ctx.lines:
if dl.linetype == LineType.PREPROCESSOR:
continue
if dl.linetype == LineType.BLANK:
continue
if '\t' in dl.raw:
yield (dl.lineno, 'tab character not allowed in DTS example')
def check_mixed_indent_chars(ctx):
"""Indent must be all-spaces or all-tabs, never mixed on one line."""
for dl in ctx.lines:
if not dl.indent_str:
continue
if dl.linetype == LineType.PREPROCESSOR:
continue
if ' ' in dl.indent_str and '\t' in dl.indent_str:
yield (dl.lineno, 'mixed tabs and spaces in indent')
def detect_indent_unit(ctx):
"""Find the indent unit used at depth 1 in this block.
Returns one of: ' ' (2 spaces), ' ' (4 spaces), '\\t' (tab),
or None if depth-1 is empty or ambiguous."""
for dl in ctx.lines:
if dl.depth != 1:
continue
if dl.linetype in (LineType.BLANK, LineType.PREPROCESSOR):
continue
if dl.linetype in (LineType.COMMENT_BODY, LineType.COMMENT_END):
continue
if not dl.indent_str:
continue
if dl.indent_str == '\t':
return '\t'
if dl.indent_str == ' ':
return ' '
if dl.indent_str == ' ':
return ' '
# Anything else at depth 1 is non-canonical; flag elsewhere.
return dl.indent_str
return None
def check_indent_unit_relaxed(ctx):
"""YAML examples: 2 or 4 spaces. Never tabs or other widths."""
unit = detect_indent_unit(ctx)
if unit is None:
return
if unit not in (' ', ' '):
yield (1, 'indent unit must be 2 or 4 spaces, got %r' % unit)
def check_indent_unit_dts(ctx):
"""DTS files: 1 tab per level. Always required."""
unit = detect_indent_unit(ctx)
if unit is None:
return
if unit != '\t':
yield (1, 'indent unit must be 1 tab in DTS, got %r' % unit)
def check_indent_unit_strict(ctx):
"""YAML: must be exactly 4 spaces. DTS: 1 tab (same as relaxed)."""
unit = detect_indent_unit(ctx)
if unit is None:
return
if ctx.indent_kind == 'spaces':
if unit != ' ':
yield (1, 'indent unit must be 4 spaces in strict mode, '
'got %r' % unit)
def check_indent_consistent(ctx):
"""All indented lines must be a multiple of the detected unit."""
unit = detect_indent_unit(ctx)
if unit is None:
return
if ctx.indent_kind == 'spaces':
if unit not in (' ', ' '):
return # let check_indent_unit_* report this
else:
if unit != '\t':
return
for dl in ctx.lines:
if dl.linetype in (LineType.BLANK, LineType.PREPROCESSOR):
continue
if dl.linetype == LineType.CONTINUATION:
continue # continuations align to <, not to indent unit
if dl.linetype in (LineType.COMMENT_BODY, LineType.COMMENT_END):
continue
if not dl.indent_str:
continue
# The indent must be 'unit' repeated dl.depth times, exactly.
# NODE_CLOSE lines have depth equal to the post-decrement value,
# which matches the indent expected.
expected = unit * dl.depth
if dl.indent_str != expected:
yield (dl.lineno,
'indent mismatch (expected depth %d * %r)' %
(dl.depth, unit))
def check_blank_lines(ctx):
"""No two consecutive blank lines, no leading/trailing blank lines
in any node body."""
lines = ctx.lines
# Consecutive blanks
for i in range(1, len(lines)):
if lines[i].linetype == LineType.BLANK and \
lines[i - 1].linetype == LineType.BLANK:
yield (lines[i].lineno, 'consecutive blank lines')
# Blank right after { or right before }
for i, dl in enumerate(lines):
if dl.linetype != LineType.BLANK:
continue
prev = lines[i - 1] if i > 0 else None
nxt = lines[i + 1] if i + 1 < len(lines) else None
if prev is not None and prev.linetype == LineType.NODE_OPEN:
yield (dl.lineno, 'blank line at start of node body')
if nxt is not None and nxt.linetype == LineType.NODE_CLOSE:
yield (dl.lineno, 'blank line at end of node body')
def _walk_bodies(lines):
"""Yield lists of immediate-child NODE_OPEN lines for each node body
in the input. Skips ref-nodes (&label) since those don't have an
intrinsic ordering."""
body_stack = [[]]
for dl in lines:
if dl.linetype == LineType.NODE_OPEN:
body_stack[-1].append(dl)
body_stack.append([])
continue
if dl.linetype == LineType.NODE_CLOSE:
if len(body_stack) <= 1:
# Unbalanced; ignore to avoid crashing on malformed input
continue
yield body_stack.pop()
continue
while body_stack:
yield body_stack.pop()
def _natural_sort_key(s):
"""Split a string into a tuple of (kind, value) pairs that compares
numeric runs as ints, so 'foo10' sorts after 'foo2'."""
parts = []
for part in re.split(r'(\d+)', s):
if part.isdigit():
parts.append((0, int(part)))
else:
parts.append((1, part))
return tuple(parts)
def check_child_address_order(ctx):
"""Addressed siblings (foo@N) must appear in ascending address
order within their parent node body."""
for children in _walk_bodies(ctx.lines):
addressed = []
for c in children:
if c.node_addr is None:
continue
try:
parts = tuple(int(p, 16) for p in c.node_addr.split(','))
except ValueError:
continue
addressed.append((parts, c))
for i in range(1, len(addressed)):
if addressed[i][0] < addressed[i - 1][0]:
dl = addressed[i][1]
yield (dl.lineno,
'child node @%s out of address order' %
dl.node_addr)
def check_child_name_order(ctx):
"""Unaddressed siblings must appear in natural-sort order by node
name within their parent node body. Addressed children are scoped
by check_child_address_order; reference nodes (&label { ... }) and
the root node are skipped."""
for children in _walk_bodies(ctx.lines):
unaddressed = []
for c in children:
if c.node_addr is not None:
continue
if c.node_name in (None, '/'):
continue
if c.ref_name is not None:
continue
unaddressed.append((_natural_sort_key(c.node_name), c))
for i in range(1, len(unaddressed)):
if unaddressed[i][0] < unaddressed[i - 1][0]:
dl = unaddressed[i][1]
yield (dl.lineno,
'child node %r out of name order' % dl.node_name)
def _property_bucket(name):
"""Return the canonical bucket index for a property:
0 compatible
1 reg / reg-names
2 ranges
3 standard properties (no vendor comma in #-stripped name)
4 vendor-specific properties
5 status
Plus a sub-key inside the bucket for fixed slots (compatible, reg,
reg-names, ranges, status). 'standard' and 'vendor' return None for
the sub-key, signalling that the within-bucket key is computed by
the pairing rules."""
stripped = name.lstrip('#')
if name == 'compatible':
return (0, 0)
if name == 'reg':
return (1, 0)
if name == 'reg-names':
return (1, 1)
if name == 'ranges':
return (2, 0)
if name == 'status':
return (5, 0)
return (4 if ',' in stripped else 3, None)
# Declarative pairing rules: each is a callable
# (name, all_names) -> anchor_name_or_None
# If a rule returns an anchor, the property sorts immediately after the
# anchor. Rules are tried in order; the first match wins. If none
# matches, the within-bucket key falls back to natural sort by the
# #-stripped name.
def _pair_pinctrl_names(name, all_names):
"""pinctrl-names follows the highest pinctrl-N in the same node."""
if name != 'pinctrl-names':
return None
cands = [n for n in all_names if re.match(r'^pinctrl-\d+$', n)]
if not cands:
return None
return max(cands, key=_natural_sort_key)
def _pair_x_names(name, all_names):
"""Generic <x>-names follows its owning property. The owner is
usually plural (clocks/clock-names, dmas/dma-names,
resets/reset-names) but occasionally singular (reg/reg-names is
handled by the fixed slot above; this rule catches anything else)."""
if not name.endswith('-names'):
return None
base = name[:-len('-names')]
# Try plural and singular forms.
if (base + 's') in all_names:
return base + 's'
if base in all_names:
return base
return None
PAIRING_RULES = (_pair_pinctrl_names, _pair_x_names)
def _property_sort_key(name, all_names):
"""Sort key for a property among its node-body siblings.
Format: (bucket, within_key, tiebreak). 'within_key' for
standard/vendor buckets follows pairing rules: a property paired
with anchor X sorts as if it were X with a higher tiebreak."""
bucket, fixed_sub = _property_bucket(name)
if fixed_sub is not None:
return (bucket, (), fixed_sub)
for rule in PAIRING_RULES:
anchor = rule(name, all_names)
if anchor is not None:
return (bucket, _natural_sort_key(anchor.lstrip('#')), 1)
return (bucket, _natural_sort_key(name.lstrip('#')), 0)
def check_property_order(ctx):
"""Properties within a node body must appear in canonical order:
compatible, reg(/reg-names), ranges, then the standard group, then
the vendor-specific group, then status. Inside the standard and
vendor groups, pairing rules apply (e.g. <x>-names follows <x>);
everything else falls back to natural sort by the #-stripped name."""
lines = ctx.lines
for i, dl in enumerate(lines):
if dl.linetype != LineType.NODE_OPEN:
continue
body_depth = dl.depth + 1
props = []
for j in range(i + 1, len(lines)):
d = lines[j]
if d.linetype == LineType.NODE_CLOSE and \
d.depth == body_depth - 1:
break
if d.linetype == LineType.PROPERTY and d.depth == body_depth \
and d.prop_name is not None:
props.append(d)
if len(props) < 2:
continue
all_names = [p.prop_name for p in props]
keyed = [(p, _property_sort_key(p.prop_name, all_names))
for p in props]
for k in range(1, len(keyed)):
if keyed[k][1] < keyed[k - 1][1]:
p = keyed[k][0]
prev = keyed[k - 1][0]
yield (p.lineno,
'property %r out of canonical order '
'(should sort before %r)' %
(p.prop_name, prev.prop_name))
def _strip_strings_and_comments(text):
"""Remove string literals and /* */ + // comments from a single
line, replacing them with empty strings. Used so syntactic checks
(whitespace, hex case, etc.) don't false-positive on contents of
quoted strings or comments. An unclosed /* on the line is treated
as a comment running to end of line."""
text = re.sub(r'"(?:[^"\\]|\\.)*"', '""', text)
text = re.sub(r'/\*.*?\*/', '', text)
text = re.sub(r'/\*.*$', '', text)
text = re.sub(r'//.*$', '', text)
return text
def check_required_blank_lines(ctx):
"""A blank line must precede each child node and the 'status'
property within a node body, except when these are the first
substantive item in the body."""
lines = ctx.lines
for i, open_dl in enumerate(lines):
if open_dl.linetype != LineType.NODE_OPEN:
continue
body_depth = open_dl.depth + 1
prev_substantive = None
between_blanks = 0
depth_inside = 0
for j in range(i + 1, len(lines)):
d = lines[j]
if d.linetype == LineType.NODE_CLOSE and \
d.depth == body_depth - 1 and depth_inside == 0:
break
# Track depth inside nested children so we only look at
# immediate-body items.
if d.linetype == LineType.NODE_OPEN and \
d.depth >= body_depth and depth_inside > 0:
depth_inside += 1
continue
if d.linetype == LineType.NODE_CLOSE and depth_inside > 0:
depth_inside -= 1
continue
if depth_inside > 0:
continue
if d.linetype == LineType.BLANK:
if prev_substantive is not None:
between_blanks += 1
continue
if d.linetype in (LineType.COMMENT, LineType.COMMENT_START,
LineType.COMMENT_BODY, LineType.COMMENT_END,
LineType.PREPROCESSOR):
continue
if d.linetype == LineType.CONTINUATION:
continue
needs_blank = False
if d.linetype == LineType.NODE_OPEN:
needs_blank = True
depth_inside = 1 # entered the child body
elif d.linetype == LineType.PROPERTY and d.prop_name == 'status':
needs_blank = True
if needs_blank and prev_substantive is not None and \
between_blanks == 0:
if d.linetype == LineType.NODE_OPEN:
yield (d.lineno,
'child node must be preceded by a blank line')
else:
yield (d.lineno,
'"status" must be preceded by a blank line')
prev_substantive = d
between_blanks = 0
def check_hex_case(ctx):
"""Hex literals (0xN) must use lowercase digits and prefix."""
for dl in ctx.lines:
if dl.linetype in (LineType.BLANK, LineType.COMMENT,
LineType.COMMENT_START, LineType.COMMENT_BODY,
LineType.COMMENT_END, LineType.PREPROCESSOR):
continue
text = _strip_strings_and_comments(dl.raw)
for m in re.finditer(r'\b0[xX][0-9a-fA-F]+\b', text):
lit = m.group(0)
if any(c.isupper() for c in lit[2:]) or lit[1] == 'X':
yield (dl.lineno,
'hex literal %r must be lowercase' % lit)
def check_unit_address_format(ctx):
"""Unit addresses must be lowercase hex without leading zeros and
without a '0x' prefix. For multi-cell addresses (comma-separated),
each part is checked independently. A single '0' is permitted
(canonical zero)."""
for dl in ctx.lines:
if dl.linetype != LineType.NODE_OPEN:
continue
if dl.node_addr is None:
continue
addr = dl.node_addr
for part in addr.split(','):
if part[:2] in ('0x', '0X'):
yield (dl.lineno,
'unit address %r must not have a "0x" prefix' %
addr)
break
if not re.match(r'^[0-9a-fA-F]+$', part):
yield (dl.lineno,
'unit address %r is not valid hex' % addr)
break
if any(c in 'ABCDEF' for c in part):
yield (dl.lineno,
'unit address %r must be lowercase hex' % addr)
break
if len(part) > 1 and part.startswith('0'):
yield (dl.lineno,
'unit address %r has leading zeros' % addr)
break
def check_value_whitespace(ctx):
"""A <...> cell list must have no whitespace directly after '<'
or directly before '>'. Continuation lines are joined onto the
property so a <...> split across lines is checked too; a '<' or
'>' at a line break is glued straight to the neighbouring value,
so the break itself is not counted as padding. Outside strings
and comments only."""
for dl in ctx.lines:
if dl.linetype != LineType.PROPERTY:
continue
segs = [_strip_strings_and_comments(dl.raw).strip()]
for cont in dl.continuations:
segs.append(_strip_strings_and_comments(cont.stripped).strip())
text = ''
for s in segs:
if not s:
continue
if not text or text.endswith('<') or s.startswith('>'):
text += s
else:
text += ' ' + s
for m in re.finditer(r'<([^<>]*)>', text):
content = m.group(1)
if content and content != content.strip():
yield (dl.lineno, 'extra whitespace inside <...>')
break
def check_node_close_alone(ctx):
"""The closing '};' of a node must be on its own line. The
classifier accepts a canonical "}" or "};" as NODE_CLOSE; a line
that is all closures (e.g. "}; };") is still NODE_CLOSE for depth
tracking but is flagged here via dl.closures. Any other line that
still contains '};' (in code, not in strings or comments) is
mixing a node close with something else."""
for dl in ctx.lines:
if dl.linetype == LineType.NODE_CLOSE:
if dl.closures > 1:
yield (dl.lineno,
'closing brace must be on its own line')
continue
if dl.linetype in (LineType.BLANK, LineType.COMMENT,
LineType.COMMENT_START, LineType.COMMENT_BODY,
LineType.COMMENT_END, LineType.PREPROCESSOR):
continue
text = _strip_strings_and_comments(dl.raw)
if '};' in text:
yield (dl.lineno,
'closing brace must be on its own line')
def _display_col(text):
"""Visual column width of text, with tabs expanded to the next
8-column stop, matching how printf and most editors render a
line and the kernel-wide line length convention."""
col = 0
for ch in text:
if ch == '\t':
col = (col // 8 + 1) * 8
else:
col += 1
return col
def check_line_length(ctx):
"""Lines must not exceed 80 columns; tabs count as 8 (see
_display_col)."""
for dl in ctx.lines:
if dl.linetype == LineType.BLANK:
continue
cols = _display_col(dl.raw)
if cols > 80:
yield (dl.lineno,
'line exceeds 80 columns (%d)' % cols)
def check_continuation_alignment(ctx):
"""A multi-line property's continuation lines must align their
first non-whitespace character to the display column of the first
'<' or '"' after the '=' in the leading line. Display columns are
used so tab-indented .dts files (where a continuation aligns with
tabs plus spaces) are compared correctly."""
for dl in ctx.lines:
if dl.linetype != LineType.PROPERTY:
continue
if not dl.continuations:
continue
eq = dl.raw.find('=')
if eq < 0:
continue
# First '<' or '"' after '='
rest = dl.raw[eq + 1:]
m = re.search(r'[<"]', rest)
if not m:
continue
target_col = _display_col(dl.raw[:eq + 1 + m.start()])
for cont in dl.continuations:
if _display_col(cont.indent_str) != target_col:
yield (cont.lineno,
'continuation should align to column %d '
'(under "<" or \\")' % (target_col + 1))
def check_unclosed_block_comment(ctx):
"""Every /* must have a matching */ in the same block. Catches both
a comment opened on its own line (COMMENT_START) and a tail comment
opened on a PROPERTY or other code line (where in_block_comment is
set by _split_code so the next line becomes COMMENT_BODY without a
preceding COMMENT_START)."""
open_lineno = None
for dl in ctx.lines:
if dl.linetype == LineType.COMMENT_START:
open_lineno = dl.lineno
elif dl.linetype == LineType.COMMENT_END:
open_lineno = None
elif dl.linetype == LineType.COMMENT_BODY and open_lineno is None:
# Block was opened by a /* tail on a code line; report at
# the first orphan body line since the originating line is
# already classified as something else.
open_lineno = dl.lineno
if open_lineno is not None:
yield (open_lineno, 'unclosed /* block comment')
def check_unused_labels(ctx):
"""Labels defined but never referenced are clutter."""
defined, referenced = collect_labels_and_refs(ctx.text)
for label in sorted(defined - referenced):
# Find the line where this label is defined for line-number
# reporting.
m = re.search(r'(?m)^.*\b' + re.escape(label) + r'\s*:', ctx.text)
lineno = ctx.text[:m.start()].count('\n') + 1 if m else 1
yield (lineno, 'label %r defined but never &-referenced' % label)
# --- registry --------------------------------------------------------------
RULES = [
# 'relaxed' is the default; rules in this group must produce zero
# output on a clean kernel tree (post the small prep-cleanup
# commit at the head of this series).
Rule('trailing-whitespace', 'relaxed',
'no trailing whitespace on any line',
check_trailing_whitespace),
Rule('tab-in-dts', 'relaxed',
'YAML examples may not contain tab characters',
check_tab_in_dts, applies_to=('yaml',)),
Rule('mixed-indent-chars', 'relaxed',
'indent must not mix tabs and spaces',
check_mixed_indent_chars),
Rule('unclosed-block-comment', 'relaxed',
'every /* block comment must close with */',
check_unclosed_block_comment),
# DTS files always use tabs; this is not negotiable per kernel
# coding style (.dts files are real source). Relaxed mode.
Rule('indent-unit-dts', 'relaxed',
'DTS files: 1 tab per nesting level',
check_indent_unit_dts,
applies_to=('dts', 'dtsi', 'dtso')),
# 'strict' rules are opt-in (e.g. for new submissions via
# checkpatch.pl in a follow-up series). They flag many existing
# files and can be promoted to relaxed once those are cleaned up.
Rule('indent-unit', 'strict',
'YAML: 2 or 4 spaces per level',
check_indent_unit_relaxed, applies_to=('yaml',)),
Rule('indent-unit-strict', 'strict',
'YAML: must be 4 spaces per level',
check_indent_unit_strict, applies_to=('yaml',)),
Rule('indent-consistent', 'strict',
'every line indented at depth * unit',
check_indent_consistent),
Rule('blank-lines', 'strict',
'no consecutive blanks; no blanks at node body edges',
check_blank_lines),
Rule('child-address-order', 'strict',
'addressed siblings must be in ascending address order',
check_child_address_order),
Rule('child-name-order', 'strict',
'unaddressed siblings must be in natural-sort name order',
check_child_name_order),
Rule('property-order', 'strict',
'canonical bucket + pairing + natural-sort order of properties',
check_property_order),
Rule('required-blank-lines', 'strict',
'blank line before child nodes and before "status"',
check_required_blank_lines),
Rule('hex-case', 'strict',
'hex literals must be lowercase',
check_hex_case),
Rule('unit-address-format', 'strict',
'unit addresses must be lowercase hex without leading zeros',
check_unit_address_format),
Rule('value-whitespace', 'strict',
'no whitespace directly inside <...> brackets',
check_value_whitespace),
Rule('node-close-alone', 'strict',
'closing brace must be on its own line',
check_node_close_alone),
Rule('line-length', 'strict',
'lines must not exceed 80 columns',
check_line_length),
Rule('continuation-alignment', 'strict',
'multi-line property continuations align under "<" or "\\""',
check_continuation_alignment),
Rule('unused-labels', 'strict',
'every label must be &-referenced in the same example/file '
'(skipped for .dtsi/.dtso since labels there are exported)',
check_unused_labels, applies_to=('yaml', 'dts')),
]
def select_rules(mode, input_kind):
"""Return rules that apply to the given mode and input type."""
rank = {'relaxed': 0, 'strict': 1}
out = []
for r in RULES:
if rank[r.mode] > rank[mode]:
continue
if input_kind not in r.applies_to:
continue
out.append(r)
return out
# ---------------------------------------------------------------------------
# Block runner
# ---------------------------------------------------------------------------
def check_block(text, mode, indent_kind, input_type):
"""Run all selected rules on a single block of DTS text. Returns a
list of (lineno, rule_name, message) tuples."""
lines = classify_lines(text)
ctx = Ctx(lines, text, mode, indent_kind)
rules = select_rules(mode, input_type)
findings = []
for r in rules:
for lineno, msg in r.check(ctx):
findings.append((lineno, r.name, msg))
findings.sort(key=lambda t: (t[0], t[1]))
return findings
# ---------------------------------------------------------------------------
# Input drivers (YAML examples vs raw DTS)
# ---------------------------------------------------------------------------
def _yaml_loader():
return ruamel.yaml.YAML()
def iter_yaml_examples(filepath):
"""Yield (example_text, base_lineno_in_file, example_index) tuples."""
yaml = _yaml_loader()
try:
with open(filepath, encoding='utf-8') as f:
data = yaml.load(f)
except Exception as e:
print('%s: error loading YAML: %s' % (filepath, e),
file=sys.stderr)
return
if not isinstance(data, dict) or 'examples' not in data:
return
examples = data['examples']
if not hasattr(examples, '__iter__'):
return
for i, ex in enumerate(examples):
if not isinstance(ex, str):
continue
try:
base = examples.lc.item(i)[0] + 2
except Exception:
base = 1
yield (str(ex), base, i)
def iter_dts_file(filepath):
"""Treat the whole file as a single block."""
try:
with open(filepath, encoding='utf-8') as f:
text = f.read()
except Exception as e:
print('%s: error reading: %s' % (filepath, e), file=sys.stderr)
return
yield (text, 1, None)
# ---------------------------------------------------------------------------
# Top-level processing
# ---------------------------------------------------------------------------
def input_kind(filepath):
p = filepath.lower()
if p.endswith('.yaml') or p.endswith('.yml'):
return 'yaml'
if p.endswith('.dts'):
return 'dts'
if p.endswith('.dtsi'):
return 'dtsi'
if p.endswith('.dtso'):
return 'dtso'
return None
# All input types that use tab indentation and follow DTS coding style.
DTS_FAMILY = ('dts', 'dtsi', 'dtso')
def collect_findings(filepath, mode):
"""Return a (lines, count) pair for filepath. lines is a list of
formatted output strings; count is the number of findings."""
kind = input_kind(filepath)
if kind == 'yaml':
indent_kind = 'spaces'
iterator = iter_yaml_examples(filepath)
elif kind in DTS_FAMILY:
indent_kind = 'tab'
iterator = iter_dts_file(filepath)
else:
return (['%s: unknown file type, skipping' % filepath], 0)
out = []
for text, base, idx in iterator:
for lineno, rule, msg in check_block(text, mode, indent_kind, kind):
abs_line = base + lineno - 1
ex_tag = '' if idx is None else ' example %d' % idx
out.append('%s:%d:%s [%s] %s' %
(filepath, abs_line, ex_tag, rule, msg))
return (out, len(out))
# Worker entry point for ProcessPoolExecutor.map(). Top-level so it is
# picklable on every platform.
def _worker(args):
filepath, mode = args
return collect_findings(filepath, mode)
def main():
import os
ap = argparse.ArgumentParser(
description='Check DTS coding style on YAML examples and '
'.dts/.dtsi/.dtso files.',
fromfile_prefix_chars='@')
ap.add_argument('--mode', choices=('relaxed', 'strict'),
default='relaxed',
help='which rule set to apply (default: relaxed)')
ap.add_argument('-j', '--jobs', type=int, default=0,
metavar='N',
help='run N workers in parallel (default: respect '
'the make jobserver via $PARALLELISM, otherwise '
'os.cpu_count(); use 1 to disable multiprocessing)')
ap.add_argument('--list-rules', action='store_true',
help='print all rules with their mode and exit')
ap.add_argument('files', nargs='*', metavar='file',
help='YAML binding files or .dts/.dtsi/.dtso files; '
'use @argfile to read paths from a file')
args = ap.parse_args()
if args.list_rules:
for r in RULES:
applies = ','.join(r.applies_to)
print('%-22s %-7s [%s] %s' %
(r.name, r.mode, applies, r.description))
return 0
if not args.files:
ap.error('no input files')
if args.jobs > 0:
jobs = args.jobs
else:
# When invoked under scripts/jobserver-exec, $PARALLELISM
# holds the slot count make has reserved for us; this lets
# `make -j N dt_binding_check` constrain our worker pool to N.
try:
jobs = int(os.environ['PARALLELISM'])
except (KeyError, ValueError):
jobs = os.cpu_count() or 1
# Single-process path: keep import surface small for tests and
# easy debugging.
if jobs == 1 or len(args.files) == 1:
total = 0
for f in args.files:
lines, n = collect_findings(f, args.mode)
for line in lines:
print(line, file=sys.stderr)
total += n
return 1 if total else 0
# Multi-process path. ex.map preserves input order so output is
# deterministic across runs.
from concurrent.futures import ProcessPoolExecutor
total = 0
work = [(f, args.mode) for f in args.files]
chunk = max(1, len(work) // (jobs * 8)) if work else 1
with ProcessPoolExecutor(max_workers=jobs) as ex:
for lines, n in ex.map(_worker, work, chunksize=chunk):
for line in lines:
print(line, file=sys.stderr)
total += n
return 1 if total else 0
if __name__ == '__main__':
sys.exit(main())
|