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

Lint Reasons RFC #2383

Merged
merged 2 commits into from
Sep 23, 2018
Merged

Lint Reasons RFC #2383

merged 2 commits into from
Sep 23, 2018

Conversation

myrrlyn
Copy link

@myrrlyn myrrlyn commented Apr 2, 2018

This RFC makes two additions to the lint attributes in Rust code:

  • add a reason field to the attributes that permits custom text when rendering
  • add an expect lint attribute that acts as #[allow], but raises a lint when the lint it wants to allow is not raised.

Rendered
Tracking issue

@Centril Centril added the T-lang Relevant to the language team, which will review and decide on the RFC. label Apr 2, 2018
@Manishearth
Copy link
Member

cc @oli-obk @llogiq

@Ixrec
Copy link
Contributor

Ixrec commented Apr 3, 2018

Without prior knowledge of this RFC, if I saw #[expect(...)] in some random piece of code I'm not sure I'd realize it was lint-related at first. My first guess might be some sort of design-by-contract debug assertion on preconditions.

Come to think of it, why does the existing #[allow(...)] attribute not print a warning when the allowed lint doesn't happen anymore?

@scottmcm
Copy link
Member

scottmcm commented Apr 4, 2018

Come to think of it, why does the existing #[allow(...)] attribute not print a warning when the allowed lint doesn't happen anymore?

Probably in part because sometimes macros need to suppress things that only happen sometimes -- unreachable code in the error handling if the function returns -> Result<T, !>, for example.

@DavidMikeSimon
Copy link

Perhaps #[enforce(...)]?

@scottmcm
Copy link
Member

scottmcm commented Apr 4, 2018

Bikeshed: we have #[should_panic] in tests; maybe make this #[should_lint(...)]?

@jan-hudec
Copy link

I'd like to suggest an alternative for the second part:

Define an unused_allow lint that will generate a warning when there is an #[allow(…)], but the warning would not be generated anyway. Then the user can easily #![allow(unused_allow)] or #![warn(unused_allow)] as they prefer. Whether default state should be allow or warn is up to bikeshedding. As direct alternative it would start in allow, but I don't actually see much reason why it shouldn't default to warn.

@joshtriplett
Copy link
Member

joshtriplett commented Apr 28, 2018

While I don't feel strongly about reason strings one way or another (and I definitely don't want the optional new comment form), I do very much like expect, and I'd like to see that. Almost any use of allow I've seen should become expect.

Also, you have a golden opportunity here: you should discuss the appropriate compiler behavior for #[expect(expectation_missing)] when applied to code that raises no warnings. The compiler clearly can neither warn nor not warn about the missing expectation. On the other hand, it can and should warn about a paradox.

@oli-obk
Copy link
Contributor

oli-obk commented Apr 28, 2018

Almost any use of allow I've seen should become expect.

That seems to me like an argument for the alternative with the unused_allow lint. If every occurrence of allow should become expect, then we should simply change the behaviour of allow.

@nikomatsakis
Copy link
Contributor

I would prefer to keep allow with its current semantics, for two reasons:

  • It feels like a breaking change to do otherwise, even if it strictly speaking conforms to our guidelines.
  • Some projects, like LALRPOP, generate code that may trigger lints (notably exhaustiveness-style warnings) but also may not. In that case, it is useful to have allow -- if we only had expect, we would need some way to cover this case.

@joshtriplett
Copy link
Member

We discussed this in the lang team meeting, and we were all quite excited about the expect attribute. There was strong consensus that we don't want to change the existing semantics of allow.

@rfcbot fcp merge

@rfcbot
Copy link
Collaborator

rfcbot commented May 3, 2018

Team member @joshtriplett has proposed to merge this. The next step is review by the rest of the tagged teams:

No concerns currently listed.

Once a majority of reviewers approve (and none object), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up!

See this document for info about what commands tagged team members can give me.

@rfcbot rfcbot added proposed-final-comment-period Currently awaiting signoff of all team members in order to enter the final comment period. disposition-merge This RFC is in PFCP or FCP with a disposition to merge it. labels May 3, 2018
@joshtriplett
Copy link
Member

