@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,218 @@
|
|
|
1
|
+
import type { Comment, SourceCode } from "vite-plus/lint/plugins";
|
|
2
|
+
|
|
3
|
+
interface CommentLine {
|
|
4
|
+
column: number;
|
|
5
|
+
content: string;
|
|
6
|
+
end: number;
|
|
7
|
+
line: number;
|
|
8
|
+
start: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const SENTENCE_END = /[。..!?!?]$/u;
|
|
12
|
+
const TRAILING_CLOSERS = /[」』】)》)〕〉]}"'”’]+$/u;
|
|
13
|
+
const DIRECTIVE_PREFIXES = [
|
|
14
|
+
"#!",
|
|
15
|
+
"///",
|
|
16
|
+
"oxlint-",
|
|
17
|
+
"eslint-",
|
|
18
|
+
"prettier-",
|
|
19
|
+
"@ts-",
|
|
20
|
+
"@type",
|
|
21
|
+
"@see",
|
|
22
|
+
"@link",
|
|
23
|
+
"TODO",
|
|
24
|
+
"FIXME",
|
|
25
|
+
"NOTE",
|
|
26
|
+
"HACK",
|
|
27
|
+
"XXX",
|
|
28
|
+
"istanbul",
|
|
29
|
+
"c8 ",
|
|
30
|
+
"v8 ",
|
|
31
|
+
"#region",
|
|
32
|
+
"#endregion",
|
|
33
|
+
"biome-",
|
|
34
|
+
"deno-",
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
function isBlank(text: string): boolean {
|
|
38
|
+
return text.trim() === "";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isDirective(text: string): boolean {
|
|
42
|
+
const value = text.trimStart();
|
|
43
|
+
|
|
44
|
+
return DIRECTIVE_PREFIXES.some((prefix) => value.indexOf(prefix) === 0);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isLabelLine(text: string): boolean {
|
|
48
|
+
const value = text.trim();
|
|
49
|
+
|
|
50
|
+
return /^[^\s::]{1,20}[::]\s/u.test(value) || /[::]$/u.test(value);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isTag(text: string): boolean {
|
|
54
|
+
return /^@[A-Za-z]/u.test(text.trimStart());
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isListOrHeading(text: string): boolean {
|
|
58
|
+
return /^(?:[-*+]\s|\d+[.)]\s|#{1,6}\s|\|)/u.test(text.trimStart());
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function isCodeLike(text: string): boolean {
|
|
62
|
+
const value = text.trimStart();
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
/^(?:```|~~~|const |let |var |function |return |if \(|for \(|while \(|switch \(|class |import |export |type |interface |\{|\}|\$ |> |pnpm |npm |npx |node |git |mise |yarn |bun |deno |curl |cd |echo |sudo |https?:\/\/)/u.test(
|
|
66
|
+
value,
|
|
67
|
+
) || /=>|;\s*$/u.test(value)
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function isFence(text: string): boolean {
|
|
72
|
+
const value = text.trimStart();
|
|
73
|
+
|
|
74
|
+
return value.slice(0, 3) === "```" || value.slice(0, 3) === "~~~";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function isSentenceEnd(text: string): boolean {
|
|
78
|
+
const value = text.trimEnd().replace(TRAILING_CLOSERS, "");
|
|
79
|
+
|
|
80
|
+
return SENTENCE_END.test(value);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 散文として扱わない行かどうかを返します。
|
|
85
|
+
*/
|
|
86
|
+
function isSkippableProseLine(text: string): boolean {
|
|
87
|
+
const trimmed = text.trim();
|
|
88
|
+
|
|
89
|
+
if (trimmed === "" || isFence(trimmed)) {
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
isDirective(trimmed) ||
|
|
95
|
+
isLabelLine(trimmed) ||
|
|
96
|
+
isTag(trimmed) ||
|
|
97
|
+
isListOrHeading(trimmed) ||
|
|
98
|
+
isCodeLike(trimmed)
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getBlockLineInfos(comment: Comment, sourceCode: SourceCode): (CommentLine | null)[] {
|
|
103
|
+
const raw = sourceCode.getText(comment);
|
|
104
|
+
|
|
105
|
+
if (!raw.includes("\n")) {
|
|
106
|
+
return [];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const lines = raw.split("\n");
|
|
110
|
+
const infos: (CommentLine | null)[] = [];
|
|
111
|
+
let offset = comment.range[0];
|
|
112
|
+
|
|
113
|
+
for (const [index, line] of lines.entries()) {
|
|
114
|
+
let contentStart: number;
|
|
115
|
+
let contentEnd = line.length;
|
|
116
|
+
|
|
117
|
+
if (index === 0) {
|
|
118
|
+
contentStart = raw.slice(0, 3) === "/**" ? 3 : 2;
|
|
119
|
+
} else {
|
|
120
|
+
const match = /^(?<prefix>\s*\*[ \t]?).*$/u.exec(line);
|
|
121
|
+
|
|
122
|
+
if (match === null) {
|
|
123
|
+
infos.push(null);
|
|
124
|
+
offset += line.length + 1;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
contentStart = match.groups?.["prefix"]?.length ?? 0;
|
|
129
|
+
const closeIndex = line.indexOf("*/");
|
|
130
|
+
|
|
131
|
+
if (closeIndex !== -1) {
|
|
132
|
+
contentEnd = Math.max(contentStart, closeIndex);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
infos.push({
|
|
137
|
+
end: offset + contentEnd,
|
|
138
|
+
line: comment.loc.start.line + index,
|
|
139
|
+
start: offset + contentStart,
|
|
140
|
+
column: index === 0 ? comment.loc.start.column + contentStart : contentStart,
|
|
141
|
+
content: line.slice(contentStart, contentEnd),
|
|
142
|
+
});
|
|
143
|
+
offset += line.length + 1;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return infos;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function getLineGroupInfos(group: readonly Comment[], sourceCode: SourceCode): CommentLine[] {
|
|
150
|
+
return group.map((comment) => {
|
|
151
|
+
const raw = sourceCode.getText(comment);
|
|
152
|
+
const commentPrefix = raw.slice(0, 2) === "//" ? 2 : 0;
|
|
153
|
+
const spacePrefix = raw[commentPrefix] === " " ? 1 : 0;
|
|
154
|
+
const contentStart = commentPrefix + spacePrefix;
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
end: comment.range[1],
|
|
158
|
+
line: comment.loc.start.line,
|
|
159
|
+
start: comment.range[0] + contentStart,
|
|
160
|
+
column: comment.loc.start.column + contentStart,
|
|
161
|
+
content: raw.slice(contentStart),
|
|
162
|
+
};
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function getSingleLineBlockInfo(comment: Comment, sourceCode: SourceCode): CommentLine {
|
|
167
|
+
const raw = sourceCode.getText(comment);
|
|
168
|
+
const prefixLength = raw.startsWith("/**") ? 3 : 2;
|
|
169
|
+
const closeIndex = raw.lastIndexOf("*/");
|
|
170
|
+
const contentEnd = closeIndex === -1 ? raw.length : closeIndex;
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
end: comment.range[0] + contentEnd,
|
|
174
|
+
line: comment.loc.start.line,
|
|
175
|
+
start: comment.range[0] + prefixLength,
|
|
176
|
+
column: comment.loc.start.column + prefixLength,
|
|
177
|
+
content: raw.slice(prefixLength, contentEnd),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* コメントの内容行を返します。
|
|
183
|
+
*
|
|
184
|
+
* ブロックコメントで `*` の付かない行がある場合は、その行を null として返します。
|
|
185
|
+
*/
|
|
186
|
+
function getCommentLineInfos(comment: Comment, sourceCode: SourceCode): (CommentLine | null)[] {
|
|
187
|
+
if (comment.type === "Line") {
|
|
188
|
+
return getLineGroupInfos([comment], sourceCode);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (comment.type === "Shebang") {
|
|
192
|
+
return [];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const raw = sourceCode.getText(comment);
|
|
196
|
+
|
|
197
|
+
if (!raw.includes("\n")) {
|
|
198
|
+
return [getSingleLineBlockInfo(comment, sourceCode)];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return getBlockLineInfos(comment, sourceCode);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export type { CommentLine };
|
|
205
|
+
|
|
206
|
+
export {
|
|
207
|
+
isTag,
|
|
208
|
+
isBlank,
|
|
209
|
+
isFence,
|
|
210
|
+
isCodeLike,
|
|
211
|
+
isDirective,
|
|
212
|
+
isLabelLine,
|
|
213
|
+
isSentenceEnd,
|
|
214
|
+
isListOrHeading,
|
|
215
|
+
getLineGroupInfos,
|
|
216
|
+
getCommentLineInfos,
|
|
217
|
+
isSkippableProseLine,
|
|
218
|
+
};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { defineRule } from "vite-plus/lint/plugins";
|
|
2
|
+
import type { ESTree, Context, SourceCode } from "vite-plus/lint/plugins";
|
|
3
|
+
|
|
4
|
+
import { getNodeJSDoc, getParamName, getJSDocLines, hasExampleOrFence } from "./jsdoc-utils.js";
|
|
5
|
+
|
|
6
|
+
function getParamNameFromPattern(param: ESTree.ParamPattern): string | null {
|
|
7
|
+
if (param.type === "Identifier") {
|
|
8
|
+
return param.name;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (param.type === "AssignmentPattern") {
|
|
12
|
+
return param.left.type === "Identifier" ? param.left.name : null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (param.type === "RestElement") {
|
|
16
|
+
return param.argument.type === "Identifier" ? param.argument.name : null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (param.type === "TSParameterProperty") {
|
|
20
|
+
return getParamNameFromPattern(param.parameter);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function getExpectedParams(params: readonly ESTree.ParamPattern[]): string[] | null {
|
|
27
|
+
const names: string[] = [];
|
|
28
|
+
|
|
29
|
+
for (const param of params) {
|
|
30
|
+
const name = getParamNameFromPattern(param);
|
|
31
|
+
|
|
32
|
+
if (name === null) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
names.push(name);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return names;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function checkFunction(
|
|
43
|
+
context: Context,
|
|
44
|
+
sourceCode: SourceCode,
|
|
45
|
+
node: ESTree.Node,
|
|
46
|
+
params: readonly ESTree.ParamPattern[],
|
|
47
|
+
): void {
|
|
48
|
+
const comment = getNodeJSDoc(node, sourceCode);
|
|
49
|
+
|
|
50
|
+
if (comment === null) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const lines = getJSDocLines(comment, sourceCode);
|
|
55
|
+
|
|
56
|
+
if (lines === null || hasExampleOrFence(lines)) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const tagNames = lines
|
|
61
|
+
.filter((line) => line.tagName === "param")
|
|
62
|
+
.map((line) => getParamName(line.content));
|
|
63
|
+
|
|
64
|
+
if (tagNames.length === 0 || tagNames.some((name) => name === null)) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const expected = getExpectedParams(params);
|
|
69
|
+
|
|
70
|
+
if (expected === null || expected.length !== tagNames.length) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (
|
|
75
|
+
new Set(expected).size !== expected.length ||
|
|
76
|
+
new Set(tagNames).size !== tagNames.length ||
|
|
77
|
+
tagNames.some((name) => name?.includes(".") === true)
|
|
78
|
+
) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
for (const [index, name] of tagNames.entries()) {
|
|
83
|
+
if (name !== expected[index]) {
|
|
84
|
+
context.report({
|
|
85
|
+
loc: comment.loc,
|
|
86
|
+
messageId: "paramOrder",
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* `@param` タグが関数の引数の順序に従っていることを要求します。
|
|
96
|
+
*/
|
|
97
|
+
const jsdocParamOrder = defineRule({
|
|
98
|
+
meta: {
|
|
99
|
+
docs: {
|
|
100
|
+
description: "Require @param tags to follow the order of function parameters.",
|
|
101
|
+
},
|
|
102
|
+
type: "suggestion",
|
|
103
|
+
messages: {
|
|
104
|
+
paramOrder: "@param tags must follow the order of function parameters.",
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
create(context) {
|
|
108
|
+
const sourceCode = context.sourceCode;
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
MethodDefinition(node): void {
|
|
112
|
+
checkFunction(context, sourceCode, node, node.value.params);
|
|
113
|
+
},
|
|
114
|
+
TSDeclareFunction(node): void {
|
|
115
|
+
checkFunction(context, sourceCode, node, node.params);
|
|
116
|
+
},
|
|
117
|
+
FunctionDeclaration(node): void {
|
|
118
|
+
checkFunction(context, sourceCode, node, node.params);
|
|
119
|
+
},
|
|
120
|
+
ExportNamedDeclaration(node): void {
|
|
121
|
+
const declaration = node.declaration;
|
|
122
|
+
|
|
123
|
+
if (
|
|
124
|
+
declaration !== null &&
|
|
125
|
+
(declaration.type === "FunctionDeclaration" || declaration.type === "TSDeclareFunction")
|
|
126
|
+
) {
|
|
127
|
+
checkFunction(context, sourceCode, node, declaration.params);
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
TSAbstractMethodDefinition(node): void {
|
|
131
|
+
checkFunction(context, sourceCode, node, node.value.params);
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
export default jsdocParamOrder;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { defineRule } from "vite-plus/lint/plugins";
|
|
2
|
+
|
|
3
|
+
import type { JSDocLine } from "./jsdoc-utils.js";
|
|
4
|
+
import { getJSDocLines, hasExampleOrFence } from "./jsdoc-utils.js";
|
|
5
|
+
|
|
6
|
+
interface Entry {
|
|
7
|
+
groupId: number;
|
|
8
|
+
line: JSDocLine;
|
|
9
|
+
lineIndex: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function getBlankPrefix(line: JSDocLine): string {
|
|
13
|
+
return line.prefix.trimEnd();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* description とタグ、タググループの間の空行を揃えます。
|
|
18
|
+
*/
|
|
19
|
+
const jsdocTagLines = defineRule({
|
|
20
|
+
meta: {
|
|
21
|
+
docs: {
|
|
22
|
+
description: "Require consistent blank lines between JSDoc tag groups.",
|
|
23
|
+
},
|
|
24
|
+
type: "layout",
|
|
25
|
+
fixable: "whitespace",
|
|
26
|
+
messages: {
|
|
27
|
+
missingBlankLine: "Expected a blank line between JSDoc tag groups.",
|
|
28
|
+
unexpectedBlankLine: "Unexpected blank line in a JSDoc tag group.",
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
create(context) {
|
|
32
|
+
const sourceCode = context.sourceCode;
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
"Program:exit"(): void {
|
|
36
|
+
for (const comment of sourceCode.getAllComments()) {
|
|
37
|
+
if (comment.type !== "Block") {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const lines = getJSDocLines(comment, sourceCode);
|
|
42
|
+
|
|
43
|
+
if (lines === null || lines.length < 3 || hasExampleOrFence(lines)) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const opening = lines[0];
|
|
48
|
+
const closing = lines.at(-1);
|
|
49
|
+
|
|
50
|
+
if (
|
|
51
|
+
opening === undefined ||
|
|
52
|
+
closing === undefined ||
|
|
53
|
+
opening.content.trim() !== "" ||
|
|
54
|
+
closing.content.trim() !== ""
|
|
55
|
+
) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const contentLines = lines.slice(1, -1);
|
|
60
|
+
const entries: Entry[] = [];
|
|
61
|
+
|
|
62
|
+
for (const [index, line] of contentLines.entries()) {
|
|
63
|
+
if (line.content.trim() !== "") {
|
|
64
|
+
entries.push({
|
|
65
|
+
line,
|
|
66
|
+
groupId: -1,
|
|
67
|
+
lineIndex: index,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (entries.length === 0 || !entries.some((entry) => entry.line.tagName !== null)) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let firstTagIndex = -1;
|
|
77
|
+
let currentGroup = -1;
|
|
78
|
+
let currentTag: string | null = null;
|
|
79
|
+
|
|
80
|
+
for (const [index, entry] of entries.entries()) {
|
|
81
|
+
if (entry.line.tagName === null) {
|
|
82
|
+
if (currentTag === null) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
entry.groupId = currentGroup;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (firstTagIndex === -1) {
|
|
91
|
+
firstTagIndex = index;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (currentTag === null || entry.line.tagName !== currentTag) {
|
|
95
|
+
currentGroup += 1;
|
|
96
|
+
currentTag = entry.line.tagName;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
entry.groupId = currentGroup;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (firstTagIndex === -1) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const descriptionExists = firstTagIndex > 0;
|
|
107
|
+
const result: string[] = [opening.raw];
|
|
108
|
+
let previousEntry: Entry | null = null;
|
|
109
|
+
let missingBlankLine = false;
|
|
110
|
+
|
|
111
|
+
for (const entry of entries) {
|
|
112
|
+
let desiredBlankLines: number;
|
|
113
|
+
let originalBlankLines: number;
|
|
114
|
+
|
|
115
|
+
if (previousEntry === null) {
|
|
116
|
+
originalBlankLines = entry.lineIndex;
|
|
117
|
+
desiredBlankLines = 0;
|
|
118
|
+
} else {
|
|
119
|
+
originalBlankLines = entry.lineIndex - previousEntry.lineIndex - 1;
|
|
120
|
+
|
|
121
|
+
if (previousEntry.groupId === -1 && entry.groupId === -1) {
|
|
122
|
+
desiredBlankLines = originalBlankLines;
|
|
123
|
+
} else if (previousEntry.groupId === -1) {
|
|
124
|
+
desiredBlankLines = descriptionExists ? 1 : 0;
|
|
125
|
+
} else {
|
|
126
|
+
desiredBlankLines = previousEntry.groupId === entry.groupId ? 0 : 1;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (desiredBlankLines > originalBlankLines) {
|
|
131
|
+
missingBlankLine = true;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const blankPrefix = getBlankPrefix(entry.line);
|
|
135
|
+
|
|
136
|
+
for (let count = 0; count < desiredBlankLines; count += 1) {
|
|
137
|
+
result.push(blankPrefix);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
result.push(entry.line.raw);
|
|
141
|
+
previousEntry = entry;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
result.push(closing.raw);
|
|
145
|
+
|
|
146
|
+
const replacement = result.join("\n");
|
|
147
|
+
|
|
148
|
+
if (replacement === sourceCode.getText(comment)) {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
context.report({
|
|
153
|
+
fix(fixer) {
|
|
154
|
+
return fixer.replaceText(comment, replacement);
|
|
155
|
+
},
|
|
156
|
+
loc: comment.loc,
|
|
157
|
+
messageId: missingBlankLine ? "missingBlankLine" : "unexpectedBlankLine",
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
export default jsdocTagLines;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { defineRule } from "vite-plus/lint/plugins";
|
|
2
|
+
|
|
3
|
+
import { getJSDocLines, hasExampleOrFence } from "./jsdoc-utils.js";
|
|
4
|
+
|
|
5
|
+
const TAG_ORDER: Record<string, number> = {
|
|
6
|
+
see: 70,
|
|
7
|
+
beta: 65,
|
|
8
|
+
note: 50,
|
|
9
|
+
param: 10,
|
|
10
|
+
throws: 40,
|
|
11
|
+
default: 20,
|
|
12
|
+
returns: 30,
|
|
13
|
+
internal: 65,
|
|
14
|
+
template: 5,
|
|
15
|
+
typeParam: 5,
|
|
16
|
+
deprecated: 60,
|
|
17
|
+
};
|
|
18
|
+
const UNKNOWN_TAG_ORDER = 100;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* JSDoc のタグの順序を統一します。
|
|
22
|
+
*/
|
|
23
|
+
const jsdocTagOrder = defineRule({
|
|
24
|
+
meta: {
|
|
25
|
+
docs: {
|
|
26
|
+
description: "Enforce a consistent order of JSDoc tags.",
|
|
27
|
+
},
|
|
28
|
+
type: "suggestion",
|
|
29
|
+
messages: {
|
|
30
|
+
order:
|
|
31
|
+
"JSDoc tags must be ordered as follows: @param, @default, @returns, @throws, @note, @deprecated, @see.",
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
create(context) {
|
|
35
|
+
const sourceCode = context.sourceCode;
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
"Program:exit"(): void {
|
|
39
|
+
for (const comment of sourceCode.getAllComments()) {
|
|
40
|
+
if (comment.type !== "Block") {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const lines = getJSDocLines(comment, sourceCode);
|
|
45
|
+
|
|
46
|
+
if (lines === null || hasExampleOrFence(lines)) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let previousOrder: number | null = null;
|
|
51
|
+
|
|
52
|
+
for (const line of lines) {
|
|
53
|
+
if (line.tagName === null) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const order = TAG_ORDER[line.tagName] ?? UNKNOWN_TAG_ORDER;
|
|
58
|
+
|
|
59
|
+
if (previousOrder !== null && order < previousOrder) {
|
|
60
|
+
context.report({
|
|
61
|
+
loc: comment.loc,
|
|
62
|
+
messageId: "order",
|
|
63
|
+
});
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
previousOrder = order;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
export default jsdocTagOrder;
|