dittory 0.0.4 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.mjs","names":["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/cli/loadConfig.ts","../src/cli/parseCliOptions.ts","../src/output/printAnalysisResult.ts","../src/source/createFilteredSourceFiles.ts","../src/cli.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { pathToFileURL } from \"node:url\";\nimport { VALID_VALUE_TYPES, type ValueType } from \"@/types\";\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 valueTypes?: ValueType[] | \"all\";\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 if (\"valueTypes\" in config) {\n const value = config.valueTypes;\n\n if (value === \"all\") {\n result.valueTypes = \"all\";\n } else if (Array.isArray(value)) {\n for (const type of value) {\n if (!VALID_VALUE_TYPES.includes(type as ValueType)) {\n throw new Error(\n `Invalid config: valueTypes contains invalid type \"${type}\" (valid values: ${VALID_VALUE_TYPES.join(\", \")}, all)`,\n );\n }\n }\n result.valueTypes = value as ValueType[];\n } else {\n throw new Error(\n `Invalid config: valueTypes must be \"all\" or an array of value types, got ${value}`,\n );\n }\n }\n\n return result;\n}\n","import fs from \"node:fs\";\nimport { VALID_VALUE_TYPES, type ValueType } from \"@/types\";\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 valueTypes?: ValueType[];\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 valueTypes: ValueType[] | \"all\";\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 valueTypes: \"all\",\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 \"--value-types\",\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(\"--value-types=\")) {\n const value = arg.slice(14);\n\n if (value === \"\") {\n throw new CliValidationError(\n \"Invalid value for --value-types: value cannot be empty\",\n );\n }\n\n // \"all\" の場合はデフォルト扱い(RawCliOptionsには含めない)\n if (value === \"all\") {\n continue;\n }\n\n const types = value.split(\",\").map((t) => t.trim());\n for (const type of types) {\n if (!VALID_VALUE_TYPES.includes(type as ValueType)) {\n throw new CliValidationError(\n `Invalid value type: \"${type}\" (valid values: ${VALID_VALUE_TYPES.join(\", \")}, all)`,\n );\n }\n }\n\n result.valueTypes = types as ValueType[];\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 --value-types=<types> Value types to detect: boolean, number, string, enum, undefined, all (default: all)\n Multiple types can be specified with comma: --value-types=boolean,string\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 { collectCallSites } from \"@/extraction/callSiteCollector\";\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 valueTypes:\n cliOptions.valueTypes ??\n fileConfig.valueTypes ??\n DEFAULT_OPTIONS.valueTypes,\n };\n\n const { targetDir, minUsages, target, output, tsconfig, valueTypes } =\n 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 callSiteMap = collectCallSites(sourceFilesToAnalyze);\n\n // 各解析結果を収集\n const allExported: AnalysisResult[\"exported\"] = [];\n const allConstants: AnalysisResult[\"constants\"] = [];\n\n if (target === \"all\" || target === \"components\") {\n const propsResult = analyzePropsCore(sourceFilesToAnalyze, {\n minUsages,\n valueTypes,\n callSiteMap,\n });\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 valueTypes,\n callSiteMap,\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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"}
1
+ {"version":3,"file":"cli.mjs","names":["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>>","declarationsToMerge: AnalyzedDeclarations[]","constantParamsToMerge: ConstantParams[]"],"sources":["../src/cli/loadConfig.ts","../src/cli/parseCliOptions.ts","../src/output/printAnalysisResult.ts","../src/source/createFilteredSourceFiles.ts","../src/cli.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { pathToFileURL } from \"node:url\";\nimport {\n VALID_VALUE_TYPES,\n type ValueType,\n} from \"@/extraction/valueTypeDetector\";\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 allowedValueTypes?: ValueType[] | \"all\";\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 if (\"valueTypes\" in config) {\n const value = config.valueTypes;\n\n if (value === \"all\") {\n result.allowedValueTypes = \"all\";\n } else if (Array.isArray(value)) {\n for (const type of value) {\n if (!VALID_VALUE_TYPES.includes(type as ValueType)) {\n throw new Error(\n `Invalid config: valueTypes contains invalid type \"${type}\" (valid values: ${VALID_VALUE_TYPES.join(\", \")}, all)`,\n );\n }\n }\n result.allowedValueTypes = value as ValueType[];\n } else {\n throw new Error(\n `Invalid config: valueTypes must be \"all\" or an array of value types, got ${value}`,\n );\n }\n }\n\n return result;\n}\n","import fs from \"node:fs\";\nimport {\n VALID_VALUE_TYPES,\n type ValueType,\n} from \"@/extraction/valueTypeDetector\";\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 allowedValueTypes?: ValueType[];\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 allowedValueTypes: ValueType[] | \"all\";\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 allowedValueTypes: \"all\",\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 \"--value-types\",\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(\"--value-types=\")) {\n const value = arg.slice(14);\n\n if (value === \"\") {\n throw new CliValidationError(\n \"Invalid value for --value-types: value cannot be empty\",\n );\n }\n\n // \"all\" の場合はデフォルト扱い(RawCliOptionsには含めない)\n if (value === \"all\") {\n continue;\n }\n\n const types = value.split(\",\").map((t) => t.trim());\n for (const type of types) {\n if (!VALID_VALUE_TYPES.includes(type as ValueType)) {\n throw new CliValidationError(\n `Invalid value type: \"${type}\" (valid values: ${VALID_VALUE_TYPES.join(\", \")}, all)`,\n );\n }\n }\n\n result.allowedValueTypes = types as ValueType[];\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 --value-types=<types> Value types to detect: boolean, number, string, enum, undefined, all (default: all)\n Multiple types can be specified with comma: --value-types=boolean,string\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 } from \"@/domain/analysisResult\";\nimport type { AnalyzedDeclarations } from \"@/domain/analyzedDeclarations\";\nimport type { ConstantParams } from \"@/domain/constantParams\";\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 */\nfunction printDeclarations(declarations: AnalyzedDeclarations): void {\n const lines = [\n \"Collecting exported functions...\",\n ` → Found ${declarations.length} function(s)`,\n ...declarations.map(\n (decl) =>\n ` - ${bold(green(decl.name))} (${path.relative(\n process.cwd(),\n decl.sourceFilePath,\n )})`,\n ),\n \"\",\n ];\n console.log(lines.join(\"\\n\"));\n}\n\n/**\n * 常に同じ値が渡されている引数を出力\n */\nfunction printConstantParams(constantParams: ConstantParams): void {\n if (constantParams.isEmpty()) {\n return;\n }\n\n const grouped = constantParams.groupByDeclaration();\n\n for (const constantParamGroup of grouped) {\n const relativePath = path.relative(\n process.cwd(),\n constantParamGroup.declarationSourceFile,\n );\n const usageCount = constantParamGroup.params[0]?.usageCount ?? 0;\n // 使用箇所は全パラメータで同じなので、最初のパラメータから取得\n const usages = constantParamGroup.params[0]?.usages ?? [];\n\n console.log(\n `${bold(green(constantParamGroup.declarationName))} ${relativePath}:${constantParamGroup.declarationLine}`,\n );\n console.log(\"Constant Arguments:\");\n\n for (const param of constantParamGroup.params) {\n console.log(` - ${param.paramName} = ${param.value.outputString()}`);\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.declarations.length;\n const functionsWithConstants =\n result.constantParams.groupByDeclaration().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 printDeclarations(result.declarations);\n }\n\n if (result.constantParams.isEmpty()) {\n console.log(\"No arguments with constant values were found.\");\n } else {\n printConstantParams(result.constantParams);\n }\n\n printStatistics(result);\n}\n","import path from \"node:path\";\nimport { Project, type SourceFile } from \"ts-morph\";\nimport type { FileFilter } from \"@/domain/analyzerOptions\";\nimport { isTestOrStorybookFile } from \"@/source/fileFilters\";\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 type { AnalysisResult } from \"@/domain/analysisResult\";\nimport { AnalyzedDeclarations } from \"@/domain/analyzedDeclarations\";\nimport { ConstantParams } from \"@/domain/constantParams\";\nimport { CallSiteCollector } from \"@/extraction/callSiteCollector\";\nimport { printAnalysisResult } from \"@/output/printAnalysisResult\";\nimport { createFilteredSourceFiles } from \"@/source/createFilteredSourceFiles\";\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 allowedValueTypes:\n cliOptions.allowedValueTypes ??\n fileConfig.allowedValueTypes ??\n DEFAULT_OPTIONS.allowedValueTypes,\n };\n\n const { targetDir, minUsages, target, output, tsconfig, allowedValueTypes } =\n 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 callSiteMap = new CallSiteCollector().collect(sourceFilesToAnalyze);\n\n // 各解析結果を収集\n const declarationsToMerge: AnalyzedDeclarations[] = [];\n const constantParamsToMerge: ConstantParams[] = [];\n\n if (target === \"all\" || target === \"components\") {\n const propsResult = analyzePropsCore(sourceFilesToAnalyze, {\n minUsages,\n allowedValueTypes,\n callSiteMap,\n });\n declarationsToMerge.push(propsResult.declarations);\n constantParamsToMerge.push(propsResult.constantParams);\n }\n\n if (target === \"all\" || target === \"functions\") {\n const functionsResult = analyzeFunctionsCore(sourceFilesToAnalyze, {\n minUsages,\n allowedValueTypes,\n callSiteMap,\n });\n declarationsToMerge.push(functionsResult.declarations);\n constantParamsToMerge.push(functionsResult.constantParams);\n }\n\n const result: AnalysisResult = {\n declarations: AnalyzedDeclarations.merge(...declarationsToMerge),\n constantParams: ConstantParams.merge(...constantParamsToMerge),\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
@@ -1,16 +1,49 @@
1
- import { FunctionDeclaration, MethodDeclaration, Node, SourceFile, VariableDeclaration } from "ts-morph";
1
+ import { FunctionDeclaration, MethodDeclaration, SourceFile, VariableDeclaration } from "ts-morph";
2
2
 
3
- //#region src/utils/valueTypeDetector.d.ts
3
+ //#region src/domain/argValueClasses.d.ts
4
4
  /**
5
- * フィルタリング対象の値種別
5
+ * 引数の値を表す基底抽象クラス
6
6
  */
7
- type ValueType = "boolean" | "number" | "string" | "enum" | "undefined";
8
- //#endregion
9
- //#region src/types.d.ts
7
+ declare abstract class ArgValue {
8
+ /**
9
+ * 生の値を取得する(プレフィックスなし)
10
+ */
11
+ abstract getValue(): string;
12
+ /**
13
+ * 値のプレフィックス(空文字の場合はプレフィックスなし)
14
+ */
15
+ protected abstract readonly prefix: string;
16
+ /**
17
+ * outputString() でプレフィックスを含めるかどうか
18
+ * - true: プレフィックスを含める(関数、パラメータ参照など)
19
+ * - false: 値のみ(リテラル値)
20
+ */
21
+ protected abstract readonly includePrefixInOutput: boolean;
22
+ /**
23
+ * 比較可能な文字列キーに変換する
24
+ * 同じ値かどうかの判定に使用
25
+ */
26
+ toKey(): string;
27
+ /**
28
+ * 出力用の文字列表現を取得する
29
+ */
30
+ outputString(): string;
31
+ }
10
32
  /**
11
- * ファイルパスを受け取り、除外すべきかどうかを判定する関数の型
33
+ * パラメータ参照の値
12
34
  */
13
- type FileFilter = (filePath: string) => boolean;
35
+ declare class ParamRefArgValue extends ArgValue {
36
+ readonly filePath: string;
37
+ readonly functionName: string;
38
+ readonly path: string;
39
+ readonly line: number;
40
+ constructor(filePath: string, functionName: string, path: string, line: number);
41
+ protected readonly prefix = "paramRef";
42
+ protected readonly includePrefixInOutput = true;
43
+ getValue(): string;
44
+ }
45
+ //#endregion
46
+ //#region src/domain/usagesByParam.d.ts
14
47
  /**
15
48
  * 使用箇所の共通interface
16
49
  * 関数呼び出しやJSX要素で、どのパラメータにどの値が渡されたかを記録する
@@ -19,7 +52,7 @@ interface Usage {
19
52
  /** ネストしたプロパティの場合は "param.nested.key" 形式 */
20
53
  name: string;
21
54
  /** リテラル値、enum参照、変数参照などを解決した結果 */
22
- value: string;
55
+ value: ArgValue;
23
56
  usageFilePath: string;
24
57
  usageLine: number;
25
58
  }
@@ -34,65 +67,156 @@ interface Definition {
34
67
  required: boolean;
35
68
  }
36
69
  /**
37
- * エクスポートされた対象の共通interface
70
+ * パラメータ名ごとにUsageをグループ化して管理するクラス
38
71
  */
39
- interface Exported {
72
+ declare class UsagesByParam extends Map<string, Usage[]> {
73
+ /**
74
+ * Usageを追加する
75
+ */
76
+ add(usage: Usage): void;
77
+ /**
78
+ * 複数のUsageを追加する
79
+ */
80
+ addAll(usages: readonly Usage[]): void;
81
+ }
82
+ //#endregion
83
+ //#region src/domain/analyzedDeclarations.d.ts
84
+ /**
85
+ * 分析対象(エクスポートされた関数/コンポーネント)
86
+ */
87
+ interface AnalyzedDeclaration {
40
88
  /** クラスメソッドの場合は "ClassName.methodName" 形式 */
41
89
  name: string;
42
90
  sourceFilePath: string;
43
91
  sourceLine: number;
44
92
  definitions: Definition[];
45
93
  declaration: FunctionDeclaration | VariableDeclaration | MethodDeclaration;
46
- usages: Record<string, Usage[]>;
94
+ usages: UsagesByParam;
95
+ }
96
+ /**
97
+ * 分析対象のコレクション
98
+ */
99
+ declare class AnalyzedDeclarations {
100
+ private readonly items;
101
+ constructor(items?: AnalyzedDeclaration[]);
102
+ /**
103
+ * 複数の AnalyzedDeclarations を結合して新しい AnalyzedDeclarations を作成
104
+ */
105
+ static merge(...declarationsArray: AnalyzedDeclarations[]): AnalyzedDeclarations;
106
+ /**
107
+ * 分析対象が存在しないかどうか
108
+ */
109
+ isEmpty(): boolean;
110
+ /**
111
+ * 分析対象の件数
112
+ */
113
+ get length(): number;
114
+ /**
115
+ * 分析対象を追加
116
+ */
117
+ push(item: AnalyzedDeclaration): void;
118
+ /**
119
+ * 各要素に対して関数を適用し、結果の配列を返す
120
+ */
121
+ map<T>(callback: (item: AnalyzedDeclaration, index: number) => T): T[];
122
+ /**
123
+ * 条件に一致する最初の要素を返す
124
+ */
125
+ find(predicate: (item: AnalyzedDeclaration, index: number) => boolean): AnalyzedDeclaration | undefined;
126
+ /**
127
+ * インデックスで要素を取得
128
+ */
129
+ get(index: number): AnalyzedDeclaration | undefined;
130
+ /**
131
+ * イテレーション用
132
+ */
133
+ [Symbol.iterator](): Iterator<AnalyzedDeclaration>;
47
134
  }
135
+ //#endregion
136
+ //#region src/domain/constantParams.d.ts
48
137
  /**
49
138
  * 常に同じ値が渡されているパラメータ(デフォルト値化の候補)
50
139
  */
51
- interface Constant {
52
- targetName: string;
53
- targetSourceFile: string;
54
- targetLine: number;
140
+ interface ConstantParam {
141
+ declarationName: string;
142
+ declarationSourceFile: string;
143
+ declarationLine: number;
55
144
  paramName: string;
56
- value: string;
145
+ value: ArgValue;
57
146
  usages: Usage[];
58
147
  }
148
+ /**
149
+ * 宣言(関数/コンポーネント)ごとにグループ化された定数パラメータ情報
150
+ */
151
+ interface GroupedConstantParam {
152
+ declarationName: string;
153
+ declarationSourceFile: string;
154
+ declarationLine: number;
155
+ params: Array<{
156
+ paramName: string;
157
+ value: ArgValue;
158
+ usageCount: number;
159
+ usages: Usage[];
160
+ }>;
161
+ }
162
+ /**
163
+ * 定数パラメータのコレクション
164
+ */
165
+ declare class ConstantParams {
166
+ private readonly items;
167
+ constructor(items?: ConstantParam[]);
168
+ /**
169
+ * 複数の ConstantParams を結合して新しい ConstantParams を作成
170
+ */
171
+ static merge(...paramsArray: ConstantParams[]): ConstantParams;
172
+ /**
173
+ * 定数パラメータが存在しないかどうか
174
+ */
175
+ isEmpty(): boolean;
176
+ /**
177
+ * 定数パラメータの件数
178
+ */
179
+ get length(): number;
180
+ /**
181
+ * 定数パラメータを追加
182
+ */
183
+ push(item: ConstantParam): void;
184
+ /**
185
+ * 条件に一致する最初の定数パラメータを返す
186
+ */
187
+ find(predicate: (item: ConstantParam, index: number) => boolean): ConstantParam | undefined;
188
+ /**
189
+ * 宣言(関数/コンポーネント)ごとにグループ化
190
+ */
191
+ groupByDeclaration(): GroupedConstantParam[];
192
+ /**
193
+ * イテレーション用
194
+ */
195
+ [Symbol.iterator](): Iterator<ConstantParam>;
196
+ }
197
+ //#endregion
198
+ //#region src/domain/analysisResult.d.ts
59
199
  /**
60
200
  * 分析結果
61
201
  */
62
202
  interface AnalysisResult {
63
- constants: Constant[];
64
- exported: Exported[];
203
+ constantParams: ConstantParams;
204
+ declarations: AnalyzedDeclarations;
65
205
  }
66
206
  //#endregion
67
- //#region src/extraction/callSiteCollector.d.ts
207
+ //#region src/extraction/valueTypeDetector.d.ts
68
208
  /**
69
- * ArgValueのtype識別子
209
+ * フィルタリング対象の値種別
70
210
  */
71
- declare const ArgValueType: {
72
- readonly Literal: "literal";
73
- readonly Function: "function";
74
- readonly ParamRef: "paramRef";
75
- readonly Undefined: "undefined";
76
- };
211
+ type ValueType = "boolean" | "number" | "string" | "enum" | "undefined";
212
+ //#endregion
213
+ //#region src/domain/analyzerOptions.d.ts
77
214
  /**
78
- * 引数の値を表す union 型
79
- * 文字列エンコーディングの代わりに型安全な表現を使用
215
+ * ファイルパスを受け取り、除外すべきかどうかを判定する関数の型
80
216
  */
81
- type ArgValue = {
82
- type: typeof ArgValueType.Literal;
83
- value: string;
84
- } | {
85
- type: typeof ArgValueType.Function;
86
- filePath: string;
87
- line: number;
88
- } | {
89
- type: typeof ArgValueType.ParamRef;
90
- filePath: string;
91
- functionName: string;
92
- path: string;
93
- } | {
94
- type: typeof ArgValueType.Undefined;
95
- };
217
+ type FileFilter = (filePath: string) => boolean;
218
+ //#endregion
219
+ //#region src/domain/callSiteInfo.d.ts
96
220
  /**
97
221
  * 呼び出し箇所での引数情報
98
222
  */
@@ -107,26 +231,51 @@ interface CallSiteArg {
107
231
  line: number;
108
232
  }
109
233
  /**
110
- * 関数/コンポーネントへの呼び出し情報
234
+ * 関数/コンポーネントへの呼び出し情報を管理するクラス
111
235
  * key: パラメータ名, value: 渡された値の配列
112
236
  */
113
- type CallSiteInfo = Map<string, CallSiteArg[]>;
114
- /**
115
- * すべての関数/コンポーネントの呼び出し情報
116
- * key: "ファイルパス:関数名" 形式の識別子
117
- */
118
- type CallSiteMap = Map<string, CallSiteInfo>;
237
+ declare class CallSiteInfo extends Map<string, CallSiteArg[]> {
238
+ /**
239
+ * 引数情報を追加する
240
+ * 既存の配列がある場合は追加、ない場合は新規作成
241
+ */
242
+ addArg(name: string, arg: CallSiteArg): void;
243
+ }
244
+ //#endregion
245
+ //#region src/domain/callSiteMap.d.ts
119
246
  /**
120
- * ソースファイルからすべての呼び出し情報を収集する
247
+ * すべての関数/コンポーネントの呼び出し情報を管理するクラス
121
248
  */
122
- declare function collectCallSites(sourceFiles: SourceFile[], shouldExcludeFile?: FileFilter): CallSiteMap;
249
+ declare class CallSiteMap extends Map<string, CallSiteInfo> {
250
+ /**
251
+ * 引数情報をCallSiteInfoに追加する
252
+ */
253
+ addArg(targetId: string, arg: CallSiteArg): void;
254
+ /**
255
+ * パラメータ参照を解決してArgValueを返す
256
+ * callSiteMapを使ってパラメータに渡されたすべての値を取得し、
257
+ * すべて同じ値ならその値を返す。解決できない場合は元のParamRefArgValueを返す。
258
+ */
259
+ resolveParamRef(paramRef: ParamRefArgValue): ArgValue;
260
+ /**
261
+ * パラメータ参照を解決する(内部メソッド)
262
+ * callSiteMapを使って、パラメータに渡されたすべての値を取得し、
263
+ * すべて同じ値ならその値を返す。異なる値があればundefinedを返す。
264
+ */
265
+ private resolveParameterValueInternal;
266
+ /**
267
+ * targetIdに対応する呼び出し情報を取得する
268
+ * 存在しない場合は新規作成して登録する
269
+ */
270
+ private getOrCreateInfo;
271
+ }
123
272
  //#endregion
124
273
  //#region src/analyzeFunctions.d.ts
125
274
  interface AnalyzeFunctionsOptions {
126
275
  shouldExcludeFile?: FileFilter;
127
276
  minUsages?: number;
128
277
  /** 検出対象の値種別。デフォルト: "all" */
129
- valueTypes?: ValueType[] | "all";
278
+ allowedValueTypes?: ValueType[] | "all";
130
279
  /** 呼び出し情報(パラメータ経由で渡された値を解決するために使用) */
131
280
  callSiteMap: CallSiteMap;
132
281
  }
@@ -149,7 +298,7 @@ interface AnalyzePropsOptions {
149
298
  shouldExcludeFile?: FileFilter;
150
299
  minUsages?: number;
151
300
  /** 検出対象の値種別。デフォルト: "all" */
152
- valueTypes?: ValueType[] | "all";
301
+ allowedValueTypes?: ValueType[] | "all";
153
302
  /** 呼び出し情報(パラメータ経由で渡された値を解決するために使用) */
154
303
  callSiteMap: CallSiteMap;
155
304
  }
@@ -181,8 +330,52 @@ interface DittoryConfig {
181
330
  output?: OutputMode;
182
331
  tsconfig?: string;
183
332
  targetDir?: string;
184
- valueTypes?: ValueType[] | "all";
333
+ allowedValueTypes?: ValueType[] | "all";
334
+ }
335
+ //#endregion
336
+ //#region src/extraction/callSiteCollector.d.ts
337
+ /**
338
+ * ソースファイルから呼び出し情報を収集する
339
+ */
340
+ declare class CallSiteCollector {
341
+ private shouldExcludeFile;
342
+ private callSiteMap;
343
+ constructor(shouldExcludeFile?: FileFilter);
344
+ /**
345
+ * ソースファイルからすべての呼び出し情報を収集する
346
+ */
347
+ collect(sourceFiles: SourceFile[]): CallSiteMap;
348
+ /**
349
+ * JSX要素から呼び出し情報を抽出して登録する
350
+ * タグ名がIdentifierでない場合や、宣言が解決できない場合は何もしない
351
+ */
352
+ private extractFromJsxElement;
353
+ /**
354
+ * 関数呼び出しから呼び出し情報を抽出して登録する
355
+ * 呼び出し式がIdentifierでない場合や、宣言が解決できない場合は何もしない
356
+ */
357
+ private extractFromCallExpression;
358
+ /**
359
+ * 宣言からパラメータ名と引数ノードのペア配列を取得する
360
+ * 関数宣言、アロー関数、関数式に対応
361
+ * 引数が省略されている場合、node は undefined になる
362
+ */
363
+ private getArgsWithName;
364
+ /**
365
+ * JSX要素から属性名とノードのペア配列を取得する
366
+ */
367
+ private getAttributesWithName;
368
+ /**
369
+ * 宣言ノードと名前からtargetIdを生成する
370
+ * 形式: "{ファイルパス}:{名前}"
371
+ */
372
+ private createTargetId;
373
+ /**
374
+ * 識別子から定義元の宣言ノードを取得する
375
+ * インポートを通じて実際の定義を解決し、宣言ノードを返す
376
+ */
377
+ private getDeclaration;
185
378
  }
186
379
  //#endregion
187
- export { type AnalysisResult, type AnalyzeMode, type CallSiteMap, type Constant, type Definition, type DittoryConfig, type Exported, type FileFilter, type OutputMode, type Usage, analyzeFunctionsCore, analyzePropsCore, collectCallSites };
380
+ export { type AnalysisResult, type AnalyzeMode, type AnalyzedDeclaration, CallSiteCollector, CallSiteMap, type ConstantParam, type Definition, type DittoryConfig, type FileFilter, type OutputMode, type Usage, analyzeFunctionsCore, analyzePropsCore };
188
381
  //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs CHANGED
@@ -1,3 +1,3 @@
1
- import { i as collectCallSites, n as analyzeFunctionsCore, t as analyzePropsCore } from "./analyzeProps-6k4QTasK.mjs";
1
+ import { i as analyzeFunctionsCore, n as CallSiteMap, r as analyzePropsCore, t as CallSiteCollector } from "./callSiteCollector-CGFPm0in.mjs";
2
2
 
3
- export { analyzeFunctionsCore, analyzePropsCore, collectCallSites };
3
+ export { CallSiteCollector, CallSiteMap, analyzeFunctionsCore, analyzePropsCore };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dittory",
3
- "version": "0.0.4",
4
- "description": "A static analysis CLI that detects parameters always receiving the same value in React components and functions",
3
+ "version": "0.0.5",
4
+ "description": "A static analysis CLI that detects parameters that always receive 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",
@@ -38,6 +38,7 @@
38
38
  "@biomejs/biome": "2.3.10",
39
39
  "@types/node": "^25.0.3",
40
40
  "@types/react": "^19.2.7",
41
+ "knip": "^5.80.2",
41
42
  "react": "^19.2.3",
42
43
  "tsdown": "^0.18.4",
43
44
  "typescript": "^5.9.3",
@@ -51,6 +52,7 @@
51
52
  "dev": "tsdown && node ./dist/cli.mjs",
52
53
  "lint": "biome check --write src",
53
54
  "test": "vitest --run",
54
- "typecheck": "tsc --noEmit"
55
+ "typecheck": "tsc --noEmit",
56
+ "knip": "knip --production"
55
57
  }
56
58
  }