@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.
Files changed (37) hide show
  1. package/README.md +522 -0
  2. package/dist/eslint-plugin.d.ts +15 -0
  3. package/dist/eslint-plugin.d.ts.map +1 -0
  4. package/dist/eslint-plugin.js +14 -0
  5. package/dist/eslint-plugin.js.map +6 -0
  6. package/dist/eslint-recommended.d.ts +3 -0
  7. package/dist/eslint-recommended.d.ts.map +1 -0
  8. package/dist/eslint-recommended.js +259 -0
  9. package/dist/eslint-recommended.js.map +6 -0
  10. package/dist/rules/no-hard-private.d.ts +15 -0
  11. package/dist/rules/no-hard-private.d.ts.map +1 -0
  12. package/dist/rules/no-hard-private.js +95 -0
  13. package/dist/rules/no-hard-private.js.map +6 -0
  14. package/dist/rules/no-subpath-imports-from-simplysm.d.ts +14 -0
  15. package/dist/rules/no-subpath-imports-from-simplysm.d.ts.map +1 -0
  16. package/dist/rules/no-subpath-imports-from-simplysm.js +64 -0
  17. package/dist/rules/no-subpath-imports-from-simplysm.js.map +6 -0
  18. package/dist/rules/ts-no-throw-not-implemented-error.d.ts +19 -0
  19. package/dist/rules/ts-no-throw-not-implemented-error.d.ts.map +1 -0
  20. package/dist/rules/ts-no-throw-not-implemented-error.js +63 -0
  21. package/dist/rules/ts-no-throw-not-implemented-error.js.map +6 -0
  22. package/dist/stylelint-recommended.d.ts +13 -0
  23. package/dist/stylelint-recommended.d.ts.map +1 -0
  24. package/dist/stylelint-recommended.js +20 -0
  25. package/dist/stylelint-recommended.js.map +6 -0
  26. package/dist/utils/create-rule.d.ts +22 -0
  27. package/dist/utils/create-rule.d.ts.map +1 -0
  28. package/dist/utils/create-rule.js +8 -0
  29. package/dist/utils/create-rule.js.map +6 -0
  30. package/package.json +52 -0
  31. package/src/eslint-plugin.ts +11 -0
  32. package/src/eslint-recommended.ts +274 -0
  33. package/src/rules/no-hard-private.ts +136 -0
  34. package/src/rules/no-subpath-imports-from-simplysm.ts +78 -0
  35. package/src/rules/ts-no-throw-not-implemented-error.ts +103 -0
  36. package/src/stylelint-recommended.ts +16 -0
  37. 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
@@ -0,0 +1,6 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/utils/create-rule.ts"],
4
+ "mappings": "AAAA,SAAS,mBAAmB;AAmBrB,MAAM,aAAa,YAAY;AAAA,EACpC,CAAC,SAAS,sFAAsF,IAAI;AACtG;",
5
+ "names": []
6
+ }
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
+ });