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
2 changes: 1 addition & 1 deletion .github/RELEASE_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ Performed Verification
- Build and test on QNX8 x86_64
- Thread sanitized unit test execution
- Address, undefined-behavior, and leak sanitized unit test execution
- Static analysis / linting (clang-tidy, clippy, ruff) via the Aspect CLI, with findings uploaded to GitHub Code Scanning
- Static analysis / linting (clang-tidy, clippy, ruff, buildifier) via the Aspect CLI, with findings uploaded to GitHub Code Scanning
- CodeQL MISRA C++ compliance analysis on both Linux and QNX (`--config=qnx`), merged into a single compliance report
- Code coverage report generation (llvm-cov) across the C++ test suite
- Documentation build and packaging (Sphinx) for the release version
Expand Down
53 changes: 51 additions & 2 deletions .github/workflows/_linter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ jobs:
- id: ruff
bazel-config: ruff
steps:
- uses: actions/checkout@v6.0.2
- uses: actions/checkout@v7.0.1
with:
fetch-depth: 0

Expand Down Expand Up @@ -156,11 +156,60 @@ jobs:

- name: Upload ${{ matrix.id }} findings as artifact
if: ${{ !cancelled() && inputs.fetch-only != 'true' }}
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7.0.1
with:
name: ${{ matrix.id }}-findings
path: |
${{ matrix.id }}_raw.log
${{ matrix.id }}.sarif
if-no-files-found: ignore
retention-days: 7

buildifier:
# Not part of the matrix above: buildifier is not run via a rules_lint aspect.
# //tools/lint:buildifier_json discovers all BUILD files with `find` (like
# buildifier_prebuilt's own rules), so no package needs any wiring.
# //tools/lint:buildifier_sarif runs it and converts the findings to SARIF.
name: buildifier
runs-on: ubuntu-24.04
permissions:
contents: read
actions: write
security-events: write
steps:
- uses: actions/checkout@v7.0.1
with:
fetch-depth: 0

- uses: ./.github/actions/00_infrastructure/prepare_bazel_environment
with:
disk-cache: ${{ inputs.fetch-only != 'true' && 'buildifier' || '' }}
cache-mode: ${{ inputs.cache-mode }}
ubuntu-snapshot-mirror-url: ${{ secrets.UBUNTU_SNAPSHOT_MIRROR_URL }}

- name: Fetch dependencies (--nobuild)
if: inputs.fetch-only == 'true'
run: bazel build --nobuild --config=ci //tools/lint:buildifier_sarif

- name: Run buildifier lint and convert findings to SARIF
if: inputs.fetch-only != 'true'
run: |
bazel run --config=ci --ui_event_filters=-info,-stdout --noshow_progress \
//tools/lint:buildifier_sarif > buildifier.sarif

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.

Write to file, @LittleHuba know what that means :-)


- name: Upload SARIF reports to GitHub Code Scanning
if: ${{ !cancelled() && inputs.fetch-only != 'true' }}
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: buildifier.sarif
category: buildifier
wait-for-processing: true

- name: Upload buildifier findings as artifact
if: ${{ !cancelled() && inputs.fetch-only != 'true' }}
uses: actions/upload-artifact@v7.0.1
with:
name: buildifier-findings
path: buildifier.sarif
if-no-files-found: ignore
retention-days: 7
17 changes: 16 additions & 1 deletion BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@
# *******************************************************************************

load("@aspect_rules_lint//format:defs.bzl", "format_multirun", "format_test")
load("@buildifier_prebuilt//:rules.bzl", "buildifier_test")
load("@rules_python//python:pip.bzl", "compile_pip_requirements")
load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
load("@rules_shell//shell:sh_binary.bzl", "sh_binary")
load("@score_tooling//cr_checker:cr_checker.bzl", "copyright_checker")
load("@score_tooling//skills_sync:sync_skills.bzl", "sync_skills")
load("//tools/lint:linters.bzl", "use_clang_tidy_targets", "use_ruff_targets")
load("//tools/lint:linters.bzl", "BUILDIFIER_EXCLUDE_PATTERNS", "use_buildifier_targets", "use_clang_tidy_targets", "use_ruff_targets")

exports_files(["MODULE.bazel"])

Expand Down Expand Up @@ -97,6 +98,8 @@ use_clang_tidy_targets()

use_ruff_targets()

use_buildifier_targets()

