Skip to content

Commit 2756a20

Browse files
committed
ci(lint): [WIP] lib local rules setup and bool input transform check
1 parent d1b2804 commit 2756a20

File tree

4 files changed

+136
-0
lines changed

4 files changed

+136
-0
lines changed

projects/igniteui-angular/eslint.config.mjs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { FlatCompat } from "@eslint/eslintrc";
22
import js from "@eslint/js";
33
import path from "node:path";
44
import { fileURLToPath } from "node:url";
5+
import localRules from "./rules/index.mjs";
56
import rootConfig from "../../eslint.config.mjs";
67
// import tseslint from "typescript-eslint";
78
// import angular from "angular-eslint";
@@ -18,6 +19,9 @@ export default [
1819
...rootConfig,
1920
{
2021
files: ["**/*.ts"],
22+
plugins: {
23+
'igniteui-angular-local': localRules,
24+
},
2125
rules: {
2226
"@angular-eslint/component-selector": ["error", {
2327
type: "element",
@@ -48,6 +52,7 @@ export default [
4852
}],
4953

5054
"no-debugger": "error",
55+
"igniteui-angular-local/boolean-input-transform": ["error"],
5156
},
5257
},
5358
...compat.extends(
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
## Custom ESLint rules
2+
3+
This folder contains local custom ESLint rules for the Ignite UI for Angular library.
4+
5+
6+
### Useful links for development
7+
8+
- [Writing custom ESLint rules with angular-eslint utils](https://github.com/angular-eslint/angular-eslint/blob/main/docs/WRITING_CUSTOM_RULES.md)
9+
- Working with AST
10+
- AST Specification https://typescript-eslint.io/packages/typescript-estree/ast-spec
11+
- AST Explorer https://astexplorer.net (pick from the options dropdown)
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { ASTUtils, Selectors } from '@angular-eslint/utils';
2+
// import type { TSESTree } from '@typescript-eslint/utils';
3+
import { ESLintUtils } from '@typescript-eslint/utils';
4+
import { TypeFlags } from 'typescript';
5+
6+
// export type Options = [
7+
// {
8+
// suffixes?: string[];
9+
// },
10+
// ];
11+
12+
// export type MessageIds = 'serviceSuffix';
13+
14+
export const RULE_NAME = 'boolean-input-transform';
15+
16+
/**
17+
* Check if a property is of boolean type.
18+
* @param {import('@typescript-eslint/utils').TSESTree.PropertyDefinition | import('@typescript-eslint/utils').TSESTree.MethodDefinition} property
19+
* @param {import('@typescript-eslint/utils').ParserServicesWithTypeInformation} parserServices
20+
* @returns {boolean}
21+
*/
22+
function isBooleanProperty(property, parserServices) {
23+
let isBoolean = false;
24+
let typeAnnotation = null;
25+
26+
if (property.type === 'MethodDefinition' && (property.kind === 'get' || property.kind === 'set')) {
27+
// getter/setter
28+
const typeAnnotation = property.value.returnType?.typeAnnotation || property.value.params[0]?.typeAnnotation?.typeAnnotation;
29+
isBoolean = typeAnnotation
30+
? typeAnnotation === 'TSBooleanKeyword'
31+
: isBooleanType(property, parserServices);
32+
33+
} else if (property.type === 'PropertyDefinition') {
34+
isBoolean = property.typeAnnotation
35+
&& property.typeAnnotation.typeAnnotation.type === 'TSBooleanKeyword';
36+
37+
isBoolean ||= property.value
38+
&& property.value.type === 'Literal'
39+
&& typeof property.value.value === 'boolean';
40+
}
41+
return isBoolean;
42+
}
43+
44+
/**
45+
* Type-aware check if a property is of boolean type.
46+
* @param {import('@typescript-eslint/utils').TSESTree.Node} node
47+
* @param {import('@typescript-eslint/utils').ParserServicesWithTypeInformation} parserServices
48+
* @returns {boolean}
49+
*/
50+
function isBooleanType(node, parserServices) {
51+
const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node);
52+
const checker = parserServices.program.getTypeChecker();
53+
const type = checker.getTypeAtLocation(tsNode);
54+
return (type.flags & TypeFlags.BooleanLike) !== 0;
55+
}
56+
57+
export const rule = ESLintUtils.RuleCreator.withoutDocs({
58+
name: RULE_NAME,
59+
meta: {
60+
type: 'suggestion',
61+
docs: {
62+
description:
63+
'Require boolean @Input properties to use { transform: booleanAttribute }',
64+
recommended: 'error',
65+
},
66+
schema: [],
67+
messages: {
68+
missingTransform:
69+
'Boolean @Input properties must have { transform: booleanAttribute }.',
70+
},
71+
},
72+
defaultOptions: [],
73+
create(context, [ /* options*/ ]) {
74+
// const parserServices = ESLintUtils.getParserServices(context);
75+
// const checker = parserServices.program.getTypeChecker();
76+
77+
const ruleOptions = {
78+
[Selectors.INPUT_DECORATOR](decorator) {
79+
const property = decorator.parent;
80+
81+
if (!ASTUtils.isPropertyOrMethodDefinition(property)) return;
82+
83+
const classDeclaration = ASTUtils.getNearestNodeFrom(
84+
decorator,
85+
ASTUtils.isClassDeclaration,
86+
);
87+
88+
89+
let isBoolean = isBooleanProperty(property/*, parserServices*/);
90+
91+
if (!isBoolean) return;
92+
93+
const arg = decorator.expression.arguments[0];
94+
const hasTransform =
95+
arg &&
96+
arg.type === 'ObjectExpression' &&
97+
arg.properties.some(
98+
(p) =>
99+
p.key.name === 'transform' &&
100+
p.value.name === 'booleanAttribute'
101+
);
102+
103+
if (hasTransform) return;
104+
105+
context.report({ node: decorator, messageId: 'missingTransform' });
106+
},
107+
};
108+
return ruleOptions;
109+
},
110+
});
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import {
2+
rule as booleanInputTransform,
3+
RULE_NAME as booleanInputTransformRuleName,
4+
} from './boolean-input-transform.mjs';
5+
6+
export default {
7+
rules: {
8+
[booleanInputTransformRuleName]: booleanInputTransform,
9+
}
10+
};

0 commit comments

Comments
 (0)