@ethlete/eslint-plugin 1.0.0-next.29 → 1.0.0-next.31
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 +13 -0
- package/package.json +1 -1
- package/src/index.js +4 -0
- package/src/rules/no-native-title.js +60 -0
- package/src/rules/no-silent-catch-error.js +125 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.0-next.31
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- `no-silent-catch-error` also flags a `try/catch` whose catch ignores the error and returns a fallback value, such as `catch { return [] }`.
|
|
8
|
+
|
|
9
|
+
## 1.0.0-next.30
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- Add `ethlete/no-native-title`, off in `recommendedTemplate` until projects opt in: it reports a native `title` attribute (`title="…"`, `[title]`, `[attr.title]`) on HTML elements, because the SDK tooltip replaces it. Components with a dash in their name keep their own `title` input.
|
|
14
|
+
- Add the opt-in `no-silent-catch-error` rule, which flags a `catchError` handler that ignores the error and returns `of()`, `EMPTY` or `NEVER`.
|
|
15
|
+
|
|
3
16
|
## 1.0.0-next.29
|
|
4
17
|
|
|
5
18
|
### Patch Changes
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -57,9 +57,11 @@ const preferStaticBooleanProperties = require('./rules/prefer-static-boolean-pro
|
|
|
57
57
|
const noImpureTopLevelProvider = require('./rules/no-impure-top-level-provider');
|
|
58
58
|
const noLegacyPrepareWithoutInjector = require('./rules/no-legacy-prepare-without-injector');
|
|
59
59
|
const requireFormSubmit = require('./rules/require-form-submit');
|
|
60
|
+
const noNativeTitle = require('./rules/no-native-title');
|
|
60
61
|
const noCdkImport = require('./rules/no-cdk-import');
|
|
61
62
|
const noLegacyQueryImport = require('./rules/no-legacy-query-import');
|
|
62
63
|
const noAsyncAwait = require('./rules/no-async-await');
|
|
64
|
+
const noSilentCatchError = require('./rules/no-silent-catch-error');
|
|
63
65
|
const noEnum = require('./rules/no-enum');
|
|
64
66
|
const consistentTypeDefinitions = require('./rules/consistent-type-definitions');
|
|
65
67
|
const takeUntilDestroyedLast = require('./rules/take-until-destroyed-last');
|
|
@@ -130,9 +132,11 @@ const plugin = {
|
|
|
130
132
|
'prefer-static-boolean-properties': preferStaticBooleanProperties,
|
|
131
133
|
'no-impure-top-level-provider': noImpureTopLevelProvider,
|
|
132
134
|
'require-form-submit': requireFormSubmit,
|
|
135
|
+
'no-native-title': noNativeTitle,
|
|
133
136
|
'no-cdk-import': noCdkImport,
|
|
134
137
|
'no-legacy-query-import': noLegacyQueryImport,
|
|
135
138
|
'no-async-await': noAsyncAwait,
|
|
139
|
+
'no-silent-catch-error': noSilentCatchError,
|
|
136
140
|
'no-enum': noEnum,
|
|
137
141
|
'consistent-type-definitions': consistentTypeDefinitions,
|
|
138
142
|
},
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Flags a native `title` attribute on an HTML element - it renders the browser tooltip, which the SDK tooltip replaces.
|
|
6
|
+
*
|
|
7
|
+
* <button title="Save"> ✘
|
|
8
|
+
* <button [title]="label()"> ✘
|
|
9
|
+
* <button [attr.title]="label()"> ✘
|
|
10
|
+
*
|
|
11
|
+
* An element with a dash in its name is a component or custom element; its `title` input or attribute
|
|
12
|
+
* is its own API, so only `[attr.title]` is reported there.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
16
|
+
const noNativeTitle = {
|
|
17
|
+
meta: {
|
|
18
|
+
type: 'suggestion',
|
|
19
|
+
docs: {
|
|
20
|
+
description: 'Disallow the native `title` attribute in Angular templates; use the SDK tooltip.',
|
|
21
|
+
recommended: true,
|
|
22
|
+
},
|
|
23
|
+
schema: [],
|
|
24
|
+
messages: {
|
|
25
|
+
nativeTitle:
|
|
26
|
+
'A native `title` shows the browser tooltip. Use the SDK tooltip (`etTooltip` from `@ethlete/components`) instead.',
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
create(context) {
|
|
30
|
+
const parserServices = /** @type {any} */ (context.sourceCode.parserServices);
|
|
31
|
+
|
|
32
|
+
if (!parserServices?.convertNodeSourceSpanToLoc) return {};
|
|
33
|
+
|
|
34
|
+
/** @param {any} node */
|
|
35
|
+
const report = (node) =>
|
|
36
|
+
context.report({
|
|
37
|
+
loc: parserServices.convertNodeSourceSpanToLoc(node.sourceSpan),
|
|
38
|
+
messageId: 'nativeTitle',
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
/** @param {any} node */
|
|
43
|
+
Element(node) {
|
|
44
|
+
const isCustom = node.name.includes('-');
|
|
45
|
+
|
|
46
|
+
for (const attribute of node.attributes ?? []) {
|
|
47
|
+
if (attribute.name === 'title' && !isCustom) report(attribute);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
for (const input of node.inputs ?? []) {
|
|
51
|
+
if (input.name !== 'title') continue;
|
|
52
|
+
const isAttrBinding = input.keySpan?.toString() === 'attr.title';
|
|
53
|
+
if (isAttrBinding || !isCustom) report(input);
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
module.exports = noNativeTitle;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Disallow an RxJS catchError whose handler ignores the error and returns of(...), EMPTY or NEVER,
|
|
6
|
+
* and, with the `tryCatch` option, a try/catch whose catch ignores the error and returns a fallback value.
|
|
7
|
+
*
|
|
8
|
+
* BAD:
|
|
9
|
+
* source$.pipe(catchError(() => of(null)));
|
|
10
|
+
* try { return read(); } catch { return []; }
|
|
11
|
+
*
|
|
12
|
+
* GOOD:
|
|
13
|
+
* source$.pipe(catchError((error) => { this.error.set(error); return of(null); }));
|
|
14
|
+
* try { return read(); } catch (error) { report(error); return []; }
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const SWALLOWING_CONSTANTS = new Set(['EMPTY', 'NEVER']);
|
|
18
|
+
|
|
19
|
+
const isSwallowingValue = (node) => {
|
|
20
|
+
if (!node) return false;
|
|
21
|
+
|
|
22
|
+
if (node.type === 'Identifier') return SWALLOWING_CONSTANTS.has(node.name);
|
|
23
|
+
|
|
24
|
+
return node.type === 'CallExpression' && node.callee.type === 'Identifier' && node.callee.name === 'of';
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const collectReturnedValues = (fn) => {
|
|
28
|
+
if (fn.body.type !== 'BlockStatement') return [fn.body];
|
|
29
|
+
|
|
30
|
+
return collectReturnsIn(fn.body);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const collectReturnsIn = (block) => {
|
|
34
|
+
const values = [];
|
|
35
|
+
|
|
36
|
+
const visit = (node) => {
|
|
37
|
+
if (!node || typeof node.type !== 'string') return;
|
|
38
|
+
|
|
39
|
+
if (
|
|
40
|
+
node.type === 'FunctionDeclaration' ||
|
|
41
|
+
node.type === 'FunctionExpression' ||
|
|
42
|
+
node.type === 'ArrowFunctionExpression'
|
|
43
|
+
) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (node.type === 'ReturnStatement') {
|
|
48
|
+
values.push(node.argument);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
for (const key of Object.keys(node)) {
|
|
53
|
+
if (key === 'parent') continue;
|
|
54
|
+
|
|
55
|
+
const child = node[key];
|
|
56
|
+
|
|
57
|
+
if (Array.isArray(child)) child.forEach(visit);
|
|
58
|
+
else if (child && typeof child.type === 'string') visit(child);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
block.body.forEach(visit);
|
|
63
|
+
|
|
64
|
+
return values;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
68
|
+
const noSilentCatchError = {
|
|
69
|
+
meta: {
|
|
70
|
+
type: 'suggestion',
|
|
71
|
+
docs: {
|
|
72
|
+
description:
|
|
73
|
+
'Disallow a catchError handler that ignores the error and swallows it with of(), EMPTY or NEVER, and with the `tryCatch` option a catch clause that ignores the error and returns a fallback.',
|
|
74
|
+
recommended: false,
|
|
75
|
+
},
|
|
76
|
+
messages: {
|
|
77
|
+
silentCatchError:
|
|
78
|
+
'The error disappears silently. Use it (log it, store it in a signal, rethrow it) or disable this rule with a reason.',
|
|
79
|
+
},
|
|
80
|
+
schema: [
|
|
81
|
+
{
|
|
82
|
+
type: 'object',
|
|
83
|
+
properties: { tryCatch: { type: 'boolean' } },
|
|
84
|
+
additionalProperties: false,
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
create(context) {
|
|
89
|
+
const sourceCode = context.sourceCode;
|
|
90
|
+
const checkTryCatch = context.options[0]?.tryCatch === true;
|
|
91
|
+
|
|
92
|
+
const ignoresError = (scopeNode, param) => {
|
|
93
|
+
if (!param) return true;
|
|
94
|
+
if (param.type !== 'Identifier') return false;
|
|
95
|
+
|
|
96
|
+
const variable = sourceCode.getDeclaredVariables(scopeNode).find((v) => v.name === param.name);
|
|
97
|
+
|
|
98
|
+
return !variable || variable.references.length === 0;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
CatchClause(node) {
|
|
103
|
+
if (!checkTryCatch) return;
|
|
104
|
+
if (!ignoresError(node, node.param)) return;
|
|
105
|
+
if (collectReturnsIn(node.body).length === 0) return;
|
|
106
|
+
|
|
107
|
+
context.report({ node, messageId: 'silentCatchError' });
|
|
108
|
+
},
|
|
109
|
+
CallExpression(node) {
|
|
110
|
+
if (node.callee.type !== 'Identifier' || node.callee.name !== 'catchError') return;
|
|
111
|
+
|
|
112
|
+
const [handler] = node.arguments;
|
|
113
|
+
|
|
114
|
+
if (!handler || (handler.type !== 'ArrowFunctionExpression' && handler.type !== 'FunctionExpression')) return;
|
|
115
|
+
if (!ignoresError(handler, handler.params[0])) return;
|
|
116
|
+
|
|
117
|
+
if (collectReturnedValues(handler).some(isSwallowingValue)) {
|
|
118
|
+
context.report({ node: handler, messageId: 'silentCatchError' });
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
module.exports = noSilentCatchError;
|