dittory 0.0.1 → 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/cli.mjs CHANGED
@@ -1,209 +1,111 @@
1
1
  #!/usr/bin/env node
2
- import { a as ExtractUsages, i as isTestOrStorybookFile, n as classifyDeclarations, r as BaseAnalyzer, t as analyzePropsCore } from "./analyzeProps-YWnY-Mf7.mjs";
3
- import { Node, Project, SyntaxKind } from "ts-morph";
4
- import fs from "node:fs";
2
+ import { i as isTestOrStorybookFile, n as analyzeFunctionsCore, r as collectCallSites, t as analyzePropsCore } from "./analyzeProps-B0TjCZhP.mjs";
3
+ import { Project } from "ts-morph";
5
4
  import path from "node:path";
5
+ import fs from "node:fs";
6
+ import { pathToFileURL } from "node:url";
6
7
 
7
- //#region src/analyzer/classMethodAnalyzer.ts
8
+ //#region src/cli/loadConfig.ts
9
+ /** コンフィグファイルの検索順序 */
10
+ const CONFIG_FILE_NAMES = [
11
+ "dittory.config.js",
12
+ "dittory.config.mjs",
13
+ "dittory.config.json"
14
+ ];
8
15
  /**
9
- * クラスメソッドの引数分析を行うAnalyzer
16
+ * コンフィグファイルを読み込む
10
17
  *
11
- * exportされたクラスのメソッド(static/instance)を収集し、
12
- * 各メソッドの引数使用状況を分析する。
13
- * 常に同じ値が渡されている引数を検出し、定数として報告する。
18
+ * 現在の作業ディレクトリから以下の順序でコンフィグファイルを探す:
19
+ * 1. dittory.config.js
20
+ * 2. dittory.config.mjs
21
+ * 3. dittory.config.json
14
22
  *
15
- * @example
16
- * ```ts
17
- * const analyzer = new ClassMethodAnalyzer({ minUsages: 2 });
18
- * const result = analyzer.analyze(declarations);
19
- * console.log(result.constants);
20
- * ```
23
+ * ファイルが存在しない場合は空のオブジェクトを返す。
24
+ *
25
+ * @returns コンフィグオブジェクト
26
+ * @throws {Error} コンフィグファイルの読み込みに失敗した場合
21
27
  */
