xref: /linux/scripts/dtc/dt-check-style (revision c6cf4441a3a05bb7273ed022f3e56c4fc591da08)
11e8a9af9SDaniel Golle#!/usr/bin/env python3
21e8a9af9SDaniel Golle# SPDX-License-Identifier: GPL-2.0-only
31e8a9af9SDaniel Golle#
41e8a9af9SDaniel Golle# Check DTS coding style on YAML binding examples and on
51e8a9af9SDaniel Golle# .dts/.dtsi/.dtso source files. Enforces rules from
61e8a9af9SDaniel Golle# Documentation/devicetree/bindings/dts-coding-style.rst.
71e8a9af9SDaniel Golle#
81e8a9af9SDaniel Golle# Two modes:
91e8a9af9SDaniel Golle#   --mode=relaxed (default)
101e8a9af9SDaniel Golle#     Only rules that produce zero warnings on the current tree.
111e8a9af9SDaniel Golle#     Suitable for dt_binding_check.
121e8a9af9SDaniel Golle#   --mode=strict
131e8a9af9SDaniel Golle#     All rules. Required for new submissions.
141e8a9af9SDaniel Golle#
151e8a9af9SDaniel Golle# Two input types (auto-detected by file extension):
161e8a9af9SDaniel Golle#   *.yaml             -- DT binding; check each example block
171e8a9af9SDaniel Golle#   *.dts/*.dtsi/*.dtso -- DTS source; whole file is one block
181e8a9af9SDaniel Golle#
191e8a9af9SDaniel Golle# Rules are declared in a registry (see RULES below); each rule is
201e8a9af9SDaniel Golle# tagged with the lowest mode that runs it. Promoting a rule from
211e8a9af9SDaniel Golle# 'strict' to 'relaxed' is a one-line change.
221e8a9af9SDaniel Golle
231e8a9af9SDaniel Golleimport argparse
241e8a9af9SDaniel Golleimport re
251e8a9af9SDaniel Golleimport sys
261e8a9af9SDaniel Gollefrom enum import Enum, auto
271e8a9af9SDaniel Golle
281e8a9af9SDaniel Golleimport ruamel.yaml
291e8a9af9SDaniel Golle
301e8a9af9SDaniel Golle
311e8a9af9SDaniel Golle# ---------------------------------------------------------------------------
321e8a9af9SDaniel Golle# Line classification
331e8a9af9SDaniel Golle# ---------------------------------------------------------------------------
341e8a9af9SDaniel Golle
351e8a9af9SDaniel Golleclass LineType(Enum):
361e8a9af9SDaniel Golle    BLANK = auto()
371e8a9af9SDaniel Golle    COMMENT = auto()         # // ... or /* ... */ on one line
381e8a9af9SDaniel Golle    COMMENT_START = auto()   # /* without closing */
391e8a9af9SDaniel Golle    COMMENT_BODY = auto()    # inside a multi-line comment
401e8a9af9SDaniel Golle    COMMENT_END = auto()     # closing */
411e8a9af9SDaniel Golle    PREPROCESSOR = auto()    # #include / #define / #ifdef / ...
421e8a9af9SDaniel Golle    NODE_OPEN = auto()       # something { (with optional label/name/addr)
431e8a9af9SDaniel Golle    NODE_CLOSE = auto()      # };
441e8a9af9SDaniel Golle    PROPERTY = auto()        # name = value; or name;
451e8a9af9SDaniel Golle    CONTINUATION = auto()    # continuation of a multi-line property
461e8a9af9SDaniel Golle
471e8a9af9SDaniel Golle
481e8a9af9SDaniel Gollere_cpp_directive = re.compile(
491e8a9af9SDaniel Golle    r'^#\s*(include|define|undef|ifdef|ifndef|if|else|elif|endif|'
501e8a9af9SDaniel Golle    r'pragma|error|warning)\b')
511e8a9af9SDaniel Golle
5229a91b75SKrzysztof Kozlowskire_dtc_directive = re.compile(
5329a91b75SKrzysztof Kozlowski    r'^/(dts-v1|include)/')
5429a91b75SKrzysztof Kozlowski
551e8a9af9SDaniel Golle# label: name@addr {  -- label and addr optional; name can be "/"
561e8a9af9SDaniel Golle# Per the DT spec a node name may start with a digit (e.g. 1wire@...).
571e8a9af9SDaniel Golle# The address part is captured loosely (any non-space, non-brace run) so
581e8a9af9SDaniel Golle# malformed addresses (e.g. memory@0x1000) still reach
591e8a9af9SDaniel Golle# check_unit_address_format() instead of silently bypassing the check.
601e8a9af9SDaniel Gollere_node_header = re.compile(
611e8a9af9SDaniel Golle    r'^(?:([a-zA-Z_][a-zA-Z0-9_]*):\s*)?'
621e8a9af9SDaniel Golle    r'([a-zA-Z0-9][a-zA-Z0-9,._+-]*|/)'
631e8a9af9SDaniel Golle    r'(?:@([^\s{]+))?'
641e8a9af9SDaniel Golle    r'\s*\{$')
651e8a9af9SDaniel Golle
661e8a9af9SDaniel Gollere_ref_node = re.compile(
671e8a9af9SDaniel Golle    r'^&([a-zA-Z_][a-zA-Z0-9_]*)\s*\{$')
681e8a9af9SDaniel Golle
691e8a9af9SDaniel Golle
701e8a9af9SDaniel Golledef is_preprocessor(stripped):
711e8a9af9SDaniel Golle    """Tell C preprocessor directives apart from DTS '#'-prefixed props."""
7229a91b75SKrzysztof Kozlowski    if re_cpp_directive.match(stripped) is not None:
7329a91b75SKrzysztof Kozlowski        return True
7429a91b75SKrzysztof Kozlowski    if re_dtc_directive.match(stripped) is not None:
7529a91b75SKrzysztof Kozlowski        return True
7629a91b75SKrzysztof Kozlowski    return False
771e8a9af9SDaniel Golle
781e8a9af9SDaniel Golle
791e8a9af9SDaniel Golleclass DtsLine:
801e8a9af9SDaniel Golle    __slots__ = ('lineno', 'raw', 'linetype', 'indent_str', 'stripped',
811e8a9af9SDaniel Golle                 'prop_name', 'continuations',
821e8a9af9SDaniel Golle                 'node_name', 'node_addr', 'label', 'ref_name', 'depth',
831e8a9af9SDaniel Golle                 'closures')
841e8a9af9SDaniel Golle
85*d863ae62SKrzysztof Kozlowski    def __init__(self, lineno, raw, linetype, depth, indent_str, stripped):
861e8a9af9SDaniel Golle        self.lineno = lineno      # 1-based within the block
871e8a9af9SDaniel Golle        self.raw = raw
881e8a9af9SDaniel Golle        self.linetype = linetype
891e8a9af9SDaniel Golle        self.indent_str = indent_str  # leading whitespace as-is
90*d863ae62SKrzysztof Kozlowski        self.depth = depth
911e8a9af9SDaniel Golle        self.stripped = stripped
921e8a9af9SDaniel Golle        self.prop_name = None
931e8a9af9SDaniel Golle        self.continuations = []
941e8a9af9SDaniel Golle        self.node_name = None
951e8a9af9SDaniel Golle        self.node_addr = None
961e8a9af9SDaniel Golle        self.label = None
971e8a9af9SDaniel Golle        self.ref_name = None
981e8a9af9SDaniel Golle        self.closures = 1         # count of '}' on a NODE_CLOSE line
991e8a9af9SDaniel Golle
1001e8a9af9SDaniel Golle
1011e8a9af9SDaniel Golledef _split_code(text):
1021e8a9af9SDaniel Golle    """Return (code, opens_block) for a leading-stripped line: the
1031e8a9af9SDaniel Golle    code portion with // and /* */ comments removed (string literals
1041e8a9af9SDaniel Golle    kept verbatim), and whether a /* */ block comment is left open.
1051e8a9af9SDaniel Golle    The code portion is right-stripped so the endswith() checks in
1061e8a9af9SDaniel Golle    classify_lines see code only, not a trailing comment or blanks."""
1071e8a9af9SDaniel Golle    out = []
1081e8a9af9SDaniel Golle    i = 0
1091e8a9af9SDaniel Golle    n = len(text)
1101e8a9af9SDaniel Golle    while i < n:
1111e8a9af9SDaniel Golle        c = text[i]
1121e8a9af9SDaniel Golle        if c == '"':
1131e8a9af9SDaniel Golle            j = i + 1
1141e8a9af9SDaniel Golle            while j < n:
1151e8a9af9SDaniel Golle                if text[j] == '\\':
1161e8a9af9SDaniel Golle                    j += 2
1171e8a9af9SDaniel Golle                    continue
1181e8a9af9SDaniel Golle                if text[j] == '"':
1191e8a9af9SDaniel Golle                    j += 1
1201e8a9af9SDaniel Golle                    break
1211e8a9af9SDaniel Golle                j += 1
1221e8a9af9SDaniel Golle            out.append(text[i:j])
1231e8a9af9SDaniel Golle            i = j
1241e8a9af9SDaniel Golle            continue
1251e8a9af9SDaniel Golle        if c == '/' and i + 1 < n and text[i + 1] == '/':
1261e8a9af9SDaniel Golle            break
1271e8a9af9SDaniel Golle        if c == '/' and i + 1 < n and text[i + 1] == '*':
1281e8a9af9SDaniel Golle            end = text.find('*/', i + 2)
1291e8a9af9SDaniel Golle            if end < 0:
1301e8a9af9SDaniel Golle                return (''.join(out).rstrip(), True)
1311e8a9af9SDaniel Golle            i = end + 2
1321e8a9af9SDaniel Golle            continue
1331e8a9af9SDaniel Golle        out.append(c)
1341e8a9af9SDaniel Golle        i += 1
1351e8a9af9SDaniel Golle    return (''.join(out).rstrip(), False)
1361e8a9af9SDaniel Golle
1371e8a9af9SDaniel Golle
1381e8a9af9SDaniel Gollere_only_closures = re.compile(r'(?:\}\s*;?\s*)+$')
1391e8a9af9SDaniel Golle
1401e8a9af9SDaniel Golle
1411e8a9af9SDaniel Golledef classify_lines(text):
1421e8a9af9SDaniel Golle    """Return a list of DtsLine. Tracks { } depth and groups
1431e8a9af9SDaniel Golle    continuation lines onto their leading PROPERTY line."""
1441e8a9af9SDaniel Golle    out = []
1451e8a9af9SDaniel Golle    in_block_comment = False
1461e8a9af9SDaniel Golle    in_cpp_macro = False
1471e8a9af9SDaniel Golle    prev_complete = True
1481e8a9af9SDaniel Golle    depth = 0
1491e8a9af9SDaniel Golle
1501e8a9af9SDaniel Golle    # Split preserving the indent string verbatim
1511e8a9af9SDaniel Golle    re_lead = re.compile(r'^([ \t]*)(.*)$')
1521e8a9af9SDaniel Golle
1531e8a9af9SDaniel Golle    for i, raw in enumerate(text.split('\n'), start=1):
1541e8a9af9SDaniel Golle        m = re_lead.match(raw)
1551e8a9af9SDaniel Golle        indent_str = m.group(1)
1561e8a9af9SDaniel Golle        stripped = m.group(2)
1571e8a9af9SDaniel Golle
1581e8a9af9SDaniel Golle        # Continuation of a multi-line C preprocessor directive: the
1591e8a9af9SDaniel Golle        # previous PREPROCESSOR line ended with a '\\' line splice, so
1601e8a9af9SDaniel Golle        # this line is part of the same macro. Treat it as
1611e8a9af9SDaniel Golle        # PREPROCESSOR until the splice chain ends (no trailing '\\'
1621e8a9af9SDaniel Golle        # or a blank line).
1631e8a9af9SDaniel Golle        if in_cpp_macro:
1641e8a9af9SDaniel Golle            dl = DtsLine(i, raw, LineType.PREPROCESSOR,
165*d863ae62SKrzysztof Kozlowski                         depth, indent_str, stripped)
1661e8a9af9SDaniel Golle            out.append(dl)
1671e8a9af9SDaniel Golle            in_cpp_macro = (bool(stripped) and
1681e8a9af9SDaniel Golle                            stripped.rstrip().endswith('\\'))
1691e8a9af9SDaniel Golle            continue
1701e8a9af9SDaniel Golle
1711e8a9af9SDaniel Golle        if not stripped:
172*d863ae62SKrzysztof Kozlowski            dl = DtsLine(i, raw, LineType.BLANK, depth, '', '')
1731e8a9af9SDaniel Golle            out.append(dl)
1741e8a9af9SDaniel Golle            continue
1751e8a9af9SDaniel Golle
1761e8a9af9SDaniel Golle        if in_block_comment:
1771e8a9af9SDaniel Golle            ltype = (LineType.COMMENT_END if '*/' in stripped
1781e8a9af9SDaniel Golle                     else LineType.COMMENT_BODY)
1791e8a9af9SDaniel Golle            if ltype == LineType.COMMENT_END:
1801e8a9af9SDaniel Golle                in_block_comment = False
181*d863ae62SKrzysztof Kozlowski            dl = DtsLine(i, raw, ltype, depth, indent_str, stripped)
1821e8a9af9SDaniel Golle            out.append(dl)
1831e8a9af9SDaniel Golle            continue
1841e8a9af9SDaniel Golle
18529a91b75SKrzysztof Kozlowski        if (stripped.startswith('#') or stripped.startswith('/')) and is_preprocessor(stripped):
186*d863ae62SKrzysztof Kozlowski            dl = DtsLine(i, raw, LineType.PREPROCESSOR, depth,
1871e8a9af9SDaniel Golle                         indent_str, stripped)
1881e8a9af9SDaniel Golle            out.append(dl)
1891e8a9af9SDaniel Golle            prev_complete = True
1901e8a9af9SDaniel Golle            in_cpp_macro = stripped.rstrip().endswith('\\')
1911e8a9af9SDaniel Golle            continue
1921e8a9af9SDaniel Golle
1931e8a9af9SDaniel Golle        # Strip comments first so all later structural checks see code
1941e8a9af9SDaniel Golle        # only. An unclosed /* sets in_block_comment for the next line.
1951e8a9af9SDaniel Golle        code, opens_block = _split_code(stripped)
1961e8a9af9SDaniel Golle        if opens_block:
1971e8a9af9SDaniel Golle            in_block_comment = True
1981e8a9af9SDaniel Golle
1991e8a9af9SDaniel Golle        # Pure-comment line: nothing left after stripping. Classify as
2001e8a9af9SDaniel Golle        # COMMENT_START (carries to next line) or COMMENT, and skip the
2011e8a9af9SDaniel Golle        # structural classification entirely.
2021e8a9af9SDaniel Golle        if not code:
2031e8a9af9SDaniel Golle            ltype = LineType.COMMENT_START if opens_block else LineType.COMMENT
204*d863ae62SKrzysztof Kozlowski            dl = DtsLine(i, raw, ltype, depth, indent_str, stripped)
2051e8a9af9SDaniel Golle            out.append(dl)
2061e8a9af9SDaniel Golle            continue
2071e8a9af9SDaniel Golle
2081e8a9af9SDaniel Golle        if not prev_complete:
209*d863ae62SKrzysztof Kozlowski            dl = DtsLine(i, raw, LineType.CONTINUATION, depth, indent_str, code)
2101e8a9af9SDaniel Golle            out.append(dl)
2111e8a9af9SDaniel Golle            prev_complete = (code.endswith(';') or
2121e8a9af9SDaniel Golle                             code.endswith('{') or
2131e8a9af9SDaniel Golle                             code.endswith('};'))
2141e8a9af9SDaniel Golle            continue
2151e8a9af9SDaniel Golle
2161e8a9af9SDaniel Golle        # NODE_CLOSE: the canonical form is "}" or "};" alone. A line
2171e8a9af9SDaniel Golle        # that is nothing but closures (e.g. "}; };") is still treated
2181e8a9af9SDaniel Golle        # as NODE_CLOSE for depth tracking, but the multi-closure case
2191e8a9af9SDaniel Golle        # is flagged separately by check_node_close_alone via
2201e8a9af9SDaniel Golle        # dl.closures.
2211e8a9af9SDaniel Golle        if re_only_closures.match(code):
2221e8a9af9SDaniel Golle            closures = code.count('}')
2231e8a9af9SDaniel Golle            depth = max(depth - closures, 0)
224*d863ae62SKrzysztof Kozlowski            dl = DtsLine(i, raw, LineType.NODE_CLOSE, depth, indent_str, code)
2251e8a9af9SDaniel Golle            dl.closures = closures
2261e8a9af9SDaniel Golle            out.append(dl)
2271e8a9af9SDaniel Golle            prev_complete = True
2281e8a9af9SDaniel Golle            continue
2291e8a9af9SDaniel Golle
2301e8a9af9SDaniel Golle        if code.endswith('{'):
231*d863ae62SKrzysztof Kozlowski            dl = DtsLine(i, raw, LineType.NODE_OPEN, depth, indent_str, code)
2321e8a9af9SDaniel Golle            parse_node_header(dl)
2331e8a9af9SDaniel Golle            out.append(dl)
2341e8a9af9SDaniel Golle            depth += 1
2351e8a9af9SDaniel Golle            prev_complete = True
2361e8a9af9SDaniel Golle            continue
2371e8a9af9SDaniel Golle
2381e8a9af9SDaniel Golle        # Property (or first line of a multi-line property).
239*d863ae62SKrzysztof Kozlowski        dl = DtsLine(i, raw, LineType.PROPERTY, depth, indent_str, code)
2401e8a9af9SDaniel Golle        parse_property_name(dl)
2411e8a9af9SDaniel Golle        out.append(dl)
2421e8a9af9SDaniel Golle        prev_complete = code.endswith(';')
2431e8a9af9SDaniel Golle
2441e8a9af9SDaniel Golle    # Group continuation lines onto their leading PROPERTY.
2451e8a9af9SDaniel Golle    last_prop = None
2461e8a9af9SDaniel Golle    grouped = []
2471e8a9af9SDaniel Golle    for dl in out:
2481e8a9af9SDaniel Golle        if dl.linetype == LineType.CONTINUATION and last_prop is not None:
2491e8a9af9SDaniel Golle            last_prop.continuations.append(dl)
2501e8a9af9SDaniel Golle            continue
2511e8a9af9SDaniel Golle        if dl.linetype == LineType.PROPERTY:
2521e8a9af9SDaniel Golle            last_prop = dl
2531e8a9af9SDaniel Golle        elif dl.linetype != LineType.BLANK and \
2541e8a9af9SDaniel Golle                dl.linetype not in (LineType.COMMENT, LineType.COMMENT_BODY,
2551e8a9af9SDaniel Golle                                    LineType.COMMENT_END,
2561e8a9af9SDaniel Golle                                    LineType.COMMENT_START):
2571e8a9af9SDaniel Golle            last_prop = None
2581e8a9af9SDaniel Golle        grouped.append(dl)
2591e8a9af9SDaniel Golle    return grouped
2601e8a9af9SDaniel Golle
2611e8a9af9SDaniel Golle
2621e8a9af9SDaniel Golledef parse_node_header(dl):
2631e8a9af9SDaniel Golle    m = re_node_header.match(dl.stripped)
2641e8a9af9SDaniel Golle    if m:
2651e8a9af9SDaniel Golle        dl.label = m.group(1)
2661e8a9af9SDaniel Golle        dl.node_name = m.group(2)
2671e8a9af9SDaniel Golle        dl.node_addr = m.group(3)
2681e8a9af9SDaniel Golle        return
2691e8a9af9SDaniel Golle    m = re_ref_node.match(dl.stripped)
2701e8a9af9SDaniel Golle    if m:
2711e8a9af9SDaniel Golle        dl.ref_name = m.group(1)
2721e8a9af9SDaniel Golle
2731e8a9af9SDaniel Golle
2741e8a9af9SDaniel Golledef parse_property_name(dl):
2751e8a9af9SDaniel Golle    m = re.match(r'^([a-zA-Z0-9#][a-zA-Z0-9,._+#-]*)\s*[=;]', dl.stripped)
2761e8a9af9SDaniel Golle    if m:
2771e8a9af9SDaniel Golle        dl.prop_name = m.group(1)
2781e8a9af9SDaniel Golle
2791e8a9af9SDaniel Golle
2801e8a9af9SDaniel Golledef collect_labels_and_refs(text):
2811e8a9af9SDaniel Golle    """Return (defined_labels, referenced_labels) found anywhere outside
2821e8a9af9SDaniel Golle    /* */ comments and string literals. Labels named fake_intc* (injected
2831e8a9af9SDaniel Golle    by dt-extract-example) are skipped."""
2841e8a9af9SDaniel Golle    # Strip block comments first so labels inside them don't count
2851e8a9af9SDaniel Golle    stripped = re.sub(r'/\*.*?\*/', '', text, flags=re.DOTALL)
2861e8a9af9SDaniel Golle    # Strip line comments
2871e8a9af9SDaniel Golle    stripped = re.sub(r'//[^\n]*', '', stripped)
2881e8a9af9SDaniel Golle    # Strip string literals so words inside quotes (e.g. "Error: foo")
2891e8a9af9SDaniel Golle    # are not picked up as label definitions or &-references.
2901e8a9af9SDaniel Golle    stripped = re.sub(r'"(?:[^"\\]|\\.)*"', '""', stripped)
2911e8a9af9SDaniel Golle    defined = set()
2921e8a9af9SDaniel Golle    referenced = set()
2931e8a9af9SDaniel Golle    # A label precedes a node header; the next non-space token may start
2941e8a9af9SDaniel Golle    # with a letter (foo, &ref), a digit (1wire), or '/' (root node).
2951e8a9af9SDaniel Golle    for m in re.finditer(
2961e8a9af9SDaniel Golle            r'(?:^|[\s{])([a-zA-Z_][a-zA-Z0-9_]*):\s*[a-zA-Z0-9/&]',
2971e8a9af9SDaniel Golle            stripped):
2981e8a9af9SDaniel Golle        name = m.group(1)
2991e8a9af9SDaniel Golle        if not name.startswith('fake_intc'):
3001e8a9af9SDaniel Golle            defined.add(name)
3011e8a9af9SDaniel Golle    for m in re.finditer(r'&([a-zA-Z_][a-zA-Z0-9_]*)', stripped):
3021e8a9af9SDaniel Golle        referenced.add(m.group(1))
3031e8a9af9SDaniel Golle    return defined, referenced
3041e8a9af9SDaniel Golle
3051e8a9af9SDaniel Golle
3061e8a9af9SDaniel Golle# ---------------------------------------------------------------------------
3071e8a9af9SDaniel Golle# Rule registry
3081e8a9af9SDaniel Golle# ---------------------------------------------------------------------------
3091e8a9af9SDaniel Golle
3101e8a9af9SDaniel Golleclass Ctx:
3111e8a9af9SDaniel Golle    """Context passed to each rule check. Carries the parsed lines,
31246fb56e4SKrzysztof Kozlowski    raw text, mode and kind."""
3131e8a9af9SDaniel Golle
31446fb56e4SKrzysztof Kozlowski    def __init__(self, lines, text, mode, kind):
3151e8a9af9SDaniel Golle        self.lines = lines
3161e8a9af9SDaniel Golle        self.text = text
3171e8a9af9SDaniel Golle        self.mode = mode               # 'relaxed' or 'strict'
31846fb56e4SKrzysztof Kozlowski        if kind in DTS_FAMILY:
31946fb56e4SKrzysztof Kozlowski            self.file_type = 'dts'
32046fb56e4SKrzysztof Kozlowski        else:
32146fb56e4SKrzysztof Kozlowski            self.file_type = 'yaml'
3221e8a9af9SDaniel Golle
3231e8a9af9SDaniel Golle
3241e8a9af9SDaniel Golleclass Rule:
3251e8a9af9SDaniel Golle    __slots__ = ('name', 'mode', 'description', 'check', 'applies_to')
3261e8a9af9SDaniel Golle
3271e8a9af9SDaniel Golle    def __init__(self, name, mode, description, check,
3281e8a9af9SDaniel Golle                 applies_to=('yaml', 'dts', 'dtsi', 'dtso')):
3291e8a9af9SDaniel Golle        self.name = name
3301e8a9af9SDaniel Golle        self.mode = mode               # 'relaxed' or 'strict'
3311e8a9af9SDaniel Golle        self.description = description
3321e8a9af9SDaniel Golle        self.check = check
3331e8a9af9SDaniel Golle        self.applies_to = applies_to   # input types this rule covers
3341e8a9af9SDaniel Golle
3351e8a9af9SDaniel Golle
3361e8a9af9SDaniel Golle# --- individual rule check functions --------------------------------------
3371e8a9af9SDaniel Golle
3381e8a9af9SDaniel Golledef check_trailing_whitespace(ctx):
3391e8a9af9SDaniel Golle    for dl in ctx.lines:
3401e8a9af9SDaniel Golle        if dl.raw != dl.raw.rstrip():
3411e8a9af9SDaniel Golle            yield (dl.lineno, 'trailing whitespace')
3421e8a9af9SDaniel Golle
3431e8a9af9SDaniel Golle
34449b5cf46SKrzysztof Kozlowskidef check_tab_in_yaml_example(ctx):
3451e8a9af9SDaniel Golle    """Reject literal tabs in DTS lines when input is YAML.
3461e8a9af9SDaniel Golle
3471e8a9af9SDaniel Golle    For YAML examples, indent and content must use spaces. Tabs inside
3481e8a9af9SDaniel Golle    a #define value are tolerated (those are CPP macros, not DTS).
3491e8a9af9SDaniel Golle    For .dts files, this rule does not apply -- tabs are required.
3501e8a9af9SDaniel Golle    """
35146fb56e4SKrzysztof Kozlowski    if ctx.file_type != 'yaml':
3521e8a9af9SDaniel Golle        return
3531e8a9af9SDaniel Golle    for dl in ctx.lines:
3541e8a9af9SDaniel Golle        if dl.linetype == LineType.PREPROCESSOR:
3551e8a9af9SDaniel Golle            continue
3561e8a9af9SDaniel Golle        if dl.linetype == LineType.BLANK:
3571e8a9af9SDaniel Golle            continue
3581e8a9af9SDaniel Golle        if '\t' in dl.raw:
3591e8a9af9SDaniel Golle            yield (dl.lineno, 'tab character not allowed in DTS example')
3601e8a9af9SDaniel Golle
3611e8a9af9SDaniel Golle
3621e8a9af9SDaniel Golledef check_mixed_indent_chars(ctx):
36332541ce0SKrzysztof Kozlowski    """Indent must be all-tabs, except for aligning indentation (comments
36432541ce0SKrzysztof Kozlowski    or continued lines)."""
3651e8a9af9SDaniel Golle    for dl in ctx.lines:
3661e8a9af9SDaniel Golle        if not dl.indent_str:
3671e8a9af9SDaniel Golle            continue
3681e8a9af9SDaniel Golle        if dl.linetype == LineType.PREPROCESSOR:
3691e8a9af9SDaniel Golle            continue
37032541ce0SKrzysztof Kozlowski        if re.search(r' \t', dl.indent_str):
3711e8a9af9SDaniel Golle            yield (dl.lineno, 'mixed tabs and spaces in indent')
37232541ce0SKrzysztof Kozlowski        if dl.indent_str.count(' ') > 7:
37332541ce0SKrzysztof Kozlowski            yield (dl.lineno, 'too many space characters in indent (more than 7)')
37432541ce0SKrzysztof Kozlowski        for cont in dl.continuations:
37532541ce0SKrzysztof Kozlowski            if not cont.indent_str:
37632541ce0SKrzysztof Kozlowski                continue
37732541ce0SKrzysztof Kozlowski            if cont.linetype == LineType.PREPROCESSOR:
37832541ce0SKrzysztof Kozlowski                continue
37932541ce0SKrzysztof Kozlowski            if re.search(r' \t', cont.indent_str):
38032541ce0SKrzysztof Kozlowski                yield (cont.lineno, 'mixed tabs and spaces in indent')
3811e8a9af9SDaniel Golle
3821e8a9af9SDaniel Golle
3831e8a9af9SDaniel Golledef detect_indent_unit(ctx):
3841e8a9af9SDaniel Golle    """Find the indent unit used at depth 1 in this block.
3851e8a9af9SDaniel Golle
38642879c68SKrzysztof Kozlowski    Returns tuple of string (one of: '  ' (2 spaces), '    ' (4 spaces),
38742879c68SKrzysztof Kozlowski    '\\t' (tab), or None if depth-1 is empty or ambiguous) and line number when
38842879c68SKrzysztof Kozlowski    detection was made)."""
3891e8a9af9SDaniel Golle    for dl in ctx.lines:
3901e8a9af9SDaniel Golle        if dl.depth != 1:
3911e8a9af9SDaniel Golle            continue
3921e8a9af9SDaniel Golle        if dl.linetype in (LineType.BLANK, LineType.PREPROCESSOR):
3931e8a9af9SDaniel Golle            continue
3941e8a9af9SDaniel Golle        if dl.linetype in (LineType.COMMENT_BODY, LineType.COMMENT_END):
3951e8a9af9SDaniel Golle            continue
3961e8a9af9SDaniel Golle        if not dl.indent_str:
3971e8a9af9SDaniel Golle            continue
3981e8a9af9SDaniel Golle        if dl.indent_str == '\t':
39942879c68SKrzysztof Kozlowski            return ('\t', dl.lineno)
4001e8a9af9SDaniel Golle        if dl.indent_str == '    ':
40142879c68SKrzysztof Kozlowski            return ('    ', dl.lineno)
4021e8a9af9SDaniel Golle        if dl.indent_str == '  ':
40342879c68SKrzysztof Kozlowski            return ('  ', dl.lineno)
4041e8a9af9SDaniel Golle        # Anything else at depth 1 is non-canonical; flag elsewhere.
40542879c68SKrzysztof Kozlowski        return (dl.indent_str, dl.lineno)
40642879c68SKrzysztof Kozlowski    return (None, None)
4071e8a9af9SDaniel Golle
4081e8a9af9SDaniel Golle
4091e8a9af9SDaniel Golledef check_indent_unit_relaxed(ctx):
4101e8a9af9SDaniel Golle    """YAML examples: 2 or 4 spaces. Never tabs or other widths."""
41142879c68SKrzysztof Kozlowski    (unit, lineno) = detect_indent_unit(ctx)
4121e8a9af9SDaniel Golle    if unit is None:
4131e8a9af9SDaniel Golle        return
4141e8a9af9SDaniel Golle    if unit not in ('  ', '    '):
41542879c68SKrzysztof Kozlowski        yield (lineno, 'indent unit must be 2 or 4 spaces, got %r' % unit)
4161e8a9af9SDaniel Golle
4171e8a9af9SDaniel Golle
4181e8a9af9SDaniel Golledef check_indent_unit_dts(ctx):
4191e8a9af9SDaniel Golle    """DTS files: 1 tab per level. Always required."""
42042879c68SKrzysztof Kozlowski    (unit, lineno) = detect_indent_unit(ctx)
4211e8a9af9SDaniel Golle    if unit is None:
4221e8a9af9SDaniel Golle        return
4231e8a9af9SDaniel Golle    if unit != '\t':
42442879c68SKrzysztof Kozlowski        yield (lineno, 'indent unit must be 1 tab in DTS, got %r' % unit)
4251e8a9af9SDaniel Golle
4261e8a9af9SDaniel Golle
4271e8a9af9SDaniel Golledef check_indent_unit_strict(ctx):
4281e8a9af9SDaniel Golle    """YAML: must be exactly 4 spaces. DTS: 1 tab (same as relaxed)."""
42942879c68SKrzysztof Kozlowski    (unit, lineno) = detect_indent_unit(ctx)
4301e8a9af9SDaniel Golle    if unit is None:
4311e8a9af9SDaniel Golle        return
43246fb56e4SKrzysztof Kozlowski    if ctx.file_type == 'yaml':
4331e8a9af9SDaniel Golle        if unit != '    ':
43442879c68SKrzysztof Kozlowski            yield (lineno, 'indent unit must be 4 spaces in strict mode, '
4351e8a9af9SDaniel Golle                   'got %r' % unit)
4361e8a9af9SDaniel Golle
4371e8a9af9SDaniel Golle
4381e8a9af9SDaniel Golledef check_indent_consistent(ctx):
4391e8a9af9SDaniel Golle    """All indented lines must be a multiple of the detected unit."""
44042879c68SKrzysztof Kozlowski    (unit, lineno) = detect_indent_unit(ctx)
4411e8a9af9SDaniel Golle    if unit is None:
4421e8a9af9SDaniel Golle        return
44346fb56e4SKrzysztof Kozlowski    if ctx.file_type == 'yaml':
4441e8a9af9SDaniel Golle        if unit not in ('  ', '    '):
4451e8a9af9SDaniel Golle            return  # let check_indent_unit_* report this
4461e8a9af9SDaniel Golle    else:
4471e8a9af9SDaniel Golle        if unit != '\t':
4481e8a9af9SDaniel Golle            return
4491e8a9af9SDaniel Golle
4501e8a9af9SDaniel Golle    for dl in ctx.lines:
4511e8a9af9SDaniel Golle        if dl.linetype in (LineType.BLANK, LineType.PREPROCESSOR):
4521e8a9af9SDaniel Golle            continue
4531e8a9af9SDaniel Golle        if dl.linetype == LineType.CONTINUATION:
4541e8a9af9SDaniel Golle            continue   # continuations align to <, not to indent unit
4551e8a9af9SDaniel Golle        if dl.linetype in (LineType.COMMENT_BODY, LineType.COMMENT_END):
4561e8a9af9SDaniel Golle            continue
4571e8a9af9SDaniel Golle        if not dl.indent_str:
4581e8a9af9SDaniel Golle            continue
4591e8a9af9SDaniel Golle        # The indent must be 'unit' repeated dl.depth times, exactly.
4601e8a9af9SDaniel Golle        # NODE_CLOSE lines have depth equal to the post-decrement value,
4611e8a9af9SDaniel Golle        # which matches the indent expected.
4621e8a9af9SDaniel Golle        expected = unit * dl.depth
4631e8a9af9SDaniel Golle        if dl.indent_str != expected:
4641e8a9af9SDaniel Golle            yield (dl.lineno,
4651e8a9af9SDaniel Golle                   'indent mismatch (expected depth %d * %r)' %
4661e8a9af9SDaniel Golle                   (dl.depth, unit))
4671e8a9af9SDaniel Golle
4681e8a9af9SDaniel Golle
4691e8a9af9SDaniel Golledef check_blank_lines(ctx):
4701e8a9af9SDaniel Golle    """No two consecutive blank lines, no leading/trailing blank lines
4711e8a9af9SDaniel Golle    in any node body."""
4721e8a9af9SDaniel Golle    lines = ctx.lines
4731e8a9af9SDaniel Golle    # Consecutive blanks
4741e8a9af9SDaniel Golle    for i in range(1, len(lines)):
4751e8a9af9SDaniel Golle        if lines[i].linetype == LineType.BLANK and \
4761e8a9af9SDaniel Golle                lines[i - 1].linetype == LineType.BLANK:
4771e8a9af9SDaniel Golle            yield (lines[i].lineno, 'consecutive blank lines')
4781e8a9af9SDaniel Golle    # Blank right after { or right before }
4791e8a9af9SDaniel Golle    for i, dl in enumerate(lines):
4801e8a9af9SDaniel Golle        if dl.linetype != LineType.BLANK:
4811e8a9af9SDaniel Golle            continue
4821e8a9af9SDaniel Golle        prev = lines[i - 1] if i > 0 else None
4831e8a9af9SDaniel Golle        nxt = lines[i + 1] if i + 1 < len(lines) else None
4841e8a9af9SDaniel Golle        if prev is not None and prev.linetype == LineType.NODE_OPEN:
4851e8a9af9SDaniel Golle            yield (dl.lineno, 'blank line at start of node body')
4861e8a9af9SDaniel Golle        if nxt is not None and nxt.linetype == LineType.NODE_CLOSE:
4871e8a9af9SDaniel Golle            yield (dl.lineno, 'blank line at end of node body')
4881e8a9af9SDaniel Golle
4891e8a9af9SDaniel Golle
4901e8a9af9SDaniel Golledef _walk_bodies(lines):
4911e8a9af9SDaniel Golle    """Yield lists of immediate-child NODE_OPEN lines for each node body
4921e8a9af9SDaniel Golle    in the input. Skips ref-nodes (&label) since those don't have an
4931e8a9af9SDaniel Golle    intrinsic ordering."""
4941e8a9af9SDaniel Golle    body_stack = [[]]
4951e8a9af9SDaniel Golle    for dl in lines:
4961e8a9af9SDaniel Golle        if dl.linetype == LineType.NODE_OPEN:
4971e8a9af9SDaniel Golle            body_stack[-1].append(dl)
4981e8a9af9SDaniel Golle            body_stack.append([])
4991e8a9af9SDaniel Golle            continue
5001e8a9af9SDaniel Golle        if dl.linetype == LineType.NODE_CLOSE:
5011e8a9af9SDaniel Golle            if len(body_stack) <= 1:
5021e8a9af9SDaniel Golle                # Unbalanced; ignore to avoid crashing on malformed input
5031e8a9af9SDaniel Golle                continue
5041e8a9af9SDaniel Golle            yield body_stack.pop()
5051e8a9af9SDaniel Golle            continue
5061e8a9af9SDaniel Golle    while body_stack:
5071e8a9af9SDaniel Golle        yield body_stack.pop()
5081e8a9af9SDaniel Golle
5091e8a9af9SDaniel Golle
5101e8a9af9SDaniel Golledef _natural_sort_key(s):
5111e8a9af9SDaniel Golle    """Split a string into a tuple of (kind, value) pairs that compares
5121e8a9af9SDaniel Golle    numeric runs as ints, so 'foo10' sorts after 'foo2'."""
5131e8a9af9SDaniel Golle    parts = []
5141e8a9af9SDaniel Golle    for part in re.split(r'(\d+)', s):
5151e8a9af9SDaniel Golle        if part.isdigit():
5161e8a9af9SDaniel Golle            parts.append((0, int(part)))
5171e8a9af9SDaniel Golle        else:
5181e8a9af9SDaniel Golle            parts.append((1, part))
5191e8a9af9SDaniel Golle    return tuple(parts)
5201e8a9af9SDaniel Golle
5211e8a9af9SDaniel Golle
5221e8a9af9SDaniel Golledef check_child_address_order(ctx):
5231e8a9af9SDaniel Golle    """Addressed siblings (foo@N) must appear in ascending address
5241e8a9af9SDaniel Golle    order within their parent node body."""
5251e8a9af9SDaniel Golle    for children in _walk_bodies(ctx.lines):
5261e8a9af9SDaniel Golle        addressed = []
5271e8a9af9SDaniel Golle        for c in children:
5281e8a9af9SDaniel Golle            if c.node_addr is None:
5291e8a9af9SDaniel Golle                continue
5301e8a9af9SDaniel Golle            try:
5311e8a9af9SDaniel Golle                parts = tuple(int(p, 16) for p in c.node_addr.split(','))
5321e8a9af9SDaniel Golle            except ValueError:
5331e8a9af9SDaniel Golle                continue
5341e8a9af9SDaniel Golle            addressed.append((parts, c))
5351e8a9af9SDaniel Golle        for i in range(1, len(addressed)):
5361e8a9af9SDaniel Golle            if addressed[i][0] < addressed[i - 1][0]:
5371e8a9af9SDaniel Golle                dl = addressed[i][1]
5381e8a9af9SDaniel Golle                yield (dl.lineno,
5391e8a9af9SDaniel Golle                       'child node @%s out of address order' %
5401e8a9af9SDaniel Golle                       dl.node_addr)
5411e8a9af9SDaniel Golle
5421e8a9af9SDaniel Golle
5431e8a9af9SDaniel Golledef check_child_name_order(ctx):
5441e8a9af9SDaniel Golle    """Unaddressed siblings must appear in natural-sort order by node
5451e8a9af9SDaniel Golle    name within their parent node body. Addressed children are scoped
5461e8a9af9SDaniel Golle    by check_child_address_order; reference nodes (&label { ... }) and
5471e8a9af9SDaniel Golle    the root node are skipped."""
5481e8a9af9SDaniel Golle    for children in _walk_bodies(ctx.lines):
5491e8a9af9SDaniel Golle        unaddressed = []
5501e8a9af9SDaniel Golle        for c in children:
5511e8a9af9SDaniel Golle            if c.node_addr is not None:
5521e8a9af9SDaniel Golle                continue
5531e8a9af9SDaniel Golle            if c.node_name in (None, '/'):
5541e8a9af9SDaniel Golle                continue
5551e8a9af9SDaniel Golle            if c.ref_name is not None:
5561e8a9af9SDaniel Golle                continue
5571e8a9af9SDaniel Golle            unaddressed.append((_natural_sort_key(c.node_name), c))
5581e8a9af9SDaniel Golle        for i in range(1, len(unaddressed)):
5591e8a9af9SDaniel Golle            if unaddressed[i][0] < unaddressed[i - 1][0]:
5601e8a9af9SDaniel Golle                dl = unaddressed[i][1]
5611e8a9af9SDaniel Golle                yield (dl.lineno,
5621e8a9af9SDaniel Golle                       'child node %r out of name order' % dl.node_name)
5631e8a9af9SDaniel Golle
5641e8a9af9SDaniel Golle
5651e8a9af9SDaniel Golledef _property_bucket(name):
5661e8a9af9SDaniel Golle    """Return the canonical bucket index for a property:
567597233e2SKrzysztof Kozlowski       0 device_type
568597233e2SKrzysztof Kozlowski       1 compatible
569597233e2SKrzysztof Kozlowski       2 reg / reg-names
570597233e2SKrzysztof Kozlowski       3 ranges
571597233e2SKrzysztof Kozlowski       4 standard properties (no vendor comma in #-stripped name)
572597233e2SKrzysztof Kozlowski       5 vendor-specific properties
573597233e2SKrzysztof Kozlowski       6 status
574597233e2SKrzysztof Kozlowski    Plus a sub-key inside the bucket for fixed slots (device_type, compatible,
575597233e2SKrzysztof Kozlowski    reg, reg-names, ranges, status). 'standard' and 'vendor' return None for
5761e8a9af9SDaniel Golle    the sub-key, signalling that the within-bucket key is computed by
5771e8a9af9SDaniel Golle    the pairing rules."""
5781e8a9af9SDaniel Golle    stripped = name.lstrip('#')
579597233e2SKrzysztof Kozlowski    if name == 'device_type':
5801e8a9af9SDaniel Golle        return (0, 0)
581597233e2SKrzysztof Kozlowski    if name == 'compatible':
5821e8a9af9SDaniel Golle        return (1, 0)
583597233e2SKrzysztof Kozlowski    if name == 'reg':
5841e8a9af9SDaniel Golle        return (2, 0)
585597233e2SKrzysztof Kozlowski    if name == 'reg-names':
586597233e2SKrzysztof Kozlowski        return (2, 1)
587597233e2SKrzysztof Kozlowski    if name == 'ranges':
588597233e2SKrzysztof Kozlowski        return (3, 0)
5891e8a9af9SDaniel Golle    if name == 'status':
590597233e2SKrzysztof Kozlowski        return (6, 0)
591597233e2SKrzysztof Kozlowski    return (5 if ',' in stripped else 4, None)
5921e8a9af9SDaniel Golle
5931e8a9af9SDaniel Golle
5941e8a9af9SDaniel Golle# Declarative pairing rules: each is a callable
5951e8a9af9SDaniel Golle#   (name, all_names) -> anchor_name_or_None
5961e8a9af9SDaniel Golle# If a rule returns an anchor, the property sorts immediately after the
5971e8a9af9SDaniel Golle# anchor. Rules are tried in order; the first match wins. If none
5981e8a9af9SDaniel Golle# matches, the within-bucket key falls back to natural sort by the
5991e8a9af9SDaniel Golle# #-stripped name.
6001e8a9af9SDaniel Golle
6011e8a9af9SDaniel Golledef _pair_pinctrl_names(name, all_names):
6021e8a9af9SDaniel Golle    """pinctrl-names follows the highest pinctrl-N in the same node."""
6031e8a9af9SDaniel Golle    if name != 'pinctrl-names':
6041e8a9af9SDaniel Golle        return None
6051e8a9af9SDaniel Golle    cands = [n for n in all_names if re.match(r'^pinctrl-\d+$', n)]
6061e8a9af9SDaniel Golle    if not cands:
6071e8a9af9SDaniel Golle        return None
6081e8a9af9SDaniel Golle    return max(cands, key=_natural_sort_key)
6091e8a9af9SDaniel Golle
6101e8a9af9SDaniel Golle
6111e8a9af9SDaniel Golledef _pair_x_names(name, all_names):
6121e8a9af9SDaniel Golle    """Generic <x>-names follows its owning property. The owner is
6131e8a9af9SDaniel Golle    usually plural (clocks/clock-names, dmas/dma-names,
6141e8a9af9SDaniel Golle    resets/reset-names) but occasionally singular (reg/reg-names is
6151e8a9af9SDaniel Golle    handled by the fixed slot above; this rule catches anything else)."""
6161e8a9af9SDaniel Golle    if not name.endswith('-names'):
6171e8a9af9SDaniel Golle        return None
6181e8a9af9SDaniel Golle    base = name[:-len('-names')]
6191e8a9af9SDaniel Golle    # Try plural and singular forms.
6201e8a9af9SDaniel Golle    if (base + 's') in all_names:
6211e8a9af9SDaniel Golle        return base + 's'
6221e8a9af9SDaniel Golle    if base in all_names:
6231e8a9af9SDaniel Golle        return base
6241e8a9af9SDaniel Golle    return None
6251e8a9af9SDaniel Golle
6261e8a9af9SDaniel Golle
6271e8a9af9SDaniel GollePAIRING_RULES = (_pair_pinctrl_names, _pair_x_names)
6281e8a9af9SDaniel Golle
6291e8a9af9SDaniel Golle
6301e8a9af9SDaniel Golledef _property_sort_key(name, all_names):
6311e8a9af9SDaniel Golle    """Sort key for a property among its node-body siblings.
6321e8a9af9SDaniel Golle
6331e8a9af9SDaniel Golle    Format: (bucket, within_key, tiebreak). 'within_key' for
6341e8a9af9SDaniel Golle    standard/vendor buckets follows pairing rules: a property paired
6351e8a9af9SDaniel Golle    with anchor X sorts as if it were X with a higher tiebreak."""
6361e8a9af9SDaniel Golle    bucket, fixed_sub = _property_bucket(name)
6371e8a9af9SDaniel Golle    if fixed_sub is not None:
6381e8a9af9SDaniel Golle        return (bucket, (), fixed_sub)
6391e8a9af9SDaniel Golle
6401e8a9af9SDaniel Golle    for rule in PAIRING_RULES:
6411e8a9af9SDaniel Golle        anchor = rule(name, all_names)
6421e8a9af9SDaniel Golle        if anchor is not None:
6431e8a9af9SDaniel Golle            return (bucket, _natural_sort_key(anchor.lstrip('#')), 1)
6441e8a9af9SDaniel Golle
6451e8a9af9SDaniel Golle    return (bucket, _natural_sort_key(name.lstrip('#')), 0)
6461e8a9af9SDaniel Golle
6471e8a9af9SDaniel Golle
6481e8a9af9SDaniel Golledef check_property_order(ctx):
6491e8a9af9SDaniel Golle    """Properties within a node body must appear in canonical order:
6501e8a9af9SDaniel Golle    compatible, reg(/reg-names), ranges, then the standard group, then
6511e8a9af9SDaniel Golle    the vendor-specific group, then status. Inside the standard and
6521e8a9af9SDaniel Golle    vendor groups, pairing rules apply (e.g. <x>-names follows <x>);
6531e8a9af9SDaniel Golle    everything else falls back to natural sort by the #-stripped name."""
6541e8a9af9SDaniel Golle    lines = ctx.lines
6551e8a9af9SDaniel Golle    for i, dl in enumerate(lines):
6561e8a9af9SDaniel Golle        if dl.linetype != LineType.NODE_OPEN:
6571e8a9af9SDaniel Golle            continue
6581e8a9af9SDaniel Golle        body_depth = dl.depth + 1
6591e8a9af9SDaniel Golle        props = []
6601e8a9af9SDaniel Golle        for j in range(i + 1, len(lines)):
6611e8a9af9SDaniel Golle            d = lines[j]
6621e8a9af9SDaniel Golle            if d.linetype == LineType.NODE_CLOSE and \
6631e8a9af9SDaniel Golle                    d.depth == body_depth - 1:
6641e8a9af9SDaniel Golle                break
6651e8a9af9SDaniel Golle            if d.linetype == LineType.PROPERTY and d.depth == body_depth \
6661e8a9af9SDaniel Golle                    and d.prop_name is not None:
6671e8a9af9SDaniel Golle                props.append(d)
6681e8a9af9SDaniel Golle        if len(props) < 2:
6691e8a9af9SDaniel Golle            continue
6701e8a9af9SDaniel Golle        all_names = [p.prop_name for p in props]
6711e8a9af9SDaniel Golle        keyed = [(p, _property_sort_key(p.prop_name, all_names))
6721e8a9af9SDaniel Golle                 for p in props]
6731e8a9af9SDaniel Golle        for k in range(1, len(keyed)):
6741e8a9af9SDaniel Golle            if keyed[k][1] < keyed[k - 1][1]:
6751e8a9af9SDaniel Golle                p = keyed[k][0]
6761e8a9af9SDaniel Golle                prev = keyed[k - 1][0]
6771e8a9af9SDaniel Golle                yield (p.lineno,
6781e8a9af9SDaniel Golle                       'property %r out of canonical order '
6791e8a9af9SDaniel Golle                       '(should sort before %r)' %
6801e8a9af9SDaniel Golle                       (p.prop_name, prev.prop_name))
6811e8a9af9SDaniel Golle
6821e8a9af9SDaniel Golle
6831e8a9af9SDaniel Golledef _strip_strings_and_comments(text):
6841e8a9af9SDaniel Golle    """Remove string literals and /* */ + // comments from a single
6851e8a9af9SDaniel Golle    line, replacing them with empty strings. Used so syntactic checks
6861e8a9af9SDaniel Golle    (whitespace, hex case, etc.) don't false-positive on contents of
6871e8a9af9SDaniel Golle    quoted strings or comments. An unclosed /* on the line is treated
6881e8a9af9SDaniel Golle    as a comment running to end of line."""
6891e8a9af9SDaniel Golle    text = re.sub(r'"(?:[^"\\]|\\.)*"', '""', text)
6901e8a9af9SDaniel Golle    text = re.sub(r'/\*.*?\*/', '', text)
6911e8a9af9SDaniel Golle    text = re.sub(r'/\*.*$', '', text)
6921e8a9af9SDaniel Golle    text = re.sub(r'//.*$', '', text)
6931e8a9af9SDaniel Golle    return text
6941e8a9af9SDaniel Golle
6951e8a9af9SDaniel Golle
6961e8a9af9SDaniel Golledef check_required_blank_lines(ctx):
6971e8a9af9SDaniel Golle    """A blank line must precede each child node and the 'status'
6981e8a9af9SDaniel Golle    property within a node body, except when these are the first
6991e8a9af9SDaniel Golle    substantive item in the body."""
7001e8a9af9SDaniel Golle    lines = ctx.lines
7011e8a9af9SDaniel Golle    for i, open_dl in enumerate(lines):
7021e8a9af9SDaniel Golle        if open_dl.linetype != LineType.NODE_OPEN:
7031e8a9af9SDaniel Golle            continue
7041e8a9af9SDaniel Golle        body_depth = open_dl.depth + 1
7051e8a9af9SDaniel Golle        prev_substantive = None
7061e8a9af9SDaniel Golle        between_blanks = 0
7071e8a9af9SDaniel Golle        depth_inside = 0
7081e8a9af9SDaniel Golle        for j in range(i + 1, len(lines)):
7091e8a9af9SDaniel Golle            d = lines[j]
7101e8a9af9SDaniel Golle            if d.linetype == LineType.NODE_CLOSE and \
7111e8a9af9SDaniel Golle                    d.depth == body_depth - 1 and depth_inside == 0:
7121e8a9af9SDaniel Golle                break
7131e8a9af9SDaniel Golle            # Track depth inside nested children so we only look at
7141e8a9af9SDaniel Golle            # immediate-body items.
7151e8a9af9SDaniel Golle            if d.linetype == LineType.NODE_OPEN and \
7161e8a9af9SDaniel Golle                    d.depth >= body_depth and depth_inside > 0:
7171e8a9af9SDaniel Golle                depth_inside += 1
7181e8a9af9SDaniel Golle                continue
7191e8a9af9SDaniel Golle            if d.linetype == LineType.NODE_CLOSE and depth_inside > 0:
7201e8a9af9SDaniel Golle                depth_inside -= 1
7211e8a9af9SDaniel Golle                continue
7221e8a9af9SDaniel Golle            if depth_inside > 0:
7231e8a9af9SDaniel Golle                continue
7241e8a9af9SDaniel Golle            if d.linetype == LineType.BLANK:
7251e8a9af9SDaniel Golle                if prev_substantive is not None:
7261e8a9af9SDaniel Golle                    between_blanks += 1
7271e8a9af9SDaniel Golle                continue
7281e8a9af9SDaniel Golle            if d.linetype in (LineType.COMMENT, LineType.COMMENT_START,
7291e8a9af9SDaniel Golle                              LineType.COMMENT_BODY, LineType.COMMENT_END,
7301e8a9af9SDaniel Golle                              LineType.PREPROCESSOR):
7311e8a9af9SDaniel Golle                continue
7321e8a9af9SDaniel Golle            if d.linetype == LineType.CONTINUATION:
7331e8a9af9SDaniel Golle                continue
7341e8a9af9SDaniel Golle
7351e8a9af9SDaniel Golle            needs_blank = False
7361e8a9af9SDaniel Golle            if d.linetype == LineType.NODE_OPEN:
7371e8a9af9SDaniel Golle                needs_blank = True
7381e8a9af9SDaniel Golle                depth_inside = 1   # entered the child body
7391e8a9af9SDaniel Golle            elif d.linetype == LineType.PROPERTY and d.prop_name == 'status':
7401e8a9af9SDaniel Golle                needs_blank = True
7411e8a9af9SDaniel Golle
7421e8a9af9SDaniel Golle            if needs_blank and prev_substantive is not None and \
7431e8a9af9SDaniel Golle                    between_blanks == 0:
7441e8a9af9SDaniel Golle                if d.linetype == LineType.NODE_OPEN:
7451e8a9af9SDaniel Golle                    yield (d.lineno,
7461e8a9af9SDaniel Golle                           'child node must be preceded by a blank line')
7471e8a9af9SDaniel Golle                else:
7481e8a9af9SDaniel Golle                    yield (d.lineno,
7491e8a9af9SDaniel Golle                           '"status" must be preceded by a blank line')
7501e8a9af9SDaniel Golle
7511e8a9af9SDaniel Golle            prev_substantive = d
7521e8a9af9SDaniel Golle            between_blanks = 0
7531e8a9af9SDaniel Golle
7541e8a9af9SDaniel Golle
7551e8a9af9SDaniel Golledef check_hex_case(ctx):
7561e8a9af9SDaniel Golle    """Hex literals (0xN) must use lowercase digits and prefix."""
7571e8a9af9SDaniel Golle    for dl in ctx.lines:
7581e8a9af9SDaniel Golle        if dl.linetype in (LineType.BLANK, LineType.COMMENT,
7591e8a9af9SDaniel Golle                           LineType.COMMENT_START, LineType.COMMENT_BODY,
7601e8a9af9SDaniel Golle                           LineType.COMMENT_END, LineType.PREPROCESSOR):
7611e8a9af9SDaniel Golle            continue
7621e8a9af9SDaniel Golle        text = _strip_strings_and_comments(dl.raw)
7631e8a9af9SDaniel Golle        for m in re.finditer(r'\b0[xX][0-9a-fA-F]+\b', text):
7641e8a9af9SDaniel Golle            lit = m.group(0)
7651e8a9af9SDaniel Golle            if any(c.isupper() for c in lit[2:]) or lit[1] == 'X':
7661e8a9af9SDaniel Golle                yield (dl.lineno,
7671e8a9af9SDaniel Golle                       'hex literal %r must be lowercase' % lit)
7681e8a9af9SDaniel Golle
7691e8a9af9SDaniel Golle
7701e8a9af9SDaniel Golledef check_unit_address_format(ctx):
7711e8a9af9SDaniel Golle    """Unit addresses must be lowercase hex without leading zeros and
7721e8a9af9SDaniel Golle    without a '0x' prefix. For multi-cell addresses (comma-separated),
7731e8a9af9SDaniel Golle    each part is checked independently. A single '0' is permitted
7741e8a9af9SDaniel Golle    (canonical zero)."""
7751e8a9af9SDaniel Golle    for dl in ctx.lines:
7761e8a9af9SDaniel Golle        if dl.linetype != LineType.NODE_OPEN:
7771e8a9af9SDaniel Golle            continue
7781e8a9af9SDaniel Golle        if dl.node_addr is None:
7791e8a9af9SDaniel Golle            continue
7801e8a9af9SDaniel Golle        addr = dl.node_addr
7811e8a9af9SDaniel Golle        for part in addr.split(','):
7821e8a9af9SDaniel Golle            if part[:2] in ('0x', '0X'):
7831e8a9af9SDaniel Golle                yield (dl.lineno,
7841e8a9af9SDaniel Golle                       'unit address %r must not have a "0x" prefix' %
7851e8a9af9SDaniel Golle                       addr)
7861e8a9af9SDaniel Golle                break
7871e8a9af9SDaniel Golle            if not re.match(r'^[0-9a-fA-F]+$', part):
7881e8a9af9SDaniel Golle                yield (dl.lineno,
7891e8a9af9SDaniel Golle                       'unit address %r is not valid hex' % addr)
7901e8a9af9SDaniel Golle                break
7911e8a9af9SDaniel Golle            if any(c in 'ABCDEF' for c in part):
7921e8a9af9SDaniel Golle                yield (dl.lineno,
7931e8a9af9SDaniel Golle                       'unit address %r must be lowercase hex' % addr)
7941e8a9af9SDaniel Golle                break
7951e8a9af9SDaniel Golle            if len(part) > 1 and part.startswith('0'):
7961e8a9af9SDaniel Golle                yield (dl.lineno,
7971e8a9af9SDaniel Golle                       'unit address %r has leading zeros' % addr)
7981e8a9af9SDaniel Golle                break
7991e8a9af9SDaniel Golle
8001e8a9af9SDaniel Golle
8011e8a9af9SDaniel Golledef check_value_whitespace(ctx):
8021e8a9af9SDaniel Golle    """A <...> cell list must have no whitespace directly after '<'
8031e8a9af9SDaniel Golle    or directly before '>'. Continuation lines are joined onto the
8041e8a9af9SDaniel Golle    property so a <...> split across lines is checked too; a '<' or
8051e8a9af9SDaniel Golle    '>' at a line break is glued straight to the neighbouring value,
8061e8a9af9SDaniel Golle    so the break itself is not counted as padding. Outside strings
8071e8a9af9SDaniel Golle    and comments only."""
8081e8a9af9SDaniel Golle    for dl in ctx.lines:
8091e8a9af9SDaniel Golle        if dl.linetype != LineType.PROPERTY:
8101e8a9af9SDaniel Golle            continue
8111e8a9af9SDaniel Golle        segs = [_strip_strings_and_comments(dl.raw).strip()]
8121e8a9af9SDaniel Golle        for cont in dl.continuations:
8131e8a9af9SDaniel Golle            segs.append(_strip_strings_and_comments(cont.stripped).strip())
8141e8a9af9SDaniel Golle        text = ''
8151e8a9af9SDaniel Golle        for s in segs:
8161e8a9af9SDaniel Golle            if not s:
8171e8a9af9SDaniel Golle                continue
8181e8a9af9SDaniel Golle            if not text or text.endswith('<') or s.startswith('>'):
8191e8a9af9SDaniel Golle                text += s
8201e8a9af9SDaniel Golle            else:
8211e8a9af9SDaniel Golle                text += ' ' + s
8221e8a9af9SDaniel Golle        for m in re.finditer(r'<([^<>]*)>', text):
8231e8a9af9SDaniel Golle            content = m.group(1)
8241e8a9af9SDaniel Golle            if content and content != content.strip():
8251e8a9af9SDaniel Golle                yield (dl.lineno, 'extra whitespace inside <...>')
8261e8a9af9SDaniel Golle                break
8271e8a9af9SDaniel Golle
8281e8a9af9SDaniel Golle
8291e8a9af9SDaniel Golledef check_node_close_alone(ctx):
8301e8a9af9SDaniel Golle    """The closing '};' of a node must be on its own line. The
8311e8a9af9SDaniel Golle    classifier accepts a canonical "}" or "};" as NODE_CLOSE; a line
8321e8a9af9SDaniel Golle    that is all closures (e.g. "}; };") is still NODE_CLOSE for depth
8331e8a9af9SDaniel Golle    tracking but is flagged here via dl.closures. Any other line that
8341e8a9af9SDaniel Golle    still contains '};' (in code, not in strings or comments) is
8351e8a9af9SDaniel Golle    mixing a node close with something else."""
8361e8a9af9SDaniel Golle    for dl in ctx.lines:
8371e8a9af9SDaniel Golle        if dl.linetype == LineType.NODE_CLOSE:
8381e8a9af9SDaniel Golle            if dl.closures > 1:
8391e8a9af9SDaniel Golle                yield (dl.lineno,
8401e8a9af9SDaniel Golle                       'closing brace must be on its own line')
8411e8a9af9SDaniel Golle            continue
8421e8a9af9SDaniel Golle        if dl.linetype in (LineType.BLANK, LineType.COMMENT,
8431e8a9af9SDaniel Golle                           LineType.COMMENT_START, LineType.COMMENT_BODY,
8441e8a9af9SDaniel Golle                           LineType.COMMENT_END, LineType.PREPROCESSOR):
8451e8a9af9SDaniel Golle            continue
8461e8a9af9SDaniel Golle        text = _strip_strings_and_comments(dl.raw)
8471e8a9af9SDaniel Golle        if '};' in text:
8481e8a9af9SDaniel Golle            yield (dl.lineno,
8491e8a9af9SDaniel Golle                   'closing brace must be on its own line')
8501e8a9af9SDaniel Golle
8511e8a9af9SDaniel Golle
8521e8a9af9SDaniel Golledef _display_col(text):
8531e8a9af9SDaniel Golle    """Visual column width of text, with tabs expanded to the next
8541e8a9af9SDaniel Golle    8-column stop, matching how printf and most editors render a
8551e8a9af9SDaniel Golle    line and the kernel-wide line length convention."""
8561e8a9af9SDaniel Golle    col = 0
8571e8a9af9SDaniel Golle    for ch in text:
8581e8a9af9SDaniel Golle        if ch == '\t':
8591e8a9af9SDaniel Golle            col = (col // 8 + 1) * 8
8601e8a9af9SDaniel Golle        else:
8611e8a9af9SDaniel Golle            col += 1
8621e8a9af9SDaniel Golle    return col
8631e8a9af9SDaniel Golle
8641e8a9af9SDaniel Golle
8651e8a9af9SDaniel Golledef check_line_length(ctx):
8661e8a9af9SDaniel Golle    """Lines must not exceed 80 columns; tabs count as 8 (see
8671e8a9af9SDaniel Golle    _display_col)."""
8681e8a9af9SDaniel Golle    for dl in ctx.lines:
8691e8a9af9SDaniel Golle        if dl.linetype == LineType.BLANK:
8701e8a9af9SDaniel Golle            continue
8711e8a9af9SDaniel Golle        cols = _display_col(dl.raw)
8721e8a9af9SDaniel Golle        if cols > 80:
8731e8a9af9SDaniel Golle            yield (dl.lineno,
8741e8a9af9SDaniel Golle                   'line exceeds 80 columns (%d)' % cols)
8751e8a9af9SDaniel Golle
8761e8a9af9SDaniel Golle
8771e8a9af9SDaniel Golledef check_continuation_alignment(ctx):
8781e8a9af9SDaniel Golle    """A multi-line property's continuation lines must align their
8791e8a9af9SDaniel Golle    first non-whitespace character to the display column of the first
8801e8a9af9SDaniel Golle    '<' or '"' after the '=' in the leading line. Display columns are
8811e8a9af9SDaniel Golle    used so tab-indented .dts files (where a continuation aligns with
8821e8a9af9SDaniel Golle    tabs plus spaces) are compared correctly."""
8831e8a9af9SDaniel Golle    for dl in ctx.lines:
8841e8a9af9SDaniel Golle        if dl.linetype != LineType.PROPERTY:
8851e8a9af9SDaniel Golle            continue
8861e8a9af9SDaniel Golle        if not dl.continuations:
8871e8a9af9SDaniel Golle            continue
8881e8a9af9SDaniel Golle        eq = dl.raw.find('=')
8891e8a9af9SDaniel Golle        if eq < 0:
8901e8a9af9SDaniel Golle            continue
8911e8a9af9SDaniel Golle        # First '<' or '"' after '='
8921e8a9af9SDaniel Golle        rest = dl.raw[eq + 1:]
8931e8a9af9SDaniel Golle        m = re.search(r'[<"]', rest)
8941e8a9af9SDaniel Golle        if not m:
8951e8a9af9SDaniel Golle            continue
8961e8a9af9SDaniel Golle        target_col = _display_col(dl.raw[:eq + 1 + m.start()])
8971e8a9af9SDaniel Golle        for cont in dl.continuations:
8981e8a9af9SDaniel Golle            if _display_col(cont.indent_str) != target_col:
8991e8a9af9SDaniel Golle                yield (cont.lineno,
9001e8a9af9SDaniel Golle                       'continuation should align to column %d '
9011e8a9af9SDaniel Golle                       '(under "<" or \\")' % (target_col + 1))
9021e8a9af9SDaniel Golle
9031e8a9af9SDaniel Golle
9041e8a9af9SDaniel Golledef check_unclosed_block_comment(ctx):
9051e8a9af9SDaniel Golle    """Every /* must have a matching */ in the same block. Catches both
9061e8a9af9SDaniel Golle    a comment opened on its own line (COMMENT_START) and a tail comment
9071e8a9af9SDaniel Golle    opened on a PROPERTY or other code line (where in_block_comment is
9081e8a9af9SDaniel Golle    set by _split_code so the next line becomes COMMENT_BODY without a
9091e8a9af9SDaniel Golle    preceding COMMENT_START)."""
9101e8a9af9SDaniel Golle    open_lineno = None
9111e8a9af9SDaniel Golle    for dl in ctx.lines:
9121e8a9af9SDaniel Golle        if dl.linetype == LineType.COMMENT_START:
9131e8a9af9SDaniel Golle            open_lineno = dl.lineno
9141e8a9af9SDaniel Golle        elif dl.linetype == LineType.COMMENT_END:
9151e8a9af9SDaniel Golle            open_lineno = None
9161e8a9af9SDaniel Golle        elif dl.linetype == LineType.COMMENT_BODY and open_lineno is None:
9171e8a9af9SDaniel Golle            # Block was opened by a /* tail on a code line; report at
9181e8a9af9SDaniel Golle            # the first orphan body line since the originating line is
9191e8a9af9SDaniel Golle            # already classified as something else.
9201e8a9af9SDaniel Golle            open_lineno = dl.lineno
9211e8a9af9SDaniel Golle    if open_lineno is not None:
9221e8a9af9SDaniel Golle        yield (open_lineno, 'unclosed /* block comment')
9231e8a9af9SDaniel Golle
9241e8a9af9SDaniel Golle
9251e8a9af9SDaniel Golledef check_unused_labels(ctx):
9261e8a9af9SDaniel Golle    """Labels defined but never referenced are clutter."""
9271e8a9af9SDaniel Golle    defined, referenced = collect_labels_and_refs(ctx.text)
9281e8a9af9SDaniel Golle    for label in sorted(defined - referenced):
9291e8a9af9SDaniel Golle        # Find the line where this label is defined for line-number
9301e8a9af9SDaniel Golle        # reporting.
9311e8a9af9SDaniel Golle        m = re.search(r'(?m)^.*\b' + re.escape(label) + r'\s*:', ctx.text)
9321e8a9af9SDaniel Golle        lineno = ctx.text[:m.start()].count('\n') + 1 if m else 1
9331e8a9af9SDaniel Golle        yield (lineno, 'label %r defined but never &-referenced' % label)
9341e8a9af9SDaniel Golle
9351e8a9af9SDaniel Golle
9361e8a9af9SDaniel Golle# --- registry --------------------------------------------------------------
9371e8a9af9SDaniel Golle
9381e8a9af9SDaniel GolleRULES = [
9391e8a9af9SDaniel Golle    # 'relaxed' is the default; rules in this group must produce zero
9401e8a9af9SDaniel Golle    # output on a clean kernel tree (post the small prep-cleanup
9411e8a9af9SDaniel Golle    # commit at the head of this series).
9421e8a9af9SDaniel Golle    Rule('trailing-whitespace', 'relaxed',
9431e8a9af9SDaniel Golle         'no trailing whitespace on any line',
9441e8a9af9SDaniel Golle         check_trailing_whitespace),
94549b5cf46SKrzysztof Kozlowski    Rule('tab-in-yaml', 'relaxed',
94649b5cf46SKrzysztof Kozlowski         'YAML (also DTS examples) may not contain tab characters',
94749b5cf46SKrzysztof Kozlowski         check_tab_in_yaml_example, applies_to=('yaml',)),
9481e8a9af9SDaniel Golle    Rule('mixed-indent-chars', 'relaxed',
9491e8a9af9SDaniel Golle         'indent must not mix tabs and spaces',
95032541ce0SKrzysztof Kozlowski         check_mixed_indent_chars, applies_to=('dts', 'dtsi', 'dtso')),
9511e8a9af9SDaniel Golle    Rule('unclosed-block-comment', 'relaxed',
9521e8a9af9SDaniel Golle         'every /* block comment must close with */',
9531e8a9af9SDaniel Golle         check_unclosed_block_comment),
9541e8a9af9SDaniel Golle
9551e8a9af9SDaniel Golle    # DTS files always use tabs; this is not negotiable per kernel
9561e8a9af9SDaniel Golle    # coding style (.dts files are real source). Relaxed mode.
9571e8a9af9SDaniel Golle    Rule('indent-unit-dts', 'relaxed',
9581e8a9af9SDaniel Golle         'DTS files: 1 tab per nesting level',
9591e8a9af9SDaniel Golle         check_indent_unit_dts,
9601e8a9af9SDaniel Golle         applies_to=('dts', 'dtsi', 'dtso')),
9611e8a9af9SDaniel Golle
9621e8a9af9SDaniel Golle    # 'strict' rules are opt-in (e.g. for new submissions via
9631e8a9af9SDaniel Golle    # checkpatch.pl in a follow-up series). They flag many existing
9641e8a9af9SDaniel Golle    # files and can be promoted to relaxed once those are cleaned up.
9651e8a9af9SDaniel Golle    Rule('indent-unit', 'strict',
9661e8a9af9SDaniel Golle         'YAML: 2 or 4 spaces per level',
9671e8a9af9SDaniel Golle         check_indent_unit_relaxed, applies_to=('yaml',)),
9681e8a9af9SDaniel Golle    Rule('indent-unit-strict', 'strict',
9691e8a9af9SDaniel Golle         'YAML: must be 4 spaces per level',
9701e8a9af9SDaniel Golle         check_indent_unit_strict, applies_to=('yaml',)),
9711e8a9af9SDaniel Golle    Rule('indent-consistent', 'strict',
9721e8a9af9SDaniel Golle         'every line indented at depth * unit',
9731e8a9af9SDaniel Golle         check_indent_consistent),
9741e8a9af9SDaniel Golle    Rule('blank-lines', 'strict',
9751e8a9af9SDaniel Golle         'no consecutive blanks; no blanks at node body edges',
9761e8a9af9SDaniel Golle         check_blank_lines),
9771e8a9af9SDaniel Golle    Rule('child-address-order', 'strict',
9781e8a9af9SDaniel Golle         'addressed siblings must be in ascending address order',
9791e8a9af9SDaniel Golle         check_child_address_order),
9801e8a9af9SDaniel Golle    Rule('child-name-order', 'strict',
9811e8a9af9SDaniel Golle         'unaddressed siblings must be in natural-sort name order',
9821e8a9af9SDaniel Golle         check_child_name_order),
9831e8a9af9SDaniel Golle    Rule('property-order', 'strict',
9841e8a9af9SDaniel Golle         'canonical bucket + pairing + natural-sort order of properties',
9851e8a9af9SDaniel Golle         check_property_order),
9861e8a9af9SDaniel Golle    Rule('required-blank-lines', 'strict',
9871e8a9af9SDaniel Golle         'blank line before child nodes and before "status"',
9881e8a9af9SDaniel Golle         check_required_blank_lines),
9891e8a9af9SDaniel Golle    Rule('hex-case', 'strict',
9901e8a9af9SDaniel Golle         'hex literals must be lowercase',
9911e8a9af9SDaniel Golle         check_hex_case),
9921e8a9af9SDaniel Golle    Rule('unit-address-format', 'strict',
9931e8a9af9SDaniel Golle         'unit addresses must be lowercase hex without leading zeros',
9941e8a9af9SDaniel Golle         check_unit_address_format),
9951e8a9af9SDaniel Golle    Rule('value-whitespace', 'strict',
9961e8a9af9SDaniel Golle         'no whitespace directly inside <...> brackets',
9971e8a9af9SDaniel Golle         check_value_whitespace),
9981e8a9af9SDaniel Golle    Rule('node-close-alone', 'strict',
9991e8a9af9SDaniel Golle         'closing brace must be on its own line',
10001e8a9af9SDaniel Golle         check_node_close_alone),
10011e8a9af9SDaniel Golle    Rule('line-length', 'strict',
10021e8a9af9SDaniel Golle         'lines must not exceed 80 columns',
10031e8a9af9SDaniel Golle         check_line_length),
10041e8a9af9SDaniel Golle    Rule('continuation-alignment', 'strict',
10051e8a9af9SDaniel Golle         'multi-line property continuations align under "<" or "\\""',
10061e8a9af9SDaniel Golle         check_continuation_alignment),
10071e8a9af9SDaniel Golle    Rule('unused-labels', 'strict',
10081e8a9af9SDaniel Golle         'every label must be &-referenced in the same example/file '
10091e8a9af9SDaniel Golle         '(skipped for .dtsi/.dtso since labels there are exported)',
10101e8a9af9SDaniel Golle         check_unused_labels, applies_to=('yaml', 'dts')),
10111e8a9af9SDaniel Golle]
10121e8a9af9SDaniel Golle
10131e8a9af9SDaniel Golle
10141e8a9af9SDaniel Golledef select_rules(mode, input_kind):
10151e8a9af9SDaniel Golle    """Return rules that apply to the given mode and input type."""
10161e8a9af9SDaniel Golle    rank = {'relaxed': 0, 'strict': 1}
10171e8a9af9SDaniel Golle    out = []
10181e8a9af9SDaniel Golle    for r in RULES:
10191e8a9af9SDaniel Golle        if rank[r.mode] > rank[mode]:
10201e8a9af9SDaniel Golle            continue
10211e8a9af9SDaniel Golle        if input_kind not in r.applies_to:
10221e8a9af9SDaniel Golle            continue
10231e8a9af9SDaniel Golle        out.append(r)
10241e8a9af9SDaniel Golle    return out
10251e8a9af9SDaniel Golle
10261e8a9af9SDaniel Golle
10271e8a9af9SDaniel Golle# ---------------------------------------------------------------------------
10281e8a9af9SDaniel Golle# Block runner
10291e8a9af9SDaniel Golle# ---------------------------------------------------------------------------
10301e8a9af9SDaniel Golle
103146fb56e4SKrzysztof Kozlowskidef check_block(text, mode, input_type):
10321e8a9af9SDaniel Golle    """Run all selected rules on a single block of DTS text. Returns a
10331e8a9af9SDaniel Golle    list of (lineno, rule_name, message) tuples."""
10341e8a9af9SDaniel Golle    lines = classify_lines(text)
103546fb56e4SKrzysztof Kozlowski    ctx = Ctx(lines, text, mode, input_type)
10361e8a9af9SDaniel Golle    rules = select_rules(mode, input_type)
10371e8a9af9SDaniel Golle    findings = []
10381e8a9af9SDaniel Golle    for r in rules:
10391e8a9af9SDaniel Golle        for lineno, msg in r.check(ctx):
10401e8a9af9SDaniel Golle            findings.append((lineno, r.name, msg))
10411e8a9af9SDaniel Golle    findings.sort(key=lambda t: (t[0], t[1]))
10421e8a9af9SDaniel Golle    return findings
10431e8a9af9SDaniel Golle
10441e8a9af9SDaniel Golle
10451e8a9af9SDaniel Golle# ---------------------------------------------------------------------------
10461e8a9af9SDaniel Golle# Input drivers (YAML examples vs raw DTS)
10471e8a9af9SDaniel Golle# ---------------------------------------------------------------------------
10481e8a9af9SDaniel Golle
10491e8a9af9SDaniel Golledef _yaml_loader():
10501e8a9af9SDaniel Golle    return ruamel.yaml.YAML()
10511e8a9af9SDaniel Golle
10521e8a9af9SDaniel Golle
10531e8a9af9SDaniel Golledef iter_yaml_examples(filepath):
10541e8a9af9SDaniel Golle    """Yield (example_text, base_lineno_in_file, example_index) tuples."""
10551e8a9af9SDaniel Golle    yaml = _yaml_loader()
10561e8a9af9SDaniel Golle    try:
10571e8a9af9SDaniel Golle        with open(filepath, encoding='utf-8') as f:
10581e8a9af9SDaniel Golle            data = yaml.load(f)
10591e8a9af9SDaniel Golle    except Exception as e:
10601e8a9af9SDaniel Golle        print('%s: error loading YAML: %s' % (filepath, e),
10611e8a9af9SDaniel Golle              file=sys.stderr)
10621e8a9af9SDaniel Golle        return
10631e8a9af9SDaniel Golle    if not isinstance(data, dict) or 'examples' not in data:
10641e8a9af9SDaniel Golle        return
10651e8a9af9SDaniel Golle    examples = data['examples']
10661e8a9af9SDaniel Golle    if not hasattr(examples, '__iter__'):
10671e8a9af9SDaniel Golle        return
10681e8a9af9SDaniel Golle    for i, ex in enumerate(examples):
10691e8a9af9SDaniel Golle        if not isinstance(ex, str):
10701e8a9af9SDaniel Golle            continue
10711e8a9af9SDaniel Golle        try:
10721e8a9af9SDaniel Golle            base = examples.lc.item(i)[0] + 2
10731e8a9af9SDaniel Golle        except Exception:
10741e8a9af9SDaniel Golle            base = 1
10751e8a9af9SDaniel Golle        yield (str(ex), base, i)
10761e8a9af9SDaniel Golle
10771e8a9af9SDaniel Golle
10781e8a9af9SDaniel Golledef iter_dts_file(filepath):
10791e8a9af9SDaniel Golle    """Treat the whole file as a single block."""
10801e8a9af9SDaniel Golle    try:
10811e8a9af9SDaniel Golle        with open(filepath, encoding='utf-8') as f:
10821e8a9af9SDaniel Golle            text = f.read()
10831e8a9af9SDaniel Golle    except Exception as e:
10841e8a9af9SDaniel Golle        print('%s: error reading: %s' % (filepath, e), file=sys.stderr)
10851e8a9af9SDaniel Golle        return
10861e8a9af9SDaniel Golle    yield (text, 1, None)
10871e8a9af9SDaniel Golle
10881e8a9af9SDaniel Golle
10891e8a9af9SDaniel Golle# ---------------------------------------------------------------------------
10901e8a9af9SDaniel Golle# Top-level processing
10911e8a9af9SDaniel Golle# ---------------------------------------------------------------------------
10921e8a9af9SDaniel Golle
10931e8a9af9SDaniel Golledef input_kind(filepath):
10941e8a9af9SDaniel Golle    p = filepath.lower()
10951e8a9af9SDaniel Golle    if p.endswith('.yaml') or p.endswith('.yml'):
10961e8a9af9SDaniel Golle        return 'yaml'
10971e8a9af9SDaniel Golle    if p.endswith('.dts'):
10981e8a9af9SDaniel Golle        return 'dts'
10991e8a9af9SDaniel Golle    if p.endswith('.dtsi'):
11001e8a9af9SDaniel Golle        return 'dtsi'
11011e8a9af9SDaniel Golle    if p.endswith('.dtso'):
11021e8a9af9SDaniel Golle        return 'dtso'
11031e8a9af9SDaniel Golle    return None
11041e8a9af9SDaniel Golle
11051e8a9af9SDaniel Golle
11061e8a9af9SDaniel Golle# All input types that use tab indentation and follow DTS coding style.
11071e8a9af9SDaniel GolleDTS_FAMILY = ('dts', 'dtsi', 'dtso')
11081e8a9af9SDaniel Golle
11091e8a9af9SDaniel Golle
11101e8a9af9SDaniel Golledef collect_findings(filepath, mode):
11111e8a9af9SDaniel Golle    """Return a (lines, count) pair for filepath. lines is a list of
11121e8a9af9SDaniel Golle    formatted output strings; count is the number of findings."""
11131e8a9af9SDaniel Golle    kind = input_kind(filepath)
11141e8a9af9SDaniel Golle    if kind == 'yaml':
11151e8a9af9SDaniel Golle        iterator = iter_yaml_examples(filepath)
11161e8a9af9SDaniel Golle    elif kind in DTS_FAMILY:
11171e8a9af9SDaniel Golle        iterator = iter_dts_file(filepath)
11181e8a9af9SDaniel Golle    else:
11191e8a9af9SDaniel Golle        return (['%s: unknown file type, skipping' % filepath], 0)
11201e8a9af9SDaniel Golle
11211e8a9af9SDaniel Golle    out = []
11221e8a9af9SDaniel Golle    for text, base, idx in iterator:
112346fb56e4SKrzysztof Kozlowski        for lineno, rule, msg in check_block(text, mode, kind):
11241e8a9af9SDaniel Golle            abs_line = base + lineno - 1
11251e8a9af9SDaniel Golle            ex_tag = '' if idx is None else ' example %d' % idx
11261e8a9af9SDaniel Golle            out.append('%s:%d:%s [%s] %s' %
11271e8a9af9SDaniel Golle                       (filepath, abs_line, ex_tag, rule, msg))
11281e8a9af9SDaniel Golle    return (out, len(out))
11291e8a9af9SDaniel Golle
11301e8a9af9SDaniel Golle
11311e8a9af9SDaniel Golle# Worker entry point for ProcessPoolExecutor.map(). Top-level so it is
11321e8a9af9SDaniel Golle# picklable on every platform.
11331e8a9af9SDaniel Golledef _worker(args):
11341e8a9af9SDaniel Golle    filepath, mode = args
11351e8a9af9SDaniel Golle    return collect_findings(filepath, mode)
11361e8a9af9SDaniel Golle
11371e8a9af9SDaniel Golle
11381e8a9af9SDaniel Golledef main():
11391e8a9af9SDaniel Golle    import os
11401e8a9af9SDaniel Golle    ap = argparse.ArgumentParser(
11411e8a9af9SDaniel Golle        description='Check DTS coding style on YAML examples and '
11421e8a9af9SDaniel Golle        '.dts/.dtsi/.dtso files.',
11431e8a9af9SDaniel Golle        fromfile_prefix_chars='@')
11441e8a9af9SDaniel Golle    ap.add_argument('--mode', choices=('relaxed', 'strict'),
11451e8a9af9SDaniel Golle                    default='relaxed',
11461e8a9af9SDaniel Golle                    help='which rule set to apply (default: relaxed)')
11471e8a9af9SDaniel Golle    ap.add_argument('-j', '--jobs', type=int, default=0,
11481e8a9af9SDaniel Golle                    metavar='N',
11491e8a9af9SDaniel Golle                    help='run N workers in parallel (default: respect '
11501e8a9af9SDaniel Golle                    'the make jobserver via $PARALLELISM, otherwise '
11511e8a9af9SDaniel Golle                    'os.cpu_count(); use 1 to disable multiprocessing)')
11521e8a9af9SDaniel Golle    ap.add_argument('--list-rules', action='store_true',
11531e8a9af9SDaniel Golle                    help='print all rules with their mode and exit')
11541e8a9af9SDaniel Golle    ap.add_argument('files', nargs='*', metavar='file',
11551e8a9af9SDaniel Golle                    help='YAML binding files or .dts/.dtsi/.dtso files; '
11561e8a9af9SDaniel Golle                    'use @argfile to read paths from a file')
11571e8a9af9SDaniel Golle    args = ap.parse_args()
11581e8a9af9SDaniel Golle
11591e8a9af9SDaniel Golle    if args.list_rules:
11601e8a9af9SDaniel Golle        for r in RULES:
11611e8a9af9SDaniel Golle            applies = ','.join(r.applies_to)
11621e8a9af9SDaniel Golle            print('%-22s %-7s [%s] %s' %
11631e8a9af9SDaniel Golle                  (r.name, r.mode, applies, r.description))
11641e8a9af9SDaniel Golle        return 0
11651e8a9af9SDaniel Golle
11661e8a9af9SDaniel Golle    if not args.files:
11671e8a9af9SDaniel Golle        ap.error('no input files')
11681e8a9af9SDaniel Golle
11691e8a9af9SDaniel Golle    if args.jobs > 0:
11701e8a9af9SDaniel Golle        jobs = args.jobs
11711e8a9af9SDaniel Golle    else:
11721e8a9af9SDaniel Golle        # When invoked under scripts/jobserver-exec, $PARALLELISM
11731e8a9af9SDaniel Golle        # holds the slot count make has reserved for us; this lets
11741e8a9af9SDaniel Golle        # `make -j N dt_binding_check` constrain our worker pool to N.
11751e8a9af9SDaniel Golle        try:
11761e8a9af9SDaniel Golle            jobs = int(os.environ['PARALLELISM'])
11771e8a9af9SDaniel Golle        except (KeyError, ValueError):
11781e8a9af9SDaniel Golle            jobs = os.cpu_count() or 1
11791e8a9af9SDaniel Golle    # Single-process path: keep import surface small for tests and
11801e8a9af9SDaniel Golle    # easy debugging.
11811e8a9af9SDaniel Golle    if jobs == 1 or len(args.files) == 1:
11821e8a9af9SDaniel Golle        total = 0
11831e8a9af9SDaniel Golle        for f in args.files:
11841e8a9af9SDaniel Golle            lines, n = collect_findings(f, args.mode)
11851e8a9af9SDaniel Golle            for line in lines:
11861e8a9af9SDaniel Golle                print(line, file=sys.stderr)
11871e8a9af9SDaniel Golle            total += n
11881e8a9af9SDaniel Golle        return 1 if total else 0
11891e8a9af9SDaniel Golle
11901e8a9af9SDaniel Golle    # Multi-process path. ex.map preserves input order so output is
11911e8a9af9SDaniel Golle    # deterministic across runs.
11921e8a9af9SDaniel Golle    from concurrent.futures import ProcessPoolExecutor
11931e8a9af9SDaniel Golle    total = 0
11941e8a9af9SDaniel Golle    work = [(f, args.mode) for f in args.files]
11951e8a9af9SDaniel Golle    chunk = max(1, len(work) // (jobs * 8)) if work else 1
11961e8a9af9SDaniel Golle    with ProcessPoolExecutor(max_workers=jobs) as ex:
11971e8a9af9SDaniel Golle        for lines, n in ex.map(_worker, work, chunksize=chunk):
11981e8a9af9SDaniel Golle            for line in lines:
11991e8a9af9SDaniel Golle                print(line, file=sys.stderr)
12001e8a9af9SDaniel Golle            total += n
12011e8a9af9SDaniel Golle    return 1 if total else 0
12021e8a9af9SDaniel Golle
12031e8a9af9SDaniel Golle
12041e8a9af9SDaniel Golleif __name__ == '__main__':
12051e8a9af9SDaniel Golle    sys.exit(main())
1206