@tai-kun/vp-lint-plugins 0.0.0 → 0.0.1
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.
- package/LICENSE +21 -0
- package/README.md +1 -43
- package/dist/src/index.d.ts +22 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +21 -0
- package/dist/src/plugin.d.ts +3 -0
- package/dist/src/plugin.d.ts.map +1 -0
- package/dist/src/plugin.js +46 -0
- package/dist/src/rules/blank-line-after-control-block.d.ts +7 -0
- package/dist/src/rules/blank-line-after-control-block.d.ts.map +1 -0
- package/dist/src/rules/blank-line-after-control-block.js +54 -0
- package/dist/src/rules/blank-line-after-declaration.d.ts +7 -0
- package/dist/src/rules/blank-line-after-declaration.d.ts.map +1 -0
- package/dist/src/rules/blank-line-after-declaration.js +79 -0
- package/dist/src/rules/blank-line-after-variable-declaration.d.ts +8 -0
- package/dist/src/rules/blank-line-after-variable-declaration.d.ts.map +1 -0
- package/dist/src/rules/blank-line-after-variable-declaration.js +62 -0
- package/dist/src/rules/blank-line-before-return.d.ts +7 -0
- package/dist/src/rules/blank-line-before-return.d.ts.map +1 -0
- package/dist/src/rules/blank-line-before-return.js +81 -0
- package/dist/src/rules/blank-line-between-class-members.d.ts +8 -0
- package/dist/src/rules/blank-line-between-class-members.d.ts.map +1 -0
- package/dist/src/rules/blank-line-between-class-members.js +85 -0
- package/dist/src/rules/blank-line-between-switch-cases.d.ts +8 -0
- package/dist/src/rules/blank-line-between-switch-cases.d.ts.map +1 -0
- package/dist/src/rules/blank-line-between-switch-cases.js +45 -0
- package/dist/src/rules/blank-line.d.ts +14 -0
- package/dist/src/rules/blank-line.d.ts.map +1 -0
- package/dist/src/rules/blank-line.js +68 -0
- package/dist/src/rules/comment-japanese-spacing.d.ts +8 -0
- package/dist/src/rules/comment-japanese-spacing.d.ts.map +1 -0
- package/dist/src/rules/comment-japanese-spacing.js +195 -0
- package/dist/src/rules/comment-one-sentence-per-line.d.ts +9 -0
- package/dist/src/rules/comment-one-sentence-per-line.d.ts.map +1 -0
- package/dist/src/rules/comment-one-sentence-per-line.js +190 -0
- package/dist/src/rules/comment-utils.d.ts +30 -0
- package/dist/src/rules/comment-utils.d.ts.map +1 -0
- package/dist/src/rules/comment-utils.js +153 -0
- package/dist/src/rules/jsdoc-param-order.d.ts +6 -0
- package/dist/src/rules/jsdoc-param-order.d.ts.map +1 -0
- package/dist/src/rules/jsdoc-param-order.js +101 -0
- package/dist/src/rules/jsdoc-tag-lines.d.ts +6 -0
- package/dist/src/rules/jsdoc-tag-lines.d.ts.map +1 -0
- package/dist/src/rules/jsdoc-tag-lines.js +128 -0
- package/dist/src/rules/jsdoc-tag-order.d.ts +6 -0
- package/dist/src/rules/jsdoc-tag-order.d.ts.map +1 -0
- package/dist/src/rules/jsdoc-tag-order.js +62 -0
- package/dist/src/rules/jsdoc-utils.d.ts +26 -0
- package/dist/src/rules/jsdoc-utils.d.ts.map +1 -0
- package/dist/src/rules/jsdoc-utils.js +132 -0
- package/dist/src/rules/member-ordering.d.ts +6 -0
- package/dist/src/rules/member-ordering.d.ts.map +1 -0
- package/dist/src/rules/member-ordering.js +66 -0
- package/dist/src/rules/object-properties-newline.d.ts +9 -0
- package/dist/src/rules/object-properties-newline.d.ts.map +1 -0
- package/dist/src/rules/object-properties-newline.js +47 -0
- package/dist/src/rules/object-sort-utils.d.ts +32 -0
- package/dist/src/rules/object-sort-utils.d.ts.map +1 -0
- package/dist/src/rules/object-sort-utils.js +221 -0
- package/dist/src/rules/overload-signatures-adjacent.d.ts +6 -0
- package/dist/src/rules/overload-signatures-adjacent.d.ts.map +1 -0
- package/dist/src/rules/overload-signatures-adjacent.js +143 -0
- package/dist/src/rules/sort-import-export-specifiers.d.ts +8 -0
- package/dist/src/rules/sort-import-export-specifiers.d.ts.map +1 -0
- package/dist/src/rules/sort-import-export-specifiers.js +130 -0
- package/dist/src/rules/sort-jsx-props.d.ts +7 -0
- package/dist/src/rules/sort-jsx-props.d.ts.map +1 -0
- package/dist/src/rules/sort-jsx-props.js +102 -0
- package/dist/src/rules/sort-object-properties.d.ts +9 -0
- package/dist/src/rules/sort-object-properties.d.ts.map +1 -0
- package/dist/src/rules/sort-object-properties.js +54 -0
- package/dist/src/rules/sort-type-members.d.ts +8 -0
- package/dist/src/rules/sort-type-members.d.ts.map +1 -0
- package/dist/src/rules/sort-type-members.js +213 -0
- package/dist/src/rules/spaced-comment.d.ts +6 -0
- package/dist/src/rules/spaced-comment.d.ts.map +1 -0
- package/dist/src/rules/spaced-comment.js +105 -0
- package/dist/src/rules/type-literal-properties-newline.d.ts +8 -0
- package/dist/src/rules/type-literal-properties-newline.d.ts.map +1 -0
- package/dist/src/rules/type-literal-properties-newline.js +39 -0
- package/package.json +40 -8
- package/src/index.ts +22 -0
- package/src/plugin.ts +48 -0
- package/src/rules/blank-line-after-control-block.ts +82 -0
- package/src/rules/blank-line-after-declaration.ts +106 -0
- package/src/rules/blank-line-after-variable-declaration.ts +84 -0
- package/src/rules/blank-line-before-return.ts +101 -0
- package/src/rules/blank-line-between-class-members.ts +112 -0
- package/src/rules/blank-line-between-switch-cases.ts +53 -0
- package/src/rules/blank-line.ts +113 -0
- package/src/rules/comment-japanese-spacing.ts +271 -0
- package/src/rules/comment-one-sentence-per-line.ts +264 -0
- package/src/rules/comment-utils.ts +218 -0
- package/src/rules/jsdoc-param-order.ts +137 -0
- package/src/rules/jsdoc-tag-lines.ts +165 -0
- package/src/rules/jsdoc-tag-order.ts +75 -0
- package/src/rules/jsdoc-utils.ts +179 -0
- package/src/rules/member-ordering.ts +93 -0
- package/src/rules/object-properties-newline.ts +54 -0
- package/src/rules/object-sort-utils.ts +339 -0
- package/src/rules/overload-signatures-adjacent.ts +204 -0
- package/src/rules/sort-import-export-specifiers.ts +194 -0
- package/src/rules/sort-jsx-props.ts +149 -0
- package/src/rules/sort-object-properties.ts +62 -0
- package/src/rules/sort-type-members.ts +299 -0
- package/src/rules/spaced-comment.ts +129 -0
- package/src/rules/type-literal-properties-newline.ts +44 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import type { ESTree, Comment, SourceCode } from "vite-plus/lint/plugins";
|
|
2
|
+
|
|
3
|
+
import { isFence } from "./comment-utils.js";
|
|
4
|
+
|
|
5
|
+
interface JSDocLine {
|
|
6
|
+
content: string;
|
|
7
|
+
prefix: string;
|
|
8
|
+
raw: string;
|
|
9
|
+
tagName: string | null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function getTagName(text: string): string | null {
|
|
13
|
+
const match = /^@(?<tag>[A-Za-z][\w-]*)/u.exec(text.trimStart());
|
|
14
|
+
|
|
15
|
+
return match?.groups?.["tag"] ?? null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function stripType(value: string): string {
|
|
19
|
+
if (!value.startsWith("{")) {
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let depth = 0;
|
|
24
|
+
|
|
25
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
26
|
+
const character = value[index];
|
|
27
|
+
|
|
28
|
+
if (character === "{") {
|
|
29
|
+
depth += 1;
|
|
30
|
+
} else if (character === "}") {
|
|
31
|
+
depth -= 1;
|
|
32
|
+
if (depth === 0) {
|
|
33
|
+
return value.slice(index + 1).trimStart();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* `@param` 行からパラメーター名を取得します。
|
|
43
|
+
*/
|
|
44
|
+
function getParamName(content: string): string | null {
|
|
45
|
+
let value = content.trimStart();
|
|
46
|
+
|
|
47
|
+
if (getTagName(value) !== "param") {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
value = stripType(value.slice("@param".length).trimStart());
|
|
52
|
+
|
|
53
|
+
const match = /^(?<name>\S+)/u.exec(value);
|
|
54
|
+
const name = match?.groups?.["name"] ?? null;
|
|
55
|
+
|
|
56
|
+
if (name === null) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return name
|
|
61
|
+
.replace(/^\[/u, "")
|
|
62
|
+
.replace(/\]$/u, "")
|
|
63
|
+
.replace(/^\.\.\./u, "")
|
|
64
|
+
.replace(/=.*$/u, "");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* JSDoc コメントの各行を返します。
|
|
69
|
+
*
|
|
70
|
+
* JSDoc でない場合や、`*` の付かない行がある場合は null を返します。
|
|
71
|
+
*/
|
|
72
|
+
function getJSDocLines(comment: Comment, sourceCode: SourceCode): JSDocLine[] | null {
|
|
73
|
+
const raw = sourceCode.getText(comment);
|
|
74
|
+
|
|
75
|
+
if (!raw.startsWith("/**") || comment.type !== "Block") {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const rawLines = raw.split("\n");
|
|
80
|
+
|
|
81
|
+
if (rawLines.length === 1) {
|
|
82
|
+
const contentStart = 3;
|
|
83
|
+
const contentEnd = raw.lastIndexOf("*/");
|
|
84
|
+
const content = raw.slice(contentStart, contentEnd);
|
|
85
|
+
|
|
86
|
+
return [
|
|
87
|
+
{
|
|
88
|
+
raw,
|
|
89
|
+
prefix: raw.slice(0, contentStart),
|
|
90
|
+
content,
|
|
91
|
+
tagName: getTagName(content),
|
|
92
|
+
},
|
|
93
|
+
];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const lines: JSDocLine[] = [];
|
|
97
|
+
|
|
98
|
+
for (const [index, line] of rawLines.entries()) {
|
|
99
|
+
if (index === 0) {
|
|
100
|
+
const contentStart = 3;
|
|
101
|
+
const closeIndex = line.lastIndexOf("*/");
|
|
102
|
+
const contentEnd = closeIndex === -1 ? line.length : Math.max(contentStart, closeIndex);
|
|
103
|
+
const content = line.slice(contentStart, contentEnd);
|
|
104
|
+
|
|
105
|
+
lines.push({
|
|
106
|
+
raw: line,
|
|
107
|
+
prefix: line.slice(0, contentStart),
|
|
108
|
+
content,
|
|
109
|
+
tagName: getTagName(content),
|
|
110
|
+
});
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const match = /^(?<prefix>\s*\*[ \t]?).*$/u.exec(line);
|
|
115
|
+
|
|
116
|
+
if (match === null) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const prefix = match.groups?.["prefix"] ?? "";
|
|
121
|
+
const contentStart = prefix.length;
|
|
122
|
+
const closeIndex = line.lastIndexOf("*/");
|
|
123
|
+
const contentEnd = closeIndex === -1 ? line.length : Math.max(contentStart, closeIndex);
|
|
124
|
+
const content = line.slice(contentStart, contentEnd);
|
|
125
|
+
|
|
126
|
+
lines.push({
|
|
127
|
+
raw: line,
|
|
128
|
+
prefix,
|
|
129
|
+
content,
|
|
130
|
+
tagName: getTagName(content),
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return lines;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function hasExampleOrFence(lines: readonly JSDocLine[]): boolean {
|
|
138
|
+
return lines.some((line) => line.tagName === "example" || isFence(line.content));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function getNodeStart(node: ESTree.Node): number {
|
|
142
|
+
if ("decorators" in node) {
|
|
143
|
+
const decorator = node.decorators?.[0];
|
|
144
|
+
|
|
145
|
+
if (decorator !== undefined) {
|
|
146
|
+
return decorator.range[0];
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return node.range[0];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* ノードに付いている JSDoc コメントを返します。
|
|
155
|
+
*/
|
|
156
|
+
function getNodeJSDoc(node: ESTree.Node, sourceCode: SourceCode): Comment | null {
|
|
157
|
+
const start = getNodeStart(node);
|
|
158
|
+
const comment = sourceCode.getAllComments().findLast((candidate) => candidate.range[1] <= start);
|
|
159
|
+
|
|
160
|
+
if (comment === null || comment === undefined || comment.type !== "Block") {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (!sourceCode.getText(comment).startsWith("/**")) {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (sourceCode.text.slice(comment.range[1], start).trim() !== "") {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const startLine = sourceCode.getLocFromIndex(start).line;
|
|
173
|
+
|
|
174
|
+
return comment.loc.end.line === startLine - 1 ? comment : null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export type { JSDocLine };
|
|
178
|
+
|
|
179
|
+
export { getTagName, getNodeJSDoc, getParamName, getJSDocLines, hasExampleOrFence };
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { defineRule } from "vite-plus/lint/plugins";
|
|
2
|
+
import type { ESTree } from "vite-plus/lint/plugins";
|
|
3
|
+
|
|
4
|
+
type MemberGroup =
|
|
5
|
+
| "static-field"
|
|
6
|
+
| "static-block"
|
|
7
|
+
| "instance-field"
|
|
8
|
+
| "constructor"
|
|
9
|
+
| "static-method"
|
|
10
|
+
| "instance-method";
|
|
11
|
+
|
|
12
|
+
const MEMBER_GROUP_ORDER: Record<MemberGroup, number> = {
|
|
13
|
+
constructor: 3,
|
|
14
|
+
"static-block": 1,
|
|
15
|
+
"static-field": 0,
|
|
16
|
+
"static-method": 4,
|
|
17
|
+
"instance-field": 2,
|
|
18
|
+
"instance-method": 5,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function getMemberGroup(member: ESTree.ClassElement): MemberGroup | null {
|
|
22
|
+
if (member.type === "StaticBlock") {
|
|
23
|
+
return "static-block";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (member.type === "TSIndexSignature") {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (
|
|
31
|
+
member.type === "PropertyDefinition" ||
|
|
32
|
+
member.type === "TSAbstractPropertyDefinition" ||
|
|
33
|
+
member.type === "AccessorProperty" ||
|
|
34
|
+
member.type === "TSAbstractAccessorProperty"
|
|
35
|
+
) {
|
|
36
|
+
return member.static ? "static-field" : "instance-field";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (member.type === "MethodDefinition" || member.type === "TSAbstractMethodDefinition") {
|
|
40
|
+
if (member.kind === "constructor") {
|
|
41
|
+
return "constructor";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return member.static ? "static-method" : "instance-method";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* クラスメンバーを「static フィールド → static ブロック → インスタンスフィールド → constructor → static メソッド → インスタンスメソッド」の順に要求します。
|
|
52
|
+
*/
|
|
53
|
+
const memberOrdering = defineRule({
|
|
54
|
+
meta: {
|
|
55
|
+
docs: {
|
|
56
|
+
description:
|
|
57
|
+
"Require class members to be ordered as static fields, static blocks, instance fields, constructor, static methods, and instance methods.",
|
|
58
|
+
},
|
|
59
|
+
type: "suggestion",
|
|
60
|
+
messages: {
|
|
61
|
+
order:
|
|
62
|
+
"Class members must be ordered as static fields, static blocks, instance fields, constructor, static methods, and instance methods.",
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
create(context) {
|
|
66
|
+
return {
|
|
67
|
+
ClassBody(node): void {
|
|
68
|
+
let previousRank: number | null = null;
|
|
69
|
+
|
|
70
|
+
for (const member of node.body) {
|
|
71
|
+
const group = getMemberGroup(member);
|
|
72
|
+
|
|
73
|
+
if (group === null) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const rank = MEMBER_GROUP_ORDER[group];
|
|
78
|
+
|
|
79
|
+
if (previousRank !== null && rank < previousRank) {
|
|
80
|
+
context.report({
|
|
81
|
+
node: member,
|
|
82
|
+
messageId: "order",
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
previousRank = rank;
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
export default memberOrdering;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { defineRule } from "vite-plus/lint/plugins";
|
|
2
|
+
|
|
3
|
+
import { getSortPlan } from "./object-sort-utils.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* プロパティーが 2 つ以上あるオブジェクトリテラルを複数行にします。
|
|
7
|
+
*
|
|
8
|
+
* Oxfmt の `objectWrap` には「プロパティー数で改行を強制する」オプションがないため、改行を挿入する最小限の fix を提供します。
|
|
9
|
+
* `objectWrap: "preserve"` と組み合わせることで、後続の Oxfmt がプロパティーごとの改行とインデントを整えます。
|
|
10
|
+
*/
|
|
11
|
+
const objectPropertiesNewline = defineRule({
|
|
12
|
+
meta: {
|
|
13
|
+
docs: {
|
|
14
|
+
description: "Enforce multiline object literals when they have two or more properties.",
|
|
15
|
+
},
|
|
16
|
+
type: "layout",
|
|
17
|
+
fixable: "whitespace",
|
|
18
|
+
messages: {
|
|
19
|
+
multiline: "Objects with 2 or more properties must be multiline.",
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
create(context) {
|
|
23
|
+
return {
|
|
24
|
+
ObjectExpression(node): void {
|
|
25
|
+
if (node.properties.length < 2) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// 並び替え可能なオブジェクトは sort-object-properties が複数行化も担当します。
|
|
30
|
+
if (getSortPlan(node, context.sourceCode) !== null) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Oxfmt は `{` の直後に改行があるかどうかで複数行を維持するため、最初のプロパティーが `{` と同じ行にある場合だけを対象にします。
|
|
35
|
+
const firstProperty = node.properties[0];
|
|
36
|
+
|
|
37
|
+
if (firstProperty === undefined || firstProperty.loc.start.line !== node.loc.start.line) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
context.report({
|
|
42
|
+
fix(fixer) {
|
|
43
|
+
// Oxfmt は `{` の直後の改行を複数行オブジェクトの目印にするため、ここでは改行を 1 つ挿入するだけで十分です。
|
|
44
|
+
return fixer.insertTextAfterRange([node.range[0], node.range[0] + 1], "\n");
|
|
45
|
+
},
|
|
46
|
+
node,
|
|
47
|
+
messageId: "multiline",
|
|
48
|
+
});
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
export default objectPropertiesNewline;
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import type { ESTree, Comment, SourceCode } from "vite-plus/lint/plugins";
|
|
2
|
+
|
|
3
|
+
type SortableObject = ESTree.ObjectExpression | ESTree.ObjectPattern;
|
|
4
|
+
|
|
5
|
+
type SortableProperty = SortableObject["properties"][number];
|
|
6
|
+
|
|
7
|
+
interface SortPlanOptions {
|
|
8
|
+
/**
|
|
9
|
+
* 末尾の RestElement を許可するかどうかです。
|
|
10
|
+
*/
|
|
11
|
+
allowRest?: boolean;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 1 行のオブジェクトを複数行に整形するかどうかです。
|
|
15
|
+
*/
|
|
16
|
+
forceMultiline?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface SortUnit {
|
|
20
|
+
endLine: number;
|
|
21
|
+
name: string | null;
|
|
22
|
+
property: SortableProperty;
|
|
23
|
+
startLine: number;
|
|
24
|
+
text: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type NamedSortUnit = SortUnit & { name: string };
|
|
28
|
+
|
|
29
|
+
type SortPlan =
|
|
30
|
+
| {
|
|
31
|
+
needsFix: false;
|
|
32
|
+
replacement: null;
|
|
33
|
+
}
|
|
34
|
+
| {
|
|
35
|
+
needsFix: true;
|
|
36
|
+
replacement: string;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
function getLineStarts(text: string): number[] {
|
|
40
|
+
const starts = [0];
|
|
41
|
+
|
|
42
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
43
|
+
if (text[index] === "\n") {
|
|
44
|
+
starts.push(index + 1);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return starts;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getLineIndent(text: string, lineStarts: readonly number[], line: number): string {
|
|
52
|
+
const start = lineStarts[line - 1] ?? 0;
|
|
53
|
+
const match = /^[ \t]*/u.exec(text.slice(start));
|
|
54
|
+
|
|
55
|
+
return match?.[0] ?? "";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function getPropertyName(property: SortableProperty): string | null {
|
|
59
|
+
if (property.type !== "Property" || property.computed || property.kind !== "init") {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const key = property.key;
|
|
64
|
+
let name: string;
|
|
65
|
+
|
|
66
|
+
if (key.type === "Identifier") {
|
|
67
|
+
name = key.name;
|
|
68
|
+
} else if (
|
|
69
|
+
key.type === "Literal" &&
|
|
70
|
+
(typeof key.value === "string" || typeof key.value === "number")
|
|
71
|
+
) {
|
|
72
|
+
name = String(key.value);
|
|
73
|
+
} else {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return name === "__proto__" ? null : name;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function isSortableObject(node: SortableObject, allowRest: boolean): boolean {
|
|
81
|
+
if (node.properties.length < 2) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const names = new Set<string>();
|
|
86
|
+
|
|
87
|
+
for (const [index, property] of node.properties.entries()) {
|
|
88
|
+
if (property.type === "RestElement") {
|
|
89
|
+
if (!allowRest || index !== node.properties.length - 1) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const name = getPropertyName(property);
|
|
97
|
+
|
|
98
|
+
if (name === null || names.has(name)) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
names.add(name);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function compareNames(left: string, right: string): number {
|
|
109
|
+
if (left.length !== right.length) {
|
|
110
|
+
return left.length - right.length;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const leftLower = left.toLowerCase();
|
|
114
|
+
const rightLower = right.toLowerCase();
|
|
115
|
+
|
|
116
|
+
if (leftLower !== rightLower) {
|
|
117
|
+
return leftLower < rightLower ? -1 : 1;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (left === right) {
|
|
121
|
+
return 0;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return left < right ? -1 : 1;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function compareUnits(left: NamedSortUnit, right: NamedSortUnit): number {
|
|
128
|
+
return compareNames(left.name, right.name);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function sortGroup(group: readonly SortUnit[]): SortUnit[] {
|
|
132
|
+
const sortable = group.filter((unit): unit is NamedSortUnit => unit.name !== null);
|
|
133
|
+
|
|
134
|
+
sortable.sort(compareUnits);
|
|
135
|
+
const rest = group.filter((unit) => unit.name === null);
|
|
136
|
+
|
|
137
|
+
return [...sortable, ...rest];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* オブジェクトリテラル/オブジェクトパターンの並び替え計画を返します。
|
|
142
|
+
*
|
|
143
|
+
* - プロパティー名の長さが短い順、同じ長さなら名前順
|
|
144
|
+
* - 空行で区切られたグループ内だけで並び替え
|
|
145
|
+
* - プロパティー直上のコメントはプロパティーと一緒に移動
|
|
146
|
+
*
|
|
147
|
+
* 並び替えが安全でない場合は null を返します。
|
|
148
|
+
*/
|
|
149
|
+
function getSortPlan(
|
|
150
|
+
node: SortableObject,
|
|
151
|
+
sourceCode: SourceCode,
|
|
152
|
+
options: SortPlanOptions = {},
|
|
153
|
+
): SortPlan | null {
|
|
154
|
+
const { allowRest = false, forceMultiline = true } = options;
|
|
155
|
+
|
|
156
|
+
if (!isSortableObject(node, allowRest)) {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const source = sourceCode.text;
|
|
161
|
+
const lineStarts = getLineStarts(source);
|
|
162
|
+
const lineIndent = getLineIndent(source, lineStarts, node.loc.start.line);
|
|
163
|
+
const comments = sourceCode
|
|
164
|
+
.getAllComments()
|
|
165
|
+
.filter((comment) => comment.range[0] >= node.range[0] && comment.range[1] <= node.range[1]);
|
|
166
|
+
const firstProperty = node.properties[0];
|
|
167
|
+
|
|
168
|
+
if (firstProperty === undefined) {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const inlineStart = firstProperty.loc.start.line === node.loc.start.line;
|
|
173
|
+
const singleLine = node.loc.start.line === node.loc.end.line;
|
|
174
|
+
|
|
175
|
+
const units: SortUnit[] = [];
|
|
176
|
+
|
|
177
|
+
if (inlineStart) {
|
|
178
|
+
if (comments.length > 0) {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
for (const property of node.properties) {
|
|
183
|
+
const isRest = property.type === "RestElement";
|
|
184
|
+
|
|
185
|
+
units.push({
|
|
186
|
+
name: isRest ? null : getPropertyName(property),
|
|
187
|
+
text: source.slice(property.range[0], property.range[1]),
|
|
188
|
+
endLine: property.loc.end.line,
|
|
189
|
+
property,
|
|
190
|
+
startLine: property.loc.start.line,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
let previousProperty: SortableProperty | null = null;
|
|
195
|
+
|
|
196
|
+
for (const property of node.properties) {
|
|
197
|
+
const previous = previousProperty;
|
|
198
|
+
const boundary = previous === null ? node.range[0] + 1 : previous.range[1];
|
|
199
|
+
const candidates = comments.filter(
|
|
200
|
+
(comment) => comment.range[0] >= boundary && comment.range[1] <= property.range[0],
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
if (
|
|
204
|
+
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
|
+
)
|
|
211
|
+
) {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const attached: Comment[] = [];
|
|
216
|
+
let expectedLine = property.loc.start.line;
|
|
217
|
+
|
|
218
|
+
for (let index = candidates.length - 1; index >= 0; index -= 1) {
|
|
219
|
+
const comment = candidates[index];
|
|
220
|
+
|
|
221
|
+
if (comment === undefined) {
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (comment.loc.end.line !== expectedLine && comment.loc.end.line !== expectedLine - 1) {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (comment.loc.start.line === expectedLine) {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (comment.loc.start.line === node.loc.start.line) {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
attached.unshift(comment);
|
|
238
|
+
expectedLine = comment.loc.start.line;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (attached.length !== candidates.length) {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const first = attached[0] ?? property;
|
|
246
|
+
const startLine = first.loc.start.line;
|
|
247
|
+
const unitStart = lineStarts[startLine - 1] ?? 0;
|
|
248
|
+
const isRest = property.type === "RestElement";
|
|
249
|
+
|
|
250
|
+
units.push({
|
|
251
|
+
name: isRest ? null : getPropertyName(property),
|
|
252
|
+
text: source.slice(unitStart, property.range[1]),
|
|
253
|
+
endLine: property.loc.end.line,
|
|
254
|
+
property,
|
|
255
|
+
startLine,
|
|
256
|
+
});
|
|
257
|
+
previousProperty = property;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const lastProperty = node.properties.at(-1);
|
|
261
|
+
|
|
262
|
+
if (lastProperty === undefined) {
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (
|
|
267
|
+
comments.some(
|
|
268
|
+
(comment) =>
|
|
269
|
+
comment.range[0] >= lastProperty.range[1] && comment.range[1] <= node.range[1] - 1,
|
|
270
|
+
)
|
|
271
|
+
) {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const groups: SortUnit[][] = [];
|
|
277
|
+
let currentGroup: SortUnit[] = [];
|
|
278
|
+
let previousEndLine: number | null = null;
|
|
279
|
+
|
|
280
|
+
for (const unit of units) {
|
|
281
|
+
if (previousEndLine !== null && unit.startLine > previousEndLine + 1) {
|
|
282
|
+
groups.push(currentGroup);
|
|
283
|
+
currentGroup = [];
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
currentGroup.push(unit);
|
|
287
|
+
previousEndLine = unit.endLine;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (currentGroup.length > 0) {
|
|
291
|
+
groups.push(currentGroup);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
let needsSort = false;
|
|
295
|
+
|
|
296
|
+
for (const group of groups) {
|
|
297
|
+
const sorted = sortGroup(group);
|
|
298
|
+
|
|
299
|
+
if (sorted.some((unit, index) => unit !== group[index])) {
|
|
300
|
+
needsSort = true;
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const needsFormat = forceMultiline && inlineStart;
|
|
306
|
+
|
|
307
|
+
if (!needsSort && !needsFormat) {
|
|
308
|
+
return {
|
|
309
|
+
needsFix: false,
|
|
310
|
+
replacement: null,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (singleLine && !forceMultiline) {
|
|
315
|
+
const sortedUnits = sortGroup(units);
|
|
316
|
+
|
|
317
|
+
return {
|
|
318
|
+
needsFix: true,
|
|
319
|
+
replacement: `{ ${sortedUnits.map((unit) => unit.text).join(", ")} }`,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
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
|
+
return {
|
|
334
|
+
needsFix: true,
|
|
335
|
+
replacement: `{\n${groupsText}${trailingComma}\n${lineIndent}}`,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export { getSortPlan, compareNames };
|