Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

new lint: danger_not_accepted #11600

Closed
wants to merge 18 commits into from
Closed

Conversation

Radbuglet
Copy link

@Radbuglet Radbuglet commented Oct 2, 2023

Adds the danger_not_accepted lint:

What it does

Checks for uses of functions, inherent methods, and trait methods which have been marked as
dangerous with the #[clippy::dangerous(...)] attribute and whose dangers have not been
explicitly accepted.

Each #[clippy::dangerous(reason_1, reason_2, ...)] attribute specifies a list of dangers
that the user must accept using the #[clippy::accept_danger(reason_1, reason_2, ...)]
attribute before using the dangerous item to avoid triggering this lint.

Why is this bad?

Some functionality in a project may be dangerous to use without giving it the appropriate
caution, even if its misuse does not cause undefined behavior—for example, the method could
be the source of tricky logic bugs. Other functionality may be dangerous in some contexts
but not others. This lint helps ensure that users do not unknowingly call into these
dangerous functions while still allowing users who know what they're doing to call these
functions without issue.

Example

#[clippy::dangerous(use_of_lib_1_dangerous_module)]
mod dangerous_module {
    #[clippy::dangerous(may_break_program)]
    pub fn do_something_innocuous_looking() {
        break_the_program();
    }
}

use unsuspecting_module {
   fn do_something() {
       // This function call causes clippy to issue a warning
       super::dangerous_module::do_something_innocuous_looking();
   }
}

Use instead:

#[clippy::dangerous(use_of_lib_1_dangerous_module)]
mod dangerous_module {
    #[clippy::dangerous(may_break_program)]
    pub fn do_something_innocuous_looking() {
        break_the_program();
    }
}

// This entire module can use functions with the danger `use_of_lib_1_dangerous_module`.
#[clippy::accept_danger(use_of_lib_1_dangerous_module)]
use unsuspecting_module {
   fn do_something() {
       // Only this statement can call functions with the danger `may_break_program`.
       #[clippy::accept_danger(may_break_program)]
       super::dangerous_module::do_something_innocuous_looking();
   }
}

The following features have been deemed as future improvements and have not been included in this MVP:

  • Allow users to override modules as not having a specific danger.
  • Allow users to specify additional dangerous items in the clippy config.
  • Devise a scheme (maybe path compression?) to reduce the amount of ancestry tracing we have to do to determine the dangers posed by a method.
  • Implement a way to forbid accept_danger in a given module.
  • Allow accept_danger and dangerous to be used as inner attributes on stable Rust.

changelog: new lint: danger_not_accepted

@rustbot
Copy link
Collaborator

rustbot commented Oct 2, 2023

Thanks for the pull request, and welcome! The Rust team is excited to review your changes, and you should hear from @Alexendoo (or someone else) soon.

Please see the contribution instructions for more information. Namely, in order to ensure the minimum review times lag, PR authors and assigned reviewers should ensure that the review label (S-waiting-on-review and S-waiting-on-author) stays updated, invoking these commands when appropriate:

  • @rustbot author: the review is finished, PR author should check the comments and take action accordingly
  • @rustbot review: the author is ready for a review, this PR will be queued again in the reviewer's queue

@rustbot rustbot added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties label Oct 2, 2023
@rustbot
Copy link
Collaborator

rustbot commented Oct 2, 2023

There are merge commits (commits with multiple parents) in your changes. We have a no merge policy so these commits will need to be removed for this pull request to be merged.

You can start a rebase with the following commands:

$ # rebase
$ git rebase -i master
$ # delete any merge commits in the editor that appears
$ git push --force-with-lease

The following commits are merge commits:

@Radbuglet
Copy link
Author

I am having trouble getting the doc-tests to pass locally. After fixing a typo where I declared my module with use rather than mod, the doc-test runner complains about not being able to find the dangerous_module in the crate root.

Here are the offending snippets:

#[clippy::dangerous(use_of_lib_1_dangerous_module)]
pub mod dangerous_module {
    # fn break_the_program() {}
    #[clippy::dangerous(may_break_program)]
    pub fn do_something_innocuous_looking() {
        break_the_program();
    }
}

pub mod unsuspecting_module {
   fn do_something() {
       // This function call causes clippy to issue a warning
       crate::dangerous_module::do_something_innocuous_looking();
   }
}

...produces the error:

