cisco-ai-skill-scanner 1.0.0__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 (100) hide show
  1. cisco_ai_skill_scanner-1.0.0.dist-info/METADATA +253 -0
  2. cisco_ai_skill_scanner-1.0.0.dist-info/RECORD +100 -0
  3. cisco_ai_skill_scanner-1.0.0.dist-info/WHEEL +4 -0
  4. cisco_ai_skill_scanner-1.0.0.dist-info/entry_points.txt +4 -0
  5. cisco_ai_skill_scanner-1.0.0.dist-info/licenses/LICENSE +17 -0
  6. skillanalyzer/__init__.py +45 -0
  7. skillanalyzer/_version.py +34 -0
  8. skillanalyzer/api/__init__.py +25 -0
  9. skillanalyzer/api/api.py +34 -0
  10. skillanalyzer/api/api_cli.py +78 -0
  11. skillanalyzer/api/api_server.py +634 -0
  12. skillanalyzer/api/router.py +527 -0
  13. skillanalyzer/cli/__init__.py +25 -0
  14. skillanalyzer/cli/cli.py +816 -0
  15. skillanalyzer/config/__init__.py +26 -0
  16. skillanalyzer/config/config.py +149 -0
  17. skillanalyzer/config/config_parser.py +122 -0
  18. skillanalyzer/config/constants.py +85 -0
  19. skillanalyzer/core/__init__.py +24 -0
  20. skillanalyzer/core/analyzers/__init__.py +75 -0
  21. skillanalyzer/core/analyzers/aidefense_analyzer.py +872 -0
  22. skillanalyzer/core/analyzers/base.py +53 -0
  23. skillanalyzer/core/analyzers/behavioral/__init__.py +30 -0
  24. skillanalyzer/core/analyzers/behavioral/alignment/__init__.py +45 -0
  25. skillanalyzer/core/analyzers/behavioral/alignment/alignment_llm_client.py +240 -0
  26. skillanalyzer/core/analyzers/behavioral/alignment/alignment_orchestrator.py +216 -0
  27. skillanalyzer/core/analyzers/behavioral/alignment/alignment_prompt_builder.py +422 -0
  28. skillanalyzer/core/analyzers/behavioral/alignment/alignment_response_validator.py +136 -0
  29. skillanalyzer/core/analyzers/behavioral/alignment/threat_vulnerability_classifier.py +198 -0
  30. skillanalyzer/core/analyzers/behavioral_analyzer.py +453 -0
  31. skillanalyzer/core/analyzers/cross_skill_analyzer.py +490 -0
  32. skillanalyzer/core/analyzers/llm_analyzer.py +440 -0
  33. skillanalyzer/core/analyzers/llm_prompt_builder.py +270 -0
  34. skillanalyzer/core/analyzers/llm_provider_config.py +215 -0
  35. skillanalyzer/core/analyzers/llm_request_handler.py +284 -0
  36. skillanalyzer/core/analyzers/llm_response_parser.py +81 -0
  37. skillanalyzer/core/analyzers/meta_analyzer.py +845 -0
  38. skillanalyzer/core/analyzers/static.py +1105 -0
  39. skillanalyzer/core/analyzers/trigger_analyzer.py +341 -0
  40. skillanalyzer/core/analyzers/virustotal_analyzer.py +463 -0
  41. skillanalyzer/core/exceptions.py +77 -0
  42. skillanalyzer/core/loader.py +377 -0
  43. skillanalyzer/core/models.py +300 -0
  44. skillanalyzer/core/reporters/__init__.py +26 -0
  45. skillanalyzer/core/reporters/json_reporter.py +65 -0
  46. skillanalyzer/core/reporters/markdown_reporter.py +209 -0
  47. skillanalyzer/core/reporters/sarif_reporter.py +246 -0
  48. skillanalyzer/core/reporters/table_reporter.py +195 -0
  49. skillanalyzer/core/rules/__init__.py +19 -0
  50. skillanalyzer/core/rules/patterns.py +165 -0
  51. skillanalyzer/core/rules/yara_scanner.py +157 -0
  52. skillanalyzer/core/scanner.py +437 -0
  53. skillanalyzer/core/static_analysis/__init__.py +27 -0
  54. skillanalyzer/core/static_analysis/cfg/__init__.py +21 -0
  55. skillanalyzer/core/static_analysis/cfg/builder.py +439 -0
  56. skillanalyzer/core/static_analysis/context_extractor.py +742 -0
  57. skillanalyzer/core/static_analysis/dataflow/__init__.py +25 -0
  58. skillanalyzer/core/static_analysis/dataflow/forward_analysis.py +715 -0
  59. skillanalyzer/core/static_analysis/interprocedural/__init__.py +21 -0
  60. skillanalyzer/core/static_analysis/interprocedural/call_graph_analyzer.py +406 -0
  61. skillanalyzer/core/static_analysis/interprocedural/cross_file_analyzer.py +190 -0
  62. skillanalyzer/core/static_analysis/parser/__init__.py +21 -0
  63. skillanalyzer/core/static_analysis/parser/python_parser.py +380 -0
  64. skillanalyzer/core/static_analysis/semantic/__init__.py +28 -0
  65. skillanalyzer/core/static_analysis/semantic/name_resolver.py +206 -0
  66. skillanalyzer/core/static_analysis/semantic/type_analyzer.py +200 -0
  67. skillanalyzer/core/static_analysis/taint/__init__.py +21 -0
  68. skillanalyzer/core/static_analysis/taint/tracker.py +252 -0
  69. skillanalyzer/core/static_analysis/types/__init__.py +36 -0
  70. skillanalyzer/data/__init__.py +30 -0
  71. skillanalyzer/data/prompts/boilerplate_protection_rule_prompt.md +26 -0
  72. skillanalyzer/data/prompts/code_alignment_threat_analysis_prompt.md +901 -0
  73. skillanalyzer/data/prompts/llm_response_schema.json +71 -0
  74. skillanalyzer/data/prompts/skill_meta_analysis_prompt.md +303 -0
  75. skillanalyzer/data/prompts/skill_threat_analysis_prompt.md +263 -0
  76. skillanalyzer/data/prompts/unified_response_schema.md +97 -0
  77. skillanalyzer/data/rules/signatures.yaml +440 -0
  78. skillanalyzer/data/yara_rules/autonomy_abuse.yara +66 -0
  79. skillanalyzer/data/yara_rules/code_execution.yara +61 -0
  80. skillanalyzer/data/yara_rules/coercive_injection.yara +115 -0
  81. skillanalyzer/data/yara_rules/command_injection.yara +54 -0
  82. skillanalyzer/data/yara_rules/credential_harvesting.yara +115 -0
  83. skillanalyzer/data/yara_rules/prompt_injection.yara +71 -0
  84. skillanalyzer/data/yara_rules/script_injection.yara +83 -0
  85. skillanalyzer/data/yara_rules/skill_discovery_abuse.yara +57 -0
  86. skillanalyzer/data/yara_rules/sql_injection.yara +73 -0
  87. skillanalyzer/data/yara_rules/system_manipulation.yara +65 -0
  88. skillanalyzer/data/yara_rules/tool_chaining_abuse.yara +60 -0
  89. skillanalyzer/data/yara_rules/transitive_trust_abuse.yara +73 -0
  90. skillanalyzer/data/yara_rules/unicode_steganography.yara +65 -0
  91. skillanalyzer/hooks/__init__.py +21 -0
  92. skillanalyzer/hooks/pre_commit.py +450 -0
  93. skillanalyzer/threats/__init__.py +25 -0
  94. skillanalyzer/threats/threats.py +480 -0
  95. skillanalyzer/utils/__init__.py +28 -0
  96. skillanalyzer/utils/command_utils.py +129 -0
  97. skillanalyzer/utils/di_container.py +154 -0
  98. skillanalyzer/utils/file_utils.py +86 -0
  99. skillanalyzer/utils/logging_config.py +96 -0
  100. skillanalyzer/utils/logging_utils.py +71 -0
