@lewishowles/lint-config 0.1.3 → 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,111 @@
1
+ import { isFunctionValue, reportFunctionDocumentation } from "../utils/documentation.js";
2
+
3
+ /**
4
+ * Return the declaration node that owns the documentation position.
5
+ *
6
+ * @param {object} node
7
+ * The function declaration node.
8
+ *
9
+ * @returns {object}
10
+ * The node immediately following the documentation block.
11
+ */
12
+ function getDocumentationNode(node) {
13
+ // Walks up through export wrappers to find the documented position.
14
+ let documentationNode = node;
15
+
16
+ while (
17
+ documentationNode.parent?.type === "ExportDefaultDeclaration" ||
18
+ documentationNode.parent?.type === "ExportNamedDeclaration"
19
+ ) {
20
+ documentationNode = documentationNode.parent;
21
+ }
22
+
23
+ return documentationNode;
24
+ }
25
+
26
+ /**
27
+ * Return whether an object property belongs to the outermost object literal.
28
+ *
29
+ * @param {object} node
30
+ * The property node to inspect.
31
+ *
32
+ * @returns {boolean}
33
+ * Whether the property is not nested inside another object literal.
34
+ */
35
+ function isFirstLevelObjectProperty(node) {
36
+ // Finds the object literal that owns the property.
37
+ const object = node.parent;
38
+
39
+ if (object?.type !== "ObjectExpression") {
40
+ return false;
41
+ }
42
+
43
+ return object.parent?.parent?.type !== "ObjectExpression";
44
+ }
45
+
46
+ /**
47
+ * Create the function-documentation rule.
48
+ *
49
+ * @returns {object}
50
+ * The Oxlint rule definition.
51
+ */
52
+ export default {
53
+ meta: {
54
+ docs: { description: "Require JSDoc documentation for named functions and methods." },
55
+ type: "suggestion",
56
+ },
57
+ /**
58
+ * Create the rule's node visitors.
59
+ *
60
+ * @param {object} context
61
+ * The Oxlint rule context.
62
+ *
63
+ * @returns {object}
64
+ * The visitor functions for this rule.
65
+ */
66
+ createOnce(context) {
67
+ return {
68
+ /**
69
+ * Check a named function declaration for documentation.
70
+ *
71
+ * @param {object} node
72
+ * The function declaration node.
73
+ */
74
+ FunctionDeclaration(node) {
75
+ if (node.id) {
76
+ reportFunctionDocumentation(context, getDocumentationNode(node), node);
77
+ }
78
+ },
79
+ /**
80
+ * Check a first-level object method for documentation.
81
+ *
82
+ * @param {object} node
83
+ * The property node.
84
+ */
85
+ Property(node) {
86
+ if (!isFirstLevelObjectProperty(node) || !isFunctionValue(node.value)) {
87
+ return;
88
+ }
89
+
90
+ reportFunctionDocumentation(context, node, node.value);
91
+ },
92
+ /**
93
+ * Check a const function variable for documentation.
94
+ *
95
+ * @param {object} node
96
+ * The variable declarator node.
97
+ */
98
+ VariableDeclarator(node) {
99
+ if (
100
+ node.id.type !== "Identifier" ||
101
+ node.parent?.kind !== "const" ||
102
+ !isFunctionValue(node.init)
103
+ ) {
104
+ return;
105
+ }
106
+
107
+ reportFunctionDocumentation(context, node.parent, node.init);
108
+ },
109
+ };
110
+ },
111
+ };
@@ -0,0 +1,70 @@
1
+ import { formatJSDocTagFormatting, hasTargetJSDocTag, isJSDoc } from "../utils/jsdoc.js";
2
+ import { getCommentText, replaceMinimalComment } from "../utils/source.js";
3
+
4
+ /**
5
+ * Create the JSDoc tag-formatting rule.
6
+ *
7
+ * @returns {object}
8
+ * The Oxlint rule definition.
9
+ */
10
+ export default {
11
+ meta: {
12
+ docs: { description: "Format Phase 1 JSDoc tag spacing and grouping." },
13
+ fixable: "code",
14
+ type: "layout",
15
+ },
16
+ /**
17
+ * Create the rule's node visitors.
18
+ *
19
+ * @param {object} context
20
+ * The Oxlint rule context.
21
+ *
22
+ * @returns {object}
23
+ * The visitor functions for this rule.
24
+ */
25
+ createOnce(context) {
26
+ return {
27
+ /**
28
+ * Format every JSDoc comment's tags in the file.
29
+ */
30
+ Program() {
31
+ for (const comment of context.sourceCode.getAllComments()) {
32
+ if (comment.type !== "Block") {
33
+ continue;
34
+ }
35
+
36
+ // The comment's raw source text.
37
+ const commentText = getCommentText(context.sourceCode, comment);
38
+
39
+ if (!isJSDoc(commentText) || !hasTargetJSDocTag(context.sourceCode, comment)) {
40
+ continue;
41
+ }
42
+
43
+ // The comment, with its tag spacing, order, and grouping normalised.
44
+ const formattedComment = formatJSDocTagFormatting(context.sourceCode, comment);
45
+
46
+ if (formattedComment === commentText) {
47
+ continue;
48
+ }
49
+
50
+ context.report({
51
+ /**
52
+ * Apply the formatted replacement to the comment.
53
+ *
54
+ * @param {object} fixer
55
+ * The Oxlint fixer.
56
+ *
57
+ * @returns {object}
58
+ * The fix to apply.
59
+ */
60
+ fix: (fixer) => {
61
+ return replaceMinimalComment(fixer, comment, commentText, formattedComment);
62
+ },
63
+ message: "JSDoc tags must use the configured spacing, order, and grouping.",
64
+ node: comment,
65
+ });
66
+ }
67
+ },
68
+ };
69
+ },
70
+ };
@@ -0,0 +1,86 @@
1
+ import {
2
+ getCommentText,
3
+ getLineCommentGroups,
4
+ getLineIndent,
5
+ getLineStart,
6
+ } from "../utils/source.js";
7
+
8
+ /**
9
+ * Create the line-comment alignment rule.
10
+ *
11
+ * @returns {object}
12
+ * The Oxlint rule definition.
13
+ */
14
+ export default {
15
+ meta: {
16
+ docs: { description: "Align wrapped line comments with their first marker." },
17
+ fixable: "code",
18
+ type: "layout",
19
+ },
20
+ /**
21
+ * Create the rule's node visitors.
22
+ *
23
+ * @param {object} context
24
+ * The Oxlint rule context.
25
+ *
26
+ * @returns {object}
27
+ * The visitor functions for this rule.
28
+ */
29
+ createOnce(context) {
30
+ return {
31
+ /**
32
+ * Align every wrapped line-comment group in the file.
33
+ */
34
+ Program() {
35
+ for (const commentGroup of getLineCommentGroups(context.sourceCode)) {
36
+ if (commentGroup.length < 2) {
37
+ continue;
38
+ }
39
+
40
+ // The group's leading comment's indentation.
41
+ const firstIndent = getLineIndent(context.sourceCode, commentGroup[0].range[0]);
42
+
43
+ if (firstIndent === null) {
44
+ continue;
45
+ }
46
+
47
+ // The reindentation fixes for the group's comments.
48
+ const fixes = [];
49
+
50
+ for (const comment of commentGroup) {
51
+ // The comment's current indentation.
52
+ const indentation = getLineIndent(context.sourceCode, comment.range[0]);
53
+
54
+ if (indentation === null || indentation === firstIndent) {
55
+ continue;
56
+ }
57
+
58
+ fixes.push({
59
+ range: [getLineStart(context.sourceCode, comment.range[0]), comment.range[1]],
60
+ text: `${firstIndent}${getCommentText(context.sourceCode, comment)}`,
61
+ });
62
+ }
63
+
64
+ if (fixes.length === 0) {
65
+ continue;
66
+ }
67
+
68
+ context.report({
69
+ /**
70
+ * Apply the group's alignment fixes.
71
+ *
72
+ * @param {object} fixer
73
+ * The Oxlint fixer.
74
+ *
75
+ * @returns {object[]}
76
+ * The fixes to apply.
77
+ */
78
+ fix: (fixer) => fixes.map((fix) => fixer.replaceTextRange(fix.range, fix.text)),
79
+ message: "Wrapped line comments must align with the first comment marker.",
80
+ node: commentGroup[0],
81
+ });
82
+ }
83
+ },
84
+ };
85
+ },
86
+ };
@@ -0,0 +1,159 @@
1
+ import { formatJSDocWrapping, isJSDoc } from "../utils/jsdoc.js";
2
+
3
+ import {
4
+ getCommentText,
5
+ getLineIndent,
6
+ getNewline,
7
+ isDirectiveComment,
8
+ replaceMinimalComment,
9
+ } from "../utils/source.js";
10
+
11
+ import { formatSentence, wrapWords } from "../utils/wrap.js";
12
+
13
+ // The line length this rule wraps comments to.
14
+ const maximumLineLength = 80;
15
+
16
+ /**
17
+ * Wrap a line comment to the configured maximum width.
18
+ *
19
+ * @param {object} sourceCode
20
+ * The Oxlint source code object.
21
+ * @param {object} comment
22
+ * The line comment token.
23
+ *
24
+ * @returns {string|null}
25
+ * The wrapped comment, or null when it is not a standalone comment.
26
+ */
27
+ function formatLineComment(sourceCode, comment) {
28
+ // The comment's current indentation.
29
+ const indentation = getLineIndent(sourceCode, comment.range[0]);
30
+
31
+ if (indentation === null) {
32
+ return null;
33
+ }
34
+
35
+ // The available width, allowing for the indent and "// " prefix.
36
+ const width = maximumLineLength - indentation.length - 3;
37
+ // The comment's undecorated text.
38
+ const text = comment.value.trim();
39
+
40
+ if (isDirectiveComment(comment)) {
41
+ return null;
42
+ }
43
+
44
+ if (text === "") {
45
+ return "//";
46
+ }
47
+
48
+ return wrapWords(text, Math.max(1, width))
49
+ .map((line, index) => `${index === 0 ? "" : indentation}// ${line}`)
50
+ .join(getNewline(sourceCode.text));
51
+ }
52
+
53
+ /**
54
+ * Wrap an ordinary block comment to the configured maximum width.
55
+ *
56
+ * @param {object} sourceCode
57
+ * The Oxlint source code object.
58
+ * @param {object} comment
59
+ * The block comment token.
60
+ *
61
+ * @returns {string|null}
62
+ * The wrapped comment, or null when it is not a standalone comment.
63
+ */
64
+ function formatBlockComment(sourceCode, comment) {
65
+ // The comment's current indentation.
66
+ const indentation = getLineIndent(sourceCode, comment.range[0]);
67
+
68
+ if (indentation === null) {
69
+ return null;
70
+ }
71
+
72
+ // The comment's raw source text.
73
+ const commentText = getCommentText(sourceCode, comment);
74
+ // The comment body, sentence-formatted.
75
+ const text = formatSentence(commentText.slice(2, -2).trim());
76
+ // The available width, allowing for the indent and " * " prefix.
77
+ const width = maximumLineLength - indentation.length - 3;
78
+ // The comment body, rewrapped to the available width.
79
+ const lines = wrapWords(text, Math.max(1, width));
80
+
81
+ return ["/*", ...lines.map((line) => `${indentation} * ${line}`), `${indentation} */`].join(
82
+ getNewline(sourceCode.text),
83
+ );
84
+ }
85
+
86
+ /**
87
+ * Create the maximum-line-length rule.
88
+ *
89
+ * @returns {object}
90
+ * The Oxlint rule definition.
91
+ */
92
+ export default {
93
+ meta: {
94
+ docs: { description: "Wrap comments at 80 characters." },
95
+ fixable: "code",
96
+ type: "layout",
97
+ },
98
+ /**
99
+ * Create the rule's node visitors.
100
+ *
101
+ * @param {object} context
102
+ * The Oxlint rule context.
103
+ *
104
+ * @returns {object}
105
+ * The visitor functions for this rule.
106
+ */
107
+ createOnce(context) {
108
+ return {
109
+ /**
110
+ * Wrap every over-length comment in the file.
111
+ */
112
+ Program() {
113
+ for (const comment of context.sourceCode.getAllComments()) {
114
+ if (comment.type === "Shebang") {
115
+ continue;
116
+ }
117
+
118
+ // The comment's raw source text.
119
+ const commentText = getCommentText(context.sourceCode, comment);
120
+ // The comment's individual source lines.
121
+ const lines = commentText.split(/\r\n|\n|\r/);
122
+
123
+ if (!lines.some((line) => line.length > maximumLineLength)) {
124
+ continue;
125
+ }
126
+
127
+ // The comment, rewrapped using the formatter matching its type.
128
+ const formattedComment =
129
+ comment.type === "Line"
130
+ ? formatLineComment(context.sourceCode, comment)
131
+ : isJSDoc(commentText)
132
+ ? formatJSDocWrapping(context.sourceCode, comment)
133
+ : formatBlockComment(context.sourceCode, comment);
134
+
135
+ if (formattedComment === null || formattedComment === commentText) {
136
+ continue;
137
+ }
138
+
139
+ context.report({
140
+ /**
141
+ * Apply the wrapped replacement to the comment.
142
+ *
143
+ * @param {object} fixer
144
+ * The Oxlint fixer.
145
+ *
146
+ * @returns {object}
147
+ * The fix to apply.
148
+ */
149
+ fix: (fixer) => {
150
+ return replaceMinimalComment(fixer, comment, commentText, formattedComment);
151
+ },
152
+ message: "Comment exceeds 80 characters.",
153
+ node: comment,
154
+ });
155
+ }
156
+ },
157
+ };
158
+ },
159
+ };
@@ -0,0 +1,295 @@
1
+ import {
2
+ getCommentNeighbours,
3
+ getCommentText,
4
+ getLineCommentGroups,
5
+ getLineIndent,
6
+ getLineStart,
7
+ getNewline,
8
+ isLeadingComment,
9
+ } from "../utils/source.js";
10
+
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
+ /**
29
+ * Return continuation comments indexed by their group leader.
30
+ *
31
+ * @param {object[][]} lineCommentGroups
32
+ * The adjacent line-comment groups.
33
+ *
34
+ * @returns {object}
35
+ * The continuation comments and their leaders.
36
+ */
37
+ function getLineCommentContinuations(lineCommentGroups) {
38
+ // The comments that follow a group's leader, across every group.
39
+ const continuationComments = new Set();
40
+ // Each group's continuation comments, indexed by their leader.
41
+ const continuationsByLeader = new Map();
42
+
43
+ for (const group of lineCommentGroups) {
44
+ if (group.length < 2) {
45
+ continue;
46
+ }
47
+
48
+ // The group's leading comment and its continuations.
49
+ const [leader, ...continuations] = group;
50
+
51
+ continuationsByLeader.set(leader, continuations);
52
+
53
+ for (const continuation of continuations) {
54
+ continuationComments.add(continuation);
55
+ }
56
+ }
57
+
58
+ return { continuationComments, continuationsByLeader };
59
+ }
60
+
61
+ /**
62
+ * Return indentation relative to a comment's current indentation.
63
+ *
64
+ * @param {string} indentation
65
+ * The line indentation.
66
+ * @param {string} commentIndent
67
+ * The leading comment's current indentation.
68
+ *
69
+ * @returns {string}
70
+ * The indentation to preserve after reindenting.
71
+ */
72
+ function getRelativeIndent(indentation, commentIndent) {
73
+ return indentation.startsWith(commentIndent)
74
+ ? indentation.slice(commentIndent.length)
75
+ : indentation;
76
+ }
77
+
78
+ /**
79
+ * Reindent every line of a leading comment.
80
+ *
81
+ * @param {object} sourceCode
82
+ * The Oxlint source code object.
83
+ * @param {object} comment
84
+ * The leading comment token.
85
+ * @param {string} commentIndent
86
+ * The comment's current indentation.
87
+ * @param {string} expectedIndent
88
+ * The documented code's indentation.
89
+ *
90
+ * @returns {string}
91
+ * The reindented comment text.
92
+ */
93
+ function getReindentedCommentText(sourceCode, comment, commentIndent, expectedIndent) {
94
+ return getCommentText(sourceCode, comment)
95
+ .split(/\r\n|\n|\r/)
96
+ .map((line, lineIndex) => {
97
+ if (lineIndex === 0) {
98
+ return `${expectedIndent}${line}`;
99
+ }
100
+
101
+ // The line's current indentation.
102
+ const lineIndent = line.match(/^[ \t]*/)[0];
103
+ // The indentation to preserve relative to the comment's own indent.
104
+ const relativeIndent = getRelativeIndent(lineIndent, commentIndent);
105
+
106
+ return `${expectedIndent}${relativeIndent}${line.slice(lineIndent.length)}`;
107
+ })
108
+ .join(getNewline(sourceCode.text));
109
+ }
110
+
111
+ /**
112
+ * Return replacements that align a leading comment group with its code.
113
+ *
114
+ * @param {object} sourceCode
115
+ * The Oxlint source code object.
116
+ * @param {object} comment
117
+ * The leading comment token.
118
+ * @param {object[]} continuations
119
+ * The comment group's continuation tokens.
120
+ * @param {string} actualIndent
121
+ * The comment's current indentation.
122
+ * @param {string} expectedIndent
123
+ * The documented code's indentation.
124
+ *
125
+ * @returns {object[]}
126
+ * The indentation replacements.
127
+ */
128
+ function getCommentIndentationFixes(
129
+ sourceCode,
130
+ comment,
131
+ continuations,
132
+ actualIndent,
133
+ expectedIndent,
134
+ ) {
135
+ if (actualIndent === expectedIndent) {
136
+ return [];
137
+ }
138
+
139
+ // The replacements, starting with the leading comment's reindent.
140
+ const fixes = [
141
+ {
142
+ range: [getLineStart(sourceCode, comment.range[0]), comment.range[1]],
143
+ text: getReindentedCommentText(sourceCode, comment, actualIndent, expectedIndent),
144
+ },
145
+ ];
146
+
147
+ for (const continuation of continuations) {
148
+ // The continuation's current indentation.
149
+ const continuationIndent = getLineIndent(sourceCode, continuation.range[0]) ?? "";
150
+ // The indentation to preserve relative to the leading comment's indent.
151
+ const relativeIndent = getRelativeIndent(continuationIndent, actualIndent);
152
+
153
+ fixes.push({
154
+ range: [getLineStart(sourceCode, continuation.range[0]), continuation.range[1]],
155
+ text: `${expectedIndent}${relativeIndent}${getCommentText(sourceCode, continuation)}`,
156
+ });
157
+ }
158
+
159
+ return fixes;
160
+ }
161
+
162
+ /**
163
+ * Return the replacement that places a final leading comment against its code.
164
+ *
165
+ * @param {object} sourceCode
166
+ * The Oxlint source code object.
167
+ * @param {object} comment
168
+ * The leading comment token.
169
+ * @param {object} next
170
+ * The documented source token.
171
+ * @param {object|undefined} followingComment
172
+ * The next comment token.
173
+ * @param {string} expectedIndent
174
+ * The documented code's indentation.
175
+ *
176
+ * @returns {object|null}
177
+ * The gap replacement, or null when none is needed.
178
+ */
179
+ function getCommentGapFix(sourceCode, comment, next, followingComment, expectedIndent) {
180
+ if (followingComment !== undefined && followingComment.range[0] <= next.range[0]) {
181
+ return null;
182
+ }
183
+
184
+ // The source text currently between the comment and its documented code.
185
+ const gap = getCommentToTokenGap(sourceCode, comment, next);
186
+ // The gap the documented code's indentation requires.
187
+ const desiredGap = `${getNewline(sourceCode.text)}${expectedIndent}`;
188
+
189
+ return gap === desiredGap ? null : { range: [comment.range[1], next.range[0]], text: desiredGap };
190
+ }
191
+
192
+ /**
193
+ * Create the immediate-comment-placement rule.
194
+ *
195
+ * @returns {object}
196
+ * The Oxlint rule definition.
197
+ */
198
+ export default {
199
+ meta: {
200
+ docs: { description: "Keep comments immediately before documented code." },
201
+ fixable: "code",
202
+ type: "layout",
203
+ },
204
+ /**
205
+ * Create the rule's node visitors.
206
+ *
207
+ * @param {object} context
208
+ * The Oxlint rule context.
209
+ *
210
+ * @returns {object}
211
+ * The visitor functions for this rule.
212
+ */
213
+ createOnce(context) {
214
+ return {
215
+ /**
216
+ * Align every leading comment in the file with its documented code.
217
+ */
218
+ Program() {
219
+ // Every comment token in the file, in source order.
220
+ const comments = context.sourceCode.getAllComments();
221
+
222
+ // The continuation comments and their group leaders.
223
+ const { continuationComments, continuationsByLeader } = getLineCommentContinuations(
224
+ getLineCommentGroups(context.sourceCode),
225
+ );
226
+
227
+ for (const [index, comment] of comments.entries()) {
228
+ if (comment.type === "Shebang" || continuationComments.has(comment)) {
229
+ continue;
230
+ }
231
+
232
+ // The comment's neighbouring token and comment.
233
+ const { next, previous } = getCommentNeighbours(context.sourceCode, comment);
234
+
235
+ if (next === null || !isLeadingComment(context.sourceCode, comment, previous)) {
236
+ continue;
237
+ }
238
+
239
+ // The documented code's indentation.
240
+ const expectedIndent = getLineIndent(context.sourceCode, next.range[0]);
241
+ // The comment's current indentation.
242
+ const actualIndent = getLineIndent(context.sourceCode, comment.range[0]);
243
+
244
+ if (expectedIndent === null || actualIndent === null) {
245
+ continue;
246
+ }
247
+
248
+ // The reindentation fixes for the comment and its continuations.
249
+ const fixes = getCommentIndentationFixes(
250
+ context.sourceCode,
251
+ comment,
252
+ continuationsByLeader.get(comment) ?? [],
253
+ actualIndent,
254
+ expectedIndent,
255
+ );
256
+
257
+ // The next comment token, used to avoid overlapping gap fixes.
258
+ const followingComment = comments[index + 1];
259
+
260
+ // The fix that closes the gap between the comment and its code, when needed.
261
+ const gapFix = getCommentGapFix(
262
+ context.sourceCode,
263
+ comment,
264
+ next,
265
+ followingComment,
266
+ expectedIndent,
267
+ );
268
+
269
+ if (gapFix !== null) {
270
+ fixes.push(gapFix);
271
+ }
272
+
273
+ if (fixes.length === 0) {
274
+ continue;
275
+ }
276
+
277
+ context.report({
278
+ /**
279
+ * Apply the comment's alignment fixes.
280
+ *
281
+ * @param {object} fixer
282
+ * The Oxlint fixer.
283
+ *
284
+ * @returns {object[]}
285
+ * The fixes to apply.
286
+ */
287
+ fix: (fixer) => fixes.map((fix) => fixer.replaceTextRange(fix.range, fix.text)),
288
+ message: "Comment must be immediately before the documented code.",
289
+ node: comment,
290
+ });
291
+ }
292
+ },
293
+ };
294
+ },
295
+ };