source-kb 0.2.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cli/__init__.py +50 -0
- cli/__main__.py +5 -0
- cli/commands/__init__.py +1 -0
- cli/commands/anchor_fix.py +47 -0
- cli/commands/diff_doc.py +52 -0
- cli/commands/dispatch.py +77 -0
- cli/commands/extract.py +72 -0
- cli/commands/file_list.py +74 -0
- cli/commands/index.py +84 -0
- cli/commands/lock.py +89 -0
- cli/commands/merge.py +60 -0
- cli/commands/merge_delta.py +19 -0
- cli/commands/metadata.py +24 -0
- cli/commands/pipeline.py +45 -0
- cli/commands/post_merge.py +43 -0
- cli/commands/query.py +52 -0
- cli/commands/render.py +101 -0
- cli/commands/scan_repos.py +46 -0
- cli/commands/setup.py +94 -0
- cli/commands/split.py +196 -0
- cli/commands/stale_files.py +98 -0
- cli/commands/validate.py +191 -0
- core/__init__.py +32 -0
- core/config.py +261 -0
- core/docs/__init__.py +7 -0
- core/docs/section_updater.py +286 -0
- core/docs/shared.py +149 -0
- core/git.py +294 -0
- core/interfaces.py +249 -0
- core/monitor/__init__.py +5 -0
- core/monitor/progress.py +83 -0
- core/monitor/prompt_store.py +49 -0
- core/paths.py +141 -0
- core/preset.py +237 -0
- core/preset_accessors.py +202 -0
- core/preset_classify.py +132 -0
- core/preset_hooks.py +129 -0
- core/preset_profile.py +89 -0
- core/prompt/__init__.py +7 -0
- core/prompt/__main__.py +147 -0
- core/prompt/content.py +320 -0
- core/prompt/context_manager.py +164 -0
- core/prompt/renderer.py +236 -0
- core/prompt/response_parser.py +274 -0
- core/prompt/templates.py +357 -0
- core/prompt/validate_parity.py +162 -0
- core/prompt/variables.py +339 -0
- core/rag/__init__.py +22 -0
- core/rag/__main__.py +136 -0
- core/rag/bm25_index.py +268 -0
- core/rag/chunker.py +273 -0
- core/rag/embedder.py +151 -0
- core/rag/indexer.py +292 -0
- core/rag/loader.py +89 -0
- core/rag/retriever.py +82 -0
- core/skeleton/__init__.py +11 -0
- core/skeleton/__main__.py +934 -0
- core/skeleton/anchor_fix.py +250 -0
- core/skeleton/classify.py +331 -0
- core/skeleton/cmd_anchor_fix.py +43 -0
- core/skeleton/cmd_diff_doc.py +44 -0
- core/skeleton/cmd_lock.py +87 -0
- core/skeleton/cmd_merge_delta.py +41 -0
- core/skeleton/community.py +233 -0
- core/skeleton/dependency_graph.py +306 -0
- core/skeleton/diff_doc.py +248 -0
- core/skeleton/dispatch.py +273 -0
- core/skeleton/dispatch_render.py +319 -0
- core/skeleton/dispatch_source.py +111 -0
- core/skeleton/extract.py +218 -0
- core/skeleton/extract_methods.py +298 -0
- core/skeleton/file_list.py +239 -0
- core/skeleton/impact.py +278 -0
- core/skeleton/jar_download.py +177 -0
- core/skeleton/jar_resolver.py +186 -0
- core/skeleton/loader.py +162 -0
- core/skeleton/merge.py +278 -0
- core/skeleton/merge_delta.py +229 -0
- core/skeleton/metadata.py +96 -0
- core/skeleton/metadata_builders.py +264 -0
- core/skeleton/module_dag.py +330 -0
- core/skeleton/parsers/__init__.py +71 -0
- core/skeleton/parsers/jqassistant.py +300 -0
- core/skeleton/parsers/jqassistant_cypher.py +225 -0
- core/skeleton/parsers/regex.py +171 -0
- core/skeleton/parsers/treesitter.py +324 -0
- core/skeleton/parsers/treesitter_java.py +284 -0
- core/skeleton/parsers/treesitter_multi.py +289 -0
- core/skeleton/pom_parser.py +299 -0
- core/skeleton/post_merge.py +295 -0
- core/skeleton/post_merge_llm.py +82 -0
- core/skeleton/query.py +195 -0
- core/skeleton/shard_context.py +177 -0
- core/skeleton/split.py +180 -0
- core/skeleton/split_cache.py +107 -0
- core/skeleton/split_feedback.py +174 -0
- core/skeleton/split_plan.py +219 -0
- core/skeleton/split_plan_helpers.py +305 -0
- core/skeleton/split_plan_llm.py +274 -0
- core/utils.py +135 -0
- core/validators/__init__.py +65 -0
- core/validators/__main__.py +215 -0
- core/validators/consistency.py +203 -0
- core/validators/coverage.py +171 -0
- core/validators/duplicates.py +76 -0
- core/validators/engine.py +224 -0
- core/validators/links.py +76 -0
- core/validators/sampling.py +169 -0
- core/validators/structure.py +144 -0
- engine/__init__.py +7 -0
- engine/assembler.py +231 -0
- engine/confirm.py +65 -0
- engine/dedup.py +106 -0
- engine/main.py +211 -0
- engine/pipeline/__init__.py +163 -0
- engine/pipeline/recovery.py +250 -0
- engine/pipeline/steps/__init__.py +23 -0
- engine/pipeline/steps/audit.py +220 -0
- engine/pipeline/steps/audit_apply.py +195 -0
- engine/pipeline/steps/audit_helpers.py +155 -0
- engine/pipeline/steps/classify_llm.py +236 -0
- engine/pipeline/steps/classify_prompt.py +223 -0
- engine/pipeline/steps/finalize.py +160 -0
- engine/pipeline/steps/generate.py +169 -0
- engine/pipeline/steps/generate_batch.py +197 -0
- engine/pipeline/steps/generate_recovery.py +170 -0
- engine/pipeline/steps/llm_plan_split.py +253 -0
- engine/pipeline/steps/lock.py +64 -0
- engine/pipeline/steps/preflight.py +237 -0
- engine/pipeline/steps/preflight_adjust.py +147 -0
- engine/pipeline/steps/pregenerate.py +130 -0
- engine/pipeline/steps/quality.py +81 -0
- engine/pipeline/steps/skeleton.py +149 -0
- engine/pipeline/steps/source.py +163 -0
- engine/pipeline/steps/sync.py +117 -0
- engine/pipeline/steps/sync_finalize.py +237 -0
- engine/pipeline/steps/sync_update.py +341 -0
- engine/pipelines.py +91 -0
- engine/runner.py +335 -0
- engine/strategies/__init__.py +86 -0
- engine/strategies/api.py +128 -0
- engine/strategies/delegated.py +50 -0
- engine/strategies/dryrun.py +25 -0
- engine/two_phase.py +143 -0
- mcp_server/__init__.py +73 -0
- mcp_server/__main__.py +5 -0
- mcp_server/tools/__init__.py +1 -0
- mcp_server/tools/config.py +63 -0
- mcp_server/tools/discovery.py +276 -0
- mcp_server/tools/generation.py +184 -0
- mcp_server/tools/planning.py +144 -0
- mcp_server/tools/source.py +175 -0
- mcp_server/tools/validation.py +140 -0
- mcp_server/tools/workflow.py +166 -0
- mcp_server/workflow_loader.py +204 -0
- presets/generic/audit_dimensions.md +132 -0
- presets/generic/doc_types.yaml +152 -0
- presets/generic/preset.yaml +115 -0
- presets/java-spring/audit_dimensions.md +228 -0
- presets/java-spring/audit_dimensions.yaml +203 -0
- presets/java-spring/doc_types.yaml +269 -0
- presets/java-spring/hooks.py +122 -0
- presets/java-spring/preset.yaml +341 -0
- presets/java-spring/templates/README.md +34 -0
- presets/java-spring/templates/audit-system.md +15 -0
- presets/java-spring/templates/subagent-aop.md +105 -0
- presets/java-spring/templates/subagent-api.md +63 -0
- presets/java-spring/templates/subagent-architecture.md +111 -0
- presets/java-spring/templates/subagent-async-events.md +107 -0
- presets/java-spring/templates/subagent-audit-api-contracts.md +40 -0
- presets/java-spring/templates/subagent-audit-architecture.md +38 -0
- presets/java-spring/templates/subagent-audit-business.md +40 -0
- presets/java-spring/templates/subagent-audit-data-models.md +40 -0
- presets/java-spring/templates/subagent-business.md +129 -0
- presets/java-spring/templates/subagent-caching.md +75 -0
- presets/java-spring/templates/subagent-database-access.md +114 -0
- presets/java-spring/templates/subagent-enum.md +75 -0
- presets/java-spring/templates/subagent-error-handling.md +91 -0
- presets/java-spring/templates/subagent-external-integrations.md +80 -0
- presets/java-spring/templates/subagent-index.md +122 -0
- presets/java-spring/templates/subagent-messaging.md +97 -0
- presets/java-spring/templates/subagent-model.md +88 -0
- presets/java-spring/templates/subagent-observability.md +91 -0
- presets/java-spring/templates/subagent-scheduled.md +81 -0
- presets/java-spring/templates/subagent-security.md +102 -0
- presets/java-spring/templates/subagent-structure.md +101 -0
- presets/java-spring/templates/subagent-sync-section.md +34 -0
- presets/java-spring/templates/subagent-utils.md +73 -0
- presets/java-spring/templates/sync-system.md +8 -0
- presets/java-spring/workflow-extensions.md +112 -0
- skills/__init__.py +1 -0
- skills/_shared/README.md +30 -0
- skills/_shared/doc-coverage-shared.md +134 -0
- skills/_shared/doc-quality-standard.md +1058 -0
- skills/_shared/doc-subagent-rules.md +762 -0
- skills/_shared/windows-compat.md +89 -0
- skills/kb-audit/SKILL.md +52 -0
- skills/kb-audit/rules.md +88 -0
- skills/kb-audit/steps/step-01-prepare.md +75 -0
- skills/kb-audit/steps/step-02-audit.md +96 -0
- skills/kb-audit/steps/step-03-verify.md +65 -0
- skills/kb-audit/steps/step-04-report.md +64 -0
- skills/kb-init/SKILL.md +146 -0
- skills/kb-init/rules.md +187 -0
- skills/kb-init/steps/step-01-scope.md +62 -0
- skills/kb-init/steps/step-02-source.md +410 -0
- skills/kb-init/steps/step-03-generate.md +307 -0
- skills/kb-init/steps/step-04-quality.md +92 -0
- skills/kb-init/steps/step-05-finalize.md +132 -0
- skills/kb-init/templates/core/execution-modes.md +29 -0
- skills/kb-init/templates/core/output-only.md +4 -0
- skills/kb-init/templates/core/readwrite.md +33 -0
- skills/kb-search/SKILL.md +138 -0
- skills/kb-search/rules.md +64 -0
- skills/kb-sync/SKILL.md +43 -0
- skills/kb-sync/rules.md +70 -0
- skills/kb-sync/scripts/rebuild_module.py +91 -0
- skills/kb-sync/scripts/scan_repos.py +687 -0
- skills/kb-sync/steps/step-01-detect.md +72 -0
- skills/kb-sync/steps/step-02-update.md +71 -0
- skills/kb-sync/steps/step-03-verify.md +47 -0
- skills/kb-sync/steps/step-04-finalize.md +52 -0
- source_kb-0.2.2.dist-info/METADATA +194 -0
- source_kb-0.2.2.dist-info/RECORD +228 -0
- source_kb-0.2.2.dist-info/WHEEL +5 -0
- source_kb-0.2.2.dist-info/entry_points.txt +3 -0
- source_kb-0.2.2.dist-info/licenses/LICENSE +21 -0
- source_kb-0.2.2.dist-info/top_level.txt +6 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""Regex-based skeleton parser — zero-dependency fallback.
|
|
2
|
+
|
|
3
|
+
Uses language-specific regex patterns (loaded from preset config) to extract
|
|
4
|
+
class/method declarations. Less accurate than tree-sitter but works everywhere.
|
|
5
|
+
|
|
6
|
+
Pattern configuration comes from preset.yaml → parser_config.regex_rules.
|
|
7
|
+
If no rules configured, uses built-in defaults for common languages.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
import re
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from core.interfaces import SkeletonParser
|
|
18
|
+
from core.skeleton.parsers import register_parser
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
# Built-in regex patterns per language (used when preset doesn't define custom rules)
|
|
23
|
+
DEFAULT_PATTERNS: dict[str, dict[str, str]] = {
|
|
24
|
+
"java": {
|
|
25
|
+
"class": r"^\s*(?:(?:public|protected|private|abstract|final|static)\s+)*(?:class|interface|enum|record)\s+(\w+)",
|
|
26
|
+
"method": r"^\s*(?:(?:public|protected|private|static|final|abstract|synchronized|native)\s+)*[\w<>\[\],\s]+\s+(\w+)\s*\(",
|
|
27
|
+
"field": r"^\s*(?:(?:public|protected|private|static|final|volatile|transient)\s+)*[\w<>\[\],\s]+\s+(\w+)\s*[;=]",
|
|
28
|
+
},
|
|
29
|
+
"python": {
|
|
30
|
+
"class": r"^\s*class\s+(\w+)",
|
|
31
|
+
"method": r"^\s*(?:async\s+)?def\s+(\w+)\s*\(",
|
|
32
|
+
},
|
|
33
|
+
"go": {
|
|
34
|
+
"class": r"^type\s+(\w+)\s+struct\b",
|
|
35
|
+
"method": r"^func\s+(?:\(\w+\s+\*?\w+\)\s+)?(\w+)\s*\(",
|
|
36
|
+
},
|
|
37
|
+
"typescript": {
|
|
38
|
+
"class": r"^\s*(?:export\s+)?(?:abstract\s+)?(?:class|interface|enum)\s+(\w+)",
|
|
39
|
+
"method": r"^\s*(?:(?:public|private|protected|static|async|abstract)\s+)*(\w+)\s*\(",
|
|
40
|
+
},
|
|
41
|
+
"kotlin": {
|
|
42
|
+
"class": r"^\s*(?:(?:open|abstract|data|sealed|internal|private)\s+)*(?:class|interface|object|enum\s+class)\s+(\w+)",
|
|
43
|
+
"method": r"^\s*(?:(?:override|open|private|internal|suspend)\s+)*fun\s+(\w+)\s*\(",
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# Extension → language key
|
|
48
|
+
EXT_TO_LANG: dict[str, str] = {
|
|
49
|
+
".java": "java", ".py": "python", ".go": "go",
|
|
50
|
+
".ts": "typescript", ".tsx": "typescript", ".js": "typescript",
|
|
51
|
+
".kt": "kotlin", ".kts": "kotlin",
|
|
52
|
+
".cs": "java", # C# patterns similar enough to Java for basic extraction
|
|
53
|
+
".scala": "java",
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
BRANCH_PATTERN = re.compile(
|
|
57
|
+
r"^\s*(?:if|else|elif|switch|case|match|for|while|catch|except|when)\b"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@register_parser
|
|
62
|
+
class RegexSkeletonParser(SkeletonParser):
|
|
63
|
+
"""Regex-based fallback parser. Registered as 'regex'. Lowest priority."""
|
|
64
|
+
|
|
65
|
+
name = "regex"
|
|
66
|
+
priority = 50
|
|
67
|
+
|
|
68
|
+
def can_parse(self, repo_path: Path, preset: dict[str, Any]) -> bool:
|
|
69
|
+
"""Always available — zero dependencies."""
|
|
70
|
+
return True
|
|
71
|
+
|
|
72
|
+
def parse(self, repo_path: Path, preset: dict[str, Any], **kwargs: Any) -> list[dict[str, Any]]:
|
|
73
|
+
"""Parse source files using regex patterns from preset config."""
|
|
74
|
+
from core.git import ls_tree
|
|
75
|
+
|
|
76
|
+
ref = kwargs.get("ref", "HEAD")
|
|
77
|
+
subpath = kwargs.get("subpath")
|
|
78
|
+
parser_config = preset.get("parser_config", {})
|
|
79
|
+
extensions = parser_config.get("extensions") or list(EXT_TO_LANG.keys())
|
|
80
|
+
skip_dirs = set(parser_config.get("skip_dirs", ["test", "tests", "generated"]))
|
|
81
|
+
custom_rules = parser_config.get("regex_rules", {})
|
|
82
|
+
|
|
83
|
+
all_files = ls_tree(repo_path, ref)
|
|
84
|
+
|
|
85
|
+
subpath_prefix = ""
|
|
86
|
+
if subpath:
|
|
87
|
+
subpath_prefix = subpath.rstrip("/") + "/"
|
|
88
|
+
all_files = [f for f in all_files if f.startswith(subpath_prefix)]
|
|
89
|
+
|
|
90
|
+
entries: list[dict[str, Any]] = []
|
|
91
|
+
|
|
92
|
+
for filepath in all_files:
|
|
93
|
+
rel_path = filepath[len(subpath_prefix):] if subpath_prefix else filepath
|
|
94
|
+
if any(f"/{d}/" in rel_path or rel_path.startswith(f"{d}/") for d in skip_dirs):
|
|
95
|
+
continue
|
|
96
|
+
if not any(rel_path.endswith(ext) for ext in extensions):
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
content = _read_file(repo_path, ref, filepath)
|
|
100
|
+
if content is None:
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
entry = _parse_file(content, rel_path, custom_rules)
|
|
104
|
+
if entry:
|
|
105
|
+
entries.append(entry)
|
|
106
|
+
|
|
107
|
+
return entries
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _read_file(repo_path: Path, ref: str, filepath: str) -> str | None:
|
|
111
|
+
"""Read file content from git or filesystem."""
|
|
112
|
+
from core.git import read_file
|
|
113
|
+
try:
|
|
114
|
+
return read_file(repo_path, ref, filepath)
|
|
115
|
+
except Exception:
|
|
116
|
+
full = repo_path / filepath
|
|
117
|
+
if full.exists():
|
|
118
|
+
try:
|
|
119
|
+
return full.read_text(encoding="utf-8")
|
|
120
|
+
except (OSError, UnicodeDecodeError):
|
|
121
|
+
pass
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _parse_file(content: str, filepath: str, custom_rules: dict) -> dict[str, Any] | None:
|
|
126
|
+
"""Parse a single file using regex patterns."""
|
|
127
|
+
ext = Path(filepath).suffix.lower()
|
|
128
|
+
lang = EXT_TO_LANG.get(ext)
|
|
129
|
+
if not lang:
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
# Use custom rules if provided, otherwise built-in defaults
|
|
133
|
+
patterns = custom_rules if custom_rules else DEFAULT_PATTERNS.get(lang, {})
|
|
134
|
+
if not patterns:
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
lines = content.splitlines()
|
|
138
|
+
total_lines = len(lines)
|
|
139
|
+
|
|
140
|
+
class_re = re.compile(patterns["class"]) if "class" in patterns else None
|
|
141
|
+
method_re = re.compile(patterns["method"]) if "method" in patterns else None
|
|
142
|
+
|
|
143
|
+
classes: list[dict] = []
|
|
144
|
+
method_starts: list[tuple[str, int]] = []
|
|
145
|
+
|
|
146
|
+
for i, line in enumerate(lines):
|
|
147
|
+
if class_re:
|
|
148
|
+
m = class_re.match(line)
|
|
149
|
+
if m:
|
|
150
|
+
classes.append({"name": m.group(1)})
|
|
151
|
+
if method_re:
|
|
152
|
+
m = method_re.match(line)
|
|
153
|
+
if m:
|
|
154
|
+
method_starts.append((m.group(1), i))
|
|
155
|
+
|
|
156
|
+
# Compute method metrics
|
|
157
|
+
methods: list[dict] = []
|
|
158
|
+
for idx, (name, start) in enumerate(method_starts):
|
|
159
|
+
end = method_starts[idx + 1][1] if idx + 1 < len(method_starts) else total_lines
|
|
160
|
+
line_count = end - start
|
|
161
|
+
branches = sum(1 for l in lines[start:end] if BRANCH_PATTERN.match(l))
|
|
162
|
+
complexity = "high" if line_count > 50 or branches > 10 else "medium" if line_count > 25 or branches > 5 else "low"
|
|
163
|
+
methods.append({
|
|
164
|
+
"name": name, "line_count": line_count,
|
|
165
|
+
"complexity": complexity, "branches": branches,
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
if not methods and not classes:
|
|
169
|
+
return None
|
|
170
|
+
|
|
171
|
+
return {"file": filepath, "total_lines": total_lines, "methods": methods, "classes": classes}
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
"""Tree-sitter based skeleton parser — multi-language AST extraction.
|
|
2
|
+
|
|
3
|
+
Wraps tree-sitter-languages to extract classes, methods, fields, imports,
|
|
4
|
+
and call targets from source files. Language-specific rules come from preset config.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from core.interfaces import SkeletonParser
|
|
13
|
+
from core.skeleton.parsers import register_parser
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
# Extension → tree-sitter language name
|
|
18
|
+
EXT_TO_LANG: dict[str, str] = {
|
|
19
|
+
".java": "java", ".py": "python", ".go": "go",
|
|
20
|
+
".ts": "typescript", ".tsx": "tsx", ".js": "javascript", ".jsx": "javascript",
|
|
21
|
+
".kt": "kotlin", ".kts": "kotlin", ".scala": "scala", ".rs": "rust",
|
|
22
|
+
".rb": "ruby", ".cs": "c_sharp", ".cpp": "cpp", ".cc": "cpp", ".cxx": "cpp",
|
|
23
|
+
".c": "c", ".h": "c", ".hpp": "cpp", ".swift": "swift", ".dart": "dart",
|
|
24
|
+
".lua": "lua", ".php": "php", ".groovy": "groovy", ".r": "r", ".R": "r",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
# tree-sitter node types for classes/structs per language
|
|
28
|
+
CLASS_NODES: dict[str, list[str]] = {
|
|
29
|
+
"java": ["class_declaration", "interface_declaration", "enum_declaration", "record_declaration"],
|
|
30
|
+
"python": ["class_definition"], "go": ["type_declaration"],
|
|
31
|
+
"typescript": ["class_declaration", "interface_declaration", "enum_declaration"],
|
|
32
|
+
"tsx": ["class_declaration", "interface_declaration", "enum_declaration"],
|
|
33
|
+
"javascript": ["class_declaration"],
|
|
34
|
+
"kotlin": ["class_declaration", "object_declaration", "interface_declaration"],
|
|
35
|
+
"scala": ["class_definition", "object_definition", "trait_definition"],
|
|
36
|
+
"rust": ["struct_item", "enum_item", "impl_item", "trait_item"],
|
|
37
|
+
"ruby": ["class", "module"],
|
|
38
|
+
"c_sharp": ["class_declaration", "interface_declaration", "enum_declaration",
|
|
39
|
+
"struct_declaration", "record_declaration"],
|
|
40
|
+
"cpp": ["class_specifier", "struct_specifier"], "c": ["struct_specifier"],
|
|
41
|
+
"swift": ["class_declaration", "struct_declaration", "protocol_declaration", "enum_declaration"],
|
|
42
|
+
"php": ["class_declaration", "interface_declaration", "enum_declaration"],
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
# tree-sitter node types for functions/methods per language
|
|
46
|
+
METHOD_NODES: dict[str, list[str]] = {
|
|
47
|
+
"java": ["method_declaration", "constructor_declaration"],
|
|
48
|
+
"python": ["function_definition"],
|
|
49
|
+
"go": ["function_declaration", "method_declaration"],
|
|
50
|
+
"typescript": ["function_declaration", "method_definition", "arrow_function"],
|
|
51
|
+
"tsx": ["function_declaration", "method_definition", "arrow_function"],
|
|
52
|
+
"javascript": ["function_declaration", "method_definition", "arrow_function"],
|
|
53
|
+
"kotlin": ["function_declaration"], "scala": ["function_definition", "val_definition"],
|
|
54
|
+
"rust": ["function_item"], "ruby": ["method", "singleton_method"],
|
|
55
|
+
"c_sharp": ["method_declaration", "constructor_declaration"],
|
|
56
|
+
"cpp": ["function_definition"], "c": ["function_definition"],
|
|
57
|
+
"swift": ["function_declaration"], "php": ["method_declaration", "function_definition"],
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
# Branch node types for complexity analysis
|
|
61
|
+
BRANCH_TYPES = frozenset({
|
|
62
|
+
"if_statement", "if_expression", "else_clause", "elif_clause",
|
|
63
|
+
"switch_statement", "switch_expression", "case_clause", "match_expression",
|
|
64
|
+
"match_arm", "for_statement", "for_expression", "while_statement",
|
|
65
|
+
"try_statement", "catch_clause", "except_clause",
|
|
66
|
+
"ternary_expression", "conditional_expression", "guard_statement",
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def is_available() -> bool:
|
|
72
|
+
"""Check if tree-sitter-languages is installed."""
|
|
73
|
+
try:
|
|
74
|
+
import tree_sitter_languages # noqa: F401
|
|
75
|
+
return True
|
|
76
|
+
except ImportError:
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def detect_language(filepath: str) -> str | None:
|
|
81
|
+
"""Detect language from file extension."""
|
|
82
|
+
return EXT_TO_LANG.get(Path(filepath).suffix.lower())
|
|
83
|
+
|
|
84
|
+
@register_parser
|
|
85
|
+
class TreeSitterSkeletonParser(SkeletonParser):
|
|
86
|
+
"""Tree-sitter skeleton parser. Registered as 'treesitter'."""
|
|
87
|
+
|
|
88
|
+
name = "treesitter"
|
|
89
|
+
priority = 80
|
|
90
|
+
|
|
91
|
+
def can_parse(self, repo_path: Path, preset: dict[str, Any]) -> bool:
|
|
92
|
+
return is_available()
|
|
93
|
+
|
|
94
|
+
def parse(self, repo_path: Path, preset: dict[str, Any], **kwargs: Any) -> list[dict[str, Any]]:
|
|
95
|
+
"""Parse all source files in repo, returning skeleton entries."""
|
|
96
|
+
from core.git import ls_tree
|
|
97
|
+
ref = kwargs.get("ref", "HEAD")
|
|
98
|
+
subpath = kwargs.get("subpath")
|
|
99
|
+
parser_config = preset.get("parser_config", {})
|
|
100
|
+
extensions = parser_config.get("extensions") or list(EXT_TO_LANG.keys())
|
|
101
|
+
skip_dirs = set(parser_config.get("skip_dirs", ["test", "tests", "generated"]))
|
|
102
|
+
all_files = ls_tree(repo_path, ref)
|
|
103
|
+
|
|
104
|
+
subpath_prefix = ""
|
|
105
|
+
if subpath:
|
|
106
|
+
subpath_prefix = subpath.rstrip("/") + "/"
|
|
107
|
+
all_files = [f for f in all_files if f.startswith(subpath_prefix)]
|
|
108
|
+
|
|
109
|
+
entries: list[dict[str, Any]] = []
|
|
110
|
+
for filepath in all_files:
|
|
111
|
+
rel_path = filepath[len(subpath_prefix):] if subpath_prefix else filepath
|
|
112
|
+
if any(f"/{d}/" in rel_path or rel_path.startswith(f"{d}/") for d in skip_dirs):
|
|
113
|
+
continue
|
|
114
|
+
if not any(rel_path.endswith(ext) for ext in extensions):
|
|
115
|
+
continue
|
|
116
|
+
content = _read_file_content(repo_path, ref, filepath)
|
|
117
|
+
if content is None:
|
|
118
|
+
continue
|
|
119
|
+
entry = parse_file(content, rel_path)
|
|
120
|
+
if entry:
|
|
121
|
+
entries.append(entry)
|
|
122
|
+
return entries
|
|
123
|
+
|
|
124
|
+
def list_source_files(self, repo_path: Path, preset: dict[str, Any], ref: str = "HEAD", **kwargs: Any) -> list[str]:
|
|
125
|
+
"""List source files (excluding test and generated files)."""
|
|
126
|
+
from core.git import ls_tree
|
|
127
|
+
subpath = kwargs.get("subpath")
|
|
128
|
+
parser_config = preset.get("parser_config", {})
|
|
129
|
+
extensions = parser_config.get("extensions") or list(EXT_TO_LANG.keys())
|
|
130
|
+
test_patterns = parser_config.get("test_patterns", [])
|
|
131
|
+
generated_patterns = parser_config.get("generated_patterns", [])
|
|
132
|
+
all_files = ls_tree(repo_path, ref)
|
|
133
|
+
|
|
134
|
+
subpath_prefix = ""
|
|
135
|
+
if subpath:
|
|
136
|
+
subpath_prefix = subpath.rstrip("/") + "/"
|
|
137
|
+
all_files = [f for f in all_files if f.startswith(subpath_prefix)]
|
|
138
|
+
|
|
139
|
+
result = []
|
|
140
|
+
for f in all_files:
|
|
141
|
+
rel_path = f[len(subpath_prefix):] if subpath_prefix else f
|
|
142
|
+
if any(p in rel_path for p in test_patterns):
|
|
143
|
+
continue
|
|
144
|
+
if any(p in rel_path for p in generated_patterns):
|
|
145
|
+
continue
|
|
146
|
+
if extensions and not any(rel_path.endswith(ext) for ext in extensions):
|
|
147
|
+
continue
|
|
148
|
+
result.append(rel_path)
|
|
149
|
+
return result
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
# ---------------------------------------------------------------------------
|
|
153
|
+
# Internal helpers
|
|
154
|
+
# ---------------------------------------------------------------------------
|
|
155
|
+
_parser_cache: dict[str, Any] = {}
|
|
156
|
+
_parser_lock = __import__("threading").Lock()
|
|
157
|
+
|
|
158
|
+
def _get_ts_parser(lang: str):
|
|
159
|
+
"""Get or create a tree-sitter parser for a language."""
|
|
160
|
+
with _parser_lock:
|
|
161
|
+
if lang in _parser_cache:
|
|
162
|
+
return _parser_cache[lang]
|
|
163
|
+
try:
|
|
164
|
+
import tree_sitter_languages
|
|
165
|
+
parser = tree_sitter_languages.get_parser(lang)
|
|
166
|
+
with _parser_lock:
|
|
167
|
+
_parser_cache[lang] = parser
|
|
168
|
+
return parser
|
|
169
|
+
except (ImportError, Exception) as e:
|
|
170
|
+
logger.warning("No tree-sitter grammar for %s: %s", lang, e)
|
|
171
|
+
return None
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _read_file_content(repo_path: Path, ref: str, filepath: str) -> str | None:
|
|
175
|
+
"""Read file content from git or filesystem."""
|
|
176
|
+
from core.git import read_file
|
|
177
|
+
try:
|
|
178
|
+
return read_file(repo_path, ref, filepath)
|
|
179
|
+
except Exception:
|
|
180
|
+
full = repo_path / filepath
|
|
181
|
+
if full.exists():
|
|
182
|
+
try:
|
|
183
|
+
return full.read_text(encoding="utf-8")
|
|
184
|
+
except (OSError, UnicodeDecodeError):
|
|
185
|
+
pass
|
|
186
|
+
return None
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def parse_file(content: str, filepath: str) -> dict[str, Any] | None:
|
|
190
|
+
"""Parse a single file into a skeleton entry. Public for direct use."""
|
|
191
|
+
from core.skeleton.parsers.treesitter_java import extract_call_targets
|
|
192
|
+
from core.skeleton.parsers.treesitter_multi import (
|
|
193
|
+
fallback_parse, extract_imports, extract_classes, extract_methods, extract_fields,
|
|
194
|
+
)
|
|
195
|
+
lang = detect_language(filepath)
|
|
196
|
+
if not lang:
|
|
197
|
+
return fallback_parse(content, filepath)
|
|
198
|
+
parser = _get_ts_parser(lang)
|
|
199
|
+
if not parser:
|
|
200
|
+
return fallback_parse(content, filepath)
|
|
201
|
+
try:
|
|
202
|
+
tree = parser.parse(content.encode("utf-8"))
|
|
203
|
+
except Exception as e:
|
|
204
|
+
logger.debug("Parse failed for %s: %s", filepath, e)
|
|
205
|
+
return fallback_parse(content, filepath)
|
|
206
|
+
|
|
207
|
+
root = tree.root_node
|
|
208
|
+
classes = extract_classes(root, lang)
|
|
209
|
+
methods = extract_methods(root, lang, content)
|
|
210
|
+
if not methods and not classes:
|
|
211
|
+
return None
|
|
212
|
+
entry: dict[str, Any] = {
|
|
213
|
+
"file": filepath, "total_lines": len(content.splitlines()),
|
|
214
|
+
"methods": methods, "classes": classes,
|
|
215
|
+
}
|
|
216
|
+
fields = extract_fields(root, lang)
|
|
217
|
+
if fields:
|
|
218
|
+
entry["fields"] = fields
|
|
219
|
+
imports = extract_imports(root, lang)
|
|
220
|
+
if imports:
|
|
221
|
+
entry["imports"] = imports
|
|
222
|
+
calls = extract_call_targets(root, lang, content)
|
|
223
|
+
if calls:
|
|
224
|
+
entry["calls"] = calls
|
|
225
|
+
return entry
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
# ---------------------------------------------------------------------------
|
|
229
|
+
# Common AST traversal utilities (used by treesitter_java and treesitter_multi)
|
|
230
|
+
# ---------------------------------------------------------------------------
|
|
231
|
+
|
|
232
|
+
def _walk_nodes(root, node_types: list[str]):
|
|
233
|
+
"""Recursively yield nodes matching given types."""
|
|
234
|
+
stack = [root]
|
|
235
|
+
while stack:
|
|
236
|
+
node = stack.pop()
|
|
237
|
+
if node.type in node_types:
|
|
238
|
+
yield node
|
|
239
|
+
stack.extend(reversed(node.children))
|
|
240
|
+
|
|
241
|
+
def _get_name(node, lang: str) -> str | None:
|
|
242
|
+
"""Extract name identifier from a declaration node."""
|
|
243
|
+
name_node = node.child_by_field_name("name")
|
|
244
|
+
if name_node:
|
|
245
|
+
return name_node.text.decode("utf-8")
|
|
246
|
+
for child in node.children:
|
|
247
|
+
if child.type in ("identifier", "type_identifier", "property_identifier"):
|
|
248
|
+
return child.text.decode("utf-8")
|
|
249
|
+
return None
|
|
250
|
+
|
|
251
|
+
def _classify_type(node, lang: str) -> str:
|
|
252
|
+
"""Classify a class-like node into class/interface/enum/struct."""
|
|
253
|
+
_type_map = {
|
|
254
|
+
"interface_declaration": "interface", "enum_declaration": "enum",
|
|
255
|
+
"record_declaration": "record", "struct_item": "struct",
|
|
256
|
+
"struct_specifier": "struct", "struct_declaration": "struct",
|
|
257
|
+
"trait_item": "interface", "trait_definition": "interface",
|
|
258
|
+
"object_declaration": "object", "object_definition": "object",
|
|
259
|
+
"protocol_declaration": "interface", "module": "module",
|
|
260
|
+
}
|
|
261
|
+
return _type_map.get(node.type, "class")
|
|
262
|
+
|
|
263
|
+
def _get_access(node, lang: str) -> str:
|
|
264
|
+
"""Extract access modifier."""
|
|
265
|
+
if lang in ("java", "c_sharp", "kotlin"):
|
|
266
|
+
for child in node.children:
|
|
267
|
+
if child.type == "modifiers":
|
|
268
|
+
text = child.text.decode("utf-8")
|
|
269
|
+
for mod in ("public", "protected", "private"):
|
|
270
|
+
if mod in text:
|
|
271
|
+
return mod
|
|
272
|
+
elif lang == "go":
|
|
273
|
+
name = _get_name(node, lang)
|
|
274
|
+
return "public" if name and name[0].isupper() else "private"
|
|
275
|
+
elif lang == "rust":
|
|
276
|
+
if any(c.type == "visibility_modifier" for c in node.children):
|
|
277
|
+
return "public"
|
|
278
|
+
return "private"
|
|
279
|
+
elif lang == "python":
|
|
280
|
+
name = _get_name(node, lang)
|
|
281
|
+
if name and name.startswith("__") and not name.endswith("__"):
|
|
282
|
+
return "private"
|
|
283
|
+
if name and name.startswith("_"):
|
|
284
|
+
return "protected"
|
|
285
|
+
return "public"
|
|
286
|
+
|
|
287
|
+
def _get_params(node, lang: str) -> str | None:
|
|
288
|
+
"""Extract parameter string."""
|
|
289
|
+
params_node = (
|
|
290
|
+
node.child_by_field_name("parameters")
|
|
291
|
+
or node.child_by_field_name("formal_parameters")
|
|
292
|
+
)
|
|
293
|
+
if params_node:
|
|
294
|
+
text = params_node.text.decode("utf-8").strip("()")
|
|
295
|
+
return text[:200]
|
|
296
|
+
return None
|
|
297
|
+
|
|
298
|
+
def _get_return_type(node, lang: str) -> str | None:
|
|
299
|
+
"""Extract return type."""
|
|
300
|
+
ret_node = node.child_by_field_name("return_type") or node.child_by_field_name("type")
|
|
301
|
+
if ret_node:
|
|
302
|
+
return ret_node.text.decode("utf-8")[:100]
|
|
303
|
+
return None
|
|
304
|
+
|
|
305
|
+
def _get_annotations(node, lang: str) -> list[str | dict]:
|
|
306
|
+
"""Extract annotations/decorators."""
|
|
307
|
+
from core.skeleton.parsers.treesitter_java import parse_java_annotation
|
|
308
|
+
|
|
309
|
+
annotations: list[str | dict] = []
|
|
310
|
+
if lang in ("java", "kotlin", "c_sharp"):
|
|
311
|
+
for child in node.children:
|
|
312
|
+
if child.type in ("modifiers", "annotation"):
|
|
313
|
+
for sub in child.children:
|
|
314
|
+
if sub.type in ("marker_annotation", "annotation"):
|
|
315
|
+
annotations.append(parse_java_annotation(sub, lang))
|
|
316
|
+
prev = node.prev_sibling
|
|
317
|
+
while prev and prev.type in ("marker_annotation", "annotation"):
|
|
318
|
+
annotations.append(parse_java_annotation(prev, lang))
|
|
319
|
+
prev = prev.prev_sibling
|
|
320
|
+
elif lang == "python":
|
|
321
|
+
for child in node.children:
|
|
322
|
+
if child.type == "decorator":
|
|
323
|
+
annotations.append(child.text.decode("utf-8"))
|
|
324
|
+
return annotations[:10]
|