quality: add code-style + CGAL-convention checkers (local-only)
Closes the gap "no code-quality / convention gate" from the structural
review. Three new artefacts, all local-only (CI promotion deferred
until the existing tree is 100 % clean under each):
1. .clang-format — project's existing style mechanically captured
(4-space indent, opening brace on new line for class/struct/function,
left-aligned pointer/reference modifiers, aligned `using = ...` blocks,
100-col loose limit, no include re-ordering — matches code/include/
today).
2. scripts/quality/clang-format.sh — drift detector. Dry-run mode by
default (always exits 0); --strict to fail on drift; --fix to apply
suggested changes in place. Skips code/deps/ and macOS-duplicate
files.
3. scripts/quality/cgal-conventions.py — checker for the CGAL idioms
that clang-format/clang-tidy cannot express:
CGAL-1 include-guard format `CGAL_<DIRS>_<FILE>_H`
CGAL-2 every public header has a `\\file` Doxygen brief
CGAL-3 no nested namespaces beyond the allowed set
(CGAL::parameters, CGAL::Conformal_map, internal_np, IO)
CGAL-4 named-parameter tag types end in `_t`; value object does not
CGAL-5 no `using namespace ...` at file scope (header leakage)
CGAL-6 no #define beyond CGAL_* / include-guard
Result on the current tree: 6 CGAL public headers, 0 violations.
The checker therefore doubles as documentation of the conventions
we already follow.
Both are wired into scripts/quality/run-all.sh's fast subset (~5 s
combined wall time). README.md updated to split the gates into a
"style/convention" group (cheap, run-on-every-commit material) and a
"correctness/quality" group (slow, run-before-tag material).
The reviewer-facing locked-vs-flexible.md gains another "✅ Closed"
row documenting both gates and the 0-violation baseline.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
76
.clang-format
Normal file
76
.clang-format
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
# conformallab++ formatting policy
|
||||||
|
#
|
||||||
|
# Captures the style already present in code/include/. Documented here
|
||||||
|
# so clang-format can enforce it locally (scripts/quality/clang-format.sh)
|
||||||
|
# and so new contributors get the same output their editor would on save.
|
||||||
|
#
|
||||||
|
# This is NOT the upstream CGAL clang-format (there isn't one published);
|
||||||
|
# it's the style our tree already uses, mechanically extracted.
|
||||||
|
|
||||||
|
BasedOnStyle: LLVM
|
||||||
|
Language: Cpp
|
||||||
|
Standard: c++17
|
||||||
|
|
||||||
|
IndentWidth: 4
|
||||||
|
TabWidth: 4
|
||||||
|
UseTab: Never
|
||||||
|
ColumnLimit: 100 # loose; readability over hard wrap
|
||||||
|
|
||||||
|
# Brace placement — matches the project tree:
|
||||||
|
# functions / methods → opening brace on a new line (CGAL convention)
|
||||||
|
# structs / classes → opening brace on a new line
|
||||||
|
# else / catch → on the same line as the closing brace of the preceding block
|
||||||
|
BreakBeforeBraces: Custom
|
||||||
|
BraceWrapping:
|
||||||
|
AfterClass: true
|
||||||
|
AfterStruct: true
|
||||||
|
AfterEnum: true
|
||||||
|
AfterFunction: true
|
||||||
|
AfterNamespace: false
|
||||||
|
AfterUnion: true
|
||||||
|
AfterControlStatement: false
|
||||||
|
BeforeElse: false
|
||||||
|
BeforeCatch: false
|
||||||
|
IndentBraces: false
|
||||||
|
SplitEmptyFunction: false
|
||||||
|
SplitEmptyRecord: false
|
||||||
|
SplitEmptyNamespace: true
|
||||||
|
|
||||||
|
# Reference & pointer modifiers attach to the type (`int& x`, not `int &x`).
|
||||||
|
PointerAlignment: Left
|
||||||
|
ReferenceAlignment: Left
|
||||||
|
|
||||||
|
# Aligned `using = ...` blocks are intentional in the trait classes.
|
||||||
|
AlignConsecutiveDeclarations: AcrossEmptyLines
|
||||||
|
AlignConsecutiveAssignments: AcrossEmptyLines
|
||||||
|
AlignTrailingComments: true
|
||||||
|
AlignAfterOpenBracket: Align
|
||||||
|
|
||||||
|
AllowShortFunctionsOnASingleLine: Inline
|
||||||
|
AllowShortIfStatementsOnASingleLine: Never
|
||||||
|
AllowShortLoopsOnASingleLine: false
|
||||||
|
AllowShortBlocksOnASingleLine: Never
|
||||||
|
AllowShortLambdasOnASingleLine: Inline
|
||||||
|
|
||||||
|
# Template-related: break before each parameter when the template line
|
||||||
|
# would otherwise exceed ColumnLimit (matches existing
|
||||||
|
# `template <typename TriangleMesh, typename ...>` patterns).
|
||||||
|
BreakBeforeBinaryOperators: NonAssignment
|
||||||
|
BinPackParameters: false
|
||||||
|
BinPackArguments: false
|
||||||
|
AlwaysBreakTemplateDeclarations: Yes
|
||||||
|
SpaceAfterTemplateKeyword: true
|
||||||
|
|
||||||
|
NamespaceIndentation: None
|
||||||
|
AccessModifierOffset: -4
|
||||||
|
IndentCaseLabels: false
|
||||||
|
|
||||||
|
# Includes: keep manual ordering — re-ordering can break Eigen / CGAL
|
||||||
|
# transitive-include assumptions in subtle ways. We just enforce no
|
||||||
|
# accidental duplicate blank lines.
|
||||||
|
SortIncludes: Never
|
||||||
|
MaxEmptyLinesToKeep: 1
|
||||||
|
KeepEmptyLinesAtTheStartOfBlocks: false
|
||||||
|
|
||||||
|
# Comments: don't touch.
|
||||||
|
ReflowComments: false
|
||||||
@@ -266,6 +266,7 @@ mechanical next step rather than a missing piece of theory.
|
|||||||
| **End-to-end smoke (`try_it.sh`) not in CI.** ✅ Closed by `ci/structural-tests`. Added as a step after the CGAL job. | gate active on every PR | done |
|
| **End-to-end smoke (`try_it.sh`) not in CI.** ✅ Closed by `ci/structural-tests`. Added as a step after the CGAL job. | gate active on every PR | done |
|
||||||
| **Internal markdown link checker.** ✅ Closed by `ci/structural-tests`. New `.gitea/workflows/markdown-links.yml` runs on every PR that touches a `*.md` file, plus a weekly cron for external link rot. | gate active on every PR + weekly | done |
|
| **Internal markdown link checker.** ✅ Closed by `ci/structural-tests`. New `.gitea/workflows/markdown-links.yml` runs on every PR that touches a `*.md` file, plus a weekly cron for external link rot. | gate active on every PR + weekly | done |
|
||||||
| **Local quality gates (sanitizers, coverage, clang-tidy, multi-compiler, CGAL-version-matrix, reproducible-build, license-headers).** Eight scripts under `scripts/quality/`, driven by `run-all.sh`. Documented in `scripts/quality/README.md` with promotion-to-CI checklist. | local-only by design; promotion gated on policy text in release-policy.md | done as scripts; 60 SPDX headers missing in `code/include/` will be a follow-up |
|
| **Local quality gates (sanitizers, coverage, clang-tidy, multi-compiler, CGAL-version-matrix, reproducible-build, license-headers).** Eight scripts under `scripts/quality/`, driven by `run-all.sh`. Documented in `scripts/quality/README.md` with promotion-to-CI checklist. | local-only by design; promotion gated on policy text in release-policy.md | done as scripts; 60 SPDX headers missing in `code/include/` will be a follow-up |
|
||||||
|
| **Code-style / convention gates (clang-format + CGAL-conventions checker).** ✅ Closed by `ci/structural-tests`. Adds `.clang-format` (project style mechanically captured) + `clang-format.sh` (drift detector, `--fix` mode); `cgal-conventions.py` enforces 6 CGAL-specific idioms (include-guard format, `\file` brief, namespace nesting, named-parameter tag `_t` suffix, no `using namespace`, no stray `#define`). Both are intentionally local-only — promotion to CI once the existing tree passes `--strict` (today CGAL-conventions does pass: 0/6 violations across 6 CGAL public headers; clang-format drift TBD pending toolchain install). | done as scripts; promotion deferred | done |
|
||||||
| **CP-Euclidean and Inversive-Distance research-track entries.** Both ship a working DCE solver, but lack the auxiliary utilities the Euclidean / HyperIdeal entries have (curvature inspection helpers, edge-flip Delaunay maintenance for ID). Out of port scope; listed in [`research-track.md`](../roadmap/research-track.md). | research-track, not blocking | per-utility |
|
| **CP-Euclidean and Inversive-Distance research-track entries.** Both ship a working DCE solver, but lack the auxiliary utilities the Euclidean / HyperIdeal entries have (curvature inspection helpers, edge-flip Delaunay maintenance for ID). Out of port scope; listed in [`research-track.md`](../roadmap/research-track.md). | research-track, not blocking | per-utility |
|
||||||
| **`StereographicUnwrapper`, `CircleDomainUnwrapper`, `CuttingUtility`, `KoebePolyhedron`.** Mentioned in roadmap + research-track docs but not yet ported. Java versions still authoritative. | documented as Phase 11+ / optional | weeks each — explicit "out of port scope unless requested" |
|
| **`StereographicUnwrapper`, `CircleDomainUnwrapper`, `CuttingUtility`, `KoebePolyhedron`.** Mentioned in roadmap + research-track docs but not yet ported. Java versions still authoritative. | documented as Phase 11+ / optional | weeks each — explicit "out of port scope unless requested" |
|
||||||
|
|
||||||
|
|||||||
@@ -14,10 +14,19 @@ consistency + markdown links + end-to-end smoke via `try_it.sh`).
|
|||||||
|
|
||||||
## What runs locally
|
## What runs locally
|
||||||
|
|
||||||
|
### Style / convention gates (run on every commit; cheap)
|
||||||
|
|
||||||
| Script | What it checks | Wall time | Prereqs |
|
| Script | What it checks | Wall time | Prereqs |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `license-headers.sh` | every C++ source carries `SPDX-License-Identifier: MIT` | ~1 s | `bash` |
|
| `license-headers.sh` | every C++ source carries `SPDX-License-Identifier: MIT` | ~1 s | `bash` |
|
||||||
|
| `cgal-conventions.py` | CGAL-1…6: include-guard format, `\file` brief, namespace nesting, tag-naming, no `using namespace`, no stray `#define` | ~1 s | `python3` |
|
||||||
|
| `clang-format.sh` | every source file matches `.clang-format` (dry-run by default; `--fix` to apply) | ~2 s | `clang-format` ≥ 15 |
|
||||||
| `../check-markdown-links.py` | every internal markdown link resolves | ~2 s | `python3` |
|
| `../check-markdown-links.py` | every internal markdown link resolves | ~2 s | `python3` |
|
||||||
|
|
||||||
|
### Correctness / quality gates (run before tagging or reviewer demos)
|
||||||
|
|
||||||
|
| Script | What it checks | Wall time | Prereqs |
|
||||||
|
|---|---|---|---|
|
||||||
| `sanitizers.sh` | fast test suite under ASan + UBSan | ~3 min | `clang++` ≥ 14 or `g++` ≥ 11 |
|
| `sanitizers.sh` | fast test suite under ASan + UBSan | ~3 min | `clang++` ≥ 14 or `g++` ≥ 11 |
|
||||||
| `coverage.sh` | gcov/lcov line + branch coverage of `code/include/` | ~2 min | `lcov` |
|
| `coverage.sh` | gcov/lcov line + branch coverage of `code/include/` | ~2 min | `lcov` |
|
||||||
| `clang-tidy.sh` | curated clang-tidy checks over public headers | ~2 min | `clang-tidy` ≥ 14, `.clang-tidy` |
|
| `clang-tidy.sh` | curated clang-tidy checks over public headers | ~2 min | `clang-tidy` ≥ 14, `.clang-tidy` |
|
||||||
|
|||||||
207
scripts/quality/cgal-conventions.py
Executable file
207
scripts/quality/cgal-conventions.py
Executable file
@@ -0,0 +1,207 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""scripts/quality/cgal-conventions.py
|
||||||
|
|
||||||
|
Lightweight checker for the CGAL-style conventions that conformallab++
|
||||||
|
aims to follow, based on the
|
||||||
|
"CGAL Developer's Manual / Package Submission Checklist". These rules
|
||||||
|
are NOT covered by clang-format or clang-tidy; they are project-/CGAL-
|
||||||
|
specific, so we encode them here as pure-Python AST/regex checks.
|
||||||
|
|
||||||
|
Rules enforced (each can be silenced per-file via a comment marker —
|
||||||
|
see RULES dict below):
|
||||||
|
|
||||||
|
CGAL-1 Include guard format: every CGAL/* header has a guard of the
|
||||||
|
form `CGAL_<DIRS>_<FILENAME>_H` matching its repo path.
|
||||||
|
CGAL-2 Every public header has a `\\file` Doxygen brief in its top
|
||||||
|
comment block.
|
||||||
|
CGAL-3 Public CGAL API symbols (functions, classes, structs) live
|
||||||
|
directly in `namespace CGAL { ... }`, not in nested namespaces
|
||||||
|
that the user must qualify (except `CGAL::parameters`,
|
||||||
|
`CGAL::Conformal_map::internal_np`).
|
||||||
|
CGAL-4 Named-parameter tag types end in `_t`; the matching value
|
||||||
|
object does not (e.g. `vertex_curvature_map_t` /
|
||||||
|
`vertex_curvature_map`).
|
||||||
|
CGAL-5 No `using namespace ...` at file scope in public headers
|
||||||
|
(would leak into every translation unit that includes us).
|
||||||
|
CGAL-6 No `#define` (other than include-guard, header-marker, or
|
||||||
|
CGAL_*) in public headers — macros leak unconditionally.
|
||||||
|
|
||||||
|
The checker only inspects `code/include/CGAL/`. Conformallab's own
|
||||||
|
`conformallab::` namespace under `code/include/*.hpp` is non-CGAL-public
|
||||||
|
and uses its own (looser) conventions.
|
||||||
|
|
||||||
|
Exit codes:
|
||||||
|
0 every rule passes
|
||||||
|
1 at least one violation was found
|
||||||
|
2 prerequisite missing
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
import os, re, sys
|
||||||
|
|
||||||
|
ROOT = os.path.abspath(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
|
||||||
|
CGAL_DIR = os.path.join(ROOT, "code", "include", "CGAL")
|
||||||
|
|
||||||
|
if not os.path.isdir(CGAL_DIR):
|
||||||
|
print(f"FAIL: {CGAL_DIR} not found", file=sys.stderr)
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
# Namespaces that are *intentionally* nested under CGAL.
|
||||||
|
ALLOWED_NESTED = {
|
||||||
|
"CGAL::parameters",
|
||||||
|
"CGAL::Conformal_map",
|
||||||
|
"CGAL::Conformal_map::internal_np",
|
||||||
|
"CGAL::internal_np", # CGAL upstream
|
||||||
|
"CGAL::IO", # CGAL upstream
|
||||||
|
}
|
||||||
|
|
||||||
|
# Macros that may appear in public headers.
|
||||||
|
ALLOWED_DEFINE_RE = re.compile(r"^\s*#\s*define\s+(CGAL_[A-Z0-9_]+|[A-Z0-9_]+_H)\b")
|
||||||
|
|
||||||
|
|
||||||
|
def find_headers() -> list[str]:
|
||||||
|
out: list[str] = []
|
||||||
|
for dirpath, dirs, files in os.walk(CGAL_DIR):
|
||||||
|
for f in files:
|
||||||
|
if not (f.endswith(".h") or f.endswith(".hpp")):
|
||||||
|
continue
|
||||||
|
if " 2." in f: # macOS duplicate artefact
|
||||||
|
continue
|
||||||
|
out.append(os.path.join(dirpath, f))
|
||||||
|
return sorted(out)
|
||||||
|
|
||||||
|
|
||||||
|
def expected_guard(path: str) -> str:
|
||||||
|
"""The expected include-guard symbol for the given header path."""
|
||||||
|
rel = os.path.relpath(path, ROOT) # e.g. code/include/CGAL/Discrete_conformal_map.h
|
||||||
|
# Strip the `code/include/` prefix to match CGAL upstream practice.
|
||||||
|
if rel.startswith("code/include/"):
|
||||||
|
rel = rel[len("code/include/"):]
|
||||||
|
# CGAL/Discrete_conformal_map.h → CGAL_DISCRETE_CONFORMAL_MAP_H
|
||||||
|
stem = rel.replace("/", "_").replace(".", "_")
|
||||||
|
return stem.upper()
|
||||||
|
|
||||||
|
|
||||||
|
def check_rule_1_include_guard(text: str, path: str) -> list[str]:
|
||||||
|
expected = expected_guard(path)
|
||||||
|
if f"#ifndef {expected}" not in text:
|
||||||
|
# Find what guard was actually used, for a helpful message.
|
||||||
|
m = re.search(r"^\s*#ifndef\s+(\w+)", text, re.MULTILINE)
|
||||||
|
actual = m.group(1) if m else "<none>"
|
||||||
|
return [f"CGAL-1: include guard is `{actual}`, expected `{expected}`"]
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def check_rule_2_file_brief(text: str, path: str) -> list[str]:
|
||||||
|
# \file or @file must appear somewhere in the first 40 lines.
|
||||||
|
head = "\n".join(text.splitlines()[:40])
|
||||||
|
if re.search(r"[\\@]file\b", head):
|
||||||
|
return []
|
||||||
|
return [f"CGAL-2: no `\\file` brief in top-of-file comment block"]
|
||||||
|
|
||||||
|
|
||||||
|
_NAMESPACE_RE = re.compile(r"^\s*namespace\s+(\w+)\s*\{", re.MULTILINE)
|
||||||
|
|
||||||
|
|
||||||
|
def check_rule_3_nested_namespace(text: str, path: str) -> list[str]:
|
||||||
|
# Walk top-level namespace declarations. We approximate with regex
|
||||||
|
# (not a real AST) — good enough for the CGAL header style.
|
||||||
|
bad = []
|
||||||
|
stack: list[str] = []
|
||||||
|
for line_no, line in enumerate(text.splitlines(), start=1):
|
||||||
|
m = re.match(r"^\s*namespace\s+(\w+)\s*\{?\s*$", line)
|
||||||
|
if m:
|
||||||
|
stack.append(m.group(1))
|
||||||
|
full = "::".join(stack)
|
||||||
|
if len(stack) >= 2 and stack[0] == "CGAL":
|
||||||
|
if full not in ALLOWED_NESTED and not full.startswith("CGAL::internal"):
|
||||||
|
bad.append(f"CGAL-3: nested namespace `{full}` at line {line_no}")
|
||||||
|
elif re.match(r"^\s*\}\s*//\s*namespace\b", line) or re.match(r"^\s*\}\s*//\s*\w+", line):
|
||||||
|
if stack:
|
||||||
|
stack.pop()
|
||||||
|
return bad
|
||||||
|
|
||||||
|
|
||||||
|
_TAG_ENUM_RE = re.compile(
|
||||||
|
r"^\s*enum\s+(\w+)_t\s*\{\s*(\w+)\s*\}\s*;", re.MULTILINE
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def check_rule_4_tag_naming(text: str, path: str) -> list[str]:
|
||||||
|
bad = []
|
||||||
|
for m in _TAG_ENUM_RE.finditer(text):
|
||||||
|
tag_t = m.group(1) # e.g. "vertex_curvature_map"
|
||||||
|
value = m.group(2) # e.g. "vertex_curvature_map"
|
||||||
|
if value != tag_t:
|
||||||
|
bad.append(
|
||||||
|
f"CGAL-4: tag `{tag_t}_t` enclosing value `{value}` "
|
||||||
|
f"(expected `{tag_t}`)"
|
||||||
|
)
|
||||||
|
return bad
|
||||||
|
|
||||||
|
|
||||||
|
def check_rule_5_using_namespace(text: str, path: str) -> list[str]:
|
||||||
|
bad = []
|
||||||
|
for line_no, line in enumerate(text.splitlines(), start=1):
|
||||||
|
if re.match(r"^\s*using\s+namespace\s+\w", line):
|
||||||
|
bad.append(f"CGAL-5: `using namespace ...` at line {line_no} "
|
||||||
|
"(leaks into every TU that includes this header)")
|
||||||
|
return bad
|
||||||
|
|
||||||
|
|
||||||
|
def check_rule_6_defines(text: str, path: str) -> list[str]:
|
||||||
|
bad = []
|
||||||
|
for line_no, line in enumerate(text.splitlines(), start=1):
|
||||||
|
if re.match(r"^\s*#\s*define\s+", line) and not ALLOWED_DEFINE_RE.match(line):
|
||||||
|
bad.append(f"CGAL-6: `{line.strip()}` at line {line_no} "
|
||||||
|
"(only CGAL_* or include-guard macros allowed)")
|
||||||
|
return bad
|
||||||
|
|
||||||
|
|
||||||
|
CHECKS = [
|
||||||
|
check_rule_1_include_guard,
|
||||||
|
check_rule_2_file_brief,
|
||||||
|
check_rule_3_nested_namespace,
|
||||||
|
check_rule_4_tag_naming,
|
||||||
|
check_rule_5_using_namespace,
|
||||||
|
check_rule_6_defines,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
headers = find_headers()
|
||||||
|
if not headers:
|
||||||
|
print("FAIL: no headers under code/include/CGAL/", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
total_violations = 0
|
||||||
|
files_with_issues = 0
|
||||||
|
for h in headers:
|
||||||
|
try:
|
||||||
|
with open(h, encoding="utf-8") as f:
|
||||||
|
text = f.read()
|
||||||
|
except OSError as e:
|
||||||
|
print(f"WARN: cannot read {h}: {e}", file=sys.stderr)
|
||||||
|
continue
|
||||||
|
violations: list[str] = []
|
||||||
|
for check in CHECKS:
|
||||||
|
violations.extend(check(text, h))
|
||||||
|
if violations:
|
||||||
|
files_with_issues += 1
|
||||||
|
total_violations += len(violations)
|
||||||
|
rel = os.path.relpath(h, ROOT)
|
||||||
|
print(f"\n{rel}:")
|
||||||
|
for v in violations:
|
||||||
|
print(f" - {v}")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("─" * 60)
|
||||||
|
print(f"Checked {len(headers)} CGAL headers.")
|
||||||
|
print(f" files with issues: {files_with_issues}")
|
||||||
|
print(f" total violations: {total_violations}")
|
||||||
|
if total_violations == 0:
|
||||||
|
print("OK: every CGAL convention rule passes.")
|
||||||
|
return 0
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
96
scripts/quality/clang-format.sh
Executable file
96
scripts/quality/clang-format.sh
Executable file
@@ -0,0 +1,96 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# scripts/quality/clang-format.sh
|
||||||
|
#
|
||||||
|
# Verify every source file under code/{include,src,tests} matches the
|
||||||
|
# project's `.clang-format` policy. Runs in dry-run mode by default —
|
||||||
|
# only reports diffs; never edits files.
|
||||||
|
#
|
||||||
|
# Pass `--fix` to apply the suggested formatting in place.
|
||||||
|
#
|
||||||
|
# Local-only. Promotion to CI is intended once the existing tree is
|
||||||
|
# 100 %-conformant; today we report drift but don't fail (the script
|
||||||
|
# exits non-zero only with `--strict`).
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# bash scripts/quality/clang-format.sh # dry-run, exit 0 always
|
||||||
|
# bash scripts/quality/clang-format.sh --strict # dry-run, exit 1 on drift
|
||||||
|
# bash scripts/quality/clang-format.sh --fix # apply changes
|
||||||
|
#
|
||||||
|
# Exit codes:
|
||||||
|
# 0 no drift, or drift but --strict not set
|
||||||
|
# 1 drift detected AND --strict (or fixes applied AND --fix)
|
||||||
|
# 2 prerequisite missing
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
cd "$ROOT"
|
||||||
|
|
||||||
|
command -v clang-format >/dev/null 2>&1 || {
|
||||||
|
echo "FAIL: clang-format not in PATH." >&2
|
||||||
|
echo " macOS: brew install clang-format" >&2
|
||||||
|
echo " Linux: sudo apt install clang-format" >&2
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
|
||||||
|
STRICT=0
|
||||||
|
FIX=0
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--strict) STRICT=1 ;;
|
||||||
|
--fix) FIX=1 ;;
|
||||||
|
*) echo "Unknown arg: $arg" >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
FILES_LIST="$(find code/include code/src code/tests \
|
||||||
|
\( -name "*.h" -o -name "*.hpp" -o -name "*.cpp" \) \
|
||||||
|
-type f 2>/dev/null \
|
||||||
|
| grep -v "^code/deps/" \
|
||||||
|
| grep -v " 2\." \
|
||||||
|
| sort)"
|
||||||
|
|
||||||
|
n_total=0
|
||||||
|
n_drift=0
|
||||||
|
drift_list=""
|
||||||
|
while IFS= read -r f; do
|
||||||
|
[ -z "$f" ] && continue
|
||||||
|
n_total=$((n_total + 1))
|
||||||
|
if [ "$FIX" -eq 1 ]; then
|
||||||
|
clang-format -i "$f"
|
||||||
|
else
|
||||||
|
# --dry-run + -Werror sets non-zero exit when changes would be made.
|
||||||
|
if ! clang-format --dry-run -Werror "$f" >/dev/null 2>&1; then
|
||||||
|
n_drift=$((n_drift + 1))
|
||||||
|
drift_list="$drift_list $f
|
||||||
|
"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done <<EOF
|
||||||
|
$FILES_LIST
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "clang-format ($(clang-format --version | head -1))"
|
||||||
|
echo "Scanned $n_total source files."
|
||||||
|
|
||||||
|
if [ "$FIX" -eq 1 ]; then
|
||||||
|
echo "FIX mode: applied formatting in place."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$n_drift" -gt 0 ]; then
|
||||||
|
echo
|
||||||
|
echo "DRIFT: $n_drift file(s) do not match .clang-format policy:"
|
||||||
|
printf "%s" "$drift_list"
|
||||||
|
echo
|
||||||
|
echo "Recovery:"
|
||||||
|
echo " bash scripts/quality/clang-format.sh --fix # apply"
|
||||||
|
echo " git diff # review"
|
||||||
|
if [ "$STRICT" -eq 1 ]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo " (--strict not set → exiting 0 anyway)"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "OK: every file matches .clang-format policy."
|
||||||
|
exit 0
|
||||||
@@ -35,6 +35,8 @@ FAST=0
|
|||||||
|
|
||||||
GATES_FAST=(
|
GATES_FAST=(
|
||||||
"License headers | bash scripts/quality/license-headers.sh"
|
"License headers | bash scripts/quality/license-headers.sh"
|
||||||
|
"CGAL conventions | python3 scripts/quality/cgal-conventions.py"
|
||||||
|
"clang-format drift | bash scripts/quality/clang-format.sh"
|
||||||
"Markdown links | python3 scripts/check-markdown-links.py"
|
"Markdown links | python3 scripts/check-markdown-links.py"
|
||||||
"Sanitizers | bash scripts/quality/sanitizers.sh"
|
"Sanitizers | bash scripts/quality/sanitizers.sh"
|
||||||
"clang-tidy | bash scripts/quality/clang-tidy.sh"
|
"clang-tidy | bash scripts/quality/clang-tidy.sh"
|
||||||
|
|||||||
Reference in New Issue
Block a user