@dineroregnskab/eslint-plugin-custom-rules 2.1.4 → 3.0.1
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/.DS_Store +0 -0
- package/.prettierrc +12 -0
- package/.vscode/settings.json +3 -0
- package/README.md +11 -17
- package/eslint-plugin-custom-rules.js +18 -15
- package/package.json +12 -5
- package/rules/camel-case-attributes.js +16 -11
- package/rules/dayjs-with-timezone.js +6 -6
- package/rules/disallow-multiple-calls.js +33 -0
- package/rules/disallow-signal-property-reassignment.js +35 -0
- package/rules/enum-comparison-reminder.js +103 -0
- package/rules/enum-lowercase.js +50 -0
- package/rules/filter-before-take.js +58 -0
- package/rules/no-viewencapsulation-none.js +51 -0
- package/rules/reducers-should-always-return.js +3 -1
- package/rules/replace-first-with-take.js +14 -14
- package/rules/signal-naming-convention.js +61 -0
- package/rules/use-danish-currency-pipe.js +7 -7
package/.DS_Store
ADDED
|
Binary file
|
package/.prettierrc
ADDED
package/README.md
CHANGED
|
@@ -18,39 +18,34 @@ Run `npm i` in root and in `/example`.
|
|
|
18
18
|
|
|
19
19
|
### Adding rules
|
|
20
20
|
|
|
21
|
-
-
|
|
21
|
+
- Create a new js rule in this directory: `./rules`
|
|
22
22
|
|
|
23
|
-
- Add the new
|
|
23
|
+
- Add the new rule to this file `./eslint-plugin-custom-rules.js`.
|
|
24
24
|
|
|
25
|
-
-
|
|
25
|
+
- Add the new rule to `./example/.eslintrc.json`
|
|
26
26
|
|
|
27
|
-
-
|
|
27
|
+
- Test the rule by adding some HTML/TS code here `./example/test.html` or `./example/test.ts` and restart the ESLint server in vs code by pressing F1 -> ESLint: Restart ESLint Server.
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
- You can also test the rules via the terminal directly without reloading eslint server and to see code debugging in the rule definition:
|
|
30
30
|
|
|
31
31
|
```bash
|
|
32
32
|
npm run testhtml
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
Add ts rules in `test.ts` and run test with
|
|
36
|
-
|
|
37
|
-
```bash
|
|
38
33
|
npm run testts
|
|
39
34
|
```
|
|
40
35
|
|
|
41
36
|
#### Debugging what the eslint sees
|
|
42
37
|
|
|
43
|
-
Add a rule, and log it with console log, to see in terminal
|
|
38
|
+
Add a rule, and log it with console log, to see in terminal if the element function even runs "Program" in this case. Then run `npm run testhtml` npm script to see it in terminal.
|
|
44
39
|
|
|
45
40
|
```js
|
|
46
41
|
module.exports = {
|
|
47
42
|
meta: {
|
|
48
|
-
type:
|
|
43
|
+
type: 'suggestion',
|
|
49
44
|
docs: {
|
|
50
45
|
description:
|
|
51
|
-
|
|
46
|
+
'Enforce using `danishCurrency` pipe instead of `currency` pipe in Angular HTML templates.',
|
|
52
47
|
},
|
|
53
|
-
fixable:
|
|
48
|
+
fixable: 'code',
|
|
54
49
|
schema: [],
|
|
55
50
|
},
|
|
56
51
|
|
|
@@ -58,12 +53,11 @@ module.exports = {
|
|
|
58
53
|
return {
|
|
59
54
|
// Target the entire file and traverse each node
|
|
60
55
|
Program(node) {
|
|
61
|
-
console.log(
|
|
56
|
+
console.log('Parsed Node Types:', node);
|
|
62
57
|
},
|
|
63
58
|
};
|
|
64
59
|
},
|
|
65
60
|
};
|
|
66
|
-
|
|
67
61
|
```
|
|
68
62
|
|
|
69
63
|
### Publish & install new rule locally
|
|
@@ -89,7 +83,7 @@ Example:
|
|
|
89
83
|
|
|
90
84
|
```bash
|
|
91
85
|
npm i
|
|
92
|
-
|
|
86
|
+
```
|
|
93
87
|
|
|
94
88
|
> **Note: You need to restart ESLint to apply new rules. Restart the ESLint server in vs code by pressing F1 -> ESLint: Restart ESLint Server or F1 -> reload window**
|
|
95
89
|
|
|
@@ -1,17 +1,20 @@
|
|
|
1
|
-
const
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
},
|
|
1
|
+
const rules = {
|
|
2
|
+
'reducers-should-always-return': require('./rules/reducers-should-always-return'),
|
|
3
|
+
'attr-camel-case-rule': require('./rules/camel-case-attributes'),
|
|
4
|
+
'dayjs-with-timezone': require('./rules/dayjs-with-timezone'),
|
|
5
|
+
'use-danish-currency-pipe': require('./rules/use-danish-currency-pipe'),
|
|
6
|
+
'replace-first-with-take': require('./rules/replace-first-with-take'),
|
|
7
|
+
'disallow-multiple-calls': require('./rules/disallow-multiple-calls'),
|
|
8
|
+
'disallow-signal-property-reassignment': require('./rules/disallow-signal-property-reassignment'),
|
|
9
|
+
'signal-naming-convention': require('./rules/signal-naming-convention'),
|
|
10
|
+
'filter-before-take': require('./rules/filter-before-take'),
|
|
11
|
+
'no-viewencapsulation-none': require('./rules/no-viewencapsulation-none'),
|
|
12
|
+
'enum-comparison-reminder': require('./rules/enum-comparison-reminder'),
|
|
13
|
+
'enum-lowercase': require('./rules/enum-lowercase'),
|
|
15
14
|
};
|
|
16
15
|
|
|
17
|
-
|
|
16
|
+
console.log('Custom ESLint rules loaded:', Object.keys(rules)); // Debug log
|
|
17
|
+
|
|
18
|
+
module.exports = {
|
|
19
|
+
rules,
|
|
20
|
+
};
|
package/package.json
CHANGED
|
@@ -1,23 +1,30 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dineroregnskab/eslint-plugin-custom-rules",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.1",
|
|
4
4
|
"description": "ESLint plugin with custom rules for Dinero Regnskab",
|
|
5
5
|
"main": "eslint-plugin-custom-rules.js",
|
|
6
|
-
"scripts": {
|
|
6
|
+
"scripts": {
|
|
7
|
+
"prettier:write": "prettier \"./**/*.{ts,html,md,scss,js}\" --write"
|
|
8
|
+
},
|
|
7
9
|
"keywords": [],
|
|
8
10
|
"author": "",
|
|
9
11
|
"license": "ISC",
|
|
10
12
|
"devDependencies": {
|
|
11
|
-
"eslint": "^
|
|
13
|
+
"@typescript-eslint/parser": "^7.18.0",
|
|
14
|
+
"eslint": "8.57.1",
|
|
15
|
+
"eslint-config-prettier": "^9.1.0",
|
|
16
|
+
"eslint-plugin-prettier": "^5.2.1",
|
|
17
|
+
"prettier": "^3.4.1"
|
|
12
18
|
},
|
|
13
19
|
"peerDependencies": {
|
|
14
|
-
"eslint": ">=8.0.0"
|
|
20
|
+
"eslint": ">=8.0.0",
|
|
21
|
+
"@typescript-eslint/parser": ">=7"
|
|
15
22
|
},
|
|
16
23
|
"files": [
|
|
17
24
|
"**/*",
|
|
18
25
|
"!example/**/*"
|
|
19
26
|
],
|
|
20
27
|
"dependencies": {
|
|
21
|
-
"@angular-eslint/template-parser": "
|
|
28
|
+
"@angular-eslint/template-parser": "17.5.3"
|
|
22
29
|
}
|
|
23
30
|
}
|
|
@@ -11,18 +11,23 @@ module.exports = {
|
|
|
11
11
|
create(context) {
|
|
12
12
|
return {
|
|
13
13
|
TextAttribute(node) {
|
|
14
|
-
if (
|
|
15
|
-
|
|
14
|
+
if (
|
|
15
|
+
(node.name === 'data-cy' || node.name === 'id') &&
|
|
16
|
+
node.value
|
|
17
|
+
) {
|
|
18
|
+
const camelCasedValue = new RegExp(
|
|
19
|
+
/^[a-z]+([A-Z]?[a-z]*)*$/,
|
|
20
|
+
);
|
|
16
21
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
if (!camelCasedValue.test(node.value)) {
|
|
23
|
+
context.report({
|
|
24
|
+
node,
|
|
25
|
+
message:
|
|
26
|
+
'The value of data-cy and id attributes should be in camelCase.',
|
|
27
|
+
});
|
|
28
|
+
}
|
|
24
29
|
}
|
|
25
|
-
}
|
|
26
|
-
}
|
|
30
|
+
},
|
|
31
|
+
};
|
|
27
32
|
},
|
|
28
33
|
};
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
module.exports = {
|
|
2
2
|
meta: {
|
|
3
|
-
type:
|
|
3
|
+
type: 'suggestion',
|
|
4
4
|
docs: {
|
|
5
|
-
description:
|
|
6
|
-
category:
|
|
5
|
+
description: 'Enforce using dayjs.tz() instead of dayjs()',
|
|
6
|
+
category: 'Best Practices',
|
|
7
7
|
recommended: false,
|
|
8
8
|
},
|
|
9
9
|
fixable: null,
|
|
@@ -14,13 +14,13 @@ module.exports = {
|
|
|
14
14
|
return {
|
|
15
15
|
CallExpression(node) {
|
|
16
16
|
if (
|
|
17
|
-
node.callee.type ===
|
|
18
|
-
node.callee.name ===
|
|
17
|
+
node.callee.type === 'Identifier' &&
|
|
18
|
+
node.callee.name === 'dayjs'
|
|
19
19
|
) {
|
|
20
20
|
context.report({
|
|
21
21
|
node,
|
|
22
22
|
message:
|
|
23
|
-
|
|
23
|
+
'Consider using dayjs.tz() instead of dayjs() for timezone support',
|
|
24
24
|
});
|
|
25
25
|
}
|
|
26
26
|
},
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
meta: {
|
|
3
|
+
type: 'suggestion',
|
|
4
|
+
docs: {
|
|
5
|
+
description:
|
|
6
|
+
'Disallow multiple function calls in a single event binding.',
|
|
7
|
+
},
|
|
8
|
+
schema: [], // No options for this rule
|
|
9
|
+
},
|
|
10
|
+
|
|
11
|
+
create(context) {
|
|
12
|
+
return {
|
|
13
|
+
BoundEvent(node) {
|
|
14
|
+
if (
|
|
15
|
+
node.handler &&
|
|
16
|
+
node.handler.ast &&
|
|
17
|
+
node.handler.ast.type === 'Chain'
|
|
18
|
+
) {
|
|
19
|
+
const expressions = node.handler.ast.expressions;
|
|
20
|
+
|
|
21
|
+
// If there are multiple expressions (function calls), report
|
|
22
|
+
if (expressions.length > 1) {
|
|
23
|
+
context.report({
|
|
24
|
+
node,
|
|
25
|
+
message:
|
|
26
|
+
'Multiple function calls are not allowed in a single event binding. Use one function per event.',
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
},
|
|
33
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
meta: {
|
|
3
|
+
type: 'problem',
|
|
4
|
+
docs: {
|
|
5
|
+
description:
|
|
6
|
+
'Avoid reassigning properties of signal values, as it will fail at runtime.',
|
|
7
|
+
},
|
|
8
|
+
schema: [], // No options for this rule
|
|
9
|
+
},
|
|
10
|
+
|
|
11
|
+
create(context) {
|
|
12
|
+
return {
|
|
13
|
+
AssignmentExpression(node) {
|
|
14
|
+
// Check if the left-hand side is a MemberExpression
|
|
15
|
+
if (
|
|
16
|
+
node.left.type === 'MemberExpression' &&
|
|
17
|
+
node.left.object.type === 'CallExpression' &&
|
|
18
|
+
node.left.object.callee.type === 'MemberExpression' &&
|
|
19
|
+
node.left.object.callee.property.name === 'get' && // Match `.get()` method
|
|
20
|
+
node.left.object.callee.object.type ===
|
|
21
|
+
'MemberExpression' &&
|
|
22
|
+
node.left.object.callee.object.property.name.endsWith(
|
|
23
|
+
'Signal',
|
|
24
|
+
) // Match signals
|
|
25
|
+
) {
|
|
26
|
+
context.report({
|
|
27
|
+
node,
|
|
28
|
+
message:
|
|
29
|
+
'Reassigning properties of a signal value will fail at runtime. Use a setter or update mechanism instead.',
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
},
|
|
35
|
+
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
meta: {
|
|
3
|
+
type: "suggestion",
|
|
4
|
+
docs: {
|
|
5
|
+
description: "Reminder to convert backend data to lower case when enum comparisons are made.",
|
|
6
|
+
},
|
|
7
|
+
schema: [],
|
|
8
|
+
messages: {
|
|
9
|
+
enumComparisonTip: "Remember '{{property}}' should be converted to lower case in the model class if the data is from a HTTP response, to ensure a valid comparison."
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
create(context) {
|
|
13
|
+
// Handle TypeScript Files
|
|
14
|
+
function checkTsEnumComparisons(node) {
|
|
15
|
+
if (node.operator !== "===" && node.operator !== "!==") {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const left = node.left;
|
|
20
|
+
const right = node.right;
|
|
21
|
+
|
|
22
|
+
function isPascalCase(value) {
|
|
23
|
+
return /^[A-Z][a-zA-Z0-9]*$/.test(value);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isPropertyAccess(node) {
|
|
27
|
+
return node.type === "MemberExpression" && node.object && node.property;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isEnumReference(node) {
|
|
31
|
+
return isPropertyAccess(node) && node.object.type === "Identifier" && isPascalCase(node.object.name);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (
|
|
35
|
+
(isPropertyAccess(left) && isEnumReference(right)) ||
|
|
36
|
+
(isPropertyAccess(right) && isEnumReference(left))
|
|
37
|
+
) {
|
|
38
|
+
const property = isPropertyAccess(left) ? context.getSourceCode().getText(left) : context.getSourceCode().getText(right);
|
|
39
|
+
const enumValue = isEnumReference(left) ? context.getSourceCode().getText(left) : context.getSourceCode().getText(right);
|
|
40
|
+
|
|
41
|
+
context.report({
|
|
42
|
+
node,
|
|
43
|
+
messageId: "enumComparisonTip",
|
|
44
|
+
data: {
|
|
45
|
+
property,
|
|
46
|
+
enumValue
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Handle Angular Templates (`*.html`)
|
|
53
|
+
function checkHtmlEnumComparisons(node) {
|
|
54
|
+
if (
|
|
55
|
+
node.type !== "Binary" &&
|
|
56
|
+
node.type !== "Conditional" // Some AST parsers use ConditionalExpression
|
|
57
|
+
) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Ensure we're checking the correct properties
|
|
62
|
+
const operator = node.operator || node.operation; // Adjust for Angular's AST
|
|
63
|
+
if (operator !== "===" && operator !== "!==") {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const left = node.left;
|
|
68
|
+
const right = node.right;
|
|
69
|
+
|
|
70
|
+
function isPascalCase(value) {
|
|
71
|
+
return typeof value === "string" && /^[A-Z][a-zA-Z0-9]*$/.test(value);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function extractNodeText(node) {
|
|
75
|
+
if (node && typeof node === "object" && node.name) {
|
|
76
|
+
return node.name;
|
|
77
|
+
}
|
|
78
|
+
return "";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const leftValue = extractNodeText(left);
|
|
82
|
+
const rightValue = extractNodeText(right);
|
|
83
|
+
|
|
84
|
+
if (isPascalCase(leftValue) || isPascalCase(rightValue)) {
|
|
85
|
+
context.report({
|
|
86
|
+
node,
|
|
87
|
+
messageId: "enumComparisonTip",
|
|
88
|
+
data: {
|
|
89
|
+
property: leftValue || rightValue,
|
|
90
|
+
enumValue: rightValue || leftValue
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
BinaryExpression: checkTsEnumComparisons, // ✅ TypeScript file support
|
|
98
|
+
Binary: checkHtmlEnumComparisons, // ✅ Angular v18 template (`@if`) support
|
|
99
|
+
ConditionalExpression: checkHtmlEnumComparisons // Support variations
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
meta: {
|
|
3
|
+
type: "suggestion",
|
|
4
|
+
docs: {
|
|
5
|
+
description: "Warn when an enum string value is not in full lowercase.",
|
|
6
|
+
},
|
|
7
|
+
schema: [],
|
|
8
|
+
messages: {
|
|
9
|
+
enumLowercase:
|
|
10
|
+
"Enum value '{{enumKey}}' should be in full lowercase (e.g., '{{suggestedValue}}'). Avoid uppercase, spaces, dashes, or special characters."
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
create(context) {
|
|
14
|
+
return {
|
|
15
|
+
VariableDeclarator(node) {
|
|
16
|
+
// Check if it's an Enum declaration
|
|
17
|
+
if (
|
|
18
|
+
node.init &&
|
|
19
|
+
node.init.type === "ObjectExpression" &&
|
|
20
|
+
node.id.type === "Identifier"
|
|
21
|
+
) {
|
|
22
|
+
node.init.properties.forEach((prop) => {
|
|
23
|
+
if (
|
|
24
|
+
prop.key &&
|
|
25
|
+
prop.key.type === "Identifier" &&
|
|
26
|
+
prop.value &&
|
|
27
|
+
prop.value.type === "Literal" &&
|
|
28
|
+
typeof prop.value.value === "string"
|
|
29
|
+
) {
|
|
30
|
+
const enumKey = prop.key.name;
|
|
31
|
+
const enumValue = prop.value.value;
|
|
32
|
+
|
|
33
|
+
// Check if the value is not fully lowercase
|
|
34
|
+
if (!/^[a-z]+$/.test(enumValue)) {
|
|
35
|
+
context.report({
|
|
36
|
+
node: prop.value,
|
|
37
|
+
messageId: "enumLowercase",
|
|
38
|
+
data: {
|
|
39
|
+
enumKey,
|
|
40
|
+
suggestedValue: enumValue.toLowerCase(),
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
meta: {
|
|
3
|
+
type: 'suggestion',
|
|
4
|
+
docs: {
|
|
5
|
+
description:
|
|
6
|
+
'Ensure `filter()` comes before `take(1)` in a `pipe()`.',
|
|
7
|
+
},
|
|
8
|
+
messages: {
|
|
9
|
+
filterBeforeTake:
|
|
10
|
+
'`filter()` must come before `take(1)` in a `pipe()` to ensure proper data filtering.',
|
|
11
|
+
},
|
|
12
|
+
schema: [],
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
create(context) {
|
|
16
|
+
return {
|
|
17
|
+
CallExpression(node) {
|
|
18
|
+
// Check if the callee is a `pipe()` method
|
|
19
|
+
if (
|
|
20
|
+
node.callee.type === 'MemberExpression' &&
|
|
21
|
+
node.callee.property.name === 'pipe'
|
|
22
|
+
) {
|
|
23
|
+
const pipeArguments = node.arguments;
|
|
24
|
+
|
|
25
|
+
let filterIndex = -1;
|
|
26
|
+
let takeIndex = -1;
|
|
27
|
+
|
|
28
|
+
// Iterate over arguments to find the positions of `filter` and `take(1)`
|
|
29
|
+
pipeArguments.forEach((arg, index) => {
|
|
30
|
+
if (
|
|
31
|
+
arg.type === 'CallExpression' &&
|
|
32
|
+
arg.callee.name === 'filter'
|
|
33
|
+
) {
|
|
34
|
+
filterIndex = index;
|
|
35
|
+
}
|
|
36
|
+
if (
|
|
37
|
+
arg.type === 'CallExpression' &&
|
|
38
|
+
arg.callee.name === 'take' &&
|
|
39
|
+
arg.arguments.length === 1 &&
|
|
40
|
+
arg.arguments[0].type === 'Literal' &&
|
|
41
|
+
arg.arguments[0].value === 1
|
|
42
|
+
) {
|
|
43
|
+
takeIndex = index;
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Report if `take(1)` appears before `filter`
|
|
48
|
+
if (takeIndex !== -1 && filterIndex > takeIndex) {
|
|
49
|
+
context.report({
|
|
50
|
+
node: pipeArguments[takeIndex],
|
|
51
|
+
messageId: 'filterBeforeTake',
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
},
|
|
58
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
meta: {
|
|
3
|
+
type: 'problem',
|
|
4
|
+
docs: {
|
|
5
|
+
description:
|
|
6
|
+
'Warn against using `encapsulation: ViewEncapsulation.None` in `@Component` because it breaks Angular modularity.',
|
|
7
|
+
},
|
|
8
|
+
messages: {
|
|
9
|
+
noViewEncapsulationNone:
|
|
10
|
+
'Avoid using `ViewEncapsulation.None` as it disables Angulars modular styling system, which can lead to unintended style leakage and maintenance challenges. Use it only when absolutely necessary and no alternative solution exists.',
|
|
11
|
+
},
|
|
12
|
+
schema: [], // No options
|
|
13
|
+
},
|
|
14
|
+
create(context) {
|
|
15
|
+
return {
|
|
16
|
+
CallExpression(node) {
|
|
17
|
+
// Check if the decorator is `@Component`
|
|
18
|
+
if (
|
|
19
|
+
node.callee.type === 'Identifier' &&
|
|
20
|
+
node.callee.name === 'Component'
|
|
21
|
+
) {
|
|
22
|
+
const decoratorArguments = node.arguments;
|
|
23
|
+
if (
|
|
24
|
+
decoratorArguments.length === 1 &&
|
|
25
|
+
decoratorArguments[0].type === 'ObjectExpression'
|
|
26
|
+
) {
|
|
27
|
+
const properties = decoratorArguments[0].properties;
|
|
28
|
+
|
|
29
|
+
// Look for `encapsulation` property with value `ViewEncapsulation.None`
|
|
30
|
+
const encapsulationProperty = properties.find(
|
|
31
|
+
(prop) =>
|
|
32
|
+
prop.type === 'Property' &&
|
|
33
|
+
prop.key.name === 'encapsulation' &&
|
|
34
|
+
prop.value.type === 'MemberExpression' &&
|
|
35
|
+
prop.value.object.name ===
|
|
36
|
+
'ViewEncapsulation' &&
|
|
37
|
+
prop.value.property.name === 'None',
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
if (encapsulationProperty) {
|
|
41
|
+
context.report({
|
|
42
|
+
node: encapsulationProperty,
|
|
43
|
+
messageId: 'noViewEncapsulationNone',
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
},
|
|
51
|
+
};
|
|
@@ -10,7 +10,9 @@ function hasReturnStatement(node) {
|
|
|
10
10
|
if (node.type === 'IfStatement') {
|
|
11
11
|
return (
|
|
12
12
|
hasReturnStatement(node.consequent) &&
|
|
13
|
-
(node.alternate !== null
|
|
13
|
+
(node.alternate !== null
|
|
14
|
+
? hasReturnStatement(node.alternate)
|
|
15
|
+
: false)
|
|
14
16
|
);
|
|
15
17
|
}
|
|
16
18
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
module.exports = {
|
|
2
2
|
meta: {
|
|
3
|
-
type:
|
|
3
|
+
type: 'suggestion',
|
|
4
4
|
docs: {
|
|
5
5
|
description:
|
|
6
|
-
|
|
6
|
+
'Replace `first()` with `take(1)` and require `filter()` to be used with `take(1)` in pipe.',
|
|
7
7
|
},
|
|
8
|
-
fixable:
|
|
8
|
+
fixable: 'code',
|
|
9
9
|
schema: [],
|
|
10
10
|
},
|
|
11
11
|
|
|
@@ -15,38 +15,38 @@ module.exports = {
|
|
|
15
15
|
CallExpression(node) {
|
|
16
16
|
// Check if the callee (function being called) is `first`
|
|
17
17
|
if (
|
|
18
|
-
node.callee.type ===
|
|
19
|
-
node.callee.name ===
|
|
18
|
+
node.callee.type === 'Identifier' &&
|
|
19
|
+
node.callee.name === 'first' &&
|
|
20
20
|
node.arguments.length === 0
|
|
21
21
|
) {
|
|
22
22
|
context.report({
|
|
23
23
|
node,
|
|
24
24
|
message:
|
|
25
|
-
|
|
25
|
+
'Replace `first()` with `take(1)` to avoid errors if no value is ever emitted.',
|
|
26
26
|
});
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
// Check for `pipe()` with `take(1)`
|
|
30
30
|
if (
|
|
31
|
-
node.callee.type ===
|
|
32
|
-
node.callee.property.name ===
|
|
31
|
+
node.callee.type === 'MemberExpression' &&
|
|
32
|
+
node.callee.property.name === 'pipe'
|
|
33
33
|
) {
|
|
34
34
|
const pipeArguments = node.arguments;
|
|
35
35
|
|
|
36
36
|
// Check if `take(1)` is present
|
|
37
37
|
const hasTakeOne = pipeArguments.some(
|
|
38
38
|
(arg) =>
|
|
39
|
-
arg.type ===
|
|
40
|
-
arg.callee.name ===
|
|
39
|
+
arg.type === 'CallExpression' &&
|
|
40
|
+
arg.callee.name === 'take' &&
|
|
41
41
|
arg.arguments.length === 1 &&
|
|
42
|
-
arg.arguments[0].value === 1
|
|
42
|
+
arg.arguments[0].value === 1,
|
|
43
43
|
);
|
|
44
44
|
|
|
45
45
|
// Check if `filter()` is present
|
|
46
46
|
const hasFilter = pipeArguments.some(
|
|
47
47
|
(arg) =>
|
|
48
|
-
arg.type ===
|
|
49
|
-
arg.callee.name ===
|
|
48
|
+
arg.type === 'CallExpression' &&
|
|
49
|
+
arg.callee.name === 'filter',
|
|
50
50
|
);
|
|
51
51
|
|
|
52
52
|
// Report an error if `take(1)` is used without `filter()`
|
|
@@ -54,7 +54,7 @@ module.exports = {
|
|
|
54
54
|
context.report({
|
|
55
55
|
node,
|
|
56
56
|
message:
|
|
57
|
-
|
|
57
|
+
'Using `take(1)` requires `filter()` to be used in the same pipe to avoid `null` or `undefined` values which will trigger an error. Example `filter((value) => Boolean(value ?? false)),`',
|
|
58
58
|
});
|
|
59
59
|
}
|
|
60
60
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
meta: {
|
|
3
|
+
type: 'suggestion',
|
|
4
|
+
docs: {
|
|
5
|
+
description:
|
|
6
|
+
'Ensure variables initialized with "signal()" or "input()" have valid naming conventions.',
|
|
7
|
+
},
|
|
8
|
+
messages: {
|
|
9
|
+
missingSignal: 'Variable "{{ name }}" should end with "Signal".',
|
|
10
|
+
invalidSuffix:
|
|
11
|
+
'Variable "{{ name }}" should end with "Signal". Invalid suffix "{{ invalidSuffix }}".',
|
|
12
|
+
},
|
|
13
|
+
schema: [],
|
|
14
|
+
},
|
|
15
|
+
create(context) {
|
|
16
|
+
const invalidSuffixes = [
|
|
17
|
+
'ComputedSignal',
|
|
18
|
+
'WriteableSignal',
|
|
19
|
+
'InputSignal',
|
|
20
|
+
'OutputSignal',
|
|
21
|
+
'LinkedSignal',
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
PropertyDefinition(node) {
|
|
26
|
+
// Check if the initializer exists and is a CallExpression
|
|
27
|
+
if (node.value && node.value.type === 'CallExpression') {
|
|
28
|
+
const { callee } = node.value;
|
|
29
|
+
|
|
30
|
+
// Check if the function called is named "signal" or "input"
|
|
31
|
+
if (
|
|
32
|
+
callee.type === 'Identifier' &&
|
|
33
|
+
(callee.name === 'signal' || callee.name === 'input')
|
|
34
|
+
) {
|
|
35
|
+
const variableName = node.key.name;
|
|
36
|
+
|
|
37
|
+
// Check for invalid suffixes
|
|
38
|
+
const invalidSuffix = invalidSuffixes.find((suffix) =>
|
|
39
|
+
variableName.endsWith(suffix),
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
if (invalidSuffix) {
|
|
43
|
+
context.report({
|
|
44
|
+
node: node.key,
|
|
45
|
+
messageId: 'invalidSuffix',
|
|
46
|
+
data: { name: variableName, invalidSuffix },
|
|
47
|
+
});
|
|
48
|
+
} else if (!variableName.endsWith('Signal')) {
|
|
49
|
+
// Ensure the variable name ends with "Signal"
|
|
50
|
+
context.report({
|
|
51
|
+
node: node.key,
|
|
52
|
+
messageId: 'missingSignal',
|
|
53
|
+
data: { name: variableName },
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
};
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
module.exports = {
|
|
2
2
|
meta: {
|
|
3
|
-
type:
|
|
3
|
+
type: 'suggestion',
|
|
4
4
|
docs: {
|
|
5
5
|
description:
|
|
6
|
-
|
|
6
|
+
'Enforce using `danishCurrency` pipe instead of `currency` pipe in Angular HTML templates.',
|
|
7
7
|
},
|
|
8
|
-
fixable:
|
|
8
|
+
fixable: 'code',
|
|
9
9
|
schema: [],
|
|
10
10
|
},
|
|
11
11
|
|
|
@@ -24,18 +24,18 @@ module.exports = {
|
|
|
24
24
|
|
|
25
25
|
const startLine = templateContent
|
|
26
26
|
.slice(0, matchStart)
|
|
27
|
-
.split(
|
|
27
|
+
.split('\n').length;
|
|
28
28
|
const startColumn =
|
|
29
29
|
matchStart -
|
|
30
|
-
templateContent.lastIndexOf(
|
|
30
|
+
templateContent.lastIndexOf('\n', matchStart) -
|
|
31
31
|
1;
|
|
32
32
|
|
|
33
33
|
const endLine = templateContent
|
|
34
34
|
.slice(0, matchEnd)
|
|
35
|
-
.split(
|
|
35
|
+
.split('\n').length;
|
|
36
36
|
const endColumn =
|
|
37
37
|
matchEnd -
|
|
38
|
-
templateContent.lastIndexOf(
|
|
38
|
+
templateContent.lastIndexOf('\n', matchEnd) -
|
|
39
39
|
1;
|
|
40
40
|
|
|
41
41
|
context.report({
|