[PATCH 2/2] checkpatch: rewrite in Python

Guido De Rossi posted 2 patches 5 hours ago
[PATCH 2/2] checkpatch: rewrite in Python
Posted by Guido De Rossi 5 hours ago
Add scripts/checkpatch.py, a Python 3.6+ rewrite of
scripts/checkpatch.pl. This continues the effort to deprecate Perl in
kernel scripts, following scripts/get_maintainer.py.

The Python version implements the core checking infrastructure and the
most commonly triggered checks. It is designed as a drop-in replacement
with identical CLI options and output format.

Implemented checks include:
- Commit message validation (sign-off, fixes tag, line length, diff
  content, gerrit change-id, git commit references)
- Whitespace (trailing, DOS endings, tabs vs spaces, space before tab)
- SPDX license tags
- Line length limits with URL/string exceptions
- Code style (brace placement, spacing around operators, function
  parentheses, if/while/for spacing)
- API usage (volatile, printk levels, BUG variants, deprecated APIs,
  strcpy/strlcpy/strncpy, udelay/msleep, jiffies comparison)
- Type checks (new typedefs, sizeof usage, CamelCase)
- File checks (execute permissions, embedded filename, FSF address)
- Spelling/typo detection via spelling.txt and optional codespell

Checks requiring full C statement context analysis (ctx_statement_block,
annotate_values, operator spacing, macro analysis, brace balancing) are
scaffolded but simplified. These represent the remaining checks and will
be completed incrementally.

Benchmark comparison (wall time):

  Mode                       Perl    Python   Speedup
  ---------------------------------------------------
  core.c (10.9k lines)       9.0s      4.1s      2.2x
  dev.c (13.3k lines)       11.7s      4.8s      2.4x
  super.c (7.6k lines)       7.3s      3.3s      2.2x
  5 files (~50k lines)      40.7s     16.6s      2.5x
  Patch mode (1 commit)      2.8s      2.7s      1.0x
  Git mode (5 commits)      14.3s     11.8s      1.2x

Signed-off-by: Guido De Rossi <guido.derossi91@gmail.com>
---
 scripts/checkpatch.py | 2417 +++++++++++++++++++++++++++++++++++++++++
 1 file changed, 2417 insertions(+)
 create mode 100755 scripts/checkpatch.py

