@ethlete/eslint-plugin 1.0.0-next.12 → 1.0.0-next.13
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,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.0-next.13
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#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.
|
|
8
|
+
|
|
9
|
+
- [#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.
|
|
10
|
+
|
|
3
11
|
## 1.0.0-next.12
|
|
4
12
|
|
|
5
13
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -169,10 +169,12 @@ const recommendedTs = {
|
|
|
169
169
|
message:
|
|
170
170
|
'No legacy Angular lifecycle hooks. Use afterNextRender, inject(DestroyRef).onDestroy, or constructor-based effects instead.',
|
|
171
171
|
},
|
|
172
|
-
// No @Injectable — use createRootProvider / createProvider from @ethlete/core
|
|
172
|
+
// No @Injectable / @Service — use createRootProvider / createProvider from @ethlete/core
|
|
173
173
|
{
|
|
174
|
-
selector:
|
|
175
|
-
|
|
174
|
+
selector:
|
|
175
|
+
'Decorator[expression.callee.name=/^(Injectable|Service)$/], Decorator[expression.name=/^(Injectable|Service)$/]',
|
|
176
|
+
message:
|
|
177
|
+
'No @Injectable / @Service services. Use createRootProvider / createProvider from @ethlete/core instead.',
|
|
176
178
|
},
|
|
177
179
|
// No route guards — handle access control inside the component itself
|
|
178
180
|
{
|
|
@@ -403,6 +405,10 @@ const recommendedTemplate = {
|
|
|
403
405
|
'@angular-eslint/template/no-any': 'error',
|
|
404
406
|
// Prefer plain attribute over unnecessary property binding for static strings: etIcon="foo" not [etIcon]="'foo'"
|
|
405
407
|
'@angular-eslint/template/prefer-static-string-properties': 'error',
|
|
408
|
+
// Prefer plain attribute over unnecessary property binding for static booleans: isReadonly not [isReadonly]="true"
|
|
409
|
+
// (warn + suggestion-only: the rewrite is only safe when the input has a booleanAttribute transform,
|
|
410
|
+
// which a template rule cannot verify)
|
|
411
|
+
'ethlete/prefer-static-boolean-properties': 'warn',
|
|
406
412
|
},
|
|
407
413
|
};
|
|
408
414
|
|
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 = {
|
|
@@ -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;
|