graphlint 0.3.2__tar.gz → 0.3.3__tar.gz

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.
Files changed (116) hide show
  1. {graphlint-0.3.2 → graphlint-0.3.3}/CHANGELOG.md +11 -0
  2. {graphlint-0.3.2/graphlint.egg-info → graphlint-0.3.3}/PKG-INFO +1 -1
  3. {graphlint-0.3.2 → graphlint-0.3.3}/graphlint/__init__.py +1 -1
  4. {graphlint-0.3.2 → graphlint-0.3.3}/graphlint/analyzer/_graph_algo.py +85 -48
  5. {graphlint-0.3.2 → graphlint-0.3.3/graphlint.egg-info}/PKG-INFO +1 -1
  6. {graphlint-0.3.2 → graphlint-0.3.3}/pyproject.toml +1 -1
  7. {graphlint-0.3.2 → graphlint-0.3.3}/CONTRIBUTING.md +0 -0
  8. {graphlint-0.3.2 → graphlint-0.3.3}/LICENSE +0 -0
  9. {graphlint-0.3.2 → graphlint-0.3.3}/MANIFEST.in +0 -0
  10. {graphlint-0.3.2 → graphlint-0.3.3}/README.md +0 -0
  11. {graphlint-0.3.2 → graphlint-0.3.3}/README_PYPI.md +0 -0
  12. {graphlint-0.3.2 → graphlint-0.3.3}/docs/en/api/build.md +0 -0
  13. {graphlint-0.3.2 → graphlint-0.3.3}/docs/en/api/configure.md +0 -0
  14. {graphlint-0.3.2 → graphlint-0.3.3}/docs/en/api/exceptions.md +0 -0
  15. {graphlint-0.3.2 → graphlint-0.3.3}/docs/en/api/query.md +0 -0
  16. {graphlint-0.3.2 → graphlint-0.3.3}/docs/en/architecture/overview.md +0 -0
  17. {graphlint-0.3.2 → graphlint-0.3.3}/docs/en/cli/usage.md +0 -0
  18. {graphlint-0.3.2 → graphlint-0.3.3}/docs/en/guide/agent-integration.md +0 -0
  19. {graphlint-0.3.2 → graphlint-0.3.3}/docs/en/guide/configuration.md +0 -0
  20. {graphlint-0.3.2 → graphlint-0.3.3}/docs/en/guide/entry-detection.md +0 -0
  21. {graphlint-0.3.2 → graphlint-0.3.3}/docs/en/guide/getting-started.md +0 -0
  22. {graphlint-0.3.2 → graphlint-0.3.3}/docs/en/guide/warnings.md +0 -0
  23. {graphlint-0.3.2 → graphlint-0.3.3}/docs/en/index.md +0 -0
  24. {graphlint-0.3.2 → graphlint-0.3.3}/docs/zh/README.md +0 -0
  25. {graphlint-0.3.2 → graphlint-0.3.3}/docs/zh/api/build.md +0 -0
  26. {graphlint-0.3.2 → graphlint-0.3.3}/docs/zh/api/configure.md +0 -0
  27. {graphlint-0.3.2 → graphlint-0.3.3}/docs/zh/api/exceptions.md +0 -0
  28. {graphlint-0.3.2 → graphlint-0.3.3}/docs/zh/api/query.md +0 -0
  29. {graphlint-0.3.2 → graphlint-0.3.3}/docs/zh/architecture/overview.md +0 -0
  30. {graphlint-0.3.2 → graphlint-0.3.3}/docs/zh/cli/usage.md +0 -0
  31. {graphlint-0.3.2 → graphlint-0.3.3}/docs/zh/guide/agent-integration.md +0 -0
  32. {graphlint-0.3.2 → graphlint-0.3.3}/docs/zh/guide/configuration.md +0 -0
  33. {graphlint-0.3.2 → graphlint-0.3.3}/docs/zh/guide/entry-detection.md +0 -0
  34. {graphlint-0.3.2 → graphlint-0.3.3}/docs/zh/guide/getting-started.md +0 -0
  35. {graphlint-0.3.2 → graphlint-0.3.3}/docs/zh/guide/warnings.md +0 -0
  36. {graphlint-0.3.2 → graphlint-0.3.3}/docs/zh/index.md +0 -0
  37. {graphlint-0.3.2 → graphlint-0.3.3}/graphlint/agent_tools.py +0 -0
  38. {graphlint-0.3.2 → graphlint-0.3.3}/graphlint/analyzer/__init__.py +0 -0
  39. {graphlint-0.3.2 → graphlint-0.3.3}/graphlint/analyzer/_types.py +0 -0
  40. {graphlint-0.3.2 → graphlint-0.3.3}/graphlint/analyzer/graph.py +0 -0
  41. {graphlint-0.3.2 → graphlint-0.3.3}/graphlint/analyzer/language/__init__.py +0 -0
  42. {graphlint-0.3.2 → graphlint-0.3.3}/graphlint/analyzer/language/base.py +0 -0
  43. {graphlint-0.3.2 → graphlint-0.3.3}/graphlint/analyzer/language/python/__init__.py +0 -0
  44. {graphlint-0.3.2 → graphlint-0.3.3}/graphlint/analyzer/language/python/constants.py +0 -0
  45. {graphlint-0.3.2 → graphlint-0.3.3}/graphlint/analyzer/language/python/decorators.py +0 -0
  46. {graphlint-0.3.2 → graphlint-0.3.3}/graphlint/analyzer/language/python/entry.py +0 -0
  47. {graphlint-0.3.2 → graphlint-0.3.3}/graphlint/analyzer/language/python/imports.py +0 -0
  48. {graphlint-0.3.2 → graphlint-0.3.3}/graphlint/analyzer/language/python/parser.py +0 -0
  49. {graphlint-0.3.2 → graphlint-0.3.3}/graphlint/analyzer/language/python/visitor.py +0 -0
  50. {graphlint-0.3.2 → graphlint-0.3.3}/graphlint/analyzer/language/registry.py +0 -0
  51. {graphlint-0.3.2 → graphlint-0.3.3}/graphlint/analyzer/language/rust/__init__.py +0 -0
  52. {graphlint-0.3.2 → graphlint-0.3.3}/graphlint/analyzer/language/rust/constants.py +0 -0
  53. {graphlint-0.3.2 → graphlint-0.3.3}/graphlint/analyzer/language/rust/entry.py +0 -0
  54. {graphlint-0.3.2 → graphlint-0.3.3}/graphlint/analyzer/language/rust/imports.py +0 -0
  55. {graphlint-0.3.2 → graphlint-0.3.3}/graphlint/analyzer/language/rust/parser.py +0 -0
  56. {graphlint-0.3.2 → graphlint-0.3.3}/graphlint/analyzer/language/rust/visitor.py +0 -0
  57. {graphlint-0.3.2 → graphlint-0.3.3}/graphlint/analyzer/warnings.py +0 -0
  58. {graphlint-0.3.2 → graphlint-0.3.3}/graphlint/api.py +0 -0
  59. {graphlint-0.3.2 → graphlint-0.3.3}/graphlint/cli.py +0 -0
  60. {graphlint-0.3.2 → graphlint-0.3.3}/graphlint/config/__init__.py +0 -0
  61. {graphlint-0.3.2 → graphlint-0.3.3}/graphlint/config/defaults.py +0 -0
  62. {graphlint-0.3.2 → graphlint-0.3.3}/graphlint/config/manager.py +0 -0
  63. {graphlint-0.3.2 → graphlint-0.3.3}/graphlint/exceptions.py +0 -0
  64. {graphlint-0.3.2 → graphlint-0.3.3}/graphlint/i18n/__init__.py +0 -0
  65. {graphlint-0.3.2 → graphlint-0.3.3}/graphlint/i18n/en.py +0 -0
  66. {graphlint-0.3.2 → graphlint-0.3.3}/graphlint/i18n/zh_CN.py +0 -0
  67. {graphlint-0.3.2 → graphlint-0.3.3}/graphlint/incremental/__init__.py +0 -0
  68. {graphlint-0.3.2 → graphlint-0.3.3}/graphlint/incremental/_db_ops.py +0 -0
  69. {graphlint-0.3.2 → graphlint-0.3.3}/graphlint/incremental/indexer.py +0 -0
  70. {graphlint-0.3.2 → graphlint-0.3.3}/graphlint/params.py +0 -0
  71. {graphlint-0.3.2 → graphlint-0.3.3}/graphlint/query/__init__.py +0 -0
  72. {graphlint-0.3.2 → graphlint-0.3.3}/graphlint/query/engine.py +0 -0
  73. {graphlint-0.3.2 → graphlint-0.3.3}/graphlint/query/formatter.py +0 -0
  74. {graphlint-0.3.2 → graphlint-0.3.3}/graphlint/query/volume.py +0 -0
  75. {graphlint-0.3.2 → graphlint-0.3.3}/graphlint/storage/__init__.py +0 -0
  76. {graphlint-0.3.2 → graphlint-0.3.3}/graphlint/storage/db.py +0 -0
  77. {graphlint-0.3.2 → graphlint-0.3.3}/graphlint/storage/hashing.py +0 -0
  78. {graphlint-0.3.2 → graphlint-0.3.3}/graphlint/storage/schema.py +0 -0
  79. {graphlint-0.3.2 → graphlint-0.3.3}/graphlint.egg-info/SOURCES.txt +0 -0
  80. {graphlint-0.3.2 → graphlint-0.3.3}/graphlint.egg-info/dependency_links.txt +0 -0
  81. {graphlint-0.3.2 → graphlint-0.3.3}/graphlint.egg-info/entry_points.txt +0 -0
  82. {graphlint-0.3.2 → graphlint-0.3.3}/graphlint.egg-info/requires.txt +0 -0
  83. {graphlint-0.3.2 → graphlint-0.3.3}/graphlint.egg-info/top_level.txt +0 -0
  84. {graphlint-0.3.2 → graphlint-0.3.3}/setup.cfg +0 -0
  85. {graphlint-0.3.2 → graphlint-0.3.3}/tests/__init__.py +0 -0
  86. {graphlint-0.3.2 → graphlint-0.3.3}/tests/integration/__init__.py +0 -0
  87. {graphlint-0.3.2 → graphlint-0.3.3}/tests/integration/test_api.py +0 -0
  88. {graphlint-0.3.2 → graphlint-0.3.3}/tests/integration/test_cli.py +0 -0
  89. {graphlint-0.3.2 → graphlint-0.3.3}/tests/integration/test_data_transfer.py +0 -0
  90. {graphlint-0.3.2 → graphlint-0.3.3}/tests/integration/test_dead_code_query.py +0 -0
  91. {graphlint-0.3.2 → graphlint-0.3.3}/tests/integration/test_full_pipeline.py +0 -0
  92. {graphlint-0.3.2 → graphlint-0.3.3}/tests/integration/test_incremental.py +0 -0
  93. {graphlint-0.3.2 → graphlint-0.3.3}/tests/integration/test_json_output.py +0 -0
  94. {graphlint-0.3.2 → graphlint-0.3.3}/tests/performance/__init__.py +0 -0
  95. {graphlint-0.3.2 → graphlint-0.3.3}/tests/performance/test_large_codebase.py +0 -0
  96. {graphlint-0.3.2 → graphlint-0.3.3}/tests/unit/__init__.py +0 -0
  97. {graphlint-0.3.2 → graphlint-0.3.3}/tests/unit/test_auto_build.py +0 -0
  98. {graphlint-0.3.2 → graphlint-0.3.3}/tests/unit/test_config.py +0 -0
  99. {graphlint-0.3.2 → graphlint-0.3.3}/tests/unit/test_db.py +0 -0
  100. {graphlint-0.3.2 → graphlint-0.3.3}/tests/unit/test_db_ops.py +0 -0
  101. {graphlint-0.3.2 → graphlint-0.3.3}/tests/unit/test_decorators.py +0 -0
  102. {graphlint-0.3.2 → graphlint-0.3.3}/tests/unit/test_engine.py +0 -0
  103. {graphlint-0.3.2 → graphlint-0.3.3}/tests/unit/test_entry_detect.py +0 -0
  104. {graphlint-0.3.2 → graphlint-0.3.3}/tests/unit/test_exceptions.py +0 -0
  105. {graphlint-0.3.2 → graphlint-0.3.3}/tests/unit/test_formatter.py +0 -0
  106. {graphlint-0.3.2 → graphlint-0.3.3}/tests/unit/test_graph.py +0 -0
  107. {graphlint-0.3.2 → graphlint-0.3.3}/tests/unit/test_graph_algo.py +0 -0
  108. {graphlint-0.3.2 → graphlint-0.3.3}/tests/unit/test_hashing.py +0 -0
  109. {graphlint-0.3.2 → graphlint-0.3.3}/tests/unit/test_i18n.py +0 -0
  110. {graphlint-0.3.2 → graphlint-0.3.3}/tests/unit/test_imports.py +0 -0
  111. {graphlint-0.3.2 → graphlint-0.3.3}/tests/unit/test_mock_boundary.py +0 -0
  112. {graphlint-0.3.2 → graphlint-0.3.3}/tests/unit/test_params.py +0 -0
  113. {graphlint-0.3.2 → graphlint-0.3.3}/tests/unit/test_parser.py +0 -0
  114. {graphlint-0.3.2 → graphlint-0.3.3}/tests/unit/test_schema.py +0 -0
  115. {graphlint-0.3.2 → graphlint-0.3.3}/tests/unit/test_volume.py +0 -0
  116. {graphlint-0.3.2 → graphlint-0.3.3}/tests/unit/test_warnings.py +0 -0
