@simplysm/eslint-plugin 12.9.16 → 12.9.18
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 +4 -2
- package/src/configs/root.js +109 -12
- package/src/index.js +1 -5
- package/src/plugin.js +5 -1
- package/src/rules/ng-template-no-todo-comments.js +11 -17
- package/src/rules/ts-no-buffer-in-typedarray-context.js +165 -0
- package/src/rules/ts-no-exported-types.js +199 -0
- package/src/rules/ts-no-throw-not-implement-error.js +31 -19
- package/tests/ng-template-no-todo-comments.spec.js +54 -55
- package/tests/ts-no-buffer-in-typedarray-context.spec.js +213 -0
- package/tests/ts-no-exported-types.spec.js +307 -0
- package/vitest.config.js +9 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simplysm/eslint-plugin",
|
|
3
|
-
"version": "12.9.
|
|
3
|
+
"version": "12.9.18",
|
|
4
4
|
"description": "심플리즘 패키지 - ESLINT 플러그인",
|
|
5
5
|
"author": "김석래",
|
|
6
6
|
"repository": {
|
|
@@ -14,14 +14,16 @@
|
|
|
14
14
|
"dependencies": {
|
|
15
15
|
"@eslint/compat": "^1.2.9",
|
|
16
16
|
"@typescript-eslint/utils": "^8.32.0",
|
|
17
|
-
"angular-eslint": "^19.
|
|
17
|
+
"angular-eslint": "^19.4.0",
|
|
18
18
|
"eslint": "^9.26.0",
|
|
19
|
+
"eslint-import-resolver-typescript": "^4.3.4",
|
|
19
20
|
"eslint-plugin-import": "^2.31.0",
|
|
20
21
|
"globals": "^16.1.0",
|
|
21
22
|
"typescript": "~5.7.3",
|
|
22
23
|
"typescript-eslint": "^8.32.0"
|
|
23
24
|
},
|
|
24
25
|
"devDependencies": {
|
|
26
|
+
"@typescript-eslint/rule-tester": "^8.32.0",
|
|
25
27
|
"vitest": "^3.1.3"
|
|
26
28
|
}
|
|
27
29
|
}
|
package/src/configs/root.js
CHANGED
|
@@ -27,11 +27,15 @@ export default [
|
|
|
27
27
|
// 기본
|
|
28
28
|
"no-console": ["warn"],
|
|
29
29
|
"no-warning-comments": ["warn"],
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
"no-restricted-syntax": [
|
|
31
|
+
"error",
|
|
32
|
+
{
|
|
33
|
+
selector: "PropertyDefinition[key.type='PrivateIdentifier']",
|
|
34
|
+
message: "Do not use ECMAScript private fields (e.g. #myField); use TypeScript \"private\" instead.",
|
|
35
|
+
},
|
|
32
36
|
{
|
|
33
|
-
selector:
|
|
34
|
-
message:
|
|
37
|
+
selector: "MethodDefinition[key.type='PrivateIdentifier']",
|
|
38
|
+
message: "Do not use ECMAScript private methods (e.g. #myMethod); use TypeScript \"private\" instead.",
|
|
35
39
|
},
|
|
36
40
|
],
|
|
37
41
|
"eqeqeq": ["error", "always", { "null": "ignore" }],
|
|
@@ -56,6 +60,15 @@ export default [
|
|
|
56
60
|
"@angular-eslint": ngeslint.tsPlugin,
|
|
57
61
|
"import": importPlugin,
|
|
58
62
|
},
|
|
63
|
+
settings: {
|
|
64
|
+
"import/resolver": {
|
|
65
|
+
typescript: {
|
|
66
|
+
project: [
|
|
67
|
+
"./tsconfig.base.json",
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
},
|
|
59
72
|
processor: ngeslint.processInlineTemplates,
|
|
60
73
|
languageOptions: {
|
|
61
74
|
parser: tseslint.parser,
|
|
@@ -67,11 +80,15 @@ export default [
|
|
|
67
80
|
// 기본
|
|
68
81
|
"no-console": ["warn"],
|
|
69
82
|
"no-warning-comments": ["warn"],
|
|
70
|
-
|
|
71
|
-
|
|
83
|
+
"no-restricted-syntax": [
|
|
84
|
+
"error",
|
|
72
85
|
{
|
|
73
|
-
selector:
|
|
74
|
-
message:
|
|
86
|
+
selector: "PropertyDefinition[key.type='PrivateIdentifier']",
|
|
87
|
+
message: "Do not use ECMAScript private fields (e.g. #myField); use TypeScript \"private\" instead.",
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
selector: "MethodDefinition[key.type='PrivateIdentifier']",
|
|
91
|
+
message: "Do not use ECMAScript private methods (e.g. #myMethod); use TypeScript \"private\" instead.",
|
|
75
92
|
},
|
|
76
93
|
],
|
|
77
94
|
"eqeqeq": ["error", "always", { "null": "ignore" }],
|
|
@@ -115,12 +132,92 @@ export default [
|
|
|
115
132
|
// "ignoreTernaryTests": true,
|
|
116
133
|
},
|
|
117
134
|
],*/
|
|
135
|
+
"@typescript-eslint/naming-convention": [
|
|
136
|
+
"error",
|
|
137
|
+
// (1) private 대문자 필드 → 예외 허용
|
|
138
|
+
{
|
|
139
|
+
"selector": "classProperty",
|
|
140
|
+
"modifiers": ["private"],
|
|
141
|
+
"format": ["UPPER_CASE"],
|
|
142
|
+
"leadingUnderscore": "forbid",
|
|
143
|
+
"filter": {
|
|
144
|
+
"regex": "^[A-Z0-9_]+$",
|
|
145
|
+
"match": true
|
|
146
|
+
}
|
|
147
|
+
},
|
|
118
148
|
|
|
119
|
-
|
|
120
|
-
|
|
149
|
+
// (2) private 필드
|
|
150
|
+
{
|
|
151
|
+
"selector": "classProperty",
|
|
152
|
+
"modifiers": ["private"],
|
|
153
|
+
"format": ["camelCase"],
|
|
154
|
+
"leadingUnderscore": "require"
|
|
155
|
+
},
|
|
156
|
+
// private 메서드
|
|
157
|
+
{
|
|
158
|
+
"selector": "method",
|
|
159
|
+
"modifiers": ["private"],
|
|
160
|
+
"format": ["camelCase"],
|
|
161
|
+
"leadingUnderscore": "require",
|
|
162
|
+
},
|
|
163
|
+
// (1) protected readonly 필드 → 예외 허용
|
|
164
|
+
{
|
|
165
|
+
"selector": "classProperty",
|
|
166
|
+
"modifiers": ["protected", "readonly"],
|
|
167
|
+
"format": null,
|
|
168
|
+
"leadingUnderscore": "allow" // 언더스코어도 허용
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
// (2) protected 필드
|
|
172
|
+
{
|
|
173
|
+
"selector": "classProperty",
|
|
174
|
+
"modifiers": ["protected"],
|
|
175
|
+
"format": ["camelCase"],
|
|
176
|
+
"leadingUnderscore": "require"
|
|
177
|
+
},
|
|
178
|
+
// protected 메서드
|
|
179
|
+
{
|
|
180
|
+
"selector": "method",
|
|
181
|
+
"modifiers": ["protected"],
|
|
182
|
+
"format": ["camelCase"],
|
|
183
|
+
"leadingUnderscore": "require",
|
|
184
|
+
},
|
|
185
|
+
],
|
|
121
186
|
|
|
122
|
-
|
|
187
|
+
//-- 심플리즘
|
|
123
188
|
"@simplysm/ts-no-throw-not-implement-error": ["warn"],
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
// -- 아래 룰들은 매우 느림
|
|
192
|
+
// 라이브러리 작성시, 그때그때 필요할 때만 사용
|
|
193
|
+
|
|
194
|
+
// "import/no-extraneous-dependencies": [
|
|
195
|
+
// "error",
|
|
196
|
+
// {
|
|
197
|
+
// "devDependencies": [
|
|
198
|
+
// "**/*.spec.ts",
|
|
199
|
+
// "**/lib/**",
|
|
200
|
+
// "**!/eslint.config.js",
|
|
201
|
+
// "**!/simplysm.js",
|
|
202
|
+
// "**/vitest.config.js",
|
|
203
|
+
// ],
|
|
204
|
+
// },
|
|
205
|
+
// ],
|
|
206
|
+
// "@simplysm/ts-no-exported-types": [
|
|
207
|
+
// "error", {
|
|
208
|
+
// types: [
|
|
209
|
+
// {
|
|
210
|
+
// ban: "Uint8Array",
|
|
211
|
+
// safe: "Buffer",
|
|
212
|
+
// }, {
|
|
213
|
+
// ban: "ArrayBuffer",
|
|
214
|
+
// safe: "Buffer",
|
|
215
|
+
// ignoreInGeneric: true,
|
|
216
|
+
// },
|
|
217
|
+
// ],
|
|
218
|
+
// },
|
|
219
|
+
// ],
|
|
220
|
+
// "@simplysm/ts-no-buffer-in-typedarray-context": ["error"],
|
|
124
221
|
},
|
|
125
222
|
},
|
|
126
223
|
{
|
|
@@ -137,4 +234,4 @@ export default [
|
|
|
137
234
|
// "@angular-eslint/template/use-track-by-function": "error",
|
|
138
235
|
},
|
|
139
236
|
},
|
|
140
|
-
];
|
|
237
|
+
];
|
package/src/index.js
CHANGED
package/src/plugin.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import tsNoThrowNotImplementError from "./rules/ts-no-throw-not-implement-error.js";
|
|
2
2
|
import ngTemplateNoTodoComments from "./rules/ng-template-no-todo-comments.js";
|
|
3
|
+
import tsNoExportedTypes from "./rules/ts-no-exported-types.js";
|
|
4
|
+
import tsNoBufferInTypedArrayContext from "./rules/ts-no-buffer-in-typedarray-context.js";
|
|
3
5
|
|
|
4
6
|
export default {
|
|
5
7
|
rules: {
|
|
6
8
|
"ts-no-throw-not-implement-error": tsNoThrowNotImplementError,
|
|
9
|
+
"ts-no-exported-types": tsNoExportedTypes,
|
|
10
|
+
"ts-no-buffer-in-typedarray-context": tsNoBufferInTypedArrayContext,
|
|
7
11
|
"ng-template-no-todo-comments": ngTemplateNoTodoComments,
|
|
8
12
|
},
|
|
9
|
-
};
|
|
13
|
+
};
|
|
@@ -2,33 +2,29 @@ export default {
|
|
|
2
2
|
meta: {
|
|
3
3
|
type: "problem",
|
|
4
4
|
docs: {
|
|
5
|
-
description: "HTML
|
|
5
|
+
description: "HTML 템플릿 내 TODO 주석을 경고합니다.",
|
|
6
6
|
},
|
|
7
7
|
schema: [],
|
|
8
8
|
messages: {
|
|
9
|
-
noTodo: "
|
|
9
|
+
noTodo: "{{content}}",
|
|
10
10
|
},
|
|
11
11
|
},
|
|
12
12
|
|
|
13
13
|
create(context) {
|
|
14
14
|
const source = context.getSourceCode().getText();
|
|
15
|
-
|
|
16
|
-
const commentRegex = /<!--[\s\S]*?-->/g;
|
|
15
|
+
const commentRegex = /<!--([\s\S]*?)-->/g;
|
|
17
16
|
let match;
|
|
18
17
|
|
|
19
18
|
while ((match = commentRegex.exec(source)) !== null) {
|
|
20
|
-
const
|
|
21
|
-
|
|
19
|
+
const commentContent = match[1];
|
|
20
|
+
const todoIndex = commentContent.indexOf("TODO:");
|
|
21
|
+
if (todoIndex < 0) continue;
|
|
22
22
|
|
|
23
23
|
const start = match.index;
|
|
24
|
-
const end = start +
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const todoIndex = content.indexOf("TODO:");
|
|
29
|
-
if (todoIndex !== -1) {
|
|
30
|
-
content = content.substring(todoIndex + 5).trim();
|
|
31
|
-
}
|
|
24
|
+
const end = start + match[0].length;
|
|
25
|
+
const content = commentContent
|
|
26
|
+
.slice(todoIndex + 5)
|
|
27
|
+
.trim();
|
|
32
28
|
|
|
33
29
|
const loc = context.getSourceCode().getLocFromIndex(start);
|
|
34
30
|
const endLoc = context.getSourceCode().getLocFromIndex(end);
|
|
@@ -36,9 +32,7 @@ export default {
|
|
|
36
32
|
context.report({
|
|
37
33
|
loc: { start: loc, end: endLoc },
|
|
38
34
|
messageId: "noTodo",
|
|
39
|
-
data: {
|
|
40
|
-
content: content,
|
|
41
|
-
},
|
|
35
|
+
data: { content },
|
|
42
36
|
});
|
|
43
37
|
}
|
|
44
38
|
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
import { ESLintUtils } from '@typescript-eslint/utils';
|
|
3
|
+
|
|
4
|
+
const TYPED_ARRAY_NAMES = new Set([
|
|
5
|
+
'Uint8Array', 'Uint8ClampedArray',
|
|
6
|
+
'Int8Array', 'Uint16Array', 'Int16Array',
|
|
7
|
+
'Uint32Array', 'Int32Array',
|
|
8
|
+
'Float32Array', 'Float64Array',
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
export default {
|
|
12
|
+
meta: {
|
|
13
|
+
type: 'problem',
|
|
14
|
+
docs: {
|
|
15
|
+
description: 'Disallow Buffer being used directly in TypedArray contexts. Use new TypedArray(buffer) instead.',
|
|
16
|
+
recommended: 'error',
|
|
17
|
+
},
|
|
18
|
+
messages: {
|
|
19
|
+
directBuffer: 'Do not use Buffer directly where {{expected}} is expected. Use new {{expected}}(buffer) instead.',
|
|
20
|
+
},
|
|
21
|
+
schema: [],
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
create(context) {
|
|
25
|
+
const parserServices = ESLintUtils.getParserServices(context);
|
|
26
|
+
const checker = parserServices.program.getTypeChecker();
|
|
27
|
+
const typeCache = new WeakMap();
|
|
28
|
+
|
|
29
|
+
function getCachedType(tsNode) {
|
|
30
|
+
if (typeCache.has(tsNode)) return typeCache.get(tsNode);
|
|
31
|
+
const type = checker.getTypeAtLocation(tsNode);
|
|
32
|
+
typeCache.set(tsNode, type);
|
|
33
|
+
return type;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function getTypeName(type) {
|
|
37
|
+
const symbol = type.getSymbol();
|
|
38
|
+
return symbol ? symbol.getName() : undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isBuffer(type) {
|
|
42
|
+
return getTypeName(type) === 'Buffer';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isTypedArray(type) {
|
|
46
|
+
const name = getTypeName(type);
|
|
47
|
+
return name && TYPED_ARRAY_NAMES.has(name);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isInsideBufferStaticCall(tsNode) {
|
|
51
|
+
let current = tsNode.parent;
|
|
52
|
+
while (current) {
|
|
53
|
+
if (
|
|
54
|
+
current.kind === ts.SyntaxKind.CallExpression &&
|
|
55
|
+
current.expression.kind === ts.SyntaxKind.PropertyAccessExpression
|
|
56
|
+
) {
|
|
57
|
+
const expr = current.expression;
|
|
58
|
+
if (
|
|
59
|
+
expr.expression.kind === ts.SyntaxKind.Identifier &&
|
|
60
|
+
expr.expression.escapedText === 'Buffer'
|
|
61
|
+
) {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
current = current.parent;
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function reportIfInvalid(node, expectedType) {
|
|
71
|
+
const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node);
|
|
72
|
+
const actualType = getCachedType(tsNode);
|
|
73
|
+
|
|
74
|
+
// 예외: Buffer.~~~ 함수 호출 내의 인자는 검사 대상에서 제외
|
|
75
|
+
if (isInsideBufferStaticCall(tsNode)) return;
|
|
76
|
+
|
|
77
|
+
if (!isBuffer(actualType)) return;
|
|
78
|
+
if (!isTypedArray(expectedType)) return;
|
|
79
|
+
|
|
80
|
+
context.report({
|
|
81
|
+
node,
|
|
82
|
+
messageId: 'directBuffer',
|
|
83
|
+
data: {
|
|
84
|
+
expected: getTypeName(expectedType) || 'TypedArray',
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function checkTypedAssignment(lhsNode, rhsNode) {
|
|
90
|
+
if (lhsNode.type !== 'Identifier') return;
|
|
91
|
+
|
|
92
|
+
const lhsTsNode = parserServices.esTreeNodeToTSNodeMap.get(lhsNode);
|
|
93
|
+
const expectedType = getCachedType(lhsTsNode);
|
|
94
|
+
|
|
95
|
+
reportIfInvalid(rhsNode, expectedType);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
VariableDeclarator(node) {
|
|
100
|
+
if (node.init && node.id) {
|
|
101
|
+
checkTypedAssignment(node.id, node.init);
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
AssignmentExpression(node) {
|
|
106
|
+
checkTypedAssignment(node.left, node.right);
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
ReturnStatement(node) {
|
|
110
|
+
if (!node.argument) return;
|
|
111
|
+
const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node.argument);
|
|
112
|
+
const contextualType = checker.getContextualType(tsNode);
|
|
113
|
+
if (contextualType) {
|
|
114
|
+
reportIfInvalid(node.argument, contextualType);
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
CallExpression(node) {
|
|
119
|
+
const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node);
|
|
120
|
+
const signature = checker.getResolvedSignature(tsNode);
|
|
121
|
+
if (!signature) return;
|
|
122
|
+
|
|
123
|
+
const params = signature.getParameters();
|
|
124
|
+
node.arguments.forEach((arg, index) => {
|
|
125
|
+
const param = params[index];
|
|
126
|
+
if (!param) return;
|
|
127
|
+
|
|
128
|
+
const paramType = checker.getTypeOfSymbolAtLocation(param, tsNode);
|
|
129
|
+
reportIfInvalid(arg, paramType);
|
|
130
|
+
});
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
Property(node) {
|
|
134
|
+
if (!node.value) return;
|
|
135
|
+
const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node.value);
|
|
136
|
+
const contextualType = checker.getContextualType(tsNode);
|
|
137
|
+
if (contextualType) {
|
|
138
|
+
reportIfInvalid(node.value, contextualType);
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
ArrayExpression(node) {
|
|
143
|
+
node.elements.forEach(el => {
|
|
144
|
+
if (el && el.type !== 'SpreadElement') {
|
|
145
|
+
const tsNode = parserServices.esTreeNodeToTSNodeMap.get(el);
|
|
146
|
+
const contextualType = checker.getContextualType(tsNode);
|
|
147
|
+
if (contextualType) {
|
|
148
|
+
reportIfInvalid(el, contextualType);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
ConditionalExpression(node) {
|
|
155
|
+
const thenTsNode = parserServices.esTreeNodeToTSNodeMap.get(node.consequent);
|
|
156
|
+
const elseTsNode = parserServices.esTreeNodeToTSNodeMap.get(node.alternate);
|
|
157
|
+
const thenType = checker.getContextualType(thenTsNode);
|
|
158
|
+
const elseType = checker.getContextualType(elseTsNode);
|
|
159
|
+
|
|
160
|
+
if (thenType) reportIfInvalid(node.consequent, thenType);
|
|
161
|
+
if (elseType) reportIfInvalid(node.alternate, elseType);
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
},
|
|
165
|
+
};
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
import { ESLintUtils } from "@typescript-eslint/utils";
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
meta: {
|
|
6
|
+
type: "problem",
|
|
7
|
+
docs: {
|
|
8
|
+
description:
|
|
9
|
+
"지정된 타입이 export API나 public 클래스 멤버에서 노출되는 것을 금지하고, 대체 타입을 안내합니다.",
|
|
10
|
+
recommended: 'error',
|
|
11
|
+
},
|
|
12
|
+
schema: [
|
|
13
|
+
{
|
|
14
|
+
type: "object",
|
|
15
|
+
required: ["types"],
|
|
16
|
+
properties: {
|
|
17
|
+
types: {
|
|
18
|
+
type: "array",
|
|
19
|
+
items: {
|
|
20
|
+
type: "object",
|
|
21
|
+
required: ["ban"],
|
|
22
|
+
properties: {
|
|
23
|
+
ban: { type: "string" },
|
|
24
|
+
safe: { type: "string" },
|
|
25
|
+
ignoreInGeneric: { type: "boolean" },
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
additionalProperties: false,
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
messages: {
|
|
34
|
+
noExportedTypes:
|
|
35
|
+
'타입 "{{typeName}}"은(는) 공개적으로 노출되어서는 안됩니다.{{safeSuggestion}}',
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
defaultOptions: [{ types: [] }],
|
|
40
|
+
|
|
41
|
+
create(context) {
|
|
42
|
+
const optTypes = context.options[0].types;
|
|
43
|
+
const optTypeMap = new Map(optTypes.map(t => [
|
|
44
|
+
t.ban,
|
|
45
|
+
{ safe: t.safe ?? undefined, ignoreInGeneric: t.ignoreInGeneric ?? false },
|
|
46
|
+
]));
|
|
47
|
+
const bannedTypeNames = new Set(optTypes.map(t => t.ban));
|
|
48
|
+
|
|
49
|
+
const parserServices = ESLintUtils.getParserServices(context);
|
|
50
|
+
const checker = parserServices.program.getTypeChecker();
|
|
51
|
+
const typeCache = new WeakMap();
|
|
52
|
+
|
|
53
|
+
function getCachedType(tsNode) {
|
|
54
|
+
if (typeCache.has(tsNode)) return typeCache.get(tsNode);
|
|
55
|
+
const type = checker.getTypeAtLocation(tsNode);
|
|
56
|
+
typeCache.set(tsNode, type);
|
|
57
|
+
return type;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function isBannedTypeRecursive(type) {
|
|
61
|
+
const visited = new Set();
|
|
62
|
+
|
|
63
|
+
function visit(t) {
|
|
64
|
+
if (visited.has(t)) return undefined;
|
|
65
|
+
visited.add(t);
|
|
66
|
+
|
|
67
|
+
const name = t.aliasSymbol?.escapedName ?? t.symbol?.escapedName;
|
|
68
|
+
if (typeof name === "string" && bannedTypeNames.has(name)) {
|
|
69
|
+
const entry = optTypeMap.get(name);
|
|
70
|
+
if (entry.ignoreInGeneric) return undefined;
|
|
71
|
+
return { ban: name, safe: entry.safe };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const typeArgs = ("aliasTypeArguments" in t && Array.isArray(t.aliasTypeArguments))
|
|
75
|
+
? t.aliasTypeArguments
|
|
76
|
+
: (t.flags & ts.TypeFlags.Object && t.objectFlags & ts.ObjectFlags.Reference)
|
|
77
|
+
? checker.getTypeArguments(t)
|
|
78
|
+
: [];
|
|
79
|
+
|
|
80
|
+
for (const arg of typeArgs) {
|
|
81
|
+
const match = visit(arg);
|
|
82
|
+
if (match) return match;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (t.isUnionOrIntersection?.()) {
|
|
86
|
+
for (const sub of t.types) {
|
|
87
|
+
const match = visit(sub);
|
|
88
|
+
if (match) return match;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const elementType = t.getNumberIndexType?.();
|
|
93
|
+
if (elementType) {
|
|
94
|
+
const match = visit(elementType);
|
|
95
|
+
if (match) return match;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return visit(type);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function reportIfBanned(type, node) {
|
|
105
|
+
const match = isBannedTypeRecursive(type);
|
|
106
|
+
if (match) {
|
|
107
|
+
context.report({
|
|
108
|
+
node,
|
|
109
|
+
messageId: "noExportedTypes",
|
|
110
|
+
data: {
|
|
111
|
+
typeName: match.ban,
|
|
112
|
+
safeSuggestion: match.safe
|
|
113
|
+
? ` 더 안전한 대체 타입 "${match.safe}"을(를) 사용하세요.`
|
|
114
|
+
: "",
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
FunctionDeclaration(node) {
|
|
122
|
+
const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node);
|
|
123
|
+
const isExported =
|
|
124
|
+
(ts.getCombinedModifierFlags(tsNode) & ts.ModifierFlags.Export) !== 0;
|
|
125
|
+
if (!isExported) return;
|
|
126
|
+
|
|
127
|
+
const signature = checker.getSignatureFromDeclaration(tsNode);
|
|
128
|
+
if (!signature) return;
|
|
129
|
+
|
|
130
|
+
reportIfBanned(checker.getReturnTypeOfSignature(signature), node);
|
|
131
|
+
|
|
132
|
+
for (const param of node.params) {
|
|
133
|
+
const tsParam = parserServices.esTreeNodeToTSNodeMap.get(param);
|
|
134
|
+
reportIfBanned(getCachedType(tsParam), param);
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
MethodDefinition(node) {
|
|
139
|
+
const isConstructor = node.kind === "constructor";
|
|
140
|
+
const isPublic =
|
|
141
|
+
node.accessibility !== "private" &&
|
|
142
|
+
node.accessibility !== "protected";
|
|
143
|
+
if (!isConstructor && !isPublic) return;
|
|
144
|
+
|
|
145
|
+
const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node.value);
|
|
146
|
+
const signature = checker.getSignatureFromDeclaration(tsNode);
|
|
147
|
+
if (!signature) return;
|
|
148
|
+
|
|
149
|
+
if (!isConstructor) {
|
|
150
|
+
reportIfBanned(checker.getReturnTypeOfSignature(signature), node);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
for (const param of node.value.params) {
|
|
154
|
+
const tsParam = parserServices.esTreeNodeToTSNodeMap.get(param);
|
|
155
|
+
reportIfBanned(getCachedType(tsParam), param);
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
PropertyDefinition(node) {
|
|
160
|
+
const isPublic =
|
|
161
|
+
node.accessibility !== "private" && node.accessibility !== "protected";
|
|
162
|
+
if (!isPublic) return;
|
|
163
|
+
|
|
164
|
+
const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node);
|
|
165
|
+
reportIfBanned(getCachedType(tsNode), node);
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
VariableDeclarator(node) {
|
|
169
|
+
const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node);
|
|
170
|
+
let modParent = tsNode.parent;
|
|
171
|
+
while (modParent && !("modifiers" in modParent)) {
|
|
172
|
+
modParent = modParent.parent;
|
|
173
|
+
}
|
|
174
|
+
if (!modParent || !("modifiers" in modParent)) return;
|
|
175
|
+
|
|
176
|
+
const isExported = (modParent.modifiers ?? []).some(
|
|
177
|
+
(m) => m.kind === ts.SyntaxKind.ExportKeyword,
|
|
178
|
+
);
|
|
179
|
+
if (!isExported) return;
|
|
180
|
+
|
|
181
|
+
let type;
|
|
182
|
+
if (node.id.typeAnnotation) {
|
|
183
|
+
const tsType = parserServices.esTreeNodeToTSNodeMap.get(
|
|
184
|
+
node.id.typeAnnotation.typeAnnotation,
|
|
185
|
+
);
|
|
186
|
+
type = checker.getTypeFromTypeNode(tsType);
|
|
187
|
+
}
|
|
188
|
+
else if (node.init) {
|
|
189
|
+
const tsInit = parserServices.esTreeNodeToTSNodeMap.get(node.init);
|
|
190
|
+
type = getCachedType(tsInit);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (type) {
|
|
194
|
+
reportIfBanned(type, node.id);
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
},
|
|
199
|
+
};
|