dittory 0.0.2 → 0.0.3

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.
@@ -0,0 +1,1103 @@
1
+ import { Node, SyntaxKind } from "ts-morph";
2
+
3
+ //#region src/source/fileFilters.ts
4
+ /**
5
+ * ファイルパスがテストファイルまたはStorybookファイルかどうかを判定する
6
+ * - 拡張子が .test.* / .spec.* / .stories.* のファイル
7
+ * - __tests__ / __stories__ フォルダ内のファイル
8
+ */
9
+ function isTestOrStorybookFile(filePath) {
10
+ if (/\.(test|spec|stories)\.(ts|tsx|js|jsx)$/.test(filePath)) return true;
11
+ if (/\b__tests__\b|\b__stories__\b/.test(filePath)) return true;
12
+ return false;
13
+ }
14
+
15
+ //#endregion
16
+ //#region src/extraction/parameterUtils.ts
17
+ /**
18
+ * 式がパラメータ(関数の引数)を参照しているかどうかを判定する
19
+ * ネストしたプロパティアクセス(例: props.nested.value)にも対応
20
+ */
21
+ function isParameterReference(expression) {
22
+ if (Node.isIdentifier(expression)) {
23
+ const decl = expression.getSymbol()?.getDeclarations()[0];
24
+ if (!decl) return false;
25
+ const kind = decl.getKind();
26
+ return kind === SyntaxKind.Parameter || kind === SyntaxKind.BindingElement;
27
+ }
28
+ if (Node.isPropertyAccessExpression(expression)) return isParameterReference(expression.getExpression());
29
+ return false;
30
+ }
31
+ /**
32
+ * 式を含む関数宣言を見つける
33
+ */
34
+ function findContainingFunction(node) {
35
+ let current = node;
36
+ while (current) {
37
+ if (Node.isFunctionDeclaration(current) || Node.isArrowFunction(current) || Node.isFunctionExpression(current) || Node.isMethodDeclaration(current)) return current;
38
+ current = current.getParent();
39
+ }
40
+ }
41
+ /**
42
+ * 関数スコープから関数名を取得する
43
+ */
44
+ function getFunctionName(functionScope) {
45
+ if (Node.isFunctionDeclaration(functionScope)) return functionScope.getName() ?? "anonymous";
46
+ if (Node.isArrowFunction(functionScope) || Node.isFunctionExpression(functionScope)) {
47
+ const parent = functionScope.getParent();
48
+ if (parent && Node.isVariableDeclaration(parent)) return parent.getName();
49
+ return "anonymous";
50
+ }
51
+ if (Node.isMethodDeclaration(functionScope)) {
52
+ const className = functionScope.getParent()?.asKind(SyntaxKind.ClassDeclaration)?.getName();
53
+ const methodName = functionScope.getName();
54
+ return className ? `${className}.${methodName}` : methodName;
55
+ }
56
+ return "anonymous";
57
+ }
58
+ /**
59
+ * 式からパラメータパスを構築
60
+ * 例: props.nested.value → "props.nested.value"
61
+ */
62
+ function buildParameterPath(expression) {
63
+ if (Node.isIdentifier(expression)) return expression.getText();
64
+ if (Node.isPropertyAccessExpression(expression)) return `${buildParameterPath(expression.getExpression())}.${expression.getName()}`;
65
+ return expression.getText();
66
+ }
67
+ /**
68
+ * パラメータ参照の ArgValue を作成する
69
+ */
70
+ function createParamRefValue(expression) {
71
+ const filePath = expression.getSourceFile().getFilePath();
72
+ const functionScope = findContainingFunction(expression);
73
+ if (!functionScope) return {
74
+ type: ArgValueType.Literal,
75
+ value: expression.getText()
76
+ };
77
+ const functionName = getFunctionName(functionScope);
78
+ const path = buildParameterPath(expression);
79
+ return {
80
+ type: ArgValueType.ParamRef,
81
+ filePath,
82
+ functionName,
83
+ path
84
+ };
85
+ }
86
+ /**
87
+ * ArgValue を比較可能な文字列キーに変換
88
+ * 同じ値かどうかの判定に使用
89
+ */
90
+ function argValueToKey(value) {
91
+ switch (value.type) {
92
+ case ArgValueType.Literal: return `literal:${value.value}`;
93
+ case ArgValueType.Function: return `function:${value.filePath}:${value.line}`;
94
+ case ArgValueType.ParamRef: return `paramRef:${value.filePath}:${value.functionName}:${value.path}`;
95
+ case ArgValueType.Undefined: return "undefined";
96
+ }
97
+ }
98
+ /**
99
+ * パラメータ参照を解決する
100
+ * callSiteMapを使って、パラメータに渡されたすべての値を取得し、
101
+ * すべて同じ値ならその値を返す。異なる値があればundefinedを返す。
102
+ *
103
+ * @param argValue - 解決対象の ArgValue
104
+ * @param callSiteMap - 呼び出し情報マップ
105
+ * @param visited - 循環参照防止用のセット
106
+ * @returns 解決された ArgValue。異なる値がある場合や解決できない場合はundefined
107
+ */
108
+ function resolveParameterValue(argValue, callSiteMap, visited = /* @__PURE__ */ new Set()) {
109
+ if (argValue.type !== ArgValueType.ParamRef) return argValue;
110
+ const key = argValueToKey(argValue);
111
+ if (visited.has(key)) return;
112
+ visited.add(key);
113
+ const { filePath, functionName, path } = argValue;
114
+ const targetId = `${filePath}:${functionName}`;
115
+ const callSiteInfo = callSiteMap.get(targetId);
116
+ if (!callSiteInfo) return;
117
+ const paramParts = path.split(".");
118
+ const propName = paramParts.length > 1 ? paramParts[paramParts.length - 1] : paramParts[0];
119
+ const args = callSiteInfo.get(propName);
120
+ if (!args || args.length === 0) return;
121
+ const resolvedKeys = /* @__PURE__ */ new Set();
122
+ let resolvedValue;
123
+ for (const arg of args) {
124
+ const resolved = resolveParameterValue(arg.value, callSiteMap, new Set(visited));
125
+ if (resolved === void 0) return;
126
+ const resolvedKey = argValueToKey(resolved);
127
+ resolvedKeys.add(resolvedKey);
128
+ resolvedValue = resolved;
129
+ }
130
+ if (resolvedKeys.size === 1) return resolvedValue;
131
+ }
132
+
133
+ //#endregion
134
+ //#region src/extraction/callSiteCollector.ts
135
+ /**
136
+ * ArgValueのtype識別子
137
+ */
138
+ const ArgValueType = {
139
+ Literal: "literal",
140
+ Function: "function",
141
+ ParamRef: "paramRef",
142
+ Undefined: "undefined"
143
+ };
144
+ /**
145
+ * 関数/コンポーネントの識別子を生成する
146
+ */
147
+ function createTargetId(filePath, name) {
148
+ return `${filePath}:${name}`;
149
+ }
150
+ /**
151
+ * 式から ArgValue を抽出する
152
+ *
153
+ * 式の値を型安全な ArgValue として返す。
154
+ * 呼び出し情報収集時および式の値解決時に使用する。
155
+ *
156
+ * @param expression - 解析対象の式ノード
157
+ * @returns 式の値を表す ArgValue
158
+ */
159
+ function extractArgValue(expression) {
160
+ const type = expression.getType();
161
+ if (type.getCallSignatures().length > 0) {
162
+ const sourceFile = expression.getSourceFile();
163
+ const line = expression.getStartLineNumber();
164
+ return {
165
+ type: ArgValueType.Function,
166
+ filePath: sourceFile.getFilePath(),
167
+ line
168
+ };
169
+ }
170
+ if (Node.isPropertyAccessExpression(expression)) {
171
+ const decl = expression.getSymbol()?.getDeclarations()[0];
172
+ if (decl && Node.isEnumMember(decl)) {
173
+ const enumDecl = decl.getParent();
174
+ if (Node.isEnumDeclaration(enumDecl)) {
175
+ const filePath = enumDecl.getSourceFile().getFilePath();
176
+ const enumName = enumDecl.getName();
177
+ const memberName = decl.getName();
178
+ const value = decl.getValue();
179
+ return {
180
+ type: ArgValueType.Literal,
181
+ value: `${filePath}:${enumName}.${memberName}=${JSON.stringify(value)}`
182
+ };
183
+ }
184
+ }
185
+ if (isParameterReference(expression.getExpression())) return createParamRefValue(expression);
186
+ }
187
+ if (Node.isIdentifier(expression)) {
188
+ if (expression.getText() === "undefined") return { type: ArgValueType.Undefined };
189
+ const symbol = expression.getSymbol();
190
+ const decl = (symbol?.getAliasedSymbol() ?? symbol)?.getDeclarations()[0];
191
+ if (decl) {
192
+ const kind = decl.getKind();
193
+ if (kind === SyntaxKind.Parameter || kind === SyntaxKind.BindingElement) return createParamRefValue(expression);
194
+ if (Node.isVariableDeclaration(decl)) {
195
+ const initializer = decl.getInitializer();
196
+ if (initializer) return extractArgValue(initializer);
197
+ return {
198
+ type: ArgValueType.Literal,
199
+ value: `${decl.getSourceFile().getFilePath()}:${expression.getText()}`
200
+ };
201
+ }
202
+ return {
203
+ type: ArgValueType.Literal,
204
+ value: `${decl.getSourceFile().getFilePath()}:${expression.getText()}`
205
+ };
206
+ }
207
+ }
208
+ if (type.isStringLiteral() || type.isNumberLiteral()) return {
209
+ type: ArgValueType.Literal,
210
+ value: JSON.stringify(type.getLiteralValue())
211
+ };
212
+ if (type.isBooleanLiteral()) return {
213
+ type: ArgValueType.Literal,
214
+ value: type.getText()
215
+ };
216
+ return {
217
+ type: ArgValueType.Literal,
218
+ value: expression.getText()
219
+ };
220
+ }
221
+ /**
222
+ * JSX要素から呼び出し情報を抽出
223
+ */
224
+ function extractFromJsxElement(element, targetId, callSiteMap) {
225
+ const filePath = element.getSourceFile().getFilePath();
226
+ let info = callSiteMap.get(targetId);
227
+ if (!info) {
228
+ info = /* @__PURE__ */ new Map();
229
+ callSiteMap.set(targetId, info);
230
+ }
231
+ for (const attr of element.getAttributes()) {
232
+ if (!Node.isJsxAttribute(attr)) continue;
233
+ const propName = attr.getNameNode().getText();
234
+ const initializer = attr.getInitializer();
235
+ let value;
236
+ if (!initializer) value = {
237
+ type: ArgValueType.Literal,
238
+ value: "true"
239
+ };
240
+ else if (Node.isJsxExpression(initializer)) {
241
+ const expr = initializer.getExpression();
242
+ value = expr ? extractArgValue(expr) : { type: ArgValueType.Undefined };
243
+ } else value = {
244
+ type: ArgValueType.Literal,
245
+ value: initializer.getText()
246
+ };
247
+ const args = info.get(propName) ?? [];
248
+ args.push({
249
+ name: propName,
250
+ value,
251
+ filePath,
252
+ line: element.getStartLineNumber()
253
+ });
254
+ info.set(propName, args);
255
+ }
256
+ }
257
+ /**
258
+ * 関数呼び出しから呼び出し情報を抽出
259
+ */
260
+ function extractFromCallExpression(callExpr, targetId, paramNames, callSiteMap) {
261
+ const filePath = callExpr.getSourceFile().getFilePath();
262
+ let info = callSiteMap.get(targetId);
263
+ if (!info) {
264
+ info = /* @__PURE__ */ new Map();
265
+ callSiteMap.set(targetId, info);
266
+ }
267
+ const args = callExpr.getArguments();
268
+ for (let i = 0; i < paramNames.length; i++) {
269
+ const paramName = paramNames[i];
270
+ const arg = args[i];
271
+ const value = arg ? extractArgValue(arg) : { type: ArgValueType.Undefined };
272
+ const argList = info.get(paramName) ?? [];
273
+ argList.push({
274
+ name: paramName,
275
+ value,
276
+ filePath,
277
+ line: callExpr.getStartLineNumber()
278
+ });
279
+ info.set(paramName, argList);
280
+ }
281
+ }
282
+ /**
283
+ * ソースファイルからすべての呼び出し情報を収集する
284
+ */
285
+ function collectCallSites(sourceFiles, shouldExcludeFile = isTestOrStorybookFile) {
286
+ const callSiteMap = /* @__PURE__ */ new Map();
287
+ for (const sourceFile of sourceFiles) {
288
+ if (shouldExcludeFile(sourceFile.getFilePath())) continue;
289
+ const jsxElements = [...sourceFile.getDescendantsOfKind(SyntaxKind.JsxOpeningElement), ...sourceFile.getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement)];
290
+ for (const element of jsxElements) {
291
+ const tagName = element.getTagNameNode();
292
+ if (!Node.isIdentifier(tagName)) continue;
293
+ const symbol = tagName.getSymbol();
294
+ const decl = (symbol?.getAliasedSymbol() ?? symbol)?.getDeclarations()[0];
295
+ if (!decl) continue;
296
+ extractFromJsxElement(element, createTargetId(decl.getSourceFile().getFilePath(), tagName.getText()), callSiteMap);
297
+ }
298
+ const callExprs = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression);
299
+ for (const callExpr of callExprs) {
300
+ const expr = callExpr.getExpression();
301
+ if (!Node.isIdentifier(expr)) continue;
302
+ const symbol = expr.getSymbol();
303
+ const decl = (symbol?.getAliasedSymbol() ?? symbol)?.getDeclarations()[0];
304
+ if (!decl) continue;
305
+ let paramNames = [];
306
+ if (Node.isFunctionDeclaration(decl)) paramNames = decl.getParameters().map((p) => p.getName());
307
+ else if (Node.isVariableDeclaration(decl)) {
308
+ const init = decl.getInitializer();
309
+ if (init && Node.isArrowFunction(init)) paramNames = init.getParameters().map((p) => p.getName());
310
+ else if (init && Node.isFunctionExpression(init)) paramNames = init.getParameters().map((p) => p.getName());
311
+ else continue;
312
+ } else continue;
313
+ extractFromCallExpression(callExpr, createTargetId(decl.getSourceFile().getFilePath(), expr.getText()), paramNames, callSiteMap);
314
+ }
315
+ }
316
+ return callSiteMap;
317
+ }
318
+
319
+ //#endregion
320
+ //#region src/extraction/resolveExpressionValue.ts
321
+ /**
322
+ * 引数が渡されなかった場合を表す特別な値
323
+ * 必須/任意を問わず、引数未指定の使用箇所を統一的に扱うために使用
324
+ *
325
+ * 注意: この値は文字列 "[undefined]" であり、JavaScriptの undefined とは異なる。
326
+ * 解決関数が undefined を返す場合は「値を解決できなかった」ことを意味し、
327
+ * この文字列を返す場合は「実際に undefined が渡された」ことを意味する。
328
+ */
329
+ const UNDEFINED_VALUE = "[undefined]";
330
+ /**
331
+ * 関数型の値を表すプレフィックス
332
+ * コールバック関数など、関数が渡された場合は使用箇所ごとにユニークな値として扱う
333
+ * これにより、同じコールバック関数を渡していても「定数」として検出されない
334
+ */
335
+ const FUNCTION_VALUE_PREFIX = "[function]";
336
+ /**
337
+ * 式の実際の値を解決する
338
+ *
339
+ * 異なるファイルで同じenum値やリテラル値を使用している場合でも、
340
+ * 同一の値として認識できるよう、値を正規化して文字列表現で返す。
341
+ * 同名だが異なる定義(別ファイルの同名enum等)を区別するため、
342
+ * 必要に応じてファイルパスを含めた識別子を返す。
343
+ *
344
+ * @param expression - 解決対象の式
345
+ * @param context - 呼び出し情報などのコンテキスト
346
+ */
347
+ function resolveExpressionValue(expression, context) {
348
+ return argValueToString(extractArgValue(expression), context);
349
+ }
350
+ /**
351
+ * ArgValue を文字列表現に変換する
352
+ *
353
+ * @param value - 変換対象の ArgValue
354
+ * @param context - パラメータ参照解決用のコンテキスト
355
+ */
356
+ function argValueToString(value, context) {
357
+ switch (value.type) {
358
+ case ArgValueType.Literal: return value.value;
359
+ case ArgValueType.Function: return `${FUNCTION_VALUE_PREFIX}${value.filePath}:${value.line}`;
360
+ case ArgValueType.ParamRef: {
361
+ const resolved = resolveParameterValue(value, context.callSiteMap);
362
+ if (resolved !== void 0) return argValueToKey(resolved);
363
+ return argValueToKey(value);
364
+ }
365
+ case ArgValueType.Undefined: return UNDEFINED_VALUE;
366
+ }
367
+ }
368
+
369
+ //#endregion
370
+ //#region src/extraction/flattenObjectExpression.ts
371
+ /**
372
+ * オブジェクトリテラルを再帰的に解析し、フラットなkey-valueペアを返す
373
+ *
374
+ * @param expression - 解析対象の式ノード
375
+ * @param prefix - キー名のプレフィックス(ネストしたプロパティの親パスを表す)
376
+ * @param context - 呼び出し情報などのコンテキスト
377
+ * @returns フラット化されたkey-valueペアの配列
378
+ *
379
+ * @example
380
+ * // { a: { b: 1, c: 2 } } → [{ key: "prefix.a.b", value: "1" }, { key: "prefix.a.c", value: "2" }]
381
+ */
382
+ function flattenObjectExpression(expression, prefix, context) {
383
+ if (!Node.isObjectLiteralExpression(expression)) return [{
384
+ key: prefix,
385
+ value: resolveExpressionValue(expression, context)
386
+ }];
387
+ return expression.getProperties().flatMap((property) => {
388
+ if (Node.isPropertyAssignment(property)) {
389
+ const propertyName = property.getName();
390
+ const nestedPrefix = prefix ? `${prefix}.${propertyName}` : propertyName;
391
+ const initializer = property.getInitializer();
392
+ return initializer ? flattenObjectExpression(initializer, nestedPrefix, context) : [];
393
+ }
394
+ if (Node.isShorthandPropertyAssignment(property)) {
395
+ const propertyName = property.getName();
396
+ return [{
397
+ key: prefix ? `${prefix}.${propertyName}` : propertyName,
398
+ value: resolveExpressionValue(property.getNameNode(), context)
399
+ }];
400
+ }
401
+ return [];
402
+ });
403
+ }
404
+
405
+ //#endregion
406
+ //#region src/extraction/hasDisableComment.ts
407
+ const DISABLE_NEXT_LINE = "dittory-disable-next-line";
408
+ const DISABLE_LINE = "dittory-disable-line";
409
+ /**
410
+ * ノードに除外コメントがあるかを判定する
411
+ *
412
+ * 以下の2パターンをサポート:
413
+ * - "dittory-disable-next-line": 次の行を除外(leading comments をチェック)
414
+ * - "dittory-disable-line": 同じ行を除外(trailing comments をチェック)
415
+ *
416
+ * 祖先ノードを辿り、いずれかのノードのコメントに
417
+ * 除外キーワードが含まれていれば除外対象とする。
418
+ *
419
+ * @param node - 判定対象のノード
420
+ * @returns 除外コメントが存在すれば true
421
+ */
422
+ function hasDisableComment(node) {
423
+ let current = node;
424
+ while (current) {
425
+ const leadingComments = current.getLeadingCommentRanges();
426
+ const trailingComments = current.getTrailingCommentRanges();
427
+ for (const comment of leadingComments) if (comment.getText().includes(DISABLE_NEXT_LINE)) return true;
428
+ for (const comment of trailingComments) if (comment.getText().includes(DISABLE_LINE)) return true;
429
+ current = current.getParent();
430
+ }
431
+ return false;
432
+ }
433
+
434
+ //#endregion
435
+ //#region src/extraction/extractUsages.ts
436
+ /**
437
+ * 使用状況を抽出するユーティリティクラス
438
+ */
439
+ var ExtractUsages = class {
440
+ /**
441
+ * 関数呼び出しから引数の使用状況を抽出する
442
+ *
443
+ * オブジェクトリテラルの場合は再帰的にフラット化し、
444
+ * 各プロパティを「引数名.プロパティ名」形式で記録する。
445
+ *
446
+ * @param callExpression - 関数呼び出しノード
447
+ * @param callable - 対象の関数情報
448
+ * @param context - 呼び出し情報などのコンテキスト
449
+ * @returns 引数使用状況の配列
450
+ */
451
+ static fromCall(callExpression, callable, context) {
452
+ if (hasDisableComment(callExpression)) return [];
453
+ const usages = [];
454
+ const sourceFile = callExpression.getSourceFile();
455
+ const args = callExpression.getArguments();
456
+ for (const param of callable.definitions) {
457
+ const arg = args[param.index];
458
+ if (!arg) {
459
+ usages.push({
460
+ name: param.name,
461
+ value: UNDEFINED_VALUE,
462
+ usageFilePath: sourceFile.getFilePath(),
463
+ usageLine: callExpression.getStartLineNumber()
464
+ });
465
+ continue;
466
+ }
467
+ for (const { key, value } of flattenObjectExpression(arg, param.name, context)) usages.push({
468
+ name: key,
469
+ value,
470
+ usageFilePath: sourceFile.getFilePath(),
471
+ usageLine: arg.getStartLineNumber()
472
+ });
473
+ }
474
+ return usages;
475
+ }
476
+ /**
477
+ * JSX要素からprops使用状況を抽出する
478
+ *
479
+ * @param element - JSX要素ノード
480
+ * @param definitions - props定義の配列
481
+ * @param context - 呼び出し情報などのコンテキスト
482
+ * @returns props使用状況の配列
483
+ */
484
+ static fromJsxElement(element, definitions, context) {
485
+ if (hasDisableComment(element)) return [];
486
+ const usages = [];
487
+ const sourceFile = element.getSourceFile();
488
+ const attributeMap = /* @__PURE__ */ new Map();
489
+ for (const attr of element.getAttributes()) if (Node.isJsxAttribute(attr)) attributeMap.set(attr.getNameNode().getText(), attr);
490
+ for (const prop of definitions) {
491
+ const attr = attributeMap.get(prop.name);
492
+ if (!attr) {
493
+ usages.push({
494
+ name: prop.name,
495
+ value: UNDEFINED_VALUE,
496
+ usageFilePath: sourceFile.getFilePath(),
497
+ usageLine: element.getStartLineNumber()
498
+ });
499
+ continue;
500
+ }
501
+ const initializer = attr.getInitializer();
502
+ if (!initializer) usages.push({
503
+ name: prop.name,
504
+ value: "true",
505
+ usageFilePath: sourceFile.getFilePath(),
506
+ usageLine: attr.getStartLineNumber()
507
+ });
508
+ else if (Node.isJsxExpression(initializer)) {
509
+ const expression = initializer.getExpression();
510
+ if (!expression) continue;
511
+ for (const { key, value } of flattenObjectExpression(expression, prop.name, context)) usages.push({
512
+ name: key,
513
+ value,
514
+ usageFilePath: sourceFile.getFilePath(),
515
+ usageLine: attr.getStartLineNumber()
516
+ });
517
+ } else usages.push({
518
+ name: prop.name,
519
+ value: initializer.getText(),
520
+ usageFilePath: sourceFile.getFilePath(),
521
+ usageLine: attr.getStartLineNumber()
522
+ });
523
+ }
524
+ return usages;
525
+ }
526
+ };
527
+
528
+ //#endregion
529
+ //#region src/utils/getSingleValueFromSet.ts
530
+ /**
531
+ * Setから唯一の値を安全に取得する
532
+ */
533
+ function getSingleValueFromSet(values) {
534
+ if (values.size !== 1) throw new Error(`Expected exactly 1 value, got ${values.size}`);
535
+ const [firstValue] = Array.from(values);
536
+ return firstValue;
537
+ }
538
+
539
+ //#endregion
540
+ //#region src/analyzer/baseAnalyzer.ts
541
+ /**
542
+ * 分析処理の基底クラス
543
+ */
544
+ var BaseAnalyzer = class {
545
+ shouldExcludeFile;
546
+ minUsages;
547
+ callSiteMap;
548
+ constructor(options = {}) {
549
+ this.shouldExcludeFile = options.shouldExcludeFile ?? isTestOrStorybookFile;
550
+ this.minUsages = options.minUsages ?? 2;
551
+ }
552
+ /**
553
+ * 呼び出し情報を設定する
554
+ * パラメータ経由で渡された値を解決するために使用
555
+ *
556
+ * @param callSiteMap - 呼び出し情報マップ
557
+ */
558
+ setCallSiteMap(callSiteMap) {
559
+ this.callSiteMap = callSiteMap;
560
+ }
561
+ /**
562
+ * コンテキストを取得する
563
+ */
564
+ getResolveContext() {
565
+ return { callSiteMap: this.callSiteMap };
566
+ }
567
+ /**
568
+ * 識別子から全参照を検索し、除外対象ファイルからの参照をフィルタリングする
569
+ *
570
+ * @param nameNode - 検索対象の識別子ノード
571
+ * @returns フィルタリングされた参照エントリの配列
572
+ */
573
+ findFilteredReferences(nameNode) {
574
+ return nameNode.findReferences().flatMap((referencedSymbol) => referencedSymbol.getReferences()).filter((ref) => !this.shouldExcludeFile(ref.getSourceFile().getFilePath()));
575
+ }
576
+ /**
577
+ * 使用状況をグループに追加する
578
+ *
579
+ * @param groupedUsages - 使用状況のグループ(パラメータ名 → 使用状況配列)
580
+ * @param usages - 追加する使用状況の配列
581
+ */
582
+ addUsagesToGroup(groupedUsages, usages) {
583
+ for (const usage of usages) {
584
+ if (!groupedUsages[usage.name]) groupedUsages[usage.name] = [];
585
+ groupedUsages[usage.name].push(usage);
586
+ }
587
+ }
588
+ /**
589
+ * ノードからパラメータ定義を取得する
590
+ *
591
+ * FunctionDeclaration, MethodDeclaration, VariableDeclaration(ArrowFunction/FunctionExpression)
592
+ * からパラメータを抽出し、Definition配列として返す。
593
+ *
594
+ * @param node - パラメータを抽出する対象のノード
595
+ * @returns パラメータ定義の配列
596
+ */
597
+ getParameterDefinitions(node) {
598
+ return this.extractParameterDeclarations(node).map((param, index) => ({
599
+ name: param.getName(),
600
+ index,
601
+ required: !param.hasQuestionToken() && !param.hasInitializer()
602
+ }));
603
+ }
604
+ /**
605
+ * ノードからParameterDeclarationの配列を抽出する
606
+ *
607
+ * 以下のノードタイプに対応:
608
+ * - FunctionDeclaration: 直接パラメータを取得
609
+ * - MethodDeclaration: 直接パラメータを取得
610
+ * - VariableDeclaration: 初期化子がArrowFunctionまたはFunctionExpressionの場合にパラメータを取得
611
+ *
612
+ * @param node - パラメータを抽出する対象のノード
613
+ * @returns ParameterDeclarationの配列
614
+ */
615
+ extractParameterDeclarations(node) {
616
+ if (Node.isFunctionDeclaration(node)) return node.getParameters();
617
+ if (Node.isMethodDeclaration(node)) return node.getParameters();
618
+ if (Node.isVariableDeclaration(node)) {
619
+ const initializer = node.getInitializer();
620
+ if (initializer) {
621
+ if (Node.isArrowFunction(initializer) || Node.isFunctionExpression(initializer)) return initializer.getParameters();
622
+ }
623
+ }
624
+ return [];
625
+ }
626
+ /**
627
+ * メイン分析処理
628
+ *
629
+ * @param declarations - 事前分類済みの宣言配列
630
+ */
631
+ analyze(declarations) {
632
+ const exported = this.collect(declarations);
633
+ const groupedMap = this.createGroupedMap(exported);
634
+ return {
635
+ constants: this.extractConstants(groupedMap),
636
+ exported
637
+ };
638
+ }
639
+ /**
640
+ * 使用状況をグループ化したマップを作成
641
+ */
642
+ createGroupedMap(exported) {
643
+ const groupedMap = /* @__PURE__ */ new Map();
644
+ for (const item of exported) {
645
+ let fileMap = groupedMap.get(item.sourceFilePath);
646
+ if (!fileMap) {
647
+ fileMap = /* @__PURE__ */ new Map();
648
+ groupedMap.set(item.sourceFilePath, fileMap);
649
+ }
650
+ const paramMap = /* @__PURE__ */ new Map();
651
+ for (const [paramName, usages] of Object.entries(item.usages)) {
652
+ const values = /* @__PURE__ */ new Set();
653
+ for (const usage of usages) values.add(usage.value);
654
+ paramMap.set(paramName, {
655
+ values,
656
+ usages
657
+ });
658
+ }
659
+ fileMap.set(item.name, {
660
+ line: item.sourceLine,
661
+ params: paramMap
662
+ });
663
+ }
664
+ return groupedMap;
665
+ }
666
+ /**
667
+ * 常に同じ値が渡されているパラメータを抽出
668
+ */
669
+ extractConstants(groupedMap) {
670
+ const result = [];
671
+ for (const [sourceFile, targetMap] of groupedMap) for (const [targetName, targetInfo] of targetMap) for (const [paramName, usageData] of targetInfo.params) {
672
+ if (!(usageData.usages.length >= this.minUsages && usageData.values.size === 1)) continue;
673
+ const value = getSingleValueFromSet(usageData.values);
674
+ if (value.startsWith(FUNCTION_VALUE_PREFIX)) continue;
675
+ result.push({
676
+ targetName,
677
+ targetSourceFile: sourceFile,
678
+ targetLine: targetInfo.line,
679
+ paramName,
680
+ value,
681
+ usages: usageData.usages
682
+ });
683
+ }
684
+ return result;
685
+ }
686
+ };
687
+
688
+ //#endregion
689
+ //#region src/analyzer/classMethodAnalyzer.ts
690
+ /**
691
+ * クラスメソッドの引数分析を行うAnalyzer
692
+ *
693
+ * exportされたクラスのメソッド(static/instance)を収集し、
694
+ * 各メソッドの引数使用状況を分析する。
695
+ * 常に同じ値が渡されている引数を検出し、定数として報告する。
696
+ *
697
+ * @example
698
+ * ```ts
699
+ * const analyzer = new ClassMethodAnalyzer({ minUsages: 2 });
700
+ * const result = analyzer.analyze(declarations);
701
+ * console.log(result.constants);
702
+ * ```
703
+ */
704
+ var ClassMethodAnalyzer = class extends BaseAnalyzer {
705
+ constructor(options = {}) {
706
+ super(options);
707
+ }
708
+ /**
709
+ * 事前分類済みの宣言からクラスメソッドを収集する
710
+ *
711
+ * @param declarations - 事前分類済みの宣言配列(type: "class")
712
+ * @returns クラスメソッドとその使用状況の配列(名前は「ClassName.methodName」形式)
713
+ */
714
+ collect(declarations) {
715
+ const results = [];
716
+ for (const classified of declarations) {
717
+ const { exportName, sourceFile, declaration } = classified;
718
+ if (!Node.isClassDeclaration(declaration)) continue;
719
+ const methods = declaration.getMethods();
720
+ for (const method of methods) {
721
+ const methodName = method.getName();
722
+ const parameters = this.getParameterDefinitions(method);
723
+ const callable = {
724
+ name: `${exportName}.${methodName}`,
725
+ sourceFilePath: sourceFile.getFilePath(),
726
+ sourceLine: method.getStartLineNumber(),
727
+ definitions: parameters,
728
+ declaration: method,
729
+ usages: {}
730
+ };
731
+ const nameNode = method.getNameNode();
732
+ if (!Node.isIdentifier(nameNode)) continue;
733
+ const references = this.findFilteredReferences(nameNode);
734
+ const groupedUsages = {};
735
+ for (const reference of references) {
736
+ const propertyAccess = reference.getNode().getParent();
737
+ if (!propertyAccess || !Node.isPropertyAccessExpression(propertyAccess)) continue;
738
+ const callExpression = propertyAccess.getParent();
739
+ if (!callExpression || !Node.isCallExpression(callExpression)) continue;
740
+ if (callExpression.getExpression() !== propertyAccess) continue;
741
+ const usages = ExtractUsages.fromCall(callExpression, callable, this.getResolveContext());
742
+ this.addUsagesToGroup(groupedUsages, usages);
743
+ }
744
+ callable.usages = groupedUsages;
745
+ results.push(callable);
746
+ }
747
+ }
748
+ return results;
749
+ }
750
+ };
751
+
752
+ //#endregion
753
+ //#region src/analyzer/functionAnalyzer.ts
754
+ /**
755
+ * 関数の引数分析を行うAnalyzer
756
+ *
757
+ * exportされた関数を収集し、各関数の引数使用状況を分析する。
758
+ * 常に同じ値が渡されている引数を検出し、定数として報告する。
759
+ *
760
+ * @example
761
+ * ```ts
762
+ * const analyzer = new FunctionAnalyzer({ minUsages: 2 });
763
+ * const result = analyzer.analyze(declarations);
764
+ * console.log(result.constants);
765
+ * ```
766
+ */
767
+ var FunctionAnalyzer = class extends BaseAnalyzer {
768
+ constructor(options = {}) {
769
+ super(options);
770
+ }
771
+ /**
772
+ * 事前分類済みの宣言から関数を収集する
773
+ *
774
+ * @param declarations - 事前分類済みの宣言配列(type: "function")
775
+ * @returns exportされた関数とその使用状況の配列
776
+ */
777
+ collect(declarations) {
778
+ const results = [];
779
+ for (const classified of declarations) {
780
+ const { exportName, sourceFile, declaration } = classified;
781
+ if (!Node.isFunctionDeclaration(declaration) && !Node.isVariableDeclaration(declaration)) continue;
782
+ const nameNode = declaration.getNameNode();
783
+ if (!nameNode || !Node.isIdentifier(nameNode)) continue;
784
+ const references = this.findFilteredReferences(nameNode);
785
+ const parameters = this.getParameterDefinitions(declaration);
786
+ const callable = {
787
+ name: exportName,
788
+ sourceFilePath: sourceFile.getFilePath(),
789
+ sourceLine: declaration.getStartLineNumber(),
790
+ definitions: parameters,
791
+ declaration,
792
+ usages: {}
793
+ };
794
+ const groupedUsages = {};
795
+ for (const reference of references) {
796
+ const refNode = reference.getNode();
797
+ const parent = refNode.getParent();
798
+ if (!parent) continue;
799
+ const callExpression = parent.asKind(SyntaxKind.CallExpression);
800
+ if (!callExpression) continue;
801
+ if (callExpression.getExpression() !== refNode) continue;
802
+ const usages = ExtractUsages.fromCall(callExpression, callable, this.getResolveContext());
803
+ this.addUsagesToGroup(groupedUsages, usages);
804
+ }
805
+ callable.usages = groupedUsages;
806
+ results.push(callable);
807
+ }
808
+ return results;
809
+ }
810
+ };
811
+
812
+ //#endregion
813
+ //#region src/components/isReactComponent.ts
814
+ /**
815
+ * 宣言がReactコンポーネントかどうかを判定する
816
+ * - 関数がJSXを返しているかチェック
817
+ * - React.FC型注釈を持っているかチェック
818
+ */
819
+ function isReactComponent(declaration) {
820
+ if (Node.isFunctionDeclaration(declaration)) return containsJsx(declaration);
821
+ if (Node.isVariableDeclaration(declaration)) {
822
+ const initializer = declaration.getInitializer();
823
+ if (!initializer) return false;
824
+ if (Node.isArrowFunction(initializer) || Node.isFunctionExpression(initializer)) return containsJsx(initializer);
825
+ if (Node.isCallExpression(initializer)) {
826
+ const args = initializer.getArguments();
827
+ for (const arg of args) if (Node.isArrowFunction(arg) || Node.isFunctionExpression(arg)) {
828
+ if (containsJsx(arg)) return true;
829
+ }
830
+ }
831
+ }
832
+ return false;
833
+ }
834
+ /**
835
+ * ノード内にJSX要素が含まれているかチェック
836
+ * 効率化: 1回のトラバースで全てのJSX要素をチェック
837
+ */
838
+ function containsJsx(node) {
839
+ let hasJsx = false;
840
+ node.forEachDescendant((descendant) => {
841
+ if (Node.isJsxElement(descendant) || Node.isJsxSelfClosingElement(descendant) || Node.isJsxFragment(descendant)) {
842
+ hasJsx = true;
843
+ return true;
844
+ }
845
+ });
846
+ return hasJsx;
847
+ }
848
+
849
+ //#endregion
850
+ //#region src/source/classifyDeclarations.ts
851
+ /**
852
+ * ソースファイルからexportされた関数/コンポーネント/クラス宣言を収集し、
853
+ * 種別(react/function/class)を事前に分類する
854
+ *
855
+ * @param sourceFiles - 分析対象のソースファイル配列
856
+ * @returns 分類済みの宣言配列
857
+ */
858
+ function classifyDeclarations(sourceFiles) {
859
+ const results = [];
860
+ for (const sourceFile of sourceFiles) {
861
+ const exportedDecls = sourceFile.getExportedDeclarations();
862
+ for (const [exportName, declarations] of exportedDecls) {
863
+ const funcDecl = declarations.find((decl) => isFunctionLike(decl));
864
+ if (funcDecl) {
865
+ if (Node.isFunctionDeclaration(funcDecl) || Node.isVariableDeclaration(funcDecl)) {
866
+ const type = isReactComponent(funcDecl) ? "react" : "function";
867
+ results.push({
868
+ exportName,
869
+ sourceFile,
870
+ declaration: funcDecl,
871
+ type
872
+ });
873
+ }
874
+ continue;
875
+ }
876
+ const classDecl = declarations.find((decl) => Node.isClassDeclaration(decl));
877
+ if (classDecl && Node.isClassDeclaration(classDecl)) results.push({
878
+ exportName,
879
+ sourceFile,
880
+ declaration: classDecl,
881
+ type: "class"
882
+ });
883
+ }
884
+ }
885
+ return results;
886
+ }
887
+ /**
888
+ * 宣言が関数的なもの(関数宣言、アロー関数、関数式)かどうかを判定
889
+ */
890
+ function isFunctionLike(declaration) {
891
+ if (Node.isFunctionDeclaration(declaration)) return true;
892
+ if (Node.isVariableDeclaration(declaration)) {
893
+ const initializer = declaration.getInitializer();
894
+ if (!initializer) return false;
895
+ return Node.isArrowFunction(initializer) || Node.isFunctionExpression(initializer) || Node.isCallExpression(initializer);
896
+ }
897
+ return false;
898
+ }
899
+
900
+ //#endregion
901
+ //#region src/analyzeFunctions.ts
902
+ /**
903
+ * 関数・クラスメソッドの引数使用状況を解析し、常に同じ値が渡されている引数を検出する
904
+ *
905
+ * @param sourceFiles - 解析対象のソースファイル配列
906
+ * @param options - オプション設定
907
+ * @returns 解析結果(定数引数、統計情報、exportされた関数・メソッド)
908
+ *
909
+ * @example
910
+ * const project = new Project();
911
+ * project.addSourceFilesAtPaths("src/**\/*.ts");
912
+ * const result = analyzeFunctionsCore(project.getSourceFiles());
913
+ */
914
+ function analyzeFunctionsCore(sourceFiles, options) {
915
+ const { shouldExcludeFile = isTestOrStorybookFile, minUsages = 2, callSiteMap } = options;
916
+ const declarations = classifyDeclarations(sourceFiles);
917
+ const functions = declarations.filter((decl) => decl.type === "function");
918
+ const classes = declarations.filter((decl) => decl.type === "class");
919
+ const analyzerOptions = {
920
+ shouldExcludeFile,
921
+ minUsages
922
+ };
923
+ const functionAnalyzer = new FunctionAnalyzer(analyzerOptions);
924
+ functionAnalyzer.setCallSiteMap(callSiteMap);
925
+ const functionResult = functionAnalyzer.analyze(functions);
926
+ const classMethodAnalyzer = new ClassMethodAnalyzer(analyzerOptions);
927
+ classMethodAnalyzer.setCallSiteMap(callSiteMap);
928
+ const classMethodResult = classMethodAnalyzer.analyze(classes);
929
+ return {
930
+ constants: [...functionResult.constants, ...classMethodResult.constants],
931
+ exported: [...functionResult.exported, ...classMethodResult.exported]
932
+ };
933
+ }
934
+
935
+ //#endregion
936
+ //#region src/components/getProps.ts
937
+ /**
938
+ * コンポーネントのprops定義を取得する
939
+ *
940
+ * 関数の第一パラメータの型情報からpropsを抽出する。
941
+ * 「Props」などの命名規則に依存せず、TypeScriptの型システムから直接取得するため、
942
+ * どのような命名でもpropsを正確に取得できる。
943
+ *
944
+ * 対応パターン:
945
+ * - function Component(props: Props)
946
+ * - const Component = (props: Props) => ...
947
+ * - React.forwardRef((props, ref) => ...)
948
+ * - React.memo((props) => ...)
949
+ */
950
+ function getProps(declaration) {
951
+ let propsParam;
952
+ if (Node.isFunctionDeclaration(declaration)) {
953
+ const params = declaration.getParameters();
954
+ if (params.length > 0) propsParam = params[0];
955
+ } else if (Node.isVariableDeclaration(declaration)) {
956
+ const initializer = declaration.getInitializer();
957
+ if (initializer) {
958
+ if (Node.isArrowFunction(initializer) || Node.isFunctionExpression(initializer)) {
959
+ const params = initializer.getParameters();
960
+ if (params.length > 0) propsParam = params[0];
961
+ } else if (Node.isCallExpression(initializer)) {
962
+ const args = initializer.getArguments();
963
+ for (const arg of args) if (Node.isArrowFunction(arg) || Node.isFunctionExpression(arg)) {
964
+ const params = arg.getParameters();
965
+ if (params.length > 0) {
966
+ propsParam = params[0];
967
+ break;
968
+ }
969
+ }
970
+ }
971
+ }
972
+ }
973
+ if (!propsParam) return [];
974
+ return extractPropsFromType(propsParam.getType());
975
+ }
976
+ /**
977
+ * 型からprops定義を抽出する
978
+ *
979
+ * Mapを使用する理由:
980
+ * - 交差型(A & B)の場合、同じprop名が複数の型に存在する可能性がある
981
+ * - 最初に見つかった定義を優先し、重複を排除するためにMapを使用
982
+ * - 抽出後にindexを付与してDefinition配列として返す
983
+ */
984
+ function extractPropsFromType(type) {
985
+ const propsMap = /* @__PURE__ */ new Map();
986
+ collectPropsFromType(type, propsMap);
987
+ return Array.from(propsMap.values()).map((prop, index) => ({
988
+ ...prop,
989
+ index
990
+ }));
991
+ }
992
+ /**
993
+ * 型からpropsを収集する(交差型の場合は再帰的に処理)
994
+ */
995
+ function collectPropsFromType(type, propsMap) {
996
+ if (type.isIntersection()) {
997
+ for (const intersectionType of type.getIntersectionTypes()) collectPropsFromType(intersectionType, propsMap);
998
+ return;
999
+ }
1000
+ const properties = type.getProperties();
1001
+ for (const prop of properties) {
1002
+ const propName = prop.getName();
1003
+ const declarations = prop.getDeclarations();
1004
+ let isOptional = false;
1005
+ for (const decl of declarations) if (Node.isPropertySignature(decl) && decl.hasQuestionToken()) {
1006
+ isOptional = true;
1007
+ break;
1008
+ }
1009
+ if (!propsMap.has(propName)) propsMap.set(propName, {
1010
+ name: propName,
1011
+ required: !isOptional
1012
+ });
1013
+ }
1014
+ }
1015
+
1016
+ //#endregion
1017
+ //#region src/analyzer/componentAnalyzer.ts
1018
+ /**
1019
+ * Reactコンポーネントのprops分析を行うAnalyzer
1020
+ *
1021
+ * exportされたReactコンポーネントを収集し、各コンポーネントのprops使用状況を分析する。
1022
+ * 常に同じ値が渡されているpropsを検出し、定数として報告する。
1023
+ *
1024
+ * @example
1025
+ * ```ts
1026
+ * const analyzer = new ComponentAnalyzer({ minUsages: 2 });
1027
+ * const result = analyzer.analyze(sourceFiles);
1028
+ * console.log(result.constants);
1029
+ * ```
1030
+ */
1031
+ var ComponentAnalyzer = class extends BaseAnalyzer {
1032
+ constructor(options = {}) {
1033
+ super(options);
1034
+ }
1035
+ /**
1036
+ * 事前分類済みの宣言からReactコンポーネントを収集する
1037
+ *
1038
+ * @param declarations - 事前分類済みの宣言配列
1039
+ * @returns exportされたコンポーネントとその使用状況の配列
1040
+ */
1041
+ collect(declarations) {
1042
+ const exportedComponents = [];
1043
+ for (const classified of declarations) {
1044
+ const { exportName, sourceFile, declaration } = classified;
1045
+ if (!Node.isFunctionDeclaration(declaration) && !Node.isVariableDeclaration(declaration)) continue;
1046
+ const nameNode = declaration.getNameNode();
1047
+ if (!nameNode || !Node.isIdentifier(nameNode)) continue;
1048
+ const references = this.findFilteredReferences(nameNode);
1049
+ const props = getProps(declaration);
1050
+ const component = {
1051
+ name: exportName,
1052
+ sourceFilePath: sourceFile.getFilePath(),
1053
+ sourceLine: declaration.getStartLineNumber(),
1054
+ definitions: props,
1055
+ declaration,
1056
+ usages: {}
1057
+ };
1058
+ const groupedUsages = {};
1059
+ for (const reference of references) {
1060
+ const refNode = reference.getNode();
1061
+ const parent = refNode.getParent();
1062
+ if (!parent) continue;
1063
+ const jsxElement = parent.asKind(SyntaxKind.JsxOpeningElement) ?? parent.asKind(SyntaxKind.JsxSelfClosingElement);
1064
+ if (!jsxElement) continue;
1065
+ if (jsxElement.getTagNameNode() !== refNode) continue;
1066
+ const usages = ExtractUsages.fromJsxElement(jsxElement, component.definitions, this.getResolveContext());
1067
+ this.addUsagesToGroup(groupedUsages, usages);
1068
+ }
1069
+ component.usages = groupedUsages;
1070
+ exportedComponents.push(component);
1071
+ }
1072
+ return exportedComponents;
1073
+ }
1074
+ };
1075
+
1076
+ //#endregion
1077
+ //#region src/analyzeProps.ts
1078
+ /**
1079
+ * コンポーネントのprops使用状況を解析し、常に同じ値が渡されているpropsを検出する
1080
+ *
1081
+ * @param sourceFiles - 解析対象のソースファイル配列
1082
+ * @param options - オプション設定
1083
+ * @returns 解析結果(定数props、統計情報、exportされたコンポーネント)
1084
+ *
1085
+ * @example
1086
+ * const project = new Project();
1087
+ * project.addSourceFilesAtPaths("src/**\/*.tsx");
1088
+ * const result = analyzePropsCore(project.getSourceFiles());
1089
+ */
1090
+ function analyzePropsCore(sourceFiles, options) {
1091
+ const { shouldExcludeFile = isTestOrStorybookFile, minUsages = 2, callSiteMap } = options;
1092
+ const components = classifyDeclarations(sourceFiles).filter((decl) => decl.type === "react");
1093
+ const analyzer = new ComponentAnalyzer({
1094
+ shouldExcludeFile,
1095
+ minUsages
1096
+ });
1097
+ analyzer.setCallSiteMap(callSiteMap);
1098
+ return analyzer.analyze(components);
1099
+ }
1100
+
1101
+ //#endregion
1102
+ export { isTestOrStorybookFile as i, analyzeFunctionsCore as n, collectCallSites as r, analyzePropsCore as t };
1103
+ //# sourceMappingURL=analyzeProps-B0TjCZhP.mjs.map