Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
7 changes: 6 additions & 1 deletion .github/actions/bc-lint/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ inputs:
description: 'Link to the docs to display in case of failure'
required: false
default: ''
config_dir:
description: 'Directory to load .bc-linter.yml from (defaults to repository root)'
required: false
default: ''
runs:
using: 'composite'
steps:
Expand Down Expand Up @@ -66,7 +70,8 @@ runs:
../_test-infra/tools/stronghold/bin/check-api-compatibility \
--base-commit=${{ inputs.base_sha }} \
--head-commit=${{ steps.merge_changes.outputs.new_head_sha }} \
${{ inputs.suppression == 'true' && '--suppressed' || '' }}
${{ inputs.suppression == 'true' && '--suppressed' || '' }} \
${{ inputs.config_dir != '' && '--config-dir=' + inputs.config_dir || '' }}

- name: Display documentation link if failed
if: ${{ failure() && inputs.docs_link }}
Expand Down
6 changes: 4 additions & 2 deletions tools/stronghold/docs/bc_linter_config.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ The config enables repo‑specific path selection, rule suppression, and custom
annotations to include/exclude specific APIs.

### Config file location
- Place a YAML file named `.bc-linter.yml` at the repository root being linted
(the target repo).
- By default the linter searches for a `.bc-linter.yml` file at the root of
the repository being linted.
- Provide an alternative directory with `--config-dir` if the file lives
somewhere else.
- If the file is missing or empty, defaults are applied (see below).

### Schema (YAML)
Expand Down
10 changes: 9 additions & 1 deletion tools/stronghold/src/api/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ def run() -> None:
action="store_true",
help="Enable verbose output",
)
parser.add_argument(
"--config-dir",
type=str,
default=None,
help="Directory to load .bc-linter.yml from (defaults to repository root)",
)
args = parser.parse_args(sys.argv[1:])

repo = api.git.Repository(pathlib.Path("."))
Expand All @@ -46,7 +52,9 @@ def run() -> None:
print("::endgroup::")

# Load config and optionally print when detected
cfg, cfg_status = api.config.load_config_with_status(repo.dir)
cfg, cfg_status = api.config.load_config_with_status(
repo.dir, config_dir=args.config_dir
)
if cfg_status == "parsed":
# Explicitly log successful config discovery and parsing
print("BC-linter: Using .bc-linter.yml (parsed successfully)")
Expand Down
32 changes: 25 additions & 7 deletions tools/stronghold/src/api/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,15 +54,29 @@ def default_config() -> Config:
return Config()


def load_config_with_status(repo_root: pathlib.Path) -> tuple[Config, str]:
"""Loads configuration from `.bc-linter.yml` in the given repository root.
def load_config_with_status(
repo_root: pathlib.Path, *, config_dir: pathlib.Path | str | None = None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

is there a reason to pass these as two separate params, instead of a single concatenated path to load config from?

) -> tuple[Config, str]:
"""Loads configuration from `.bc-linter.yml`.

By default, configuration is loaded from the repository root. A custom
configuration directory may be provided via ``config_dir`` either as an
absolute path or a path relative to ``repo_root``.

Returns (config, status) where status is one of:
- 'parsed' -> config file existed and parsed successfully
- 'default_missing' -> no config file found
- 'default_error' -> file existed but YAML missing/invalid or parser unavailable
"""
cfg_path = repo_root / ".bc-linter.yml"

cfg_base = repo_root
if config_dir is not None:
cfg_dir_path = pathlib.Path(config_dir)
if not cfg_dir_path.is_absolute():
cfg_dir_path = repo_root / cfg_dir_path
cfg_base = cfg_dir_path

cfg_path = cfg_base / ".bc-linter.yml"
if not cfg_path.exists():
return (default_config(), "default_missing")

Expand Down Expand Up @@ -159,12 +173,16 @@ def _ann_list(raw: Any) -> list[AnnotationSpec]:
return (cfg, "parsed")


def load_config(repo_root: pathlib.Path) -> Config:
"""Loads configuration from `.bc-linter.yml` in the given repository root.
def load_config(
repo_root: pathlib.Path, *, config_dir: pathlib.Path | str | None = None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

same here

) -> Config:
"""Loads configuration from `.bc-linter.yml`.

If the file does not exist or cannot be parsed, returns defaults.
By default the configuration file is read from the repository root; a
custom directory may be supplied via ``config_dir``. If the file does not
exist or cannot be parsed, defaults are returned.
"""
cfg, _ = load_config_with_status(repo_root)
cfg, _ = load_config_with_status(repo_root, config_dir=config_dir)
return cfg


Expand Down
20 changes: 20 additions & 0 deletions tools/stronghold/tests/api/test_config_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,23 @@ def test_warn_on_unknown_top_level_keys(tmp_path: pathlib.Path, capsys) -> None:
assert status == "parsed"
out = capsys.readouterr().out
assert "::warning::BC-linter: Unknown keys in .bc-linter.yml: ['typpo']" in out


def test_load_config_from_custom_directory(tmp_path: pathlib.Path) -> None:
sub = tmp_path / "subdir"
sub.mkdir()
yml = textwrap.dedent(
"""
version: 1
include: ["src/**/*.py"]
"""
)
(sub / ".bc-linter.yml").write_text(yml)

cfg_rel, status_rel = load_config_with_status(tmp_path, config_dir="subdir")
assert status_rel == "parsed"
assert cfg_rel.include == ["src/**/*.py"]

cfg_abs, status_abs = load_config_with_status(tmp_path, config_dir=sub)
assert status_abs == "parsed"
assert cfg_abs.include == ["src/**/*.py"]