Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Changelog

* add rule to convert Luau numbers (`convert_luau_number`) ([#274](https://github.com/seaofvoices/darklua/pull/274))
* export the `PathRequireMode` struct when using the darklua library and refactor AST node types to reduce size difference between variants ([#273](https://github.com/seaofvoices/darklua/pull/273))

## 0.16.0
Expand Down
10 changes: 10 additions & 0 deletions site/content/rules/convert_luau_number.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
description: Convert Luau numbers into Lua compatible numbers
added_in: "unreleased"
parameters: []
examples:
- content: "print(0b1000_0001)"
- content: "return 0xA000_FFFF"
---

This rule transforms Luau-specific number literals into Lua compatible number literals. It converts binary literals (prefixed with `0b` or `0B`) to their hexadecimal equivalents. It also removes underscores used as digit separators in numbers.
35 changes: 32 additions & 3 deletions src/nodes/expressions/number.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,18 @@ impl DecimalNumber {
self.token = Some(token);
}

/// Returns a reference to the token attached to this decimal number, if any.
#[inline]
fn get_token(&self) -> Option<&Token> {
pub fn get_token(&self) -> Option<&Token> {
self.token.as_ref()
}

/// Returns a mutable reference to the token attached to this decimal number, if any.
#[inline]
pub fn mutate_token(&mut self) -> Option<&mut Token> {
self.token.as_mut()
}

/// Sets an exponent for this decimal number and returns the updated number.
///
/// The `is_uppercase` parameter determines whether the exponent uses uppercase 'E'
Expand Down Expand Up @@ -121,10 +128,16 @@ impl HexNumber {

/// Returns a reference to the token attached to this hexadecimal number, if any.
#[inline]
fn get_token(&self) -> Option<&Token> {
pub fn get_token(&self) -> Option<&Token> {
self.token.as_ref()
}

/// Returns a mutable reference to the token attached to this hexadecimal number, if any.
#[inline]
pub fn mutate_token(&mut self) -> Option<&mut Token> {
self.token.as_mut()
}

/// Sets a binary exponent for this hexadecimal number and returns the updated number.
///
/// The `is_uppercase` parameter determines whether the exponent uses uppercase 'P'
Expand Down Expand Up @@ -213,10 +226,16 @@ impl BinaryNumber {

/// Returns a reference to the token attached to this binary number, if any.
#[inline]
fn get_token(&self) -> Option<&Token> {
pub fn get_token(&self) -> Option<&Token> {
self.token.as_ref()
}

/// Returns a mutable reference to the token attached to this binary number, if any.
#[inline]
pub fn mutate_token(&mut self) -> Option<&mut Token> {
self.token.as_mut()
}

/// Sets whether the binary prefix should use uppercase 'B' (0B) or lowercase 'b' (0b).
pub fn set_uppercase(&mut self, is_uppercase: bool) {
self.is_b_uppercase = is_uppercase;
Expand Down Expand Up @@ -302,6 +321,16 @@ impl NumberExpression {
}
}

/// Returns a mutable reference to the token attached to this number expression, if any.
#[inline]
pub fn mutate_token(&mut self) -> Option<&mut Token> {
match self {
NumberExpression::Decimal(number) => number.mutate_token(),
NumberExpression::Hex(number) => number.mutate_token(),
NumberExpression::Binary(number) => number.mutate_token(),
}
}

/// Clears all comments from the tokens in this node.
pub fn clear_comments(&mut self) {
match self {
Expand Down
108 changes: 108 additions & 0 deletions src/rules/convert_luau_number.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
use crate::{
nodes::{Block, HexNumber, NumberExpression, Token},
process::{DefaultVisitor, NodeProcessor, NodeVisitor},
rules::{Context, FlawlessRule, RuleConfiguration, RuleConfigurationError, RuleProperties},
};

use super::verify_no_rule_properties;

#[derive(Default)]
struct Processor<'a> {
code: &'a str,
}

impl Processor<'_> {
fn trim_underscores(&self, token: &mut Token) {
let content = token.read(self.code);

if content.contains('_') {
token.replace_with_content(content.chars().filter(|c| *c != '_').collect::<String>());
}
}
}

impl NodeProcessor for Processor<'_> {
fn process_number_expression(&mut self, number: &mut NumberExpression) {
match number {
NumberExpression::Binary(binary) => {
let value = binary.get_raw_value();
*number = HexNumber::new(value, false).into();
}
NumberExpression::Hex(hex_number) => {
if let Some(token) = hex_number.mutate_token() {
self.trim_underscores(token);
}
}
NumberExpression::Decimal(decimal_number) => {
if let Some(token) = decimal_number.mutate_token() {
self.trim_underscores(token);
}
}
}
}
}

impl<'a> Processor<'a> {
fn new(code: &'a str) -> Self {
Self { code }
}
}

pub const CONVERT_LUAU_NUMBER_RULE_NAME: &str = "convert_luau_number";

/// A rule that converts Luau number literals to regular Lua numbers.
#[derive(Default, Debug, PartialEq, Eq)]
pub struct ConvertLuauNumber {}

impl FlawlessRule for ConvertLuauNumber {
fn flawless_process(&self, block: &mut Block, context: &Context) {
let mut processor = Processor::new(context.original_code());
DefaultVisitor::visit_block(block, &mut processor);
}
}

impl RuleConfiguration for ConvertLuauNumber {
fn configure(&mut self, properties: RuleProperties) -> Result<(), RuleConfigurationError> {
verify_no_rule_properties(&properties)?;

Ok(())
}

fn get_name(&self) -> &'static str {
CONVERT_LUAU_NUMBER_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() -> ConvertLuauNumber {
ConvertLuauNumber::default()
}

#[test]
fn serialize_default_rule() {
let rule: Box<dyn Rule> = Box::new(new_rule());

assert_json_snapshot!("default_convert_luau_number", rule);
}

#[test]
fn configure_with_extra_field_error() {
let result = json5::from_str::<Box<dyn Rule>>(
r#"{
rule: 'convert_luau_number',
prop: "something",
}"#,
);
pretty_assertions::assert_eq!(result.unwrap_err().to_string(), "unexpected field 'prop'");
}
}
4 changes: 4 additions & 0 deletions src/rules/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ mod call_parens;
mod compute_expression;
mod configuration_error;
mod convert_index_to_field;
mod convert_luau_number;
mod convert_require;
mod empty_do;
mod filter_early_return;
Expand Down Expand Up @@ -43,6 +44,7 @@ pub use call_parens::*;
pub use compute_expression::*;
pub use configuration_error::RuleConfigurationError;
pub use convert_index_to_field::*;
pub use convert_luau_number::*;
pub use convert_require::*;
pub use empty_do::*;
pub use filter_early_return::*;
Expand Down Expand Up @@ -290,6 +292,7 @@ pub fn get_all_rule_names() -> Vec<&'static str> {
COMPUTE_EXPRESSIONS_RULE_NAME,
CONVERT_INDEX_TO_FIELD_RULE_NAME,
CONVERT_LOCAL_FUNCTION_TO_ASSIGN_RULE_NAME,
CONVERT_LUAU_NUMBER_RULE_NAME,
CONVERT_REQUIRE_RULE_NAME,
FILTER_AFTER_EARLY_RETURN_RULE_NAME,
GROUP_LOCAL_ASSIGNMENT_RULE_NAME,
Expand Down Expand Up @@ -325,6 +328,7 @@ impl FromStr for Box<dyn Rule> {
CONVERT_LOCAL_FUNCTION_TO_ASSIGN_RULE_NAME => {
Box::<ConvertLocalFunctionToAssign>::default()
}
CONVERT_LUAU_NUMBER_RULE_NAME => Box::<ConvertLuauNumber>::default(),
CONVERT_REQUIRE_RULE_NAME => Box::<ConvertRequire>::default(),
FILTER_AFTER_EARLY_RETURN_RULE_NAME => Box::<FilterAfterEarlyReturn>::default(),
GROUP_LOCAL_ASSIGNMENT_RULE_NAME => Box::<GroupLocalAssignment>::default(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
source: src/rules/convert_luau_number.rs
expression: rule
---
"convert_luau_number"
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ expression: rule_names
"compute_expression",
"convert_index_to_field",
"convert_local_function_to_assign",
"convert_luau_number",
"convert_require",
"filter_after_early_return",
"group_local_assignment",
Expand Down
71 changes: 71 additions & 0 deletions tests/rule_tests/convert_luau_number.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
use darklua_core::rules::{ConvertLuauNumber, Rule};

test_rule!(
convert_luau_number,
ConvertLuauNumber::default(),
binary_literal_lowercase("local a = 0b10101010")
=> "local a = 0xAA",
binary_literal_uppercase("local b = 0B11001100")
=> "local b = 0xCC",
binary_literal_lowercase_with_underscores("local c = 0b_1111_0000")
=> "local c = 0xF0",
binary_literal_uppercase_with_underscores("local d = 0B_0000_1111")
=> "local d = 0x0F",
binary_literal_with_multiple_underscores("local e = 0b__________1010_1010")
=> "local e = 0xAA",
decimal_with_underscores("local a = 1_048_576")
=> "local a = 1048576",
decimal_with_multiple_underscores("local a1 = 1___048__576__")
=> "local a1 = 1048576",
hexadecimal_with_underscores("local b = 0xFFFF_FFFF")
=> "local b = 0xFFFFFFFF",
hexadecimal_lowercase("local c = 0xabcd_ef12")
=> "local c = 0xabcdef12",
hexadecimal_mixed_case("local d = 0xA1b2_C3d4")
=> "local d = 0xA1b2C3d4",
hexadecimal_single_underscore("local e = 0x1234_5678")
=> "local e = 0x12345678",
hexadecimal_multiple_underscores("local f = 0x1___234__567")
=> "local f = 0x1234567",
hexadecimal_leading_underscore("local g = 0x_DEAD_BEEF")
=> "local g = 0xDEADBEEF",
hexadecimal_trailing_underscore("local h = 0xCAFE_BABE_")
=> "local h = 0xCAFEBABE",
hexadecimal_small_number("local i = 0x1_2_3_4")
=> "local i = 0x1234",
hexadecimal_zero("local j = 0x0_0_0_0")
=> "local j = 0x0000",
hexadecimal_single_digit("local k = 0xF")
=> "local k = 0xF",
hexadecimal_single_digit_with_underscore("local l = 0xF_")
=> "local l = 0xF",
hexadecimal_two_digits("local m = 0xAB")
=> "local m = 0xAB",
hexadecimal_three_digits("local n = 0x123")
=> "local n = 0x123",
hexadecimal_four_digits("local o = 0xABCD")
=> "local o = 0xABCD",
hexadecimal_six_digits("local p = 0x123___456")
=> "local p = 0x123456",
hexadecimal_eight_digits("local q = 0x1234_5678")
=> "local q = 0x12345678",
hexadecimal_ten_digits("local r = 0x12_34_56_78_9A")
=> "local r = 0x123456789A",
hexadecimal_twelve_digits("local s = 0x123_456_789_ABC")
=> "local s = 0x123456789ABC",
);

#[test]
fn deserialize_from_object_notation() {
json5::from_str::<Box<dyn Rule>>(
r#"{
rule: 'convert_luau_number',
}"#,
)
.unwrap();
}

#[test]
fn deserialize_from_string() {
json5::from_str::<Box<dyn Rule>>("'convert_luau_number'").unwrap();
}
1 change: 1 addition & 0 deletions tests/rule_tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,7 @@ macro_rules! test_rule_snapshot {
mod append_text_comment;
mod compute_expression;
mod convert_index_to_field;
mod convert_luau_number;
mod convert_require;
mod filter_early_return;
mod group_local_assignment;
Expand Down
Loading