Add dpdk-checkpatch.py as a standalone alternative to checkpatches.sh.
Unlike the existing shell script wrapper around checkpatch.pl, this
requires no Linux kernel source tree or Perl installation and is
significantly faster (~0.4s vs ~2m23s on a recent patch series).

Supports the same usage patterns: patch files, mbox bundles, git commit
ranges (-r), last N commits (-n), and stdin. Implements common style
checks (spacing, line length, spelling via codespell) plus
DPDK-specific forbidden token and tag checks.

Signed-off-by: Stephen Hemminger <[email protected]>
Acked-by: Bruce Richardson <[email protected]>
---
v6 - cleanup several false positives

 devtools/dpdk-checkpatch.py | 1829 +++++++++++++++++++++++++++++++++++
 1 file changed, 1829 insertions(+)
 create mode 100755 devtools/dpdk-checkpatch.py

diff --git a/devtools/dpdk-checkpatch.py b/devtools/dpdk-checkpatch.py
new file mode 100755
index 0000000000..64111c4eb7
--- /dev/null
+++ b/devtools/dpdk-checkpatch.py
@@ -0,0 +1,1829 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright(c) 2026 Stephen Hemminger
+#
+# dpdk-checkpatch.py - Check patches for common style issues
+#
+# This is a standalone Python replacement for the DPDK checkpatches.sh
+# script that previously wrapped the Linux kernel's checkpatch.pl.
+#
+# Usage examples:
+#   # Check patch files
+#   dpdk-checkpatch.py *.patch
+#
+#   # Check patches before applying
+#   dpdk-checkpatch.py *.patch && git am *.patch
+#
+#   # Check commits since origin/main
+#   dpdk-checkpatch.py
+#
+#   # Quiet mode for scripting
+#   if dpdk-checkpatch.py -q "$patch"; then
+#       echo "Clean, applying..."
+#       git am "$patch"
+#   else
+#       echo "Issues found, skipping"
+#   fi
+#
+#   # Verbose output with context
+#   dpdk-checkpatch.py -v my-feature.patch
+
+import argparse
+import os
+import re
+import subprocess
+import sys
+import tempfile
+from dataclasses import dataclass, field
+from pathlib import Path
+from typing import Optional
+
+VERSION = "1.0"
+
+# Default configuration
+DEFAULT_LINE_LENGTH = 100
+DEFAULT_CODESPELL_DICT = "/usr/share/codespell/dictionary.txt"
+
+
+@dataclass
+class CheckResult:
+    """Result of a single check."""
+
+    level: str  # ERROR, WARNING, CHECK
+    type_name: str
+    message: str
+    filename: str = ""
+    line_num: int = 0
+    line_content: str = ""
+
+
+@dataclass
+class PatchInfo:
+    """Information extracted from a patch."""
+
+    subject: str = ""
+    author: str = ""
+    author_email: str = ""
+    signoffs: list = field(default_factory=list)
+    files: list = field(default_factory=list)
+    added_lines: dict = field(default_factory=dict)  # filename -> [(line_num, 
content)]
+    context_before: dict = field(
+        default_factory=dict
+    )  # filename -> {line_num: context_line}
+    has_fixes_tag: bool = False
+    fixes_commits: list = field(default_factory=list)
+
+
+class CheckPatch:
+    """Main class for checking patches."""
+
+    def __init__(self, config: dict):
+        self.config = config
+        self.results: list[CheckResult] = []
+        self.errors = 0
+        self.warnings = 0
+        self.checks = 0
+        self.lines_checked = 0
+
+        # Load codespell dictionary if enabled
+        self.spelling_dict = {}
+        if config.get("codespell"):
+            self._load_codespell_dict()
+
+        # Forbidden token rules for DPDK
+        self.forbidden_rules = self._init_forbidden_rules()
+
+    def _load_codespell_dict(self) -> None:
+        """Load the codespell dictionary."""
+        dict_path = self.config.get("codespell_file")
+
+        if not dict_path:
+            # Search common locations for the dictionary
+            search_paths = [
+                DEFAULT_CODESPELL_DICT,
+                
"/usr/local/lib/python3.12/dist-packages/codespell_lib/data/dictionary.txt",
+                
"/usr/local/lib/python3.11/dist-packages/codespell_lib/data/dictionary.txt",
+                
"/usr/local/lib/python3.10/dist-packages/codespell_lib/data/dictionary.txt",
+                
"/usr/lib/python3/dist-packages/codespell_lib/data/dictionary.txt",
+            ]
+
+            # Also try to find it via codespell module
+            try:
+                import codespell_lib
+
+                module_path = os.path.join(
+                    os.path.dirname(codespell_lib.__file__), "data", 
"dictionary.txt"
+                )
+                search_paths.insert(0, module_path)
+            except ImportError:
+                pass
+
+            for path in search_paths:
+                if os.path.exists(path):
+                    dict_path = path
+                    break
+
+        if not dict_path or not os.path.exists(dict_path):
+            return
+
+        try:
+            with open(dict_path, "r", encoding="utf-8", errors="ignore") as f:
+                for line in f:
+                    line = line.strip()
+                    if not line or line.startswith("#"):
+                        continue
+                    parts = line.split("->")
+                    if len(parts) >= 2:
+                        wrong = parts[0].strip().lower()
+                        correct = parts[1].strip().split(",")[0].strip()
+                        self.spelling_dict[wrong] = correct
+        except IOError:
+            pass
+
+    def _init_forbidden_rules(self) -> list:
+        """Initialize DPDK-specific forbidden token rules."""
+        return [
+            # Refrain from new calls to RTE_LOG in libraries
+            {
+                "folders": ["lib"],
+                "patterns": [r"RTE_LOG\("],
+                "message": "Prefer RTE_LOG_LINE",
+            },
+            # Refrain from new calls to RTE_LOG in drivers
+            {
+                "folders": ["drivers"],
+                "skip_files": [r".*osdep\.h$"],
+                "patterns": [r"RTE_LOG\(", r"RTE_LOG_DP\(", r"rte_log\("],
+                "message": "Prefer RTE_LOG_LINE/RTE_LOG_DP_LINE",
+            },
+            # No output on stdout or stderr
+            {
+                "folders": ["lib", "drivers"],
+                "patterns": [r"\bprintf\b", r"fprintf\(stdout,", 
r"fprintf\(stderr,"],
+                "message": "Writing to stdout or stderr",
+            },
+            # Refrain from rte_panic() and rte_exit()
+            {
+                "folders": ["lib", "drivers"],
+                "patterns": [r"rte_panic\(", r"rte_exit\("],
+                "message": "Using rte_panic/rte_exit",
+            },
+            # Don't call directly install_headers()
+            {
+                "folders": ["lib", "drivers"],
+                "patterns": [r"\binstall_headers\b"],
+                "message": "Using install_headers()",
+            },
+            # Refrain from using compiler attribute without common macro
+            {
+                "folders": ["lib", "drivers", "app", "examples"],
+                "skip_files": [r"lib/eal/include/rte_common\.h"],
+                "patterns": [r"__attribute__"],
+                "message": "Using compiler attribute directly",
+            },
+            # Check %l or %ll format specifier
+            {
+                "folders": ["lib", "drivers", "app", "examples"],
+                "patterns": [r"%ll*[xud]"],
+                "message": "Using %l format, prefer %PRI*64 if type is 
[u]int64_t",
+            },
+            # Refrain from 16/32/64 bits rte_atomicNN_xxx()
+            {
+                "folders": ["lib", "drivers", "app", "examples"],
+                "patterns": [r"rte_atomic[0-9][0-9]_.*\("],
+                "message": "Using rte_atomicNN_xxx",
+            },
+            # Refrain from rte_smp_[r/w]mb()
+            {
+                "folders": ["lib", "drivers", "app", "examples"],
+                "patterns": [r"rte_smp_(r|w)?mb\("],
+                "message": "Using rte_smp_[r/w]mb",
+            },
+            # Refrain from __sync_xxx builtins
+            {
+                "folders": ["lib", "drivers", "app", "examples"],
+                "patterns": [r"__sync_.*\("],
+                "message": "Using __sync_xxx builtins",
+            },
+            # Refrain from __rte_atomic_thread_fence()
+            {
+                "folders": ["lib", "drivers", "app", "examples"],
+                "patterns": [r"__rte_atomic_thread_fence\("],
+                "message": "Using __rte_atomic_thread_fence, prefer 
rte_atomic_thread_fence",
+            },
+            # Refrain from __atomic_xxx builtins
+            {
+                "folders": ["lib", "drivers", "app", "examples"],
+                "skip_files": [r"drivers/common/cnxk/"],
+                "patterns": [
+                    r"__atomic_.*\(",
+                    
r"__ATOMIC_(RELAXED|CONSUME|ACQUIRE|RELEASE|ACQ_REL|SEQ_CST)",
+                ],
+                "message": "Using __atomic_xxx/__ATOMIC_XXX built-ins, prefer 
rte_atomic_xxx/rte_memory_order_xxx",
+            },
+            # Refrain from some pthread functions
+            {
+                "folders": ["lib", "drivers", "app", "examples"],
+                "patterns": [
+                    
r"pthread_(create|join|detach|set(_?name_np|affinity_np)|attr_set(inheritsched|schedpolicy))\("
+                ],
+                "message": "Using pthread functions, prefer rte_thread",
+            },
+            # Forbid use of __reserved
+            {
+                "folders": ["lib", "drivers", "app", "examples"],
+                "patterns": [r"\b__reserved\b"],
+                "message": "Using __reserved",
+            },
+            # Forbid use of __alignof__
+            {
+                "folders": ["lib", "drivers", "app", "examples"],
+                "patterns": [r"\b__alignof__\b"],
+                "message": "Using __alignof__, prefer C11 alignof",
+            },
+            # Forbid use of __typeof__
+            {
+                "folders": ["lib", "drivers", "app", "examples"],
+                "patterns": [r"\b__typeof__\b"],
+                "message": "Using __typeof__, prefer typeof",
+            },
+            # Forbid use of __builtin_*
+            {
+                "folders": ["lib", "drivers", "app", "examples"],
+                "skip_files": [
+                    r"lib/eal/",
+                    r"drivers/.*/base/",
+                    r"drivers/.*osdep\.h$",
+                ],
+                "patterns": [r"\b__builtin_"],
+                "message": "Using __builtin helpers, prefer EAL macros",
+            },
+            # Forbid inclusion of linux/pci_regs.h
+            {
+                "folders": ["lib", "drivers", "app", "examples"],
+                "patterns": [r"include.*linux/pci_regs\.h"],
+                "message": "Using linux/pci_regs.h, prefer rte_pci.h",
+            },
+            # Forbid variadic argument pack extension in macros
+            {
+                "folders": ["lib", "drivers", "app", "examples"],
+                "patterns": [r"#\s*define.*[^(,\s]\.\.\.[\s]*\)"],
+                "message": "Do not use variadic argument pack in macros",
+            },
+            # Forbid __rte_packed_begin with enums
+            {
+                "folders": ["lib", "drivers", "app", "examples"],
+                "patterns": [r"enum.*__rte_packed_begin"],
+                "message": "Using __rte_packed_begin with enum is not allowed",
+            },
+            # Forbid use of #pragma
+            {
+                "folders": ["lib", "drivers", "app", "examples"],
+                "skip_files": [r"lib/eal/include/rte_common\.h"],
+                "patterns": [r"(#pragma|_Pragma)"],
+                "message": "Using compilers pragma is not allowed",
+            },
+            # Forbid experimental build flag except in examples
+            {
+                "folders": ["lib", "drivers", "app"],
+                "patterns": [r"-DALLOW_EXPERIMENTAL_API", 
r"allow_experimental_apis"],
+                "message": "Using experimental build flag for in-tree 
compilation",
+            },
+            # Refrain from using RTE_LOG_REGISTER for drivers and libs
+            {
+                "folders": ["lib", "drivers"],
+                "patterns": [r"\bRTE_LOG_REGISTER\b"],
+                "message": "Using RTE_LOG_REGISTER, prefer 
RTE_LOG_REGISTER_(DEFAULT|SUFFIX)",
+            },
+            # Forbid non-internal thread in drivers and libs
+            {
+                "folders": ["lib", "drivers"],
+                "patterns": [r"rte_thread_(set_name|create_control)\("],
+                "message": "Prefer 
rte_thread_(set_prefixed_name|create_internal_control)",
+            },
+        ]
+
+    def add_result(
+        self,
+        level: str,
+        type_name: str,
+        message: str,
+        filename: str = "",
+        line_num: int = 0,
+        line_content: str = "",
+    ) -> None:
+        """Add a check result."""
+        result = CheckResult(
+            level=level,
+            type_name=type_name,
+            message=message,
+            filename=filename,
+            line_num=line_num,
+            line_content=line_content,
+        )
+        self.results.append(result)
+
+        if level == "ERROR":
+            self.errors += 1
+        elif level == "WARNING":
+            self.warnings += 1
+        else:
+            self.checks += 1
+
+    def parse_patch(self, content: str) -> PatchInfo:
+        """Parse a patch and extract information."""
+        info = PatchInfo()
+        current_file = ""
+        in_diff = False
+        line_num_in_new = 0
+
+        lines = content.split("\n")
+        for i, line in enumerate(lines):
+            # Extract subject
+            if line.startswith("Subject:"):
+                subject = line[8:].strip()
+                # Handle multi-line subjects
+                j = i + 1
+                while j < len(lines) and lines[j].startswith(" "):
+                    subject += " " + lines[j].strip()
+                    j += 1
+                info.subject = subject
+
+            # Extract author
+            if line.startswith("From:"):
+                info.author = line[5:].strip()
+                match = re.search(r"<([^>]+)>", info.author)
+                if match:
+                    info.author_email = match.group(1)
+
+            # Extract Signed-off-by
+            match = re.match(r"^Signed-off-by:\s*(.+)$", line, re.IGNORECASE)
+            if match:
+                info.signoffs.append(match.group(1).strip())
+
+            # Extract Fixes tag
+            match = re.match(r"^Fixes:\s*([0-9a-fA-F]+)", line)
+            if match:
+                info.has_fixes_tag = True
+                info.fixes_commits.append(match.group(1))
+
+            # Track files in diff
+            if line.startswith("diff --git"):
+                match = re.match(r"diff --git a/(\S+) b/(\S+)", line)
+                if match:
+                    current_file = match.group(2)
+                    if current_file not in info.files:
+                        info.files.append(current_file)
+                    info.added_lines[current_file] = []
+                in_diff = True
+
+            # Track hunks
+            if line.startswith("@@"):
+                match = re.match(r"@@ -\d+(?:,\d+)? \+(\d+)", line)
+                if match:
+                    line_num_in_new = int(match.group(1))
+                continue
+
+            # Track added lines
+            if in_diff and current_file:
+                if line.startswith("+") and not line.startswith("+++"):
+                    info.added_lines[current_file].append((line_num_in_new, 
line[1:]))
+                    line_num_in_new += 1
+                elif line.startswith("-"):
+                    pass  # Deleted line, don't increment
+                elif not line.startswith("\\"):
+                    # Context line - store it for reference by line number
+                    if current_file not in info.context_before:
+                        info.context_before[current_file] = {}
+                    info.context_before[current_file][line_num_in_new] = (
+                        line[1:] if line.startswith(" ") else line
+                    )
+                    line_num_in_new += 1
+
+        return info
+
+    def check_line_length(self, patch_info: PatchInfo) -> None:
+        """Check for lines exceeding maximum length."""
+        max_len = self.config.get("max_line_length", DEFAULT_LINE_LENGTH)
+
+        for filename, lines in patch_info.added_lines.items():
+            # Skip documentation files - they have tables and other content
+            # where long lines are acceptable
+            if filename.endswith((".rst", ".md", ".txt")) or "/doc/" in 
filename:
+                continue
+
+            for line_num, content in lines:
+                # Skip strings that span multiple lines
+                if len(content) > max_len:
+                    # Don't warn about long strings or URLs
+                    if '"' in content and content.count('"') >= 2:
+                        continue
+                    if "http://"; in content or "https://"; in content:
+                        continue
+                    # Check if it's a comment line
+                    if (
+                        content.strip().startswith("/*")
+                        or content.strip().startswith("*")
+                        or content.strip().startswith("//")
+                    ):
+                        self.add_result(
+                            "WARNING",
+                            "LONG_LINE_COMMENT",
+                            f"line length of {len(content)} exceeds {max_len} 
columns",
+                            filename,
+                            line_num,
+                            content,
+                        )
+                    else:
+                        self.add_result(
+                            "WARNING",
+                            "LONG_LINE",
+                            f"line length of {len(content)} exceeds {max_len} 
columns",
+                            filename,
+                            line_num,
+                            content,
+                        )
+
+    def check_trailing_whitespace(self, patch_info: PatchInfo) -> None:
+        """Check for trailing whitespace."""
+        for filename, lines in patch_info.added_lines.items():
+            for line_num, content in lines:
+                if content != content.rstrip():
+                    self.add_result(
+                        "WARNING",
+                        "TRAILING_WHITESPACE",
+                        "trailing whitespace",
+                        filename,
+                        line_num,
+                        content,
+                    )
+
+    def check_tabs_spaces(self, patch_info: PatchInfo) -> None:
+        """Check for space before tab and mixed indentation."""
+        for filename, lines in patch_info.added_lines.items():
+            for line_num, content in lines:
+                if " \t" in content:
+                    self.add_result(
+                        "WARNING",
+                        "SPACE_BEFORE_TAB",
+                        "space before tab in indent",
+                        filename,
+                        line_num,
+                        content,
+                    )
+
+    def check_signoff(self, patch_info: PatchInfo) -> None:
+        """Check for Signed-off-by line."""
+        if not patch_info.signoffs:
+            self.add_result(
+                "ERROR", "MISSING_SIGN_OFF", "Missing Signed-off-by: line(s)"
+            )
+
+    def check_coding_style(self, patch_info: PatchInfo) -> None:
+        """Check various coding style issues."""
+        for filename, lines in patch_info.added_lines.items():
+            # Skip non-C files for most checks
+            is_c_file = filename.endswith((".c", ".h"))
+            is_c_source = filename.endswith(".c")
+            is_header = filename.endswith(".h")
+
+            prev_line = ""
+            indent_stack = []
+            context_before = patch_info.context_before.get(filename, {})
+            for line_num, content in lines:
+                self.lines_checked += 1
+
+                # Check if the line immediately before this one (which may be
+                # a context line from the patch) ended with backslash 
continuation
+                prev_context = context_before.get(line_num - 1, "")
+                in_macro_continuation = prev_context.rstrip().endswith("\\")
+
+                if is_c_file:
+                    # Check for extern function declarations in .c files
+                    # Only flag functions (have parentheses), not data
+                    if is_c_source and re.match(r"^\s*extern\b", content):
+                        if re.search(r"\(", content):
+                            self.add_result(
+                                "WARNING",
+                                "AVOID_EXTERNS",
+                                "extern is not needed for function 
declarations",
+                                filename,
+                                line_num,
+                                content,
+                            )
+
+                    # Check for unnecessary break after goto/return/continue
+                    # Only flag if the previous statement is unconditional 
(not inside an if)
+                    if re.match(r"^\s*break\s*;", content):
+                        # Check if previous line is an unconditional 
return/goto/continue
+                        # It's unconditional if it starts at the same or lower 
indentation as break
+                        # or if it's a plain return/goto not inside an if block
+                        prev_stripped = prev_line.strip() if prev_line else ""
+                        if re.match(
+                            r"^(goto\s+\w+|return\b|continue)\s*[^;]*;\s*$",
+                            prev_stripped,
+                        ):
+                            # Check indentation - if prev line has same or 
less indentation, it's unconditional
+                            break_indent = len(content) - len(content.lstrip())
+                            prev_indent = (
+                                len(prev_line) - len(prev_line.lstrip())
+                                if prev_line
+                                else 0
+                            )
+                            # Only flag if the return/goto is at the same 
indentation level
+                            # (meaning it's not inside a nested if block)
+                            if prev_indent <= break_indent:
+                                self.add_result(
+                                    "WARNING",
+                                    "UNNECESSARY_BREAK",
+                                    "break is not useful after a goto or 
return",
+                                    filename,
+                                    line_num,
+                                    content,
+                                )
+
+                    # STRNCPY: should use strlcpy
+                    if re.search(r"\bstrncpy\s*\(", content):
+                        self.add_result(
+                            "WARNING",
+                            "STRNCPY",
+                            "Prefer strlcpy over strncpy - see: 
https://lore.kernel.org/r/CAHk-=wgfRnXz0W3D37d01q3JFkr_i_uTL=v6a6g1ouzcprm...@mail.gmail.com/";,
+                            filename,
+                            line_num,
+                            content,
+                        )
+
+                    # STRCPY: unsafe string copy
+                    if re.search(r"\bstrcpy\s*\(", content):
+                        self.add_result(
+                            "ERROR",
+                            "STRCPY",
+                            "strcpy is unsafe - use strlcpy or snprintf",
+                            filename,
+                            line_num,
+                            content,
+                        )
+
+                    # Check for complex macros without proper enclosure
+                    # Note: Compound literal macros like (type[]){...} are 
valid C99
+                    # and commonly used in DPDK, so we don't flag those.
+                    # Only flag macros with multiple statements without 
do-while wrapping.
+                    if re.match(r"^\s*#\s*define\s+\w+\s*\([^)]*\)\s+\{", 
content):
+                        # Macro body starts with { but is not a compound 
literal
+                        # Check if it's missing do { } while(0)
+                        if not re.search(r"\bdo\s*\{", content):
+                            self.add_result(
+                                "ERROR",
+                                "COMPLEX_MACRO",
+                                "Macros with complex values should be enclosed 
in parentheses or do { } while(0)",
+                                filename,
+                                line_num,
+                                content,
+                            )
+
+                    # SPACING: missing space before ( in control statements
+                    if re.search(r"\b(if|while|for|switch)\(", content):
+                        self.add_result(
+                            "WARNING",
+                            "SPACING",
+                            "space required before the open parenthesis '('",
+                            filename,
+                            line_num,
+                            content,
+                        )
+
+                    # SPACING: space prohibited after open square bracket
+                    # Remove string contents first to avoid false positives
+                    code_only = re.sub(r'"[^"]*"', '""', content)
+                    if re.search(r"\[\s+[^\]]", code_only) and not re.search(
+                        r"\[\s*\]", code_only
+                    ):
+                        self.add_result(
+                            "WARNING",
+                            "SPACING",
+                            "space prohibited after that open square bracket 
'['",
+                            filename,
+                            line_num,
+                            content,
+                        )
+
+                    # SPACING: space prohibited before close square bracket
+                    if re.search(r"[^\[]\s+\]", code_only):
+                        self.add_result(
+                            "WARNING",
+                            "SPACING",
+                            "space prohibited before that close square bracket 
']'",
+                            filename,
+                            line_num,
+                            content,
+                        )
+
+                    # RETURN_PARENTHESES: return with parentheses wrapping the
+                    # entire expression, e.g. "return (x + y);"
+                    # Only flag when the outermost parens span the whole value,
+                    # so "return (a) | (b);" is NOT flagged (parens are for
+                    # sub-expression grouping, not wrapping the return value).
+                    if re.search(r"\breturn\s*\([^;]+\)\s*;", content):
+                        m = re.search(r"\breturn\s+(.*?)\s*;", content)
+                        if m:
+                            expr = m.group(1).strip()
+                            if expr.startswith("(") and expr.endswith(")"):
+                                # Walk to find the matching close paren for 
the first '('
+                                depth = 0
+                                match_pos = -1
+                                for ci, ch in enumerate(expr):
+                                    if ch == "(":
+                                        depth += 1
+                                    elif ch == ")":
+                                        depth -= 1
+                                        if depth == 0:
+                                            match_pos = ci
+                                            break
+                                # Only flag if the matching ')' is the last 
character,
+                                # meaning the outer parens wrap the entire 
expression
+                                if match_pos == len(expr) - 1:
+                                    inner = expr[1:-1].strip()
+                                    # Exclude casts: (type)expr and function 
calls
+                                    is_cast = re.match(
+                                        r"^[a-zA-Z_][\w\s\*]*\b\s*[\w(]", inner
+                                    )
+                                    is_func = re.search(r"\w\s*\(", inner)
+                                    if not is_cast and not is_func:
+                                        self.add_result(
+                                            "WARNING",
+                                            "RETURN_PARENTHESES",
+                                            "return is not a function, 
parentheses are not required",
+                                            filename,
+                                            line_num,
+                                            content,
+                                        )
+
+                    # BRACES: single statement blocks that need braces
+                    # Check for if/else/while/for without braces on multiline
+                    if re.match(r"^\s*(if|else\s+if|while|for)\s*\([^{]*$", 
content):
+                        # Control statement without opening brace - check next 
line
+                        pass  # Would need lookahead
+
+                    # INITIALISED_STATIC: static initialized to 0/NULL
+                    if re.match(
+                        r"^\s*static\s+.*=\s*(0|NULL|0L|0UL|0ULL|0LL)\s*;", 
content
+                    ):
+                        self.add_result(
+                            "WARNING",
+                            "INITIALISED_STATIC",
+                            "do not initialise statics to 0 or NULL",
+                            filename,
+                            line_num,
+                            content,
+                        )
+
+                    # GLOBAL_INITIALISERS: global initialized to 0/NULL
+                    if re.match(
+                        
r"^[a-zA-Z_][a-zA-Z0-9_\s\*]*=\s*(0|NULL|0L|0UL|0ULL|0LL)\s*;",
+                        content,
+                    ):
+                        if not re.match(r"^\s*static\s+", content):
+                            self.add_result(
+                                "WARNING",
+                                "GLOBAL_INITIALISERS",
+                                "do not initialise globals to 0 or NULL",
+                                filename,
+                                line_num,
+                                content,
+                            )
+
+                    # Note: DEEP_INDENTATION check removed - without full brace
+                    # nesting tracking (as in checkpatch.pl), tab counting 
produces
+                    # too many false positives in legitimate code like 
switch/case
+                    # blocks and nested loops in driver transmit paths.
+
+                    # TRAILING_STATEMENTS: code on same line as } OR control 
statement
+                    # But allow struct/union member declarations: } name; or } 
name; /* comment */
+                    # Skip macro definitions - they often have intentional 
one-line constructs
+                    is_macro_line = content.rstrip().endswith("\\") or 
re.match(
+                        r"^\s*#\s*define", content
+                    )
+                    if not is_macro_line:
+                        if re.search(r"\}\s*[a-zA-Z_]", content) and not 
re.search(
+                            r"\}\s*(else|while)\b", content
+                        ):
+                            # Check if this is a struct/union member 
declaration or
+                            # named initializer list (e.g. } errata_vals[] = {)
+                            # Pattern: } identifier; or } identifier[]; or } 
identifier[] = {
+                            if not re.search(
+                                
r"\}\s*\w+\s*(\[\d*\])?\s*;\s*(/\*.*\*/|//.*)?\s*$",
+                                content,
+                            ) and not re.search(
+                                r"\}\s*\w+\s*(\[\d*\])?\s*=\s*\{", content
+                            ):
+                                self.add_result(
+                                    "ERROR",
+                                    "TRAILING_STATEMENTS",
+                                    "trailing statements should be on next 
line",
+                                    filename,
+                                    line_num,
+                                    content,
+                                )
+                        # Also check for if/while with statement on same line 
(not opening brace)
+                        # Pattern: if (cond) statement; or while (cond) 
statement;
+                        # Note: 'for' is excluded because its header contains 
semicolons
+                        # and nested parentheses (e.g. sizeof()) break simple 
regex matching
+                        if re.search(
+                            r"\b(if|while)\s*\([^)]+\)\s+(?![\s{])[^;]*;", 
content
+                        ):
+                            self.add_result(
+                                "ERROR",
+                                "TRAILING_STATEMENTS",
+                                "trailing statements should be on next line",
+                                filename,
+                                line_num,
+                                content,
+                            )
+
+                    # CONSTANT_COMPARISON: Yoda conditions (constant on left)
+                    # Check for constants on left side of any comparison 
operator
+                    # Strip comments and strings to avoid false positives
+                    cmp_content = content.strip()
+                    cmp_content = re.sub(r"/\*.*?\*/", "", cmp_content)  # 
inline /* */
+                    cmp_content = re.sub(r"//.*$", "", cmp_content)  # 
trailing //
+                    cmp_content = re.sub(r'"[^"]*"', '""', cmp_content)  # 
strings
+                    if cmp_content and not cmp_content.startswith("*"):
+                        if re.search(
+                            r"\b(NULL|true|false)\s*[!=<>]=?\s*[&*\w]", 
cmp_content
+                        ) or re.search(r"[\s(]\s*0\s*[!=<>]=?\s*[&*\w]", 
cmp_content):
+                            # Exclude static_assert - operand order doesn't 
matter
+                            if not re.match(r"^\s*static_assert\s*\(", 
cmp_content):
+                                self.add_result(
+                                    "WARNING",
+                                    "CONSTANT_COMPARISON",
+                                    "Comparisons should place the constant on 
the right side",
+                                    filename,
+                                    line_num,
+                                    content,
+                                )
+
+                    # BRACES: single statement block should not have braces 
(or vice versa)
+                    # Check for if/else/while/for with single statement in 
braces
+                    if re.match(r"^\s*(if|while|for)\s*\([^)]+\)\s*\{\s*$", 
prev_line):
+                        if re.match(r"^\s*\w.*;\s*$", content) and not 
re.search(
+                            
r"^\s*(if|else|while|for|switch|case|default|return\s*;)",
+                            content,
+                        ):
+                            # Check if next line is just closing brace - would 
need lookahead
+                            pass
+
+                    # ONE_SEMICOLON: double semicolon
+                    if re.search(r";;", content) and not re.search(
+                        r"for\s*\([^)]*;;", content
+                    ):
+                        self.add_result(
+                            "WARNING",
+                            "ONE_SEMICOLON",
+                            "Statements terminations use 1 semicolon",
+                            filename,
+                            line_num,
+                            content,
+                        )
+
+                    # CODE_INDENT/LEADING_SPACE: spaces used for indentation 
instead of tabs
+                    if re.match(
+                        r"^    +[^\s]", content
+                    ) and not content.strip().startswith("*"):
+                        # Line starts with spaces (not tabs) - but allow for 
alignment in comments
+                        self.add_result(
+                            "WARNING",
+                            "CODE_INDENT",
+                            "code indent should use tabs where possible",
+                            filename,
+                            line_num,
+                            content,
+                        )
+
+                    # LEADING_SPACE: spaces at start of line (more general)
+                    if re.match(r"^ +\t", content):
+                        self.add_result(
+                            "WARNING",
+                            "LEADING_SPACE",
+                            "please, no spaces at the start of a line",
+                            filename,
+                            line_num,
+                            content,
+                        )
+
+                    # LINE_CONTINUATIONS: backslash continuation outside macros
+                    # Check if this line has a backslash continuation
+                    if content.rstrip().endswith("\\"):
+                        # Only flag if not inside a macro definition
+                        # A macro context means either:
+                        # - This line starts a #define
+                        # - The previous line (added or context) was a 
continuation
+                        # - This line is a preprocessor directive
+                        is_in_macro = (
+                            re.match(r"^\s*#", content)
+                            or (prev_line and 
prev_line.rstrip().endswith("\\"))
+                            or in_macro_continuation
+                        )
+                        if not is_in_macro:
+                            self.add_result(
+                                "WARNING",
+                                "LINE_CONTINUATIONS",
+                                "Avoid unnecessary line continuations",
+                                filename,
+                                line_num,
+                                content,
+                            )
+
+                    # OPEN_ENDED_LINE: lines should not end with '('
+                    # This suggests arguments should start on the same line
+                    # as the function name rather than wrapping immediately
+                    # Skip macro continuation lines and preprocessor directives
+                    stripped_end = content.rstrip()
+                    if not is_macro_line and stripped_end.endswith("("):
+                        # Don't flag control flow statements - their parens
+                        # contain conditions, not argument lists
+                        if not re.search(
+                            r"\b(if|while|for|switch)\s*\($", stripped_end
+                        ):
+                            self.add_result(
+                                "CHECK",
+                                "OPEN_ENDED_LINE",
+                                "Lines should not end with a '('",
+                                filename,
+                                line_num,
+                                content,
+                            )
+
+                    # FUNCTION_WITHOUT_ARGS: empty parens instead of (void)
+                    if is_header and re.search(r"\b\w+\s*\(\s*\)\s*;", 
content):
+                        if not re.search(
+                            r"\b(while|if|for|switch|return)\s*\(\s*\)", 
content
+                        ):
+                            self.add_result(
+                                "ERROR",
+                                "FUNCTION_WITHOUT_ARGS",
+                                "Bad function definition - use (void) instead 
of ()",
+                                filename,
+                                line_num,
+                                content,
+                            )
+
+                    # INLINE_LOCATION: inline should come after storage class
+                    if re.match(r"^\s*inline\s+(static|extern)", content):
+                        self.add_result(
+                            "ERROR",
+                            "INLINE_LOCATION",
+                            "inline keyword should sit between storage class 
and type",
+                            filename,
+                            line_num,
+                            content,
+                        )
+
+                    # STATIC_CONST: const should come after static
+                    if re.match(r"^\s*const\s+static\b", content):
+                        self.add_result(
+                            "WARNING",
+                            "STATIC_CONST",
+                            "Move const after static - use 'static const'",
+                            filename,
+                            line_num,
+                            content,
+                        )
+                        self.add_result(
+                            "WARNING",
+                            "STORAGE_CLASS",
+                            "storage class should be at the beginning of the 
declaration",
+                            filename,
+                            line_num,
+                            content,
+                        )
+
+                    # CONST_CONST: const used twice
+                    if re.search(r"\bconst\s+\w+\s+const\b", content):
+                        self.add_result(
+                            "WARNING",
+                            "CONST_CONST",
+                            "const used twice - remove duplicate const",
+                            filename,
+                            line_num,
+                            content,
+                        )
+
+                    # SELF_ASSIGNMENT: x = x (simple variable, not struct 
members)
+                    # Match only simple identifiers, not struct/pointer member 
access
+                    match = re.search(r"^\s*(\w+)\s*=\s*(\w+)\s*;", content)
+                    if match and match.group(1) == match.group(2):
+                        self.add_result(
+                            "WARNING",
+                            "SELF_ASSIGNMENT",
+                            "Do not use self-assignments to avoid compiler 
warnings",
+                            filename,
+                            line_num,
+                            content,
+                        )
+
+                    # PREFER_DEFINED_ATTRIBUTE_MACRO: prefer DPDK/kernel 
macros over __attribute__
+                    attr_macros = {
+                        "cold": "__rte_cold",
+                        "hot": "__rte_hot",
+                        "noinline": "__rte_noinline",
+                        "always_inline": "__rte_always_inline",
+                        "unused": "__rte_unused",
+                        "packed": "__rte_packed",
+                        "aligned": "__rte_aligned",
+                        "weak": "__rte_weak",
+                        "pure": "__rte_pure",
+                    }
+                    for attr, replacement in attr_macros.items():
+                        if re.search(rf"__attribute__\s*\(\s*\(\s*{attr}\b", 
content):
+                            self.add_result(
+                                "WARNING",
+                                "PREFER_DEFINED_ATTRIBUTE_MACRO",
+                                f"Prefer {replacement} over 
__attribute__(({attr}))",
+                                filename,
+                                line_num,
+                                content,
+                            )
+
+                    # POINTER_LOCATION: char* instead of char *
+                    if re.search(
+                        
r"\b(char|int|void|short|long|float|double|unsigned|signed)\*\s+\w",
+                        content,
+                    ):
+                        self.add_result(
+                            "ERROR",
+                            "POINTER_LOCATION",
+                            '"foo* bar" should be "foo *bar"',
+                            filename,
+                            line_num,
+                            content,
+                        )
+
+                    # MACRO_WITH_FLOW_CONTROL: macros with return/goto/break
+                    if re.match(
+                        
r"^\s*#\s*define\s+\w+.*\b(return|goto|break|continue)\b",
+                        content,
+                    ):
+                        self.add_result(
+                            "WARNING",
+                            "MACRO_WITH_FLOW_CONTROL",
+                            "Macros with flow control statements should be 
avoided",
+                            filename,
+                            line_num,
+                            content,
+                        )
+
+                    # MULTISTATEMENT_MACRO_USE_DO_WHILE: macros with multiple 
statements
+                    if re.match(
+                        r"^\s*#\s*define\s+\w+\([^)]*\)\s+.*;\s*[^\\]", content
+                    ):
+                        if not re.search(r"do\s*\{", content):
+                            self.add_result(
+                                "WARNING",
+                                "MULTISTATEMENT_MACRO_USE_DO_WHILE",
+                                "Macros with multiple statements should use do 
{} while(0)",
+                                filename,
+                                line_num,
+                                content,
+                            )
+
+                    # MULTISTATEMENT_MACRO_USE_DO_WHILE: macros starting with 
if
+                    if re.match(r"^\s*#\s*define\s+\w+\([^)]*\)\s+if\s*\(", 
content):
+                        self.add_result(
+                            "ERROR",
+                            "MULTISTATEMENT_MACRO_USE_DO_WHILE",
+                            "Macros starting with if should be enclosed by a 
do - while loop",
+                            filename,
+                            line_num,
+                            content,
+                        )
+
+                    # Multiple statements on one line (skip comments and 
strings)
+                    stripped_content = content.strip()
+                    if re.search(r";\s*[a-zA-Z_]", content) and "for" not in 
content:
+                        # Skip if line is a comment
+                        if not (
+                            stripped_content.startswith("/*")
+                            or stripped_content.startswith("*")
+                            or stripped_content.startswith("//")
+                        ):
+                            # Skip if the semicolon is inside a string or 
comment
+                            # Remove strings and comments before checking
+                            code_only = re.sub(
+                                r'"[^"]*"', '""', content
+                            )  # Remove string contents
+                            code_only = re.sub(
+                                r"/\*.*?\*/", "", code_only
+                            )  # Remove /* */ comments
+                            code_only = re.sub(
+                                r"//.*$", "", code_only
+                            )  # Remove // comments
+                            if re.search(r";\s*[a-zA-Z_]", code_only):
+                                self.add_result(
+                                    "CHECK",
+                                    "MULTIPLE_STATEMENTS",
+                                    "multiple statements on one line",
+                                    filename,
+                                    line_num,
+                                    content,
+                                )
+
+                    # Check for C99 comments in headers that should use C89
+                    if is_header and "//" in content:
+                        # Only flag if not in a string
+                        stripped = re.sub(r'"[^"]*"', "", content)
+                        if "//" in stripped:
+                            self.add_result(
+                                "CHECK",
+                                "C99_COMMENTS",
+                                "C99 // comments are acceptable but /* */ is 
preferred in headers",
+                                filename,
+                                line_num,
+                                content,
+                            )
+
+                    # BLOCK_COMMENT_STYLE: block comments style issues
+                    # Leading /* on its own line (but allow Doxygen /** style)
+                    if re.match(r"^\s*/\*\*+\s*$", content):
+                        # Allow /** (Doxygen) but not /*** or more
+                        if not re.match(r"^\s*/\*\*\s*$", content):
+                            self.add_result(
+                                "WARNING",
+                                "BLOCK_COMMENT_STYLE",
+                                "Block comments should not use a leading /* on 
a line by itself",
+                                filename,
+                                line_num,
+                                content,
+                            )
+                    # Trailing */ on separate line after block comment
+                    if re.match(
+                        r"^\s*\*+/\s*$", content
+                    ) and prev_line.strip().startswith("*"):
+                        pass  # This is actually acceptable
+                    # Block with trailing */ but content before it (like === 
*/)
+                    if re.search(r"\S\s*=+\s*\*/\s*$", content):
+                        self.add_result(
+                            "WARNING",
+                            "BLOCK_COMMENT_STYLE",
+                            "Block comments use a trailing */ on a separate 
line",
+                            filename,
+                            line_num,
+                            content,
+                        )
+
+                    # REPEATED_WORD: check for repeated words (case-sensitive 
to
+                    # avoid false positives like "--format FORMAT" in help 
text).
+                    # Also skip "struct foo foo;" / "union foo foo;" where the
+                    # type name and variable name are legitimately identical.
+                    words = re.findall(r"\b(\w+)\s+\1\b", content)
+                    for word in words:
+                        word_lower = word.lower()
+                        # Skip common valid repeated patterns
+                        if word_lower in ("that", "had", "long", "int", 
"short"):
+                            continue
+                        # Skip struct/union type-name used as variable name:
+                        #   struct foo foo;  or  } foo foo;
+                        if re.search(
+                            r"\b(struct|union)\s+" + re.escape(word) + r"\s+" 
+ re.escape(word) + r"\b",
+                            content,
+                        ):
+                            continue
+                        self.add_result(
+                            "WARNING",
+                            "REPEATED_WORD",
+                            f"Possible repeated word: '{word}'",
+                            filename,
+                            line_num,
+                            content,
+                        )
+
+                    # STRING_FRAGMENTS: unnecessary string concatenation like 
"foo" "bar"
+                    # Must have closing quote, whitespace, opening quote 
pattern
+                    if re.search(r'"\s*"\s*[^)]', content) and not re.search(
+                        r"#\s*define", content
+                    ):
+                        # Verify it's actually two separate strings being 
concatenated
+                        # by checking for the pattern: "..." "..."
+                        if re.search(r'"[^"]*"\s+"[^"]*"', content):
+                            self.add_result(
+                                "CHECK",
+                                "STRING_FRAGMENTS",
+                                "Consecutive strings are generally better as a 
single string",
+                                filename,
+                                line_num,
+                                content,
+                            )
+
+                prev_line = content
+
+    def check_spelling(self, patch_info: PatchInfo) -> None:
+        """Check for spelling errors using codespell dictionary."""
+        for filename, lines in patch_info.added_lines.items():
+            for line_num, content in lines:
+                # REPEATED_WORD check for non-C files (C files handled in 
check_coding_style)
+                if not filename.endswith((".c", ".h")):
+                    words = re.findall(r"\b(\w+)\s+\1\b", content)
+                    for word in words:
+                        word_lower = word.lower()
+                        if word_lower in ("that", "had", "long", "int", 
"short"):
+                            continue
+                        if re.search(
+                            r"\b(struct|union)\s+" + re.escape(word) + r"\s+" 
+ re.escape(word) + r"\b",
+                            content,
+                        ):
+                            continue
+                        self.add_result(
+                            "WARNING",
+                            "REPEATED_WORD",
+                            f"Possible repeated word: '{word}'",
+                            filename,
+                            line_num,
+                            content,
+                        )
+
+                # Spelling check
+                if self.spelling_dict:
+                    # Common abbreviations that should not be flagged as typos
+                    abbreviations = {
+                        "nd",
+                        "ns",
+                        "na",
+                        "ra",
+                        "rs",  # IPv6 Neighbor Discovery
+                        "tx",
+                        "rx",
+                        "id",
+                        "io",
+                        "ip",  # Common networking
+                        "tcp",
+                        "udp",
+                        "arp",
+                        "dns",  # Protocols
+                        "hw",
+                        "sw",
+                        "fw",  # Hardware/Software/Firmware
+                        "src",
+                        "dst",
+                        "ptr",
+                        "buf",  # Common code abbreviations
+                        "cfg",
+                        "ctx",
+                        "idx",
+                        "cnt",  # Config/Context/Index/Count
+                        "len",
+                        "num",
+                        "max",
+                        "min",  # Length/Number/Max/Min
+                        "prev",
+                        "next",
+                        "curr",  # Previous/Next/Current
+                        "init",
+                        "fini",
+                        "deinit",  # Initialize/Finish
+                        "alloc",
+                        "dealloc",
+                        "realloc",  # Memory
+                        "endcode",  # Doxygen tag
+                    }
+                    # Extract words, but skip contractions (don't, couldn't, 
etc.)
+                    # by removing them before word extraction
+                    spell_content = re.sub(r"[a-zA-Z]+n't\b", "", content)
+                    spell_content = re.sub(r"[a-zA-Z]+'[a-zA-Z]+", "", 
spell_content)
+                    words = re.findall(r"\b[a-zA-Z]+\b", spell_content)
+                    for word in words:
+                        lower_word = word.lower()
+                        if (
+                            lower_word in self.spelling_dict
+                            and lower_word not in abbreviations
+                        ):
+                            self.add_result(
+                                "WARNING",
+                                "TYPO_SPELLING",
+                                f"'{word}' may be misspelled - perhaps 
'{self.spelling_dict[lower_word]}'?",
+                                filename,
+                                line_num,
+                                content,
+                            )
+
+    def check_forbidden_tokens(self, patch_info: PatchInfo) -> None:
+        """Check for DPDK-specific forbidden tokens."""
+        for filename, lines in patch_info.added_lines.items():
+            for rule in self.forbidden_rules:
+                # Check if file is in one of the target folders
+                in_folder = False
+                for folder in rule["folders"]:
+                    if filename.startswith(folder + "/") or 
filename.startswith(
+                        "b/" + folder + "/"
+                    ):
+                        in_folder = True
+                        break
+
+                if not in_folder:
+                    continue
+
+                # Check if file should be skipped
+                skip = False
+                for skip_pattern in rule.get("skip_files", []):
+                    if re.search(skip_pattern, filename):
+                        skip = True
+                        break
+
+                if skip:
+                    continue
+
+                # Check each line for forbidden patterns
+                for line_num, content in lines:
+                    for pattern in rule["patterns"]:
+                        if re.search(pattern, content):
+                            self.add_result(
+                                "WARNING",
+                                "FORBIDDEN_TOKEN",
+                                rule["message"],
+                                filename,
+                                line_num,
+                                content,
+                            )
+                            break
+
+    def check_experimental_tags(self, patch_info: PatchInfo) -> None:
+        """Check __rte_experimental tag placement."""
+        for filename, lines in patch_info.added_lines.items():
+            for line_num, content in lines:
+                if "__rte_experimental" in content:
+                    # Should only be in headers
+                    if filename.endswith(".c"):
+                        self.add_result(
+                            "WARNING",
+                            "EXPERIMENTAL_TAG",
+                            f"Please only put __rte_experimental tags in 
headers ({filename})",
+                            filename,
+                            line_num,
+                            content,
+                        )
+                    # Should appear alone on the line
+                    stripped = content.strip()
+                    if stripped != "__rte_experimental":
+                        self.add_result(
+                            "WARNING",
+                            "EXPERIMENTAL_TAG",
+                            "__rte_experimental must appear alone on the line 
immediately preceding the return type of a function",
+                            filename,
+                            line_num,
+                            content,
+                        )
+
+    def check_internal_tags(self, patch_info: PatchInfo) -> None:
+        """Check __rte_internal tag placement."""
+        for filename, lines in patch_info.added_lines.items():
+            for line_num, content in lines:
+                if "__rte_internal" in content:
+                    # Should only be in headers
+                    if filename.endswith(".c"):
+                        self.add_result(
+                            "WARNING",
+                            "INTERNAL_TAG",
+                            f"Please only put __rte_internal tags in headers 
({filename})",
+                            filename,
+                            line_num,
+                            content,
+                        )
+                    # Should appear alone on the line
+                    stripped = content.strip()
+                    if stripped != "__rte_internal":
+                        self.add_result(
+                            "WARNING",
+                            "INTERNAL_TAG",
+                            "__rte_internal must appear alone on the line 
immediately preceding the return type of a function",
+                            filename,
+                            line_num,
+                            content,
+                        )
+
+    def check_aligned_attributes(self, patch_info: PatchInfo) -> None:
+        """Check alignment attribute usage."""
+        align_tokens = [
+            "__rte_aligned",
+            "__rte_cache_aligned",
+            "__rte_cache_min_aligned",
+        ]
+
+        for filename, lines in patch_info.added_lines.items():
+            for line_num, content in lines:
+                for token in align_tokens:
+                    if re.search(rf"\b{token}\b", content):
+                        # Should only be used with struct or union
+                        if not re.search(rf"\b(struct|union)\s*{token}\b", 
content):
+                            self.add_result(
+                                "WARNING",
+                                "ALIGNED_ATTRIBUTE",
+                                f"Please use {token} only for struct or union 
types alignment",
+                                filename,
+                                line_num,
+                                content,
+                            )
+
+    def check_packed_attributes(self, patch_info: PatchInfo) -> None:
+        """Check packed attribute usage."""
+        begin_count = 0
+        end_count = 0
+
+        for filename, lines in patch_info.added_lines.items():
+            for line_num, content in lines:
+                if "__rte_packed_begin" in content:
+                    begin_count += 1
+                    # Should be after struct, union, or alignment attributes
+                    if (
+                        not re.search(
+                            r"\b(struct|union)\s*__rte_packed_begin\b", content
+                        )
+                        and not re.search(
+                            r"__rte_cache_aligned\s*__rte_packed_begin", 
content
+                        )
+                        and not re.search(
+                            r"__rte_cache_min_aligned\s*__rte_packed_begin", 
content
+                        )
+                        and not re.search(
+                            r"__rte_aligned\(.*\)\s*__rte_packed_begin", 
content
+                        )
+                    ):
+                        self.add_result(
+                            "WARNING",
+                            "PACKED_ATTRIBUTE",
+                            "Use __rte_packed_begin only after struct, union 
or alignment attributes",
+                            filename,
+                            line_num,
+                            content,
+                        )
+
+                if "__rte_packed_end" in content:
+                    end_count += 1
+
+        if begin_count != end_count:
+            self.add_result(
+                "WARNING",
+                "PACKED_ATTRIBUTE",
+                "__rte_packed_begin and __rte_packed_end should always be used 
in pairs",
+            )
+
+    def check_patch(self, content: str, patch_file: str = None) -> bool:
+        """Run all checks on a patch."""
+        self.results = []
+        self.errors = 0
+        self.warnings = 0
+        self.checks = 0
+        self.lines_checked = 0
+
+        # Check patch format first
+        self.check_patch_format(content, patch_file)
+
+        patch_info = self.parse_patch(content)
+
+        # Run all checks
+        self.check_signoff(patch_info)
+        self.check_line_length(patch_info)
+        self.check_trailing_whitespace(patch_info)
+        self.check_tabs_spaces(patch_info)
+        self.check_coding_style(patch_info)
+        self.check_spelling(patch_info)
+        self.check_forbidden_tokens(patch_info)
+        self.check_experimental_tags(patch_info)
+        self.check_internal_tags(patch_info)
+        self.check_aligned_attributes(patch_info)
+        self.check_packed_attributes(patch_info)
+        self.check_commit_message(patch_info, content)
+
+        return self.errors == 0 and self.warnings == 0
+
+    def check_patch_format(self, content: str, patch_file: str = None) -> None:
+        """Check basic patch format for corruption."""
+        lines = content.split("\n")
+
+        # Track patch structure
+        has_diff = False
+        has_hunk = False
+        in_hunk = False
+        hunk_line = 0
+
+        for i, line in enumerate(lines, 1):
+            # Track diff headers
+            if line.startswith("diff --git"):
+                has_diff = True
+                in_hunk = False
+
+            # Parse hunk header
+            if line.startswith("@@"):
+                has_hunk = True
+                in_hunk = True
+                hunk_line = i
+                # Validate hunk header format
+                if not re.match(r"@@ -\d+(?:,\d+)? \+\d+(?:,\d+)? @@", line):
+                    self.add_result(
+                        "ERROR",
+                        "CORRUPTED_PATCH",
+                        f"patch seems to be corrupt (malformed hunk header) at 
line {i}",
+                    )
+
+            # End of patch content (signature separator)
+            elif line == "-- ":
+                in_hunk = False
+
+            # Check for lines that look like they should be in a hunk but 
aren't prefixed
+            elif (
+                in_hunk
+                and line
+                and not line.startswith(
+                    (
+                        "+",
+                        "-",
+                        " ",
+                        "\\",
+                        "diff ",
+                        "@@",
+                        "index ",
+                        "--- ",
+                        "+++ ",
+                        "new file",
+                        "deleted file",
+                        "old mode",
+                        "new mode",
+                        "rename ",
+                        "similarity",
+                        "copy ",
+                    )
+                )
+            ):
+                # This could be a wrapped line or corruption
+                # But be careful - empty lines and commit message lines are OK
+                if not line.startswith(
+                    (
+                        "From ",
+                        "Subject:",
+                        "Date:",
+                        "Signed-off-by:",
+                        "Acked-by:",
+                        "Reviewed-by:",
+                        "Tested-by:",
+                        "Fixes:",
+                        "Cc:",
+                        "---",
+                        "Message-Id:",
+                    )
+                ):
+                    # Likely a corrupted/wrapped line in the diff
+                    self.add_result(
+                        "ERROR",
+                        "CORRUPTED_PATCH",
+                        f"patch seems to be corrupt (line wrapped?) at line 
{i}",
+                    )
+                    in_hunk = False  # Stop checking this hunk
+
+        if has_diff and not has_hunk:
+            self.add_result(
+                "ERROR",
+                "CORRUPTED_PATCH",
+                "Patch appears to be corrupted (has diff but no hunks)",
+            )
+
+        # Check for DOS line endings
+        if "\r\n" in content:
+            self.add_result(
+                "ERROR",
+                "DOS_LINE_ENDINGS",
+                "Patch has DOS line endings, should be UNIX line endings",
+            )
+
+    def check_commit_message(self, patch_info: PatchInfo, content: str) -> 
None:
+        """Check commit message for issues."""
+        lines = content.split("\n")
+
+        in_commit_msg = False
+        commit_msg_lines = []
+
+        for i, line in enumerate(lines):
+            if line.startswith("Subject:"):
+                in_commit_msg = True
+                continue
+            if line.startswith("---") or line.startswith("diff --git"):
+                in_commit_msg = False
+                continue
+            if in_commit_msg:
+                commit_msg_lines.append((i + 1, line))
+
+        for line_num, line in commit_msg_lines:
+            # UNKNOWN_COMMIT_ID: Fixes tag with short or invalid commit ID
+            match = re.match(r"^Fixes:\s*([0-9a-fA-F]+)", line)
+            if match:
+                commit_id = match.group(1)
+                if len(commit_id) < 12:
+                    self.add_result(
+                        "WARNING",
+                        "UNKNOWN_COMMIT_ID",
+                        f"Commit id '{commit_id}' is too short, use at least 
12 characters",
+                        line_num=line_num,
+                        line_content=line,
+                    )
+                # Check Fixes format: should be Fixes: <hash> ("commit 
subject")
+                if not 
re.match(r'^Fixes:\s+[0-9a-fA-F]{12,}\s+\("[^"]+"\)\s*$', line):
+                    self.add_result(
+                        "WARNING",
+                        "BAD_FIXES_TAG",
+                        'Fixes: tag format should be: Fixes: <12+ char hash> 
("commit subject")',
+                        line_num=line_num,
+                        line_content=line,
+                    )
+
+    def format_results(self, show_types: bool = True) -> str:
+        """Format the results for output."""
+        output = []
+
+        for result in self.results:
+            if result.filename and result.line_num:
+                prefix = f"{result.filename}:{result.line_num}:"
+            elif result.filename:
+                prefix = f"{result.filename}:"
+            else:
+                prefix = ""
+
+            type_str = f" [{result.type_name}]" if show_types else ""
+            output.append(f"{result.level}:{type_str} {result.message}")
+
+            if prefix:
+                output.append(f"#  {prefix}")
+            if result.line_content:
+                output.append(f"+  {result.line_content}")
+            output.append("")
+
+        return "\n".join(output)
+
+    def get_summary(self) -> str:
+        """Get a summary of the check results."""
+        return f"total: {self.errors} errors, {self.warnings} warnings, 
{self.checks} checks, {self.lines_checked} lines checked"
+
+
+def split_mbox(content: str) -> list[str]:
+    """Split an mbox file into individual messages.
+
+    Mbox format uses 'From ' at the start of a line as message separator.
+    """
+    messages = []
+    current = []
+
+    for line in content.split("\n"):
+        # Standard mbox separator: line starting with "From " followed by
+        # an address or identifier and a date
+        if line.startswith("From ") and current:
+            messages.append("\n".join(current))
+            current = [line]
+        else:
+            current.append(line)
+
+    if current:
+        messages.append("\n".join(current))
+
+    return messages
+
+
+def check_single_patch(
+    checker: CheckPatch,
+    patch_path: Optional[str],
+    commit: Optional[str],
+    verbose: bool,
+    quiet: bool,
+    pre_content: Optional[str] = None,
+) -> bool:
+    """Check a single patch file or commit."""
+    subject = ""
+    content = ""
+
+    if pre_content:
+        content = pre_content
+    elif patch_path:
+        try:
+            with open(patch_path, "r", encoding="utf-8", errors="replace") as 
f:
+                content = f.read()
+        except IOError as e:
+            print(f"Error reading {patch_path}: {e}", file=sys.stderr)
+            return False
+    elif commit:
+        try:
+            result = subprocess.run(
+                [
+                    "git",
+                    "format-patch",
+                    "--find-renames",
+                    "--no-stat",
+                    "--stdout",
+                    "-1",
+                    commit,
+                ],
+                capture_output=True,
+                text=True,
+            )
+            if result.returncode != 0:
+                print(f"Error getting commit {commit}", file=sys.stderr)
+                return False
+            content = result.stdout
+        except (subprocess.CalledProcessError, FileNotFoundError) as e:
+            print(f"Error running git: {e}", file=sys.stderr)
+            return False
+    else:
+        content = sys.stdin.read()
+
+    # Extract subject
+    match = re.search(
+        r"^Subject:\s*(.+?)(?:\n(?=\S)|\n\n)", content, re.MULTILINE | 
re.DOTALL
+    )
+    if match:
+        subject = match.group(1).replace("\n ", " ").strip()
+
+    if verbose:
+        print(f"\n### {subject}\n")
+
+    is_clean = checker.check_patch(content, patch_path)
+    has_issues = checker.errors > 0 or checker.warnings > 0
+    has_any_results = has_issues or checker.checks > 0
+
+    if has_any_results or verbose:
+        if not verbose and subject:
+            print(f"\n### {subject}\n")
+        print(checker.format_results(show_types=True))
+        print(checker.get_summary())
+
+    return is_clean
+
+
+def parse_args() -> argparse.Namespace:
+    """Parse command line arguments."""
+    parser = argparse.ArgumentParser(
+        description="Check patches for DPDK coding style and common issues",
+        formatter_class=argparse.RawDescriptionHelpFormatter,
+        epilog="""
+Examples:
+  %(prog)s patch.diff                Check a patch file
+  %(prog)s -n 3                      Check last 3 commits
+  %(prog)s -r origin/main..HEAD      Check commits in range
+  cat patch.diff | %(prog)s          Check patch from stdin
+""",
+    )
+
+    parser.add_argument("patches", nargs="*", help="Patch files to check")
+    parser.add_argument("-n", type=int, metavar="NUM", help="Check last NUM 
commits")
+    parser.add_argument(
+        "-r",
+        "--range",
+        metavar="RANGE",
+        help="Check commits in git range (default: origin/main..)",
+    )
+    parser.add_argument(
+        "-q", "--quiet", action="store_true", help="Quiet mode - only show 
summary"
+    )
+    parser.add_argument(
+        "-v", "--verbose", action="store_true", help="Verbose mode - show all 
checks"
+    )
+    parser.add_argument(
+        "--max-line-length",
+        type=int,
+        default=DEFAULT_LINE_LENGTH,
+        help=f"Maximum line length (default: {DEFAULT_LINE_LENGTH})",
+    )
+    parser.add_argument(
+        "--codespell",
+        action="store_true",
+        default=True,
+        help="Enable spell checking (default: enabled)",
+    )
+    parser.add_argument(
+        "--no-codespell",
+        dest="codespell",
+        action="store_false",
+        help="Disable spell checking",
+    )
+    parser.add_argument(
+        "--codespellfile", metavar="FILE", help="Path to codespell dictionary"
+    )
+    parser.add_argument(
+        "--show-types",
+        action="store_true",
+        default=True,
+        help="Show message types (default: enabled)",
+    )
+    parser.add_argument(
+        "--no-show-types",
+        dest="show_types",
+        action="store_false",
+        help="Hide message types",
+    )
+
+    return parser.parse_args()
+
+
+def main():
+    """Main entry point."""
+    args = parse_args()
+
+    # Build configuration
+    config = {
+        "max_line_length": args.max_line_length,
+        "codespell": args.codespell,
+        "show_types": args.show_types,
+    }
+
+    if args.codespellfile:
+        config["codespell_file"] = args.codespellfile
+
+    checker = CheckPatch(config)
+
+    total = 0
+    failed = 0
+
+    if args.patches:
+        # Check specified patch files
+        for patch in args.patches:
+            try:
+                with open(patch, "r", encoding="utf-8", errors="replace") as f:
+                    content = f.read()
+            except IOError as e:
+                print(f"Error reading {patch}: {e}", file=sys.stderr)
+                total += 1
+                failed += 1
+                continue
+
+            # Check if this is an mbox with multiple patches
+            messages = split_mbox(content)
+            if len(messages) > 1:
+                for msg in messages:
+                    # Only process messages that contain diffs
+                    if "diff --git" in msg or "---" in msg:
+                        total += 1
+                        if not check_single_patch(
+                            checker, None, None, args.verbose, args.quiet, msg
+                        ):
+                            failed += 1
+            else:
+                total += 1
+                if not check_single_patch(
+                    checker, patch, None, args.verbose, args.quiet
+                ):
+                    failed += 1
+
+    elif args.n or args.range:
+        # Check git commits
+        if args.n:
+            result = subprocess.run(
+                ["git", "rev-list", "--reverse", f"--max-count={args.n}", 
"HEAD"],
+                capture_output=True,
+                text=True,
+            )
+        else:
+            git_range = args.range if args.range else "origin/main.."
+            result = subprocess.run(
+                ["git", "rev-list", "--reverse", git_range],
+                capture_output=True,
+                text=True,
+            )
+
+        if result.returncode != 0:
+            print("Error getting git commits", file=sys.stderr)
+            sys.exit(1)
+
+        commits = result.stdout.strip().split("\n")
+        for commit in commits:
+            if commit:
+                total += 1
+                if not check_single_patch(
+                    checker, None, commit, args.verbose, args.quiet
+                ):
+                    failed += 1
+
+    elif not sys.stdin.isatty():
+        # Read from stdin
+        total = 1
+        if not check_single_patch(checker, None, None, args.verbose, 
args.quiet):
+            failed += 1
+
+    else:
+        # Default to checking commits since origin/main
+        result = subprocess.run(
+            ["git", "rev-list", "--reverse", "origin/main.."],
+            capture_output=True,
+            text=True,
+        )
+
+        commits = result.stdout.strip().split("\n") if result.stdout.strip() 
else []
+        for commit in commits:
+            if commit:
+                total += 1
+                if not check_single_patch(
+                    checker, None, commit, args.verbose, args.quiet
+                ):
+                    failed += 1
+
+    # Print summary
+    passed = total - failed
+    if not args.quiet:
+        print(f"\n{passed}/{total} valid patch{'es' if passed != 1 else ''}")
+
+    sys.exit(0 if failed == 0 else 1)
+
+
+if __name__ == "__main__":
+    main()
-- 
2.53.0

Reply via email to