You've already forked coreutils-tracking
mirror of
https://github.com/uutils/coreutils-tracking.git
synced 2026-06-10 16:12:04 -07:00
125 lines
3.8 KiB
Python
125 lines
3.8 KiB
Python
# This file is part of the uutils coreutils package.
|
|
#
|
|
# For the full copyright and license information, please view the LICENSE
|
|
# file that was distributed with this source code.
|
|
|
|
"""Count `unsafe` usage in a uutils/coreutils checkout at a given revision.
|
|
|
|
For each `.rs` file outside `vendor/` and `target/`, we bucket each line that
|
|
contains an `unsafe` keyword form into:
|
|
|
|
type buckets — one per syntactic kind:
|
|
blocks — `unsafe { ... }`
|
|
fn — `unsafe fn` items
|
|
impl — `unsafe impl`
|
|
trait — `unsafe trait`
|
|
extern — `unsafe extern`
|
|
attr — `#[unsafe(...)]` attributes (Rust 2024 edition)
|
|
|
|
location buckets — one per source area:
|
|
code — production source
|
|
test — paths under `tests/` or `fuzz/`
|
|
|
|
A line contributes to exactly one type bucket and exactly one location bucket.
|
|
`total = sum(types) = code + test`.
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
|
|
# Each pattern is anchored on `\bunsafe` and matches the keyword followed by
|
|
# its expected punctuation/keyword. Order matters — more specific matches
|
|
# (`fn`, `impl`, ...) before the catch-all `{`.
|
|
TYPE_PATTERNS: list[tuple[str, re.Pattern]] = [
|
|
("attr", re.compile(r"#\[unsafe\(")),
|
|
("fn", re.compile(r"\bunsafe\s+fn\b")),
|
|
("impl", re.compile(r"\bunsafe\s+impl\b")),
|
|
("trait", re.compile(r"\bunsafe\s+trait\b")),
|
|
("extern", re.compile(r"\bunsafe\s+extern\b")),
|
|
("blocks", re.compile(r"\bunsafe\s*\{")),
|
|
]
|
|
TYPES = [name for name, _ in TYPE_PATTERNS]
|
|
|
|
|
|
def git(repo: str, *args: str) -> str:
|
|
return subprocess.check_output(["git", "-C", repo, *args], text=True)
|
|
|
|
|
|
def is_test_path(path: str) -> bool:
|
|
parts = path.split("/")
|
|
return "tests" in parts or "fuzz" in parts
|
|
|
|
|
|
def count_at(repo: str, sha: str) -> dict[str, int]:
|
|
counts = {t: 0 for t in TYPES}
|
|
counts["code"] = 0
|
|
counts["test"] = 0
|
|
|
|
files = git(repo, "ls-tree", "-r", "--name-only", sha).splitlines()
|
|
rs_files = [
|
|
f
|
|
for f in files
|
|
if f.endswith(".rs")
|
|
and not f.startswith("vendor/")
|
|
and "/vendor/" not in f
|
|
and not f.startswith("target/")
|
|
and "/target/" not in f
|
|
]
|
|
if not rs_files:
|
|
return counts
|
|
|
|
for path in rs_files:
|
|
location = "test" if is_test_path(path) else "code"
|
|
try:
|
|
blob = git(repo, "show", f"{sha}:{path}")
|
|
except subprocess.CalledProcessError:
|
|
continue
|
|
for line in blob.splitlines():
|
|
stripped = line.lstrip()
|
|
if stripped.startswith("//") or stripped.startswith("*"):
|
|
continue
|
|
for type_name, regex in TYPE_PATTERNS:
|
|
if regex.search(line):
|
|
counts[type_name] += 1
|
|
counts[location] += 1
|
|
break
|
|
return counts
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("repo", help="path to a uutils/coreutils git checkout")
|
|
parser.add_argument(
|
|
"--sha",
|
|
default="HEAD",
|
|
help="commit to count at (default: HEAD)",
|
|
)
|
|
parser.add_argument(
|
|
"--date-format",
|
|
default="%ad",
|
|
help="git format string for the JSON key (default: %%ad — RFC2822-ish)",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
sha = git(args.repo, "rev-parse", args.sha).strip()
|
|
date = git(args.repo, "show", "-s", f"--format={args.date_format}", sha).strip()
|
|
|
|
counts = count_at(args.repo, sha)
|
|
entry = {
|
|
"sha": sha,
|
|
"total": str(counts["code"] + counts["test"]),
|
|
"code": str(counts["code"]),
|
|
"test": str(counts["test"]),
|
|
**{t: str(counts[t]) for t in TYPES},
|
|
}
|
|
json.dump({date: entry}, sys.stdout, indent=2)
|
|
sys.stdout.write("\n")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|