@@ -84,7 +84,7 @@ declare_oxc_lint!(
8484 SortKeys ,
8585 eslint,
8686 style,
87- pending
87+ conditional_fix
8888) ;
8989
9090impl Rule for SortKeys {
@@ -191,6 +191,147 @@ impl Rule for SortKeys {
191191 all ( property_groups. iter ( ) . zip ( & sorted_property_groups) , |( a, b) | a == b) ;
192192
193193 if !is_sorted {
194+ // Try to provide a safe autofix when possible.
195+ // Conditions for providing a fix:
196+ // - No spread properties (reordering spreads is unsafe)
197+ // - All properties have a static key name
198+ // - No comments between adjacent properties
199+ // - No special grouping markers (we only support a single contiguous group)
200+
201+ let all_props = & dec. properties ;
202+ let mut can_fix = true ;
203+ let mut props: Vec < ( String , Span ) > = Vec :: with_capacity ( all_props. len ( ) ) ;
204+
205+ for ( i, prop) in all_props. iter ( ) . enumerate ( ) {
206+ match prop {
207+ ObjectPropertyKind :: SpreadProperty ( _) => {
208+ can_fix = false ;
209+ break ;
210+ }
211+ ObjectPropertyKind :: ObjectProperty ( obj) => {
212+ let Some ( key) = obj. key . static_name ( ) else {
213+ can_fix = false ;
214+ break ;
215+ } ;
216+ props. push ( ( key. to_string ( ) , prop. span ( ) ) ) ;
217+ // check comments between this and next
218+ if i + 1 < all_props. len ( ) {
219+ let next_span = all_props[ i + 1 ] . span ( ) ;
220+ let between = Span :: new ( prop. span ( ) . end , next_span. start ) ;
221+ if ctx. has_comments_between ( between) {
222+ can_fix = false ;
223+ break ;
224+ }
225+ }
226+ }
227+ }
228+ }
229+
230+ if can_fix
231+ && !props. is_empty ( )
232+ && property_groups. len ( ) == 1
233+ && !property_groups[ 0 ] . iter ( ) . any ( |s| s. starts_with ( '<' ) )
234+ {
235+ // Build full text slices for each property spanning from the start of the
236+ // property to the start of the next property (or the end of the last one).
237+ // This preserves commas and formatting attached to each property so that
238+ // reordering produces syntactically-correct output.
239+ let mut prop_texts: Vec < String > = Vec :: with_capacity ( props. len ( ) ) ;
240+
241+ for i in 0 ..props. len ( ) {
242+ let start = props[ i] . 1 . start ;
243+ let end =
244+ if i + 1 < props. len ( ) { props[ i + 1 ] . 1 . start } else { props[ i] . 1 . end } ;
245+ prop_texts. push ( ctx. source_range ( Span :: new ( start, end) ) . to_string ( ) ) ;
246+ }
247+
248+ // Prepare keys for comparison according to options
249+ let keys_for_cmp: Vec < String > = props
250+ . iter ( )
251+ . map ( |( k, _) | {
252+ if self . case_sensitive {
253+ k. to_string ( )
254+ } else {
255+ k. cow_to_ascii_lowercase ( ) . to_string ( )
256+ }
257+ } )
258+ . collect ( ) ;
259+
260+ // Compute the sorted key order using the same helpers as the main rule
261+ // so the autofix ordering matches the diagnostic ordering.
262+ let mut sorted_keys = keys_for_cmp. clone ( ) ;
263+ if self . natural {
264+ natural_sort ( & mut sorted_keys) ;
265+ } else {
266+ alphanumeric_sort ( & mut sorted_keys) ;
267+ }
268+ if self . sort_order == SortOrder :: Desc {
269+ sorted_keys. reverse ( ) ;
270+ }
271+
272+ // Map sorted keys back to indices in the original list. For duplicate
273+ // keys we consume the first unused occurrence.
274+ let mut used = vec ! [ false ; keys_for_cmp. len( ) ] ;
275+ let mut indices: Vec < usize > = Vec :: with_capacity ( keys_for_cmp. len ( ) ) ;
276+
277+ for sk in & sorted_keys {
278+ if let Some ( pos) = keys_for_cmp
279+ . iter ( )
280+ . enumerate ( )
281+ . find ( |( idx, k) | !used[ * idx] && k. as_str ( ) == sk. as_str ( ) )
282+ . map ( |( i, _) | i)
283+ {
284+ used[ pos] = true ;
285+ indices. push ( pos) ;
286+ }
287+ }
288+
289+ // Build sorted text by concatenating the full property snippets.
290+ // When moving snippets that used to be non-last (and thus include a
291+ // trailing comma) to the end, we must remove their trailing comma so
292+ // the resulting object doesn't end up with an extra comma before `}`.
293+ // Also normalize separators between properties to `, ` for clarity.
294+ let mut sorted_text = String :: new ( ) ;
295+ let trim_end_commas = |s : & str | -> String {
296+ let s = s. trim_end ( ) ;
297+ let mut trimmed = s. to_string ( ) ;
298+
299+ // remove a single trailing comma if present
300+ if trimmed. ends_with ( ',' ) {
301+ trimmed. pop ( ) ;
302+ trimmed = trimmed. trim_end ( ) . to_string ( ) ;
303+ }
304+ trimmed
305+ } ;
306+
307+ for ( pos, & idx) in indices. iter ( ) . enumerate ( ) {
308+ let is_last_in_new = pos + 1 == indices. len ( ) ;
309+ let part = & prop_texts[ idx] ;
310+
311+ if is_last_in_new {
312+ // Ensure last property does not end with a comma or extra space
313+ sorted_text. push_str ( & trim_end_commas ( part) ) ;
314+ } else {
315+ // For non-last properties, ensure there is exactly ", " after the
316+ // property (regardless of how it appeared originally).
317+ let trimmed = trim_end_commas ( part) ;
318+ sorted_text. push_str ( & trimmed) ;
319+ sorted_text. push_str ( ", " ) ;
320+ }
321+ }
322+
323+ // Replace the full properties range
324+ let replace_span = Span :: new ( props[ 0 ] . 1 . start , props[ props. len ( ) - 1 ] . 1 . end ) ;
325+
326+ ctx. diagnostic_with_fix ( sort_properties_diagnostic ( node. span ( ) ) , |fixer| {
327+ fixer. replace ( replace_span, sorted_text)
328+ } ) ;
329+
330+ // we've emitted a fix for this node; stop processing this node
331+ return ;
332+ }
333+
334+ // Fallback: still emit diagnostic if we couldn't produce a safe fix
194335 ctx. diagnostic ( sort_properties_diagnostic ( node. span ( ) ) ) ;
195336 }
196337 }
@@ -1024,5 +1165,22 @@ fn test() {
10241165 ) , // { "ecmaVersion": 2018 }
10251166 ] ;
10261167
1027- Tester :: new ( SortKeys :: NAME , SortKeys :: PLUGIN , pass, fail) . test_and_snapshot ( ) ;
1168+ // Add comprehensive fixer tests: the rule now advertises conditional fixes,
1169+ // so provide expect_fix cases.
1170+ let fix = vec ! [
1171+ // Basic alphabetical sorting
1172+ ( "var obj = {b:1, a:2}" , "var obj = {a:2, b:1}" ) ,
1173+ // Case sensitivity - lowercase comes after uppercase, so a:2 should come after B:1
1174+ ( "var obj = {a:1, B:2}" , "var obj = {B:2, a:1}" ) ,
1175+ // Trailing commas preserved
1176+ ( "var obj = {b:1, a:2,}" , "var obj = {a:2, b:1,}" ) ,
1177+ // With spaces and various formatting
1178+ ( "var obj = { z: 1, a: 2 }" , "var obj = { a: 2, z: 1 }" ) ,
1179+ // Three properties
1180+ ( "var obj = {c:1, a:2, b:3}" , "var obj = {a:2, b:3, c:1}" ) ,
1181+ // Mixed types
1182+ ( "var obj = {2:1, a:2, 1:3}" , "var obj = {1:3, 2:1, a:2}" ) ,
1183+ ] ;
1184+
1185+ Tester :: new ( SortKeys :: NAME , SortKeys :: PLUGIN , pass, fail) . expect_fix ( fix) . test_and_snapshot ( ) ;
10281186}
0 commit comments