Skip to content

Commit bdd6cd2

Browse files
authored
Merge pull request #27 from DataDog/feat/more-function-lints
feat(function-lint): add Self-return patterns and MustNotExist rule for builder-style APIs
2 parents fc19c5d + 19b67f1 commit bdd6cd2

File tree

13 files changed

+270
-1
lines changed

13 files changed

+270
-1
lines changed

cargo_pup_lint_config/src/function_lint/builder.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,12 @@ impl<'a> FunctionConstraintBuilder<'a> {
123123
self
124124
}
125125

126+
/// Require that no function matching the selector exists
127+
pub fn must_not_exist(mut self) -> Self {
128+
self.add_rule_internal(FunctionRule::MustNotExist(self.current_severity));
129+
self
130+
}
131+
126132
/// Create a new MaxLength rule with the current severity
127133
pub fn create_max_length_rule(&self, length: usize) -> FunctionRule {
128134
FunctionRule::MaxLength(length, self.current_severity)

cargo_pup_lint_config/src/function_lint/matcher.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,21 @@ impl FunctionMatcher {
6060
pattern.into(),
6161
)))
6262
}
63+
64+
/// Matches functions that return `Self` by value
65+
pub fn returns_self(&self) -> FunctionMatchNode {
66+
FunctionMatchNode::Leaf(FunctionMatch::ReturnsType(ReturnTypePattern::SelfValue))
67+
}
68+
69+
/// Matches functions that return `&Self`
70+
pub fn returns_self_ref(&self) -> FunctionMatchNode {
71+
FunctionMatchNode::Leaf(FunctionMatch::ReturnsType(ReturnTypePattern::SelfRef))
72+
}
73+
74+
/// Matches functions that return `&mut Self`
75+
pub fn returns_self_mut_ref(&self) -> FunctionMatchNode {
76+
FunctionMatchNode::Leaf(FunctionMatch::ReturnsType(ReturnTypePattern::SelfMutRef))
77+
}
6378
}
6479

6580
/// Node in the matcher expression tree

cargo_pup_lint_config/src/function_lint/types.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ pub enum ReturnTypePattern {
1616
Regex(String),
1717
/// Match Result<T, E> where E implements Error trait
1818
ResultWithErrorImpl,
19+
/// Match when the function returns `Self` by value (e.g., a consuming builder-style method)
20+
SelfValue,
21+
/// Match when the function returns `&Self` (immutable reference, e.g., fluent interface)
22+
SelfRef,
23+
/// Match when the function returns `&mut Self` (mutable reference, e.g., classic builder setter)
24+
SelfMutRef,
1925
}
2026

2127
/// Specifies how to match functions for linting
@@ -52,6 +58,8 @@ pub enum FunctionRule {
5258
MaxLength(usize, Severity),
5359
/// Enforces that Result error types must implement the Error trait
5460
ResultErrorMustImplementError(Severity),
61+
/// Enforces that a function matching the selector must not exist at all
62+
MustNotExist(Severity),
5563
}
5664

5765
// Helper methods for FunctionRule

cargo_pup_lint_impl/src/lints/function_lint/lint.rs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,16 @@ use rustc_middle::ty::TyKind;
1212
use rustc_session::impl_lint_pass;
1313
use rustc_span::BytePos;
1414

15+
// Helper: retrieve the concrete Self type of the impl the method belongs to, if any
16+
fn get_self_type<'tcx>(
17+
ctx: &LateContext<'tcx>,
18+
fn_def_id: rustc_hir::def_id::DefId,
19+
) -> Option<rustc_middle::ty::Ty<'tcx>> {
20+
ctx.tcx
21+
.impl_of_method(fn_def_id)
22+
.map(|impl_def_id| ctx.tcx.type_of(impl_def_id).instantiate_identity())
23+
}
24+
1525
pub struct FunctionLint {
1626
name: String,
1727
matches: FunctionMatch,
@@ -143,6 +153,23 @@ fn evaluate_function_match(
143153
Err(_) => false,
144154
}
145155
}
156+
ReturnTypePattern::SelfValue => get_self_type(ctx, fn_def_id) == Some(return_ty),
157+
ReturnTypePattern::SelfRef => {
158+
match (get_self_type(ctx, fn_def_id), return_ty.kind()) {
159+
(Some(self_ty), &TyKind::Ref(_, inner, rustc_hir::Mutability::Not)) => {
160+
inner == self_ty
161+
}
162+
_ => false,
163+
}
164+
}
165+
ReturnTypePattern::SelfMutRef => {
166+
match (get_self_type(ctx, fn_def_id), return_ty.kind()) {
167+
(Some(self_ty), &TyKind::Ref(_, inner, rustc_hir::Mutability::Mut)) => {
168+
inner == self_ty
169+
}
170+
_ => false,
171+
}
172+
}
146173
}
147174
}
148175
FunctionMatch::AndMatches(left, right) => {
@@ -284,6 +311,21 @@ impl<'tcx> LateLintPass<'tcx> for FunctionLint {
284311
}
285312
}
286313
}
314+
FunctionRule::MustNotExist(severity) => {
315+
let sig_span = item
316+
.span
317+
.with_hi(item.span.lo() + BytePos((item_name.len() + 5) as u32));
318+
319+
span_lint_and_help(
320+
ctx,
321+
FUNCTION_LINT::get_by_severity(*severity),
322+
self.name().as_str(),
323+
sig_span,
324+
format!("Function '{item_name}' is forbidden by lint rule"),
325+
None,
326+
"Remove this function to satisfy the architectural rule",
327+
);
328+
}
287329
}
288330
}
289331
}
@@ -372,6 +414,21 @@ impl<'tcx> LateLintPass<'tcx> for FunctionLint {
372414
}
373415
}
374416
}
417+
FunctionRule::MustNotExist(severity) => {
418+
let sig_span = impl_item
419+
.span
420+
.with_hi(impl_item.span.lo() + BytePos((item_name.len() + 5) as u32));
421+
422+
span_lint_and_help(
423+
ctx,
424+
FUNCTION_LINT::get_by_severity(*severity),
425+
self.name().as_str(),
426+
sig_span,
427+
format!("Function '{item_name}' is forbidden by lint rule"),
428+
None,
429+
"Remove this function to satisfy the architectural rule",
430+
);
431+
}
375432
}
376433
}
377434
}