(Also, I wasn't entirely joking about the semantics of #[expect(expectation_missing)]; that should probably have an explicit definition just for the sake of not leaving the weird corner case.)

@scottmcm
Copy link
Member

A few questions here:

  • Does the reason on an allow get used anywhere?
  • What happens with groups like #[expect(warnings)]?
  • If I have crate-level #![expect(unused_mut)], is that usefully different from just allow?

@joshtriplett
Copy link
Member

If I have crate-level #![expect(unused_mut)], is that usefully different from just allow?

Somewhat: the compiler will tell you if you don't have any unused_mut warnings, so you can remove it. It's more useful to put it directly where you need it, but you can put it at the crate level too and still get value from it.

@myrrlyn
Copy link
Author

myrrlyn commented Jun 13, 2018

@scottmcm

Does the reason on an allow get used anywhere?

Not in the rustc linting system at present, which does nothing with allow flags. I have included it so that all the lint flags share the same grammar, and so that it can be left open for future use. At present it is able to be a comment viewed in source code, or there may be a hypothetical tool that collects all lint flags for display and renders what behavior is allowed/warned/denied/expected and why. But in short, allow(reason) is at present unused in the compiler.

What happens with groups?

Anything that is a member of the group matches; this is as true of warn and deny as it is of expect.

@joshtriplett

semantics of #[expect(expectation_missing)]

Let me see if I understand this correctly:

mod foo {
  #![expect(expectation_missing)]

  #[expect(unused_mut)]
  fn bar() {
    let mut v = v[3, 2, 4, 1, 5];
    v.sort();
  }
}
  1. v mutates with .sort(), and so the mut binding is used. The linter does not raise an unused_mut event.

  2. fn bar expects an unused_mut event and does not receive one. It therefore raises an expectation_missing event.

  3. mod foo receives an expectation_missing event and traps it, and no lint event exits the module.

Now eliminate v.sort().

  1. v is mutable and never mutated. The linter raises an unused_mut event.

  2. fn bar receives an unused_mut event and traps it.

  3. mod foo expects an expectation_missing event and does not receive one. It therefore raises an expectation_missing event, and a lint event exits the module. If this event is not silenced with allow or expect, then it will reach the crate boundary and the linter will display that mod foo expected to receive an expectation_missing event, but no such event occurred; please look at mod foo.

As far as I can understand this, these are consistent and non-paradoxical behaviors. The expect attribute is essentially a logical NOT gate: when a (matching) lint arrives, no lint departs; when a lint does not arrive, a lint departs.

All:

Should allow take on the lint-raising behavior of the proposed expect?

I agree that allow should remain unchanged for the reasons given.

Should expect be named something else?

I have become more aware that we use attributes for altering control flow (#[async]) and therefore an attribute name that looks like a control-flow term should be avoided for something that does not affect the actual generated code. I like should_lint as it matches an existing expectation of raised events, and I also like enforce as a single word to match the other lint attribute names. I am in favor of moving away from expect as the new linter, and have no strong preference for either name put forward so far. I suppose I would go with should_lint, as using enforce feels stronger than is warranted to me, and should_lint clearly (to me) conveys the idea that if the behavior that should happen does not, then an event will be raised.


I definitely don't want the optional new comment form

Agreed. I included it solely as a brainstorm, and to provided a prior-art reference for if a formalization of magic comments ever does arise. It is unrelated to the main thrust of this RFC and I will happily strip it from the text if/when this is approved to merge.

@joshtriplett
Copy link
Member

@myrrlyn So, you're suggesting that an "expectation_missing" lint doesn't get looked at at the level it's produced, only at the next level out? So, in particular, if you'd put #[expect(expectation_missing)] on bar then your first case wouldn't work because the expectation_missing event would start working outward from mod foo and not from fn bar?

That's...consistent, but it seems wrong somehow. I think I'd prefer to have expectation_missing start at the same level as the expect that produced it, and then have a special case for expect(expectation_missing) or similar.

@myrrlyn
Copy link
Author

myrrlyn commented Jun 14, 2018

... I think I don't know how lints travel the scope tree. The way I have always thought about it, they start at the scope where they were raised and then the linter searches "down", that is, through the progressively enclosing scopes, for a modifier and halts the search at the first one it finds. I also might be misunderstanding where lint control attributes are placed.

Suppose we have the following code:

#[expect(expectation_missing)]
mod foo {
  #[expect(expectation_missing)]
  fn bar() {
    #[expect(unused_mut)]
    let mut v = 0;
  }
}

This is what I think is the chronological order of events when this code gets linted.

  • The linter begins searching crate scope. The crate has no lint handlers attached.
    • The linter begins searching mod foo scope. The module scope has a lint handler attached, and so the linter sets a trap for expectation_missing.
      • The linter begins searching fn bar scope, which also has a handler, and the linter sets a trap here as well.
        • The linter begins searching statement scope. The statement has a handler, so the linter sets that up.
          • The linter observes that v does not mutate, and adds unused_mut to its set of lints.
          • The statement ends, and the linter unwinds one step, to be inside fn bar scope.
        • When the linter crosses the statement scope boundary into fn bar, it runs the statement lint controllers.
        • The expect handler consumes the unused_mut lint, and then having received what it expected, disarms.
        • The linter continues searching fn bar scope (empty).
        • The linter unwinds one step, to be inside mod foo scope.
      • When the linter crosses the fn bar scope boundary into mod foo, it runs the lint controllers on fn bar. expect(expectation_missing) executes, and has no lint to consume, so it adds expectation_missing to the set of active lint events.
      • The linter continues searching mod foo scope (empty).
      • The linter unwinds one step, to be inside crate scope.
    • The linter has a standing lint item: expectation_missing. When the linter crosses the mod foo boundary into crate scope, it runs the lint controllers on mod foo. expect(expectation_missing) consumes the pending lint, and disarms.
    • The linter continues searching crate scope (empty).
    • The linter unwinds one step, to be OUTSIDE crate scope.
  • The linter did not carry any lints outside crate scope, and the departure from crate scope did not run any expect traps, so the linter exits quietly.

Assuming I know how lints work, which is very questionable, each scope generates lints internally, that are then processed whenever the linter passes a semicolon or a closing brace.

Each expect handler can only run once. It either receives a lint from the set of lints generated in the scopes over which it has domain and destroys the lint, or it does not receive one and inserts an expectation_missing into the set of lints generated in the scope in which it resides. After either of these actions, the linter moves forward and will never revisit that handler. Therefore, any expectation_missing lint can only be processed when the linter departs the scope in which that lint was created.

This seems like it makes sense in my head but that's predicated on my current model of how the lint system works.


Update: I ran an experiment rather than make shit up. Consecutive lints on an item are processed as a stack.

#[deny(keyboard_smash)] // made up lint name
#[deny(unknown_lints)]
mod foo {}

When deny(unknown_lints) runs first, there are no lint events raised by the linter saying "I don't know what you said".

When deny(keyboard_smash) runs second, the linter says "I don't know what this is" and adds unknown_lint to its set of standing lints.

It runs the default unknown_lints handler at exit (warn) and compiles.

If I switch those two lines around, so keyboard_smash runs and lints before unknown_lints, which detects the lint, the compilation halts.

The expect handler and expectation_missing lint should behave the same way. They only process lints raised below them in scope and after them in peers on the same item, and the lints they themselves raise are only processed by handlers above them in scope or before them in peers on the same item.

#[expect(expectation_missing)]
#[expect(unused_mut)]
let v = 0;

No unused_mut is raised, so the lower expect fires. An expectation_missing is raised, so the upper expect disarms. No lints leave.

#[expect(unused_mut)]
#[expect(expectation_missing)]
let v = 0;

No expectation_missing is raised, so the lower lint fires. No unused_mut is raised, so the upper expect fires. Two lints leave.

@myrrlyn
Copy link
Author

myrrlyn commented Jun 14, 2018

If lints aren't a stack machine then I don't really have any ideas :/

I feel like expect can really only work for running linters and catching lints down-scope of each expect declaration though, especially since expect is directly dependent on the state of the linter itself rather than being a pure analysis of the source code.

@oli-obk oli-obk mentioned this pull request Jun 21, 2018
@nikomatsakis
Copy link
Contributor

@rfcbot reviewed -- d'oh, thought I did this already

@rfcbot rfcbot added proposed-final-comment-period Currently awaiting signoff of all team members in order to enter the final comment period. disposition-merge This RFC is in PFCP or FCP with a disposition to merge it. labels Sep 6, 2018
@rfcbot
Copy link
Collaborator

rfcbot commented Sep 6, 2018

🔔 This is now entering its final comment period, as per the review above. 🔔

@rfcbot rfcbot added final-comment-period Will be merged/postponed/closed in ~10 calendar days unless new substational objections are raised. and removed proposed-final-comment-period Currently awaiting signoff of all team members in order to enter the final comment period. labels Sep 6, 2018
@rfcbot rfcbot added the finished-final-comment-period The final comment period is finished for this RFC. label Sep 16, 2018
@rfcbot
Copy link
Collaborator

rfcbot commented Sep 16, 2018

The final comment period, with a disposition to merge, as per the review above, is now complete.

@rfcbot rfcbot removed the final-comment-period Will be merged/postponed/closed in ~10 calendar days unless new substational objections are raised. label Sep 16, 2018
@Centril Centril merged commit 863b37c into rust-lang:master Sep 23, 2018
@Centril
Copy link
Contributor

Centril commented Sep 23, 2018

Huzzah! This RFC has been merged!

Tracking issue: rust-lang/rust#54503

bors added a commit to rust-lang-ci/rust that referenced this pull request Mar 3, 2022
…th-ids, r=wesleywiser

Implementation of the `expect` attribute (RFC 2383)

This is an implementation of the `expect` attribute as described in [RFC-2383](https://rust-lang.github.io/rfcs/2383-lint-reasons.html). The attribute allows the suppression of lint message by expecting them. Unfulfilled lint expectations (meaning no expected lint was caught) will emit the `unfulfilled_lint_expectations` lint at the `expect` attribute.

### Example
#### input
```rs
// required feature flag
#![feature(lint_reasons)]

#[expect(unused_mut)] // Will warn about an unfulfilled expectation
#[expect(unused_variables)] // Will be fulfilled by x
fn main() {
    let x = 0;
}
```

#### output

```txt
warning: this lint expectation is unfulfilled
  --> $DIR/trigger_lint.rs:3:1
   |
LL | #[expect(unused_mut)] // Will warn about an unfulfilled expectation
   |          ^^^^^^^^^^
   |
   = note: `#[warn(unfulfilled_lint_expectations)]` on by default
```

### Implementation

This implementation introduces `Expect` as a new lint level for diagnostics, which have been expected. All lint expectations marked via the `expect` attribute are collected in the [`LintLevelsBuilder`] and assigned an ID that is stored in the new lint level. The `LintLevelsBuilder` stores all found expectations and the data needed to emit the `unfulfilled_lint_expectations` in the [`LintLevelsMap`] which is the result of the [`lint_levels()`] query.

The [`rustc_errors::HandlerInner`] is the central error handler in rustc and handles the emission of all diagnostics. Lint message with the level `Expect` are suppressed during this emission, while the expectation ID is stored in a set which marks them as fulfilled. The last step is then so simply check if all expectations collected by the [`LintLevelsBuilder`] in the [`LintLevelsMap`] have been marked as fulfilled in the [`rustc_errors::HandlerInner`]. Otherwise, a new lint message will be emitted.

The implementation of the `LintExpectationId` required some special handling to make it stable between sessions. Lints can be emitted during [`EarlyLintPass`]es. At this stage, it's not possible to create a stable identifier. The level instead stores an unstable identifier, which is later converted to a stable `LintExpectationId`.

### Followup TO-DOs
All open TO-DOs have been marked with `FIXME` comments in the code. This is the combined list of them:

* [ ] The current implementation doesn't cover cases where the `unfulfilled_lint_expectations` lint is actually expected by another `expect` attribute.
   * This should be easily possible, but I wanted to get some feedback before putting more work into this.
   * This could also be done in a new PR to not add to much more code to this one
* [ ] Update unstable documentation to reflect this change.
* [ ] Update unstable expectation ids in [`HandlerInner::stashed_diagnostics`](https://doc.rust-lang.org/nightly/nightly-rustc/rustc_errors/struct.HandlerInner.html#structfield.stashed_diagnostics)

### Open questions
I also have a few open questions where I would like to get feedback on:
1. The RFC discussion included a suggestion to change the `expect` attribute to something else. (Initiated by `@Ixrec` [here](rust-lang/rfcs#2383 (comment)), suggestion from `@scottmcm` to use `#[should_lint(...)]` [here](rust-lang/rfcs#2383 (comment))). No real conclusion was drawn on that point from my understanding. Is this still open for discussion, or was this discarded with the merge of the RFC?
2. How should the expect attribute deal with the new `force-warn` lint level?

---

This approach was inspired by a discussion with `@LeSeulArtichaut.`

RFC tracking issue: rust-lang#54503

Mentoring/Implementation issue: rust-lang#85549

[`LintLevelsBuilder`]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_lint/levels/struct.LintLevelsBuilder.html
[`LintLevelsMap`]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/lint/struct.LintLevelMap.html
[`lint_levels()`]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/ty/context/struct.TyCtxt.html#method.lint_levels
[`rustc_errors::HandlerInner`]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_errors/struct.HandlerInner.html
[`EarlyLintPass`]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_lint/trait.EarlyLintPass.html
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
disposition-merge This RFC is in PFCP or FCP with a disposition to merge it. finished-final-comment-period The final comment period is finished for this RFC. T-lang Relevant to the language team, which will review and decide on the RFC.
Projects
None yet
Development

Successfully merging this pull request may close these issues.