Skip to content

Commit 982fbe1

Browse files
Add new include_file_outside_project lint
1 parent 73bad36 commit 982fbe1

File tree

6 files changed

+171
-2
lines changed

6 files changed

+171
-2
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5555,6 +5555,7 @@ Released 2018-09-13
55555555
[`implied_bounds_in_impls`]: https://rust-lang.github.io/rust-clippy/master/index.html#implied_bounds_in_impls
55565556
[`impossible_comparisons`]: https://rust-lang.github.io/rust-clippy/master/index.html#impossible_comparisons
55575557
[`imprecise_flops`]: https://rust-lang.github.io/rust-clippy/master/index.html#imprecise_flops
5558+
[`include_file_outside_project`]: https://rust-lang.github.io/rust-clippy/master/index.html#include_file_outside_project
55585559
[`incompatible_msrv`]: https://rust-lang.github.io/rust-clippy/master/index.html#incompatible_msrv
55595560
[`inconsistent_digit_grouping`]: https://rust-lang.github.io/rust-clippy/master/index.html#inconsistent_digit_grouping
55605561
[`inconsistent_struct_constructor`]: https://rust-lang.github.io/rust-clippy/master/index.html#inconsistent_struct_constructor

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
A collection of lints to catch common mistakes and improve your [Rust](https://github.com/rust-lang/rust) code.
77

8-
[There are over 700 lints included in this crate!](https://rust-lang.github.io/rust-clippy/master/index.html)
8+
[There are over 750 lints included in this crate!](https://rust-lang.github.io/rust-clippy/master/index.html)
99

1010
Lints are divided into categories, each with a default [lint level](https://doc.rust-lang.org/rustc/lints/levels.html).
1111
You can choose how much Clippy is supposed to ~~annoy~~ help you by changing the lint level by category.

book/src/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
A collection of lints to catch common mistakes and improve your
77
[Rust](https://github.com/rust-lang/rust) code.
88

9-
[There are over 700 lints included in this crate!](https://rust-lang.github.io/rust-clippy/master/index.html)
9+
[There are over 750 lints included in this crate!](https://rust-lang.github.io/rust-clippy/master/index.html)
1010

1111
Lints are divided into categories, each with a default [lint
1212
level](https://doc.rust-lang.org/rustc/lints/levels.html). You can choose how

clippy_lints/src/declared_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@ pub static LINTS: &[&crate::LintInfo] = &[
222222
crate::implicit_saturating_sub::IMPLICIT_SATURATING_SUB_INFO,
223223
crate::implicit_saturating_sub::INVERTED_SATURATING_SUB_INFO,
224224
crate::implied_bounds_in_impls::IMPLIED_BOUNDS_IN_IMPLS_INFO,
225+
crate::include_file_outside_project::INCLUDE_FILE_OUTSIDE_PROJECT_INFO,
225226
crate::incompatible_msrv::INCOMPATIBLE_MSRV_INFO,
226227
crate::inconsistent_struct_constructor::INCONSISTENT_STRUCT_CONSTRUCTOR_INFO,
227228
crate::index_refutable_slice::INDEX_REFUTABLE_SLICE_INFO,
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
use rustc_ast::{Attribute, LitKind, MetaItem, MetaItemInner};
2+
use rustc_data_structures::fx::FxHashSet;
3+
use rustc_hir::{Expr, ExprKind, HirId, Item};
4+
use rustc_lint::{LateContext, LateLintPass};
5+
use rustc_session::impl_lint_pass;
6+
use rustc_span::{FileName, Span, sym};
7+
8+
use clippy_utils::diagnostics::span_lint_and_then;
9+
use clippy_utils::macros::root_macro_call_first_node;
10+
11+
use std::path::{Path, PathBuf};
12+
13+
declare_clippy_lint! {
14+
/// ### What it does
15+
/// Check if files included with one of the `include` macros (ie, `include!`, `include_bytes!`
16+
/// and `include_str!`) or the `path` attribute are actually part of the project.
17+
///
18+
/// ### Why is this bad?
19+
/// If the included file is outside of the project folder, it will not be part of the releases,
20+
/// prevent project to work when others use it.
21+
///
22+
/// ### Example
23+
/// ```ignore
24+
/// let x = include_str!("/etc/passwd");
25+
/// ```
26+
/// Use instead:
27+
/// ```ignore
28+
/// let x = include_str!("./passwd");
29+
/// ```
30+
#[clippy::version = "1.84.0"]
31+
pub INCLUDE_FILE_OUTSIDE_PROJECT,
32+
suspicious,
33+
"checks that all included files are inside the project folder"
34+
}
35+
36+
pub(crate) struct IncludeFileOutsideProject {
37+
cargo_manifest_dir: Option<PathBuf>,
38+
warned_spans: FxHashSet<PathBuf>,
39+
}
40+
41+
impl_lint_pass!(IncludeFileOutsideProject => [INCLUDE_FILE_OUTSIDE_PROJECT]);
42+
43+
impl IncludeFileOutsideProject {
44+
pub(crate) fn new() -> Self {
45+
Self {
46+
cargo_manifest_dir: std::env::var("CARGO_MANIFEST_DIR").ok().map(|dir| PathBuf::from(dir)),
47+
warned_spans: FxHashSet::default(),
48+
}
49+
}
50+
51+
fn check_file_path(&mut self, cx: &LateContext<'_>, span: Span) {
52+
if span.is_dummy() {
53+
return;
54+
}
55+
let source_map = cx.tcx.sess.source_map();
56+
let file = source_map.lookup_char_pos(span.lo()).file;
57+
if let FileName::Real(real_filename) = file.name.clone()
58+
&& let Some(file_path) = real_filename.into_local_path()
59+
&& let Ok(file_path) = file_path.canonicalize()
60+
// Only lint once per path for `include` macros.
61+
&& !self.warned_spans.contains(&file_path)
62+
&& !self.is_part_of_project_dir(&file_path)
63+
{
64+
let span = span.source_callsite();
65+
self.emit_error(cx, span.with_hi(span.lo()), file_path);
66+
}
67+
}
68+
69+
fn is_part_of_project_dir(&self, file_path: &PathBuf) -> bool {
70+
if let Some(ref cargo_manifest_dir) = self.cargo_manifest_dir {
71+
// Check if both paths start with the same thing.
72+
let mut file_iter = file_path.iter();
73+
74+
for cargo_item in cargo_manifest_dir.iter() {
75+
match file_iter.next() {
76+
Some(file_path) if file_path == cargo_item => {},
77+
_ => {
78+
// If we enter this arm, it means that the included file path is not
79+
// into the cargo manifest folder.
80+
return false;
81+
},
82+
}
83+
}
84+
}
85+
true
86+
}
87+
88+
fn emit_error(&mut self, cx: &LateContext<'_>, span: Span, file_path: PathBuf) {
89+
#[expect(clippy::collapsible_span_lint_calls, reason = "rust-clippy#7797")]
90+
span_lint_and_then(
91+
cx,
92+
INCLUDE_FILE_OUTSIDE_PROJECT,
93+
span,
94+
"attempted to include a file outside of the project",
95+
|diag| {
96+
diag.note(format!(
97+
"file is located at `{}` which is outside of project folder (`{}`)",
98+
file_path.display(),
99+
self.cargo_manifest_dir.as_ref().unwrap().display(),
100+
));
101+
},
102+
);
103+
self.warned_spans.insert(file_path);
104+
}
105+
106+
fn check_hir_id(&mut self, cx: &LateContext<'_>, span: Span, hir_id: HirId) {
107+
if self.cargo_manifest_dir.is_some()
108+
&& let hir = cx.tcx.hir()
109+
&& let Some(parent_hir_id) = hir.parent_id_iter(hir_id).next()
110+
&& let parent_span = hir.span(parent_hir_id)
111+
&& !parent_span.contains(span)
112+
{
113+
self.check_file_path(cx, span);
114+
}
115+
}
116+
117+
fn check_attribute(&mut self, cx: &LateContext<'_>, attr: &MetaItem) {
118+
let Some(ident) = attr.ident() else { return };
119+
if ident.name == sym::path {
120+
if let Some(value) = attr.value_str()
121+
&& let Some(span) = attr.name_value_literal_span()
122+
&& let file_path = Path::new(value.as_str())
123+
&& let Ok(file_path) = file_path.canonicalize()
124+
&& !self.is_part_of_project_dir(&file_path)
125+
{
126+
self.emit_error(cx, span, file_path);
127+
}
128+
} else if ident.name == sym::cfg_attr
129+
&& let Some(&[_, MetaItemInner::MetaItem(ref attr)]) = attr.meta_item_list()
130+
{
131+
self.check_attribute(cx, attr);
132+
}
133+
}
134+
}
135+
136+
impl LateLintPass<'_> for IncludeFileOutsideProject {
137+
fn check_expr(&mut self, cx: &LateContext<'_>, expr: &'_ Expr<'_>) {
138+
if !expr.span.from_expansion() {
139+
self.check_hir_id(cx, expr.span, expr.hir_id);
140+
} else if let ExprKind::Lit(lit) = &expr.kind
141+
&& matches!(lit.node, LitKind::ByteStr(..) | LitKind::Str(..))
142+
&& let Some(macro_call) = root_macro_call_first_node(cx, expr)
143+
&& (cx.tcx.is_diagnostic_item(sym::include_bytes_macro, macro_call.def_id)
144+
|| cx.tcx.is_diagnostic_item(sym::include_str_macro, macro_call.def_id))
145+
{
146+
self.check_hir_id(cx, expr.span, expr.hir_id);
147+
}
148+
}
149+
150+
fn check_item(&mut self, cx: &LateContext<'_>, item: &'_ Item<'_>) {
151+
// Interestingly enough, `include!` content is not considered expanded. Which allows us
152+
// to easily filter out items we're not interested into.
153+
if !item.span.from_expansion() {
154+
self.check_hir_id(cx, item.span, item.hir_id());
155+
}
156+
}
157+
158+
fn check_attributes(&mut self, cx: &LateContext<'_>, attrs: &[Attribute]) {
159+
for attr in attrs {
160+
if let Some(attr) = attr.meta() {
161+
self.check_attribute(cx, &attr);
162+
}
163+
}
164+
}
165+
}

clippy_lints/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ mod implicit_return;
160160
mod implicit_saturating_add;
161161
mod implicit_saturating_sub;
162162
mod implied_bounds_in_impls;
163+
mod include_file_outside_project;
163164
mod incompatible_msrv;
164165
mod inconsistent_struct_constructor;
165166
mod index_refutable_slice;
@@ -949,5 +950,6 @@ pub fn register_lints(store: &mut rustc_lint::LintStore, conf: &'static Conf) {
949950
store.register_late_pass(move |_| Box::new(unused_trait_names::UnusedTraitNames::new(conf)));
950951
store.register_late_pass(|_| Box::new(manual_ignore_case_cmp::ManualIgnoreCaseCmp));
951952
store.register_late_pass(|_| Box::new(unnecessary_literal_bound::UnnecessaryLiteralBound));
953+
store.register_late_pass(|_| Box::new(include_file_outside_project::IncludeFileOutsideProject::new()));
952954
// add lints here, do not remove this comment, it's used in `new_lint`
953955
}

0 commit comments

Comments
 (0)