@lewishowles/lint-config 0.7.0 → 0.9.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 +29 -0
- package/README.md +44 -23
- package/base.json +47 -0
- package/comments/rules/formatting.js +5 -7
- package/comments/rules/function-documentation.js +21 -2
- package/comments/utils/documentation.js +61 -29
- package/comments/utils/jsdoc.js +347 -79
- package/comments/utils/source.js +59 -26
- package/comments/utils/wrap.js +48 -14
- package/comments.json +1 -0
- package/layers.js +60 -0
- package/package.json +10 -5
- package/testing/plugin.js +8 -0
- package/testing/rules/no-text-lookups.js +148 -0
- package/vue.json +2 -0
package/comments/utils/source.js
CHANGED
|
@@ -301,28 +301,68 @@ export function isLeadingComment(sourceCode, comment, previous) {
|
|
|
301
301
|
* The source node.
|
|
302
302
|
*
|
|
303
303
|
* @returns {boolean}
|
|
304
|
-
* Whether an ordinary line comment
|
|
304
|
+
* Whether an ordinary line comment sits directly above the node. Tool
|
|
305
|
+
* directive comments may sit between them.
|
|
305
306
|
*/
|
|
306
307
|
export function hasImmediateLineComment(sourceCode, node) {
|
|
307
|
-
//
|
|
308
|
-
const comment = sourceCode
|
|
309
|
-
.getAllComments()
|
|
310
|
-
.findLast((candidate) => candidate.range[1] <= node.range[0]);
|
|
308
|
+
// The closest comment above the node, looking past tool directive comments.
|
|
309
|
+
const comment = getImmediateNonDirectiveComment(sourceCode, node);
|
|
311
310
|
|
|
312
|
-
if (comment?.type !== "Line"
|
|
311
|
+
if (comment?.type !== "Line") {
|
|
313
312
|
return false;
|
|
314
313
|
}
|
|
315
314
|
|
|
316
315
|
// Checks the comments immediately around the node.
|
|
317
316
|
const { next, previous } = getCommentNeighbours(sourceCode, comment);
|
|
318
|
-
// Confirms there is no blank line before the node.
|
|
319
|
-
const gap = sourceCode.text.slice(comment.range[1], node.range[0]);
|
|
320
317
|
|
|
321
|
-
return (
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
318
|
+
return next?.range[0] === node.range[0] && isLeadingComment(sourceCode, comment, previous);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Return the closest comment above a node, skipping tool directive comments
|
|
323
|
+
* such as `// eslint-disable-next-line`. The comment, each directive and the
|
|
324
|
+
* node must each start on the line after the one before, with no blank lines.
|
|
325
|
+
*
|
|
326
|
+
* @param {object} sourceCode
|
|
327
|
+
* The Oxlint source code object.
|
|
328
|
+
* @param {object} node
|
|
329
|
+
* The source node.
|
|
330
|
+
*
|
|
331
|
+
* @returns {object|null}
|
|
332
|
+
* The comment above the node, or null when no comment sits directly above
|
|
333
|
+
* the node.
|
|
334
|
+
*/
|
|
335
|
+
export function getImmediateNonDirectiveComment(sourceCode, node) {
|
|
336
|
+
// Comments before the node, in source order.
|
|
337
|
+
const comments = sourceCode
|
|
338
|
+
.getAllComments()
|
|
339
|
+
.filter((comment) => comment.range[1] <= node.range[0]);
|
|
340
|
+
|
|
341
|
+
// The position of the closest comment that is not a tool directive.
|
|
342
|
+
const commentIndex = comments.findLastIndex((comment) => !isDirectiveComment(comment));
|
|
343
|
+
|
|
344
|
+
if (commentIndex < 0) {
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// The comment that may document the node.
|
|
349
|
+
const comment = comments[commentIndex];
|
|
350
|
+
|
|
351
|
+
// Where the comment or directive checked last ends.
|
|
352
|
+
let previousEnd = comment.range[1];
|
|
353
|
+
|
|
354
|
+
for (const next of [...comments.slice(commentIndex + 1), node]) {
|
|
355
|
+
// The text between the previous item and this one.
|
|
356
|
+
const gap = sourceCode.text.slice(previousEnd, next.range[0]);
|
|
357
|
+
|
|
358
|
+
if (!/^\r?\n[ \t]*$/.test(gap)) {
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
previousEnd = next.range[1];
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return comment;
|
|
326
366
|
}
|
|
327
367
|
|
|
328
368
|
/**
|
|
@@ -334,26 +374,19 @@ export function hasImmediateLineComment(sourceCode, node) {
|
|
|
334
374
|
* The source node.
|
|
335
375
|
*
|
|
336
376
|
* @returns {boolean}
|
|
337
|
-
* Whether an ordinary block comment
|
|
377
|
+
* Whether an ordinary block comment sits directly above the node. Tool
|
|
378
|
+
* directive comments may sit between them.
|
|
338
379
|
*/
|
|
339
380
|
export function hasImmediateBlockComment(sourceCode, node) {
|
|
340
|
-
//
|
|
341
|
-
const comment = sourceCode
|
|
342
|
-
.getAllComments()
|
|
343
|
-
.findLast((candidate) => candidate.range[1] <= node.range[0]);
|
|
381
|
+
// The closest comment above the node, looking past tool directive comments.
|
|
382
|
+
const comment = getImmediateNonDirectiveComment(sourceCode, node);
|
|
344
383
|
|
|
345
|
-
if (comment?.type !== "Block"
|
|
384
|
+
if (comment?.type !== "Block") {
|
|
346
385
|
return false;
|
|
347
386
|
}
|
|
348
387
|
|
|
349
388
|
// Checks the comments immediately around the node.
|
|
350
389
|
const { next, previous } = getCommentNeighbours(sourceCode, comment);
|
|
351
|
-
// Confirms there is no blank line before the node.
|
|
352
|
-
const gap = sourceCode.text.slice(comment.range[1], node.range[0]);
|
|
353
390
|
|
|
354
|
-
return (
|
|
355
|
-
next?.range[0] === node.range[0] &&
|
|
356
|
-
isLeadingComment(sourceCode, comment, previous) &&
|
|
357
|
-
/^\r?\n[ \t]*$/.test(gap)
|
|
358
|
-
);
|
|
391
|
+
return next?.range[0] === node.range[0] && isLeadingComment(sourceCode, comment, previous);
|
|
359
392
|
}
|
package/comments/utils/wrap.js
CHANGED
|
@@ -1,8 +1,22 @@
|
|
|
1
1
|
import { getDisplayWidth } from "./source.js";
|
|
2
2
|
|
|
3
|
+
// One word of comment text. A backtick code span counts as part of the word
|
|
4
|
+
// around it, so wrapping never splits the code inside it across lines.
|
|
5
|
+
const wordPattern = /(?:`[^`]*`|[^\s`]+|`)+/g;
|
|
6
|
+
// Text that is only an inline code span, which reads as code, not a sentence.
|
|
7
|
+
const codeSpanOnlyPattern = /^`[^`]*`$/;
|
|
8
|
+
// An inline code span anywhere in the text.
|
|
9
|
+
const codeSpanPattern = /`[^`]*`/;
|
|
10
|
+
// Text that is only a quotation, in straight or curly quotes.
|
|
11
|
+
const quoteOnlyPattern = /^(?:"[^"]*"|'[^']*'|“[^”]*”|‘[^’]*’)$/;
|
|
12
|
+
|
|
3
13
|
/**
|
|
4
14
|
* Wrap words to a maximum line width.
|
|
5
15
|
*
|
|
16
|
+
* A code span is never split: one wider than the line goes whole on its own
|
|
17
|
+
* line. Any other word wider than the line also gets its own line, and is cut
|
|
18
|
+
* at the width only when nothing comes before it on the line.
|
|
19
|
+
*
|
|
6
20
|
* @param {string} text
|
|
7
21
|
* The text to wrap.
|
|
8
22
|
* @param {number} width
|
|
@@ -12,8 +26,8 @@ import { getDisplayWidth } from "./source.js";
|
|
|
12
26
|
* Wrapped lines.
|
|
13
27
|
*/
|
|
14
28
|
export function wrapWords(text, width) {
|
|
15
|
-
// The text's
|
|
16
|
-
const words = text.trim().
|
|
29
|
+
// The text's words, with code spans kept whole.
|
|
30
|
+
const words = text.trim().match(wordPattern) ?? [];
|
|
17
31
|
// The wrapped lines, built up in place.
|
|
18
32
|
const lines = [];
|
|
19
33
|
|
|
@@ -22,6 +36,13 @@ export function wrapWords(text, width) {
|
|
|
22
36
|
|
|
23
37
|
for (const word of words) {
|
|
24
38
|
if (word.length > width && currentLine === "") {
|
|
39
|
+
// Code stays whole even when it overflows the line.
|
|
40
|
+
if (codeSpanPattern.test(word)) {
|
|
41
|
+
lines.push(word);
|
|
42
|
+
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
25
46
|
for (let index = 0; index < word.length; index += width) {
|
|
26
47
|
lines.push(word.slice(index, index + width));
|
|
27
48
|
}
|
|
@@ -74,8 +95,8 @@ export function refillCommentLines(lines, maximumLineLength) {
|
|
|
74
95
|
break;
|
|
75
96
|
}
|
|
76
97
|
|
|
77
|
-
// The words
|
|
78
|
-
const nextWords = nextLine.text.
|
|
98
|
+
// The next line's words, with code spans kept whole.
|
|
99
|
+
const nextWords = nextLine.text.match(wordPattern) ?? [];
|
|
79
100
|
|
|
80
101
|
// Whether the following line was removed after giving up all its
|
|
81
102
|
// words.
|
|
@@ -180,6 +201,9 @@ export function formatSentence(text) {
|
|
|
180
201
|
/**
|
|
181
202
|
* Capitalise the first letter of sentence text.
|
|
182
203
|
*
|
|
204
|
+
* Text that starts with anything other than a letter, such as a code span, a
|
|
205
|
+
* quote or an emoji, is returned unchanged.
|
|
206
|
+
*
|
|
183
207
|
* @param {string} text
|
|
184
208
|
* The sentence text.
|
|
185
209
|
*
|
|
@@ -194,15 +218,14 @@ export function capitaliseSentence(text) {
|
|
|
194
218
|
return text;
|
|
195
219
|
}
|
|
196
220
|
|
|
197
|
-
//
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
if (firstLetter < 0) {
|
|
221
|
+
// Code, quoted text and emoji keep their own casing, so only a sentence
|
|
222
|
+
// that starts with a letter is capitalised.
|
|
223
|
+
if (!/^\p{L}/u.test(trimmedText)) {
|
|
201
224
|
return text;
|
|
202
225
|
}
|
|
203
226
|
|
|
204
|
-
// The leading word,
|
|
205
|
-
const leadingWord = trimmedText.
|
|
227
|
+
// The leading word, before any space or punctuation.
|
|
228
|
+
const leadingWord = trimmedText.match(/^\p{L}[\p{L}\p{N}]*/u)?.[0] ?? "";
|
|
206
229
|
|
|
207
230
|
// A camelCase word (lowercase start, later uppercase) is a code identifier
|
|
208
231
|
// and must keep its own casing rather than sentence casing.
|
|
@@ -211,15 +234,20 @@ export function capitaliseSentence(text) {
|
|
|
211
234
|
}
|
|
212
235
|
|
|
213
236
|
// The first letter character.
|
|
214
|
-
const letter = trimmedText[
|
|
237
|
+
const letter = trimmedText[0];
|
|
215
238
|
// The text with its first letter capitalised.
|
|
216
|
-
const formattedText = `${
|
|
239
|
+
const formattedText = `${letter.toLocaleUpperCase()}${trimmedText.slice(1)}`;
|
|
217
240
|
|
|
218
241
|
return text.replace(trimmedText, formattedText);
|
|
219
242
|
}
|
|
220
243
|
|
|
221
244
|
/**
|
|
222
|
-
* Add
|
|
245
|
+
* Add a full stop to sentence text that has no closing punctuation.
|
|
246
|
+
*
|
|
247
|
+
* Text ending in a colon introduces what follows, and text that is only a code
|
|
248
|
+
* span or a quotation is not a sentence, so both are returned unchanged. A
|
|
249
|
+
* sentence that ends in a code span or quotation gets its full stop after the
|
|
250
|
+
* closing mark.
|
|
223
251
|
*
|
|
224
252
|
* @param {string} text
|
|
225
253
|
* The sentence text.
|
|
@@ -231,7 +259,13 @@ export function addTerminalPunctuation(text) {
|
|
|
231
259
|
// The sentence text, without leading or trailing whitespace.
|
|
232
260
|
const trimmedText = text.trim();
|
|
233
261
|
|
|
234
|
-
if (
|
|
262
|
+
if (
|
|
263
|
+
trimmedText === "" ||
|
|
264
|
+
trimmedText.startsWith("@") ||
|
|
265
|
+
/[.!?:]$/.test(trimmedText) ||
|
|
266
|
+
codeSpanOnlyPattern.test(trimmedText) ||
|
|
267
|
+
quoteOnlyPattern.test(trimmedText)
|
|
268
|
+
) {
|
|
235
269
|
return text;
|
|
236
270
|
}
|
|
237
271
|
|
package/comments.json
CHANGED
package/layers.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import base from "./base.json" with { type: "json" };
|
|
2
|
+
import comments from "./comments.json" with { type: "json" };
|
|
3
|
+
import vueConfig from "./vue.json" with { type: "json" };
|
|
4
|
+
|
|
5
|
+
// The Vue layer. It includes base as an object because Oxlint rejects the file
|
|
6
|
+
// path that vue.json uses to extend base.
|
|
7
|
+
export const vue = { ...vueConfig, extends: [base] };
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Build a Vite+ lint block from the selected layers and local settings.
|
|
11
|
+
*
|
|
12
|
+
* @param {object[]} layers
|
|
13
|
+
* The layer objects to extend. When two layers set the same env or global,
|
|
14
|
+
* the later layer's value is used.
|
|
15
|
+
* @param {object} local
|
|
16
|
+
* Project settings that take priority over the layers. Its extends list is
|
|
17
|
+
* ignored, so every layer must be passed in layers.
|
|
18
|
+
*
|
|
19
|
+
* @returns {object}
|
|
20
|
+
* A lint block with inherited environments and globals at the top level.
|
|
21
|
+
*/
|
|
22
|
+
export function lintConfig(layers, local = {}) {
|
|
23
|
+
// The environments inherited from the selected layers.
|
|
24
|
+
const env = {};
|
|
25
|
+
// The globals inherited from the selected layers.
|
|
26
|
+
const globals = {};
|
|
27
|
+
|
|
28
|
+
for (const layer of layers) {
|
|
29
|
+
collectEnvAndGlobals(layer, env, globals);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
...local,
|
|
34
|
+
env: { ...env, ...local.env },
|
|
35
|
+
globals: { ...globals, ...local.globals },
|
|
36
|
+
extends: layers,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Copy a layer's env and globals into the collected values. The layers it
|
|
42
|
+
* extends are copied first, so the layer's own values win.
|
|
43
|
+
*
|
|
44
|
+
* @param {object} layer
|
|
45
|
+
* The layer to read, along with every layer it extends.
|
|
46
|
+
* @param {object} env
|
|
47
|
+
* The collected environments, updated in place.
|
|
48
|
+
* @param {object} globals
|
|
49
|
+
* The collected globals, updated in place.
|
|
50
|
+
*/
|
|
51
|
+
function collectEnvAndGlobals(layer, env, globals) {
|
|
52
|
+
for (const extendedLayer of layer.extends ?? []) {
|
|
53
|
+
collectEnvAndGlobals(extendedLayer, env, globals);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
Object.assign(env, layer.env);
|
|
57
|
+
Object.assign(globals, layer.globals);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export { base, comments };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lewishowles/lint-config",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Shared oxlint configuration for Lewis Howles projects",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"config",
|
|
@@ -24,18 +24,23 @@
|
|
|
24
24
|
"comments",
|
|
25
25
|
"comments.json",
|
|
26
26
|
"imports.json",
|
|
27
|
+
"layers.js",
|
|
28
|
+
"testing",
|
|
27
29
|
"vue.json",
|
|
28
30
|
"README.md"
|
|
29
31
|
],
|
|
30
32
|
"type": "module",
|
|
31
33
|
"imports": {
|
|
32
|
-
"#comments/*": "./comments/*"
|
|
34
|
+
"#comments/*": "./comments/*",
|
|
35
|
+
"#testing/*": "./testing/*"
|
|
33
36
|
},
|
|
34
37
|
"exports": {
|
|
35
38
|
"./base.json": "./base.json",
|
|
36
39
|
"./comments.json": "./comments.json",
|
|
37
40
|
"./comments/plugin": "./comments/plugin.js",
|
|
38
41
|
"./imports.json": "./imports.json",
|
|
42
|
+
"./layers": "./layers.js",
|
|
43
|
+
"./testing/plugin": "./testing/plugin.js",
|
|
39
44
|
"./vue.json": "./vue.json"
|
|
40
45
|
},
|
|
41
46
|
"publishConfig": {
|
|
@@ -46,15 +51,15 @@
|
|
|
46
51
|
"lint:fix": "vp check --fix",
|
|
47
52
|
"prepare": "vp config --no-agent",
|
|
48
53
|
"publint": "publint",
|
|
49
|
-
"test:unit": "node --test test/comments/*.test.js"
|
|
54
|
+
"test:unit": "node --test test/comments/*.test.js test/layers/*.test.js test/testing/*.test.js"
|
|
50
55
|
},
|
|
51
56
|
"devDependencies": {
|
|
52
57
|
"publint": "^0.3.22",
|
|
53
|
-
"vite-plus": "0.
|
|
58
|
+
"vite-plus": "1.0.0"
|
|
54
59
|
},
|
|
55
60
|
"peerDependencies": {
|
|
56
61
|
"@stylistic/eslint-plugin": "*",
|
|
57
|
-
"vite-plus": "0.
|
|
62
|
+
"vite-plus": "^1.0.0"
|
|
58
63
|
},
|
|
59
64
|
"engines": {
|
|
60
65
|
"node": ">=20"
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// The array methods that count as a lookup when their callback reads an
|
|
2
|
+
// element's text.
|
|
3
|
+
const searchMethods = new Set([
|
|
4
|
+
"filter",
|
|
5
|
+
"find",
|
|
6
|
+
"findLast",
|
|
7
|
+
"findIndex",
|
|
8
|
+
"findLastIndex",
|
|
9
|
+
"some",
|
|
10
|
+
"every",
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
// The DOM properties that hold an element's visible text.
|
|
14
|
+
const textProperties = new Set(["textContent", "innerText"]);
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Return whether a callback body reads an element's text anywhere inside it.
|
|
18
|
+
*
|
|
19
|
+
* @param {object} node
|
|
20
|
+
* The callback body, or a node inside it while searching.
|
|
21
|
+
*
|
|
22
|
+
* @returns {boolean}
|
|
23
|
+
* Whether any part of the body calls text() or reads textContent or
|
|
24
|
+
* innerText.
|
|
25
|
+
*/
|
|
26
|
+
function containsTextRead(node) {
|
|
27
|
+
// A nested search callback is reported at its own call, so it isn't
|
|
28
|
+
// counted again here.
|
|
29
|
+
if (isSearchCallback(node)) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (isTextRead(node)) {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
for (const [key, child] of Object.entries(node)) {
|
|
38
|
+
if (key === "parent") {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (Array.isArray(child)) {
|
|
43
|
+
for (const entry of child) {
|
|
44
|
+
if (entry?.type && containsTextRead(entry)) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
} else if (child?.type && containsTextRead(child)) {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Return whether a node is an inline callback passed to an array search, such
|
|
58
|
+
* as the arrow function in items.find(item => item.id === 1).
|
|
59
|
+
*
|
|
60
|
+
* @param {object|undefined} node
|
|
61
|
+
* The node to inspect, or undefined when a call has no arguments.
|
|
62
|
+
*
|
|
63
|
+
* @returns {boolean}
|
|
64
|
+
* Whether the rule checks this callback's body for text reads.
|
|
65
|
+
*/
|
|
66
|
+
function isSearchCallback(node) {
|
|
67
|
+
if (node?.type !== "ArrowFunctionExpression" && node?.type !== "FunctionExpression") {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// The call the function is passed to, if it is passed to one.
|
|
72
|
+
const call = node.parent;
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
call?.type === "CallExpression" &&
|
|
76
|
+
call.arguments[0] === node &&
|
|
77
|
+
call.callee.type === "MemberExpression" &&
|
|
78
|
+
!call.callee.computed &&
|
|
79
|
+
searchMethods.has(call.callee.property.name)
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Return whether a node itself reads an element's text, by calling text() or
|
|
85
|
+
* reading textContent or innerText.
|
|
86
|
+
*
|
|
87
|
+
* @param {object} node
|
|
88
|
+
* The AST node to inspect.
|
|
89
|
+
*
|
|
90
|
+
* @returns {boolean}
|
|
91
|
+
* Whether the node calls text() or reads a DOM text property.
|
|
92
|
+
*/
|
|
93
|
+
function isTextRead(node) {
|
|
94
|
+
if (
|
|
95
|
+
node.type === "CallExpression" &&
|
|
96
|
+
node.callee.type === "MemberExpression" &&
|
|
97
|
+
!node.callee.computed &&
|
|
98
|
+
node.callee.property.name === "text"
|
|
99
|
+
) {
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
node.type === "MemberExpression" && !node.computed && textProperties.has(node.property.name)
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export default {
|
|
109
|
+
meta: {
|
|
110
|
+
docs: { description: "Find test elements by data-test attributes instead of visible text." },
|
|
111
|
+
type: "problem",
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Create the rule's node visitor.
|
|
116
|
+
*
|
|
117
|
+
* @param {object} context
|
|
118
|
+
* The Oxlint rule context.
|
|
119
|
+
*
|
|
120
|
+
* @returns {object}
|
|
121
|
+
* The visitor for array search calls.
|
|
122
|
+
*/
|
|
123
|
+
createOnce(context) {
|
|
124
|
+
return {
|
|
125
|
+
/**
|
|
126
|
+
* Report an array search when its inline callback reads visible
|
|
127
|
+
* text.
|
|
128
|
+
*
|
|
129
|
+
* @param {object} node
|
|
130
|
+
* The call expression being checked.
|
|
131
|
+
*/
|
|
132
|
+
CallExpression(node) {
|
|
133
|
+
// Only an inline callback is checked, because a callback passed
|
|
134
|
+
// by name has its body somewhere else.
|
|
135
|
+
const callback = node.arguments[0];
|
|
136
|
+
|
|
137
|
+
if (!isSearchCallback(callback) || !containsTextRead(callback.body)) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
context.report({
|
|
142
|
+
message: "Find test elements by a data-test attribute instead of visible text.",
|
|
143
|
+
node,
|
|
144
|
+
});
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
},
|
|
148
|
+
};
|
package/vue.json
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
},
|
|
13
13
|
"rules": {
|
|
14
14
|
"vue/no-arrow-functions-in-watch": "error",
|
|
15
|
+
"vue/no-async-in-computed-properties": "error",
|
|
15
16
|
"vue/no-deprecated-data-object-declaration": "error",
|
|
16
17
|
"vue/no-deprecated-delete-set": "error",
|
|
17
18
|
"vue/no-deprecated-destroyed-lifecycle": "error",
|
|
@@ -20,6 +21,7 @@
|
|
|
20
21
|
"vue/no-deprecated-vue-config-keycodes": "error",
|
|
21
22
|
"vue/no-export-in-script-setup": "error",
|
|
22
23
|
"vue/no-lifecycle-after-await": "error",
|
|
24
|
+
"vue/no-side-effects-in-computed-properties": "error",
|
|
23
25
|
"vue/prefer-import-from-vue": "error",
|
|
24
26
|
"vue/return-in-computed-property": "error",
|
|
25
27
|
"vue/valid-define-emits": "error",
|