darklua_core/rules/
remove_interpolated_string.rs

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