dead-cst 0.1.0__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 (106) hide show
  1. dead_cst-0.1.0/.claude/settings.json +20 -0
  2. dead_cst-0.1.0/.github/workflows/ci.yml +55 -0
  3. dead_cst-0.1.0/.github/workflows/publish.yml +68 -0
  4. dead_cst-0.1.0/.gitignore +210 -0
  5. dead_cst-0.1.0/.pre-commit-config.yaml +38 -0
  6. dead_cst-0.1.0/.python-version +1 -0
  7. dead_cst-0.1.0/CHANGELOG.md +122 -0
  8. dead_cst-0.1.0/CONTRIBUTING.md +154 -0
  9. dead_cst-0.1.0/LICENSE +21 -0
  10. dead_cst-0.1.0/PKG-INFO +223 -0
  11. dead_cst-0.1.0/README.md +197 -0
  12. dead_cst-0.1.0/ROADMAP.md +150 -0
  13. dead_cst-0.1.0/codecov.yml +82 -0
  14. dead_cst-0.1.0/dead_cst/__init__.py +99 -0
  15. dead_cst-0.1.0/dead_cst/_analyze.py +258 -0
  16. dead_cst-0.1.0/dead_cst/_branches.py +203 -0
  17. dead_cst-0.1.0/dead_cst/_codemod.py +135 -0
  18. dead_cst-0.1.0/dead_cst/_flow.py +173 -0
  19. dead_cst-0.1.0/dead_cst/_fqn.py +30 -0
  20. dead_cst-0.1.0/dead_cst/_plugins/__init__.py +106 -0
  21. dead_cst-0.1.0/dead_cst/_plugins/_core.py +524 -0
  22. dead_cst-0.1.0/dead_cst/_plugins/click.py +181 -0
  23. dead_cst-0.1.0/dead_cst/_plugins/explicit.py +58 -0
  24. dead_cst-0.1.0/dead_cst/_plugins/fastapi.py +154 -0
  25. dead_cst-0.1.0/dead_cst/_plugins/flask.py +172 -0
  26. dead_cst-0.1.0/dead_cst/_plugins/init_subclass.py +296 -0
  27. dead_cst-0.1.0/dead_cst/_plugins/main_block.py +72 -0
  28. dead_cst-0.1.0/dead_cst/_plugins/module_dunders.py +55 -0
  29. dead_cst-0.1.0/dead_cst/_plugins/project_scripts.py +60 -0
  30. dead_cst-0.1.0/dead_cst/_plugins/pytest.py +127 -0
  31. dead_cst-0.1.0/dead_cst/_plugins/typer.py +101 -0
  32. dead_cst-0.1.0/dead_cst/_plugins/unittest.py +171 -0
  33. dead_cst-0.1.0/dead_cst/_resolve.py +232 -0
  34. dead_cst-0.1.0/dead_cst/_resolvers/__init__.py +58 -0
  35. dead_cst-0.1.0/dead_cst/_resolvers/_core.py +52 -0
  36. dead_cst-0.1.0/dead_cst/_resolvers/_exports.py +111 -0
  37. dead_cst-0.1.0/dead_cst/_resolvers/pyproject.py +49 -0
  38. dead_cst-0.1.0/dead_cst/_resolvers/uv_workspace.py +104 -0
  39. dead_cst-0.1.0/dead_cst/_resolvers/venv.py +117 -0
  40. dead_cst-0.1.0/dead_cst/_symbols.py +177 -0
  41. dead_cst-0.1.0/dead_cst/_version.py +24 -0
  42. dead_cst-0.1.0/dead_cst/_visitor.py +542 -0
  43. dead_cst-0.1.0/dead_cst/cli.py +556 -0
  44. dead_cst-0.1.0/dead_cst/py.typed +0 -0
  45. dead_cst-0.1.0/examples/README.md +30 -0
  46. dead_cst-0.1.0/examples/scripts-and-all/README.md +72 -0
  47. dead_cst-0.1.0/examples/scripts-and-all/pyproject.toml +12 -0
  48. dead_cst-0.1.0/examples/scripts-and-all/src/reportkit/__init__.py +0 -0
  49. dead_cst-0.1.0/examples/scripts-and-all/src/reportkit/cli.py +6 -0
  50. dead_cst-0.1.0/examples/scripts-and-all/src/reportkit/formatters.py +21 -0
  51. dead_cst-0.1.0/examples/scripts-and-all/src/reportkit/legacy_writer.py +8 -0
  52. dead_cst-0.1.0/examples/scripts-and-all/src/reportkit/renderer.py +9 -0
  53. dead_cst-0.1.0/examples/simple-app/README.md +50 -0
  54. dead_cst-0.1.0/examples/simple-app/pyproject.toml +9 -0
  55. dead_cst-0.1.0/examples/simple-app/simple_app/__init__.py +0 -0
  56. dead_cst-0.1.0/examples/simple-app/simple_app/__main__.py +11 -0
  57. dead_cst-0.1.0/examples/simple-app/simple_app/core.py +11 -0
  58. dead_cst-0.1.0/examples/simple-app/simple_app/utils.py +8 -0
  59. dead_cst-0.1.0/examples/uv-workspace/README.md +117 -0
  60. dead_cst-0.1.0/examples/uv-workspace/packages/app/app/__init__.py +0 -0
  61. dead_cst-0.1.0/examples/uv-workspace/packages/app/app/cli.py +9 -0
  62. dead_cst-0.1.0/examples/uv-workspace/packages/app/app/helpers.py +4 -0
  63. dead_cst-0.1.0/examples/uv-workspace/packages/app/pyproject.toml +13 -0
  64. dead_cst-0.1.0/examples/uv-workspace/packages/app/tests/__init__.py +0 -0
  65. dead_cst-0.1.0/examples/uv-workspace/packages/app/tests/conftest.py +8 -0
  66. dead_cst-0.1.0/examples/uv-workspace/packages/core/core/__init__.py +0 -0
  67. dead_cst-0.1.0/examples/uv-workspace/packages/core/core/api.py +7 -0
  68. dead_cst-0.1.0/examples/uv-workspace/packages/core/pyproject.toml +12 -0
  69. dead_cst-0.1.0/examples/uv-workspace/packages/core/tests/__init__.py +0 -0
  70. dead_cst-0.1.0/examples/uv-workspace/packages/core/tests/conftest.py +8 -0
  71. dead_cst-0.1.0/examples/uv-workspace/pyproject.toml +12 -0
  72. dead_cst-0.1.0/examples/uv-workspace/uv.lock +31 -0
  73. dead_cst-0.1.0/pyproject.toml +106 -0
  74. dead_cst-0.1.0/tests/conftest.py +114 -0
  75. dead_cst-0.1.0/tests/test_branches.py +327 -0
  76. dead_cst-0.1.0/tests/test_cli.py +658 -0
  77. dead_cst-0.1.0/tests/test_codemod.py +680 -0
  78. dead_cst-0.1.0/tests/test_declarations.py +1258 -0
  79. dead_cst-0.1.0/tests/test_flow_sensitive_filter.py +352 -0
  80. dead_cst-0.1.0/tests/test_imports.py +329 -0
  81. dead_cst-0.1.0/tests/test_limitations.py +87 -0
  82. dead_cst-0.1.0/tests/test_plugins/__init__.py +0 -0
  83. dead_cst-0.1.0/tests/test_plugins/conftest.py +17 -0
  84. dead_cst-0.1.0/tests/test_plugins/test_click.py +664 -0
  85. dead_cst-0.1.0/tests/test_plugins/test_core.py +401 -0
  86. dead_cst-0.1.0/tests/test_plugins/test_explicit.py +45 -0
  87. dead_cst-0.1.0/tests/test_plugins/test_fastapi.py +440 -0
  88. dead_cst-0.1.0/tests/test_plugins/test_file_cache.py +188 -0
  89. dead_cst-0.1.0/tests/test_plugins/test_flask.py +646 -0
  90. dead_cst-0.1.0/tests/test_plugins/test_init_subclass.py +378 -0
  91. dead_cst-0.1.0/tests/test_plugins/test_main_block.py +51 -0
  92. dead_cst-0.1.0/tests/test_plugins/test_module_dunders.py +96 -0
  93. dead_cst-0.1.0/tests/test_plugins/test_project_scripts.py +26 -0
  94. dead_cst-0.1.0/tests/test_plugins/test_pytest.py +162 -0
  95. dead_cst-0.1.0/tests/test_plugins/test_typer.py +449 -0
  96. dead_cst-0.1.0/tests/test_plugins/test_unittest.py +207 -0
  97. dead_cst-0.1.0/tests/test_resolvers/__init__.py +0 -0
  98. dead_cst-0.1.0/tests/test_resolvers/test_core.py +66 -0
  99. dead_cst-0.1.0/tests/test_resolvers/test_exports.py +132 -0
  100. dead_cst-0.1.0/tests/test_resolvers/test_pyproject.py +41 -0
  101. dead_cst-0.1.0/tests/test_resolvers/test_uv_workspace.py +405 -0
  102. dead_cst-0.1.0/tests/test_resolvers/test_venv.py +38 -0
  103. dead_cst-0.1.0/tests/test_scope_provider_contract.py +259 -0
  104. dead_cst-0.1.0/tests/test_top_level_only.py +130 -0
  105. dead_cst-0.1.0/tests/test_unreachable_branches.py +272 -0
  106. dead_cst-0.1.0/uv.lock +922 -0
