dittory 0.0.3 → 0.0.5

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,1650 @@
1
+ import { Node, SyntaxKind } from "ts-morph";
2
+
3
+ //#region src/domain/analyzedDeclarations.ts
4
+ /**
5
+ * 分析対象のコレクション
6
+ */
7
+ var AnalyzedDeclarations = class AnalyzedDeclarations {
8
+ items;
9
+ constructor(items = []) {
10
+ this.items = items;
11
+ }
12
+ /**
13
+ * 複数の AnalyzedDeclarations を結合して新しい AnalyzedDeclarations を作成
14
+ */
15
+ static merge(...declarationsArray) {
16
+ const merged = [];
17
+ for (const declarations of declarationsArray) merged.push(...declarations.items);
18
+ return new AnalyzedDeclarations(merged);
19
+ }
20
+ /**
21
+ * 分析対象が存在しないかどうか
22
+ */
23
+ isEmpty() {
24
+ return this.items.length === 0;
25
+ }
26
+ /**
27
+ * 分析対象の件数
28
+ */
29
+ get length() {
30
+ return this.items.length;
31
+ }
32
+ /**
33
+ * 分析対象を追加
34
+ */
35
+ push(item) {
36
+ this.items.push(item);
37
+ }
38
+ /**
39
+ * 各要素に対して関数を適用し、結果の配列を返す
40
+ */
41
+ map(callback) {
42
+ return this.items.map(callback);
43
+ }
44
+ /**
45
+ * 条件に一致する最初の要素を返す
46
+ */
47
+ find(predicate) {
48
+ return this.items.find(predicate);
49
+ }
50
+ /**
51
+ * インデックスで要素を取得
52
+ */
53
+ get(index) {
54
+ return this.items[index];
55
+ }
56
+ /**
57
+ * イテレーション用
58
+ */
59
+ *[Symbol.iterator]() {
60
+ yield* this.items;
61
+ }
62
+ };
63
+
64
+ //#endregion
65
+ //#region src/domain/usagesByParam.ts
66
+ /**
67
+ * パラメータ名ごとにUsageをグループ化して管理するクラス
68
+ */
69
+ var UsagesByParam = class extends Map {
70
+ /**
71
+ * Usageを追加する
72
+ */
73
+ add(usage) {
74
+ const usages = this.get(usage.name) ?? [];
75
+ usages.push(usage);
76
+ this.set(usage.name, usages);
77
+ }
78
+ /**
79
+ * 複数のUsageを追加する
80
+ */
81
+ addAll(usages) {
82
+ for (const usage of usages) this.add(usage);
83
+ }
84
+ };
85
+
86
+ //#endregion
87
+ //#region src/domain/argValueClasses.ts
88
+ /**
89
+ * 引数の値を表す基底抽象クラス
90
+ */
91
+ var ArgValue = class {
92
+ /**
93
+ * 比較可能な文字列キーに変換する
94
+ * 同じ値かどうかの判定に使用
95
+ */
96
+ toKey() {
97
+ return `[${this.prefix}]${this.getValue()}`;
98
+ }
99
+ /**
100
+ * 出力用の文字列表現を取得する
101
+ */
102
+ outputString() {
103
+ if (this.includePrefixInOutput) return `[${this.prefix}]${this.getValue()}`;
104
+ return this.getValue();
105
+ }
106
+ };
107
+ /**
108
+ * リテラル値の基底抽象クラス
109
+ * 出力時はプレフィックスなし、比較キーには [literal] プレフィックスを付ける
110
+ */
111
+ var LiteralArgValue = class extends ArgValue {
112
+ prefix = "literal";
113
+ includePrefixInOutput = false;
114
+ };
115
+ /**
116
+ * enum メンバーのリテラル値
117
+ */
118
+ var EnumLiteralArgValue = class extends LiteralArgValue {
119
+ constructor(filePath, enumName, memberName, enumValue) {
120
+ super();
121
+ this.filePath = filePath;
122
+ this.enumName = enumName;
123
+ this.memberName = memberName;
124
+ this.enumValue = enumValue;
125
+ }
126
+ getValue() {
127
+ return `${this.filePath}:${this.enumName}.${this.memberName}=${JSON.stringify(this.enumValue)}`;
128
+ }
129
+ outputString() {
130
+ return `${this.enumName}.${this.memberName}`;
131
+ }
132
+ };
133
+ /**
134
+ * this プロパティアクセスのリテラル値
135
+ */
136
+ var ThisLiteralArgValue = class extends LiteralArgValue {
137
+ constructor(filePath, line, expression) {
138
+ super();
139
+ this.filePath = filePath;
140
+ this.line = line;
141
+ this.expression = expression;
142
+ }
143
+ prefix = "this";
144
+ includePrefixInOutput = true;
145
+ getValue() {
146
+ return `${this.filePath}:${this.line}:${this.expression}`;
147
+ }
148
+ };
149
+ /**
150
+ * メソッド呼び出しのリテラル値
151
+ */
152
+ var MethodCallLiteralArgValue = class extends LiteralArgValue {
153
+ constructor(filePath, line, expression) {
154
+ super();
155
+ this.filePath = filePath;
156
+ this.line = line;
157
+ this.expression = expression;
158
+ }
159
+ prefix = "methodCall";
160
+ includePrefixInOutput = true;
161
+ getValue() {
162
+ return `${this.filePath}:${this.line}:${this.expression}`;
163
+ }
164
+ };
165
+ /**
166
+ * 変数参照のリテラル値
167
+ */
168
+ var VariableLiteralArgValue = class extends LiteralArgValue {
169
+ constructor(filePath, identifier, declarationLine) {
170
+ super();
171
+ this.filePath = filePath;
172
+ this.identifier = identifier;
173
+ this.declarationLine = declarationLine;
174
+ }
175
+ getValue() {
176
+ return `${this.filePath}:${this.declarationLine}:${this.identifier}`;
177
+ }
178
+ };
179
+ /**
180
+ * boolean リテラル値
181
+ */
182
+ var BooleanLiteralArgValue = class extends LiteralArgValue {
183
+ constructor(value) {
184
+ super();
185
+ this.value = value;
186
+ }
187
+ getValue() {
188
+ return String(this.value);
189
+ }
190
+ };
191
+ /**
192
+ * 文字列リテラル値
193
+ */
194
+ var StringLiteralArgValue = class extends LiteralArgValue {
195
+ constructor(value) {
196
+ super();
197
+ this.value = value;
198
+ }
199
+ getValue() {
200
+ return JSON.stringify(this.value);
201
+ }
202
+ };
203
+ /**
204
+ * 数値リテラル値
205
+ */
206
+ var NumberLiteralArgValue = class extends LiteralArgValue {
207
+ constructor(value) {
208
+ super();
209
+ this.value = value;
210
+ }
211
+ getValue() {
212
+ return JSON.stringify(this.value);
213
+ }
214
+ };
215
+ /**
216
+ * JSX boolean shorthand のリテラル値
217
+ */
218
+ var JsxShorthandLiteralArgValue = class extends LiteralArgValue {
219
+ getValue() {
220
+ return "true";
221
+ }
222
+ };
223
+ /**
224
+ * その他のリテラル値(フォールバック)
225
+ */
226
+ var OtherLiteralArgValue = class extends LiteralArgValue {
227
+ constructor(expression) {
228
+ super();
229
+ this.expression = expression;
230
+ }
231
+ getValue() {
232
+ return this.expression;
233
+ }
234
+ };
235
+ /**
236
+ * 関数型の値
237
+ */
238
+ var FunctionArgValue = class extends ArgValue {
239
+ constructor(filePath, line) {
240
+ super();
241
+ this.filePath = filePath;
242
+ this.line = line;
243
+ }
244
+ prefix = "function";
245
+ includePrefixInOutput = true;
246
+ getValue() {
247
+ return `${this.filePath}:${this.line}`;
248
+ }
249
+ };
250
+ /**
251
+ * パラメータ参照の値
252
+ */
253
+ var ParamRefArgValue = class extends ArgValue {
254
+ constructor(filePath, functionName, path, line) {
255
+ super();
256
+ this.filePath = filePath;
257
+ this.functionName = functionName;
258
+ this.path = path;
259
+ this.line = line;
260
+ }
261
+ prefix = "paramRef";
262
+ includePrefixInOutput = true;
263
+ getValue() {
264
+ return `${this.filePath}:${this.functionName}:${this.path}`;
265
+ }
266
+ };
267
+ /**
268
+ * undefined の値
269
+ */
270
+ var UndefinedArgValue = class extends ArgValue {
271
+ prefix = "undefined";
272
+ includePrefixInOutput = true;
273
+ getValue() {
274
+ return "";
275
+ }
276
+ };
277
+
278
+ //#endregion
279
+ //#region src/extraction/hasDisableComment.ts
280
+ const DISABLE_NEXT_LINE = "dittory-disable-next-line";
281
+ const DISABLE_LINE = "dittory-disable-line";
282
+ /**
283
+ * ノードに除外コメントがあるかを判定する
284
+ *
285
+ * 以下の2パターンをサポート:
286
+ * - "dittory-disable-next-line": 次の行を除外(leading comments をチェック)
287
+ * - "dittory-disable-line": 同じ行を除外(trailing comments をチェック)
288
+ *
289
+ * 祖先ノードを辿り、いずれかのノードのコメントに
290
+ * 除外キーワードが含まれていれば除外対象とする。
291
+ *
292
+ * @param node - 判定対象のノード
293
+ * @returns 除外コメントが存在すれば true
294
+ */
295
+ function hasDisableComment(node) {
296
+ let current = node;
297
+ while (current) {
298
+ const leadingComments = current.getLeadingCommentRanges();
299
+ const trailingComments = current.getTrailingCommentRanges();
300
+ for (const comment of leadingComments) if (comment.getText().includes(DISABLE_NEXT_LINE)) return true;
301
+ for (const comment of trailingComments) if (comment.getText().includes(DISABLE_LINE)) return true;
302
+ current = current.getParent();
303
+ }
304
+ return false;
305
+ }
306
+
307
+ //#endregion
308
+ //#region src/extraction/extractUsages.ts
309
+ /**
310
+ * 使用状況を抽出するユーティリティクラス
311
+ */
312
+ var ExtractUsages = class {
313
+ /**
314
+ * 関数呼び出しから引数の使用状況を抽出する
315
+ *
316
+ * オブジェクトリテラルの場合は再帰的にフラット化し、
317
+ * 各プロパティを「引数名.プロパティ名」形式で記録する。
318
+ *
319
+ * @param callExpression - 関数呼び出しノード
320
+ * @param declaration - 分析対象の関数情報
321
+ * @param resolver - 式を解決するためのリゾルバ
322
+ * @returns 引数使用状況の配列
323
+ */
324
+ static fromCall(callExpression, declaration, resolver) {
325
+ if (hasDisableComment(callExpression)) return [];
326
+ const usages = [];
327
+ const sourceFile = callExpression.getSourceFile();
328
+ const args = callExpression.getArguments();
329
+ for (const definition of declaration.definitions) {
330
+ const arg = args[definition.index];
331
+ if (!arg) {
332
+ usages.push({
333
+ name: definition.name,
334
+ value: new UndefinedArgValue(),
335
+ usageFilePath: sourceFile.getFilePath(),
336
+ usageLine: callExpression.getStartLineNumber()
337
+ });
338
+ continue;
339
+ }
340
+ for (const { key, value } of resolver.flattenObject(arg, definition.name)) usages.push({
341
+ name: key,
342
+ value,
343
+ usageFilePath: sourceFile.getFilePath(),
344
+ usageLine: arg.getStartLineNumber()
345
+ });
346
+ }
347
+ return usages;
348
+ }
349
+ /**
350
+ * JSX要素からprops使用状況を抽出する
351
+ *
352
+ * @param element - JSX要素ノード
353
+ * @param definitions - props定義の配列
354
+ * @param resolver - 式を解決するためのリゾルバ
355
+ * @returns props使用状況の配列
356
+ */
357
+ static fromJsxElement(element, definitions, resolver) {
358
+ if (hasDisableComment(element)) return [];
359
+ const usages = [];
360
+ const sourceFile = element.getSourceFile();
361
+ const attributeMap = /* @__PURE__ */ new Map();
362
+ for (const attr of element.getAttributes()) if (Node.isJsxAttribute(attr)) attributeMap.set(attr.getNameNode().getText(), attr);
363
+ for (const definition of definitions) {
364
+ const attr = attributeMap.get(definition.name);
365
+ if (!attr) {
366
+ usages.push({
367
+ name: definition.name,
368
+ value: new UndefinedArgValue(),
369
+ usageFilePath: sourceFile.getFilePath(),
370
+ usageLine: element.getStartLineNumber()
371
+ });
372
+ continue;
373
+ }
374
+ const initializer = attr.getInitializer();
375
+ if (!initializer) usages.push({
376
+ name: definition.name,
377
+ value: new JsxShorthandLiteralArgValue(),
378
+ usageFilePath: sourceFile.getFilePath(),
379
+ usageLine: attr.getStartLineNumber()
380
+ });
381
+ else if (Node.isJsxExpression(initializer)) {
382
+ const expression = initializer.getExpression();
383
+ if (!expression) continue;
384
+ for (const { key, value } of resolver.flattenObject(expression, definition.name)) usages.push({
385
+ name: key,
386
+ value,
387
+ usageFilePath: sourceFile.getFilePath(),
388
+ usageLine: attr.getStartLineNumber()
389
+ });
390
+ } else usages.push({
391
+ name: definition.name,
392
+ value: resolver.resolve(initializer),
393
+ usageFilePath: sourceFile.getFilePath(),
394
+ usageLine: attr.getStartLineNumber()
395
+ });
396
+ }
397
+ return usages;
398
+ }
399
+ };
400
+
401
+ //#endregion
402
+ //#region src/domain/constantCandidate.ts
403
+ /**
404
+ * 定数候補
405
+ *
406
+ * あるパラメータに渡された値を集約し、定数として扱えるかを判定する
407
+ */
408
+ var ConstantCandidate = class {
409
+ /** 値の比較キー(toKey()の結果)のセット */
410
+ valueKeys;
411
+ /** 代表的な値(定数検出時に使用) */
412
+ representativeValue;
413
+ usages;
414
+ constructor(usages) {
415
+ this.usages = usages;
416
+ this.valueKeys = /* @__PURE__ */ new Set();
417
+ let representativeValue = new UndefinedArgValue();
418
+ for (const usage of usages) {
419
+ this.valueKeys.add(usage.value.toKey());
420
+ representativeValue = usage.value;
421
+ }
422
+ this.representativeValue = representativeValue;
423
+ }
424
+ /**
425
+ * 定数として認識できるかを判定
426
+ *
427
+ * 条件:
428
+ * 1. 使用回数が最小使用回数以上
429
+ * 2. すべての使用箇所で同じ値(valueKeys.size === 1)
430
+ * 3. Usage数が総呼び出し回数と一致(すべての呼び出しで値が存在)
431
+ * これにより、オプショナルなプロパティが一部の呼び出しでのみ
432
+ * 指定されている場合を定数として誤検出しない
433
+ */
434
+ isConstant(minUsages, totalCallCount) {
435
+ return this.usages.length >= minUsages && this.valueKeys.size === 1 && this.usages.length === totalCallCount;
436
+ }
437
+ };
438
+
439
+ //#endregion
440
+ //#region src/domain/constantParams.ts
441
+ /**
442
+ * 定数パラメータのコレクション
443
+ */
444
+ var ConstantParams = class ConstantParams {
445
+ items;
446
+ constructor(items = []) {
447
+ this.items = items;
448
+ }
449
+ /**
450
+ * 複数の ConstantParams を結合して新しい ConstantParams を作成
451
+ */
452
+ static merge(...paramsArray) {
453
+ const merged = [];
454
+ for (const params of paramsArray) merged.push(...params.items);
455
+ return new ConstantParams(merged);
456
+ }
457
+ /**
458
+ * 定数パラメータが存在しないかどうか
459
+ */
460
+ isEmpty() {
461
+ return this.items.length === 0;
462
+ }
463
+ /**
464
+ * 定数パラメータの件数
465
+ */
466
+ get length() {
467
+ return this.items.length;
468
+ }
469
+ /**
470
+ * 定数パラメータを追加
471
+ */
472
+ push(item) {
473
+ this.items.push(item);
474
+ }
475
+ /**
476
+ * 条件に一致する最初の定数パラメータを返す
477
+ */
478
+ find(predicate) {
479
+ return this.items.find(predicate);
480
+ }
481
+ /**
482
+ * 宣言(関数/コンポーネント)ごとにグループ化
483
+ */
484
+ groupByDeclaration() {
485
+ const groupMap = /* @__PURE__ */ new Map();
486
+ for (const constantParam of this.items) {
487
+ const key = `${constantParam.declarationSourceFile}:${constantParam.declarationName}`;
488
+ let constantParamGroup = groupMap.get(key);
489
+ if (!constantParamGroup) {
490
+ constantParamGroup = {
491
+ declarationName: constantParam.declarationName,
492
+ declarationSourceFile: constantParam.declarationSourceFile,
493
+ declarationLine: constantParam.declarationLine,
494
+ params: []
495
+ };
496
+ groupMap.set(key, constantParamGroup);
497
+ }
498
+ constantParamGroup.params.push({
499
+ paramName: constantParam.paramName,
500
+ value: constantParam.value,
501
+ usageCount: constantParam.usages.length,
502
+ usages: constantParam.usages
503
+ });
504
+ }
505
+ return Array.from(groupMap.values());
506
+ }
507
+ /**
508
+ * イテレーション用
509
+ */
510
+ *[Symbol.iterator]() {
511
+ yield* this.items;
512
+ }
513
+ };
514
+
515
+ //#endregion
516
+ //#region src/extraction/parameterUtils.ts
517
+ /**
518
+ * 式がパラメータ(関数の引数)を参照しているかどうかを判定する
519
+ * ネストしたプロパティアクセス(例: props.nested.value)にも対応
520
+ */
521
+ function isParameterReference(expression) {
522
+ if (Node.isIdentifier(expression)) {
523
+ const declaration = expression.getSymbol()?.getDeclarations()[0];
524
+ if (!declaration) return false;
525
+ const kind = declaration.getKind();
526
+ return kind === SyntaxKind.Parameter || kind === SyntaxKind.BindingElement;
527
+ }
528
+ if (Node.isPropertyAccessExpression(expression)) return isParameterReference(expression.getExpression());
529
+ return false;
530
+ }
531
+ /**
532
+ * 式を含む関数宣言を見つける
533
+ */
534
+ function findContainingFunction(node) {
535
+ let current = node;
536
+ while (current) {
537
+ if (Node.isFunctionDeclaration(current) || Node.isArrowFunction(current) || Node.isFunctionExpression(current) || Node.isMethodDeclaration(current)) return current;
538
+ current = current.getParent();
539
+ }
540
+ }
541
+ /**
542
+ * 関数スコープから関数名を取得する
543
+ */
544
+ function getFunctionName(functionScope) {
545
+ if (Node.isFunctionDeclaration(functionScope)) return functionScope.getName() ?? "anonymous";
546
+ if (Node.isArrowFunction(functionScope) || Node.isFunctionExpression(functionScope)) {
547
+ const parent = functionScope.getParent();
548
+ if (parent && Node.isVariableDeclaration(parent)) return parent.getName();
549
+ return "anonymous";
550
+ }
551
+ if (Node.isMethodDeclaration(functionScope)) {
552
+ const className = functionScope.getParent()?.asKind(SyntaxKind.ClassDeclaration)?.getName();
553
+ const methodName = functionScope.getName();
554
+ return className ? `${className}.${methodName}` : methodName;
555
+ }
556
+ return "anonymous";
557
+ }
558
+ /**
559
+ * 式からパラメータパスを構築
560
+ * 例: props.nested.value → "props.nested.value"
561
+ */
562
+ function buildParameterPath(expression) {
563
+ if (Node.isIdentifier(expression)) return expression.getText();
564
+ if (Node.isPropertyAccessExpression(expression)) return `${buildParameterPath(expression.getExpression())}.${expression.getName()}`;
565
+ return expression.getText();
566
+ }
567
+ /**
568
+ * パラメータ参照の ArgValue を作成する
569
+ */
570
+ function createParamRefValue(expression) {
571
+ const filePath = expression.getSourceFile().getFilePath();
572
+ const functionScope = findContainingFunction(expression);
573
+ if (!functionScope) return new OtherLiteralArgValue(expression.getText());
574
+ return new ParamRefArgValue(filePath, getFunctionName(functionScope), buildParameterPath(expression), expression.getStartLineNumber());
575
+ }
576
+
577
+ //#endregion
578
+ //#region src/extraction/extractArgValue.ts
579
+ /**
580
+ * 式がthisキーワードへのプロパティアクセスかどうかを判定する
581
+ * ネストしたアクセス(例: this.logger.name)にも対応
582
+ */
583
+ function isThisPropertyAccess(expression) {
584
+ if (Node.isPropertyAccessExpression(expression)) {
585
+ const baseExpr = expression.getExpression();
586
+ if (baseExpr.getKind() === SyntaxKind.ThisKeyword) return true;
587
+ return isThisPropertyAccess(baseExpr);
588
+ }
589
+ return false;
590
+ }
591
+ /**
592
+ * 式から ArgValue を抽出する
593
+ *
594
+ * 式の値を型安全な ArgValue として返す。
595
+ * 呼び出し情報収集時および式の値解決時に使用する。
596
+ *
597
+ * @param expression - 解析対象の式ノード(undefinedの場合はUndefinedArgValueを返す)
598
+ * @returns 式の値を表す ArgValue
599
+ */
600
+ function extractArgValue(expression) {
601
+ if (!expression) return new UndefinedArgValue();
602
+ if (Node.isJsxAttribute(expression)) {
603
+ const initializer = expression.getInitializer();
604
+ if (!initializer) return new JsxShorthandLiteralArgValue();
605
+ if (Node.isJsxExpression(initializer)) return extractArgValue(initializer.getExpression());
606
+ if (Node.isStringLiteral(initializer)) return new StringLiteralArgValue(initializer.getLiteralValue());
607
+ return new OtherLiteralArgValue(initializer.getText());
608
+ }
609
+ const type = expression.getType();
610
+ if (type.getCallSignatures().length > 0) {
611
+ const sourceFile = expression.getSourceFile();
612
+ const line = expression.getStartLineNumber();
613
+ return new FunctionArgValue(sourceFile.getFilePath(), line);
614
+ }
615
+ if (Node.isPropertyAccessExpression(expression)) {
616
+ const declaration = expression.getSymbol()?.getDeclarations()[0];
617
+ if (declaration && Node.isEnumMember(declaration)) {
618
+ const enumDeclaration = declaration.getParent();
619
+ if (Node.isEnumDeclaration(enumDeclaration)) return new EnumLiteralArgValue(enumDeclaration.getSourceFile().getFilePath(), enumDeclaration.getName(), declaration.getName(), declaration.getValue());
620
+ }
621
+ if (isParameterReference(expression.getExpression())) return createParamRefValue(expression);
622
+ if (isThisPropertyAccess(expression)) return new ThisLiteralArgValue(expression.getSourceFile().getFilePath(), expression.getStartLineNumber(), expression.getText());
623
+ }
624
+ if (Node.isIdentifier(expression)) {
625
+ if (expression.getText() === "undefined") return new UndefinedArgValue();
626
+ const symbol = expression.getSymbol();
627
+ const declaration = (symbol?.getAliasedSymbol() ?? symbol)?.getDeclarations()[0];
628
+ if (declaration) {
629
+ let actualDeclaration = declaration;
630
+ let kind = declaration.getKind();
631
+ if (kind === SyntaxKind.ShorthandPropertyAssignment) {
632
+ const definitions = expression.getDefinitions();
633
+ for (const def of definitions) {
634
+ const node = def.getDeclarationNode();
635
+ if (!node) continue;
636
+ const k = node.getKind();
637
+ if (k === SyntaxKind.BindingElement || k === SyntaxKind.Parameter || k === SyntaxKind.VariableDeclaration) {
638
+ actualDeclaration = node;
639
+ kind = k;
640
+ break;
641
+ }
642
+ }
643
+ }
644
+ if (kind === SyntaxKind.Parameter || kind === SyntaxKind.BindingElement) return createParamRefValue(expression);
645
+ if (Node.isVariableDeclaration(actualDeclaration)) {
646
+ const initializer = actualDeclaration.getInitializer();
647
+ if (initializer) return extractArgValue(initializer);
648
+ return new VariableLiteralArgValue(actualDeclaration.getSourceFile().getFilePath(), expression.getText(), actualDeclaration.getStartLineNumber());
649
+ }
650
+ return new VariableLiteralArgValue(actualDeclaration.getSourceFile().getFilePath(), expression.getText(), actualDeclaration.getStartLineNumber());
651
+ }
652
+ }
653
+ const literalValue = type.getLiteralValue();
654
+ if (type.isStringLiteral() && typeof literalValue === "string") return new StringLiteralArgValue(literalValue);
655
+ if (type.isNumberLiteral() && typeof literalValue === "number") return new NumberLiteralArgValue(literalValue);
656
+ if (type.isBooleanLiteral()) return new BooleanLiteralArgValue(type.getText() === "true");
657
+ if (Node.isCallExpression(expression)) {
658
+ const calleeExpr = expression.getExpression();
659
+ const hasParamRefArg = expression.getArguments().some((arg) => isParameterReference(arg));
660
+ if (Node.isPropertyAccessExpression(calleeExpr) || hasParamRefArg) return new MethodCallLiteralArgValue(expression.getSourceFile().getFilePath(), expression.getStartLineNumber(), expression.getText());
661
+ }
662
+ return new OtherLiteralArgValue(expression.getText());
663
+ }
664
+
665
+ //#endregion
666
+ //#region src/extraction/extractObjectType.ts
667
+ /**
668
+ * 型からオブジェクト部分を抽出する
669
+ * ユニオン型(例: `{ a: number } | undefined`)から `undefined` を除外して
670
+ * オブジェクト型部分を返す。オブジェクト型でない場合は null を返す。
671
+ */
672
+ function extractObjectType(type) {
673
+ if (type.isUnion()) {
674
+ for (const unionType of type.getUnionTypes()) {
675
+ if (unionType.isUndefined() || unionType.isNull()) continue;
676
+ const objType = extractObjectType(unionType);
677
+ if (objType) return objType;
678
+ }
679
+ return null;
680
+ }
681
+ if (type.isString() || type.isNumber() || type.isBoolean() || type.isUndefined() || type.isNull() || type.isLiteral()) return null;
682
+ if (type.isArray()) return null;
683
+ if (type.isObject() && type.getProperties().length > 0) return type;
684
+ return null;
685
+ }
686
+
687
+ //#endregion
688
+ //#region src/extraction/expressionResolver.ts
689
+ /**
690
+ * 式の値を解決するクラス
691
+ * CallSiteMap を使ってパラメータ参照を解決し、ArgValueを返す
692
+ */
693
+ var ExpressionResolver = class {
694
+ constructor(callSiteMap) {
695
+ this.callSiteMap = callSiteMap;
696
+ }
697
+ /**
698
+ * 式の実際の値を解決する
699
+ *
700
+ * 異なるファイルで同じenum値やリテラル値を使用している場合でも、
701
+ * 同一の値として認識できるよう、ArgValueとして返す。
702
+ * パラメータ参照の場合はcallSiteMapを使って解決を試みる。
703
+ */
704
+ resolve(expression) {
705
+ const argValue = extractArgValue(expression);
706
+ if (argValue instanceof ParamRefArgValue) return this.callSiteMap.resolveParamRef(argValue);
707
+ return argValue;
708
+ }
709
+ /**
710
+ * オブジェクトリテラルを再帰的に解析し、フラットなkey-valueペアを返す
711
+ *
712
+ * 期待される型(コンテキスト型)から省略されたプロパティも検出し、
713
+ * [undefined] として記録する。
714
+ *
715
+ * @param expression - 解析対象の式ノード
716
+ * @param prefix - キー名のプレフィックス(ネストしたプロパティの親パスを表す)
717
+ * @returns フラット化されたkey-valueペアの配列
718
+ *
719
+ * @example
720
+ * // { a: { b: 1, c: 2 } } → [{ key: "prefix.a.b", value: "1" }, { key: "prefix.a.c", value: "2" }]
721
+ */
722
+ flattenObject(expression, prefix) {
723
+ if (!Node.isObjectLiteralExpression(expression)) return [{
724
+ key: prefix,
725
+ value: this.resolve(expression)
726
+ }];
727
+ const existingValues = this.flattenExistingProperties(expression, prefix);
728
+ const contextualType = expression.getContextualType();
729
+ const objectType = contextualType ? extractObjectType(contextualType) : null;
730
+ const missingValues = objectType ? getMissingProperties(expression, objectType, prefix) : [];
731
+ return [...existingValues, ...missingValues];
732
+ }
733
+ /**
734
+ * オブジェクトリテラル内の既存プロパティをフラット化する
735
+ */
736
+ flattenExistingProperties(expression, prefix) {
737
+ return expression.getProperties().flatMap((property) => {
738
+ if (Node.isPropertyAssignment(property)) {
739
+ const propertyName = property.getName();
740
+ const nestedPrefix = prefix ? `${prefix}.${propertyName}` : propertyName;
741
+ const initializer = property.getInitializer();
742
+ return initializer ? this.flattenObject(initializer, nestedPrefix) : [];
743
+ }
744
+ if (Node.isShorthandPropertyAssignment(property)) {
745
+ const propertyName = property.getName();
746
+ return [{
747
+ key: prefix ? `${prefix}.${propertyName}` : propertyName,
748
+ value: this.resolve(property.getNameNode())
749
+ }];
750
+ }
751
+ return [];
752
+ });
753
+ }
754
+ };
755
+ /**
756
+ * 期待される型と比較して、省略されたプロパティを検出する
757
+ *
758
+ * 省略されたプロパティがオブジェクト型の場合、そのネストプロパティも
759
+ * 再帰的に UndefinedArgValue として出力する。これにより、親プロパティが
760
+ * 省略された場合でも、ネストプロパティが他の呼び出しと比較可能になる。
761
+ */
762
+ function getMissingProperties(objectExpression, expectedType, prefix) {
763
+ const existingPropNames = /* @__PURE__ */ new Set();
764
+ for (const property of objectExpression.getProperties()) if (Node.isPropertyAssignment(property)) existingPropNames.add(property.getName());
765
+ else if (Node.isShorthandPropertyAssignment(property)) existingPropNames.add(property.getName());
766
+ const missingValues = [];
767
+ for (const propSymbol of expectedType.getProperties()) {
768
+ const propName = propSymbol.getName();
769
+ if (existingPropNames.has(propName)) continue;
770
+ const nestedPrefix = prefix ? `${prefix}.${propName}` : propName;
771
+ missingValues.push({
772
+ key: nestedPrefix,
773
+ value: new UndefinedArgValue()
774
+ });
775
+ const propType = propSymbol.getValueDeclaration()?.getType();
776
+ if (propType) {
777
+ const objType = extractObjectType(propType);
778
+ if (objType) missingValues.push(...getNestedMissingProperties(objType, nestedPrefix));
779
+ }
780
+ }
781
+ return missingValues;
782
+ }
783
+ /**
784
+ * 省略された親プロパティのネストプロパティを再帰的に UndefinedArgValue として出力
785
+ */
786
+ function getNestedMissingProperties(parentType, prefix) {
787
+ const result = [];
788
+ for (const propSymbol of parentType.getProperties()) {
789
+ const nestedPrefix = `${prefix}.${propSymbol.getName()}`;
790
+ result.push({
791
+ key: nestedPrefix,
792
+ value: new UndefinedArgValue()
793
+ });
794
+ const propType = propSymbol.getValueDeclaration()?.getType();
795
+ if (propType) {
796
+ const objType = extractObjectType(propType);
797
+ if (objType) result.push(...getNestedMissingProperties(objType, nestedPrefix));
798
+ }
799
+ }
800
+ return result;
801
+ }
802
+
803
+ //#endregion
804
+ //#region src/extraction/valueTypeDetector.ts
805
+ /**
806
+ * 有効な値種別の一覧
807
+ */
808
+ const VALID_VALUE_TYPES = [
809
+ "boolean",
810
+ "number",
811
+ "string",
812
+ "enum",
813
+ "undefined"
814
+ ];
815
+ /**
816
+ * ArgValueから種別を判定する
817
+ *
818
+ * @param value - ArgValue インスタンス
819
+ * @returns 検出された種別、または判定不能な場合は null
820
+ */
821
+ function detectValueType(value) {
822
+ if (value instanceof BooleanLiteralArgValue) return "boolean";
823
+ if (value instanceof UndefinedArgValue) return "undefined";
824
+ if (value instanceof EnumLiteralArgValue) return "enum";
825
+ if (value instanceof StringLiteralArgValue) return "string";
826
+ if (value instanceof NumberLiteralArgValue) return "number";
827
+ return null;
828
+ }
829
+ /**
830
+ * 値が指定された種別に含まれるか判定する
831
+ *
832
+ * @param value - ArgValue インスタンス
833
+ * @param allowedTypes - 許可する種別の配列、または "all"
834
+ * @returns 指定種別に含まれる場合は true
835
+ */
836
+ function matchesValueTypes(value, allowedTypes) {
837
+ if (allowedTypes === "all") return true;
838
+ const detectedType = detectValueType(value);
839
+ if (detectedType === null) return false;
840
+ return allowedTypes.includes(detectedType);
841
+ }
842
+
843
+ //#endregion
844
+ //#region src/source/fileFilters.ts
845
+ /**
846
+ * ファイルパスがテストファイルまたはStorybookファイルかどうかを判定する
847
+ * - 拡張子が .test.* / .spec.* / .stories.* のファイル
848
+ * - __tests__ / __stories__ フォルダ内のファイル
849
+ */
850
+ function isTestOrStorybookFile(filePath) {
851
+ if (/\.(test|spec|stories)\.(ts|tsx|js|jsx)$/.test(filePath)) return true;
852
+ if (/\b__tests__\b|\b__stories__\b/.test(filePath)) return true;
853
+ return false;
854
+ }
855
+
856
+ //#endregion
857
+ //#region src/analyzer/baseAnalyzer.ts
858
+ /**
859
+ * 宣言ごとの使用状況プロファイル(行番号とパラメータ使用状況)
860
+ */
861
+ var DeclarationUsageProfile = class {
862
+ sourceLine;
863
+ candidatesByParam;
864
+ /** 総呼び出し回数(ネストしたプロパティの存在チェックに使用) */
865
+ totalCallCount;
866
+ constructor(sourceLine, candidatesByParam, totalCallCount) {
867
+ this.sourceLine = sourceLine;
868
+ this.candidatesByParam = candidatesByParam;
869
+ this.totalCallCount = totalCallCount;
870
+ }
871
+ /**
872
+ * 定数として認識されるパラメータを返す
873
+ */
874
+ *findConstantParams(minUsages) {
875
+ for (const [paramName, candidate] of this.candidatesByParam) if (candidate.isConstant(minUsages, this.totalCallCount)) yield [paramName, candidate];
876
+ }
877
+ };
878
+ /**
879
+ * ファイル内の宣言(関数/コンポーネント)を管理するレジストリ
880
+ */
881
+ var DeclarationRegistry = class extends Map {
882
+ /**
883
+ * AnalyzedDeclaration から DeclarationUsageProfile を作成して追加
884
+ */
885
+ addFromDeclaration(analyzedDeclaration) {
886
+ const paramMap = /* @__PURE__ */ new Map();
887
+ for (const [paramName, usages] of analyzedDeclaration.usages.entries()) paramMap.set(paramName, new ConstantCandidate(usages));
888
+ const totalCallCount = Math.max(...[...analyzedDeclaration.usages.values()].map((usages) => usages.length), 0);
889
+ this.set(analyzedDeclaration.name, new DeclarationUsageProfile(analyzedDeclaration.sourceLine, paramMap, totalCallCount));
890
+ }
891
+ };
892
+ /**
893
+ * 使用状況を階層的に管理するレジストリ
894
+ *
895
+ * 2階層の構造で使用状況を整理する:
896
+ * 1. ソースファイルパス: どのファイルで定義された宣言か
897
+ * 2. 宣言名: 関数名/コンポーネント名(+ 行番号とパラメータ使用状況)
898
+ *
899
+ * この構造により、定数検出時に効率的に走査できる。
900
+ */
901
+ var UsageRegistry = class UsageRegistry extends Map {
902
+ /**
903
+ * DeclarationRegistry を取得(存在しなければ作成)
904
+ */
905
+ getOrCreateDeclarationRegistry(sourceFilePath) {
906
+ let declarationRegistry = this.get(sourceFilePath);
907
+ if (!declarationRegistry) {
908
+ declarationRegistry = new DeclarationRegistry();
909
+ this.set(sourceFilePath, declarationRegistry);
910
+ }
911
+ return declarationRegistry;
912
+ }
913
+ /**
914
+ * AnalyzedDeclarations から UsageRegistry を作成
915
+ */
916
+ static fromDeclarations(declarations) {
917
+ const usageRegistry = new UsageRegistry();
918
+ for (const analyzedDeclaration of declarations) usageRegistry.getOrCreateDeclarationRegistry(analyzedDeclaration.sourceFilePath).addFromDeclaration(analyzedDeclaration);
919
+ return usageRegistry;
920
+ }
921
+ /**
922
+ * すべての定数パラメータをフラットに取得
923
+ */
924
+ *findAllConstantParams(minUsages) {
925
+ for (const [sourceFile, declarationRegistry] of this) for (const [declarationName, usageProfile] of declarationRegistry) for (const [paramName, candidate] of usageProfile.findConstantParams(minUsages)) yield {
926
+ sourceFile,
927
+ declarationName,
928
+ declarationLine: usageProfile.sourceLine,
929
+ paramName,
930
+ candidate
931
+ };
932
+ }
933
+ };
934
+ /**
935
+ * 分析処理の基底クラス
936
+ */
937
+ var BaseAnalyzer = class {
938
+ shouldExcludeFile;
939
+ minUsages;
940
+ allowedValueTypes;
941
+ callSiteMap;
942
+ constructor(options = {}) {
943
+ this.shouldExcludeFile = options.shouldExcludeFile ?? isTestOrStorybookFile;
944
+ this.minUsages = options.minUsages ?? 2;
945
+ this.allowedValueTypes = options.allowedValueTypes ?? "all";
946
+ }
947
+ /**
948
+ * 呼び出し情報を設定する
949
+ * パラメータ経由で渡された値を解決するために使用
950
+ *
951
+ * @param callSiteMap - 呼び出し情報マップ
952
+ */
953
+ setCallSiteMap(callSiteMap) {
954
+ this.callSiteMap = callSiteMap;
955
+ }
956
+ /**
957
+ * 式のリゾルバを取得する
958
+ */
959
+ getExpressionResolver() {
960
+ return new ExpressionResolver(this.callSiteMap);
961
+ }
962
+ /**
963
+ * 識別子から全参照を検索し、除外対象ファイルからの参照をフィルタリングする
964
+ *
965
+ * @param nameNode - 検索対象の識別子ノード
966
+ * @returns フィルタリングされた参照エントリの配列
967
+ */
968
+ findFilteredReferences(nameNode) {
969
+ return nameNode.findReferences().flatMap((referencedSymbol) => referencedSymbol.getReferences()).filter((ref) => !this.shouldExcludeFile(ref.getSourceFile().getFilePath()));
970
+ }
971
+ /**
972
+ * ノードからパラメータ定義を取得する
973
+ *
974
+ * FunctionDeclaration, MethodDeclaration, VariableDeclaration(ArrowFunction/FunctionExpression)
975
+ * からパラメータを抽出し、Definition配列として返す。
976
+ *
977
+ * @param node - パラメータを抽出する対象のノード
978
+ * @returns パラメータ定義の配列
979
+ */
980
+ getParameterDefinitions(node) {
981
+ return this.extractParameterDeclarations(node).map((param, index) => ({
982
+ name: param.getName(),
983
+ index,
984
+ required: !param.hasQuestionToken() && !param.hasInitializer()
985
+ }));
986
+ }
987
+ /**
988
+ * ノードからParameterDeclarationの配列を抽出する
989
+ *
990
+ * 以下のノードタイプに対応:
991
+ * - FunctionDeclaration: 直接パラメータを取得
992
+ * - MethodDeclaration: 直接パラメータを取得
993
+ * - VariableDeclaration: 初期化子がArrowFunctionまたはFunctionExpressionの場合にパラメータを取得
994
+ *
995
+ * @param node - パラメータを抽出する対象のノード
996
+ * @returns ParameterDeclarationの配列
997
+ */
998
+ extractParameterDeclarations(node) {
999
+ if (Node.isFunctionDeclaration(node)) return node.getParameters();
1000
+ if (Node.isMethodDeclaration(node)) return node.getParameters();
1001
+ if (Node.isVariableDeclaration(node)) {
1002
+ const initializer = node.getInitializer();
1003
+ if (initializer) {
1004
+ if (Node.isArrowFunction(initializer) || Node.isFunctionExpression(initializer)) return initializer.getParameters();
1005
+ }
1006
+ }
1007
+ return [];
1008
+ }
1009
+ /**
1010
+ * メイン分析処理
1011
+ *
1012
+ * @param classifiedDeclarations - 事前分類済みの宣言配列
1013
+ */
1014
+ analyze(classifiedDeclarations) {
1015
+ const declarations = this.collect(classifiedDeclarations);
1016
+ const usageRegistry = UsageRegistry.fromDeclarations(declarations);
1017
+ return {
1018
+ constantParams: this.extractConstantParams(usageRegistry),
1019
+ declarations
1020
+ };
1021
+ }
1022
+ /**
1023
+ * 常に同じ値が渡されているパラメータを抽出
1024
+ */
1025
+ extractConstantParams(usageRegistry) {
1026
+ const constantParams = new ConstantParams();
1027
+ for (const entry of usageRegistry.findAllConstantParams(this.minUsages)) {
1028
+ const { sourceFile, declarationName, declarationLine, paramName, candidate } = entry;
1029
+ const value = candidate.representativeValue;
1030
+ if (value instanceof FunctionArgValue || value instanceof ParamRefArgValue || value instanceof MethodCallLiteralArgValue) continue;
1031
+ if (!matchesValueTypes(value, this.allowedValueTypes)) continue;
1032
+ constantParams.push({
1033
+ declarationName,
1034
+ declarationSourceFile: sourceFile,
1035
+ declarationLine,
1036
+ paramName,
1037
+ value,
1038
+ usages: candidate.usages
1039
+ });
1040
+ }
1041
+ return constantParams;
1042
+ }
1043
+ };
1044
+
1045
+ //#endregion
1046
+ //#region src/analyzer/classMethodAnalyzer.ts
1047
+ /**
1048
+ * クラスメソッドの引数分析を行うAnalyzer
1049
+ *
1050
+ * exportされたクラスのメソッド(static/instance)を収集し、
1051
+ * 各メソッドの引数使用状況を分析する。
1052
+ * 常に同じ値が渡されている引数を検出し、定数として報告する。
1053
+ *
1054
+ * @example
1055
+ * ```ts
1056
+ * const analyzer = new ClassMethodAnalyzer({ minUsages: 2 });
1057
+ * const result = analyzer.analyze(declarations);
1058
+ * console.log(result.constants);
1059
+ * ```
1060
+ */
1061
+ var ClassMethodAnalyzer = class extends BaseAnalyzer {
1062
+ constructor(options = {}) {
1063
+ super(options);
1064
+ }
1065
+ /**
1066
+ * 事前分類済みの宣言からクラスメソッドを収集する
1067
+ *
1068
+ * @param classifiedDeclarations - 事前分類済みの宣言配列(type: "class")
1069
+ * @returns 分析対象のメソッドとその使用状況(名前は「ClassName.methodName」形式)
1070
+ */
1071
+ collect(classifiedDeclarations) {
1072
+ const analyzedDeclarations = new AnalyzedDeclarations();
1073
+ for (const classifiedDeclaration of classifiedDeclarations) {
1074
+ const { exportName, sourceFile, declaration } = classifiedDeclaration;
1075
+ if (!Node.isClassDeclaration(declaration)) continue;
1076
+ const methods = declaration.getMethods();
1077
+ for (const method of methods) {
1078
+ const methodName = method.getName();
1079
+ const parameters = this.getParameterDefinitions(method);
1080
+ const usageGroup = new UsagesByParam();
1081
+ const analyzed = {
1082
+ name: `${exportName}.${methodName}`,
1083
+ sourceFilePath: sourceFile.getFilePath(),
1084
+ sourceLine: method.getStartLineNumber(),
1085
+ definitions: parameters,
1086
+ declaration: method,
1087
+ usages: usageGroup
1088
+ };
1089
+ const nameNode = method.getNameNode();
1090
+ if (!Node.isIdentifier(nameNode)) continue;
1091
+ const references = this.findFilteredReferences(nameNode);
1092
+ for (const reference of references) {
1093
+ const propertyAccess = reference.getNode().getParent();
1094
+ if (!propertyAccess || !Node.isPropertyAccessExpression(propertyAccess)) continue;
1095
+ const callExpression = propertyAccess.getParent();
1096
+ if (!callExpression || !Node.isCallExpression(callExpression)) continue;
1097
+ if (callExpression.getExpression() !== propertyAccess) continue;
1098
+ const usages = ExtractUsages.fromCall(callExpression, analyzed, this.getExpressionResolver());
1099
+ usageGroup.addAll(usages);
1100
+ }
1101
+ analyzedDeclarations.push(analyzed);
1102
+ }
1103
+ }
1104
+ return analyzedDeclarations;
1105
+ }
1106
+ };
1107
+
1108
+ //#endregion
1109
+ //#region src/analyzer/functionAnalyzer.ts
1110
+ /**
1111
+ * 関数の引数分析を行うAnalyzer
1112
+ *
1113
+ * exportされた関数を収集し、各関数の引数使用状況を分析する。
1114
+ * 常に同じ値が渡されている引数を検出し、定数として報告する。
1115
+ *
1116
+ * @example
1117
+ * ```ts
1118
+ * const analyzer = new FunctionAnalyzer({ minUsages: 2 });
1119
+ * const result = analyzer.analyze(declarations);
1120
+ * console.log(result.constants);
1121
+ * ```
1122
+ */
1123
+ var FunctionAnalyzer = class extends BaseAnalyzer {
1124
+ constructor(options = {}) {
1125
+ super(options);
1126
+ }
1127
+ /**
1128
+ * 事前分類済みの宣言から関数を収集する
1129
+ *
1130
+ * @param classifiedDeclarations - 事前分類済みの宣言配列(type: "function")
1131
+ * @returns 分析対象の関数とその使用状況
1132
+ */
1133
+ collect(classifiedDeclarations) {
1134
+ const analyzedDeclarations = new AnalyzedDeclarations();
1135
+ for (const classifiedDeclaration of classifiedDeclarations) {
1136
+ const { exportName, sourceFile, declaration } = classifiedDeclaration;
1137
+ if (!Node.isFunctionDeclaration(declaration) && !Node.isVariableDeclaration(declaration)) continue;
1138
+ const nameNode = declaration.getNameNode();
1139
+ if (!nameNode || !Node.isIdentifier(nameNode)) continue;
1140
+ const references = this.findFilteredReferences(nameNode);
1141
+ const parameters = this.getParameterDefinitions(declaration);
1142
+ const usageGroup = new UsagesByParam();
1143
+ const analyzed = {
1144
+ name: exportName,
1145
+ sourceFilePath: sourceFile.getFilePath(),
1146
+ sourceLine: declaration.getStartLineNumber(),
1147
+ definitions: parameters,
1148
+ declaration,
1149
+ usages: usageGroup
1150
+ };
1151
+ for (const reference of references) {
1152
+ const refNode = reference.getNode();
1153
+ const parent = refNode.getParent();
1154
+ if (!parent) continue;
1155
+ const callExpression = parent.asKind(SyntaxKind.CallExpression);
1156
+ if (!callExpression) continue;
1157
+ if (callExpression.getExpression() !== refNode) continue;
1158
+ const usages = ExtractUsages.fromCall(callExpression, analyzed, this.getExpressionResolver());
1159
+ usageGroup.addAll(usages);
1160
+ }
1161
+ analyzedDeclarations.push(analyzed);
1162
+ }
1163
+ return analyzedDeclarations;
1164
+ }
1165
+ };
1166
+
1167
+ //#endregion
1168
+ //#region src/react/isReactComponent.ts
1169
+ /**
1170
+ * 宣言がReactコンポーネントかどうかを判定する
1171
+ * - 関数がJSXを返しているかチェック
1172
+ * - React.FC型注釈を持っているかチェック
1173
+ */
1174
+ function isReactComponent(declaration) {
1175
+ if (Node.isFunctionDeclaration(declaration)) return containsJsx(declaration);
1176
+ if (Node.isVariableDeclaration(declaration)) {
1177
+ const initializer = declaration.getInitializer();
1178
+ if (!initializer) return false;
1179
+ if (Node.isArrowFunction(initializer) || Node.isFunctionExpression(initializer)) return containsJsx(initializer);
1180
+ if (Node.isCallExpression(initializer)) {
1181
+ const args = initializer.getArguments();
1182
+ for (const arg of args) if (Node.isArrowFunction(arg) || Node.isFunctionExpression(arg)) {
1183
+ if (containsJsx(arg)) return true;
1184
+ }
1185
+ }
1186
+ }
1187
+ return false;
1188
+ }
1189
+ /**
1190
+ * ノード内にJSX要素が含まれているかチェック
1191
+ * 効率化: 1回のトラバースで全てのJSX要素をチェック
1192
+ */
1193
+ function containsJsx(node) {
1194
+ let hasJsx = false;
1195
+ node.forEachDescendant((descendant) => {
1196
+ if (Node.isJsxElement(descendant) || Node.isJsxSelfClosingElement(descendant) || Node.isJsxFragment(descendant)) {
1197
+ hasJsx = true;
1198
+ return true;
1199
+ }
1200
+ });
1201
+ return hasJsx;
1202
+ }
1203
+
1204
+ //#endregion
1205
+ //#region src/source/classifyDeclarations.ts
1206
+ /**
1207
+ * ソースファイルからexportされた関数/コンポーネント/クラス宣言を収集し、
1208
+ * 種別(react/function/class)を事前に分類する
1209
+ *
1210
+ * @param sourceFiles - 分析対象のソースファイル配列
1211
+ * @returns 分類済みの宣言配列
1212
+ */
1213
+ function classifyDeclarations(sourceFiles) {
1214
+ const classifiedDeclarations = [];
1215
+ for (const sourceFile of sourceFiles) {
1216
+ const exportedDecls = sourceFile.getExportedDeclarations();
1217
+ for (const [exportName, declarations] of exportedDecls) {
1218
+ const functionLikeDeclaration = declarations.find((declaration) => isFunctionLike(declaration));
1219
+ if (functionLikeDeclaration) {
1220
+ if (Node.isFunctionDeclaration(functionLikeDeclaration) || Node.isVariableDeclaration(functionLikeDeclaration)) {
1221
+ const type = isReactComponent(functionLikeDeclaration) ? "react" : "function";
1222
+ classifiedDeclarations.push({
1223
+ exportName,
1224
+ sourceFile,
1225
+ declaration: functionLikeDeclaration,
1226
+ type
1227
+ });
1228
+ }
1229
+ continue;
1230
+ }
1231
+ const classDeclaration = declarations.find((declaration) => Node.isClassDeclaration(declaration));
1232
+ if (classDeclaration && Node.isClassDeclaration(classDeclaration)) classifiedDeclarations.push({
1233
+ exportName,
1234
+ sourceFile,
1235
+ declaration: classDeclaration,
1236
+ type: "class"
1237
+ });
1238
+ }
1239
+ }
1240
+ return classifiedDeclarations;
1241
+ }
1242
+ /**
1243
+ * 宣言が関数的なもの(関数宣言、アロー関数、関数式)かどうかを判定
1244
+ */
1245
+ function isFunctionLike(declaration) {
1246
+ if (Node.isFunctionDeclaration(declaration)) return true;
1247
+ if (Node.isVariableDeclaration(declaration)) {
1248
+ const initializer = declaration.getInitializer();
1249
+ if (!initializer) return false;
1250
+ return Node.isArrowFunction(initializer) || Node.isFunctionExpression(initializer) || Node.isCallExpression(initializer);
1251
+ }
1252
+ return false;
1253
+ }
1254
+
1255
+ //#endregion
1256
+ //#region src/analyzeFunctions.ts
1257
+ /**
1258
+ * 関数・クラスメソッドの引数使用状況を解析し、常に同じ値が渡されている引数を検出する
1259
+ *
1260
+ * @param sourceFiles - 解析対象のソースファイル配列
1261
+ * @param options - オプション設定
1262
+ * @returns 解析結果(定数引数、統計情報、exportされた関数・メソッド)
1263
+ *
1264
+ * @example
1265
+ * const project = new Project();
1266
+ * project.addSourceFilesAtPaths("src/**\/*.ts");
1267
+ * const result = analyzeFunctionsCore(project.getSourceFiles());
1268
+ */
1269
+ function analyzeFunctionsCore(sourceFiles, options) {
1270
+ const { shouldExcludeFile = isTestOrStorybookFile, minUsages = 2, allowedValueTypes = "all", callSiteMap } = options;
1271
+ const declarations = classifyDeclarations(sourceFiles);
1272
+ const functions = declarations.filter((decl) => decl.type === "function");
1273
+ const classes = declarations.filter((decl) => decl.type === "class");
1274
+ const analyzerOptions = {
1275
+ shouldExcludeFile,
1276
+ minUsages,
1277
+ allowedValueTypes
1278
+ };
1279
+ const functionAnalyzer = new FunctionAnalyzer(analyzerOptions);
1280
+ functionAnalyzer.setCallSiteMap(callSiteMap);
1281
+ const functionResult = functionAnalyzer.analyze(functions);
1282
+ const classMethodAnalyzer = new ClassMethodAnalyzer(analyzerOptions);
1283
+ classMethodAnalyzer.setCallSiteMap(callSiteMap);
1284
+ const classMethodResult = classMethodAnalyzer.analyze(classes);
1285
+ return {
1286
+ constantParams: ConstantParams.merge(functionResult.constantParams, classMethodResult.constantParams),
1287
+ declarations: AnalyzedDeclarations.merge(functionResult.declarations, classMethodResult.declarations)
1288
+ };
1289
+ }
1290
+
1291
+ //#endregion
1292
+ //#region src/react/getProps.ts
1293
+ /**
1294
+ * コンポーネントのprops定義を取得する
1295
+ *
1296
+ * 関数の第一パラメータの型情報からpropsを抽出する。
1297
+ * 「Props」などの命名規則に依存せず、TypeScriptの型システムから直接取得するため、
1298
+ * どのような命名でもpropsを正確に取得できる。
1299
+ *
1300
+ * 対応パターン:
1301
+ * - function Component(props: Props)
1302
+ * - const Component = (props: Props) => ...
1303
+ * - React.forwardRef((props, ref) => ...)
1304
+ * - React.memo((props) => ...)
1305
+ */
1306
+ function getProps(declaration) {
1307
+ let propsParam;
1308
+ if (Node.isFunctionDeclaration(declaration)) {
1309
+ const params = declaration.getParameters();
1310
+ if (params.length > 0) propsParam = params[0];
1311
+ } else if (Node.isVariableDeclaration(declaration)) {
1312
+ const initializer = declaration.getInitializer();
1313
+ if (initializer) {
1314
+ if (Node.isArrowFunction(initializer) || Node.isFunctionExpression(initializer)) {
1315
+ const params = initializer.getParameters();
1316
+ if (params.length > 0) propsParam = params[0];
1317
+ } else if (Node.isCallExpression(initializer)) {
1318
+ const args = initializer.getArguments();
1319
+ for (const arg of args) if (Node.isArrowFunction(arg) || Node.isFunctionExpression(arg)) {
1320
+ const params = arg.getParameters();
1321
+ if (params.length > 0) {
1322
+ propsParam = params[0];
1323
+ break;
1324
+ }
1325
+ }
1326
+ }
1327
+ }
1328
+ }
1329
+ if (!propsParam) return [];
1330
+ return extractPropsFromType(propsParam.getType());
1331
+ }
1332
+ /**
1333
+ * 型からprops定義を抽出する
1334
+ *
1335
+ * Mapを使用する理由:
1336
+ * - 交差型(A & B)の場合、同じprop名が複数の型に存在する可能性がある
1337
+ * - 最初に見つかった定義を優先し、重複を排除するためにMapを使用
1338
+ * - 抽出後にindexを付与してDefinition配列として返す
1339
+ */
1340
+ function extractPropsFromType(type) {
1341
+ const propsMap = /* @__PURE__ */ new Map();
1342
+ collectPropsFromType(type, propsMap);
1343
+ return Array.from(propsMap.values()).map((prop, index) => ({
1344
+ ...prop,
1345
+ index
1346
+ }));
1347
+ }
1348
+ /**
1349
+ * 型からpropsを収集する(交差型の場合は再帰的に処理)
1350
+ */
1351
+ function collectPropsFromType(type, propsMap) {
1352
+ if (type.isIntersection()) {
1353
+ for (const intersectionType of type.getIntersectionTypes()) collectPropsFromType(intersectionType, propsMap);
1354
+ return;
1355
+ }
1356
+ const properties = type.getProperties();
1357
+ for (const prop of properties) {
1358
+ const propName = prop.getName();
1359
+ const declarations = prop.getDeclarations();
1360
+ let isOptional = false;
1361
+ for (const declaration of declarations) if (Node.isPropertySignature(declaration) && declaration.hasQuestionToken()) {
1362
+ isOptional = true;
1363
+ break;
1364
+ }
1365
+ if (!propsMap.has(propName)) propsMap.set(propName, {
1366
+ name: propName,
1367
+ required: !isOptional
1368
+ });
1369
+ }
1370
+ }
1371
+
1372
+ //#endregion
1373
+ //#region src/analyzer/componentAnalyzer.ts
1374
+ /**
1375
+ * Reactコンポーネントのprops分析を行うAnalyzer
1376
+ *
1377
+ * exportされたReactコンポーネントを収集し、各コンポーネントのprops使用状況を分析する。
1378
+ * 常に同じ値が渡されているpropsを検出し、定数として報告する。
1379
+ *
1380
+ * @example
1381
+ * ```ts
1382
+ * const analyzer = new ComponentAnalyzer({ minUsages: 2 });
1383
+ * const result = analyzer.analyze(sourceFiles);
1384
+ * console.log(result.constants);
1385
+ * ```
1386
+ */
1387
+ var ComponentAnalyzer = class extends BaseAnalyzer {
1388
+ constructor(options = {}) {
1389
+ super(options);
1390
+ }
1391
+ /**
1392
+ * 事前分類済みの宣言からReactコンポーネントを収集する
1393
+ *
1394
+ * @param classifiedDeclarations - 事前分類済みの宣言配列
1395
+ * @returns 分析対象のコンポーネントとその使用状況
1396
+ */
1397
+ collect(classifiedDeclarations) {
1398
+ const analyzedDeclarations = new AnalyzedDeclarations();
1399
+ for (const classifiedDeclaration of classifiedDeclarations) {
1400
+ const { exportName, sourceFile, declaration } = classifiedDeclaration;
1401
+ if (!Node.isFunctionDeclaration(declaration) && !Node.isVariableDeclaration(declaration)) continue;
1402
+ const nameNode = declaration.getNameNode();
1403
+ if (!nameNode || !Node.isIdentifier(nameNode)) continue;
1404
+ const references = this.findFilteredReferences(nameNode);
1405
+ const props = getProps(declaration);
1406
+ const usageGroup = new UsagesByParam();
1407
+ const analyzed = {
1408
+ name: exportName,
1409
+ sourceFilePath: sourceFile.getFilePath(),
1410
+ sourceLine: declaration.getStartLineNumber(),
1411
+ definitions: props,
1412
+ declaration,
1413
+ usages: usageGroup
1414
+ };
1415
+ for (const reference of references) {
1416
+ const refNode = reference.getNode();
1417
+ const parent = refNode.getParent();
1418
+ if (!parent) continue;
1419
+ const jsxElement = parent.asKind(SyntaxKind.JsxOpeningElement) ?? parent.asKind(SyntaxKind.JsxSelfClosingElement);
1420
+ if (!jsxElement) continue;
1421
+ if (jsxElement.getTagNameNode() !== refNode) continue;
1422
+ const usages = ExtractUsages.fromJsxElement(jsxElement, analyzed.definitions, this.getExpressionResolver());
1423
+ usageGroup.addAll(usages);
1424
+ }
1425
+ analyzedDeclarations.push(analyzed);
1426
+ }
1427
+ return analyzedDeclarations;
1428
+ }
1429
+ };
1430
+
1431
+ //#endregion
1432
+ //#region src/analyzeProps.ts
1433
+ /**
1434
+ * コンポーネントのprops使用状況を解析し、常に同じ値が渡されているpropsを検出する
1435
+ *
1436
+ * @param sourceFiles - 解析対象のソースファイル配列
1437
+ * @param options - オプション設定
1438
+ * @returns 解析結果(定数props、統計情報、exportされたコンポーネント)
1439
+ *
1440
+ * @example
1441
+ * const project = new Project();
1442
+ * project.addSourceFilesAtPaths("src/**\/*.tsx");
1443
+ * const result = analyzePropsCore(project.getSourceFiles());
1444
+ */
1445
+ function analyzePropsCore(sourceFiles, options) {
1446
+ const { shouldExcludeFile = isTestOrStorybookFile, minUsages = 2, allowedValueTypes = "all", callSiteMap } = options;
1447
+ const components = classifyDeclarations(sourceFiles).filter((decl) => decl.type === "react");
1448
+ const analyzer = new ComponentAnalyzer({
1449
+ shouldExcludeFile,
1450
+ minUsages,
1451
+ allowedValueTypes
1452
+ });
1453
+ analyzer.setCallSiteMap(callSiteMap);
1454
+ return analyzer.analyze(components);
1455
+ }
1456
+
1457
+ //#endregion
1458
+ //#region src/domain/callSiteInfo.ts
1459
+ /**
1460
+ * 関数/コンポーネントへの呼び出し情報を管理するクラス
1461
+ * key: パラメータ名, value: 渡された値の配列
1462
+ */
1463
+ var CallSiteInfo = class extends Map {
1464
+ /**
1465
+ * 引数情報を追加する
1466
+ * 既存の配列がある場合は追加、ない場合は新規作成
1467
+ */
1468
+ addArg(name, arg) {
1469
+ const args = this.get(name) ?? [];
1470
+ args.push(arg);
1471
+ this.set(name, args);
1472
+ }
1473
+ };
1474
+
1475
+ //#endregion
1476
+ //#region src/domain/callSiteMap.ts
1477
+ /**
1478
+ * すべての関数/コンポーネントの呼び出し情報を管理するクラス
1479
+ */
1480
+ var CallSiteMap = class extends Map {
1481
+ /**
1482
+ * 引数情報をCallSiteInfoに追加する
1483
+ */
1484
+ addArg(targetId, arg) {
1485
+ this.getOrCreateInfo(targetId).addArg(arg.name, arg);
1486
+ }
1487
+ /**
1488
+ * パラメータ参照を解決してArgValueを返す
1489
+ * callSiteMapを使ってパラメータに渡されたすべての値を取得し、
1490
+ * すべて同じ値ならその値を返す。解決できない場合は元のParamRefArgValueを返す。
1491
+ */
1492
+ resolveParamRef(paramRef) {
1493
+ const resolved = this.resolveParameterValueInternal(paramRef);
1494
+ if (resolved !== void 0) return resolved;
1495
+ return paramRef;
1496
+ }
1497
+ /**
1498
+ * パラメータ参照を解決する(内部メソッド)
1499
+ * callSiteMapを使って、パラメータに渡されたすべての値を取得し、
1500
+ * すべて同じ値ならその値を返す。異なる値があればundefinedを返す。
1501
+ */
1502
+ resolveParameterValueInternal(paramRef, visited = /* @__PURE__ */ new Set()) {
1503
+ const key = paramRef.toKey();
1504
+ if (visited.has(key)) return;
1505
+ visited.add(key);
1506
+ const { filePath, functionName, path } = paramRef;
1507
+ const targetId = `${filePath}:${functionName}`;
1508
+ const callSiteInfo = this.get(targetId);
1509
+ if (!callSiteInfo) return;
1510
+ const paramParts = path.split(".");
1511
+ const propName = paramParts.length > 1 ? paramParts[paramParts.length - 1] : paramParts[0];
1512
+ const args = callSiteInfo.get(propName);
1513
+ if (!args || args.length === 0) return;
1514
+ const resolvedKeys = /* @__PURE__ */ new Set();
1515
+ let resolvedValue;
1516
+ for (const arg of args) {
1517
+ let resolved;
1518
+ if (arg.value instanceof ParamRefArgValue) resolved = this.resolveParameterValueInternal(arg.value, new Set(visited));
1519
+ else resolved = arg.value;
1520
+ if (resolved === void 0) return;
1521
+ const resolvedKey = resolved.toKey();
1522
+ resolvedKeys.add(resolvedKey);
1523
+ resolvedValue = resolved;
1524
+ }
1525
+ if (resolvedKeys.size === 1) return resolvedValue;
1526
+ }
1527
+ /**
1528
+ * targetIdに対応する呼び出し情報を取得する
1529
+ * 存在しない場合は新規作成して登録する
1530
+ */
1531
+ getOrCreateInfo(targetId) {
1532
+ let callSiteInfo = this.get(targetId);
1533
+ if (!callSiteInfo) {
1534
+ callSiteInfo = new CallSiteInfo();
1535
+ this.set(targetId, callSiteInfo);
1536
+ }
1537
+ return callSiteInfo;
1538
+ }
1539
+ };
1540
+
1541
+ //#endregion
1542
+ //#region src/extraction/callSiteCollector.ts
1543
+ /**
1544
+ * ソースファイルから呼び出し情報を収集する
1545
+ */
1546
+ var CallSiteCollector = class {
1547
+ shouldExcludeFile;
1548
+ callSiteMap;
1549
+ constructor(shouldExcludeFile = isTestOrStorybookFile) {
1550
+ this.shouldExcludeFile = shouldExcludeFile;
1551
+ this.callSiteMap = new CallSiteMap();
1552
+ }
1553
+ /**
1554
+ * ソースファイルからすべての呼び出し情報を収集する
1555
+ */
1556
+ collect(sourceFiles) {
1557
+ for (const sourceFile of sourceFiles) {
1558
+ if (this.shouldExcludeFile(sourceFile.getFilePath())) continue;
1559
+ for (const jsxElement of sourceFile.getDescendantsOfKind(SyntaxKind.JsxOpeningElement)) this.extractFromJsxElement(jsxElement);
1560
+ for (const jsxElement of sourceFile.getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement)) this.extractFromJsxElement(jsxElement);
1561
+ for (const callExpression of sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) this.extractFromCallExpression(callExpression);
1562
+ }
1563
+ return this.callSiteMap;
1564
+ }
1565
+ /**
1566
+ * JSX要素から呼び出し情報を抽出して登録する
1567
+ * タグ名がIdentifierでない場合や、宣言が解決できない場合は何もしない
1568
+ */
1569
+ extractFromJsxElement(jsxElement) {
1570
+ const tagName = jsxElement.getTagNameNode();
1571
+ if (!Node.isIdentifier(tagName)) return;
1572
+ const declaration = this.getDeclaration(tagName);
1573
+ if (!declaration) return;
1574
+ const targetId = this.createTargetId(declaration, tagName.getText());
1575
+ const filePath = jsxElement.getSourceFile().getFilePath();
1576
+ const line = jsxElement.getStartLineNumber();
1577
+ const attributesWithName = this.getAttributesWithName(jsxElement);
1578
+ for (const { name, node } of attributesWithName) this.callSiteMap.addArg(targetId, {
1579
+ name,
1580
+ value: extractArgValue(node),
1581
+ filePath,
1582
+ line
1583
+ });
1584
+ }
1585
+ /**
1586
+ * 関数呼び出しから呼び出し情報を抽出して登録する
1587
+ * 呼び出し式がIdentifierでない場合や、宣言が解決できない場合は何もしない
1588
+ */
1589
+ extractFromCallExpression(callExpression) {
1590
+ const calleeExpression = callExpression.getExpression();
1591
+ if (!Node.isIdentifier(calleeExpression)) return;
1592
+ const declaration = this.getDeclaration(calleeExpression);
1593
+ if (!declaration) return;
1594
+ const targetId = this.createTargetId(declaration, calleeExpression.getText());
1595
+ const filePath = callExpression.getSourceFile().getFilePath();
1596
+ const line = callExpression.getStartLineNumber();
1597
+ const argsWithName = this.getArgsWithName(declaration, callExpression.getArguments());
1598
+ for (const { name, node } of argsWithName) this.callSiteMap.addArg(targetId, {
1599
+ name,
1600
+ value: extractArgValue(node),
1601
+ filePath,
1602
+ line
1603
+ });
1604
+ }
1605
+ /**
1606
+ * 宣言からパラメータ名と引数ノードのペア配列を取得する
1607
+ * 関数宣言、アロー関数、関数式に対応
1608
+ * 引数が省略されている場合、node は undefined になる
1609
+ */
1610
+ getArgsWithName(declaration, args) {
1611
+ let paramNames = [];
1612
+ if (Node.isFunctionDeclaration(declaration)) paramNames = declaration.getParameters().map((p) => p.getName());
1613
+ else if (Node.isVariableDeclaration(declaration)) {
1614
+ const initializer = declaration.getInitializer();
1615
+ if (initializer && (Node.isArrowFunction(initializer) || Node.isFunctionExpression(initializer))) paramNames = initializer.getParameters().map((p) => p.getName());
1616
+ }
1617
+ return paramNames.map((name, i) => ({
1618
+ name,
1619
+ node: args[i]
1620
+ }));
1621
+ }
1622
+ /**
1623
+ * JSX要素から属性名とノードのペア配列を取得する
1624
+ */
1625
+ getAttributesWithName(jsxElement) {
1626
+ return jsxElement.getAttributes().filter(Node.isJsxAttribute).map((attr) => ({
1627
+ name: attr.getNameNode().getText(),
1628
+ node: attr
1629
+ }));
1630
+ }
1631
+ /**
1632
+ * 宣言ノードと名前からtargetIdを生成する
1633
+ * 形式: "{ファイルパス}:{名前}"
1634
+ */
1635
+ createTargetId(declaration, name) {
1636
+ return `${declaration.getSourceFile().getFilePath()}:${name}`;
1637
+ }
1638
+ /**
1639
+ * 識別子から定義元の宣言ノードを取得する
1640
+ * インポートを通じて実際の定義を解決し、宣言ノードを返す
1641
+ */
1642
+ getDeclaration(identifier) {
1643
+ const symbol = identifier.getSymbol();
1644
+ return (symbol?.getAliasedSymbol() ?? symbol)?.getDeclarations()[0];
1645
+ }
1646
+ };
1647
+
1648
+ //#endregion
1649
+ export { isTestOrStorybookFile as a, AnalyzedDeclarations as c, analyzeFunctionsCore as i, CallSiteMap as n, VALID_VALUE_TYPES as o, analyzePropsCore as r, ConstantParams as s, CallSiteCollector as t };
1650
+ //# sourceMappingURL=callSiteCollector-CGFPm0in.mjs.map