@lewishowles/lint-config 0.2.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.
@@ -0,0 +1,346 @@
1
+ // The token types treated as comments.
2
+ const commentTypes = new Set(["Block", "Line", "Shebang"]);
3
+
4
+ /**
5
+ * Return whether a source item is a comment token.
6
+ *
7
+ * @param {object} item
8
+ * The source item to inspect.
9
+ *
10
+ * @returns {boolean}
11
+ * Whether the item is a comment token.
12
+ */
13
+ export function isComment(item) {
14
+ return commentTypes.has(item.type);
15
+ }
16
+
17
+ /**
18
+ * Return the source text for a comment token.
19
+ *
20
+ * @param {object} sourceCode
21
+ * The Oxlint source code object.
22
+ * @param {object} comment
23
+ * The comment token.
24
+ *
25
+ * @returns {string}
26
+ * The comment's source text.
27
+ */
28
+ export function getCommentText(sourceCode, comment) {
29
+ return sourceCode.text.slice(comment.range[0], comment.range[1]);
30
+ }
31
+
32
+ /**
33
+ * Return the smallest replacement that changes one comment to formatted text.
34
+ *
35
+ * @param {object} comment
36
+ * The comment token.
37
+ * @param {string} commentText
38
+ * The comment's source text.
39
+ * @param {string} formattedComment
40
+ * The formatted replacement text.
41
+ *
42
+ * @returns {object}
43
+ * The source range and replacement text.
44
+ */
45
+ export function getMinimalCommentReplacement(comment, commentText, formattedComment) {
46
+ // The count of unchanged characters shared at the start of both texts.
47
+ let start = 0;
48
+
49
+ while (
50
+ start < commentText.length &&
51
+ start < formattedComment.length &&
52
+ commentText[start] === formattedComment[start]
53
+ ) {
54
+ start += 1;
55
+ }
56
+
57
+ // The count of unchanged characters shared at the end of both texts.
58
+ let end = 0;
59
+
60
+ while (
61
+ end < commentText.length - start &&
62
+ end < formattedComment.length - start &&
63
+ commentText.at(-end - 1) === formattedComment.at(-end - 1)
64
+ ) {
65
+ end += 1;
66
+ }
67
+
68
+ return {
69
+ range: [comment.range[0] + start, comment.range[1] - end],
70
+ text: formattedComment.slice(start, formattedComment.length - end),
71
+ };
72
+ }
73
+
74
+ /**
75
+ * Replace a comment using the smallest changed source range.
76
+ *
77
+ * @param {object} fixer
78
+ * The Oxlint fixer.
79
+ * @param {object} comment
80
+ * The comment token.
81
+ * @param {string} sourceText
82
+ * The comment's source text.
83
+ * @param {string} formattedText
84
+ * The formatted replacement text.
85
+ *
86
+ * @returns {object}
87
+ * The fixer replacement.
88
+ */
89
+ export function replaceMinimalComment(fixer, comment, sourceText, formattedText) {
90
+ // The smallest source range and text that changes the comment.
91
+ const replacement = getMinimalCommentReplacement(comment, sourceText, formattedText);
92
+
93
+ return fixer.replaceTextRange(replacement.range, replacement.text);
94
+ }
95
+
96
+ /**
97
+ * Return whether a comment is an inline tool directive.
98
+ *
99
+ * @param {object} comment
100
+ * The comment token.
101
+ *
102
+ * @returns {boolean}
103
+ * Whether the comment starts with a recognised directive prefix.
104
+ */
105
+ export function isDirectiveComment(comment) {
106
+ return /^(?:eslint|oxlint|istanbul|c8)-/.test(comment.value.trim());
107
+ }
108
+
109
+ /**
110
+ * Return the source line containing an offset.
111
+ *
112
+ * @param {object} sourceCode
113
+ * The Oxlint source code object.
114
+ * @param {number} offset
115
+ * The source offset.
116
+ *
117
+ * @returns {number}
118
+ * The one-based source line.
119
+ */
120
+ export function getLineNumber(sourceCode, offset) {
121
+ return sourceCode.getLocFromIndex(offset).line;
122
+ }
123
+
124
+ /**
125
+ * Return the offset at which an offset's source line starts.
126
+ *
127
+ * @param {object} sourceCode
128
+ * The Oxlint source code object.
129
+ * @param {number} offset
130
+ * The source offset.
131
+ *
132
+ * @returns {number}
133
+ * The line-start offset.
134
+ */
135
+ export function getLineStart(sourceCode, offset) {
136
+ return sourceCode.lineStartIndices[getLineNumber(sourceCode, offset) - 1];
137
+ }
138
+
139
+ /**
140
+ * Return the whitespace before a source offset on its line.
141
+ *
142
+ * @param {object} sourceCode
143
+ * The Oxlint source code object.
144
+ * @param {number} offset
145
+ * The source offset.
146
+ *
147
+ * @returns {string|null}
148
+ * The line indentation, or null when code precedes the offset.
149
+ */
150
+ export function getLineIndent(sourceCode, offset) {
151
+ // The offset at which the offset's line begins.
152
+ const lineStart = getLineStart(sourceCode, offset);
153
+ // The source text between the line start and the offset.
154
+ const prefix = sourceCode.text.slice(lineStart, offset);
155
+
156
+ return /^\s*$/.test(prefix) ? prefix : null;
157
+ }
158
+
159
+ /**
160
+ * Return the source items immediately around a comment.
161
+ *
162
+ * @param {object} sourceCode
163
+ * The Oxlint source code object.
164
+ * @param {object} comment
165
+ * The comment token.
166
+ *
167
+ * @returns {object}
168
+ * The previous and next non-comment source items.
169
+ */
170
+ export function getCommentNeighbours(sourceCode, comment) {
171
+ // Every token and comment in the file, in source order.
172
+ const sourceItems = sourceCode.tokensAndComments;
173
+
174
+ // The comment's position within sourceItems.
175
+ const commentIndex = sourceItems.findIndex(
176
+ (item) => item.range[0] === comment.range[0] && item.range[1] === comment.range[1],
177
+ );
178
+
179
+ // The preceding non-comment source item, once found.
180
+ let previous = null;
181
+ // The following non-comment source item, once found.
182
+ let next = null;
183
+
184
+ for (let index = commentIndex - 1; index >= 0; index -= 1) {
185
+ if (!isComment(sourceItems[index])) {
186
+ previous = sourceItems[index];
187
+
188
+ break;
189
+ }
190
+ }
191
+
192
+ for (let index = commentIndex + 1; index < sourceItems.length; index += 1) {
193
+ if (!isComment(sourceItems[index])) {
194
+ next = sourceItems[index];
195
+
196
+ break;
197
+ }
198
+ }
199
+
200
+ return { next, previous };
201
+ }
202
+
203
+ /**
204
+ * Return the comments that form one adjacent line-comment group.
205
+ *
206
+ * @param {object} sourceCode
207
+ * The Oxlint source code object.
208
+ *
209
+ * @returns {object[][]}
210
+ * Adjacent line-comment groups.
211
+ */
212
+ export function getLineCommentGroups(sourceCode) {
213
+ // Every line comment in the file, in source order.
214
+ const comments = sourceCode.getAllComments().filter((comment) => comment.type === "Line");
215
+ // The adjacent comment groups, built up in place.
216
+ const groups = [];
217
+
218
+ for (const comment of comments) {
219
+ if (isDirectiveComment(comment)) {
220
+ continue;
221
+ }
222
+
223
+ // The group currently being built, when there is one.
224
+ const group = groups.at(-1);
225
+ // The current group's last comment, when there is one.
226
+ const previousComment = group?.at(-1);
227
+
228
+ // The source text between the previous comment and this one.
229
+ const gap = previousComment
230
+ ? sourceCode.text.slice(previousComment.range[1], comment.range[0])
231
+ : "";
232
+
233
+ // How many newlines separate this comment from the previous one.
234
+ const newlineCount = gap.match(/\r\n|\n|\r/g)?.length ?? 0;
235
+
236
+ if (group && newlineCount === 1 && /^[ \t]*\r?\n[ \t]*$/.test(gap)) {
237
+ group.push(comment);
238
+ } else {
239
+ groups.push([comment]);
240
+ }
241
+ }
242
+
243
+ return groups;
244
+ }
245
+
246
+ /**
247
+ * Return the newline sequence used by a source file.
248
+ *
249
+ * @param {string} source
250
+ * The source text.
251
+ *
252
+ * @returns {string}
253
+ * The source newline sequence.
254
+ */
255
+ export function getNewline(source) {
256
+ return source.includes("\r\n") ? "\r\n" : "\n";
257
+ }
258
+
259
+ /**
260
+ * Return whether a comment is a leading comment for the next source token.
261
+ *
262
+ * @param {object} sourceCode
263
+ * The Oxlint source code object.
264
+ * @param {object} comment
265
+ * The comment token.
266
+ * @param {object|null} previous
267
+ * The preceding non-comment source item.
268
+ *
269
+ * @returns {boolean}
270
+ * Whether the comment is on a line of its own before code.
271
+ */
272
+ export function isLeadingComment(sourceCode, comment, previous) {
273
+ if (getLineIndent(sourceCode, comment.range[0]) === null) {
274
+ return false;
275
+ }
276
+
277
+ return previous === null || comment.loc.start.line > previous.loc.end.line;
278
+ }
279
+
280
+ /**
281
+ * Return whether a line comment immediately documents a source node.
282
+ *
283
+ * @param {object} sourceCode
284
+ * The Oxlint source code object.
285
+ * @param {object} node
286
+ * The source node.
287
+ *
288
+ * @returns {boolean}
289
+ * Whether an ordinary line comment immediately precedes the node.
290
+ */
291
+ export function hasImmediateLineComment(sourceCode, node) {
292
+ // Finds the closest preceding comment.
293
+ const comment = sourceCode
294
+ .getAllComments()
295
+ .findLast((candidate) => candidate.range[1] <= node.range[0]);
296
+
297
+ if (comment?.type !== "Line" || isDirectiveComment(comment)) {
298
+ return false;
299
+ }
300
+
301
+ // Checks the comments immediately around the node.
302
+ const { next, previous } = getCommentNeighbours(sourceCode, comment);
303
+
304
+ // Confirms there is no blank line before the node.
305
+ const gap = sourceCode.text.slice(comment.range[1], node.range[0]);
306
+
307
+ return (
308
+ next?.range[0] === node.range[0] &&
309
+ isLeadingComment(sourceCode, comment, previous) &&
310
+ /^\r?\n[ \t]*$/.test(gap)
311
+ );
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
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Return whether a node is a call expression invoking the given function name.
3
+ *
4
+ * @param {object} node
5
+ * The node to inspect.
6
+ * @param {string} name
7
+ * The function name to match.
8
+ *
9
+ * @returns {boolean}
10
+ * Whether the node matches.
11
+ */
12
+ export function isNamedCall(node, name) {
13
+ return (
14
+ node?.type === "CallExpression" &&
15
+ node.callee?.type === "Identifier" &&
16
+ node.callee.name === name
17
+ );
18
+ }
19
+
20
+ /**
21
+ * Return an object argument from a call, when one is present.
22
+ *
23
+ * @param {object} callNode
24
+ * The call whose argument should be inspected.
25
+ * @param {number} argumentIndex
26
+ * The argument position to inspect.
27
+ *
28
+ * @returns {object|null}
29
+ * The object argument, or null when it is missing or not an object.
30
+ */
31
+ export function getObjectArgument(callNode, argumentIndex) {
32
+ // The candidate object argument for runtime declarations.
33
+ const argument = callNode.arguments[argumentIndex];
34
+
35
+ return argument?.type === "ObjectExpression" ? argument : null;
36
+ }
37
+
38
+ /**
39
+ * Return the object properties that represent runtime declarations.
40
+ *
41
+ * @param {object} objectExpression
42
+ * The object expression to inspect.
43
+ *
44
+ * @returns {object[]}
45
+ * Its ordinary properties, excluding spread elements.
46
+ */
47
+ export function getObjectProperties(objectExpression) {
48
+ return objectExpression.properties.filter((property) => property.type === "Property");
49
+ }
50
+
51
+ /**
52
+ * Return the name of an object property.
53
+ *
54
+ * @param {object} property
55
+ * The property to inspect.
56
+ *
57
+ * @returns {string|null}
58
+ * The property name, when it is a string or number.
59
+ */
60
+ export function getPropertyName(property) {
61
+ if (property.key.type === "Identifier") {
62
+ return property.key.name;
63
+ }
64
+
65
+ if (typeof property.key.value === "string" || typeof property.key.value === "number") {
66
+ return String(property.key.value);
67
+ }
68
+
69
+ return null;
70
+ }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Wrap words to a maximum line width.
3
+ *
4
+ * @param {string} text
5
+ * The text to wrap.
6
+ * @param {number} width
7
+ * The maximum output width.
8
+ *
9
+ * @returns {string[]}
10
+ * Wrapped lines.
11
+ */
12
+ export function wrapWords(text, width) {
13
+ // The text's individual words.
14
+ const words = text.trim().split(/\s+/).filter(Boolean);
15
+ // The wrapped lines, built up in place.
16
+ const lines = [];
17
+
18
+ // The line currently being filled.
19
+ let currentLine = "";
20
+
21
+ for (const word of words) {
22
+ if (word.length > width && currentLine === "") {
23
+ for (let index = 0; index < word.length; index += width) {
24
+ lines.push(word.slice(index, index + width));
25
+ }
26
+ continue;
27
+ }
28
+
29
+ if (currentLine === "") {
30
+ currentLine = word;
31
+ } else if (currentLine.length + word.length + 1 <= width) {
32
+ currentLine += ` ${word}`;
33
+ } else {
34
+ lines.push(currentLine);
35
+ currentLine = word;
36
+ }
37
+ }
38
+
39
+ if (currentLine !== "") {
40
+ lines.push(currentLine);
41
+ }
42
+
43
+ return lines;
44
+ }
45
+
46
+ /**
47
+ * Capitalise a sentence and ensure it has terminal punctuation.
48
+ *
49
+ * @param {string} text
50
+ * The sentence text.
51
+ *
52
+ * @returns {string}
53
+ * The corrected sentence text.
54
+ */
55
+ export function formatSentence(text) {
56
+ return addTerminalPunctuation(capitaliseSentence(text));
57
+ }
58
+
59
+ /**
60
+ * Capitalise the first letter of sentence text.
61
+ *
62
+ * @param {string} text
63
+ * The sentence text.
64
+ *
65
+ * @returns {string}
66
+ * The capitalised sentence text.
67
+ */
68
+ export function capitaliseSentence(text) {
69
+ // The sentence text, without leading or trailing whitespace.
70
+ const trimmedText = text.trim();
71
+
72
+ if (trimmedText === "" || trimmedText.startsWith("@")) {
73
+ return text;
74
+ }
75
+
76
+ // The index of the first letter character, ignoring leading punctuation.
77
+ const firstLetter = trimmedText.search(/\p{L}/u);
78
+
79
+ if (firstLetter < 0) {
80
+ return text;
81
+ }
82
+
83
+ // The leading word, starting from the first letter character.
84
+ const leadingWord = trimmedText.slice(firstLetter).match(/^\p{L}[\p{L}\p{N}]*/u)?.[0] ?? "";
85
+
86
+ // A camelCase word (lowercase start, later uppercase) is a code
87
+ // identifier and must keep its own casing rather than sentence casing.
88
+ if (/^\p{Ll}[\p{Ll}\p{N}]*\p{Lu}/u.test(leadingWord)) {
89
+ return text;
90
+ }
91
+
92
+ // The first letter character.
93
+ const letter = trimmedText[firstLetter];
94
+ // The text with its first letter capitalised.
95
+ const formattedText = `${trimmedText.slice(0, firstLetter)}${letter.toLocaleUpperCase()}${trimmedText.slice(firstLetter + 1)}`;
96
+
97
+ return text.replace(trimmedText, formattedText);
98
+ }
99
+
100
+ /**
101
+ * Add terminal punctuation to sentence text.
102
+ *
103
+ * @param {string} text
104
+ * The sentence text.
105
+ *
106
+ * @returns {string}
107
+ * The punctuated sentence text.
108
+ */
109
+ export function addTerminalPunctuation(text) {
110
+ // The sentence text, without leading or trailing whitespace.
111
+ const trimmedText = text.trim();
112
+
113
+ if (trimmedText === "" || trimmedText.startsWith("@") || /[.!?]$/.test(trimmedText)) {
114
+ return text;
115
+ }
116
+
117
+ return text.replace(trimmedText, `${trimmedText}.`);
118
+ }
package/comments.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "jsPlugins": [
3
+ {
4
+ "name": "comments",
5
+ "specifier": "@lewishowles/lint-config/comments/plugin"
6
+ }
7
+ ],
8
+ "rules": {
9
+ "comments/block-comments": "error",
10
+ "comments/class-documentation": "error",
11
+ "comments/configured-api-calls": "error",
12
+ "comments/function-documentation": "error",
13
+ "comments/jsdoc-tag-formatting": "error",
14
+ "comments/line-comments": "error",
15
+ "comments/max-line-length": "error",
16
+ "comments/placement": "error",
17
+ "comments/sentence-punctuation": "error",
18
+ "comments/variable-declarations": "error",
19
+ "comments/vue-component-documentation": "error",
20
+ "comments/vue-emit-documentation": "error",
21
+ "comments/vue-prop-documentation": "error"
22
+ }
23
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lewishowles/lint-config",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Shared oxlint configuration for Lewis Howles projects",
5
5
  "keywords": [
6
6
  "config",
@@ -19,27 +19,36 @@
19
19
  "url": "git+https://github.com/lewishowles/lint-config.git"
20
20
  },
21
21
  "files": [
22
+ "CHANGELOG.md",
22
23
  "base.json",
24
+ "comments",
25
+ "comments.json",
23
26
  "vue.json",
24
27
  "README.md"
25
28
  ],
26
29
  "type": "module",
27
30
  "exports": {
28
31
  "./base.json": "./base.json",
32
+ "./comments.json": "./comments.json",
33
+ "./comments/plugin": "./comments/plugin.js",
29
34
  "./vue.json": "./vue.json"
30
35
  },
31
36
  "publishConfig": {
32
37
  "access": "public"
33
38
  },
34
39
  "scripts": {
40
+ "lint": "vp check",
41
+ "lint:fix": "vp check --fix",
42
+ "prepare": "vp config --no-agent",
35
43
  "publint": "publint"
36
44
  },
37
45
  "devDependencies": {
38
- "publint": "^0.3.21"
46
+ "publint": "^0.3.22",
47
+ "vite-plus": "0.2.8"
39
48
  },
40
49
  "peerDependencies": {
41
50
  "@stylistic/eslint-plugin": "*",
42
- "vite-plus": "*"
51
+ "vite-plus": "0.2.8"
43
52
  },
44
53
  "engines": {
45
54
  "node": ">=20"