dirlens 1.0.10 → 1.0.11
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 +56 -3
- package/dirlens.py +755 -40
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -133,19 +133,46 @@ Desktop/ (2 dirs, 2 files, 3.74 MB)
|
|
|
133
133
|
- **JSON出力** — `--json` / `-J` で機械可読な構造データを出力
|
|
134
134
|
- **HTMLレポート** — `--html` でブラウザで閲覧できる折りたたみツリーを生成
|
|
135
135
|
- **クリップボードコピー** — `-C` で出力を自動コピー(ANSIコードを自動除去)
|
|
136
|
-
- **AI
|
|
136
|
+
- **AIチャット貼り付けモード** — `--ai` 一発で gitignore除外・日時・Markdown・クリップボードコピーを全適用(人間がコピペする用)
|
|
137
|
+
- **エージェント解析モード** — `--agent` 一発で下記のAI/エージェント向け解析機能を全適用(クリップボードは使わない、自律実行しても安全)
|
|
137
138
|
- **隠しファイル対応** — `-a` で表示切り替え
|
|
138
139
|
- **サイズ順ソート** — `-s` で大きいものから表示
|
|
139
140
|
|
|
141
|
+
### AI/エージェント向け解析機能(`--agent` でまとめて有効化)
|
|
142
|
+
|
|
143
|
+
これらはAIチャットやコーディングエージェントがプロジェクト構造を理解する際に、ファイルの中身を逐一読まなくても済むよう設計された機能です。個別フラグでも使えます。
|
|
144
|
+
|
|
145
|
+
- **トークン数概算** — `-T` でファイルごとの推定トークン数を表示(英数字記号は約4文字/トークン、日本語等は約1.5文字/トークンとして概算)
|
|
146
|
+
- **git連携** — `-H` で各ファイルの最終コミット情報(メッセージ・相対日時)を表示。直近2000コミットまで走査
|
|
147
|
+
- **TODO/FIXME抽出** — `-K` で `TODO`/`FIXME`/`HACK`/`XXX` コメントを抽出し、行番号付きで一覧表示
|
|
148
|
+
- **テスト欠落検知** — `-V` で対応するテストファイルが見つからないソースファイルをマーク(命名規則ベースのヒューリスティック)
|
|
149
|
+
- **エントリーポイント検出** — `-N` で `main.py`・`index.js`・`package.json` の `main`/`bin` フィールドなどから入口ファイルを推測してマーク
|
|
150
|
+
- **シンボルアウトライン** — `-O` で関数・クラス名を簡易抽出(Python/JS/TS/Go/Rust対応、正規表現ベースのbest-effort)
|
|
151
|
+
- **import/依存グラフ** — `-M` でファイル間のローカルなimport関係を解析し、`imports×N`(依存先数)・`used-by×N`(被参照数)を表示。Pythonは標準ライブラリの`ast`で正確に解析、他言語は正規表現+パス解決
|
|
152
|
+
|
|
140
153
|
---
|
|
141
154
|
|
|
142
155
|
## 使い方
|
|
143
156
|
|
|
144
157
|
```bash
|
|
145
|
-
# ── AI
|
|
158
|
+
# ── AI チャットへの貼り付け(人間がコピペする用)──────────────
|
|
146
159
|
dirlens --ai # gitignore除外 + 日時 + Markdown + クリップボードコピー
|
|
147
160
|
dirlens --ai -L 3 # 深さ指定と組み合わせ可
|
|
148
161
|
|
|
162
|
+
# ── エージェント向け解析(自律実行向け・クリップボードは使わない)──
|
|
163
|
+
dirlens --agent # トークン数・git情報・TODO・テスト欠落・
|
|
164
|
+
# エントリーポイント・アウトライン・import依存グラフを一括表示
|
|
165
|
+
dirlens --agent --json # 同上をJSON形式で(スクリプト/エージェント連携向け)
|
|
166
|
+
|
|
167
|
+
# ── AI/エージェント向け解析(個別フラグ)────────────────────────
|
|
168
|
+
dirlens -T # ファイルごとの推定トークン数
|
|
169
|
+
dirlens -H # 最終コミット情報(要git)
|
|
170
|
+
dirlens -K # TODO/FIXME/HACK/XXXを抽出
|
|
171
|
+
dirlens -V # テストが無いソースファイルを表示
|
|
172
|
+
dirlens -N # エントリーポイントらしきファイルをマーク
|
|
173
|
+
dirlens -O # 関数・クラスの簡易アウトライン
|
|
174
|
+
dirlens -M # ローカルなimport/依存関係を解析
|
|
175
|
+
|
|
149
176
|
# ── 表示制御 ──────────────────────────────────────────────────
|
|
150
177
|
dirlens # カレントディレクトリ
|
|
151
178
|
dirlens ~/Desktop # 指定ディレクトリ
|
|
@@ -199,7 +226,8 @@ dirlens --no-color > dirlens.txt # テキストファイルに書き出す
|
|
|
199
226
|
| オプション | 省略形 | 説明 |
|
|
200
227
|
|---------------------|--------------|-------------------------------------------------------------|
|
|
201
228
|
| `path` | — | 対象ディレクトリ(省略時はカレント) |
|
|
202
|
-
| **`--ai`** | — | **`-G --date -m -C`
|
|
229
|
+
| **`--ai`** | — | **`-G --date -m -C` のショートカット。人間がAIチャットに貼り付ける用** |
|
|
230
|
+
| **`--agent`** | — | **`-G --date -T -H -K -V -N -O -M` のショートカット。エージェント向け解析(クリップボードは使わない)** |
|
|
203
231
|
| `--depth N` | `-L N` | 表示する最大の深さ |
|
|
204
232
|
| `--all` | `-a` | 隠しファイル・ディレクトリも表示 |
|
|
205
233
|
| `-d` | — | ディレクトリのみ表示 |
|
|
@@ -224,6 +252,13 @@ dirlens --no-color > dirlens.txt # テキストファイルに書き出す
|
|
|
224
252
|
| `--max-size SIZE` | — | 指定サイズ以下のファイルのみ表示 |
|
|
225
253
|
| `--bar` | — | ディスク占有率バーを表示 |
|
|
226
254
|
| `--emoji` | — | 拡張子に応じた絵文字アイコンを表示 |
|
|
255
|
+
| `--tokens` | `-T` | ファイルごとの推定トークン数を表示(概算) |
|
|
256
|
+
| `--git` | `-H` | 最終コミット情報を表示(要git、直近2000コミットまで走査) |
|
|
257
|
+
| `--todo` | `-K` | TODO/FIXME/HACK/XXXコメントを抽出 |
|
|
258
|
+
| `--missing-tests` | `-V` | 対応するテストファイルが見つからないソースファイルを表示 |
|
|
259
|
+
| `--entry` | `-N` | エントリーポイントらしきファイルを検出してマーク |
|
|
260
|
+
| `--outline` | `-O` | 関数・クラスの簡易アウトラインを表示(正規表現ベース・対応言語限定)|
|
|
261
|
+
| `--imports` | `-M` | ローカルなimport/依存関係を解析して表示(外部パッケージは対象外) |
|
|
227
262
|
| `--markdown` | `-m` | Markdown コードブロック形式で出力(カラー自動無効) |
|
|
228
263
|
| `--json` | `-J` | JSON 形式で標準出力に出力 |
|
|
229
264
|
| `--html [FILE]` | — | HTML レポートを生成(デフォルト: `dirlens.html`) |
|
|
@@ -255,3 +290,21 @@ dirlens --no-color > dirlens.txt # テキストファイルに書き出す
|
|
|
255
290
|
- **ホームフォルダ(`~/`)やルート(`/`)で実行すると固まる場合があります** — サイズ計算は表示深さに関わらず底まで全再帰するため、`~/Library` や iCloud Drive など大容量ディレクトリで時間がかかります
|
|
256
291
|
- **`-G` の否定パターン(`!`)** は対応していますが、ディレクトリを除外した後にその中のファイルを `!` で復活させることは非対応です(`*.log` + `!important.log` のようなファイル単位の否定は動作します)
|
|
257
292
|
- `-p`(パーミッション)・`-u`(ユーザー名)・`-g`(グループ名)は macOS / Linux のみ対応(Windows では ID 番号を表示)
|
|
293
|
+
|
|
294
|
+
### AI/エージェント向け解析機能の精度について
|
|
295
|
+
|
|
296
|
+
dirlens は外部ライブラリに依存しない方針で作られているため、以下の機能は**完全な精度を保証しません**。重要な判断の根拠にする場合は、該当ファイルの中身を直接確認することを推奨します。
|
|
297
|
+
|
|
298
|
+
| 機能 | 制限事項 |
|
|
299
|
+
|---|---|
|
|
300
|
+
| トークン数概算(`-T`) | 文字数ベースの大まかな目安。実際のトークナイザーとは一致しない |
|
|
301
|
+
| シンボルアウトライン(`-O`) | 正規表現ベースの簡易抽出(ASTではない)。デコレータが複雑な場合や複数行にまたがる関数シグネチャは取得漏れすることがある。対応言語は Python・JS/TS・Go・Rust のみ |
|
|
302
|
+
| import/依存グラフ(`-M`) | Pythonは標準ライブラリの `ast` で正確に解析されるため信頼度が高い。JS/TS/Go/Rustは正規表現+パス解決のため、JS/TSのbare import(`react`等)・Rustの`self::`/`super::`・外部crate・モジュール外のGoパッケージは解決されず「external」扱いになる |
|
|
303
|
+
| テスト欠落検知(`-V`) | ファイル命名規則のみで判定(`test_foo.py`等)。実際のテストカバレッジは見ていない。Rustは対象外(インラインテストの慣習のため検出不可) |
|
|
304
|
+
| エントリーポイント検出(`-N`) | 既知のファイル名パターンと `package.json` の `main`/`bin` フィールドのみで判定 |
|
|
305
|
+
| TODO/FIXME抽出(`-K`) | 単純な文字列マッチ。コメント外の文字列内に偶然該当語があっても拾われる場合がある |
|
|
306
|
+
| git連携(`-H`) | 直近2000コミットのみ走査。それより古い変更しかないファイルは情報が出ない |
|
|
307
|
+
|
|
308
|
+
### AIエージェントへの指示テンプレート
|
|
309
|
+
|
|
310
|
+
エージェント(Claude Code・Cursor等)にプロジェクト探索の手順として `dirlens --agent` を使わせたい場合、`AGENT_RULE.md` のテンプレートを `CLAUDE.md`・`.cursorrules` 等のグローバルルールファイルにそのまま貼り付けて使えます。
|
package/dirlens.py
CHANGED
|
@@ -4,7 +4,7 @@ dirlens – ファイルサイズ付きディレクトリツリー表示ツー
|
|
|
4
4
|
対応環境: macOS / Linux / Windows (Python 3.8+)
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
import io, json, os, sys, stat as _stat, argparse, fnmatch, datetime, subprocess
|
|
7
|
+
import io, json, os, sys, stat as _stat, argparse, fnmatch, datetime, subprocess, re, ast
|
|
8
8
|
from concurrent.futures import ThreadPoolExecutor
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
|
|
@@ -26,7 +26,7 @@ def _enable_color():
|
|
|
26
26
|
USE_COLOR = _enable_color()
|
|
27
27
|
RESET = "\033[0m"; BOLD = "\033[1m"; DIM = "\033[2m"
|
|
28
28
|
BLUE = "\033[34m"; CYAN = "\033[36m"; GREEN = "\033[32m"; MAGENTA = "\033[35m"
|
|
29
|
-
RED = "\033[31m"
|
|
29
|
+
RED = "\033[31m"; YELLOW = "\033[33m"
|
|
30
30
|
|
|
31
31
|
def c(text, *codes):
|
|
32
32
|
return ("".join(codes) + text + RESET) if USE_COLOR else text
|
|
@@ -43,7 +43,6 @@ def fmt_size(n, partial=False):
|
|
|
43
43
|
|
|
44
44
|
def fmt_count(nd, nf, denied=False):
|
|
45
45
|
sfx = "+" if denied else ""
|
|
46
|
-
# denied=True のとき数が不明なので複数形固定
|
|
47
46
|
d_word = "dir" if (nd == 1 and not denied) else "dirs"
|
|
48
47
|
f_word = "file" if (nf == 1 and not denied) else "files"
|
|
49
48
|
return f"{nd}{sfx} {d_word}, {nf}{sfx} {f_word}"
|
|
@@ -120,9 +119,8 @@ def copy_to_clipboard(text):
|
|
|
120
119
|
return False
|
|
121
120
|
except Exception: return False
|
|
122
121
|
|
|
123
|
-
import re as _re
|
|
124
122
|
def strip_ansi(text):
|
|
125
|
-
return
|
|
123
|
+
return re.sub(r'\033\[[0-9;]*[mK]', '', text)
|
|
126
124
|
|
|
127
125
|
|
|
128
126
|
# ─── 絵文字 ───────────────────────────────────────────────────
|
|
@@ -247,6 +245,537 @@ def _prefetch_sizes(root_path):
|
|
|
247
245
|
list(ex.map(dir_size, top))
|
|
248
246
|
|
|
249
247
|
|
|
248
|
+
# ════════════════════════════════════════════════════════════
|
|
249
|
+
# AI/エージェント向け解析機能
|
|
250
|
+
# ════════════════════════════════════════════════════════════
|
|
251
|
+
|
|
252
|
+
_BINARY_EXTS = {
|
|
253
|
+
".png",".jpg",".jpeg",".gif",".bmp",".ico",".webp",
|
|
254
|
+
".mp3",".mp4",".mov",".avi",".wav",".flac",".ogg",".webm",".mkv",
|
|
255
|
+
".zip",".tar",".gz",".rar",".7z",".bz2",".xz",
|
|
256
|
+
".pdf",".doc",".docx",".xls",".xlsx",".ppt",".pptx",
|
|
257
|
+
".exe",".dll",".so",".dylib",".bin",".o",".a",".class",".jar",
|
|
258
|
+
".woff",".woff2",".ttf",".otf",".eot",
|
|
259
|
+
".db",".sqlite",".sqlite3",".pyc",".pyo",".whl",
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
def _is_probably_binary(name):
|
|
263
|
+
return os.path.splitext(name)[1].lower() in _BINARY_EXTS
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
# --- トークン数概算(-T / --tokens) -------------------------
|
|
267
|
+
def estimate_tokens(path, actual_size=None):
|
|
268
|
+
"""テキストファイルのトークン数を概算する。バイナリ等は None。
|
|
269
|
+
あくまで大まかな目安(英数字記号は約4文字/トークン、それ以外(日本語等)は約1.5文字/トークンとして概算)。
|
|
270
|
+
"""
|
|
271
|
+
SAMPLE_LIMIT = 2_000_000
|
|
272
|
+
try:
|
|
273
|
+
with open(path, "rb") as f:
|
|
274
|
+
data = f.read(SAMPLE_LIMIT + 1)
|
|
275
|
+
except OSError:
|
|
276
|
+
return None
|
|
277
|
+
truncated = len(data) > SAMPLE_LIMIT
|
|
278
|
+
if truncated:
|
|
279
|
+
data = data[:SAMPLE_LIMIT]
|
|
280
|
+
if b"\x00" in data[:8192]:
|
|
281
|
+
return None # バイナリ判定
|
|
282
|
+
text = data.decode("utf-8", errors="ignore")
|
|
283
|
+
if not text:
|
|
284
|
+
return 0
|
|
285
|
+
ascii_chars = sum(1 for ch in text if ord(ch) < 128)
|
|
286
|
+
other_chars = len(text) - ascii_chars
|
|
287
|
+
tokens = ascii_chars / 4 + other_chars / 1.5
|
|
288
|
+
if truncated and actual_size and len(data) > 0:
|
|
289
|
+
tokens *= actual_size / len(data)
|
|
290
|
+
return max(1, round(tokens))
|
|
291
|
+
|
|
292
|
+
def fmt_tokens(n):
|
|
293
|
+
if n is None: return None
|
|
294
|
+
if n >= 1000:
|
|
295
|
+
s = f"{n/1000:.1f}".rstrip("0").rstrip(".")
|
|
296
|
+
return f"~{s}K tok"
|
|
297
|
+
return f"~{n} tok"
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
# --- git連携(-H / --git) ------------------------------------
|
|
301
|
+
def load_git_log(root, max_commits=2000):
|
|
302
|
+
"""直近コミット履歴から各ファイルの最終更新コミット情報を取得する。
|
|
303
|
+
gitが無い/リポジトリでない場合は空dictを返す(エラーにはしない)。
|
|
304
|
+
パフォーマンスのため履歴は直近 max_commits 件までに限定(古いファイルは情報なしになる場合あり)。
|
|
305
|
+
"""
|
|
306
|
+
try:
|
|
307
|
+
proc = subprocess.run(
|
|
308
|
+
["git", "-C", root, "log", "-n", str(max_commits),
|
|
309
|
+
"--name-only", "--date=relative",
|
|
310
|
+
"--pretty=format:\x01%H\x02%ad\x02%an\x02%s\x03"],
|
|
311
|
+
capture_output=True, text=True, timeout=8, check=True,
|
|
312
|
+
)
|
|
313
|
+
except (subprocess.CalledProcessError, FileNotFoundError,
|
|
314
|
+
subprocess.TimeoutExpired, OSError):
|
|
315
|
+
return {}
|
|
316
|
+
|
|
317
|
+
file_map = {}
|
|
318
|
+
current = None
|
|
319
|
+
for raw in proc.stdout.split("\n"):
|
|
320
|
+
line = raw.strip("\r")
|
|
321
|
+
if line.startswith("\x01"):
|
|
322
|
+
body = line[1:]
|
|
323
|
+
if body.endswith("\x03"):
|
|
324
|
+
body = body[:-1]
|
|
325
|
+
parts = body.split("\x02", 3)
|
|
326
|
+
current = ({"hash": parts[0][:7], "date": parts[1],
|
|
327
|
+
"author": parts[2], "subject": parts[3]}
|
|
328
|
+
if len(parts) == 4 else None)
|
|
329
|
+
elif line.strip() and current is not None:
|
|
330
|
+
fp = line.strip().replace("\\", "/")
|
|
331
|
+
if fp not in file_map:
|
|
332
|
+
file_map[fp] = current
|
|
333
|
+
return file_map
|
|
334
|
+
|
|
335
|
+
def fmt_git(g):
|
|
336
|
+
if not g: return None
|
|
337
|
+
subj = g["subject"].strip()
|
|
338
|
+
if len(subj) > 30:
|
|
339
|
+
subj = subj[:30] + "…"
|
|
340
|
+
return f'"{subj}" ({g["date"]})'
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
# --- TODO/FIXME抽出(-K / --todo) -----------------------------
|
|
344
|
+
_TODO_RE = re.compile(r'\b(TODO|FIXME|HACK|XXX)\b[:\s]?(.*)', re.IGNORECASE)
|
|
345
|
+
|
|
346
|
+
def scan_todos(path, limit_bytes=2_000_000):
|
|
347
|
+
if _is_probably_binary(path):
|
|
348
|
+
return []
|
|
349
|
+
try:
|
|
350
|
+
with open(path, "rb") as f:
|
|
351
|
+
data = f.read(limit_bytes)
|
|
352
|
+
except OSError:
|
|
353
|
+
return []
|
|
354
|
+
if b"\x00" in data[:8192]:
|
|
355
|
+
return []
|
|
356
|
+
text = data.decode("utf-8", errors="ignore")
|
|
357
|
+
results = []
|
|
358
|
+
for i, line in enumerate(text.split("\n"), 1):
|
|
359
|
+
m = _TODO_RE.search(line)
|
|
360
|
+
if m:
|
|
361
|
+
snippet = line.strip()
|
|
362
|
+
if len(snippet) > 80:
|
|
363
|
+
snippet = snippet[:80] + "…"
|
|
364
|
+
results.append((i, m.group(1).upper(), snippet))
|
|
365
|
+
return results
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
# --- テスト欠落検知(-V / --missing-tests) ---------------------
|
|
369
|
+
_SOURCE_EXTS_FOR_TESTS = {".py", ".js", ".jsx", ".ts", ".tsx", ".go"}
|
|
370
|
+
|
|
371
|
+
def _is_test_file(name):
|
|
372
|
+
lower = name.lower()
|
|
373
|
+
stem, ext = os.path.splitext(lower)
|
|
374
|
+
if stem.startswith("test_") or stem.endswith("_test"):
|
|
375
|
+
return True
|
|
376
|
+
if stem.endswith(".test") or stem.endswith(".spec"):
|
|
377
|
+
return True
|
|
378
|
+
return False
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
# --- import/依存グラフ解析(-M / --imports) ---------------------
|
|
382
|
+
# Pythonは標準ライブラリの ast モジュールで正確に解析。
|
|
383
|
+
# JS/TS/Go/Rustは正規表現ベースの抽出+相対パス解決(best-effort)。
|
|
384
|
+
# 外部パッケージ(react, lodash, requests 等)はプロジェクト内ファイルに
|
|
385
|
+
# 解決できないため "external" 扱いとし、依存グラフには含めない。
|
|
386
|
+
|
|
387
|
+
def extract_imports_py(path):
|
|
388
|
+
"""ASTでPythonのimport文を正確に抽出する。
|
|
389
|
+
戻り値: [(module_str, level, [imported_names])] level>0 は相対import。
|
|
390
|
+
"""
|
|
391
|
+
try:
|
|
392
|
+
with open(path, encoding="utf-8", errors="ignore") as f:
|
|
393
|
+
source = f.read()
|
|
394
|
+
tree = ast.parse(source, filename=path)
|
|
395
|
+
except (OSError, SyntaxError, ValueError, RecursionError):
|
|
396
|
+
return []
|
|
397
|
+
out = []
|
|
398
|
+
for node in ast.walk(tree):
|
|
399
|
+
if isinstance(node, ast.Import):
|
|
400
|
+
for alias in node.names:
|
|
401
|
+
out.append((alias.name, 0, None))
|
|
402
|
+
elif isinstance(node, ast.ImportFrom):
|
|
403
|
+
out.append((node.module or "", node.level, [a.name for a in node.names]))
|
|
404
|
+
return out
|
|
405
|
+
|
|
406
|
+
_JS_IMPORT_PATTERNS = [
|
|
407
|
+
re.compile(r'''import\s+(?:[\w*\s{},]+\s+from\s+)?['"]([^'"]+)['"]'''),
|
|
408
|
+
re.compile(r'''export\s+(?:[\w*\s{},]+\s+from\s+)?['"]([^'"]+)['"]'''),
|
|
409
|
+
re.compile(r'''require\(\s*['"]([^'"]+)['"]\s*\)'''),
|
|
410
|
+
re.compile(r'''import\(\s*['"]([^'"]+)['"]\s*\)'''), # 動的import()
|
|
411
|
+
]
|
|
412
|
+
|
|
413
|
+
def extract_imports_js(path):
|
|
414
|
+
try:
|
|
415
|
+
with open(path, encoding="utf-8", errors="ignore") as f:
|
|
416
|
+
text = f.read()
|
|
417
|
+
except OSError:
|
|
418
|
+
return []
|
|
419
|
+
found = []
|
|
420
|
+
for pat in _JS_IMPORT_PATTERNS:
|
|
421
|
+
found.extend(pat.findall(text))
|
|
422
|
+
return found
|
|
423
|
+
|
|
424
|
+
_GO_IMPORT_BLOCK_RE = re.compile(r'import\s*\(([^)]*)\)', re.DOTALL)
|
|
425
|
+
_GO_IMPORT_LINE_RE = re.compile(r'import\s+"([^"]+)"')
|
|
426
|
+
_GO_IMPORT_ITEM_RE = re.compile(r'"([^"]+)"')
|
|
427
|
+
|
|
428
|
+
def extract_imports_go(path):
|
|
429
|
+
try:
|
|
430
|
+
with open(path, encoding="utf-8", errors="ignore") as f:
|
|
431
|
+
text = f.read()
|
|
432
|
+
except OSError:
|
|
433
|
+
return []
|
|
434
|
+
found = []
|
|
435
|
+
block = _GO_IMPORT_BLOCK_RE.search(text)
|
|
436
|
+
if block:
|
|
437
|
+
found.extend(_GO_IMPORT_ITEM_RE.findall(block.group(1)))
|
|
438
|
+
found.extend(_GO_IMPORT_LINE_RE.findall(text))
|
|
439
|
+
return found
|
|
440
|
+
|
|
441
|
+
_RS_USE_RE = re.compile(r'^\s*(?:pub\s+)?use\s+([\w:]+)', re.MULTILINE)
|
|
442
|
+
_RS_MOD_RE = re.compile(r'^\s*(?:pub\s+)?mod\s+(\w+)\s*;', re.MULTILINE)
|
|
443
|
+
|
|
444
|
+
def extract_imports_rs(path):
|
|
445
|
+
"""戻り値: (use文のパスリスト, mod宣言のモジュール名リスト)"""
|
|
446
|
+
try:
|
|
447
|
+
with open(path, encoding="utf-8", errors="ignore") as f:
|
|
448
|
+
text = f.read()
|
|
449
|
+
except OSError:
|
|
450
|
+
return [], []
|
|
451
|
+
return _RS_USE_RE.findall(text), _RS_MOD_RE.findall(text)
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def _py_module_key(relpath):
|
|
455
|
+
"""'pkg/sub/mod.py' -> 'pkg.sub.mod'、'pkg/sub/__init__.py' -> 'pkg.sub'"""
|
|
456
|
+
parts = relpath.replace("\\", "/").split("/")
|
|
457
|
+
if parts[-1] == "__init__.py":
|
|
458
|
+
parts = parts[:-1]
|
|
459
|
+
elif parts[-1].endswith(".py"):
|
|
460
|
+
parts[-1] = parts[-1][:-3]
|
|
461
|
+
return ".".join(parts)
|
|
462
|
+
|
|
463
|
+
def _resolve_relative_path(base_dir, target, project_files):
|
|
464
|
+
"""JS/TS の相対import('./foo'、'../bar')をプロジェクト内ファイルに解決する。"""
|
|
465
|
+
candidate = os.path.normpath(os.path.join(base_dir, target)).replace("\\", "/")
|
|
466
|
+
for suffix in ("", ".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs",
|
|
467
|
+
"/index.js", "/index.ts", "/index.jsx", "/index.tsx"):
|
|
468
|
+
cand = candidate + suffix
|
|
469
|
+
if cand in project_files:
|
|
470
|
+
return cand
|
|
471
|
+
return None
|
|
472
|
+
|
|
473
|
+
def _resolve_rust_crate_path(use_path, project_files):
|
|
474
|
+
"""'crate::foo::bar::Baz' をプロジェクト内ファイルに解決する(src/foo/bar.rs 等)。
|
|
475
|
+
self:: / super:: および外部crateは簡易実装のため非対応(external扱い)。
|
|
476
|
+
"""
|
|
477
|
+
if not use_path.startswith("crate::"):
|
|
478
|
+
return None
|
|
479
|
+
body = use_path.split("::", 1)[1]
|
|
480
|
+
segments = [s for s in body.split("::") if s and s not in ("self", "*")]
|
|
481
|
+
for cut in (len(segments), len(segments) - 1):
|
|
482
|
+
if cut <= 0: continue
|
|
483
|
+
path_part = "/".join(segments[:cut])
|
|
484
|
+
for cand in (f"src/{path_part}.rs", f"src/{path_part}/mod.rs"):
|
|
485
|
+
if cand in project_files:
|
|
486
|
+
return cand
|
|
487
|
+
return None
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
# --- エントリーポイント検出(-N / --entry) ----------------------
|
|
491
|
+
_ENTRY_NAMES_LOWER = {
|
|
492
|
+
"main.py", "__main__.py", "app.py", "server.py", "manage.py", "wsgi.py", "asgi.py",
|
|
493
|
+
"index.js", "index.ts", "index.mjs", "index.cjs",
|
|
494
|
+
"main.js", "main.ts", "server.js", "server.ts", "app.js", "app.ts",
|
|
495
|
+
"main.go", "main.rs",
|
|
496
|
+
"makefile", "dockerfile", "docker-compose.yml", "docker-compose.yaml",
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def build_project_index(root, cfg):
|
|
501
|
+
"""テスト欠落検知・エントリーポイント検出・import依存グラフのため、
|
|
502
|
+
プロジェクト全体を一度だけスキャンする。
|
|
503
|
+
.gitignore は -G 指定時のみ尊重する。-G なしで巨大な node_modules 等があると遅くなる場合がある。
|
|
504
|
+
戻り値: (untested_relpaths, entry_relpaths, imports_map, imported_by_map, external_map)
|
|
505
|
+
"""
|
|
506
|
+
all_names = set()
|
|
507
|
+
all_relpaths = set()
|
|
508
|
+
source_files = []
|
|
509
|
+
entry_set = set()
|
|
510
|
+
py_module_map = {}
|
|
511
|
+
go_module_name = [None] # nonlocalの代わりにリストで包む
|
|
512
|
+
|
|
513
|
+
def walk(path, active_pats):
|
|
514
|
+
pats = _extend_pats(active_pats, path, cfg) if cfg.use_gitignore else active_pats
|
|
515
|
+
try:
|
|
516
|
+
entries = list(os.scandir(path))
|
|
517
|
+
except OSError:
|
|
518
|
+
return
|
|
519
|
+
entries = [e for e in entries if cfg.show_all or not e.name.startswith(".")]
|
|
520
|
+
if pats:
|
|
521
|
+
entries = [e for e in entries
|
|
522
|
+
if not is_ignored(e.name, os.path.relpath(e.path, root),
|
|
523
|
+
e.is_dir(follow_symlinks=False), pats)]
|
|
524
|
+
for e in entries:
|
|
525
|
+
if e.is_dir(follow_symlinks=False):
|
|
526
|
+
walk(e.path, pats)
|
|
527
|
+
continue
|
|
528
|
+
relpath = os.path.relpath(e.path, root).replace("\\", "/")
|
|
529
|
+
stem, ext = os.path.splitext(e.name)
|
|
530
|
+
all_names.add(e.name.lower())
|
|
531
|
+
all_relpaths.add(relpath)
|
|
532
|
+
if ext.lower() in _SOURCE_EXTS_FOR_TESTS and not _is_test_file(e.name):
|
|
533
|
+
source_files.append((relpath, stem, ext.lower()))
|
|
534
|
+
if e.name.lower() in _ENTRY_NAMES_LOWER:
|
|
535
|
+
entry_set.add(relpath)
|
|
536
|
+
if ext.lower() == ".py":
|
|
537
|
+
py_module_map[_py_module_key(relpath)] = relpath
|
|
538
|
+
if e.name == "go.mod":
|
|
539
|
+
try:
|
|
540
|
+
with open(e.path, encoding="utf-8", errors="ignore") as f:
|
|
541
|
+
for line in f:
|
|
542
|
+
line = line.strip()
|
|
543
|
+
if line.startswith("module "):
|
|
544
|
+
go_module_name[0] = line.split(None, 1)[1].strip()
|
|
545
|
+
break
|
|
546
|
+
except OSError:
|
|
547
|
+
pass
|
|
548
|
+
if e.name == "package.json":
|
|
549
|
+
try:
|
|
550
|
+
with open(e.path, encoding="utf-8") as f:
|
|
551
|
+
pkg = json.load(f)
|
|
552
|
+
base_dir = os.path.dirname(relpath)
|
|
553
|
+
main_field = pkg.get("main")
|
|
554
|
+
if isinstance(main_field, str):
|
|
555
|
+
entry_set.add(os.path.normpath(
|
|
556
|
+
os.path.join(base_dir, main_field)).replace("\\", "/"))
|
|
557
|
+
bin_field = pkg.get("bin")
|
|
558
|
+
if isinstance(bin_field, str):
|
|
559
|
+
entry_set.add(os.path.normpath(
|
|
560
|
+
os.path.join(base_dir, bin_field)).replace("\\", "/"))
|
|
561
|
+
elif isinstance(bin_field, dict):
|
|
562
|
+
for v in bin_field.values():
|
|
563
|
+
if isinstance(v, str):
|
|
564
|
+
entry_set.add(os.path.normpath(
|
|
565
|
+
os.path.join(base_dir, v)).replace("\\", "/"))
|
|
566
|
+
except (OSError, json.JSONDecodeError, AttributeError, ValueError):
|
|
567
|
+
pass
|
|
568
|
+
|
|
569
|
+
walk(root, [])
|
|
570
|
+
|
|
571
|
+
untested = set()
|
|
572
|
+
for relpath, stem, ext in source_files:
|
|
573
|
+
candidates = {f"test_{stem}{ext}", f"{stem}_test{ext}",
|
|
574
|
+
f"{stem}.test{ext}", f"{stem}.spec{ext}"}
|
|
575
|
+
if not (candidates & all_names):
|
|
576
|
+
untested.add(relpath)
|
|
577
|
+
|
|
578
|
+
imports_map, imported_by_acc, external_map = {}, {}, {}
|
|
579
|
+
if cfg.show_imports:
|
|
580
|
+
for relpath in sorted(all_relpaths):
|
|
581
|
+
ext = os.path.splitext(relpath)[1].lower()
|
|
582
|
+
full_path = os.path.join(root, relpath)
|
|
583
|
+
base_dir = os.path.dirname(relpath)
|
|
584
|
+
local_targets, external_raw = set(), []
|
|
585
|
+
|
|
586
|
+
if ext == ".py":
|
|
587
|
+
for mod, level, names in extract_imports_py(full_path):
|
|
588
|
+
if level and level > 0:
|
|
589
|
+
pkg_parts = base_dir.split("/") if base_dir else []
|
|
590
|
+
up = level - 1
|
|
591
|
+
pkg_parts = pkg_parts[:-up] if (up and up <= len(pkg_parts)) else \
|
|
592
|
+
(pkg_parts if not up else [])
|
|
593
|
+
target_key = ".".join(pkg_parts + ([mod] if mod else []))
|
|
594
|
+
resolved = None
|
|
595
|
+
# 'from .pkg import sub' は sub がサブモジュールの可能性があるため、
|
|
596
|
+
# まず names を「パッケージ内のサブモジュール」として解決を試みる
|
|
597
|
+
# (例: from . import app_helpers → app_helpers.py)
|
|
598
|
+
if names:
|
|
599
|
+
for nm in names:
|
|
600
|
+
cand_key = f"{target_key}.{nm}" if target_key else nm
|
|
601
|
+
resolved = py_module_map.get(cand_key)
|
|
602
|
+
if resolved: break
|
|
603
|
+
# 解決できなければ、import先自体がモジュールである通常パターンにフォールバック
|
|
604
|
+
# (例: from .utils import helper_function → utils.py)
|
|
605
|
+
if not resolved:
|
|
606
|
+
resolved = py_module_map.get(target_key)
|
|
607
|
+
if resolved and resolved != relpath:
|
|
608
|
+
local_targets.add(resolved)
|
|
609
|
+
else:
|
|
610
|
+
external_raw.append("." * level + (mod or ""))
|
|
611
|
+
else:
|
|
612
|
+
resolved = py_module_map.get(mod)
|
|
613
|
+
if resolved and resolved != relpath:
|
|
614
|
+
local_targets.add(resolved)
|
|
615
|
+
else:
|
|
616
|
+
external_raw.append(mod)
|
|
617
|
+
|
|
618
|
+
elif ext in (".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"):
|
|
619
|
+
for spec in extract_imports_js(full_path):
|
|
620
|
+
if spec.startswith(".") or spec.startswith("/"):
|
|
621
|
+
resolved = _resolve_relative_path(base_dir, spec, all_relpaths)
|
|
622
|
+
if resolved and resolved != relpath:
|
|
623
|
+
local_targets.add(resolved)
|
|
624
|
+
else:
|
|
625
|
+
external_raw.append(spec)
|
|
626
|
+
else:
|
|
627
|
+
external_raw.append(spec)
|
|
628
|
+
|
|
629
|
+
elif ext == ".go":
|
|
630
|
+
mod_name = go_module_name[0]
|
|
631
|
+
for spec in extract_imports_go(full_path):
|
|
632
|
+
if mod_name and spec.startswith(mod_name):
|
|
633
|
+
sub = spec[len(mod_name):].lstrip("/")
|
|
634
|
+
candidates = [r for r in all_relpaths if r.endswith(".go") and
|
|
635
|
+
(r.startswith(sub + "/") or os.path.dirname(r) == sub)]
|
|
636
|
+
if candidates:
|
|
637
|
+
for cand in candidates:
|
|
638
|
+
if cand != relpath:
|
|
639
|
+
local_targets.add(cand)
|
|
640
|
+
else:
|
|
641
|
+
external_raw.append(spec)
|
|
642
|
+
else:
|
|
643
|
+
external_raw.append(spec)
|
|
644
|
+
|
|
645
|
+
elif ext == ".rs":
|
|
646
|
+
uses, mods = extract_imports_rs(full_path)
|
|
647
|
+
for m in mods:
|
|
648
|
+
for cand in (f"{base_dir}/{m}.rs" if base_dir else f"{m}.rs",
|
|
649
|
+
f"{base_dir}/{m}/mod.rs" if base_dir else f"{m}/mod.rs"):
|
|
650
|
+
if cand in all_relpaths:
|
|
651
|
+
local_targets.add(cand)
|
|
652
|
+
for u in uses:
|
|
653
|
+
resolved = _resolve_rust_crate_path(u, all_relpaths)
|
|
654
|
+
if resolved and resolved != relpath:
|
|
655
|
+
local_targets.add(resolved)
|
|
656
|
+
else:
|
|
657
|
+
external_raw.append(u)
|
|
658
|
+
|
|
659
|
+
if local_targets:
|
|
660
|
+
imports_map[relpath] = sorted(local_targets)
|
|
661
|
+
for t in local_targets:
|
|
662
|
+
imported_by_acc.setdefault(t, set()).add(relpath)
|
|
663
|
+
if external_raw:
|
|
664
|
+
# 重複除去しつつ最大10件まで
|
|
665
|
+
seen = []
|
|
666
|
+
for x in external_raw:
|
|
667
|
+
if x and x not in seen:
|
|
668
|
+
seen.append(x)
|
|
669
|
+
external_map[relpath] = seen[:10]
|
|
670
|
+
|
|
671
|
+
imported_by_map = {k: sorted(v) for k, v in imported_by_acc.items()}
|
|
672
|
+
return untested, entry_set, imports_map, imported_by_map, external_map
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
# --- シンボルアウトライン(-O / --outline) ----------------------
|
|
676
|
+
# 正規表現ベースの簡易抽出。AST解析ではないため、デコレータが複雑な場合や
|
|
677
|
+
# 複数行にまたがる関数シグネチャ等は取得漏れすることがある(best-effort)。
|
|
678
|
+
_JS_TS_PATTERNS = [
|
|
679
|
+
(re.compile(r'^\s*(?:export\s+)?(?:default\s+)?class\s+(\w+)'), "class"),
|
|
680
|
+
(re.compile(r'^\s*(?:export\s+)?(?:default\s+)?(?:async\s+)?function\s*\*?\s+(\w+)\s*\('), "func"),
|
|
681
|
+
(re.compile(r'^\s*export\s+(?:default\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s*)?\('), "func"),
|
|
682
|
+
(re.compile(r'^\s*(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s*)?\(.*\)\s*=>'), "func"),
|
|
683
|
+
]
|
|
684
|
+
_OUTLINE_PATTERNS = {
|
|
685
|
+
".py": [
|
|
686
|
+
(re.compile(r'^(\s*)class\s+(\w+)'), "class"),
|
|
687
|
+
(re.compile(r'^(\s*)(?:async\s+)?def\s+(\w+)\s*\('), "def"),
|
|
688
|
+
],
|
|
689
|
+
".go": [
|
|
690
|
+
(re.compile(r'^func\s+(?:\([^)]*\)\s+)?(\w+)\s*\('), "func"),
|
|
691
|
+
(re.compile(r'^type\s+(\w+)\s+struct'), "struct"),
|
|
692
|
+
(re.compile(r'^type\s+(\w+)\s+interface'), "interface"),
|
|
693
|
+
],
|
|
694
|
+
".rs": [
|
|
695
|
+
(re.compile(r'^\s*(?:pub(?:\([^)]*\))?\s+)?(?:async\s+)?fn\s+(\w+)'), "fn"),
|
|
696
|
+
(re.compile(r'^\s*(?:pub(?:\([^)]*\))?\s+)?struct\s+(\w+)'), "struct"),
|
|
697
|
+
(re.compile(r'^\s*(?:pub(?:\([^)]*\))?\s+)?enum\s+(\w+)'), "enum"),
|
|
698
|
+
(re.compile(r'^\s*(?:pub(?:\([^)]*\))?\s+)?trait\s+(\w+)'), "trait"),
|
|
699
|
+
],
|
|
700
|
+
}
|
|
701
|
+
for _e in (".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"):
|
|
702
|
+
_OUTLINE_PATTERNS[_e] = _JS_TS_PATTERNS
|
|
703
|
+
|
|
704
|
+
def extract_outline(path, ext, limit_lines=4000):
|
|
705
|
+
"""対応言語(Python/JS/TS/Go/Rust)の関数・クラス名を正規表現で簡易抽出する。
|
|
706
|
+
対応外の拡張子は None を返す(「対応していない」ことを明示するため空リストとは区別)。
|
|
707
|
+
"""
|
|
708
|
+
patterns = _OUTLINE_PATTERNS.get(ext)
|
|
709
|
+
if not patterns:
|
|
710
|
+
return None
|
|
711
|
+
try:
|
|
712
|
+
with open(path, encoding="utf-8", errors="ignore") as f:
|
|
713
|
+
lines = []
|
|
714
|
+
for i, line in enumerate(f):
|
|
715
|
+
if i >= limit_lines:
|
|
716
|
+
break
|
|
717
|
+
lines.append(line)
|
|
718
|
+
except OSError:
|
|
719
|
+
return []
|
|
720
|
+
out = []
|
|
721
|
+
for line in lines:
|
|
722
|
+
for pat, kind in patterns:
|
|
723
|
+
m = pat.match(line)
|
|
724
|
+
if m:
|
|
725
|
+
out.append((kind, m.group(m.lastindex)))
|
|
726
|
+
break
|
|
727
|
+
return out
|
|
728
|
+
|
|
729
|
+
def fmt_outline(outline, limit=5):
|
|
730
|
+
if not outline:
|
|
731
|
+
return None
|
|
732
|
+
items = [f"{kind} {name}" for kind, name in outline]
|
|
733
|
+
shown = items[:limit]
|
|
734
|
+
s = ", ".join(shown)
|
|
735
|
+
if len(items) > limit:
|
|
736
|
+
s += f", +{len(items)-limit}"
|
|
737
|
+
return s
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
def _file_extras(entry, relpath, cfg):
|
|
741
|
+
"""有効になっているAI解析フラグに応じて、ファイル単位の追加情報を計算する。"""
|
|
742
|
+
extras = {}
|
|
743
|
+
ext = os.path.splitext(entry.name)[1].lower()
|
|
744
|
+
|
|
745
|
+
if cfg.show_tokens:
|
|
746
|
+
if _is_probably_binary(entry.name):
|
|
747
|
+
extras["tokens"] = None
|
|
748
|
+
else:
|
|
749
|
+
try: sz = entry.stat(follow_symlinks=True).st_size
|
|
750
|
+
except OSError: sz = None
|
|
751
|
+
extras["tokens"] = estimate_tokens(entry.path, actual_size=sz)
|
|
752
|
+
|
|
753
|
+
if cfg.show_git:
|
|
754
|
+
extras["git"] = cfg.git_map.get(relpath)
|
|
755
|
+
|
|
756
|
+
if cfg.show_todo:
|
|
757
|
+
extras["todos"] = scan_todos(entry.path)
|
|
758
|
+
|
|
759
|
+
if cfg.show_entry:
|
|
760
|
+
extras["is_entry"] = relpath in cfg.entry_set
|
|
761
|
+
|
|
762
|
+
if cfg.show_tests:
|
|
763
|
+
extras["no_test"] = relpath in cfg.untested_set
|
|
764
|
+
|
|
765
|
+
if cfg.show_outline:
|
|
766
|
+
extras["outline"] = extract_outline(entry.path, ext)
|
|
767
|
+
|
|
768
|
+
if cfg.show_imports:
|
|
769
|
+
extras["imports"] = cfg.imports_map.get(relpath, [])
|
|
770
|
+
extras["imported_by"] = cfg.imported_by_map.get(relpath, [])
|
|
771
|
+
extras["external_imports"] = cfg.external_map.get(relpath, [])
|
|
772
|
+
|
|
773
|
+
return extras
|
|
774
|
+
|
|
775
|
+
|
|
776
|
+
# ════════════════════════════════════════════════════════════
|
|
777
|
+
|
|
778
|
+
|
|
250
779
|
# ─── 設定クラス ───────────────────────────────────────────────
|
|
251
780
|
class Cfg:
|
|
252
781
|
def __init__(self, args, root):
|
|
@@ -275,6 +804,25 @@ class Cfg:
|
|
|
275
804
|
self.reverse = args.reverse
|
|
276
805
|
self.files_first = args.filesfirst
|
|
277
806
|
|
|
807
|
+
# AI/エージェント向け解析フラグ
|
|
808
|
+
self.show_tokens = args.tokens # -T
|
|
809
|
+
self.show_git = args.git # -H
|
|
810
|
+
self.show_todo = args.todo # -K
|
|
811
|
+
self.show_tests = args.tests # -V
|
|
812
|
+
self.show_entry = args.entry # -N
|
|
813
|
+
self.show_outline = args.outline # -O
|
|
814
|
+
self.show_imports = args.imports # -M
|
|
815
|
+
self.has_extras = any([self.show_tokens, self.show_git, self.show_todo,
|
|
816
|
+
self.show_tests, self.show_entry, self.show_outline,
|
|
817
|
+
self.show_imports])
|
|
818
|
+
# main() 側で必要に応じて埋める
|
|
819
|
+
self.git_map = {}
|
|
820
|
+
self.untested_set = set()
|
|
821
|
+
self.entry_set = set()
|
|
822
|
+
self.imports_map = {}
|
|
823
|
+
self.imported_by_map = {}
|
|
824
|
+
self.external_map = {}
|
|
825
|
+
|
|
278
826
|
|
|
279
827
|
# ─── 共通フィルタリング ───────────────────────────────────────
|
|
280
828
|
def _filter(path, cfg, active_pats):
|
|
@@ -298,7 +846,6 @@ def _filter(path, cfg, active_pats):
|
|
|
298
846
|
and e.is_dir(follow_symlinks=True)]
|
|
299
847
|
dirs = dirs + sym_dirs
|
|
300
848
|
|
|
301
|
-
# -d (dirs only): ファイルを非表示
|
|
302
849
|
if cfg.dirs_only:
|
|
303
850
|
files = []
|
|
304
851
|
else:
|
|
@@ -336,7 +883,7 @@ def _has_content(path, depth, cfg, active_pats):
|
|
|
336
883
|
if cfg.max_depth is not None and depth >= cfg.max_depth: return False
|
|
337
884
|
pats = _extend_pats(active_pats, path, cfg)
|
|
338
885
|
dirs, files = _filter(path, cfg, pats)
|
|
339
|
-
if dirs is None: return False
|
|
886
|
+
if dirs is None: return False
|
|
340
887
|
if files: return True
|
|
341
888
|
for d in dirs:
|
|
342
889
|
if _has_content(d.path, depth + 1, cfg, pats): return True
|
|
@@ -345,7 +892,6 @@ def _has_content(path, depth, cfg, active_pats):
|
|
|
345
892
|
|
|
346
893
|
# ─── ソートヘルパー ───────────────────────────────────────────
|
|
347
894
|
def _sort_entries(dirs, files, cfg):
|
|
348
|
-
"""設定に基づいてdirs/filesをソートする(-t/-c/-S/-r対応)。"""
|
|
349
895
|
def emtime(e):
|
|
350
896
|
try: return e.stat(follow_symlinks=True).st_mtime
|
|
351
897
|
except OSError: return 0
|
|
@@ -357,16 +903,16 @@ def _sort_entries(dirs, files, cfg):
|
|
|
357
903
|
except OSError: return 0
|
|
358
904
|
|
|
359
905
|
rev = cfg.reverse
|
|
360
|
-
if cfg.sort_mtime:
|
|
906
|
+
if cfg.sort_mtime:
|
|
361
907
|
dirs.sort(key=emtime, reverse=not rev)
|
|
362
908
|
files.sort(key=emtime, reverse=not rev)
|
|
363
|
-
elif cfg.sort_ctime:
|
|
909
|
+
elif cfg.sort_ctime:
|
|
364
910
|
dirs.sort(key=ectime, reverse=not rev)
|
|
365
911
|
files.sort(key=ectime, reverse=not rev)
|
|
366
|
-
elif cfg.by_size:
|
|
912
|
+
elif cfg.by_size:
|
|
367
913
|
dirs.sort(key=lambda e: dir_size(e.path)[0], reverse=not rev)
|
|
368
914
|
files.sort(key=esz, reverse=not rev)
|
|
369
|
-
else:
|
|
915
|
+
else:
|
|
370
916
|
dirs.sort(key=lambda e: e.name.casefold(), reverse=rev)
|
|
371
917
|
files.sort(key=lambda e: e.name.casefold(), reverse=rev)
|
|
372
918
|
|
|
@@ -389,7 +935,7 @@ def render(path, prefix, depth, cfg, stats, active_pats, _seen=None):
|
|
|
389
935
|
|
|
390
936
|
cur_pats = _extend_pats(active_pats, path, cfg)
|
|
391
937
|
dirs, files = _filter(path, cfg, cur_pats)
|
|
392
|
-
if dirs is None:
|
|
938
|
+
if dirs is None:
|
|
393
939
|
print(f"{prefix}{LAST}{c('[アクセス拒否]', BOLD, RED)}")
|
|
394
940
|
return
|
|
395
941
|
|
|
@@ -450,21 +996,62 @@ def render(path, prefix, depth, cfg, stats, active_pats, _seen=None):
|
|
|
450
996
|
stats["extensions"][ext or "(no ext)"] = \
|
|
451
997
|
stats["extensions"].get(ext or "(no ext)", 0) + 1
|
|
452
998
|
|
|
999
|
+
rel = os.path.relpath(entry.path, cfg.root).replace("\\", "/")
|
|
1000
|
+
extras = _file_extras(entry, rel, cfg) if cfg.has_extras else {}
|
|
1001
|
+
|
|
1002
|
+
entry_mark = ""
|
|
1003
|
+
if extras.get("is_entry"):
|
|
1004
|
+
entry_mark = "🎯 " if cfg.show_emoji else "* "
|
|
1005
|
+
stats["entries"] += 1
|
|
1006
|
+
|
|
453
1007
|
emoji = (get_emoji(entry.name) + " ") if cfg.show_emoji else ""
|
|
454
|
-
parts = [fmt_size(sz)]
|
|
1008
|
+
parts = [fmt_size(sz)]
|
|
455
1009
|
if cfg.show_date:
|
|
456
1010
|
mt = emtime(entry)
|
|
457
1011
|
if mt: parts.append(fmt_date(mt))
|
|
1012
|
+
|
|
1013
|
+
if cfg.show_tokens and extras.get("tokens") is not None:
|
|
1014
|
+
parts.append(fmt_tokens(extras["tokens"]))
|
|
1015
|
+
stats["tokens"] += extras["tokens"]
|
|
1016
|
+
|
|
1017
|
+
if cfg.show_git and extras.get("git"):
|
|
1018
|
+
parts.append(fmt_git(extras["git"]))
|
|
1019
|
+
|
|
1020
|
+
if cfg.show_todo and extras.get("todos"):
|
|
1021
|
+
n_todo = len(extras["todos"])
|
|
1022
|
+
parts.append(f"TODO×{n_todo}")
|
|
1023
|
+
stats["todo_total"] += n_todo
|
|
1024
|
+
for item in extras["todos"][:3]:
|
|
1025
|
+
if len(stats["todo_samples"]) < 20:
|
|
1026
|
+
stats["todo_samples"].append((rel, *item))
|
|
1027
|
+
|
|
1028
|
+
if cfg.show_tests and extras.get("no_test"):
|
|
1029
|
+
parts.append("テスト無し")
|
|
1030
|
+
stats["no_test"] += 1
|
|
1031
|
+
|
|
1032
|
+
if cfg.show_outline and extras.get("outline"):
|
|
1033
|
+
ostr = fmt_outline(extras["outline"])
|
|
1034
|
+
if ostr: parts.append(ostr)
|
|
1035
|
+
|
|
1036
|
+
if cfg.show_imports:
|
|
1037
|
+
imp_n = len(extras.get("imports") or [])
|
|
1038
|
+
used_n = len(extras.get("imported_by") or [])
|
|
1039
|
+
if imp_n: parts.append(f"imports×{imp_n}")
|
|
1040
|
+
if used_n: parts.append(f"used-by×{used_n}")
|
|
1041
|
+
|
|
458
1042
|
bar = (" " + fmt_bar(sz, cur_dir_size)) if cfg.show_bar and cur_dir_size else ""
|
|
459
1043
|
|
|
460
|
-
name = c(f"{
|
|
1044
|
+
name = c(f"{entry_mark}{display}{sym_target}",
|
|
461
1045
|
MAGENTA if entry.is_symlink() else GREEN)
|
|
462
1046
|
meta = c(f"({', '.join(parts)}){bar}", DIM)
|
|
463
1047
|
print(f"{prefix}{branch}{perm_prefix}{name} {meta}")
|
|
464
1048
|
|
|
465
1049
|
|
|
466
1050
|
# ─── JSON出力 ─────────────────────────────────────────────────
|
|
467
|
-
def build_json_tree(path, depth, cfg, active_pats):
|
|
1051
|
+
def build_json_tree(path, depth, cfg, active_pats, stats=None):
|
|
1052
|
+
if stats is None:
|
|
1053
|
+
stats = {"tokens": 0, "todo_total": 0, "todo_samples": [], "no_test": 0, "entries": 0}
|
|
1054
|
+
|
|
468
1055
|
cur_pats = _extend_pats(active_pats, path, cfg)
|
|
469
1056
|
dirs, files = _filter(path, cfg, cur_pats)
|
|
470
1057
|
denied = dirs is None
|
|
@@ -483,15 +1070,47 @@ def build_json_tree(path, depth, cfg, active_pats):
|
|
|
483
1070
|
combined = (files + dirs) if cfg.files_first else (dirs + files)
|
|
484
1071
|
for entry in combined:
|
|
485
1072
|
if entry.is_dir(follow_symlinks=False):
|
|
486
|
-
children.append(build_json_tree(entry.path, depth + 1, cfg, cur_pats))
|
|
1073
|
+
children.append(build_json_tree(entry.path, depth + 1, cfg, cur_pats, stats))
|
|
487
1074
|
else:
|
|
488
1075
|
f_sz = esz(entry)
|
|
489
|
-
|
|
1076
|
+
rel = os.path.relpath(entry.path, cfg.root).replace("\\", "/")
|
|
1077
|
+
extras = _file_extras(entry, rel, cfg) if cfg.has_extras else {}
|
|
1078
|
+
|
|
1079
|
+
file_obj = {
|
|
490
1080
|
"name": entry.name, "type": "file",
|
|
491
1081
|
"size": f_sz, "size_human": fmt_size(f_sz),
|
|
492
1082
|
"ext": os.path.splitext(entry.name)[1].lower(),
|
|
493
|
-
"path":
|
|
494
|
-
}
|
|
1083
|
+
"path": rel,
|
|
1084
|
+
}
|
|
1085
|
+
if cfg.show_tokens:
|
|
1086
|
+
file_obj["tokens"] = extras.get("tokens")
|
|
1087
|
+
if extras.get("tokens") is not None:
|
|
1088
|
+
stats["tokens"] += extras["tokens"]
|
|
1089
|
+
if cfg.show_git:
|
|
1090
|
+
file_obj["git"] = extras.get("git")
|
|
1091
|
+
if cfg.show_todo:
|
|
1092
|
+
todos = extras.get("todos") or []
|
|
1093
|
+
file_obj["todos"] = [{"line": ln, "kind": k, "text": s} for ln, k, s in todos]
|
|
1094
|
+
stats["todo_total"] += len(todos)
|
|
1095
|
+
for item in todos[:3]:
|
|
1096
|
+
if len(stats["todo_samples"]) < 20:
|
|
1097
|
+
stats["todo_samples"].append((rel, *item))
|
|
1098
|
+
if cfg.show_tests:
|
|
1099
|
+
file_obj["has_test"] = not extras.get("no_test", False)
|
|
1100
|
+
if extras.get("no_test"): stats["no_test"] += 1
|
|
1101
|
+
if cfg.show_entry:
|
|
1102
|
+
file_obj["is_entry"] = bool(extras.get("is_entry"))
|
|
1103
|
+
if extras.get("is_entry"): stats["entries"] += 1
|
|
1104
|
+
if cfg.show_outline:
|
|
1105
|
+
outline = extras.get("outline")
|
|
1106
|
+
file_obj["outline"] = ([{"kind": k, "name": n} for k, n in outline]
|
|
1107
|
+
if outline else outline) # None=対応外言語
|
|
1108
|
+
if cfg.show_imports:
|
|
1109
|
+
file_obj["imports"] = extras.get("imports") or []
|
|
1110
|
+
file_obj["imported_by"] = extras.get("imported_by") or []
|
|
1111
|
+
file_obj["external_imports"] = extras.get("external_imports") or []
|
|
1112
|
+
|
|
1113
|
+
children.append(file_obj)
|
|
495
1114
|
|
|
496
1115
|
name = os.path.basename(path) or path
|
|
497
1116
|
return {
|
|
@@ -537,10 +1156,21 @@ def generate_html(root_path, cfg, active_pats):
|
|
|
537
1156
|
if entry.is_symlink():
|
|
538
1157
|
try: sym = f' → {os.readlink(entry.path)}'
|
|
539
1158
|
except OSError: sym = ' →'
|
|
1159
|
+
|
|
1160
|
+
rel = os.path.relpath(entry.path, cfg.root).replace("\\", "/")
|
|
1161
|
+
extras = _file_extras(entry, rel, cfg) if cfg.has_extras else {}
|
|
1162
|
+
badges = ""
|
|
1163
|
+
if extras.get("is_entry"):
|
|
1164
|
+
badges += '<span class="badge entry">entry</span>'
|
|
1165
|
+
if extras.get("no_test"):
|
|
1166
|
+
badges += '<span class="badge notest">no test</span>'
|
|
1167
|
+
if extras.get("todos"):
|
|
1168
|
+
badges += f'<span class="badge todo">TODO×{len(extras["todos"])}</span>'
|
|
1169
|
+
|
|
540
1170
|
ch += (f'<div class="item file">'
|
|
541
1171
|
f'<span class="emoji">{get_emoji(entry.name)}</span>'
|
|
542
1172
|
f'<span class="fname"> {entry.name}{sym}</span>'
|
|
543
|
-
f'<span class="sz"> {fmt_size(f_sz)}</span
|
|
1173
|
+
f'<span class="sz"> {fmt_size(f_sz)}</span>{badges}</div>\n')
|
|
544
1174
|
|
|
545
1175
|
nd, nf = len(dirs), len(files)
|
|
546
1176
|
opened = " open" if depth == 0 else ""
|
|
@@ -573,6 +1203,11 @@ summary:hover{{background:rgba(255,255,255,.06)}}
|
|
|
573
1203
|
.item:hover{{background:rgba(255,255,255,.05);border-radius:4px}}
|
|
574
1204
|
.fname{{color:#a6e3a1}}.sz{{color:#585b70;font-size:12px}}
|
|
575
1205
|
.emoji{{width:1.6em;display:inline-block}}.hidden{{display:none!important}}
|
|
1206
|
+
.badge{{display:inline-block;margin-left:6px;padding:0 6px;border-radius:8px;
|
|
1207
|
+
font-size:10px;vertical-align:middle}}
|
|
1208
|
+
.badge.entry{{background:#89b4fa;color:#1e1e2e}}
|
|
1209
|
+
.badge.notest{{background:#f9e2af;color:#1e1e2e}}
|
|
1210
|
+
.badge.todo{{background:#f38ba8;color:#1e1e2e}}
|
|
576
1211
|
</style></head><body>
|
|
577
1212
|
<h1>🌳 dirlens — {root_name}</h1>
|
|
578
1213
|
<input id="q" type="text" placeholder="ファイル名で検索…" oninput="search(this.value)">
|
|
@@ -580,18 +1215,13 @@ summary:hover{{background:rgba(255,255,255,.06)}}
|
|
|
580
1215
|
<script>
|
|
581
1216
|
function search(q){{
|
|
582
1217
|
q=q.toLowerCase().trim();
|
|
583
|
-
// まず全要素を表示に戻す
|
|
584
1218
|
document.querySelectorAll('.hidden').forEach(el=>el.classList.remove('hidden'));
|
|
585
1219
|
if(!q) return;
|
|
586
|
-
// 全ディレクトリを展開
|
|
587
1220
|
document.querySelectorAll('details').forEach(d=>d.open=true);
|
|
588
|
-
// マッチしないファイルを非表示
|
|
589
1221
|
document.querySelectorAll('.file').forEach(el=>{{
|
|
590
1222
|
const n=el.querySelector('.fname')?.textContent.toLowerCase()||'';
|
|
591
1223
|
if(!n.includes(q)) el.classList.add('hidden');
|
|
592
1224
|
}});
|
|
593
|
-
// visible なファイルを1つも持たないディレクトリを非表示
|
|
594
|
-
// (空ディレクトリ・検索に引っかからないディレクトリ両方を処理)
|
|
595
1225
|
document.querySelectorAll('#tree details').forEach(detail=>{{
|
|
596
1226
|
const hasVisible=[...detail.querySelectorAll('.file')]
|
|
597
1227
|
.some(f=>!f.classList.contains('hidden'));
|
|
@@ -612,15 +1242,18 @@ def main():
|
|
|
612
1242
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
613
1243
|
epilog=(
|
|
614
1244
|
"使用例:\n"
|
|
615
|
-
" dirlens --ai
|
|
616
|
-
" dirlens
|
|
617
|
-
" dirlens -
|
|
618
|
-
" dirlens -
|
|
619
|
-
" dirlens -G --prune
|
|
620
|
-
" dirlens -
|
|
621
|
-
" dirlens -
|
|
622
|
-
" dirlens -
|
|
623
|
-
" dirlens -
|
|
1245
|
+
" dirlens --ai AIチャット貼り付け用(人間がコピペする想定)\n"
|
|
1246
|
+
" dirlens --agent エージェント向け解析(クリップボードは使わない)\n"
|
|
1247
|
+
" dirlens -d ディレクトリのみ表示(tree -d 互換)\n"
|
|
1248
|
+
" dirlens -L 2 深さ 2 まで表示(tree -L 互換)\n"
|
|
1249
|
+
" dirlens -G --prune gitignore除外 + 空枝を剪定\n"
|
|
1250
|
+
" dirlens -T ファイルごとの推定トークン数を表示\n"
|
|
1251
|
+
" dirlens -H 最終コミット情報を表示(要git)\n"
|
|
1252
|
+
" dirlens -K TODO/FIXME/HACKを抽出\n"
|
|
1253
|
+
" dirlens -V テストが無いソースファイルを表示\n"
|
|
1254
|
+
" dirlens -N エントリーポイントらしきファイルをマーク\n"
|
|
1255
|
+
" dirlens -O 関数・クラスの簡易アウトラインを表示\n"
|
|
1256
|
+
" dirlens -M ローカルなimport/依存関係を解析\n"
|
|
624
1257
|
" dirlens --no-color > dirlens.txt ファイルに書き出す"
|
|
625
1258
|
),
|
|
626
1259
|
)
|
|
@@ -667,6 +1300,23 @@ def main():
|
|
|
667
1300
|
ap.add_argument("-I", dest="exclude_tree", metavar="PATTERN", action="append",
|
|
668
1301
|
help="除外パターン(tree -I 互換)")
|
|
669
1302
|
|
|
1303
|
+
# ── AI/エージェント向け解析フラグ ─────────────────────────
|
|
1304
|
+
ap.add_argument("-T", "--tokens", action="store_true",
|
|
1305
|
+
help="ファイルごとの推定トークン数を表示(概算)")
|
|
1306
|
+
ap.add_argument("-H", "--git", action="store_true",
|
|
1307
|
+
help="最終コミット情報を表示(要git、直近2000コミットまで走査)")
|
|
1308
|
+
ap.add_argument("-K", "--todo", action="store_true",
|
|
1309
|
+
help="TODO/FIXME/HACK/XXXコメントを抽出")
|
|
1310
|
+
ap.add_argument("-V", "--missing-tests", action="store_true", dest="tests",
|
|
1311
|
+
help="対応するテストファイルが見つからないソースファイルを表示")
|
|
1312
|
+
ap.add_argument("-N", "--entry", action="store_true",
|
|
1313
|
+
help="エントリーポイントらしきファイルを検出してマーク")
|
|
1314
|
+
ap.add_argument("-O", "--outline", action="store_true",
|
|
1315
|
+
help="関数・クラスの簡易アウトラインを表示(正規表現ベース・対応言語限定)")
|
|
1316
|
+
ap.add_argument("-M", "--imports", action="store_true",
|
|
1317
|
+
help="ローカルなimport/依存関係を解析して表示(Python/JS/TS/Go/Rust対応、"
|
|
1318
|
+
"正確さは言語による。外部パッケージは対象外)")
|
|
1319
|
+
|
|
670
1320
|
# ── dirlens独自オプション ─────────────────────────────────
|
|
671
1321
|
ap.add_argument("path", nargs="?", default=".")
|
|
672
1322
|
ap.add_argument("--depth", type=int, metavar="N",
|
|
@@ -685,7 +1335,9 @@ def main():
|
|
|
685
1335
|
ap.add_argument("--prune", action="store_true")
|
|
686
1336
|
ap.add_argument("--filesfirst", action="store_true")
|
|
687
1337
|
ap.add_argument("--ai", action="store_true",
|
|
688
|
-
help="-G --date -m -C
|
|
1338
|
+
help="-G --date -m -C のショートカット(人間がAIチャットに貼り付ける用)")
|
|
1339
|
+
ap.add_argument("--agent", action="store_true",
|
|
1340
|
+
help="-G -T -H -K -V -N -O のショートカット(エージェント向け解析、クリップボードは使わない)")
|
|
689
1341
|
args = ap.parse_args()
|
|
690
1342
|
|
|
691
1343
|
# ── エイリアスのマージ ────────────────────────────────────
|
|
@@ -696,13 +1348,25 @@ def main():
|
|
|
696
1348
|
if args.no_color_tree: args.no_color = True
|
|
697
1349
|
if args.json_tree: args.json = True
|
|
698
1350
|
|
|
699
|
-
# --ai:
|
|
1351
|
+
# --ai: 人間がAIチャットに貼り付けるためのショートカット(クリップボードを使う)
|
|
700
1352
|
if args.ai:
|
|
701
1353
|
args.gitignore = True
|
|
702
1354
|
args.date = True
|
|
703
1355
|
args.markdown = True
|
|
704
1356
|
args.copy = True
|
|
705
1357
|
|
|
1358
|
+
# --agent: エージェントが自律実行しても安全なショートカット(クリップボードは使わない)
|
|
1359
|
+
if args.agent:
|
|
1360
|
+
args.gitignore = True
|
|
1361
|
+
args.date = True
|
|
1362
|
+
args.tokens = True
|
|
1363
|
+
args.git = True
|
|
1364
|
+
args.todo = True
|
|
1365
|
+
args.tests = True
|
|
1366
|
+
args.entry = True
|
|
1367
|
+
args.outline = True
|
|
1368
|
+
args.imports = True
|
|
1369
|
+
|
|
706
1370
|
if args.no_color or args.markdown or args.json:
|
|
707
1371
|
USE_COLOR = False
|
|
708
1372
|
|
|
@@ -714,12 +1378,33 @@ def main():
|
|
|
714
1378
|
|
|
715
1379
|
cfg = Cfg(args, str(target))
|
|
716
1380
|
active_pats = load_gitignore(str(target)) if args.gitignore else []
|
|
1381
|
+
|
|
1382
|
+
if cfg.show_tests or cfg.show_entry or cfg.show_imports:
|
|
1383
|
+
(cfg.untested_set, cfg.entry_set,
|
|
1384
|
+
cfg.imports_map, cfg.imported_by_map, cfg.external_map) = build_project_index(str(target), cfg)
|
|
1385
|
+
if cfg.show_git:
|
|
1386
|
+
cfg.git_map = load_git_log(str(target))
|
|
1387
|
+
|
|
717
1388
|
_prefetch_sizes(str(target))
|
|
718
1389
|
|
|
719
1390
|
# ── JSON ─────────────────────────────────────────────────
|
|
720
1391
|
if args.json:
|
|
721
|
-
|
|
722
|
-
|
|
1392
|
+
stats = {"tokens": 0, "todo_total": 0, "todo_samples": [], "no_test": 0, "entries": 0}
|
|
1393
|
+
tree = build_json_tree(str(target), 0, cfg, active_pats, stats)
|
|
1394
|
+
if cfg.has_extras:
|
|
1395
|
+
most_depended = None
|
|
1396
|
+
if cfg.show_imports and cfg.imported_by_map:
|
|
1397
|
+
top = sorted(cfg.imported_by_map.items(), key=lambda kv: -len(kv[1]))[:10]
|
|
1398
|
+
most_depended = [{"path": p, "used_by_count": len(v)} for p, v in top]
|
|
1399
|
+
tree["project_summary"] = {
|
|
1400
|
+
"estimated_tokens": stats["tokens"] if cfg.show_tokens else None,
|
|
1401
|
+
"todo_count": stats["todo_total"] if cfg.show_todo else None,
|
|
1402
|
+
"missing_tests_count": stats["no_test"] if cfg.show_tests else None,
|
|
1403
|
+
"entry_points_count": stats["entries"] if cfg.show_entry else None,
|
|
1404
|
+
"git_available": bool(cfg.git_map) if cfg.show_git else None,
|
|
1405
|
+
"most_depended_on": most_depended,
|
|
1406
|
+
}
|
|
1407
|
+
print(json.dumps(tree, ensure_ascii=False, indent=2)); return
|
|
723
1408
|
|
|
724
1409
|
# ── HTML ─────────────────────────────────────────────────
|
|
725
1410
|
if args.html:
|
|
@@ -746,7 +1431,8 @@ def main():
|
|
|
746
1431
|
print(f"{c(root_emoji + root_label + '/', BOLD, BLUE)} "
|
|
747
1432
|
f"{c('(' + ', '.join(root_parts) + ')', DIM)}")
|
|
748
1433
|
|
|
749
|
-
stats = {"files": 0, "dirs": 0, "extensions": {}
|
|
1434
|
+
stats = {"files": 0, "dirs": 0, "extensions": {},
|
|
1435
|
+
"tokens": 0, "todo_total": 0, "todo_samples": [], "no_test": 0, "entries": 0}
|
|
750
1436
|
render(str(target), "", 0, cfg, stats, active_pats)
|
|
751
1437
|
|
|
752
1438
|
print()
|
|
@@ -767,6 +1453,35 @@ def main():
|
|
|
767
1453
|
exts = sorted(stats["extensions"].items(), key=lambda x: -x[1])
|
|
768
1454
|
print(c(" " + " ".join(f"{e} ×{n}" for e, n in exts[:8]), DIM))
|
|
769
1455
|
|
|
1456
|
+
if cfg.show_tokens:
|
|
1457
|
+
print(c(f" 推定トークン数: {fmt_tokens(stats['tokens'])}", DIM))
|
|
1458
|
+
|
|
1459
|
+
if cfg.show_todo:
|
|
1460
|
+
if stats["todo_total"]:
|
|
1461
|
+
print(c(f" TODO/FIXME等: {stats['todo_total']}件", DIM))
|
|
1462
|
+
for rel, ln, kind, snippet in stats["todo_samples"][:8]:
|
|
1463
|
+
print(c(f" {rel}:{ln} [{kind}] {snippet}", DIM))
|
|
1464
|
+
if stats["todo_total"] > min(len(stats["todo_samples"]), 8):
|
|
1465
|
+
rest = stats["todo_total"] - min(len(stats["todo_samples"]), 8)
|
|
1466
|
+
print(c(f" …他 {rest} 件", DIM))
|
|
1467
|
+
else:
|
|
1468
|
+
print(c(" TODO/FIXME等: 0件", DIM))
|
|
1469
|
+
|
|
1470
|
+
if cfg.show_tests:
|
|
1471
|
+
print(c(f" テスト未整備: {stats['no_test']} ファイル", DIM))
|
|
1472
|
+
|
|
1473
|
+
if cfg.show_entry:
|
|
1474
|
+
print(c(f" エントリーポイント候補: {stats['entries']} 件検出", DIM))
|
|
1475
|
+
|
|
1476
|
+
if cfg.show_imports and cfg.imported_by_map:
|
|
1477
|
+
top = sorted(cfg.imported_by_map.items(), key=lambda kv: -len(kv[1]))[:5]
|
|
1478
|
+
print(c(" 依存度が高いファイル(多くのファイルから参照されている):", DIM))
|
|
1479
|
+
for relpath, importers in top:
|
|
1480
|
+
print(c(f" {relpath} (used by {len(importers)})", DIM))
|
|
1481
|
+
|
|
1482
|
+
if cfg.show_git and not cfg.git_map:
|
|
1483
|
+
print(c(" (gitリポジトリではないか、git未インストールのためコミット情報は取得できませんでした)", DIM))
|
|
1484
|
+
|
|
770
1485
|
if args.markdown: print("```")
|
|
771
1486
|
|
|
772
1487
|
if args.copy:
|