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