@simplysm/eslint-plugin 12.15.68 → 12.15.70

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simplysm/eslint-plugin",
3
- "version": "12.15.68",
3
+ "version": "12.15.70",
4
4
  "description": "심플리즘 패키지 - ESLINT 플러그인",
5
5
  "author": "김석래",
6
6
  "repository": {
@@ -19,6 +19,7 @@
19
19
  "eslint": "^9.39.1",
20
20
  "eslint-import-resolver-typescript": "^4.4.4",
21
21
  "eslint-plugin-import": "^2.32.0",
22
+ "eslint-plugin-unused-imports": "^4.3.0",
22
23
  "globals": "^16.5.0",
23
24
  "typescript": "~5.8.3",
24
25
  "typescript-eslint": "^8.49.0"
@@ -3,6 +3,7 @@ import tseslint from "typescript-eslint";
3
3
  import plugin from "../plugin.js";
4
4
  import ngeslint from "angular-eslint";
5
5
  import importPlugin from "eslint-plugin-import";
6
+ import unusedImportsPlugin from "eslint-plugin-unused-imports";
6
7
 
7
8
  export default [
8
9
  {
@@ -24,6 +25,7 @@ export default [
24
25
  plugins: {
25
26
  "import": importPlugin,
26
27
  "@simplysm": plugin,
28
+ "unused-imports": unusedImportsPlugin,
27
29
  },
28
30
  rules: {
29
31
  // 기본
@@ -47,9 +49,21 @@ export default [
47
49
  "no-shadow": ["error"],
48
50
  "no-duplicate-imports": ["error"],
49
51
  "no-unused-expressions": ["error"],
50
- "no-unused-vars": ["error"],
52
+ // "no-unused-vars": ["error"],
51
53
  "no-undef": ["error"],
52
54
 
55
+ // unused
56
+ "unused-imports/no-unused-imports": "error",
57
+ "unused-imports/no-unused-vars": [
58
+ "error",
59
+ {
60
+ vars: "all",
61
+ varsIgnorePattern: "^_",
62
+ args: "after-used",
63
+ argsIgnorePattern: "^_",
64
+ },
65
+ ],
66
+
53
67
  // import
54
68
  "import/no-extraneous-dependencies": [
55
69
  "error",
@@ -76,6 +90,7 @@ export default [
76
90
  "@simplysm": plugin,
77
91
  "@angular-eslint": ngeslint.tsPlugin,
78
92
  "import": importPlugin,
93
+ "unused-imports": unusedImportsPlugin,
79
94
  },
80
95
  /*settings: {
81
96
  "import/resolver": {
@@ -126,7 +141,7 @@ export default [
126
141
  "@typescript-eslint/prefer-return-this-type": ["error"],
127
142
  "@typescript-eslint/typedef": ["error"],
128
143
  "@typescript-eslint/no-unused-expressions": ["error"],
129
- "@typescript-eslint/no-unused-vars": ["error", { args: "none" }],
144
+ // "@typescript-eslint/no-unused-vars": ["error", { args: "none" }],
130
145
  "@typescript-eslint/strict-boolean-expressions": [
131
146
  "error",
132
147
  {
@@ -218,6 +233,20 @@ export default [
218
233
  "@simplysm/ts-no-throw-not-implement-error": ["warn"],
219
234
  "@simplysm/no-subpath-imports-from-simplysm": ["error"],
220
235
  "@simplysm/no-hard-private": ["error"],
236
+ "@simplysm/ts-no-unused-injects": ["error"],
237
+ "@simplysm/ts-no-unused-protected-readonly": ["error"],
238
+
239
+ // unused
240
+ "unused-imports/no-unused-imports": ["error"],
241
+ "unused-imports/no-unused-vars": [
242
+ "error",
243
+ {
244
+ vars: "all",
245
+ varsIgnorePattern: "^_",
246
+ args: "after-used",
247
+ argsIgnorePattern: "^_",
248
+ },
249
+ ],
221
250
 
222
251
  // -- 아래 적용 검토가 필요한것
223
252
  "import/no-extraneous-dependencies": [
package/src/plugin.js CHANGED
@@ -5,6 +5,8 @@ import tsNoBufferInTypedArrayContext from "./rules/ts-no-buffer-in-typedarray-co
5
5
  import noSubpathImportsFromSimplysm from "./rules/no-subpath-imports-from-simplysm.js";
6
6
  import ngTemplateSdRequireBindingAttrs from "./rules/ng-template-sd-require-binding-attrs.js";
7
7
  import noHardPrivate from "./rules/no-hard-private.js";
8
+ import tsNoUnusedInjects from "./rules/ts-no-unused-injects.js";
9
+ import tsNoUnusedProtectedReadonly from "./rules/ts-no-unused-protected-readonly.js";
8
10
 
9
11
  export default {
10
12
  rules: {
@@ -15,5 +17,7 @@ export default {
15
17
  "no-subpath-imports-from-simplysm": noSubpathImportsFromSimplysm,
16
18
  "ng-template-sd-require-binding-attrs": ngTemplateSdRequireBindingAttrs,
17
19
  "no-hard-private": noHardPrivate,
20
+ "ts-no-unused-injects": tsNoUnusedInjects,
21
+ "ts-no-unused-protected-readonly": tsNoUnusedProtectedReadonly,
18
22
  },
19
23
  };
@@ -0,0 +1,81 @@
1
+ // packages/eslint-plugin/src/rules/ts-no-unused-injects.js
2
+
3
+ export default {
4
+ meta: {
5
+ type: "problem",
6
+ docs: {
7
+ description: "Disallow unused Angular inject() fields",
8
+ },
9
+ fixable: "code",
10
+ messages: {
11
+ unusedInject: 'inject() field "{{name}}" is never used.',
12
+ },
13
+ schema: [],
14
+ },
15
+ create(context) {
16
+ const sourceCode = context.sourceCode;
17
+
18
+ return {
19
+ ClassBody(classBody) {
20
+ // inject() 호출로 초기화된 필드 수집
21
+ const injectFields = classBody.body.filter(
22
+ (node) =>
23
+ node.type === "PropertyDefinition" &&
24
+ node.value?.type === "CallExpression" &&
25
+ node.value.callee?.name === "inject" &&
26
+ node.key?.type === "Identifier"
27
+ );
28
+
29
+ for (const field of injectFields) {
30
+ const fieldName = field.key.name;
31
+
32
+ // 클래스 내 모든 Identifier 중 해당 이름 참조 찾기
33
+ const allIdentifiers = [];
34
+ traverseNode(classBody, (node) => {
35
+ if (node.type === "Identifier" && node.name === fieldName) {
36
+ allIdentifiers.push(node);
37
+ }
38
+ });
39
+
40
+ // 선언부 자신을 제외한 참조가 있는지 확인
41
+ const references = allIdentifiers.filter((id) => id !== field.key);
42
+
43
+ if (references.length === 0) {
44
+ context.report({
45
+ node: field,
46
+ messageId: "unusedInject",
47
+ data: { name: fieldName },
48
+ fix(fixer) {
49
+ // 필드 전체 라인 삭제 (앞뒤 공백/개행 포함)
50
+ const tokenBefore = sourceCode.getTokenBefore(field);
51
+ const tokenAfter = sourceCode.getTokenAfter(field);
52
+
53
+ const start = tokenBefore ? tokenBefore.range[1] : field.range[0];
54
+ const end = tokenAfter ? field.range[1] : field.range[1];
55
+
56
+ return fixer.removeRange([start, end]);
57
+ },
58
+ });
59
+ }
60
+ }
61
+ },
62
+ };
63
+ },
64
+ };
65
+
66
+ // 간단한 AST 순회 헬퍼
67
+ function traverseNode(node, callback) {
68
+ if (!node || typeof node !== "object") return;
69
+
70
+ callback(node);
71
+
72
+ for (const key of Object.keys(node)) {
73
+ if (key === "parent") continue;
74
+ const child = node[key];
75
+ if (Array.isArray(child)) {
76
+ child.forEach((c) => traverseNode(c, callback));
77
+ } else if (child && typeof child === "object" && child.type) {
78
+ traverseNode(child, callback);
79
+ }
80
+ }
81
+ }
@@ -0,0 +1,101 @@
1
+ export default {
2
+ meta: {
3
+ type: "problem",
4
+ docs: {
5
+ description: "Disallow unused protected readonly fields in Angular components",
6
+ },
7
+ fixable: "code",
8
+ messages: {
9
+ unusedField: 'Protected readonly field "{{name}}" is not used in class or template.',
10
+ },
11
+ schema: [],
12
+ },
13
+ create(context) {
14
+ const sourceCode = context.sourceCode;
15
+
16
+ return {
17
+ ClassDeclaration(classNode) {
18
+ // @Component 데코레이터 찾기
19
+ const componentDecorator = classNode.decorators?.find(
20
+ (d) =>
21
+ d.expression?.type === "CallExpression" && d.expression.callee?.name === "Component",
22
+ );
23
+
24
+ if (!componentDecorator) return;
25
+
26
+ // template 문자열 추출
27
+ const decoratorArg = componentDecorator.expression.arguments?.[0];
28
+ if (decoratorArg?.type !== "ObjectExpression") return;
29
+
30
+ const templateProp = decoratorArg.properties.find((p) => p.key?.name === "template");
31
+ if (!templateProp) return;
32
+
33
+ const templateValue = templateProp.value;
34
+ let templateText = "";
35
+
36
+ if (templateValue.type === "TemplateLiteral") {
37
+ templateText = templateValue.quasis.map((q) => q.value.raw).join("");
38
+ } else if (templateValue.type === "Literal") {
39
+ templateText = String(templateValue.value);
40
+ }
41
+
42
+ if (!templateText) return;
43
+
44
+ // protected readonly 필드 수집
45
+ const protectedReadonlyFields = classNode.body.body.filter(
46
+ (node) =>
47
+ node.type === "PropertyDefinition" &&
48
+ node.accessibility === "protected" &&
49
+ node.readonly === true &&
50
+ !node.static &&
51
+ node.key?.type === "Identifier",
52
+ );
53
+
54
+ for (const field of protectedReadonlyFields) {
55
+ const fieldName = field.key.name;
56
+
57
+ // 템플릿에서 사용 여부 (바인딩 컨텍스트)
58
+ const usedInTemplate =
59
+ templateText.includes(fieldName) && new RegExp(`\\b${fieldName}\\b`).test(templateText);
60
+
61
+ // 클래스 내 다른 곳에서 참조 여부
62
+ const allIdentifiers = [];
63
+ traverseNode(classNode.body, (node) => {
64
+ if (node.type === "Identifier" && node.name === fieldName) {
65
+ allIdentifiers.push(node);
66
+ }
67
+ });
68
+
69
+ const usedInClass = allIdentifiers.some((id) => id !== field.key);
70
+
71
+ if (!usedInTemplate && !usedInClass) {
72
+ context.report({
73
+ node: field,
74
+ messageId: "unusedField",
75
+ data: { name: fieldName },
76
+ fix(fixer) {
77
+ const tokenBefore = sourceCode.getTokenBefore(field);
78
+ const start = tokenBefore ? tokenBefore.range[1] : field.range[0];
79
+ return fixer.removeRange([start, field.range[1]]);
80
+ },
81
+ });
82
+ }
83
+ }
84
+ },
85
+ };
86
+ },
87
+ };
88
+
89
+ function traverseNode(node, callback) {
90
+ if (!node || typeof node !== "object") return;
91
+ callback(node);
92
+ for (const key of Object.keys(node)) {
93
+ if (key === "parent") continue;
94
+ const child = node[key];
95
+ if (Array.isArray(child)) {
96
+ child.forEach((c) => traverseNode(c, callback));
97
+ } else if (child && typeof child === "object" && child.type) {
98
+ traverseNode(child, callback);
99
+ }
100
+ }
101
+ }