@yoo-digital/eslint-plugin-angular 1.3.0 → 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 CHANGED
@@ -1,4 +1,4 @@
1
- # eslint-plugin-angular
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 raises running : `npm run lint`
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
- ## Rule 1 : boolean input conversion
13
+ This feature consists of **two complementary rules**:
14
14
 
15
- `booleanAttribute @angular/core`
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
- ### Examples
28
+ ### HTML
30
29
 
30
+ #### True value
31
31
  ```html
32
- <!-- this will raise lint issue -->
33
- <myMealComponent [isVegan]="true" />
34
- <!-- false value raises no lint issue (ignored) -->
35
- <myMealComponent [isVegan]="false" />
32
+ <mealComponent [isVegan]="true" />
33
+ <!-- Lint issue enforcing to be : -->
34
+ <mealComponent isVegan />
36
35
  ```
37
36
 
38
- Lint enforces use of boolean attribute :
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
- // Modern signal way
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
- ## Rule 2 : ...
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
  };
@@ -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';
@@ -1,6 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.RULE_NAME = exports.preferBooleanAttributeShorthandRule = void 0;
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, "RULE_NAME", { enumerable: true, get: function () { return prefer_boolean_attribute_shorthand_1.RULE_NAME; } });
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
- hasSuggestions: true,
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
- suggest: [
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.3.0",
4
- "description": "Yoo Digital custom Angular ESLint plugin providing prefer-boolean-attribute-shorthand rule.",
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",