├── .github
├── PULL_REQUEST_TEMPLATE.md
├── dependabot.yml
└── workflows
│ ├── main.yml
│ └── pre-commit.yml
├── .gitignore
├── .pre-commit-config.yaml
├── .pre-commit-hooks.yaml
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE.txt
├── MANIFEST.in
├── README.md
├── TODO.md
├── dev
├── make-release-notes.py
└── release.sh
├── pyproject.toml
├── requirements.txt
├── setup.cfg
├── setup.py
├── tests
├── __init__.py
├── test_conditions.py
├── test_confidence.py
├── test_config.py
├── test_encoding.py
├── test_errors.py
├── test_format_strings.py
├── test_ignore.py
├── test_imports.py
├── test_item.py
├── test_make_whitelist.py
├── test_noqa.py
├── test_pytype.py
├── test_reachability.py
├── test_report.py
├── test_scavenging.py
├── test_script.py
├── test_size.py
├── test_sorting.py
├── test_utils.py
└── toml
│ └── mock_pyproject.toml
├── tox.ini
└── vulture
├── __init__.py
├── __main__.py
├── config.py
├── core.py
├── lines.py
├── noqa.py
├── reachability.py
├── utils.py
├── version.py
└── whitelists
├── argparse_whitelist.py
├── ast_whitelist.py
├── collections_whitelist.py
├── ctypes_whitelist.py
├── enum_whitelist.py
├── logging_whitelist.py
├── pint_whitelist.py
├── socketserver_whitelist.py
├── string_whitelist.py
├── sys_whitelist.py
├── threading_whitelist.py
├── unittest_whitelist.py
└── whitelist_utils.py
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | ## Description
4 |
5 |
6 | ## Related Issue
7 |
8 |
9 |
10 | ## Checklist:
11 |
12 |
13 | - [ ] I have updated the documentation in the README.md file or my changes don't require an update.
14 | - [ ] I have added an entry in CHANGELOG.md.
15 | - [ ] I have added or adapted tests to cover my changes.
16 | - [ ] I have run `pre-commit run --all-files` to format and lint my code.
17 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: pip
4 | directory: "/"
5 | schedule:
6 | interval: daily
7 | time: "04:00"
8 | open-pull-requests-limit: 10
9 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 | schedule:
9 | # Run daily (* needs quotes).
10 | - cron: '0 4 * * *'
11 |
12 | jobs:
13 | test:
14 | runs-on: ${{ matrix.os }}
15 | strategy:
16 | fail-fast: false
17 | matrix:
18 | os: [macos-latest, ubuntu-latest, windows-latest]
19 | python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13']
20 |
21 | steps:
22 | - name: Checkout code
23 | uses: actions/checkout@v3
24 |
25 | - name: Set up Python ${{ matrix.python-version }}
26 | uses: actions/setup-python@v4
27 | with:
28 | python-version: ${{ matrix.python-version }}
29 | allow-prereleases: true
30 |
31 | - name: Install dependencies
32 | run: |
33 | python -m pip install --upgrade pip
34 | python -m pip install --upgrade build coveralls setuptools tox wheel
35 | python -m pip install -r requirements.txt
36 |
37 | - name: Build Vulture wheel
38 | run: python -m build
39 |
40 | - name: Install Vulture wheel
41 | run: "python -m pip install --only-binary=:all: --ignore-installed --find-links=dist/ vulture"
42 |
43 | - name: Run Vulture
44 | run: |
45 | vulture vulture/ tests/
46 | python -m vulture vulture/ tests/
47 |
48 | - name: Run tests
49 | run: python -m tox -e py
50 |
51 | - name: Report coverage to Codecov
52 | uses: codecov/codecov-action@v3
53 | with:
54 | files: ./coverage.xml
55 |
--------------------------------------------------------------------------------
/.github/workflows/pre-commit.yml:
--------------------------------------------------------------------------------
1 | name: pre-commit
2 |
3 | on:
4 | pull_request:
5 | push:
6 | branches: [main]
7 |
8 | jobs:
9 | pre-commit:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v4.1.1
13 | - uses: pre-commit/action@v3.0.0
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.py[cod]
2 | MANIFEST
3 | build/
4 | coverage.xml
5 | dist/
6 | htmlcov/
7 | vulture.egg-info/
8 | .cache/
9 | .coverage
10 | .pytest_cache/
11 | .tox/
12 | .venv/
13 | .vscode/
14 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | repos:
2 | - repo: https://github.com/astral-sh/ruff-pre-commit
3 | rev: v0.1.13
4 | hooks:
5 | - id: ruff
6 | - id: ruff-format
7 |
--------------------------------------------------------------------------------
/.pre-commit-hooks.yaml:
--------------------------------------------------------------------------------
1 | - id: vulture
2 | name: vulture
3 | language: python
4 | entry: vulture
5 | description: Find unused Python code.
6 | types: [python]
7 | # Vulture needs access to all files for a complete analysis, so we
8 | # prevent pre-commit from passing only the changed files. Instead,
9 | # please create a `pyproject.toml` file in your repository and specify
10 | # all files that Vulture should check under `[tool.vulture] --> paths`.
11 | pass_filenames: false
12 | require_serial: true
13 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # next (unreleased)
2 |
3 | * Handle `while True` loops without `break` statements (kreathon).
4 |
5 | # 2.14 (2024-12-08)
6 |
7 | * Improve reachability analysis (kreathon, #270, #302).
8 | * Add type hints for `get_unused_code` and the fields of the `Item` class (John Doknjas, #361).
9 |
10 | # 2.13 (2024-10-02)
11 |
12 | * Add support for Python 3.13 (Jendrik Seipp, #369).
13 | * Add PyPI and conda-forge badges to README file (Trevor James Smith, #356).
14 | * Include `tests/**/*.toml` in sdist (Colin Watson).
15 |
16 | # 2.12 (2024-09-17)
17 |
18 | * Use `ruff` for linting and formatting (Anh Trinh, #347, #349).
19 | * Replace `tox` by `pre-commit` for linting and formatting (Anh Trinh, #349).
20 | * Add `--config` flag to specify path to pyproject.toml configuration file (Glen Robertson, #352).
21 |
22 | # 2.11 (2024-01-06)
23 |
24 | * Switch to tomllib/tomli to support heterogeneous arrays (Sebastian Csar, #340).
25 | * Bump flake8, flake8-comprehensions and flake8-bugbear (Sebastian Csar, #341).
26 | * Provide whitelist parity for `MagicMock` and `Mock` (maxrake, #342).
27 |
28 | # 2.10 (2023-10-06)
29 |
30 | * Drop support for Python 3.7 (Jendrik Seipp, #323).
31 | * Add support for Python 3.12 (Jendrik Seipp, #332).
32 | * Use `end_lineno` AST attribute to obtain more accurate line counts (Jendrik Seipp).
33 |
34 | # 2.9.1 (2023-08-21)
35 |
36 | * Use exit code 0 for `--help` and `--version` again (Jendrik Seipp, #321).
37 |
38 | # 2.9 (2023-08-20)
39 |
40 | * Use exit code 3 when dead code is found (whosayn, #319).
41 | * Treat non-supported decorator names as "@" instead of crashing (Llandy3d and Jendrik Seipp, #284).
42 | * Drop support for Python 3.6 (Jendrik Seipp).
43 |
44 | # 2.8 (2023-08-10)
45 |
46 | * Add `UnicodeEncodeError` exception handling to `core.py` (milanbalazs, #299).
47 | * Add whitelist for `Enum` attributes `_name_` and `_value_` (Eugene Toder, #305).
48 | * Run tests and add PyPI trove for Python 3.11 (Jendrik Seipp).
49 |
50 | # 2.7 (2023-01-08)
51 |
52 | * Ignore `setup_module()`, `teardown_module()`, etc. in pytest `test_*.py` files (Jendrik Seipp).
53 | * Add whitelist for `socketserver.TCPServer.allow_reuse_address` (Ben Elliston).
54 | * Clarify that `--exclude` patterns are matched against absolute paths (Jendrik Seipp, #260).
55 | * Fix example in README file (Jendrik Seipp, #272).
56 |
57 | # 2.6 (2022-09-19)
58 |
59 | * Add basic `match` statement support (kreathon, #276, #291).
60 |
61 | # 2.5 (2022-07-03)
62 |
63 | * Mark imports in `__all__` as used (kreathon, #172, #282).
64 | * Add whitelist for `pint.UnitRegistry.default_formatter` (Ben Elliston, #258).
65 |
66 | # 2.4 (2022-05-19)
67 |
68 | * Print absolute filepaths as relative again (as in version 2.1 and before)
69 | if they are below the current directory (The-Compiler, #246).
70 | * Run tests and add PyPI trove for Python 3.10 (chayim, #266).
71 | * Allow using the `del` keyword to mark unused variables (sshishov, #279).
72 |
73 | # 2.3 (2021-01-16)
74 |
75 | * Add [pre-commit](https://pre-commit.com) hook (Clément Robert, #244).
76 |
77 | # 2.2 (2021-01-15)
78 |
79 | * Only parse format strings when being used with `locals()` (jingw, #225).
80 | * Don't override paths in pyproject.toml with empty CLI paths (bcbnz, #228).
81 | * Run continuous integration tests for Python 3.9 (ju-sh, #232).
82 | * Use pathlib internally (ju-sh, #226).
83 |
84 | # 2.1 (2020-08-19)
85 |
86 | * Treat `getattr/hasattr(obj, "constant_string", ...)` as a reference to
87 | `obj.constant_string` (jingw, #219).
88 | * Fix false positives when assigning to `x.some_name` but reading via
89 | `some_name`, at the cost of potential false negatives (jingw, #221).
90 | * Allow reading options from `pyproject.toml` (Michel Albert, #164, #215).
91 |
92 | # 2.0 (2020-08-11)
93 |
94 | * Parse `# type: ...` comments if on Python 3.8+ (jingw, #220).
95 | * Bump minimum Python version to 3.6 (Jendrik Seipp, #218). The last
96 | Vulture release that supports Python 2.7 and Python 3.5 is version 1.6.
97 | * Consider all files under `test` or `tests` directories test files
98 | (Jendrik Seipp).
99 | * Ignore `logging.Logger.propagate` attribute (Jendrik Seipp).
100 |
101 | # 1.6 (2020-07-28)
102 |
103 | * Differentiate between functions and methods (Jendrik Seipp, #112, #209).
104 | * Move from Travis to GitHub actions (RJ722, #211).
105 |
106 | # 1.5 (2020-05-24)
107 |
108 | * Support flake8 "noqa" error codes F401 (unused import) and F841 (unused
109 | local variable) (RJ722, #195).
110 | * Detect unreachable code in conditional expressions
111 | (Agathiyan Bragadeesh, #178).
112 |
113 | # 1.4 (2020-03-30)
114 |
115 | * Ignore unused import statements in `__init__.py` (RJ722, #192).
116 | * Report first decorator's line number for unused decorated objects on
117 | Python 3.8+ (RJ722, #200).
118 | * Check code with black and pyupgrade.
119 |
120 | # 1.3 (2020-02-03)
121 |
122 | * Detect redundant 'if' conditions without 'else' blocks.
123 | * Add whitelist for `string.Formatter` (Joseph Bylund, #183).
124 |
125 | # 1.2 (2019-11-22)
126 |
127 | * Fix tests for Python 3.8 (#166).
128 | * Use new `Constant` AST node under Python 3.8+ (#175).
129 | * Add test for f-strings (#177).
130 | * Add whitelist for `logging` module.
131 |
132 | # 1.1 (2019-09-23)
133 |
134 | * Add `sys.excepthook` to `sys` whitelist.
135 | * Add whitelist for `ctypes` module.
136 | * Check that type annotations are parsed and type comments are ignored
137 | (thanks @kx-chen).
138 | * Support checking files with BOM under Python 2.7 (#170).
139 |
140 | # 1.0 (2018-10-23)
141 |
142 | * Add `--ignore-decorators` flag (thanks @RJ722).
143 | * Add whitelist for `threading` module (thanks @andrewhalle).
144 |
145 | # 0.29 (2018-07-31)
146 |
147 | * Add `--ignore-names` flag for ignoring names matching the given glob
148 | patterns (thanks @RJ722).
149 |
150 | # 0.28 (2018-07-05)
151 |
152 | * Add `--make-whitelist` flag for reporting output in whitelist format
153 | (thanks @RJ722).
154 | * Ignore case of `--exclude` arguments on Windows.
155 | * Add `*-test.py` to recognized test file patterns.
156 | * Add `failureException`, `longMessage` and `maxDiff` to `unittest`
157 | whitelist.
158 | * Refer to actual objects rather than their mocks in default
159 | whitelists (thanks @RJ722).
160 | * Don't import any Vulture modules in setup.py (thanks @RJ722).
161 |
162 | # 0.27 (2018-06-05)
163 |
164 | * Report `while (True): ... else: ...` as unreachable (thanks @RJ722).
165 | * Use `argparse` instead of `optparse`.
166 | * Whitelist Mock.return\_value and Mock.side\_effect in unittest.mock
167 | module.
168 | * Drop support for Python 2.6 and 3.3.
169 | * Improve documentation and test coverage (thanks @RJ722).
170 |
171 | # 0.26 (2017-08-28)
172 |
173 | * Detect `async` function definitions (thanks @RJ722).
174 | * Add `Item.get_report()` method (thanks @RJ722).
175 | * Move method for finding Python modules out of Vulture class.
176 |
177 | # 0.25 (2017-08-15)
178 |
179 | * Detect unsatisfiable statements containing `and`, `or` and `not`.
180 | * Use filenames and line numbers as tie-breakers when sorting by size.
181 | * Store first and last line numbers in Item objects.
182 | * Pass relevant options directly to `scavenge()` and `report()`.
183 |
184 | # 0.24 (2017-08-14)
185 |
186 | * Detect unsatisfiable `while`-conditions (thanks @RJ722).
187 | * Detect unsatisfiable `if`- and `else`-conditions (thanks @RJ722).
188 | * Handle null bytes in source code.
189 |
190 | # 0.23 (2017-08-10)
191 |
192 | * Add `--min-confidence` flag (thanks @RJ722).
193 |
194 | # 0.22 (2017-08-04)
195 |
196 | * Detect unreachable code after `return`, `break`, `continue` and
197 | `raise` (thanks @RJ722).
198 | * Parse all variable and attribute names in new format strings.
199 | * Extend ast whitelist.
200 |
201 | # 0.21 (2017-07-26)
202 |
203 | * If an unused item is defined multiple times, report it multiple
204 | times.
205 | * Make size estimates for function calls more accurate.
206 | * Create wheel files for Vulture (thanks @RJ722).
207 |
208 | # 0.20 (2017-07-26)
209 |
210 | * Report unused tuple assignments as dead code.
211 | * Report attribute names that have the same names as variables as dead
212 | code.
213 | * Let Item class inherit from `object` (thanks @RJ722).
214 | * Handle names imported as aliases like all other used variable names.
215 | * Rename Vulture.used\_vars to Vulture.used\_names.
216 | * Use function for determining which imports to ignore.
217 | * Only try to import each whitelist file once.
218 | * Store used names and used attributes in sets instead of lists.
219 | * Fix estimating the size of code containing ellipses (...).
220 | * Refactor and simplify code.
221 |
222 | # 0.19 (2017-07-20)
223 |
224 | * Don't ignore \_\_foo variable names.
225 | * Use separate methods for determining whether to ignore classes and
226 | functions.
227 | * Only try to find a whitelist for each defined import once (thanks
228 | @roivanov).
229 | * Fix finding the last child for many types of AST nodes.
230 |
231 | # 0.18 (2017-07-17)
232 |
233 | * Make --sort-by-size faster and more
234 | accurate (thanks @RJ722).
235 |
236 | # 0.17 (2017-07-17)
237 |
238 | * Add get\_unused\_code() method.
239 | * Return with exit code 1 when syntax errors are found or files can't
240 | be read.
241 |
242 | # 0.16 (2017-07-12)
243 |
244 | * Differentiate between unused classes and functions (thanks @RJ722).
245 | * Add --sort-by-size option (thanks @jackric and @RJ722).
246 | * Count imports as used if they are accessed as module attributes.
247 |
248 | # 0.15 (2017-07-04)
249 |
250 | * Automatically include whitelists based on imported modules (thanks
251 | @RJ722).
252 | * Add --version parameter (thanks @RJ722).
253 | * Add appveyor tests for testing on Windows (thanks @RJ722).
254 |
255 | # 0.14 (2017-04-06)
256 |
257 | * Add stub whitelist file for Python standard library (thanks @RJ722)
258 | * Ignore class names starting with "Test" in "test\_" files (thanks
259 | @thisch).
260 | * Ignore "test\_" functions only in "test\_" files.
261 |
262 | # 0.13 (2017-03-06)
263 |
264 | * Ignore star-imported names since we cannot detect whether they are
265 | used.
266 | * Move repository to GitHub.
267 |
268 | # 0.12 (2017-01-05)
269 |
270 | * Detect unused imports.
271 | * Use tokenize.open() on Python \>= 3.2 for reading input files,
272 | assume UTF-8 encoding on older Python versions.
273 |
274 | # 0.11 (2016-11-27)
275 |
276 | * Use the system's default encoding when reading files.
277 | * Report syntax errors instead of aborting.
278 |
279 | # 0.10 (2016-07-14)
280 |
281 | * Detect unused function and method arguments (issue #15).
282 | * Detect unused \*args and \*\*kwargs parameters.
283 | * Change license from GPL to MIT.
284 |
285 | # 0.9 (2016-06-29)
286 |
287 | * Don't flag attributes as unused if they are used as global variables
288 | in another module (thanks Florian Bruhin).
289 | * Don't consider "True" and "False" variable names.
290 | * Abort with error message when invoked on .pyc files.
291 |
292 | # 0.8.1 (2015-09-28)
293 |
294 | * Fix code for Python 3.
295 |
296 | # 0.8 (2015-09-28)
297 |
298 | * Do not flag names imported with "import as" as dead code (thanks Tom
299 | Terrace).
300 |
301 | # 0.7 (2015-09-26)
302 |
303 | * Exit with exitcode 1 if path on commandline can't be found.
304 | * Test vulture with vulture using a whitelist module for false
305 | positives.
306 | * Add tests that run vulture as a script.
307 | * Add "python setup.py test" command for running tests.
308 | * Add support for tox.
309 | * Raise test coverage to 100%.
310 | * Remove ez\_setup.py.
311 |
312 | # 0.6 (2014-09-06)
313 |
314 | * Ignore function names starting with "test\_".
315 | * Parse variable names in new format strings (e.g. "This is
316 | {x}".format(x="nice")).
317 | * Only parse alphanumeric variable names in format strings and ignore
318 | types.
319 | * Abort with exit code 1 on syntax errors.
320 | * Support installation under Windows by using setuptools (thanks
321 | Reuben Fletcher-Costin).
322 |
323 | # 0.5 (2014-05-09)
324 |
325 | * If dead code is found, exit with 1.
326 |
327 | # 0.4.1 (2013-09-17)
328 |
329 | * Only warn if a path given on the command line cannot be found.
330 |
331 | # 0.4 (2013-06-23)
332 |
333 | * Ignore unused variables starting with an underscore.
334 | * Show warning for syntax errors instead of aborting directly.
335 | * Print warning if a file cannot be found.
336 |
337 | # 0.3 (2012-03-19)
338 |
339 | * Add support for python3
340 | * Report unused attributes
341 | * Find tuple assignments in comprehensions
342 | * Scan files given on the command line even if they don't end with .py
343 |
344 | # 0.2 (2012-03-18)
345 |
346 | * Only format nodes in verbose mode (gives 4x speedup).
347 |
348 | # 0.1 (2012-03-17)
349 |
350 | * First release.
351 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at jendrikseipp@gmail.com. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 | For answers to common questions about this code of conduct, see
76 | https://www.contributor-covenant.org/faq
77 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to Vulture
2 |
3 | ## Creating and cloning a fork
4 |
5 | Fork the Vulture repository on GitHub by clicking the "fork" button on the
6 | top right. Then clone your fork to your local machine:
7 |
8 | $ git clone https://github.com/USERNAME/vulture.git # Use your GitHub username.
9 | $ cd vulture
10 |
11 | ## Installation
12 |
13 | We recommend using a Python virtual environment to isolate the
14 | installation of vulture.
15 |
16 | ### Setting up the virtual environment
17 |
18 | You can read more about `virtualenv` in the [virtualenv
19 | documentation](http://virtualenv.readthedocs.org).
20 |
21 | To install the `virtualenv` package using `pip`, run:
22 |
23 | $ python3 -m pip install virtualenv
24 |
25 | Now you can create your own environment (named `vulture_dev`):
26 |
27 | $ virtualenv vulture_dev
28 |
29 | Now, whenever you work on the project, activate the corresponding
30 | environment.
31 |
32 | - On **Unix-based** systems, this can be done with:
33 |
34 | $ source vulture_dev/bin/activate
35 |
36 | - And on **Windows** this is done with:
37 |
38 | $ vulture_dev\scripts\activate
39 |
40 | To leave the virtual environment use:
41 |
42 | (vulture_dev)$ deactivate
43 |
44 | ### Installing vulture
45 |
46 | Navigate to your cloned `vulture` directory, and run the following to
47 | install in development mode:
48 |
49 | $ pip install --editable .
50 |
51 | ### Installing test tools
52 |
53 | Vulture uses tox for testing. You can read more about it in the [tox
54 | documentation](https://tox.readthedocs.io).
55 |
56 | To install `tox`, run:
57 |
58 | $ pip install tox
59 |
60 | It's also recommended that you use `pre-commit` to catch style errors
61 | early:
62 |
63 | $ pip install pre-commit
64 | $ pre-commit install
65 |
66 | ## Coding standards
67 |
68 | ### Creating a new branch
69 |
70 | To start working on a pull request, create a new branch to work on. You
71 | should never develop on your main branch because your main branch
72 | should always be synchronized with the main repo’s main branch, which
73 | is challenging if it has new commits. Create a branch using:
74 |
75 | $ git checkout -b your-new-branch
76 |
77 | #### Naming branches
78 |
79 | Branch names should describe the feature/issue that you want to work on,
80 | but at the same time be short.
81 |
82 | ### Commits
83 |
84 | Each commit should be atomic and its message should adequately describe
85 | the change in a clear manner. Use imperative, e.g., "Fix issue12." instead
86 | of "Fixed issue12.". Please make sure that you only fix the issue at hand
87 | or implement the desired new feature instead of making "drive-by" changes
88 | like adding type hints.
89 |
90 | ### Formating and linting
91 |
92 | Run `pre-commit` using:
93 |
94 | $ pre-commit run --all-files
95 |
96 | ## Testing
97 |
98 | Run `tox` using:
99 |
100 | $ tox
101 |
102 | ## Pull requests
103 |
104 | ### How to send a pull request?
105 |
106 | Push your changes to your fork with:
107 |
108 | $ git push --set-upstream origin BRANCHNAME
109 |
110 | Then visit your fork on GitHub, change the branch to the one you committed
111 | to, and click the `New Pull Request` button.
112 |
113 | ### Follow-up
114 |
115 | In case your PR needs to be updated (tests fail or reviewer requests some
116 | changes), update it by committing on top of your branch. It is not
117 | necessary to amend your previous commit, since we will usually squash all
118 | commits when merging anyway.
119 |
120 | ### Feedback
121 |
122 | Take reviewer feedback positively. It's unlikely for a PR to be merged on
123 | the first attempt, but don’t worry that’s just how it works. It helps to
124 | keep the code clean.
125 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2012-2020 Jendrik Seipp (jendrikseipp@gmail.com)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a
6 | copy of this software and associated documentation files (the
7 | "Software"), to deal in the Software without restriction, including
8 | without limitation the rights to use, copy, modify, merge, publish,
9 | distribute, sublicense, and/or sell copies of the Software, and to
10 | permit persons to whom the Software is furnished to do so, subject to
11 | the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included
14 | in all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
17 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include *.md
2 | include *.txt
3 | include tests/*.py
4 | include tests/**/*.toml
5 | include tox.ini
6 | include vulture/whitelists/*.py
7 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Vulture - Find dead code
2 |
3 | [](https://pypi.python.org/pypi/vulture)
4 | [](https://anaconda.org/conda-forge/vulture)
5 | 
6 | [](https://codecov.io/gh/jendrikseipp/vulture?branch=main)
7 |
8 | Vulture finds unused code in Python programs. This is useful for
9 | cleaning up and finding errors in large code bases. If you run Vulture
10 | on both your library and test suite you can find untested code.
11 |
12 | Due to Python's dynamic nature, static code analyzers like Vulture are
13 | likely to miss some dead code. Also, code that is only called implicitly
14 | may be reported as unused. Nonetheless, Vulture can be a very helpful
15 | tool for higher code quality.
16 |
17 | ## Features
18 |
19 | * fast: uses static code analysis
20 | * tested: tests itself and has complete test coverage
21 | * complements pyflakes and has the same output syntax
22 | * sorts unused classes and functions by size with `--sort-by-size`
23 |
24 | ## Installation
25 |
26 | $ pip install vulture
27 |
28 | ## Usage
29 |
30 | $ vulture myscript.py # or
31 | $ python3 -m vulture myscript.py
32 | $ vulture myscript.py mypackage/
33 | $ vulture myscript.py --min-confidence 100 # Only report 100% dead code.
34 |
35 | The provided arguments may be Python files or directories. For each
36 | directory Vulture analyzes all contained
37 | \*.py files.
38 |
39 | After you have found and deleted dead code, run Vulture again, because
40 | it may discover more dead code.
41 |
42 | ## Types of unused code
43 |
44 | In addition to finding unused functions, classes, etc., Vulture can detect
45 | unreachable code. Each chunk of dead code is assigned a *confidence value*
46 | between 60% and 100%, where a value of 100% signals that it is certain that the
47 | code won't be executed. Values below 100% are *very rough* estimates (based on
48 | the type of code chunk) for how likely it is that the code is unused.
49 |
50 | | Code type | Confidence value |
51 | | ------------------- | -- |
52 | | function/method/class argument, unreachable code | 100% |
53 | | import | 90% |
54 | | attribute, class, function, method, property, variable | 60% |
55 |
56 | You can use the `--min-confidence` flag to set the minimum confidence
57 | for code to be reported as unused. Use `--min-confidence 100` to only
58 | report code that is guaranteed to be unused within the analyzed files.
59 |
60 | ## Handling false positives
61 |
62 | When Vulture incorrectly reports chunks of code as unused, you have
63 | several options for suppressing the false positives. If fixing your false
64 | positives could benefit other users as well, please file an issue report.
65 |
66 | #### Whitelists
67 |
68 | The recommended option is to add used code that is reported as unused to a
69 | Python module and add it to the list of scanned paths. To obtain such a
70 | whitelist automatically, pass `--make-whitelist` to Vulture:
71 |
72 | $ vulture mydir --make-whitelist > whitelist.py
73 | $ vulture mydir whitelist.py
74 |
75 | Note that the resulting `whitelist.py` file will contain valid Python
76 | syntax, but for Python to be able to *run* it, you will usually have to
77 | make some modifications.
78 |
79 | We collect whitelists for common Python modules and packages in
80 | `vulture/whitelists/` (pull requests are welcome).
81 |
82 | #### Ignoring files
83 |
84 | If you want to ignore a whole file or directory, use the `--exclude` parameter
85 | (e.g., `--exclude "*settings.py,*/docs/*.py,*/test_*.py,*/.venv/*.py"`). The
86 | exclude patterns are matched against absolute paths.
87 |
88 | #### Flake8 noqa comments
89 |
90 |
99 |
100 | For compatibility with [flake8](https://flake8.pycqa.org/), Vulture
101 | supports the [F401 and
102 | F841](https://flake8.pycqa.org/en/latest/user/error-codes.html) error
103 | codes for ignoring unused imports (`# noqa: F401`) and unused local
104 | variables (`# noqa: F841`). However, we recommend using whitelists instead
105 | of `noqa` comments, since `noqa` comments add visual noise to the code and
106 | make it harder to read.
107 |
108 | #### Ignoring names
109 |
110 | You can use `--ignore-names foo*,ba[rz]` to let Vulture ignore all names
111 | starting with `foo` and the names `bar` and `baz`. Additionally, the
112 | `--ignore-decorators` option can be used to ignore the names of functions
113 | decorated with the given decorator (but not their arguments or function body).
114 | This is helpful for example in Flask
115 | projects, where you can use `--ignore-decorators "@app.route"` to ignore all
116 | function names with the `@app.route` decorator. Note that Vulture simplifies
117 | decorators it cannot parse: `@foo.bar(x, y)` becomes "@foo.bar" and
118 | `@foo.bar(x, y).baz` becomes "@" internally.
119 |
120 | We recommend using whitelists instead of `--ignore-names` or
121 | `--ignore-decorators` whenever possible, since whitelists are
122 | automatically checked for syntactic correctness when passed to Vulture
123 | and often you can even pass them to your Python interpreter and let it
124 | check that all whitelisted code actually still exists in your project.
125 |
126 | #### Marking unused variables
127 |
128 | There are situations where you can't just remove unused variables, e.g.,
129 | in function signatures. The recommended solution is to use the `del`
130 | keyword as described in the
131 | [PyLint manual](http://pylint-messages.wikidot.com/messages:w0613) and on
132 | [StackOverflow](https://stackoverflow.com/a/14836005):
133 |
134 | ```python
135 | def foo(x, y):
136 | del y
137 | return x + 3
138 | ```
139 |
140 | Vulture will also ignore all variables that start with an underscore, so
141 | you can use `_x, y = get_pos()` to mark unused tuple assignments or
142 | function arguments, e.g., `def foo(x, _y)`.
143 |
144 | #### Minimum confidence
145 |
146 | Raise the minimum [confidence value](#types-of-unused-code) with the `--min-confidence` flag.
147 |
148 | #### Unreachable code
149 |
150 | If Vulture complains about code like `if False:`, you can use a Boolean
151 | flag `debug = False` and write `if debug:` instead. This makes the code
152 | more readable and silences Vulture.
153 |
154 | #### Forward references for type annotations
155 |
156 | See [#216](https://github.com/jendrikseipp/vulture/issues/216). For
157 | example, instead of `def foo(arg: "Sequence"): ...`, we recommend using
158 |
159 | ``` python
160 | from __future__ import annotations
161 |
162 | def foo(arg: Sequence):
163 | ...
164 | ```
165 |
166 |
167 | ## Configuration
168 |
169 | You can also store command line arguments in `pyproject.toml` under the
170 | `tool.vulture` section. Simply remove leading dashes and replace all
171 | remaining dashes with underscores.
172 |
173 | Options given on the command line have precedence over options in
174 | `pyproject.toml`.
175 |
176 | Example Config:
177 |
178 | ``` toml
179 | [tool.vulture]
180 | exclude = ["*file*.py", "dir/"]
181 | ignore_decorators = ["@app.route", "@require_*"]
182 | ignore_names = ["visit_*", "do_*"]
183 | make_whitelist = true
184 | min_confidence = 80
185 | paths = ["myscript.py", "mydir", "whitelist.py"]
186 | sort_by_size = true
187 | verbose = true
188 | ```
189 |
190 | Vulture will automatically look for a `pyproject.toml` in the current working directory.
191 |
192 | To use a `pyproject.toml` in another directory, you can use the `--config path/to/pyproject.toml` flag.
193 |
194 | ## Integrations
195 |
196 | You can use a [pre-commit](https://pre-commit.com/#install) hook to run
197 | Vulture before each commit. For this, install pre-commit and add the
198 | following to the `.pre-commit-config.yaml` file in your repository:
199 |
200 | ```yaml
201 | repos:
202 | - repo: https://github.com/jendrikseipp/vulture
203 | rev: 'v2.3' # or any later Vulture version
204 | hooks:
205 | - id: vulture
206 | ```
207 |
208 | Then run `pre-commit install`. Finally, create a `pyproject.toml` file
209 | in your repository and specify all files that Vulture should check under
210 | `[tool.vulture] --> paths` (see above).
211 |
212 | There's also a [GitHub Action for Vulture](https://github.com/gtkacz/vulture-action),
213 | a [VS Code extension](https://marketplace.visualstudio.com/items?itemName=sebastienfi.dead-code-finder)
214 | and you can use Vulture programatically. For example:
215 |
216 | ``` python
217 | import vulture
218 |
219 | v = vulture.Vulture()
220 | v.scavenge(['.'])
221 | unused_code = v.get_unused_code() # returns a list of `Item` objects
222 | ```
223 |
224 | ## How does it work?
225 |
226 | Vulture uses the `ast` module to build abstract syntax trees for all
227 | given files. While traversing all syntax trees it records the names of
228 | defined and used objects. Afterwards, it reports the objects which have
229 | been defined, but not used. This analysis ignores scopes and only takes
230 | object names into account.
231 |
232 | Vulture also detects unreachable code by looking for code after
233 | `return`, `break`, `continue` and `raise` statements, and by searching
234 | for unsatisfiable `if`- and `while`-conditions.
235 |
236 | ## Sort by size
237 |
238 | When using the `--sort-by-size` option, Vulture sorts unused code by its
239 | number of lines. This helps developers prioritize where to look for dead
240 | code first.
241 |
242 | ## Examples
243 |
244 | Consider the following Python script (`dead_code.py`):
245 |
246 | ``` python
247 | import os
248 |
249 | class Greeter:
250 | def greet(self):
251 | print("Hi")
252 |
253 | def hello_world():
254 | message = "Hello, world!"
255 | greeter = Greeter()
256 | func_name = "greet"
257 | greet_func = getattr(greeter, func_name)
258 | greet_func()
259 |
260 | if __name__ == "__main__":
261 | hello_world()
262 | ```
263 |
264 | Calling :
265 |
266 | $ vulture dead_code.py
267 |
268 | results in the following output:
269 |
270 | dead_code.py:1: unused import 'os' (90% confidence)
271 | dead_code.py:4: unused function 'greet' (60% confidence)
272 | dead_code.py:8: unused variable 'message' (60% confidence)
273 |
274 | Vulture correctly reports `os` and `message` as unused but it fails to
275 | detect that `greet` is actually used. The recommended method to deal
276 | with false positives like this is to create a whitelist Python file.
277 |
278 | **Preparing whitelists**
279 |
280 | In a whitelist we simulate the usage of variables, attributes, etc. For
281 | the program above, a whitelist could look as follows:
282 |
283 | ``` python
284 | # whitelist_dead_code.py
285 | from dead_code import Greeter
286 | Greeter.greet
287 | ```
288 |
289 | Alternatively, you can pass `--make-whitelist` to Vulture and obtain an
290 | automatically generated whitelist.
291 |
292 | Passing both the original program and the whitelist to Vulture
293 |
294 | $ vulture dead_code.py whitelist_dead_code.py
295 |
296 | makes Vulture ignore the `greet` method:
297 |
298 | dead_code.py:1: unused import 'os' (90% confidence)
299 | dead_code.py:8: unused variable 'message' (60% confidence)
300 |
301 |
331 |
332 | ## Exit codes
333 |
334 | | Exit code | Description |
335 | | --------- | ------------------------------------------------------------- |
336 | | 0 | No dead code found |
337 | | 1 | Invalid input (file missing, syntax error, wrong encoding) |
338 | | 2 | Invalid command line arguments |
339 | | 3 | Dead code found |
340 |
341 | ## Similar programs
342 |
343 | - [pyflakes](https://pypi.org/project/pyflakes/) finds unused imports
344 | and unused local variables (in addition to many other programmatic
345 | errors).
346 | - [coverage](https://pypi.org/project/coverage/) finds unused code
347 | more reliably than Vulture, but requires all branches of the code to
348 | actually be run.
349 | - [uncalled](https://pypi.org/project/uncalled/) finds dead code by
350 | using the abstract syntax tree (like Vulture), regular expressions,
351 | or both.
352 | - [dead](https://pypi.org/project/dead/) finds dead code by using the
353 | abstract syntax tree (like Vulture).
354 |
355 | ## Participate
356 |
357 | Please visit to report any
358 | issues or to make pull requests.
359 |
360 | - Contributing guide:
361 | [CONTRIBUTING.md](https://github.com/jendrikseipp/vulture/blob/main/CONTRIBUTING.md)
362 | - Release notes:
363 | [CHANGELOG.md](https://github.com/jendrikseipp/vulture/blob/main/CHANGELOG.md)
364 | - Roadmap:
365 | [TODO.md](https://github.com/jendrikseipp/vulture/blob/main/TODO.md)
366 |
--------------------------------------------------------------------------------
/TODO.md:
--------------------------------------------------------------------------------
1 | # TODOs
2 |
3 | * Add --ignore-attributes-for-classes option. When visiting such a class,
4 | mark all its attributes as used. Fixes (partly):
5 | * https://github.com/jendrikseipp/vulture/issues/309
6 | * https://github.com/jendrikseipp/vulture/issues/264
7 | * https://github.com/jendrikseipp/vulture/issues/249
8 | * https://github.com/jendrikseipp/vulture/issues/315
9 | Use these as test cases.
10 | * Honor (speaking) pylint error codes (e.g., # pylint:
11 | disable=unused-import): unused-import, unused-variable, unused-argument,
12 | possibly-unused-variable and unreachable-code. See
13 | https://github.com/janjur/readable-pylint-messages#unused-import.
14 |
15 | # Non-TODOs
16 |
17 | * Ignore hidden files and directories (might be unexpected, use
18 | --exclude instead).
19 | * Use Assign instead of Name AST nodes for estimating the size of
20 | assignments (KISS).
21 | * Only count lines for unused code by storing a function `get_size` in
22 | Item for computing the size on demand. This is 1.5 times as slow as
23 | computing no sizes.
24 | * Compute sizes on demand. Storing nodes increases memory usage from
25 | ~120 MiB to ~580 MiB for tensorflow's Python code.
26 | * Detect unreachable code for `ast.Assert` (`assert False` is common
27 | idiom for aborting rogue code).
28 | * Detect superfluous expressions like `a <= b`, `42`, `foo and bar`
29 | occurring outside of a statement (hard to detect if code is
30 | unneeded).
31 | * Detect that body of `if foo:` is unreachable if foo is only assigned
32 | "false" values (complicated: e.g., foo = \[\]; foo.append(1); if
33 | foo: ...).
34 | * Use coverage.py to detect false-positives (\#109). Workflow too
35 | complicated.
36 | * Ignore some decorators by default: @app.route, @cli.command.
37 | * Ignore functions in conftest.py files that start with "pytest_".
38 |
--------------------------------------------------------------------------------
/dev/make-release-notes.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env python3
2 |
3 | import re
4 | import sys
5 |
6 | _, VERSION, CHANGELOG, LIST = sys.argv
7 | HEADER_REGEX = rf"# {VERSION} \(\d\d\d\d-\d\d-\d\d\)\n"
8 |
9 | notes_list = []
10 |
11 |
12 | with open(CHANGELOG) as f:
13 | first_line = next(f)
14 | if not re.match(HEADER_REGEX, first_line):
15 | sys.exit(
16 | f'First changelog line "{first_line.rstrip()}" must '
17 | f'start with "{HEADER_REGEX.rstrip()}"'
18 | )
19 | notes_list.extend([first_line[2:], "\n"])
20 | line = next(f) # Skip empty line.
21 | if line.strip():
22 | sys.exit("There must be an empty line after each header")
23 | for line in f:
24 | if not line.strip():
25 | break
26 | else:
27 | notes_list.append(line)
28 |
29 |
30 | def check(name, text):
31 | print("*" * 60)
32 | print(text)
33 | print("*" * 60)
34 | response = input(f"Accept this {name} (Y/n)? ").strip().lower()
35 | if response and response != "y":
36 | sys.exit(1)
37 |
38 |
39 | check("changelog", "".join(notes_list))
40 |
41 | with open(LIST, "w") as f:
42 | f.writelines(notes_list)
43 |
--------------------------------------------------------------------------------
/dev/release.sh:
--------------------------------------------------------------------------------
1 | #! /bin/bash
2 |
3 | set -exuo pipefail
4 |
5 | VERSION="$1"
6 | CHANGES="/tmp/vulture-$VERSION-changes"
7 |
8 | cd "$(dirname ${0})/../"
9 |
10 | # Check dependencies.
11 | hub --version > /dev/null
12 | tox --version > /dev/null
13 | twine -h > /dev/null
14 |
15 | # Check for uncommitted changes.
16 | set +e
17 | git diff --quiet && git diff --cached --quiet
18 | retcode=$?
19 | set -e
20 | if [[ $retcode != 0 ]]; then
21 | echo "There are uncommitted changes:"
22 | git status
23 | exit 1
24 | fi
25 |
26 | git pull
27 |
28 | tox
29 |
30 | # Bump version.
31 | sed -i -e "s/__version__ = \".*\"/__version__ = \"$VERSION\"/" vulture/version.py
32 | git commit -am "Update version number to ${VERSION} for release."
33 | git tag -a "v$VERSION" -m "v$VERSION" HEAD
34 |
35 | python3 setup.py sdist bdist_wheel --universal
36 | twine upload dist/vulture-${VERSION}.tar.gz dist/vulture-${VERSION}-py2.py3-none-any.whl
37 |
38 | git push
39 | git push --tags
40 |
41 | # Add changelog to Github release.
42 | ./dev/make-release-notes.py "$VERSION" CHANGELOG.md "$CHANGES"
43 | hub release create v"$VERSION" --file="$CHANGES"
44 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [tool.ruff]
2 | exclude = [
3 | ".eggs",
4 | ".git",
5 | "_build",
6 | "build",
7 | "dist",
8 | "htmlcov",
9 | "vulture.egg-info",
10 | ".cache",
11 | ".coverage",
12 | ".pytest_cache",
13 | ".tox",
14 | ".venv",
15 | ".vscode",
16 | ]
17 |
18 | # Same as Black.
19 | line-length = 79
20 | indent-width = 4
21 |
22 | target-version = "py38"
23 |
24 | [tool.ruff.lint]
25 | # ruff enables Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
26 | select = [
27 | "B", # flake8-bugbear
28 | "C4", # comprehensions
29 | "E", # pycodestyle
30 | "F", # pyflakes
31 | "I001", # isort
32 | "SIM", # flake8-simplify
33 | "UP", # pyupgrade
34 | ]
35 | ignore = [
36 | "C408", # unnecessary dict call
37 | "SIM115", # Use context handler for opening files
38 | ]
39 |
40 | # Allow fix for all enabled rules (when `--fix`) is provided.
41 | fixable = ["ALL"]
42 | unfixable = []
43 |
44 | # Allow unused variables when underscore-prefixed.
45 | dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
46 |
47 | [tool.ruff.lint.per-file-ignores]
48 | "vulture/whitelists/*.py" = ["B018"]
49 |
50 | [tool.ruff.format]
51 | # Like Black, use double quotes for strings.
52 | quote-style = "double"
53 |
54 | # Like Black, indent with spaces, rather than tabs.
55 | indent-style = "space"
56 |
57 | # Like Black, respect magic trailing commas.
58 | skip-magic-trailing-comma = false
59 |
60 | # Like Black, automatically detect the appropriate line ending.
61 | line-ending = "auto"
62 |
63 | # Enable auto-formatting of code examples in docstrings. Markdown,
64 | # reStructuredText code/literal blocks and doctests are all supported.
65 | #
66 | # This is currently disabled by default, but it is planned for this
67 | # to be opt-out in the future.
68 | docstring-code-format = false
69 |
70 | # Set the line length limit used when formatting code snippets in
71 | # docstrings.
72 | #
73 | # This only has an effect when the `docstring-code-format` setting is
74 | # enabled.
75 | docstring-code-line-length = "dynamic"
76 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | tomli >= 1.1.0; python_version < '3.11'
2 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [coverage:run]
2 | omit =
3 | setup.py
4 | .tox/*
5 | parallel = true
6 |
7 | [tool:pytest]
8 | addopts = --cov vulture --cov-report=html --cov-report=term --cov-report=xml --cov-append
9 |
10 | [bdist_wheel]
11 | universal = 1
12 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env python
2 |
3 | import pathlib
4 | import re
5 |
6 | import setuptools
7 |
8 |
9 | def find_version(*parts):
10 | here = pathlib.Path(__file__).parent
11 | version_file = here.joinpath(*parts).read_text()
12 | version_match = re.search(
13 | r"^__version__ = ['\"]([^'\"]*)['\"]$", version_file, re.M
14 | )
15 | if version_match:
16 | return version_match.group(1)
17 | raise RuntimeError("Unable to find version string.")
18 |
19 |
20 | with open("README.md") as f1, open("CHANGELOG.md") as f2:
21 | long_description = f1.read() + "\n\n" + f2.read()
22 |
23 | with open("requirements.txt") as f:
24 | install_requires = f.read().splitlines()
25 |
26 | setuptools.setup(
27 | name="vulture",
28 | version=find_version("vulture", "version.py"),
29 | description="Find dead code",
30 | long_description=long_description,
31 | long_description_content_type="text/markdown",
32 | keywords="dead-code-removal",
33 | author="Jendrik Seipp",
34 | author_email="jendrikseipp@gmail.com",
35 | url="https://github.com/jendrikseipp/vulture",
36 | license="MIT",
37 | classifiers=[
38 | "Development Status :: 5 - Production/Stable",
39 | "Environment :: Console",
40 | "Intended Audience :: Developers",
41 | "License :: OSI Approved :: MIT License",
42 | "Programming Language :: Python",
43 | "Programming Language :: Python :: 3",
44 | "Programming Language :: Python :: 3.8",
45 | "Programming Language :: Python :: 3.9",
46 | "Programming Language :: Python :: 3.10",
47 | "Programming Language :: Python :: 3.11",
48 | "Programming Language :: Python :: 3.12",
49 | "Programming Language :: Python :: 3.13",
50 | "Programming Language :: Python :: Implementation :: CPython",
51 | "Programming Language :: Python :: Implementation :: PyPy",
52 | "Topic :: Software Development :: Quality Assurance",
53 | ],
54 | install_requires=install_requires,
55 | entry_points={"console_scripts": ["vulture = vulture.core:main"]},
56 | python_requires=">=3.8",
57 | packages=setuptools.find_packages(exclude=["tests"]),
58 | package_data={"vulture": ["whitelists/*.py"]},
59 | )
60 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
1 | import pathlib
2 | import subprocess
3 | import sys
4 |
5 | import pytest
6 |
7 | from vulture import core
8 |
9 | REPO = pathlib.Path(__file__).resolve().parents[1]
10 | WHITELISTS = [
11 | str(path)
12 | for path in (REPO / "vulture" / "whitelists").glob("*.py")
13 | # Pint is incompatible with Python 3.13 (https://github.com/hgrecco/pint/issues/1969).
14 | if sys.version_info < (3, 13) or path.name != "pint_whitelist.py"
15 | ]
16 |
17 |
18 | def call_vulture(args, **kwargs):
19 | return subprocess.call(
20 | [sys.executable, "-m", "vulture"] + args, cwd=REPO, **kwargs
21 | )
22 |
23 |
24 | def check(items_or_names, expected_names):
25 | """items_or_names must be a collection of Items or a set of strings."""
26 | try:
27 | assert sorted(item.name for item in items_or_names) == sorted(
28 | expected_names
29 | )
30 | except AttributeError:
31 | assert items_or_names == set(expected_names)
32 |
33 |
34 | def check_unreachable(v, lineno, size, name):
35 | assert len(v.unreachable_code) == 1
36 | item = v.unreachable_code[0]
37 | assert item.first_lineno == lineno
38 | assert item.size == size
39 | assert item.name == name
40 |
41 |
42 | def check_multiple_unreachable(v, checks):
43 | assert len(v.unreachable_code) == len(checks)
44 | for item, (lineno, size, name) in zip(v.unreachable_code, checks):
45 | assert item.first_lineno == lineno
46 | assert item.size == size
47 | assert item.name == name
48 |
49 |
50 | @pytest.fixture
51 | def v():
52 | return core.Vulture(verbose=True)
53 |
--------------------------------------------------------------------------------
/tests/test_conditions.py:
--------------------------------------------------------------------------------
1 | import ast
2 |
3 | from vulture import utils
4 |
5 | from . import v
6 |
7 | assert v # Silence pyflakes
8 |
9 |
10 | def check_condition(code, result):
11 | condition = ast.parse(code, mode="eval").body
12 | if result:
13 | assert utils.condition_is_always_true(condition)
14 | else:
15 | assert utils.condition_is_always_false(condition)
16 |
17 |
18 | def test_false():
19 | check_condition("False", False)
20 | check_condition("None", False)
21 | check_condition("0", False)
22 |
23 |
24 | def test_empty():
25 | check_condition("''", False)
26 | check_condition("[]", False)
27 | check_condition("{}", False)
28 |
29 |
30 | def test_true():
31 | check_condition("True", True)
32 | check_condition("2", True)
33 | check_condition("'s'", True)
34 | check_condition("['foo', 'bar']", True)
35 | check_condition("{'a': 1, 'b': 2}", True)
36 |
37 |
38 | def test_complex_conditions():
39 | conditions = [
40 | ("foo and False", True, False),
41 | ("foo or False", False, False),
42 | ("foo and True", False, False),
43 | ("foo or True", False, True),
44 | ("False and foo", True, False),
45 | ("False and 1", True, False),
46 | ("not False", False, True),
47 | ("not True", True, False),
48 | ("not foo", False, False),
49 | ("foo and (False or [])", True, False),
50 | ('(foo and bar) or {"a": 1}', False, True),
51 | ]
52 | for condition, always_false, always_true in conditions:
53 | condition = ast.parse(condition, mode="eval").body
54 | assert not (always_false and always_true)
55 | assert utils.condition_is_always_false(condition) == always_false
56 | assert utils.condition_is_always_true(condition) == always_true
57 |
58 |
59 | def test_errors():
60 | conditions = [
61 | "foo",
62 | '__name__ == "__main__"',
63 | "chr(-1)",
64 | 'getattr(True, "foo")',
65 | 'hasattr(str, "foo")',
66 | "isinstance(True, True)",
67 | "globals()",
68 | "locals()",
69 | "().__class__",
70 | ]
71 | for condition in conditions:
72 | condition = ast.parse(condition, mode="eval").body
73 | assert not utils.condition_is_always_false(condition)
74 | assert not utils.condition_is_always_true(condition)
75 |
--------------------------------------------------------------------------------
/tests/test_confidence.py:
--------------------------------------------------------------------------------
1 | from vulture import core
2 |
3 | dc = core.DEFAULT_CONFIDENCE
4 |
5 |
6 | def check_min_confidence(code, min_confidence, expected):
7 | v = core.Vulture(verbose=True)
8 | v.scan(code)
9 | detected = {
10 | item.name: item.confidence
11 | for item in v.get_unused_code(min_confidence=min_confidence)
12 | }
13 | assert detected == expected
14 |
15 |
16 | def test_confidence_import():
17 | code = """\
18 | import foo
19 | """
20 | check_min_confidence(code, 50, {"foo": 90})
21 | check_min_confidence(code, 100, {})
22 |
23 |
24 | def test_confidence_unreachable():
25 | code = """\
26 | def foo():
27 | return
28 | bar()
29 |
30 | foo()
31 | """
32 | check_min_confidence(code, 50, {"return": 100})
33 | check_min_confidence(code, 100, {"return": 100})
34 |
35 |
36 | def test_function_arg():
37 | code = """\
38 | def foo(a):
39 | b = 3
40 |
41 | foo(5)
42 | """
43 | check_min_confidence(code, 50, {"a": 100, "b": dc})
44 | check_min_confidence(code, dc, {"a": 100, "b": dc})
45 | check_min_confidence(code, 100, {"a": 100})
46 |
47 |
48 | def test_confidence_class():
49 | code = """\
50 | class Foo:
51 | pass
52 | """
53 | check_min_confidence(code, 50, {"Foo": dc})
54 | check_min_confidence(code, 100, {})
55 |
56 |
57 | def test_confidence_attr():
58 | code = "A.b = 'something'"
59 | check_min_confidence(code, 50, {"b": dc})
60 | check_min_confidence(code, 100, {})
61 |
62 |
63 | def test_confidence_props():
64 | code = """\
65 | class Foo:
66 | @property
67 | def some_prop():
68 | pass
69 |
70 | Foo()
71 | """
72 | check_min_confidence(code, 50, {"some_prop": dc})
73 | check_min_confidence(code, 100, {})
74 |
75 |
76 | def test_confidence_async_def():
77 | code = """\
78 | async def foo():
79 | if bar():
80 | pass
81 | else:
82 | print("Else")
83 | """
84 | check_min_confidence(code, 50, {"foo": dc})
85 | check_min_confidence(code, 75, {})
86 |
--------------------------------------------------------------------------------
/tests/test_config.py:
--------------------------------------------------------------------------------
1 | """
2 | Unit tests for config file and CLI argument parsing.
3 | """
4 |
5 | import pathlib
6 | from io import BytesIO
7 | from textwrap import dedent
8 |
9 | import pytest
10 |
11 | from vulture.config import (
12 | DEFAULTS,
13 | InputError,
14 | _check_input_config,
15 | _parse_args,
16 | _parse_toml,
17 | make_config,
18 | )
19 |
20 |
21 | def get_toml_bytes(toml_str: str) -> BytesIO:
22 | """
23 | Wrap a string in BytesIO to play the role of the incoming config stream.
24 | """
25 | return BytesIO(bytes(toml_str, "utf-8"))
26 |
27 |
28 | def test_cli_args():
29 | """
30 | Ensure that CLI arguments are converted to a config object.
31 | """
32 | expected = dict(
33 | paths=["path1", "path2"],
34 | exclude=["file*.py", "dir/"],
35 | ignore_decorators=["deco1", "deco2"],
36 | ignore_names=["name1", "name2"],
37 | config="pyproject.toml",
38 | make_whitelist=True,
39 | min_confidence=10,
40 | sort_by_size=True,
41 | verbose=True,
42 | )
43 | result = _parse_args(
44 | [
45 | "--exclude=file*.py,dir/",
46 | "--ignore-decorators=deco1,deco2",
47 | "--ignore-names=name1,name2",
48 | "--make-whitelist",
49 | "--min-confidence=10",
50 | "--sort-by-size",
51 | "--verbose",
52 | "path1",
53 | "path2",
54 | ]
55 | )
56 | assert isinstance(result, dict)
57 | assert result == expected
58 |
59 |
60 | def test_toml_config():
61 | """
62 | Ensure parsing of TOML files results in a valid config object.
63 | """
64 | expected = dict(
65 | paths=["path1", "path2"],
66 | exclude=["file*.py", "dir/"],
67 | ignore_decorators=["deco1", "deco2"],
68 | ignore_names=["name1", "name2"],
69 | make_whitelist=True,
70 | min_confidence=10,
71 | sort_by_size=True,
72 | verbose=True,
73 | )
74 | data = get_toml_bytes(
75 | dedent(
76 | """\
77 | [tool.vulture]
78 | exclude = ["file*.py", "dir/"]
79 | ignore_decorators = ["deco1", "deco2"]
80 | ignore_names = ["name1", "name2"]
81 | make_whitelist = true
82 | min_confidence = 10
83 | sort_by_size = true
84 | verbose = true
85 | paths = ["path1", "path2"]
86 | """
87 | )
88 | )
89 | result = _parse_toml(data)
90 | assert isinstance(result, dict)
91 | assert result == expected
92 |
93 |
94 | def test_toml_config_with_heterogenous_array():
95 | """
96 | Ensure parsing of TOML files results in a valid config object, even if some
97 | other part of the file contains an array of mixed types.
98 | """
99 | expected = dict(
100 | paths=["path1", "path2"],
101 | exclude=["file*.py", "dir/"],
102 | ignore_decorators=["deco1", "deco2"],
103 | ignore_names=["name1", "name2"],
104 | make_whitelist=True,
105 | min_confidence=10,
106 | sort_by_size=True,
107 | verbose=True,
108 | )
109 | data = get_toml_bytes(
110 | dedent(
111 | """\
112 | [tool.foo]
113 | # comment for good measure
114 | problem_array = [{a = 1}, [2,3,4], "foo"]
115 |
116 | [tool.vulture]
117 | exclude = ["file*.py", "dir/"]
118 | ignore_decorators = ["deco1", "deco2"]
119 | ignore_names = ["name1", "name2"]
120 | make_whitelist = true
121 | min_confidence = 10
122 | sort_by_size = true
123 | verbose = true
124 | paths = ["path1", "path2"]
125 | """
126 | )
127 | )
128 | result = _parse_toml(data)
129 | assert isinstance(result, dict)
130 | assert result == expected
131 |
132 |
133 | def test_config_merging():
134 | """
135 | If we have both CLI args and a ``pyproject.toml`` file, the CLI args should
136 | have precedence.
137 | """
138 | toml = get_toml_bytes(
139 | dedent(
140 | """\
141 | [tool.vulture]
142 | exclude = ["toml_exclude"]
143 | ignore_decorators = ["toml_deco"]
144 | ignore_names = ["toml_name"]
145 | make_whitelist = false
146 | min_confidence = 10
147 | sort_by_size = false
148 | verbose = false
149 | paths = ["toml_path"]
150 | """
151 | )
152 | )
153 | cliargs = [
154 | "--exclude=cli_exclude",
155 | "--ignore-decorators=cli_deco",
156 | "--ignore-names=cli_name",
157 | "--make-whitelist",
158 | "--min-confidence=20",
159 | "--sort-by-size",
160 | "--verbose",
161 | "cli_path",
162 | ]
163 | result = make_config(cliargs, toml)
164 | expected = dict(
165 | paths=["cli_path"],
166 | exclude=["cli_exclude"],
167 | ignore_decorators=["cli_deco"],
168 | ignore_names=["cli_name"],
169 | config="pyproject.toml",
170 | make_whitelist=True,
171 | min_confidence=20,
172 | sort_by_size=True,
173 | verbose=True,
174 | )
175 | assert result == expected
176 |
177 |
178 | def test_toml_config_custom_path():
179 | """
180 | Ensure that TOML pyproject.toml files can be read from a custom path,
181 | other than the current working directory.
182 |
183 | Test file is in tests/toml/mock_pyproject.toml
184 | """
185 | here = pathlib.Path(__file__).parent
186 | tomlfile_path = here.joinpath("toml", "mock_pyproject.toml")
187 | cliargs = [
188 | f"--config={tomlfile_path}",
189 | "cli_path",
190 | ]
191 | result = make_config(cliargs)
192 | assert result["ignore_names"] == ["name_from_toml_file"]
193 |
194 |
195 | def test_config_merging_missing():
196 | """
197 | If we have set a boolean value in the TOML file, but not on the CLI, we
198 | want the TOML value to be taken.
199 | """
200 | toml = get_toml_bytes(
201 | dedent(
202 | """\
203 | [tool.vulture]
204 | verbose = true
205 | ignore_names = ["name1"]
206 | """
207 | )
208 | )
209 | cliargs = [
210 | "cli_path",
211 | ]
212 | result = make_config(cliargs, toml)
213 | assert result["verbose"] is True
214 | assert result["ignore_names"] == ["name1"]
215 |
216 |
217 | def test_config_merging_toml_paths_only():
218 | """
219 | If we have paths in the TOML but not on the CLI, the TOML paths should be
220 | used.
221 | """
222 | toml = get_toml_bytes(
223 | dedent(
224 | """\
225 | [tool.vulture]
226 | paths = ["path1", "path2"]
227 | """
228 | )
229 | )
230 | cliargs = [
231 | "--exclude=test_*.py",
232 | ]
233 | result = make_config(cliargs, toml)
234 | assert result["paths"] == ["path1", "path2"]
235 | assert result["exclude"] == ["test_*.py"]
236 |
237 |
238 | def test_invalid_config_options_output():
239 | """
240 | If the config file contains unknown options we want to abort.
241 | """
242 |
243 | with pytest.raises(InputError):
244 | _check_input_config({"unknown_key_1": 1})
245 |
246 |
247 | @pytest.mark.parametrize("key, value", list(DEFAULTS.items()))
248 | def test_incompatible_option_type(key, value):
249 | """
250 | If a config value has a different type from the default value we abort.
251 | """
252 | wrong_types = {int, str, list, bool} - {type(value)}
253 | for wrong_type in wrong_types:
254 | test_value = wrong_type()
255 | with pytest.raises(InputError):
256 | _check_input_config({key: test_value})
257 |
258 |
259 | def test_missing_paths():
260 | """
261 | If the script is run without any paths, we want to abort.
262 | """
263 | with pytest.raises(InputError):
264 | make_config([])
265 |
--------------------------------------------------------------------------------
/tests/test_encoding.py:
--------------------------------------------------------------------------------
1 | import codecs
2 |
3 | from vulture.utils import ExitCode
4 |
5 | from . import v
6 |
7 | assert v # Silence pyflakes.
8 |
9 |
10 | def test_encoding1(v):
11 | v.scan(
12 | """\
13 | # -*- coding: utf-8 -*-
14 | pass
15 | """
16 | )
17 | assert v.exit_code == ExitCode.NoDeadCode
18 |
19 |
20 | def test_encoding2(v):
21 | v.scan(
22 | """\
23 | #! /usr/bin/env python
24 | # -*- coding: utf-8 -*-
25 | pass
26 | """
27 | )
28 | assert v.exit_code == ExitCode.NoDeadCode
29 |
30 |
31 | def test_non_utf8_encoding(v, tmp_path):
32 | code = ""
33 | name = "non_utf8"
34 | non_utf_8_file = tmp_path / (name + ".py")
35 | with open(non_utf_8_file, mode="wb") as f:
36 | f.write(codecs.BOM_UTF16_LE)
37 | f.write(code.encode("utf_16_le"))
38 | v.scavenge([non_utf_8_file])
39 | assert v.exit_code == ExitCode.InvalidInput
40 |
41 |
42 | def test_utf8_with_bom(v, tmp_path):
43 | name = "utf8_bom"
44 | filepath = tmp_path / (name + ".py")
45 | # utf8_sig prepends the BOM to the file.
46 | filepath.write_text("", encoding="utf-8-sig")
47 | v.scavenge([filepath])
48 | assert v.exit_code == ExitCode.NoDeadCode
49 |
--------------------------------------------------------------------------------
/tests/test_errors.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from vulture.utils import ExitCode
4 |
5 | from . import call_vulture, v
6 |
7 | assert v # Silence pyflakes.
8 |
9 |
10 | def test_syntax_error(v):
11 | v.scan("foo bar")
12 | assert int(v.report()) == ExitCode.InvalidInput
13 |
14 |
15 | def test_null_byte(v):
16 | v.scan("\x00")
17 | assert int(v.report()) == ExitCode.InvalidInput
18 |
19 |
20 | def test_confidence_range(v):
21 | v.scan(
22 | """\
23 | def foo():
24 | pass
25 | """
26 | )
27 | with pytest.raises(ValueError):
28 | v.get_unused_code(min_confidence=150)
29 |
30 |
31 | def test_invalid_cmdline_args():
32 | assert (
33 | call_vulture(["vulture/", "--invalid-argument"])
34 | == ExitCode.InvalidCmdlineArguments
35 | )
36 |
--------------------------------------------------------------------------------
/tests/test_format_strings.py:
--------------------------------------------------------------------------------
1 | from . import check, v
2 |
3 | assert v # Silence pyflakes.
4 |
5 |
6 | def test_old_format_string(v):
7 | v.scan("'%(a)s, %(b)d' % locals()")
8 | check(v.used_names, ["a", "b", "locals"])
9 |
10 |
11 | def test_new_format_string(v):
12 | v.scan("'{a}, {b:0d} {c:<30} {d:.2%}'.format(**locals())")
13 | check(v.used_names, ["a", "b", "c", "d", "format", "locals"])
14 |
15 |
16 | def test_f_string(v):
17 | v.scan(
18 | """\
19 | f'{a}, {b:0d} {c:<30} {d:.2%} {e()} {f:{width}.{precision}}'
20 | f'{ {x:y for (x, y) in ((1, 2), (3, 4))} }'
21 | """
22 | )
23 | check(
24 | v.used_names,
25 | ["a", "b", "c", "d", "e", "f", "precision", "width", "x", "y"],
26 | )
27 |
28 |
29 | def test_new_format_string_access(v):
30 | v.scan("'{a.b}, {c.d.e} {f[g]} {h[i][j].k}'.format(**locals())")
31 | check(
32 | v.used_names,
33 | ["a", "b", "c", "d", "e", "f", "h", "k", "format", "locals"],
34 | )
35 |
36 |
37 | def test_new_format_string_numbers(v):
38 | v.scan("'{0.b}, {0.d.e} {0[1]} {0[1][1].k}'.format(**locals())")
39 | check(v.used_names, ["b", "d", "e", "k", "format", "locals"])
40 |
41 |
42 | def test_incorrect_format_string(v):
43 | v.scan('"{"')
44 | v.scan('"{!-a:}"')
45 | check(v.used_names, [])
46 |
47 |
48 | def test_format_string_not_using_locals(v):
49 | """Strings that are not formatted with locals() should not be parsed."""
50 | v.scan(
51 | """\
52 | "{variable}"
53 |
54 | def foobar():
55 | '''
56 | Return data of the form
57 | {this_looks_like_a_format_string: 1}
58 | '''
59 | pass
60 |
61 | "%(thing)s" % {"thing": 1}
62 |
63 | "%(apple)s" * locals()
64 |
65 | "{} {a} {b}".format(1, a=used_var, b=locals())
66 | """
67 | )
68 | check(v.used_names, ["used_var", "locals", "format"])
69 |
--------------------------------------------------------------------------------
/tests/test_ignore.py:
--------------------------------------------------------------------------------
1 | from vulture import core
2 |
3 | from . import check
4 |
5 |
6 | def check_ignore(code, ignore_names, ignore_decorators, expected):
7 | v = core.Vulture(
8 | verbose=True,
9 | ignore_names=ignore_names,
10 | ignore_decorators=ignore_decorators,
11 | )
12 | v.scan(code)
13 | check(v.get_unused_code(), expected)
14 |
15 |
16 | def test_var():
17 | code = """\
18 | fio = 1
19 | fao = 2
20 | bar = 2
21 | ftobar = 3
22 | baz = 10000
23 | funny = True
24 | """
25 | check_ignore(code, ["f?o*", "ba[rz]"], [], ["funny"])
26 |
27 |
28 | def test_function():
29 | code = """\
30 | def foo_one():
31 | pass
32 | def foo_two():
33 | pass
34 | def foo():
35 | pass
36 | def bar():
37 | pass
38 | """
39 | check_ignore(code, ["foo*"], [], ["bar"])
40 |
41 |
42 | def test_async_function():
43 | code = """\
44 | async def foobar():
45 | pass
46 | async def bar():
47 | pass
48 | """
49 | check_ignore(code, ["foo*"], [], ["bar"])
50 |
51 |
52 | def test_class():
53 | code = """\
54 | class Foo:
55 | def __init__(self):
56 | pass
57 | """
58 | check_ignore(code, ["Foo"], [], [])
59 |
60 |
61 | def test_class_ignore():
62 | code = """\
63 | @bar
64 | class Foo:
65 | pass
66 |
67 | class Bar:
68 | pass
69 | """
70 | check_ignore(code, [], [], ["Foo", "Bar"])
71 |
72 |
73 | def test_property():
74 | code = """\
75 | class Foo:
76 | @property
77 | def some_property(self, a):
78 | return a
79 |
80 | @property
81 | @bar
82 | def foo_bar(self):
83 | return 'bar'
84 | """
85 | check_ignore(code, ["Foo"], ["@property"], [])
86 | check_ignore(code, ["Foo"], [], ["some_property", "foo_bar"])
87 |
88 |
89 | def test_attribute():
90 | code = """\
91 | class Foo:
92 | def __init__(self, attr_foo, attr_bar):
93 | self._attr_foo = attr_foo
94 | self._attr_bar = attr_bar
95 | """
96 | check_ignore(code, ["foo", "*_foo"], [], ["Foo", "_attr_bar"])
97 |
98 |
99 | def test_decorated_functions():
100 | code = """\
101 | def decor():
102 | return help
103 |
104 | class FooBar:
105 | def foobar(self):
106 | return help
107 |
108 | @property
109 | def prop_one(self):
110 | pass
111 |
112 | f = FooBar()
113 |
114 | @decor()
115 | def bar():
116 | pass
117 |
118 | @f.foobar
119 | def foo():
120 | pass
121 |
122 | @bar
123 | @foo
124 | @f.foobar()
125 | def barfoo():
126 | pass
127 | """
128 | check_ignore(code, [], ["@decor", "*@f.foobar"], ["prop_one"])
129 | check_ignore(code, [], ["*decor", "@*f.foobar"], ["prop_one"])
130 |
131 |
132 | def test_decorated_async_functions():
133 | code = """\
134 | @app.route('something')
135 | @foobar
136 | async def async_function():
137 | pass
138 |
139 | @a.b.c
140 | async def foo():
141 | pass
142 | """
143 | check_ignore(code, [], ["@app.route", "@a.b"], ["foo"])
144 |
145 |
146 | def test_decorated_property():
147 | code = """\
148 | @bar
149 | @property
150 | def foo():
151 | pass
152 | """
153 | check_ignore(code, [], ["@bar"], [])
154 | check_ignore(code, [], ["@baz"], ["foo"])
155 | check_ignore(code, [], ["@property"], [])
156 |
157 |
158 | def test_decorated_property_reversed():
159 | code = """\
160 | @property
161 | @bar
162 | def foo():
163 | pass
164 | """
165 | check_ignore(code, [], ["@bar"], [])
166 | check_ignore(code, [], ["@property"], [])
167 | check_ignore(code, [], ["@b*r"], [])
168 | check_ignore(code, [], ["@barfoo"], ["foo"])
169 |
170 |
171 | def test_decorated_class():
172 | code = """\
173 | @barfoo
174 | @foo.bar('foo')
175 | class Bar:
176 | def __init__(self):
177 | pass
178 | """
179 | check_ignore(code, [], [], ["Bar"])
180 | check_ignore(code, [], ["@bar*"], [])
181 |
--------------------------------------------------------------------------------
/tests/test_imports.py:
--------------------------------------------------------------------------------
1 | from . import check, v
2 |
3 | assert v # Silence pyflakes.
4 |
5 |
6 | def test_import_star(v):
7 | v.scan(
8 | """\
9 | from a import *
10 | from a.b import *
11 | """
12 | )
13 | check(v.defined_imports, [])
14 | check(v.unused_imports, [])
15 |
16 |
17 | def test_import_from_future(v):
18 | v.scan("""from __future__ import division""")
19 | check(v.defined_imports, [])
20 | check(v.unused_imports, [])
21 |
22 |
23 | def test_double_import(v):
24 | v.scan(
25 | """\
26 | import foo as bar
27 | import foo
28 | """
29 | )
30 | check(v.defined_imports, ["bar", "foo"])
31 | # Once the bar import is removed, the foo import will be detected.
32 | check(v.unused_imports, ["bar"])
33 |
34 |
35 | def test_attribute_access(v):
36 | v.scan(
37 | """\
38 | # foo.py
39 | class Foo:
40 | pass
41 |
42 | # bar.py
43 | from foo import Foo
44 |
45 | # main.py
46 | import bar
47 | bar.Foo
48 | """
49 | )
50 | check(v.defined_imports, ["Foo", "bar"])
51 | check(v.unused_imports, [])
52 |
53 |
54 | def test_nested_import(v):
55 | v.scan(
56 | """\
57 | import os.path
58 | os.path.expanduser("~")
59 | """
60 | )
61 | check(v.defined_imports, ["os"])
62 | check(v.used_names, ["os", "path", "expanduser"])
63 | check(v.unused_funcs, [])
64 | check(v.unused_imports, [])
65 | check(v.unused_vars, [])
66 |
67 |
68 | definitions = """\
69 | class A(object):
70 | pass
71 | class B(object):
72 | pass
73 | def C():
74 | pass
75 | D = 42
76 | """
77 |
78 | imports = """\
79 | from any_module import A
80 | import B, C
81 | import D
82 | """
83 |
84 | aliased_imports = """\
85 | from any_module import A as AliasA
86 | import B as AliasB, C as AliasC
87 | import D as AliasD
88 | """
89 |
90 | uses = """\
91 | A()
92 | B()
93 | C()
94 | D()
95 | """
96 |
97 | aliased_uses = """\
98 | AliasA()
99 | AliasB()
100 | AliasC()
101 | AliasD()
102 | """
103 |
104 |
105 | def test_definitions(v):
106 | v.scan(definitions)
107 | check(v.defined_classes, ["A", "B"])
108 | check(v.defined_funcs, ["C"])
109 | check(v.defined_imports, [])
110 | check(v.defined_vars, ["D"])
111 | check(v.used_names, [])
112 | check(v.unused_classes, ["A", "B"])
113 | check(v.unused_funcs, ["C"])
114 | check(v.unused_imports, [])
115 | check(v.unused_vars, ["D"])
116 |
117 |
118 | def test_use_original(v):
119 | v.scan(definitions + uses)
120 | check(v.defined_classes, ["A", "B"])
121 | check(v.defined_funcs, ["C"])
122 | check(v.defined_imports, [])
123 | check(v.defined_vars, ["D"])
124 | check(v.used_names, ["A", "B", "C", "D"])
125 | check(v.unused_funcs, [])
126 | check(v.unused_classes, [])
127 | check(v.unused_imports, [])
128 | check(v.unused_vars, [])
129 |
130 |
131 | def test_import_original(v):
132 | v.scan(definitions + imports)
133 | check(v.defined_classes, ["A", "B"])
134 | check(v.defined_funcs, ["C"])
135 | check(v.defined_imports, ["A", "B", "C", "D"])
136 | check(v.defined_vars, ["D"])
137 | check(v.used_names, [])
138 | check(v.unused_classes, ["A", "B"])
139 | check(v.unused_funcs, ["C"])
140 | check(v.unused_imports, ["A", "B", "C", "D"])
141 | check(v.unused_vars, ["D"])
142 |
143 |
144 | def test_import_original_use_original(v):
145 | v.scan(definitions + imports + uses)
146 | check(v.defined_classes, ["A", "B"])
147 | check(v.defined_funcs, ["C"])
148 | check(v.defined_imports, ["A", "B", "C", "D"])
149 | check(v.defined_vars, ["D"])
150 | check(v.used_names, ["A", "B", "C", "D"])
151 | check(v.unused_classes, [])
152 | check(v.unused_funcs, [])
153 | check(v.unused_imports, [])
154 | check(v.unused_vars, [])
155 |
156 |
157 | def test_import_original_use_alias(v):
158 | v.scan(definitions + imports + aliased_uses)
159 | check(v.defined_classes, ["A", "B"])
160 | check(v.defined_funcs, ["C"])
161 | check(v.defined_imports, ["A", "B", "C", "D"])
162 | check(v.defined_vars, ["D"])
163 | check(v.used_names, ["AliasA", "AliasB", "AliasC", "AliasD"])
164 | check(v.unused_classes, ["A", "B"])
165 | check(v.unused_funcs, ["C"])
166 | check(v.unused_imports, ["A", "B", "C", "D"])
167 | check(v.unused_vars, ["D"])
168 |
169 |
170 | def test_import_alias(v):
171 | v.scan(definitions + aliased_imports)
172 | check(v.defined_classes, ["A", "B"])
173 | check(v.defined_funcs, ["C"])
174 | check(v.defined_imports, ["AliasA", "AliasB", "AliasC", "AliasD"])
175 | check(v.defined_vars, ["D"])
176 | check(v.used_names, ["A", "B", "C", "D"])
177 | check(v.unused_classes, [])
178 | check(v.unused_funcs, [])
179 | check(v.unused_imports, ["AliasA", "AliasB", "AliasC", "AliasD"])
180 | check(v.unused_vars, [])
181 |
182 |
183 | def test_import_alias_use_original(v):
184 | v.scan(definitions + aliased_imports + uses)
185 | check(v.defined_classes, ["A", "B"])
186 | check(v.defined_funcs, ["C"])
187 | check(v.defined_imports, ["AliasA", "AliasB", "AliasC", "AliasD"])
188 | check(v.defined_vars, ["D"])
189 | check(v.used_names, ["A", "B", "C", "D"])
190 | check(v.unused_classes, [])
191 | check(v.unused_funcs, [])
192 | check(v.unused_imports, ["AliasA", "AliasB", "AliasC", "AliasD"])
193 | check(v.unused_vars, [])
194 |
195 |
196 | def test_import_alias_use_alias(v):
197 | v.scan(definitions + aliased_imports + aliased_uses)
198 | check(v.defined_classes, ["A", "B"])
199 | check(v.defined_funcs, ["C"])
200 | check(v.defined_imports, ["AliasA", "AliasB", "AliasC", "AliasD"])
201 | check(v.defined_vars, ["D"])
202 | check(
203 | v.used_names,
204 | ["A", "B", "C", "D", "AliasA", "AliasB", "AliasC", "AliasD"],
205 | )
206 | check(v.unused_classes, [])
207 | check(v.unused_funcs, [])
208 | check(v.unused_imports, [])
209 | check(v.unused_vars, [])
210 |
211 |
212 | def test_import_with__all__(v):
213 | v.scan(
214 | """\
215 | # define.py
216 | class Foo:
217 | pass
218 |
219 | class Bar:
220 | pass
221 |
222 | # main.py
223 | from define import Foo, Bar
224 |
225 | __all__ = ["Foo"]
226 |
227 | """
228 | )
229 | check(v.defined_imports, ["Foo", "Bar"])
230 | check(v.unused_imports, ["Bar"])
231 |
232 |
233 | def test_import_with__all__normal_reference(v):
234 | v.scan(
235 | """\
236 | # define.py
237 | class Foo:
238 | pass
239 |
240 | class Bar:
241 | pass
242 |
243 | # main.py
244 | from define import Foo, Bar
245 |
246 | __all__ = [Foo]
247 |
248 | """
249 | )
250 | check(v.defined_imports, ["Foo", "Bar"])
251 | check(v.unused_imports, ["Bar"])
252 |
253 |
254 | def test_import_with__all__string(v):
255 | v.scan(
256 | """\
257 | # define.py
258 | class Foo:
259 | pass
260 |
261 | class Bar:
262 | pass
263 |
264 | # main.py
265 | from define import Foo, Bar
266 |
267 | __all__ = "Foo"
268 |
269 | """
270 | )
271 | check(v.defined_imports, ["Foo", "Bar"])
272 | # __all__ is not a list or tuple, so Foo is unused.
273 | check(v.unused_imports, ["Foo", "Bar"])
274 |
275 |
276 | def test_import_with__all__assign_other_module(v):
277 | v.scan(
278 | """\
279 | # define.py
280 | class Foo:
281 | pass
282 |
283 | class Bar:
284 | pass
285 |
286 | # main.py
287 | import define
288 | from define import Foo, Bar
289 |
290 | define.__all__ = ["Foo"]
291 |
292 | """
293 | )
294 | check(v.defined_imports, ["define", "Foo", "Bar"])
295 | # Only assignments to __all__ of the current module are covered.
296 | check(v.unused_imports, ["Foo", "Bar"])
297 |
298 |
299 | def test_ignore_init_py_files(v):
300 | v.scan(
301 | """\
302 | import bar
303 | from foo import *
304 | from zoo import zebra
305 |
306 | unused_var = 'monty'
307 | """,
308 | filename="nested/project/__init__.py",
309 | )
310 | check(v.unused_imports, [])
311 | check(v.unused_vars, ["unused_var"])
312 |
--------------------------------------------------------------------------------
/tests/test_item.py:
--------------------------------------------------------------------------------
1 | from . import v
2 |
3 | assert v # Silence pyflakes
4 |
5 |
6 | def test_item_repr(v):
7 | v.scan(
8 | """\
9 | import os
10 |
11 | message = "foobar"
12 |
13 | class Foo:
14 | def bar():
15 | pass
16 | """
17 | )
18 | for item in v.get_unused_code():
19 | assert repr(item) == f"{item.name!r}"
20 |
21 |
22 | def test_item_attr(v):
23 | v.scan("foo.bar = 'bar'")
24 | assert len(v.unused_attrs) == 1
25 | a = v.unused_attrs[0]
26 | assert a.name == "bar"
27 | assert a.first_lineno == 1
28 | assert a.last_lineno == 1
29 |
30 |
31 | def test_item_class(v):
32 | v.scan(
33 | """\
34 | class Foo:
35 | pass
36 | """
37 | )
38 | assert len(v.unused_classes) == 1
39 | c = v.unused_classes[0]
40 | assert c.name == "Foo"
41 | assert c.first_lineno == 1
42 | assert c.last_lineno == 2
43 |
44 |
45 | def test_item_function(v):
46 | v.scan(
47 | """\
48 | def add(a, b):
49 | return a + b
50 | """
51 | )
52 | assert len(v.unused_funcs) == 1
53 | f = v.unused_funcs[0]
54 | assert f.name == "add"
55 | assert f.first_lineno == 1
56 | assert f.last_lineno == 2
57 |
58 |
59 | def test_item_import(v):
60 | v.scan(
61 | """\
62 | import bar
63 | from foo import *
64 | """
65 | )
66 | assert len(v.unused_imports) == 1
67 | i = v.unused_imports[0]
68 | assert i.name == "bar"
69 | assert i.first_lineno == 1
70 | assert i.last_lineno == 1
71 |
72 |
73 | def test_item_property(v):
74 | v.scan(
75 | """\
76 | @awesomify
77 | class Foo:
78 | @property
79 | @wifi(
80 | username='dog',
81 | password='cat',
82 | )
83 | def bar(self):
84 | pass
85 | Foo()
86 | """
87 | )
88 | assert len(v.unused_props) == 1
89 | p = v.unused_props[0]
90 | assert p.name == "bar"
91 | assert p.first_lineno == 3
92 | assert p.last_lineno == 9
93 |
94 |
95 | def test_item_variable(v):
96 | v.scan("v = 'Vulture'")
97 | assert len(v.unused_vars) == 1
98 | var = v.unused_vars[0]
99 | assert var.name == "v"
100 | assert var.first_lineno == 1
101 | assert var.last_lineno == 1
102 |
--------------------------------------------------------------------------------
/tests/test_make_whitelist.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from . import check, v
4 |
5 | assert v # silence pyflakes
6 |
7 |
8 | @pytest.fixture
9 | def check_whitelist(v):
10 | def examine(code, results_before, results_after):
11 | v.scan(code)
12 | check(v.get_unused_code(), results_before)
13 | for item in v.get_unused_code():
14 | v.scan(item.get_whitelist_string())
15 | check(v.get_unused_code(), results_after)
16 |
17 | return examine
18 |
19 |
20 | def test_unused_function(check_whitelist):
21 | code = """\
22 | def func():
23 | pass
24 | """
25 | check_whitelist(code, ["func"], [])
26 |
27 |
28 | def test_unused_class(check_whitelist):
29 | code = """\
30 | class Foo:
31 | def __init__(self):
32 | pass
33 | """
34 | check_whitelist(code, ["Foo"], [])
35 |
36 |
37 | def test_unused_variables(check_whitelist):
38 | code = """\
39 | foo = 'unused'
40 | bar = 'variable'
41 | """
42 | check_whitelist(code, ["foo", "bar"], [])
43 |
44 |
45 | def test_unused_import(check_whitelist):
46 | code = """\
47 | import xyz
48 | import foo as bar
49 | from abc import iou
50 | from lorem import ipsum as dolor
51 | """
52 | check_whitelist(code, ["xyz", "bar", "iou", "dolor"], [])
53 |
54 |
55 | def test_unused_attribute(check_whitelist):
56 | code = """\
57 | class Foo:
58 | def bar(self):
59 | self.foobar = 'unused attr'
60 | """
61 | check_whitelist(code, ["Foo", "bar", "foobar"], [])
62 |
63 |
64 | def test_unused_property(check_whitelist):
65 | code = """\
66 | class Foo:
67 | @property
68 | def bar(self):
69 | pass
70 | """
71 | check_whitelist(code, ["Foo", "bar"], [])
72 |
73 |
74 | def test_unreachable_code(check_whitelist):
75 | code = """\
76 | def foo():
77 | return "Foo Bar"
78 | print("Hello")
79 | """
80 | check_whitelist(code, ["foo", "return"], ["return"])
81 |
--------------------------------------------------------------------------------
/tests/test_noqa.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from vulture.core import ERROR_CODES
4 | from vulture.noqa import NOQA_CODE_MAP, NOQA_REGEXP, _parse_error_codes
5 |
6 | from . import check, v
7 |
8 | assert v # Silence pyflakes.
9 |
10 |
11 | @pytest.mark.parametrize(
12 | "line, codes",
13 | [
14 | ("# noqa", ["all"]),
15 | ("## noqa", ["all"]),
16 | ("# noqa Hi, go on.", ["all"]),
17 | ("# noqa: V101", ["V101"]),
18 | ("# noqa: V101, V106", ["V101", "V106"]),
19 | ("# NoQA: V101, V103, \t V104", ["V101", "V103", "V104"]),
20 | ],
21 | )
22 | def test_noqa_regex_present(line, codes):
23 | match = NOQA_REGEXP.search(line)
24 | parsed = _parse_error_codes(match)
25 | assert parsed == codes
26 |
27 |
28 | @pytest.mark.parametrize(
29 | "line",
30 | [
31 | ("# noqa: 123V"),
32 | ("# noqa explanation: V012"),
33 | ("# noqa: ,V101"),
34 | ("# noqa: #noqa: V102"),
35 | ("# noqa: # noqa: V102"),
36 | ],
37 | )
38 | def test_noqa_regex_no_groups(line):
39 | assert NOQA_REGEXP.search(line).groupdict()["codes"] is None
40 |
41 |
42 | @pytest.mark.parametrize(
43 | "line",
44 | [("#noqa"), ("##noqa"), ("# n o q a"), ("#NOQA"), ("# Hello, noqa")],
45 | )
46 | def test_noqa_regex_not_present(line):
47 | assert not NOQA_REGEXP.search(line)
48 |
49 |
50 | def test_noqa_without_codes(v):
51 | v.scan(
52 | """\
53 | import this # noqa
54 |
55 | @underground # noqa
56 | class Cellar:
57 | @property # noqa
58 | def wine(self):
59 | grapes = True # noqa
60 |
61 | @without_ice # noqa
62 | def serve(self, quantity=50):
63 | self.quantity_served = quantity # noqa
64 | return
65 | self.pour() # noqa
66 | """
67 | )
68 | check(v.unused_attrs, [])
69 | check(v.unused_classes, [])
70 | check(v.unused_funcs, [])
71 | check(v.unused_imports, [])
72 | check(v.unused_props, [])
73 | check(v.unreachable_code, [])
74 | check(v.unused_vars, [])
75 |
76 |
77 | def test_noqa_specific_issue_codes(v):
78 | v.scan(
79 | """\
80 | import this # noqa: V104
81 |
82 | @underground # noqa: V102
83 | class Cellar:
84 | @property # noqa: V106
85 | def wine(self):
86 | grapes = True # noqa: V107
87 |
88 | @without_ice # noqa: V103
89 | def serve(self, quantity=50):
90 | self.quantity_served = quantity # noqa: V101
91 | return
92 | self.pour() # noqa: V201
93 | """
94 | )
95 | check(v.unused_attrs, [])
96 | check(v.unused_classes, [])
97 | check(v.unused_funcs, [])
98 | check(v.unused_imports, [])
99 | check(v.unused_methods, ["serve"])
100 | check(v.unused_props, [])
101 | check(v.unreachable_code, [])
102 | check(v.unused_vars, [])
103 |
104 |
105 | def test_noqa_attributes(v):
106 | v.scan(
107 | """\
108 | something.x = 'x' # noqa: V101
109 | something.z = 'z' # noqa: V107 (code for unused variable)
110 | something.u = 'u' # noqa
111 | """
112 | )
113 | check(v.unused_attrs, ["z"])
114 |
115 |
116 | def test_noqa_classes(v):
117 | v.scan(
118 | """\
119 | class QtWidget: # noqa: V102
120 | pass
121 |
122 | class ABC(QtWidget):
123 | pass # noqa: V102 (should not ignore)
124 |
125 | class DEF: # noqa
126 | pass
127 | """
128 | )
129 | check(v.unused_classes, ["ABC"])
130 |
131 |
132 | def test_noqa_functions(v):
133 | v.scan(
134 | """\
135 | def play(tune, instrument='bongs', _hz='50'): # noqa: V103
136 | pass
137 |
138 |
139 | # noqa
140 | def problems(): # noqa: V104
141 | pass # noqa: V103
142 |
143 | def hello(name): # noqa
144 | print("Hello")
145 | """
146 | )
147 | check(v.unused_funcs, ["problems"])
148 | check(v.unused_vars, ["instrument", "tune"])
149 |
150 |
151 | def test_noqa_imports(v):
152 | v.scan(
153 | """\
154 | import foo
155 | import this # noqa: V104
156 | import zoo
157 | from koo import boo # noqa
158 | from me import *
159 | import dis # noqa: V101 (code for unused attr)
160 | """
161 | )
162 | check(v.unused_imports, ["foo", "zoo", "dis"])
163 |
164 |
165 | def test_noqa_properties(v):
166 | v.scan(
167 | """\
168 | class Zoo:
169 | @property
170 | def no_of_koalas(self): # noqa
171 | pass
172 |
173 | @property
174 | def area(self, width, depth): # noqa: V105
175 | pass
176 |
177 | @property # noqa
178 | def entry_gates(self):
179 | pass
180 |
181 | @property # noqa: V103 (code for unused function)
182 | def tickets(self):
183 | pass
184 | """
185 | )
186 | check(v.unused_props, ["no_of_koalas", "area", "tickets"])
187 | check(v.unused_classes, ["Zoo"])
188 | check(v.unused_vars, ["width", "depth"])
189 |
190 |
191 | def test_noqa_multiple_decorators(v):
192 | v.scan(
193 | """\
194 | @bar # noqa: V102
195 | class Foo:
196 | @property # noqa: V106
197 | @make_it_cool
198 | @log
199 | def something(self):
200 | pass
201 |
202 | @coolify
203 | @property
204 | def something_else(self): # noqa: V106
205 | pass
206 |
207 | @a
208 | @property
209 | @b # noqa
210 | def abcd(self):
211 | pass
212 | """
213 | )
214 | check(v.unused_props, ["something_else", "abcd"])
215 | check(v.unused_classes, [])
216 |
217 |
218 | def test_noqa_unreacahble_code(v):
219 | v.scan(
220 | """\
221 | def shave_sheep(sheep):
222 | for a_sheep in sheep:
223 | if a_sheep.is_bald:
224 | continue
225 | a_sheep.grow_hair() # noqa: V201
226 | a_sheep.shave()
227 | return
228 | for a_sheep in sheep: # noqa: V201
229 | if a_sheep.still_has_hair:
230 | a_sheep.shave_again()
231 | """
232 | )
233 | check(v.unreachable_code, [])
234 | check(v.unused_funcs, ["shave_sheep"])
235 |
236 |
237 | def test_noqa_variables(v):
238 | v.scan(
239 | """\
240 | mitsi = "Mother" # noqa: V107
241 | harry = "Father" # noqa
242 | shero = "doggy" # noqa: V101, V104 (code for unused import, attr)
243 | shinchan.friend = ['masao'] # noqa: V107 (code for unused variable)
244 | """
245 | )
246 | check(v.unused_vars, ["shero"])
247 | check(v.unused_attrs, ["friend"])
248 |
249 |
250 | def test_noqa_with_multiple_issue_codes(v):
251 | v.scan(
252 | """\
253 | def world(axis): # noqa: V103, V201
254 | pass
255 |
256 |
257 | for _ in range(3):
258 | continue
259 | xyz = hello(something, else): # noqa: V201, V107
260 | """
261 | )
262 | check(v.get_unused_code(), [])
263 |
264 |
265 | def test_noqa_on_empty_line(v):
266 | v.scan(
267 | """\
268 | # noqa
269 | import this
270 | # noqa
271 | """
272 | )
273 | check(v.unused_imports, ["this"])
274 |
275 |
276 | def test_noqa_with_invalid_codes(v):
277 | v.scan(
278 | """\
279 | import this # V098, A123, F876
280 | """
281 | )
282 | check(v.unused_imports, ["this"])
283 |
284 |
285 | @pytest.mark.parametrize(
286 | "first_file, second_file",
287 | [
288 | ("foo = None", "bar = None # noqa"),
289 | ("bar = None # noqa", "foo = None"),
290 | ],
291 | )
292 | def test_noqa_multiple_files(first_file, second_file, v):
293 | v.scan(first_file, filename="first_file.py")
294 | v.scan(second_file, filename="second_file.py")
295 | check(v.unused_vars, ["foo"])
296 |
297 |
298 | def test_flake8_noqa_codes(v):
299 | assert NOQA_CODE_MAP["F401"] == ERROR_CODES["import"]
300 | assert NOQA_CODE_MAP["F841"] == ERROR_CODES["variable"]
301 | v.scan(
302 | """\
303 | import this # noqa: F401
304 |
305 | def foo():
306 | bar = 2 # noqa: F841
307 | """
308 | )
309 | check(v.unused_funcs, ["foo"])
310 | check(v.unused_imports, [])
311 | check(v.unused_vars, [])
312 |
--------------------------------------------------------------------------------
/tests/test_pytype.py:
--------------------------------------------------------------------------------
1 | import subprocess
2 | import sys
3 |
4 | import pytest
5 |
6 |
7 | @pytest.mark.skipif(
8 | sys.version_info >= (3, 13), reason="needs Python < 3.13 for pytype"
9 | )
10 | def test_pytype():
11 | assert subprocess.run(["pytype", "vulture/core.py"]).returncode == 0
12 |
--------------------------------------------------------------------------------
/tests/test_reachability.py:
--------------------------------------------------------------------------------
1 | from . import check_multiple_unreachable, check_unreachable, v
2 |
3 | assert v # Silence pyflakes
4 |
5 |
6 | def test_return_assignment(v):
7 | v.scan(
8 | """\
9 | def foo():
10 | print("Hello World")
11 | return
12 | a = 1
13 | """
14 | )
15 | check_unreachable(v, 4, 1, "return")
16 |
17 |
18 | def test_return_multiline_return_statements(v):
19 | v.scan(
20 | """\
21 | def foo():
22 | print("Something")
23 | return (something,
24 | that,
25 | spans,
26 | over,
27 | multiple,
28 | lines)
29 | print("Hello World")
30 | """
31 | )
32 | check_unreachable(v, 9, 1, "return")
33 |
34 |
35 | def test_return_multiple_return_statements(v):
36 | v.scan(
37 | """\
38 | def foo():
39 | return something
40 | return None
41 | return (some, statement)
42 | """
43 | )
44 | check_unreachable(v, 3, 2, "return")
45 |
46 |
47 | def test_return_pass(v):
48 | v.scan(
49 | """\
50 | def foo():
51 | return
52 | pass
53 | return something
54 | """
55 | )
56 | check_unreachable(v, 3, 2, "return")
57 |
58 |
59 | def test_return_multiline_return(v):
60 | v.scan(
61 | """
62 | def foo():
63 | return \
64 | "Hello"
65 | print("Unreachable code")
66 | """
67 | )
68 | check_unreachable(v, 4, 1, "return")
69 |
70 |
71 | def test_return_recursive_functions(v):
72 | v.scan(
73 | """\
74 | def foo(a):
75 | if a == 1:
76 | return 1
77 | else:
78 | return foo(a - 1)
79 | print("This line is never executed")
80 | """
81 | )
82 | check_unreachable(v, 6, 1, "return")
83 |
84 |
85 | def test_return_semicolon(v):
86 | v.scan(
87 | """\
88 | def foo():
89 | return; a = 1
90 | """
91 | )
92 | check_unreachable(v, 2, 1, "return")
93 |
94 |
95 | def test_return_list(v):
96 | v.scan(
97 | """\
98 | def foo(a):
99 | return
100 | a[1:2]
101 | """
102 | )
103 | check_unreachable(v, 3, 1, "return")
104 |
105 |
106 | def test_return_continue(v):
107 | v.scan(
108 | """\
109 | def foo():
110 | if foo():
111 | return True
112 | continue
113 | else:
114 | return False
115 | """
116 | )
117 | check_unreachable(v, 4, 1, "return")
118 |
119 |
120 | def test_return_function_definition(v):
121 | v.scan(
122 | """\
123 | def foo():
124 | return True
125 | def bar():
126 | return False
127 | """
128 | )
129 | check_unreachable(v, 3, 2, "return")
130 |
131 |
132 | def test_raise_global(v):
133 | v.scan(
134 | """\
135 | raise ValueError
136 | a = 1
137 | """
138 | )
139 | check_unreachable(v, 2, 1, "raise")
140 |
141 |
142 | def test_raise_assignment(v):
143 | v.scan(
144 | """\
145 | def foo():
146 | raise ValueError
147 | li = []
148 | """
149 | )
150 | check_unreachable(v, 3, 1, "raise")
151 |
152 |
153 | def test_multiple_raise_statements(v):
154 | v.scan(
155 | """\
156 | def foo():
157 | a = 1
158 | raise
159 | raise KeyError
160 | # a comment
161 | b = 2
162 | raise CustomDefinedError
163 | """
164 | )
165 | check_unreachable(v, 4, 4, "raise")
166 |
167 |
168 | def test_return_with_raise(v):
169 | v.scan(
170 | """\
171 | def foo():
172 | a = 1
173 | return
174 | raise ValueError
175 | return
176 | """
177 | )
178 | check_unreachable(v, 4, 2, "return")
179 |
180 |
181 | def test_return_comment_and_code(v):
182 | v.scan(
183 | """\
184 | def foo():
185 | return
186 | # This is a comment
187 | print("Hello World")
188 | """
189 | )
190 | check_unreachable(v, 4, 1, "return")
191 |
192 |
193 | def test_raise_with_return(v):
194 | v.scan(
195 | """\
196 | def foo():
197 | a = 1
198 | raise
199 | return a
200 | """
201 | )
202 | check_unreachable(v, 4, 1, "raise")
203 |
204 |
205 | def test_raise_error_message(v):
206 | v.scan(
207 | """\
208 | def foo():
209 | raise SomeError("There is a problem")
210 | print("I am unreachable")
211 | """
212 | )
213 | check_unreachable(v, 3, 1, "raise")
214 |
215 |
216 | def test_raise_try_except(v):
217 | v.scan(
218 | """\
219 | def foo():
220 | try:
221 | a = 1
222 | raise
223 | except IOError as e:
224 | print("We have some problem.")
225 | raise
226 | print(":-(")
227 | """
228 | )
229 | check_unreachable(v, 8, 1, "raise")
230 |
231 |
232 | def test_raise_with_comment_and_code(v):
233 | v.scan(
234 | """\
235 | def foo():
236 | raise
237 | # This is a comment
238 | print("Something")
239 | return None
240 | """
241 | )
242 | check_unreachable(v, 4, 2, "raise")
243 |
244 |
245 | def test_continue_basic(v):
246 | v.scan(
247 | """\
248 | def foo():
249 | if bar():
250 | a = 1
251 | else:
252 | continue
253 | a = 2
254 | """
255 | )
256 | check_unreachable(v, 6, 1, "continue")
257 |
258 |
259 | def test_continue_one_liner(v):
260 | v.scan(
261 | """\
262 | def foo():
263 | for i in range(1, 10):
264 | if i == 5: continue
265 | print(1 / i)
266 | """
267 | )
268 | assert v.unreachable_code == []
269 |
270 |
271 | def test_continue_nested_loops(v):
272 | v.scan(
273 | """\
274 | def foo():
275 | a = 0
276 | if something():
277 | foo()
278 | if bar():
279 | a = 2
280 | continue
281 | # This is unreachable
282 | a = 1
283 | elif a == 1:
284 | pass
285 | else:
286 | a = 3
287 | continue
288 | else:
289 | continue
290 | """
291 | )
292 | check_unreachable(v, 9, 1, "continue")
293 |
294 |
295 | def test_continue_with_comment_and_code(v):
296 | v.scan(
297 | """\
298 | def foo():
299 | if bar1():
300 | bar2()
301 | else:
302 | a = 1
303 | continue
304 | # Just a comment
305 | raise ValueError
306 | """
307 | )
308 | check_unreachable(v, 8, 1, "continue")
309 |
310 |
311 | def test_break_basic(v):
312 | v.scan(
313 | """\
314 | def foo():
315 | for i in range(123):
316 | break
317 | # A comment
318 | return
319 | dead = 1
320 | """
321 | )
322 | check_unreachable(v, 5, 2, "break")
323 |
324 |
325 | def test_break_one_liner(v):
326 | v.scan(
327 | """\
328 | def foo():
329 | for i in range(10):
330 | if i == 3: break
331 | print(i)
332 | """
333 | )
334 | assert v.unreachable_code == []
335 |
336 |
337 | def test_break_with_comment_and_code(v):
338 | v.scan(
339 | """\
340 | while True:
341 | break
342 | # some comment
343 | print("Hello")
344 | """
345 | )
346 | check_unreachable(v, 4, 1, "break")
347 |
348 |
349 | def test_if_false(v):
350 | v.scan(
351 | """\
352 | if False:
353 | pass
354 | """
355 | )
356 | check_unreachable(v, 1, 2, "if")
357 |
358 |
359 | def test_elif_false(v):
360 | v.scan(
361 | """\
362 | if bar():
363 | pass
364 | elif False:
365 | print("Unreachable")
366 | """
367 | )
368 | check_unreachable(v, 3, 2, "if")
369 |
370 |
371 | def test_nested_if_statements_false(v):
372 | v.scan(
373 | """\
374 | if foo():
375 | if bar():
376 | pass
377 | elif False:
378 | print("Unreachable")
379 | pass
380 | elif something():
381 | print("Reachable")
382 | else:
383 | pass
384 | else:
385 | pass
386 | """
387 | )
388 | check_unreachable(v, 4, 3, "if")
389 |
390 |
391 | def test_if_false_same_line(v):
392 | v.scan(
393 | """\
394 | if False: a = 1
395 | else: c = 3
396 | """
397 | )
398 | check_unreachable(v, 1, 1, "if")
399 |
400 |
401 | def test_if_true(v):
402 | v.scan(
403 | """\
404 | if True:
405 | a = 1
406 | b = 2
407 | else:
408 | c = 3
409 | d = 3
410 | """
411 | )
412 | # For simplicity, we don't report the "else" line as dead code.
413 | check_unreachable(v, 5, 2, "else")
414 |
415 |
416 | def test_if_true_same_line(v):
417 | v.scan(
418 | """\
419 | if True:
420 | a = 1
421 | b = 2
422 | else: c = 3
423 | d = 3
424 | """
425 | )
426 | check_unreachable(v, 4, 1, "else")
427 |
428 |
429 | def test_nested_if_statements_true(v):
430 | v.scan(
431 | """\
432 | if foo():
433 | if bar():
434 | pass
435 | elif True:
436 | if something():
437 | pass
438 | else:
439 | pass
440 | elif something_else():
441 | print("foo")
442 | else:
443 | print("bar")
444 | else:
445 | pass
446 | """
447 | )
448 | check_unreachable(v, 9, 4, "else")
449 |
450 |
451 | def test_redundant_if(v):
452 | v.scan(
453 | """\
454 | if [5]:
455 | pass
456 | """
457 | )
458 | print(v.unreachable_code[0].size)
459 | check_unreachable(v, 1, 2, "if")
460 |
461 |
462 | def test_if_exp_true(v):
463 | v.scan("foo if True else bar")
464 | check_unreachable(v, 1, 1, "ternary")
465 |
466 |
467 | def test_if_exp_false(v):
468 | v.scan("foo if False else bar")
469 | check_unreachable(v, 1, 1, "ternary")
470 |
471 |
472 | def test_if_true_return(v):
473 | v.scan(
474 | """\
475 | def foo(a):
476 | if True:
477 | return 0
478 | print(":-(")
479 | """
480 | )
481 | check_multiple_unreachable(v, [(2, 2, "if"), (4, 1, "if")])
482 |
483 |
484 | def test_if_true_return_else(v):
485 | v.scan(
486 | """\
487 | def foo(a):
488 | if True:
489 | return 0
490 | else:
491 | return 1
492 | print(":-(")
493 | """
494 | )
495 | check_multiple_unreachable(v, [(5, 1, "else"), (6, 1, "if")])
496 |
497 |
498 | def test_if_some_branches_return(v):
499 | v.scan(
500 | """\
501 | def foo(a):
502 | if a == 0:
503 | return 0
504 | elif a == 1:
505 | pass
506 | else:
507 | return 2
508 | print(":-(")
509 | """
510 | )
511 | assert v.unreachable_code == []
512 |
513 |
514 | def test_if_all_branches_return(v):
515 | v.scan(
516 | """\
517 | def foo(a):
518 | if a == 0:
519 | return 0
520 | elif a == 1:
521 | return 1
522 | else:
523 | return 2
524 | print(":-(")
525 | """
526 | )
527 | check_unreachable(v, 8, 1, "if")
528 |
529 |
530 | def test_if_all_branches_return_nested(v):
531 | v.scan(
532 | """\
533 | def foo(a, b):
534 | if a:
535 | if b:
536 | return 1
537 | return 2
538 | else:
539 | return 3
540 | print(":-(")
541 | """
542 | )
543 | check_unreachable(v, 8, 1, "if")
544 |
545 |
546 | def test_if_all_branches_return_or_raise(v):
547 | v.scan(
548 | """\
549 | def foo(a):
550 | if a == 0:
551 | return 0
552 | else:
553 | raise Exception()
554 | print(":-(")
555 | """
556 | )
557 | check_unreachable(v, 6, 1, "if")
558 |
559 |
560 | def test_try_fall_through(v):
561 | v.scan(
562 | """\
563 | def foo():
564 | try:
565 | pass
566 | except IndexError as e:
567 | raise e
568 | print(":-(")
569 | """
570 | )
571 | assert v.unreachable_code == []
572 |
573 |
574 | def test_try_some_branches_raise(v):
575 | v.scan(
576 | """\
577 | def foo(e):
578 | try:
579 | raise e
580 | except IndexError as e:
581 | pass
582 | except Exception as e:
583 | raise e
584 | print(":-(")
585 | """
586 | )
587 | assert v.unreachable_code == []
588 |
589 |
590 | def test_try_all_branches_return_or_raise(v):
591 | v.scan(
592 | """\
593 | def foo():
594 | try:
595 | return 2
596 | except IndexError as e:
597 | raise e
598 | except Exception as e:
599 | raise e
600 | print(":-(")
601 | """
602 | )
603 | check_unreachable(v, 8, 1, "try")
604 |
605 |
606 | def test_try_nested_no_fall_through(v):
607 | v.scan(
608 | """\
609 | def foo(a):
610 | try:
611 | raise a
612 | except:
613 | try:
614 | return
615 | except Exception as e:
616 | raise e
617 | print(":-(")
618 | """
619 | )
620 | check_unreachable(v, 9, 1, "try")
621 |
622 |
623 | def test_try_reachable_else(v):
624 | v.scan(
625 | """\
626 | def foo():
627 | try:
628 | print(":-)")
629 | except:
630 | return 1
631 | else:
632 | print(":-(")
633 | """
634 | )
635 | assert v.unreachable_code == []
636 |
637 |
638 | def test_try_unreachable_else(v):
639 | v.scan(
640 | """\
641 | def foo():
642 | try:
643 | raise Exception()
644 | except Exception as e:
645 | return 1
646 | else:
647 | print(":-(")
648 | """
649 | )
650 | check_unreachable(v, 7, 1, "else")
651 |
652 |
653 | def test_with_fall_through(v):
654 | v.scan(
655 | """\
656 | def foo(a):
657 | with a():
658 | raise Exception()
659 | print(":-(")
660 | """
661 | )
662 | assert v.unreachable_code == []
663 |
664 |
665 | def test_async_with_fall_through(v):
666 | v.scan(
667 | """\
668 | async def foo(a):
669 | async with a():
670 | raise Exception()
671 | print(":-(")
672 | """
673 | )
674 | assert v.unreachable_code == []
675 |
676 |
677 | def test_for_fall_through(v):
678 | v.scan(
679 | """\
680 | def foo(a):
681 | for i in a:
682 | raise Exception()
683 | print(":-(")
684 | """
685 | )
686 | assert v.unreachable_code == []
687 |
688 |
689 | def test_async_for_fall_through(v):
690 | v.scan(
691 | """\
692 | async def foo(a):
693 | async for i in a:
694 | raise Exception()
695 | print(":-(")
696 | """
697 | )
698 | assert v.unreachable_code == []
699 |
700 |
701 | def test_while_false(v):
702 | v.scan(
703 | """\
704 | while False:
705 | pass
706 | """
707 | )
708 | check_unreachable(v, 1, 2, "while")
709 |
710 |
711 | def test_while_nested(v):
712 | v.scan(
713 | """\
714 | while True:
715 | while False:
716 | pass
717 | """
718 | )
719 | check_unreachable(v, 2, 2, "while")
720 |
721 |
722 | def test_while_true_else(v):
723 | v.scan(
724 | """\
725 | while True:
726 | print("I won't stop")
727 | else:
728 | print("I won't run")
729 | """
730 | )
731 | check_unreachable(v, 4, 1, "else")
732 |
733 |
734 | def test_while_true_no_fall_through(v):
735 | v.scan(
736 | """\
737 | while True:
738 | raise Exception()
739 | print(":-(")
740 | """
741 | )
742 | check_unreachable(v, 3, 1, "while")
743 |
744 |
745 | def test_while_true_no_fall_through_nested(v):
746 | v.scan(
747 | """\
748 | while True:
749 | if a > 3:
750 | raise Exception()
751 | else:
752 | pass
753 | print(":-(")
754 | """
755 | )
756 | check_unreachable(v, 6, 1, "while")
757 |
758 |
759 | def test_while_true_no_fall_through_nested_loops(v):
760 | v.scan(
761 | """\
762 | while True:
763 | for _ in range(3):
764 | break
765 | while False:
766 | break
767 | print(":-(")
768 | """
769 | )
770 | check_multiple_unreachable(v, [(4, 2, "while"), (6, 1, "while")])
771 |
772 |
773 | def test_while_true_fall_through(v):
774 | v.scan(
775 | """\
776 | while True:
777 | break
778 | print(":-)")
779 | """
780 | )
781 | assert v.unreachable_code == []
782 |
783 |
784 | def test_while_true_fall_through_nested(v):
785 | v.scan(
786 | """\
787 | while True:
788 | if a > 3:
789 | raise Exception()
790 | else:
791 | break
792 | print(":-(")
793 | """
794 | )
795 | assert v.unreachable_code == []
796 |
797 |
798 | def test_while_fall_through(v):
799 | v.scan(
800 | """\
801 | def foo(a):
802 | while a > 0:
803 | return 1
804 | print(":-)")
805 | """
806 | )
807 | assert v.unreachable_code == []
808 |
--------------------------------------------------------------------------------
/tests/test_report.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from . import v
4 |
5 | assert v # Silence pyflakes
6 |
7 | mock_code = """\
8 | import foo
9 |
10 | class Foo:
11 | def __init__(self):
12 | print("Initialized foo")
13 |
14 | def bar(self):
15 | self.foobar = "unused attribute"
16 | foobar = "unused variable"
17 | 日本人 = "unused variable"
18 | return
19 | print("unreachable")
20 |
21 | @property
22 | def myprop(self):
23 | pass
24 |
25 | def myfunc():
26 | pass
27 | """
28 |
29 |
30 | @pytest.fixture
31 | def check_report(v, capsys):
32 | def test_report(code, expected, make_whitelist=False):
33 | filename = "foo.py"
34 | v.scan(code, filename=filename)
35 | capsys.readouterr()
36 | ret = v.report(make_whitelist=make_whitelist)
37 | assert ret
38 | assert capsys.readouterr().out == expected.format(filename=filename)
39 |
40 | return test_report
41 |
42 |
43 | def test_logging(v, capsys):
44 | expected = "\u65e5\u672c\u4eba\xc0\n"
45 | v._log("日本人À")
46 | assert capsys.readouterr().out == expected
47 |
48 |
49 | def test_item_report(check_report):
50 | expected = """\
51 | {filename}:1: unused import 'foo' (90% confidence)
52 | {filename}:3: unused class 'Foo' (60% confidence)
53 | {filename}:7: unused method 'bar' (60% confidence)
54 | {filename}:8: unused attribute 'foobar' (60% confidence)
55 | {filename}:9: unused variable 'foobar' (60% confidence)
56 | {filename}:10: unused variable '\u65e5\u672c\u4eba' (60% confidence)
57 | {filename}:12: unreachable code after 'return' (100% confidence)
58 | {filename}:14: unused property 'myprop' (60% confidence)
59 | {filename}:18: unused function 'myfunc' (60% confidence)
60 | """
61 | check_report(mock_code, expected)
62 |
63 |
64 | def test_make_whitelist(check_report):
65 | expected = """\
66 | foo # unused import ({filename}:1)
67 | Foo # unused class ({filename}:3)
68 | _.bar # unused method ({filename}:7)
69 | _.foobar # unused attribute ({filename}:8)
70 | foobar # unused variable ({filename}:9)
71 | \u65e5\u672c\u4eba # unused variable ({filename}:10)
72 | # unreachable code after 'return' ({filename}:12)
73 | _.myprop # unused property ({filename}:14)
74 | myfunc # unused function ({filename}:18)
75 | """
76 | check_report(mock_code, expected, make_whitelist=True)
77 |
--------------------------------------------------------------------------------
/tests/test_scavenging.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | import pytest
4 |
5 | from vulture.utils import ExitCode
6 |
7 | from . import check, v
8 |
9 | assert v # Silence pyflakes.
10 |
11 |
12 | def test_function_object1(v):
13 | v.scan(
14 | """\
15 | def func():
16 | pass
17 |
18 | a = func
19 | """
20 | )
21 | check(v.defined_funcs, ["func"])
22 | check(v.unused_funcs, [])
23 |
24 |
25 | def test_function_object2(v):
26 | v.scan(
27 | """\
28 | def func():
29 | pass
30 |
31 | func
32 | """
33 | )
34 | check(v.defined_funcs, ["func"])
35 | check(v.unused_funcs, [])
36 |
37 |
38 | def test_function1(v):
39 | v.scan(
40 | """\
41 | def func1(a):
42 | pass
43 |
44 | def func2(b):
45 | func1(b)
46 | """
47 | )
48 | check(v.defined_funcs, ["func1", "func2"])
49 | check(v.unused_funcs, ["func2"])
50 |
51 |
52 | def test_function2(v):
53 | v.scan(
54 | """\
55 | def func(a):
56 | pass
57 |
58 | func(5)
59 | """
60 | )
61 | check(v.unused_funcs, [])
62 | check(v.defined_funcs, ["func"])
63 |
64 |
65 | def test_function3(v):
66 | v.scan(
67 | """\
68 | def foo(a):
69 | pass
70 |
71 | b = foo(5)
72 | """
73 | )
74 | check(v.unused_funcs, [])
75 | check(v.defined_funcs, ["foo"])
76 |
77 |
78 | def test_async_function(v):
79 | v.scan(
80 | """\
81 | async def foo():
82 | pass
83 | """
84 | )
85 | check(v.defined_funcs, ["foo"])
86 | check(v.unused_funcs, ["foo"])
87 |
88 |
89 | def test_async_method(v):
90 | v.scan(
91 | """\
92 | class Foo:
93 | async def bar(self):
94 | pass
95 | """
96 | )
97 | check(v.defined_classes, ["Foo"])
98 | check(v.defined_funcs, [])
99 | check(v.defined_methods, ["bar"])
100 | check(v.unused_classes, ["Foo"])
101 | check(v.unused_methods, ["bar"])
102 |
103 |
104 | def test_function_and_method1(v):
105 | v.scan(
106 | """\
107 | class Bar(object):
108 | def func(self):
109 | pass
110 |
111 | def func():
112 | pass
113 |
114 | func()
115 | """
116 | )
117 | check(v.defined_classes, ["Bar"])
118 | check(v.defined_funcs, ["func"])
119 | check(v.defined_methods, ["func"])
120 | check(v.unused_classes, ["Bar"])
121 | check(v.unused_funcs, [])
122 | # Bar.func is unused, but it's hard to detect this without producing a
123 | # false positive in test_function_and_method2.
124 | check(v.unused_methods, [])
125 |
126 |
127 | def test_function_and_method2(v):
128 | v.scan(
129 | """\
130 | class Bar(object):
131 | def func(self):
132 | pass
133 |
134 | other_name_for_func = func
135 |
136 | Bar().other_name_for_func()
137 | """
138 | )
139 | check(v.defined_classes, ["Bar"])
140 | check(v.defined_funcs, [])
141 | check(v.defined_methods, ["func"])
142 | check(v.defined_vars, ["other_name_for_func"])
143 | check(v.unused_classes, [])
144 | check(v.unused_funcs, [])
145 | check(v.unused_methods, [])
146 | check(v.unused_vars, [])
147 |
148 |
149 | def test_attribute1(v):
150 | v.scan(
151 | """\
152 | foo.bar = 1
153 | foo.bar = 2
154 | """
155 | )
156 | check(v.unused_funcs, [])
157 | check(v.defined_funcs, [])
158 | check(v.defined_attrs, ["bar", "bar"])
159 | check(v.used_names, ["foo"])
160 | check(v.unused_attrs, ["bar", "bar"])
161 |
162 |
163 | def test_ignored_attributes(v):
164 | v.scan(
165 | """\
166 | A._ = 0
167 | A._a = 1
168 | A.__b = 2
169 | A.__c__ = 3
170 | A._d_ = 4
171 | """
172 | )
173 | check(v.defined_attrs, ["_", "_a", "__b", "__c__", "_d_"])
174 | check(v.used_names, ["A"])
175 | check(v.unused_attrs, ["_", "__b", "__c__", "_a", "_d_"])
176 | check(v.unused_vars, [])
177 |
178 |
179 | def test_getattr(v):
180 | v.scan(
181 | """\
182 | class Thing:
183 | used_attr1 = 1
184 | used_attr2 = 2
185 | used_attr3 = 3
186 | unused_attr = 4
187 |
188 | getattr(Thing, "used_attr1")
189 | getattr(Thing, "used_attr2", None)
190 | hasattr(Thing, "used_attr3")
191 |
192 | # Weird calls ignored
193 | hasattr(Thing, "unused_attr", None)
194 | getattr(Thing)
195 | getattr("unused_attr")
196 | getattr(Thing, "unused_attr", 1, 2)
197 | """
198 | )
199 | check(v.unused_vars, ["unused_attr"])
200 | check(
201 | v.used_names,
202 | [
203 | "Thing",
204 | "getattr",
205 | "hasattr",
206 | "used_attr1",
207 | "used_attr2",
208 | "used_attr3",
209 | ],
210 | )
211 |
212 |
213 | def test_callback1(v):
214 | v.scan(
215 | """\
216 | class Bar(object):
217 | def foo(self):
218 | pass
219 |
220 | b = Bar()
221 | b.foo
222 | """
223 | )
224 | check(v.used_names, ["Bar", "b", "foo"])
225 | check(v.defined_classes, ["Bar"])
226 | check(v.defined_funcs, [])
227 | check(v.defined_methods, ["foo"])
228 | check(v.unused_classes, [])
229 | check(v.unused_funcs, [])
230 |
231 |
232 | def test_class1(v):
233 | v.scan(
234 | """\
235 | class Bar(object):
236 | pass
237 | """
238 | )
239 | check(v.used_names, [])
240 | check(v.defined_classes, ["Bar"])
241 | check(v.unused_classes, ["Bar"])
242 |
243 |
244 | def test_class2(v):
245 | v.scan(
246 | """\
247 | class Bar():
248 | pass
249 | class Foo(Bar):
250 | pass
251 | Foo()
252 | """
253 | )
254 | check(v.used_names, ["Bar", "Foo"])
255 | check(v.defined_classes, ["Bar", "Foo"])
256 | check(v.unused_classes, [])
257 |
258 |
259 | def test_class3(v):
260 | v.scan(
261 | """\
262 | class Bar():
263 | pass
264 | [Bar]
265 | """
266 | )
267 | check(v.used_names, ["Bar"])
268 | check(v.defined_classes, ["Bar"])
269 | check(v.unused_classes, [])
270 |
271 |
272 | def test_class4(v):
273 | v.scan(
274 | """\
275 | class Bar():
276 | pass
277 | Bar()
278 | """
279 | )
280 | check(v.used_names, ["Bar"])
281 | check(v.defined_classes, ["Bar"])
282 | check(v.unused_classes, [])
283 |
284 |
285 | def test_class5(v):
286 | v.scan(
287 | """\
288 | class Bar():
289 | pass
290 | b = Bar()
291 | """
292 | )
293 | check(v.used_names, ["Bar"])
294 | check(v.defined_classes, ["Bar"])
295 | check(v.unused_classes, [])
296 | check(v.unused_vars, ["b"])
297 |
298 |
299 | def test_class6(v):
300 | v.scan(
301 | """\
302 | class Bar():
303 | pass
304 | a = []
305 | a.insert(0, Bar())
306 | """
307 | )
308 | check(v.defined_classes, ["Bar"])
309 | check(v.unused_classes, [])
310 |
311 |
312 | def test_class7(v):
313 | v.scan(
314 | """\
315 | class Bar(object):
316 | pass
317 |
318 | class Foo(object):
319 | def __init__(self):
320 | self.b = xyz.Bar(self)
321 | """
322 | )
323 | check(v.defined_classes, ["Bar", "Foo"])
324 | check(v.unused_classes, ["Foo"])
325 |
326 |
327 | def test_method1(v):
328 | v.scan(
329 | """\
330 | def __init__(self):
331 | self.a.foo()
332 |
333 | class Bar(object):
334 | def foo(self):
335 | pass
336 |
337 | @classmethod
338 | def bar(cls):
339 | pass
340 |
341 | @staticmethod
342 | def foobar():
343 | pass
344 | """
345 | )
346 | check(v.defined_classes, ["Bar"])
347 | check(v.defined_funcs, [])
348 | check(v.defined_methods, ["foo", "bar", "foobar"])
349 | check(v.unused_classes, ["Bar"])
350 | check(v.unused_funcs, [])
351 | check(v.unused_methods, ["bar", "foobar"])
352 |
353 |
354 | def test_token_types(v):
355 | v.scan(
356 | """\
357 | a
358 | b = 2
359 | c()
360 | x.d
361 | """
362 | )
363 | check(v.defined_funcs, [])
364 | check(v.defined_vars, ["b"])
365 | check(v.used_names, ["a", "c", "d", "x"])
366 | check(v.unused_attrs, [])
367 | check(v.unused_funcs, [])
368 | check(v.unused_props, [])
369 | check(v.unused_vars, ["b"])
370 |
371 |
372 | def test_variable1(v):
373 | v.scan("a = 1\nb = a")
374 | check(v.defined_funcs, [])
375 | check(v.used_names, ["a"])
376 | check(v.defined_vars, ["a", "b"])
377 | check(v.unused_vars, ["b"])
378 |
379 |
380 | def test_variable2(v):
381 | v.scan("a = 1\nc = b.a")
382 | check(v.defined_funcs, [])
383 | check(v.defined_vars, ["a", "c"])
384 | check(v.used_names, ["a", "b"])
385 | check(v.unused_vars, ["c"])
386 |
387 |
388 | def test_variable3(v):
389 | v.scan("(a, b), c = (d, e, f)")
390 | check(v.defined_funcs, [])
391 | check(v.defined_vars, ["a", "b", "c"])
392 | check(v.used_names, ["d", "e", "f"])
393 | check(v.unused_vars, ["a", "b", "c"])
394 |
395 |
396 | def test_variable4(v):
397 | v.scan("for a, b in func(): a")
398 | check(v.defined_funcs, [])
399 | check(v.defined_vars, ["a", "b"])
400 | check(v.used_names, ["a", "func"])
401 | check(v.unused_vars, ["b"])
402 |
403 |
404 | def test_variable5(v):
405 | v.scan("[a for a, b in func()]")
406 | check(v.defined_vars, ["a", "b"])
407 | check(v.used_names, ["a", "func"])
408 | check(v.unused_vars, ["b"])
409 |
410 |
411 | def test_ignored_variables(v):
412 | v.scan(
413 | """\
414 | _ = 0
415 | _a = 1
416 | __b = 2
417 | __c__ = 3
418 | _d_ = 4
419 | """
420 | )
421 | check(v.defined_vars, ["__b"])
422 | check(sorted(v.used_names), [])
423 | check(v.unused_vars, ["__b"])
424 |
425 |
426 | def test_prop1(v):
427 | v.scan(
428 | """\
429 | class Bar(object):
430 | @property
431 | def prop(self):
432 | pass
433 |
434 | c = Bar()
435 | c.prop
436 | """
437 | )
438 | check(v.defined_classes, ["Bar"])
439 | check(v.defined_props, ["prop"])
440 | check(v.unused_classes, [])
441 | check(v.unused_props, [])
442 |
443 |
444 | def test_prop2(v):
445 | v.scan(
446 | """\
447 | class Bar(object):
448 | @property
449 | def prop(self):
450 | pass
451 |
452 | prop = 1
453 | """
454 | )
455 | check(v.defined_classes, ["Bar"])
456 | check(v.defined_props, ["prop"])
457 | check(v.defined_vars, ["prop"])
458 | check(v.unused_classes, ["Bar"])
459 | check(v.unused_props, ["prop"])
460 |
461 |
462 | def test_object_attribute(v):
463 | v.scan(
464 | """\
465 | class Bar(object):
466 | def __init__(self):
467 | self.a = []
468 | """
469 | )
470 | check(v.defined_attrs, ["a"])
471 | check(v.defined_classes, ["Bar"])
472 | check(v.defined_vars, [])
473 | check(v.used_names, [])
474 | check(v.unused_attrs, ["a"])
475 | check(v.unused_classes, ["Bar"])
476 |
477 |
478 | def test_function_names_in_test_file(v):
479 | v.scan(
480 | """\
481 | def setup_module(module):
482 | module
483 |
484 | def teardown_module(module):
485 | module
486 |
487 | def setup_function(function):
488 | function
489 |
490 | def teardown_function(function):
491 | function
492 |
493 | def test_func():
494 | pass
495 |
496 | def other_func():
497 | pass
498 |
499 | class TestClass:
500 | @classmethod
501 | def setup_class(cls):
502 | cls
503 |
504 | @classmethod
505 | def teardown_class(cls):
506 | pass
507 |
508 | def setup_method(self, method):
509 | method
510 |
511 | def teardown_method(self, method):
512 | pass
513 |
514 | class BasicTestCase:
515 | pass
516 |
517 | class OtherClass:
518 | pass
519 | """,
520 | filename="dir/test_function_names.py",
521 | )
522 | check(v.defined_attrs, [])
523 | check(v.defined_classes, ["OtherClass"])
524 | check(v.defined_funcs, ["other_func"])
525 | check(v.defined_methods, [])
526 | check(
527 | v.defined_vars,
528 | [
529 | "cls",
530 | "cls",
531 | "function",
532 | "function",
533 | "method",
534 | "method",
535 | "module",
536 | "module",
537 | ],
538 | )
539 | check(v.used_names, ["classmethod", "cls", "function", "method", "module"])
540 | check(v.unused_attrs, [])
541 | check(v.unused_classes, ["OtherClass"])
542 | check(v.unused_funcs, ["other_func"])
543 | check(v.unused_methods, [])
544 | check(v.unused_vars, [])
545 |
546 |
547 | def test_async_function_name_in_test_file(v):
548 | v.scan(
549 | """\
550 | async def test_func():
551 | pass
552 |
553 | async def other_func():
554 | pass
555 | """,
556 | filename="dir/test_function_names.py",
557 | )
558 | check(v.defined_funcs, ["other_func"])
559 | check(v.unused_funcs, ["other_func"])
560 |
561 |
562 | def test_async_function_name_in_normal_file(v):
563 | v.scan(
564 | """\
565 | async def test_func():
566 | pass
567 |
568 | async def other_func():
569 | pass
570 | """,
571 | filename="dir/function_names.py",
572 | )
573 | check(v.defined_funcs, ["test_func", "other_func"])
574 | check(v.unused_funcs, ["other_func", "test_func"])
575 |
576 |
577 | def test_function_names_in_normal_file(v):
578 | v.scan(
579 | """\
580 | def test_func():
581 | pass
582 |
583 | def other_func():
584 | pass
585 |
586 | class TestClass:
587 | pass
588 |
589 | class BasicTestCase:
590 | pass
591 |
592 | class OtherClass:
593 | pass
594 | """
595 | )
596 | check(v.defined_attrs, [])
597 | check(v.defined_classes, ["BasicTestCase", "OtherClass", "TestClass"])
598 | check(v.defined_funcs, ["test_func", "other_func"])
599 | check(v.defined_vars, [])
600 | check(v.used_names, [])
601 | check(v.unused_attrs, [])
602 | check(v.unused_classes, ["BasicTestCase", "OtherClass", "TestClass"])
603 | check(v.unused_funcs, ["other_func", "test_func"])
604 |
605 |
606 | def test_global_attribute(v):
607 | v.scan(
608 | """\
609 | # Module foo:
610 | a = 1
611 | if a == 1:
612 | pass
613 |
614 | # Module bar:
615 | import foo
616 | foo.a = 2
617 | """
618 | )
619 | check(v.defined_attrs, ["a"])
620 | check(v.defined_vars, ["a"])
621 | check(v.used_names, ["a", "foo"])
622 | check(v.unused_attrs, [])
623 |
624 |
625 | def test_boolean(v):
626 | v.scan(
627 | """\
628 | a = True
629 | a
630 | """
631 | )
632 | check(v.defined_vars, ["a"])
633 | check(v.used_names, ["a"])
634 | check(v.unused_vars, [])
635 |
636 |
637 | def test_builtin_types(v):
638 | v.scan(
639 | """\
640 | a = b
641 | a = 1
642 | a = "s"
643 | a = object
644 | a = False
645 | """
646 | )
647 | check(v.defined_vars, ["a"] * 5)
648 | check(v.used_names, ["b"])
649 | check(v.unused_vars, ["a"] * 5)
650 |
651 |
652 | def test_unused_args(v):
653 | v.scan(
654 | """\
655 | def foo(x, y):
656 | return x + 1
657 | """
658 | )
659 | check(v.defined_vars, ["x", "y"])
660 | check(v.used_names, ["x"])
661 | check(v.unused_vars, ["y"])
662 |
663 |
664 | def test_unused_kwargs(v):
665 | v.scan(
666 | """\
667 | def foo(x, y=3, **kwargs):
668 | return x + 1
669 | """
670 | )
671 | check(v.defined_vars, ["kwargs", "x", "y"])
672 | check(v.used_names, ["x"])
673 | check(v.unused_vars, ["kwargs", "y"])
674 |
675 |
676 | def test_unused_kwargs_with_odd_name(v):
677 | v.scan(
678 | """\
679 | def foo(**bar):
680 | pass
681 | """
682 | )
683 | check(v.defined_vars, ["bar"])
684 | check(v.used_names, [])
685 | check(v.unused_vars, ["bar"])
686 |
687 |
688 | def test_unused_vararg(v):
689 | v.scan(
690 | """\
691 | def foo(*bar):
692 | pass
693 | """
694 | )
695 | check(v.defined_vars, ["bar"])
696 | check(v.used_names, [])
697 | check(v.unused_vars, ["bar"])
698 |
699 |
700 | def test_multiple_definition(v):
701 | v.scan(
702 | """\
703 | a = 1
704 | a = 2
705 | """
706 | )
707 | check(v.defined_vars, ["a", "a"])
708 | check(v.used_names, [])
709 | check(v.unused_vars, ["a", "a"])
710 |
711 |
712 | def test_arg_type_annotation(v):
713 | v.scan(
714 | """\
715 | from typing import Iterable
716 |
717 | def f(n: int) -> Iterable[int]:
718 | yield n
719 | """
720 | )
721 |
722 | check(v.unused_vars, [])
723 | check(v.unused_funcs, ["f"])
724 | check(v.unused_imports, [])
725 |
726 |
727 | def test_var_type_annotation(v):
728 | v.scan(
729 | """\
730 | from typing import List
731 |
732 | x: List[int] = [1]
733 | """
734 | )
735 |
736 | check(v.unused_vars, ["x"])
737 | check(v.unused_funcs, [])
738 | check(v.unused_imports, [])
739 |
740 |
741 | def test_type_hint_comments(v):
742 | v.scan(
743 | """\
744 | from typing import Any, Dict, List, Text, Tuple
745 |
746 |
747 | def plain_function(arg):
748 | # type: (Text) -> None
749 | pass
750 |
751 | async def async_function(arg):
752 | # type: (List[int]) -> None
753 | pass
754 |
755 | some_var = {} # type: Dict[str, str]
756 |
757 | class Thing:
758 | def __init__(self):
759 | self.some_attr = (1, 2) # type: Tuple[int, int]
760 |
761 | for x in []: # type: Any
762 | print(x)
763 | """
764 | )
765 |
766 | check(v.unused_imports, [])
767 | assert v.exit_code == ExitCode.NoDeadCode
768 |
769 |
770 | def test_invalid_type_comment(v):
771 | v.scan(
772 | """\
773 | def bad():
774 | # type: bogus
775 | pass
776 | bad()
777 | """
778 | )
779 |
780 | assert v.exit_code == ExitCode.InvalidInput
781 |
782 |
783 | def test_unused_args_with_del(v):
784 | v.scan(
785 | """\
786 | def foo(a, b, c, d=3):
787 | del c, d
788 | return a + b
789 |
790 | foo(1, 2)
791 | """
792 | )
793 |
794 | check(v.defined_funcs, ["foo"])
795 | check(v.defined_vars, ["a", "b", "c", "d"])
796 | check(v.used_names, ["foo", "a", "b", "c", "d"])
797 | check(v.unused_vars, [])
798 | check(v.unused_funcs, [])
799 |
800 |
801 | @pytest.mark.skipif(
802 | sys.version_info < (3, 10), reason="requires python3.10 or higher"
803 | )
804 | def test_match_class_simple(v):
805 | v.scan(
806 | """\
807 | from dataclasses import dataclass
808 |
809 |
810 | @dataclass
811 | class X:
812 | a: int
813 | b: int
814 | c: int
815 | u: int
816 |
817 | x = input()
818 |
819 | match x:
820 | case X(a=0):
821 | print("a")
822 | case X(b=0, c=0):
823 | print("b c")
824 | """
825 | )
826 | check(v.defined_classes, ["X"])
827 | check(v.defined_vars, ["a", "b", "c", "u", "x"])
828 |
829 | check(v.unused_classes, [])
830 | check(v.unused_vars, ["u"])
831 |
832 |
833 | @pytest.mark.skipif(
834 | sys.version_info < (3, 10), reason="requires python3.10 or higher"
835 | )
836 | def test_match_class_embedded(v):
837 | v.scan(
838 | """\
839 | from dataclasses import dataclass
840 |
841 |
842 | @dataclass
843 | class X:
844 | a: int
845 | b: int
846 | c: int
847 | d: int
848 | e: int
849 | u: int
850 |
851 | x = input()
852 |
853 | match x:
854 | case X(a=1) | X(b=0):
855 | print("Or")
856 | case [X(c=1), X(d=0)]:
857 | print("Sequence")
858 | case {"k": X(e=1)}:
859 | print("Mapping")
860 | """
861 | )
862 | check(v.defined_classes, ["X"])
863 | check(v.defined_vars, ["a", "b", "c", "d", "e", "u", "x"])
864 |
865 | check(v.unused_classes, [])
866 | check(v.unused_vars, ["u"])
867 |
868 |
869 | @pytest.mark.skipif(
870 | sys.version_info < (3, 10), reason="requires python3.10 or higher"
871 | )
872 | def test_match_enum(v):
873 | v.scan(
874 | """\
875 | from enum import Enum
876 |
877 |
878 | class Color(Enum):
879 | RED = 0
880 | YELLOW = 1
881 | GREEN = 2
882 | BLUE = 3
883 |
884 | color = input()
885 |
886 | match color:
887 | case Color.RED:
888 | print("Real danger!")
889 | case Color.YELLOW | Color.GREEN:
890 | print("No danger!")
891 | """
892 | )
893 | check(v.defined_classes, ["Color"])
894 | check(v.defined_vars, ["RED", "YELLOW", "GREEN", "BLUE", "color"])
895 |
896 | check(v.unused_classes, [])
897 | check(v.unused_vars, ["BLUE"])
898 |
--------------------------------------------------------------------------------
/tests/test_script.py:
--------------------------------------------------------------------------------
1 | import glob
2 | import os.path
3 | import subprocess
4 | import sys
5 |
6 | from vulture.utils import ExitCode
7 |
8 | from . import REPO, WHITELISTS, call_vulture
9 |
10 |
11 | def test_module_with_explicit_whitelists():
12 | assert call_vulture(["vulture/"] + WHITELISTS) == ExitCode.NoDeadCode
13 |
14 |
15 | def test_module_with_implicit_whitelists():
16 | assert call_vulture(["vulture/"]) == ExitCode.NoDeadCode
17 |
18 |
19 | def test_module_without_whitelists():
20 | assert (
21 | call_vulture(["vulture/", "--exclude", "whitelists"])
22 | == ExitCode.DeadCode
23 | )
24 |
25 |
26 | def test_missing_file():
27 | assert call_vulture(["missing.py"]) == ExitCode.InvalidInput
28 |
29 |
30 | def test_tests():
31 | assert call_vulture(["tests/"]) == ExitCode.NoDeadCode
32 |
33 |
34 | def test_whitelists_with_python():
35 | for whitelist in WHITELISTS:
36 | assert (
37 | subprocess.call([sys.executable, whitelist], cwd=REPO)
38 | == ExitCode.NoDeadCode
39 | )
40 |
41 |
42 | def test_pyc():
43 | assert call_vulture(["missing.pyc"]) == 1
44 |
45 |
46 | def test_sort_by_size():
47 | assert (
48 | call_vulture(["vulture/utils.py", "--sort-by-size"])
49 | == ExitCode.DeadCode
50 | )
51 |
52 |
53 | def test_min_confidence():
54 | assert (
55 | call_vulture(
56 | [
57 | "vulture/core.py",
58 | "--exclude",
59 | "whitelists",
60 | "--min-confidence",
61 | "100",
62 | ]
63 | )
64 | == ExitCode.NoDeadCode
65 | )
66 |
67 |
68 | def test_exclude():
69 | def get_csv(paths):
70 | return ",".join(os.path.join("vulture", path) for path in paths)
71 |
72 | def call_vulture_with_excludes(excludes):
73 | return call_vulture(["vulture/", "--exclude", get_csv(excludes)])
74 |
75 | assert (
76 | call_vulture_with_excludes(["core.py", "utils.py"])
77 | == ExitCode.DeadCode
78 | )
79 | assert (
80 | call_vulture_with_excludes(glob.glob("vulture/*.py"))
81 | == ExitCode.NoDeadCode
82 | )
83 |
84 |
85 | def test_make_whitelist():
86 | assert (
87 | call_vulture(
88 | ["vulture/", "--make-whitelist", "--exclude", "whitelists"]
89 | )
90 | == ExitCode.DeadCode
91 | )
92 | assert (
93 | call_vulture(["vulture/", "--make-whitelist"]) == ExitCode.NoDeadCode
94 | )
95 |
96 |
97 | def test_version():
98 | assert call_vulture(["--version"]) == ExitCode.NoDeadCode
99 |
--------------------------------------------------------------------------------
/tests/test_size.py:
--------------------------------------------------------------------------------
1 | import ast
2 |
3 | from vulture import lines
4 |
5 |
6 | def count_lines(node):
7 | """Estimate the number of lines of the given AST node."""
8 | last_lineno = lines.get_last_line_number(node)
9 | return last_lineno - lines.get_first_line_number(node) + 1
10 |
11 |
12 | def check_size(example, size):
13 | tree = ast.parse(example)
14 | for node in tree.body:
15 | if isinstance(node, ast.ClassDef) and node.name == "Foo":
16 | assert count_lines(node) == size
17 | break
18 | else:
19 | raise AssertionError('Failed to find top-level class "Foo" in code')
20 |
21 |
22 | def test_size_basic():
23 | example = """
24 | class Foo:
25 | foo = 1
26 | bar = 2
27 | """
28 | check_size(example, 3)
29 |
30 |
31 | def test_size_class():
32 | example = """
33 | class Foo(object):
34 | def bar():
35 | pass
36 |
37 | @staticmethod
38 | def func():
39 | if "foo" == "bar":
40 | return "xyz"
41 | import sys
42 | return len(sys.argv)
43 | """
44 | check_size(example, 10)
45 |
46 |
47 | def test_size_if_else():
48 | example = """
49 | @identity
50 | class Foo(object):
51 | @identity
52 | @identity
53 | def bar(self):
54 | if "a" == "b":
55 | pass
56 | elif "b" == "c":
57 | pass
58 | else:
59 | pass
60 | """
61 | size = 11
62 | check_size(example, size)
63 |
64 |
65 | def test_size_decorated_class():
66 | example = """
67 | @foo
68 | @property
69 | @xoo
70 | class Foo:
71 | def zoo(self):
72 | pass
73 | """
74 | check_size(example, 6)
75 |
76 |
77 | def test_size_while():
78 | example = """
79 | class Foo:
80 | while 1:
81 | print(1)
82 | """
83 | check_size(example, 3)
84 |
85 |
86 | def test_size_while_else():
87 | example = """
88 | class Foo:
89 | while "b" > "a":
90 | pass
91 | else:
92 | pass
93 | """
94 | check_size(example, 5)
95 |
96 |
97 | def test_size_with():
98 | example = """
99 | class Foo:
100 | with open("/dev/null") as f:
101 | f.write("")
102 | """
103 | check_size(example, 3)
104 |
105 |
106 | def test_size_try_except_else():
107 | example = """
108 | class Foo:
109 | try:
110 | x = sys.argv[99]
111 | except IndexError:
112 | pass
113 | except Exception:
114 | pass
115 | else:
116 | pass
117 | """
118 | check_size(example, 9)
119 |
120 |
121 | def test_size_try_finally():
122 | example = """
123 | class Foo:
124 | try:
125 | 1/0
126 | finally:
127 | return 99
128 | """
129 | check_size(example, 5)
130 |
131 |
132 | def test_size_try_except():
133 | example = """
134 | class Foo:
135 | try:
136 | foo()
137 | except:
138 | bar()
139 | """
140 | check_size(example, 5)
141 |
142 |
143 | def test_size_try_excepts():
144 | example = """
145 | class Foo:
146 | try:
147 | foo()
148 | except IOError:
149 | bar()
150 | except AttributeError:
151 | pass
152 | """
153 | check_size(example, 7)
154 |
155 |
156 | def test_size_for():
157 | example = """
158 | class Foo:
159 | for i in range(10):
160 | print(i)
161 | """
162 | check_size(example, 3)
163 |
164 |
165 | def test_size_for_else():
166 | example = """
167 | class Foo:
168 | for arg in sys.argv:
169 | print("loop")
170 | else:
171 | print("else")
172 | """
173 | check_size(example, 5)
174 |
175 |
176 | def test_size_class_nested():
177 | example = """
178 | class Foo:
179 | class Bar:
180 | pass
181 | """
182 | check_size(example, 3)
183 |
184 |
185 | # We currently cannot handle code ending with multiline strings.
186 | def test_size_multi_line_return():
187 | example = """
188 | class Foo:
189 | def foo():
190 | return (
191 | 'very'
192 | 'long'
193 | 'string')
194 | """
195 | check_size(example, 6)
196 |
197 |
198 | # We currently cannot handle code ending with comment lines.
199 | def test_size_comment_after_last_line():
200 | example = """
201 | class Foo:
202 | def bar():
203 | # A comment.
204 | pass
205 | # This comment won't be detected.
206 | """
207 | check_size(example, 4)
208 |
209 |
210 | def test_size_generator():
211 | example = """
212 | class Foo:
213 | def bar():
214 | yield something
215 | """
216 | check_size(example, 3)
217 |
218 |
219 | def test_size_exec():
220 | example = """
221 | class Foo:
222 | exec('a')
223 | """
224 | check_size(example, 2)
225 |
226 |
227 | def test_size_print1():
228 | example = """
229 | class Foo:
230 | print(
231 | 'foo')
232 | """
233 | check_size(example, 3)
234 |
235 |
236 | def test_size_print2():
237 | example = """
238 | class Foo:
239 | print(
240 | 'foo',)
241 | """
242 | check_size(example, 3)
243 |
244 |
245 | def test_size_return():
246 | example = """
247 | class Foo:
248 | return (True and
249 | False)
250 | """
251 | check_size(example, 3)
252 |
253 |
254 | def test_size_import_from():
255 | example = """
256 | class Foo:
257 | from a import b
258 | """
259 | check_size(example, 2)
260 |
261 |
262 | def test_size_delete():
263 | example = """
264 | class Foo:
265 | del a[:
266 | foo()]
267 | """
268 | check_size(example, 3)
269 |
270 |
271 | def test_size_list_comprehension():
272 | example = """
273 | class Foo:
274 | [a
275 | for a in
276 | b]
277 | """
278 | check_size(example, 4)
279 |
280 |
281 | # We currently cannot handle closing brackets on a separate line.
282 | def test_size_list():
283 | example = """
284 | class Foo:
285 | [a, b
286 | ]
287 | """
288 | check_size(example, 3)
289 |
290 |
291 | def test_size_ellipsis():
292 | example = """
293 | class Foo:
294 | bar[1:2,
295 | ...]
296 | """
297 | check_size(example, 3)
298 |
299 |
300 | def test_size_starargs():
301 | example = """
302 | class Foo:
303 | def foo():
304 | bar(*a,
305 | b=c)
306 | """
307 | check_size(example, 4)
308 |
309 |
310 | # If we add a line break between a and b, the code is too greedy and moves
311 | # down to the slice which has no line numbers. If we took b or c into
312 | # account, the line count would be correct.
313 | def test_size_assign():
314 | example = """
315 | class Foo:
316 | bar = foo(a, b)[c,:]
317 | """
318 | check_size(example, 2)
319 |
320 |
321 | def test_size_async_function_def():
322 | example = """
323 | class Foo:
324 | async def foo(some_attr):
325 | pass
326 | """
327 | check_size(example, 3)
328 |
329 |
330 | def test_size_async_with():
331 | example = """
332 | class Foo:
333 | async def bar():
334 | async with x:
335 | pass
336 | """
337 | check_size(example, 4)
338 |
339 |
340 | def test_size_async_for():
341 | example = """
342 | class Foo:
343 | async def foo():
344 | async for a in b:
345 | pass
346 | """
347 | check_size(example, 4)
348 |
--------------------------------------------------------------------------------
/tests/test_sorting.py:
--------------------------------------------------------------------------------
1 | from . import v
2 |
3 | assert v # Silence pyflakes
4 |
5 |
6 | def test_sorting(v):
7 | v.scan(
8 | """\
9 | def foo():
10 | print("Hello, I am a long function.")
11 | return "World"
12 |
13 | def bar():
14 | pass
15 | """
16 | )
17 | assert [item.name for item in v.get_unused_code(sort_by_size=True)] == [
18 | "bar",
19 | "foo",
20 | ]
21 | assert [item.name for item in v.get_unused_code(sort_by_size=False)] == [
22 | "foo",
23 | "bar",
24 | ]
25 |
--------------------------------------------------------------------------------
/tests/test_utils.py:
--------------------------------------------------------------------------------
1 | import ast
2 | import os
3 | import pathlib
4 | import sys
5 |
6 | import pytest
7 |
8 | from vulture import utils
9 |
10 |
11 | class TestFormatPath:
12 | @pytest.fixture
13 | def tmp_cwd(self, tmp_path, monkeypatch):
14 | cwd = tmp_path / "workingdir"
15 | cwd.mkdir()
16 | monkeypatch.chdir(cwd)
17 | return cwd
18 |
19 | def test_relative_inside(self):
20 | filepath = pathlib.Path("testfile.py")
21 | formatted = utils.format_path(filepath)
22 | assert formatted == filepath
23 | assert not formatted.is_absolute()
24 |
25 | def test_relative_outside(self, tmp_cwd):
26 | filepath = pathlib.Path(os.pardir) / "testfile.py"
27 | formatted = utils.format_path(filepath)
28 | assert formatted == filepath
29 | assert not formatted.is_absolute()
30 |
31 | def test_absolute_inside(self, tmp_cwd):
32 | filepath = tmp_cwd / "testfile.py"
33 | formatted = utils.format_path(filepath)
34 | assert formatted == pathlib.Path("testfile.py")
35 | assert not formatted.is_absolute()
36 |
37 | def test_absolute_outside(self, tmp_cwd):
38 | filepath = (tmp_cwd / os.pardir / "testfile.py").resolve()
39 | formatted = utils.format_path(filepath)
40 | assert formatted == filepath
41 | assert formatted.is_absolute()
42 |
43 |
44 | def check_decorator_names(code, expected_names):
45 | decorator_names = []
46 |
47 | def visit_FunctionDef(node):
48 | for decorator in node.decorator_list:
49 | decorator_names.append(utils.get_decorator_name(decorator))
50 |
51 | node_visitor = ast.NodeVisitor()
52 | node_visitor.visit_AsyncFunctionDef = visit_FunctionDef
53 | node_visitor.visit_ClassDef = visit_FunctionDef
54 | node_visitor.visit_FunctionDef = visit_FunctionDef
55 | node_visitor.visit(ast.parse(code))
56 | assert expected_names == decorator_names
57 |
58 |
59 | def test_get_decorator_name_simple():
60 | code = """\
61 | @foobar
62 | def hoo():
63 | pass
64 | """
65 | check_decorator_names(code, ["@foobar"])
66 |
67 |
68 | def test_get_decorator_name_call():
69 | code = """\
70 | @xyz()
71 | def bar():
72 | pass
73 | """
74 | check_decorator_names(code, ["@xyz"])
75 |
76 |
77 | def test_get_decorator_name_async():
78 | code = """\
79 | @foo.bar.route('/foobar')
80 | async def async_function(request):
81 | print(request)
82 | """
83 | check_decorator_names(code, ["@foo.bar.route"])
84 |
85 |
86 | def test_get_decorator_name_multiple_attrs():
87 | code = """\
88 | @x.y.z
89 | def doo():
90 | pass
91 | """
92 | check_decorator_names(code, ["@x.y.z"])
93 |
94 |
95 | def test_get_decorator_name_multiple_attrs_called():
96 | code = """\
97 | @a.b.c.d.foo("Foo and Bar")
98 | def hoofoo():
99 | pass
100 | """
101 | check_decorator_names(code, ["@a.b.c.d.foo"])
102 |
103 |
104 | def test_get_decorator_name_multiple_decorators():
105 | code = """\
106 | @foo
107 | @bar()
108 | @x.y.z.a('foobar')
109 | def func():
110 | pass
111 | """
112 | check_decorator_names(code, ["@foo", "@bar", "@x.y.z.a"])
113 |
114 |
115 | def test_get_decorator_name_class():
116 | code = """\
117 | @foo
118 | @bar.yz
119 | class Foo:
120 | pass
121 | """
122 | check_decorator_names(code, ["@foo", "@bar.yz"])
123 |
124 |
125 | def test_get_decorator_name_end_function_call():
126 | code = """\
127 | @foo.bar(x, y, z)
128 | def bar():
129 | pass
130 | """
131 | check_decorator_names(code, ["@foo.bar"])
132 |
133 |
134 | @pytest.mark.skipif(
135 | sys.version_info < (3, 9), reason="requires Python 3.9 or higher"
136 | )
137 | @pytest.mark.parametrize(
138 | "decorated",
139 | [
140 | ("def foo():"),
141 | ("async def foo():"),
142 | ("class Foo:"),
143 | ],
144 | )
145 | def test_get_decorator_name_multiple_callables(decorated):
146 | decorated = f"{decorated}\n pass"
147 | code = f"""\
148 | @foo
149 | @bar.prop
150 | @z.func("hi").bar().k.foo
151 | @k("hello").doo("world").x
152 | @k.hello("world")
153 | @foo[2]
154 | {decorated}
155 | """
156 | check_decorator_names(
157 | code,
158 | ["@foo", "@bar.prop", "@", "@", "@k.hello", "@"],
159 | )
160 |
--------------------------------------------------------------------------------
/tests/toml/mock_pyproject.toml:
--------------------------------------------------------------------------------
1 | # This file exists for the test case: test_config::test_toml_config_custom_path
2 |
3 | [tool.vulture]
4 | verbose = true
5 | ignore_names = ["name_from_toml_file"]
6 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | envlist = cleanup, py{38,310,311,312,313} # Skip py39 since it chokes on distutils.
3 | skip_missing_interpreters = true
4 |
5 | # Erase old coverage results, then accumulate them during this tox run.
6 | [testenv:cleanup]
7 | deps =
8 | coverage
9 | commands =
10 | coverage erase
11 |
12 | [testenv]
13 | deps =
14 | coverage
15 | pint # Use latest version to catch API changes.
16 | pytest
17 | pytest-cov
18 | pytype ; python_version < '3.13'
19 | commands =
20 | pytest {posargs}
21 | # Install package as wheel in all envs (https://hynek.me/articles/turbo-charge-tox/).
22 | package = wheel
23 | wheel_build_env = .pkg
24 |
25 | [pytest]
26 | filterwarnings =
27 | error::DeprecationWarning
28 | error::PendingDeprecationWarning
29 |
--------------------------------------------------------------------------------
/vulture/__init__.py:
--------------------------------------------------------------------------------
1 | from vulture.core import Vulture
2 | from vulture.version import __version__
3 |
4 | assert __version__
5 | assert Vulture
6 |
--------------------------------------------------------------------------------
/vulture/__main__.py:
--------------------------------------------------------------------------------
1 | from vulture.core import main
2 |
3 | main()
4 |
--------------------------------------------------------------------------------
/vulture/config.py:
--------------------------------------------------------------------------------
1 | """
2 | This module handles retrieval of configuration values from either the
3 | command-line arguments or the pyproject.toml file.
4 | """
5 | import argparse
6 | import pathlib
7 |
8 | try:
9 | import tomllib
10 | except ModuleNotFoundError:
11 | import tomli as tomllib
12 |
13 | from .version import __version__
14 |
15 | #: Possible configuration options and their respective defaults
16 | DEFAULTS = {
17 | "config": "pyproject.toml",
18 | "min_confidence": 0,
19 | "paths": [],
20 | "exclude": [],
21 | "ignore_decorators": [],
22 | "ignore_names": [],
23 | "make_whitelist": False,
24 | "sort_by_size": False,
25 | "verbose": False,
26 | }
27 |
28 |
29 | class InputError(Exception):
30 | def __init__(self, message):
31 | self.message = message
32 |
33 |
34 | def _check_input_config(data):
35 | """
36 | Checks the types of the values in *data* against the expected types of
37 | config-values. If a value has the wrong type, raise an InputError.
38 | """
39 | for key, value in data.items():
40 | if key not in DEFAULTS:
41 | raise InputError(f"Unknown configuration key: {key}")
42 | # The linter suggests to use "isinstance" here but this fails to
43 | # detect the difference between `int` and `bool`.
44 | if type(value) is not type(DEFAULTS[key]): # noqa: E721
45 | expected_type = type(DEFAULTS[key]).__name__
46 | raise InputError(f"Data type for {key} must be {expected_type!r}")
47 |
48 |
49 | def _check_output_config(config):
50 | """
51 | Run sanity checks on the generated config after all parsing and
52 | preprocessing is done.
53 |
54 | Raise InputError if an error is encountered.
55 | """
56 | if not config["paths"]:
57 | raise InputError("Please pass at least one file or directory")
58 |
59 |
60 | def _parse_toml(infile):
61 | """
62 | Parse a TOML file for config values.
63 |
64 | It will search for a section named ``[tool.vulture]`` which contains the
65 | same keys as the CLI arguments seen with ``--help``. All leading dashes are
66 | removed and other dashes are replaced by underscores (so ``--sort-by-size``
67 | becomes ``sort_by_size``).
68 |
69 | Arguments containing multiple values are standard TOML lists.
70 |
71 | Example::
72 |
73 | [tool.vulture]
74 | exclude = ["file*.py", "dir/"]
75 | ignore_decorators = ["deco1", "deco2"]
76 | ignore_names = ["name1", "name2"]
77 | make_whitelist = true
78 | min_confidence = 10
79 | sort_by_size = true
80 | verbose = true
81 | paths = ["path1", "path2"]
82 | """
83 | data = tomllib.load(infile)
84 | settings = data.get("tool", {}).get("vulture", {})
85 | _check_input_config(settings)
86 | return settings
87 |
88 |
89 | def _parse_args(args=None):
90 | """
91 | Parse CLI arguments.
92 |
93 | :param args: A list of strings representing the CLI arguments. If left to
94 | the default, this will default to ``sys.argv``.
95 | """
96 |
97 | # Sentinel value to distinguish between "False" and "no default given".
98 | missing = object()
99 |
100 | def csv(exclude):
101 | return exclude.split(",")
102 |
103 | usage = "%(prog)s [options] [PATH ...]"
104 | version = f"vulture {__version__}"
105 | glob_help = "Patterns may contain glob wildcards (*, ?, [abc], [!abc])."
106 | parser = argparse.ArgumentParser(prog="vulture", usage=usage)
107 | parser.add_argument(
108 | "paths",
109 | nargs="*",
110 | metavar="PATH",
111 | default=missing,
112 | help="Paths may be Python files or directories. For each directory"
113 | " Vulture analyzes all contained *.py files.",
114 | )
115 | parser.add_argument(
116 | "--exclude",
117 | metavar="PATTERNS",
118 | type=csv,
119 | default=missing,
120 | help=f"Comma-separated list of path patterns to ignore (e.g.,"
121 | f' "*settings.py,docs,*/test_*.py,venv"). {glob_help} A PATTERN'
122 | f" without glob wildcards is treated as *PATTERN*. Patterns are"
123 | f" matched against absolute paths.",
124 | )
125 | parser.add_argument(
126 | "--ignore-decorators",
127 | metavar="PATTERNS",
128 | type=csv,
129 | default=missing,
130 | help=f"Comma-separated list of decorators. Functions and classes using"
131 | f' these decorators are ignored (e.g., "@app.route,@require_*").'
132 | f" {glob_help}",
133 | )
134 | parser.add_argument(
135 | "--ignore-names",
136 | metavar="PATTERNS",
137 | type=csv,
138 | default=missing,
139 | help=f'Comma-separated list of names to ignore (e.g., "visit_*,do_*").'
140 | f" {glob_help}",
141 | )
142 | parser.add_argument(
143 | "--make-whitelist",
144 | action="store_true",
145 | default=missing,
146 | help="Report unused code in a format that can be added to a"
147 | " whitelist module.",
148 | )
149 | parser.add_argument(
150 | "--min-confidence",
151 | type=int,
152 | default=missing,
153 | help="Minimum confidence (between 0 and 100) for code to be"
154 | " reported as unused.",
155 | )
156 | parser.add_argument(
157 | "--sort-by-size",
158 | action="store_true",
159 | default=missing,
160 | help="Sort unused functions and classes by their lines of code.",
161 | )
162 | parser.add_argument(
163 | "--config",
164 | type=str,
165 | default="pyproject.toml",
166 | help="Path to pyproject.toml config file.",
167 | )
168 | parser.add_argument(
169 | "-v", "--verbose", action="store_true", default=missing
170 | )
171 | parser.add_argument("--version", action="version", version=version)
172 | namespace = parser.parse_args(args)
173 | cli_args = {
174 | key: value
175 | for key, value in vars(namespace).items()
176 | if value is not missing
177 | }
178 | _check_input_config(cli_args)
179 | return cli_args
180 |
181 |
182 | def make_config(argv=None, tomlfile=None):
183 | """
184 | Returns a config object for vulture, merging both ``pyproject.toml`` and
185 | CLI arguments (CLI arguments have precedence).
186 |
187 | :param argv: The CLI arguments to be parsed. This value is transparently
188 | passed through to :py:meth:`argparse.ArgumentParser.parse_args`.
189 | :param tomlfile: An IO instance containing TOML data. By default this will
190 | auto-detect an existing ``pyproject.toml`` file and exists solely for
191 | unit-testing.
192 | """
193 |
194 | # Parse CLI first to skip sanity checks when --version or --help is given.
195 | cli_config = _parse_args(argv)
196 |
197 | # If we loaded data from a TOML file, we want to print this out on stdout
198 | # in verbose mode so we need to keep the value around.
199 | detected_toml_path = ""
200 |
201 | if tomlfile:
202 | config = _parse_toml(tomlfile)
203 | detected_toml_path = str(tomlfile)
204 | else:
205 | toml_path = pathlib.Path(cli_config["config"]).resolve()
206 | if toml_path.is_file():
207 | with open(toml_path, "rb") as fconfig:
208 | config = _parse_toml(fconfig)
209 | detected_toml_path = str(toml_path)
210 | else:
211 | config = {}
212 |
213 | # Overwrite TOML options with CLI options, if given.
214 | config.update(cli_config)
215 |
216 | # Set defaults for missing options.
217 | for key, value in DEFAULTS.items():
218 | config.setdefault(key, value)
219 |
220 | if detected_toml_path and config["verbose"]:
221 | print(f"Reading configuration from {detected_toml_path}")
222 |
223 | _check_output_config(config)
224 |
225 | return config
226 |
--------------------------------------------------------------------------------
/vulture/core.py:
--------------------------------------------------------------------------------
1 | import ast
2 | import pkgutil
3 | import re
4 | import string
5 | import sys
6 | from fnmatch import fnmatch, fnmatchcase
7 | from functools import partial
8 | from pathlib import Path
9 | from typing import List
10 |
11 | from vulture import lines, noqa, utils
12 | from vulture.config import InputError, make_config
13 | from vulture.reachability import Reachability
14 | from vulture.utils import ExitCode
15 |
16 | DEFAULT_CONFIDENCE = 60
17 |
18 | IGNORED_VARIABLE_NAMES = {"object", "self"}
19 | PYTEST_FUNCTION_NAMES = {
20 | "setup_module",
21 | "teardown_module",
22 | "setup_function",
23 | "teardown_function",
24 | }
25 | PYTEST_METHOD_NAMES = {
26 | "setup_class",
27 | "teardown_class",
28 | "setup_method",
29 | "teardown_method",
30 | }
31 |
32 | ERROR_CODES = {
33 | "attribute": "V101",
34 | "class": "V102",
35 | "function": "V103",
36 | "import": "V104",
37 | "method": "V105",
38 | "property": "V106",
39 | "variable": "V107",
40 | "unreachable_code": "V201",
41 | }
42 |
43 |
44 | def _get_unused_items(defined_items, used_names):
45 | unused_items = [
46 | item for item in set(defined_items) if item.name not in used_names
47 | ]
48 | unused_items.sort(key=lambda item: item.name.lower())
49 | return unused_items
50 |
51 |
52 | def _is_special_name(name):
53 | return name.startswith("__") and name.endswith("__")
54 |
55 |
56 | def _match(name, patterns, case=True):
57 | func = fnmatchcase if case else fnmatch
58 | return any(func(name, pattern) for pattern in patterns)
59 |
60 |
61 | def _is_test_file(filename):
62 | return _match(
63 | filename.resolve(),
64 | ["*/test/*", "*/tests/*", "*/test*.py", "*[-_]test.py"],
65 | case=False,
66 | )
67 |
68 |
69 | def _assigns_special_variable__all__(node):
70 | assert isinstance(node, ast.Assign)
71 | return isinstance(node.value, (ast.List, ast.Tuple)) and any(
72 | target.id == "__all__"
73 | for target in node.targets
74 | if isinstance(target, ast.Name)
75 | )
76 |
77 |
78 | def _ignore_class(filename, class_name):
79 | return _is_test_file(filename) and "Test" in class_name
80 |
81 |
82 | def _ignore_import(filename, import_name):
83 | """
84 | Ignore star-imported names since we can't detect whether they are used.
85 | Ignore imports from __init__.py files since they're commonly used to
86 | collect objects from a package.
87 | """
88 | return filename.name == "__init__.py" or import_name == "*"
89 |
90 |
91 | def _ignore_function(filename, function_name):
92 | return (
93 | function_name in PYTEST_FUNCTION_NAMES
94 | or function_name.startswith("test_")
95 | ) and _is_test_file(filename)
96 |
97 |
98 | def _ignore_method(filename, method_name):
99 | return _is_special_name(method_name) or (
100 | (method_name in PYTEST_METHOD_NAMES or method_name.startswith("test_"))
101 | and _is_test_file(filename)
102 | )
103 |
104 |
105 | def _ignore_variable(filename, varname):
106 | """
107 | Ignore _ (Python idiom), _x (pylint convention) and
108 | __x__ (special variable or method), but not __x.
109 | """
110 | return (
111 | varname in IGNORED_VARIABLE_NAMES
112 | or (varname.startswith("_") and not varname.startswith("__"))
113 | or _is_special_name(varname)
114 | )
115 |
116 |
117 | class Item:
118 | """
119 | Hold the name, type and location of defined code.
120 | """
121 |
122 | __slots__ = (
123 | "name",
124 | "typ",
125 | "filename",
126 | "first_lineno",
127 | "last_lineno",
128 | "message",
129 | "confidence",
130 | )
131 |
132 | def __init__(
133 | self,
134 | name,
135 | typ,
136 | filename,
137 | first_lineno,
138 | last_lineno,
139 | message="",
140 | confidence=DEFAULT_CONFIDENCE,
141 | ):
142 | self.name: str = name
143 | self.typ: str = typ
144 | self.filename: Path = filename
145 | self.first_lineno: int = first_lineno
146 | self.last_lineno: int = last_lineno
147 | self.message: str = message or f"unused {typ} '{name}'"
148 | self.confidence: int = confidence
149 |
150 | @property
151 | def size(self):
152 | assert self.last_lineno >= self.first_lineno
153 | return self.last_lineno - self.first_lineno + 1
154 |
155 | def get_report(self, add_size=False):
156 | if add_size:
157 | line_format = "line" if self.size == 1 else "lines"
158 | size_report = f", {self.size:d} {line_format}"
159 | else:
160 | size_report = ""
161 | return (
162 | f"{utils.format_path(self.filename)}:{self.first_lineno:d}: "
163 | f"{self.message} ({self.confidence}% confidence{size_report})"
164 | )
165 |
166 | def get_whitelist_string(self):
167 | filename = utils.format_path(self.filename)
168 | if self.typ == "unreachable_code":
169 | return f"# {self.message} ({filename}:{self.first_lineno})"
170 | else:
171 | prefix = ""
172 | if self.typ in ["attribute", "method", "property"]:
173 | prefix = "_."
174 | return (
175 | f"{prefix}{self.name} # unused {self.typ} "
176 | f"({filename}:{self.first_lineno:d})"
177 | )
178 |
179 | def _tuple(self):
180 | return (self.filename, self.first_lineno, self.name)
181 |
182 | def __repr__(self):
183 | return repr(self.name)
184 |
185 | def __eq__(self, other):
186 | return self._tuple() == other._tuple()
187 |
188 | def __hash__(self):
189 | return hash(self._tuple())
190 |
191 |
192 | class Vulture(ast.NodeVisitor):
193 | """Find dead code."""
194 |
195 | def __init__(
196 | self, verbose=False, ignore_names=None, ignore_decorators=None
197 | ):
198 | self.verbose = verbose
199 |
200 | def get_list(typ):
201 | return utils.LoggingList(typ, self.verbose)
202 |
203 | self.defined_attrs = get_list("attribute")
204 | self.defined_classes = get_list("class")
205 | self.defined_funcs = get_list("function")
206 | self.defined_imports = get_list("import")
207 | self.defined_methods = get_list("method")
208 | self.defined_props = get_list("property")
209 | self.defined_vars = get_list("variable")
210 | self.unreachable_code = get_list("unreachable_code")
211 |
212 | self.used_names = utils.LoggingSet("name", self.verbose)
213 |
214 | self.ignore_names = ignore_names or []
215 | self.ignore_decorators = ignore_decorators or []
216 |
217 | self.filename = Path()
218 | self.code = []
219 | self.exit_code = ExitCode.NoDeadCode
220 | self.noqa_lines = {}
221 |
222 | report = partial(
223 | self._define,
224 | collection=self.unreachable_code,
225 | confidence=100,
226 | )
227 | self.reachability = Reachability(report=report)
228 |
229 | def scan(self, code, filename=""):
230 | filename = Path(filename)
231 | self.code = code.splitlines()
232 | self.noqa_lines = noqa.parse_noqa(self.code)
233 | self.filename = filename
234 |
235 | def handle_syntax_error(e):
236 | text = f' at "{e.text.strip()}"' if e.text else ""
237 | self._log(
238 | f"{utils.format_path(filename)}:{e.lineno}: {e.msg}{text}",
239 | file=sys.stderr,
240 | force=True,
241 | )
242 | self.exit_code = ExitCode.InvalidInput
243 |
244 | try:
245 | node = ast.parse(
246 | code, filename=str(self.filename), type_comments=True
247 | )
248 | except SyntaxError as err:
249 | handle_syntax_error(err)
250 | except ValueError as err:
251 | # ValueError is raised if source contains null bytes.
252 | self._log(
253 | f'{utils.format_path(filename)}: invalid source code "{err}"',
254 | file=sys.stderr,
255 | force=True,
256 | )
257 | self.exit_code = ExitCode.InvalidInput
258 | else:
259 | # When parsing type comments, visiting can throw SyntaxError.
260 | try:
261 | self.visit(node)
262 | except SyntaxError as err:
263 | handle_syntax_error(err)
264 |
265 | # Reset the reachability internals for every module to reduce memory
266 | # usage.
267 | self.reachability.reset()
268 |
269 | def scavenge(self, paths, exclude=None):
270 | def prepare_pattern(pattern):
271 | if not any(char in pattern for char in "*?["):
272 | pattern = f"*{pattern}*"
273 | return pattern
274 |
275 | exclude = [prepare_pattern(pattern) for pattern in (exclude or [])]
276 |
277 | def exclude_path(path):
278 | return _match(path, exclude, case=False)
279 |
280 | paths = [Path(path) for path in paths]
281 |
282 | for module in utils.get_modules(paths):
283 | if exclude_path(module):
284 | self._log("Excluded:", module)
285 | continue
286 |
287 | self._log("Scanning:", module)
288 | try:
289 | module_string = utils.read_file(module)
290 | except utils.VultureInputException as err: # noqa: F841
291 | self._log(
292 | f"Error: Could not read file {module} - {err}\n"
293 | f"Try to change the encoding to UTF-8.",
294 | file=sys.stderr,
295 | force=True,
296 | )
297 | self.exit_code = ExitCode.InvalidInput
298 | else:
299 | self.scan(module_string, filename=module)
300 |
301 | unique_imports = {item.name for item in self.defined_imports}
302 | for import_name in unique_imports:
303 | path = Path("whitelists") / (import_name + "_whitelist.py")
304 | if exclude_path(path):
305 | self._log("Excluded whitelist:", path)
306 | else:
307 | try:
308 | module_data = pkgutil.get_data("vulture", str(path))
309 | self._log("Included whitelist:", path)
310 | except OSError:
311 | # Most imported modules don't have a whitelist.
312 | continue
313 | assert module_data is not None
314 | module_string = module_data.decode("utf-8")
315 | self.scan(module_string, filename=path)
316 |
317 | def get_unused_code(
318 | self, min_confidence=0, sort_by_size=False
319 | ) -> List[Item]:
320 | """
321 | Return ordered list of unused Item objects.
322 | """
323 | if not 0 <= min_confidence <= 100:
324 | raise ValueError("min_confidence must be between 0 and 100.")
325 |
326 | def by_name(item):
327 | return (str(item.filename).lower(), item.first_lineno)
328 |
329 | def by_size(item):
330 | return (item.size,) + by_name(item)
331 |
332 | unused_code = (
333 | self.unused_attrs
334 | + self.unused_classes
335 | + self.unused_funcs
336 | + self.unused_imports
337 | + self.unused_methods
338 | + self.unused_props
339 | + self.unused_vars
340 | + self.unreachable_code
341 | )
342 |
343 | confidently_unused = [
344 | obj for obj in unused_code if obj.confidence >= min_confidence
345 | ]
346 |
347 | return sorted(
348 | confidently_unused, key=by_size if sort_by_size else by_name
349 | )
350 |
351 | def report(
352 | self, min_confidence=0, sort_by_size=False, make_whitelist=False
353 | ):
354 | """
355 | Print ordered list of Item objects to stdout.
356 | """
357 | for item in self.get_unused_code(
358 | min_confidence=min_confidence, sort_by_size=sort_by_size
359 | ):
360 | self._log(
361 | item.get_whitelist_string()
362 | if make_whitelist
363 | else item.get_report(add_size=sort_by_size),
364 | force=True,
365 | )
366 | self.exit_code = ExitCode.DeadCode
367 | return self.exit_code
368 |
369 | @property
370 | def unused_classes(self):
371 | return _get_unused_items(self.defined_classes, self.used_names)
372 |
373 | @property
374 | def unused_funcs(self):
375 | return _get_unused_items(self.defined_funcs, self.used_names)
376 |
377 | @property
378 | def unused_imports(self):
379 | return _get_unused_items(self.defined_imports, self.used_names)
380 |
381 | @property
382 | def unused_methods(self):
383 | return _get_unused_items(self.defined_methods, self.used_names)
384 |
385 | @property
386 | def unused_props(self):
387 | return _get_unused_items(self.defined_props, self.used_names)
388 |
389 | @property
390 | def unused_vars(self):
391 | return _get_unused_items(self.defined_vars, self.used_names)
392 |
393 | @property
394 | def unused_attrs(self):
395 | return _get_unused_items(self.defined_attrs, self.used_names)
396 |
397 | def _log(self, *args, file=None, force=False):
398 | if self.verbose or force:
399 | file = file or sys.stdout
400 | try:
401 | print(*args, file=file)
402 | except UnicodeEncodeError:
403 | # Some terminals can't print Unicode symbols.
404 | x = " ".join(map(str, args))
405 | print(x.encode(), file=file)
406 |
407 | def _add_aliases(self, node):
408 | """
409 | We delegate to this method instead of using visit_alias() to have
410 | access to line numbers and to filter imports from __future__.
411 | """
412 | assert isinstance(node, (ast.Import, ast.ImportFrom))
413 | for name_and_alias in node.names:
414 | # Store only top-level module name ("os.path" -> "os").
415 | # We can't easily detect when "os.path" is used.
416 | name = name_and_alias.name.partition(".")[0]
417 | alias = name_and_alias.asname
418 | self._define(
419 | self.defined_imports,
420 | alias or name,
421 | node,
422 | confidence=90,
423 | ignore=_ignore_import,
424 | )
425 | if alias is not None:
426 | self.used_names.add(name_and_alias.name)
427 |
428 | def _define(
429 | self,
430 | collection,
431 | name,
432 | first_node,
433 | last_node=None,
434 | message="",
435 | confidence=DEFAULT_CONFIDENCE,
436 | ignore=None,
437 | ):
438 | def ignored(lineno):
439 | return (
440 | (ignore and ignore(self.filename, name))
441 | or _match(name, self.ignore_names)
442 | or noqa.ignore_line(self.noqa_lines, lineno, ERROR_CODES[typ])
443 | )
444 |
445 | last_node = last_node or first_node
446 | typ = collection.typ
447 | first_lineno = lines.get_first_line_number(first_node)
448 |
449 | if ignored(first_lineno):
450 | self._log(f'Ignoring {typ} "{name}"')
451 | else:
452 | collection.append(
453 | Item(
454 | name,
455 | typ,
456 | self.filename,
457 | first_lineno,
458 | lines.get_last_line_number(last_node),
459 | message=message,
460 | confidence=confidence,
461 | )
462 | )
463 |
464 | def _define_variable(self, name, node, confidence=DEFAULT_CONFIDENCE):
465 | self._define(
466 | self.defined_vars,
467 | name,
468 | node,
469 | confidence=confidence,
470 | ignore=_ignore_variable,
471 | )
472 |
473 | def visit_arg(self, node):
474 | """Function argument"""
475 | self._define_variable(node.arg, node, confidence=100)
476 |
477 | def visit_AsyncFunctionDef(self, node):
478 | return self.visit_FunctionDef(node)
479 |
480 | def visit_Attribute(self, node):
481 | if isinstance(node.ctx, ast.Store):
482 | self._define(self.defined_attrs, node.attr, node)
483 | elif isinstance(node.ctx, ast.Load):
484 | self.used_names.add(node.attr)
485 |
486 | def visit_BinOp(self, node):
487 | """
488 | Parse variable names in old format strings:
489 |
490 | "%(my_var)s" % locals()
491 | """
492 | if (
493 | utils.is_ast_string(node.left)
494 | and isinstance(node.op, ast.Mod)
495 | and self._is_locals_call(node.right)
496 | ):
497 | self.used_names |= set(re.findall(r"%\((\w+)\)", node.left.value))
498 |
499 | def visit_Call(self, node):
500 | # Count getattr/hasattr(x, "some_attr", ...) as usage of some_attr.
501 | if isinstance(node.func, ast.Name) and (
502 | (node.func.id == "getattr" and 2 <= len(node.args) <= 3)
503 | or (node.func.id == "hasattr" and len(node.args) == 2)
504 | ):
505 | attr_name_arg = node.args[1]
506 | if utils.is_ast_string(attr_name_arg):
507 | self.used_names.add(attr_name_arg.value)
508 |
509 | # Parse variable names in new format strings:
510 | # "{my_var}".format(**locals())
511 | if (
512 | isinstance(node.func, ast.Attribute)
513 | and utils.is_ast_string(node.func.value)
514 | and node.func.attr == "format"
515 | and any(
516 | kw.arg is None and self._is_locals_call(kw.value)
517 | for kw in node.keywords
518 | )
519 | ):
520 | self._handle_new_format_string(node.func.value.value)
521 |
522 | def _handle_new_format_string(self, s):
523 | def is_identifier(name):
524 | return bool(re.match(r"[a-zA-Z_][a-zA-Z0-9_]*", name))
525 |
526 | parser = string.Formatter()
527 | try:
528 | names = [name for _, name, _, _ in parser.parse(s) if name]
529 | except ValueError:
530 | # Invalid format string.
531 | names = []
532 |
533 | for field_name in names:
534 | # Remove brackets and their contents: "a[0][b].c[d].e" -> "a.c.e",
535 | # then split the resulting string: "a.b.c" -> ["a", "b", "c"]
536 | vars = re.sub(r"\[\w*\]", "", field_name).split(".")
537 | for var in vars:
538 | if is_identifier(var):
539 | self.used_names.add(var)
540 |
541 | @staticmethod
542 | def _is_locals_call(node):
543 | """Return True if the node is `locals()`."""
544 | return (
545 | isinstance(node, ast.Call)
546 | and isinstance(node.func, ast.Name)
547 | and node.func.id == "locals"
548 | and not node.args
549 | and not node.keywords
550 | )
551 |
552 | def visit_ClassDef(self, node):
553 | for decorator in node.decorator_list:
554 | if _match(
555 | utils.get_decorator_name(decorator), self.ignore_decorators
556 | ):
557 | self._log(
558 | f'Ignoring class "{node.name}" (decorator whitelisted)'
559 | )
560 | break
561 | else:
562 | self._define(
563 | self.defined_classes, node.name, node, ignore=_ignore_class
564 | )
565 |
566 | def visit_FunctionDef(self, node):
567 | decorator_names = [
568 | utils.get_decorator_name(decorator)
569 | for decorator in node.decorator_list
570 | ]
571 |
572 | first_arg = node.args.args[0].arg if node.args.args else None
573 |
574 | if "@property" in decorator_names:
575 | typ = "property"
576 | elif (
577 | "@staticmethod" in decorator_names
578 | or "@classmethod" in decorator_names
579 | or first_arg == "self"
580 | ):
581 | typ = "method"
582 | else:
583 | typ = "function"
584 |
585 | if any(
586 | _match(name, self.ignore_decorators) for name in decorator_names
587 | ):
588 | self._log(f'Ignoring {typ} "{node.name}" (decorator whitelisted)')
589 | elif typ == "property":
590 | self._define(self.defined_props, node.name, node)
591 | elif typ == "method":
592 | self._define(
593 | self.defined_methods, node.name, node, ignore=_ignore_method
594 | )
595 | else:
596 | self._define(
597 | self.defined_funcs, node.name, node, ignore=_ignore_function
598 | )
599 |
600 | def visit_Import(self, node):
601 | self._add_aliases(node)
602 |
603 | def visit_ImportFrom(self, node):
604 | if node.module != "__future__":
605 | self._add_aliases(node)
606 |
607 | def visit_Name(self, node):
608 | if (
609 | isinstance(node.ctx, (ast.Load, ast.Del))
610 | and node.id not in IGNORED_VARIABLE_NAMES
611 | ):
612 | self.used_names.add(node.id)
613 | elif isinstance(node.ctx, (ast.Param, ast.Store)):
614 | self._define_variable(node.id, node)
615 |
616 | def visit_Assign(self, node):
617 | if _assigns_special_variable__all__(node):
618 | assert isinstance(node.value, (ast.List, ast.Tuple))
619 | for elt in node.value.elts:
620 | if utils.is_ast_string(elt):
621 | self.used_names.add(elt.value)
622 |
623 | def visit_MatchClass(self, node):
624 | for kwd_attr in node.kwd_attrs:
625 | self.used_names.add(kwd_attr)
626 |
627 | def visit(self, node):
628 | # Visit children nodes first to allow recursive reachability analysis.
629 | self.generic_visit(node)
630 |
631 | self.reachability.visit(node)
632 |
633 | method = "visit_" + node.__class__.__name__
634 | visitor = getattr(self, method, None)
635 | if self.verbose:
636 | lineno = getattr(node, "lineno", 1)
637 | line = self.code[lineno - 1] if self.code else ""
638 | self._log(lineno, ast.dump(node), line)
639 | if visitor:
640 | visitor(node)
641 |
642 | # There isn't a clean subset of node types that might have type
643 | # comments, so just check all of them.
644 | type_comment = getattr(node, "type_comment", None)
645 | if type_comment is not None:
646 | mode = (
647 | "func_type"
648 | if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
649 | else "eval"
650 | )
651 | self.visit(
652 | ast.parse(type_comment, filename="", mode=mode)
653 | )
654 |
655 | def generic_visit(self, node):
656 | """Called if no explicit visitor function exists for a node."""
657 | for _, value in ast.iter_fields(node):
658 | if isinstance(value, list):
659 | for item in value:
660 | if isinstance(item, ast.AST):
661 | self.visit(item)
662 | elif isinstance(value, ast.AST):
663 | self.visit(value)
664 |
665 |
666 | def main():
667 | try:
668 | config = make_config()
669 | except InputError as e:
670 | print(e, file=sys.stderr)
671 | sys.exit(ExitCode.InvalidCmdlineArguments)
672 |
673 | vulture = Vulture(
674 | verbose=config["verbose"],
675 | ignore_names=config["ignore_names"],
676 | ignore_decorators=config["ignore_decorators"],
677 | )
678 | vulture.scavenge(config["paths"], exclude=config["exclude"])
679 | sys.exit(
680 | vulture.report(
681 | min_confidence=config["min_confidence"],
682 | sort_by_size=config["sort_by_size"],
683 | make_whitelist=config["make_whitelist"],
684 | )
685 | )
686 |
--------------------------------------------------------------------------------
/vulture/lines.py:
--------------------------------------------------------------------------------
1 | def get_last_line_number(node):
2 | return node.end_lineno
3 |
4 |
5 | def get_first_line_number(node):
6 | """
7 | From Python 3.8 onwards, lineno for decorated objects is the line at which
8 | the object definition starts, which is different from what Python < 3.8
9 | reported -- the lineno of the first decorator. To preserve this behaviour
10 | of Vulture for newer Python versions, which is also more accurate for
11 | counting the size of the unused code chunk (if the property is unused, we
12 | also don't need it's decorators), we return the lineno of the first
13 | decorator, if there are any.
14 | """
15 | decorators = getattr(node, "decorator_list", [])
16 | if decorators:
17 | return decorators[0].lineno
18 | return node.lineno
19 |
--------------------------------------------------------------------------------
/vulture/noqa.py:
--------------------------------------------------------------------------------
1 | import re
2 | from collections import defaultdict
3 |
4 | NOQA_REGEXP = re.compile(
5 | # Use the same regex as flake8 does.
6 | # https://github.com/pycqa/flake8/blob/main/src/flake8/defaults.py
7 | # We're looking for items that look like this:
8 | # `# noqa`
9 | # `# noqa: E123`
10 | # `# noqa: E123,W451,F921`
11 | # `# NoQA: E123,W451,F921`
12 | r"# noqa(?::[\s]?(?P([A-Z]+[0-9]+(?:[,\s]+)?)+))?",
13 | re.IGNORECASE,
14 | )
15 |
16 | NOQA_CODE_MAP = {
17 | # flake8 F401: module imported but unused.
18 | "F401": "V104",
19 | # flake8 F841: local variable is assigned to but never used.
20 | "F841": "V107",
21 | }
22 |
23 |
24 | def _parse_error_codes(match):
25 | # If no error code is specified, add the line to the "all" category.
26 | return [
27 | c.strip() for c in (match.groupdict()["codes"] or "all").split(",")
28 | ]
29 |
30 |
31 | def parse_noqa(code):
32 | noqa_lines = defaultdict(set)
33 | for lineno, line in enumerate(code, start=1):
34 | match = NOQA_REGEXP.search(line)
35 | if match:
36 | for error_code in _parse_error_codes(match):
37 | error_code = NOQA_CODE_MAP.get(error_code, error_code)
38 | noqa_lines[error_code].add(lineno)
39 | return noqa_lines
40 |
41 |
42 | def ignore_line(noqa_lines, lineno, error_code):
43 | """Check if the reported line is annotated with "# noqa"."""
44 | return lineno in noqa_lines[error_code] or lineno in noqa_lines["all"]
45 |
--------------------------------------------------------------------------------
/vulture/reachability.py:
--------------------------------------------------------------------------------
1 | import ast
2 |
3 | from vulture import utils
4 |
5 |
6 | class Reachability:
7 | def __init__(self, report):
8 | self._report = report
9 | self._no_fall_through_nodes = set()
10 |
11 | # Since we visit the children nodes first, we need to maintain a flag
12 | # that indicates if a break statement was seen. When visiting the
13 | # parent (While, For or AsyncFor), the value is checked (for While)
14 | # and reset. Assumes code is valid (break statements only in loops).
15 | self._current_loop_has_break_statement = False
16 |
17 | def visit(self, node):
18 | """When called, all children of this node have already been visited."""
19 | if isinstance(node, (ast.Break, ast.Continue, ast.Return, ast.Raise)):
20 | self._mark_as_no_fall_through(node)
21 | if isinstance(node, ast.Break):
22 | self._current_loop_has_break_statement = True
23 |
24 | elif isinstance(
25 | node,
26 | (
27 | ast.Module,
28 | ast.FunctionDef,
29 | ast.AsyncFunctionDef,
30 | ast.With,
31 | ast.AsyncWith,
32 | ),
33 | ):
34 | self._can_fall_through_statements_analysis(node.body)
35 | elif isinstance(node, ast.While):
36 | self._handle_reachability_while(node)
37 | self._current_loop_has_break_statement = False
38 | elif isinstance(node, (ast.For, ast.AsyncFor)):
39 | self._can_fall_through_statements_analysis(node.body)
40 | self._current_loop_has_break_statement = False
41 | elif isinstance(node, ast.If):
42 | self._handle_reachability_if(node)
43 | elif isinstance(node, ast.IfExp):
44 | self._handle_reachability_if_expr(node)
45 | elif isinstance(node, ast.Try):
46 | self._handle_reachability_try(node)
47 |
48 | def reset(self):
49 | self._no_fall_through_nodes = set()
50 |
51 | def _can_fall_through(self, node):
52 | return node not in self._no_fall_through_nodes
53 |
54 | def _mark_as_no_fall_through(self, node):
55 | self._no_fall_through_nodes.add(node)
56 |
57 | def _can_fall_through_statements_analysis(self, statements):
58 | """Report unreachable statements.
59 | Return True if we can execute the full list of statements.
60 | """
61 | for idx, statement in enumerate(statements):
62 | if not self._can_fall_through(statement):
63 | try:
64 | next_sibling = statements[idx + 1]
65 | except IndexError:
66 | next_sibling = None
67 | if next_sibling is not None:
68 | class_name = statement.__class__.__name__.lower()
69 | self._report(
70 | name=class_name,
71 | first_node=next_sibling,
72 | last_node=statements[-1],
73 | message=f"unreachable code after '{class_name}'",
74 | )
75 | return False
76 | return True
77 |
78 | def _handle_reachability_if(self, node):
79 | has_else = bool(node.orelse)
80 |
81 | if utils.condition_is_always_false(node.test):
82 | self._report(
83 | name="if",
84 | first_node=node,
85 | last_node=node.body
86 | if isinstance(node, ast.IfExp)
87 | else node.body[-1],
88 | message="unsatisfiable 'if' condition",
89 | )
90 | if_can_fall_through = True
91 | else_can_fall_through = self._can_else_fall_through(
92 | node.orelse, condition_always_true=False
93 | )
94 |
95 | elif utils.condition_is_always_true(node.test):
96 | if_can_fall_through = self._can_fall_through_statements_analysis(
97 | node.body
98 | )
99 | else_can_fall_through = self._can_else_fall_through(
100 | node.orelse, condition_always_true=True
101 | )
102 |
103 | if has_else:
104 | self._report(
105 | name="else",
106 | first_node=node.orelse[0],
107 | last_node=node.orelse[-1],
108 | message="unreachable 'else' block",
109 | )
110 | else:
111 | # Redundant if-condition without else block.
112 | self._report(
113 | name="if",
114 | first_node=node,
115 | message="redundant if-condition",
116 | )
117 | else:
118 | if_can_fall_through = self._can_fall_through_statements_analysis(
119 | node.body
120 | )
121 | else_can_fall_through = self._can_else_fall_through(
122 | node.orelse, condition_always_true=False
123 | )
124 |
125 | statement_can_fall_through = (
126 | if_can_fall_through or else_can_fall_through
127 | )
128 |
129 | if not statement_can_fall_through:
130 | self._mark_as_no_fall_through(node)
131 |
132 | def _can_else_fall_through(self, orelse, condition_always_true):
133 | if not orelse:
134 | return not condition_always_true
135 | return self._can_fall_through_statements_analysis(orelse)
136 |
137 | def _handle_reachability_if_expr(self, node):
138 | if utils.condition_is_always_false(node.test):
139 | self._report(
140 | name="ternary",
141 | first_node=node,
142 | last_node=node.body
143 | if isinstance(node, ast.IfExp)
144 | else node.body[-1],
145 | message="unsatisfiable 'ternary' condition",
146 | )
147 | elif utils.condition_is_always_true(node.test):
148 | else_body = node.orelse
149 | self._report(
150 | name="ternary",
151 | first_node=else_body,
152 | message="unreachable 'else' expression",
153 | )
154 |
155 | def _handle_reachability_while(self, node):
156 | if utils.condition_is_always_false(node.test):
157 | self._report(
158 | name="while",
159 | first_node=node,
160 | last_node=node.body
161 | if isinstance(node, ast.IfExp)
162 | else node.body[-1],
163 | message="unsatisfiable 'while' condition",
164 | )
165 |
166 | elif utils.condition_is_always_true(node.test):
167 | else_body = node.orelse
168 | if else_body:
169 | self._report(
170 | name="else",
171 | first_node=else_body[0],
172 | last_node=else_body[-1],
173 | message="unreachable 'else' block",
174 | )
175 |
176 | if not self._current_loop_has_break_statement:
177 | self._mark_as_no_fall_through(node)
178 |
179 | self._can_fall_through_statements_analysis(node.body)
180 |
181 | def _handle_reachability_try(self, node):
182 | try_can_fall_through = self._can_fall_through_statements_analysis(
183 | node.body
184 | )
185 |
186 | has_else = bool(node.orelse)
187 |
188 | if not try_can_fall_through and has_else:
189 | else_body = node.orelse
190 | self._report(
191 | name="else",
192 | first_node=else_body[0],
193 | last_node=else_body[-1],
194 | message="unreachable 'else' block",
195 | )
196 |
197 | any_except_can_fall_through = any(
198 | self._can_fall_through_statements_analysis(handler.body)
199 | for handler in node.handlers
200 | )
201 |
202 | statement_can_fall_through = (
203 | try_can_fall_through or any_except_can_fall_through
204 | )
205 |
206 | if not statement_can_fall_through:
207 | self._mark_as_no_fall_through(node)
208 |
--------------------------------------------------------------------------------
/vulture/utils.py:
--------------------------------------------------------------------------------
1 | import ast
2 | import pathlib
3 | import sys
4 | import tokenize
5 | from enum import IntEnum
6 |
7 |
8 | class VultureInputException(Exception):
9 | pass
10 |
11 |
12 | class ExitCode(IntEnum):
13 | NoDeadCode = 0
14 | InvalidInput = 1
15 | InvalidCmdlineArguments = 2
16 | DeadCode = 3
17 |
18 |
19 | def _safe_eval(node, default):
20 | """
21 | Safely evaluate the Boolean expression under the given AST node.
22 |
23 | Substitute `default` for all sub-expressions that cannot be
24 | evaluated (because variables or functions are undefined).
25 |
26 | We could use eval() to evaluate more sub-expressions. However, this
27 | function is not safe for arbitrary Python code. Even after
28 | overwriting the "__builtins__" dictionary, the original dictionary
29 | can be restored
30 | (https://nedbatchelder.com/blog/201206/eval_really_is_dangerous.html).
31 |
32 | """
33 | if isinstance(node, ast.BoolOp):
34 | results = [_safe_eval(value, default) for value in node.values]
35 | if isinstance(node.op, ast.And):
36 | return all(results)
37 | else:
38 | return any(results)
39 | elif isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.Not):
40 | return not _safe_eval(node.operand, not default)
41 | else:
42 | try:
43 | return ast.literal_eval(node)
44 | except ValueError:
45 | return default
46 |
47 |
48 | def condition_is_always_false(condition):
49 | return not _safe_eval(condition, True)
50 |
51 |
52 | def condition_is_always_true(condition):
53 | return _safe_eval(condition, False)
54 |
55 |
56 | def is_ast_string(node):
57 | return isinstance(node, ast.Constant) and isinstance(node.value, str)
58 |
59 |
60 | def format_path(path):
61 | try:
62 | return path.relative_to(pathlib.Path.cwd())
63 | except ValueError:
64 | # Path is not below the current directory.
65 | return path
66 |
67 |
68 | def get_decorator_name(decorator):
69 | if isinstance(decorator, ast.Call):
70 | decorator = decorator.func
71 | try:
72 | parts = []
73 | while isinstance(decorator, ast.Attribute):
74 | parts.append(decorator.attr)
75 | decorator = decorator.value
76 | parts.append(decorator.id)
77 | except AttributeError:
78 | parts = []
79 | return "@" + ".".join(reversed(parts))
80 |
81 |
82 | def get_modules(paths):
83 | """Retrieve Python files to check.
84 |
85 | Loop over all given paths, abort if any ends with .pyc, add the other given
86 | files (even those not ending with .py) and collect all .py files under the
87 | given directories.
88 |
89 | """
90 | modules = []
91 | for path in paths:
92 | path = path.resolve()
93 | if path.is_file():
94 | if path.suffix == ".pyc":
95 | sys.exit(f"Error: *.pyc files are not supported: {path}")
96 | else:
97 | modules.append(path)
98 | elif path.is_dir():
99 | modules.extend(path.rglob("*.py"))
100 | else:
101 | sys.exit(f"Error: {path} could not be found.")
102 | return modules
103 |
104 |
105 | def read_file(filename):
106 | try:
107 | # Use encoding detected by tokenize.detect_encoding().
108 | with tokenize.open(filename) as f:
109 | return f.read()
110 | except (SyntaxError, UnicodeDecodeError) as err:
111 | raise VultureInputException from err
112 |
113 |
114 | class LoggingList(list):
115 | def __init__(self, typ, verbose):
116 | self.typ = typ
117 | self._verbose = verbose
118 | return super().__init__()
119 |
120 | def append(self, item):
121 | if self._verbose:
122 | print(f'define {self.typ} "{item.name}"')
123 | super().append(item)
124 |
125 |
126 | class LoggingSet(set):
127 | def __init__(self, typ, verbose):
128 | self.typ = typ
129 | self._verbose = verbose
130 | return super().__init__()
131 |
132 | def add(self, name):
133 | if self._verbose:
134 | print(f'use {self.typ} "{name}"')
135 | super().add(name)
136 |
--------------------------------------------------------------------------------
/vulture/version.py:
--------------------------------------------------------------------------------
1 | __version__ = "2.14"
2 |
--------------------------------------------------------------------------------
/vulture/whitelists/argparse_whitelist.py:
--------------------------------------------------------------------------------
1 | import argparse
2 |
3 | argparse.ArgumentParser().epilog
4 |
5 | argparse.ArgumentDefaultsHelpFormatter("prog")._fill_text
6 | argparse.ArgumentDefaultsHelpFormatter("prog")._get_help_string
7 |
--------------------------------------------------------------------------------
/vulture/whitelists/ast_whitelist.py:
--------------------------------------------------------------------------------
1 | from whitelist_utils import Whitelist
2 |
3 | # NodeVisitor methods are called implicitly.
4 | whitelist_node_visitor = Whitelist()
5 |
6 | whitelist_node_visitor.visit_Assert
7 | whitelist_node_visitor.visit_Assign
8 | whitelist_node_visitor.visit_AsyncFor
9 | whitelist_node_visitor.visit_AsyncFunctionDef
10 | whitelist_node_visitor.visit_AsyncWith
11 | whitelist_node_visitor.visit_Attribute
12 | whitelist_node_visitor.visit_AugAssign
13 | whitelist_node_visitor.visit_Await
14 | whitelist_node_visitor.visit_BinOp
15 | whitelist_node_visitor.visit_BoolOp
16 | whitelist_node_visitor.visit_Bytes
17 | whitelist_node_visitor.visit_Call
18 | whitelist_node_visitor.visit_ClassDef
19 | whitelist_node_visitor.visit_Compare
20 | whitelist_node_visitor.visit_Constant
21 | whitelist_node_visitor.visit_Delete
22 | whitelist_node_visitor.visit_Dict
23 | whitelist_node_visitor.visit_DictComp
24 | whitelist_node_visitor.visit_ExceptHandler
25 | whitelist_node_visitor.visit_Exec
26 | whitelist_node_visitor.visit_Expr
27 | whitelist_node_visitor.visit_Expression
28 | whitelist_node_visitor.visit_ExtSlice
29 | whitelist_node_visitor.visit_For
30 | whitelist_node_visitor.visit_FunctionDef
31 | whitelist_node_visitor.visit_GeneratorExp
32 | whitelist_node_visitor.visit_Global
33 | whitelist_node_visitor.visit_If
34 | whitelist_node_visitor.visit_IfExp
35 | whitelist_node_visitor.visit_Import
36 | whitelist_node_visitor.visit_ImportFrom
37 | whitelist_node_visitor.visit_Index
38 | whitelist_node_visitor.visit_Interactive
39 | whitelist_node_visitor.visit_Lambda
40 | whitelist_node_visitor.visit_List
41 | whitelist_node_visitor.visit_ListComp
42 | whitelist_node_visitor.visit_MatchClass
43 | whitelist_node_visitor.visit_Module
44 | whitelist_node_visitor.visit_Name
45 | whitelist_node_visitor.visit_NameConstant
46 | whitelist_node_visitor.visit_Nonlocal
47 | whitelist_node_visitor.visit_Num
48 | whitelist_node_visitor.visit_Print
49 | whitelist_node_visitor.visit_Raise
50 | whitelist_node_visitor.visit_Repr
51 | whitelist_node_visitor.visit_Return
52 | whitelist_node_visitor.visit_Set
53 | whitelist_node_visitor.visit_SetComp
54 | whitelist_node_visitor.visit_Slice
55 | whitelist_node_visitor.visit_Starred
56 | whitelist_node_visitor.visit_Str
57 | whitelist_node_visitor.visit_Subscript
58 | whitelist_node_visitor.visit_Suite
59 | whitelist_node_visitor.visit_Try
60 | whitelist_node_visitor.visit_TryExcept
61 | whitelist_node_visitor.visit_TryFinally
62 | whitelist_node_visitor.visit_Tuple
63 | whitelist_node_visitor.visit_UnaryOp
64 | whitelist_node_visitor.visit_While
65 | whitelist_node_visitor.visit_With
66 | whitelist_node_visitor.visit_Yield
67 | whitelist_node_visitor.visit_YieldFrom
68 | whitelist_node_visitor.visit_alias
69 | whitelist_node_visitor.visit_arg
70 | whitelist_node_visitor.visit_arguments
71 | whitelist_node_visitor.visit_comprehension
72 | whitelist_node_visitor.visit_keyword
73 |
--------------------------------------------------------------------------------
/vulture/whitelists/collections_whitelist.py:
--------------------------------------------------------------------------------
1 | import collections
2 |
3 | # To free memory, the "default_factory" attribute can be set to None.
4 | collections.defaultdict().default_factory
5 |
--------------------------------------------------------------------------------
/vulture/whitelists/ctypes_whitelist.py:
--------------------------------------------------------------------------------
1 | from ctypes import _CFuncPtr, _Pointer
2 |
3 | _CFuncPtr.argtypes
4 | _CFuncPtr.errcheck
5 | _CFuncPtr.restype
6 |
7 | _Pointer.contents
8 |
--------------------------------------------------------------------------------
/vulture/whitelists/enum_whitelist.py:
--------------------------------------------------------------------------------
1 | import enum
2 |
3 |
4 | class EnumWhitelist(enum.Enum):
5 | EnumWhitelist = 1
6 |
7 |
8 | # Special attributes used by enum classes.
9 | EnumWhitelist.EnumWhitelist._name_
10 | EnumWhitelist.EnumWhitelist._value_
11 |
--------------------------------------------------------------------------------
/vulture/whitelists/logging_whitelist.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | logging.Filter.filter
4 | logging.getLogger().propagate
5 | logging.StreamHandler.emit
6 |
--------------------------------------------------------------------------------
/vulture/whitelists/pint_whitelist.py:
--------------------------------------------------------------------------------
1 | import pint
2 |
3 | ureg = pint.UnitRegistry()
4 | ureg.default_format
5 |
--------------------------------------------------------------------------------
/vulture/whitelists/socketserver_whitelist.py:
--------------------------------------------------------------------------------
1 | import socketserver
2 |
3 | socketserver.TCPServer.allow_reuse_address
4 |
--------------------------------------------------------------------------------
/vulture/whitelists/string_whitelist.py:
--------------------------------------------------------------------------------
1 | import string
2 |
3 | string.Formatter.check_unused_args
4 | string.Formatter.convert_field
5 | string.Formatter.format
6 | string.Formatter.format_field
7 | string.Formatter.get_field
8 | string.Formatter.get_value
9 | string.Formatter.parse
10 | string.Formatter.vformat
11 |
--------------------------------------------------------------------------------
/vulture/whitelists/sys_whitelist.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | sys.excepthook
4 |
5 | # Never report redirected streams as unused.
6 | sys.stderr
7 | sys.stdin
8 | sys.stdout
9 |
--------------------------------------------------------------------------------
/vulture/whitelists/threading_whitelist.py:
--------------------------------------------------------------------------------
1 | import threading
2 |
3 | threading.Thread.daemon
4 | threading.Thread.name
5 | threading.Thread.run
6 |
--------------------------------------------------------------------------------
/vulture/whitelists/unittest_whitelist.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase, mock
2 |
3 | TestCase.setUp
4 | TestCase.tearDown
5 | TestCase.setUpClass
6 | TestCase.tearDownClass
7 | TestCase.run
8 | TestCase.skipTest
9 | TestCase.debug
10 | TestCase.failureException
11 | TestCase.longMessage
12 | TestCase.maxDiff
13 | TestCase.subTest
14 |
15 | mock.Mock.return_value
16 | mock.Mock.side_effect
17 |
18 | mock.MagicMock.return_value
19 | mock.MagicMock.side_effect
20 |
--------------------------------------------------------------------------------
/vulture/whitelists/whitelist_utils.py:
--------------------------------------------------------------------------------
1 | """
2 |
3 | Vulture sometimes reports used code as unused. To avoid these
4 | false-positives, you can write a Python file that explicitly uses the
5 | code and pass it to vulture:
6 |
7 | vulture myscript.py mydir mywhitelist.py
8 |
9 | When creating a whitelist file, you have to make sure not to write code
10 | that hides unused code in other files. E.g., this is why we don't import
11 | and access the "sys" module below. If we did import it, vulture would
12 | not be able to detect whether other files import "sys" without using it.
13 |
14 | This file explicitly uses code from the Python standard library that is
15 | often incorrectly detected as unused.
16 |
17 | """
18 |
19 |
20 | class Whitelist:
21 | """
22 | Helper class that allows mocking Python objects.
23 |
24 | Use it to create whitelist files that are not only syntactically
25 | correct, but can also be executed.
26 |
27 | """
28 |
29 | def __getattr__(self, _):
30 | pass
31 |
32 |
33 | assert Whitelist
34 |
--------------------------------------------------------------------------------