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/README.md +232 -38
- package/dist/analyzeProps-B0TjCZhP.mjs +1103 -0
- package/dist/analyzeProps-B0TjCZhP.mjs.map +1 -0
- package/dist/cli.mjs +255 -282
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +101 -3
- package/dist/index.mjs +2 -2
- package/package.json +11 -12
- package/dist/analyzeProps-YWnY-Mf7.mjs +0 -532
- package/dist/analyzeProps-YWnY-Mf7.mjs.map +0 -1
package/dist/cli.mjs
CHANGED
|
@@ -1,209 +1,111 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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/
|
|
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
|
-
*
|
|
16
|
+
* コンフィグファイルを読み込む
|
|
10
17
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
18
|
+
* 現在の作業ディレクトリから以下の順序でコンフィグファイルを探す:
|
|
19
|
+
* 1. dittory.config.js
|
|
20
|
+
* 2. dittory.config.mjs
|
|
21
|
+
* 3. dittory.config.json
|
|
14
22
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* console.log(result.constants);
|
|
20
|
-
* ```
|
|
23
|
+
* ファイルが存在しない場合は空のオブジェクトを返す。
|
|
24
|
+
*
|
|
25
|
+
* @returns コンフィグオブジェクト
|
|
26
|
+
* @throws {Error} コンフィグファイルの読み込みに失敗した場合
|
|
21
27
|
*/
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
90
|
-
|
|
64
|
+
}
|
|
65
|
+
const VALID_TARGETS$1 = [
|
|
66
|
+
"all",
|
|
67
|
+
"components",
|
|
68
|
+
"functions"
|
|
69
|
+
];
|
|
70
|
+
const VALID_OUTPUTS$1 = ["simple", "verbose"];
|
|
91
71
|
/**
|
|
92
|
-
*
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
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
|
-
|
|
175
|
-
|
|
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
|
-
|
|
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(
|
|
243
|
-
if (value < 1) throw new CliValidationError(`--min
|
|
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(
|
|
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(
|
|
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(
|
|
268
|
-
if (!fs.statSync(targetDir).isDirectory()) throw new CliValidationError(
|
|
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>
|
|
279
|
-
--target=<mode>
|
|
280
|
-
--
|
|
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
|
|
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
|
-
*
|
|
213
|
+
* 値を表示用にフォーマットする
|
|
214
|
+
*
|
|
215
|
+
* 内部的にはenum区別のためにファイルパスを含むが、表示時は不要なので除去する
|
|
216
|
+
* 例: "/path/to/file.ts:ButtonVariant.Primary=\"primary\"" → "ButtonVariant.Primary"
|
|
291
217
|
*/
|
|
292
|
-
function
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
*
|
|
224
|
+
* Constant[]を関数/コンポーネント単位でグループ化する
|
|
303
225
|
*/
|
|
304
|
-
function
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
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
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
console.log(
|
|
355
|
-
console.log(`
|
|
356
|
-
console.log(`
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
|
370
|
-
printExportedFunctions(result.exported);
|
|
371
|
-
|
|
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,
|
|
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
|
|
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(
|
|
322
|
+
console.error(`Error: ${message}`);
|
|
395
323
|
process.exit(1);
|
|
396
324
|
}
|
|
397
|
-
function main() {
|
|
398
|
-
let
|
|
325
|
+
async function main() {
|
|
326
|
+
let cliOptions;
|
|
399
327
|
try {
|
|
400
|
-
|
|
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
|
-
|
|
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
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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 { };
|