Is it acceptable to add new lint to deny `x = x.clone();`?

Currently, x = x.clone(); does not cause any clippy lint errors if the type of x does not implement Copy (playground).

However, I want to detect this kind of mistake by clippy if possible.
So, I'd like to ask whether it's acceptabe to add such a lint to clippy.

Considering semantics of Clone, the lint for this should be disabled by default even if it's added.

Background

Recently, I made a mistake like this:

fn set(&mut self name: Symbol, value: &Value) {
    self.properties.
        .entry(name)
        .and_modify(|prop| match prop {
            // Oh, no... the variable name `value` is already used
            // as the input parameter name.
            Property::Data { ref mut value, .. } => *value = value.clone(),
        })
        ...
}

This was entirely due to my lack of attention, but clippy didn't complain about it.

Depending on implementation of Clone::clone(), this kind of assignment is not self-assignment.
Therefore, I suppose that the self_assignment lint in clippy does not detect it.

1 Like

in my opinion, a lint for shadowing variable from outer scope with same type (or similar types, e.g. &Value vs &mut Value) could also catch the error in this special case, and may be more useful in general.

2 Likes

Indeed. Thank you fo your suggestion.

If such a lint is useful for other people and acceptable for clippy, I'd like to try adding it to clippy.
Thought, I have no experience about clippy implementation.

This PR looks like it can detect the case you suggested.
But the shadow_same is allowed by default.

1 Like

oh, great, shadow_unrelated already exists!

and it should detect the error in your example, you can try it in the playground, use the "TOOLS" -> "Clippy" button on the top right.

1 Like

Thank you for sharing info.

The shadow_unrelated lint works properly in the playground, but this lint does not allow some cases that we see in actual projects though those are Rust's idioms (playground).

It might be better to separate cases we're discussing here from the shadow_unrelated lint and add a new lint for it.

That one is caught by the unused lint. And it doesn't seem related to the original x = x.clone(); problem.


If you want to contribute to the clippy project, I suggest looking at the issues, and filing a new issue if there is a lint you think should be added. To contribute in general you can look at the "good first issue" labeled issues.

This is an example for the shadow_unrelated lint that detects other cases.
What I'd like to say in this example is that the shadow_unrelated lint can detect the case that nerditation mentioned, but it does not allow other cases used in actual rust projects.
At this point, there are no lint that detects only shadowing variables from outer scope.

of course, all lints can have false positives (and false negatives), and should be treated as guide lines, other than rules. that's why they are lints, not compile diagnoses.

that being said, I think the situations where shadow_unrelated would reject good code is rare, and should be treated as exceptions, case by case. and the example in your playground link is too trivial to prove the point. arguably, I would not say it's idiomatic at all, let alone the already mentioned unused rustc lint.

the rust idiom of reusing the same variable name frees programmers from having to think of different names for semantically similar or related variables.

here's an example to demonstrate what I mean:

/// user can specify a config file using `--config PATH` command line option
fn load_config(config: Option<&Path>) -> Config {
    let config = config.unwrap_or(DEFAULT_CONFIG_PATH);
    let Ok(config) = std::fs::read(config) else {
        return Config::default();
    };
    let config = json::from_slice::<json::Value>(&config).unwrap();
    match config.get("version").as_i64() {
        Some(1) => {
            let config: ConfigV1 = json::from_value(config).unwrap();
            config.into()
        }
        Some(2) => {
            let config: ConfigV2 = json::from_value(config).unwrap();
            config.into()
        }
        _ => {
            warn!("unsupported config file version");
            Config::default()
       }
    }
}

in languages where you cannot use the same name for different variables in scope, you are forced to give different names for the same piece of information (pseudo code):

/// `config_file_path` is specified at command line and is optional
function load_config(config_file_path: Path) -> Config {
    var config_file = if config_file_path != null {
        open_file(config_file_path)
    } else {
        open_file(DEFAULT_CONFIG_FILE)
    }
    var config_string = config_file.read_all();
    var config_json = parse_json(config_string);
    if config_json["version"] == 1 {
        var config_v1 = config_v1_from_json(config_json);
        return migrate_config_from_v1(config_v1);
    } else if config_json["version"] == 2 {
        return config_v2_from_json(config_json);
    } else {
        return default_config();
    }
}

naming is hard, people tend to avoid naming things, so they might omit the intermediate variables all along and chain all the functions and methods together, which can be equally bad to read (unless the API is purposely designed to be used like this); careful code formatting can help a bit, but not solve the problem:

config_from_json(
    parse_json(
        open_file(
            config_file_path || DEFAULT_CONFIG_FILE
        )
        .readall()
    )
)

granted, this example is simple and contrived, but it is not far from real code (I have seen very simiar code in the wild).

for this example, I'm pretty sure it will NOT trigger the shadow_unrelated lint. (but note also, not all variables with the same names are technically "shadowing" in rust, because of linear types, but most other languges don't have linear types either).

to summarize:

  • the shadow_unrelated lint, like all lints, should be treated as a guide or hint, to look for potential bad code, not as a strict rule to follow
  • but I think overall it is a very useful one: as its name suggested, it is meant to catch "unrelated" shadows, and in my opinion, false positives should NOT be very common in practice.
  • rust allows (and even encourages, when appropriate,) to use the same name for multiple variables, however, this doesn't mean absuing the same variable name multiple times is always considered idiomatic.
1 Like

Thank you for the detail explanation.

of course, all lints can have false positives (and false negatives), and should be treated as guide lines, other than rules. that's why they are lints, not compile diagnoses.

My conclusion is really summarized in this your comment. And I was remainded that mistakes should be detected by testing.

Static analisys tools including linters such as clippy can be used with simple configuration. Additionally, we can copy a configuration file from other projects. On the other hand, testing requires developers to write test code. Many developer may have the same fealing as me, writing test code is not a fun task. As a result, we (at least I) tends to rely on such tools.

I spent time to check behavior of the shadow_unrelated lint by actually applying on my personal projects in order to understand your intentions through experience. The shadow_unrelated lint detected the mistake I mentioned in the first post and addiotionally reported false positives as you mentioned in the previous post.

/*
warning: `name` shadows a previous, unrelated binding
    --> libs/jsruntime/src/llvmir/compiler/mod.rs:2431:14
     |
2431 |         let (name, name_len) = bb_name!(self);
     |              ^^^^
     |
note: previous binding is here
    --> libs/jsruntime/src/llvmir/compiler/mod.rs:2429:38
     |
2429 |     fn create_basic_block(&mut self, name: &str) -> BasicBlock {
     |                                      ^^^^
     = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#shadow_unrelated
     = note: requested on the command line with `-W clippy::shadow-unrelated`
*/
    fn create_basic_block(&mut self, name: &str) -> BasicBlock {
        push_bb_name!(self, name);
        let (name, name_len) = bb_name!(self);
        let block = self.bridge.create_basic_block(name, name_len);
        pop_bb_name!(self);
        block
    }

Probably, we can create a new lint to detect x = x.clone();, but reports for it will include false positives/negatives. The lint may solve some problems without writing test code, but it's not the main purpose of linters.

As you pointed out in your comment, my example was too simple and not appropriate for discussing about problems that may happen in actual projects. And that might have made you confused, sorry.

Finally, thanks to everyone who spent time on this topic.

1 Like