From de56443492ed4993f23ccf0cd2a58b49513c5b0f Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Wed, 14 Aug 2024 16:29:11 +0200 Subject: [PATCH] Generic typo detection in RSC directives (#68890) This PR changes the Server Actions SWC transform, to instead of having a hard coded list of `"use server"` directive typos we use a O(len) algorithm to detect possible mistakes in the directive name. Note that the previous `.iter()` approach is also theoretically slower O(n * len). This was cherry-picked from a larger change which will later make this transform more general and support different directive names (via configurations). --- .../src/transforms/server_actions.rs | 71 ++++++++++++++++--- 1 file changed, 60 insertions(+), 11 deletions(-) diff --git a/crates/next-custom-transforms/src/transforms/server_actions.rs b/crates/next-custom-transforms/src/transforms/server_actions.rs index 970c9b723e52a..63a0f919bfa5c 100644 --- a/crates/next-custom-transforms/src/transforms/server_actions.rs +++ b/crates/next-custom-transforms/src/transforms/server_actions.rs @@ -1345,14 +1345,63 @@ fn annotate_ident_as_action( } } -const DIRECTIVE_TYPOS: &[&str] = &[ - "use servers", - "use-server", - "use sevrer", - "use srever", - "use servre", - "user server", -]; +// Detects if two strings are similar (but not the same). +// This implementation is fast and simple as it allows only one +// edit (add, remove, edit, swap), instead of using a N^2 Levenshtein algorithm. +// +// Example of similar strings of "use server": +// "use servers", +// "use-server", +// "use sevrer", +// "use srever", +// "use servre", +// "user server", +// +// This avoids accidental typos as there's currently no other static analysis +// tool to help when these mistakes happen. +fn detect_similar_strings(a: &str, b: &str) -> bool { + let mut a = a.chars().collect::>(); + let mut b = b.chars().collect::>(); + + if a.len() < b.len() { + (a, b) = (b, a); + } + + if a.len() == b.len() { + // Same length, get the number of character differences. + let mut diff = 0; + for i in 0..a.len() { + if a[i] != b[i] { + diff += 1; + if diff > 2 { + return false; + } + } + } + + // Should be 1 or 2, but not 0. + diff != 0 + } else { + if a.len() - b.len() > 1 { + return false; + } + + // A has one more character than B. + for i in 0..b.len() { + if a[i] != b[i] { + // This should be the only difference, a[i+1..] should be equal to b[i..]. + // Otherwise, they're not considered similar. + // A: "use srerver" + // B: "use server" + // ^ + return a[i + 1..] == b[i..]; + } + } + + // This happens when the last character of A is an extra character. + true + } +} fn remove_server_directive_index_in_module( stmts: &mut Vec, @@ -1395,7 +1444,7 @@ fn remove_server_directive_index_in_module( } } else { // Detect typo of "use server" - if DIRECTIVE_TYPOS.iter().any(|&s| s == value) { + if detect_similar_strings(value, "use server") { HANDLER.with(|handler| { handler .struct_span_err( @@ -1421,7 +1470,7 @@ fn remove_server_directive_index_in_module( .. })) => { // Match `("use server")`. - if value == "use server" || DIRECTIVE_TYPOS.iter().any(|&s| s == value) { + if value == "use server" || detect_similar_strings(value, "use server") { if is_directive { HANDLER.with(|handler| { handler @@ -1499,7 +1548,7 @@ fn remove_server_directive_index_in_fn( } } else { // Detect typo of "use server" - if DIRECTIVE_TYPOS.iter().any(|&s| s == value) { + if detect_similar_strings(value, "use server") { HANDLER.with(|handler| { handler .struct_span_err(