Skip to content

Commit 5b8ae65

Browse files
committed
fix(ci): make invisible scan byte-safe
1 parent fc53655 commit 5b8ae65

1 file changed

Lines changed: 71 additions & 25 deletions

File tree

‎.github/workflows/dogfood-gate.yml‎

Lines changed: 71 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -115,37 +115,83 @@ jobs:
115115
# Inline invisible character detection (from empty-linter's core patterns).
116116
# Checks for: zero-width spaces, zero-width joiners, BOM, soft hyphens,
117117
# non-breaking spaces, null bytes, and other invisible Unicode in source files.
118-
set +e
119-
PATTERNS='(*UTF)[\x00-\x08\x0B\x0C\x0E-\x1F\x{a0}\x{ad}\x{200b}-\x{200f}\x{202a}-\x{202f}\x{2060}\x{2066}-\x{2069}\x{feff}]'
120-
find "$GITHUB_WORKSPACE" \
121-
-not -path '*/.git/*' -not -path '*/node_modules/*' \
122-
-not -path '*/.deno/*' -not -path '*/target/*' \
123-
-not -path '*/_build/*' -not -path '*/deps/*' \
124-
-not -path '*/external_corpora/*' -not -path '*/.lake/*' \
125-
-type f \( -name '*.rs' -o -name '*.ex' -o -name '*.exs' -o -name '*.res' \
126-
-o -name '*.js' -o -name '*.ts' -o -name '*.json' -o -name '*.toml' \
127-
-o -name '*.yml' -o -name '*.yaml' -o -name '*.md' -o -name '*.adoc' \
128-
-o -name '*.idr' -o -name '*.zig' -o -name '*.v' -o -name '*.jl' \
129-
-o -name '*.gleam' -o -name '*.hs' -o -name '*.ml' -o -name '*.sh' \) \
130-
-exec grep -aPl "$PATTERNS" {} + > /tmp/empty-lint-results.txt 2>/dev/null
131-
EL_EXIT=$?
132-
set -e
118+
python3 - <<'PY'
119+
import os
120+
from pathlib import Path
133121
134-
FINDINGS=$(wc -l < /tmp/empty-lint-results.txt 2>/dev/null || echo 0)
135-
echo "findings=$FINDINGS" >> "$GITHUB_OUTPUT"
136-
echo "exit_code=$EL_EXIT" >> "$GITHUB_OUTPUT"
137-
echo "ready=true" >> "$GITHUB_OUTPUT"
122+
root = Path(os.environ["GITHUB_WORKSPACE"])
123+
skipped_dirs = {
124+
".git", ".deno", ".lake", "_build", "deps",
125+
"external_corpora", "node_modules", "target",
126+
}
127+
source_suffixes = {
128+
".adoc", ".ex", ".exs", ".gleam", ".hs", ".idr", ".jl",
129+
".js", ".json", ".md", ".ml", ".res", ".rs", ".sh",
130+
".toml", ".ts", ".v", ".yaml", ".yml", ".zig",
131+
}
132+
invisible_codepoints = {
133+
0x00A0, 0x00AD, 0x2060, 0xFEFF,
134+
*range(0x200B, 0x2010),
135+
*range(0x202A, 0x2030),
136+
*range(0x2066, 0x206A),
137+
}
138138
139-
# Emit annotations for each file with invisible chars
140-
while IFS= read -r filepath; do
141-
[ -z "$filepath" ] && continue
142-
REL_PATH="${filepath#$GITHUB_WORKSPACE/}"
143-
echo "::warning file=${REL_PATH}::Invisible Unicode characters detected (zero-width space, BOM, NBSP, etc.)"
144-
done < /tmp/empty-lint-results.txt
139+
def annotation_escape(value):
140+
return str(value).replace("%", "%25").replace("\r", "%0D").replace("\n", "%0A")
141+
142+
findings = []
143+
errors = []
144+
for path in root.rglob("*"):
145+
relative = path.relative_to(root)
146+
if (
147+
path.is_symlink()
148+
or not path.is_file()
149+
or path.suffix.lower() not in source_suffixes
150+
or any(part in skipped_dirs for part in relative.parts[:-1])
151+
):
152+
continue
153+
try:
154+
data = path.read_bytes()
155+
except OSError as error:
156+
errors.append((relative, f"could not read file: {error}"))
157+
continue
158+
159+
reasons = set()
160+
if data.startswith(b"\xef\xbb\xbf"):
161+
reasons.add("leading UTF-8 BOM")
162+
if any(byte <= 0x08 or byte in (0x0B, 0x0C) or 0x0E <= byte <= 0x1F for byte in data):
163+
reasons.add("C0 control character")
164+
try:
165+
text_content = data.decode("utf-8", errors="strict")
166+
except UnicodeDecodeError as error:
167+
errors.append((relative, f"invalid UTF-8 at byte {error.start}"))
168+
continue
169+
if any(ord(character) in invisible_codepoints for character in text_content):
170+
reasons.add("invisible Unicode code point")
171+
if reasons:
172+
findings.append((relative, ", ".join(sorted(reasons))))
173+
174+
with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as output:
175+
output.write(f"findings={len(findings)}\n")
176+
output.write(f"exit_code={2 if errors else 0}\n")
177+
output.write("ready=true\n")
178+
179+
for relative, reasons in findings:
180+
print(f"::warning file={annotation_escape(relative)}::Invisible characters detected: {reasons}")
181+
for relative, reason in errors:
182+
print(f"::error file={annotation_escape(relative)}::Invisible-character scan failed: {annotation_escape(reason)}")
183+
PY
145184
- name: Write summary
146185
run: |
147186
if [ "${{ steps.lint.outputs.ready }}" = "true" ]; then
148187
FINDINGS="${{ steps.lint.outputs.findings }}"
188+
EXIT_CODE="${{ steps.lint.outputs.exit_code }}"
189+
if [ "$EXIT_CODE" -ne 0 ] 2>/dev/null; then
190+
echo "## Empty-Linter Results" >> "$GITHUB_STEP_SUMMARY"
191+
echo "" >> "$GITHUB_STEP_SUMMARY"
192+
echo ":x: Scanner execution failed; see error annotations above." >> "$GITHUB_STEP_SUMMARY"
193+
exit 1
194+
fi
149195
if [ "$FINDINGS" -gt 0 ] 2>/dev/null; then
150196
echo "## Empty-Linter Results" >> "$GITHUB_STEP_SUMMARY"
151197
echo "" >> "$GITHUB_STEP_SUMMARY"

0 commit comments

Comments
 (0)