python-ta 2.9.1__tar.gz → 2.10.0__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.
- {python_ta-2.9.1/python_ta.egg-info → python_ta-2.10.0}/PKG-INFO +6 -3
- {python_ta-2.9.1 → python_ta-2.10.0}/README.md +2 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/pyproject.toml +2 -1
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/__init__.py +68 -22
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/cfg/__init__.py +1 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/cfg/cfg_generator.py +29 -6
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/cfg/graph.py +4 -4
- python_ta-2.10.0/python_ta/checkers/condition_logic_checker.py +115 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/checkers/pycodestyle_checker.py +2 -2
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/checkers/redundant_assignment_checker.py +4 -2
- python_ta-2.10.0/python_ta/checkers/static_type_checker.py +134 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/config/.pylintrc +15 -2
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/config/messages_config.toml +1 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/contracts/__init__.py +11 -11
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/debug/accumulation_table.py +9 -2
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/debug/snapshot.py +7 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/patches/transforms.py +4 -1
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/reporters/core.py +22 -3
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/reporters/html_reporter.py +13 -42
- python_ta-2.10.0/python_ta/reporters/html_server.py +58 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/reporters/node_printers.py +31 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/reporters/templates/script.js +32 -7
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/reporters/templates/template.html.jinja +0 -1
- {python_ta-2.9.1 → python_ta-2.10.0/python_ta.egg-info}/PKG-INFO +6 -3
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta.egg-info/SOURCES.txt +3 -1
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta.egg-info/requires.txt +2 -1
- {python_ta-2.9.1 → python_ta-2.10.0}/tests/test_black.py +9 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/tests/test_examples.py +17 -2
- {python_ta-2.9.1 → python_ta-2.10.0}/tests/test_init_logging.py +2 -2
- {python_ta-2.9.1 → python_ta-2.10.0}/tests/test_z3_visitor.py +5 -5
- python_ta-2.9.1/python_ta/reporters/templates/jquery-3.7.1.slim.min.js +0 -2
- {python_ta-2.9.1 → python_ta-2.10.0}/LICENSE +0 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/MANIFEST.in +0 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/__main__.py +0 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/cfg/visitor.py +0 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/checkers/__init__.py +0 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/checkers/forbidden_import_checker.py +0 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/checkers/forbidden_io_function_checker.py +0 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/checkers/forbidden_python_syntax_checker.py +0 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/checkers/global_variables_checker.py +0 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/checkers/inconsistent_or_missing_returns_checker.py +0 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/checkers/invalid_for_target_checker.py +0 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/checkers/invalid_name_checker.py +0 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/checkers/invalid_range_index_checker.py +0 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/checkers/missing_space_in_doctest_checker.py +0 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/checkers/one_iteration_checker.py +0 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/checkers/possibly_undefined_checker.py +0 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/checkers/shadowing_in_comprehension_checker.py +0 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/checkers/top_level_code_checker.py +0 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/checkers/type_annotation_checker.py +0 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/checkers/unmentioned_parameter_checker.py +0 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/checkers/unnecessary_indexing_checker.py +0 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/config/__init__.py +0 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/contracts/__main__.py +0 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/debug/__init__.py +0 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/debug/recursion_table.py +0 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/debug/snapshot_tracer.py +0 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/debug/webstepper/99ee5c67fd0c522b4b6a.png +0 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/debug/webstepper/fd6133fe40f4f90440d6.png +0 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/debug/webstepper/index.bundle.js +0 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/debug/webstepper/index.bundle.js.LICENSE.txt +0 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/debug/webstepper/index.html +0 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/patches/__init__.py +0 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/patches/checkers.py +0 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/patches/error_messages.py +0 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/patches/messages.py +0 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/reporters/__init__.py +0 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/reporters/color_reporter.py +0 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/reporters/json_reporter.py +0 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/reporters/plain_reporter.py +0 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/reporters/stat_reporter.py +0 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/reporters/templates/pyta_logo_markdown.png +0 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/reporters/templates/stylesheet.css +0 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/transforms/__init__.py +0 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/transforms/setendings.py +0 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/transforms/z3_visitor.py +0 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/upload.py +0 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/util/__init__.py +0 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/util/tree.py +0 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/utils.py +0 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/z3/__init__.py +0 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/z3/z3_parser.py +0 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta.egg-info/dependency_links.txt +0 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta.egg-info/entry_points.txt +0 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta.egg-info/not-zip-safe +0 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/python_ta.egg-info/top_level.txt +0 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/setup.cfg +0 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/setup.py +0 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/tests/test_check.py +0 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/tests/test_main.py +0 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/tests/test_setendings.py +0 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/tests/test_subclass_contracts.py +0 -0
- {python_ta-2.9.1 → python_ta-2.10.0}/tests/test_validate_invariants.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: python-ta
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.10.0
|
|
4
4
|
Summary: Code checking tool for teaching Python
|
|
5
5
|
Author-email: David Liu <david@cs.toronto.edu>
|
|
6
6
|
License: MIT
|
|
@@ -15,8 +15,9 @@ Requires-Dist: astroid~=3.3.5
|
|
|
15
15
|
Requires-Dist: click<9,>=8.0.1
|
|
16
16
|
Requires-Dist: colorama~=0.4.6
|
|
17
17
|
Requires-Dist: jinja2~=3.1.2
|
|
18
|
+
Requires-Dist: mypy~=1.13
|
|
18
19
|
Requires-Dist: pycodestyle~=2.11
|
|
19
|
-
Requires-Dist: pygments<2.
|
|
20
|
+
Requires-Dist: pygments<2.20,>=2.14
|
|
20
21
|
Requires-Dist: pylint~=3.3.1
|
|
21
22
|
Requires-Dist: requests<2.33,>=2.28
|
|
22
23
|
Requires-Dist: six
|
|
@@ -123,6 +124,7 @@ Simon Chen,
|
|
|
123
124
|
Freeman Cheng,
|
|
124
125
|
Ivan Chepelev,
|
|
125
126
|
Yianni Culmone,
|
|
127
|
+
Daniel Dervishi,
|
|
126
128
|
Nigel Fong,
|
|
127
129
|
Adam Gleizer,
|
|
128
130
|
Ibrahim Hasan,
|
|
@@ -134,6 +136,7 @@ David Kim,
|
|
|
134
136
|
Simeon Krastnikov,
|
|
135
137
|
Ryan Lee,
|
|
136
138
|
Christopher Li,
|
|
139
|
+
Hoi Ching (Herena) Li,
|
|
137
140
|
Hayley Lin,
|
|
138
141
|
Bruce Liu,
|
|
139
142
|
Merrick Liu,
|
|
@@ -78,6 +78,7 @@ Simon Chen,
|
|
|
78
78
|
Freeman Cheng,
|
|
79
79
|
Ivan Chepelev,
|
|
80
80
|
Yianni Culmone,
|
|
81
|
+
Daniel Dervishi,
|
|
81
82
|
Nigel Fong,
|
|
82
83
|
Adam Gleizer,
|
|
83
84
|
Ibrahim Hasan,
|
|
@@ -89,6 +90,7 @@ David Kim,
|
|
|
89
90
|
Simeon Krastnikov,
|
|
90
91
|
Ryan Lee,
|
|
91
92
|
Christopher Li,
|
|
93
|
+
Hoi Ching (Herena) Li,
|
|
92
94
|
Hayley Lin,
|
|
93
95
|
Bruce Liu,
|
|
94
96
|
Merrick Liu,
|
|
@@ -11,8 +11,9 @@ dependencies = [
|
|
|
11
11
|
"click >= 8.0.1, < 9",
|
|
12
12
|
"colorama ~= 0.4.6",
|
|
13
13
|
"jinja2 ~= 3.1.2",
|
|
14
|
+
"mypy ~= 1.13",
|
|
14
15
|
"pycodestyle ~= 2.11",
|
|
15
|
-
"pygments >= 2.14,< 2.
|
|
16
|
+
"pygments >= 2.14,< 2.20",
|
|
16
17
|
"pylint ~= 3.3.1",
|
|
17
18
|
"requests >= 2.28,< 2.33",
|
|
18
19
|
"six",
|
|
@@ -17,7 +17,7 @@ if __name__ == '__main__':
|
|
|
17
17
|
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
|
|
20
|
-
__version__ = "2.
|
|
20
|
+
__version__ = "2.10.0" # Version number
|
|
21
21
|
# First, remove underscore from builtins if it has been bound in the REPL.
|
|
22
22
|
# Must appear before other imports from pylint/python_ta.
|
|
23
23
|
import builtins
|
|
@@ -37,7 +37,7 @@ import tokenize
|
|
|
37
37
|
import webbrowser
|
|
38
38
|
from builtins import FileNotFoundError
|
|
39
39
|
from os import listdir
|
|
40
|
-
from typing import AnyStr, Generator, Optional, TextIO, Union
|
|
40
|
+
from typing import Any, AnyStr, Generator, Optional, TextIO, Union
|
|
41
41
|
|
|
42
42
|
import pylint.config
|
|
43
43
|
import pylint.lint
|
|
@@ -66,8 +66,8 @@ PYLINT_PATCHED = False
|
|
|
66
66
|
|
|
67
67
|
def check_errors(
|
|
68
68
|
module_name: Union[list[str], str] = "",
|
|
69
|
-
config: Union[dict, str] = "",
|
|
70
|
-
output: Optional[
|
|
69
|
+
config: Union[dict[str, Any], str] = "",
|
|
70
|
+
output: Optional[str] = None,
|
|
71
71
|
load_default_config: bool = True,
|
|
72
72
|
autoformat: Optional[bool] = False,
|
|
73
73
|
) -> PythonTaReporter:
|
|
@@ -84,12 +84,38 @@ def check_errors(
|
|
|
84
84
|
|
|
85
85
|
def check_all(
|
|
86
86
|
module_name: Union[list[str], str] = "",
|
|
87
|
-
config: Union[dict, str] = "",
|
|
88
|
-
output: Optional[
|
|
87
|
+
config: Union[dict[str, Any], str] = "",
|
|
88
|
+
output: Optional[str] = None,
|
|
89
89
|
load_default_config: bool = True,
|
|
90
90
|
autoformat: Optional[bool] = False,
|
|
91
91
|
) -> PythonTaReporter:
|
|
92
|
-
"""
|
|
92
|
+
"""Analyse one or more Python modules for code issues and display the results.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
module_name:
|
|
96
|
+
If an empty string (default), the module where this function is called is checked.
|
|
97
|
+
If a non-empty string, it is interpreted as a path to a single Python module or a
|
|
98
|
+
directory containing Python modules. If the latter, all Python modules in the directory
|
|
99
|
+
are checked.
|
|
100
|
+
If a list of strings, each string is interpreted as a path to a module or directory,
|
|
101
|
+
and all modules across all paths are checked.
|
|
102
|
+
config:
|
|
103
|
+
If a string, a path to a configuration file to use.
|
|
104
|
+
If a dictionary, a map of configuration options (each key is the name of an option).
|
|
105
|
+
output:
|
|
106
|
+
If provided, the PythonTA report is written to this path. Otherwise, the report
|
|
107
|
+
is written to standard out or automatically displayed in a web browser, depending
|
|
108
|
+
on which reporter is used.
|
|
109
|
+
load_default_config:
|
|
110
|
+
If True (default), additional configuration passed with the ``config`` option is
|
|
111
|
+
merged with the default PythonTA configuration file.
|
|
112
|
+
If False, the default PythonTA configuration is not used.
|
|
113
|
+
autoformat:
|
|
114
|
+
If True, autoformat all modules using the black formatting tool before analyzing code.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
The ``PythonTaReporter`` object that generated the report.
|
|
118
|
+
"""
|
|
93
119
|
return _check(
|
|
94
120
|
module_name=module_name,
|
|
95
121
|
level="all",
|
|
@@ -103,8 +129,8 @@ def check_all(
|
|
|
103
129
|
def _check(
|
|
104
130
|
module_name: Union[list[str], str] = "",
|
|
105
131
|
level: str = "all",
|
|
106
|
-
local_config: Union[dict, str] = "",
|
|
107
|
-
output: Optional[
|
|
132
|
+
local_config: Union[dict[str, Any], str] = "",
|
|
133
|
+
output: Optional[str] = None,
|
|
108
134
|
load_default_config: bool = True,
|
|
109
135
|
autoformat: Optional[bool] = False,
|
|
110
136
|
) -> PythonTaReporter:
|
|
@@ -116,13 +142,13 @@ def _check(
|
|
|
116
142
|
- no argument -- checks the python file containing the function call.
|
|
117
143
|
`level` is used to specify which checks should be made.
|
|
118
144
|
`local_config` is a dict of config options or string (config file name).
|
|
119
|
-
`output` is an absolute or relative path to capture pyta data output.
|
|
145
|
+
`output` is an absolute or relative path to capture pyta data output. If None, stdout is used.
|
|
120
146
|
`load_default_config` is used to specify whether to load the default .pylintrc file that comes
|
|
121
147
|
with PythonTA. It will load it by default.
|
|
122
148
|
`autoformat` is used to specify whether the black formatting tool is run. It is not run by default.
|
|
123
149
|
"""
|
|
124
150
|
# Configuring logger
|
|
125
|
-
logging.basicConfig(format="[%(levelname)s] %(message)s", level=logging.
|
|
151
|
+
logging.basicConfig(format="[%(levelname)s] %(message)s", level=logging.INFO)
|
|
126
152
|
|
|
127
153
|
linter = reset_linter(config=local_config, load_default_config=load_default_config)
|
|
128
154
|
current_reporter = linter.reporter
|
|
@@ -164,16 +190,13 @@ def _check(
|
|
|
164
190
|
)
|
|
165
191
|
|
|
166
192
|
if autoformat:
|
|
167
|
-
linelen = (
|
|
168
|
-
local_config["max-line-length"] if "max-line-length" in local_config else 88
|
|
169
|
-
)
|
|
170
193
|
subprocess.run(
|
|
171
194
|
[
|
|
172
195
|
sys.executable,
|
|
173
196
|
"-m",
|
|
174
197
|
"black",
|
|
175
198
|
"--skip-string-normalization",
|
|
176
|
-
"--line-length="
|
|
199
|
+
f"--line-length={linter.config.max_line_length}",
|
|
177
200
|
file_py,
|
|
178
201
|
],
|
|
179
202
|
encoding="utf-8",
|
|
@@ -204,12 +227,12 @@ def _check(
|
|
|
204
227
|
current_reporter.print_messages(level)
|
|
205
228
|
if linter.config.pyta_file_permission:
|
|
206
229
|
f_paths.append(file_py) # Appending paths for upload
|
|
207
|
-
logging.
|
|
230
|
+
logging.debug(
|
|
208
231
|
"File: {} was checked using the configuration file: {}".format(
|
|
209
232
|
file_py, linter.config_file
|
|
210
233
|
)
|
|
211
234
|
)
|
|
212
|
-
logging.
|
|
235
|
+
logging.debug(
|
|
213
236
|
"File: {} was checked using the messages-config file: {}".format(
|
|
214
237
|
file_py, messages_config_path
|
|
215
238
|
)
|
|
@@ -256,24 +279,43 @@ def reset_linter(
|
|
|
256
279
|
- If the config argument is a dictionary, apply those options afterward.
|
|
257
280
|
Do not re-use a linter object. Returns a new linter.
|
|
258
281
|
"""
|
|
282
|
+
|
|
259
283
|
# Tuple of custom options. Note: 'type' must map to a value equal a key in the pylint/config/option.py `VALIDATORS` dict.
|
|
260
284
|
new_checker_options = (
|
|
285
|
+
(
|
|
286
|
+
"server-port",
|
|
287
|
+
{
|
|
288
|
+
"default": 0,
|
|
289
|
+
"type": "int",
|
|
290
|
+
"metavar": "<port>",
|
|
291
|
+
"help": "Port number for the HTML report server",
|
|
292
|
+
},
|
|
293
|
+
),
|
|
294
|
+
(
|
|
295
|
+
"watch",
|
|
296
|
+
{
|
|
297
|
+
"default": False,
|
|
298
|
+
"type": "yn",
|
|
299
|
+
"metavar": "<yn>",
|
|
300
|
+
"help": "Run the HTML report server in persistent mode",
|
|
301
|
+
},
|
|
302
|
+
),
|
|
261
303
|
(
|
|
262
304
|
"pyta-number-of-messages",
|
|
263
305
|
{
|
|
264
306
|
"default": 0, # If the value is 0, all messages are displayed.
|
|
265
307
|
"type": "int",
|
|
266
308
|
"metavar": "<number_messages>",
|
|
267
|
-
"help": "
|
|
309
|
+
"help": "The maximum number of occurrences of each check to report.",
|
|
268
310
|
},
|
|
269
311
|
),
|
|
270
312
|
(
|
|
271
313
|
"pyta-template-file",
|
|
272
314
|
{
|
|
273
|
-
"default": "
|
|
315
|
+
"default": "",
|
|
274
316
|
"type": "string",
|
|
275
317
|
"metavar": "<pyta_reporter>",
|
|
276
|
-
"help": "
|
|
318
|
+
"help": "HTML template file for the HTMLReporter.",
|
|
277
319
|
},
|
|
278
320
|
),
|
|
279
321
|
(
|
|
@@ -320,7 +362,7 @@ def reset_linter(
|
|
|
320
362
|
"default": False,
|
|
321
363
|
"type": "yn",
|
|
322
364
|
"metavar": "<yn>",
|
|
323
|
-
"help": "
|
|
365
|
+
"help": "Allows or disallows 'pylint:' comments",
|
|
324
366
|
},
|
|
325
367
|
),
|
|
326
368
|
(
|
|
@@ -482,7 +524,11 @@ def _get_valid_files_to_check(module_name: Union[list[str], str]) -> Generator[A
|
|
|
482
524
|
|
|
483
525
|
|
|
484
526
|
def doc(msg_id: str) -> None:
|
|
485
|
-
"""Open
|
|
527
|
+
"""Open the PythonTA documentation page for the given error message id.
|
|
528
|
+
|
|
529
|
+
Args:
|
|
530
|
+
msg_id: The five-character error code, e.g. ``"E0401"``.
|
|
531
|
+
"""
|
|
486
532
|
msg_url = HELP_URL + "#" + msg_id.lower()
|
|
487
533
|
print("Opening {} in a browser.".format(msg_url))
|
|
488
534
|
webbrowser.open(msg_url)
|
|
@@ -4,6 +4,7 @@ Provides a function to generate and display the control flow graph of a given mo
|
|
|
4
4
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
|
+
import html
|
|
7
8
|
import importlib.util
|
|
8
9
|
import os.path
|
|
9
10
|
import sys
|
|
@@ -110,7 +111,7 @@ def _get_valid_file_path(mod: str = "") -> Optional[str]:
|
|
|
110
111
|
def _display(
|
|
111
112
|
cfgs: dict[nodes.NodeNG, ControlFlowGraph], filename: str, auto_open: bool = False
|
|
112
113
|
) -> None:
|
|
113
|
-
graph = graphviz.Digraph(name=filename, **GRAPH_OPTIONS)
|
|
114
|
+
graph = graphviz.Digraph(name=filename + ".gv", **GRAPH_OPTIONS)
|
|
114
115
|
for node, cfg in cfgs.items():
|
|
115
116
|
if isinstance(node, nodes.Module):
|
|
116
117
|
subgraph_label = "__main__"
|
|
@@ -129,7 +130,7 @@ def _display(
|
|
|
129
130
|
_visit(block, c, visited, cfg.end)
|
|
130
131
|
c.attr(label=subgraph_label, **SUBGRAPH_OPTIONS)
|
|
131
132
|
|
|
132
|
-
graph.render(filename, view=auto_open)
|
|
133
|
+
graph.render(outfile=filename + ".svg", view=auto_open)
|
|
133
134
|
|
|
134
135
|
|
|
135
136
|
def _visit(block: CFGBlock, graph: graphviz.Digraph, visited: set[int], end: CFGBlock) -> None:
|
|
@@ -140,15 +141,37 @@ def _visit(block: CFGBlock, graph: graphviz.Digraph, visited: set[int], end: CFG
|
|
|
140
141
|
if node_id in visited:
|
|
141
142
|
return
|
|
142
143
|
|
|
143
|
-
label = "
|
|
144
|
+
label = ""
|
|
145
|
+
fill_color = "white"
|
|
146
|
+
|
|
147
|
+
# Identify special cases
|
|
148
|
+
if len(block.statements) == 1:
|
|
149
|
+
stmt = block.statements[0]
|
|
150
|
+
if isinstance(stmt, nodes.Arguments):
|
|
151
|
+
label = f"{stmt.as_string()}\n"
|
|
152
|
+
fill_color = "palegreen"
|
|
153
|
+
elif isinstance(stmt.parent, nodes.If) and stmt is stmt.parent.test:
|
|
154
|
+
label = f"< if<U><B>{html.escape(stmt.as_string())}</B></U><BR/> >"
|
|
155
|
+
elif isinstance(stmt.parent, nodes.While) and stmt is stmt.parent.test:
|
|
156
|
+
label = f"< while<U><B>{html.escape(stmt.as_string())}</B></U><BR/> >"
|
|
157
|
+
elif isinstance(stmt.parent, nodes.For) and stmt is stmt.parent.iter:
|
|
158
|
+
label = f"< for {html.escape(stmt.parent.target.as_string())} in<U><B>{html.escape(stmt.as_string())}</B></U><BR/> >"
|
|
159
|
+
elif isinstance(stmt.parent, nodes.For) and stmt is stmt.parent.target:
|
|
160
|
+
label = f"< for<U><B>{html.escape(stmt.as_string())} </B></U> in {html.escape(stmt.parent.iter.as_string())}<BR/> >"
|
|
161
|
+
|
|
162
|
+
if not label: # Default
|
|
163
|
+
label = "\n".join([s.as_string() for s in block.statements]) + "\n"
|
|
164
|
+
|
|
144
165
|
# Need to escape backslashes explicitly.
|
|
145
166
|
label = label.replace("\\", "\\\\")
|
|
146
167
|
# \l is used for left alignment.
|
|
147
168
|
label = label.replace("\n", "\\l")
|
|
148
169
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
170
|
+
# Change the fill colour if block is the end of the cfg or unreachable
|
|
171
|
+
if block == end:
|
|
172
|
+
fill_color = "black"
|
|
173
|
+
elif not block.reachable:
|
|
174
|
+
fill_color = "grey93"
|
|
152
175
|
|
|
153
176
|
graph.node(node_id, label=label, fillcolor=fill_color, style="filled")
|
|
154
177
|
visited.add(node_id)
|
|
@@ -56,7 +56,7 @@ class ControlFlowGraph:
|
|
|
56
56
|
# z3 constraints of preconditions
|
|
57
57
|
precondition_constraints: List[ExprRef]
|
|
58
58
|
# map from variable names to z3 variables
|
|
59
|
-
|
|
59
|
+
z3_vars: Dict[str, ExprRef]
|
|
60
60
|
|
|
61
61
|
def __init__(self, cfg_id: int = 0) -> None:
|
|
62
62
|
self.block_count = 0
|
|
@@ -64,7 +64,7 @@ class ControlFlowGraph:
|
|
|
64
64
|
self.unreachable_blocks = set()
|
|
65
65
|
self.start = self.create_block()
|
|
66
66
|
self.end = self.create_block()
|
|
67
|
-
self.
|
|
67
|
+
self.z3_vars = {}
|
|
68
68
|
self.precondition_constraints = []
|
|
69
69
|
|
|
70
70
|
def add_arguments(self, args: Arguments) -> None:
|
|
@@ -76,7 +76,7 @@ class ControlFlowGraph:
|
|
|
76
76
|
# Parse types
|
|
77
77
|
parser = Z3Parser()
|
|
78
78
|
z3_vars = parser.parse_arguments(args)
|
|
79
|
-
self.
|
|
79
|
+
self.z3_vars.update(z3_vars)
|
|
80
80
|
|
|
81
81
|
def create_block(
|
|
82
82
|
self,
|
|
@@ -276,7 +276,7 @@ class ControlFlowGraph:
|
|
|
276
276
|
|
|
277
277
|
for path_id, path in enumerate(self.get_paths()):
|
|
278
278
|
# starting a new path
|
|
279
|
-
z3_environment = Z3Environment(self.
|
|
279
|
+
z3_environment = Z3Environment(self.z3_vars, self.precondition_constraints)
|
|
280
280
|
for edge in path:
|
|
281
281
|
# traverse through edge
|
|
282
282
|
if edge.condition is not None:
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Check for redundant/impossible If or While conditions in functions based on z3 constraints
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import Any, Union
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
import z3
|
|
11
|
+
|
|
12
|
+
from ..cfg.graph import Z3Environment
|
|
13
|
+
|
|
14
|
+
z3_dependency_available = True
|
|
15
|
+
except ImportError:
|
|
16
|
+
z3 = Any
|
|
17
|
+
Z3Environment = Any
|
|
18
|
+
z3_dependency_available = False
|
|
19
|
+
|
|
20
|
+
from astroid import nodes
|
|
21
|
+
from pylint.checkers import BaseChecker
|
|
22
|
+
from pylint.checkers.utils import only_required_for_messages
|
|
23
|
+
from pylint.lint import PyLinter
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ConditionLogicChecker(BaseChecker):
|
|
27
|
+
name = "redundant-condition"
|
|
28
|
+
msgs = {
|
|
29
|
+
"R9900": (
|
|
30
|
+
"""This condition will always evaluate to True.""",
|
|
31
|
+
"redundant-condition",
|
|
32
|
+
"Used when an If or While statement is always True. Requires the z3 configuration option to be True.",
|
|
33
|
+
),
|
|
34
|
+
"R9901": (
|
|
35
|
+
"""This condition will always evaluate to False.""",
|
|
36
|
+
"impossible-condition",
|
|
37
|
+
"Used when an If or While statement is always False. Requires the z3 configuration option to be True.",
|
|
38
|
+
),
|
|
39
|
+
}
|
|
40
|
+
options = (
|
|
41
|
+
(
|
|
42
|
+
"z3",
|
|
43
|
+
{
|
|
44
|
+
"default": False,
|
|
45
|
+
"type": "yn",
|
|
46
|
+
"metavar": "<y or n>",
|
|
47
|
+
"help": "Use Z3 to perform logical feasibility analysis in program control flow.",
|
|
48
|
+
},
|
|
49
|
+
),
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
@only_required_for_messages("redundant-condition", "impossible-condition")
|
|
53
|
+
def visit_if(self, node: nodes.If) -> None:
|
|
54
|
+
"""Visit if statement"""
|
|
55
|
+
self._check_condition(node)
|
|
56
|
+
|
|
57
|
+
@only_required_for_messages("redundant-condition", "impossible-condition")
|
|
58
|
+
def visit_while(self, node: nodes.While) -> None:
|
|
59
|
+
"""Visit while statement"""
|
|
60
|
+
self._check_condition(node)
|
|
61
|
+
|
|
62
|
+
def _check_condition(self, node: Union[nodes.If, nodes.While]) -> None:
|
|
63
|
+
"""Check whether a condition in an `if` or `while` statement is redundant
|
|
64
|
+
or impossible based on the feasible execution paths.
|
|
65
|
+
|
|
66
|
+
- A condition is redundant if for every feasible execution path
|
|
67
|
+
leading to the node, the condition must be True due to precedent constraints.
|
|
68
|
+
- A condition is impossible if for every feasible execution path
|
|
69
|
+
leading to the node, the condition must be False due to precedent constraints.
|
|
70
|
+
"""
|
|
71
|
+
if (
|
|
72
|
+
not hasattr(node, "cfg_block")
|
|
73
|
+
or not z3_dependency_available
|
|
74
|
+
or not self.linter.config.z3
|
|
75
|
+
):
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
node_block = node.cfg_block
|
|
79
|
+
|
|
80
|
+
# create node condition z3 constraint
|
|
81
|
+
condition_node = node.test
|
|
82
|
+
env = Z3Environment(node.frame().cfg.z3_vars, [])
|
|
83
|
+
z3_condition = env.parse_constraint(condition_node)
|
|
84
|
+
|
|
85
|
+
if z3_condition is None:
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
if all(
|
|
89
|
+
self._check_unsat(z3.And(*constraints), z3.Not(z3_condition))
|
|
90
|
+
for edge in (pred for pred in node_block.predecessors if pred.is_feasible)
|
|
91
|
+
for constraints in edge.z3_constraints.values()
|
|
92
|
+
):
|
|
93
|
+
self.add_message("redundant-condition", node=node.test)
|
|
94
|
+
|
|
95
|
+
if all(
|
|
96
|
+
self._check_unsat(z3.And(*constraints), z3_condition)
|
|
97
|
+
for edge in (pred for pred in node_block.predecessors if pred.is_feasible)
|
|
98
|
+
for constraints in edge.z3_constraints.values()
|
|
99
|
+
):
|
|
100
|
+
self.add_message("impossible-condition", node=node.test)
|
|
101
|
+
|
|
102
|
+
def _check_unsat(self, prev_constraints: z3.ExprRef, node_constraint: z3.ExprRef) -> bool:
|
|
103
|
+
"""Check if the conjunction of the given constraints is unsatisfiable.
|
|
104
|
+
|
|
105
|
+
- prev_constraints (z3.ExprRef): Constraints from previous nodes.
|
|
106
|
+
- node_constraint (z3.ExprRef): The condition to check at the current node.
|
|
107
|
+
"""
|
|
108
|
+
solver = z3.Solver()
|
|
109
|
+
solver.add(z3.And(prev_constraints, node_constraint))
|
|
110
|
+
return solver.check() == z3.unsat
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def register(linter: PyLinter) -> None:
|
|
114
|
+
"""Required method to auto-register this checker to the linter."""
|
|
115
|
+
linter.register_checker(ConditionLogicChecker(linter))
|
|
@@ -28,9 +28,9 @@ class PycodestyleChecker(BaseRawFileChecker):
|
|
|
28
28
|
),
|
|
29
29
|
)
|
|
30
30
|
|
|
31
|
-
def process_module(self, node: nodes.
|
|
31
|
+
def process_module(self, node: nodes.Module) -> None:
|
|
32
32
|
style_guide = pycodestyle.StyleGuide(
|
|
33
|
-
paths=[node.
|
|
33
|
+
paths=[node.file],
|
|
34
34
|
reporter=JSONReport,
|
|
35
35
|
ignore=self.linter.config.pycodestyle_ignore,
|
|
36
36
|
)
|
|
@@ -76,7 +76,7 @@ class RedundantAssignmentChecker(BaseChecker):
|
|
|
76
76
|
@only_required_for_messages("redundant-assignment")
|
|
77
77
|
def visit_annassign(self, node: nodes.AnnAssign) -> None:
|
|
78
78
|
"""Visit the annotated assign node"""
|
|
79
|
-
if node in self._redundant_assignment:
|
|
79
|
+
if node.value is not None and node in self._redundant_assignment:
|
|
80
80
|
self.add_message("redundant-assignment", node=node, args=node.target.name)
|
|
81
81
|
|
|
82
82
|
def visit_module(self, node: nodes.Module) -> None:
|
|
@@ -151,7 +151,9 @@ class RedundantAssignmentChecker(BaseChecker):
|
|
|
151
151
|
parent = (
|
|
152
152
|
node.parent.parent if isinstance(node.parent, nodes.Tuple) else node.parent
|
|
153
153
|
)
|
|
154
|
-
if
|
|
154
|
+
if isinstance(parent, nodes.AnnAssign) and parent.value is None:
|
|
155
|
+
continue
|
|
156
|
+
elif node.name in gen.difference(kill):
|
|
155
157
|
# add redundant assignment
|
|
156
158
|
if parent not in self._redundant_assignment:
|
|
157
159
|
self._redundant_assignment[parent] = []
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from astroid import nodes
|
|
5
|
+
from mypy import api
|
|
6
|
+
from pylint.checkers import BaseRawFileChecker
|
|
7
|
+
from pylint.lint import PyLinter
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class StaticTypeChecker(BaseRawFileChecker):
|
|
11
|
+
"""Checker for static type checking using Mypy."""
|
|
12
|
+
|
|
13
|
+
name = "static_type_checker"
|
|
14
|
+
msgs = {
|
|
15
|
+
"E9951": (
|
|
16
|
+
"Argument %s to %s has incompatible type %s; expected %s",
|
|
17
|
+
"incompatible-argument-type",
|
|
18
|
+
"Used when a function argument has an incompatible type",
|
|
19
|
+
),
|
|
20
|
+
"E9952": (
|
|
21
|
+
"Incompatible types in assignment (expression has type %s, variable has type %s)",
|
|
22
|
+
"incompatible-assignment",
|
|
23
|
+
"Used when there is an incompatible assignment",
|
|
24
|
+
),
|
|
25
|
+
"E9953": (
|
|
26
|
+
"List item %s has incompatible type %s; expected %s",
|
|
27
|
+
"list-item-type-mismatch",
|
|
28
|
+
"Used when a list item has an incompatible type",
|
|
29
|
+
),
|
|
30
|
+
"E9954": (
|
|
31
|
+
"Unsupported operand types for %s (%s and %s)",
|
|
32
|
+
"unsupported-operand-types",
|
|
33
|
+
"Used when an operation is attempted between incompatible types",
|
|
34
|
+
),
|
|
35
|
+
"E9955": (
|
|
36
|
+
"Item of type %s in Union has no attribute %s",
|
|
37
|
+
"union-attr-error",
|
|
38
|
+
"Used when accessing an attribute that may not exist on a Union type",
|
|
39
|
+
),
|
|
40
|
+
"E9956": (
|
|
41
|
+
"Dict entry %s has incompatible type %s: %s; expected %s: %s",
|
|
42
|
+
"dict-item-type-mismatch",
|
|
43
|
+
"Used when a dictionary entry has an incompatible key or value type",
|
|
44
|
+
),
|
|
45
|
+
}
|
|
46
|
+
options = (
|
|
47
|
+
(
|
|
48
|
+
"mypy-options",
|
|
49
|
+
{
|
|
50
|
+
"default": ["ignore-missing-imports", "follow-imports=skip"],
|
|
51
|
+
"type": "csv",
|
|
52
|
+
"metavar": "<mypy options>",
|
|
53
|
+
"help": "List of configuration flags for mypy",
|
|
54
|
+
},
|
|
55
|
+
),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
COMMON_PATTERN = (
|
|
59
|
+
r"^(?P<file>[^:]+):(?P<start_line>\d+):(?P<start_col>\d+):"
|
|
60
|
+
r"(?P<end_line>\d+):(?P<end_col>\d+): error: (?P<message>.+) \[(?P<code>[\w-]+)\]"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
SPECIFIC_PATTERNS = {
|
|
64
|
+
"arg-type": re.compile(
|
|
65
|
+
r"Argument (?P<arg_num>\d+) to \"(?P<func_name>[^\"]+)\" has incompatible type \"(?P<incomp_type>[^\"]+)\"; expected \"(?P<exp_type>[^\"]+)\""
|
|
66
|
+
),
|
|
67
|
+
"assignment": re.compile(
|
|
68
|
+
r"Incompatible types in assignment \(expression has type \"(?P<expr_type>[^\"]+)\", variable has type \"(?P<var_type>[^\"]+)\"\)"
|
|
69
|
+
),
|
|
70
|
+
"list-item": re.compile(
|
|
71
|
+
r"List item (?P<item_index>\d+) has incompatible type \"(?P<item_type>[^\"]+)\"; expected \"(?P<exp_type>[^\"]+)\""
|
|
72
|
+
),
|
|
73
|
+
"operator": re.compile(
|
|
74
|
+
r"Unsupported operand types for (?P<operator>\S+) \(\"(?P<left_type>[^\"]+)\" and \"(?P<right_type>[^\"]+)\"\)"
|
|
75
|
+
),
|
|
76
|
+
"union-attr": re.compile(
|
|
77
|
+
r"Item \"(?P<item_type>[^\"]+)\" of \"[^\"]+\" has no attribute \"(?P<attribute>[^\"]+)\""
|
|
78
|
+
),
|
|
79
|
+
"dict-item": re.compile(
|
|
80
|
+
r"Dict entry (?P<entry_index>\d+) has incompatible type \"(?P<key_type>[^\"]+)\": \"(?P<value_type>[^\"]+)\"; expected \"(?P<exp_key_type>[^\"]+)\": \"(?P<exp_value_type>[^\"]+)\""
|
|
81
|
+
),
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
def process_module(self, node: nodes.Module) -> None:
|
|
85
|
+
"""Run Mypy on the current file and handle type errors."""
|
|
86
|
+
filename = node.file
|
|
87
|
+
|
|
88
|
+
mypy_options = ["--show-error-end"]
|
|
89
|
+
for arg in self.linter.config.mypy_options:
|
|
90
|
+
mypy_options.append("--" + arg)
|
|
91
|
+
|
|
92
|
+
result, _, _ = api.run([filename] + mypy_options)
|
|
93
|
+
|
|
94
|
+
for line in result.splitlines():
|
|
95
|
+
common_match = re.match(self.COMMON_PATTERN, line)
|
|
96
|
+
if not common_match:
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
common_data = common_match.groupdict()
|
|
100
|
+
|
|
101
|
+
specific_pattern = self.SPECIFIC_PATTERNS.get(common_data["code"])
|
|
102
|
+
if specific_pattern:
|
|
103
|
+
specific_match = specific_pattern.search(common_data["message"])
|
|
104
|
+
if specific_match:
|
|
105
|
+
specific_data = specific_match.groupdict()
|
|
106
|
+
self._add_message(common_data, specific_data)
|
|
107
|
+
|
|
108
|
+
def _add_message(self, common_data: dict, specific_data: dict) -> None:
|
|
109
|
+
"""Add a message using the common and specific data."""
|
|
110
|
+
code = common_data["code"]
|
|
111
|
+
code_to_msgid = {
|
|
112
|
+
"arg-type": "incompatible-argument-type",
|
|
113
|
+
"assignment": "incompatible-assignment",
|
|
114
|
+
"list-item": "list-item-type-mismatch",
|
|
115
|
+
"operator": "unsupported-operand-types",
|
|
116
|
+
"union-attr": "union-attr-error",
|
|
117
|
+
"dict-item": "dict-item-type-mismatch",
|
|
118
|
+
}
|
|
119
|
+
msgid = code_to_msgid.get(code)
|
|
120
|
+
if not msgid:
|
|
121
|
+
return
|
|
122
|
+
self.add_message(
|
|
123
|
+
msgid,
|
|
124
|
+
line=int(common_data["start_line"]),
|
|
125
|
+
col_offset=int(common_data["start_col"]),
|
|
126
|
+
end_lineno=int(common_data["end_line"]),
|
|
127
|
+
end_col_offset=int(common_data["end_col"]),
|
|
128
|
+
args=tuple(specific_data.values()),
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def register(linter: PyLinter) -> None:
|
|
133
|
+
"""Register the static type checker with the PyLinter."""
|
|
134
|
+
linter.register_checker(StaticTypeChecker(linter))
|