from __future__ import annotations
import re
import sys
from dataclasses import dataclass
from pathlib import Path
@dataclass(frozen=True)
class Rule:
name: str
pattern: re.Pattern[str]
hint: str
platform_scoped: bool = False
RULES: list[Rule] = [
Rule(
"abs-posix-path",
re.compile(r'"(?:/(?:tmp|etc|usr|var|opt|home|Users|Library|proc|dev)\b|~/)'),
"absolute POSIX path literal — resolve through core::config paths or std::env::temp_dir()",
),
Rule(
"embedded-separator",
re.compile(r'\.join\(\s*"[^"]*/[^"]*"\s*\)'),
"path separator inside a join() segment — chain .join() per segment instead",
),
Rule(
"home-env-read",
re.compile(r'env::var(?:_os)?\(\s*"(?:HOME|USERPROFILE|APPDATA|XDG_[A-Z_]+)"'),
"raw home/config env read — go through the single resolver (dirs::* behind core::config)",
platform_scoped=True,
),
]
POSIX_CFG = re.compile(
r'(?:\bunix\b|target_family\s*=\s*"unix"|target_os\s*=\s*"(?:linux|macos|ios|android|freebsd)")'
)
NOT_WINDOWS_CFG = re.compile(r'not\s*\(\s*(?:windows|target_os\s*=\s*"windows")\s*\)')
WINDOWS_CFG = re.compile(r'(?:\bwindows\b|target_os\s*=\s*"windows")')
ATTR = re.compile(r"^\s*#!?\[")
CFG_ATTR = re.compile(r"^\s*#!?\[\s*cfg\s*\(")
WAIVER = re.compile(r"portability-ok:\s*\S")
LITERALS = re.compile(
r'r#+"(?:.|\n)*?"#+' r'|"(?:\\.|[^"\\])*"' r"|'(?:\\.|[^'\\])'" )
LINE_COMMENT = re.compile(r"//.*$")
@dataclass
class Violation:
path: Path
line: int
rule: str
text: str
hint: str
def strip_noise(line: str) -> str:
return LINE_COMMENT.sub("", LITERALS.sub('""', line))
def cfg_is_posix_only(expr: str) -> bool:
return bool(NOT_WINDOWS_CFG.search(expr) or POSIX_CFG.search(expr))
def skipped_lines(lines: list[str]) -> tuple[set[int], set[int]]:
always: set[int] = set()
platform: set[int] = set()
for i, raw in enumerate(lines):
if not CFG_ATTR.match(raw):
continue
expr, end = raw, i
while expr.count("(") > expr.count(")") and end + 1 < len(lines):
end += 1
expr += lines[end]
is_test = re.search(r"\bcfg\s*\(\s*test\s*\)", expr) is not None
posix_only = is_test or cfg_is_posix_only(expr)
windows_only = not posix_only and WINDOWS_CFG.search(expr) is not None
if not (posix_only or windows_only):
continue
target = always if posix_only else platform
if raw.lstrip().startswith("#!["):
target.update(range(i, len(lines)))
continue
target.update(range(i, end + 1))
target.update(item_span(lines, end + 1))
always |= raw_string_lines(lines)
return always, always | platform
def raw_string_lines(lines: list[str]) -> set[int]:
inside: set[int] = set()
open_at: int | None = None
for i, line in enumerate(lines):
if open_at is None:
if re.search(r'r#+"', LITERALS.sub('""', line)):
open_at = i
elif re.search(r'"#+', line):
inside.update(range(open_at, i + 1))
open_at = None
return inside
def item_span(lines: list[str], start: int) -> range:
j = start
while j < len(lines) and (ATTR.match(lines[j]) or lines[j].lstrip().startswith("///")):
j += 1
depth = 0
opened = False
for k in range(j, len(lines)):
clean = strip_noise(lines[k])
depth += clean.count("{") - clean.count("}")
if "{" in clean:
opened = True
if opened and depth <= 0:
return range(start, k + 1)
if not opened and clean.rstrip().endswith(";"):
return range(start, k + 1)
return range(start, len(lines))
def scan(path: Path) -> list[Violation]:
lines = path.read_text(encoding="utf-8").splitlines()
if any(WAIVER.search(ln) for ln in lines[:40] if ln.lstrip().startswith("//!")):
return []
skip_all, skip_platform = skipped_lines(lines)
out: list[Violation] = []
for i, raw in enumerate(lines):
if i in skip_all:
continue
stripped = raw.lstrip()
if stripped.startswith("//") or WAIVER.search(raw):
continue
for rule in RULES:
if rule.platform_scoped and i in skip_platform:
continue
if rule.pattern.search(raw):
out.append(Violation(path, i + 1, rule.name, raw.strip(), rule.hint))
break
return out
def main(argv: list[str]) -> int:
root = Path(argv[1]) if len(argv) > 1 else Path.cwd()
if not root.is_dir():
print(f"error: {root} is not a directory", file=sys.stderr)
return 2
targets = sorted(
p for p in (root / "src").rglob("*.rs") if "generated" not in p.relative_to(root).parts
)
if (root / "build.rs").exists():
targets.append(root / "build.rs")
violations = [v for path in targets for v in scan(path)]
if not violations:
print(f"portability: clean ({len(targets)} files scanned)")
return 0
print("::error::POSIX-only assumptions in cross-platform code:\n")
for v in violations:
rel = v.path.relative_to(root)
print(f" {rel}:{v.line} [{v.rule}]")
print(f" {v.text}")
print(f" → {v.hint}\n")
print(f"{len(violations)} violation(s).")
print(
"Fix them, move the code under a POSIX-only cfg, or append "
"`// portability-ok: <reason>` when the path really is POSIX-only."
)
return 1
if __name__ == "__main__":
sys.exit(main(sys.argv))