@dxos/eslint-plugin-rules 0.14.0 → 0.15.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 +2 -0
- package/index.js +2 -0
- package/package.json +2 -2
- package/rules/no-error-class-name.js +89 -0
- package/rules/operation-key-shape.js +2 -0
- package/src/no-error-class-name.test.ts +49 -0
package/CHANGELOG.md
CHANGED
package/index.js
CHANGED
|
@@ -17,6 +17,7 @@ import noBareDotImports from './rules/no-bare-dot-imports.js';
|
|
|
17
17
|
import noDeadTailwindLogical from './rules/no-dead-tailwind-logical.js';
|
|
18
18
|
import noEffectRunPromise from './rules/no-effect-run-promise.js';
|
|
19
19
|
import noEmptyPromiseCatch from './rules/no-empty-promise-catch.js';
|
|
20
|
+
import noErrorClassName from './rules/no-error-class-name.js';
|
|
20
21
|
import noSimilarSiblingFiles from './rules/no-similar-sibling-files.js';
|
|
21
22
|
import operationKeyShape from './rules/operation-key-shape.js';
|
|
22
23
|
import preferSizingUtilities from './rules/prefer-sizing-utilities.js';
|
|
@@ -46,6 +47,7 @@ const plugin = {
|
|
|
46
47
|
'operation-key-shape': operationKeyShape,
|
|
47
48
|
'prefer-sizing-utilities': preferSizingUtilities,
|
|
48
49
|
'no-empty-promise-catch': noEmptyPromiseCatch,
|
|
50
|
+
'no-error-class-name': noErrorClassName,
|
|
49
51
|
'no-similar-sibling-files': noSimilarSiblingFiles,
|
|
50
52
|
'translation-key-format': translationKeyFormat,
|
|
51
53
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/eslint-plugin-rules",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"homepage": "https://dxos.org",
|
|
5
5
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
6
6
|
"repository": {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
18
|
"@typescript-eslint/parser": "^8.71.0",
|
|
19
|
-
"@dxos/plugin-chess": "0.
|
|
19
|
+
"@dxos/plugin-chess": "0.15.0"
|
|
20
20
|
},
|
|
21
21
|
"publishConfig": {
|
|
22
22
|
"access": "public"
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
const ERROR_CLASS_RE = /^[A-Z]\w*Error$/;
|
|
8
|
+
|
|
9
|
+
const EQUALITY_OPERATORS = new Set(['===', '!==', '==', '!=']);
|
|
10
|
+
|
|
11
|
+
/** The trailing identifier of `Foo` or `Ns.Foo`. */
|
|
12
|
+
const className = (node) => {
|
|
13
|
+
if (node.type === 'Identifier') {
|
|
14
|
+
return node.name;
|
|
15
|
+
}
|
|
16
|
+
if (node.type === 'MemberExpression' && !node.computed && node.property.type === 'Identifier') {
|
|
17
|
+
return node.property.name;
|
|
18
|
+
}
|
|
19
|
+
return undefined;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const isPropertyAccess = (node, property) =>
|
|
23
|
+
node?.type === 'MemberExpression' &&
|
|
24
|
+
!node.computed &&
|
|
25
|
+
node.property.type === 'Identifier' &&
|
|
26
|
+
node.property.name === property;
|
|
27
|
+
|
|
28
|
+
const unwrapChain = (node) => (node?.type === 'ChainExpression' ? node.expression : node);
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Disallow matching errors by their class's runtime `name`, which minification renames.
|
|
32
|
+
* @example
|
|
33
|
+
* // bad
|
|
34
|
+
* registerError(UnknownProtocolError.name, decode);
|
|
35
|
+
* if (err.constructor.name === 'TimeoutError') {}
|
|
36
|
+
*
|
|
37
|
+
* // good
|
|
38
|
+
* registerError('UnknownProtocolError', decode);
|
|
39
|
+
* if (TimeoutError.is(err)) {}
|
|
40
|
+
*/
|
|
41
|
+
export default {
|
|
42
|
+
meta: {
|
|
43
|
+
type: 'problem',
|
|
44
|
+
docs: {
|
|
45
|
+
description: 'Disallow identifying errors by the class name, which minified builds rename.',
|
|
46
|
+
recommended: true,
|
|
47
|
+
},
|
|
48
|
+
messages: {
|
|
49
|
+
errorClassName:
|
|
50
|
+
'`{{name}}.name` is renamed by minification. Use the declared name string, or `{{name}}.is(error)` for a `BaseError.extend` class.',
|
|
51
|
+
constructorName:
|
|
52
|
+
'`constructor.name` is renamed by minification. Compare `error.name`, or use `.is(error)` for a `BaseError.extend` class.',
|
|
53
|
+
},
|
|
54
|
+
schema: [],
|
|
55
|
+
},
|
|
56
|
+
create(context) {
|
|
57
|
+
return {
|
|
58
|
+
MemberExpression(node) {
|
|
59
|
+
if (!isPropertyAccess(node, 'name')) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const name = className(node.object);
|
|
63
|
+
if (name && ERROR_CLASS_RE.test(name)) {
|
|
64
|
+
context.report({ node, messageId: 'errorClassName', data: { name } });
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
BinaryExpression(node) {
|
|
68
|
+
if (!EQUALITY_OPERATORS.has(node.operator)) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
for (const [access, other] of [
|
|
72
|
+
[node.left, node.right],
|
|
73
|
+
[node.right, node.left],
|
|
74
|
+
]) {
|
|
75
|
+
const member = unwrapChain(access);
|
|
76
|
+
if (
|
|
77
|
+
isPropertyAccess(member, 'name') &&
|
|
78
|
+
isPropertyAccess(member.object, 'constructor') &&
|
|
79
|
+
other.type === 'Literal' &&
|
|
80
|
+
typeof other.value === 'string' &&
|
|
81
|
+
ERROR_CLASS_RE.test(other.value)
|
|
82
|
+
) {
|
|
83
|
+
context.report({ node, messageId: 'constructorName' });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
},
|
|
89
|
+
};
|
|
@@ -44,6 +44,7 @@ const VERBS = new Set([
|
|
|
44
44
|
'close',
|
|
45
45
|
'collect',
|
|
46
46
|
'complete',
|
|
47
|
+
'configure',
|
|
47
48
|
'connect',
|
|
48
49
|
'convert',
|
|
49
50
|
'copy',
|
|
@@ -84,6 +85,7 @@ const VERBS = new Set([
|
|
|
84
85
|
'insert',
|
|
85
86
|
'inspect',
|
|
86
87
|
'install',
|
|
88
|
+
'invite',
|
|
87
89
|
'invoke',
|
|
88
90
|
'join',
|
|
89
91
|
'label',
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { RuleTester } from 'eslint';
|
|
6
|
+
import { describe, it } from 'vitest';
|
|
7
|
+
|
|
8
|
+
import rule from '../rules/no-error-class-name.js';
|
|
9
|
+
|
|
10
|
+
const ruleTester = new RuleTester({
|
|
11
|
+
languageOptions: {
|
|
12
|
+
ecmaVersion: 2022,
|
|
13
|
+
sourceType: 'module',
|
|
14
|
+
parser: await import('@typescript-eslint/parser'),
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe('no-error-class-name', () => {
|
|
19
|
+
it('reports identity checks on the minified class name', () => {
|
|
20
|
+
ruleTester.run('no-error-class-name', rule, {
|
|
21
|
+
valid: [
|
|
22
|
+
"registerError('UnknownProtocolError', decode);",
|
|
23
|
+
'if (TimeoutError.is(err)) {}',
|
|
24
|
+
"if (err.name === 'TimeoutError') {}",
|
|
25
|
+
// Non-error classes and built-ins are out of scope.
|
|
26
|
+
'log(Foo.name);',
|
|
27
|
+
"if (value.constructor.name === 'ArrayBuffer') {}",
|
|
28
|
+
],
|
|
29
|
+
invalid: [
|
|
30
|
+
{
|
|
31
|
+
code: 'registerError(UnknownProtocolError.name, decode);',
|
|
32
|
+
errors: [{ messageId: 'errorClassName' }],
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
code: 'new Set([EchoError.EntityNotFoundError.name]);',
|
|
36
|
+
errors: [{ messageId: 'errorClassName' }],
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
code: "if (err.constructor.name === 'TimeoutError') {}",
|
|
40
|
+
errors: [{ messageId: 'constructorName' }],
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
code: "if ('TimeoutError' !== err?.constructor.name) {}",
|
|
44
|
+
errors: [{ messageId: 'constructorName' }],
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
});
|