codedebrief 0.11.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- codedebrief/__init__.py +12 -0
- codedebrief/analysis/__init__.py +16 -0
- codedebrief/analysis/common.py +527 -0
- codedebrief/analysis/discovery.py +100 -0
- codedebrief/analysis/languages/__init__.py +6 -0
- codedebrief/analysis/languages/_common.py +68 -0
- codedebrief/analysis/languages/c.py +96 -0
- codedebrief/analysis/languages/cpp.py +146 -0
- codedebrief/analysis/languages/csharp.py +137 -0
- codedebrief/analysis/languages/go.py +157 -0
- codedebrief/analysis/languages/java.py +158 -0
- codedebrief/analysis/languages/php.py +83 -0
- codedebrief/analysis/languages/ruby.py +75 -0
- codedebrief/analysis/languages/rust.py +96 -0
- codedebrief/analysis/project.py +373 -0
- codedebrief/analysis/python.py +939 -0
- codedebrief/analysis/registry.py +320 -0
- codedebrief/analysis/treesitter.py +884 -0
- codedebrief/analysis/typescript.py +1019 -0
- codedebrief/artifacts.py +49 -0
- codedebrief/cli.py +585 -0
- codedebrief/config.py +226 -0
- codedebrief/doctor.py +175 -0
- codedebrief/install.py +441 -0
- codedebrief/mcp_server.py +2720 -0
- codedebrief/model.py +189 -0
- codedebrief/py.typed +1 -0
- codedebrief/quality.py +392 -0
- codedebrief/query.py +641 -0
- codedebrief/render/__init__.py +6 -0
- codedebrief/render/assets/generated/codedebrief-viewer-runtime.iife.js +10 -0
- codedebrief/render/assets/panels.js +462 -0
- codedebrief/render/assets/shell.js +1649 -0
- codedebrief/render/assets/styles.css +1715 -0
- codedebrief/render/assets/tree.js +616 -0
- codedebrief/render/html.py +191 -0
- codedebrief/render/markdown.py +153 -0
- codedebrief/render/payload.py +326 -0
- codedebrief/render/snapshot.py +769 -0
- codedebrief/schema/codedebrief.schema.json +449 -0
- codedebrief/util.py +65 -0
- codedebrief/validation.py +214 -0
- codedebrief-0.11.0.dist-info/METADATA +426 -0
- codedebrief-0.11.0.dist-info/RECORD +48 -0
- codedebrief-0.11.0.dist-info/WHEEL +4 -0
- codedebrief-0.11.0.dist-info/entry_points.txt +2 -0
- codedebrief-0.11.0.dist-info/licenses/LICENSE +176 -0
- codedebrief-0.11.0.dist-info/licenses/NOTICE +9 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Shared helpers for the tree-sitter language profiles.
|
|
2
|
+
|
|
3
|
+
Centralizes the byte-slice text, named-children, and directory-as-module helpers every
|
|
4
|
+
profile needs, plus the definition walker shared by class/method profiles, to avoid
|
|
5
|
+
copy-paste drift across the language modules.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections.abc import Callable, Iterable
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from codedebrief.analysis.treesitter import LanguageProfile, TSDefinition
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def text(node: Any | None, source: bytes) -> str:
|
|
18
|
+
if node is None:
|
|
19
|
+
return ""
|
|
20
|
+
return source[node.start_byte : node.end_byte].decode("utf-8", "replace")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def named(node: Any | None) -> Iterable[Any]:
|
|
24
|
+
return (child for child in node.children if child.is_named) if node is not None else ()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def module_name(relative: str) -> str:
|
|
28
|
+
"""The directory as a dotted module name, so files in one package share a namespace."""
|
|
29
|
+
return Path(relative).parent.as_posix().replace("/", ".").strip(".")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def container_definitions(
|
|
33
|
+
containers: frozenset[str],
|
|
34
|
+
methods: frozenset[str],
|
|
35
|
+
*,
|
|
36
|
+
name_field: str = "name",
|
|
37
|
+
body_field: str = "body",
|
|
38
|
+
) -> Callable[[Any, bytes, str, LanguageProfile], Iterable[TSDefinition]]:
|
|
39
|
+
"""A `definitions()` for languages whose functions live inside class/module containers.
|
|
40
|
+
|
|
41
|
+
Recurses into each container, tagging found methods with the container name as owner,
|
|
42
|
+
and also yields matching top-level functions.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def definitions(
|
|
46
|
+
root: Any, source: bytes, relative: str, profile: LanguageProfile
|
|
47
|
+
) -> Iterable[TSDefinition]:
|
|
48
|
+
yield from _walk(root, source, "")
|
|
49
|
+
|
|
50
|
+
def _walk(node: Any, source: bytes, owner: str) -> Iterable[TSDefinition]:
|
|
51
|
+
if node.type in containers:
|
|
52
|
+
name = text(node.child_by_field_name(name_field), source) or owner
|
|
53
|
+
body = node.child_by_field_name(body_field) or node.child_by_field_name(
|
|
54
|
+
"declaration_list"
|
|
55
|
+
)
|
|
56
|
+
for child in named(body if body is not None else node):
|
|
57
|
+
yield from _walk(child, source, name)
|
|
58
|
+
return
|
|
59
|
+
if node.type in methods:
|
|
60
|
+
name = text(node.child_by_field_name(name_field), source)
|
|
61
|
+
body = node.child_by_field_name(body_field)
|
|
62
|
+
if name and body is not None:
|
|
63
|
+
yield TSDefinition(name=name, node=node, body=body, owner=owner)
|
|
64
|
+
return
|
|
65
|
+
for child in named(node):
|
|
66
|
+
yield from _walk(child, source, owner)
|
|
67
|
+
|
|
68
|
+
return definitions
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""C language profile for the profile-driven tree-sitter analyzer."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import tree_sitter_c
|
|
10
|
+
|
|
11
|
+
from codedebrief.analysis.languages._common import module_name, text
|
|
12
|
+
from codedebrief.analysis.treesitter import (
|
|
13
|
+
LanguageProfile,
|
|
14
|
+
TreeSitterAnalyzer,
|
|
15
|
+
TSDefinition,
|
|
16
|
+
)
|
|
17
|
+
from codedebrief.config import CodeDebriefConfig
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _definitions(
|
|
21
|
+
root: Any, source: bytes, relative: str, profile: LanguageProfile
|
|
22
|
+
) -> Iterable[TSDefinition]:
|
|
23
|
+
for node in root.children:
|
|
24
|
+
if node.type != "function_definition":
|
|
25
|
+
continue
|
|
26
|
+
name = _function_name(node.child_by_field_name("declarator"), source)
|
|
27
|
+
body = node.child_by_field_name("body")
|
|
28
|
+
if name and body is not None:
|
|
29
|
+
yield TSDefinition(name=name, node=node, body=body, owner="")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _function_name(declarator: Any | None, source: bytes) -> str:
|
|
33
|
+
"""The identifier inside a (possibly pointer-wrapped) function_declarator."""
|
|
34
|
+
node = declarator
|
|
35
|
+
while node is not None:
|
|
36
|
+
if node.type == "identifier":
|
|
37
|
+
return text(node, source)
|
|
38
|
+
inner = node.child_by_field_name("declarator")
|
|
39
|
+
if inner is None:
|
|
40
|
+
break
|
|
41
|
+
node = inner
|
|
42
|
+
if node is not None:
|
|
43
|
+
ident = next((c for c in node.children if c.type == "identifier"), None)
|
|
44
|
+
return text(ident, source)
|
|
45
|
+
return ""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _classify(
|
|
49
|
+
definition: TSDefinition, relative: str, source: str, config: CodeDebriefConfig
|
|
50
|
+
) -> tuple[str, str, bool]:
|
|
51
|
+
override = config.entrypoint_override(f"{relative}:{definition.name}")
|
|
52
|
+
if definition.name == "main":
|
|
53
|
+
return "c", "main", override if override is not None else True
|
|
54
|
+
is_static = any(
|
|
55
|
+
c.type == "storage_class_specifier" and c.text.decode() == "static"
|
|
56
|
+
for c in definition.node.children
|
|
57
|
+
)
|
|
58
|
+
public = config.include_public_functions and not is_static
|
|
59
|
+
return "generic", "function", override if override is not None else public
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _is_test(relative: str, name: str) -> bool:
|
|
63
|
+
# Anchor to path SEGMENTS (a `test`/`tests` directory or a test_*.c / *_test.c file),
|
|
64
|
+
# not a substring of the whole path: `latest/` or `contest.c` must not count, and a
|
|
65
|
+
# real function named `test_harness` outside a test file must not be misclassified.
|
|
66
|
+
lowered = relative.lower()
|
|
67
|
+
segments = lowered.split("/")
|
|
68
|
+
filename = segments[-1]
|
|
69
|
+
return (
|
|
70
|
+
any(segment in {"test", "tests"} for segment in segments[:-1])
|
|
71
|
+
or filename.startswith("test_")
|
|
72
|
+
or filename.endswith(("_test.c", "_test.h"))
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
C_PROFILE = LanguageProfile(
|
|
77
|
+
language="c",
|
|
78
|
+
grammar_loader=tree_sitter_c.language,
|
|
79
|
+
function_types=frozenset({"function_definition"}),
|
|
80
|
+
definitions=_definitions,
|
|
81
|
+
classify=_classify,
|
|
82
|
+
is_test=_is_test,
|
|
83
|
+
module_name=module_name,
|
|
84
|
+
block_types=frozenset({"compound_statement"}),
|
|
85
|
+
switch_types=frozenset({"switch_statement"}),
|
|
86
|
+
switch_value_field="condition",
|
|
87
|
+
case_types=frozenset({"case_statement"}),
|
|
88
|
+
default_when_no_value=True,
|
|
89
|
+
case_fall_through=True,
|
|
90
|
+
loop_types=frozenset({"for_statement", "while_statement", "do_statement"}),
|
|
91
|
+
assignment_types=frozenset({"declaration"}),
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def build_analyzer(root: Path, config: CodeDebriefConfig) -> TreeSitterAnalyzer:
|
|
96
|
+
return TreeSitterAnalyzer(root, config, C_PROFILE)
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""C++ language profile for the profile-driven tree-sitter analyzer."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import tree_sitter_cpp
|
|
10
|
+
|
|
11
|
+
from codedebrief.analysis.languages._common import module_name, text
|
|
12
|
+
from codedebrief.analysis.treesitter import (
|
|
13
|
+
LanguageProfile,
|
|
14
|
+
TreeSitterAnalyzer,
|
|
15
|
+
TSDefinition,
|
|
16
|
+
)
|
|
17
|
+
from codedebrief.config import CodeDebriefConfig
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _definitions(
|
|
21
|
+
root: Any, source: bytes, relative: str, profile: LanguageProfile
|
|
22
|
+
) -> Iterable[TSDefinition]:
|
|
23
|
+
yield from _walk(root, source, "")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _walk(node: Any, source: bytes, owner: str) -> Iterable[TSDefinition]:
|
|
27
|
+
if node.type in {"class_specifier", "struct_specifier", "namespace_definition"}:
|
|
28
|
+
name = text(node.child_by_field_name("name"), source)
|
|
29
|
+
next_owner = ".".join(part for part in (owner, name) if part)
|
|
30
|
+
body = node.child_by_field_name("body")
|
|
31
|
+
for child in body.children if body is not None else node.children:
|
|
32
|
+
if child.is_named:
|
|
33
|
+
yield from _walk(child, source, next_owner)
|
|
34
|
+
return
|
|
35
|
+
if node.type == "function_definition":
|
|
36
|
+
name, explicit_owner = _qualified_function_name(
|
|
37
|
+
node.child_by_field_name("declarator"), source
|
|
38
|
+
)
|
|
39
|
+
body = node.child_by_field_name("body")
|
|
40
|
+
if name and body is not None:
|
|
41
|
+
yield TSDefinition(name=name, node=node, body=body, owner=explicit_owner or owner)
|
|
42
|
+
return
|
|
43
|
+
for child in node.children:
|
|
44
|
+
if child.is_named:
|
|
45
|
+
yield from _walk(child, source, owner)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _qualified_function_name(declarator: Any | None, source: bytes) -> tuple[str, str]:
|
|
49
|
+
"""Return (name, owner) from a possibly nested/qualified function declarator."""
|
|
50
|
+
target = declarator
|
|
51
|
+
while target is not None:
|
|
52
|
+
inner = target.child_by_field_name("declarator")
|
|
53
|
+
if inner is None:
|
|
54
|
+
break
|
|
55
|
+
target = inner
|
|
56
|
+
identifiers = _identifier_texts(target, source)
|
|
57
|
+
if not identifiers:
|
|
58
|
+
identifiers = _identifier_texts(declarator, source)
|
|
59
|
+
if not identifiers:
|
|
60
|
+
return "", ""
|
|
61
|
+
return identifiers[-1], ".".join(identifiers[:-1])
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _identifier_texts(node: Any | None, source: bytes) -> list[str]:
|
|
65
|
+
if node is None:
|
|
66
|
+
return []
|
|
67
|
+
values: list[str] = []
|
|
68
|
+
stack = [node]
|
|
69
|
+
while stack:
|
|
70
|
+
current = stack.pop()
|
|
71
|
+
if current.type in {"identifier", "field_identifier", "type_identifier", "destructor_name"}:
|
|
72
|
+
values.append(text(current, source).lstrip("~"))
|
|
73
|
+
continue
|
|
74
|
+
stack.extend(reversed(current.children))
|
|
75
|
+
return [value for value in values if value]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _classify(
|
|
79
|
+
definition: TSDefinition, relative: str, source: str, config: CodeDebriefConfig
|
|
80
|
+
) -> tuple[str, str, bool]:
|
|
81
|
+
owner_prefix = f"{definition.owner}." if definition.owner else ""
|
|
82
|
+
override = config.entrypoint_override(f"{relative}:{owner_prefix}{definition.name}")
|
|
83
|
+
if definition.name == "main" and not definition.owner:
|
|
84
|
+
return "cpp", "main", override if override is not None else True
|
|
85
|
+
public = config.include_public_functions and not _has_static_storage(definition.node)
|
|
86
|
+
return (
|
|
87
|
+
"generic",
|
|
88
|
+
"method" if definition.owner else "function",
|
|
89
|
+
(override if override is not None else public),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _has_static_storage(node: Any) -> bool:
|
|
94
|
+
return any(
|
|
95
|
+
child.type == "storage_class_specifier" and child.text.decode() == "static"
|
|
96
|
+
for child in node.children
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _is_test(relative: str, name: str) -> bool:
|
|
101
|
+
lowered = relative.lower()
|
|
102
|
+
segments = lowered.split("/")
|
|
103
|
+
filename = segments[-1]
|
|
104
|
+
return (
|
|
105
|
+
any(segment in {"test", "tests"} for segment in segments[:-1])
|
|
106
|
+
or filename.startswith("test_")
|
|
107
|
+
or filename.endswith(
|
|
108
|
+
(
|
|
109
|
+
"_test.cc",
|
|
110
|
+
"_test.cpp",
|
|
111
|
+
"_test.cxx",
|
|
112
|
+
"_test.hh",
|
|
113
|
+
"_test.hpp",
|
|
114
|
+
"_test.hxx",
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
CPP_PROFILE = LanguageProfile(
|
|
121
|
+
language="cpp",
|
|
122
|
+
grammar_loader=tree_sitter_cpp.language,
|
|
123
|
+
function_types=frozenset({"function_definition"}),
|
|
124
|
+
definitions=_definitions,
|
|
125
|
+
classify=_classify,
|
|
126
|
+
is_test=_is_test,
|
|
127
|
+
module_name=module_name,
|
|
128
|
+
block_types=frozenset({"compound_statement"}),
|
|
129
|
+
switch_types=frozenset({"switch_statement"}),
|
|
130
|
+
switch_value_field="condition",
|
|
131
|
+
case_types=frozenset({"case_statement"}),
|
|
132
|
+
default_when_no_value=True,
|
|
133
|
+
case_fall_through=True,
|
|
134
|
+
loop_types=frozenset(
|
|
135
|
+
{"for_statement", "while_statement", "do_statement", "range_based_for_statement"}
|
|
136
|
+
),
|
|
137
|
+
throw_types=frozenset({"throw_statement"}),
|
|
138
|
+
call_types=frozenset({"call_expression"}),
|
|
139
|
+
try_type="try_statement",
|
|
140
|
+
catch_types=frozenset({"catch_clause"}),
|
|
141
|
+
assignment_types=frozenset({"declaration"}),
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def build_analyzer(root: Path, config: CodeDebriefConfig) -> TreeSitterAnalyzer:
|
|
146
|
+
return TreeSitterAnalyzer(root, config, CPP_PROFILE)
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""C# language profile for the profile-driven tree-sitter analyzer."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import tree_sitter_c_sharp
|
|
9
|
+
|
|
10
|
+
from codedebrief.analysis.common import DEFAULT as DEFAULT_LABEL
|
|
11
|
+
from codedebrief.analysis.languages._common import container_definitions, module_name, named, text
|
|
12
|
+
from codedebrief.analysis.treesitter import (
|
|
13
|
+
CaseInfo,
|
|
14
|
+
LanguageProfile,
|
|
15
|
+
TreeSitterAnalyzer,
|
|
16
|
+
TSDefinition,
|
|
17
|
+
)
|
|
18
|
+
from codedebrief.config import CodeDebriefConfig
|
|
19
|
+
|
|
20
|
+
_CONTAINERS = frozenset(
|
|
21
|
+
{
|
|
22
|
+
"class_declaration",
|
|
23
|
+
"struct_declaration",
|
|
24
|
+
"record_declaration",
|
|
25
|
+
"interface_declaration",
|
|
26
|
+
"namespace_declaration",
|
|
27
|
+
"file_scoped_namespace_declaration",
|
|
28
|
+
}
|
|
29
|
+
)
|
|
30
|
+
_METHODS = frozenset({"method_declaration", "constructor_declaration", "local_function_statement"})
|
|
31
|
+
_ROUTE_TAGS = ("HttpGet", "HttpPost", "Route", "HttpPut", "HttpDelete")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _modifiers(node: Any, source: bytes) -> str:
|
|
35
|
+
kinds = {"modifier", "attribute_list"}
|
|
36
|
+
return " ".join(text(c, source) for c in node.children if c.type in kinds)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _classify(
|
|
40
|
+
definition: TSDefinition, relative: str, source: str, config: CodeDebriefConfig
|
|
41
|
+
) -> tuple[str, str, bool]:
|
|
42
|
+
override = config.entrypoint_override(f"{relative}:{definition.owner}.{definition.name}")
|
|
43
|
+
modifiers = _modifiers(definition.node, source.encode("utf-8"))
|
|
44
|
+
if definition.name == "Main":
|
|
45
|
+
return "csharp", "main", override if override is not None else True
|
|
46
|
+
if any(tag in modifiers for tag in _ROUTE_TAGS):
|
|
47
|
+
return "aspnet", "route", override if override is not None else True
|
|
48
|
+
public = config.include_public_functions and "public" in modifiers
|
|
49
|
+
return "generic", "method", override if override is not None else public
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _is_test(relative: str, name: str) -> bool:
|
|
53
|
+
# Anchor to path SEGMENTS and the C# *Test.cs / *Tests.cs class-file convention, not a
|
|
54
|
+
# substring of the whole path or a bare `Test`-prefixed method name (`TestRunner`,
|
|
55
|
+
# `TestData`...) - those are real methods. The file suffix is matched case-sensitively
|
|
56
|
+
# so `Latest.cs` (a real class) does not look like a test.
|
|
57
|
+
segments = relative.split("/")
|
|
58
|
+
return any(segment.lower() in {"test", "tests"} for segment in segments[:-1]) or segments[
|
|
59
|
+
-1
|
|
60
|
+
].endswith(("Test.cs", "Tests.cs"))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _import_map(root: Any, source: bytes, relative: str) -> dict[str, str]:
|
|
64
|
+
mapping: dict[str, str] = {}
|
|
65
|
+
for directive in root.children:
|
|
66
|
+
if directive.type != "using_directive":
|
|
67
|
+
continue
|
|
68
|
+
specifier = _using_specifier(directive, source)
|
|
69
|
+
if not specifier:
|
|
70
|
+
continue
|
|
71
|
+
alias = directive.child_by_field_name("name")
|
|
72
|
+
is_static = any(child.type == "static" for child in directive.children)
|
|
73
|
+
if alias is not None:
|
|
74
|
+
mapping[text(alias, source)] = f"{specifier}:"
|
|
75
|
+
elif is_static:
|
|
76
|
+
mapping[f"__static_using__:{specifier}"] = f"{specifier}:"
|
|
77
|
+
else:
|
|
78
|
+
mapping[f"__namespace_using__:{specifier}"] = f"{specifier}:"
|
|
79
|
+
return mapping
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _using_specifier(directive: Any, source: bytes) -> str:
|
|
83
|
+
for child in directive.children:
|
|
84
|
+
if child.type in {"qualified_name", "alias_qualified_name"}:
|
|
85
|
+
return text(child, source)
|
|
86
|
+
alias = directive.child_by_field_name("name")
|
|
87
|
+
for child in directive.children:
|
|
88
|
+
if child.type == "identifier" and child is not alias:
|
|
89
|
+
value = text(child, source)
|
|
90
|
+
if value != "static":
|
|
91
|
+
return value
|
|
92
|
+
return ""
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _switch_cases(switch_node: Any, source: bytes, profile: LanguageProfile) -> list[CaseInfo]:
|
|
96
|
+
body = switch_node.child_by_field_name("body")
|
|
97
|
+
cases: list[CaseInfo] = []
|
|
98
|
+
for section in named(body):
|
|
99
|
+
if section.type != "switch_section":
|
|
100
|
+
continue
|
|
101
|
+
labels = [c for c in named(section) if "pattern" in c.type or "switch_label" in c.type]
|
|
102
|
+
statements = [c for c in named(section) if c not in labels]
|
|
103
|
+
values = [text(label, source) for label in labels]
|
|
104
|
+
if not values:
|
|
105
|
+
cases.append(CaseInfo(DEFAULT_LABEL, True, [], statements))
|
|
106
|
+
else:
|
|
107
|
+
cases.append(CaseInfo(", ".join(values), False, values, statements))
|
|
108
|
+
return cases
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
CSHARP_PROFILE = LanguageProfile(
|
|
112
|
+
language="csharp",
|
|
113
|
+
grammar_loader=tree_sitter_c_sharp.language,
|
|
114
|
+
function_types=_METHODS,
|
|
115
|
+
definitions=container_definitions(_CONTAINERS, _METHODS),
|
|
116
|
+
classify=_classify,
|
|
117
|
+
is_test=_is_test,
|
|
118
|
+
module_name=module_name,
|
|
119
|
+
import_map=_import_map,
|
|
120
|
+
dependency_module_suffixes=(".cs",),
|
|
121
|
+
dependency_package_directories=True,
|
|
122
|
+
switch_types=frozenset({"switch_statement"}),
|
|
123
|
+
switch_value_field="value",
|
|
124
|
+
switch_cases=_switch_cases,
|
|
125
|
+
loop_types=frozenset({"for_statement", "foreach_statement", "while_statement", "do_statement"}),
|
|
126
|
+
throw_types=frozenset({"throw_statement"}),
|
|
127
|
+
call_types=frozenset({"invocation_expression"}),
|
|
128
|
+
try_type="try_statement",
|
|
129
|
+
catch_types=frozenset({"catch_clause"}),
|
|
130
|
+
finally_types=frozenset({"finally_clause"}),
|
|
131
|
+
assignment_types=frozenset({"local_declaration_statement", "assignment_expression"}),
|
|
132
|
+
nested_def_types=frozenset({"lambda_expression", "anonymous_method_expression"}),
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def build_analyzer(root: Path, config: CodeDebriefConfig) -> TreeSitterAnalyzer:
|
|
137
|
+
return TreeSitterAnalyzer(root, config, CSHARP_PROFILE)
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""Go language profile for the profile-driven tree-sitter analyzer."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections import deque
|
|
6
|
+
from collections.abc import Iterable
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import tree_sitter_go
|
|
11
|
+
|
|
12
|
+
from codedebrief.analysis.languages._common import module_name, text
|
|
13
|
+
from codedebrief.analysis.treesitter import (
|
|
14
|
+
LanguageProfile,
|
|
15
|
+
TreeSitterAnalyzer,
|
|
16
|
+
TSDefinition,
|
|
17
|
+
)
|
|
18
|
+
from codedebrief.config import CodeDebriefConfig
|
|
19
|
+
from codedebrief.model import Flow
|
|
20
|
+
|
|
21
|
+
_TEST_PREFIXES = ("Test", "Benchmark", "Example", "Fuzz")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _definitions(
|
|
25
|
+
root: Any, source: bytes, relative: str, profile: LanguageProfile
|
|
26
|
+
) -> Iterable[TSDefinition]:
|
|
27
|
+
for node in root.children:
|
|
28
|
+
if node.type == "function_declaration":
|
|
29
|
+
name = text(node.child_by_field_name("name"), source)
|
|
30
|
+
body = node.child_by_field_name("body")
|
|
31
|
+
if name and body is not None:
|
|
32
|
+
yield TSDefinition(name=name, node=node, body=body, owner="")
|
|
33
|
+
elif node.type == "method_declaration":
|
|
34
|
+
name = text(node.child_by_field_name("name"), source)
|
|
35
|
+
body = node.child_by_field_name("body")
|
|
36
|
+
owner = _receiver_type(node.child_by_field_name("receiver"), source)
|
|
37
|
+
if name and body is not None:
|
|
38
|
+
yield TSDefinition(name=name, node=node, body=body, owner=owner)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _receiver_type(receiver: Any | None, source: bytes) -> str:
|
|
42
|
+
if receiver is None:
|
|
43
|
+
return ""
|
|
44
|
+
stack = [receiver]
|
|
45
|
+
while stack:
|
|
46
|
+
current = stack.pop()
|
|
47
|
+
if current.type == "type_identifier":
|
|
48
|
+
return text(current, source)
|
|
49
|
+
stack.extend(current.children)
|
|
50
|
+
return ""
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _import_map(root: Any, source: bytes, relative: str) -> dict[str, str]:
|
|
54
|
+
mapping: dict[str, str] = {}
|
|
55
|
+
for declaration in root.children:
|
|
56
|
+
if declaration.type != "import_declaration":
|
|
57
|
+
continue
|
|
58
|
+
for spec in _import_specs(declaration):
|
|
59
|
+
path = _import_path(spec, source)
|
|
60
|
+
if not path:
|
|
61
|
+
continue
|
|
62
|
+
module = path.replace("/", ".").strip(".")
|
|
63
|
+
if not module:
|
|
64
|
+
continue
|
|
65
|
+
alias = _import_alias(spec, source)
|
|
66
|
+
if alias in {"_", "."}:
|
|
67
|
+
mapping[f"__side_effect_import__:{module}"] = f"{module}:"
|
|
68
|
+
continue
|
|
69
|
+
binding = alias or path.rsplit("/", 1)[-1]
|
|
70
|
+
if binding:
|
|
71
|
+
mapping[binding] = f"{module}:"
|
|
72
|
+
return mapping
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _import_specs(declaration: Any) -> Iterable[Any]:
|
|
76
|
+
stack = deque(declaration.children)
|
|
77
|
+
while stack:
|
|
78
|
+
current = stack.popleft()
|
|
79
|
+
if current.type == "import_spec":
|
|
80
|
+
yield current
|
|
81
|
+
continue
|
|
82
|
+
stack.extendleft(reversed(current.children))
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _import_path(spec: Any, source: bytes) -> str:
|
|
86
|
+
literal = next(
|
|
87
|
+
(
|
|
88
|
+
child
|
|
89
|
+
for child in spec.children
|
|
90
|
+
if child.type in {"interpreted_string_literal", "raw_string_literal"}
|
|
91
|
+
),
|
|
92
|
+
None,
|
|
93
|
+
)
|
|
94
|
+
return text(literal, source).strip('"`')
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _import_alias(spec: Any, source: bytes) -> str:
|
|
98
|
+
for child in spec.children:
|
|
99
|
+
if child.type in {"package_identifier", "blank_identifier", "dot"}:
|
|
100
|
+
return text(child, source)
|
|
101
|
+
return ""
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _classify(
|
|
105
|
+
definition: TSDefinition, relative: str, source: str, config: CodeDebriefConfig
|
|
106
|
+
) -> tuple[str, str, bool]:
|
|
107
|
+
owner_prefix = f"{definition.owner}." if definition.owner else ""
|
|
108
|
+
override = config.entrypoint_override(f"{relative}:{owner_prefix}{definition.name}")
|
|
109
|
+
exported = definition.name[:1].isupper()
|
|
110
|
+
if definition.name == "main" and not definition.owner:
|
|
111
|
+
return "go", "main", override if override is not None else True
|
|
112
|
+
entry_kind = "method" if definition.owner else "function"
|
|
113
|
+
public = config.include_public_functions and exported
|
|
114
|
+
return "generic", entry_kind, override if override is not None else public
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _is_test(relative: str, name: str) -> bool:
|
|
118
|
+
return relative.endswith("_test.go") or name.startswith(_TEST_PREFIXES)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _dependency_path_filter(relative: str) -> bool:
|
|
122
|
+
return not relative.endswith("_test.go")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _entry_label(flow: Flow) -> str:
|
|
126
|
+
prefix = {"main": "Main", "test": "Test"}.get(flow.entry_kind)
|
|
127
|
+
return f"{prefix}: {flow.name}" if prefix else flow.name
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
GO_PROFILE = LanguageProfile(
|
|
131
|
+
language="go",
|
|
132
|
+
grammar_loader=tree_sitter_go.language,
|
|
133
|
+
function_types=frozenset({"function_declaration", "method_declaration"}),
|
|
134
|
+
definitions=_definitions,
|
|
135
|
+
classify=_classify,
|
|
136
|
+
is_test=_is_test,
|
|
137
|
+
module_name=module_name,
|
|
138
|
+
import_map=_import_map,
|
|
139
|
+
dependency_module_suffixes=(".go",),
|
|
140
|
+
dependency_package_directories=True,
|
|
141
|
+
dependency_path_filter=_dependency_path_filter,
|
|
142
|
+
entry_label=_entry_label,
|
|
143
|
+
switch_types=frozenset({"expression_switch_statement", "type_switch_statement"}),
|
|
144
|
+
switch_body_field=None,
|
|
145
|
+
case_types=frozenset({"expression_case", "type_case"}),
|
|
146
|
+
default_types=frozenset({"default_case", "communication_case"}),
|
|
147
|
+
case_value_field="value",
|
|
148
|
+
loop_types=frozenset({"for_statement"}),
|
|
149
|
+
throw_types=frozenset(),
|
|
150
|
+
assignment_types=frozenset({"short_var_declaration", "assignment_statement"}),
|
|
151
|
+
assignment_target_field="left",
|
|
152
|
+
nested_def_types=frozenset({"func_literal"}),
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def build_analyzer(root: Path, config: CodeDebriefConfig) -> TreeSitterAnalyzer:
|
|
157
|
+
return TreeSitterAnalyzer(root, config, GO_PROFILE)
|