@simplysm/lint 13.0.2
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/README.md +522 -0
- package/dist/eslint-plugin.d.ts +15 -0
- package/dist/eslint-plugin.d.ts.map +1 -0
- package/dist/eslint-plugin.js +14 -0
- package/dist/eslint-plugin.js.map +6 -0
- package/dist/eslint-recommended.d.ts +3 -0
- package/dist/eslint-recommended.d.ts.map +1 -0
- package/dist/eslint-recommended.js +259 -0
- package/dist/eslint-recommended.js.map +6 -0
- package/dist/rules/no-hard-private.d.ts +15 -0
- package/dist/rules/no-hard-private.d.ts.map +1 -0
- package/dist/rules/no-hard-private.js +95 -0
- package/dist/rules/no-hard-private.js.map +6 -0
- package/dist/rules/no-subpath-imports-from-simplysm.d.ts +14 -0
- package/dist/rules/no-subpath-imports-from-simplysm.d.ts.map +1 -0
- package/dist/rules/no-subpath-imports-from-simplysm.js +64 -0
- package/dist/rules/no-subpath-imports-from-simplysm.js.map +6 -0
- package/dist/rules/ts-no-throw-not-implemented-error.d.ts +19 -0
- package/dist/rules/ts-no-throw-not-implemented-error.d.ts.map +1 -0
- package/dist/rules/ts-no-throw-not-implemented-error.js +63 -0
- package/dist/rules/ts-no-throw-not-implemented-error.js.map +6 -0
- package/dist/stylelint-recommended.d.ts +13 -0
- package/dist/stylelint-recommended.d.ts.map +1 -0
- package/dist/stylelint-recommended.js +20 -0
- package/dist/stylelint-recommended.js.map +6 -0
- package/dist/utils/create-rule.d.ts +22 -0
- package/dist/utils/create-rule.d.ts.map +1 -0
- package/dist/utils/create-rule.js +8 -0
- package/dist/utils/create-rule.js.map +6 -0
- package/package.json +52 -0
- package/src/eslint-plugin.ts +11 -0
- package/src/eslint-recommended.ts +274 -0
- package/src/rules/no-hard-private.ts +136 -0
- package/src/rules/no-subpath-imports-from-simplysm.ts +78 -0
- package/src/rules/ts-no-throw-not-implemented-error.ts +103 -0
- package/src/stylelint-recommended.ts +16 -0
- package/src/utils/create-rule.ts +22 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { ESLintUtils } from "@typescript-eslint/utils";
|
|
2
|
+
const createRule = ESLintUtils.RuleCreator(
|
|
3
|
+
(name) => `https://github.com/kslhunter/simplysm/blob/master/packages/eslint-plugin/README.md#${name}`
|
|
4
|
+
);
|
|
5
|
+
export {
|
|
6
|
+
createRule
|
|
7
|
+
};
|
|
8
|
+
//# sourceMappingURL=create-rule.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@simplysm/lint",
|
|
3
|
+
"sideEffects": false,
|
|
4
|
+
"version": "13.0.2",
|
|
5
|
+
"description": "심플리즘 패키지 - Lint 설정 (ESLint + Stylelint)",
|
|
6
|
+
"author": "김석래",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/kslhunter/simplysm.git",
|
|
10
|
+
"directory": "packages/lint"
|
|
11
|
+
},
|
|
12
|
+
"license": "Apache-2.0",
|
|
13
|
+
"type": "module",
|
|
14
|
+
"exports": {
|
|
15
|
+
"./eslint-plugin": {
|
|
16
|
+
"types": "./dist/eslint-plugin.d.ts",
|
|
17
|
+
"default": "./dist/eslint-plugin.js"
|
|
18
|
+
},
|
|
19
|
+
"./eslint-recommended": {
|
|
20
|
+
"types": "./dist/eslint-recommended.d.ts",
|
|
21
|
+
"default": "./dist/eslint-recommended.js"
|
|
22
|
+
},
|
|
23
|
+
"./stylelint-recommended": {
|
|
24
|
+
"types": "./dist/stylelint-recommended.d.ts",
|
|
25
|
+
"default": "./dist/stylelint-recommended.js"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist",
|
|
30
|
+
"src"
|
|
31
|
+
],
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@typescript-eslint/utils": "^8.55.0",
|
|
34
|
+
"eslint": "^9.39.2",
|
|
35
|
+
"eslint-plugin-import": "^2.32.0",
|
|
36
|
+
"eslint-plugin-solid": "^0.14.5",
|
|
37
|
+
"eslint-plugin-tailwindcss": "^3.18.2",
|
|
38
|
+
"eslint-plugin-unused-imports": "^4.4.1",
|
|
39
|
+
"globals": "^17.3.0",
|
|
40
|
+
"typescript": "^5.9.3",
|
|
41
|
+
"typescript-eslint": "^8.55.0",
|
|
42
|
+
"stylelint": "^16.26.1",
|
|
43
|
+
"stylelint-config-standard": "^37.0.0",
|
|
44
|
+
"stylelint-config-tailwindcss": "^1.0.1",
|
|
45
|
+
"stylelint-no-unsupported-browser-features": "^8.1.1",
|
|
46
|
+
"stylelint-no-unresolved-module": "^2.5.2"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@types/eslint-plugin-tailwindcss": "^3.17.0",
|
|
50
|
+
"@typescript-eslint/rule-tester": "^8.55.0"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import noHardPrivate from "./rules/no-hard-private";
|
|
2
|
+
import noSubpathImportsFromSimplysm from "./rules/no-subpath-imports-from-simplysm";
|
|
3
|
+
import tsNoThrowNotImplementedError from "./rules/ts-no-throw-not-implemented-error";
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
rules: {
|
|
7
|
+
"no-hard-private": noHardPrivate,
|
|
8
|
+
"no-subpath-imports-from-simplysm": noSubpathImportsFromSimplysm,
|
|
9
|
+
"ts-no-throw-not-implemented-error": tsNoThrowNotImplementedError,
|
|
10
|
+
},
|
|
11
|
+
};
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import globals from "globals";
|
|
2
|
+
import tseslint, { type FlatConfig } from "typescript-eslint";
|
|
3
|
+
import plugin from "./eslint-plugin";
|
|
4
|
+
import importPlugin from "eslint-plugin-import";
|
|
5
|
+
import unusedImportsPlugin from "eslint-plugin-unused-imports";
|
|
6
|
+
import solidPlugin from "eslint-plugin-solid";
|
|
7
|
+
import tailwindcssPlugin from "eslint-plugin-tailwindcss";
|
|
8
|
+
import { defineConfig, globalIgnores } from "eslint/config";
|
|
9
|
+
import { ESLint } from "eslint";
|
|
10
|
+
|
|
11
|
+
//#region 공통 규칙 설정
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* JS/TS 공통 규칙
|
|
15
|
+
* - no-console: 프로덕션 코드에서 console 사용 금지 (성능 저하 방지)
|
|
16
|
+
* - no-warning-comments: TODO/FIXME 주석 경고 (미완성 코드 확인용)
|
|
17
|
+
* - eqeqeq: `===` 사용 강제 (null 체크는 `== null` 허용)
|
|
18
|
+
* - no-self-compare: `x === x` 같은 오타 방지
|
|
19
|
+
* - array-callback-return: map/filter 등에서 return 빠뜨림 방지
|
|
20
|
+
*/
|
|
21
|
+
const commonRules: FlatConfig.Rules = {
|
|
22
|
+
"no-console": "error",
|
|
23
|
+
"no-warning-comments": "warn",
|
|
24
|
+
"eqeqeq": ["error", "always", { null: "ignore" }],
|
|
25
|
+
"no-self-compare": "error",
|
|
26
|
+
"array-callback-return": "error",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 모든 패키지에서 Node.js 전용 API 사용 금지 (코드 통일)
|
|
31
|
+
* - Buffer → Uint8Array, bytesToHex/bytesFromHex/bytesConcat 사용
|
|
32
|
+
* - EventEmitter → SdEventEmitter 사용
|
|
33
|
+
*/
|
|
34
|
+
const noNodeBuiltinsRules: FlatConfig.Rules = {
|
|
35
|
+
"no-restricted-globals": [
|
|
36
|
+
"error",
|
|
37
|
+
{
|
|
38
|
+
name: "Buffer",
|
|
39
|
+
message: "Uint8Array를 사용하세요. 복잡한 연산은 @simplysm/core-common의 BytesUtils를 사용하세요.",
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
"no-restricted-imports": [
|
|
43
|
+
"error",
|
|
44
|
+
{
|
|
45
|
+
paths: [
|
|
46
|
+
{
|
|
47
|
+
name: "buffer",
|
|
48
|
+
message: "Uint8Array를 사용하세요. 복잡한 연산은 @simplysm/core-common의 BytesUtils를 사용하세요.",
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: "events",
|
|
52
|
+
message: "@simplysm/core-common의 SdEventEmitter를 사용하세요.",
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: "eventemitter3",
|
|
56
|
+
message: "@simplysm/core-common의 SdEventEmitter를 사용하세요.",
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 미사용 import 처리 규칙
|
|
65
|
+
* - 미사용 import 자동 제거
|
|
66
|
+
* - `_` 접두사 변수/인자는 미사용 허용 (예: `_unused`)
|
|
67
|
+
*/
|
|
68
|
+
const unusedImportsRules: FlatConfig.Rules = {
|
|
69
|
+
"unused-imports/no-unused-imports": "error",
|
|
70
|
+
"unused-imports/no-unused-vars": [
|
|
71
|
+
"error",
|
|
72
|
+
{
|
|
73
|
+
vars: "all",
|
|
74
|
+
varsIgnorePattern: "^_",
|
|
75
|
+
args: "after-used",
|
|
76
|
+
argsIgnorePattern: "^_",
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
//#endregion
|
|
82
|
+
|
|
83
|
+
export default defineConfig([
|
|
84
|
+
globalIgnores([
|
|
85
|
+
// directory/** 형태로 순회 자체를 건너뜀
|
|
86
|
+
"**/node_modules/**",
|
|
87
|
+
"**/dist/**",
|
|
88
|
+
"**/.*/**",
|
|
89
|
+
"**/_*/**",
|
|
90
|
+
]),
|
|
91
|
+
{
|
|
92
|
+
languageOptions: {
|
|
93
|
+
globals: {
|
|
94
|
+
...globals.node,
|
|
95
|
+
...globals.es2024,
|
|
96
|
+
...globals.browser,
|
|
97
|
+
},
|
|
98
|
+
ecmaVersion: 2024,
|
|
99
|
+
sourceType: "module",
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
files: ["**/*.js", "**/*.jsx"],
|
|
104
|
+
plugins: {
|
|
105
|
+
"import": importPlugin,
|
|
106
|
+
"@simplysm": plugin as unknown as ESLint.Plugin,
|
|
107
|
+
"unused-imports": unusedImportsPlugin,
|
|
108
|
+
},
|
|
109
|
+
rules: {
|
|
110
|
+
...commonRules,
|
|
111
|
+
|
|
112
|
+
"require-await": "error",
|
|
113
|
+
"no-shadow": "error",
|
|
114
|
+
"no-duplicate-imports": "error",
|
|
115
|
+
"no-unused-expressions": "error",
|
|
116
|
+
"no-undef": "error",
|
|
117
|
+
|
|
118
|
+
...unusedImportsRules,
|
|
119
|
+
|
|
120
|
+
"import/no-extraneous-dependencies": [
|
|
121
|
+
"error",
|
|
122
|
+
{
|
|
123
|
+
devDependencies: ["**/lib/**", "**/eslint.config.js", "**/simplysm.js", "**/vitest.config.js"],
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
|
|
127
|
+
// JS/TS 공통
|
|
128
|
+
"@simplysm/no-subpath-imports-from-simplysm": "error",
|
|
129
|
+
"@simplysm/no-hard-private": "error",
|
|
130
|
+
|
|
131
|
+
...noNodeBuiltinsRules,
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
files: ["**/*.ts", "**/*.tsx"],
|
|
136
|
+
plugins: {
|
|
137
|
+
"@typescript-eslint": tseslint.plugin,
|
|
138
|
+
"@simplysm": plugin as unknown as ESLint.Plugin,
|
|
139
|
+
"import": importPlugin,
|
|
140
|
+
"unused-imports": unusedImportsPlugin,
|
|
141
|
+
},
|
|
142
|
+
languageOptions: {
|
|
143
|
+
parser: tseslint.parser,
|
|
144
|
+
parserOptions: {
|
|
145
|
+
project: true,
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
rules: {
|
|
149
|
+
...commonRules,
|
|
150
|
+
|
|
151
|
+
"@typescript-eslint/require-await": "error",
|
|
152
|
+
"@typescript-eslint/await-thenable": "error",
|
|
153
|
+
"@typescript-eslint/return-await": ["error", "in-try-catch"],
|
|
154
|
+
"@typescript-eslint/no-floating-promises": "error",
|
|
155
|
+
"@typescript-eslint/no-shadow": "error",
|
|
156
|
+
"@typescript-eslint/no-unnecessary-condition": ["error", { allowConstantLoopConditions: true }],
|
|
157
|
+
"@typescript-eslint/no-unnecessary-type-assertion": "error",
|
|
158
|
+
// "@typescript-eslint/non-nullable-type-assertion-style": "error",
|
|
159
|
+
"@typescript-eslint/prefer-reduce-type-parameter": "error",
|
|
160
|
+
"@typescript-eslint/prefer-return-this-type": "error",
|
|
161
|
+
"@typescript-eslint/no-unused-expressions": "error",
|
|
162
|
+
"@typescript-eslint/strict-boolean-expressions": [
|
|
163
|
+
"error",
|
|
164
|
+
{
|
|
165
|
+
allowNullableBoolean: true,
|
|
166
|
+
allowNullableObject: true,
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
"@typescript-eslint/ban-ts-comment": [
|
|
170
|
+
"error",
|
|
171
|
+
{
|
|
172
|
+
"ts-expect-error": "allow-with-description",
|
|
173
|
+
"minimumDescriptionLength": 3,
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
"@typescript-eslint/prefer-readonly": "error",
|
|
177
|
+
|
|
178
|
+
// 실수 방지: void 콜백에 async 함수 전달 (에러 누락 방지)
|
|
179
|
+
// - arguments: false → socket.on("event", async () => {}) 허용 (내부 try-catch로 처리)
|
|
180
|
+
// - attributes: false → JSX 이벤트 핸들러 허용 (SolidJS 호환)
|
|
181
|
+
"@typescript-eslint/no-misused-promises": [
|
|
182
|
+
"error",
|
|
183
|
+
{ checksVoidReturn: { arguments: false, attributes: false } },
|
|
184
|
+
],
|
|
185
|
+
// 실수 방지: Error 아닌 것을 throw (스택 트레이스 손실 방지)
|
|
186
|
+
"@typescript-eslint/only-throw-error": "error",
|
|
187
|
+
// 실수 방지: 배열에 delete 사용 (희소 배열 버그 방지)
|
|
188
|
+
"@typescript-eslint/no-array-delete": "error",
|
|
189
|
+
|
|
190
|
+
"@simplysm/no-hard-private": "error",
|
|
191
|
+
"@simplysm/no-subpath-imports-from-simplysm": "error",
|
|
192
|
+
"@simplysm/ts-no-throw-not-implemented-error": "warn",
|
|
193
|
+
|
|
194
|
+
...unusedImportsRules,
|
|
195
|
+
...noNodeBuiltinsRules,
|
|
196
|
+
|
|
197
|
+
"import/no-extraneous-dependencies": [
|
|
198
|
+
"error",
|
|
199
|
+
{
|
|
200
|
+
devDependencies: [
|
|
201
|
+
"**/lib/**",
|
|
202
|
+
"**/eslint.config.ts",
|
|
203
|
+
"**/simplysm.ts",
|
|
204
|
+
"**/vitest.config.ts",
|
|
205
|
+
"**/vitest.setup.ts",
|
|
206
|
+
],
|
|
207
|
+
},
|
|
208
|
+
],
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
// 테스트 폴더: 루트 devDependencies(vitest 등) 사용 허용
|
|
212
|
+
{
|
|
213
|
+
files: ["**/tests/**/*.ts", "**/tests/**/*.tsx"],
|
|
214
|
+
rules: {
|
|
215
|
+
"no-console": "off",
|
|
216
|
+
"import/no-extraneous-dependencies": "off",
|
|
217
|
+
"@simplysm/ts-no-throw-not-implemented-error": "off",
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
// SolidJS TSX 파일: 모든 규칙 명시적으로 설정 (error)
|
|
221
|
+
{
|
|
222
|
+
files: ["**/*.ts", "**/*.tsx"],
|
|
223
|
+
plugins: {
|
|
224
|
+
solid: solidPlugin as unknown as ESLint.Plugin,
|
|
225
|
+
tailwindcss: tailwindcssPlugin as unknown as ESLint.Plugin,
|
|
226
|
+
},
|
|
227
|
+
settings: {
|
|
228
|
+
tailwindcss: {
|
|
229
|
+
// 템플릿 리터럴 태그 지원: clsx`py-0.5 px-1.5` 형태 인식
|
|
230
|
+
tags: ["clsx"],
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
rules: {
|
|
234
|
+
// ─── 실수 방지 ───
|
|
235
|
+
"solid/reactivity": ["error", { customReactiveFunctions: ["makePersisted"] }], // 반응성 손실 (가장 중요!)
|
|
236
|
+
"solid/no-destructure": "error", // props 구조분해 → 반응성 손실
|
|
237
|
+
"solid/components-return-once": "error", // early return → 버그
|
|
238
|
+
"solid/jsx-no-duplicate-props": "error", // 중복 props
|
|
239
|
+
"solid/jsx-no-undef": ["error", { typescriptEnabled: true }], // 정의 안 된 변수
|
|
240
|
+
"solid/no-react-deps": "error", // React 의존성 배열 실수
|
|
241
|
+
"solid/no-react-specific-props": "error", // React props 실수 (className 등)
|
|
242
|
+
|
|
243
|
+
// ─── 보안 ───
|
|
244
|
+
"solid/no-innerhtml": "error", // XSS 방지
|
|
245
|
+
"solid/jsx-no-script-url": "error", // javascript: URL 방지
|
|
246
|
+
|
|
247
|
+
// ─── 도구 지원 ───
|
|
248
|
+
"solid/jsx-uses-vars": "error", // unused import 오탐 방지
|
|
249
|
+
|
|
250
|
+
// ─── 컨벤션 ───
|
|
251
|
+
"solid/prefer-for": "error", // For 컴포넌트 권장
|
|
252
|
+
"solid/event-handlers": "error", // 이벤트 핸들러 네이밍
|
|
253
|
+
"solid/imports": "error", // import 일관성
|
|
254
|
+
"solid/style-prop": "error", // style prop 형식
|
|
255
|
+
"solid/self-closing-comp": "error", // 자체 닫기 태그
|
|
256
|
+
|
|
257
|
+
// ─── Tailwind CSS ───
|
|
258
|
+
"tailwindcss/classnames-order": "warn", // 클래스 순서 자동 정렬
|
|
259
|
+
"tailwindcss/enforces-negative-arbitrary-values": "error", // 음수 임의값 형식 통일
|
|
260
|
+
"tailwindcss/enforces-shorthand": "error", // 축약형 사용 권장
|
|
261
|
+
"tailwindcss/no-contradicting-classname": "error", // 충돌하는 클래스 금지 (p-2 p-4 등)
|
|
262
|
+
"tailwindcss/no-custom-classname": "error", // Tailwind에 없는 커스텀 클래스 금지
|
|
263
|
+
"tailwindcss/no-unnecessary-arbitrary-value": "error", // 불필요한 임의값 금지
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
// 테스트 폴더: solid/reactivity 비활성화
|
|
267
|
+
{
|
|
268
|
+
files: ["**/tests/**/*.ts", "**/tests/**/*.tsx"],
|
|
269
|
+
rules: {
|
|
270
|
+
// 테스트에서는 waitFor 등 비동기 콜백 내 signal 접근이 의도된 동작
|
|
271
|
+
"solid/reactivity": "off",
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
]);
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { AST_NODE_TYPES, type TSESTree } from "@typescript-eslint/utils";
|
|
2
|
+
import type { RuleFix } from "@typescript-eslint/utils/ts-eslint";
|
|
3
|
+
import { createRule } from "../utils/create-rule";
|
|
4
|
+
|
|
5
|
+
type ClassMemberWithAccessibility = TSESTree.PropertyDefinition | TSESTree.MethodDefinition | TSESTree.AccessorProperty;
|
|
6
|
+
|
|
7
|
+
function isClassMemberWithAccessibility(node: TSESTree.Node | undefined): node is ClassMemberWithAccessibility {
|
|
8
|
+
return (
|
|
9
|
+
node?.type === AST_NODE_TYPES.PropertyDefinition ||
|
|
10
|
+
node?.type === AST_NODE_TYPES.MethodDefinition ||
|
|
11
|
+
node?.type === AST_NODE_TYPES.AccessorProperty
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* ECMAScript private 필드(`#field`) 사용을 제한하고 TypeScript `private` 키워드 사용을 강제하는 ESLint 규칙
|
|
17
|
+
*
|
|
18
|
+
* @remarks
|
|
19
|
+
* 이 규칙은 다음을 검사한다:
|
|
20
|
+
* - 클래스 필드 선언: `#field`
|
|
21
|
+
* - 클래스 메서드 선언: `#method()`
|
|
22
|
+
* - 클래스 접근자 선언: `accessor #field`
|
|
23
|
+
* - 멤버 접근 표현식: `this.#field`
|
|
24
|
+
*/
|
|
25
|
+
export default createRule({
|
|
26
|
+
name: "no-hard-private",
|
|
27
|
+
meta: {
|
|
28
|
+
type: "problem",
|
|
29
|
+
docs: {
|
|
30
|
+
description: '하드 프라이빗 필드(#) 대신 TypeScript "private _" 스타일을 강제한다.',
|
|
31
|
+
},
|
|
32
|
+
messages: {
|
|
33
|
+
preferSoftPrivate: '하드 프라이빗 필드(#)는 허용되지 않습니다. "private _" 스타일을 사용하세요.',
|
|
34
|
+
nameConflict:
|
|
35
|
+
'하드 프라이빗 필드 "#{{name}}"을 "_{{name}}"으로 변환할 수 없습니다. 동일한 이름의 멤버가 이미 존재합니다.',
|
|
36
|
+
},
|
|
37
|
+
fixable: "code",
|
|
38
|
+
schema: [],
|
|
39
|
+
},
|
|
40
|
+
defaultOptions: [],
|
|
41
|
+
create(context) {
|
|
42
|
+
const sourceCode = context.sourceCode;
|
|
43
|
+
// 중첩 클래스 지원을 위한 스택 구조
|
|
44
|
+
const classStack: Set<string>[] = [];
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
// 0. 클래스 진입 시 멤버 이름 수집
|
|
48
|
+
"ClassBody"(node: TSESTree.ClassBody) {
|
|
49
|
+
const memberNames = new Set<string>();
|
|
50
|
+
for (const member of node.body) {
|
|
51
|
+
if (!("key" in member)) continue;
|
|
52
|
+
const key = member.key;
|
|
53
|
+
if (key.type === AST_NODE_TYPES.Identifier) {
|
|
54
|
+
memberNames.add(key.name);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
classStack.push(memberNames);
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
"ClassBody:exit"() {
|
|
61
|
+
classStack.pop();
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
// 1. 선언부 감지 (PropertyDefinition, MethodDefinition, AccessorProperty)
|
|
65
|
+
"PropertyDefinition > PrivateIdentifier, MethodDefinition > PrivateIdentifier, AccessorProperty > PrivateIdentifier"(
|
|
66
|
+
node: TSESTree.PrivateIdentifier,
|
|
67
|
+
) {
|
|
68
|
+
const parent = node.parent;
|
|
69
|
+
if (!isClassMemberWithAccessibility(parent)) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const identifierName = node.name; // '#'을 제외한 이름
|
|
74
|
+
const targetName = `_${identifierName}`;
|
|
75
|
+
const currentClassMembers = classStack.at(-1);
|
|
76
|
+
|
|
77
|
+
// 이름 충돌 검사
|
|
78
|
+
if (currentClassMembers?.has(targetName)) {
|
|
79
|
+
context.report({
|
|
80
|
+
node,
|
|
81
|
+
messageId: "nameConflict",
|
|
82
|
+
data: { name: identifierName },
|
|
83
|
+
});
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
context.report({
|
|
88
|
+
node,
|
|
89
|
+
messageId: "preferSoftPrivate",
|
|
90
|
+
fix(fixer) {
|
|
91
|
+
const fixes: RuleFix[] = [];
|
|
92
|
+
|
|
93
|
+
// 1-1. 이름 변경 (#a -> _a)
|
|
94
|
+
fixes.push(fixer.replaceText(node, targetName));
|
|
95
|
+
|
|
96
|
+
// 1-2. 'private' 접근 제어자 추가 위치 계산
|
|
97
|
+
if (parent.accessibility == null) {
|
|
98
|
+
// 기본 삽입 위치: 부모 노드의 시작 지점 (static, async 등 포함)
|
|
99
|
+
let tokenToInsertBefore = sourceCode.getFirstToken(parent);
|
|
100
|
+
|
|
101
|
+
// 데코레이터가 있다면, 마지막 데코레이터 '다음' 토큰 앞에 삽입
|
|
102
|
+
// (@Deco private static _foo)
|
|
103
|
+
if (parent.decorators.length > 0) {
|
|
104
|
+
const lastDecorator = parent.decorators.at(-1)!;
|
|
105
|
+
tokenToInsertBefore = sourceCode.getTokenAfter(lastDecorator);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// tokenToInsertBefore는 이제 'static', 'async', 'readonly' 또는 변수명('_foo')입니다.
|
|
109
|
+
// 이 앞에 'private '를 붙이면 자연스럽게 'private static ...' 순서가 됩니다.
|
|
110
|
+
// tokenToInsertBefore가 null인 경우는 AST 파싱 오류 등 예외 상황이므로,
|
|
111
|
+
// 이름만 변경되는 불완전한 fix를 방지하기 위해 전체 fix를 생략한다.
|
|
112
|
+
if (tokenToInsertBefore == null) {
|
|
113
|
+
return [];
|
|
114
|
+
}
|
|
115
|
+
fixes.push(fixer.insertTextBefore(tokenToInsertBefore, "private "));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return fixes;
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
// 2. 사용부 감지 (this.#field)
|
|
124
|
+
"MemberExpression > PrivateIdentifier"(node: TSESTree.PrivateIdentifier) {
|
|
125
|
+
const identifierName = node.name;
|
|
126
|
+
context.report({
|
|
127
|
+
node,
|
|
128
|
+
messageId: "preferSoftPrivate",
|
|
129
|
+
fix(fixer) {
|
|
130
|
+
return fixer.replaceText(node, `_${identifierName}`);
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
},
|
|
136
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { AST_NODE_TYPES, TSESTree } from "@typescript-eslint/utils";
|
|
2
|
+
import { createRule } from "../utils/create-rule";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `@simplysm/*` 패키지에서 'src' 서브경로 import를 금지하는 ESLint 규칙
|
|
6
|
+
*
|
|
7
|
+
* @remarks
|
|
8
|
+
* 이 규칙은 다음을 검사한다:
|
|
9
|
+
* - 정적 import 문: `import ... from '...'`
|
|
10
|
+
* - 동적 import: `import('...')`
|
|
11
|
+
* - re-export 문: `export { ... } from '...'`, `export * from '...'`
|
|
12
|
+
*/
|
|
13
|
+
export default createRule({
|
|
14
|
+
name: "no-subpath-imports-from-simplysm",
|
|
15
|
+
meta: {
|
|
16
|
+
type: "problem",
|
|
17
|
+
docs: {
|
|
18
|
+
description: "@simplysm 패키지에서 'src' 서브경로 import를 금지한다. (ex: @simplysm/pkg/src/x → 금지)",
|
|
19
|
+
},
|
|
20
|
+
fixable: "code",
|
|
21
|
+
schema: [],
|
|
22
|
+
messages: {
|
|
23
|
+
noSubpathImport: "'@simplysm/{{pkg}}' 패키지는 'src' 서브경로를 import할 수 없습니다: '{{importPath}}'",
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
defaultOptions: [],
|
|
27
|
+
create(context) {
|
|
28
|
+
function checkAndReport(sourceNode: TSESTree.StringLiteral, importPath: string) {
|
|
29
|
+
if (!importPath.startsWith("@simplysm/")) return;
|
|
30
|
+
|
|
31
|
+
const parts = importPath.split("/");
|
|
32
|
+
|
|
33
|
+
// 허용: @simplysm/pkg, @simplysm/pkg/xxx, @simplysm/pkg/xxx/yyy
|
|
34
|
+
// 금지: @simplysm/pkg/src, @simplysm/pkg/src/xxx
|
|
35
|
+
if (parts.length >= 3 && parts[2] === "src") {
|
|
36
|
+
const fixedPath = `@simplysm/${parts[1]}`;
|
|
37
|
+
context.report({
|
|
38
|
+
node: sourceNode,
|
|
39
|
+
messageId: "noSubpathImport",
|
|
40
|
+
data: {
|
|
41
|
+
pkg: parts[1],
|
|
42
|
+
importPath,
|
|
43
|
+
},
|
|
44
|
+
fix(fixer) {
|
|
45
|
+
const quote = sourceNode.raw[0];
|
|
46
|
+
return fixer.replaceText(sourceNode, `${quote}${fixedPath}${quote}`);
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
// 정적 import: import { x } from '...'
|
|
54
|
+
ImportDeclaration(node) {
|
|
55
|
+
checkAndReport(node.source, node.source.value);
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
// 동적 import: import('...')
|
|
59
|
+
ImportExpression(node) {
|
|
60
|
+
if (node.source.type !== AST_NODE_TYPES.Literal) return;
|
|
61
|
+
const importPath = node.source.value;
|
|
62
|
+
if (typeof importPath !== "string") return;
|
|
63
|
+
checkAndReport(node.source, importPath);
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
// re-export: export { x } from '...'
|
|
67
|
+
ExportNamedDeclaration(node) {
|
|
68
|
+
if (!node.source) return;
|
|
69
|
+
checkAndReport(node.source, node.source.value);
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
// re-export all: export * from '...'
|
|
73
|
+
ExportAllDeclaration(node) {
|
|
74
|
+
checkAndReport(node.source, node.source.value);
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { AST_NODE_TYPES, ASTUtils, type TSESTree } from "@typescript-eslint/utils";
|
|
2
|
+
import { createRule } from "../utils/create-rule";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `@simplysm/core-common`의 `NotImplementedError` 사용을 감지하여 경고하는 ESLint 규칙
|
|
6
|
+
*
|
|
7
|
+
* @remarks
|
|
8
|
+
* 이 규칙은 `@simplysm/core-common`에서 import된 `NotImplementedError`를 `new`로 생성하는 코드를 감지한다.
|
|
9
|
+
* 미구현 코드가 프로덕션에 포함되는 것을 방지한다.
|
|
10
|
+
*
|
|
11
|
+
* 지원하는 import 형태:
|
|
12
|
+
* - named import: `import { NotImplementedError } from "@simplysm/core-common"`
|
|
13
|
+
* - aliased import: `import { NotImplementedError as NIE } from "@simplysm/core-common"`
|
|
14
|
+
* - namespace import: `import * as CC from "@simplysm/core-common"` → `new CC.NotImplementedError()`
|
|
15
|
+
*
|
|
16
|
+
* 동적 import(`await import(...)`)는 감지하지 않는다.
|
|
17
|
+
*/
|
|
18
|
+
export default createRule({
|
|
19
|
+
name: "ts-no-throw-not-implemented-error",
|
|
20
|
+
meta: {
|
|
21
|
+
type: "suggestion",
|
|
22
|
+
docs: {
|
|
23
|
+
description: "'NotImplementedError' 사용 경고",
|
|
24
|
+
},
|
|
25
|
+
schema: [],
|
|
26
|
+
messages: {
|
|
27
|
+
noThrowNotImplementedError: "{{text}}",
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
defaultOptions: [],
|
|
31
|
+
create(context) {
|
|
32
|
+
/**
|
|
33
|
+
* identifier가 @simplysm/core-common에서 import된 것인지 확인
|
|
34
|
+
* @param identifier - 확인할 identifier
|
|
35
|
+
* @param expectedImportedName - named import인 경우 확인할 원본 이름 (namespace import는 undefined)
|
|
36
|
+
* @returns import 출처가 @simplysm/core-common이면 true, 아니면 false
|
|
37
|
+
*/
|
|
38
|
+
function isImportedFromSimplysm(
|
|
39
|
+
identifier: TSESTree.Identifier,
|
|
40
|
+
expectedImportedName: string | undefined,
|
|
41
|
+
): boolean {
|
|
42
|
+
const scope = context.sourceCode.getScope(identifier);
|
|
43
|
+
const variable = ASTUtils.findVariable(scope, identifier.name);
|
|
44
|
+
if (!variable) return false;
|
|
45
|
+
|
|
46
|
+
for (const def of variable.defs) {
|
|
47
|
+
if (def.type !== "ImportBinding") continue;
|
|
48
|
+
if (def.parent.type !== AST_NODE_TYPES.ImportDeclaration) continue;
|
|
49
|
+
if (def.parent.source.value !== "@simplysm/core-common") continue;
|
|
50
|
+
|
|
51
|
+
// named/aliased import: import { NotImplementedError } 또는 import { NotImplementedError as NIE }
|
|
52
|
+
if (def.node.type === AST_NODE_TYPES.ImportSpecifier && expectedImportedName != null) {
|
|
53
|
+
const imported = def.node.imported;
|
|
54
|
+
if (imported.type === AST_NODE_TYPES.Identifier && imported.name === expectedImportedName) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// namespace import: import * as CC
|
|
60
|
+
if (def.node.type === AST_NODE_TYPES.ImportNamespaceSpecifier && expectedImportedName == null) {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
NewExpression(node: TSESTree.NewExpression) {
|
|
70
|
+
let shouldReport = false;
|
|
71
|
+
|
|
72
|
+
// Case 1: new NotImplementedError() 또는 new NIE() (named/aliased import)
|
|
73
|
+
if (node.callee.type === AST_NODE_TYPES.Identifier) {
|
|
74
|
+
shouldReport = isImportedFromSimplysm(node.callee, "NotImplementedError");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Case 2: new CC.NotImplementedError() (namespace import)
|
|
78
|
+
else if (
|
|
79
|
+
node.callee.type === AST_NODE_TYPES.MemberExpression &&
|
|
80
|
+
node.callee.property.type === AST_NODE_TYPES.Identifier &&
|
|
81
|
+
node.callee.property.name === "NotImplementedError" &&
|
|
82
|
+
node.callee.object.type === AST_NODE_TYPES.Identifier
|
|
83
|
+
) {
|
|
84
|
+
shouldReport = isImportedFromSimplysm(node.callee.object, undefined);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!shouldReport) return;
|
|
88
|
+
|
|
89
|
+
let msg = "미구현";
|
|
90
|
+
const firstArg = node.arguments.at(0);
|
|
91
|
+
if (firstArg?.type === AST_NODE_TYPES.Literal && typeof firstArg.value === "string" && firstArg.value !== "") {
|
|
92
|
+
msg = firstArg.value;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
context.report({
|
|
96
|
+
node,
|
|
97
|
+
messageId: "noThrowNotImplementedError",
|
|
98
|
+
data: { text: msg },
|
|
99
|
+
});
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
},
|
|
103
|
+
});
|