@dineroregnskab/eslint-plugin-custom-rules 2.1.4 → 3.0.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/.prettierrc ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "singleQuote": true,
3
+ "endOfLine": "auto",
4
+ "overrides": [
5
+ {
6
+ "files": "*.html",
7
+ "options": {
8
+ "parser": "angular"
9
+ }
10
+ }
11
+ ]
12
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "eslint.workingDirectories": ["./example"]
3
+ }
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
- - Add the rule here: `dinero-web-2.0/Dinero.Packages/Dinero.EslintCustomRules/rules`
21
+ - Create a new js rule in this directory: `./rules`
22
22
 
23
- - Add the new js rule to this file `eslint-plugin-custom-rules.js`.
23
+ - Add the new rule to this file `./eslint-plugin-custom-rules.js`.
24
24
 
25
- - You can test the rule by adding some HTML code to test it on here `dinero-web-2.0/Dinero.Packages/Dinero.EslintCustomRules/example`
25
+ - Add the new rule to `./example/.eslintrc.json`
26
26
 
27
- - After adding the rule you can restart the ESLint server in vs code by pressing F1 -> ESLint: Restart ESLint Server.
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
- Add html rules in `test.html` and run test with
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 what is going on. Then run `npm run testhtml` npm script to see it 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: "suggestion",
43
+ type: 'suggestion',
49
44
  docs: {
50
45
  description:
51
- "Enforce using `danishCurrency` pipe instead of `currency` pipe in Angular HTML templates.",
46
+ 'Enforce using `danishCurrency` pipe instead of `currency` pipe in Angular HTML templates.',
52
47
  },
53
- fixable: "code",
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("Parsed Node Types:", node);
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,18 @@
1
- const returnReducerRule = require("./rules/reducers-should-always-return");
2
- const camelCaseRule = require("./rules/camel-case-attributes");
3
- const dayjsWithTimeZoneRule = require("./rules/dayjs-with-timezone");
4
- const useDanishCurrencyPipeRule = require("./rules/use-danish-currency-pipe");
5
- const replaceFirstWithTakeRule = require("./rules/replace-first-with-take");
6
-
7
- const customRulesPlugin = {
8
- rules: {
9
- "reducers-should-always-return": returnReducerRule,
10
- "attr-camel-case-rule": camelCaseRule,
11
- "dayjs-with-timezone": dayjsWithTimeZoneRule,
12
- "use-danish-currency-pipe": useDanishCurrencyPipeRule,
13
- "replace-first-with-take": replaceFirstWithTakeRule,
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'),
15
12
  };
16
13
 
17
- module.exports = customRulesPlugin;
14
+ console.log('Custom ESLint rules loaded:', Object.keys(rules)); // Debug log
15
+
16
+ module.exports = {
17
+ rules,
18
+ };
package/package.json CHANGED
@@ -1,23 +1,30 @@
1
1
  {
2
2
  "name": "@dineroregnskab/eslint-plugin-custom-rules",
3
- "version": "2.1.4",
3
+ "version": "3.0.0",
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": "^8.53.0"
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": "^17.1.0"
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 ((node.name === 'data-cy' || node.name === 'id') && node.value) {
15
- const camelCasedValue = new RegExp(/^[a-z]+([A-Z]?[a-z]*)*$/);
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
- if (!camelCasedValue.test(node.value)) {
18
- context.report({
19
- node,
20
- message:
21
- 'The value of data-cy and id attributes should be in camelCase.',
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: "suggestion",
3
+ type: 'suggestion',
4
4
  docs: {
5
- description: "Enforce using dayjs.tz() instead of dayjs()",
6
- category: "Best Practices",
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 === "Identifier" &&
18
- node.callee.name === "dayjs"
17
+ node.callee.type === 'Identifier' &&
18
+ node.callee.name === 'dayjs'
19
19
  ) {
20
20
  context.report({
21
21
  node,
22
22
  message:
23
- "Consider using dayjs.tz() instead of dayjs() for timezone support",
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,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 ? hasReturnStatement(node.alternate) : false)
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: "suggestion",
3
+ type: 'suggestion',
4
4
  docs: {
5
5
  description:
6
- "Replace `first()` with `take(1)` and require `filter()` to be used with `take(1)` in pipe.",
6
+ 'Replace `first()` with `take(1)` and require `filter()` to be used with `take(1)` in pipe.',
7
7
  },
8
- fixable: "code",
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 === "Identifier" &&
19
- node.callee.name === "first" &&
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
- "Replace `first()` with `take(1)` to avoid errors if no value is ever emitted.",
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 === "MemberExpression" &&
32
- node.callee.property.name === "pipe"
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 === "CallExpression" &&
40
- arg.callee.name === "take" &&
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 === "CallExpression" &&
49
- arg.callee.name === "filter"
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
- "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)),`",
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: "suggestion",
3
+ type: 'suggestion',
4
4
  docs: {
5
5
  description:
6
- "Enforce using `danishCurrency` pipe instead of `currency` pipe in Angular HTML templates.",
6
+ 'Enforce using `danishCurrency` pipe instead of `currency` pipe in Angular HTML templates.',
7
7
  },
8
- fixable: "code",
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("\n").length;
27
+ .split('\n').length;
28
28
  const startColumn =
29
29
  matchStart -
30
- templateContent.lastIndexOf("\n", matchStart) -
30
+ templateContent.lastIndexOf('\n', matchStart) -
31
31
  1;
32
32
 
33
33
  const endLine = templateContent
34
34
  .slice(0, matchEnd)
35
- .split("\n").length;
35
+ .split('\n').length;
36
36
  const endColumn =
37
37
  matchEnd -
38
- templateContent.lastIndexOf("\n", matchEnd) -
38
+ templateContent.lastIndexOf('\n', matchEnd) -
39
39
  1;
40
40
 
41
41
  context.report({