@@ -0,0 +1,380 @@
1
+ # Copyright 2026 Cisco Systems, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ #
15
+ # SPDX-License-Identifier: Apache-2.0
16
+
17
+ """
18
+ Python AST parser for Claude Skills scripts.
19
+ """
20
+
21
+ import ast
22
+ from dataclasses import dataclass, field
23
+
24
+
25
+ @dataclass
26
+ class FunctionInfo:
27
+ """Information about a function in a skill script."""
28
+
29
+ name: str
30
+ parameters: list[str]
31
+ docstring: str | None
32
+ line_number: int
33
+ source_code: str
34
+ ast_node: ast.FunctionDef
35
+
36
+ # Security indicators
37
+ has_network_calls: bool = False
38
+ has_file_operations: bool = False
39
+ has_subprocess: bool = False
40
+ has_eval_exec: bool = False
41
+
42
+ # Collected elements
43
+ imports: list[str] = field(default_factory=list)
44
+ function_calls: list[str] = field(default_factory=list)
45
+ string_literals: list[str] = field(default_factory=list)
46
+ assignments: list[str] = field(default_factory=list)
47
+
48
+
49
+ class PythonParser:
50
+ """Parse Python source code and extract security-relevant information."""
51
+
52
+ # Security-relevant patterns
53
+ NETWORK_MODULES = ["requests", "urllib", "http", "socket", "aiohttp"]
54
+ FILE_OPERATIONS = ["open", "read", "write", "Path", "os.remove", "shutil"]
55
+ SUBPROCESS_PATTERNS = ["subprocess", "os.system", "os.popen"]
56
+ DANGEROUS_FUNCTIONS = ["eval", "exec", "compile", "__import__"]
57
+
58
+ # Claude Code tool indicators - map code patterns to Claude Code tools
59
+ TOOL_INDICATORS = {
60
+ "Read": {
61
+ "open",
62
+ "read",
63
+ "readline",
64
+ "readlines",
65
+ "Path.read_text",
66
+ "Path.read_bytes",
67
+ "json.load",
68
+ "yaml.safe_load",
69
+ "configparser",
70
+ },
71
+ "Write": {
72
+ "write",
73
+ "writelines",
74
+ "Path.write_text",
75
+ "Path.write_bytes",
76
+ "json.dump",
77
+ "yaml.dump",
78
+ },
79
+ "Bash": {
80
+ "subprocess.run",
81
+ "subprocess.call",
82
+ "subprocess.Popen",
83
+ "subprocess.check_output",
84
+ "subprocess.check_call",
85
+ "os.system",
86
+ "os.popen",
87
+ "os.spawn",
88
+ "commands.getoutput",
89
+ "commands.getstatusoutput",
90
+ },
91
+ "Grep": {
92
+ "re.search",
93
+ "re.match",
94
+ "re.findall",
95
+ "re.finditer",
96
+ "re.sub",
97
+ "re.split",
98
+ },
99
+ "Glob": {
100
+ "glob.glob",
101
+ "glob.iglob",
102
+ "Path.glob",
103
+ "Path.rglob",
104
+ "fnmatch.fnmatch",
105
+ "fnmatch.filter",
106
+ },
107
+ "Network": {
108
+ "requests.get",
109
+ "requests.post",
110
+ "requests.put",
111
+ "requests.delete",
112
+ "urllib.request.urlopen",
113
+ "urllib.urlopen",
114
+ "http.client.HTTPConnection",
115
+ "http.client.HTTPSConnection",
116
+ "socket.connect",
117
+ "socket.create_connection",
118
+ "aiohttp.ClientSession",
119
+ "httpx.get",
120
+ "httpx.post",
121
+ },
122
+ }
123
+
124
+ def __init__(self, source_code: str):
125
+ """
126
+ Initialize parser with source code.
127
+
128
+ Args:
129
+ source_code: Python source code to parse
130
+ """
131
+ self.source_code = source_code
132
+ self.tree: ast.Module | None = None
133
+ self.functions: list[FunctionInfo] = []
134
+ self.imports: list[str] = []
135
+ self.global_calls: list[str] = []
136
+ self.module_strings: list[str] = [] # All strings at module/class level
137
+ self.class_attributes: list[dict[str, str]] = [] # Class-level attributes
138
+
139
+ def parse(self) -> bool:
140
+ """
141
+ Parse the source code.
142
+
143
+ Returns:
144
+ True if parsing succeeded, False otherwise
145
+ """
146
+ try:
147
+ self.tree = ast.parse(self.source_code)
148
+ self._extract_imports()
149
+ self._extract_module_level_strings()
150
+ self._extract_functions()
151
+ self._extract_global_code()
152
+ return True
153
+ except SyntaxError as e:
154
+ print(f"Syntax error in source: {e}")
155
+ return False
156
+
157
+ def _extract_module_level_strings(self) -> None:
158
+ """Extract strings from module and class level (not in functions)."""
159
+ if self.tree is None:
160
+ return
161
+ for node in self.tree.body:
162
+ # Module-level assignments
163
+ if isinstance(node, ast.Assign):
164
+ for value_node in ast.walk(node.value):
165
+ if isinstance(value_node, ast.Constant) and isinstance(value_node.value, str):
166
+ self.module_strings.append(value_node.value)
167
+
168
+ # Class definitions
169
+ elif isinstance(node, ast.ClassDef):
170
+ for class_node in node.body:
171
+ if isinstance(class_node, ast.Assign):
172
+ # Class attribute
173
+ for target in class_node.targets:
174
+ if isinstance(target, ast.Name):
175
+ # Extract value if it's a string constant
176
+ if isinstance(class_node.value, ast.Constant):
177
+ if isinstance(class_node.value.value, str):
178
+ self.module_strings.append(class_node.value.value)
179
+ self.class_attributes.append(
180
+ {"name": target.id, "value": class_node.value.value}
181
+ )
182
+
183
+ def _extract_imports(self) -> None:
184
+ """Extract all import statements."""
185
+ if self.tree is None:
186
+ return
187
+ for node in ast.walk(self.tree):
188
+ if isinstance(node, ast.Import):
189
+ for alias in node.names:
190
+ self.imports.append(alias.name)
191
+ elif isinstance(node, ast.ImportFrom):
192
+ if node.module:
193
+ self.imports.append(node.module)
194
+
195
+ def _extract_functions(self) -> None:
196
+ """Extract all function definitions."""
197
+ if self.tree is None:
198
+ return
199
+ for node in ast.walk(self.tree):
200
+ if isinstance(node, ast.FunctionDef):
201
+ func_info = self._analyze_function(node)
202
+ self.functions.append(func_info)
203
+
204
+ def _analyze_function(self, node: ast.FunctionDef) -> FunctionInfo:
205
+ """
206
+ Analyze a function and extract security information.
207
+
208
+ Args:
209
+ node: AST FunctionDef node
210
+
211
+ Returns:
212
+ FunctionInfo with extracted data
213
+ """
214
+ # Extract parameters
215
+ parameters = [arg.arg for arg in node.args.args]
216
+
217
+ # Extract docstring
218
+ docstring = ast.get_docstring(node)
219
+
220
+ # Get source code snippet
221
+ source_lines = self.source_code.split("\n")
222
+ func_source = "\n".join(source_lines[node.lineno - 1 : node.end_lineno])
223
+
224
+ # Create function info
225
+ func_info = FunctionInfo(
226
+ name=node.name,
227
+ parameters=parameters,
228
+ docstring=docstring,
229
+ line_number=node.lineno,
230
+ source_code=func_source,
231
+ ast_node=node,
232
+ imports=self.imports.copy(),
233
+ )
234
+
235
+ # Analyze function body for security indicators
236
+ self._analyze_function_body(node, func_info)
237
+
238
+ return func_info
239
+
240
+ def _analyze_function_body(self, node: ast.FunctionDef, func_info: FunctionInfo):
241
+ """Analyze function body for security patterns."""
242
+
243
+ for child in ast.walk(node):
244
+ # Check for function calls
245
+ if isinstance(child, ast.Call):
246
+ call_name = self._get_call_name(child)
247
+ if call_name:
248
+ func_info.function_calls.append(call_name)
249
+
250
+ # Check for security-relevant calls
251
+ if any(net in call_name for net in self.NETWORK_MODULES):
252
+ func_info.has_network_calls = True
253
+ if any(file_op in call_name for file_op in self.FILE_OPERATIONS):
254
+ func_info.has_file_operations = True
255
+ if any(sub in call_name for sub in self.SUBPROCESS_PATTERNS):
256
+ func_info.has_subprocess = True
257
+ if any(danger in call_name for danger in self.DANGEROUS_FUNCTIONS):
258
+ func_info.has_eval_exec = True
259
+
260
+ # Extract string literals
261
+ elif isinstance(child, ast.Constant) and isinstance(child.value, str):
262
+ if len(child.value) > 5: # Skip very short strings
263
+ func_info.string_literals.append(child.value)
264
+
265
+ # Track assignments
266
+ elif isinstance(child, ast.Assign):
267
+ for target in child.targets:
268
+ if isinstance(target, ast.Name):
269
+ func_info.assignments.append(target.id)
270
+
271
+ def _get_call_name(self, node: ast.Call) -> str | None:
272
+ """Extract function call name from AST node."""
273
+ if isinstance(node.func, ast.Name):
274
+ return node.func.id
275
+ elif isinstance(node.func, ast.Attribute):
276
+ # Handle module.function() calls
277
+ if isinstance(node.func.value, ast.Name):
278
+ return f"{node.func.value.id}.{node.func.attr}"
279
+ return node.func.attr
280
+ return None
281
+
282
+ def _extract_global_code(self) -> None:
283
+ """Extract code executed at module level (not in functions)."""
284
+ if self.tree is None:
285
+ return
286
+ for node in self.tree.body:
287
+ if isinstance(node, ast.Expr) and isinstance(node.value, ast.Call):
288
+ call_name = self._get_call_name(node.value)
289
+ if call_name:
290
+ self.global_calls.append(call_name)
291
+
292
+ def get_functions(self) -> list[FunctionInfo]:
293
+ """Get all analyzed functions."""
294
+ return self.functions
295
+
296
+ def has_security_indicators(self) -> dict[str, bool]:
297
+ """Check if code has any security indicators."""
298
+ return {
299
+ "has_network": any(f.has_network_calls for f in self.functions),
300
+ "has_file_ops": any(f.has_file_operations for f in self.functions),
301
+ "has_subprocess": any(f.has_subprocess for f in self.functions),
302
+ "has_eval_exec": any(f.has_eval_exec for f in self.functions),
303
+ "has_dangerous_imports": any(
304
+ mod in self.imports for mod in self.NETWORK_MODULES + self.SUBPROCESS_PATTERNS
305
+ ),
306
+ }
307
+
308
+ def get_inferred_tools(self) -> dict[str, bool]:
309
+ """
310
+ Determine which Claude Code tools are implied by the code patterns.
311
+
312
+ Analyzes function calls, imports, and patterns in the code to infer
313
+ which Claude Code tools would be needed to execute similar operations.
314
+
315
+ Returns:
316
+ Dictionary mapping tool names to whether they are detected
317
+ """
318
+ inferred = {tool: False for tool in self.TOOL_INDICATORS.keys()}
319
+
320
+ # Collect all function calls from all functions
321
+ all_calls = set()
322
+ for func in self.functions:
323
+ all_calls.update(func.function_calls)
324
+ all_calls.update(self.global_calls)
325
+
326
+ # Also check imports for module-level indicators
327
+ import_based_tools = {
328
+ "requests": "Network",
329
+ "urllib": "Network",
330
+ "aiohttp": "Network",
331
+ "httpx": "Network",
332
+ "socket": "Network",
333
+ "subprocess": "Bash",
334
+ "glob": "Glob",
335
+ "fnmatch": "Glob",
336
+ "re": "Grep",
337
+ }
338
+
339
+ for module in self.imports:
340
+ base_module = module.split(".")[0]
341
+ if base_module in import_based_tools:
342
+ tool = import_based_tools[base_module]
343
+ inferred[tool] = True
344
+
345
+ # Check function calls against tool indicators
346
+ for tool, patterns in self.TOOL_INDICATORS.items():
347
+ for call in all_calls:
348
+ # Check both exact match and partial match for method calls
349
+ for pattern in patterns:
350
+ if pattern in call or call.endswith(pattern.split(".")[-1] if "." in pattern else pattern):
351
+ inferred[tool] = True
352
+ break
353
+ if inferred[tool]:
354
+ break
355
+
356
+ # Check for file operations that indicate Read/Write
357
+ for func in self.functions:
358
+ if func.has_file_operations:
359
+ # Need to check if it's read or write
360
+ for call in func.function_calls:
361
+ if any(r in call for r in ["read", "load"]):
362
+ inferred["Read"] = True
363
+ if any(w in call for w in ["write", "dump"]):
364
+ inferred["Write"] = True
365
+ if func.has_subprocess:
366
+ inferred["Bash"] = True
367
+ if func.has_network_calls:
368
+ inferred["Network"] = True
369
+
370
+ return inferred
371
+
372
+ def get_detected_tools_list(self) -> list[str]:
373
+ """
374
+ Get a list of Claude Code tools that are detected in the code.
375
+
376
+ Returns:
377
+ List of tool names that were detected
378
+ """
379
+ inferred = self.get_inferred_tools()
380
+ return [tool for tool, detected in inferred.items() if detected]
@@ -0,0 +1,28 @@
1
+ # Copyright 2026 Cisco Systems, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ #
15
+ # SPDX-License-Identifier: Apache-2.0
16
+
17
+ """Semantic analysis modules for static analysis."""
18
+
19
+ from .name_resolver import NameResolver, Scope
20
+ from .type_analyzer import Type, TypeAnalyzer, TypeKind
21
+
22
+ __all__ = [
23
+ "NameResolver",
24
+ "Scope",
25
+ "TypeAnalyzer",
26
+ "Type",
27
+ "TypeKind",
28
+ ]
@@ -0,0 +1,206 @@
1
+ # Copyright 2026 Cisco Systems, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ #
15
+ # SPDX-License-Identifier: Apache-2.0
16
+
17
+ """Name resolution analysis for skill code.
18
+
19
+ Tracks variable definitions and resolves name references to their definitions.
20
+ """
21
+
22
+ import ast
23
+ from typing import Any, Optional
24
+
25
+
26
+ class Scope:
27
+ """Represents a lexical scope."""
28
+
29
+ def __init__(self, parent: Optional["Scope"] = None) -> None:
30
+ """Initialize scope.
31
+
32
+ Args:
33
+ parent: Parent scope
34
+ """
35
+ self.parent = parent
36
+ self.symbols: dict[str, Any] = {}
37
+ self.children: list[Scope] = []
38
+
39
+ def define(self, name: str, node: Any) -> None:
40
+ """Define a symbol in this scope.
41
+
42
+ Args:
43
+ name: Symbol name
44
+ node: AST node defining the symbol
45
+ """
46
+ self.symbols[name] = node
47
+
48
+ def lookup(self, name: str) -> Any | None:
49
+ """Look up a symbol in this scope or parent scopes.
50
+
51
+ Args:
52
+ name: Symbol name
53
+
54
+ Returns:
55
+ AST node or None if not found
56
+ """
57
+ if name in self.symbols:
58
+ return self.symbols[name]
59
+ elif self.parent:
60
+ return self.parent.lookup(name)
61
+ return None
62
+
63
+
64
+ class NameResolver:
65
+ """Resolves names to their definitions."""
66
+
67
+ def __init__(self, ast_root: ast.AST):
68
+ """Initialize name resolver.
69
+
70
+ Args:
71
+ ast_root: Root AST node
72
+ """
73
+ self.ast_root = ast_root
74
+ self.global_scope = Scope()
75
+ self.current_scope = self.global_scope
76
+ self.name_to_def: dict[Any, Any] = {}
77
+
78
+ def resolve(self) -> None:
79
+ """Resolve all names in the AST."""
80
+ self._resolve_python(self.ast_root)
81
+
82
+ def _resolve_python(self, node: ast.AST) -> None:
83
+ """Resolve names in Python AST.
84
+
85
+ Args:
86
+ node: Python AST node
87
+ """
88
+ self._visit_node(node)
89
+
90
+ def _visit_node(self, node: ast.AST) -> None:
91
+ """Visit a node and its children with proper scope tracking.
92
+
93
+ Args:
94
+ node: AST node to visit
95
+ """
96
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
97
+ self._visit_function(node)
98
+ elif isinstance(node, ast.ClassDef):
99
+ self._visit_class(node)
100
+ elif isinstance(node, ast.Assign):
101
+ self._define_assignment(node)
102
+ for child in ast.iter_child_nodes(node):
103
+ self._visit_node(child)
104
+ elif isinstance(node, ast.Import):
105
+ self._define_import(node)
106
+ elif isinstance(node, ast.ImportFrom):
107
+ self._define_import_from(node)
108
+ elif isinstance(node, ast.Name) and isinstance(node.ctx, ast.Load):
109
+ self._resolve_name(node)
110
+ else:
111
+ for child in ast.iter_child_nodes(node):
112
+ self._visit_node(child)
113
+
114
+ def _visit_function(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> None:
115
+ """Visit function with proper scope management.
116
+
117
+ Args:
118
+ node: Function definition node
119
+ """
120
+ self.current_scope.define(node.name, node)
121
+
122
+ func_scope = Scope(parent=self.current_scope)
123
+ self.current_scope.children.append(func_scope)
124
+ old_scope = self.current_scope
125
+ self.current_scope = func_scope
126
+
127
+ for arg in node.args.args:
128
+ func_scope.define(arg.arg, arg)
129
+
130
+ for child in node.body:
131
+ self._visit_node(child)
132
+
133
+ self.current_scope = old_scope
134
+
135
+ def _visit_class(self, node: ast.ClassDef) -> None:
136
+ """Visit class with proper scope management.
137
+
138
+ Args:
139
+ node: Class definition node
140
+ """
141
+ self.current_scope.define(node.name, node)
142
+
143
+ class_scope = Scope(parent=self.current_scope)
144
+ self.current_scope.children.append(class_scope)
145
+ old_scope = self.current_scope
146
+ self.current_scope = class_scope
147
+
148
+ for child in node.body:
149
+ self._visit_node(child)
150
+
151
+ self.current_scope = old_scope
152
+
153
+ def _define_assignment(self, node: ast.Assign) -> None:
154
+ """Define variables from assignment.
155
+
156
+ Args:
157
+ node: Assignment node
158
+ """
159
+ for target in node.targets:
160
+ if isinstance(target, ast.Name):
161
+ self.current_scope.define(target.id, node)
162
+ elif isinstance(target, ast.Tuple):
163
+ for elt in target.elts:
164
+ if isinstance(elt, ast.Name):
165
+ self.current_scope.define(elt.id, node)
166
+
167
+ def _define_import(self, node: ast.Import) -> None:
168
+ """Define imported names.
169
+
170
+ Args:
171
+ node: Import node
172
+ """
173
+ for alias in node.names:
174
+ name = alias.asname if alias.asname else alias.name
175
+ self.current_scope.define(name, node)
176
+
177
+ def _define_import_from(self, node: ast.ImportFrom) -> None:
178
+ """Define names from 'from ... import' statement.
179
+
180
+ Args:
181
+ node: ImportFrom node
182
+ """
183
+ for alias in node.names:
184
+ name = alias.asname if alias.asname else alias.name
185
+ self.current_scope.define(name, node)
186
+
187
+ def _resolve_name(self, node: ast.Name) -> None:
188
+ """Resolve a name reference to its definition.
189
+
190
+ Args:
191
+ node: Name node
192
+ """
193
+ definition = self.current_scope.lookup(node.id)
194
+ if definition:
195
+ self.name_to_def[node] = definition
196
+
197
+ def get_definition(self, node: Any) -> Any | None:
198
+ """Get the definition for a name usage.
199
+
200
+ Args:
201
+ node: Name usage node
202
+
203
+ Returns:
204
+ Definition node or None
205
+ """
206
+ return self.name_to_def.get(node)