Skip to contentSkip to conversation

dt-bindings: automated coding style check for DTS examples

PATCH Applied Daniel Golle 5 participants 12 messages May 27 – Jun 13

Subsystems (3)
lore
Share & tools
lore
#0 lore raw

v5 cover dt-bindings: automated coding style check for DTS examples

Documentation/devicetree/bindings/dts-coding-style.rst documents the style expected of DT examples and source files, but nothing existing actually enforces it. dtc tolerates arbitrary whitespace, yamllint only sees the YAML envelope and not the DTS content inside literal block scalars, and so reviewers spend cycles flagging issues that tooling could catch.

Add scripts/dtc/dt-check-style, a Python tool that checks DTS coding style on YAML binding examples and on .dts/.dtsi/.dtso source files. Rules live in a small declarative registry; each rule is tagged 'relaxed' (default) or 'strict' (opt-in for new submissions). Promoting a rule from strict to relaxed is a one-line edit once the tree catches up.

Relaxed mode covers trailing whitespace, tab characters in YAML examples, mixed tab+space indents, unclosed /* block comments, and the per-input-type indent character (1 tab for .dts/.dtsi/.dtso). It is zero-violation on the current tree and runs as part of dt_binding_check_one, so no new warnings appear in make dt_binding_check.

Strict mode adds indent unit and depth consistency; blank-line policy (no consecutive blanks or blanks at body edges; required blank line before each child node and before "status"); property ordering by canonical bucket (compatible, reg/reg-names, ranges, standard, vendor, status) with declarative within-bucket pairing rules (<x>-names follows <x>, pinctrl-names follows the last pinctrl-N) and natural-sort fallback; sibling node ordering by unit address or natural-sort name; line length (80 columns); continuation alignment of multi-line property values under the first '<' or '"' after the '='; lowercase hex literals and unit addresses; no whitespace padding inside <...>; closing-brace placement; and unused-label detection (skipped for .dtsi/.dtso since labels there are exported to includers/applies-to). Together these constrain a DT structure to a single canonical rendering modulo the author's choice of when to wrap properties for readability. Comments are intentionally out of scope. Strict mode is opt-in, intended for use by checkpatch.pl in a follow-up series.

The tool reads file paths from @argfile and parallelises across CPUs via -j N. With no -j given it picks up $PARALLELISM (set by scripts/jobserver-exec from the GNU make jobserver) and falls back to os.cpu_count() otherwise. Running as one Python invocation amortises the ruamel.yaml import across the whole tree -- ~2s on a 32-CPU host vs ~28s sequential. ruamel.yaml is the only non-stdlib dependency, already required by dtschema.

A selftest under scripts/dtc/dt-style-selftest/ pairs good/ and bad/ fixtures with expected output so rule behaviour can be exercised independently of the full tree via the dt_style_selftest top-level make target.
---
v1: https://lore.kernel.org/all/[email protected]/
v2: https://lore.kernel.org/all/[email protected]/
v3: https://lore.kernel.org/all/[email protected]/
v4: https://lore.kernel.org/all/[email protected]/

Changes since v4:
- dt-check-style: parser robustness fixes from a sashiko.dev AI review (multi-line CPP macros, "/* ... */ code" on one line, "}; };" depth tracking, malformed unit addresses, labels inside strings and on digit-leading node names, unclosed /* on PROPERTY lines)
- dt-check-style: new relaxed-mode rule unclosed-block-comment
- new patch: scripts/jobserver-exec propagates child exit status (prereq so chk_style errors stay reported)
- selftest: add fixtures covering the parser fixes
- selftest: move SPDX header to "// SPDX-..." on line 1 in the three .dts fixtures (checkpatch)

Changes since v3:
- dt-check-style: accept node and property names that begin with a digit (the DT spec permits names like 1wire@10)
- dt-check-style: strip trailing // and /* */ comments before line classification, so a trailing comment on a node-open or property line no longer causes misclassification
- dt-check-style: continuation-alignment compares display columns (tabs expanded to 8) instead of raw string length, removing many false positives on tab-indented .dts files
- dt-check-style: value-whitespace flags only whitespace directly inside the brackets (after '<', before '>'), on single- and multi-line cell lists; it no longer flags inter-value spacing, which is often deliberate column alignment
- Makefile: build the @argfile with mktemp and remove it, so no stale .tmp_.dt-style.checked is left in the build tree
- run.sh: drop bash-only process substitution (POSIX sh now) and check the checker's exit status
- selftest: add fixtures for the above (digit-leading nodes, trailing comments, multi-line cell arrays, tab+space aligned continuations) and update yaml-value-ws for the reworked value-whitespace rule

Changes since v2:
- route findings to stderr so a quiet dt_binding_check produces no output (Rob)
- switch the Makefile recipe from mktemp to Kbuild's $(tmp-target) so build output stays inside the build folder (Nathan)
- collapse the recipe's exit-handling tail into the familiar "&& touch $@ || true" pattern, matching cmd_chk_bindings; keeps future warnings non-fatal (Rob, Nathan)
- explain in patch 3/4 why the recipe uses an explicit $(PYTHON3) prefix where the neighbouring recipes call their scripts directly (Rob)
- append a trailing newline to every expected/*.txt selftest fixture (Rob)
- restore the trailing whitespace inside yaml-trailing-ws.yaml that had been silently stripped during re-application, so the selftest actually exercises the trailing-whitespace rule

Changes since v1:
- rules declared in a registry tagged relaxed/strict; default relaxed mode is zero-violation on the current tree
- tool now also accepts .dts/.dtsi/.dtso files directly (with a tab-indent variant); unused-labels skipped for .dtsi/.dtso where labels are exported to includers/applies-to
- renamed dt-check-example-style -> dt-check-style
- added -j N with $PARALLELISM (jobserver) awareness
- dropped node-name [a-z0-9-] check (Rob: better as a meta-schema)
- property-order rebuilt around buckets + declarative pairing rules plus natural-sort fallback
- added child-name-order, required-blank-lines, hex-case, unit-address-format, value-whitespace, node-close-alone, line-length and continuation-alignment to strict mode
- new selftest patch (Krzysztof: "would be happy to see at least a few test cases for it")

Default relaxed mode is zero-violation on the current tree.

Strict-mode violation counts on a current tree (5506 YAML bindings, 6530 in-tree .dts/.dtsi/.dtso under arch/):

rule yaml dts
property-order 14554 245000

  continuation-alignment   1857    93536
  indent-consistent        1562    48720
  required-blank-lines     1876    48563
  line-length               105    26322
  child-name-order          667    16821
  mixed-indent-chars          0    13294
  unused-labels            3611    11628
  child-address-order        63     4076
  blank-lines                82     1866
  node-close-alone           38      774
  value-whitespace           38      715
  hex-case                   78      669
  indent-unit-dts            --      190
  unit-address-format         5       39
  trailing-whitespace         0       10
  indent-unit-strict       1492       --
  indent-unit               179       --

(YAML and DTS columns reflect the rules that apply to each input type; e.g. indent-unit-strict is YAML-only since .dts files use tabs, indent-unit-dts is the equivalent DTS-only check. unused-labels is skipped for .dtsi/.dtso since labels there are exported to includers/applies-to.)

The script was written with generous help from Claude Opus 4.7, since my Python is even worse than my DTS coding style.

Daniel Golle (4):
dt-bindings: add DTS style checker scripts/jobserver-exec: propagate child exit status
dt-bindings: wire style checker into dt_binding_check
dt-bindings: add self-test fixtures for style checker

 Documentation/devicetree/bindings/Makefile    |   19 +-
 Makefile                                      |    6 +
 scripts/dtc/dt-check-style                    | 1192 +++++++++++++++++
 .../dtc/dt-style-selftest/bad/dts-spaces.dts  |   12 +
 .../bad/yaml-child-addr-order.yaml            |   41 +
 .../bad/yaml-child-name-order.yaml            |   37 +
 .../bad/yaml-cont-align.yaml                  |   30 +
 .../bad/yaml-digit-node-order.yaml            |   37 +
 .../dt-style-selftest/bad/yaml-hex-case.yaml  |   29 +
 .../bad/yaml-indent-strict.yaml               |   29 +
 .../bad/yaml-label-in-string.yaml             |   30 +
 .../bad/yaml-line-length.yaml                 |   29 +
 .../bad/yaml-mixed-indent.yaml                |   29 +
 .../bad/yaml-multi-close.yaml                 |   35 +
 .../bad/yaml-node-close.yaml                  |   31 +
 .../bad/yaml-prop-order.yaml                  |   29 +
 .../bad/yaml-prop-pairing.yaml                |   33 +
 .../bad/yaml-required-blank.yaml              |   33 +
 .../dtc/dt-style-selftest/bad/yaml-tab.yaml   |   29 +
 .../bad/yaml-trailing-comment.yaml            |   26 +
 .../bad/yaml-trailing-ws.yaml                 |   29 +
 .../bad/yaml-unclosed-comment.yaml            |   30 +
 .../bad/yaml-unit-addr-prefix.yaml            |   29 +
 .../dt-style-selftest/bad/yaml-unit-addr.yaml |   29 +
 .../bad/yaml-unused-label.yaml                |   29 +
 .../bad/yaml-value-ws-multiline.yaml          |   27 +
 .../dt-style-selftest/bad/yaml-value-ws.yaml  |   29 +
 .../expected/dts-spaces.dts.txt               |    2 +
 .../expected/yaml-child-addr-order.yaml.txt   |    2 +
 .../expected/yaml-child-name-order.yaml.txt   |    2 +
 .../expected/yaml-cont-align.yaml.txt         |    2 +
 .../expected/yaml-digit-node-order.yaml.txt   |    2 +
 .../expected/yaml-hex-case.yaml.txt           |    2 +
 .../expected/yaml-indent-strict.yaml.txt      |    2 +
 .../expected/yaml-label-in-string.yaml.txt    |    2 +
 .../expected/yaml-line-length.yaml.txt        |    2 +
 .../expected/yaml-mixed-indent.yaml.txt       |    3 +
 .../expected/yaml-multi-close.yaml.txt        |    3 +
 .../expected/yaml-node-close.yaml.txt         |    2 +
 .../expected/yaml-prop-order.yaml.txt         |    2 +
 .../expected/yaml-prop-pairing.yaml.txt       |    3 +
 .../expected/yaml-required-blank.yaml.txt     |    3 +
 .../expected/yaml-tab.yaml.txt                |    2 +
 .../expected/yaml-trailing-comment.yaml.txt   |    2 +
 .../expected/yaml-trailing-ws.yaml.txt        |    2 +
 .../expected/yaml-unclosed-comment.yaml.txt   |    2 +
 .../expected/yaml-unit-addr-prefix.yaml.txt   |    2 +
 .../expected/yaml-unit-addr.yaml.txt          |    2 +
 .../expected/yaml-unused-label.yaml.txt       |    2 +
 .../expected/yaml-value-ws-multiline.yaml.txt |    2 +
 .../expected/yaml-value-ws.yaml.txt           |    2 +
 .../dt-style-selftest/good/dts-cont-align.dts |   26 +
 .../dtc/dt-style-selftest/good/dts-tab.dts    |   29 +
 .../dt-style-selftest/good/yaml-4space.yaml   |   41 +
 .../good/yaml-tricky-parsing.yaml             |   57 +
 scripts/dtc/dt-style-selftest/run.sh          |   71 +
 scripts/jobserver-exec                        |    4 +-
 57 files changed, 2214 insertions(+), 4 deletions(-)
 create mode 100755 scripts/dtc/dt-check-style
 create mode 100644 scripts/dtc/dt-style-selftest/bad/dts-spaces.dts
 create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-child-addr-order.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-child-name-order.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-cont-align.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-digit-node-order.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-hex-case.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-indent-strict.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-label-in-string.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-line-length.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-mixed-indent.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-multi-close.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-node-close.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-prop-order.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-prop-pairing.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-required-blank.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-tab.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-trailing-comment.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-trailing-ws.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-unclosed-comment.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-unit-addr-prefix.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-unit-addr.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-unused-label.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-value-ws-multiline.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-value-ws.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/expected/dts-spaces.dts.txt
 create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-child-addr-order.yaml.txt
 create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-child-name-order.yaml.txt
 create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-cont-align.yaml.txt
 create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-digit-node-order.yaml.txt
 create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-hex-case.yaml.txt
 create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-indent-strict.yaml.txt
 create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-label-in-string.yaml.txt
 create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-line-length.yaml.txt
 create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-mixed-indent.yaml.txt
 create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-multi-close.yaml.txt
 create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-node-close.yaml.txt
 create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-prop-order.yaml.txt
 create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-prop-pairing.yaml.txt
 create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-required-blank.yaml.txt
 create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-tab.yaml.txt
 create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-trailing-comment.yaml.txt
 create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-trailing-ws.yaml.txt
 create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-unclosed-comment.yaml.txt
 create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-unit-addr-prefix.yaml.txt
 create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-unit-addr.yaml.txt
 create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-unused-label.yaml.txt
 create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-value-ws-multiline.yaml.txt
 create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-value-ws.yaml.txt
 create mode 100644 scripts/dtc/dt-style-selftest/good/dts-cont-align.dts
 create mode 100644 scripts/dtc/dt-style-selftest/good/dts-tab.dts
 create mode 100644 scripts/dtc/dt-style-selftest/good/yaml-4space.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/good/yaml-tricky-parsing.yaml
 create mode 100755 scripts/dtc/dt-style-selftest/run.sh
Signature · 1 line
2.54.0
#1 lore raw

PATCH v5 1/4 dt-bindings: add DTS style checker

Add a Python tool that checks DTS coding style on examples in YAML binding files and on .dts/.dtsi/.dtso source files. Rules are kept in a small declarative registry, each tagged 'relaxed' (default; must be zero-violation on the current tree) or 'strict' (opt-in for new submissions). Promoting a rule from strict to relaxed is a one-line edit once the tree is clean.

Relaxed mode covers trailing whitespace, tab characters in YAML examples, mixed tab+space indents, and missing tabs in .dts files. Strict adds indent unit and consistency checks, blank-line placement, sibling address ordering, "compatible" and "reg" ordering, and unused labels.

The tool reads file paths from @argfile and parallelises across CPUs via -j N. With no -j given it picks up $PARALLELISM (set by scripts/jobserver-exec from the GNU make jobserver) and falls back to os.cpu_count() otherwise. Running as one Python invocation amortises the ruamel.yaml import across the whole tree -- ~2s on a 32-CPU host vs ~28s sequential.

---
v5:
- parser robustness fixes from a sashiko.dev AI review:
multi-line CPP macros, "/* ... */ code" on one line, "}; };" depth tracking, loosen the unit-address regex and harden check_unit_address_format(), strip strings in collect_labels_and_refs() and admit digit-leading node names, strip unclosed "/*" in _strip_strings_and_comments()
- new relaxed-mode rule unclosed-block-comment

v4:
- node and property name regexes now accept a leading digit; the DT spec permits node names like 1wire@10 or 3d-engine@20
- classify_lines() strips trailing // and /* */ comments before the structural endswith() checks (new _split_code() helper), so a line such as "node { /* c */" or "prop = <1>; // c" is no longer misclassified as a property or continuation
- continuation-alignment now compares display columns (tabs expanded to 8) instead of raw string length, so tab-and-space aligned .dts continuation lines are not falsely flagged; the column helper is shared with line-length
- value-whitespace flags only whitespace directly inside the brackets (after '<', before '>') and checks single- and multi-line cell lists; it no longer flags inter-value spacing

v3:
- route findings output from stdout to stderr so a quiet dt_binding_check produces no output (Rob)

v2:
- renamed dt-check-example-style -> dt-check-style; tool now also accepts .dts/.dtsi/.dtso files directly (tab-indent variant) and distinguishes .dts/.dtsi/.dtso so unused-labels skips .dtsi/.dtso where labels are exported to includers/applies-to
- rules declared in a registry tagged relaxed/strict; default relaxed mode is zero-violation on the current tree
- added -j N with $PARALLELISM (jobserver) awareness
- dropped node-name [a-z0-9-] check (Rob: better as a meta-schema)
- property-order rebuilt around buckets + declarative pairing rules (<x>-names after <x>, pinctrl-names after last pinctrl-N) plus natural-sort fallback
- added child-name-order, required-blank-lines, hex-case, unit-address-format, value-whitespace, node-close-alone, line-length and continuation-alignment to strict mode

 scripts/dtc/dt-check-style | 1192 ++++++++++++++++++++++++++++++++++++
 1 file changed, 1192 insertions(+)
 create mode 100755 scripts/dtc/dt-check-style