@@ -0,0 +1,20 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/claude-code-settings.json",
3
+ "hooks": {
4
+ "PostToolUse": [
5
+ {
6
+ "matcher": "Write|Edit|MultiEdit",
7
+ "hooks": [
8
+ {
9
+ "type": "command",
10
+ "command": "jq -r '.tool_input.file_path // .tool_response.filePath // empty' | { read -r f; [ -z \"$f\" ] && exit 0; case \"$f\" in *.py) cd /home/user/dead-cst && uv run --quiet ruff format \"$f\" && uv run --quiet ruff check --fix --unfixable F401 \"$f\" ;; esac; } 2>/dev/null || true"
11
+ },
12
+ {
13
+ "type": "command",
14
+ "command": "jq -r '.tool_input.file_path // .tool_response.filePath // empty' | { read -r f; [ -z \"$f\" ] && exit 0; case \"$f\" in *.py) cd /home/user/dead-cst && uv run --quiet ty check ;; esac; } 2>/dev/null || true"
15
+ }
16
+ ]
17
+ }
18
+ ]
19
+ }
20
+ }
@@ -0,0 +1,55 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ lint:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v6
13
+ with:
14
+ fetch-depth: 0
15
+ - uses: astral-sh/setup-uv@v7
16
+ with:
17
+ python-version: 3.12
18
+ activate-environment: true
19
+ enable-cache: true
20
+ cache-dependency-glob: "**/uv.lock"
21
+ - uses: actions/cache@v5
22
+ with:
23
+ path: ~/.cache/prek
24
+ key: prek-${{ hashFiles('.pre-commit-config.yaml') }}
25
+ - run: uv sync --frozen
26
+ - run: uv run prek run --show-diff-on-failure --color=always --all-files
27
+ env:
28
+ RUFF_OUTPUT_FORMAT: github
29
+ CONFTEST_OUTPUT: github
30
+
31
+ test:
32
+ runs-on: ubuntu-latest
33
+ strategy:
34
+ matrix:
35
+ python-version: ["3.11", "3.12", "3.13", "3.14"]
36
+ steps:
37
+ - uses: actions/checkout@v6
38
+ with:
39
+ fetch-depth: 0
40
+ - uses: astral-sh/setup-uv@v7
41
+ with:
42
+ python-version: ${{ matrix.python-version }}
43
+ activate-environment: true
44
+ enable-cache: true
45
+ cache-dependency-glob: "**/uv.lock"
46
+ - run: uv sync --frozen
47
+ - name: Run tests
48
+ run: uv run pytest ${{ matrix.python-version == '3.13' && '--cov=dead_cst --cov-branch --cov-report=xml' || '' }}
49
+ - name: Upload coverage to Codecov
50
+ if: matrix.python-version == '3.13'
51
+ uses: codecov/codecov-action@v5
52
+ with:
53
+ files: ./coverage.xml
54
+ fail_ci_if_error: false
55
+ token: ${{ secrets.CODECOV_TOKEN }}
@@ -0,0 +1,68 @@
1
+ name: Publish
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ tags: ["v*"]
7
+
8
+ permissions:
9
+ id-token: write
10
+ contents: read
11
+
12
+ jobs:
13
+ build:
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@v6
17
+ with:
18
+ fetch-depth: 0
19
+ - uses: astral-sh/setup-uv@v7
20
+ - run: uv build
21
+ - uses: actions/upload-artifact@v4
22
+ with:
23
+ name: dist
24
+ path: dist/
25
+
26
+ publish-testpypi:
27
+ if: github.event_name == 'push' && github.ref == 'refs/heads/main'
28
+ needs: build
29
+ runs-on: ubuntu-latest
30
+ environment: testpypi
31
+ steps:
32
+ - uses: actions/download-artifact@v4
33
+ with:
34
+ name: dist
35
+ path: dist/
36
+ - uses: pypa/gh-action-pypi-publish@release/v1
37
+ with:
38
+ repository-url: https://test.pypi.org/legacy/
39
+
40
+ publish-pypi:
41
+ if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
42
+ needs: build
43
+ runs-on: ubuntu-latest
44
+ environment: pypi
45
+ steps:
46
+ - uses: actions/download-artifact@v4
47
+ with:
48
+ name: dist
49
+ path: dist/
50
+ - uses: pypa/gh-action-pypi-publish@release/v1
51
+
52
+ github-release:
53
+ if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
54
+ needs: publish-pypi
55
+ runs-on: ubuntu-latest
56
+ permissions:
57
+ contents: write
58
+ steps:
59
+ - uses: actions/download-artifact@v4
60
+ with:
61
+ name: dist
62
+ path: dist/
63
+ - uses: softprops/action-gh-release@v2
64
+ with:
65
+ generate_release_notes: true
66
+ files: dist/*
67
+ prerelease: true
68
+ fail_on_unmatched_files: true
@@ -0,0 +1,210 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py.cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ #Pipfile.lock
96
+
97
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ #uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ #poetry.lock
109
+ #poetry.toml
110
+
111
+ # pdm
112
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
113
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
114
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
115
+ #pdm.lock
116
+ #pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # pixi
121
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
122
+ #pixi.lock
123
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
124
+ # in the .venv directory. It is recommended not to include this directory in version control.
125
+ .pixi
126
+
127
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
128
+ __pypackages__/
129
+
130
+ # Celery stuff
131
+ celerybeat-schedule
132
+ celerybeat.pid
133
+
134
+ # SageMath parsed files
135
+ *.sage.py
136
+
137
+ # Environments
138
+ .env
139
+ .envrc
140
+ .venv
141
+ env/
142
+ venv/
143
+ ENV/
144
+ env.bak/
145
+ venv.bak/
146
+
147
+ # Spyder project settings
148
+ .spyderproject
149
+ .spyproject
150
+
151
+ # Rope project settings
152
+ .ropeproject
153
+
154
+ # mkdocs documentation
155
+ /site
156
+
157
+ # mypy
158
+ .mypy_cache/
159
+ .dmypy.json
160
+ dmypy.json
161
+
162
+ # Pyre type checker
163
+ .pyre/
164
+
165
+ # pytype static type analyzer
166
+ .pytype/
167
+
168
+ # Cython debug symbols
169
+ cython_debug/
170
+
171
+ # PyCharm
172
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
173
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
174
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
175
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
176
+ #.idea/
177
+
178
+ # Abstra
179
+ # Abstra is an AI-powered process automation framework.
180
+ # Ignore directories containing user credentials, local state, and settings.
181
+ # Learn more at https://abstra.io/docs
182
+ .abstra/
183
+
184
+ # Visual Studio Code
185
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
186
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
187
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
188
+ # you could uncomment the following to ignore the entire vscode folder
189
+ # .vscode/
190
+
191
+ # hatch-vcs generated version file
192
+ dead_cst/_version.py
193
+
194
+ # Ruff stuff:
195
+ .ruff_cache/
196
+
197
+ # PyPI configuration file
198
+ .pypirc
199
+
200
+ # Cursor
201
+ # Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
202
+ # exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
203
+ # refer to https://docs.cursor.com/context/ignore-files
204
+ .cursorignore
205
+ .cursorindexingignore
206
+
207
+ # Marimo
208
+ marimo/_static/
209
+ marimo/_lsp/
210
+ __marimo__/
@@ -0,0 +1,38 @@
1
+ repos:
2
+ - repo: https://github.com/pre-commit/pre-commit-hooks
3
+ rev: v5.0.0
4
+ hooks:
5
+ - id: trailing-whitespace
6
+ types: [python]
7
+ - id: end-of-file-fixer
8
+ types: [python]
9
+ - id: check-merge-conflict
10
+ - id: check-case-conflict
11
+ - id: check-json
12
+ - id: check-toml
13
+ - id: check-yaml
14
+ - id: check-ast
15
+ - id: debug-statements
16
+ - id: check-docstring-first
17
+
18
+ - repo: https://github.com/astral-sh/ruff-pre-commit
19
+ rev: v0.11.2
20
+ hooks:
21
+ - id: ruff
22
+ args: [--fix, --exit-non-zero-on-fix]
23
+ - id: ruff-format
24
+
25
+ - repo: https://github.com/astral-sh/uv-pre-commit
26
+ rev: 0.7.2
27
+ hooks:
28
+ - id: uv-lock
29
+
30
+ - repo: local
31
+ hooks:
32
+ - id: ty
33
+ name: ty check
34
+ entry: uv run --frozen ty check
35
+ language: system
36
+ types_or: [python, pyi]
37
+ pass_filenames: false
38
+ require_serial: true
@@ -0,0 +1 @@
1
+ 3.13
@@ -0,0 +1,122 @@
1
+ # Changelog
2
+
3
+ All notable changes to `dead-cst` will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+ Until the first stable release the public API and CLI may change between any
8
+ two versions.
9
+
10
+ ## [Unreleased]
11
+
12
+ ## [0.1.0] - 2026-04-28
13
+
14
+ Initial alpha release. `dead-cst` is pre-1.0 software: the public Python API,
15
+ CLI flags, and output formats may change without notice between any two
16
+ versions until the first stable release.
17
+
18
+ ### Added
19
+ - Symbol-level reachability analysis built on LibCST's
20
+ `FullyQualifiedNameProvider` and `ScopeProvider`.
21
+ - Resolution of relative imports, aliased imports, and re-export chains
22
+ through `__init__.py`.
23
+ - `dead-cst analyze` CLI for reporting unreachable symbols and unreachable
24
+ branches (`if False:`, raise-only suites, etc.), with `text` and `json`
25
+ output formats.
26
+ - `dead-cst why-alive` CLI for explaining why a symbol is kept alive.
27
+ - `dead-cst remove` CLI that rewrites files in place via a LibCST codemod,
28
+ with import pruning when the last local user of an import is deleted and
29
+ position-aware shadowing so a shadowed dead binding no longer drags its
30
+ live sibling out with it.
31
+ - `dead-cst unused-exports` CLI command: report `__all__` entries whose
32
+ targets are kept alive only because they are listed in `__all__`.
33
+ - `dead-cst dependencies` CLI command: list third-party distributions and
34
+ files imported by the codebase, surfaced as synthetic
35
+ `[external dist] <name>` / `[external file] <name>` graph nodes.
36
+ - Multi-package / monorepo support via the `-p base:dep1,dep2` search-path
37
+ spec, with topological ordering of bases.
38
+ - Edge plugin architecture (`EdgePlugin`, `CSTAwareEdgePlugin`,
39
+ `PluginContext`, `GraphOp`/`AddNode`/`AddEdge`/`RemoveEdge`, `apply_ops`,
40
+ `synthetic_node`). Built-in plugins: `MainBlockPlugin`,
41
+ `ProjectScriptsPlugin`, `ExplicitEntrypointPlugin`, `ModuleDundersPlugin`,
42
+ `PytestPlugin`, `FastAPIPlugin`, `FlaskPlugin`, `TyperPlugin`,
43
+ `ClickPlugin`, `UnittestPlugin`, `InitSubclassPlugin`. Third-party
44
+ plugins register under the `dead_cst.plugins` entry-point group and load
45
+ via `load_plugin`.
46
+ - `PytestPlugin` (`--plugin pytest`): keep pytest-discovered tests,
47
+ `conftest.py` decls, and `@pytest.fixture` functions alive.
48
+ - `FastAPIPlugin` (`--plugin fastapi`): detect top-level `FastAPI()` and
49
+ `APIRouter()` instances (including factory-style apps), mark `FastAPI`
50
+ apps as entrypoints, and emit `instance -> handler` edges for
51
+ `@app.get(...)`-style decorators (HTTP methods, websockets, middleware,
52
+ exception handlers, `on_event`). Routers stay pass-through, so an
53
+ `APIRouter` that's never `include_router`'d remains dead.
54
+ - `FlaskPlugin` (`--plugin flask`): detect top-level `Flask()` /
55
+ `Blueprint()` instances (including factory-style apps) and emit
56
+ `instance -> handler` edges for `@app.route(...)`, HTTP-verb shortcuts,
57
+ request-lifecycle hooks (`before_request`, `after_request`,
58
+ `teardown_*`), error handlers, template helpers (`context_processor`,
59
+ `template_filter`, ...), and URL processors. `Flask` apps are seeded as
60
+ entrypoints (WSGI servers load `module:app`); `Blueprint`s stay
61
+ pass-through, so a blueprint never `register_blueprint`'d remains dead,
62
+ mirroring the `APIRouter` behavior in `FastAPIPlugin`.
63
+ - `TyperPlugin` (`--plugin typer`): detect top-level `Typer()` instances
64
+ and emit `instance -> handler` edges for `@app.command(...)` and
65
+ `@app.callback(...)` decorators. Typer apps are pass-through;
66
+ reachability is expected through `[project.scripts]` or a `__main__`
67
+ block, after which every registered command and callback stays alive.
68
+ Sub-typers that are never `add_typer`'d remain dead.
69
+ - `ClickPlugin` (`--plugin click`): detect top-level Click `Group`
70
+ instances (functions decorated `@click.group(...)` / `@click.Group(...)`,
71
+ `X = click.Group(...)` constructor calls, and inline sub-groups
72
+ registered via `@<group>.group(...)`, all resolved via fixpoint so a
73
+ chain of nested groups is fully discovered) and emit
74
+ `instance -> handler` edges for `@<group>.command(...)`,
75
+ `@<group>.group(...)`, and `@<group>.result_callback(...)` decorators.
76
+ Click groups stay pass-through; reachability is expected through
77
+ `[project.scripts]` or a `__main__` block, mirroring `TyperPlugin`.
78
+ - `UnittestPlugin` (`--plugin unittest`): mark stdlib `unittest.TestCase`
79
+ and `unittest.IsolatedAsyncioTestCase` subclasses, plus module-level
80
+ `setUpModule` / `tearDownModule` / `load_tests` hooks, as entrypoints.
81
+ Discovery is CST-based and prefiltered to files whose import nodes
82
+ reference `unittest`. Supports `import unittest` (with alias),
83
+ `from unittest import TestCase` (with alias), and module-prefixed base
84
+ references. Only direct base-class matches are recognised; transitive
85
+ subclasses through a project-local mixin need an explicit `-e`
86
+ entrypoint or coverage from `PytestPlugin`'s filename heuristics.
87
+ `from unittest import *`-only files are skipped (the resolver doesn't
88
+ surface stdlib star imports as graph nodes); use a non-star import.
89
+ - `InitSubclassPlugin` (`--plugin init_subclass`): detect classes that
90
+ define `__init_subclass__` and route reachability through a synthetic
91
+ marker node `<__init_subclass__>:<parent.fqname>` with edges
92
+ `parent -> marker -> subclass` for every transitive first-party
93
+ subclass. Registry-pattern subclasses stay alive whenever the parent
94
+ class does; the marker shows up in `why-alive` chains as a labeled
95
+ breadcrumb. Parents are pass-through, so a registry base nobody else
96
+ uses still surfaces as dead code.
97
+ - `ModuleDundersPlugin`: keep module-level dunder variables (`__all__`,
98
+ `__version__`, `__future__` imports, etc.) alive. Always registered by
99
+ the CLI.
100
+ - Path resolver architecture (`PathResolver`, `merge_paths`). Built-in
101
+ resolvers: `VenvResolver`, `PyprojectResolver`, `UvWorkspaceResolver`
102
+ (parses `uv.lock` to discover workspace members and inter-member
103
+ edges, including virtual workspace members that don't ship as wheels).
104
+ Third-party resolvers register under `dead_cst.resolvers` and load via
105
+ `load_resolver`.
106
+ - `exported_roots(base)` in `dead_cst._resolvers`: inspect a base's
107
+ `pyproject.toml` (src-layout, hatchling/setuptools/poetry/pdm/flit
108
+ backends, name-match fallback) to determine which subdirs the build
109
+ backend would actually ship, so internal dirs like `tests/` stay scoped
110
+ to their owning workspace member during cross-member import resolution.
111
+ - `--resolver` and `--plugin` flags on `analyze`, `why-alive`,
112
+ `unused-exports`, and `remove` for selecting path resolvers and edge
113
+ plugins.
114
+ - Public Python API: `build_symbol_graph`, `find_reachable`,
115
+ `count_nodes`, `order_paths`, `remove_code`, plus a `position` field
116
+ on `SymbolNode`.
117
+ - `py.typed` marker for downstream type-checking.
118
+ - `README.md`, `CONTRIBUTING.md`, `CHANGELOG.md`, and `ROADMAP.md` with a
119
+ stack-ranked plan from alpha to 1.0.
120
+
121
+ [Unreleased]: https://github.com/lpetre/dead-cst/compare/v0.1.0...HEAD
122
+ [0.1.0]: https://github.com/lpetre/dead-cst/releases/tag/v0.1.0
@@ -0,0 +1,154 @@
1
+ # Contributing to dead-cst
2
+
3
+ Thanks for your interest. `dead-cst` is small and pre-release — bug reports with minimal repros, real-world test cases, and PRs are all welcome. Expect APIs and CLI flags to keep moving until the first stable release. See [`ROADMAP.md`](ROADMAP.md) for the planned trajectory.
4
+
5
+ ## Development setup
6
+
7
+ `dead-cst` uses [uv](https://github.com/astral-sh/uv) for environment management.
8
+
9
+ ```bash
10
+ git clone https://github.com/lpetre/dead-cst
11
+ cd dead-cst
12
+ uv sync
13
+ ```
14
+
15
+ That installs the package in editable mode along with the `dev` group: `pytest`, `ruff`, `prek`, and `ty`.
16
+
17
+ ## Running tests
18
+
19
+ ```bash
20
+ uv run pytest
21
+ ```
22
+
23
+ For a tight inner loop while editing the visitor, resolver, or a plugin, `pytest-watcher` is in the dev group:
24
+
25
+ ```bash
26
+ uv run ptw
27
+ ```
28
+
29
+ To collect coverage locally:
30
+
31
+ ```bash
32
+ uv run pytest --cov=dead_cst --cov-branch --cov-report=term-missing
33
+ ```
34
+
35
+ CI uploads branch coverage from the 3.13 matrix entry to Codecov on every push and PR.
36
+
37
+ ## Coverage policy
38
+
39
+ Per-component thresholds reflect blast radius, not total LOC. The full
40
+ configuration lives in `codecov.yml`; the targets are:
41
+
42
+ | Component | Target | Why |
43
+ |---|---|---|
44
+ | `_codemod.py` | 95% | Rewrites user files. Regressions corrupt source. |
45
+ | `cli.py` | 80% | User-visible trust surface. |
46
+ | `_plugins/**` | 85% | Each plugin is a framework promise. |
47
+ | `_resolvers/**` | 85% | Path resolution drives every analysis. |
48
+ | `_analyze.py`, `_visitor.py`, `_resolve.py`, `_flow.py`, `_branches.py`, `_symbols.py`, `_fqn.py` | 80% | Analytical core. |
49
+
50
+ Per-component statuses are **informational** until Tier 1 of `ROADMAP.md`
51
+ (CLI integration tests, remaining framework presets) lands and the numbers
52
+ clear the bars above. The patch-coverage gate on new code is **enforcing**
53
+ at 90% — regressions are caught at the diff, not in aggregate.
54
+
55
+ `if TYPE_CHECKING:` blocks, `@overload` stubs, and `_version.py` are
56
+ excluded; see `[tool.coverage.*]` in `pyproject.toml`.
57
+
58
+ ## Linting and formatting
59
+
60
+ Ruff (lint + format), `ty` (type check), and a small set of hooks run on every commit via [`prek`](https://github.com/j178/prek), a fast Rust-based drop-in replacement for `pre-commit` that reads the same `.pre-commit-config.yaml`.
61
+
62
+ ```bash
63
+ uv run prek install # one-time, sets up the git hook
64
+ uv run prek run --all-files
65
+ ```
66
+
67
+ CI runs `prek run --all-files` on every push and pull request, so running it locally before committing avoids round-trips.
68
+
69
+ ## Project layout
70
+
71
+ ```
72
+ dead_cst/
73
+ __init__.py # public API surface
74
+ _analyze.py # build_symbol_graph, find_reachable, count_nodes, order_paths
75
+ _codemod.py # remove_code -- LibCST transformer + import pruner
76
+ _resolve.py # import resolution (stdlib / first-party / third-party)
77
+ _resolvers/ # path resolvers:
78
+ venv.py # sibling .venv -> site-packages
79
+ pyproject.py # [tool.dead-cst] paths or src/ fallback
80
+ uv_workspace.py # uv.lock workspace members + inter-member deps
81
+ _exports.py # exported_roots: hide internal dirs from consumers
82
+ _symbols.py # SymbolNode, SymbolTrie data classes
83
+ _visitor.py # SymbolVisitor -- walks each file and emits symbols + edges
84
+ _flow.py # flow-sensitive live-at-exit analysis for shadowing
85
+ _branches.py # statically-dead suite detection
86
+ _fqn.py # FullyQualifiedNameProvider patches / wrappers
87
+ _plugins/ # edge plugins:
88
+ main_block.py # if __name__ == "__main__"
89
+ project_scripts.py # pyproject.toml [project.scripts]
90
+ explicit.py # user-supplied -e specs
91
+ module_dunders.py # __all__, __version__, ...
92
+ pytest.py # conftest, test_*.py, @pytest.fixture
93
+ fastapi.py # FastAPI / APIRouter route handlers
94
+ cli.py # Typer entrypoints: analyze, why-alive, unused-exports,
95
+ # dependencies, remove
96
+ tests/ # pytest suite, fixture-driven from inline source snippets
97
+ ```
98
+
99
+ Modules prefixed with `_` are internal; only the names re-exported from `dead_cst/__init__.py` are part of the supported API. Within the package, plugins and resolvers are stable extension points — see below.
100
+
101
+ ## Adding a plugin
102
+
103
+ A plugin is any class that satisfies `EdgePlugin` (or `CSTAwareEdgePlugin` if it needs LibCST metadata). Implement `name: str` and a `contribute(ctx)` method that yields `AddNode`, `AddEdge`, or `RemoveEdge` ops.
104
+
105
+ ```python
106
+ from dead_cst import AddEdge, AddNode, GraphOp, PluginContext, synthetic_node
107
+
108
+ class FlaskRoutesPlugin:
109
+ name = "flask_routes"
110
+
111
+ def contribute(self, ctx: PluginContext):
112
+ for node in ctx.graph.nodes:
113
+ ... # detect @app.route handlers and emit AddNode(..., entrypoint=True)
114
+ ```
115
+
116
+ Built-in plugins live in `dead_cst/_plugins/<name>.py`; out-of-tree plugins register under the `dead_cst.plugins` entry-point group:
117
+
118
+ ```toml
119
+ [project.entry-points."dead_cst.plugins"]
120
+ flask_routes = "myproj.plugins:FlaskRoutesPlugin"
121
+ ```
122
+
123
+ `FastAPIPlugin` is a good full-featured reference — it walks each module's CST, recognises the `FastAPI()` / `APIRouter()` instance pattern, and wires `instance -> handler` edges through decorator detection.
124
+
125
+ ## Adding a resolver
126
+
127
+ A resolver implements `PathResolver`: a `name: str` attribute and a `resolve(project_root)` method returning a `{base: [dep_paths]}` dict. Built-in resolvers live in `_resolvers.py`; third-party resolvers register under `dead_cst.resolvers`.
128
+
129
+ ## Adding a test
130
+
131
+ Tests use the `build_decl_graph` fixture (in `tests/conftest.py`), which writes a dict of `{filename: source}` to a tmpdir, runs `build_symbol_graph` on it, and returns the resulting graph. The `assert_edges` fixture compares edges as `"src.fqname -> dst.fqname"` strings.
132
+
133
+ ```python
134
+ def test_something(build_decl_graph, assert_edges):
135
+ graph = build_decl_graph({"mod.py": "def a(): pass\na()"})
136
+ assert_edges(graph, {"mod.a -> mod", "mod -> mod.a"})
137
+ ```
138
+
139
+ Plugin tests follow the same pattern in `tests/test_plugins/`. For codemod regressions, `tests/test_codemod.py` writes a source snippet, runs `remove_code`, and asserts on the rewritten text.
140
+
141
+ ## Reporting bugs
142
+
143
+ A good bug report contains a minimal `.py` file (or pair of files) and the entrypoint flag you ran, plus the actual vs. expected dead-symbol output. The smaller the repro, the faster it gets fixed.
144
+
145
+ ## Pull requests
146
+
147
+ - Keep PRs focused — one logical change per PR.
148
+ - Add or update tests for behaviour changes.
149
+ - Run `prek run --all-files` and `pytest` before pushing.
150
+ - If your change is user-visible, add an entry to `CHANGELOG.md` under `[Unreleased]`.
151
+
152
+ ## Releasing
153
+
154
+ Releases are tag-driven. Pushing a `vX.Y.Z` tag triggers `.github/workflows/publish.yml`, which builds the package with `uv build` and publishes to PyPI via OIDC. The version is read from the tag by `hatch-vcs`, so no manual version bumps in source files are needed.