@simplysm/lint 14.0.1 → 14.0.4

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.
Files changed (31) hide show
  1. package/README.md +107 -0
  2. package/dist/eslint-plugin.d.ts +12 -0
  3. package/dist/eslint-plugin.d.ts.map +1 -1
  4. package/dist/eslint-plugin.js +8 -0
  5. package/dist/eslint-plugin.js.map +1 -1
  6. package/dist/eslint-recommended.d.ts.map +1 -1
  7. package/dist/eslint-recommended.js +16 -2
  8. package/dist/eslint-recommended.js.map +1 -1
  9. package/dist/rules/ng-template-no-todo-comments.d.ts +12 -0
  10. package/dist/rules/ng-template-no-todo-comments.d.ts.map +1 -0
  11. package/dist/rules/ng-template-no-todo-comments.js +46 -0
  12. package/dist/rules/ng-template-no-todo-comments.js.map +1 -0
  13. package/dist/rules/ng-template-sd-require-binding-attrs.d.ts +18 -0
  14. package/dist/rules/ng-template-sd-require-binding-attrs.d.ts.map +1 -0
  15. package/dist/rules/ng-template-sd-require-binding-attrs.js +88 -0
  16. package/dist/rules/ng-template-sd-require-binding-attrs.js.map +1 -0
  17. package/dist/rules/ts-no-unused-injects.d.ts +13 -0
  18. package/dist/rules/ts-no-unused-injects.d.ts.map +1 -0
  19. package/dist/rules/ts-no-unused-injects.js +81 -0
  20. package/dist/rules/ts-no-unused-injects.js.map +1 -0
  21. package/dist/rules/ts-no-unused-protected-readonly.d.ts +13 -0
  22. package/dist/rules/ts-no-unused-protected-readonly.d.ts.map +1 -0
  23. package/dist/rules/ts-no-unused-protected-readonly.js +127 -0
  24. package/dist/rules/ts-no-unused-protected-readonly.js.map +1 -0
  25. package/package.json +5 -3
  26. package/src/eslint-plugin.ts +8 -0
  27. package/src/eslint-recommended.ts +16 -2
  28. package/src/rules/ng-template-no-todo-comments.ts +48 -0
  29. package/src/rules/ng-template-sd-require-binding-attrs.ts +111 -0
  30. package/src/rules/ts-no-unused-injects.ts +88 -0
  31. package/src/rules/ts-no-unused-protected-readonly.ts +151 -0
package/README.md ADDED
@@ -0,0 +1,107 @@
1
+ # @simplysm/lint
2
+
3
+ Shared ESLint plugin and recommended configuration for Simplysm projects.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @simplysm/lint
9
+ ```
10
+
11
+ ## Entrypoints
12
+
13
+ ### `@simplysm/lint/eslint-plugin`
14
+
15
+ ESLint plugin providing 7 custom rules for Angular/TypeScript projects.
16
+
17
+ ```typescript
18
+ import plugin from "@simplysm/lint/eslint-plugin";
19
+ ```
20
+
21
+ Default export: `{ rules: { ... } }`
22
+
23
+ #### Rules
24
+
25
+ | Rule | Type | Fixable | Description |
26
+ |---|---|---|---|
27
+ | `ng-template-no-todo-comments` | problem | no | Warn on TODO comments inside Angular HTML templates |
28
+ | `ng-template-sd-require-binding-attrs` | problem | yes | Disallow non-whitelisted plain attributes on `sd-*` prefixed components; require Angular property bindings instead |
29
+ | `no-hard-private` | problem | yes | Disallow ES hard private fields (`#field`); require TypeScript `private _field` style instead |
30
+ | `no-subpath-imports-from-simplysm` | problem | yes | Disallow importing from `@simplysm/*/src/*` subpaths (use package entrypoints) |
31
+ | `ts-no-throw-not-implemented-error` | suggestion | no | Warn on `NotImplementedError` usage (indicates unfinished implementation) |
32
+ | `ts-no-unused-injects` | problem | yes | Disallow unused Angular `inject()` fields |
33
+ | `ts-no-unused-protected-readonly` | problem | yes | Disallow unused `protected readonly` fields in Angular components |
34
+
35
+ ### `@simplysm/lint/eslint-recommended`
36
+
37
+ Pre-configured ESLint flat config for Simplysm projects.
38
+
39
+ ```typescript
40
+ import recommended from "@simplysm/lint/eslint-recommended";
41
+ ```
42
+
43
+ Default export: a complete ESLint flat config array (from `tseslint.config()`).
44
+
45
+ #### Included Configurations
46
+
47
+ - `angular-eslint` template recommended + accessibility
48
+ - `typescript-eslint` recommended rules
49
+ - `eslint-plugin-import` (extraneous dependency checking)
50
+ - `eslint-plugin-unused-imports` (auto-remove unused imports)
51
+ - All 7 `@simplysm/lint/eslint-plugin` rules
52
+
53
+ #### Key Rules Enabled
54
+
55
+ - `eqeqeq` (always, null-ignore)
56
+ - `no-console` (error in .ts files)
57
+ - `@typescript-eslint/require-await`, `await-thenable`, `no-floating-promises`
58
+ - `@typescript-eslint/strict-boolean-expressions` (nullable boolean/object allowed)
59
+ - `@typescript-eslint/prefer-readonly`
60
+ - `@typescript-eslint/no-unnecessary-condition`
61
+ - Bans `Buffer`, `events`, `eventemitter3` imports
62
+ - Test files (`**/tests/**/*.ts`): relaxes `no-console`, `import/no-extraneous-dependencies`, `ts-no-throw-not-implemented-error`
63
+
64
+ #### File Patterns
65
+
66
+ | Pattern | Applied Rules |
67
+ |---|---|
68
+ | `**/*.js`, `**/*.mjs`, `**/*.cjs` | Common rules + import checks + unused imports + no-hard-private + no-subpath-imports |
69
+ | `**/*.ts` | Full TypeScript rules + Angular rules + all custom rules |
70
+ | `**/*.html` | Angular template rules (no-todo-comments, sd-require-binding-attrs) |
71
+ | `**/tests/**/*.ts` | Relaxed rules (no-console off, extraneous deps off) |
72
+
73
+ #### Ignored Patterns
74
+
75
+ - `**/node_modules/**`
76
+ - `**/dist/**`
77
+ - `**/.*/**`
78
+ - `**/_*/**`
79
+
80
+ ## Usage
81
+
82
+ ### eslint.config.js
83
+
84
+ ```javascript
85
+ import recommended from "@simplysm/lint/eslint-recommended";
86
+
87
+ export default [
88
+ ...recommended,
89
+ // add project-specific overrides here
90
+ ];
91
+ ```
92
+
93
+ ### Use plugin rules individually
94
+
95
+ ```javascript
96
+ import plugin from "@simplysm/lint/eslint-plugin";
97
+
98
+ export default [
99
+ {
100
+ plugins: { "@simplysm": plugin },
101
+ rules: {
102
+ "@simplysm/no-hard-private": "error",
103
+ "@simplysm/ts-no-unused-injects": "error",
104
+ },
105
+ },
106
+ ];
107
+ ```
@@ -1,5 +1,11 @@
1
1
  declare const _default: {
2
2
  rules: {
3
+ "ng-template-no-todo-comments": import("@typescript-eslint/utils/ts-eslint").RuleModule<"noTodo", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
4
+ name: string;
5
+ };
6
+ "ng-template-sd-require-binding-attrs": import("@typescript-eslint/utils/ts-eslint").RuleModule<"requireBindingForAttribute", [import("./rules/ng-template-sd-require-binding-attrs").RuleOptions], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
7
+ name: string;
8
+ };
3
9
  "no-hard-private": import("@typescript-eslint/utils/ts-eslint").RuleModule<"preferSoftPrivate" | "nameConflict", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
