From 5d1c2d74d1e18e80b788e5778b10b94053343c2c Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 13 May 2026 20:35:38 +0000 Subject: doc-linter: realpath() resolution, DOCS_ROOT in walker, indent fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three Copilot findings on ab497bf: 1. is_docs_path() docstring claimed paths 'resolve' under docs/, but the implementation only normalized via abspath() — a symlink under docs/ that points outside the tree would be treated as in-scope. Switch both inputs to os.path.realpath() so symlinks are followed to their real targets. The reverse case is also handled: if docs/ is itself a symlink (some CI checkouts), realpath() resolves it consistently for both sides of the commonpath comparison. Verified with a synthetic case: docs/poison.md -> /etc/hosts now returns False (with abspath() it returned True). 2. The auto-discover fallback in main() still hardcoded os.walk('docs') instead of using the new DOCS_ROOT constant. Use DOCS_ROOT in both paths so the docs root is configured in exactly one place. 3. Indentation inside 'for file in files:' was double-indented (8 spaces under the for, instead of 4) — pre-existing oddity from before 65a8e9f, preserved through the is_docs_path() addition. Normalize to a single indent level under the loop. CI behavior unchanged: tj-actions/changed-files passes repo-relative paths with no symlinks under docs/, which were already handled. The realpath() switch only changes behavior in the symlink-escape case, which was a bug. --- scripts/doc-linter.py | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) (limited to 'scripts') diff --git a/scripts/doc-linter.py b/scripts/doc-linter.py index 9f1e6f57..c3304718 100644 --- a/scripts/doc-linter.py +++ b/scripts/doc-linter.py @@ -38,12 +38,17 @@ DOCS_ROOT = 'docs' def is_docs_path(path): """Return True iff `path` resolves under the repo's `docs/` tree. - Accepts both repo-relative and absolute paths. Paths are normalized - against the linter's working directory (CI invokes it from the repo - root; the same is expected for local runs). + Accepts both repo-relative and absolute paths. Both `path` and + `DOCS_ROOT` are resolved with `os.path.realpath`, so symlinks are + followed to their real targets — a symlink under `docs/` that + points outside the tree is correctly treated as out-of-scope, and + a `docs/` that is itself a symlink (e.g., in some CI checkouts) is + correctly treated as the docs root. Paths are normalized against + the linter's working directory (CI invokes it from the repo root; + the same is expected for local runs). """ - abs_path = os.path.abspath(path) - abs_docs = os.path.abspath(DOCS_ROOT) + abs_path = os.path.realpath(path) + abs_docs = os.path.realpath(DOCS_ROOT) try: return os.path.commonpath([abs_path, abs_docs]) == abs_docs except ValueError: @@ -248,15 +253,15 @@ def main(): try: files = ast.literal_eval(sys.argv[1]) for file in files: - if ( - file.endswith(SUPPORTED_EXTS) - and "_build" not in file - and is_docs_path(file) - ): - if handle_file_action(file) is False: - bool_error = False + if ( + file.endswith(SUPPORTED_EXTS) + and "_build" not in file + and is_docs_path(file) + ): + if handle_file_action(file) is False: + bool_error = False except Exception as e: - for root, dirs, files in os.walk("docs"): + for root, dirs, files in os.walk(DOCS_ROOT): path = root.split(os.sep) for file in files: if file.endswith(SUPPORTED_EXTS) and "_build" not in path: -- cgit v1.2.3