13
13
14
14
from mypy .build import BuildResult
15
15
from mypy .nodes import (
16
+ AssignmentStmt ,
16
17
CallExpr ,
18
+ ClassDef ,
19
+ Decorator ,
20
+ DictionaryComprehension ,
17
21
Expression ,
18
22
ForStmt ,
19
23
FuncDef ,
24
+ GeneratorExpr ,
25
+ IndexExpr ,
20
26
LambdaExpr ,
21
27
MemberExpr ,
22
28
MypyFile ,
29
+ NamedTupleExpr ,
23
30
NameExpr ,
31
+ NewTypeExpr ,
24
32
Node ,
33
+ OpExpr ,
25
34
RefExpr ,
26
35
TupleExpr ,
36
+ TypedDictExpr ,
27
37
TypeInfo ,
38
+ TypeVarExpr ,
28
39
Var ,
40
+ WithStmt ,
29
41
)
30
42
from mypy .traverser import TraverserVisitor
31
43
from mypy .types import AnyType , Instance , ProperType , Type , TypeOfAny , get_proper_type
32
44
from mypy .util import FancyFormatter
33
45
from mypyc .ir .func_ir import FuncIR
34
46
from mypyc .ir .module_ir import ModuleIR
35
47
from mypyc .ir .ops import CallC , LoadLiteral , LoadStatic , Value
48
+ from mypyc .irbuild .mapper import Mapper
36
49
37
50
38
51
class Annotation :
@@ -71,18 +84,21 @@ def __init__(self, message: str, priority: int = 1) -> None:
71
84
72
85
stdlib_hints : Final = {
73
86
"functools.partial" : Annotation (
74
- '"functools.partial" is inefficient in compiled code.' , priority = 2
87
+ '"functools.partial" is inefficient in compiled code.' , priority = 3
75
88
),
76
89
"itertools.chain" : Annotation (
77
90
'"itertools.chain" is inefficient in compiled code (hint: replace with for loops).' ,
78
- priority = 2 ,
91
+ priority = 3 ,
79
92
),
80
93
"itertools.groupby" : Annotation (
81
- '"itertools.groupby" is inefficient in compiled code.' , priority = 2
94
+ '"itertools.groupby" is inefficient in compiled code.' , priority = 3
82
95
),
83
96
"itertools.islice" : Annotation (
84
97
'"itertools.islice" is inefficient in compiled code (hint: replace with for loop over index range).' ,
85
- priority = 2 ,
98
+ priority = 3 ,
99
+ ),
100
+ "copy.deepcopy" : Annotation (
101
+ '"copy.deepcopy" tends to be slow. Make a shallow copy if possible.' , priority = 2
86
102
),
87
103
}
88
104
@@ -127,14 +143,16 @@ def __init__(self, path: str, annotations: dict[int, list[Annotation]]) -> None:
127
143
128
144
129
145
def generate_annotated_html (
130
- html_fnam : str , result : BuildResult , modules : dict [str , ModuleIR ]
146
+ html_fnam : str , result : BuildResult , modules : dict [str , ModuleIR ], mapper : Mapper
131
147
) -> None :
132
148
annotations = []
133
149
for mod , mod_ir in modules .items ():
134
150
path = result .graph [mod ].path
135
151
tree = result .graph [mod ].tree
136
152
assert tree is not None
137
- annotations .append (generate_annotations (path or "<source>" , tree , mod_ir , result .types ))
153
+ annotations .append (
154
+ generate_annotations (path or "<source>" , tree , mod_ir , result .types , mapper )
155
+ )
138
156
html = generate_html_report (annotations )
139
157
with open (html_fnam , "w" ) as f :
140
158
f .write (html )
@@ -145,15 +163,18 @@ def generate_annotated_html(
145
163
146
164
147
165
def generate_annotations (
148
- path : str , tree : MypyFile , ir : ModuleIR , type_map : dict [Expression , Type ]
166
+ path : str , tree : MypyFile , ir : ModuleIR , type_map : dict [Expression , Type ], mapper : Mapper
149
167
) -> AnnotatedSource :
150
168
anns = {}
151
169
for func_ir in ir .functions :
152
170
anns .update (function_annotations (func_ir , tree ))
153
- visitor = ASTAnnotateVisitor (type_map )
171
+ visitor = ASTAnnotateVisitor (type_map , mapper )
154
172
for defn in tree .defs :
155
173
defn .accept (visitor )
156
174
anns .update (visitor .anns )
175
+ for line in visitor .ignored_lines :
176
+ if line in anns :
177
+ del anns [line ]
157
178
return AnnotatedSource (path , anns )
158
179
159
180
@@ -168,18 +189,28 @@ def function_annotations(func_ir: FuncIR, tree: MypyFile) -> dict[int, list[Anno
168
189
ann : str | Annotation | None = None
169
190
if name == "CPyObject_GetAttr" :
170
191
attr_name = get_str_literal (op .args [1 ])
171
- if attr_name == "__prepare__" :
172
- # These attributes are internal to mypyc/CPython, and the user has
173
- # little control over them.
192
+ if attr_name in ("__prepare__" , "GeneratorExit" , "StopIteration" ):
193
+ # These attributes are internal to mypyc/CPython, and/or accessed
194
+ # implicitly in generated code. The user has little control over
195
+ # them.
174
196
ann = None
175
197
elif attr_name :
176
198
ann = f'Get non-native attribute "{ attr_name } ".'
177
199
else :
178
200
ann = "Dynamic attribute lookup."
201
+ elif name == "PyObject_SetAttr" :
202
+ attr_name = get_str_literal (op .args [1 ])
203
+ if attr_name == "__mypyc_attrs__" :
204
+ # This is set implicitly and can't be avoided.
205
+ ann = None
206
+ elif attr_name :
207
+ ann = f'Set non-native attribute "{ attr_name } ".'
208
+ else :
209
+ ann = "Dynamic attribute set."
179
210
elif name == "PyObject_VectorcallMethod" :
180
211
method_name = get_str_literal (op .args [0 ])
181
212
if method_name :
182
- ann = f'Call non-native method "{ method_name } ".'
213
+ ann = f'Call non-native method "{ method_name } " (it may be defined in a non-native class, or decorated) .'
183
214
else :
184
215
ann = "Dynamic method call."
185
216
elif name in op_hints :
@@ -218,10 +249,12 @@ def function_annotations(func_ir: FuncIR, tree: MypyFile) -> dict[int, list[Anno
218
249
class ASTAnnotateVisitor (TraverserVisitor ):
219
250
"""Generate annotations from mypy AST and inferred types."""
220
251
221
- def __init__ (self , type_map : dict [Expression , Type ]) -> None :
252
+ def __init__ (self , type_map : dict [Expression , Type ], mapper : Mapper ) -> None :
222
253
self .anns : dict [int , list [Annotation ]] = {}
254
+ self .ignored_lines : set [int ] = set ()
223
255
self .func_depth = 0
224
256
self .type_map = type_map
257
+ self .mapper = mapper
225
258
226
259
def visit_func_def (self , o : FuncDef , / ) -> None :
227
260
if self .func_depth > 0 :
@@ -235,21 +268,84 @@ def visit_func_def(self, o: FuncDef, /) -> None:
235
268
self .func_depth -= 1
236
269
237
270
def visit_for_stmt (self , o : ForStmt , / ) -> None :
238
- typ = self .get_type (o .expr )
239
- if isinstance (typ , AnyType ):
240
- self .annotate (o .expr , 'For loop uses generic operations (iterable has type "Any").' )
241
- elif isinstance (typ , Instance ) and typ .type .fullname in (
242
- "typing.Iterable" ,
243
- "typing.Iterator" ,
244
- "typing.Sequence" ,
245
- "typing.MutableSequence" ,
246
- ):
247
- self .annotate (
248
- o .expr ,
249
- f'For loop uses generic operations (iterable has the abstract type "{ typ .type .fullname } ").' ,
250
- )
271
+ self .check_iteration ([o .expr ], "For loop" )
251
272
super ().visit_for_stmt (o )
252
273
274
+ def visit_dictionary_comprehension (self , o : DictionaryComprehension , / ) -> None :
275
+ self .check_iteration (o .sequences , "Comprehension" )
276
+ super ().visit_dictionary_comprehension (o )
277
+
278
+ def visit_generator_expr (self , o : GeneratorExpr , / ) -> None :
279
+ self .check_iteration (o .sequences , "Comprehension or generator" )
280
+ super ().visit_generator_expr (o )
281
+
282
+ def check_iteration (self , expressions : list [Expression ], kind : str ) -> None :
283
+ for expr in expressions :
284
+ typ = self .get_type (expr )
285
+ if isinstance (typ , AnyType ):
286
+ self .annotate (expr , f'{ kind } uses generic operations (iterable has type "Any").' )
287
+ elif isinstance (typ , Instance ) and typ .type .fullname in (
288
+ "typing.Iterable" ,
289
+ "typing.Iterator" ,
290
+ "typing.Sequence" ,
291
+ "typing.MutableSequence" ,
292
+ ):
293
+ self .annotate (
294
+ expr ,
295
+ f'{ kind } uses generic operations (iterable has the abstract type "{ typ .type .fullname } ").' ,
296
+ )
297
+
298
+ def visit_class_def (self , o : ClassDef , / ) -> None :
299
+ super ().visit_class_def (o )
300
+ if self .func_depth == 0 :
301
+ # Don't complain about base classes at top level
302
+ for base in o .base_type_exprs :
303
+ self .ignored_lines .add (base .line )
304
+
305
+ for s in o .defs .body :
306
+ if isinstance (s , AssignmentStmt ):
307
+ # Don't complain about attribute initializers
308
+ self .ignored_lines .add (s .line )
309
+ elif isinstance (s , Decorator ):
310
+ # Don't complain about decorator definitions that generate some
311
+ # dynamic operations. This is a bit heavy-handed.
312
+ self .ignored_lines .add (s .func .line )
313
+
314
+ def visit_with_stmt (self , o : WithStmt , / ) -> None :
315
+ for expr in o .expr :
316
+ if isinstance (expr , CallExpr ) and isinstance (expr .callee , RefExpr ):
317
+ node = expr .callee .node
318
+ if isinstance (node , Decorator ):
319
+ if any (
320
+ isinstance (d , RefExpr )
321
+ and d .node
322
+ and d .node .fullname == "contextlib.contextmanager"
323
+ for d in node .decorators
324
+ ):
325
+ self .annotate (
326
+ expr ,
327
+ f'"{ node .name } " uses @contextmanager, which is slow '
328
+ + "in compiled code. Use a native class with "
329
+ + '"__enter__" and "__exit__" methods instead.' ,
330
+ priority = 3 ,
331
+ )
332
+ super ().visit_with_stmt (o )
333
+
334
+ def visit_assignment_stmt (self , o : AssignmentStmt , / ) -> None :
335
+ special_form = False
336
+ if self .func_depth == 0 :
337
+ analyzed : Expression | None = o .rvalue
338
+ if isinstance (o .rvalue , (CallExpr , IndexExpr , OpExpr )):
339
+ analyzed = o .rvalue .analyzed
340
+ if o .is_alias_def or isinstance (
341
+ analyzed , (TypeVarExpr , NamedTupleExpr , TypedDictExpr , NewTypeExpr )
342
+ ):
343
+ special_form = True
344
+ if special_form :
345
+ # TODO: Ignore all lines if multi-line
346
+ self .ignored_lines .add (o .line )
347
+ super ().visit_assignment_stmt (o )
348
+
253
349
def visit_name_expr (self , o : NameExpr , / ) -> None :
254
350
if ann := stdlib_hints .get (o .fullname ):
255
351
self .annotate (o , ann )
@@ -268,6 +364,30 @@ def visit_call_expr(self, o: CallExpr, /) -> None:
268
364
):
269
365
arg = o .args [1 ]
270
366
self .check_isinstance_arg (arg )
367
+ elif isinstance (o .callee , RefExpr ) and isinstance (o .callee .node , TypeInfo ):
368
+ info = o .callee .node
369
+ class_ir = self .mapper .type_to_ir .get (info )
370
+ if (class_ir and not class_ir .is_ext_class ) or (
371
+ class_ir is None and not info .fullname .startswith ("builtins." )
372
+ ):
373
+ self .annotate (
374
+ o , f'Creating an instance of non-native class "{ info .name } " ' + "is slow." , 2
375
+ )
376
+ elif class_ir and class_ir .is_augmented :
377
+ self .annotate (
378
+ o ,
379
+ f'Class "{ info .name } " is only partially native, and '
380
+ + "constructing an instance is slow." ,
381
+ 2 ,
382
+ )
383
+ elif isinstance (o .callee , RefExpr ) and isinstance (o .callee .node , Decorator ):
384
+ decorator = o .callee .node
385
+ if self .mapper .is_native_ref_expr (o .callee ):
386
+ self .annotate (
387
+ o ,
388
+ f'Calling a decorated function ("{ decorator .name } ") is inefficient, even if it\' s native.' ,
389
+ 2 ,
390
+ )
271
391
272
392
def check_isinstance_arg (self , arg : Expression ) -> None :
273
393
if isinstance (arg , RefExpr ):
@@ -287,9 +407,9 @@ def visit_lambda_expr(self, o: LambdaExpr, /) -> None:
287
407
)
288
408
super ().visit_lambda_expr (o )
289
409
290
- def annotate (self , o : Node , ann : str | Annotation ) -> None :
410
+ def annotate (self , o : Node , ann : str | Annotation , priority : int = 1 ) -> None :
291
411
if isinstance (ann , str ):
292
- ann = Annotation (ann )
412
+ ann = Annotation (ann , priority = priority )
293
413
self .anns .setdefault (o .line , []).append (ann )
294
414
295
415
def get_type (self , e : Expression ) -> ProperType :
0 commit comments