Compare commits

...
3 Commits
Author SHA1 Message Date
HEL c29c0ed3ec tests: add tests for type checker 2026-05-29 19:08:58 +02:00
HEL fa7e56cb77 tests: add checker tester 2026-05-29 19:08:41 +02:00
HEL 13c19db818 fix(checker): stabilize call error message
display missing arguments in a stable format, similar to how native Python does
2026-05-29 19:08:13 +02:00
11 changed files with 511 additions and 11 deletions

No files matched your search

+28 -11
View File
@@ -311,12 +311,12 @@ class Checker(
}
set_args: set[str] = set()
required_positional: set[str] = {
required_positional: list[str] = [
arg.name for arg in function.pos_args + function.args if arg.required
}
required_keyword: set[str] = {
]
required_keyword: list[str] = [
arg.name for arg in function.kw_args if arg.required
}
]
mapped: list[MappedArgument] = []
@@ -336,9 +336,12 @@ class Checker(
else:
self.error(arg[0].location, "Too many positional arguments")
break
required_positional.discard(param.name)
required_keyword.discard(param.name)
set_args.add(param.name)
name: str = param.name
if name in required_positional:
required_positional.remove(name)
if name in required_keyword:
required_keyword.remove(name)
set_args.add(name)
mapped.append(
MappedArgument(
expr=arg[0],
@@ -359,8 +362,10 @@ class Checker(
self.error(arg[0].location, f"Unknown keyword argument '{name}'")
continue
param = kw_params.pop(name)
required_positional.discard(name)
required_keyword.discard(name)
if name in required_positional:
required_positional.remove(name)
if name in required_keyword:
required_keyword.remove(name)
set_args.add(name)
mapped.append(
MappedArgument(
@@ -370,16 +375,28 @@ class Checker(
)
)
def join_args(args: list[str]) -> str:
args = list(map(lambda a: f"'{a}'", args))
if len(args) == 0:
return ""
if len(args) == 1:
return args[0]
return ", ".join(args[:-1]) + " and " + args[-1]
if len(required_positional) != 0:
plural: str = "" if len(required_positional) == 1 else "s"
args: str = join_args(required_positional)
self.error(
call.location,
f"Missing required positional arguments: {required_positional}",
f"Missing required positional argument{plural}: {args}",
)
if len(required_keyword) != 0:
plural: str = "" if len(required_keyword) == 1 else "s"
args: str = join_args(required_keyword)
self.error(
call.location,
f"Missing required keyword arguments: {required_keyword}",
f"Missing required keyword argument{plural}: {args}",
)
return mapped
+14
View File
@@ -0,0 +1,14 @@
# type: ignore
# ruff: disable[F821]
from __future__ import annotations
df: Frame[
verified: bool,
birth_year: int,
height: float + ( _ > 0 ) + ( _ < 250 ),
name: str,
date: datetime,
float,
unknown: _,
_
]
@@ -0,0 +1,3 @@
{
"diagnostics": []
}
@@ -0,0 +1,11 @@
a: int = 3
b: int = 4
c = a + b
c = "invalid"
d = True
e = d + d
f: float = a
@@ -0,0 +1,46 @@
{
"diagnostics": [
{
"type": "Error",
"location": {
"start": [
6,
0
],
"end": [
6,
13
]
},
"message": "Cannot assign BaseType(name='str') to c of type BaseType(name='int')"
},
{
"type": "Error",
"location": {
"start": [
9,
4
],
"end": [
9,
9
]
},
"message": "Undefined operation __add__ between BaseType(name='bool') and BaseType(name='bool')"
},
{
"type": "Error",
"location": {
"start": [
11,
0
],
"end": [
11,
12
]
},
"message": "Cannot assign BaseType(name='int') to f of type BaseType(name='float')"
}
]
}
+18
View File
@@ -0,0 +1,18 @@
def foo(a: int, /, b: float, *, c: str):
return True
r1 = foo()
r2 = foo(1)
r3 = foo(1, 2.0)
r4 = foo(1, b=2.0)
r5 = foo(1, 2.0, "test")
r6 = foo(1, 2.0, b=3.0)
r7 = foo(a=1)
r8 = foo(g="test")
r9a = foo(1, 2.0, c="test")
r9b = foo(1, b=2.0, c="test")
r9c = foo(1, c="test", b=2.0)
r10 = foo("a", 3, c=False)
@@ -0,0 +1,270 @@
{
"diagnostics": [
{
"type": "Error",
"location": {
"start": [
5,
5
],
"end": [
5,
10
]
},
"message": "Missing required positional arguments: 'a' and 'b'"
},
{
"type": "Error",
"location": {
"start": [
5,
5
],
"end": [
5,
10
]
},
"message": "Missing required keyword argument: 'c'"
},
{
"type": "Error",
"location": {
"start": [
6,
5
],
"end": [
6,
11
]
},
"message": "Missing required positional argument: 'b'"
},
{
"type": "Error",
"location": {
"start": [
6,
5
],
"end": [
6,
11
]
},
"message": "Missing required keyword argument: 'c'"
},
{
"type": "Error",
"location": {
"start": [
7,
5
],
"end": [
7,
16
]
},
"message": "Missing required keyword argument: 'c'"
},
{
"type": "Error",
"location": {
"start": [
8,
5
],
"end": [
8,
18
]
},
"message": "Missing required keyword argument: 'c'"
},
{
"type": "Error",
"location": {
"start": [
9,
17
],
"end": [
9,
23
]
},
"message": "Too many positional arguments"
},
{
"type": "Error",
"location": {
"start": [
9,
5
],
"end": [
9,
24
]
},
"message": "Missing required keyword argument: 'c'"
},
{
"type": "Error",
"location": {
"start": [
10,
19
],
"end": [
10,
22
]
},
"message": "Multiple values for argument 'b'"
},
{
"type": "Error",
"location": {
"start": [
10,
5
],
"end": [
10,
23
]
},
"message": "Missing required keyword argument: 'c'"
},
{
"type": "Error",
"location": {
"start": [
11,
11
],
"end": [
11,
12
]
},
"message": "Unknown keyword argument 'a'"
},
{
"type": "Error",
"location": {
"start": [
11,
5
],
"end": [
11,
13
]
},
"message": "Missing required positional arguments: 'a' and 'b'"
},
{
"type": "Error",
"location": {
"start": [
11,
5
],
"end": [
11,
13
]
},
"message": "Missing required keyword argument: 'c'"
},
{
"type": "Error",
"location": {
"start": [
12,
11
],
"end": [
12,
17
]
},
"message": "Unknown keyword argument 'g'"
},
{
"type": "Error",
"location": {
"start": [
12,
5
],
"end": [
12,
18
]
},
"message": "Missing required positional arguments: 'a' and 'b'"
},
{
"type": "Error",
"location": {
"start": [
12,
5
],
"end": [
12,
18
]
},
"message": "Missing required keyword argument: 'c'"
},
{
"type": "Error",
"location": {
"start": [
18,
10
],
"end": [
18,
13
]
},
"message": "Wrong type for argument 'a', expected BaseType(name='int'), got BaseType(name='str')"
},
{
"type": "Error",
"location": {
"start": [
18,
15
],
"end": [
18,
16
]
},
"message": "Wrong type for argument 'b', expected BaseType(name='float'), got BaseType(name='int')"
},
{
"type": "Error",
"location": {
"start": [
18,
20
],
"end": [
18,
25
]
},
"message": "Wrong type for argument 'c', expected BaseType(name='str'), got BaseType(name='bool')"
}
]
}
+14
View File
@@ -0,0 +1,14 @@
type Meter(float)
type Second(float)
type MeterPerSecond(float)
extend Meter {
op __add__(Meter) -> Meter
op __sub__(Meter) -> Meter
op __truediv__(Second) -> MeterPerSecond
}
extend Second {
op __add__(Second) -> Second
op __sub__(Second) -> Second
}
+8
View File
@@ -0,0 +1,8 @@
# type: ignore
# ruff: disable [F821]
midas.using("04_custom_types.midas")
distance: Meter = 123.45
time: Second = 6.7
speed = distance / time
@@ -0,0 +1,32 @@
{
"diagnostics": [
{
"type": "Error",
"location": {
"start": [
6,
0
],
"end": [
6,
24
]
},
"message": "Cannot assign BaseType(name='float') to distance of type SimpleType(name='Meter', base=BaseType(name='float'))"
},
{
"type": "Error",
"location": {
"start": [
7,
0
],
"end": [
7,
18
]
},
"message": "Cannot assign BaseType(name='float') to time of type SimpleType(name='Second', base=BaseType(name='float'))"
}
]
}
+67
View File
@@ -0,0 +1,67 @@
import ast
import json
from dataclasses import asdict, dataclass, field
from pathlib import Path
import midas.ast.python as p
from midas.checker.checker import Checker
from midas.checker.diagnostic import Diagnostic
from midas.parser.python import PythonParser
from midas.resolver.resolver import Resolver
from tests.base import Tester
@dataclass
class CaseResult:
diagnostics: list[dict] = field(default_factory=list)
def dumps(self) -> str:
return json.dumps(asdict(self), indent=2)
class CheckerTester(Tester):
@property
def namespace(self) -> str:
return "checker"
def _list_tests(self) -> list[Path]:
return list(self.base_dir.rglob("*.py"))
def _exec_case(self, path: Path) -> CaseResult:
if not path.exists():
raise FileNotFoundError(f"Could not find test '{path}'")
if not path.is_file():
raise TypeError(f"Test '{path}' is not a file")
source: str = path.read_text()
tree: ast.Module = ast.parse(source, filename=path)
parser = PythonParser()
stmts: list[p.Stmt] = parser.parse_module(tree)
resolver = Resolver()
resolver.resolve(*stmts)
result: CaseResult = CaseResult()
checker = Checker(resolver.locals, file_path=path)
diagnostics: list[Diagnostic] = checker.check(stmts)
for diagnostic in diagnostics:
result.diagnostics.append(
{
"type": str(diagnostic.type),
"location": {
"start": (
diagnostic.location.lineno,
diagnostic.location.col_offset,
),
"end": (
diagnostic.location.end_lineno,
diagnostic.location.end_col_offset,
),
},
"message": diagnostic.message,
}
)
return result
if __name__ == "__main__":
CheckerTester.main()