mago_linter/plugin/consistency/rules/
string_interpolation_braces.rs

1use 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                // Either literal string part or braced expression, so continue.
59                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}