Skip to content
Open
256 changes: 181 additions & 75 deletions mypy/build.py

Large diffs are not rendered by default.

8 changes: 5 additions & 3 deletions mypy/checkstrformat.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,12 @@
MemberExpr,
MypyFile,
NameExpr,
Node,
StarExpr,
StrExpr,
TempNode,
TupleExpr,
)
from mypy.parse import parse
from mypy.parse import parse, report_parse_error
from mypy.subtypes import is_subtype
from mypy.typeops import custom_special_method
from mypy.types import (
Expand Down Expand Up @@ -582,9 +581,12 @@ def apply_field_accessors(

temp_errors = Errors(self.chk.options)
dummy = DUMMY_FIELD_NAME + spec.field[len(spec.key) :]
temp_ast: Node = parse(
temp_ast, parse_errors = parse(
dummy, fnam="<format>", module=None, options=self.chk.options, errors=temp_errors
)
for error in parse_errors:
# New parser reports errors lazily.
report_parse_error(error, temp_errors)
if temp_errors.is_errors():
self.msg.fail(
f'Syntax error in format specifier "{spec.field}"',
Expand Down
2 changes: 1 addition & 1 deletion mypy/metastore.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ def close(self) -> None:
def connect_db(db_file: str) -> sqlite3.Connection:
import sqlite3.dbapi2

db = sqlite3.dbapi2.connect(db_file)
db = sqlite3.dbapi2.connect(db_file, check_same_thread=False)
# This is a bit unfortunate (as we may get corrupt cache after e.g. Ctrl + C),
# but without this flag, commits are *very* slow, especially when using HDDs,
# see https://www.sqlite.org/faq.html#q19 for details.
Expand Down
26 changes: 13 additions & 13 deletions mypy/nativeparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
from __future__ import annotations

import os
from typing import Any, Final, cast
import time
from typing import Final, cast

import ast_serialize # type: ignore[import-untyped, import-not-found, unused-ignore]
from librt.internal import (
Expand Down Expand Up @@ -101,6 +102,7 @@
OpExpr,
OverloadedFuncDef,
OverloadPart,
ParseError,
PassStmt,
RaiseStmt,
RefExpr,
Expand Down Expand Up @@ -168,17 +170,11 @@
class State:
def __init__(self, options: Options) -> None:
self.options = options
self.errors: list[dict[str, Any]] = []
self.errors: list[ParseError] = []
self.num_funcs = 0

def add_error(
self,
message: str,
line: int,
column: int,
*,
blocker: bool = False,
code: str | None = None,
self, message: str, line: int, column: int, *, blocker: bool = False, code: str
) -> None:
"""Report an error at a specific location.

Expand All @@ -196,7 +192,7 @@ def add_error(

def native_parse(
filename: str, options: Options, skip_function_bodies: bool = False, imports_only: bool = False
) -> tuple[MypyFile, list[dict[str, Any]], TypeIgnores]:
) -> tuple[MypyFile, list[ParseError], TypeIgnores]:
"""Parse a Python file using the native Rust-based parser.

Uses the ast_serialize Rust extension to parse Python code and deserialize
Expand All @@ -214,7 +210,7 @@ def native_parse(
Returns:
A tuple containing:
- MypyFile: The parsed AST as a mypy AST node
- list[dict[str, Any]]: List of parse errors and deserialization errors
- list[ParseError]: List of parse errors and deserialization errors
- TypeIgnores: List of (line_number, ignored_codes) tuples for type: ignore comments
"""
# If the path is a directory, return empty AST (matching fastparse behavior)
Expand Down Expand Up @@ -272,7 +268,11 @@ def read_statements(state: State, data: ReadBuffer, n: int) -> list[Statement]:

def parse_to_binary_ast(
filename: str, options: Options, skip_function_bodies: bool = False
) -> tuple[bytes, list[dict[str, Any]], TypeIgnores, bytes, bool, bool]:
) -> tuple[bytes, list[ParseError], TypeIgnores, bytes, bool, bool]:
# This is a horrible hack to work around a mypyc bug where imported
# module may be not ready in a thread sometimes.
while ast_serialize is None:
time.sleep(0.0001) # type: ignore[unreachable]
ast_bytes, errors, ignores, import_bytes, ast_data = ast_serialize.parse(
filename,
skip_function_bodies=skip_function_bodies,
Expand All @@ -284,7 +284,7 @@ def parse_to_binary_ast(
)
return (
ast_bytes,
cast("list[dict[str, Any]]", errors),
errors,
ignores,
import_bytes,
ast_data["is_partial_package"],
Expand Down
45 changes: 41 additions & 4 deletions mypy/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@
Final,
Optional,
TypeAlias as _TypeAlias,
TypedDict,
TypeGuard,
TypeVar,
Union,
cast,
)
from typing_extensions import NotRequired

from librt.internal import (
extract_symbol,
Expand All @@ -39,7 +41,9 @@
LIST_GEN,
LIST_STR,
LITERAL_COMPLEX,
LITERAL_FALSE,
LITERAL_NONE,
LITERAL_TRUE,
ReadBuffer,
Tag,
WriteBuffer,
Expand Down Expand Up @@ -313,6 +317,39 @@ def read(cls, data: ReadBuffer) -> SymbolNode:
Definition: _TypeAlias = tuple[str, "SymbolTableNode", Optional["TypeInfo"]]


class ParseError(TypedDict):
line: int
column: int
message: str
blocker: NotRequired[bool]
code: NotRequired[str]


def write_parse_error(data: WriteBuffer, err: ParseError) -> None:
write_int(data, err["line"])
write_int(data, err["column"])
write_str(data, err["message"])
if (blocker := err.get("blocker")) is not None:
write_bool(data, blocker)
else:
write_tag(data, LITERAL_NONE)
write_str_opt(data, err.get("code"))


def read_parse_error(data: ReadBuffer) -> ParseError:
err: ParseError = {"line": read_int(data), "column": read_int(data), "message": read_str(data)}
tag = read_tag(data)
if tag == LITERAL_TRUE:
err["blocker"] = True
elif tag == LITERAL_FALSE:
err["blocker"] = False
else:
assert tag == LITERAL_NONE
if (code := read_str_opt(data)) is not None:
err["code"] = code
return err


class FileRawData:
"""Raw (binary) data representing parsed, but not deserialized file."""

Expand All @@ -327,7 +364,7 @@ class FileRawData:

defs: bytes
imports: bytes
raw_errors: list[dict[str, Any]] # TODO: switch to more precise type here.
raw_errors: list[ParseError]
ignored_lines: dict[int, list[str]]
is_partial_stub_package: bool
uses_template_strings: bool
Expand All @@ -336,7 +373,7 @@ def __init__(
self,
defs: bytes,
imports: bytes,
raw_errors: list[dict[str, Any]],
raw_errors: list[ParseError],
ignored_lines: dict[int, list[str]],
is_partial_stub_package: bool,
uses_template_strings: bool,
Expand All @@ -354,7 +391,7 @@ def write(self, data: WriteBuffer) -> None:
write_tag(data, LIST_GEN)
write_int_bare(data, len(self.raw_errors))
for err in self.raw_errors:
write_json(data, err)
write_parse_error(data, err)
write_tag(data, DICT_INT_GEN)
write_int_bare(data, len(self.ignored_lines))
for line, codes in self.ignored_lines.items():
Expand All @@ -368,7 +405,7 @@ def read(cls, data: ReadBuffer) -> FileRawData:
defs = read_bytes(data)
imports = read_bytes(data)
assert read_tag(data) == LIST_GEN
raw_errors = [read_json(data) for _ in range(read_int_bare(data))]
raw_errors = [read_parse_error(data) for _ in range(read_int_bare(data))]
assert read_tag(data) == DICT_INT_GEN
ignored_lines = {read_int(data): read_str_list(data) for _ in range(read_int_bare(data))}
return FileRawData(
Expand Down
57 changes: 20 additions & 37 deletions mypy/parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from mypy import errorcodes as codes
from mypy.cache import read_int
from mypy.errors import Errors
from mypy.nodes import FileRawData, MypyFile
from mypy.nodes import FileRawData, MypyFile, ParseError
from mypy.options import Options


Expand All @@ -18,9 +18,8 @@ def parse(
module: str | None,
errors: Errors,
options: Options,
raise_on_error: bool = False,
imports_only: bool = False,
) -> MypyFile:
) -> tuple[MypyFile, list[ParseError]]:
"""Parse a source file, without doing any semantic analysis.

Return the parse tree. If errors is not provided, raise ParseError
Expand All @@ -37,8 +36,6 @@ def parse(
ignore_errors = options.ignore_errors or fnam in errors.ignored_files
# If errors are ignored, we can drop many function bodies to speed up type checking.
strip_function_bodies = ignore_errors and not options.preserve_asts

errors.set_file(fnam, module, options=options)
tree, parse_errors, type_ignores = mypy.nativeparse.native_parse(
fnam,
options,
Expand All @@ -51,26 +48,7 @@ def parse(
tree.is_stub = fnam.endswith(".pyi")
# Note: tree.imports is populated directly by native_parse with deserialized
# import metadata, so we don't need to collect imports via AST traversal

# Report parse errors
for error in parse_errors:
message = error["message"]
# Standardize error message by capitalizing the first word
message = re.sub(r"^(\s*\w)", lambda m: m.group(1).upper(), message)
# Respect blocker status from error, default to True for syntax errors
is_blocker = error.get("blocker", True)
error_code = error.get("code")
if error_code is None:
error_code = codes.SYNTAX
else:
# Fallback to [syntax] for backwards compatibility.
error_code = codes.error_codes.get(error_code) or codes.SYNTAX
errors.report(
error["line"], error["column"], message, blocker=is_blocker, code=error_code
)
if raise_on_error and errors.is_errors():
errors.raise_error()
return tree
return tree, parse_errors
# Fall through to fastparse for non-existent files

assert not imports_only
Expand All @@ -79,9 +57,7 @@ def parse(
import mypy.fastparse

tree = mypy.fastparse.parse(source, fnam=fnam, module=module, errors=errors, options=options)
if raise_on_error and errors.is_errors():
errors.raise_error()
return tree
return tree, []


def load_from_raw(
Expand Down Expand Up @@ -112,14 +88,21 @@ def load_from_raw(
all_errors = raw_data.raw_errors + state.errors
errors.set_file(fnam, module, options=options)
for error in all_errors:
message = error["message"]
message = re.sub(r"^(\s*\w)", lambda m: m.group(1).upper(), message)
is_blocker = error.get("blocker", True)
error_code = error.get("code")
if error_code is None:
error_code = codes.SYNTAX
else:
error_code = codes.error_codes.get(error_code) or codes.SYNTAX
# Note we never raise in this function, so it should not be called in coordinator.
errors.report(error["line"], error["column"], message, blocker=is_blocker, code=error_code)
report_parse_error(error, errors)
return tree


def report_parse_error(error: ParseError, errors: Errors) -> None:
message = error["message"]
# Standardize error message by capitalizing the first word
message = re.sub(r"^(\s*\w)", lambda m: m.group(1).upper(), message)
# Respect blocker status from error, default to True for syntax errors
is_blocker = error.get("blocker", True)
error_code = error.get("code")
if error_code is None:
error_code = codes.SYNTAX
else:
# Fallback to [syntax] for backwards compatibility.
error_code = codes.error_codes.get(error_code) or codes.SYNTAX
errors.report(error["line"], error["column"], message, blocker=is_blocker, code=error_code)
28 changes: 16 additions & 12 deletions mypy/semanal_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -463,17 +463,18 @@ def apply_class_plugin_hooks(graph: Graph, scc: list[str], errors: Errors) -> No
state = graph[module]
tree = state.tree
assert tree
for _, node, _ in tree.local_definitions():
if isinstance(node.node, TypeInfo):
if not apply_hooks_to_class(
state.manager.semantic_analyzer,
module,
node.node,
state.options,
tree,
errors,
):
incomplete = True
with state.wrap_context():
for _, node, _ in tree.local_definitions():
if isinstance(node.node, TypeInfo):
if not apply_hooks_to_class(
state.manager.semantic_analyzer,
module,
node.node,
state.options,
tree,
errors,
):
incomplete = True


def apply_hooks_to_class(
Expand Down Expand Up @@ -524,7 +525,10 @@ def calculate_class_properties(graph: Graph, scc: list[str], errors: Errors) ->
assert tree
for _, node, _ in tree.local_definitions():
if isinstance(node.node, TypeInfo):
with state.manager.semantic_analyzer.file_context(tree, state.options, node.node):
with (
state.wrap_context(),
state.manager.semantic_analyzer.file_context(tree, state.options, node.node),
):
calculate_class_abstract_status(node.node, tree.is_stub, errors)
check_protocol_status(node.node, errors)
calculate_class_vars(node.node)
Expand Down
4 changes: 3 additions & 1 deletion mypy/stubgen.py
Original file line number Diff line number Diff line change
Expand Up @@ -1744,10 +1744,12 @@ def parse_source_file(mod: StubSource, mypy_options: MypyOptions) -> None:
data = f.read()
source = mypy.util.decode_python_encoding(data)
errors = Errors(mypy_options)
mod.ast = mypy.parse.parse(
mod.ast, errs = mypy.parse.parse(
source, fnam=mod.path, module=mod.module, errors=errors, options=mypy_options
)
mod.ast._fullname = mod.module
for err in errs:
mypy.parse.report_parse_error(err, errors)
if errors.is_blockers():
# Syntax error!
for m in errors.new_messages():
Expand Down
5 changes: 2 additions & 3 deletions mypy/test/test_nativeparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
import tempfile
import unittest
from collections.abc import Iterator
from typing import Any

from mypy import defaults, nodes
from mypy.cache import (
Expand All @@ -25,7 +24,7 @@
)
from mypy.config_parser import parse_mypy_comments
from mypy.errors import CompileError
from mypy.nodes import MypyFile
from mypy.nodes import MypyFile, ParseError
from mypy.options import Options
from mypy.test.data import DataDrivenTestCase, DataSuite
from mypy.test.helpers import assert_string_arrays_equal
Expand Down Expand Up @@ -102,7 +101,7 @@ def test_parser(testcase: DataDrivenTestCase) -> None:
)


def format_error(err: dict[str, Any]) -> str:
def format_error(err: ParseError) -> str:
return f"{err['line']}:{err['column']}: error: {err['message']}"


Expand Down
Loading
Loading