@lewishowles/lint-config 0.2.0 → 0.4.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.
@@ -0,0 +1,349 @@
1
+ import { getJSDocContent, isJSDoc } from "./jsdoc.js";
2
+ import { getPropertyName } from "./vue-macro.js";
3
+
4
+ import {
5
+ getCommentNeighbours,
6
+ getCommentText,
7
+ isDirectiveComment,
8
+ isLeadingComment,
9
+ } from "./source.js";
10
+
11
+ /**
12
+ * Return the declaration node that owns the documentation position.
13
+ *
14
+ * @param {object} node
15
+ * The declaration node.
16
+ *
17
+ * @returns {object}
18
+ * The node immediately following the documentation block.
19
+ */
20
+ export function getDocumentationNode(node) {
21
+ // Walks up through export wrappers to find the documented position.
22
+ let documentationNode = node;
23
+
24
+ while (
25
+ documentationNode.parent?.type === "ExportDefaultDeclaration" ||
26
+ documentationNode.parent?.type === "ExportNamedDeclaration"
27
+ ) {
28
+ documentationNode = documentationNode.parent;
29
+ }
30
+
31
+ return documentationNode;
32
+ }
33
+
34
+ /**
35
+ * Return whether a node is a function value, used to tell function-valued
36
+ * options and properties apart from named function declarations.
37
+ *
38
+ * @param {object} node
39
+ * The node to inspect.
40
+ *
41
+ * @returns {boolean}
42
+ * Whether the node is a function expression or arrow function.
43
+ */
44
+ export function isFunctionValue(node) {
45
+ return node?.type === "ArrowFunctionExpression" || node?.type === "FunctionExpression";
46
+ }
47
+
48
+ /**
49
+ * Return the JSDoc comment immediately preceding a node, when the comment
50
+ * qualifies as that node's documentation.
51
+ *
52
+ * @param {object} sourceCode
53
+ * The Oxlint source code object.
54
+ * @param {object} node
55
+ * The node to check for a preceding comment.
56
+ *
57
+ * @returns {object|null}
58
+ * The qualifying JSDoc comment, or null when none precedes the node.
59
+ */
60
+ function getDocumentationComment(sourceCode, node) {
61
+ // Finds the closest preceding comment.
62
+ const comment = sourceCode
63
+ .getAllComments()
64
+ .findLast((candidate) => candidate.range[1] <= node.range[0]);
65
+
66
+ if (comment?.type !== "Block" || isDirectiveComment(comment)) {
67
+ return null;
68
+ }
69
+
70
+ // Checks the comments immediately around the documented node.
71
+ const { next, previous } = getCommentNeighbours(sourceCode, comment);
72
+ // Confirms there is no blank line before the documented node.
73
+ const gap = sourceCode.text.slice(comment.range[1], node.range[0]);
74
+
75
+ if (
76
+ !next ||
77
+ next.range[0] > node.range[0] ||
78
+ next.range[1] > node.range[1] ||
79
+ !isLeadingComment(sourceCode, comment, previous) ||
80
+ !/^\r?\n[ \t]*$/.test(gap)
81
+ ) {
82
+ return null;
83
+ }
84
+
85
+ return isJSDoc(getCommentText(sourceCode, comment)) ? comment : null;
86
+ }
87
+
88
+ /**
89
+ * Return a default value's original source text.
90
+ *
91
+ * @param {object} sourceCode
92
+ * The Oxlint source code object.
93
+ * @param {object} node
94
+ * The default-value node.
95
+ *
96
+ * @returns {string}
97
+ * The default value's source text.
98
+ */
99
+ function getDefaultValue(sourceCode, node) {
100
+ return sourceCode.text.slice(node.range[0], node.range[1]);
101
+ }
102
+
103
+ /**
104
+ * Return JSDoc paths for the properties in an object pattern.
105
+ *
106
+ * @param {object} sourceCode
107
+ * The Oxlint source code object.
108
+ * @param {object} node
109
+ * The object-pattern node.
110
+ * @param {string} parentPath
111
+ * The path for the containing object.
112
+ *
113
+ * @returns {string[]}
114
+ * The required JSDoc parameter paths.
115
+ */
116
+ function getObjectPatternPaths(sourceCode, node, parentPath) {
117
+ // Collects the parent path alongside each nested property path.
118
+ const paths = [parentPath];
119
+
120
+ for (const property of node.properties) {
121
+ if (property.type !== "Property") {
122
+ continue;
123
+ }
124
+
125
+ // Skips properties whose key cannot be represented in a JSDoc path.
126
+ const propertyName = getPropertyName(property);
127
+
128
+ if (propertyName === null) {
129
+ continue;
130
+ }
131
+
132
+ // Builds the dotted path used to document this property.
133
+ const propertyPath = `${parentPath}.${propertyName}`;
134
+ // Inspects the property's value to decide how it should be documented.
135
+ const value = property.value;
136
+
137
+ if (value.type === "ObjectPattern") {
138
+ paths.push(...getObjectPatternPaths(sourceCode, value, propertyPath));
139
+ continue;
140
+ }
141
+
142
+ if (value.type === "AssignmentPattern") {
143
+ paths.push(`[${propertyPath}=${getDefaultValue(sourceCode, value.right)}]`);
144
+ continue;
145
+ }
146
+
147
+ paths.push(propertyPath);
148
+ }
149
+
150
+ return paths;
151
+ }
152
+
153
+ /**
154
+ * Return JSDoc paths for one declared parameter.
155
+ *
156
+ * @param {object} sourceCode
157
+ * The Oxlint source code object.
158
+ * @param {object} node
159
+ * The parameter node.
160
+ *
161
+ * @returns {string[]}
162
+ * The required JSDoc parameter paths.
163
+ */
164
+ function getParameterPaths(sourceCode, node) {
165
+ if (node.type === "Identifier") {
166
+ return [node.name];
167
+ }
168
+
169
+ if (node.type === "RestElement" && node.argument.type === "Identifier") {
170
+ return [node.argument.name];
171
+ }
172
+
173
+ if (node.type === "ObjectPattern") {
174
+ return getObjectPatternPaths(sourceCode, node, "options");
175
+ }
176
+
177
+ if (node.type === "AssignmentPattern") {
178
+ return getParameterPaths(sourceCode, node.left);
179
+ }
180
+
181
+ return [];
182
+ }
183
+
184
+ /**
185
+ * Return JSDoc parameter names from a documentation block.
186
+ *
187
+ * @param {object} sourceCode
188
+ * The Oxlint source code object.
189
+ * @param {object} comment
190
+ * The JSDoc comment token.
191
+ *
192
+ * @returns {Set<string>}
193
+ * The documented parameter names.
194
+ */
195
+ function getDocumentedParameters(sourceCode, comment) {
196
+ // Splits the JSDoc block into its individual lines.
197
+ const content = getJSDocContent(getCommentText(sourceCode, comment));
198
+ // Collects the parameter paths documented by @param tags.
199
+ const names = new Set();
200
+
201
+ for (const line of content) {
202
+ // Matches an @param tag and captures its documented path.
203
+ const match = line.trim().match(/^@param(?:\s+\{[^}]+\})?\s+(\[[^\]]+\]|\S+)/);
204
+
205
+ if (match) {
206
+ names.add(match[1]);
207
+ }
208
+ }
209
+
210
+ return names;
211
+ }
212
+
213
+ /**
214
+ * Return whether a function body contains a node type outside nested functions.
215
+ *
216
+ * @param {object} node
217
+ * The node to inspect.
218
+ * @param {string} targetType
219
+ * The statement type to find.
220
+ * @param {Function} matches
221
+ * Return whether a matching statement meets the requirement.
222
+ *
223
+ * @returns {boolean}
224
+ * Whether the target statement appears in the body.
225
+ */
226
+ function containsStatement(node, targetType, matches = () => true) {
227
+ if (!node || typeof node !== "object") {
228
+ return false;
229
+ }
230
+
231
+ if (node.type === targetType) {
232
+ return matches(node);
233
+ }
234
+
235
+ if (isFunctionValue(node) || node.type === "FunctionDeclaration") {
236
+ return false;
237
+ }
238
+
239
+ for (const [key, value] of Object.entries(node)) {
240
+ if (key === "parent") {
241
+ continue;
242
+ }
243
+
244
+ if (Array.isArray(value)) {
245
+ if (value.some((item) => containsStatement(item, targetType, matches))) {
246
+ return true;
247
+ }
248
+ } else if (containsStatement(value, targetType, matches)) {
249
+ return true;
250
+ }
251
+ }
252
+
253
+ return false;
254
+ }
255
+
256
+ /**
257
+ * Return whether a function explicitly returns a value.
258
+ *
259
+ * @param {object} node
260
+ * The function node to inspect.
261
+ *
262
+ * @returns {boolean}
263
+ * Whether the function returns a value.
264
+ */
265
+ function hasValueReturn(node) {
266
+ if (node.type === "ArrowFunctionExpression" && node.body.type !== "BlockStatement") {
267
+ return true;
268
+ }
269
+
270
+ return containsStatement(
271
+ node.body,
272
+ "ReturnStatement",
273
+ (statement) => statement.argument !== null,
274
+ );
275
+ }
276
+
277
+ /**
278
+ * Report missing documentation requirements for one function.
279
+ *
280
+ * @param {object} context
281
+ * The Oxlint rule context.
282
+ * @param {object} node
283
+ * The documentation-position node.
284
+ * @param {object} functionNode
285
+ * The function node to inspect.
286
+ * @param {object} [options]
287
+ * Optional reporting options.
288
+ * @param {boolean} [options.requiresReturns=true]
289
+ * Whether a returned value requires an @returns tag.
290
+ * @param {string} [options.subject="Functions"]
291
+ * The declaration kind named in report messages, such as "Constructors".
292
+ */
293
+ export function reportFunctionDocumentation(context, node, functionNode, options = {}) {
294
+ // Defaults to "Functions" when the caller names no declaration kind.
295
+ const subject = options.subject ?? "Functions";
296
+
297
+ // Finds the JSDoc block documenting this function, when present.
298
+ const comment = getDocumentationComment(context.sourceCode, node);
299
+
300
+ if (!comment) {
301
+ context.report({
302
+ message: `${subject} require an immediately preceding JSDoc block.`,
303
+ node,
304
+ });
305
+
306
+ return;
307
+ }
308
+
309
+ // Reads the parameter paths already documented by @param tags.
310
+ const documentedParameters = getDocumentedParameters(context.sourceCode, comment);
311
+
312
+ // Derives the parameter paths the function actually requires.
313
+ const parameterPaths = functionNode.params.flatMap((parameter) =>
314
+ getParameterPaths(context.sourceCode, parameter),
315
+ );
316
+
317
+ for (const path of parameterPaths) {
318
+ // The optional JSDoc spelling for this parameter path.
319
+ const optionalPath = `[${path}]`;
320
+
321
+ if (!documentedParameters.has(path) && !documentedParameters.has(optionalPath)) {
322
+ context.report({
323
+ message: `${subject} require an @param for ${path}.`,
324
+ node,
325
+ });
326
+ }
327
+ }
328
+
329
+ // Splits the JSDoc block into its individual lines.
330
+ const content = getJSDocContent(getCommentText(context.sourceCode, comment));
331
+ // Checks whether the return value is documented.
332
+ const hasReturns = content.some((line) => /^@returns\b/.test(line.trim()));
333
+ // Checks whether thrown errors are documented.
334
+ const hasThrows = content.some((line) => /^@throws\b/.test(line.trim()));
335
+
336
+ if (options.requiresReturns !== false && hasValueReturn(functionNode) && !hasReturns) {
337
+ context.report({
338
+ message: `${subject} that return a value require an @returns tag.`,
339
+ node,
340
+ });
341
+ }
342
+
343
+ if (containsStatement(functionNode.body, "ThrowStatement") && !hasThrows) {
344
+ context.report({
345
+ message: `${subject} that throw require an @throws tag.`,
346
+ node,
347
+ });
348
+ }
349
+ }