Skip to content

Commit

Permalink
Generic typo detection in RSC directives (#68890)
Browse files Browse the repository at this point in the history
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).
  • Loading branch information
shuding authored and ForsakenHarmony committed Aug 14, 2024
1 parent 22bac28 commit ec9e38e
Showing 1 changed file with 60 additions and 11 deletions.
71 changes: 60 additions & 11 deletions crates/next-custom-transforms/src/transforms/server_actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<Vec<char>>();
let mut b = b.chars().collect::<Vec<char>>();

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<ModuleItem>,
Expand Down Expand Up @@ -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(
Expand All @@ -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
Expand Down Expand Up @@ -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(
Expand Down

0 comments on commit ec9e38e

Please sign in to comment.