Skip to content

Commit c2ac3f8

Browse files
committed
test
1 parent b6a59b4 commit c2ac3f8

File tree

1 file changed

+297
-0
lines changed

1 file changed

+297
-0
lines changed
Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
use std::fs;
2+
3+
// Ensures no field in non-`*Params` structs in src/protocol.rs uses
4+
// `#[serde(skip_serializing_if = "Option::is_none")]`.
5+
//
6+
// Intent: Responses, Notifications, and other objects must always serialize
7+
// `null` for optional fields; only `*Params` structs may use this attribute.
8+
#[test]
9+
fn no_skip_serializing_if_on_non_params_structs() {
10+
let path = format!("{}/src/protocol.rs", env!("CARGO_MANIFEST_DIR"));
11+
let src =
12+
fs::read_to_string(&path).unwrap_or_else(|e| panic!("failed to read {}: {}", path, e));
13+
14+
// Collect struct bodies with their names and byte ranges.
15+
#[derive(Debug)]
16+
struct Block {
17+
name: String,
18+
start: usize,
19+
end: usize,
20+
}
21+
22+
let mut i = 0usize;
23+
let bytes = src.as_bytes();
24+
let mut structs: Vec<Block> = Vec::new();
25+
26+
while let Some(pos) = find_token(bytes, i, b"struct") {
27+
// Ensure it's a standalone token (preceded/followed by non-ident chars)
28+
if !is_boundary(bytes, pos.saturating_sub(1)) || !is_boundary(bytes, pos + 6) {
29+
i = pos + 6;
30+
continue;
31+
}
32+
33+
let mut j = skip_ws(bytes, pos + 6);
34+
let (name, next) = if let Some(t) = parse_ident(bytes, j) {
35+
t
36+
} else {
37+
i = j; // advance and continue searching
38+
continue;
39+
};
40+
if name.is_empty() {
41+
i = j;
42+
continue;
43+
}
44+
45+
j = skip_ws(bytes, next);
46+
// Skip generics if present: <...>
47+
if j < bytes.len() && bytes[j] == b'<' {
48+
j = match skip_angle_block(bytes, j) {
49+
Some(n) => n,
50+
None => break,
51+
};
52+
j = skip_ws(bytes, j);
53+
}
54+
55+
if j >= bytes.len() {
56+
break;
57+
}
58+
59+
let (open, close) = match bytes[j] {
60+
b'{' => (b'{', b'}'),
61+
b'(' => (b'(', b')'),
62+
_ => {
63+
i = j + 1;
64+
continue;
65+
}
66+
};
67+
68+
if let Some(end) = find_matching(bytes, j, open, close) {
69+
structs.push(Block {
70+
name,
71+
start: j,
72+
end,
73+
});
74+
i = end + 1;
75+
} else {
76+
break;
77+
}
78+
}
79+
80+
let mut violations: Vec<String> = Vec::new();
81+
for blk in structs {
82+
if blk.name.ends_with("Params") {
83+
continue; // Allowed to use skip_serializing_if
84+
}
85+
86+
let body = &src[blk.start..=blk.end];
87+
88+
// Fast-path check for the attribute pattern within this struct body.
89+
let mut search_from = 0usize;
90+
while let Some(rel) = body[search_from..].find("skip_serializing_if") {
91+
let abs = blk.start + search_from + rel;
92+
// Check the line that contains this occurrence.
93+
let line_start = src[..abs].rfind('\n').map(|p| p + 1).unwrap_or(0);
94+
let line_end = src[abs..].find('\n').map(|p| abs + p).unwrap_or(src.len());
95+
let line = &src[line_start..line_end];
96+
97+
// If `//` appears before `#[`, treat as commented-out and ignore.
98+
let idx_hash = line.find("#[");
99+
let idx_comment = line.find("//");
100+
let looks_like_attr =
101+
idx_hash.is_some() && (idx_comment.is_none() || idx_hash < idx_comment);
102+
103+
if looks_like_attr && line.contains("serde") && line.contains("Option::is_none") {
104+
// Record a helpful message with the struct and line.
105+
violations.push(format!(
106+
"{}: disallowed #[serde(skip_serializing_if = \"Option::is_none\")] in non-Params struct `{}`\n> {}",
107+
path, blk.name, line.trim()
108+
));
109+
break; // one hit is enough per struct
110+
}
111+
112+
search_from = search_from + rel + "skip_serializing_if".len();
113+
}
114+
}
115+
116+
if !violations.is_empty() {
117+
panic!(
118+
"Found disallowed serde skip_serializing_if on non-Params structs:\n{}",
119+
violations.join("\n\n")
120+
);
121+
}
122+
}
123+
124+
fn is_ident_char(b: u8) -> bool {
125+
b.is_ascii_alphanumeric() || b == b'_'
126+
}
127+
128+
fn is_boundary(bytes: &[u8], idx: usize) -> bool {
129+
if idx >= bytes.len() {
130+
return true;
131+
}
132+
!is_ident_char(bytes[idx])
133+
}
134+
135+
fn skip_ws(bytes: &[u8], mut i: usize) -> usize {
136+
while i < bytes.len() {
137+
match bytes[i] {
138+
b' ' | b'\t' | b'\r' | b'\n' => {
139+
i += 1;
140+
}
141+
b'/' if i + 1 < bytes.len() && bytes[i + 1] == b'/' => {
142+
// line comment
143+
i += 2;
144+
while i < bytes.len() && bytes[i] != b'\n' {
145+
i += 1;
146+
}
147+
}
148+
b'/' if i + 1 < bytes.len() && bytes[i + 1] == b'*' => {
149+
// block comment (non-nested for simplicity)
150+
i += 2;
151+
while i + 1 < bytes.len() && !(bytes[i] == b'*' && bytes[i + 1] == b'/') {
152+
i += 1;
153+
}
154+
i = (i + 2).min(bytes.len());
155+
}
156+
_ => break,
157+
}
158+
}
159+
i
160+
}
161+
162+
fn parse_ident(bytes: &[u8], mut i: usize) -> Option<(String, usize)> {
163+
let start = i;
164+
while i < bytes.len() && is_ident_char(bytes[i]) {
165+
i += 1;
166+
}
167+
if i == start {
168+
return Some((String::new(), i));
169+
}
170+
let name = String::from_utf8(bytes[start..i].to_vec()).ok()?;
171+
Some((name, i))
172+
}
173+
174+
fn skip_angle_block(bytes: &[u8], mut i: usize) -> Option<usize> {
175+
// assumes bytes[i] == b'<'
176+
let mut depth = 0i32;
177+
while i < bytes.len() {
178+
match bytes[i] {
179+
b'<' => {
180+
depth += 1;
181+
i += 1;
182+
}
183+
b'>' => {
184+
depth -= 1;
185+
i += 1;
186+
if depth == 0 {
187+
return Some(i);
188+
}
189+
}
190+
b'\"' => {
191+
i = skip_string(bytes, i)?;
192+
}
193+
b'\'' => {
194+
i = skip_char(bytes, i)?;
195+
}
196+
_ => {
197+
i += 1;
198+
}
199+
}
200+
}
201+
None
202+
}
203+
204+
fn skip_string(bytes: &[u8], mut i: usize) -> Option<usize> {
205+
// assumes bytes[i] == '"'
206+
i += 1;
207+
while i < bytes.len() {
208+
match bytes[i] {
209+
b'\\' => {
210+
i += 2;
211+
}
212+
b'\"' => {
213+
i += 1;
214+
return Some(i);
215+
}
216+
_ => {
217+
i += 1;
218+
}
219+
}
220+
}
221+
None
222+
}
223+
224+
fn skip_char(bytes: &[u8], mut i: usize) -> Option<usize> {
225+
// assumes bytes[i] == '\''
226+
i += 1;
227+
while i < bytes.len() {
228+
match bytes[i] {
229+
b'\\' => {
230+
i += 2;
231+
}
232+
b'\'' => {
233+
i += 1;
234+
return Some(i);
235+
}
236+
_ => {
237+
i += 1;
238+
}
239+
}
240+
}
241+
None
242+
}
243+
244+
fn find_matching(bytes: &[u8], mut i: usize, open: u8, close: u8) -> Option<usize> {
245+
// assumes bytes[i] == open
246+
let mut depth = 0i32;
247+
while i < bytes.len() {
248+
let b = bytes[i];
249+
if b == b'\"' {
250+
i = skip_string(bytes, i)?;
251+
continue;
252+
}
253+
if b == b'\'' {
254+
i = skip_char(bytes, i)?;
255+
continue;
256+
}
257+
if b == b'/' && i + 1 < bytes.len() && bytes[i + 1] == b'/' {
258+
// line comment
259+
i += 2;
260+
while i < bytes.len() && bytes[i] != b'\n' {
261+
i += 1;
262+
}
263+
continue;
264+
}
265+
if b == b'/' && i + 1 < bytes.len() && bytes[i + 1] == b'*' {
266+
// block comment (non-nested)
267+
i += 2;
268+
while i + 1 < bytes.len() && !(bytes[i] == b'*' && bytes[i + 1] == b'/') {
269+
i += 1;
270+
}
271+
i = (i + 2).min(bytes.len());
272+
continue;
273+
}
274+
if b == open {
275+
depth += 1;
276+
}
277+
if b == close {
278+
depth -= 1;
279+
if depth == 0 {
280+
return Some(i);
281+
}
282+
}
283+
i += 1;
284+
}
285+
None
286+
}
287+
288+
fn find_token(bytes: &[u8], from: usize, token: &[u8]) -> Option<usize> {
289+
let mut i = from;
290+
while i + token.len() <= bytes.len() {
291+
if &bytes[i..i + token.len()] == token {
292+
return Some(i);
293+
}
294+
i += 1;
295+
}
296+
None
297+
}

0 commit comments

Comments
 (0)