@@ -52,11 +52,15 @@ impl Rule for PreferSpread {
5252 return ;
5353 } ;
5454
55- check_unicorn_prefer_spread ( call_expr, ctx) ;
55+ check_unicorn_prefer_spread ( node , call_expr, ctx) ;
5656 }
5757}
5858
59- fn check_unicorn_prefer_spread ( call_expr : & CallExpression , ctx : & LintContext ) {
59+ fn check_unicorn_prefer_spread < ' a > (
60+ node : & AstNode < ' a > ,
61+ call_expr : & CallExpression < ' a > ,
62+ ctx : & LintContext < ' a > ,
63+ ) {
6064 let Some ( member_expr) = call_expr. callee . without_parentheses ( ) . as_member_expression ( ) else {
6165 return ;
6266 } ;
@@ -87,7 +91,7 @@ fn check_unicorn_prefer_spread(call_expr: &CallExpression, ctx: &LintContext) {
8791 return ;
8892 }
8993
90- report_with_spread_fixer ( ctx, call_expr. span , "Array.from()" , expr) ;
94+ report_with_spread_fixer ( node , ctx, call_expr. span , "Array.from()" , expr) ;
9195 }
9296 // `array.concat()`
9397 "concat" => {
@@ -131,7 +135,7 @@ fn check_unicorn_prefer_spread(call_expr: &CallExpression, ctx: &LintContext) {
131135 }
132136 }
133137
134- report_with_spread_fixer ( ctx, call_expr. span , "array.slice()" , member_expr_obj) ;
138+ report_with_spread_fixer ( node , ctx, call_expr. span , "array.slice()" , member_expr_obj) ;
135139 }
136140 // `array.toSpliced()`
137141 "toSpliced" => {
@@ -145,6 +149,7 @@ fn check_unicorn_prefer_spread(call_expr: &CallExpression, ctx: &LintContext) {
145149 }
146150
147151 report_with_spread_fixer (
152+ node,
148153 ctx,
149154 call_expr. span ,
150155 "array.toSpliced()" ,
@@ -171,10 +176,15 @@ fn check_unicorn_prefer_spread(call_expr: &CallExpression, ctx: &LintContext) {
171176 ctx. diagnostic_with_fix (
172177 unicorn_prefer_spread_diagnostic ( call_expr. span , "string.split()" ) ,
173178 |fixer| {
179+ let needs_semi = ast_util:: could_be_asi_hazard ( node, ctx) ;
174180 let callee_obj = member_expr. object ( ) . without_parentheses ( ) ;
181+ let prefix = if needs_semi { ";" } else { "" } ;
175182 fixer. replace (
176183 call_expr. span ,
177- format ! ( "[...{}]" , callee_obj. span( ) . source_text( ctx. source_text( ) ) ) ,
184+ format ! (
185+ "{prefix}[...{}]" ,
186+ callee_obj. span( ) . source_text( ctx. source_text( ) )
187+ ) ,
178188 )
179189 } ,
180190 ) ;
@@ -241,13 +251,18 @@ fn is_not_array(expr: &Expression, ctx: &LintContext) -> bool {
241251}
242252
243253fn report_with_spread_fixer (
254+ node : & AstNode ,
244255 ctx : & LintContext ,
245256 span : Span ,
246257 bad_method : & str ,
247258 expr_to_spread : & Expression ,
248259) {
249260 ctx. diagnostic_with_fix ( unicorn_prefer_spread_diagnostic ( span, bad_method) , |fixer| {
261+ let needs_semi = ast_util:: could_be_asi_hazard ( node, ctx) ;
250262 let mut codegen = fixer. codegen ( ) ;
263+ if needs_semi {
264+ codegen. print_str ( ";" ) ;
265+ }
251266 codegen. print_str ( "[..." ) ;
252267 codegen. print_expression ( expr_to_spread) ;
253268 codegen. print_str ( "]" ) ;
@@ -542,22 +557,118 @@ fn test() {
542557 // `Array.from()`
543558 ( "const x = Array.from(set);" , "const x = [...set];" , None ) ,
544559 ( "Array.from(new Set([1, 2])).map(() => {});" , "[...new Set([1, 2])].map(() => {});" , None ) ,
560+ // `Array.from()` - ASI hazard cases (need semicolon prefix)
561+ (
562+ "const foo = bar\n Array.from(set).map(() => {})" ,
563+ "const foo = bar\n ;[...set].map(() => {})" ,
564+ None ,
565+ ) ,
566+ (
567+ "foo()\n Array.from(set).forEach(doSomething)" ,
568+ "foo()\n ;[...set].forEach(doSomething)" ,
569+ None ,
570+ ) ,
571+ // `Array.from()` - No ASI hazard (semicolon already present)
572+ (
573+ "const foo = bar;\n Array.from(set).map(() => {})" ,
574+ "const foo = bar;\n [...set].map(() => {})" ,
575+ None ,
576+ ) ,
577+ // `Array.from()` - ASI hazard with comments before
578+ (
579+ "foo() /* comment */\n Array.from(set).map(() => {})" ,
580+ "foo() /* comment */\n ;[...set].map(() => {})" ,
581+ None ,
582+ ) ,
583+ (
584+ "foo() // comment\n Array.from(set).map(() => {})" ,
585+ "foo() // comment\n ;[...set].map(() => {})" ,
586+ None ,
587+ ) ,
545588 // `array.slice()`
546589 ( "array.slice()" , "[...array]" , None ) ,
547590 ( "array.slice(1).slice()" , "[...array.slice(1)]" , None ) ,
591+ // `array.slice()` - ASI hazard cases
592+ ( "foo()\n array.slice()" , "foo()\n ;[...array]" , None ) ,
548593 // `array.toSpliced()`
549594 ( "array.toSpliced()" , "[...array]" , None ) ,
550595 ( "const copy = array.toSpliced()" , "const copy = [...array]" , None ) ,
551- // ("", "", None),
552- // (" ", "", None),
596+ // `array.toSpliced()` - ASI hazard cases
597+ ( "foo() \n array.toSpliced() ", "foo() \n ;[...array] " , None ) ,
553598 // `string.split()`
554599 ( r#""🦄".split("")"# , r#"[..."🦄"]"# , None ) ,
555600 ( r#""foo bar baz".split("")"# , r#"[..."foo bar baz"]"# , None ) ,
601+ // `string.split()` - ASI hazard cases
602+ ( "foo()\n str.split(\" \" )" , "foo()\n ;[...str]" , None ) ,
556603 (
557604 r"Array.from(path.matchAll(/\{([^{}?]+\??)\}/g))" ,
558605 "[...path.matchAll(/\\ {([^{}?]+\\ ??)\\ }/g)]" ,
559606 None ,
560607 ) ,
608+ // Cases where NO semicolon should be added (not an ExpressionStatement)
609+ ( "return Array.from(set)" , "return [...set]" , None ) ,
610+ ( "const x = Array.from(set)" , "const x = [...set]" , None ) ,
611+ ( "foo(Array.from(set))" , "foo([...set])" , None ) ,
612+ ( "if (Array.from(set).length) {}" , "if ([...set].length) {}" , None ) ,
613+ // `Array.from()` - ASI hazard with multi-byte Unicode identifiers
614+ ( "日本語\n Array.from(set).map(() => {})" , "日本語\n ;[...set].map(() => {})" , None ) ,
615+ (
616+ "const foo = 日本語\n Array.from(set).map(() => {})" ,
617+ "const foo = 日本語\n ;[...set].map(() => {})" ,
618+ None ,
619+ ) ,
620+ ( "/**/Array.from(set).map(() => {})" , "/**/[...set].map(() => {})" , None ) ,
621+ ( "/regex/\n Array.from(set).map(() => {})" , "/regex/\n ;[...set].map(() => {})" , None ) ,
622+ ( "/regex/g\n Array.from(set).map(() => {})" , "/regex/g\n ;[...set].map(() => {})" , None ) ,
623+ ( "0.\n Array.from(set).map(() => {})" , "0.\n ;[...set].map(() => {})" , None ) ,
624+ (
625+ "foo()\u{00A0} \n Array.from(set).map(() => {})" ,
626+ "foo()\u{00A0} \n ;[...set].map(() => {})" ,
627+ None ,
628+ ) ,
629+ (
630+ "foo()\u{FEFF} \n Array.from(set).map(() => {})" ,
631+ "foo()\u{FEFF} \n ;[...set].map(() => {})" ,
632+ None ,
633+ ) ,
634+ ( "foo() /* a */ /* b */\n Array.from(set)" , "foo() /* a */ /* b */\n ;[...set]" , None ) ,
635+ ( "x++\n array.slice()" , "x++\n ;[...array]" , None ) ,
636+ ( "x--\n array.slice()" , "x--\n ;[...array]" , None ) ,
637+ ( "arr[0]\n array.slice()" , "arr[0]\n ;[...array]" , None ) ,
638+ ( "obj.prop\n array.slice()" , "obj.prop\n ;[...array]" , None ) ,
639+ ( "while (array.slice().length) {}" , "while ([...array].length) {}" , None ) ,
640+ ( "do {} while (array.slice().length)" , "do {} while ([...array].length)" , None ) ,
641+ ( "for (array.slice();;) {}" , "for ([...array];;) {}" , None ) ,
642+ ( "switch (array.slice()[0]) {}" , "switch ([...array][0]) {}" , None ) ,
643+ ( "`template`\n array.toSpliced()" , "`template`\n ;[...array]" , None ) ,
644+ (
645+ r#"'string'
646+ str.split("")"# ,
647+ "'string'\n ;[...str]" ,
648+ None ,
649+ ) ,
650+ (
651+ r#""string"
652+ str.split("")"# ,
653+ r#""string"
654+ ;[...str]"# ,
655+ None ,
656+ ) ,
657+ (
658+ "foo()\n Array.from(set).map(x => x).filter(Boolean).length" ,
659+ "foo()\n ;[...set].map(x => x).filter(Boolean).length" ,
660+ None ,
661+ ) ,
662+ ( "const fn = () => Array.from(set)" , "const fn = () => [...set]" , None ) ,
663+ ( "foo ? Array.from(a) : b" , "foo ? [...a] : b" , None ) ,
664+ ( "foo || Array.from(set)" , "foo || [...set]" , None ) ,
665+ ( "foo && Array.from(set)" , "foo && [...set]" , None ) ,
666+ ( "foo + Array.from(set).length" , "foo + [...set].length" , None ) ,
667+ ( "x = Array.from(set)" , "x = [...set]" , None ) ,
668+ ( "const obj = { arr: Array.from(set) }" , "const obj = { arr: [...set] }" , None ) ,
669+ ( "(foo, Array.from(set))" , "(foo, [...set])" , None ) ,
670+ ( "[Array.from(set)]" , "[[...set]]" , None ) ,
671+ ( "async () => await Array.from(set)" , "async () => await [...set]" , None ) ,
561672 ] ;
562673
563674 Tester :: new ( PreferSpread :: NAME , PreferSpread :: PLUGIN , pass, fail)
0 commit comments