darklua_core/rules/
remove_interpolated_string.rs

1use std::{iter, ops};
2
3use crate::nodes::{
4    Block, Expression, FieldExpression, FunctionCall, Identifier, InterpolatedStringExpression,
5    InterpolationSegment, LocalAssignStatement, Prefix, StringExpression, TupleArguments,
6    TypedIdentifier,
7};
8use crate::process::{IdentifierTracker, NodeProcessor, NodeVisitor, ScopeVisitor};
9use crate::rules::{
10    Context, FlawlessRule, RuleConfiguration, RuleConfigurationError, RuleProperties,
11};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14enum ReplacementStrategy {
15    StringSpecifier,
16    ToStringSpecifier,
17}
18
19impl Default for ReplacementStrategy {
20    fn default() -> Self {
21        Self::StringSpecifier
22    }
23}
24
25struct RemoveInterpolatedStringProcessor {
26    string_format_identifier: String,
27    tostring_identifier: String,
28    define_string_format: bool,
29    define_tostring: bool,
30    identifier_tracker: IdentifierTracker,
31    strategy: ReplacementStrategy,
32}
33
34impl ops::Deref for RemoveInterpolatedStringProcessor {
35    type Target = IdentifierTracker;
36
37    fn deref(&self) -> &Self::Target {
38        &self.identifier_tracker
39    }
40}
41
42impl ops::DerefMut for RemoveInterpolatedStringProcessor {
43    fn deref_mut(&mut self) -> &mut Self::Target {
44        &mut self.identifier_tracker
45    }
46}
47
48const DEFAULT_TOSTRING_IDENTIFIER: &str = "tostring";
49const DEFAULT_STRING_LIBRARY: &str = "string";
50const DEFAULT_STRING_FORMAT_NAME: &str = "format";
51
52impl RemoveInterpolatedStringProcessor {
53    fn new(
54        strategy: ReplacementStrategy,
55        string_format_identifier: impl Into<String>,
56        tostring_identifier: impl Into<String>,
57    ) -> Self {
58        Self {
59            string_format_identifier: string_format_identifier.into(),
60            tostring_identifier: tostring_identifier.into(),
61            define_string_format: false,
62            define_tostring: false,
63            identifier_tracker: Default::default(),
64            strategy,
65        }
66    }
67
68    fn replace_with(&mut self, string: &InterpolatedStringExpression) -> Expression {
69        if string.is_empty() {
70            StringExpression::from_value("").into()
71        } else if string.len() == 1 {
72            match string.iter_segments().next().unwrap() {
73                InterpolationSegment::String(string_segment) => {
74                    StringExpression::from_value(string_segment.get_value()).into()
75                }
76                InterpolationSegment::Value(value_segment) => FunctionCall::from_name(
77                    if self.is_identifier_used(DEFAULT_TOSTRING_IDENTIFIER) {
78                        self.define_tostring = true;
79                        &self.tostring_identifier
80                    } else {
81                        DEFAULT_TOSTRING_IDENTIFIER
82                    },
83                )
84                .with_argument(value_segment.get_expression().clone())
85                .into(),
86            }
87        } else {
88            let arguments = iter::once(
89                StringExpression::from_value(string.iter_segments().fold(
90                    String::new(),
91                    |mut format_string, segment| {
92                        match segment {
93                            InterpolationSegment::String(string_segment) => {
94                                format_string
95                                    .push_str(&string_segment.get_value().replace('%', "%%"));
96                            }
97                            InterpolationSegment::Value(_) => {
98                                format_string.push_str(match self.strategy {
99                                    ReplacementStrategy::StringSpecifier => "%s",
100                                    ReplacementStrategy::ToStringSpecifier => "%*",
101                                });
102                            }
103                        }
104                        format_string
105                    },
106                ))
107                .into(),
108            )
109            .chain(
110                string
111                    .iter_segments()
112                    .filter_map(|segment| match segment {
113                        InterpolationSegment::Value(segment) => {
114                            Some(segment.get_expression().clone())
115                        }
116                        InterpolationSegment::String(_) => None,
117                    })
118                    .map(|value| match self.strategy {
119                        ReplacementStrategy::ToStringSpecifier => value,
120                        ReplacementStrategy::StringSpecifier => FunctionCall::from_name(
121                            if self.is_identifier_used(DEFAULT_TOSTRING_IDENTIFIER) {
122                                self.define_tostring = true;
123                                &self.tostring_identifier
124                            } else {
125                                DEFAULT_TOSTRING_IDENTIFIER
126                            },
127                        )
128                        .with_argument(value)
129                        .into(),
130                    }),
131            )
132            .collect::<TupleArguments>();
133
134            FunctionCall::from_prefix(if self.is_identifier_used(DEFAULT_STRING_LIBRARY) {
135                self.define_string_format = true;
136                Prefix::from_name(&self.string_format_identifier)
137            } else {
138                FieldExpression::new(
139                    Prefix::from_name(DEFAULT_STRING_LIBRARY),
140                    DEFAULT_STRING_FORMAT_NAME,
141                )
142                .into()
143            })
144            .with_arguments(arguments)
145            .into()
146        }
147    }
148}
149
150impl NodeProcessor for RemoveInterpolatedStringProcessor {
151    fn process_expression(&mut self, expression: &mut Expression) {
152        if let Expression::InterpolatedString(string) = expression {
153            *expression = self.replace_with(string);
154        }
155    }
156}
157
158pub const REMOVE_INTERPOLATED_STRING_RULE_NAME: &str = "remove_interpolated_string";
159
160/// A rule that removes interpolated strings.
161#[derive(Debug, Default, PartialEq, Eq)]
162pub struct RemoveInterpolatedString {
163    strategy: ReplacementStrategy,
164}
165
166impl FlawlessRule for RemoveInterpolatedString {
167    fn flawless_process(&self, block: &mut Block, _: &Context) {
168        const STRING_FORMAT_IDENTIFIER: &str = "__DARKLUA_STR_FMT";
169        const TOSTRING_IDENTIFIER: &str = "__DARKLUA_TO_STR";
170
171        let mut processor = RemoveInterpolatedStringProcessor::new(
172            self.strategy,
173            STRING_FORMAT_IDENTIFIER,
174            TOSTRING_IDENTIFIER,
175        );
176        ScopeVisitor::visit_block(block, &mut processor);
177
178        if processor.define_string_format || processor.define_tostring {
179            let mut variables = Vec::new();
180            let mut values = Vec::new();
181
182            if processor.define_string_format {
183                variables.push(TypedIdentifier::new(STRING_FORMAT_IDENTIFIER));
184                values.push(
185                    FieldExpression::new(
186                        Prefix::from_name(DEFAULT_STRING_LIBRARY),
187                        DEFAULT_STRING_FORMAT_NAME,
188                    )
189                    .into(),
190                );
191            }
192
193            if processor.define_tostring {
194                variables.push(TypedIdentifier::new(TOSTRING_IDENTIFIER));
195                values.push(Identifier::new(DEFAULT_TOSTRING_IDENTIFIER).into());
196            }
197
198            block.insert_statement(0, LocalAssignStatement::new(variables, values));
199        }
200    }
201}
202
203impl RuleConfiguration for RemoveInterpolatedString {
204    fn configure(&mut self, properties: RuleProperties) -> Result<(), RuleConfigurationError> {
205        for (key, value) in properties {
206            match key.as_str() {
207                "strategy" => {
208                    self.strategy = match value.expect_string(&key)?.as_str() {
209                        "string" => ReplacementStrategy::StringSpecifier,
210                        "tostring" => ReplacementStrategy::ToStringSpecifier,
211                        unexpected => {
212                            return Err(RuleConfigurationError::UnexpectedValue {
213                                property: "strategy".to_owned(),
214                                message: format!(
215                                    "invalid value `{}` (must be `string` or `tostring`)",
216                                    unexpected
217                                ),
218                            })
219                        }
220                    };
221                }
222                _ => return Err(RuleConfigurationError::UnexpectedProperty(key)),
223            }
224        }
225
226        Ok(())
227    }
228
229    fn get_name(&self) -> &'static str {
230        REMOVE_INTERPOLATED_STRING_RULE_NAME
231    }
232
233    fn serialize_to_properties(&self) -> RuleProperties {
234        let mut properties = RuleProperties::new();
235
236        match self.strategy {
237            ReplacementStrategy::StringSpecifier => {}
238            ReplacementStrategy::ToStringSpecifier => {
239                properties.insert("strategy".to_owned(), "tostring".into());
240            }
241        }
242
243        properties
244    }
245}
246
247#[cfg(test)]
248mod test {
249    use super::*;
250    use crate::rules::Rule;
251
252    use insta::assert_json_snapshot;
253
254    fn new_rule() -> RemoveInterpolatedString {
255        RemoveInterpolatedString::default()
256    }
257
258    #[test]
259    fn serialize_default_rule() {
260        let rule: Box<dyn Rule> = Box::new(new_rule());
261
262        assert_json_snapshot!("default_remove_interpolated_string", rule);
263    }
264
265    #[test]
266    fn serialize_rule_with_tostring_strategy() {
267        let rule: Box<dyn Rule> = Box::new(RemoveInterpolatedString {
268            strategy: ReplacementStrategy::ToStringSpecifier,
269        });
270
271        assert_json_snapshot!("remove_interpolated_string_tostring_strategy", rule);
272    }
273
274    #[test]
275    fn configure_with_extra_field_error() {
276        let result = json5::from_str::<Box<dyn Rule>>(
277            r#"{
278            rule: 'remove_interpolated_string',
279            prop: "something",
280        }"#,
281        );
282        pretty_assertions::assert_eq!(result.unwrap_err().to_string(), "unexpected field 'prop'");
283    }
284}