deadcode 0.0.1__tar.gz → 0.0.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {deadcode-0.0.1 → deadcode-0.0.2}/PKG-INFO +3 -1
- deadcode-0.0.2/deadcode/actions/argument_parsing.py +27 -0
- deadcode-0.0.2/deadcode/cli.py +138 -0
- deadcode-0.0.2/deadcode/data_types.py +16 -0
- {deadcode-0.0.1/dead_code → deadcode-0.0.2/deadcode}/tests/test_case1/hello_world.py +0 -1
- {deadcode-0.0.1/dead_code → deadcode-0.0.2/deadcode}/tests/test_case1/subdir/another_subdir/__init__.py +0 -1
- {deadcode-0.0.1/dead_code → deadcode-0.0.2/deadcode}/tests/test_case1/subdir/one_more_file.py +0 -2
- deadcode-0.0.2/deadcode/tests/test_deadcode.py +76 -0
- deadcode-0.0.2/deadcode/utils.py +16 -0
- {deadcode-0.0.1 → deadcode-0.0.2}/pyproject.toml +17 -12
- deadcode-0.0.1/.flake8 +0 -9
- deadcode-0.0.1/Makefile +0 -23
- deadcode-0.0.1/dead_code/Makefile +0 -10
- deadcode-0.0.1/dead_code/cli.py +0 -78
- deadcode-0.0.1/dead_code/tests/test_dead_code_finder.py +0 -17
- deadcode-0.0.1/notes +0 -88
- deadcode-0.0.1/requirements-dev.txt +0 -30
- {deadcode-0.0.1 → deadcode-0.0.2}/.gitignore +0 -0
- {deadcode-0.0.1 → deadcode-0.0.2}/README.md +0 -0
- {deadcode-0.0.1/dead_code → deadcode-0.0.2/deadcode}/__init__.py +0 -0
- /deadcode-0.0.1/requirements.txt → /deadcode-0.0.2/deadcode/actions/__init__.py +0 -0
- {deadcode-0.0.1/dead_code → deadcode-0.0.2/deadcode}/tests/module_a.py +0 -0
- {deadcode-0.0.1/dead_code → deadcode-0.0.2/deadcode}/tests/module_b.py +0 -0
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: deadcode
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.2
|
|
4
4
|
Summary: Find and remove dead code.
|
|
5
|
+
Project-URL: Homepage, https://github.com/albertas/deadcode
|
|
6
|
+
Project-URL: Documentation, https://deadcode.readthedocs.io/
|
|
5
7
|
Author-email: Albertas Gimbutas <albertasgim@gmail.com>
|
|
6
8
|
Classifier: Development Status :: 3 - Alpha
|
|
7
9
|
Classifier: License :: OSI Approved :: GNU Affero General Public License v3
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
from typing import List, Optional
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from deadcode.data_types import Args
|
|
6
|
+
from deadcode.utils import split_comma_separated_values
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def parse_command_line_arguments(args: Optional[List[str]]) -> Args:
|
|
10
|
+
if not args:
|
|
11
|
+
args = sys.argv[1:]
|
|
12
|
+
|
|
13
|
+
parser = argparse.ArgumentParser() # Docs: https://docs.python.org/3/library/argparse.html
|
|
14
|
+
parser.add_argument("paths", help="Paths where to search for python files", nargs="+")
|
|
15
|
+
parser.add_argument("--exclude", help="PATHS to files not to analyse at all (comma separated values)",
|
|
16
|
+
nargs="*", action="append", default=[], type=str)
|
|
17
|
+
parser.add_argument("--ignore-names", help="NAMES not to report (comma separated values)",
|
|
18
|
+
nargs="*", action="append", default=[], type=str)
|
|
19
|
+
parser.add_argument("--ignore-files", help="PATHS to files not to report NAMES from (comma separated values)",
|
|
20
|
+
nargs="*", action="append", default=[], type=str)
|
|
21
|
+
|
|
22
|
+
parsed_args = parser.parse_args(args).__dict__
|
|
23
|
+
|
|
24
|
+
for field_name in ["exclude", "ignore_names", "ignore_files"]:
|
|
25
|
+
parsed_args[field_name] = split_comma_separated_values(parsed_args.get(field_name))
|
|
26
|
+
|
|
27
|
+
return Args(**parsed_args)
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
from typing import Dict, List, Optional
|
|
2
|
+
from collections import defaultdict
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import re
|
|
6
|
+
from fnmatch import fnmatch
|
|
7
|
+
|
|
8
|
+
from deadcode.data_types import Args, FileContent, Filename, Pathname, VariableName
|
|
9
|
+
from deadcode.actions.argument_parsing import parse_command_line_arguments
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def does_path_match_patterns(path: Pathname, patterns: Optional[List[Pathname]]) -> bool:
|
|
13
|
+
if path != "." and patterns:
|
|
14
|
+
for pattern in patterns:
|
|
15
|
+
if fnmatch(path, pattern):
|
|
16
|
+
return True
|
|
17
|
+
return False
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def find_python_filenames(args: Args):
|
|
21
|
+
"""Recursively searches for Python filenames in provided paths."""
|
|
22
|
+
python_filenames = []
|
|
23
|
+
for path in args.paths:
|
|
24
|
+
if does_path_match_patterns(path, args.exclude):
|
|
25
|
+
continue
|
|
26
|
+
|
|
27
|
+
if os.path.isfile(path):
|
|
28
|
+
filename = path
|
|
29
|
+
if os.path.splitext(filename)[1] == ".py":
|
|
30
|
+
python_filenames.append(filename)
|
|
31
|
+
else:
|
|
32
|
+
for path, _, filenames in os.walk(path):
|
|
33
|
+
if does_path_match_patterns(path, args.exclude):
|
|
34
|
+
continue
|
|
35
|
+
|
|
36
|
+
for f in filenames:
|
|
37
|
+
filename = os.path.join(path, f)
|
|
38
|
+
if (os.path.splitext(filename)[1] == ".py" and
|
|
39
|
+
not does_path_match_patterns(filename, args.exclude)):
|
|
40
|
+
python_filenames.append(filename)
|
|
41
|
+
|
|
42
|
+
return python_filenames
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def read_files(filenames: List[Filename]) -> Dict[Filename, FileContent]:
|
|
46
|
+
files = {}
|
|
47
|
+
for filename in filenames:
|
|
48
|
+
with open(filename, "r") as f:
|
|
49
|
+
files[filename] = f.read()
|
|
50
|
+
return files
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def parse_global_names(
|
|
54
|
+
files: Dict[Filename, FileContent], args: Args
|
|
55
|
+
) -> Dict[VariableName, Filename]:
|
|
56
|
+
patterns = [
|
|
57
|
+
r"^(\w+)\s*=",
|
|
58
|
+
r"^def\s+(\w+)\s*\(?",
|
|
59
|
+
r"^class\s+(\w+)\s*\(?:?",
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
global_variable_names = {}
|
|
63
|
+
for filename, file_content in files.items():
|
|
64
|
+
if does_path_match_patterns(filename, args.ignore_files):
|
|
65
|
+
continue
|
|
66
|
+
|
|
67
|
+
for pattern in patterns:
|
|
68
|
+
for line_nr, line in enumerate(file_content.split("\n"), 1):
|
|
69
|
+
if global_variable_name_in_file := re.findall(pattern, line):
|
|
70
|
+
variable_name = global_variable_name_in_file[0]
|
|
71
|
+
# TODO: could find the column name and add it to the output.
|
|
72
|
+
global_variable_names[variable_name] = f"{filename}:{line_nr}"
|
|
73
|
+
|
|
74
|
+
return global_variable_names
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def remove_comments_from_file_content(file_content: str) -> str:
|
|
78
|
+
# TODO: add a unit test for this feature.
|
|
79
|
+
file_lines_without_comments = []
|
|
80
|
+
for line in file_content:
|
|
81
|
+
if "#" in line:
|
|
82
|
+
# TODO: should ignore # in strings.
|
|
83
|
+
line = line.split("#", 1)[0]
|
|
84
|
+
file_lines_without_comments.append(line)
|
|
85
|
+
return "".join(file_lines_without_comments)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def remove_comments(files: Dict[Filename, FileContent]) -> Dict[Filename, FileContent]:
|
|
89
|
+
files_without_comments = {}
|
|
90
|
+
for filename, file_content in files.items():
|
|
91
|
+
files_without_comments[filename] = remove_comments_from_file_content(file_content)
|
|
92
|
+
return files_without_comments
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def find_unused_names(
|
|
96
|
+
files: Dict[str, str], global_names: Dict[VariableName, Filename], args: Args
|
|
97
|
+
) -> Dict[VariableName, Filename]:
|
|
98
|
+
name_occourencies = defaultdict(int)
|
|
99
|
+
for name in global_names.keys():
|
|
100
|
+
for file_content in files.values():
|
|
101
|
+
name_occourencies[name] += file_content.count(name)
|
|
102
|
+
|
|
103
|
+
unused_variables = {
|
|
104
|
+
name: filename
|
|
105
|
+
for name, filename in global_names.items()
|
|
106
|
+
if name_occourencies[name] == 1 and name not in args.ignore_names
|
|
107
|
+
}
|
|
108
|
+
return unused_variables
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def print_unused_names(unused_names: Dict[VariableName, Filename]):
|
|
112
|
+
# TODO: add line number to output
|
|
113
|
+
for name, filename in unused_names.items():
|
|
114
|
+
print(
|
|
115
|
+
filename,
|
|
116
|
+
"\033[91mF851\033[0m",
|
|
117
|
+
f"Global \033[1m{name}\033[0m is never used",
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def main(command_line_args: Optional[List[str]] = None) -> Dict[VariableName, Filename]:
|
|
122
|
+
args = parse_command_line_arguments(command_line_args)
|
|
123
|
+
|
|
124
|
+
python_filenames = find_python_filenames(args=args)
|
|
125
|
+
files = read_files(python_filenames)
|
|
126
|
+
files_without_comments = remove_comments(files)
|
|
127
|
+
|
|
128
|
+
global_names = parse_global_names(files, args=args)
|
|
129
|
+
unused_names = find_unused_names(files_without_comments, global_names, args=args)
|
|
130
|
+
|
|
131
|
+
print_unused_names(unused_names)
|
|
132
|
+
if unused_names:
|
|
133
|
+
sys.exit(1)
|
|
134
|
+
return unused_names
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
if __name__ == "__main__":
|
|
138
|
+
main()
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import List, Optional
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
FileContent = str
|
|
6
|
+
Filename = str # Contains full path to existing file
|
|
7
|
+
Pathname = str # Can contain wildewards
|
|
8
|
+
VariableName = str
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class Args:
|
|
13
|
+
paths: List[Pathname]
|
|
14
|
+
exclude: List[Pathname]
|
|
15
|
+
ignore_names: List[Pathname]
|
|
16
|
+
ignore_files: List[Pathname]
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from subprocess import PIPE, Popen
|
|
2
|
+
from unittest import TestCase
|
|
3
|
+
from unittest.mock import patch
|
|
4
|
+
|
|
5
|
+
from deadcode.cli import main
|
|
6
|
+
from deadcode.actions.argument_parsing import parse_command_line_arguments
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class FindDeadCodeTests(TestCase):
|
|
10
|
+
@patch("deadcode.cli.sys")
|
|
11
|
+
def test_run_dead_code_finder_with_a_subprocess_in_a_right_directory_main(self, sys_mock):
|
|
12
|
+
unused_names = main(["tests/test_case1"])
|
|
13
|
+
self.assertEqual(
|
|
14
|
+
unused_names,
|
|
15
|
+
{
|
|
16
|
+
"ANOTHER_GLOBAL_VARIABLE": "tests/test_case1/hello_world.py:4",
|
|
17
|
+
"AnotherUnusedClass": "tests/test_case1/hello_world.py:19",
|
|
18
|
+
"UNUSED_VARIABLE": "tests/test_case1/subdir/another_subdir/__init__.py:1",
|
|
19
|
+
"another_unused_function": "tests/test_case1/subdir/one_more_file.py:1",
|
|
20
|
+
"unused_global_variable": "tests/test_case1/hello_world.py:3",
|
|
21
|
+
},
|
|
22
|
+
)
|
|
23
|
+
sys_mock.exit.assert_called_once_with(1)
|
|
24
|
+
|
|
25
|
+
## TODO: Add test for . path
|
|
26
|
+
# def test_using_dot_to_find_files_recursively(self):
|
|
27
|
+
# unused_names = main([".", "--exclude=tests,venv"])
|
|
28
|
+
|
|
29
|
+
# self.assertEqual(
|
|
30
|
+
# unused_names, {
|
|
31
|
+
# 'not_used_function': './tests/module_a.py:5',
|
|
32
|
+
# 'FindDeadCodeTests': './tests/test_deadcode.py:7',
|
|
33
|
+
# # "ANOTHER_GLOBAL_VARIABLE": "tests/test_case1/hello_world.py:4",
|
|
34
|
+
# # "AnotherUnusedClass": "tests/test_case1/hello_world.py:19",
|
|
35
|
+
# # "UNUSED_VARIABLE": "tests/test_case1/subdir/another_subdir/__init__.py:1",
|
|
36
|
+
# # "another_unused_function": "tests/test_case1/subdir/one_more_file.py:1",
|
|
37
|
+
# # "unused_global_variable": "tests/test_case1/hello_world.py:3",
|
|
38
|
+
# },
|
|
39
|
+
# )
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class CommandLineArgParsingTests(TestCase):
|
|
43
|
+
def test_calling_with_one_paths_argument(self):
|
|
44
|
+
args = parse_command_line_arguments(["."])
|
|
45
|
+
self.assertEqual(args.paths, ["."])
|
|
46
|
+
self.assertEqual(args.exclude, [])
|
|
47
|
+
self.assertEqual(args.ignore_names, [])
|
|
48
|
+
self.assertEqual(args.ignore_files, [])
|
|
49
|
+
|
|
50
|
+
def test_calling_with_several_paths_argument(self):
|
|
51
|
+
args = parse_command_line_arguments([".", "tests"])
|
|
52
|
+
self.assertEqual(args.paths, [".", "tests"])
|
|
53
|
+
self.assertEqual(args.exclude, [])
|
|
54
|
+
self.assertEqual(args.ignore_names, [])
|
|
55
|
+
self.assertEqual(args.ignore_files, [])
|
|
56
|
+
|
|
57
|
+
def test_calling_with_exclude(self):
|
|
58
|
+
args = parse_command_line_arguments([".", "--exclude=tests,venv"])
|
|
59
|
+
self.assertEqual(args.paths, ["."])
|
|
60
|
+
self.assertEqual(args.exclude, ["tests", "venv"])
|
|
61
|
+
self.assertEqual(args.ignore_names, [])
|
|
62
|
+
self.assertEqual(args.ignore_files, [])
|
|
63
|
+
|
|
64
|
+
def test_calling_with_several_exclude_options(self):
|
|
65
|
+
args = parse_command_line_arguments([".", "--exclude=tests,venv", "--exclude=migrations"])
|
|
66
|
+
self.assertEqual(args.paths, ["."])
|
|
67
|
+
self.assertEqual(args.exclude, ["tests", "venv", "migrations"])
|
|
68
|
+
self.assertEqual(args.ignore_names, [])
|
|
69
|
+
self.assertEqual(args.ignore_files, [])
|
|
70
|
+
|
|
71
|
+
def test_ignore_names_and_ignore_files_command_line_argument_parsing(self):
|
|
72
|
+
args = parse_command_line_arguments([".", "--ignore-files=tests,venv", "--ignore-names=BaseTestCase,lambda_handler"])
|
|
73
|
+
self.assertEqual(args.paths, ["."])
|
|
74
|
+
self.assertEqual(args.exclude, [])
|
|
75
|
+
self.assertEqual(args.ignore_names, ["BaseTestCase", "lambda_handler"])
|
|
76
|
+
self.assertEqual(args.ignore_files, ["tests", "venv"])
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from typing import List, Optional
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def split_comma_separated_values(list_of_comma_separated_values: Optional[List[str]]) -> List[str]:
|
|
5
|
+
"""Concatenates lists into one list."""
|
|
6
|
+
if not list_of_comma_separated_values:
|
|
7
|
+
return []
|
|
8
|
+
list_of_comma_separated_values = flatten_list(list_of_comma_separated_values)
|
|
9
|
+
return flatten_list([v.split(",") for v in list_of_comma_separated_values])
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def flatten_list(list_of_lists: Optional[List[List[str]]]) -> List[str]:
|
|
13
|
+
"""Concatenates lists into one list."""
|
|
14
|
+
if not list_of_lists:
|
|
15
|
+
return []
|
|
16
|
+
return [elem for elem_list in list_of_lists for elem in elem_list]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "deadcode"
|
|
3
|
-
version = "0.0.
|
|
3
|
+
version = "0.0.2"
|
|
4
4
|
authors = [
|
|
5
5
|
{name = "Albertas Gimbutas", email = "albertasgim@gmail.com"},
|
|
6
6
|
]
|
|
@@ -22,18 +22,28 @@ dependencies = [
|
|
|
22
22
|
]
|
|
23
23
|
|
|
24
24
|
[project.scripts]
|
|
25
|
-
deadcode = "
|
|
25
|
+
deadcode = "deadcode.cli:main"
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Homepage = "https://github.com/albertas/deadcode"
|
|
29
|
+
Documentation = "https://deadcode.readthedocs.io/"
|
|
26
30
|
|
|
27
31
|
[build-system]
|
|
28
32
|
requires = ["hatchling"]
|
|
29
33
|
build-backend = "hatchling.build"
|
|
30
34
|
|
|
31
|
-
|
|
35
|
+
|
|
36
|
+
[tool.hatch.publish.index] # Hatch docs: https://hatch.pypa.io/latest/config/build/
|
|
32
37
|
disable = true
|
|
33
38
|
|
|
39
|
+
[tool.hatch.build]
|
|
40
|
+
include = ["**/*.py"]
|
|
41
|
+
exclude = ["tests/**"]
|
|
42
|
+
|
|
34
43
|
[tool.mypy]
|
|
35
44
|
exclude = ["build", "dist", "venv"]
|
|
36
45
|
python_version = "3.8"
|
|
46
|
+
strict = true
|
|
37
47
|
pretty = true
|
|
38
48
|
color_output = true
|
|
39
49
|
show_error_codes = true
|
|
@@ -43,13 +53,8 @@ warn_unused_configs = true
|
|
|
43
53
|
[tool.isort]
|
|
44
54
|
profile = "black"
|
|
45
55
|
multi_line_output = 3
|
|
56
|
+
known_third_party = ["deadcode"]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
|
|
46
60
|
|
|
47
|
-
# https://github.com/PyCQA/flake8/issues/234
|
|
48
|
-
# [tool.flake8]
|
|
49
|
-
# inline-quotes = "double"
|
|
50
|
-
# exclude = ["venv"]
|
|
51
|
-
# max-line-length = 120
|
|
52
|
-
# max-complexity = 10
|
|
53
|
-
# max-function-length= 120
|
|
54
|
-
# import-order-style = "google"
|
|
55
|
-
# ignore = "I101,"
|
deadcode-0.0.1/.flake8
DELETED
deadcode-0.0.1/Makefile
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
env: venv
|
|
2
|
-
venv/bin/pip install --upgrade pip
|
|
3
|
-
venv/bin/pip install -r requirements-dev.txt --use-pep517
|
|
4
|
-
venv/bin/pip install -e .
|
|
5
|
-
|
|
6
|
-
venv:
|
|
7
|
-
python3.9 -m venv venv
|
|
8
|
-
|
|
9
|
-
publish: venv
|
|
10
|
-
rm -fr dist/*
|
|
11
|
-
venv/bin/hatch build
|
|
12
|
-
venv/bin/hatch publish
|
|
13
|
-
|
|
14
|
-
flake8:
|
|
15
|
-
venv/bin/flake8 dead_code_finder
|
|
16
|
-
|
|
17
|
-
mypy:
|
|
18
|
-
venv/bin/mypy dead_code_finder
|
|
19
|
-
|
|
20
|
-
test:
|
|
21
|
-
venv/bin/pytest $(PYTEST_ME_PLEASE)
|
|
22
|
-
|
|
23
|
-
check: test flake8 mypy
|
deadcode-0.0.1/dead_code/cli.py
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
from typing import Dict, List, Optional
|
|
2
|
-
from collections import defaultdict
|
|
3
|
-
import os
|
|
4
|
-
import sys
|
|
5
|
-
import re
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
FileContent = str
|
|
9
|
-
Filename = str
|
|
10
|
-
Pathname = str
|
|
11
|
-
VariableName = str
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def find_python_filenames(paths: List[Pathname]):
|
|
15
|
-
"""Recursively searches for Python filenames in provided paths."""
|
|
16
|
-
filenames = []
|
|
17
|
-
for path in paths:
|
|
18
|
-
if os.path.isfile(path):
|
|
19
|
-
if os.path.splitext(path)[1] == ".py":
|
|
20
|
-
filenames.append(path)
|
|
21
|
-
else:
|
|
22
|
-
filenames += [os.path.join(dp, f) for dp, dn, filenames in os.walk(path)
|
|
23
|
-
for f in filenames if os.path.splitext(f)[1] == ".py"]
|
|
24
|
-
return filenames
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def read_files(filenames: List[Filename]) -> Dict[Filename, FileContent]:
|
|
28
|
-
files = {}
|
|
29
|
-
for filename in filenames:
|
|
30
|
-
with open(filename, "r") as f:
|
|
31
|
-
files[filename] = f.read()
|
|
32
|
-
return files
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def parse_global_names(files: Dict[Filename, FileContent]) -> Dict[VariableName, Filename]:
|
|
36
|
-
patterns = [r"^(\w+)\s*=", r"^def\s+(\w+)\s*\(?", r"^class\s+(\w+)\s*\(?:?"]
|
|
37
|
-
|
|
38
|
-
global_variable_names = {}
|
|
39
|
-
for filename, file_content in files.items():
|
|
40
|
-
for pattern in patterns:
|
|
41
|
-
global_variable_names_in_file = re.findall(pattern, file_content, re.MULTILINE)
|
|
42
|
-
for name in global_variable_names_in_file:
|
|
43
|
-
global_variable_names[name] = filename
|
|
44
|
-
return global_variable_names
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def find_unused_names(files: Dict[str, str], global_names: Dict[VariableName, Filename]
|
|
48
|
-
) -> Dict[VariableName, Filename]:
|
|
49
|
-
name_occourencies = defaultdict(int)
|
|
50
|
-
for name in global_names.keys():
|
|
51
|
-
for file_content in files.values():
|
|
52
|
-
name_occourencies[name] += file_content.count(name)
|
|
53
|
-
|
|
54
|
-
unused_variables = {name: filename for name, filename in global_names.items() if name_occourencies[name] == 1}
|
|
55
|
-
return unused_variables
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def print_unused_names(unused_names: Dict[VariableName, Filename]):
|
|
59
|
-
# TODO: add line number to output
|
|
60
|
-
print()
|
|
61
|
-
for name, filename in unused_names.items():
|
|
62
|
-
print(filename, "\033[91mF851\033[0m", f"Global \033[1m{name}\033[0m is never used")
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def main(args: Optional[List[str]] = None) -> None:
|
|
66
|
-
args = args or sys.argv[1:]
|
|
67
|
-
|
|
68
|
-
python_filenames = find_python_filenames(paths=args)
|
|
69
|
-
files = read_files(python_filenames)
|
|
70
|
-
|
|
71
|
-
global_names = parse_global_names(files)
|
|
72
|
-
unused_names = find_unused_names(files, global_names)
|
|
73
|
-
|
|
74
|
-
print_unused_names(unused_names)
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if __name__ == "__main__":
|
|
78
|
-
main()
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
from subprocess import PIPE, Popen
|
|
2
|
-
from unittest import TestCase
|
|
3
|
-
|
|
4
|
-
from dead_code.cli import main
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class DeadCodeTests(TestCase):
|
|
8
|
-
def test_main(self):
|
|
9
|
-
self.assertIsNone(main())
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class DetectUnimportedDefinitionInRoot(TestCase):
|
|
13
|
-
def test_run_dead_code_finder_with_a_subprocess_in_a_right_directory_main(self):
|
|
14
|
-
main(["tests/test_case1"])
|
|
15
|
-
|
|
16
|
-
# def testrun_dead_code_finder_with_a_subprocess_in_a_right_directory(self):
|
|
17
|
-
# result = Popen(["../venv/bin/dead-code-finder", "tests/test_case1"], stdout=PIPE).communicate()[0].decode()
|
deadcode-0.0.1/notes
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
##################
|
|
2
|
-
### Aggreed plan ##########################################
|
|
3
|
-
# Create a pip package with a good name, write some tests #
|
|
4
|
-
###########################################################
|
|
5
|
-
#
|
|
6
|
-
# [2023-06-05]
|
|
7
|
-
# Solution: load all the files
|
|
8
|
-
# Check accourance count of the definition in all the files (if more than 1) - is used.
|
|
9
|
-
# Can remove string and comments before checking.
|
|
10
|
-
#
|
|
11
|
-
# Subtasks:
|
|
12
|
-
# expand provided file paths
|
|
13
|
-
# read all files into memory (filename, content)
|
|
14
|
-
# is string in for 5k file a long operation?
|
|
15
|
-
# Should use a counter instead.
|
|
16
|
-
#
|
|
17
|
-
# Check SolarCube checks and Ruff if it detects such cases
|
|
18
|
-
# Check Vulture for false positives with Mainhive repository.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
#
|
|
23
|
-
|
|
24
|
-
SonarQube and ide linters: do they detect unused class definitions in other files?
|
|
25
|
-
#############
|
|
26
|
-
|
|
27
|
-
List of imported objects in dot separated form (how does import as work for this?):
|
|
28
|
-
sys.modules.keys()
|
|
29
|
-
#############
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
Go through files and detect, what has been defined in those files and whether all definitions are used.
|
|
33
|
-
entry point: lambdas_handler.py
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
Get Tree Representation of Loaded Python Modules (Python recipe)
|
|
38
|
-
https://code.activestate.com/recipes/442478-get-tree-representation-of-loaded-python-modules/
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
from xml.dom.minidom import Document
|
|
42
|
-
|
|
43
|
-
def get_tree_representation_of_loaded_python_modules(self):
|
|
44
|
-
modules = sys.modules.keys()
|
|
45
|
-
modules.sort()
|
|
46
|
-
|
|
47
|
-
tree_data = Document()
|
|
48
|
-
node = tree_data.createElement('li')
|
|
49
|
-
text = tree_data.createTextNode('Loaded Python Modules')
|
|
50
|
-
node.appendChild(text)
|
|
51
|
-
tree_data.appendChild(node)
|
|
52
|
-
map = {}
|
|
53
|
-
for module in modules:
|
|
54
|
-
ms = module.split('.')
|
|
55
|
-
path = node
|
|
56
|
-
for level in range(len(ms)):
|
|
57
|
-
nodes = map.get(level)
|
|
58
|
-
if nodes == None:
|
|
59
|
-
node1 = tree_data.createElement('ul')
|
|
60
|
-
node2 = tree_data.createElement('li')
|
|
61
|
-
textNode = tree_data.createTextNode(ms[level])
|
|
62
|
-
node2.appendChild(textNode)
|
|
63
|
-
node1.appendChild(node2)
|
|
64
|
-
path.appendChild(node1)
|
|
65
|
-
nodes = [node2]
|
|
66
|
-
map[level] = nodes
|
|
67
|
-
else:
|
|
68
|
-
node2 = None
|
|
69
|
-
for tmpNode in nodes:
|
|
70
|
-
if tmpNode.firstChild.data == ms[level]:
|
|
71
|
-
node2 = tmpNode
|
|
72
|
-
node1 = node2.parentNode
|
|
73
|
-
if node2 == None:
|
|
74
|
-
node1 = path.childNodes.item(1)
|
|
75
|
-
if node1==None:
|
|
76
|
-
node1 = tree_data.createElement('ul')
|
|
77
|
-
path.appendChild(node1)
|
|
78
|
-
node2 = tree_data.createElement('li')
|
|
79
|
-
textNode = tree_data.createTextNode(ms[level])
|
|
80
|
-
node2.appendChild(textNode)
|
|
81
|
-
node1.appendChild(node2)
|
|
82
|
-
nodes += [node2]
|
|
83
|
-
map[level] = nodes
|
|
84
|
-
path = node2
|
|
85
|
-
|
|
86
|
-
str = tree_data.toprettyxml()
|
|
87
|
-
str = str[len('<?xml version="1.0" ?>')+1:]
|
|
88
|
-
return str
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
-r requirements.txt
|
|
2
|
-
|
|
3
|
-
cognitive-complexity==1.3.0
|
|
4
|
-
flake8==5.0.4
|
|
5
|
-
flake8-annotations-complexity==0.0.7
|
|
6
|
-
flake8-bandit==4.1.1
|
|
7
|
-
flake8-blind-except==0.2.1
|
|
8
|
-
flake8-breakpoint==1.1.0
|
|
9
|
-
flake8-bugbear==22.10.27
|
|
10
|
-
flake8-builtins==2.0.1
|
|
11
|
-
flake8-cognitive-complexity==0.1.0
|
|
12
|
-
flake8-comprehensions==3.10.1
|
|
13
|
-
flake8-eradicate==1.4.0
|
|
14
|
-
flake8-expression-complexity==0.0.11
|
|
15
|
-
flake8-functions==0.0.7
|
|
16
|
-
flake8-import-order==0.18.1
|
|
17
|
-
flake8-isort==5.0.0
|
|
18
|
-
flake8-logging-format==0.8.1
|
|
19
|
-
flake8-print==5.0.0
|
|
20
|
-
flake8-quotes==3.3.1
|
|
21
|
-
flake8-variables-names==0.0.5
|
|
22
|
-
isort==5.10.1
|
|
23
|
-
mypy==0.990
|
|
24
|
-
mypy-extensions==0.4.3
|
|
25
|
-
pep8-naming==0.13.2
|
|
26
|
-
pylint==2.15.5
|
|
27
|
-
pytest==7.2.0
|
|
28
|
-
vulture==2.6
|
|
29
|
-
hatch==1.6.3
|
|
30
|
-
hatchling==1.11.1
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|