Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ examples/no-std-examples/target
examples/complex-examples/target
examples/patch-examples/target
examples/filler-examples/target
result
21 changes: 19 additions & 2 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,17 @@
(builtins.readFile ./nix/scripts/check-filler.sh);
checkPatchScript = pkgs.writeShellScriptBin "check-patch"
(builtins.readFile ./nix/scripts/check-patch.sh);
checkLeakScript = let
script = pkgs.writeText "check-feature-leak.py" (builtins.readFile ./nix/scripts/check-feature-leak.py);
in pkgs.writeShellScriptBin "check-feature-leak"
''
if [ $# -eq 0 ]; then
root="$(git rev-parse --show-toplevel 2>/dev/null)" ||
{ echo "check-feature-leak: not in a git repo; pass a path" >&2; exit 2; }
set -- "$root/derive/src"
fi
exec ${pkgs.python3}/bin/python3 ${script} "$@"
'';
testScript = pkgs.writeShellScriptBin "run-test"
(builtins.readFile ./nix/scripts/run-test.sh);
PROMPT = ''
Expand All @@ -40,10 +51,16 @@
in
{
devShells = {
default = import ./nix/shells/default.nix { inherit pkgs PROMPT; checkScripts = [ checkComplexScript checkFillerScript checkPatchScript testScript ]; };
ci = import ./nix/shells/ci.nix { inherit pkgs publishScript PROMPT; checkScripts = [ checkComplexScript checkFillerScript checkPatchScript testScript ]; };
default = import ./nix/shells/default.nix { inherit pkgs PROMPT; checkScripts = [ checkComplexScript checkFillerScript checkPatchScript testScript checkLeakScript ]; };
ci = import ./nix/shells/ci.nix { inherit pkgs publishScript PROMPT; checkScripts = [ checkComplexScript checkFillerScript checkPatchScript testScript checkLeakScript ]; };
no-std = import ./nix/shells/no-std.nix { inherit pkgs PROMPT; checkScripts = [ checkNoStdScript ]; };
};
checks = {
check-feature-leak = pkgs.runCommand "check-feature-leak" { src = self; } ''
${checkLeakScript}/bin/check-feature-leak $src/derive/src
mkdir -p $out
'';
};
}
);
}
166 changes: 166 additions & 0 deletions nix/scripts/check-feature-leak.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
#!/usr/bin/env python3
"""Check that no `#[cfg(feature = ...)]` appears inside a quote!{}/quote!() block.

`cfg` attributes inside generated code are evaluated with the features of the
crate that *contains* the derive, not the user crate, so they must be resolved
before quote!{}/quote!() and emitted via #(#cfg)* repetition instead.

Usage: check-feature-leak.py <PATH>... (e.g. derive/src)
Exit code 1 if any violation is found, 2 on usage errors.
"""

import sys
import pathlib

CFG_FEATURE = "#[cfg(feature"
QUOTE = "quote"


OPENERS = {"{": "}", "(": ")", "[": "]"}


def scan(text):
"""Yield (line_number, snippet) for each `#[cfg(feature` inside quote!{}/quote!()."""
findings = []
i, n = 0, len(text)
# Stack entries: (belongs_to_quote_block, expected_closer).
stack = []
quote_blocks = 0

def ident_start(idx):
return idx == 0 or not (text[idx - 1].isalnum() or text[idx - 1] in "_")

while i < n:
c = text[i]

# comments
if text.startswith("//", i):
i = text.find("\n", i)
i = n if i == -1 else i + 1
continue
if text.startswith("/*", i):
depth, i = 1, i + 2
while i < n and depth:
if text.startswith("/*", i):
depth, i = depth + 1, i + 2
elif text.startswith("*/", i):
depth, i = depth - 1, i + 2
else:
i += 1
continue

# string literals (also raw strings like r#"..."#)
if c == '"' or (
c == "r" and i + 1 < n and text[i + 1] in '#"' and ident_start(i)
):
if c == "r":
i += 1
hashes = 0
while i < n and text[i] == "#":
hashes, i = hashes + 1, i + 1
if i >= n or text[i] != '"': # not a raw string, plain ident
i += 1
continue
i += 1
while i < n:
if text[i] == "\\":
i += 2
elif text[i] == '"':
i += 1
break
else:
i += 1
continue

# char literals ('x', '\n') but not lifetimes ('a, '_)
if c == "'" and i + 1 < n and text[i + 1] in "\\(":
if text[i + 1] == "\\":
i += 4 # '\n' and friends
else:
close = text.find("}'", i + 2)
i = close + 1 if close != -1 else i + 3
continue

if text.startswith(QUOTE, i) and ident_start(i):
j = i + len(QUOTE)
while j < n and text[j].isspace():
j += 1
if j < n and text[j] == "!":
k = j + 1
while k < n and text[k].isspace():
k += 1
if k < n and text[k] in OPENERS:
stack.append((True, OPENERS[text[k]]))
quote_blocks += 1
i = k + 1
continue

if c in OPENERS:
stack.append((False, OPENERS[c]))
i += 1
continue
if c in "})]":
if stack:
is_quote, _ = stack.pop()
if is_quote:
quote_blocks -= 1
i += 1
continue

if text.startswith(CFG_FEATURE, i) and quote_blocks:
line = text.count("\n", 0, i) + 1
end = text.find("]", i)
snippet = text[i : (end + 1 if end != -1 else n)]
snippet = " ".join(snippet.split())[:80]
findings.append((line, snippet))
i += len(CFG_FEATURE)
continue

i += 1

return findings


def main():
if len(sys.argv) < 2:
print("usage: check-feature-leak.py <PATH>...", file=sys.stderr)
return 2
paths = [pathlib.Path(p) for p in sys.argv[1:]]

missing = [str(p) for p in paths if not p.exists()]
if missing:
print(
f"check-feature-leak.py: path(s) not found: {', '.join(missing)}",
file=sys.stderr,
)
return 2

files = []
for p in paths:
if p.is_dir():
files.extend(p.rglob("*.rs"))
else:
files.append(p)

violations = 0
for f in sorted(files):
try:
text = f.read_text()
except OSError as e:
print(f"{f}: {e}", file=sys.stderr)
continue
for line, snippet in scan(text):
print(f"{f}:{line}: `!! Feature leak detected: {snippet}")
violations += 1

if violations:
print(f"\n{violations} violation(s) found.", file=sys.stderr)
return 1
dirs = [str(p) for p in paths if p.is_dir()]
where = f" under {', '.join(dirs)}" if dirs else ""
print(f"OK: no feature leak detects in {len(files)} file(s){where}.")
return 0


if __name__ == "__main__":
sys.exit(main())
Loading