@tai-kun/vp-lint-plugins 0.0.1 → 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.
Files changed (29) hide show
  1. package/dist/src/rules/blank-line-after-variable-declaration.d.ts +1 -0
  2. package/dist/src/rules/blank-line-after-variable-declaration.d.ts.map +1 -1
  3. package/dist/src/rules/blank-line-after-variable-declaration.js +68 -12
  4. package/dist/src/rules/blank-line.d.ts +5 -1
  5. package/dist/src/rules/blank-line.d.ts.map +1 -1
  6. package/dist/src/rules/blank-line.js +19 -1
  7. package/dist/src/rules/comment-one-sentence-per-line.d.ts.map +1 -1
  8. package/dist/src/rules/jsdoc-utils.d.ts.map +1 -1
  9. package/dist/src/rules/object-sort-utils.d.ts +12 -0
  10. package/dist/src/rules/object-sort-utils.d.ts.map +1 -1
  11. package/dist/src/rules/object-sort-utils.js +100 -47
  12. package/dist/src/rules/overload-signatures-adjacent.d.ts.map +1 -1
  13. package/dist/src/rules/overload-signatures-adjacent.js +2 -17
  14. package/dist/src/rules/sort-jsx-props.d.ts.map +1 -1
  15. package/dist/src/rules/sort-object-properties.d.ts +1 -1
  16. package/dist/src/rules/sort-object-properties.d.ts.map +1 -1
  17. package/dist/src/rules/sort-object-properties.js +7 -1
  18. package/dist/src/rules/spaced-comment.d.ts.map +1 -1
  19. package/package.json +1 -1
  20. package/src/rules/blank-line-after-variable-declaration.ts +93 -12
  21. package/src/rules/blank-line.ts +30 -0
  22. package/src/rules/comment-one-sentence-per-line.ts +1 -2
  23. package/src/rules/jsdoc-utils.ts +1 -2
  24. package/src/rules/object-sort-utils.ts +134 -61
  25. package/src/rules/overload-signatures-adjacent.ts +3 -29
  26. package/src/rules/sort-jsx-props.ts +0 -1
  27. package/src/rules/sort-object-properties.ts +13 -1
  28. package/src/rules/sort-type-members.ts +1 -1
  29. package/src/rules/spaced-comment.ts +0 -2
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * 連続する変数宣言の後に別の種類の文が続く場合、空行を 1 行以上確保します。
3
3
  *
4
+ * 宣言した変数を使う式文は同じ宣言グループとして扱い、グループ内の空行は削除します。
4
5
  * return の直前は blank-line-before-return が担当するため対象外にします。
5
6
  */
6
7
  declare const blankLineAfterVariableDeclaration: import("vite-plus/lint/plugins").Rule;
@@ -1 +1 @@
1
- {"version":3,"file":"blank-line-after-variable-declaration.d.ts","sourceRoot":"","sources":["../../../src/rules/blank-line-after-variable-declaration.ts"],"names":[],"mappings":"AAkBA;;;;GAIG;AACH,QAAA,MAAM,iCAAiC,uCA0DrC,CAAC;eAEY,iCAAiC"}
1
+ {"version":3,"file":"blank-line-after-variable-declaration.d.ts","sourceRoot":"","sources":["../../../src/rules/blank-line-after-variable-declaration.ts"],"names":[],"mappings":"AA8CA;;;;;GAKG;AACH,QAAA,MAAM,iCAAiC,uCA8GrC,CAAC;eAEY,iCAAiC"}
@@ -1,49 +1,105 @@
1
1
  import { defineRule } from "vite-plus/lint/plugins";
