@lewishowles/lint-config 0.3.0 → 0.5.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 +25 -0
- package/README.md +43 -3
- package/comments/plugin.js +2 -0
- package/comments/rules/block-comments.js +2 -1
- package/comments/rules/class-documentation.js +134 -0
- package/comments/rules/configured-api-calls.js +13 -5
- package/comments/rules/jsdoc-tag-formatting.js +2 -1
- package/comments/rules/max-line-length.js +14 -4
- package/comments/rules/placement.js +25 -27
- package/comments/rules/sentence-punctuation.js +12 -6
- package/comments/rules/variable-declarations.js +48 -4
- package/comments/rules/vue-component-documentation.js +6 -3
- package/comments/rules/vue-emit-documentation.js +2 -1
- package/comments/utils/documentation.js +32 -4
- package/comments/utils/jsdoc.js +6 -4
- package/comments/utils/source.js +49 -0
- package/comments.json +10 -1
- package/package.json +4 -2
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.5.0: 2026-09-11
|
|
4
|
+
|
|
5
|
+
### Changes
|
|
6
|
+
|
|
7
|
+
- `comments/variable-declarations`: add a `rootOnly` option; `comments.json` enables it for test files so only root-level variables need comments there. If you override `comments.json` rules, concatenate the test-file override (see README).
|
|
8
|
+
|
|
9
|
+
### Fixes
|
|
10
|
+
|
|
11
|
+
- Measure comment width in display columns, with tabs counted as four, so indented comments wrap and validate against the visible 80-column limit.
|
|
12
|
+
|
|
13
|
+
## 0.4.0: 2026-08-28
|
|
14
|
+
|
|
15
|
+
### New rules
|
|
16
|
+
|
|
17
|
+
- 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.
|
|
18
|
+
|
|
19
|
+
### Fixes
|
|
20
|
+
|
|
21
|
+
- 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.
|
|
22
|
+
- Documentation placed before an exported declaration is now recognised.
|
|
23
|
+
- `using` and `await using` declarations now require a preceding line comment, like `const` and `let`.
|
|
24
|
+
|
|
25
|
+
Earlier versions predate this changelog; see the git history for their changes.
|
package/README.md
CHANGED
|
@@ -92,6 +92,46 @@ The `comments/configured-api-calls` rule requires an immediately preceding line
|
|
|
92
92
|
}
|
|
93
93
|
```
|
|
94
94
|
|
|
95
|
+
The `comments/variable-declarations` rule accepts a `rootOnly` option to require comments only before root-level `const`, `let`, and `using` declarations. The comments layer already enables `rootOnly` for test files matched by `**/*.test.*`, `**/*.spec.*`, and `**/test/**`; other files keep the default of `false`. Override it for another scope or to change the behaviour:
|
|
96
|
+
|
|
97
|
+
```json
|
|
98
|
+
{
|
|
99
|
+
"rules": {
|
|
100
|
+
"comments/variable-declarations": ["error", { "rootOnly": true }]
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
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.
|
|
106
|
+
|
|
107
|
+
### `vp check` configuration
|
|
108
|
+
|
|
109
|
+
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.
|
|
110
|
+
|
|
111
|
+
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. Concatenate the `overrides` arrays from every imported layer before the local overrides so shared file-specific rules still apply. Verify the setup with `vp lint --print-config <file>` and check that your real values, not defaults, are active.
|
|
112
|
+
|
|
113
|
+
```js
|
|
114
|
+
import { defineConfig } from "vite-plus";
|
|
115
|
+
import lintConfigBase from "@lewishowles/lint-config/base.json" with { type: "json" };
|
|
116
|
+
import lintConfigComments from "@lewishowles/lint-config/comments.json" with { type: "json" };
|
|
117
|
+
import oxlintrc from "./.oxlintrc.json" with { type: "json" };
|
|
118
|
+
|
|
119
|
+
const lint = {
|
|
120
|
+
...lintConfigBase,
|
|
121
|
+
env: oxlintrc.env,
|
|
122
|
+
ignorePatterns: oxlintrc.ignorePatterns,
|
|
123
|
+
jsPlugins: [...lintConfigBase.jsPlugins, ...lintConfigComments.jsPlugins],
|
|
124
|
+
overrides: [
|
|
125
|
+
...(lintConfigBase.overrides ?? []),
|
|
126
|
+
...(lintConfigComments.overrides ?? []),
|
|
127
|
+
...(oxlintrc.overrides ?? []),
|
|
128
|
+
],
|
|
129
|
+
rules: { ...lintConfigBase.rules, ...lintConfigComments.rules },
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
export default defineConfig({ lint });
|
|
133
|
+
```
|
|
134
|
+
|
|
95
135
|
## Customising
|
|
96
136
|
|
|
97
137
|
Your project's `.oxlintrc.json` can override rules, add ignore patterns, add overrides, or add plugins on top of the shared layer.
|
|
@@ -122,7 +162,7 @@ Ignore patterns are project-specific, so they always live in your project config
|
|
|
122
162
|
|
|
123
163
|
### Adding overrides
|
|
124
164
|
|
|
125
|
-
Overrides are additive: shared overrides
|
|
165
|
+
Overrides are additive in the `vite.config.js` lint block: shared overrides still apply, and your local ones are appended.
|
|
126
166
|
|
|
127
167
|
```json
|
|
128
168
|
{
|
|
@@ -159,7 +199,7 @@ The base layer sorts named members within each import statement, but leaves decl
|
|
|
159
199
|
## What stays repo-local
|
|
160
200
|
|
|
161
201
|
- `ignorePatterns`, since every project has different build output and tool directories
|
|
162
|
-
- `overrides` for project-specific directories (e.g. `bin/**/*.js`, `src/cli/**/*.js`, `src/playwright/**/*.js`), since the file paths differ per project and can't be generalised
|
|
202
|
+
- `overrides` for project-specific directories (e.g. `bin/**/*.js`, `src/cli/**/*.js`, `src/playwright/**/*.js`), since the file paths differ per project and can't be generalised. The `vite.config.js` lint block appends these local entries after the shared layer overrides.
|
|
163
203
|
- Rule relaxations for specific file patterns (e.g. turning off `vite-plus/prefer-vite-plus-imports` in generated `.d.ts` files)
|
|
164
204
|
- Additional plugins, only for projects that need them
|
|
165
205
|
|
|
@@ -168,6 +208,6 @@ The base layer sorts named members within each import statement, but leaves decl
|
|
|
168
208
|
When a project's `.oxlintrc.json` extends a shared layer:
|
|
169
209
|
|
|
170
210
|
- **Rules** shallow-merge by key: your value wins for any rule defined in both
|
|
171
|
-
- **Overrides** are additive:
|
|
211
|
+
- **Overrides** are additive in the `vite.config.js` lint block: it concatenates shared layer and local `overrides` entries, including any `env` declared inside an override block
|
|
172
212
|
- **Plugins** are additive: both shared and local `plugins`/`jsPlugins` load, deduplicated
|
|
173
213
|
- **`env`, `globals`, and `ignorePatterns` don't merge through `extends` at all** (an open Oxlint bug), which is why the usage examples above redeclare `env`/`globals` directly. See [known limitations](docs/limitations.md) for the full detail, including the separate `vite-plus` caveat around resolving `extends` paths.
|
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,
|
|
@@ -40,7 +40,8 @@ export default {
|
|
|
40
40
|
continue;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
// The comment, with its block structure and delimiters
|
|
43
|
+
// The comment, with its block structure and delimiters
|
|
44
|
+
// normalised.
|
|
44
45
|
const formattedComment = formatJSDocBlockStructure(context.sourceCode, comment);
|
|
45
46
|
|
|
46
47
|
if (formattedComment === commentText) {
|
|
@@ -0,0 +1,134 @@
|
|
|
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
|
|
37
|
+
// documentation.
|
|
38
|
+
const documentationNode = getDocumentationNode(node);
|
|
39
|
+
|
|
40
|
+
if (!hasImmediateBlockComment(context.sourceCode, documentationNode)) {
|
|
41
|
+
context.report({
|
|
42
|
+
message: "Classes require an immediately preceding block comment.",
|
|
43
|
+
node: documentationNode,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
/**
|
|
48
|
+
* Check class methods for required JSDoc blocks and tags.
|
|
49
|
+
* Constructors are exempt from the @returns requirement.
|
|
50
|
+
*
|
|
51
|
+
* @param {object} node
|
|
52
|
+
* The method-definition node.
|
|
53
|
+
*/
|
|
54
|
+
MethodDefinition(node) {
|
|
55
|
+
if (node.kind === "constructor") {
|
|
56
|
+
reportFunctionDocumentation(context, node, node.value, {
|
|
57
|
+
requiresReturns: false,
|
|
58
|
+
subject: "Constructors",
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (node.kind === "get") {
|
|
65
|
+
reportFunctionDocumentation(context, node, node.value, {
|
|
66
|
+
subject: "Getters",
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (node.kind === "set") {
|
|
73
|
+
reportFunctionDocumentation(context, node, node.value, {
|
|
74
|
+
subject: "Setters",
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (node.kind === "method") {
|
|
81
|
+
reportFunctionDocumentation(context, node, node.value, {
|
|
82
|
+
subject: "Methods",
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
/**
|
|
87
|
+
* Check instance and static fields for a preceding line comment.
|
|
88
|
+
*
|
|
89
|
+
* @param {object} node
|
|
90
|
+
* The property-definition node.
|
|
91
|
+
*/
|
|
92
|
+
PropertyDefinition(node) {
|
|
93
|
+
if (hasImmediateLineComment(context.sourceCode, node)) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Static and instance fields share the requirement; only the
|
|
98
|
+
// wording differs.
|
|
99
|
+
const message = node.static
|
|
100
|
+
? "Static fields require an immediately preceding line comment."
|
|
101
|
+
: "Instance fields require an immediately preceding line comment.";
|
|
102
|
+
|
|
103
|
+
context.report({ message, node });
|
|
104
|
+
},
|
|
105
|
+
/**
|
|
106
|
+
* Check a const class expression for a preceding block comment.
|
|
107
|
+
*
|
|
108
|
+
* @param {object} node
|
|
109
|
+
* The variable declarator node.
|
|
110
|
+
*/
|
|
111
|
+
VariableDeclarator(node) {
|
|
112
|
+
if (
|
|
113
|
+
node.parent?.kind !== "const" ||
|
|
114
|
+
node.parent.declarations.length !== 1 ||
|
|
115
|
+
node.id?.type !== "Identifier" ||
|
|
116
|
+
node.init?.type !== "ClassExpression"
|
|
117
|
+
) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Resolves any export wrapper before checking for
|
|
122
|
+
// documentation.
|
|
123
|
+
const documentationNode = getDocumentationNode(node.parent);
|
|
124
|
+
|
|
125
|
+
if (!hasImmediateBlockComment(context.sourceCode, documentationNode)) {
|
|
126
|
+
context.report({
|
|
127
|
+
message: "Classes require an immediately preceding block comment.",
|
|
128
|
+
node: documentationNode,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
},
|
|
134
|
+
};
|
|
@@ -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
|
/**
|
|
@@ -111,8 +117,10 @@ export default {
|
|
|
111
117
|
* The call expression node.
|
|
112
118
|
*/
|
|
113
119
|
CallExpression(node) {
|
|
114
|
-
// Read fresh for every call: createOnce's visitor is shared
|
|
115
|
-
//
|
|
120
|
+
// Read fresh for every call: createOnce's visitor is shared
|
|
121
|
+
// across every file
|
|
122
|
+
// in the run, so caching this at closure-creation time would
|
|
123
|
+
// freeze the first
|
|
116
124
|
// file's options.
|
|
117
125
|
const configuredApis = getConfiguredApis(context);
|
|
118
126
|
|
|
@@ -40,7 +40,8 @@ export default {
|
|
|
40
40
|
continue;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
// The comment, with its tag spacing, order, and grouping
|
|
43
|
+
// The comment, with its tag spacing, order, and grouping
|
|
44
|
+
// normalised.
|
|
44
45
|
const formattedComment = formatJSDocTagFormatting(context.sourceCode, comment);
|
|
45
46
|
|
|
46
47
|
if (formattedComment === commentText) {
|
|
@@ -2,6 +2,7 @@ import { formatJSDocWrapping, isJSDoc } from "../utils/jsdoc.js";
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
getCommentText,
|
|
5
|
+
getDisplayWidth,
|
|
5
6
|
getLineIndent,
|
|
6
7
|
getNewline,
|
|
7
8
|
isDirectiveComment,
|
|
@@ -33,7 +34,7 @@ function formatLineComment(sourceCode, comment) {
|
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
// The available width, allowing for the indent and "// " prefix.
|
|
36
|
-
const width = maximumLineLength - indentation
|
|
37
|
+
const width = maximumLineLength - getDisplayWidth(indentation) - 3;
|
|
37
38
|
// The comment's undecorated text.
|
|
38
39
|
const text = comment.value.trim();
|
|
39
40
|
|
|
@@ -74,7 +75,7 @@ function formatBlockComment(sourceCode, comment) {
|
|
|
74
75
|
// The comment body, sentence-formatted.
|
|
75
76
|
const text = formatSentence(commentText.slice(2, -2).trim());
|
|
76
77
|
// The available width, allowing for the indent and " * " prefix.
|
|
77
|
-
const width = maximumLineLength - indentation
|
|
78
|
+
const width = maximumLineLength - getDisplayWidth(indentation) - 3;
|
|
78
79
|
// The comment body, rewrapped to the available width.
|
|
79
80
|
const lines = wrapWords(text, Math.max(1, width));
|
|
80
81
|
|
|
@@ -120,11 +121,20 @@ export default {
|
|
|
120
121
|
// The comment's individual source lines.
|
|
121
122
|
const lines = commentText.split(/\r\n|\n|\r/);
|
|
122
123
|
|
|
123
|
-
|
|
124
|
+
// The whitespace before the comment, absent when code
|
|
125
|
+
// precedes it.
|
|
126
|
+
const indentation = getLineIndent(context.sourceCode, comment.range[0]) ?? "";
|
|
127
|
+
// The comment lines as they appear on screen. The raw text
|
|
128
|
+
// omits the first line's indentation, so it is restored
|
|
129
|
+
// before measuring.
|
|
130
|
+
const displayLines = [`${indentation}${lines[0]}`, ...lines.slice(1)];
|
|
131
|
+
|
|
132
|
+
if (!displayLines.some((line) => getDisplayWidth(line) > maximumLineLength)) {
|
|
124
133
|
continue;
|
|
125
134
|
}
|
|
126
135
|
|
|
127
|
-
// The comment, rewrapped using the formatter matching its
|
|
136
|
+
// The comment, rewrapped using the formatter matching its
|
|
137
|
+
// type.
|
|
128
138
|
const formattedComment =
|
|
129
139
|
comment.type === "Line"
|
|
130
140
|
? formatLineComment(context.sourceCode, comment)
|
|
@@ -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
|
|
|
@@ -245,7 +240,8 @@ export default {
|
|
|
245
240
|
continue;
|
|
246
241
|
}
|
|
247
242
|
|
|
248
|
-
// The reindentation fixes for the comment and its
|
|
243
|
+
// The reindentation fixes for the comment and its
|
|
244
|
+
// continuations.
|
|
249
245
|
const fixes = getCommentIndentationFixes(
|
|
250
246
|
context.sourceCode,
|
|
251
247
|
comment,
|
|
@@ -254,10 +250,12 @@ export default {
|
|
|
254
250
|
expectedIndent,
|
|
255
251
|
);
|
|
256
252
|
|
|
257
|
-
// The next comment token, used to avoid overlapping gap
|
|
253
|
+
// The next comment token, used to avoid overlapping gap
|
|
254
|
+
// fixes.
|
|
258
255
|
const followingComment = comments[index + 1];
|
|
259
256
|
|
|
260
|
-
// The fix that closes the gap between the comment and its
|
|
257
|
+
// The fix that closes the gap between the comment and its
|
|
258
|
+
// code, when needed.
|
|
261
259
|
const gapFix = getCommentGapFix(
|
|
262
260
|
context.sourceCode,
|
|
263
261
|
comment,
|
|
@@ -136,7 +136,8 @@ function formatOrdinaryBlockComment(sourceCode, comment) {
|
|
|
136
136
|
if (lines[0] === "/*" && lines.at(-1).trim() === "*/" && proseLineIndexes.length > 0) {
|
|
137
137
|
// The comment lines, formatted in place.
|
|
138
138
|
const formattedLines = [...lines];
|
|
139
|
-
// The first and last prose line indexes, which start and end the
|
|
139
|
+
// The first and last prose line indexes, which start and end the
|
|
140
|
+
// sentence.
|
|
140
141
|
const firstProseLine = proseLineIndexes[0];
|
|
141
142
|
// The last prose line index, which ends the sentence.
|
|
142
143
|
const lastProseLine = proseLineIndexes.at(-1);
|
|
@@ -193,13 +194,15 @@ export default {
|
|
|
193
194
|
*/
|
|
194
195
|
Program() {
|
|
195
196
|
for (const commentGroup of getLineCommentGroups(context.sourceCode)) {
|
|
196
|
-
// The group's replacement text, or null when it already
|
|
197
|
+
// The group's replacement text, or null when it already
|
|
198
|
+
// reads as a sentence.
|
|
197
199
|
const formattedGroup = formatLineCommentGroup(context.sourceCode, commentGroup);
|
|
198
200
|
|
|
199
201
|
if (formattedGroup) {
|
|
200
202
|
context.report({
|
|
201
203
|
/**
|
|
202
|
-
* Apply the formatted replacement to the comment
|
|
204
|
+
* Apply the formatted replacement to the comment
|
|
205
|
+
* group.
|
|
203
206
|
*
|
|
204
207
|
* @param {object} fixer
|
|
205
208
|
* The Oxlint fixer.
|
|
@@ -208,7 +211,8 @@ export default {
|
|
|
208
211
|
* The fixes to apply.
|
|
209
212
|
*/
|
|
210
213
|
fix: (fixer) => {
|
|
211
|
-
// The fixes to apply, starting with the first
|
|
214
|
+
// The fixes to apply, starting with the first
|
|
215
|
+
// comment's replacement.
|
|
212
216
|
const fixes = [
|
|
213
217
|
replaceMinimalComment(
|
|
214
218
|
fixer,
|
|
@@ -245,7 +249,8 @@ export default {
|
|
|
245
249
|
// The comment's raw source text.
|
|
246
250
|
const commentText = getCommentText(context.sourceCode, comment);
|
|
247
251
|
|
|
248
|
-
// The comment, sentence-formatted using the JSDoc or
|
|
252
|
+
// The comment, sentence-formatted using the JSDoc or
|
|
253
|
+
// ordinary-block formatter.
|
|
249
254
|
const formattedComment = isJSDoc(commentText)
|
|
250
255
|
? formatJSDocPunctuation(context.sourceCode, comment)
|
|
251
256
|
: formatOrdinaryBlockComment(context.sourceCode, comment);
|
|
@@ -253,7 +258,8 @@ export default {
|
|
|
253
258
|
if (formattedComment !== commentText) {
|
|
254
259
|
context.report({
|
|
255
260
|
/**
|
|
256
|
-
* Apply the sentence-formatted replacement to the
|
|
261
|
+
* Apply the sentence-formatted replacement to the
|
|
262
|
+
* comment.
|
|
257
263
|
*
|
|
258
264
|
* @param {object} fixer
|
|
259
265
|
* The Oxlint fixer.
|
|
@@ -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
|
*
|
|
@@ -29,6 +33,18 @@ export default {
|
|
|
29
33
|
meta: {
|
|
30
34
|
docs: { description: "Require comments before variable declarations." },
|
|
31
35
|
type: "suggestion",
|
|
36
|
+
schema: [
|
|
37
|
+
{
|
|
38
|
+
type: "object",
|
|
39
|
+
properties: {
|
|
40
|
+
rootOnly: {
|
|
41
|
+
type: "boolean",
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
additionalProperties: false,
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
defaultOptions: [{ rootOnly: false }],
|
|
32
48
|
},
|
|
33
49
|
/**
|
|
34
50
|
* Create the rule's node visitors.
|
|
@@ -42,20 +58,48 @@ export default {
|
|
|
42
58
|
createOnce(context) {
|
|
43
59
|
return {
|
|
44
60
|
/**
|
|
45
|
-
* Check a
|
|
61
|
+
* Check a variable declaration for a preceding line comment.
|
|
46
62
|
*
|
|
47
63
|
* @param {object} node
|
|
48
64
|
* The variable declaration node.
|
|
49
65
|
*/
|
|
50
66
|
VariableDeclaration(node) {
|
|
51
|
-
if ((node.kind
|
|
67
|
+
if (!documentedKinds.has(node.kind) || isLoopHeaderDeclaration(node)) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// A lone const class expression is documented by
|
|
72
|
+
// class-documentation.
|
|
73
|
+
if (
|
|
74
|
+
node.kind === "const" &&
|
|
75
|
+
node.declarations.length === 1 &&
|
|
76
|
+
node.declarations[0].id?.type === "Identifier" &&
|
|
77
|
+
node.declarations[0].init?.type === "ClassExpression"
|
|
78
|
+
) {
|
|
52
79
|
return;
|
|
53
80
|
}
|
|
54
81
|
|
|
55
|
-
|
|
82
|
+
// The rule's resolved options for the file currently being
|
|
83
|
+
// visited.
|
|
84
|
+
const options = context.options?.[0];
|
|
85
|
+
|
|
86
|
+
// Resolves any export wrapper before checking for
|
|
87
|
+
// documentation.
|
|
88
|
+
const documentationNode = getDocumentationNode(node);
|
|
89
|
+
|
|
90
|
+
// With rootOnly, only declarations directly under the
|
|
91
|
+
// Program need a comment, so a nested variable inside
|
|
92
|
+
// a function is left alone.
|
|
93
|
+
const shouldCheckDocumentation =
|
|
94
|
+
!options?.rootOnly || documentationNode.parent?.type === "Program";
|
|
95
|
+
|
|
96
|
+
if (
|
|
97
|
+
shouldCheckDocumentation &&
|
|
98
|
+
!hasImmediateLineComment(context.sourceCode, documentationNode)
|
|
99
|
+
) {
|
|
56
100
|
context.report({
|
|
57
101
|
message: "Variable declarations require an immediately preceding line comment.",
|
|
58
|
-
node,
|
|
102
|
+
node: documentationNode,
|
|
59
103
|
});
|
|
60
104
|
}
|
|
61
105
|
|
|
@@ -119,7 +119,8 @@ function hasComponentDocumentation(context, scriptBlock) {
|
|
|
119
119
|
return false;
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
// A blank line would separate the component documentation from its entry
|
|
122
|
+
// A blank line would separate the component documentation from its entry
|
|
123
|
+
// point.
|
|
123
124
|
const commentGap = context.sourceCode.text.slice(0, comment.range[0]);
|
|
124
125
|
|
|
125
126
|
return immediateCommentGapPattern.test(commentGap);
|
|
@@ -142,7 +143,8 @@ export default {
|
|
|
142
143
|
createOnce(context) {
|
|
143
144
|
return {
|
|
144
145
|
/**
|
|
145
|
-
* Check the current component's script setup block for
|
|
146
|
+
* Check the current component's script setup block for
|
|
147
|
+
* documentation.
|
|
146
148
|
*
|
|
147
149
|
* @param {object} node
|
|
148
150
|
* The entry point parsed from the current script block.
|
|
@@ -150,7 +152,8 @@ export default {
|
|
|
150
152
|
Program(node) {
|
|
151
153
|
// createOnce builds this visitor once for the whole run, and a
|
|
152
154
|
// single file's script blocks are not necessarily visited
|
|
153
|
-
// consecutively, so the matching block is looked up fresh on
|
|
155
|
+
// consecutively, so the matching block is looked up fresh on
|
|
156
|
+
// each
|
|
154
157
|
// call rather than tracked with shared state.
|
|
155
158
|
const scriptBlock = findScriptBlock(context);
|
|
156
159
|
|
|
@@ -111,7 +111,8 @@ export default {
|
|
|
111
111
|
return;
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
// Array and type-only forms have no runtime properties to
|
|
114
|
+
// Array and type-only forms have no runtime properties to
|
|
115
|
+
// document.
|
|
115
116
|
const emitsObject = getObjectArgument(node, 0);
|
|
116
117
|
|
|
117
118
|
if (emitsObject) {
|
|
@@ -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/jsdoc.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getCommentText, getLineIndent, getNewline } from "./source.js";
|
|
1
|
+
import { getCommentText, getDisplayWidth, getLineIndent, getNewline } from "./source.js";
|
|
2
2
|
import { addTerminalPunctuation, capitaliseSentence, formatSentence, wrapWords } from "./wrap.js";
|
|
3
3
|
|
|
4
4
|
// The JSDoc tags this package formats, in their required output order.
|
|
@@ -217,7 +217,8 @@ function formatUnwrappedProse(lines, addPunctuation) {
|
|
|
217
217
|
// The formatted lines, built up in place.
|
|
218
218
|
const result = lines.map((line) => line.trim());
|
|
219
219
|
|
|
220
|
-
// The index of the current paragraph's first line, or null between
|
|
220
|
+
// The index of the current paragraph's first line, or null between
|
|
221
|
+
// paragraphs.
|
|
221
222
|
let paragraphStart = null;
|
|
222
223
|
|
|
223
224
|
for (let index = 0; index < result.length; index += 1) {
|
|
@@ -336,7 +337,8 @@ function formatTags(tagLines, width, addPunctuation, normaliseTags) {
|
|
|
336
337
|
// The formatted lines, built up in place.
|
|
337
338
|
const result = [];
|
|
338
339
|
|
|
339
|
-
// The tag type of the previously written entry, used to detect group
|
|
340
|
+
// The tag type of the previously written entry, used to detect group
|
|
341
|
+
// changes.
|
|
340
342
|
let lastType = null;
|
|
341
343
|
|
|
342
344
|
// The entries regrouped into the required tag order.
|
|
@@ -526,7 +528,7 @@ function getJSDocFormattingContext(sourceCode, comment) {
|
|
|
526
528
|
// The newline style used by the surrounding source.
|
|
527
529
|
const newline = getNewline(sourceCode.text);
|
|
528
530
|
// The available content width, allowing for the indent and " * " prefix.
|
|
529
|
-
const width = Math.max(1, 80 - indent
|
|
531
|
+
const width = Math.max(1, 80 - getDisplayWidth(indent) - 3);
|
|
530
532
|
// The undecorated comment content lines.
|
|
531
533
|
const content = getJSDocContent(commentText);
|
|
532
534
|
// The content split into its prose and tag sections.
|
package/comments/utils/source.js
CHANGED
|
@@ -156,6 +156,21 @@ export function getLineIndent(sourceCode, offset) {
|
|
|
156
156
|
return /^\s*$/.test(prefix) ? prefix : null;
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
+
/**
|
|
160
|
+
* Return the number of columns a source string occupies on screen, so comment
|
|
161
|
+
* lines are measured and wrapped against the 80-column limit under tab
|
|
162
|
+
* indentation. A tab counts as four columns, the width this package assumes.
|
|
163
|
+
*
|
|
164
|
+
* @param {string} sourceText
|
|
165
|
+
* The source string to measure, such as a line or its indentation.
|
|
166
|
+
*
|
|
167
|
+
* @returns {number}
|
|
168
|
+
* The source string's width in display columns.
|
|
169
|
+
*/
|
|
170
|
+
export function getDisplayWidth(sourceText) {
|
|
171
|
+
return sourceText.replaceAll("\t", " ").length;
|
|
172
|
+
}
|
|
173
|
+
|
|
159
174
|
/**
|
|
160
175
|
* Return the source items immediately around a comment.
|
|
161
176
|
*
|
|
@@ -310,3 +325,37 @@ export function hasImmediateLineComment(sourceCode, node) {
|
|
|
310
325
|
/^\r?\n[ \t]*$/.test(gap)
|
|
311
326
|
);
|
|
312
327
|
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Return whether a block comment immediately documents a source node.
|
|
331
|
+
*
|
|
332
|
+
* @param {object} sourceCode
|
|
333
|
+
* The Oxlint source code object.
|
|
334
|
+
* @param {object} node
|
|
335
|
+
* The source node.
|
|
336
|
+
*
|
|
337
|
+
* @returns {boolean}
|
|
338
|
+
* Whether an ordinary block comment immediately precedes the node.
|
|
339
|
+
*/
|
|
340
|
+
export function hasImmediateBlockComment(sourceCode, node) {
|
|
341
|
+
// Finds the closest preceding comment.
|
|
342
|
+
const comment = sourceCode
|
|
343
|
+
.getAllComments()
|
|
344
|
+
.findLast((candidate) => candidate.range[1] <= node.range[0]);
|
|
345
|
+
|
|
346
|
+
if (comment?.type !== "Block" || isDirectiveComment(comment)) {
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Checks the comments immediately around the node.
|
|
351
|
+
const { next, previous } = getCommentNeighbours(sourceCode, comment);
|
|
352
|
+
|
|
353
|
+
// Confirms there is no blank line before the node.
|
|
354
|
+
const gap = sourceCode.text.slice(comment.range[1], node.range[0]);
|
|
355
|
+
|
|
356
|
+
return (
|
|
357
|
+
next?.range[0] === node.range[0] &&
|
|
358
|
+
isLeadingComment(sourceCode, comment, previous) &&
|
|
359
|
+
/^\r?\n[ \t]*$/.test(gap)
|
|
360
|
+
);
|
|
361
|
+
}
|
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",
|
|
@@ -18,5 +19,13 @@
|
|
|
18
19
|
"comments/vue-component-documentation": "error",
|
|
19
20
|
"comments/vue-emit-documentation": "error",
|
|
20
21
|
"comments/vue-prop-documentation": "error"
|
|
21
|
-
}
|
|
22
|
+
},
|
|
23
|
+
"overrides": [
|
|
24
|
+
{
|
|
25
|
+
"files": ["**/*.test.*", "**/*.spec.*", "**/test/**"],
|
|
26
|
+
"rules": {
|
|
27
|
+
"comments/variable-declarations": ["error", { "rootOnly": true }]
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
]
|
|
22
31
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lewishowles/lint-config",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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",
|
|
@@ -39,7 +40,8 @@
|
|
|
39
40
|
"lint": "vp check",
|
|
40
41
|
"lint:fix": "vp check --fix",
|
|
41
42
|
"prepare": "vp config --no-agent",
|
|
42
|
-
"publint": "publint"
|
|
43
|
+
"publint": "publint",
|
|
44
|
+
"test:unit": "node --test test/comments/*.test.js"
|
|
43
45
|
},
|
|
44
46
|
"devDependencies": {
|
|
45
47
|
"publint": "^0.3.22",
|