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