-
Notifications
You must be signed in to change notification settings - Fork 63
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Enable checks based on mypy static type checking (#1115)
Adds mypy to project dependencies.
- Loading branch information
Showing
13 changed files
with
526 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
11 changes: 11 additions & 0 deletions
11
examples/custom_checkers/static_type_checker_examples/e9951_incompatible_argument_type.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
def calculate_area(radius: float) -> float: | ||
"""Calculate the area of a circle with the given radius""" | ||
return 3.14159 * radius * radius | ||
|
||
area = calculate_area("five") # Error: Function argument should be float, but got str | ||
|
||
def convert_to_upper(text: str) -> str: | ||
"""Convert the given text to uppercase""" | ||
return text.upper() | ||
|
||
result = convert_to_upper(5) # Error: Function argument should be str, but got int |
4 changes: 4 additions & 0 deletions
4
examples/custom_checkers/static_type_checker_examples/e9952_incompatible_assignment.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
age: int = 25 | ||
age = "twenty-five" # Error: Incompatible types in assignment (expression has type "str", variable has type "int") | ||
|
||
count: int = "ten" # Error: Incompatible types in assignment (expression has type "str", variable has type "int") |
5 changes: 5 additions & 0 deletions
5
examples/custom_checkers/static_type_checker_examples/e9953_list_item_type_mismatch.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
names: list[str] = ["Alice", "Bob", 3] # Error: List item 2 has incompatible type "int"; expected "str" | ||
|
||
numbers: list[int] = [1, 2, "three"] # Error: List item 2 has incompatible type "str"; expected "int" | ||
|
||
mixed: list[float] = [1.1, 2.2, "3.3"] # Error: List item 2 has incompatible type "str"; expected "float" |
5 changes: 5 additions & 0 deletions
5
examples/custom_checkers/static_type_checker_examples/e9954_unsupported_operand_types.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
result = "hello" - 5 # Error: Unsupported operand types for - ("str" and "int") | ||
|
||
total = 10 + "20" # Error: Unsupported operand types for + ("int" and "str") | ||
|
||
value = 5.5 * "3" # Error: Unsupported operand types for * ("float" and "str") |
9 changes: 9 additions & 0 deletions
9
examples/custom_checkers/static_type_checker_examples/e9955_union_attr_error.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
from typing import Union | ||
|
||
def get_status(status: Union[str, int, float]) -> str: | ||
return (status | ||
.upper()) # Error: Item "int" of "str | int | float" has no attribute "upper" | ||
# Error: Item "float" of "str | int | float" has no attribute "upper" | ||
|
||
def get_keys(data: Union[dict, list]) -> list: | ||
return data.keys() # Error: Item "list" of "dict | list" has no attribute "keys" |
3 changes: 3 additions & 0 deletions
3
examples/custom_checkers/static_type_checker_examples/e9956_dict_item_type_mismatch.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
data: dict[int, str] = {1: "one", 2: "two", "three": 3} # Error: Dict entry 2 has incompatible type "str": "int"; expected "int": "str" | ||
|
||
info: dict[str, float] = {"pi": 3.14, "e": 2.71, 42: "1.618"} # Error: Dict entry 2 has incompatible type "int": "str"; expected "str": "float" |
45 changes: 45 additions & 0 deletions
45
examples/custom_checkers/static_type_checker_examples/static_type_checker_no_error.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
# Correct Function Usage | ||
def calculate_area(radius: float) -> float: | ||
"""Calculate the area of a circle.""" | ||
return 3.14159 * radius * radius | ||
|
||
area = calculate_area(5.0) | ||
|
||
# Proper Variable Assignments | ||
name: str = "Alice" | ||
age: int = 30 | ||
height: float = 5.9 | ||
|
||
# Correct List Usage | ||
names: list[str] = ["Alice", "Bob", "Charlie"] | ||
numbers: list[int] = [1, 2, 3, 4] | ||
prices: list[float] = [9.99, 19.99, 29.99] | ||
|
||
# Correct Dict Usage | ||
data: dict[int, str] = {1: "one", 2: "two", 3: "three"} | ||
config: dict[str, float] = {"pi": 3.14, "e": 2.71} | ||
|
||
# Valid Operations | ||
result = 10 + 5 | ||
value = 3.5 * 2 | ||
combined_name = "Alice" + " Bob" | ||
|
||
# Union with Compatible Attribute Access | ||
from typing import Union | ||
|
||
def get_length(value: Union[str, list]) -> int: | ||
return len(value) | ||
|
||
length_of_name = get_length("Alice") | ||
length_of_list = get_length([1, 2, 3]) | ||
|
||
# Valid Empty Structures | ||
empty_list: list[int] = [] | ||
empty_dict: dict[str, int] = {} | ||
|
||
# Functions with Default Arguments | ||
def greet(name: str = "Guest") -> str: | ||
return f"Hello, {name}" | ||
|
||
greeting = greet() | ||
custom_greeting = greet("Alice") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
import re | ||
from typing import Optional | ||
|
||
from astroid import nodes | ||
from mypy import api | ||
from pylint.checkers import BaseRawFileChecker | ||
from pylint.lint import PyLinter | ||
|
||
|
||
class StaticTypeChecker(BaseRawFileChecker): | ||
"""Checker for static type checking using Mypy.""" | ||
|
||
name = "static_type_checker" | ||
msgs = { | ||
"E9951": ( | ||
"Argument %s to %s has incompatible type %s; expected %s", | ||
"incompatible-argument-type", | ||
"Used when a function argument has an incompatible type", | ||
), | ||
"E9952": ( | ||
"Incompatible types in assignment (expression has type %s, variable has type %s)", | ||
"incompatible-assignment", | ||
"Used when there is an incompatible assignment", | ||
), | ||
"E9953": ( | ||
"List item %s has incompatible type %s; expected %s", | ||
"list-item-type-mismatch", | ||
"Used when a list item has an incompatible type", | ||
), | ||
"E9954": ( | ||
"Unsupported operand types for %s (%s and %s)", | ||
"unsupported-operand-types", | ||
"Used when an operation is attempted between incompatible types", | ||
), | ||
"E9955": ( | ||
"Item of type %s in Union has no attribute %s", | ||
"union-attr-error", | ||
"Used when accessing an attribute that may not exist on a Union type", | ||
), | ||
"E9956": ( | ||
"Dict entry %s has incompatible type %s: %s; expected %s: %s", | ||
"dict-item-type-mismatch", | ||
"Used when a dictionary entry has an incompatible key or value type", | ||
), | ||
} | ||
|
||
COMMON_PATTERN = ( | ||
r"^(?P<file>[^:]+):(?P<start_line>\d+):(?P<start_col>\d+):" | ||
r"(?P<end_line>\d+):(?P<end_col>\d+): error: (?P<message>.+) \[(?P<code>[\w-]+)\]" | ||
) | ||
|
||
SPECIFIC_PATTERNS = { | ||
"arg-type": re.compile( | ||
r"Argument (?P<arg_num>\d+) to \"(?P<func_name>[^\"]+)\" has incompatible type \"(?P<incomp_type>[^\"]+)\"; expected \"(?P<exp_type>[^\"]+)\"" | ||
), | ||
"assignment": re.compile( | ||
r"Incompatible types in assignment \(expression has type \"(?P<expr_type>[^\"]+)\", variable has type \"(?P<var_type>[^\"]+)\"\)" | ||
), | ||
"list-item": re.compile( | ||
r"List item (?P<item_index>\d+) has incompatible type \"(?P<item_type>[^\"]+)\"; expected \"(?P<exp_type>[^\"]+)\"" | ||
), | ||
"operator": re.compile( | ||
r"Unsupported operand types for (?P<operator>\S+) \(\"(?P<left_type>[^\"]+)\" and \"(?P<right_type>[^\"]+)\"\)" | ||
), | ||
"union-attr": re.compile( | ||
r"Item \"(?P<item_type>[^\"]+)\" of \"[^\"]+\" has no attribute \"(?P<attribute>[^\"]+)\"" | ||
), | ||
"dict-item": re.compile( | ||
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>[^\"]+)\"" | ||
), | ||
} | ||
|
||
def process_module(self, node: nodes.NodeNG) -> None: | ||
"""Run Mypy on the current file and handle type errors.""" | ||
filename = node.stream().name | ||
mypy_options = [ | ||
"--ignore-missing-imports", | ||
"--show-error-end", | ||
] | ||
result, _, _ = api.run([filename] + mypy_options) | ||
|
||
for line in result.splitlines(): | ||
common_match = re.match(self.COMMON_PATTERN, line) | ||
if not common_match: | ||
continue | ||
|
||
common_data = common_match.groupdict() | ||
|
||
specific_pattern = self.SPECIFIC_PATTERNS.get(common_data["code"]) | ||
if specific_pattern: | ||
specific_match = specific_pattern.search(common_data["message"]) | ||
if specific_match: | ||
specific_data = specific_match.groupdict() | ||
self._add_message(common_data, specific_data) | ||
|
||
def _add_message(self, common_data: dict, specific_data: dict) -> None: | ||
"""Add a message using the common and specific data.""" | ||
code = common_data["code"] | ||
code_to_msgid = { | ||
"arg-type": "incompatible-argument-type", | ||
"assignment": "incompatible-assignment", | ||
"list-item": "list-item-type-mismatch", | ||
"operator": "unsupported-operand-types", | ||
"union-attr": "union-attr-error", | ||
"dict-item": "dict-item-type-mismatch", | ||
} | ||
msgid = code_to_msgid.get(code) | ||
if not msgid: | ||
return | ||
self.add_message( | ||
msgid, | ||
line=int(common_data["start_line"]), | ||
col_offset=int(common_data["start_col"]), | ||
end_lineno=int(common_data["end_line"]), | ||
end_col_offset=int(common_data["end_col"]), | ||
args=tuple(specific_data.values()), | ||
) | ||
|
||
|
||
def register(linter: PyLinter) -> None: | ||
"""Register the static type checker with the PyLinter.""" | ||
linter.register_checker(StaticTypeChecker(linter)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.