4
10
  name: string;
5
11
  };
@@ -9,6 +15,12 @@ declare const _default: {
9
15
  "ts-no-throw-not-implemented-error": import("@typescript-eslint/utils/ts-eslint").RuleModule<"noThrowNotImplementedError", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
10
16
  name: string;
11
17
  };
18
+ "ts-no-unused-injects": import("@typescript-eslint/utils/ts-eslint").RuleModule<"unusedInject", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
19
+ name: string;
20
+ };
21
+ "ts-no-unused-protected-readonly": import("@typescript-eslint/utils/ts-eslint").RuleModule<"unusedField", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
22
+ name: string;
23
+ };
12
24
  };
13
25
  };
14
26
  export default _default;
@@ -1 +1 @@
1
- {"version":3,"file":"eslint-plugin.d.ts","sourceRoot":"","sources":["../src/eslint-plugin.ts"],"names":[],"mappings":";;;;;;;;;;;;;AAIA,wBAME"}
1
+ {"version":3,"file":"eslint-plugin.d.ts","sourceRoot":"","sources":["../src/eslint-plugin.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAQA,wBAUE"}
@@ -1,11 +1,19 @@
1
+ import ngTemplateNoTodoComments from "./rules/ng-template-no-todo-comments.js";
2
+ import ngTemplateSdRequireBindingAttrs from "./rules/ng-template-sd-require-binding-attrs.js";
1
3
  import noHardPrivate from "./rules/no-hard-private.js";
2
4
  import noSubpathImportsFromSimplysm from "./rules/no-subpath-imports-from-simplysm.js";
3
5
  import tsNoThrowNotImplementedError from "./rules/ts-no-throw-not-implemented-error.js";
6
+ import tsNoUnusedInjects from "./rules/ts-no-unused-injects.js";
7
+ import tsNoUnusedProtectedReadonly from "./rules/ts-no-unused-protected-readonly.js";
4
8
  export default {
5
9
  rules: {
10
+ "ng-template-no-todo-comments": ngTemplateNoTodoComments,
11
+ "ng-template-sd-require-binding-attrs": ngTemplateSdRequireBindingAttrs,
6
12
  "no-hard-private": noHardPrivate,
7
13
  "no-subpath-imports-from-simplysm": noSubpathImportsFromSimplysm,
8
14
  "ts-no-throw-not-implemented-error": tsNoThrowNotImplementedError,
15
+ "ts-no-unused-injects": tsNoUnusedInjects,
16
+ "ts-no-unused-protected-readonly": tsNoUnusedProtectedReadonly,
9
17
  },
10
18
  };
11
19
  //# sourceMappingURL=eslint-plugin.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"eslint-plugin.js","sourceRoot":"","sources":["../src/eslint-plugin.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,MAAM,yBAAyB,CAAC;AACpD,OAAO,4BAA4B,MAAM,0CAA0C,CAAC;AACpF,OAAO,4BAA4B,MAAM,2CAA2C,CAAC;AAErF,eAAe;IACb,KAAK,EAAE;QACL,iBAAiB,EAAE,aAAa;QAChC,kCAAkC,EAAE,4BAA4B;QAChE,mCAAmC,EAAE,4BAA4B;KAClE;CACF,CAAC"}
1
+ {"version":3,"file":"eslint-plugin.js","sourceRoot":"","sources":["../src/eslint-plugin.ts"],"names":[],"mappings":"AAAA,OAAO,wBAAwB,MAAM,sCAAsC,CAAC;AAC5E,OAAO,+BAA+B,MAAM,8CAA8C,CAAC;AAC3F,OAAO,aAAa,MAAM,yBAAyB,CAAC;AACpD,OAAO,4BAA4B,MAAM,0CAA0C,CAAC;AACpF,OAAO,4BAA4B,MAAM,2CAA2C,CAAC;AACrF,OAAO,iBAAiB,MAAM,8BAA8B,CAAC;AAC7D,OAAO,2BAA2B,MAAM,yCAAyC,CAAC;AAElF,eAAe;IACb,KAAK,EAAE;QACL,8BAA8B,EAAE,wBAAwB;QACxD,sCAAsC,EAAE,+BAA+B;QACvE,iBAAiB,EAAE,aAAa;QAChC,kCAAkC,EAAE,4BAA4B;QAChE,mCAAmC,EAAE,4BAA4B;QACjE,sBAAsB,EAAE,iBAAiB;QACzC,iCAAiC,EAAE,2BAA2B;KAC/D;CACF,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"eslint-recommended.d.ts","sourceRoot":"","sources":["../src/eslint-recommended.ts"],"names":[],"mappings":"AAAA,OAAiB,EAAE,KAAK,UAAU,EAAE,MAAM,mBAAmB,CAAC;;AA2D9D,wBA0IE"}
1
+ {"version":3,"file":"eslint-recommended.d.ts","sourceRoot":"","sources":["../src/eslint-recommended.ts"],"names":[],"mappings":"AAAA,OAAiB,EAAE,KAAK,UAAU,EAAE,MAAM,mBAAmB,CAAC;;AA2D9D,wBAwJE"}
@@ -1,12 +1,12 @@
1
1
  import tseslint, {} from "typescript-eslint";
2
2
  import angular from "angular-eslint";
3
+ import globals from "globals";
3
4
  import plugin from "./eslint-plugin.js";
4
5
  import importPlugin from "eslint-plugin-import";
5
6
  import unusedImportsPlugin from "eslint-plugin-unused-imports";
6
7
  import { ESLint } from "eslint";
7
8
  import { fileURLToPath } from "url";
8
9
  const commonRules = {
9
- "no-console": "error",
10
10
  "no-warning-comments": "warn",
11
11
  "eqeqeq": ["error", "always", { null: "ignore" }],
12
12
  "no-self-compare": "error",
@@ -62,11 +62,14 @@ export default tseslint.config({
62
62
  ],
63
63
  }, {
64
64
  languageOptions: {
65
- ecmaVersion: 2024,
65
+ ecmaVersion: "latest",
66
66
  sourceType: "module",
67
67
  },
68
68
  }, {
69
69
  files: ["**/*.js", "**/*.mjs", "**/*.cjs"],
70
+ languageOptions: {
71
+ globals: globals.node,
72
+ },
70
73
  plugins: {
71
74
  "import": importPlugin,
72
75
  "@simplysm": plugin,
@@ -120,6 +123,7 @@ export default tseslint.config({
120
123
  },
121
124
  rules: {
122
125
  ...commonRules,
126
+ "no-console": "error",
123
127
  "@typescript-eslint/require-await": "error",
124
128
  "@typescript-eslint/await-thenable": "error",
125
129
  "@typescript-eslint/return-await": ["error", "in-try-catch"],
@@ -158,6 +162,9 @@ export default tseslint.config({
158
162
  "@simplysm/no-hard-private": "error",
159
163
  "@simplysm/no-subpath-imports-from-simplysm": "error",
160
164
  "@simplysm/ts-no-throw-not-implemented-error": "warn",
165
+ "@simplysm/ts-no-unused-injects": "error",
166
+ "@simplysm/ts-no-unused-protected-readonly": "error",
167
+ "@angular-eslint/no-output-native": "off",
161
168
  ...unusedImportsRules,
162
169
  ...noNodeBuiltinsRules,
163
170
  "import/no-extraneous-dependencies": "error",
@@ -165,6 +172,13 @@ export default tseslint.config({
165
172
  }, {
166
173
  files: ["**/*.html"],
167
174
  extends: [...angular.configs.templateRecommended, ...angular.configs.templateAccessibility],
175
+ plugins: {
176
+ "@simplysm": plugin,
177
+ },
178
+ rules: {
179
+ "@simplysm/ng-template-no-todo-comments": "warn",
180
+ "@simplysm/ng-template-sd-require-binding-attrs": "error",
181
+ },
168
182
  }, {
169
183
  files: ["**/tests/**/*.ts"],
170
184
  rules: {
@@ -1 +1 @@
1
- {"version":3,"file":"eslint-recommended.js","sourceRoot":"","sources":["../src/eslint-recommended.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"eslint-recommended.js","sourceRoot":"","sources":["../src/eslint-recommended.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * HTML 템플릿 내 TODO 주석을 감지하여 경고하는 ESLint 규칙.
3
+ *
4
+ * @remarks
5
+ * `<!-- TODO: ... -->` 형태의 HTML 주석을 찾아 보고합니다.
6
+ * raw text regex 방식으로 동작하므로 AST 노드 방문자가 아닌 빈 객체를 반환합니다.
7
+ */
8
+ declare const _default: import("@typescript-eslint/utils/ts-eslint").RuleModule<"noTodo", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
9
+ name: string;
10
+ };
11
+ export default _default;
12
+ //# sourceMappingURL=ng-template-no-todo-comments.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ng-template-no-todo-comments.d.ts","sourceRoot":"","sources":["../../src/rules/ng-template-no-todo-comments.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;;;;AACH,wBAsCG"}
@@ -0,0 +1,46 @@
1
+ import { createRule } from "../utils/create-rule.js";
2
+ /**
3
+ * HTML 템플릿 내 TODO 주석을 감지하여 경고하는 ESLint 규칙.
4
+ *
5
+ * @remarks
6
+ * `<!-- TODO: ... -->` 형태의 HTML 주석을 찾아 보고합니다.
7
+ * raw text regex 방식으로 동작하므로 AST 노드 방문자가 아닌 빈 객체를 반환합니다.
8
+ */
9
+ export default createRule({
10
+ name: "ng-template-no-todo-comments",
11
+ meta: {
12
+ type: "problem",
13
+ docs: {
14
+ description: "HTML 템플릿 내 TODO 주석을 경고합니다.",
15
+ },
16
+ schema: [],
17
+ messages: {
18
+ noTodo: "{{content}}",
19
+ },
20
+ },
21
+ defaultOptions: [],
22
+ create(context) {
23
+ const sourceCode = context.sourceCode;
24
+ const source = sourceCode.getText();
25
+ const commentRegex = /<!--([\s\S]*?)-->/g;
26
+ let match;
27
+ while ((match = commentRegex.exec(source)) !== null) {
28
+ const commentContent = match[1];
29
+ const todoIndex = commentContent.indexOf("TODO:");
30
+ if (todoIndex < 0)
31
+ continue;
32
+ const start = match.index;
33
+ const end = start + match[0].length;
34
+ const content = commentContent.slice(todoIndex + 5).trim();
35
+ const loc = sourceCode.getLocFromIndex(start);
36
+ const endLoc = sourceCode.getLocFromIndex(end);
37
+ context.report({
38
+ loc: { start: loc, end: endLoc },
39
+ messageId: "noTodo",
40
+ data: { content },
41
+ });
42
+ }
43
+ return {};
44
+ },
45
+ });
46
+ //# sourceMappingURL=ng-template-no-todo-comments.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ng-template-no-todo-comments.js","sourceRoot":"","sources":["../../src/rules/ng-template-no-todo-comments.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAElD;;;;;;GAMG;AACH,eAAe,UAAU,CAAC;IACxB,IAAI,EAAE,8BAA8B;IACpC,IAAI,EAAE;QACJ,IAAI,EAAE,SAAS;QACf,IAAI,EAAE;YACJ,WAAW,EAAE,4BAA4B;SAC1C;QACD,MAAM,EAAE,EAAE;QACV,QAAQ,EAAE;YACR,MAAM,EAAE,aAAa;SACtB;KACF;IACD,cAAc,EAAE,EAAE;IAClB,MAAM,CAAC,OAAO;QACZ,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;QACtC,MAAM,MAAM,GAAG,UAAU,CAAC,OAAO,EAAE,CAAC;QACpC,MAAM,YAAY,GAAG,oBAAoB,CAAC;QAC1C,IAAI,KAAK,CAAC;QACV,OAAO,CAAC,KAAK,GAAG,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;YACpD,MAAM,cAAc,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;YAChC,MAAM,SAAS,GAAG,cAAc,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;YAClD,IAAI,SAAS,GAAG,CAAC;gBAAE,SAAS;YAE5B,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC;YAC1B,MAAM,GAAG,GAAG,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;YACpC,MAAM,OAAO,GAAG,cAAc,CAAC,KAAK,CAAC,SAAS,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;YAC3D,MAAM,GAAG,GAAG,UAAU,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC;YAC9C,MAAM,MAAM,GAAG,UAAU,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC;YAE/C,OAAO,CAAC,MAAM,CAAC;gBACb,GAAG,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE;gBAChC,SAAS,EAAE,QAAQ;gBACnB,IAAI,EAAE,EAAE,OAAO,EAAE;aAClB,CAAC,CAAC;QACL,CAAC;QAED,OAAO,EAAE,CAAC;IACZ,CAAC;CACF,CAAC,CAAC"}
@@ -0,0 +1,18 @@
1
+ export interface RuleOptions {
2
+ selectorPrefixes?: string[];
3
+ allowAttributes?: string[];
4
+ allowAttributePrefixes?: string[];
5
+ }
6
+ /**
7
+ * sd-* 컴포넌트에서 plain attribute 사용을 제한하고 Angular property binding을 강제하는 ESLint 규칙.
8
+ *
9
+ * @remarks
10
+ * `sd-` 접두사를 가진 커스텀 컴포넌트에서 허용되지 않은 plain attribute를 감지합니다.
11
+ * 허용 목록(id, class, style, title, tabindex, role)과 허용 접두사(aria-, data-, sd-)에
12
+ * 해당하지 않는 attribute는 `[attr]="..."` 형태의 property binding으로 변환하도록 autofix합니다.
13
+ */
14
+ declare const _default: import("@typescript-eslint/utils/ts-eslint").RuleModule<"requireBindingForAttribute", [RuleOptions], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
15
+ name: string;
16
+ };
17
+ export default _default;
18
+ //# sourceMappingURL=ng-template-sd-require-binding-attrs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ng-template-sd-require-binding-attrs.d.ts","sourceRoot":"","sources":["../../src/rules/ng-template-sd-require-binding-attrs.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,WAAW;IAC1B,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC5B,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,sBAAsB,CAAC,EAAE,MAAM,EAAE,CAAC;CACnC;AAQD;;;;;;;GAOG;;;;AACH,wBAuFG"}
@@ -0,0 +1,88 @@
1
+ import { getTemplateParserServices } from "@angular-eslint/utils";
2
+ import { createRule } from "../utils/create-rule.js";
3
+ const DEFAULT_OPTIONS = {
4
+ selectorPrefixes: ["sd-"],
5
+ allowAttributes: ["id", "class", "style", "title", "tabindex", "role"],
6
+ allowAttributePrefixes: ["aria-", "data-", "sd-"],
7
+ };
8
+ /**
9
+ * sd-* 컴포넌트에서 plain attribute 사용을 제한하고 Angular property binding을 강제하는 ESLint 규칙.
10
+ *
11
+ * @remarks
12
+ * `sd-` 접두사를 가진 커스텀 컴포넌트에서 허용되지 않은 plain attribute를 감지합니다.
13
+ * 허용 목록(id, class, style, title, tabindex, role)과 허용 접두사(aria-, data-, sd-)에
14
+ * 해당하지 않는 attribute는 `[attr]="..."` 형태의 property binding으로 변환하도록 autofix합니다.
15
+ */
16
+ export default createRule({
17
+ name: "ng-template-sd-require-binding-attrs",
18
+ meta: {
19
+ type: "problem",
20
+ docs: {
21
+ description: 'Disallow non-whitelisted plain attributes on prefixed components (e.g. sd-*) and require using Angular property bindings instead.',
22
+ },
23
+ fixable: "code",
24
+ schema: [
25
+ {
26
+ type: "object",
27
+ properties: {
28
+ selectorPrefixes: { type: "array", items: { type: "string" } },
29
+ allowAttributes: { type: "array", items: { type: "string" } },
30
+ allowAttributePrefixes: { type: "array", items: { type: "string" } },
31
+ },
32
+ additionalProperties: false,
33
+ },
34
+ ],
35
+ messages: {
36
+ requireBindingForAttribute: 'Attribute "{{attrName}}" is not allowed as a plain attribute on "{{elementName}}". Use a property binding instead, e.g. [{{attrName}}]="…".',
37
+ },
38
+ },
39
+ defaultOptions: [{}],
40
+ create(context) {
41
+ const parserServices = getTemplateParserServices(context);
42
+ const userOptions = context.options.at(0) ?? {};
43
+ const selectorPrefixes = userOptions.selectorPrefixes ?? DEFAULT_OPTIONS.selectorPrefixes;
44
+ const allowAttributes = userOptions.allowAttributes ?? DEFAULT_OPTIONS.allowAttributes;
45
+ const allowAttributePrefixes = userOptions.allowAttributePrefixes ?? DEFAULT_OPTIONS.allowAttributePrefixes;
46
+ const allowedAttrSet = new Set(allowAttributes.map((attr) => attr.toLowerCase()));
47
+ function isTargetElement(node) {
48
+ const tagName = node.name.toLowerCase();
49
+ return selectorPrefixes.some((prefix) => tagName.startsWith(prefix.toLowerCase()));
50
+ }
51
+ function isWhitelistedPlainAttr(attr) {
52
+ const name = attr.name.toLowerCase();
53
+ if (allowedAttrSet.has(name))
54
+ return true;
55
+ return allowAttributePrefixes.some((prefix) => name.startsWith(prefix.toLowerCase()));
56
+ }
57
+ return {
58
+ Element(node) {
59
+ if (!isTargetElement(node))
60
+ return;
61
+ for (const attr of node.attributes) {
62
+ if (isWhitelistedPlainAttr(attr))
63
+ continue;
64
+ const span = attr.sourceSpan;
65
+ const loc = parserServices.convertNodeSourceSpanToLoc(span);
66
+ context.report({
67
+ loc,
68
+ messageId: "requireBindingForAttribute",
69
+ data: { attrName: attr.name, elementName: node.name },
70
+ fix(fixer) {
71
+ const start = span.start.offset;
72
+ const end = span.end.offset;
73
+ if (start >= end)
74
+ return null;
75
+ const rawValue = attr.value;
76
+ if (rawValue === "") {
77
+ return fixer.replaceTextRange([start, end], `[${attr.name}]="true"`);
78
+ }
79
+ const escaped = rawValue.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
80
+ return fixer.replaceTextRange([start, end], `[${attr.name}]="'${escaped}'"`);
81
+ },
82
+ });
83
+ }
84
+ },
85
+ };
86
+ },
87
+ });
88
+ //# sourceMappingURL=ng-template-sd-require-binding-attrs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ng-template-sd-require-binding-attrs.js","sourceRoot":"","sources":["../../src/rules/ng-template-sd-require-binding-attrs.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * 미사용 Angular `inject()` 필드를 감지하는 ESLint 규칙.
3
+ *
4
+ * @remarks
5
+ * 클래스 내에서 `inject()` 호출로 초기화된 프로퍼티 중
6
+ * 같은 클래스 내 다른 곳에서 참조되지 않는 필드를 보고합니다.
7
+ * autofix로 해당 필드를 제거합니다.
8
+ */
9
+ declare const _default: import("@typescript-eslint/utils/ts-eslint").RuleModule<"unusedInject", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
10
+ name: string;
11
+ };
12
+ export default _default;
13
+ //# sourceMappingURL=ts-no-unused-injects.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ts-no-unused-injects.d.ts","sourceRoot":"","sources":["../../src/rules/ts-no-unused-injects.ts"],"names":[],"mappings":"AAoBA;;;;;;;GAOG;;;;AACH,wBA2DG"}
@@ -0,0 +1,81 @@
1
+ import { AST_NODE_TYPES } from "@typescript-eslint/utils";
2
+ import { createRule } from "../utils/create-rule.js";
3
+ function traverseNode(node, callback) {
4
+ callback(node);
5
+ for (const key of Object.keys(node)) {
6
+ if (key === "parent")
7
+ continue;
8
+ const child = node[key];
9
+ if (Array.isArray(child)) {
10
+ for (const c of child) {
11
+ if (c != null && typeof c === "object" && "type" in c) {
12
+ traverseNode(c, callback);
13
+ }
14
+ }
15
+ }
16
+ else if (child != null && typeof child === "object" && "type" in child) {
17
+ traverseNode(child, callback);
18
+ }
19
+ }
20
+ }
21
+ /**
22
+ * 미사용 Angular `inject()` 필드를 감지하는 ESLint 규칙.
23
+ *
24
+ * @remarks
25
+ * 클래스 내에서 `inject()` 호출로 초기화된 프로퍼티 중
26
+ * 같은 클래스 내 다른 곳에서 참조되지 않는 필드를 보고합니다.
27
+ * autofix로 해당 필드를 제거합니다.
28
+ */
29
+ export default createRule({
30
+ name: "ts-no-unused-injects",
31
+ meta: {
32
+ type: "problem",
33
+ docs: {
34
+ description: "Disallow unused Angular inject() fields",
35
+ },
36
+ fixable: "code",
37
+ messages: {
38
+ unusedInject: 'inject() field "{{name}}" is never used.',
39
+ },
40
+ schema: [],
41
+ },
42
+ defaultOptions: [],
43
+ create(context) {
44
+ const sourceCode = context.sourceCode;
45
+ return {
46
+ ClassBody(classBody) {
47
+ const injectFields = classBody.body.filter((node) => node.type === AST_NODE_TYPES.PropertyDefinition &&
48
+ node.value != null &&
49
+ node.value.type === AST_NODE_TYPES.CallExpression &&
50
+ node.value.callee.type === AST_NODE_TYPES.Identifier &&
51
+ node.value.callee.name === "inject" &&
52
+ node.key.type === AST_NODE_TYPES.Identifier);
53
+ for (const field of injectFields) {
54
+ const fieldName = field.key.name;
55
+ const allIdentifiers = [];
56
+ traverseNode(classBody, (node) => {
57
+ if (node.type === AST_NODE_TYPES.Identifier && node.name === fieldName) {
58
+ allIdentifiers.push(node);
59
+ }
60
+ });
61
+ const references = allIdentifiers.filter((id) => id !== field.key);
62
+ if (references.length === 0) {
63
+ context.report({
64
+ node: field,
65
+ messageId: "unusedInject",
66
+ data: { name: fieldName },
67
+ fix(fixer) {
68
+ const tokenBefore = sourceCode.getTokenBefore(field);
69
+ const tokenAfter = sourceCode.getTokenAfter(field);
70
+ const start = tokenBefore ? tokenBefore.range[1] : field.range[0];
71
+ const end = tokenAfter ? field.range[1] : field.range[1];
72
+ return fixer.removeRange([start, end]);
73
+ },
74
+ });
75
+ }
76
+ }
77
+ },
78
+ };
79
+ },
80
+ });
81
+ //# sourceMappingURL=ts-no-unused-injects.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ts-no-unused-injects.js","sourceRoot":"","sources":["../../src/rules/ts-no-unused-injects.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Angular `@Component` 내 미사용 `protected readonly` 필드를 감지하는 ESLint 규칙.
3
+ *
4
+ * @remarks
5
+ * `@Component` 데코레이터가 있는 클래스에서 `protected readonly` 필드가
6
+ * 인라인 템플릿과 클래스 본문 어디에서도 참조되지 않으면 보고합니다.
7
+ * autofix로 해당 필드를 제거합니다.
8
+ */
9
+ declare const _default: import("@typescript-eslint/utils/ts-eslint").RuleModule<"unusedField", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
10
+ name: string;
11
+ };
12
+ export default _default;
13
+ //# sourceMappingURL=ts-no-unused-protected-readonly.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ts-no-unused-protected-readonly.d.ts","sourceRoot":"","sources":["../../src/rules/ts-no-unused-protected-readonly.ts"],"names":[],"mappings":"AA2BA;;;;;;;GAOG;;;;AACH,wBAmHG"}
@@ -0,0 +1,127 @@
1
+ import { AST_NODE_TYPES } from "@typescript-eslint/utils";
2
+ import { createRule } from "../utils/create-rule.js";
3
+ function traverseNode(node, callback) {
4
+ callback(node);
5
+ for (const key of Object.keys(node)) {
6
+ if (key === "parent" || key === "range" || key === "loc")
7
+ continue;
8
+ const child = node[key];
9
+ if (Array.isArray(child)) {
10
+ for (const c of child) {
11
+ if (c != null && typeof c === "object" && "type" in c) {
12
+ traverseNode(c, callback);
13
+ }
14
+ }
15
+ }
16
+ else if (child != null && typeof child === "object" && "type" in child) {
17
+ traverseNode(child, callback);
18
+ }
19
+ }
20
+ }
21
+ function escapeRegExp(string) {
22
+ return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
23
+ }
24
+ /**
25
+ * Angular `@Component` 내 미사용 `protected readonly` 필드를 감지하는 ESLint 규칙.
26
+ *
27
+ * @remarks
28
+ * `@Component` 데코레이터가 있는 클래스에서 `protected readonly` 필드가
29
+ * 인라인 템플릿과 클래스 본문 어디에서도 참조되지 않으면 보고합니다.
30
+ * autofix로 해당 필드를 제거합니다.
31
+ */
32
+ export default createRule({
33
+ name: "ts-no-unused-protected-readonly",
34
+ meta: {
35
+ type: "problem",
36
+ docs: {
37
+ description: "Disallow unused protected readonly fields in Angular components",
38
+ },
39
+ fixable: "code",
40
+ messages: {
41
+ unusedField: 'Protected readonly field "{{name}}" is not used in class or template.',
42
+ },
43
+ schema: [],
44
+ },
45
+ defaultOptions: [],
46
+ create(context) {
47
+ const sourceCode = context.sourceCode;
48
+ return {
49
+ "ClassDeclaration, ClassExpression"(classNode) {
50
+ const componentDecorator = classNode.decorators.find((d) => {
51
+ if (d.expression.type === AST_NODE_TYPES.CallExpression) {
52
+ const callee = d.expression.callee;
53
+ return callee.type === AST_NODE_TYPES.Identifier && callee.name === "Component";
54
+ }
55
+ return false;
56
+ });
57
+ if (componentDecorator == null)
58
+ return;
59
+ const expr = componentDecorator.expression;
60
+ const args = expr.arguments;
61
+ const firstArg = args.at(0);
62
+ if (firstArg == null || firstArg.type !== AST_NODE_TYPES.ObjectExpression)
63
+ return;
64
+ const templateProp = firstArg.properties.find((p) => p.type === AST_NODE_TYPES.Property &&
65
+ p.key.type === AST_NODE_TYPES.Identifier &&
66
+ p.key.name === "template");
67
+ if (templateProp == null)
68
+ return;
69
+ let templateText = "";
70
+ const templateValue = templateProp.value;
71
+ if (templateValue.type === AST_NODE_TYPES.TemplateLiteral) {
72
+ templateText = templateValue.quasis.map((q) => q.value.raw).join("");
73
+ }
74
+ else if (templateValue.type === AST_NODE_TYPES.Literal &&
75
+ typeof templateValue.value === "string") {
76
+ templateText = templateValue.value;
77
+ }
78
+ if (templateText === "")
79
+ return;
80
+ const protectedReadonlyFields = classNode.body.body.filter((node) => node.type === AST_NODE_TYPES.PropertyDefinition &&
81
+ node.accessibility === "protected" &&
82
+ node.readonly === true &&
83
+ !node.static &&
84
+ node.key.type === AST_NODE_TYPES.Identifier);
85
+ for (const field of protectedReadonlyFields) {
86
+ const fieldName = field.key.name;
87
+ const identifierPattern = new RegExp(`(?<![a-zA-Z0-9_$])${escapeRegExp(fieldName)}(?![a-zA-Z0-9_$])`);
88
+ const usedInTemplate = identifierPattern.test(templateText);
89
+ const usedInClass = classNode.body.body.some((member) => {
90
+ if (member === field)
91
+ return false;
92
+ let found = false;
93
+ traverseNode(member, (node) => {
94
+ if (node.type === AST_NODE_TYPES.Identifier && node.name === fieldName) {
95
+ found = true;
96
+ }
97
+ });
98
+ return found;
99
+ });
100
+ if (!usedInTemplate && !usedInClass) {
101
+ context.report({
102
+ node: field,
103
+ messageId: "unusedField",
104
+ data: { name: fieldName },
105
+ fix(fixer) {
106
+ let start = field.range[0];
107
+ let end = field.range[1];
108
+ const textBefore = sourceCode.text.slice(0, start);
109
+ const leadingMatch = textBefore.match(/\n[ \t]*$/);
110
+ if (leadingMatch) {
111
+ start -= leadingMatch[0].length - 1;
112
+ }
113
+ const afterText = sourceCode.text.slice(end);
114
+ const trailingMatch = afterText.match(/^;?[ \t]*\r?\n/);
115
+ if (trailingMatch) {
116
+ end += trailingMatch[0].length;
117
+ }
118
+ return fixer.removeRange([start, end]);
119
+ },
120
+ });
121
+ }
122
+ }
123
+ },
124
+ };
125
+ },
126
+ });
127
+ //# sourceMappingURL=ts-no-unused-protected-readonly.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ts-no-unused-protected-readonly.js","sourceRoot":"","sources":["../../src/rules/ts-no-unused-protected-readonly.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simplysm/lint",
3
- "version": "14.0.1",
3
+ "version": "14.0.4",
4
4
  "description": "심플리즘 패키지 - ESLint plugin",
5
5
  "author": "심플리즘",
6
6
  "license": "Apache-2.0",
@@ -26,17 +26,19 @@
26
26
  }
27
27
  },
28
28
  "dependencies": {
29
+ "@angular-eslint/utils": "^21.3.1",
29
30
  "@typescript-eslint/utils": "^8.57.2",
30
31
  "angular-eslint": "^21.3.1",
31
32
  "eslint": "^9.39.4",
32
33
  "eslint-import-resolver-typescript": "^4.4.4",
33
34
  "eslint-plugin-import": "^2.32.0",
34
35
  "eslint-plugin-unused-imports": "^4.4.1",
36
+ "globals": "^17.4.0",
35
37
  "typescript": "^5.9.3",
36
38
  "typescript-eslint": "^8.57.2"
37
39
  },
38
40
  "devDependencies": {
39
- "@typescript-eslint/rule-tester": "^8.57.2",
40
- "@types/node": "^20.19.37"
41
+ "@types/node": "^20.19.37",
42
+ "@typescript-eslint/rule-tester": "^8.57.2"
41
43
  }
42
44
  }
@@ -1,11 +1,19 @@
1
+ import ngTemplateNoTodoComments from "./rules/ng-template-no-todo-comments";
2
+ import ngTemplateSdRequireBindingAttrs from "./rules/ng-template-sd-require-binding-attrs";
1
3
  import noHardPrivate from "./rules/no-hard-private";
2
4
  import noSubpathImportsFromSimplysm from "./rules/no-subpath-imports-from-simplysm";
3
5
  import tsNoThrowNotImplementedError from "./rules/ts-no-throw-not-implemented-error";
6
+ import tsNoUnusedInjects from "./rules/ts-no-unused-injects";
7
+ import tsNoUnusedProtectedReadonly from "./rules/ts-no-unused-protected-readonly";
4
8
 
5
9
  export default {
6
10
  rules: {
11
+ "ng-template-no-todo-comments": ngTemplateNoTodoComments,
12
+ "ng-template-sd-require-binding-attrs": ngTemplateSdRequireBindingAttrs,
7
13
  "no-hard-private": noHardPrivate,
8
14
  "no-subpath-imports-from-simplysm": noSubpathImportsFromSimplysm,
9
15
  "ts-no-throw-not-implemented-error": tsNoThrowNotImplementedError,
16
+ "ts-no-unused-injects": tsNoUnusedInjects,
17
+ "ts-no-unused-protected-readonly": tsNoUnusedProtectedReadonly,
10
18
  },
11
19
  };
@@ -1,5 +1,6 @@
1
1
  import tseslint, { type FlatConfig } from "typescript-eslint";
2
2
  import angular from "angular-eslint";
3
+ import globals from "globals";
3
4
  import plugin from "./eslint-plugin";
4
5
  import importPlugin from "eslint-plugin-import";
5
6
  import unusedImportsPlugin from "eslint-plugin-unused-imports";
@@ -7,7 +8,6 @@ import { ESLint } from "eslint";
7
8
  import { fileURLToPath } from "url";
8
9
 
9
10
  const commonRules: FlatConfig.Rules = {
10
- "no-console": "error",
11
11
  "no-warning-comments": "warn",
12
12
  "eqeqeq": ["error", "always", { null: "ignore" }],
13
13
  "no-self-compare": "error",
@@ -69,12 +69,15 @@ export default tseslint.config(
69
69
  },
70
70
  {
71
71
  languageOptions: {
72
- ecmaVersion: 2024,
72
+ ecmaVersion: "latest",
73
73
  sourceType: "module",
74
74
  },
75
75
  },
76
76
  {
77
77
  files: ["**/*.js", "**/*.mjs", "**/*.cjs"],
78
+ languageOptions: {
79
+ globals: globals.node,
80
+ },
78
81
  plugins: {
79
82
  "import": importPlugin,
80
83
  "@simplysm": plugin as unknown as ESLint.Plugin,
@@ -135,6 +138,7 @@ export default tseslint.config(
135
138
  },
136
139
  rules: {
137
140
  ...commonRules,
141
+ "no-console": "error",
138
142
 
139
143
  "@typescript-eslint/require-await": "error",
140
144
  "@typescript-eslint/await-thenable": "error",
@@ -176,6 +180,9 @@ export default tseslint.config(
176
180
  "@simplysm/no-hard-private": "error",
177
181
  "@simplysm/no-subpath-imports-from-simplysm": "error",
178
182
  "@simplysm/ts-no-throw-not-implemented-error": "warn",
183
+ "@simplysm/ts-no-unused-injects": "error",
184
+ "@simplysm/ts-no-unused-protected-readonly": "error",
185
+ "@angular-eslint/no-output-native": "off",
179
186
 
180
187
  ...unusedImportsRules,
181
188
  ...noNodeBuiltinsRules,
@@ -186,6 +193,13 @@ export default tseslint.config(
186
193
  {
187
194
  files: ["**/*.html"],
188
195
  extends: [...angular.configs.templateRecommended, ...angular.configs.templateAccessibility],
196
+ plugins: {
197
+ "@simplysm": plugin as unknown as ESLint.Plugin,
198
+ },
199
+ rules: {
200
+ "@simplysm/ng-template-no-todo-comments": "warn",
201
+ "@simplysm/ng-template-sd-require-binding-attrs": "error",
202
+ },
189
203
  },
190
204
  {
191
205
  files: ["**/tests/**/*.ts"],
@@ -0,0 +1,48 @@
1
+ import { createRule } from "../utils/create-rule";
2
+
3
+ /**
4
+ * HTML 템플릿 내 TODO 주석을 감지하여 경고하는 ESLint 규칙.
5
+ *
6
+ * @remarks
7
+ * `<!-- TODO: ... -->` 형태의 HTML 주석을 찾아 보고합니다.
8
+ * raw text regex 방식으로 동작하므로 AST 노드 방문자가 아닌 빈 객체를 반환합니다.
9
+ */
10
+ export default createRule({
11
+ name: "ng-template-no-todo-comments",
12
+ meta: {
13
+ type: "problem",
14
+ docs: {
15
+ description: "HTML 템플릿 내 TODO 주석을 경고합니다.",
16
+ },
17
+ schema: [],
18
+ messages: {
19
+ noTodo: "{{content}}",
20
+ },
21
+ },
22
+ defaultOptions: [],
23
+ create(context) {
24
+ const sourceCode = context.sourceCode;
25
+ const source = sourceCode.getText();
26
+ const commentRegex = /<!--([\s\S]*?)-->/g;
27
+ let match;
28
+ while ((match = commentRegex.exec(source)) !== null) {
29
+ const commentContent = match[1];
30
+ const todoIndex = commentContent.indexOf("TODO:");
31
+ if (todoIndex < 0) continue;
32
+
33
+ const start = match.index;
34
+ const end = start + match[0].length;
35
+ const content = commentContent.slice(todoIndex + 5).trim();
36
+ const loc = sourceCode.getLocFromIndex(start);
37
+ const endLoc = sourceCode.getLocFromIndex(end);
38
+
39
+ context.report({
40
+ loc: { start: loc, end: endLoc },
41
+ messageId: "noTodo",
42
+ data: { content },
43
+ });
44
+ }
45
+
46
+ return {};
47
+ },
48
+ });
@@ -0,0 +1,111 @@
1
+ import { getTemplateParserServices } from "@angular-eslint/utils";
2
+ import { createRule } from "../utils/create-rule";
3
+
4
+ export interface RuleOptions {
5
+ selectorPrefixes?: string[];
6
+ allowAttributes?: string[];
7
+ allowAttributePrefixes?: string[];
8
+ }
9
+
10
+ const DEFAULT_OPTIONS: Required<RuleOptions> = {
11
+ selectorPrefixes: ["sd-"],
12
+ allowAttributes: ["id", "class", "style", "title", "tabindex", "role"],
13
+ allowAttributePrefixes: ["aria-", "data-", "sd-"],
14
+ };
15
+
16
+ /**
17
+ * sd-* 컴포넌트에서 plain attribute 사용을 제한하고 Angular property binding을 강제하는 ESLint 규칙.
18
+ *
19
+ * @remarks
20
+ * `sd-` 접두사를 가진 커스텀 컴포넌트에서 허용되지 않은 plain attribute를 감지합니다.
21
+ * 허용 목록(id, class, style, title, tabindex, role)과 허용 접두사(aria-, data-, sd-)에
22
+ * 해당하지 않는 attribute는 `[attr]="..."` 형태의 property binding으로 변환하도록 autofix합니다.
23
+ */
24
+ export default createRule({
25
+ name: "ng-template-sd-require-binding-attrs",
26
+ meta: {
27
+ type: "problem",
28
+ docs: {
29
+ description:
30
+ 'Disallow non-whitelisted plain attributes on prefixed components (e.g. sd-*) and require using Angular property bindings instead.',
31
+ },
32
+ fixable: "code",
33
+ schema: [
34
+ {
35
+ type: "object",
36
+ properties: {
37
+ selectorPrefixes: { type: "array", items: { type: "string" } },
38
+ allowAttributes: { type: "array", items: { type: "string" } },
39
+ allowAttributePrefixes: { type: "array", items: { type: "string" } },
40
+ },
41
+ additionalProperties: false,
42
+ },
43
+ ],
44
+ messages: {
45
+ requireBindingForAttribute:
46
+ 'Attribute "{{attrName}}" is not allowed as a plain attribute on "{{elementName}}". Use a property binding instead, e.g. [{{attrName}}]="…".',
47
+ },
48
+ },
49
+ defaultOptions: [{}] as [RuleOptions],
50
+ create(context) {
51
+ const parserServices = getTemplateParserServices(context as never);
52
+ const userOptions = context.options.at(0) ?? {};
53
+ const selectorPrefixes = userOptions.selectorPrefixes ?? DEFAULT_OPTIONS.selectorPrefixes;
54
+ const allowAttributes = userOptions.allowAttributes ?? DEFAULT_OPTIONS.allowAttributes;
55
+ const allowAttributePrefixes =
56
+ userOptions.allowAttributePrefixes ?? DEFAULT_OPTIONS.allowAttributePrefixes;
57
+
58
+ const allowedAttrSet = new Set(allowAttributes.map((attr) => attr.toLowerCase()));
59
+
60
+ function isTargetElement(node: { name: string }): boolean {
61
+ const tagName = node.name.toLowerCase();
62
+ return selectorPrefixes.some((prefix) => tagName.startsWith(prefix.toLowerCase()));
63
+ }
64
+
65
+ function isWhitelistedPlainAttr(attr: { name: string }): boolean {
66
+ const name = attr.name.toLowerCase();
67
+ if (allowedAttrSet.has(name)) return true;
68
+ return allowAttributePrefixes.some((prefix) => name.startsWith(prefix.toLowerCase()));
69
+ }
70
+
71
+ return {
72
+ Element(node: {
73
+ name: string;
74
+ attributes: Array<{
75
+ name: string;
76
+ value: string;
77
+ sourceSpan: { start: { offset: number }; end: { offset: number } };
78
+ }>;
79
+ sourceSpan: { start: { offset: number }; end: { offset: number } };
80
+ }) {
81
+ if (!isTargetElement(node)) return;
82
+
83
+ for (const attr of node.attributes) {
84
+ if (isWhitelistedPlainAttr(attr)) continue;
85
+
86
+ const span = attr.sourceSpan;
87
+ const loc = parserServices.convertNodeSourceSpanToLoc(span as never);
88
+
89
+ context.report({
90
+ loc,
91
+ messageId: "requireBindingForAttribute",
92
+ data: { attrName: attr.name, elementName: node.name },
93
+ fix(fixer) {
94
+ const start = span.start.offset;
95
+ const end = span.end.offset;
96
+ if (start >= end) return null;
97
+
98
+ const rawValue = attr.value;
99
+ if (rawValue === "") {
100
+ return fixer.replaceTextRange([start, end], `[${attr.name}]="true"`);
101
+ }
102
+
103
+ const escaped = rawValue.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
104
+ return fixer.replaceTextRange([start, end], `[${attr.name}]="'${escaped}'"`);
105
+ },
106
+ });
107
+ }
108
+ },
109
+ } as never;
110
+ },
111
+ });
@@ -0,0 +1,88 @@
1
+ import { AST_NODE_TYPES, type TSESTree } from "@typescript-eslint/utils";
2
+ import { createRule } from "../utils/create-rule";
3
+
4
+ function traverseNode(node: TSESTree.Node, callback: (n: TSESTree.Node) => void): void {
5
+ callback(node);
6
+ for (const key of Object.keys(node)) {
7
+ if (key === "parent") continue;
8
+ const child = (node as unknown as Record<string, unknown>)[key];
9
+ if (Array.isArray(child)) {
10
+ for (const c of child) {
11
+ if (c != null && typeof c === "object" && "type" in c) {
12
+ traverseNode(c as TSESTree.Node, callback);
13
+ }
14
+ }
15
+ } else if (child != null && typeof child === "object" && "type" in child) {
16
+ traverseNode(child as TSESTree.Node, callback);
17
+ }
18
+ }
19
+ }
20
+
21
+ /**
22
+ * 미사용 Angular `inject()` 필드를 감지하는 ESLint 규칙.
23
+ *
24
+ * @remarks
25
+ * 클래스 내에서 `inject()` 호출로 초기화된 프로퍼티 중
26
+ * 같은 클래스 내 다른 곳에서 참조되지 않는 필드를 보고합니다.
27
+ * autofix로 해당 필드를 제거합니다.
28
+ */
29
+ export default createRule({
30
+ name: "ts-no-unused-injects",
31
+ meta: {
32
+ type: "problem",
33
+ docs: {
34
+ description: "Disallow unused Angular inject() fields",
35
+ },
36
+ fixable: "code",
37
+ messages: {
38
+ unusedInject: 'inject() field "{{name}}" is never used.',
39
+ },
40
+ schema: [],
41
+ },
42
+ defaultOptions: [],
43
+ create(context) {
44
+ const sourceCode = context.sourceCode;
45
+
46
+ return {
47
+ ClassBody(classBody: TSESTree.ClassBody) {
48
+ const injectFields = classBody.body.filter(
49
+ (node): node is TSESTree.PropertyDefinition =>
50
+ node.type === AST_NODE_TYPES.PropertyDefinition &&
51
+ node.value != null &&
52
+ node.value.type === AST_NODE_TYPES.CallExpression &&
53
+ node.value.callee.type === AST_NODE_TYPES.Identifier &&
54
+ node.value.callee.name === "inject" &&
55
+ node.key.type === AST_NODE_TYPES.Identifier,
56
+ );
57
+
58
+ for (const field of injectFields) {
59
+ const fieldName = (field.key as TSESTree.Identifier).name;
60
+
61
+ const allIdentifiers: TSESTree.Identifier[] = [];
62
+ traverseNode(classBody, (node) => {
63
+ if (node.type === AST_NODE_TYPES.Identifier && node.name === fieldName) {
64
+ allIdentifiers.push(node);
65
+ }
66
+ });
67
+
68
+ const references = allIdentifiers.filter((id) => id !== field.key);
69
+
70
+ if (references.length === 0) {
71
+ context.report({
72
+ node: field,
73
+ messageId: "unusedInject",
74
+ data: { name: fieldName },
75
+ fix(fixer) {
76
+ const tokenBefore = sourceCode.getTokenBefore(field);
77
+ const tokenAfter = sourceCode.getTokenAfter(field);
78
+ const start = tokenBefore ? tokenBefore.range[1] : field.range[0];
79
+ const end = tokenAfter ? field.range[1] : field.range[1];
80
+ return fixer.removeRange([start, end]);
81
+ },
82
+ });
83
+ }
84
+ }
85
+ },
86
+ };
87
+ },
88
+ });
@@ -0,0 +1,151 @@
1
+ import { AST_NODE_TYPES, type TSESTree } from "@typescript-eslint/utils";
2
+ import { createRule } from "../utils/create-rule";
3
+
4
+ function traverseNode(
5
+ node: TSESTree.Node,
6
+ callback: (n: TSESTree.Node) => void,
7
+ ): void {
8
+ callback(node);
9
+ for (const key of Object.keys(node)) {
10
+ if (key === "parent" || key === "range" || key === "loc") continue;
11
+ const child = (node as unknown as Record<string, unknown>)[key];
12
+ if (Array.isArray(child)) {
13
+ for (const c of child) {
14
+ if (c != null && typeof c === "object" && "type" in c) {
15
+ traverseNode(c as TSESTree.Node, callback);
16
+ }
17
+ }
18
+ } else if (child != null && typeof child === "object" && "type" in child) {
19
+ traverseNode(child as TSESTree.Node, callback);
20
+ }
21
+ }
22
+ }
23
+
24
+ function escapeRegExp(string: string): string {
25
+ return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
26
+ }
27
+
28
+ /**
29
+ * Angular `@Component` 내 미사용 `protected readonly` 필드를 감지하는 ESLint 규칙.
30
+ *
31
+ * @remarks
32
+ * `@Component` 데코레이터가 있는 클래스에서 `protected readonly` 필드가
33
+ * 인라인 템플릿과 클래스 본문 어디에서도 참조되지 않으면 보고합니다.
34
+ * autofix로 해당 필드를 제거합니다.
35
+ */
36
+ export default createRule({
37
+ name: "ts-no-unused-protected-readonly",
38
+ meta: {
39
+ type: "problem",
40
+ docs: {
41
+ description: "Disallow unused protected readonly fields in Angular components",
42
+ },
43
+ fixable: "code",
44
+ messages: {
45
+ unusedField: 'Protected readonly field "{{name}}" is not used in class or template.',
46
+ },
47
+ schema: [],
48
+ },
49
+ defaultOptions: [],
50
+ create(context) {
51
+ const sourceCode = context.sourceCode;
52
+
53
+ return {
54
+ "ClassDeclaration, ClassExpression"(
55
+ classNode: TSESTree.ClassDeclaration | TSESTree.ClassExpression,
56
+ ) {
57
+ const componentDecorator = classNode.decorators.find((d) => {
58
+ if (d.expression.type === AST_NODE_TYPES.CallExpression) {
59
+ const callee = d.expression.callee;
60
+ return callee.type === AST_NODE_TYPES.Identifier && callee.name === "Component";
61
+ }
62
+ return false;
63
+ });
64
+
65
+ if (componentDecorator == null) return;
66
+
67
+ const expr = componentDecorator.expression as TSESTree.CallExpression;
68
+ const args = expr.arguments;
69
+ const firstArg = args.at(0);
70
+ if (firstArg == null || firstArg.type !== AST_NODE_TYPES.ObjectExpression) return;
71
+
72
+ const templateProp = firstArg.properties.find(
73
+ (p): p is TSESTree.Property =>
74
+ p.type === AST_NODE_TYPES.Property &&
75
+ p.key.type === AST_NODE_TYPES.Identifier &&
76
+ p.key.name === "template",
77
+ );
78
+
79
+ if (templateProp == null) return;
80
+
81
+ let templateText = "";
82
+ const templateValue = templateProp.value;
83
+ if (templateValue.type === AST_NODE_TYPES.TemplateLiteral) {
84
+ templateText = templateValue.quasis.map((q) => q.value.raw).join("");
85
+ } else if (
86
+ templateValue.type === AST_NODE_TYPES.Literal &&
87
+ typeof templateValue.value === "string"
88
+ ) {
89
+ templateText = templateValue.value;
90
+ }
91
+
92
+ if (templateText === "") return;
93
+
94
+ const protectedReadonlyFields = classNode.body.body.filter(
95
+ (node): node is TSESTree.PropertyDefinition =>
96
+ node.type === AST_NODE_TYPES.PropertyDefinition &&
97
+ node.accessibility === "protected" &&
98
+ node.readonly === true &&
99
+ !node.static &&
100
+ node.key.type === AST_NODE_TYPES.Identifier,
101
+ );
102
+
103
+ for (const field of protectedReadonlyFields) {
104
+ const fieldName = (field.key as TSESTree.Identifier).name;
105
+
106
+ const identifierPattern = new RegExp(
107
+ `(?<![a-zA-Z0-9_$])${escapeRegExp(fieldName)}(?![a-zA-Z0-9_$])`,
108
+ );
109
+ const usedInTemplate = identifierPattern.test(templateText);
110
+
111
+ const usedInClass = classNode.body.body.some((member) => {
112
+ if (member === field) return false;
113
+ let found = false;
114
+ traverseNode(member, (node) => {
115
+ if (node.type === AST_NODE_TYPES.Identifier && node.name === fieldName) {
116
+ found = true;
117
+ }
118
+ });
119
+ return found;
120
+ });
121
+
122
+ if (!usedInTemplate && !usedInClass) {
123
+ context.report({
124
+ node: field,
125
+ messageId: "unusedField",
126
+ data: { name: fieldName },
127
+ fix(fixer) {
128
+ let start = field.range[0];
129
+ let end = field.range[1];
130
+
131
+ const textBefore = sourceCode.text.slice(0, start);
132
+ const leadingMatch = textBefore.match(/\n[ \t]*$/);
133
+ if (leadingMatch) {
134
+ start -= leadingMatch[0].length - 1;
135
+ }
136
+
137
+ const afterText = sourceCode.text.slice(end);
138
+ const trailingMatch = afterText.match(/^;?[ \t]*\r?\n/);
139
+ if (trailingMatch) {
140
+ end += trailingMatch[0].length;
141
+ }
142
+
143
+ return fixer.removeRange([start, end]);
144
+ },
145
+ });
146
+ }
147
+ }
148
+ },
149
+ };
150
+ },
151
+ });