wowlab_tidy/languages/rust/rules/interop/
concrete_io_param.rs1use ra_ap_syntax::ast;
2
3use crate::{AstCtx, Example, Violation};
4
5#[rustfmt::skip]
6const EXAMPLES: &[Example] = &[
7 Example {
8 label: "File parameter by value",
9 code: "fn parse(file: File) {}",
10 pass: false,
11 },
12 Example {
13 label: "File parameter by mutable reference",
14 code: "fn parse(file: &mut File) {}",
15 pass: false,
16 },
17 Example {
18 label: "TcpStream parameter",
19 code: "fn read_frame(stream: TcpStream) {}",
20 pass: false,
21 },
22 Example {
23 label: "fully qualified TcpStream parameter",
24 code: "fn read_frame(stream: std::net::TcpStream) {}",
25 pass: false,
26 },
27 Example {
28 label: "Stdout parameter",
29 code: "fn log_to(out: Stdout) {}",
30 pass: false,
31 },
32 Example {
33 label: "File in impl method",
34 code: "struct S;\nimpl S {\n fn parse(&self, file: &File) {}\n}",
35 pass: false,
36 },
37 Example {
38 label: "impl Read parameter",
39 code: "fn parse(data: impl std::io::Read) {}",
40 pass: true,
41 },
42 Example {
43 label: "byte slice parameter",
44 code: "fn parse(data: &[u8]) {}",
45 pass: true,
46 },
47 Example {
48 label: "File as return type is fine",
49 code: "fn open(path: &std::path::Path) -> File { make(path) }",
50 pass: true,
51 },
52 Example {
53 label: "unlisted type passes",
54 code: "fn parse(reader: BufReader<u8>) {}",
55 pass: true,
56 },
57 Example {
58 label: "File parameter in test module",
59 code: "#[cfg(test)]\nmod tests {\n fn parse(file: File) {}\n}",
60 pass: true,
61 },
62 Example {
63 label: "syntax tree parameter",
64 code: "fn inspect(file: &syn::File) {}",
65 pass: true,
66 },
67];
68
69crate::ast_rule!(
70 concrete_io_param,
71 "Flag fn parameters typed as concrete I/O handles like `File` or `TcpStream`.",
72 "Concrete I/O parameter types couple logic to one byte source; `impl std::io::Read`/`impl std::io::Write` works with files, sockets, and buffers alike (M-IMPL-IO).",
73 Low,
74 params {
75 types: [String] = ["File", "Stdin", "Stdout", "TcpStream", "UnixStream"]
76 },
77);
78
79fn check_concrete_io_param(ctx: &AstCtx<'_>) -> Vec<Violation> {
80 let types = ctx
81 .file
82 .config
83 .get_str_array("rust_concrete_io_param", &PARAMS[0]);
84
85 ctx.nodes::<ast::Fn>()
86 .filter(|function| {
87 super::support::is_item_or_impl_fn(function) && !ctx.is_in_test(function)
88 })
89 .flat_map(|function| {
90 function
91 .param_list()
92 .into_iter()
93 .flat_map(|params| params.params())
94 .filter_map(|param| {
95 let ty = param.ty()?;
96
97 io_type_name(ty.clone(), &types).map(|name| {
98 ctx.violation(
99 &ty,
100 format!(
101 "parameter typed `{name}` couples the function to concrete I/O — accept `impl std::io::Read`/`impl std::io::Write` (sans-io) instead"
102 ),
103 )
104 })
105 })
106 .collect::<Vec<_>>()
107 })
108 .collect()
109}
110
111fn io_type_name(ty: ast::Type, types: &[String]) -> Option<String> {
112 let path = match ty {
113 ast::Type::RefType(reference) => match reference.ty()? {
114 ast::Type::PathType(path) => path.path()?,
115 _ => return None,
116 },
117 ast::Type::PathType(path) => path.path()?,
118 _ => return None,
119 };
120 let segments = super::support::path_names(path);
121 let last = segments.last()?;
122
123 if segments.len() > 1 && !is_std_io_path(&segments) {
124 return None;
125 }
126
127 types.iter().find(|name| *name == last).cloned()
128}
129
130fn is_std_io_path(segments: &[String]) -> bool {
131 const GRANDPARENT_SEGMENT: usize = 2;
132
133 let type_name = segments.last().map(String::as_str);
134
135 match type_name {
136 Some("File") => segments
137 .iter()
138 .rev()
139 .nth(1)
140 .is_some_and(|module| module == "fs"),
141 Some("Stdin" | "Stdout") => segments
142 .iter()
143 .rev()
144 .nth(1)
145 .is_some_and(|module| module == "io"),
146 Some("TcpStream") => segments
147 .iter()
148 .rev()
149 .nth(1)
150 .is_some_and(|module| module == "net"),
151 Some("UnixStream") => {
152 segments
153 .iter()
154 .rev()
155 .nth(1)
156 .is_some_and(|module| module == "net")
157 && segments
158 .iter()
159 .rev()
160 .nth(GRANDPARENT_SEGMENT)
161 .is_some_and(|module| module == "unix")
162 }
163 _ => false,
164 }
165}
166
167crate::tidy_ast_test!(check_concrete_io_param, {
168 crate::example_tests!(EXAMPLES, check_concrete_io_param);
169});