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,284 @@
|
|
|
1
|
+
"""Java-specific tree-sitter extraction logic.
|
|
2
|
+
|
|
3
|
+
Handles Javadoc extraction, annotation parsing, call target analysis,
|
|
4
|
+
and other Java/Kotlin/C# specific AST operations.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# ---------------------------------------------------------------------------
|
|
14
|
+
# Javadoc / docstring extraction
|
|
15
|
+
# ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def extract_javadoc(node, lang: str) -> str | None:
|
|
19
|
+
"""Extract the first meaningful line from JavaDoc/docstring above a node.
|
|
20
|
+
|
|
21
|
+
For Java/Kotlin/C#: extracts from /** ... */ block comments
|
|
22
|
+
For Python: extracts from triple-quoted docstrings
|
|
23
|
+
|
|
24
|
+
Returns the first non-annotation line (business description), or None.
|
|
25
|
+
"""
|
|
26
|
+
if lang in ("java", "kotlin", "c_sharp"):
|
|
27
|
+
prev = node.prev_sibling
|
|
28
|
+
# Skip past annotations to find the comment
|
|
29
|
+
while prev and prev.type in ("marker_annotation", "annotation", "modifiers"):
|
|
30
|
+
prev = prev.prev_sibling
|
|
31
|
+
|
|
32
|
+
if prev and prev.type in ("comment", "block_comment", "line_comment"):
|
|
33
|
+
text = prev.text.decode("utf-8")
|
|
34
|
+
if "/**" in text:
|
|
35
|
+
lines = text.split("\n")
|
|
36
|
+
for line in lines:
|
|
37
|
+
cleaned = line.strip().lstrip("/*").strip()
|
|
38
|
+
if cleaned and not cleaned.startswith("@") and len(cleaned) > 3:
|
|
39
|
+
return cleaned[:200]
|
|
40
|
+
elif text.strip().startswith("//"):
|
|
41
|
+
cleaned = text.strip().lstrip("/").strip()
|
|
42
|
+
if cleaned and len(cleaned) > 3:
|
|
43
|
+
return cleaned[:200]
|
|
44
|
+
|
|
45
|
+
elif lang == "python":
|
|
46
|
+
body = node.child_by_field_name("body")
|
|
47
|
+
if body and body.children:
|
|
48
|
+
first = body.children[0]
|
|
49
|
+
if first.type == "expression_statement":
|
|
50
|
+
for child in first.children:
|
|
51
|
+
if child.type == "string":
|
|
52
|
+
text = child.text.decode("utf-8")
|
|
53
|
+
text = text.strip("'\"").strip()
|
|
54
|
+
first_line = text.split("\n")[0].strip()
|
|
55
|
+
if first_line:
|
|
56
|
+
return first_line[:200]
|
|
57
|
+
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# ---------------------------------------------------------------------------
|
|
62
|
+
# Annotation extraction
|
|
63
|
+
# ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def get_annotation_values(node, lang: str) -> list[dict]:
|
|
67
|
+
"""Extract annotations WITH their attribute values (structured format).
|
|
68
|
+
|
|
69
|
+
Returns list of dicts: [{"name": "@KafkaListener", "attributes": {"topics": "order-events"}}]
|
|
70
|
+
|
|
71
|
+
Used for detecting implicit dependencies (Kafka topics, Feign clients, Redis keys).
|
|
72
|
+
"""
|
|
73
|
+
if lang not in ("java", "kotlin", "c_sharp"):
|
|
74
|
+
return []
|
|
75
|
+
|
|
76
|
+
annotations = []
|
|
77
|
+
anno_nodes = []
|
|
78
|
+
|
|
79
|
+
for child in node.children:
|
|
80
|
+
if child.type in ("modifiers",):
|
|
81
|
+
for sub in child.children:
|
|
82
|
+
if sub.type in ("marker_annotation", "annotation"):
|
|
83
|
+
anno_nodes.append(sub)
|
|
84
|
+
elif child.type in ("marker_annotation", "annotation"):
|
|
85
|
+
anno_nodes.append(child)
|
|
86
|
+
|
|
87
|
+
prev = node.prev_sibling
|
|
88
|
+
while prev and prev.type in ("marker_annotation", "annotation"):
|
|
89
|
+
anno_nodes.append(prev)
|
|
90
|
+
prev = prev.prev_sibling
|
|
91
|
+
|
|
92
|
+
for anno_node in anno_nodes:
|
|
93
|
+
text = anno_node.text.decode("utf-8")
|
|
94
|
+
name_match = re.match(r'@(\w+)', text)
|
|
95
|
+
if not name_match:
|
|
96
|
+
continue
|
|
97
|
+
|
|
98
|
+
anno_name = "@" + name_match.group(1)
|
|
99
|
+
attributes: dict[str, str] = {}
|
|
100
|
+
|
|
101
|
+
args_match = re.search(r'\((.+)\)', text, re.DOTALL)
|
|
102
|
+
if args_match:
|
|
103
|
+
args_text = args_match.group(1).strip()
|
|
104
|
+
# Key-value pairs: key = "value"
|
|
105
|
+
for kv in re.finditer(r'(\w+)\s*=\s*"([^"]*)"', args_text):
|
|
106
|
+
attributes[kv.group(1)] = kv.group(2)
|
|
107
|
+
# Array patterns: key = {values}
|
|
108
|
+
for kv in re.finditer(r'(\w+)\s*=\s*\{([^}]*)\}', args_text):
|
|
109
|
+
attributes[kv.group(1)] = kv.group(2).strip().strip('"')
|
|
110
|
+
# Single value: @Foo("value")
|
|
111
|
+
if not attributes:
|
|
112
|
+
single_match = re.match(r'^"([^"]*)"', args_text)
|
|
113
|
+
if single_match:
|
|
114
|
+
attributes["value"] = single_match.group(1)
|
|
115
|
+
|
|
116
|
+
annotations.append({"name": anno_name, "attributes": attributes})
|
|
117
|
+
|
|
118
|
+
return annotations[:10]
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def parse_java_annotation(node, lang: str) -> str | dict:
|
|
122
|
+
"""Parse a Java/Kotlin annotation node into structured format.
|
|
123
|
+
|
|
124
|
+
Simple annotations (no args): returns "@Service"
|
|
125
|
+
Annotations with args: returns {"name": "@Service", "attrs": {"value": "..."}}
|
|
126
|
+
"""
|
|
127
|
+
if node.type == "marker_annotation":
|
|
128
|
+
return node.text.decode("utf-8")
|
|
129
|
+
|
|
130
|
+
name = None
|
|
131
|
+
attrs: dict[str, str] = {}
|
|
132
|
+
|
|
133
|
+
for child in node.children:
|
|
134
|
+
if child.type in ("identifier", "scoped_identifier"):
|
|
135
|
+
name = "@" + child.text.decode("utf-8")
|
|
136
|
+
elif child.type == "annotation_argument_list":
|
|
137
|
+
attrs = parse_annotation_args(child)
|
|
138
|
+
|
|
139
|
+
if not name:
|
|
140
|
+
return node.text.decode("utf-8")
|
|
141
|
+
|
|
142
|
+
if attrs:
|
|
143
|
+
return {"name": name, "attrs": attrs}
|
|
144
|
+
return name
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def parse_annotation_args(args_node) -> dict[str, str]:
|
|
148
|
+
"""Parse annotation_argument_list into key-value dict.
|
|
149
|
+
|
|
150
|
+
Handles:
|
|
151
|
+
@Annotation("value") → {"value": "\"value\""}
|
|
152
|
+
@Annotation(name = "x", url = "y") → {"name": "\"x\"", "url": "\"y\""}
|
|
153
|
+
"""
|
|
154
|
+
attrs: dict[str, str] = {}
|
|
155
|
+
|
|
156
|
+
for child in args_node.children:
|
|
157
|
+
if child.type == "element_value_pair":
|
|
158
|
+
key_node = child.child_by_field_name("key")
|
|
159
|
+
value_node = child.child_by_field_name("value")
|
|
160
|
+
if key_node and value_node:
|
|
161
|
+
key = key_node.text.decode("utf-8")
|
|
162
|
+
value = value_node.text.decode("utf-8")[:200]
|
|
163
|
+
attrs[key] = value
|
|
164
|
+
elif child.type in ("string_literal", "true", "false", "number_literal",
|
|
165
|
+
"identifier", "field_access", "class_literal"):
|
|
166
|
+
attrs["value"] = child.text.decode("utf-8")[:200]
|
|
167
|
+
elif child.type == "element_value_array_initializer":
|
|
168
|
+
attrs["value"] = child.text.decode("utf-8")[:200]
|
|
169
|
+
|
|
170
|
+
return attrs
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# ---------------------------------------------------------------------------
|
|
174
|
+
# Call target extraction
|
|
175
|
+
# ---------------------------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
_CALL_NOISE = frozenset({
|
|
178
|
+
"this", "super", "System", "Math", "String", "Integer", "Long",
|
|
179
|
+
"Double", "Float", "Boolean", "Byte", "Short", "Character",
|
|
180
|
+
"Arrays", "Collections", "Objects", "Optional", "Stream",
|
|
181
|
+
"List", "Map", "Set", "HashMap", "ArrayList", "HashSet",
|
|
182
|
+
"StringBuilder", "StringBuffer", "Thread", "Class",
|
|
183
|
+
"Logger", "log", "logger", "LOG", "LOGGER",
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def extract_call_targets(root, lang: str, content: str) -> list[str]:
|
|
188
|
+
"""Extract class-level call targets from method bodies.
|
|
189
|
+
|
|
190
|
+
Identifies receiver objects in method calls
|
|
191
|
+
(e.g. 'orderService.createOrder()' → 'orderService').
|
|
192
|
+
Returns deduplicated list of likely dependency variable names.
|
|
193
|
+
"""
|
|
194
|
+
from core.skeleton.parsers.treesitter import _walk_nodes, METHOD_NODES
|
|
195
|
+
|
|
196
|
+
if lang not in ("java", "kotlin", "c_sharp"):
|
|
197
|
+
return []
|
|
198
|
+
|
|
199
|
+
method_node_types = METHOD_NODES.get(lang, [])
|
|
200
|
+
all_receivers: set[str] = set()
|
|
201
|
+
|
|
202
|
+
for node in _walk_nodes(root, method_node_types):
|
|
203
|
+
body = node.text.decode("utf-8") if node.text else ""
|
|
204
|
+
for m in re.finditer(r'\b(\w+)\.(\w+)\s*\(', body):
|
|
205
|
+
receiver = m.group(1)
|
|
206
|
+
if receiver in _CALL_NOISE:
|
|
207
|
+
continue
|
|
208
|
+
if receiver[0].isupper() and receiver not in _CALL_NOISE:
|
|
209
|
+
all_receivers.add(receiver)
|
|
210
|
+
elif receiver[0].islower() and len(receiver) > 2:
|
|
211
|
+
all_receivers.add(receiver)
|
|
212
|
+
|
|
213
|
+
return sorted(all_receivers)[:50]
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def extract_method_calls(node, lang: str) -> list[str]:
|
|
217
|
+
"""Extract call targets from a single method body.
|
|
218
|
+
|
|
219
|
+
Returns list of receiver names (e.g. ['orderService', 'paymentService']).
|
|
220
|
+
"""
|
|
221
|
+
if lang not in ("java", "kotlin", "c_sharp"):
|
|
222
|
+
return []
|
|
223
|
+
|
|
224
|
+
body = node.text.decode("utf-8") if node.text else ""
|
|
225
|
+
if not body:
|
|
226
|
+
return []
|
|
227
|
+
|
|
228
|
+
receivers: set[str] = set()
|
|
229
|
+
for m in re.finditer(r'\b(\w+)\.(\w+)\s*\(', body):
|
|
230
|
+
receiver = m.group(1)
|
|
231
|
+
if receiver in _CALL_NOISE or len(receiver) <= 2:
|
|
232
|
+
continue
|
|
233
|
+
if receiver[0].islower() or (receiver[0].isupper() and receiver not in _CALL_NOISE):
|
|
234
|
+
receivers.add(receiver)
|
|
235
|
+
|
|
236
|
+
return sorted(receivers)[:20]
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
# ---------------------------------------------------------------------------
|
|
240
|
+
# Inheritance extraction
|
|
241
|
+
# ---------------------------------------------------------------------------
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def get_extends(node, lang: str) -> str | None:
|
|
245
|
+
"""Extract superclass."""
|
|
246
|
+
sc = node.child_by_field_name("superclass")
|
|
247
|
+
if sc:
|
|
248
|
+
return sc.text.decode("utf-8")
|
|
249
|
+
for child in node.children:
|
|
250
|
+
if child.type == "superclass":
|
|
251
|
+
return child.text.decode("utf-8").replace("extends ", "")
|
|
252
|
+
return None
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def get_implements(node, lang: str) -> list[str] | None:
|
|
256
|
+
"""Extract implemented interfaces."""
|
|
257
|
+
for child in node.children:
|
|
258
|
+
if child.type in ("super_interfaces", "interfaces"):
|
|
259
|
+
text = child.text.decode("utf-8")
|
|
260
|
+
text = text.replace("implements ", "")
|
|
261
|
+
return [i.strip() for i in text.split(",")]
|
|
262
|
+
return None
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
# ---------------------------------------------------------------------------
|
|
266
|
+
# Complexity and constants
|
|
267
|
+
# ---------------------------------------------------------------------------
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def count_branches(node) -> int:
|
|
271
|
+
"""Count branching nodes for complexity analysis."""
|
|
272
|
+
from core.skeleton.parsers.treesitter import _walk_nodes, BRANCH_TYPES
|
|
273
|
+
count = 0
|
|
274
|
+
for _ in _walk_nodes(node, list(BRANCH_TYPES)):
|
|
275
|
+
count += 1
|
|
276
|
+
return count
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def extract_constants(node, content: str) -> list[str]:
|
|
280
|
+
"""Extract constant references from method body (UPPER_CASE identifiers)."""
|
|
281
|
+
body_text = node.text.decode("utf-8") if node.text else ""
|
|
282
|
+
constants = set(re.findall(r'\b([A-Z][A-Z0-9_]{2,})\b', body_text))
|
|
283
|
+
noise = {"NULL", "TRUE", "FALSE", "NONE", "THIS", "SELF", "GET", "SET", "PUT", "POST", "DELETE"}
|
|
284
|
+
return sorted(constants - noise)
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"""Multi-language tree-sitter specializations.
|
|
2
|
+
|
|
3
|
+
Handles fallback parsing, document-based method extraction, field extraction,
|
|
4
|
+
and extraction orchestration for classes/methods/fields/imports.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# ---------------------------------------------------------------------------
|
|
14
|
+
# Fallback regex-based parsing
|
|
15
|
+
# ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
def fallback_parse(content: str, filepath: str) -> dict[str, Any] | None:
|
|
18
|
+
"""Fallback to simple regex parsing when tree-sitter can't handle the file."""
|
|
19
|
+
lines = content.splitlines()
|
|
20
|
+
total_lines = len(lines)
|
|
21
|
+
methods: list[dict[str, Any]] = []
|
|
22
|
+
classes: list[dict[str, Any]] = []
|
|
23
|
+
|
|
24
|
+
func_re = re.compile(
|
|
25
|
+
r'^\s*(?:(?:public|private|protected|static|async|export|def|func|fn|fun)\s+)*'
|
|
26
|
+
r'(?:function\s+)?(\w+)\s*\('
|
|
27
|
+
)
|
|
28
|
+
class_re = re.compile(
|
|
29
|
+
r'^\s*(?:(?:public|abstract|final|export)\s+)*'
|
|
30
|
+
r'(?:class|struct|interface|enum|trait|object|type)\s+(\w+)'
|
|
31
|
+
)
|
|
32
|
+
branch_re = re.compile(
|
|
33
|
+
r'^\s*(?:if|else|elif|switch|case|match|for|while|catch|except)\b'
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
func_starts: list[tuple[str, int]] = []
|
|
37
|
+
for i, line in enumerate(lines):
|
|
38
|
+
m = func_re.match(line)
|
|
39
|
+
if m:
|
|
40
|
+
func_starts.append((m.group(1), i))
|
|
41
|
+
m = class_re.match(line)
|
|
42
|
+
if m:
|
|
43
|
+
classes.append({"name": m.group(1)})
|
|
44
|
+
|
|
45
|
+
for idx, (name, start) in enumerate(func_starts):
|
|
46
|
+
end = func_starts[idx + 1][1] if idx + 1 < len(func_starts) else total_lines
|
|
47
|
+
line_count = end - start
|
|
48
|
+
branches = sum(1 for ln in lines[start:end] if branch_re.match(ln))
|
|
49
|
+
|
|
50
|
+
if line_count > 50 or branches > 10:
|
|
51
|
+
complexity = "high"
|
|
52
|
+
elif line_count > 25 or branches > 5:
|
|
53
|
+
complexity = "medium"
|
|
54
|
+
else:
|
|
55
|
+
complexity = "low"
|
|
56
|
+
|
|
57
|
+
methods.append({
|
|
58
|
+
"name": name, "access": "public", "line_count": line_count,
|
|
59
|
+
"complexity": complexity, "branches": branches,
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
if not methods and not classes:
|
|
63
|
+
return None
|
|
64
|
+
return {"file": filepath, "total_lines": total_lines, "methods": methods, "classes": classes}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
# Document-based method extraction (for coverage checks)
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
def extract_methods_from_doc(doc_path: str | Path) -> tuple[set[str], set[str]]:
|
|
72
|
+
"""Extract method names and class names from documentation files.
|
|
73
|
+
|
|
74
|
+
Returns (methods, classes) sets.
|
|
75
|
+
"""
|
|
76
|
+
try:
|
|
77
|
+
content = Path(doc_path).read_text(encoding="utf-8")
|
|
78
|
+
except UnicodeDecodeError:
|
|
79
|
+
try:
|
|
80
|
+
content = Path(doc_path).read_text(encoding="gbk")
|
|
81
|
+
except Exception:
|
|
82
|
+
return set(), set()
|
|
83
|
+
|
|
84
|
+
methods: set[str] = set()
|
|
85
|
+
classes: set[str] = set()
|
|
86
|
+
|
|
87
|
+
for m in re.finditer(r'`(\w+(?:\.\w+)?)`', content):
|
|
88
|
+
token = m.group(1)
|
|
89
|
+
if "." in token:
|
|
90
|
+
parts = token.split(".")
|
|
91
|
+
classes.add(parts[0])
|
|
92
|
+
methods.add(parts[-1])
|
|
93
|
+
elif token[0].isupper():
|
|
94
|
+
classes.add(token)
|
|
95
|
+
else:
|
|
96
|
+
methods.add(token)
|
|
97
|
+
|
|
98
|
+
for m in re.finditer(r'^#{2,4}\s+.*?(\w+)\s*$', content, re.MULTILINE):
|
|
99
|
+
token = m.group(1)
|
|
100
|
+
if token[0].isupper():
|
|
101
|
+
classes.add(token)
|
|
102
|
+
else:
|
|
103
|
+
methods.add(token)
|
|
104
|
+
|
|
105
|
+
return methods, classes
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# ---------------------------------------------------------------------------
|
|
109
|
+
# Language-specific field extraction
|
|
110
|
+
# ---------------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
def get_field_name(node, lang: str) -> str | None:
|
|
113
|
+
"""Extract field name from a field declaration."""
|
|
114
|
+
name_node = node.child_by_field_name("declarator") or node.child_by_field_name("name")
|
|
115
|
+
if name_node:
|
|
116
|
+
inner = name_node.child_by_field_name("name")
|
|
117
|
+
if inner:
|
|
118
|
+
return inner.text.decode("utf-8")
|
|
119
|
+
return name_node.text.decode("utf-8").split("=")[0].strip()
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def get_field_type(node, lang: str) -> str | None:
|
|
124
|
+
"""Extract field type from a field declaration."""
|
|
125
|
+
type_node = node.child_by_field_name("type")
|
|
126
|
+
if type_node:
|
|
127
|
+
return type_node.text.decode("utf-8")[:100]
|
|
128
|
+
return None
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# ---------------------------------------------------------------------------
|
|
132
|
+
# Extraction orchestration (classes, methods, fields, imports)
|
|
133
|
+
# ---------------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
def extract_classes(root, lang: str) -> list[dict]:
|
|
136
|
+
"""Extract class/interface/enum declarations with full metadata."""
|
|
137
|
+
from core.skeleton.parsers.treesitter import (
|
|
138
|
+
_walk_nodes, _get_name, _classify_type, _get_annotations, CLASS_NODES,
|
|
139
|
+
)
|
|
140
|
+
from core.skeleton.parsers.treesitter_java import (
|
|
141
|
+
extract_javadoc, get_annotation_values, get_extends, get_implements,
|
|
142
|
+
)
|
|
143
|
+
node_types = CLASS_NODES.get(lang, [])
|
|
144
|
+
classes = []
|
|
145
|
+
for node in _walk_nodes(root, node_types):
|
|
146
|
+
name = _get_name(node, lang)
|
|
147
|
+
if not name:
|
|
148
|
+
continue
|
|
149
|
+
cls: dict[str, Any] = {"name": name, "type": _classify_type(node, lang)}
|
|
150
|
+
annotations = _get_annotations(node, lang)
|
|
151
|
+
if annotations:
|
|
152
|
+
cls["annotations"] = annotations
|
|
153
|
+
anno_values = get_annotation_values(node, lang)
|
|
154
|
+
if anno_values:
|
|
155
|
+
cls["annotation_values"] = anno_values
|
|
156
|
+
extends = get_extends(node, lang)
|
|
157
|
+
if extends:
|
|
158
|
+
cls["extends"] = extends
|
|
159
|
+
implements = get_implements(node, lang)
|
|
160
|
+
if implements:
|
|
161
|
+
cls["implements"] = implements
|
|
162
|
+
javadoc = extract_javadoc(node, lang)
|
|
163
|
+
if javadoc:
|
|
164
|
+
cls["doc"] = javadoc
|
|
165
|
+
classes.append(cls)
|
|
166
|
+
return classes
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def extract_methods(root, lang: str, content: str) -> list[dict]:
|
|
170
|
+
"""Extract method declarations with complexity analysis."""
|
|
171
|
+
from core.skeleton.parsers.treesitter import (
|
|
172
|
+
_walk_nodes, _get_name, _get_access, _get_params,
|
|
173
|
+
_get_return_type, _get_annotations, METHOD_NODES,
|
|
174
|
+
)
|
|
175
|
+
from core.skeleton.parsers.treesitter_java import (
|
|
176
|
+
extract_javadoc, get_annotation_values, extract_constants,
|
|
177
|
+
extract_method_calls, count_branches,
|
|
178
|
+
)
|
|
179
|
+
node_types = METHOD_NODES.get(lang, [])
|
|
180
|
+
methods = []
|
|
181
|
+
for node in _walk_nodes(root, node_types):
|
|
182
|
+
name = _get_name(node, lang)
|
|
183
|
+
if not name:
|
|
184
|
+
continue
|
|
185
|
+
line_count = node.end_point[0] - node.start_point[0] + 1
|
|
186
|
+
branches = count_branches(node)
|
|
187
|
+
if line_count > 50 or branches > 10:
|
|
188
|
+
complexity = "high"
|
|
189
|
+
elif line_count > 25 or branches > 5:
|
|
190
|
+
complexity = "medium"
|
|
191
|
+
else:
|
|
192
|
+
complexity = "low"
|
|
193
|
+
method: dict[str, Any] = {
|
|
194
|
+
"name": name, "access": _get_access(node, lang),
|
|
195
|
+
"line_count": line_count, "complexity": complexity, "branches": branches,
|
|
196
|
+
}
|
|
197
|
+
params = _get_params(node, lang)
|
|
198
|
+
if params is not None:
|
|
199
|
+
method["params"] = params
|
|
200
|
+
return_type = _get_return_type(node, lang)
|
|
201
|
+
if return_type:
|
|
202
|
+
method["return_type"] = return_type
|
|
203
|
+
annotations = _get_annotations(node, lang)
|
|
204
|
+
if annotations:
|
|
205
|
+
method["annotations"] = annotations
|
|
206
|
+
anno_values = get_annotation_values(node, lang)
|
|
207
|
+
if anno_values:
|
|
208
|
+
method["annotation_values"] = anno_values
|
|
209
|
+
javadoc = extract_javadoc(node, lang)
|
|
210
|
+
if javadoc:
|
|
211
|
+
method["doc"] = javadoc
|
|
212
|
+
constants = extract_constants(node, content)
|
|
213
|
+
if constants:
|
|
214
|
+
method["constants"] = constants[:30]
|
|
215
|
+
method_calls = extract_method_calls(node, lang)
|
|
216
|
+
if method_calls:
|
|
217
|
+
method["calls"] = method_calls
|
|
218
|
+
methods.append(method)
|
|
219
|
+
return methods
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def extract_fields(root, lang: str) -> list[dict]:
|
|
223
|
+
"""Extract class fields/properties."""
|
|
224
|
+
from core.skeleton.parsers.treesitter import _walk_nodes, _get_annotations
|
|
225
|
+
field_types = {
|
|
226
|
+
"java": ["field_declaration"], "kotlin": ["property_declaration"],
|
|
227
|
+
"typescript": ["public_field_definition", "property_declaration"],
|
|
228
|
+
"c_sharp": ["field_declaration", "property_declaration"], "python": [],
|
|
229
|
+
}
|
|
230
|
+
node_types = field_types.get(lang, [])
|
|
231
|
+
if not node_types:
|
|
232
|
+
return []
|
|
233
|
+
fields = []
|
|
234
|
+
for node in _walk_nodes(root, node_types):
|
|
235
|
+
name = get_field_name(node, lang)
|
|
236
|
+
if name:
|
|
237
|
+
field: dict[str, Any] = {"name": name}
|
|
238
|
+
ftype = get_field_type(node, lang)
|
|
239
|
+
if ftype:
|
|
240
|
+
field["type"] = ftype
|
|
241
|
+
annotations = _get_annotations(node, lang)
|
|
242
|
+
if annotations:
|
|
243
|
+
field["annotations"] = annotations
|
|
244
|
+
fields.append(field)
|
|
245
|
+
return fields
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def extract_imports(root, lang: str) -> list[str]:
|
|
249
|
+
"""Extract import statements for all supported languages."""
|
|
250
|
+
from core.skeleton.parsers.treesitter import _walk_nodes
|
|
251
|
+
import_node_types = {
|
|
252
|
+
"java": ["import_declaration"], "python": ["import_statement", "import_from_statement"],
|
|
253
|
+
"go": ["import_declaration", "import_spec"], "typescript": ["import_statement"],
|
|
254
|
+
"tsx": ["import_statement"], "javascript": ["import_statement"],
|
|
255
|
+
"kotlin": ["import_header"], "c_sharp": ["using_directive"],
|
|
256
|
+
}
|
|
257
|
+
node_types = import_node_types.get(lang, [])
|
|
258
|
+
if not node_types:
|
|
259
|
+
return []
|
|
260
|
+
imports = []
|
|
261
|
+
for node in _walk_nodes(root, node_types):
|
|
262
|
+
text = node.text.decode("utf-8").strip()
|
|
263
|
+
if lang == "java":
|
|
264
|
+
m = re.search(r'import\s+(?:static\s+)?([\w.]+)', text)
|
|
265
|
+
if m and not m.group(1).startswith(("java.", "javax.", "sun.", "jdk.")):
|
|
266
|
+
imports.append(m.group(1))
|
|
267
|
+
elif lang == "python":
|
|
268
|
+
m = re.search(r'from\s+([\w.]+)\s+import', text) or re.search(r'import\s+([\w.]+)', text)
|
|
269
|
+
if m:
|
|
270
|
+
imports.append(m.group(1))
|
|
271
|
+
elif lang == "go":
|
|
272
|
+
m = re.search(r'"([^"]+)"', text)
|
|
273
|
+
if m:
|
|
274
|
+
imports.append(m.group(1))
|
|
275
|
+
elif lang in ("typescript", "tsx", "javascript"):
|
|
276
|
+
m = re.search(r'''from\s+['"]([^'"]+)['"]''', text)
|
|
277
|
+
if not m:
|
|
278
|
+
m = re.search(r'''import\s+['"]([^'"]+)['"]''', text)
|
|
279
|
+
if m:
|
|
280
|
+
imports.append(m.group(1))
|
|
281
|
+
elif lang == "kotlin":
|
|
282
|
+
m = re.search(r'import\s+([\w.]+)', text)
|
|
283
|
+
if m and not m.group(1).startswith(("java.", "javax.", "kotlin.")):
|
|
284
|
+
imports.append(m.group(1))
|
|
285
|
+
elif lang == "c_sharp":
|
|
286
|
+
m = re.search(r'using\s+(?:static\s+)?([\w.]+)', text)
|
|
287
|
+
if m and not m.group(1).startswith("System."):
|
|
288
|
+
imports.append(m.group(1))
|
|
289
|
+
return imports
|