sh_binary(
name = "clang-tidy.fix",
srcs = [":clang-tidy.fix_script"],
Expand All @@ -120,3 +123,15 @@ sh_binary(
srcs = [":ruff.check_script"],
target_compatible_with = ["@platforms//os:linux"],
)

# Gating test: fails if buildifier reports any lint warning or formatting
# difference (runs as part of `bazel test //...`). Mirrors format_test (no_sandbox + workspace) to see all files.
buildifier_test(
name = "buildifier_test",
exclude_patterns = BUILDIFIER_EXCLUDE_PATTERNS,
lint_mode = "warn",
mode = "check",
no_sandbox = True,
tags = ["no-flaky-test-detection"],
workspace = "//:LICENSE",
)
4 changes: 4 additions & 0 deletions CI.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,10 @@ There are many linters for Python with no clear recommendation.
We chose ruff because it is fast, and it has a good set of default checks.
Runs the [ruff checks](./.ruff.toml) for the host.

#### Linting for Bazel files

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.

I think this can be removed because it should already be covered by https://github.com/eclipse-score/communication/blob/main/CI.md#formatting-and-linting-of-bazel-files


Runs [buildifier](https://github.com/bazelbuild/buildtools/tree/main/buildifier) lint warnings on all Starlark files. Findings are uploaded as SARIF to Code Scanning and any finding fails the job.

#### API checks

Make sure that the publicly visible targets are part of the public API specification.
Expand Down
29 changes: 28 additions & 1 deletion quality/quality.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Quality Tools

This document provides instructions for developers on how to execute the quality tools available in Score (Clang-Tidy, Ruff, CodeQL, Coverage, Sanitizers, Copyright Checker, and C++, Bazel Files Formatter) locally.
This document provides instructions for developers on how to execute the quality tools available in Score (Clang-Tidy, Ruff, Buildifier, CodeQL, Coverage, Sanitizers, Copyright Checker, and C++, Bazel Files Formatter) locally.

## Clang-Tidy

Expand Down Expand Up @@ -363,6 +363,33 @@ bazel run //:ruff.fix -- //docs/sphinx/utils/...
> **Note:** Violations without an automatic fix are still reported in the terminal but leave no
> patch file, same as Clang-Tidy.

### Buildifier

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.


Buildifier lints the `BUILD`, `.bzl`, `.bazel` and `.BUILD` files and is used like the other linters
(`//:buildifier.check` / `//:buildifier.fix`, optionally with Bazel target patterns). Files are discovered
per package directory, so no package needs any buildifier-specific configuration.

```bash
# Check all Starlark files (prints findings, fails if there are any)
bazel run //:buildifier.check

# Check / fix only some packages
bazel run //:buildifier.check -- //score/mw/com/...
bazel run //:buildifier.fix -- //score/mw/com/...
bazel run //:buildifier.check -- //... -//third_party/...

# Gating test (also part of `bazel test //...`)
bazel test //:buildifier_test

# Print the findings as SARIF (used by CI for Code Scanning)
bazel run //tools/lint:buildifier_sarif
```

> **Note:** Unlike the other linters, buildifier is not run via a `rules_lint` aspect (it has no good way to
> include BUILD files), so there is no `--config=buildifier` and no hold-the-line. Only
> `//pkg`, `//pkg:target` and `//pkg/...` patterns and their `-` exclusions are supported.
> Formatting is additionally checked by the formatter below.

### C++ and Bazel Files Formatter

```bash
Expand Down
44 changes: 44 additions & 0 deletions tools/lint/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@
# SPDX-License-Identifier: Apache-2.0
# *******************************************************************************

load("@buildifier_prebuilt//:rules.bzl", "buildifier")
load("@rules_python//python:defs.bzl", "py_binary", "py_test")
load("@rules_shell//shell:sh_binary.bzl", "sh_binary")
load("//bazel/rules:expand_template.bzl", "expand_template")
load(":linters.bzl", "BUILDIFIER_EXCLUDE_PATTERNS")

# Makes tools/lint/ a Bazel package so linters.bzl can be referenced as a
# label target (e.g. //tools/lint:linters.bzl%clang_tidy) in bazelrc.
Expand Down Expand Up @@ -65,3 +68,44 @@ sh_binary(
visibility = ["//:__pkg__"],
deps = ["@bazel_tools//tools/bash/runfiles"],
)

buildifier(
name = "buildifier_json",
exclude_patterns = BUILDIFIER_EXCLUDE_PATTERNS,
format = "json",
lint_mode = "warn",
mode = "check",
tags = ["manual"],
)

py_binary(
name = "buildifier_sarif",
srcs = ["buildifier_to_sarif.py"],
args = ["$(rootpath :buildifier_json)"],
data = [":buildifier_json"],
main = "buildifier_to_sarif.py",
tags = ["manual"],
)

exports_files([
"buildifier.py",
"buildifier_files.py",
])

py_test(
name = "buildifier_files_test",
srcs = [
"buildifier_files.py",
"buildifier_files_test.py",
],
main = "buildifier_files_test.py",
)

py_test(
name = "buildifier_to_sarif_test",
srcs = [
"buildifier_to_sarif.py",
"buildifier_to_sarif_test.py",
],
main = "buildifier_to_sarif_test.py",
)
64 changes: 64 additions & 0 deletions tools/lint/buildifier.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# *******************************************************************************
# Copyright (c) 2026 Contributors to the Eclipse Foundation
#
# See the NOTICE file(s) distributed with this work for additional
# information regarding copyright ownership.
#
# This program and the accompanying materials are made available under the
# terms of the Apache License Version 2.0 which is available at
# https://www.apache.org/licenses/LICENSE-2.0
#
# SPDX-License-Identifier: Apache-2.0
# *******************************************************************************

"""Developer entry point for buildifier: `bazel run //:buildifier.check|fix -- [patterns]`.

Resolves the Bazel-style package patterns (default `//...`) to files and runs
the buildifier binary on them. `check` prints the findings and fails if there
are any, `fix` applies all auto-fixable findings and formatting.
"""

import argparse
import os
import subprocess
import sys
from pathlib import Path

from tools.lint.buildifier_files import resolve_files

_MODES = {
"check": ["-mode=check", "-lint=warn"],
"fix": ["-mode=fix", "-lint=fix"],
}


def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--mode", choices=sorted(_MODES), required=True)
parser.add_argument("--buildifier", required=True, help="Path of the buildifier binary.")
parser.add_argument("patterns", nargs="*")
# argparse would take `-//pkg/...` exclusions for options.
args, extra = parser.parse_known_args()
patterns = args.patterns + extra
if any(not p.lstrip("-").startswith("//") for p in extra):
parser.error(f"unrecognized arguments: {' '.join(extra)}")
if not patterns:
patterns = ["//..."]

workspace = Path(os.environ["BUILD_WORKSPACE_DIRECTORY"])
print(f"=== buildifier {args.mode}: {' '.join(patterns)} ===", flush=True)
try:
files = resolve_files(workspace, patterns)
except ValueError as error:
print(error, file=sys.stderr)
return 1

command = [os.path.abspath(args.buildifier), *_MODES[args.mode], *map(str, files)]
exit_code = subprocess.run(command, cwd=workspace).returncode
if args.mode == "fix" and exit_code == 0:
print("Review with: git diff")
return exit_code


if __name__ == "__main__":
sys.exit(main())
77 changes: 77 additions & 0 deletions tools/lint/buildifier_files.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# *******************************************************************************
# Copyright (c) 2026 Contributors to the Eclipse Foundation
#
# See the NOTICE file(s) distributed with this work for additional
# information regarding copyright ownership.
#
# This program and the accompanying materials are made available under the
# terms of the Apache License Version 2.0 which is available at
# https://www.apache.org/licenses/LICENSE-2.0
#
# SPDX-License-Identifier: Apache-2.0
# *******************************************************************************

"""Resolves Bazel-style package patterns to the Starlark files buildifier checks.

Supported patterns: `//pkg`, `//pkg:target` and `//pkg/...`, each optionally
prefixed with `-` to exclude the files it resolves to.
"""

from pathlib import Path

_SUFFIXES = (".bzl", ".bazel", ".sky", ".BUILD")


def _is_starlark_file(path: Path) -> bool:
return path.name == "BUILD" or path.name.endswith(_SUFFIXES)


def _is_skipped_dir(path: Path) -> bool:
# Hidden directories (.git, IDE state) hold no sources, and symlinked
# directories (the bazel-* output links) lead out of the source tree.
return path.name.startswith(".") or path.is_symlink()


def _resolve(workspace: Path, pattern: str) -> set[Path]:
if not pattern.startswith("//"):
raise ValueError(f"Unsupported pattern (expected [-]//pkg[:target] or [-]//pkg/...): {pattern}")
package = pattern[2:].split(":", 1)[0]
recursive = package == "..." or package.endswith("/...")
if recursive:
package = package[: -len("...")].rstrip("/")
root = workspace / package
if not root.is_dir():
raise ValueError(f"No such package directory: {package or '.'}")

if not recursive:
return {p for p in root.iterdir() if p.is_file() and _is_starlark_file(p)}

files = set()
stack = [root]
while stack:
for entry in stack.pop().iterdir():
if entry.is_dir():
if not _is_skipped_dir(entry):
stack.append(entry)
elif _is_starlark_file(entry):
files.add(entry)
return files


def resolve_files(workspace: Path, patterns: list[str]) -> list[Path]:
"""Returns the sorted Starlark files selected by `patterns`.

Raises ValueError for unsupported patterns, unknown directories or an
empty selection.
"""
included: set[Path] = set()
excluded: set[Path] = set()
for pattern in patterns:
if pattern.startswith("-"):
excluded |= _resolve(workspace, pattern[1:])
else:
included |= _resolve(workspace, pattern)
selected = sorted(included - excluded)
if not selected:
raise ValueError(f"No Starlark files found for: {' '.join(patterns)}")
return selected
Loading
Loading