tree-sitter-analyzer 1.9.17.1__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 (149) hide show
  1. tree_sitter_analyzer/__init__.py +132 -0
  2. tree_sitter_analyzer/__main__.py +11 -0
  3. tree_sitter_analyzer/api.py +853 -0
  4. tree_sitter_analyzer/cli/__init__.py +39 -0
  5. tree_sitter_analyzer/cli/__main__.py +12 -0
  6. tree_sitter_analyzer/cli/argument_validator.py +89 -0
  7. tree_sitter_analyzer/cli/commands/__init__.py +26 -0
  8. tree_sitter_analyzer/cli/commands/advanced_command.py +226 -0
  9. tree_sitter_analyzer/cli/commands/base_command.py +181 -0
  10. tree_sitter_analyzer/cli/commands/default_command.py +18 -0
  11. tree_sitter_analyzer/cli/commands/find_and_grep_cli.py +188 -0
  12. tree_sitter_analyzer/cli/commands/list_files_cli.py +133 -0
  13. tree_sitter_analyzer/cli/commands/partial_read_command.py +139 -0
  14. tree_sitter_analyzer/cli/commands/query_command.py +109 -0
  15. tree_sitter_analyzer/cli/commands/search_content_cli.py +161 -0
  16. tree_sitter_analyzer/cli/commands/structure_command.py +156 -0
  17. tree_sitter_analyzer/cli/commands/summary_command.py +116 -0
  18. tree_sitter_analyzer/cli/commands/table_command.py +414 -0
  19. tree_sitter_analyzer/cli/info_commands.py +124 -0
  20. tree_sitter_analyzer/cli_main.py +472 -0
  21. tree_sitter_analyzer/constants.py +85 -0
  22. tree_sitter_analyzer/core/__init__.py +15 -0
  23. tree_sitter_analyzer/core/analysis_engine.py +580 -0
  24. tree_sitter_analyzer/core/cache_service.py +333 -0
  25. tree_sitter_analyzer/core/engine.py +585 -0
  26. tree_sitter_analyzer/core/parser.py +293 -0
  27. tree_sitter_analyzer/core/query.py +605 -0
  28. tree_sitter_analyzer/core/query_filter.py +200 -0
  29. tree_sitter_analyzer/core/query_service.py +340 -0
  30. tree_sitter_analyzer/encoding_utils.py +530 -0
  31. tree_sitter_analyzer/exceptions.py +747 -0
  32. tree_sitter_analyzer/file_handler.py +246 -0
  33. tree_sitter_analyzer/formatters/__init__.py +1 -0
  34. tree_sitter_analyzer/formatters/base_formatter.py +201 -0
  35. tree_sitter_analyzer/formatters/csharp_formatter.py +367 -0
  36. tree_sitter_analyzer/formatters/formatter_config.py +197 -0
  37. tree_sitter_analyzer/formatters/formatter_factory.py +84 -0
  38. tree_sitter_analyzer/formatters/formatter_registry.py +377 -0
  39. tree_sitter_analyzer/formatters/formatter_selector.py +96 -0
  40. tree_sitter_analyzer/formatters/go_formatter.py +368 -0
  41. tree_sitter_analyzer/formatters/html_formatter.py +498 -0
  42. tree_sitter_analyzer/formatters/java_formatter.py +423 -0
  43. tree_sitter_analyzer/formatters/javascript_formatter.py +611 -0
  44. tree_sitter_analyzer/formatters/kotlin_formatter.py +268 -0
  45. tree_sitter_analyzer/formatters/language_formatter_factory.py +123 -0
  46. tree_sitter_analyzer/formatters/legacy_formatter_adapters.py +228 -0
  47. tree_sitter_analyzer/formatters/markdown_formatter.py +725 -0
  48. tree_sitter_analyzer/formatters/php_formatter.py +301 -0
  49. tree_sitter_analyzer/formatters/python_formatter.py +830 -0
  50. tree_sitter_analyzer/formatters/ruby_formatter.py +278 -0
  51. tree_sitter_analyzer/formatters/rust_formatter.py +233 -0
  52. tree_sitter_analyzer/formatters/sql_formatter_wrapper.py +689 -0
  53. tree_sitter_analyzer/formatters/sql_formatters.py +536 -0
  54. tree_sitter_analyzer/formatters/typescript_formatter.py +543 -0
  55. tree_sitter_analyzer/formatters/yaml_formatter.py +462 -0
  56. tree_sitter_analyzer/interfaces/__init__.py +9 -0
  57. tree_sitter_analyzer/interfaces/cli.py +535 -0
  58. tree_sitter_analyzer/interfaces/cli_adapter.py +359 -0
  59. tree_sitter_analyzer/interfaces/mcp_adapter.py +224 -0
  60. tree_sitter_analyzer/interfaces/mcp_server.py +428 -0
  61. tree_sitter_analyzer/language_detector.py +553 -0
  62. tree_sitter_analyzer/language_loader.py +271 -0
  63. tree_sitter_analyzer/languages/__init__.py +10 -0
  64. tree_sitter_analyzer/languages/csharp_plugin.py +1076 -0
  65. tree_sitter_analyzer/languages/css_plugin.py +449 -0
  66. tree_sitter_analyzer/languages/go_plugin.py +836 -0
  67. tree_sitter_analyzer/languages/html_plugin.py +496 -0
  68. tree_sitter_analyzer/languages/java_plugin.py +1299 -0
  69. tree_sitter_analyzer/languages/javascript_plugin.py +1622 -0
  70. tree_sitter_analyzer/languages/kotlin_plugin.py +656 -0
  71. tree_sitter_analyzer/languages/markdown_plugin.py +1928 -0
  72. tree_sitter_analyzer/languages/php_plugin.py +862 -0
  73. tree_sitter_analyzer/languages/python_plugin.py +1636 -0
  74. tree_sitter_analyzer/languages/ruby_plugin.py +757 -0
  75. tree_sitter_analyzer/languages/rust_plugin.py +673 -0
  76. tree_sitter_analyzer/languages/sql_plugin.py +2444 -0
  77. tree_sitter_analyzer/languages/typescript_plugin.py +1892 -0
  78. tree_sitter_analyzer/languages/yaml_plugin.py +695 -0
  79. tree_sitter_analyzer/legacy_table_formatter.py +860 -0
  80. tree_sitter_analyzer/mcp/__init__.py +34 -0
  81. tree_sitter_analyzer/mcp/resources/__init__.py +43 -0
  82. tree_sitter_analyzer/mcp/resources/code_file_resource.py +208 -0
  83. tree_sitter_analyzer/mcp/resources/project_stats_resource.py +586 -0
  84. tree_sitter_analyzer/mcp/server.py +869 -0
  85. tree_sitter_analyzer/mcp/tools/__init__.py +28 -0
  86. tree_sitter_analyzer/mcp/tools/analyze_scale_tool.py +779 -0
  87. tree_sitter_analyzer/mcp/tools/analyze_scale_tool_cli_compatible.py +291 -0
  88. tree_sitter_analyzer/mcp/tools/base_tool.py +139 -0
  89. tree_sitter_analyzer/mcp/tools/fd_rg_utils.py +816 -0
  90. tree_sitter_analyzer/mcp/tools/find_and_grep_tool.py +686 -0
  91. tree_sitter_analyzer/mcp/tools/list_files_tool.py +413 -0
  92. tree_sitter_analyzer/mcp/tools/output_format_validator.py +148 -0
  93. tree_sitter_analyzer/mcp/tools/query_tool.py +443 -0
  94. tree_sitter_analyzer/mcp/tools/read_partial_tool.py +464 -0
  95. tree_sitter_analyzer/mcp/tools/search_content_tool.py +836 -0
  96. tree_sitter_analyzer/mcp/tools/table_format_tool.py +572 -0
  97. tree_sitter_analyzer/mcp/tools/universal_analyze_tool.py +653 -0
  98. tree_sitter_analyzer/mcp/utils/__init__.py +113 -0
  99. tree_sitter_analyzer/mcp/utils/error_handler.py +569 -0
  100. tree_sitter_analyzer/mcp/utils/file_output_factory.py +217 -0
  101. tree_sitter_analyzer/mcp/utils/file_output_manager.py +322 -0
  102. tree_sitter_analyzer/mcp/utils/gitignore_detector.py +358 -0
  103. tree_sitter_analyzer/mcp/utils/path_resolver.py +414 -0
  104. tree_sitter_analyzer/mcp/utils/search_cache.py +343 -0
  105. tree_sitter_analyzer/models.py +840 -0
  106. tree_sitter_analyzer/mypy_current_errors.txt +2 -0
  107. tree_sitter_analyzer/output_manager.py +255 -0
  108. tree_sitter_analyzer/platform_compat/__init__.py +3 -0
  109. tree_sitter_analyzer/platform_compat/adapter.py +324 -0
  110. tree_sitter_analyzer/platform_compat/compare.py +224 -0
  111. tree_sitter_analyzer/platform_compat/detector.py +67 -0
  112. tree_sitter_analyzer/platform_compat/fixtures.py +228 -0
  113. tree_sitter_analyzer/platform_compat/profiles.py +217 -0
  114. tree_sitter_analyzer/platform_compat/record.py +55 -0
  115. tree_sitter_analyzer/platform_compat/recorder.py +155 -0
  116. tree_sitter_analyzer/platform_compat/report.py +92 -0
  117. tree_sitter_analyzer/plugins/__init__.py +280 -0
  118. tree_sitter_analyzer/plugins/base.py +647 -0
  119. tree_sitter_analyzer/plugins/manager.py +384 -0
  120. tree_sitter_analyzer/project_detector.py +328 -0
  121. tree_sitter_analyzer/queries/__init__.py +27 -0
  122. tree_sitter_analyzer/queries/csharp.py +216 -0
  123. tree_sitter_analyzer/queries/css.py +615 -0
  124. tree_sitter_analyzer/queries/go.py +275 -0
  125. tree_sitter_analyzer/queries/html.py +543 -0
  126. tree_sitter_analyzer/queries/java.py +402 -0
  127. tree_sitter_analyzer/queries/javascript.py +724 -0
  128. tree_sitter_analyzer/queries/kotlin.py +192 -0
  129. tree_sitter_analyzer/queries/markdown.py +258 -0
  130. tree_sitter_analyzer/queries/php.py +95 -0
  131. tree_sitter_analyzer/queries/python.py +859 -0
  132. tree_sitter_analyzer/queries/ruby.py +92 -0
  133. tree_sitter_analyzer/queries/rust.py +223 -0
  134. tree_sitter_analyzer/queries/sql.py +555 -0
  135. tree_sitter_analyzer/queries/typescript.py +871 -0
  136. tree_sitter_analyzer/queries/yaml.py +236 -0
  137. tree_sitter_analyzer/query_loader.py +272 -0
  138. tree_sitter_analyzer/security/__init__.py +22 -0
  139. tree_sitter_analyzer/security/boundary_manager.py +277 -0
  140. tree_sitter_analyzer/security/regex_checker.py +297 -0
  141. tree_sitter_analyzer/security/validator.py +599 -0
  142. tree_sitter_analyzer/table_formatter.py +782 -0
  143. tree_sitter_analyzer/utils/__init__.py +53 -0
  144. tree_sitter_analyzer/utils/logging.py +433 -0
  145. tree_sitter_analyzer/utils/tree_sitter_compat.py +289 -0
  146. tree_sitter_analyzer-1.9.17.1.dist-info/METADATA +485 -0
  147. tree_sitter_analyzer-1.9.17.1.dist-info/RECORD +149 -0
  148. tree_sitter_analyzer-1.9.17.1.dist-info/WHEEL +4 -0
  149. tree_sitter_analyzer-1.9.17.1.dist-info/entry_points.txt +25 -0
