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