codeabc 0.1.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 (62) hide show
  1. backend/__init__.py +0 -0
  2. backend/app.py +95 -0
  3. backend/desktop_server.py +46 -0
  4. backend/models.py +629 -0
  5. backend/prompts/__init__.py +0 -0
  6. backend/prompts/annotate.py +44 -0
  7. backend/prompts/edit.py +62 -0
  8. backend/prompts/overview.py +57 -0
  9. backend/prompts/qa.py +45 -0
  10. backend/routers/__init__.py +0 -0
  11. backend/routers/analyze.py +271 -0
  12. backend/routers/project.py +872 -0
  13. backend/services/__init__.py +0 -0
  14. backend/services/action_plan.py +366 -0
  15. backend/services/activity.py +313 -0
  16. backend/services/apimap.py +391 -0
  17. backend/services/async_surface.py +123 -0
  18. backend/services/cache.py +183 -0
  19. backend/services/churn.py +244 -0
  20. backend/services/ci_checks.py +317 -0
  21. backend/services/codemap_export.py +136 -0
  22. backend/services/commands.py +276 -0
  23. backend/services/complexity.py +111 -0
  24. backend/services/config_files.py +190 -0
  25. backend/services/contributing.py +308 -0
  26. backend/services/coverage.py +283 -0
  27. backend/services/datamodels.py +190 -0
  28. backend/services/deep_nesting.py +110 -0
  29. backend/services/dependencies.py +270 -0
  30. backend/services/docs.py +255 -0
  31. backend/services/docstrings.py +163 -0
  32. backend/services/duplicate_code.py +165 -0
  33. backend/services/entrypoints.py +152 -0
  34. backend/services/envscan.py +167 -0
  35. backend/services/error_handling.py +239 -0
  36. backend/services/filenames.py +261 -0
  37. backend/services/github_clone.py +129 -0
  38. backend/services/glossary.py +96 -0
  39. backend/services/health_score.py +381 -0
  40. backend/services/importgraph.py +986 -0
  41. backend/services/integrations.py +263 -0
  42. backend/services/licenses.py +505 -0
  43. backend/services/llm.py +117 -0
  44. backend/services/long_functions.py +101 -0
  45. backend/services/ownership.py +194 -0
  46. backend/services/package_purposes.py +112 -0
  47. backend/services/pr_reading.py +181 -0
  48. backend/services/release_map.py +402 -0
  49. backend/services/report_export.py +232 -0
  50. backend/services/risk.py +88 -0
  51. backend/services/scanner.py +546 -0
  52. backend/services/schedules.py +369 -0
  53. backend/services/security.py +445 -0
  54. backend/services/settings_map.py +234 -0
  55. backend/services/symbols.py +605 -0
  56. backend/services/techdebt.py +124 -0
  57. backend/services/too_many_params.py +102 -0
  58. backend/services/typing_coverage.py +184 -0
  59. codeabc-0.1.0.dist-info/METADATA +283 -0
  60. codeabc-0.1.0.dist-info/RECORD +62 -0
  61. codeabc-0.1.0.dist-info/WHEEL +4 -0
  62. codeabc-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,124 @@