2
- import { hasBlankLine, getBlankLineFix, getNextStatement, getInsertionTarget, } from "./blank-line.js";
3
- function isVariableDeclarationStatement(node) {
2
+ import { hasBlankLine, getBlankLineFix, getNextStatement, getInsertionTarget, getPreviousStatement, getRemoveBlankLineFix, } from "./blank-line.js";
3
+ function getVariableDeclaration(node) {
4
4
  if (node.type === "VariableDeclaration") {
5
- return true;
5
+ return node;
6
6
  }
7
- return node.type === "ExportNamedDeclaration" && node.declaration?.type === "VariableDeclaration";
7
+ if (node.type === "ExportNamedDeclaration" && node.declaration?.type === "VariableDeclaration") {
8
+ return node.declaration;
9
+ }
10
+ return null;
11
+ }
12
+ function isRelatedExpression(statement, variables) {
13
+ if (statement.type !== "ExpressionStatement") {
14
+ return false;
15
+ }
16
+ for (const variable of variables) {
17
+ for (const reference of variable.references) {
18
+ if (reference.identifier.range[0] >= statement.range[0] &&
19
+ reference.identifier.range[1] <= statement.range[1]) {
20
+ return true;
21
+ }
22
+ }
23
+ }
24
+ return false;
8
25
  }
9
26
  /**
10
27
  * 連続する変数宣言の後に別の種類の文が続く場合、空行を 1 行以上確保します。
11
28
  *
29
+ * 宣言した変数を使う式文は同じ宣言グループとして扱い、グループ内の空行は削除します。
12
30
  * return の直前は blank-line-before-return が担当するため対象外にします。
13
31
  */
14
32
  const blankLineAfterVariableDeclaration = defineRule({
15
33
  meta: {
16
34
  docs: {
17
- description: "Require a blank line after a variable declaration group when another statement follows.",
35
+ description: "Require a blank line after a variable declaration group and remove blank lines before related statements.",
18
36
  },
19
37
  type: "layout",
20
38
  fixable: "whitespace",
21
39
  messages: {
22
40
  blankLine: "Expected a blank line after a variable declaration group when another statement follows.",
41
+ unexpectedBlankLine: "Expected no blank line between a variable declaration and its related statement.",
23
42
  },
24
43
  },
25
44
  create(context) {
26
45
  const sourceCode = context.sourceCode;
27
46
  function checkStatement(node) {
47
+ const variableDeclaration = getVariableDeclaration(node);
48
+ if (variableDeclaration === null) {
49
+ return;
50
+ }
28
51
  if (node.type === "VariableDeclaration" && node.parent.type === "ExportNamedDeclaration") {
29
52
  return;
30
53
  }
31
- const nextStatement = getNextStatement(node);
54
+ const previousStatement = getPreviousStatement(node);
55
+ if (previousStatement !== null && getVariableDeclaration(previousStatement) !== null) {
56
+ return;
57
+ }
58
+ const variables = new Set(sourceCode.getDeclaredVariables(variableDeclaration));
59
+ let last = node;
60
+ let nextStatement = getNextStatement(last);
61
+ while (nextStatement !== null) {
62
+ const nextVariableDeclaration = getVariableDeclaration(nextStatement);
63
+ if (nextVariableDeclaration !== null && getVariableDeclaration(last) !== null) {
64
+ for (const variable of sourceCode.getDeclaredVariables(nextVariableDeclaration)) {
65
+ variables.add(variable);
66
+ }
67
+ last = nextStatement;
68
+ nextStatement = getNextStatement(last);
69
+ continue;
70
+ }
71
+ if (nextStatement.type === "ExpressionStatement" &&
72
+ isRelatedExpression(nextStatement, variables)) {
73
+ const target = getInsertionTarget(last, nextStatement, sourceCode);
74
+ if (hasBlankLine(last, target)) {
75
+ context.report({
76
+ fix(fixer) {
77
+ return getRemoveBlankLineFix(fixer, sourceCode, last, target);
78
+ },
79
+ node: nextStatement,
80
+ messageId: "unexpectedBlankLine",
81
+ });
82
+ }
83
+ last = nextStatement;
84
+ nextStatement = getNextStatement(last);
85
+ continue;
86
+ }
87
+ break;
88
+ }
32
89
  if (nextStatement === null ||
33
90
  nextStatement.type === "EmptyStatement" ||
34
- nextStatement.type === "ReturnStatement" ||
35
- isVariableDeclarationStatement(nextStatement)) {
91
+ nextStatement.type === "ReturnStatement") {
36
92
  return;
37
93
  }
38
- const target = getInsertionTarget(node, nextStatement, sourceCode);
39
- if (hasBlankLine(node, target)) {
94
+ const target = getInsertionTarget(last, nextStatement, sourceCode);
95
+ if (hasBlankLine(last, target)) {
40
96
  return;
41
97
  }
42
98
  context.report({
43
99
  fix(fixer) {
44
- return getBlankLineFix(fixer, sourceCode, node, target);
100
+ return getBlankLineFix(fixer, sourceCode, last, target);
45
101
  },
46
- node,
102
+ node: nextStatement,
47
103
  messageId: "blankLine",
48
104
  });
49
105
  }
@@ -10,5 +10,9 @@ declare function getPreviousStatement(node: ESTree.Node): ESTree.Node | null;
10
10
  declare function getInsertionTarget(node: ESTree.Node, nextStatement: ESTree.Node, sourceCode: SourceCode): ESTree.Node | Comment;
11
11
  declare function hasBlankLine(node: ESTree.Node, target: ESTree.Node | Comment): boolean;
12
12
  declare function getBlankLineFix(fixer: Fixer, sourceCode: SourceCode, node: ESTree.Node, target: ESTree.Node | Comment): Fix | null;
13
- export { hasBlankLine, getBlankLineFix, getNextStatement, getInsertionTarget, getPreviousStatement, };
13
+ /**
14
+ * 指定したノードと対象の間の空行を削除します。
15
+ */
16
+ declare function getRemoveBlankLineFix(fixer: Fixer, sourceCode: SourceCode, node: ESTree.Node, target: ESTree.Node | Comment): Fix | null;
17
+ export { hasBlankLine, getBlankLineFix, getNextStatement, getInsertionTarget, getPreviousStatement, getRemoveBlankLineFix, };
14
18
  //# sourceMappingURL=blank-line.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"blank-line.d.ts","sourceRoot":"","sources":["../../../src/rules/blank-line.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AA4BtF,iBAAS,gBAAgB,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,GAAG,MAAM,CAAC,IAAI,GAAG,IAAI,CAc/D;AAED,iBAAS,oBAAoB,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,GAAG,MAAM,CAAC,IAAI,GAAG,IAAI,CAcnE;AAED;;;;;GAKG;AACH,iBAAS,kBAAkB,CACzB,IAAI,EAAE,MAAM,CAAC,IAAI,EACjB,aAAa,EAAE,MAAM,CAAC,IAAI,EAC1B,UAAU,EAAE,UAAU,GACrB,MAAM,CAAC,IAAI,GAAG,OAAO,CAWvB;AAED,iBAAS,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,IAAI,GAAG,OAAO,GAAG,OAAO,CAE/E;AAED,iBAAS,eAAe,CACtB,KAAK,EAAE,KAAK,EACZ,UAAU,EAAE,UAAU,EACtB,IAAI,EAAE,MAAM,CAAC,IAAI,EACjB,MAAM,EAAE,MAAM,CAAC,IAAI,GAAG,OAAO,GAC5B,GAAG,GAAG,IAAI,CAYZ;AAED,OAAO,EACL,YAAY,EACZ,eAAe,EACf,gBAAgB,EAChB,kBAAkB,EAClB,oBAAoB,GACrB,CAAC"}
1
+ {"version":3,"file":"blank-line.d.ts","sourceRoot":"","sources":["../../../src/rules/blank-line.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AA4BtF,iBAAS,gBAAgB,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,GAAG,MAAM,CAAC,IAAI,GAAG,IAAI,CAc/D;AAED,iBAAS,oBAAoB,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,GAAG,MAAM,CAAC,IAAI,GAAG,IAAI,CAcnE;AAED;;;;;GAKG;AACH,iBAAS,kBAAkB,CACzB,IAAI,EAAE,MAAM,CAAC,IAAI,EACjB,aAAa,EAAE,MAAM,CAAC,IAAI,EAC1B,UAAU,EAAE,UAAU,GACrB,MAAM,CAAC,IAAI,GAAG,OAAO,CAWvB;AAED,iBAAS,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,IAAI,GAAG,OAAO,GAAG,OAAO,CAE/E;AAED,iBAAS,eAAe,CACtB,KAAK,EAAE,KAAK,EACZ,UAAU,EAAE,UAAU,EACtB,IAAI,EAAE,MAAM,CAAC,IAAI,EACjB,MAAM,EAAE,MAAM,CAAC,IAAI,GAAG,OAAO,GAC5B,GAAG,GAAG,IAAI,CAYZ;AAED;;GAEG;AACH,iBAAS,qBAAqB,CAC5B,KAAK,EAAE,KAAK,EACZ,UAAU,EAAE,UAAU,EACtB,IAAI,EAAE,MAAM,CAAC,IAAI,EACjB,MAAM,EAAE,MAAM,CAAC,IAAI,GAAG,OAAO,GAC5B,GAAG,GAAG,IAAI,CAmBZ;AAED,OAAO,EACL,YAAY,EACZ,eAAe,EACf,gBAAgB,EAChB,kBAAkB,EAClB,oBAAoB,EACpB,qBAAqB,GACtB,CAAC"}
@@ -65,4 +65,22 @@ function getBlankLineFix(fixer, sourceCode, node, target) {
65
65
  }
66
66
  return fixer.insertTextBeforeRange([lineEnd, lineEnd + 1], "\n");
67
67
  }
68
- export { hasBlankLine, getBlankLineFix, getNextStatement, getInsertionTarget, getPreviousStatement, };
68
+ /**
69
+ * 指定したノードと対象の間の空行を削除します。
70
+ */
71
+ function getRemoveBlankLineFix(fixer, sourceCode, node, target) {
72
+ if (target.loc.start.line <= node.loc.end.line + 1) {
73
+ return null;
74
+ }
75
+ const fromLineEnd = sourceCode.text.indexOf("\n", node.range[1]);
76
+ if (fromLineEnd === -1) {
77
+ return null;
78
+ }
79
+ const targetLineStart = sourceCode.text.lastIndexOf("\n", target.range[0]) + 1;
80
+ const indent = sourceCode.text.slice(targetLineStart, target.range[0]);
81
+ if (indent.trim() !== "") {
82
+ return null;
83
+ }
84
+ return fixer.replaceTextRange([fromLineEnd, target.range[0]], `\n${indent}`);
85
+ }
86
+ export { hasBlankLine, getBlankLineFix, getNextStatement, getInsertionTarget, getPreviousStatement, getRemoveBlankLineFix, };
@@ -1 +1 @@
1
- {"version":3,"file":"comment-one-sentence-per-line.d.ts","sourceRoot":"","sources":["../../../src/rules/comment-one-sentence-per-line.ts"],"names":[],"mappings":"AAiKA;;;;;GAKG;AACH,QAAA,MAAM,yBAAyB,uCA8F7B,CAAC;eAEY,yBAAyB"}
1
+ {"version":3,"file":"comment-one-sentence-per-line.d.ts","sourceRoot":"","sources":["../../../src/rules/comment-one-sentence-per-line.ts"],"names":[],"mappings":"AAiKA;;;;;GAKG;AACH,QAAA,MAAM,yBAAyB,uCA6F7B,CAAC;eAEY,yBAAyB"}
@@ -1 +1 @@
1
- {"version":3,"file":"jsdoc-utils.d.ts","sourceRoot":"","sources":["../../../src/rules/jsdoc-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAI1E,UAAU,SAAS;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED,iBAAS,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAI/C;AAyBD;;GAEG;AACH,iBAAS,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAqBpD;AAED;;;;GAIG;AACH,iBAAS,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,UAAU,GAAG,SAAS,EAAE,GAAG,IAAI,CA+DnF;AAED,iBAAS,iBAAiB,CAAC,KAAK,EAAE,SAAS,SAAS,EAAE,GAAG,OAAO,CAE/D;AAcD;;GAEG;AACH,iBAAS,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,UAAU,EAAE,UAAU,GAAG,OAAO,GAAG,IAAI,CAmB/E;AAED,YAAY,EAAE,SAAS,EAAE,CAAC;AAE1B,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,iBAAiB,EAAE,CAAC"}
1
+ {"version":3,"file":"jsdoc-utils.d.ts","sourceRoot":"","sources":["../../../src/rules/jsdoc-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAI1E,UAAU,SAAS;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED,iBAAS,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAI/C;AAyBD;;GAEG;AACH,iBAAS,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAqBpD;AAED;;;;GAIG;AACH,iBAAS,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,UAAU,GAAG,SAAS,EAAE,GAAG,IAAI,CA8DnF;AAED,iBAAS,iBAAiB,CAAC,KAAK,EAAE,SAAS,SAAS,EAAE,GAAG,OAAO,CAE/D;AAcD;;GAEG;AACH,iBAAS,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,UAAU,EAAE,UAAU,GAAG,OAAO,GAAG,IAAI,CAmB/E;AAED,YAAY,EAAE,SAAS,EAAE,CAAC;AAE1B,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,iBAAiB,EAAE,CAAC"}
@@ -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,CAwLjB;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 };
@@ -1 +1 @@
1
- {"version":3,"file":"overload-signatures-adjacent.d.ts","sourceRoot":"","sources":["../../../src/rules/overload-signatures-adjacent.ts"],"names":[],"mappings":"AAiGA;;GAEG;AACH,QAAA,MAAM,0BAA0B,uCAqG9B,CAAC;eAEY,0BAA0B"}
1
+ {"version":3,"file":"overload-signatures-adjacent.d.ts","sourceRoot":"","sources":["../../../src/rules/overload-signatures-adjacent.ts"],"names":[],"mappings":"AAuEA;;GAEG;AACH,QAAA,MAAM,0BAA0B,uCAqG9B,CAAC;eAEY,0BAA0B"}
@@ -1,5 +1,5 @@
1
1
  import { defineRule } from "vite-plus/lint/plugins";
2
- import { getNextStatement } from "./blank-line.js";
2
+ import { getNextStatement, getRemoveBlankLineFix } from "./blank-line.js";
3
3
  function getFunction(node) {
4
4
  if (node.type === "FunctionDeclaration" || node.type === "TSDeclareFunction") {
5
5
  return node;
@@ -25,21 +25,6 @@ function getKeyName(node) {
25
25
  }
26
26
  return null;
27
27
  }
28
- function getRemoveBlankLinesFix(fixer, sourceCode, previous, next) {
29
- if (next.loc.start.line <= previous.loc.end.line + 1) {
30
- return null;
31
- }
32
- const fromLineEnd = sourceCode.text.indexOf("\n", previous.range[1]);
33
- if (fromLineEnd === -1) {
34
- return null;
35
- }
36
- const nextLineStart = sourceCode.text.lastIndexOf("\n", next.range[0]) + 1;
37
- const indent = sourceCode.text.slice(nextLineStart, next.range[0]);
38
- if (indent.trim() !== "") {
39
- return null;
40
- }
41
- return fixer.replaceTextRange([fromLineEnd, next.range[0]], `\n${indent}`);
42
- }
43
28
  function hasCommentBetween(sourceCode, previous, next) {
44
29
  return sourceCode
45
30
  .getAllComments()
@@ -52,7 +37,7 @@ function reportIfSeparated(context, sourceCode, previous, next) {
52
37
  }
53
38
  context.report({
54
39
  fix(fixer) {
55
- return getRemoveBlankLinesFix(fixer, sourceCode, previous, next);
40
+ return getRemoveBlankLineFix(fixer, sourceCode, previous, next);
56
41
  },
57
42
  node: next,
58
43
  messageId: "adjacent",
@@ -1 +1 @@
1
- {"version":3,"file":"sort-jsx-props.d.ts","sourceRoot":"","sources":["../../../src/rules/sort-jsx-props.ts"],"names":[],"mappings":"AAsEA;;;GAGG;AACH,QAAA,MAAM,YAAY,uCAwEhB,CAAC;eAEY,YAAY"}
1
+ {"version":3,"file":"sort-jsx-props.d.ts","sourceRoot":"","sources":["../../../src/rules/sort-jsx-props.ts"],"names":[],"mappings":"AAsEA;;;GAGG;AACH,QAAA,MAAM,YAAY,uCAuEhB,CAAC;eAEY,YAAY"}
@@ -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,
@@ -1 +1 @@
1
- {"version":3,"file":"spaced-comment.d.ts","sourceRoot":"","sources":["../../../src/rules/spaced-comment.ts"],"names":[],"mappings":"AAyBA;;GAEG;AACH,QAAA,MAAM,aAAa,uCAkGjB,CAAC;eAEY,aAAa"}
1
+ {"version":3,"file":"spaced-comment.d.ts","sourceRoot":"","sources":["../../../src/rules/spaced-comment.ts"],"names":[],"mappings":"AAyBA;;GAEG;AACH,QAAA,MAAM,aAAa,uCAgGjB,CAAC;eAEY,aAAa"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tai-kun/vp-lint-plugins",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "",
5
5
  "homepage": "https://tai-kun.github.io/vp-lint-plugins/",
6
6
  "license": "MIT",
@@ -1,69 +1,150 @@
1
1
  import { defineRule } from "vite-plus/lint/plugins";
2
- import type { ESTree } from "vite-plus/lint/plugins";
2
+ import type { ESTree, Variable } from "vite-plus/lint/plugins";
3
3
 
4
4
  import {
5
5
  hasBlankLine,
6
6
  getBlankLineFix,
7
7
  getNextStatement,
8
8
  getInsertionTarget,
9
+ getPreviousStatement,
10
+ getRemoveBlankLineFix,
9
11
  } from "./blank-line.js";
10
12
 
11
- function isVariableDeclarationStatement(node: ESTree.Node): boolean {
13
+ function getVariableDeclaration(node: ESTree.Node): ESTree.VariableDeclaration | null {
12
14
  if (node.type === "VariableDeclaration") {
13
- return true;
15
+ return node;
14
16
  }
15
17
 
16
- return node.type === "ExportNamedDeclaration" && node.declaration?.type === "VariableDeclaration";
18
+ if (node.type === "ExportNamedDeclaration" && node.declaration?.type === "VariableDeclaration") {
19
+ return node.declaration;
20
+ }
21
+
22
+ return null;
23
+ }
24
+
25
+ function isRelatedExpression(
26
+ statement: ESTree.Statement,
27
+ variables: ReadonlySet<Variable>,
28
+ ): boolean {
29
+ if (statement.type !== "ExpressionStatement") {
30
+ return false;
31
+ }
32
+
33
+ for (const variable of variables) {
34
+ for (const reference of variable.references) {
35
+ if (
36
+ reference.identifier.range[0] >= statement.range[0] &&
37
+ reference.identifier.range[1] <= statement.range[1]
38
+ ) {
39
+ return true;
40
+ }
41
+ }
42
+ }
43
+
44
+ return false;
17
45
  }
18
46
 
19
47
  /**
20
48
  * 連続する変数宣言の後に別の種類の文が続く場合、空行を 1 行以上確保します。
21
49
  *
50
+ * 宣言した変数を使う式文は同じ宣言グループとして扱い、グループ内の空行は削除します。
22
51
  * return の直前は blank-line-before-return が担当するため対象外にします。
23
52
  */
24
53
  const blankLineAfterVariableDeclaration = defineRule({
25
54
  meta: {
26
55
  docs: {
27
56
  description:
28
- "Require a blank line after a variable declaration group when another statement follows.",
57
+ "Require a blank line after a variable declaration group and remove blank lines before related statements.",
29
58
  },
30
59
  type: "layout",
31
60
  fixable: "whitespace",
32
61
  messages: {
33
62
  blankLine:
34
63
  "Expected a blank line after a variable declaration group when another statement follows.",
64
+ unexpectedBlankLine:
65
+ "Expected no blank line between a variable declaration and its related statement.",
35
66
  },
36
67
  },
37
68
  create(context) {
38
69
  const sourceCode = context.sourceCode;
39
70
 
40
71
  function checkStatement(node: ESTree.Node): void {
72
+ const variableDeclaration = getVariableDeclaration(node);
73
+
74
+ if (variableDeclaration === null) {
75
+ return;
76
+ }
77
+
41
78
  if (node.type === "VariableDeclaration" && node.parent.type === "ExportNamedDeclaration") {
42
79
  return;
43
80
  }
44
81
 
45
- const nextStatement = getNextStatement(node);
82
+ const previousStatement = getPreviousStatement(node);
83
+
84
+ if (previousStatement !== null && getVariableDeclaration(previousStatement) !== null) {
85
+ return;
86
+ }
87
+
88
+ const variables = new Set<Variable>(sourceCode.getDeclaredVariables(variableDeclaration));
89
+ let last = node;
90
+ let nextStatement = getNextStatement(last);
91
+
92
+ while (nextStatement !== null) {
93
+ const nextVariableDeclaration = getVariableDeclaration(nextStatement);
94
+
95
+ if (nextVariableDeclaration !== null && getVariableDeclaration(last) !== null) {
96
+ for (const variable of sourceCode.getDeclaredVariables(nextVariableDeclaration)) {
97
+ variables.add(variable);
98
+ }
99
+
100
+ last = nextStatement;
101
+ nextStatement = getNextStatement(last);
102
+ continue;
103
+ }
104
+
105
+ if (
106
+ nextStatement.type === "ExpressionStatement" &&
107
+ isRelatedExpression(nextStatement, variables)
108
+ ) {
109
+ const target = getInsertionTarget(last, nextStatement, sourceCode);
110
+
111
+ if (hasBlankLine(last, target)) {
112
+ context.report({
113
+ fix(fixer) {
114
+ return getRemoveBlankLineFix(fixer, sourceCode, last, target);
115
+ },
116
+ node: nextStatement,
117
+ messageId: "unexpectedBlankLine",
118
+ });
119
+ }
120
+
121
+ last = nextStatement;
122
+ nextStatement = getNextStatement(last);
123
+ continue;
124
+ }
125
+
126
+ break;
127
+ }
46
128
 
47
129
  if (
48
130
  nextStatement === null ||
49
131
  nextStatement.type === "EmptyStatement" ||
50
- nextStatement.type === "ReturnStatement" ||
51
- isVariableDeclarationStatement(nextStatement)
132
+ nextStatement.type === "ReturnStatement"
52
133
  ) {
53
134
  return;
54
135
  }
55
136
 
56
- const target = getInsertionTarget(node, nextStatement, sourceCode);
137
+ const target = getInsertionTarget(last, nextStatement, sourceCode);
57
138
 
58
- if (hasBlankLine(node, target)) {
139
+ if (hasBlankLine(last, target)) {
59
140
  return;
60
141
  }
61
142
 
62
143
  context.report({
63
144
  fix(fixer) {
64
- return getBlankLineFix(fixer, sourceCode, node, target);
145
+ return getBlankLineFix(fixer, sourceCode, last, target);
65
146
  },
66
- node,
147
+ node: nextStatement,
67
148
  messageId: "blankLine",
68
149
  });
69
150
  }
@@ -104,10 +104,40 @@ function getBlankLineFix(
104
104
  return fixer.insertTextBeforeRange([lineEnd, lineEnd + 1], "\n");
105
105
  }
106
106
 
107
+ /**
108
+ * 指定したノードと対象の間の空行を削除します。
109
+ */
110
+ function getRemoveBlankLineFix(
111
+ fixer: Fixer,
112
+ sourceCode: SourceCode,
113
+ node: ESTree.Node,
114
+ target: ESTree.Node | Comment,
115
+ ): Fix | null {
116
+ if (target.loc.start.line <= node.loc.end.line + 1) {
117
+ return null;
118
+ }
119
+
120
+ const fromLineEnd = sourceCode.text.indexOf("\n", node.range[1]);
121
+
122
+ if (fromLineEnd === -1) {
123
+ return null;
124
+ }
125
+
126
+ const targetLineStart = sourceCode.text.lastIndexOf("\n", target.range[0]) + 1;
127
+ const indent = sourceCode.text.slice(targetLineStart, target.range[0]);
128
+
129
+ if (indent.trim() !== "") {
130
+ return null;
131
+ }
132
+
133
+ return fixer.replaceTextRange([fromLineEnd, target.range[0]], `\n${indent}`);
134
+ }
135
+
107
136
  export {
108
137
  hasBlankLine,
109
138
  getBlankLineFix,
110
139
  getNextStatement,
111
140
  getInsertionTarget,
112
141
  getPreviousStatement,
142
+ getRemoveBlankLineFix,
113
143
  };
@@ -129,7 +129,6 @@ function collectDiagnostics(
129
129
  if (next !== undefined && next !== null && canContinue(content, next)) {
130
130
  const nextLeadingSpace = /^\s*/u.exec(next.content)?.[0] ?? "";
131
131
  const separator = nextLeadingSpace === "" && needsSpace(content, next.content) ? " " : "";
132
-
133
132
  fixes.push({
134
133
  end: next.start,
135
134
  loc: {
@@ -145,6 +144,7 @@ function collectDiagnostics(
145
144
  text: separator,
146
145
  start: info.end,
147
146
  });
147
+
148
148
  continue;
149
149
  }
150
150
 
@@ -205,7 +205,6 @@ const commentOneSentencePerLine = defineRule({
205
205
 
206
206
  for (const info of diagnostics.missingPunctuation) {
207
207
  const column = info.column + info.content.trimEnd().length;
208
-
209
208
  context.report({
210
209
  loc: {
211
210
  end: {
@@ -101,13 +101,13 @@ function getJSDocLines(comment: Comment, sourceCode: SourceCode): JSDocLine[] |
101
101
  const closeIndex = line.lastIndexOf("*/");
102
102
  const contentEnd = closeIndex === -1 ? line.length : Math.max(contentStart, closeIndex);
103
103
  const content = line.slice(contentStart, contentEnd);
104
-
105
104
  lines.push({
106
105
  raw: line,
107
106
  prefix: line.slice(0, contentStart),
108
107
  content,
109
108
  tagName: getTagName(content),
110
109
  });
110
+
111
111
  continue;
112
112
  }
113
113
 
@@ -122,7 +122,6 @@ function getJSDocLines(comment: Comment, sourceCode: SourceCode): JSDocLine[] |
122
122
  const closeIndex = line.lastIndexOf("*/");
123
123
  const contentEnd = closeIndex === -1 ? line.length : Math.max(contentStart, closeIndex);
124
124
  const content = line.slice(contentStart, contentEnd);
125
-
126
125
  lines.push({
127
126
  raw: line,
128
127
  prefix,
@@ -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[] {
@@ -130,19 +145,46 @@ function compareUnits(left: NamedSortUnit, right: NamedSortUnit): number {
130
145
 
131
146
  function sortGroup(group: readonly SortUnit[]): SortUnit[] {
132
147
  const sortable = group.filter((unit): unit is NamedSortUnit => unit.name !== null);
133
-
134
148
  sortable.sort(compareUnits);
149
+
135
150
  const rest = group.filter((unit) => unit.name === null);
136
151
 
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) {
@@ -181,13 +221,14 @@ function getSortPlan(
181
221
 
182
222
  for (const property of node.properties) {
183
223
  const isRest = property.type === "RestElement";
184
-
185
224
  units.push({
186
225
  name: isRest ? null : getPropertyName(property),
187
226
  text: source.slice(property.range[0], property.range[1]),
188
227
  endLine: property.loc.end.line,
189
228
  property,
229
+ endOffset: property.range[1],
190
230
  startLine: property.loc.start.line,
231
+ startOffset: property.range[0],
191
232
  });
192
233
  }
193
234
  } else {
@@ -202,16 +243,15 @@ function getSortPlan(
202
243
 
203
244
  if (
204
245
  previous !== null &&
205
- comments.some(
206
- (comment) =>
207
- comment.loc.start.line === previous.loc.end.line &&
208
- comment.range[0] >= previous.range[1] &&
209
- comment.range[1] <= property.range[0],
210
- )
246
+ candidates.some((comment) => comment.loc.start.line === previous.loc.end.line)
211
247
  ) {
212
248
  return null;
213
249
  }
214
250
 
251
+ if (candidates.some((comment) => comment.loc.start.line === node.loc.start.line)) {
252
+ return null;
253
+ }
254
+
215
255
  const attached: Comment[] = [];
216
256
  let expectedLine = property.loc.start.line;
217
257
 
@@ -222,15 +262,15 @@ function getSortPlan(
222
262
  continue;
223
263
  }
224
264
 
225
- if (comment.loc.end.line !== expectedLine && comment.loc.end.line !== expectedLine - 1) {
265
+ if (comment.loc.end.line === expectedLine) {
226
266
  return null;
227
267
  }
228
268
 
229
- if (comment.loc.start.line === expectedLine) {
230
- return null;
269
+ if (comment.loc.end.line !== expectedLine - 1) {
270
+ break;
231
271
  }
232
272
 
233
- if (comment.loc.start.line === node.loc.start.line) {
273
+ if (comment.loc.start.line === expectedLine) {
234
274
  return null;
235
275
  }
236
276
 
@@ -238,22 +278,41 @@ function getSortPlan(
238
278
  expectedLine = comment.loc.start.line;
239
279
  }
240
280
 
241
- if (attached.length !== candidates.length) {
242
- return null;
243
- }
244
-
245
281
  const first = attached[0] ?? property;
246
282
  const startLine = first.loc.start.line;
247
- const unitStart = lineStarts[startLine - 1] ?? 0;
248
- const isRest = property.type === "RestElement";
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
+ }
249
301
 
302
+ endOffset -= 1;
303
+ }
304
+
305
+ const isRest = property.type === "RestElement";
250
306
  units.push({
251
307
  name: isRest ? null : getPropertyName(property),
252
- text: source.slice(unitStart, property.range[1]),
308
+ text: source.slice(startOffset, endOffset),
253
309
  endLine: property.loc.end.line,
254
310
  property,
311
+ endOffset,
255
312
  startLine,
313
+ startOffset,
256
314
  });
315
+
257
316
  previousProperty = property;
258
317
  }
259
318
 
@@ -273,66 +332,80 @@ function getSortPlan(
273
332
  }
274
333
  }
275
334
 
276
- const groups: SortUnit[][] = [];
277
- let currentGroup: SortUnit[] = [];
278
- 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
+ }
279
352
 
280
- for (const unit of units) {
281
- if (previousEndLine !== null && unit.startLine > previousEndLine + 1) {
282
- groups.push(currentGroup);
283
- 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
+ };
284
361
  }
285
362
 
286
- currentGroup.push(unit);
287
- previousEndLine = unit.endLine;
288
- }
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" ? "" : ",";
289
366
 
290
- if (currentGroup.length > 0) {
291
- groups.push(currentGroup);
367
+ return {
368
+ edits: null,
369
+ needsFix: true,
370
+ replacement: `{\n${groupsText}${trailingComma}\n${lineIndent}}`,
371
+ };
292
372
  }
293
373
 
294
- let needsSort = false;
374
+ const edits: SortEdit[] = [];
295
375
 
296
- for (const group of groups) {
376
+ for (const group of splitGroups(units)) {
297
377
  const sorted = sortGroup(group);
298
378
 
299
- if (sorted.some((unit, index) => unit !== group[index])) {
300
- needsSort = true;
301
- break;
379
+ if (sorted.every((unit, index) => unit === group[index])) {
380
+ continue;
302
381
  }
303
- }
304
382
 
305
- const needsFormat = forceMultiline && inlineStart;
383
+ const first = group[0];
384
+ const last = group.at(-1);
306
385
 
307
- if (!needsSort && !needsFormat) {
308
- return {
309
- needsFix: false,
310
- replacement: null,
311
- };
312
- }
386
+ if (first === undefined || last === undefined) {
387
+ continue;
388
+ }
313
389
 
314
- if (singleLine && !forceMultiline) {
315
- const sortedUnits = sortGroup(units);
390
+ edits.push({
391
+ end: last.endOffset,
392
+ text: getEditText(sorted),
393
+ start: first.startOffset,
394
+ });
395
+ }
316
396
 
397
+ if (edits.length === 0) {
317
398
  return {
318
- needsFix: true,
319
- replacement: `{ ${sortedUnits.map((unit) => unit.text).join(", ")} }`,
399
+ edits: null,
400
+ needsFix: false,
401
+ replacement: null,
320
402
  };
321
403
  }
322
404
 
323
- const groupsText = groups
324
- .map((group) =>
325
- sortGroup(group)
326
- .map((unit) => unit.text)
327
- .join(",\n"),
328
- )
329
- .join(",\n\n");
330
- const lastUnit = groups.flatMap((group) => sortGroup(group)).at(-1);
331
- const trailingComma = lastUnit?.property.type === "RestElement" ? "" : ",";
332
-
333
405
  return {
406
+ edits,
334
407
  needsFix: true,
335
- replacement: `{\n${groupsText}${trailingComma}\n${lineIndent}}`,
408
+ replacement: null,
336
409
  };
337
410
  }
338
411
 
@@ -1,7 +1,7 @@
1
1
  import { defineRule } from "vite-plus/lint/plugins";
2
- import type { Fix, Fixer, ESTree, Context, SourceCode } from "vite-plus/lint/plugins";
2
+ import type { ESTree, Context, SourceCode } from "vite-plus/lint/plugins";
3
3
 
4
- import { getNextStatement } from "./blank-line.js";
4
+ import { getNextStatement, getRemoveBlankLineFix } from "./blank-line.js";
5
5
 
6
6
  function getFunction(node: ESTree.Node): ESTree.Function | null {
7
7
  if (node.type === "FunctionDeclaration" || node.type === "TSDeclareFunction") {
@@ -37,32 +37,6 @@ function getKeyName(node: ESTree.MethodDefinition): string | null {
37
37
  return null;
38
38
  }
39
39
 
40
- function getRemoveBlankLinesFix(
41
- fixer: Fixer,
42
- sourceCode: SourceCode,
43
- previous: ESTree.Node,
44
- next: ESTree.Node,
45
- ): Fix | null {
46
- if (next.loc.start.line <= previous.loc.end.line + 1) {
47
- return null;
48
- }
49
-
50
- const fromLineEnd = sourceCode.text.indexOf("\n", previous.range[1]);
51
-
52
- if (fromLineEnd === -1) {
53
- return null;
54
- }
55
-
56
- const nextLineStart = sourceCode.text.lastIndexOf("\n", next.range[0]) + 1;
57
- const indent = sourceCode.text.slice(nextLineStart, next.range[0]);
58
-
59
- if (indent.trim() !== "") {
60
- return null;
61
- }
62
-
63
- return fixer.replaceTextRange([fromLineEnd, next.range[0]], `\n${indent}`);
64
- }
65
-
66
40
  function hasCommentBetween(
67
41
  sourceCode: SourceCode,
68
42
  previous: ESTree.Node,
@@ -88,7 +62,7 @@ function reportIfSeparated(
88
62
 
89
63
  context.report({
90
64
  fix(fixer) {
91
- return getRemoveBlankLinesFix(fixer, sourceCode, previous, next);
65
+ return getRemoveBlankLineFix(fixer, sourceCode, previous, next);
92
66
  },
93
67
  node: next,
94
68
  messageId: "adjacent",
@@ -130,7 +130,6 @@ const sortJsxProps = defineRule({
130
130
  const separator = multiline
131
131
  ? `\n${getLineIndent(sourceCode.text, firstAttribute.range[0])}`
132
132
  : " ";
133
-
134
133
  context.report({
135
134
  fix(fixer) {
136
135
  return fixer.replaceTextRange(
@@ -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,
@@ -176,8 +176,8 @@ function splitGroups(units: readonly MemberUnit[]): MemberUnit[][] {
176
176
 
177
177
  function sortGroup(group: readonly MemberUnit[]): MemberUnit[] {
178
178
  const sortable = group.filter((unit): unit is NamedUnit => unit.name !== null);
179
-
180
179
  sortable.sort((left, right) => compareNames(left.name, right.name));
180
+
181
181
  const rest = group.filter((unit) => unit.name === null);
182
182
 
183
183
  return [...sortable, ...rest];
@@ -70,7 +70,6 @@ const spacedComment = defineRule({
70
70
  const contentStart = prefixLength;
71
71
  const contentEnd = raw.lastIndexOf("*/");
72
72
  const content = raw.slice(contentStart, contentEnd);
73
-
74
73
  needsLeading = content !== "" && !isWhitespace(content[0]);
75
74
  needsTrailing = content !== "" && !isWhitespace(content.at(-1));
76
75
  } else {
@@ -79,7 +78,6 @@ const spacedComment = defineRule({
79
78
  const leadingContent = firstLine.slice(prefixLength);
80
79
  const closeIndex = lastLine.lastIndexOf("*/");
81
80
  const trailingContent = lastLine.slice(0, closeIndex);
82
-
83
81
  needsLeading = leadingContent !== "" && !isWhitespace(leadingContent[0]);
84
82
  needsTrailing = trailingContent !== "" && !isWhitespace(trailingContent.at(-1));
85
83
  }