dittory 0.0.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 warabi1062
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,73 @@
1
+ # dittory
2
+
3
+ Reactコンポーネントや関数の引数使用状況を静的解析し、**常に同じ値が渡されているパラメータ**を検出するCLIツールです。
4
+
5
+ ## 用途
6
+
7
+ - 不要なパラメータの発見(デフォルト値化の候補)
8
+ - コードベースのリファクタリング支援
9
+ - APIの簡素化
10
+
11
+ ## インストール
12
+
13
+ ```bash
14
+ npm install -g dittory
15
+ ```
16
+
17
+ ## 使い方
18
+
19
+ ```bash
20
+ # srcディレクトリを解析(デフォルト)
21
+ dittory
22
+
23
+ # 特定のディレクトリを解析
24
+ dittory ./path/to/src
25
+
26
+ # 最小使用回数を指定(デフォルト: 2)
27
+ dittory --min=3
28
+
29
+ # 解析対象を指定
30
+ dittory --target=components # Reactコンポーネントのみ
31
+ dittory --target=functions # 関数・クラスメソッドのみ
32
+ dittory --target=all # 両方(デフォルト)
33
+
34
+ # ヘルプ
35
+ dittory --help
36
+ ```
37
+
38
+ ## 出力例
39
+
40
+ ```
41
+ 解析対象ディレクトリ: ./src
42
+ 最小使用箇所数: 2
43
+ 解析対象: all
44
+
45
+ 1. exportされたコンポーネントを収集中...
46
+ → 15個のコンポーネントを検出
47
+
48
+ === 常に同じ値が渡されているprops ===
49
+
50
+ コンポーネント: Button
51
+ 定義: src/components/Button.tsx
52
+ prop: variant
53
+ 常に渡される値: "primary"
54
+ 使用箇所: 5箇所
55
+ - src/pages/Home.tsx:23
56
+ - src/pages/About.tsx:45
57
+ ...
58
+ ```
59
+
60
+ ## 検出対象
61
+
62
+ - **Reactコンポーネント**: JSX要素として使用されているコンポーネントのprops
63
+ - **関数**: exportされた関数の引数
64
+ - **クラスメソッド**: exportされたクラスのメソッド引数
65
+
66
+ ## 要件
67
+
68
+ - Node.js >= 18
69
+ - プロジェクトに `tsconfig.json` が必要
70
+
71
+ ## ライセンス
72
+
73
+ MIT
@@ -0,0 +1,532 @@
1
+ import { Node, SyntaxKind } from "ts-morph";
2
+
3
+ //#region src/components/getProps.ts
4
+ /**
5
+ * コンポーネントのprops定義を取得する
6
+ *
7
+ * 関数の第一パラメータの型情報からpropsを抽出する。
8
+ * 「Props」などの命名規則に依存せず、TypeScriptの型システムから直接取得するため、
9
+ * どのような命名でもpropsを正確に取得できる。
10
+ *
11
+ * 対応パターン:
12
+ * - function Component(props: Props)
13
+ * - const Component = (props: Props) => ...
14
+ * - React.forwardRef((props, ref) => ...)
15
+ * - React.memo((props) => ...)
16
+ */
17
+ function getProps(declaration) {
18
+ let propsParam;
19
+ if (Node.isFunctionDeclaration(declaration)) {
20
+ const params = declaration.getParameters();
21
+ if (params.length > 0) propsParam = params[0];
22
+ } else if (Node.isVariableDeclaration(declaration)) {
23
+ const initializer = declaration.getInitializer();
24
+ if (initializer) {
25
+ if (Node.isArrowFunction(initializer) || Node.isFunctionExpression(initializer)) {
26
+ const params = initializer.getParameters();
27
+ if (params.length > 0) propsParam = params[0];
28
+ } else if (Node.isCallExpression(initializer)) {
29
+ const args = initializer.getArguments();
30
+ for (const arg of args) if (Node.isArrowFunction(arg) || Node.isFunctionExpression(arg)) {
31
+ const params = arg.getParameters();
32
+ if (params.length > 0) {
33
+ propsParam = params[0];
34
+ break;
35
+ }
36
+ }
37
+ }
38
+ }
39
+ }
40
+ if (!propsParam) return [];
41
+ return extractPropsFromType(propsParam.getType());
42
+ }
43
+ /**
44
+ * 型からprops定義を抽出する
45
+ *
46
+ * Mapを使用する理由:
47
+ * - 交差型(A & B)の場合、同じprop名が複数の型に存在する可能性がある
48
+ * - 最初に見つかった定義を優先し、重複を排除するためにMapを使用
49
+ * - 抽出後にindexを付与してDefinition配列として返す
50
+ */
51
+ function extractPropsFromType(type) {
52
+ const propsMap = /* @__PURE__ */ new Map();
53
+ collectPropsFromType(type, propsMap);
54
+ return Array.from(propsMap.values()).map((prop, index) => ({
55
+ ...prop,
56
+ index
57
+ }));
58
+ }
59
+ /**
60
+ * 型からpropsを収集する(交差型の場合は再帰的に処理)
61
+ */
62
+ function collectPropsFromType(type, propsMap) {
63
+ if (type.isIntersection()) {
64
+ for (const intersectionType of type.getIntersectionTypes()) collectPropsFromType(intersectionType, propsMap);
65
+ return;
66
+ }
67
+ const properties = type.getProperties();
68
+ for (const prop of properties) {
69
+ const propName = prop.getName();
70
+ const declarations = prop.getDeclarations();
71
+ let isOptional = false;
72
+ for (const decl of declarations) if (Node.isPropertySignature(decl) && decl.hasQuestionToken()) {
73
+ isOptional = true;
74
+ break;
75
+ }
76
+ if (!propsMap.has(propName)) propsMap.set(propName, {
77
+ name: propName,
78
+ required: !isOptional
79
+ });
80
+ }
81
+ }
82
+
83
+ //#endregion
84
+ //#region src/extraction/resolveExpressionValue.ts
85
+ /**
86
+ * 引数が渡されなかった場合を表す特別な値
87
+ * 必須/任意を問わず、引数未指定の使用箇所を統一的に扱うために使用
88
+ */
89
+ const UNDEFINED_VALUE = "undefined";
90
+ /**
91
+ * 関数型の値を表すプレフィックス
92
+ * コールバック関数など、関数が渡された場合は使用箇所ごとにユニークな値として扱う
93
+ * これにより、同じコールバック関数を渡していても「定数」として検出されない
94
+ */
95
+ const FUNCTION_VALUE_PREFIX = "[function]";
96
+ /**
97
+ * 式の実際の値を解決する
98
+ *
99
+ * 異なるファイルで同じenum値やリテラル値を使用している場合でも、
100
+ * 同一の値として認識できるよう、値を正規化して文字列表現で返す。
101
+ * 同名だが異なる定義(別ファイルの同名enum等)を区別するため、
102
+ * 必要に応じてファイルパスを含めた識別子を返す。
103
+ */
104
+ function resolveExpressionValue(expression) {
105
+ const type = expression.getType();
106
+ if (type.getCallSignatures().length > 0) {
107
+ const sourceFile = expression.getSourceFile();
108
+ const line = expression.getStartLineNumber();
109
+ return `${FUNCTION_VALUE_PREFIX}${sourceFile.getFilePath()}:${line}`;
110
+ }
111
+ if (Node.isPropertyAccessExpression(expression)) {
112
+ const decl = expression.getSymbol()?.getDeclarations()[0];
113
+ if (decl && Node.isEnumMember(decl)) {
114
+ const enumDecl = decl.getParent();
115
+ if (Node.isEnumDeclaration(enumDecl)) {
116
+ const filePath = enumDecl.getSourceFile().getFilePath();
117
+ const enumName = enumDecl.getName();
118
+ const memberName = decl.getName();
119
+ const value = decl.getValue();
120
+ return `${filePath}:${enumName}.${memberName}=${JSON.stringify(value)}`;
121
+ }
122
+ }
123
+ }
124
+ if (Node.isIdentifier(expression)) {
125
+ if (expression.getText() === "undefined") return UNDEFINED_VALUE;
126
+ const decl = expression.getSymbol()?.getDeclarations()[0];
127
+ if (decl && Node.isVariableDeclaration(decl)) {
128
+ const initializer = decl.getInitializer();
129
+ return initializer ? initializer.getText().replace(/\s+/g, " ") : `${decl.getSourceFile().getFilePath()}:${expression.getText()}`;
130
+ }
131
+ if (decl) return `${decl.getSourceFile().getFilePath()}:${expression.getText()}`;
132
+ }
133
+ if (type.isStringLiteral() || type.isNumberLiteral()) return JSON.stringify(type.getLiteralValue());
134
+ if (type.isBooleanLiteral()) return type.getText();
135
+ return expression.getText();
136
+ }
137
+
138
+ //#endregion
139
+ //#region src/extraction/flattenObjectExpression.ts
140
+ /**
141
+ * オブジェクトリテラルを再帰的に解析し、フラットなkey-valueペアを返す
142
+ *
143
+ * @param expression - 解析対象の式ノード
144
+ * @param prefix - キー名のプレフィックス(ネストしたプロパティの親パスを表す)
145
+ * @returns フラット化されたkey-valueペアの配列
146
+ *
147
+ * @example
148
+ * // { a: { b: 1, c: 2 } } → [{ key: "prefix.a.b", value: "1" }, { key: "prefix.a.c", value: "2" }]
149
+ */
150
+ function flattenObjectExpression(expression, prefix) {
151
+ if (!Node.isObjectLiteralExpression(expression)) return [{
152
+ key: prefix,
153
+ value: resolveExpressionValue(expression)
154
+ }];
155
+ return expression.getProperties().flatMap((property) => {
156
+ if (Node.isPropertyAssignment(property)) {
157
+ const propertyName = property.getName();
158
+ const nestedPrefix = prefix ? `${prefix}.${propertyName}` : propertyName;
159
+ const initializer = property.getInitializer();
160
+ return initializer ? flattenObjectExpression(initializer, nestedPrefix) : [];
161
+ }
162
+ if (Node.isShorthandPropertyAssignment(property)) {
163
+ const propertyName = property.getName();
164
+ return [{
165
+ key: prefix ? `${prefix}.${propertyName}` : propertyName,
166
+ value: resolveExpressionValue(property.getNameNode())
167
+ }];
168
+ }
169
+ return [];
170
+ });
171
+ }
172
+
173
+ //#endregion
174
+ //#region src/extraction/extractUsages.ts
175
+ /**
176
+ * 使用状況を抽出するユーティリティクラス
177
+ */
178
+ var ExtractUsages = class {
179
+ /**
180
+ * 関数呼び出しから引数の使用状況を抽出する
181
+ *
182
+ * オブジェクトリテラルの場合は再帰的にフラット化し、
183
+ * 各プロパティを「引数名.プロパティ名」形式で記録する。
184
+ *
185
+ * @param callExpression - 関数呼び出しノード
186
+ * @param callable - 対象の関数情報
187
+ * @returns 引数使用状況の配列
188
+ */
189
+ static fromCall(callExpression, callable) {
190
+ const usages = [];
191
+ const sourceFile = callExpression.getSourceFile();
192
+ const args = callExpression.getArguments();
193
+ for (const param of callable.definitions) {
194
+ const arg = args[param.index];
195
+ if (!arg) {
196
+ usages.push({
197
+ name: param.name,
198
+ value: UNDEFINED_VALUE,
199
+ usageFilePath: sourceFile.getFilePath(),
200
+ usageLine: callExpression.getStartLineNumber()
201
+ });
202
+ continue;
203
+ }
204
+ for (const { key, value } of flattenObjectExpression(arg, param.name)) usages.push({
205
+ name: key,
206
+ value,
207
+ usageFilePath: sourceFile.getFilePath(),
208
+ usageLine: arg.getStartLineNumber()
209
+ });
210
+ }
211
+ return usages;
212
+ }
213
+ /**
214
+ * JSX要素からprops使用状況を抽出する
215
+ *
216
+ * @param element - JSX要素ノード
217
+ * @param definitions - props定義の配列
218
+ * @returns props使用状況の配列
219
+ */
220
+ static fromJsxElement(element, definitions) {
221
+ const usages = [];
222
+ const sourceFile = element.getSourceFile();
223
+ const attributeMap = /* @__PURE__ */ new Map();
224
+ for (const attr of element.getAttributes()) if (Node.isJsxAttribute(attr)) attributeMap.set(attr.getNameNode().getText(), attr);
225
+ for (const prop of definitions) {
226
+ const attr = attributeMap.get(prop.name);
227
+ if (!attr) {
228
+ usages.push({
229
+ name: prop.name,
230
+ value: UNDEFINED_VALUE,
231
+ usageFilePath: sourceFile.getFilePath(),
232
+ usageLine: element.getStartLineNumber()
233
+ });
234
+ continue;
235
+ }
236
+ const initializer = attr.getInitializer();
237
+ if (!initializer) usages.push({
238
+ name: prop.name,
239
+ value: "true",
240
+ usageFilePath: sourceFile.getFilePath(),
241
+ usageLine: attr.getStartLineNumber()
242
+ });
243
+ else if (Node.isJsxExpression(initializer)) {
244
+ const expression = initializer.getExpression();
245
+ if (!expression) continue;
246
+ for (const { key, value } of flattenObjectExpression(expression, prop.name)) usages.push({
247
+ name: key,
248
+ value,
249
+ usageFilePath: sourceFile.getFilePath(),
250
+ usageLine: attr.getStartLineNumber()
251
+ });
252
+ } else usages.push({
253
+ name: prop.name,
254
+ value: initializer.getText(),
255
+ usageFilePath: sourceFile.getFilePath(),
256
+ usageLine: attr.getStartLineNumber()
257
+ });
258
+ }
259
+ return usages;
260
+ }
261
+ };
262
+
263
+ //#endregion
264
+ //#region src/source/fileFilters.ts
265
+ /**
266
+ * ファイルパスがテストファイルまたはStorybookファイルかどうかを判定する
267
+ * - 拡張子が .test.* / .spec.* / .stories.* のファイル
268
+ * - __tests__ / __stories__ フォルダ内のファイル
269
+ */
270
+ function isTestOrStorybookFile(filePath) {
271
+ if (/\.(test|spec|stories)\.(ts|tsx|js|jsx)$/.test(filePath)) return true;
272
+ if (/\b__tests__\b|\b__stories__\b/.test(filePath)) return true;
273
+ return false;
274
+ }
275
+
276
+ //#endregion
277
+ //#region src/utils/getSingleValueFromSet.ts
278
+ /**
279
+ * Setから唯一の値を安全に取得する
280
+ */
281
+ function getSingleValueFromSet(values) {
282
+ if (values.size !== 1) throw new Error(`Expected exactly 1 value, got ${values.size}`);
283
+ const [firstValue] = Array.from(values);
284
+ return firstValue;
285
+ }
286
+
287
+ //#endregion
288
+ //#region src/analyzer/baseAnalyzer.ts
289
+ /**
290
+ * 分析処理の基底クラス
291
+ */
292
+ var BaseAnalyzer = class {
293
+ shouldExcludeFile;
294
+ minUsages;
295
+ constructor(options = {}) {
296
+ this.shouldExcludeFile = options.shouldExcludeFile ?? isTestOrStorybookFile;
297
+ this.minUsages = options.minUsages ?? 2;
298
+ }
299
+ /**
300
+ * メイン分析処理
301
+ *
302
+ * @param declarations - 事前分類済みの宣言配列
303
+ */
304
+ analyze(declarations) {
305
+ const exported = this.collect(declarations);
306
+ const groupedMap = this.createGroupedMap(exported);
307
+ return {
308
+ constants: this.extractConstants(groupedMap),
309
+ exported
310
+ };
311
+ }
312
+ /**
313
+ * 使用状況をグループ化したマップを作成
314
+ */
315
+ createGroupedMap(exported) {
316
+ const groupedMap = /* @__PURE__ */ new Map();
317
+ for (const item of exported) {
318
+ let fileMap = groupedMap.get(item.sourceFilePath);
319
+ if (!fileMap) {
320
+ fileMap = /* @__PURE__ */ new Map();
321
+ groupedMap.set(item.sourceFilePath, fileMap);
322
+ }
323
+ const paramMap = /* @__PURE__ */ new Map();
324
+ for (const [paramName, usages] of Object.entries(item.usages)) {
325
+ const values = /* @__PURE__ */ new Set();
326
+ for (const usage of usages) values.add(usage.value);
327
+ paramMap.set(paramName, {
328
+ values,
329
+ usages
330
+ });
331
+ }
332
+ fileMap.set(item.name, paramMap);
333
+ }
334
+ return groupedMap;
335
+ }
336
+ /**
337
+ * 常に同じ値が渡されているパラメータを抽出
338
+ */
339
+ extractConstants(groupedMap) {
340
+ const result = [];
341
+ for (const [sourceFile, targetMap] of groupedMap) for (const [targetName, paramMap] of targetMap) for (const [paramName, usageData] of paramMap) {
342
+ if (!(usageData.usages.length >= this.minUsages && usageData.values.size === 1)) continue;
343
+ const value = getSingleValueFromSet(usageData.values);
344
+ if (value.startsWith(FUNCTION_VALUE_PREFIX)) continue;
345
+ result.push({
346
+ targetName,
347
+ targetSourceFile: sourceFile,
348
+ paramName,
349
+ value,
350
+ usages: usageData.usages
351
+ });
352
+ }
353
+ return result;
354
+ }
355
+ };
356
+
357
+ //#endregion
358
+ //#region src/analyzer/componentAnalyzer.ts
359
+ /**
360
+ * Reactコンポーネントのprops分析を行うAnalyzer
361
+ *
362
+ * exportされたReactコンポーネントを収集し、各コンポーネントのprops使用状況を分析する。
363
+ * 常に同じ値が渡されているpropsを検出し、定数として報告する。
364
+ *
365
+ * @example
366
+ * ```ts
367
+ * const analyzer = new ComponentAnalyzer({ minUsages: 2 });
368
+ * const result = analyzer.analyze(sourceFiles);
369
+ * console.log(result.constants);
370
+ * ```
371
+ */
372
+ var ComponentAnalyzer = class extends BaseAnalyzer {
373
+ constructor(options = {}) {
374
+ super(options);
375
+ }
376
+ /**
377
+ * 事前分類済みの宣言からReactコンポーネントを収集する
378
+ *
379
+ * @param declarations - 事前分類済みの宣言配列
380
+ * @returns exportされたコンポーネントとその使用状況の配列
381
+ */
382
+ collect(declarations) {
383
+ const exportedComponents = [];
384
+ for (const classified of declarations) {
385
+ const { exportName, sourceFile, declaration } = classified;
386
+ if (!Node.isFunctionDeclaration(declaration) && !Node.isVariableDeclaration(declaration)) continue;
387
+ const nameNode = declaration.getNameNode();
388
+ if (!nameNode || !Node.isIdentifier(nameNode)) continue;
389
+ const references = nameNode.findReferences().flatMap((referencedSymbol) => referencedSymbol.getReferences()).filter((ref) => !this.shouldExcludeFile(ref.getSourceFile().getFilePath()));
390
+ const props = getProps(declaration);
391
+ const component = {
392
+ name: exportName,
393
+ sourceFilePath: sourceFile.getFilePath(),
394
+ definitions: props,
395
+ declaration,
396
+ usages: {}
397
+ };
398
+ const groupedUsages = {};
399
+ for (const reference of references) {
400
+ const refNode = reference.getNode();
401
+ const parent = refNode.getParent();
402
+ if (!parent) continue;
403
+ const jsxElement = parent.asKind(SyntaxKind.JsxOpeningElement) ?? parent.asKind(SyntaxKind.JsxSelfClosingElement);
404
+ if (!jsxElement) continue;
405
+ if (jsxElement.getTagNameNode() !== refNode) continue;
406
+ const usages = ExtractUsages.fromJsxElement(jsxElement, component.definitions);
407
+ for (const usage of usages) {
408
+ if (!groupedUsages[usage.name]) groupedUsages[usage.name] = [];
409
+ groupedUsages[usage.name].push(usage);
410
+ }
411
+ }
412
+ component.usages = groupedUsages;
413
+ exportedComponents.push(component);
414
+ }
415
+ return exportedComponents;
416
+ }
417
+ };
418
+
419
+ //#endregion
420
+ //#region src/components/isReactComponent.ts
421
+ /**
422
+ * 宣言がReactコンポーネントかどうかを判定する
423
+ * - 関数がJSXを返しているかチェック
424
+ * - React.FC型注釈を持っているかチェック
425
+ */
426
+ function isReactComponent(declaration) {
427
+ if (Node.isFunctionDeclaration(declaration)) return containsJsx(declaration);
428
+ if (Node.isVariableDeclaration(declaration)) {
429
+ const initializer = declaration.getInitializer();
430
+ if (!initializer) return false;
431
+ if (Node.isArrowFunction(initializer) || Node.isFunctionExpression(initializer)) return containsJsx(initializer);
432
+ if (Node.isCallExpression(initializer)) {
433
+ const args = initializer.getArguments();
434
+ for (const arg of args) if (Node.isArrowFunction(arg) || Node.isFunctionExpression(arg)) {
435
+ if (containsJsx(arg)) return true;
436
+ }
437
+ }
438
+ }
439
+ return false;
440
+ }
441
+ /**
442
+ * ノード内にJSX要素が含まれているかチェック
443
+ * 効率化: 1回のトラバースで全てのJSX要素をチェック
444
+ */
445
+ function containsJsx(node) {
446
+ let hasJsx = false;
447
+ node.forEachDescendant((descendant) => {
448
+ if (Node.isJsxElement(descendant) || Node.isJsxSelfClosingElement(descendant) || Node.isJsxFragment(descendant)) {
449
+ hasJsx = true;
450
+ return true;
451
+ }
452
+ });
453
+ return hasJsx;
454
+ }
455
+
456
+ //#endregion
457
+ //#region src/source/classifyDeclarations.ts
458
+ /**
459
+ * ソースファイルからexportされた関数/コンポーネント/クラス宣言を収集し、
460
+ * 種別(react/function/class)を事前に分類する
461
+ *
462
+ * @param sourceFiles - 分析対象のソースファイル配列
463
+ * @returns 分類済みの宣言配列
464
+ */
465
+ function classifyDeclarations(sourceFiles) {
466
+ const results = [];
467
+ for (const sourceFile of sourceFiles) {
468
+ const exportedDecls = sourceFile.getExportedDeclarations();
469
+ for (const [exportName, declarations] of exportedDecls) {
470
+ const funcDecl = declarations.find((decl) => isFunctionLike(decl));
471
+ if (funcDecl) {
472
+ if (Node.isFunctionDeclaration(funcDecl) || Node.isVariableDeclaration(funcDecl)) {
473
+ const type = isReactComponent(funcDecl) ? "react" : "function";
474
+ results.push({
475
+ exportName,
476
+ sourceFile,
477
+ declaration: funcDecl,
478
+ type
479
+ });
480
+ }
481
+ continue;
482
+ }
483
+ const classDecl = declarations.find((decl) => Node.isClassDeclaration(decl));
484
+ if (classDecl && Node.isClassDeclaration(classDecl)) results.push({
485
+ exportName,
486
+ sourceFile,
487
+ declaration: classDecl,
488
+ type: "class"
489
+ });
490
+ }
491
+ }
492
+ return results;
493
+ }
494
+ /**
495
+ * 宣言が関数的なもの(関数宣言、アロー関数、関数式)かどうかを判定
496
+ */
497
+ function isFunctionLike(declaration) {
498
+ if (Node.isFunctionDeclaration(declaration)) return true;
499
+ if (Node.isVariableDeclaration(declaration)) {
500
+ const initializer = declaration.getInitializer();
501
+ if (!initializer) return false;
502
+ return Node.isArrowFunction(initializer) || Node.isFunctionExpression(initializer) || Node.isCallExpression(initializer);
503
+ }
504
+ return false;
505
+ }
506
+
507
+ //#endregion
508
+ //#region src/analyzeProps.ts
509
+ /**
510
+ * コンポーネントのprops使用状況を解析し、常に同じ値が渡されているpropsを検出する
511
+ *
512
+ * @param sourceFiles - 解析対象のソースファイル配列
513
+ * @param options - オプション設定
514
+ * @returns 解析結果(定数props、統計情報、exportされたコンポーネント)
515
+ *
516
+ * @example
517
+ * const project = new Project();
518
+ * project.addSourceFilesAtPaths("src/**\/*.tsx");
519
+ * const result = analyzePropsCore(project.getSourceFiles());
520
+ */
521
+ function analyzePropsCore(sourceFiles, options = {}) {
522
+ const { shouldExcludeFile = isTestOrStorybookFile, minUsages = 2 } = options;
523
+ const components = classifyDeclarations(sourceFiles).filter((decl) => decl.type === "react");
524
+ return new ComponentAnalyzer({
525
+ shouldExcludeFile,
526
+ minUsages
527
+ }).analyze(components);
528
+ }
529
+
530
+ //#endregion
531
+ export { ExtractUsages as a, isTestOrStorybookFile as i, classifyDeclarations as n, BaseAnalyzer as r, analyzePropsCore as t };
532
+ //# sourceMappingURL=analyzeProps-YWnY-Mf7.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analyzeProps-YWnY-Mf7.mjs","names":["propsParam: Node | undefined","usages: Usage[]","groupedMap: GroupedMap","result: Constant[]","exportedComponents: Exported[]","component: Exported","groupedUsages: Record<string, Usage[]>","results: ClassifiedDeclaration[]","type: DeclarationType"],"sources":["../src/components/getProps.ts","../src/extraction/resolveExpressionValue.ts","../src/extraction/flattenObjectExpression.ts","../src/extraction/extractUsages.ts","../src/source/fileFilters.ts","../src/utils/getSingleValueFromSet.ts","../src/analyzer/baseAnalyzer.ts","../src/analyzer/componentAnalyzer.ts","../src/components/isReactComponent.ts","../src/source/classifyDeclarations.ts","../src/analyzeProps.ts"],"sourcesContent":["import { Node, type Type } from \"ts-morph\";\nimport type { Definition } from \"@/types\";\n\n/**\n * コンポーネントのprops定義を取得する\n *\n * 関数の第一パラメータの型情報からpropsを抽出する。\n * 「Props」などの命名規則に依存せず、TypeScriptの型システムから直接取得するため、\n * どのような命名でもpropsを正確に取得できる。\n *\n * 対応パターン:\n * - function Component(props: Props)\n * - const Component = (props: Props) => ...\n * - React.forwardRef((props, ref) => ...)\n * - React.memo((props) => ...)\n */\nexport function getProps(declaration: Node): Definition[] {\n // 関数宣言、アロー関数、関数式から第一パラメータ(props)を取得\n let propsParam: Node | undefined;\n\n if (Node.isFunctionDeclaration(declaration)) {\n const params = declaration.getParameters();\n if (params.length > 0) {\n propsParam = params[0];\n }\n } else if (Node.isVariableDeclaration(declaration)) {\n const initializer = declaration.getInitializer();\n if (initializer) {\n if (\n Node.isArrowFunction(initializer) ||\n Node.isFunctionExpression(initializer)\n ) {\n const params = initializer.getParameters();\n if (params.length > 0) {\n propsParam = params[0];\n }\n } else if (Node.isCallExpression(initializer)) {\n // React.forwardRef, React.memo などのラッパー関数の場合\n const args = initializer.getArguments();\n for (const arg of args) {\n if (Node.isArrowFunction(arg) || Node.isFunctionExpression(arg)) {\n const params = arg.getParameters();\n if (params.length > 0) {\n propsParam = params[0];\n break;\n }\n }\n }\n }\n }\n }\n\n if (!propsParam) {\n return [];\n }\n\n // パラメータの型情報を取得\n const propsType = propsParam.getType();\n return extractPropsFromType(propsType);\n}\n\n/**\n * 型からprops定義を抽出する\n *\n * Mapを使用する理由:\n * - 交差型(A & B)の場合、同じprop名が複数の型に存在する可能性がある\n * - 最初に見つかった定義を優先し、重複を排除するためにMapを使用\n * - 抽出後にindexを付与してDefinition配列として返す\n */\nfunction extractPropsFromType(type: Type): Definition[] {\n const propsMap = new Map<string, Omit<Definition, \"index\">>();\n\n collectPropsFromType(type, propsMap);\n\n return Array.from(propsMap.values()).map((prop, index) => ({\n ...prop,\n index,\n }));\n}\n\n/**\n * 型からpropsを収集する(交差型の場合は再帰的に処理)\n */\nfunction collectPropsFromType(\n type: Type,\n propsMap: Map<string, Omit<Definition, \"index\">>,\n): void {\n if (type.isIntersection()) {\n for (const intersectionType of type.getIntersectionTypes()) {\n collectPropsFromType(intersectionType, propsMap);\n }\n return;\n }\n\n const properties = type.getProperties();\n for (const prop of properties) {\n const propName = prop.getName();\n const declarations = prop.getDeclarations();\n\n let isOptional = false;\n for (const decl of declarations) {\n if (Node.isPropertySignature(decl) && decl.hasQuestionToken()) {\n isOptional = true;\n break;\n }\n }\n\n // 同じ名前のpropが既に存在する場合は上書きしない(最初に見つかった定義を優先)\n if (!propsMap.has(propName)) {\n propsMap.set(propName, {\n name: propName,\n required: !isOptional,\n });\n }\n }\n}\n","import { Node } from \"ts-morph\";\n\n/**\n * 引数が渡されなかった場合を表す特別な値\n * 必須/任意を問わず、引数未指定の使用箇所を統一的に扱うために使用\n */\nexport const UNDEFINED_VALUE = \"undefined\";\n\n/**\n * 関数型の値を表すプレフィックス\n * コールバック関数など、関数が渡された場合は使用箇所ごとにユニークな値として扱う\n * これにより、同じコールバック関数を渡していても「定数」として検出されない\n */\nexport const FUNCTION_VALUE_PREFIX = \"[function]\";\n\n/**\n * 式の実際の値を解決する\n *\n * 異なるファイルで同じenum値やリテラル値を使用している場合でも、\n * 同一の値として認識できるよう、値を正規化して文字列表現で返す。\n * 同名だが異なる定義(別ファイルの同名enum等)を区別するため、\n * 必要に応じてファイルパスを含めた識別子を返す。\n */\nexport function resolveExpressionValue(expression: Node): string {\n // 関数型の場合は定数として扱わない。\n // 同じ関数参照でも使用箇所ごとに異なる値として扱うことで、\n // コールバック関数が「常に同じ値」として誤検出されるのを防ぐ\n const type = expression.getType();\n if (type.getCallSignatures().length > 0) {\n const sourceFile = expression.getSourceFile();\n const line = expression.getStartLineNumber();\n return `${FUNCTION_VALUE_PREFIX}${sourceFile.getFilePath()}:${line}`;\n }\n\n // PropertyAccessExpression (例: Status.Active) の場合\n // enum参照を正確に識別するために、ファイルパスを含めた完全修飾名を返す\n if (Node.isPropertyAccessExpression(expression)) {\n const symbol = expression.getSymbol();\n const decl = symbol?.getDeclarations()[0];\n\n // enum memberの場合は、同名enumの誤認識を防ぐためファイルパスを含める\n if (decl && Node.isEnumMember(decl)) {\n const enumDecl = decl.getParent();\n if (Node.isEnumDeclaration(enumDecl)) {\n const filePath = enumDecl.getSourceFile().getFilePath();\n const enumName = enumDecl.getName();\n const memberName = decl.getName();\n const value = decl.getValue();\n // 例: \"/path/to/file.ts:Status.Active=0\"\n return `${filePath}:${enumName}.${memberName}=${JSON.stringify(value)}`;\n }\n }\n }\n\n // Identifier (変数参照) の場合\n // 変数の定義元を辿って実際の値を解決する\n if (Node.isIdentifier(expression)) {\n if (expression.getText() === \"undefined\") {\n return UNDEFINED_VALUE;\n }\n\n const symbol = expression.getSymbol();\n const decl = symbol?.getDeclarations()[0];\n\n if (decl && Node.isVariableDeclaration(decl)) {\n const initializer = decl.getInitializer();\n // 初期化子がある場合はその値を使用(空白は正規化して比較しやすくする)\n // 初期化子がない場合は、ファイルパス + 変数名で一意に識別\n return initializer\n ? initializer.getText().replace(/\\s+/g, \" \")\n : `${decl.getSourceFile().getFilePath()}:${expression.getText()}`;\n }\n\n // パラメータや分割代入など、VariableDeclaration以外の宣言の場合\n // ファイルパス + 変数名で識別する\n if (decl) {\n return `${decl.getSourceFile().getFilePath()}:${expression.getText()}`;\n }\n }\n\n // リテラル型の場合は型情報から値を取得\n // JSON.stringifyで文字列と数値を区別できる形式にする(\"foo\" vs 42)\n if (type.isStringLiteral() || type.isNumberLiteral()) {\n return JSON.stringify(type.getLiteralValue());\n }\n\n // booleanリテラルの場合はgetLiteralValue()がundefinedを返すためgetText()を使用\n if (type.isBooleanLiteral()) {\n return type.getText();\n }\n\n // 上記のいずれにも該当しない場合(オブジェクト、配列、テンプレートリテラル等)\n // ソースコードのテキストをそのまま返す\n return expression.getText();\n}\n","import { Node } from \"ts-morph\";\nimport { resolveExpressionValue } from \"@/extraction/resolveExpressionValue\";\n\nexport type FlattenedValue = { key: string; value: string };\n\n/**\n * オブジェクトリテラルを再帰的に解析し、フラットなkey-valueペアを返す\n *\n * @param expression - 解析対象の式ノード\n * @param prefix - キー名のプレフィックス(ネストしたプロパティの親パスを表す)\n * @returns フラット化されたkey-valueペアの配列\n *\n * @example\n * // { a: { b: 1, c: 2 } } → [{ key: \"prefix.a.b\", value: \"1\" }, { key: \"prefix.a.c\", value: \"2\" }]\n */\nexport function flattenObjectExpression(\n expression: Node,\n prefix: string,\n): FlattenedValue[] {\n if (!Node.isObjectLiteralExpression(expression)) {\n // オブジェクトリテラル以外の場合は単一の値として返す\n return [{ key: prefix, value: resolveExpressionValue(expression) }];\n }\n\n return expression.getProperties().flatMap((property) => {\n if (Node.isPropertyAssignment(property)) {\n const propertyName = property.getName();\n const nestedPrefix = prefix ? `${prefix}.${propertyName}` : propertyName;\n const initializer = property.getInitializer();\n\n return initializer\n ? flattenObjectExpression(initializer, nestedPrefix)\n : [];\n }\n\n if (Node.isShorthandPropertyAssignment(property)) {\n // { foo } のような省略形\n const propertyName = property.getName();\n const nestedPrefix = prefix ? `${prefix}.${propertyName}` : propertyName;\n return [\n {\n key: nestedPrefix,\n value: resolveExpressionValue(property.getNameNode()),\n },\n ];\n }\n\n return [];\n });\n}\n","import {\n type CallExpression,\n type JsxAttribute,\n type JsxOpeningElement,\n type JsxSelfClosingElement,\n Node,\n} from \"ts-morph\";\nimport type { Definition, Exported, Usage } from \"@/types\";\nimport { flattenObjectExpression } from \"./flattenObjectExpression\";\nimport { UNDEFINED_VALUE } from \"./resolveExpressionValue\";\n\n/**\n * 使用状況を抽出するユーティリティクラス\n */\nexport class ExtractUsages {\n /**\n * 関数呼び出しから引数の使用状況を抽出する\n *\n * オブジェクトリテラルの場合は再帰的にフラット化し、\n * 各プロパティを「引数名.プロパティ名」形式で記録する。\n *\n * @param callExpression - 関数呼び出しノード\n * @param callable - 対象の関数情報\n * @returns 引数使用状況の配列\n */\n static fromCall(callExpression: CallExpression, callable: Exported): Usage[] {\n const usages: Usage[] = [];\n const sourceFile = callExpression.getSourceFile();\n const args = callExpression.getArguments();\n\n for (const param of callable.definitions) {\n const arg = args[param.index];\n\n if (!arg) {\n // 引数が渡されていない場合はundefinedとして記録\n usages.push({\n name: param.name,\n value: UNDEFINED_VALUE,\n usageFilePath: sourceFile.getFilePath(),\n usageLine: callExpression.getStartLineNumber(),\n });\n continue;\n }\n\n // オブジェクトリテラルの場合は再帰的にフラット化\n for (const { key, value } of flattenObjectExpression(arg, param.name)) {\n usages.push({\n name: key,\n value,\n usageFilePath: sourceFile.getFilePath(),\n usageLine: arg.getStartLineNumber(),\n });\n }\n }\n\n return usages;\n }\n\n /**\n * JSX要素からprops使用状況を抽出する\n *\n * @param element - JSX要素ノード\n * @param definitions - props定義の配列\n * @returns props使用状況の配列\n */\n static fromJsxElement(\n element: JsxOpeningElement | JsxSelfClosingElement,\n definitions: Definition[],\n ): Usage[] {\n const usages: Usage[] = [];\n const sourceFile = element.getSourceFile();\n\n // JSX属性をMapに変換\n const attributeMap = new Map<string, JsxAttribute>();\n for (const attr of element.getAttributes()) {\n if (Node.isJsxAttribute(attr)) {\n attributeMap.set(attr.getNameNode().getText(), attr);\n }\n }\n\n // definitionsをループして処理\n for (const prop of definitions) {\n const attr = attributeMap.get(prop.name);\n\n if (!attr) {\n // 渡されていない場合(required/optional問わず記録)\n usages.push({\n name: prop.name,\n value: UNDEFINED_VALUE,\n usageFilePath: sourceFile.getFilePath(),\n usageLine: element.getStartLineNumber(),\n });\n continue;\n }\n\n // 属性が渡されている場合、値を抽出\n const initializer = attr.getInitializer();\n\n if (!initializer) {\n // boolean shorthand (例: <Component disabled />)\n usages.push({\n name: prop.name,\n value: \"true\",\n usageFilePath: sourceFile.getFilePath(),\n usageLine: attr.getStartLineNumber(),\n });\n } else if (Node.isJsxExpression(initializer)) {\n // {expression} 形式\n const expression = initializer.getExpression();\n if (!expression) {\n continue;\n }\n for (const { key, value } of flattenObjectExpression(\n expression,\n prop.name,\n )) {\n usages.push({\n name: key,\n value,\n usageFilePath: sourceFile.getFilePath(),\n usageLine: attr.getStartLineNumber(),\n });\n }\n } else {\n // \"string\" 形式\n usages.push({\n name: prop.name,\n value: initializer.getText(),\n usageFilePath: sourceFile.getFilePath(),\n usageLine: attr.getStartLineNumber(),\n });\n }\n }\n\n return usages;\n }\n}\n","/**\n * ファイルパスがテストファイルまたはStorybookファイルかどうかを判定する\n * - 拡張子が .test.* / .spec.* / .stories.* のファイル\n * - __tests__ / __stories__ フォルダ内のファイル\n */\nexport function isTestOrStorybookFile(filePath: string): boolean {\n // 拡張子ベースの判定\n if (/\\.(test|spec|stories)\\.(ts|tsx|js|jsx)$/.test(filePath)) {\n return true;\n }\n\n // フォルダ名ベースの判定\n if (/\\b__tests__\\b|\\b__stories__\\b/.test(filePath)) {\n return true;\n }\n\n return false;\n}\n","/**\n * Setから唯一の値を安全に取得する\n */\nexport function getSingleValueFromSet(values: Set<string>): string {\n if (values.size !== 1) {\n throw new Error(`Expected exactly 1 value, got ${values.size}`);\n }\n const [firstValue] = Array.from(values);\n return firstValue;\n}\n","import { FUNCTION_VALUE_PREFIX } from \"@/extraction/resolveExpressionValue\";\nimport { isTestOrStorybookFile } from \"@/source/fileFilters\";\nimport type {\n AnalysisResult,\n AnalyzerOptions,\n ClassifiedDeclaration,\n Constant,\n Exported,\n FileFilter,\n Usage,\n} from \"@/types\";\nimport { getSingleValueFromSet } from \"@/utils/getSingleValueFromSet\";\n\n/**\n * 使用データのグループ\n * values.size === 1 の場合、そのパラメータは「定数」として検出される\n */\ninterface UsageData {\n values: Set<string>;\n usages: Usage[];\n}\n\n/**\n * 使用状況を階層的にグループ化したマップ\n *\n * 3階層の構造で使用状況を整理する:\n * 1. ソースファイルパス: どのファイルで定義された対象か\n * 2. 対象名: 関数名/コンポーネント名\n * 3. パラメータ名: 各パラメータごとの使用状況\n *\n * この構造により、定数検出時に効率的に走査できる。\n */\ntype GroupedMap = Map<string, Map<string, Map<string, UsageData>>>;\n\n/**\n * 分析処理の基底クラス\n */\nexport abstract class BaseAnalyzer {\n protected shouldExcludeFile: FileFilter;\n protected minUsages: number;\n\n constructor(options: AnalyzerOptions = {}) {\n this.shouldExcludeFile = options.shouldExcludeFile ?? isTestOrStorybookFile;\n this.minUsages = options.minUsages ?? 2;\n }\n\n /**\n * メイン分析処理\n *\n * @param declarations - 事前分類済みの宣言配列\n */\n analyze(declarations: ClassifiedDeclaration[]): AnalysisResult {\n // 1. エクスポートされた対象を収集\n const exported = this.collect(declarations);\n\n // 2. 使用状況をグループ化\n const groupedMap = this.createGroupedMap(exported);\n\n // 3. 常に同じ値が渡されているパラメータを抽出\n const constants = this.extractConstants(groupedMap);\n\n // 4. 結果を構築\n return { constants, exported };\n }\n\n /**\n * エクスポートされた対象を収集する(サブクラスで実装)\n *\n * @param declarations - 事前分類済みの宣言配列\n */\n protected abstract collect(declarations: ClassifiedDeclaration[]): Exported[];\n\n /**\n * 使用状況をグループ化したマップを作成\n */\n private createGroupedMap(exported: Exported[]): GroupedMap {\n const groupedMap: GroupedMap = new Map();\n\n for (const item of exported) {\n let fileMap = groupedMap.get(item.sourceFilePath);\n if (!fileMap) {\n fileMap = new Map();\n groupedMap.set(item.sourceFilePath, fileMap);\n }\n\n const paramMap = new Map<string, UsageData>();\n for (const [paramName, usages] of Object.entries(item.usages)) {\n const values = new Set<string>();\n for (const usage of usages) {\n values.add(usage.value);\n }\n paramMap.set(paramName, { values, usages });\n }\n fileMap.set(item.name, paramMap);\n }\n\n return groupedMap;\n }\n\n /**\n * 常に同じ値が渡されているパラメータを抽出\n */\n private extractConstants(groupedMap: GroupedMap): Constant[] {\n const result: Constant[] = [];\n\n for (const [sourceFile, targetMap] of groupedMap) {\n for (const [targetName, paramMap] of targetMap) {\n for (const [paramName, usageData] of paramMap) {\n const isConstant =\n usageData.usages.length >= this.minUsages &&\n usageData.values.size === 1;\n\n if (!isConstant) {\n continue;\n }\n\n const value = getSingleValueFromSet(usageData.values);\n\n // 関数型の値は定数として報告しない\n // (onClickに同じハンドラを渡している等は、デフォルト値化の候補ではない)\n if (value.startsWith(FUNCTION_VALUE_PREFIX)) {\n continue;\n }\n\n result.push({\n targetName,\n targetSourceFile: sourceFile,\n paramName,\n value,\n usages: usageData.usages,\n });\n }\n }\n }\n\n return result;\n }\n}\n","import { Node, SyntaxKind } from \"ts-morph\";\nimport { getProps } from \"@/components/getProps\";\nimport { ExtractUsages } from \"@/extraction/extractUsages\";\nimport type {\n AnalyzerOptions,\n ClassifiedDeclaration,\n Exported,\n Usage,\n} from \"@/types\";\nimport { BaseAnalyzer } from \"./baseAnalyzer\";\n\n/**\n * Reactコンポーネントのprops分析を行うAnalyzer\n *\n * exportされたReactコンポーネントを収集し、各コンポーネントのprops使用状況を分析する。\n * 常に同じ値が渡されているpropsを検出し、定数として報告する。\n *\n * @example\n * ```ts\n * const analyzer = new ComponentAnalyzer({ minUsages: 2 });\n * const result = analyzer.analyze(sourceFiles);\n * console.log(result.constants);\n * ```\n */\nexport class ComponentAnalyzer extends BaseAnalyzer {\n constructor(options: AnalyzerOptions = {}) {\n super(options);\n }\n\n /**\n * 事前分類済みの宣言からReactコンポーネントを収集する\n *\n * @param declarations - 事前分類済みの宣言配列\n * @returns exportされたコンポーネントとその使用状況の配列\n */\n protected collect(declarations: ClassifiedDeclaration[]): Exported[] {\n const exportedComponents: Exported[] = [];\n\n for (const classified of declarations) {\n const { exportName, sourceFile, declaration } = classified;\n\n // FunctionDeclaration または VariableDeclaration のみを処理\n if (\n !Node.isFunctionDeclaration(declaration) &&\n !Node.isVariableDeclaration(declaration)\n ) {\n continue;\n }\n\n // コンポーネントの定義から名前ノードを取得\n const nameNode = declaration.getNameNode();\n if (!nameNode || !Node.isIdentifier(nameNode)) {\n continue;\n }\n\n // 名前ノードから全参照を検索し、除外対象ファイルからの参照をフィルタ\n const references = nameNode\n .findReferences()\n .flatMap((referencedSymbol) => referencedSymbol.getReferences())\n .filter(\n (ref) => !this.shouldExcludeFile(ref.getSourceFile().getFilePath()),\n );\n\n // コンポーネントの宣言からprops定義を取得\n const props = getProps(declaration);\n\n const component: Exported = {\n name: exportName,\n sourceFilePath: sourceFile.getFilePath(),\n definitions: props,\n declaration,\n usages: {},\n };\n\n // 参照からJSX要素を抽出し、usagesをprop名ごとにグループ化\n const groupedUsages: Record<string, Usage[]> = {};\n for (const reference of references) {\n const refNode = reference.getNode();\n const parent = refNode.getParent();\n if (!parent) {\n continue;\n }\n\n // <Component> または <Component /> の形でJSX要素として使われているかチェック\n const jsxElement =\n parent.asKind(SyntaxKind.JsxOpeningElement) ??\n parent.asKind(SyntaxKind.JsxSelfClosingElement);\n\n if (!jsxElement) {\n continue;\n }\n\n // タグ名ノードが参照ノードと一致するか確認\n const tagNameNode = jsxElement.getTagNameNode();\n if (tagNameNode !== refNode) {\n continue;\n }\n\n // JSX要素からprops使用状況を抽出\n const usages = ExtractUsages.fromJsxElement(\n jsxElement,\n component.definitions,\n );\n for (const usage of usages) {\n if (!groupedUsages[usage.name]) {\n groupedUsages[usage.name] = [];\n }\n groupedUsages[usage.name].push(usage);\n }\n }\n\n component.usages = groupedUsages;\n exportedComponents.push(component);\n }\n\n return exportedComponents;\n }\n}\n","import { Node } from \"ts-morph\";\n\n/**\n * 宣言がReactコンポーネントかどうかを判定する\n * - 関数がJSXを返しているかチェック\n * - React.FC型注釈を持っているかチェック\n */\nexport function isReactComponent(declaration: Node): boolean {\n // 関数宣言の場合\n if (Node.isFunctionDeclaration(declaration)) {\n return containsJsx(declaration);\n }\n\n // 変数宣言の場合 (const Button = ...)\n if (Node.isVariableDeclaration(declaration)) {\n const initializer = declaration.getInitializer();\n if (!initializer) return false;\n\n // アロー関数または関数式の場合\n if (\n Node.isArrowFunction(initializer) ||\n Node.isFunctionExpression(initializer)\n ) {\n return containsJsx(initializer);\n }\n\n // React.forwardRef, React.memo などのラッパー関数の場合\n if (Node.isCallExpression(initializer)) {\n const args = initializer.getArguments();\n for (const arg of args) {\n if (Node.isArrowFunction(arg) || Node.isFunctionExpression(arg)) {\n if (containsJsx(arg)) return true;\n }\n }\n }\n }\n\n return false;\n}\n\n/**\n * ノード内にJSX要素が含まれているかチェック\n * 効率化: 1回のトラバースで全てのJSX要素をチェック\n */\nfunction containsJsx(node: Node): boolean {\n let hasJsx = false;\n\n node.forEachDescendant((descendant) => {\n if (\n Node.isJsxElement(descendant) ||\n Node.isJsxSelfClosingElement(descendant) ||\n Node.isJsxFragment(descendant)\n ) {\n hasJsx = true;\n return true; // 早期終了\n }\n return undefined;\n });\n\n return hasJsx;\n}\n","import { Node, type SourceFile } from \"ts-morph\";\nimport { isReactComponent } from \"@/components/isReactComponent\";\nimport type { ClassifiedDeclaration, DeclarationType } from \"@/types\";\n\n/**\n * ソースファイルからexportされた関数/コンポーネント/クラス宣言を収集し、\n * 種別(react/function/class)を事前に分類する\n *\n * @param sourceFiles - 分析対象のソースファイル配列\n * @returns 分類済みの宣言配列\n */\nexport function classifyDeclarations(\n sourceFiles: SourceFile[],\n): ClassifiedDeclaration[] {\n const results: ClassifiedDeclaration[] = [];\n\n for (const sourceFile of sourceFiles) {\n const exportedDecls = sourceFile.getExportedDeclarations();\n\n for (const [exportName, declarations] of exportedDecls) {\n // 関数宣言または変数宣言(アロー関数/関数式)を見つける\n const funcDecl = declarations.find((decl) => isFunctionLike(decl));\n\n if (funcDecl) {\n if (\n Node.isFunctionDeclaration(funcDecl) ||\n Node.isVariableDeclaration(funcDecl)\n ) {\n const type: DeclarationType = isReactComponent(funcDecl)\n ? \"react\"\n : \"function\";\n results.push({\n exportName,\n sourceFile,\n declaration: funcDecl,\n type,\n });\n }\n continue;\n }\n\n // クラス宣言を見つける\n const classDecl = declarations.find((decl) =>\n Node.isClassDeclaration(decl),\n );\n\n if (classDecl && Node.isClassDeclaration(classDecl)) {\n results.push({\n exportName,\n sourceFile,\n declaration: classDecl,\n type: \"class\",\n });\n }\n }\n }\n\n return results;\n}\n\n/**\n * 宣言が関数的なもの(関数宣言、アロー関数、関数式)かどうかを判定\n */\nfunction isFunctionLike(declaration: Node): boolean {\n if (Node.isFunctionDeclaration(declaration)) {\n return true;\n }\n\n if (Node.isVariableDeclaration(declaration)) {\n const initializer = declaration.getInitializer();\n if (!initializer) {\n return false;\n }\n\n return (\n Node.isArrowFunction(initializer) ||\n Node.isFunctionExpression(initializer) ||\n Node.isCallExpression(initializer) // React.forwardRef, React.memo など\n );\n }\n\n return false;\n}\n","import type { SourceFile } from \"ts-morph\";\nimport { ComponentAnalyzer } from \"@/analyzer/componentAnalyzer\";\nimport { classifyDeclarations } from \"@/source/classifyDeclarations\";\nimport { isTestOrStorybookFile } from \"@/source/fileFilters\";\nimport type { AnalysisResult, FileFilter } from \"@/types\";\n\ninterface AnalyzePropsOptions {\n shouldExcludeFile?: FileFilter;\n minUsages?: number;\n}\n\n/**\n * コンポーネントのprops使用状況を解析し、常に同じ値が渡されているpropsを検出する\n *\n * @param sourceFiles - 解析対象のソースファイル配列\n * @param options - オプション設定\n * @returns 解析結果(定数props、統計情報、exportされたコンポーネント)\n *\n * @example\n * const project = new Project();\n * project.addSourceFilesAtPaths(\"src/**\\/*.tsx\");\n * const result = analyzePropsCore(project.getSourceFiles());\n */\nexport function analyzePropsCore(\n sourceFiles: SourceFile[],\n options: AnalyzePropsOptions = {},\n): AnalysisResult {\n const { shouldExcludeFile = isTestOrStorybookFile, minUsages = 2 } = options;\n\n // 宣言を事前分類し、Reactコンポーネントのみ抽出\n const declarations = classifyDeclarations(sourceFiles);\n const components = declarations.filter((decl) => decl.type === \"react\");\n\n const analyzer = new ComponentAnalyzer({\n shouldExcludeFile,\n minUsages,\n });\n\n return analyzer.analyze(components);\n}\n"],"mappings":"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"}
package/dist/cli.d.mts ADDED
@@ -0,0 +1 @@
1
+ export { };
package/dist/cli.mjs ADDED
@@ -0,0 +1,427 @@
1
+ #!/usr/bin/env node
2
+ import { a as ExtractUsages, i as isTestOrStorybookFile, n as classifyDeclarations, r as BaseAnalyzer, t as analyzePropsCore } from "./analyzeProps-YWnY-Mf7.mjs";
3
+ import { Node, Project, SyntaxKind } from "ts-morph";
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+
7
+ //#region src/analyzer/classMethodAnalyzer.ts
8
+ /**
9
+ * クラスメソッドの引数分析を行うAnalyzer
10
+ *
11
+ * exportされたクラスのメソッド(static/instance)を収集し、
12
+ * 各メソッドの引数使用状況を分析する。
13
+ * 常に同じ値が渡されている引数を検出し、定数として報告する。
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * const analyzer = new ClassMethodAnalyzer({ minUsages: 2 });
18
+ * const result = analyzer.analyze(declarations);
19
+ * console.log(result.constants);
20
+ * ```
21
+ */
22
+ var ClassMethodAnalyzer = class extends BaseAnalyzer {
23
+ constructor(options = {}) {
24
+ super(options);
25
+ }
26
+ /**
27
+ * 事前分類済みの宣言からクラスメソッドを収集する
28
+ *
29
+ * @param declarations - 事前分類済みの宣言配列(type: "class")
30
+ * @returns クラスメソッドとその使用状況の配列(名前は「ClassName.methodName」形式)
31
+ */
32
+ collect(declarations) {
33
+ const results = [];
34
+ for (const classified of declarations) {
35
+ const { exportName, sourceFile, declaration } = classified;
36
+ if (!Node.isClassDeclaration(declaration)) continue;
37
+ const methods = declaration.getMethods();
38
+ for (const method of methods) {
39
+ const methodName = method.getName();
40
+ const parameters = this.getParameters(method);
41
+ const callable = {
42
+ name: `${exportName}.${methodName}`,
43
+ sourceFilePath: sourceFile.getFilePath(),
44
+ definitions: parameters,
45
+ declaration: method,
46
+ usages: {}
47
+ };
48
+ const nameNode = method.getNameNode();
49
+ if (!Node.isIdentifier(nameNode)) continue;
50
+ const references = nameNode.findReferences().flatMap((referencedSymbol) => referencedSymbol.getReferences()).filter((ref) => !this.shouldExcludeFile(ref.getSourceFile().getFilePath()));
51
+ const groupedUsages = {};
52
+ for (const reference of references) {
53
+ const propertyAccess = reference.getNode().getParent();
54
+ if (!propertyAccess || !Node.isPropertyAccessExpression(propertyAccess)) continue;
55
+ const callExpression = propertyAccess.getParent();
56
+ if (!callExpression || !Node.isCallExpression(callExpression)) continue;
57
+ if (callExpression.getExpression() !== propertyAccess) continue;
58
+ const usages = ExtractUsages.fromCall(callExpression, callable);
59
+ for (const usage of usages) {
60
+ if (!groupedUsages[usage.name]) groupedUsages[usage.name] = [];
61
+ groupedUsages[usage.name].push(usage);
62
+ }
63
+ }
64
+ callable.usages = groupedUsages;
65
+ results.push(callable);
66
+ }
67
+ }
68
+ return results;
69
+ }
70
+ /**
71
+ * メソッドのパラメータ定義を取得する
72
+ */
73
+ getParameters(method) {
74
+ return this.extractParameterDeclarations(method).map((param, index) => ({
75
+ name: param.getName(),
76
+ index,
77
+ required: !param.hasQuestionToken() && !param.hasInitializer()
78
+ }));
79
+ }
80
+ /**
81
+ * メソッド宣言からParameterDeclarationの配列を抽出する
82
+ */
83
+ extractParameterDeclarations(method) {
84
+ if (Node.isMethodDeclaration(method)) return method.getParameters();
85
+ return [];
86
+ }
87
+ };
88
+
89
+ //#endregion
90
+ //#region src/analyzer/functionAnalyzer.ts
91
+ /**
92
+ * 関数の引数分析を行うAnalyzer
93
+ *
94
+ * exportされた関数を収集し、各関数の引数使用状況を分析する。
95
+ * 常に同じ値が渡されている引数を検出し、定数として報告する。
96
+ *
97
+ * @example
98
+ * ```ts
99
+ * const analyzer = new FunctionAnalyzer({ minUsages: 2 });
100
+ * const result = analyzer.analyze(declarations);
101
+ * console.log(result.constants);
102
+ * ```
103
+ */
104
+ var FunctionAnalyzer = class extends BaseAnalyzer {
105
+ constructor(options = {}) {
106
+ super(options);
107
+ }
108
+ /**
109
+ * 事前分類済みの宣言から関数を収集する
110
+ *
111
+ * @param declarations - 事前分類済みの宣言配列(type: "function")
112
+ * @returns exportされた関数とその使用状況の配列
113
+ */
114
+ collect(declarations) {
115
+ const results = [];
116
+ for (const classified of declarations) {
117
+ const { exportName, sourceFile, declaration } = classified;
118
+ if (!Node.isFunctionDeclaration(declaration) && !Node.isVariableDeclaration(declaration)) continue;
119
+ const nameNode = declaration.getNameNode();
120
+ if (!nameNode || !Node.isIdentifier(nameNode)) continue;
121
+ const references = nameNode.findReferences().flatMap((referencedSymbol) => referencedSymbol.getReferences()).filter((ref) => !this.shouldExcludeFile(ref.getSourceFile().getFilePath()));
122
+ const parameters = this.getParameters(declaration);
123
+ const callable = {
124
+ name: exportName,
125
+ sourceFilePath: sourceFile.getFilePath(),
126
+ definitions: parameters,
127
+ declaration,
128
+ usages: {}
129
+ };
130
+ const groupedUsages = {};
131
+ for (const reference of references) {
132
+ const refNode = reference.getNode();
133
+ const parent = refNode.getParent();
134
+ if (!parent) continue;
135
+ const callExpression = parent.asKind(SyntaxKind.CallExpression);
136
+ if (!callExpression) continue;
137
+ if (callExpression.getExpression() !== refNode) continue;
138
+ const usages = ExtractUsages.fromCall(callExpression, callable);
139
+ for (const usage of usages) {
140
+ if (!groupedUsages[usage.name]) groupedUsages[usage.name] = [];
141
+ groupedUsages[usage.name].push(usage);
142
+ }
143
+ }
144
+ callable.usages = groupedUsages;
145
+ results.push(callable);
146
+ }
147
+ return results;
148
+ }
149
+ /**
150
+ * 関数のパラメータ定義を取得する
151
+ */
152
+ getParameters(declaration) {
153
+ return this.extractParameterDeclarations(declaration).map((param, index) => ({
154
+ name: param.getName(),
155
+ index,
156
+ required: !param.hasQuestionToken() && !param.hasInitializer()
157
+ }));
158
+ }
159
+ /**
160
+ * 宣言からParameterDeclarationの配列を抽出する
161
+ */
162
+ extractParameterDeclarations(declaration) {
163
+ if (Node.isFunctionDeclaration(declaration)) return declaration.getParameters();
164
+ if (Node.isVariableDeclaration(declaration)) {
165
+ const initializer = declaration.getInitializer();
166
+ if (initializer) {
167
+ if (Node.isArrowFunction(initializer) || Node.isFunctionExpression(initializer)) return initializer.getParameters();
168
+ }
169
+ }
170
+ return [];
171
+ }
172
+ };
173
+
174
+ //#endregion
175
+ //#region src/analyzeFunctions.ts
176
+ /**
177
+ * 関数・クラスメソッドの引数使用状況を解析し、常に同じ値が渡されている引数を検出する
178
+ *
179
+ * @param sourceFiles - 解析対象のソースファイル配列
180
+ * @param options - オプション設定
181
+ * @returns 解析結果(定数引数、統計情報、exportされた関数・メソッド)
182
+ *
183
+ * @example
184
+ * const project = new Project();
185
+ * project.addSourceFilesAtPaths("src/**\/*.ts");
186
+ * const result = analyzeFunctionsCore(project.getSourceFiles());
187
+ */
188
+ function analyzeFunctionsCore(sourceFiles, options = {}) {
189
+ const { shouldExcludeFile = isTestOrStorybookFile, minUsages = 2 } = options;
190
+ const declarations = classifyDeclarations(sourceFiles);
191
+ const functions = declarations.filter((decl) => decl.type === "function");
192
+ const classes = declarations.filter((decl) => decl.type === "class");
193
+ const analyzerOptions = {
194
+ shouldExcludeFile,
195
+ minUsages
196
+ };
197
+ const functionResult = new FunctionAnalyzer(analyzerOptions).analyze(functions);
198
+ const classMethodResult = new ClassMethodAnalyzer(analyzerOptions).analyze(classes);
199
+ return {
200
+ constants: [...functionResult.constants, ...classMethodResult.constants],
201
+ exported: [...functionResult.exported, ...classMethodResult.exported]
202
+ };
203
+ }
204
+
205
+ //#endregion
206
+ //#region src/cli/parseCliOptions.ts
207
+ var CliValidationError = class extends Error {
208
+ constructor(message) {
209
+ super(message);
210
+ this.name = "CliValidationError";
211
+ }
212
+ };
213
+ const VALID_TARGETS = [
214
+ "all",
215
+ "components",
216
+ "functions"
217
+ ];
218
+ /** 不明なオプションの検出に使用 */
219
+ const VALID_OPTIONS = [
220
+ "--min",
221
+ "--target",
222
+ "--help"
223
+ ];
224
+ /**
225
+ * CLIオプションをパースする
226
+ *
227
+ * @throws {CliValidationError} オプションが無効な場合
228
+ */
229
+ function parseCliOptions(args) {
230
+ let targetDir = path.join(process.cwd(), "src");
231
+ let minUsages = 2;
232
+ let target = "all";
233
+ let showHelp = false;
234
+ for (const arg of args) {
235
+ if (arg === "--help") {
236
+ showHelp = true;
237
+ continue;
238
+ }
239
+ if (arg.startsWith("--min=")) {
240
+ const valueStr = arg.slice(6);
241
+ const value = Number.parseInt(valueStr, 10);
242
+ if (valueStr === "" || Number.isNaN(value)) throw new CliValidationError(`--min の値が無効です: "${valueStr}" (数値を指定してください)`);
243
+ if (value < 1) throw new CliValidationError(`--min の値は1以上である必要があります: ${value}`);
244
+ minUsages = value;
245
+ } else if (arg.startsWith("--target=")) {
246
+ const value = arg.slice(9);
247
+ if (!VALID_TARGETS.includes(value)) throw new CliValidationError(`--target の値が無効です: "${value}" (有効な値: ${VALID_TARGETS.join(", ")})`);
248
+ target = value;
249
+ } else if (arg.startsWith("--")) {
250
+ const optionName = arg.split("=")[0];
251
+ if (!VALID_OPTIONS.includes(optionName)) throw new CliValidationError(`不明なオプション: ${optionName}`);
252
+ } else targetDir = arg;
253
+ }
254
+ return {
255
+ targetDir,
256
+ minUsages,
257
+ target,
258
+ showHelp
259
+ };
260
+ }
261
+ /**
262
+ * 対象ディレクトリの存在を検証する
263
+ *
264
+ * @throws {CliValidationError} ディレクトリが存在しない、またはディレクトリでない場合
265
+ */
266
+ function validateTargetDir(targetDir) {
267
+ if (!fs.existsSync(targetDir)) throw new CliValidationError(`ディレクトリが存在しません: ${targetDir}`);
268
+ if (!fs.statSync(targetDir).isDirectory()) throw new CliValidationError(`指定されたパスはディレクトリではありません: ${targetDir}`);
269
+ }
270
+ /**
271
+ * ヘルプメッセージを取得する
272
+ */
273
+ function getHelpMessage() {
274
+ return `
275
+ Usage: dittory [options] [directory]
276
+
277
+ Options:
278
+ --min=<number> 最小使用箇所数 (デフォルト: 2)
279
+ --target=<mode> 解析対象: all, components, functions (デフォルト: all)
280
+ --help このヘルプを表示
281
+
282
+ Arguments:
283
+ directory 解析対象ディレクトリ (デフォルト: ./src)
284
+ `;
285
+ }
286
+
287
+ //#endregion
288
+ //#region src/output/printAnalysisResult.ts
289
+ /**
290
+ * exportされたコンポーネントの一覧を出力
291
+ */
292
+ function printExportedComponents(exported) {
293
+ const lines = [
294
+ "1. exportされたコンポーネントを収集中...",
295
+ ` → ${exported.length}個のコンポーネントを検出`,
296
+ ...exported.map((comp) => ` - ${comp.name} (${path.relative(process.cwd(), comp.sourceFilePath)})`),
297
+ ""
298
+ ];
299
+ console.log(lines.join("\n"));
300
+ }
301
+ /**
302
+ * 常に同じ値が渡されているpropsを出力
303
+ */
304
+ function printConstantProps(constants) {
305
+ console.log("=== 常に同じ値が渡されているprops ===\n");
306
+ if (constants.length === 0) {
307
+ console.log("常に同じ値が渡されているpropsは見つかりませんでした。");
308
+ return;
309
+ }
310
+ for (const prop of constants) {
311
+ const relativeComponentPath = path.relative(process.cwd(), prop.targetSourceFile);
312
+ console.log(`コンポーネント: ${prop.targetName}`);
313
+ console.log(` 定義: ${relativeComponentPath}`);
314
+ console.log(` prop: ${prop.paramName}`);
315
+ console.log(` 常に渡される値: ${prop.value}`);
316
+ console.log(` 使用箇所: ${prop.usages.length}箇所`);
317
+ for (const usage of prop.usages) {
318
+ const relativePath = path.relative(process.cwd(), usage.usageFilePath);
319
+ console.log(` - ${relativePath}:${usage.usageLine}`);
320
+ }
321
+ console.log("");
322
+ }
323
+ }
324
+ /**
325
+ * 解析結果を全て出力
326
+ */
327
+ function printAnalysisResult(result) {
328
+ printExportedComponents(result.exported);
329
+ printConstantProps(result.constants);
330
+ }
331
+ /**
332
+ * exportされた関数の一覧を出力
333
+ */
334
+ function printExportedFunctions(exported) {
335
+ const lines = [
336
+ "2. exportされた関数を収集中...",
337
+ ` → ${exported.length}個の関数を検出`,
338
+ ...exported.map((fn) => ` - ${fn.name} (${path.relative(process.cwd(), fn.sourceFilePath)})`),
339
+ ""
340
+ ];
341
+ console.log(lines.join("\n"));
342
+ }
343
+ /**
344
+ * 常に同じ値が渡されている引数を出力
345
+ */
346
+ function printConstantArguments(constants) {
347
+ console.log("=== 常に同じ値が渡されている引数 ===\n");
348
+ if (constants.length === 0) {
349
+ console.log("常に同じ値が渡されている引数は見つかりませんでした。");
350
+ return;
351
+ }
352
+ for (const arg of constants) {
353
+ const relativeFunctionPath = path.relative(process.cwd(), arg.targetSourceFile);
354
+ console.log(`関数: ${arg.targetName}`);
355
+ console.log(` 定義: ${relativeFunctionPath}`);
356
+ console.log(` 引数: ${arg.paramName}`);
357
+ console.log(` 常に渡される値: ${arg.value}`);
358
+ console.log(` 使用箇所: ${arg.usages.length}箇所`);
359
+ for (const usage of arg.usages) {
360
+ const relativePath = path.relative(process.cwd(), usage.usageFilePath);
361
+ console.log(` - ${relativePath}:${usage.usageLine}`);
362
+ }
363
+ console.log("");
364
+ }
365
+ }
366
+ /**
367
+ * 関数解析結果を全て出力
368
+ */
369
+ function printFunctionAnalysisResult(result) {
370
+ printExportedFunctions(result.exported);
371
+ printConstantArguments(result.constants);
372
+ }
373
+
374
+ //#endregion
375
+ //#region src/source/createFilteredSourceFiles.ts
376
+ /**
377
+ * プロジェクトを初期化し、フィルタリングされたソースファイルを取得する
378
+ */
379
+ function createFilteredSourceFiles(targetDir, shouldExcludeFile = isTestOrStorybookFile) {
380
+ const project = new Project({
381
+ tsConfigFilePath: path.join(process.cwd(), "tsconfig.json"),
382
+ skipAddingFilesFromTsConfig: true
383
+ });
384
+ project.addSourceFilesAtPaths(`${targetDir}/**/*.{ts,tsx,js,jsx}`);
385
+ return project.getSourceFiles().filter((sourceFile) => !shouldExcludeFile(sourceFile.getFilePath()));
386
+ }
387
+
388
+ //#endregion
389
+ //#region src/cli.ts
390
+ /**
391
+ * エラーメッセージを表示してプロセスを終了する
392
+ */
393
+ function exitWithError(message) {
394
+ console.error(`エラー: ${message}`);
395
+ process.exit(1);
396
+ }
397
+ function main() {
398
+ let options;
399
+ try {
400
+ options = parseCliOptions(process.argv.slice(2));
401
+ } catch (error) {
402
+ if (error instanceof CliValidationError) exitWithError(error.message);
403
+ throw error;
404
+ }
405
+ const { targetDir, minUsages, target, showHelp } = options;
406
+ if (showHelp) {
407
+ console.log(getHelpMessage());
408
+ process.exit(0);
409
+ }
410
+ try {
411
+ validateTargetDir(targetDir);
412
+ } catch (error) {
413
+ if (error instanceof CliValidationError) exitWithError(error.message);
414
+ throw error;
415
+ }
416
+ console.log(`解析対象ディレクトリ: ${targetDir}`);
417
+ console.log(`最小使用箇所数: ${minUsages}`);
418
+ console.log(`解析対象: ${target}\n`);
419
+ const sourceFilesToAnalyze = createFilteredSourceFiles(targetDir);
420
+ if (target === "all" || target === "components") printAnalysisResult(analyzePropsCore(sourceFilesToAnalyze, { minUsages }));
421
+ if (target === "all" || target === "functions") printFunctionAnalysisResult(analyzeFunctionsCore(sourceFilesToAnalyze, { minUsages }));
422
+ }
423
+ main();
424
+
425
+ //#endregion
426
+ export { };
427
+ //# sourceMappingURL=cli.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.mjs","names":["results: Exported[]","callable: Exported","groupedUsages: Record<string, Usage[]>","results: Exported[]","callable: Exported","groupedUsages: Record<string, Usage[]>","VALID_TARGETS: readonly AnalyzeMode[]","VALID_OPTIONS: readonly string[]","target: AnalyzeMode","options: CliOptions"],"sources":["../src/analyzer/classMethodAnalyzer.ts","../src/analyzer/functionAnalyzer.ts","../src/analyzeFunctions.ts","../src/cli/parseCliOptions.ts","../src/output/printAnalysisResult.ts","../src/source/createFilteredSourceFiles.ts","../src/cli.ts"],"sourcesContent":["import { Node, type ParameterDeclaration } from \"ts-morph\";\nimport { ExtractUsages } from \"@/extraction/extractUsages\";\nimport type {\n AnalyzerOptions,\n ClassifiedDeclaration,\n Definition,\n Exported,\n Usage,\n} from \"@/types\";\nimport { BaseAnalyzer } from \"./baseAnalyzer\";\n\n/**\n * クラスメソッドの引数分析を行うAnalyzer\n *\n * exportされたクラスのメソッド(static/instance)を収集し、\n * 各メソッドの引数使用状況を分析する。\n * 常に同じ値が渡されている引数を検出し、定数として報告する。\n *\n * @example\n * ```ts\n * const analyzer = new ClassMethodAnalyzer({ minUsages: 2 });\n * const result = analyzer.analyze(declarations);\n * console.log(result.constants);\n * ```\n */\nexport class ClassMethodAnalyzer extends BaseAnalyzer {\n constructor(options: AnalyzerOptions = {}) {\n super(options);\n }\n\n /**\n * 事前分類済みの宣言からクラスメソッドを収集する\n *\n * @param declarations - 事前分類済みの宣言配列(type: \"class\")\n * @returns クラスメソッドとその使用状況の配列(名前は「ClassName.methodName」形式)\n */\n protected collect(declarations: ClassifiedDeclaration[]): Exported[] {\n const results: Exported[] = [];\n\n for (const classified of declarations) {\n const { exportName, sourceFile, declaration } = classified;\n\n if (!Node.isClassDeclaration(declaration)) {\n continue;\n }\n\n const methods = declaration.getMethods();\n\n for (const method of methods) {\n const methodName = method.getName();\n const parameters = this.getParameters(method);\n\n const callable: Exported = {\n name: `${exportName}.${methodName}`,\n sourceFilePath: sourceFile.getFilePath(),\n definitions: parameters,\n declaration: method,\n usages: {},\n };\n\n // メソッド名から参照を検索\n const nameNode = method.getNameNode();\n if (!Node.isIdentifier(nameNode)) {\n continue;\n }\n\n // 名前ノードから全参照を検索し、除外対象ファイルからの参照をフィルタ\n const references = nameNode\n .findReferences()\n .flatMap((referencedSymbol) => referencedSymbol.getReferences())\n .filter(\n (ref) => !this.shouldExcludeFile(ref.getSourceFile().getFilePath()),\n );\n\n // 参照からメソッド呼び出しを抽出し、usagesをパラメータ名ごとにグループ化\n const groupedUsages: Record<string, Usage[]> = {};\n for (const reference of references) {\n const refNode = reference.getNode();\n\n // obj.method の形でPropertyAccessExpressionの一部かチェック\n const propertyAccess = refNode.getParent();\n if (\n !propertyAccess ||\n !Node.isPropertyAccessExpression(propertyAccess)\n ) {\n continue;\n }\n\n // obj.method(...) の形でCallExpressionかチェック\n const callExpression = propertyAccess.getParent();\n if (!callExpression || !Node.isCallExpression(callExpression)) {\n continue;\n }\n\n // 呼び出し対象がPropertyAccessExpressionと一致するか確認\n if (callExpression.getExpression() !== propertyAccess) {\n continue;\n }\n\n // メソッド呼び出しから引数使用状況を抽出\n const usages = ExtractUsages.fromCall(callExpression, callable);\n for (const usage of usages) {\n if (!groupedUsages[usage.name]) {\n groupedUsages[usage.name] = [];\n }\n groupedUsages[usage.name].push(usage);\n }\n }\n\n callable.usages = groupedUsages;\n results.push(callable);\n }\n }\n\n return results;\n }\n\n /**\n * メソッドのパラメータ定義を取得する\n */\n private getParameters(method: Node): Definition[] {\n const params = this.extractParameterDeclarations(method);\n\n return params.map((param, index) => ({\n name: param.getName(),\n index,\n required: !param.hasQuestionToken() && !param.hasInitializer(),\n }));\n }\n\n /**\n * メソッド宣言からParameterDeclarationの配列を抽出する\n */\n private extractParameterDeclarations(method: Node): ParameterDeclaration[] {\n if (Node.isMethodDeclaration(method)) {\n return method.getParameters();\n }\n\n return [];\n }\n}\n","import { Node, type ParameterDeclaration, SyntaxKind } from \"ts-morph\";\nimport { ExtractUsages } from \"@/extraction/extractUsages\";\nimport type {\n AnalyzerOptions,\n ClassifiedDeclaration,\n Definition,\n Exported,\n Usage,\n} from \"@/types\";\nimport { BaseAnalyzer } from \"./baseAnalyzer\";\n\n/**\n * 関数の引数分析を行うAnalyzer\n *\n * exportされた関数を収集し、各関数の引数使用状況を分析する。\n * 常に同じ値が渡されている引数を検出し、定数として報告する。\n *\n * @example\n * ```ts\n * const analyzer = new FunctionAnalyzer({ minUsages: 2 });\n * const result = analyzer.analyze(declarations);\n * console.log(result.constants);\n * ```\n */\nexport class FunctionAnalyzer extends BaseAnalyzer {\n constructor(options: AnalyzerOptions = {}) {\n super(options);\n }\n\n /**\n * 事前分類済みの宣言から関数を収集する\n *\n * @param declarations - 事前分類済みの宣言配列(type: \"function\")\n * @returns exportされた関数とその使用状況の配列\n */\n protected collect(declarations: ClassifiedDeclaration[]): Exported[] {\n const results: Exported[] = [];\n\n for (const classified of declarations) {\n const { exportName, sourceFile, declaration } = classified;\n\n // FunctionDeclaration または VariableDeclaration のみを処理\n if (\n !Node.isFunctionDeclaration(declaration) &&\n !Node.isVariableDeclaration(declaration)\n ) {\n continue;\n }\n\n // 関数の定義から名前ノードを取得\n const nameNode = declaration.getNameNode();\n if (!nameNode || !Node.isIdentifier(nameNode)) {\n continue;\n }\n\n // 名前ノードから全参照を検索し、除外対象ファイルからの参照をフィルタ\n const references = nameNode\n .findReferences()\n .flatMap((referencedSymbol) => referencedSymbol.getReferences())\n .filter(\n (ref) => !this.shouldExcludeFile(ref.getSourceFile().getFilePath()),\n );\n\n // 関数の宣言からパラメータ定義を取得\n const parameters = this.getParameters(declaration);\n\n const callable: Exported = {\n name: exportName,\n sourceFilePath: sourceFile.getFilePath(),\n definitions: parameters,\n declaration,\n usages: {},\n };\n\n // 参照から関数呼び出しを抽出し、usagesをパラメータ名ごとにグループ化\n const groupedUsages: Record<string, Usage[]> = {};\n for (const reference of references) {\n const refNode = reference.getNode();\n const parent = refNode.getParent();\n if (!parent) {\n continue;\n }\n\n // func(...) の形で関数呼び出しとして使われているかチェック\n const callExpression = parent.asKind(SyntaxKind.CallExpression);\n if (!callExpression) {\n continue;\n }\n\n // 呼び出し対象が参照ノードと一致するか確認\n const expression = callExpression.getExpression();\n if (expression !== refNode) {\n continue;\n }\n\n // 関数呼び出しから引数使用状況を抽出\n const usages = ExtractUsages.fromCall(callExpression, callable);\n for (const usage of usages) {\n if (!groupedUsages[usage.name]) {\n groupedUsages[usage.name] = [];\n }\n groupedUsages[usage.name].push(usage);\n }\n }\n\n callable.usages = groupedUsages;\n results.push(callable);\n }\n\n return results;\n }\n\n /**\n * 関数のパラメータ定義を取得する\n */\n private getParameters(declaration: Node): Definition[] {\n const params = this.extractParameterDeclarations(declaration);\n\n return params.map((param, index) => ({\n name: param.getName(),\n index,\n required: !param.hasQuestionToken() && !param.hasInitializer(),\n }));\n }\n\n /**\n * 宣言からParameterDeclarationの配列を抽出する\n */\n private extractParameterDeclarations(\n declaration: Node,\n ): ParameterDeclaration[] {\n if (Node.isFunctionDeclaration(declaration)) {\n return declaration.getParameters();\n }\n\n if (Node.isVariableDeclaration(declaration)) {\n const initializer = declaration.getInitializer();\n if (initializer) {\n if (\n Node.isArrowFunction(initializer) ||\n Node.isFunctionExpression(initializer)\n ) {\n return initializer.getParameters();\n }\n }\n }\n\n return [];\n }\n}\n","import type { SourceFile } from \"ts-morph\";\nimport { ClassMethodAnalyzer } from \"@/analyzer/classMethodAnalyzer\";\nimport { FunctionAnalyzer } from \"@/analyzer/functionAnalyzer\";\nimport { classifyDeclarations } from \"@/source/classifyDeclarations\";\nimport { isTestOrStorybookFile } from \"@/source/fileFilters\";\nimport type { AnalysisResult, FileFilter } from \"@/types\";\n\ninterface AnalyzeFunctionsOptions {\n shouldExcludeFile?: FileFilter;\n minUsages?: number;\n}\n\n/**\n * 関数・クラスメソッドの引数使用状況を解析し、常に同じ値が渡されている引数を検出する\n *\n * @param sourceFiles - 解析対象のソースファイル配列\n * @param options - オプション設定\n * @returns 解析結果(定数引数、統計情報、exportされた関数・メソッド)\n *\n * @example\n * const project = new Project();\n * project.addSourceFilesAtPaths(\"src/**\\/*.ts\");\n * const result = analyzeFunctionsCore(project.getSourceFiles());\n */\nexport function analyzeFunctionsCore(\n sourceFiles: SourceFile[],\n options: AnalyzeFunctionsOptions = {},\n): AnalysisResult {\n const { shouldExcludeFile = isTestOrStorybookFile, minUsages = 2 } = options;\n\n // 宣言を事前分類\n const declarations = classifyDeclarations(sourceFiles);\n const functions = declarations.filter((decl) => decl.type === \"function\");\n const classes = declarations.filter((decl) => decl.type === \"class\");\n\n const analyzerOptions = { shouldExcludeFile, minUsages };\n\n // 関数を分析\n const functionAnalyzer = new FunctionAnalyzer(analyzerOptions);\n const functionResult = functionAnalyzer.analyze(functions);\n\n // クラスメソッドを分析\n const classMethodAnalyzer = new ClassMethodAnalyzer(analyzerOptions);\n const classMethodResult = classMethodAnalyzer.analyze(classes);\n\n // 結果をマージ\n return {\n constants: [...functionResult.constants, ...classMethodResult.constants],\n exported: [...functionResult.exported, ...classMethodResult.exported],\n };\n}\n","import fs from \"node:fs\";\nimport path from \"node:path\";\n\nexport type AnalyzeMode = \"all\" | \"components\" | \"functions\";\n\nexport interface CliOptions {\n targetDir: string;\n minUsages: number;\n target: AnalyzeMode;\n showHelp: boolean;\n}\n\nexport class CliValidationError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"CliValidationError\";\n }\n}\n\nconst VALID_TARGETS: readonly AnalyzeMode[] = [\n \"all\",\n \"components\",\n \"functions\",\n];\n\n/** 不明なオプションの検出に使用 */\nconst VALID_OPTIONS: readonly string[] = [\"--min\", \"--target\", \"--help\"];\n\n/**\n * CLIオプションをパースする\n *\n * @throws {CliValidationError} オプションが無効な場合\n */\nexport function parseCliOptions(args: string[]): CliOptions {\n let targetDir = path.join(process.cwd(), \"src\");\n let minUsages = 2;\n let target: AnalyzeMode = \"all\";\n let showHelp = false;\n\n for (const arg of args) {\n if (arg === \"--help\") {\n showHelp = true;\n continue;\n }\n\n if (arg.startsWith(\"--min=\")) {\n const valueStr = arg.slice(6);\n const value = Number.parseInt(valueStr, 10);\n\n if (valueStr === \"\" || Number.isNaN(value)) {\n throw new CliValidationError(\n `--min の値が無効です: \"${valueStr}\" (数値を指定してください)`,\n );\n }\n if (value < 1) {\n throw new CliValidationError(\n `--min の値は1以上である必要があります: ${value}`,\n );\n }\n\n minUsages = value;\n } else if (arg.startsWith(\"--target=\")) {\n const value = arg.slice(9);\n\n if (!VALID_TARGETS.includes(value as AnalyzeMode)) {\n throw new CliValidationError(\n `--target の値が無効です: \"${value}\" (有効な値: ${VALID_TARGETS.join(\", \")})`,\n );\n }\n\n target = value as AnalyzeMode;\n } else if (arg.startsWith(\"--\")) {\n const optionName = arg.split(\"=\")[0];\n\n if (!VALID_OPTIONS.includes(optionName)) {\n throw new CliValidationError(`不明なオプション: ${optionName}`);\n }\n } else {\n targetDir = arg;\n }\n }\n\n return { targetDir, minUsages, target, showHelp };\n}\n\n/**\n * 対象ディレクトリの存在を検証する\n *\n * @throws {CliValidationError} ディレクトリが存在しない、またはディレクトリでない場合\n */\nexport function validateTargetDir(targetDir: string): void {\n if (!fs.existsSync(targetDir)) {\n throw new CliValidationError(`ディレクトリが存在しません: ${targetDir}`);\n }\n\n const stat = fs.statSync(targetDir);\n if (!stat.isDirectory()) {\n throw new CliValidationError(\n `指定されたパスはディレクトリではありません: ${targetDir}`,\n );\n }\n}\n\n/**\n * ヘルプメッセージを取得する\n */\nexport function getHelpMessage(): string {\n return `\nUsage: dittory [options] [directory]\n\nOptions:\n --min=<number> 最小使用箇所数 (デフォルト: 2)\n --target=<mode> 解析対象: all, components, functions (デフォルト: all)\n --help このヘルプを表示\n\nArguments:\n directory 解析対象ディレクトリ (デフォルト: ./src)\n`;\n}\n","import path from \"node:path\";\nimport type { AnalysisResult, Constant, Exported } from \"@/types\";\n\n/**\n * exportされたコンポーネントの一覧を出力\n */\nexport function printExportedComponents(exported: Exported[]): void {\n const lines = [\n \"1. exportされたコンポーネントを収集中...\",\n ` → ${exported.length}個のコンポーネントを検出`,\n ...exported.map(\n (comp) =>\n ` - ${comp.name} (${path.relative(process.cwd(), comp.sourceFilePath)})`,\n ),\n \"\",\n ];\n console.log(lines.join(\"\\n\"));\n}\n\n/**\n * 常に同じ値が渡されているpropsを出力\n */\nexport function printConstantProps(constants: Constant[]): void {\n console.log(\"=== 常に同じ値が渡されているprops ===\\n\");\n\n if (constants.length === 0) {\n console.log(\"常に同じ値が渡されているpropsは見つかりませんでした。\");\n return;\n }\n\n for (const prop of constants) {\n const relativeComponentPath = path.relative(\n process.cwd(),\n prop.targetSourceFile,\n );\n\n console.log(`コンポーネント: ${prop.targetName}`);\n console.log(` 定義: ${relativeComponentPath}`);\n console.log(` prop: ${prop.paramName}`);\n console.log(` 常に渡される値: ${prop.value}`);\n console.log(` 使用箇所: ${prop.usages.length}箇所`);\n\n for (const usage of prop.usages) {\n const relativePath = path.relative(process.cwd(), usage.usageFilePath);\n console.log(` - ${relativePath}:${usage.usageLine}`);\n }\n\n console.log(\"\");\n }\n}\n\n/**\n * 解析結果を全て出力\n */\nexport function printAnalysisResult(result: AnalysisResult): void {\n printExportedComponents(result.exported);\n printConstantProps(result.constants);\n}\n\n/**\n * exportされた関数の一覧を出力\n */\nexport function printExportedFunctions(exported: Exported[]): void {\n const lines = [\n \"2. exportされた関数を収集中...\",\n ` → ${exported.length}個の関数を検出`,\n ...exported.map(\n (fn) =>\n ` - ${fn.name} (${path.relative(process.cwd(), fn.sourceFilePath)})`,\n ),\n \"\",\n ];\n console.log(lines.join(\"\\n\"));\n}\n\n/**\n * 常に同じ値が渡されている引数を出力\n */\nexport function printConstantArguments(constants: Constant[]): void {\n console.log(\"=== 常に同じ値が渡されている引数 ===\\n\");\n\n if (constants.length === 0) {\n console.log(\"常に同じ値が渡されている引数は見つかりませんでした。\");\n return;\n }\n\n for (const arg of constants) {\n const relativeFunctionPath = path.relative(\n process.cwd(),\n arg.targetSourceFile,\n );\n\n console.log(`関数: ${arg.targetName}`);\n console.log(` 定義: ${relativeFunctionPath}`);\n console.log(` 引数: ${arg.paramName}`);\n console.log(` 常に渡される値: ${arg.value}`);\n console.log(` 使用箇所: ${arg.usages.length}箇所`);\n\n for (const usage of arg.usages) {\n const relativePath = path.relative(process.cwd(), usage.usageFilePath);\n console.log(` - ${relativePath}:${usage.usageLine}`);\n }\n\n console.log(\"\");\n }\n}\n\n/**\n * 関数解析結果を全て出力\n */\nexport function printFunctionAnalysisResult(result: AnalysisResult): void {\n printExportedFunctions(result.exported);\n printConstantArguments(result.constants);\n}\n","import path from \"node:path\";\nimport { Project, type SourceFile } from \"ts-morph\";\nimport { isTestOrStorybookFile } from \"@/source/fileFilters\";\nimport type { FileFilter } from \"@/types\";\n\n/**\n * プロジェクトを初期化し、フィルタリングされたソースファイルを取得する\n */\nexport function createFilteredSourceFiles(\n targetDir: string,\n shouldExcludeFile: FileFilter = isTestOrStorybookFile,\n): SourceFile[] {\n // プロジェクトを初期化\n const project = new Project({\n tsConfigFilePath: path.join(process.cwd(), \"tsconfig.json\"),\n skipAddingFilesFromTsConfig: true,\n });\n\n // 対象ディレクトリのファイルを追加\n project.addSourceFilesAtPaths(`${targetDir}/**/*.{ts,tsx,js,jsx}`);\n\n // ファイルをフィルタリング\n const allSourceFiles = project.getSourceFiles();\n const sourceFilesToAnalyze = allSourceFiles.filter(\n (sourceFile) => !shouldExcludeFile(sourceFile.getFilePath()),\n );\n\n return sourceFilesToAnalyze;\n}\n","#!/usr/bin/env node\nimport { analyzeFunctionsCore } from \"@/analyzeFunctions\";\nimport { analyzePropsCore } from \"@/analyzeProps\";\nimport {\n type CliOptions,\n CliValidationError,\n getHelpMessage,\n parseCliOptions,\n validateTargetDir,\n} from \"@/cli/parseCliOptions\";\nimport {\n printAnalysisResult,\n printFunctionAnalysisResult,\n} from \"@/output/printAnalysisResult\";\nimport { createFilteredSourceFiles } from \"@/source/createFilteredSourceFiles\";\n\n/**\n * エラーメッセージを表示してプロセスを終了する\n */\nfunction exitWithError(message: string): never {\n console.error(`エラー: ${message}`);\n process.exit(1);\n}\n\nfunction main(): void {\n let options: CliOptions;\n\n try {\n options = parseCliOptions(process.argv.slice(2));\n } catch (error) {\n if (error instanceof CliValidationError) {\n exitWithError(error.message);\n }\n throw error;\n }\n\n const { targetDir, minUsages, target, showHelp } = options;\n\n if (showHelp) {\n console.log(getHelpMessage());\n process.exit(0);\n }\n\n // 対象ディレクトリの存在を検証\n try {\n validateTargetDir(targetDir);\n } catch (error) {\n if (error instanceof CliValidationError) {\n exitWithError(error.message);\n }\n throw error;\n }\n\n console.log(`解析対象ディレクトリ: ${targetDir}`);\n console.log(`最小使用箇所数: ${minUsages}`);\n console.log(`解析対象: ${target}\\n`);\n\n const sourceFilesToAnalyze = createFilteredSourceFiles(targetDir);\n\n if (target === \"all\" || target === \"components\") {\n const propsResult = analyzePropsCore(sourceFilesToAnalyze, { minUsages });\n printAnalysisResult(propsResult);\n }\n\n if (target === \"all\" || target === \"functions\") {\n const functionsResult = analyzeFunctionsCore(sourceFilesToAnalyze, {\n minUsages,\n });\n printFunctionAnalysisResult(functionsResult);\n }\n}\n\nmain();\n"],"mappings":"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"}
@@ -0,0 +1,80 @@
1
+ import { FunctionDeclaration, MethodDeclaration, SourceFile, VariableDeclaration } from "ts-morph";
2
+
3
+ //#region src/types.d.ts
4
+
5
+ /**
6
+ * ファイルパスを受け取り、除外すべきかどうかを判定する関数の型
7
+ */
8
+ type FileFilter = (filePath: string) => boolean;
9
+ /**
10
+ * 使用箇所の共通interface
11
+ * 関数呼び出しやJSX要素で、どのパラメータにどの値が渡されたかを記録する
12
+ */
13
+ interface Usage {
14
+ /** ネストしたプロパティの場合は "param.nested.key" 形式 */
15
+ name: string;
16
+ /** リテラル値、enum参照、変数参照などを解決した結果 */
17
+ value: string;
18
+ usageFilePath: string;
19
+ usageLine: number;
20
+ }
21
+ /**
22
+ * パラメータ/props定義の共通interface
23
+ */
24
+ interface Definition {
25
+ name: string;
26
+ /** 引数リストにおける位置(0始まり) */
27
+ index: number;
28
+ /** ?がなく、デフォルト値もない場合はtrue */
29
+ required: boolean;
30
+ }
31
+ /**
32
+ * エクスポートされた対象の共通interface
33
+ */
34
+ interface Exported {
35
+ /** クラスメソッドの場合は "ClassName.methodName" 形式 */
36
+ name: string;
37
+ sourceFilePath: string;
38
+ definitions: Definition[];
39
+ declaration: FunctionDeclaration | VariableDeclaration | MethodDeclaration;
40
+ usages: Record<string, Usage[]>;
41
+ }
42
+ /**
43
+ * 常に同じ値が渡されているパラメータ(デフォルト値化の候補)
44
+ */
45
+ interface Constant {
46
+ targetName: string;
47
+ targetSourceFile: string;
48
+ paramName: string;
49
+ value: string;
50
+ usages: Usage[];
51
+ }
52
+ /**
53
+ * 分析結果
54
+ */
55
+ interface AnalysisResult {
56
+ constants: Constant[];
57
+ exported: Exported[];
58
+ }
59
+ //#endregion
60
+ //#region src/analyzeProps.d.ts
61
+ interface AnalyzePropsOptions {
62
+ shouldExcludeFile?: FileFilter;
63
+ minUsages?: number;
64
+ }
65
+ /**
66
+ * コンポーネントのprops使用状況を解析し、常に同じ値が渡されているpropsを検出する
67
+ *
68
+ * @param sourceFiles - 解析対象のソースファイル配列
69
+ * @param options - オプション設定
70
+ * @returns 解析結果(定数props、統計情報、exportされたコンポーネント)
71
+ *
72
+ * @example
73
+ * const project = new Project();
74
+ * project.addSourceFilesAtPaths("src/**\/*.tsx");
75
+ * const result = analyzePropsCore(project.getSourceFiles());
76
+ */
77
+ declare function analyzePropsCore(sourceFiles: SourceFile[], options?: AnalyzePropsOptions): AnalysisResult;
78
+ //#endregion
79
+ export { type AnalysisResult, type Constant, type Definition, type Exported, type FileFilter, type Usage, analyzePropsCore };
80
+ //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs ADDED
@@ -0,0 +1,3 @@
1
+ import { t as analyzePropsCore } from "./analyzeProps-YWnY-Mf7.mjs";
2
+
3
+ export { analyzePropsCore };
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "dittory",
3
+ "version": "0.0.1",
4
+ "description": "Reactコンポーネントのprops使用状況を解析し、常に同じ値が渡されているpropsを検出するツール",
5
+ "type": "module",
6
+ "main": "./dist/index.mjs",
7
+ "types": "./dist/index.d.mts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.mjs",
11
+ "types": "./dist/index.d.mts"
12
+ }
13
+ },
14
+ "bin": {
15
+ "dittory": "./dist/cli.mjs"
16
+ },
17
+ "files": [
18
+ "dist"
19
+ ],
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "scripts": {
24
+ "build": "tsdown",
25
+ "dev": "tsdown && node ./dist/cli.mjs",
26
+ "lint": "biome check --write src",
27
+ "test": "vitest --run",
28
+ "typecheck": "tsc --noEmit",
29
+ "prepublishOnly": "pnpm run build"
30
+ },
31
+ "keywords": [
32
+ "react",
33
+ "props",
34
+ "analyzer",
35
+ "static-analysis",
36
+ "typescript"
37
+ ],
38
+ "author": "warabi1062",
39
+ "engines": {
40
+ "node": ">=18"
41
+ },
42
+ "license": "MIT",
43
+ "repository": "github:warabi1062/dittory",
44
+ "packageManager": "pnpm@10.6.4",
45
+ "devDependencies": {
46
+ "@biomejs/biome": "2.3.10",
47
+ "@types/node": "^25.0.3",
48
+ "@types/react": "^19.2.7",
49
+ "react": "^19.2.3",
50
+ "tsdown": "^0.18.4",
51
+ "typescript": "^5.9.3",
52
+ "vitest": "^4.0.16"
53
+ },
54
+ "dependencies": {
55
+ "ts-morph": "^27.0.2"
56
+ }
57
+ }