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