@lewishowles/lint-config 0.8.0 → 0.9.1

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 CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.1: 2026-10-10
4
+
5
+ ### Fixes
6
+
7
+ - `comments/formatting` accepts a comment as the only thing between a pair of braces, such as in an empty catch block, function body or object literal. It still checks the comment's sentence formatting.
8
+
9
+ ## 0.9.0: 2026-10-07
10
+
11
+ ### Changes
12
+
13
+ - `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.
14
+ - `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.
15
+ - `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.
16
+ - `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.
17
+ - `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.
18
+
19
+ ### Fixes
20
+
21
+ - `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.
22
+ - 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.
23
+
3
24
  ## 0.8.0: 2026-10-05
4
25
 
5
26
  ### 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": {
@@ -347,6 +347,10 @@ function getBlockCommentDisplayLines(commentText, indentation) {
347
347
  * the fix never edits the directive. When an ordinary comment sits between them
348
348
  * instead, only the indentation is fixed and the gap is left alone.
349
349
  *
350
+ * A comment that is the only thing between a pair of braces, such as in an
351
+ * empty catch block or object literal, has no code to sit above, so it is left
352
+ * where it is.
353
+ *
350
354
  * @param {object} sourceCode
351
355
  * The Oxlint source code object.
352
356
  * @param {object} comment
@@ -364,7 +368,11 @@ function getLeadingCommentPlacement(sourceCode, comment, lastComment, comments)
364
368
  // The code token the comment documents, and the token before the comment.
365
369
  const { next, previous } = getCommentNeighbours(sourceCode, comment);
366
370
 
367
- if (next === null || !isLeadingComment(sourceCode, comment, previous)) {
371
+ if (
372
+ next === null ||
373
+ !isLeadingComment(sourceCode, comment, previous) ||
374
+ (previous?.value === "{" && next.value === "}")
375
+ ) {
368
376
  return null;
369
377
  }
370
378
 
@@ -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 object
156
- * parameter. Defaults to options.
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.8.0",
3
+ "version": "0.9.1",
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,8 @@
1
+ import noTextLookups from "./rules/no-text-lookups.js";
2
+
3
+ export default {
4
+ meta: { name: "testing" },
5
+ rules: {
6
+ "no-text-lookups": noTextLookups,
7
+ },
8
+ };
@@ -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",