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