@lewishowles/lint-config 0.8.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 +15 -0
- package/README.md +22 -5
- package/base.json +47 -0
- package/comments/utils/documentation.js +26 -2
- package/package.json +6 -3
- package/testing/plugin.js +8 -0
- package/testing/rules/no-text-lookups.js +148 -0
- package/vue.json +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.0: 2026-10-07
|
|
4
|
+
|
|
5
|
+
### Changes
|
|
6
|
+
|
|
7
|
+
- `base` now reports an error for TypeScript non-null assertions (`!`) in `.ts` files and Vue `<script lang="ts">` blocks (`typescript/no-non-null-assertion`). Check for `null` or `undefined` before using the value instead. The rule finds nothing in components or helpers, so upgrading needs no fixes there.
|
|
8
|
+
- `vue` now reports an error when a computed property changes state (`vue/no-side-effects-in-computed-properties`) or runs async code (`vue/no-async-in-computed-properties`). Neither rule finds anything in components or helpers, so upgrading needs no fixes there. `vue/no-mutating-props`, `vue/no-use-v-if-with-v-for` and `vue/require-explicit-emits` will follow once Oxlint supports them.
|
|
9
|
+
- `base` now reports an error when a unit test (`*.test.*` or `*.spec.*`) searches an array for an element by reading its visible text (`testing/no-text-lookups`). Find the element by a `data-test` attribute instead. Playwright and Cypress files are unaffected.
|
|
10
|
+
- `comments/function-documentation` is stricter: a parameter written as an array, such as `function f([x, y])` or `function f(...[x, y])`, now needs one `@param` for the whole array. The tag can have any name, as it can for a destructured object, and a missing tag is reported as `options`. The items inside the array still need no `@param` of their own.
|
|
11
|
+
- `comments/function-documentation` is stricter for object rest properties: `{ a, ...rest }` now needs `@param options.rest`, and `{ a: { ...rest } }` needs `@param options.a.rest`. A documented root name replaces `options` in those paths.
|
|
12
|
+
|
|
13
|
+
### Fixes
|
|
14
|
+
|
|
15
|
+
- `comments/function-documentation` now checks properties inside a nested object that has a default value. In `function f({ a: { b = 1 } = {} })`, `b` now needs `@param options.a.b`, as it already did without the `= {}`. Projects that skipped those tags will see new errors.
|
|
16
|
+
- In test files (`*.test.*`, `*.spec.*`, `*.pw.*` and `*.cy.*`), blank lines between calls, awaits, assignments and multiline expressions are now up to you, so you can separate test steps with them. `base` no longer adds or removes those lines in tests. Blank lines around declarations, blocks, `return` and `break` are still checked. If you ran the 0.8 auto-fix on your tests, you may want to restore the blank lines it removed.
|
|
17
|
+
|
|
3
18
|
## 0.8.0: 2026-10-05
|
|
4
19
|
|
|
5
20
|
### Changes
|
package/README.md
CHANGED
|
@@ -190,11 +190,11 @@ Plugins are additive and deduplicated: your local plugins are added to the share
|
|
|
190
190
|
|
|
191
191
|
## Layers
|
|
192
192
|
|
|
193
|
-
| Layer | File | Contents
|
|
194
|
-
| ---------- | --------------- |
|
|
195
|
-
| `base` | `base.json` | Correctness and formatting rules, import sorting, `import`/`oxc`/`typescript`/`unicorn` plugins |
|
|
196
|
-
| `comments` | `comments.json` | Optional comment-formatting rules, variable-declaration documentation, JSDoc checks
|
|
197
|
-
| `vue` | `vue.json` | Extends `base`, adds the `vue` plugin, Vue compiler macro globals, Vue-specific rules
|
|
193
|
+
| Layer | File | Contents |
|
|
194
|
+
| ---------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
195
|
+
| `base` | `base.json` | Correctness and formatting rules, import sorting, `import`/`oxc`/`typescript`/`unicorn` plugins; errors when unit tests (`*.test.*`, `*.spec.*`) find elements by visible text |
|
|
196
|
+
| `comments` | `comments.json` | Optional comment-formatting rules, variable-declaration documentation, JSDoc checks |
|
|
197
|
+
| `vue` | `vue.json` | Extends `base`, adds the `vue` plugin, Vue compiler macro globals, Vue-specific rules |
|
|
198
198
|
|
|
199
199
|
### Import sorting
|
|
200
200
|
|
|
@@ -218,6 +218,23 @@ This puts named imports first, including `import type { … }` and imports with
|
|
|
218
218
|
|
|
219
219
|
The base layer reports imports from a parent folder (`../`), with no automatic fix. Same-folder (`./`), `@/` alias and package `#` subpath imports are allowed. After upgrading, any existing `../` imports fail lint until they move to an `@/` alias or, in a package without one, to [package subpath imports](https://nodejs.org/api/packages.html#subpath-imports).
|
|
220
220
|
|
|
221
|
+
### Text lookups in unit tests
|
|
222
|
+
|
|
223
|
+
The base layer's `testing/no-text-lookups` rule reports an array search such as `find`, `filter` or `some` when its inline callback reads `.text()`, `.textContent` or `.innerText`. Find the element by a `data-test` attribute instead. It doesn't report data-test lookups, assertions about the text of an element you have already found, or callbacks passed by name. It applies to `*.test.*` and `*.spec.*` files, not Playwright `*.pw.*` or Cypress `*.cy.*` files.
|
|
224
|
+
|
|
225
|
+
To turn it off, add an override for unit tests to your project's `.oxlintrc.json`:
|
|
226
|
+
|
|
227
|
+
```json
|
|
228
|
+
{
|
|
229
|
+
"overrides": [
|
|
230
|
+
{
|
|
231
|
+
"files": ["**/*.test.*", "**/*.spec.*"],
|
|
232
|
+
"rules": { "testing/no-text-lookups": "off" }
|
|
233
|
+
}
|
|
234
|
+
]
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
221
238
|
## What stays repo-local
|
|
222
239
|
|
|
223
240
|
- `ignorePatterns`, since every project has different build output and tool directories
|
package/base.json
CHANGED
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
{
|
|
6
6
|
"name": "vite-plus",
|
|
7
7
|
"specifier": "vite-plus/oxlint-plugin"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"name": "testing",
|
|
11
|
+
"specifier": "@lewishowles/lint-config/testing/plugin"
|
|
8
12
|
}
|
|
9
13
|
],
|
|
10
14
|
"categories": {
|
|
@@ -26,6 +30,7 @@
|
|
|
26
30
|
"no-useless-assignment": "error",
|
|
27
31
|
"preserve-caught-error": "error",
|
|
28
32
|
"import/no-relative-parent-imports": "error",
|
|
33
|
+
"typescript/no-non-null-assertion": "error",
|
|
29
34
|
"sort-imports": [
|
|
30
35
|
"error",
|
|
31
36
|
{
|
|
@@ -89,6 +94,48 @@
|
|
|
89
94
|
{
|
|
90
95
|
"files": ["**/vite.config.*", "**/vitest.config.*", "**/playwright*.config.*"],
|
|
91
96
|
"env": { "node": true }
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"files": ["**/*.test.*", "**/*.spec.*", "**/*.pw.*", "**/*.cy.*"],
|
|
100
|
+
"rules": {
|
|
101
|
+
"@stylistic/padding-line-between-statements": [
|
|
102
|
+
"error",
|
|
103
|
+
{ "blankLine": "always", "prev": "*", "next": "break" },
|
|
104
|
+
{ "blankLine": "always", "prev": ["const", "let"], "next": "*" },
|
|
105
|
+
{ "blankLine": "always", "prev": "*", "next": ["const", "let"] },
|
|
106
|
+
{ "blankLine": "always", "prev": "*", "next": "return" },
|
|
107
|
+
{
|
|
108
|
+
"blankLine": "always",
|
|
109
|
+
"prev": [
|
|
110
|
+
"block-like",
|
|
111
|
+
"class",
|
|
112
|
+
"do",
|
|
113
|
+
"for",
|
|
114
|
+
"function",
|
|
115
|
+
"if",
|
|
116
|
+
"switch",
|
|
117
|
+
"try",
|
|
118
|
+
"while"
|
|
119
|
+
],
|
|
120
|
+
"next": "*"
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
"blankLine": "always",
|
|
124
|
+
"prev": "*",
|
|
125
|
+
"next": ["block-like", "class", "do", "for", "function", "if", "switch", "try", "while"]
|
|
126
|
+
},
|
|
127
|
+
{ "blankLine": "never", "prev": "const", "next": "const" },
|
|
128
|
+
{ "blankLine": "never", "prev": "let", "next": "let" },
|
|
129
|
+
{ "blankLine": "always", "prev": "multiline-const", "next": "*" },
|
|
130
|
+
{ "blankLine": "always", "prev": "*", "next": "multiline-const" }
|
|
131
|
+
]
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
"files": ["**/*.test.*", "**/*.spec.*"],
|
|
136
|
+
"rules": {
|
|
137
|
+
"testing/no-text-lookups": "error"
|
|
138
|
+
}
|
|
92
139
|
}
|
|
93
140
|
],
|
|
94
141
|
"options": {
|
|
@@ -112,6 +112,13 @@ function getObjectPatternPaths(sourceCode, node, parentPath) {
|
|
|
112
112
|
const paths = [parentPath];
|
|
113
113
|
|
|
114
114
|
for (const property of node.properties) {
|
|
115
|
+
// A rest property holds every key not named before it, so it is
|
|
116
|
+
// documented at its own dotted path, such as options.rest.
|
|
117
|
+
if (property.type === "RestElement") {
|
|
118
|
+
paths.push(`${parentPath}.${property.argument.name}`);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
115
122
|
if (property.type !== "Property") {
|
|
116
123
|
continue;
|
|
117
124
|
}
|
|
@@ -135,6 +142,14 @@ function getObjectPatternPaths(sourceCode, node, parentPath) {
|
|
|
135
142
|
|
|
136
143
|
if (value.type === "AssignmentPattern") {
|
|
137
144
|
paths.push(`[${propertyPath}=${getDefaultValue(sourceCode, value.right)}]`);
|
|
145
|
+
|
|
146
|
+
// A defaulted nested object still needs its own properties
|
|
147
|
+
// documented. The first nested path is the object itself, which
|
|
148
|
+
// is already listed above with its default, so it is skipped.
|
|
149
|
+
if (value.left.type === "ObjectPattern") {
|
|
150
|
+
paths.push(...getObjectPatternPaths(sourceCode, value.left, propertyPath).slice(1));
|
|
151
|
+
}
|
|
152
|
+
|
|
138
153
|
continue;
|
|
139
154
|
}
|
|
140
155
|
|
|
@@ -152,8 +167,8 @@ function getObjectPatternPaths(sourceCode, node, parentPath) {
|
|
|
152
167
|
* @param {object} node
|
|
153
168
|
* The parameter node.
|
|
154
169
|
* @param {string} [rootPath]
|
|
155
|
-
* The documented name that starts each path for a destructured
|
|
156
|
-
*
|
|
170
|
+
* The documented name that starts each path for a destructured parameter.
|
|
171
|
+
* Defaults to options.
|
|
157
172
|
*
|
|
158
173
|
* @returns {string[]}
|
|
159
174
|
* The required JSDoc parameter paths.
|
|
@@ -167,6 +182,15 @@ function getParameterPaths(sourceCode, node, rootPath = "options") {
|
|
|
167
182
|
return [node.argument.name];
|
|
168
183
|
}
|
|
169
184
|
|
|
185
|
+
// An array is documented as one value. Its items get no paths of their
|
|
186
|
+
// own because JSDoc has no standard way to name them.
|
|
187
|
+
if (
|
|
188
|
+
node.type === "ArrayPattern" ||
|
|
189
|
+
(node.type === "RestElement" && node.argument.type === "ArrayPattern")
|
|
190
|
+
) {
|
|
191
|
+
return [rootPath];
|
|
192
|
+
}
|
|
193
|
+
|
|
170
194
|
if (node.type === "ObjectPattern") {
|
|
171
195
|
return getObjectPatternPaths(sourceCode, node, rootPath);
|
|
172
196
|
}
|
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",
|
|
@@ -25,12 +25,14 @@
|
|
|
25
25
|
"comments.json",
|
|
26
26
|
"imports.json",
|
|
27
27
|
"layers.js",
|
|
28
|
+
"testing",
|
|
28
29
|
"vue.json",
|
|
29
30
|
"README.md"
|
|
30
31
|
],
|
|
31
32
|
"type": "module",
|
|
32
33
|
"imports": {
|
|
33
|
-
"#comments/*": "./comments/*"
|
|
34
|
+
"#comments/*": "./comments/*",
|
|
35
|
+
"#testing/*": "./testing/*"
|
|
34
36
|
},
|
|
35
37
|
"exports": {
|
|
36
38
|
"./base.json": "./base.json",
|
|
@@ -38,6 +40,7 @@
|
|
|
38
40
|
"./comments/plugin": "./comments/plugin.js",
|
|
39
41
|
"./imports.json": "./imports.json",
|
|
40
42
|
"./layers": "./layers.js",
|
|
43
|
+
"./testing/plugin": "./testing/plugin.js",
|
|
41
44
|
"./vue.json": "./vue.json"
|
|
42
45
|
},
|
|
43
46
|
"publishConfig": {
|
|
@@ -48,7 +51,7 @@
|
|
|
48
51
|
"lint:fix": "vp check --fix",
|
|
49
52
|
"prepare": "vp config --no-agent",
|
|
50
53
|
"publint": "publint",
|
|
51
|
-
"test:unit": "node --test test/comments/*.test.js test/layers/*.test.js"
|
|
54
|
+
"test:unit": "node --test test/comments/*.test.js test/layers/*.test.js test/testing/*.test.js"
|
|
52
55
|
},
|
|
53
56
|
"devDependencies": {
|
|
54
57
|
"publint": "^0.3.22",
|
|
@@ -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",
|