scripts/dtc/dt-check-style new file +1192 -0
new file mode 100755
index 000000000000..2d5723d41ea3
@@ -0,0 +1,1192 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# Check DTS coding style on YAML binding examples and on
+# .dts/.dtsi/.dtso source files. Enforces rules from
+# Documentation/devicetree/bindings/dts-coding-style.rst.
+#
+# Two modes:
+#   --mode=relaxed (default)
+#     Only rules that produce zero warnings on the current tree.
+#     Suitable for dt_binding_check.
+#   --mode=strict
+#     All rules. Required for new submissions.
+#
+# Two input types (auto-detected by file extension):
+#   *.yaml             -- DT binding; check each example block
+#   *.dts/*.dtsi/*.dtso -- DTS source; whole file is one block
+#
+# Rules are declared in a registry (see RULES below); each rule is
+# tagged with the lowest mode that runs it. Promoting a rule from
+# 'strict' to 'relaxed' is a one-line change.
+
+import argparse
+import re
+import sys
+from enum import Enum, auto
+
+import ruamel.yaml
+
+
+# ---------------------------------------------------------------------------
+# Line classification
+# ---------------------------------------------------------------------------
+
+class LineType(Enum):
+    BLANK = auto()
+    COMMENT = auto()         # // ... or /* ... */ on one line
+    COMMENT_START = auto()   # /* without closing */
+    COMMENT_BODY = auto()    # inside a multi-line comment
+    COMMENT_END = auto()     # closing */
+    PREPROCESSOR = auto()    # #include / #define / #ifdef / ...
+    NODE_OPEN = auto()       # something { (with optional label/name/addr)
+    NODE_CLOSE = auto()      # };
+    PROPERTY = auto()        # name = value; or name;
+    CONTINUATION = auto()    # continuation of a multi-line property
+
+
+re_cpp_directive = re.compile(
+    r'^#\s*(include|define|undef|ifdef|ifndef|if|else|elif|endif|'
+    r'pragma|error|warning)\b')
+
+# label: name@addr {  -- label and addr optional; name can be "/"
+# Per the DT spec a node name may start with a digit (e.g. 1wire@...).
+# The address part is captured loosely (any non-space, non-brace run) so
+# malformed addresses (e.g. memory@0x1000) still reach
+# check_unit_address_format() instead of silently bypassing the check.
+re_node_header = re.compile(
+    r'^(?:([a-zA-Z_][a-zA-Z0-9_]*):\s*)?'
+    r'([a-zA-Z0-9][a-zA-Z0-9,._+-]*|/)'
+    r'(?:@([^\s{]+))?'
+    r'\s*\{$')
+
+re_ref_node = re.compile(
+    r'^&([a-zA-Z_][a-zA-Z0-9_]*)\s*\{$')
+
+
+def is_preprocessor(stripped):
+    """Tell C preprocessor directives apart from DTS '#'-prefixed props."""
+    return re_cpp_directive.match(stripped) is not None
+
+
+class DtsLine:
+    __slots__ = ('lineno', 'raw', 'linetype', 'indent_str', 'stripped',
+                 'prop_name', 'continuations',
+                 'node_name', 'node_addr', 'label', 'ref_name', 'depth',
+                 'closures')
+
+    def __init__(self, lineno, raw, linetype, indent_str, stripped):
+        self.lineno = lineno      # 1-based within the block
+        self.raw = raw
+        self.linetype = linetype
+        self.indent_str = indent_str  # leading whitespace as-is
+        self.stripped = stripped
+        self.prop_name = None
+        self.continuations = []
+        self.node_name = None
+        self.node_addr = None
+        self.label = None
+        self.ref_name = None
+        self.depth = 0            # filled in by classify_lines
+        self.closures = 1         # count of '}' on a NODE_CLOSE line
+
+
+def _split_code(text):
+    """Return (code, opens_block) for a leading-stripped line: the
+    code portion with // and /* */ comments removed (string literals
+    kept verbatim), and whether a /* */ block comment is left open.
+    The code portion is right-stripped so the endswith() checks in
+    classify_lines see code only, not a trailing comment or blanks."""
+    out = []
+    i = 0
+    n = len(text)
+    while i < n:
+        c = text[i]
+        if c == '"':
+            j = i + 1
+            while j < n:
+                if text[j] == '\\':
+                    j += 2
+                    continue
+                if text[j] == '"':
+                    j += 1
+                    break
+                j += 1
+            out.append(text[i:j])
+            i = j
+            continue
+        if c == '/' and i + 1 < n and text[i + 1] == '/':
+            break
+        if c == '/' and i + 1 < n and text[i + 1] == '*':
+            end = text.find('*/', i + 2)
+            if end < 0:
+                return (''.join(out).rstrip(), True)
+            i = end + 2
+            continue
+        out.append(c)
+        i += 1
+    return (''.join(out).rstrip(), False)
+
+
+re_only_closures = re.compile(r'(?:\}\s*;?\s*)+$')
+
+
+def classify_lines(text):
+    """Return a list of DtsLine. Tracks { } depth and groups
+    continuation lines onto their leading PROPERTY line."""
+    out = []
+    in_block_comment = False
+    in_cpp_macro = False
+    prev_complete = True
+    depth = 0
+
+    # Split preserving the indent string verbatim
+    re_lead = re.compile(r'^([ \t]*)(.*)$')
+
+    for i, raw in enumerate(text.split('\n'), start=1):
+        m = re_lead.match(raw)
+        indent_str = m.group(1)
+        stripped = m.group(2)
+
+        # Continuation of a multi-line C preprocessor directive: the
+        # previous PREPROCESSOR line ended with a '\\' line splice, so
+        # this line is part of the same macro. Treat it as
+        # PREPROCESSOR until the splice chain ends (no trailing '\\'
+        # or a blank line).
+        if in_cpp_macro:
+            dl = DtsLine(i, raw, LineType.PREPROCESSOR,
+                         indent_str, stripped)
+            dl.depth = depth
+            out.append(dl)
+            in_cpp_macro = (bool(stripped) and
+                            stripped.rstrip().endswith('\\'))
+            continue
+
+        if not stripped:
+            dl = DtsLine(i, raw, LineType.BLANK, '', '')
+            dl.depth = depth
+            out.append(dl)
+            continue
+
+        if in_block_comment:
+            ltype = (LineType.COMMENT_END if '*/' in stripped
+                     else LineType.COMMENT_BODY)
+            if ltype == LineType.COMMENT_END:
+                in_block_comment = False
+            dl = DtsLine(i, raw, ltype, indent_str, stripped)
+            dl.depth = depth
+            out.append(dl)
+            continue
+
+        if stripped.startswith('#') and is_preprocessor(stripped):
+            dl = DtsLine(i, raw, LineType.PREPROCESSOR,
+                         indent_str, stripped)
+            dl.depth = depth
+            out.append(dl)
+            prev_complete = True
+            in_cpp_macro = stripped.rstrip().endswith('\\')
+            continue
+
+        # Strip comments first so all later structural checks see code
+        # only. An unclosed /* sets in_block_comment for the next line.
+        code, opens_block = _split_code(stripped)
+        if opens_block:
+            in_block_comment = True
+
+        # Pure-comment line: nothing left after stripping. Classify as
+        # COMMENT_START (carries to next line) or COMMENT, and skip the
+        # structural classification entirely.
+        if not code:
+            ltype = LineType.COMMENT_START if opens_block else LineType.COMMENT
+            dl = DtsLine(i, raw, ltype, indent_str, stripped)
+            dl.depth = depth
+            out.append(dl)
+            continue
+
+        if not prev_complete:
+            dl = DtsLine(i, raw, LineType.CONTINUATION, indent_str, code)
+            dl.depth = depth
+            out.append(dl)
+            prev_complete = (code.endswith(';') or
+                             code.endswith('{') or
+                             code.endswith('};'))
+            continue
+
+        # NODE_CLOSE: the canonical form is "}" or "};" alone. A line
+        # that is nothing but closures (e.g. "}; };") is still treated
+        # as NODE_CLOSE for depth tracking, but the multi-closure case
+        # is flagged separately by check_node_close_alone via
+        # dl.closures.
+        if re_only_closures.match(code):
+            closures = code.count('}')
+            depth = max(depth - closures, 0)
+            dl = DtsLine(i, raw, LineType.NODE_CLOSE, indent_str, code)
+            dl.depth = depth
+            dl.closures = closures
+            out.append(dl)
+            prev_complete = True
+            continue
+
+        if code.endswith('{'):
+            dl = DtsLine(i, raw, LineType.NODE_OPEN, indent_str, code)
+            parse_node_header(dl)
+            dl.depth = depth
+            out.append(dl)
+            depth += 1
+            prev_complete = True
+            continue
+
+        # Property (or first line of a multi-line property).
+        dl = DtsLine(i, raw, LineType.PROPERTY, indent_str, code)
+        parse_property_name(dl)
+        dl.depth = depth
+        out.append(dl)
+        prev_complete = code.endswith(';')
+
+    # Group continuation lines onto their leading PROPERTY.
+    last_prop = None
+    grouped = []
+    for dl in out:
+        if dl.linetype == LineType.CONTINUATION and last_prop is not None:
+            last_prop.continuations.append(dl)
+            continue
+        if dl.linetype == LineType.PROPERTY:
+            last_prop = dl
+        elif dl.linetype != LineType.BLANK and \
+                dl.linetype not in (LineType.COMMENT, LineType.COMMENT_BODY,
+                                    LineType.COMMENT_END,
+                                    LineType.COMMENT_START):
+            last_prop = None
+        grouped.append(dl)
+    return grouped
+
+
+def parse_node_header(dl):
+    m = re_node_header.match(dl.stripped)
+    if m:
+        dl.label = m.group(1)
+        dl.node_name = m.group(2)
+        dl.node_addr = m.group(3)
+        return
+    m = re_ref_node.match(dl.stripped)
+    if m:
+        dl.ref_name = m.group(1)
+
+
+def parse_property_name(dl):
+    m = re.match(r'^([a-zA-Z0-9#][a-zA-Z0-9,._+#-]*)\s*[=;]', dl.stripped)
+    if m:
+        dl.prop_name = m.group(1)
+
+
+def collect_labels_and_refs(text):
+    """Return (defined_labels, referenced_labels) found anywhere outside
+    /* */ comments and string literals. Labels named fake_intc* (injected
+    by dt-extract-example) are skipped."""
+    # Strip block comments first so labels inside them don't count
+    stripped = re.sub(r'/\*.*?\*/', '', text, flags=re.DOTALL)
+    # Strip line comments
+    stripped = re.sub(r'//[^\n]*', '', stripped)
+    # Strip string literals so words inside quotes (e.g. "Error: foo")
+    # are not picked up as label definitions or &-references.
+    stripped = re.sub(r'"(?:[^"\\]|\\.)*"', '""', stripped)
+    defined = set()
+    referenced = set()
+    # A label precedes a node header; the next non-space token may start
+    # with a letter (foo, &ref), a digit (1wire), or '/' (root node).
+    for m in re.finditer(
+            r'(?:^|[\s{])([a-zA-Z_][a-zA-Z0-9_]*):\s*[a-zA-Z0-9/&]',
+            stripped):
+        name = m.group(1)
+        if not name.startswith('fake_intc'):
+            defined.add(name)
+    for m in re.finditer(r'&([a-zA-Z_][a-zA-Z0-9_]*)', stripped):
+        referenced.add(m.group(1))
+    return defined, referenced
+
+
+# ---------------------------------------------------------------------------
+# Rule registry
+# ---------------------------------------------------------------------------
+
+class Ctx:
+    """Context passed to each rule check. Carries the parsed lines,
+    raw text, mode, and indent kind."""
+
+    def __init__(self, lines, text, mode, indent_kind):
+        self.lines = lines
+        self.text = text
+        self.mode = mode               # 'relaxed' or 'strict'
+        self.indent_kind = indent_kind  # 'spaces' or 'tab'
+
+
+class Rule:
+    __slots__ = ('name', 'mode', 'description', 'check', 'applies_to')
+
+    def __init__(self, name, mode, description, check,
+                 applies_to=('yaml', 'dts', 'dtsi', 'dtso')):
+        self.name = name
+        self.mode = mode               # 'relaxed' or 'strict'
+        self.description = description
+        self.check = check
+        self.applies_to = applies_to   # input types this rule covers
+
+
+# --- individual rule check functions --------------------------------------
+
+def check_trailing_whitespace(ctx):
+    for dl in ctx.lines:
+        if dl.raw != dl.raw.rstrip():
+            yield (dl.lineno, 'trailing whitespace')
+
+
+def check_tab_in_dts(ctx):
+    """Reject literal tabs in DTS lines when input is YAML.
+
+    For YAML examples, indent and content must use spaces. Tabs inside
+    a #define value are tolerated (those are CPP macros, not DTS).
+    For .dts files, this rule does not apply -- tabs are required.
+    """
+    if ctx.indent_kind != 'spaces':
+        return
+    for dl in ctx.lines:
+        if dl.linetype == LineType.PREPROCESSOR:
+            continue
+        if dl.linetype == LineType.BLANK:
+            continue
+        if '\t' in dl.raw:
+            yield (dl.lineno, 'tab character not allowed in DTS example')
+
+
+def check_mixed_indent_chars(ctx):
+    """Indent must be all-spaces or all-tabs, never mixed on one line."""
+    for dl in ctx.lines:
+        if not dl.indent_str:
+            continue
+        if dl.linetype == LineType.PREPROCESSOR:
+            continue
+        if ' ' in dl.indent_str and '\t' in dl.indent_str:
+            yield (dl.lineno, 'mixed tabs and spaces in indent')
+
+
+def detect_indent_unit(ctx):
+    """Find the indent unit used at depth 1 in this block.
+
+    Returns one of: '  ' (2 spaces), '    ' (4 spaces), '\\t' (tab),
+    or None if depth-1 is empty or ambiguous."""
+    for dl in ctx.lines:
+        if dl.depth != 1:
+            continue
+        if dl.linetype in (LineType.BLANK, LineType.PREPROCESSOR):
+            continue
+        if dl.linetype in (LineType.COMMENT_BODY, LineType.COMMENT_END):
+            continue
+        if not dl.indent_str:
+            continue
+        if dl.indent_str == '\t':
+            return '\t'
+        if dl.indent_str == '    ':
+            return '    '
+        if dl.indent_str == '  ':
+            return '  '
+        # Anything else at depth 1 is non-canonical; flag elsewhere.
+        return dl.indent_str
+    return None
+
+
+def check_indent_unit_relaxed(ctx):
+    """YAML examples: 2 or 4 spaces. Never tabs or other widths."""
+    unit = detect_indent_unit(ctx)
+    if unit is None:
+        return
+    if unit not in ('  ', '    '):
+        yield (1, 'indent unit must be 2 or 4 spaces, got %r' % unit)
+
+
+def check_indent_unit_dts(ctx):
+    """DTS files: 1 tab per level. Always required."""
+    unit = detect_indent_unit(ctx)
+    if unit is None:
+        return
+    if unit != '\t':
+        yield (1, 'indent unit must be 1 tab in DTS, got %r' % unit)
+
+
+def check_indent_unit_strict(ctx):
+    """YAML: must be exactly 4 spaces. DTS: 1 tab (same as relaxed)."""
+    unit = detect_indent_unit(ctx)
+    if unit is None:
+        return
+    if ctx.indent_kind == 'spaces':
+        if unit != '    ':
+            yield (1, 'indent unit must be 4 spaces in strict mode, '
+                   'got %r' % unit)
+
+
+def check_indent_consistent(ctx):
+    """All indented lines must be a multiple of the detected unit."""
+    unit = detect_indent_unit(ctx)
+    if unit is None:
+        return
+    if ctx.indent_kind == 'spaces':
+        if unit not in ('  ', '    '):
+            return  # let check_indent_unit_* report this
+    else:
+        if unit != '\t':
+            return
+
+    for dl in ctx.lines:
+        if dl.linetype in (LineType.BLANK, LineType.PREPROCESSOR):
+            continue
+        if dl.linetype == LineType.CONTINUATION:
+            continue   # continuations align to <, not to indent unit
+        if dl.linetype in (LineType.COMMENT_BODY, LineType.COMMENT_END):
+            continue
+        if not dl.indent_str:
+            continue
+        # The indent must be 'unit' repeated dl.depth times, exactly.
+        # NODE_CLOSE lines have depth equal to the post-decrement value,
+        # which matches the indent expected.
+        expected = unit * dl.depth
+        if dl.indent_str != expected:
+            yield (dl.lineno,
+                   'indent mismatch (expected depth %d * %r)' %
+                   (dl.depth, unit))
+
+
+def check_blank_lines(ctx):
+    """No two consecutive blank lines, no leading/trailing blank lines
+    in any node body."""
+    lines = ctx.lines
+    # Consecutive blanks
+    for i in range(1, len(lines)):
+        if lines[i].linetype == LineType.BLANK and \
+                lines[i - 1].linetype == LineType.BLANK:
+            yield (lines[i].lineno, 'consecutive blank lines')
+    # Blank right after { or right before }
+    for i, dl in enumerate(lines):
+        if dl.linetype != LineType.BLANK:
+            continue
+        prev = lines[i - 1] if i > 0 else None
+        nxt = lines[i + 1] if i + 1 < len(lines) else None
+        if prev is not None and prev.linetype == LineType.NODE_OPEN:
+            yield (dl.lineno, 'blank line at start of node body')
+        if nxt is not None and nxt.linetype == LineType.NODE_CLOSE:
+            yield (dl.lineno, 'blank line at end of node body')
+
+
+def _walk_bodies(lines):
+    """Yield lists of immediate-child NODE_OPEN lines for each node body
+    in the input. Skips ref-nodes (&label) since those don't have an
+    intrinsic ordering."""
+    body_stack = [[]]
+    for dl in lines:
+        if dl.linetype == LineType.NODE_OPEN:
+            body_stack[-1].append(dl)
+            body_stack.append([])
+            continue
+        if dl.linetype == LineType.NODE_CLOSE:
+            if len(body_stack) <= 1:
+                # Unbalanced; ignore to avoid crashing on malformed input
+                continue
+            yield body_stack.pop()
+            continue
+    while body_stack:
+        yield body_stack.pop()
+
+
+def _natural_sort_key(s):
+    """Split a string into a tuple of (kind, value) pairs that compares
+    numeric runs as ints, so 'foo10' sorts after 'foo2'."""
+    parts = []
+    for part in re.split(r'(\d+)', s):
+        if part.isdigit():
+            parts.append((0, int(part)))
+        else:
+            parts.append((1, part))
+    return tuple(parts)
+
+
+def check_child_address_order(ctx):
+    """Addressed siblings (foo@N) must appear in ascending address
+    order within their parent node body."""
+    for children in _walk_bodies(ctx.lines):
+        addressed = []
+        for c in children:
+            if c.node_addr is None:
+                continue
+            try:
+                parts = tuple(int(p, 16) for p in c.node_addr.split(','))
+            except ValueError:
+                continue
+            addressed.append((parts, c))
+        for i in range(1, len(addressed)):
+            if addressed[i][0] < addressed[i - 1][0]:
+                dl = addressed[i][1]
+                yield (dl.lineno,
+                       'child node @%s out of address order' %
+                       dl.node_addr)
+
+
+def check_child_name_order(ctx):
+    """Unaddressed siblings must appear in natural-sort order by node
+    name within their parent node body. Addressed children are scoped
+    by check_child_address_order; reference nodes (&label { ... }) and
+    the root node are skipped."""
+    for children in _walk_bodies(ctx.lines):
+        unaddressed = []
+        for c in children:
+            if c.node_addr is not None:
+                continue
+            if c.node_name in (None, '/'):
+                continue
+            if c.ref_name is not None:
+                continue
+            unaddressed.append((_natural_sort_key(c.node_name), c))
+        for i in range(1, len(unaddressed)):
+            if unaddressed[i][0] < unaddressed[i - 1][0]:
+                dl = unaddressed[i][1]
+                yield (dl.lineno,
+                       'child node %r out of name order' % dl.node_name)
+
+
+def _property_bucket(name):
+    """Return the canonical bucket index for a property:
+       0 compatible
+       1 reg / reg-names
+       2 ranges
+       3 standard properties (no vendor comma in #-stripped name)
+       4 vendor-specific properties
+       5 status
+    Plus a sub-key inside the bucket for fixed slots (compatible, reg,
+    reg-names, ranges, status). 'standard' and 'vendor' return None for
+    the sub-key, signalling that the within-bucket key is computed by
+    the pairing rules."""
+    stripped = name.lstrip('#')
+    if name == 'compatible':
+        return (0, 0)
+    if name == 'reg':
+        return (1, 0)
+    if name == 'reg-names':
+        return (1, 1)
+    if name == 'ranges':
+        return (2, 0)
+    if name == 'status':
+        return (5, 0)
+    return (4 if ',' in stripped else 3, None)
+
+
+# Declarative pairing rules: each is a callable
+#   (name, all_names) -> anchor_name_or_None
+# If a rule returns an anchor, the property sorts immediately after the
+# anchor. Rules are tried in order; the first match wins. If none
+# matches, the within-bucket key falls back to natural sort by the
+# #-stripped name.
+
+def _pair_pinctrl_names(name, all_names):
+    """pinctrl-names follows the highest pinctrl-N in the same node."""
+    if name != 'pinctrl-names':
+        return None
+    cands = [n for n in all_names if re.match(r'^pinctrl-\d+$', n)]
+    if not cands:
+        return None
+    return max(cands, key=_natural_sort_key)
+
+
+def _pair_x_names(name, all_names):
+    """Generic <x>-names follows its owning property. The owner is
+    usually plural (clocks/clock-names, dmas/dma-names,
+    resets/reset-names) but occasionally singular (reg/reg-names is
+    handled by the fixed slot above; this rule catches anything else)."""
+    if not name.endswith('-names'):
+        return None
+    base = name[:-len('-names')]
+    # Try plural and singular forms.
+    if (base + 's') in all_names:
+        return base + 's'
+    if base in all_names:
+        return base
+    return None
+
+
+PAIRING_RULES = (_pair_pinctrl_names, _pair_x_names)
+
+
+def _property_sort_key(name, all_names):
+    """Sort key for a property among its node-body siblings.
+
+    Format: (bucket, within_key, tiebreak). 'within_key' for
+    standard/vendor buckets follows pairing rules: a property paired
+    with anchor X sorts as if it were X with a higher tiebreak."""
+    bucket, fixed_sub = _property_bucket(name)
+    if fixed_sub is not None:
+        return (bucket, (), fixed_sub)
+
+    for rule in PAIRING_RULES:
+        anchor = rule(name, all_names)
+        if anchor is not None:
+            return (bucket, _natural_sort_key(anchor.lstrip('#')), 1)
+
+    return (bucket, _natural_sort_key(name.lstrip('#')), 0)
+
+
+def check_property_order(ctx):
+    """Properties within a node body must appear in canonical order:
+    compatible, reg(/reg-names), ranges, then the standard group, then
+    the vendor-specific group, then status. Inside the standard and
+    vendor groups, pairing rules apply (e.g. <x>-names follows <x>);
+    everything else falls back to natural sort by the #-stripped name."""
+    lines = ctx.lines
+    for i, dl in enumerate(lines):
+        if dl.linetype != LineType.NODE_OPEN:
+            continue
+        body_depth = dl.depth + 1
+        props = []
+        for j in range(i + 1, len(lines)):
+            d = lines[j]
+            if d.linetype == LineType.NODE_CLOSE and \
+                    d.depth == body_depth - 1:
+                break
+            if d.linetype == LineType.PROPERTY and d.depth == body_depth \
+                    and d.prop_name is not None:
+                props.append(d)
+        if len(props) < 2:
+            continue
+        all_names = [p.prop_name for p in props]
+        keyed = [(p, _property_sort_key(p.prop_name, all_names))
+                 for p in props]
+        for k in range(1, len(keyed)):
+            if keyed[k][1] < keyed[k - 1][1]:
+                p = keyed[k][0]
+                prev = keyed[k - 1][0]
+                yield (p.lineno,
+                       'property %r out of canonical order '
+                       '(should sort before %r)' %
+                       (p.prop_name, prev.prop_name))
+
+
+def _strip_strings_and_comments(text):
+    """Remove string literals and /* */ + // comments from a single
+    line, replacing them with empty strings. Used so syntactic checks
+    (whitespace, hex case, etc.) don't false-positive on contents of
+    quoted strings or comments. An unclosed /* on the line is treated
+    as a comment running to end of line."""
+    text = re.sub(r'"(?:[^"\\]|\\.)*"', '""', text)
+    text = re.sub(r'/\*.*?\*/', '', text)
+    text = re.sub(r'/\*.*$', '', text)
+    text = re.sub(r'//.*$', '', text)
+    return text
+
+
+def check_required_blank_lines(ctx):
+    """A blank line must precede each child node and the 'status'
+    property within a node body, except when these are the first
+    substantive item in the body."""
+    lines = ctx.lines
+    for i, open_dl in enumerate(lines):
+        if open_dl.linetype != LineType.NODE_OPEN:
+            continue
+        body_depth = open_dl.depth + 1
+        prev_substantive = None
+        between_blanks = 0
+        depth_inside = 0
+        for j in range(i + 1, len(lines)):
+            d = lines[j]
+            if d.linetype == LineType.NODE_CLOSE and \
+                    d.depth == body_depth - 1 and depth_inside == 0:
+                break
+            # Track depth inside nested children so we only look at
+            # immediate-body items.
+            if d.linetype == LineType.NODE_OPEN and \
+                    d.depth >= body_depth and depth_inside > 0:
+                depth_inside += 1
+                continue
+            if d.linetype == LineType.NODE_CLOSE and depth_inside > 0:
+                depth_inside -= 1
+                continue
+            if depth_inside > 0:
+                continue
+            if d.linetype == LineType.BLANK:
+                if prev_substantive is not None:
+                    between_blanks += 1
+                continue
+            if d.linetype in (LineType.COMMENT, LineType.COMMENT_START,
+                              LineType.COMMENT_BODY, LineType.COMMENT_END,
+                              LineType.PREPROCESSOR):
+                continue
+            if d.linetype == LineType.CONTINUATION:
+                continue
+
+            needs_blank = False
+            if d.linetype == LineType.NODE_OPEN:
+                needs_blank = True
+                depth_inside = 1   # entered the child body
+            elif d.linetype == LineType.PROPERTY and d.prop_name == 'status':
+                needs_blank = True
+
+            if needs_blank and prev_substantive is not None and \
+                    between_blanks == 0:
+                if d.linetype == LineType.NODE_OPEN:
+                    yield (d.lineno,
+                           'child node must be preceded by a blank line')
+                else:
+                    yield (d.lineno,
+                           '"status" must be preceded by a blank line')
+
+            prev_substantive = d
+            between_blanks = 0
+
+
+def check_hex_case(ctx):
+    """Hex literals (0xN) must use lowercase digits and prefix."""
+    for dl in ctx.lines:
+        if dl.linetype in (LineType.BLANK, LineType.COMMENT,
+                           LineType.COMMENT_START, LineType.COMMENT_BODY,
+                           LineType.COMMENT_END, LineType.PREPROCESSOR):
+            continue
+        text = _strip_strings_and_comments(dl.raw)
+        for m in re.finditer(r'\b0[xX][0-9a-fA-F]+\b', text):
+            lit = m.group(0)
+            if any(c.isupper() for c in lit[2:]) or lit[1] == 'X':
+                yield (dl.lineno,
+                       'hex literal %r must be lowercase' % lit)
+
+
+def check_unit_address_format(ctx):
+    """Unit addresses must be lowercase hex without leading zeros and
+    without a '0x' prefix. For multi-cell addresses (comma-separated),
+    each part is checked independently. A single '0' is permitted
+    (canonical zero)."""
+    for dl in ctx.lines:
+        if dl.linetype != LineType.NODE_OPEN:
+            continue
+        if dl.node_addr is None:
+            continue
+        addr = dl.node_addr
+        for part in addr.split(','):
+            if part[:2] in ('0x', '0X'):
+                yield (dl.lineno,
+                       'unit address %r must not have a "0x" prefix' %
+                       addr)
+                break
+            if not re.match(r'^[0-9a-fA-F]+$', part):
+                yield (dl.lineno,
+                       'unit address %r is not valid hex' % addr)
+                break
+            if any(c in 'ABCDEF' for c in part):
+                yield (dl.lineno,
+                       'unit address %r must be lowercase hex' % addr)
+                break
+            if len(part) > 1 and part.startswith('0'):
+                yield (dl.lineno,
+                       'unit address %r has leading zeros' % addr)
+                break
+
+
+def check_value_whitespace(ctx):
+    """A <...> cell list must have no whitespace directly after '<'
+    or directly before '>'. Continuation lines are joined onto the
+    property so a <...> split across lines is checked too; a '<' or
+    '>' at a line break is glued straight to the neighbouring value,
+    so the break itself is not counted as padding. Outside strings
+    and comments only."""
+    for dl in ctx.lines:
+        if dl.linetype != LineType.PROPERTY:
+            continue
+        segs = [_strip_strings_and_comments(dl.raw).strip()]
+        for cont in dl.continuations:
+            segs.append(_strip_strings_and_comments(cont.stripped).strip())
+        text = ''
+        for s in segs:
+            if not s:
+                continue
+            if not text or text.endswith('<') or s.startswith('>'):
+                text += s
+            else:
+                text += ' ' + s
+        for m in re.finditer(r'<([^<>]*)>', text):
+            content = m.group(1)
+            if content and content != content.strip():
+                yield (dl.lineno, 'extra whitespace inside <...>')
+                break
+
+
+def check_node_close_alone(ctx):
+    """The closing '};' of a node must be on its own line. The
+    classifier accepts a canonical "}" or "};" as NODE_CLOSE; a line
+    that is all closures (e.g. "}; };") is still NODE_CLOSE for depth
+    tracking but is flagged here via dl.closures. Any other line that
+    still contains '};' (in code, not in strings or comments) is
+    mixing a node close with something else."""
+    for dl in ctx.lines:
+        if dl.linetype == LineType.NODE_CLOSE:
+            if dl.closures > 1:
+                yield (dl.lineno,
+                       'closing brace must be on its own line')
+            continue
+        if dl.linetype in (LineType.BLANK, LineType.COMMENT,
+                           LineType.COMMENT_START, LineType.COMMENT_BODY,
+                           LineType.COMMENT_END, LineType.PREPROCESSOR):
+            continue
+        text = _strip_strings_and_comments(dl.raw)
+        if '};' in text:
+            yield (dl.lineno,
+                   'closing brace must be on its own line')
+
+
+def _display_col(text):
+    """Visual column width of text, with tabs expanded to the next
+    8-column stop, matching how printf and most editors render a
+    line and the kernel-wide line length convention."""
+    col = 0
+    for ch in text:
+        if ch == '\t':
+            col = (col // 8 + 1) * 8
+        else:
+            col += 1
+    return col
+
+
+def check_line_length(ctx):
+    """Lines must not exceed 80 columns; tabs count as 8 (see
+    _display_col)."""
+    for dl in ctx.lines:
+        if dl.linetype == LineType.BLANK:
+            continue
+        cols = _display_col(dl.raw)
+        if cols > 80:
+            yield (dl.lineno,
+                   'line exceeds 80 columns (%d)' % cols)
+
+
+def check_continuation_alignment(ctx):
+    """A multi-line property's continuation lines must align their
+    first non-whitespace character to the display column of the first
+    '<' or '"' after the '=' in the leading line. Display columns are
+    used so tab-indented .dts files (where a continuation aligns with
+    tabs plus spaces) are compared correctly."""
+    for dl in ctx.lines:
+        if dl.linetype != LineType.PROPERTY:
+            continue
+        if not dl.continuations:
+            continue
+        eq = dl.raw.find('=')
+        if eq < 0:
+            continue
+        # First '<' or '"' after '='
+        rest = dl.raw[eq + 1:]
+        m = re.search(r'[<"]', rest)
+        if not m:
+            continue
+        target_col = _display_col(dl.raw[:eq + 1 + m.start()])
+        for cont in dl.continuations:
+            if _display_col(cont.indent_str) != target_col:
+                yield (cont.lineno,
+                       'continuation should align to column %d '
+                       '(under "<" or \\")' % (target_col + 1))
+
+
+def check_unclosed_block_comment(ctx):
+    """Every /* must have a matching */ in the same block. Catches both
+    a comment opened on its own line (COMMENT_START) and a tail comment
+    opened on a PROPERTY or other code line (where in_block_comment is
+    set by _split_code so the next line becomes COMMENT_BODY without a
+    preceding COMMENT_START)."""
+    open_lineno = None
+    for dl in ctx.lines:
+        if dl.linetype == LineType.COMMENT_START:
+            open_lineno = dl.lineno
+        elif dl.linetype == LineType.COMMENT_END:
+            open_lineno = None
+        elif dl.linetype == LineType.COMMENT_BODY and open_lineno is None:
+            # Block was opened by a /* tail on a code line; report at
+            # the first orphan body line since the originating line is
+            # already classified as something else.
+            open_lineno = dl.lineno
+    if open_lineno is not None:
+        yield (open_lineno, 'unclosed /* block comment')
+
+
+def check_unused_labels(ctx):
+    """Labels defined but never referenced are clutter."""
+    defined, referenced = collect_labels_and_refs(ctx.text)
+    for label in sorted(defined - referenced):
+        # Find the line where this label is defined for line-number
+        # reporting.
+        m = re.search(r'(?m)^.*\b' + re.escape(label) + r'\s*:', ctx.text)
+        lineno = ctx.text[:m.start()].count('\n') + 1 if m else 1
+        yield (lineno, 'label %r defined but never &-referenced' % label)
+
+
+# --- registry --------------------------------------------------------------
+
+RULES = [
+    # 'relaxed' is the default; rules in this group must produce zero
+    # output on a clean kernel tree (post the small prep-cleanup
+    # commit at the head of this series).
+    Rule('trailing-whitespace', 'relaxed',
+         'no trailing whitespace on any line',
+         check_trailing_whitespace),
+    Rule('tab-in-dts', 'relaxed',
+         'YAML examples may not contain tab characters',
+         check_tab_in_dts, applies_to=('yaml',)),
+    Rule('mixed-indent-chars', 'relaxed',
+         'indent must not mix tabs and spaces',
+         check_mixed_indent_chars),
+    Rule('unclosed-block-comment', 'relaxed',
+         'every /* block comment must close with */',
+         check_unclosed_block_comment),
+
+    # DTS files always use tabs; this is not negotiable per kernel
+    # coding style (.dts files are real source). Relaxed mode.
+    Rule('indent-unit-dts', 'relaxed',
+         'DTS files: 1 tab per nesting level',
+         check_indent_unit_dts,
+         applies_to=('dts', 'dtsi', 'dtso')),
+
+    # 'strict' rules are opt-in (e.g. for new submissions via
+    # checkpatch.pl in a follow-up series). They flag many existing
+    # files and can be promoted to relaxed once those are cleaned up.
+    Rule('indent-unit', 'strict',
+         'YAML: 2 or 4 spaces per level',
+         check_indent_unit_relaxed, applies_to=('yaml',)),
+    Rule('indent-unit-strict', 'strict',
+         'YAML: must be 4 spaces per level',
+         check_indent_unit_strict, applies_to=('yaml',)),
+    Rule('indent-consistent', 'strict',
+         'every line indented at depth * unit',
+         check_indent_consistent),
+    Rule('blank-lines', 'strict',
+         'no consecutive blanks; no blanks at node body edges',
+         check_blank_lines),
+    Rule('child-address-order', 'strict',
+         'addressed siblings must be in ascending address order',
+         check_child_address_order),
+    Rule('child-name-order', 'strict',
+         'unaddressed siblings must be in natural-sort name order',
+         check_child_name_order),
+    Rule('property-order', 'strict',
+         'canonical bucket + pairing + natural-sort order of properties',
+         check_property_order),
+    Rule('required-blank-lines', 'strict',
+         'blank line before child nodes and before "status"',
+         check_required_blank_lines),
+    Rule('hex-case', 'strict',
+         'hex literals must be lowercase',
+         check_hex_case),
+    Rule('unit-address-format', 'strict',
+         'unit addresses must be lowercase hex without leading zeros',
+         check_unit_address_format),
+    Rule('value-whitespace', 'strict',
+         'no whitespace directly inside <...> brackets',
+         check_value_whitespace),
+    Rule('node-close-alone', 'strict',
+         'closing brace must be on its own line',
+         check_node_close_alone),
+    Rule('line-length', 'strict',
+         'lines must not exceed 80 columns',
+         check_line_length),
+    Rule('continuation-alignment', 'strict',
+         'multi-line property continuations align under "<" or "\\""',
+         check_continuation_alignment),
+    Rule('unused-labels', 'strict',
+         'every label must be &-referenced in the same example/file '
+         '(skipped for .dtsi/.dtso since labels there are exported)',
+         check_unused_labels, applies_to=('yaml', 'dts')),
+]
+
+
+def select_rules(mode, input_kind):
+    """Return rules that apply to the given mode and input type."""
+    rank = {'relaxed': 0, 'strict': 1}
+    out = []
+    for r in RULES:
+        if rank[r.mode] > rank[mode]:
+            continue
+        if input_kind not in r.applies_to:
+            continue
+        out.append(r)
+    return out
+
+
+# ---------------------------------------------------------------------------
+# Block runner
+# ---------------------------------------------------------------------------
+
+def check_block(text, mode, indent_kind, input_type):
+    """Run all selected rules on a single block of DTS text. Returns a
+    list of (lineno, rule_name, message) tuples."""
+    lines = classify_lines(text)
+    ctx = Ctx(lines, text, mode, indent_kind)
+    rules = select_rules(mode, input_type)
+    findings = []
+    for r in rules:
+        for lineno, msg in r.check(ctx):
+            findings.append((lineno, r.name, msg))
+    findings.sort(key=lambda t: (t[0], t[1]))
+    return findings
+
+
+# ---------------------------------------------------------------------------
+# Input drivers (YAML examples vs raw DTS)
+# ---------------------------------------------------------------------------
+
+def _yaml_loader():
+    return ruamel.yaml.YAML()
+
+
+def iter_yaml_examples(filepath):
+    """Yield (example_text, base_lineno_in_file, example_index) tuples."""
+    yaml = _yaml_loader()
+    try:
+        with open(filepath, encoding='utf-8') as f:
+            data = yaml.load(f)
+    except Exception as e:
+        print('%s: error loading YAML: %s' % (filepath, e),
+              file=sys.stderr)
+        return
+    if not isinstance(data, dict) or 'examples' not in data:
+        return
+    examples = data['examples']
+    if not hasattr(examples, '__iter__'):
+        return
+    for i, ex in enumerate(examples):
+        if not isinstance(ex, str):
+            continue
+        try:
+            base = examples.lc.item(i)[0] + 2
+        except Exception:
+            base = 1
+        yield (str(ex), base, i)
+
+
+def iter_dts_file(filepath):
+    """Treat the whole file as a single block."""
+    try:
+        with open(filepath, encoding='utf-8') as f:
+            text = f.read()
+    except Exception as e:
+        print('%s: error reading: %s' % (filepath, e), file=sys.stderr)
+        return
+    yield (text, 1, None)
+
+
+# ---------------------------------------------------------------------------
+# Top-level processing
+# ---------------------------------------------------------------------------
+
+def input_kind(filepath):
+    p = filepath.lower()
+    if p.endswith('.yaml') or p.endswith('.yml'):
+        return 'yaml'
+    if p.endswith('.dts'):
+        return 'dts'
+    if p.endswith('.dtsi'):
+        return 'dtsi'
+    if p.endswith('.dtso'):
+        return 'dtso'
+    return None
+
+
+# All input types that use tab indentation and follow DTS coding style.
+DTS_FAMILY = ('dts', 'dtsi', 'dtso')
+
+
+def collect_findings(filepath, mode):
+    """Return a (lines, count) pair for filepath. lines is a list of
+    formatted output strings; count is the number of findings."""
+    kind = input_kind(filepath)
+    if kind == 'yaml':
+        indent_kind = 'spaces'
+        iterator = iter_yaml_examples(filepath)
+    elif kind in DTS_FAMILY:
+        indent_kind = 'tab'
+        iterator = iter_dts_file(filepath)
+    else:
+        return (['%s: unknown file type, skipping' % filepath], 0)
+
+    out = []
+    for text, base, idx in iterator:
+        for lineno, rule, msg in check_block(text, mode, indent_kind, kind):
+            abs_line = base + lineno - 1
+            ex_tag = '' if idx is None else ' example %d' % idx
+            out.append('%s:%d:%s [%s] %s' %
+                       (filepath, abs_line, ex_tag, rule, msg))
+    return (out, len(out))
+
+
+# Worker entry point for ProcessPoolExecutor.map(). Top-level so it is
+# picklable on every platform.
+def _worker(args):
+    filepath, mode = args
+    return collect_findings(filepath, mode)
+
+
+def main():
+    import os
+    ap = argparse.ArgumentParser(
+        description='Check DTS coding style on YAML examples and '
+        '.dts/.dtsi/.dtso files.',
+        fromfile_prefix_chars='@')
+    ap.add_argument('--mode', choices=('relaxed', 'strict'),
+                    default='relaxed',
+                    help='which rule set to apply (default: relaxed)')
+    ap.add_argument('-j', '--jobs', type=int, default=0,
+                    metavar='N',
+                    help='run N workers in parallel (default: respect '
+                    'the make jobserver via $PARALLELISM, otherwise '
+                    'os.cpu_count(); use 1 to disable multiprocessing)')
+    ap.add_argument('--list-rules', action='store_true',
+                    help='print all rules with their mode and exit')
+    ap.add_argument('files', nargs='*', metavar='file',
+                    help='YAML binding files or .dts/.dtsi/.dtso files; '
+                    'use @argfile to read paths from a file')
+    args = ap.parse_args()
+
+    if args.list_rules:
+        for r in RULES:
+            applies = ','.join(r.applies_to)
+            print('%-22s %-7s [%s] %s' %
+                  (r.name, r.mode, applies, r.description))
+        return 0
+
+    if not args.files:
+        ap.error('no input files')
+
+    if args.jobs > 0:
+        jobs = args.jobs
+    else:
+        # When invoked under scripts/jobserver-exec, $PARALLELISM
+        # holds the slot count make has reserved for us; this lets
+        # `make -j N dt_binding_check` constrain our worker pool to N.
+        try:
+            jobs = int(os.environ['PARALLELISM'])
+        except (KeyError, ValueError):
+            jobs = os.cpu_count() or 1
+    # Single-process path: keep import surface small for tests and
+    # easy debugging.
+    if jobs == 1 or len(args.files) == 1:
+        total = 0
+        for f in args.files:
+            lines, n = collect_findings(f, args.mode)
+            for line in lines:
+                print(line, file=sys.stderr)
+            total += n
+        return 1 if total else 0
+
+    # Multi-process path. ex.map preserves input order so output is
+    # deterministic across runs.
+    from concurrent.futures import ProcessPoolExecutor
+    total = 0
+    work = [(f, args.mode) for f in args.files]
+    chunk = max(1, len(work) // (jobs * 8)) if work else 1
+    with ProcessPoolExecutor(max_workers=jobs) as ex:
+        for lines, n in ex.map(_worker, work, chunksize=chunk):
+            for line in lines:
+                print(line, file=sys.stderr)
+            total += n
+    return 1 if total else 0
+
+
+if __name__ == '__main__':
+    sys.exit(main())
Signature · 1 line
2.54.0
#2 lore raw

Re: [PATCH v5 1/4] dt-bindings: add DTS style checker

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider: - [Medium] `check_mixed_indent_chars` incorrectly flags property continuations and block comments as mixed…

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [Medium] check_mixed_indent_chars incorrectly flags property continuations and block comments as mixed indent violations, causing widespread false positives.
- [Medium] Multi-closure lines (}; };) break tree traversal in structural checks.
- [Medium] Regex ordering in collect_labels_and_refs corrupts label detection when strings contain comment sequences.
--

