apisec-code-bolt 0.1.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 (111) hide show
  1. apisec_code_bolt/__init__.py +42 -0
  2. apisec_code_bolt/__main__.py +11 -0
  3. apisec_code_bolt/analysis/__init__.py +96 -0
  4. apisec_code_bolt/analysis/analyzer.py +2309 -0
  5. apisec_code_bolt/analysis/binding_tracker.py +341 -0
  6. apisec_code_bolt/analysis/call_graph.py +1197 -0
  7. apisec_code_bolt/analysis/call_graph_types.py +332 -0
  8. apisec_code_bolt/analysis/call_resolver.py +988 -0
  9. apisec_code_bolt/analysis/capability_tagger.py +322 -0
  10. apisec_code_bolt/analysis/config_scanner.py +197 -0
  11. apisec_code_bolt/analysis/data_flow.py +1883 -0
  12. apisec_code_bolt/analysis/dependency_extractor.py +959 -0
  13. apisec_code_bolt/analysis/flow_analysis.py +1406 -0
  14. apisec_code_bolt/analysis/hof_catalog.py +61 -0
  15. apisec_code_bolt/analysis/integration_detector.py +1399 -0
  16. apisec_code_bolt/analysis/literal_scanner.py +300 -0
  17. apisec_code_bolt/analysis/path_normalizer.py +55 -0
  18. apisec_code_bolt/analysis/read_site_detector.py +310 -0
  19. apisec_code_bolt/analysis/request_patterns.py +162 -0
  20. apisec_code_bolt/analysis/sensitivity_classifier.py +224 -0
  21. apisec_code_bolt/analysis/sink_evidence.py +333 -0
  22. apisec_code_bolt/analysis/url_prefix_resolver.py +338 -0
  23. apisec_code_bolt/cli/__init__.py +5 -0
  24. apisec_code_bolt/cli/exit_codes.py +17 -0
  25. apisec_code_bolt/cli/main.py +1069 -0
  26. apisec_code_bolt/cloud/__init__.py +1 -0
  27. apisec_code_bolt/cloud/apisec_client.py +118 -0
  28. apisec_code_bolt/cloud/client.py +255 -0
  29. apisec_code_bolt/core/__init__.py +75 -0
  30. apisec_code_bolt/core/config.py +528 -0
  31. apisec_code_bolt/core/credentials.py +65 -0
  32. apisec_code_bolt/core/discovery.py +433 -0
  33. apisec_code_bolt/core/log_format.py +115 -0
  34. apisec_code_bolt/core/manifest.py +1009 -0
  35. apisec_code_bolt/core/repo.py +280 -0
  36. apisec_code_bolt/core/state.py +59 -0
  37. apisec_code_bolt/core/telemetry.py +451 -0
  38. apisec_code_bolt/core/types.py +587 -0
  39. apisec_code_bolt/fingerprinting/__init__.py +1 -0
  40. apisec_code_bolt/frameworks/__init__.py +29 -0
  41. apisec_code_bolt/frameworks/_jwt_common.py +50 -0
  42. apisec_code_bolt/frameworks/auth_helpers.py +437 -0
  43. apisec_code_bolt/frameworks/base.py +608 -0
  44. apisec_code_bolt/frameworks/dotnet/__init__.py +17 -0
  45. apisec_code_bolt/frameworks/dotnet/_path_helpers.py +43 -0
  46. apisec_code_bolt/frameworks/dotnet/aspnet_plugin.py +2546 -0
  47. apisec_code_bolt/frameworks/dotnet/grpc_plugin.py +559 -0
  48. apisec_code_bolt/frameworks/dotnet/jwt_config_extractor.py +545 -0
  49. apisec_code_bolt/frameworks/dotnet/legacy_aspnet_plugin.py +732 -0
  50. apisec_code_bolt/frameworks/dotnet/refit_plugin.py +374 -0
  51. apisec_code_bolt/frameworks/dotnet/wcf_plugin.py +1239 -0
  52. apisec_code_bolt/frameworks/java/__init__.py +6 -0
  53. apisec_code_bolt/frameworks/java/_annotations.py +167 -0
  54. apisec_code_bolt/frameworks/java/_constraints.py +128 -0
  55. apisec_code_bolt/frameworks/java/graphql_plugin.py +287 -0
  56. apisec_code_bolt/frameworks/java/jaxrs_plugin.py +748 -0
  57. apisec_code_bolt/frameworks/java/jwt_config_extractor.py +361 -0
  58. apisec_code_bolt/frameworks/java/micronaut_plugin.py +1059 -0
  59. apisec_code_bolt/frameworks/java/spring_plugin.py +1293 -0
  60. apisec_code_bolt/frameworks/js/__init__.py +8 -0
  61. apisec_code_bolt/frameworks/js/express_plugin.py +391 -0
  62. apisec_code_bolt/frameworks/js/fastify_plugin.py +381 -0
  63. apisec_code_bolt/frameworks/js/graphql_plugin.py +198 -0
  64. apisec_code_bolt/frameworks/js/nestjs_plugin.py +423 -0
  65. apisec_code_bolt/frameworks/python/__init__.py +19 -0
  66. apisec_code_bolt/frameworks/python/celery_plugin.py +393 -0
  67. apisec_code_bolt/frameworks/python/click_plugin.py +427 -0
  68. apisec_code_bolt/frameworks/python/django_plugin.py +867 -0
  69. apisec_code_bolt/frameworks/python/fastapi/__init__.py +28 -0
  70. apisec_code_bolt/frameworks/python/fastapi/plugin.py +1390 -0
  71. apisec_code_bolt/frameworks/python/flask_plugin.py +205 -0
  72. apisec_code_bolt/frameworks/python/graphql_plugin.py +274 -0
  73. apisec_code_bolt/frameworks/python/prefect_plugin.py +251 -0
  74. apisec_code_bolt/frameworks/python/webhook_plugin.py +255 -0
  75. apisec_code_bolt/parsing/__init__.py +62 -0
  76. apisec_code_bolt/parsing/base.py +554 -0
  77. apisec_code_bolt/parsing/csharp/__init__.py +5 -0
  78. apisec_code_bolt/parsing/csharp/language_services.py +203 -0
  79. apisec_code_bolt/parsing/csharp/literals.py +72 -0
  80. apisec_code_bolt/parsing/csharp/parser.py +1158 -0
  81. apisec_code_bolt/parsing/csharp/type_resolver.py +568 -0
  82. apisec_code_bolt/parsing/js/__init__.py +5 -0
  83. apisec_code_bolt/parsing/js/language_services.py +118 -0
  84. apisec_code_bolt/parsing/js/parser.py +622 -0
  85. apisec_code_bolt/parsing/jvm/__init__.py +7 -0
  86. apisec_code_bolt/parsing/jvm/language_services.py +270 -0
  87. apisec_code_bolt/parsing/jvm/parser.py +774 -0
  88. apisec_code_bolt/parsing/jvm/type_resolver.py +422 -0
  89. apisec_code_bolt/parsing/python/__init__.py +150 -0
  90. apisec_code_bolt/parsing/python/cbv_extractor.py +606 -0
  91. apisec_code_bolt/parsing/python/constant_resolver.py +500 -0
  92. apisec_code_bolt/parsing/python/cross_file_resolver.py +1054 -0
  93. apisec_code_bolt/parsing/python/dynamic_route_detector.py +532 -0
  94. apisec_code_bolt/parsing/python/expression_utils.py +221 -0
  95. apisec_code_bolt/parsing/python/extraction_types.py +271 -0
  96. apisec_code_bolt/parsing/python/language_services.py +487 -0
  97. apisec_code_bolt/parsing/python/parameter_analyzer.py +789 -0
  98. apisec_code_bolt/parsing/python/parser.py +719 -0
  99. apisec_code_bolt/parsing/python/path_resolver.py +576 -0
  100. apisec_code_bolt/parsing/python/router_registry.py +806 -0
  101. apisec_code_bolt/parsing/python/type_resolver.py +730 -0
  102. apisec_code_bolt/parsing/python/visitors.py +1544 -0
  103. apisec_code_bolt/parsing/services.py +544 -0
  104. apisec_code_bolt/query/__init__.py +1 -0
  105. apisec_code_bolt/query/ast_cache.py +182 -0
  106. apisec_code_bolt/query/executor.py +283 -0
  107. apisec_code_bolt/query/handlers.py +832 -0
  108. apisec_code_bolt-0.1.0.dist-info/METADATA +230 -0
  109. apisec_code_bolt-0.1.0.dist-info/RECORD +111 -0
  110. apisec_code_bolt-0.1.0.dist-info/WHEEL +4 -0
  111. apisec_code_bolt-0.1.0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,500 @@
