Skip to content

Commit 111e521

Browse files
Add new include_file_outside_project lint
1 parent ccf7c88 commit 111e521

File tree

4 files changed

+175
-0
lines changed

4 files changed

+175
-0
lines changed

CHANGELOG.md

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

clippy_lints/src/declared_lints.rs

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

clippy_lints/src/lib.rs

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

0 commit comments

Comments
 (0)