mago_linter/plugin/consistency/rules/
string_interpolation_braces.rs1use indoc::indoc;
2
3use mago_fixer::SafetyClassification;
4use mago_reporting::*;
5use mago_span::HasSpan;
6use mago_syntax::ast::*;
7
8use crate::context::LintContext;
9use crate::definition::RuleDefinition;
10use crate::definition::RuleUsageExample;
11use crate::directive::LintDirective;
12use crate::rule::Rule;
13
14#[derive(Clone, Debug)]
15pub struct StringInterpolationBracesRule;
16
17impl Rule for StringInterpolationBracesRule {
18 fn get_definition(&self) -> RuleDefinition {
19 RuleDefinition::enabled("String Interpolation Braces", Level::Note)
20 .with_description(indoc! {"
21 Enforces the use of curly braces around variables within string interpolation.
22
23 Using curly braces (`{$variable}`) within interpolated strings ensures clarity and avoids potential ambiguity, especially when variables are followed by alphanumeric characters. This rule promotes consistent and predictable code.
24 "})
25 .with_example(RuleUsageExample::valid(
26 "Using braces within string interpolation",
27 indoc! {r#"
28 <?php
29
30 $a = "Hello, {$name}!";
31 $b = "Hello, {$name}!";
32 $c = "Hello, {$$name}!";
33 $d = "Hello, {${$object->getMethod()}}!";
34 "#},
35 ))
36
37 .with_example(RuleUsageExample::invalid(
38 "Using variables without braces within string interpolation",
39 indoc! {r#"
40 <?php
41
42 $a = "Hello, $name!";
43 $b = "Hello, ${name}!";
44 $c = "Hello, ${$name}!";
45 $d = "Hello, ${$object->getMethod()}!";
46 "#},
47 ))
48 }
49
50 fn lint_node(&self, node: Node<'_>, context: &mut LintContext<'_>) -> LintDirective {
51 let Node::CompositeString(composite_string) = node else {
52 return LintDirective::default();
53 };
54
55 let mut unbraced_expressions = vec![];
56 for part in composite_string.parts().iter() {
57 let StringPart::Expression(expression) = part else {
58 continue;
60 };
61
62 unbraced_expressions.push((
63 expression.span(),
64 !matches!(
65 expression.as_ref(),
66 Expression::Variable(Variable::Indirect(variable))
67 if matches!(
68 variable.expression.as_ref(),
69 Expression::Identifier(_) | Expression::Variable(_)
70 )
71 ),
72 ));
73 }
74
75 if unbraced_expressions.is_empty() {
76 return LintDirective::default();
77 }
78
79 let mut issue = Issue::new(context.level(), "Unbraced variable in string interpolation").with_annotation(
80 Annotation::primary(composite_string.span())
81 .with_message("String interpolation contains unbraced variables."),
82 );
83
84 for (span, _) in &unbraced_expressions {
85 issue = issue.with_annotation(
86 Annotation::secondary(*span).with_message("Variable should be enclosed in curly braces."),
87 );
88 }
89
90 issue = issue.with_note("Using curly braces around variables in interpolated strings improves readability and prevents potential parsing issues.")
91 .with_help("Wrap the variable in curly braces, e.g., `{$variable}`.");
92
93 context.propose(issue, |plan| {
94 for (span, wrap_in_braces) in unbraced_expressions {
95 if wrap_in_braces {
96 plan.insert(span.start.offset, "{", SafetyClassification::Safe);
97 plan.insert(span.end.offset, "}", SafetyClassification::Safe);
98 } else {
99 plan.replace(span.start.offset..span.start.offset + 2, "{$", SafetyClassification::Safe);
100 }
101 }
102 });
103
104 LintDirective::default()
105 }
106}