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.
Files changed (93) hide show
  1. {python_ta-2.9.1/python_ta.egg-info → python_ta-2.10.0}/PKG-INFO +6 -3
  2. {python_ta-2.9.1 → python_ta-2.10.0}/README.md +2 -0
  3. {python_ta-2.9.1 → python_ta-2.10.0}/pyproject.toml +2 -1
  4. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/__init__.py +68 -22
  5. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/cfg/__init__.py +1 -0
  6. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/cfg/cfg_generator.py +29 -6
  7. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/cfg/graph.py +4 -4
  8. python_ta-2.10.0/python_ta/checkers/condition_logic_checker.py +115 -0
  9. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/checkers/pycodestyle_checker.py +2 -2
  10. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/checkers/redundant_assignment_checker.py +4 -2
  11. python_ta-2.10.0/python_ta/checkers/static_type_checker.py +134 -0
  12. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/config/.pylintrc +15 -2
  13. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/config/messages_config.toml +1 -0
  14. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/contracts/__init__.py +11 -11
  15. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/debug/accumulation_table.py +9 -2
  16. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/debug/snapshot.py +7 -0
  17. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/patches/transforms.py +4 -1
  18. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/reporters/core.py +22 -3
  19. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/reporters/html_reporter.py +13 -42
  20. python_ta-2.10.0/python_ta/reporters/html_server.py +58 -0
  21. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/reporters/node_printers.py +31 -0
  22. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/reporters/templates/script.js +32 -7
  23. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/reporters/templates/template.html.jinja +0 -1
  24. {python_ta-2.9.1 → python_ta-2.10.0/python_ta.egg-info}/PKG-INFO +6 -3
  25. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta.egg-info/SOURCES.txt +3 -1
  26. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta.egg-info/requires.txt +2 -1
  27. {python_ta-2.9.1 → python_ta-2.10.0}/tests/test_black.py +9 -0
  28. {python_ta-2.9.1 → python_ta-2.10.0}/tests/test_examples.py +17 -2
  29. {python_ta-2.9.1 → python_ta-2.10.0}/tests/test_init_logging.py +2 -2
  30. {python_ta-2.9.1 → python_ta-2.10.0}/tests/test_z3_visitor.py +5 -5
  31. python_ta-2.9.1/python_ta/reporters/templates/jquery-3.7.1.slim.min.js +0 -2
  32. {python_ta-2.9.1 → python_ta-2.10.0}/LICENSE +0 -0
  33. {python_ta-2.9.1 → python_ta-2.10.0}/MANIFEST.in +0 -0
  34. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/__main__.py +0 -0
  35. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/cfg/visitor.py +0 -0
  36. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/checkers/__init__.py +0 -0
  37. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/checkers/forbidden_import_checker.py +0 -0
  38. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/checkers/forbidden_io_function_checker.py +0 -0
  39. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/checkers/forbidden_python_syntax_checker.py +0 -0
  40. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/checkers/global_variables_checker.py +0 -0
  41. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/checkers/inconsistent_or_missing_returns_checker.py +0 -0
  42. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/checkers/invalid_for_target_checker.py +0 -0
  43. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/checkers/invalid_name_checker.py +0 -0
  44. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/checkers/invalid_range_index_checker.py +0 -0
  45. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/checkers/missing_space_in_doctest_checker.py +0 -0
  46. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/checkers/one_iteration_checker.py +0 -0
  47. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/checkers/possibly_undefined_checker.py +0 -0
  48. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/checkers/shadowing_in_comprehension_checker.py +0 -0
  49. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/checkers/top_level_code_checker.py +0 -0
  50. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/checkers/type_annotation_checker.py +0 -0
  51. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/checkers/unmentioned_parameter_checker.py +0 -0
  52. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/checkers/unnecessary_indexing_checker.py +0 -0
  53. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/config/__init__.py +0 -0
  54. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/contracts/__main__.py +0 -0
  55. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/debug/__init__.py +0 -0
  56. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/debug/recursion_table.py +0 -0
  57. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/debug/snapshot_tracer.py +0 -0
  58. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/debug/webstepper/99ee5c67fd0c522b4b6a.png +0 -0
  59. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/debug/webstepper/fd6133fe40f4f90440d6.png +0 -0
  60. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/debug/webstepper/index.bundle.js +0 -0
  61. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/debug/webstepper/index.bundle.js.LICENSE.txt +0 -0
  62. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/debug/webstepper/index.html +0 -0
  63. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/patches/__init__.py +0 -0
  64. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/patches/checkers.py +0 -0
  65. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/patches/error_messages.py +0 -0
  66. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/patches/messages.py +0 -0
  67. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/reporters/__init__.py +0 -0
  68. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/reporters/color_reporter.py +0 -0
  69. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/reporters/json_reporter.py +0 -0
  70. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/reporters/plain_reporter.py +0 -0
  71. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/reporters/stat_reporter.py +0 -0
  72. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/reporters/templates/pyta_logo_markdown.png +0 -0
  73. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/reporters/templates/stylesheet.css +0 -0
  74. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/transforms/__init__.py +0 -0
  75. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/transforms/setendings.py +0 -0
  76. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/transforms/z3_visitor.py +0 -0
  77. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/upload.py +0 -0
  78. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/util/__init__.py +0 -0
  79. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/util/tree.py +0 -0
  80. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/utils.py +0 -0
  81. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/z3/__init__.py +0 -0
  82. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta/z3/z3_parser.py +0 -0
  83. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta.egg-info/dependency_links.txt +0 -0
  84. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta.egg-info/entry_points.txt +0 -0
  85. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta.egg-info/not-zip-safe +0 -0
  86. {python_ta-2.9.1 → python_ta-2.10.0}/python_ta.egg-info/top_level.txt +0 -0
  87. {python_ta-2.9.1 → python_ta-2.10.0}/setup.cfg +0 -0
  88. {python_ta-2.9.1 → python_ta-2.10.0}/setup.py +0 -0
  89. {python_ta-2.9.1 → python_ta-2.10.0}/tests/test_check.py +0 -0
  90. {python_ta-2.9.1 → python_ta-2.10.0}/tests/test_main.py +0 -0
  91. {python_ta-2.9.1 → python_ta-2.10.0}/tests/test_setendings.py +0 -0
  92. {python_ta-2.9.1 → python_ta-2.10.0}/tests/test_subclass_contracts.py +0 -0
  93. {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
1
+ Metadata-Version: 2.2
2
2
  Name: python-ta
3
- Version: 2.9.1
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.19,>=2.14
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.19",
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.9.1" # Version number
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[TextIO] = None,
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[TextIO] = None,
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
- """Check a module for errors and style warnings, printing a report."""
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[TextIO] = None,
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. Default std out.
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.NOTSET)
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=" + str(linelen),
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.info(
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.info(
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": "Display a certain number of messages to the user, without overwhelming them.",
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": "template.html.jinja",
315
+ "default": "",
274
316
  "type": "string",
275
317
  "metavar": "<pyta_reporter>",
276
- "help": "Template file for html format of htmlreporter output.",
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": "allows or disallows pylint: comments",
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 a webpage explaining the error for the given message."""
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)
@@ -1,2 +1,3 @@
1
+ from .cfg_generator import generate_cfg
1
2
  from .graph import *
2
3
  from .visitor import *
@@ -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 = "\n".join([s.as_string() for s in block.statements]) + "\n"
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
- fill_color = "grey93" if not block.reachable else "white"
150
- # Change the fill colour if block is the end of the cfg
151
- fill_color = "black" if block == end else fill_color
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
- _z3_vars: Dict[str, ExprRef]
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._z3_vars = {}
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._z3_vars.update(z3_vars)
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._z3_vars, self.precondition_constraints)
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.NodeNG) -> None:
31
+ def process_module(self, node: nodes.Module) -> None:
32
32
  style_guide = pycodestyle.StyleGuide(
33
- paths=[node.stream().name],
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 node.name in gen.difference(kill):
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))