---- src/danger_not_accepted.rs - danger_not_accepted::DANGER_NOT_ACCEPTED (line 42) stdout ----
error[E0433]: failed to resolve: could not find `dangerous_module` in the crate root
  --> src/danger_not_accepted.rs:55:15
   |
15 |        crate::dangerous_module::do_something_innocuous_looking();
   |               ^^^^^^^^^^^^^^^^ could not find `dangerous_module` in the crate root

error: aborting due to previous error

For more information about this error, try `rustc --explain E0433`.
Couldn't compile the test.

...and the snippet:

#[clippy::dangerous(use_of_lib_1_dangerous_module)]
pub mod dangerous_module {
    # fn break_the_program() {}

    #[clippy::dangerous(may_break_program)]
    pub fn do_something_innocuous_looking() {
        break_the_program();
    }
}

// This entire module can use functions with the danger `use_of_lib_1_dangerous_module`.
#[clippy::accept_danger(use_of_lib_1_dangerous_module)]
pub mod unsuspecting_module {
    fn do_something() {
        // Only this statement can call functions with the danger `may_break_program`.
        #[clippy::accept_danger(may_break_program)]
        crate::dangerous_module::do_something_innocuous_looking();
    }
}

...produces the error:

---- src/danger_not_accepted.rs - danger_not_accepted::DANGER_NOT_ACCEPTED (line 60) stdout ----
error[E0433]: failed to resolve: could not find `dangerous_module` in the crate root
  --> src/danger_not_accepted.rs:76:15
   |
18 |        crate::dangerous_module::do_something_innocuous_looking();
   |               ^^^^^^^^^^^^^^^^ could not find `dangerous_module` in the crate root

error: aborting due to previous error

For more information about this error, try `rustc --explain E0433`.
Couldn't compile the test.

These two snippets pass this fork of Clippy when checked on a standalone project so I'm not sure what's happening.

@Alexendoo
Copy link
Member

The doctests don't get run on a local cargo test due to the weird workspace setup we have, but it'll be because doctests wrap the contents in fn main() { } the module doesn't exist in the crate root, you can put # fn main() {} at the end of the doctest to disable the automatic wrapping

@Alexendoo Alexendoo added the I-nominated Issue: Nominated to be discussed at the next Clippy meeting label Oct 2, 2023
@Alexendoo
Copy link
Member

Nominating this for discussion in tomorrow's meeting (Tuesday, 15:00 UTC in https://rust-lang.zulipchat.com/#narrow/stream/257328-clippy) since it's a novel lint

@Centri3
Copy link
Member

Centri3 commented Oct 3, 2023

Initial thoughts: I quite like the idea! Currently the closest to this is either making it an unsafe fn (which is terrible) or marking it as deprecated (which is misleading). However, allowing this using an attribute doesn't seem right, not because it's more verbose or anything but because it can't be used everywhere. For example:

#[clippy::accept_danger(a)]
expands_to_smth_dangerous!(my_dangerous_function);

Would apply the attribute to the macro invocation afaik, which is stripped during macro expansion, and thus is still linted. You have to apply the attribute to a block, which is really ugly, or ignore macros entirely (which would be valid, but imo is a bit silly). Another one:

#[clippy::allow_dangerous(my_dangerous_function)]
my_dangerous_function() + 1;

An attribute in this position is currently nightly-only (afaik because of ambiguities) and so you'd need a block to enclose this, too.

Both of these are issues with allowing any lint but I think allowing a lint is rarer than, for example, unsafe. This would probably be used more often than allow.

I'm not sure how this would be solved.

@Centri3
Copy link
Member

Centri3 commented Oct 3, 2023

Implement a way to forbid accept_danger in a given module.

This is already possible with #![forbid(clippy::accept_danger)].

@Radbuglet
Copy link
Author

Implement a way to forbid accept_danger in a given module.

This is already possible with #![forbid(clippy::accept_danger)].

Whoops—that explanation was a bit ambiguous. What I meant by that post-MVP item was that, in much the same way that some users may wish to #[forbid(...)] a specific lint, some users may find it helpful to forbid people from accepting certain dangers with another custom attribute like #[clippy::forbid_danger(...)]. That would look something like this:

#[clippy::dangerous(d)]
mod whee {
    pub fn woo() {}
}

#[clippy::forbid_danger(my_danger)]
mod i_am_super_safe {
    pub fn waz() {
        #[clippy::accept_danger(my_danger)]  // This would be forbidden.
        super::whee::woo();
    }
}

