@tai-kun/vp-lint-plugins 0.0.2 → 0.0.3

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.
@@ -10,12 +10,23 @@ interface SortPlanOptions {
10
10
  */
11
11
  forceMultiline?: boolean;
12
12
  }
13
+ interface SortEdit {
14
+ end: number;
15
+ start: number;
16
+ text: string;
17
+ }
13
18
  type SortPlan = {
19
+ edits: null;
14
20
  needsFix: false;
15
21
  replacement: null;
16
22
  } | {
23
+ edits: null;
17
24
  needsFix: true;
18
25
  replacement: string;
26
+ } | {
27
+ edits: SortEdit[];
28
+ needsFix: true;
29
+ replacement: null;
19
30
  };
20
31
  declare function compareNames(left: string, right: string): number;
21
32
  /**
@@ -24,6 +35,7 @@ declare function compareNames(left: string, right: string): number;
24
35
  * - プロパティー名の長さが短い順、同じ長さなら名前順
25
36
  * - 空行で区切られたグループ内だけで並び替え
26
37
  * - プロパティー直上のコメントはプロパティーと一緒に移動
38
+ * - 空行を挟んだコメントはグループ見出しとしてその場に残す
27
39
  *
28
40
  * 並び替えが安全でない場合は null を返します。
29
41
  */
@@ -1 +1 @@
1
- {"version":3,"file":"object-sort-utils.d.ts","sourceRoot":"","sources":["../../../src/rules/object-sort-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAW,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAE1E,KAAK,cAAc,GAAG,MAAM,CAAC,gBAAgB,GAAG,MAAM,CAAC,aAAa,CAAC;AAIrE,UAAU,eAAe;IACvB;;OAEG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IAEpB;;OAEG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAYD,KAAK,QAAQ,GACT;IACE,QAAQ,EAAE,KAAK,CAAC;IAChB,WAAW,EAAE,IAAI,CAAC;CACnB,GACD;IACE,QAAQ,EAAE,IAAI,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC;AAuEN,iBAAS,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAiBzD;AAeD;;;;;;;;GAQG;AACH,iBAAS,WAAW,CAClB,IAAI,EAAE,cAAc,EACpB,UAAU,EAAE,UAAU,EACtB,OAAO,GAAE,eAAoB,GAC5B,QAAQ,GAAG,IAAI,CAuLjB;AAED,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,CAAC"}
1
+ {"version":3,"file":"object-sort-utils.d.ts","sourceRoot":"","sources":["../../../src/rules/object-sort-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAW,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAE1E,KAAK,cAAc,GAAG,MAAM,CAAC,gBAAgB,GAAG,MAAM,CAAC,aAAa,CAAC;AAIrE,UAAU,eAAe;IACvB;;OAEG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IAEpB;;OAEG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAYD,UAAU,QAAQ;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;CACd;AAID,KAAK,QAAQ,GACT;IACE,KAAK,EAAE,IAAI,CAAC;IACZ,QAAQ,EAAE,KAAK,CAAC;IAChB,WAAW,EAAE,IAAI,CAAC;CACnB,GACD;IACE,KAAK,EAAE,IAAI,CAAC;IACZ,QAAQ,EAAE,IAAI,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;CACrB,GACD;IACE,KAAK,EAAE,QAAQ,EAAE,CAAC;IAClB,QAAQ,EAAE,IAAI,CAAC;IACf,WAAW,EAAE,IAAI,CAAC;CACnB,CAAC;AAuEN,iBAAS,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAiBzD;AAyCD;;;;;;;;;GASG;AACH,iBAAS,WAAW,CAClB,IAAI,EAAE,cAAc,EACpB,UAAU,EAAE,UAAU,EACtB,OAAO,GAAE,eAAoB,GAC5B,QAAQ,GAAG,IAAI,CAuNjB;AAED,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,CAAC"}
@@ -73,12 +73,33 @@ function sortGroup(group) {
73
73
  const rest = group.filter((unit) => unit.name === null);
74
74
  return [...sortable, ...rest];
75
75
  }
76
+ function splitGroups(units) {
77
+ const groups = [];
78
+ let currentGroup = [];
79
+ let previousEndLine = null;
80
+ for (const unit of units) {
81
+ if (previousEndLine !== null && unit.startLine > previousEndLine + 1) {
82
+ groups.push(currentGroup);
83
+ currentGroup = [];
84
+ }
85
+ currentGroup.push(unit);
86
+ previousEndLine = unit.endLine;
87
+ }
88
+ if (currentGroup.length > 0) {
89
+ groups.push(currentGroup);
90
+ }
91
+ return groups;
92
+ }
93
+ function getEditText(units) {
94
+ return units.map((unit) => unit.text).join(",\n");
95
+ }
76
96
  /**
77
97
  * オブジェクトリテラル/オブジェクトパターンの並び替え計画を返します。
78
98
  *
79
99
  * - プロパティー名の長さが短い順、同じ長さなら名前順
80
100
  * - 空行で区切られたグループ内だけで並び替え
81
101
  * - プロパティー直上のコメントはプロパティーと一緒に移動
102
+ * - 空行を挟んだコメントはグループ見出しとしてその場に残す
82
103
  *
83
104
  * 並び替えが安全でない場合は null を返します。
84
105
  */
@@ -89,7 +110,6 @@ function getSortPlan(node, sourceCode, options = {}) {
89
110
  }
90
111
  const source = sourceCode.text;
91
112
  const lineStarts = getLineStarts(source);
92
- const lineIndent = getLineIndent(source, lineStarts, node.loc.start.line);
93
113
  const comments = sourceCode
94
114
  .getAllComments()
95
115
  .filter((comment) => comment.range[0] >= node.range[0] && comment.range[1] <= node.range[1]);
@@ -111,7 +131,9 @@ function getSortPlan(node, sourceCode, options = {}) {
111
131
  text: source.slice(property.range[0], property.range[1]),
112
132
  endLine: property.loc.end.line,
113
133
  property,
134
+ endOffset: property.range[1],
114
135
  startLine: property.loc.start.line,
136
+ startOffset: property.range[0],
115
137
  });
116
138
  }
