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.
- tree_sitter_analyzer/__init__.py +132 -0
- tree_sitter_analyzer/__main__.py +11 -0
- tree_sitter_analyzer/api.py +853 -0
- tree_sitter_analyzer/cli/__init__.py +39 -0
- tree_sitter_analyzer/cli/__main__.py +12 -0
- tree_sitter_analyzer/cli/argument_validator.py +89 -0
- tree_sitter_analyzer/cli/commands/__init__.py +26 -0
- tree_sitter_analyzer/cli/commands/advanced_command.py +226 -0
- tree_sitter_analyzer/cli/commands/base_command.py +181 -0
- tree_sitter_analyzer/cli/commands/default_command.py +18 -0
- tree_sitter_analyzer/cli/commands/find_and_grep_cli.py +188 -0
- tree_sitter_analyzer/cli/commands/list_files_cli.py +133 -0
- tree_sitter_analyzer/cli/commands/partial_read_command.py +139 -0
- tree_sitter_analyzer/cli/commands/query_command.py +109 -0
- tree_sitter_analyzer/cli/commands/search_content_cli.py +161 -0
- tree_sitter_analyzer/cli/commands/structure_command.py +156 -0
- tree_sitter_analyzer/cli/commands/summary_command.py +116 -0
- tree_sitter_analyzer/cli/commands/table_command.py +414 -0
- tree_sitter_analyzer/cli/info_commands.py +124 -0
- tree_sitter_analyzer/cli_main.py +472 -0
- tree_sitter_analyzer/constants.py +85 -0
- tree_sitter_analyzer/core/__init__.py +15 -0
- tree_sitter_analyzer/core/analysis_engine.py +580 -0
- tree_sitter_analyzer/core/cache_service.py +333 -0
- tree_sitter_analyzer/core/engine.py +585 -0
- tree_sitter_analyzer/core/parser.py +293 -0
- tree_sitter_analyzer/core/query.py +605 -0
- tree_sitter_analyzer/core/query_filter.py +200 -0
- tree_sitter_analyzer/core/query_service.py +340 -0
- tree_sitter_analyzer/encoding_utils.py +530 -0
- tree_sitter_analyzer/exceptions.py +747 -0
- tree_sitter_analyzer/file_handler.py +246 -0
- tree_sitter_analyzer/formatters/__init__.py +1 -0
- tree_sitter_analyzer/formatters/base_formatter.py +201 -0
- tree_sitter_analyzer/formatters/csharp_formatter.py +367 -0
- tree_sitter_analyzer/formatters/formatter_config.py +197 -0
- tree_sitter_analyzer/formatters/formatter_factory.py +84 -0
- tree_sitter_analyzer/formatters/formatter_registry.py +377 -0
- tree_sitter_analyzer/formatters/formatter_selector.py +96 -0
- tree_sitter_analyzer/formatters/go_formatter.py +368 -0
- tree_sitter_analyzer/formatters/html_formatter.py +498 -0
- tree_sitter_analyzer/formatters/java_formatter.py +423 -0
- tree_sitter_analyzer/formatters/javascript_formatter.py +611 -0
- tree_sitter_analyzer/formatters/kotlin_formatter.py +268 -0
- tree_sitter_analyzer/formatters/language_formatter_factory.py +123 -0
- tree_sitter_analyzer/formatters/legacy_formatter_adapters.py +228 -0
- tree_sitter_analyzer/formatters/markdown_formatter.py +725 -0
- tree_sitter_analyzer/formatters/php_formatter.py +301 -0
- tree_sitter_analyzer/formatters/python_formatter.py +830 -0
- tree_sitter_analyzer/formatters/ruby_formatter.py +278 -0
- tree_sitter_analyzer/formatters/rust_formatter.py +233 -0
- tree_sitter_analyzer/formatters/sql_formatter_wrapper.py +689 -0
- tree_sitter_analyzer/formatters/sql_formatters.py +536 -0
- tree_sitter_analyzer/formatters/typescript_formatter.py +543 -0
- tree_sitter_analyzer/formatters/yaml_formatter.py +462 -0
- tree_sitter_analyzer/interfaces/__init__.py +9 -0
- tree_sitter_analyzer/interfaces/cli.py +535 -0
- tree_sitter_analyzer/interfaces/cli_adapter.py +359 -0
- tree_sitter_analyzer/interfaces/mcp_adapter.py +224 -0
- tree_sitter_analyzer/interfaces/mcp_server.py +428 -0
- tree_sitter_analyzer/language_detector.py +553 -0
- tree_sitter_analyzer/language_loader.py +271 -0
- tree_sitter_analyzer/languages/__init__.py +10 -0
- tree_sitter_analyzer/languages/csharp_plugin.py +1076 -0
- tree_sitter_analyzer/languages/css_plugin.py +449 -0
- tree_sitter_analyzer/languages/go_plugin.py +836 -0
- tree_sitter_analyzer/languages/html_plugin.py +496 -0
- tree_sitter_analyzer/languages/java_plugin.py +1299 -0
- tree_sitter_analyzer/languages/javascript_plugin.py +1622 -0
- tree_sitter_analyzer/languages/kotlin_plugin.py +656 -0
- tree_sitter_analyzer/languages/markdown_plugin.py +1928 -0
- tree_sitter_analyzer/languages/php_plugin.py +862 -0
- tree_sitter_analyzer/languages/python_plugin.py +1636 -0
- tree_sitter_analyzer/languages/ruby_plugin.py +757 -0
- tree_sitter_analyzer/languages/rust_plugin.py +673 -0
- tree_sitter_analyzer/languages/sql_plugin.py +2444 -0
- tree_sitter_analyzer/languages/typescript_plugin.py +1892 -0
- tree_sitter_analyzer/languages/yaml_plugin.py +695 -0
- tree_sitter_analyzer/legacy_table_formatter.py +860 -0
- tree_sitter_analyzer/mcp/__init__.py +34 -0
- tree_sitter_analyzer/mcp/resources/__init__.py +43 -0
- tree_sitter_analyzer/mcp/resources/code_file_resource.py +208 -0
- tree_sitter_analyzer/mcp/resources/project_stats_resource.py +586 -0
- tree_sitter_analyzer/mcp/server.py +869 -0
- tree_sitter_analyzer/mcp/tools/__init__.py +28 -0
- tree_sitter_analyzer/mcp/tools/analyze_scale_tool.py +779 -0
- tree_sitter_analyzer/mcp/tools/analyze_scale_tool_cli_compatible.py +291 -0
- tree_sitter_analyzer/mcp/tools/base_tool.py +139 -0
- tree_sitter_analyzer/mcp/tools/fd_rg_utils.py +816 -0
- tree_sitter_analyzer/mcp/tools/find_and_grep_tool.py +686 -0
- tree_sitter_analyzer/mcp/tools/list_files_tool.py +413 -0
- tree_sitter_analyzer/mcp/tools/output_format_validator.py +148 -0
- tree_sitter_analyzer/mcp/tools/query_tool.py +443 -0
- tree_sitter_analyzer/mcp/tools/read_partial_tool.py +464 -0
- tree_sitter_analyzer/mcp/tools/search_content_tool.py +836 -0
- tree_sitter_analyzer/mcp/tools/table_format_tool.py +572 -0
- tree_sitter_analyzer/mcp/tools/universal_analyze_tool.py +653 -0
- tree_sitter_analyzer/mcp/utils/__init__.py +113 -0
- tree_sitter_analyzer/mcp/utils/error_handler.py +569 -0
- tree_sitter_analyzer/mcp/utils/file_output_factory.py +217 -0
- tree_sitter_analyzer/mcp/utils/file_output_manager.py +322 -0
- tree_sitter_analyzer/mcp/utils/gitignore_detector.py +358 -0
- tree_sitter_analyzer/mcp/utils/path_resolver.py +414 -0
- tree_sitter_analyzer/mcp/utils/search_cache.py +343 -0
- tree_sitter_analyzer/models.py +840 -0
- tree_sitter_analyzer/mypy_current_errors.txt +2 -0
- tree_sitter_analyzer/output_manager.py +255 -0
- tree_sitter_analyzer/platform_compat/__init__.py +3 -0
- tree_sitter_analyzer/platform_compat/adapter.py +324 -0
- tree_sitter_analyzer/platform_compat/compare.py +224 -0
- tree_sitter_analyzer/platform_compat/detector.py +67 -0
- tree_sitter_analyzer/platform_compat/fixtures.py +228 -0
- tree_sitter_analyzer/platform_compat/profiles.py +217 -0
- tree_sitter_analyzer/platform_compat/record.py +55 -0
- tree_sitter_analyzer/platform_compat/recorder.py +155 -0
- tree_sitter_analyzer/platform_compat/report.py +92 -0
- tree_sitter_analyzer/plugins/__init__.py +280 -0
- tree_sitter_analyzer/plugins/base.py +647 -0
- tree_sitter_analyzer/plugins/manager.py +384 -0
- tree_sitter_analyzer/project_detector.py +328 -0
- tree_sitter_analyzer/queries/__init__.py +27 -0
- tree_sitter_analyzer/queries/csharp.py +216 -0
- tree_sitter_analyzer/queries/css.py +615 -0
- tree_sitter_analyzer/queries/go.py +275 -0
- tree_sitter_analyzer/queries/html.py +543 -0
- tree_sitter_analyzer/queries/java.py +402 -0
- tree_sitter_analyzer/queries/javascript.py +724 -0
- tree_sitter_analyzer/queries/kotlin.py +192 -0
- tree_sitter_analyzer/queries/markdown.py +258 -0
- tree_sitter_analyzer/queries/php.py +95 -0
- tree_sitter_analyzer/queries/python.py +859 -0
- tree_sitter_analyzer/queries/ruby.py +92 -0
- tree_sitter_analyzer/queries/rust.py +223 -0
- tree_sitter_analyzer/queries/sql.py +555 -0
- tree_sitter_analyzer/queries/typescript.py +871 -0
- tree_sitter_analyzer/queries/yaml.py +236 -0
- tree_sitter_analyzer/query_loader.py +272 -0
- tree_sitter_analyzer/security/__init__.py +22 -0
- tree_sitter_analyzer/security/boundary_manager.py +277 -0
- tree_sitter_analyzer/security/regex_checker.py +297 -0
- tree_sitter_analyzer/security/validator.py +599 -0
- tree_sitter_analyzer/table_formatter.py +782 -0
- tree_sitter_analyzer/utils/__init__.py +53 -0
- tree_sitter_analyzer/utils/logging.py +433 -0
- tree_sitter_analyzer/utils/tree_sitter_compat.py +289 -0
- tree_sitter_analyzer-1.9.17.1.dist-info/METADATA +485 -0
- tree_sitter_analyzer-1.9.17.1.dist-info/RECORD +149 -0
- tree_sitter_analyzer-1.9.17.1.dist-info/WHEEL +4 -0
- 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)
|