I could also see the benefit in forbidding users from accepting any more dangers beyond the ones declared by a similar attribute like #[forbid_dangers_besides(allow_list_here)].

Again, however, I'm under the impression that making too big an addition to the Rust programming language is frowned upon so I'm leaving these as post-MVP features that can be taken up later.

@Radbuglet
Copy link
Author

Radbuglet commented Oct 3, 2023

Another one:

#[clippy::allow_dangerous(my_dangerous_function)]
my_dangerous_function() + 1;

An attribute in this position is currently nightly-only (afaik because of ambiguities) and so you'd need a block to enclose this, too.

Both of these are issues with allowing any lint but I think allowing a lint is rarer than, for example, unsafe. This would probably be used more often than allow.

I'm not sure how this would be solved.

Hmm... this second issue is somewhat difficult. Here are a few ideas!

Like you said, the second issue only seems to be a problem in return positions and, luckily, return x; is a statement so you can do:

fn main() {
    #[clippy::accept_danger(whee)]
    let foo = dangerous() + 3;

    woo();
}

fn woo() -> u32 {
    #[clippy::accept_danger(whee)]
    return dangerous() + 4;
}

#[clippy::dangerous(whee)]
fn dangerous() -> u32 {
    3
}

...which is admittedly a bit goofy.

This still leaves sub-expressions and the return-position of blocks as potential pain points (I mean, you could break out of blocks now but that feels exceedingly goofy). I don't think these are that bad, however, since you could just mark the enclosing statement as accepting the danger and I think that should be safe enough for most people unless they start writing super long statements.

Another thing a user (or we!) could do is expose a macro to accept dangers in expression positions:

macro_rules! accept_danger {
    ($($allow:ident),*$(,)? ; $expr:expr) => {{
        #[clippy::accept_danger($($allow)*)]
        let res = { $expr };
        res
    }};
}

fn main() {
    let foo = {
        println!("Hi!");
        accept_danger!(waz; dangerous() + 3)
    };
}

#[clippy::dangerous(waz)]
fn dangerous() -> i32 {
    4
}

I'm not a massive fan of this idea: Clippy is supposed to be a linter and it would be a bit weird to expose something that's not an attribute.

I think the best solution in the long-term would be to wait for Rust to add support for at least marking block expressions with attributes. That way, the user could type:

fn main() {
    let foo = {
        println!("Hi!");
        #[clippy::accept_danger(waz)] {
            dangerous() + 3
        }
    };
}

#[clippy::dangerous(waz)]
fn dangerous() -> i32 {
    4
}

...which looks pretty close to an unsafe block and doesn't suffer from the same ambiguity issues that seem to be blocking the larger proposal from going through.

Alternatively, users could just mark their entire function as accepting a danger but I don't know how well that will signal the dangers.

@Centri3
Copy link
Member

Centri3 commented Oct 3, 2023

I think the best solution in the long-term would be to wait for Rust to add support for at least marking block expressions with attributes. That way, the user could type:

fn main() {
    let foo = {
        println!("Hi!");
        #[clippy::accept_danger(waz)] {
            dangerous() + 3
        }
    };
}

#[clippy::dangerous(waz)]
fn dangerous() -> i32 {
    4
}

Isn't this already possible? This is what you usually do to allow a lint for a macro (though rustfmt moves the opening bracket onto its own line; which, honestly, I hate).

I'm a bit unsure what you mean by "only happening in return positions", as an attribute there is perfectly fine:

fn awa() {
    #[allow(warnings)]
    ()
}

The reason why it didn't work there was because it could either be applied to the function call or the addition. The parser currently goes with the function call with feature(stmt_expr_attributes). Adding a block removes the ambiguity.

Relevant rustfmt issue that shows what the parser does here: rust-lang/rustfmt#5873

I do agree that clippy shouldn't expose a macro like this, and I'm unsure how it would even work.

I see what you mean now about forbidding specific dangers. The reverse (allowing) could be added to the disallowed_* lints, too

@Radbuglet
Copy link
Author

I'm a bit unsure what you mean by "only happening in return positions", as an attribute there is perfectly fine:

fn awa() {
    #[allow(warnings)]
    ()
}

allow is a special rustc attribute which gets special rustc treatment. If I try awa with a clippy-specific attribute, I get an error:

fn awa() -> () {
    #[clippy::msrv = ":)"]  // attributes on expressions are experimental
    ()
}

Link to playground

