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#[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}