117
139
  }
@@ -122,9 +144,10 @@ function getSortPlan(node, sourceCode, options = {}) {
122
144
  const boundary = previous === null ? node.range[0] + 1 : previous.range[1];
123
145
  const candidates = comments.filter((comment) => comment.range[0] >= boundary && comment.range[1] <= property.range[0]);
124
146
  if (previous !== null &&
125
- comments.some((comment) => comment.loc.start.line === previous.loc.end.line &&
126
- comment.range[0] >= previous.range[1] &&
127
- comment.range[1] <= property.range[0])) {
147
+ candidates.some((comment) => comment.loc.start.line === previous.loc.end.line)) {
148
+ return null;
149
+ }
150
+ if (candidates.some((comment) => comment.loc.start.line === node.loc.start.line)) {
128
151
  return null;
129
152
  }
130
153
  const attached = [];
@@ -134,31 +157,47 @@ function getSortPlan(node, sourceCode, options = {}) {
134
157
  if (comment === undefined) {
135
158
  continue;
136
159
  }
137
- if (comment.loc.end.line !== expectedLine && comment.loc.end.line !== expectedLine - 1) {
160
+ if (comment.loc.end.line === expectedLine) {
138
161
  return null;
139
162
  }
140
- if (comment.loc.start.line === expectedLine) {
141
- return null;
163
+ if (comment.loc.end.line !== expectedLine - 1) {
164
+ break;
142
165
  }
143
- if (comment.loc.start.line === node.loc.start.line) {
166
+ if (comment.loc.start.line === expectedLine) {
144
167
  return null;
145
168
  }
146
169
  attached.unshift(comment);
147
170
  expectedLine = comment.loc.start.line;
148
171
  }
149
- if (attached.length !== candidates.length) {
150
- return null;
151
- }
152
172
  const first = attached[0] ?? property;
153
173
  const startLine = first.loc.start.line;
154
- const unitStart = lineStarts[startLine - 1] ?? 0;
174
+ let startOffset;
175
+ if (attached.length > 0) {
176
+ startOffset = lineStarts[startLine - 1] ?? first.range[0];
177
+ }
178
+ else if (startLine === previous?.loc.end.line) {
179
+ startOffset = property.range[0];
180
+ }
181
+ else {
182
+ startOffset = lineStarts[startLine - 1] ?? property.range[0];
183
+ }
184
+ let endOffset = property.range[1];
185
+ while (endOffset > property.range[0]) {
186
+ const character = source[endOffset - 1];
187
+ if (character !== "," && character !== ";") {
188
+ break;
189
+ }
190
+ endOffset -= 1;
191
+ }
155
192
  const isRest = property.type === "RestElement";
156
193
  units.push({
157
194
  name: isRest ? null : getPropertyName(property),
158
- text: source.slice(unitStart, property.range[1]),
195
+ text: source.slice(startOffset, endOffset),
159
196
  endLine: property.loc.end.line,
160
197
  property,
198
+ endOffset,
161
199
  startLine,
200
+ startOffset,
162
201
  });
163
202
  previousProperty = property;
164
203
  }
@@ -170,52 +209,66 @@ function getSortPlan(node, sourceCode, options = {}) {
170
209
  return null;
171
210
  }
172
211
  }
173
- const groups = [];
174
- let currentGroup = [];
175
- let previousEndLine = null;
176
- for (const unit of units) {
177
- if (previousEndLine !== null && unit.startLine > previousEndLine + 1) {
178
- groups.push(currentGroup);
179
- currentGroup = [];
212
+ if (inlineStart) {
213
+ const lineIndent = getLineIndent(source, lineStarts, node.loc.start.line);
214
+ const groups = splitGroups(units);
215
+ const needsFormat = forceMultiline;
216
+ const needsSort = groups.some((group) => {
217
+ const sorted = sortGroup(group);
218
+ return sorted.some((unit, index) => unit !== group[index]);
219
+ });
220
+ if (!needsSort && !needsFormat) {
221
+ return {
222
+ edits: null,
223
+ needsFix: false,
224
+ replacement: null,
225
+ };
180
226
  }
181
- currentGroup.push(unit);
182
- previousEndLine = unit.endLine;
183
- }
184
- if (currentGroup.length > 0) {
185
- groups.push(currentGroup);
227
+ if (singleLine && !forceMultiline) {
228
+ const sortedUnits = sortGroup(units);
229
+ return {
230
+ edits: null,
231
+ needsFix: true,
232
+ replacement: `{ ${sortedUnits.map((unit) => unit.text).join(", ")} }`,
233
+ };
234
+ }
235
+ const groupsText = groups.map((group) => getEditText(sortGroup(group))).join(",\n\n");
236
+ const lastUnit = groups.flatMap((group) => sortGroup(group)).at(-1);
237
+ const trailingComma = lastUnit?.property.type === "RestElement" ? "" : ",";
238
+ return {
239
+ edits: null,
240
+ needsFix: true,
241
+ replacement: `{\n${groupsText}${trailingComma}\n${lineIndent}}`,
242
+ };
186
243
  }
187
- let needsSort = false;
188
- for (const group of groups) {
244
+ const edits = [];
245
+ for (const group of splitGroups(units)) {
189
246
  const sorted = sortGroup(group);
190
- if (sorted.some((unit, index) => unit !== group[index])) {
191
- needsSort = true;
192
- break;
247
+ if (sorted.every((unit, index) => unit === group[index])) {
248
+ continue;
193
249
  }
250
+ const first = group[0];
251
+ const last = group.at(-1);
252
+ if (first === undefined || last === undefined) {
253
+ continue;
254
+ }
255
+ edits.push({
256
+ end: last.endOffset,
257
+ text: getEditText(sorted),
258
+ start: first.startOffset,
259
+ });
194
260
  }
195
- const needsFormat = forceMultiline && inlineStart;
196
- if (!needsSort && !needsFormat) {
261
+ if (edits.length === 0) {
197
262
  return {
263
+ edits: null,
198
264
  needsFix: false,
199
265
  replacement: null,
200
266
  };
201
267
  }
202
- if (singleLine && !forceMultiline) {
203
- const sortedUnits = sortGroup(units);
204
- return {
205
- needsFix: true,
206
- replacement: `{ ${sortedUnits.map((unit) => unit.text).join(", ")} }`,
207
- };
208
- }
209
- const groupsText = groups
210
- .map((group) => sortGroup(group)
211
- .map((unit) => unit.text)
212
- .join(",\n"))
213
- .join(",\n\n");
214
- const lastUnit = groups.flatMap((group) => sortGroup(group)).at(-1);
215
- const trailingComma = lastUnit?.property.type === "RestElement" ? "" : ",";
216
268
  return {
269
+ edits,
217
270
  needsFix: true,
218
- replacement: `{\n${groupsText}${trailingComma}\n${lineIndent}}`,
271
+ replacement: null,
219
272
  };
220
273
  }
221
274
  export { getSortPlan, compareNames };
@@ -2,7 +2,7 @@
2
2
  * オブジェクトリテラルのプロパティーを、名前の長さが短い順、同じ長さなら名前順に並べます。
3
3
  *
4
4
  * 空行で区切られたグループは意図的なまとまりとみなし、グループ内だけで並べ替えます。
5
- * プロパティー直上のコメントはプロパティーと一緒に移動します。
5
+ * プロパティー直上のコメントはプロパティーと一緒に移動し、空行を挟んだコメントはグループ見出しとして残します。
6
6
  */
7
7
  declare const sortObjectProperties: import("vite-plus/lint/plugins").Rule;
8
8
  export default sortObjectProperties;
@@ -1 +1 @@
1
- {"version":3,"file":"sort-object-properties.d.ts","sourceRoot":"","sources":["../../../src/rules/sort-object-properties.ts"],"names":[],"mappings":"AAIA;;;;;GAKG;AACH,QAAA,MAAM,oBAAoB,uCAiDxB,CAAC;eAEY,oBAAoB"}
1
+ {"version":3,"file":"sort-object-properties.d.ts","sourceRoot":"","sources":["../../../src/rules/sort-object-properties.ts"],"names":[],"mappings":"AAIA;;;;;GAKG;AACH,QAAA,MAAM,oBAAoB,uCA6DxB,CAAC;eAEY,oBAAoB"}
@@ -4,7 +4,7 @@ import { getSortPlan } from "./object-sort-utils.js";
4
4
  * オブジェクトリテラルのプロパティーを、名前の長さが短い順、同じ長さなら名前順に並べます。
5
5
  *
6
6
  * 空行で区切られたグループは意図的なまとまりとみなし、グループ内だけで並べ替えます。
7
- * プロパティー直上のコメントはプロパティーと一緒に移動します。
7
+ * プロパティー直上のコメントはプロパティーと一緒に移動し、空行を挟んだコメントはグループ見出しとして残します。
8
8
  */
9
9
  const sortObjectProperties = defineRule({
10
10
  meta: {
@@ -29,6 +29,9 @@ const sortObjectProperties = defineRule({
29
29
  }
30
30
  context.report({
31
31
  fix(fixer) {
32
+ if (plan.edits !== null) {
33
+ return plan.edits.map((edit) => fixer.replaceTextRange([edit.start, edit.end], edit.text));
34
+ }
32
35
  return fixer.replaceText(node, plan.replacement);
33
36
  },
34
37
  node,
@@ -42,6 +45,9 @@ const sortObjectProperties = defineRule({
42
45
  }
43
46
  context.report({
44
47
  fix(fixer) {
48
+ if (plan.edits !== null) {
49
+ return plan.edits.map((edit) => fixer.replaceTextRange([edit.start, edit.end], edit.text));
50
+ }
45
51
  return fixer.replaceText(node, plan.replacement);
46
52
  },
47
53
  node,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tai-kun/vp-lint-plugins",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "",
5
5
  "homepage": "https://tai-kun.github.io/vp-lint-plugins/",
6
6
  "license": "MIT",
@@ -18,9 +18,17 @@ interface SortPlanOptions {
18
18
 
19
19
  interface SortUnit {
20
20
  endLine: number;
21
+ endOffset: number;
21
22
  name: string | null;
22
23
  property: SortableProperty;
23
24
  startLine: number;
25
+ startOffset: number;
26
+ text: string;
27
+ }
28
+
29
+ interface SortEdit {
30
+ end: number;
31
+ start: number;
24
32
  text: string;
25
33
  }
26
34
 
@@ -28,12 +36,19 @@ type NamedSortUnit = SortUnit & { name: string };
28
36
 
29
37
  type SortPlan =
30
38
  | {
39
+ edits: null;
31
40
  needsFix: false;
32
41
  replacement: null;
33
42
  }
34
43
  | {
44
+ edits: null;
35
45
  needsFix: true;
36
46
  replacement: string;
47
+ }
48
+ | {
49
+ edits: SortEdit[];
50
+ needsFix: true;
51
+ replacement: null;
37
52
  };
38
53
 
39
54
  function getLineStarts(text: string): number[] {
@@ -137,12 +152,39 @@ function sortGroup(group: readonly SortUnit[]): SortUnit[] {
137
152
  return [...sortable, ...rest];
138
153
  }
139
154
 
155
+ function splitGroups(units: readonly SortUnit[]): SortUnit[][] {
156
+ const groups: SortUnit[][] = [];
157
+ let currentGroup: SortUnit[] = [];
158
+ let previousEndLine: number | null = null;
159
+
160
+ for (const unit of units) {
161
+ if (previousEndLine !== null && unit.startLine > previousEndLine + 1) {
162
+ groups.push(currentGroup);
163
+ currentGroup = [];
164
+ }
165
+
166
+ currentGroup.push(unit);
167
+ previousEndLine = unit.endLine;
168
+ }
169
+
170
+ if (currentGroup.length > 0) {
171
+ groups.push(currentGroup);
172
+ }
173
+
174
+ return groups;
175
+ }
176
+
177
+ function getEditText(units: readonly SortUnit[]): string {
178
+ return units.map((unit) => unit.text).join(",\n");
179
+ }
180
+
140
181
  /**
141
182
  * オブジェクトリテラル/オブジェクトパターンの並び替え計画を返します。
142
183
  *
143
184
  * - プロパティー名の長さが短い順、同じ長さなら名前順
144
185
  * - 空行で区切られたグループ内だけで並び替え
145
186
  * - プロパティー直上のコメントはプロパティーと一緒に移動
187
+ * - 空行を挟んだコメントはグループ見出しとしてその場に残す
146
188
  *
147
189
  * 並び替えが安全でない場合は null を返します。
148
190
  */
@@ -159,7 +201,6 @@ function getSortPlan(
159
201
 
160
202
  const source = sourceCode.text;
161
203
  const lineStarts = getLineStarts(source);
162
- const lineIndent = getLineIndent(source, lineStarts, node.loc.start.line);
163
204
  const comments = sourceCode
164
205
  .getAllComments()
165
206
  .filter((comment) => comment.range[0] >= node.range[0] && comment.range[1] <= node.range[1]);
@@ -171,7 +212,6 @@ function getSortPlan(
171
212
 
172
213
  const inlineStart = firstProperty.loc.start.line === node.loc.start.line;
173
214
  const singleLine = node.loc.start.line === node.loc.end.line;
174
-
175
215
  const units: SortUnit[] = [];
176
216
 
177
217
  if (inlineStart) {
@@ -186,7 +226,9 @@ function getSortPlan(
186
226
  text: source.slice(property.range[0], property.range[1]),
187
227
  endLine: property.loc.end.line,
188
228
  property,
229
+ endOffset: property.range[1],
189
230
  startLine: property.loc.start.line,
231
+ startOffset: property.range[0],
190
232
  });
191
233
  }
192
234
  } else {
@@ -201,16 +243,15 @@ function getSortPlan(
201
243
 
202
244
  if (
203
245
  previous !== null &&
204
- comments.some(
205
- (comment) =>
206
- comment.loc.start.line === previous.loc.end.line &&
207
- comment.range[0] >= previous.range[1] &&
208
- comment.range[1] <= property.range[0],
209
- )
246
+ candidates.some((comment) => comment.loc.start.line === previous.loc.end.line)
210
247
  ) {
211
248
  return null;
212
249
  }
213
250
 
251
+ if (candidates.some((comment) => comment.loc.start.line === node.loc.start.line)) {
252
+ return null;
253
+ }
254
+
214
255
  const attached: Comment[] = [];
215
256
  let expectedLine = property.loc.start.line;
216
257
 
@@ -221,15 +262,15 @@ function getSortPlan(
221
262
  continue;
222
263
  }
223
264
 
224
- if (comment.loc.end.line !== expectedLine && comment.loc.end.line !== expectedLine - 1) {
265
+ if (comment.loc.end.line === expectedLine) {
225
266
  return null;
226
267
  }
227
268
 
228
- if (comment.loc.start.line === expectedLine) {
229
- return null;
269
+ if (comment.loc.end.line !== expectedLine - 1) {
270
+ break;
230
271
  }
231
272
 
232
- if (comment.loc.start.line === node.loc.start.line) {
273
+ if (comment.loc.start.line === expectedLine) {
233
274
  return null;
234
275
  }
235
276
 
@@ -237,20 +278,39 @@ function getSortPlan(
237
278
  expectedLine = comment.loc.start.line;
238
279
  }
239
280
 
240
- if (attached.length !== candidates.length) {
241
- return null;
242
- }
243
-
244
281
  const first = attached[0] ?? property;
245
282
  const startLine = first.loc.start.line;
246
- const unitStart = lineStarts[startLine - 1] ?? 0;
283
+ let startOffset: number;
284
+
285
+ if (attached.length > 0) {
286
+ startOffset = lineStarts[startLine - 1] ?? first.range[0];
287
+ } else if (startLine === previous?.loc.end.line) {
288
+ startOffset = property.range[0];
289
+ } else {
290
+ startOffset = lineStarts[startLine - 1] ?? property.range[0];
291
+ }
292
+
293
+ let endOffset = property.range[1];
294
+
295
+ while (endOffset > property.range[0]) {
296
+ const character = source[endOffset - 1];
297
+
298
+ if (character !== "," && character !== ";") {
299
+ break;
300
+ }
301
+
302
+ endOffset -= 1;
303
+ }
304
+
247
305
  const isRest = property.type === "RestElement";
248
306
  units.push({
249
307
  name: isRest ? null : getPropertyName(property),
250
- text: source.slice(unitStart, property.range[1]),
308
+ text: source.slice(startOffset, endOffset),
251
309
  endLine: property.loc.end.line,
252
310
  property,
311
+ endOffset,
253
312
  startLine,
313
+ startOffset,
254
314
  });
255
315
 
256
316
  previousProperty = property;
@@ -272,66 +332,80 @@ function getSortPlan(
272
332
  }
273
333
  }
274
334
 
275
- const groups: SortUnit[][] = [];
276
- let currentGroup: SortUnit[] = [];
277
- let previousEndLine: number | null = null;
335
+ if (inlineStart) {
336
+ const lineIndent = getLineIndent(source, lineStarts, node.loc.start.line);
337
+ const groups = splitGroups(units);
338
+ const needsFormat = forceMultiline;
339
+ const needsSort = groups.some((group) => {
340
+ const sorted = sortGroup(group);
341
+
342
+ return sorted.some((unit, index) => unit !== group[index]);
343
+ });
344
+
345
+ if (!needsSort && !needsFormat) {
346
+ return {
347
+ edits: null,
348
+ needsFix: false,
349
+ replacement: null,
350
+ };
351
+ }
278
352
 
279
- for (const unit of units) {
280
- if (previousEndLine !== null && unit.startLine > previousEndLine + 1) {
281
- groups.push(currentGroup);
282
- currentGroup = [];
353
+ if (singleLine && !forceMultiline) {
354
+ const sortedUnits = sortGroup(units);
355
+
356
+ return {
357
+ edits: null,
358
+ needsFix: true,
359
+ replacement: `{ ${sortedUnits.map((unit) => unit.text).join(", ")} }`,
360
+ };
283
361
  }
284
362
 
285
- currentGroup.push(unit);
286
- previousEndLine = unit.endLine;
287
- }
363
+ const groupsText = groups.map((group) => getEditText(sortGroup(group))).join(",\n\n");
364
+ const lastUnit = groups.flatMap((group) => sortGroup(group)).at(-1);
365
+ const trailingComma = lastUnit?.property.type === "RestElement" ? "" : ",";
288
366
 
289
- if (currentGroup.length > 0) {
290
- groups.push(currentGroup);
367
+ return {
368
+ edits: null,
369
+ needsFix: true,
370
+ replacement: `{\n${groupsText}${trailingComma}\n${lineIndent}}`,
371
+ };
291
372
  }
292
373
 
293
- let needsSort = false;
374
+ const edits: SortEdit[] = [];
294
375
 
295
- for (const group of groups) {
376
+ for (const group of splitGroups(units)) {
296
377
  const sorted = sortGroup(group);
297
378
 
298
- if (sorted.some((unit, index) => unit !== group[index])) {
299
- needsSort = true;
300
- break;
379
+ if (sorted.every((unit, index) => unit === group[index])) {
380
+ continue;
301
381
  }
302
- }
303
382
 
304
- const needsFormat = forceMultiline && inlineStart;
383
+ const first = group[0];
384
+ const last = group.at(-1);
305
385
 
306
- if (!needsSort && !needsFormat) {
307
- return {
308
- needsFix: false,
309
- replacement: null,
310
- };
311
- }
386
+ if (first === undefined || last === undefined) {
387
+ continue;
388
+ }
312
389
 
313
- if (singleLine && !forceMultiline) {
314
- const sortedUnits = sortGroup(units);
390
+ edits.push({
391
+ end: last.endOffset,
392
+ text: getEditText(sorted),
393
+ start: first.startOffset,
394
+ });
395
+ }
315
396
 
397
+ if (edits.length === 0) {
316
398
  return {
317
- needsFix: true,
318
- replacement: `{ ${sortedUnits.map((unit) => unit.text).join(", ")} }`,
399
+ edits: null,
400
+ needsFix: false,
401
+ replacement: null,
319
402
  };
320
403
  }
321
404
 
322
- const groupsText = groups
323
- .map((group) =>
324
- sortGroup(group)
325
- .map((unit) => unit.text)
326
- .join(",\n"),
327
- )
328
- .join(",\n\n");
329
- const lastUnit = groups.flatMap((group) => sortGroup(group)).at(-1);
330
- const trailingComma = lastUnit?.property.type === "RestElement" ? "" : ",";
331
-
332
405
  return {
406
+ edits,
333
407
  needsFix: true,
334
- replacement: `{\n${groupsText}${trailingComma}\n${lineIndent}}`,
408
+ replacement: null,
335
409
  };
336
410
  }
337
411
 
@@ -6,7 +6,7 @@ import { getSortPlan } from "./object-sort-utils.js";
6
6
  * オブジェクトリテラルのプロパティーを、名前の長さが短い順、同じ長さなら名前順に並べます。
7
7
  *
8
8
  * 空行で区切られたグループは意図的なまとまりとみなし、グループ内だけで並べ替えます。
9
- * プロパティー直上のコメントはプロパティーと一緒に移動します。
9
+ * プロパティー直上のコメントはプロパティーと一緒に移動し、空行を挟んだコメントはグループ見出しとして残します。
10
10
  */
11
11
  const sortObjectProperties = defineRule({
12
12
  meta: {
@@ -34,6 +34,12 @@ const sortObjectProperties = defineRule({
34
34
 
35
35
  context.report({
36
36
  fix(fixer) {
37
+ if (plan.edits !== null) {
38
+ return plan.edits.map((edit) =>
39
+ fixer.replaceTextRange([edit.start, edit.end], edit.text),
40
+ );
41
+ }
42
+
37
43
  return fixer.replaceText(node, plan.replacement);
38
44
  },
39
45
  node,
@@ -49,6 +55,12 @@ const sortObjectProperties = defineRule({
49
55
 
50
56
  context.report({
51
57
  fix(fixer) {
58
+ if (plan.edits !== null) {
59
+ return plan.edits.map((edit) =>
60
+ fixer.replaceTextRange([edit.start, edit.end], edit.text),
61
+ );
62
+ }
63
+
52
64
  return fixer.replaceText(node, plan.replacement);
53
65
  },
54
66
  node,