It may be worthwhile seeing if we can get clippy's attribute namespace promoted to exhibit some of the properties of builtin attributes. That way, #[clippy::accept_danger(...)] and company could be used in all contexts where an allow attribute is allowed. If that were the case, we would be able to write:

fn woo() -> i32 {
    id(
        #[clippy::accept_danger(my_danger)]
        (dangerous() + 1),
    )
}

#[clippy::dangerous(my_danger)]
fn dangerous() -> i32 {
    3
}

fn id<T>(v: T) -> T {
    v
}

...which feels pretty convenient to me.

@Alexendoo
Copy link
Member

allow is a special rustc attribute which gets special rustc treatment

Huh, TIL

From the meeting we'd like for it to have a human readable message for more detailed error messages similar to #[must_use]

We also discussed that it should work across crate boundaries, but it looks like that should already work, I misread it as using a HIR visitor for both attributes initially

@Alexendoo
Copy link
Member

re: the attributes, yeah some of those are unfortunate but it's still the only real option for syntax. Some of those issues like the macro expansion one are shared with unsafe also

@Radbuglet
Copy link
Author

Radbuglet commented Oct 11, 2023

From the meeting we'd like for it to have a human readable message for more detailed error messages similar to #[must_use]

This should be implemented now for #[clippy::dangerous] and #[clippy::accept_danger] although only #[clippy::dangerous]'s reasons show up in lint messages.

@Alexendoo Alexendoo removed the I-nominated Issue: Nominated to be discussed at the next Clippy meeting label Oct 17, 2023
// Edge case attr tests
#[rustfmt::skip]
#[clippy::dangerous(whee, woo,)]
#[clippy::dangerous(whee, woo, reason = "sdfhsdf",)]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the reason should be per danger, i.e. #[clippy::dangerous(foo = "reason")]

Bikeshedding a bit, what about something like #[clippy::danger::foo = "reason"]? It's more similar to #[must_use] that way

Copy link
Author

@Radbuglet Radbuglet Nov 7, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. I like the #[clippy::dangerous(a, b, reason = "foo")] syntax because it allows you to share the same reason among multiple dangers. For example, foo_internal_api and raw_foo_access could share the reason "low level building block; use bar instead." If you want several different reasons, you could always write it as:

#[clippy::dangerous(foo, reason = "reason 1")]
#[clippy::dangerous(bar, reason = "reason 2")]
fn baz() {}

However, I do see the verbosity benefit of your solution compared to this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally I don't think it will be that common to want to use multiple dangers on the same node, particularly so where the reason can be the same for all of them

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Relatedly there was discussion about whether or not the reason should be required, I don't think we fully came to an agreement on that but initially let's make it required, it can be made optional later if we want that but not the other way around

clippy_lints/src/danger_not_accepted.rs Show resolved Hide resolved
@jhpratt
Copy link
Member

jhpratt commented Nov 13, 2023

What if two crates use the same ident? Should they be namespaced to the crate?

@Radbuglet
Copy link
Author

Radbuglet commented Nov 13, 2023

What if two crates use the same ident? Should they be namespaced to the crate?

I don't think they should have to be namespaced to a crate since some dangers may be quite common (e.g. main_thread_exclusive could be used by e.g. libc and winit and both can be handled at the same time). However, I do see the value of implementing some generic way to namespace things, even if it doesn't have any particularly strong enforcement. Maybe we could add support for :: in danger idents?

@jhpratt
Copy link
Member

jhpratt commented Nov 13, 2023

Sure, supporting :: without enforcing anything would also work. I would personally adopt the convention of prefixing the crate name.

@bors
Copy link
Collaborator

bors commented Nov 14, 2023

