Skip to content
Merged

Linting #9734

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
61 changes: 61 additions & 0 deletions BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ load("@rules_cc//cc:cc_library.bzl", "cc_library")
load("@rules_pkg//pkg:tar.bzl", "pkg_tar")
load("@rules_python//python:defs.bzl", "py_library")
load("@rules_shell//shell:sh_binary.bzl", "sh_binary")
load("@rules_shell//shell:sh_test.bzl", "sh_test")
load("//bazel:notification.bzl", "notification_rule")
load("//bazel:python_wrap_cc.bzl", "PYTHON_EXTENSION_LINKOPTS", "PYTHON_STABLE_API_DEFINE", "python_wrap_cc")
load("//bazel:tcl_encode_or.bzl", "tcl_encode")
Expand Down Expand Up @@ -430,3 +431,63 @@ pkg_tar(
include_runfiles = True,
package_file_name = "openroad.tar",
)

# ---------------------------------------------------------------------------
# Lint & tidy targets
#
# bazelisk test //:lint_test — run all linters (currently: tclint + tclfmt)
# bazelisk run //:fix_lint — auto-fix what can be fixed (currently: tclfmt)
# ---------------------------------------------------------------------------

# --- TCL linting (tclint) -------------------------------------------------

sh_test(
name = "lint_tcl_test",
srcs = ["//bazel:tcl_lint_test.sh"],
args = ["$(rootpath //bazel:tclint)"],
data = [
"tclint.toml",
"//bazel:tclint",
],
tags = ["local"],
)

# --- TCL formatting check (tclfmt --check) --------------------------------

sh_test(
name = "fmt_tcl_test",
srcs = ["//bazel:tcl_fmt_test.sh"],
args = ["$(rootpath //bazel:tclfmt)"],
data = [
"tclint.toml",
"//bazel:tclfmt",
],
tags = ["local"],
)

# --- TCL auto-format (tclfmt --in-place) ----------------------------------

sh_binary(
name = "tidy_tcl",
srcs = ["//bazel:tcl_tidy.sh"],
args = ["$(rootpath //bazel:tclfmt)"],
data = [
"tclint.toml",
"//bazel:tclfmt",
],
)

# --- Umbrella targets ------------------------------------------------------

test_suite(
name = "lint_test",
tests = [
":fmt_tcl_test",
":lint_tcl_test",
],
)

alias(
name = "fix_lint",
actual = ":tidy_tcl",
)
21 changes: 20 additions & 1 deletion bazel/BUILD
Original file line number Diff line number Diff line change
@@ -1,11 +1,30 @@
load("@bazel_skylib//rules:build_test.bzl", "build_test")
load("@rules_cc//cc:cc_library.bzl", "cc_library")
load("@rules_python//python:pip.bzl", "compile_pip_requirements")
load("@rules_python//python/entry_points:py_console_script_binary.bzl", "py_console_script_binary")

package(features = ["layering_check"])

exports_files(
["install.sh"],
[
"install.sh",
"tcl_lint_test.sh",
"tcl_fmt_test.sh",
"tcl_tidy.sh",
],
visibility = ["//visibility:public"],
)

# tclint / tclfmt binaries from pip — version pinned in requirements.in
py_console_script_binary(
name = "tclint",
pkg = "@openroad-pip//tclint",
visibility = ["//visibility:public"],
)

py_console_script_binary(
name = "tclfmt",
pkg = "@openroad-pip//tclint",
visibility = ["//visibility:public"],
)

