aboutsummaryrefslogtreecommitdiff
path: root/dylints/non_authenticated_routes/src/lib.rs
blob: 78bac58630bdf01f745fdf9e6a9d8bc3f9a3fc51 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
#![feature(rustc_private)]
#![feature(let_chains)]

extern crate rustc_arena;
extern crate rustc_ast;
extern crate rustc_ast_pretty;
extern crate rustc_attr;
extern crate rustc_data_structures;
extern crate rustc_errors;
extern crate rustc_hir;
extern crate rustc_hir_pretty;
extern crate rustc_index;
extern crate rustc_infer;
extern crate rustc_lexer;
extern crate rustc_middle;
extern crate rustc_mir_dataflow;
extern crate rustc_parse;
extern crate rustc_span;
extern crate rustc_target;
extern crate rustc_trait_selection;

use clippy_utils::diagnostics::span_lint;
use rustc_hir::{def_id::DefId, Item, ItemKind, QPath, TyKind};
use rustc_lint::{LateContext, LateLintPass};
use rustc_span::{symbol::Ident, Span, Symbol};

dylint_linting::impl_late_lint! {
    /// ### What it does
    ///
    /// ### Why is this bad?
    ///
    /// ### Known problems
    /// Remove if none.
    ///
    /// ### Example
    /// ```rust
    /// // example code where a warning is issued
    /// ```
    /// Use instead:
    /// ```rust
    /// // example code that does not raise a warning
    /// ```
    pub NON_AUTHENTICATED_ROUTES,
    Warn,
    "description goes here",
    NonAuthenticatedRoutes::default()
}

#[derive(Default)]
pub struct NonAuthenticatedRoutes {
    last_function_item: Option<(Ident, Span, bool)>,
}

// Collect all the attribute macros that are applied to the given span
fn attr_def_ids(mut span: rustc_span::Span) -> Vec<(DefId, Symbol, Option<DefId>)> {
    use rustc_span::hygiene::{walk_chain, ExpnKind, MacroKind};
    use rustc_span::{ExpnData, SyntaxContext};

    let mut def_ids = Vec::new();
    while span.ctxt() != SyntaxContext::root() {
        if let ExpnData {
            kind: ExpnKind::Macro(MacroKind::Attr, macro_symbol),
            macro_def_id: Some(def_id),
            parent_module,
            ..
        } = span.ctxt().outer_expn_data()
        {
            def_ids.push((def_id, macro_symbol, parent_module));
        }
        span = walk_chain(span, SyntaxContext::root());
    }
    def_ids
}

const ROCKET_MACRO_EXCEPTIONS: [(&str, &str); 1] = [("rocket::catch", "catch")];

const VALID_AUTH_HEADERS: [&str; 6] = [
    "auth::Headers",
    "auth::OrgHeaders",
    "auth::AdminHeaders",
    "auth::ManagerHeaders",
    "auth::ManagerHeadersLoose",
    "auth::OwnerHeaders",
];

impl<'tcx> LateLintPass<'tcx> for NonAuthenticatedRoutes {
    fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx Item) {
        if let ItemKind::Fn(sig, ..) = item.kind {
            let mut has_auth_headers = false;

            for input in sig.decl.inputs {
                let TyKind::Path(QPath::Resolved(_, path)) = input.kind else {
                    continue;
                };

                for seg in path.segments {
                    if let Some(def_id) = seg.res.opt_def_id() {
                        let def = cx.tcx.def_path_str(def_id);
                        if VALID_AUTH_HEADERS.contains(&def.as_str()) {
                            has_auth_headers = true;
                        }
                    }
                }
            }

            self.last_function_item = Some((item.ident, sig.span, has_auth_headers));
            return;
        }

        let ItemKind::Struct(_data, _generics) = item.kind else {
            return;
        };

        let def_ids = attr_def_ids(item.span);

        let mut is_rocket_route = false;

        for (def_id, sym, parent) in &def_ids {
            let def_id = cx.tcx.def_path_str(*def_id);
            let sym = sym.as_str();
            let parent = parent.map(|parent| cx.tcx.def_path_str(parent));

            if ROCKET_MACRO_EXCEPTIONS.contains(&(&def_id, sym)) {
                is_rocket_route = false;
                break;
            }

            if def_id.starts_with("rocket::") || parent.as_deref() == Some("rocket_codegen") {
                is_rocket_route = true;
                break;
            }
        }

        if !is_rocket_route {
            return;
        }

        let Some((func_ident, func_span, has_auth_headers)) = self.last_function_item.take() else {
            span_lint(cx, NON_AUTHENTICATED_ROUTES, item.span, "No function found before the expanded route");
            return;
        };

        if func_ident != item.ident {
            span_lint(
                cx,
                NON_AUTHENTICATED_ROUTES,
                item.span,
                "The function before the expanded route does not match the route",
            );
            return;
        }

        if !has_auth_headers {
            span_lint(
                cx,
                NON_AUTHENTICATED_ROUTES,
                func_span,
                "This Rocket route does not have any authentication headers",
            );
        }
    }
}

#[test]
fn ui() {
    dylint_testing::ui_test(env!("CARGO_PKG_NAME"), "ui");
}