22
- var ClassMethodAnalyzer = class extends BaseAnalyzer {
23
- constructor(options = {}) {
24
- super(options);
25
- }
26
- /**
27
- * 事前分類済みの宣言からクラスメソッドを収集する
28
- *
29
- * @param declarations - 事前分類済みの宣言配列(type: "class")
30
- * @returns クラスメソッドとその使用状況の配列(名前は「ClassName.methodName」形式)
31
- */
32
- collect(declarations) {
33
- const results = [];
34
- for (const classified of declarations) {
35
- const { exportName, sourceFile, declaration } = classified;
36
- if (!Node.isClassDeclaration(declaration)) continue;
37
- const methods = declaration.getMethods();
38
- for (const method of methods) {
39
- const methodName = method.getName();
40
- const parameters = this.getParameters(method);
41
- const callable = {
42
- name: `${exportName}.${methodName}`,
43
- sourceFilePath: sourceFile.getFilePath(),
44
- definitions: parameters,
45
- declaration: method,
46
- usages: {}
47
- };
48
- const nameNode = method.getNameNode();
49
- if (!Node.isIdentifier(nameNode)) continue;
50
- const references = nameNode.findReferences().flatMap((referencedSymbol) => referencedSymbol.getReferences()).filter((ref) => !this.shouldExcludeFile(ref.getSourceFile().getFilePath()));
51
- const groupedUsages = {};
52
- for (const reference of references) {
53
- const propertyAccess = reference.getNode().getParent();
54
- if (!propertyAccess || !Node.isPropertyAccessExpression(propertyAccess)) continue;
55
- const callExpression = propertyAccess.getParent();
56
- if (!callExpression || !Node.isCallExpression(callExpression)) continue;
57
- if (callExpression.getExpression() !== propertyAccess) continue;
58
- const usages = ExtractUsages.fromCall(callExpression, callable);
59
- for (const usage of usages) {
60
- if (!groupedUsages[usage.name]) groupedUsages[usage.name] = [];
61
- groupedUsages[usage.name].push(usage);
62
- }
63
- }
64
- callable.usages = groupedUsages;
65
- results.push(callable);
66
- }
67
- }
68
- return results;
28
+ async function loadConfig() {
29
+ const cwd = process.cwd();
30
+ for (const fileName of CONFIG_FILE_NAMES) {
31
+ const configPath = path.join(cwd, fileName);
32
+ if (!fs.existsSync(configPath)) continue;
33
+ if (fileName.endsWith(".json")) return loadJsonConfig(configPath);
34
+ return loadJsConfig(configPath);
69
35
  }
70
- /**
71
- * メソッドのパラメータ定義を取得する
72
- */
73
- getParameters(method) {
74
- return this.extractParameterDeclarations(method).map((param, index) => ({
75
- name: param.getName(),
76
- index,
77
- required: !param.hasQuestionToken() && !param.hasInitializer()
78
- }));
36
+ return {};
37
+ }
38
+ /**
39
+ * JSON コンフィグを読み込む
40
+ */
41
+ function loadJsonConfig(configPath) {
42
+ const content = fs.readFileSync(configPath, "utf-8");
43
+ try {
44
+ const config = JSON.parse(content);
45
+ if (typeof config !== "object" || config === null) throw new Error(`Invalid config: expected object, got ${typeof config}`);
46
+ return validateConfig(config);
47
+ } catch (error) {
48
+ if (error instanceof SyntaxError) throw new Error(`Failed to parse ${path.basename(configPath)}: ${error.message}`);
49
+ throw error;
79
50
  }
80
- /**
81
- * メソッド宣言からParameterDeclarationの配列を抽出する
82
- */
83
- extractParameterDeclarations(method) {
84
- if (Node.isMethodDeclaration(method)) return method.getParameters();
85
- return [];
51
+ }
52
+ /**
53
+ * JS コンフィグを読み込む
54
+ */
55
+ async function loadJsConfig(configPath) {
56
+ try {
57
+ const config = (await import(pathToFileURL(configPath).href)).default;
58
+ if (typeof config !== "object" || config === null) throw new Error(`Invalid config: expected object, got ${typeof config}`);
59
+ return validateConfig(config);
60
+ } catch (error) {
61
+ if (error instanceof Error) throw new Error(`Failed to load ${path.basename(configPath)}: ${error.message}`);
62
+ throw error;
86
63
  }
87
- };
88
-
89
- //#endregion
90
- //#region src/analyzer/functionAnalyzer.ts
64
+ }
65
+ const VALID_TARGETS$1 = [
66
+ "all",
67
+ "components",
68
+ "functions"
69
+ ];
70
+ const VALID_OUTPUTS$1 = ["simple", "verbose"];
91
71
  /**
92
- * 関数の引数分析を行うAnalyzer
93
- *
94
- * exportされた関数を収集し、各関数の引数使用状況を分析する。
95
- * 常に同じ値が渡されている引数を検出し、定数として報告する。
96
- *
97
- * @example
98
- * ```ts
99
- * const analyzer = new FunctionAnalyzer({ minUsages: 2 });
100
- * const result = analyzer.analyze(declarations);
101
- * console.log(result.constants);
102
- * ```
72
+ * コンフィグの値を検証する
103
73
  */
104
- var FunctionAnalyzer = class extends BaseAnalyzer {
105
- constructor(options = {}) {
106
- super(options);
74
+ function validateConfig(config) {
75
+ const result = {};
76
+ if ("minUsages" in config) {
77
+ if (typeof config.minUsages !== "number" || config.minUsages < 1) throw new Error(`Invalid config: minUsages must be a number >= 1, got ${config.minUsages}`);
78
+ result.minUsages = config.minUsages;
107
79
  }
108
- /**
109
- * 事前分類済みの宣言から関数を収集する
110
- *
111
- * @param declarations - 事前分類済みの宣言配列(type: "function")
112
- * @returns exportされた関数とその使用状況の配列
113
- */
114
- collect(declarations) {
115
- const results = [];
116
- for (const classified of declarations) {
117
- const { exportName, sourceFile, declaration } = classified;
118
- if (!Node.isFunctionDeclaration(declaration) && !Node.isVariableDeclaration(declaration)) continue;
119
- const nameNode = declaration.getNameNode();
120
- if (!nameNode || !Node.isIdentifier(nameNode)) continue;
121
- const references = nameNode.findReferences().flatMap((referencedSymbol) => referencedSymbol.getReferences()).filter((ref) => !this.shouldExcludeFile(ref.getSourceFile().getFilePath()));
122
- const parameters = this.getParameters(declaration);
123
- const callable = {
124
- name: exportName,
125
- sourceFilePath: sourceFile.getFilePath(),
126
- definitions: parameters,
127
- declaration,
128
- usages: {}
129
- };
130
- const groupedUsages = {};
131
- for (const reference of references) {
132
- const refNode = reference.getNode();
133
- const parent = refNode.getParent();
134
- if (!parent) continue;
135
- const callExpression = parent.asKind(SyntaxKind.CallExpression);
136
- if (!callExpression) continue;
137
- if (callExpression.getExpression() !== refNode) continue;
138
- const usages = ExtractUsages.fromCall(callExpression, callable);
139
- for (const usage of usages) {
140
- if (!groupedUsages[usage.name]) groupedUsages[usage.name] = [];
141
- groupedUsages[usage.name].push(usage);
142
- }
143
- }
144
- callable.usages = groupedUsages;
145
- results.push(callable);
146
- }
147
- return results;
80
+ if ("target" in config) {
81
+ if (!VALID_TARGETS$1.includes(config.target)) throw new Error(`Invalid config: target must be one of ${VALID_TARGETS$1.join(", ")}, got ${config.target}`);
82
+ result.target = config.target;
148
83
  }
149
- /**
150
- * 関数のパラメータ定義を取得する
151
- */
152
- getParameters(declaration) {
153
- return this.extractParameterDeclarations(declaration).map((param, index) => ({
154
- name: param.getName(),
155
- index,
156
- required: !param.hasQuestionToken() && !param.hasInitializer()
157
- }));
84
+ if ("output" in config) {
85
+ if (!VALID_OUTPUTS$1.includes(config.output)) throw new Error(`Invalid config: output must be one of ${VALID_OUTPUTS$1.join(", ")}, got ${config.output}`);
86
+ result.output = config.output;
158
87
  }
159
- /**
160
- * 宣言からParameterDeclarationの配列を抽出する
161
- */
162
- extractParameterDeclarations(declaration) {
163
- if (Node.isFunctionDeclaration(declaration)) return declaration.getParameters();
164
- if (Node.isVariableDeclaration(declaration)) {
165
- const initializer = declaration.getInitializer();
166
- if (initializer) {
167
- if (Node.isArrowFunction(initializer) || Node.isFunctionExpression(initializer)) return initializer.getParameters();
168
- }
169
- }
170
- return [];
88
+ if ("tsconfig" in config) {
89
+ if (typeof config.tsconfig !== "string" || config.tsconfig === "") throw new Error(`Invalid config: tsconfig must be a non-empty string, got ${config.tsconfig}`);
90
+ result.tsconfig = config.tsconfig;
171
91
  }
172
- };
173
-
174
- //#endregion
175
- //#region src/analyzeFunctions.ts
176
- /**
177
- * 関数・クラスメソッドの引数使用状況を解析し、常に同じ値が渡されている引数を検出する
178
- *
179
- * @param sourceFiles - 解析対象のソースファイル配列
180
- * @param options - オプション設定
181
- * @returns 解析結果(定数引数、統計情報、exportされた関数・メソッド)
182
- *
183
- * @example
184
- * const project = new Project();
185
- * project.addSourceFilesAtPaths("src/**\/*.ts");
186
- * const result = analyzeFunctionsCore(project.getSourceFiles());
187
- */
188
- function analyzeFunctionsCore(sourceFiles, options = {}) {
189
- const { shouldExcludeFile = isTestOrStorybookFile, minUsages = 2 } = options;
190
- const declarations = classifyDeclarations(sourceFiles);
191
- const functions = declarations.filter((decl) => decl.type === "function");
192
- const classes = declarations.filter((decl) => decl.type === "class");
193
- const analyzerOptions = {
194
- shouldExcludeFile,
195
- minUsages
196
- };
197
- const functionResult = new FunctionAnalyzer(analyzerOptions).analyze(functions);
198
- const classMethodResult = new ClassMethodAnalyzer(analyzerOptions).analyze(classes);
199
- return {
200
- constants: [...functionResult.constants, ...classMethodResult.constants],
201
- exported: [...functionResult.exported, ...classMethodResult.exported]
202
- };
92
+ if ("targetDir" in config) {
93
+ if (typeof config.targetDir !== "string" || config.targetDir === "") throw new Error(`Invalid config: targetDir must be a non-empty string, got ${config.targetDir}`);
94
+ result.targetDir = config.targetDir;
95
+ }
96
+ return result;
203
97
  }
204
98
 
205
99
  //#endregion
206
100
  //#region src/cli/parseCliOptions.ts
101
+ /** デフォルトのオプション値 */
102
+ const DEFAULT_OPTIONS = {
103
+ targetDir: "./src",
104
+ minUsages: 2,
105
+ target: "all",
106
+ output: "simple",
107
+ tsconfig: "./tsconfig.json"
108
+ };
207
109
  var CliValidationError = class extends Error {
208
110
  constructor(message) {
209
111
  super(message);
@@ -215,48 +117,53 @@ const VALID_TARGETS = [
215
117
  "components",
216
118
  "functions"
217
119
  ];
120
+ const VALID_OUTPUTS = ["simple", "verbose"];
218
121
  /** 不明なオプションの検出に使用 */
219
122
  const VALID_OPTIONS = [
220
123
  "--min",
221
124
  "--target",
125
+ "--output",
126
+ "--tsconfig",
222
127
  "--help"
223
128
  ];
224
129
  /**
225
130
  * CLIオプションをパースする
226
131
  *
132
+ * 明示的に指定されたオプションのみを返す(デフォルト値は含まない)
133
+ *
227
134
  * @throws {CliValidationError} オプションが無効な場合
228
135
  */
229
136
  function parseCliOptions(args) {
230
- let targetDir = path.join(process.cwd(), "src");
231
- let minUsages = 2;
232
- let target = "all";
233
- let showHelp = false;
137
+ const result = { showHelp: false };
234
138
  for (const arg of args) {
235
139
  if (arg === "--help") {
236
- showHelp = true;
140
+ result.showHelp = true;
237
141
  continue;
238
142
  }
239
143
  if (arg.startsWith("--min=")) {
240
144
  const valueStr = arg.slice(6);
241
145
  const value = Number.parseInt(valueStr, 10);
242
- if (valueStr === "" || Number.isNaN(value)) throw new CliValidationError(`--min の値が無効です: "${valueStr}" (数値を指定してください)`);
243
- if (value < 1) throw new CliValidationError(`--min の値は1以上である必要があります: ${value}`);
244
- minUsages = value;
146
+ if (valueStr === "" || Number.isNaN(value)) throw new CliValidationError(`Invalid value for --min: "${valueStr}" (must be a number)`);
147
+ if (value < 1) throw new CliValidationError(`--min must be at least 1: ${value}`);
148
+ result.minUsages = value;
245
149
  } else if (arg.startsWith("--target=")) {
246
150
  const value = arg.slice(9);
247
- if (!VALID_TARGETS.includes(value)) throw new CliValidationError(`--target の値が無効です: "${value}" (有効な値: ${VALID_TARGETS.join(", ")})`);
248
- target = value;
151
+ if (!VALID_TARGETS.includes(value)) throw new CliValidationError(`Invalid value for --target: "${value}" (valid values: ${VALID_TARGETS.join(", ")})`);
152
+ result.target = value;
153
+ } else if (arg.startsWith("--output=")) {
154
+ const value = arg.slice(9);
155
+ if (!VALID_OUTPUTS.includes(value)) throw new CliValidationError(`Invalid value for --output: "${value}" (valid values: ${VALID_OUTPUTS.join(", ")})`);
156
+ result.output = value;
157
+ } else if (arg.startsWith("--tsconfig=")) {
158
+ const value = arg.slice(11);
159
+ if (value === "") throw new CliValidationError("Invalid value for --tsconfig: path cannot be empty");
160
+ result.tsconfig = value;
249
161
  } else if (arg.startsWith("--")) {
250
162
  const optionName = arg.split("=")[0];
251
- if (!VALID_OPTIONS.includes(optionName)) throw new CliValidationError(`不明なオプション: ${optionName}`);
252
- } else targetDir = arg;
163
+ if (!VALID_OPTIONS.includes(optionName)) throw new CliValidationError(`Unknown option: ${optionName}`);
164
+ } else result.targetDir = arg;
253
165
  }
254
- return {
255
- targetDir,
256
- minUsages,
257
- target,
258
- showHelp
259
- };
166
+ return result;
260
167
  }
261
168
  /**
262
169
  * 対象ディレクトリの存在を検証する
@@ -264,8 +171,16 @@ function parseCliOptions(args) {
264
171
  * @throws {CliValidationError} ディレクトリが存在しない、またはディレクトリでない場合
265
172
  */
266
173
  function validateTargetDir(targetDir) {
267
- if (!fs.existsSync(targetDir)) throw new CliValidationError(`ディレクトリが存在しません: ${targetDir}`);
268
- if (!fs.statSync(targetDir).isDirectory()) throw new CliValidationError(`指定されたパスはディレクトリではありません: ${targetDir}`);
174
+ if (!fs.existsSync(targetDir)) throw new CliValidationError(`Directory does not exist: ${targetDir}`);
175
+ if (!fs.statSync(targetDir).isDirectory()) throw new CliValidationError(`Path is not a directory: ${targetDir}`);
176
+ }
177
+ /**
178
+ * tsconfig.json の存在を検証する
179
+ *
180
+ * @throws {CliValidationError} ファイルが存在しない場合
181
+ */
182
+ function validateTsConfig(tsConfigPath) {
183
+ if (!fs.existsSync(tsConfigPath)) throw new CliValidationError(`tsconfig not found: ${tsConfigPath}`);
269
184
  }
270
185
  /**
271
186
  * ヘルプメッセージを取得する
@@ -275,67 +190,70 @@ function getHelpMessage() {
275
190
  Usage: dittory [options] [directory]
276
191
 
277
192
  Options:
278
- --min=<number> 最小使用箇所数 (デフォルト: 2)
279
- --target=<mode> 解析対象: all, components, functions (デフォルト: all)
280
- --help このヘルプを表示
193
+ --min=<number> Minimum usage count (default: 2)
194
+ --target=<mode> Analysis target: all, components, functions (default: all)
195
+ --output=<mode> Output mode: simple, verbose (default: simple)
196
+ --tsconfig=<path> Path to tsconfig.json (default: ./tsconfig.json)
197
+ --help Show this help message
281
198
 
282
199
  Arguments:
283
- directory 解析対象ディレクトリ (デフォルト: ./src)
200
+ directory Target directory to analyze (default: ./src)
284
201
  `;
285
202
  }
286
203
 
287
204
  //#endregion
288
205
  //#region src/output/printAnalysisResult.ts
206
+ function bold(text) {
207
+ return `\x1b[1m${text}\x1b[0m`;
208
+ }
209
+ function green(text) {
210
+ return `\x1b[32m${text}\x1b[0m`;
211
+ }
289
212
  /**
290
- * exportされたコンポーネントの一覧を出力
213
+ * 値を表示用にフォーマットする
214
+ *
215
+ * 内部的にはenum区別のためにファイルパスを含むが、表示時は不要なので除去する
216
+ * 例: "/path/to/file.ts:ButtonVariant.Primary=\"primary\"" → "ButtonVariant.Primary"
291
217
  */
292
- function printExportedComponents(exported) {
293
- const lines = [
294
- "1. exportされたコンポーネントを収集中...",
295
- ` → ${exported.length}個のコンポーネントを検出`,
296
- ...exported.map((comp) => ` - ${comp.name} (${path.relative(process.cwd(), comp.sourceFilePath)})`),
297
- ""
298
- ];
299
- console.log(lines.join("\n"));
218
+ function formatValueForDisplay(value) {
219
+ const enumMatch = value.match(/^.+:(\w+\.\w+)=.+$/);
220
+ if (enumMatch) return enumMatch[1];
221
+ return value;
300
222
  }
301
223
  /**
302
- * 常に同じ値が渡されているpropsを出力
224
+ * Constant[]を関数/コンポーネント単位でグループ化する
303
225
  */
304
- function printConstantProps(constants) {
305
- console.log("=== 常に同じ値が渡されているprops ===\n");
306
- if (constants.length === 0) {
307
- console.log("常に同じ値が渡されているpropsは見つかりませんでした。");
308
- return;
309
- }
310
- for (const prop of constants) {
311
- const relativeComponentPath = path.relative(process.cwd(), prop.targetSourceFile);
312
- console.log(`コンポーネント: ${prop.targetName}`);
313
- console.log(` 定義: ${relativeComponentPath}`);
314
- console.log(` prop: ${prop.paramName}`);
315
- console.log(` 常に渡される値: ${prop.value}`);
316
- console.log(` 使用箇所: ${prop.usages.length}箇所`);
317
- for (const usage of prop.usages) {
318
- const relativePath = path.relative(process.cwd(), usage.usageFilePath);
319
- console.log(` - ${relativePath}:${usage.usageLine}`);
226
+ function groupConstantsByTarget(constants) {
227
+ const groupMap = /* @__PURE__ */ new Map();
228
+ for (const constant of constants) {
229
+ const key = `${constant.targetSourceFile}:${constant.targetName}`;
230
+ let group = groupMap.get(key);
231
+ if (!group) {
232
+ group = {
233
+ targetName: constant.targetName,
234
+ targetSourceFile: constant.targetSourceFile,
235
+ targetLine: constant.targetLine,
236
+ params: []
237
+ };
238
+ groupMap.set(key, group);
320
239
  }
321
- console.log("");
240
+ group.params.push({
241
+ paramName: constant.paramName,
242
+ value: constant.value,
243
+ usageCount: constant.usages.length,
244
+ usages: constant.usages
245
+ });
322
246
  }
323
- }
324
- /**
325
- * 解析結果を全て出力
326
- */
327
- function printAnalysisResult(result) {
328
- printExportedComponents(result.exported);
329
- printConstantProps(result.constants);
247
+ return Array.from(groupMap.values());
330
248
  }
331
249
  /**
332
250
  * exportされた関数の一覧を出力
333
251
  */
334
252
  function printExportedFunctions(exported) {
335
253
  const lines = [
336
- "2. exportされた関数を収集中...",
337
- ` → ${exported.length}個の関数を検出`,
338
- ...exported.map((fn) => ` - ${fn.name} (${path.relative(process.cwd(), fn.sourceFilePath)})`),
254
+ "Collecting exported functions...",
255
+ ` → Found ${exported.length} function(s)`,
256
+ ...exported.map((fn) => ` - ${bold(green(fn.name))} (${path.relative(process.cwd(), fn.sourceFilePath)})`),
339
257
  ""
340
258
  ];
341
259
  console.log(lines.join("\n"));
@@ -344,31 +262,40 @@ function printExportedFunctions(exported) {
344
262
  * 常に同じ値が渡されている引数を出力
345
263
  */
346
264
  function printConstantArguments(constants) {
347
- console.log("=== 常に同じ値が渡されている引数 ===\n");
348
- if (constants.length === 0) {
349
- console.log("常に同じ値が渡されている引数は見つかりませんでした。");
350
- return;
351
- }
352
- for (const arg of constants) {
353
- const relativeFunctionPath = path.relative(process.cwd(), arg.targetSourceFile);
354
- console.log(`関数: ${arg.targetName}`);
355
- console.log(` 定義: ${relativeFunctionPath}`);
356
- console.log(` 引数: ${arg.paramName}`);
357
- console.log(` 常に渡される値: ${arg.value}`);
358
- console.log(` 使用箇所: ${arg.usages.length}箇所`);
359
- for (const usage of arg.usages) {
360
- const relativePath = path.relative(process.cwd(), usage.usageFilePath);
361
- console.log(` - ${relativePath}:${usage.usageLine}`);
265
+ if (constants.length === 0) return;
266
+ const grouped = groupConstantsByTarget(constants);
267
+ for (const group of grouped) {
268
+ const relativePath = path.relative(process.cwd(), group.targetSourceFile);
269
+ const usageCount = group.params[0]?.usageCount ?? 0;
270
+ const usages = group.params[0]?.usages ?? [];
271
+ console.log(`${bold(green(group.targetName))} ${relativePath}:${group.targetLine}`);
272
+ console.log("Constant Arguments:");
273
+ for (const param of group.params) console.log(` - ${param.paramName} = ${formatValueForDisplay(param.value)}`);
274
+ console.log(`Usages (${usageCount}):`);
275
+ for (const usage of usages) {
276
+ const usagePath = path.relative(process.cwd(), usage.usageFilePath);
277
+ console.log(` - ${usagePath}:${usage.usageLine}`);
362
278
  }
363
- console.log("");
279
+ console.log("\n");
364
280
  }
365
281
  }
366
282
  /**
367
- * 関数解析結果を全て出力
283
+ * 統計情報を出力
284
+ */
285
+ function printStatistics(result) {
286
+ const totalFunctions = result.exported.length;
287
+ const functionsWithConstants = groupConstantsByTarget(result.constants).length;
288
+ console.log("---");
289
+ console.log(`Found ${functionsWithConstants} function(s) with constant arguments out of ${totalFunctions} function(s).`);
290
+ }
291
+ /**
292
+ * 解析結果を出力
368
293
  */
369
- function printFunctionAnalysisResult(result) {
370
- printExportedFunctions(result.exported);
371
- printConstantArguments(result.constants);
294
+ function printAnalysisResult(result, mode) {
295
+ if (mode === "verbose") printExportedFunctions(result.exported);
296
+ if (result.constants.length === 0) console.log("No arguments with constant values were found.");
297
+ else printConstantArguments(result.constants);
298
+ printStatistics(result);
372
299
  }
373
300
 
374
301
  //#endregion
@@ -376,9 +303,10 @@ function printFunctionAnalysisResult(result) {
376
303
  /**
377
304
  * プロジェクトを初期化し、フィルタリングされたソースファイルを取得する
378
305
  */
379
- function createFilteredSourceFiles(targetDir, shouldExcludeFile = isTestOrStorybookFile) {
306
+ function createFilteredSourceFiles(targetDir, options = {}) {
307
+ const { shouldExcludeFile = isTestOrStorybookFile, tsConfigFilePath = path.join(process.cwd(), "tsconfig.json") } = options;
380
308
  const project = new Project({
381
- tsConfigFilePath: path.join(process.cwd(), "tsconfig.json"),
309
+ tsConfigFilePath,
382
310
  skipAddingFilesFromTsConfig: true
383
311
  });
384
312
  project.addSourceFilesAtPaths(`${targetDir}/**/*.{ts,tsx,js,jsx}`);
@@ -391,36 +319,81 @@ function createFilteredSourceFiles(targetDir, shouldExcludeFile = isTestOrStoryb
391
319
  * エラーメッセージを表示してプロセスを終了する
392
320
  */
393
321
  function exitWithError(message) {
394
- console.error(`エラー: ${message}`);
322
+ console.error(`Error: ${message}`);
395
323
  process.exit(1);
396
324
  }
397
- function main() {
398
- let options;
325
+ async function main() {
326
+ let cliOptions;
399
327
  try {
400
- options = parseCliOptions(process.argv.slice(2));
328
+ cliOptions = parseCliOptions(process.argv.slice(2));
401
329
  } catch (error) {
402
330
  if (error instanceof CliValidationError) exitWithError(error.message);
403
331
  throw error;
404
332
  }
405
- const { targetDir, minUsages, target, showHelp } = options;
406
- if (showHelp) {
333
+ if (cliOptions.showHelp) {
407
334
  console.log(getHelpMessage());
408
335
  process.exit(0);
409
336
  }
337
+ let fileConfig;
338
+ try {
339
+ fileConfig = await loadConfig();
340
+ } catch (error) {
341
+ if (error instanceof Error) exitWithError(error.message);
342
+ throw error;
343
+ }
344
+ const { targetDir, minUsages, target, output, tsconfig } = {
345
+ targetDir: path.resolve(cliOptions.targetDir ?? fileConfig.targetDir ?? DEFAULT_OPTIONS.targetDir),
346
+ minUsages: cliOptions.minUsages ?? fileConfig.minUsages ?? DEFAULT_OPTIONS.minUsages,
347
+ target: cliOptions.target ?? fileConfig.target ?? DEFAULT_OPTIONS.target,
348
+ output: cliOptions.output ?? fileConfig.output ?? DEFAULT_OPTIONS.output,
349
+ tsconfig: cliOptions.tsconfig ?? fileConfig.tsconfig ?? DEFAULT_OPTIONS.tsconfig
350
+ };
410
351
  try {
411
352
  validateTargetDir(targetDir);
412
353
  } catch (error) {
413
354
  if (error instanceof CliValidationError) exitWithError(error.message);
414
355
  throw error;
415
356
  }
416
- console.log(`解析対象ディレクトリ: ${targetDir}`);
417
- console.log(`最小使用箇所数: ${minUsages}`);
418
- console.log(`解析対象: ${target}\n`);
419
- const sourceFilesToAnalyze = createFilteredSourceFiles(targetDir);
420
- if (target === "all" || target === "components") printAnalysisResult(analyzePropsCore(sourceFilesToAnalyze, { minUsages }));
421
- if (target === "all" || target === "functions") printFunctionAnalysisResult(analyzeFunctionsCore(sourceFilesToAnalyze, { minUsages }));
357
+ try {
358
+ validateTsConfig(tsconfig);
359
+ } catch (error) {
360
+ if (error instanceof CliValidationError) exitWithError(error.message);
361
+ throw error;
362
+ }
363
+ if (output === "verbose") {
364
+ console.log(`Target directory: ${targetDir}`);
365
+ console.log(`Minimum usage count: ${minUsages}`);
366
+ console.log(`Analysis target: ${target}\n`);
367
+ }
368
+ const sourceFilesToAnalyze = createFilteredSourceFiles(targetDir, { tsConfigFilePath: tsconfig });
369
+ const callSiteMap = collectCallSites(sourceFilesToAnalyze);
370
+ const allExported = [];
371
+ const allConstants = [];
372
+ if (target === "all" || target === "components") {
373
+ const propsResult = analyzePropsCore(sourceFilesToAnalyze, {
374
+ minUsages,
375
+ callSiteMap
376
+ });
377
+ allExported.push(...propsResult.exported);
378
+ allConstants.push(...propsResult.constants);
379
+ }
380
+ if (target === "all" || target === "functions") {
381
+ const functionsResult = analyzeFunctionsCore(sourceFilesToAnalyze, {
382
+ minUsages,
383
+ callSiteMap
384
+ });
385
+ allExported.push(...functionsResult.exported);
386
+ allConstants.push(...functionsResult.constants);
387
+ }
388
+ printAnalysisResult({
389
+ exported: allExported,
390
+ constants: allConstants
391
+ }, output);
422
392
  }
423
- main();
393
+ main().catch((error) => {
394
+ console.error(error);
395
+ process.exit(1);
396
+ });
424
397
 
425
398
  //#endregion
426
399
  export { };