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.
Files changed (48) hide show
  1. codedebrief/__init__.py +12 -0
  2. codedebrief/analysis/__init__.py +16 -0
  3. codedebrief/analysis/common.py +527 -0
  4. codedebrief/analysis/discovery.py +100 -0
  5. codedebrief/analysis/languages/__init__.py +6 -0
  6. codedebrief/analysis/languages/_common.py +68 -0
  7. codedebrief/analysis/languages/c.py +96 -0
  8. codedebrief/analysis/languages/cpp.py +146 -0
  9. codedebrief/analysis/languages/csharp.py +137 -0
  10. codedebrief/analysis/languages/go.py +157 -0
  11. codedebrief/analysis/languages/java.py +158 -0
  12. codedebrief/analysis/languages/php.py +83 -0
  13. codedebrief/analysis/languages/ruby.py +75 -0
  14. codedebrief/analysis/languages/rust.py +96 -0
  15. codedebrief/analysis/project.py +373 -0
  16. codedebrief/analysis/python.py +939 -0
  17. codedebrief/analysis/registry.py +320 -0
  18. codedebrief/analysis/treesitter.py +884 -0
  19. codedebrief/analysis/typescript.py +1019 -0
  20. codedebrief/artifacts.py +49 -0
  21. codedebrief/cli.py +585 -0
  22. codedebrief/config.py +226 -0
  23. codedebrief/doctor.py +175 -0
  24. codedebrief/install.py +441 -0
  25. codedebrief/mcp_server.py +2720 -0
  26. codedebrief/model.py +189 -0
  27. codedebrief/py.typed +1 -0
  28. codedebrief/quality.py +392 -0
  29. codedebrief/query.py +641 -0
  30. codedebrief/render/__init__.py +6 -0
  31. codedebrief/render/assets/generated/codedebrief-viewer-runtime.iife.js +10 -0
  32. codedebrief/render/assets/panels.js +462 -0
  33. codedebrief/render/assets/shell.js +1649 -0
  34. codedebrief/render/assets/styles.css +1715 -0
  35. codedebrief/render/assets/tree.js +616 -0
  36. codedebrief/render/html.py +191 -0
  37. codedebrief/render/markdown.py +153 -0
  38. codedebrief/render/payload.py +326 -0
  39. codedebrief/render/snapshot.py +769 -0
  40. codedebrief/schema/codedebrief.schema.json +449 -0
  41. codedebrief/util.py +65 -0
  42. codedebrief/validation.py +214 -0
  43. codedebrief-0.11.0.dist-info/METADATA +426 -0
  44. codedebrief-0.11.0.dist-info/RECORD +48 -0
  45. codedebrief-0.11.0.dist-info/WHEEL +4 -0
  46. codedebrief-0.11.0.dist-info/entry_points.txt +2 -0
  47. codedebrief-0.11.0.dist-info/licenses/LICENSE +176 -0
  48. 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)