feat(lint): wire Vale as deterministic prefilter for skill-audit/agent-audit #85
@@ -168,3 +168,21 @@ that makes the two disagree already touches `$HOOKS_MANIFEST`, itself a release-
|
|||||||
unreadable tagged tree (shallow clone, truncated fetch) fails closed rather than silently degrading
|
unreadable tagged tree (shallow clone, truncated fetch) fails closed rather than silently degrading
|
||||||
to worktree-only derivation; a manifest simply absent at the tag — legitimate, it was added since —
|
to worktree-only derivation; a manifest simply absent at the tag — legitimate, it was added since —
|
||||||
does not.
|
does not.
|
||||||
|
|
||||||
|
**Update — the flattener rewrites no characters.** This ADR never recorded it as a decision, but
|
||||||
|
`vale-wrap.sh`'s flattener carried a lossy last-resort branch: when a description needed quoting
|
||||||
|
*and* held an ASCII apostrophe *and* held a double quote or backslash, it substituted U+2019 (`’`)
|
||||||
|
for every `'` before writing the scratch copy, on the stated rationale that no verbatim YAML scalar
|
||||||
|
could carry that combination. The rationale was wrong. A `|-` literal block with a single indented
|
||||||
|
content line carries `'`, `"`, `\` and `: ` byte for byte — a block scalar's body has no escape
|
||||||
|
syntax at all — and vale's `text.frontmatter.description` scope still matches and fires rules on it
|
||||||
|
(verified against vale 3.15.2; it is the same property that makes the `|` blocks in the wrapper's
|
||||||
|
header safe to leave unflattened). The branch fired on 12 of the 54 in-scope files in this repo,
|
||||||
|
silently disabling every rule whose token contains an apostrophe on each of them. The flattener now
|
||||||
|
emits that literal block instead, so its output is verbatim in all four forms and no Vale rule can
|
||||||
|
be silently disabled by the prefilter. The `|-` form is two physical lines where the three inline
|
||||||
|
forms are one, so the blank-line pad that preserves later line numbers drops by one — reachable
|
||||||
|
only when the original span is already two or more lines, so the pad count stays non-negative.
|
||||||
|
`tests/test-vale-wrap.sh` case 20 asserts an apostrophe-bearing token actually fires on a flattened
|
||||||
|
description in all three apostrophe-carrying branches, and case 20b pins the pad arithmetic against
|
||||||
|
a body line's true line number.
|
||||||
|
|||||||
@@ -9,8 +9,10 @@ set -euo pipefail
|
|||||||
# the same way. A `|`/`|-`/`|+` literal block scalar is NOT affected: its parsed
|
# the same way. A `|`/`|-`/`|+` literal block scalar is NOT affected: its parsed
|
||||||
# value keeps exactly the line breaks the source has, and vale matches it fine
|
# value keeps exactly the line breaks the source has, and vale matches it fine
|
||||||
# (verified against vale 3.15.2), so literal blocks are deliberately left alone.
|
# (verified against vale 3.15.2), so literal blocks are deliberately left alone.
|
||||||
# This script flattens an affected description to one physical line in a scratch
|
# This script flattens an affected description to a one-line scalar in a scratch
|
||||||
# copy (padding with blank lines so every other line number is unchanged), then
|
# copy — or, for the rare value no inline scalar can spell out verbatim, to a
|
||||||
|
# `|-` literal block with a single content line, which vale matches just as well
|
||||||
|
# (padding with blank lines so every other line number is unchanged), then
|
||||||
# runs the real `vale` binary against the copies. Drop-in replacement for calling
|
# runs the real `vale` binary against the copies. Drop-in replacement for calling
|
||||||
# `vale` directly: same args, same exit code, bar the two documented divergences
|
# `vale` directly: same args, same exit code, bar the two documented divergences
|
||||||
# below.
|
# below.
|
||||||
@@ -61,6 +63,20 @@ vale_args=()
|
|||||||
path_args=()
|
path_args=()
|
||||||
pending_flag=""
|
pending_flag=""
|
||||||
config_given=false
|
config_given=false
|
||||||
|
|
||||||
|
# `--output` takes either one of vale's built-in style names or a template file
|
||||||
|
# path. Only the file form needs absolutizing, and the built-in names have to be
|
||||||
|
# excluded by name *before* the existence test below: a file or directory
|
||||||
|
# literally called `line` in the caller's cwd would otherwise rewrite the
|
||||||
|
# built-in into `$cwd/line`, flipping vale into template mode (`E100 [template]
|
||||||
|
# Runtime error`) where bare vale just uses the built-in. `--path` has no such
|
||||||
|
# names — it is always a path — so the check is keyed on the flag too.
|
||||||
|
is_builtin_output() {
|
||||||
|
case "$2" in
|
||||||
|
line|JSON|CLI) [[ "$1" == "--output" ]] ;;
|
||||||
|
*) false ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
for arg in "$@"; do
|
for arg in "$@"; do
|
||||||
if [[ -n "$pending_flag" ]]; then
|
if [[ -n "$pending_flag" ]]; then
|
||||||
# Value of a separated two-argv flag. It is never a lint target, however
|
# Value of a separated two-argv flag. It is never a lint target, however
|
||||||
@@ -76,11 +92,12 @@ for arg in "$@"; do
|
|||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
--output|--path)
|
--output|--path)
|
||||||
# `--output` is either a built-in style name (`line`, `JSON`) or a
|
# See `is_builtin_output` above for why the built-in `--output` names
|
||||||
# template file; only the file form needs resolving. `--path` is
|
# are excluded first. Anything that names nothing is passed through and
|
||||||
# likewise a path. Anything that names nothing is passed through and
|
|
||||||
# left for vale to interpret.
|
# left for vale to interpret.
|
||||||
if [[ "$arg" != /* && -e "$arg" ]]; then
|
if is_builtin_output "$pending_flag" "$arg"; then
|
||||||
|
vale_args+=("$arg")
|
||||||
|
elif [[ "$arg" != /* && -e "$arg" ]]; then
|
||||||
vale_args+=("$cwd/$arg")
|
vale_args+=("$cwd/$arg")
|
||||||
else
|
else
|
||||||
vale_args+=("$arg")
|
vale_args+=("$arg")
|
||||||
@@ -114,7 +131,9 @@ for arg in "$@"; do
|
|||||||
# other path-valued flags.
|
# other path-valued flags.
|
||||||
--output=*|--path=*)
|
--output=*|--path=*)
|
||||||
flag_val="${arg#*=}"
|
flag_val="${arg#*=}"
|
||||||
if [[ "$flag_val" != /* && -n "$flag_val" && -e "$flag_val" ]]; then
|
if is_builtin_output "${arg%%=*}" "$flag_val"; then
|
||||||
|
vale_args+=("$arg")
|
||||||
|
elif [[ "$flag_val" != /* && -n "$flag_val" && -e "$flag_val" ]]; then
|
||||||
vale_args+=("${arg%%=*}=$cwd/$flag_val")
|
vale_args+=("${arg%%=*}=$cwd/$flag_val")
|
||||||
else
|
else
|
||||||
vale_args+=("$arg")
|
vale_args+=("$arg")
|
||||||
@@ -286,13 +305,14 @@ def continuation_lines(rest):
|
|||||||
|
|
||||||
|
|
||||||
def emit(value):
|
def emit(value):
|
||||||
"""Render `value` as a one-line YAML scalar whose source text spells the
|
"""Render `value` as a YAML scalar whose source text spells the value out
|
||||||
value out verbatim. Vale locates the description by matching the parsed
|
verbatim. Vale locates the description by matching the parsed value back
|
||||||
value back against the source, so a scalar carrying any escape — `''` in a
|
against the source, so a scalar carrying any escape — `''` in a
|
||||||
single-quoted scalar, `\\"` or `\\\\` in a double-quoted one — makes the
|
single-quoted scalar, `\\"` or `\\\\` in a double-quoted one — makes the
|
||||||
whole `text.frontmatter.description` scope vanish, the same failure this
|
whole `text.frontmatter.description` scope vanish, the same failure this
|
||||||
script exists to work around. Verbatim forms only, therefore, tried in
|
script exists to work around. Verbatim forms only, therefore, tried in
|
||||||
descending order of fidelity."""
|
descending order of fidelity. The first three occupy one physical line; the
|
||||||
|
`|-` fallback occupies two, which the caller accounts for when padding."""
|
||||||
if (value
|
if (value
|
||||||
and value[0] not in PLAIN_UNSAFE_FIRST
|
and value[0] not in PLAIN_UNSAFE_FIRST
|
||||||
and ': ' not in value
|
and ': ' not in value
|
||||||
@@ -304,11 +324,14 @@ def emit(value):
|
|||||||
if '"' not in value and '\\' not in value:
|
if '"' not in value and '\\' not in value:
|
||||||
return '"' + value + '"' # double-quoted: only `"`/`\` would
|
return '"' + value + '"' # double-quoted: only `"`/`\` would
|
||||||
# Last resort: the value needs quoting AND holds an apostrophe AND a double
|
# Last resort: the value needs quoting AND holds an apostrophe AND a double
|
||||||
# quote or backslash, so no verbatim YAML scalar can carry it. Substituting
|
# quote or backslash, so no *inline* scalar can carry it verbatim. A `|-`
|
||||||
# U+2019 for the apostrophe keeps the scope alive, at the cost of any style
|
# literal block can — a block scalar's body has no escape syntax at all, so
|
||||||
# rule whose token contains a literal ASCII apostrophe. Scratch-copy only —
|
# `'`, `"`, `\` and `: ` all survive byte for byte, and vale still matches
|
||||||
# never written back to the real file.
|
# the description scope against it (the header above says the same of the
|
||||||
return "'" + value.replace("'", '’') + "'"
|
# `|` blocks this script deliberately leaves alone; verified against vale
|
||||||
|
# 3.15.2). One content line, indented two spaces, `-`-chomped so the parsed
|
||||||
|
# value is exactly `value` with no trailing newline.
|
||||||
|
return '|-\n ' + value
|
||||||
|
|
||||||
|
|
||||||
fm_match = re.match(r'^(---\n)(.*?\n)(---\n)', content, re.DOTALL)
|
fm_match = re.match(r'^(---\n)(.*?\n)(---\n)', content, re.DOTALL)
|
||||||
@@ -392,11 +415,21 @@ if header_m:
|
|||||||
newline = fm.find('\n', value_end)
|
newline = fm.find('\n', value_end)
|
||||||
span_end = len(fm) if newline == -1 else newline + 1
|
span_end = len(fm) if newline == -1 else newline + 1
|
||||||
trailer = fm[value_end:span_end].rstrip('\n')
|
trailer = fm[value_end:span_end].rstrip('\n')
|
||||||
# One line replaces the span, so the blank-line pad is one short of the
|
scalar = emit(flat)
|
||||||
# newline count it displaced — every later line number is unchanged.
|
# A trailing comment carried across from the original line stays on the
|
||||||
pad = '\n' * (fm[head_start:span_end].count('\n') - 1)
|
# `description:` line itself: after a block scalar's `|-` header it is
|
||||||
new_fm = (fm[:head_start] + 'description: ' + emit(flat) + trailer
|
# still a comment, but inside the block body it would become part of the
|
||||||
+ '\n' + pad + fm[span_end:])
|
# value.
|
||||||
|
head, newline_sep, block_body = scalar.partition('\n')
|
||||||
|
# The replacement displaces the whole span, so the blank-line pad makes
|
||||||
|
# up the difference between the lines it displaced and the lines it
|
||||||
|
# occupies — every later line number is unchanged. That is one line for
|
||||||
|
# the three inline forms and two for the `|-` block; the span itself is
|
||||||
|
# at least two lines here (`value_lines >= 2` is a precondition), so the
|
||||||
|
# pad count never goes negative.
|
||||||
|
pad = '\n' * (fm[head_start:span_end].count('\n') - 1 - scalar.count('\n'))
|
||||||
|
new_fm = (fm[:head_start] + 'description: ' + head + trailer
|
||||||
|
+ newline_sep + block_body + '\n' + pad + fm[span_end:])
|
||||||
content = (fm_match.group(1) + new_fm + fm_match.group(3)
|
content = (fm_match.group(1) + new_fm + fm_match.group(3)
|
||||||
+ content[fm_match.end():])
|
+ content[fm_match.end():])
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,10 @@ set -euo pipefail
|
|||||||
# the same way. A `|`/`|-`/`|+` literal block scalar is NOT affected: its parsed
|
# the same way. A `|`/`|-`/`|+` literal block scalar is NOT affected: its parsed
|
||||||
# value keeps exactly the line breaks the source has, and vale matches it fine
|
# value keeps exactly the line breaks the source has, and vale matches it fine
|
||||||
# (verified against vale 3.15.2), so literal blocks are deliberately left alone.
|
# (verified against vale 3.15.2), so literal blocks are deliberately left alone.
|
||||||
# This script flattens an affected description to one physical line in a scratch
|
# This script flattens an affected description to a one-line scalar in a scratch
|
||||||
# copy (padding with blank lines so every other line number is unchanged), then
|
# copy — or, for the rare value no inline scalar can spell out verbatim, to a
|
||||||
|
# `|-` literal block with a single content line, which vale matches just as well
|
||||||
|
# (padding with blank lines so every other line number is unchanged), then
|
||||||
# runs the real `vale` binary against the copies. Drop-in replacement for calling
|
# runs the real `vale` binary against the copies. Drop-in replacement for calling
|
||||||
# `vale` directly: same args, same exit code, bar the two documented divergences
|
# `vale` directly: same args, same exit code, bar the two documented divergences
|
||||||
# below.
|
# below.
|
||||||
@@ -61,6 +63,20 @@ vale_args=()
|
|||||||
path_args=()
|
path_args=()
|
||||||
pending_flag=""
|
pending_flag=""
|
||||||
config_given=false
|
config_given=false
|
||||||
|
|
||||||
|
# `--output` takes either one of vale's built-in style names or a template file
|
||||||
|
# path. Only the file form needs absolutizing, and the built-in names have to be
|
||||||
|
# excluded by name *before* the existence test below: a file or directory
|
||||||
|
# literally called `line` in the caller's cwd would otherwise rewrite the
|
||||||
|
# built-in into `$cwd/line`, flipping vale into template mode (`E100 [template]
|
||||||
|
# Runtime error`) where bare vale just uses the built-in. `--path` has no such
|
||||||
|
# names — it is always a path — so the check is keyed on the flag too.
|
||||||
|
is_builtin_output() {
|
||||||
|
case "$2" in
|
||||||
|
line|JSON|CLI) [[ "$1" == "--output" ]] ;;
|
||||||
|
*) false ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
for arg in "$@"; do
|
for arg in "$@"; do
|
||||||
if [[ -n "$pending_flag" ]]; then
|
if [[ -n "$pending_flag" ]]; then
|
||||||
# Value of a separated two-argv flag. It is never a lint target, however
|
# Value of a separated two-argv flag. It is never a lint target, however
|
||||||
@@ -76,11 +92,12 @@ for arg in "$@"; do
|
|||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
--output|--path)
|
--output|--path)
|
||||||
# `--output` is either a built-in style name (`line`, `JSON`) or a
|
# See `is_builtin_output` above for why the built-in `--output` names
|
||||||
# template file; only the file form needs resolving. `--path` is
|
# are excluded first. Anything that names nothing is passed through and
|
||||||
# likewise a path. Anything that names nothing is passed through and
|
|
||||||
# left for vale to interpret.
|
# left for vale to interpret.
|
||||||
if [[ "$arg" != /* && -e "$arg" ]]; then
|
if is_builtin_output "$pending_flag" "$arg"; then
|
||||||
|
vale_args+=("$arg")
|
||||||
|
elif [[ "$arg" != /* && -e "$arg" ]]; then
|
||||||
vale_args+=("$cwd/$arg")
|
vale_args+=("$cwd/$arg")
|
||||||
else
|
else
|
||||||
vale_args+=("$arg")
|
vale_args+=("$arg")
|
||||||
@@ -114,7 +131,9 @@ for arg in "$@"; do
|
|||||||
# other path-valued flags.
|
# other path-valued flags.
|
||||||
--output=*|--path=*)
|
--output=*|--path=*)
|
||||||
flag_val="${arg#*=}"
|
flag_val="${arg#*=}"
|
||||||
if [[ "$flag_val" != /* && -n "$flag_val" && -e "$flag_val" ]]; then
|
if is_builtin_output "${arg%%=*}" "$flag_val"; then
|
||||||
|
vale_args+=("$arg")
|
||||||
|
elif [[ "$flag_val" != /* && -n "$flag_val" && -e "$flag_val" ]]; then
|
||||||
vale_args+=("${arg%%=*}=$cwd/$flag_val")
|
vale_args+=("${arg%%=*}=$cwd/$flag_val")
|
||||||
else
|
else
|
||||||
vale_args+=("$arg")
|
vale_args+=("$arg")
|
||||||
@@ -286,13 +305,14 @@ def continuation_lines(rest):
|
|||||||
|
|
||||||
|
|
||||||
def emit(value):
|
def emit(value):
|
||||||
"""Render `value` as a one-line YAML scalar whose source text spells the
|
"""Render `value` as a YAML scalar whose source text spells the value out
|
||||||
value out verbatim. Vale locates the description by matching the parsed
|
verbatim. Vale locates the description by matching the parsed value back
|
||||||
value back against the source, so a scalar carrying any escape — `''` in a
|
against the source, so a scalar carrying any escape — `''` in a
|
||||||
single-quoted scalar, `\\"` or `\\\\` in a double-quoted one — makes the
|
single-quoted scalar, `\\"` or `\\\\` in a double-quoted one — makes the
|
||||||
whole `text.frontmatter.description` scope vanish, the same failure this
|
whole `text.frontmatter.description` scope vanish, the same failure this
|
||||||
script exists to work around. Verbatim forms only, therefore, tried in
|
script exists to work around. Verbatim forms only, therefore, tried in
|
||||||
descending order of fidelity."""
|
descending order of fidelity. The first three occupy one physical line; the
|
||||||
|
`|-` fallback occupies two, which the caller accounts for when padding."""
|
||||||
if (value
|
if (value
|
||||||
and value[0] not in PLAIN_UNSAFE_FIRST
|
and value[0] not in PLAIN_UNSAFE_FIRST
|
||||||
and ': ' not in value
|
and ': ' not in value
|
||||||
@@ -304,11 +324,14 @@ def emit(value):
|
|||||||
if '"' not in value and '\\' not in value:
|
if '"' not in value and '\\' not in value:
|
||||||
return '"' + value + '"' # double-quoted: only `"`/`\` would
|
return '"' + value + '"' # double-quoted: only `"`/`\` would
|
||||||
# Last resort: the value needs quoting AND holds an apostrophe AND a double
|
# Last resort: the value needs quoting AND holds an apostrophe AND a double
|
||||||
# quote or backslash, so no verbatim YAML scalar can carry it. Substituting
|
# quote or backslash, so no *inline* scalar can carry it verbatim. A `|-`
|
||||||
# U+2019 for the apostrophe keeps the scope alive, at the cost of any style
|
# literal block can — a block scalar's body has no escape syntax at all, so
|
||||||
# rule whose token contains a literal ASCII apostrophe. Scratch-copy only —
|
# `'`, `"`, `\` and `: ` all survive byte for byte, and vale still matches
|
||||||
# never written back to the real file.
|
# the description scope against it (the header above says the same of the
|
||||||
return "'" + value.replace("'", '’') + "'"
|
# `|` blocks this script deliberately leaves alone; verified against vale
|
||||||
|
# 3.15.2). One content line, indented two spaces, `-`-chomped so the parsed
|
||||||
|
# value is exactly `value` with no trailing newline.
|
||||||
|
return '|-\n ' + value
|
||||||
|
|
||||||
|
|
||||||
fm_match = re.match(r'^(---\n)(.*?\n)(---\n)', content, re.DOTALL)
|
fm_match = re.match(r'^(---\n)(.*?\n)(---\n)', content, re.DOTALL)
|
||||||
@@ -392,11 +415,21 @@ if header_m:
|
|||||||
newline = fm.find('\n', value_end)
|
newline = fm.find('\n', value_end)
|
||||||
span_end = len(fm) if newline == -1 else newline + 1
|
span_end = len(fm) if newline == -1 else newline + 1
|
||||||
trailer = fm[value_end:span_end].rstrip('\n')
|
trailer = fm[value_end:span_end].rstrip('\n')
|
||||||
# One line replaces the span, so the blank-line pad is one short of the
|
scalar = emit(flat)
|
||||||
# newline count it displaced — every later line number is unchanged.
|
# A trailing comment carried across from the original line stays on the
|
||||||
pad = '\n' * (fm[head_start:span_end].count('\n') - 1)
|
# `description:` line itself: after a block scalar's `|-` header it is
|
||||||
new_fm = (fm[:head_start] + 'description: ' + emit(flat) + trailer
|
# still a comment, but inside the block body it would become part of the
|
||||||
+ '\n' + pad + fm[span_end:])
|
# value.
|
||||||
|
head, newline_sep, block_body = scalar.partition('\n')
|
||||||
|
# The replacement displaces the whole span, so the blank-line pad makes
|
||||||
|
# up the difference between the lines it displaced and the lines it
|
||||||
|
# occupies — every later line number is unchanged. That is one line for
|
||||||
|
# the three inline forms and two for the `|-` block; the span itself is
|
||||||
|
# at least two lines here (`value_lines >= 2` is a precondition), so the
|
||||||
|
# pad count never goes negative.
|
||||||
|
pad = '\n' * (fm[head_start:span_end].count('\n') - 1 - scalar.count('\n'))
|
||||||
|
new_fm = (fm[:head_start] + 'description: ' + head + trailer
|
||||||
|
+ newline_sep + block_body + '\n' + pad + fm[span_end:])
|
||||||
content = (fm_match.group(1) + new_fm + fm_match.group(3)
|
content = (fm_match.group(1) + new_fm + fm_match.group(3)
|
||||||
+ content[fm_match.end():])
|
+ content[fm_match.end():])
|
||||||
|
|
||||||
|
|||||||
@@ -443,8 +443,11 @@ fi
|
|||||||
# is declared and never reset to empty: it cannot be empty at any expansion
|
# is declared and never reset to empty: it cannot be empty at any expansion
|
||||||
# site, so the construct is not a hazard there and demanding the guarded form
|
# site, so the construct is not a hazard there and demanding the guarded form
|
||||||
# would be a wrong test. The file list covers every script this repo ships or
|
# would be a wrong test. The file list covers every script this repo ships or
|
||||||
# runs that a macOS user reaches: the wrapper itself plus the two pre-commit
|
# runs that a macOS user reaches: the wrapper itself, the two pre-commit hook
|
||||||
# hook scripts.
|
# scripts, and the test runner AGENTS.md tells contributors to run by hand.
|
||||||
|
# `mapfile` is checked alongside, because it is bash 4.0+ and the expansion scan
|
||||||
|
# cannot see it — run-tests.sh carried one until it was replaced with a
|
||||||
|
# `while read` loop, and nothing would have caught its return.
|
||||||
echo ""
|
echo ""
|
||||||
echo "--- no unguarded array expansion remains in the macOS-facing scripts ---"
|
echo "--- no unguarded array expansion remains in the macOS-facing scripts ---"
|
||||||
unguarded_expansions() {
|
unguarded_expansions() {
|
||||||
@@ -470,11 +473,19 @@ HAZARDS16=""
|
|||||||
for BASH32_SCRIPT in \
|
for BASH32_SCRIPT in \
|
||||||
"$SCRIPT" \
|
"$SCRIPT" \
|
||||||
"$REPO_ROOT/scripts/skill-size-check.sh" \
|
"$REPO_ROOT/scripts/skill-size-check.sh" \
|
||||||
"$REPO_ROOT/scripts/check-release-needed.sh"; do
|
"$REPO_ROOT/scripts/check-release-needed.sh" \
|
||||||
|
"$REPO_ROOT/tests/run-tests.sh"; do
|
||||||
FOUND16="$(unguarded_expansions "$BASH32_SCRIPT")"
|
FOUND16="$(unguarded_expansions "$BASH32_SCRIPT")"
|
||||||
if [[ -n "$FOUND16" ]]; then
|
if [[ -n "$FOUND16" ]]; then
|
||||||
HAZARDS16+="$FOUND16 "
|
HAZARDS16+="$FOUND16 "
|
||||||
fi
|
fi
|
||||||
|
# `mapfile`/`readarray` are bash 4.0+ builtins with no 3.2 fallback. Whole-line
|
||||||
|
# comments are blanked first so prose naming the builtin is not a hit.
|
||||||
|
FOUND16B="$(awk '{ if ($0 ~ /^[[:space:]]*#/) print ""; else print }' "$BASH32_SCRIPT" \
|
||||||
|
| grep -nE '(^|[^[:alnum:]_])(mapfile|readarray)[[:space:]]' || true)"
|
||||||
|
if [[ -n "$FOUND16B" ]]; then
|
||||||
|
HAZARDS16+="${BASH32_SCRIPT##*/}:$FOUND16B "
|
||||||
|
fi
|
||||||
done
|
done
|
||||||
if [[ -n "$HAZARDS16" ]]; then
|
if [[ -n "$HAZARDS16" ]]; then
|
||||||
fail "unguarded array expansion(s) abort on bash < 4.4 under set -u: $(echo "$HAZARDS16" | tr '\n' ' ')"
|
fail "unguarded array expansion(s) abort on bash < 4.4 under set -u: $(echo "$HAZARDS16" | tr '\n' ' ')"
|
||||||
@@ -588,10 +599,15 @@ make_form_fixture() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Alert text with the `line:col` prefix and ANSI colouring stripped, sorted.
|
# Alert text with the `line:col` prefix and ANSI colouring stripped, sorted.
|
||||||
|
# The `|| true` matters under this file's `set -o pipefail`: a report with no
|
||||||
|
# alerts at all makes grep exit 1, which would abort the whole run inside the
|
||||||
|
# command substitutions below — silently, before the empty-baseline guard could
|
||||||
|
# print anything. Returning empty output instead is what makes that guard
|
||||||
|
# reachable.
|
||||||
alert_text() {
|
alert_text() {
|
||||||
echo "$1" \
|
echo "$1" \
|
||||||
| sed -E 's/\x1b\[[0-9;]*m//g' \
|
| sed -E 's/\x1b\[[0-9;]*m//g' \
|
||||||
| grep -oE '(error|warning|suggestion)[[:space:]]+.*' \
|
| { grep -oE '(error|warning|suggestion)[[:space:]]+.*' || true; } \
|
||||||
| sed -E 's/[[:space:]]+/ /g' \
|
| sed -E 's/[[:space:]]+/ /g' \
|
||||||
| sort
|
| sort
|
||||||
}
|
}
|
||||||
@@ -601,29 +617,35 @@ echo "--- every multi-line description form reports what its single-line form re
|
|||||||
FIXTURE19_SINGLE="$(make_form_fixture single)"
|
FIXTURE19_SINGLE="$(make_form_fixture single)"
|
||||||
BASELINE19="$(alert_text "$(run_wrap "$FIXTURE19_SINGLE" --config "$VALE_CONFIG" "$REL_SKILL19")")"
|
BASELINE19="$(alert_text "$(run_wrap "$FIXTURE19_SINGLE" --config "$VALE_CONFIG" "$REL_SKILL19")")"
|
||||||
if [[ -z "$BASELINE19" ]]; then
|
if [[ -z "$BASELINE19" ]]; then
|
||||||
fail "the single-line baseline reported nothing — the comparison below would be vacuous"
|
# The loop below has to be skipped, not merely reported on: an empty baseline
|
||||||
|
# compares equal to five empty results, so it would print five vacuous PASSes
|
||||||
|
# alongside this one FAIL. The FAIL alone still fails the run at the end.
|
||||||
|
fail "the single-line baseline reported nothing — the comparisons below would be vacuous, so they are skipped"
|
||||||
|
else
|
||||||
|
for FORM19 in folded plain dquote squote keyonly; do
|
||||||
|
DIR19="$(make_form_fixture "$FORM19")"
|
||||||
|
BARE19="$(cd "$DIR19" && vale --config "$VALE_CONFIG" "$REL_SKILL19" 2>&1 || true)"
|
||||||
|
GOT19="$(alert_text "$(run_wrap "$DIR19" --config "$VALE_CONFIG" "$REL_SKILL19")")"
|
||||||
|
if echo "$BARE19" | grep -q "VagueWording"; then
|
||||||
|
fail "bare vale already flags the $FORM19 form, so this case can't detect a silently-skipped flattening"
|
||||||
|
elif [[ "$GOT19" == "$BASELINE19" ]]; then
|
||||||
|
pass "a $FORM19 multi-line description reports the same alerts as its single-line form"
|
||||||
|
else
|
||||||
|
fail "a $FORM19 multi-line description diverged from its single-line form: got [$GOT19]"
|
||||||
|
fi
|
||||||
|
done
|
||||||
fi
|
fi
|
||||||
for FORM19 in folded plain dquote squote keyonly; do
|
|
||||||
DIR19="$(make_form_fixture "$FORM19")"
|
|
||||||
BARE19="$(cd "$DIR19" && vale --config "$VALE_CONFIG" "$REL_SKILL19" 2>&1 || true)"
|
|
||||||
GOT19="$(alert_text "$(run_wrap "$DIR19" --config "$VALE_CONFIG" "$REL_SKILL19")")"
|
|
||||||
if echo "$BARE19" | grep -q "VagueWording"; then
|
|
||||||
fail "bare vale already flags the $FORM19 form, so this case can't detect a silently-skipped flattening"
|
|
||||||
elif [[ "$GOT19" == "$BASELINE19" ]]; then
|
|
||||||
pass "a $FORM19 multi-line description reports the same alerts as its single-line form"
|
|
||||||
else
|
|
||||||
fail "a $FORM19 multi-line description diverged from its single-line form: got [$GOT19]"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# --- 20. A style token containing an ASCII apostrophe matches inside a
|
# --- 20. A style token containing an ASCII apostrophe matches inside a
|
||||||
# flattened description. The flattener used to substitute U+2019 for every `'`
|
# flattened description. The flattener used to substitute U+2019 for every `'`
|
||||||
# before writing the scratch copy, so no rule whose token carried an apostrophe
|
# before writing the scratch copy, so no rule whose token carried an apostrophe
|
||||||
# could ever fire on a flattened description — a silent, rule-shaped blind spot.
|
# could ever fire on a flattened description — a silent, rule-shaped blind spot.
|
||||||
# Both branches that can hold an apostrophe verbatim are exercised: a value that
|
# All three branches that can hold an apostrophe are exercised: a value that is
|
||||||
# is safe unquoted, and one that must be quoted (it contains `: `) and so has to
|
# safe unquoted; one that must be quoted (it contains `: `) and so lands in a
|
||||||
# land in a double-quoted scalar, since a single-quoted one would need the `''`
|
# double-quoted scalar, since a single-quoted one would need the `''` escape
|
||||||
# escape that kills the scope outright.
|
# that kills the scope outright; and one that also holds a double quote, which
|
||||||
|
# no inline scalar can spell verbatim and which therefore lands in a `|-`
|
||||||
|
# literal block.
|
||||||
echo ""
|
echo ""
|
||||||
echo "--- a style token containing an apostrophe matches in a flattened description ---"
|
echo "--- a style token containing an apostrophe matches in a flattened description ---"
|
||||||
APOS_STYLE="$(mktemp -d)"
|
APOS_STYLE="$(mktemp -d)"
|
||||||
@@ -638,6 +660,17 @@ ignorecase: true
|
|||||||
tokens:
|
tokens:
|
||||||
- "user's task"
|
- "user's task"
|
||||||
EOF
|
EOF
|
||||||
|
# A body-scoped companion rule, used by case 20b to read back the line number of
|
||||||
|
# a line *after* the frontmatter — the only way to catch the blank-line pad
|
||||||
|
# being off in either direction.
|
||||||
|
cat > "$APOS_STYLE/styles/Apostrophe/Body.yml" <<'EOF'
|
||||||
|
extends: existence
|
||||||
|
message: "body token: '%s'"
|
||||||
|
level: error
|
||||||
|
scope: text
|
||||||
|
tokens:
|
||||||
|
- flattening marker phrase
|
||||||
|
EOF
|
||||||
cat > "$APOS_STYLE/.vale.ini" <<'EOF'
|
cat > "$APOS_STYLE/.vale.ini" <<'EOF'
|
||||||
StylesPath = styles
|
StylesPath = styles
|
||||||
|
|
||||||
@@ -668,7 +701,23 @@ Body.
|
|||||||
EOF
|
EOF
|
||||||
)"
|
)"
|
||||||
new_fixture "$FIXTURE20_QUOTED"
|
new_fixture "$FIXTURE20_QUOTED"
|
||||||
for CASE20 in "unquoted:$FIXTURE20_PLAIN" "double-quoted:$FIXTURE20_QUOTED"; do
|
# Needs quoting (`: `), holds an apostrophe AND a double quote — the one
|
||||||
|
# combination no inline scalar can carry, so this is the `|-` literal-block
|
||||||
|
# branch. The VagueWording tokens are there for case 20b, which reuses it.
|
||||||
|
FIXTURE20_BLOCK="$(make_raw_fixture <<'EOF'
|
||||||
|
---
|
||||||
|
name: zzzskill
|
||||||
|
description: >
|
||||||
|
Triggers on: the user's task and "audit this" phrasing, which helps
|
||||||
|
with and utilize things across a second physical line.
|
||||||
|
---
|
||||||
|
|
||||||
|
Body carrying a flattening marker phrase for the line-number check.
|
||||||
|
EOF
|
||||||
|
)"
|
||||||
|
new_fixture "$FIXTURE20_BLOCK"
|
||||||
|
for CASE20 in "unquoted:$FIXTURE20_PLAIN" "double-quoted:$FIXTURE20_QUOTED" \
|
||||||
|
"literal-block:$FIXTURE20_BLOCK"; do
|
||||||
if run_wrap "${CASE20#*:}" --config "$APOS_STYLE/.vale.ini" "$REL_SKILL19" \
|
if run_wrap "${CASE20#*:}" --config "$APOS_STYLE/.vale.ini" "$REL_SKILL19" \
|
||||||
| grep -q "Apostrophe.Token"; then
|
| grep -q "Apostrophe.Token"; then
|
||||||
pass "an apostrophe-bearing token matches in a flattened ${CASE20%%:*} description"
|
pass "an apostrophe-bearing token matches in a flattened ${CASE20%%:*} description"
|
||||||
@@ -677,29 +726,31 @@ for CASE20 in "unquoted:$FIXTURE20_PLAIN" "double-quoted:$FIXTURE20_QUOTED"; do
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
# --- 20b. The one combination no verbatim YAML scalar can carry — needs
|
# --- 20b. The `|-` literal-block branch that case 20 just proved lossless must
|
||||||
# quoting, holds an apostrophe, and holds a double quote — falls back to the
|
# also keep the rest of the scope working and keep the line accounting right.
|
||||||
# lossy U+2019 substitution. Apostrophe-bearing tokens are lost there by
|
# The block is 2 physical lines where every inline form is 1, so the blank-line
|
||||||
# design, but the scope must stay alive so every other rule still fires.
|
# pad that preserves later line numbers has to drop by one. The second
|
||||||
|
# assertion pins that arithmetic against the body line's true number: case 3's
|
||||||
|
# `<= original line count` bound would not, since a pad that is one line short
|
||||||
|
# shifts every later line *up*, staying inside the bound while still lying.
|
||||||
echo ""
|
echo ""
|
||||||
echo "--- the unrepresentable combination keeps the description scope alive ---"
|
echo "--- the |- literal-block fallback lints normally and preserves line numbers ---"
|
||||||
FIXTURE20C="$(make_raw_fixture <<'EOF'
|
OUT20B=$(run_wrap "$FIXTURE20_BLOCK" --config "$VALE_CONFIG" "$REL_SKILL19")
|
||||||
---
|
if echo "$OUT20B" | grep -q "VagueWording"; then
|
||||||
name: zzzskill
|
|
||||||
description: >
|
|
||||||
Triggers on: the user's "audit this" phrasing, which helps with
|
|
||||||
and utilize things across a second physical line.
|
|
||||||
---
|
|
||||||
|
|
||||||
Body.
|
|
||||||
EOF
|
|
||||||
)"
|
|
||||||
new_fixture "$FIXTURE20C"
|
|
||||||
if run_wrap "$FIXTURE20C" --config "$VALE_CONFIG" "$REL_SKILL19" | grep -q "VagueWording"; then
|
|
||||||
pass "a description needing quotes with both an apostrophe and a double quote is still linted"
|
pass "a description needing quotes with both an apostrophe and a double quote is still linted"
|
||||||
else
|
else
|
||||||
fail "a description needing quotes with both an apostrophe and a double quote produced no alerts"
|
fail "a description needing quotes with both an apostrophe and a double quote produced no alerts"
|
||||||
fi
|
fi
|
||||||
|
WANT20B_LINE="$(grep -n 'flattening marker phrase' "$FIXTURE20_BLOCK/$REL_SKILL19" | cut -d: -f1)"
|
||||||
|
# `--output line` prints `file:line:col:Rule:message`, so the line number reads
|
||||||
|
# back without any wrapping or colour to strip.
|
||||||
|
GOT20B_LINE="$(run_wrap "$FIXTURE20_BLOCK" --config "$APOS_STYLE/.vale.ini" --output line "$REL_SKILL19" \
|
||||||
|
| grep 'Apostrophe.Body' | head -1 | cut -d: -f2)"
|
||||||
|
if [[ "$GOT20B_LINE" == "$WANT20B_LINE" ]]; then
|
||||||
|
pass "a body line after a |- flattened description keeps its original line number ($WANT20B_LINE)"
|
||||||
|
else
|
||||||
|
fail "the |- block's blank-line pad shifted the body: vale reported line $GOT20B_LINE, the file has it at $WANT20B_LINE"
|
||||||
|
fi
|
||||||
|
|
||||||
# --- 21. A symlinked file inside a directory argument is mirrored and linted.
|
# --- 21. A symlinked file inside a directory argument is mirrored and linted.
|
||||||
# Vale follows symlinks (both a symlinked file and a file under a symlinked
|
# Vale follows symlinks (both a symlinked file and a file under a symlinked
|
||||||
@@ -766,6 +817,31 @@ else
|
|||||||
fail "a typo'd path exited $RC23 but the message does not name it: $OUT23"
|
fail "a typo'd path exited $RC23 but the message does not name it: $OUT23"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# --- 24. `--output`'s built-in style names must not be path-absolutized. The
|
||||||
|
# wrapper rewrites path-valued flag values to absolute form so they still
|
||||||
|
# resolve after the `cd` into the scratch mirror, deciding with an `-e`
|
||||||
|
# existence test — but `line`, `JSON` and `CLI` are style names, not paths. With
|
||||||
|
# a file or directory of that name sitting in the caller's cwd the test hit, the
|
||||||
|
# built-in became `$cwd/line`, and vale flipped into template mode and died with
|
||||||
|
# `E100 [template] Runtime error` where bare vale prints a normal report.
|
||||||
|
echo ""
|
||||||
|
echo "--- a built-in --output style name survives a same-named entry in the cwd ---"
|
||||||
|
FIXTURE24="$(make_fixture 2)"
|
||||||
|
new_fixture "$FIXTURE24"
|
||||||
|
mkdir -p "$FIXTURE24/line"
|
||||||
|
: > "$FIXTURE24/JSON"
|
||||||
|
for FORM24 in "--output line" "--output=line" "--output JSON" "--output=JSON"; do
|
||||||
|
# shellcheck disable=SC2086 # deliberate word splitting of the argv fixture
|
||||||
|
OUT24="$(run_wrap "$FIXTURE24" --config "$VALE_CONFIG" $FORM24 "$REL_SKILL19")"
|
||||||
|
if echo "$OUT24" | grep -q "E100"; then
|
||||||
|
fail "'$FORM24' was rewritten to a cwd path and vale flipped into template mode — the bug this test guards against"
|
||||||
|
elif echo "$OUT24" | grep -q "VagueWording"; then
|
||||||
|
pass "'$FORM24' is passed through as a built-in style name"
|
||||||
|
else
|
||||||
|
fail "'$FORM24' produced no alert: $OUT24"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "Results: $PASS passed, $FAIL failed"
|
echo "Results: $PASS passed, $FAIL failed"
|
||||||
[[ $FAIL -eq 0 ]]
|
[[ $FAIL -eq 0 ]]
|
||||||
|
|||||||
Reference in New Issue
Block a user