From 98463ad991f5ac92dbb8cc003fde6303f99a136d Mon Sep 17 00:00:00 2001 From: Microindole Date: Tue, 12 May 2026 23:44:16 +0800 Subject: [PATCH] tools: split structure checks by responsibility --- docs/agents/code-style.md | 5 +- scripts/tools/.gitignore | 2 + scripts/tools/check_structure.py | 451 +----------------- scripts/tools/structure_checks/__init__.py | 1 + scripts/tools/structure_checks/common.py | 83 ++++ scripts/tools/structure_checks/makefiles.py | 87 ++++ .../structure_checks/public_header_decls.py | 89 ++++ .../tools/structure_checks/public_headers.py | 114 +++++ .../tools/structure_checks/source_rules.py | 66 +++ 9 files changed, 470 insertions(+), 428 deletions(-) create mode 100644 scripts/tools/.gitignore create mode 100644 scripts/tools/structure_checks/__init__.py create mode 100644 scripts/tools/structure_checks/common.py create mode 100644 scripts/tools/structure_checks/makefiles.py create mode 100644 scripts/tools/structure_checks/public_header_decls.py create mode 100644 scripts/tools/structure_checks/public_headers.py create mode 100644 scripts/tools/structure_checks/source_rules.py diff --git a/docs/agents/code-style.md b/docs/agents/code-style.md index 455ffd6..acff7b5 100644 --- a/docs/agents/code-style.md +++ b/docs/agents/code-style.md @@ -75,7 +75,10 @@ - 可独立执行的检查放在 `scripts/checks/`。 - 多个检查共享的函数放在 `scripts/lib/`。 - 不把所有检查逻辑持续堆进 `scripts/check.sh`。 -- 项目结构规则放在 `scripts/tools/check_structure.py`,由 `scripts/checks/structure.sh` 调用;优先用宿主 Linux/LLVM 工具实现底层检查,把 Tianole 自己的目录和 include 约束固化在轻量 Python 工具里。 +- 项目结构规则由 `scripts/tools/check_structure.py` 编排,具体规则拆到 + `scripts/tools/structure_checks/`;不要把 Python 工具堆成单个超大文件。 +- 优先用宿主 Linux/LLVM 工具实现底层检查,把 Tianole 自己的目录和 include + 约束固化在轻量 Python 工具里。 - 启动阶段内核自测集中放在 `kernel/selftest/`,不要散落在具体实现目录里。 ## 验证 diff --git a/scripts/tools/.gitignore b/scripts/tools/.gitignore new file mode 100644 index 0000000..43ae0e2 --- /dev/null +++ b/scripts/tools/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +*.py[cod] diff --git a/scripts/tools/check_structure.py b/scripts/tools/check_structure.py index 10db333..b471cad 100644 --- a/scripts/tools/check_structure.py +++ b/scripts/tools/check_structure.py @@ -1,449 +1,46 @@ #!/usr/bin/env python3 import argparse -import re -import subprocess import sys -from pathlib import Path - -SOURCE_ROOTS = ("arch", "include", "kernel", "mm") -C_SOURCE_ROOTS = ("arch", "kernel", "mm") -MAKEFILE_SOURCE_VARS = ( - "ARCH_KERNEL_SRCS", - "ARCH_MM_SRCS", - "BOOT_SRCS", - "KERNEL_SRCS", - "MM_SRCS", +from structure_checks.common import ( + all_files, + c_files, + changed_files, + public_headers, + repo_root, + source_files, +) +from structure_checks.makefiles import check_makefile_source_lists +from structure_checks.public_headers import check_public_header_docs +from structure_checks.source_rules import ( + check_no_bare_negative_errno, + check_no_relative_parent_includes, + check_scheduler_state_writes, + check_selftests_are_centralized, ) -def repo_root() -> Path: - return Path(__file__).resolve().parents[2] - - -def run_git(root: Path, args: list[str]) -> list[str]: - result = subprocess.run( - ["git", *args], - cwd=root, - check=True, - text=True, - stdout=subprocess.PIPE, - ) - return [line for line in result.stdout.splitlines() if line] - - -def changed_files(root: Path) -> set[Path]: - files = set() - - for path in run_git(root, ["diff", "--name-only", "--diff-filter=ACMR"]): - files.add(Path(path)) - - for path in run_git( - root, ["diff", "--cached", "--name-only", "--diff-filter=ACMR"] - ): - files.add(Path(path)) - - for path in run_git(root, ["ls-files", "--others", "--exclude-standard"]): - files.add(Path(path)) - - return files - - -def all_files(root: Path) -> set[Path]: - files = set() - - for base in SOURCE_ROOTS: - root_dir = root / base - if not root_dir.exists(): - continue - - for path in root_dir.rglob("*"): - if path.is_file(): - files.add(path.relative_to(root)) - - for path in (root / "scripts").rglob("*"): - if path.is_file(): - files.add(path.relative_to(root)) - - return files - - -def source_files(files: set[Path]) -> list[Path]: - return sorted(path for path in files if path.suffix in (".c", ".h")) - - -def public_headers(files: set[Path]) -> list[Path]: - return sorted( - path - for path in files - if path.suffix == ".h" - and len(path.parts) == 3 - and path.parts[:2] == ("include", "tianole") - ) - - -def c_files(files: set[Path]) -> list[Path]: - return sorted( - path - for path in files - if path.suffix == ".c" and path.parts and path.parts[0] in C_SOURCE_ROOTS - ) - - -def read_text(root: Path, path: Path) -> str: - return (root / path).read_text(encoding="utf-8") - - -def check_no_relative_parent_includes(root: Path, files: list[Path]) -> list[str]: - errors = [] - include_re = re.compile(r'^\s*#include\s+[<"]\.\./') - - for path in files: - for line_no, line in enumerate(read_text(root, path).splitlines(), 1): - if include_re.search(line): - errors.append( - f"{path}:{line_no}: do not use ../ includes; " - "promote shared headers or add a private include root" - ) - - return errors - - -def check_no_bare_negative_errno(root: Path, files: list[Path]) -> list[str]: - errors = [] - return_re = re.compile(r"\breturn\s+-[0-9]+\s*;") - - for path in files: - for line_no, line in enumerate(read_text(root, path).splitlines(), 1): - if return_re.search(line): - errors.append( - f"{path}:{line_no}: return a symbolic negative errno " - "such as -EINVAL instead of a bare negative number" - ) - - return errors - - -def check_scheduler_state_writes(root: Path, files: list[Path]) -> list[str]: - errors = [] - state_write_re = re.compile(r"->state\s*=") - allowed_path = Path("kernel/sched/sched.h") - - for path in files: - if path == allowed_path: - continue - - for line_no, line in enumerate(read_text(root, path).splitlines(), 1): - if state_write_re.search(line): - errors.append( - f"{path}:{line_no}: thread state writes must use " - "kernel/sched/sched.h helpers" - ) - - return errors - - -def is_function_declaration_start(line: str) -> bool: - stripped = line.strip() - - if stripped == "" or stripped.startswith("#"): - return False - - if stripped.startswith(("typedef ", "struct ", "enum ", "union ")): - return False - - if "(" not in stripped or stripped.startswith("*"): - return False - - if "(*" in stripped or stripped.endswith("{"): - return False - - return True - - -def collect_function_declarations(lines: list[str]) -> list[tuple[int, int, str]]: - decls = [] - index = 0 - - while index < len(lines): - if not is_function_declaration_start(lines[index]): - index += 1 - continue - - start = index - text = lines[index].strip() - while ";" not in lines[index]: - index += 1 - if index >= len(lines): - break - text += " " + lines[index].strip() - - if index < len(lines): - decls.append((start, index, text)) - - index += 1 - - return decls - - -def is_public_type_start(line: str) -> bool: - stripped = line.strip() - - if stripped.startswith(("struct ", "enum ", "union ")): - return stripped.endswith("{") or "{" in stripped - - if not stripped.startswith("typedef "): - return False - - return "(" not in stripped - - -def collect_public_type_declarations(lines: list[str]) -> list[tuple[int, int, str]]: - decls = [] - index = 0 - - while index < len(lines): - if not is_public_type_start(lines[index]): - index += 1 - continue - - start = index - text = lines[index].strip() - while ";" not in lines[index]: - index += 1 - if index >= len(lines): - break - text += " " + lines[index].strip() - - if index < len(lines): - decls.append((start, index, text)) - - index += 1 - - return decls - - -def is_documented_public_define(lines: list[str], index: int, guard: str | None) -> bool: - stripped = lines[index].strip() - - if not stripped.startswith("#define "): - return False - - parts = stripped.split() - if len(parts) < 2: - return False - - name = parts[1].split("(", 1)[0] - if name == guard: - return False - - return True - - -def header_guard_name(lines: list[str]) -> str | None: - for line in lines: - stripped = line.strip() - if stripped.startswith("#define "): - parts = stripped.split() - if len(parts) >= 2: - return parts[1] - - return None - - -def has_kernel_doc_before(lines: list[str], start: int) -> bool: - index = start - 1 - - while index >= 0 and lines[index].strip() == "": - index -= 1 - - if index < 0 or lines[index].strip() != "*/": - return False - - while index >= 0: - if lines[index].strip().startswith("/**"): - return True - index -= 1 - - return False - - -def has_blank_line_before_doc(lines: list[str], start: int) -> bool: - index = start - 1 - - while index >= 0 and lines[index].strip() == "": - index -= 1 - - if index < 0 or lines[index].strip() != "*/": - return True - - while index >= 0 and not lines[index].strip().startswith("/**"): - index -= 1 - - if index <= 0: - return True - - return lines[index - 1].strip() == "" - - -def symbol_name_from_type_declaration(text: str) -> str: - if text.startswith("typedef "): - return text.rsplit("}", 1)[-1].strip().rstrip(";").split()[-1] - - parts = text.split() - if len(parts) >= 2: - return parts[1] - - return "" - - -def check_kernel_doc_block( - errors: list[str], path: Path, lines: list[str], start: int, kind: str, name: str -) -> None: - line_no = start + 1 - - if not has_kernel_doc_before(lines, start): - errors.append( - f"{path}:{line_no}: public {kind} '{name}' needs " - "a kernel-doc comment immediately before it" - ) - return - - if not has_blank_line_before_doc(lines, start): - errors.append( - f"{path}:{line_no}: public {kind} '{name}' " - "comment block must be separated by a blank line" - ) - - -def check_public_header_docs(root: Path, files: list[Path]) -> list[str]: - errors = [] - - for path in files: - lines = read_text(root, path).splitlines() - for start, _end, text in collect_function_declarations(lines): - name = text.split("(", 1)[0].split()[-1].lstrip("*") - check_kernel_doc_block(errors, path, lines, start, "function", name) - - for start, _end, text in collect_public_type_declarations(lines): - name = symbol_name_from_type_declaration(text) - check_kernel_doc_block(errors, path, lines, start, "type", name) - - guard = header_guard_name(lines) - for index, line in enumerate(lines): - if not is_documented_public_define(lines, index, guard): - continue - - name = line.strip().split()[1].split("(", 1)[0] - check_kernel_doc_block(errors, path, lines, index, "macro", name) - - return errors - - -def check_selftests_are_centralized(files: list[Path]) -> list[str]: - errors = [] - - for path in files: - if "selftest" not in path.name: - continue - - if len(path.parts) < 2 or path.parts[:2] != ("kernel", "selftest"): - errors.append(f"{path}: kernel selftests belong under kernel/selftest/") - - return errors - - -def expand_make_var_token(token: str, makefile: Path) -> str: - return token.replace("$(ARCH_DIR)", "arch/x86").replace( - "$(BUILD_DIR)", "build" - ) - - -def parse_makefile_sources(root: Path, makefile: Path) -> set[Path]: - text = read_text(root, makefile) - lines = text.splitlines() - sources = set() - index = 0 - - while index < len(lines): - line = lines[index] - match = re.match(r"^([A-Z0-9_]+)\s*:=", line) - if match == None or match.group(1) not in MAKEFILE_SOURCE_VARS: - index += 1 - continue - - remainder = line.split(":=", 1)[1].strip() - while True: - continued = remainder.endswith("\\") - remainder = remainder[:-1].strip() if continued else remainder - if remainder: - for token in remainder.split(): - if token.endswith(".c"): - sources.add(Path(expand_make_var_token(token, makefile))) - - if not continued: - break - - index += 1 - if index >= len(lines): - break - remainder = lines[index].strip() - - index += 1 - - return sources - - -def makefile_sources(root: Path) -> set[Path]: - sources = set() - - for makefile in sorted(root.rglob("Makefile")): - if "build" in makefile.parts: - continue - sources.update(parse_makefile_sources(root, makefile.relative_to(root))) - - return sources - - -def check_makefile_source_lists(root: Path, files: set[Path], all_mode: bool) -> list[str]: - errors = [] - listed = makefile_sources(root) - existing = set(c_files(all_files(root))) - - for path in sorted(listed): - if not (root / path).exists(): - errors.append(f"{path}: listed in Makefile but file does not exist") - - if all_mode: - for path in sorted(existing - listed): - errors.append(f"{path}: C source is not listed in a Makefile source list") - else: - for path in sorted(c_files(files)): - if path not in listed: - errors.append( - f"{path}: changed C source is not listed in a Makefile source list" - ) - - return errors - - -def main() -> int: +def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description="Check Tianole structure rules") mode = parser.add_mutually_exclusive_group() mode.add_argument("--all", action="store_true", help="check the whole tree") mode.add_argument( "--changed", action="store_true", help="check changed and untracked files" ) - args = parser.parse_args() + return parser.parse_args() + +def main() -> int: + args = parse_args() root = repo_root() all_mode = not args.changed files = all_files(root) if all_mode else changed_files(root) errors = [] - errors.extend(check_no_relative_parent_includes(root, source_files(files))) - errors.extend(check_no_bare_negative_errno(root, source_files(files))) - errors.extend(check_scheduler_state_writes(root, source_files(files))) + sources = source_files(files) + errors.extend(check_no_relative_parent_includes(root, sources)) + errors.extend(check_no_bare_negative_errno(root, sources)) + errors.extend(check_scheduler_state_writes(root, sources)) errors.extend(check_public_header_docs(root, public_headers(files))) errors.extend(check_selftests_are_centralized(c_files(files))) errors.extend(check_makefile_source_lists(root, files, all_mode)) diff --git a/scripts/tools/structure_checks/__init__.py b/scripts/tools/structure_checks/__init__.py new file mode 100644 index 0000000..4657a49 --- /dev/null +++ b/scripts/tools/structure_checks/__init__.py @@ -0,0 +1 @@ +"""Structure checks for Tianole source layout.""" diff --git a/scripts/tools/structure_checks/common.py b/scripts/tools/structure_checks/common.py new file mode 100644 index 0000000..237a446 --- /dev/null +++ b/scripts/tools/structure_checks/common.py @@ -0,0 +1,83 @@ +import subprocess +from pathlib import Path + + +SOURCE_ROOTS = ("arch", "include", "kernel", "mm") +C_SOURCE_ROOTS = ("arch", "kernel", "mm") + + +def repo_root() -> Path: + return Path(__file__).resolve().parents[3] + + +def run_git(root: Path, args: list[str]) -> list[str]: + result = subprocess.run( + ["git", *args], + cwd=root, + check=True, + text=True, + stdout=subprocess.PIPE, + ) + return [line for line in result.stdout.splitlines() if line] + + +def changed_files(root: Path) -> set[Path]: + files = set() + + for path in run_git(root, ["diff", "--name-only", "--diff-filter=ACMR"]): + files.add(Path(path)) + + for path in run_git( + root, ["diff", "--cached", "--name-only", "--diff-filter=ACMR"] + ): + files.add(Path(path)) + + for path in run_git(root, ["ls-files", "--others", "--exclude-standard"]): + files.add(Path(path)) + + return files + + +def all_files(root: Path) -> set[Path]: + files = set() + + for base in SOURCE_ROOTS: + root_dir = root / base + if not root_dir.exists(): + continue + + for path in root_dir.rglob("*"): + if path.is_file(): + files.add(path.relative_to(root)) + + for path in (root / "scripts").rglob("*"): + if path.is_file(): + files.add(path.relative_to(root)) + + return files + + +def source_files(files: set[Path]) -> list[Path]: + return sorted(path for path in files if path.suffix in (".c", ".h")) + + +def public_headers(files: set[Path]) -> list[Path]: + return sorted( + path + for path in files + if path.suffix == ".h" + and len(path.parts) == 3 + and path.parts[:2] == ("include", "tianole") + ) + + +def c_files(files: set[Path]) -> list[Path]: + return sorted( + path + for path in files + if path.suffix == ".c" and path.parts and path.parts[0] in C_SOURCE_ROOTS + ) + + +def read_text(root: Path, path: Path) -> str: + return (root / path).read_text(encoding="utf-8") diff --git a/scripts/tools/structure_checks/makefiles.py b/scripts/tools/structure_checks/makefiles.py new file mode 100644 index 0000000..6009226 --- /dev/null +++ b/scripts/tools/structure_checks/makefiles.py @@ -0,0 +1,87 @@ +import re +from pathlib import Path + +from .common import all_files, c_files, read_text + + +MAKEFILE_SOURCE_VARS = ( + "ARCH_KERNEL_SRCS", + "ARCH_MM_SRCS", + "BOOT_SRCS", + "KERNEL_SRCS", + "MM_SRCS", +) + + +def expand_make_var_token(token: str, makefile: Path) -> str: + return token.replace("$(ARCH_DIR)", "arch/x86").replace( + "$(BUILD_DIR)", "build" + ) + + +def parse_makefile_sources(root: Path, makefile: Path) -> set[Path]: + text = read_text(root, makefile) + lines = text.splitlines() + sources = set() + index = 0 + + while index < len(lines): + line = lines[index] + match = re.match(r"^([A-Z0-9_]+)\s*:=", line) + if match == None or match.group(1) not in MAKEFILE_SOURCE_VARS: + index += 1 + continue + + remainder = line.split(":=", 1)[1].strip() + while True: + continued = remainder.endswith("\\") + remainder = remainder[:-1].strip() if continued else remainder + if remainder: + for token in remainder.split(): + if token.endswith(".c"): + sources.add(Path(expand_make_var_token(token, makefile))) + + if not continued: + break + + index += 1 + if index >= len(lines): + break + remainder = lines[index].strip() + + index += 1 + + return sources + + +def makefile_sources(root: Path) -> set[Path]: + sources = set() + + for makefile in sorted(root.rglob("Makefile")): + if "build" in makefile.parts: + continue + sources.update(parse_makefile_sources(root, makefile.relative_to(root))) + + return sources + + +def check_makefile_source_lists(root: Path, files: set[Path], all_mode: bool) -> list[str]: + errors = [] + listed = makefile_sources(root) + existing = set(c_files(all_files(root))) + + for path in sorted(listed): + if not (root / path).exists(): + errors.append(f"{path}: listed in Makefile but file does not exist") + + if all_mode: + for path in sorted(existing - listed): + errors.append(f"{path}: C source is not listed in a Makefile source list") + else: + for path in sorted(c_files(files)): + if path not in listed: + errors.append( + f"{path}: changed C source is not listed in a Makefile source list" + ) + + return errors diff --git a/scripts/tools/structure_checks/public_header_decls.py b/scripts/tools/structure_checks/public_header_decls.py new file mode 100644 index 0000000..9e308ba --- /dev/null +++ b/scripts/tools/structure_checks/public_header_decls.py @@ -0,0 +1,89 @@ +def is_function_declaration_start(line: str) -> bool: + stripped = line.strip() + + if stripped == "" or stripped.startswith("#"): + return False + + if stripped.startswith(("typedef ", "struct ", "enum ", "union ")): + return False + + if "(" not in stripped or stripped.startswith("*"): + return False + + if "(*" in stripped or stripped.endswith("{"): + return False + + return True + + +def collect_function_declarations(lines: list[str]) -> list[tuple[int, int, str]]: + decls = [] + index = 0 + + while index < len(lines): + if not is_function_declaration_start(lines[index]): + index += 1 + continue + + start = index + text = lines[index].strip() + while ";" not in lines[index]: + index += 1 + if index >= len(lines): + break + text += " " + lines[index].strip() + + if index < len(lines): + decls.append((start, index, text)) + + index += 1 + + return decls + + +def is_public_type_start(line: str) -> bool: + stripped = line.strip() + + if stripped.startswith(("struct ", "enum ", "union ")): + return stripped.endswith("{") or "{" in stripped + + if not stripped.startswith("typedef "): + return False + + return "(" not in stripped + + +def collect_public_type_declarations(lines: list[str]) -> list[tuple[int, int, str]]: + decls = [] + index = 0 + + while index < len(lines): + if not is_public_type_start(lines[index]): + index += 1 + continue + + start = index + text = lines[index].strip() + while ";" not in lines[index]: + index += 1 + if index >= len(lines): + break + text += " " + lines[index].strip() + + if index < len(lines): + decls.append((start, index, text)) + + index += 1 + + return decls + + +def symbol_name_from_type_declaration(text: str) -> str: + if text.startswith("typedef "): + return text.rsplit("}", 1)[-1].strip().rstrip(";").split()[-1] + + parts = text.split() + if len(parts) >= 2: + return parts[1] + + return "" diff --git a/scripts/tools/structure_checks/public_headers.py b/scripts/tools/structure_checks/public_headers.py new file mode 100644 index 0000000..7896a53 --- /dev/null +++ b/scripts/tools/structure_checks/public_headers.py @@ -0,0 +1,114 @@ +from pathlib import Path + +from .common import read_text +from .public_header_decls import ( + collect_function_declarations, + collect_public_type_declarations, + symbol_name_from_type_declaration, +) + + +def is_documented_public_define(lines: list[str], index: int, guard: str | None) -> bool: + stripped = lines[index].strip() + + if not stripped.startswith("#define "): + return False + + parts = stripped.split() + if len(parts) < 2: + return False + + name = parts[1].split("(", 1)[0] + if name == guard: + return False + + return True + + +def header_guard_name(lines: list[str]) -> str | None: + for line in lines: + stripped = line.strip() + if stripped.startswith("#define "): + parts = stripped.split() + if len(parts) >= 2: + return parts[1] + + return None + + +def has_kernel_doc_before(lines: list[str], start: int) -> bool: + index = start - 1 + + while index >= 0 and lines[index].strip() == "": + index -= 1 + + if index < 0 or lines[index].strip() != "*/": + return False + + while index >= 0: + if lines[index].strip().startswith("/**"): + return True + index -= 1 + + return False + + +def has_blank_line_before_doc(lines: list[str], start: int) -> bool: + index = start - 1 + + while index >= 0 and lines[index].strip() == "": + index -= 1 + + if index < 0 or lines[index].strip() != "*/": + return True + + while index >= 0 and not lines[index].strip().startswith("/**"): + index -= 1 + + if index <= 0: + return True + + return lines[index - 1].strip() == "" + + +def check_kernel_doc_block( + errors: list[str], path: Path, lines: list[str], start: int, kind: str, name: str +) -> None: + line_no = start + 1 + + if not has_kernel_doc_before(lines, start): + errors.append( + f"{path}:{line_no}: public {kind} '{name}' needs " + "a kernel-doc comment immediately before it" + ) + return + + if not has_blank_line_before_doc(lines, start): + errors.append( + f"{path}:{line_no}: public {kind} '{name}' " + "comment block must be separated by a blank line" + ) + + +def check_public_header_docs(root: Path, files: list[Path]) -> list[str]: + errors = [] + + for path in files: + lines = read_text(root, path).splitlines() + for start, _end, text in collect_function_declarations(lines): + name = text.split("(", 1)[0].split()[-1].lstrip("*") + check_kernel_doc_block(errors, path, lines, start, "function", name) + + for start, _end, text in collect_public_type_declarations(lines): + name = symbol_name_from_type_declaration(text) + check_kernel_doc_block(errors, path, lines, start, "type", name) + + guard = header_guard_name(lines) + for index, line in enumerate(lines): + if not is_documented_public_define(lines, index, guard): + continue + + name = line.strip().split()[1].split("(", 1)[0] + check_kernel_doc_block(errors, path, lines, index, "macro", name) + + return errors diff --git a/scripts/tools/structure_checks/source_rules.py b/scripts/tools/structure_checks/source_rules.py new file mode 100644 index 0000000..e9009a5 --- /dev/null +++ b/scripts/tools/structure_checks/source_rules.py @@ -0,0 +1,66 @@ +import re +from pathlib import Path + +from .common import read_text + + +def check_no_relative_parent_includes(root: Path, files: list[Path]) -> list[str]: + errors = [] + include_re = re.compile(r'^\s*#include\s+[<"]\.\./') + + for path in files: + for line_no, line in enumerate(read_text(root, path).splitlines(), 1): + if include_re.search(line): + errors.append( + f"{path}:{line_no}: do not use ../ includes; " + "promote shared headers or add a private include root" + ) + + return errors + + +def check_no_bare_negative_errno(root: Path, files: list[Path]) -> list[str]: + errors = [] + return_re = re.compile(r"\breturn\s+-[0-9]+\s*;") + + for path in files: + for line_no, line in enumerate(read_text(root, path).splitlines(), 1): + if return_re.search(line): + errors.append( + f"{path}:{line_no}: return a symbolic negative errno " + "such as -EINVAL instead of a bare negative number" + ) + + return errors + + +def check_scheduler_state_writes(root: Path, files: list[Path]) -> list[str]: + errors = [] + state_write_re = re.compile(r"->state\s*=") + allowed_path = Path("kernel/sched/sched.h") + + for path in files: + if path == allowed_path: + continue + + for line_no, line in enumerate(read_text(root, path).splitlines(), 1): + if state_write_re.search(line): + errors.append( + f"{path}:{line_no}: thread state writes must use " + "kernel/sched/sched.h helpers" + ) + + return errors + + +def check_selftests_are_centralized(files: list[Path]) -> list[str]: + errors = [] + + for path in files: + if "selftest" not in path.name: + continue + + if len(path.parts) < 2 or path.parts[:2] != ("kernel", "selftest"): + errors.append(f"{path}: kernel selftests belong under kernel/selftest/") + + return errors