Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
29 changes: 27 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,9 +184,11 @@ tests, build/install, and running `beman-tidy` on `bemanproject/exemplar`.

The following configuration options may be used in a `.beman-tidy.yaml` file:

- `ignored_paths` - A list of paths to be excluded from all checks.
- `ignored_paths` - A list of patterns for paths to be excluded from all checks.
- Entries use [`.gitignore` pattern syntax](https://git-scm.com/docs/gitignore#_pattern_format): wildcards (`*`, `?`, `[abc]`), `**` for spanning directories, a leading `/` to anchor a pattern to the repository root, a trailing `/` to match directories only, and a leading `!` to re-include a path.
- To ignore a specific file, provide its full path relative to the repository root.
- To ignore a directory, provide the path to that directory. This will ignore the directory itself and all files and subdirectories within it. A trailing slash (`/`) is optional.
- A pattern without a `/` matches at any depth, so `detail` ignores every `detail` entry in the repository, while `/detail` only ignores the one in the root.

- Example:
```yaml
Expand All @@ -196,8 +198,29 @@ The following configuration options may be used in a `.beman-tidy.yaml` file:

# Ignores a directory and everything inside it
- include/beman/optional/another_dir

# Ignores every generated header, wherever it is
- "**/generated/*.hpp"

# Checks one file anyway, even though the pattern above covers it
- "!include/beman/optional/generated/api.hpp"
```

- `use_gitignore` - Whether the `.gitignore` files of the repository are honored. Defaults to `true`.
- Every `.gitignore` in the repository is applied, each relative to its own directory, exactly as git applies them. A `.gitignore` inside an ignored directory is not read.
- `README.md` and `LICENSE` are mandatory and are checked even if a `.gitignore` matches them. A warning is printed when that happens.
- As in git, a path below an ignored *directory* cannot be re-included: if a `.gitignore` has `generated/`, no `!generated/api.hpp` entry brings `api.hpp` back. The repository has to ignore `generated/*` instead.
- Set it to `false` to check paths that the repository does not track:

```yaml
use_gitignore: false
```

- Ignore sources are consulted in this order, and the first pattern that matches decides:
1. `ignored_paths` from `.beman-tidy.yaml`
2. the `.gitignore` files of the repository, deepest directory first
3. beman-tidy's built-in ignores (`build/`, `.git/`, `__pycache__/`, IDE directories, ...)

- `disabled_rules` - A list of rule names (or patterns) to be completely skipped during checks.
- To disable a specific rule, provide its exact name (e.g., `readme.title`).
- To disable all rules in a category, use a glob pattern with `*` (e.g., `readme.*` to skip all readme checks).
Expand Down Expand Up @@ -229,7 +252,9 @@ The following configuration options may be used in a `.beman-tidy.yaml` file:
- Why do I see "not implemented" in the summary?
- The check exists in the Beman Standard snapshot but does not yet have an implemented checker.
- How do I ignore files/directories?
- Use `ignored_paths` in `.beman-tidy.yaml`.
- Use `ignored_paths` in `.beman-tidy.yaml`. Entries use `.gitignore` pattern syntax.
- Why is a file in my repository not being checked at all?
- It is probably matched by your `.gitignore`, which beman-tidy honors by default. Set `use_gitignore: false` in `.beman-tidy.yaml` to check ignored files too, or re-include a single path with a `!` entry in `ignored_paths`.
- How do I disable specific rules?
- Use `disabled_rules` in `.beman-tidy.yaml`. You can specify exact rule names or glob patterns like `readme.*`.
- How do I get more detail?
Expand Down
2 changes: 1 addition & 1 deletion beman_tidy/lib/checks/base/directory_base_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def should_skip(self):
"""
if super().should_skip():
return True
return is_ignored(self.repo_info, self.relative_path)
return is_ignored(self.repo_info, self.relative_path, is_dir=True)

@abstractmethod
def check(self):
Expand Down
4 changes: 2 additions & 2 deletions beman_tidy/lib/checks/base/file_base_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from beman_tidy.lib.utils.string import normalize_path_for_display
from .base_check import BaseCheck
from ...utils.config import is_ignored, get_ignores
from ...utils.config import is_ignored, get_ignore_matcher


class FileBaseCheck(BaseCheck):
Expand Down Expand Up @@ -189,7 +189,7 @@ def _run_batch_operation(self, operation_callback):
self._validate()
assert self.file_path_generator is not None

ignores = get_ignores(self.repo_info)
ignores = get_ignore_matcher(self.repo_info)

all_files = self.file_path_generator(self.repo_path, ignores=ignores)
all_successful = True
Expand Down
57 changes: 37 additions & 20 deletions beman_tidy/lib/utils/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@
import logging

from pathlib import Path
from beman_tidy.lib.utils.file import get_repo_ignorable_subdirectories
from beman_tidy.lib.utils import ignore
from beman_tidy.lib.utils.logger_config import setup_logging

setup_logging()

# .gitignore is honored unless the repository configuration opts out.
DEFAULT_USE_GITIGNORE = True

def validate_config(config):
"""
Validate the repository configuration.
Expand Down Expand Up @@ -47,6 +50,25 @@ def validate_config(config):
if not _validate_disabled_rules(config):
return False

if not _validate_use_gitignore(config):
return False

return True


def _validate_use_gitignore(config):
"""
Validate the 'use_gitignore' configuration.
Returns True if valid, False otherwise.
"""
use_gitignore = config.get("use_gitignore")
if use_gitignore is None:
return True

if not isinstance(use_gitignore, bool):
logging.error(f"Error: 'use_gitignore' in .beman-tidy.yaml must be a boolean, but got {type(use_gitignore).__name__}.")
return False

return True


Expand Down Expand Up @@ -160,31 +182,26 @@ def load_repo_config(repo_path, config_path=None):
return merged_config


def get_ignores(repo_info):
def get_ignore_matcher(repo_info):
"""
Returns a combined list of default system ignores and user-configured ignores.
Returns the IgnoreMatcher for the repository, combining the built-in ignores,
the configured 'ignored_paths' and - unless 'use_gitignore' is false - the
.gitignore files of the repository.
"""

default_ignores = get_repo_ignorable_subdirectories()
user_ignores = repo_info.get("config", {}).get("ignored_paths") or []
return list(default_ignores) + user_ignores
config = repo_info.get("config", {})
return ignore.get_ignore_matcher(
repo_info.get("top_level", "."),
config.get("ignored_paths") or [],
config.get("use_gitignore", DEFAULT_USE_GITIGNORE),
)


def is_ignored(repo_info, relative_path):
def is_ignored(repo_info, relative_path, is_dir=None):
"""
Check if a given path is ignored by the configuration.
A path can be a file or a directory.
If a directory is ignored, all its children are also ignored.
"""
ignores = get_ignores(repo_info)
rel_path_str = relative_path.as_posix()
for ignore in ignores:
ignore_str = str(ignore).rstrip('/')

if rel_path_str == ignore_str:
return True

if rel_path_str.startswith(ignore_str + '/'):
return True

return False
@param is_dir: Whether the path is a directory. Determined from disk when None.
"""
return get_ignore_matcher(repo_info).is_ignored(relative_path, is_dir=is_dir)
56 changes: 25 additions & 31 deletions beman_tidy/lib/utils/file.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,14 @@
import os
from pathlib import Path
from .comments import determine_comment_type
from .ignore import DEFAULT_IGNORE_PATTERNS, IgnoreMatcher, get_ignore_matcher


def get_repo_ignorable_subdirectories():
"""
Returns a set of common build and IDE directories to ignore.
"""
return {
".git/",
"build/",
"cmake-build-debug/",
"cmake-build-release/",
".idea/",
".vscode/",
"__pycache__/",
".pytest_cache/",
".ruff_cache/",
"node_modules/",
"venv/",
"env/",
}
return set(DEFAULT_IGNORE_PATTERNS)


def get_cpp_header_extensions():
Expand All @@ -47,39 +35,46 @@ def get_cpp_extensions():
return get_cpp_header_extensions() | get_cpp_source_extensions()


def _is_ignored(path, ignores):
path_str = path.as_posix()
def _as_matcher(ignores, repo_path):
"""
Accept either an IgnoreMatcher or a plain iterable of ignore patterns.
"""
if isinstance(ignores, IgnoreMatcher):
return ignores

for pattern in ignores:
clean_pattern = pattern.rstrip("/") # trailing slash is optional
if path_str == clean_pattern or path_str.startswith(clean_pattern + "/"):
return True
return False
if ignores is None:
ignores = get_repo_ignorable_subdirectories()

# Pattern order decides which one wins, so only unordered input is sorted.
if isinstance(ignores, (set, frozenset)):
ignores = sorted(ignores)

# An explicit list of patterns is the whole story: .gitignore is not added.
return get_ignore_matcher(repo_path, ignores, use_gitignore=False)


def get_matched_paths(repo_path, extensions, ignores=None):
"""
Get all files in the repository matching the given extensions.
Ignores paths specified in 'ignores'.
"""
if ignores is None:
ignores = get_repo_ignorable_subdirectories()
repo_path = Path(repo_path)
matcher = _as_matcher(ignores, repo_path)

matched_files = []
repo_path = Path(repo_path)

for root, dirs, files in os.walk(repo_path):
rel_root = Path(root).relative_to(repo_path)

for d in list(dirs):
d_path = rel_root / d
if _is_ignored(d_path, ignores):
if matcher.is_ignored(d_path, is_dir=True):
dirs.remove(d)

for f in files:
f_path = rel_root / f
if f_path.suffix in extensions:
if not _is_ignored(f_path, ignores):
if not matcher.is_ignored(f_path, is_dir=False):
matched_files.append(f_path)

return sorted(list(set(matched_files)))
Expand Down Expand Up @@ -149,24 +144,23 @@ def get_commentable_files(repo_path, ignores=None):
Get all files that can contain a comment (and thus should have an SPDX identifier).
Covers C++, CMake, Python, shell scripts, and YAML files.
"""
if ignores is None:
ignores = get_repo_ignorable_subdirectories()
repo_path = Path(repo_path)
matcher = _as_matcher(ignores, repo_path)

matched_files = []
repo_path = Path(repo_path)

for root, dirs, files in os.walk(repo_path):
rel_root = Path(root).relative_to(repo_path)

for d in list(dirs):
d_path = rel_root / d
if _is_ignored(d_path, ignores):
if matcher.is_ignored(d_path, is_dir=True):
dirs.remove(d)

for f in files:
f_path = rel_root / f
if f_path.suffix in COMMENTABLE_EXTENSIONS or f_path.name in COMMENTABLE_FILENAMES:
if not _is_ignored(f_path, ignores):
if not matcher.is_ignored(f_path, is_dir=False):
matched_files.append(f_path)

return sorted(list(set(matched_files)))
Expand Down
Loading
Loading