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,158 @@
|
|
|
1
|
+
"""Java 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_java
|
|
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
|
+
{"class_declaration", "interface_declaration", "enum_declaration", "record_declaration"}
|
|
22
|
+
)
|
|
23
|
+
_METHODS = frozenset({"method_declaration", "constructor_declaration"})
|
|
24
|
+
_ROUTE_ANNOTATIONS = (
|
|
25
|
+
"@GetMapping",
|
|
26
|
+
"@PostMapping",
|
|
27
|
+
"@PutMapping",
|
|
28
|
+
"@DeleteMapping",
|
|
29
|
+
"@PatchMapping",
|
|
30
|
+
"@RequestMapping",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _modifiers(node: Any, source: bytes) -> str:
|
|
35
|
+
for child in node.children:
|
|
36
|
+
if child.type == "modifiers":
|
|
37
|
+
return text(child, source)
|
|
38
|
+
return ""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _classify(
|
|
42
|
+
definition: TSDefinition, relative: str, source: str, config: CodeDebriefConfig
|
|
43
|
+
) -> tuple[str, str, bool]:
|
|
44
|
+
override = config.entrypoint_override(f"{relative}:{definition.owner}.{definition.name}")
|
|
45
|
+
modifiers = _modifiers(definition.node, source.encode("utf-8"))
|
|
46
|
+
if definition.name == "main" and "static" in modifiers:
|
|
47
|
+
return "java", "main", override if override is not None else True
|
|
48
|
+
if any(annotation in modifiers for annotation in _ROUTE_ANNOTATIONS):
|
|
49
|
+
return "spring", "route", override if override is not None else True
|
|
50
|
+
public = config.include_public_functions and "public" in modifiers
|
|
51
|
+
return "generic", "method", override if override is not None else public
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _is_test(relative: str, name: str) -> bool:
|
|
55
|
+
# Maven/Gradle put tests under a `test` source-set segment; the class file is
|
|
56
|
+
# *Test.java / *Tests.java / *IT.java. A bare `test`-prefixed METHOD name
|
|
57
|
+
# (`testConnection` in a production class) is a real method, so it must not classify.
|
|
58
|
+
segments = relative.split("/")
|
|
59
|
+
return any(segment == "test" for segment in segments[:-1]) or segments[-1].endswith(
|
|
60
|
+
("Test.java", "Tests.java", "IT.java")
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _import_map(root: Any, source: bytes, relative: str) -> dict[str, str]:
|
|
65
|
+
mapping: dict[str, str] = {}
|
|
66
|
+
for declaration in root.children:
|
|
67
|
+
if declaration.type != "import_declaration":
|
|
68
|
+
continue
|
|
69
|
+
specifier = _import_specifier(declaration, source)
|
|
70
|
+
if not specifier:
|
|
71
|
+
continue
|
|
72
|
+
is_static = any(child.type == "static" for child in declaration.children)
|
|
73
|
+
is_wildcard = any(child.type == "asterisk" for child in declaration.children)
|
|
74
|
+
if is_wildcard:
|
|
75
|
+
mapping[f"__wildcard_import__:{specifier}"] = f"{specifier}:"
|
|
76
|
+
continue
|
|
77
|
+
if is_static:
|
|
78
|
+
owner, _, member = specifier.rpartition(".")
|
|
79
|
+
package, _, class_name = owner.rpartition(".")
|
|
80
|
+
if package and class_name and member:
|
|
81
|
+
mapping[member] = f"{package}:{class_name}.{member}"
|
|
82
|
+
mapping[f"__dependency_import__:{owner}"] = f"{owner}:"
|
|
83
|
+
continue
|
|
84
|
+
package, _, class_name = specifier.rpartition(".")
|
|
85
|
+
if package and class_name:
|
|
86
|
+
mapping[class_name] = f"{package}:{class_name}"
|
|
87
|
+
mapping[f"__dependency_import__:{specifier}"] = f"{specifier}:"
|
|
88
|
+
return mapping
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _import_specifier(declaration: Any, source: bytes) -> str:
|
|
92
|
+
for child in declaration.children:
|
|
93
|
+
if child.type in {"scoped_identifier", "identifier"}:
|
|
94
|
+
return text(child, source)
|
|
95
|
+
return ""
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _switch_cases(switch_node: Any, source: bytes, profile: LanguageProfile) -> list[CaseInfo]:
|
|
99
|
+
body = switch_node.child_by_field_name("body")
|
|
100
|
+
cases: list[CaseInfo] = []
|
|
101
|
+
for group in named(body):
|
|
102
|
+
if group.type != "switch_block_statement_group":
|
|
103
|
+
continue
|
|
104
|
+
labels = [c for c in named(group) if c.type == "switch_label"]
|
|
105
|
+
statements = [c for c in named(group) if c.type != "switch_label"]
|
|
106
|
+
values: list[str] = []
|
|
107
|
+
is_default = False
|
|
108
|
+
for label in labels:
|
|
109
|
+
value = next(iter(named(label)), None)
|
|
110
|
+
if value is None:
|
|
111
|
+
is_default = True
|
|
112
|
+
else:
|
|
113
|
+
values.append(text(value, source))
|
|
114
|
+
if is_default and not values:
|
|
115
|
+
cases.append(CaseInfo(DEFAULT_LABEL, True, [], statements))
|
|
116
|
+
else:
|
|
117
|
+
cases.append(CaseInfo(", ".join(values) or "case", False, values, statements))
|
|
118
|
+
return cases
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _call_name(call: Any, source: bytes) -> str:
|
|
122
|
+
if call.type == "method_invocation":
|
|
123
|
+
return text(call.child_by_field_name("name"), source)
|
|
124
|
+
if call.type == "object_creation_expression":
|
|
125
|
+
return text(call.child_by_field_name("type"), source)
|
|
126
|
+
return ""
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
JAVA_PROFILE = LanguageProfile(
|
|
130
|
+
language="java",
|
|
131
|
+
grammar_loader=tree_sitter_java.language,
|
|
132
|
+
function_types=_METHODS,
|
|
133
|
+
definitions=container_definitions(_CONTAINERS, _METHODS),
|
|
134
|
+
classify=_classify,
|
|
135
|
+
is_test=_is_test,
|
|
136
|
+
module_name=module_name,
|
|
137
|
+
import_map=_import_map,
|
|
138
|
+
dependency_module_suffixes=(".java",),
|
|
139
|
+
dependency_package_directories=True,
|
|
140
|
+
switch_types=frozenset({"switch_expression"}),
|
|
141
|
+
switch_value_field="condition",
|
|
142
|
+
switch_cases=_switch_cases,
|
|
143
|
+
loop_types=frozenset(
|
|
144
|
+
{"for_statement", "enhanced_for_statement", "while_statement", "do_statement"}
|
|
145
|
+
),
|
|
146
|
+
throw_types=frozenset({"throw_statement"}),
|
|
147
|
+
call_types=frozenset({"method_invocation", "object_creation_expression"}),
|
|
148
|
+
call_name=_call_name,
|
|
149
|
+
try_type="try_statement",
|
|
150
|
+
catch_types=frozenset({"catch_clause"}),
|
|
151
|
+
finally_types=frozenset({"finally_clause"}),
|
|
152
|
+
assignment_types=frozenset({"local_variable_declaration", "assignment_expression"}),
|
|
153
|
+
nested_def_types=frozenset({"lambda_expression"}),
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def build_analyzer(root: Path, config: CodeDebriefConfig) -> TreeSitterAnalyzer:
|
|
158
|
+
return TreeSitterAnalyzer(root, config, JAVA_PROFILE)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""PHP 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_php
|
|
9
|
+
|
|
10
|
+
from codedebrief.analysis.languages._common import container_definitions, module_name, text
|
|
11
|
+
from codedebrief.analysis.treesitter import (
|
|
12
|
+
LanguageProfile,
|
|
13
|
+
TreeSitterAnalyzer,
|
|
14
|
+
TSDefinition,
|
|
15
|
+
)
|
|
16
|
+
from codedebrief.config import CodeDebriefConfig
|
|
17
|
+
|
|
18
|
+
_CONTAINERS = frozenset(
|
|
19
|
+
{"class_declaration", "interface_declaration", "trait_declaration", "enum_declaration"}
|
|
20
|
+
)
|
|
21
|
+
_METHODS = frozenset({"method_declaration", "function_definition"})
|
|
22
|
+
_VISIBILITY = {"visibility_modifier", "static_modifier", "abstract_modifier"}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _classify(
|
|
26
|
+
definition: TSDefinition, relative: str, source: str, config: CodeDebriefConfig
|
|
27
|
+
) -> tuple[str, str, bool]:
|
|
28
|
+
override = config.entrypoint_override(f"{relative}:{definition.owner}.{definition.name}")
|
|
29
|
+
visibility = " ".join(
|
|
30
|
+
text(c, source.encode("utf-8")) for c in definition.node.children if c.type in _VISIBILITY
|
|
31
|
+
)
|
|
32
|
+
is_private = "private" in visibility or "protected" in visibility
|
|
33
|
+
entry_kind = "method" if definition.owner else "function"
|
|
34
|
+
public = config.include_public_functions and not is_private
|
|
35
|
+
return "generic", entry_kind, override if override is not None else public
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _is_test(relative: str, name: str) -> bool:
|
|
39
|
+
# PHPUnit convention: a *Test.php class under a `test`/`tests` segment. Drop the old
|
|
40
|
+
# `*test.php` substring match (it caught legitimate files like `latest.php` /
|
|
41
|
+
# `request.php`) and the bare `test`-prefixed method name (a real method otherwise).
|
|
42
|
+
segments = relative.split("/")
|
|
43
|
+
return any(segment.lower() in {"test", "tests"} for segment in segments[:-1]) or segments[
|
|
44
|
+
-1
|
|
45
|
+
].endswith("Test.php")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _call_name(call: Any, source: bytes) -> str:
|
|
49
|
+
if call.type == "function_call_expression":
|
|
50
|
+
return text(call.child_by_field_name("function"), source)
|
|
51
|
+
return text(call.child_by_field_name("name"), source)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
PHP_PROFILE = LanguageProfile(
|
|
55
|
+
language="php",
|
|
56
|
+
grammar_loader=tree_sitter_php.language_php,
|
|
57
|
+
function_types=_METHODS,
|
|
58
|
+
definitions=container_definitions(_CONTAINERS, _METHODS),
|
|
59
|
+
classify=_classify,
|
|
60
|
+
is_test=_is_test,
|
|
61
|
+
module_name=module_name,
|
|
62
|
+
block_types=frozenset({"compound_statement"}),
|
|
63
|
+
consequence_field="body",
|
|
64
|
+
switch_types=frozenset({"switch_statement"}),
|
|
65
|
+
switch_value_field="condition",
|
|
66
|
+
case_types=frozenset({"case_statement"}),
|
|
67
|
+
default_types=frozenset({"default_statement"}),
|
|
68
|
+
case_fall_through=True,
|
|
69
|
+
loop_types=frozenset({"for_statement", "while_statement", "foreach_statement", "do_statement"}),
|
|
70
|
+
call_types=frozenset(
|
|
71
|
+
{"function_call_expression", "member_call_expression", "scoped_call_expression"}
|
|
72
|
+
),
|
|
73
|
+
call_name=_call_name,
|
|
74
|
+
try_type="try_statement",
|
|
75
|
+
catch_types=frozenset({"catch_clause"}),
|
|
76
|
+
finally_types=frozenset({"finally_clause"}),
|
|
77
|
+
assignment_types=frozenset({"assignment_expression"}),
|
|
78
|
+
nested_def_types=frozenset({"anonymous_function_creation_expression", "arrow_function"}),
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def build_analyzer(root: Path, config: CodeDebriefConfig) -> TreeSitterAnalyzer:
|
|
83
|
+
return TreeSitterAnalyzer(root, config, PHP_PROFILE)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Ruby 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_ruby
|
|
9
|
+
|
|
10
|
+
from codedebrief.analysis.languages._common import container_definitions, module_name, text
|
|
11
|
+
from codedebrief.analysis.treesitter import (
|
|
12
|
+
LanguageProfile,
|
|
13
|
+
TreeSitterAnalyzer,
|
|
14
|
+
TSDefinition,
|
|
15
|
+
)
|
|
16
|
+
from codedebrief.config import CodeDebriefConfig
|
|
17
|
+
|
|
18
|
+
_CONTAINERS = frozenset({"class", "module", "singleton_class"})
|
|
19
|
+
_METHODS = frozenset({"method", "singleton_method"})
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _classify(
|
|
23
|
+
definition: TSDefinition, relative: str, source: str, config: CodeDebriefConfig
|
|
24
|
+
) -> tuple[str, str, bool]:
|
|
25
|
+
owner_prefix = f"{definition.owner}." if definition.owner else ""
|
|
26
|
+
override = config.entrypoint_override(f"{relative}:{owner_prefix}{definition.name}")
|
|
27
|
+
entry_kind = "method" if definition.owner else "function"
|
|
28
|
+
public = config.include_public_functions and not definition.name.startswith("_")
|
|
29
|
+
return "generic", entry_kind, override if override is not None else public
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _is_test(relative: str, name: str) -> bool:
|
|
33
|
+
# RSpec/minitest convention: a `spec`/`test` directory segment or a *_spec.rb /
|
|
34
|
+
# *_test.rb file. Anchor to path SEGMENTS so `contest/` won't match, and drop the
|
|
35
|
+
# bare `test`-prefixed method name (`test_helper` is a real method in `lib/`).
|
|
36
|
+
segments = relative.lower().split("/")
|
|
37
|
+
return any(segment in {"spec", "test"} for segment in segments[:-1]) or segments[-1].endswith(
|
|
38
|
+
("_spec.rb", "_test.rb")
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _call_name(call: Any, source: bytes) -> str:
|
|
43
|
+
method = call.child_by_field_name("method")
|
|
44
|
+
if method is not None:
|
|
45
|
+
return text(method, source)
|
|
46
|
+
ident = next((c for c in call.children if c.type in {"identifier", "constant"}), None)
|
|
47
|
+
return text(ident, source)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
RUBY_PROFILE = LanguageProfile(
|
|
51
|
+
language="ruby",
|
|
52
|
+
grammar_loader=tree_sitter_ruby.language,
|
|
53
|
+
function_types=_METHODS,
|
|
54
|
+
definitions=container_definitions(_CONTAINERS, _METHODS),
|
|
55
|
+
classify=_classify,
|
|
56
|
+
is_test=_is_test,
|
|
57
|
+
module_name=module_name,
|
|
58
|
+
block_types=frozenset({"body_statement", "then", "else", "do_block", "begin"}),
|
|
59
|
+
if_type="if",
|
|
60
|
+
alternative_types=frozenset({"else"}),
|
|
61
|
+
switch_types=frozenset({"case"}),
|
|
62
|
+
switch_value_field="value",
|
|
63
|
+
switch_body_field=None,
|
|
64
|
+
case_types=frozenset({"when"}),
|
|
65
|
+
case_value_field="pattern",
|
|
66
|
+
default_types=frozenset({"else"}),
|
|
67
|
+
return_type="return",
|
|
68
|
+
loop_types=frozenset({"while", "until", "for"}),
|
|
69
|
+
call_types=frozenset({"call", "command_call", "method_call"}),
|
|
70
|
+
call_name=_call_name,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def build_analyzer(root: Path, config: CodeDebriefConfig) -> TreeSitterAnalyzer:
|
|
75
|
+
return TreeSitterAnalyzer(root, config, RUBY_PROFILE)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Rust 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_rust
|
|
10
|
+
|
|
11
|
+
from codedebrief.analysis.languages._common import module_name, named, 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, owner="")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _walk(node: Any, source: bytes, owner: str) -> Iterable[TSDefinition]:
|
|
27
|
+
if node.type == "impl_item":
|
|
28
|
+
name = text(node.child_by_field_name("type"), source)
|
|
29
|
+
for child in named(node.child_by_field_name("body")):
|
|
30
|
+
yield from _walk(child, source, name)
|
|
31
|
+
return
|
|
32
|
+
if node.type in {"mod_item", "trait_item"}:
|
|
33
|
+
body = node.child_by_field_name("body")
|
|
34
|
+
for child in named(body if body is not None else node):
|
|
35
|
+
yield from _walk(child, source, owner)
|
|
36
|
+
return
|
|
37
|
+
if node.type == "function_item":
|
|
38
|
+
name = text(node.child_by_field_name("name"), source)
|
|
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=owner)
|
|
42
|
+
return
|
|
43
|
+
for child in named(node):
|
|
44
|
+
yield from _walk(child, source, owner)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _classify(
|
|
48
|
+
definition: TSDefinition, relative: str, source: str, config: CodeDebriefConfig
|
|
49
|
+
) -> tuple[str, str, bool]:
|
|
50
|
+
owner_prefix = f"{definition.owner}." if definition.owner else ""
|
|
51
|
+
override = config.entrypoint_override(f"{relative}:{owner_prefix}{definition.name}")
|
|
52
|
+
if definition.name == "main" and not definition.owner:
|
|
53
|
+
return "rust", "main", override if override is not None else True
|
|
54
|
+
is_pub = any(c.type == "visibility_modifier" for c in definition.node.children)
|
|
55
|
+
entry_kind = "method" if definition.owner else "function"
|
|
56
|
+
public = config.include_public_functions and is_pub
|
|
57
|
+
return "generic", entry_kind, override if override is not None else public
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _is_test(relative: str, name: str) -> bool:
|
|
61
|
+
# A Rust test is a `#[test]`/`#[cfg(test)]` item or a file under the `tests/`
|
|
62
|
+
# integration directory - never a bare `test`-prefixed name (a real `test_render`
|
|
63
|
+
# helper in `src/` must stay an analyzable function). The `(relative, name)` signature
|
|
64
|
+
# can't see attributes, so anchor to the `tests/` path SEGMENT (the convention this
|
|
65
|
+
# profile can detect); attribute-gated `#[test]` items are handled where they parse.
|
|
66
|
+
segments = relative.lower().split("/")
|
|
67
|
+
return "tests" in segments[:-1] or segments[-1] == "tests.rs"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
RUST_PROFILE = LanguageProfile(
|
|
71
|
+
language="rust",
|
|
72
|
+
grammar_loader=tree_sitter_rust.language,
|
|
73
|
+
function_types=frozenset({"function_item"}),
|
|
74
|
+
definitions=_definitions,
|
|
75
|
+
classify=_classify,
|
|
76
|
+
is_test=_is_test,
|
|
77
|
+
module_name=module_name,
|
|
78
|
+
if_type="if_expression",
|
|
79
|
+
return_type="return_expression",
|
|
80
|
+
switch_types=frozenset({"match_expression"}),
|
|
81
|
+
switch_value_field="value",
|
|
82
|
+
switch_body_field="body",
|
|
83
|
+
case_types=frozenset({"match_arm"}),
|
|
84
|
+
case_value_field="pattern",
|
|
85
|
+
wildcard_values=frozenset({"_"}),
|
|
86
|
+
exhaustive_switch=True,
|
|
87
|
+
loop_types=frozenset({"loop_expression", "while_expression", "for_expression"}),
|
|
88
|
+
call_types=frozenset({"call_expression"}),
|
|
89
|
+
assignment_types=frozenset({"let_declaration"}),
|
|
90
|
+
nested_def_types=frozenset({"closure_expression"}),
|
|
91
|
+
unwrap_types=frozenset({"expression_statement"}),
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def build_analyzer(root: Path, config: CodeDebriefConfig) -> TreeSitterAnalyzer:
|
|
96
|
+
return TreeSitterAnalyzer(root, config, RUST_PROFILE)
|