dittory 0.0.1 → 0.0.2

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 CHANGED
@@ -1,73 +1,165 @@
1
1
  # dittory
2
2
 
3
- Reactコンポーネントや関数の引数使用状況を静的解析し、**常に同じ値が渡されているパラメータ**を検出するCLIツールです。
3
+ [![npm version](https://img.shields.io/npm/v/dittory.svg)](https://www.npmjs.com/package/dittory)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4
5
 
5
- ## 用途
6
+ A static analysis CLI that detects **parameters always receiving the same value** in React components and functions.
6
7
 
7
- - 不要なパラメータの発見(デフォルト値化の候補)
8
- - コードベースのリファクタリング支援
9
- - APIの簡素化
8
+ > **dittory** = "ditto" (same) + "-ory" — finds repetitive patterns in your code
10
9
 
11
- ## インストール
10
+ ## Why?
11
+
12
+ When a prop or argument is always passed the same value across your codebase, it's often a sign that:
13
+
14
+ - The parameter could be **removed** and replaced with a default value
15
+ - The API could be **simplified** by eliminating unnecessary options
16
+ - There's **copy-paste code** that should be refactored
17
+
18
+ dittory helps you identify these opportunities automatically.
19
+
20
+ ## Installation
12
21
 
13
22
  ```bash
14
23
  npm install -g dittory
15
24
  ```
16
25
 
17
- ## 使い方
26
+ Or use directly with npx:
27
+
28
+ ```bash
29
+ npx dittory
30
+ ```
31
+
32
+ ## Usage
18
33
 
19
34
  ```bash
20
- # srcディレクトリを解析(デフォルト)
35
+ # Analyze ./src directory (default)
21
36
  dittory
22
37
 
23
- # 特定のディレクトリを解析
38
+ # Analyze a specific directory
24
39
  dittory ./path/to/src
25
40
 
26
- # 最小使用回数を指定(デフォルト: 2
41
+ # Set minimum usage count (default: 2)
27
42
  dittory --min=3
28
43
 
29
- # 解析対象を指定
30
- dittory --target=components # Reactコンポーネントのみ
31
- dittory --target=functions # 関数・クラスメソッドのみ
32
- dittory --target=all # 両方(デフォルト)
44
+ # Analyze specific targets
45
+ dittory --target=components # React components only
46
+ dittory --target=functions # Functions and class methods only
47
+ dittory --target=all # Both (default)
48
+
49
+ # Output mode
50
+ dittory --output=simple # Show only constants (default)
51
+ dittory --output=verbose # Show all exported functions and details
52
+ ```
53
+
54
+ ## Example Output
55
+
56
+ ```
57
+ Button src/components/Button.tsx:15
58
+ Constant Arguments:
59
+ - variant = "primary"
60
+ Usages (5):
61
+ - src/pages/Home.tsx:23
62
+ - src/pages/About.tsx:45
63
+ - src/pages/Contact.tsx:12
64
+ - src/features/auth/Login.tsx:67
65
+ - src/features/auth/Register.tsx:89
66
+
67
+
68
+ fetchUser src/api/users.ts:42
69
+ Constant Arguments:
70
+ - includeProfile = true
71
+ - cache = false
72
+ Usages (3):
73
+ - src/hooks/useUser.ts:18
74
+ - src/pages/Profile.tsx:31
75
+ - src/components/UserCard.tsx:55
76
+
77
+
78
+ ---
79
+ Found 2 function(s) with constant arguments out of 24 function(s).
80
+ ```
33
81
 
34
- # ヘルプ
35
- dittory --help
82
+ ## What It Detects
83
+
84
+ | Target | Description |
85
+ |--------|-------------|
86
+ | **React Components** | Props passed to JSX elements (`<Button variant="primary" />`) |
87
+ | **Functions** | Arguments passed to exported function calls |
88
+ | **Class Methods** | Arguments passed to methods of exported classes |
89
+
90
+ ## CLI Options
91
+
92
+ | Option | Description | Default |
93
+ |--------|-------------|---------|
94
+ | `--min=<n>` | Minimum number of usages to consider | `2` |
95
+ | `--target=<mode>` | What to analyze: `all`, `components`, `functions` | `all` |
96
+ | `--output=<mode>` | Output verbosity: `simple`, `verbose` | `simple` |
97
+ | `--tsconfig=<path>` | Path to tsconfig.json | `./tsconfig.json` |
98
+ | `--help` | Show help message | — |
99
+
100
+ ## Configuration File
101
+
102
+ Create a configuration file to set default options. dittory looks for:
103
+
104
+ 1. `dittory.config.js` or `dittory.config.mjs`
105
+ 2. `dittory.config.json`
106
+
107
+ ```js
108
+ // dittory.config.js
109
+ /** @type {import('dittory').DittoryConfig} */
110
+ export default {
111
+ minUsages: 3,
112
+ target: "components",
113
+ output: "verbose",
114
+ tsconfig: "./tsconfig.app.json",
115
+ targetDir: "./src",
116
+ };
36
117
  ```
37
118
 
38
- ## 出力例
119
+ **Priority:** CLI options > Config file > Default values
120
+
121
+ ## Disabling Detection
39
122
 
123
+ Exclude specific usages from detection using comments:
124
+
125
+ ```ts
126
+ // Exclude the next line
127
+ // dittory-disable-next-line
128
+ fetchData(id, { cache: false });
129
+
130
+ // Exclude the same line
131
+ fetchData(id, { cache: false }); // dittory-disable-line
40
132
  ```
41
- 解析対象ディレクトリ: ./src
42
- 最小使用箇所数: 2
43
- 解析対象: all
44
-
45
- 1. exportされたコンポーネントを収集中...
46
- 15個のコンポーネントを検出
47
-
48
- === 常に同じ値が渡されているprops ===
49
-
50
- コンポーネント: Button
51
- 定義: src/components/Button.tsx
52
- prop: variant
53
- 常に渡される値: "primary"
54
- 使用箇所: 5箇所
55
- - src/pages/Home.tsx:23
56
- - src/pages/About.tsx:45
57
- ...
133
+
134
+ Works alongside other directives like `eslint-disable-line` or `@ts-ignore`.
135
+
136
+ ## Use Cases
137
+
138
+ ### Simplify Component APIs
139
+
140
+ ```tsx
141
+ // Before: variant is always "primary" across 20 usages
142
+ <Button variant="primary" onClick={handleClick}>Submit</Button>
143
+
144
+ // After: make "primary" the default
145
+ <Button onClick={handleClick}>Submit</Button>
58
146
  ```
59
147
 
60
- ## 検出対象
148
+ ### Remove Unused Flexibility
149
+
150
+ ```ts
151
+ // Before: cache is always false in all 15 call sites
152
+ const data = await fetchData(id, { cache: false });
61
153
 
62
- - **Reactコンポーネント**: JSX要素として使用されているコンポーネントのprops
63
- - **関数**: exportされた関数の引数
64
- - **クラスメソッド**: exportされたクラスのメソッド引数
154
+ // After: remove the option or change the default
155
+ const data = await fetchData(id);
156
+ ```
65
157
 
66
- ## 要件
158
+ ## Requirements
67
159
 
68
160
  - Node.js >= 18
69
- - プロジェクトに `tsconfig.json` が必要
161
+ - Project must have a `tsconfig.json`
70
162
 
71
- ## ライセンス
163
+ ## License
72
164
 
73
165
  MIT
@@ -170,6 +170,35 @@ function flattenObjectExpression(expression, prefix) {
170
170
  });
171
171
  }
172
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
+
173
202
  //#endregion
174
203
  //#region src/extraction/extractUsages.ts
175
204
  /**
@@ -187,6 +216,7 @@ var ExtractUsages = class {
187
216
  * @returns 引数使用状況の配列
188
217
  */
189
218
  static fromCall(callExpression, callable) {
219
+ if (hasDisableComment(callExpression)) return [];
190
220
  const usages = [];
191
221
  const sourceFile = callExpression.getSourceFile();
192
222
  const args = callExpression.getArguments();
@@ -218,6 +248,7 @@ var ExtractUsages = class {
218
248
  * @returns props使用状況の配列
219
249
  */
220
250
  static fromJsxElement(element, definitions) {
251
+ if (hasDisableComment(element)) return [];
221
252
  const usages = [];
222
253
  const sourceFile = element.getSourceFile();
223
254
  const attributeMap = /* @__PURE__ */ new Map();
@@ -329,7 +360,10 @@ var BaseAnalyzer = class {
329
360
  usages
330
361
  });
331
362
  }
332
- fileMap.set(item.name, paramMap);
363
+ fileMap.set(item.name, {
364
+ line: item.sourceLine,
365
+ params: paramMap
366
+ });
333
367
  }
334
368
  return groupedMap;
335
369
  }