diff --git a/scripts/checkpatch.py b/scripts/checkpatch.py
new file mode 100755
index 000000000000..712aa373c03c
--- /dev/null
+++ b/scripts/checkpatch.py
@@ -0,0 +1,2417 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+#
+# (c) 2001, Dave Jones. (the file handling bit)
+# (c) 2005, Joel Schopp <jschopp@austin.ibm.com> (the ugly bit)
+# (c) 2007,2008, Andy Whitcroft <apw@uk.ibm.com> (new conditions, test suite)
+# (c) 2008-2010 Andy Whitcroft <apw@canonical.com>
+#
+# Python rewrite of scripts/checkpatch.pl
+
+import argparse
+import os
+import re
+import shutil
+import subprocess
+import sys
+
+P = os.path.basename(sys.argv[0])
+D = os.path.dirname(os.path.abspath(sys.argv[0]))
+
+V = '0.32'
+
+# ---- Global options with defaults ----
+quiet = 0
+verbose = False
+verbose_messages = {}
+verbose_emitted = {}
+tree = True
+chk_signoff = True
+chk_fixes_tag = True
+chk_patch = True
+tst_only = None
+emacs = False
+terse = False
+showfile = False
+file_mode = False
+git_mode = False
+git_commits = {}
+check = False
+check_orig = False
+summary = True
+mailback = False
+summary_file = False
+show_types = False
+list_types = False
+fix = False
+fix_inplace = False
+root = None
+gitroot = os.environ.get('GIT_DIR', '.git')
+debug = {}
+camelcase = {}
+use_type = {}
+use_list = []
+ignore_type = {}
+ignore_list = []
+max_line_length = 100
+min_conf_desc_length = 4
+spelling_file = os.path.join(D, 'spelling.txt')
+codespell = False
+codespellfile = '/usr/share/codespell/dictionary.txt'
+user_codespellfile = ''
+conststructsfile = os.path.join(D, 'const_structs.checkpatch')
+docsfile = os.path.join(D, '..', 'Documentation', 'dev-tools', 'checkpatch.rst')
+typedefsfile = None
+color = 'auto'
+allow_c99_comments = True
+git_command = 'export LANGUAGE=en_US.UTF-8; git'
+tabsize = 8
+CONFIG_ = 'CONFIG_'
+configuration_file = '.checkpatch.conf'
+
+maybe_linker_symbol = {}
+
+# ---- ANSI color codes ----
+RED = '\033[31m'
+YELLOW = '\033[33m'
+GREEN = '\033[32m'
+BLUE = '\033[34m'
+RESET = '\033[0m'
+
+# ---- Regex patterns (matching Perl exactly) ----
+
+Ident = r'[A-Za-z_][A-Za-z\d_]*(?:\s*\#\#\s*[A-Za-z_][A-Za-z\d_]*)*'
+Storage = r'(?:extern|static|asmlinkage)'
+Sparse = r'(?:__user|__kernel|__force|__iomem|__must_check|__kprobes|__ref|__refconst|__refdata|__rcu|__private)'
+InitAttributePrefix = r'__(?:mem|cpu|dev|net_|)'
+InitAttributeData = InitAttributePrefix + r'(?:initdata\b)'
+InitAttributeConst = InitAttributePrefix + r'(?:initconst\b)'
+InitAttributeInit = InitAttributePrefix + r'(?:init\b)'
+InitAttribute = f'(?:{InitAttributeData}|{InitAttributeConst}|{InitAttributeInit})'
+
+Attribute = (r'(?:const|volatile|__percpu|__nocast|__safe|__bitwise|__packed__|__packed2__|'
+             r'__naked|__maybe_unused|__always_unused|__noreturn|__used|__cold|__pure|'
+             r'__noclone|__deprecated|__read_mostly|__ro_after_init|__kprobes|'
+             + InitAttribute + r'|'
+             r'__aligned\s*\(.*\)|____cacheline_aligned|____cacheline_aligned_in_smp|'
+             r'____cacheline_internodealigned_in_smp|__weak|'
+             r'__alloc_size\s*\(\s*\d+\s*(?:,\s*\d+\s*)?\))')
+
+Inline = r'(?:inline|__always_inline|noinline|__inline|__inline__)'
+Member = f'(?:->{Ident}|\\.{Ident}|\\[[^\\]]*\\])'
+Lval = f'(?:{Ident}(?:{Member})*)'
+
+Int_type = r'(?:[iI])?(?:llu|ull|ll|lu|ul|l|u)'
+Binary = r'(?:[iI])?0[bB][01]+(?:' + Int_type + r')?'
+Hex = r'(?:[iI])?0[xX][0-9a-fA-F]+(?:' + Int_type + r')?'
+Int = r'[0-9]+(?:' + Int_type + r')?'
+Octal = r'0[0-7]+(?:' + Int_type + r')?'
+String = r'(?:\b[Lu])?"[X\t]*"'
+Float_hex = r'(?:[iI])?0[xX][0-9a-fA-F]+[pP]-?[0-9]+[fFlL]?'
+Float_dec = r'(?:[iI])?(?:[0-9]+\.[0-9]*|[0-9]*\.[0-9]+)(?:[eE]-?[0-9]+)?[fFlL]?'
+Float_int = r'(?:[iI])?[0-9]+[eE]-?[0-9]+[fFlL]?'
+Float = f'(?:{Float_hex}|{Float_dec}|{Float_int})'
+Constant = f'(?:{Float}|{Binary}|{Octal}|{Hex}|{Int})'
+Assignment = r'(?:\*=|/=|%=|\+=|-=|<<=|>>=|&=|\^=|\|=|=)'
+Compare = r'(?:<=|>=|==|!=|<|(?<!-)>)'
+Arithmetic = r'(?:\+|-|\*|/|%)'
+Operators = r'(?:<=|>=|==|!=|=>|->|<<|>>|<|>|!|~|&&|\|\||,|\^|\+\+|--|&|\||' + Arithmetic + r')'
+
+c90_Keywords = r'(?:do|for|while|if|else|return|goto|continue|switch|default|case|break)'
+
+NON_ASCII_UTF8 = (r'(?:[\xC2-\xDF][\x80-\xBF]'
+                  r'|\xE0[\xA0-\xBF][\x80-\xBF]'
+                  r'|[\xE1-\xEC\xEE\xEF][\x80-\xBF]{2}'
+                  r'|\xED[\x80-\x9F][\x80-\xBF]'
+                  r'|\xF0[\x90-\xBF][\x80-\xBF]{2}'
+                  r'|[\xF1-\xF3][\x80-\xBF]{3}'
+                  r'|\xF4[\x80-\x8F][\x80-\xBF]{2})')
+UTF8 = r'(?:[\x09\x0A\x0D\x20-\x7E]|' + NON_ASCII_UTF8 + r')'
+
+typeC99Typedefs = r'(?:__)?(?:[us]_?)?int_?(?:8|16|32|64)_t'
+typeOtherOSTypedefs = r'(?:u_(?:char|short|int|long)|u(?:nchar|short|int|long))'
+typeKernelTypedefs = r'(?:(?:__)?(?:u|s|be|le)(?:8|16|32|64)|atomic_t)'
+typeStdioTypedefs = r'(?:FILE)'
+typeTypedefs = f'(?:{typeC99Typedefs}\\b|{typeOtherOSTypedefs}\\b|{typeKernelTypedefs}\\b|{typeStdioTypedefs}\\b)'
+
+zero_initializer = r'(?:(?:0[xX])?0+(?:' + Int_type + r')?|NULL|false)\b'
+
+logFunctions = (r'(?:printk(?:_ratelimited|_once|_deferred_once|_deferred|)|'
+                r'(?:[a-z0-9]+_){1,2}(?:printk|emerg|alert|crit|err|warning|warn|notice|info|debug|dbg|vdbg|devel|cont|WARN)(?:_ratelimited|_once|)|'
+                r'TP_printk|WARN(?:_RATELIMIT|_ONCE|)|panic|'
+                r'MODULE_[A-Z_]+|seq_vprintf|seq_printf|seq_puts)')
+
+allocFunctions = (r'(?:(?:(?:devm_)?(?:kv|k|v)[czm]alloc(?:_array)?(?:_node)?|'
+                  r'kstrdup(?:_const)?|kmemdup(?:_nul)?)|'
+                  r'(?:\w+)?alloc_skb(?:_ip_align)?|dma_alloc_coherent)')
+
+signature_tags = (r'(?:Signed-off-by:|Co-developed-by:|Acked-by:|Tested-by:|'
+                  r'Reviewed-by:|Reported-by:|Suggested-by:|To:|Cc:)')
+
+link_tags = ['Link', 'Closes']
+link_tags_search = '(?:' + '|'.join(t + ':' for t in link_tags) + ')'
+link_tags_print = ' or '.join("'" + t + ":'" for t in link_tags)
+
+tracing_logging_tags = (r'(?:[=\-]*>|<[=\-]*|\[|\]|start|called|entered|entry|enter|in|'
+                        r'inside|here|begin|exit|end|done|leave|completed|out|return|[\.\!:\s]*)')
+
+dev_id_types = r'\b[a-z]\w*_device_id\b'
+
+obsolete_archives = (r'(?:freedesktop\.org/archives/dri-devel|'
+                     r'lists\.infradead\.org|lkml\.org|mail-archive\.com|'
+                     r'mailman\.alsa-project\.org/pipermail|marc\.info|'
+                     r'ozlabs\.org/pipermail|spinics\.net)')
+
+typeListMisordered = [
+    r'char\s+(?:un)?signed',
+    r'int\s+(?:(?:un)?signed\s+)?short\s',
+    r'int\s+short(?:\s+(?:un)?signed)',
+    r'short\s+int(?:\s+(?:un)?signed)',
+    r'(?:un)?signed\s+int\s+short',
+    r'short\s+(?:un)?signed',
+    r'long\s+int\s+(?:un)?signed',
+    r'int\s+long\s+(?:un)?signed',
+    r'long\s+(?:un)?signed\s+int',
+    r'int\s+(?:un)?signed\s+long',
+    r'int\s+(?:un)?signed',
+    r'int\s+long\s+long\s+(?:un)?signed',
+    r'long\s+long\s+int\s+(?:un)?signed',
+    r'long\s+long\s+(?:un)?signed\s+int',
+    r'long\s+long\s+(?:un)?signed',
+    r'long\s+(?:un)?signed',
+]
+
+typeList = [
+    r'void',
+    r'(?:(?:un)?signed\s+)?char',
+    r'(?:(?:un)?signed\s+)?short\s+int',
+    r'(?:(?:un)?signed\s+)?short',
+    r'(?:(?:un)?signed\s+)?int',
+    r'(?:(?:un)?signed\s+)?long\s+int',
+    r'(?:(?:un)?signed\s+)?long\s+long\s+int',
+    r'(?:(?:un)?signed\s+)?long\s+long',
+    r'(?:(?:un)?signed\s+)?long',
+    r'(?:un)?signed',
+    r'float',
+    r'double',
+    r'bool',
+    f'struct\\s+{Ident}',
+    f'union\\s+{Ident}',
+    f'enum\\s+{Ident}',
+    f'{Ident}_t',
+    f'{Ident}_handler',
+    f'{Ident}_handler_fn',
+] + typeListMisordered
+
+C90_int_types = (r'(?:long\s+long\s+int\s+(?:un)?signed|long\s+long\s+(?:un)?signed\s+int|'
+                 r'long\s+long\s+(?:un)?signed|(?:(?:un)?signed\s+)?long\s+long\s+int|'
+                 r'(?:(?:un)?signed\s+)?long\s+long|int\s+long\s+long\s+(?:un)?signed|'
+                 r'int\s+(?:(?:un)?signed\s+)?long\s+long|'
+                 r'long\s+int\s+(?:un)?signed|long\s+(?:un)?signed\s+int|'
+                 r'long\s+(?:un)?signed|(?:(?:un)?signed\s+)?long\s+int|'
+                 r'(?:(?:un)?signed\s+)?long|int\s+long\s+(?:un)?signed|'
+                 r'int\s+(?:(?:un)?signed\s+)?long|'
+                 r'int\s+(?:un)?signed|(?:(?:un)?signed\s+)?int)')
+
+typeListFile = []
+typeListWithAttr = typeList + [
+    f'struct\\s+{InitAttribute}\\s+{Ident}',
+    f'union\\s+{InitAttribute}\\s+{Ident}',
+]
+
+modifierList = [r'fastcall']
+modifierListFile = []
+
+mode_permission_funcs = [
+    ("module_param", 3),
+    ("module_param_(?:array|named|string)", 4),
+    ("module_param_array_named", 5),
+    ("debugfs_create_(?:file|u8|u16|u32|u64|x8|x16|x32|x64|size_t|atomic_t|bool|blob|regset32|u32_array)", 2),
+    ("proc_create(?:_data|)", 2),
+    ("(?:CLASS|DEVICE|SENSOR|SENSOR_DEVICE|IIO_DEVICE)_ATTR", 2),
+    ("IIO_DEV_ATTR_[A-Z_]+", 1),
+    ("SENSOR_(?:DEVICE_|)ATTR_2", 2),
+    ("SENSOR_TEMPLATE(?:_2|)", 3),
+    ("__ATTR", 2),
+]
+
+word_pattern = r'\b[A-Z]?[a-z]{2,}\b'
+
+mode_perms_search = '(?:' + '|'.join(e[0] for e in mode_permission_funcs) + ')'
+
+deprecated_apis = {
+    "kmap": "kmap_local_page",
+    "kunmap": "kunmap_local",
+    "kmap_atomic": "kmap_local_page",
+    "kunmap_atomic": "kunmap_local",
+    "DEFINE_IDR": "DEFINE_XARRAY",
+    "idr_init": "xa_init",
+    "idr_init_base": "xa_init_flags",
+}
+
+deprecated_apis_search = '(?:' + '|'.join(re.escape(k) for k in deprecated_apis) + ')'
+
+mode_perms_world_writable = r'(?:S_IWUGO|S_IWOTH|S_IRWXUGO|S_IALLUGO|0[0-7][0-7][2367])'
+
+mode_permission_string_types = {
+    "S_IRWXU": 0o700, "S_IRUSR": 0o400, "S_IWUSR": 0o200, "S_IXUSR": 0o100,
+    "S_IRWXG": 0o070, "S_IRGRP": 0o040, "S_IWGRP": 0o020, "S_IXGRP": 0o010,
+    "S_IRWXO": 0o007, "S_IROTH": 0o004, "S_IWOTH": 0o002, "S_IXOTH": 0o001,
+    "S_IRWXUGO": 0o777, "S_IRUGO": 0o444, "S_IWUGO": 0o222, "S_IXUGO": 0o111,
+}
+
+single_mode_perms_string_search = '(?:' + '|'.join(mode_permission_string_types.keys()) + ')'
+multi_mode_perms_string_search = single_mode_perms_string_search + r'(?:\s*\|\s*' + single_mode_perms_string_search + r')*'
+
+allowed_asm_includes = r'(?:irq|memory|time|reboot)'
+
+allow_repeated_words = {'add', 'added', 'bad', 'be'}
+
+# ---- Dynamically built type patterns ----
+Modifier = ''
+BasicType = ''
+NonptrType = ''
+NonptrTypeMisordered = ''
+NonptrTypeWithAttr = ''
+Type = ''
+TypeMisordered = ''
+Declare = ''
+DeclareMisordered = ''
+
+def build_types():
+    global Modifier, BasicType, NonptrType, NonptrTypeMisordered, NonptrTypeWithAttr
+    global Type, TypeMisordered, Declare, DeclareMisordered
+
+    mods = '(?:' + '|'.join(modifierList + modifierListFile) + ')'
+    all_types = '(?:' + '|'.join(typeList + typeListFile) + ')'
+    mis = '(?:' + '|'.join(typeListMisordered) + ')'
+    allWithAttr = '(?:' + '|'.join(typeListWithAttr) + ')'
+
+    Modifier = f'(?:{Attribute}|{Sparse}|{mods})'
+    BasicType = f'(?:{typeTypedefs}\\b|(?:{all_types})\\b)'
+    NonptrType = (f'(?:(?:{Modifier}\\s+|const\\s+)*'
+                  f'(?:(?:typeof|__typeof__)\\s*\\([^\\)]*\\)|{typeTypedefs}\\b|(?:{all_types})\\b)'
+                  f'(?:\\s+{Modifier}|\\s+const)*)')
+    NonptrTypeMisordered = (f'(?:(?:{Modifier}\\s+|const\\s+)*'
+                            f'(?:(?:{mis})\\b)'
+                            f'(?:\\s+{Modifier}|\\s+const)*)')
+    NonptrTypeWithAttr = (f'(?:(?:{Modifier}\\s+|const\\s+)*'
+                          f'(?:(?:typeof|__typeof__)\\s*\\([^\\)]*\\)|{typeTypedefs}\\b|(?:{allWithAttr})\\b)'
+                          f'(?:\\s+{Modifier}|\\s+const)*)')
+    Type = (f'(?:{NonptrType}'
+            f'(?:(?:\\s|\\*|\\[\\])+\\s*const|(?:\\s|\\*\\s*(?:const\\s*)?|\\[\\])+|(?:\\s*\\[\\s*\\])+){{0,4}}'
+            f'(?:\\s+{Inline}|\\s+{Modifier})*)')
+    TypeMisordered = (f'(?:{NonptrTypeMisordered}'
+                      f'(?:(?:\\s|\\*|\\[\\])+\\s*const|(?:\\s|\\*\\s*(?:const\\s*)?|\\[\\])+|(?:\\s*\\[\\s*\\])+){{0,4}}'
+                      f'(?:\\s+{Inline}|\\s+{Modifier})*)')
+    Declare = f'(?:(?:{Storage}\\s+(?:{Inline}\\s+)?)?{Type})'
+    DeclareMisordered = f'(?:(?:{Storage}\\s+(?:{Inline}\\s+)?)?{TypeMisordered})'
+
+build_types()
+
+Typecast = f'(?:\\s*(?:\\(\\s*{NonptrType}\\s*\\)){{0,1}}\\s*)'
+
+# These require recursive regex which Python doesn't have natively.
+# We approximate balanced_parens with a non-recursive depth-limited version.
+balanced_parens = r'(\((?:[^()]*|\((?:[^()]*|\([^()]*\))*\))*\))'
+LvalOrFunc = f'(?:(?:[&*]\\s*)?{Lval})\\s*(?:{balanced_parens}{{0,1}})\\s*'
+FuncArg = f'(?:{Typecast}{{0,1}}(?:{LvalOrFunc}|{Constant}|{String}))'
+
+declaration_macros = (f'(?:(?:{Storage}\\s+)?(?:[A-Z_][A-Z0-9]*_){{0,2}}(?:DEFINE|DECLARE)(?:_[A-Z0-9]+){{1,6}}\\s*\\(|'
+                      f'(?:{Storage}\\s+)?[HLP]?LIST_HEAD\\s*\\(|'
+                      r'(?:SKCIPHER_REQUEST|SHASH_DESC|AHASH_REQUEST)_ON_STACK\s*\(|'
+                      f'(?:{Storage}\\s+)?(?:XA_STATE|XA_STATE_ORDER)\\s*\\()')
+
+# Comment character used to sanitize lines
+COMMENT_CHAR = chr(1)  # \x01 used like Perl's $;
+
+# ---- Utility functions ----
+
+def which(bin_name):
+    path = shutil.which(bin_name)
+    return path if path else ""
+
+def which_conf(conf):
+    for path_dir in ['.', os.environ.get('HOME', ''), '.scripts']:
+        p = os.path.join(path_dir, conf)
+        if os.path.exists(p):
+            return p
+    return ""
+
+def top_of_kernel_tree(root_path):
+    checks = ["COPYING", "CREDITS", "Kbuild", "MAINTAINERS", "Makefile",
+              "README", "Documentation", "arch", "include", "drivers",
+              "fs", "init", "ipc", "kernel", "lib", "scripts"]
+    for c in checks:
+        if not os.path.exists(os.path.join(root_path, c)):
+            return False
+    return True
+
+def trim(s):
+    return s.strip()
+
+def ltrim(s):
+    return s.lstrip()
+
+def rtrim(s):
+    return s.rstrip()
+
+def expand_tabs(s):
+    res = ''
+    n = 0
+    for c in s:
+        if c == '\t':
+            res += ' '
+            n += 1
+            while n % tabsize != 0:
+                res += ' '
+                n += 1
+        else:
+            res += c
+            n += 1
+    return res
+
+def copy_spacing(s):
+    return re.sub(r'[^\t]', ' ', s)
+
+def line_stats(line):
+    line = line[1:] if line else ''  # drop diff marker
+    line = expand_tabs(line)
+    m = re.match(r'^(\s*)', line)
+    white = len(m.group(1)) if m else 0
+    return (len(line), white)
+
+def cat_vet(vet):
+    res = ''
+    for c in vet:
+        if c == '\t':
+            res += c
+        elif ord(c) < 32 or ord(c) == 127:
+            res += '^' + chr(ord(c) + 64) if ord(c) < 127 else '^?'
+        else:
+            res += c
+    res += '$'
+    return res
+
+def tabify(leading):
+    spaces = ' ' * tabsize
+    while spaces in leading:
+        leading = leading.replace(spaces, '\t', 1)
+    return leading
+
+def string_find_replace(string, find, replace):
+    return re.sub(find, replace, string)
+
+def deparenthesize(string):
+    if string is None:
+        return ""
+    while re.match(r'^\s*\(.*\)\s*$', string, re.DOTALL):
+        string = re.sub(r'^\s*\(\s*', '', string)
+        string = re.sub(r'\s*\)\s*$', '', string)
+    string = re.sub(r'\s+', ' ', string)
+    return string
+
+def get_edit_distance(str1, str2):
+    str1 = str1.lower().replace('-', '')
+    str2 = str2.lower().replace('-', '')
+    len1, len2 = len(str1), len(str2)
+    d = [[0] * (len2 + 1) for _ in range(len1 + 1)]
+    for i in range(len1 + 1):
+        d[i][0] = i
+    for j in range(len2 + 1):
+        d[0][j] = j
+    for i in range(1, len1 + 1):
+        for j in range(1, len2 + 1):
+            if str1[i-1] == str2[j-1]:
+                d[i][j] = d[i-1][j-1]
+            else:
+                d[i][j] = 1 + min(d[i][j-1], d[i-1][j], d[i-1][j-1])
+    return d[len1][len2]
+
+def find_standard_signature(sign_off):
+    standard = ['Signed-off-by:', 'Co-developed-by:', 'Acked-by:', 'Tested-by:',
+                'Reviewed-by:', 'Reported-by:', 'Suggested-by:']
+    for sig in standard:
+        if get_edit_distance(sign_off, sig) <= 2:
+            return sig
+    return ""
+
+def perms_to_octal(string):
+    string = string.strip()
+    if re.match(r'^\s*0[0-7]{3}\s*$', string):
+        return string.strip()
+    to_val = 0
+    for m in re.finditer(r'\b(' + single_mode_perms_string_search + r')\b', string):
+        match = m.group(1)
+        if match in mode_permission_string_types:
+            to_val |= mode_permission_string_types[match]
+    return f'{to_val:04o}'
+
+def is_userspace(realfile):
+    return bool(re.match(r'^tools/', realfile) or re.match(r'^scripts/', realfile))
+
+def exclude_global_initialisers(realfile):
+    return bool(re.match(r'^tools/testing/selftests/bpf/progs/.*\.c$', realfile) or
+                re.match(r'^samples/bpf/.*_kern\.c$', realfile) or
+                re.search(r'/bpf/.*\.bpf\.c$', realfile))
+
+# ---- Sanitize line (replace comments/strings with placeholders) ----
+
+_sanitise_quote = ''
+
+def sanitise_line_reset(in_comment=False):
+    global _sanitise_quote
+    _sanitise_quote = '*/' if in_comment else ''
+
+def sanitise_line(line):
+    global _sanitise_quote
+    if not line:
+        return line
+
+    res = list(line[0])  # copy diff marker
+    rest = line[1:] if len(line) > 1 else ''
+
+    off = 0
+    length = len(rest)
+    while off < length:
+        c = rest[off]
+
+        # Block comments
+        if _sanitise_quote == '' and off + 1 < length and rest[off:off+2] == '/*':
+            _sanitise_quote = '*/'
+            res.append(COMMENT_CHAR)
+            res.append(COMMENT_CHAR)
+            off += 2
+            continue
+        if _sanitise_quote == '*/' and off + 1 < length and rest[off:off+2] == '*/':
+            _sanitise_quote = ''
+            res.append(COMMENT_CHAR)
+            res.append(COMMENT_CHAR)
+            off += 2
+            continue
+        if _sanitise_quote == '' and off + 1 < length and rest[off:off+2] == '//':
+            _sanitise_quote = '//'
+            res.append('/')
+            res.append('/')
+            off += 2
+            continue
+
+        # Escaped chars in strings
+        if _sanitise_quote in ("'", '"') and c == '\\' and off + 1 < length:
+            res.append('X')
+            res.append('X')
+            off += 2
+            continue
+
+        # Quotes
+        if c in ("'", '"'):
+            if _sanitise_quote == '':
+                _sanitise_quote = c
+                res.append(c)
+                off += 1
+                continue
+            elif _sanitise_quote == c:
+                _sanitise_quote = ''
+
+        # Replace content
+        if _sanitise_quote == '*/' and c != '\t':
+            res.append(COMMENT_CHAR)
+        elif _sanitise_quote == '//' and c != '\t':
+            res.append(COMMENT_CHAR)
+        elif _sanitise_quote and _sanitise_quote not in ('*/', '//') and c != '\t':
+            res.append('X')
+        else:
+            res.append(c)
+        off += 1
+
+    if _sanitise_quote == '//':
+        _sanitise_quote = ''
+
+    result = ''.join(res)
+
+    # Clean up #include paths
+    m = re.match(r'^.\s*\#\s*include\s+<(.*)>', result)
+    if m:
+        clean = 'X' * len(m.group(1))
+        result = re.sub(r'<.*>', f'<{clean}>', result, count=1)
+    else:
+        m = re.match(r'^.\s*\#\s*(?:error|warning)\s+(.*)\b', result)
+        if m:
+            clean = 'X' * len(m.group(1))
+            result = re.sub(r'(\#\s*(?:error|warning)\s+).*', r'\g<1>' + clean, result, count=1)
+
+    if allow_c99_comments:
+        m = re.search(r'(//.*$)', result)
+        if m:
+            repl = COMMENT_CHAR * len(m.group(1))
+            result = result[:m.start(1)] + repl + result[m.end(1):]
+
+    return result
+
+def get_quoted_string(line, rawline):
+    if not line or not rawline:
+        return ""
+    m = re.search(String, line)
+    if not m:
+        return ""
+    return rawline[m.start():m.end()]
+
+# ---- Reporting functions ----
+
+report_list = []
+cnt_lines = 0
+cnt_error = 0
+cnt_warn = 0
+cnt_chk = 0
+clean = 1
+prefix = ''
+rpt_cleaners = 0
+
+def show_type(msg_type):
+    msg_type = msg_type.upper()
+    if use_type:
+        return msg_type in use_type
+    return msg_type not in ignore_type
+
+def report(level, msg_type, msg):
+    global prefix, report_list
+    if not show_type(msg_type):
+        return False
+    if tst_only and tst_only not in msg:
+        return False
+
+    output = ''
+    if color_enabled:
+        if level == 'ERROR':
+            output += RED
+        elif level == 'WARNING':
+            output += YELLOW
+        else:
+            output += GREEN
+
+    output += prefix + level + ':'
+    if show_types:
+        if color_enabled:
+            output += BLUE
+        output += msg_type + ':'
+    if color_enabled:
+        output += RESET
+    output += ' ' + msg + '\n'
+
+    if showfile:
+        lines = output.split('\n', 2)
+        if len(lines) > 2:
+            output = lines[0] + '\n' + '\n'.join(lines[2:])
+
+    if terse:
+        output = output.split('\n')[0] + '\n'
+
+    if verbose and msg_type in verbose_messages and msg_type not in verbose_emitted:
+        output += verbose_messages[msg_type] + '\n\n'
+        verbose_emitted[msg_type] = True
+
+    report_list.append(output)
+    return True
+
+def ERROR(msg_type, msg):
+    global clean, cnt_error
+    if report("ERROR", msg_type, msg):
+        clean = 0
+        cnt_error += 1
+        return True
+    return False
+
+def WARN(msg_type, msg):
+    global clean, cnt_warn
+    if report("WARNING", msg_type, msg):
+        clean = 0
+        cnt_warn += 1
+        return True
+    return False
+
+def CHK(msg_type, msg):
+    global clean, cnt_chk
+    if check and report("CHECK", msg_type, msg):
+        clean = 0
+        cnt_chk += 1
+        return True
+    return False
+
+# ---- Fix tracking ----
+
+fixed = []
+fixed_inserted = []
+fixed_deleted = []
+fixlinenr = -1
+
+def fix_insert_line(linenr, line):
+    fixed_inserted.append({'LINENR': linenr, 'LINE': line})
+
+def fix_delete_line(linenr, line):
+    fixed_deleted.append({'LINENR': linenr, 'LINE': line})
+
+def fixup_current_range(lines_list, idx, offset, length):
+    if idx < len(lines_list):
+        m = re.match(r'^(@@ -\d+,\d+ \+)(\d+),(\d+)( @@)', lines_list[idx])
+        if m:
+            new_o = int(m.group(2)) + offset
+            new_l = int(m.group(3)) + length
+            lines_list[idx] = f'{m.group(1)}{new_o},{new_l}{m.group(4)}'
+
+def fix_inserted_deleted_lines(lines_ref, inserted_ref, deleted_ref):
+    range_last = 0
+    delta_offset = 0
+    old_linenr = 0
+    new_linenr = 0
+    next_insert = 0
+    next_delete = 0
+    result = []
+
+    for old_line in lines_ref:
+        save = True
+        line = old_line
+
+        if re.match(r'^(?:\+\+\+|---)\s+\S+', line):
+            delta_offset = 0
+        elif re.match(r'^@@ -\d+,\d+ \+\d+,\d+ @@', line):
+            range_last = new_linenr
+            fixup_current_range(result, range_last, delta_offset, 0) if result else None
+
+        while next_delete < len(deleted_ref) and deleted_ref[next_delete]['LINENR'] == old_linenr:
+            next_delete += 1
+            save = False
+            delta_offset -= 1
+
+        while next_insert < len(inserted_ref) and inserted_ref[next_insert]['LINENR'] == old_linenr:
+            result.append(inserted_ref[next_insert]['LINE'])
+            next_insert += 1
+            new_linenr += 1
+            delta_offset += 1
+
+        if save:
+            result.append(line)
+            new_linenr += 1
+
+        old_linenr += 1
+
+    return result
+
+# ---- Context analysis functions ----
+
+def raw_line(linenr, cnt, rawlines):
+    offset = linenr - 1
+    cnt += 1
+    line = None
+    while cnt > 0:
+        if offset >= len(rawlines):
+            return None
+        line = rawlines[offset]
+        offset += 1
+        if line is not None and line.startswith('-'):
+            continue
+        cnt -= 1
+    return line
+
+def get_stat_real(linenr, lc, rawlines):
+    stat_real = raw_line(linenr, 0, rawlines)
+    if stat_real is None:
+        return ""
+    for count in range(linenr + 1, lc + 1):
+        rl = raw_line(count, 0, rawlines)
+        if rl is not None:
+            stat_real += '\n' + rl
+    return stat_real
+
+def get_stat_here(linenr, cnt, here, rawlines):
+    herectx = here + '\n'
+    for n in range(cnt):
+        rl = raw_line(linenr, n, rawlines)
+        if rl is not None:
+            herectx += rl + '\n'
+    return herectx
+
+def ctx_has_comment(first_line, end_line, rawlines):
+    """Check if there's a comment in the context around end_line."""
+    # Check current, previous, and next lines for // comments
+    for idx in [end_line - 1, end_line - 2, end_line]:
+        if 0 <= idx < len(rawlines):
+            m = re.search(r'//.*$', rawlines[idx])
+            if m:
+                return True
+    # Check for inline /* */ comment
+    if 0 <= end_line - 1 < len(rawlines):
+        if re.search(r'/\*.*\*/', rawlines[end_line - 1]):
+            return True
+    # Check for block comment in context
+    in_comment = False
+    for ln in range(first_line - 1, end_line):
+        if 0 <= ln < len(rawlines):
+            if '/*' in rawlines[ln]:
+                in_comment = True
+            if in_comment:
+                return True
+            if '*/' in rawlines[ln]:
+                in_comment = False
+    return False
+
+def ctx_statement_block(linenr, remain, off, lines, rawlines):
+    """Extract a statement block from the source."""
+    line = linenr - 1
+    blk = ''
+    soff = off
+    coff = off - 1
+    coff_set = False
+    loff = 0
+    ptype = ''
+    level = 0
+    stack = []
+    p = None
+    length = 0
+
+    while True:
+        if not stack:
+            stack = [('', 0)]
+
+        if off >= length:
+            while remain > 0:
+                if line >= len(lines):
+                    break
+                if lines[line] is not None and lines[line].startswith('-'):
+                    line += 1
+                    continue
+                remain -= 1
+                loff = length
+                blk += (lines[line] if lines[line] is not None else '') + '\n'
+                length = len(blk)
+                line += 1
+                break
+
+            if off >= length:
+                break
+
+            if level == 0 and re.match(r'^.\s*#\s*define', blk[off:]):
+                level += 1
+                ptype = '#'
+
+        p_prev = p
+        p = blk[off] if off < length else ''
+        remainder = blk[off:]
+
+        # Handle nested #if/#else
+        if re.match(r'^#\s*(?:ifndef|ifdef|if)\s', remainder):
+            stack.append((ptype, level))
+        elif re.match(r'^#\s*(?:else|elif)\b', remainder):
+            if len(stack) >= 2:
+                ptype, level = stack[-2]
+        elif re.match(r'^#\s*endif\b', remainder):
+            if stack:
+                ptype, level = stack.pop()
+
+        if level == 0 and p == ';':
+            break
+
+        # else detection
+        if (level == 0 and not coff_set and
+            (p_prev is None or re.match(r'[\s}+]', str(p_prev))) and
+            re.match(r'^(else)(?:\s|{)', remainder) and
+            not re.match(r'^else\s+if\b', remainder)):
+            m = re.match(r'^(else)', remainder)
+            if m:
+                coff = off + len(m.group(1)) - 1
+                coff_set = True
+
+        if (ptype == '' or ptype == '(') and p == '(':
+            level += 1
+            ptype = '('
+        if ptype == '(' and p == ')':
+            level -= 1
+            ptype = '(' if level != 0 else ''
+            if level == 0 and coff < soff:
+                coff = off
+                coff_set = True
+
+        if (ptype == '' or ptype == '{') and p == '{':
+            level += 1
+            ptype = '{'
+        if ptype == '{' and p == '}':
+            level -= 1
+            ptype = '{' if level != 0 else ''
+            if level == 0:
+                if off + 1 < length and blk[off + 1] == ';':
+                    off += 1
+                break
+
+        if ptype == '#' and p == '\n' and p_prev != '\\':
+            level -= 1
+            ptype = ''
+            off += 1
+            break
+
+        off += 1
+
+    if off == length:
+        loff = length + 1
+        line += 1
+        remain -= 1
+
+    statement = blk[soff:off + 1] if off + 1 <= len(blk) else blk[soff:]
+    condition = blk[soff:coff + 1] if coff + 1 <= len(blk) else blk[soff:]
+
+    return (statement, condition, line, max(remain + 1, 0), off - loff + 1, level)
+
+def statement_lines(stmt):
+    stmt = re.sub(r'(?:^|\n).', '\n', stmt)
+    stmt = stmt.strip()
+    return stmt.count('\n') + 1
+
+def statement_rawlines(stmt):
+    return stmt.count('\n') + 1
+
+def statement_block_size(stmt):
+    stmt = re.sub(r'(?:^|\n).', '\n', stmt)
+    stmt = re.sub(r'^\s*\{', '', stmt)
+    stmt = re.sub(r'\}\s*$', '', stmt)
+    stmt = stmt.strip()
+    stmt_lines_count = stmt.count('\n') + 1
+    stmt_stmts = stmt.count(';')
+    return max(stmt_lines_count, stmt_stmts)
+
+def pos_last_openparen(line):
+    opens = line.count('(')
+    closes = line.count(')')
+    if opens == 0 or closes >= opens:
+        return -1
+    last_open = 0
+    for pos, c in enumerate(line):
+        if c == '(':
+            last_open = pos
+    return len(expand_tabs(line[:last_open])) + 1
+
+# ---- Value annotation (simplified from Perl) ----
+
+av_preprocessor = False
+av_pending = '_'
+av_paren_type = ['E']
+av_pend_colon = 'O'
+
+def annotate_reset():
+    global av_preprocessor, av_pending, av_paren_type, av_pend_colon
+    av_preprocessor = False
+    av_pending = '_'
+    av_paren_type = ['E']
+    av_pend_colon = 'O'
+
+def annotate_values(stream, vtype):
+    global av_preprocessor, av_pending, av_paren_type, av_pend_colon
+    res = ''
+    var = '_' * len(stream)
+    var_list = list(var)
+    cur = stream
+
+    while cur:
+        if not av_paren_type:
+            av_paren_type = ['E']
+
+        consumed = None
+
+        m = re.match(r'^(\s+)', cur)
+        if m:
+            consumed = m.group(1)
+            if '\n' in consumed and av_preprocessor:
+                if av_paren_type:
+                    vtype = av_paren_type.pop()
+                av_preprocessor = False
+        if consumed is None:
+            m = re.match(r'^(\(\s*' + Type + r'\s*)\)', cur)
+            if m and av_pending == '_':
+                consumed = m.group(1)
+                av_paren_type.append(vtype)
+                vtype = 'c'
+        if consumed is None:
+            m = re.match(r'^(' + Type + r')\s*(?:' + Ident + r'|,|\)|\(|\s*$)', cur)
+            if m:
+                consumed = m.group(1)
+                vtype = 'T'
+        if consumed is None:
+            m = re.match(r'^(' + Modifier + r')\s*', cur)
+            if m:
+                consumed = m.group(1)
+                vtype = 'T'
+        if consumed is None:
+            m = re.match(r'^(\#\s*define\s*' + Ident + r')(\(?)', cur)
+            if m:
+                consumed = m.group(1)
+                av_preprocessor = True
+                av_paren_type.append(vtype)
+                if m.group(2):
+                    av_pending = 'N'
+                vtype = 'E'
+        if consumed is None:
+            m = re.match(r'^(\#\s*(?:undef\s*' + Ident + r'|include\b))', cur)
+            if m:
+                consumed = m.group(1)
+                av_preprocessor = True
+                av_paren_type.append(vtype)
+        if consumed is None:
+            m = re.match(r'^(\#\s*(?:ifdef|ifndef|if))', cur)
+            if m:
+                consumed = m.group(1)
+                av_preprocessor = True
+                av_paren_type.append(vtype)
+                av_paren_type.append(vtype)
+                vtype = 'E'
+        if consumed is None:
+            m = re.match(r'^(\#\s*(?:else|elif))', cur)
+            if m:
+                consumed = m.group(1)
+                av_preprocessor = True
+                if av_paren_type:
+                    av_paren_type.append(av_paren_type[-1])
+                vtype = 'E'
+        if consumed is None:
+            m = re.match(r'^(\#\s*endif)', cur)
+            if m:
+                consumed = m.group(1)
+                av_preprocessor = True
+                if av_paren_type:
+                    av_paren_type.pop()
+                av_paren_type.append(vtype)
+                vtype = 'E'
+        if consumed is None:
+            m = re.match(r'^(\\\n)', cur)
+            if m:
+                consumed = m.group(1)
+        if consumed is None:
+            m = re.match(r'^(__attribute__)\s*\(?', cur)
+            if m:
+                consumed = m.group(1)
+                av_pending = vtype
+                vtype = 'N'
+        if consumed is None:
+            m = re.match(r'^(sizeof)\s*(\()?', cur)
+            if m:
+                consumed = m.group(1)
+                if m.group(2):
+                    av_pending = 'V'
+                vtype = 'N'
+        if consumed is None:
+            m = re.match(r'^(if|while|for)\b', cur)
+            if m:
+                consumed = m.group(1)
+                av_pending = 'E'
+                vtype = 'N'
+        if consumed is None:
+            m = re.match(r'^(case)', cur)
+            if m:
+                consumed = m.group(1)
+                av_pend_colon = 'C'
+                vtype = 'N'
+        if consumed is None:
+            m = re.match(r'^(return|else|goto|typeof|__typeof__)\b', cur)
+            if m:
+                consumed = m.group(1)
+                vtype = 'N'
+        if consumed is None:
+            m = re.match(r'^(\()', cur)
+            if m:
+                consumed = m.group(1)
+                av_paren_type.append(av_pending)
+                av_pending = '_'
+                vtype = 'N'
+        if consumed is None:
+            m = re.match(r'^(\))', cur)
+            if m:
+                consumed = m.group(1)
+                new_type = av_paren_type.pop() if av_paren_type else '_'
+                if new_type != '_':
+                    vtype = new_type
+        if consumed is None:
+            m = re.match(r'^(' + Ident + r')\s*\(', cur)
+            if m:
+                consumed = m.group(1)
+                vtype = 'V'
+                av_pending = 'V'
+        if consumed is None:
+            m = re.match(r'^(' + Ident + r'\s*):(?:\s*\d+\s*(,|=|;))?', cur)
+            if m:
+                consumed = m.group(1)
+                if m.group(2) and vtype in ('C', 'T'):
+                    av_pend_colon = 'B'
+                elif vtype == 'E':
+                    av_pend_colon = 'L'
+                vtype = 'V'
+        if consumed is None:
+            m = re.match(r'^(' + Ident + r'|' + Constant + r')', cur)
+            if m:
+                consumed = m.group(1)
+                vtype = 'V'
+        if consumed is None:
+            m = re.match(r'^(' + Assignment + r')', cur)
+            if m:
+                consumed = m.group(1)
+                vtype = 'N'
+        if consumed is None:
+            m = re.match(r'^(;|\{|\})', cur)
+            if m:
+                consumed = m.group(1)
+                vtype = 'E'
+                av_pend_colon = 'O'
+        if consumed is None:
+            m = re.match(r'^(,)', cur)
+            if m:
+                consumed = m.group(1)
+                vtype = 'C'
+        if consumed is None:
+            m = re.match(r'^(\?)', cur)
+            if m:
+                consumed = m.group(1)
+                vtype = 'N'
+        if consumed is None:
+            m = re.match(r'^(:)', cur)
+            if m:
+                consumed = m.group(1)
+                idx = len(res)
+                if idx < len(var_list):
+                    var_list[idx] = av_pend_colon
+                if av_pend_colon in ('C', 'L'):
+                    vtype = 'E'
+                else:
+                    vtype = 'N'
+                av_pend_colon = 'O'
+        if consumed is None:
+            m = re.match(r'^(\[)', cur)
+            if m:
+                consumed = m.group(1)
+                vtype = 'N'
+        if consumed is None:
+            m = re.match(r'^(-(?![->])|\+(?!\+)|\*|&&|&)', cur)
+            if m:
+                consumed = m.group(1)
+                variant = 'B' if vtype == 'V' else 'U'
+                idx = len(res)
+                if idx < len(var_list):
+                    var_list[idx] = variant
+                vtype = 'N'
+        if consumed is None:
+            m = re.match(r'^(' + Operators + r')', cur)
+            if m:
+                consumed = m.group(1)
+                if consumed not in ('++', '--'):
+                    vtype = 'N'
+        if consumed is None:
+            m = re.match(r'^(.)', cur)
+            if m:
+                consumed = m.group(1)
+
+        if consumed:
+            cur = cur[len(consumed):]
+            res += vtype * len(consumed)
+
+    return (res, ''.join(var_list))
+
+# ---- Spelling ----
+
+misspellings = None
+spelling_fix = {}
+
+def load_spelling():
+    global misspellings, spelling_fix
+    if os.path.isfile(spelling_file):
+        try:
+            with open(spelling_file, 'r', encoding='utf-8', errors='replace') as f:
+                for line in f:
+                    line = line.strip()
+                    if not line or line.startswith('#'):
+                        continue
+                    parts = line.split('||', 1)
+                    if len(parts) == 2:
+                        spelling_fix[parts[0]] = parts[1]
+        except IOError:
+            print(f"No typos will be found - file '{spelling_file}': not readable", file=sys.stderr)
+
+    if codespell and os.path.isfile(codespellfile):
+        try:
+            with open(codespellfile, 'r', encoding='utf-8', errors='replace') as f:
+                for line in f:
+                    line = line.strip()
+                    if not line or line.startswith('#') or ', disabled' in line.lower():
+                        continue
+                    line = line.split(',')[0]
+                    parts = line.split('->', 1)
+                    if len(parts) == 2:
+                        spelling_fix[parts[0]] = parts[1]
+        except IOError:
+            print(f"No codespell typos will be found - file '{codespellfile}': not readable", file=sys.stderr)
+
+    if spelling_fix:
+        misspellings = '|'.join(sorted(spelling_fix.keys()))
+
+# ---- Const structs ----
+
+const_structs = None
+
+def load_const_structs():
+    global const_structs
+    if os.path.isfile(conststructsfile):
+        words = []
+        try:
+            with open(conststructsfile, 'r', encoding='utf-8', errors='replace') as f:
+                for line in f:
+                    line = line.strip()
+                    if not line or line.startswith('#') or ' ' in line:
+                        continue
+                    words.append(line)
+        except IOError:
+            pass
+        if words:
+            const_structs = '|'.join(words)
+
+# ---- Git helpers ----
+
+def git_is_single_file(filename):
+    if not which("git") or not os.path.exists(gitroot):
+        return False
+    try:
+        output = subprocess.run(f'{git_command} ls-files -- {filename}',
+                                shell=True, capture_output=True, text=True).stdout
+        count = output.count('\n')
+        return count == 1 and output.strip() == filename
+    except Exception:
+        return False
+
+def git_commit_info(commit, cid, desc):
+    if not which("git") or not os.path.exists(gitroot):
+        return (cid, desc)
+    try:
+        output = subprocess.run(
+            f"{git_command} log --no-color --format='%H %s' -1 {commit}",
+            shell=True, capture_output=True, text=True, stderr=subprocess.STDOUT).stdout.strip()
+        lines = output.split('\n')
+    except Exception:
+        return (cid, desc)
+
+    if not lines:
+        return (cid, desc)
+
+    if 'error: short SHA1' in lines[0] and 'is ambiguous' in lines[0]:
+        pass
+    elif 'fatal: ambiguous argument' in lines[0] or 'fatal: bad object' in lines[0]:
+        cid = None
+    else:
+        cid = lines[0][:12]
+        desc = lines[0][41:] if len(lines[0]) > 41 else ''
+
+    return (cid, desc)
+
+# ---- Maintained/obsolete check ----
+
+maintained_status = {}
+
+def is_maintained_obsolete(filename):
+    global maintained_status
+    if not tree or not root:
+        return False
+    gm_script = os.path.join(root, 'scripts', 'get_maintainer.pl')
+    if not os.path.exists(gm_script):
+        gm_script = os.path.join(root, 'scripts', 'get_maintainer.py')
+        if not os.path.exists(gm_script):
+            return False
+
+    if filename not in maintained_status:
+        try:
+            if gm_script.endswith('.py'):
+                cmd = f'python3 {gm_script} --status --nom --nol --nogit --nogit-fallback -f {filename}'
+            else:
+                cmd = f'perl {gm_script} --status --nom --nol --nogit --nogit-fallback -f {filename}'
+            maintained_status[filename] = subprocess.run(
+                cmd, shell=True, capture_output=True, text=True).stdout
+        except Exception:
+            return False
+
+    return bool(re.search(r'obsolete', maintained_status.get(filename, ''), re.IGNORECASE))
+
+def is_SPDX_License_valid(license_str):
+    if not tree or not which("python3") or not root:
+        return True
+    spdxcheck = os.path.join(root, 'scripts', 'spdxcheck.py')
+    if not os.path.isfile(spdxcheck) or not os.path.exists(gitroot):
+        return True
+    try:
+        result = subprocess.run(
+            f'cd "{os.path.abspath(root)}"; echo "{license_str}" | scripts/spdxcheck.py -',
+            shell=True, capture_output=True, text=True)
+        return result.stdout == ""
+    except Exception:
+        return True
+
+# ---- Verbose docs loading ----
+
+def load_docs():
+    global verbose_messages
+    if not os.path.isfile(docsfile):
+        return
+    try:
+        with open(docsfile, 'r', encoding='utf-8', errors='replace') as f:
+            msg_type = ''
+            desc = ''
+            in_desc = False
+            for raw_line_text in f:
+                line = raw_line_text.rstrip()
+                m = re.match(r'^\s*\*\*(.+)\*\*$', line)
+                if m:
+                    if desc:
+                        verbose_messages[msg_type] = desc.strip()
+                    msg_type = m.group(1)
+                    desc = ''
+                    in_desc = True
+                elif in_desc:
+                    if re.match(r'^(?:\s{4,}|$)', line):
+                        desc += re.sub(r'^\s{4}', '', line) + '\n'
+                    else:
+                        if desc:
+                            verbose_messages[msg_type] = desc.strip()
+                        msg_type = ''
+                        desc = ''
+                        in_desc = False
+            if desc:
+                verbose_messages[msg_type] = desc.strip()
+    except IOError:
+        pass
+
+# ---- List types ----
+
+def do_list_types():
+    """List all message types by scanning our own source."""
+    try:
+        with open(os.path.abspath(sys.argv[0]), 'r', encoding='utf-8') as f:
+            text = f.read()
+    except IOError:
+        return
+
+    types = {}
+    for m in re.finditer(r'(?:(\bCHK|\bWARN|\bERROR)\s*\()\s*["\']([^"\']+)["\']', text):
+        level = m.group(1)
+        msg_type = m.group(2)
+        if msg_type in types:
+            if types[msg_type] != level:
+                types[msg_type] += ',' + level
+        else:
+            types[msg_type] = level
+
+    print("#\tMessage type\n")
+    if color_enabled:
+        print(f" ( Color coding: {RED}ERROR{RESET} | {YELLOW}WARNING{RESET} | {GREEN}CHECK{RESET} | Multiple levels / Undetermined )\n")
+
+    count = 0
+    for msg_type in sorted(types.keys()):
+        count += 1
+        display = msg_type
+        if color_enabled:
+            level = types[msg_type]
+            if level == 'ERROR':
+                display = RED + msg_type + RESET
+            elif level == 'WARN':
+                display = YELLOW + msg_type + RESET
+            elif level == 'CHK':
+                display = GREEN + msg_type + RESET
+        print(f"{count}\t{display}")
+        if verbose and msg_type in verbose_messages:
+            msg = verbose_messages[msg_type].replace('\n', '\n\t')
+            print(f"\t{msg}\n")
+
+# ---- Main process function ----
+
+def process(filename):
+    global rawlines, lines, fixed, fixed_inserted, fixed_deleted, fixlinenr
+    global report_list, cnt_lines, cnt_error, cnt_warn, cnt_chk, clean
+    global prefix, rpt_cleaners, check, modifierListFile, typeListFile
+
+    linenr = 0
+    prevline = ""
+    prevrawline = ""
+    stashline = ""
+    stashrawline = ""
+    length = 0
+    indent = 0
+    previndent = 0
+    stashindent = 0
+
+    clean = 1
+    signoff = 0
+    fixes_tag = 0
+    is_revert = False
+    needs_fixes_tag = ""
+    author = ''
+    authorsignoff = 0
+    author_sob = ''
+    is_patch = False
+    in_header_lines = 0 if file_mode else 1
+    in_commit_log = False
+    has_patch_separator = False
+    has_commit_log = False
+    commit_log_lines = 0
+    commit_log_possible_stack_dump = False
+    commit_log_long_line = False
+    commit_log_has_diff = False
+    reported_maintainer_file = False
+    non_utf8_charset = False
+    last_git_commit_id_linenr = -1
+    last_blank_line = 0
+    last_coalesced_string_linenr = -1
+
+    report_list = []
+    cnt_lines = 0
+    cnt_error = 0
+    cnt_warn = 0
+    cnt_chk = 0
+
+    realfile = ''
+    realline = 0
+    realcnt = 0
+    here = ''
+    context_function = None
+    in_comment = False
+    first_line = 0
+    p1_prefix = ''
+    prev_values = 'E'
+
+    suppress_ifbraces = {}
+    suppress_whiletrailers = {}
+    suppress_export = {}
+    suppress_statement = 0
+
+    signatures = {}
+    setup_docs_list = []
+    setup_docs = False
+    camelcase_file_seeded = False
+    checklicenseline = 1
+
+    emitted_corrupt = 0
+
+    # Pre-scan: sanitize lines and build lines[]
+    sanitise_line_reset()
+    lines_local = []
+    local_fixed = []
+
+    for raw_idx, rawline_text in enumerate(rawlines):
+        line_text = rawline_text
+
+        if fix:
+            local_fixed.append(rawline_text)
+
+        if re.match(r'^\+\+\+\s+(\S+)', rawline_text):
+            setup_docs = False
+            m = re.match(r'^\+\+\+\s+(\S+)', rawline_text)
+            if m and re.search(r'Documentation/admin-guide/kernel-parameters\.txt$', m.group(1)):
+                setup_docs = True
+
+        m = re.match(r'^@@ -\d+(?:,\d+)? \+(\d+)(,(\d+))? @@', rawline_text)
+        if m:
+            realline = int(m.group(1)) - 1
+            realcnt = int(m.group(3)) + 1 if m.group(2) else 2
+            in_comment = False
+
+            # Guess if we're in a comment
+            edge = None
+            cnt = realcnt
+            for ln in range(raw_idx + 1, len(rawlines)):
+                if rawlines[ln].startswith('-'):
+                    continue
+                cnt -= 1
+                if cnt <= 0:
+                    break
+                m2 = re.search(r'(/\*|\*/)', rawlines[ln])
+                if m2 and not re.search(r'"[^"]*(?:/\*|\*/)[^"]*"', rawlines[ln]):
+                    edge = m2.group(1)
+                    break
+
+            if edge == '*/':
+                in_comment = True
+
+            if edge is None and raw_idx + 1 < len(rawlines):
+                if re.match(r'^.\s*(?:\*\*+| \*)(?:\s|$)', rawlines[raw_idx + 1] if raw_idx + 1 < len(rawlines) else ''):
+                    in_comment = True
+
+            sanitise_line_reset(in_comment)
+        elif realcnt and re.match(r'^(?:\+| |$)', rawline_text):
+            line_text = sanitise_line(rawline_text)
+
+        lines_local.append(line_text)
+
+        if realcnt > 1:
+            if re.match(r'^(?:\+| |$)', line_text):
+                realcnt -= 1
+        else:
+            realcnt = 0
+
+        if setup_docs and line_text.startswith('+'):
+            setup_docs_list.append(line_text)
+
+    lines = lines_local
+    if fix:
+        fixed[:] = local_fixed
+
+    # Main processing loop
+    prefix_local = ''
+    realcnt = 0
+    linenr = 0
+    fixlinenr = -1
+    hunk_line = False
+
+    for line_idx, line in enumerate(lines):
+        linenr = line_idx + 1
+        fixlinenr += 1
+
+        sline = line.replace(COMMENT_CHAR, ' ') if line else ''
+        rawline = rawlines[line_idx] if line_idx < len(rawlines) else ''
+
+        # Check mode change / rename / patch start
+        if not in_commit_log:
+            if (re.match(r'^ mode change [0-7]+ => [0-7]+ \S+\s*$', line or '') or
+                re.match(r'^rename (?:from|to) \S+\s*$', line or '') or
+                re.match(r'^diff --git a/[\w/._\-]+ b/\S+\s*$', line or '')):
+                is_patch = True
+
+        # Extract line range
+        if not in_commit_log:
+            m = re.match(r'^@@ -\d+(?:,\d+)? \+(\d+)(,(\d+))? @@(.*)', line or '')
+            if m:
+                context_text = m.group(4)
+                is_patch = True
+                first_line = linenr + 1
+                realline = int(m.group(1)) - 1
+                realcnt = int(m.group(3)) + 1 if m.group(2) else 2
+                annotate_reset()
+                prev_values = 'E'
+                suppress_ifbraces = {}
+                suppress_whiletrailers = {}
+                suppress_export = {}
+                suppress_statement = 0
+                m2 = re.search(r'\b(\w+)\s*\(', context_text or '')
+                context_function = m2.group(1) if m2 else None
+                continue
+
+        # Track lines in the hunk
+        if re.match(r'^( |\+|$)', line or ''):
+            realline += 1
+            if realcnt:
+                realcnt -= 1
+            length, indent = line_stats(rawline)
+            prevline, stashline = stashline, line
+            previndent, stashindent = stashindent, indent
+            prevrawline, stashrawline = stashrawline, rawline
+        elif realcnt == 1:
+            realcnt = 0
+
+        hunk_line = (realcnt != 0)
+
+        if not file_mode:
+            here = f"#{linenr}: "
+        else:
+            here = f"#{realline}: "
+
+        # Extract filename
+        found_file = False
+        m = re.match(r'^diff --git.*?(\S+)$', line or '')
+        if m:
+            realfile = m.group(1)
+            if not file_mode:
+                realfile = re.sub(r'^[^/]*/', '', realfile)
+            in_commit_log = False
+            found_file = True
+        else:
+            m = re.match(r'^\+\+\+\s+(\S+)', line or '')
+            if m:
+                realfile = m.group(1)
+                if not file_mode:
+                    realfile = re.sub(r'^[^/]*/', '', realfile)
+                in_commit_log = False
+                p1_prefix = re.match(r'^([^/]*/)', m.group(1))
+                p1_prefix = p1_prefix.group(1) if p1_prefix else ''
+
+                if re.match(r'^include/asm/', realfile):
+                    ERROR("MODIFIED_INCLUDE_ASM",
+                          "do not modify files in include/asm, change architecture specific files in arch/<architecture>/include/asm\n" + f"{here}{rawline}\n")
+                found_file = True
+
+        # Set prefix for error reporting
+        if showfile:
+            prefix = f"{realfile}:{realline}: "
+        elif emacs:
+            prefix = f"{filename}:{realline if file_mode else linenr}: "
+        else:
+            prefix = ''
+
+        if found_file:
+            if is_maintained_obsolete(realfile):
+                WARN("OBSOLETE",
+                     f"{realfile} is marked as 'obsolete' in the MAINTAINERS hierarchy.  No unnecessary modifications please.\n")
+            if re.match(r'^(?:drivers/net/|net/|drivers/staging/)', realfile):
+                check = True
+            else:
+                check = check_orig
+            checklicenseline = 1
+            continue
+
+        here += f"FILE: {realfile}:{realline}:" if realcnt else ''
+
+        hereline = f"{here}\n{rawline}\n"
+        herecurr = f"{here}\n{rawline}\n"
+        hereprev = f"{here}\n{prevrawline}\n{rawline}\n"
+
+        if realcnt:
+            cnt_lines += 1
+
+        # ---- Commit log checks ----
+        if in_commit_log:
+            if line and not re.match(r'^\s*$', line):
+                commit_log_lines += 1
+        elif has_commit_log and commit_log_lines < 2:
+            WARN("COMMIT_MESSAGE",
+                 "Missing commit description - Add an appropriate one\n")
+            commit_log_lines = 2
+
+        # Check for diff in commit message
+        if (in_commit_log and not commit_log_has_diff and line and
+            (re.match(r'^\s+diff\b.*a/([\w/]+)', line) or
+             re.match(r'^\s*(?:---\s+a/|\+\+\+\s+b/)', line) or
+             re.match(r'^\s*@@ -\d+,\d+ \+\d+,\d+ @@', line))):
+            ERROR("DIFF_IN_COMMIT_MSG",
+                  "Avoid using diff content in the commit message - patch(1) might not work\n" + herecurr)
+            commit_log_has_diff = True
+
+        # Incorrect file permissions
+        if line and re.match(r'^new (file )?mode.*[7531]\d{0,2}$', line):
+            permhere = here + f"FILE: {realfile}\n"
+            if not re.match(r'^scripts/', realfile) and not re.search(r'\.(py|pl|awk|sh)$', realfile):
+                ERROR("EXECUTE_PERMISSIONS",
+                      "do not set execute permissions for source files\n" + permhere)
+
+        # Check for From:
+        if line and re.match(r'^From:\s*(.*)', line, re.IGNORECASE):
+            author = re.match(r'^From:\s*(.*)', line, re.IGNORECASE).group(1)
+
+        # Check for signoff
+        if line and re.match(r'^\s*signed-off-by:\s*(.*)', line, re.IGNORECASE):
+            signoff += 1
+            in_commit_log = False
+
+        # Check for patch separator
+        if line == '---':
+            has_patch_separator = True
+            in_commit_log = False
+
+        # MAINTAINERS update check
+        if line and re.match(r'^\s*MAINTAINERS\s*\|', line):
+            reported_maintainer_file = True
+
+        # Check if it's the start of commit log
+        if in_header_lines and realfile == '':
+            if rawline and not (re.match(r'^\s+(?:\S|$)', rawline) or
+                                re.match(r'^(?:commit\b|from\b|[\w-]+:)', rawline, re.IGNORECASE)):
+                in_header_lines = 0
+                in_commit_log = True
+                has_commit_log = True
+
+        # Check for Fixes: tag
+        if (not in_header_lines and line and
+            re.match(r'^\s*(fixes:?)\s*(?:commit\s*)?([0-9a-f]{5,40})', line, re.IGNORECASE)):
+            fixes_tag = 1
+            m = re.match(r'^\s*(fixes:?)\s*(?:commit\s*)?([0-9a-f]{5,40})', line, re.IGNORECASE)
+            tag = m.group(1)
+            orig_commit = m.group(2)
+            # Simplified Fixes: tag check
+            if tag != "Fixes:":
+                cid, ctitle = git_commit_info(orig_commit, '0123456789ab', 'commit title')
+                if cid is not None:
+                    fixed_str = f'Fixes: {cid} ("{ctitle}")'
+                    WARN("BAD_FIXES_TAG",
+                         f"Please use correct Fixes: style 'Fixes: <12+ chars of sha1> (\"<title line>\")' - ie: '{fixed_str}'\n" + herecurr)
+
+        # Check for Gerrit Change-Id
+        if realfile == '' and not has_patch_separator and line and re.match(r'^\s*change-id:', line, re.IGNORECASE):
+            ERROR("GERRIT_CHANGE_ID",
+                  "Remove Gerrit Change-Id's before submitting upstream\n" + herecurr)
+
+        # Check commit log line length
+        if (in_commit_log and not commit_log_long_line and line and
+            len(line) > 75 and
+            not re.match(r'^\s*[a-zA-Z0-9_/\.]+\s+\|\s+\d+', line) and
+            not re.match(r'^\s*(?:[\w.\-+]*/)+[\w.\-+]+:', line) and
+            not re.match(r'^\s*(?:Fixes:|https?:|' + link_tags_search + '|' + signature_tags + r')', line, re.IGNORECASE) and
+            not commit_log_possible_stack_dump):
+            WARN("COMMIT_LOG_LONG_LINE",
+                 f"Prefer a maximum 75 chars per line (possible unwrapped commit description?)\n" + herecurr)
+            commit_log_long_line = True
+
+        # Check for This reverts commit
+        if not in_header_lines and not is_patch and line and re.match(r'^This reverts commit', line):
+            is_revert = True
+
+        # Bug/crash indicators
+        if (not in_header_lines and not is_patch and line and
+            re.search(r'((?:(?:BUG: K\.|UB)SAN: |Call Trace:|stable@|syzkaller))', line)):
+            needs_fixes_tag = re.search(r'((?:(?:BUG: K\.|UB)SAN: |Call Trace:|stable@|syzkaller))', line).group(1)
+
+        # Check for lines starting with #
+        if in_commit_log and line and line.startswith('#'):
+            WARN("COMMIT_COMMENT_SYMBOL",
+                 "Commit log lines starting with '#' are dropped by git as comments\n" + herecurr)
+
+        # ignore non-hunk lines and lines being removed
+        if not hunk_line or (line and line.startswith('-')):
+            continue
+
+        # ---- Whitespace checks ----
+
+        # DOS line endings
+        if line and line.startswith('+') and '\r' in rawline:
+            herevet = f"{here}\n{cat_vet(rawline)}\n"
+            ERROR("DOS_LINE_ENDINGS", "DOS line endings\n" + herevet)
+        # Trailing whitespace
+        elif rawline and (re.match(r'^\+.*\S\s+$', rawline) or re.match(r'^\+\s+$', rawline)):
+            herevet = f"{here}\n{cat_vet(rawline)}\n"
+            ERROR("TRAILING_WHITESPACE", "trailing whitespace\n" + herevet)
+            rpt_cleaners = 1
+
+        # Check we are in a valid source file
+        if not re.search(r'\.(h|c|rs|s|S|sh|dtsi|dts)$', realfile):
+            continue
+
+        # SPDX check
+        if realline == checklicenseline:
+            if rawline and rawline.startswith('+'):
+                if re.search(r'SPDX-License-Identifier:', rawline):
+                    m = re.search(r'(SPDX-License-Identifier: .*)', rawline)
+                    if m:
+                        spdx_license = m.group(1)
+                        if not is_SPDX_License_valid(spdx_license):
+                            WARN("SPDX_LICENSE_TAG",
+                                 f"'{spdx_license}' is not supported in LICENSES/...\n" + herecurr)
+
+        # Line length check
+        if line and line.startswith('+') and length > max_line_length:
+            msg_type = "LONG_LINE"
+
+            # Skip URLs
+            if re.search(r'\b[a-z][\w.+\-]*://\S+', rawline, re.IGNORECASE):
+                msg_type = ""
+            # Skip strings
+            elif re.match(r'^\+\s*' + String + r'\s*(?:\s*|,|\)\s*;)\s*$', line):
+                msg_type = ""
+            elif re.match(r'^\+\s*#\s*define\s+\w+\s+' + String + r'$', line):
+                msg_type = ""
+
+            if msg_type and show_type("LONG_LINE") and show_type(msg_type):
+                msg_level = CHK if file_mode else WARN
+                msg_level(msg_type,
+                         f"line length of {length} exceeds {max_line_length} columns\n" + herecurr)
+
+        # Check we are in a valid C source file
+        if not re.search(r'\.(h|c|pl|dtsi|dts)$', realfile):
+            continue
+
+        # Code indent (tabs vs spaces)
+        # At the beginning of a line any tabs must come first and anything
+        # more than tabsize spaces must use tabs
+        if rawline and (re.match(r'^\+\s* \t\s*\S', rawline) or
+                        re.match(r'^\+ {' + str(tabsize) + r',}\s*', rawline)):
+            herevet = f"{here}\n{cat_vet(rawline)}\n"
+            ERROR("CODE_INDENT", "code indent should use tabs where possible\n" + herevet)
+            rpt_cleaners = 1
+
+        # Space before tab
+        if rawline and rawline.startswith('+') and ' \t' in rawline:
+            herevet = f"{here}\n{cat_vet(rawline)}\n"
+            WARN("SPACE_BEFORE_TAB", "please, no space before tabs\n" + herevet)
+
+        # Check we are in a valid C source file
+        if not re.search(r'\.(h|c)$', realfile):
+            continue
+
+        # Function start detection
+        if (sline and re.match(r'^\+\{\s*$', sline) and
+            prevline and re.match(r'^\+(?:(?:(?:' + Storage + r'|' + Inline + r')\s*)*\s*' + Type + r'\s*)?(' + Ident + r')\(', prevline)):
+            m = re.match(r'^\+(?:(?:(?:' + Storage + r'|' + Inline + r')\s*)*\s*' + Type + r'\s*)?(' + Ident + r')\(', prevline)
+            if m:
+                context_function = m.group(1)
+
+        # Function end
+        if sline and re.match(r'^\+\}\s*$', sline):
+            context_function = None
+
+        # ---- Simple pattern-based checks ----
+
+        # printk should use KERN_* levels
+        if line and re.search(r'\bprintk\s*\(\s*(?!KERN_[A-Z]+\b)', line):
+            WARN("PRINTK_WITHOUT_KERN_LEVEL",
+                 "printk() should include KERN_<LEVEL> facility level\n" + herecurr)
+
+        # ENOSYS
+        if line and re.search(r'\bENOSYS\b', line):
+            WARN("ENOSYS",
+                 "ENOSYS means 'invalid syscall nr' and nothing else\n" + herecurr)
+
+        # ENOTSUPP
+        if not file_mode and line and re.search(r'\bENOTSUPP\b', line):
+            WARN("ENOTSUPP",
+                 "ENOTSUPP is not a SUSV4 error code, prefer EOPNOTSUPP\n" + herecurr)
+
+        # BUG
+        if line and re.search(r'\b(?!AA_|BUILD_|IDA_|KVM_|RWLOCK_|snd_|SPIN_)(?:[a-zA-Z_]*_)?BUG(?:_ON)?(?:_[A-Z_]+)?\s*\(', line):
+            msg_level = CHK if file_mode else WARN
+            msg_level("AVOID_BUG",
+                      "Do not crash the kernel unless it is absolutely unavoidable--use WARN_ON_ONCE() plus recovery code (if feasible) instead of BUG() or variants\n" + herecurr)
+
+        # LINUX_VERSION_CODE
+        if line and re.search(r'\bLINUX_VERSION_CODE\b', line):
+            WARN("LINUX_VERSION_CODE",
+                 "LINUX_VERSION_CODE should be avoided, code should be for the version to which it is merged\n" + herecurr)
+
+        # volatile
+        if line and re.search(r'\bvolatile\b', line) and not re.search(r'\b(?:__asm__|asm)\s+(?:__volatile__|volatile)\b', line):
+            WARN("VOLATILE",
+                 "Use of volatile is usually wrong: see Documentation/process/volatile-considered-harmful.rst\n" + herecurr)
+
+        # trace_printk
+        if line:
+            m = re.search(r'\b(trace_printk|trace_puts|ftrace_vprintk)\s*\(', line)
+            if m:
+                WARN("TRACE_PRINTK",
+                     f"Do not use {m.group(1)}() in production code (this can be ignored if built only with a debug config option)\n" + herecurr)
+
+        # printk_ratelimit
+        if line and re.search(r'\bprintk_ratelimit\s*\(', line):
+            WARN("PRINTK_RATELIMITED",
+                 "Prefer printk_ratelimited or pr_<level>_ratelimited to printk_ratelimit\n" + herecurr)
+
+        # udelay
+        if line:
+            m = re.search(r'\budelay\s*\(\s*(\d+)\s*\)', line)
+            if m:
+                delay = int(m.group(1))
+                if delay >= 10:
+                    CHK("USLEEP_RANGE",
+                        "usleep_range is preferred over udelay; see function description of usleep_range() and udelay().\n" + herecurr)
+                if delay > 2000:
+                    WARN("LONG_UDELAY",
+                         "long udelay - prefer mdelay; see function description of mdelay().\n" + herecurr)
+
+        # msleep
+        if line:
+            m = re.search(r'\bmsleep\s*\((\d+)\);', line)
+            if m and int(m.group(1)) < 20:
+                WARN("MSLEEP",
+                     "msleep < 20ms can sleep for up to 20ms; see function description of msleep().\n" + herecurr)
+
+        # jiffies comparison
+        if line and re.search(r'\bjiffies\s*' + Compare + r'|' + Compare + r'\s*jiffies\b', line):
+            WARN("JIFFIES_COMPARISON",
+                 "Comparing jiffies is almost always wrong; prefer time_after, time_before and friends\n" + herecurr)
+
+        # strcpy/strlcpy/strncpy
+        if line and re.search(r'\bstrcpy\s*\(', line) and not is_userspace(realfile):
+            WARN("STRCPY",
+                 "Prefer strscpy over strcpy - see: https://github.com/KSPP/linux/issues/88\n" + herecurr)
+        if line and re.search(r'\bstrlcpy\s*\(', line) and not is_userspace(realfile):
+            WARN("STRLCPY",
+                 "Prefer strscpy over strlcpy - see: https://github.com/KSPP/linux/issues/89\n" + herecurr)
+        if line and re.search(r'\bstrncpy\s*\(', line) and not is_userspace(realfile):
+            WARN("STRNCPY",
+                 "Prefer strscpy, strscpy_pad, or __nonstring over strncpy - see: https://github.com/KSPP/linux/issues/90\n" + herecurr)
+
+        # yield()
+        if line and re.search(r'\byield\s*\(\s*\)', line):
+            WARN("YIELD",
+                 "Using yield() is generally wrong. See yield() kernel-doc (sched/core.c)\n" + herecurr)
+
+        # __FUNCTION__
+        if line and re.search(r'\b__FUNCTION__\b', line):
+            WARN("USE_FUNC",
+                 "__func__ should be used instead of gcc specific __FUNCTION__\n" + herecurr)
+
+        # __DATE__, __TIME__, __TIMESTAMP__
+        if line:
+            for m in re.finditer(r'\b(__(?:DATE|TIME|TIMESTAMP)__)\b', line):
+                ERROR("DATE_TIME",
+                      f"Use of the '{m.group(1)}' macro makes the build non-deterministic\n" + herecurr)
+
+        # #if 0
+        if line and re.match(r'^.\s*\#\s*if\s+0\b', line):
+            WARN("IF_0",
+                 "Consider removing the code enclosed by this #if 0 and its #endif\n" + herecurr)
+
+        # #if 1
+        if line and re.match(r'^.\s*\#\s*if\s+1\b', line):
+            WARN("IF_1",
+                 "Consider removing the #if 1 and its #endif\n" + herecurr)
+
+        # sizeof without parenthesis
+        if line:
+            m = re.search(r'\bsizeof\s+((?:\*\s*|)' + Lval + r'|' + Type + r'(?:\s+' + Lval + r'|))', line)
+            if m:
+                WARN("SIZEOF_PARENTHESIS",
+                     f"sizeof {m.group(1)} should be sizeof({m.group(1).strip()})\n" + herecurr)
+
+        # sizeof(&)
+        if line and re.search(r'\bsizeof\s*\(\s*&', line):
+            WARN("SIZEOF_ADDRESS", "sizeof(& should be avoided\n" + herecurr)
+
+        # spinlock_t
+        if line and re.match(r'^.\s*\bstruct\s+spinlock\s+\w+\s*;', line):
+            WARN("USE_SPINLOCK_T",
+                 "struct spinlock should be spinlock_t\n" + herecurr)
+
+        # new typedefs
+        if (line and re.search(r'\btypedef\s', line) and
+            not re.search(r'\btypedef\s+' + Type + r'\s*\(\s*\*?' + Ident + r'\s*\)\s*\(', line) and
+            not re.search(r'\btypedef\s+' + Type + r'\s+' + Ident + r'\s*\(', line) and
+            not re.search(r'\b' + typeTypedefs + r'\b', line) and
+            not re.search(r'\b__bitwise\b', line)):
+            WARN("NEW_TYPEDEFS", "do not add new typedefs\n" + herecurr)
+
+        # in_atomic
+        if line and re.search(r'\bin_atomic\s*\(', line):
+            if re.match(r'^drivers/', realfile):
+                ERROR("IN_ATOMIC", "do not use in_atomic in drivers\n" + herecurr)
+            elif not re.match(r'^kernel/', realfile):
+                WARN("IN_ATOMIC",
+                     "use of in_atomic() is incorrect outside core kernel code\n" + herecurr)
+
+        # NR_CPUS
+        if (line and re.search(r'\bNR_CPUS\b', line) and
+            not re.search(r'^\s*#\s*if\b.*\bNR_CPUS\b', line) and
+            not re.search(r'^\s*#\s*define\b.*\bNR_CPUS\b', line) and
+            not re.search(r'\bNR_CPUS[^\]]*\]', line)):
+            WARN("NR_CPUS",
+                 "usage of NR_CPUS is often wrong - consider using cpu_possible(), num_possible_cpus(), for_each_possible_cpu(), etc\n" + herecurr)
+
+        # deprecated APIs
+        if line:
+            m = re.search(r'\b(' + deprecated_apis_search + r')\b\s*\(', line)
+            if m:
+                api = m.group(1)
+                new_api = deprecated_apis.get(api, '')
+                WARN("DEPRECATED_API",
+                     f"Deprecated use of '{api}', prefer '{new_api}' instead\n" + herecurr)
+
+        # const_structs
+        if (const_structs and line and
+            not re.search(r'\bconst\b', line) and
+            re.search(r'\bstruct\s+(' + const_structs + r')\b(?!\s*\{)', line)):
+            m = re.search(r'\bstruct\s+(' + const_structs + r')\b', line)
+            WARN("CONST_STRUCT",
+                 f"struct {m.group(1)} should normally be const\n" + herecurr)
+
+        # multiple semicolons
+        if line and re.search(r';\s*;\s*$', line):
+            WARN("ONE_SEMICOLON",
+                 "Statements terminations use 1 semicolon\n" + herecurr)
+
+        # spaces between function name and (
+        if line:
+            for m in re.finditer(r'(' + Ident + r')\s+\(', line):
+                name = m.group(1)
+                if name in ('if', 'for', 'while', 'switch', 'return', 'case',
+                            'volatile', '__volatile__', '__attribute__', 'format',
+                            '__extension__', 'asm', '__asm__', 'scoped_guard'):
+                    continue
+                ctx_before = line[:m.start()]
+                if re.match(r'^.\s*#\s*define\s*$', ctx_before):
+                    continue
+                if re.match(r'^.\s*#\s*elif\s*$', ctx_before + name):
+                    continue
+                WARN("SPACING",
+                     f"space prohibited between function name and open parenthesis '('\n" + herecurr)
+
+        # space before open brace
+        if line and (re.search(r'\(.*\)\{', line) and not re.search(r'\(' + Type + r'\)\{', line)):
+            ERROR("SPACING",
+                  "space required before the open brace '{'\n" + herecurr)
+        if line and re.search(r'\b(?:else|do)\{', line):
+            ERROR("SPACING",
+                  "space required before the open brace '{'\n" + herecurr)
+
+        # space after close brace
+        if line and re.search(r'\}(?!(?:,|;|\)|\}))\S', line):
+            ERROR("SPACING",
+                  "space required after that close brace '}'\n" + herecurr)
+
+        # Need space before ( after if/while etc
+        if line and re.search(r'\b(if|while|for|switch)\(', line):
+            ERROR("SPACING",
+                  "space required before the open parenthesis '('\n" + herecurr)
+
+        # return errno should be negative
+        if sline:
+            m = re.search(r'\breturn(?:\s*\(+\s*|\s+)(E[A-Z]+)(?:\s*\)+\s*|\s*)[;:,]', sline)
+            if m:
+                name = m.group(1)
+                if name not in ('EOF', 'ERROR') and not name.startswith('EPOLL'):
+                    WARN("USE_NEGATIVE_ERRNO",
+                         f"return of an errno should typically be negative (ie: return -{name})\n" + herecurr)
+
+        # Malformed #include
+        if rawline:
+            m = re.match(r'^.\s*#\s*include\s+[<"](.*)[">]', rawline)
+            if m:
+                if '//' in m.group(1):
+                    ERROR("MALFORMED_INCLUDE",
+                          "malformed #include filename\n" + herecurr)
+
+        # CamelCase (simplified)
+        if line and line.startswith('+'):
+            for m in re.finditer(r'\b(' + Ident + r')\b', line):
+                word = m.group(1)
+                if (re.search(r'[A-Z][a-z]|[a-z][A-Z]', word) and
+                    word != '_Generic' and
+                    not re.match(r'^(?:[A-Z]+_){1,5}[A-Z]{1,3}[a-z]', word) and
+                    not re.match(r'^(?:Clear|Set|TestClear|TestSet|)Page[A-Z]', word) and
+                    not re.match(r'^ETHTOOL_LINK_MODE_', word)):
+                    if word not in camelcase:
+                        camelcase[word] = True
+                        CHK("CAMELCASE", f"Avoid CamelCase: <{word}>\n" + herecurr)
+
+        # Embedded filename
+        if rawline and realfile and re.search(r'^\+.*\b' + re.escape(realfile) + r'\b', rawline):
+            WARN("EMBEDDED_FILENAME",
+                 "It's generally not useful to have the filename in the file\n" + herecurr)
+
+        # FSF mailing address
+        if rawline and (re.search(r'\bwrite to the Free', rawline, re.IGNORECASE) or
+                        re.search(r'\b675\s+Mass\s+Ave', rawline, re.IGNORECASE) or
+                        re.search(r'\b59\s+Temple\s+Pl', rawline, re.IGNORECASE) or
+                        re.search(r'\b51\s+Franklin\s+St', rawline, re.IGNORECASE)):
+            msg_level = CHK if file_mode else ERROR
+            msg_level("FSF_MAILING_ADDRESS",
+                      "Do not include the paragraph about writing to the Free Software Foundation's mailing address from the sample GPL notice. The FSF has changed addresses in the past, and may do so again. Linux already includes a copy of the GPL.\n" + f"{here}\n{cat_vet(rawline)}\n")
+
+        # Spelling check
+        if (misspellings and line and
+            (in_commit_log or line.startswith('+') or re.match(r'^Subject:', line, re.IGNORECASE))):
+            for m in re.finditer(r'(?:^|[^\w\-\'`])(' + misspellings + r')(?:[^\w\-\'`]|$)', rawline, re.IGNORECASE):
+                typo = m.group(1)
+                typo_fix_val = spelling_fix.get(typo.lower(), '')
+                if typo_fix_val:
+                    if typo[0].isupper():
+                        typo_fix_val = typo_fix_val[0].upper() + typo_fix_val[1:]
+                    if typo.isupper():
+                        typo_fix_val = typo_fix_val.upper()
+                    msg_level = CHK if file_mode else WARN
+                    msg_level("TYPO_SPELLING",
+                              f"'{typo}' may be misspelled - perhaps '{typo_fix_val}'?\n" + herecurr)
+
+    # ---- End of file checks ----
+
+    if not rawlines:
+        return 0
+
+    if mailback and (clean == 1 or not is_patch):
+        return 0
+
+    if not chk_patch and not is_patch:
+        return 0
+
+    if not is_patch and not re.search(r'cover-letter\.patch$', filename):
+        ERROR("NOT_UNIFIED_DIFF",
+              "Does not appear to be a unified-diff format patch\n")
+
+    if is_patch and has_commit_log and chk_fixes_tag:
+        if needs_fixes_tag and not is_revert and not fixes_tag:
+            WARN("MISSING_FIXES_TAG",
+                 f"The commit message has '{needs_fixes_tag}', perhaps it also needs a 'Fixes:' tag?\n")
+
+    if is_patch and has_commit_log and chk_signoff:
+        if signoff == 0:
+            ERROR("MISSING_SIGN_OFF",
+                  "Missing Signed-off-by: line(s)\n")
+
+    # Print report
+    output = ''.join(report_list)
+    if output:
+        sys.stdout.write(output)
+
+    if summary and not (clean == 1 and quiet >= 1):
+        if summary_file:
+            sys.stdout.write(f"{filename} ")
+        chk_str = f"{cnt_chk} checks, " if check else ""
+        print(f"total: {cnt_error} errors, {cnt_warn} warnings, {chk_str}{cnt_lines} lines checked")
+
+    if quiet == 0:
+        if not clean and not fix:
+            print("\nNOTE: For some of the reported defects, checkpatch may be able to\n"
+                  "      mechanically convert to the typical style using --fix or --fix-inplace.")
+        if rpt_cleaners:
+            print("\nNOTE: Whitespace errors detected.\n"
+                  "      You may wish to use scripts/cleanpatch or scripts/cleanfile")
+
+    if clean == 0 and fix and fixed != rawlines:
+        newfile = filename
+        if not fix_inplace:
+            newfile += '.EXPERIMENTAL-checkpatch-fixes'
+        try:
+            with open(newfile, 'w', encoding='utf-8') as f:
+                linecount = 0
+                for fixed_line in fixed:
+                    linecount += 1
+                    if file_mode:
+                        if linecount > 3:
+                            f.write(re.sub(r'^\+', '', fixed_line) + '\n')
+                    else:
+                        f.write(fixed_line + '\n')
+            if not quiet:
+                print(f"\nWrote EXPERIMENTAL --fix correction(s) to '{newfile}'\n\n"
+                      "Do _NOT_ trust the results written to this file.\n"
+                      "Do _NOT_ submit these changes without inspecting them for correctness.\n\n"
+                      "This EXPERIMENTAL file is simply a convenience to help rewrite patches.\n"
+                      "No warranties, expressed or implied...")
+        except IOError as e:
+            print(f"{P}: Can't open {newfile} for write: {e}", file=sys.stderr)
+
+    if quiet == 0:
+        print()
+        if clean == 1:
+            print(f"{vname} has no obvious style problems and is ready for submission.")
+        else:
+            print(f"{vname} has style problems, please review.")
+
+    return clean
+
+# ---- CLI + main ----
+
+color_enabled = False
+
+def hash_save_array_words(hash_ref, array_ref):
+    for item in array_ref:
+        for word in item.split(','):
+            word = word.strip().upper()
+            if word and not word.startswith('#'):
+                hash_ref[word] = hash_ref.get(word, 0) + 1
+
+def main():
+    global P, quiet, verbose, tree, chk_signoff, chk_fixes_tag, chk_patch
+    global tst_only, emacs, terse, showfile, file_mode, git_mode
+    global check, check_orig, summary, mailback, summary_file
+    global show_types, list_types, fix, fix_inplace, root, debug
+    global use_type, ignore_type, max_line_length, min_conf_desc_length
+    global codespell, codespellfile, user_codespellfile, typedefsfile
+    global color, allow_c99_comments, tabsize, CONFIG_
+    global color_enabled, rawlines, lines, vname, gitroot
+    global use_list, ignore_list
+
+    # Load config file
+    conf = which_conf(configuration_file)
+    conf_args = []
+    if conf and os.path.isfile(conf):
+        try:
+            with open(conf, 'r', encoding='utf-8') as f:
+                for line in f:
+                    line = line.strip()
+                    line = re.sub(r'\s+', ' ', line)
+                    if not line or line.startswith('#'):
+                        continue
+                    for word in line.split():
+                        if word.startswith('#'):
+                            break
+                        conf_args.append(word)
+        except IOError:
+            pass
+
+    parser = argparse.ArgumentParser(add_help=False)
+    parser.add_argument('-q', '--quiet', action='count', default=0)
+    parser.add_argument('-v', '--verbose', action='store_true', default=False)
+    parser.add_argument('--tree', action='store_true', default=True)
+    parser.add_argument('--no-tree', dest='tree', action='store_false')
+    parser.add_argument('--signoff', action='store_true', default=True)
+    parser.add_argument('--no-signoff', dest='signoff', action='store_false')
+    parser.add_argument('--fixes-tag', action='store_true', default=True)
+    parser.add_argument('--no-fixes-tag', dest='fixes_tag', action='store_false')
+    parser.add_argument('--patch', action='store_true', default=True)
+    parser.add_argument('--no-patch', dest='patch', action='store_false')
+    parser.add_argument('--emacs', action='store_true', default=False)
+    parser.add_argument('--terse', action='store_true', default=False)
+    parser.add_argument('--showfile', action='store_true', default=False)
+    parser.add_argument('-f', '--file', dest='file_mode', action='store_true', default=False)
+    parser.add_argument('-g', '--git', dest='git_mode', action='store_true', default=False)
+    parser.add_argument('--subjective', '--strict', dest='strict', action='store_true', default=False)
+    parser.add_argument('--list-types', action='store_true', default=False)
+    parser.add_argument('--types', action='append', default=[])
+    parser.add_argument('--ignore', action='append', default=[])
+    parser.add_argument('--show-types', action='store_true', default=False)
+    parser.add_argument('--max-line-length', type=int, default=100)
+    parser.add_argument('--min-conf-desc-length', type=int, default=4)
+    parser.add_argument('--tab-size', type=int, default=8)
+    parser.add_argument('--root', default=None)
+    parser.add_argument('--summary', action='store_true', default=True)
+    parser.add_argument('--no-summary', dest='summary', action='store_false')
+    parser.add_argument('--mailback', action='store_true', default=False)
+    parser.add_argument('--summary-file', action='store_true', default=False)
+    parser.add_argument('--fix', action='store_true', default=False)
+    parser.add_argument('--fix-inplace', action='store_true', default=False)
+    parser.add_argument('--debug', action='append', default=[])
+    parser.add_argument('--test-only', default=None)
+    parser.add_argument('--codespell', action='store_true', default=False)
+    parser.add_argument('--codespellfile', default='')
+    parser.add_argument('--typedefsfile', default=None)
+    parser.add_argument('--color', nargs='?', const='auto', default='auto')
+    parser.add_argument('--no-color', dest='color', action='store_const', const='never')
+    parser.add_argument('--nocolor', dest='color', action='store_const', const='never')
+    parser.add_argument('--kconfig-prefix', default='CONFIG_')
+    parser.add_argument('-h', '--help', '--version', action='store_true', dest='show_help', default=False)
+    parser.add_argument('files', nargs='*')
+
+    args = parser.parse_args(conf_args + sys.argv[1:])
+
+    if args.show_help:
+        print(f"Usage: {P} [OPTION]... [FILE]...")
+        print(f"Version: {V}")
+        print("""
+Options:
+  -q, --quiet                quiet
+  -v, --verbose              verbose mode
+  --no-tree                  run without a kernel tree
+  --no-signoff               do not check for 'Signed-off-by' line
+  --patch                    treat FILE as patchfile (default)
+  --emacs                    emacs compile window format
+  --terse                    one line per report
+  -f, --file                 treat FILE as regular source file
+  -g, --git                  treat FILE as a single commit or git revision range
+  --subjective, --strict     enable more subjective tests
+  --list-types               list the possible message types
+  --types TYPE(,TYPE2...)    show only these comma separated message types
+  --ignore TYPE(,TYPE2...)   ignore various comma separated message types
+  --show-types               show the specific message type in the output
+  --max-line-length=n        set the maximum line length (default 100)
+  --root=PATH                PATH to the kernel tree root
+  --fix                      EXPERIMENTAL - create fix file
+  --fix-inplace              EXPERIMENTAL - fix in place
+  --codespell                Use the codespell dictionary
+  --color[=WHEN]             Use colors 'always', 'never', or 'auto' (default)
+  -h, --help, --version      display this help and exit""")
+        sys.exit(0)
+
+    quiet = args.quiet
+    verbose = args.verbose
+    tree = args.tree
+    chk_signoff = args.signoff
+    chk_fixes_tag = args.fixes_tag
+    chk_patch = args.patch
+    emacs = args.emacs
+    terse = args.terse
+    showfile = args.showfile
+    file_mode = args.file_mode
+    git_mode = args.git_mode
+    check = args.strict
+    summary = args.summary
+    mailback = args.mailback
+    summary_file = args.summary_file
+    fix = args.fix
+    fix_inplace = args.fix_inplace
+    show_types = args.show_types
+    list_types = args.list_types
+    max_line_length = args.max_line_length
+    min_conf_desc_length = args.min_conf_desc_length
+    tabsize = args.tab_size
+    root = args.root
+    tst_only = args.test_only
+    codespell = args.codespell
+    typedefsfile = args.typedefsfile
+    CONFIG_ = args.kconfig_prefix
+    color = args.color
+
+    if args.codespellfile:
+        codespellfile = args.codespellfile
+        user_codespellfile = args.codespellfile
+
+    # Process debug
+    for d in args.debug:
+        if '=' in d:
+            k, v = d.split('=', 1)
+            debug[k] = v
+
+    # Process types/ignore
+    hash_save_array_words(ignore_type, args.ignore)
+    hash_save_array_words(use_type, args.types)
+
+    # Color setup
+    if color in ('0', '1'):
+        color_enabled = (color == '0')  # inverted like Perl
+    elif color.lower() == 'always':
+        color_enabled = True
+    elif color.lower() == 'never':
+        color_enabled = False
+    elif color.lower() == 'auto':
+        color_enabled = sys.stdout.isatty()
+    else:
+        print(f"{P}: Invalid color mode: {color}", file=sys.stderr)
+        sys.exit(1)
+
+    if verbose:
+        load_docs()
+    if list_types:
+        do_list_types()
+        sys.exit(0)
+
+    if fix_inplace:
+        fix = True
+    check_orig = check
+
+    if git_mode and (file_mode or fix):
+        print(f"{P}: --git cannot be used with --file or --fix", file=sys.stderr)
+        sys.exit(1)
+    if verbose and terse:
+        print(f"{P}: --verbose cannot be used with --terse", file=sys.stderr)
+        sys.exit(1)
+
+    if terse:
+        emacs = True
+        quiet += 1
+
+    if tabsize < 2:
+        print(f"{P}: Invalid TAB size: {tabsize}", file=sys.stderr)
+        sys.exit(1)
+
+    # Find kernel tree root
+    if tree:
+        if root:
+            if not top_of_kernel_tree(root):
+                print(f"{P}: {root}: --root does not point at a valid tree", file=sys.stderr)
+                sys.exit(1)
+        else:
+            if top_of_kernel_tree('.'):
+                root = '.'
+            else:
+                # Try to find root from script location
+                script_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
+                parent = os.path.dirname(script_dir)
+                if top_of_kernel_tree(parent):
+                    root = parent
+
+        if not root:
+            print("Must be run from the top-level dir. of a kernel tree", file=sys.stderr)
+            sys.exit(2)
+
+    if file_mode:
+        chk_signoff = False
+        chk_fixes_tag = False
+
+    allow_c99_comments = 'C99_COMMENT_TOLERANCE' not in ignore_type
+
+    # Load spelling data
+    load_spelling()
+    load_const_structs()
+
+    # Handle input files
+    input_files = args.files if args.files else ['-']
+
+    # Handle git mode
+    if git_mode:
+        if not os.path.exists(gitroot):
+            print(f"{P}: No git repository found", file=sys.stderr)
+            sys.exit(1)
+        commits = []
+        for commit_expr in input_files:
+            if re.match(r'^(.*)-(\d+)$', commit_expr):
+                m = re.match(r'^(.*)-(\d+)$', commit_expr)
+                git_range = f"-{m.group(2)} {m.group(1)}"
+            elif '..' in commit_expr:
+                git_range = commit_expr
+            else:
+                git_range = f"-1 {commit_expr}"
+            try:
+                output = subprocess.run(
+                    f"{git_command} log --no-color --no-merges --pretty=format:'%H %s' {git_range}",
+                    shell=True, capture_output=True, text=True).stdout
+                for line in output.split('\n'):
+                    m = re.match(r'^([0-9a-fA-F]{40}) (.*)$', line)
+                    if m:
+                        sha1 = m.group(1)
+                        subject = m.group(2)
+                        commits.insert(0, sha1)
+                        git_commits[sha1] = subject
+            except Exception:
+                pass
+
+        if not commits:
+            print(f"{P}: no git commits after extraction!", file=sys.stderr)
+            sys.exit(1)
+        input_files = commits
+
+    exit_code = 0
+
+    for filename in input_files:
+        rawlines = []
+        is_git_file = git_is_single_file(filename)
+        old_file_mode = file_mode
+        if is_git_file:
+            file_mode = True
+
+        if git_mode:
+            try:
+                proc = subprocess.run(f"git format-patch -M --stdout -1 {filename}",
+                                      shell=True, capture_output=True, text=True)
+                rawlines = proc.stdout.rstrip('\n').split('\n')
+            except Exception as e:
+                print(f"{P}: {filename}: git format-patch failed - {e}", file=sys.stderr)
+                sys.exit(1)
+        elif file_mode:
+            try:
+                proc = subprocess.run(f"diff -u /dev/null {filename}",
+                                      shell=True, capture_output=True, text=True)
+                rawlines = proc.stdout.rstrip('\n').split('\n')
+            except Exception as e:
+                print(f"{P}: {filename}: diff failed - {e}", file=sys.stderr)
+                sys.exit(1)
+        elif filename == '-':
+            rawlines = [line.rstrip('\n') for line in sys.stdin]
+        else:
+            try:
+                with open(filename, 'r', encoding='utf-8', errors='replace') as f:
+                    rawlines = [line.rstrip('\n') for line in f]
+            except IOError as e:
+                print(f"{P}: {filename}: open failed - {e}", file=sys.stderr)
+                sys.exit(1)
+
+        if filename == '-':
+            vname = 'Your patch'
+        elif git_mode:
+            vname = f'Commit {filename[:12]} ("{git_commits.get(filename, "")}")'
+        else:
+            vname = filename
+
+        # Check Subject line for vname
+        for rl in rawlines:
+            m = re.match(r'^Subject:\s+(.+)', rl, re.IGNORECASE)
+            if m and filename == '-':
+                vname = f'"{m.group(1)}"'
+
+        if len(input_files) > 1 and quiet == 0:
+            print('-' * len(vname))
+            print(vname)
+            print('-' * len(vname))
+
+        if not process(filename):
+            exit_code = 1
+
+        rawlines = []
+        lines = []
+        fixed = []
+        modifierListFile = []
+        typeListFile = []
+        build_types()
+        if is_git_file:
+            file_mode = old_file_mode
+
+    if not quiet:
+        if use_type:
+            print("\nNOTE: Used message types: " + ' '.join(sorted(use_type.keys())))
+        if ignore_type:
+            print("\nNOTE: Ignored message types: " + ' '.join(sorted(ignore_type.keys())))
+        if exit_code:
+            print("\nNOTE: If any of the errors are false positives, please report\n"
+                  "      them to the maintainer, see CHECKPATCH in MAINTAINERS.")
+
+    sys.exit(exit_code)
+
+if __name__ == '__main__':
+    main()
-- 
2.52.0