-
-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add rule to remove interpolated strings (#156)
A rule that replaces interpolated strings with `string.format` calls
1 parent
8f957a9
commit 9ac592f
Showing
12 changed files
with
414 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
--- | ||
description: Removes interpolated strings (backtick strings) | ||
added_in: "unreleased" | ||
parameters: | ||
- name: strategy | ||
type: '"string" or "tostring"' | ||
description: Defines how darklua converts the interpolated strings into `string.format` calls. The "string" strategy will make the rule use the `%s` specifier and the "tostring" strategy will use the `%*` specifier. | ||
default: string | ||
examples: | ||
- content: "return `abc`" | ||
- content: "return ``" | ||
- content: "return `+{value} (in seconds)`" | ||
- content: "return `Total = {#elements}`" | ||
--- | ||
|
||
This rule removes all interpolated strings and replaces them with `string.format` calls. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,266 @@ | ||
use std::{iter, ops}; | ||
|
||
use crate::nodes::{ | ||
Block, Expression, FieldExpression, FunctionCall, Identifier, InterpolatedStringExpression, | ||
InterpolationSegment, LocalAssignStatement, Prefix, StringExpression, TupleArguments, | ||
TypedIdentifier, | ||
}; | ||
use crate::process::{IdentifierTracker, NodeProcessor, NodeVisitor, ScopeVisitor}; | ||
use crate::rules::{ | ||
Context, FlawlessRule, RuleConfiguration, RuleConfigurationError, RuleProperties, | ||
}; | ||
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq)] | ||
enum ReplacementStrategy { | ||
StringSpecifier, | ||
ToStringSpecifier, | ||
} | ||
|
||
impl Default for ReplacementStrategy { | ||
fn default() -> Self { | ||
Self::StringSpecifier | ||
} | ||
} | ||
|
||
struct RemoveInterpolatedStringProcessor { | ||
string_format_identifier: String, | ||
tostring_identifier: String, | ||
define_string_format: bool, | ||
define_tostring: bool, | ||
identifier_tracker: IdentifierTracker, | ||
strategy: ReplacementStrategy, | ||
} | ||
|
||
impl ops::Deref for RemoveInterpolatedStringProcessor { | ||
type Target = IdentifierTracker; | ||
|
||
fn deref(&self) -> &Self::Target { | ||
&self.identifier_tracker | ||
} | ||
} | ||
|
||
impl ops::DerefMut for RemoveInterpolatedStringProcessor { | ||
fn deref_mut(&mut self) -> &mut Self::Target { | ||
&mut self.identifier_tracker | ||
} | ||
} | ||
|
||
const DEFAULT_TOSTRING_IDENTIFIER: &str = "tostring"; | ||
const DEFAULT_STRING_LIBRARY: &str = "string"; | ||
const DEFAULT_STRING_FORMAT_NAME: &str = "format"; | ||
|
||
impl RemoveInterpolatedStringProcessor { | ||
fn new( | ||
strategy: ReplacementStrategy, | ||
string_format_identifier: impl Into<String>, | ||
tostring_identifier: impl Into<String>, | ||
) -> Self { | ||
Self { | ||
string_format_identifier: string_format_identifier.into(), | ||
tostring_identifier: tostring_identifier.into(), | ||
define_string_format: false, | ||
define_tostring: false, | ||
identifier_tracker: Default::default(), | ||
strategy, | ||
} | ||
} | ||
|
||
fn replace_with(&mut self, string: &InterpolatedStringExpression) -> Expression { | ||
if string.is_empty() { | ||
StringExpression::from_value("").into() | ||
} else if string.len() == 1 { | ||
match string.iter_segments().next().unwrap() { | ||
InterpolationSegment::String(string_segment) => { | ||
StringExpression::from_value(string_segment.get_value()).into() | ||
} | ||
InterpolationSegment::Value(value_segment) => FunctionCall::from_name( | ||
if self.is_identifier_used(DEFAULT_TOSTRING_IDENTIFIER) { | ||
self.define_tostring = true; | ||
&self.tostring_identifier | ||
} else { | ||
DEFAULT_TOSTRING_IDENTIFIER | ||
}, | ||
) | ||
.with_argument(value_segment.get_expression().clone()) | ||
.into(), | ||
} | ||
} else { | ||
let arguments = iter::once( | ||
StringExpression::from_value(string.iter_segments().fold( | ||
String::new(), | ||
|mut format_string, segment| { | ||
match segment { | ||
InterpolationSegment::String(string_segment) => { | ||
format_string | ||
.push_str(&string_segment.get_value().replace('%', "%%")); | ||
} | ||
InterpolationSegment::Value(_) => { | ||
format_string.push_str(match self.strategy { | ||
ReplacementStrategy::StringSpecifier => "%s", | ||
ReplacementStrategy::ToStringSpecifier => "%*", | ||
}); | ||
} | ||
} | ||
format_string | ||
}, | ||
)) | ||
.into(), | ||
) | ||
.chain( | ||
string | ||
.iter_segments() | ||
.filter_map(|segment| match segment { | ||
InterpolationSegment::Value(segment) => { | ||
Some(segment.get_expression().clone()) | ||
} | ||
InterpolationSegment::String(_) => None, | ||
}) | ||
.map(|value| match self.strategy { | ||
ReplacementStrategy::ToStringSpecifier => value, | ||
ReplacementStrategy::StringSpecifier => FunctionCall::from_name( | ||
if self.is_identifier_used(DEFAULT_TOSTRING_IDENTIFIER) { | ||
self.define_tostring = true; | ||
&self.tostring_identifier | ||
} else { | ||
DEFAULT_TOSTRING_IDENTIFIER | ||
}, | ||
) | ||
.with_argument(value) | ||
.into(), | ||
}), | ||
) | ||
.collect::<TupleArguments>(); | ||
|
||
FunctionCall::from_prefix(if self.is_identifier_used(DEFAULT_STRING_LIBRARY) { | ||
self.define_string_format = true; | ||
Prefix::from_name(&self.string_format_identifier) | ||
} else { | ||
FieldExpression::new( | ||
Prefix::from_name(DEFAULT_STRING_LIBRARY), | ||
DEFAULT_STRING_FORMAT_NAME, | ||
) | ||
.into() | ||
}) | ||
.with_arguments(arguments) | ||
.into() | ||
} | ||
} | ||
} | ||
|
||
impl NodeProcessor for RemoveInterpolatedStringProcessor { | ||
fn process_expression(&mut self, expression: &mut Expression) { | ||
if let Expression::InterpolatedString(string) = expression { | ||
*expression = self.replace_with(string); | ||
} | ||
} | ||
} | ||
|
||
pub const REMOVE_INTERPOLATED_STRING_RULE_NAME: &str = "remove_interpolated_string"; | ||
|
||
/// A rule that removes interpolated strings. | ||
#[derive(Debug, Default, PartialEq, Eq)] | ||
pub struct RemoveInterpolatedString { | ||
strategy: ReplacementStrategy, | ||
} | ||
|
||
impl FlawlessRule for RemoveInterpolatedString { | ||
fn flawless_process(&self, block: &mut Block, _: &Context) { | ||
const STRING_FORMAT_IDENTIFIER: &str = "__DARKLUA_STR_FMT"; | ||
const TOSTRING_IDENTIFIER: &str = "__DARKLUA_TO_STR"; | ||
|
||
let mut processor = RemoveInterpolatedStringProcessor::new( | ||
self.strategy, | ||
STRING_FORMAT_IDENTIFIER, | ||
TOSTRING_IDENTIFIER, | ||
); | ||
ScopeVisitor::visit_block(block, &mut processor); | ||
|
||
if processor.define_string_format || processor.define_tostring { | ||
let mut variables = Vec::new(); | ||
let mut values = Vec::new(); | ||
|
||
if processor.define_string_format { | ||
variables.push(TypedIdentifier::new(STRING_FORMAT_IDENTIFIER)); | ||
values.push( | ||
FieldExpression::new( | ||
Prefix::from_name(DEFAULT_STRING_LIBRARY), | ||
DEFAULT_STRING_FORMAT_NAME, | ||
) | ||
.into(), | ||
); | ||
} | ||
|
||
if processor.define_tostring { | ||
variables.push(TypedIdentifier::new(TOSTRING_IDENTIFIER)); | ||
values.push(Identifier::new(DEFAULT_TOSTRING_IDENTIFIER).into()); | ||
} | ||
|
||
block.insert_statement(0, LocalAssignStatement::new(variables, values)); | ||
} | ||
} | ||
} | ||
|
||
impl RuleConfiguration for RemoveInterpolatedString { | ||
fn configure(&mut self, properties: RuleProperties) -> Result<(), RuleConfigurationError> { | ||
for (key, value) in properties { | ||
match key.as_str() { | ||
"strategy" => { | ||
self.strategy = match value.expect_string(&key)?.as_str() { | ||
"string" => ReplacementStrategy::StringSpecifier, | ||
"tostring" => ReplacementStrategy::ToStringSpecifier, | ||
unexpected => { | ||
return Err(RuleConfigurationError::UnexpectedValue { | ||
property: "strategy".to_owned(), | ||
message: format!( | ||
"invalid value `{}` (must be `string` or `tostring`)", | ||
unexpected | ||
), | ||
}) | ||
} | ||
}; | ||
} | ||
_ => return Err(RuleConfigurationError::UnexpectedProperty(key)), | ||
} | ||
} | ||
|
||
Ok(()) | ||
} | ||
|
||
fn get_name(&self) -> &'static str { | ||
REMOVE_INTERPOLATED_STRING_RULE_NAME | ||
} | ||
|
||
fn serialize_to_properties(&self) -> RuleProperties { | ||
RuleProperties::new() | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod test { | ||
use super::*; | ||
use crate::rules::Rule; | ||
|
||
use insta::assert_json_snapshot; | ||
|
||
fn new_rule() -> RemoveInterpolatedString { | ||
RemoveInterpolatedString::default() | ||
} | ||
|
||
#[test] | ||
fn serialize_default_rule() { | ||
let rule: Box<dyn Rule> = Box::new(new_rule()); | ||
|
||
assert_json_snapshot!("default_remove_interpolated_string", rule); | ||
} | ||
|
||
#[test] | ||
fn configure_with_extra_field_error() { | ||
let result = json5::from_str::<Box<dyn Rule>>( | ||
r#"{ | ||
rule: 'remove_interpolated_string', | ||
prop: "something", | ||
}"#, | ||
); | ||
pretty_assertions::assert_eq!(result.unwrap_err().to_string(), "unexpected field 'prop'"); | ||
} | ||
} |
5 changes: 5 additions & 0 deletions
5
...ua_core__rules__remove_interpolated_string__test__default_remove_interpolated_string.snap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
source: src/rules/remove_interpolated_string.rs | ||
expression: rule | ||
--- | ||
"remove_interpolated_string" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
use darklua_core::rules::{RemoveInterpolatedString, Rule}; | ||
|
||
test_rule!( | ||
remove_interpolated_string, | ||
RemoveInterpolatedString::default(), | ||
empty_string("return ``") => "return ''", | ||
regular_string("return `abc`") => "return 'abc'", | ||
string_with_single_quote("return `'`") => "return \"'\"", | ||
string_with_double_quote("return `\"`") => "return '\"'", | ||
string_with_variable("return `{object}`") => "return tostring(object)", | ||
nested_interpolated_string("return `{'+' .. `{object}`}`") => "return tostring('+' .. tostring(object))", | ||
string_prefix_with_variable("return `-{object}`") => "return string.format('-%s', tostring(object))", | ||
string_prefix_need_escaping_with_variable("return `%{object}`") => "return string.format('%%%s', tostring(object))", | ||
string_suffix_with_variable("return `{object}-`") => "return string.format('%s-', tostring(object))", | ||
string_with_variable_shadowing_tostring("local tostring return `{object}`") | ||
=> "local __DARKLUA_TO_STR = tostring local tostring return __DARKLUA_TO_STR(object)", | ||
string_prefix_need_escaping_with_variable_shadowing_tostring("local tostring return `%{object}`") | ||
=> "local __DARKLUA_TO_STR = tostring local tostring return string.format('%%%s', __DARKLUA_TO_STR(object))", | ||
string_prefix_need_escaping_with_variable_shadowing_string("local string return `%{object}`") | ||
=> "local __DARKLUA_STR_FMT = string.format local string return __DARKLUA_STR_FMT('%%%s', tostring(object))", | ||
string_prefix_need_escaping_with_variable_shadowing_string_and_tostring("local string, tostring return `%{object}`") | ||
=> "local __DARKLUA_STR_FMT, __DARKLUA_TO_STR = string.format, tostring local string, tostring return __DARKLUA_STR_FMT('%%%s', __DARKLUA_TO_STR(object))", | ||
two_strings_with_variable_shadowing_tostring("local tostring local a, b = `{object}`, `{var}`") | ||
=> "local __DARKLUA_TO_STR = tostring local tostring local a, b = __DARKLUA_TO_STR(object), __DARKLUA_TO_STR(var)", | ||
); | ||
|
||
test_rule!( | ||
remove_interpolated_string_using_tostring_specifier, | ||
json5::from_str::<Box<dyn Rule>>( | ||
r#"{ | ||
rule: 'remove_interpolated_string', | ||
strategy: 'tostring', | ||
}"#, | ||
) | ||
.unwrap(), | ||
empty_string("return ``") => "return ''", | ||
regular_string("return `abc`") => "return 'abc'", | ||
string_with_single_quote("return `'`") => "return \"'\"", | ||
string_with_double_quote("return `\"`") => "return '\"'", | ||
string_with_variable("return `{object}`") => "return tostring(object)", | ||
nested_interpolated_string("return `{'+' .. `{object}`}`") => "return tostring('+' .. tostring(object))", | ||
string_prefix_with_variable("return `-{object}`") => "return string.format('-%*', object)", | ||
string_prefix_need_escaping_with_variable("return `%{object}`") => "return string.format('%%%*', object)", | ||
string_suffix_with_variable("return `{object}-`") => "return string.format('%*-', object)", | ||
string_with_variable_shadowing_tostring("local tostring return `{object}`") | ||
=> "local __DARKLUA_TO_STR = tostring local tostring return __DARKLUA_TO_STR(object)", | ||
string_prefix_need_escaping_with_variable_shadowing_tostring("local tostring return `%{object}`") | ||
=> "local tostring return string.format('%%%*', object)", | ||
string_prefix_need_escaping_with_variable_shadowing_string("local string return `%{object}`") | ||
=> "local __DARKLUA_STR_FMT = string.format local string return __DARKLUA_STR_FMT('%%%*', object)", | ||
string_prefix_need_escaping_with_variable_shadowing_string_and_tostring("local string, tostring return `%{object}`") | ||
=> "local __DARKLUA_STR_FMT = string.format local string, tostring return __DARKLUA_STR_FMT('%%%*', object)", | ||
two_strings_with_variable_shadowing_tostring("local tostring local a, b = `{object}`, `{var}`") | ||
=> "local __DARKLUA_TO_STR = tostring local tostring local a, b = __DARKLUA_TO_STR(object), __DARKLUA_TO_STR(var)", | ||
); | ||
|
||
#[test] | ||
fn deserialize_from_object_notation() { | ||
json5::from_str::<Box<dyn Rule>>( | ||
r#"{ | ||
rule: 'remove_interpolated_string', | ||
}"#, | ||
) | ||
.unwrap(); | ||
} | ||
|
||
#[test] | ||
fn deserialize_from_string() { | ||
json5::from_str::<Box<dyn Rule>>("'remove_interpolated_string'").unwrap(); | ||
} |