commit d397a00edbe5f6dcccc252ff4744ba16b6e2e91d
Author: Daniel Golle <[email protected]>

dt-bindings: add DTS style checker

This patch adds a Python tool that checks DTS coding style on examples in YAML binding files and on DTS source files. It introduces rules defined in a declarative registry for relaxed and strict mode checking.

Quoted 5 lines
scripts/dtc/dt-check-style new file +0 -0
new file mode 100755
index 0000000000000..2d5723d41ea39

[ ... ]

Quoted 9 lines
scripts/dtc/dt-check-style new file +9 -0
+        if re_only_closures.match(code):
+            closures = code.count('}')
+            depth = max(depth - closures, 0)
+            dl = DtsLine(i, raw, LineType.NODE_CLOSE, indent_str, code)
+            dl.depth = depth
+            dl.closures = closures
+            out.append(dl)
+            prev_complete = True
+            continue

[ ... ]

Quoted 2 lines
scripts/dtc/dt-check-style new file +2 -0
+def check_property_order(ctx):
+    """Properties within a node body must appear in canonical order:

[ ... ]

scripts/dtc/dt-check-style line 649

Quoted 10 lines
scripts/dtc/dt-check-style new file +10 -0
+    for i, dl in enumerate(lines):
+        if dl.linetype != LineType.NODE_OPEN:
+            continue
+        body_depth = dl.depth + 1
+        props = []
+        for j in range(i + 1, len(lines)):
+            d = lines[j]
+            if d.linetype == LineType.NODE_CLOSE and \
+                    d.depth == body_depth - 1:
+                break

[Severity: Medium]
When a line contains multiple closures like "}; };", classify_lines() decrements the depth by the number of closures.

Will this cause the depth to drop by 2 or more, skipping the exact termination condition of d.depth == body_depth - 1?

It looks like tree traversal in structural checks like check_property_order() or _walk_bodies() might miss the end of the node body and improperly mix properties from different siblings.

[ ... ]

scripts/dtc/dt-check-style line 292

Quoted 11 lines
scripts/dtc/dt-check-style new file +11 -0
+def collect_labels_and_refs(text):
+    """Return (defined_labels, referenced_labels) found anywhere outside
+    /* */ comments and string literals. Labels named fake_intc* (injected
+    by dt-extract-example) are skipped."""
+    # Strip block comments first so labels inside them don't count
+    stripped = re.sub(r'/\*.*?\*/', '', text, flags=re.DOTALL)
+    # Strip line comments
+    stripped = re.sub(r'//[^\n]*', '', stripped)
+    # Strip string literals so words inside quotes (e.g. "Error: foo")
+    # are not picked up as label definitions or &-references.
+    stripped = re.sub(r'"(?:[^"\\]|\\.)*"', '""', stripped)

[Severity: Medium]
Since comments are stripped before string literals, does this break if a string literal contains sequences like // or /*?

If a string contains a URL like "http://..." or a /* sequence without a closing */, the comment regex might activate inside the string and consume valid source code and labels up to the next */ somewhere else in the file, causing false positives for the unused-labels check.

[ ... ]

scripts/dtc/dt-check-style line 369

Quoted 9 lines
scripts/dtc/dt-check-style new file +9 -0
+def check_mixed_indent_chars(ctx):
+    """Indent must be all-spaces or all-tabs, never mixed on one line."""
+    for dl in ctx.lines:
+        if not dl.indent_str:
+            continue
+        if dl.linetype == LineType.PREPROCESSOR:
+            continue
+        if ' ' in dl.indent_str and '\t' in dl.indent_str:
+            yield (dl.lineno, 'mixed tabs and spaces in indent')

[Severity: Medium]
Does this code flag legitimate property continuations or block comments as mixed indent violations?

Multi-line property continuations in DTS typically use tabs for base indentation and spaces for visual alignment under a specific character, which is an alignment pattern expected by check_continuation_alignment().

If check_mixed_indent_chars() flags these, it might cause widespread false positives in the default relaxed mode.

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=1
Reviewed-by on v5 1/4
#3 lore raw

Re: [PATCH v5 1/4] dt-bindings: add DTS style checker

In reply to Daniel Golle
Quoted 20 lines · Daniel Golle

Add a Python tool that checks DTS coding style on examples in YAML binding files and on .dts/.dtsi/.dtso source files. Rules are kept in a small declarative registry, each tagged 'relaxed' (default; must be zero-violation on the current tree) or 'strict' (opt-in for new submissions). Promoting a rule from strict to relaxed is a one-line edit once the tree is clean.

Relaxed mode covers trailing whitespace, tab characters in YAML examples, mixed tab+space indents, and missing tabs in .dts files. Strict adds indent unit and consistency checks, blank-line placement, sibling address ordering, "compatible" and "reg" ordering, and unused labels.

The tool reads file paths from @argfile and parallelises across CPUs via -j N. With no -j given it picks up $PARALLELISM (set by scripts/jobserver-exec from the GNU make jobserver) and falls back to os.cpu_count() otherwise. Running as one Python invocation amortises the ruamel.yaml import across the whole tree -- ~2s on a 32-CPU host vs ~28s sequential.

Quoted 2 lines

---

Probably by relying mostly on regex instead of proper token parsing we will hit a wall at some point, but it's better than what I would write.

Best regards,
Krzysztof

#4 lore raw

PATCH v5 2/4 scripts/jobserver-exec: propagate child exit status

In reply to Daniel Golle

main() called JobserverExec().run() and discarded its return value, then the script exited with the implicit status 0. As a result, any Makefile that wired a build step through jobserver-exec saw the step silently succeed even when the wrapped command had failed.

Two in-tree callers were affected:

Documentation/devicetree/bindings/Makefile
cmd_chk_style runs a python checker via jobserver-exec and uses "&& touch $@ || true" so failures leave the stamp file untouched and the next make rerun reports them again. The swallowed exit code made the stamp file get created even on failure, caching the failed run and hiding the reported issues until the inputs change.

scripts/Makefile.vmlinux_o
cmd_gen_initcalls_lds runs scripts/generate_initcall_order.pl via jobserver-exec; a perl failure was masked by the wrapper.

Return the subprocess exit code from main() and pass it to sys.exit() so the wrapped command's status reaches make.

---
v5: new patch; prereq for chk_style in 3/4 so style failures
leave the stamp file untouched instead of being cached

 scripts/jobserver-exec | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)
scripts/jobserver-exec +2 -2
index 758e947a6fb9..21b319e6c9a5 100755
@@ -28,8 +28,8 @@ def main():
         sys.exit("usage: " + name +" command [args ...]\n" + __doc__)
 
     with JobserverExec() as jobserver:
-        jobserver.run(sys.argv[1:])
+        return jobserver.run(sys.argv[1:])
 
 
 if __name__ == "__main__":
-    main()
+    sys.exit(main())
Signature · 1 line
2.54.0
Reviewed-by on v5 2/4
#5 lore raw

Re: [PATCH v5 2/4] scripts/jobserver-exec: propagate child exit status

Quoted 21 lines · Daniel Golle

main() called JobserverExec().run() and discarded its return value, then the script exited with the implicit status 0. As a result, any Makefile that wired a build step through jobserver-exec saw the step silently succeed even when the wrapped command had failed.

Two in-tree callers were affected:

Documentation/devicetree/bindings/Makefile
cmd_chk_style runs a python checker via jobserver-exec and uses "&& touch $@ || true" so failures leave the stamp file untouched and the next make rerun reports them again. The swallowed exit code made the stamp file get created even on failure, caching the failed run and hiding the reported issues until the inputs change.

scripts/Makefile.vmlinux_o
cmd_gen_initcalls_lds runs scripts/generate_initcall_order.pl via jobserver-exec; a perl failure was masked by the wrapper.

Return the subprocess exit code from main() and pass it to sys.exit() so the wrapped command's status reaches make.

Quoted 7 lines

---
v5: new patch; prereq for chk_style in 3/4 so style failures
leave the stamp file untouched instead of being cached

 scripts/jobserver-exec | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

Best regards,
Krzysztof

maintainer Reviewed-by on v5 2/4
#6 lore raw

Re: [PATCH v5 2/4] scripts/jobserver-exec: propagate child exit status

In reply to Daniel Golle
Quoted 46 lines · Daniel Golle

main() called JobserverExec().run() and discarded its return value, then the script exited with the implicit status 0. As a result, any Makefile that wired a build step through jobserver-exec saw the step silently succeed even when the wrapped command had failed.

Two in-tree callers were affected:

Documentation/devicetree/bindings/Makefile
cmd_chk_style runs a python checker via jobserver-exec and uses "&& touch $@ || true" so failures leave the stamp file untouched and the next make rerun reports them again. The swallowed exit code made the stamp file get created even on failure, caching the failed run and hiding the reported issues until the inputs change.

scripts/Makefile.vmlinux_o
cmd_gen_initcalls_lds runs scripts/generate_initcall_order.pl via jobserver-exec; a perl failure was masked by the wrapper.

Return the subprocess exit code from main() and pass it to sys.exit() so the wrapped command's status reaches make.

---
v5: new patch; prereq for chk_style in 3/4 so style failures
leave the stamp file untouched instead of being cached

 scripts/jobserver-exec | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)
scripts/jobserver-exec +2 -2
index 758e947a6fb9..21b319e6c9a5 100755
@@ -28,8 +28,8 @@ def main():
         sys.exit("usage: " + name +" command [args ...]\n" + __doc__)
 
     with JobserverExec() as jobserver:
-        jobserver.run(sys.argv[1:])
+        return jobserver.run(sys.argv[1:])
 
 
 if __name__ == "__main__":
-    main()
+    sys.exit(main())
Signature · 1 line
2.54.0

Good catch, thanks!

For kbuild:

Signature · 1 line
Nicolas
#7 lore raw

PATCH v5 3/4 dt-bindings: wire style checker into dt_binding_check

In reply to Daniel Golle

Run dt-check-style as part of dt_binding_check_one. The recipe wraps the tool with scripts/jobserver-exec so worker count follows the GNU make jobserver -- make -j N dt_binding_check constrains the checker to N workers rather than spawning one per CPU.

Default mode (relaxed) is zero-violation on the current tree, so this does not introduce new warnings into make dt_binding_check. Stricter rules are available via --mode=strict (eg. for use by checkpatch.pl in a future series).

---
v5:
- no change; depends on the new jobserver-exec fix in 2/4 so style failures stay visible instead of being cached

v4:
- build the @argfile with f=$(mktemp) and remove it with rm -f (matching cmd_mk_schema), instead of Kbuild's $(tmp-target) which leaves a stale .tmp_.dt-style.checked in the build tree

v3:
- use Kbuild's $(tmp-target) instead of mktemp so build output stays inside the build folder (Nathan)
- collapse the conditional cleanup tail into the familiar "&& touch $@ || true" pattern, matching cmd_chk_bindings; keeps future warnings non-fatal (Rob, Nathan)
- retained the explicit $(PYTHON3) prefix (Rob asked why it differs from the rest of this Makefile): per Documentation/kbuild/makefiles.rst "Script invocation", in-tree scripts should be called through their interpreter so the executable bit and shebang are not relied on and the user's $(PYTHON3) override is respected. The neighbouring recipes invoke their Python helpers directly because those come from external packages (dtschema's dt-extract-*, dt-check-compatible, dt-doc-validate), which is the case Rob asked about and which sits outside that rule.

v2:
- dropped xargs -n200 -P$(nproc) sharding; single Python invocation with file list via @argfile
- dropped || true: relaxed mode is zero-output today
- wrapped under scripts/jobserver-exec so worker count follows the make jobserver

 Documentation/devicetree/bindings/Makefile | 19 +++++++++++++++++--
 1 file changed, 17 insertions(+), 2 deletions(-)
Documentation/devicetree/bindings/Makefile +17 -2
index 7b668f7fd400..00149e824261 100644
@@ -46,6 +46,18 @@ quiet_cmd_chk_bindings = CHKDT   $(src)
 			  xargs -n200 -P$$(nproc) $(DT_DOC_CHECKER) -u $(src)) \
 			  && touch $@ || true
 
+DT_CHK_STYLE = $(srctree)/scripts/dtc/dt-check-style
+
+# Feed the file list to the checker via @argfile in a single Python
+# process so the ruamel.yaml import is paid once. scripts/jobserver-exec
+# claims slots from the GNU make jobserver and exposes the count via
+# $PARALLELISM, which dt-check-style picks up to size its worker pool.
+quiet_cmd_chk_style = STYLE   $(src)
+      cmd_chk_style = f=$$(mktemp) && $(find_cmd) > $$f && \
+		      $(PYTHON3) $(srctree)/scripts/jobserver-exec \
+		      $(PYTHON3) $(DT_CHK_STYLE) @$$f \
+		      && touch $@ || true; rm -f $$f
+
 quiet_cmd_mk_schema = SCHEMA  $@
       cmd_mk_schema = f=$$(mktemp) ; \
                       $(find_all_cmd) > $$f ; \
@@ -62,13 +74,16 @@ override DTC_FLAGS := \
 $(obj)/processed-schema.json: $(DT_DOCS) check_dtschema_version FORCE
 	$(call if_changed,mk_schema)
 
-targets += .dt-binding.checked .yamllint.checked
+targets += .dt-binding.checked .yamllint.checked .dt-style.checked
 $(obj)/.yamllint.checked: $(DT_DOCS) $(src)/.yamllint FORCE
 	$(if $(DT_SCHEMA_LINT),$(call if_changed,yamllint),)
 
 $(obj)/.dt-binding.checked: $(DT_DOCS) FORCE
 	$(call if_changed,chk_bindings)
 
+$(obj)/.dt-style.checked: $(DT_DOCS) FORCE
+	$(call if_changed,chk_style)
+
 always-y += processed-schema.json
 targets += $(patsubst $(obj)/%,%, $(CHK_DT_EXAMPLES))
 targets += $(patsubst $(obj)/%.dtb,%.dts, $(CHK_DT_EXAMPLES))
@@ -82,7 +97,7 @@ dt_compatible_check: $(obj)/processed-schema.json
 	$(Q)$(srctree)/scripts/dtc/dt-extract-compatibles $(srctree) | xargs dt-check-compatible -v -s $<
 
 PHONY += dt_binding_check_one
-dt_binding_check_one: $(obj)/.dt-binding.checked $(obj)/.yamllint.checked
+dt_binding_check_one: $(obj)/.dt-binding.checked $(obj)/.yamllint.checked $(obj)/.dt-style.checked
 
 PHONY += dt_binding_check
 dt_binding_check: dt_binding_check_one $(CHK_DT_EXAMPLES)
Signature · 1 line
2.54.0
#8 lore raw

Re: [PATCH v5 3/4] dt-bindings: wire style checker into dt_binding_check

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider: - [Medium] The `cmd_chk_style` recipe contains implementation flaws: failure to connect to the GNU Make…

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] The cmd_chk_style recipe contains implementation flaws: failure to connect to the GNU Make jobserver and unhandled build failure on empty file lists. --

commit a0bf52b4283b270bea18b1e64b7f4c19cfa75a3e
Author: Daniel Golle <[email protected]>

dt-bindings: wire style checker into dt_binding_check

This commit wires the new dt-check-style tool into the dt_binding_check target. It uses scripts/jobserver-exec to constrain the number of workers based on the GNU make jobserver slots, defaulting to relaxed mode.

Quoted 7 lines
Documentation/devicetree/bindings/Makefile +0 -0
@@ -46,6 +46,18 @@ quiet_cmd_chk_bindings = CHKDT   $(src)
 			  xargs -n200 -P$$(nproc) $(DT_DOC_CHECKER) -u $(src)) \
 			  && touch $@ || true
 

Documentation/devicetree/bindings/Makefile quiet_cmd_chk_bindings = CHKDT $(src) · line 58

Quoted 10 lines
Documentation/devicetree/bindings/Makefile +10 -0
+DT_CHK_STYLE = $(srctree)/scripts/dtc/dt-check-style
+
+# Feed the file list to the checker via @argfile in a single Python
+# process so the ruamel.yaml import is paid once. scripts/jobserver-exec
+# claims slots from the GNU make jobserver and exposes the count via
+# $PARALLELISM, which dt-check-style picks up to size its worker pool.
+quiet_cmd_chk_style = STYLE   $(src)
+      cmd_chk_style = f=$$(mktemp) && $(find_cmd) > $$f && \
+		      $(PYTHON3) $(srctree)/scripts/jobserver-exec \
+		      $(PYTHON3) $(DT_CHK_STYLE) @$$f \

[Severity: Medium]
What happens if the file list is empty (e.g., when filtering with DT_SCHEMA_FILES)?

If $(find_cmd) matches no files, the temporary file $$f will be empty. Passing @$$f to dt-check-style when it is empty could result in zero arguments, triggering an argparse error that abruptly fails the build instead of gracefully skipping the check.

Quoted 2 lines
Documentation/devicetree/bindings/Makefile +2 -0
+		      && touch $@ || true; rm -f $$f
+

[ ... ]

Quoted 4 lines
Documentation/devicetree/bindings/Makefile +0 -0
@@ -62,13 +74,16 @@ override DTC_FLAGS := \
 $(obj)/processed-schema.json: $(DT_DOCS) check_dtschema_version FORCE
 	$(call if_changed,mk_schema)
 

Documentation/devicetree/bindings/Makefile override DTC_FLAGS := \ · line 85

Quoted 10 lines
Documentation/devicetree/bindings/Makefile +3 -1
-targets += .dt-binding.checked .yamllint.checked
+targets += .dt-binding.checked .yamllint.checked .dt-style.checked
 $(obj)/.yamllint.checked: $(DT_DOCS) $(src)/.yamllint FORCE
 	$(if $(DT_SCHEMA_LINT),$(call if_changed,yamllint),)
 
 $(obj)/.dt-binding.checked: $(DT_DOCS) FORCE
 	$(call if_changed,chk_bindings)
 
+$(obj)/.dt-style.checked: $(DT_DOCS) FORCE
+	$(call if_changed,chk_style)

[Severity: Medium]
Does this recipe successfully connect to the GNU make jobserver?

The recipe uses $(call if_changed,chk_style) without the '+' prefix required for recursive make.

Without this prefix, GNU make (prior to v4.4) closes the jobserver file descriptors before executing the shell. This causes jobserver-exec to fail to reopen them and silently default to os.cpu_count(), ignoring the user's -j constraint and potentially overloading the system.

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=3
maintainer
#9 lore raw

Re: [PATCH v5 3/4] dt-bindings: wire style checker into dt_binding_check

In reply to Daniel Golle
Quoted 57 lines · Daniel Golle

Run dt-check-style as part of dt_binding_check_one. The recipe wraps the tool with scripts/jobserver-exec so worker count follows the GNU make jobserver -- make -j N dt_binding_check constrains the checker to N workers rather than spawning one per CPU.

Default mode (relaxed) is zero-violation on the current tree, so this does not introduce new warnings into make dt_binding_check. Stricter rules are available via --mode=strict (eg. for use by checkpatch.pl in a future series).

---
v5:
- no change; depends on the new jobserver-exec fix in 2/4 so style failures stay visible instead of being cached

v4:
- build the @argfile with f=$(mktemp) and remove it with rm -f (matching cmd_mk_schema), instead of Kbuild's $(tmp-target) which leaves a stale .tmp_.dt-style.checked in the build tree

v3:
- use Kbuild's $(tmp-target) instead of mktemp so build output stays inside the build folder (Nathan)
- collapse the conditional cleanup tail into the familiar "&& touch $@ || true" pattern, matching cmd_chk_bindings; keeps future warnings non-fatal (Rob, Nathan)
- retained the explicit $(PYTHON3) prefix (Rob asked why it differs from the rest of this Makefile): per Documentation/kbuild/makefiles.rst "Script invocation", in-tree scripts should be called through their interpreter so the executable bit and shebang are not relied on and the user's $(PYTHON3) override is respected. The neighbouring recipes invoke their Python helpers directly because those come from external packages (dtschema's dt-extract-*, dt-check-compatible, dt-doc-validate), which is the case Rob asked about and which sits outside that rule.

v2:
- dropped xargs -n200 -P$(nproc) sharding; single Python invocation with file list via @argfile
- dropped || true: relaxed mode is zero-output today
- wrapped under scripts/jobserver-exec so worker count follows the make jobserver

 Documentation/devicetree/bindings/Makefile | 19 +++++++++++++++++--
 1 file changed, 17 insertions(+), 2 deletions(-)
Documentation/devicetree/bindings/Makefile +1 -0
index 7b668f7fd400..00149e824261 100644
@@ -46,6 +46,18 @@ quiet_cmd_chk_bindings = CHKDT   $(src)
 			  xargs -n200 -P$$(nproc) $(DT_DOC_CHECKER) -u $(src)) \
 			  && touch $@ || true
 
+DT_CHK_STYLE = $(srctree)/scripts/dtc/dt-check-style

Documentation/devicetree/bindings/Makefile quiet_cmd_chk_bindings = CHKDT $(src) · line 59

Quoted 10 lines
Documentation/devicetree/bindings/Makefile +10 -0
+
+# Feed the file list to the checker via @argfile in a single Python
+# process so the ruamel.yaml import is paid once. scripts/jobserver-exec
+# claims slots from the GNU make jobserver and exposes the count via
+# $PARALLELISM, which dt-check-style picks up to size its worker pool.
+quiet_cmd_chk_style = STYLE   $(src)
+      cmd_chk_style = f=$$(mktemp) && $(find_cmd) > $$f && \
+		      $(PYTHON3) $(srctree)/scripts/jobserver-exec \
+		      $(PYTHON3) $(DT_CHK_STYLE) @$$f \
+		      && touch $@ || true; rm -f $$f

As usage of $(mktemp) requires an unconditional 'rm -f $$f', too, I'd like to repeat Nathans suggestion to use Kbuild's $(tmp-target) instead. The rationale, as Nathan wrote, is to keep generated files within the build tree.

Signature · 1 line
Nicolas
#10 lore raw

PATCH v5 4/4 dt-bindings: add self-test fixtures for style checker

In reply to Daniel Golle

Provide good/ and bad/ DTS and YAML fixtures plus a small runner that feeds them to dt-check-style and diffs the output against expected text files. Wired into a new top-level dt_style_selftest make target so the suite can be exercised independently of the full tree.

---
v5:
- add fixtures covering the parser fixes in 1/4 (yaml-unit-addr-prefix, yaml-unclosed-comment, yaml-multi-close, yaml-label-in-string, yaml-tricky-parsing)
- move SPDX header in the three .dts fixtures to a "// SPDX-..." comment on line 1 (checkpatch)

v4:
- run.sh: replace the bash-only "<()" process substitution with a POSIX temp-file plus stdin diff, so the runner works when /bin/sh is dash
- run.sh: check dt-check-style's exit status -- good/ fixtures must exit 0, bad/ fixtures must exit 1 -- instead of discarding it with "|| true"
- add fixtures for the checker fixes in this revision:
good/dts-cont-align.dts (tab-and-space aligned multi-line property), bad/yaml-digit-node-order.yaml (digit-leading node names), bad/yaml-trailing-comment.yaml (trailing // and /* */ comments) and bad/yaml-value-ws-multiline.yaml (multi-line cell array)
- update bad/yaml-value-ws.yaml (and its expected output) to exercise the reworked value-whitespace rule

v3:
- append a trailing newline to every expected/*.txt fixture (Rob)
- restore the trailing whitespace inside yaml-trailing-ws.yaml that had been silently stripped during re-application, so the selftest actually exercises the trailing-whitespace rule

v2:
- new patch (Krzysztof: "would be happy to see at least a few test cases for it")

 Makefile                                      |  6 ++
 .../dtc/dt-style-selftest/bad/dts-spaces.dts  | 12 ++++
 .../bad/yaml-child-addr-order.yaml            | 41 +++++++++++
 .../bad/yaml-child-name-order.yaml            | 37 ++++++++++
 .../bad/yaml-cont-align.yaml                  | 30 ++++++++
 .../bad/yaml-digit-node-order.yaml            | 37 ++++++++++
 .../dt-style-selftest/bad/yaml-hex-case.yaml  | 29 ++++++++
 .../bad/yaml-indent-strict.yaml               | 29 ++++++++
 .../bad/yaml-label-in-string.yaml             | 30 ++++++++
 .../bad/yaml-line-length.yaml                 | 29 ++++++++
 .../bad/yaml-mixed-indent.yaml                | 29 ++++++++
 .../bad/yaml-multi-close.yaml                 | 35 +++++++++
 .../bad/yaml-node-close.yaml                  | 31 ++++++++
 .../bad/yaml-prop-order.yaml                  | 29 ++++++++
 .../bad/yaml-prop-pairing.yaml                | 33 +++++++++
 .../bad/yaml-required-blank.yaml              | 33 +++++++++
 .../dtc/dt-style-selftest/bad/yaml-tab.yaml   | 29 ++++++++
 .../bad/yaml-trailing-comment.yaml            | 26 +++++++
 .../bad/yaml-trailing-ws.yaml                 | 29 ++++++++
 .../bad/yaml-unclosed-comment.yaml            | 30 ++++++++
 .../bad/yaml-unit-addr-prefix.yaml            | 29 ++++++++
 .../dt-style-selftest/bad/yaml-unit-addr.yaml | 29 ++++++++
 .../bad/yaml-unused-label.yaml                | 29 ++++++++
 .../bad/yaml-value-ws-multiline.yaml          | 27 +++++++
 .../dt-style-selftest/bad/yaml-value-ws.yaml  | 29 ++++++++
 .../expected/dts-spaces.dts.txt               |  2 +
 .../expected/yaml-child-addr-order.yaml.txt   |  2 +
 .../expected/yaml-child-name-order.yaml.txt   |  2 +
 .../expected/yaml-cont-align.yaml.txt         |  2 +
 .../expected/yaml-digit-node-order.yaml.txt   |  2 +
 .../expected/yaml-hex-case.yaml.txt           |  2 +
 .../expected/yaml-indent-strict.yaml.txt      |  2 +
 .../expected/yaml-label-in-string.yaml.txt    |  2 +
 .../expected/yaml-line-length.yaml.txt        |  2 +
 .../expected/yaml-mixed-indent.yaml.txt       |  3 +
 .../expected/yaml-multi-close.yaml.txt        |  3 +
 .../expected/yaml-node-close.yaml.txt         |  2 +
 .../expected/yaml-prop-order.yaml.txt         |  2 +
 .../expected/yaml-prop-pairing.yaml.txt       |  3 +
 .../expected/yaml-required-blank.yaml.txt     |  3 +
 .../expected/yaml-tab.yaml.txt                |  2 +
 .../expected/yaml-trailing-comment.yaml.txt   |  2 +
 .../expected/yaml-trailing-ws.yaml.txt        |  2 +
 .../expected/yaml-unclosed-comment.yaml.txt   |  2 +
 .../expected/yaml-unit-addr-prefix.yaml.txt   |  2 +
 .../expected/yaml-unit-addr.yaml.txt          |  2 +
 .../expected/yaml-unused-label.yaml.txt       |  2 +
 .../expected/yaml-value-ws-multiline.yaml.txt |  2 +
 .../expected/yaml-value-ws.yaml.txt           |  2 +
 .../dt-style-selftest/good/dts-cont-align.dts | 26 +++++++
 .../dtc/dt-style-selftest/good/dts-tab.dts    | 29 ++++++++
 .../dt-style-selftest/good/yaml-4space.yaml   | 41 +++++++++++
 .../good/yaml-tricky-parsing.yaml             | 57 +++++++++++++++
 scripts/dtc/dt-style-selftest/run.sh          | 71 +++++++++++++++++++
 54 files changed, 1003 insertions(+)
 create mode 100644 scripts/dtc/dt-style-selftest/bad/dts-spaces.dts
 create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-child-addr-order.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-child-name-order.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-cont-align.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-digit-node-order.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-hex-case.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-indent-strict.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-label-in-string.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-line-length.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-mixed-indent.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-multi-close.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-node-close.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-prop-order.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-prop-pairing.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-required-blank.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-tab.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-trailing-comment.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-trailing-ws.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-unclosed-comment.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-unit-addr-prefix.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-unit-addr.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-unused-label.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-value-ws-multiline.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-value-ws.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/expected/dts-spaces.dts.txt
 create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-child-addr-order.yaml.txt
 create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-child-name-order.yaml.txt
 create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-cont-align.yaml.txt
 create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-digit-node-order.yaml.txt
 create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-hex-case.yaml.txt
 create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-indent-strict.yaml.txt
 create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-label-in-string.yaml.txt
 create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-line-length.yaml.txt
 create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-mixed-indent.yaml.txt
 create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-multi-close.yaml.txt
 create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-node-close.yaml.txt
 create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-prop-order.yaml.txt
 create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-prop-pairing.yaml.txt
 create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-required-blank.yaml.txt
 create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-tab.yaml.txt
 create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-trailing-comment.yaml.txt
 create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-trailing-ws.yaml.txt
 create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-unclosed-comment.yaml.txt
 create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-unit-addr-prefix.yaml.txt
 create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-unit-addr.yaml.txt
 create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-unused-label.yaml.txt
 create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-value-ws-multiline.yaml.txt
 create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-value-ws.yaml.txt
 create mode 100644 scripts/dtc/dt-style-selftest/good/dts-cont-align.dts
 create mode 100644 scripts/dtc/dt-style-selftest/good/dts-tab.dts
 create mode 100644 scripts/dtc/dt-style-selftest/good/yaml-4space.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/good/yaml-tricky-parsing.yaml
 create mode 100755 scripts/dtc/dt-style-selftest/run.sh
Makefile +6 -0
index d59f703f9797..e0b83e6832a7 100644
@@ -295,6 +295,7 @@ no-dot-config-targets := $(clean-targets) \
 			 cscope gtags TAGS tags help% %docs check% coccicheck \
 			 $(version_h) headers headers_% archheaders archscripts \
 			 %asm-generic kernelversion %src-pkg dt_binding_check \
+			 dt_style_selftest \
 			 outputmakefile rustavailable rustfmt rustfmtcheck \
 			 run-command
 no-sync-config-targets := $(no-dot-config-targets) %install modules_sign kernelrelease \
@@ -1666,6 +1667,10 @@ PHONY += dt_compatible_check
 dt_compatible_check: dt_binding_schemas
 	$(Q)$(MAKE) $(build)=$(dtbindingtree) $@
 
+PHONY += dt_style_selftest
+dt_style_selftest:
+	$(Q)$(srctree)/scripts/dtc/dt-style-selftest/run.sh
+
 # ---------------------------------------------------------------------------
 # Modules
 
@@ -1869,6 +1874,7 @@ help:
 		echo '  dtbs_install       - Install dtbs to $(INSTALL_DTBS_PATH)'; \
 		echo '  dt_binding_check   - Validate device tree binding documents and examples'; \
 		echo '  dt_binding_schemas - Build processed device tree binding schemas'; \
+		echo '  dt_style_selftest  - Run dt-check-style fixture tests'; \
 		echo '  dtbs_check         - Validate device tree source files';\
 		echo '')
 
scripts/dtc/dt-style-selftest/bad/dts-spaces.dts new file +12 -0
new file mode 100644
index 000000000000..9dad22adce51
@@ -0,0 +1,12 @@
+// SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause
+/*
+ * Test fixture: a .dts using space indent (must use tabs).
+ */
+
+/dts-v1/;
+
+/ {
+    compatible = "example,test-board";
+    #address-cells = <1>;
+    #size-cells = <1>;
+};
scripts/dtc/dt-style-selftest/bad/yaml-child-addr-order.yaml new file +41 -0

41 lines not on this page · raw message

scripts/dtc/dt-style-selftest/bad/yaml-child-name-order.yaml new file +37 -0
new file mode 100644
index 000000000000..35d85e5573c2
@@ -0,0 +1,37 @@
+# SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/test-bad-child-name-order.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Test fixture with unaddressed children out of name order
+
+maintainers:
+  - Test User <[email protected]>
+
+properties:
+  compatible:
+    const: example,test-child-name-order
+  reg:
+    maxItems: 1
+
+required:
+  - compatible
+  - reg
+
+additionalProperties: false
+
+examples:
+  - |
+    bus@10000 {
+        compatible = "simple-bus";
+        reg = <0x10000 0x1000>;
+
+        foo {
+            label = "foo";
+        };
+
+        bar {
+            label = "bar";
+        };
+    };
scripts/dtc/dt-style-selftest/bad/yaml-cont-align.yaml new file +30 -0
new file mode 100644
index 000000000000..92778540b056
@@ -0,0 +1,30 @@
+# SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/test-bad-cont-align.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Test fixture with mis-aligned multi-line property
+
+maintainers:
+  - Test User <[email protected]>
+
+properties:
+  compatible:
+    const: example,test-cont-align
+  reg:
+    maxItems: 2
+
+required:
+  - compatible
+  - reg
+
+additionalProperties: false
+
+examples:
+  - |
+    foo@1000 {
+        compatible = "example,test-cont-align";
+        reg = <0x1000 0x100>,
+            <0x2000 0x100>;
+    };
scripts/dtc/dt-style-selftest/bad/yaml-digit-node-order.yaml new file +37 -0
new file mode 100644
index 000000000000..44a9d25e5ba0
@@ -0,0 +1,37 @@
+# SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/test-bad-digit-node-order.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Test fixture with digit-leading nodes out of address order
+
+maintainers:
+  - Test User <[email protected]>
+
+properties:
+  compatible:
+    const: example,test-digit-node-order
+
+required:
+  - compatible
+
+additionalProperties: false
+
+examples:
+  - |
+    bus@0 {
+        compatible = "simple-bus";
+        #address-cells = <1>;
+        #size-cells = <1>;
+
+        3d-engine@20 {
+            compatible = "example,3d-engine";
+            reg = <0x20 0x4>;
+        };
+
+        1wire@10 {
+            compatible = "example,1wire";
+            reg = <0x10 0x4>;
+        };
+    };
scripts/dtc/dt-style-selftest/bad/yaml-hex-case.yaml new file +29 -0
new file mode 100644
index 000000000000..b26d1bf58de9
@@ -0,0 +1,29 @@
+# SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/test-bad-hex-case.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Test fixture with uppercase hex literals
+
+maintainers:
+  - Test User <[email protected]>
+
+properties:
+  compatible:
+    const: example,test-hex-case
+  reg:
+    maxItems: 1
+
+required:
+  - compatible
+  - reg
+
+additionalProperties: false
+
+examples:
+  - |
+    foo@1000 {
+        compatible = "example,test-hex-case";
+        reg = <0xABCD 0x100>;
+    };
scripts/dtc/dt-style-selftest/bad/yaml-indent-strict.yaml new file +29 -0
new file mode 100644
index 000000000000..bee4cf118d73
@@ -0,0 +1,29 @@
+# SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/test-bad-indent-strict.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Test fixture using 2-space indent (rejected by strict mode)
+
+maintainers:
+  - Test User <[email protected]>
+
+properties:
+  compatible:
+    const: example,test-indent-strict
+  reg:
+    maxItems: 1
+
+required:
+  - compatible
+  - reg
+
+additionalProperties: false
+
+examples:
+  - |
+    device@1000 {
+      compatible = "example,test-indent-strict";
+      reg = <0x1000 0x100>;
+    };
scripts/dtc/dt-style-selftest/bad/yaml-label-in-string.yaml new file +30 -0
new file mode 100644
index 000000000000..ba512869b702
@@ -0,0 +1,30 @@
+# SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/test-bad-label-in-string.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Test fixture where a label is only "referenced" inside a string
+
+maintainers:
+  - Test User <[email protected]>
+
+properties:
+  compatible:
+    const: example,test-label-in-string
+  reg:
+    maxItems: 1
+
+required:
+  - compatible
+  - reg
+
+additionalProperties: false
+
+examples:
+  - |
+    foo: device@1000 {
+        compatible = "example,test-label-in-string";
+        reg = <0x1000 0x100>;
+        info = "see &foo for details";
+    };
scripts/dtc/dt-style-selftest/bad/yaml-line-length.yaml new file +29 -0
new file mode 100644
index 000000000000..64427bf1c385
@@ -0,0 +1,29 @@
+# SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/test-bad-line-length.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Test fixture exceeding 80 columns
+
+maintainers:
+  - Test User <[email protected]>
+
+properties:
+  compatible:
+    const: example,test-line-length
+  reg:
+    maxItems: 1
+
+required:
+  - compatible
+  - reg
+
+additionalProperties: false
+
+examples:
+  - |
+    foo@1000 {
+        compatible = "example,test-line-length-this-is-a-very-long-name-indeed-yeah";
+        reg = <0x1000 0x100>;
+    };
scripts/dtc/dt-style-selftest/bad/yaml-mixed-indent.yaml new file +29 -0
new file mode 100644
index 000000000000..5401d1a423a1
@@ -0,0 +1,29 @@
+# SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/test-bad-mixed-indent.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Test fixture mixing tabs and spaces in indent
+
+maintainers:
+  - Test User <[email protected]>
+
+properties:
+  compatible:
+    const: example,test-mixed
+  reg:
+    maxItems: 1
+
+required:
+  - compatible
+  - reg
+
+additionalProperties: false
+
+examples:
+  - |
+    device@1000 {
+    	   compatible = "example,test-mixed";
+        reg = <0x1000 0x100>;
+    };
scripts/dtc/dt-style-selftest/bad/yaml-multi-close.yaml new file +35 -0
new file mode 100644
index 000000000000..4d9fa27b50a2
@@ -0,0 +1,35 @@
+# SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/test-bad-multi-close.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Test fixture with two closing braces on one line
+
+maintainers:
+  - Test User <[email protected]>
+
+properties:
+  compatible:
+    const: example,test-multi-close
+  reg:
+    maxItems: 1
+
+required:
+  - compatible
+  - reg
+
+additionalProperties: false
+
+examples:
+  - |
+    bus@10000 {
+        compatible = "simple-bus";
+        reg = <0x10000 0x1000>;
+        #address-cells = <1>;
+        #size-cells = <1>;
+
+        device@100 {
+            compatible = "example,test-multi-close";
+            reg = <0x100 0x10>;
+        }; };
scripts/dtc/dt-style-selftest/bad/yaml-node-close.yaml new file +31 -0
new file mode 100644
index 000000000000..e107659fd9e8
@@ -0,0 +1,31 @@
+# SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/test-bad-node-close.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Test fixture with closing brace not on its own line
+
+maintainers:
+  - Test User <[email protected]>
+
+properties:
+  compatible:
+    const: example,test-node-close
+  reg:
+    maxItems: 1
+
+required:
+  - compatible
+  - reg
+
+additionalProperties: false
+
+examples:
+  - |
+    bus@10000 {
+        compatible = "simple-bus";
+        reg = <0x10000 0x1000>;
+
+        empty {};
+    };
scripts/dtc/dt-style-selftest/bad/yaml-prop-order.yaml new file +29 -0
new file mode 100644
index 000000000000..75582a3d2f6e
@@ -0,0 +1,29 @@
+# SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/test-bad-prop-order.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Test fixture with reg before compatible
+
+maintainers:
+  - Test User <[email protected]>
+
+properties:
+  compatible:
+    const: example,test-prop-order
+  reg:
+    maxItems: 1
+
+required:
+  - compatible
+  - reg
+
+additionalProperties: false
+
+examples:
+  - |
+    device@1000 {
+        reg = <0x1000 0x100>;
+        compatible = "example,test-prop-order";
+    };
scripts/dtc/dt-style-selftest/bad/yaml-prop-pairing.yaml new file +33 -0
new file mode 100644
index 000000000000..767ab21c39f3
@@ -0,0 +1,33 @@
+# SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/test-bad-prop-pairing.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Test fixture exercising <x>-names and pinctrl-names pairing
+
+maintainers:
+  - Test User <[email protected]>
+
+properties:
+  compatible:
+    const: example,test-prop-pairing
+  reg:
+    maxItems: 1
+
+required:
+  - compatible
+  - reg
+
+additionalProperties: false
+
+examples:
+  - |
+    foo@1000 {
+        compatible = "example,test-prop-pairing";
+        reg = <0x1000 0x100>;
+        clock-names = "bus";
+        clocks = <&clk 0>;
+        pinctrl-names = "default";
+        pinctrl-0 = <&p0>;
+    };
scripts/dtc/dt-style-selftest/bad/yaml-required-blank.yaml new file +33 -0
new file mode 100644
index 000000000000..8bb53240cffa
@@ -0,0 +1,33 @@
+# SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/test-bad-required-blank.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Test fixture missing required blank lines
+
+maintainers:
+  - Test User <[email protected]>
+
+properties:
+  compatible:
+    const: example,test-required-blank
+  reg:
+    maxItems: 1
+
+required:
+  - compatible
+  - reg
+
+additionalProperties: false
+
+examples:
+  - |
+    bus@10000 {
+        compatible = "simple-bus";
+        reg = <0x10000 0x1000>;
+        status = "okay";
+        child@100 {
+            reg = <0x100>;
+        };
+    };
scripts/dtc/dt-style-selftest/bad/yaml-tab.yaml new file +29 -0
new file mode 100644
index 000000000000..487d07ff8cb6
@@ -0,0 +1,29 @@
+# SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/test-bad-tab.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Test fixture with a tab in a DTS line
+
+maintainers:
+  - Test User <[email protected]>
+
+properties:
+  compatible:
+    const: example,test-tab
+  reg:
+    maxItems: 1
+
+required:
+  - compatible
+  - reg
+
+additionalProperties: false
+
+examples:
+  - |
+    device@1000 {
+        compatible = "example,test-tab";
+        reg = <0x1000 0x100>;	/* registers */
+    };
scripts/dtc/dt-style-selftest/bad/yaml-trailing-comment.yaml new file +26 -0
new file mode 100644
index 000000000000..2368ada8106f
@@ -0,0 +1,26 @@
+# SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/test-bad-trailing-comment.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Test fixture with properties out of order behind trailing comments
+
+maintainers:
+  - Test User <[email protected]>
+
+properties:
+  compatible:
+    const: example,test-trailing-comment
+
+required:
+  - compatible
+
+additionalProperties: false
+
+examples:
+  - |
+    foo@0 {  /* the device node */
+        reg = <0x0 0x4>;  /* registers */
+        compatible = "example,test-trailing-comment";  // misplaced
+    };
scripts/dtc/dt-style-selftest/bad/yaml-trailing-ws.yaml new file +29 -0
new file mode 100644
index 000000000000..5c4b4bd833c5
@@ -0,0 +1,29 @@
+# SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/test-bad-trailing.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Test fixture with trailing whitespace
+
+maintainers:
+  - Test User <[email protected]>
+
+properties:
+  compatible:
+    const: example,test-trailing
+  reg:
+    maxItems: 1
+
+required:
+  - compatible
+  - reg
+
+additionalProperties: false
+
+examples:
+  - |
+    device@1000 {
+        compatible = "example,test-trailing";  
+        reg = <0x1000 0x100>;
+    };
scripts/dtc/dt-style-selftest/bad/yaml-unclosed-comment.yaml new file +30 -0
new file mode 100644
index 000000000000..63c1c08712a5
@@ -0,0 +1,30 @@
+# SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/test-bad-unclosed-comment.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Test fixture with an unclosed /* block comment
+
+maintainers:
+  - Test User <[email protected]>
+
+properties:
+  compatible:
+    const: example,test-unclosed-comment
+  reg:
+    maxItems: 1
+
+required:
+  - compatible
+  - reg
+
+additionalProperties: false
+
+examples:
+  - |
+    /* this comment never closes
+    device@1000 {
+        compatible = "example,test-unclosed-comment";
+        reg = <0x1000 0x100>;
+    };
scripts/dtc/dt-style-selftest/bad/yaml-unit-addr-prefix.yaml new file +29 -0
new file mode 100644
index 000000000000..9b3fe508c5fd
@@ -0,0 +1,29 @@
+# SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/test-bad-unit-addr-prefix.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Test fixture with 0x-prefixed unit address
+
+maintainers:
+  - Test User <[email protected]>
+
+properties:
+  compatible:
+    const: example,test-unit-addr-prefix
+  reg:
+    maxItems: 1
+
+required:
+  - compatible
+  - reg
+
+additionalProperties: false
+
+examples:
+  - |
+    memory@0x1000 {
+        compatible = "example,test-unit-addr-prefix";
+        reg = <0x1000 0x100>;
+    };
scripts/dtc/dt-style-selftest/bad/yaml-unit-addr.yaml new file +29 -0
new file mode 100644
index 000000000000..93705cd45410
@@ -0,0 +1,29 @@
+# SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/test-bad-unit-addr.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Test fixture with malformed unit address
+
+maintainers:
+  - Test User <[email protected]>
+
+properties:
+  compatible:
+    const: example,test-unit-addr
+  reg:
+    maxItems: 1
+
+required:
+  - compatible
+  - reg
+
+additionalProperties: false
+
+examples:
+  - |
+    foo@01000 {
+        compatible = "example,test-unit-addr";
+        reg = <0x1000 0x100>;
+    };
scripts/dtc/dt-style-selftest/bad/yaml-unused-label.yaml new file +29 -0
new file mode 100644
index 000000000000..28d7176cbf08
@@ -0,0 +1,29 @@
+# SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/test-bad-unused-label.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Test fixture with an unused label
+
+maintainers:
+  - Test User <[email protected]>
+
+properties:
+  compatible:
+    const: example,test-unused-label
+  reg:
+    maxItems: 1
+
+required:
+  - compatible
+  - reg
+
+additionalProperties: false
+
+examples:
+  - |
+    dev: device@1000 {
+        compatible = "example,test-unused-label";
+        reg = <0x1000 0x100>;
+    };
scripts/dtc/dt-style-selftest/bad/yaml-value-ws-multiline.yaml new file +27 -0
new file mode 100644
index 000000000000..504bf0931c27
@@ -0,0 +1,27 @@
+# SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/test-bad-value-ws-multiline.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Test fixture with extra whitespace in a multi-line cell array
+
+maintainers:
+  - Test User <[email protected]>
+
+properties:
+  compatible:
+    const: example,test-value-ws-multiline
+
+required:
+  - compatible
+
+additionalProperties: false
+
+examples:
+  - |
+    foo@0 {
+        compatible = "example,test-value-ws-multiline";
+        reg = < 0x0 0x4
+              0x8 0xc>;
+    };
scripts/dtc/dt-style-selftest/bad/yaml-value-ws.yaml new file +29 -0
new file mode 100644
index 000000000000..342ab9f399f1
@@ -0,0 +1,29 @@
+# SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/test-bad-value-ws.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Test fixture with extra whitespace inside <...>
+
+maintainers:
+  - Test User <[email protected]>
+
+properties:
+  compatible:
+    const: example,test-value-ws
+  reg:
+    maxItems: 1
+
+required:
+  - compatible
+  - reg
+
+additionalProperties: false
+
+examples:
+  - |
+    foo@1000 {
+        compatible = "example,test-value-ws";
+        reg = < 0x1000 0x100 >;
+    };
scripts/dtc/dt-style-selftest/expected/dts-spaces.dts.txt new file +2 -0
new file mode 100644
index 000000000000..070025c4568c
@@ -0,0 +1,2 @@
+# mode=relaxed
+bad/dts-spaces.dts:1: [indent-unit-dts] indent unit must be 1 tab in DTS, got '    '
scripts/dtc/dt-style-selftest/expected/yaml-child-addr-order.yaml.txt new file +2 -0
new file mode 100644
index 000000000000..f0db79a0018b
@@ -0,0 +1,2 @@
+# mode=strict
+bad/yaml-child-addr-order.yaml:37: example 0 [child-address-order] child node @100 out of address order
scripts/dtc/dt-style-selftest/expected/yaml-child-name-order.yaml.txt new file +2 -0
new file mode 100644
index 000000000000..bb434b126191
@@ -0,0 +1,2 @@
+# mode=strict
+bad/yaml-child-name-order.yaml:34: example 0 [child-name-order] child node 'bar' out of name order
scripts/dtc/dt-style-selftest/expected/yaml-cont-align.yaml.txt new file +2 -0
new file mode 100644
index 000000000000..b5576dd0f6b1
@@ -0,0 +1,2 @@
+# mode=strict
+bad/yaml-cont-align.yaml:29: example 0 [continuation-alignment] continuation should align to column 11 (under "<" or \")
scripts/dtc/dt-style-selftest/expected/yaml-digit-node-order.yaml.txt new file +2 -0
new file mode 100644
index 000000000000..6de275e2dcb5
@@ -0,0 +1,2 @@
+# mode=strict
+bad/yaml-digit-node-order.yaml:33: example 0 [child-address-order] child node @10 out of address order
scripts/dtc/dt-style-selftest/expected/yaml-hex-case.yaml.txt new file +2 -0
new file mode 100644
index 000000000000..6600f7cd1ba5
@@ -0,0 +1,2 @@
+# mode=strict
+bad/yaml-hex-case.yaml:28: example 0 [hex-case] hex literal '0xABCD' must be lowercase
scripts/dtc/dt-style-selftest/expected/yaml-indent-strict.yaml.txt new file +2 -0
new file mode 100644
index 000000000000..5ef290d3a847
@@ -0,0 +1,2 @@
+# mode=strict
+bad/yaml-indent-strict.yaml:26: example 0 [indent-unit-strict] indent unit must be 4 spaces in strict mode, got '  '
scripts/dtc/dt-style-selftest/expected/yaml-label-in-string.yaml.txt new file +2 -0
new file mode 100644
index 000000000000..05da06f81364
@@ -0,0 +1,2 @@
+# mode=strict
+bad/yaml-label-in-string.yaml:26: example 0 [unused-labels] label 'foo' defined but never &-referenced
scripts/dtc/dt-style-selftest/expected/yaml-line-length.yaml.txt new file +2 -0
new file mode 100644
index 000000000000..89b36360caa4
@@ -0,0 +1,2 @@
+# mode=strict
+bad/yaml-line-length.yaml:27: example 0 [line-length] line exceeds 80 columns (81)
scripts/dtc/dt-style-selftest/expected/yaml-mixed-indent.yaml.txt new file +3 -0
new file mode 100644
index 000000000000..c989f8f19853
@@ -0,0 +1,3 @@
+# mode=relaxed
+bad/yaml-mixed-indent.yaml:27: example 0 [mixed-indent-chars] mixed tabs and spaces in indent
+bad/yaml-mixed-indent.yaml:27: example 0 [tab-in-dts] tab character not allowed in DTS example
scripts/dtc/dt-style-selftest/expected/yaml-multi-close.yaml.txt new file +3 -0
new file mode 100644
index 000000000000..637d0f8ea103
@@ -0,0 +1,3 @@
+# mode=strict
+bad/yaml-multi-close.yaml:35: example 0 [indent-consistent] indent mismatch (expected depth 0 * '    ')
+bad/yaml-multi-close.yaml:35: example 0 [node-close-alone] closing brace must be on its own line
scripts/dtc/dt-style-selftest/expected/yaml-node-close.yaml.txt new file +2 -0
new file mode 100644
index 000000000000..ee894747b5b9
@@ -0,0 +1,2 @@
+# mode=strict
+bad/yaml-node-close.yaml:30: example 0 [node-close-alone] closing brace must be on its own line
scripts/dtc/dt-style-selftest/expected/yaml-prop-order.yaml.txt new file +2 -0
new file mode 100644
index 000000000000..578df7209170
@@ -0,0 +1,2 @@
+# mode=strict
+bad/yaml-prop-order.yaml:28: example 0 [property-order] property 'compatible' out of canonical order (should sort before 'reg')
scripts/dtc/dt-style-selftest/expected/yaml-prop-pairing.yaml.txt new file +3 -0
new file mode 100644
index 000000000000..e6e21349a939
@@ -0,0 +1,3 @@
+# mode=strict
+bad/yaml-prop-pairing.yaml:30: example 0 [property-order] property 'clocks' out of canonical order (should sort before 'clock-names')
+bad/yaml-prop-pairing.yaml:32: example 0 [property-order] property 'pinctrl-0' out of canonical order (should sort before 'pinctrl-names')
scripts/dtc/dt-style-selftest/expected/yaml-required-blank.yaml.txt new file +3 -0
new file mode 100644
index 000000000000..04ea0bacdcb9
@@ -0,0 +1,3 @@
+# mode=strict
+bad/yaml-required-blank.yaml:29: example 0 [required-blank-lines] "status" must be preceded by a blank line
+bad/yaml-required-blank.yaml:30: example 0 [required-blank-lines] child node must be preceded by a blank line
scripts/dtc/dt-style-selftest/expected/yaml-tab.yaml.txt new file +2 -0
new file mode 100644
index 000000000000..9e83246fbaa1
@@ -0,0 +1,2 @@
+# mode=relaxed
+bad/yaml-tab.yaml:28: example 0 [tab-in-dts] tab character not allowed in DTS example
scripts/dtc/dt-style-selftest/expected/yaml-trailing-comment.yaml.txt new file +2 -0
new file mode 100644
index 000000000000..69dbb1d03239
@@ -0,0 +1,2 @@
+# mode=strict
+bad/yaml-trailing-comment.yaml:25: example 0 [property-order] property 'compatible' out of canonical order (should sort before 'reg')
scripts/dtc/dt-style-selftest/expected/yaml-trailing-ws.yaml.txt new file +2 -0
new file mode 100644
index 000000000000..cfdbc8476c73
@@ -0,0 +1,2 @@
+# mode=relaxed
+bad/yaml-trailing-ws.yaml:27: example 0 [trailing-whitespace] trailing whitespace
scripts/dtc/dt-style-selftest/expected/yaml-unclosed-comment.yaml.txt new file +2 -0
new file mode 100644
index 000000000000..9a30ee7145e6
@@ -0,0 +1,2 @@
+# mode=strict
+bad/yaml-unclosed-comment.yaml:26: example 0 [unclosed-block-comment] unclosed /* block comment
scripts/dtc/dt-style-selftest/expected/yaml-unit-addr-prefix.yaml.txt new file +2 -0
new file mode 100644
index 000000000000..8dec6c1176b5
@@ -0,0 +1,2 @@
+# mode=strict
+bad/yaml-unit-addr-prefix.yaml:26: example 0 [unit-address-format] unit address '0x1000' must not have a "0x" prefix
scripts/dtc/dt-style-selftest/expected/yaml-unit-addr.yaml.txt new file +2 -0
new file mode 100644
index 000000000000..b52f0ef20bee
@@ -0,0 +1,2 @@
+# mode=strict
+bad/yaml-unit-addr.yaml:26: example 0 [unit-address-format] unit address '01000' has leading zeros
scripts/dtc/dt-style-selftest/expected/yaml-unused-label.yaml.txt new file +2 -0
new file mode 100644
index 000000000000..4f00202f0902
@@ -0,0 +1,2 @@
+# mode=strict
+bad/yaml-unused-label.yaml:26: example 0 [unused-labels] label 'dev' defined but never &-referenced
scripts/dtc/dt-style-selftest/expected/yaml-value-ws-multiline.yaml.txt new file +2 -0
new file mode 100644
index 000000000000..3df55b1762d0
@@ -0,0 +1,2 @@
+# mode=strict
+bad/yaml-value-ws-multiline.yaml:25: example 0 [value-whitespace] extra whitespace inside <...>
scripts/dtc/dt-style-selftest/expected/yaml-value-ws.yaml.txt new file +2 -0
new file mode 100644
index 000000000000..cbb5f88fe85f
@@ -0,0 +1,2 @@
+# mode=strict
+bad/yaml-value-ws.yaml:28: example 0 [value-whitespace] extra whitespace inside <...>
scripts/dtc/dt-style-selftest/good/dts-cont-align.dts new file +26 -0
new file mode 100644
index 000000000000..36fb4eefcd83
@@ -0,0 +1,26 @@
+// SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause
+/*
+ * Test fixture: tab-indented .dts with a tab-and-space aligned
+ * multi-line property. Continuation lines mix tabs for indent and
+ * spaces for alignment by design; that must not be flagged.
+ */
+
+/dts-v1/;
+
+/ {
+	compatible = "example,test-board";
+	#address-cells = <1>;
+	#size-cells = <1>;
+
+	interrupt-controller@10000 {
+		compatible = "example,intc";
+		reg = <0x10000 0x1000>;
+		interrupts = <1 2 3>,
+			     <4 5 6>,
+			     <7 8 9>;
+		pinmux = <
+			 0x01
+			 0x02
+			 >;
+	};
+};
scripts/dtc/dt-style-selftest/good/dts-tab.dts new file +29 -0
new file mode 100644
index 000000000000..ab7b5d1242ba
@@ -0,0 +1,29 @@
+// SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause
+/*
+ * Test fixture: a properly formatted .dts using one-tab indent.
+ */
+
+/dts-v1/;
+
+/ {
+	compatible = "example,test-board";
+	#address-cells = <1>;
+	#size-cells = <1>;
+
+	bus@10000 {
+		compatible = "simple-bus";
+		reg = <0x10000 0x1000>;
+		#address-cells = <1>;
+		#size-cells = <1>;
+
+		device@100 {
+			compatible = "example,test";
+			reg = <0x100 0x10>;
+		};
+
+		device@200 {
+			compatible = "example,test";
+			reg = <0x200 0x10>;
+		};
+	};
+};
scripts/dtc/dt-style-selftest/good/yaml-4space.yaml new file +41 -0

41 lines not on this page · raw message

scripts/dtc/dt-style-selftest/good/yaml-tricky-parsing.yaml new file +57 -0

57 lines not on this page · raw message

scripts/dtc/dt-style-selftest/run.sh new file +71 -0

71 lines not on this page · raw message

Signature · 1 line
2.54.0
maintainer Applied
#11 lore raw
In reply to Daniel Golle
Quoted 272 lines · Daniel Golle

Documentation/devicetree/bindings/dts-coding-style.rst documents the style expected of DT examples and source files, but nothing existing actually enforces it. dtc tolerates arbitrary whitespace, yamllint only sees the YAML envelope and not the DTS content inside literal block scalars, and so reviewers spend cycles flagging issues that tooling could catch.

Add scripts/dtc/dt-check-style, a Python tool that checks DTS coding style on YAML binding examples and on .dts/.dtsi/.dtso source files. Rules live in a small declarative registry; each rule is tagged 'relaxed' (default) or 'strict' (opt-in for new submissions). Promoting a rule from strict to relaxed is a one-line edit once the tree catches up.

Relaxed mode covers trailing whitespace, tab characters in YAML examples, mixed tab+space indents, unclosed /* block comments, and the per-input-type indent character (1 tab for .dts/.dtsi/.dtso). It is zero-violation on the current tree and runs as part of dt_binding_check_one, so no new warnings appear in make dt_binding_check.

Strict mode adds indent unit and depth consistency; blank-line policy (no consecutive blanks or blanks at body edges; required blank line before each child node and before "status"); property ordering by canonical bucket (compatible, reg/reg-names, ranges, standard, vendor, status) with declarative within-bucket pairing rules (<x>-names follows <x>, pinctrl-names follows the last pinctrl-N) and natural-sort fallback; sibling node ordering by unit address or natural-sort name; line length (80 columns); continuation alignment of multi-line property values under the first '<' or '"' after the '='; lowercase hex literals and unit addresses; no whitespace padding inside <...>; closing-brace placement; and unused-label detection (skipped for .dtsi/.dtso since labels there are exported to includers/applies-to). Together these constrain a DT structure to a single canonical rendering modulo the author's choice of when to wrap properties for readability. Comments are intentionally out of scope. Strict mode is opt-in, intended for use by checkpatch.pl in a follow-up series.

The tool reads file paths from @argfile and parallelises across CPUs via -j N. With no -j given it picks up $PARALLELISM (set by scripts/jobserver-exec from the GNU make jobserver) and falls back to os.cpu_count() otherwise. Running as one Python invocation amortises the ruamel.yaml import across the whole tree -- ~2s on a 32-CPU host vs ~28s sequential. ruamel.yaml is the only non-stdlib dependency, already required by dtschema.

A selftest under scripts/dtc/dt-style-selftest/ pairs good/ and bad/ fixtures with expected output so rule behaviour can be exercised independently of the full tree via the dt_style_selftest top-level make target.
---
v1: https://lore.kernel.org/all/[email protected]/
v2: https://lore.kernel.org/all/[email protected]/
v3: https://lore.kernel.org/all/[email protected]/
v4: https://lore.kernel.org/all/[email protected]/

Changes since v4:
- dt-check-style: parser robustness fixes from a sashiko.dev AI review (multi-line CPP macros, "/* ... */ code" on one line, "}; };" depth tracking, malformed unit addresses, labels inside strings and on digit-leading node names, unclosed /* on PROPERTY lines)
- dt-check-style: new relaxed-mode rule unclosed-block-comment
- new patch: scripts/jobserver-exec propagates child exit status (prereq so chk_style errors stay reported)
- selftest: add fixtures covering the parser fixes
- selftest: move SPDX header to "// SPDX-..." on line 1 in the three .dts fixtures (checkpatch)

Changes since v3:
- dt-check-style: accept node and property names that begin with a digit (the DT spec permits names like 1wire@10)
- dt-check-style: strip trailing // and /* */ comments before line classification, so a trailing comment on a node-open or property line no longer causes misclassification
- dt-check-style: continuation-alignment compares display columns (tabs expanded to 8) instead of raw string length, removing many false positives on tab-indented .dts files
- dt-check-style: value-whitespace flags only whitespace directly inside the brackets (after '<', before '>'), on single- and multi-line cell lists; it no longer flags inter-value spacing, which is often deliberate column alignment
- Makefile: build the @argfile with mktemp and remove it, so no stale .tmp_.dt-style.checked is left in the build tree
- run.sh: drop bash-only process substitution (POSIX sh now) and check the checker's exit status
- selftest: add fixtures for the above (digit-leading nodes, trailing comments, multi-line cell arrays, tab+space aligned continuations) and update yaml-value-ws for the reworked value-whitespace rule

Changes since v2:
- route findings to stderr so a quiet dt_binding_check produces no output (Rob)
- switch the Makefile recipe from mktemp to Kbuild's $(tmp-target) so build output stays inside the build folder (Nathan)
- collapse the recipe's exit-handling tail into the familiar "&& touch $@ || true" pattern, matching cmd_chk_bindings; keeps future warnings non-fatal (Rob, Nathan)
- explain in patch 3/4 why the recipe uses an explicit $(PYTHON3) prefix where the neighbouring recipes call their scripts directly (Rob)
- append a trailing newline to every expected/*.txt selftest fixture (Rob)
- restore the trailing whitespace inside yaml-trailing-ws.yaml that had been silently stripped during re-application, so the selftest actually exercises the trailing-whitespace rule

Changes since v1:
- rules declared in a registry tagged relaxed/strict; default relaxed mode is zero-violation on the current tree
- tool now also accepts .dts/.dtsi/.dtso files directly (with a tab-indent variant); unused-labels skipped for .dtsi/.dtso where labels are exported to includers/applies-to
- renamed dt-check-example-style -> dt-check-style
- added -j N with $PARALLELISM (jobserver) awareness
- dropped node-name [a-z0-9-] check (Rob: better as a meta-schema)
- property-order rebuilt around buckets + declarative pairing rules plus natural-sort fallback
- added child-name-order, required-blank-lines, hex-case, unit-address-format, value-whitespace, node-close-alone, line-length and continuation-alignment to strict mode
- new selftest patch (Krzysztof: "would be happy to see at least a few test cases for it")

Default relaxed mode is zero-violation on the current tree.

Strict-mode violation counts on a current tree (5506 YAML bindings, 6530 in-tree .dts/.dtsi/.dtso under arch/):

rule yaml dts
property-order 14554 245000

  continuation-alignment   1857    93536
  indent-consistent        1562    48720
  required-blank-lines     1876    48563
  line-length               105    26322
  child-name-order          667    16821
  mixed-indent-chars          0    13294
  unused-labels            3611    11628
  child-address-order        63     4076
  blank-lines                82     1866
  node-close-alone           38      774
  value-whitespace           38      715
  hex-case                   78      669
  indent-unit-dts            --      190
  unit-address-format         5       39
  trailing-whitespace         0       10
  indent-unit-strict       1492       --
  indent-unit               179       --

(YAML and DTS columns reflect the rules that apply to each input type; e.g. indent-unit-strict is YAML-only since .dts files use tabs, indent-unit-dts is the equivalent DTS-only check. unused-labels is skipped for .dtsi/.dtso since labels there are exported to includers/applies-to.)

The script was written with generous help from Claude Opus 4.7, since my Python is even worse than my DTS coding style.

Daniel Golle (4):
dt-bindings: add DTS style checker scripts/jobserver-exec: propagate child exit status
dt-bindings: wire style checker into dt_binding_check
dt-bindings: add self-test fixtures for style checker

 Documentation/devicetree/bindings/Makefile    |   19 +-
 Makefile                                      |    6 +
 scripts/dtc/dt-check-style                    | 1192 +++++++++++++++++
 .../dtc/dt-style-selftest/bad/dts-spaces.dts  |   12 +
 .../bad/yaml-child-addr-order.yaml            |   41 +
 .../bad/yaml-child-name-order.yaml            |   37 +
 .../bad/yaml-cont-align.yaml                  |   30 +
 .../bad/yaml-digit-node-order.yaml            |   37 +
 .../dt-style-selftest/bad/yaml-hex-case.yaml  |   29 +
 .../bad/yaml-indent-strict.yaml               |   29 +
 .../bad/yaml-label-in-string.yaml             |   30 +
 .../bad/yaml-line-length.yaml                 |   29 +
 .../bad/yaml-mixed-indent.yaml                |   29 +
 .../bad/yaml-multi-close.yaml                 |   35 +
 .../bad/yaml-node-close.yaml                  |   31 +
 .../bad/yaml-prop-order.yaml                  |   29 +
 .../bad/yaml-prop-pairing.yaml                |   33 +
 .../bad/yaml-required-blank.yaml              |   33 +
 .../dtc/dt-style-selftest/bad/yaml-tab.yaml   |   29 +
 .../bad/yaml-trailing-comment.yaml            |   26 +
 .../bad/yaml-trailing-ws.yaml                 |   29 +
 .../bad/yaml-unclosed-comment.yaml            |   30 +
 .../bad/yaml-unit-addr-prefix.yaml            |   29 +
 .../dt-style-selftest/bad/yaml-unit-addr.yaml |   29 +
 .../bad/yaml-unused-label.yaml                |   29 +
 .../bad/yaml-value-ws-multiline.yaml          |   27 +
 .../dt-style-selftest/bad/yaml-value-ws.yaml  |   29 +
 .../expected/dts-spaces.dts.txt               |    2 +
 .../expected/yaml-child-addr-order.yaml.txt   |    2 +
 .../expected/yaml-child-name-order.yaml.txt   |    2 +
 .../expected/yaml-cont-align.yaml.txt         |    2 +
 .../expected/yaml-digit-node-order.yaml.txt   |    2 +
 .../expected/yaml-hex-case.yaml.txt           |    2 +
 .../expected/yaml-indent-strict.yaml.txt      |    2 +
 .../expected/yaml-label-in-string.yaml.txt    |    2 +
 .../expected/yaml-line-length.yaml.txt        |    2 +
 .../expected/yaml-mixed-indent.yaml.txt       |    3 +
 .../expected/yaml-multi-close.yaml.txt        |    3 +
 .../expected/yaml-node-close.yaml.txt         |    2 +
 .../expected/yaml-prop-order.yaml.txt         |    2 +
 .../expected/yaml-prop-pairing.yaml.txt       |    3 +
 .../expected/yaml-required-blank.yaml.txt     |    3 +
 .../expected/yaml-tab.yaml.txt                |    2 +
 .../expected/yaml-trailing-comment.yaml.txt   |    2 +
 .../expected/yaml-trailing-ws.yaml.txt        |    2 +
 .../expected/yaml-unclosed-comment.yaml.txt   |    2 +
 .../expected/yaml-unit-addr-prefix.yaml.txt   |    2 +
 .../expected/yaml-unit-addr.yaml.txt          |    2 +
 .../expected/yaml-unused-label.yaml.txt       |    2 +
 .../expected/yaml-value-ws-multiline.yaml.txt |    2 +
 .../expected/yaml-value-ws.yaml.txt           |    2 +
 .../dt-style-selftest/good/dts-cont-align.dts |   26 +
 .../dtc/dt-style-selftest/good/dts-tab.dts    |   29 +
 .../dt-style-selftest/good/yaml-4space.yaml   |   41 +
 .../good/yaml-tricky-parsing.yaml             |   57 +
 scripts/dtc/dt-style-selftest/run.sh          |   71 +
 scripts/jobserver-exec                        |    4 +-
 57 files changed, 2214 insertions(+), 4 deletions(-)
 create mode 100755 scripts/dtc/dt-check-style
 create mode 100644 scripts/dtc/dt-style-selftest/bad/dts-spaces.dts
 create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-child-addr-order.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-child-name-order.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-cont-align.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-digit-node-order.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-hex-case.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-indent-strict.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-label-in-string.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-line-length.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-mixed-indent.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-multi-close.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-node-close.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-prop-order.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-prop-pairing.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-required-blank.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-tab.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-trailing-comment.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-trailing-ws.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-unclosed-comment.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-unit-addr-prefix.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-unit-addr.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-unused-label.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-value-ws-multiline.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-value-ws.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/expected/dts-spaces.dts.txt
 create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-child-addr-order.yaml.txt
 create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-child-name-order.yaml.txt
 create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-cont-align.yaml.txt
 create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-digit-node-order.yaml.txt
 create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-hex-case.yaml.txt
 create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-indent-strict.yaml.txt
 create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-label-in-string.yaml.txt
 create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-line-length.yaml.txt
 create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-mixed-indent.yaml.txt
 create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-multi-close.yaml.txt
 create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-node-close.yaml.txt
 create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-prop-order.yaml.txt
 create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-prop-pairing.yaml.txt
 create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-required-blank.yaml.txt
 create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-tab.yaml.txt
 create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-trailing-comment.yaml.txt
 create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-trailing-ws.yaml.txt
 create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-unclosed-comment.yaml.txt
 create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-unit-addr-prefix.yaml.txt
 create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-unit-addr.yaml.txt
Quoted 8 lines
 create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-unused-label.yaml.txt
 create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-value-ws-multiline.yaml.txt
 create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-value-ws.yaml.txt
 create mode 100644 scripts/dtc/dt-style-selftest/good/dts-cont-align.dts
 create mode 100644 scripts/dtc/dt-style-selftest/good/dts-tab.dts
 create mode 100644 scripts/dtc/dt-style-selftest/good/yaml-4space.yaml
 create mode 100644 scripts/dtc/dt-style-selftest/good/yaml-tricky-parsing.yaml
 create mode 100755 scripts/dtc/dt-style-selftest/run.sh

Applied, thanks!

Rob