☔ The latest upstream changes (presumably #11791) made this pull request unmergeable. Please resolve the merge conflicts.

@Radbuglet
Copy link
Author

Radbuglet commented Dec 2, 2023

Sorry for the long wait.

Danger identifiers can now take the form of paths and reasons are mandatory for #[dangerous] attributes using the danger::path = "justification literal" syntax. I also reserved some danger prefixes (e.g. clippy::foo, etc) and danger identifiers (e.g. reason, version, etc).

@Alexendoo
Copy link
Member

Did the #[clippy::danger::foo = "reason"] change get pushed? Sorry for the large delay I thought that was being waited on as the tests still show the original syntax, but I missed that you commented that it was implemented

@xFrednet
Copy link
Member

Hey @Radbuglet , this is a ping from triage, since there hasn't been any activity in some time. Are you still planning to continue this implementation?

If you have any questions, you're always welcome to ask them in this PR or on Zulip.

@rustbot author

@rustbot rustbot added S-waiting-on-author Status: This is awaiting some action from the author. (Use `@rustbot ready` to update this status) and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties labels Mar 31, 2024
@Radbuglet
Copy link
Author

Whoops. I meant to respond to this thread but forgot. Sorry!

Yes, the #[clippy::danger::foo = "reason"] syntax is implemented and currently replaces the old #[clippy::danger(clippy_danger_foo, reason = "reason")] syntax. The parser for this custom syntax provides good diagnostics, too.

The following, however, are not implemented:

  • I think I forgot to update the documentation but we should probably wait on that until all the other details are finalized.
  • I don't yet support accept_danger on entire namespaces—maybe that's a good idea?
  • You can still place #[clippy::danger] attributes on any HIR item and all the descendants will still inherit the danger set.

@Radbuglet
Copy link
Author

Bumping this PR a bit... are there any additional changes I should make to this PR? My previous comment includes a list of incomplete things I remember being mentioned.

@Alexendoo
Copy link
Member

  • I think I forgot to update the documentation but we should probably wait on that until all the other details are finalized.

Yeah that's fine, the PR itself is still using the old syntax though

  • I don't yet support accept_danger on entire namespaces—maybe that's a good idea?

What do you mean by namespace?

  • You can still place #[clippy::danger] attributes on any HIR item and all the descendants will still inherit the danger set.

Let's start conservative and have it be direct only like #[must_use]

@Radbuglet
Copy link
Author

  • I think I forgot to update the documentation but we should probably wait on that until all the other details are finalized.

Yeah that's fine, the PR itself is still using the old syntax though

Huh? The line you shared uses the new syntax:

#[clippy::dangerous(may_deadlock = "your program may deadlock in calling this function")]
pub fn woo2() {}

The new syntax is:

fn caller() {
    #[clippy::accept_danger(path::to::danger, reason = "this part is optional")]
    callee();
}

#[clippy::dangerous(path::to::danger = "why it's dangerous")]
fn callee() {

}
  • I don't yet support accept_danger on entire namespaces—maybe that's a good idea?

What do you mean by namespace?

I think it might be useful to be able to accept entire classes of dangers. For example:

#[clippy::accept_danger(my_lib, reason = "we know what we're doing")]
mod trusted {
    fn whee() {
        super::func_1();
        super::func_2();
    }
}

#[clippy::dangerous(my_lib::func1 = "reason 1")]
fn func_1() { ... }

#[clippy::dangerous(my_lib::func2 = "reason 2")]
fn func_2() { ... }

However, if this is out-of-scope for an initial implementation, we can skip it.

  • You can still place #[clippy::danger] attributes on any HIR item and all the descendants will still inherit the danger set.

Let's start conservative and have it be direct only like #[must_use]

Sounds good!

@Alexendoo
Copy link
Member

Ah I see the confusion, the intention was to have the danger be part of the attr path e.g.

#[clippy::danger::may_deadlock = "your program may deadlock in calling this function"]
pub fn woo2() {}

We could accept paths on top of just idents, but it may be worth avoiding so people don't feel the need to add namespaces everywhere

@xFrednet
Copy link
Member

Hey @Radbuglet , this is a ping from triage, since there hasn't been any activity in some time. Are you still planning to continue this implementation?

If you have any questions, you're always welcome to ask them in this PR or on Zulip.

@rustbot author

@bors
Copy link
Collaborator

bors commented Jul 21, 2024

☔ The latest upstream changes (presumably #11700) made this pull request unmergeable. Please resolve the merge conflicts.

@xFrednet
Copy link
Member

Hey this is triage, I'm closing this due to inactivity. If you want to continue this implementation, you're welcome to create a new PR. Thank you for the time, you already put into this!

Interested parties are welcome to pick this implementation up as well :)

@rustbot label +S-inactive-closed -S-waiting-on-author -S-waiting-on-review

@xFrednet xFrednet closed this Jul 21, 2024
@rustbot rustbot added S-inactive-closed Status: Closed due to inactivity and removed S-waiting-on-author Status: This is awaiting some action from the author. (Use `@rustbot ready` to update this status) labels Jul 21, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
S-inactive-closed Status: Closed due to inactivity
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants