skylos 1.0.10__py3-none-any.whl → 2.5.2__py3-none-any.whl

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 (53) hide show
  1. skylos/__init__.py +9 -3
  2. skylos/analyzer.py +674 -168
  3. skylos/cfg_visitor.py +60 -0
  4. skylos/cli.py +719 -235
  5. skylos/codemods.py +277 -0
  6. skylos/config.py +50 -0
  7. skylos/constants.py +78 -0
  8. skylos/gatekeeper.py +147 -0
  9. skylos/linter.py +18 -0
  10. skylos/rules/base.py +20 -0
  11. skylos/rules/danger/calls.py +119 -0
  12. skylos/rules/danger/danger.py +157 -0
  13. skylos/rules/danger/danger_cmd/cmd_flow.py +75 -0
  14. skylos/rules/danger/danger_fs/__init__.py +0 -0
  15. skylos/rules/danger/danger_fs/path_flow.py +79 -0
  16. skylos/rules/danger/danger_net/__init__.py +0 -0
  17. skylos/rules/danger/danger_net/ssrf_flow.py +80 -0
  18. skylos/rules/danger/danger_sql/__init__.py +0 -0
  19. skylos/rules/danger/danger_sql/sql_flow.py +245 -0
  20. skylos/rules/danger/danger_sql/sql_raw_flow.py +96 -0
  21. skylos/rules/danger/danger_web/__init__.py +0 -0
  22. skylos/rules/danger/danger_web/xss_flow.py +170 -0
  23. skylos/rules/danger/taint.py +110 -0
  24. skylos/rules/quality/__init__.py +0 -0
  25. skylos/rules/quality/complexity.py +95 -0
  26. skylos/rules/quality/logic.py +96 -0
  27. skylos/rules/quality/nesting.py +101 -0
  28. skylos/rules/quality/structure.py +99 -0
  29. skylos/rules/secrets.py +325 -0
  30. skylos/server.py +554 -0
  31. skylos/visitor.py +502 -90
  32. skylos/visitors/__init__.py +0 -0
  33. skylos/visitors/framework_aware.py +437 -0
  34. skylos/visitors/test_aware.py +74 -0
  35. skylos-2.5.2.dist-info/METADATA +21 -0
  36. skylos-2.5.2.dist-info/RECORD +42 -0
  37. {skylos-1.0.10.dist-info → skylos-2.5.2.dist-info}/WHEEL +1 -1
  38. {skylos-1.0.10.dist-info → skylos-2.5.2.dist-info}/top_level.txt +0 -1
  39. skylos-1.0.10.dist-info/METADATA +0 -8
  40. skylos-1.0.10.dist-info/RECORD +0 -21
  41. test/compare_tools.py +0 -604
  42. test/diagnostics.py +0 -364
  43. test/sample_repo/app.py +0 -13
  44. test/sample_repo/sample_repo/commands.py +0 -81
  45. test/sample_repo/sample_repo/models.py +0 -122
  46. test/sample_repo/sample_repo/routes.py +0 -89
  47. test/sample_repo/sample_repo/utils.py +0 -36
  48. test/test_skylos.py +0 -456
  49. test/test_visitor.py +0 -220
  50. {test → skylos/rules}/__init__.py +0 -0
  51. {test/sample_repo → skylos/rules/danger}/__init__.py +0 -0
  52. {test/sample_repo/sample_repo → skylos/rules/danger/danger_cmd}/__init__.py +0 -0
  53. {skylos-1.0.10.dist-info → skylos-2.5.2.dist-info}/entry_points.txt +0 -0
skylos/visitor.py CHANGED
@@ -1,24 +1,75 @@
1
1
  #!/usr/bin/env python3
2
- import ast,re
2
+ import ast
3
3
  from pathlib import Path
