Skip to content

Commit d1c3d32

Browse files
feat(linter): add rule for no assignment in function parameters (#821)
Co-authored-by: Seifeddine Gmati <[email protected]>
1 parent 9f20854 commit d1c3d32

File tree

5 files changed

+321
-0
lines changed

5 files changed

+321
-0
lines changed

crates/linter/src/rule/correctness/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ pub mod identity_comparison;
44
pub mod ineffective_format_ignore_next;
55
pub mod ineffective_format_ignore_region;
66
pub mod invalid_open_tag;
7+
pub mod no_assign_in_argument;
78
pub mod no_assign_in_condition;
89
pub mod no_boolean_literal_comparison;
910
pub mod no_empty_catch_clause;
@@ -21,6 +22,7 @@ pub use identity_comparison::*;
2122
pub use ineffective_format_ignore_next::*;
2223
pub use ineffective_format_ignore_region::*;
2324
pub use invalid_open_tag::*;
25+
pub use no_assign_in_argument::*;
2426
pub use no_assign_in_condition::*;
2527
pub use no_boolean_literal_comparison::*;
2628
pub use no_empty_catch_clause::*;
Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
use indoc::indoc;
2+
use schemars::JsonSchema;
3+
use serde::Deserialize;
4+
use serde::Serialize;
5+
6+
use mago_reporting::Annotation;
7+
use mago_reporting::Issue;
8+
use mago_reporting::Level;
9+
use mago_span::HasSpan;
10+
use mago_span::Span;
11+
use mago_syntax::ast::Assignment;
12+
use mago_syntax::ast::AssignmentOperator;
13+
use mago_syntax::ast::Expression;
14+
use mago_syntax::ast::Node;
15+
use mago_syntax::ast::NodeKind;
16+
use mago_syntax::ast::PartialArgument;
17+
18+
use crate::category::Category;
19+
use crate::context::LintContext;
20+
use crate::requirements::RuleRequirements;
21+
use crate::rule::Config;
22+
use crate::rule::LintRule;
23+
use crate::rule_meta::RuleMeta;
24+
use crate::settings::RuleSettings;
25+
26+
#[derive(Debug, Clone)]
27+
pub struct NoAssignInArgumentRule {
28+
meta: &'static RuleMeta,
29+
cfg: NoAssignInArgumentConfig,
30+
}
31+
32+
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize, Deserialize, JsonSchema)]
33+
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
34+
pub struct NoAssignInArgumentConfig {
35+
pub level: Level,
36+
}
37+
38+
impl Default for NoAssignInArgumentConfig {
39+
fn default() -> Self {
40+
Self { level: Level::Warning }
41+
}
42+
}
43+
44+
impl Config for NoAssignInArgumentConfig {
45+
fn default_enabled() -> bool {
46+
false
47+
}
48+
49+
fn level(&self) -> Level {
50+
self.level
51+
}
52+
}
53+
54+
impl LintRule for NoAssignInArgumentRule {
55+
type Config = NoAssignInArgumentConfig;
56+
57+
fn meta() -> &'static RuleMeta {
58+
const META: RuleMeta = RuleMeta {
59+
name: "No Assign In Argument",
60+
code: "no-assign-in-argument",
61+
description: indoc! {"
62+
Detects assignments in function call arguments which can lead to unexpected behavior and make
63+
the code harder to read and understand.
64+
"},
65+
good_example: indoc! {r"
66+
<?php
67+
68+
$x = 5;
69+
foo($x);
70+
"},
71+
bad_example: indoc! {r"
72+
<?php
73+
74+
foo($x = 5);
75+
"},
76+
category: Category::Correctness,
77+
requirements: RuleRequirements::None,
78+
};
79+
80+
&META
81+
}
82+
83+
fn targets() -> &'static [NodeKind] {
84+
const TARGETS: &[NodeKind] = &[NodeKind::ArgumentList, NodeKind::PartialArgumentList];
85+
86+
TARGETS
87+
}
88+
89+
fn build(settings: &RuleSettings<Self::Config>) -> Self {
90+
Self { meta: Self::meta(), cfg: settings.config }
91+
}
92+
93+
fn check<'arena>(&self, ctx: &mut LintContext<'_, 'arena>, node: Node<'_, 'arena>) {
94+
match node {
95+
Node::ArgumentList(list) => {
96+
for argument in list.arguments.iter() {
97+
self.check_expression_for_assignment(ctx, argument.value(), list.span());
98+
}
99+
}
100+
Node::PartialArgumentList(partial_list) => {
101+
// Check expression for assignment on non-placeholder arguments
102+
for argument in partial_list.arguments.iter() {
103+
let value = match argument {
104+
PartialArgument::Positional(pos) => &pos.value,
105+
PartialArgument::Named(named) => &named.value,
106+
_ => continue,
107+
};
108+
self.check_expression_for_assignment(ctx, value, partial_list.span());
109+
}
110+
}
111+
_ => (),
112+
}
113+
}
114+
}
115+
116+
impl NoAssignInArgumentRule {
117+
fn check_expression_for_assignment<'arena>(
118+
&self,
119+
ctx: &mut LintContext<'_, 'arena>,
120+
expression: &Expression<'arena>,
121+
argument_list_span: Span,
122+
) {
123+
if let Some(assignment) = get_assignment_from_expression(expression) {
124+
let mut issue = Issue::new(self.cfg.level(), "Avoid assignments in function call arguments.")
125+
.with_code(self.meta.code)
126+
.with_annotation(
127+
Annotation::primary(assignment.span()).with_message("This is an assignment"),
128+
)
129+
.with_annotation(
130+
Annotation::secondary(argument_list_span).with_message("In this argument list"),
131+
)
132+
.with_note("Assigning a value within a function call argument can lead to unexpected behavior and make the code harder to read and understand.")
133+
.with_help("Consider assigning the variable before the function call.");
134+
135+
if matches!(&assignment.operator, AssignmentOperator::Assign(_)) {
136+
issue = issue.with_note("It's easy to confuse assignment (`=`) with comparison (`==`) in this context. Ensure you're using the correct operator.");
137+
}
138+
139+
ctx.collector.report(issue);
140+
}
141+
}
142+
}
143+
144+
#[inline]
145+
fn get_assignment_from_expression<'ast, 'arena>(
146+
expression: &'ast Expression<'arena>,
147+
) -> Option<&'ast Assignment<'arena>> {
148+
match expression {
149+
Expression::Parenthesized(parenthesized) => get_assignment_from_expression(parenthesized.expression),
150+
Expression::Assignment(assignment) => Some(assignment),
151+
_ => None,
152+
}
153+
}
154+
155+
#[cfg(test)]
156+
mod tests {
157+
use indoc::indoc;
158+
159+
use super::NoAssignInArgumentRule;
160+
use crate::test_lint_failure;
161+
use crate::test_lint_success;
162+
163+
test_lint_failure! {
164+
name = assignment_in_function_argument,
165+
rule = NoAssignInArgumentRule,
166+
count = 1,
167+
code = indoc! {r"
168+
<?php
169+
170+
foo($x = 5);
171+
"}
172+
}
173+
174+
test_lint_failure! {
175+
name = assignment_in_method_argument,
176+
rule = NoAssignInArgumentRule,
177+
count = 1,
178+
code = indoc! {r"
179+
<?php
180+
181+
$obj->method($x = getValue());
182+
"}
183+
}
184+
185+
test_lint_failure! {
186+
name = assignment_in_static_method_argument,
187+
rule = NoAssignInArgumentRule,
188+
count = 1,
189+
code = indoc! {r"
190+
<?php
191+
192+
MyClass::method($x = 10);
193+
"}
194+
}
195+
196+
test_lint_failure! {
197+
name = assignment_in_null_safe_method_argument,
198+
rule = NoAssignInArgumentRule,
199+
count = 1,
200+
code = indoc! {r"
201+
<?php
202+
203+
$obj?->method($x = 5);
204+
"}
205+
}
206+
207+
test_lint_failure! {
208+
name = assignment_with_parentheses,
209+
rule = NoAssignInArgumentRule,
210+
count = 1,
211+
code = indoc! {r"
212+
<?php
213+
214+
foo(($x = 5));
215+
"}
216+
}
217+
218+
test_lint_failure! {
219+
name = multiple_arguments_with_assignment,
220+
rule = NoAssignInArgumentRule,
221+
count = 1,
222+
code = indoc! {r"
223+
<?php
224+
225+
foo($a, $b = 5, $c);
226+
"}
227+
}
228+
229+
test_lint_failure! {
230+
name = multiple_arguments_with_assignment_in_partial_arguments,
231+
rule = NoAssignInArgumentRule,
232+
count = 1,
233+
code = indoc! {r"
234+
<?php
235+
236+
foo(?, $a, $b = 5, $c);
237+
"}
238+
}
239+
240+
test_lint_success! {
241+
name = variable_used_as_argument,
242+
rule = NoAssignInArgumentRule,
243+
code = indoc! {r"
244+
<?php
245+
246+
$x = 5;
247+
foo($x);
248+
"}
249+
}
250+
251+
test_lint_success! {
252+
name = variable_used_as_argument_in_partial_arguments,
253+
rule = NoAssignInArgumentRule,
254+
code = indoc! {r"
255+
<?php
256+
257+
$x = 5;
258+
foo(?, $x);
259+
"}
260+
}
261+
262+
test_lint_success! {
263+
name = expression_without_assignment,
264+
rule = NoAssignInArgumentRule,
265+
code = indoc! {r"
266+
<?php
267+
268+
foo($x + 5);
269+
"}
270+
}
271+
272+
test_lint_success! {
273+
name = function_call_as_argument,
274+
rule = NoAssignInArgumentRule,
275+
code = indoc! {r"
276+
<?php
277+
278+
foo(getValue());
279+
"}
280+
}
281+
}

crates/linter/src/rule/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,7 @@ define_rules! {
248248
NoClosingTag(no_closing_tag @ NoClosingTagRule),
249249
NoBooleanLiteralComparison(no_boolean_literal_comparison @ NoBooleanLiteralComparisonRule),
250250
NoBooleanFlagParameter(no_boolean_flag_parameter @ NoBooleanFlagParameterRule),
251+
NoAssignInArgument(no_assign_in_argument @ NoAssignInArgumentRule),
251252
NoAssignInCondition(no_assign_in_condition @ NoAssignInConditionRule),
252253
NoAliasFunction(no_alias_function @ NoAliasFunctionRule),
253254
LowercaseTypeHint(lowercase_type_hint @ LowercaseTypeHintRule),

crates/linter/src/settings.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ use crate::rule::LowercaseKeywordConfig;
4242
use crate::rule::LowercaseTypeHintConfig;
4343
use crate::rule::MiddlewareInRoutesConfig;
4444
use crate::rule::NoAliasFunctionConfig;
45+
use crate::rule::NoAssignInArgumentConfig;
4546
use crate::rule::NoAssignInConditionConfig;
4647
use crate::rule::NoBooleanFlagParameterConfig;
4748
use crate::rule::NoBooleanLiteralComparisonConfig;
@@ -234,6 +235,7 @@ pub struct RulesSettings {
234235
pub no_closing_tag: RuleSettings<NoClosingTagConfig>,
235236
pub no_boolean_literal_comparison: RuleSettings<NoBooleanLiteralComparisonConfig>,
236237
pub no_boolean_flag_parameter: RuleSettings<NoBooleanFlagParameterConfig>,
238+
pub no_assign_in_argument: RuleSettings<NoAssignInArgumentConfig>,
237239
pub no_assign_in_condition: RuleSettings<NoAssignInConditionConfig>,
238240
pub no_alias_function: RuleSettings<NoAliasFunctionConfig>,
239241
pub lowercase_type_hint: RuleSettings<LowercaseTypeHintConfig>,

docs/tools/linter/rules/correctness.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ This document details the rules available in the `Correctness` category.
1515
| Ineffective Format Ignore Next | [`ineffective-format-ignore-next`](#ineffective-format-ignore-next) |
1616
| Ineffective Format Ignore Region | [`ineffective-format-ignore-region`](#ineffective-format-ignore-region) |
1717
| Invalid Open Tag | [`invalid-open-tag`](#invalid-open-tag) |
18+
| No Assign In Argument | [`no-assign-in-argument`](#no-assign-in-argument) |
1819
| No Assign In Condition | [`no-assign-in-condition`](#no-assign-in-condition) |
1920
| No Boolean Literal Comparison | [`no-boolean-literal-comparison`](#no-boolean-literal-comparison) |
2021
| No Empty Catch Clause | [`no-empty-catch-clause`](#no-empty-catch-clause) |
@@ -297,6 +298,40 @@ echo 'Hello, world!';
297298
```
298299

299300

301+
## <a id="no-assign-in-argument"></a>`no-assign-in-argument`
302+
303+
Detects assignments in function call arguments which can lead to unexpected behavior and make
304+
the code harder to read and understand.
305+
306+
307+
308+
### Configuration
309+
310+
| Option | Type | Default |
311+
| :--- | :--- | :--- |
312+
| `enabled` | `boolean` | `false` |
313+
| `level` | `string` | `"warning"` |
314+
315+
### Examples
316+
317+
#### Correct code
318+
319+
```php
320+
<?php
321+
322+
$x = 5;
323+
foo($x);
324+
```
325+
326+
#### Incorrect code
327+
328+
```php
329+
<?php
330+
331+
foo($x = 5);
332+
```
333+
334+
300335
## <a id="no-assign-in-condition"></a>`no-assign-in-condition`
301336

302337
Detects assignments in conditions which can lead to unexpected behavior and make the code harder

0 commit comments

Comments
 (0)