@@ -121,12 +121,20 @@ jobs:
121121
122122 root = Path(os.environ["GITHUB_WORKSPACE"])
123123 skipped_dirs = {
124- ".git", ".deno", ".lake", "_build", "deps",
125- "external_corpora", "node_modules", "target",
124+ ".cache", ".deno", ".elixir_ls", ".git", ".lake", ".zig-cache",
125+ "_build", "build", "coverage", "deps", "dist", "external_corpora",
126+ "node_modules", "out", "target", "vendor", "zig-cache", "zig-out",
127+ }
128+ intentional_fixture_dirs = {
129+ ("tests", "fixtures", "bom-detection"),
130+ ("tests", "fixtures", "empty-linter"),
126131 }
127132 source_suffixes = {
128- ".adoc", ".ex", ".exs", ".gleam", ".hs", ".idr", ".jl",
129- ".js", ".json", ".md", ".ml", ".res", ".rs", ".sh",
133+ ".adoc", ".adb", ".ads", ".agda", ".c", ".cc", ".clj", ".cljs",
134+ ".cpp", ".erl", ".ex", ".exs", ".fs", ".fsi", ".fsx", ".gleam",
135+ ".h", ".hh", ".hpp", ".hrl", ".hs", ".idr", ".java", ".jl",
136+ ".js", ".json", ".kt", ".kts", ".lean", ".lua", ".md", ".ml",
137+ ".php", ".r", ".rb", ".res", ".rs", ".scala", ".sh", ".swift",
130138 ".toml", ".ts", ".v", ".yaml", ".yml", ".zig",
131139 }
132140 invisible_codepoints = {
@@ -136,50 +144,62 @@ jobs:
136144 *range(0x2066, 0x206A),
137145 }
138146
139- def annotation_escape (value):
147+ def command_escape (value):
140148 return str(value).replace("%", "%25").replace("\r", "%0D").replace("\n", "%0A")
141149
150+ def property_escape(value):
151+ return command_escape(value).replace(":", "%3A").replace(",", "%2C")
152+
153+ # Runtime regression for GitHub workflow-command property delimiters.
154+ assert property_escape("docs/a,b::c.md") == "docs/a%2Cb%3A%3Ac.md"
155+
156+ def intentionally_invalid_fixture(relative):
157+ return any(relative.parts[:len(prefix)] == prefix for prefix in intentional_fixture_dirs)
158+
142159 findings = []
143160 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
161+ for directory, dirnames, filenames in os.walk(root, topdown=True):
162+ dirnames[:] = [name for name in dirnames if name not in skipped_dirs]
163+ directory_path = Path(directory)
164+ for filename in filenames:
165+ path = directory_path / filename
166+ relative = path.relative_to(root)
167+ if (
168+ path.is_symlink()
169+ or path.suffix.lower() not in source_suffixes
170+ or intentionally_invalid_fixture(relative)
171+ ):
172+ continue
173+ try:
174+ data = path.read_bytes()
175+ except OSError as error:
176+ errors.append((relative, f"could not read file: {error}"))
177+ continue
158178
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))))
179+ reasons = set()
180+ if data.startswith(b"\xef\xbb\xbf"):
181+ reasons.add("leading UTF-8 BOM")
182+ if any(byte <= 0x08 or byte in (0x0B, 0x0C) or 0x0E <= byte <= 0x1F for byte in data):
183+ reasons.add("C0 control character")
184+ try:
185+ text_content = data.decode("utf-8", errors="strict")
186+ except UnicodeDecodeError as error:
187+ errors.append((relative, f"invalid UTF-8 at byte {error.start}"))
188+ continue
189+ if any(ord(character) in invisible_codepoints for character in text_content):
190+ reasons.add("invisible Unicode code point")
191+ if reasons:
192+ findings.append((relative, ", ".join(sorted(reasons))))
173193
174194 with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as output:
175195 output.write(f"findings={len(findings)}\n")
176196 output.write(f"exit_code={2 if errors else 0}\n")
177197 output.write("ready=true\n")
178198
179199 for relative, reasons in findings:
180- print(f"::warning file={annotation_escape (relative)}::Invisible characters detected: {reasons}")
200+ print(f"::warning file={property_escape (relative)}::Invisible characters detected: {command_escape( reasons) }")
181201 for relative, reason in errors:
182- print(f"::error file={annotation_escape (relative)}::Invisible-character scan failed: {annotation_escape (reason)}")
202+ print(f"::error file={property_escape (relative)}::Invisible-character scan failed: {command_escape (reason)}")
183203 PY
184204 - name : Write summary
185205 run : |
@@ -196,6 +216,7 @@ jobs:
196216 echo "## Empty-Linter Results" >> "$GITHUB_STEP_SUMMARY"
197217 echo "" >> "$GITHUB_STEP_SUMMARY"
198218 echo "Found **${FINDINGS}** invisible character issue(s). See annotations above." >> "$GITHUB_STEP_SUMMARY"
219+ exit 1
199220 else
200221 echo "## Empty-Linter Results" >> "$GITHUB_STEP_SUMMARY"
201222 echo "" >> "$GITHUB_STEP_SUMMARY"
0 commit comments