diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml index 985a80fc..dc76b499 100644 --- a/.forgejo/workflows/ci.yml +++ b/.forgejo/workflows/ci.yml @@ -40,3 +40,18 @@ jobs: # merge. Promote to a required check once the tree is clean. # See scripts/check-issue-refs.sh. run: sh scripts/check-issue-refs.sh + + comment-blocks: + name: comment-block lint + runs-on: [hive-ci] + # Pure git+awk — seconds. + timeout-minutes: 5 + steps: + - uses: actions/checkout@v3 + - name: lint + # Flags contiguous comment blocks over 30 lines (a giant prose + # block belongs in docs/ as implementation notes, not in source). + # Own job, kept out of required checks while the tree settles: a + # hit reds this check without blocking merge. Promote to required + # once clean. See scripts/check-comment-blocks.sh. + run: sh scripts/check-comment-blocks.sh diff --git a/scripts/check-comment-blocks.sh b/scripts/check-comment-blocks.sh new file mode 100755 index 00000000..c3b90fe6 --- /dev/null +++ b/scripts/check-comment-blocks.sh @@ -0,0 +1,76 @@ +#!/bin/sh +# CI lint: flags a contiguous comment block longer than MAX lines. A giant +# prose block in source is a signal it should live in docs/ as an +# "implementation notes" section instead — short "why"/invariant/contract +# comments stay, walls of text move out. See the hive convention in #2077. +# +# Emits a CI error annotation per offending block and exits 1 if any block +# exceeds MAX. Runs as its own CI job, kept OUT of the required checks while +# the tree settles: a hit turns the job red (visible, non-blocking) without +# blocking merge. Promote to required once the tree is clean. +# +# Scope: tracked *.rs *.nix *.sh *.js *.ts *.css *.html. Markdown is exempt +# (it is prose by nature). Comment forms: `#` line comments (nix/sh), `//` +# line comments (rs/js/ts), and `/* */` (rs/js/ts/css) + `` (html) +# block comments. A blank line separates two line-comment blocks (does NOT +# extend a run); a blank inside a `/* */` / `` block stays part of it. +# +# Escape hatch: a `lint:allow-long-comment` marker anywhere in a block +# exempts it (reserve for a genuinely irreducible block; prefer relocating). +set -eu + +MAX=30 + +# Collect annotations into a variable (not via xargs/awk exit codes) so a +# hit in any xargs batch is preserved — mirrors scripts/check-issue-refs.sh. +hits="$( + git ls-files -z '*.rs' '*.nix' '*.sh' '*.js' '*.ts' '*.css' '*.html' \ + | xargs -0 -r awk -v MAX="$MAX" ' + function mode_of(fn) { + if (fn ~ /\.(nix|sh)$/) return "hash" + if (fn ~ /\.(rs|js|ts)$/) return "slash" + if (fn ~ /\.css$/) return "cstyle" + if (fn ~ /\.html$/) return "html" + return "" + } + function flush() { + if (run > MAX && !allow) + printf "::error file=%s,line=%d::comment block of %d lines exceeds the %d-line max — move long prose to docs/ as an implementation-notes section (or mark lint:allow-long-comment)\n", curfile, start, run, MAX + run = 0; allow = 0 + } + FNR == 1 { flush(); curfile = FILENAME; mode = mode_of(FILENAME); inblock = 0 } + { + if (mode == "") next + c = 0 + if (inblock) { + c = 1 + if (index($0, blockend) > 0) inblock = 0 + } else if (mode == "hash") { + if ($0 ~ /^[ \t]*#/) c = 1 + } else if (mode == "slash") { + if ($0 ~ /^[ \t]*\/\//) c = 1 + else if ($0 ~ /^[ \t]*\/\*/) { c = 1; if (index($0, "*/") == 0) { inblock = 1; blockend = "*/" } } + } else if (mode == "cstyle") { + if ($0 ~ /^[ \t]*\/\*/) { c = 1; if (index($0, "*/") == 0) { inblock = 1; blockend = "*/" } } + } else if (mode == "html") { + if ($0 ~ /^[ \t]*") == 0) { inblock = 1; blockend = "-->" } } + } + if (c) { + if (run == 0) start = FNR + run++ + if ($0 ~ /lint:allow-long-comment/) allow = 1 + } else { + flush() + } + } + END { flush() } + ' +)" + +if [ -n "$hits" ]; then + printf '%s\n' "$hits" + count="$(printf '%s\n' "$hits" | grep -c '::error' || true)" + printf 'check-comment-blocks: %s comment block(s) over %d lines found\n' "$count" "$MAX" >&2 + exit 1 +fi +exit 0