@@ -338,13 +372,14 @@ var BaseAnalyzer = class {
338
372
  */
339
373
  extractConstants(groupedMap) {
340
374
  const result = [];
341
- for (const [sourceFile, targetMap] of groupedMap) for (const [targetName, paramMap] of targetMap) for (const [paramName, usageData] of paramMap) {
375
+ for (const [sourceFile, targetMap] of groupedMap) for (const [targetName, targetInfo] of targetMap) for (const [paramName, usageData] of targetInfo.params) {
342
376
  if (!(usageData.usages.length >= this.minUsages && usageData.values.size === 1)) continue;
343
377
  const value = getSingleValueFromSet(usageData.values);
344
378
  if (value.startsWith(FUNCTION_VALUE_PREFIX)) continue;
345
379
  result.push({
346
380
  targetName,
347
381
  targetSourceFile: sourceFile,
382
+ targetLine: targetInfo.line,
348
383
  paramName,
349
384
  value,
350
385
  usages: usageData.usages
@@ -391,6 +426,7 @@ var ComponentAnalyzer = class extends BaseAnalyzer {
391
426
  const component = {
392
427
  name: exportName,
393
428
  sourceFilePath: sourceFile.getFilePath(),
429
+ sourceLine: declaration.getStartLineNumber(),
394
430
  definitions: props,
395
431
  declaration,
396
432
  usages: {}
@@ -529,4 +565,4 @@ function analyzePropsCore(sourceFiles, options = {}) {
529
565
 
530
566
  //#endregion
531
567
  export { ExtractUsages as a, isTestOrStorybookFile as i, classifyDeclarations as n, BaseAnalyzer as r, analyzePropsCore as t };
532
- //# sourceMappingURL=analyzeProps-YWnY-Mf7.mjs.map
568
+ //# sourceMappingURL=analyzeProps-CoEqudRM.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analyzeProps-CoEqudRM.mjs","names":["propsParam: Node | undefined","current: Node | undefined","usages: Usage[]","groupedMap: GroupedMap","result: Constant[]","exportedComponents: Exported[]","component: Exported","groupedUsages: Record<string, Usage[]>","results: ClassifiedDeclaration[]","type: DeclarationType"],"sources":["../src/components/getProps.ts","../src/extraction/resolveExpressionValue.ts","../src/extraction/flattenObjectExpression.ts","../src/extraction/hasDisableComment.ts","../src/extraction/extractUsages.ts","../src/source/fileFilters.ts","../src/utils/getSingleValueFromSet.ts","../src/analyzer/baseAnalyzer.ts","../src/analyzer/componentAnalyzer.ts","../src/components/isReactComponent.ts","../src/source/classifyDeclarations.ts","../src/analyzeProps.ts"],"sourcesContent":["import { Node, type Type } from \"ts-morph\";\nimport type { Definition } from \"@/types\";\n\n/**\n * コンポーネントのprops定義を取得する\n *\n * 関数の第一パラメータの型情報からpropsを抽出する。\n * 「Props」などの命名規則に依存せず、TypeScriptの型システムから直接取得するため、\n * どのような命名でもpropsを正確に取得できる。\n *\n * 対応パターン:\n * - function Component(props: Props)\n * - const Component = (props: Props) => ...\n * - React.forwardRef((props, ref) => ...)\n * - React.memo((props) => ...)\n */\nexport function getProps(declaration: Node): Definition[] {\n // 関数宣言、アロー関数、関数式から第一パラメータ(props)を取得\n let propsParam: Node | undefined;\n\n if (Node.isFunctionDeclaration(declaration)) {\n const params = declaration.getParameters();\n if (params.length > 0) {\n propsParam = params[0];\n }\n } else if (Node.isVariableDeclaration(declaration)) {\n const initializer = declaration.getInitializer();\n if (initializer) {\n if (\n Node.isArrowFunction(initializer) ||\n Node.isFunctionExpression(initializer)\n ) {\n const params = initializer.getParameters();\n if (params.length > 0) {\n propsParam = params[0];\n }\n } else if (Node.isCallExpression(initializer)) {\n // React.forwardRef, React.memo などのラッパー関数の場合\n const args = initializer.getArguments();\n for (const arg of args) {\n if (Node.isArrowFunction(arg) || Node.isFunctionExpression(arg)) {\n const params = arg.getParameters();\n if (params.length > 0) {\n propsParam = params[0];\n break;\n }\n }\n }\n }\n }\n }\n\n if (!propsParam) {\n return [];\n }\n\n // パラメータの型情報を取得\n const propsType = propsParam.getType();\n return extractPropsFromType(propsType);\n}\n\n/**\n * 型からprops定義を抽出する\n *\n * Mapを使用する理由:\n * - 交差型(A & B)の場合、同じprop名が複数の型に存在する可能性がある\n * - 最初に見つかった定義を優先し、重複を排除するためにMapを使用\n * - 抽出後にindexを付与してDefinition配列として返す\n */\nfunction extractPropsFromType(type: Type): Definition[] {\n const propsMap = new Map<string, Omit<Definition, \"index\">>();\n\n collectPropsFromType(type, propsMap);\n\n return Array.from(propsMap.values()).map((prop, index) => ({\n ...prop,\n index,\n }));\n}\n\n/**\n * 型からpropsを収集する(交差型の場合は再帰的に処理)\n */\nfunction collectPropsFromType(\n type: Type,\n propsMap: Map<string, Omit<Definition, \"index\">>,\n): void {\n if (type.isIntersection()) {\n for (const intersectionType of type.getIntersectionTypes()) {\n collectPropsFromType(intersectionType, propsMap);\n }\n return;\n }\n\n const properties = type.getProperties();\n for (const prop of properties) {\n const propName = prop.getName();\n const declarations = prop.getDeclarations();\n\n let isOptional = false;\n for (const decl of declarations) {\n if (Node.isPropertySignature(decl) && decl.hasQuestionToken()) {\n isOptional = true;\n break;\n }\n }\n\n // 同じ名前のpropが既に存在する場合は上書きしない(最初に見つかった定義を優先)\n if (!propsMap.has(propName)) {\n propsMap.set(propName, {\n name: propName,\n required: !isOptional,\n });\n }\n }\n}\n","import { Node } from \"ts-morph\";\n\n/**\n * 引数が渡されなかった場合を表す特別な値\n * 必須/任意を問わず、引数未指定の使用箇所を統一的に扱うために使用\n */\nexport const UNDEFINED_VALUE = \"undefined\";\n\n/**\n * 関数型の値を表すプレフィックス\n * コールバック関数など、関数が渡された場合は使用箇所ごとにユニークな値として扱う\n * これにより、同じコールバック関数を渡していても「定数」として検出されない\n */\nexport const FUNCTION_VALUE_PREFIX = \"[function]\";\n\n/**\n * 式の実際の値を解決する\n *\n * 異なるファイルで同じenum値やリテラル値を使用している場合でも、\n * 同一の値として認識できるよう、値を正規化して文字列表現で返す。\n * 同名だが異なる定義(別ファイルの同名enum等)を区別するため、\n * 必要に応じてファイルパスを含めた識別子を返す。\n */\nexport function resolveExpressionValue(expression: Node): string {\n // 関数型の場合は定数として扱わない。\n // 同じ関数参照でも使用箇所ごとに異なる値として扱うことで、\n // コールバック関数が「常に同じ値」として誤検出されるのを防ぐ\n const type = expression.getType();\n if (type.getCallSignatures().length > 0) {\n const sourceFile = expression.getSourceFile();\n const line = expression.getStartLineNumber();\n return `${FUNCTION_VALUE_PREFIX}${sourceFile.getFilePath()}:${line}`;\n }\n\n // PropertyAccessExpression (例: Status.Active) の場合\n // enum参照を正確に識別するために、ファイルパスを含めた完全修飾名を返す\n if (Node.isPropertyAccessExpression(expression)) {\n const symbol = expression.getSymbol();\n const decl = symbol?.getDeclarations()[0];\n\n // enum memberの場合は、同名enumの誤認識を防ぐためファイルパスを含める\n if (decl && Node.isEnumMember(decl)) {\n const enumDecl = decl.getParent();\n if (Node.isEnumDeclaration(enumDecl)) {\n const filePath = enumDecl.getSourceFile().getFilePath();\n const enumName = enumDecl.getName();\n const memberName = decl.getName();\n const value = decl.getValue();\n // 例: \"/path/to/file.ts:Status.Active=0\"\n return `${filePath}:${enumName}.${memberName}=${JSON.stringify(value)}`;\n }\n }\n }\n\n // Identifier (変数参照) の場合\n // 変数の定義元を辿って実際の値を解決する\n if (Node.isIdentifier(expression)) {\n if (expression.getText() === \"undefined\") {\n return UNDEFINED_VALUE;\n }\n\n const symbol = expression.getSymbol();\n const decl = symbol?.getDeclarations()[0];\n\n if (decl && Node.isVariableDeclaration(decl)) {\n const initializer = decl.getInitializer();\n // 初期化子がある場合はその値を使用(空白は正規化して比較しやすくする)\n // 初期化子がない場合は、ファイルパス + 変数名で一意に識別\n return initializer\n ? initializer.getText().replace(/\\s+/g, \" \")\n : `${decl.getSourceFile().getFilePath()}:${expression.getText()}`;\n }\n\n // パラメータや分割代入など、VariableDeclaration以外の宣言の場合\n // ファイルパス + 変数名で識別する\n if (decl) {\n return `${decl.getSourceFile().getFilePath()}:${expression.getText()}`;\n }\n }\n\n // リテラル型の場合は型情報から値を取得\n // JSON.stringifyで文字列と数値を区別できる形式にする(\"foo\" vs 42)\n if (type.isStringLiteral() || type.isNumberLiteral()) {\n return JSON.stringify(type.getLiteralValue());\n }\n\n // booleanリテラルの場合はgetLiteralValue()がundefinedを返すためgetText()を使用\n if (type.isBooleanLiteral()) {\n return type.getText();\n }\n\n // 上記のいずれにも該当しない場合(オブジェクト、配列、テンプレートリテラル等)\n // ソースコードのテキストをそのまま返す\n return expression.getText();\n}\n","import { Node } from \"ts-morph\";\nimport { resolveExpressionValue } from \"@/extraction/resolveExpressionValue\";\n\nexport type FlattenedValue = { key: string; value: string };\n\n/**\n * オブジェクトリテラルを再帰的に解析し、フラットなkey-valueペアを返す\n *\n * @param expression - 解析対象の式ノード\n * @param prefix - キー名のプレフィックス(ネストしたプロパティの親パスを表す)\n * @returns フラット化されたkey-valueペアの配列\n *\n * @example\n * // { a: { b: 1, c: 2 } } → [{ key: \"prefix.a.b\", value: \"1\" }, { key: \"prefix.a.c\", value: \"2\" }]\n */\nexport function flattenObjectExpression(\n expression: Node,\n prefix: string,\n): FlattenedValue[] {\n if (!Node.isObjectLiteralExpression(expression)) {\n // オブジェクトリテラル以外の場合は単一の値として返す\n return [{ key: prefix, value: resolveExpressionValue(expression) }];\n }\n\n return expression.getProperties().flatMap((property) => {\n if (Node.isPropertyAssignment(property)) {\n const propertyName = property.getName();\n const nestedPrefix = prefix ? `${prefix}.${propertyName}` : propertyName;\n const initializer = property.getInitializer();\n\n return initializer\n ? flattenObjectExpression(initializer, nestedPrefix)\n : [];\n }\n\n if (Node.isShorthandPropertyAssignment(property)) {\n // { foo } のような省略形\n const propertyName = property.getName();\n const nestedPrefix = prefix ? `${prefix}.${propertyName}` : propertyName;\n return [\n {\n key: nestedPrefix,\n value: resolveExpressionValue(property.getNameNode()),\n },\n ];\n }\n\n return [];\n });\n}\n","import type { Node } from \"ts-morph\";\n\nconst DISABLE_NEXT_LINE = \"dittory-disable-next-line\";\nconst DISABLE_LINE = \"dittory-disable-line\";\n\n/**\n * ノードに除外コメントがあるかを判定する\n *\n * 以下の2パターンをサポート:\n * - \"dittory-disable-next-line\": 次の行を除外(leading comments をチェック)\n * - \"dittory-disable-line\": 同じ行を除外(trailing comments をチェック)\n *\n * 祖先ノードを辿り、いずれかのノードのコメントに\n * 除外キーワードが含まれていれば除外対象とする。\n *\n * @param node - 判定対象のノード\n * @returns 除外コメントが存在すれば true\n */\nexport function hasDisableComment(node: Node): boolean {\n let current: Node | undefined = node;\n\n while (current) {\n const leadingComments = current.getLeadingCommentRanges();\n const trailingComments = current.getTrailingCommentRanges();\n\n for (const comment of leadingComments) {\n if (comment.getText().includes(DISABLE_NEXT_LINE)) {\n return true;\n }\n }\n\n for (const comment of trailingComments) {\n if (comment.getText().includes(DISABLE_LINE)) {\n return true;\n }\n }\n\n current = current.getParent();\n }\n\n return false;\n}\n","import {\n type CallExpression,\n type JsxAttribute,\n type JsxOpeningElement,\n type JsxSelfClosingElement,\n Node,\n} from \"ts-morph\";\nimport type { Definition, Exported, Usage } from \"@/types\";\nimport { flattenObjectExpression } from \"./flattenObjectExpression\";\nimport { hasDisableComment } from \"./hasDisableComment\";\nimport { UNDEFINED_VALUE } from \"./resolveExpressionValue\";\n\n/**\n * 使用状況を抽出するユーティリティクラス\n */\nexport class ExtractUsages {\n /**\n * 関数呼び出しから引数の使用状況を抽出する\n *\n * オブジェクトリテラルの場合は再帰的にフラット化し、\n * 各プロパティを「引数名.プロパティ名」形式で記録する。\n *\n * @param callExpression - 関数呼び出しノード\n * @param callable - 対象の関数情報\n * @returns 引数使用状況の配列\n */\n static fromCall(callExpression: CallExpression, callable: Exported): Usage[] {\n // dittory-disable-next-line コメントがある場合は除外\n if (hasDisableComment(callExpression)) {\n return [];\n }\n\n const usages: Usage[] = [];\n const sourceFile = callExpression.getSourceFile();\n const args = callExpression.getArguments();\n\n for (const param of callable.definitions) {\n const arg = args[param.index];\n\n if (!arg) {\n // 引数が渡されていない場合はundefinedとして記録\n usages.push({\n name: param.name,\n value: UNDEFINED_VALUE,\n usageFilePath: sourceFile.getFilePath(),\n usageLine: callExpression.getStartLineNumber(),\n });\n continue;\n }\n\n // オブジェクトリテラルの場合は再帰的にフラット化\n for (const { key, value } of flattenObjectExpression(arg, param.name)) {\n usages.push({\n name: key,\n value,\n usageFilePath: sourceFile.getFilePath(),\n usageLine: arg.getStartLineNumber(),\n });\n }\n }\n\n return usages;\n }\n\n /**\n * JSX要素からprops使用状況を抽出する\n *\n * @param element - JSX要素ノード\n * @param definitions - props定義の配列\n * @returns props使用状況の配列\n */\n static fromJsxElement(\n element: JsxOpeningElement | JsxSelfClosingElement,\n definitions: Definition[],\n ): Usage[] {\n // dittory-disable-next-line コメントがある場合は除外\n if (hasDisableComment(element)) {\n return [];\n }\n\n const usages: Usage[] = [];\n const sourceFile = element.getSourceFile();\n\n // JSX属性をMapに変換\n const attributeMap = new Map<string, JsxAttribute>();\n for (const attr of element.getAttributes()) {\n if (Node.isJsxAttribute(attr)) {\n attributeMap.set(attr.getNameNode().getText(), attr);\n }\n }\n\n // definitionsをループして処理\n for (const prop of definitions) {\n const attr = attributeMap.get(prop.name);\n\n if (!attr) {\n // 渡されていない場合(required/optional問わず記録)\n usages.push({\n name: prop.name,\n value: UNDEFINED_VALUE,\n usageFilePath: sourceFile.getFilePath(),\n usageLine: element.getStartLineNumber(),\n });\n continue;\n }\n\n // 属性が渡されている場合、値を抽出\n const initializer = attr.getInitializer();\n\n if (!initializer) {\n // boolean shorthand (例: <Component disabled />)\n usages.push({\n name: prop.name,\n value: \"true\",\n usageFilePath: sourceFile.getFilePath(),\n usageLine: attr.getStartLineNumber(),\n });\n } else if (Node.isJsxExpression(initializer)) {\n // {expression} 形式\n const expression = initializer.getExpression();\n if (!expression) {\n continue;\n }\n for (const { key, value } of flattenObjectExpression(\n expression,\n prop.name,\n )) {\n usages.push({\n name: key,\n value,\n usageFilePath: sourceFile.getFilePath(),\n usageLine: attr.getStartLineNumber(),\n });\n }\n } else {\n // \"string\" 形式\n usages.push({\n name: prop.name,\n value: initializer.getText(),\n usageFilePath: sourceFile.getFilePath(),\n usageLine: attr.getStartLineNumber(),\n });\n }\n }\n\n return usages;\n }\n}\n","/**\n * ファイルパスがテストファイルまたはStorybookファイルかどうかを判定する\n * - 拡張子が .test.* / .spec.* / .stories.* のファイル\n * - __tests__ / __stories__ フォルダ内のファイル\n */\nexport function isTestOrStorybookFile(filePath: string): boolean {\n // 拡張子ベースの判定\n if (/\\.(test|spec|stories)\\.(ts|tsx|js|jsx)$/.test(filePath)) {\n return true;\n }\n\n // フォルダ名ベースの判定\n if (/\\b__tests__\\b|\\b__stories__\\b/.test(filePath)) {\n return true;\n }\n\n return false;\n}\n","/**\n * Setから唯一の値を安全に取得する\n */\nexport function getSingleValueFromSet(values: Set<string>): string {\n if (values.size !== 1) {\n throw new Error(`Expected exactly 1 value, got ${values.size}`);\n }\n const [firstValue] = Array.from(values);\n return firstValue;\n}\n","import { FUNCTION_VALUE_PREFIX } from \"@/extraction/resolveExpressionValue\";\nimport { isTestOrStorybookFile } from \"@/source/fileFilters\";\nimport type {\n AnalysisResult,\n AnalyzerOptions,\n ClassifiedDeclaration,\n Constant,\n Exported,\n FileFilter,\n Usage,\n} from \"@/types\";\nimport { getSingleValueFromSet } from \"@/utils/getSingleValueFromSet\";\n\n/**\n * 使用データのグループ\n * values.size === 1 の場合、そのパラメータは「定数」として検出される\n */\ninterface UsageData {\n values: Set<string>;\n usages: Usage[];\n}\n\n/**\n * 対象ごとの情報(行番号とパラメータ使用状況)\n */\ninterface TargetInfo {\n line: number;\n params: Map<string, UsageData>;\n}\n\n/**\n * 使用状況を階層的にグループ化したマップ\n *\n * 3階層の構造で使用状況を整理する:\n * 1. ソースファイルパス: どのファイルで定義された対象か\n * 2. 対象名: 関数名/コンポーネント名(+ 行番号とパラメータ使用状況)\n *\n * この構造により、定数検出時に効率的に走査できる。\n */\ntype GroupedMap = Map<string, Map<string, TargetInfo>>;\n\n/**\n * 分析処理の基底クラス\n */\nexport abstract class BaseAnalyzer {\n protected shouldExcludeFile: FileFilter;\n protected minUsages: number;\n\n constructor(options: AnalyzerOptions = {}) {\n this.shouldExcludeFile = options.shouldExcludeFile ?? isTestOrStorybookFile;\n this.minUsages = options.minUsages ?? 2;\n }\n\n /**\n * メイン分析処理\n *\n * @param declarations - 事前分類済みの宣言配列\n */\n analyze(declarations: ClassifiedDeclaration[]): AnalysisResult {\n // 1. エクスポートされた対象を収集\n const exported = this.collect(declarations);\n\n // 2. 使用状況をグループ化\n const groupedMap = this.createGroupedMap(exported);\n\n // 3. 常に同じ値が渡されているパラメータを抽出\n const constants = this.extractConstants(groupedMap);\n\n // 4. 結果を構築\n return { constants, exported };\n }\n\n /**\n * エクスポートされた対象を収集する(サブクラスで実装)\n *\n * @param declarations - 事前分類済みの宣言配列\n */\n protected abstract collect(declarations: ClassifiedDeclaration[]): Exported[];\n\n /**\n * 使用状況をグループ化したマップを作成\n */\n private createGroupedMap(exported: Exported[]): GroupedMap {\n const groupedMap: GroupedMap = new Map();\n\n for (const item of exported) {\n let fileMap = groupedMap.get(item.sourceFilePath);\n if (!fileMap) {\n fileMap = new Map();\n groupedMap.set(item.sourceFilePath, fileMap);\n }\n\n const paramMap = new Map<string, UsageData>();\n for (const [paramName, usages] of Object.entries(item.usages)) {\n const values = new Set<string>();\n for (const usage of usages) {\n values.add(usage.value);\n }\n paramMap.set(paramName, { values, usages });\n }\n fileMap.set(item.name, { line: item.sourceLine, params: paramMap });\n }\n\n return groupedMap;\n }\n\n /**\n * 常に同じ値が渡されているパラメータを抽出\n */\n private extractConstants(groupedMap: GroupedMap): Constant[] {\n const result: Constant[] = [];\n\n for (const [sourceFile, targetMap] of groupedMap) {\n for (const [targetName, targetInfo] of targetMap) {\n for (const [paramName, usageData] of targetInfo.params) {\n const isConstant =\n usageData.usages.length >= this.minUsages &&\n usageData.values.size === 1;\n\n if (!isConstant) {\n continue;\n }\n\n const value = getSingleValueFromSet(usageData.values);\n\n // 関数型の値は定数として報告しない\n // (onClickに同じハンドラを渡している等は、デフォルト値化の候補ではない)\n if (value.startsWith(FUNCTION_VALUE_PREFIX)) {\n continue;\n }\n\n result.push({\n targetName,\n targetSourceFile: sourceFile,\n targetLine: targetInfo.line,\n paramName,\n value,\n usages: usageData.usages,\n });\n }\n }\n }\n\n return result;\n }\n}\n","import { Node, SyntaxKind } from \"ts-morph\";\nimport { getProps } from \"@/components/getProps\";\nimport { ExtractUsages } from \"@/extraction/extractUsages\";\nimport type {\n AnalyzerOptions,\n ClassifiedDeclaration,\n Exported,\n Usage,\n} from \"@/types\";\nimport { BaseAnalyzer } from \"./baseAnalyzer\";\n\n/**\n * Reactコンポーネントのprops分析を行うAnalyzer\n *\n * exportされたReactコンポーネントを収集し、各コンポーネントのprops使用状況を分析する。\n * 常に同じ値が渡されているpropsを検出し、定数として報告する。\n *\n * @example\n * ```ts\n * const analyzer = new ComponentAnalyzer({ minUsages: 2 });\n * const result = analyzer.analyze(sourceFiles);\n * console.log(result.constants);\n * ```\n */\nexport class ComponentAnalyzer extends BaseAnalyzer {\n constructor(options: AnalyzerOptions = {}) {\n super(options);\n }\n\n /**\n * 事前分類済みの宣言からReactコンポーネントを収集する\n *\n * @param declarations - 事前分類済みの宣言配列\n * @returns exportされたコンポーネントとその使用状況の配列\n */\n protected collect(declarations: ClassifiedDeclaration[]): Exported[] {\n const exportedComponents: Exported[] = [];\n\n for (const classified of declarations) {\n const { exportName, sourceFile, declaration } = classified;\n\n // FunctionDeclaration または VariableDeclaration のみを処理\n if (\n !Node.isFunctionDeclaration(declaration) &&\n !Node.isVariableDeclaration(declaration)\n ) {\n continue;\n }\n\n // コンポーネントの定義から名前ノードを取得\n const nameNode = declaration.getNameNode();\n if (!nameNode || !Node.isIdentifier(nameNode)) {\n continue;\n }\n\n // 名前ノードから全参照を検索し、除外対象ファイルからの参照をフィルタ\n const references = nameNode\n .findReferences()\n .flatMap((referencedSymbol) => referencedSymbol.getReferences())\n .filter(\n (ref) => !this.shouldExcludeFile(ref.getSourceFile().getFilePath()),\n );\n\n // コンポーネントの宣言からprops定義を取得\n const props = getProps(declaration);\n\n const component: Exported = {\n name: exportName,\n sourceFilePath: sourceFile.getFilePath(),\n sourceLine: declaration.getStartLineNumber(),\n definitions: props,\n declaration,\n usages: {},\n };\n\n // 参照からJSX要素を抽出し、usagesをprop名ごとにグループ化\n const groupedUsages: Record<string, Usage[]> = {};\n for (const reference of references) {\n const refNode = reference.getNode();\n const parent = refNode.getParent();\n if (!parent) {\n continue;\n }\n\n // <Component> または <Component /> の形でJSX要素として使われているかチェック\n const jsxElement =\n parent.asKind(SyntaxKind.JsxOpeningElement) ??\n parent.asKind(SyntaxKind.JsxSelfClosingElement);\n\n if (!jsxElement) {\n continue;\n }\n\n // タグ名ノードが参照ノードと一致するか確認\n const tagNameNode = jsxElement.getTagNameNode();\n if (tagNameNode !== refNode) {\n continue;\n }\n\n // JSX要素からprops使用状況を抽出\n const usages = ExtractUsages.fromJsxElement(\n jsxElement,\n component.definitions,\n );\n for (const usage of usages) {\n if (!groupedUsages[usage.name]) {\n groupedUsages[usage.name] = [];\n }\n groupedUsages[usage.name].push(usage);\n }\n }\n\n component.usages = groupedUsages;\n exportedComponents.push(component);\n }\n\n return exportedComponents;\n }\n}\n","import { Node } from \"ts-morph\";\n\n/**\n * 宣言がReactコンポーネントかどうかを判定する\n * - 関数がJSXを返しているかチェック\n * - React.FC型注釈を持っているかチェック\n */\nexport function isReactComponent(declaration: Node): boolean {\n // 関数宣言の場合\n if (Node.isFunctionDeclaration(declaration)) {\n return containsJsx(declaration);\n }\n\n // 変数宣言の場合 (const Button = ...)\n if (Node.isVariableDeclaration(declaration)) {\n const initializer = declaration.getInitializer();\n if (!initializer) return false;\n\n // アロー関数または関数式の場合\n if (\n Node.isArrowFunction(initializer) ||\n Node.isFunctionExpression(initializer)\n ) {\n return containsJsx(initializer);\n }\n\n // React.forwardRef, React.memo などのラッパー関数の場合\n if (Node.isCallExpression(initializer)) {\n const args = initializer.getArguments();\n for (const arg of args) {\n if (Node.isArrowFunction(arg) || Node.isFunctionExpression(arg)) {\n if (containsJsx(arg)) return true;\n }\n }\n }\n }\n\n return false;\n}\n\n/**\n * ノード内にJSX要素が含まれているかチェック\n * 効率化: 1回のトラバースで全てのJSX要素をチェック\n */\nfunction containsJsx(node: Node): boolean {\n let hasJsx = false;\n\n node.forEachDescendant((descendant) => {\n if (\n Node.isJsxElement(descendant) ||\n Node.isJsxSelfClosingElement(descendant) ||\n Node.isJsxFragment(descendant)\n ) {\n hasJsx = true;\n return true; // 早期終了\n }\n return undefined;\n });\n\n return hasJsx;\n}\n","import { Node, type SourceFile } from \"ts-morph\";\nimport { isReactComponent } from \"@/components/isReactComponent\";\nimport type { ClassifiedDeclaration, DeclarationType } from \"@/types\";\n\n/**\n * ソースファイルからexportされた関数/コンポーネント/クラス宣言を収集し、\n * 種別(react/function/class)を事前に分類する\n *\n * @param sourceFiles - 分析対象のソースファイル配列\n * @returns 分類済みの宣言配列\n */\nexport function classifyDeclarations(\n sourceFiles: SourceFile[],\n): ClassifiedDeclaration[] {\n const results: ClassifiedDeclaration[] = [];\n\n for (const sourceFile of sourceFiles) {\n const exportedDecls = sourceFile.getExportedDeclarations();\n\n for (const [exportName, declarations] of exportedDecls) {\n // 関数宣言または変数宣言(アロー関数/関数式)を見つける\n const funcDecl = declarations.find((decl) => isFunctionLike(decl));\n\n if (funcDecl) {\n if (\n Node.isFunctionDeclaration(funcDecl) ||\n Node.isVariableDeclaration(funcDecl)\n ) {\n const type: DeclarationType = isReactComponent(funcDecl)\n ? \"react\"\n : \"function\";\n results.push({\n exportName,\n sourceFile,\n declaration: funcDecl,\n type,\n });\n }\n continue;\n }\n\n // クラス宣言を見つける\n const classDecl = declarations.find((decl) =>\n Node.isClassDeclaration(decl),\n );\n\n if (classDecl && Node.isClassDeclaration(classDecl)) {\n results.push({\n exportName,\n sourceFile,\n declaration: classDecl,\n type: \"class\",\n });\n }\n }\n }\n\n return results;\n}\n\n/**\n * 宣言が関数的なもの(関数宣言、アロー関数、関数式)かどうかを判定\n */\nfunction isFunctionLike(declaration: Node): boolean {\n if (Node.isFunctionDeclaration(declaration)) {\n return true;\n }\n\n if (Node.isVariableDeclaration(declaration)) {\n const initializer = declaration.getInitializer();\n if (!initializer) {\n return false;\n }\n\n return (\n Node.isArrowFunction(initializer) ||\n Node.isFunctionExpression(initializer) ||\n Node.isCallExpression(initializer) // React.forwardRef, React.memo など\n );\n }\n\n return false;\n}\n","import type { SourceFile } from \"ts-morph\";\nimport { ComponentAnalyzer } from \"@/analyzer/componentAnalyzer\";\nimport { classifyDeclarations } from \"@/source/classifyDeclarations\";\nimport { isTestOrStorybookFile } from \"@/source/fileFilters\";\nimport type { AnalysisResult, FileFilter } from \"@/types\";\n\ninterface AnalyzePropsOptions {\n shouldExcludeFile?: FileFilter;\n minUsages?: number;\n}\n\n/**\n * コンポーネントのprops使用状況を解析し、常に同じ値が渡されているpropsを検出する\n *\n * @param sourceFiles - 解析対象のソースファイル配列\n * @param options - オプション設定\n * @returns 解析結果(定数props、統計情報、exportされたコンポーネント)\n *\n * @example\n * const project = new Project();\n * project.addSourceFilesAtPaths(\"src/**\\/*.tsx\");\n * const result = analyzePropsCore(project.getSourceFiles());\n */\nexport function analyzePropsCore(\n sourceFiles: SourceFile[],\n options: AnalyzePropsOptions = {},\n): AnalysisResult {\n const { shouldExcludeFile = isTestOrStorybookFile, minUsages = 2 } = options;\n\n // 宣言を事前分類し、Reactコンポーネントのみ抽出\n const declarations = classifyDeclarations(sourceFiles);\n const components = declarations.filter((decl) => decl.type === \"react\");\n\n const analyzer = new ComponentAnalyzer({\n shouldExcludeFile,\n minUsages,\n });\n\n return analyzer.analyze(components);\n}\n"],"mappings":"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"}
package/dist/cli.mjs CHANGED
@@ -1,8 +1,9 @@
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";
2
+ import { a as ExtractUsages, i as isTestOrStorybookFile, n as classifyDeclarations, r as BaseAnalyzer, t as analyzePropsCore } from "./analyzeProps-CoEqudRM.mjs";
3
3
  import { Node, Project, SyntaxKind } from "ts-morph";
4
- import fs from "node:fs";
5
4
  import path from "node:path";
5
+ import fs from "node:fs";
6
+ import { pathToFileURL } from "node:url";
6
7
 
7
8
  //#region src/analyzer/classMethodAnalyzer.ts
8
9
  /**
@@ -41,6 +42,7 @@ var ClassMethodAnalyzer = class extends BaseAnalyzer {
41
42
  const callable = {
42
43
  name: `${exportName}.${methodName}`,
43
44
  sourceFilePath: sourceFile.getFilePath(),
45
+ sourceLine: method.getStartLineNumber(),
44
46
  definitions: parameters,
45
47
  declaration: method,
46
48
  usages: {}
@@ -123,6 +125,7 @@ var FunctionAnalyzer = class extends BaseAnalyzer {
123
125
  const callable = {
124
126
  name: exportName,
125
127
  sourceFilePath: sourceFile.getFilePath(),
128
+ sourceLine: declaration.getStartLineNumber(),
126
129
  definitions: parameters,
127
130
  declaration,
128
131
  usages: {}
@@ -202,8 +205,108 @@ function analyzeFunctionsCore(sourceFiles, options = {}) {
202
205
  };
203
206
  }
204
207
 
208
+ //#endregion
209
+ //#region src/cli/loadConfig.ts
210
+ /** コンフィグファイルの検索順序 */
211
+ const CONFIG_FILE_NAMES = [
212
+ "dittory.config.js",
213
+ "dittory.config.mjs",
214
+ "dittory.config.json"
215
+ ];
216
+ /**
217
+ * コンフィグファイルを読み込む
218
+ *
219
+ * 現在の作業ディレクトリから以下の順序でコンフィグファイルを探す:
220
+ * 1. dittory.config.js
221
+ * 2. dittory.config.mjs
222
+ * 3. dittory.config.json
223
+ *
224
+ * ファイルが存在しない場合は空のオブジェクトを返す。
225
+ *
226
+ * @returns コンフィグオブジェクト
227
+ * @throws {Error} コンフィグファイルの読み込みに失敗した場合
228
+ */
229
+ async function loadConfig() {
230
+ const cwd = process.cwd();
231
+ for (const fileName of CONFIG_FILE_NAMES) {
232
+ const configPath = path.join(cwd, fileName);
233
+ if (!fs.existsSync(configPath)) continue;
234
+ if (fileName.endsWith(".json")) return loadJsonConfig(configPath);
235
+ return loadJsConfig(configPath);
236
+ }
237
+ return {};
238
+ }
239
+ /**
240
+ * JSON コンフィグを読み込む
241
+ */
242
+ function loadJsonConfig(configPath) {
243
+ const content = fs.readFileSync(configPath, "utf-8");
244
+ try {
245
+ const config = JSON.parse(content);
246
+ if (typeof config !== "object" || config === null) throw new Error(`Invalid config: expected object, got ${typeof config}`);
247
+ return validateConfig(config);
248
+ } catch (error) {
249
+ if (error instanceof SyntaxError) throw new Error(`Failed to parse ${path.basename(configPath)}: ${error.message}`);
250
+ throw error;
251
+ }
252
+ }
253
+ /**
254
+ * JS コンフィグを読み込む
255
+ */
256
+ async function loadJsConfig(configPath) {
257
+ try {
258
+ const config = (await import(pathToFileURL(configPath).href)).default;
259
+ if (typeof config !== "object" || config === null) throw new Error(`Invalid config: expected object, got ${typeof config}`);
260
+ return validateConfig(config);
261
+ } catch (error) {
262
+ if (error instanceof Error) throw new Error(`Failed to load ${path.basename(configPath)}: ${error.message}`);
263
+ throw error;
264
+ }
265
+ }
266
+ const VALID_TARGETS$1 = [
267
+ "all",
268
+ "components",
269
+ "functions"
270
+ ];
271
+ const VALID_OUTPUTS$1 = ["simple", "verbose"];
272
+ /**
273
+ * コンフィグの値を検証する
274
+ */
275
+ function validateConfig(config) {
276
+ const result = {};
277
+ if ("minUsages" in config) {
278
+ if (typeof config.minUsages !== "number" || config.minUsages < 1) throw new Error(`Invalid config: minUsages must be a number >= 1, got ${config.minUsages}`);
279
+ result.minUsages = config.minUsages;
280
+ }
281
+ if ("target" in config) {
282
+ if (!VALID_TARGETS$1.includes(config.target)) throw new Error(`Invalid config: target must be one of ${VALID_TARGETS$1.join(", ")}, got ${config.target}`);
283
+ result.target = config.target;
284
+ }
285
+ if ("output" in config) {
286
+ if (!VALID_OUTPUTS$1.includes(config.output)) throw new Error(`Invalid config: output must be one of ${VALID_OUTPUTS$1.join(", ")}, got ${config.output}`);
287
+ result.output = config.output;
288
+ }
289
+ if ("tsconfig" in config) {
290
+ if (typeof config.tsconfig !== "string" || config.tsconfig === "") throw new Error(`Invalid config: tsconfig must be a non-empty string, got ${config.tsconfig}`);
291
+ result.tsconfig = config.tsconfig;
292
+ }
293
+ if ("targetDir" in config) {
294
+ if (typeof config.targetDir !== "string" || config.targetDir === "") throw new Error(`Invalid config: targetDir must be a non-empty string, got ${config.targetDir}`);
295
+ result.targetDir = config.targetDir;
296
+ }
297
+ return result;
298
+ }
299
+
205
300
  //#endregion
206
301
  //#region src/cli/parseCliOptions.ts
302
+ /** デフォルトのオプション値 */
303
+ const DEFAULT_OPTIONS = {
304
+ targetDir: "./src",
305
+ minUsages: 2,
306
+ target: "all",
307
+ output: "simple",
308
+ tsconfig: "./tsconfig.json"
309
+ };
207
310
  var CliValidationError = class extends Error {
208
311
  constructor(message) {
209
312
  super(message);
@@ -215,48 +318,53 @@ const VALID_TARGETS = [
215
318
  "components",
216
319
  "functions"
217
320
  ];
321
+ const VALID_OUTPUTS = ["simple", "verbose"];
218
322
  /** 不明なオプションの検出に使用 */
219
323
  const VALID_OPTIONS = [
220
324
  "--min",
221
325
  "--target",
326
+ "--output",
327
+ "--tsconfig",
222
328
  "--help"
223
329
  ];
224
330
  /**
225
331
  * CLIオプションをパースする
226
332
  *
333
+ * 明示的に指定されたオプションのみを返す(デフォルト値は含まない)
334
+ *
227
335
  * @throws {CliValidationError} オプションが無効な場合
228
336
  */
229
337
  function parseCliOptions(args) {
230
- let targetDir = path.join(process.cwd(), "src");
231
- let minUsages = 2;
232
- let target = "all";
233
- let showHelp = false;
338
+ const result = { showHelp: false };
234
339
  for (const arg of args) {
235
340
  if (arg === "--help") {
236
- showHelp = true;
341
+ result.showHelp = true;
237
342
  continue;
238
343
  }
239
344
  if (arg.startsWith("--min=")) {
240
345
  const valueStr = arg.slice(6);
241
346
  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;
347
+ if (valueStr === "" || Number.isNaN(value)) throw new CliValidationError(`Invalid value for --min: "${valueStr}" (must be a number)`);
348
+ if (value < 1) throw new CliValidationError(`--min must be at least 1: ${value}`);
349
+ result.minUsages = value;
245
350
  } else if (arg.startsWith("--target=")) {
246
351
  const value = arg.slice(9);
247
- if (!VALID_TARGETS.includes(value)) throw new CliValidationError(`--target の値が無効です: "${value}" (有効な値: ${VALID_TARGETS.join(", ")})`);
248
- target = value;
352
+ if (!VALID_TARGETS.includes(value)) throw new CliValidationError(`Invalid value for --target: "${value}" (valid values: ${VALID_TARGETS.join(", ")})`);
353
+ result.target = value;
354
+ } else if (arg.startsWith("--output=")) {
355
+ const value = arg.slice(9);
356
+ if (!VALID_OUTPUTS.includes(value)) throw new CliValidationError(`Invalid value for --output: "${value}" (valid values: ${VALID_OUTPUTS.join(", ")})`);
357
+ result.output = value;
358
+ } else if (arg.startsWith("--tsconfig=")) {
359
+ const value = arg.slice(11);
360
+ if (value === "") throw new CliValidationError("Invalid value for --tsconfig: path cannot be empty");
361
+ result.tsconfig = value;
249
362
  } else if (arg.startsWith("--")) {
250
363
  const optionName = arg.split("=")[0];
251
- if (!VALID_OPTIONS.includes(optionName)) throw new CliValidationError(`不明なオプション: ${optionName}`);
252
- } else targetDir = arg;
364
+ if (!VALID_OPTIONS.includes(optionName)) throw new CliValidationError(`Unknown option: ${optionName}`);
365
+ } else result.targetDir = arg;
253
366
  }
254
- return {
255
- targetDir,
256
- minUsages,
257
- target,
258
- showHelp
259
- };
367
+ return result;
260
368
  }
261
369
  /**
262
370
  * 対象ディレクトリの存在を検証する
@@ -264,8 +372,16 @@ function parseCliOptions(args) {
264
372
  * @throws {CliValidationError} ディレクトリが存在しない、またはディレクトリでない場合
265
373
  */
266
374
  function validateTargetDir(targetDir) {
267
- if (!fs.existsSync(targetDir)) throw new CliValidationError(`ディレクトリが存在しません: ${targetDir}`);
268
- if (!fs.statSync(targetDir).isDirectory()) throw new CliValidationError(`指定されたパスはディレクトリではありません: ${targetDir}`);
375
+ if (!fs.existsSync(targetDir)) throw new CliValidationError(`Directory does not exist: ${targetDir}`);
376
+ if (!fs.statSync(targetDir).isDirectory()) throw new CliValidationError(`Path is not a directory: ${targetDir}`);
377
+ }
378
+ /**
379
+ * tsconfig.json の存在を検証する
380
+ *
381
+ * @throws {CliValidationError} ファイルが存在しない場合
382
+ */
383
+ function validateTsConfig(tsConfigPath) {
384
+ if (!fs.existsSync(tsConfigPath)) throw new CliValidationError(`tsconfig not found: ${tsConfigPath}`);
269
385
  }
270
386
  /**
271
387
  * ヘルプメッセージを取得する
@@ -275,67 +391,70 @@ function getHelpMessage() {
275
391
  Usage: dittory [options] [directory]
276
392
 
277
393
  Options:
278
- --min=<number> 最小使用箇所数 (デフォルト: 2)
279
- --target=<mode> 解析対象: all, components, functions (デフォルト: all)
280
- --help このヘルプを表示
394
+ --min=<number> Minimum usage count (default: 2)
395
+ --target=<mode> Analysis target: all, components, functions (default: all)
396
+ --output=<mode> Output mode: simple, verbose (default: simple)
397
+ --tsconfig=<path> Path to tsconfig.json (default: ./tsconfig.json)
398
+ --help Show this help message
281
399
 
282
400
  Arguments:
283
- directory 解析対象ディレクトリ (デフォルト: ./src)
401
+ directory Target directory to analyze (default: ./src)
284
402
  `;
285
403
  }
286
404
 
287
405
  //#endregion
288
406
  //#region src/output/printAnalysisResult.ts
407
+ function bold(text) {
408
+ return `\x1b[1m${text}\x1b[0m`;
409
+ }
410
+ function green(text) {
411
+ return `\x1b[32m${text}\x1b[0m`;
412
+ }
289
413
  /**
290
- * exportされたコンポーネントの一覧を出力
414
+ * 値を表示用にフォーマットする
415
+ *
416
+ * 内部的にはenum区別のためにファイルパスを含むが、表示時は不要なので除去する
417
+ * 例: "/path/to/file.ts:ButtonVariant.Primary=\"primary\"" → "ButtonVariant.Primary"
291
418
  */
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"));
419
+ function formatValueForDisplay(value) {
420
+ const enumMatch = value.match(/^.+:(\w+\.\w+)=.+$/);
421
+ if (enumMatch) return enumMatch[1];
422
+ return value;
300
423
  }
301
424
  /**
302
- * 常に同じ値が渡されているpropsを出力
425
+ * Constant[]を関数/コンポーネント単位でグループ化する
303
426
  */
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}`);
427
+ function groupConstantsByTarget(constants) {
428
+ const groupMap = /* @__PURE__ */ new Map();
429
+ for (const constant of constants) {
430
+ const key = `${constant.targetSourceFile}:${constant.targetName}`;
431
+ let group = groupMap.get(key);
432
+ if (!group) {
433
+ group = {
434
+ targetName: constant.targetName,
435
+ targetSourceFile: constant.targetSourceFile,
436
+ targetLine: constant.targetLine,
437
+ params: []
438
+ };
439
+ groupMap.set(key, group);
320
440
  }
321
- console.log("");
441
+ group.params.push({
442
+ paramName: constant.paramName,
443
+ value: constant.value,
444
+ usageCount: constant.usages.length,
445
+ usages: constant.usages
446
+ });
322
447
  }
323
- }
324
- /**
325
- * 解析結果を全て出力
326
- */
327
- function printAnalysisResult(result) {
328
- printExportedComponents(result.exported);
329
- printConstantProps(result.constants);
448
+ return Array.from(groupMap.values());
330
449
  }
331
450
  /**
332
451
  * exportされた関数の一覧を出力
333
452
  */
334
453
  function printExportedFunctions(exported) {
335
454
  const lines = [
336
- "2. exportされた関数を収集中...",
337
- ` → ${exported.length}個の関数を検出`,
338
- ...exported.map((fn) => ` - ${fn.name} (${path.relative(process.cwd(), fn.sourceFilePath)})`),
455
+ "Collecting exported functions...",
456
+ ` → Found ${exported.length} function(s)`,
457
+ ...exported.map((fn) => ` - ${bold(green(fn.name))} (${path.relative(process.cwd(), fn.sourceFilePath)})`),
339
458
  ""
340
459
  ];
341
460
  console.log(lines.join("\n"));
@@ -344,31 +463,40 @@ function printExportedFunctions(exported) {
344
463
  * 常に同じ値が渡されている引数を出力
345
464
  */
346
465
  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}`);
466
+ if (constants.length === 0) return;
467
+ const grouped = groupConstantsByTarget(constants);
468
+ for (const group of grouped) {
469
+ const relativePath = path.relative(process.cwd(), group.targetSourceFile);
470
+ const usageCount = group.params[0]?.usageCount ?? 0;
471
+ const usages = group.params[0]?.usages ?? [];
472
+ console.log(`${bold(green(group.targetName))} ${relativePath}:${group.targetLine}`);
473
+ console.log("Constant Arguments:");
474
+ for (const param of group.params) console.log(` - ${param.paramName} = ${formatValueForDisplay(param.value)}`);
475
+ console.log(`Usages (${usageCount}):`);
476
+ for (const usage of usages) {
477
+ const usagePath = path.relative(process.cwd(), usage.usageFilePath);
478
+ console.log(` - ${usagePath}:${usage.usageLine}`);
362
479
  }
363
- console.log("");
480
+ console.log("\n");
364
481
  }
365
482
  }
366
483
  /**
367
- * 関数解析結果を全て出力
484
+ * 統計情報を出力
485
+ */
486
+ function printStatistics(result) {
487
+ const totalFunctions = result.exported.length;
488
+ const functionsWithConstants = groupConstantsByTarget(result.constants).length;
489
+ console.log("---");
490
+ console.log(`Found ${functionsWithConstants} function(s) with constant arguments out of ${totalFunctions} function(s).`);
491
+ }
492
+ /**
493
+ * 解析結果を出力
368
494
  */
369
- function printFunctionAnalysisResult(result) {
370
- printExportedFunctions(result.exported);
371
- printConstantArguments(result.constants);
495
+ function printAnalysisResult(result, mode) {
496
+ if (mode === "verbose") printExportedFunctions(result.exported);
497
+ if (result.constants.length === 0) console.log("No arguments with constant values were found.");
498
+ else printConstantArguments(result.constants);
499
+ printStatistics(result);
372
500
  }
373
501
 
374
502
  //#endregion
@@ -376,9 +504,10 @@ function printFunctionAnalysisResult(result) {
376
504
  /**
377
505
  * プロジェクトを初期化し、フィルタリングされたソースファイルを取得する
378
506
  */
379
- function createFilteredSourceFiles(targetDir, shouldExcludeFile = isTestOrStorybookFile) {
507
+ function createFilteredSourceFiles(targetDir, options = {}) {
508
+ const { shouldExcludeFile = isTestOrStorybookFile, tsConfigFilePath = path.join(process.cwd(), "tsconfig.json") } = options;
380
509
  const project = new Project({
381
- tsConfigFilePath: path.join(process.cwd(), "tsconfig.json"),
510
+ tsConfigFilePath,
382
511
  skipAddingFilesFromTsConfig: true
383
512
  });
384
513
  project.addSourceFilesAtPaths(`${targetDir}/**/*.{ts,tsx,js,jsx}`);
@@ -391,36 +520,74 @@ function createFilteredSourceFiles(targetDir, shouldExcludeFile = isTestOrStoryb
391
520
  * エラーメッセージを表示してプロセスを終了する
392
521
  */
393
522
  function exitWithError(message) {
394
- console.error(`エラー: ${message}`);
523
+ console.error(`Error: ${message}`);
395
524
  process.exit(1);
396
525
  }
397
- function main() {
398
- let options;
526
+ async function main() {
527
+ let cliOptions;
399
528
  try {
400
- options = parseCliOptions(process.argv.slice(2));
529
+ cliOptions = parseCliOptions(process.argv.slice(2));
401
530
  } catch (error) {
402
531
  if (error instanceof CliValidationError) exitWithError(error.message);
403
532
  throw error;
404
533
  }
405
- const { targetDir, minUsages, target, showHelp } = options;
406
- if (showHelp) {
534
+ if (cliOptions.showHelp) {
407
535
  console.log(getHelpMessage());
408
536
  process.exit(0);
409
537
  }
538
+ let fileConfig;
539
+ try {
540
+ fileConfig = await loadConfig();
541
+ } catch (error) {
542
+ if (error instanceof Error) exitWithError(error.message);
543
+ throw error;
544
+ }
545
+ const { targetDir, minUsages, target, output, tsconfig } = {
546
+ targetDir: path.resolve(cliOptions.targetDir ?? fileConfig.targetDir ?? DEFAULT_OPTIONS.targetDir),
547
+ minUsages: cliOptions.minUsages ?? fileConfig.minUsages ?? DEFAULT_OPTIONS.minUsages,
548
+ target: cliOptions.target ?? fileConfig.target ?? DEFAULT_OPTIONS.target,
549
+ output: cliOptions.output ?? fileConfig.output ?? DEFAULT_OPTIONS.output,
550
+ tsconfig: cliOptions.tsconfig ?? fileConfig.tsconfig ?? DEFAULT_OPTIONS.tsconfig
551
+ };
410
552
  try {
411
553
  validateTargetDir(targetDir);
412
554
  } catch (error) {
413
555
  if (error instanceof CliValidationError) exitWithError(error.message);
414
556
  throw error;
415
557
  }
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 }));
558
+ try {
559
+ validateTsConfig(tsconfig);
560
+ } catch (error) {
561
+ if (error instanceof CliValidationError) exitWithError(error.message);
562
+ throw error;
563
+ }
564
+ if (output === "verbose") {
565
+ console.log(`Target directory: ${targetDir}`);
566
+ console.log(`Minimum usage count: ${minUsages}`);
567
+ console.log(`Analysis target: ${target}\n`);
568
+ }
569
+ const sourceFilesToAnalyze = createFilteredSourceFiles(targetDir, { tsConfigFilePath: tsconfig });
570
+ const allExported = [];
571
+ const allConstants = [];
572
+ if (target === "all" || target === "components") {
573
+ const propsResult = analyzePropsCore(sourceFilesToAnalyze, { minUsages });
574
+ allExported.push(...propsResult.exported);
575
+ allConstants.push(...propsResult.constants);
576
+ }
577
+ if (target === "all" || target === "functions") {
578
+ const functionsResult = analyzeFunctionsCore(sourceFilesToAnalyze, { minUsages });
579
+ allExported.push(...functionsResult.exported);
580
+ allConstants.push(...functionsResult.constants);
581
+ }
582
+ printAnalysisResult({
583
+ exported: allExported,
584
+ constants: allConstants
585
+ }, output);
422
586
  }
423
- main();
587
+ main().catch((error) => {
588
+ console.error(error);
589
+ process.exit(1);
590
+ });
424
591
 
425
592
  //#endregion
426
593
  export { };
package/dist/cli.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.mjs","names":["results: Exported[]","callable: Exported","groupedUsages: Record<string, Usage[]>","results: Exported[]","callable: Exported","groupedUsages: Record<string, Usage[]>","VALID_TARGETS: readonly AnalyzeMode[]","VALID_OPTIONS: readonly string[]","target: AnalyzeMode","options: CliOptions"],"sources":["../src/analyzer/classMethodAnalyzer.ts","../src/analyzer/functionAnalyzer.ts","../src/analyzeFunctions.ts","../src/cli/parseCliOptions.ts","../src/output/printAnalysisResult.ts","../src/source/createFilteredSourceFiles.ts","../src/cli.ts"],"sourcesContent":["import { Node, type ParameterDeclaration } from \"ts-morph\";\nimport { ExtractUsages } from \"@/extraction/extractUsages\";\nimport type {\n AnalyzerOptions,\n ClassifiedDeclaration,\n Definition,\n Exported,\n Usage,\n} from \"@/types\";\nimport { BaseAnalyzer } from \"./baseAnalyzer\";\n\n/**\n * クラスメソッドの引数分析を行うAnalyzer\n *\n * exportされたクラスのメソッド(static/instance)を収集し、\n * 各メソッドの引数使用状況を分析する。\n * 常に同じ値が渡されている引数を検出し、定数として報告する。\n *\n * @example\n * ```ts\n * const analyzer = new ClassMethodAnalyzer({ minUsages: 2 });\n * const result = analyzer.analyze(declarations);\n * console.log(result.constants);\n * ```\n */\nexport class ClassMethodAnalyzer extends BaseAnalyzer {\n constructor(options: AnalyzerOptions = {}) {\n super(options);\n }\n\n /**\n * 事前分類済みの宣言からクラスメソッドを収集する\n *\n * @param declarations - 事前分類済みの宣言配列(type: \"class\")\n * @returns クラスメソッドとその使用状況の配列(名前は「ClassName.methodName」形式)\n */\n protected collect(declarations: ClassifiedDeclaration[]): Exported[] {\n const results: Exported[] = [];\n\n for (const classified of declarations) {\n const { exportName, sourceFile, declaration } = classified;\n\n if (!Node.isClassDeclaration(declaration)) {\n continue;\n }\n\n const methods = declaration.getMethods();\n\n for (const method of methods) {\n const methodName = method.getName();\n const parameters = this.getParameters(method);\n\n const callable: Exported = {\n name: `${exportName}.${methodName}`,\n sourceFilePath: sourceFile.getFilePath(),\n definitions: parameters,\n declaration: method,\n usages: {},\n };\n\n // メソッド名から参照を検索\n const nameNode = method.getNameNode();\n if (!Node.isIdentifier(nameNode)) {\n continue;\n }\n\n // 名前ノードから全参照を検索し、除外対象ファイルからの参照をフィルタ\n const references = nameNode\n .findReferences()\n .flatMap((referencedSymbol) => referencedSymbol.getReferences())\n .filter(\n (ref) => !this.shouldExcludeFile(ref.getSourceFile().getFilePath()),\n );\n\n // 参照からメソッド呼び出しを抽出し、usagesをパラメータ名ごとにグループ化\n const groupedUsages: Record<string, Usage[]> = {};\n for (const reference of references) {\n const refNode = reference.getNode();\n\n // obj.method の形でPropertyAccessExpressionの一部かチェック\n const propertyAccess = refNode.getParent();\n if (\n !propertyAccess ||\n !Node.isPropertyAccessExpression(propertyAccess)\n ) {\n continue;\n }\n\n // obj.method(...) の形でCallExpressionかチェック\n const callExpression = propertyAccess.getParent();\n if (!callExpression || !Node.isCallExpression(callExpression)) {\n continue;\n }\n\n // 呼び出し対象がPropertyAccessExpressionと一致するか確認\n if (callExpression.getExpression() !== propertyAccess) {\n continue;\n }\n\n // メソッド呼び出しから引数使用状況を抽出\n const usages = ExtractUsages.fromCall(callExpression, callable);\n for (const usage of usages) {\n if (!groupedUsages[usage.name]) {\n groupedUsages[usage.name] = [];\n }\n groupedUsages[usage.name].push(usage);\n }\n }\n\n callable.usages = groupedUsages;\n results.push(callable);\n }\n }\n\n return results;\n }\n\n /**\n * メソッドのパラメータ定義を取得する\n */\n private getParameters(method: Node): Definition[] {\n const params = this.extractParameterDeclarations(method);\n\n return params.map((param, index) => ({\n name: param.getName(),\n index,\n required: !param.hasQuestionToken() && !param.hasInitializer(),\n }));\n }\n\n /**\n * メソッド宣言からParameterDeclarationの配列を抽出する\n */\n private extractParameterDeclarations(method: Node): ParameterDeclaration[] {\n if (Node.isMethodDeclaration(method)) {\n return method.getParameters();\n }\n\n return [];\n }\n}\n","import { Node, type ParameterDeclaration, SyntaxKind } from \"ts-morph\";\nimport { ExtractUsages } from \"@/extraction/extractUsages\";\nimport type {\n AnalyzerOptions,\n ClassifiedDeclaration,\n Definition,\n Exported,\n Usage,\n} from \"@/types\";\nimport { BaseAnalyzer } from \"./baseAnalyzer\";\n\n/**\n * 関数の引数分析を行うAnalyzer\n *\n * exportされた関数を収集し、各関数の引数使用状況を分析する。\n * 常に同じ値が渡されている引数を検出し、定数として報告する。\n *\n * @example\n * ```ts\n * const analyzer = new FunctionAnalyzer({ minUsages: 2 });\n * const result = analyzer.analyze(declarations);\n * console.log(result.constants);\n * ```\n */\nexport class FunctionAnalyzer extends BaseAnalyzer {\n constructor(options: AnalyzerOptions = {}) {\n super(options);\n }\n\n /**\n * 事前分類済みの宣言から関数を収集する\n *\n * @param declarations - 事前分類済みの宣言配列(type: \"function\")\n * @returns exportされた関数とその使用状況の配列\n */\n protected collect(declarations: ClassifiedDeclaration[]): Exported[] {\n const results: Exported[] = [];\n\n for (const classified of declarations) {\n const { exportName, sourceFile, declaration } = classified;\n\n // FunctionDeclaration または VariableDeclaration のみを処理\n if (\n !Node.isFunctionDeclaration(declaration) &&\n !Node.isVariableDeclaration(declaration)\n ) {\n continue;\n }\n\n // 関数の定義から名前ノードを取得\n const nameNode = declaration.getNameNode();\n if (!nameNode || !Node.isIdentifier(nameNode)) {\n continue;\n }\n\n // 名前ノードから全参照を検索し、除外対象ファイルからの参照をフィルタ\n const references = nameNode\n .findReferences()\n .flatMap((referencedSymbol) => referencedSymbol.getReferences())\n .filter(\n (ref) => !this.shouldExcludeFile(ref.getSourceFile().getFilePath()),\n );\n\n // 関数の宣言からパラメータ定義を取得\n const parameters = this.getParameters(declaration);\n\n const callable: Exported = {\n name: exportName,\n sourceFilePath: sourceFile.getFilePath(),\n definitions: parameters,\n declaration,\n usages: {},\n };\n\n // 参照から関数呼び出しを抽出し、usagesをパラメータ名ごとにグループ化\n const groupedUsages: Record<string, Usage[]> = {};\n for (const reference of references) {\n const refNode = reference.getNode();\n const parent = refNode.getParent();\n if (!parent) {\n continue;\n }\n\n // func(...) の形で関数呼び出しとして使われているかチェック\n const callExpression = parent.asKind(SyntaxKind.CallExpression);\n if (!callExpression) {\n continue;\n }\n\n // 呼び出し対象が参照ノードと一致するか確認\n const expression = callExpression.getExpression();\n if (expression !== refNode) {\n continue;\n }\n\n // 関数呼び出しから引数使用状況を抽出\n const usages = ExtractUsages.fromCall(callExpression, callable);\n for (const usage of usages) {\n if (!groupedUsages[usage.name]) {\n groupedUsages[usage.name] = [];\n }\n groupedUsages[usage.name].push(usage);\n }\n }\n\n callable.usages = groupedUsages;\n results.push(callable);\n }\n\n return results;\n }\n\n /**\n * 関数のパラメータ定義を取得する\n */\n private getParameters(declaration: Node): Definition[] {\n const params = this.extractParameterDeclarations(declaration);\n\n return params.map((param, index) => ({\n name: param.getName(),\n index,\n required: !param.hasQuestionToken() && !param.hasInitializer(),\n }));\n }\n\n /**\n * 宣言からParameterDeclarationの配列を抽出する\n */\n private extractParameterDeclarations(\n declaration: Node,\n ): ParameterDeclaration[] {\n if (Node.isFunctionDeclaration(declaration)) {\n return declaration.getParameters();\n }\n\n if (Node.isVariableDeclaration(declaration)) {\n const initializer = declaration.getInitializer();\n if (initializer) {\n if (\n Node.isArrowFunction(initializer) ||\n Node.isFunctionExpression(initializer)\n ) {\n return initializer.getParameters();\n }\n }\n }\n\n return [];\n }\n}\n","import type { SourceFile } from \"ts-morph\";\nimport { ClassMethodAnalyzer } from \"@/analyzer/classMethodAnalyzer\";\nimport { FunctionAnalyzer } from \"@/analyzer/functionAnalyzer\";\nimport { classifyDeclarations } from \"@/source/classifyDeclarations\";\nimport { isTestOrStorybookFile } from \"@/source/fileFilters\";\nimport type { AnalysisResult, FileFilter } from \"@/types\";\n\ninterface AnalyzeFunctionsOptions {\n shouldExcludeFile?: FileFilter;\n minUsages?: number;\n}\n\n/**\n * 関数・クラスメソッドの引数使用状況を解析し、常に同じ値が渡されている引数を検出する\n *\n * @param sourceFiles - 解析対象のソースファイル配列\n * @param options - オプション設定\n * @returns 解析結果(定数引数、統計情報、exportされた関数・メソッド)\n *\n * @example\n * const project = new Project();\n * project.addSourceFilesAtPaths(\"src/**\\/*.ts\");\n * const result = analyzeFunctionsCore(project.getSourceFiles());\n */\nexport function analyzeFunctionsCore(\n sourceFiles: SourceFile[],\n options: AnalyzeFunctionsOptions = {},\n): AnalysisResult {\n const { shouldExcludeFile = isTestOrStorybookFile, minUsages = 2 } = options;\n\n // 宣言を事前分類\n const declarations = classifyDeclarations(sourceFiles);\n const functions = declarations.filter((decl) => decl.type === \"function\");\n const classes = declarations.filter((decl) => decl.type === \"class\");\n\n const analyzerOptions = { shouldExcludeFile, minUsages };\n\n // 関数を分析\n const functionAnalyzer = new FunctionAnalyzer(analyzerOptions);\n const functionResult = functionAnalyzer.analyze(functions);\n\n // クラスメソッドを分析\n const classMethodAnalyzer = new ClassMethodAnalyzer(analyzerOptions);\n const classMethodResult = classMethodAnalyzer.analyze(classes);\n\n // 結果をマージ\n return {\n constants: [...functionResult.constants, ...classMethodResult.constants],\n exported: [...functionResult.exported, ...classMethodResult.exported],\n };\n}\n","import fs from \"node:fs\";\nimport path from \"node:path\";\n\nexport type AnalyzeMode = \"all\" | \"components\" | \"functions\";\n\nexport interface CliOptions {\n targetDir: string;\n minUsages: number;\n target: AnalyzeMode;\n showHelp: boolean;\n}\n\nexport class CliValidationError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"CliValidationError\";\n }\n}\n\nconst VALID_TARGETS: readonly AnalyzeMode[] = [\n \"all\",\n \"components\",\n \"functions\",\n];\n\n/** 不明なオプションの検出に使用 */\nconst VALID_OPTIONS: readonly string[] = [\"--min\", \"--target\", \"--help\"];\n\n/**\n * CLIオプションをパースする\n *\n * @throws {CliValidationError} オプションが無効な場合\n */\nexport function parseCliOptions(args: string[]): CliOptions {\n let targetDir = path.join(process.cwd(), \"src\");\n let minUsages = 2;\n let target: AnalyzeMode = \"all\";\n let showHelp = false;\n\n for (const arg of args) {\n if (arg === \"--help\") {\n showHelp = true;\n continue;\n }\n\n if (arg.startsWith(\"--min=\")) {\n const valueStr = arg.slice(6);\n const value = Number.parseInt(valueStr, 10);\n\n if (valueStr === \"\" || Number.isNaN(value)) {\n throw new CliValidationError(\n `--min の値が無効です: \"${valueStr}\" (数値を指定してください)`,\n );\n }\n if (value < 1) {\n throw new CliValidationError(\n `--min の値は1以上である必要があります: ${value}`,\n );\n }\n\n minUsages = value;\n } else if (arg.startsWith(\"--target=\")) {\n const value = arg.slice(9);\n\n if (!VALID_TARGETS.includes(value as AnalyzeMode)) {\n throw new CliValidationError(\n `--target の値が無効です: \"${value}\" (有効な値: ${VALID_TARGETS.join(\", \")})`,\n );\n }\n\n target = value as AnalyzeMode;\n } else if (arg.startsWith(\"--\")) {\n const optionName = arg.split(\"=\")[0];\n\n if (!VALID_OPTIONS.includes(optionName)) {\n throw new CliValidationError(`不明なオプション: ${optionName}`);\n }\n } else {\n targetDir = arg;\n }\n }\n\n return { targetDir, minUsages, target, showHelp };\n}\n\n/**\n * 対象ディレクトリの存在を検証する\n *\n * @throws {CliValidationError} ディレクトリが存在しない、またはディレクトリでない場合\n */\nexport function validateTargetDir(targetDir: string): void {\n if (!fs.existsSync(targetDir)) {\n throw new CliValidationError(`ディレクトリが存在しません: ${targetDir}`);\n }\n\n const stat = fs.statSync(targetDir);\n if (!stat.isDirectory()) {\n throw new CliValidationError(\n `指定されたパスはディレクトリではありません: ${targetDir}`,\n );\n }\n}\n\n/**\n * ヘルプメッセージを取得する\n */\nexport function getHelpMessage(): string {\n return `\nUsage: dittory [options] [directory]\n\nOptions:\n --min=<number> 最小使用箇所数 (デフォルト: 2)\n --target=<mode> 解析対象: all, components, functions (デフォルト: all)\n --help このヘルプを表示\n\nArguments:\n directory 解析対象ディレクトリ (デフォルト: ./src)\n`;\n}\n","import path from \"node:path\";\nimport type { AnalysisResult, Constant, Exported } from \"@/types\";\n\n/**\n * exportされたコンポーネントの一覧を出力\n */\nexport function printExportedComponents(exported: Exported[]): void {\n const lines = [\n \"1. exportされたコンポーネントを収集中...\",\n ` → ${exported.length}個のコンポーネントを検出`,\n ...exported.map(\n (comp) =>\n ` - ${comp.name} (${path.relative(process.cwd(), comp.sourceFilePath)})`,\n ),\n \"\",\n ];\n console.log(lines.join(\"\\n\"));\n}\n\n/**\n * 常に同じ値が渡されているpropsを出力\n */\nexport function printConstantProps(constants: Constant[]): void {\n console.log(\"=== 常に同じ値が渡されているprops ===\\n\");\n\n if (constants.length === 0) {\n console.log(\"常に同じ値が渡されているpropsは見つかりませんでした。\");\n return;\n }\n\n for (const prop of constants) {\n const relativeComponentPath = path.relative(\n process.cwd(),\n prop.targetSourceFile,\n );\n\n console.log(`コンポーネント: ${prop.targetName}`);\n console.log(` 定義: ${relativeComponentPath}`);\n console.log(` prop: ${prop.paramName}`);\n console.log(` 常に渡される値: ${prop.value}`);\n console.log(` 使用箇所: ${prop.usages.length}箇所`);\n\n for (const usage of prop.usages) {\n const relativePath = path.relative(process.cwd(), usage.usageFilePath);\n console.log(` - ${relativePath}:${usage.usageLine}`);\n }\n\n console.log(\"\");\n }\n}\n\n/**\n * 解析結果を全て出力\n */\nexport function printAnalysisResult(result: AnalysisResult): void {\n printExportedComponents(result.exported);\n printConstantProps(result.constants);\n}\n\n/**\n * exportされた関数の一覧を出力\n */\nexport function printExportedFunctions(exported: Exported[]): void {\n const lines = [\n \"2. exportされた関数を収集中...\",\n ` → ${exported.length}個の関数を検出`,\n ...exported.map(\n (fn) =>\n ` - ${fn.name} (${path.relative(process.cwd(), fn.sourceFilePath)})`,\n ),\n \"\",\n ];\n console.log(lines.join(\"\\n\"));\n}\n\n/**\n * 常に同じ値が渡されている引数を出力\n */\nexport function printConstantArguments(constants: Constant[]): void {\n console.log(\"=== 常に同じ値が渡されている引数 ===\\n\");\n\n if (constants.length === 0) {\n console.log(\"常に同じ値が渡されている引数は見つかりませんでした。\");\n return;\n }\n\n for (const arg of constants) {\n const relativeFunctionPath = path.relative(\n process.cwd(),\n arg.targetSourceFile,\n );\n\n console.log(`関数: ${arg.targetName}`);\n console.log(` 定義: ${relativeFunctionPath}`);\n console.log(` 引数: ${arg.paramName}`);\n console.log(` 常に渡される値: ${arg.value}`);\n console.log(` 使用箇所: ${arg.usages.length}箇所`);\n\n for (const usage of arg.usages) {\n const relativePath = path.relative(process.cwd(), usage.usageFilePath);\n console.log(` - ${relativePath}:${usage.usageLine}`);\n }\n\n console.log(\"\");\n }\n}\n\n/**\n * 関数解析結果を全て出力\n */\nexport function printFunctionAnalysisResult(result: AnalysisResult): void {\n printExportedFunctions(result.exported);\n printConstantArguments(result.constants);\n}\n","import path from \"node:path\";\nimport { Project, type SourceFile } from \"ts-morph\";\nimport { isTestOrStorybookFile } from \"@/source/fileFilters\";\nimport type { FileFilter } from \"@/types\";\n\n/**\n * プロジェクトを初期化し、フィルタリングされたソースファイルを取得する\n */\nexport function createFilteredSourceFiles(\n targetDir: string,\n shouldExcludeFile: FileFilter = isTestOrStorybookFile,\n): SourceFile[] {\n // プロジェクトを初期化\n const project = new Project({\n tsConfigFilePath: path.join(process.cwd(), \"tsconfig.json\"),\n skipAddingFilesFromTsConfig: true,\n });\n\n // 対象ディレクトリのファイルを追加\n project.addSourceFilesAtPaths(`${targetDir}/**/*.{ts,tsx,js,jsx}`);\n\n // ファイルをフィルタリング\n const allSourceFiles = project.getSourceFiles();\n const sourceFilesToAnalyze = allSourceFiles.filter(\n (sourceFile) => !shouldExcludeFile(sourceFile.getFilePath()),\n );\n\n return sourceFilesToAnalyze;\n}\n","#!/usr/bin/env node\nimport { analyzeFunctionsCore } from \"@/analyzeFunctions\";\nimport { analyzePropsCore } from \"@/analyzeProps\";\nimport {\n type CliOptions,\n CliValidationError,\n getHelpMessage,\n parseCliOptions,\n validateTargetDir,\n} from \"@/cli/parseCliOptions\";\nimport {\n printAnalysisResult,\n printFunctionAnalysisResult,\n} from \"@/output/printAnalysisResult\";\nimport { createFilteredSourceFiles } from \"@/source/createFilteredSourceFiles\";\n\n/**\n * エラーメッセージを表示してプロセスを終了する\n */\nfunction exitWithError(message: string): never {\n console.error(`エラー: ${message}`);\n process.exit(1);\n}\n\nfunction main(): void {\n let options: CliOptions;\n\n try {\n options = parseCliOptions(process.argv.slice(2));\n } catch (error) {\n if (error instanceof CliValidationError) {\n exitWithError(error.message);\n }\n throw error;\n }\n\n const { targetDir, minUsages, target, showHelp } = options;\n\n if (showHelp) {\n console.log(getHelpMessage());\n process.exit(0);\n }\n\n // 対象ディレクトリの存在を検証\n try {\n validateTargetDir(targetDir);\n } catch (error) {\n if (error instanceof CliValidationError) {\n exitWithError(error.message);\n }\n throw error;\n }\n\n console.log(`解析対象ディレクトリ: ${targetDir}`);\n console.log(`最小使用箇所数: ${minUsages}`);\n console.log(`解析対象: ${target}\\n`);\n\n const sourceFilesToAnalyze = createFilteredSourceFiles(targetDir);\n\n if (target === \"all\" || target === \"components\") {\n const propsResult = analyzePropsCore(sourceFilesToAnalyze, { minUsages });\n printAnalysisResult(propsResult);\n }\n\n if (target === \"all\" || target === \"functions\") {\n const functionsResult = analyzeFunctionsCore(sourceFilesToAnalyze, {\n minUsages,\n });\n printFunctionAnalysisResult(functionsResult);\n }\n}\n\nmain();\n"],"mappings":"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"}
1
+ {"version":3,"file":"cli.mjs","names":["results: Exported[]","callable: Exported","groupedUsages: Record<string, Usage[]>","results: Exported[]","callable: Exported","groupedUsages: Record<string, Usage[]>","config: unknown","VALID_TARGETS: readonly AnalyzeMode[]","VALID_OUTPUTS: readonly OutputMode[]","result: DittoryConfig","VALID_TARGETS","VALID_OUTPUTS","DEFAULT_OPTIONS: ResolvedOptions","VALID_TARGETS: readonly AnalyzeMode[]","VALID_OUTPUTS: readonly OutputMode[]","VALID_OPTIONS: readonly string[]","result: RawCliOptions","cliOptions: ReturnType<typeof parseCliOptions>","fileConfig: Awaited<ReturnType<typeof loadConfig>>","allExported: AnalysisResult[\"exported\"]","allConstants: AnalysisResult[\"constants\"]"],"sources":["../src/analyzer/classMethodAnalyzer.ts","../src/analyzer/functionAnalyzer.ts","../src/analyzeFunctions.ts","../src/cli/loadConfig.ts","../src/cli/parseCliOptions.ts","../src/output/printAnalysisResult.ts","../src/source/createFilteredSourceFiles.ts","../src/cli.ts"],"sourcesContent":["import { Node, type ParameterDeclaration } from \"ts-morph\";\nimport { ExtractUsages } from \"@/extraction/extractUsages\";\nimport type {\n AnalyzerOptions,\n ClassifiedDeclaration,\n Definition,\n Exported,\n Usage,\n} from \"@/types\";\nimport { BaseAnalyzer } from \"./baseAnalyzer\";\n\n/**\n * クラスメソッドの引数分析を行うAnalyzer\n *\n * exportされたクラスのメソッド(static/instance)を収集し、\n * 各メソッドの引数使用状況を分析する。\n * 常に同じ値が渡されている引数を検出し、定数として報告する。\n *\n * @example\n * ```ts\n * const analyzer = new ClassMethodAnalyzer({ minUsages: 2 });\n * const result = analyzer.analyze(declarations);\n * console.log(result.constants);\n * ```\n */\nexport class ClassMethodAnalyzer extends BaseAnalyzer {\n constructor(options: AnalyzerOptions = {}) {\n super(options);\n }\n\n /**\n * 事前分類済みの宣言からクラスメソッドを収集する\n *\n * @param declarations - 事前分類済みの宣言配列(type: \"class\")\n * @returns クラスメソッドとその使用状況の配列(名前は「ClassName.methodName」形式)\n */\n protected collect(declarations: ClassifiedDeclaration[]): Exported[] {\n const results: Exported[] = [];\n\n for (const classified of declarations) {\n const { exportName, sourceFile, declaration } = classified;\n\n if (!Node.isClassDeclaration(declaration)) {\n continue;\n }\n\n const methods = declaration.getMethods();\n\n for (const method of methods) {\n const methodName = method.getName();\n const parameters = this.getParameters(method);\n\n const callable: Exported = {\n name: `${exportName}.${methodName}`,\n sourceFilePath: sourceFile.getFilePath(),\n sourceLine: method.getStartLineNumber(),\n definitions: parameters,\n declaration: method,\n usages: {},\n };\n\n // メソッド名から参照を検索\n const nameNode = method.getNameNode();\n if (!Node.isIdentifier(nameNode)) {\n continue;\n }\n\n // 名前ノードから全参照を検索し、除外対象ファイルからの参照をフィルタ\n const references = nameNode\n .findReferences()\n .flatMap((referencedSymbol) => referencedSymbol.getReferences())\n .filter(\n (ref) => !this.shouldExcludeFile(ref.getSourceFile().getFilePath()),\n );\n\n // 参照からメソッド呼び出しを抽出し、usagesをパラメータ名ごとにグループ化\n const groupedUsages: Record<string, Usage[]> = {};\n for (const reference of references) {\n const refNode = reference.getNode();\n\n // obj.method の形でPropertyAccessExpressionの一部かチェック\n const propertyAccess = refNode.getParent();\n if (\n !propertyAccess ||\n !Node.isPropertyAccessExpression(propertyAccess)\n ) {\n continue;\n }\n\n // obj.method(...) の形でCallExpressionかチェック\n const callExpression = propertyAccess.getParent();\n if (!callExpression || !Node.isCallExpression(callExpression)) {\n continue;\n }\n\n // 呼び出し対象がPropertyAccessExpressionと一致するか確認\n if (callExpression.getExpression() !== propertyAccess) {\n continue;\n }\n\n // メソッド呼び出しから引数使用状況を抽出\n const usages = ExtractUsages.fromCall(callExpression, callable);\n for (const usage of usages) {\n if (!groupedUsages[usage.name]) {\n groupedUsages[usage.name] = [];\n }\n groupedUsages[usage.name].push(usage);\n }\n }\n\n callable.usages = groupedUsages;\n results.push(callable);\n }\n }\n\n return results;\n }\n\n /**\n * メソッドのパラメータ定義を取得する\n */\n private getParameters(method: Node): Definition[] {\n const params = this.extractParameterDeclarations(method);\n\n return params.map((param, index) => ({\n name: param.getName(),\n index,\n required: !param.hasQuestionToken() && !param.hasInitializer(),\n }));\n }\n\n /**\n * メソッド宣言からParameterDeclarationの配列を抽出する\n */\n private extractParameterDeclarations(method: Node): ParameterDeclaration[] {\n if (Node.isMethodDeclaration(method)) {\n return method.getParameters();\n }\n\n return [];\n }\n}\n","import { Node, type ParameterDeclaration, SyntaxKind } from \"ts-morph\";\nimport { ExtractUsages } from \"@/extraction/extractUsages\";\nimport type {\n AnalyzerOptions,\n ClassifiedDeclaration,\n Definition,\n Exported,\n Usage,\n} from \"@/types\";\nimport { BaseAnalyzer } from \"./baseAnalyzer\";\n\n/**\n * 関数の引数分析を行うAnalyzer\n *\n * exportされた関数を収集し、各関数の引数使用状況を分析する。\n * 常に同じ値が渡されている引数を検出し、定数として報告する。\n *\n * @example\n * ```ts\n * const analyzer = new FunctionAnalyzer({ minUsages: 2 });\n * const result = analyzer.analyze(declarations);\n * console.log(result.constants);\n * ```\n */\nexport class FunctionAnalyzer extends BaseAnalyzer {\n constructor(options: AnalyzerOptions = {}) {\n super(options);\n }\n\n /**\n * 事前分類済みの宣言から関数を収集する\n *\n * @param declarations - 事前分類済みの宣言配列(type: \"function\")\n * @returns exportされた関数とその使用状況の配列\n */\n protected collect(declarations: ClassifiedDeclaration[]): Exported[] {\n const results: Exported[] = [];\n\n for (const classified of declarations) {\n const { exportName, sourceFile, declaration } = classified;\n\n // FunctionDeclaration または VariableDeclaration のみを処理\n if (\n !Node.isFunctionDeclaration(declaration) &&\n !Node.isVariableDeclaration(declaration)\n ) {\n continue;\n }\n\n // 関数の定義から名前ノードを取得\n const nameNode = declaration.getNameNode();\n if (!nameNode || !Node.isIdentifier(nameNode)) {\n continue;\n }\n\n // 名前ノードから全参照を検索し、除外対象ファイルからの参照をフィルタ\n const references = nameNode\n .findReferences()\n .flatMap((referencedSymbol) => referencedSymbol.getReferences())\n .filter(\n (ref) => !this.shouldExcludeFile(ref.getSourceFile().getFilePath()),\n );\n\n // 関数の宣言からパラメータ定義を取得\n const parameters = this.getParameters(declaration);\n\n const callable: Exported = {\n name: exportName,\n sourceFilePath: sourceFile.getFilePath(),\n sourceLine: declaration.getStartLineNumber(),\n definitions: parameters,\n declaration,\n usages: {},\n };\n\n // 参照から関数呼び出しを抽出し、usagesをパラメータ名ごとにグループ化\n const groupedUsages: Record<string, Usage[]> = {};\n for (const reference of references) {\n const refNode = reference.getNode();\n const parent = refNode.getParent();\n if (!parent) {\n continue;\n }\n\n // func(...) の形で関数呼び出しとして使われているかチェック\n const callExpression = parent.asKind(SyntaxKind.CallExpression);\n if (!callExpression) {\n continue;\n }\n\n // 呼び出し対象が参照ノードと一致するか確認\n const expression = callExpression.getExpression();\n if (expression !== refNode) {\n continue;\n }\n\n // 関数呼び出しから引数使用状況を抽出\n const usages = ExtractUsages.fromCall(callExpression, callable);\n for (const usage of usages) {\n if (!groupedUsages[usage.name]) {\n groupedUsages[usage.name] = [];\n }\n groupedUsages[usage.name].push(usage);\n }\n }\n\n callable.usages = groupedUsages;\n results.push(callable);\n }\n\n return results;\n }\n\n /**\n * 関数のパラメータ定義を取得する\n */\n private getParameters(declaration: Node): Definition[] {\n const params = this.extractParameterDeclarations(declaration);\n\n return params.map((param, index) => ({\n name: param.getName(),\n index,\n required: !param.hasQuestionToken() && !param.hasInitializer(),\n }));\n }\n\n /**\n * 宣言からParameterDeclarationの配列を抽出する\n */\n private extractParameterDeclarations(\n declaration: Node,\n ): ParameterDeclaration[] {\n if (Node.isFunctionDeclaration(declaration)) {\n return declaration.getParameters();\n }\n\n if (Node.isVariableDeclaration(declaration)) {\n const initializer = declaration.getInitializer();\n if (initializer) {\n if (\n Node.isArrowFunction(initializer) ||\n Node.isFunctionExpression(initializer)\n ) {\n return initializer.getParameters();\n }\n }\n }\n\n return [];\n }\n}\n","import type { SourceFile } from \"ts-morph\";\nimport { ClassMethodAnalyzer } from \"@/analyzer/classMethodAnalyzer\";\nimport { FunctionAnalyzer } from \"@/analyzer/functionAnalyzer\";\nimport { classifyDeclarations } from \"@/source/classifyDeclarations\";\nimport { isTestOrStorybookFile } from \"@/source/fileFilters\";\nimport type { AnalysisResult, FileFilter } from \"@/types\";\n\ninterface AnalyzeFunctionsOptions {\n shouldExcludeFile?: FileFilter;\n minUsages?: number;\n}\n\n/**\n * 関数・クラスメソッドの引数使用状況を解析し、常に同じ値が渡されている引数を検出する\n *\n * @param sourceFiles - 解析対象のソースファイル配列\n * @param options - オプション設定\n * @returns 解析結果(定数引数、統計情報、exportされた関数・メソッド)\n *\n * @example\n * const project = new Project();\n * project.addSourceFilesAtPaths(\"src/**\\/*.ts\");\n * const result = analyzeFunctionsCore(project.getSourceFiles());\n */\nexport function analyzeFunctionsCore(\n sourceFiles: SourceFile[],\n options: AnalyzeFunctionsOptions = {},\n): AnalysisResult {\n const { shouldExcludeFile = isTestOrStorybookFile, minUsages = 2 } = options;\n\n // 宣言を事前分類\n const declarations = classifyDeclarations(sourceFiles);\n const functions = declarations.filter((decl) => decl.type === \"function\");\n const classes = declarations.filter((decl) => decl.type === \"class\");\n\n const analyzerOptions = { shouldExcludeFile, minUsages };\n\n // 関数を分析\n const functionAnalyzer = new FunctionAnalyzer(analyzerOptions);\n const functionResult = functionAnalyzer.analyze(functions);\n\n // クラスメソッドを分析\n const classMethodAnalyzer = new ClassMethodAnalyzer(analyzerOptions);\n const classMethodResult = classMethodAnalyzer.analyze(classes);\n\n // 結果をマージ\n return {\n constants: [...functionResult.constants, ...classMethodResult.constants],\n exported: [...functionResult.exported, ...classMethodResult.exported],\n };\n}\n","import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { pathToFileURL } from \"node:url\";\nimport type { AnalyzeMode, OutputMode } from \"./parseCliOptions\";\n\n/** コンフィグファイルの検索順序 */\nconst CONFIG_FILE_NAMES = [\n \"dittory.config.js\",\n \"dittory.config.mjs\",\n \"dittory.config.json\",\n] as const;\n\n/**\n * コンフィグファイルの設定項目\n */\nexport interface DittoryConfig {\n minUsages?: number;\n target?: AnalyzeMode;\n output?: OutputMode;\n tsconfig?: string;\n targetDir?: string;\n}\n\n/**\n * コンフィグファイルを読み込む\n *\n * 現在の作業ディレクトリから以下の順序でコンフィグファイルを探す:\n * 1. dittory.config.js\n * 2. dittory.config.mjs\n * 3. dittory.config.json\n *\n * ファイルが存在しない場合は空のオブジェクトを返す。\n *\n * @returns コンフィグオブジェクト\n * @throws {Error} コンフィグファイルの読み込みに失敗した場合\n */\nexport async function loadConfig(): Promise<DittoryConfig> {\n const cwd = process.cwd();\n\n for (const fileName of CONFIG_FILE_NAMES) {\n const configPath = path.join(cwd, fileName);\n\n if (!fs.existsSync(configPath)) {\n continue;\n }\n\n if (fileName.endsWith(\".json\")) {\n return loadJsonConfig(configPath);\n }\n return loadJsConfig(configPath);\n }\n\n return {};\n}\n\n/**\n * JSON コンフィグを読み込む\n */\nfunction loadJsonConfig(configPath: string): DittoryConfig {\n const content = fs.readFileSync(configPath, \"utf-8\");\n\n try {\n const config: unknown = JSON.parse(content);\n\n if (typeof config !== \"object\" || config === null) {\n throw new Error(`Invalid config: expected object, got ${typeof config}`);\n }\n\n return validateConfig(config as Record<string, unknown>);\n } catch (error) {\n if (error instanceof SyntaxError) {\n throw new Error(\n `Failed to parse ${path.basename(configPath)}: ${error.message}`,\n );\n }\n throw error;\n }\n}\n\n/**\n * JS コンフィグを読み込む\n */\nasync function loadJsConfig(configPath: string): Promise<DittoryConfig> {\n try {\n // Windows 対応のため file:// URL に変換\n const fileUrl = pathToFileURL(configPath).href;\n const module = (await import(fileUrl)) as { default?: unknown };\n const config = module.default;\n\n if (typeof config !== \"object\" || config === null) {\n throw new Error(`Invalid config: expected object, got ${typeof config}`);\n }\n\n return validateConfig(config as Record<string, unknown>);\n } catch (error) {\n if (error instanceof Error) {\n throw new Error(\n `Failed to load ${path.basename(configPath)}: ${error.message}`,\n );\n }\n throw error;\n }\n}\n\nconst VALID_TARGETS: readonly AnalyzeMode[] = [\n \"all\",\n \"components\",\n \"functions\",\n];\nconst VALID_OUTPUTS: readonly OutputMode[] = [\"simple\", \"verbose\"];\n\n/**\n * コンフィグの値を検証する\n */\nfunction validateConfig(config: Record<string, unknown>): DittoryConfig {\n const result: DittoryConfig = {};\n\n if (\"minUsages\" in config) {\n if (typeof config.minUsages !== \"number\" || config.minUsages < 1) {\n throw new Error(\n `Invalid config: minUsages must be a number >= 1, got ${config.minUsages}`,\n );\n }\n result.minUsages = config.minUsages;\n }\n\n if (\"target\" in config) {\n if (!VALID_TARGETS.includes(config.target as AnalyzeMode)) {\n throw new Error(\n `Invalid config: target must be one of ${VALID_TARGETS.join(\", \")}, got ${config.target}`,\n );\n }\n result.target = config.target as AnalyzeMode;\n }\n\n if (\"output\" in config) {\n if (!VALID_OUTPUTS.includes(config.output as OutputMode)) {\n throw new Error(\n `Invalid config: output must be one of ${VALID_OUTPUTS.join(\", \")}, got ${config.output}`,\n );\n }\n result.output = config.output as OutputMode;\n }\n\n if (\"tsconfig\" in config) {\n if (typeof config.tsconfig !== \"string\" || config.tsconfig === \"\") {\n throw new Error(\n `Invalid config: tsconfig must be a non-empty string, got ${config.tsconfig}`,\n );\n }\n result.tsconfig = config.tsconfig;\n }\n\n if (\"targetDir\" in config) {\n if (typeof config.targetDir !== \"string\" || config.targetDir === \"\") {\n throw new Error(\n `Invalid config: targetDir must be a non-empty string, got ${config.targetDir}`,\n );\n }\n result.targetDir = config.targetDir;\n }\n\n return result;\n}\n","import fs from \"node:fs\";\n\nexport type AnalyzeMode = \"all\" | \"components\" | \"functions\";\nexport type OutputMode = \"simple\" | \"verbose\";\n\n/**\n * CLI で明示的に指定されたオプション(デフォルト値なし)\n */\nexport interface RawCliOptions {\n targetDir?: string;\n minUsages?: number;\n target?: AnalyzeMode;\n output?: OutputMode;\n tsconfig?: string;\n showHelp: boolean;\n}\n\n/**\n * 解決済みのオプション(デフォルト値適用後)\n */\nexport interface ResolvedOptions {\n targetDir: string;\n minUsages: number;\n target: AnalyzeMode;\n output: OutputMode;\n tsconfig: string;\n}\n\n/** デフォルトのオプション値 */\nexport const DEFAULT_OPTIONS: ResolvedOptions = {\n targetDir: \"./src\",\n minUsages: 2,\n target: \"all\",\n output: \"simple\",\n tsconfig: \"./tsconfig.json\",\n};\n\nexport class CliValidationError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"CliValidationError\";\n }\n}\n\nconst VALID_TARGETS: readonly AnalyzeMode[] = [\n \"all\",\n \"components\",\n \"functions\",\n];\n\nconst VALID_OUTPUTS: readonly OutputMode[] = [\"simple\", \"verbose\"];\n\n/** 不明なオプションの検出に使用 */\nconst VALID_OPTIONS: readonly string[] = [\n \"--min\",\n \"--target\",\n \"--output\",\n \"--tsconfig\",\n \"--help\",\n];\n\n/**\n * CLIオプションをパースする\n *\n * 明示的に指定されたオプションのみを返す(デフォルト値は含まない)\n *\n * @throws {CliValidationError} オプションが無効な場合\n */\nexport function parseCliOptions(args: string[]): RawCliOptions {\n const result: RawCliOptions = { showHelp: false };\n\n for (const arg of args) {\n if (arg === \"--help\") {\n result.showHelp = true;\n continue;\n }\n\n if (arg.startsWith(\"--min=\")) {\n const valueStr = arg.slice(6);\n const value = Number.parseInt(valueStr, 10);\n\n if (valueStr === \"\" || Number.isNaN(value)) {\n throw new CliValidationError(\n `Invalid value for --min: \"${valueStr}\" (must be a number)`,\n );\n }\n if (value < 1) {\n throw new CliValidationError(`--min must be at least 1: ${value}`);\n }\n\n result.minUsages = value;\n } else if (arg.startsWith(\"--target=\")) {\n const value = arg.slice(9);\n\n if (!VALID_TARGETS.includes(value as AnalyzeMode)) {\n throw new CliValidationError(\n `Invalid value for --target: \"${value}\" (valid values: ${VALID_TARGETS.join(\n \", \",\n )})`,\n );\n }\n\n result.target = value as AnalyzeMode;\n } else if (arg.startsWith(\"--output=\")) {\n const value = arg.slice(9);\n\n if (!VALID_OUTPUTS.includes(value as OutputMode)) {\n throw new CliValidationError(\n `Invalid value for --output: \"${value}\" (valid values: ${VALID_OUTPUTS.join(\n \", \",\n )})`,\n );\n }\n\n result.output = value as OutputMode;\n } else if (arg.startsWith(\"--tsconfig=\")) {\n const value = arg.slice(11);\n\n if (value === \"\") {\n throw new CliValidationError(\n \"Invalid value for --tsconfig: path cannot be empty\",\n );\n }\n\n result.tsconfig = value;\n } else if (arg.startsWith(\"--\")) {\n const optionName = arg.split(\"=\")[0];\n\n if (!VALID_OPTIONS.includes(optionName)) {\n throw new CliValidationError(`Unknown option: ${optionName}`);\n }\n } else {\n result.targetDir = arg;\n }\n }\n\n return result;\n}\n\n/**\n * 対象ディレクトリの存在を検証する\n *\n * @throws {CliValidationError} ディレクトリが存在しない、またはディレクトリでない場合\n */\nexport function validateTargetDir(targetDir: string): void {\n if (!fs.existsSync(targetDir)) {\n throw new CliValidationError(`Directory does not exist: ${targetDir}`);\n }\n\n const stat = fs.statSync(targetDir);\n if (!stat.isDirectory()) {\n throw new CliValidationError(`Path is not a directory: ${targetDir}`);\n }\n}\n\n/**\n * tsconfig.json の存在を検証する\n *\n * @throws {CliValidationError} ファイルが存在しない場合\n */\nexport function validateTsConfig(tsConfigPath: string): void {\n if (!fs.existsSync(tsConfigPath)) {\n throw new CliValidationError(`tsconfig not found: ${tsConfigPath}`);\n }\n}\n\n/**\n * ヘルプメッセージを取得する\n */\nexport function getHelpMessage(): string {\n return `\nUsage: dittory [options] [directory]\n\nOptions:\n --min=<number> Minimum usage count (default: 2)\n --target=<mode> Analysis target: all, components, functions (default: all)\n --output=<mode> Output mode: simple, verbose (default: simple)\n --tsconfig=<path> Path to tsconfig.json (default: ./tsconfig.json)\n --help Show this help message\n\nArguments:\n directory Target directory to analyze (default: ./src)\n`;\n}\n","import path from \"node:path\";\nimport type { OutputMode } from \"@/cli/parseCliOptions\";\nimport type { AnalysisResult, Constant, Exported } from \"@/types\";\n\nfunction bold(text: string): string {\n return `\\x1b[1m${text}\\x1b[0m`;\n}\n\nfunction green(text: string): string {\n return `\\x1b[32m${text}\\x1b[0m`;\n}\n\n/**\n * 値を表示用にフォーマットする\n *\n * 内部的にはenum区別のためにファイルパスを含むが、表示時は不要なので除去する\n * 例: \"/path/to/file.ts:ButtonVariant.Primary=\\\"primary\\\"\" → \"ButtonVariant.Primary\"\n */\nfunction formatValueForDisplay(value: string): string {\n // enum形式: \"ファイルパス:EnumName.MemberName=値\" のパターンをチェック\n const enumMatch = value.match(/^.+:(\\w+\\.\\w+)=.+$/);\n if (enumMatch) {\n return enumMatch[1];\n }\n return value;\n}\n\n/**\n * グループ化された定数情報\n */\ninterface GroupedConstant {\n targetName: string;\n targetSourceFile: string;\n targetLine: number;\n params: Array<{\n paramName: string;\n value: string;\n usageCount: number;\n usages: Constant[\"usages\"];\n }>;\n}\n\n/**\n * Constant[]を関数/コンポーネント単位でグループ化する\n */\nfunction groupConstantsByTarget(constants: Constant[]): GroupedConstant[] {\n const groupMap = new Map<string, GroupedConstant>();\n\n for (const constant of constants) {\n const key = `${constant.targetSourceFile}:${constant.targetName}`;\n\n let group = groupMap.get(key);\n if (!group) {\n group = {\n targetName: constant.targetName,\n targetSourceFile: constant.targetSourceFile,\n targetLine: constant.targetLine,\n params: [],\n };\n groupMap.set(key, group);\n }\n\n group.params.push({\n paramName: constant.paramName,\n value: constant.value,\n usageCount: constant.usages.length,\n usages: constant.usages,\n });\n }\n\n return Array.from(groupMap.values());\n}\n\n/**\n * exportされた関数の一覧を出力\n */\nfunction printExportedFunctions(exported: Exported[]): void {\n const lines = [\n \"Collecting exported functions...\",\n ` → Found ${exported.length} function(s)`,\n ...exported.map(\n (fn) =>\n ` - ${bold(green(fn.name))} (${path.relative(\n process.cwd(),\n fn.sourceFilePath,\n )})`,\n ),\n \"\",\n ];\n console.log(lines.join(\"\\n\"));\n}\n\n/**\n * 常に同じ値が渡されている引数を出力\n */\nfunction printConstantArguments(constants: Constant[]): void {\n if (constants.length === 0) {\n return;\n }\n\n const grouped = groupConstantsByTarget(constants);\n\n for (const group of grouped) {\n const relativePath = path.relative(process.cwd(), group.targetSourceFile);\n const usageCount = group.params[0]?.usageCount ?? 0;\n // 使用箇所は全パラメータで同じなので、最初のパラメータから取得\n const usages = group.params[0]?.usages ?? [];\n\n console.log(\n `${bold(green(group.targetName))} ${relativePath}:${group.targetLine}`,\n );\n console.log(\"Constant Arguments:\");\n\n for (const param of group.params) {\n console.log(\n ` - ${param.paramName} = ${formatValueForDisplay(param.value)}`,\n );\n }\n\n console.log(`Usages (${usageCount}):`);\n for (const usage of usages) {\n const usagePath = path.relative(process.cwd(), usage.usageFilePath);\n console.log(` - ${usagePath}:${usage.usageLine}`);\n }\n\n console.log(\"\\n\");\n }\n}\n\n/**\n * 統計情報を出力\n */\nfunction printStatistics(result: AnalysisResult): void {\n const totalFunctions = result.exported.length;\n const functionsWithConstants = groupConstantsByTarget(\n result.constants,\n ).length;\n\n console.log(\"---\");\n console.log(\n `Found ${functionsWithConstants} function(s) with constant arguments out of ${totalFunctions} function(s).`,\n );\n}\n\n/**\n * 解析結果を出力\n */\nexport function printAnalysisResult(\n result: AnalysisResult,\n mode: OutputMode,\n): void {\n if (mode === \"verbose\") {\n printExportedFunctions(result.exported);\n }\n\n if (result.constants.length === 0) {\n console.log(\"No arguments with constant values were found.\");\n } else {\n printConstantArguments(result.constants);\n }\n\n printStatistics(result);\n}\n","import path from \"node:path\";\nimport { Project, type SourceFile } from \"ts-morph\";\nimport { isTestOrStorybookFile } from \"@/source/fileFilters\";\nimport type { FileFilter } from \"@/types\";\n\nexport interface CreateFilteredSourceFilesOptions {\n shouldExcludeFile?: FileFilter;\n tsConfigFilePath?: string;\n}\n\n/**\n * プロジェクトを初期化し、フィルタリングされたソースファイルを取得する\n */\nexport function createFilteredSourceFiles(\n targetDir: string,\n options: CreateFilteredSourceFilesOptions = {},\n): SourceFile[] {\n const {\n shouldExcludeFile = isTestOrStorybookFile,\n tsConfigFilePath = path.join(process.cwd(), \"tsconfig.json\"),\n } = options;\n\n // プロジェクトを初期化\n const project = new Project({\n tsConfigFilePath,\n skipAddingFilesFromTsConfig: true,\n });\n\n // 対象ディレクトリのファイルを追加\n project.addSourceFilesAtPaths(`${targetDir}/**/*.{ts,tsx,js,jsx}`);\n\n // ファイルをフィルタリング\n const allSourceFiles = project.getSourceFiles();\n const sourceFilesToAnalyze = allSourceFiles.filter(\n (sourceFile) => !shouldExcludeFile(sourceFile.getFilePath()),\n );\n\n return sourceFilesToAnalyze;\n}\n","#!/usr/bin/env node\nimport path from \"node:path\";\nimport { analyzeFunctionsCore } from \"@/analyzeFunctions\";\nimport { analyzePropsCore } from \"@/analyzeProps\";\nimport { loadConfig } from \"@/cli/loadConfig\";\nimport {\n CliValidationError,\n DEFAULT_OPTIONS,\n getHelpMessage,\n parseCliOptions,\n type ResolvedOptions,\n validateTargetDir,\n validateTsConfig,\n} from \"@/cli/parseCliOptions\";\nimport { printAnalysisResult } from \"@/output/printAnalysisResult\";\nimport { createFilteredSourceFiles } from \"@/source/createFilteredSourceFiles\";\nimport type { AnalysisResult } from \"@/types\";\n\n/**\n * エラーメッセージを表示してプロセスを終了する\n */\nfunction exitWithError(message: string): never {\n console.error(`Error: ${message}`);\n process.exit(1);\n}\n\nasync function main(): Promise<void> {\n // CLI オプションをパース\n let cliOptions: ReturnType<typeof parseCliOptions>;\n try {\n cliOptions = parseCliOptions(process.argv.slice(2));\n } catch (error) {\n if (error instanceof CliValidationError) {\n exitWithError(error.message);\n }\n throw error;\n }\n\n if (cliOptions.showHelp) {\n console.log(getHelpMessage());\n process.exit(0);\n }\n\n // コンフィグファイルを読み込む\n let fileConfig: Awaited<ReturnType<typeof loadConfig>>;\n try {\n fileConfig = await loadConfig();\n } catch (error) {\n if (error instanceof Error) {\n exitWithError(error.message);\n }\n throw error;\n }\n\n // オプションをマージ: CLI > コンフィグ > デフォルト\n const options: ResolvedOptions = {\n targetDir: path.resolve(\n cliOptions.targetDir ?? fileConfig.targetDir ?? DEFAULT_OPTIONS.targetDir,\n ),\n minUsages:\n cliOptions.minUsages ?? fileConfig.minUsages ?? DEFAULT_OPTIONS.minUsages,\n target: cliOptions.target ?? fileConfig.target ?? DEFAULT_OPTIONS.target,\n output: cliOptions.output ?? fileConfig.output ?? DEFAULT_OPTIONS.output,\n tsconfig:\n cliOptions.tsconfig ?? fileConfig.tsconfig ?? DEFAULT_OPTIONS.tsconfig,\n };\n\n const { targetDir, minUsages, target, output, tsconfig } = options;\n\n // 対象ディレクトリの存在を検証\n try {\n validateTargetDir(targetDir);\n } catch (error) {\n if (error instanceof CliValidationError) {\n exitWithError(error.message);\n }\n throw error;\n }\n\n // tsconfig.json の存在を検証\n try {\n validateTsConfig(tsconfig);\n } catch (error) {\n if (error instanceof CliValidationError) {\n exitWithError(error.message);\n }\n throw error;\n }\n\n if (output === \"verbose\") {\n console.log(`Target directory: ${targetDir}`);\n console.log(`Minimum usage count: ${minUsages}`);\n console.log(`Analysis target: ${target}\\n`);\n }\n\n const sourceFilesToAnalyze = createFilteredSourceFiles(targetDir, {\n tsConfigFilePath: tsconfig,\n });\n\n // 各解析結果を収集\n const allExported: AnalysisResult[\"exported\"] = [];\n const allConstants: AnalysisResult[\"constants\"] = [];\n\n if (target === \"all\" || target === \"components\") {\n const propsResult = analyzePropsCore(sourceFilesToAnalyze, { minUsages });\n allExported.push(...propsResult.exported);\n allConstants.push(...propsResult.constants);\n }\n\n if (target === \"all\" || target === \"functions\") {\n const functionsResult = analyzeFunctionsCore(sourceFilesToAnalyze, {\n minUsages,\n });\n allExported.push(...functionsResult.exported);\n allConstants.push(...functionsResult.constants);\n }\n\n const result: AnalysisResult = {\n exported: allExported,\n constants: allConstants,\n };\n\n printAnalysisResult(result, output);\n}\n\nmain().catch((error) => {\n console.error(error);\n process.exit(1);\n});\n"],"mappings":"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"}
package/dist/index.d.mts CHANGED
@@ -35,6 +35,7 @@ interface Exported {
35
35
  /** クラスメソッドの場合は "ClassName.methodName" 形式 */
36
36
  name: string;
37
37
  sourceFilePath: string;
38
+ sourceLine: number;
38
39
  definitions: Definition[];
39
40
  declaration: FunctionDeclaration | VariableDeclaration | MethodDeclaration;
40
41
  usages: Record<string, Usage[]>;
@@ -45,6 +46,7 @@ interface Exported {
45
46
  interface Constant {
46
47
  targetName: string;
47
48
  targetSourceFile: string;
49
+ targetLine: number;
48
50
  paramName: string;
49
51
  value: string;
50
52
  usages: Usage[];
@@ -76,5 +78,21 @@ interface AnalyzePropsOptions {
76
78
  */
77
79
  declare function analyzePropsCore(sourceFiles: SourceFile[], options?: AnalyzePropsOptions): AnalysisResult;
78
80
  //#endregion
79
- export { type AnalysisResult, type Constant, type Definition, type Exported, type FileFilter, type Usage, analyzePropsCore };
81
+ //#region src/cli/parseCliOptions.d.ts
82
+ type AnalyzeMode = "all" | "components" | "functions";
83
+ type OutputMode = "simple" | "verbose";
84
+ //#endregion
85
+ //#region src/cli/loadConfig.d.ts
86
+ /**
87
+ * コンフィグファイルの設定項目
88
+ */
89
+ interface DittoryConfig {
90
+ minUsages?: number;
91
+ target?: AnalyzeMode;
92
+ output?: OutputMode;
93
+ tsconfig?: string;
94
+ targetDir?: string;
95
+ }
96
+ //#endregion
97
+ export { type AnalysisResult, type AnalyzeMode, type Constant, type Definition, type DittoryConfig, type Exported, type FileFilter, type OutputMode, type Usage, analyzePropsCore };
80
98
  //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs CHANGED
@@ -1,3 +1,3 @@
1
- import { t as analyzePropsCore } from "./analyzeProps-YWnY-Mf7.mjs";
1
+ import { t as analyzePropsCore } from "./analyzeProps-CoEqudRM.mjs";
2
2
 
3
3
  export { analyzePropsCore };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dittory",
3
- "version": "0.0.1",
4
- "description": "Reactコンポーネントのprops使用状況を解析し、常に同じ値が渡されているpropsを検出するツール",
3
+ "version": "0.0.2",
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",
7
7
  "types": "./dist/index.d.mts",
@@ -20,14 +20,6 @@
20
20
  "publishConfig": {
21
21
  "access": "public"
22
22
  },
23
- "scripts": {
24
- "build": "tsdown",
25
- "dev": "tsdown && node ./dist/cli.mjs",
26
- "lint": "biome check --write src",
27
- "test": "vitest --run",
28
- "typecheck": "tsc --noEmit",
29
- "prepublishOnly": "pnpm run build"
30
- },
31
23
  "keywords": [
32
24
  "react",
33
25
  "props",
@@ -41,7 +33,7 @@
41
33
  },
42
34
  "license": "MIT",
43
35
  "repository": "github:warabi1062/dittory",
44
- "packageManager": "pnpm@10.6.4",
36
+ "homepage": "https://warabi1062.github.io/dittory/",
45
37
  "devDependencies": {
46
38
  "@biomejs/biome": "2.3.10",
47
39
  "@types/node": "^25.0.3",
@@ -53,5 +45,12 @@
53
45
  },
54
46
  "dependencies": {
55
47
  "ts-morph": "^27.0.2"
48
+ },
49
+ "scripts": {
50
+ "build": "tsdown",
51
+ "dev": "tsdown && node ./dist/cli.mjs",
52
+ "lint": "biome check --write src",
53
+ "test": "vitest --run",
54
+ "typecheck": "tsc --noEmit"
56
55
  }
57
- }
56
+ }
@@ -1 +0,0 @@
1
- {"version":3,"file":"analyzeProps-YWnY-Mf7.mjs","names":["propsParam: Node | undefined","usages: Usage[]","groupedMap: GroupedMap","result: Constant[]","exportedComponents: Exported[]","component: Exported","groupedUsages: Record<string, Usage[]>","results: ClassifiedDeclaration[]","type: DeclarationType"],"sources":["../src/components/getProps.ts","../src/extraction/resolveExpressionValue.ts","../src/extraction/flattenObjectExpression.ts","../src/extraction/extractUsages.ts","../src/source/fileFilters.ts","../src/utils/getSingleValueFromSet.ts","../src/analyzer/baseAnalyzer.ts","../src/analyzer/componentAnalyzer.ts","../src/components/isReactComponent.ts","../src/source/classifyDeclarations.ts","../src/analyzeProps.ts"],"sourcesContent":["import { Node, type Type } from \"ts-morph\";\nimport type { Definition } from \"@/types\";\n\n/**\n * コンポーネントのprops定義を取得する\n *\n * 関数の第一パラメータの型情報からpropsを抽出する。\n * 「Props」などの命名規則に依存せず、TypeScriptの型システムから直接取得するため、\n * どのような命名でもpropsを正確に取得できる。\n *\n * 対応パターン:\n * - function Component(props: Props)\n * - const Component = (props: Props) => ...\n * - React.forwardRef((props, ref) => ...)\n * - React.memo((props) => ...)\n */\nexport function getProps(declaration: Node): Definition[] {\n // 関数宣言、アロー関数、関数式から第一パラメータ(props)を取得\n let propsParam: Node | undefined;\n\n if (Node.isFunctionDeclaration(declaration)) {\n const params = declaration.getParameters();\n if (params.length > 0) {\n propsParam = params[0];\n }\n } else if (Node.isVariableDeclaration(declaration)) {\n const initializer = declaration.getInitializer();\n if (initializer) {\n if (\n Node.isArrowFunction(initializer) ||\n Node.isFunctionExpression(initializer)\n ) {\n const params = initializer.getParameters();\n if (params.length > 0) {\n propsParam = params[0];\n }\n } else if (Node.isCallExpression(initializer)) {\n // React.forwardRef, React.memo などのラッパー関数の場合\n const args = initializer.getArguments();\n for (const arg of args) {\n if (Node.isArrowFunction(arg) || Node.isFunctionExpression(arg)) {\n const params = arg.getParameters();\n if (params.length > 0) {\n propsParam = params[0];\n break;\n }\n }\n }\n }\n }\n }\n\n if (!propsParam) {\n return [];\n }\n\n // パラメータの型情報を取得\n const propsType = propsParam.getType();\n return extractPropsFromType(propsType);\n}\n\n/**\n * 型からprops定義を抽出する\n *\n * Mapを使用する理由:\n * - 交差型(A & B)の場合、同じprop名が複数の型に存在する可能性がある\n * - 最初に見つかった定義を優先し、重複を排除するためにMapを使用\n * - 抽出後にindexを付与してDefinition配列として返す\n */\nfunction extractPropsFromType(type: Type): Definition[] {\n const propsMap = new Map<string, Omit<Definition, \"index\">>();\n\n collectPropsFromType(type, propsMap);\n\n return Array.from(propsMap.values()).map((prop, index) => ({\n ...prop,\n index,\n }));\n}\n\n/**\n * 型からpropsを収集する(交差型の場合は再帰的に処理)\n */\nfunction collectPropsFromType(\n type: Type,\n propsMap: Map<string, Omit<Definition, \"index\">>,\n): void {\n if (type.isIntersection()) {\n for (const intersectionType of type.getIntersectionTypes()) {\n collectPropsFromType(intersectionType, propsMap);\n }\n return;\n }\n\n const properties = type.getProperties();\n for (const prop of properties) {\n const propName = prop.getName();\n const declarations = prop.getDeclarations();\n\n let isOptional = false;\n for (const decl of declarations) {\n if (Node.isPropertySignature(decl) && decl.hasQuestionToken()) {\n isOptional = true;\n break;\n }\n }\n\n // 同じ名前のpropが既に存在する場合は上書きしない(最初に見つかった定義を優先)\n if (!propsMap.has(propName)) {\n propsMap.set(propName, {\n name: propName,\n required: !isOptional,\n });\n }\n }\n}\n","import { Node } from \"ts-morph\";\n\n/**\n * 引数が渡されなかった場合を表す特別な値\n * 必須/任意を問わず、引数未指定の使用箇所を統一的に扱うために使用\n */\nexport const UNDEFINED_VALUE = \"undefined\";\n\n/**\n * 関数型の値を表すプレフィックス\n * コールバック関数など、関数が渡された場合は使用箇所ごとにユニークな値として扱う\n * これにより、同じコールバック関数を渡していても「定数」として検出されない\n */\nexport const FUNCTION_VALUE_PREFIX = \"[function]\";\n\n/**\n * 式の実際の値を解決する\n *\n * 異なるファイルで同じenum値やリテラル値を使用している場合でも、\n * 同一の値として認識できるよう、値を正規化して文字列表現で返す。\n * 同名だが異なる定義(別ファイルの同名enum等)を区別するため、\n * 必要に応じてファイルパスを含めた識別子を返す。\n */\nexport function resolveExpressionValue(expression: Node): string {\n // 関数型の場合は定数として扱わない。\n // 同じ関数参照でも使用箇所ごとに異なる値として扱うことで、\n // コールバック関数が「常に同じ値」として誤検出されるのを防ぐ\n const type = expression.getType();\n if (type.getCallSignatures().length > 0) {\n const sourceFile = expression.getSourceFile();\n const line = expression.getStartLineNumber();\n return `${FUNCTION_VALUE_PREFIX}${sourceFile.getFilePath()}:${line}`;\n }\n\n // PropertyAccessExpression (例: Status.Active) の場合\n // enum参照を正確に識別するために、ファイルパスを含めた完全修飾名を返す\n if (Node.isPropertyAccessExpression(expression)) {\n const symbol = expression.getSymbol();\n const decl = symbol?.getDeclarations()[0];\n\n // enum memberの場合は、同名enumの誤認識を防ぐためファイルパスを含める\n if (decl && Node.isEnumMember(decl)) {\n const enumDecl = decl.getParent();\n if (Node.isEnumDeclaration(enumDecl)) {\n const filePath = enumDecl.getSourceFile().getFilePath();\n const enumName = enumDecl.getName();\n const memberName = decl.getName();\n const value = decl.getValue();\n // 例: \"/path/to/file.ts:Status.Active=0\"\n return `${filePath}:${enumName}.${memberName}=${JSON.stringify(value)}`;\n }\n }\n }\n\n // Identifier (変数参照) の場合\n // 変数の定義元を辿って実際の値を解決する\n if (Node.isIdentifier(expression)) {\n if (expression.getText() === \"undefined\") {\n return UNDEFINED_VALUE;\n }\n\n const symbol = expression.getSymbol();\n const decl = symbol?.getDeclarations()[0];\n\n if (decl && Node.isVariableDeclaration(decl)) {\n const initializer = decl.getInitializer();\n // 初期化子がある場合はその値を使用(空白は正規化して比較しやすくする)\n // 初期化子がない場合は、ファイルパス + 変数名で一意に識別\n return initializer\n ? initializer.getText().replace(/\\s+/g, \" \")\n : `${decl.getSourceFile().getFilePath()}:${expression.getText()}`;\n }\n\n // パラメータや分割代入など、VariableDeclaration以外の宣言の場合\n // ファイルパス + 変数名で識別する\n if (decl) {\n return `${decl.getSourceFile().getFilePath()}:${expression.getText()}`;\n }\n }\n\n // リテラル型の場合は型情報から値を取得\n // JSON.stringifyで文字列と数値を区別できる形式にする(\"foo\" vs 42)\n if (type.isStringLiteral() || type.isNumberLiteral()) {\n return JSON.stringify(type.getLiteralValue());\n }\n\n // booleanリテラルの場合はgetLiteralValue()がundefinedを返すためgetText()を使用\n if (type.isBooleanLiteral()) {\n return type.getText();\n }\n\n // 上記のいずれにも該当しない場合(オブジェクト、配列、テンプレートリテラル等)\n // ソースコードのテキストをそのまま返す\n return expression.getText();\n}\n","import { Node } from \"ts-morph\";\nimport { resolveExpressionValue } from \"@/extraction/resolveExpressionValue\";\n\nexport type FlattenedValue = { key: string; value: string };\n\n/**\n * オブジェクトリテラルを再帰的に解析し、フラットなkey-valueペアを返す\n *\n * @param expression - 解析対象の式ノード\n * @param prefix - キー名のプレフィックス(ネストしたプロパティの親パスを表す)\n * @returns フラット化されたkey-valueペアの配列\n *\n * @example\n * // { a: { b: 1, c: 2 } } → [{ key: \"prefix.a.b\", value: \"1\" }, { key: \"prefix.a.c\", value: \"2\" }]\n */\nexport function flattenObjectExpression(\n expression: Node,\n prefix: string,\n): FlattenedValue[] {\n if (!Node.isObjectLiteralExpression(expression)) {\n // オブジェクトリテラル以外の場合は単一の値として返す\n return [{ key: prefix, value: resolveExpressionValue(expression) }];\n }\n\n return expression.getProperties().flatMap((property) => {\n if (Node.isPropertyAssignment(property)) {\n const propertyName = property.getName();\n const nestedPrefix = prefix ? `${prefix}.${propertyName}` : propertyName;\n const initializer = property.getInitializer();\n\n return initializer\n ? flattenObjectExpression(initializer, nestedPrefix)\n : [];\n }\n\n if (Node.isShorthandPropertyAssignment(property)) {\n // { foo } のような省略形\n const propertyName = property.getName();\n const nestedPrefix = prefix ? `${prefix}.${propertyName}` : propertyName;\n return [\n {\n key: nestedPrefix,\n value: resolveExpressionValue(property.getNameNode()),\n },\n ];\n }\n\n return [];\n });\n}\n","import {\n type CallExpression,\n type JsxAttribute,\n type JsxOpeningElement,\n type JsxSelfClosingElement,\n Node,\n} from \"ts-morph\";\nimport type { Definition, Exported, Usage } from \"@/types\";\nimport { flattenObjectExpression } from \"./flattenObjectExpression\";\nimport { UNDEFINED_VALUE } from \"./resolveExpressionValue\";\n\n/**\n * 使用状況を抽出するユーティリティクラス\n */\nexport class ExtractUsages {\n /**\n * 関数呼び出しから引数の使用状況を抽出する\n *\n * オブジェクトリテラルの場合は再帰的にフラット化し、\n * 各プロパティを「引数名.プロパティ名」形式で記録する。\n *\n * @param callExpression - 関数呼び出しノード\n * @param callable - 対象の関数情報\n * @returns 引数使用状況の配列\n */\n static fromCall(callExpression: CallExpression, callable: Exported): Usage[] {\n const usages: Usage[] = [];\n const sourceFile = callExpression.getSourceFile();\n const args = callExpression.getArguments();\n\n for (const param of callable.definitions) {\n const arg = args[param.index];\n\n if (!arg) {\n // 引数が渡されていない場合はundefinedとして記録\n usages.push({\n name: param.name,\n value: UNDEFINED_VALUE,\n usageFilePath: sourceFile.getFilePath(),\n usageLine: callExpression.getStartLineNumber(),\n });\n continue;\n }\n\n // オブジェクトリテラルの場合は再帰的にフラット化\n for (const { key, value } of flattenObjectExpression(arg, param.name)) {\n usages.push({\n name: key,\n value,\n usageFilePath: sourceFile.getFilePath(),\n usageLine: arg.getStartLineNumber(),\n });\n }\n }\n\n return usages;\n }\n\n /**\n * JSX要素からprops使用状況を抽出する\n *\n * @param element - JSX要素ノード\n * @param definitions - props定義の配列\n * @returns props使用状況の配列\n */\n static fromJsxElement(\n element: JsxOpeningElement | JsxSelfClosingElement,\n definitions: Definition[],\n ): Usage[] {\n const usages: Usage[] = [];\n const sourceFile = element.getSourceFile();\n\n // JSX属性をMapに変換\n const attributeMap = new Map<string, JsxAttribute>();\n for (const attr of element.getAttributes()) {\n if (Node.isJsxAttribute(attr)) {\n attributeMap.set(attr.getNameNode().getText(), attr);\n }\n }\n\n // definitionsをループして処理\n for (const prop of definitions) {\n const attr = attributeMap.get(prop.name);\n\n if (!attr) {\n // 渡されていない場合(required/optional問わず記録)\n usages.push({\n name: prop.name,\n value: UNDEFINED_VALUE,\n usageFilePath: sourceFile.getFilePath(),\n usageLine: element.getStartLineNumber(),\n });\n continue;\n }\n\n // 属性が渡されている場合、値を抽出\n const initializer = attr.getInitializer();\n\n if (!initializer) {\n // boolean shorthand (例: <Component disabled />)\n usages.push({\n name: prop.name,\n value: \"true\",\n usageFilePath: sourceFile.getFilePath(),\n usageLine: attr.getStartLineNumber(),\n });\n } else if (Node.isJsxExpression(initializer)) {\n // {expression} 形式\n const expression = initializer.getExpression();\n if (!expression) {\n continue;\n }\n for (const { key, value } of flattenObjectExpression(\n expression,\n prop.name,\n )) {\n usages.push({\n name: key,\n value,\n usageFilePath: sourceFile.getFilePath(),\n usageLine: attr.getStartLineNumber(),\n });\n }\n } else {\n // \"string\" 形式\n usages.push({\n name: prop.name,\n value: initializer.getText(),\n usageFilePath: sourceFile.getFilePath(),\n usageLine: attr.getStartLineNumber(),\n });\n }\n }\n\n return usages;\n }\n}\n","/**\n * ファイルパスがテストファイルまたはStorybookファイルかどうかを判定する\n * - 拡張子が .test.* / .spec.* / .stories.* のファイル\n * - __tests__ / __stories__ フォルダ内のファイル\n */\nexport function isTestOrStorybookFile(filePath: string): boolean {\n // 拡張子ベースの判定\n if (/\\.(test|spec|stories)\\.(ts|tsx|js|jsx)$/.test(filePath)) {\n return true;\n }\n\n // フォルダ名ベースの判定\n if (/\\b__tests__\\b|\\b__stories__\\b/.test(filePath)) {\n return true;\n }\n\n return false;\n}\n","/**\n * Setから唯一の値を安全に取得する\n */\nexport function getSingleValueFromSet(values: Set<string>): string {\n if (values.size !== 1) {\n throw new Error(`Expected exactly 1 value, got ${values.size}`);\n }\n const [firstValue] = Array.from(values);\n return firstValue;\n}\n","import { FUNCTION_VALUE_PREFIX } from \"@/extraction/resolveExpressionValue\";\nimport { isTestOrStorybookFile } from \"@/source/fileFilters\";\nimport type {\n AnalysisResult,\n AnalyzerOptions,\n ClassifiedDeclaration,\n Constant,\n Exported,\n FileFilter,\n Usage,\n} from \"@/types\";\nimport { getSingleValueFromSet } from \"@/utils/getSingleValueFromSet\";\n\n/**\n * 使用データのグループ\n * values.size === 1 の場合、そのパラメータは「定数」として検出される\n */\ninterface UsageData {\n values: Set<string>;\n usages: Usage[];\n}\n\n/**\n * 使用状況を階層的にグループ化したマップ\n *\n * 3階層の構造で使用状況を整理する:\n * 1. ソースファイルパス: どのファイルで定義された対象か\n * 2. 対象名: 関数名/コンポーネント名\n * 3. パラメータ名: 各パラメータごとの使用状況\n *\n * この構造により、定数検出時に効率的に走査できる。\n */\ntype GroupedMap = Map<string, Map<string, Map<string, UsageData>>>;\n\n/**\n * 分析処理の基底クラス\n */\nexport abstract class BaseAnalyzer {\n protected shouldExcludeFile: FileFilter;\n protected minUsages: number;\n\n constructor(options: AnalyzerOptions = {}) {\n this.shouldExcludeFile = options.shouldExcludeFile ?? isTestOrStorybookFile;\n this.minUsages = options.minUsages ?? 2;\n }\n\n /**\n * メイン分析処理\n *\n * @param declarations - 事前分類済みの宣言配列\n */\n analyze(declarations: ClassifiedDeclaration[]): AnalysisResult {\n // 1. エクスポートされた対象を収集\n const exported = this.collect(declarations);\n\n // 2. 使用状況をグループ化\n const groupedMap = this.createGroupedMap(exported);\n\n // 3. 常に同じ値が渡されているパラメータを抽出\n const constants = this.extractConstants(groupedMap);\n\n // 4. 結果を構築\n return { constants, exported };\n }\n\n /**\n * エクスポートされた対象を収集する(サブクラスで実装)\n *\n * @param declarations - 事前分類済みの宣言配列\n */\n protected abstract collect(declarations: ClassifiedDeclaration[]): Exported[];\n\n /**\n * 使用状況をグループ化したマップを作成\n */\n private createGroupedMap(exported: Exported[]): GroupedMap {\n const groupedMap: GroupedMap = new Map();\n\n for (const item of exported) {\n let fileMap = groupedMap.get(item.sourceFilePath);\n if (!fileMap) {\n fileMap = new Map();\n groupedMap.set(item.sourceFilePath, fileMap);\n }\n\n const paramMap = new Map<string, UsageData>();\n for (const [paramName, usages] of Object.entries(item.usages)) {\n const values = new Set<string>();\n for (const usage of usages) {\n values.add(usage.value);\n }\n paramMap.set(paramName, { values, usages });\n }\n fileMap.set(item.name, paramMap);\n }\n\n return groupedMap;\n }\n\n /**\n * 常に同じ値が渡されているパラメータを抽出\n */\n private extractConstants(groupedMap: GroupedMap): Constant[] {\n const result: Constant[] = [];\n\n for (const [sourceFile, targetMap] of groupedMap) {\n for (const [targetName, paramMap] of targetMap) {\n for (const [paramName, usageData] of paramMap) {\n const isConstant =\n usageData.usages.length >= this.minUsages &&\n usageData.values.size === 1;\n\n if (!isConstant) {\n continue;\n }\n\n const value = getSingleValueFromSet(usageData.values);\n\n // 関数型の値は定数として報告しない\n // (onClickに同じハンドラを渡している等は、デフォルト値化の候補ではない)\n if (value.startsWith(FUNCTION_VALUE_PREFIX)) {\n continue;\n }\n\n result.push({\n targetName,\n targetSourceFile: sourceFile,\n paramName,\n value,\n usages: usageData.usages,\n });\n }\n }\n }\n\n return result;\n }\n}\n","import { Node, SyntaxKind } from \"ts-morph\";\nimport { getProps } from \"@/components/getProps\";\nimport { ExtractUsages } from \"@/extraction/extractUsages\";\nimport type {\n AnalyzerOptions,\n ClassifiedDeclaration,\n Exported,\n Usage,\n} from \"@/types\";\nimport { BaseAnalyzer } from \"./baseAnalyzer\";\n\n/**\n * Reactコンポーネントのprops分析を行うAnalyzer\n *\n * exportされたReactコンポーネントを収集し、各コンポーネントのprops使用状況を分析する。\n * 常に同じ値が渡されているpropsを検出し、定数として報告する。\n *\n * @example\n * ```ts\n * const analyzer = new ComponentAnalyzer({ minUsages: 2 });\n * const result = analyzer.analyze(sourceFiles);\n * console.log(result.constants);\n * ```\n */\nexport class ComponentAnalyzer extends BaseAnalyzer {\n constructor(options: AnalyzerOptions = {}) {\n super(options);\n }\n\n /**\n * 事前分類済みの宣言からReactコンポーネントを収集する\n *\n * @param declarations - 事前分類済みの宣言配列\n * @returns exportされたコンポーネントとその使用状況の配列\n */\n protected collect(declarations: ClassifiedDeclaration[]): Exported[] {\n const exportedComponents: Exported[] = [];\n\n for (const classified of declarations) {\n const { exportName, sourceFile, declaration } = classified;\n\n // FunctionDeclaration または VariableDeclaration のみを処理\n if (\n !Node.isFunctionDeclaration(declaration) &&\n !Node.isVariableDeclaration(declaration)\n ) {\n continue;\n }\n\n // コンポーネントの定義から名前ノードを取得\n const nameNode = declaration.getNameNode();\n if (!nameNode || !Node.isIdentifier(nameNode)) {\n continue;\n }\n\n // 名前ノードから全参照を検索し、除外対象ファイルからの参照をフィルタ\n const references = nameNode\n .findReferences()\n .flatMap((referencedSymbol) => referencedSymbol.getReferences())\n .filter(\n (ref) => !this.shouldExcludeFile(ref.getSourceFile().getFilePath()),\n );\n\n // コンポーネントの宣言からprops定義を取得\n const props = getProps(declaration);\n\n const component: Exported = {\n name: exportName,\n sourceFilePath: sourceFile.getFilePath(),\n definitions: props,\n declaration,\n usages: {},\n };\n\n // 参照からJSX要素を抽出し、usagesをprop名ごとにグループ化\n const groupedUsages: Record<string, Usage[]> = {};\n for (const reference of references) {\n const refNode = reference.getNode();\n const parent = refNode.getParent();\n if (!parent) {\n continue;\n }\n\n // <Component> または <Component /> の形でJSX要素として使われているかチェック\n const jsxElement =\n parent.asKind(SyntaxKind.JsxOpeningElement) ??\n parent.asKind(SyntaxKind.JsxSelfClosingElement);\n\n if (!jsxElement) {\n continue;\n }\n\n // タグ名ノードが参照ノードと一致するか確認\n const tagNameNode = jsxElement.getTagNameNode();\n if (tagNameNode !== refNode) {\n continue;\n }\n\n // JSX要素からprops使用状況を抽出\n const usages = ExtractUsages.fromJsxElement(\n jsxElement,\n component.definitions,\n );\n for (const usage of usages) {\n if (!groupedUsages[usage.name]) {\n groupedUsages[usage.name] = [];\n }\n groupedUsages[usage.name].push(usage);\n }\n }\n\n component.usages = groupedUsages;\n exportedComponents.push(component);\n }\n\n return exportedComponents;\n }\n}\n","import { Node } from \"ts-morph\";\n\n/**\n * 宣言がReactコンポーネントかどうかを判定する\n * - 関数がJSXを返しているかチェック\n * - React.FC型注釈を持っているかチェック\n */\nexport function isReactComponent(declaration: Node): boolean {\n // 関数宣言の場合\n if (Node.isFunctionDeclaration(declaration)) {\n return containsJsx(declaration);\n }\n\n // 変数宣言の場合 (const Button = ...)\n if (Node.isVariableDeclaration(declaration)) {\n const initializer = declaration.getInitializer();\n if (!initializer) return false;\n\n // アロー関数または関数式の場合\n if (\n Node.isArrowFunction(initializer) ||\n Node.isFunctionExpression(initializer)\n ) {\n return containsJsx(initializer);\n }\n\n // React.forwardRef, React.memo などのラッパー関数の場合\n if (Node.isCallExpression(initializer)) {\n const args = initializer.getArguments();\n for (const arg of args) {\n if (Node.isArrowFunction(arg) || Node.isFunctionExpression(arg)) {\n if (containsJsx(arg)) return true;\n }\n }\n }\n }\n\n return false;\n}\n\n/**\n * ノード内にJSX要素が含まれているかチェック\n * 効率化: 1回のトラバースで全てのJSX要素をチェック\n */\nfunction containsJsx(node: Node): boolean {\n let hasJsx = false;\n\n node.forEachDescendant((descendant) => {\n if (\n Node.isJsxElement(descendant) ||\n Node.isJsxSelfClosingElement(descendant) ||\n Node.isJsxFragment(descendant)\n ) {\n hasJsx = true;\n return true; // 早期終了\n }\n return undefined;\n });\n\n return hasJsx;\n}\n","import { Node, type SourceFile } from \"ts-morph\";\nimport { isReactComponent } from \"@/components/isReactComponent\";\nimport type { ClassifiedDeclaration, DeclarationType } from \"@/types\";\n\n/**\n * ソースファイルからexportされた関数/コンポーネント/クラス宣言を収集し、\n * 種別(react/function/class)を事前に分類する\n *\n * @param sourceFiles - 分析対象のソースファイル配列\n * @returns 分類済みの宣言配列\n */\nexport function classifyDeclarations(\n sourceFiles: SourceFile[],\n): ClassifiedDeclaration[] {\n const results: ClassifiedDeclaration[] = [];\n\n for (const sourceFile of sourceFiles) {\n const exportedDecls = sourceFile.getExportedDeclarations();\n\n for (const [exportName, declarations] of exportedDecls) {\n // 関数宣言または変数宣言(アロー関数/関数式)を見つける\n const funcDecl = declarations.find((decl) => isFunctionLike(decl));\n\n if (funcDecl) {\n if (\n Node.isFunctionDeclaration(funcDecl) ||\n Node.isVariableDeclaration(funcDecl)\n ) {\n const type: DeclarationType = isReactComponent(funcDecl)\n ? \"react\"\n : \"function\";\n results.push({\n exportName,\n sourceFile,\n declaration: funcDecl,\n type,\n });\n }\n continue;\n }\n\n // クラス宣言を見つける\n const classDecl = declarations.find((decl) =>\n Node.isClassDeclaration(decl),\n );\n\n if (classDecl && Node.isClassDeclaration(classDecl)) {\n results.push({\n exportName,\n sourceFile,\n declaration: classDecl,\n type: \"class\",\n });\n }\n }\n }\n\n return results;\n}\n\n/**\n * 宣言が関数的なもの(関数宣言、アロー関数、関数式)かどうかを判定\n */\nfunction isFunctionLike(declaration: Node): boolean {\n if (Node.isFunctionDeclaration(declaration)) {\n return true;\n }\n\n if (Node.isVariableDeclaration(declaration)) {\n const initializer = declaration.getInitializer();\n if (!initializer) {\n return false;\n }\n\n return (\n Node.isArrowFunction(initializer) ||\n Node.isFunctionExpression(initializer) ||\n Node.isCallExpression(initializer) // React.forwardRef, React.memo など\n );\n }\n\n return false;\n}\n","import type { SourceFile } from \"ts-morph\";\nimport { ComponentAnalyzer } from \"@/analyzer/componentAnalyzer\";\nimport { classifyDeclarations } from \"@/source/classifyDeclarations\";\nimport { isTestOrStorybookFile } from \"@/source/fileFilters\";\nimport type { AnalysisResult, FileFilter } from \"@/types\";\n\ninterface AnalyzePropsOptions {\n shouldExcludeFile?: FileFilter;\n minUsages?: number;\n}\n\n/**\n * コンポーネントのprops使用状況を解析し、常に同じ値が渡されているpropsを検出する\n *\n * @param sourceFiles - 解析対象のソースファイル配列\n * @param options - オプション設定\n * @returns 解析結果(定数props、統計情報、exportされたコンポーネント)\n *\n * @example\n * const project = new Project();\n * project.addSourceFilesAtPaths(\"src/**\\/*.tsx\");\n * const result = analyzePropsCore(project.getSourceFiles());\n */\nexport function analyzePropsCore(\n sourceFiles: SourceFile[],\n options: AnalyzePropsOptions = {},\n): AnalysisResult {\n const { shouldExcludeFile = isTestOrStorybookFile, minUsages = 2 } = options;\n\n // 宣言を事前分類し、Reactコンポーネントのみ抽出\n const declarations = classifyDeclarations(sourceFiles);\n const components = declarations.filter((decl) => decl.type === \"react\");\n\n const analyzer = new ComponentAnalyzer({\n shouldExcludeFile,\n minUsages,\n });\n\n return analyzer.analyze(components);\n}\n"],"mappings":"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"}