codebread 1.1.0__py3-none-any.whl → 1.1.3__py3-none-any.whl

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.
codebread/connections.py CHANGED
@@ -26,6 +26,16 @@ COMMON_NAMES = { # never resolve these across files by name alone
26
26
  ENTRY_NAMES = {
27
27
  "main", "__init__", "__main__", "__new__", "__call__", "setup", "setUp",
28
28
  "tearDown", "run", "handler", "index", "render", "constructor",
29
+ # PHP magic methods
30
+ "__construct", "__destruct", "__invoke", "__get", "__set", "__toString",
31
+ # Laravel: migrations, providers, jobs, requests, factories, policies
32
+ "up", "down", "boot", "booted", "register", "handle", "schedule", "rules",
33
+ "authorize", "definition", "casts", "toArray", "via", "toMail", "broadcastOn",
34
+ "store", "show", "create", "edit", "update", "destroy",
35
+ # Filament / Livewire hooks
36
+ "form", "table", "configure", "panel", "mount", "getPages", "getRelations",
37
+ "getHeaderActions", "getStats", "getData", "getType", "getEloquentQuery",
38
+ "getGloballySearchableAttributes", "canAccessPanel",
29
39
  }
30
40
 
31
41
 
@@ -80,6 +90,10 @@ def _resolve_file_ref(base_file: str, target: str, file_set) -> Optional[str]:
80
90
  posixpath.normpath(target.lstrip("/")),
81
91
  target.lstrip("./"),
82
92
  ]
93
+ # PSR-4 namespaces: `use App\Support\MjIcon` lives in app/Support/MjIcon.php
94
+ head, _, rest = target.partition("/")
95
+ if rest and head[:1].isupper():
96
+ candidates.append(head.lower() + "/" + rest)
83
97
  # python dotted modules: models / app.utils -> models.py / app/utils.py
84
98
  if "/" not in target and "." not in posixpath.basename(target):
85
99
  candidates.append(target.replace(".", "/"))
@@ -233,8 +247,9 @@ def build_graph(files: List[FileInfo], tree: Dict,
233
247
  if t is not None: # enrich a placeholder with real definition
234
248
  for n in nodes:
235
249
  if n["id"] == nid:
236
- if t.fields:
237
- n["fields"] = t.fields
250
+ if t.fields: # merge: migrations add columns over time
251
+ n["fields"] = n.get("fields", []) + [
252
+ c for c in t.fields if c not in n.get("fields", [])]
238
253
  if t.model:
239
254
  n["model"] = t.model
240
255
  if t.source:
@@ -252,11 +267,17 @@ def build_graph(files: List[FileInfo], tree: Dict,
252
267
  })
253
268
  return nid
254
269
 
270
+ # real definitions first, so a model's guessed name can snap to one
271
+ defined = {t.name.lower() for f in files for t in f.tables if not t.aliases}
255
272
  for f in files:
256
- for t in f.tables:
273
+ for t in sorted(f.tables, key=lambda t: bool(t.aliases)):
274
+ if t.aliases:
275
+ t.name = next((a for a in t.aliases if a.lower() in defined), t.name)
257
276
  nid = table_node(t.name, t)
258
277
  if t.model:
259
278
  model_to_table[t.model.lower()] = nid
279
+ if t.aliases: # Eloquent model file -> its table
280
+ add_edge(f.path, nid, "db", f"{t.model} model → {t.name}")
260
281
 
261
282
  # ---- call edges --------------------------------------------------
262
283
  for f in files:
