Skip to main content

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, RuleMetadata, 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    metadata: RuleMetadata,
162    strategy: ReplacementStrategy,
163}
164
165impl FlawlessRule for RemoveInterpolatedString {
166    fn flawless_process(&self, block: &mut Block, _: &Context) {
167        const STRING_FORMAT_IDENTIFIER: &str = "__DARKLUA_STR_FMT";
168        const TOSTRING_IDENTIFIER: &str = "__DARKLUA_TO_STR";
169
170        let mut processor = RemoveInterpolatedStringProcessor::new(
171            self.strategy,
172            STRING_FORMAT_IDENTIFIER,
173            TOSTRING_IDENTIFIER,
174        );
175        ScopeVisitor::visit_block(block, &mut processor);
176
177        if processor.define_string_format || processor.define_tostring {
178            let mut variables = Vec::new();
179            let mut values = Vec::new();
180
181            if processor.define_string_format {
182                variables.push(TypedIdentifier::new(STRING_FORMAT_IDENTIFIER));
183                values.push(
184                    FieldExpression::new(
185                        Prefix::from_name(DEFAULT_STRING_LIBRARY),
186                        DEFAULT_STRING_FORMAT_NAME,
187                    )
188                    .into(),
189                );
190            }
191
192            if processor.define_tostring {
193                variables.push(TypedIdentifier::new(TOSTRING_IDENTIFIER));
194                values.push(Identifier::new(DEFAULT_TOSTRING_IDENTIFIER).into());
195            }
196
197            block.insert_statement(0, LocalAssignStatement::new(variables, values));
198        }
199    }
200}
201
202impl RuleConfiguration for RemoveInterpolatedString {
203    fn configure(&mut self, properties: RuleProperties) -> Result<(), RuleConfigurationError> {
204        for (key, value) in properties {
205            match key.as_str() {
206                "strategy" => {
207                    self.strategy = match value.expect_string(&key)?.as_str() {
208                        "string" => ReplacementStrategy::StringSpecifier,
209                        "tostring" => ReplacementStrategy::ToStringSpecifier,
210                        unexpected => {
211                            return Err(RuleConfigurationError::UnexpectedValue {
212                                property: "strategy".to_owned(),
213                                message: format!(
214                                    "invalid value `{}` (must be `string` or `tostring`)",
215                                    unexpected
216                                ),
217                            })
218                        }
219                    };
220                }
221                _ => return Err(RuleConfigurationError::UnexpectedProperty(key)),
222            }
223        }
224
225        Ok(())
226    }
227
228    fn get_name(&self) -> &'static str {
229        REMOVE_INTERPOLATED_STRING_RULE_NAME
230    }
231
232    fn serialize_to_properties(&self) -> RuleProperties {
233        let mut properties = RuleProperties::new();
234
235        match self.strategy {
236            ReplacementStrategy::StringSpecifier => {}
237            ReplacementStrategy::ToStringSpecifier => {
238                properties.insert("strategy".to_owned(), "tostring".into());
239            }
240        }
241
242        properties
243    }
244
245    fn set_metadata(&mut self, metadata: RuleMetadata) {
246        self.metadata = metadata;
247    }
248
249    fn metadata(&self) -> &RuleMetadata {
250        &self.metadata
251    }
252}
253
254#[cfg(test)]
255mod test {
256    use super::*;
257    use crate::rules::Rule;
258
259    use insta::assert_json_snapshot;
260
261    fn new_rule() -> RemoveInterpolatedString {
262        RemoveInterpolatedString::default()
263    }
264
265    #[test]
266    fn serialize_default_rule() {
267        let rule: Box<dyn Rule> = Box::new(new_rule());
268
269        assert_json_snapshot!(rule, @r###""remove_interpolated_string""###);
270    }
271
272    #[test]
273    fn serialize_rule_with_tostring_strategy() {
274        let rule: Box<dyn Rule> = Box::new(RemoveInterpolatedString {
275            metadata: RuleMetadata::default(),
276            strategy: ReplacementStrategy::ToStringSpecifier,
277        });
278
279        assert_json_snapshot!(rule, @r###"
280        {
281          "rule": "remove_interpolated_string",
282          "strategy": "tostring"
283        }
284        "###);
285    }
286
287    #[test]
288    fn configure_with_extra_field_error() {
289        let result = json5::from_str::<Box<dyn Rule>>(
290            r#"{
291            rule: 'remove_interpolated_string',
292            prop: "something",
293        }"#,
294        );
295        insta::assert_snapshot!(result.unwrap_err().to_string(), @"unexpected field 'prop' at line 1 column 1");
296    }
297}