Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions cr_checker/tests/test_cr_checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,67 @@ def test_exclusion_file_respected_in_nested_directory(tmp_path):
assert test_file.read_text(encoding="utf-8") == original_content


# test that a glob entry in the exclusion file is expanded recursively:
# `.claude/**/*` covers direct children as well as arbitrarily nested files,
# and a literal entry in the same file keeps working alongside it
def test_exclusion_file_expands_glob_pattern(tmp_path, monkeypatch):
cr_checker = load_cr_checker_module()
header_template = load_template("py")

workspace_dir = tmp_path / "workspace"
nested_dir = workspace_dir / ".claude" / "skills" / "some_skill" / "scripts"
nested_dir.mkdir(parents=True)
(workspace_dir / ".claude" / "agents").mkdir()
(workspace_dir / "tool").mkdir()

original_content = "some content\n"
nested_file = nested_dir / "fix_titles.py"
direct_child = workspace_dir / ".claude" / "settings.py"
agent_file = workspace_dir / ".claude" / "agents" / "reviewer.py"
literal_file = workspace_dir / "tool" / "generated.py"
outside_file = workspace_dir / "tool" / "checked.py"
for excluded in (nested_file, direct_child, agent_file, literal_file, outside_file):
excluded.write_text(original_content, encoding="utf-8")

exclusion_file = workspace_dir / "exclusion.txt"
exclusion_file.write_text(
"# AI - Stuff\n.claude/**/*\n\ntool/generated.py\n",
encoding="utf-8",
)

execroot = tmp_path / "execroot"
execroot.mkdir()
monkeypatch.chdir(execroot)
monkeypatch.setenv("BUILD_WORKSPACE_DIRECTORY", str(workspace_dir))

exclusion, valid = cr_checker.load_exclusion(exclusion_file)

assert valid is True
excluded_files = sorted(item for item in exclusion if Path(item).is_file())
assert excluded_files == sorted(
str(path) for path in (nested_file, direct_child, agent_file, literal_file)
)
assert str(outside_file) not in exclusion

results = cr_checker.process_files(
files=[nested_file, direct_child, agent_file, literal_file, outside_file],
templates={"py": header_template},
fix=True,
exclusion=exclusion,
use_mmap=False,
encoding="utf-8",
)

assert results["no_copyright"] == 1
assert results["fixed"] == 1
for skipped in (nested_file, direct_child, agent_file, literal_file):
assert skipped.read_text(encoding="utf-8") == original_content
skipped_header_inserted = outside_file.read_text(encoding="utf-8")
assert skipped_header_inserted.startswith(
header_template.format(year=datetime.now().year)
)


# test that a workspace-relative exclusion entry (as produced by e.g. `git ls-files`)
# is still resolved correctly when the process's cwd is not the workspace root, which
# is what happens under `bazel run`/`bazel test` (BUILD_WORKSPACE_DIRECTORY is set to
Expand Down
41 changes: 29 additions & 12 deletions cr_checker/tool/cr_checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@

BORDER_FILL_PATTERN = re.compile(r"([/*#'\-=+])\1{4,}")
FILL_CHARS_REGEX = r"[/*#'\-=+]+"
GLOB_CHARS = ("*", "?", "[")

LOGGER = logging.getLogger()

Expand Down Expand Up @@ -165,7 +166,7 @@ def add_template_for_extensions(
return templates


def load_exclusion(path):
def load_exclusion(path: Path) -> tuple[list[str], bool]:
"""
Loads the list of files being excluded from the copyright check.

Expand All @@ -176,23 +177,40 @@ def load_exclusion(path):
exclusion list is normalized the same way so it can be matched against
the paths produced by `collect_inputs`.

Lines may be either a literal path or a glob pattern. A line containing
any of ``*``, ``?`` or ``[`` is expanded with `Path.glob` relative to the
same base directory; ``**`` matches zero or more directory levels, so
``.claude/**/*`` excludes every file and sub-directory below ``.claude``
at any depth. A literal path must point at an existing file, as before.
Comment on lines +180 to +184
Blank lines and lines starting with ``#`` are ignored.

Args:
path (str): Path to the exclusion file.

Returns:
tuple(list, bool): a list of files that are excluded from the copyright check and a boolean indicating whether
all paths listed in the exclusion file exist and are files.
tuple(list[str], bool): a sorted, de-duplicated list of paths (as str) that are
excluded from the copyright check, and a boolean
indicating whether every line resolved to
something: literal paths must exist and be files,
glob patterns must match at least one path.
"""

workspace_dir = Path(os.environ.get("BUILD_WORKSPACE_DIRECTORY", "").strip())

exclusion = []
exclusion: set[str] = set()
valid = True
with open(path, "r", encoding="utf-8") as file:
for item in file.read().splitlines():
if not item:
for line in file.read().splitlines():
item = line.strip()
if not item or item.startswith("#"):
continue
if any(char in item for char in GLOB_CHARS):
Comment thread
MaximilianSoerenPollak marked this conversation as resolved.
matches = {str(match) for match in workspace_dir.glob(item)}
if not matches:
LOGGER.error("Exclusion pattern %s matched nothing.", item)
valid = False
continue
Comment on lines +212 to +215

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good finding. WIll include a test for it.

exclusion |= matches
continue
resolved = Path(workspace_dir / item)
resolved = workspace_dir / item
if not resolved.exists():
LOGGER.error("Excluded file %s does not exist.", item)
valid = False
Expand All @@ -201,10 +219,9 @@ def load_exclusion(path):
LOGGER.error("Excluded file %s is not a file.", item)
valid = False
continue
exclusion.append(str(resolved))

exclusion.add(str(resolved))
LOGGER.debug(exclusion)
return exclusion, valid
return sorted(exclusion), valid
Comment thread
MaximilianSoerenPollak marked this conversation as resolved.
Outdated


def configure_logging(log_file_path=None, verbose=False):
Expand Down