@@ -288,12 +309,14 @@ def build_graph(files: List[FileInfo], tree: Dict,
288
309
 
289
310
  # ---- file-level connections ---------------------------------------
290
311
  file_set = {f.path for f in files}
312
+ called_from_page: set = set() # own functions a page calls (onclick="f()")
291
313
  for f in files:
292
314
  fname = f.path.split("/")[-1]
293
315
  local = {fn.name: fn_nodes[(f.path, fn.name)] for fn in f.functions}
294
316
  # top-level calls: page/script code calling into functions
295
317
  for callee in f.calls:
296
318
  if callee in local:
319
+ called_from_page.add(local[callee])
297
320
  continue # a file already implicitly contains its own fns
298
321
  candidates = name_index.get(callee, [])
299
322
  if not candidates or callee in COMMON_NAMES:
@@ -332,13 +355,13 @@ def build_graph(files: List[FileInfo], tree: Dict,
332
355
  target = handler_id or f.path
333
356
  route_index.append((r.method, _norm_path(r.path), target,
334
357
  f"{r.method} {r.path}"))
335
- if handler_id:
336
- # mark handler node with its route
337
- for n in nodes:
338
- if n["id"] == handler_id:
339
- n.setdefault("routes", []).append(
340
- {"method": r.method, "path": r.path})
341
- break
358
+ # mark the handler with its route; closure routes (Laravel
359
+ # `Route::get('/', function () {...})`) land on the file itself
360
+ for n in nodes:
361
+ if n["id"] == target:
362
+ n.setdefault("routes", []).append(
363
+ {"method": r.method, "path": r.path})
364
+ break
342
365
 
343
366
  # ---- api edges (frontend call -> backend route) -------------------
344
367
  def match_route(method: str, url: str) -> Optional[Tuple[str, str]]:
@@ -374,6 +397,8 @@ def build_graph(files: List[FileInfo], tree: Dict,
374
397
  tid = model_to_table.get(ref.table.lower()) \
375
398
  or table_ids.get(ref.table.lower())
376
399
  if tid is None:
400
+ if ref.via == "orm-guess":
401
+ continue # `Foo::where()` where Foo isn't a known model
377
402
  # unknown table: create it only for confident refs
378
403
  if ref.via == "sql" or ref.table.lower() in table_ids:
379
404
  tid = table_node(ref.table)
@@ -389,6 +414,12 @@ def build_graph(files: List[FileInfo], tree: Dict,
389
414
 
390
415
  # ---- orphans + circular dependencies --------------------------------
391
416
  cycles = _mark_orphans_and_cycles(nodes, edges)
417
+ # Eloquent model methods (relations, accessors) are reached through magic
418
+ # properties like $booking->user, which static analysis can't see
419
+ model_files = {f.path for f in files for t in f.tables if t.model and t.aliases}
420
+ for n in nodes:
421
+ if n.get("orphan") and (n.get("file") in model_files or n["id"] in called_from_page):
422
+ del n["orphan"]
392
423
 
393
424
  # ---- stats ---------------------------------------------------------
394
425
  n_fns = sum(1 for n in nodes if n["kind"] in ("function", "method"))
codebread/context.py CHANGED
@@ -8,6 +8,7 @@ it once, then opens only the files that matter for the task.
8
8
  from __future__ import annotations
9
9
 
10
10
  import os
11
+ import re
11
12
  from collections import Counter, defaultdict
12
13
  from typing import Dict, Iterable
13
14
 
@@ -24,6 +25,20 @@ def _short(items: Iterable[str], cap: int = LIST_CAP) -> str:
24
25
  return ", ".join(items[:cap]) + more
25
26
 
26
27
 
28
+ def _human(name: str) -> str:
29
+ return " ".join(re.sub(r"(?<=[a-z0-9])(?=[A-Z])|_+", " ", name).split()).lower()
30
+
31
+
32
+ def _desc(fn: Dict) -> str:
33
+ """Drop auto-descriptions that only repeat the name ("Get Header Actions."),
34
+ keep what they add after it ("handles POST /users; queries users")."""
35
+ d = (fn.get("description") or "").strip()
36
+ head, _, tail = d.partition(" — ")
37
+ if head.rstrip(".").lower() == _human(fn["label"]):
38
+ return tail.rstrip(".")
39
+ return d
40
+
41
+
27
42
  def to_markdown(graph: Dict) -> str:
28
43
  nodes = {n["id"]: n for n in graph.get("nodes", [])}
29
44
  edges = [e for e in graph.get("edges", [])
@@ -76,11 +91,16 @@ def to_markdown(graph: Dict) -> str:
76
91
  ""]
77
92
 
78
93
  # ---- entry points
79
- routes = sorted(((r["method"], r["path"], fn) for fl in fns.values() for fn in fl
80
- for r in fn.get("routes") or []), key=lambda x: (x[1], x[0]))
94
+ routes = set()
95
+ for n in nodes.values():
96
+ for r in n.get("routes") or []:
97
+ if n["kind"] == "file": # inline handler, e.g. a Laravel route closure
98
+ routes.add((r["method"], r["path"], f"`{n['file']}` (inline)"))
99
+ elif n["kind"] in ("function", "method"):
100
+ routes.add((r["method"], r["path"], f"`{n['file']}:{n['label']}()`"))
81
101
  if routes:
82
102
  L += ["## HTTP routes", "", "| Method | Path | Handler |", "|---|---|---|"]
83
- L += [f"| {m} | `{p}` | `{fn['file']}:{fn['label']}()` |" for m, p, fn in routes]
103
+ L += [f"| {m} | `{p}` | {h} |" for m, p, h in sorted(routes, key=lambda x: (x[1], x[0], x[2]))]
84
104
  L.append("")
85
105
 
86
106
  # ---- data model
@@ -106,6 +126,7 @@ def to_markdown(graph: Dict) -> str:
106
126
  by_layer[f.get("layer") or "unknown"].append(f)
107
127
  for layer in sorted(by_layer, key=layer_rank):
108
128
  L += [f"### {LAYER_NAME.get(layer, layer)}", ""]
129
+ quiet = [] # files with nothing to say beyond their name
109
130
  for f in by_layer[layer]:
110
131
  path = f["file"]
111
132
  members = [f["id"]] + [fn["id"] for fn in fns.get(path, [])]
@@ -113,6 +134,9 @@ def to_markdown(graph: Dict) -> str:
113
134
  if owner(e["target"]) != f["id"]]
114
135
  heard = [ref(owner(e["source"])) for m in members for e in in_e[m]
115
136
  if owner(e["source"]) != f["id"]]
137
+ if not (talks or heard or fns.get(path) or f.get("warnings")):
138
+ quiet.append(f"`{path}`")
139
+ continue
116
140
  L.append(f"#### `{path}` ({f.get('language', '?')}, {f.get('loc', 0)} lines)")
117
141
  if talks:
118
142
  L.append("Uses: " + _short(talks))
@@ -125,28 +149,30 @@ def to_markdown(graph: Dict) -> str:
125
149
  params = ", ".join(fn.get("params") or [])
126
150
  cls = f"{fn['parentClass']}." if fn.get("parentClass") else ""
127
151
  line = f"- `{cls}{fn['label']}({params})` L{fn.get('line', '?')}"
128
- if fn.get("description"):
129
- line += f": {fn['description']}"
152
+ if _desc(fn):
153
+ line += f": {_desc(fn)}"
130
154
  calls = [ref(e["target"], path) for e in out_e[fn["id"]] if e["kind"] != "db"]
131
155
  tbls = [nodes[e["target"]]["label"] for e in out_e[fn["id"]] if e["kind"] == "db"]
132
156
  callers = [ref(e["source"], path) for e in in_e[fn["id"]]]
133
157
  extra = []
134
158
  if calls:
135
159
  extra.append("calls " + _short(calls))
136
- if tbls and not fn.get("description"): # descriptions already name their tables
160
+ if tbls and not all(t in _desc(fn) for t in tbls): # skip if the description names them
137
161
  extra.append("tables " + _short(tbls))
138
162
  if callers:
139
163
  extra.append("called by " + _short(callers))
140
164
  if fn.get("cycle"):
141
165
  extra.append("in a call cycle")
142
- elif fn.get("orphan"):
143
- extra.append("no callers found")
166
+ # no "no callers found": framework hooks (up(), boot(), form())
167
+ # are called by the framework, so it would mislead more than help
144
168
  if extra:
145
169
  line += " | " + "; ".join(extra)
146
170
  L.append(line)
147
171
  if len(fl) > FNS_PER_FILE:
148
172
  L.append(f"- … {len(fl) - FNS_PER_FILE} more functions")
149
173
  L.append("")
174
+ if quiet:
175
+ L += ["Other files: " + _short(quiet, 40), ""]
150
176
 
151
177
  # ---- things to be careful about
152
178
  notes = []
codebread/models.py CHANGED
@@ -58,6 +58,9 @@ class TableInfo:
58
58
  fields: List[str] = field(default_factory=list)
59
59
  line: int = 0
60
60
  source: str = "" # file that defines it
61
+ # candidate names when `name` is only a guess (e.g. Laravel's pluralised
62
+ # Eloquent table names); resolved against real tables in build_graph
63
+ aliases: List[str] = field(default_factory=list)
61
64
 
62
65
 
63
66
  @dataclass
@@ -15,6 +15,7 @@ KEYWORDS = {
15
15
  "try", "throw", "case", "using", "lock", "foreach", "select", "func",
16
16
  "defer", "go", "range", "elsif", "unless", "until", "when", "match",
17
17
  "sizeof", "typeof", "yield", "puts", "print", "println", "printf",
18
+ "function", "fn", "use", # PHP closures: `function () use ($x)`, `fn () =>`
18
19
  }
19
20
 
20
21
  SQL_RE = re.compile(
@@ -23,6 +24,100 @@ SQL_RE = re.compile(
23
24
  re.IGNORECASE)
24
25
  CALL_RE = re.compile(r"(?<![\w$.:])([A-Za-z_][\w]*)\s*\(")
25
26
 
27
+ # ---- PHP / Laravel ------------------------------------------------------
28
+ # CALL_RE skips `X::f(` and would read `$obj->label(` as a call to any
29
+ # function named label. In PHP only these member calls are trustworthy:
30
+ PHP_MEMBER_CALL_RE = re.compile(
31
+ r"(?:\$this->|\b(?:self|static|parent)::|\b[A-Z]\w*::)(\w+)\s*\(")
32
+ PHP_USE_RE = re.compile(r"^\s*use\s+([A-Za-z_][\w\\]*\\\w+)\s*(?:as\s+\w+\s*)?;",
33
+ re.MULTILINE)
34
+ PHP_VIEW_RE = re.compile(r"\bview\s*\(\s*['\"]([\w.\-/]+)['\"]")
35
+ # Eloquent: Booking::where(...), Booking::create(...), DB::table('bookings')
36
+ PHP_ORM_RE = re.compile(
37
+ r"\b([A-Z]\w*)::(query|where\w*|find\w*|first\w*|create|insert\w*|"
38
+ r"update\w*|upsert|destroy|all|with|latest|oldest|orderBy|count|sum|"
39
+ r"pluck|paginate|select)\s*\(")
40
+ PHP_DB_TABLE_RE = re.compile(r"\bDB::table\(\s*['\"](\w+)['\"]")
41
+ ORM_OP = {"create": "insert", "insert": "insert", "upsert": "insert",
42
+ "update": "update", "destroy": "delete"}
43
+ NOT_MODELS = {"DB", "Route", "Schema", "Carbon", "Str", "Arr", "Auth", "Cache",
44
+ "Log", "Http", "Storage", "Validator", "Hash", "Session", "Config",
45
+ "Gate", "Mail", "Event", "Queue", "App", "Request", "Response"}
46
+ SCHEMA_RE = re.compile(r"Schema::(create|table)\s*\(\s*['\"](\w+)['\"]")
47
+ BLUEPRINT_COL_RE = re.compile(r"\$\w+->(\w+)\s*\(\s*(?:['\"](\w+)['\"])?")
48
+ BLUEPRINT_IMPLIED = {"id": ["id"], "timestamps": ["created_at", "updated_at"],
49
+ "timestampsTz": ["created_at", "updated_at"],
50
+ "softDeletes": ["deleted_at"], "rememberToken": ["remember_token"]}
51
+ BLUEPRINT_SKIP = {"index", "unique", "primary", "foreign", "dropColumn",
52
+ "dropForeign", "dropIndex", "renameColumn", "fullText",
53
+ "spatialIndex", "comment", "engine", "charset", "collation"}
54
+ ELOQUENT_RE = re.compile(
55
+ r"^\s*(?:final\s+)?class\s+(\w+)\s+extends\s+(?:Model|Authenticatable|Pivot)\b",
56
+ re.MULTILINE)
57
+ PHP_TABLE_PROP_RE = re.compile(r"\$table\s*=\s*['\"](\w+)['\"]")
58
+
59
+
60
+ def _snake(name: str) -> str:
61
+ return re.sub(r"(?<=[a-z0-9])([A-Z])", r"_\1", name).lower()
62
+
63
+
64
+ def _plural_candidates(snake: str) -> List[str]:
65
+ """Laravel's table guess for a model, best first (Booking -> bookings)."""
66
+ if snake.endswith("y") and snake[-2:-1] not in "aeiou":
67
+ first = snake[:-1] + "ies"
68
+ elif snake.endswith(("s", "x", "z", "ch", "sh")):
69
+ first = snake + "es"
70
+ else:
71
+ first = snake + "s"
72
+ return [first, snake] # bare name covers uncountables like `equipment`
73
+
74
+
75
+ def _php_member_calls(body: str) -> List[str]:
76
+ return [m.group(1) for m in PHP_MEMBER_CALL_RE.finditer(body)]
77
+
78
+
79
+ def _php_db_refs(body: str, line: int) -> List[DbRef]:
80
+ refs = [DbRef(table=m.group(1), op="query", via="sql", line=line)
81
+ for m in PHP_DB_TABLE_RE.finditer(body)]
82
+ for m in PHP_ORM_RE.finditer(body):
83
+ if m.group(1) in NOT_MODELS:
84
+ continue
85
+ op = next((v for k, v in ORM_OP.items() if m.group(2).startswith(k)), "select")
86
+ refs.append(DbRef(table=m.group(1), op=op, via="orm-guess", line=line))
87
+ return refs
88
+
89
+
90
+ def parse_laravel(info: FileInfo, text: str) -> None:
91
+ """File-level Laravel facts: `use` imports, view() links, migrations,
92
+ Eloquent models. Cheap regex passes; nothing matches in non-Laravel PHP."""
93
+ info.imports.extend(m.group(1).replace("\\", "/") for m in PHP_USE_RE.finditer(text))
94
+ for m in PHP_VIEW_RE.finditer(text):
95
+ info.links.append("resources/views/" + m.group(1).replace(".", "/") + ".blade.php")
96
+
97
+ for m in SCHEMA_RE.finditer(text):
98
+ end = _brace_end_line(text, m.end())
99
+ block = "\n".join(text.split("\n")[_line_of(text, m.start()) - 1:end or None])
100
+ cols: List[str] = []
101
+ for c in BLUEPRINT_COL_RE.finditer(block):
102
+ method, col = c.group(1), c.group(2)
103
+ if method in BLUEPRINT_SKIP:
104
+ continue
105
+ for name in BLUEPRINT_IMPLIED.get(method, [col] if col else []):
106
+ if name not in cols:
107
+ cols.append(name)
108
+ # Schema::table() only alters: it adds columns but doesn't define the table
109
+ info.tables.append(TableInfo(name=m.group(2), fields=cols[:40],
110
+ line=_line_of(text, m.start()),
111
+ source=info.path if m.group(1) == "create" else ""))
112
+
113
+ for m in ELOQUENT_RE.finditer(text):
114
+ cls = m.group(1)
115
+ explicit = PHP_TABLE_PROP_RE.search(text)
116
+ cands = [explicit.group(1)] if explicit else _plural_candidates(_snake(cls))
117
+ # no source: the migration defines the table; the model links to it by edge
118
+ info.tables.append(TableInfo(name=cands[0], model=cls, aliases=cands,
119
+ line=_line_of(text, m.start())))
120
+
26
121
  LANG_PATTERNS: Dict[str, Dict[str, Pattern]] = {
27
122
  "go": {
28
123
  "function": re.compile(
@@ -173,8 +268,16 @@ def parse_generic(info: FileInfo, text: str, language: str) -> None:
173
268
  cname = cm.group(1)
174
269
  if cname in KEYWORDS or cname == name or cname in seen:
175
270
  continue
271
+ if language == "php" and body[max(0, cm.start() - 2):cm.start()] == "->":
272
+ continue # $obj->method(): unknown receiver, see PHP_MEMBER_CALL_RE
176
273
  seen.add(cname)
177
274
  fn.calls.append(cname)
275
+ if language == "php":
276
+ for cname in _php_member_calls(body):
277
+ if cname not in seen and cname != name and cname not in KEYWORDS:
278
+ seen.add(cname)
279
+ fn.calls.append(cname)
280
+ fn.db_refs.extend(_php_db_refs(body, start_line))
178
281
  fn.calls = fn.calls[:40]
179
282
  fn.description = describe(fn, body[:400])
180
283
  info.functions.append(fn)
@@ -196,6 +299,10 @@ def parse_generic(info: FileInfo, text: str, language: str) -> None:
196
299
 
197
300
  if language in ("php", "ruby"):
198
301
  extract_toplevel(info, text)
302
+ if language == "php":
303
+ parse_laravel(info, text)
304
+ info.imports = sorted(set(info.imports))[:60]
305
+ info.links = list(dict.fromkeys(info.links))[:30]
199
306
 
200
307
 
201
308
  PAGE_LINK_RE = re.compile(
@@ -218,14 +325,23 @@ def extract_toplevel(info: FileInfo, text: str) -> None:
218
325
  def outside(line: int) -> bool:
219
326
  return not any(s <= line <= e for s, e in fn_ranges)
220
327
 
328
+ is_php = info.language == "php"
221
329
  seen = set()
222
330
  for m in CALL_RE.finditer(text):
223
331
  name = m.group(1)
224
332
  if name in TOPLEVEL_NOISE or name in seen:
225
333
  continue
334
+ if is_php and text[max(0, m.start() - 2):m.start()] == "->":
335
+ continue
226
336
  if outside(_line_of(text, m.start())):
227
337
  seen.add(name)
228
338
  info.calls.append(name)
339
+ if is_php: # routes/web.php closures call App\X::y() at top level
340
+ for m in PHP_MEMBER_CALL_RE.finditer(text):
341
+ name = m.group(1)
342
+ if name not in seen and name not in TOPLEVEL_NOISE and outside(_line_of(text, m.start())):
343
+ seen.add(name)
344
+ info.calls.append(name)
229
345
  info.calls = info.calls[:40]
230
346
 
231
347
  seen_l = set()
codebread/scanner.py CHANGED
@@ -85,32 +85,45 @@ class _SimpleGitignore:
85
85
 
86
86
 
87
87
  class GitignoreStack:
88
- """Loads the root .gitignore (via pathspec when available)."""
88
+ """Every .gitignore in the tree, each scoped to its own folder — like git.
89
+
90
+ Frameworks lean on nested ones (Laravel's storage/framework/views/.gitignore
91
+ is just `*` + `!.gitignore`), so reading only the root file lets compiled
92
+ caches flood the scan. Uses pathspec when available.
93
+ """
89
94
 
90
95
  def __init__(self, root: str):
91
96
  self.root = root
92
- self._spec = None
93
- self._simple: Optional[_SimpleGitignore] = None
94
- gi = os.path.join(root, ".gitignore")
95
- if os.path.isfile(gi):
96
- try:
97
- with open(gi, "r", encoding="utf-8", errors="replace") as f:
98
- lines = f.readlines()
99
- except OSError:
100
- lines = []
101
- try:
102
- import pathspec # type: ignore
103
- self._spec = pathspec.PathSpec.from_lines("gitwildmatch", lines)
104
- except ImportError:
105
- self._simple = _SimpleGitignore(lines)
97
+ self._rules: List[Tuple[str, object]] = [] # (folder rel to root, matcher)
98
+ self.load_dir(root, "")
99
+
100
+ def load_dir(self, abs_dir: str, rel_dir: str) -> None:
101
+ gi = os.path.join(abs_dir, ".gitignore")
102
+ if not os.path.isfile(gi):
103
+ return
104
+ try:
105
+ with open(gi, "r", encoding="utf-8", errors="replace") as f:
106
+ lines = f.readlines()
107
+ except OSError:
108
+ return
109
+ try:
110
+ import pathspec # type: ignore
111
+ matcher: object = pathspec.GitIgnoreSpec.from_lines(lines)
112
+ except ImportError:
113
+ matcher = _SimpleGitignore(lines)
114
+ self._rules.append((rel_dir.replace(os.sep, "/").strip("/"), matcher))
106
115
 
107
116
  def ignored(self, rel_path: str, is_dir: bool) -> bool:
108
117
  rel = rel_path.replace(os.sep, "/")
109
- if self._spec is not None:
110
- probe = rel + "/" if is_dir else rel
111
- return self._spec.match_file(probe)
112
- if self._simple is not None:
113
- return self._simple.matches(rel, is_dir)
118
+ for base, m in self._rules:
119
+ if base and not rel.startswith(base + "/"):
120
+ continue
121
+ sub = rel[len(base) + 1:] if base else rel
122
+ if isinstance(m, _SimpleGitignore):
123
+ if m.matches(sub, is_dir):
124
+ return True
125
+ elif m.match_file(sub + "/" if is_dir else sub):
126
+ return True
114
127
  return False
115
128
 
116
129
 
@@ -157,6 +170,7 @@ def scan(root: str, max_file_kb: int = 1024) -> Dict:
157
170
  continue
158
171
  if gitignore.ignored(rel, True):
159
172
  continue
173
+ gitignore.load_dir(entry.path, rel)
160
174
  child = walk_dir(entry.path, rel)
161
175
  # keep dirs that contain anything (incl. warning stubs)
162
176
  if child["children"] or child.get("warning"):
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codebread
3
- Version: 1.1.0
3
+ Version: 1.1.3
4
4
  Summary: Interactive codebase analyzer & visualizer — slice open a project and see how it's wired together.
5
5
  Author: Danial Irsyad
6
6
  License: MIT
7
- Keywords: codebase,analyzer,visualizer,call-graph,architecture
7
+ Keywords: codebase,analyzer,visualizer,call-graph,architecture,dependency-graph,code-visualization,static-analysis,code-map
8
8
  Requires-Python: >=3.9
9
9
  Description-Content-Type: text/markdown
10
10
  License-File: LICENSE
@@ -23,6 +23,11 @@ Dynamic: license-file
23
23
  Files, functions, API routes and database tables, mapped in your browser.
24
24
  </p>
25
25
 
26
+ <p align="center">
27
+ An interactive codebase visualizer: call graph, dependency map and API-to-database tracing<br>
28
+ for Python, JavaScript, TypeScript, PHP, Java, Go, C# and Ruby.
29
+ </p>
30
+
26
31
  <p align="center">
27
32
  <a href="https://pypi.org/project/codebread/"><img alt="PyPI" src="https://img.shields.io/pypi/v/codebread?color=b0541c&label=pypi"></a>
28
33
  <a href="https://github.com/honow48-tech/CodeBread/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/honow48-tech/CodeBread/actions/workflows/ci.yml/badge.svg"></a>
@@ -2,25 +2,25 @@ codebread/__init__.py,sha256=_39lK3zPAtLdbqIfJqITPeUkeIbS7gSbhMBcNHcjrpg,1400
2
2
  codebread/analyzer.py,sha256=072OhQjz9fxO4CtcjU7ShjAg2AXsFTMHnsmy0E__f24,3576
3
3
  codebread/classifier.py,sha256=jehD-XQ80CP_O4x9YRi4JQqzd4AaHxKpzI83wLG1yvM,4067
4
4
  codebread/cli.py,sha256=IyrJwXbNSuuHTZCFxyEfIqsibDp_SCApNOz7qkZQ2XI,4949
5
- codebread/connections.py,sha256=6kk4FX78p_UmX5d0vkGllZK_igz4mkTVfWNXjdQrCyo,16733
6
- codebread/context.py,sha256=2E1idSNdWfYTFLT3bvCAvQU93VNU1P6Ya4kRQeXUsp0,7861
5
+ codebread/connections.py,sha256=bEMKmrae63tTujuhLOc44zlDGXHELXiiYNWxAkxsS48,18863
6
+ codebread/context.py,sha256=asN8oe4yZmPi04Avx8G0D_Huecgr6jBhz2RXqEf_DaE,8942
7
7
  codebread/deobfuscate.py,sha256=AjtEQrfyqln42X0i4TkfdoG5vSJ_U6KuIRlwoEXfX3o,26027
8
8
  codebread/diff.py,sha256=5SDGW56yo2zyCEeeDD_CZWiQN6Ls7knDI1j_Zwi-JvQ,4960
9
9
  codebread/export.py,sha256=rKYmzuSEXYDrgcItnhdyXI_lo0LN2PSoMe0hYElzU88,1512
10
10
  codebread/languages.py,sha256=SKHy9v7QPreWZG6FbuX3B0-kq1jqVn-rmwNqJ9ESFqo,4494
11
- codebread/models.py,sha256=Bd3W-DfJo2ZjasKrCWfyUkqUEkHgOPwt95Wf_o1_EYk,4322
12
- codebread/scanner.py,sha256=bEXSaoENmJ-C_oabj2IF1aWs9-LaIqMc80xXQvo7-eg,8020
11
+ codebread/models.py,sha256=pXKnC65tDnndCi3axwTK40q8WKSUX-kAtE1JImUYumA,4525
12
+ codebread/scanner.py,sha256=8wu4HjIp2J2PEyfiOEYUNUrqRcul-NTZQjudNGyL6ds,8618
13
13
  codebread/server.py,sha256=Uh7L2n0pumpt1UNSlC1mvcjg6yBl1LAxTmbz9cmsilA,4597
14
14
  codebread/parsers/__init__.py,sha256=ovsvmXGClshiUlDdx6mkBmRu8__OZIqYos6-1eLb9eM,2285
15
- codebread/parsers/generic_parser.py,sha256=lz-wLsmQYERoDRM27lhEP3cvNYbomHfA--VI_3LmJ5E,11966
15
+ codebread/parsers/generic_parser.py,sha256=-dvujzkz2rJwN7eCitKqbk9WUXrBoudTLgWREk2heI0,17966
16
16
  codebread/parsers/javascript_parser.py,sha256=lVIlydQ3--Owd78816XIM77NntfQ97DeLzYvPrjd5GE,15916
17
17
  codebread/parsers/python_parser.py,sha256=QnhCLx01bKYLYqav33cI27qbEQNPUiXnS5uN7cb8poQ,11671
18
18
  codebread/web/app.js,sha256=cGoa971zaMHpIZxVngU8bY4VM9dPO8xXhSvEXUdkh_M,102265
19
19
  codebread/web/index.html,sha256=Q9PLFj2nYzAgUvSkeVEbrVS9VdACPUkLM9lyMvCtZ_A,12324
20
20
  codebread/web/style.css,sha256=UFwhbHC5HpgAjl5-sziFGL1pIQbGYNTkUrJmNlRyr8A,42244
21
- codebread-1.1.0.dist-info/licenses/LICENSE,sha256=okINupIvMgiuOlvMVpsM9DT5jb6QQmKBd--yfZq4bjc,1070
22
- codebread-1.1.0.dist-info/METADATA,sha256=9NW6xZbV7I42Y3NVyYXHobf_L4Si4AT4djKpHn3rAfw,11000
23
- codebread-1.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
24
- codebread-1.1.0.dist-info/entry_points.txt,sha256=ItG61aUP1YTvA_zA-jlUshBc5BPpJDpCyVnsyaxNXKA,49
25
- codebread-1.1.0.dist-info/top_level.txt,sha256=KctFB7UQb4tN9YotaJD95GuHWIQuoO5VqyLdo_gEswQ,10
26
- codebread-1.1.0.dist-info/RECORD,,
21
+ codebread-1.1.3.dist-info/licenses/LICENSE,sha256=okINupIvMgiuOlvMVpsM9DT5jb6QQmKBd--yfZq4bjc,1070
22
+ codebread-1.1.3.dist-info/METADATA,sha256=_C-gmGtiUMEkEJ3Bz2PNkOdP8u8JKAZYr-U0S4l7TQc,11249
23
+ codebread-1.1.3.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
24
+ codebread-1.1.3.dist-info/entry_points.txt,sha256=ItG61aUP1YTvA_zA-jlUshBc5BPpJDpCyVnsyaxNXKA,49
25
+ codebread-1.1.3.dist-info/top_level.txt,sha256=KctFB7UQb4tN9YotaJD95GuHWIQuoO5VqyLdo_gEswQ,10
26
+ codebread-1.1.3.dist-info/RECORD,,