4
+ import re
5
+
6
+ PYTHON_BUILTINS = {
7
+ "print",
8
+ "len",
9
+ "str",
10
+ "int",
11
+ "float",
12
+ "list",
13
+ "dict",
14
+ "set",
15
+ "tuple",
16
+ "range",
17
+ "open",
18
+ "reversed",
19
+ "super",
20
+ "object",
21
+ "type",
22
+ "enumerate",
23
+ "zip",
24
+ "map",
25
+ "filter",
26
+ "sorted",
27
+ "sum",
28
+ "min",
29
+ "next",
30
+ "iter",
31
+ "bytes",
32
+ "bytearray",
33
+ "format",
34
+ "round",
35
+ "abs",
36
+ "complex",
37
+ "hash",
38
+ "id",
39
+ "bool",
40
+ "callable",
41
+ "getattr",
42
+ "max",
43
+ "all",
44
+ "any",
45
+ "setattr",
46
+ "hasattr",
47
+ "isinstance",
48
+ "globals",
49
+ "locals",
50
+ "vars",
51
+ "dir",
52
+ "property",
53
+ "classmethod",
54
+ "staticmethod",
55
+ }
56
+ DYNAMIC_PATTERNS = {"getattr", "globals", "eval", "exec"}
57
+
58
+ ## "🥚" hi :)
4
59
 
5
- PYTHON_BUILTINS={"print","len","str","int","float","list","dict","set","tuple","range","open","super","object","type","enumerate","zip","map","filter","sorted","reversed","sum","min","max","all","any","next","iter","repr","chr","ord","bytes","bytearray","memoryview","format","round","abs","pow","divmod","complex","hash","id","bool","callable","getattr","setattr","delattr","hasattr","isinstance","issubclass","globals","locals","vars","dir","property","classmethod","staticmethod"}
6
- DYNAMIC_PATTERNS={"getattr","globals","eval","exec"}
7
60
 
8
61
  class Definition:
9
- __slots__ = ('name', 'type', 'filename', 'line', 'simple_name', 'confidence', 'references', 'is_exported', 'in_init')
10
-
11
- def __init__(self, n, t, f, l):
12
- self.name = n
62
+ def __init__(self, name, t, filename, line):
63
+ self.name = name
13
64
  self.type = t
14
- self.filename = f
15
- self.line = l
16
- self.simple_name = n.split('.')[-1]
65
+ self.filename = filename
66
+ self.line = line
67
+ self.simple_name = name.split(".")[-1]
17
68
  self.confidence = 100
18
69
  self.references = 0
19
70
  self.is_exported = False
20
- self.in_init = "__init__.py" in str(f)
21
-
71
+ self.in_init = "__init__.py" in str(filename)
72
+
22
73
  def to_dict(self):
23
74
  if self.type == "method" and "." in self.name:
24
75
  parts = self.name.split(".")
@@ -28,8 +79,8 @@ class Definition:
28
79
  output_name = self.name
29
80
  else:
30
81
  output_name = self.simple_name