@@ -2,6 +2,17 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.3.3] - 2026-07-25
6
+
7
+ ### Changed
8
+ - Pre-built entry indexes (`nid_to_entries`, `fid_to_entries`) for O(1)
9
+ per-component entry matching, eliminating repeated linear scans in
10
+ `_match_entries` during connected component analysis
11
+ - Connected component BFS now collects reachable node seeds in a single
12
+ traversal pass instead of a post-hoc set comprehension
13
+ - Dead-code component merge loop replaced with BFS-based group merging,
14
+ eliminating repeated list scans and improving large-codebase performance
15
+
5
16
  ## [0.3.2] - 2026-07-25
6
17
 
7
18
  ### Changed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: graphlint
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Summary: A static analysis tool for identifying dead code in a codebase. It provides Python API and CLI for use by agents.
5
5
  Author-email: Yutong Zou <yutong.zou.24@alumni.ucl.ac.uk>
6
6
  License-Expression: MIT
@@ -3,7 +3,7 @@
3
3
 
4
4
  from typing import Any
5
5
 
6
- __version__ = "0.3.2"
6
+ __version__ = "0.3.3"
7
7
 
8
8
  __all__ = ["query", "build", "configure", "__version__"]
9
9
 
@@ -258,6 +258,17 @@ def find_connected_components(
258
258
  elif _e.target_id == 0 and _e.source_id:
259
259
  _zero_sources.add(_e.source_id)
260
260
 
261
+ # Pre-build entry indexes for O(1) per-component matching
262
+ _nid_to_entries: dict[int, list[EntryInfo]] = {}
263
+ _fid_to_entries: dict[int, list[EntryInfo]] = {}
264
+ for entry in entries:
265
+ if entry.node_id:
266
+ _nid_to_entries.setdefault(entry.node_id, []).append(entry)
267
+ elif file_id_map:
268
+ fid = file_id_map.get(entry.file_path, 0)
269
+ if fid:
270
+ _fid_to_entries.setdefault(fid, []).append(entry)
271
+
261
272
  visited: set[int] = set()
262
273
  component_map: dict[int, int] = {}
263
274
  components: list[ComponentInfo] = []
@@ -268,9 +279,13 @@ def find_connected_components(
268
279
  if nid in visited:
269
280
  continue
270
281
 
282
+ # discover component nodes and initial reachable seeds
271
283
  queue: deque[int] = deque([nid])
272
284
  visited.add(nid)
273
285
  comp_nodes: set[int] = {nid}
286
+ comp_reachable: set[int] = set()
287
+ if nid in reachable:
288
+ comp_reachable.add(nid)
274
289
 
275
290
  while queue:
276
291
  current = queue.popleft()
@@ -279,15 +294,19 @@ def find_connected_components(
279
294
  visited.add(neighbor)
280
295
  queue.append(neighbor)
281
296
  comp_nodes.add(neighbor)
297
+ if neighbor in reachable:
298
+ comp_reachable.add(neighbor)
282
299
 
283
- comp_entries = _match_entries(entries, comp_nodes, node_id_map, file_id_map)
300
+ comp_entries = _match_entries(
301
+ entries, comp_nodes, node_id_map, file_id_map,
302
+ nid_to_entries=_nid_to_entries, fid_to_entries=_fid_to_entries,
303
+ )
284
304
  has_entry = len(comp_entries) > 0
285
- comp_nodes.discard(0) # Exclude module root pseudo-node
286
- comp_reachable = {nid for nid in comp_nodes if nid in reachable}
305
+ comp_nodes.discard(0)
306
+ comp_reachable.discard(0)
287
307
  comp_unreachable = comp_nodes - comp_reachable
288
308
 
289
- # BFS expansion from reachable seed set via undirected edges.
290
- # Node 0 (module pseudo-node): only expand via read/call edges.
309
+ # BFS expansion from reachable seeds via undirected edges
291
310
  seed = comp_reachable
292
311
  if noprop_ids:
293
312
  seed = seed - noprop_ids
@@ -296,13 +315,13 @@ def find_connected_components(
296
315
  cur = q.popleft()
297
316
  if cur == 0:
298
317
  continue
299
- for nid in adj.get(cur, set()):
300
- if nid == 0 or nid in comp_reachable:
318
+ for nb in adj.get(cur, set()):
319
+ if nb == 0 or nb in comp_reachable:
301
320
  continue
302
- if nid in comp_unreachable:
303
- comp_reachable.add(nid)
304
- comp_unreachable.discard(nid)
305
- q.append(nid)
321
+ if nb in comp_unreachable:
322
+ comp_reachable.add(nb)
323
+ comp_unreachable.discard(nb)
324
+ q.append(nb)
306
325
  if cur in class_special_map:
307
326
  for sm_nid in class_special_map[cur]:
308
327
  if sm_nid in comp_unreachable:
@@ -370,51 +389,52 @@ def find_connected_components(
370
389
  component_map.update(sub_map)
371
390
  components.extend(sub_comps)
372
391
 
373
- # Post-process: merge dead code sub-components into live components.
374
- x_edges: dict[int, set[int]] = {}
392
+ # Post-process: merge dead components into live ones via inherit/decorate edges.
393
+ comp_adj: dict[int, set[int]] = {}
375
394
  for e in edges:
376
395
  if e.edge_type not in ("inherit", "decorate"):
377
396
  continue
378
397
  cs = component_map.get(e.source_id)
379
398
  ct = component_map.get(e.target_id)
380
399
  if cs is not None and ct is not None and cs != ct:
381
- x_edges.setdefault(cs, set()).add(ct)
382
- x_edges.setdefault(ct, set()).add(cs)
400
+ comp_adj.setdefault(cs, set()).add(ct)
401
+ comp_adj.setdefault(ct, set()).add(cs)
383
402
 
384
403
  comp_by_id: dict[int, ComponentInfo] = {c.component_id: c for c in components}
404
+ live_comp_ids: set[int] = {c.component_id for c in components if not c.is_dead_code}
385
405
 
386
- changed = True
387
- while changed:
388
- changed = False
389
- for comp in list(components):
390
- if not comp.is_dead_code:
391
- continue
392
- neighbors = x_edges.get(comp.component_id, set())
393
- if not neighbors:
394
- continue
395
- # Find all adjacent live components
396
- live_nbrs = []
397
- for nbr in neighbors:
398
- nbr_comp = comp_by_id.get(nbr)
399
- if nbr_comp and not nbr_comp.is_dead_code:
400
- live_nbrs.append(nbr_comp)
401
- if not live_nbrs:
406
+ # each connected group containing a live
407
+ # component merges all its dead members into the largest live one.
408
+ comp_visited: set[int] = set()
409
+ for live_cid in sorted(live_comp_ids):
410
+ if live_cid in comp_visited:
411
+ continue
412
+ group: set[int] = {live_cid}
413
+ comp_visited.add(live_cid)
414
+ q_comp: deque[int] = deque([live_cid])
415
+ while q_comp:
416
+ cur_cid = q_comp.popleft()
417
+ for nb_cid in comp_adj.get(cur_cid, set()):
418
+ if nb_cid not in comp_visited:
419
+ comp_visited.add(nb_cid)
420
+ q_comp.append(nb_cid)
421
+ group.add(nb_cid)
422
+
423
+ group_live = group & live_comp_ids
424
+ group_dead = group - group_live
425
+ if not group_dead:
426
+ continue
427
+
428
+ target_cid = max(group_live, key=lambda cid: len(comp_by_id[cid].node_ids))
429
+ target_comp = comp_by_id[target_cid]
430
+ for dead_cid in group_dead:
431
+ dead_comp = comp_by_id.pop(dead_cid, None)
432
+ if dead_comp is None:
402
433
  continue
403
- target = max(live_nbrs, key=lambda c: len(c.node_ids))
404
- target.node_ids.update(comp.node_ids)
405
- for nid in list(comp.node_ids):
406
- component_map[nid] = target.component_id
407
- for other in list(neighbors):
408
- if other == target.component_id:
409
- continue
410
- x_edges.setdefault(other, set()).discard(comp.component_id)
411
- x_edges.setdefault(other, set()).add(target.component_id)
412
- x_edges.setdefault(target.component_id, set()).add(other)
413
- x_edges.pop(comp.component_id, None)
414
- comp_by_id.pop(comp.component_id, None)
415
- components.remove(comp)
416
- changed = True
417
- break
434
+ target_comp.node_ids.update(dead_comp.node_ids)
435
+ for _nid in dead_comp.node_ids:
436
+ component_map[_nid] = target_cid
437
+ components.remove(dead_comp)
418
438
 
419
439
  return component_map, components
420
440
 
@@ -512,11 +532,28 @@ def _match_entries(
512
532
  comp_nodes: set[int],
513
533
  node_id_map: dict[int, NodeInfo],
514
534
  file_id_map: Optional[dict[str, int]] = None,
535
+ nid_to_entries: Optional[dict[int, list[EntryInfo]]] = None,
536
+ fid_to_entries: Optional[dict[int, list[EntryInfo]]] = None,
515
537
  ) -> list[EntryInfo]:
516
- """Match entry points to connected components."""
538
+ """Match entry points to connected components.
539
+ """
540
+ matched: list[EntryInfo] = []
541
+
542
+ if nid_to_entries is not None and fid_to_entries is not None:
543
+ comp_file_ids: set[int] = set()
544
+ for nid in comp_nodes:
545
+ for entry in nid_to_entries.get(nid, []):
546
+ matched.append(entry)
547
+ enode = node_id_map.get(nid)
548
+ if enode:
549
+ comp_file_ids.add(enode.file_id)
550
+ for fid in comp_file_ids:
551
+ for entry in fid_to_entries.get(fid, []):
552
+ matched.append(entry)
553
+ return matched
554
+
517
555
  if file_id_map is None:
518
556
  file_id_map = {}
519
- matched: list[EntryInfo] = []
520
557
  for entry in entries:
521
558
  if entry.node_id in comp_nodes:
522
559
  matched.append(entry)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: graphlint
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Summary: A static analysis tool for identifying dead code in a codebase. It provides Python API and CLI for use by agents.
5
5
  Author-email: Yutong Zou <yutong.zou.24@alumni.ucl.ac.uk>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "graphlint"
7
- version = "0.3.2"
7
+ version = "0.3.3"
8
8
  description = "A static analysis tool for identifying dead code in a codebase. It provides Python API and CLI for use by agents."
9
9
  readme = "README_PYPI.md"
10
10
  license = "MIT"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes