@simplysm/eslint-plugin 12.15.69 → 12.15.71

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.69",
3
+ "version": "12.15.71",
4
4
  "description": "심플리즘 패키지 - ESLINT 플러그인",
5
5
  "author": "김석래",
6
6
  "repository": {
@@ -14,29 +14,35 @@ export default {
14
14
  const sourceCode = context.sourceCode;
15
15
 
16
16
  return {
17
- ClassDeclaration(classNode) {
17
+ "ClassDeclaration, ClassExpression"(classNode) {
18
18
  // @Component 데코레이터 찾기
19
- const componentDecorator = classNode.decorators?.find(
20
- (d) =>
21
- d.expression?.type === "CallExpression" && d.expression.callee?.name === "Component",
22
- );
19
+ const componentDecorator = classNode.decorators?.find((d) => {
20
+ if (d.expression?.type === "CallExpression") {
21
+ const callee = d.expression.callee;
22
+ return callee?.type === "Identifier" && callee?.name === "Component";
23
+ }
24
+ return false;
25
+ });
23
26
 
24
27
  if (!componentDecorator) return;
25
28
 
26
29
  // template 문자열 추출
27
- const decoratorArg = componentDecorator.expression.arguments?.[0];
28
- if (decoratorArg?.type !== "ObjectExpression") return;
30
+ const args = componentDecorator.expression.arguments;
31
+ if (!args?.[0] || args[0].type !== "ObjectExpression") return;
32
+
33
+ const templateProp = args[0].properties.find(
34
+ (p) => p.type === "Property" && p.key?.type === "Identifier" && p.key?.name === "template"
35
+ );
29
36
 
30
- const templateProp = decoratorArg.properties.find((p) => p.key?.name === "template");
31
37
  if (!templateProp) return;
32
38
 
33
- const templateValue = templateProp.value;
34
39
  let templateText = "";
40
+ const templateValue = templateProp.value;
35
41
 
36
42
  if (templateValue.type === "TemplateLiteral") {
37
43
  templateText = templateValue.quasis.map((q) => q.value.raw).join("");
38
- } else if (templateValue.type === "Literal") {
39
- templateText = String(templateValue.value);
44
+ } else if (templateValue.type === "Literal" && typeof templateValue.value === "string") {
45
+ templateText = templateValue.value;
40
46
  }
41
47
 
42
48
  if (!templateText) return;
@@ -48,25 +54,33 @@ export default {
48
54
  node.accessibility === "protected" &&
49
55
  node.readonly === true &&
50
56
  !node.static &&
51
- node.key?.type === "Identifier",
57
+ node.key?.type === "Identifier"
52
58
  );
53
59
 
54
60
  for (const field of protectedReadonlyFields) {
55
61
  const fieldName = field.key.name;
56
62
 
57
- // 템플릿에서 사용 여부 (바인딩 컨텍스트)
58
- const usedInTemplate =
59
- templateText.includes(fieldName) && new RegExp(`\\b${fieldName}\\b`).test(templateText);
63
+ // 템플릿에서 사용 여부
64
+ // \b는 $로 시작하는 identifier에서 동작 안 함 → lookahead/lookbehind 사용
65
+ const identifierPattern = new RegExp(
66
+ `(?<![a-zA-Z0-9_$])${escapeRegExp(fieldName)}(?![a-zA-Z0-9_$])`
67
+ );
68
+ const usedInTemplate = identifierPattern.test(templateText);
69
+
70
+ // 클래스 내 다른 곳에서 참조 여부 (해당 필드 선언부 제외)
71
+ let usedInClass = false;
72
+ for (const member of classNode.body.body) {
73
+ // 자기 자신 필드 선언은 스킵
74
+ if (member === field) continue;
60
75
 
61
- // 클래스 다른 곳에서 참조 여부
62
- const allIdentifiers = [];
63
- traverseNode(classNode.body, (node) => {
64
- if (node.type === "Identifier" && node.name === fieldName) {
65
- allIdentifiers.push(node);
66
- }
67
- });
76
+ traverseNode(member, (node) => {
77
+ if (node.type === "Identifier" && node.name === fieldName) {
78
+ usedInClass = true;
79
+ }
80
+ });
68
81
 
69
- const usedInClass = allIdentifiers.some((id) => id !== field.key);
82
+ if (usedInClass) break;
83
+ }
70
84
 
71
85
  if (!usedInTemplate && !usedInClass) {
72
86
  context.report({
@@ -74,9 +88,24 @@ export default {
74
88
  messageId: "unusedField",
75
89
  data: { name: fieldName },
76
90
  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]]);
91
+ let start = field.range[0];
92
+ let end = field.range[1];
93
+
94
+ // 앞쪽 공백/줄바꿈 포함
95
+ const textBefore = sourceCode.text.slice(0, start);
96
+ const leadingMatch = textBefore.match(/\n[ \t]*$/);
97
+ if (leadingMatch) {
98
+ start -= leadingMatch[0].length - 1;
99
+ }
100
+
101
+ // 뒤쪽 세미콜론과 줄바꿈까지 포함
102
+ const afterText = sourceCode.text.slice(end);
103
+ const trailingMatch = afterText.match(/^;?[ \t]*\r?\n/);
104
+ if (trailingMatch) {
105
+ end += trailingMatch[0].length;
106
+ }
107
+
108
+ return fixer.removeRange([start, end]);
80
109
  },
81
110
  });
82
111
  }
@@ -90,7 +119,7 @@ function traverseNode(node, callback) {
90
119
  if (!node || typeof node !== "object") return;
91
120
  callback(node);
92
121
  for (const key of Object.keys(node)) {
93
- if (key === "parent") continue;
122
+ if (key === "parent" || key === "range" || key === "loc") continue;
94
123
  const child = node[key];
95
124
  if (Array.isArray(child)) {
96
125
  child.forEach((c) => traverseNode(c, callback));
@@ -99,3 +128,7 @@ function traverseNode(node, callback) {
99
128
  }
100
129
  }
101
130
  }
131
+
132
+ function escapeRegExp(string) {
133
+ return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
134
+ }