Skip to content

Commit f8ba47b

Browse files
Add rule for no assignment in function parameters
1 parent 450e2ee commit f8ba47b

File tree

5 files changed

+282
-0
lines changed

5 files changed

+282
-0
lines changed

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

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

crates/linter/src/rule/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@ define_rules! {
222222
NoClosingTag(no_closing_tag @ NoClosingTagRule),
223223
NoBooleanLiteralComparison(no_boolean_literal_comparison @ NoBooleanLiteralComparisonRule),
224224
NoBooleanFlagParameter(no_boolean_flag_parameter @ NoBooleanFlagParameterRule),
225+
NoAssignInArgument(no_assign_in_argument @ NoAssignInArgumentRule),
225226
NoAssignInCondition(no_assign_in_condition @ NoAssignInConditionRule),
226227
NoAliasFunction(no_alias_function @ NoAliasFunctionRule),
227228
LowercaseTypeHint(lowercase_type_hint @ LowercaseTypeHintRule),

crates/linter/src/settings.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ use crate::rule::LowercaseKeywordConfig;
3939
use crate::rule::LowercaseTypeHintConfig;
4040
use crate::rule::MiddlewareInRoutesConfig;
4141
use crate::rule::NoAliasFunctionConfig;
42+
use crate::rule::NoAssignInArgumentConfig;
4243
use crate::rule::NoAssignInConditionConfig;
4344
use crate::rule::NoBooleanFlagParameterConfig;
4445
use crate::rule::NoBooleanLiteralComparisonConfig;
@@ -227,6 +228,7 @@ pub struct RulesSettings {
227228
pub no_closing_tag: RuleSettings<NoClosingTagConfig>,
228229
pub no_boolean_literal_comparison: RuleSettings<NoBooleanLiteralComparisonConfig>,
229230
pub no_boolean_flag_parameter: RuleSettings<NoBooleanFlagParameterConfig>,
231+
pub no_assign_in_argument: RuleSettings<NoAssignInArgumentConfig>,
230232
pub no_assign_in_condition: RuleSettings<NoAssignInConditionConfig>,
231233
pub no_alias_function: RuleSettings<NoAliasFunctionConfig>,
232234
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
@@ -13,6 +13,7 @@ This document details the rules available in the `Correctness` category.
1313
| Constant Type | [`constant-type`](#constant-type) |
1414
| Identity Comparison | [`identity-comparison`](#identity-comparison) |
1515
| Invalid Open Tag | [`invalid-open-tag`](#invalid-open-tag) |
16+
| No Assign In Argument | [`no-assign-in-argument`](#no-assign-in-argument) |
1617
| No Assign In Condition | [`no-assign-in-condition`](#no-assign-in-condition) |
1718
| No Boolean Literal Comparison | [`no-boolean-literal-comparison`](#no-boolean-literal-comparison) |
1819
| No Empty Catch Clause | [`no-empty-catch-clause`](#no-empty-catch-clause) |
@@ -195,6 +196,40 @@ echo 'Hello, world!';
195196
```
196197

197198

199+
## <a id="no-assign-in-argument"></a>`no-assign-in-argument`
200+
201+
Detects assignments in function call arguments which can lead to unexpected behavior and make
202+
the code harder to read and understand.
203+
204+
205+
206+
### Configuration
207+
208+
| Option | Type | Default |
209+
| :--- | :--- | :--- |
210+
| `enabled` | `boolean` | `true` |
211+
| `level` | `string` | `"warning"` |
212+
213+
### Examples
214+
215+
#### Correct code
216+
217+
```php
218+
<?php
219+
220+
$x = 5;
221+
foo($x);
222+
```
223+
224+
#### Incorrect code
225+
226+
```php
227+
<?php
228+
229+
foo($x = 5);
230+
```
231+
232+
198233
## <a id="no-assign-in-condition"></a>`no-assign-in-condition`
199234

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

0 commit comments

Comments
 (0)