Repository navigation
Expand file tree
/
Copy pathMakefile
More file actions
2301 lines (2063 loc) · 114 KB
/
Copy pathMakefile
File metadata and controls
2301 lines (2063 loc) · 114 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
MAKE_MAJOR_VER := $(shell echo $(MAKE_VERSION) | cut -d'.' -f1)
ifneq ($(shell test $(MAKE_MAJOR_VER) -gt 3; echo $$?),0)
$(error Make version $(MAKE_VERSION) is not supported, please install GNU Make 4.x)
endif
# Strict shell and Make settings for robust recipes
SHELL := bash
.SHELLFLAGS := -eu -o pipefail -c
.DELETE_ON_ERROR:
MAKEFLAGS += --warn-undefined-variables
MAKEFLAGS += --no-builtin-rules
.DEFAULT_GOAL := help
# Directory path of Makefile
BASE_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
# Path to the Python bindings crate. Absolute path so the recipes
# below resolve correctly even when Make is invoked from a nested
# directory. BASE_DIR ends in a trailing slash (from $(dir ...))
# which absorbs into the concatenation; BCA_PY_DIR has no trailing
# slash and that's intended (matches how cargo / maturin invocations
# read it).
BCA_PY_DIR := $(BASE_DIR)big-code-analysis-py
# The release/wheel smoke harness lives outside the bindings package, so the
# package-scoped py-* gates below pull it in explicitly (#995). Linting it
# with the bindings' ruff/mypy config — rather than letting it drift outside
# every per-PR gate — is the whole point of extracting it from workflow YAML
# (lesson #80). `--config` forces the bindings config since there is no
# config to discover from scripts/smoke upward.
SMOKE_PY_DIR := $(BASE_DIR)scripts/smoke
SMOKE_LIB_PY := $(SMOKE_PY_DIR)/lib_wheel_smoke.py
# Directories excluded from linting and file-search operations.
# `tests/repositories` holds vendored fixtures (incl. the
# big-code-analysis-output submodule); `tree-sitter-*` are vendored
# grammar crates that follow upstream conventions; `enums/` is excluded
# from the workspace and owns its own files. None of these may be
# reformatted by this project's tooling. Glob entries (e.g. `tree-sitter-*`)
# are quoted at the use site below — keep this list plain.
EXCLUDE_DIRS := .claude .git target tests/repositories \
big-code-analysis-book/book \
tree-sitter-* enums
# File finder: prefer fd/fdfind (fast, .gitignore-aware), fall back to find
FD := $(shell command -v fdfind 2>/dev/null || command -v fd 2>/dev/null)
# Precomputed exclusion flags for fd and find. Single-quote each entry so
# the recipe shell does not glob-expand patterns like `tree-sitter-*` into
# absolute paths before fd/find see them (see #160). `find -path` uses its
# own glob engine and accepts unquoted patterns the same way fd does.
FD_EXCLUDE := $(foreach dir,$(EXCLUDE_DIRS),--exclude '$(dir)')
FIND_EXCLUDE := $(foreach dir,$(EXCLUDE_DIRS),! -path './$(dir)/*')
# Find files by extension with fd (preferred) or find (fallback).
# Usage: $(call find-by-ext,EXTENSION,EXTRA_FD_ARGS). Always pass the
# second arg (empty if unused) to avoid --warn-undefined-variables
# warnings on `$(2)`, e.g. $(call find-by-ext,md,).
find-by-ext = $(if $(FD),$(FD) --extension $(1) $(FD_EXCLUDE) $(2),find . -name "*.$(1)" -type f $(FIND_EXCLUDE))
# Test runner: prefer cargo-nextest, fall back to `cargo test` (#1120).
# `cargo test` runs the workspace's 72 test binaries strictly one after
# another — parallelism exists only *within* a binary, so the ten root
# integration binaries that hold a single `#[test]` each pin the whole
# machine to one core for their duration. nextest schedules every
# binary's tests into one global pool instead. CI already runs nextest
# (the `test` job in .github/workflows/ci.yml); this is the local half.
#
# The two paths execute an identical target set: nextest's default
# selection is exactly `--lib --bins --tests`, verified by diffing
# `cargo nextest list --workspace --all-features` against the same
# invocation with those flags spelled out (4,731 tests, plus the 10
# `#[ignore]`d ones both runners skip).
#
# Expect a smaller wall-clock win than the scheduling change implies:
# one test (`cfg_predicate::tests::rust_attr_test_handles_deeply_nested_
# cfg_without_overflow`, the O(len^2) blowup tracked in #1105) runs for
# ~78s and is now the critical path. The other 4,730 finish in ~17s
# together, so fixing #1105 is what converts this into a ~4x win.
#
# Held in a variable rather than probed inline with `command -v`, and used
# as the command itself, so it overrides the same way `FD` above does:
# `make test NEXTEST=` exercises the fallback without uninstalling
# anything, and `make test NEXTEST=/path/to/cargo-nextest` picks a
# specific build. A cargo subcommand binary takes its own name as the
# first argument, hence `$(NEXTEST) nextest run`.
NEXTEST := $(shell command -v cargo-nextest 2>/dev/null)
TEST_CMD = $(if $(NEXTEST),$(NEXTEST) nextest run --workspace --all-features,cargo test --workspace --all-features --lib --bins --tests)
.PHONY: help check-tools worktree-setup worktree-setup-test build build-release check test test-doc chain-audit fmt fmt-check markdown-fmt markdown-lint shellcheck sh-fmt sh-fmt-check toml-fmt toml-fmt-check toml-lint makefile-check actionlint snapshot-anchors snapshot-anchors-test rustfmt-bail rustfmt-bail-test grammar-marker-sync grammar-marker-sync-test check-versions check-excluded-manifests check-excluded-manifests-test check-ruff-lockstep check-ruff-lockstep-test check-publish-metadata check-publish-metadata-test check-manpage-assets check-manpage-drift-test check-diagnostic-prefix check-diagnostic-prefix-test check-feature-gates check-feature-gates-test check-test-lang-gates check-test-lang-gates-compare check-test-lang-gates-test check-safety-doc-pin check-safety-doc-pin-test gate-status-test check-tools-test enums-check enums-codegen-drift enums-codegen-drift-test self-scan self-scan-headroom self-scan-write-baseline self-scan-write-baseline-headroom vcs lint clippy udeps insta-review insta-accept clean distclean install install-cli install-web doc doc-open doc-check doc-check-docsrs book book-serve book-pot book-po-update book-ja book-deploy all pre-commit ci release-check verify-changelog pkg-deb-local pkg-rpm-local dev-env-build dev-env-run dev-env-shell dev-env-rm py-bootstrap py-sync py-relock py-clean py-fmt py-fmt-check py-lint py-typecheck py-test py-stubtest smoke smoke-cli smoke-lib bench bench-scaling bench-walk fuzz-check fuzz-smoke fuzz-replay fuzz-run fuzz-tmin _check-find _pc-all _pc-fmt _pc-clippy _pc-test _pc-doc-check _pc-udeps _pc-shellcheck _pc-markdown-lint _pc-toml-lint _pc-makefile-check _pc-actionlint _pc-snapshot-anchors _pc-snapshot-anchors-test _pc-rustfmt-bail _pc-rustfmt-bail-test _pc-grammar-marker-sync _pc-grammar-marker-sync-test _pc-check-versions _pc-check-versions-test _pc-check-grammar-crate-test _pc-check-excluded-manifests _pc-check-excluded-manifests-test _pc-check-ruff-lockstep _pc-check-ruff-lockstep-test _pc-check-publish-metadata _pc-check-publish-metadata-test _pc-check-manpage-assets _pc-check-manpage-drift-test _pc-check-diagnostic-prefix _pc-check-diagnostic-prefix-test _pc-check-feature-gates-test _pc-check-test-lang-gates _pc-check-test-lang-gates-test _pc-check-safety-doc-pin _pc-check-safety-doc-pin-test _pc-worktree-setup-test _pc-gate-status-test _pc-check-tools-test _pc-enums-check _pc-enums-codegen-drift _pc-enums-codegen-drift-test _pc-self-scan _pc-self-scan-headroom _pc-py-fmt _pc-py-typecheck _pc-py-test _pc-py-stubtest _ci-all _ci-fmt-check _ci-clippy _ci-test _ci-doc-check _ci-build _ci-udeps _ci-shellcheck _ci-markdown-lint _ci-toml-lint _ci-makefile-check _ci-actionlint _ci-snapshot-anchors _ci-snapshot-anchors-test _ci-rustfmt-bail _ci-rustfmt-bail-test _ci-grammar-marker-sync _ci-grammar-marker-sync-test _ci-check-versions _ci-check-versions-test _ci-check-grammar-crate-test _ci-check-excluded-manifests _ci-check-excluded-manifests-test _ci-check-ruff-lockstep _ci-check-ruff-lockstep-test _ci-check-publish-metadata _ci-check-publish-metadata-test _ci-check-manpage-assets _ci-check-manpage-drift-test _ci-check-diagnostic-prefix _ci-check-diagnostic-prefix-test _ci-check-feature-gates-test _ci-check-test-lang-gates _ci-check-test-lang-gates-test _ci-check-safety-doc-pin _ci-check-safety-doc-pin-test _ci-worktree-setup-test _ci-gate-status-test _ci-check-tools-test _ci-enums-check _ci-enums-codegen-drift _ci-enums-codegen-drift-test _ci-enums-codegen-drift-test _ci-self-scan _ci-self-scan-headroom _ci-cargo-pipeline _ci-py-fmt-check _ci-py-lint _ci-py-typecheck _ci-py-test _ci-py-stubtest
# Default target
help:
@echo "Build and test commands for big-code-analysis"
@echo ""
@echo "Usage: make <target>"
@echo ""
@echo "Prerequisites:"
@echo " check-tools Verify required tools are present"
@echo " worktree-setup Bootstrap a fresh clone/worktree: corpora + Python venv"
@echo ""
@echo "Build targets:"
@echo " build Build debug binaries"
@echo " build-release Build optimized release binaries"
@echo " check Run cargo check"
@echo ""
@echo "Test targets:"
@echo " test Run unit and integration tests (nextest if present)"
@echo " test-doc Run cargo doc tests"
@echo " chain-audit Re-run the suite with the exact ancestor-chain assertion (out-of-band)"
@echo " insta-review Review pending insta snapshot diffs"
@echo " insta-accept Accept all pending insta snapshots"
@echo ""
@echo "Code quality:"
@echo " fmt Format Rust + Markdown + TOML + Bash"
@echo " fmt-check Verify formatting without modifying files"
@echo " clippy Run clippy with -D warnings"
@echo " udeps Detect unused deps (requires nightly)"
@echo " markdown-fmt Auto-fix Markdown with rumdl"
@echo " markdown-lint Lint Markdown with rumdl"
@echo " shellcheck Lint bash scripts"
@echo " sh-fmt Format bash scripts with shfmt"
@echo " sh-fmt-check Check bash formatting without modifying"
@echo " toml-fmt Format TOML files with taplo"
@echo " toml-fmt-check Check TOML formatting without modifying"
@echo " toml-lint Lint TOML files with taplo"
@echo " makefile-check Lint Makefile with checkmake"
@echo " actionlint Lint GitHub Actions workflows with actionlint"
@echo " snapshot-anchors Block new bare insta snapshots"
@echo " snapshot-anchors-test Self-tests for the snapshot-anchors gate"
@echo " rustfmt-bail Block new match arms rustfmt refuses to format"
@echo " rustfmt-bail-test Self-tests for the rustfmt-bail gate"
@echo " grammar-marker-sync Block grammar-marker bumps without source regen"
@echo " grammar-marker-sync-test Self-tests for the grammar-marker-sync gate"
@echo " check-versions Enforce lockstep version invariant across owned crates"
@echo " check-versions-test Self-tests for the check-versions gate"
@echo " check-grammar-crate-test Sync-test EXTENSIONS table vs langs.rs"
@echo " check-excluded-manifests Assert excluded crates root a workspace, declare lints, and =-pin grammars"
@echo " check-excluded-manifests-test Self-tests for the check-excluded-manifests gate"
@echo " check-ruff-lockstep Assert the ruff-pre-commit rev, uv.lock, and the requirements export agree"
@echo " check-ruff-lockstep-test Self-tests for the check-ruff-lockstep gate"
@echo " check-publish-metadata Assert every publishable crate carries the metadata crates.io needs"
@echo " check-publish-metadata-test Self-tests for the check-publish-metadata gate"
@echo " check-manpage-assets Assert every bca-*.1 man page is in deb+rpm asset lists"
@echo " check-manpage-drift-test Self-tests for the man-page drift gate"
@echo " check-diagnostic-prefix Block capitalised Warning:/Error:/Note: literals"
@echo " check-diagnostic-prefix-test Self-tests for the diagnostic-prefix gate"
@echo " check-feature-gates Assert union-gated tests are absent under a disjoint feature set"
@echo " check-feature-gates-test Self-tests for the feature-gates gate"
@echo " check-test-lang-gates Assert every test naming a language carries its cfg"
@echo " check-test-lang-gates-compare Fail if a test stopped being built by a language leg since COMPARE_REF"
@echo " check-test-lang-gates-test Self-tests for the test-language gate"
@echo " check-safety-doc-pin Assert node.rs's unsafe soundness doc cites the live tree-sitter pin"
@echo " check-safety-doc-pin-test Self-tests for the safety-doc-pin gate"
@echo " worktree-setup-test Self-tests for the worktree-setup submodule classifier"
@echo " gate-status-test Self-tests for the pre-commit/ci BCA_GATE verdict line"
@echo " check-tools-test Self-tests for the check-tools tool probes and hints"
@echo " enums-check cargo clippy + cargo test on workspace-excluded enums crate"
@echo " enums-codegen-drift Block enums codegen output drifting from checked-in files"
@echo " enums-codegen-drift-test Self-tests for the enums-codegen-drift gate"
@echo " self-scan bca threshold gate against this repo (hard: 100%)"
@echo " self-scan-headroom bca threshold gate (soft: BCA_HEADROOM, default 0.95)"
@echo " self-scan-write-baseline Refresh .bca-baseline.toml at the hard thresholds"
@echo " self-scan-write-baseline-headroom Refresh .bca-baseline.toml at the soft thresholds"
@echo " vcs Change-history (VCS) risk report for this repo"
@echo " bench bench-scaling + bench-walk (out-of-band; never in ci)"
@echo " bench-scaling Complexity-class gate for the metric walk"
@echo " bench-walk Criterion metric-walk benchmarks over a corpus slice"
@echo " fuzz-check Lint, test + build the cargo-fuzz targets (out-of-band)"
@echo " fuzz-smoke Short bounded fuzz run over the committed corpora"
@echo " fuzz-replay Replay the committed seeds only (regression check)"
@echo " fuzz-run One target, FUZZ_RUNS iterations (FUZZ_TARGET=name)"
@echo " fuzz-tmin Minimise a crash artifact (FUZZ_TARGET, FUZZ_INPUT)"
@echo " lint Run all linters"
@echo ""
@echo "Python bindings (big-code-analysis-py):"
@echo " py-bootstrap Create .venv from uv.lock (alias: py-sync) — requires uv"
@echo " py-relock Regenerate uv.lock from pyproject.toml (run after editing deps)"
@echo " py-clean Remove .venv, _native*.so, *_cache, __pycache__"
@echo " py-fmt Format Python sources with ruff"
@echo " py-fmt-check Verify Python formatting"
@echo " py-lint Lint Python sources with ruff"
@echo " py-typecheck Type-check with mypy --strict + pyright"
@echo " py-test maturin develop + pytest (needs active venv)"
@echo " py-stubtest maturin develop + mypy stubtest of _native.pyi (needs venv)"
@echo " smoke Run the release/wheel smoke scripts locally (smoke-cli + smoke-lib)"
@echo " smoke-cli CLI smoke (scripts/smoke/cli_wheel_smoke.sh) against a debug bca"
@echo " smoke-lib Library smoke (scripts/smoke/lib_wheel_smoke.py) via maturin develop"
@echo " (first-time setup: 'make worktree-setup' — corpora + this venv; 'make py-bootstrap' for the venv alone)"
@echo ""
@echo "Maintenance:"
@echo " clean Remove cargo build artifacts (target/) only"
@echo " distclean Remove cargo + Python artifacts (full wipe — chains py-clean and clean)"
@echo " install Install both CLI and web binaries"
@echo " install-cli Install bca"
@echo " install-web Install bca-web"
@echo ""
@echo "Documentation:"
@echo " doc Generate rustdoc (warning-tolerant viewer)"
@echo " doc-open Generate and open rustdoc (warning-tolerant viewer)"
@echo " doc-check Strict rustdoc gate (RUSTDOCFLAGS appends -D warnings)"
@echo " doc-check-docsrs Reproduce the docs.rs build (nightly, --cfg docsrs); skips if nightly absent"
@echo " book Build the mdBook"
@echo " book-serve Serve the mdBook with live reload"
@echo " book-pot Regenerate the translation template (po/messages.pot)"
@echo " book-po-update Merge English doc changes into po/ja.po (msgmerge)"
@echo " book-ja Build the Japanese mdBook into book/ja"
@echo " book-deploy Publish the mdBook to the gh-pages branch (manual fallback)"
@echo ""
@echo "Combined targets:"
@echo " all Check, test, build release"
@echo " pre-commit Verify formatting, lint, test (recommended before commit)"
@echo " ci Validate formatting, lint, test (no auto-fix)"
@echo ""
@echo "Release engineering:"
@echo " release-check Pre-tag gate: deny + about + CHANGELOG (VERSION=x.y.z)"
@echo " verify-changelog Verify CHANGELOG.md has section for VERSION=x.y.z"
@echo " pkg-deb-local Build .deb locally (host target, no CI matrix)"
@echo " pkg-rpm-local Build .rpm locally (host target, no CI matrix)"
@echo ""
@echo "Dev container (Claude Code + MCP servers):"
@echo " dev-env-build Build the dev image (Rust/Python/Node + MCP servers)"
@echo " dev-env-run Start the dev container (detached, repo mounted)"
@echo " dev-env-shell Open a shell in the running dev container"
@echo " dev-env-rm Stop and remove the dev container"
# ---------------------------------------------------------------------------
# Prerequisites
# ---------------------------------------------------------------------------
check-tools:
@bash $(BASE_DIR)utils/check-tools.sh
# Self-tests for utils/check-tools.sh. The install hints are the whole
# product of that script, and one of them — cargo-about's — is only
# correct with `--features cli`; without the flag the install exits 0
# and installs nothing (#1226). Cheap enough (~0.1s, stubbed cargo) to
# run in both gates.
check-tools-test:
@bash $(BASE_DIR)utils/check-tools-test.sh
# One-shot bootstrap for a fresh clone or a fresh `git worktree`: the
# two steps nothing else states and nothing else detects (#1171).
#
# 1. The integration corpora under tests/repositories/. Without them 24
# tests fail — 5 corpus tests and 19 CLI tests that analyse a real
# source file from the DeepSpeech corpus.
# 2. big-code-analysis-py/.venv. Without it `make pre-commit`'s
# py-typecheck stage reports ~33 mypy errors (pytest is untyped
# without its stubs) and py-test dies with "Couldn't find a
# virtualenv".
#
# Idempotent and cheap when there is nothing to do: the corpus half
# classifies each submodule with a stat-only `git diff` (~60 ms for all
# four) and skips the ones already checked out, and `uv sync --locked`
# re-audits an up-to-date venv in ~30 ms. It does rebuild the compiled
# extension when the Rust sources have moved since the last sync — that
# is the venv being brought up to date, not waste, and it is why this
# delegates to py-bootstrap rather than short-circuiting on a `.venv`
# directory that an interrupted bootstrap may have left half-built.
# Deliberately NOT wired into pre-commit or ci — it is a developer
# bootstrap, not a gate.
#
# The venv half is a soft skip when uv is absent, matching how the other
# py-* targets behave on a barebones host; `make py-bootstrap` remains
# the hard-failing entry point for someone who asked for it by name.
worktree-setup:
@python3 $(BASE_DIR)utils/worktree-setup.py
@if command -v uv >/dev/null 2>&1; then \
$(MAKE) --no-print-directory py-bootstrap; \
else \
echo "worktree-setup: uv not found; skipping the Python venv."; \
echo " Until it exists, 'make pre-commit' reports ~33 mypy errors from"; \
echo " py-typecheck and py-test dies with \"Couldn't find a virtualenv\"."; \
echo " Install uv (curl -LsSf https://astral.sh/uv/install.sh | sh), then"; \
echo " re-run 'make worktree-setup'."; \
fi
# Self-tests for the worktree-setup classifier. Not a gate over the
# tree's contents, but the classifier decides when to run a destructive
# `git submodule update --force`, so it is held to the same standard as
# the utils/ gates and runs in pre-commit alongside them.
worktree-setup-test:
@(cd $(BASE_DIR) && python3 -m unittest -q utils/worktree-setup-test.py)
# Self-tests for utils/gate-status.sh, which wraps `pre-commit` and `ci`
# and is the only thing standing between a red gate and a log that reads
# as green. Held to the same standard as the utils/ gates for that
# reason, and cheap enough (~0.1s, no cargo) to run in both.
gate-status-test:
@bash $(BASE_DIR)utils/gate-status-test.sh
# ---------------------------------------------------------------------------
# Build
# ---------------------------------------------------------------------------
build:
cargo build --workspace --all-targets
build-release:
cargo build --workspace --release
check:
cargo check --workspace --all-targets
# ---------------------------------------------------------------------------
# Test
# ---------------------------------------------------------------------------
test:
@if [ -z "$(NEXTEST)" ]; then \
echo "cargo-nextest not found or disabled; using 'cargo test' (same tests, slower)."; \
echo "Install with: cargo install --locked cargo-nextest"; \
fi
$(TEST_CMD)
# Doctests stay on `cargo test`: nextest cannot run them at all, so this
# is a separate target rather than an oversight (see .config/nextest.toml).
test-doc:
cargo test --workspace --all-features --doc
# The ancestor-chain audit (#1122). `--cfg chain_audit` restores the
# exact `chain.last() == node.parent()` assertion in `Ancestors::checked`
# that the default build trades for an O(1) approximation, so every walk
# that threads a chain is verified node-by-node against the tree.
#
# Out of band because that assertion is `Node::parent`'s O(depth) lookup
# per node, which turns the linear shipped walk quadratic and roughly
# triples the lib suite's wall time. Runs in the `chain-audit` CI lane;
# run it locally around any change to a walk's truncate/push bookkeeping.
#
# Library-scoped, matching that lane: the walks that thread a chain live
# in the root crate (`spaces::compute`, `ops`, `suppression`) and in
# `big-code-analysis-ast` (`comment_rm`, `Search::act_on_node` — which
# `find` and `count` also walk through — and the `Ast` dump's `build`),
# so the CLI / web / integration tiers would re-pay the quadratic cost
# without reaching an assertion the lib tests do not already reach.
#
# RUSTFLAGS rather than a Cargo feature on purpose: `make test` passes
# `--all-features`, so a feature would switch the audit back on in the
# very inner loop this target exists to keep fast.
chain-audit:
RUSTFLAGS="$${RUSTFLAGS:-} --cfg chain_audit" \
cargo test -p big-code-analysis -p big-code-analysis-ast --lib --all-features
# `cargo insta test` shells out to `cargo test`, not $(TEST_CMD): insta's
# nextest integration needs `--test-runner nextest`, and under it insta
# cannot force-pass a failing assertion to collect every pending snapshot
# in one run — which is exactly what review/accept need. Snapshot review
# is an interactive, non-hot-loop operation, so it keeps the slower runner.
insta-review:
cargo insta test --review
insta-accept:
cargo insta test --accept
# ---------------------------------------------------------------------------
# Formatting
# ---------------------------------------------------------------------------
fmt:
@echo "Formatting Rust code..."
@cargo fmt --all
@echo "Formatting Markdown files..."
@$(MAKE) --no-print-directory markdown-fmt
@echo "Formatting bash scripts..."
@$(MAKE) --no-print-directory sh-fmt
@echo "Formatting TOML files..."
@$(MAKE) --no-print-directory toml-fmt
fmt-check:
@echo "Checking Rust code formatting..."
@cargo fmt --all --check || { echo "Rust code is not formatted (run 'make fmt')"; exit 1; }
@echo "Checking Markdown formatting..."
@$(MAKE) --no-print-directory markdown-lint
@echo "Checking bash script formatting..."
@$(MAKE) --no-print-directory sh-fmt-check
@echo "Checking TOML formatting..."
@$(MAKE) --no-print-directory toml-fmt-check
@echo "All formatting checks passed"
# Sanity guard for the find-by-ext helper. If EXCLUDE_DIRS over-matches
# (as it did in #160 when `tree-sitter-*` was unquoted and the recipe
# shell expanded the glob into absolute paths), every lint that pipes
# through `xargs -r` silently no-ops. Run as a prerequisite of every
# recipe that consumes find-by-ext.
_check-find:
@N=$$($(call find-by-ext,md,) | wc -l); \
[ "$$N" -ge 5 ] || { echo "ERROR: find-by-ext returned $$N .md files (expected >=5); EXCLUDE_DIRS is over-matching — see #160"; exit 1; }
# `rumdl check --fix` is preferred over `rumdl fmt` here: `fmt`
# exits 0 even when unfixable findings remain (formatter semantics),
# whereas `check --fix` applies every auto-fix it can and then exits
# non-zero if any finding is still outstanding. The latter is what
# we want for a Makefile gate — auto-fix what you can, then fail
# loudly on the rest so the contributor knows to edit manually.
markdown-fmt: _check-find
@echo "Auto-fixing Markdown files..."
@$(call find-by-ext,md,) | xargs -r rumdl check --fix || { echo "rumdl could not auto-fix all issues"; exit 1; }
markdown-lint: _check-find
@echo "Linting Markdown files..."
@$(call find-by-ext,md,) | xargs -r rumdl check || { echo "rumdl found issues"; exit 1; }
sh-fmt: _check-find
@$(call find-by-ext,sh,) | xargs -r shfmt -w -i 0 -ci -bn
sh-fmt-check: _check-find
@$(call find-by-ext,sh,) | xargs -r shfmt -d -i 0 -ci -bn || { echo "Bash scripts are not formatted (run 'make sh-fmt')"; exit 1; }
shellcheck: _check-find
@echo "Linting bash scripts with shellcheck..."
@$(call find-by-ext,sh,) | xargs -r shellcheck || { echo "Shellcheck found issues"; exit 1; }
toml-fmt:
@taplo fmt
toml-fmt-check:
@taplo fmt --check || { echo "TOML files are not formatted (run 'make toml-fmt')"; exit 1; }
toml-lint:
@echo "Linting TOML files..."
@taplo lint || { echo "TOML lint found issues"; exit 1; }
makefile-check:
@echo "Linting Makefile with checkmake..."
@checkmake --config $(BASE_DIR).checkmake.ini $(BASE_DIR)Makefile || { echo "checkmake found issues"; exit 1; }
# actionlint scans every workflow under .github/workflows/ and shells
# out to shellcheck (when present on PATH) for the `run:` blocks. It
# takes no file arguments here: invoked at the repo root it discovers
# .github/workflows/ automatically, which matches the canonical
# upstream invocation and keeps the recipe robust against new
# workflows being added.
actionlint:
@echo "Linting GitHub Actions workflows with actionlint..."
@(cd $(BASE_DIR) && actionlint -no-color) || { echo "actionlint found issues"; exit 1; }
snapshot-anchors:
@echo "Checking insta snapshot anchors..."
@python3 $(BASE_DIR)utils/check-snapshot-anchors.py
# Self-tests for the snapshot-anchors gate. Separate target for the
# same reason as rustfmt-bail-test below: the gate stays a one-line
# invariant check and test failures get their own parallel-arm output.
snapshot-anchors-test:
@echo "Running snapshot-anchors self-tests..."
@(cd $(BASE_DIR) && python3 -m unittest -q utils/check-snapshot-anchors-test.py)
# rustfmt-bail gate (#1136). `cargo fmt --check` exits 0 over regions
# rustfmt declined to format — a comment inside a match pattern makes
# it emit the whole match verbatim, silently. This probe over-indents
# every match arm and counts the ones rustfmt refuses to put back;
# any increase over `.rustfmt-bail-baseline.txt` fails.
rustfmt-bail:
@echo "Checking rustfmt bail count..."
@python3 $(BASE_DIR)utils/check-rustfmt-bail.py
# Self-tests for the rustfmt-bail gate. Kept separate from the gate
# target so the gate stays a one-line invariant check (matching the
# grammar-marker-sync pattern) and test failures get their own clean
# parallel-arm output in the pre-commit/CI DAG.
rustfmt-bail-test:
@echo "Running rustfmt-bail self-tests..."
@(cd $(BASE_DIR) && python3 -m unittest -q utils/check-rustfmt-bail-test.py)
# Grammar-marker-sync gate. Blocks the failure mode from #400:
# bumping the notification-only `tree-sitter-{javascript,cpp}`
# marker in `tree-sitter-{mozjs,mozcpp}/Cargo.toml` without
# re-running the matching `./generate-grammars/generate-*.sh`.
# Static lint — no network, no cargo, runs in milliseconds.
grammar-marker-sync:
@echo "Checking grammar-marker sync against baseline..."
@python3 $(BASE_DIR)utils/check-grammar-marker-sync.py
# Enums-codegen drift gate. Closes #405: running any
# `recreate-grammars.sh` invocation silently regenerated
# `big-code-analysis-ast/src/c_langs_macros/{c_macros,c_specials}.rs`
# to a pre-optimization form. This gate runs the codegen into a
# tempdir
# and diffs against the checked-in files; drift fails.
enums-codegen-drift:
@echo "Checking enums codegen drift..."
@bash $(BASE_DIR)utils/check-enums-codegen-drift.sh
# Self-tests for the enums-codegen-drift gate. Kept separate from
# the gate target so the gate stays a one-line invariant check
# (matching the grammar-marker-sync pattern) and test failures
# get their own clean parallel-arm output in the pre-commit/CI DAG.
enums-codegen-drift-test:
@echo "Running enums-codegen-drift self-tests..."
@(cd $(BASE_DIR) && python3 -m unittest -q utils/check-enums-codegen-drift-test.py)
# Self-tests for the grammar-marker-sync gate. Kept separate from
# the gate target so the gate stays a one-line invariant check
# (matching the snapshot-anchors / check-versions pattern) and
# the test failures get their own clean parallel-arm output in
# the pre-commit/CI DAG.
grammar-marker-sync-test:
@echo "Running grammar-marker-sync self-tests..."
@(cd $(BASE_DIR) && python3 -m unittest -q utils/check-grammar-marker-sync-test.py)
# Regenerate the man pages under `man/` from the live clap schema.
# Auto-fix flavour: `cargo xtask` rewrites every `.1` file so a
# subsequent `git diff --exit-code -- man/` is clean. Used by
# `_pc-manpages` so contributors can stage the regenerated output.
manpages:
@echo "Regenerating man pages from clap schema..."
@cargo xtask
# Regenerate-then-assert-clean drift gate. Like the CI `manpage`
# job in `.github/workflows/ci.yml`, this recipe runs `cargo xtask`
# (which rewrites `man/*.1` in place — the same side effect CI
# accepts on its ephemeral runners) and then fails if the resulting
# tree differs from what is committed — modified, deleted, *and*
# added pages (#1249; `git diff` alone cannot see an untracked new
# page). Used by `_ci-manpages` and `_pc-manpages`. Locally,
# contributors with hand-edited `.1` files will see those edits
# overwritten; man pages are generated artifacts and should not be
# hand-edited.
#
# The assertion itself lives in `utils/check-manpage-drift.py`, which
# the CI job's "Verify no drift" step calls too, so the two sites
# cannot drift apart. Its self-tests are `check-manpage-drift-test`.
manpages-check:
@echo "Checking man pages match clap schema..."
@cargo xtask
@python3 $(BASE_DIR)utils/check-manpage-drift.py
# Self-tests for the man-page drift gate. Kept separate from
# `manpages-check` (matching the check-versions-test pattern) so the
# gate stays a regenerate-then-assert pair and the test failures get
# their own clean parallel-arm output in the pre-commit/CI DAG. The
# tests run against scratch repositories only — never the real `man/`,
# which `_pc-manpages` rewrites concurrently.
check-manpage-drift-test:
@echo "Running manpage-drift self-tests..."
@(cd $(BASE_DIR) && python3 -m unittest -q utils/check-manpage-drift-test.py)
# Lockstep-version invariant: every owned crate and every internal
# `=<v>` dep pin must equal `[workspace.package].version`. See
# `RELEASING.md` "Lockstep version policy" and `check-versions.py`.
check-versions:
@echo "Checking lockstep version invariant..."
@python3 $(BASE_DIR)utils/check-versions.py
# Self-tests for the lockstep-version gate. Kept separate from the
# gate target (matching the grammar-marker-sync-test pattern) so the
# gate stays a one-line invariant check and the test failures get
# their own clean parallel-arm output in the pre-commit/CI DAG.
check-versions-test:
@echo "Running check-versions self-tests..."
@(cd $(BASE_DIR) && python3 -m unittest -q utils/check-versions-test.py)
# Workspace-exclusion gate. Closes #1145: a crate in the root
# `[workspace] exclude` array must root its own workspace, because
# `exclude` denies membership without stopping cargo's upward search
# for a workspace root. In a git worktree under `.claude/worktrees/`
# that search escapes the worktree and resolves against the main
# checkout, breaking `cargo fmt --all` and every `make pre-commit`
# stage chained behind it. Also asserts every tree-sitter dependency
# in the root manifest and in each excluded crate carries an `=X.Y.Z`
# pin (#1151), and that each excluded crate declares its own `[lints]`
# table or is listed exempt — `[workspace.lints]` reaches members only,
# so without one a crate is gated at `-D warnings` against the compiler
# defaults (#1228). Static lint — no network, no cargo.
check-excluded-manifests:
@echo "Checking workspace-excluded manifests..."
@python3 $(BASE_DIR)utils/check-excluded-manifests.py
# Self-tests for the workspace-exclusion gate. Kept separate from the
# gate target (matching the check-versions-test pattern) so the gate
# stays a one-line invariant check and test failures get their own
# clean parallel-arm output in the pre-commit/CI DAG.
check-excluded-manifests-test:
@echo "Running check-excluded-manifests self-tests..."
@(cd $(BASE_DIR) && python3 -m unittest -q utils/check-excluded-manifests-test.py)
# ruff version lockstep (#1230). One adopted ruff is declared in four
# places and nothing compared them: the `ruff-pre-commit` `rev:` in
# .pre-commit-config.yaml, the version big-code-analysis-py/uv.lock
# resolves, the `ruff==` pin in its requirements/dev.txt export (what
# CI installs), and the bound in its pyproject.toml. The rev had
# already drifted once — v0.15.14 against a lockfile resolving
# 0.15.22 — which is silent until the two versions happen to disagree,
# and then presents as "works locally, red in CI". Static lint — no
# network, no cargo.
check-ruff-lockstep:
@echo "Checking ruff version lockstep..."
@python3 $(BASE_DIR)utils/check-ruff-lockstep.py
# Self-tests for the ruff-lockstep gate. Separate target for the same
# reason as the other gate self-tests: the gate stays a one-line
# invariant check and test failures get their own parallel-arm output.
check-ruff-lockstep-test:
@echo "Running check-ruff-lockstep self-tests..."
@(cd $(BASE_DIR) && python3 -m unittest -q utils/check-ruff-lockstep-test.py)
# Publish-metadata gate (#1224). `cargo publish --dry-run` is the
# natural pre-tag check, but it cannot run for the four top-level
# crates: each pins an internal dependency at `=<version>`, and the
# Lockstep policy makes that the version being released — which is by
# definition not yet on crates.io. The workaround it replaces skipped
# the parent dry-run whenever the pinned leaf was absent from the
# sparse index, so it skipped on every release and the crate went out
# with no metadata gate at all. This asserts the same fields
# registry-free, from `cargo metadata` (which resolves
# `[workspace.package]` inheritance) plus `cargo package --list` (the
# one packaging step that does not resolve dependencies). Two cargo
# calls per crate, no build, and no workspace `target/` lock — measured
# at ~0.8s alongside a running build, so it fans out with the other
# static gates rather than chaining into the cargo pipeline.
check-publish-metadata:
@echo "Checking crates.io publish metadata..."
@python3 $(BASE_DIR)utils/check-publish-metadata.py
# Self-tests for the publish-metadata gate. Separate target for the
# same reason as the other gate self-tests: the gate stays a one-line
# invariant check and test failures get their own parallel-arm output.
check-publish-metadata-test:
@echo "Running check-publish-metadata self-tests..."
@(cd $(BASE_DIR) && python3 -m unittest -q utils/check-publish-metadata-test.py)
# Man-page packaging gate. Blocks the failure mode from #444:
# a bca subcommand man page that drops out of the hand-maintained
# deb/rpm asset lists in the CLI / web crate Cargo.toml. Static
# lint — no network, no cargo, runs in milliseconds. See #446.
check-manpage-assets:
@echo "Checking man-page packaging asset lists..."
@python3 $(BASE_DIR)utils/check-manpage-assets.py
# Diagnostic-prefix gate (#1199). Every severity prefix this workspace
# prints is lowercase and written by one helper per crate (`diag.rs`);
# a fresh `eprintln!("Warning: ...")` reads as correct in review and
# nothing else notices. Static lint — no network, no cargo.
check-diagnostic-prefix:
@echo "Checking diagnostic severity prefixes..."
@python3 $(BASE_DIR)utils/check-diagnostic-prefix.py
# Self-tests for the diagnostic-prefix gate. Separate target for the
# same reason as the other gate self-tests: a source-scanning gate that
# stops matching reports a clean tree, so its own tests have to fail
# loudly and on their own parallel arm.
check-diagnostic-prefix-test:
@echo "Running check-diagnostic-prefix self-tests..."
@(cd $(BASE_DIR) && python3 -m unittest -q utils/check-diagnostic-prefix-test.py)
# Feature-gate reachability (#1472). Deliberately NOT in `pre-commit` /
# `ci`: it needs a test-binary build per feature set, and the answer is
# only interesting for a feature set that enables none of some subject's
# rows — which is what the `feature-matrix` job in ci.yml already builds
# ten of. This target is the manual spelling; pass a leg's flags, e.g.
# make check-feature-gates FLAGS="--no-default-features --features go -p big-code-analysis"
check-feature-gates:
@echo "Checking union-gated test reachability..."
@(cd $(BASE_DIR) && python3 utils/check-feature-gates.py $(FLAGS))
# Self-tests for the feature-gates gate. Pure Python with the cargo call
# stubbed, so this one *is* cheap enough for the parallel gate — and a
# scanner that silently stops matching reports a clean tree, which is
# precisely why it needs its own arm.
check-feature-gates-test:
@echo "Running check-feature-gates self-tests..."
@(cd $(BASE_DIR) && python3 -m unittest -q utils/check-feature-gates-test.py)
# Per-language test gating (#1472). The companion to the gate above,
# and the opposite question: that one asks whether a *declared* union
# still excludes what it claims to, this one derives what the marker
# should be from the languages an item names and reports the ones
# missing it. A scanner of declared markers cannot see a missing one —
# deleting the marker deletes the subject it scans for.
#
# Unlike `check-feature-gates`, this IS in `pre-commit` / `ci`: it is a
# pure source scan with no cargo invocation, so it costs a few seconds.
# `--fix` writes the missing markers in place.
check-test-lang-gates:
@echo "Checking per-language test gates..."
@(cd $(BASE_DIR) && python3 utils/check-test-lang-gates.py)
# The third direction (#1478), and the only one that needs history.
# Both checks above compare a marker against the derivation; when the
# two agree and are both wrong, nothing static can tell. This scans the
# tree at COMPARE_REF too, computes which single-language builds compile
# each test in each, and fails on a test that still exists but stopped
# being built somewhere. No cargo: `git archive` plus the same scanner.
#
# Deliberately NOT in `pre-commit`. It needs a base revision to be
# meaningful, and a working tree mid-edit has none — the PR's merge
# base is the answer, so CI supplies it. Locally:
#
# make check-test-lang-gates-compare COMPARE_REF=origin/main
#
# Spelled out rather than a bare `REF`, which `?=` would take from
# an exported environment variable of that name.
COMPARE_REF ?= origin/main
check-test-lang-gates-compare:
@echo "Comparing per-language test membership against $(COMPARE_REF)..."
@(cd $(BASE_DIR) && python3 utils/check-test-lang-gates.py --compare $(COMPARE_REF))
# Self-tests for the test-language gate. Its `RepositoryTest` also pins
# that the derivation still reproduces every marker written by hand,
# which is what licenses the ~3,000 it generated.
check-test-lang-gates-test:
@echo "Running check-test-lang-gates self-tests..."
@(cd $(BASE_DIR) && python3 -m unittest -q utils/check-test-lang-gates-test.py)
# Safety-doc pin gate (#1057). The module doc of
# big-code-analysis-py/src/node.rs is the canonical soundness argument
# for this workspace's only sanctioned `unsafe` block, and it reasons
# about a named tree-sitter release. When the pin moves and that literal
# does not, the argument keeps reading as verified while describing a
# crate nobody compiles against. Static lint - no network, no cargo.
check-safety-doc-pin:
@echo "Checking the unsafe soundness doc's tree-sitter citation..."
@python3 $(BASE_DIR)utils/check-safety-doc-pin.py
# Self-tests for the safety-doc-pin gate. Separate target for the same
# reason as the other gate self-tests: a source-scanning gate that stops
# matching reports a clean tree.
check-safety-doc-pin-test:
@echo "Running check-safety-doc-pin self-tests..."
@(cd $(BASE_DIR) && python3 -m unittest -q utils/check-safety-doc-pin-test.py)
# Sync gate for check-grammar-crate.py's EXTENSIONS table. Re-derives
# the grammar -> extension mapping from the parse layer's langs.rs
# `mk_langs!` and fails if the hand-maintained table has drifted
# (#869). Static — no
# network, no cargo — so it rides the cheap test DAG alongside the
# other helper-script self-tests.
check-grammar-crate-test:
@echo "Running check-grammar-crate self-tests..."
@(cd $(BASE_DIR) && python3 -m unittest -q utils/check-grammar-crate-test.py)
# The `enums/` crate is listed in `[workspace].exclude` (it ships a
# non-published codegen binary used only by `recreate-grammars.sh`), so
# it is invisible to `cargo {check,clippy,test} --workspace` and to the
# `lint` / `test` CI jobs. Without an explicit gate, lint regressions in
# `enums/src/*.rs` drift silently — the `unused_imports` warning fixed
# in #162 went unnoticed for that exact reason (see #164).
#
# RUSTFLAGS is set per-recipe (defensive): CI exports the same value at
# workflow scope, so the recipe-local export is redundant in CI but
# necessary locally to keep `make enums-check` behave identically
# everywhere.
#
# Uses `cargo clippy` (not `cargo check`) so the gate enforces the same
# lint floor as the workspace `clippy` job: rustc-level warnings plus the
# clippy default group. The three `manual_is_ascii_check` sites that
# previously blocked this (tracked as #166) have been fixed.
#
# Also runs `cargo test` on the same manifest because the workspace
# `test` job (`cargo test --workspace`) skips this crate by exclusion,
# leaving `enums/tests/dispatch.rs` and any other integration tests
# unexecuted in CI / pre-commit. The dispatch test pins each `Lang`
# variant to its expected backing grammar crate (issue #350); without
# this runtime gate, an arm in `mk_get_language!` pointing at the wrong
# grammar would compile cleanly and only fail when a developer
# manually invoked `cargo test` against the enums manifest.
enums-check:
@echo "Linting workspace-excluded enums crate..."
@RUSTFLAGS="-D warnings" cargo clippy \
--manifest-path $(BASE_DIR)enums/Cargo.toml \
--all-targets --locked -- -D warnings
@echo "Running tests for workspace-excluded enums crate..."
@cargo test \
--manifest-path $(BASE_DIR)enums/Cargo.toml \
--locked
# ---------------------------------------------------------------------------
# bca self-scan threshold gate
#
# Re-runs the CI threshold gate against the in-tree bca binary.
# Mirrors the `Threshold gate (baseline-ratcheted)` step in
# .github/workflows/pages.yml. We build from source on purpose:
# any in-progress change to metric computation is reflected in the
# values we gate on. When pages.yml bumps the pinned release
# binary, the two invocations re-sync.
#
# Path selection (`paths = ["."]`), the ignore file
# (`exclude_from = ".bcaignore"`), the baseline
# (`baseline = ".bca-baseline.toml"`), the per-function thresholds,
# and the cyclomatic `?` policy (`cyclomatic_count_try = false`) all
# live in the auto-discovered `bca.toml` manifest, so a bare
# `bca check` reproduces the full hard-tier gate with no flags.
#
# Two tiers:
#
# self-scan hard gate (limits as configured in bca.toml;
# absorbed by .bca-baseline.toml). Mirrors CI.
#
# self-scan-headroom soft gate (`--tier=soft=BCA_HEADROOM`).
# Scales every limit by BCA_HEADROOM (default
# 0.95) and runs the same baseline-ratcheted
# check, so new functions encroaching into the
# 95-100% band fail before they would trip the
# hard gate. Set BCA_HEADROOM=0.90 to widen the
# band, 0.99 to tighten it. The ratio rides on
# the soft tier itself (#688): `--tier=soft=R`
# replaces the retired `--headroom R` dial.
#
# Refresh the baseline (after an intentional regression or after
# raising a limit):
#
# self-scan-write-baseline refreshes .bca-baseline.toml in place.
# ---------------------------------------------------------------------------
SELF_SCAN_BCA := cargo run --quiet --release -p big-code-analysis-cli --
# `--jobs` defaults to `auto` (effective CPU count, cgroup-/cpuset-
# aware on Linux via Rust's std lib), so no `$(nproc)` plumbing is
# needed for the self-scan recipes (issue #383).
#
# The walk root is left to the manifest's `paths = ["."]` key: a bare
# `bca check` resolves it to an absolute root, but the walker now
# re-anchors that root to the `./`-prefixed relative form before
# matching (#488), so the `./`-anchored `.bcaignore` deny-set matches
# the same files whether the seed was `.`, `$PWD`, or a manifest-
# resolved absolute path. The previous `--paths .` workaround is no
# longer needed.
#
# Diff-aware footer partitioning in the self-scan gate (#356/#387). Set
# BCA_SINCE=<ref> to append `--since <ref>`, which splits the per-file
# rollup footer into "Files in this range:" (offenders the diff touched)
# vs "Other offenders:". CI (`.github/workflows/pages.yml`) sets it to
# `origin/${GITHUB_BASE_REF:-main}` so PR runs highlight the offenders a
# PR introduced; local runs leave it unset, so the gate is unchanged and
# the local/CI invocation stays identical by construction. `--since` is
# presentational only — it never changes pass/fail (that would be
# `--changed-only`, deliberately not adopted here, see #387). The
# `--github-annotations` / `$GITHUB_STEP_SUMMARY` digest / remediation
# block all auto-enable from GitHub Actions env vars, so no flag is
# needed for them.
#
# `--since` is a `check` *subcommand* argument, so it is spliced in
# after `check` in each gate target below.
#
# Default to empty so the `:=` expansion does not trip
# `--warn-undefined-variables` when the var is unset (the common
# case); CI's environment value still overrides, since `?=` only
# assigns when undefined.
BCA_SINCE ?=
SELF_SCAN_SINCE_ARGS := $(if $(BCA_SINCE),--since $(BCA_SINCE),)
self-scan:
@echo "bca self-scan (hard gate)..."
@$(SELF_SCAN_BCA) check $(SELF_SCAN_SINCE_ARGS)
self-scan-headroom:
@echo "bca self-scan (soft gate, BCA_HEADROOM=$${BCA_HEADROOM:-0.95})..."
@$(SELF_SCAN_BCA) check $(SELF_SCAN_SINCE_ARGS) \
--tier=soft=$${BCA_HEADROOM:-0.95}
self-scan-write-baseline:
@echo "Refreshing .bca-baseline.toml from current offenders..."
@$(SELF_SCAN_BCA) check --write-baseline
# Refresh `.bca-baseline.toml` against the SOFT thresholds
# (the `bca.toml` limits scaled by BCA_HEADROOM, default 0.95).
# Records every current offender at the soft tier — strictly a
# superset of the hard-tier offenders. Use this when launching the
# soft gate or after raising BCA_HEADROOM so the baseline absorbs
# the new headroom band rather than firing on every commit.
self-scan-write-baseline-headroom:
@echo "Refreshing .bca-baseline.toml from current soft-tier offenders (BCA_HEADROOM=$${BCA_HEADROOM:-0.95})..."
@$(SELF_SCAN_BCA) check \
--tier=soft=$${BCA_HEADROOM:-0.95} \
--write-baseline
# ---------------------------------------------------------------------------
# Change-history (VCS) risk report (issue #328). Dogfoods `bca vcs` on
# this repo: ranks tracked files by composite change-history risk (churn,
# commit/author counts, ownership dilution, bug-/security-fix history)
# over the default 12mo / 90d windows. Informational only — it never
# gates (always exits 0). Path selection and the `.bcaignore` deny-set
# come from the auto-discovered `bca.toml` manifest, exactly like
# `make self-scan`. The two file universes are close but not identical:
# `.bcaignore` shapes what is *walked* and both commands honour it,
# whereas `[check] exclude` shapes what is *gated* and only `check`
# consults it — so the dev-tooling trees exempted there (#1146) are
# ranked here and absent from the gate.
# `BCA_VCS_TOP` overrides the row cap (default 40; 0 = all). This prints
# the quick terminal table; for the styled, sortable page (published to
# Pages by the `pages.yml` workflow, issue #573) run
# `bca vcs --format html --output vcs.html`.
# ---------------------------------------------------------------------------
vcs:
@$(SELF_SCAN_BCA) vcs --top $${BCA_VCS_TOP:-40}
# ---------------------------------------------------------------------------
# Python tooling (big-code-analysis-py)
#
# Most targets in this section gracefully no-op when the corresponding
# tool is absent — matching how the markdown / TOML lint families
# behave on a barebones host. CI installs all tools, so the skip path
# never fires there. Exception: `py-bootstrap` is the documented
# setup entry point and hard-fails when `uv` is missing, so a
# contributor sees the install instruction immediately rather than
# silently skipping.
#
# Tools used:
# uv — Python env / lockfile manager (required for py-bootstrap)
# ruff — lint + format
# mypy — type check (strict mode, invoked from the bindings dir)
# pyright — type check (strict mode, second opinion)
# maturin — build the compiled extension into the active venv
#
# `py-test` requires an active venv that maturin can write the .so
# into. The recipe does NOT create one — if `VIRTUAL_ENV` is not set,
# maturin will fail with a clear error. CI explicitly creates one per
# matrix leg (see `.github/workflows/ci.yml`). Locally, run
# `make py-bootstrap` once to create big-code-analysis-py/.venv from
# the checked-in uv.lock.
# ---------------------------------------------------------------------------
# Bootstrap the bindings dev environment from uv.lock. Hard-fails if
# uv is missing — there is no pip fallback because uv.lock is the
# project's source of truth for the resolved dev set, and consuming
# it requires uv. `uv sync --locked` creates big-code-analysis-py/.venv,
# installs the locked `dev` extra into it, and is idempotent. The
# `--locked` flag refuses to mutate uv.lock — if pyproject.toml has
# drifted from the lockfile, this target fails loudly and points at
# `make py-relock` instead of silently rewriting the lockfile on
# every contributor's machine.
py-bootstrap:
@command -v uv >/dev/null 2>&1 || { \
echo "ERROR: uv missing — install via 'curl -LsSf https://astral.sh/uv/install.sh | sh' (or 'brew install uv', 'pipx install uv')"; \
exit 1; }
@cd "$(BCA_PY_DIR)" && uv sync --locked --extra dev
# Alias so contributors familiar with `uv sync` find the right target.
py-sync: py-bootstrap
# Regenerate uv.lock after editing pyproject.toml (e.g. bumping a
# dev-extra floor). Separated from py-bootstrap so lockfile churn is
# always a deliberate, reviewable act — `make py-bootstrap` will not
# silently update the lockfile on contributors' machines.
# The hash-pinned exports under big-code-analysis-py/requirements/
# are regenerated in the same breath: CI installs its Python tooling
# with `pip install --require-hashes -r` from those files (OpenSSF
# Scorecard Pinned-Dependencies), so they must always mirror uv.lock.
py-relock:
@command -v uv >/dev/null 2>&1 || { \
echo "ERROR: uv missing — install via 'curl -LsSf https://astral.sh/uv/install.sh | sh' (or 'brew install uv', 'pipx install uv')"; \
exit 1; }
@cd "$(BCA_PY_DIR)" && uv lock
@cd "$(BCA_PY_DIR)" && mkdir -p requirements && \
uv export --locked --no-emit-project --extra dev -o requirements/dev.txt && \
uv export --locked --no-emit-project --extra examples -o requirements/examples.txt
# `scripts/smoke` is a second, separate ruff invocation rather than an extra
# path on the bindings call: `--config` would otherwise re-root the bindings
# dir's own `extend-exclude` (e.g. `_native.pyi`) against the wrong base and
# spuriously flag excluded files. The smoke dir has nothing to exclude, so
# pointing `--config` at the bindings pyproject just gives it the same
# ruleset (line-length 100, the `[tool.ruff.lint] select` set) (#995).
#
# Resolve ruff the way `py-typecheck` resolves mypy and pyright: prefer
# the bindings dir's `.venv/bin/ruff`, fall back to PATH, skip cleanly
# when neither exists. Hoisted into one variable because all three ruff
# targets need the identical resolution, and because PATH-first was
# wrong in the same way three times over (#1230). Three provisioning
# paths install an *unpinned* ruff onto PATH — `mise.toml`'s
# `"pipx:ruff" = "latest"`, the Dockerfile's `uv tool install ruff`, and
# the `pipx install ruff` CONTRIBUTING documents — so PATH-first let the
# local gate run a ruff outside the `pyproject.toml` `<0.17` ceiling
# while CI ran the `uv.lock`-resolved one. The venv copy is by
# construction the version `make check-ruff-lockstep` gates.
#
# `exit 0` (not a shell `if/else` around the whole body) keeps each
# recipe a single logical line: the resolve step ends the sub-shell
# successfully and make moves on, exactly as the old `else echo
# "...skipping"` arm did.
#
# The `|| true` is load-bearing under this Makefile's
# `.SHELLFLAGS := -eu -o pipefail -c`. An assignment takes the exit
# status of its command substitution, and the right-hand side of `||`
# is the list's last command, so a failing `command -v` (bash returns 1
# when the name is absent) kills the shell under `-e` — turning the
# skip-when-missing path into `make: *** [py-lint] Error 1` with no
# diagnostic at all. Measured, not theorised.
PY_RUFF_RESOLVE = ruff="$(BCA_PY_DIR)/.venv/bin/ruff"; \
[ -x "$$ruff" ] || ruff="$$(command -v ruff 2>/dev/null || true)"; \
if [ -z "$$ruff" ]; then echo "ruff not found; skipping $@"; exit 0; fi
py-fmt:
@$(PY_RUFF_RESOLVE); \
echo "Formatting Python sources..."; \
"$$ruff" format "$(BCA_PY_DIR)" && \
"$$ruff" format --config "$(BCA_PY_DIR)/pyproject.toml" "$(SMOKE_PY_DIR)" || \
{ echo "ruff format failed"; exit 1; }
py-fmt-check:
@$(PY_RUFF_RESOLVE); \
echo "Checking Python formatting..."; \
"$$ruff" format --check "$(BCA_PY_DIR)" && \
"$$ruff" format --check --config "$(BCA_PY_DIR)/pyproject.toml" "$(SMOKE_PY_DIR)" || \
{ echo "Python files not formatted (run 'make py-fmt')"; exit 1; }
py-lint:
@$(PY_RUFF_RESOLVE); \
echo "Linting Python sources..."; \
"$$ruff" check "$(BCA_PY_DIR)" && \
"$$ruff" check --config "$(BCA_PY_DIR)/pyproject.toml" "$(SMOKE_PY_DIR)" || \
{ echo "ruff lint found issues"; exit 1; }