Repository: swc-project/swc (rust)

Issue:
expr_simplifier does not handle numeric operations involving strings
### Describe the feature

`swc_core::ecma::transforms::optimization::simplify::expr_simplifier` should simplify binary expressions when the left, right or both expression is a string literal if the operator isn't addition. Currently, these expressions are not simplified and are left as-is by the simplifier. Example:

```javascript
"10" - 5 // output: 5
"4" / 2 // output: 2
"11" % 2 // output: 1
"10" ** 2 // output: 100

10 - "5" // output: 5
"10" - "5" // output: 5
// same examples as above but with the right side being a string instead of left
```

The file responsible seems to be: https://github.com/swc-project/swc/blob/main/crates/swc_ecma_transforms_optimization/src/simplify/expr/mod.rs

The issue currently is that `get_type().casted_to_number_on_add()` is being checked regardless of the operator. This should only be checked if the operator isn't addition.

### Babel plugin or link to the feature description

_No response_

### Additional context

Babel's `path.evaluate()` correctly handles this.

As far as I know, you simply need to parse the string as a 64-bit float and perform a numeric operation with the parsed float.

Pull request description:
feat(es/minifier): Allow `expr_simplifier` to do arithmetic with string literals
**Description:**
This PR implements the feature described for the issue in #8682.


**Related issue:**

 - Closes #8682.

Patch:
diff --git a/crates/swc_ecma_transforms_optimization/src/simplify/expr/mod.rs b/crates/swc_ecma_transforms_optimization/src/simplify/expr/mod.rs
index 175324c9ac..3fc880ecea 100644
--- a/crates/swc_ecma_transforms_optimization/src/simplify/expr/mod.rs
+++ b/crates/swc_ecma_transforms_optimization/src/simplify/expr/mod.rs
@@ -896,8 +896,9 @@ impl SimplifyExpr {
         );
 
         if (lv.is_unknown() && rv.is_unknown())
-            || !left.get_type().casted_to_number_on_add()
-            || !right.get_type().casted_to_number_on_add()
+            || op == op!(bin, "+")
+                && (!left.get_type().casted_to_number_on_add()
+                    || !right.get_type().casted_to_number_on_add())
         {
             return Unknown;
         }


Test patch:
diff --git a/crates/swc/tests/tsc-references/enumConstantMemberWithString.2.minified.js b/crates/swc/tests/tsc-references/enumConstantMemberWithString.2.minified.js
index da9113fe52..79e3fbf55c 100644
--- a/crates/swc/tests/tsc-references/enumConstantMemberWithString.2.minified.js
+++ b/crates/swc/tests/tsc-references/enumConstantMemberWithString.2.minified.js
@@ -1,3 +1,3 @@
 //// [enumConstantMemberWithString.ts]
 var T1, T2, T3, T4, T5, T11, T21, T31;
-(T11 = T1 || (T1 = {})).a = "1", T11.b = "12", T11.c = "123", T11[T11.d = "a" - "a"] = "d", T11.e = "a1", (T21 = T2 || (T2 = {})).a = "1", T21.b = "12", (T31 = T3 || (T3 = {})).a = "1", T31.b = "12", T31[T31.c = 1] = "c", T31[T31.d = 3] = "d", (T4 || (T4 = {})).a = "1", (T5 || (T5 = {})).a = "12";
+(T11 = T1 || (T1 = {})).a = "1", T11.b = "12", T11.c = "123", T11[T11.d = NaN] = "d", T11.e = "a1", (T21 = T2 || (T2 = {})).a = "1", T21.b = "12", (T31 = T3 || (T3 = {})).a = "1", T31.b = "12", T31[T31.c = 1] = "c", T31[T31.d = 3] = "d", (T4 || (T4 = {})).a = "1", (T5 || (T5 = {})).a = "12";
diff --git a/crates/swc/tests/tsc-references/enumConstantMemberWithTemplateLiterals.2.minified.js b/crates/swc/tests/tsc-references/enumConstantMemberWithTemplateLiterals.2.minified.js
index a79a8f04e5..ce6ef711ef 100644
--- a/crates/swc/tests/tsc-references/enumConstantMemberWithTemplateLiterals.2.minified.js
+++ b/crates/swc/tests/tsc-references/enumConstantMemberWithTemplateLiterals.2.minified.js
@@ -1,3 +1,3 @@
 //// [enumConstantMemberWithTemplateLiterals.ts]
 var T1, T2, T3, T4, T5, T6, T21, T41, T51, T61;
-(T1 || (T1 = {})).a = "1", (T21 = T2 || (T2 = {})).a = "1", T21.b = "2", T21[T21.c = 3] = "c", (T3 || (T3 = {})).a = "11", (T41 = T4 || (T4 = {})).a = "1", T41.b = "11", T41.c = "12", T41.d = "21", T41.e = "211", (T51 = T5 || (T5 = {})).a = "1", T51.b = "12", T51.c = "123", T51[T51.d = 1] = "d", T51[T51.e = "1" - "1"] = "e", T51.f = "11", T51.g = "123", T51[T51.h = 1] = "h", (T61 = T6 || (T6 = {}))[T61.a = 1] = "a", T61[T61.b = 2] = "b";
+(T1 || (T1 = {})).a = "1", (T21 = T2 || (T2 = {})).a = "1", T21.b = "2", T21[T21.c = 3] = "c", (T3 || (T3 = {})).a = "11", (T41 = T4 || (T4 = {})).a = "1", T41.b = "11", T41.c = "12", T41.d = "21", T41.e = "211", (T51 = T5 || (T5 = {})).a = "1", T51.b = "12", T51.c = "123", T51[T51.d = 1] = "d", T51[T51.e = 0] = "e", T51.f = "11", T51.g = "123", T51[T51.h = 1] = "h", (T61 = T6 || (T6 = {}))[T61.a = 1] = "a", T61[T61.b = 2] = "b";
diff --git a/crates/swc_ecma_transforms_optimization/src/simplify/expr/tests.rs b/crates/swc_ecma_transforms_optimization/src/simplify/expr/tests.rs
index 55a6c7ef91..472c4b3506 100644
--- a/crates/swc_ecma_transforms_optimization/src/simplify/expr/tests.rs
+++ b/crates/swc_ecma_transforms_optimization/src/simplify/expr/tests.rs
@@ -1314,6 +1314,33 @@ fn test_fold_literals_as_numbers() {
     fold("x/false", "x/0"); // should we add an error check? :)
 }
 
+#[test]
+fn test_fold_arithmetic_with_strings() {
+    // Left side of expression is a string
+    fold("'10' - 5", "5");
+    fold("'4' / 2", "2");
+    fold("'11' % 2", "1");
+    fold("'10' ** 2", "100");
+    fold("'Infinity' * 2", "Infinity");
+    fold("'NaN' * 2", "NaN");
+
+    // Right side of expression is a string
+    fold("10 - '5'", "5");
+    fold("4 / '2'", "2");
+    fold("11 % '2'", "1");
+    fold("10 ** '2'", "100");
+    fold("2 * 'Infinity'", "Infinity");
+    fold("2 * 'NaN'", "NaN");
+
+    // Both sides are strings
+    fold("'10' - '5'", "5");
+    fold("'4' / '2'", "2");
+    fold("'11' % '2'", "1");
+    fold("'10' ** '2'", "100");
+    fold("'Infinity' * '2'", "Infinity");
+    fold("'NaN' * '2'", "NaN");
+}
+
 #[test]
 fn test_not_fold_back_to_true_false() {
     fold("!0", "!0");