@@ -0,0 +1,217 @@
1
+ import json
2
+ import logging
3
+ import threading
4
+ from dataclasses import asdict, dataclass, field
5
+ from pathlib import Path
6
+ from typing import Any, Optional
7
+
8
+ import jsonschema
9
+ from cachetools import TTLCache
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ PROFILE_SCHEMA_VERSION = "1.0.0"
14
+
15
+
16
+ @dataclass
17
+ class ParsingBehavior:
18
+ """Describes how a specific SQL construct parses on a platform."""
19
+
20
+ construct_id: str
21
+ node_type: str
22
+ element_count: int
23
+ attributes: list[str]
24
+ has_error: bool
25
+ known_issues: list[str] = field(default_factory=list)
26
+
27
+
28
+ @dataclass
29
+ class BehaviorProfile:
30
+ """Complete behavior profile for a platform."""
31
+
32
+ schema_version: str
33
+ platform_key: str
34
+ behaviors: dict[str, ParsingBehavior]
35
+ adaptation_rules: list[str]
36
+
37
+ def __post_init__(self):
38
+ """Ensure behaviors are ParsingBehavior objects."""
39
+ if self.behaviors:
40
+ for key, value in self.behaviors.items():
41
+ if isinstance(value, dict):
42
+ self.behaviors[key] = ParsingBehavior(**value)
43
+
44
+ @classmethod
45
+ def load(
46
+ cls, platform_key: str, base_path: Path | None = None
47
+ ) -> Optional["BehaviorProfile"]:
48
+ """
49
+ Loads a profile for the given platform key.
50
+
51
+ Args:
52
+ platform_key: The platform key (e.g. "windows-3.12").
53
+ base_path: The base directory where profiles are stored.
54
+
55
+ Returns:
56
+ BehaviorProfile: The loaded profile, or None if not found.
57
+ """
58
+ if base_path is None:
59
+ # Default to tests/platform_profiles relative to package root?
60
+ # Or maybe we should require base_path.
61
+ # For now, let's assume the caller provides it or we look in a standard location.
62
+ # Let's try to find the package root.
63
+ current_file = Path(__file__)
64
+ # tree_sitter_analyzer/platform_compat/profiles.py -> tree_sitter_analyzer/ -> root
65
+ package_root = current_file.parent.parent.parent
66
+ base_path = package_root / "tests" / "platform_profiles"
67
+
68
+ # We need to reconstruct the path from the key
69
+ # key format: os-version
70
+ try:
71
+ parts = platform_key.split("-")
72
+ os_name = parts[0]
73
+ python_version = parts[1]
74
+ except IndexError:
75
+ logger.error(f"Invalid platform key format: {platform_key}")
76
+ return None
77
+
78
+ profile_path = base_path / os_name / python_version / "profile.json"
79
+
80
+ if not profile_path.exists():
81
+ logger.warning(f"Profile not found for {platform_key} at {profile_path}")
82
+ return None
83
+
84
+ try:
85
+ with open(profile_path, encoding="utf-8") as f:
86
+ data = json.load(f)
87
+
88
+ validate_profile(data)
89
+ data = migrate_profile_schema(data)
90
+
91
+ # Convert behaviors dict to ParsingBehavior objects
92
+ behaviors = {}
93
+ for key, b_data in data.get("behaviors", {}).items():
94
+ behaviors[key] = ParsingBehavior(**b_data)
95
+
96
+ return cls(
97
+ schema_version=data["schema_version"],
98
+ platform_key=data["platform_key"],
99
+ behaviors=behaviors,
100
+ adaptation_rules=data.get("adaptation_rules", []),
101
+ )
102
+ except Exception as e:
103
+ logger.error(f"Error loading profile for {platform_key}: {e}")
104
+ return None
105
+
106
+ def save(self, base_path: Path) -> None:
107
+ """Saves the profile to disk."""
108
+ parts = self.platform_key.split("-")
109
+ os_name = parts[0]
110
+ python_version = parts[1]
111
+
112
+ profile_dir = base_path / os_name / python_version
113
+ profile_dir.mkdir(parents=True, exist_ok=True)
114
+
115
+ profile_path = profile_dir / "profile.json"
116
+
117
+ data = asdict(self)
118
+
119
+ with open(profile_path, "w", encoding="utf-8") as f:
120
+ json.dump(data, f, indent=2)
121
+
122
+
123
+ # Schema definition
124
+ PROFILE_SCHEMA = {
125
+ "type": "object",
126
+ "properties": {
127
+ "schema_version": {"type": "string"},
128
+ "platform_key": {"type": "string"},
129
+ "behaviors": {
130
+ "type": "object",
131
+ "additionalProperties": {
132
+ "type": "object",
133
+ "properties": {
134
+ "construct_id": {"type": "string"},
135
+ "node_type": {"type": "string"},
136
+ "element_count": {"type": "integer"},
137
+ "attributes": {"type": "array", "items": {"type": "string"}},
138
+ "has_error": {"type": "boolean"},
139
+ "known_issues": {"type": "array", "items": {"type": "string"}},
140
+ },
141
+ "required": [
142
+ "construct_id",
143
+ "node_type",
144
+ "element_count",
145
+ "attributes",
146
+ "has_error",
147
+ ],
148
+ },
149
+ },
150
+ "adaptation_rules": {"type": "array", "items": {"type": "string"}},
151
+ },
152
+ "required": ["schema_version", "platform_key", "behaviors", "adaptation_rules"],
153
+ }
154
+
155
+
156
+ def validate_profile(data: dict[str, Any]) -> None:
157
+ """Validates profile data against the schema."""
158
+ jsonschema.validate(instance=data, schema=PROFILE_SCHEMA)
159
+
160
+
161
+ def migrate_profile_schema(data: dict[str, Any]) -> dict[str, Any]:
162
+ """Migrates profile data to the current schema version."""
163
+ version = data.get("schema_version", "0.0.0")
164
+ if version == PROFILE_SCHEMA_VERSION:
165
+ return data
166
+
167
+ if version == "0.0.0":
168
+ return migrate_to_1_0_0(data)
169
+
170
+ return data
171
+
172
+
173
+ def migrate_to_1_0_0(data: dict[str, Any]) -> dict[str, Any]:
174
+ """Initial migration to 1.0.0."""
175
+ data["schema_version"] = "1.0.0"
176
+ if "behaviors" not in data:
177
+ data["behaviors"] = {}
178
+ if "adaptation_rules" not in data:
179
+ data["adaptation_rules"] = []
180
+ return data
181
+
182
+
183
+ class ProfileCache:
184
+ """Thread-safe cache for behavior profiles."""
185
+
186
+ def __init__(self, maxsize: int = 10, ttl: int = 3600):
187
+ self._cache = TTLCache(maxsize=maxsize, ttl=ttl)
188
+ self._lock = threading.RLock()
189
+ self._hits = 0
190
+ self._misses = 0
191
+
192
+ def get(self, key: str) -> BehaviorProfile | None:
193
+ with self._lock:
194
+ if key in self._cache:
195
+ self._hits += 1
196
+ return self._cache[key]
197
+ self._misses += 1
198
+ return None
199
+
200
+ def put(self, key: str, profile: BehaviorProfile) -> None:
201
+ with self._lock:
202
+ self._cache[key] = profile
203
+
204
+ def clear(self) -> None:
205
+ with self._lock:
206
+ self._cache.clear()
207
+ self._hits = 0
208
+ self._misses = 0
209
+
210
+ @property
211
+ def stats(self) -> dict[str, int]:
212
+ with self._lock:
213
+ return {
214
+ "hits": self._hits,
215
+ "misses": self._misses,
216
+ "size": len(self._cache),
217
+ }
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ CLI tool for recording SQL behavior profiles.
4
+ """
5
+
6
+ import argparse
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ from tree_sitter_analyzer.platform_compat.recorder import BehaviorRecorder
11
+ from tree_sitter_analyzer.utils import setup_logger
12
+
13
+ logger = setup_logger(__name__)
14
+
15
+
16
+ def main():
17
+ parser = argparse.ArgumentParser(
18
+ description="Record SQL behavior profile for current platform"
19
+ )
20
+ parser.add_argument(
21
+ "--output-dir",
22
+ type=str,
23
+ help="Directory to save the profile",
24
+ default="tests/platform_profiles",
25
+ )
26
+ args = parser.parse_args()
27
+
28
+ try:
29
+ logger.info("Starting SQL behavior recording...")
30
+ recorder = BehaviorRecorder()
31
+ profile = recorder.record_all()
32
+
33
+ logger.info(f"Recorded profile for {profile.platform_key}")
34
+ logger.info(f"Captured {len(profile.behaviors)} behaviors")
35
+
36
+ output_dir = Path(args.output_dir)
37
+ output_dir.mkdir(parents=True, exist_ok=True)
38
+
39
+ # Save profile
40
+ # The save method expects a base path and constructs the structure {os}/{python}/profile.json
41
+ # But if we want to save to a specific artifact directory in CI, we might want more control.
42
+ # BehaviorProfile.save(base_path) does:
43
+ # path = base_path / self.platform_key.replace("-", "/") / "profile.json"
44
+
45
+ # Let's use the standard save mechanism
46
+ profile.save(output_dir)
47
+ logger.info(f"Profile saved to {output_dir}")
48
+
49
+ except Exception as e:
50
+ logger.error(f"Failed to record profile: {e}")
51
+ sys.exit(1)
52
+
53
+
54
+ if __name__ == "__main__":
55
+ main()
@@ -0,0 +1,155 @@
1
+ import logging
2
+ from pathlib import Path
3
+ from typing import Any
4
+
5
+ import tree_sitter
6
+ import tree_sitter_sql
7
+
8
+ from tree_sitter_analyzer.platform_compat.detector import PlatformDetector
9
+ from tree_sitter_analyzer.platform_compat.fixtures import ALL_FIXTURES, SQLTestFixture
10
+ from tree_sitter_analyzer.platform_compat.profiles import (
11
+ PROFILE_SCHEMA_VERSION,
12
+ BehaviorProfile,
13
+ ParsingBehavior,
14
+ )
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class BehaviorRecorder:
20
+ """Records SQL parsing behavior on the current platform."""
21
+
22
+ def __init__(self):
23
+ self.language = tree_sitter.Language(tree_sitter_sql.language())
24
+ self.parser = tree_sitter.Parser(self.language)
25
+ self.platform_info = PlatformDetector.detect()
26
+
27
+ def record_all(self) -> BehaviorProfile:
28
+ """
29
+ Records behavior for all fixtures.
30
+
31
+ Returns:
32
+ BehaviorProfile: The recorded profile.
33
+ """
34
+ behaviors = {}
35
+
36
+ for fixture in ALL_FIXTURES:
37
+ behavior = self.record_fixture(fixture)
38
+ behaviors[fixture.id] = behavior
39
+
40
+ return BehaviorProfile(
41
+ schema_version=PROFILE_SCHEMA_VERSION,
42
+ platform_key=self.platform_info.platform_key,
43
+ behaviors=behaviors,
44
+ adaptation_rules=[], # Rules are added manually or via analysis, not recording
45
+ )
46
+
47
+ def record_fixture(self, fixture: SQLTestFixture) -> ParsingBehavior:
48
+ """
49
+ Records behavior for a single fixture.
50
+
51
+ Args:
52
+ fixture: The fixture to record.
53
+
54
+ Returns:
55
+ ParsingBehavior: The recorded behavior.
56
+ """
57
+ tree = self.parser.parse(bytes(fixture.sql, "utf8"))
58
+ root_node = tree.root_node
59
+
60
+ # Analyze AST
61
+ analysis = self.analyze_ast(root_node)
62
+
63
+ return ParsingBehavior(
64
+ construct_id=fixture.id,
65
+ node_type=root_node.type,
66
+ element_count=analysis["element_count"],
67
+ attributes=analysis["attributes"],
68
+ has_error=analysis["has_error"],
69
+ known_issues=[], # Populated by comparison or manual review
70
+ )
71
+
72
+ def analyze_ast(self, node: Any) -> dict[str, Any]:
73
+ """
74
+ Analyzes the AST to extract characteristics.
75
+
76
+ Args:
77
+ node: The root node of the AST.
78
+
79
+ Returns:
80
+ Dict containing analysis results.
81
+ """
82
+ element_count = 0
83
+ attributes = set()
84
+ has_error = False
85
+
86
+ # Traverse the tree
87
+ cursor = node.walk()
88
+ visited_children = False
89
+
90
+ while True:
91
+ if not visited_children:
92
+ # Process current node
93
+ if cursor.node.type == "ERROR":
94
+ has_error = True
95
+
96
+ # Count "interesting" elements (top-level statements usually)
97
+ # This is a simplification; we might want to count specific types
98
+ # based on the fixture expectation.
99
+ # For now, let's count nodes that look like definitions.
100
+ if cursor.node.type in {
101
+ "create_table_statement",
102
+ "create_view_statement",
103
+ "create_procedure_statement",
104
+ "create_function_statement",
105
+ "create_trigger_statement",
106
+ "create_index_statement",
107
+ }:
108
+ element_count += 1
109
+
110
+ # Collect attributes (field names)
111
+ if cursor.node.type == "column_definition":
112
+ # Try to find column name
113
+ name_node = cursor.node.child_by_field_name("name")
114
+ if name_node:
115
+ attributes.add(f"col:{name_node.text.decode('utf8')}")
116
+
117
+ # Check for specific attributes we care about
118
+ # e.g. if it's a function, does it have parameters?
119
+
120
+ if cursor.goto_first_child():
121
+ continue
122
+
123
+ if cursor.goto_next_sibling():
124
+ visited_children = False
125
+ continue
126
+
127
+ if cursor.goto_parent():
128
+ visited_children = True
129
+ continue
130
+
131
+ break
132
+
133
+ return {
134
+ "element_count": element_count,
135
+ "attributes": sorted(attributes),
136
+ "has_error": has_error,
137
+ }
138
+
139
+ def save_profile(self, profile: BehaviorProfile, base_path: Path) -> None:
140
+ """
141
+ Saves the recorded profile to disk.
142
+
143
+ Args:
144
+ profile: The profile to save.
145
+ base_path: The base directory.
146
+ """
147
+ profile.save(base_path)
148
+
149
+
150
+ if __name__ == "__main__":
151
+ # Simple CLI for testing
152
+ recorder = BehaviorRecorder()
153
+ profile = recorder.record_all()
154
+ print(f"Recorded profile for {profile.platform_key}")
155
+ print(f"Behaviors: {len(profile.behaviors)}")
@@ -0,0 +1,92 @@
1
+ import json
2
+ from pathlib import Path
3
+
4
+ from .profiles import BehaviorProfile, ParsingBehavior
5
+
6
+
7
+ def generate_compatibility_matrix(profiles_dir: Path) -> str:
8
+ """
9
+ Generates a compatibility matrix report from a directory of profiles.
10
+
11
+ Args:
12
+ profiles_dir: Directory containing profile JSON files (recursively).
13
+
14
+ Returns:
15
+ str: Markdown formatted report.
16
+ """
17
+ profiles: list[BehaviorProfile] = []
18
+
19
+ # Find all profile.json files
20
+ for path in profiles_dir.rglob("profile.json"):
21
+ try:
22
+ with open(path, encoding="utf-8") as f:
23
+ data = json.load(f)
24
+ # Basic validation
25
+ if "platform_key" in data:
26
+ # Manual deserialization of nested objects
27
+ behaviors = {}
28
+ for key, b_data in data.get("behaviors", {}).items():
29
+ if isinstance(b_data, dict):
30
+ behaviors[key] = ParsingBehavior(**b_data)
31
+ else:
32
+ behaviors[key] = b_data
33
+
34
+ profile = BehaviorProfile(
35
+ schema_version=data.get("schema_version", "1.0.0"),
36
+ platform_key=data["platform_key"],
37
+ behaviors=behaviors,
38
+ adaptation_rules=data.get("adaptation_rules", []),
39
+ )
40
+ profiles.append(profile)
41
+ except Exception: # nosec
42
+ continue
43
+
44
+ if not profiles:
45
+ return "No profiles found."
46
+
47
+ # Sort profiles
48
+ profiles.sort(key=lambda p: p.platform_key)
49
+
50
+ # Collect all constructs
51
+ all_constructs = set()
52
+ for p in profiles:
53
+ all_constructs.update(p.behaviors.keys())
54
+ sorted_constructs = sorted(all_constructs)
55
+
56
+ # Build Matrix
57
+ # Rows: Constructs
58
+ # Cols: Platforms
59
+
60
+ lines = ["# SQL Compatibility Matrix", "", "| Construct |"]
61
+
62
+ # Header row
63
+ for p in profiles:
64
+ lines[0] += f" {p.platform_key} |"
65
+ lines.append("|" + "---|" * (len(profiles) + 1))
66
+
67
+ # Data rows
68
+ for construct in sorted_constructs:
69
+ row = f"| {construct} |"
70
+ for p in profiles:
71
+ behavior = p.behaviors.get(construct)
72
+ if not behavior:
73
+ status = "❌ Missing"
74
+ elif behavior.has_error:
75
+ status = "⚠️ Error"
76
+ else:
77
+ status = "✅ OK"
78
+ row += f" {status} |"
79
+ lines.append(row)
80
+
81
+ return "\n".join(lines)
82
+
83
+
84
+ if __name__ == "__main__":
85
+ import argparse
86
+
87
+ parser = argparse.ArgumentParser(description="Generate compatibility matrix")
88
+ parser.add_argument("profiles_dir", type=str, help="Directory containing profiles")
89
+ args = parser.parse_args()
90
+
91
+ report = generate_compatibility_matrix(Path(args.profiles_dir))
92
+ print(report)