31
-
32
- return{
82
+
83
+ return {
33
84
  "name": output_name,
34
85
  "full_name": self.name,
35
86
  "simple_name": self.simple_name,
@@ -38,70 +89,125 @@ class Definition:
38
89
  "basename": Path(self.filename).name,
39
90
  "line": self.line,
40
91
  "confidence": self.confidence,
41
- "references": self.references
92
+ "references": self.references,
42
93
  }
43
94
 
95
+
44
96
  class Visitor(ast.NodeVisitor):
45
- def __init__(self,mod,file):
46
- self.mod=mod
47
- self.file=file
48
- self.defs=[]
49
- self.refs=[]
50
- self.cls=None
51
- self.alias={}
52
- self.dyn=set()
53
- self.exports=set()
97
+ def __init__(self, mod, file):
98
+ self.mod = mod
99
+ self.file = file
100
+ self.defs = []
101
+ self.refs = []
102
+ self.cls = None
103
+ self.alias = {}
104
+ self.dyn = set()
105
+ self.exports = set()
54
106
  self.current_function_scope = []
107
+ self.current_function_params = []
108
+ self.local_var_maps = []
109
+ self.in_cst_class = 0
110
+ self.local_type_maps = []
111
+ self._dataclass_stack = []
112
+ self.dataclass_fields = set()
113
+ self.first_read_lineno = {}
114
+
115
+ def add_def(self, name, t, line):
116
+ found = False
117
+ for d in self.defs:
118
+ if d.name == name:
119
+ found = True
120
+ break
121
+ if not found:
122
+ self.defs.append(Definition(name, t, self.file, line))
55
123
 
56
- def add_def(self,n,t,l):
57
- if n not in{d.name for d in self.defs}:self.defs.append(Definition(n,t,self.file,l))
124
+ def add_ref(self, name):
125
+ self.refs.append((name, self.file))
58
126
 
59
- def add_ref(self,n):self.refs.append((n,self.file))
127
+ def qual(self, name):
128
+ if name in self.alias:
129
+ return self.alias[name]
130
+ if name in PYTHON_BUILTINS:
131
+ if self.mod:
132
+ mod_candidate = f"{self.mod}.{name}"
133
+ else:
134
+ mod_candidate = name
135
+ if any(d.name == mod_candidate for d in self.defs):
136
+ return mod_candidate
137
+
138
+ if self.mod:
139
+ return f"{self.mod}.{name}"
140
+ else:
141
+ return name
60
142
 
61
- def qual(self,n):
62
- if n in self.alias:return self.alias[n]
63
- if n in PYTHON_BUILTINS:return n
64
- return f"{self.mod}.{n}"if self.mod else n
143
+ def visit_Global(self, node):
144
+ if self.current_function_scope and self.local_var_maps:
145
+ for name in node.names:
146
+ self.local_var_maps[-1][name] = f"{self.mod}.{name}"
147
+ return
65
148
 
66
149
  def visit_annotation(self, node):
67
150
  if node is not None:
68
151
  if isinstance(node, ast.Constant) and isinstance(node.value, str):
69
152
  self.visit_string_annotation(node.value)
70
- elif hasattr(node, 's') and isinstance(node.s, str):
153
+ elif hasattr(node, "s") and isinstance(node.s, str):
71
154
  self.visit_string_annotation(node.s)
72
155
  else:
73
156
  self.visit(node)
74
157
 
75
158
  def visit_string_annotation(self, annotation_str):
159
+ if not isinstance(annotation_str, str):
160
+ return
161
+
76
162
  try:
77
- parsed = ast.parse(annotation_str, mode='eval')
163
+ parsed = ast.parse(annotation_str, mode="eval")
78
164
  self.visit(parsed.body)
79
165
  except:
80
- pass
166
+ for tok in re.findall(r"[A-Za-z_][A-Za-z0-9_]*", annotation_str):
167
+ self.add_ref(tok)
81
168
 
82
- def visit_Import(self,node):
169
+ def visit_Import(self, node):
83
170
  for a in node.names:
84
- full=a.name
85
- self.alias[a.asname or a.name.split(".")[-1]]=full
86
- self.add_def(full,"import",node.lineno)
171
+ full = a.name
87
172
 
88
- def visit_ImportFrom(self,node):
89
- if node.module is None:return
173
+ if a.asname:
174
+ alias_name = a.asname
175
+ target = full
176
+ else:
177
+ head = full.split(".", 1)[0]
178
+ alias_name = head
179
+ target = head
180
+
181
+ self.alias[alias_name] = target
182
+ self.add_def(target, "import", node.lineno)
183
+
184
+ def visit_ImportFrom(self, node):
185
+ if node.module is None:
186
+ return
90
187
  for a in node.names:
91
- if a.name=="*":continue
92
- base=node.module
188
+ if a.name == "*":
189
+ continue
190
+ base = node.module
93
191
  if node.level:
94
- parts=self.mod.split(".")
95
- base=".".join(parts[:-node.level])+(f".{node.module}"if node.module else"")
96
- full=f"{base}.{a.name}"
97
- self.alias[a.asname or a.name]=full
98
- self.add_def(full,"import",node.lineno)
192
+ parts = self.mod.split(".")
193
+ base = ".".join(parts[: -node.level]) + (
194
+ f".{node.module}" if node.module else ""
195
+ )
196
+
197
+ full = f"{base}.{a.name}"
198
+ if a.asname:
199
+ self.alias[a.asname] = full
200
+ self.add_def(full, "import", node.lineno)
201
+ else:
202
+ self.alias[a.name] = full
203
+ self.add_def(full, "import", node.lineno)
99
204
 
100
205
  def visit_arguments(self, args):
101
206
  for arg in args.args:
102
207
  self.visit_annotation(arg.annotation)
103
208
  for arg in args.posonlyargs:
104
209
  self.visit_annotation(arg.annotation)
210
+
105
211
  for arg in args.kwonlyargs:
106
212
  self.visit_annotation(arg.annotation)
107
213
  if args.vararg:
@@ -114,55 +220,183 @@ class Visitor(ast.NodeVisitor):
114
220
  if default:
115
221
  self.visit(default)
116
222
 
117
- def visit_FunctionDef(self,node):
118
- outer_scope_prefix = '.'.join(self.current_function_scope) + '.' if self.current_function_scope else ''
119
-
223
+ def visit_FunctionDef(self, node):
224
+ outer_scope_prefix = (
225
+ ".".join(self.current_function_scope) + "."
226
+ if self.current_function_scope
227
+ else ""
228
+ )
229
+
120
230
  if self.cls:
121
231
  name_parts = [self.mod, self.cls, outer_scope_prefix + node.name]
122
232
  else:
123
233
  name_parts = [self.mod, outer_scope_prefix + node.name]
124
-
234
+
125
235
  qualified_name = ".".join(filter(None, name_parts))
126
236
 
127
- self.add_def(qualified_name,"method"if self.cls else"function",node.lineno)
128
-
237
+ if self.cls:
238
+ def_type = "method"
239
+ else:
240
+ def_type = "function"
241
+ self.add_def(qualified_name, def_type, node.lineno)
242
+
129
243
  self.current_function_scope.append(node.name)
130
-
244
+ self.local_var_maps.append({})
245
+ self.local_type_maps.append({})
246
+
247
+ old_params = self.current_function_params
248
+ self.current_function_params = []
249
+
131
250
  for d_node in node.decorator_list:
132
251
  self.visit(d_node)
133
-
252
+
253
+ for arg in node.args.args:
254
+ param_name = f"{qualified_name}.{arg.arg}"
255
+ self.add_def(param_name, "parameter", node.lineno)
256
+ self.current_function_params.append((arg.arg, param_name))
257
+
134
258
  self.visit_arguments(node.args)
135
259
  self.visit_annotation(node.returns)
136
-
260
+
137
261
  for stmt in node.body:
138
262
  self.visit(stmt)
263
+
139
264
  self.current_function_scope.pop()
140
-
141
- visit_AsyncFunctionDef=visit_FunctionDef
265
+ self.current_function_params = old_params
266
+ self.local_var_maps.pop()
267
+ self.local_type_maps.pop()
268
+
269
+ visit_AsyncFunctionDef = visit_FunctionDef
270
+
271
+ def visit_ClassDef(self, node):
272
+ cname = f"{self.mod}.{node.name}"
273
+ self.add_def(cname, "class", node.lineno)
274
+
275
+ is_cst = False
276
+ is_dc = False
142
277
 
143
- def visit_ClassDef(self,node):
144
- cname=f"{self.mod}.{node.name}"
145
- self.add_def(cname,"class",node.lineno)
146
-
147
278
  for base in node.bases:
279
+ base_name = ""
280
+
281
+ if isinstance(base, ast.Attribute):
282
+ base_name = base.attr
283
+
284
+ elif isinstance(base, ast.Name):
285
+ base_name = base.id
148
286
  self.visit(base)
287
+
288
+ if base_name in {"CSTTransformer", "CSTVisitor"}:
289
+ is_cst = True
290
+
149
291
  for keyword in node.keywords:
150
292
  self.visit(keyword.value)
293
+
151
294
  for decorator in node.decorator_list:
295
+
296
+ def _is_dc(dec):
297
+ if isinstance(dec, ast.Call):
298
+ target = dec.func
299
+ else:
300
+ target = dec
301
+
302
+ if isinstance(target, ast.Name):
303
+ return target.id == "dataclass"
304
+
305
+ if isinstance(target, ast.Attribute):
306
+ return target.attr == "dataclass"
307
+
308
+ return False
309
+
310
+ if _is_dc(decorator):
311
+ is_dc = True
152
312
  self.visit(decorator)
153
-
154
- prev=self.cls;self.cls=node.name
155
- for b in node.body:self.visit(b)
156
- self.cls=prev
313
+
314
+ prev = self.cls
315
+ if is_cst:
316
+ self.in_cst_class += 1
317
+
318
+ self.cls = node.name
319
+ self._dataclass_stack.append(is_dc)
320
+ for b in node.body:
321
+ self.visit(b)
322
+
323
+ self.cls = prev
324
+ self._dataclass_stack.pop()
325
+
326
+ if is_cst:
327
+ self.in_cst_class -= 1
157
328
 
158
329
  def visit_AnnAssign(self, node):
159
330
  self.visit_annotation(node.annotation)
160
331
  if node.value:
161
332
  self.visit(node.value)
162
- self.visit(node.target)
333
+
334
+ def _define(t):
335
+ if isinstance(t, ast.Name):
336
+ name_simple = t.id
337
+ scope_parts = [self.mod]
338
+ if self.cls:
339
+ scope_parts.append(self.cls)
340
+
341
+ if self.current_function_scope:
342
+ scope_parts.extend(self.current_function_scope)
343
+ prefix = ".".join(filter(None, scope_parts))
344
+ if prefix:
345
+ var_name = f"{prefix}.{name_simple}"
346
+ else:
347
+ var_name = name_simple
348
+
349
+ self.add_def(var_name, "variable", t.lineno)
350
+ if (
351
+ self._dataclass_stack
352
+ and self._dataclass_stack[-1]
353
+ and self.cls
354
+ and not self.current_function_scope
355
+ ):
356
+ self.dataclass_fields.add(var_name)
357
+
358
+ if self.current_function_scope and self.local_var_maps:
359
+ self.local_var_maps[-1][name_simple] = var_name
360
+
361
+ elif isinstance(t, (ast.Tuple, ast.List)):
362
+ for elt in t.elts:
363
+ _define(elt)
364
+
365
+ _define(node.target)
163
366
 
164
367
  def visit_AugAssign(self, node):
165
- self.visit(node.target)
368
+ if isinstance(node.target, ast.Name):
369
+ nm = node.target.id
370
+ if (
371
+ self.current_function_scope
372
+ and self.local_var_maps
373
+ and self.local_var_maps
374
+ and nm in self.local_var_maps[-1]
375
+ ):
376
+ # self.add_ref(self.local_var_maps[-1][nm])
377
+ fq = self.local_var_maps[-1][nm]
378
+ self.add_ref(fq)
379
+ var_name = fq
380
+
381
+ else:
382
+ self.add_ref(self.qual(nm))
383
+ scope_parts = [self.mod]
384
+ if self.cls:
385
+ scope_parts.append(self.cls)
386
+
387
+ if self.current_function_scope:
388
+ scope_parts.extend(self.current_function_scope)
389
+ prefix = ".".join(filter(None, scope_parts))
390
+ if prefix:
391
+ var_name = f"{prefix}.{nm}"
392
+ else:
393
+ var_name = nm
394
+
395
+ self.add_def(var_name, "variable", node.lineno)
396
+ if self.current_function_scope and self.local_var_maps:
397
+ self.local_var_maps[-1][nm] = var_name
398
+ else:
399
+ self.visit(node.target)
166
400
  self.visit(node.value)
167
401
 
168
402
  def visit_Subscript(self, node):
@@ -178,6 +412,56 @@ class Visitor(ast.NodeVisitor):
178
412
  self.visit(node.step)
179
413
 
180
414
  def visit_Assign(self, node):
415
+ def process_target_for_def(target_node):
416
+ if isinstance(target_node, ast.Name):
417
+ name_simple = target_node.id
418
+ if (
419
+ name_simple == "METADATA_DEPENDENCIES"
420
+ and self.cls
421
+ and self.in_cst_class > 0
422
+ ):
423
+ return
424
+ if (
425
+ name_simple == "__all__"
426
+ and not self.current_function_scope
427
+ and not self.cls
428
+ ):
429
+ return
430
+
431
+ scope_parts = [self.mod]
432
+ if self.cls:
433
+ scope_parts.append(self.cls)
434
+ if self.current_function_scope:
435
+ scope_parts.extend(self.current_function_scope)
436
+
437
+ if (
438
+ self.current_function_scope
439
+ and self.local_var_maps
440
+ and name_simple in self.local_var_maps[-1]
441
+ ):
442
+ var_name = self.local_var_maps[-1][name_simple]
443
+ else:
444
+ prefix = ".".join(filter(None, scope_parts))
445
+ if prefix:
446
+ var_name = f"{prefix}.{name_simple}"
447
+ else:
448
+ var_name = name_simple
449
+
450
+ self.add_def(var_name, "variable", target_node.lineno)
451
+ if (
452
+ self.current_function_scope
453
+ and self.local_var_maps
454
+ and name_simple not in self.local_var_maps[-1]
455
+ ):
456
+ self.local_var_maps[-1][name_simple] = var_name
457
+
458
+ elif isinstance(target_node, (ast.Tuple, ast.List)):
459
+ for elt in target_node.elts:
460
+ process_target_for_def(elt)
461
+
462
+ for t in node.targets:
463
+ process_target_for_def(t)
464
+
181
465
  for target in node.targets:
182
466
  if isinstance(target, ast.Name) and target.id == "__all__":
183
467
  if isinstance(node.value, (ast.List, ast.Tuple)):
@@ -185,47 +469,175 @@ class Visitor(ast.NodeVisitor):
185
469
  value = None
186
470
  if isinstance(elt, ast.Constant) and isinstance(elt.value, str):
187
471
  value = elt.value
188
- elif hasattr(elt, 's') and isinstance(elt.s, str):
472
+ elif hasattr(elt, "s") and isinstance(elt.s, str):
189
473
  value = elt.s
190
-
474
+
191
475
  if value is not None:
192
- full_name = f"{self.mod}.{value}"
193
- self.add_ref(full_name)
476
+ if self.mod:
477
+ export_name = f"{self.mod}.{value}"
478
+ else:
479
+ export_name = value
480
+
481
+ self.add_ref(export_name)
194
482
  self.add_ref(value)
483
+
484
+ try:
485
+ if isinstance(node.value, ast.Call):
486
+ callee = node.value.func
487
+ fqname = None
488
+
489
+ if isinstance(callee, ast.Name):
490
+ fqname = self.alias.get(callee.id, self.qual(callee.id))
491
+
492
+ elif isinstance(callee, ast.Attribute):
493
+ parts = []
494
+ cur = callee
495
+ while isinstance(cur, ast.Attribute):
496
+ parts.append(cur.attr)
497
+ cur = cur.value
498
+ head = None
499
+ if isinstance(cur, ast.Name):
500
+ head = self.alias.get(cur.id, self.qual(cur.id))
501
+ if head:
502
+ fqname = ".".join([head] + list(reversed(parts)))
503
+
504
+ if fqname and self.current_function_scope and self.local_type_maps:
505
+
506
+ def _mark_target(t):
507
+ if isinstance(t, ast.Name):
508
+ self.local_type_maps[-1][t.id] = fqname
509
+ elif isinstance(t, (ast.Tuple, ast.List)):
510
+ for elt in t.elts:
511
+ _mark_target(elt)
512
+
513
+ for t in node.targets:
514
+ _mark_target(t)
515
+ except Exception:
516
+ pass
517
+
195
518
  self.generic_visit(node)
196
519
 
197
520
  def visit_Call(self, node):
198
521
  self.generic_visit(node)
199
-
200
- if isinstance(node.func, ast.Name) and node.func.id in ("getattr", "hasattr") and len(node.args) >= 2:
201
- if isinstance(node.args[1], ast.Constant) and isinstance(node.args[1].value, str):
522
+
523
+ if (
524
+ isinstance(node.func, ast.Name)
525
+ and node.func.id in ("getattr", "hasattr")
526
+ and len(node.args) >= 2
527
+ ):
528
+ if isinstance(node.args[1], ast.Constant) and isinstance(
529
+ node.args[1].value, str
530
+ ):
202
531
  attr_name = node.args[1].value
203
532
  self.add_ref(attr_name)
204
-
533
+
205
534
  if isinstance(node.args[0], ast.Name):
206
535
  module_name = node.args[0].id
207
- if module_name != "self":
536
+ if module_name != "self":
208
537
  qualified_name = f"{self.qual(module_name)}.{attr_name}"
209
538
  self.add_ref(qualified_name)
210
-
539
+
540
+ elif isinstance(node.args[0], ast.Name):
541
+ target_name = node.args[0].id
542
+ if target_name != "self":
543
+ if self.mod:
544
+ self.dyn.add(self.mod.split(".")[0])
545
+ else:
546
+ self.dyn.add("")
547
+
211
548
  elif isinstance(node.func, ast.Name) and node.func.id == "globals":
212
- parent = getattr(node, 'parent', None)
213
- if (isinstance(parent, ast.Subscript) and
214
- isinstance(parent.slice, ast.Constant) and
215
- isinstance(parent.slice.value, str)):
549
+ parent = getattr(node, "parent", None)
550
+ if (
551
+ isinstance(parent, ast.Subscript)
552
+ and isinstance(parent.slice, ast.Constant)
553
+ and isinstance(parent.slice.value, str)
554
+ ):
216
555
  func_name = parent.slice.value
217
556
  self.add_ref(func_name)
218
557
  self.add_ref(f"{self.mod}.{func_name}")
219
558
 
220
- def visit_Name(self,node):
221
- if isinstance(node.ctx,ast.Load):
222
- self.add_ref(self.qual(node.id))
223
- if node.id in DYNAMIC_PATTERNS:self.dyn.add(self.mod.split(".")[0])
559
+ elif isinstance(node.func, ast.Name) and node.func.id in ("eval", "exec"):
560
+ root_mod = ""
561
+ if self.mod:
562
+ root_mod = self.mod.split(".")[0]
563
+ self.dyn.add(root_mod)
564
+
565
+ def visit_Name(self, node):
566
+ if isinstance(node.ctx, ast.Load):
567
+ for param_name, param_full_name in self.current_function_params:
568
+ if node.id == param_name:
569
+ self.first_read_lineno.setdefault(param_full_name, node.lineno)
570
+ self.add_ref(param_full_name)
571
+ return
572
+
573
+ if (
574
+ self.current_function_scope
575
+ and self.local_var_maps
576
+ and node.id in self.local_var_maps[-1]
577
+ ):
578
+ fq = self.local_var_maps[-1][node.id]
579
+ self.first_read_lineno.setdefault(fq, node.lineno)
580
+ self.add_ref(fq)
581
+ return
224
582
 
225
- def visit_Attribute(self,node):
583
+ qualified = self.qual(node.id)
584
+ self.first_read_lineno.setdefault(qualified, node.lineno)
585
+ self.add_ref(qualified)
586
+ if node.id in DYNAMIC_PATTERNS:
587
+ self.dyn.add(self.mod.split(".")[0])
588
+
589
+ def visit_Attribute(self, node):
226
590
  self.generic_visit(node)
227
- if isinstance(node.ctx,ast.Load)and isinstance(node.value,ast.Name):
228
- self.add_ref(f"{self.qual(node.value.id)}.{node.attr}")
591
+
592
+ if not isinstance(node.ctx, ast.Load):
593
+ return
594
+
595
+ if isinstance(node.value, ast.Name):
596
+ base = node.value.id
597
+
598
+ param_hit = None
599
+ for param_name, param_full in self.current_function_params:
600
+ if base == param_name:
601
+ param_hit = (param_name, param_full)
602
+ break
603
+
604
+ if param_hit:
605
+ self.add_ref(param_hit[1])
606
+
607
+ if self.cls and base in {"self", "cls"}:
608
+ if self.mod:
609
+ owner = f"{self.mod}.{self.cls}"
610
+ else:
611
+ owner = self.cls
612
+
613
+ self.add_ref(f"{owner}.{node.attr}")
614
+ return
615
+
616
+ if (
617
+ self.current_function_scope
618
+ and self.local_type_maps
619
+ and self.local_type_maps[-1].get(base)
620
+ ):
621
+ self.add_ref(f"{self.local_type_maps[-1][base]}.{node.attr}")
622
+ return
623
+
624
+ self.add_ref(f"{self.qual(base)}.{node.attr}")
625
+
626
+ def visit_NamedExpr(self, node):
627
+ self.visit(node.value)
628
+ if isinstance(node.target, ast.Name):
629
+ nm = node.target.id
630
+ scope_parts = [self.mod]
631
+ if self.cls:
632
+ scope_parts.append(self.cls)
633
+ if self.current_function_scope:
634
+ scope_parts.extend(self.current_function_scope)
635
+ prefix = ".".join(filter(None, scope_parts))
636
+ var_name = f"{prefix}.{nm}" if prefix else nm
637
+ self.add_def(var_name, "variable", node.lineno)
638
+ if self.current_function_scope and self.local_var_maps:
639
+ self.local_var_maps[-1][nm] = var_name
640
+ self.add_ref(var_name)
229
641
 
230
642
  def visit_keyword(self, node):
231
643
  self.visit(node.value)
@@ -250,4 +662,4 @@ class Visitor(ast.NodeVisitor):
250
662
  self.visit(item)
251
663
  elif isinstance(value, ast.AST):
252
664
  value.parent = node
253
- self.visit(value)
665
+ self.visit(value)