Skip to content

Commit 7732c87

Browse files
authored
Prepare CodeClone 2.0.2 release (#32)
* chore(release): prepare 2.0.2 docs and lockfile * fix(cli): show 2.0.2 dead-code migration note
1 parent 58b01b0 commit 7732c87

10 files changed

Lines changed: 188 additions & 56 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Changelog
22

3-
## Unreleased
3+
## [2.0.2] - 2026-05-19
44

55
`2.0.2` is a focused patch release for VS Code extension packaging metadata,
66
README link behavior, and dead-code runtime reachability precision.
@@ -24,6 +24,9 @@ README link behavior, and dead-code runtime reachability precision.
2424
- Treat `__all__` re-exports, PEP 562 lazy `_EXPORTS` modules, and guarded
2525
dynamic `getattr(..., "method")` callable dispatch as dead-code reachability
2626
evidence.
27+
- Show a one-time interactive CLI migration note when a trusted `2.0.1`
28+
baseline is analyzed by `2.0.2`, clarifying that fewer dead-code findings are
29+
expected after the refined reachability model.
2730

2831
### Internal
2932

codeclone/surfaces/cli/tips.py

Lines changed: 55 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import sys
1111
from collections.abc import Mapping
1212
from pathlib import Path
13-
from typing import TextIO
13+
from typing import NamedTuple, TextIO
1414

1515
from packaging.version import InvalidVersion, Version
1616

@@ -20,16 +20,16 @@
2020
from .types import PrinterLike
2121

2222
_VSCODE_EXTENSION_TIP_KEY = "vscode_extension"
23-
_DEAD_CODE_REACHABILITY_MIGRATION_TIP_KEY = (
23+
_DEAD_CODE_REACHABILITY_2_0_1_MIGRATION_TIP_KEY = (
2424
"dead_code_reachability_2_0_1_migration_shown"
2525
)
26+
_DEAD_CODE_REACHABILITY_2_0_2_MIGRATION_TIP_KEY = (
27+
"dead_code_reachability_2_0_2_migration_shown"
28+
)
2629
_TIPS_SCHEMA_VERSION = 1
2730
_VSCODE_EXTENSION_URL = (
2831
"https://marketplace.visualstudio.com/items?itemName=orenlab.codeclone"
2932
)
30-
_DEAD_CODE_REACHABILITY_BASELINE_MIN = Version("2.0.0b1")
31-
_DEAD_CODE_REACHABILITY_BASELINE_MAX = Version("2.0.0")
32-
_DEAD_CODE_REACHABILITY_CURRENT_MIN = Version("2.0.1")
3333
_CI_ENV_KEYS: tuple[str, ...] = (
3434
"CI",
3535
"GITHUB_ACTIONS",
@@ -44,6 +44,35 @@
4444
)
4545

4646

47+
class _DeadCodeReachabilityMigration(NamedTuple):
48+
tip_key: str
49+
baseline_min: Version
50+
baseline_max: Version
51+
current_min: Version
52+
target_version: str
53+
54+
55+
_DEAD_CODE_REACHABILITY_MIGRATIONS: tuple[
56+
_DeadCodeReachabilityMigration,
57+
...,
58+
] = (
59+
_DeadCodeReachabilityMigration(
60+
tip_key=_DEAD_CODE_REACHABILITY_2_0_2_MIGRATION_TIP_KEY,
61+
baseline_min=Version("2.0.1"),
62+
baseline_max=Version("2.0.1"),
63+
current_min=Version("2.0.2"),
64+
target_version="2.0.2",
65+
),
66+
_DeadCodeReachabilityMigration(
67+
tip_key=_DEAD_CODE_REACHABILITY_2_0_1_MIGRATION_TIP_KEY,
68+
baseline_min=Version("2.0.0b1"),
69+
baseline_max=Version("2.0.0"),
70+
current_min=Version("2.0.1"),
71+
target_version="2.0.1",
72+
),
73+
)
74+
75+
4776
def _tips_state_path(cache_path: Path) -> Path:
4877
return cache_path.parent / "tips.json"
4978

@@ -165,24 +194,25 @@ def _tip_context_allowed(
165194
return _stream_is_tty(stream)
166195

167196

168-
def _dead_code_reachability_migration_applies(
197+
def _dead_code_reachability_migration(
169198
*,
170199
baseline_generator_version: str | None,
171200
codeclone_version: str,
172-
) -> bool:
201+
) -> _DeadCodeReachabilityMigration | None:
173202
if not baseline_generator_version:
174-
return False
203+
return None
175204
try:
176205
baseline_version = Version(baseline_generator_version)
177206
current_version = Version(codeclone_version)
178207
except InvalidVersion:
179-
return False
180-
return (
181-
_DEAD_CODE_REACHABILITY_BASELINE_MIN
182-
<= baseline_version
183-
<= _DEAD_CODE_REACHABILITY_BASELINE_MAX
184-
and current_version >= _DEAD_CODE_REACHABILITY_CURRENT_MIN
185-
)
208+
return None
209+
for migration in _DEAD_CODE_REACHABILITY_MIGRATIONS:
210+
if (
211+
migration.baseline_min <= baseline_version <= migration.baseline_max
212+
and current_version >= migration.current_min
213+
):
214+
return migration
215+
return None
186216

187217

188218
def maybe_print_vscode_extension_tip(
@@ -239,10 +269,11 @@ def maybe_print_dead_code_reachability_migration_note(
239269
) -> bool:
240270
if not baseline_trusted_for_diff:
241271
return False
242-
if not _dead_code_reachability_migration_applies(
272+
migration = _dead_code_reachability_migration(
243273
baseline_generator_version=baseline_generator_version,
244274
codeclone_version=codeclone_version,
245-
):
275+
)
276+
if migration is None:
246277
return False
247278

248279
effective_environ = os.environ if environ is None else environ
@@ -258,16 +289,20 @@ def maybe_print_dead_code_reachability_migration_note(
258289
state = _load_tips_state(state_path)
259290
if _tip_was_shown(
260291
state,
261-
tip_key=_DEAD_CODE_REACHABILITY_MIGRATION_TIP_KEY,
292+
tip_key=migration.tip_key,
262293
):
263294
return False
264295

265-
console.print(ui.fmt_dead_code_reachability_migration_note())
296+
console.print(
297+
ui.fmt_dead_code_reachability_migration_note(
298+
target_version=migration.target_version,
299+
)
300+
)
266301
try:
267302
_remember_tip_shown(
268303
path=state_path,
269304
state=state,
270-
tip_key=_DEAD_CODE_REACHABILITY_MIGRATION_TIP_KEY,
305+
tip_key=migration.tip_key,
271306
)
272307
except OSError:
273308
return True

codeclone/ui_messages/__init__.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -360,12 +360,19 @@
360360
"navigation.\n"
361361
"[dim]{url}[/dim]"
362362
)
363-
NOTE_DEAD_CODE_REACHABILITY_MIGRATION = (
363+
NOTE_DEAD_CODE_REACHABILITY_2_0_1_MIGRATION = (
364364
"\n[dim]Note:[/dim] Dead-code reachability was refined in 2.0.1 for "
365365
"common Python frameworks.\n"
366366
"[dim]Fewer dead-code findings after upgrading from 2.0.0 are expected: "
367367
"this usually means reduced false positives, not weaker detection.[/dim]"
368368
)
369+
NOTE_DEAD_CODE_REACHABILITY_2_0_2_MIGRATION = (
370+
"\n[dim]Note:[/dim] Dead-code reachability was refined again in 2.0.2.\n"
371+
"[dim]Fewer dead-code findings after upgrading from 2.0.1 are expected: "
372+
"framework hooks, public exports, and guarded dynamic dispatch now produce "
373+
"fewer false positives, not weaker detection.[/dim]"
374+
)
375+
NOTE_DEAD_CODE_REACHABILITY_MIGRATION = NOTE_DEAD_CODE_REACHABILITY_2_0_1_MIGRATION
369376

370377
_RICH_MARKUP_TAG_RE = re.compile(r"\[/?[a-zA-Z][a-zA-Z0-9_ .#:-]*]")
371378

@@ -445,8 +452,13 @@ def fmt_vscode_extension_tip(*, url: str) -> str:
445452
return TIP_VSCODE_EXTENSION.format(url=url)
446453

447454

448-
def fmt_dead_code_reachability_migration_note() -> str:
449-
return NOTE_DEAD_CODE_REACHABILITY_MIGRATION
455+
def fmt_dead_code_reachability_migration_note(
456+
*,
457+
target_version: str = "2.0.1",
458+
) -> str:
459+
if target_version == "2.0.2":
460+
return NOTE_DEAD_CODE_REACHABILITY_2_0_2_MIGRATION
461+
return NOTE_DEAD_CODE_REACHABILITY_2_0_1_MIGRATION
450462

451463

452464
def fmt_legacy_cache_warning(*, legacy_path: Path, new_path: Path) -> str:

docs/book/08-report.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
## Purpose
44

5-
Define the canonical report contract in `2.0.1`: report schema `2.11` plus
6-
deterministic text/Markdown/SARIF/HTML projections.
5+
Define the canonical report contract for the current 2.0 release line: report
6+
schema `2.11` plus deterministic text/Markdown/SARIF/HTML projections.
77

88
## Public surface
99

docs/book/09-cli.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,12 @@ Refs:
5656
after summary output. The hint is suppressed in `--quiet`, CI, and non-TTY
5757
contexts, and is tracked per CodeClone version next to the resolved project
5858
cache path.
59-
- In interactive non-CI runs, the CLI may print a one-time migration note when
60-
a trusted baseline from the `2.0.0` line is analyzed by `2.0.1` or newer. The
61-
note explains expected dead-code count reductions from the refined framework
62-
reachability model and is remembered next to the resolved project cache path.
59+
- In interactive non-CI runs, the CLI may print one-time migration notes when a
60+
trusted baseline was produced by a release whose dead-code reachability model
61+
is known to be narrower than the current version, such as `2.0.0` -> `2.0.1`
62+
or `2.0.1` -> `2.0.2`. Notes explain expected dead-code count reductions from
63+
refined reachability evidence and are remembered next to the resolved project
64+
cache path.
6365
- Changed-scope review uses:
6466
- `--changed-only`
6567
- `--diff-against`

docs/book/16-dead-code-contract.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,24 @@ Refs:
6060
- A top-level symbol listed in a literal `__all__` export is not dead. This is
6161
resolved to exact module-level function/class qualnames and does not mark
6262
same-named methods live.
63+
- A symbol re-exported through a literal `__all__` entry and an exact
64+
`from module import Symbol` binding is resolved back to the imported
65+
canonical qualname.
66+
- A symbol exposed through a PEP 562 lazy-export module is resolved when the
67+
module has a module-level `__getattr__`, a literal `_EXPORTS` mapping, and a
68+
matching literal `__all__` entry. Dynamic or non-literal export maps are not
69+
interpreted.
6370
- A symbol referenced by package metadata entry points is not dead when
6471
`[project.scripts]`, `[project.gui-scripts]`, `[project.entry-points.*]`, or
6572
`[tool.poetry.scripts]` resolves to an exact known candidate qualname. Unique
6673
suffix matches are allowed only for common `src.<package>` style layouts;
6774
ambiguous matches are ignored.
6875
- A symbol referenced only by qualified-name suffix (without canonical module
6976
match) downgrades confidence to `medium`.
77+
- A method name observed through guarded dynamic lookup is treated as a
78+
referenced local name only when the same callable scope contains all three
79+
pieces of evidence: `getattr(obj, "method", ...)`, `callable(local)` guard,
80+
and a subsequent call through that same local binding.
7081
- Runtime framework registration facts can mark a symbol live when the extractor
7182
observes a deterministic edge from modern Python runtime surfaces:
7283
FastAPI/Starlette route and dependency registration, including
@@ -123,7 +134,10 @@ Refs:
123134
| Symbol used only from tests | Remains actionable dead-code candidate |
124135
| Symbol used through import alias / module alias | Matched via canonical qualname usage |
125136
| Symbol exported through literal `__all__` | Matched via exact module-level qualname |
137+
| Symbol re-exported through literal `__all__` | Matched via exact imported qualname |
138+
| Symbol exposed through literal lazy `_EXPORTS` | Matched via exact lazy-export qualname |
126139
| Symbol exposed through package entry point | Matched via exact/unique project qualname |
140+
| Guarded `getattr(obj, "method")` callable dispatch | Method name becomes runtime reference |
127141
| Symbol registered through a supported runtime edge | Candidate skipped as runtime-reachable |
128142
| `--fail-dead-code` with high-confidence dead items | Gating failure, exit `3` |
129143

@@ -136,6 +150,9 @@ Refs:
136150
not suppress arbitrary same-named local decorators.
137151
- Package entry-point liveness reads only local project metadata and ignores
138152
invalid, dynamic, or ambiguous entry-point references.
153+
- Lazy export and guarded dynamic `getattr` handling require literal AST
154+
evidence and same-scope call evidence; CodeClone does not execute import
155+
hooks or infer arbitrary dynamic dispatch.
139156
- Candidate and result ordering is deterministic.
140157

141158
Refs:
@@ -162,6 +179,9 @@ Refs:
162179
- `tests/test_extractor.py::test_dead_code_uses_cli_and_task_registration_reachability`
163180
- `tests/test_extractor.py::test_extract_collects_referenced_qualnames_for_import_aliases`
164181
- `tests/test_extractor.py::test_extract_collects_referenced_qualnames_for_module_all_exports`
182+
- `tests/test_extractor.py::test_extract_resolves_public_reexports_to_source_symbols`
183+
- `tests/test_extractor.py::test_extract_treats_guarded_dynamic_getattr_call_as_runtime_reference`
184+
- `tests/test_extractor.py::test_extract_ignores_uncalled_dynamic_getattr_probe`
165185
- `tests/test_extractor.py::test_collect_dead_candidates_skips_protocol_and_stub_like_symbols`
166186
- `tests/test_extractor.py::test_collect_dead_candidates_skips_pydantic_hooks_and_dataclass_post_init`
167187
- `tests/test_core_branch_coverage.py::test_project_entrypoints_mark_exact_and_unique_layout_symbols_live`

docs/book/appendix/b-schema-layouts.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@
22

33
## Purpose
44

5-
Compact structural layouts for baseline/cache/report contracts in `2.0.1`.
5+
Compact structural layouts for baseline/cache/report contracts in the current
6+
2.0 release line.
67

78
## Baseline schema (`2.1`)
89

910
```json
1011
{
1112
"meta": {
12-
"generator": { "name": "codeclone", "version": "2.0.1" },
13+
"generator": { "name": "codeclone", "version": "2.0.2" },
1314
"schema_version": "2.1",
1415
"fingerprint_version": "1",
1516
"python_tag": "cp314",
@@ -60,7 +61,7 @@ Notes:
6061
```json
6162
{
6263
"meta": {
63-
"generator": { "name": "codeclone", "version": "2.0.1" },
64+
"generator": { "name": "codeclone", "version": "2.0.2" },
6465
"schema_version": "1.2",
6566
"python_tag": "cp314",
6667
"created_at": "2026-03-11T00:00:00Z",
@@ -156,7 +157,7 @@ Notes:
156157
{
157158
"report_schema_version": "2.11",
158159
"meta": {
159-
"codeclone_version": "2.0.1",
160+
"codeclone_version": "2.0.2",
160161
"project_name": "codeclone",
161162
"scan_root": ".",
162163
"analysis_mode": "full",
@@ -515,7 +516,7 @@ Notes:
515516
"tool": {
516517
"driver": {
517518
"name": "codeclone",
518-
"version": "2.0.1",
519+
"version": "2.0.2",
519520
"rules": [
520521
{
521522
"id": "CCLONE001",

tests/test_cli_inprocess.py

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2338,16 +2338,34 @@ def test_cli_shows_vscode_extension_tip_once_per_version(
23382338
assert "VS Code detected" not in second_out
23392339

23402340

2341+
@pytest.mark.parametrize(
2342+
("generator_version", "expected_message", "expected_tip_key"),
2343+
[
2344+
(
2345+
"2.0.0",
2346+
"Dead-code reachability was refined in 2.0.1",
2347+
"dead_code_reachability_2_0_1_migration_shown",
2348+
),
2349+
(
2350+
"2.0.1",
2351+
"Dead-code reachability was refined again in 2.0.2",
2352+
"dead_code_reachability_2_0_2_migration_shown",
2353+
),
2354+
],
2355+
)
23412356
def test_cli_shows_dead_code_reachability_migration_note_once(
23422357
tmp_path: Path,
23432358
monkeypatch: pytest.MonkeyPatch,
23442359
capsys: pytest.CaptureFixture[str],
2360+
generator_version: str,
2361+
expected_message: str,
2362+
expected_tip_key: str,
23452363
) -> None:
23462364
_write_default_source(tmp_path)
23472365
baseline_path = _write_baseline(
23482366
tmp_path / "baseline.json",
23492367
python_version=_current_py_minor(),
2350-
generator_version="2.0.0",
2368+
generator_version=generator_version,
23512369
)
23522370
tips_path = tmp_path / ".cache" / "codeclone" / "tips.json"
23532371

@@ -2371,14 +2389,12 @@ def test_cli_shows_dead_code_reachability_migration_note_once(
23712389
_assert_after_summary(
23722390
first_out,
23732391
"Note:",
2374-
"Dead-code reachability was refined in 2.0.1",
2392+
expected_message,
23752393
"not weaker detection",
23762394
)
23772395

23782396
state = json.loads(tips_path.read_text("utf-8"))
2379-
assert (
2380-
state["tips"]["dead_code_reachability_2_0_1_migration_shown"]["shown"] is True
2381-
)
2397+
assert state["tips"][expected_tip_key]["shown"] is True
23822398

23832399
_run_parallel_main(
23842400
monkeypatch,
@@ -2392,7 +2408,7 @@ def test_cli_shows_dead_code_reachability_migration_note_once(
23922408
)
23932409
second_out = capsys.readouterr().out
23942410

2395-
assert "Dead-code reachability was refined in 2.0.1" not in second_out
2411+
assert expected_message not in second_out
23962412

23972413

23982414
def test_cli_update_baseline_skips_version_check(

0 commit comments

Comments
 (0)