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.
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { FunctionDeclaration, MethodDeclaration, SourceFile, VariableDeclaration } from "ts-morph";
1
+ import { FunctionDeclaration, MethodDeclaration, Node, SourceFile, VariableDeclaration } from "ts-morph";
2
2
 
3
3
  //#region src/types.d.ts
4
4
 
@@ -59,10 +59,90 @@ interface AnalysisResult {
59
59
  exported: Exported[];
60
60
  }
61
61
  //#endregion
62
+ //#region src/extraction/callSiteCollector.d.ts
63
+ /**
64
+ * ArgValueのtype識別子
65
+ */
66
+ declare const ArgValueType: {
67
+ readonly Literal: "literal";
68
+ readonly Function: "function";
69
+ readonly ParamRef: "paramRef";
70
+ readonly Undefined: "undefined";
71
+ };
72
+ /**
73
+ * 引数の値を表す union 型
74
+ * 文字列エンコーディングの代わりに型安全な表現を使用
75
+ */
76
+ type ArgValue = {
77
+ type: typeof ArgValueType.Literal;
78
+ value: string;
79
+ } | {
80
+ type: typeof ArgValueType.Function;
81
+ filePath: string;
82
+ line: number;
83
+ } | {
84
+ type: typeof ArgValueType.ParamRef;
85
+ filePath: string;
86
+ functionName: string;
87
+ path: string;
88
+ } | {
89
+ type: typeof ArgValueType.Undefined;
90
+ };
91
+ /**
92
+ * 呼び出し箇所での引数情報
93
+ */
94
+ interface CallSiteArg {
95
+ /** 引数のインデックス(0始まり)またはプロパティ名 */
96
+ name: string;
97
+ /** 引数の値 */
98
+ value: ArgValue;
99
+ /** 呼び出し元ファイルパス */
100
+ filePath: string;
101
+ /** 呼び出し元行番号 */
102
+ line: number;
103
+ }
104
+ /**
105
+ * 関数/コンポーネントへの呼び出し情報
106
+ * key: パラメータ名, value: 渡された値の配列
107
+ */
108
+ type CallSiteInfo = Map<string, CallSiteArg[]>;
109
+ /**
110
+ * すべての関数/コンポーネントの呼び出し情報
111
+ * key: "ファイルパス:関数名" 形式の識別子
112
+ */
113
+ type CallSiteMap = Map<string, CallSiteInfo>;
114
+ /**
115
+ * ソースファイルからすべての呼び出し情報を収集する
116
+ */
117
+ declare function collectCallSites(sourceFiles: SourceFile[], shouldExcludeFile?: FileFilter): CallSiteMap;
118
+ //#endregion
119
+ //#region src/analyzeFunctions.d.ts
120
+ interface AnalyzeFunctionsOptions {
121
+ shouldExcludeFile?: FileFilter;
122
+ minUsages?: number;
123
+ /** 呼び出し情報(パラメータ経由で渡された値を解決するために使用) */
124
+ callSiteMap: CallSiteMap;
125
+ }
126
+ /**
127
+ * 関数・クラスメソッドの引数使用状況を解析し、常に同じ値が渡されている引数を検出する
128
+ *
129
+ * @param sourceFiles - 解析対象のソースファイル配列
130
+ * @param options - オプション設定
131
+ * @returns 解析結果(定数引数、統計情報、exportされた関数・メソッド)
132
+ *
133
+ * @example
134
+ * const project = new Project();
135
+ * project.addSourceFilesAtPaths("src/**\/*.ts");
136
+ * const result = analyzeFunctionsCore(project.getSourceFiles());
137
+ */
138
+ declare function analyzeFunctionsCore(sourceFiles: SourceFile[], options: AnalyzeFunctionsOptions): AnalysisResult;
139
+ //#endregion
62
140
  //#region src/analyzeProps.d.ts
63
141
  interface AnalyzePropsOptions {
64
142
  shouldExcludeFile?: FileFilter;
65
143
  minUsages?: number;
144
+ /** 呼び出し情報(パラメータ経由で渡された値を解決するために使用) */
145
+ callSiteMap: CallSiteMap;
66
146
  }
