@ethlete/eslint-plugin 1.0.0-next.12 → 1.0.0-next.14
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,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.0-next.14
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [`423b30b`](https://github.com/ethlete-io/ethdk/commit/423b30ba4f368d7cee8c464ebd89b5df20d3934c) Thanks [@TomTomB](https://github.com/TomTomB)! - The `static` class member ban now excepts `ngTemplateContextGuard`, which Angular's template type checker requires to be static.
|
|
8
|
+
|
|
9
|
+
- [`7ca77d4`](https://github.com/ethlete-io/ethdk/commit/7ca77d4a9e1dd3abad7237227c62643159719b74) Thanks [@TomTomB](https://github.com/TomTomB)! - `no-trivial-return-type`: self-referencing (recursive) functions keep their return type annotation — TypeScript cannot infer a return type that depends on itself (TS7023), so the fixer no longer strips it there.
|
|
10
|
+
|
|
11
|
+
## 1.0.0-next.13
|
|
12
|
+
|
|
13
|
+
### Minor Changes
|
|
14
|
+
|
|
15
|
+
- [#3022](https://github.com/ethlete-io/ethdk/pull/3022) [`5a43c83`](https://github.com/ethlete-io/ethdk/commit/5a43c8347a98a815562d1bb0f59bc0db1d765262) Thanks [@github-actions](https://github.com/apps/github-actions)! - Recommended config: the `@Injectable` ban now also flags Angular 22's `@Service` decorator — use `createProvider` / `createRootProvider` from `@ethlete/core` instead.
|
|
16
|
+
|
|
17
|
+
- [#3022](https://github.com/ethlete-io/ethdk/pull/3022) [`9be3738`](https://github.com/ethlete-io/ethdk/commit/9be3738e18ef1837841c79131127fa42406a3e92) Thanks [@github-actions](https://github.com/apps/github-actions)! - New template rule `ethlete/prefer-static-boolean-properties` (in `recommendedTemplate` as `warn`): flags property bindings of static booleans like `[isReadonly]="true"` and suggests the static-attribute form (`isReadonly` / `isReadonly="false"`). Suggestion-only, since the rewrite is only safe for inputs with a `booleanAttribute` transform.
|
|
18
|
+
|
|
3
19
|
## 1.0.0-next.12
|
|
4
20
|
|
|
5
21
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -139,9 +139,10 @@ const recommendedTs = {
|
|
|
139
139
|
selector: 'FunctionExpression[async=true]',
|
|
140
140
|
message: 'No async/await. Use RxJS for all async operations.',
|
|
141
141
|
},
|
|
142
|
-
// No static class members
|
|
142
|
+
// No static class members — except ngTemplateContextGuard, which Angular's template
|
|
143
|
+
// type checker requires to be static (it types a structural directive's `let-` bindings)
|
|
143
144
|
{
|
|
144
|
-
selector:
|
|
145
|
+
selector: "MethodDefinition[static=true]:not([key.name='ngTemplateContextGuard'])",
|
|
145
146
|
message: 'No static class members.',
|
|
146
147
|
},
|
|
147
148
|
{
|
|
@@ -169,10 +170,12 @@ const recommendedTs = {
|
|
|
169
170
|
message:
|
|
170
171
|
'No legacy Angular lifecycle hooks. Use afterNextRender, inject(DestroyRef).onDestroy, or constructor-based effects instead.',
|
|
171
172
|
},
|
|
172
|
-
// No @Injectable — use createRootProvider / createProvider from @ethlete/core
|
|
173
|
+
// No @Injectable / @Service — use createRootProvider / createProvider from @ethlete/core
|
|
173
174
|
{
|
|
174
|
-
selector:
|
|
175
|
-
|
|
175
|
+
selector:
|
|
176
|
+
'Decorator[expression.callee.name=/^(Injectable|Service)$/], Decorator[expression.name=/^(Injectable|Service)$/]',
|
|
177
|
+
message:
|
|
178
|
+
'No @Injectable / @Service services. Use createRootProvider / createProvider from @ethlete/core instead.',
|
|
176
179
|
},
|
|
177
180
|
// No route guards — handle access control inside the component itself
|
|
178
181
|
{
|
|
@@ -403,6 +406,10 @@ const recommendedTemplate = {
|
|
|
403
406
|
'@angular-eslint/template/no-any': 'error',
|
|
404
407
|
// Prefer plain attribute over unnecessary property binding for static strings: etIcon="foo" not [etIcon]="'foo'"
|
|
405
408
|
'@angular-eslint/template/prefer-static-string-properties': 'error',
|
|
409
|
+
// Prefer plain attribute over unnecessary property binding for static booleans: isReadonly not [isReadonly]="true"
|
|
410
|
+
// (warn + suggestion-only: the rewrite is only safe when the input has a booleanAttribute transform,
|
|
411
|
+
// which a template rule cannot verify)
|
|
412
|
+
'ethlete/prefer-static-boolean-properties': 'warn',
|
|
406
413
|
},
|
|
407
414
|
};
|
|
408
415
|
|
package/src/index.js
CHANGED
|
@@ -50,6 +50,7 @@ const preferConciseAngularStyleMetadata = require('./rules/prefer-concise-angula
|
|
|
50
50
|
const noTypedInjectedElementRef = require('./rules/no-typed-injected-element-ref');
|
|
51
51
|
const noNativeHtmlInputName = require('./rules/no-native-html-input-name');
|
|
52
52
|
const preferPresentTenseOutput = require('./rules/prefer-present-tense-output');
|
|
53
|
+
const preferStaticBooleanProperties = require('./rules/prefer-static-boolean-properties');
|
|
53
54
|
const { recommendedTs, recommendedTemplate, recommendedSpec } = require('./configs/recommended');
|
|
54
55
|
|
|
55
56
|
/** @type {import('eslint').ESLint.Plugin} */
|
|
@@ -108,6 +109,7 @@ const plugin = {
|
|
|
108
109
|
'no-typed-injected-element-ref': noTypedInjectedElementRef,
|
|
109
110
|
'no-native-html-input-name': noNativeHtmlInputName,
|
|
110
111
|
'prefer-present-tense-output': preferPresentTenseOutput,
|
|
112
|
+
'prefer-static-boolean-properties': preferStaticBooleanProperties,
|
|
111
113
|
},
|
|
112
114
|
};
|
|
113
115
|
|
|
@@ -121,6 +123,7 @@ const recommendedTsWithPlugin = {
|
|
|
121
123
|
/** @type {import('eslint').Linter.Config} */
|
|
122
124
|
const recommendedTemplateWithPlugin = {
|
|
123
125
|
...recommendedTemplate,
|
|
126
|
+
plugins: { ethlete: plugin },
|
|
124
127
|
};
|
|
125
128
|
|
|
126
129
|
const configs = {
|
|
@@ -11,6 +11,11 @@
|
|
|
11
11
|
*
|
|
12
12
|
* ALSO GOOD (non-trivial, keep it):
|
|
13
13
|
* const parse = (): Date => new Date(value)
|
|
14
|
+
*
|
|
15
|
+
* Self-referencing (recursive) functions are exempt: TypeScript cannot infer a
|
|
16
|
+
* return type that depends on itself (TS7023 under noImplicitAny), so the
|
|
17
|
+
* annotation is required there:
|
|
18
|
+
* const walk = (node): boolean => node.ok && walk(node.next)
|
|
14
19
|
*/
|
|
15
20
|
|
|
16
21
|
const TRIVIAL_TYPES = new Set([
|
|
@@ -69,6 +74,93 @@ const noTrivialReturnType = {
|
|
|
69
74
|
return false;
|
|
70
75
|
};
|
|
71
76
|
|
|
77
|
+
/**
|
|
78
|
+
* The names under which a function implementation can call itself: its own name
|
|
79
|
+
* (named function expression, or the variable an arrow is assigned to) and/or
|
|
80
|
+
* `this.<key>` for class/object methods and class-field arrows.
|
|
81
|
+
* @param {any} fn
|
|
82
|
+
*/
|
|
83
|
+
const selfReferenceNames = (fn) => {
|
|
84
|
+
/** @type {{ kind: 'name' | 'this', name: string }[]} */
|
|
85
|
+
const names = [];
|
|
86
|
+
|
|
87
|
+
if (fn.id && fn.id.type === 'Identifier') {
|
|
88
|
+
names.push({ kind: 'name', name: fn.id.name });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const parent = fn.parent;
|
|
92
|
+
|
|
93
|
+
if (parent) {
|
|
94
|
+
if (
|
|
95
|
+
(parent.type === 'MethodDefinition' || parent.type === 'PropertyDefinition' || parent.type === 'Property') &&
|
|
96
|
+
!parent.computed &&
|
|
97
|
+
parent.key &&
|
|
98
|
+
parent.key.type === 'Identifier'
|
|
99
|
+
) {
|
|
100
|
+
names.push({ kind: 'this', name: parent.key.name });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (parent.type === 'VariableDeclarator' && parent.id && parent.id.type === 'Identifier') {
|
|
104
|
+
names.push({ kind: 'name', name: parent.id.name });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return names;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Whether the function body references the function itself (recursion) — there the
|
|
113
|
+
* annotation is required, since TypeScript cannot infer a self-dependent return type.
|
|
114
|
+
* @param {any} fn
|
|
115
|
+
*/
|
|
116
|
+
const isSelfReferencing = (fn) => {
|
|
117
|
+
const names = selfReferenceNames(fn);
|
|
118
|
+
|
|
119
|
+
if (!names.length || !fn.body) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const stack = [fn.body];
|
|
124
|
+
|
|
125
|
+
while (stack.length) {
|
|
126
|
+
const current = stack.pop();
|
|
127
|
+
|
|
128
|
+
if (!current || typeof current !== 'object') continue;
|
|
129
|
+
|
|
130
|
+
if (Array.isArray(current)) {
|
|
131
|
+
stack.push(...current);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (typeof current.type !== 'string') continue;
|
|
136
|
+
|
|
137
|
+
for (const { kind, name } of names) {
|
|
138
|
+
if (kind === 'name' && current.type === 'Identifier' && current.name === name) {
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (
|
|
143
|
+
kind === 'this' &&
|
|
144
|
+
current.type === 'MemberExpression' &&
|
|
145
|
+
current.object.type === 'ThisExpression' &&
|
|
146
|
+
!current.computed &&
|
|
147
|
+
current.property.type === 'Identifier' &&
|
|
148
|
+
current.property.name === name
|
|
149
|
+
) {
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
for (const key of Object.keys(current)) {
|
|
155
|
+
if (key === 'parent') continue;
|
|
156
|
+
const value = current[key];
|
|
157
|
+
if (value && typeof value === 'object') stack.push(value);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return false;
|
|
162
|
+
};
|
|
163
|
+
|
|
72
164
|
/**
|
|
73
165
|
* @param {import('eslint').Rule.Node} node
|
|
74
166
|
*/
|
|
@@ -81,6 +173,7 @@ const noTrivialReturnType = {
|
|
|
81
173
|
|
|
82
174
|
const annotation = fn.returnType.typeAnnotation;
|
|
83
175
|
if (!TRIVIAL_TYPES.has(annotation.type)) return;
|
|
176
|
+
if (isSelfReferencing(fn)) return;
|
|
84
177
|
|
|
85
178
|
const label = TRIVIAL_LABELS[annotation.type] ?? annotation.type;
|
|
86
179
|
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Flags property bindings whose value is a static boolean literal:
|
|
6
|
+
*
|
|
7
|
+
* <my-cmp [isReadonly]="true" />
|
|
8
|
+
* <my-cmp [isReadonly]="false" />
|
|
9
|
+
*
|
|
10
|
+
* If the target input uses a `booleanAttribute` transform (the styleguide
|
|
11
|
+
* default for boolean inputs), the binding is unnecessary — a static attribute
|
|
12
|
+
* expresses the same thing without a change-detection expression:
|
|
13
|
+
*
|
|
14
|
+
* <my-cmp isReadonly /> (true)
|
|
15
|
+
* <my-cmp isReadonly="false" /> (false — `booleanAttribute` coerces the string 'false' to false)
|
|
16
|
+
*
|
|
17
|
+
* Whether the input actually has that transform is invisible to a template
|
|
18
|
+
* rule (no type information), so the rewrite is offered as a suggestion, not
|
|
19
|
+
* an auto-fix: without the transform a static attribute would pass the string
|
|
20
|
+
* 'true' / 'false' instead of a boolean.
|
|
21
|
+
*
|
|
22
|
+
* Companion to `@angular-eslint/template/prefer-static-string-properties`,
|
|
23
|
+
* which covers the string-literal case (where the rewrite is always safe).
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
27
|
+
const preferStaticBooleanProperties = {
|
|
28
|
+
meta: {
|
|
29
|
+
type: 'suggestion',
|
|
30
|
+
docs: {
|
|
31
|
+
description:
|
|
32
|
+
'Prefer a static attribute over a property binding for static boolean values when the input uses a booleanAttribute transform.',
|
|
33
|
+
},
|
|
34
|
+
hasSuggestions: true,
|
|
35
|
+
schema: [],
|
|
36
|
+
messages: {
|
|
37
|
+
preferStaticBooleanProperty:
|
|
38
|
+
'Binding a static boolean is unnecessary if the `{{ name }}` input uses a `booleanAttribute` transform — use a static attribute instead. Keep the binding if the input takes a plain boolean without a transform.',
|
|
39
|
+
useBareAttribute: 'Replace with the bare attribute `{{ name }}`',
|
|
40
|
+
useStaticAttribute: 'Replace with the static attribute `{{ name }}="false"`',
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
create(context) {
|
|
44
|
+
const parserServices = /** @type {any} */ (context.sourceCode.parserServices);
|
|
45
|
+
|
|
46
|
+
// Only meaningful when parsed with @angular-eslint/template-parser
|
|
47
|
+
if (!parserServices?.convertNodeSourceSpanToLoc) return {};
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
/** @param {any} node */
|
|
51
|
+
['BoundAttribute.inputs'](node) {
|
|
52
|
+
const { name, sourceSpan, keySpan, value } = node;
|
|
53
|
+
|
|
54
|
+
// Exclude @animation, class./style./attr. sub-properties, and
|
|
55
|
+
// structural directives (*foo has no keySpan details)
|
|
56
|
+
const isBindingProperty = keySpan?.details && !keySpan.details.includes('@') && !keySpan.details.includes('.');
|
|
57
|
+
if (!isBindingProperty) return;
|
|
58
|
+
|
|
59
|
+
// Only literal `true` / `false` expressions. `value.ast` is a
|
|
60
|
+
// LiteralPrimitive for literals; the raw-source check guards against
|
|
61
|
+
// other node shapes that happen to carry a boolean `value`.
|
|
62
|
+
const source = typeof value?.source === 'string' ? value.source.trim() : null;
|
|
63
|
+
if (typeof value?.ast?.value !== 'boolean' || (source !== 'true' && source !== 'false')) return;
|
|
64
|
+
|
|
65
|
+
const isTrue = value.ast.value === true;
|
|
66
|
+
const replacement = isTrue ? name : `${name}="false"`;
|
|
67
|
+
|
|
68
|
+
context.report({
|
|
69
|
+
loc: parserServices.convertNodeSourceSpanToLoc(sourceSpan),
|
|
70
|
+
messageId: 'preferStaticBooleanProperty',
|
|
71
|
+
data: { name },
|
|
72
|
+
suggest: [
|
|
73
|
+
{
|
|
74
|
+
messageId: isTrue ? 'useBareAttribute' : 'useStaticAttribute',
|
|
75
|
+
data: { name },
|
|
76
|
+
fix: (fixer) => fixer.replaceTextRange([sourceSpan.start.offset, sourceSpan.end.offset], replacement),
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
});
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
module.exports = preferStaticBooleanProperties;
|