@lewishowles/lint-config 0.3.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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,15 @@
1
+ # Changelog
2
+
3
+ ## 0.4.0: 2026-08-28
4
+
5
+ ### New rules
6
+
7
+ - Added `comments/class-documentation`: requires a block comment on class declarations and const-assigned class expressions, JSDoc on constructors and ordinary methods, return-aware JSDoc on getters and setters, and line comments on instance and static fields. Ships in the opt-in `comments.json` layer.
8
+
9
+ ### Fixes
10
+
11
+ - A directive comment (`eslint-`, `oxlint-`, and similar) sitting between a doc comment and its code is now reported, instead of the doc being treated as attached to the code.
12
+ - Documentation placed before an exported declaration is now recognised.
13
+ - `using` and `await using` declarations now require a preceding line comment, like `const` and `let`.
14
+
15
+ Earlier versions predate this changelog; see the git history for their changes.
package/README.md CHANGED
@@ -92,6 +92,14 @@ The `comments/configured-api-calls` rule requires an immediately preceding line
92
92
  }
93
93
  ```
94
94
 
95
+ The `comments/class-documentation` rule requires an immediately preceding block comment before class declarations and const-assigned class expressions. Constructors, methods, getters, and setters require full JSDoc; constructors never need an `@returns` tag, and getters and setters need one only when they return a value. Instance and static fields require an immediately preceding line comment.
96
+
97
+ ### `vp check` configuration
98
+
99
+ The `vp check` and `vp lint` commands read Oxlint settings only from a `lint` block in `vite.config.js`; they do not read `.oxlintrc.json` directly. If `vite.config.js` is missing, they silently use an unrelated default configuration. You may still see plausible warnings and exit codes, but none of your rules are applied.
100
+
101
+ Each consuming repo needs a `vite.config.js` with a `lint` block built from the config layer(s) and the repo's `.oxlintrc.json`, imported as JSON. Verify the setup with `vp lint --print-config <file>` and check that your real values, not defaults, are active.
102
+
95
103
  ## Customising
96
104
 
97
105
  Your project's `.oxlintrc.json` can override rules, add ignore patterns, add overrides, or add plugins on top of the shared layer.
@@ -1,4 +1,5 @@
1
1
  import blockComments from "./rules/block-comments.js";
2
+ import classDocumentation from "./rules/class-documentation.js";
2
3
  import configuredApiCalls from "./rules/configured-api-calls.js";
3
4
  import functionDocumentation from "./rules/function-documentation.js";
4
5
  import jsdocTagFormatting from "./rules/jsdoc-tag-formatting.js";
@@ -15,6 +16,7 @@ export default {
15
16
  meta: { name: "comments" },
16
17
  rules: {
17
18
  "block-comments": blockComments,
19
+ "class-documentation": classDocumentation,
18
20
  "configured-api-calls": configuredApiCalls,
19
21
  "function-documentation": functionDocumentation,
20
22
  "jsdoc-tag-formatting": jsdocTagFormatting,
@@ -0,0 +1,131 @@
1
+ import { getDocumentationNode, reportFunctionDocumentation } from "../utils/documentation.js";
2
+ import { hasImmediateBlockComment, hasImmediateLineComment } from "../utils/source.js";
3
+
4
+ /**
5
+ * Create the class-documentation rule.
6
+ *
7
+ * @returns {object}
8
+ * The Oxlint rule definition.
9
+ */
10
+ export default {
11
+ meta: {
12
+ docs: {
13
+ description:
14
+ "Require documentation for classes, their methods, getters, setters, and fields.",
15
+ },
16
+ type: "suggestion",
17
+ },
18
+ /**
19
+ * Create the rule's node visitors.
20
+ *
21
+ * @param {object} context
22
+ * The Oxlint rule context.
23
+ *
24
+ * @returns {object}
25
+ * The visitor functions for this rule.
26
+ */
27
+ createOnce(context) {
28
+ return {
29
+ /**
30
+ * Check a class declaration for a preceding block comment.
31
+ *
32
+ * @param {object} node
33
+ * The class declaration node.
34
+ */
35
+ ClassDeclaration(node) {
36
+ // Resolves any export wrapper before checking for documentation.
37
+ const documentationNode = getDocumentationNode(node);
38
+
39
+ if (!hasImmediateBlockComment(context.sourceCode, documentationNode)) {
40
+ context.report({
41
+ message: "Classes require an immediately preceding block comment.",
42
+ node: documentationNode,
43
+ });
44
+ }
45
+ },
46
+ /**
47
+ * Check class methods for required JSDoc blocks and tags. Constructors are
48
+ * exempt from the @returns requirement.
49
+ *
50
+ * @param {object} node
51
+ * The method-definition node.
52
+ */
53
+ MethodDefinition(node) {
54
+ if (node.kind === "constructor") {
55
+ reportFunctionDocumentation(context, node, node.value, {
56
+ requiresReturns: false,
57
+ subject: "Constructors",
58
+ });
59
+
60
+ return;
61
+ }
62
+
63
+ if (node.kind === "get") {
64
+ reportFunctionDocumentation(context, node, node.value, {
65
+ subject: "Getters",
66
+ });
67
+
68
+ return;
69
+ }
70
+
71
+ if (node.kind === "set") {
72
+ reportFunctionDocumentation(context, node, node.value, {
73
+ subject: "Setters",
74
+ });
75
+
76
+ return;
77
+ }
78
+
79
+ if (node.kind === "method") {
80
+ reportFunctionDocumentation(context, node, node.value, {
81
+ subject: "Methods",
82
+ });
83
+ }
84
+ },
85
+ /**
86
+ * Check instance and static fields for a preceding line comment.
87
+ *
88
+ * @param {object} node
89
+ * The property-definition node.
90
+ */
91
+ PropertyDefinition(node) {
92
+ if (hasImmediateLineComment(context.sourceCode, node)) {
93
+ return;
94
+ }
95
+
96
+ // Static and instance fields share the requirement; only the wording differs.
97
+ const message = node.static
98
+ ? "Static fields require an immediately preceding line comment."
99
+ : "Instance fields require an immediately preceding line comment.";
100
+
101
+ context.report({ message, node });
102
+ },
103
+ /**
104
+ * Check a const class expression for a preceding block comment.
105
+ *
106
+ * @param {object} node
107
+ * The variable declarator node.
108
+ */
109
+ VariableDeclarator(node) {
110
+ if (
111
+ node.parent?.kind !== "const" ||
112
+ node.parent.declarations.length !== 1 ||
113
+ node.id?.type !== "Identifier" ||
114
+ node.init?.type !== "ClassExpression"
115
+ ) {
116
+ return;
117
+ }
118
+
119
+ // Resolves any export wrapper before checking for documentation.
120
+ const documentationNode = getDocumentationNode(node.parent);
121
+
122
+ if (!hasImmediateBlockComment(context.sourceCode, documentationNode)) {
123
+ context.report({
124
+ message: "Classes require an immediately preceding block comment.",
125
+ node: documentationNode,
126
+ });
127
+ }
128
+ },
129
+ };
130
+ },
131
+ };
@@ -1,3 +1,4 @@
1
+ import { getDocumentationNode } from "../utils/documentation.js";
1
2
  import { hasImmediateLineComment } from "../utils/source.js";
2
3
 
3
4
  // The built-in APIs that require a preceding comment by default.
@@ -44,9 +45,14 @@ function hasDocumentedVariableDeclaration(sourceCode, node) {
44
45
  // The declarator's enclosing variable declaration.
45
46
  const declaration = declarator.parent;
46
47
 
47
- return (
48
- declaration?.type === "VariableDeclaration" && hasImmediateLineComment(sourceCode, declaration)
49
- );
48
+ if (declaration?.type !== "VariableDeclaration") {
49
+ return false;
50
+ }
51
+
52
+ // Resolves any export wrapper before checking for documentation.
53
+ const documentationNode = getDocumentationNode(declaration);
54
+
55
+ return hasImmediateLineComment(sourceCode, documentationNode);
50
56
  }
51
57
 
52
58
  /**
@@ -5,26 +5,10 @@ import {
5
5
  getLineIndent,
6
6
  getLineStart,
7
7
  getNewline,
8
+ isDirectiveComment,
8
9
  isLeadingComment,
9
10
  } from "../utils/source.js";
10
11
 
11
- /**
12
- * Return the end-to-token gap for a leading comment.
13
- *
14
- * @param {object} sourceCode
15
- * The Oxlint source code object.
16
- * @param {object} comment
17
- * The comment token.
18
- * @param {object} next
19
- * The next source token.
20
- *
21
- * @returns {string}
22
- * The source gap after the comment.
23
- */
24
- function getCommentToTokenGap(sourceCode, comment, next) {
25
- return sourceCode.text.slice(comment.range[1], next.range[0]);
26
- }
27
-
28
12
  /**
29
13
  * Return continuation comments indexed by their group leader.
30
14
  *
@@ -160,7 +144,7 @@ function getCommentIndentationFixes(
160
144
  }
161
145
 
162
146
  /**
163
- * Return the replacement that places a final leading comment against its code.
147
+ * Return the replacement that closes the gap after a final leading comment.
164
148
  *
165
149
  * @param {object} sourceCode
166
150
  * The Oxlint source code object.
@@ -169,7 +153,7 @@ function getCommentIndentationFixes(
169
153
  * @param {object} next
170
154
  * The documented source token.
171
155
  * @param {object|undefined} followingComment
172
- * The next comment token.
156
+ * The comment after this one in source order, when there is one.
173
157
  * @param {string} expectedIndent
174
158
  * The documented code's indentation.
175
159
  *
@@ -177,16 +161,23 @@ function getCommentIndentationFixes(
177
161
  * The gap replacement, or null when none is needed.
178
162
  */
179
163
  function getCommentGapFix(sourceCode, comment, next, followingComment, expectedIndent) {
180
- if (followingComment !== undefined && followingComment.range[0] <= next.range[0]) {
164
+ // Whether another comment sits between this one and its documented code.
165
+ const followingCommentIntervenes =
166
+ followingComment !== undefined && followingComment.range[0] <= next.range[0];
167
+
168
+ if (followingCommentIntervenes && !isDirectiveComment(followingComment)) {
181
169
  return null;
182
170
  }
183
171
 
184
- // The source text currently between the comment and its documented code.
185
- const gap = getCommentToTokenGap(sourceCode, comment, next);
172
+ // Stop at an intervening directive so the fix range never overlaps it.
173
+ const gapEnd = followingCommentIntervenes ? followingComment.range[0] : next.range[0];
174
+
175
+ // What currently follows the comment, up to the code or directive.
176
+ const gap = sourceCode.text.slice(comment.range[1], gapEnd);
186
177
  // The gap the documented code's indentation requires.
187
178
  const desiredGap = `${getNewline(sourceCode.text)}${expectedIndent}`;
188
179
 
189
- return gap === desiredGap ? null : { range: [comment.range[1], next.range[0]], text: desiredGap };
180
+ return gap === desiredGap ? null : { range: [comment.range[1], gapEnd], text: desiredGap };
190
181
  }
191
182
 
192
183
  /**
@@ -225,7 +216,11 @@ export default {
225
216
  );
226
217
 
227
218
  for (const [index, comment] of comments.entries()) {
228
- if (comment.type === "Shebang" || continuationComments.has(comment)) {
219
+ if (
220
+ comment.type === "Shebang" ||
221
+ isDirectiveComment(comment) ||
222
+ continuationComments.has(comment)
223
+ ) {
229
224
  continue;
230
225
  }
231
226
 
@@ -1,5 +1,9 @@
1
+ import { getDocumentationNode } from "../utils/documentation.js";
1
2
  import { hasImmediateLineComment } from "../utils/source.js";
2
3
 
4
+ // Declaration kinds that require an immediately preceding line comment.
5
+ const documentedKinds = new Set(["await using", "const", "let", "using"]);
6
+
3
7
  /**
4
8
  * Return whether a declaration is inside a loop header.
5
9
  *
@@ -42,20 +46,33 @@ export default {
42
46
  createOnce(context) {
43
47
  return {
44
48
  /**
45
- * Check a const or let declaration for a preceding comment.
49
+ * Check a variable declaration for a preceding line comment.
46
50
  *
47
51
  * @param {object} node
48
52
  * The variable declaration node.
49
53
  */
50
54
  VariableDeclaration(node) {
51
- if ((node.kind !== "const" && node.kind !== "let") || isLoopHeaderDeclaration(node)) {
55
+ if (!documentedKinds.has(node.kind) || isLoopHeaderDeclaration(node)) {
56
+ return;
57
+ }
58
+
59
+ // A lone const class expression is documented by class-documentation.
60
+ if (
61
+ node.kind === "const" &&
62
+ node.declarations.length === 1 &&
63
+ node.declarations[0].id?.type === "Identifier" &&
64
+ node.declarations[0].init?.type === "ClassExpression"
65
+ ) {
52
66
  return;
53
67
  }
54
68
 
55
- if (!hasImmediateLineComment(context.sourceCode, node)) {
69
+ // Resolves any export wrapper before checking for documentation.
70
+ const documentationNode = getDocumentationNode(node);
71
+
72
+ if (!hasImmediateLineComment(context.sourceCode, documentationNode)) {
56
73
  context.report({
57
74
  message: "Variable declarations require an immediately preceding line comment.",
58
- node,
75
+ node: documentationNode,
59
76
  });
60
77
  }
61
78
 
@@ -8,6 +8,29 @@ import {
8
8
  isLeadingComment,
9
9
  } from "./source.js";
10
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
+
11
34
  /**
12
35
  * Return whether a node is a function value, used to tell function-valued
13
36
  * options and properties apart from named function declarations.
@@ -264,14 +287,19 @@ function hasValueReturn(node) {
264
287
  * Optional reporting options.
265
288
  * @param {boolean} [options.requiresReturns=true]
266
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".
267
292
  */
268
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
+
269
297
  // Finds the JSDoc block documenting this function, when present.
270
298
  const comment = getDocumentationComment(context.sourceCode, node);
271
299
 
272
300
  if (!comment) {
273
301
  context.report({
274
- message: "Functions require an immediately preceding JSDoc block.",
302
+ message: `${subject} require an immediately preceding JSDoc block.`,
275
303
  node,
276
304
  });
277
305
 
@@ -292,7 +320,7 @@ export function reportFunctionDocumentation(context, node, functionNode, options
292
320
 
293
321
  if (!documentedParameters.has(path) && !documentedParameters.has(optionalPath)) {
294
322
  context.report({
295
- message: `Functions require an @param for ${path}.`,
323
+ message: `${subject} require an @param for ${path}.`,
296
324
  node,
297
325
  });
298
326
  }
@@ -307,14 +335,14 @@ export function reportFunctionDocumentation(context, node, functionNode, options
307
335
 
308
336
  if (options.requiresReturns !== false && hasValueReturn(functionNode) && !hasReturns) {
309
337
  context.report({
310
- message: "Functions that return a value require an @returns tag.",
338
+ message: `${subject} that return a value require an @returns tag.`,
311
339
  node,
312
340
  });
313
341
  }
314
342
 
315
343
  if (containsStatement(functionNode.body, "ThrowStatement") && !hasThrows) {
316
344
  context.report({
317
- message: "Functions that throw require an @throws tag.",
345
+ message: `${subject} that throw require an @throws tag.`,
318
346
  node,
319
347
  });
320
348
  }
@@ -310,3 +310,37 @@ export function hasImmediateLineComment(sourceCode, node) {
310
310
  /^\r?\n[ \t]*$/.test(gap)
311
311
  );
312
312
  }
313
+
314
+ /**
315
+ * Return whether a block comment immediately documents a source node.
316
+ *
317
+ * @param {object} sourceCode
318
+ * The Oxlint source code object.
319
+ * @param {object} node
320
+ * The source node.
321
+ *
322
+ * @returns {boolean}
323
+ * Whether an ordinary block comment immediately precedes the node.
324
+ */
325
+ export function hasImmediateBlockComment(sourceCode, node) {
326
+ // Finds the closest preceding comment.
327
+ const comment = sourceCode
328
+ .getAllComments()
329
+ .findLast((candidate) => candidate.range[1] <= node.range[0]);
330
+
331
+ if (comment?.type !== "Block" || isDirectiveComment(comment)) {
332
+ return false;
333
+ }
334
+
335
+ // Checks the comments immediately around the node.
336
+ const { next, previous } = getCommentNeighbours(sourceCode, comment);
337
+
338
+ // Confirms there is no blank line before the node.
339
+ const gap = sourceCode.text.slice(comment.range[1], node.range[0]);
340
+
341
+ return (
342
+ next?.range[0] === node.range[0] &&
343
+ isLeadingComment(sourceCode, comment, previous) &&
344
+ /^\r?\n[ \t]*$/.test(gap)
345
+ );
346
+ }
package/comments.json CHANGED
@@ -7,6 +7,7 @@
7
7
  ],
8
8
  "rules": {
9
9
  "comments/block-comments": "error",
10
+ "comments/class-documentation": "error",
10
11
  "comments/configured-api-calls": "error",
11
12
  "comments/function-documentation": "error",
12
13
  "comments/jsdoc-tag-formatting": "error",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lewishowles/lint-config",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Shared oxlint configuration for Lewis Howles projects",
5
5
  "keywords": [
6
6
  "config",
@@ -19,6 +19,7 @@
19
19
  "url": "git+https://github.com/lewishowles/lint-config.git"
20
20
  },
21
21
  "files": [
22
+ "CHANGELOG.md",
22
23
  "base.json",
23
24
  "comments",
24
25
  "comments.json",