Expand Down
1 change: 1 addition & 0 deletions bazel/requirements.in
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
tclint==0.7.0
48 changes: 48 additions & 0 deletions bazel/requirements_lock_3_13.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,51 @@
#
# bazel run //bazel:requirements.update
#
attrs==25.4.0 \
--hash=sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11 \
--hash=sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373
# via
# cattrs
# lsprotocol
cattrs==26.1.0 \
--hash=sha256:d1e0804c42639494d469d08d4f26d6b9de9b8ab26b446db7b5f8c2e97f7c3096 \
--hash=sha256:fa239e0f0ec0715ba34852ce813986dfed1e12117e209b816ab87401271cdd40
# via
# lsprotocol
# pygls
importlib-metadata==6.8.0 \
--hash=sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb \
--hash=sha256:dbace7892d8c0c4ac1ad096662232f831d4e64f4c4545bd53016a3e9d4654743
# via tclint
lsprotocol==2023.0.1 \
--hash=sha256:c75223c9e4af2f24272b14c6375787438279369236cd568f596d4951052a60f2 \
--hash=sha256:cc5c15130d2403c18b734304339e51242d3018a05c4f7d0f198ad6e0cd21861d
# via pygls
pathspec==0.11.2 \
--hash=sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20 \
--hash=sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3
# via tclint
ply==3.11 \
--hash=sha256:00c7c1aaa88358b9c765b6d3000c6eec0ba42abca5351b095321aef446081da3 \
--hash=sha256:096f9b8350b65ebd2fd1346b12452efe5b9607f7482813ffca50c22722a807ce
# via tclint
pygls==1.3.1 \
--hash=sha256:140edceefa0da0e9b3c533547c892a42a7d2fd9217ae848c330c53d266a55018 \
--hash=sha256:6e00f11efc56321bdeb6eac04f6d86131f654c7d49124344a9ebb968da3dd91e
# via tclint
tclint==0.7.0 \
--hash=sha256:58b54bf333a96ef4b4eac3bde23da997a64a4414a4cdec8e5e0a9fbafb6dcd25 \
--hash=sha256:bd605b11d44708e1537b902e63d7dd1d05f2d85c2c99a36854b157606eac1e8a
# via -r bazel/requirements.in
typing-extensions==4.15.0 \
--hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \
--hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548
# via cattrs
voluptuous==0.15.2 \
--hash=sha256:016348bc7788a9af9520b1764ebd4de0df41fe2138ebe9e06fa036bf86a65566 \
--hash=sha256:6ffcab32c4d3230b4d2af3a577c87e1908a714a11f6f95570456b1849b0279aa
# via tclint
zipp==3.23.0 \
--hash=sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e \
--hash=sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166
# via importlib-metadata
10 changes: 10 additions & 0 deletions bazel/tcl_fmt_test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: BSD-3-Clause
# Copyright (c) 2025-2025, The OpenROAD Authors
#
# Check that all TCL files are properly formatted.
set -euo pipefail
TOOL="$(readlink -f "$1")"
WORKSPACE="$(dirname "$(readlink -f tclint.toml)")"
cd "$WORKSPACE"
git ls-files '*.tcl' '*.sdc' '*.upf' -z | xargs -0 "$TOOL" --check
10 changes: 10 additions & 0 deletions bazel/tcl_lint_test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: BSD-3-Clause
# Copyright (c) 2025-2025, The OpenROAD Authors
#
# Lint all TCL files using tclint.
set -euo pipefail
TOOL="$(readlink -f "$1")"
WORKSPACE="$(dirname "$(readlink -f tclint.toml)")"
cd "$WORKSPACE"
git ls-files '*.tcl' '*.sdc' '*.upf' -z | xargs -0 "$TOOL"
9 changes: 9 additions & 0 deletions bazel/tcl_tidy.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: BSD-3-Clause
# Copyright (c) 2025-2025, The OpenROAD Authors
#
# Auto-format all TCL files in-place.
set -euo pipefail
TOOL="$(cd "$(dirname "$1")" && pwd)/$(basename "$1")"
cd "${BUILD_WORKSPACE_DIRECTORY:-$PWD}"
git ls-files '*.tcl' -z | xargs -0 "$TOOL" --in-place
85 changes: 85 additions & 0 deletions docs/contrib/LintTargets.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Bazel Lint Targets

