@yoo-digital/eslint-plugin-angular 1.2.3 → 1.4.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/README.md +73 -19
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/rules/index.d.ts +2 -1
- package/dist/rules/index.js +5 -2
- package/dist/rules/prefer-boolean-attribute-shorthand.js +2 -8
- package/dist/rules/require-boolean-attribute-transform.d.ts +16 -0
- package/dist/rules/require-boolean-attribute-transform.js +151 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# YOO ESLint plugin Angular
|
|
2
2
|
|
|
3
3
|
## Custom lint purpose
|
|
4
4
|
|
|
@@ -6,44 +6,98 @@ Here should live all custom Angular lint rules that eslint does not already prov
|
|
|
6
6
|
|
|
7
7
|
## Linting
|
|
8
8
|
|
|
9
|
-
Wrong code is yellow/red underlined in VScode, it can also be
|
|
9
|
+
Wrong code is yellow/red underlined in VScode, it can also be raised running : `npm run lint`, autofixing them with : `npm run lint:fix`
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
## 1️⃣ Boolean input conversion
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
This feature consists of **two complementary rules**:
|
|
14
14
|
|
|
15
|
-
`booleanAttribute
|
|
16
|
-
|
|
17
|
-
`BooleanInput @angular/cdk/coercion`
|
|
18
|
-
|
|
19
|
-
The rule only flags components inputs `[isVegan]="true"` bindings and ignores `[isVegan]="false"` bindings:
|
|
15
|
+
1. **`require-boolean-attribute-transform`** - TypeScript rule that enforces `booleanAttribute` transform on boolean inputs
|
|
16
|
+
2. **`prefer-boolean-attribute-shorthand`** - Template rule that enforces shorthand syntax for `[attr]="true"` bindings
|
|
20
17
|
|
|
18
|
+
### Setting
|
|
21
19
|
```json
|
|
22
20
|
{
|
|
23
21
|
"rules": {
|
|
22
|
+
"@yoo-digital/eslint-plugin-angular/require-boolean-attribute-transform": "error",
|
|
24
23
|
"@yoo-digital/eslint-plugin-angular/prefer-boolean-attribute-shorthand": "error"
|
|
25
24
|
}
|
|
26
25
|
}
|
|
27
26
|
```
|
|
28
27
|
|
|
29
|
-
###
|
|
28
|
+
### HTML
|
|
30
29
|
|
|
30
|
+
#### True value
|
|
31
31
|
```html
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
<myMealComponent [isVegan]="false" />
|
|
32
|
+
<mealComponent [isVegan]="true" />
|
|
33
|
+
<!-- Lint issue enforcing to be : -->
|
|
34
|
+
<mealComponent isVegan />
|
|
36
35
|
```
|
|
37
36
|
|
|
38
|
-
|
|
37
|
+
#### False value (bypassed)
|
|
38
|
+
```html
|
|
39
|
+
<mealComponent [isVegan]="false" />
|
|
40
|
+
<!-- No lint issue, to be able to address false value for a default true input -->
|
|
41
|
+
```
|
|
39
42
|
|
|
43
|
+
### Typescript
|
|
44
|
+
|
|
45
|
+
#### Imports
|
|
46
|
+
`booleanAttribute @angular/core`
|
|
47
|
+
|
|
48
|
+
`BooleanInput @angular/cdk/coercion`
|
|
49
|
+
|
|
50
|
+
#### Modern signal way
|
|
40
51
|
```typescript
|
|
41
|
-
|
|
52
|
+
isVegan = input<boolean>();
|
|
53
|
+
// Lint issue, must become :
|
|
54
|
+
isVegan = input<boolean, BooleanInput>(true|false, { transform: booleanAttribute });
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
#### Old decorator way
|
|
58
|
+
```typescript
|
|
59
|
+
@Input() isVegan?: boolean;
|
|
60
|
+
// Lint issue, must become :
|
|
61
|
+
@Input({ transform: booleanAttribute }) isVegan: boolean = true|false;
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Default value
|
|
65
|
+
#### Default true
|
|
66
|
+
|
|
67
|
+
If boolean input is by default **true**
|
|
68
|
+
```typescript
|
|
69
|
+
// Modern signal way :
|
|
70
|
+
isVegan = input<boolean, BooleanInput>(true, { transform: booleanAttribute });
|
|
71
|
+
// Old decorator way :
|
|
72
|
+
@Input({ transform: booleanAttribute }) isVegan: boolean = true;
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
HTML set it true or false this way :
|
|
76
|
+
|
|
77
|
+
```html
|
|
78
|
+
<!-- True value -->
|
|
79
|
+
<mealComponent />
|
|
80
|
+
<!-- False value -->
|
|
81
|
+
<mealComponent [isVegan]="false" />
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
#### Default false
|
|
85
|
+
|
|
86
|
+
If boolean input is by default **false**
|
|
87
|
+
```typescript
|
|
88
|
+
// Modern signal way :
|
|
42
89
|
isVegan = input<boolean, BooleanInput>(false, { transform: booleanAttribute });
|
|
43
|
-
// Old decorator way
|
|
90
|
+
// Old decorator way :
|
|
44
91
|
@Input({ transform: booleanAttribute }) isVegan: boolean = false;
|
|
45
92
|
```
|
|
46
93
|
|
|
47
|
-
|
|
94
|
+
HTML set it true or false this way :
|
|
95
|
+
|
|
96
|
+
```html
|
|
97
|
+
<!-- True value -->
|
|
98
|
+
<mealComponent isVegan />
|
|
99
|
+
<!-- False value -->
|
|
100
|
+
<mealComponent />
|
|
101
|
+
```
|
|
48
102
|
|
|
49
|
-
##
|
|
103
|
+
## 2️⃣ ...
|
package/dist/index.d.ts
CHANGED
|
@@ -4,12 +4,14 @@ export declare const configs: {
|
|
|
4
4
|
plugins: string[];
|
|
5
5
|
rules: {
|
|
6
6
|
'@yoo-digital/angular/prefer-boolean-attribute-shorthand': string;
|
|
7
|
+
'@yoo-digital/angular/require-boolean-attribute-transform': string;
|
|
7
8
|
};
|
|
8
9
|
};
|
|
9
10
|
recommended: {
|
|
10
11
|
plugins: string[];
|
|
11
12
|
rules: {
|
|
12
13
|
'@yoo-digital/angular/prefer-boolean-attribute-shorthand': string;
|
|
14
|
+
'@yoo-digital/angular/require-boolean-attribute-transform': string;
|
|
13
15
|
};
|
|
14
16
|
};
|
|
15
17
|
};
|
package/dist/index.js
CHANGED
|
@@ -4,18 +4,21 @@ exports.configs = exports.rules = void 0;
|
|
|
4
4
|
const rules_1 = require("./rules");
|
|
5
5
|
exports.rules = {
|
|
6
6
|
'prefer-boolean-attribute-shorthand': rules_1.preferBooleanAttributeShorthandRule,
|
|
7
|
+
'require-boolean-attribute-transform': rules_1.requireBooleanAttributeTransformRule,
|
|
7
8
|
};
|
|
8
9
|
exports.configs = {
|
|
9
10
|
default: {
|
|
10
11
|
plugins: ['@yoo-digital/eslint-plugin-angular'],
|
|
11
12
|
rules: {
|
|
12
13
|
'@yoo-digital/angular/prefer-boolean-attribute-shorthand': 'warn',
|
|
14
|
+
'@yoo-digital/angular/require-boolean-attribute-transform': 'warn',
|
|
13
15
|
},
|
|
14
16
|
},
|
|
15
17
|
recommended: {
|
|
16
18
|
plugins: ['@yoo-digital/eslint-plugin-angular'],
|
|
17
19
|
rules: {
|
|
18
20
|
'@yoo-digital/angular/prefer-boolean-attribute-shorthand': 'error',
|
|
21
|
+
'@yoo-digital/angular/require-boolean-attribute-transform': 'error',
|
|
19
22
|
},
|
|
20
23
|
},
|
|
21
24
|
};
|
package/dist/rules/index.d.ts
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export { preferBooleanAttributeShorthandRule, RULE_NAME } from './prefer-boolean-attribute-shorthand';
|
|
1
|
+
export { preferBooleanAttributeShorthandRule, RULE_NAME as PREFER_BOOLEAN_ATTRIBUTE_SHORTHAND_RULE_NAME } from './prefer-boolean-attribute-shorthand';
|
|
2
|
+
export { requireBooleanAttributeTransformRule, RULE_NAME as REQUIRE_BOOLEAN_ATTRIBUTE_TRANSFORM_RULE_NAME } from './require-boolean-attribute-transform';
|
package/dist/rules/index.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.REQUIRE_BOOLEAN_ATTRIBUTE_TRANSFORM_RULE_NAME = exports.requireBooleanAttributeTransformRule = exports.PREFER_BOOLEAN_ATTRIBUTE_SHORTHAND_RULE_NAME = exports.preferBooleanAttributeShorthandRule = void 0;
|
|
4
4
|
var prefer_boolean_attribute_shorthand_1 = require("./prefer-boolean-attribute-shorthand");
|
|
5
5
|
Object.defineProperty(exports, "preferBooleanAttributeShorthandRule", { enumerable: true, get: function () { return prefer_boolean_attribute_shorthand_1.preferBooleanAttributeShorthandRule; } });
|
|
6
|
-
Object.defineProperty(exports, "
|
|
6
|
+
Object.defineProperty(exports, "PREFER_BOOLEAN_ATTRIBUTE_SHORTHAND_RULE_NAME", { enumerable: true, get: function () { return prefer_boolean_attribute_shorthand_1.RULE_NAME; } });
|
|
7
|
+
var require_boolean_attribute_transform_1 = require("./require-boolean-attribute-transform");
|
|
8
|
+
Object.defineProperty(exports, "requireBooleanAttributeTransformRule", { enumerable: true, get: function () { return require_boolean_attribute_transform_1.requireBooleanAttributeTransformRule; } });
|
|
9
|
+
Object.defineProperty(exports, "REQUIRE_BOOLEAN_ATTRIBUTE_TRANSFORM_RULE_NAME", { enumerable: true, get: function () { return require_boolean_attribute_transform_1.RULE_NAME; } });
|
|
@@ -29,7 +29,7 @@ exports.preferBooleanAttributeShorthandRule = {
|
|
|
29
29
|
docs: {
|
|
30
30
|
description: 'Prefer boolean input attribute shorthand when binding to true (e.g., use "disabled" instead of [disabled]="true").',
|
|
31
31
|
},
|
|
32
|
-
|
|
32
|
+
fixable: 'code',
|
|
33
33
|
schema: [],
|
|
34
34
|
messages: {
|
|
35
35
|
preferTrue: 'Use attribute shorthand "{{attr}}" instead of [{{attr}}]="true".',
|
|
@@ -57,13 +57,7 @@ exports.preferBooleanAttributeShorthandRule = {
|
|
|
57
57
|
loc,
|
|
58
58
|
messageId: 'preferTrue',
|
|
59
59
|
data: { attr: attrName },
|
|
60
|
-
|
|
61
|
-
{
|
|
62
|
-
messageId: 'suggestTrue',
|
|
63
|
-
data: { attr: attrName },
|
|
64
|
-
fix: (fixer) => fixer.replaceTextRange([start, end], attrName),
|
|
65
|
-
},
|
|
66
|
-
],
|
|
60
|
+
fix: (fixer) => fixer.replaceTextRange([start, end], attrName),
|
|
67
61
|
});
|
|
68
62
|
}
|
|
69
63
|
// [attr]="false" is explicitly ignored - no warning
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { TSESLint } from '@typescript-eslint/utils';
|
|
2
|
+
type MessageIds = 'requireTransformDecorator' | 'requireTransformSignal';
|
|
3
|
+
export declare const RULE_NAME = "require-boolean-attribute-transform";
|
|
4
|
+
/**
|
|
5
|
+
* This rule enforces that boolean @Input() properties and input() signals
|
|
6
|
+
* use the booleanAttribute transform.
|
|
7
|
+
*
|
|
8
|
+
* BEHAVIOR:
|
|
9
|
+
* - @Input() foo: boolean = true/false → Requires @Input({ transform: booleanAttribute })
|
|
10
|
+
* - input<boolean>(true/false) → Requires input<boolean, BooleanInput>(..., { transform: booleanAttribute })
|
|
11
|
+
*
|
|
12
|
+
* This ensures consistency across the codebase and enables the shorthand syntax
|
|
13
|
+
* in templates (e.g., <comp disabled /> instead of <comp [disabled]="true" />).
|
|
14
|
+
*/
|
|
15
|
+
export declare const requireBooleanAttributeTransformRule: TSESLint.RuleModule<MessageIds, []>;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.requireBooleanAttributeTransformRule = exports.RULE_NAME = void 0;
|
|
4
|
+
exports.RULE_NAME = 'require-boolean-attribute-transform';
|
|
5
|
+
/**
|
|
6
|
+
* This rule enforces that boolean @Input() properties and input() signals
|
|
7
|
+
* use the booleanAttribute transform.
|
|
8
|
+
*
|
|
9
|
+
* BEHAVIOR:
|
|
10
|
+
* - @Input() foo: boolean = true/false → Requires @Input({ transform: booleanAttribute })
|
|
11
|
+
* - input<boolean>(true/false) → Requires input<boolean, BooleanInput>(..., { transform: booleanAttribute })
|
|
12
|
+
*
|
|
13
|
+
* This ensures consistency across the codebase and enables the shorthand syntax
|
|
14
|
+
* in templates (e.g., <comp disabled /> instead of <comp [disabled]="true" />).
|
|
15
|
+
*/
|
|
16
|
+
exports.requireBooleanAttributeTransformRule = {
|
|
17
|
+
meta: {
|
|
18
|
+
type: 'suggestion',
|
|
19
|
+
docs: {
|
|
20
|
+
description: 'Require booleanAttribute transform on boolean @Input() properties and input() signals.',
|
|
21
|
+
},
|
|
22
|
+
fixable: 'code',
|
|
23
|
+
schema: [],
|
|
24
|
+
messages: {
|
|
25
|
+
requireTransformDecorator: 'Boolean @Input() "{{name}}" must use transform: booleanAttribute',
|
|
26
|
+
requireTransformSignal: 'Boolean input() signal "{{name}}" must use transform: booleanAttribute and BooleanInput type',
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
defaultOptions: [],
|
|
30
|
+
create(context) {
|
|
31
|
+
const sourceCode = context.sourceCode || context.getSourceCode();
|
|
32
|
+
return {
|
|
33
|
+
// Handle @Input() decorator syntax
|
|
34
|
+
PropertyDefinition(node) {
|
|
35
|
+
// Check if it's a boolean property with @Input() decorator
|
|
36
|
+
if (!node.typeAnnotation || node.typeAnnotation.typeAnnotation.type !== 'TSBooleanKeyword') {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const hasInputDecorator = node.decorators?.some((decorator) => decorator.expression.type === 'CallExpression' &&
|
|
40
|
+
decorator.expression.callee.type === 'Identifier' &&
|
|
41
|
+
decorator.expression.callee.name === 'Input');
|
|
42
|
+
if (!hasInputDecorator) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
// Check if it already has transform: booleanAttribute
|
|
46
|
+
const inputDecorator = node.decorators?.find((decorator) => decorator.expression.type === 'CallExpression' &&
|
|
47
|
+
decorator.expression.callee.type === 'Identifier' &&
|
|
48
|
+
decorator.expression.callee.name === 'Input');
|
|
49
|
+
if (inputDecorator?.expression.type === 'CallExpression') {
|
|
50
|
+
const args = inputDecorator.expression.arguments;
|
|
51
|
+
if (args.length > 0 && args[0].type === 'ObjectExpression') {
|
|
52
|
+
const hasTransform = args[0].properties.some((prop) => prop.type === 'Property' &&
|
|
53
|
+
prop.key.type === 'Identifier' &&
|
|
54
|
+
prop.key.name === 'transform' &&
|
|
55
|
+
prop.value.type === 'Identifier' &&
|
|
56
|
+
prop.value.name === 'booleanAttribute');
|
|
57
|
+
if (hasTransform) {
|
|
58
|
+
return; // Already has transform
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Report the issue
|
|
63
|
+
const propertyName = node.key.type === 'Identifier' ? node.key.name : 'unknown';
|
|
64
|
+
context.report({
|
|
65
|
+
node: node.key,
|
|
66
|
+
messageId: 'requireTransformDecorator',
|
|
67
|
+
data: { name: propertyName },
|
|
68
|
+
fix(fixer) {
|
|
69
|
+
const decoratorNode = inputDecorator?.expression;
|
|
70
|
+
if (!decoratorNode || decoratorNode.type !== 'CallExpression') {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
const decoratorStart = decoratorNode.range[0];
|
|
74
|
+
const decoratorEnd = decoratorNode.range[1];
|
|
75
|
+
const decoratorText = sourceCode.getText(decoratorNode);
|
|
76
|
+
// Generate the fixed decorator
|
|
77
|
+
let newDecorator;
|
|
78
|
+
if (decoratorNode.arguments.length === 0) {
|
|
79
|
+
// @Input() → @Input({ transform: booleanAttribute })
|
|
80
|
+
newDecorator = '@Input({ transform: booleanAttribute })';
|
|
81
|
+
}
|
|
82
|
+
else if (decoratorNode.arguments[0].type === 'ObjectExpression') {
|
|
83
|
+
// @Input({ ... }) → @Input({ ..., transform: booleanAttribute })
|
|
84
|
+
const objExpr = decoratorNode.arguments[0];
|
|
85
|
+
const objText = sourceCode.getText(objExpr);
|
|
86
|
+
const closingBrace = objText.lastIndexOf('}');
|
|
87
|
+
const existingProps = objText.substring(1, closingBrace).trim();
|
|
88
|
+
const separator = existingProps ? ', ' : '';
|
|
89
|
+
newDecorator = `@Input({ ${existingProps}${separator}transform: booleanAttribute })`;
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
// @Input('alias') → @Input({ alias: 'alias', transform: booleanAttribute })
|
|
93
|
+
const aliasArg = sourceCode.getText(decoratorNode.arguments[0]);
|
|
94
|
+
newDecorator = `@Input({ alias: ${aliasArg}, transform: booleanAttribute })`;
|
|
95
|
+
}
|
|
96
|
+
return fixer.replaceTextRange([decoratorStart, decoratorEnd], newDecorator);
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
},
|
|
100
|
+
// Handle input() signal syntax
|
|
101
|
+
VariableDeclarator(node) {
|
|
102
|
+
if (node.init?.type !== 'CallExpression' ||
|
|
103
|
+
node.init.callee.type !== 'Identifier' ||
|
|
104
|
+
node.init.callee.name !== 'input') {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const callExpr = node.init;
|
|
108
|
+
// Check if it's a boolean input
|
|
109
|
+
if (!callExpr.typeArguments || callExpr.typeArguments.params.length === 0) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const firstTypeParam = callExpr.typeArguments.params[0];
|
|
113
|
+
if (firstTypeParam.type !== 'TSBooleanKeyword') {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
// Check if it already has BooleanInput as second type parameter
|
|
117
|
+
const hasBooleanInput = callExpr.typeArguments.params.length > 1 &&
|
|
118
|
+
callExpr.typeArguments.params[1].type === 'TSTypeReference' &&
|
|
119
|
+
callExpr.typeArguments.params[1].typeName.type === 'Identifier' &&
|
|
120
|
+
callExpr.typeArguments.params[1].typeName.name === 'BooleanInput';
|
|
121
|
+
// Check if it already has transform in options
|
|
122
|
+
const hasTransformOption = callExpr.arguments.length > 1 &&
|
|
123
|
+
callExpr.arguments[1].type === 'ObjectExpression' &&
|
|
124
|
+
callExpr.arguments[1].properties.some((prop) => prop.type === 'Property' &&
|
|
125
|
+
prop.key.type === 'Identifier' &&
|
|
126
|
+
prop.key.name === 'transform' &&
|
|
127
|
+
prop.value.type === 'Identifier' &&
|
|
128
|
+
prop.value.name === 'booleanAttribute');
|
|
129
|
+
if (hasBooleanInput && hasTransformOption) {
|
|
130
|
+
return; // Already correctly configured
|
|
131
|
+
}
|
|
132
|
+
// Report the issue
|
|
133
|
+
const propertyName = node.id.type === 'Identifier' ? node.id.name : 'unknown';
|
|
134
|
+
context.report({
|
|
135
|
+
node: node.id,
|
|
136
|
+
messageId: 'requireTransformSignal',
|
|
137
|
+
data: { name: propertyName },
|
|
138
|
+
fix(fixer) {
|
|
139
|
+
const callStart = callExpr.range[0];
|
|
140
|
+
const callEnd = callExpr.range[1];
|
|
141
|
+
// Get the default value argument
|
|
142
|
+
const defaultValue = callExpr.arguments[0] ? sourceCode.getText(callExpr.arguments[0]) : 'false';
|
|
143
|
+
// Build the new call expression
|
|
144
|
+
const newCall = `input<boolean, BooleanInput>(${defaultValue}, {\n transform: booleanAttribute,\n })`;
|
|
145
|
+
return fixer.replaceTextRange([callStart, callEnd], newCall);
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
},
|
|
151
|
+
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yoo-digital/eslint-plugin-angular",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Yoo Digital custom Angular ESLint plugin
|
|
3
|
+
"version": "1.4.0",
|
|
4
|
+
"description": "Yoo Digital custom Angular ESLint plugin for enforcing boolean attribute best practices.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|