67
147
  /**
68
148
  * コンポーネントのprops使用状況を解析し、常に同じ値が渡されているpropsを検出する
@@ -76,7 +156,7 @@ interface AnalyzePropsOptions {
76
156
  * project.addSourceFilesAtPaths("src/**\/*.tsx");
77
157
  * const result = analyzePropsCore(project.getSourceFiles());
78
158
  */
79
- declare function analyzePropsCore(sourceFiles: SourceFile[], options?: AnalyzePropsOptions): AnalysisResult;
159
+ declare function analyzePropsCore(sourceFiles: SourceFile[], options: AnalyzePropsOptions): AnalysisResult;
80
160
  //#endregion
81
161
  //#region src/cli/parseCliOptions.d.ts
82
162
  type AnalyzeMode = "all" | "components" | "functions";
@@ -94,5 +174,5 @@ interface DittoryConfig {
94
174
  targetDir?: string;
95
175
  }
96
176
  //#endregion
97
- export { type AnalysisResult, type AnalyzeMode, type Constant, type Definition, type DittoryConfig, type Exported, type FileFilter, type OutputMode, type Usage, analyzePropsCore };
177
+ export { type AnalysisResult, type AnalyzeMode, type CallSiteMap, type Constant, type Definition, type DittoryConfig, type Exported, type FileFilter, type OutputMode, type Usage, analyzeFunctionsCore, analyzePropsCore, collectCallSites };
98
178
  //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs CHANGED
@@ -1,3 +1,3 @@
1
- import { t as analyzePropsCore } from "./analyzeProps-CoEqudRM.mjs";
1
+ import { n as analyzeFunctionsCore, r as collectCallSites, t as analyzePropsCore } from "./analyzeProps-B0TjCZhP.mjs";
2
2
 
3
- export { analyzePropsCore };
3
+ export { analyzeFunctionsCore, analyzePropsCore, collectCallSites };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dittory",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "A static analysis CLI that detects parameters always receiving the same value in React components and functions",
5
5
  "type": "module",
6
6
  "main": "./dist/index.mjs",