Bazel lint targets provide a single entry point for running linters and
auto-formatters locally, with all tool versions managed by Bazel — no manual
installs needed.

## Why Bazel for linting?

Managing linter dependencies is a surprisingly painful part of development.
Each tool has its own version, its own install method (`pip`, `npm`, `go
install`, distro packages), and its own set of transitive dependencies that
can conflict with each other or with the project. CI scripts paper over this
with ad-hoc `pip install` steps pinned in YAML files that drift out of sync,
break silently, and can't be tested locally without replicating the exact CI
environment.

Bazel solves this by treating linter tools as hermetic dependencies — the same
way it treats compilers and libraries. Tool versions are pinned in
`bazel/requirements.in` (for pip packages) or `MODULE.bazel` (for Bazel
modules), lock files ensure reproducibility, and `bazelisk test //:lint_test`
works identically on every developer's machine and in CI. No virtualenvs, no
"works on my machine", no guessing which version of `tclint` CI is running.

## Quick start

```bash
# Run all lint checks (~0.3s, no C++ build)
bazelisk test //:lint_test

# Auto-fix formatting
bazelisk run //:fix_lint
```

## Available targets

| Target | Type | What it does |
|--------|------|-------------|
| `//:lint_test` | `test_suite` | Umbrella: runs all lint checks |
| `//:fix_lint` | `alias` | Umbrella: auto-fixes what can be fixed |
| `//:lint_tcl_test` | `sh_test` | Runs `tclint .` (lint rules) |
| `//:fmt_tcl_test` | `sh_test` | Runs `tclfmt --check .` (formatting) |
| `//:tidy_tcl` | `sh_binary` | Runs `tclfmt --in-place .` (auto-format) |

## Configuration

TCL linting and formatting are controlled by `tclint.toml` at the repository
root. See the [tclint documentation](https://tclint.readthedocs.io/) for
available options.


## Adding new linters

The umbrella targets (`//:lint_test` and `//:fix_lint`) are designed for extension.
To add a new linter (e.g., C++ tidy):

1. Add the tool dependency to `bazel/requirements.in` (pip) or as a Bazel
module dependency
2. Create a wrapper script in `bazel/` (e.g., `bazel/cpp_tidy.sh`)
3. Add `sh_test` / `sh_binary` targets in `BUILD.bazel`
4. Add the new test to the `//:lint_test` `test_suite`

## Planned additions

The following linters and formatters are planned for `//:lint_test` and
`//:fix_lint`, replacing their ad-hoc CI equivalents:

- **C++ clang-tidy** — static analysis for C++ sources
- **C++ clang-format** — formatting check/fix for C++ and header files
- **Python ruff** — lint + format for Python scripts in `etc/`, `docs/`, tests
- **Buildifier** — lint + format for BUILD, .bzl, and MODULE.bazel files
- **ShellCheck** — lint for bash scripts in `test/`, `bazel/`, `etc/`
- **Duplicate message ID check** — replace Jenkins "Find Duplicated Message IDs" stage
- **Doc consistency checks** — replace Jenkins "Documentation Checks" stage
(`man_tcl_check`, `readme_msgs_check`)

The goal is to phase out hand-coded GitHub Actions YAML and Jenkins scripts in
favor of Bazel-managed targets that are reproducible, version-pinned, and
testable locally with a single command.

## Relationship to CI

The GitHub Actions workflow (`.github/workflows/github-actions-lint-tcl.yml`)
runs the same `tclint` and `tclfmt` checks. Once Bazel lint targets are
validated in CI, the GitHub Actions workflow can be retired. The same applies
to the Jenkins documentation and duplicate ID check stages.
3 changes: 2 additions & 1 deletion tclint.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
exclude = [
exclude = [
"src/sta/",
"tmp/",
]

ignore = [
Expand Down
Loading