1
+ """
2
+ Constant and configuration value resolver for Python projects.
3
+
4
+ This module handles resolving constants, config attributes, and settings
5
+ even when they CAN be overwritten by environment variables. The philosophy:
6
+
7
+ "Resolve what we can statically. Note that it may vary at runtime."
8
+
9
+ Handles:
10
+ 1. Module-level constants: API_PREFIX = "/api"
11
+ 2. Class attributes: class Config: PREFIX = "/api"
12
+ 3. Pydantic Settings with defaults: class Settings(BaseSettings): prefix: str = "/api"
13
+ 4. Attribute chains: config.api.prefix
14
+ 5. Environment variable defaults: os.getenv("PREFIX", "/api") -> "/api"
15
+
16
+ CRITICAL: Enterprise apps often use configuration objects. Even if values CAN
17
+ be overridden, the default value is usually what's deployed in most environments.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import re
23
+ from dataclasses import dataclass, field
24
+ from pathlib import Path
25
+ from typing import TYPE_CHECKING
26
+
27
+ if TYPE_CHECKING:
28
+ from ..base import ParsedClass, ParsedFile
29
+
30
+
31
+ # =============================================================================
32
+ # Data Types
33
+ # =============================================================================
34
+
35
+
36
+ @dataclass
37
+ class ResolvedConstant:
38
+ """A resolved constant value."""
39
+
40
+ name: str # Full name (e.g., "config.api.PREFIX")
41
+ value: str # Resolved value
42
+
43
+ # Source information
44
+ source_file: Path | None = None
45
+ source_line: int = 0
46
+ source_type: str = "" # "module_var", "class_attr", "settings", "env_default"
47
+
48
+ # Confidence and notes
49
+ is_default: bool = True # True if this is a default that could be overridden
50
+ confidence: float = 1.0
51
+ override_sources: list[str] = field(default_factory=list) # e.g., ["ENV:API_PREFIX"]
52
+
53
+ # For documentation
54
+ description: str = ""
55
+
56
+
57
+ @dataclass
58
+ class ConfigClass:
59
+ """A configuration class definition."""
60
+
61
+ name: str
62
+ qualified_name: str
63
+ file_path: Path
64
+
65
+ # Is it a Pydantic Settings class?
66
+ is_pydantic_settings: bool = False
67
+
68
+ # Attributes with their default values
69
+ attributes: dict[str, ResolvedConstant] = field(default_factory=dict)
70
+
71
+ # For nested config: config.api.prefix
72
+ nested_configs: dict[str, str] = field(default_factory=dict) # attr_name -> class_name
73
+
74
+
75
+ # =============================================================================
76
+ # Constant Resolver
77
+ # =============================================================================
78
+
79
+
80
+ class ConstantResolver:
81
+ """
82
+ Resolves constants and configuration values across a project.
83
+
84
+ Strategy:
85
+ 1. Scan all files for module-level constants (UPPER_CASE assignments)
86
+ 2. Identify configuration classes (Settings, Config, etc.)
87
+ 3. Extract default values from class attributes
88
+ 4. Build an attribute chain resolver for nested access
89
+ 5. Track which values can be overridden and how
90
+
91
+ Usage:
92
+ resolver = ConstantResolver()
93
+
94
+ for parsed in parsed_files:
95
+ resolver.process_file(parsed)
96
+
97
+ resolver.build_resolution_graph()
98
+
99
+ value = resolver.resolve("settings.API_PREFIX", file_path)
100
+ """
101
+
102
+ def __init__(self, project_root: Path | None = None):
103
+ """Initialize the resolver."""
104
+ self._project_root = project_root
105
+
106
+ # Module-level constants per file
107
+ self._module_constants: dict[Path, dict[str, ResolvedConstant]] = {}
108
+
109
+ # Configuration classes
110
+ self._config_classes: dict[str, ConfigClass] = {}
111
+
112
+ # Variable to class instance mapping per file
113
+ # e.g., {"settings": "app.config.Settings"}
114
+ self._instance_mapping: dict[Path, dict[str, str]] = {}
115
+
116
+ # Global constant lookup (name -> value)
117
+ self._global_lookup: dict[str, str] = {}
118
+
119
+ # Import mappings per file
120
+ self._imports: dict[Path, dict[str, str]] = {}
121
+
122
+ def process_file(self, parsed: ParsedFile) -> None:
123
+ """Process a parsed file to extract constants."""
124
+ if not parsed.success:
125
+ return
126
+
127
+ file_path = parsed.path
128
+ self._module_constants[file_path] = {}
129
+ self._instance_mapping[file_path] = {}
130
+ self._imports[file_path] = {}
131
+
132
+ # 1. Build import mappings
133
+ self._extract_imports(parsed)
134
+
135
+ # 2. Extract module-level constants
136
+ self._extract_module_constants(parsed)
137
+
138
+ # 3. Extract configuration classes
139
+ self._extract_config_classes(parsed)
140
+
141
+ # 4. Track config instance assignments
142
+ self._extract_config_instances(parsed)
143
+
144
+ def _extract_imports(self, parsed: ParsedFile) -> None:
145
+ """Extract import mappings."""
146
+ file_path = parsed.path
147
+
148
+ for imp in parsed.imports:
149
+ if imp.is_from_import:
150
+ for name in imp.names:
151
+ local_name = imp.alias if len(imp.names) == 1 and imp.alias else name
152
+ self._imports[file_path][local_name] = f"{imp.module}.{name}"
153
+ else:
154
+ local_name = imp.alias or imp.module.split(".")[-1]
155
+ self._imports[file_path][local_name] = imp.module
156
+
157
+ def _extract_module_constants(self, parsed: ParsedFile) -> None:
158
+ """Extract module-level constant assignments."""
159
+ file_path = parsed.path
160
+
161
+ for assign in parsed.assignments:
162
+ # Skip function-local assignments
163
+ if assign.in_function:
164
+ continue
165
+
166
+ name = assign.target
167
+ source = assign.value_source or ""
168
+
169
+ # Check for constant naming (UPPER_CASE or Title_Case with underscore)
170
+ if not (name.isupper() or (name[0].isupper() and "_" in name)):
171
+ # Also accept common config names
172
+ if name.lower() not in {
173
+ "prefix",
174
+ "api_prefix",
175
+ "base_path",
176
+ "root_path",
177
+ "version",
178
+ }:
179
+ continue
180
+
181
+ # Try to extract value
182
+ value, source_type, override_sources = self._extract_constant_value(source)
183
+
184
+ if value is not None:
185
+ const = ResolvedConstant(
186
+ name=name,
187
+ value=value,
188
+ source_file=file_path,
189
+ source_line=assign.location.line,
190
+ source_type=source_type,
191
+ is_default=len(override_sources) > 0,
192
+ override_sources=override_sources,
193
+ )
194
+
195
+ self._module_constants[file_path][name] = const
196
+ self._global_lookup[name] = value
197
+
198
+ def _extract_constant_value(self, source: str) -> tuple[str | None, str, list[str]]:
199
+ """
200
+ Extract constant value from source code.
201
+
202
+ Returns:
203
+ Tuple of (value, source_type, override_sources)
204
+ """
205
+ source = source.strip()
206
+ override_sources: list[str] = []
207
+
208
+ # 1. Simple string literal
209
+ string_match = re.match(r'^["\']([^"\']*)["\']$', source)
210
+ if string_match:
211
+ return string_match.group(1), "literal", []
212
+
213
+ # 2. os.getenv() or os.environ.get() with default
214
+ env_match = re.search(
215
+ r'(?:os\.)?(?:getenv|environ\.get)\s*\(\s*["\']([^"\']+)["\']\s*(?:,\s*["\']([^"\']*)["\'])?\s*\)',
216
+ source,
217
+ )
218
+ if env_match:
219
+ env_name = env_match.group(1)
220
+ default = env_match.group(2)
221
+ override_sources.append(f"ENV:{env_name}")
222
+ return default, "env_default", override_sources
223
+
224
+ # 3. f-string without variables (just a literal)
225
+ fstring_match = re.match(r'^f["\']([^{}"\']*)["\']\s*$', source)
226
+ if fstring_match:
227
+ return fstring_match.group(1), "literal", []
228
+
229
+ # 4. Integer literal
230
+ if source.isdigit():
231
+ return source, "literal", []
232
+
233
+ # 5. Name reference to another constant
234
+ if re.match(r"^[A-Z][A-Z0-9_]*$", source):
235
+ # It's a reference to another constant
236
+ if source in self._global_lookup:
237
+ return self._global_lookup[source], "reference", []
238
+
239
+ return None, "unknown", []
240
+
241
+ def _extract_config_classes(self, parsed: ParsedFile) -> None:
242
+ """Extract configuration class definitions."""
243
+ file_path = parsed.path
244
+
245
+ for cls in parsed.classes:
246
+ # Check if it's a config class
247
+ is_config = self._is_config_class(cls)
248
+ if not is_config:
249
+ continue
250
+
251
+ # Check for Pydantic Settings
252
+ is_settings = any(
253
+ base in {"BaseSettings", "pydantic_settings.BaseSettings", "pydantic.BaseSettings"}
254
+ for base in cls.base_classes
255
+ )
256
+
257
+ config = ConfigClass(
258
+ name=cls.name,
259
+ qualified_name=cls.qualified_name.full,
260
+ file_path=file_path,
261
+ is_pydantic_settings=is_settings,
262
+ )
263
+
264
+ # Extract attributes from fields
265
+ for f in cls.fields:
266
+ value, source_type, overrides = self._extract_field_default(f, is_settings)
267
+
268
+ if value is not None:
269
+ config.attributes[f.name] = ResolvedConstant(
270
+ name=f"{cls.name}.{f.name}",
271
+ value=value,
272
+ source_file=file_path,
273
+ source_line=f.location.line if f.location else 0,
274
+ source_type=source_type,
275
+ is_default=is_settings or len(overrides) > 0,
276
+ override_sources=overrides,
277
+ description=f.description or "",
278
+ )
279
+
280
+ self._config_classes[config.qualified_name] = config
281
+
282
+ # Also register by simple name
283
+ if cls.name not in self._config_classes:
284
+ self._config_classes[cls.name] = config
285
+
286
+ def _is_config_class(self, cls: ParsedClass) -> bool:
287
+ """Check if a class is a configuration class."""
288
+ name_lower = cls.name.lower()
289
+
290
+ # Name-based detection
291
+ config_names = {"config", "settings", "configuration", "options", "params"}
292
+ if any(n in name_lower for n in config_names):
293
+ return True
294
+
295
+ # Base class detection
296
+ config_bases = {
297
+ "BaseSettings",
298
+ "BaseConfig",
299
+ "Configuration",
300
+ "pydantic.BaseSettings",
301
+ "pydantic_settings.BaseSettings",
302
+ }
303
+ return bool(any(base in config_bases for base in cls.base_classes))
304
+
305
+ def _extract_field_default(
306
+ self,
307
+ field,
308
+ is_pydantic: bool,
309
+ ) -> tuple[str | None, str, list[str]]:
310
+ """Extract default value from a class field."""
311
+ default = field.default_value
312
+ override_sources: list[str] = []
313
+
314
+ if not default:
315
+ # For Pydantic Settings, field might be set from env
316
+ if is_pydantic:
317
+ # Env var name is typically UPPER_CASE of field name
318
+ env_name = field.name.upper()
319
+ override_sources.append(f"ENV:{env_name}")
320
+ return None, "no_default", override_sources
321
+
322
+ # Try to extract the value
323
+ value, source_type, overrides = self._extract_constant_value(default)
324
+ override_sources.extend(overrides)
325
+
326
+ # For Pydantic Settings, always add potential env override
327
+ if is_pydantic and not any("ENV:" in o for o in override_sources):
328
+ env_name = field.name.upper()
329
+ override_sources.append(f"ENV:{env_name}")
330
+
331
+ return value, source_type, override_sources
332
+
333
+ def _extract_config_instances(self, parsed: ParsedFile) -> None:
334
+ """Track assignments of config class instances."""
335
+ file_path = parsed.path
336
+
337
+ for assign in parsed.assignments:
338
+ if assign.in_function:
339
+ continue
340
+
341
+ source = assign.source_call or ""
342
+
343
+ # Check if it's instantiating a known config class
344
+ resolved = self._resolve_call_name(source, file_path)
345
+
346
+ if resolved in self._config_classes:
347
+ self._instance_mapping[file_path][assign.target] = resolved
348
+
349
+ def _resolve_call_name(self, name: str, file_path: Path) -> str:
350
+ """Resolve a call name using imports."""
351
+ imports = self._imports.get(file_path, {})
352
+
353
+ parts = name.split(".")
354
+ if parts[0] in imports:
355
+ resolved = imports[parts[0]]
356
+ if len(parts) > 1:
357
+ return f"{resolved}.{'.'.join(parts[1:])}"
358
+ return resolved
359
+
360
+ return name
361
+
362
+ # =========================================================================
363
+ # Resolution
364
+ # =========================================================================
365
+
366
+ def build_resolution_graph(self) -> None:
367
+ """Build the resolution graph after processing all files."""
368
+ # Add all config class attributes to global lookup
369
+ for config in self._config_classes.values():
370
+ for attr_name, const in config.attributes.items():
371
+ # Register with full name
372
+ self._global_lookup[const.name] = const.value
373
+ # Register with class.attr format
374
+ self._global_lookup[f"{config.name}.{attr_name}"] = const.value
375
+
376
+ def resolve(
377
+ self,
378
+ expression: str,
379
+ in_file: Path | None = None,
380
+ ) -> ResolvedConstant | None:
381
+ """
382
+ Resolve a constant expression.
383
+
384
+ Handles:
385
+ - Simple names: API_PREFIX
386
+ - Attribute access: config.PREFIX
387
+ - Nested access: settings.api.prefix
388
+
389
+ Args:
390
+ expression: The expression to resolve
391
+ in_file: File context for local resolution
392
+
393
+ Returns:
394
+ ResolvedConstant if resolvable, None otherwise
395
+ """
396
+ expression = expression.strip()
397
+
398
+ # 1. Direct lookup
399
+ if expression in self._global_lookup:
400
+ return ResolvedConstant(
401
+ name=expression,
402
+ value=self._global_lookup[expression],
403
+ source_type="direct",
404
+ )
405
+
406
+ # 2. Check file-local constants
407
+ if in_file and in_file in self._module_constants:
408
+ if expression in self._module_constants[in_file]:
409
+ return self._module_constants[in_file][expression]
410
+
411
+ # 3. Handle attribute access: config.PREFIX
412
+ if "." in expression:
413
+ parts = expression.split(".")
414
+
415
+ # Try to resolve the base object
416
+ base = parts[0]
417
+ attr_path = ".".join(parts[1:])
418
+
419
+ # Check if base is a config instance in this file
420
+ if in_file and in_file in self._instance_mapping:
421
+ instance_map = self._instance_mapping[in_file]
422
+ if base in instance_map:
423
+ config_name = instance_map[base]
424
+ config = self._config_classes.get(config_name)
425
+ if config:
426
+ # Look up the attribute
427
+ if attr_path in config.attributes:
428
+ return config.attributes[attr_path]
429
+ # Try lowercase version
430
+ if attr_path.lower() in config.attributes:
431
+ return config.attributes[attr_path.lower()]
432
+
433
+ # Try looking up the full expression in global
434
+ if expression in self._global_lookup:
435
+ return ResolvedConstant(
436
+ name=expression,
437
+ value=self._global_lookup[expression],
438
+ source_type="config_attr",
439
+ )
440
+
441
+ # Try class.attr format directly
442
+ for config_name, _config in self._config_classes.items():
443
+ full_name = f"{config_name}.{attr_path}"
444
+ if full_name in self._global_lookup:
445
+ return ResolvedConstant(
446
+ name=full_name,
447
+ value=self._global_lookup[full_name],
448
+ source_type="config_attr",
449
+ )
450
+
451
+ return None
452
+
453
+ def resolve_value(
454
+ self,
455
+ expression: str,
456
+ in_file: Path | None = None,
457
+ ) -> str | None:
458
+ """Convenience method to just get the value."""
459
+ const = self.resolve(expression, in_file)
460
+ return const.value if const else None
461
+
462
+ def get_all_constants(self) -> dict[str, str]:
463
+ """Get all resolved constants."""
464
+ return dict(self._global_lookup)
465
+
466
+ def get_config_classes(self) -> dict[str, ConfigClass]:
467
+ """Get all detected configuration classes."""
468
+ return dict(self._config_classes)
469
+
470
+ def get_constants_for_file(self, file_path: Path) -> dict[str, ResolvedConstant]:
471
+ """Get constants defined in a specific file."""
472
+ return self._module_constants.get(file_path, {})
473
+
474
+
475
+ # =============================================================================
476
+ # Convenience Functions
477
+ # =============================================================================
478
+
479
+
480
+ def build_constant_resolver(
481
+ parsed_files: list[ParsedFile],
482
+ project_root: Path | None = None,
483
+ ) -> ConstantResolver:
484
+ """
485
+ Build a constant resolver from parsed files.
486
+
487
+ Args:
488
+ parsed_files: List of successfully parsed files
489
+ project_root: Optional project root
490
+
491
+ Returns:
492
+ Configured ConstantResolver
493
+ """
494
+ resolver = ConstantResolver(project_root)
495
+
496
+ for parsed in parsed_files:
497
+ resolver.process_file(parsed)
498
+
499
+ resolver.build_resolution_graph()
500
+ return resolver