@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 +15 -0
- package/README.md +8 -0
- package/comments/plugin.js +2 -0
- package/comments/rules/class-documentation.js +131 -0
- package/comments/rules/configured-api-calls.js +9 -3
- package/comments/rules/placement.js +19 -24
- package/comments/rules/variable-declarations.js +21 -4
- package/comments/utils/documentation.js +32 -4
- package/comments/utils/source.js +34 -0
- package/comments.json +1 -0
- package/package.json +2 -1
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.
|
package/comments/plugin.js
CHANGED
|
@@ -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
|
-
|
|
48
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
185
|
-
const
|
|
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],
|
|
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 (
|
|
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
|
|
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
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
345
|
+
message: `${subject} that throw require an @throws tag.`,
|
|
318
346
|
node,
|
|
319
347
|
});
|
|
320
348
|
}
|
package/comments/utils/source.js
CHANGED
|
@@ -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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lewishowles/lint-config",
|
|
3
|
-
"version": "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",
|