Skip to content

Commit 5f3a060

Browse files
committed
Auto merge of #11608 - atwam:suspicious-open-options, r=y21
Add suspicious_open_options lint. changelog: [`suspicious_open_options`]: Checks for the suspicious use of std::fs::OpenOptions::create() without an explicit OpenOptions::truncate(). create() alone will either create a new file or open an existing file. If the file already exists, it will be overwritten when written to, but the file will not be truncated by default. If less data is written to the file than it already contains, the remainder of the file will remain unchanged, and the end of the file will contain old data. In most cases, one should either use `create_new` to ensure the file is created from scratch, or ensure `truncate` is called so that the truncation behaviour is explicit. `truncate(true)` will ensure the file is entirely overwritten with new data, whereas `truncate(false)` will explicitely keep the default behavior. ```rust use std::fs::OpenOptions; OpenOptions::new().create(true).truncate(true); ``` - [x] Followed [lint naming conventions][lint_naming] - [x] Added passing UI tests (including committed `.stderr` file) - [x] `cargo test` passes locally - [x] Executed `cargo dev update_lints` - [x] Added lint documentation - [x] Run `cargo dev fmt`
2 parents ca7b54b + f09cd88 commit 5f3a060

15 files changed

+283
-122
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5606,6 +5606,7 @@ Released 2018-09-13
56065606
[`suspicious_else_formatting`]: https://rust-lang.github.io/rust-clippy/master/index.html#suspicious_else_formatting
56075607
[`suspicious_map`]: https://rust-lang.github.io/rust-clippy/master/index.html#suspicious_map
56085608
[`suspicious_op_assign_impl`]: https://rust-lang.github.io/rust-clippy/master/index.html#suspicious_op_assign_impl
5609+
[`suspicious_open_options`]: https://rust-lang.github.io/rust-clippy/master/index.html#suspicious_open_options
56095610
[`suspicious_operation_groupings`]: https://rust-lang.github.io/rust-clippy/master/index.html#suspicious_operation_groupings
56105611
[`suspicious_splitn`]: https://rust-lang.github.io/rust-clippy/master/index.html#suspicious_splitn
56115612
[`suspicious_to_owned`]: https://rust-lang.github.io/rust-clippy/master/index.html#suspicious_to_owned

