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