1
+ """Tech-debt markers: surface the codebase's own TODO/FIXME notes.
2
+
3
+ A codebase usually documents its own rough edges inline — ``TODO`` for things
4
+ left unfinished, ``FIXME`` for known-broken spots, ``HACK`` for deliberate
5
+ shortcuts, ``XXX`` for "look here, this is suspicious". Collecting them gives a
6
+ newcomer a fast, honest map of where the authors themselves flagged debt,
7
+ without having to read every file.
8
+
9
+ This is a *self-declared* signal (what the authors wrote down), complementary to
10
+ the churn/ownership history and the static import graph. :func:`scan_tech_debt`
11
+ is pure — it operates on the already-read file contents, so it is unit-testable
12
+ with plain strings and needs no repository.
13
+
14
+ Markers are matched as whole, upper-case words (the near-universal convention),
15
+ which keeps prose like "todo list" or "debugging" from being mistaken for a
16
+ marker.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import re
22
+ from collections import Counter
23
+
24
+ # Whole-word, upper-case markers only. Leading comment punctuation and the
25
+ # usual separators after the marker are dropped so the captured note is clean.
26
+ _MARKER_RE = re.compile(r"\b(TODO|FIXME|HACK|XXX)\b[:\s)\-]*(.*)")
27
+
28
+ # A note longer than this is almost certainly a run-on line; keep the map terse.
29
+ _MAX_NOTE_LEN = 160
30
+
31
+
32
+ def _kind_reason(kind: str, count: int) -> str:
33
+ if kind == "FIXME":
34
+ return f"{count} 处 FIXME:作者标了“这里是坏的/要修”,读到相关代码时别当它是对的。"
35
+ if kind == "HACK":
36
+ return f"{count} 处 HACK:作者承认是临时绕过/取巧,改动前先确认有没有更干净的做法。"
37
+ if kind == "XXX":
38
+ return f"{count} 处 XXX:作者标了“可疑、看这里”,往往藏着隐患或待定决策。"
39
+ return f"{count} 处 TODO:作者留的未完成事项,是了解“还差什么”的最快线索。"
40
+
41
+
42
+ def scan_tech_debt(
43
+ file_contents: dict[str, str],
44
+ *,
45
+ limit: int = 15,
46
+ per_file_limit: int = 5,
47
+ ) -> dict:
48
+ """Collect ``TODO``/``FIXME``/``HACK``/``XXX`` markers from file contents.
49
+
50
+ Args:
51
+ file_contents: mapping of path to file text (as CodeABC already read it).
52
+ limit: how many files to return in the ranked list.
53
+ per_file_limit: how many individual markers to keep per file.
54
+
55
+ Returns ``{"total", "by_kind", "files"}`` where ``files`` is ranked by how
56
+ many markers each file carries (most first), and ``by_kind`` counts each
57
+ marker type across the whole project.
58
+ """
59
+ by_file: dict[str, list[dict]] = {}
60
+ kind_counts: Counter[str] = Counter()
61
+ total = 0
62
+
63
+ for path, content in file_contents.items():
64
+ if not content:
65
+ continue
66
+ for line_no, line in enumerate(content.splitlines(), start=1):
67
+ match = _MARKER_RE.search(line)
68
+ if not match:
69
+ continue
70
+ kind = match.group(1)
71
+ note = match.group(2).strip()[:_MAX_NOTE_LEN]
72
+ by_file.setdefault(path, []).append({"line": line_no, "kind": kind, "note": note})
73
+ kind_counts[kind] += 1
74
+ total += 1
75
+
76
+ files = [
77
+ {"path": path, "count": len(markers), "markers": markers[:per_file_limit]}
78
+ for path, markers in by_file.items()
79
+ ]
80
+ files.sort(key=lambda f: (-f["count"], f["path"]))
81
+
82
+ return {
83
+ "total": total,
84
+ "by_kind": dict(kind_counts),
85
+ "files": files[:limit],
86
+ }
87
+
88
+
89
+ def render_techdebt_markdown(project_name: str, techdebt_data: dict | None) -> str:
90
+ """Render the tech-debt markers as a Markdown section, or ``""`` if none."""
91
+ data = techdebt_data or {}
92
+ files = data.get("files") or []
93
+ if not files:
94
+ return ""
95
+
96
+ total = data.get("total", 0)
97
+ by_kind = data.get("by_kind") or {}
98
+ lines = [
99
+ f"# {project_name} — 待办与技术债",
100
+ "",
101
+ f"> 扫描代码里作者自己留的 TODO / FIXME / HACK / XXX 标记,共 {total} 处,"
102
+ "是了解“作者知道哪里还没做好”的最快线索。",
103
+ "",
104
+ ]
105
+
106
+ if by_kind:
107
+ lines.append("## 按类型")
108
+ lines.append("")
109
+ for kind in ("FIXME", "HACK", "XXX", "TODO"):
110
+ count = by_kind.get(kind)
111
+ if count:
112
+ lines.append(f"- {_kind_reason(kind, count)}")
113
+ lines.append("")
114
+
115
+ lines.append("## 标记最多的文件")
116
+ lines.append("")
117
+ for entry in files:
118
+ lines.append(f"- `{entry['path']}` — {entry['count']} 处")
119
+ for marker in entry["markers"]:
120
+ note = f":{marker['note']}" if marker["note"] else ""
121
+ lines.append(f" - L{marker['line']} `{marker['kind']}`{note}")
122
+ lines.append("")
123
+
124
+ return "\n".join(lines).rstrip() + "\n"
@@ -0,0 +1,102 @@
1
+ """Flag functions that take too many parameters.
2
+
3
+ A long parameter list is a readability and design signal that is orthogonal to
4
+ :mod:`complexity` (branch count), :mod:`deep_nesting` (indentation depth), and
5
+ :mod:`long_functions` (line span): a short, flat function can still be painful to
6
+ call and read if it asks for eight separate arguments. The caller has to remember
7
+ what each positional slot means and keep the order straight, and the reader has to
8
+ hold the whole list in mind to understand what the function needs. These are the
9
+ functions most worth refactoring by grouping related arguments into an object (or
10
+ dataclass), or by splitting the work into smaller, single-purpose helpers.
11
+
12
+ A leading ``self`` / ``cls`` receiver is not counted, and ``*args`` / ``**kwargs``
13
+ are ignored on purpose: they collapse a variable number of arguments rather than
14
+ adding named ones, so they do not carry the same "remember every slot" cost.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import ast
20
+
21
+ # Functions with at least this many named parameters are surfaced.
22
+ _PARAM_THRESHOLD = 6
23
+
24
+
25
+ def _param_count(node: ast.FunctionDef | ast.AsyncFunctionDef) -> int:
26
+ """Number of named parameters, excluding a ``self`` / ``cls`` receiver.
27
+
28
+ Positional-only, regular, and keyword-only parameters are all counted;
29
+ ``*args`` / ``**kwargs`` are not, since they aggregate arguments rather than
30
+ naming individual ones.
31
+ """
32
+ args = node.args
33
+ count = len(args.posonlyargs) + len(args.args) + len(args.kwonlyargs)
34
+ leading = args.posonlyargs or args.args
35
+ if leading and leading[0].arg in ("self", "cls"):
36
+ count -= 1
37
+ return count
38
+
39
+
40
+ def scan_too_many_params(
41
+ file_contents: dict[str, str], *, threshold: int = _PARAM_THRESHOLD, limit: int = 15
42
+ ) -> dict:
43
+ """Find functions that declare ``threshold`` named parameters or more.
44
+
45
+ Args:
46
+ file_contents: mapping of path to file text (as CodeABC already read it).
47
+ threshold: minimum named-parameter count to flag a function.
48
+ limit: how many of the widest functions to return.
49
+
50
+ Returns ``{"total", "max_params", "threshold", "files"}`` where ``files`` lists
51
+ the widest functions (descending), each ``{"path", "function", "params"}``.
52
+ Test files and non-Python files are skipped.
53
+ """
54
+ flagged: list[dict] = []
55
+ overall_max = 0
56
+ for path, content in file_contents.items():
57
+ if not content or not path.endswith(".py"):
58
+ continue
59
+ base = path.rsplit("/", 1)[-1]
60
+ if base.startswith("test_") or base.endswith("_test.py") or "/tests/" in path:
61
+ continue
62
+ try:
63
+ tree = ast.parse(content)
64
+ except (SyntaxError, ValueError):
65
+ continue
66
+ for node in ast.walk(tree):
67
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
68
+ params = _param_count(node)
69
+ overall_max = max(overall_max, params)
70
+ if params >= threshold:
71
+ flagged.append({"path": path, "function": node.name, "params": params})
72
+
73
+ flagged.sort(key=lambda f: (-f["params"], f["path"], f["function"]))
74
+ return {
75
+ "total": len(flagged),
76
+ "max_params": overall_max,
77
+ "threshold": threshold,
78
+ "files": flagged[:limit],
79
+ }
80
+
81
+
82
+ def render_too_many_params_markdown(project_name: str, data: dict | None) -> str:
83
+ """Render the too-many-parameters section, or an empty string when nothing is flagged."""
84
+ if not data or not data.get("files"):
85
+ return ""
86
+
87
+ threshold = data.get("threshold", _PARAM_THRESHOLD)
88
+ lines = [
89
+ f"## 参数过多的函数({project_name})",
90
+ "",
91
+ "下面这些函数一次要接收很多个参数。参数一多,调用它的时候就得记住每个位置该"
92
+ "传什么、顺序还不能错,很容易传错或漏传;读代码的人也要同时盯着一长串参数才"
93
+ f"能明白这个函数到底需要什么。这里列出参数达到 **{threshold} 个或更多**的函数,"
94
+ "它们通常值得把关系紧密的几个参数打包成一个对象(或 dataclass)一起传,或者"
95
+ "干脆拆成职责更单一的小函数。",
96
+ "",
97
+ "| 文件 | 函数 | 参数个数 |",
98
+ "| --- | --- | --- |",
99
+ ]
100
+ for f in data["files"]:
101
+ lines.append(f"| `{f['path']}` | `{f['function']}` | {f['params']} |")
102
+ return "\n".join(lines)
@@ -0,0 +1,184 @@
1
+ """Type-annotation coverage: which public functions leave their types unstated.
2
+
3
+ ``docstrings`` asks whether a module's public surface *explains* itself; this
4
+ asks whether it *types* itself. A function is "typed" when it declares a return
5
+ annotation and every parameter (other than a leading ``self``/``cls``) carries
6
+ one — the same bar mypy's ``disallow-untyped-defs`` enforces. Fully typed code
7
+ says what goes in and what comes out without being run, and it is the difference
8
+ between an editor that can autocomplete and catch mistakes and one that guesses.
9
+
10
+ For someone modifying an unfamiliar codebase, an un-typed public function is a
11
+ quiet trap: ``def merge(a, b)`` gives no hint whether ``a`` is a dict, a path, or
12
+ a dataframe. This ranks the files whose public API is least annotated so the
13
+ riskiest places to change surface at a glance. ``scan_typing_coverage`` is pure
14
+ over the file contents and parses with the standard library, so it needs no
15
+ repository; non-Python files, test files, and files that don't parse are skipped.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import ast
21
+
22
+ _MISSING_SAMPLE = 5 # how many un-typed symbol names to surface per file
23
+
24
+
25
+ def _is_public(name: str) -> bool:
26
+ """Public API names carry no leading underscore (skips dunders and privates)."""
27
+ return not name.startswith("_")
28
+
29
+
30
+ def _is_test_path(path: str) -> bool:
31
+ """Type hints on test functions are noise, so tests are left out of the metric."""
32
+ lower = path.lower()
33
+ base = lower.rsplit("/", 1)[-1]
34
+ return (
35
+ "/tests/" in lower
36
+ or "/test/" in lower
37
+ or base.startswith("test_")
38
+ or base.endswith("_test.py")
39
+ or base == "conftest.py"
40
+ )
41
+
42
+
43
+ def _is_typed(node: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
44
+ """True when the function declares a return type and every parameter is annotated.
45
+
46
+ A leading ``self``/``cls`` is exempt (methods conventionally leave it bare),
47
+ as are the ``*args``/``**kwargs`` only when the codebase omits them; both the
48
+ variadic forms are required to be annotated when present. This matches mypy's
49
+ ``disallow-untyped-defs`` "typed def" bar.
50
+ """
51
+ if node.returns is None:
52
+ return False
53
+ args = node.args
54
+ positional = [*args.posonlyargs, *args.args]
55
+ if positional and positional[0].arg in ("self", "cls"):
56
+ positional = positional[1:]
57
+ for arg in (*positional, *args.kwonlyargs):
58
+ if arg.annotation is None:
59
+ return False
60
+ if args.vararg is not None and args.vararg.annotation is None:
61
+ return False
62
+ if args.kwarg is not None and args.kwarg.annotation is None:
63
+ return False
64
+ return True
65
+
66
+
67
+ def _scan_module(tree: ast.Module) -> tuple[int, int, list[str]]:
68
+ """Return (typeable, typed, untyped_names) for a parsed module.
69
+
70
+ Counts public top-level functions plus public methods of public classes. A
71
+ class itself has no call signature to annotate, so only its methods count.
72
+ Walks the module and class bodies rather than ``ast.walk`` so nested/local
73
+ helpers — which are not part of the callable surface — are left out.
74
+ """
75
+ typeable = 0
76
+ typed = 0
77
+ missing: list[str] = []
78
+
79
+ def _record(is_typed: bool, label: str) -> None:
80
+ nonlocal typeable, typed
81
+ typeable += 1
82
+ if is_typed:
83
+ typed += 1
84
+ else:
85
+ missing.append(label)
86
+
87
+ for node in tree.body:
88
+ if isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef) and _is_public(node.name):
89
+ _record(_is_typed(node), node.name)
90
+ elif isinstance(node, ast.ClassDef) and _is_public(node.name):
91
+ for sub in node.body:
92
+ if isinstance(sub, ast.FunctionDef | ast.AsyncFunctionDef) and _is_public(sub.name):
93
+ _record(_is_typed(sub), f"{node.name}.{sub.name}")
94
+
95
+ return typeable, typed, missing
96
+
97
+
98
+ def scan_typing_coverage(file_contents: dict[str, str], *, limit: int = 15) -> dict:
99
+ """Rank the project's Python files by how much of their public API lacks type hints.
100
+
101
+ Args:
102
+ file_contents: mapping of path to file text (as CodeABC already read it).
103
+ limit: how many of the least-annotated files to return.
104
+
105
+ Returns ``{"total_symbols", "typed", "coverage", "files"}`` where ``coverage``
106
+ is the repo-wide typed fraction (0.0-1.0, and 1.0 when there is no public API
107
+ to type) and ``files`` are the files with the most un-typed public functions
108
+ (descending), each ``{"path", "symbols", "typed", "coverage", "missing"}``.
109
+ """
110
+ total_symbols = 0
111
+ total_typed = 0
112
+ scored: list[dict] = []
113
+
114
+ for path, content in file_contents.items():
115
+ if not content or not path.endswith(".py") or _is_test_path(path):
116
+ continue
117
+ try:
118
+ tree = ast.parse(content)
119
+ except (SyntaxError, ValueError):
120
+ continue
121
+
122
+ typeable, typed, missing = _scan_module(tree)
123
+ if typeable == 0:
124
+ continue # no public functions to type
125
+
126
+ total_symbols += typeable
127
+ total_typed += typed
128
+ if typed >= typeable:
129
+ continue # fully typed, nothing to flag
130
+
131
+ scored.append(
132
+ {
133
+ "path": path,
134
+ "symbols": typeable,
135
+ "typed": typed,
136
+ "coverage": round(typed / typeable, 3),
137
+ "missing": missing[:_MISSING_SAMPLE],
138
+ }
139
+ )
140
+
141
+ coverage = round(total_typed / total_symbols, 3) if total_symbols else 1.0
142
+ # Worst first: most un-typed functions, then lowest coverage, then path.
143
+ scored.sort(key=lambda f: (-(f["symbols"] - f["typed"]), f["coverage"], f["path"]))
144
+ return {
145
+ "total_symbols": total_symbols,
146
+ "typed": total_typed,
147
+ "coverage": coverage,
148
+ "files": scored[:limit],
149
+ }
150
+
151
+
152
+ def render_typing_coverage_markdown(project_name: str, data: dict | None) -> str:
153
+ """Render the type-annotation coverage ranking as Markdown, or ``""`` if nothing to report."""
154
+ data = data or {}
155
+ total = data.get("total_symbols") or 0
156
+ if not total:
157
+ return ""
158
+
159
+ coverage_pct = round((data.get("coverage") or 0.0) * 100)
160
+ typed = data.get("typed") or 0
161
+ files = data.get("files") or []
162
+
163
+ lines = [
164
+ f"# {project_name} — 类型注解覆盖率(哪些公开函数没写全类型标注)",
165
+ "",
166
+ "> 统计每个文件里「别人会调用」的公开函数、方法中有多少写全了类型标注"
167
+ "(参数和返回值都标了)。写全类型的代码不用运行就能看出「传什么、返回什么」,"
168
+ "编辑器也能自动补全并提前报错;没标类型的公开函数正是改起来最容易踩坑的地方。"
169
+ "开头的 `self`/`cls` 不计,私有成员和 dunder(下划线开头)也不计。",
170
+ "",
171
+ f"**整体覆盖率:{coverage_pct}%**({typed}/{total} 个公开函数写全了类型标注)。",
172
+ "",
173
+ ]
174
+ if files:
175
+ lines.append("类型标注最缺的文件:")
176
+ lines.append("")
177
+ for f in files:
178
+ missing = "、".join(f["missing"])
179
+ more = "…" if f["symbols"] - f["typed"] > len(f["missing"]) else ""
180
+ lines.append(
181
+ f"- `{f['path']}` — {f['typed']}/{f['symbols']} 标了类型"
182
+ f"({round(f['coverage'] * 100)}%),缺:{missing}{more}"
183
+ )
184
+ return "\n".join(lines).rstrip() + "\n"