clippy_lints/src/declared_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[
439439
crate::methods::STR_SPLIT_AT_NEWLINE_INFO,
440440
crate::methods::SUSPICIOUS_COMMAND_ARG_SPACE_INFO,
441441
crate::methods::SUSPICIOUS_MAP_INFO,
442+
crate::methods::SUSPICIOUS_OPEN_OPTIONS_INFO,
442443
crate::methods::SUSPICIOUS_SPLITN_INFO,
443444
crate::methods::SUSPICIOUS_TO_OWNED_INFO,
444445
crate::methods::TYPE_ID_ON_BOX_INFO,

clippy_lints/src/methods/mod.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2827,6 +2827,44 @@ declare_clippy_lint! {
28272827
"nonsensical combination of options for opening a file"
28282828
}
28292829

2830+
declare_clippy_lint! {
2831+
/// ### What it does
2832+
/// Checks for the suspicious use of `OpenOptions::create()`
2833+
/// without an explicit `OpenOptions::truncate()`.
2834+
///
2835+
/// ### Why is this bad?
2836+
/// `create()` alone will either create a new file or open an
2837+
/// existing file. If the file already exists, it will be
2838+
/// overwritten when written to, but the file will not be
2839+
/// truncated by default.
2840+
/// If less data is written to the file
2841+
/// than it already contains, the remainder of the file will
2842+
/// remain unchanged, and the end of the file will contain old
2843+
/// data.
2844+
/// In most cases, one should either use `create_new` to ensure
2845+
/// the file is created from scratch, or ensure `truncate` is
2846+
/// called so that the truncation behaviour is explicit. `truncate(true)`
2847+
/// will ensure the file is entirely overwritten with new data, whereas
2848+
/// `truncate(false)` will explicitely keep the default behavior.
2849+
///
2850+
/// ### Example
2851+
/// ```rust,no_run
2852+
/// use std::fs::OpenOptions;
2853+
///
2854+
/// OpenOptions::new().create(true);
2855+
/// ```
2856+
/// Use instead:
2857+
/// ```rust,no_run
2858+
/// use std::fs::OpenOptions;
2859+
///
2860+
/// OpenOptions::new().create(true).truncate(true);
2861+
/// ```
2862+
#[clippy::version = "1.75.0"]
2863+
pub SUSPICIOUS_OPEN_OPTIONS,
2864+
suspicious,
2865+
"suspicious combination of options for opening a file"
2866+
}
2867+
28302868
declare_clippy_lint! {
28312869
/// ### What it does
28322870
///* Checks for [push](https://doc.rust-lang.org/std/path/struct.PathBuf.html#method.push)
@@ -4033,6 +4071,7 @@ impl_lint_pass!(Methods => [
40334071
MAP_ERR_IGNORE,
40344072
MUT_MUTEX_LOCK,
40354073
NONSENSICAL_OPEN_OPTIONS,
4074+
SUSPICIOUS_OPEN_OPTIONS,
40364075
PATH_BUF_PUSH_OVERWRITE,
40374076
RANGE_ZIP_WITH_LEN,
40384077
REPEAT_ONCE,
Lines changed: 129 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -1,178 +1,206 @@
1-
use clippy_utils::diagnostics::span_lint;
2-
use clippy_utils::ty::is_type_diagnostic_item;
1+
use rustc_data_structures::fx::FxHashMap;
2+
3+
use clippy_utils::diagnostics::{span_lint, span_lint_and_then};
4+
use clippy_utils::ty::{is_type_diagnostic_item, match_type};
5+
use clippy_utils::{match_any_def_paths, paths};
36
use rustc_ast::ast::LitKind;
47
use rustc_hir::{Expr, ExprKind};
58
use rustc_lint::LateContext;
9+
use rustc_middle::ty::Ty;
610
use rustc_span::source_map::Spanned;
711
use rustc_span::{sym, Span};
812

9-
use super::NONSENSICAL_OPEN_OPTIONS;
13+
use super::{NONSENSICAL_OPEN_OPTIONS, SUSPICIOUS_OPEN_OPTIONS};
14+
15+
fn is_open_options(cx: &LateContext<'_>, ty: Ty<'_>) -> bool {
16+
is_type_diagnostic_item(cx, ty, sym::FsOpenOptions) || match_type(cx, ty, &paths::TOKIO_IO_OPEN_OPTIONS)
17+
}
1018

1119
pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, e: &'tcx Expr<'_>, recv: &'tcx Expr<'_>) {
1220
if let Some(method_id) = cx.typeck_results().type_dependent_def_id(e.hir_id)
1321
&& let Some(impl_id) = cx.tcx.impl_of_method(method_id)
14-
&& is_type_diagnostic_item(cx, cx.tcx.type_of(impl_id).instantiate_identity(), sym::FsOpenOptions)
22+
&& is_open_options(cx, cx.tcx.type_of(impl_id).instantiate_identity())
1523
{
1624
let mut options = Vec::new();
17-
get_open_options(cx, recv, &mut options);
18-
check_open_options(cx, &options, e.span);
25+
if get_open_options(cx, recv, &mut options) {
26+
check_open_options(cx, &options, e.span);
27+
}
1928
}
2029
}
2130

22-
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
31+
#[derive(Eq, PartialEq, Clone, Debug)]
2332
enum Argument {
24-
True,
25-
False,
33+
Set(bool),
2634
Unknown,
2735
}
2836

29-
#[derive(Debug)]
37+
#[derive(Debug, Eq, PartialEq, Hash, Clone)]
3038
enum OpenOption {
31-
Write,
39+
Append,
40+
Create,
41+
CreateNew,
3242
Read,
3343
Truncate,
34-
Create,
35-
Append,
44+
Write,
45+
}
46+
impl std::fmt::Display for OpenOption {
47+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48+
match self {
49+
OpenOption::Append => write!(f, "append"),
50+
OpenOption::Create => write!(f, "create"),
51+
OpenOption::CreateNew => write!(f, "create_new"),
52+
OpenOption::Read => write!(f, "read"),
53+
OpenOption::Truncate => write!(f, "truncate"),
54+
OpenOption::Write => write!(f, "write"),
55+
}
56+
}
3657
}
3758

38-
fn get_open_options(cx: &LateContext<'_>, argument: &Expr<'_>, options: &mut Vec<(OpenOption, Argument)>) {
39-
if let ExprKind::MethodCall(path, receiver, arguments, _) = argument.kind {
59+
/// Collects information about a method call chain on `OpenOptions`.
60+
/// Returns false if an unexpected expression kind was found "on the way",
61+
/// and linting should then be avoided.
62+
fn get_open_options(
63+
cx: &LateContext<'_>,
64+
argument: &Expr<'_>,
65+
options: &mut Vec<(OpenOption, Argument, Span)>,
66+
) -> bool {
67+
if let ExprKind::MethodCall(path, receiver, arguments, span) = argument.kind {
4068
let obj_ty = cx.typeck_results().expr_ty(receiver).peel_refs();
4169

4270
// Only proceed if this is a call on some object of type std::fs::OpenOptions
43-
if is_type_diagnostic_item(cx, obj_ty, sym::FsOpenOptions) && !arguments.is_empty() {
71+
if !arguments.is_empty() && is_open_options(cx, obj_ty) {
4472
let argument_option = match arguments[0].kind {
4573
ExprKind::Lit(span) => {
4674
if let Spanned {
4775
node: LitKind::Bool(lit),
4876
..
4977
} = span
5078
{
51-
if *lit { Argument::True } else { Argument::False }
79+
Argument::Set(*lit)
5280
} else {
5381
// The function is called with a literal which is not a boolean literal.
5482
// This is theoretically possible, but not very likely.
55-
return;
83+
// We'll ignore it for now
84+
return get_open_options(cx, receiver, options);
5685
}
5786
},
5887
_ => Argument::Unknown,
5988
};
6089

6190
match path.ident.as_str() {
6291
"create" => {
63-
options.push((OpenOption::Create, argument_option));
92+
options.push((OpenOption::Create, argument_option, span));
93+
},
94+
"create_new" => {
95+
options.push((OpenOption::CreateNew, argument_option, span));
6496
},
6597
"append" => {
66-
options.push((OpenOption::Append, argument_option));
98+
options.push((OpenOption::Append, argument_option, span));
6799
},
68100
"truncate" => {
69-
options.push((OpenOption::Truncate, argument_option));
101+
options.push((OpenOption::Truncate, argument_option, span));
70102
},
71103
"read" => {
72-
options.push((OpenOption::Read, argument_option));
104+
options.push((OpenOption::Read, argument_option, span));
73105
},
74106
"write" => {
75-
options.push((OpenOption::Write, argument_option));
107+
options.push((OpenOption::Write, argument_option, span));
108+
},
109+
_ => {
110+
// Avoid linting altogether if this method is from a trait.
111+
// This might be a user defined extension trait with a method like `truncate_write`
112+
// which would be a false positive
113+
if let Some(method_def_id) = cx.typeck_results().type_dependent_def_id(argument.hir_id)
114+
&& cx.tcx.trait_of_item(method_def_id).is_some()
115+
{
116+
return false;
117+
}
76118
},
77-
_ => (),
78119
}
79120

80-
get_open_options(cx, receiver, options);
121+
get_open_options(cx, receiver, options)
122+
} else {
123+
false
81124
}
125+
} else if let ExprKind::Call(callee, _) = argument.kind
126+
&& let ExprKind::Path(path) = callee.kind
127+
&& let Some(did) = cx.qpath_res(&path, callee.hir_id).opt_def_id()
128+
{
129+
match_any_def_paths(
130+
cx,
131+
did,
132+
&[
133+
&paths::TOKIO_IO_OPEN_OPTIONS_NEW,
134+
&paths::OPEN_OPTIONS_NEW,
135+
&paths::FILE_OPTIONS,
136+
&paths::TOKIO_FILE_OPTIONS,
137+
],
138+
)
139+
.is_some()
140+
} else {
141+
false
82142
}
83143
}
84144

85-
fn check_open_options(cx: &LateContext<'_>, options: &[(OpenOption, Argument)], span: Span) {
86-
let (mut create, mut append, mut truncate, mut read, mut write) = (false, false, false, false, false);
87-
let (mut create_arg, mut append_arg, mut truncate_arg, mut read_arg, mut write_arg) =
88-
(false, false, false, false, false);
89-
// This code is almost duplicated (oh, the irony), but I haven't found a way to
90-
// unify it.
91-
92-
for option in options {
93-
match *option {
94-
(OpenOption::Create, arg) => {
95-
if create {
96-
span_lint(
97-
cx,
98-
NONSENSICAL_OPEN_OPTIONS,
99-
span,
100-
"the method `create` is called more than once",
101-
);
102-
} else {
103-
create = true;
104-
}
105-
create_arg = create_arg || (arg == Argument::True);
106-
},
107-
(OpenOption::Append, arg) => {
108-
if append {
109-
span_lint(
110-
cx,
111-
NONSENSICAL_OPEN_OPTIONS,
112-
span,
113-
"the method `append` is called more than once",
114-
);
115-
} else {
116-
append = true;
117-
}
118-
append_arg = append_arg || (arg == Argument::True);
119-
},
120-
(OpenOption::Truncate, arg) => {
121-
if truncate {
122-
span_lint(
123-
cx,
124-
NONSENSICAL_OPEN_OPTIONS,
125-
span,
126-
"the method `truncate` is called more than once",
127-
);
128-
} else {
129-
truncate = true;
130-
}
131-
truncate_arg = truncate_arg || (arg == Argument::True);
132-
},
133-
(OpenOption::Read, arg) => {
134-
if read {
135-
span_lint(
136-
cx,
137-
NONSENSICAL_OPEN_OPTIONS,
138-
span,
139-
"the method `read` is called more than once",
140-
);
141-
} else {
142-
read = true;
143-
}
144-
read_arg = read_arg || (arg == Argument::True);
145-
},
146-
(OpenOption::Write, arg) => {
147-
if write {
148-
span_lint(
149-
cx,
150-
NONSENSICAL_OPEN_OPTIONS,
151-
span,
152-
"the method `write` is called more than once",
153-
);
154-
} else {
155-
write = true;
156-
}
157-
write_arg = write_arg || (arg == Argument::True);
158-
},
145+
fn check_open_options(cx: &LateContext<'_>, settings: &[(OpenOption, Argument, Span)], span: Span) {
146+
// The args passed to these methods, if they have been called
147+
let mut options = FxHashMap::default();
148+
for (option, arg, sp) in settings {
149+
if let Some((_, prev_span)) = options.insert(option.clone(), (arg.clone(), *sp)) {
150+
span_lint(
151+
cx,
152+
NONSENSICAL_OPEN_OPTIONS,
153+
prev_span,
154+
&format!("the method `{}` is called more than once", &option),
155+
);
159156
}
160157
}
161158

162-
if read && truncate && read_arg && truncate_arg && !(write && write_arg) {
159+
if let Some((Argument::Set(true), _)) = options.get(&OpenOption::Read)
160+
&& let Some((Argument::Set(true), _)) = options.get(&OpenOption::Truncate)
161+
&& let None | Some((Argument::Set(false), _)) = options.get(&OpenOption::Write)
162+
{
163163
span_lint(
164164
cx,
165165
NONSENSICAL_OPEN_OPTIONS,
166166
span,
167167
"file opened with `truncate` and `read`",
168168
);
169169
}
170-
if append && truncate && append_arg && truncate_arg {
170+
171+
if let Some((Argument::Set(true), _)) = options.get(&OpenOption::Append)
172+
&& let Some((Argument::Set(true), _)) = options.get(&OpenOption::Truncate)
173+
{
171174
span_lint(
172175
cx,
173176
NONSENSICAL_OPEN_OPTIONS,
174177
span,
175178
"file opened with `append` and `truncate`",
176179
);
177180
}
181+
182+
if let Some((Argument::Set(true), create_span)) = options.get(&OpenOption::Create)
183+
&& let None = options.get(&OpenOption::Truncate)
184+
&& let None | Some((Argument::Set(false), _)) = options.get(&OpenOption::Append)
185+
{
186+
span_lint_and_then(
187+
cx,
188+
SUSPICIOUS_OPEN_OPTIONS,
189+
*create_span,
190+
"file opened with `create`, but `truncate` behavior not defined",
191+
|diag| {
192+
diag.span_suggestion(
193+
create_span.shrink_to_hi(),
194+
"add",
195+
".truncate(true)".to_string(),
196+
rustc_errors::Applicability::MaybeIncorrect,
197+
)
198+
.help("if you intend to overwrite an existing file entirely, call `.truncate(true)`")
199+
.help(
200+
"if you instead know that you may want to keep some parts of the old file, call `.truncate(false)`",
201+
)
202+
.help("alternatively, use `.append(true)` to append to the file instead of overwriting it");
203+
},
204+
);
205+
}
178206
}

0 commit comments

Comments
 (0)