返回 CodeWhale
check-reqwest-builders.py
根目录 / scripts / check-reqwest-builders.py
1 #!/usr/bin/env python3
2 """Forbid bare reqwest client constructors outside crates/release.
3
4 The workspace builds reqwest with `rustls-no-provider`, so a bare
5 `reqwest::Client::builder()` (or `::new()`, or the blocking counterparts)
6 panics in `default_rustls_crypto_provider` whenever it runs before any other
7 client has installed the crypto provider. Every client must therefore go
8 through `codewhale_release::tls` (`reqwest_client_builder()`,
9 `reqwest_blocking_client_builder()`, `reqwest_client()`), which installs the
10 provider exactly once before the first build. The only sanctioned bare
11 constructors are the platform builders in `crates/release/src/`.
12
13 See #6153 (0.9.13 shipped this panic on the first-run Ollama probe).
14 """
15
16 from __future__ import annotations
17
18 import re
19 import sys
20 from pathlib import Path
21
22 ROOT = Path(__file__).resolve().parents[1]
23 CRATES = ROOT / "crates"
24 # The TLS-owner crate: its platform builders are the sanctioned constructors.
25 ALLOWED_PREFIX = CRATES / "release" / "src"
26
27 # Fully-qualified bare constructors. Always a violation outside ALLOWED_PREFIX.
28 QUALIFIED_RE = re.compile(
29 r"reqwest::(?:blocking::)?Client::(?:builder|new)\s*\("
30 )
31 # Bare `Client::builder()` / `Client::new()` spellings. Only a violation in a
32 # file that imports reqwest's Client, so custom `FooClient` types never match
33 # (`(?<![\w:])` also keeps `FixedSummaryClient::default()`-style hits out).
34 BARE_RE = re.compile(r"(?<![\w:])Client::(?:builder|new)\s*\(")
35 REQWEST_CLIENT_IMPORT_RE = re.compile(r"use\s+reqwest::[^\n;]*\bClient\b")
36
37
38 def _is_comment_only(line: str) -> bool:
39 stripped = line.lstrip()
40 return stripped.startswith("//")
41
42
43 def file_violations(path: Path, allowed_prefix: Path = ALLOWED_PREFIX) -> list[str]:
44 """Return `path:line: <match>` entries for bare constructors in one file."""
45 try:
46 text = path.read_text(encoding="utf-8")
47 except (OSError, UnicodeDecodeError):
48 return []
49 try:
50 rel = path.relative_to(ROOT)
51 except ValueError:
52 rel = path
53 if allowed_prefix in path.parents or path == allowed_prefix:
54 return []
55 hits: list[str] = []
56 bare_allowed = REQWEST_CLIENT_IMPORT_RE.search(text) is None
57 for lineno, line in enumerate(text.splitlines(), start=1):
58 if _is_comment_only(line):
59 continue
60 match = QUALIFIED_RE.search(line)
61 if match is None and not bare_allowed:
62 match = BARE_RE.search(line)
63 if match is not None:
64 hits.append(f"{rel}:{lineno}: {match.group(0)}")
65 return hits
66
67
68 def find_violations(
69 root: Path = CRATES, allowed_prefix: Path = ALLOWED_PREFIX
70 ) -> list[str]:
71 """Walk `root/**/*.rs` and collect every bare-constructor violation."""
72 violations: list[str] = []
73 for path in sorted(root.rglob("*.rs")):
74 violations.extend(file_violations(path, allowed_prefix))
75 return violations
76
77
78 def main() -> int:
79 violations = find_violations()
80 if violations:
81 print(
82 "bare reqwest client constructor(s) outside crates/release/src "
83 "(use codewhale_release::tls instead; see #6153):",
84 file=sys.stderr,
85 )
86 for violation in violations:
87 print(f" {violation}", file=sys.stderr)
88 return 1
89 print("reqwest builder check OK: no bare Client::builder()/new() outside crates/release/src.")
90 return 0
91
92
93 if __name__ == "__main__":
94 sys.exit(main())
95
95 lines PYTHON