deadcode 2.5.0__py3-none-any.whl → 2.6.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- .venv310/bin/activate_this.py +59 -0
- .venv311/bin/activate_this.py +59 -0
- .venv314/bin/activate_this.py +59 -0
- .venv315/bin/activate_this.py +59 -0
- deadcode/__init__.py +1 -1
- deadcode/actions/find_python_filenames.py +2 -18
- deadcode/actions/find_unused_names.py +2 -2
- deadcode/actions/fix_or_show_unused_code.py +12 -9
- deadcode/actions/get_unused_import_parts.py +207 -0
- deadcode/actions/get_unused_names_error_message.py +16 -12
- deadcode/actions/merge_overlaping_file_parts.py +8 -9
- deadcode/actions/parse_abstract_syntax_tree.py +3 -2
- deadcode/actions/parse_arguments.py +38 -18
- deadcode/actions/remove_file_parts_from_content.py +33 -11
- deadcode/cli.py +40 -18
- deadcode/constants.py +7 -4
- deadcode/data_types.py +2 -1
- deadcode/utils/fix_indent.py +4 -12
- deadcode/utils/flatten_lists.py +4 -4
- deadcode/utils/nested_scopes.py +6 -8
- deadcode/utils/print_safely.py +29 -0
- deadcode/visitor/code_item.py +12 -69
- deadcode/visitor/dead_code_visitor.py +392 -99
- deadcode/visitor/ignore.py +56 -9
- deadcode/visitor/lines.py +7 -6
- deadcode/visitor/module_exports.py +80 -0
- deadcode/visitor/noqa.py +3 -4
- deadcode/visitor/utils.py +40 -17
- deadcode-2.6.1.dist-info/METADATA +442 -0
- deadcode-2.6.1.dist-info/RECORD +39 -0
- docs/conf.py +20 -0
- deadcode/utils/base_test_case.py +0 -88
- deadcode-2.5.0.dist-info/METADATA +0 -244
- deadcode-2.5.0.dist-info/RECORD +0 -32
- {deadcode-2.5.0.dist-info → deadcode-2.6.1.dist-info}/WHEEL +0 -0
- {deadcode-2.5.0.dist-info → deadcode-2.6.1.dist-info}/entry_points.txt +0 -0
- {deadcode-2.5.0.dist-info → deadcode-2.6.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Copyright (c) 2020-202x The virtualenv developers
|
|
2
|
+
#
|
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
# a copy of this software and associated documentation files (the
|
|
5
|
+
# "Software"), to deal in the Software without restriction, including
|
|
6
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
# the following conditions:
|
|
10
|
+
#
|
|
11
|
+
# The above copyright notice and this permission notice shall be
|
|
12
|
+
# included in all copies or substantial portions of the Software.
|
|
13
|
+
#
|
|
14
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
21
|
+
|
|
22
|
+
"""
|
|
23
|
+
Activate virtualenv for current interpreter:
|
|
24
|
+
|
|
25
|
+
import runpy
|
|
26
|
+
runpy.run_path(this_file)
|
|
27
|
+
|
|
28
|
+
This can be used when you must use an existing Python interpreter, not the virtualenv bin/python.
|
|
29
|
+
""" # noqa: D415
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import os
|
|
34
|
+
import site
|
|
35
|
+
import sys
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
abs_file = os.path.abspath(__file__)
|
|
39
|
+
except NameError as exc:
|
|
40
|
+
msg = "You must use import runpy; runpy.run_path(this_file)"
|
|
41
|
+
raise AssertionError(msg) from exc
|
|
42
|
+
|
|
43
|
+
bin_dir = os.path.dirname(abs_file)
|
|
44
|
+
base = bin_dir[: -len("bin") - 1] # strip away the bin part from the __file__, plus the path separator
|
|
45
|
+
|
|
46
|
+
# prepend bin to PATH (this file is inside the bin directory)
|
|
47
|
+
os.environ["PATH"] = os.pathsep.join([bin_dir, *os.environ.get("PATH", "").split(os.pathsep)])
|
|
48
|
+
os.environ["VIRTUAL_ENV"] = base # virtual env is right above bin directory
|
|
49
|
+
os.environ["VIRTUAL_ENV_PROMPT"] = "" or os.path.basename(base) # noqa: SIM222
|
|
50
|
+
|
|
51
|
+
# add the virtual environments libraries to the host python import mechanism
|
|
52
|
+
prev_length = len(sys.path)
|
|
53
|
+
for lib in "../lib/python3.10/site-packages".split(os.pathsep):
|
|
54
|
+
path = os.path.realpath(os.path.join(bin_dir, lib))
|
|
55
|
+
site.addsitedir(path)
|
|
56
|
+
sys.path[:] = sys.path[prev_length:] + sys.path[0:prev_length]
|
|
57
|
+
|
|
58
|
+
sys.real_prefix = sys.prefix
|
|
59
|
+
sys.prefix = base
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Copyright (c) 2020-202x The virtualenv developers
|
|
2
|
+
#
|
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
# a copy of this software and associated documentation files (the
|
|
5
|
+
# "Software"), to deal in the Software without restriction, including
|
|
6
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
# the following conditions:
|
|
10
|
+
#
|
|
11
|
+
# The above copyright notice and this permission notice shall be
|
|
12
|
+
# included in all copies or substantial portions of the Software.
|
|
13
|
+
#
|
|
14
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
21
|
+
|
|
22
|
+
"""
|
|
23
|
+
Activate virtualenv for current interpreter:
|
|
24
|
+
|
|
25
|
+
import runpy
|
|
26
|
+
runpy.run_path(this_file)
|
|
27
|
+
|
|
28
|
+
This can be used when you must use an existing Python interpreter, not the virtualenv bin/python.
|
|
29
|
+
""" # noqa: D415
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import os
|
|
34
|
+
import site
|
|
35
|
+
import sys
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
abs_file = os.path.abspath(__file__)
|
|
39
|
+
except NameError as exc:
|
|
40
|
+
msg = "You must use import runpy; runpy.run_path(this_file)"
|
|
41
|
+
raise AssertionError(msg) from exc
|
|
42
|
+
|
|
43
|
+
bin_dir = os.path.dirname(abs_file)
|
|
44
|
+
base = bin_dir[: -len("bin") - 1] # strip away the bin part from the __file__, plus the path separator
|
|
45
|
+
|
|
46
|
+
# prepend bin to PATH (this file is inside the bin directory)
|
|
47
|
+
os.environ["PATH"] = os.pathsep.join([bin_dir, *os.environ.get("PATH", "").split(os.pathsep)])
|
|
48
|
+
os.environ["VIRTUAL_ENV"] = base # virtual env is right above bin directory
|
|
49
|
+
os.environ["VIRTUAL_ENV_PROMPT"] = "" or os.path.basename(base) # noqa: SIM222
|
|
50
|
+
|
|
51
|
+
# add the virtual environments libraries to the host python import mechanism
|
|
52
|
+
prev_length = len(sys.path)
|
|
53
|
+
for lib in "../lib/python3.11/site-packages".split(os.pathsep):
|
|
54
|
+
path = os.path.realpath(os.path.join(bin_dir, lib))
|
|
55
|
+
site.addsitedir(path)
|
|
56
|
+
sys.path[:] = sys.path[prev_length:] + sys.path[0:prev_length]
|
|
57
|
+
|
|
58
|
+
sys.real_prefix = sys.prefix
|
|
59
|
+
sys.prefix = base
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Copyright (c) 2020-202x The virtualenv developers
|
|
2
|
+
#
|
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
# a copy of this software and associated documentation files (the
|
|
5
|
+
# "Software"), to deal in the Software without restriction, including
|
|
6
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
# the following conditions:
|
|
10
|
+
#
|
|
11
|
+
# The above copyright notice and this permission notice shall be
|
|
12
|
+
# included in all copies or substantial portions of the Software.
|
|
13
|
+
#
|
|
14
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
21
|
+
|
|
22
|
+
"""
|
|
23
|
+
Activate virtualenv for current interpreter:
|
|
24
|
+
|
|
25
|
+
import runpy
|
|
26
|
+
runpy.run_path(this_file)
|
|
27
|
+
|
|
28
|
+
This can be used when you must use an existing Python interpreter, not the virtualenv bin/python.
|
|
29
|
+
""" # noqa: D415
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import os
|
|
34
|
+
import site
|
|
35
|
+
import sys
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
abs_file = os.path.abspath(__file__)
|
|
39
|
+
except NameError as exc:
|
|
40
|
+
msg = "You must use import runpy; runpy.run_path(this_file)"
|
|
41
|
+
raise AssertionError(msg) from exc
|
|
42
|
+
|
|
43
|
+
bin_dir = os.path.dirname(abs_file)
|
|
44
|
+
base = bin_dir[: -len("bin") - 1] # strip away the bin part from the __file__, plus the path separator
|
|
45
|
+
|
|
46
|
+
# prepend bin to PATH (this file is inside the bin directory)
|
|
47
|
+
os.environ["PATH"] = os.pathsep.join([bin_dir, *os.environ.get("PATH", "").split(os.pathsep)])
|
|
48
|
+
os.environ["VIRTUAL_ENV"] = base # virtual env is right above bin directory
|
|
49
|
+
os.environ["VIRTUAL_ENV_PROMPT"] = "" or os.path.basename(base) # noqa: SIM222
|
|
50
|
+
|
|
51
|
+
# add the virtual environments libraries to the host python import mechanism
|
|
52
|
+
prev_length = len(sys.path)
|
|
53
|
+
for lib in "../lib/python3.14/site-packages".split(os.pathsep):
|
|
54
|
+
path = os.path.realpath(os.path.join(bin_dir, lib))
|
|
55
|
+
site.addsitedir(path)
|
|
56
|
+
sys.path[:] = sys.path[prev_length:] + sys.path[0:prev_length]
|
|
57
|
+
|
|
58
|
+
sys.real_prefix = sys.prefix
|
|
59
|
+
sys.prefix = base
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Copyright (c) 2020-202x The virtualenv developers
|
|
2
|
+
#
|
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
# a copy of this software and associated documentation files (the
|
|
5
|
+
# "Software"), to deal in the Software without restriction, including
|
|
6
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
# the following conditions:
|
|
10
|
+
#
|
|
11
|
+
# The above copyright notice and this permission notice shall be
|
|
12
|
+
# included in all copies or substantial portions of the Software.
|
|
13
|
+
#
|
|
14
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
21
|
+
|
|
22
|
+
"""
|
|
23
|
+
Activate virtualenv for current interpreter:
|
|
24
|
+
|
|
25
|
+
import runpy
|
|
26
|
+
runpy.run_path(this_file)
|
|
27
|
+
|
|
28
|
+
This can be used when you must use an existing Python interpreter, not the virtualenv bin/python.
|
|
29
|
+
""" # noqa: D415
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import os
|
|
34
|
+
import site
|
|
35
|
+
import sys
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
abs_file = os.path.abspath(__file__)
|
|
39
|
+
except NameError as exc:
|
|
40
|
+
msg = "You must use import runpy; runpy.run_path(this_file)"
|
|
41
|
+
raise AssertionError(msg) from exc
|
|
42
|
+
|
|
43
|
+
bin_dir = os.path.dirname(abs_file)
|
|
44
|
+
base = bin_dir[: -len("bin") - 1] # strip away the bin part from the __file__, plus the path separator
|
|
45
|
+
|
|
46
|
+
# prepend bin to PATH (this file is inside the bin directory)
|
|
47
|
+
os.environ["PATH"] = os.pathsep.join([bin_dir, *os.environ.get("PATH", "").split(os.pathsep)])
|
|
48
|
+
os.environ["VIRTUAL_ENV"] = base # virtual env is right above bin directory
|
|
49
|
+
os.environ["VIRTUAL_ENV_PROMPT"] = "" or os.path.basename(base) # noqa: SIM222
|
|
50
|
+
|
|
51
|
+
# add the virtual environments libraries to the host python import mechanism
|
|
52
|
+
prev_length = len(sys.path)
|
|
53
|
+
for lib in "../lib/python3.15/site-packages".split(os.pathsep):
|
|
54
|
+
path = os.path.realpath(os.path.join(bin_dir, lib))
|
|
55
|
+
site.addsitedir(path)
|
|
56
|
+
sys.path[:] = sys.path[prev_length:] + sys.path[0:prev_length]
|
|
57
|
+
|
|
58
|
+
sys.real_prefix = sys.prefix
|
|
59
|
+
sys.prefix = base
|
deadcode/__init__.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
from logging import getLogger
|
|
2
|
-
from typing import List
|
|
3
2
|
from pathlib import Path
|
|
4
3
|
|
|
5
4
|
from deadcode.data_types import Args
|
|
@@ -8,9 +7,9 @@ from deadcode.visitor.ignore import _match
|
|
|
8
7
|
logger = getLogger()
|
|
9
8
|
|
|
10
9
|
|
|
11
|
-
def find_python_filenames(args: Args) ->
|
|
10
|
+
def find_python_filenames(args: Args) -> list[str]:
|
|
12
11
|
filenames = []
|
|
13
|
-
paths:
|
|
12
|
+
paths: list[str] = list(args.paths)
|
|
14
13
|
while paths:
|
|
15
14
|
path = Path(paths.pop())
|
|
16
15
|
|
|
@@ -32,18 +31,3 @@ def find_python_filenames(args: Args) -> List[str]:
|
|
|
32
31
|
sep = '\n - '
|
|
33
32
|
logger.info(f'Files to be checked for dead code: {sep.join(filenames)}')
|
|
34
33
|
return filenames
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
# Should show errors, if files do not exist.
|
|
38
|
-
# Could mock directories: os.path.isfile os.walk -> replace this one with rglob
|
|
39
|
-
|
|
40
|
-
# Should use list dir myself.
|
|
41
|
-
|
|
42
|
-
# TODO: Whats the solution for mocking the file system checking?
|
|
43
|
-
# Every single provided file is assumed to exist.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
# # TODO: investigate what can be reused from this module
|
|
47
|
-
# Loop over all given paths, abort if any ends with .pyc, add the other given
|
|
48
|
-
# files (even those not ending with .py) and collect all .py files under the
|
|
49
|
-
# given directories.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from
|
|
1
|
+
from collections.abc import Iterable
|
|
2
2
|
|
|
3
3
|
from deadcode.data_types import Args, Filename
|
|
4
4
|
from deadcode.visitor.code_item import CodeItem
|
|
@@ -6,7 +6,7 @@ from deadcode.visitor.dead_code_visitor import DeadCodeVisitor
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
def find_unused_names(
|
|
9
|
-
filenames:
|
|
9
|
+
filenames: list[Filename],
|
|
10
10
|
args: Args,
|
|
11
11
|
) -> Iterable[CodeItem]:
|
|
12
12
|
dead_code_visitor = DeadCodeVisitor(filenames, args)
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
from collections import defaultdict
|
|
2
2
|
from difflib import diff_bytes, unified_diff
|
|
3
|
-
from
|
|
3
|
+
from collections.abc import Iterable
|
|
4
4
|
import os
|
|
5
5
|
|
|
6
|
+
from deadcode.actions.get_unused_import_parts import get_unused_import_parts
|
|
6
7
|
from deadcode.actions.merge_overlaping_file_parts import merge_overlaping_file_parts
|
|
7
8
|
from deadcode.actions.remove_file_parts_from_content import remove_file_parts_from_content
|
|
8
9
|
from deadcode.data_types import Args
|
|
@@ -19,7 +20,7 @@ def fix_or_show_unused_code(unused_items: Iterable[CodeItem], args: Args) -> str
|
|
|
19
20
|
# For each filename find file parts, which have to be fixed.
|
|
20
21
|
# And fix those file parts.
|
|
21
22
|
#
|
|
22
|
-
# TODO: trailing commas,
|
|
23
|
+
# TODO: remove leftover trailing commas, whitespace and empty lines for non-import expressions.
|
|
23
24
|
|
|
24
25
|
filename_to_unused_items = defaultdict(list)
|
|
25
26
|
for unused_item in unused_items:
|
|
@@ -28,13 +29,20 @@ def fix_or_show_unused_code(unused_items: Iterable[CodeItem], args: Args) -> str
|
|
|
28
29
|
result = []
|
|
29
30
|
|
|
30
31
|
for filename, file_unused_items in filename_to_unused_items.items():
|
|
31
|
-
|
|
32
|
+
import_items = [item for item in file_unused_items if item.type_ == 'import']
|
|
33
|
+
other_items = [item for item in file_unused_items if item.type_ != 'import']
|
|
32
34
|
|
|
33
|
-
|
|
35
|
+
with open(filename, 'rb') as f:
|
|
36
|
+
file_content = f.read()
|
|
34
37
|
|
|
35
38
|
with open(filename, 'rb') as f:
|
|
36
39
|
file_content_lines = f.readlines()
|
|
37
40
|
|
|
41
|
+
import_parts = get_unused_import_parts(file_content, import_items) if import_items else []
|
|
42
|
+
file_parts = flatten_list([item.code_parts for item in other_items]) + import_parts
|
|
43
|
+
|
|
44
|
+
unused_file_parts = merge_overlaping_file_parts(file_parts)
|
|
45
|
+
|
|
38
46
|
updated_file_content_lines = remove_file_parts_from_content(file_content_lines, unused_file_parts)
|
|
39
47
|
updated_file_content = b''.join(updated_file_content_lines)
|
|
40
48
|
if updated_file_content.strip():
|
|
@@ -58,7 +66,6 @@ def fix_or_show_unused_code(unused_items: Iterable[CodeItem], args: Args) -> str
|
|
|
58
66
|
|
|
59
67
|
elif args.fix:
|
|
60
68
|
with open(filename, 'wb') as f:
|
|
61
|
-
# TODO: is there a method writelines?
|
|
62
69
|
f.write(updated_file_content)
|
|
63
70
|
else:
|
|
64
71
|
os.remove(filename)
|
|
@@ -66,7 +73,3 @@ def fix_or_show_unused_code(unused_items: Iterable[CodeItem], args: Args) -> str
|
|
|
66
73
|
if result:
|
|
67
74
|
return b'\n'.join(result).decode()
|
|
68
75
|
return ''
|
|
69
|
-
|
|
70
|
-
# TODO: update this one: solution is to use read and write operations.
|
|
71
|
-
#
|
|
72
|
-
# Solution is to open file for reading and then for writing.
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterable
|
|
4
|
+
|
|
5
|
+
from deadcode.data_types import Part
|
|
6
|
+
from deadcode.utils.flatten_lists import flatten_list
|
|
7
|
+
from deadcode.visitor.code_item import CodeItem
|
|
8
|
+
|
|
9
|
+
ImportAliasKey = tuple[str, int | None, int | None]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_import_alias_key(alias: ast.alias) -> ImportAliasKey:
|
|
13
|
+
"""Returns the key that identifies an import alias in reported findings."""
|
|
14
|
+
return alias.asname or alias.name.partition('.')[0], alias.lineno, alias.col_offset
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _line_bytes(content_lines: list[bytes], line_number: int) -> bytes:
|
|
18
|
+
if 1 <= line_number <= len(content_lines):
|
|
19
|
+
return content_lines[line_number - 1]
|
|
20
|
+
return b''
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _find_byte(content_lines: list[bytes], line_number: int, start_col: int, end_col: int, needle: bytes) -> int | None:
|
|
24
|
+
position = _line_bytes(content_lines, line_number).find(needle, start_col, end_col)
|
|
25
|
+
if position == -1:
|
|
26
|
+
return None
|
|
27
|
+
return position
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _find_comma_between(
|
|
31
|
+
content_lines: list[bytes], start: tuple[int, int], end: tuple[int, int]
|
|
32
|
+
) -> tuple[int, int] | None:
|
|
33
|
+
"""Returns the position of the first comma between two source positions."""
|
|
34
|
+
start_line, start_col = start
|
|
35
|
+
end_line, end_col = end
|
|
36
|
+
|
|
37
|
+
for line_number in range(start_line, end_line + 1):
|
|
38
|
+
line = _line_bytes(content_lines, line_number)
|
|
39
|
+
if line_number == start_line and line_number == end_line:
|
|
40
|
+
position = _find_byte(content_lines, line_number, start_col, end_col, b',')
|
|
41
|
+
elif line_number == start_line:
|
|
42
|
+
position = _find_byte(content_lines, line_number, start_col, len(line), b',')
|
|
43
|
+
elif line_number == end_line:
|
|
44
|
+
position = _find_byte(content_lines, line_number, 0, end_col, b',')
|
|
45
|
+
else:
|
|
46
|
+
position = _find_byte(content_lines, line_number, 0, len(line), b',')
|
|
47
|
+
|
|
48
|
+
if position is not None:
|
|
49
|
+
return line_number, position
|
|
50
|
+
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _end_of_line_content(content_lines: list[bytes], line_number: int) -> int:
|
|
55
|
+
"""Returns the length of a line without its trailing line terminator."""
|
|
56
|
+
line = _line_bytes(content_lines, line_number)
|
|
57
|
+
for terminator in (b'\r\n', b'\n', b'\r'):
|
|
58
|
+
if line.endswith(terminator):
|
|
59
|
+
return len(line) - len(terminator)
|
|
60
|
+
return len(line)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _first_non_whitespace_column(content_lines: list[bytes], line_number: int, start_column: int) -> int | None:
|
|
64
|
+
"""Returns the first non-whitespace column at or after ``start_column``."""
|
|
65
|
+
line = _line_bytes(content_lines, line_number)
|
|
66
|
+
end_column = _end_of_line_content(content_lines, line_number)
|
|
67
|
+
for column in range(start_column, end_column):
|
|
68
|
+
if line[column : column + 1] not in (b' ', b'\t'):
|
|
69
|
+
return column
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _last_non_whitespace_column_before(content_lines: list[bytes], line_number: int, end_column: int) -> int | None:
|
|
74
|
+
"""Returns the last non-whitespace column strictly before ``end_column``."""
|
|
75
|
+
line = _line_bytes(content_lines, line_number)
|
|
76
|
+
for column in range(end_column - 1, -1, -1):
|
|
77
|
+
if line[column : column + 1] not in (b' ', b'\t'):
|
|
78
|
+
return column
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _is_only_whitespace_or_comment(remainder: bytes) -> bool:
|
|
83
|
+
"""Returns whether the remainder is whitespace or a trailing comment."""
|
|
84
|
+
stripped_remainder = remainder.strip()
|
|
85
|
+
return not stripped_remainder or stripped_remainder.startswith(b'#')
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _end_of_whitespace_after_comma(content_lines: list[bytes], comma: tuple[int, int]) -> int:
|
|
89
|
+
line = _line_bytes(content_lines, comma[0])
|
|
90
|
+
end_col = comma[1] + 1
|
|
91
|
+
while end_col < len(line) and line[end_col : end_col + 1] in (b' ', b'\t'):
|
|
92
|
+
end_col += 1
|
|
93
|
+
return end_col
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def get_unused_import_parts(content: bytes, unused_import_items: Iterable[CodeItem]) -> list[Part]:
|
|
97
|
+
"""Returns code parts to remove for unused import findings.
|
|
98
|
+
|
|
99
|
+
When every non-wildcard alias of an import statement is unused, the whole
|
|
100
|
+
statement is returned as a single part. Otherwise each unused alias is
|
|
101
|
+
returned together with one adjacent comma, so the remaining aliases keep
|
|
102
|
+
valid comma placement.
|
|
103
|
+
"""
|
|
104
|
+
unused_import_items = list(unused_import_items)
|
|
105
|
+
fallback_parts = flatten_list([item.code_parts for item in unused_import_items])
|
|
106
|
+
removable_keys = {(item.name, item.name_line, item.name_column) for item in unused_import_items}
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
tree = ast.parse(content)
|
|
110
|
+
except SyntaxError:
|
|
111
|
+
return fallback_parts
|
|
112
|
+
|
|
113
|
+
content_lines = content.splitlines(keepends=True)
|
|
114
|
+
import_parts: list[Part] = []
|
|
115
|
+
matched_keys: set[ImportAliasKey] = set()
|
|
116
|
+
|
|
117
|
+
for node in ast.walk(tree):
|
|
118
|
+
if not isinstance(node, (ast.Import, ast.ImportFrom)):
|
|
119
|
+
continue
|
|
120
|
+
|
|
121
|
+
# A wildcard alias is never removable.
|
|
122
|
+
aliases = [alias for alias in node.names if alias.name != '*']
|
|
123
|
+
if not aliases:
|
|
124
|
+
continue
|
|
125
|
+
|
|
126
|
+
alias_keys = [get_import_alias_key(alias) for alias in aliases]
|
|
127
|
+
|
|
128
|
+
if all(key in removable_keys for key in alias_keys):
|
|
129
|
+
if node.end_lineno is None or node.end_col_offset is None:
|
|
130
|
+
continue
|
|
131
|
+
|
|
132
|
+
start_column = node.col_offset
|
|
133
|
+
end_column = node.end_col_offset
|
|
134
|
+
following_column = _first_non_whitespace_column(content_lines, node.end_lineno, end_column)
|
|
135
|
+
if (
|
|
136
|
+
following_column is not None
|
|
137
|
+
and _line_bytes(content_lines, node.end_lineno)[following_column : following_column + 1] == b';'
|
|
138
|
+
):
|
|
139
|
+
end_column = following_column + 1
|
|
140
|
+
after_separator_column = _first_non_whitespace_column(content_lines, node.end_lineno, end_column)
|
|
141
|
+
if after_separator_column is not None:
|
|
142
|
+
end_column = after_separator_column
|
|
143
|
+
else:
|
|
144
|
+
preceding_column = _last_non_whitespace_column_before(content_lines, node.lineno, node.col_offset)
|
|
145
|
+
if (
|
|
146
|
+
preceding_column is not None
|
|
147
|
+
and _line_bytes(content_lines, node.lineno)[preceding_column : preceding_column + 1] == b';'
|
|
148
|
+
):
|
|
149
|
+
start_column = preceding_column
|
|
150
|
+
|
|
151
|
+
line_content_end = _end_of_line_content(content_lines, node.end_lineno)
|
|
152
|
+
remainder = _line_bytes(content_lines, node.end_lineno)[end_column:line_content_end]
|
|
153
|
+
if _is_only_whitespace_or_comment(remainder):
|
|
154
|
+
end_column = line_content_end
|
|
155
|
+
|
|
156
|
+
import_parts.append(Part(node.lineno, node.end_lineno, start_column, end_column))
|
|
157
|
+
matched_keys.update(alias_keys)
|
|
158
|
+
continue
|
|
159
|
+
|
|
160
|
+
for index, alias in enumerate(aliases):
|
|
161
|
+
key = alias_keys[index]
|
|
162
|
+
if key not in removable_keys or alias.end_lineno is None or alias.end_col_offset is None:
|
|
163
|
+
continue
|
|
164
|
+
|
|
165
|
+
if index + 1 < len(aliases):
|
|
166
|
+
next_alias = aliases[index + 1]
|
|
167
|
+
comma = _find_comma_between(
|
|
168
|
+
content_lines,
|
|
169
|
+
(alias.end_lineno, alias.end_col_offset),
|
|
170
|
+
(next_alias.lineno, next_alias.col_offset),
|
|
171
|
+
)
|
|
172
|
+
if comma is None:
|
|
173
|
+
matched_keys.add(key)
|
|
174
|
+
continue
|
|
175
|
+
end_col = _end_of_whitespace_after_comma(content_lines, comma)
|
|
176
|
+
import_parts.append(Part(alias.lineno, comma[0], alias.col_offset, end_col))
|
|
177
|
+
else:
|
|
178
|
+
previous_alias = aliases[index - 1]
|
|
179
|
+
if previous_alias.end_lineno is None or previous_alias.end_col_offset is None:
|
|
180
|
+
import_parts.append(Part(alias.lineno, alias.end_lineno, alias.col_offset, alias.end_col_offset))
|
|
181
|
+
else:
|
|
182
|
+
comma = _find_comma_between(
|
|
183
|
+
content_lines,
|
|
184
|
+
(previous_alias.end_lineno, previous_alias.end_col_offset),
|
|
185
|
+
(alias.lineno, alias.col_offset),
|
|
186
|
+
)
|
|
187
|
+
if comma is None:
|
|
188
|
+
matched_keys.add(key)
|
|
189
|
+
continue
|
|
190
|
+
if previous_alias.end_lineno != alias.lineno:
|
|
191
|
+
import_parts.append(
|
|
192
|
+
Part(comma[0], comma[0], comma[1], _end_of_line_content(content_lines, comma[0]))
|
|
193
|
+
)
|
|
194
|
+
import_parts.append(Part(alias.lineno, alias.end_lineno, 0, alias.end_col_offset))
|
|
195
|
+
else:
|
|
196
|
+
import_parts.append(Part(comma[0], alias.end_lineno, comma[1], alias.end_col_offset))
|
|
197
|
+
|
|
198
|
+
matched_keys.add(key)
|
|
199
|
+
|
|
200
|
+
remaining_fallback_parts = [
|
|
201
|
+
part
|
|
202
|
+
for item in unused_import_items
|
|
203
|
+
if (item.name, item.name_line, item.name_column) not in matched_keys
|
|
204
|
+
for part in item.code_parts
|
|
205
|
+
]
|
|
206
|
+
|
|
207
|
+
return list(dict.fromkeys(import_parts + remaining_fallback_parts))
|
|
@@ -1,15 +1,20 @@
|
|
|
1
|
-
from
|
|
1
|
+
from collections.abc import Iterable
|
|
2
2
|
|
|
3
3
|
from deadcode.data_types import Args
|
|
4
4
|
from deadcode.visitor.code_item import CodeItem
|
|
5
5
|
from deadcode.visitor.ignore import _match
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
def get_unused_names_error_message(unused_names: Iterable[CodeItem], args: Args) ->
|
|
8
|
+
def get_unused_names_error_message(unused_names: Iterable[CodeItem], args: Args) -> str | None:
|
|
9
9
|
unused_names = list(unused_names)
|
|
10
|
+
had_unused_names = bool(unused_names)
|
|
11
|
+
|
|
12
|
+
if args.only:
|
|
13
|
+
unused_names = [item for item in unused_names if _match(item.filename, args.only)]
|
|
10
14
|
|
|
11
15
|
if not unused_names:
|
|
12
|
-
|
|
16
|
+
# Filtering away every finding keeps the historical empty message instead of reporting a clean run.
|
|
17
|
+
return '' if had_unused_names and not args.count and not args.quiet else None
|
|
13
18
|
|
|
14
19
|
if args.quiet:
|
|
15
20
|
return ''
|
|
@@ -19,17 +24,16 @@ def get_unused_names_error_message(unused_names: Iterable[CodeItem], args: Args)
|
|
|
19
24
|
|
|
20
25
|
messages = []
|
|
21
26
|
for item in unused_names:
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
messages.append(message)
|
|
27
|
+
message = f'{item.filename_with_position} \033[91m{item.error_code}\033[0m '
|
|
28
|
+
message += item.message or (
|
|
29
|
+
f'{item.type_.replace("_", " ").capitalize()} `\033[1m{item.name}\033[0m` is never used'
|
|
30
|
+
)
|
|
31
|
+
if args.no_color:
|
|
32
|
+
message = message.replace('\033[91m', '').replace('\033[1m', '').replace('\033[0m', '')
|
|
33
|
+
messages.append(message)
|
|
30
34
|
|
|
31
35
|
if args.fix:
|
|
32
|
-
message = f
|
|
36
|
+
message = f'\nRemoved \033[1m{len(messages)}\033[0m unused code item{"s" if len(messages) > 1 else ""}!'
|
|
33
37
|
if args.no_color:
|
|
34
38
|
message = message.replace('\x1b[1m', '').replace('\x1b[0m', '')
|
|
35
39
|
messages.append(message)
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
from typing import List, Optional, Tuple
|
|
2
1
|
from deadcode.data_types import Part
|
|
3
2
|
|
|
4
3
|
|
|
@@ -13,11 +12,11 @@ def does_include(bigger_part: Part, smaller_part: Part) -> bool:
|
|
|
13
12
|
return bool(starts_later and ends_faster)
|
|
14
13
|
|
|
15
14
|
|
|
16
|
-
def sort_parts(bigger_part: Part, smaller_part: Part) ->
|
|
15
|
+
def sort_parts(bigger_part: Part, smaller_part: Part) -> tuple[Part, Part]:
|
|
17
16
|
"""Returns code part which begins first following by another code part."""
|
|
18
17
|
# TODO: Column should go first (tuple comparison would be possible)
|
|
19
|
-
line_start_b,
|
|
20
|
-
line_start_s,
|
|
18
|
+
line_start_b, _line_end_b, col_start_b, _col_end_b = bigger_part
|
|
19
|
+
line_start_s, _line_end_s, col_start_s, _col_end_s = smaller_part
|
|
21
20
|
|
|
22
21
|
# Pirmo pradžios eilutė ir stulpelis yra pirmiau.
|
|
23
22
|
if (line_start_s < line_start_b) or ((line_start_s == line_start_b) and (col_start_s < col_start_b)):
|
|
@@ -28,14 +27,14 @@ def sort_parts(bigger_part: Part, smaller_part: Part) -> Tuple[Part, Part]:
|
|
|
28
27
|
def does_overlap(bigger_part: Part, smaller_part: Part) -> bool:
|
|
29
28
|
bigger_part, smaller_part = sort_parts(bigger_part, smaller_part)
|
|
30
29
|
|
|
31
|
-
|
|
32
|
-
line_start_s,
|
|
30
|
+
_line_start_b, line_end_b, _col_start_b, col_end_b = bigger_part
|
|
31
|
+
line_start_s, _line_end_s, col_start_s, _col_end_s = smaller_part
|
|
33
32
|
|
|
34
33
|
# ar pirmo pabaiga perlipa antro pradžia
|
|
35
34
|
return bool((line_end_b > line_start_s) or ((line_end_b == line_start_s) and (col_end_b > col_start_s)))
|
|
36
35
|
|
|
37
36
|
|
|
38
|
-
def merge_parts(p1: Part, p2: Part) ->
|
|
37
|
+
def merge_parts(p1: Part, p2: Part) -> Part | None:
|
|
39
38
|
p1, p2 = sort_parts(p1, p2)
|
|
40
39
|
|
|
41
40
|
line_start1, line_end1, col_start1, col_end1 = p1
|
|
@@ -58,10 +57,10 @@ def merge_parts(p1: Part, p2: Part) -> Optional[Part]:
|
|
|
58
57
|
return None
|
|
59
58
|
|
|
60
59
|
|
|
61
|
-
def merge_overlaping_file_parts(overlaping_file_parts:
|
|
60
|
+
def merge_overlaping_file_parts(overlaping_file_parts: list[Part]) -> list[Part]:
|
|
62
61
|
# Make algorithm O(n^2) by checking every single part if overlaps
|
|
63
62
|
|
|
64
|
-
non_overlaping_file_parts:
|
|
63
|
+
non_overlaping_file_parts: list[Part] = []
|
|
65
64
|
for p1 in sorted(overlaping_file_parts):
|
|
66
65
|
merged_part = None
|
|
67
66
|
merged_with_index = None
|