Skip to content

Commit

Permalink
Ignore results annotated with '# noqa' (#195)
Browse files Browse the repository at this point in the history
* Ignore results annotated with '# noqa'

* Report issue codes
  • Loading branch information
RJ722 authored Mar 31, 2020
1 parent c710f84 commit 2196537
Show file tree
Hide file tree
Showing 9 changed files with 482 additions and 19 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# Unreleased

* Report issue codes in output (e.g., `code.py:1: V104 unused import ...`)
(RJ722, #195)
* Support `# noqa` comments to suppress results on that line. (RJ722, #195).

# 1.4 (2020-03-30)

* Ignore unused import statements in `__init__.py` (RJ722, #192).
Expand Down
44 changes: 39 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ tool for higher code quality.
* tested: tests itself and has complete test coverage
* complements pyflakes and has the same output syntax
* sorts unused classes and functions by size with `--sort-by-size`
* respects `# noqa` comments
* supports Python 2.7 and Python \>= 3.5

## Installation
Expand Down Expand Up @@ -58,6 +59,17 @@ We collect whitelists for common Python modules and packages in
a whole file or directory, use the `--exclude` parameter (e.g.,
`--exclude *settings.py,docs/`).

Another way of ignoring errors is to annotate the line causing the false
positive with `# noqa: <ERROR_CODE>` in a trailing comment (e.g.,
`# noqa: V103`).

The `ERROR_CODE` specifies what kind of dead code to ignore (see the table
below for the list of error codes). In case no error code is specified,
Vulture ignores all results for the line.

Note that the line number for any decorated object is the same as the line
number of the first decorator.

**Ignoring names**

You can use `--ignore-names foo*,ba[rz]` to let Vulture ignore all names
Expand Down Expand Up @@ -130,9 +142,9 @@ Calling :

results in the following output:

dead_code.py:1: unused import 'os' (90% confidence)
dead_code.py:4: unused function 'greet' (60% confidence)
dead_code.py:8: unused variable 'message' (60% confidence)
dead_code.py:1: V104 unused import 'os' (90% confidence)
dead_code.py:4: V103 unused function 'greet' (60% confidence)
dead_code.py:8: V106 unused variable 'message' (60% confidence)

Vulture correctly reports "os" and "message" as unused, but it fails to
detect that "greet" is actually used. The recommended method to deal
Expand All @@ -158,8 +170,30 @@ Passing both the original program and the whitelist to Vulture

makes Vulture ignore the `greet` method:

dead_code.py:1: unused import 'os' (90% confidence)
dead_code.py:8: unused variable 'message' (60% confidence)
dead_code.py:1: V104 unused import 'os' (90% confidence)
dead_code.py:8: V106 unused variable 'message' (60% confidence)

**Using "# noqa"**

```python
import os # noqa

class Greeter: # noqa: 102
def greet(self): # noqa: V103
print("Hi")
```

## Error codes

| Error code | Description |
| ---------- | ----------------- |
| V101 | Unused attribute |
| V102 | Unused class |
| V103 | Unused function |
| V104 | Unused import |
| V105 | Unused property |
| V106 | Unused variable |
| V201 | Unreachable code |

## Exit codes

Expand Down
292 changes: 292 additions & 0 deletions tests/test_noqa.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,292 @@
import pytest

from vulture.noqa import NOQA_REGEXP, _parse_error_codes
from . import check, v

assert v # Silence pyflakes.


@pytest.mark.parametrize(
"line, codes",
[
("# noqa", ["all"]),
("## noqa", ["all"]),
("# noqa Hi, go on.", ["all"]),
("# noqa: V101", ["V101"]),
("# noqa: V101, V106", ["V101", "V106"]),
("# NoQA: V101, V103, \t V104", ["V101", "V103", "V104"]),
],
)
def test_noqa_regex_present(line, codes):
match = NOQA_REGEXP.search(line)
parsed = _parse_error_codes(match)
assert parsed == codes


@pytest.mark.parametrize(
"line",
[
("# noqa: 123V"),
("# noqa explanation: V012"),
("# noqa: ,V101"),
("# noqa: #noqa: V102"),
("# noqa: # noqa: V102"),
],
)
def test_noqa_regex_no_groups(line):
assert NOQA_REGEXP.search(line).groupdict()["codes"] is None


@pytest.mark.parametrize(
"line",
[("#noqa"), ("##noqa"), ("# n o q a"), ("#NOQA"), ("# Hello, noqa")],
)
def test_noqa_regex_not_present(line):
assert not NOQA_REGEXP.search(line)


def test_noqa_without_codes(v):
v.scan(
"""\
import this # noqa
@underground # noqa
class Cellar:
@property # noqa
def wine(self):
grapes = True # noqa
@without_ice # noqa
def serve(self, quantity=50):
self.quantity_served = quantity # noqa
return
self.pour() # noqa
"""
)
check(v.unused_attrs, [])
check(v.unused_classes, [])
check(v.unused_funcs, [])
check(v.unused_imports, [])
check(v.unused_props, [])
check(v.unreachable_code, [])
check(v.unused_vars, [])


def test_noqa_specific_issue_codes(v):
v.scan(
"""\
import this # noqa: V104
@underground # noqa: V102
class Cellar:
@property # noqa: V105
def wine(self):
grapes = True # noqa: V106
@without_ice # noqa: V103
def serve(self, quantity=50):
self.quantity_served = quantity # noqa: V101
return
self.pour() # noqa: V201
"""
)
check(v.unused_attrs, [])
check(v.unused_classes, [])
check(v.unused_funcs, [])
check(v.unused_imports, [])
check(v.unused_props, [])
check(v.unreachable_code, [])
check(v.unused_vars, [])


def test_noqa_attributes(v):
v.scan(
"""\
something.x = 'x' # noqa: V101
something.z = 'z' # noqa: V106 (code for unused variable)
something.u = 'u' # noqa
"""
)
check(v.unused_attrs, ["z"])


def test_noqa_classes(v):
v.scan(
"""\
class QtWidget: # noqa: V102
pass
class ABC(QtWidget):
pass # noqa: V102 (should not ignore)
class DEF: # noqa
pass
"""
)
check(v.unused_classes, ["ABC"])


def test_noqa_functions(v):
v.scan(
"""\
def play(tune, instrument='bongs', _hz='50'): # noqa: V103
pass
# noqa
def problems(): # noqa: V104
pass # noqa: V103
def hello(name): # noqa
print("Hello")
"""
)
check(v.unused_funcs, ["problems"])
check(v.unused_vars, ["instrument", "tune"])


def test_noqa_imports(v):
v.scan(
"""\
import foo
import this # noqa: V104
import zoo
from koo import boo # noqa
from me import *
import dis # noqa: V101 (code for unused attr)
"""
)
check(v.unused_imports, ["foo", "zoo", "dis"])


def test_noqa_properties(v):
v.scan(
"""\
class Zoo:
@property
def no_of_koalas(self): # noqa
pass
@property
def area(self, width, depth): # noqa: V105
pass
@property # noqa
def entry_gates(self):
pass
@property # noqa: V103 (code for unused function)
def tickets(self):
pass
"""
)
check(v.unused_props, ["no_of_koalas", "area", "tickets"])
check(v.unused_classes, ["Zoo"])
check(v.unused_vars, ["width", "depth"])


def test_noqa_multiple_decorators(v):
v.scan(
"""\
@bar # noqa: V102
class Foo:
@property # noqa: V105
@make_it_cool
@log
def something(self):
pass
@coolify
@property
def something_else(self): # noqa: V105
pass
@a
@property
@b # noqa
def abcd(self):
pass
"""
)
check(v.unused_props, ["something_else", "abcd"])
check(v.unused_classes, [])


def test_noqa_unreacahble_code(v):
v.scan(
"""\
def shave_sheep(sheep):
for a_sheep in sheep:
if a_sheep.is_bald:
continue
a_sheep.grow_hair() # noqa: V201
a_sheep.shave()
return
for a_sheep in sheep: # noqa: V201
if a_sheep.still_has_hair:
a_sheep.shave_again()
"""
)
check(v.unreachable_code, [])
check(v.unused_funcs, ["shave_sheep"])


def test_noqa_variables(v):
v.scan(
"""\
mitsi = "Mother" # noqa: V106
harry = "Father" # noqa
shero = "doggy" # noqa: V101, V104 (code for unused import, attr)
shinchan.friend = ['masao'] # noqa: V106 (code for unused variable)
"""
)
check(v.unused_vars, ["shero"])
check(v.unused_attrs, ["friend"])


def test_noqa_with_multiple_issue_codes(v):
v.scan(
"""\
def world(axis): # noqa: V103, V201
pass
for _ in range(3):
continue
xyz = hello(something, else): # noqa: V201, V106
"""
)
check(v.get_unused_code(), [])


def test_noqa_on_empty_line(v):
v.scan(
"""\
# noqa
import this
# noqa
"""
)
check(v.unused_imports, ["this"])


def test_noqa_with_invalid_codes(v):
v.scan(
"""\
import this # V098, A123, F876
"""
)
check(v.unused_imports, ["this"])


@pytest.mark.parametrize(
"first_file, second_file",
[
("foo = None", "bar = None # noqa"),
("bar = None # noqa", "foo = None"),
],
)
def test_noqa_multiple_files(first_file, second_file, v):
v.scan(first_file, filename="first_file.py")
v.scan(second_file, filename="second_file.py")
check(v.unused_vars, ["foo"])
14 changes: 7 additions & 7 deletions tests/test_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,13 @@ def test_report(code, expected, make_whitelist=False):

def test_item_report(check_report):
expected = """\
{filename}:1: unused import 'foo' (90% confidence)
{filename}:3: unused class 'Foo' (60% confidence)
{filename}:7: unused function 'bar' (60% confidence)
{filename}:8: unused attribute 'foobar' (60% confidence)
{filename}:9: unused variable 'foobar' (60% confidence)
{filename}:11: unreachable code after 'return' (100% confidence)
{filename}:13: unused property 'myprop' (60% confidence)
{filename}:1: V104 unused import 'foo' (90% confidence)
{filename}:3: V102 unused class 'Foo' (60% confidence)
{filename}:7: V103 unused function 'bar' (60% confidence)
{filename}:8: V101 unused attribute 'foobar' (60% confidence)
{filename}:9: V106 unused variable 'foobar' (60% confidence)
{filename}:11: V201 unreachable code after 'return' (100% confidence)
{filename}:13: V105 unused property 'myprop' (60% confidence)
"""
check_report(mock_code, expected)

Expand Down
Loading

0 comments on commit 2196537

Please sign in to comment.