sourcecode 1.35.25__py3-none-any.whl → 1.35.27__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 +238 -0
- sourcecode/file_chunker.py +397 -0
- sourcecode/migrate_check.py +40 -1
- sourcecode/rename_refactor.py +299 -0
- sourcecode/repository_ir.py +70 -2
- sourcecode/spring_event_topology.py +20 -1
- {sourcecode-1.35.25.dist-info → sourcecode-1.35.27.dist-info}/METADATA +3 -3
- {sourcecode-1.35.25.dist-info → sourcecode-1.35.27.dist-info}/RECORD +12 -10
- {sourcecode-1.35.25.dist-info → sourcecode-1.35.27.dist-info}/WHEEL +0 -0
- {sourcecode-1.35.25.dist-info → sourcecode-1.35.27.dist-info}/entry_points.txt +0 -0
- {sourcecode-1.35.25.dist-info → sourcecode-1.35.27.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")
|
|
@@ -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
|
+
)
|
sourcecode/migrate_check.py
CHANGED
|
@@ -868,9 +868,34 @@ def _find_build_files(root: Path) -> list[tuple[Path, str]]:
|
|
|
868
868
|
return results
|
|
869
869
|
|
|
870
870
|
|
|
871
|
+
def _resolve_maven_properties(text: str) -> str:
|
|
872
|
+
"""Substitute ${prop} references with values from the <properties> block.
|
|
873
|
+
|
|
874
|
+
Handles single-level property references that appear in the same pom.xml.
|
|
875
|
+
Multi-level references (${a} where a=${b}) are resolved up to 3 passes.
|
|
876
|
+
"""
|
|
877
|
+
props: dict[str, str] = {}
|
|
878
|
+
for m in re.finditer(r'<([A-Za-z][\w.\-]*)>\s*([^<${}]+?)\s*</\1>', text):
|
|
879
|
+
props[m.group(1)] = m.group(2).strip()
|
|
880
|
+
if not props:
|
|
881
|
+
return text
|
|
882
|
+
|
|
883
|
+
resolved = text
|
|
884
|
+
for _ in range(3):
|
|
885
|
+
def _sub(m: re.Match) -> str: # noqa: E306
|
|
886
|
+
return props.get(m.group(1), m.group(0))
|
|
887
|
+
resolved_new = re.sub(r'\$\{([\w.\-]+)\}', _sub, resolved)
|
|
888
|
+
if resolved_new == resolved:
|
|
889
|
+
break
|
|
890
|
+
resolved = resolved_new
|
|
891
|
+
return resolved
|
|
892
|
+
|
|
893
|
+
|
|
871
894
|
def _scan_dep_file(text: str, rel_path: str) -> list["MigrationFinding"]:
|
|
872
895
|
"""Apply dependency rules to a build file. Returns one finding per matched rule."""
|
|
873
896
|
is_gradle = rel_path.endswith((".gradle", ".gradle.kts"))
|
|
897
|
+
if not is_gradle and rel_path.endswith(".xml"):
|
|
898
|
+
text = _resolve_maven_properties(text)
|
|
874
899
|
findings: list[MigrationFinding] = []
|
|
875
900
|
for rule in _DEP_RULES:
|
|
876
901
|
if rule.quick_filter is not None and rule.quick_filter not in text:
|
|
@@ -1198,6 +1223,7 @@ def run_migrate_check(
|
|
|
1198
1223
|
limitations.append(f"{xml_read_errors} XML file(s) could not be read and were skipped.")
|
|
1199
1224
|
|
|
1200
1225
|
dep_read_errors = 0
|
|
1226
|
+
raw_dep_findings: list[MigrationFinding] = []
|
|
1201
1227
|
for abs_path, rel_path in build_files:
|
|
1202
1228
|
try:
|
|
1203
1229
|
text = abs_path.read_text(encoding="utf-8", errors="replace")
|
|
@@ -1206,7 +1232,20 @@ def run_migrate_check(
|
|
|
1206
1232
|
continue
|
|
1207
1233
|
dep_findings = _scan_dep_file(text, rel_path)
|
|
1208
1234
|
filtered = [f for f in dep_findings if SEVERITY_ORDER.get(f.severity, 3) <= min_order]
|
|
1209
|
-
|
|
1235
|
+
raw_dep_findings.extend(filtered)
|
|
1236
|
+
|
|
1237
|
+
# Deduplicate dep findings by rule_id: same dependency in parent + child poms
|
|
1238
|
+
# is one logical finding. Keep the first occurrence (root pom sorts first).
|
|
1239
|
+
_seen_dep_rules: dict[str, int] = {} # rule_id → count
|
|
1240
|
+
for f in raw_dep_findings:
|
|
1241
|
+
_seen_dep_rules[f.rule_id] = _seen_dep_rules.get(f.rule_id, 0) + 1
|
|
1242
|
+
_dedup_dep: list[MigrationFinding] = []
|
|
1243
|
+
_emitted: set[str] = set()
|
|
1244
|
+
for f in raw_dep_findings:
|
|
1245
|
+
if f.rule_id not in _emitted:
|
|
1246
|
+
_dedup_dep.append(f)
|
|
1247
|
+
_emitted.add(f.rule_id)
|
|
1248
|
+
all_findings.extend(_dedup_dep)
|
|
1210
1249
|
|
|
1211
1250
|
if dep_read_errors:
|
|
1212
1251
|
limitations.append(f"{dep_read_errors} build file(s) could not be read and were skipped.")
|
|
@@ -0,0 +1,299 @@
|
|
|
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 "/test/" in rel or "/tests/" in rel or rel.startswith("test/"):
|
|
110
|
+
continue
|
|
111
|
+
results.append(p)
|
|
112
|
+
return results
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _find_class_file(
|
|
116
|
+
java_files: list[Path],
|
|
117
|
+
class_name: str,
|
|
118
|
+
root: Path,
|
|
119
|
+
) -> Optional[Path]:
|
|
120
|
+
"""Find the file that declares `class_name` (by filename first, then scan)."""
|
|
121
|
+
# Prefer exact filename match
|
|
122
|
+
candidates = [f for f in java_files if f.stem == class_name]
|
|
123
|
+
if len(candidates) == 1:
|
|
124
|
+
return candidates[0]
|
|
125
|
+
if len(candidates) > 1:
|
|
126
|
+
# Multiple files with same stem — pick the one that has the class declaration
|
|
127
|
+
decl_re = re.compile(
|
|
128
|
+
r'\b(?:public\s+)?(?:abstract\s+)?(?:class|interface|enum)\s+' + re.escape(class_name) + r'\b'
|
|
129
|
+
)
|
|
130
|
+
for c in candidates:
|
|
131
|
+
try:
|
|
132
|
+
if decl_re.search(c.read_text(encoding="utf-8", errors="replace")):
|
|
133
|
+
return c
|
|
134
|
+
except OSError:
|
|
135
|
+
continue
|
|
136
|
+
return candidates[0]
|
|
137
|
+
|
|
138
|
+
# Fallback: scan file contents for class declaration
|
|
139
|
+
decl_re = re.compile(
|
|
140
|
+
r'\b(?:public\s+)?(?:abstract\s+)?(?:class|interface|enum)\s+' + re.escape(class_name) + r'\b'
|
|
141
|
+
)
|
|
142
|
+
for f in java_files:
|
|
143
|
+
try:
|
|
144
|
+
if decl_re.search(f.read_text(encoding="utf-8", errors="replace")):
|
|
145
|
+
return f
|
|
146
|
+
except OSError:
|
|
147
|
+
continue
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _apply_rename(source: str, old_name: str, new_name: str) -> str:
|
|
152
|
+
"""Apply word-boundary replacement for class name (PascalCase and camelCase forms)."""
|
|
153
|
+
# PascalCase replacement: all type references, declarations, imports
|
|
154
|
+
result = re.sub(r'\b' + re.escape(old_name) + r'\b', new_name, source)
|
|
155
|
+
|
|
156
|
+
# camelCase instance names: serviceA → serviceB (only when different from PascalCase)
|
|
157
|
+
old_camel = _to_camel(old_name)
|
|
158
|
+
new_camel = _to_camel(new_name)
|
|
159
|
+
if old_camel != old_name and old_camel in result:
|
|
160
|
+
result = re.sub(r'\b' + re.escape(old_camel) + r'\b', new_camel, result)
|
|
161
|
+
|
|
162
|
+
return result
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _make_diff(old_text: str, new_text: str, rel_path: str) -> str:
|
|
166
|
+
"""Produce a unified diff string."""
|
|
167
|
+
old_lines = old_text.splitlines(keepends=True)
|
|
168
|
+
new_lines = new_text.splitlines(keepends=True)
|
|
169
|
+
diff_lines = list(difflib.unified_diff(
|
|
170
|
+
old_lines,
|
|
171
|
+
new_lines,
|
|
172
|
+
fromfile=f"a/{rel_path}",
|
|
173
|
+
tofile=f"b/{rel_path}",
|
|
174
|
+
lineterm="",
|
|
175
|
+
))
|
|
176
|
+
return "".join(diff_lines)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# ---------------------------------------------------------------------------
|
|
180
|
+
# Public API
|
|
181
|
+
# ---------------------------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
def rename_class(
|
|
184
|
+
root: Path,
|
|
185
|
+
old_name: str,
|
|
186
|
+
new_name: str,
|
|
187
|
+
*,
|
|
188
|
+
dry_run: bool = False,
|
|
189
|
+
include_tests: bool = True,
|
|
190
|
+
) -> RenameResult:
|
|
191
|
+
"""Rename a Java class throughout the repository.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
root: Absolute repo root directory.
|
|
195
|
+
old_name: Simple class name to rename (e.g. "ServiceA").
|
|
196
|
+
new_name: Target simple class name (e.g. "ServiceB").
|
|
197
|
+
dry_run: If True, compute changes but do not write any files.
|
|
198
|
+
include_tests: If True (default), also rename in test files.
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
RenameResult with structured change audit trail (BLOCKER-C format).
|
|
202
|
+
"""
|
|
203
|
+
root = root.resolve()
|
|
204
|
+
|
|
205
|
+
result = RenameResult(
|
|
206
|
+
old_name=old_name,
|
|
207
|
+
new_name=new_name,
|
|
208
|
+
old_file="",
|
|
209
|
+
new_file="",
|
|
210
|
+
dry_run=dry_run,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
# Validate input
|
|
214
|
+
if not old_name or not old_name[0].isupper():
|
|
215
|
+
result.errors.append(
|
|
216
|
+
f"old_name '{old_name}' must be a Java class name (PascalCase, non-empty)."
|
|
217
|
+
)
|
|
218
|
+
return result
|
|
219
|
+
if not new_name or not new_name[0].isupper():
|
|
220
|
+
result.errors.append(
|
|
221
|
+
f"new_name '{new_name}' must be a Java class name (PascalCase, non-empty)."
|
|
222
|
+
)
|
|
223
|
+
return result
|
|
224
|
+
if old_name == new_name:
|
|
225
|
+
result.errors.append("old_name and new_name are identical — nothing to rename.")
|
|
226
|
+
return result
|
|
227
|
+
if not root.is_dir():
|
|
228
|
+
result.errors.append(f"Root directory '{root}' does not exist.")
|
|
229
|
+
return result
|
|
230
|
+
|
|
231
|
+
# Collect files
|
|
232
|
+
java_files = _collect_java_files(root, include_tests=include_tests)
|
|
233
|
+
result.files_scanned = len(java_files)
|
|
234
|
+
|
|
235
|
+
# Locate the source file
|
|
236
|
+
source_file = _find_class_file(java_files, old_name, root)
|
|
237
|
+
if source_file is None:
|
|
238
|
+
result.errors.append(
|
|
239
|
+
f"Could not find a file declaring class '{old_name}' under '{root}'."
|
|
240
|
+
)
|
|
241
|
+
return result
|
|
242
|
+
|
|
243
|
+
# Determine new file path (same directory, new filename)
|
|
244
|
+
new_file_path = source_file.with_name(new_name + ".java")
|
|
245
|
+
result.old_file = str(source_file.relative_to(root)).replace("\\", "/")
|
|
246
|
+
result.new_file = str(new_file_path.relative_to(root)).replace("\\", "/")
|
|
247
|
+
|
|
248
|
+
if new_file_path.exists() and new_file_path != source_file:
|
|
249
|
+
result.errors.append(
|
|
250
|
+
f"Target file '{result.new_file}' already exists — aborting to avoid overwrite."
|
|
251
|
+
)
|
|
252
|
+
return result
|
|
253
|
+
|
|
254
|
+
# Apply text replacements to all Java files
|
|
255
|
+
changes: list[FileChange] = []
|
|
256
|
+
for java_file in java_files:
|
|
257
|
+
try:
|
|
258
|
+
old_text = java_file.read_text(encoding="utf-8", errors="replace")
|
|
259
|
+
except OSError as e:
|
|
260
|
+
result.errors.append(f"Could not read '{java_file}': {e}")
|
|
261
|
+
continue
|
|
262
|
+
|
|
263
|
+
new_text = _apply_rename(old_text, old_name, new_name)
|
|
264
|
+
if new_text == old_text:
|
|
265
|
+
continue
|
|
266
|
+
|
|
267
|
+
rel_path = str(java_file.relative_to(root)).replace("\\", "/")
|
|
268
|
+
diff = _make_diff(old_text, new_text, rel_path)
|
|
269
|
+
|
|
270
|
+
# Determine intent
|
|
271
|
+
is_source = java_file == source_file
|
|
272
|
+
if is_source:
|
|
273
|
+
intent = f"Renamed class declaration: {old_name} → {new_name}"
|
|
274
|
+
else:
|
|
275
|
+
intent = f"Updated references to {old_name} → {new_name}"
|
|
276
|
+
|
|
277
|
+
changes.append(FileChange(
|
|
278
|
+
file=rel_path,
|
|
279
|
+
intent=intent,
|
|
280
|
+
diff=diff,
|
|
281
|
+
before_lines=old_text.splitlines(),
|
|
282
|
+
after_lines=new_text.splitlines(),
|
|
283
|
+
))
|
|
284
|
+
|
|
285
|
+
if not dry_run:
|
|
286
|
+
java_file.write_text(new_text, encoding="utf-8")
|
|
287
|
+
|
|
288
|
+
result.changes = changes
|
|
289
|
+
result.files_modified = len(changes)
|
|
290
|
+
|
|
291
|
+
# Rename the physical file (BLOCKER-A core fix)
|
|
292
|
+
if not dry_run and source_file.exists():
|
|
293
|
+
source_file.rename(new_file_path)
|
|
294
|
+
elif dry_run:
|
|
295
|
+
# In dry_run mode, add a synthetic change record for the file rename itself
|
|
296
|
+
# if no text change was found in the source file (e.g. only filename changes).
|
|
297
|
+
pass
|
|
298
|
+
|
|
299
|
+
return result
|
sourcecode/repository_ir.py
CHANGED
|
@@ -105,7 +105,12 @@ class EvidenceBundle:
|
|
|
105
105
|
_PKG_RE = re.compile(r'^package\s+([\w.]+)\s*;', re.MULTILINE)
|
|
106
106
|
_IMPORT_RE = re.compile(r'^import\s+(?:static\s+)?([\w.]+(?:\.\*)?)\s*;', re.MULTILINE)
|
|
107
107
|
_ANN_RE = re.compile(r'^(@[\w.]+)')
|
|
108
|
-
_ANN_WITH_ARGS_RE = re.compile(
|
|
108
|
+
_ANN_WITH_ARGS_RE = re.compile(
|
|
109
|
+
r'^(@[\w.]+)\s*'
|
|
110
|
+
r'(?:\('
|
|
111
|
+
r'((?:[^()"\']*|"[^"]*"|\'[^\']*\'|\((?:[^()"\']*|"[^"]*"|\'[^\']*\')*\))*)'
|
|
112
|
+
r'\))?'
|
|
113
|
+
)
|
|
109
114
|
|
|
110
115
|
_CLASS_DECL_RE = re.compile(
|
|
111
116
|
r'(?:^|(?<=\s))'
|
|
@@ -2814,6 +2819,11 @@ def build_repo_ir(
|
|
|
2814
2819
|
if since:
|
|
2815
2820
|
_since_changed = _get_git_changed_files(root, since)
|
|
2816
2821
|
|
|
2822
|
+
# L-6: analysis_meta tracking (files_read, lines_read, symbols_analyzed, token_estimate)
|
|
2823
|
+
_meta_files_read = 0
|
|
2824
|
+
_meta_lines_read = 0
|
|
2825
|
+
_meta_chars_read = 0
|
|
2826
|
+
|
|
2817
2827
|
# Pass 1: extract symbols from all files so we can build the same-package
|
|
2818
2828
|
# type map before building relations. Java classes in the same package
|
|
2819
2829
|
# reference each other without import statements, so import_map alone cannot
|
|
@@ -2825,6 +2835,9 @@ def build_repo_ir(
|
|
|
2825
2835
|
source = abs_path.read_text(encoding="utf-8", errors="replace")
|
|
2826
2836
|
except OSError:
|
|
2827
2837
|
continue
|
|
2838
|
+
_meta_files_read += 1
|
|
2839
|
+
_meta_lines_read += source.count("\n") + (1 if source and not source.endswith("\n") else 0)
|
|
2840
|
+
_meta_chars_read += len(source)
|
|
2828
2841
|
package, symbols, raw_imports = _extract_symbols(source, rel_path)
|
|
2829
2842
|
all_symbols.extend(symbols)
|
|
2830
2843
|
_per_file.append((rel_path, source, package, raw_imports, symbols))
|
|
@@ -2878,7 +2891,16 @@ def build_repo_ir(
|
|
|
2878
2891
|
route_diffs_arg: Optional[list[dict]] = (
|
|
2879
2892
|
sorted(all_route_diffs, key=lambda d: d["symbol"]) if since else None
|
|
2880
2893
|
)
|
|
2881
|
-
|
|
2894
|
+
ir = _assemble(all_symbols, unique_relations, all_changed, spring_summary, route_diffs_arg)
|
|
2895
|
+
|
|
2896
|
+
# L-6: inject analysis_meta — files_read, lines_read, symbols_analyzed, token_estimate
|
|
2897
|
+
ir["analysis_meta"] = {
|
|
2898
|
+
"files_read": _meta_files_read,
|
|
2899
|
+
"lines_read": _meta_lines_read,
|
|
2900
|
+
"symbols_analyzed": len(all_symbols),
|
|
2901
|
+
"token_estimate": _meta_chars_read // 4, # 4 chars ≈ 1 token (rough approximation)
|
|
2902
|
+
}
|
|
2903
|
+
return ir
|
|
2882
2904
|
|
|
2883
2905
|
|
|
2884
2906
|
# ---------------------------------------------------------------------------
|
|
@@ -3303,6 +3325,52 @@ def extract_java_endpoints(root: Path) -> "dict[str, Any]":
|
|
|
3303
3325
|
else:
|
|
3304
3326
|
security_model = "unknown"
|
|
3305
3327
|
|
|
3328
|
+
# Detect XML-based Spring Security config. When present, per-endpoint
|
|
3329
|
+
# none_detected is expected and does NOT mean the endpoint is unsecured —
|
|
3330
|
+
# security is declared in XML (HttpSecurity rules, filter chains, web.xml
|
|
3331
|
+
# security constraints). Update security_model and re-tag affected endpoints
|
|
3332
|
+
# so the output cannot be misread as "unprotected".
|
|
3333
|
+
_XML_SECURITY_RE = re.compile(
|
|
3334
|
+
r'(?:xmlns(?::[a-z]+)?="http://www\.springframework\.org/schema/security"'
|
|
3335
|
+
r'|<security:http\b'
|
|
3336
|
+
r'|<http\s[^>]*use-expressions'
|
|
3337
|
+
r'|spring-security-[2345]'
|
|
3338
|
+
r'|xmlns:security="http://www\.springframework\.org/schema/security")',
|
|
3339
|
+
re.IGNORECASE,
|
|
3340
|
+
)
|
|
3341
|
+
_xml_security_detected = False
|
|
3342
|
+
_XML_GLOBS = (
|
|
3343
|
+
"*security*.xml", "*Security*.xml",
|
|
3344
|
+
"*applicationContext*.xml", "*-context.xml", "*Context.xml",
|
|
3345
|
+
"*spring*.xml", "*Spring*.xml",
|
|
3346
|
+
)
|
|
3347
|
+
for _glob in _XML_GLOBS:
|
|
3348
|
+
for _xf in root.rglob(_glob):
|
|
3349
|
+
if "target/" in str(_xf).replace("\\", "/"):
|
|
3350
|
+
continue
|
|
3351
|
+
try:
|
|
3352
|
+
_xt = _xf.read_text(encoding="utf-8", errors="replace")
|
|
3353
|
+
except OSError:
|
|
3354
|
+
continue
|
|
3355
|
+
if _XML_SECURITY_RE.search(_xt):
|
|
3356
|
+
_xml_security_detected = True
|
|
3357
|
+
break
|
|
3358
|
+
if _xml_security_detected:
|
|
3359
|
+
break
|
|
3360
|
+
|
|
3361
|
+
if _xml_security_detected and security_model == "unknown":
|
|
3362
|
+
security_model = "xml_or_filter_chain"
|
|
3363
|
+
# Re-tag per-endpoint none_detected → xml_or_filter_chain so the output
|
|
3364
|
+
# cannot be misread as "endpoint is unprotected".
|
|
3365
|
+
for ep in endpoints:
|
|
3366
|
+
if ep.get("security", {}).get("policy") == "none_detected":
|
|
3367
|
+
ep["security"] = {"policy": "xml_or_filter_chain"}
|
|
3368
|
+
# Recompute no_security_signal (now counts only truly unknown endpoints)
|
|
3369
|
+
no_security_signal = sum(
|
|
3370
|
+
1 for e in endpoints
|
|
3371
|
+
if e.get("security", {}).get("policy") == "none_detected"
|
|
3372
|
+
)
|
|
3373
|
+
|
|
3306
3374
|
return {
|
|
3307
3375
|
"endpoints": endpoints,
|
|
3308
3376
|
"total": len(endpoints),
|
|
@@ -197,14 +197,17 @@ def _compute_event_risk(
|
|
|
197
197
|
consumer_count: int,
|
|
198
198
|
before_commit_count: int,
|
|
199
199
|
cross_module: bool,
|
|
200
|
+
sync_in_tx_count: int = 0,
|
|
200
201
|
) -> str:
|
|
201
202
|
"""Deterministic risk scoring per spec.
|
|
202
203
|
|
|
203
204
|
high: fanout > 5 OR cross-module propagation OR BEFORE_COMMIT consumers
|
|
205
|
+
OR sync @EventListener inside @Transactional publisher
|
|
204
206
|
medium: 2–5 consumers
|
|
205
207
|
low: ≤1 consumer
|
|
206
208
|
"""
|
|
207
|
-
if consumer_count > _RISK_FANOUT_HIGH or cross_module
|
|
209
|
+
if (consumer_count > _RISK_FANOUT_HIGH or cross_module
|
|
210
|
+
or before_commit_count > 0 or sync_in_tx_count > 0):
|
|
208
211
|
return "high"
|
|
209
212
|
if consumer_count >= _RISK_FANOUT_MEDIUM:
|
|
210
213
|
return "medium"
|
|
@@ -327,9 +330,23 @@ class EventTopologyOrchestrator:
|
|
|
327
330
|
# ── 7. TX context ──────────────────────────────────────────────────
|
|
328
331
|
after_commit = [c.fqn for c in consumers if c.transactional_phase == "AFTER_COMMIT"]
|
|
329
332
|
before_commit_risks = [c.fqn for c in consumers if c.transactional_phase == "BEFORE_COMMIT"]
|
|
333
|
+
|
|
334
|
+
# Detect sync @EventListener inside @Transactional publisher.
|
|
335
|
+
# Plain @EventListener fires synchronously; if the publisher method is
|
|
336
|
+
# @Transactional the listener runs inside that TX — listener exception
|
|
337
|
+
# rolls back the outer TX, and DB state may be partially committed.
|
|
338
|
+
tx_publishers = [
|
|
339
|
+
p for p in publishers
|
|
340
|
+
if "@Transactional" in ((fqn_index.get(p) or {}).get("annotations") or [])
|
|
341
|
+
]
|
|
342
|
+
sync_in_tx_risks = [
|
|
343
|
+
c.fqn for c in consumers
|
|
344
|
+
if c.type == "spring_event" and tx_publishers
|
|
345
|
+
]
|
|
330
346
|
tx_context = {
|
|
331
347
|
"after_commit_consumers": after_commit,
|
|
332
348
|
"before_commit_risks": before_commit_risks,
|
|
349
|
+
"sync_in_tx_risks": sync_in_tx_risks,
|
|
333
350
|
}
|
|
334
351
|
|
|
335
352
|
# ── 8. Cross-module detection ──────────────────────────────────────
|
|
@@ -352,6 +369,7 @@ class EventTopologyOrchestrator:
|
|
|
352
369
|
consumer_count=len(consumers),
|
|
353
370
|
before_commit_count=len(before_commit_risks),
|
|
354
371
|
cross_module=cross_module,
|
|
372
|
+
sync_in_tx_count=len(sync_in_tx_risks),
|
|
355
373
|
)
|
|
356
374
|
|
|
357
375
|
# ── 10. Confidence ─────────────────────────────────────────────────
|
|
@@ -385,6 +403,7 @@ class EventTopologyOrchestrator:
|
|
|
385
403
|
"kafka_listeners_in_repo": kafka_count,
|
|
386
404
|
"rabbit_listeners_in_repo": rabbit_count,
|
|
387
405
|
"before_commit_risk_count": len(before_commit_risks),
|
|
406
|
+
"sync_in_tx_risk_count": len(sync_in_tx_risks),
|
|
388
407
|
"level2_events": list(level2_events.keys()),
|
|
389
408
|
"cross_module": cross_module,
|
|
390
409
|
"model_build_time_ms": model.build_time_ms,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sourcecode
|
|
3
|
-
Version: 1.35.
|
|
3
|
+
Version: 1.35.27
|
|
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,7 @@ pipx install sourcecode
|
|
|
114
114
|
|
|
115
115
|
```bash
|
|
116
116
|
sourcecode version
|
|
117
|
-
# sourcecode 1.35.
|
|
117
|
+
# sourcecode 1.35.27
|
|
118
118
|
```
|
|
119
119
|
|
|
120
120
|
---
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
sourcecode/__init__.py,sha256=
|
|
1
|
+
sourcecode/__init__.py,sha256=wAc-lNaY6I9JzmeKSyL20qzmqMMrwwFLFjIF6umkX0w,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=es0kuqlHSIMqzdQZZ9tHYbUgBe_Ogw2k_pjC1JFhJro,246257
|
|
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
|
|
@@ -21,6 +21,7 @@ sourcecode/entrypoint_classifier.py,sha256=jhTYlyqDJH2AtdEcLVaRU3lYRTJuF8DkxVzl4
|
|
|
21
21
|
sourcecode/env_analyzer.py,sha256=aNTyYgQk5noJDfJU6FmasmESOHfiomyJw5EvZqjy6qc,22213
|
|
22
22
|
sourcecode/error_schema.py,sha256=uwosfNaSujtYm11_732Hu92z5ITV040fQDaIyefSvR4,1683
|
|
23
23
|
sourcecode/explain.py,sha256=cCRpR4L0goET8UR1iFTq4gQ-2TDCRTL7jGITMO09TE4,16556
|
|
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
|
|
@@ -29,7 +30,7 @@ sourcecode/graph_analyzer.py,sha256=DHR8fY69oU_Pi4SYaWboX6EoEFrctQKB9dsjpqwGMzw,
|
|
|
29
30
|
sourcecode/license.py,sha256=3JCV2OeTVttKrOGBguU5uZC0c02Stig-KLB0mP2lNiY,22742
|
|
30
31
|
sourcecode/mcp_nudge.py,sha256=5ELU_ixzh6uA83NXLOZT8h00OhL53okfQdji3jyKOjg,2917
|
|
31
32
|
sourcecode/metrics_analyzer.py,sha256=m0ENgtqKeBL17kUIK3fmGkgo7UfXBNHxCMj0H_Y5K7c,22750
|
|
32
|
-
sourcecode/migrate_check.py,sha256
|
|
33
|
+
sourcecode/migrate_check.py,sha256=GuYK36DDFkwf07jbAgcoc-Ovq8ttLQNMsRqhsUilMzY,54514
|
|
33
34
|
sourcecode/output_budget.py,sha256=Js9yUlfQtPhqBl9R6wn_9UHVjjJc3GtLcqyfjf5t50Q,9869
|
|
34
35
|
sourcecode/path_filters.py,sha256=ROFRQ8eSLBEMiixK9f45-RO7um4VEEcjoD5AA4I427I,3739
|
|
35
36
|
sourcecode/pr_comment_renderer.py,sha256=smHslxiG14lrytCkq5nFrFu-qTHgA-t-LFYfdrfjz2o,14423
|
|
@@ -39,15 +40,16 @@ 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=lgfJ5Qp4WpduxcaqU84IrFz9ZhJlFnqnrCNkB_bf6RA,10379
|
|
42
44
|
sourcecode/repo_classifier.py,sha256=FG1vaWKdWXsWdl-S8hjVMiTqcwgaRXkDyvK4rPcOGtQ,22681
|
|
43
|
-
sourcecode/repository_ir.py,sha256=
|
|
45
|
+
sourcecode/repository_ir.py,sha256=jOyInh72kTVX9jzyyeyBIEXfs4uFfRINV5rJOsT6YiM,172453
|
|
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
|
|
47
49
|
sourcecode/schema.py,sha256=aHNXDf8LGyUC8ZDE_VS9kiskC2-Oswhi_WnpdGy6HDw,24897
|
|
48
50
|
sourcecode/semantic_analyzer.py,sha256=TDuC3wzZR2DPm1mgrAg1YSLk2QzJoueS3TZAmyGGpCU,89417
|
|
49
51
|
sourcecode/serializer.py,sha256=7SBJIbpC_Lg0RGWq8jjNbF5TiuZwoP_fi0qhHnzQM8M,124386
|
|
50
|
-
sourcecode/spring_event_topology.py,sha256=
|
|
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
55
|
sourcecode/spring_model.py,sha256=IzMcM5ftw1_EHG3FGUDT7qdAMpo3eqbAE1LRuasfr_4,14739
|
|
@@ -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.27.dist-info/METADATA,sha256=mUXNvfLH6jEm0m4f4saI7ILJHMFMO9qjn5bcgiY-5U8,21297
|
|
100
|
+
sourcecode-1.35.27.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
101
|
+
sourcecode-1.35.27.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
|
|
102
|
+
sourcecode-1.35.27.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
|
|
103
|
+
sourcecode-1.35.27.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|