@lewishowles/lint-config 0.2.0 → 0.3.0
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/README.md +79 -64
- package/base.json +57 -57
- package/comments/plugin.js +30 -0
- package/comments/rules/block-comments.js +70 -0
- package/comments/rules/configured-api-calls.js +137 -0
- package/comments/rules/function-documentation.js +111 -0
- package/comments/rules/jsdoc-tag-formatting.js +70 -0
- package/comments/rules/line-comments.js +86 -0
- package/comments/rules/max-line-length.js +159 -0
- package/comments/rules/placement.js +295 -0
- package/comments/rules/sentence-punctuation.js +275 -0
- package/comments/rules/variable-declarations.js +71 -0
- package/comments/rules/vue-component-documentation.js +169 -0
- package/comments/rules/vue-emit-documentation.js +123 -0
- package/comments/rules/vue-prop-documentation.js +224 -0
- package/comments/utils/documentation.js +321 -0
- package/comments/utils/jsdoc.js +756 -0
- package/comments/utils/source.js +312 -0
- package/comments/utils/vue-macro.js +70 -0
- package/comments/utils/wrap.js +118 -0
- package/comments.json +22 -0
- package/package.json +11 -3
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { addTerminalPunctuation, capitaliseSentence, formatSentence } from "../utils/wrap.js";
|
|
2
|
+
import { formatJSDocPunctuation, isJSDoc } from "../utils/jsdoc.js";
|
|
3
|
+
import {
|
|
4
|
+
getCommentText,
|
|
5
|
+
getLineCommentGroups,
|
|
6
|
+
getLineIndent,
|
|
7
|
+
getNewline,
|
|
8
|
+
replaceMinimalComment,
|
|
9
|
+
} from "../utils/source.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Replace only the value of a line comment.
|
|
13
|
+
*
|
|
14
|
+
* @param {object} sourceCode
|
|
15
|
+
* The Oxlint source code object.
|
|
16
|
+
* @param {object} comment
|
|
17
|
+
* The line comment token.
|
|
18
|
+
* @param {string} value
|
|
19
|
+
* The replacement comment value.
|
|
20
|
+
*
|
|
21
|
+
* @returns {string}
|
|
22
|
+
* The replacement comment text.
|
|
23
|
+
*/
|
|
24
|
+
function replaceLineCommentValue(sourceCode, comment, value) {
|
|
25
|
+
// The comment's raw source text.
|
|
26
|
+
const commentText = getCommentText(sourceCode, comment);
|
|
27
|
+
|
|
28
|
+
return `${commentText.slice(0, 2)}${value}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Format the prose text in a standard block-comment line.
|
|
33
|
+
*
|
|
34
|
+
* @param {string} line
|
|
35
|
+
* The block-comment line.
|
|
36
|
+
* @param {function} formatProse
|
|
37
|
+
* The formatter for the line's prose.
|
|
38
|
+
*
|
|
39
|
+
* @returns {string}
|
|
40
|
+
* The formatted block-comment line.
|
|
41
|
+
*/
|
|
42
|
+
function formatBlockCommentLine(line, formatProse) {
|
|
43
|
+
// The line's leading `*` decoration, when present.
|
|
44
|
+
const marker = line.match(/^\s*\*\s?/);
|
|
45
|
+
|
|
46
|
+
if (marker === null) {
|
|
47
|
+
return line;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return `${marker[0]}${formatProse(line.slice(marker[0].length).trim())}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Return a sentence-formatted line-comment group.
|
|
55
|
+
*
|
|
56
|
+
* @param {object} sourceCode
|
|
57
|
+
* The Oxlint source code object.
|
|
58
|
+
* @param {object[]} comments
|
|
59
|
+
* The adjacent line comments.
|
|
60
|
+
*
|
|
61
|
+
* @returns {object|null}
|
|
62
|
+
* The first and last replacements, or null when no sentence needs work.
|
|
63
|
+
*/
|
|
64
|
+
function formatLineCommentGroup(sourceCode, comments) {
|
|
65
|
+
// The group's first comment token.
|
|
66
|
+
const firstComment = comments[0];
|
|
67
|
+
// The group's last comment token.
|
|
68
|
+
const lastComment = comments.at(-1);
|
|
69
|
+
// The first comment's undecorated text.
|
|
70
|
+
const firstText = firstComment.value.trim();
|
|
71
|
+
|
|
72
|
+
if (firstText === "" || firstText.startsWith("@")) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// The first comment's value, capitalised or fully sentence-formatted.
|
|
77
|
+
const firstValue =
|
|
78
|
+
comments.length === 1
|
|
79
|
+
? formatSentence(firstComment.value)
|
|
80
|
+
: capitaliseSentence(firstComment.value);
|
|
81
|
+
|
|
82
|
+
// The last comment's value, punctuated to close the sentence.
|
|
83
|
+
const lastValue = comments.length === 1 ? firstValue : addTerminalPunctuation(lastComment.value);
|
|
84
|
+
// The replacement comment text for the first and last comments.
|
|
85
|
+
const firstReplacement = replaceLineCommentValue(sourceCode, firstComment, firstValue);
|
|
86
|
+
// The replacement comment text for the last comment.
|
|
87
|
+
const lastReplacement = replaceLineCommentValue(sourceCode, lastComment, lastValue);
|
|
88
|
+
|
|
89
|
+
if (
|
|
90
|
+
firstReplacement === getCommentText(sourceCode, firstComment) &&
|
|
91
|
+
lastReplacement === getCommentText(sourceCode, lastComment)
|
|
92
|
+
) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return { firstComment, firstReplacement, lastComment, lastReplacement };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Format prose in an ordinary block comment.
|
|
101
|
+
*
|
|
102
|
+
* @param {object} sourceCode
|
|
103
|
+
* The Oxlint source code object.
|
|
104
|
+
* @param {object} comment
|
|
105
|
+
* The block comment token.
|
|
106
|
+
*
|
|
107
|
+
* @returns {string}
|
|
108
|
+
* The sentence-formatted comment text.
|
|
109
|
+
*/
|
|
110
|
+
function formatOrdinaryBlockComment(sourceCode, comment) {
|
|
111
|
+
// The comment's raw source text.
|
|
112
|
+
const commentText = getCommentText(sourceCode, comment);
|
|
113
|
+
// The indentation the comment's lines are aligned to.
|
|
114
|
+
const indentation = getLineIndent(sourceCode, comment.range[0]);
|
|
115
|
+
// The comment body, stripped of its /* */ delimiters.
|
|
116
|
+
const content = commentText.slice(2, -2).trim();
|
|
117
|
+
|
|
118
|
+
if (indentation === null || content === "" || /^(?:eslint|oxlint|istanbul|c8)-/.test(content)) {
|
|
119
|
+
return commentText;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (!commentText.includes("\n") && !commentText.includes("\r")) {
|
|
123
|
+
return `/* ${formatSentence(content)} */`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// The comment's individual source lines.
|
|
127
|
+
const lines = commentText.split(/\r\n|\n|\r/);
|
|
128
|
+
|
|
129
|
+
// The indexes of lines carrying prose, excluding the delimiter lines.
|
|
130
|
+
const proseLineIndexes = lines
|
|
131
|
+
.slice(1, -1)
|
|
132
|
+
.map((line, index) => ({ index: index + 1, text: line.replace(/^\s*\*?\s?/, "").trim() }))
|
|
133
|
+
.filter((line) => line.text !== "")
|
|
134
|
+
.map((line) => line.index);
|
|
135
|
+
|
|
136
|
+
if (lines[0] === "/*" && lines.at(-1).trim() === "*/" && proseLineIndexes.length > 0) {
|
|
137
|
+
// The comment lines, formatted in place.
|
|
138
|
+
const formattedLines = [...lines];
|
|
139
|
+
// The first and last prose line indexes, which start and end the sentence.
|
|
140
|
+
const firstProseLine = proseLineIndexes[0];
|
|
141
|
+
// The last prose line index, which ends the sentence.
|
|
142
|
+
const lastProseLine = proseLineIndexes.at(-1);
|
|
143
|
+
|
|
144
|
+
formattedLines[firstProseLine] = formatBlockCommentLine(
|
|
145
|
+
formattedLines[firstProseLine],
|
|
146
|
+
capitaliseSentence,
|
|
147
|
+
);
|
|
148
|
+
formattedLines[lastProseLine] = formatBlockCommentLine(
|
|
149
|
+
formattedLines[lastProseLine],
|
|
150
|
+
addTerminalPunctuation,
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
return formattedLines.join(getNewline(sourceCode.text));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// The comment's prose, joined into a single paragraph.
|
|
157
|
+
const paragraphs = content
|
|
158
|
+
.split(/\r\n|\n|\r/)
|
|
159
|
+
.map((line) => line.replace(/^\s*\*?\s?/, "").trim())
|
|
160
|
+
.filter(Boolean)
|
|
161
|
+
.join(" ");
|
|
162
|
+
|
|
163
|
+
return ["/*", `${indentation} * ${formatSentence(paragraphs)}`, `${indentation} */`].join(
|
|
164
|
+
getNewline(sourceCode.text),
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Create the complete-sentence comment rule.
|
|
170
|
+
*
|
|
171
|
+
* @returns {object}
|
|
172
|
+
* The Oxlint rule definition.
|
|
173
|
+
*/
|
|
174
|
+
export default {
|
|
175
|
+
meta: {
|
|
176
|
+
docs: { description: "Format existing comments as complete sentences." },
|
|
177
|
+
fixable: "code",
|
|
178
|
+
type: "layout",
|
|
179
|
+
},
|
|
180
|
+
/**
|
|
181
|
+
* Create the rule's node visitors.
|
|
182
|
+
*
|
|
183
|
+
* @param {object} context
|
|
184
|
+
* The Oxlint rule context.
|
|
185
|
+
*
|
|
186
|
+
* @returns {object}
|
|
187
|
+
* The visitor functions for this rule.
|
|
188
|
+
*/
|
|
189
|
+
createOnce(context) {
|
|
190
|
+
return {
|
|
191
|
+
/**
|
|
192
|
+
* Format every comment in the file as a complete sentence.
|
|
193
|
+
*/
|
|
194
|
+
Program() {
|
|
195
|
+
for (const commentGroup of getLineCommentGroups(context.sourceCode)) {
|
|
196
|
+
// The group's replacement text, or null when it already reads as a sentence.
|
|
197
|
+
const formattedGroup = formatLineCommentGroup(context.sourceCode, commentGroup);
|
|
198
|
+
|
|
199
|
+
if (formattedGroup) {
|
|
200
|
+
context.report({
|
|
201
|
+
/**
|
|
202
|
+
* Apply the formatted replacement to the comment group.
|
|
203
|
+
*
|
|
204
|
+
* @param {object} fixer
|
|
205
|
+
* The Oxlint fixer.
|
|
206
|
+
*
|
|
207
|
+
* @returns {object[]}
|
|
208
|
+
* The fixes to apply.
|
|
209
|
+
*/
|
|
210
|
+
fix: (fixer) => {
|
|
211
|
+
// The fixes to apply, starting with the first comment's replacement.
|
|
212
|
+
const fixes = [
|
|
213
|
+
replaceMinimalComment(
|
|
214
|
+
fixer,
|
|
215
|
+
formattedGroup.firstComment,
|
|
216
|
+
getCommentText(context.sourceCode, formattedGroup.firstComment),
|
|
217
|
+
formattedGroup.firstReplacement,
|
|
218
|
+
),
|
|
219
|
+
];
|
|
220
|
+
|
|
221
|
+
if (formattedGroup.lastComment.range[0] !== formattedGroup.firstComment.range[0]) {
|
|
222
|
+
fixes.push(
|
|
223
|
+
replaceMinimalComment(
|
|
224
|
+
fixer,
|
|
225
|
+
formattedGroup.lastComment,
|
|
226
|
+
getCommentText(context.sourceCode, formattedGroup.lastComment),
|
|
227
|
+
formattedGroup.lastReplacement,
|
|
228
|
+
),
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return fixes;
|
|
233
|
+
},
|
|
234
|
+
message: "Comment text must be a complete sentence.",
|
|
235
|
+
node: formattedGroup.firstComment,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
for (const comment of context.sourceCode.getAllComments()) {
|
|
241
|
+
if (comment.type !== "Block") {
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// The comment's raw source text.
|
|
246
|
+
const commentText = getCommentText(context.sourceCode, comment);
|
|
247
|
+
|
|
248
|
+
// The comment, sentence-formatted using the JSDoc or ordinary-block formatter.
|
|
249
|
+
const formattedComment = isJSDoc(commentText)
|
|
250
|
+
? formatJSDocPunctuation(context.sourceCode, comment)
|
|
251
|
+
: formatOrdinaryBlockComment(context.sourceCode, comment);
|
|
252
|
+
|
|
253
|
+
if (formattedComment !== commentText) {
|
|
254
|
+
context.report({
|
|
255
|
+
/**
|
|
256
|
+
* Apply the sentence-formatted replacement to the comment.
|
|
257
|
+
*
|
|
258
|
+
* @param {object} fixer
|
|
259
|
+
* The Oxlint fixer.
|
|
260
|
+
*
|
|
261
|
+
* @returns {object}
|
|
262
|
+
* The fix to apply.
|
|
263
|
+
*/
|
|
264
|
+
fix: (fixer) => {
|
|
265
|
+
return replaceMinimalComment(fixer, comment, commentText, formattedComment);
|
|
266
|
+
},
|
|
267
|
+
message: "Comment text must be a complete sentence.",
|
|
268
|
+
node: comment,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
},
|
|
275
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { hasImmediateLineComment } from "../utils/source.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Return whether a declaration is inside a loop header.
|
|
5
|
+
*
|
|
6
|
+
* @param {object} node
|
|
7
|
+
* The variable declaration node.
|
|
8
|
+
*
|
|
9
|
+
* @returns {boolean}
|
|
10
|
+
* Whether the declaration is exempt from the rule.
|
|
11
|
+
*/
|
|
12
|
+
function isLoopHeaderDeclaration(node) {
|
|
13
|
+
// Inspects the declaration's parent to identify loop headers.
|
|
14
|
+
const parent = node.parent;
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
(parent.type === "ForStatement" && parent.init === node) ||
|
|
18
|
+
((parent.type === "ForInStatement" || parent.type === "ForOfStatement") && parent.left === node)
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Create the variable-declaration comment rule.
|
|
24
|
+
*
|
|
25
|
+
* @returns {object}
|
|
26
|
+
* The Oxlint rule definition.
|
|
27
|
+
*/
|
|
28
|
+
export default {
|
|
29
|
+
meta: {
|
|
30
|
+
docs: { description: "Require comments before variable declarations." },
|
|
31
|
+
type: "suggestion",
|
|
32
|
+
},
|
|
33
|
+
/**
|
|
34
|
+
* Create the rule's node visitors.
|
|
35
|
+
*
|
|
36
|
+
* @param {object} context
|
|
37
|
+
* The Oxlint rule context.
|
|
38
|
+
*
|
|
39
|
+
* @returns {object}
|
|
40
|
+
* The visitor functions for this rule.
|
|
41
|
+
*/
|
|
42
|
+
createOnce(context) {
|
|
43
|
+
return {
|
|
44
|
+
/**
|
|
45
|
+
* Check a const or let declaration for a preceding comment.
|
|
46
|
+
*
|
|
47
|
+
* @param {object} node
|
|
48
|
+
* The variable declaration node.
|
|
49
|
+
*/
|
|
50
|
+
VariableDeclaration(node) {
|
|
51
|
+
if ((node.kind !== "const" && node.kind !== "let") || isLoopHeaderDeclaration(node)) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!hasImmediateLineComment(context.sourceCode, node)) {
|
|
56
|
+
context.report({
|
|
57
|
+
message: "Variable declarations require an immediately preceding line comment.",
|
|
58
|
+
node,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (node.declarations.length > 1) {
|
|
63
|
+
context.report({
|
|
64
|
+
message: "Declare one variable per declaration statement.",
|
|
65
|
+
node,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
};
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { isDirectiveComment } from "../utils/source.js";
|
|
3
|
+
|
|
4
|
+
// Captures the opening tag's attributes separately, so the setup attribute
|
|
5
|
+
// can be tested without the tag being available from Oxlint's extracted AST.
|
|
6
|
+
const scriptBlockPattern =
|
|
7
|
+
/(?<openingTag><script\b(?<attributes>[^>]*)>)(?<content>[\s\S]*?)<\/script\s*>/gi;
|
|
8
|
+
|
|
9
|
+
// Only a standalone setup attribute qualifies, avoiding matches such as
|
|
10
|
+
// data-setup or setup-mode.
|
|
11
|
+
const setupAttributePattern = /(?:^|\s)setup(?:\s|=|$)/i;
|
|
12
|
+
// Match only indentation and one line break, so documentation sits directly
|
|
13
|
+
// after the script tag.
|
|
14
|
+
const immediateCommentGapPattern = /^[ \t]*(?:\r?\n[ \t]*)?$/;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Read raw Vue source because extracted script text cannot identify script
|
|
18
|
+
* setup.
|
|
19
|
+
*
|
|
20
|
+
* @param {object} context
|
|
21
|
+
* The Oxlint rule context.
|
|
22
|
+
*
|
|
23
|
+
* @returns {string|null}
|
|
24
|
+
* The Vue source, when the physical filename can be read.
|
|
25
|
+
*/
|
|
26
|
+
function getVueSource(context) {
|
|
27
|
+
try {
|
|
28
|
+
// The extracted script text does not include the component markup.
|
|
29
|
+
// Read the opening tag from the Vue file instead.
|
|
30
|
+
const physicalFilename = context.physicalFilename;
|
|
31
|
+
|
|
32
|
+
if (typeof physicalFilename !== "string" || !physicalFilename.endsWith(".vue")) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!existsSync(physicalFilename)) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return readFileSync(physicalFilename, "utf8");
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Get raw Vue script blocks in source order.
|
|
48
|
+
*
|
|
49
|
+
* @param {object} context
|
|
50
|
+
* The Oxlint rule context.
|
|
51
|
+
*
|
|
52
|
+
* @returns {RegExpMatchArray[]}
|
|
53
|
+
* The Vue script blocks, or an empty array when the source is unavailable.
|
|
54
|
+
*/
|
|
55
|
+
function getScriptBlocks(context) {
|
|
56
|
+
// Component markup is only available from the physical Vue file.
|
|
57
|
+
const vueSource = getVueSource(context);
|
|
58
|
+
|
|
59
|
+
return vueSource ? Array.from(vueSource.matchAll(scriptBlockPattern)) : [];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Find the raw script block matching the current Program's extracted text.
|
|
64
|
+
*
|
|
65
|
+
* @param {object} context
|
|
66
|
+
* The rule context for the extracted script block.
|
|
67
|
+
*
|
|
68
|
+
* @returns {RegExpMatchArray|undefined}
|
|
69
|
+
* The matching raw script block, when one is found.
|
|
70
|
+
*/
|
|
71
|
+
function findScriptBlock(context) {
|
|
72
|
+
// Re-read on every call rather than caching, since this file's other
|
|
73
|
+
// script blocks may not have been visited yet or ever in this run.
|
|
74
|
+
const scriptBlocks = getScriptBlocks(context);
|
|
75
|
+
|
|
76
|
+
return scriptBlocks.find(
|
|
77
|
+
(scriptBlock) => scriptBlock.groups.content.trim() === context.sourceCode.text.trim(),
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Check whether a script block uses Vue's setup attribute.
|
|
83
|
+
*
|
|
84
|
+
* @param {RegExpMatchArray} scriptBlock
|
|
85
|
+
* The raw Vue script block.
|
|
86
|
+
*
|
|
87
|
+
* @returns {boolean}
|
|
88
|
+
* Whether the script block is a script setup block.
|
|
89
|
+
*/
|
|
90
|
+
function isScriptSetupBlock(scriptBlock) {
|
|
91
|
+
// Inspect only the opening tag, so setup code cannot affect the block type.
|
|
92
|
+
const scriptAttributes = scriptBlock.groups?.attributes ?? "";
|
|
93
|
+
|
|
94
|
+
return setupAttributePattern.test(scriptAttributes);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Check whether a Vue script setup block starts with a documentation comment.
|
|
99
|
+
*
|
|
100
|
+
* @param {object} context
|
|
101
|
+
* The rule context for the extracted script block.
|
|
102
|
+
* @param {RegExpMatchArray|undefined} scriptBlock
|
|
103
|
+
* The script block matched to the current component's entry point.
|
|
104
|
+
*
|
|
105
|
+
* @returns {boolean}
|
|
106
|
+
* Whether the component has the required documentation comment.
|
|
107
|
+
*/
|
|
108
|
+
function hasComponentDocumentation(context, scriptBlock) {
|
|
109
|
+
if (!scriptBlock || !isScriptSetupBlock(scriptBlock)) {
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// This Program visitor runs once per script block, so sourceCode is scoped
|
|
114
|
+
// to the current block and [0] cannot pick up a comment from another one.
|
|
115
|
+
const comment = context.sourceCode.getAllComments()[0];
|
|
116
|
+
|
|
117
|
+
// Lint directives alter rule execution but cannot document a component.
|
|
118
|
+
if (comment?.type !== "Block" || isDirectiveComment(comment)) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// A blank line would separate the component documentation from its entry point.
|
|
123
|
+
const commentGap = context.sourceCode.text.slice(0, comment.range[0]);
|
|
124
|
+
|
|
125
|
+
return immediateCommentGapPattern.test(commentGap);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export default {
|
|
129
|
+
meta: {
|
|
130
|
+
docs: { description: "Require documentation for Vue script setup components." },
|
|
131
|
+
type: "suggestion",
|
|
132
|
+
},
|
|
133
|
+
/**
|
|
134
|
+
* Create the checks that inspect each script block.
|
|
135
|
+
*
|
|
136
|
+
* @param {object} context
|
|
137
|
+
* The Oxlint rule context.
|
|
138
|
+
*
|
|
139
|
+
* @returns {object}
|
|
140
|
+
* The script-block checks for this rule.
|
|
141
|
+
*/
|
|
142
|
+
createOnce(context) {
|
|
143
|
+
return {
|
|
144
|
+
/**
|
|
145
|
+
* Check the current component's script setup block for documentation.
|
|
146
|
+
*
|
|
147
|
+
* @param {object} node
|
|
148
|
+
* The entry point parsed from the current script block.
|
|
149
|
+
*/
|
|
150
|
+
Program(node) {
|
|
151
|
+
// createOnce builds this visitor once for the whole run, and a
|
|
152
|
+
// single file's script blocks are not necessarily visited
|
|
153
|
+
// consecutively, so the matching block is looked up fresh on each
|
|
154
|
+
// call rather than tracked with shared state.
|
|
155
|
+
const scriptBlock = findScriptBlock(context);
|
|
156
|
+
|
|
157
|
+
if (hasComponentDocumentation(context, scriptBlock)) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
context.report({
|
|
162
|
+
message:
|
|
163
|
+
"Vue script setup components require a documentation block after the opening tag.",
|
|
164
|
+
node: node.body[0] ?? node,
|
|
165
|
+
});
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
},
|
|
169
|
+
};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { getCommentNeighbours, isDirectiveComment, isLeadingComment } from "../utils/source.js";
|
|
2
|
+
import { getObjectArgument, getObjectProperties, isNamedCall } from "../utils/vue-macro.js";
|
|
3
|
+
|
|
4
|
+
// Matches the single newline and indentation allowed between a comment and an
|
|
5
|
+
// event.
|
|
6
|
+
const immediateCommentGapPattern = /^\r?\n[ \t]*$/;
|
|
7
|
+
|
|
8
|
+
// The message shared by all undocumented runtime events.
|
|
9
|
+
const missingCommentMessage =
|
|
10
|
+
"Vue emit declarations require an immediately preceding block comment.";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Return whether the current lint target is a physical Vue single-file
|
|
14
|
+
* component.
|
|
15
|
+
*
|
|
16
|
+
* @param {object} context
|
|
17
|
+
* The Oxlint rule context.
|
|
18
|
+
*
|
|
19
|
+
* @returns {boolean}
|
|
20
|
+
* Whether the current file has a Vue filename.
|
|
21
|
+
*/
|
|
22
|
+
function isVueFile(context) {
|
|
23
|
+
try {
|
|
24
|
+
// Use the physical path because the AST does not identify Vue files.
|
|
25
|
+
const physicalFilename = context.physicalFilename;
|
|
26
|
+
|
|
27
|
+
return typeof physicalFilename === "string" && physicalFilename.endsWith(".vue");
|
|
28
|
+
} catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Return whether a property has an immediately preceding block comment.
|
|
35
|
+
*
|
|
36
|
+
* @param {object} sourceCode
|
|
37
|
+
* The Oxlint source code object.
|
|
38
|
+
* @param {object} property
|
|
39
|
+
* The property to inspect.
|
|
40
|
+
*
|
|
41
|
+
* @returns {boolean}
|
|
42
|
+
* Whether the property has the required documentation comment.
|
|
43
|
+
*/
|
|
44
|
+
function hasBlockComment(sourceCode, property) {
|
|
45
|
+
// Finds the closest preceding comment.
|
|
46
|
+
const comment = sourceCode
|
|
47
|
+
.getAllComments()
|
|
48
|
+
.findLast((candidate) => candidate.range[1] <= property.range[0]);
|
|
49
|
+
|
|
50
|
+
if (comment?.type !== "Block" || isDirectiveComment(comment)) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Checks the comments immediately around the property.
|
|
55
|
+
const { next, previous } = getCommentNeighbours(sourceCode, comment);
|
|
56
|
+
// The source text between the comment and the property.
|
|
57
|
+
const gap = sourceCode.text.slice(comment.range[1], property.range[0]);
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
next?.range[0] === property.range[0] &&
|
|
61
|
+
isLeadingComment(sourceCode, comment, previous) &&
|
|
62
|
+
immediateCommentGapPattern.test(gap)
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Report missing comments for runtime emit events.
|
|
68
|
+
*
|
|
69
|
+
* @param {object} context
|
|
70
|
+
* The Oxlint rule context.
|
|
71
|
+
* @param {object} objectExpression
|
|
72
|
+
* The runtime emits object to inspect.
|
|
73
|
+
*/
|
|
74
|
+
function reportEventDocumentation(context, objectExpression) {
|
|
75
|
+
for (const property of getObjectProperties(objectExpression)) {
|
|
76
|
+
// Checks whether this event has the required documentation.
|
|
77
|
+
const hasDocumentation = hasBlockComment(context.sourceCode, property);
|
|
78
|
+
|
|
79
|
+
if (!hasDocumentation) {
|
|
80
|
+
context.report({ message: missingCommentMessage, node: property });
|
|
81
|
+
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export default {
|
|
88
|
+
meta: {
|
|
89
|
+
docs: { description: "Require block comments for Vue runtime emits." },
|
|
90
|
+
type: "suggestion",
|
|
91
|
+
},
|
|
92
|
+
/**
|
|
93
|
+
* Create the rule's node visitors.
|
|
94
|
+
*
|
|
95
|
+
* @param {object} context
|
|
96
|
+
* The Oxlint rule context.
|
|
97
|
+
*
|
|
98
|
+
* @returns {object}
|
|
99
|
+
* The call-expression visitor for this rule.
|
|
100
|
+
*/
|
|
101
|
+
createOnce(context) {
|
|
102
|
+
return {
|
|
103
|
+
/**
|
|
104
|
+
* Check runtime defineEmits calls for event documentation.
|
|
105
|
+
*
|
|
106
|
+
* @param {object} node
|
|
107
|
+
* The call expression to inspect.
|
|
108
|
+
*/
|
|
109
|
+
CallExpression(node) {
|
|
110
|
+
if (!isVueFile(context) || !isNamedCall(node, "defineEmits")) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Array and type-only forms have no runtime properties to document.
|
|
115
|
+
const emitsObject = getObjectArgument(node, 0);
|
|
116
|
+
|
|
117
|
+
if (emitsObject) {
|
|
118
|
+
reportEventDocumentation(context, emitsObject);
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
},
|
|
123
|
+
};
|