From 93dc2cd528001d8a103d38a75db0450b31333214 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 27 Jun 2026 09:55:56 +0000 Subject: [PATCH] fix(scanner): repair SD022 compile error (undefined real_subdirs) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #545's rewrite of sd022_stale_path_after_rename/1 left a dangling `real_subdirs` after the binding was renamed to `real_basenames` (src_dir_index/1). `mix escript.build` fails with "undefined function real_subdirs/0", which breaks the Hypatia scan job estate-wide — the reusable workflow clones hypatia and builds the escript before scanning, so every consumer repo's scan dies at build. Fixes: - real_subdirs -> real_basenames in the final reject. - The lookbehind regex has one capture group, so the 2-group `[_, prefix, dir]` destructure could never match (FunctionClauseError after the compile error was resolved); reduced to `[_, dir]` and removed the now-unused real_path_reference?/4 + foreign_prefix?/2 (the negative lookbehind already excludes prefixed/foreign refs). - Guard also checks File.dir?(repo/src) so an empty `src/` directory still counts (git ls-files cannot see empty dirs). Verified locally: mix test test/structural_drift_test.exs -> 32 tests, 0 failures; module compiles via elixirc. Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_012JvDqQCW4CHxkEBAoJV5FU --- lib/rules/structural_drift.ex | 29 ++++------------------------- 1 file changed, 4 insertions(+), 25 deletions(-) diff --git a/lib/rules/structural_drift.ex b/lib/rules/structural_drift.ex index 3372c50e..ed870854 100644 --- a/lib/rules/structural_drift.ex +++ b/lib/rules/structural_drift.ex @@ -960,9 +960,9 @@ defmodule Hypatia.Rules.StructuralDrift do # written unanchored (crate-relative `scripts/x/src/bin`, doc-relative # `ffi/zig` → `src/connectors`, or prefixed `cli/src/commands`). Only a # `` that exists nowhere is a drift candidate. - {real_basenames, top_dirs} = src_dir_index(repo_path) + {real_basenames, _top_dirs} = src_dir_index(repo_path) - if MapSet.size(real_basenames) == 0 do + if MapSet.size(real_basenames) == 0 and not File.dir?(Path.join(repo_path, "src")) do [] else # Corpus / vendored / historical docs cite paths that are illustrative by @@ -1003,11 +1003,7 @@ defmodule Hypatia.Rules.StructuralDrift do # belongs to that other tree/crate/repo, not this repo's `src/`. ~r{(? Regex.scan(content) - |> Enum.map(fn [_, prefix, dir] -> {prefix, dir} end) - |> Enum.reject(fn {prefix, dir} -> - real_path_reference?(prefix, dir, real_basenames, top_dirs) - end) - |> Enum.map(fn {_prefix, dir} -> dir end) + |> Enum.map(fn [_, dir] -> dir end) |> Enum.uniq() |> Enum.reject(fn dir -> # Real repo-root `src//`, OR a crate-/module-relative @@ -1016,7 +1012,7 @@ defmodule Hypatia.Rules.StructuralDrift do # `path = "src/bin/…"`, or `ffi/zig/README.adoc` describing # its sibling `src/connectors/`). Only a reference that # resolves NOWHERE is genuine post-rename drift. - MapSet.member?(real_subdirs, dir) or + MapSet.member?(real_basenames, dir) or File.dir?(Path.join([repo_path, Path.dirname(rel), "src", dir])) end) |> Enum.map(fn stale_dir -> @@ -1039,23 +1035,6 @@ defmodule Hypatia.Rules.StructuralDrift do end end - # A `src//` reference is REAL (not drift) when either: - # * exists as a `**/src/` directory anywhere in the tree - # (resolves regardless of how it was anchored), or - # * it carries a path prefix whose leading segment is NOT a real - # top-level directory of this repo — i.e. it cites another repo or an - # example project (`vcl-ut/src/core`, `examples/nestjs/src/i18n`). - defp real_path_reference?(prefix, dir, real_basenames, top_dirs) do - MapSet.member?(real_basenames, dir) or foreign_prefix?(prefix, top_dirs) - end - - defp foreign_prefix?("", _top_dirs), do: false - - defp foreign_prefix?(prefix, top_dirs) do - first = prefix |> String.trim_trailing("/") |> String.split("/") |> List.first() - first != nil and not MapSet.member?(top_dirs, first) - end - # Index every `**/src/` directory in the tracked tree (via the file # list, so it respects .gitignore and skips _build/deps), returning the set # of basenames plus the set of real top-level directory names.