@blumintinc/eslint-plugin-blumint 1.20.65 → 1.20.67

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,6 +3,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.preferSxPropOverSystemProps = void 0;
4
4
  const utils_1 = require("@typescript-eslint/utils");
5
5
  const createRule_1 = require("../utils/createRule");
6
+ /**
7
+ * Matches Prettier's own default. The autofix rewrites JSX that a formatter
8
+ * owns, so a line it emits past this width is rewritten on the next
9
+ * `prettier --write` — and fails `prettier --check` in the meantime.
10
+ */
11
+ const DEFAULT_PRINT_WIDTH = 80;
6
12
  /**
7
13
  * The canonical set of MUI system props that are deprecated in favor of `sx`.
8
14
  * These are all props that MUI resolves via its system/styled engine and will
@@ -235,6 +241,491 @@ function attrValueToSxValue(attr, sourceCode) {
235
241
  }
236
242
  return sourceCode.getText(value);
237
243
  }
244
+ /** The whitespace that indents the line containing `offset`. */
245
+ const indentationAt = (sourceCode, offset) => {
246
+ const text = sourceCode.getText();
247
+ const lineStart = text.lastIndexOf('\n', offset - 1) + 1;
248
+ const match = /^[ \t]*/.exec(text.slice(lineStart, offset));
249
+ return match ? match[0] : '';
250
+ };
251
+ /**
252
+ * Ranges whose interior line breaks carry string data rather than formatting.
253
+ * A multi-line template literal (or a string spliced together with line
254
+ * continuations) evaluates to the whitespace written inside it, so shifting
255
+ * those lines would silently change the value the code produces — the same
256
+ * carve-out `use-latest-callback` makes for a relocated callback body.
257
+ */
258
+ const stringDataRangesOf = (sourceCode, node) => sourceCode
259
+ .getTokens(node)
260
+ .filter((token) => (token.type === utils_1.AST_TOKEN_TYPES.Template ||
261
+ token.type === utils_1.AST_TOKEN_TYPES.String) &&
262
+ token.loc.start.line !== token.loc.end.line)
263
+ .map((token) => token.range);
264
+ /**
265
+ * A per-line transform moving text written at `fromIndent` to `toIndent`, or
266
+ * null when neither indentation is a prefix of the other (tabs against spaces),
267
+ * where no delta can be applied without corrupting the layout.
268
+ */
269
+ const lineShifterBetween = (fromIndent, toIndent) => {
270
+ if (fromIndent === toIndent) {
271
+ return (line) => line;
272
+ }
273
+ if (fromIndent.startsWith(toIndent)) {
274
+ const removed = fromIndent.slice(toIndent.length);
275
+ return (line) => line.startsWith(removed) ? line.slice(removed.length) : line;
276
+ }
277
+ if (toIndent.startsWith(fromIndent)) {
278
+ const added = toIndent.slice(fromIndent.length);
279
+ return (line) => `${added}${line}`;
280
+ }
281
+ return null;
282
+ };
283
+ /**
284
+ * A node's text with its continuation lines moved from the depth it was
285
+ * written at to `toIndent`. Null when the move is not expressible, which asks
286
+ * the caller to leave the source as the author wrote it.
287
+ */
288
+ const reindentedText = (sourceCode, node, fromIndent, toIndent) => {
289
+ const text = sourceCode.getText(node);
290
+ if (!text.includes('\n')) {
291
+ return text;
292
+ }
293
+ const shiftLine = lineShifterBetween(fromIndent, toIndent);
294
+ if (!shiftLine) {
295
+ return null;
296
+ }
297
+ const stringData = stringDataRangesOf(sourceCode, node);
298
+ const carriesStringData = (offset) => stringData.some(([start, end]) => start < offset && offset < end);
299
+ let offset = node.range[0];
300
+ return text
301
+ .split('\n')
302
+ .map((line, index) => {
303
+ const lineStart = offset;
304
+ offset += line.length + 1;
305
+ // The first line is spliced in after the property key, so it has no
306
+ // indentation of its own left to adjust.
307
+ if (index === 0 || line.trim() === '' || carriesStringData(lineStart)) {
308
+ return line;
309
+ }
310
+ return shiftLine(line);
311
+ })
312
+ .join('\n');
313
+ };
314
+ /**
315
+ * Ranges of the file's block comments. Their interior lines are excluded from
316
+ * the indentation census below, so only a comment's opening line — which does
317
+ * sit at the surrounding code's depth — is ever measured.
318
+ */
319
+ const blockCommentRangesOf = (sourceCode) => sourceCode
320
+ .getAllComments()
321
+ .filter((comment) => comment.type === utils_1.AST_TOKEN_TYPES.Block)
322
+ .map((comment) => comment.range);
323
+ /**
324
+ * The file's own nesting step, taken as the most common indentation increase
325
+ * between consecutive lines. Reading it from the source keeps emitted code in
326
+ * the author's units instead of assuming a two-space, space-indented file.
327
+ */
328
+ const indentUnitOf = (sourceCode) => {
329
+ const text = sourceCode.getText();
330
+ const blockComments = blockCommentRangesOf(sourceCode);
331
+ // A block comment's continuation lines align on the `*` one column in from
332
+ // the comment's own indentation. That is comment alignment, not a nesting
333
+ // step, and counting it makes any JSDoc-heavy file look 1-space indented.
334
+ const continuesBlockComment = (offset) => blockComments.some(([start, end]) => start < offset && offset < end);
335
+ const frequencies = new Map();
336
+ let previous = '';
337
+ let offset = 0;
338
+ for (const line of text.split('\n')) {
339
+ const lineStart = offset;
340
+ offset += line.length + 1;
341
+ if (line.trim() === '')
342
+ continue;
343
+ if (continuesBlockComment(lineStart))
344
+ continue;
345
+ const match = /^[ \t]*/.exec(line);
346
+ const indent = match ? match[0] : '';
347
+ if (indent.length > previous.length && indent.startsWith(previous)) {
348
+ const delta = indent.slice(previous.length);
349
+ frequencies.set(delta, (frequencies.get(delta) ?? 0) + 1);
350
+ }
351
+ previous = indent;
352
+ }
353
+ let unit = ' ';
354
+ let best = 0;
355
+ for (const [delta, count] of frequencies) {
356
+ if (count > best) {
357
+ unit = delta;
358
+ best = count;
359
+ }
360
+ }
361
+ return unit;
362
+ };
363
+ /** The source with every planned edit applied, used to measure emitted lines. */
364
+ const applyEdits = (text, edits) => {
365
+ const ordered = [...edits].sort((a, b) => b.range[0] - a.range[0]);
366
+ let result = text;
367
+ for (const edit of ordered) {
368
+ result = `${result.slice(0, edit.range[0])}${edit.text}${result.slice(edit.range[1])}`;
369
+ }
370
+ return result;
371
+ };
372
+ const sxSlotOf = (sxAttr) => {
373
+ if (sxAttr === null) {
374
+ return { kind: 'new' };
375
+ }
376
+ const { value } = sxAttr;
377
+ if (value === null) {
378
+ // `<Box sx />` — there is no value to merge into.
379
+ return { kind: 'unsupported' };
380
+ }
381
+ if (value.type === utils_1.AST_NODE_TYPES.JSXExpressionContainer) {
382
+ const { expression } = value;
383
+ if (expression.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
384
+ return { kind: 'object', object: expression };
385
+ }
386
+ if (expression.type === utils_1.AST_NODE_TYPES.ArrayExpression) {
387
+ return { kind: 'array', array: expression };
388
+ }
389
+ return { kind: 'spread', expression };
390
+ }
391
+ return { kind: 'spread', expression: value };
392
+ };
393
+ /**
394
+ * Plans every edit the autofix makes for one JSX element.
395
+ *
396
+ * Prettier keeps an author's decision to break an object literal across lines
397
+ * but rewrites any line past the print width, so under-wrapping is the only
398
+ * defect that matters here: every branch below emits the compact single-line
399
+ * form and only adds line breaks when that form would overflow, or when the
400
+ * object being merged into is already expanded.
401
+ */
402
+ function planSxEdits(sourceCode, node, systemPropAttrs, sxAttr, printWidth) {
403
+ const source = sourceCode.getText();
404
+ const slot = sxSlotOf(sxAttr);
405
+ if (slot.kind === 'unsupported') {
406
+ return [];
407
+ }
408
+ const anchor = systemPropAttrs[0];
409
+ const sxSlotNode = sxAttr ?? anchor;
410
+ const indentUnit = indentUnitOf(sourceCode);
411
+ const systemPropSet = new Set(systemPropAttrs);
412
+ /**
413
+ * The `key: value` pairs for the moved props. `targetIndent` is the depth the
414
+ * pairs are emitted at, which only matters for a value spanning several lines
415
+ * (a nested object, a template literal): its continuation lines have to move
416
+ * with it. Null asks for the value verbatim, as the single-line form leaves
417
+ * every pair on the line it already occupies.
418
+ */
419
+ const renderEntries = (targetIndent) => {
420
+ const entries = [];
421
+ for (const attr of systemPropAttrs) {
422
+ const key = attr.name.type === utils_1.AST_NODE_TYPES.JSXIdentifier
423
+ ? attr.name.name
424
+ : sourceCode.getText(attr.name);
425
+ const rawValue = attrValueToSxValue(attr, sourceCode);
426
+ if (targetIndent === null || !rawValue.includes('\n')) {
427
+ entries.push(`${key}: ${rawValue}`);
428
+ continue;
429
+ }
430
+ // Only an expression container's text comes straight from the source, so
431
+ // it is the only multi-line value whose lines can be located and moved.
432
+ const value = attr.value;
433
+ if (!value ||
434
+ value.type !== utils_1.AST_NODE_TYPES.JSXExpressionContainer ||
435
+ value.expression.type === utils_1.AST_NODE_TYPES.JSXEmptyExpression) {
436
+ return null;
437
+ }
438
+ const moved = reindentedText(sourceCode, value.expression, indentationAt(sourceCode, attr.range[0]), targetIndent);
439
+ if (moved === null) {
440
+ return null;
441
+ }
442
+ entries.push(`${key}: ${moved}`);
443
+ }
444
+ return entries;
445
+ };
446
+ const inlineEntries = renderEntries(null) ?? [];
447
+ const joinedEntries = inlineEntries.join(', ');
448
+ /** Deleting the whitespace ahead of a prop keeps attributes from drifting apart. */
449
+ const removalOf = (attr) => {
450
+ let start = attr.range[0];
451
+ while (start > 0 && /\s/.test(source[start - 1])) {
452
+ start--;
453
+ }
454
+ return { range: [start, attr.range[1]], text: '' };
455
+ };
456
+ // The first system prop is reused as the insertion point when the element has
457
+ // no `sx` yet, so it is replaced rather than removed.
458
+ const removals = (sxAttr === null ? systemPropAttrs.slice(1) : systemPropAttrs).map(removalOf);
459
+ const emptyRangeAt = (offset) => [offset, offset];
460
+ const inlineEditFor = () => {
461
+ switch (slot.kind) {
462
+ case 'new':
463
+ return { range: anchor.range, text: `sx={{ ${joinedEntries} }}` };
464
+ case 'object': {
465
+ const first = slot.object.properties[0];
466
+ return first
467
+ ? {
468
+ range: emptyRangeAt(first.range[0]),
469
+ text: `${joinedEntries}, `,
470
+ }
471
+ : { range: slot.object.range, text: `{ ${joinedEntries} }` };
472
+ }
473
+ case 'array': {
474
+ const first = slot.array.elements[0];
475
+ if (slot.array.elements.length === 0) {
476
+ return { range: slot.array.range, text: `[{ ${joinedEntries} }]` };
477
+ }
478
+ // A hole (`sx={[, base]}`) has no node to anchor an insertion on.
479
+ return first
480
+ ? {
481
+ range: emptyRangeAt(first.range[0]),
482
+ text: `{ ${joinedEntries} }, `,
483
+ }
484
+ : null;
485
+ }
486
+ case 'spread':
487
+ return sxAttr
488
+ ? {
489
+ range: sxAttr.range,
490
+ text: `sx={{ ${joinedEntries}, ...${sourceCode.getText(slot.expression)} }}`,
491
+ }
492
+ : null;
493
+ }
494
+ };
495
+ const inlineEdit = inlineEditFor();
496
+ if (inlineEdit === null) {
497
+ return [];
498
+ }
499
+ const inlineEdits = [inlineEdit, ...removals];
500
+ /** The indentation of a node that starts its own line, else null. */
501
+ const ownLineIndentOf = (offset) => {
502
+ const lineStart = source.lastIndexOf('\n', offset - 1) + 1;
503
+ const lead = source.slice(lineStart, offset);
504
+ return lead.trim() === '' ? lead : null;
505
+ };
506
+ /**
507
+ * Whether the author already broke a literal open, which Prettier preserves.
508
+ * Merging into it has to follow that shape at any width; splicing onto the
509
+ * first member's line leaves the literal formatted two ways at once.
510
+ */
511
+ const firstMemberOnOwnLine = (literal, first) => {
512
+ if (!source.slice(literal.range[0] + 1, first.range[0]).includes('\n')) {
513
+ return null;
514
+ }
515
+ return ownLineIndentOf(first.range[0]);
516
+ };
517
+ const objectText = (parts, indent, expand) => expand
518
+ ? `{\n${parts
519
+ .map((part) => `${indent}${indentUnit}${part},`)
520
+ .join('\n')}\n${indent}}`
521
+ : `{ ${parts.join(', ')} }`;
522
+ /** Existing members reproduced verbatim, or null when any spans lines. */
523
+ const verbatimTextsOf = (nodes) => {
524
+ const texts = [];
525
+ for (const member of nodes) {
526
+ if (!member) {
527
+ return null;
528
+ }
529
+ const text = sourceCode.getText(member);
530
+ if (text.includes('\n')) {
531
+ return null;
532
+ }
533
+ texts.push(text);
534
+ }
535
+ return texts;
536
+ };
537
+ /** The object literal for the moved props, expanded only when it must be. */
538
+ const objectAt = (indent) => {
539
+ const inline = `{ ${joinedEntries} }`;
540
+ if (!inline.includes('\n') && indent.length + inline.length <= printWidth) {
541
+ return inline;
542
+ }
543
+ const parts = renderEntries(`${indent}${indentUnit}`);
544
+ return parts === null ? null : objectText(parts, indent, true);
545
+ };
546
+ /**
547
+ * The whole rewritten `sx={...}` attribute rendered at `indent`, breaking the
548
+ * literal open only when the compact form would overflow the print width.
549
+ */
550
+ const sxAttributeText = (indent) => {
551
+ if (slot.kind === 'array') {
552
+ const existing = verbatimTextsOf(slot.array.elements);
553
+ if (existing === null) {
554
+ return null;
555
+ }
556
+ const inline = `sx={[${[`{ ${joinedEntries} }`, ...existing].join(', ')}]}`;
557
+ if (!inline.includes('\n') &&
558
+ indent.length + inline.length <= printWidth) {
559
+ return inline;
560
+ }
561
+ const elementIndent = `${indent}${indentUnit}`;
562
+ const object = objectAt(elementIndent);
563
+ if (object === null) {
564
+ return null;
565
+ }
566
+ const elements = [object, ...existing]
567
+ .map((element) => `${elementIndent}${element},`)
568
+ .join('\n');
569
+ return `sx={[\n${elements}\n${indent}]}`;
570
+ }
571
+ const trailing = slot.kind === 'object'
572
+ ? verbatimTextsOf(slot.object.properties)
573
+ : slot.kind === 'spread'
574
+ ? [`...${sourceCode.getText(slot.expression)}`]
575
+ : [];
576
+ if (trailing === null || trailing.some((text) => text.includes('\n'))) {
577
+ return null;
578
+ }
579
+ const inline = `sx={${objectText([...inlineEntries, ...trailing], indent, false)}}`;
580
+ if (!inline.includes('\n') && indent.length + inline.length <= printWidth) {
581
+ return inline;
582
+ }
583
+ const parts = renderEntries(`${indent}${indentUnit}`);
584
+ if (parts === null) {
585
+ return null;
586
+ }
587
+ return `sx={${objectText([...parts, ...trailing], indent, true)}}`;
588
+ };
589
+ /**
590
+ * Merging into a literal the author already expanded: one new member per
591
+ * line, at the indentation the existing members sit at.
592
+ */
593
+ const expandedMergeEdits = () => {
594
+ if (slot.kind === 'object') {
595
+ const first = slot.object.properties[0];
596
+ if (!first) {
597
+ return null;
598
+ }
599
+ const indent = firstMemberOnOwnLine(slot.object, first);
600
+ if (indent === null) {
601
+ return null;
602
+ }
603
+ const parts = renderEntries(indent);
604
+ if (parts === null) {
605
+ return null;
606
+ }
607
+ return [
608
+ {
609
+ range: emptyRangeAt(first.range[0]),
610
+ text: parts.map((part) => `${part},\n${indent}`).join(''),
611
+ },
612
+ ...removals,
613
+ ];
614
+ }
615
+ if (slot.kind === 'array') {
616
+ const first = slot.array.elements[0];
617
+ if (!first) {
618
+ return null;
619
+ }
620
+ const indent = firstMemberOnOwnLine(slot.array, first);
621
+ if (indent === null) {
622
+ return null;
623
+ }
624
+ const object = objectAt(indent);
625
+ if (object === null) {
626
+ return null;
627
+ }
628
+ return [
629
+ {
630
+ range: emptyRangeAt(first.range[0]),
631
+ text: `${object},\n${indent}`,
632
+ },
633
+ ...removals,
634
+ ];
635
+ }
636
+ return null;
637
+ };
638
+ const expandedMerge = expandedMergeEdits();
639
+ if (expandedMerge) {
640
+ return expandedMerge;
641
+ }
642
+ // Measure the line the compact form actually lands on, with every removal
643
+ // already applied, rather than guessing at the element's final shape.
644
+ const simulated = applyEdits(source, inlineEdits);
645
+ const shiftBefore = (offset) => inlineEdits
646
+ .filter((edit) => edit.range[1] <= offset)
647
+ .reduce((total, edit) => total + edit.text.length - (edit.range[1] - edit.range[0]), 0);
648
+ const slotStart = sxSlotNode.range[0] + shiftBefore(sxSlotNode.range[0]);
649
+ const slotEnd = sxSlotNode.range[1] + shiftBefore(sxSlotNode.range[1]);
650
+ const lineStart = simulated.lastIndexOf('\n', slotStart - 1) + 1;
651
+ const lineBreak = simulated.indexOf('\n', slotStart);
652
+ const lineEnd = lineBreak === -1 ? simulated.length : lineBreak;
653
+ // A prop whose value already spans lines cannot sit in a one-line object: the
654
+ // literal would open on one line and close on another with its remaining
655
+ // entries crammed against the value. Width says nothing here, because the
656
+ // measured line stops at the value's first break.
657
+ const hasMultilineEntry = inlineEntries.some((entry) => entry.includes('\n'));
658
+ if (!hasMultilineEntry && lineEnd - lineStart <= printWidth) {
659
+ return inlineEdits;
660
+ }
661
+ // The attribute may end several lines below where it starts once a multi-line
662
+ // value is folded in, so ownership is judged against the line it ends on.
663
+ const slotLineBreak = simulated.indexOf('\n', slotEnd);
664
+ const slotLineEnd = slotLineBreak === -1 ? simulated.length : slotLineBreak;
665
+ const attributeOwnsLine = simulated.slice(lineStart, slotStart).trim() === '' &&
666
+ simulated.slice(slotEnd, slotLineEnd).trim() === '';
667
+ if (attributeOwnsLine) {
668
+ const rewritten = sxAttributeText(simulated.slice(lineStart, slotStart));
669
+ if (rewritten !== null) {
670
+ return [{ range: sxSlotNode.range, text: rewritten }, ...removals];
671
+ }
672
+ }
673
+ /**
674
+ * An element whose attributes all share one line has no room left for the
675
+ * merged `sx`; Prettier's answer is to give every attribute its own line,
676
+ * which is reproduced here so the emitted element is already in the shape the
677
+ * formatter would put it in.
678
+ */
679
+ const restructuredElement = () => {
680
+ if (node.loc.start.line !== node.loc.end.line) {
681
+ return null;
682
+ }
683
+ // Rebuilding from the attribute list would drop anything the list does not
684
+ // cover, and would leave a trailing fragment of an unrelated statement.
685
+ if (sourceCode.getCommentsInside(node).length > 0) {
686
+ return null;
687
+ }
688
+ const elementIndent = ownLineIndentOf(node.range[0]);
689
+ if (elementIndent === null) {
690
+ return null;
691
+ }
692
+ const tailBreak = source.indexOf('\n', node.range[1]);
693
+ const tail = source.slice(node.range[1], tailBreak === -1 ? source.length : tailBreak);
694
+ if (tail.trim() !== '') {
695
+ return null;
696
+ }
697
+ const first = node.attributes[0];
698
+ if (!first) {
699
+ return null;
700
+ }
701
+ const attributeIndent = `${elementIndent}${indentUnit}`;
702
+ const attributes = [];
703
+ for (const attr of node.attributes) {
704
+ if (attr === sxSlotNode) {
705
+ const rewritten = sxAttributeText(attributeIndent);
706
+ if (rewritten === null) {
707
+ return null;
708
+ }
709
+ attributes.push(rewritten);
710
+ continue;
711
+ }
712
+ if (systemPropSet.has(attr)) {
713
+ continue;
714
+ }
715
+ attributes.push(sourceCode.getText(attr));
716
+ }
717
+ const head = source.slice(node.range[0], first.range[0]).trimEnd();
718
+ const body = attributes
719
+ .map((attribute) => `${attributeIndent}${attribute}`)
720
+ .join('\n');
721
+ return `${head}\n${body}\n${elementIndent}${node.selfClosing ? '/>' : '>'}`;
722
+ };
723
+ const restructured = restructuredElement();
724
+ if (restructured !== null) {
725
+ return [{ range: node.range, text: restructured }];
726
+ }
727
+ return inlineEdits;
728
+ }
238
729
  exports.preferSxPropOverSystemProps = (0, createRule_1.createRule)({
239
730
  name: 'prefer-sx-prop-over-system-props',
240
731
  meta: {
@@ -256,6 +747,10 @@ exports.preferSxPropOverSystemProps = (0, createRule_1.createRule)({
256
747
  type: 'array',
257
748
  items: { type: 'string' },
258
749
  },
750
+ printWidth: {
751
+ type: 'number',
752
+ minimum: 1,
753
+ },
259
754
  },
260
755
  additionalProperties: false,
261
756
  },
@@ -272,6 +767,9 @@ exports.preferSxPropOverSystemProps = (0, createRule_1.createRule)({
272
767
  const extraAllowed = options.allowedProps
273
768
  ? new Set(options.allowedProps)
274
769
  : new Set();
770
+ const printWidth = typeof options.printWidth === 'number' && options.printWidth > 0
771
+ ? options.printWidth
772
+ : DEFAULT_PRINT_WIDTH;
275
773
  function isAllowedProp(name) {
276
774
  if (DEFAULT_ALLOWED_PROPS.has(name))
277
775
  return true;
@@ -319,21 +817,6 @@ exports.preferSxPropOverSystemProps = (0, createRule_1.createRule)({
319
817
  if (systemPropAttrs.length === 0)
320
818
  return;
321
819
  const sourceCode = context.getSourceCode();
322
- const fullSource = sourceCode.getText();
323
- /**
324
- * Remove an attribute AND the whitespace that precedes it so the
325
- * output does not accumulate extra spaces between attributes.
326
- */
327
- function removeAttrWithLeadingSpace(fixer, attr) {
328
- const range = attr.range ?? [0, 0];
329
- const [start, end] = range;
330
- // Walk backwards from the attribute start to consume any whitespace
331
- let wsStart = start;
332
- while (wsStart > 0 && /\s/.test(fullSource[wsStart - 1])) {
333
- wsStart--;
334
- }
335
- return fixer.removeRange([wsStart, end]);
336
- }
337
820
  // Report each system prop. Only the first carries the fixer to avoid
338
821
  // overlapping fix ranges on the same element.
339
822
  systemPropAttrs.forEach((attr, index) => {
@@ -345,73 +828,7 @@ exports.preferSxPropOverSystemProps = (0, createRule_1.createRule)({
345
828
  messageId: 'preferSxProp',
346
829
  data: { prop: propName },
347
830
  fix: index === 0
348
- ? (fixer) => {
349
- const sc = sourceCode;
350
- const newEntries = systemPropAttrs
351
- .map((a) => {
352
- const key = a.name.type === utils_1.AST_NODE_TYPES.JSXIdentifier
353
- ? a.name.name
354
- : sc.getText(a.name);
355
- const val = attrValueToSxValue(a, sc);
356
- return `${key}: ${val}`;
357
- })
358
- .join(', ');
359
- const fixes = [];
360
- if (sxAttr === null) {
361
- // No existing sx — replace the first system prop with sx={{...}}
362
- // and delete the rest (including their leading whitespace).
363
- fixes.push(fixer.replaceText(systemPropAttrs[0], `sx={{ ${newEntries} }}`));
364
- for (let i = 1; i < systemPropAttrs.length; i++) {
365
- fixes.push(removeAttrWithLeadingSpace(fixer, systemPropAttrs[i]));
366
- }
367
- }
368
- else if (sxAttr.value?.type ===
369
- utils_1.AST_NODE_TYPES.JSXExpressionContainer &&
370
- sxAttr.value.expression.type ===
371
- utils_1.AST_NODE_TYPES.ObjectExpression) {
372
- // Existing sx={{ ... }} — merge system props at the front.
373
- const existingObj = sxAttr.value.expression;
374
- const existingProps = existingObj.properties;
375
- if (existingProps.length === 0) {
376
- fixes.push(fixer.replaceText(existingObj, `{ ${newEntries} }`));
377
- }
378
- else {
379
- fixes.push(fixer.insertTextBefore(existingProps[0], `${newEntries}, `));
380
- }
381
- for (const a of systemPropAttrs) {
382
- fixes.push(removeAttrWithLeadingSpace(fixer, a));
383
- }
384
- }
385
- else if (sxAttr.value?.type ===
386
- utils_1.AST_NODE_TYPES.JSXExpressionContainer &&
387
- sxAttr.value.expression.type ===
388
- utils_1.AST_NODE_TYPES.ArrayExpression) {
389
- // sx={[...]} — prepend a new object as the first array element.
390
- const arr = sxAttr.value.expression;
391
- if (arr.elements.length === 0) {
392
- fixes.push(fixer.replaceText(arr, `[{ ${newEntries} }]`));
393
- }
394
- else {
395
- fixes.push(fixer.insertTextBefore(arr.elements[0], `{ ${newEntries} }, `));
396
- }
397
- for (const a of systemPropAttrs) {
398
- fixes.push(removeAttrWithLeadingSpace(fixer, a));
399
- }
400
- }
401
- else if (sxAttr.value !== null) {
402
- // sx is a variable/expression — spread it: sx={{ entries, ...expr }}
403
- const innerExpr = sxAttr.value.type ===
404
- utils_1.AST_NODE_TYPES.JSXExpressionContainer
405
- ? sc.getText(sxAttr.value
406
- .expression)
407
- : sc.getText(sxAttr.value);
408
- fixes.push(fixer.replaceText(sxAttr, `sx={{ ${newEntries}, ...${innerExpr} }}`));
409
- for (const a of systemPropAttrs) {
410
- fixes.push(removeAttrWithLeadingSpace(fixer, a));
411
- }
412
- }
413
- return fixes;
414
- }
831
+ ? (fixer) => planSxEdits(sourceCode, node, systemPropAttrs, sxAttr, printWidth).map((edit) => fixer.replaceTextRange(edit.range, edit.text))
415
832
  : null,
416
833
  });
417
834
  });
@@ -1,3 +1,8 @@
1
1
  import { TSESLint } from '@typescript-eslint/utils';
2
- export declare const preferUnionFromConstArray: TSESLint.RuleModule<"preferDerivedUnion", [], TSESLint.RuleListener>;
2
+ type Options = [
3
+ {
4
+ printWidth?: number;
5
+ }
6
+ ];
7
+ export declare const preferUnionFromConstArray: TSESLint.RuleModule<"preferDerivedUnion", Options, TSESLint.RuleListener>;
3
8
  export default preferUnionFromConstArray;