import argparse
import json
import re
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
MAX_LINES = 10
STATEMENT_BLOCK = re.compile(
r"""(
\bfn\b.*\{$ | ^\s*(\}\s*else\s+)?(if|while|for|loop)\b.*\{$ | \belse\s*\{$ | ^\s*(unsafe\s+|async\s+(move\s+)?)?\{$ | =>\s*(unsafe\s+)?\{$ | \|[^|]*\|(\s*->\s*[^{]+)?\s*\{$ | \bmove\s*\{$
)""",
re.VERBOSE,
)
CLOSERS = ("}", ")", "]")
HEADER = re.compile(r"^\s*((pub(\([^)]*\))?\s+)?((const|async|unsafe|extern\s+\"[^\"]*\")\s+)*fn\b"
r"|(\}\s*else\s+)?(if|while|for)\b)")
FUNCTION = re.compile(r"^\s*(pub(\([^)]*\))?\s+)?((const|async|unsafe|extern\s+\"[^\"]*\")\s+)*fn\b")
def indent(line):
return len(line) - len(line.lstrip(" "))
def scan(line, state):
code, index = [], 0
while index < len(line):
if state is not None:
if state == "" and line[index] == "\\":
index += 2
continue
if line.startswith('"' + state[1:], index) if state else line[index] == '"':
index += len(state) if state else 1
state = None
code.append('""')
continue
index += 1
continue
if line.startswith("//", index):
break
char = re.match(r"'(\\.|[^\\'])'", line[index:])
if char:
code.append("' '")
index += len(char.group(0))
continue
raw = re.match(r'b?r(#*)"', line[index:])
if raw and (index == 0 or not (line[index - 1].isalnum() or line[index - 1] == "_")):
state = '"' + raw.group(1)
index += len(raw.group(0))
continue
if line[index] == '"':
state = ""
index += 1
continue
code.append(line[index])
index += 1
return "".join(code).rstrip(), state
def code_part(line):
return scan(line, None)[0]
def paragraphs(lines, base_indent=None):
found, functions = [], []
stack = []
if base_indent is not None:
stack.append([base_indent, None, 0, 0])
def close(block):
if block[1] is not None:
found.append((block[1], block[2], block[3]))
block[1:] = [None, 0, 0]
pending_opener = None
pending_header = None
blank = False
string = None
for number, line in enumerate(lines):
if string is not None:
for block in stack:
if block[1] is not None:
block[2] += 1
string = scan(line, string)[1]
continue
if not line.strip():
blank = True
continue
here = indent(line)
code, string = scan(line, None)
if pending_opener is not None:
opener_indent = pending_opener
pending_opener = None
if here == opener_indent + 4:
stack.append([here, None, 0, 0])
blank = False
while stack and here < stack[-1][0]:
close(stack.pop())
if blank:
if stack and stack[-1][0] == here:
close(stack[-1])
blank = False
for block in stack:
if block[1] is None:
block[1] = number
block[2] += 1
top = stack[-1] if stack else None
if top and here == top[0] and code and code.strip() != "{" and not code.lstrip().startswith(CLOSERS) \
and not line.lstrip().startswith(("//", "#[", "#!")):
top[3] += 1
if FUNCTION.match(code) and not code.endswith(";"):
functions.append(number)
if code and STATEMENT_BLOCK.search(code) and not re.search(r"\bmatch\b", code.split("=>")[-1]):
pending_opener = here
pending_header = None
elif pending_header is not None and here == pending_header:
if code.endswith("{"):
pending_opener = here
if code.endswith(("{", ";", "}")):
pending_header = None
elif code and HEADER.match(code) and not code.endswith((";", "}")):
pending_header = here
while stack:
close(stack.pop())
return found, functions
def flagged(lines, base_indent=None):
found, functions = paragraphs(lines, base_indent)
return [(start, count, statements) for start, count, statements in found
if count > MAX_LINES and statements >= 2], functions
def fenced_rust(text):
lines = text.splitlines()
index = 0
while index < len(lines):
match = re.match(r"^(\s*)```rust\b", lines[index])
if match:
end = index + 1
while end < len(lines) and not lines[end].strip().startswith("```"):
end += 1
yield index + 1, lines[index + 1:end]
index = end
index += 1
def doc_examples(text):
lines = text.splitlines()
block, start = None, 0
for number, line in enumerate(lines):
match = re.match(r"^\s*(///|//!) ?(.*)$", line)
if not match:
block = None
continue
body = match.group(2)
if body.lstrip().startswith("```"):
if block is None:
info = body.strip()[3:]
rust = info == "" or "rust" in info or info.split(",")[0] in (
"no_run", "ignore", "should_panic", "compile_fail", "edition2021")
block, start = ([] if rust else None), number + 1
if not rust:
block = "skip"
else:
if block != "skip":
yield start, block
block = None
elif block is not None and block != "skip":
hidden = re.match(r"^#(\s|$)", body.lstrip())
block.append(None if hidden else body)
return
def visible(lines):
return [line for line in lines if line is not None]
SURFACES = [
("website guides", ["site/src/content/docs/**/*.md", "site/src/content/docs/**/*.mdx"], "markdown"),
("repository docs", ["*.md", "docs/**/*.md"], "markdown"),
("Rustdoc examples", ["src/**/*.rs", "girt-cli/src/**/*.rs"], "rustdoc"),
("examples", ["examples/**/*.rs"], "rust"),
("library source", ["src/**/*.rs"], "rust"),
("CLI source", ["girt-cli/src/**/*.rs"], "rust"),
("tests and benches", ["tests/**/*.rs", "benches/**/*.rs", "girt-cli/tests/**/*.rs"], "rust"),
]
def measure(prefixes):
surfaces = []
for name, patterns, kind in SURFACES:
files = sorted({path for pattern in patterns for path in ROOT.glob(pattern)})
files = [p for p in files if not prefixes or any(
str(p.relative_to(ROOT)).startswith(prefix.rstrip("/")) for prefix in prefixes)]
items = []
total = 0
dense = set()
for path in files:
relative = str(path.relative_to(ROOT))
text = path.read_text()
if kind == "markdown":
for start, lines in fenced_rust(text):
total += 1
found = flagged(lines, 0)[0]
items += [(relative, start + s + 1, c, n) for s, c, n in found]
dense |= {(relative, start)} if found else set()
elif kind == "rustdoc":
for start, lines in doc_examples(text):
total += 1
shown = visible(lines)
source_lines = [start + i for i, line in enumerate(lines) if line is not None]
found = flagged(shown, 0)[0]
items += [(relative, source_lines[s] + 1, c, n) for s, c, n in found]
dense |= {(relative, start)} if found else set()
else:
found, functions = flagged(text.splitlines())
total += len(functions)
for s, c, n in found:
items.append((relative, s + 1, c, n))
dense.add((relative, max((f for f in functions if f < s), default=-1)))
surfaces.append({"surface": name, "blocks": total, "dense_blocks": len(dense), "flagged": len(items),
"paragraphs": sorted(items, key=lambda item: -item[2])})
return surfaces
def main():
parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
parser.add_argument("prefixes", nargs="*", help="only measure paths under these prefixes")
parser.add_argument("--list", action="store_true", help="list every flagged paragraph")
parser.add_argument("--json", action="store_true", help="print a JSON summary")
args = parser.parse_args()
surfaces = measure(args.prefixes)
if args.json:
print(json.dumps([{**s, "paragraphs": [f"{f}:{n} ({c} lines, {k} statements)"
for f, n, c, k in s["paragraphs"]]}
for s in surfaces], indent=2))
return
print(f"Paragraphs over {MAX_LINES} lines with two or more statements:")
for surface in surfaces:
unit = "blocks" if surface["surface"] in ("website guides", "repository docs", "Rustdoc examples") else "functions"
print(f" {surface['surface']:<18} {surface['flagged']:>5} paragraphs in {surface['dense_blocks']:>5} of "
f"{surface['blocks']} {unit}")
if args.list:
for file, line, count, statements in surface["paragraphs"]:
print(f" {file}:{line} {count} lines, {statements} statements")
if __name__ == "__main__":
main()