@@ -1,568 +0,0 @@
1
- import { Node, SyntaxKind } from "ts-morph";
2
-
3
- //#region src/components/getProps.ts
4
- /**
5
- * コンポーネントのprops定義を取得する
6
- *
7
- * 関数の第一パラメータの型情報からpropsを抽出する。
8
- * 「Props」などの命名規則に依存せず、TypeScriptの型システムから直接取得するため、
9
- * どのような命名でもpropsを正確に取得できる。
10
- *
11
- * 対応パターン:
12
- * - function Component(props: Props)
13
- * - const Component = (props: Props) => ...
14
- * - React.forwardRef((props, ref) => ...)
15
- * - React.memo((props) => ...)
16
- */
17
- function getProps(declaration) {
18
- let propsParam;
19
- if (Node.isFunctionDeclaration(declaration)) {
20
- const params = declaration.getParameters();
21
- if (params.length > 0) propsParam = params[0];
22
- } else if (Node.isVariableDeclaration(declaration)) {
23
- const initializer = declaration.getInitializer();
24
- if (initializer) {
25
- if (Node.isArrowFunction(initializer) || Node.isFunctionExpression(initializer)) {
26
- const params = initializer.getParameters();
27
- if (params.length > 0) propsParam = params[0];
28
- } else if (Node.isCallExpression(initializer)) {
29
- const args = initializer.getArguments();
30
- for (const arg of args) if (Node.isArrowFunction(arg) || Node.isFunctionExpression(arg)) {
31
- const params = arg.getParameters();
32
- if (params.length > 0) {
33
- propsParam = params[0];
34
- break;
35
- }
36
- }
37
- }
38
- }
39
- }
40
- if (!propsParam) return [];
41
- return extractPropsFromType(propsParam.getType());
42
- }
43
- /**
44
- * 型からprops定義を抽出する
45
- *
46
- * Mapを使用する理由:
47
- * - 交差型(A & B)の場合、同じprop名が複数の型に存在する可能性がある
48
- * - 最初に見つかった定義を優先し、重複を排除するためにMapを使用
49
- * - 抽出後にindexを付与してDefinition配列として返す
50
- */
51
- function extractPropsFromType(type) {
52
- const propsMap = /* @__PURE__ */ new Map();
53
- collectPropsFromType(type, propsMap);
54
- return Array.from(propsMap.values()).map((prop, index) => ({
55
- ...prop,
56
- index
57
- }));
58
- }
59
- /**
60
- * 型からpropsを収集する(交差型の場合は再帰的に処理)
61
- */
62
- function collectPropsFromType(type, propsMap) {
63
- if (type.isIntersection()) {
64
- for (const intersectionType of type.getIntersectionTypes()) collectPropsFromType(intersectionType, propsMap);
65
- return;
66
- }
67
- const properties = type.getProperties();
68
- for (const prop of properties) {
69
- const propName = prop.getName();
70
- const declarations = prop.getDeclarations();
71
- let isOptional = false;
72
- for (const decl of declarations) if (Node.isPropertySignature(decl) && decl.hasQuestionToken()) {
73
- isOptional = true;
74
- break;
75
- }
76
- if (!propsMap.has(propName)) propsMap.set(propName, {
77
- name: propName,
78
- required: !isOptional
79
- });
80
- }
81
- }
82
-
83
- //#endregion
84
- //#region src/extraction/resolveExpressionValue.ts
85
- /**
86
- * 引数が渡されなかった場合を表す特別な値
87
- * 必須/任意を問わず、引数未指定の使用箇所を統一的に扱うために使用
88
- */
89
- const UNDEFINED_VALUE = "undefined";
90
- /**
91
- * 関数型の値を表すプレフィックス
92
- * コールバック関数など、関数が渡された場合は使用箇所ごとにユニークな値として扱う
93
- * これにより、同じコールバック関数を渡していても「定数」として検出されない
94
- */
95
- const FUNCTION_VALUE_PREFIX = "[function]";
96
- /**
97
- * 式の実際の値を解決する
98
- *
99
- * 異なるファイルで同じenum値やリテラル値を使用している場合でも、
100
- * 同一の値として認識できるよう、値を正規化して文字列表現で返す。
101
- * 同名だが異なる定義(別ファイルの同名enum等)を区別するため、
102
- * 必要に応じてファイルパスを含めた識別子を返す。
103
- */
104
- function resolveExpressionValue(expression) {
105
- const type = expression.getType();
106
- if (type.getCallSignatures().length > 0) {
107
- const sourceFile = expression.getSourceFile();
108
- const line = expression.getStartLineNumber();
109
- return `${FUNCTION_VALUE_PREFIX}${sourceFile.getFilePath()}:${line}`;
110
- }
111
- if (Node.isPropertyAccessExpression(expression)) {
112
- const decl = expression.getSymbol()?.getDeclarations()[0];
113
- if (decl && Node.isEnumMember(decl)) {
114
- const enumDecl = decl.getParent();
115
- if (Node.isEnumDeclaration(enumDecl)) {
116
- const filePath = enumDecl.getSourceFile().getFilePath();
117
- const enumName = enumDecl.getName();
118
- const memberName = decl.getName();
119
- const value = decl.getValue();
120
- return `${filePath}:${enumName}.${memberName}=${JSON.stringify(value)}`;
121
- }
122
- }
123
- }
124
- if (Node.isIdentifier(expression)) {
125
- if (expression.getText() === "undefined") return UNDEFINED_VALUE;
126
- const decl = expression.getSymbol()?.getDeclarations()[0];
127
- if (decl && Node.isVariableDeclaration(decl)) {
128
- const initializer = decl.getInitializer();
129
- return initializer ? initializer.getText().replace(/\s+/g, " ") : `${decl.getSourceFile().getFilePath()}:${expression.getText()}`;
130
- }
131
- if (decl) return `${decl.getSourceFile().getFilePath()}:${expression.getText()}`;
132
- }
133
- if (type.isStringLiteral() || type.isNumberLiteral()) return JSON.stringify(type.getLiteralValue());
134
- if (type.isBooleanLiteral()) return type.getText();
135
- return expression.getText();
136
- }
137
-
138
- //#endregion
139
- //#region src/extraction/flattenObjectExpression.ts
140
- /**
141
- * オブジェクトリテラルを再帰的に解析し、フラットなkey-valueペアを返す
142
- *
143
- * @param expression - 解析対象の式ノード
144
- * @param prefix - キー名のプレフィックス(ネストしたプロパティの親パスを表す)
145
- * @returns フラット化されたkey-valueペアの配列
146
- *
147
- * @example
148
- * // { a: { b: 1, c: 2 } } → [{ key: "prefix.a.b", value: "1" }, { key: "prefix.a.c", value: "2" }]
149
- */
150
- function flattenObjectExpression(expression, prefix) {
151
- if (!Node.isObjectLiteralExpression(expression)) return [{
152
- key: prefix,
153
- value: resolveExpressionValue(expression)
154
- }];
155
- return expression.getProperties().flatMap((property) => {
156
- if (Node.isPropertyAssignment(property)) {
157
- const propertyName = property.getName();
158
- const nestedPrefix = prefix ? `${prefix}.${propertyName}` : propertyName;
159
- const initializer = property.getInitializer();
160
- return initializer ? flattenObjectExpression(initializer, nestedPrefix) : [];
161
- }
162
- if (Node.isShorthandPropertyAssignment(property)) {
163
- const propertyName = property.getName();
164
- return [{
165
- key: prefix ? `${prefix}.${propertyName}` : propertyName,
166
- value: resolveExpressionValue(property.getNameNode())
167
- }];
168
- }
169
- return [];
170
- });
171
- }
172
-
173
- //#endregion
174
- //#region src/extraction/hasDisableComment.ts
175
- const DISABLE_NEXT_LINE = "dittory-disable-next-line";
176
- const DISABLE_LINE = "dittory-disable-line";
177
- /**
178
- * ノードに除外コメントがあるかを判定する
179
- *
180
- * 以下の2パターンをサポート:
181
- * - "dittory-disable-next-line": 次の行を除外(leading comments をチェック)
182
- * - "dittory-disable-line": 同じ行を除外(trailing comments をチェック)
183
- *
184
- * 祖先ノードを辿り、いずれかのノードのコメントに
185
- * 除外キーワードが含まれていれば除外対象とする。
186
- *
187
- * @param node - 判定対象のノード
188
- * @returns 除外コメントが存在すれば true
189
- */
190
- function hasDisableComment(node) {
191
- let current = node;
192
- while (current) {
193
- const leadingComments = current.getLeadingCommentRanges();
194
- const trailingComments = current.getTrailingCommentRanges();
195
- for (const comment of leadingComments) if (comment.getText().includes(DISABLE_NEXT_LINE)) return true;
196
- for (const comment of trailingComments) if (comment.getText().includes(DISABLE_LINE)) return true;
197
- current = current.getParent();
198
- }
199
- return false;
200
- }
201
-
202
- //#endregion
203
- //#region src/extraction/extractUsages.ts
204
- /**
205
- * 使用状況を抽出するユーティリティクラス
206
- */
207
- var ExtractUsages = class {
208
- /**
209
- * 関数呼び出しから引数の使用状況を抽出する
210
- *
211
- * オブジェクトリテラルの場合は再帰的にフラット化し、
212
- * 各プロパティを「引数名.プロパティ名」形式で記録する。
213
- *
214
- * @param callExpression - 関数呼び出しノード
215
- * @param callable - 対象の関数情報
216
- * @returns 引数使用状況の配列
217
- */
218
- static fromCall(callExpression, callable) {
219
- if (hasDisableComment(callExpression)) return [];
220
- const usages = [];
221
- const sourceFile = callExpression.getSourceFile();
222
- const args = callExpression.getArguments();
223
- for (const param of callable.definitions) {
224
- const arg = args[param.index];
225
- if (!arg) {
226
- usages.push({
227
- name: param.name,
228
- value: UNDEFINED_VALUE,
229
- usageFilePath: sourceFile.getFilePath(),
230
- usageLine: callExpression.getStartLineNumber()
231
- });
232
- continue;
233
- }
234
- for (const { key, value } of flattenObjectExpression(arg, param.name)) usages.push({
235
- name: key,
236
- value,
237
- usageFilePath: sourceFile.getFilePath(),
238
- usageLine: arg.getStartLineNumber()
239
- });
240
- }
241
- return usages;
242
- }
243
- /**
244
- * JSX要素からprops使用状況を抽出する
245
- *
246
- * @param element - JSX要素ノード
247
- * @param definitions - props定義の配列
248
- * @returns props使用状況の配列
249
- */
250
- static fromJsxElement(element, definitions) {
251
- if (hasDisableComment(element)) return [];
252
- const usages = [];
253
- const sourceFile = element.getSourceFile();
254
- const attributeMap = /* @__PURE__ */ new Map();
255
- for (const attr of element.getAttributes()) if (Node.isJsxAttribute(attr)) attributeMap.set(attr.getNameNode().getText(), attr);
256
- for (const prop of definitions) {
257
- const attr = attributeMap.get(prop.name);
258
- if (!attr) {
259
- usages.push({
260
- name: prop.name,
261
- value: UNDEFINED_VALUE,
262
- usageFilePath: sourceFile.getFilePath(),
263
- usageLine: element.getStartLineNumber()
264
- });
265
- continue;
266
- }
267
- const initializer = attr.getInitializer();
268
- if (!initializer) usages.push({
269
- name: prop.name,
270
- value: "true",
271
- usageFilePath: sourceFile.getFilePath(),
272
- usageLine: attr.getStartLineNumber()
273
- });
274
- else if (Node.isJsxExpression(initializer)) {
275
- const expression = initializer.getExpression();
276
- if (!expression) continue;
277
- for (const { key, value } of flattenObjectExpression(expression, prop.name)) usages.push({
278
- name: key,
279
- value,
280
- usageFilePath: sourceFile.getFilePath(),
281
- usageLine: attr.getStartLineNumber()
282
- });
283
- } else usages.push({
284
- name: prop.name,
285
- value: initializer.getText(),
286
- usageFilePath: sourceFile.getFilePath(),
287
- usageLine: attr.getStartLineNumber()
288
- });
289
- }
290
- return usages;
291
- }
292
- };
293
-
294
- //#endregion
295
- //#region src/source/fileFilters.ts
296
- /**
297
- * ファイルパスがテストファイルまたはStorybookファイルかどうかを判定する
298
- * - 拡張子が .test.* / .spec.* / .stories.* のファイル
299
- * - __tests__ / __stories__ フォルダ内のファイル
300
- */
301
- function isTestOrStorybookFile(filePath) {
302
- if (/\.(test|spec|stories)\.(ts|tsx|js|jsx)$/.test(filePath)) return true;
303
- if (/\b__tests__\b|\b__stories__\b/.test(filePath)) return true;
304
- return false;
305
- }
306
-
307
- //#endregion
308
- //#region src/utils/getSingleValueFromSet.ts
309
- /**
310
- * Setから唯一の値を安全に取得する
311
- */
312
- function getSingleValueFromSet(values) {
313
- if (values.size !== 1) throw new Error(`Expected exactly 1 value, got ${values.size}`);
314
- const [firstValue] = Array.from(values);
315
- return firstValue;
316
- }
317
-
318
- //#endregion
319
- //#region src/analyzer/baseAnalyzer.ts
320
- /**
321
- * 分析処理の基底クラス
322
- */
323
- var BaseAnalyzer = class {
324
- shouldExcludeFile;
325
- minUsages;
326
- constructor(options = {}) {
327
- this.shouldExcludeFile = options.shouldExcludeFile ?? isTestOrStorybookFile;
328
- this.minUsages = options.minUsages ?? 2;
329
- }
330
- /**
331
- * メイン分析処理
332
- *
333
- * @param declarations - 事前分類済みの宣言配列
334
- */
335
- analyze(declarations) {
336
- const exported = this.collect(declarations);
337
- const groupedMap = this.createGroupedMap(exported);
338
- return {
339
- constants: this.extractConstants(groupedMap),
340
- exported
341
- };
342
- }
343
- /**
344
- * 使用状況をグループ化したマップを作成
345
- */
346
- createGroupedMap(exported) {
347
- const groupedMap = /* @__PURE__ */ new Map();
348
- for (const item of exported) {
349
- let fileMap = groupedMap.get(item.sourceFilePath);
350
- if (!fileMap) {
351
- fileMap = /* @__PURE__ */ new Map();
352
- groupedMap.set(item.sourceFilePath, fileMap);
353
- }
354
- const paramMap = /* @__PURE__ */ new Map();
355
- for (const [paramName, usages] of Object.entries(item.usages)) {
356
- const values = /* @__PURE__ */ new Set();
357
- for (const usage of usages) values.add(usage.value);
358
- paramMap.set(paramName, {
359
- values,
360
- usages
361
- });
362
- }
363
- fileMap.set(item.name, {
364
- line: item.sourceLine,
365
- params: paramMap
366
- });
367
- }
368
- return groupedMap;
369
- }
370
- /**
371
- * 常に同じ値が渡されているパラメータを抽出
372
- */
373
- extractConstants(groupedMap) {
374
- const result = [];
375
- for (const [sourceFile, targetMap] of groupedMap) for (const [targetName, targetInfo] of targetMap) for (const [paramName, usageData] of targetInfo.params) {
376
- if (!(usageData.usages.length >= this.minUsages && usageData.values.size === 1)) continue;
377
- const value = getSingleValueFromSet(usageData.values);
378
- if (value.startsWith(FUNCTION_VALUE_PREFIX)) continue;
379
- result.push({
380
- targetName,
381
- targetSourceFile: sourceFile,
382
- targetLine: targetInfo.line,
383
- paramName,
384
- value,
385
- usages: usageData.usages
386
- });
387
- }
388
- return result;
389
- }
390
- };
391
-
392
- //#endregion
393
- //#region src/analyzer/componentAnalyzer.ts
394
- /**
395
- * Reactコンポーネントのprops分析を行うAnalyzer
396
- *
397
- * exportされたReactコンポーネントを収集し、各コンポーネントのprops使用状況を分析する。
398
- * 常に同じ値が渡されているpropsを検出し、定数として報告する。
399
- *
400
- * @example
401
- * ```ts
402
- * const analyzer = new ComponentAnalyzer({ minUsages: 2 });
403
- * const result = analyzer.analyze(sourceFiles);
404
- * console.log(result.constants);
405
- * ```
406
- */
407
- var ComponentAnalyzer = class extends BaseAnalyzer {
408
- constructor(options = {}) {
409
- super(options);
410
- }
411
- /**
412
- * 事前分類済みの宣言からReactコンポーネントを収集する
413
- *
414
- * @param declarations - 事前分類済みの宣言配列
415
- * @returns exportされたコンポーネントとその使用状況の配列
416
- */
417
- collect(declarations) {
418
- const exportedComponents = [];
419
- for (const classified of declarations) {
420
- const { exportName, sourceFile, declaration } = classified;
421
- if (!Node.isFunctionDeclaration(declaration) && !Node.isVariableDeclaration(declaration)) continue;
422
- const nameNode = declaration.getNameNode();
423
- if (!nameNode || !Node.isIdentifier(nameNode)) continue;
424
- const references = nameNode.findReferences().flatMap((referencedSymbol) => referencedSymbol.getReferences()).filter((ref) => !this.shouldExcludeFile(ref.getSourceFile().getFilePath()));
425
- const props = getProps(declaration);
426
- const component = {
427
- name: exportName,
428
- sourceFilePath: sourceFile.getFilePath(),
429
- sourceLine: declaration.getStartLineNumber(),
430
- definitions: props,
431
- declaration,
432
- usages: {}
433
- };
434
- const groupedUsages = {};
435
- for (const reference of references) {
436
- const refNode = reference.getNode();
437
- const parent = refNode.getParent();
438
- if (!parent) continue;
439
- const jsxElement = parent.asKind(SyntaxKind.JsxOpeningElement) ?? parent.asKind(SyntaxKind.JsxSelfClosingElement);
440
- if (!jsxElement) continue;
441
- if (jsxElement.getTagNameNode() !== refNode) continue;
442
- const usages = ExtractUsages.fromJsxElement(jsxElement, component.definitions);
443
- for (const usage of usages) {
444
- if (!groupedUsages[usage.name]) groupedUsages[usage.name] = [];
445
- groupedUsages[usage.name].push(usage);
446
- }
447
- }
448
- component.usages = groupedUsages;
449
- exportedComponents.push(component);
450
- }
451
- return exportedComponents;
452
- }
453
- };
454
-
455
- //#endregion
456
- //#region src/components/isReactComponent.ts
457
- /**
458
- * 宣言がReactコンポーネントかどうかを判定する
459
- * - 関数がJSXを返しているかチェック
460
- * - React.FC型注釈を持っているかチェック
461
- */
462
- function isReactComponent(declaration) {
463
- if (Node.isFunctionDeclaration(declaration)) return containsJsx(declaration);
464
- if (Node.isVariableDeclaration(declaration)) {
465
- const initializer = declaration.getInitializer();
466
- if (!initializer) return false;
467
- if (Node.isArrowFunction(initializer) || Node.isFunctionExpression(initializer)) return containsJsx(initializer);
468
- if (Node.isCallExpression(initializer)) {
469
- const args = initializer.getArguments();
470
- for (const arg of args) if (Node.isArrowFunction(arg) || Node.isFunctionExpression(arg)) {
471
- if (containsJsx(arg)) return true;
472
- }
473
- }
474
- }
475
- return false;
476
- }
477
- /**
478
- * ノード内にJSX要素が含まれているかチェック
479
- * 効率化: 1回のトラバースで全てのJSX要素をチェック
480
- */
481
- function containsJsx(node) {
482
- let hasJsx = false;
483
- node.forEachDescendant((descendant) => {
484
- if (Node.isJsxElement(descendant) || Node.isJsxSelfClosingElement(descendant) || Node.isJsxFragment(descendant)) {
485
- hasJsx = true;
486
- return true;
487
- }
488
- });
489
- return hasJsx;
490
- }
491
-
492
- //#endregion
493
- //#region src/source/classifyDeclarations.ts
494
- /**
495
- * ソースファイルからexportされた関数/コンポーネント/クラス宣言を収集し、
496
- * 種別(react/function/class)を事前に分類する
497
- *
498
- * @param sourceFiles - 分析対象のソースファイル配列
499
- * @returns 分類済みの宣言配列
500
- */
501
- function classifyDeclarations(sourceFiles) {
502
- const results = [];
503
- for (const sourceFile of sourceFiles) {
504
- const exportedDecls = sourceFile.getExportedDeclarations();
505
- for (const [exportName, declarations] of exportedDecls) {
506
- const funcDecl = declarations.find((decl) => isFunctionLike(decl));
507
- if (funcDecl) {
508
- if (Node.isFunctionDeclaration(funcDecl) || Node.isVariableDeclaration(funcDecl)) {
509
- const type = isReactComponent(funcDecl) ? "react" : "function";
510
- results.push({
511
- exportName,
512
- sourceFile,
513
- declaration: funcDecl,
514
- type
515
- });
516
- }
517
- continue;
518
- }
519
- const classDecl = declarations.find((decl) => Node.isClassDeclaration(decl));
520
- if (classDecl && Node.isClassDeclaration(classDecl)) results.push({
521
- exportName,
522
- sourceFile,
523
- declaration: classDecl,
524
- type: "class"
525
- });
526
- }
527
- }
528
- return results;
529
- }
530
- /**
531
- * 宣言が関数的なもの(関数宣言、アロー関数、関数式)かどうかを判定
532
- */
533
- function isFunctionLike(declaration) {
534
- if (Node.isFunctionDeclaration(declaration)) return true;
535
- if (Node.isVariableDeclaration(declaration)) {
536
- const initializer = declaration.getInitializer();
537
- if (!initializer) return false;
538
- return Node.isArrowFunction(initializer) || Node.isFunctionExpression(initializer) || Node.isCallExpression(initializer);
539
- }
540
- return false;
541
- }
542
-
543
- //#endregion
544
- //#region src/analyzeProps.ts
545
- /**
546
- * コンポーネントのprops使用状況を解析し、常に同じ値が渡されているpropsを検出する
547
- *
548
- * @param sourceFiles - 解析対象のソースファイル配列
549
- * @param options - オプション設定
550
- * @returns 解析結果(定数props、統計情報、exportされたコンポーネント)
551
- *
552
- * @example
553
- * const project = new Project();
554
- * project.addSourceFilesAtPaths("src/**\/*.tsx");
555
- * const result = analyzePropsCore(project.getSourceFiles());
556
- */
557
- function analyzePropsCore(sourceFiles, options = {}) {
558
- const { shouldExcludeFile = isTestOrStorybookFile, minUsages = 2 } = options;
559
- const components = classifyDeclarations(sourceFiles).filter((decl) => decl.type === "react");
560
- return new ComponentAnalyzer({
561
- shouldExcludeFile,
562
- minUsages
563
- }).analyze(components);
564
- }
565
-
566
- //#endregion
567
- export { ExtractUsages as a, isTestOrStorybookFile as i, classifyDeclarations as n, BaseAnalyzer as r, analyzePropsCore as t };
568
- //# sourceMappingURL=analyzeProps-CoEqudRM.mjs.map