Skip to main content

wowlab_tidy/languages/rust/rules/interop/
concrete_io_param.rs

1use 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});