test_app/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test_app/expected_output

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,4 +240,24 @@ warning: Struct 'MyBadlyNamedThing' is public, but must be private
240240
= note: Applied by cargo-pup rule 'trait_restrictions'.
241241
= note: `#[warn(struct_lint_must_be_private)]` on by default
242242

243+
error: Function 'with_width' is forbidden by lint rule
244+
--> src/builder_style/mod.rs:8:5
245+
|
246+
8 | pub fn with_width(self, width: u32) -> Self {
247+
| ^^^^^^^^^^^^^^^
248+
|
249+
= help: Remove this function to satisfy the architectural rule
250+
= note: Applied by cargo-pup rule 'builder_style_with_consuming_forbidden'.
251+
= note: `#[deny(function_lint)]` on by default
252+
253+
error: Function 'set_height' is forbidden by lint rule
254+
--> src/builder_style/mod.rs:13:5
255+
|
256+
13 | pub fn set_height(self, height: u32) -> Self {
257+
| ^^^^^^^^^^^^^^^
258+
|
259+
= help: Remove this function to satisfy the architectural rule
260+
= note: Applied by cargo-pup rule 'builder_style_set_consuming_forbidden'.
261+
243262
warning: `test_app` (bin "test_app") generated 20 warnings
263+
error: could not compile `test_app` (bin "test_app") due to 2 previous errors; 20 warnings emitted

test_app/pup.ron

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,5 +99,25 @@
9999
),
100100
],
101101
)),
102+
Function((
103+
name: "builder_style_with_consuming_forbidden",
104+
matches: AndMatches(
105+
NameRegex("^with_.*"),
106+
ReturnsType(SelfValue)
107+
),
108+
rules: [
109+
MustNotExist(Error),
110+
],
111+
)),
112+
Function((
113+
name: "builder_style_set_consuming_forbidden",
114+
matches: AndMatches(
115+
NameRegex("^set_.*"),
116+
ReturnsType(SelfValue)
117+
),
118+
rules: [
119+
MustNotExist(Error),
120+
],
121+
)),
102122
],
103123
)

test_app/src/builder_style/mod.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
pub struct WidgetBuilder {
2+
width: u32,
3+
height: u32,
4+
}
5+
6+
impl WidgetBuilder {
7+
// BAD: Consuming builder – should be caught by the lint (returns `Self`)
8+
pub fn with_width(self, width: u32) -> Self {
9+
Self { width, ..self }
10+
}
11+
12+
// BAD: Consumes self but uses a `set_` prefix – also forbidden
13+
pub fn set_height(self, height: u32) -> Self {
14+
Self { height, ..self }
15+
}
16+
17+
// GOOD: Reference-based setter – allowed (`&mut self` → `&mut Self`)
18+
pub fn set_width(&mut self, width: u32) -> &mut Self {
19+
self.width = width;
20+
self
21+
}
22+
23+
// GOOD: Reference-based "with_" method – allowed
24+
pub fn with_height(&mut self, height: u32) -> &mut Self {
25+
self.height = height;
26+
self
27+
}
28+
}

test_app/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ mod module_usage;
1212
mod must_be_empty;
1313
mod result_error;
1414
mod trait_impl;
15+
mod builder_style;
1516

1617
fn main() {
1718
println!("Hello, world!");

test_app/tests/pup_ron_test.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,26 @@ fn test_lint_config() {
9898
.enforce_error_trait_implementation()
9999
.build();
100100

101+
// ------------------------------------------------------------------
102+
// Builder style lint rules (demonstrates consuming vs reference pattern)
103+
// ------------------------------------------------------------------
104+
105+
builder
106+
.function_lint()
107+
.lint_named("builder_style_with_consuming_forbidden")
108+
.matching(|m| m.name_regex("^with_.*").and(m.returns_self()))
109+
.with_severity(Severity::Error)
110+
.must_not_exist()
111+
.build();
112+
113+
builder
114+
.function_lint()
115+
.lint_named("builder_style_set_consuming_forbidden")
116+
.matching(|m| m.name_regex("^set_.*").and(m.returns_self()))
117+
.with_severity(Severity::Error)
118+
.must_not_exist()
119+
.build();
120+
101121
// Write the configuration to pup.ron using the fixed write_to_file method
102122
builder
103123
.write_to_file("pup.ron")

0 commit comments

Comments
 (0)