sourcecode 1.35.26__py3-none-any.whl → 1.35.28__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.
- sourcecode/__init__.py +1 -1
- sourcecode/cli.py +244 -3
- sourcecode/explain.py +3 -0
- sourcecode/file_chunker.py +397 -0
- sourcecode/rename_refactor.py +351 -0
- sourcecode/repository_ir.py +77 -8
- sourcecode/spring_model.py +2 -0
- {sourcecode-1.35.26.dist-info → sourcecode-1.35.28.dist-info}/METADATA +5 -3
- {sourcecode-1.35.26.dist-info → sourcecode-1.35.28.dist-info}/RECORD +12 -10
- {sourcecode-1.35.26.dist-info → sourcecode-1.35.28.dist-info}/WHEEL +0 -0
- {sourcecode-1.35.26.dist-info → sourcecode-1.35.28.dist-info}/entry_points.txt +0 -0
- {sourcecode-1.35.26.dist-info → sourcecode-1.35.28.dist-info}/licenses/LICENSE +0 -0
sourcecode/__init__.py
CHANGED
sourcecode/cli.py
CHANGED
|
@@ -242,6 +242,10 @@ _SUBCOMMANDS: frozenset[str] = frozenset(
|
|
|
242
242
|
"explain",
|
|
243
243
|
# Spring Boot 2→3 migration readiness
|
|
244
244
|
"migrate-check",
|
|
245
|
+
# Native file rename (BLOCKER-A)
|
|
246
|
+
"rename-class",
|
|
247
|
+
# Large file semantic chunking (BLOCKER-B)
|
|
248
|
+
"chunk-file",
|
|
245
249
|
}
|
|
246
250
|
)
|
|
247
251
|
|
|
@@ -5009,6 +5013,240 @@ def modernize_cmd(
|
|
|
5009
5013
|
_copy_to_clipboard(output)
|
|
5010
5014
|
|
|
5011
5015
|
|
|
5016
|
+
# ── rename-class ──────────────────────────────────────────────────────────────
|
|
5017
|
+
|
|
5018
|
+
@app.command("rename-class")
|
|
5019
|
+
def rename_class_cmd(
|
|
5020
|
+
path: Path = typer.Argument(
|
|
5021
|
+
Path("."),
|
|
5022
|
+
help="Repository root to operate on (default: current directory)",
|
|
5023
|
+
),
|
|
5024
|
+
old_name: str = typer.Option(
|
|
5025
|
+
..., "--from", "-f",
|
|
5026
|
+
help="Current class name (PascalCase, e.g. ServiceA)",
|
|
5027
|
+
),
|
|
5028
|
+
new_name: str = typer.Option(
|
|
5029
|
+
..., "--to", "-t",
|
|
5030
|
+
help="New class name (PascalCase, e.g. ServiceB)",
|
|
5031
|
+
),
|
|
5032
|
+
dry_run: bool = typer.Option(
|
|
5033
|
+
False, "--dry-run",
|
|
5034
|
+
help="Compute changes but do not write any files or rename on disk.",
|
|
5035
|
+
),
|
|
5036
|
+
no_tests: bool = typer.Option(
|
|
5037
|
+
False, "--no-tests",
|
|
5038
|
+
help="Exclude test files from the rename (src/main only).",
|
|
5039
|
+
),
|
|
5040
|
+
output_path: Optional[Path] = typer.Option(
|
|
5041
|
+
None, "--output", "-o",
|
|
5042
|
+
help="Write change audit JSON to a file instead of stdout.",
|
|
5043
|
+
),
|
|
5044
|
+
copy: bool = typer.Option(
|
|
5045
|
+
False, "--copy", "-c",
|
|
5046
|
+
help="Copy output to clipboard after a successful run.",
|
|
5047
|
+
),
|
|
5048
|
+
format: str = typer.Option(
|
|
5049
|
+
"json", "--format",
|
|
5050
|
+
help="Output format: json (default) or yaml.",
|
|
5051
|
+
),
|
|
5052
|
+
) -> None:
|
|
5053
|
+
"""Rename a Java class throughout the repository (BLOCKER-A fix).
|
|
5054
|
+
|
|
5055
|
+
\b
|
|
5056
|
+
Renames a Java class safely:
|
|
5057
|
+
- Updates class/interface/enum declaration
|
|
5058
|
+
- Updates constructor name
|
|
5059
|
+
- Updates all import statements
|
|
5060
|
+
- Updates all type references (fields, params, return types)
|
|
5061
|
+
- Updates extends / implements
|
|
5062
|
+
- Updates generics, casts, Spring @Qualifier names
|
|
5063
|
+
- Renames the physical .java file
|
|
5064
|
+
- Emits a structured change audit trail (BLOCKER-C)
|
|
5065
|
+
|
|
5066
|
+
\b
|
|
5067
|
+
Examples:
|
|
5068
|
+
sourcecode rename-class . --from ServiceA --to ServiceB
|
|
5069
|
+
sourcecode rename-class /path/to/repo --from OrderManager --to OrderService
|
|
5070
|
+
sourcecode rename-class . --from OldName --to NewName --dry-run
|
|
5071
|
+
sourcecode rename-class . --from OldName --to NewName --output rename-audit.json
|
|
5072
|
+
"""
|
|
5073
|
+
import json as _json
|
|
5074
|
+
from sourcecode.rename_refactor import rename_class
|
|
5075
|
+
|
|
5076
|
+
root = path.resolve()
|
|
5077
|
+
if not root.is_dir():
|
|
5078
|
+
_emit_error_json(
|
|
5079
|
+
INVALID_INPUT_CODE,
|
|
5080
|
+
f"'{root}' is not a valid directory.",
|
|
5081
|
+
path=str(root),
|
|
5082
|
+
hint="Pass an existing repository directory.",
|
|
5083
|
+
expected="A directory path.",
|
|
5084
|
+
)
|
|
5085
|
+
raise typer.Exit(1)
|
|
5086
|
+
|
|
5087
|
+
result = rename_class(
|
|
5088
|
+
root,
|
|
5089
|
+
old_name,
|
|
5090
|
+
new_name,
|
|
5091
|
+
dry_run=dry_run,
|
|
5092
|
+
include_tests=not no_tests,
|
|
5093
|
+
)
|
|
5094
|
+
|
|
5095
|
+
if result.errors:
|
|
5096
|
+
_emit_error_json(
|
|
5097
|
+
"RENAME_ERROR",
|
|
5098
|
+
result.errors[0],
|
|
5099
|
+
errors=result.errors,
|
|
5100
|
+
old_name=old_name,
|
|
5101
|
+
new_name=new_name,
|
|
5102
|
+
)
|
|
5103
|
+
raise typer.Exit(1)
|
|
5104
|
+
|
|
5105
|
+
result_dict = result.to_dict()
|
|
5106
|
+
|
|
5107
|
+
if format == "yaml":
|
|
5108
|
+
from sourcecode.serializer import to_yaml as _to_yaml
|
|
5109
|
+
output = _to_yaml(result_dict)
|
|
5110
|
+
else:
|
|
5111
|
+
output = _json.dumps(result_dict, indent=2, ensure_ascii=False)
|
|
5112
|
+
|
|
5113
|
+
if output_path:
|
|
5114
|
+
output_path.write_text(output, encoding="utf-8")
|
|
5115
|
+
action = "dry-run simulated" if dry_run else "applied"
|
|
5116
|
+
typer.echo(
|
|
5117
|
+
f"[rename-class] {action}: {old_name} → {new_name} "
|
|
5118
|
+
f"({result.files_modified} file(s) changed). "
|
|
5119
|
+
f"Audit written to {output_path}",
|
|
5120
|
+
err=True,
|
|
5121
|
+
)
|
|
5122
|
+
else:
|
|
5123
|
+
try:
|
|
5124
|
+
sys.stdout.buffer.write(output.encode("utf-8"))
|
|
5125
|
+
sys.stdout.buffer.write(b"\n")
|
|
5126
|
+
sys.stdout.buffer.flush()
|
|
5127
|
+
except AttributeError:
|
|
5128
|
+
sys.stdout.write(output + "\n")
|
|
5129
|
+
|
|
5130
|
+
if copy:
|
|
5131
|
+
_copy_to_clipboard(output)
|
|
5132
|
+
|
|
5133
|
+
if not dry_run and not output_path:
|
|
5134
|
+
action = "Renamed"
|
|
5135
|
+
typer.echo(
|
|
5136
|
+
f"[rename-class] {action}: {old_name} → {new_name} "
|
|
5137
|
+
f"({result.files_modified} file(s) updated, file renamed to {result.new_file})",
|
|
5138
|
+
err=True,
|
|
5139
|
+
)
|
|
5140
|
+
|
|
5141
|
+
|
|
5142
|
+
# ── chunk-file ────────────────────────────────────────────────────────────────
|
|
5143
|
+
|
|
5144
|
+
@app.command("chunk-file")
|
|
5145
|
+
def chunk_file_cmd(
|
|
5146
|
+
file: Path = typer.Argument(
|
|
5147
|
+
...,
|
|
5148
|
+
help="Java file to chunk (absolute or relative path)",
|
|
5149
|
+
),
|
|
5150
|
+
max_lines: int = typer.Option(
|
|
5151
|
+
500, "--max-lines", "-n",
|
|
5152
|
+
help="Target max lines per chunk (default: 500). Methods > max_lines emit size_warning.",
|
|
5153
|
+
),
|
|
5154
|
+
chunk_id: Optional[int] = typer.Option(
|
|
5155
|
+
None, "--chunk", "-c",
|
|
5156
|
+
help="Return only this chunk by ID (1-based). Omit to return all chunks.",
|
|
5157
|
+
),
|
|
5158
|
+
metadata_only: bool = typer.Option(
|
|
5159
|
+
False, "--metadata-only",
|
|
5160
|
+
help="Return chunk boundaries and metadata without file content.",
|
|
5161
|
+
),
|
|
5162
|
+
output_path: Optional[Path] = typer.Option(
|
|
5163
|
+
None, "--output", "-o",
|
|
5164
|
+
help="Write output to a file instead of stdout.",
|
|
5165
|
+
),
|
|
5166
|
+
format: str = typer.Option(
|
|
5167
|
+
"json", "--format",
|
|
5168
|
+
help="Output format: json (default) or yaml.",
|
|
5169
|
+
),
|
|
5170
|
+
copy: bool = typer.Option(
|
|
5171
|
+
False, "--copy",
|
|
5172
|
+
help="Copy output to clipboard after a successful run.",
|
|
5173
|
+
),
|
|
5174
|
+
) -> None:
|
|
5175
|
+
"""Split a large Java file into semantic chunks for AI agent consumption (BLOCKER-B fix).
|
|
5176
|
+
|
|
5177
|
+
\b
|
|
5178
|
+
Splits a Java file at method/class boundaries so AI agents can read
|
|
5179
|
+
large files (10K–25K+ lines) in context-sized pieces without timeout
|
|
5180
|
+
or fragmented analysis.
|
|
5181
|
+
|
|
5182
|
+
Each chunk includes:
|
|
5183
|
+
- chunk_id, start_line, end_line, chunk_type, symbol name
|
|
5184
|
+
- context_header: package + class + imports summary
|
|
5185
|
+
- content: source lines for that chunk
|
|
5186
|
+
- size_warning: True if chunk > max_lines (cannot split mid-method)
|
|
5187
|
+
|
|
5188
|
+
\b
|
|
5189
|
+
Examples:
|
|
5190
|
+
sourcecode chunk-file NominasCalculoService.java
|
|
5191
|
+
sourcecode chunk-file BigService.java --max-lines 300
|
|
5192
|
+
sourcecode chunk-file BigService.java --chunk 5 # read chunk 5 only
|
|
5193
|
+
sourcecode chunk-file BigService.java --metadata-only # sizes/boundaries only
|
|
5194
|
+
"""
|
|
5195
|
+
import json as _json
|
|
5196
|
+
from sourcecode.file_chunker import chunk_java_file
|
|
5197
|
+
|
|
5198
|
+
abs_file = file.resolve()
|
|
5199
|
+
if not abs_file.is_file():
|
|
5200
|
+
_emit_error_json(
|
|
5201
|
+
INVALID_INPUT_CODE,
|
|
5202
|
+
f"'{abs_file}' is not a valid file.",
|
|
5203
|
+
path=str(abs_file),
|
|
5204
|
+
hint="Pass an existing Java source file.",
|
|
5205
|
+
expected="A .java file path.",
|
|
5206
|
+
)
|
|
5207
|
+
raise typer.Exit(1)
|
|
5208
|
+
|
|
5209
|
+
result = chunk_java_file(abs_file, max_lines=max_lines, include_content=not metadata_only)
|
|
5210
|
+
|
|
5211
|
+
if chunk_id is not None:
|
|
5212
|
+
# Return single chunk
|
|
5213
|
+
matching = [c for c in result.chunks if c.chunk_id == chunk_id]
|
|
5214
|
+
if not matching:
|
|
5215
|
+
_emit_error_json(
|
|
5216
|
+
INVALID_INPUT_CODE,
|
|
5217
|
+
f"Chunk {chunk_id} not found. File has {result.total_chunks} chunks.",
|
|
5218
|
+
chunk_id=chunk_id,
|
|
5219
|
+
total_chunks=result.total_chunks,
|
|
5220
|
+
)
|
|
5221
|
+
raise typer.Exit(1)
|
|
5222
|
+
result_dict = matching[0].to_dict()
|
|
5223
|
+
else:
|
|
5224
|
+
result_dict = result.to_dict()
|
|
5225
|
+
|
|
5226
|
+
if format == "yaml":
|
|
5227
|
+
from sourcecode.serializer import to_yaml as _to_yaml
|
|
5228
|
+
output = _to_yaml(result_dict)
|
|
5229
|
+
else:
|
|
5230
|
+
output = _json.dumps(result_dict, indent=2, ensure_ascii=False)
|
|
5231
|
+
|
|
5232
|
+
if output_path:
|
|
5233
|
+
output_path.write_text(output, encoding="utf-8")
|
|
5234
|
+
typer.echo(
|
|
5235
|
+
f"[chunk-file] {result.total_chunks} chunks written to {output_path}",
|
|
5236
|
+
err=True,
|
|
5237
|
+
)
|
|
5238
|
+
else:
|
|
5239
|
+
try:
|
|
5240
|
+
sys.stdout.buffer.write(output.encode("utf-8"))
|
|
5241
|
+
sys.stdout.buffer.write(b"\n")
|
|
5242
|
+
sys.stdout.buffer.flush()
|
|
5243
|
+
except AttributeError:
|
|
5244
|
+
sys.stdout.write(output + "\n")
|
|
5245
|
+
|
|
5246
|
+
if copy:
|
|
5247
|
+
_copy_to_clipboard(output)
|
|
5248
|
+
|
|
5249
|
+
|
|
5012
5250
|
# ── version ───────────────────────────────────────────────────────────────────
|
|
5013
5251
|
|
|
5014
5252
|
@app.command("activate")
|
|
@@ -5159,10 +5397,13 @@ def cold_start_cmd(
|
|
|
5159
5397
|
result = _gcs(target)
|
|
5160
5398
|
if compact:
|
|
5161
5399
|
# P1-C: cap at ~10K tokens — keep only fields essential for orientation.
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
"validation", "_meta"}
|
|
5400
|
+
# BUG-6 fix: use actual RIS key names (summary/entrypoints, not stacks/entry_points)
|
|
5401
|
+
_cs_keys = {"status", "git_head", "summary", "entrypoints", "endpoints",
|
|
5402
|
+
"project_type", "validation", "_meta"}
|
|
5165
5403
|
result = {k: v for k, v in result.items() if k in _cs_keys}
|
|
5404
|
+
# Truncate endpoints to first 30 to stay within ~10K token budget
|
|
5405
|
+
if isinstance(result.get("endpoints"), list):
|
|
5406
|
+
result["endpoints"] = result["endpoints"][:30]
|
|
5166
5407
|
result["_meta"] = {**(result.get("_meta") or {}), "compact_mode": True,
|
|
5167
5408
|
"full_available": "sourcecode cold-start (without --compact)"}
|
|
5168
5409
|
_out = _json.dumps(result, indent=2, ensure_ascii=False)
|
sourcecode/explain.py
CHANGED
|
@@ -28,6 +28,9 @@ _STEREOTYPE_DESC: dict[str, str] = {
|
|
|
28
28
|
"component": "Spring @Component — general-purpose bean",
|
|
29
29
|
"configuration": "Spring @Configuration — bean factory / config",
|
|
30
30
|
"bean": "Spring @Bean — managed component",
|
|
31
|
+
"entity": "JPA @Entity — persistent domain object mapped to a database table",
|
|
32
|
+
"mappedsuperclass": "JPA @MappedSuperclass — base class sharing persistent state with subclasses",
|
|
33
|
+
"embeddable": "JPA @Embeddable — value object embedded in owning entity table",
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
_SECURITY_ANNOTATION_PREFIXES = (
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
"""file_chunker.py — Semantic chunking of large Java files for AI agent consumption.
|
|
2
|
+
|
|
3
|
+
Splits a Java source file into context-aware chunks at method/class boundaries.
|
|
4
|
+
Each chunk includes a context header so an AI agent can understand it without
|
|
5
|
+
reading prior chunks. Handles files of any size without timeout.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
chunks = chunk_java_file(path, max_lines=500)
|
|
9
|
+
# Each chunk: ChunkRecord with id, type, symbol, start_line, end_line, content
|
|
10
|
+
|
|
11
|
+
Design:
|
|
12
|
+
- Primary split at method/constructor boundaries (brace depth == class depth + 1)
|
|
13
|
+
- Secondary: class-level fields grouped together in a preamble chunk
|
|
14
|
+
- Context header: package + class name + imports summary prepended to each chunk
|
|
15
|
+
- Chunks never split mid-method: a method > max_lines is emitted as a single chunk
|
|
16
|
+
with a size warning in metadata
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import re
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Optional
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# ---------------------------------------------------------------------------
|
|
27
|
+
# Data classes
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class ChunkRecord:
|
|
32
|
+
"""A semantic chunk of a Java source file."""
|
|
33
|
+
chunk_id: int # 1-based sequential index
|
|
34
|
+
chunk_type: str # "class_header" | "method" | "constructor" | "field_block" | "class_footer"
|
|
35
|
+
symbol: str # e.g. "MyService#processOrder" or "MyService" for class_header
|
|
36
|
+
start_line: int # 1-based inclusive
|
|
37
|
+
end_line: int # 1-based inclusive
|
|
38
|
+
content: str # source lines for this chunk
|
|
39
|
+
context_header: str # package + class context prepended for AI consumption
|
|
40
|
+
size_warning: bool = False # True if chunk exceeds max_lines (cannot split further)
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def total_lines(self) -> int:
|
|
44
|
+
return self.end_line - self.start_line + 1
|
|
45
|
+
|
|
46
|
+
def to_dict(self) -> dict:
|
|
47
|
+
return {
|
|
48
|
+
"chunk_id": self.chunk_id,
|
|
49
|
+
"chunk_type": self.chunk_type,
|
|
50
|
+
"symbol": self.symbol,
|
|
51
|
+
"start_line": self.start_line,
|
|
52
|
+
"end_line": self.end_line,
|
|
53
|
+
"total_lines": self.total_lines,
|
|
54
|
+
"context_header": self.context_header,
|
|
55
|
+
"content": self.content,
|
|
56
|
+
"size_warning": self.size_warning,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class ChunkResult:
|
|
62
|
+
"""Result of chunking a Java file."""
|
|
63
|
+
file: str # relative or absolute path
|
|
64
|
+
total_lines: int
|
|
65
|
+
total_chunks: int
|
|
66
|
+
class_name: str
|
|
67
|
+
package: str
|
|
68
|
+
chunk_count_by_type: dict[str, int] = field(default_factory=dict)
|
|
69
|
+
chunks: list[ChunkRecord] = field(default_factory=list)
|
|
70
|
+
limitations: list[str] = field(default_factory=list)
|
|
71
|
+
|
|
72
|
+
def to_dict(self) -> dict:
|
|
73
|
+
return {
|
|
74
|
+
"file": self.file,
|
|
75
|
+
"total_lines": self.total_lines,
|
|
76
|
+
"total_chunks": self.total_chunks,
|
|
77
|
+
"class_name": self.class_name,
|
|
78
|
+
"package": self.package,
|
|
79
|
+
"chunk_count_by_type": self.chunk_count_by_type,
|
|
80
|
+
"limitations": self.limitations,
|
|
81
|
+
"chunks": [c.to_dict() for c in self.chunks],
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# ---------------------------------------------------------------------------
|
|
86
|
+
# Regexes
|
|
87
|
+
# ---------------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
_PKG_RE = re.compile(r'^\s*package\s+([\w.]+)\s*;')
|
|
90
|
+
_IMPORT_RE = re.compile(r'^\s*import\s+(?:static\s+)?([\w.*]+)\s*;')
|
|
91
|
+
_CLASS_RE = re.compile(
|
|
92
|
+
r'^\s*(?:(?:public|protected|private|abstract|final|sealed|non-sealed)\s+)*'
|
|
93
|
+
r'(?:class|interface|enum|@interface)\s+(\w+)'
|
|
94
|
+
)
|
|
95
|
+
_METHOD_RE = re.compile(
|
|
96
|
+
r'^\s*(?:(?:public|protected|private|static|final|synchronized|abstract|native|default|override)\s+)*'
|
|
97
|
+
r'(?:<[^>]+>\s+)?' # optional generic return type
|
|
98
|
+
r'(?:[\w<>\[\],?\s]+\s+)?' # return type (optional for constructors)
|
|
99
|
+
r'(\w+)\s*\(' # method/constructor name + opening paren
|
|
100
|
+
)
|
|
101
|
+
_FIELD_RE = re.compile(
|
|
102
|
+
r'^\s*(?:(?:public|protected|private|static|final|volatile|transient)\s+)+'
|
|
103
|
+
r'[\w<>\[\].,? ]+\s+\w+\s*(?:=|;)'
|
|
104
|
+
)
|
|
105
|
+
_ANN_RE = re.compile(r'^\s*@')
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# ---------------------------------------------------------------------------
|
|
109
|
+
# Internal helpers
|
|
110
|
+
# ---------------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
def _count_braces(line: str) -> tuple[int, int]:
|
|
113
|
+
"""Return (open_count, close_count) for non-string/comment braces in line."""
|
|
114
|
+
open_c = 0
|
|
115
|
+
close_c = 0
|
|
116
|
+
in_str = False
|
|
117
|
+
in_char = False
|
|
118
|
+
escape = False
|
|
119
|
+
i = 0
|
|
120
|
+
while i < len(line):
|
|
121
|
+
ch = line[i]
|
|
122
|
+
if escape:
|
|
123
|
+
escape = False
|
|
124
|
+
elif ch == '\\':
|
|
125
|
+
escape = True
|
|
126
|
+
elif ch == '"' and not in_char:
|
|
127
|
+
in_str = not in_str
|
|
128
|
+
elif ch == "'" and not in_str:
|
|
129
|
+
in_char = not in_char
|
|
130
|
+
elif not in_str and not in_char:
|
|
131
|
+
if ch == '{':
|
|
132
|
+
open_c += 1
|
|
133
|
+
elif ch == '}':
|
|
134
|
+
close_c += 1
|
|
135
|
+
elif ch == '/' and i + 1 < len(line) and line[i+1] == '/':
|
|
136
|
+
break # rest is line comment
|
|
137
|
+
i += 1
|
|
138
|
+
return open_c, close_c
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _build_context_header(
|
|
142
|
+
package: str,
|
|
143
|
+
class_name: str,
|
|
144
|
+
import_lines: list[str],
|
|
145
|
+
max_imports: int = 10,
|
|
146
|
+
) -> str:
|
|
147
|
+
"""Build a context header showing package + class + condensed imports."""
|
|
148
|
+
lines = []
|
|
149
|
+
if package:
|
|
150
|
+
lines.append(f"// File context: package {package};")
|
|
151
|
+
lines.append(f"// Enclosing class: {class_name}")
|
|
152
|
+
if import_lines:
|
|
153
|
+
shown = import_lines[:max_imports]
|
|
154
|
+
lines.extend(shown)
|
|
155
|
+
if len(import_lines) > max_imports:
|
|
156
|
+
lines.append(f"// ... ({len(import_lines) - max_imports} more imports omitted)")
|
|
157
|
+
lines.append("") # blank separator
|
|
158
|
+
return "\n".join(lines)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _is_method_or_constructor_start(
|
|
162
|
+
stripped: str,
|
|
163
|
+
class_name: str,
|
|
164
|
+
depth: int,
|
|
165
|
+
class_depth: int,
|
|
166
|
+
) -> tuple[bool, str, str]:
|
|
167
|
+
"""Return (is_method, method_name, chunk_type) if line starts a method/constructor."""
|
|
168
|
+
if depth != class_depth + 1:
|
|
169
|
+
return False, "", ""
|
|
170
|
+
|
|
171
|
+
# Skip annotations
|
|
172
|
+
if stripped.startswith("@"):
|
|
173
|
+
return False, "", ""
|
|
174
|
+
# Skip field declarations (end with ; or = before ;)
|
|
175
|
+
if _FIELD_RE.match(stripped) and "{" not in stripped:
|
|
176
|
+
return False, "", ""
|
|
177
|
+
# Skip class/interface/enum declarations
|
|
178
|
+
if _CLASS_RE.match(stripped):
|
|
179
|
+
return False, "", ""
|
|
180
|
+
|
|
181
|
+
m = _METHOD_RE.match(stripped)
|
|
182
|
+
if not m:
|
|
183
|
+
return False, "", ""
|
|
184
|
+
name = m.group(1)
|
|
185
|
+
# Distinguish constructor: name matches class name
|
|
186
|
+
chunk_type = "constructor" if name == class_name else "method"
|
|
187
|
+
return True, name, chunk_type
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
# ---------------------------------------------------------------------------
|
|
191
|
+
# Public API
|
|
192
|
+
# ---------------------------------------------------------------------------
|
|
193
|
+
|
|
194
|
+
def chunk_java_file(
|
|
195
|
+
path: Path,
|
|
196
|
+
*,
|
|
197
|
+
max_lines: int = 500,
|
|
198
|
+
include_content: bool = True,
|
|
199
|
+
) -> ChunkResult:
|
|
200
|
+
"""Split a Java file into semantic chunks at method/class boundaries.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
path: Path to the Java file.
|
|
204
|
+
max_lines: Target max lines per chunk. Methods exceeding this
|
|
205
|
+
are emitted as a single chunk with size_warning=True.
|
|
206
|
+
include_content: If False, content field is omitted (metadata-only mode).
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
ChunkResult with ordered list of ChunkRecord entries.
|
|
210
|
+
"""
|
|
211
|
+
try:
|
|
212
|
+
source = path.read_text(encoding="utf-8", errors="replace")
|
|
213
|
+
except OSError as e:
|
|
214
|
+
return ChunkResult(
|
|
215
|
+
file=str(path),
|
|
216
|
+
total_lines=0,
|
|
217
|
+
total_chunks=0,
|
|
218
|
+
class_name="",
|
|
219
|
+
package="",
|
|
220
|
+
limitations=[f"Could not read file: {e}"],
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
all_lines = source.splitlines()
|
|
224
|
+
total_lines = len(all_lines)
|
|
225
|
+
|
|
226
|
+
# ── Pass 1: extract package, class name, imports ──────────────────────
|
|
227
|
+
package = ""
|
|
228
|
+
class_name = ""
|
|
229
|
+
import_lines: list[str] = []
|
|
230
|
+
for raw_line in all_lines:
|
|
231
|
+
stripped = raw_line.strip()
|
|
232
|
+
if not package:
|
|
233
|
+
pm = _PKG_RE.match(raw_line)
|
|
234
|
+
if pm:
|
|
235
|
+
package = pm.group(1)
|
|
236
|
+
im = _IMPORT_RE.match(raw_line)
|
|
237
|
+
if im:
|
|
238
|
+
import_lines.append(raw_line.rstrip())
|
|
239
|
+
if not class_name:
|
|
240
|
+
cm = _CLASS_RE.match(raw_line)
|
|
241
|
+
if cm:
|
|
242
|
+
class_name = cm.group(1)
|
|
243
|
+
|
|
244
|
+
context_header = _build_context_header(package, class_name, import_lines)
|
|
245
|
+
|
|
246
|
+
# ── Pass 2: track brace depth, identify method/class boundaries ───────
|
|
247
|
+
depth = 0
|
|
248
|
+
class_depth = -1 # depth at which the primary class body starts
|
|
249
|
+
chunks: list[ChunkRecord] = []
|
|
250
|
+
chunk_id = 0
|
|
251
|
+
limitations: list[str] = []
|
|
252
|
+
|
|
253
|
+
# "pending" block: lines accumulated for current chunk
|
|
254
|
+
pending_start: int = 1
|
|
255
|
+
pending_lines: list[str] = []
|
|
256
|
+
pending_type: str = "class_header"
|
|
257
|
+
pending_symbol: str = class_name or "unknown"
|
|
258
|
+
|
|
259
|
+
# Annotation buffer for upcoming method/constructor
|
|
260
|
+
ann_buffer: list[tuple[int, str]] = [] # (line_no 1-based, raw_line)
|
|
261
|
+
|
|
262
|
+
def _flush_chunk(end_line: int) -> None:
|
|
263
|
+
nonlocal chunk_id, pending_start, pending_lines, pending_type, pending_symbol
|
|
264
|
+
if not pending_lines:
|
|
265
|
+
return
|
|
266
|
+
chunk_id += 1
|
|
267
|
+
content = "\n".join(pending_lines) if include_content else ""
|
|
268
|
+
size_warn = len(pending_lines) > max_lines
|
|
269
|
+
if size_warn:
|
|
270
|
+
limitations.append(
|
|
271
|
+
f"Chunk {chunk_id} ({pending_symbol}) has {len(pending_lines)} lines "
|
|
272
|
+
f"(exceeds max_lines={max_lines}) — cannot split mid-method."
|
|
273
|
+
)
|
|
274
|
+
chunks.append(ChunkRecord(
|
|
275
|
+
chunk_id=chunk_id,
|
|
276
|
+
chunk_type=pending_type,
|
|
277
|
+
symbol=pending_symbol,
|
|
278
|
+
start_line=pending_start,
|
|
279
|
+
end_line=end_line,
|
|
280
|
+
content=content,
|
|
281
|
+
context_header=context_header,
|
|
282
|
+
size_warning=size_warn,
|
|
283
|
+
))
|
|
284
|
+
pending_lines = []
|
|
285
|
+
pending_start = end_line + 1
|
|
286
|
+
|
|
287
|
+
in_block_comment = False
|
|
288
|
+
current_method_name = ""
|
|
289
|
+
current_method_type = ""
|
|
290
|
+
method_brace_start_depth = -1
|
|
291
|
+
|
|
292
|
+
for line_no_0, raw_line in enumerate(all_lines):
|
|
293
|
+
line_no = line_no_0 + 1 # 1-based
|
|
294
|
+
stripped = raw_line.strip()
|
|
295
|
+
|
|
296
|
+
# Block comment tracking
|
|
297
|
+
if in_block_comment:
|
|
298
|
+
pending_lines.append(raw_line)
|
|
299
|
+
if "*/" in stripped:
|
|
300
|
+
in_block_comment = False
|
|
301
|
+
continue
|
|
302
|
+
if "/*" in stripped and "*/" not in stripped:
|
|
303
|
+
in_block_comment = True
|
|
304
|
+
pending_lines.append(raw_line)
|
|
305
|
+
continue
|
|
306
|
+
|
|
307
|
+
# Track brace depth
|
|
308
|
+
opens, closes = _count_braces(raw_line)
|
|
309
|
+
|
|
310
|
+
# Detect class body start (first '{' after class declaration)
|
|
311
|
+
if class_depth < 0 and class_name and _CLASS_RE.match(raw_line):
|
|
312
|
+
class_depth = depth # depth BEFORE the '{' on this line
|
|
313
|
+
if opens > 0:
|
|
314
|
+
class_depth = depth # class body starts after this line
|
|
315
|
+
|
|
316
|
+
# Check if this line starts a method/constructor AT class_depth+1
|
|
317
|
+
if class_depth >= 0 and depth == class_depth + 1 and not current_method_name:
|
|
318
|
+
is_method, mname, mtype = _is_method_or_constructor_start(
|
|
319
|
+
stripped, class_name, depth, class_depth
|
|
320
|
+
)
|
|
321
|
+
if is_method and "{" in raw_line:
|
|
322
|
+
# Flush anything accumulated as field_block / class_header
|
|
323
|
+
# Include annotation lines in the new method chunk
|
|
324
|
+
if pending_lines:
|
|
325
|
+
# Check if last N pending lines are annotations for this method
|
|
326
|
+
# Flush everything up to ann_buffer start
|
|
327
|
+
if ann_buffer:
|
|
328
|
+
ann_start_line = ann_buffer[0][0]
|
|
329
|
+
pre_ann_lines = pending_lines[:ann_start_line - pending_start]
|
|
330
|
+
if pre_ann_lines:
|
|
331
|
+
_flush_chunk(ann_start_line - 1)
|
|
332
|
+
# Move ann_buffer lines into the new method chunk
|
|
333
|
+
pending_start = ann_start_line
|
|
334
|
+
pending_lines = [al for _, al in ann_buffer]
|
|
335
|
+
ann_buffer = []
|
|
336
|
+
else:
|
|
337
|
+
_flush_chunk(line_no - 1)
|
|
338
|
+
pending_start = line_no
|
|
339
|
+
pending_lines = []
|
|
340
|
+
|
|
341
|
+
current_method_name = mname
|
|
342
|
+
current_method_type = mtype
|
|
343
|
+
method_brace_start_depth = depth + opens - 1 # depth entering method body
|
|
344
|
+
pending_type = mtype
|
|
345
|
+
pending_symbol = f"{class_name}#{mname}" if class_name else mname
|
|
346
|
+
pending_lines.append(raw_line)
|
|
347
|
+
depth += opens - closes
|
|
348
|
+
ann_buffer = []
|
|
349
|
+
continue
|
|
350
|
+
|
|
351
|
+
# Update depth
|
|
352
|
+
depth += opens - closes
|
|
353
|
+
|
|
354
|
+
# After depth update: check if current method closed
|
|
355
|
+
if current_method_name and depth <= class_depth + 1:
|
|
356
|
+
# Method body closed
|
|
357
|
+
pending_lines.append(raw_line)
|
|
358
|
+
_flush_chunk(line_no)
|
|
359
|
+
current_method_name = ""
|
|
360
|
+
current_method_type = ""
|
|
361
|
+
# Next chunk is field_block until next method
|
|
362
|
+
pending_type = "field_block"
|
|
363
|
+
pending_symbol = class_name or "unknown"
|
|
364
|
+
pending_start = line_no + 1
|
|
365
|
+
ann_buffer = []
|
|
366
|
+
continue
|
|
367
|
+
|
|
368
|
+
# Track annotations at class level (buffered to attach to next method)
|
|
369
|
+
if (class_depth >= 0 and depth == class_depth + 1
|
|
370
|
+
and not current_method_name and stripped.startswith("@")):
|
|
371
|
+
ann_buffer.append((line_no, raw_line))
|
|
372
|
+
elif not stripped.startswith("@"):
|
|
373
|
+
# Non-annotation line: clear annotation buffer if we're not entering a method
|
|
374
|
+
if ann_buffer and not (class_depth >= 0 and depth == class_depth + 1):
|
|
375
|
+
ann_buffer = []
|
|
376
|
+
|
|
377
|
+
pending_lines.append(raw_line)
|
|
378
|
+
|
|
379
|
+
# Flush remaining lines as class_footer
|
|
380
|
+
if pending_lines:
|
|
381
|
+
pending_type = "class_footer" if depth <= (class_depth if class_depth >= 0 else 0) else pending_type
|
|
382
|
+
_flush_chunk(total_lines)
|
|
383
|
+
|
|
384
|
+
chunk_count_by_type: dict[str, int] = {}
|
|
385
|
+
for c in chunks:
|
|
386
|
+
chunk_count_by_type[c.chunk_type] = chunk_count_by_type.get(c.chunk_type, 0) + 1
|
|
387
|
+
|
|
388
|
+
return ChunkResult(
|
|
389
|
+
file=str(path),
|
|
390
|
+
total_lines=total_lines,
|
|
391
|
+
total_chunks=len(chunks),
|
|
392
|
+
class_name=class_name,
|
|
393
|
+
package=package,
|
|
394
|
+
chunk_count_by_type=chunk_count_by_type,
|
|
395
|
+
chunks=chunks,
|
|
396
|
+
limitations=limitations,
|
|
397
|
+
)
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
"""rename_refactor.py — Safe Java class rename with full reference update.
|
|
2
|
+
|
|
3
|
+
Performs a deterministic rename of a Java class/interface/enum:
|
|
4
|
+
1. Locates the source file (OldName.java or by scanning class declarations)
|
|
5
|
+
2. Updates class declaration, constructor name, all imports, all references
|
|
6
|
+
3. Renames the physical file on disk
|
|
7
|
+
4. Returns a structured ChangeAudit report (BLOCKER-C)
|
|
8
|
+
|
|
9
|
+
Covers:
|
|
10
|
+
- Class/interface/enum declaration
|
|
11
|
+
- Constructor declarations
|
|
12
|
+
- Import statements
|
|
13
|
+
- Field type declarations
|
|
14
|
+
- Method parameter and return types
|
|
15
|
+
- Variable declarations and instantiations
|
|
16
|
+
- extends / implements
|
|
17
|
+
- Generic type parameters
|
|
18
|
+
- Spring @Qualifier and @Bean names (camelCase)
|
|
19
|
+
- Test files (optional via include_tests)
|
|
20
|
+
|
|
21
|
+
Does NOT require compilation. Works on any Java source tree via
|
|
22
|
+
regex-based text transformations with word-boundary guards.
|
|
23
|
+
"""
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import difflib
|
|
27
|
+
import re
|
|
28
|
+
from dataclasses import dataclass, field
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
from typing import Optional
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
# Data classes (BLOCKER-C: structured change audit trail)
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class FileChange:
|
|
39
|
+
"""A mutation applied to a single file."""
|
|
40
|
+
file: str # relative path from repo root
|
|
41
|
+
intent: str # human-readable description of what changed
|
|
42
|
+
diff: str # unified diff (--- before / +++ after)
|
|
43
|
+
before_lines: list[str] = field(default_factory=list)
|
|
44
|
+
after_lines: list[str] = field(default_factory=list)
|
|
45
|
+
|
|
46
|
+
def to_dict(self) -> dict:
|
|
47
|
+
return {
|
|
48
|
+
"file": self.file,
|
|
49
|
+
"intent": self.intent,
|
|
50
|
+
"diff": self.diff,
|
|
51
|
+
"before_lines": self.before_lines,
|
|
52
|
+
"after_lines": self.after_lines,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class RenameResult:
|
|
58
|
+
"""Full result of a rename-class operation."""
|
|
59
|
+
old_name: str
|
|
60
|
+
new_name: str
|
|
61
|
+
old_file: str # relative path before rename (empty if not found)
|
|
62
|
+
new_file: str # relative path after rename (empty if not found)
|
|
63
|
+
changes: list[FileChange] = field(default_factory=list)
|
|
64
|
+
files_scanned: int = 0
|
|
65
|
+
files_modified: int = 0
|
|
66
|
+
dry_run: bool = False
|
|
67
|
+
errors: list[str] = field(default_factory=list)
|
|
68
|
+
|
|
69
|
+
def to_dict(self) -> dict:
|
|
70
|
+
return {
|
|
71
|
+
"old_name": self.old_name,
|
|
72
|
+
"new_name": self.new_name,
|
|
73
|
+
"old_file": self.old_file,
|
|
74
|
+
"new_file": self.new_file,
|
|
75
|
+
"files_scanned": self.files_scanned,
|
|
76
|
+
"files_modified": self.files_modified,
|
|
77
|
+
"dry_run": self.dry_run,
|
|
78
|
+
"errors": self.errors,
|
|
79
|
+
"changes": [c.to_dict() for c in self.changes],
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# ---------------------------------------------------------------------------
|
|
84
|
+
# Core rename logic
|
|
85
|
+
# ---------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
_VENDOR_DIRS = frozenset({
|
|
88
|
+
"vendor", "node_modules", "dist", "target", "build",
|
|
89
|
+
".gradle", ".mvn", "generated", "generated-sources",
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _to_camel(name: str) -> str:
|
|
94
|
+
"""PascalCase → camelCase: ServiceA → serviceA."""
|
|
95
|
+
if not name or len(name) < 2:
|
|
96
|
+
return name.lower() if name else name
|
|
97
|
+
return name[0].lower() + name[1:]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _collect_java_files(root: Path, *, include_tests: bool = True) -> list[Path]:
|
|
101
|
+
"""All .java files under root, excluding vendor/build dirs."""
|
|
102
|
+
results: list[Path] = []
|
|
103
|
+
for p in sorted(root.rglob("*.java")):
|
|
104
|
+
rel = str(p.relative_to(root)).replace("\\", "/")
|
|
105
|
+
parts = rel.split("/")
|
|
106
|
+
if any(part in _VENDOR_DIRS for part in parts[:-1]):
|
|
107
|
+
continue
|
|
108
|
+
if not include_tests:
|
|
109
|
+
if (
|
|
110
|
+
"/src/test/" in rel or rel.startswith("src/test/")
|
|
111
|
+
or "/src/tests/" in rel or rel.startswith("src/tests/")
|
|
112
|
+
or rel.startswith("test/") or rel.startswith("tests/")
|
|
113
|
+
):
|
|
114
|
+
continue
|
|
115
|
+
results.append(p)
|
|
116
|
+
return results
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _find_class_file(
|
|
120
|
+
java_files: list[Path],
|
|
121
|
+
class_name: str,
|
|
122
|
+
root: Path,
|
|
123
|
+
) -> Optional[Path]:
|
|
124
|
+
"""Find the file that declares `class_name` (by filename first, then scan)."""
|
|
125
|
+
# Prefer exact filename match
|
|
126
|
+
candidates = [f for f in java_files if f.stem == class_name]
|
|
127
|
+
if len(candidates) == 1:
|
|
128
|
+
return candidates[0]
|
|
129
|
+
if len(candidates) > 1:
|
|
130
|
+
# Multiple files with same stem — pick the one that has the class declaration
|
|
131
|
+
decl_re = re.compile(
|
|
132
|
+
r'\b(?:public\s+)?(?:abstract\s+)?(?:class|interface|enum)\s+' + re.escape(class_name) + r'\b'
|
|
133
|
+
)
|
|
134
|
+
for c in candidates:
|
|
135
|
+
try:
|
|
136
|
+
if decl_re.search(c.read_text(encoding="utf-8", errors="replace")):
|
|
137
|
+
return c
|
|
138
|
+
except OSError:
|
|
139
|
+
continue
|
|
140
|
+
return candidates[0]
|
|
141
|
+
|
|
142
|
+
# Fallback: scan file contents for class declaration
|
|
143
|
+
decl_re = re.compile(
|
|
144
|
+
r'\b(?:public\s+)?(?:abstract\s+)?(?:class|interface|enum)\s+' + re.escape(class_name) + r'\b'
|
|
145
|
+
)
|
|
146
|
+
for f in java_files:
|
|
147
|
+
try:
|
|
148
|
+
if decl_re.search(f.read_text(encoding="utf-8", errors="replace")):
|
|
149
|
+
return f
|
|
150
|
+
except OSError:
|
|
151
|
+
continue
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _apply_rename(source: str, old_name: str, new_name: str) -> str:
|
|
156
|
+
"""Apply word-boundary replacement for class name (PascalCase and camelCase forms)."""
|
|
157
|
+
result = re.sub(r'\b' + re.escape(old_name) + r'\b', new_name, source)
|
|
158
|
+
|
|
159
|
+
old_camel = _to_camel(old_name)
|
|
160
|
+
new_camel = _to_camel(new_name)
|
|
161
|
+
if old_camel != old_name and old_camel in result:
|
|
162
|
+
result = re.sub(r'\b' + re.escape(old_camel) + r'\b', new_camel, result)
|
|
163
|
+
|
|
164
|
+
return result
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# Matches a class/interface/enum/record declaration of a given name
|
|
168
|
+
_CLASS_DECL_RE_TMPL = r'\b(?:class|interface|enum|record)\s+{name}\b'
|
|
169
|
+
# Matches a constructor declaration: optional access modifier + ClassName + (
|
|
170
|
+
_CTOR_DECL_RE_TMPL = r'^\s*(?:(?:public|protected|private)\s+)?' + r'{name}\s*\('
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _apply_rename_refs_only(source: str, old_name: str, new_name: str) -> str:
|
|
174
|
+
"""Rename old_name→new_name in a non-source file (import/type references only).
|
|
175
|
+
|
|
176
|
+
Skips lines containing a class/interface/enum/record declaration or constructor
|
|
177
|
+
declaration of old_name, so that a class sharing the simple name in another
|
|
178
|
+
package is not corrupted.
|
|
179
|
+
"""
|
|
180
|
+
class_decl_re = re.compile(_CLASS_DECL_RE_TMPL.format(name=re.escape(old_name)))
|
|
181
|
+
ctor_decl_re = re.compile(_CTOR_DECL_RE_TMPL.format(name=re.escape(old_name)))
|
|
182
|
+
ref_re = re.compile(r'\b' + re.escape(old_name) + r'\b')
|
|
183
|
+
|
|
184
|
+
lines = source.splitlines(keepends=True)
|
|
185
|
+
result_lines = []
|
|
186
|
+
for line in lines:
|
|
187
|
+
if class_decl_re.search(line) or ctor_decl_re.search(line):
|
|
188
|
+
result_lines.append(line)
|
|
189
|
+
else:
|
|
190
|
+
result_lines.append(ref_re.sub(new_name, line))
|
|
191
|
+
result = ''.join(result_lines)
|
|
192
|
+
|
|
193
|
+
old_camel = _to_camel(old_name)
|
|
194
|
+
new_camel = _to_camel(new_name)
|
|
195
|
+
if old_camel != old_name and old_camel in result:
|
|
196
|
+
result = re.sub(r'\b' + re.escape(old_camel) + r'\b', new_camel, result)
|
|
197
|
+
|
|
198
|
+
return result
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _make_diff(old_text: str, new_text: str, rel_path: str) -> str:
|
|
202
|
+
"""Produce a unified diff string."""
|
|
203
|
+
old_lines = old_text.splitlines(keepends=True)
|
|
204
|
+
new_lines = new_text.splitlines(keepends=True)
|
|
205
|
+
diff_lines = list(difflib.unified_diff(
|
|
206
|
+
old_lines,
|
|
207
|
+
new_lines,
|
|
208
|
+
fromfile=f"a/{rel_path}",
|
|
209
|
+
tofile=f"b/{rel_path}",
|
|
210
|
+
lineterm="",
|
|
211
|
+
))
|
|
212
|
+
return "".join(diff_lines)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
# ---------------------------------------------------------------------------
|
|
216
|
+
# Public API
|
|
217
|
+
# ---------------------------------------------------------------------------
|
|
218
|
+
|
|
219
|
+
def rename_class(
|
|
220
|
+
root: Path,
|
|
221
|
+
old_name: str,
|
|
222
|
+
new_name: str,
|
|
223
|
+
*,
|
|
224
|
+
dry_run: bool = False,
|
|
225
|
+
include_tests: bool = True,
|
|
226
|
+
) -> RenameResult:
|
|
227
|
+
"""Rename a Java class throughout the repository.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
root: Absolute repo root directory.
|
|
231
|
+
old_name: Simple class name to rename (e.g. "ServiceA").
|
|
232
|
+
new_name: Target simple class name (e.g. "ServiceB").
|
|
233
|
+
dry_run: If True, compute changes but do not write any files.
|
|
234
|
+
include_tests: If True (default), also rename in test files.
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
RenameResult with structured change audit trail (BLOCKER-C format).
|
|
238
|
+
"""
|
|
239
|
+
root = root.resolve()
|
|
240
|
+
|
|
241
|
+
result = RenameResult(
|
|
242
|
+
old_name=old_name,
|
|
243
|
+
new_name=new_name,
|
|
244
|
+
old_file="",
|
|
245
|
+
new_file="",
|
|
246
|
+
dry_run=dry_run,
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
# Validate input
|
|
250
|
+
if not old_name or not old_name[0].isupper():
|
|
251
|
+
result.errors.append(
|
|
252
|
+
f"old_name '{old_name}' must be a Java class name (PascalCase, non-empty)."
|
|
253
|
+
)
|
|
254
|
+
return result
|
|
255
|
+
if not new_name or not new_name[0].isupper():
|
|
256
|
+
result.errors.append(
|
|
257
|
+
f"new_name '{new_name}' must be a Java class name (PascalCase, non-empty)."
|
|
258
|
+
)
|
|
259
|
+
return result
|
|
260
|
+
if old_name == new_name:
|
|
261
|
+
result.errors.append("old_name and new_name are identical — nothing to rename.")
|
|
262
|
+
return result
|
|
263
|
+
if not root.is_dir():
|
|
264
|
+
result.errors.append(f"Root directory '{root}' does not exist.")
|
|
265
|
+
return result
|
|
266
|
+
|
|
267
|
+
# Collect files
|
|
268
|
+
java_files = _collect_java_files(root, include_tests=include_tests)
|
|
269
|
+
result.files_scanned = len(java_files)
|
|
270
|
+
|
|
271
|
+
# Locate the source file
|
|
272
|
+
source_file = _find_class_file(java_files, old_name, root)
|
|
273
|
+
if source_file is None:
|
|
274
|
+
result.errors.append(
|
|
275
|
+
f"Could not find a file declaring class '{old_name}' under '{root}'."
|
|
276
|
+
)
|
|
277
|
+
return result
|
|
278
|
+
|
|
279
|
+
# Determine new file path (same directory, new filename)
|
|
280
|
+
new_file_path = source_file.with_name(new_name + ".java")
|
|
281
|
+
result.old_file = str(source_file.relative_to(root)).replace("\\", "/")
|
|
282
|
+
result.new_file = str(new_file_path.relative_to(root)).replace("\\", "/")
|
|
283
|
+
|
|
284
|
+
# BUG-2: check for collision anywhere in the repo, not just same directory
|
|
285
|
+
collision = next(
|
|
286
|
+
(f for f in java_files if f.stem == new_name and f.resolve() != new_file_path.resolve()),
|
|
287
|
+
None,
|
|
288
|
+
)
|
|
289
|
+
if collision is not None:
|
|
290
|
+
collision_rel = str(collision.relative_to(root)).replace("\\", "/")
|
|
291
|
+
result.errors.append(
|
|
292
|
+
f"'{new_name}' already exists at '{collision_rel}' — "
|
|
293
|
+
f"rename would create a duplicate class name. Pass --force to override."
|
|
294
|
+
)
|
|
295
|
+
return result
|
|
296
|
+
|
|
297
|
+
if new_file_path.exists() and new_file_path != source_file:
|
|
298
|
+
result.errors.append(
|
|
299
|
+
f"Target file '{result.new_file}' already exists — aborting to avoid overwrite."
|
|
300
|
+
)
|
|
301
|
+
return result
|
|
302
|
+
|
|
303
|
+
# Apply text replacements to all Java files
|
|
304
|
+
changes: list[FileChange] = []
|
|
305
|
+
for java_file in java_files:
|
|
306
|
+
try:
|
|
307
|
+
old_text = java_file.read_text(encoding="utf-8", errors="replace")
|
|
308
|
+
except OSError as e:
|
|
309
|
+
result.errors.append(f"Could not read '{java_file}': {e}")
|
|
310
|
+
continue
|
|
311
|
+
|
|
312
|
+
is_source = java_file == source_file
|
|
313
|
+
if is_source:
|
|
314
|
+
new_text = _apply_rename(old_text, old_name, new_name)
|
|
315
|
+
else:
|
|
316
|
+
# BUG-4: use refs-only variant to avoid clobbering same-named class in other package
|
|
317
|
+
new_text = _apply_rename_refs_only(old_text, old_name, new_name)
|
|
318
|
+
if new_text == old_text:
|
|
319
|
+
continue
|
|
320
|
+
|
|
321
|
+
rel_path = str(java_file.relative_to(root)).replace("\\", "/")
|
|
322
|
+
diff = _make_diff(old_text, new_text, rel_path)
|
|
323
|
+
|
|
324
|
+
if is_source:
|
|
325
|
+
intent = f"Renamed class declaration: {old_name} → {new_name}"
|
|
326
|
+
else:
|
|
327
|
+
intent = f"Updated references to {old_name} → {new_name}"
|
|
328
|
+
|
|
329
|
+
changes.append(FileChange(
|
|
330
|
+
file=rel_path,
|
|
331
|
+
intent=intent,
|
|
332
|
+
diff=diff,
|
|
333
|
+
before_lines=old_text.splitlines(),
|
|
334
|
+
after_lines=new_text.splitlines(),
|
|
335
|
+
))
|
|
336
|
+
|
|
337
|
+
if not dry_run:
|
|
338
|
+
java_file.write_text(new_text, encoding="utf-8")
|
|
339
|
+
|
|
340
|
+
result.changes = changes
|
|
341
|
+
result.files_modified = len(changes)
|
|
342
|
+
|
|
343
|
+
# Rename the physical file (BLOCKER-A core fix)
|
|
344
|
+
if not dry_run and source_file.exists():
|
|
345
|
+
source_file.rename(new_file_path)
|
|
346
|
+
elif dry_run:
|
|
347
|
+
# In dry_run mode, add a synthetic change record for the file rename itself
|
|
348
|
+
# if no text change was found in the source file (e.g. only filename changes).
|
|
349
|
+
pass
|
|
350
|
+
|
|
351
|
+
return result
|
sourcecode/repository_ir.py
CHANGED
|
@@ -202,8 +202,9 @@ _SECURITY_MARKER_ANNOTATIONS: frozenset[str] = frozenset({
|
|
|
202
202
|
# is expected and does NOT mean endpoints are unprotected.
|
|
203
203
|
_FILTER_SECURITY_ANNOTATIONS: frozenset[str] = frozenset({
|
|
204
204
|
"@EnableWebSecurity",
|
|
205
|
-
|
|
206
|
-
|
|
205
|
+
# @EnableMethodSecurity / @EnableGlobalMethodSecurity enable per-method annotation
|
|
206
|
+
# security (@PreAuthorize/@Secured), NOT a filter chain — must NOT be treated as
|
|
207
|
+
# filter_based or SEC-001 is suppressed for every unannotated endpoint.
|
|
207
208
|
})
|
|
208
209
|
|
|
209
210
|
# Programmatic security: method-call patterns that indicate runtime auth enforcement.
|
|
@@ -2819,6 +2820,11 @@ def build_repo_ir(
|
|
|
2819
2820
|
if since:
|
|
2820
2821
|
_since_changed = _get_git_changed_files(root, since)
|
|
2821
2822
|
|
|
2823
|
+
# L-6: analysis_meta tracking (files_read, lines_read, symbols_analyzed, token_estimate)
|
|
2824
|
+
_meta_files_read = 0
|
|
2825
|
+
_meta_lines_read = 0
|
|
2826
|
+
_meta_chars_read = 0
|
|
2827
|
+
|
|
2822
2828
|
# Pass 1: extract symbols from all files so we can build the same-package
|
|
2823
2829
|
# type map before building relations. Java classes in the same package
|
|
2824
2830
|
# reference each other without import statements, so import_map alone cannot
|
|
@@ -2830,6 +2836,9 @@ def build_repo_ir(
|
|
|
2830
2836
|
source = abs_path.read_text(encoding="utf-8", errors="replace")
|
|
2831
2837
|
except OSError:
|
|
2832
2838
|
continue
|
|
2839
|
+
_meta_files_read += 1
|
|
2840
|
+
_meta_lines_read += source.count("\n") + (1 if source and not source.endswith("\n") else 0)
|
|
2841
|
+
_meta_chars_read += len(source)
|
|
2833
2842
|
package, symbols, raw_imports = _extract_symbols(source, rel_path)
|
|
2834
2843
|
all_symbols.extend(symbols)
|
|
2835
2844
|
_per_file.append((rel_path, source, package, raw_imports, symbols))
|
|
@@ -2883,7 +2892,58 @@ def build_repo_ir(
|
|
|
2883
2892
|
route_diffs_arg: Optional[list[dict]] = (
|
|
2884
2893
|
sorted(all_route_diffs, key=lambda d: d["symbol"]) if since else None
|
|
2885
2894
|
)
|
|
2886
|
-
|
|
2895
|
+
ir = _assemble(all_symbols, unique_relations, all_changed, spring_summary, route_diffs_arg)
|
|
2896
|
+
|
|
2897
|
+
# BUG-7: XML Spring Security detection for the canonical CIR pipeline.
|
|
2898
|
+
# _assemble only sees Java symbols — XML config is invisible to it.
|
|
2899
|
+
# Scan here (where root is available) and retag route_surface entries so
|
|
2900
|
+
# build_canonical_ir produces correct CanonicalEndpoint.security values.
|
|
2901
|
+
_xml_sec_re = re.compile(
|
|
2902
|
+
r'(?:xmlns(?::[a-z]+)?="http://www\.springframework\.org/schema/security"'
|
|
2903
|
+
r'|<security:http\b'
|
|
2904
|
+
r'|<http\s[^>]*use-expressions'
|
|
2905
|
+
r'|spring-security-[2345]'
|
|
2906
|
+
r'|xmlns:security="http://www\.springframework\.org/schema/security")',
|
|
2907
|
+
re.IGNORECASE,
|
|
2908
|
+
)
|
|
2909
|
+
_xml_sec_detected = False
|
|
2910
|
+
for _xml_glob in (
|
|
2911
|
+
"*security*.xml", "*Security*.xml",
|
|
2912
|
+
"*applicationContext*.xml", "*-context.xml", "*Context.xml",
|
|
2913
|
+
"*spring*.xml", "*Spring*.xml",
|
|
2914
|
+
):
|
|
2915
|
+
for _xf in root.rglob(_xml_glob):
|
|
2916
|
+
if "target/" in str(_xf).replace("\\", "/"):
|
|
2917
|
+
continue
|
|
2918
|
+
try:
|
|
2919
|
+
_xt = _xf.read_text(encoding="utf-8", errors="replace")
|
|
2920
|
+
except OSError:
|
|
2921
|
+
continue
|
|
2922
|
+
if _xml_sec_re.search(_xt):
|
|
2923
|
+
_xml_sec_detected = True
|
|
2924
|
+
break
|
|
2925
|
+
if _xml_sec_detected:
|
|
2926
|
+
break
|
|
2927
|
+
if _xml_sec_detected:
|
|
2928
|
+
_sec_model = ir.get("security_model", "unknown")
|
|
2929
|
+
if _sec_model == "unknown":
|
|
2930
|
+
ir["security_model"] = "xml_or_filter_chain"
|
|
2931
|
+
elif _sec_model in ("annotation_based", "mixed"):
|
|
2932
|
+
ir["security_model"] = "mixed"
|
|
2933
|
+
# Retag route_surface entries that have no security (would become none_detected in CIR)
|
|
2934
|
+
for _r in ir.get("route_surface") or []:
|
|
2935
|
+
_r_sec = _r.get("security_annotations")
|
|
2936
|
+
if _r_sec is None or (isinstance(_r_sec, dict) and _r_sec.get("policy") == "none_detected"):
|
|
2937
|
+
_r["security_annotations"] = {"policy": "xml_or_filter_chain"}
|
|
2938
|
+
|
|
2939
|
+
# L-6: inject analysis_meta — files_read, lines_read, symbols_analyzed, token_estimate
|
|
2940
|
+
ir["analysis_meta"] = {
|
|
2941
|
+
"files_read": _meta_files_read,
|
|
2942
|
+
"lines_read": _meta_lines_read,
|
|
2943
|
+
"symbols_analyzed": len(all_symbols),
|
|
2944
|
+
"token_estimate": _meta_chars_read // 4, # 4 chars ≈ 1 token (rough approximation)
|
|
2945
|
+
}
|
|
2946
|
+
return ir
|
|
2887
2947
|
|
|
2888
2948
|
|
|
2889
2949
|
# ---------------------------------------------------------------------------
|
|
@@ -3341,13 +3401,18 @@ def extract_java_endpoints(root: Path) -> "dict[str, Any]":
|
|
|
3341
3401
|
if _xml_security_detected:
|
|
3342
3402
|
break
|
|
3343
3403
|
|
|
3344
|
-
if _xml_security_detected
|
|
3345
|
-
|
|
3346
|
-
#
|
|
3347
|
-
#
|
|
3404
|
+
if _xml_security_detected:
|
|
3405
|
+
# Re-tag per-endpoint none_detected → xml_or_filter_chain regardless of security_model.
|
|
3406
|
+
# BUG-7 fix: previously only ran when model == "unknown", causing false-positive SEC-001
|
|
3407
|
+
# when annotation security (@PreAuthorize) coexisted with XML security config.
|
|
3348
3408
|
for ep in endpoints:
|
|
3349
3409
|
if ep.get("security", {}).get("policy") == "none_detected":
|
|
3350
3410
|
ep["security"] = {"policy": "xml_or_filter_chain"}
|
|
3411
|
+
if security_model == "unknown":
|
|
3412
|
+
security_model = "xml_or_filter_chain"
|
|
3413
|
+
elif security_model in ("annotation_based", "mixed"):
|
|
3414
|
+
security_model = "mixed"
|
|
3415
|
+
# filter_based stays filter_based — XML + filter chain is still filter_based
|
|
3351
3416
|
# Recompute no_security_signal (now counts only truly unknown endpoints)
|
|
3352
3417
|
no_security_signal = sum(
|
|
3353
3418
|
1 for e in endpoints
|
|
@@ -3378,7 +3443,11 @@ def find_java_files(root: Path, *, max_files: int = 8000, limitations: list[str]
|
|
|
3378
3443
|
continue
|
|
3379
3444
|
parts = rel.split("/")
|
|
3380
3445
|
# Skip test dirs
|
|
3381
|
-
if
|
|
3446
|
+
if (
|
|
3447
|
+
"/src/test/" in rel or rel.startswith("src/test/")
|
|
3448
|
+
or "/src/tests/" in rel or rel.startswith("src/tests/")
|
|
3449
|
+
or rel.startswith("test/") or rel.startswith("tests/")
|
|
3450
|
+
):
|
|
3382
3451
|
continue
|
|
3383
3452
|
# Skip vendor/generated/build dirs
|
|
3384
3453
|
if any(part in _VENDOR_DIRS for part in parts[:-1]):
|
sourcecode/spring_model.py
CHANGED
|
@@ -41,6 +41,8 @@ _CALL_SKIP: frozenset[str] = frozenset({"annotated_with", "mapped_to", "containe
|
|
|
41
41
|
_BEAN_ANNOTATIONS: frozenset[str] = frozenset({
|
|
42
42
|
"@Component", "@Service", "@Repository",
|
|
43
43
|
"@Controller", "@RestController", "@Configuration", "@Bean",
|
|
44
|
+
# JPA persistence annotations — not Spring beans but need stereotype recognition in explain
|
|
45
|
+
"@Entity", "@MappedSuperclass", "@Embeddable",
|
|
44
46
|
})
|
|
45
47
|
|
|
46
48
|
_GENERIC_PARAM_RE = re.compile(r"<[A-Z][\w,\s<>?]*>")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sourcecode
|
|
3
|
-
Version: 1.35.
|
|
3
|
+
Version: 1.35.28
|
|
4
4
|
Summary: Persistent structural context and ultra-fast repeated analysis for AI coding agents
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
Keywords: agents,ai,codebase,context,developer-tools,llm
|
|
@@ -40,7 +40,7 @@ Description-Content-Type: text/markdown
|
|
|
40
40
|
|
|
41
41
|
**Persistent structural context and ultra-fast repeated analysis for AI coding agents.**
|
|
42
42
|
|
|
43
|
-

|
|
44
44
|

|
|
45
45
|
|
|
46
46
|
---
|
|
@@ -114,7 +114,9 @@ pipx install sourcecode
|
|
|
114
114
|
|
|
115
115
|
```bash
|
|
116
116
|
sourcecode version
|
|
117
|
-
# sourcecode 1.35.
|
|
117
|
+
# sourcecode 1.35.28
|
|
118
|
+
|
|
119
|
+
**v1.35.28** — 7 bug fixes: `rename-class` cross-package disambiguation (BUG-4), `rename-class` collision detection (BUG-2), `find_java_files` false positive on `com/test/` package paths (BUG-1), `cold-start --compact` correct key names (BUG-6), `@EnableMethodSecurity` no longer suppresses SEC-001 (BUG-3), `explain` @Entity stereotype detection (BUG-5), XML+annotation mixed security retagging (BUG-7).
|
|
118
120
|
```
|
|
119
121
|
|
|
120
122
|
---
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
sourcecode/__init__.py,sha256=
|
|
1
|
+
sourcecode/__init__.py,sha256=6hJmVTMbA3xWMi_K8kDZKhf82Qh1AoNAv1zZtaf2IEg,104
|
|
2
2
|
sourcecode/adaptive_scanner.py,sha256=XffluXKzJUXrMtjEiAOnSNPZnztdIcts17T9ouHeID0,10521
|
|
3
3
|
sourcecode/architecture_analyzer.py,sha256=qh749a7ykPtGmQI1MR9y6j8TtL_jBdVYFx9YRsLqOMw,44121
|
|
4
4
|
sourcecode/architecture_summary.py,sha256=z34_6v7cSwy98cof2UVciGho7SCrZ93tiqMmq5WNzRQ,20405
|
|
@@ -7,7 +7,7 @@ sourcecode/cache.py,sha256=wAyPrXN5DqiGivnMpeEuun2xHDKfBer2_oBsh6kj_vc,30447
|
|
|
7
7
|
sourcecode/canonical_ir.py,sha256=2vTLc6wL1cH3NNbEcdZpfX5okh8h5dKq7xd0m0rv_Ro,24167
|
|
8
8
|
sourcecode/cir_graphs.py,sha256=rZi8JV4ZrAa2WSCeyNa4JIEKQ_yZzDZTsrvVz2KfuKA,8919
|
|
9
9
|
sourcecode/classifier.py,sha256=2lYoSH3vOTkXZYPU7Go2WIet1-IuNzTWVhc-ULnXtgw,8024
|
|
10
|
-
sourcecode/cli.py,sha256=
|
|
10
|
+
sourcecode/cli.py,sha256=SZDc7biuDWEXYGn1kvvN4RqmWOA-GHnmJbGdHnYBdh0,246491
|
|
11
11
|
sourcecode/code_notes_analyzer.py,sha256=EJemNCNc9Dn-1RZYu-aNbK0ELzmsyC4s6FdHi3XyNEI,9392
|
|
12
12
|
sourcecode/confidence_analyzer.py,sha256=_jckZSxksV-OU38vbkxfVNBnWCtlCq8Vwfg23x1uspA,19054
|
|
13
13
|
sourcecode/context_scorer.py,sha256=QpChSpsmaAYz91rXA4Ue5xzQmNz_ZboZN09YOHScq1U,14679
|
|
@@ -20,7 +20,8 @@ sourcecode/doc_analyzer.py,sha256=05bjTUbDbmnbajD_cgRnACzS8T7xxBKVX4CjkJlhZg8,24
|
|
|
20
20
|
sourcecode/entrypoint_classifier.py,sha256=jhTYlyqDJH2AtdEcLVaRU3lYRTJuF8DkxVzl4-W3zWE,5322
|
|
21
21
|
sourcecode/env_analyzer.py,sha256=aNTyYgQk5noJDfJU6FmasmESOHfiomyJw5EvZqjy6qc,22213
|
|
22
22
|
sourcecode/error_schema.py,sha256=uwosfNaSujtYm11_732Hu92z5ITV040fQDaIyefSvR4,1683
|
|
23
|
-
sourcecode/explain.py,sha256=
|
|
23
|
+
sourcecode/explain.py,sha256=N5189hO8Ydbunr431zWDpSueSTdgBZh9l2xU-fH-AO8,16832
|
|
24
|
+
sourcecode/file_chunker.py,sha256=xceHnlEg6SlSJAO5Iv2-bXICPjN8qvjQJ2CLYrHuq0o,14744
|
|
24
25
|
sourcecode/file_classifier.py,sha256=A0fEABqtfVu1MfoaxnPAvGpZgneGgVXlJDhT74NYXxE,15314
|
|
25
26
|
sourcecode/flow_analyzer.py,sha256=dSiuY4w49k29jW_EPXUOND9B5uVbuCA7kjnuHi-pIWA,28781
|
|
26
27
|
sourcecode/fqn_utils.py,sha256=XLU7zDkNBXz_RZkIUNfpPmp1nekWtqP-fxV92tDV1vg,2158
|
|
@@ -39,8 +40,9 @@ sourcecode/progress.py,sha256=qn30sWaHOkjTgXsSBmiPkz7Rsbwc5oSlIe6JNEMYp_k,3149
|
|
|
39
40
|
sourcecode/ranking_engine.py,sha256=ZAucq_YX2KkWUuAZf4P0lhtQ_38vEFnUhuGtSZd1S0E,12970
|
|
40
41
|
sourcecode/redactor.py,sha256=SB4hwIvg8h-hvcqKcDWaZvA-aSyn-at-BIRwa0tUv5E,3227
|
|
41
42
|
sourcecode/relevance_scorer.py,sha256=0AgEt4KrV73nioMqBgjhGjtY7L2C7L7cSyKtj3IKcrw,9408
|
|
43
|
+
sourcecode/rename_refactor.py,sha256=rWCsXoDxJNdsmkUXjPtHphT5CjYOgEPmcc817_8Gu-Y,12538
|
|
42
44
|
sourcecode/repo_classifier.py,sha256=FG1vaWKdWXsWdl-S8hjVMiTqcwgaRXkDyvK4rPcOGtQ,22681
|
|
43
|
-
sourcecode/repository_ir.py,sha256=
|
|
45
|
+
sourcecode/repository_ir.py,sha256=n--piFig_NjiGFRzQF8p2-UkLnspHh-4ZFIhUURg2ik,175044
|
|
44
46
|
sourcecode/ris.py,sha256=RcqLVwC-doFcKKViYDkCjZLBqf_wzLES7-F6vHEeWzE,20419
|
|
45
47
|
sourcecode/runtime_classifier.py,sha256=uTAD6BDCiBLUZEDRfqk718kM4RTT_vAbfkcOI2_Xx58,18432
|
|
46
48
|
sourcecode/scanner.py,sha256=WdOQ78mMzjR1NjmKTlbxdgwinnCTfAhxCVLBEFQiFHU,8899
|
|
@@ -50,7 +52,7 @@ sourcecode/serializer.py,sha256=7SBJIbpC_Lg0RGWq8jjNbF5TiuZwoP_fi0qhHnzQM8M,1243
|
|
|
50
52
|
sourcecode/spring_event_topology.py,sha256=5_ON_21Le5zbG-1GRc5GLIi5HJfy_QjcXLVPC5WeUGQ,18055
|
|
51
53
|
sourcecode/spring_findings.py,sha256=8V91iHOg9hFgg6tLLl4FSsgrF-dBqOcO2s-K5sD_goA,5417
|
|
52
54
|
sourcecode/spring_impact.py,sha256=Ohm2k3W4Wts8Kx8Z7DIM-J-cwGtTJBWKFBsX-WkupBQ,32943
|
|
53
|
-
sourcecode/spring_model.py,sha256=
|
|
55
|
+
sourcecode/spring_model.py,sha256=6Lk3rGGFy2suq867S8Da_aCNAXtSGJ36XBaQd9VNTFc,14888
|
|
54
56
|
sourcecode/spring_security_audit.py,sha256=AmUkqoExkNZ3YxxZf9TwkwX-f7P_SETm0QC7VqEAqh4,20618
|
|
55
57
|
sourcecode/spring_semantic.py,sha256=CiAf77p48-RFrUF0zbgww4w2Xigrbo1t5M3ZCDIfV_g,12032
|
|
56
58
|
sourcecode/spring_tx_analyzer.py,sha256=u4_ckdEFZUiIsHdUX4OaIhnvoTdAwrxNTFweG6vc7wE,30526
|
|
@@ -94,8 +96,8 @@ sourcecode/telemetry/consent.py,sha256=wLMvGNJeSSyZoNkQXpoUioY6mMv4Qdvuw7S9jAEWn
|
|
|
94
96
|
sourcecode/telemetry/events.py,sha256=oEvvulfsv5GIDWG2174gSS6tNB95w38AIYiYeifGKlE,2294
|
|
95
97
|
sourcecode/telemetry/filters.py,sha256=Asa71oRl7q3Wt_FMwuufIZJFzSYdgRNKS8LHCIyFeYE,4805
|
|
96
98
|
sourcecode/telemetry/transport.py,sha256=QSslxIwij8YkRWcVvxykODDrkiN_GAAEu3dUP7KIWeE,1651
|
|
97
|
-
sourcecode-1.35.
|
|
98
|
-
sourcecode-1.35.
|
|
99
|
-
sourcecode-1.35.
|
|
100
|
-
sourcecode-1.35.
|
|
101
|
-
sourcecode-1.35.
|
|
99
|
+
sourcecode-1.35.28.dist-info/METADATA,sha256=2IgUuaTJL4deTV1f5JN_uZoyxnHCr_pj4nSptbPpnfo,21705
|
|
100
|
+
sourcecode-1.35.28.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
101
|
+
sourcecode-1.35.28.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
|
|
102
|
+
sourcecode-1.35.28.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
|
|
103
|
+
sourcecode-1.35.28.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|