Skip to content

Commit bd26ca4

Browse files
committed
Introduce #[suppress]: suppress Rocket lints.
Adding `#[suppress(lint)]` for a known `lint` suppresses the corresponding warning generated by Rocket's codegen. The lints are: * `unknown_format` * `dubious_payload` * `segment_chars` * `arbitrary_main` * `sync_spawn` For example, the following works as expected to suppress the warning generated by the unknown media type "application/foo": ```rust fn post_foo() -> &'static str { "post_foo" } ``` Resolves #1188.
1 parent 35a1cf1 commit bd26ca4

File tree

12 files changed

+201
-19
lines changed

12 files changed

+201
-19
lines changed

core/codegen/src/attribute/entry/launch.rs

+4-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use devise::ext::SpanDiagnosticExt;
33
use proc_macro2::{TokenStream, Span};
44

55
use super::EntryAttr;
6+
use crate::attribute::suppress::Lint;
67
use crate::exports::mixed;
78

89
/// `#[rocket::launch]`: generates a `main` function that calls the attributed
@@ -90,13 +91,15 @@ impl EntryAttr for Launch {
9091
None => quote_spanned!(ty.span() => #rocket.launch()),
9192
};
9293

93-
if f.sig.asyncness.is_none() {
94+
let lint = Lint::SyncSpawn;
95+
if f.sig.asyncness.is_none() && lint.enabled(f.sig.asyncness.span()) {
9496
if let Some(call) = likely_spawns(f) {
9597
call.span()
9698
.warning("task is being spawned outside an async context")
9799
.span_help(f.sig.span(), "declare this function as `async fn` \
98100
to require async execution")
99101
.span_note(Span::call_site(), "`#[launch]` call is here")
102+
.note(lint.how_to_suppress())
100103
.emit_as_expr_tokens();
101104
}
102105
}

core/codegen/src/attribute/entry/main.rs

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use crate::attribute::suppress::Lint;
2+
13
use super::EntryAttr;
24

35
use devise::{Spanned, Result};
@@ -12,11 +14,12 @@ impl EntryAttr for Main {
1214

1315
fn function(f: &mut syn::ItemFn) -> Result<TokenStream> {
1416
let (attrs, vis, block, sig) = (&f.attrs, &f.vis, &f.block, &mut f.sig);
15-
if sig.ident != "main" {
16-
// FIXME(diag): warning!
17+
let lint = Lint::ArbitraryMain;
18+
if sig.ident != "main" && lint.enabled(sig.ident.span()) {
1719
Span::call_site()
1820
.warning("attribute is typically applied to `main` function")
1921
.span_note(sig.ident.span(), "this function is not `main`")
22+
.note(lint.how_to_suppress())
2023
.emit_as_item_tokens();
2124
}
2225

core/codegen/src/attribute/entry/mod.rs

+4
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ use devise::{Diagnostic, Spanned, Result};
66
use devise::ext::SpanDiagnosticExt;
77
use proc_macro2::{TokenStream, Span};
88

9+
use crate::attribute::suppress::Lint;
10+
911
// Common trait implemented by `async` entry generating attributes.
1012
trait EntryAttr {
1113
/// Whether the attribute requires the attributed function to be `async`.
@@ -23,6 +25,8 @@ fn _async_entry<A: EntryAttr>(
2325
.map_err(Diagnostic::from)
2426
.map_err(|d| d.help("attribute can only be applied to functions"))?;
2527

28+
Lint::suppress_attrs(&function.attrs, function.span());
29+
2630
if A::REQUIRES_ASYNC && function.sig.asyncness.is_none() {
2731
return Err(Span::call_site()
2832
.error("attribute can only be applied to `async` functions")

core/codegen/src/attribute/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ pub mod catch;
33
pub mod route;
44
pub mod param;
55
pub mod async_bound;
6+
pub mod suppress;

core/codegen/src/attribute/param/parse.rs

+5-2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use crate::name::Name;
66
use crate::proc_macro_ext::StringLit;
77
use crate::attribute::param::{Parameter, Dynamic};
88
use crate::http::uri::fmt::{Part, Kind, Path};
9+
use crate::attribute::suppress::Lint;
910

1011
#[derive(Debug)]
1112
pub struct Error<'a> {
@@ -47,6 +48,7 @@ impl Parameter {
4748
let mut trailing = false;
4849

4950
// Check if this is a dynamic param. If so, check its well-formedness.
51+
let lint = Lint::SegmentChars;
5052
if segment.starts_with('<') && segment.ends_with('>') {
5153
let mut name = &segment[1..(segment.len() - 1)];
5254
if name.ends_with("..") {
@@ -71,12 +73,13 @@ impl Parameter {
7173
}
7274
} else if segment.is_empty() {
7375
return Err(Error::new(segment, source_span, ErrorKind::Empty));
74-
} else if segment.starts_with('<') {
76+
} else if segment.starts_with('<') && lint.enabled(source_span) {
7577
let candidate = candidate_from_malformed(segment);
7678
source_span.warning("`segment` starts with `<` but does not end with `>`")
7779
.help(format!("perhaps you meant the dynamic parameter `<{}>`?", candidate))
80+
.note(lint.how_to_suppress())
7881
.emit_as_item_tokens();
79-
} else if segment.contains('>') || segment.contains('<') {
82+
} else if (segment.contains('>') || segment.contains('<')) && lint.enabled(source_span) {
8083
source_span.warning("`segment` contains `<` or `>` but is not a dynamic parameter")
8184
.emit_as_item_tokens();
8285
}

core/codegen/src/attribute/route/mod.rs

+6-3
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ use crate::exports::mixed;
1414

1515
use self::parse::{Route, Attribute, MethodAttribute};
1616

17+
use super::suppress::Lint;
18+
1719
impl Route {
1820
pub fn guards(&self) -> impl Iterator<Item = &Guard> {
1921
self.param_guards()
@@ -399,17 +401,18 @@ fn incomplete_route(
399401
args: TokenStream,
400402
input: TokenStream
401403
) -> Result<TokenStream> {
402-
let method_str = method.to_string().to_lowercase();
403404
// FIXME(proc_macro): there should be a way to get this `Span`.
405+
let method_str = method.to_string().to_lowercase();
404406
let method_span = StringLit::new(format!("#[{}]", method), Span::call_site())
405407
.subspan(2..2 + method_str.len());
406408

407-
let method_ident = syn::Ident::new(&method_str, method_span);
408-
409+
let full_span = args.span().join(input.span()).unwrap_or(input.span());
409410
let function: syn::ItemFn = syn::parse2(input)
410411
.map_err(Diagnostic::from)
411412
.map_err(|d| d.help(format!("#[{}] can only be used on functions", method_str)))?;
412413

414+
Lint::suppress_attrs(&function.attrs, full_span);
415+
let method_ident = syn::Ident::new(&method_str, method_span);
413416
let full_attr = quote!(#method_ident(#args));
414417
let method_attribute = MethodAttribute::from_meta(&syn::parse2(full_attr)?)?;
415418

core/codegen/src/attribute/route/parse.rs

+5-3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use devise::ext::{SpanDiagnosticExt, TypeExt};
33
use indexmap::{IndexSet, IndexMap};
44
use proc_macro2::Span;
55

6+
use crate::attribute::suppress::Lint;
67
use crate::proc_macro_ext::Diagnostics;
78
use crate::http_codegen::{Method, MediaType};
89
use crate::attribute::param::{Parameter, Dynamic, Guard};
@@ -127,11 +128,12 @@ impl Route {
127128

128129
// Emit a warning if a `data` param was supplied for non-payload methods.
129130
if let Some(ref data) = attr.data {
130-
if !attr.method.0.supports_payload() {
131-
let msg = format!("'{}' does not typically support payloads", attr.method.0);
131+
let lint = Lint::DubiousPayload;
132+
if !attr.method.0.supports_payload() && lint.enabled(handler.span()) {
132133
// FIXME(diag: warning)
133134
data.full_span.warning("`data` used with non-payload-supporting method")
134-
.span_note(attr.method.span, msg)
135+
.note(format!("'{}' does not typically support payloads", attr.method.0))
136+
.note(lint.how_to_suppress())
135137
.emit_as_item_tokens();
136138
}
137139
}
+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
use std::cell::RefCell;
2+
use std::collections::{HashMap, HashSet};
3+
use std::ops::Range;
4+
5+
use devise::ext::PathExt;
6+
use proc_macro2::{Span, TokenStream};
7+
use syn::{parse::Parser, punctuated::Punctuated};
8+
9+
macro_rules! declare_lints {
10+
($($name:ident ( $string:literal) ),* $(,)?) => (
11+
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
12+
pub enum Lint {
13+
$($name),*
14+
}
15+
16+
impl Lint {
17+
fn from_str(string: &str) -> Option<Self> {
18+
$(if string.eq_ignore_ascii_case($string) {
19+
return Some(Lint::$name);
20+
})*
21+
22+
None
23+
}
24+
25+
fn as_str(&self) -> &'static str {
26+
match self {
27+
$(Lint::$name => $string),*
28+
}
29+
}
30+
31+
fn lints() -> &'static str {
32+
concat!("[" $(,$string,)", "* "]")
33+
}
34+
}
35+
)
36+
}
37+
38+
declare_lints! {
39+
UnknownFormat("unknown_format"),
40+
DubiousPayload("dubious_payload"),
41+
SegmentChars("segment_chars"),
42+
ArbitraryMain("arbitrary_main"),
43+
SyncSpawn("sync_spawn"),
44+
}
45+
46+
thread_local! {
47+
static SUPPRESSIONS: RefCell<HashMap<Lint, HashSet<Range<usize>>>> = RefCell::default();
48+
}
49+
50+
fn span_to_range(span: Span) -> Option<Range<usize>> {
51+
let string = format!("{span:?}");
52+
let i = string.find('(')?;
53+
let j = string[i..].find(')')?;
54+
let (start, end) = string[(i + 1)..(i + j)].split_once("..")?;
55+
Some(Range { start: start.parse().ok()?, end: end.parse().ok()? })
56+
}
57+
58+
impl Lint {
59+
pub fn suppress_attrs(attrs: &[syn::Attribute], ctxt: Span) {
60+
let _ = attrs.iter().try_for_each(|attr| Lint::suppress_attr(attr, ctxt));
61+
}
62+
63+
pub fn suppress_attr(attr: &syn::Attribute, ctxt: Span) -> Result<(), syn::Error> {
64+
let syn::Meta::List(list) = &attr.meta else {
65+
return Ok(());
66+
};
67+
68+
if !list.path.last_ident().map_or(false, |i| i == "suppress") {
69+
return Ok(());
70+
}
71+
72+
Self::suppress_tokens(list.tokens.clone(), ctxt)
73+
}
74+
75+
pub fn suppress_tokens(attr_tokens: TokenStream, ctxt: Span) -> Result<(), syn::Error> {
76+
let lints = Punctuated::<Lint, syn::Token![,]>::parse_terminated.parse2(attr_tokens)?;
77+
lints.iter().for_each(|lint| lint.suppress(ctxt));
78+
Ok(())
79+
}
80+
81+
pub fn suppress(self, ctxt: Span) {
82+
SUPPRESSIONS.with_borrow_mut(|s| {
83+
let range = span_to_range(ctxt).unwrap_or_default();
84+
s.entry(self).or_default().insert(range);
85+
})
86+
}
87+
88+
pub fn is_suppressed(self, ctxt: Span) -> bool {
89+
SUPPRESSIONS.with_borrow(|s| {
90+
let this = span_to_range(ctxt).unwrap_or_default();
91+
s.get(&self).map_or(false, |set| {
92+
set.iter().any(|r| this.start >= r.start && this.end <= r.end)
93+
})
94+
})
95+
}
96+
97+
pub fn enabled(self, ctxt: Span) -> bool {
98+
!self.is_suppressed(ctxt)
99+
}
100+
101+
pub fn how_to_suppress(self) -> String {
102+
format!("apply `#[suppress({})]` before the item to suppress this lint", self.as_str())
103+
}
104+
}
105+
106+
impl syn::parse::Parse for Lint {
107+
fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result<Self> {
108+
let ident: syn::Ident = input.parse()?;
109+
let name = ident.to_string();
110+
Lint::from_str(&name).ok_or_else(|| {
111+
let msg = format!("invalid lint `{name}` (known lints: {})", Lint::lints());
112+
syn::Error::new(ident.span(), msg)
113+
})
114+
}
115+
}
+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
use proc_macro2::TokenStream;
2+
use devise::Spanned;
3+
4+
mod lint;
5+
6+
pub use lint::Lint;
7+
8+
pub fn suppress_attribute(
9+
args: proc_macro::TokenStream,
10+
input: proc_macro::TokenStream
11+
) -> TokenStream {
12+
let input: TokenStream = input.into();
13+
match Lint::suppress_tokens(args.into(), input.span()) {
14+
Ok(_) => input,
15+
Err(e) => {
16+
let error: TokenStream = e.to_compile_error().into();
17+
quote!(#error #input)
18+
}
19+
}
20+
}

core/codegen/src/http_codegen.rs

+5-4
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use quote::ToTokens;
22
use devise::{FromMeta, MetaItem, Result, ext::{Split2, PathExt, SpanDiagnosticExt}};
33
use proc_macro2::{TokenStream, Span};
44

5-
use crate::http;
5+
use crate::{http, attribute::suppress::Lint};
66

77
#[derive(Debug)]
88
pub struct ContentType(pub http::ContentType);
@@ -73,10 +73,11 @@ impl FromMeta for MediaType {
7373
let mt = http::MediaType::parse_flexible(&String::from_meta(meta)?)
7474
.ok_or_else(|| meta.value_span().error("invalid or unknown media type"))?;
7575

76-
if !mt.is_known() {
77-
// FIXME(diag: warning)
76+
let lint = Lint::UnknownFormat;
77+
if !mt.is_known() && lint.enabled(meta.value_span()) {
7878
meta.value_span()
79-
.warning(format!("'{}' is not a known media type", mt))
79+
.warning(format!("'{}' is not a known format or media type", mt))
80+
.note(lint.how_to_suppress())
8081
.emit_as_item_tokens();
8182
}
8283

core/codegen/src/lib.rs

+27
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,33 @@ pub fn catch(args: TokenStream, input: TokenStream) -> TokenStream {
356356
emit!(attribute::catch::catch_attribute(args, input))
357357
}
358358

359+
/// Suppress a warning generated by a Rocket lint.
360+
///
361+
/// Lints:
362+
/// * `unknown_format`
363+
/// * `dubious_payload`
364+
/// * `segment_chars`
365+
/// * `arbitrary_main`
366+
/// * `sync_spawn`
367+
///
368+
/// # Example
369+
///
370+
/// ```rust
371+
/// # #[macro_use] extern crate rocket;
372+
///
373+
/// #[suppress(dubious_payload)]
374+
/// #[get("/", data = "<_a>")]
375+
/// fn _f(_a: String) { }
376+
///
377+
/// #[get("/", data = "<_a>", format = "foo/bar")]
378+
/// #[suppress(dubious_payload, unknown_format)]
379+
/// fn _g(_a: String) { }
380+
/// ```
381+
#[proc_macro_attribute]
382+
pub fn suppress(args: TokenStream, input: TokenStream) -> TokenStream {
383+
emit!(attribute::suppress::suppress_attribute(args, input))
384+
}
385+
359386
/// Retrofits supports for `async fn` in unit tests.
360387
///
361388
/// Simply decorate a test `async fn` with `#[async_test]` instead of `#[test]`:

core/codegen/tests/route-format.rs

+4-4
Original file line numberDiff line numberDiff line change
@@ -66,19 +66,19 @@ fn test_formats() {
6666

6767
// Test custom formats.
6868

69-
// TODO: #[rocket(allow(unknown_format))]
69+
#[suppress(unknown_format)]
7070
#[get("/", format = "application/foo")]
7171
fn get_foo() -> &'static str { "get_foo" }
7272

73-
// TODO: #[rocket(allow(unknown_format))]
73+
#[suppress(unknown_format)]
7474
#[post("/", format = "application/foo")]
7575
fn post_foo() -> &'static str { "post_foo" }
7676

77-
// TODO: #[rocket(allow(unknown_format))]
77+
#[suppress(unknown_format)]
7878
#[get("/", format = "bar/baz", rank = 2)]
7979
fn get_bar_baz() -> &'static str { "get_bar_baz" }
8080

81-
// TODO: #[rocket(allow(unknown_format))]
81+
#[suppress(unknown_format)]
8282
#[put("/", format = "bar/baz")]
8383
fn put_bar_baz() -> &'static str { "put_bar_baz" }
8484

0 commit comments

Comments
 (0)