diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ac5494b..5b38281c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# 2.9 (unreleased) +* Use exit code 3 when dead code is found (whosayn, #319). + # 2.8 (2023-08-10) * Add `UnicodeEncodeError` exception handling to `core.py` (milanbalazs, #299). diff --git a/README.md b/README.md index f096b2f9..4c892e72 100644 --- a/README.md +++ b/README.md @@ -316,9 +316,9 @@ codes. | Exit code | Description | | --------- | ------------------------------------------------------------- | | 0 | No dead code found | -| 1 | Dead code found | | 1 | Invalid input (file missing, syntax error, wrong encoding) | | 2 | Invalid command line arguments | +| 3 | Dead code found | ## Similar programs diff --git a/tests/test_encoding.py b/tests/test_encoding.py index 2fad5349..560a6e5a 100644 --- a/tests/test_encoding.py +++ b/tests/test_encoding.py @@ -1,6 +1,7 @@ import codecs from . import v +from vulture.utils import ExitCode assert v # Silence pyflakes. @@ -12,7 +13,7 @@ def test_encoding1(v): pass """ ) - assert not v.found_dead_code_or_error + assert v.exit_code == ExitCode.NoDeadCode def test_encoding2(v): @@ -23,7 +24,7 @@ def test_encoding2(v): pass """ ) - assert not v.found_dead_code_or_error + assert v.exit_code == ExitCode.NoDeadCode def test_non_utf8_encoding(v, tmp_path): @@ -34,7 +35,7 @@ def test_non_utf8_encoding(v, tmp_path): f.write(codecs.BOM_UTF16_LE) f.write(code.encode("utf_16_le")) v.scavenge([non_utf_8_file]) - assert v.found_dead_code_or_error + assert v.exit_code == ExitCode.InvalidInput def test_utf8_with_bom(v, tmp_path): @@ -43,4 +44,4 @@ def test_utf8_with_bom(v, tmp_path): # utf8_sig prepends the BOM to the file. filepath.write_text("", encoding="utf-8-sig") v.scavenge([filepath]) - assert not v.found_dead_code_or_error + assert v.exit_code == ExitCode.NoDeadCode diff --git a/tests/test_errors.py b/tests/test_errors.py index 6d53e25b..87feff43 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -1,18 +1,19 @@ import pytest -from . import v +from . import v, call_vulture +from vulture.utils import ExitCode assert v # Silence pyflakes. def test_syntax_error(v): v.scan("foo bar") - assert int(v.report()) == 1 + assert int(v.report()) == ExitCode.InvalidInput def test_null_byte(v): v.scan("\x00") - assert int(v.report()) == 1 + assert int(v.report()) == ExitCode.InvalidInput def test_confidence_range(v): @@ -24,3 +25,10 @@ def foo(): ) with pytest.raises(ValueError): v.get_unused_code(min_confidence=150) + + +def test_invalid_cmdline_args(): + assert ( + call_vulture(["vulture/", "--invalid-argument"]) + == ExitCode.InvalidCmdlineArguments + ) diff --git a/tests/test_scavenging.py b/tests/test_scavenging.py index 27154cd6..c0ccbbf6 100644 --- a/tests/test_scavenging.py +++ b/tests/test_scavenging.py @@ -3,6 +3,7 @@ import pytest from . import check, v +from vulture.utils import ExitCode assert v # Silence pyflakes. @@ -765,7 +766,7 @@ def __init__(self): check(v.unused_imports, ["Any", "Dict", "List", "Text", "Tuple"]) else: check(v.unused_imports, []) - assert not v.found_dead_code_or_error + assert v.exit_code == ExitCode.NoDeadCode def test_invalid_type_comment(v): @@ -779,9 +780,9 @@ def bad(): ) if sys.version_info < (3, 8): - assert not v.found_dead_code_or_error + assert v.exit_code == ExitCode.NoDeadCode else: - assert v.found_dead_code_or_error + assert v.exit_code == ExitCode.InvalidInput def test_unused_args_with_del(v): diff --git a/tests/test_script.py b/tests/test_script.py index b57b5d02..62c36e90 100644 --- a/tests/test_script.py +++ b/tests/test_script.py @@ -4,31 +4,38 @@ import sys from . import call_vulture, REPO, WHITELISTS +from vulture.utils import ExitCode def test_module_with_explicit_whitelists(): - assert call_vulture(["vulture/"] + WHITELISTS) == 0 + assert call_vulture(["vulture/"] + WHITELISTS) == ExitCode.NoDeadCode def test_module_with_implicit_whitelists(): - assert call_vulture(["vulture/"]) == 0 + assert call_vulture(["vulture/"]) == ExitCode.NoDeadCode def test_module_without_whitelists(): - assert call_vulture(["vulture/", "--exclude", "whitelists"]) == 1 + assert ( + call_vulture(["vulture/", "--exclude", "whitelists"]) + == ExitCode.DeadCode + ) def test_missing_file(): - assert call_vulture(["missing.py"]) == 1 + assert call_vulture(["missing.py"]) == ExitCode.InvalidInput def test_tests(): - assert call_vulture(["tests/"]) == 0 + assert call_vulture(["tests/"]) == ExitCode.NoDeadCode def test_whitelists_with_python(): for whitelist in WHITELISTS: - assert subprocess.call([sys.executable, whitelist], cwd=REPO) == 0 + assert ( + subprocess.call([sys.executable, whitelist], cwd=REPO) + == ExitCode.NoDeadCode + ) def test_pyc(): @@ -36,7 +43,10 @@ def test_pyc(): def test_sort_by_size(): - assert call_vulture(["vulture/utils.py", "--sort-by-size"]) == 1 + assert ( + call_vulture(["vulture/utils.py", "--sort-by-size"]) + == ExitCode.DeadCode + ) def test_min_confidence(): @@ -50,7 +60,7 @@ def test_min_confidence(): "100", ] ) - == 0 + == ExitCode.NoDeadCode ) @@ -61,8 +71,14 @@ def get_csv(paths): def call_vulture_with_excludes(excludes): return call_vulture(["vulture/", "--exclude", get_csv(excludes)]) - assert call_vulture_with_excludes(["core.py", "utils.py"]) == 1 - assert call_vulture_with_excludes(glob.glob("vulture/*.py")) == 0 + assert ( + call_vulture_with_excludes(["core.py", "utils.py"]) + == ExitCode.DeadCode + ) + assert ( + call_vulture_with_excludes(glob.glob("vulture/*.py")) + == ExitCode.NoDeadCode + ) def test_make_whitelist(): @@ -70,6 +86,8 @@ def test_make_whitelist(): call_vulture( ["vulture/", "--make-whitelist", "--exclude", "whitelists"] ) - == 1 + == ExitCode.DeadCode + ) + assert ( + call_vulture(["vulture/", "--make-whitelist"]) == ExitCode.NoDeadCode ) - assert call_vulture(["vulture/", "--make-whitelist"]) == 0 diff --git a/vulture/config.py b/vulture/config.py index f382276d..3576af5c 100644 --- a/vulture/config.py +++ b/vulture/config.py @@ -9,6 +9,7 @@ import toml from .version import __version__ +from vulture.utils import ExitCode #: Possible configuration options and their respective defaults DEFAULTS = { @@ -192,7 +193,10 @@ def make_config(argv=None, tomlfile=None): else: config = {} - cli_config = _parse_args(argv) + try: + cli_config = _parse_args(argv) + except SystemExit as e: + raise SystemExit(ExitCode.InvalidCmdlineArguments) from e # Overwrite TOML options with CLI options, if given. config.update(cli_config) diff --git a/vulture/core.py b/vulture/core.py index 16c71948..04166358 100644 --- a/vulture/core.py +++ b/vulture/core.py @@ -10,6 +10,7 @@ from vulture import noqa from vulture import utils from vulture.config import make_config +from vulture.utils import ExitCode DEFAULT_CONFIDENCE = 60 @@ -217,7 +218,7 @@ def get_list(typ): self.filename = Path() self.code = [] - self.found_dead_code_or_error = False + self.exit_code = ExitCode.NoDeadCode def scan(self, code, filename=""): filename = Path(filename) @@ -232,7 +233,7 @@ def handle_syntax_error(e): file=sys.stderr, force=True, ) - self.found_dead_code_or_error = True + self.exit_code = ExitCode.InvalidInput try: node = ( @@ -251,7 +252,7 @@ def handle_syntax_error(e): file=sys.stderr, force=True, ) - self.found_dead_code_or_error = True + self.exit_code = ExitCode.InvalidInput else: # When parsing type comments, visiting can throw SyntaxError. try: @@ -287,7 +288,7 @@ def exclude_path(path): file=sys.stderr, force=True, ) - self.found_dead_code_or_error = True + self.exit_code = ExitCode.InvalidInput else: self.scan(module_string, filename=module) @@ -353,8 +354,8 @@ def report( else item.get_report(add_size=sort_by_size), force=True, ) - self.found_dead_code_or_error = True - return self.found_dead_code_or_error + self.exit_code = ExitCode.DeadCode + return self.exit_code @property def unused_classes(self): diff --git a/vulture/utils.py b/vulture/utils.py index 7bed9f93..2578d868 100644 --- a/vulture/utils.py +++ b/vulture/utils.py @@ -1,4 +1,5 @@ import ast +from enum import IntEnum import pathlib import sys import tokenize @@ -8,6 +9,13 @@ class VultureInputException(Exception): pass +class ExitCode(IntEnum): + NoDeadCode = 0 + InvalidInput = 1 + InvalidCmdlineArguments = 2 + DeadCode = 3 + + def _safe_eval(node, default): """ Safely evaluate the Boolean expression under the given AST node.