sourcecode 1.69.0__py3-none-any.whl → 1.71.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sourcecode/__init__.py +1 -1
- sourcecode/architecture_analyzer.py +39 -6
- sourcecode/architecture_summary.py +66 -0
- sourcecode/detectors/java.py +40 -2
- sourcecode/hibernate_strat.py +96 -21
- sourcecode/integration_detector.py +88 -0
- sourcecode/migrate_check.py +144 -14
- sourcecode/repository_ir.py +32 -0
- sourcecode/spring_impact.py +30 -3
- {sourcecode-1.69.0.dist-info → sourcecode-1.71.0.dist-info}/METADATA +16 -5
- {sourcecode-1.69.0.dist-info → sourcecode-1.71.0.dist-info}/RECORD +14 -14
- {sourcecode-1.69.0.dist-info → sourcecode-1.71.0.dist-info}/WHEEL +0 -0
- {sourcecode-1.69.0.dist-info → sourcecode-1.71.0.dist-info}/entry_points.txt +0 -0
- {sourcecode-1.69.0.dist-info → sourcecode-1.71.0.dist-info}/licenses/LICENSE +0 -0
sourcecode/__init__.py
CHANGED
|
@@ -38,7 +38,15 @@ _CODE_EXTENSIONS = {
|
|
|
38
38
|
}
|
|
39
39
|
_GENERIC_NAMES = {"utils", "helpers", "common", "shared", "misc", "core", "root", ""}
|
|
40
40
|
|
|
41
|
-
_TEST_DIRS: frozenset[str] = frozenset({
|
|
41
|
+
_TEST_DIRS: frozenset[str] = frozenset({
|
|
42
|
+
"tests", "test", "spec", "specs", "__tests__", "e2e",
|
|
43
|
+
# Gradle/Maven test-fixture source roots are test code, not runtime architecture.
|
|
44
|
+
"testfixtures", "testfixture",
|
|
45
|
+
})
|
|
46
|
+
# Always-vendored asset trees — never a backend code layer at any depth.
|
|
47
|
+
_ASSET_DIRS: frozenset[str] = frozenset({
|
|
48
|
+
"node_modules", "bower_components",
|
|
49
|
+
})
|
|
42
50
|
_BENCHMARK_DIRS: frozenset[str] = frozenset({
|
|
43
51
|
"benchmark", "benchmarks", "bench",
|
|
44
52
|
"example", "examples",
|
|
@@ -50,7 +58,7 @@ _BENCHMARK_DIRS: frozenset[str] = frozenset({
|
|
|
50
58
|
_DOCS_DIRS: frozenset[str] = frozenset({"docs", "doc", "documentation", "wiki"})
|
|
51
59
|
_TOOLING_DIRS: frozenset[str] = frozenset({"scripts", "script", "tools", "tool", "ci"})
|
|
52
60
|
# All dirs that are not part of the runtime source architecture
|
|
53
|
-
_NON_SOURCE_DIRS: frozenset[str] = _TEST_DIRS | _BENCHMARK_DIRS | _DOCS_DIRS | _TOOLING_DIRS
|
|
61
|
+
_NON_SOURCE_DIRS: frozenset[str] = _TEST_DIRS | _BENCHMARK_DIRS | _DOCS_DIRS | _TOOLING_DIRS | _ASSET_DIRS
|
|
54
62
|
|
|
55
63
|
# Exact file stems that signal a specific architectural layer
|
|
56
64
|
_LAYER_STEM_EXACT: dict[str, str] = {
|
|
@@ -150,6 +158,15 @@ LAYER_PATTERNS: dict[str, dict[str, list[str]]] = {
|
|
|
150
158
|
},
|
|
151
159
|
}
|
|
152
160
|
|
|
161
|
+
# Layer keys a pattern MUST match to qualify, regardless of score. BUG (JobRunr
|
|
162
|
+
# field test): "mvc" was inferred from a controller-ish dir (`handlers`) plus a
|
|
163
|
+
# `model` dir, with NO view layer — that is not MVC (a library with no templates/
|
|
164
|
+
# pages/components is at most layered). MVC's defining trait is the View, so require
|
|
165
|
+
# it; without it the pattern falls through to layered / a weaker match.
|
|
166
|
+
_PATTERN_REQUIRED_KEYS: dict[str, frozenset[str]] = {
|
|
167
|
+
"mvc": frozenset({"view"}),
|
|
168
|
+
}
|
|
169
|
+
|
|
153
170
|
# Higher value = wins when score ties
|
|
154
171
|
_PATTERN_PRIORITY: dict[str, int] = {
|
|
155
172
|
"cqrs": 8,
|
|
@@ -245,8 +262,12 @@ class ArchitectureAnalyzer:
|
|
|
245
262
|
# Step 2: domain clustering
|
|
246
263
|
domains = self._cluster_domains(filtered)
|
|
247
264
|
|
|
248
|
-
# Step 3: layer detection
|
|
249
|
-
|
|
265
|
+
# Step 3: layer detection. Feed the FULL path list (not the code-extension
|
|
266
|
+
# `filtered` set): a View layer is template files (.html/.jinja/.ejs…) that
|
|
267
|
+
# `filtered` would drop, hiding the very layer that distinguishes MVC.
|
|
268
|
+
# _detect_layers applies its own non-source/asset/test dir filtering, so
|
|
269
|
+
# bundled frontend trees and test fixtures are still excluded.
|
|
270
|
+
pattern, layers = self._detect_layers(sm.file_paths)
|
|
250
271
|
if pattern in (None, "flat", "unknown"):
|
|
251
272
|
if pattern == "flat":
|
|
252
273
|
limitations.append("Layer pattern not detected: project has a flat directory structure")
|
|
@@ -518,10 +539,18 @@ class ArchitectureAnalyzer:
|
|
|
518
539
|
return domains
|
|
519
540
|
|
|
520
541
|
def _detect_layers(self, paths: list[str]) -> tuple[str, list[ArchitectureLayer]]:
|
|
521
|
-
# Exclude non-source paths (tests, benchmarks, docs, tooling
|
|
542
|
+
# Exclude non-source paths (tests, benchmarks, docs, tooling, vendored assets)
|
|
543
|
+
# from layer scoring. Also exclude anything under a `resources/` segment: in
|
|
544
|
+
# Maven/Gradle layouts `src/main/resources/**` is bundled config/assets — e.g.
|
|
545
|
+
# JobRunr ships a React dashboard SPA at
|
|
546
|
+
# `core/src/main/resources/org/jobrunr/dashboard/frontend/src/components`, whose
|
|
547
|
+
# `components` dir would otherwise be miscounted as a backend MVC "view" layer.
|
|
522
548
|
source_paths = [
|
|
523
549
|
p for p in paths
|
|
524
|
-
if not any(
|
|
550
|
+
if not any(
|
|
551
|
+
part.lower() in _NON_SOURCE_DIRS or part.lower() == "resources"
|
|
552
|
+
for part in p.replace("\\", "/").split("/")
|
|
553
|
+
)
|
|
525
554
|
]
|
|
526
555
|
if not source_paths:
|
|
527
556
|
return "unknown", []
|
|
@@ -544,6 +573,10 @@ class ArchitectureAnalyzer:
|
|
|
544
573
|
matched_dirs = [d for d in dir_names if d in keywords]
|
|
545
574
|
if matched_dirs:
|
|
546
575
|
matched[layer_key] = matched_dirs
|
|
576
|
+
# A pattern with unmet required keys cannot qualify (e.g. mvc needs a view).
|
|
577
|
+
required = _PATTERN_REQUIRED_KEYS.get(pattern_name)
|
|
578
|
+
if required and not required.issubset(matched.keys()):
|
|
579
|
+
continue
|
|
547
580
|
score = len(matched)
|
|
548
581
|
priority = _PATTERN_PRIORITY.get(pattern_name, 0)
|
|
549
582
|
if (score, priority) > (best_score, best_priority):
|
|
@@ -23,6 +23,14 @@ _JAVA_EXTENSIONS = {".java", ".kt", ".scala"}
|
|
|
23
23
|
|
|
24
24
|
_CORE_DETECTION_MODULES = {"scanner", "detectors", "classifier", "workspace"}
|
|
25
25
|
|
|
26
|
+
# BUG #4 (v1.70.0): the "REST API" project-type label is driven by HTTP-framework
|
|
27
|
+
# PRESENCE (Spring MVC / JAX-RS on the classpath), not by an actual endpoint count.
|
|
28
|
+
# The `--compact` summary is the first (often only) thing an agent reads, so it must
|
|
29
|
+
# not assert "rest api" when the authoritative `endpoints` command finds almost no
|
|
30
|
+
# high-confidence surface. Below this many high-confidence endpoints we degrade the
|
|
31
|
+
# headline to a qualified, consistent phrasing instead of overclaiming.
|
|
32
|
+
_MIN_REST_ENDPOINTS_FOR_LABEL = 5
|
|
33
|
+
|
|
26
34
|
_OPTIONAL_LABEL_MAP: dict[str, str] = {
|
|
27
35
|
"DependencyAnalyzer": "dependencias",
|
|
28
36
|
"GraphAnalyzer": "grafo de módulos",
|
|
@@ -41,6 +49,7 @@ class ArchitectureSummarizer:
|
|
|
41
49
|
|
|
42
50
|
def __init__(self, root: Path) -> None:
|
|
43
51
|
self.root = root
|
|
52
|
+
self._endpoint_support_cache: tuple[int, int] | None = None
|
|
44
53
|
|
|
45
54
|
def generate(self, sm: SourceMap) -> str | None:
|
|
46
55
|
try:
|
|
@@ -133,6 +142,7 @@ class ArchitectureSummarizer:
|
|
|
133
142
|
# Line 2: architecture pattern + layers
|
|
134
143
|
if arch and arch.pattern not in (None, "unknown", "flat"):
|
|
135
144
|
arch_line = self._describe_arch_pattern(arch)
|
|
145
|
+
arch_line = self._qualify_web_pattern(arch, arch_line, sm)
|
|
136
146
|
if arch_line:
|
|
137
147
|
lines.append(arch_line)
|
|
138
148
|
|
|
@@ -185,9 +195,65 @@ class ArchitectureSummarizer:
|
|
|
185
195
|
|
|
186
196
|
fw_str = f" using {', '.join(fw_names)}" if fw_names else ""
|
|
187
197
|
if runtime:
|
|
198
|
+
# BUG #4: never assert "rest api" in the headline unless the endpoints
|
|
199
|
+
# command actually backs it (Java/Kotlin only — that is where we have an
|
|
200
|
+
# authoritative extractor). Degrade to a qualified, consistent phrasing.
|
|
201
|
+
if sm.project_type == "api" and primary.stack in {"java", "kotlin"}:
|
|
202
|
+
total, high = self._endpoint_support()
|
|
203
|
+
if high < _MIN_REST_ENDPOINTS_FOR_LABEL:
|
|
204
|
+
plural = "s" if total != 1 else ""
|
|
205
|
+
return (
|
|
206
|
+
f"{stack_label} application{fw_str} "
|
|
207
|
+
f"(HTTP framework present; only {total} endpoint{plural} "
|
|
208
|
+
f"detected — see `endpoints`)."
|
|
209
|
+
)
|
|
188
210
|
return f"{stack_label} {runtime.lower()}{fw_str}."
|
|
189
211
|
return f"{stack_label} project{fw_str}."
|
|
190
212
|
|
|
213
|
+
def _endpoint_support(self) -> tuple[int, int]:
|
|
214
|
+
"""Return (total, high_confidence) endpoint counts from the canonical
|
|
215
|
+
Java endpoint extractor — the same source the `endpoints` command uses,
|
|
216
|
+
so the summary cannot diverge from it. Cached; failure degrades to (0, 0)."""
|
|
217
|
+
if self._endpoint_support_cache is not None:
|
|
218
|
+
return self._endpoint_support_cache
|
|
219
|
+
total, high = 0, 0
|
|
220
|
+
try:
|
|
221
|
+
from sourcecode.repository_ir import extract_java_endpoints
|
|
222
|
+
data = extract_java_endpoints(self.root)
|
|
223
|
+
eps = data.get("endpoints", [])
|
|
224
|
+
total = data.get("total", len(eps))
|
|
225
|
+
high = sum(1 for e in eps if (e.get("confidence") or "high") == "high")
|
|
226
|
+
except Exception:
|
|
227
|
+
total, high = 0, 0
|
|
228
|
+
self._endpoint_support_cache = (total, high)
|
|
229
|
+
return self._endpoint_support_cache
|
|
230
|
+
|
|
231
|
+
def _qualify_web_pattern(self, arch: Any, arch_line: str, sm: SourceMap) -> str:
|
|
232
|
+
"""BUG #1 (Jenkins field test): the "mvc" pattern is a directory-name
|
|
233
|
+
heuristic (dirs matching controller/model/view keywords). On a Java/Kotlin
|
|
234
|
+
repo, do not assert "MVC pattern with ... view layers" in prose when the
|
|
235
|
+
canonical endpoint extractor finds no HTTP controllers in the same run —
|
|
236
|
+
Jenkins (Stapler web framework) matches model/view-ish dirs but declares
|
|
237
|
+
zero Spring MVC controllers. Cross-check against `endpoints` and degrade to
|
|
238
|
+
a consistent, non-committal phrasing instead of a categorical MVC claim."""
|
|
239
|
+
if not arch_line or arch.pattern not in ("mvc", "spring_mvc_layered"):
|
|
240
|
+
return arch_line
|
|
241
|
+
primary = next((s for s in sm.stacks if s.primary), sm.stacks[0] if sm.stacks else None)
|
|
242
|
+
if primary is None or primary.stack not in {"java", "kotlin"}:
|
|
243
|
+
return arch_line
|
|
244
|
+
_total, high = self._endpoint_support()
|
|
245
|
+
if high >= _MIN_REST_ENDPOINTS_FOR_LABEL:
|
|
246
|
+
return arch_line
|
|
247
|
+
# No controller backing → the MVC/view claim is unverified. Report the
|
|
248
|
+
# directory-derived layers without the framework label or the "view" claim.
|
|
249
|
+
layer_names = [l.name for l in arch.layers[:4] if l.name != "view"] if arch.layers else []
|
|
250
|
+
if layer_names:
|
|
251
|
+
return (
|
|
252
|
+
f"Layered code organization ({', '.join(layer_names)}; "
|
|
253
|
+
f"directory-based — no HTTP controllers detected)."
|
|
254
|
+
)
|
|
255
|
+
return ""
|
|
256
|
+
|
|
191
257
|
def _describe_arch_pattern(self, arch: Any) -> str:
|
|
192
258
|
pattern_labels = {
|
|
193
259
|
"clean": "Clean Architecture",
|
sourcecode/detectors/java.py
CHANGED
|
@@ -284,10 +284,43 @@ class JavaDetector(AbstractDetector):
|
|
|
284
284
|
if parent_artifact == "spring-boot-starter-parent":
|
|
285
285
|
sb_version = (parent_elem.findtext(f"{ns}version") or "").strip() or None
|
|
286
286
|
|
|
287
|
-
|
|
287
|
+
# BUG #1 (Jenkins field test): framework tokens must be matched only inside
|
|
288
|
+
# genuine dependency / plugin / parent coordinates, never anywhere in the
|
|
289
|
+
# serialized pom. Jenkins' war/pom.xml lists `<exclude>...:spring-web</exclude>`
|
|
290
|
+
# and `<exclude>...:spring-aop</exclude>` in an enforcer-plugin bytecode rule
|
|
291
|
+
# — those are exclusions of transitive artifacts, not declared dependencies.
|
|
292
|
+
# Serializing the whole tree let them read as "uses Spring MVC / Spring AOP".
|
|
293
|
+
# Restrict the scan to real dependency/plugin/parent artifact coordinates;
|
|
294
|
+
# `<exclusion>` and `<exclude>` elements carry neither.
|
|
295
|
+
text = self._pom_coordinate_text(root_elem, ns)
|
|
288
296
|
frameworks = self._detect_jvm_frameworks(text, "pom.xml", sb_version=sb_version)
|
|
289
297
|
return frameworks
|
|
290
298
|
|
|
299
|
+
def _pom_coordinate_text(self, root_elem: ElementTree.Element, ns: str) -> str:
|
|
300
|
+
"""Collect groupId:artifactId coordinates from real dependency, plugin, and
|
|
301
|
+
parent declarations only. Skips `<exclusion>`/`<exclude>` (transitive-artifact
|
|
302
|
+
removals) and comments, which must never be read as usage signal."""
|
|
303
|
+
coords: list[str] = []
|
|
304
|
+
|
|
305
|
+
def _coord(elem: ElementTree.Element) -> None:
|
|
306
|
+
gid = (elem.findtext(f"{ns}groupId") or "").strip()
|
|
307
|
+
aid = (elem.findtext(f"{ns}artifactId") or "").strip()
|
|
308
|
+
if aid:
|
|
309
|
+
coords.append(f"{gid}:{aid}" if gid else aid)
|
|
310
|
+
|
|
311
|
+
# <dependency> nodes anywhere (dependencies, dependencyManagement, profiles).
|
|
312
|
+
# ElementTree never yields <dependency> under <exclusions> (those are
|
|
313
|
+
# <exclusion>), so exclusion coordinates are structurally excluded.
|
|
314
|
+
for dep in root_elem.iter(f"{ns}dependency"):
|
|
315
|
+
_coord(dep)
|
|
316
|
+
for plugin in root_elem.iter(f"{ns}plugin"):
|
|
317
|
+
_coord(plugin)
|
|
318
|
+
parent_elem = root_elem.find(f"{ns}parent")
|
|
319
|
+
if parent_elem is not None:
|
|
320
|
+
_coord(parent_elem)
|
|
321
|
+
|
|
322
|
+
return "\n".join(coords).lower()
|
|
323
|
+
|
|
291
324
|
def _frameworks_from_gradle(self, path: Path) -> list[FrameworkDetection]:
|
|
292
325
|
original = "\n".join(read_text_lines(path))
|
|
293
326
|
content = original.lower()
|
|
@@ -361,7 +394,12 @@ class JavaDetector(AbstractDetector):
|
|
|
361
394
|
frameworks.append(FrameworkDetection(name="Thymeleaf", source=source))
|
|
362
395
|
if "freemarker" in text:
|
|
363
396
|
frameworks.append(FrameworkDetection(name="FreeMarker", source=source))
|
|
364
|
-
if
|
|
397
|
+
if (
|
|
398
|
+
"spring-boot-starter-security" in text
|
|
399
|
+
or "spring-security-core" in text
|
|
400
|
+
or "spring-security-web" in text
|
|
401
|
+
or "spring-security-config" in text
|
|
402
|
+
):
|
|
365
403
|
frameworks.append(FrameworkDetection(name="Spring Security", source=source))
|
|
366
404
|
if "spring-boot-starter-data-jpa" in text or "spring-data-jpa" in text:
|
|
367
405
|
frameworks.append(FrameworkDetection(name="Spring Data JPA", source=source))
|
sourcecode/hibernate_strat.py
CHANGED
|
@@ -262,15 +262,33 @@ def _scan_build_dependency(root: Path) -> tuple[bool, list[str]]:
|
|
|
262
262
|
# ≥ 6 the 5→6 axis does NOT apply (the migration is already done). When it cannot
|
|
263
263
|
# be resolved the axis degrades to a low-confidence hypothesis (no headline).
|
|
264
264
|
_HIB_VER = r"(\d+\.\d+(?:\.\d+)?(?:[.\-][\w]+)*)" # 6.2.13.Final, 5.6.15, 6.2
|
|
265
|
+
_HIB_VER_ANCHORED = re.compile(r"^" + _HIB_VER + r"$")
|
|
265
266
|
_HIB_PROP_RE = re.compile(
|
|
266
267
|
r"<((?:[\w.\-]*hibernate[\w.\-]*?)\.?version)>\s*" + _HIB_VER + r"\s*</\1>",
|
|
267
268
|
re.IGNORECASE,
|
|
268
269
|
)
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
270
|
+
# A Hibernate property is ONLY a proxy for the ORM version when it is not the
|
|
271
|
+
# version of a sibling Hibernate artifact (Search, Validator, Envers, OGM, …).
|
|
272
|
+
# Those ship on their own version line (e.g. hibernate-search 6.x on a
|
|
273
|
+
# hibernate-core 5.x project) and must never be mistaken for the ORM version.
|
|
274
|
+
# BUG #1 (v1.70.0): openmrs pinned hibernate-core to 5.6.15 via ${hibernateVersion}
|
|
275
|
+
# while ${hibernateSearchVersion}=6.2.4 — the old "newest wins" scan picked the
|
|
276
|
+
# Search version and declared the project already on Hibernate 6.
|
|
277
|
+
_HIB_NON_CORE_PROP_RE = re.compile(
|
|
278
|
+
r"search|validator|envers|ogm|reactive|spatial|jpamodelgen|"
|
|
279
|
+
r"jpa-?model|gradle|tool|commons|metamodel",
|
|
280
|
+
re.IGNORECASE,
|
|
281
|
+
)
|
|
282
|
+
# Anchor: capture the <version> declared inside the hibernate-core/orm
|
|
283
|
+
# <dependency> block (the value may be a literal or a ${property} reference).
|
|
284
|
+
_HIB_CORE_DEP_RE = re.compile(
|
|
285
|
+
r"<dependency>(?P<body>(?:(?!</dependency>).)*?"
|
|
286
|
+
r"<artifactId>\s*hibernate-(?:core|orm)\s*</artifactId>"
|
|
287
|
+
r"(?:(?!</dependency>).)*?)</dependency>",
|
|
272
288
|
re.IGNORECASE | re.DOTALL,
|
|
273
289
|
)
|
|
290
|
+
_VERSION_TAG_RE = re.compile(r"<version>\s*([^<]+?)\s*</version>", re.IGNORECASE)
|
|
291
|
+
_PROP_REF_RE = re.compile(r"^\$\{([^}]+)\}$")
|
|
274
292
|
_HIB_GRADLE_VERSION_RE = re.compile(
|
|
275
293
|
r"org\.hibernate(?:\.orm)?:hibernate-(?:core|orm):" + _HIB_VER,
|
|
276
294
|
re.IGNORECASE,
|
|
@@ -290,37 +308,88 @@ def _ver_key(full: str) -> tuple[int, int]:
|
|
|
290
308
|
return (major, minor)
|
|
291
309
|
|
|
292
310
|
|
|
311
|
+
def _lookup_maven_property(texts: list[str], name: str) -> Optional[str]:
|
|
312
|
+
"""Resolve a single Maven property by exact tag name across build files."""
|
|
313
|
+
pat = re.compile(
|
|
314
|
+
r"<" + re.escape(name) + r">\s*([^<]+?)\s*</" + re.escape(name) + r">",
|
|
315
|
+
re.IGNORECASE,
|
|
316
|
+
)
|
|
317
|
+
for t in texts:
|
|
318
|
+
m = pat.search(t)
|
|
319
|
+
if m:
|
|
320
|
+
return m.group(1).strip()
|
|
321
|
+
return None
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _resolve_version_expr(texts: list[str], expr: str, depth: int = 0) -> Optional[str]:
|
|
325
|
+
"""Resolve a <version> value that may be a literal or a ${property} chain."""
|
|
326
|
+
expr = expr.strip()
|
|
327
|
+
ref = _PROP_REF_RE.match(expr)
|
|
328
|
+
if ref:
|
|
329
|
+
if depth > 5:
|
|
330
|
+
return None
|
|
331
|
+
val = _lookup_maven_property(texts, ref.group(1))
|
|
332
|
+
if val is None:
|
|
333
|
+
return None
|
|
334
|
+
return _resolve_version_expr(texts, val, depth + 1)
|
|
335
|
+
return expr if _HIB_VER_ANCHORED.match(expr) else None
|
|
336
|
+
|
|
337
|
+
|
|
293
338
|
def _resolve_hibernate_version(root: Path) -> tuple[Optional[str], Optional[int], str]:
|
|
294
339
|
"""Return (full_version_string, major, confidence) for the effective Hibernate.
|
|
295
340
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
341
|
+
Resolution order (BUG #1 — never trust a same-named-but-different artifact):
|
|
342
|
+
1. The <version> anchored to the hibernate-core / hibernate-orm dependency
|
|
343
|
+
(literal, or a ${property} resolved to that specific property), and the
|
|
344
|
+
Gradle org.hibernate(.orm):hibernate-(core|orm) coordinate. HIGH conf.
|
|
345
|
+
2. Fallback: a <hibernate*version> property that is NOT a sibling-artifact
|
|
346
|
+
version (Search/Validator/Envers/…). HIGH conf.
|
|
347
|
+
3. None resolvable → degrade to hypothesis (confidence "none").
|
|
348
|
+
The newest qualifying version wins in a multi-module build.
|
|
299
349
|
"""
|
|
300
350
|
import os
|
|
301
|
-
|
|
351
|
+
texts: list[str] = []
|
|
302
352
|
for dirpath, dirnames, filenames in os.walk(root):
|
|
303
353
|
dirnames[:] = [d for d in dirnames if d not in _SKIP_BUILD_SCAN_DIRS]
|
|
304
354
|
for fname in filenames:
|
|
305
355
|
if fname not in _BUILD_FILE_NAMES:
|
|
306
356
|
continue
|
|
307
357
|
try:
|
|
308
|
-
|
|
358
|
+
texts.append(
|
|
359
|
+
(Path(dirpath) / fname).read_text(encoding="utf-8", errors="replace")
|
|
360
|
+
)
|
|
309
361
|
except OSError:
|
|
310
362
|
continue
|
|
311
|
-
|
|
312
|
-
for m in _HIB_PROP_RE.finditer(text):
|
|
313
|
-
fulls.append(m.group(2))
|
|
314
|
-
for m in _HIB_COORD_VERSION_RE.finditer(text):
|
|
315
|
-
fulls.append(m.group(1))
|
|
316
|
-
for m in _HIB_GRADLE_VERSION_RE.finditer(text):
|
|
317
|
-
fulls.append(m.group(1))
|
|
318
|
-
for full in fulls:
|
|
319
|
-
if best_full is None or _ver_key(full) > _ver_key(best_full):
|
|
320
|
-
best_full = full
|
|
321
|
-
if best_full is None:
|
|
363
|
+
if not texts:
|
|
322
364
|
return None, None, "none"
|
|
323
|
-
|
|
365
|
+
|
|
366
|
+
# 1. Anchored to the hibernate-core / hibernate-orm coordinate.
|
|
367
|
+
anchored: list[str] = []
|
|
368
|
+
for text in texts:
|
|
369
|
+
for dep in _HIB_CORE_DEP_RE.finditer(text):
|
|
370
|
+
vm = _VERSION_TAG_RE.search(dep.group("body"))
|
|
371
|
+
if vm:
|
|
372
|
+
resolved = _resolve_version_expr(texts, vm.group(1))
|
|
373
|
+
if resolved:
|
|
374
|
+
anchored.append(resolved)
|
|
375
|
+
for m in _HIB_GRADLE_VERSION_RE.finditer(text):
|
|
376
|
+
anchored.append(m.group(1))
|
|
377
|
+
if anchored:
|
|
378
|
+
best = max(anchored, key=_ver_key)
|
|
379
|
+
return best, _ver_key(best)[0], "high"
|
|
380
|
+
|
|
381
|
+
# 2. Fallback: a Hibernate *property* that is not a sibling-artifact version.
|
|
382
|
+
prop_cands: list[str] = []
|
|
383
|
+
for text in texts:
|
|
384
|
+
for m in _HIB_PROP_RE.finditer(text):
|
|
385
|
+
if _HIB_NON_CORE_PROP_RE.search(m.group(1)):
|
|
386
|
+
continue
|
|
387
|
+
prop_cands.append(m.group(2))
|
|
388
|
+
if prop_cands:
|
|
389
|
+
best = max(prop_cands, key=_ver_key)
|
|
390
|
+
return best, _ver_key(best)[0], "high"
|
|
391
|
+
|
|
392
|
+
return None, None, "none"
|
|
324
393
|
|
|
325
394
|
# Escalation markers — dynamic / reflection-based persistence construction.
|
|
326
395
|
_ABSTRACTION_CLASS_RE = re.compile(
|
|
@@ -495,7 +564,13 @@ class HibernateStratification:
|
|
|
495
564
|
"classification": self.classification,
|
|
496
565
|
"classification_label": self.classification_label,
|
|
497
566
|
"stratified": True,
|
|
498
|
-
"hibernate_readiness"
|
|
567
|
+
# BUG #1 (v1.70.0): renamed from "hibernate_readiness" to avoid a
|
|
568
|
+
# same-name contradiction with the document-level migrate-check field.
|
|
569
|
+
# This is the RAW 5→6 rewrite-zone readiness (independent of whether the
|
|
570
|
+
# axis applies). The authoritative, applicability-gated migration score
|
|
571
|
+
# lives at the document root as "hibernate_readiness"; consult that one
|
|
572
|
+
# for decisions. They diverge by design when the axis is N/A.
|
|
573
|
+
"rewrite_zone_readiness": self.readiness,
|
|
499
574
|
"risk_matrix": [r.to_dict() for r in self.risk_matrix],
|
|
500
575
|
"module_exposure_map": self.module_exposure,
|
|
501
576
|
"incompatible_patterns": self.incompatible_patterns,
|
|
@@ -58,6 +58,13 @@ _ATTR_URL_RE = re.compile(r'url\s*=\s*"([^"]*)"')
|
|
|
58
58
|
_ATTR_NAME_RE = re.compile(r'(?:name|value)\s*=\s*"([^"]*)"')
|
|
59
59
|
_FIRST_LITERAL_RE = re.compile(r'^\s*"([^"]*)"')
|
|
60
60
|
|
|
61
|
+
# BUG #5 (Jenkins field test): thresholds for the structured coverage_confidence
|
|
62
|
+
# signal. A repo at/above _LARGE_REPO_FILE_THRESHOLD source files with fewer than
|
|
63
|
+
# _LOW_COVERAGE_COUNT recognized integration constructs is flagged "low" coverage —
|
|
64
|
+
# the count almost certainly under-represents custom-protocol/SPI integrations.
|
|
65
|
+
_LARGE_REPO_FILE_THRESHOLD: int = 300
|
|
66
|
+
_LOW_COVERAGE_COUNT: int = 10
|
|
67
|
+
|
|
61
68
|
# token -> (kind, client). Matched as whole-word usage outside imports/comments.
|
|
62
69
|
# Covers Spring AND plain-Java/Jakarta stacks (Quarkus, Micronaut, Keycloak SPI):
|
|
63
70
|
# the detector must not be Spring-centric, or a non-Spring repo with real LDAP /
|
|
@@ -200,6 +207,27 @@ def detect_integrations(file_paths: "list[str]", root: Path) -> dict:
|
|
|
200
207
|
has_mail_import = bool(_MAIL_IMPORT_RE.search(text))
|
|
201
208
|
naming_factory = _classify_naming_factory(text)
|
|
202
209
|
|
|
210
|
+
# BUG #3 (v1.70.0): "HttpClient" is a simple name that collides with
|
|
211
|
+
# user-defined classes (e.g. org.openmrs.util.HttpClient, a thin wrapper over
|
|
212
|
+
# java.net.HttpURLConnection — a completely different API from the JDK 11+
|
|
213
|
+
# java.net.http.HttpClient). Resolve the JDK client by its FULLY-QUALIFIED
|
|
214
|
+
# import, never by the bare class name. When the file imports/declares a
|
|
215
|
+
# different HttpClient (or none can be resolved), degrade to a low-confidence
|
|
216
|
+
# "custom-http-wrapper" rather than asserting a JDK client that isn't there.
|
|
217
|
+
import_fqns = set(
|
|
218
|
+
re.findall(r"^\s*import\s+(?:static\s+)?([\w.]+)\s*;", text, re.MULTILINE)
|
|
219
|
+
)
|
|
220
|
+
http_jdk_imported = (
|
|
221
|
+
"java.net.http.HttpClient" in import_fqns or "java.net.http.*" in import_fqns
|
|
222
|
+
)
|
|
223
|
+
declares_own_httpclient = bool(
|
|
224
|
+
re.search(r"\b(?:class|interface|enum)\s+HttpClient\b", text)
|
|
225
|
+
)
|
|
226
|
+
http_other_import = any(
|
|
227
|
+
fqn.endswith(".HttpClient") and not fqn.startswith("java.net.http.")
|
|
228
|
+
for fqn in import_fqns
|
|
229
|
+
)
|
|
230
|
+
|
|
203
231
|
# Token clients — per line, skipping imports/package/comment noise.
|
|
204
232
|
# First pass records the declaration site and any variable name bound to
|
|
205
233
|
# the client, so a later call site (where the URL literal usually lives)
|
|
@@ -241,6 +269,27 @@ def detect_integrations(file_paths: "list[str]", root: Path) -> dict:
|
|
|
241
269
|
kind, client, confidence = (
|
|
242
270
|
"naming-directory-unknown", "jndi-dircontext", "low",
|
|
243
271
|
)
|
|
272
|
+
# BUG #3: resolve the ambiguous bare "HttpClient" by its import, and
|
|
273
|
+
# suppress pure type-declaration sites (field / parameter / return
|
|
274
|
+
# type) — only a construction or static call is a real network site.
|
|
275
|
+
if client == "jdk-httpclient":
|
|
276
|
+
if not (http_jdk_imported and not declares_own_httpclient):
|
|
277
|
+
# Not the JDK client (own class, third-party, or unresolvable).
|
|
278
|
+
client, confidence = "custom-http-wrapper", "low"
|
|
279
|
+
if declares_own_httpclient or http_other_import:
|
|
280
|
+
confidence = "low"
|
|
281
|
+
is_construction = bool(
|
|
282
|
+
re.search(r"\bnew\s+HttpClient\b", line)
|
|
283
|
+
) or bool(re.search(r"\bHttpClient\s*\.", line))
|
|
284
|
+
if not is_construction:
|
|
285
|
+
# Type-declaration only (e.g. `HttpClient field;`,
|
|
286
|
+
# `void setX(HttpClient c)`): track the var for the URL
|
|
287
|
+
# second pass but do NOT emit it as an invocation site.
|
|
288
|
+
tok = m.group(0)
|
|
289
|
+
decl = re.search(re.escape(tok) + r"\s+(\w+)\b", line)
|
|
290
|
+
if decl:
|
|
291
|
+
var_to_client[decl.group(1)] = (kind, client)
|
|
292
|
+
continue
|
|
244
293
|
_add(kind, client, _extract_target(line), rel, lineno, confidence)
|
|
245
294
|
tok = m.group(0)
|
|
246
295
|
# `Type name` (field/local decl) and `name = new Type(` forms.
|
|
@@ -274,11 +323,50 @@ def detect_integrations(file_paths: "list[str]", root: Path) -> dict:
|
|
|
274
323
|
# clients (DI, config-driven endpoints, JCA connectors) are invisible to static
|
|
275
324
|
# text matching. Report that explicitly instead of an authoritative "0".
|
|
276
325
|
confidence = "observed" if records else "not_analyzed"
|
|
326
|
+
|
|
327
|
+
# BUG #5 (Jenkins field test): the coverage caveat lived only inside the prose
|
|
328
|
+
# `coverage_note`. Propagate it as a STRUCTURED signal so an automated consumer
|
|
329
|
+
# does not read a low `count` as "low external coupling". A large repo with few
|
|
330
|
+
# recognized constructs (Jenkins: custom remoting/SCM/Update-Center SPIs, not
|
|
331
|
+
# RestTemplate/WebClient) is under-counted, not loosely coupled.
|
|
332
|
+
_repo_size = len(file_paths)
|
|
333
|
+
_large_repo = _repo_size >= _LARGE_REPO_FILE_THRESHOLD
|
|
334
|
+
if not records:
|
|
335
|
+
coverage_confidence = "low"
|
|
336
|
+
_cov_reason = (
|
|
337
|
+
"no recognized client-library construct found; custom protocol/SPI-based "
|
|
338
|
+
"or DI-wired integrations are not statically detectable by this analyzer. "
|
|
339
|
+
"A count of 0 does not imply the system has no outbound integrations."
|
|
340
|
+
)
|
|
341
|
+
elif _large_repo and len(records) < _LOW_COVERAGE_COUNT:
|
|
342
|
+
coverage_confidence = "low"
|
|
343
|
+
_cov_reason = (
|
|
344
|
+
f"only {len(records)} recognized construct(s) across {_repo_size} source "
|
|
345
|
+
"files; count reflects only recognized client-library patterns "
|
|
346
|
+
"(RestTemplate/WebClient/JDK/Apache/OkHttp/LDAP/JMS/…). Custom protocol/"
|
|
347
|
+
"SPI-based integrations are likely under-counted — do not read a low count "
|
|
348
|
+
"as low external coupling."
|
|
349
|
+
)
|
|
350
|
+
elif len(records) < _LOW_COVERAGE_COUNT:
|
|
351
|
+
coverage_confidence = "partial"
|
|
352
|
+
_cov_reason = (
|
|
353
|
+
"recognized client-library constructs detected; custom protocol/SPI-based "
|
|
354
|
+
"or DI-wired integrations, if any, are not statically visible."
|
|
355
|
+
)
|
|
356
|
+
else:
|
|
357
|
+
coverage_confidence = "high"
|
|
358
|
+
_cov_reason = (
|
|
359
|
+
"recognized client-library constructs cover the observable integration "
|
|
360
|
+
"surface; runtime/DI-wired clients remain out of static scope."
|
|
361
|
+
)
|
|
362
|
+
|
|
277
363
|
return {
|
|
278
364
|
"integrations": records,
|
|
279
365
|
"by_kind": {k: by_kind[k] for k in sorted(by_kind)},
|
|
280
366
|
"count": len(records),
|
|
281
367
|
"confidence": confidence,
|
|
368
|
+
"coverage_confidence": coverage_confidence,
|
|
369
|
+
"coverage_confidence_reason": _cov_reason,
|
|
282
370
|
"coverage_note": (
|
|
283
371
|
"Detects HTTP (RestTemplate/WebClient/JDK/Apache/OkHttp), LDAP (Spring "
|
|
284
372
|
"+ JNDI), DNS (JNDI DirContext w/ DnsContextFactory), SMTP (JavaMail, "
|
sourcecode/migrate_check.py
CHANGED
|
@@ -652,6 +652,13 @@ def _classify_code_context(finding: "MigrationFinding") -> str:
|
|
|
652
652
|
imp.rsplit(".", 1)[-1] == "Generated" for imp in finding.imports_found
|
|
653
653
|
):
|
|
654
654
|
return "generated"
|
|
655
|
+
# BUG #3 (Jenkins field test): a namespace-migration finding (javax→jakarta,
|
|
656
|
+
# e.g. javax.servlet) on a class-level @Deprecated type is a frozen-legacy
|
|
657
|
+
# compatibility shim — kept for binary compat, with a live replacement elsewhere
|
|
658
|
+
# (Jenkins ChainedServletFilter → ChainedServletFilter2). Not an active blocker:
|
|
659
|
+
# segregate it out of blocking_count like test/generated, with its own label.
|
|
660
|
+
if finding.enclosing_deprecated and finding.migration_target in _JAKARTA_TARGETS:
|
|
661
|
+
return "deprecated_shim"
|
|
655
662
|
return "main"
|
|
656
663
|
|
|
657
664
|
|
|
@@ -1181,6 +1188,11 @@ class MigrationFinding:
|
|
|
1181
1188
|
# harness), or "generated" (autogenerated source). Only "main" counts toward
|
|
1182
1189
|
# blocking_count / readiness; test+generated are reported in a separate bucket.
|
|
1183
1190
|
code_context: str = "main"
|
|
1191
|
+
# BUG #3 (Jenkins field test): the enclosing top-level TYPE carries a class-level
|
|
1192
|
+
# @Deprecated annotation. A namespace-migration finding on such a class is a
|
|
1193
|
+
# frozen-legacy compatibility shim (kept for binary compat, replaced elsewhere),
|
|
1194
|
+
# not an active migration blocker — reclassified out of blocking_count.
|
|
1195
|
+
enclosing_deprecated: bool = False
|
|
1184
1196
|
|
|
1185
1197
|
@staticmethod
|
|
1186
1198
|
def make_id(rule_id: str, source_file: str) -> str:
|
|
@@ -1287,12 +1299,19 @@ class MigrationReport:
|
|
|
1287
1299
|
readiness_aggregate: dict = field(default_factory=dict)
|
|
1288
1300
|
blocking_count: int = 0
|
|
1289
1301
|
estimated_effort_days: float = 0.0
|
|
1302
|
+
effort_breakdown: dict = field(default_factory=dict)
|
|
1290
1303
|
# Tri-state: True = Boot 2 confirmed, False = Boot 3+ confirmed,
|
|
1291
1304
|
# None = could not determine. Absence of evidence is never reported as True.
|
|
1292
1305
|
spring_boot_2_detected: Optional[bool] = None
|
|
1293
1306
|
spring_boot_version_detected: Optional[str] = None
|
|
1294
1307
|
# BUG #2: whether Spring is used AT RUNTIME. The Boot3 dimension is N/A without it.
|
|
1295
1308
|
spring_present: bool = True
|
|
1309
|
+
# BUG #2 (Jenkins field test): whether the repo is a Spring BOOT application
|
|
1310
|
+
# (spring-boot* coordinate / @SpringBootApplication), as opposed to merely having
|
|
1311
|
+
# a Spring library (e.g. spring-security-web) on the classpath. The boot3 (Boot
|
|
1312
|
+
# 2→3) migration dimension is applicable ONLY to Boot repos — a non-Boot Spring
|
|
1313
|
+
# repo has no Boot upgrade axis and must not receive a contaminated readiness.
|
|
1314
|
+
spring_boot_present: bool = False
|
|
1296
1315
|
# BUG #1/#2: Spring appears ONLY as a test dependency (e.g. spring-test). Reported
|
|
1297
1316
|
# so a consumer can see WHY boot3 is N/A despite an org.springframework coordinate.
|
|
1298
1317
|
spring_test_only: bool = False
|
|
@@ -1365,7 +1384,19 @@ class MigrationReport:
|
|
|
1365
1384
|
self.jakarta_readiness = _dimension_score(main_findings, _JAKARTA_TARGETS)
|
|
1366
1385
|
self.boot3_readiness = _dimension_score(main_findings, _BOOT3_MIGRATION_TARGETS)
|
|
1367
1386
|
self.jdk_modernization = _dimension_score(main_findings, None)
|
|
1368
|
-
|
|
1387
|
+
# BUG #2 (Jenkins field test): the boot3 axis applies only with POSITIVE
|
|
1388
|
+
# evidence — a Spring BOOT repo (spring-boot* coordinate / @SpringBootApplication),
|
|
1389
|
+
# OR a concrete Boot-3 / Security-6 migration finding in main code. A repo that
|
|
1390
|
+
# merely carries a Spring library (spring-security-web) with no Boot and no
|
|
1391
|
+
# Boot/Security migration findings (Jenkins) has no boot3 axis — jakarta-shaped
|
|
1392
|
+
# findings must not masquerade as a boot3 readiness deficit. Note: jakarta
|
|
1393
|
+
# findings do NOT make boot3 applicable; only genuine boot3/security-6 targets do.
|
|
1394
|
+
_boot3_migration_findings = any(
|
|
1395
|
+
f.migration_target in (_BOOT3_MIGRATION_TARGETS - _JAKARTA_TARGETS)
|
|
1396
|
+
for f in main_findings
|
|
1397
|
+
)
|
|
1398
|
+
_boot3_applies = self.spring_boot_present or _boot3_migration_findings
|
|
1399
|
+
if not _boot3_applies:
|
|
1369
1400
|
self.boot3_readiness = 100
|
|
1370
1401
|
|
|
1371
1402
|
# BUG #3/#4: the jakarta dimension is applicable ONLY with POSITIVE evidence the
|
|
@@ -1441,14 +1472,23 @@ class MigrationReport:
|
|
|
1441
1472
|
else "N/A — no migratable javax.* imports detected"),
|
|
1442
1473
|
},
|
|
1443
1474
|
"boot3": {
|
|
1444
|
-
"applicable":
|
|
1445
|
-
"score": self.boot3_readiness if
|
|
1446
|
-
"reason": (
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1475
|
+
"applicable": _boot3_applies,
|
|
1476
|
+
"score": self.boot3_readiness if _boot3_applies else None,
|
|
1477
|
+
"reason": (
|
|
1478
|
+
"Spring Boot 2→3 / Security 6 migration"
|
|
1479
|
+
if self.spring_boot_present
|
|
1480
|
+
else "Spring Security 6 migration (spring_security_6 / spring_boot_3 "
|
|
1481
|
+
"findings present) — no Spring Boot detected"
|
|
1482
|
+
if _boot3_applies
|
|
1483
|
+
else "N/A — Spring present only as a TEST dependency (spring-test); "
|
|
1484
|
+
"no runtime Spring, no Spring Boot"
|
|
1485
|
+
if self.spring_test_only
|
|
1486
|
+
else "N/A — Spring library on the classpath but no spring-boot "
|
|
1487
|
+
"dependency / @SpringBootApplication and no Boot/Security-6 "
|
|
1488
|
+
"migration finding — not a Spring Boot application"
|
|
1489
|
+
if self.spring_present
|
|
1490
|
+
else "N/A — no Spring usage detected (non-Spring stack)"
|
|
1491
|
+
),
|
|
1452
1492
|
},
|
|
1453
1493
|
"jdk_modernization": {"applicable": True, "score": self.jdk_modernization,
|
|
1454
1494
|
"reason": "orthogonal JDK modernization debt (excluded from "
|
|
@@ -1499,13 +1539,32 @@ class MigrationReport:
|
|
|
1499
1539
|
|
|
1500
1540
|
# BUG #5: effort over MAIN findings only — N/A axes (Hibernate-6 phantom,
|
|
1501
1541
|
# test fixtures) no longer pad the estimate.
|
|
1502
|
-
|
|
1542
|
+
_file_effort = (
|
|
1503
1543
|
len(critical_files) * 0.5
|
|
1504
1544
|
+ len(high_files) * 0.25
|
|
1505
1545
|
+ len(medium_files) * 0.1
|
|
1506
|
-
+ len(low_files) * 0.05
|
|
1507
|
-
1,
|
|
1546
|
+
+ len(low_files) * 0.05
|
|
1508
1547
|
)
|
|
1548
|
+
# BUG #1 (v1.70.0): when the Hibernate 5→6 axis APPLIES, fold its measured
|
|
1549
|
+
# rewrite effort (risk_matrix → total_effort_range_days) into the headline
|
|
1550
|
+
# estimate. Previously a Hibernate-5 project whose ${hibernateVersion} was
|
|
1551
|
+
# misread as 6 set migration_applicable=False, which silently DROPPED this
|
|
1552
|
+
# 28.9–95.6 person-day range from estimated_effort_days, under-reporting the
|
|
1553
|
+
# real cost 1.5–2.6×. We add the range midpoint and expose the breakdown.
|
|
1554
|
+
_hib_effort = 0.0
|
|
1555
|
+
if _hibernate_applies and hib is not None:
|
|
1556
|
+
_r = hib.total_effort_range_days or {}
|
|
1557
|
+
_lo, _hi = _r.get("low"), _r.get("high")
|
|
1558
|
+
if isinstance(_lo, (int, float)) and isinstance(_hi, (int, float)):
|
|
1559
|
+
_hib_effort = (float(_lo) + float(_hi)) / 2.0
|
|
1560
|
+
self.estimated_effort_days = round(_file_effort + _hib_effort, 1)
|
|
1561
|
+
self.effort_breakdown = {
|
|
1562
|
+
"findings_effort_days": round(_file_effort, 1),
|
|
1563
|
+
"hibernate_rewrite_effort_days": round(_hib_effort, 1),
|
|
1564
|
+
"hibernate_rewrite_range": (
|
|
1565
|
+
hib.total_effort_range_days if (_hibernate_applies and hib is not None) else None
|
|
1566
|
+
),
|
|
1567
|
+
}
|
|
1509
1568
|
|
|
1510
1569
|
# BUG #6 / #8: hygiene + non-blocking buckets, surfaced separately.
|
|
1511
1570
|
self.hygiene_findings = sum(
|
|
@@ -1522,8 +1581,11 @@ class MigrationReport:
|
|
|
1522
1581
|
"count": sum(_nb_by_ctx.values()),
|
|
1523
1582
|
"by_context": _nb_by_ctx,
|
|
1524
1583
|
"by_rule": _nb_by_rule,
|
|
1525
|
-
"note": ("Findings in test/fixture harnesses
|
|
1526
|
-
"
|
|
1584
|
+
"note": ("Findings in test/fixture harnesses, autogenerated sources, or "
|
|
1585
|
+
"class-level @Deprecated frozen-legacy shims (code_context="
|
|
1586
|
+
"\"deprecated_shim\" — kept for binary compat with a live "
|
|
1587
|
+
"replacement; verify before removing). Excluded from "
|
|
1588
|
+
"blocking_count, readiness, and effort."),
|
|
1527
1589
|
}
|
|
1528
1590
|
|
|
1529
1591
|
self.summary = {
|
|
@@ -1561,9 +1623,11 @@ class MigrationReport:
|
|
|
1561
1623
|
"headline_blocker": self.headline_blocker,
|
|
1562
1624
|
"blocking_count": self.blocking_count,
|
|
1563
1625
|
"estimated_effort_days": self.estimated_effort_days,
|
|
1626
|
+
"effort_breakdown": self.effort_breakdown,
|
|
1564
1627
|
"hygiene_findings": self.hygiene_findings,
|
|
1565
1628
|
"non_blocking": self.non_blocking,
|
|
1566
1629
|
"spring_present": self.spring_present,
|
|
1630
|
+
"spring_boot_present": self.spring_boot_present,
|
|
1567
1631
|
"spring_test_only": self.spring_test_only,
|
|
1568
1632
|
"spring_boot_2_detected": self.spring_boot_2_detected,
|
|
1569
1633
|
"spring_boot_version_detected": self.spring_boot_version_detected,
|
|
@@ -1725,12 +1789,35 @@ def _neutralize_non_spring_explanation(text: str) -> str:
|
|
|
1725
1789
|
return text
|
|
1726
1790
|
|
|
1727
1791
|
|
|
1792
|
+
# BUG #3 (Jenkins field test): the primary top-level type declaration in a .java
|
|
1793
|
+
# file. Used to test whether a class-level @Deprecated annotation precedes it.
|
|
1794
|
+
_PRIMARY_TYPE_DECL_RE: re.Pattern = re.compile(
|
|
1795
|
+
r"(?m)^[ \t]*(?:public\s+|final\s+|abstract\s+|sealed\s+|non-sealed\s+|strictfp\s+)*"
|
|
1796
|
+
r"(?:class|interface|enum|record)\s+\w"
|
|
1797
|
+
)
|
|
1798
|
+
|
|
1799
|
+
|
|
1800
|
+
def _primary_type_is_deprecated(source: str) -> bool:
|
|
1801
|
+
"""True iff the file's primary top-level type carries a class-level @Deprecated
|
|
1802
|
+
annotation. Scoped to the region between the last import and the type keyword so
|
|
1803
|
+
a @Deprecated on a METHOD (below the class declaration) never counts — that is a
|
|
1804
|
+
live class with a deprecated member, not a frozen-legacy shim."""
|
|
1805
|
+
m = _PRIMARY_TYPE_DECL_RE.search(source)
|
|
1806
|
+
if not m:
|
|
1807
|
+
return False
|
|
1808
|
+
head = source[: m.start()]
|
|
1809
|
+
last_import = head.rfind("\nimport ")
|
|
1810
|
+
window = head[last_import:] if last_import != -1 else head
|
|
1811
|
+
return "@Deprecated" in window
|
|
1812
|
+
|
|
1813
|
+
|
|
1728
1814
|
def _scan_file(
|
|
1729
1815
|
source: str,
|
|
1730
1816
|
rel_path: str,
|
|
1731
1817
|
rules: list[_Rule],
|
|
1732
1818
|
) -> list[MigrationFinding]:
|
|
1733
1819
|
findings: list[MigrationFinding] = []
|
|
1820
|
+
type_deprecated = _primary_type_is_deprecated(source)
|
|
1734
1821
|
|
|
1735
1822
|
for rule in rules:
|
|
1736
1823
|
matched_imports: list[str] = []
|
|
@@ -1799,6 +1886,7 @@ def _scan_file(
|
|
|
1799
1886
|
fix_hint=rule.fix_hint,
|
|
1800
1887
|
migration_target=rule.migration_target,
|
|
1801
1888
|
openrewrite_recipe=rule.openrewrite_recipe,
|
|
1889
|
+
enclosing_deprecated=type_deprecated,
|
|
1802
1890
|
)
|
|
1803
1891
|
)
|
|
1804
1892
|
|
|
@@ -1920,6 +2008,7 @@ def run_migrate_check(
|
|
|
1920
2008
|
spring_present, spring_test_only = _detect_spring_usage(
|
|
1921
2009
|
root, spring_runtime_import_seen, spring_test_import_seen
|
|
1922
2010
|
)
|
|
2011
|
+
spring_boot_present = _detect_spring_boot_present(root)
|
|
1923
2012
|
|
|
1924
2013
|
# BUG #8: classify each finding's code context (main / test / generated) so the
|
|
1925
2014
|
# report can keep test fixtures and autogenerated sources out of blocking_count.
|
|
@@ -1944,6 +2033,7 @@ def run_migrate_check(
|
|
|
1944
2033
|
spring_boot_2_detected=spring_boot_2,
|
|
1945
2034
|
spring_boot_version_detected=spring_boot_version,
|
|
1946
2035
|
spring_present=spring_present,
|
|
2036
|
+
spring_boot_present=spring_boot_present,
|
|
1947
2037
|
spring_test_only=spring_test_only,
|
|
1948
2038
|
jakarta_namespace_adopted=jakarta_import_count > 0,
|
|
1949
2039
|
findings=all_findings,
|
|
@@ -2044,6 +2134,46 @@ def _extract_boot_versions(text: str) -> tuple[set[int], Optional[str]]:
|
|
|
2044
2134
|
return majors, full
|
|
2045
2135
|
|
|
2046
2136
|
|
|
2137
|
+
# BUG #2 (Jenkins field test): strip XML comments and exclusion/exclude blocks
|
|
2138
|
+
# before scanning for a spring-boot coordinate, so a `<exclude>...:spring-boot</…>`
|
|
2139
|
+
# or a comment never reads as "this repo uses Spring Boot" — the same exclusion
|
|
2140
|
+
# poison that produced phantom Spring MVC/AOP in framework detection.
|
|
2141
|
+
_XML_COMMENT_RE: re.Pattern = re.compile(r"<!--.*?-->", re.DOTALL)
|
|
2142
|
+
_XML_EXCLUSION_EL_RE: re.Pattern = re.compile(r"<exclusion\b.*?</exclusion\s*>", re.DOTALL | re.IGNORECASE)
|
|
2143
|
+
_XML_EXCLUDE_EL_RE: re.Pattern = re.compile(r"<exclude\b.*?</exclude\s*>", re.DOTALL | re.IGNORECASE)
|
|
2144
|
+
# A spring-boot* Maven/Gradle artifact coordinate (starter, BOM, dependency, plugin).
|
|
2145
|
+
_SPRING_BOOT_ARTIFACT_RE: re.Pattern = re.compile(r"\bspring-boot(?:-[a-z0-9.\-]+)?\b", re.IGNORECASE)
|
|
2146
|
+
|
|
2147
|
+
|
|
2148
|
+
def _strip_xml_exclusions(text: str) -> str:
|
|
2149
|
+
"""Remove XML comments and exclusion/exclude elements so their coordinates are
|
|
2150
|
+
not mistaken for declared dependencies."""
|
|
2151
|
+
text = _XML_COMMENT_RE.sub(" ", text)
|
|
2152
|
+
text = _XML_EXCLUSION_EL_RE.sub(" ", text)
|
|
2153
|
+
text = _XML_EXCLUDE_EL_RE.sub(" ", text)
|
|
2154
|
+
return text
|
|
2155
|
+
|
|
2156
|
+
|
|
2157
|
+
def _detect_spring_boot_present(root: Path) -> bool:
|
|
2158
|
+
"""True iff there is POSITIVE Spring Boot evidence — a spring-boot* build
|
|
2159
|
+
coordinate (parent / BOM / dependency / plugin) or the Spring Boot Gradle
|
|
2160
|
+
plugin. Distinguishes a Spring Boot application from a repo that merely has a
|
|
2161
|
+
Spring library on the classpath (e.g. Jenkins' spring-security-web). Gates the
|
|
2162
|
+
boot3 (Boot 2→3) migration dimension. @SpringBootApplication is not scanned
|
|
2163
|
+
separately: its annotation class ships in spring-boot-autoconfigure, so a Boot
|
|
2164
|
+
app always carries a spring-boot* coordinate."""
|
|
2165
|
+
for abs_path, _rel in _find_build_files(root):
|
|
2166
|
+
try:
|
|
2167
|
+
text = abs_path.read_text(encoding="utf-8", errors="replace")
|
|
2168
|
+
except OSError:
|
|
2169
|
+
continue
|
|
2170
|
+
if _SPRING_BOOT_PLUGIN_RE.search(text):
|
|
2171
|
+
return True
|
|
2172
|
+
if _SPRING_BOOT_ARTIFACT_RE.search(_strip_xml_exclusions(text)):
|
|
2173
|
+
return True
|
|
2174
|
+
return False
|
|
2175
|
+
|
|
2176
|
+
|
|
2047
2177
|
def _detect_spring_boot(root: Path, jakarta_import_count: int) -> tuple[Optional[bool], Optional[str]]:
|
|
2048
2178
|
"""Tri-state Spring Boot 2 detection. Returns (spring_boot_2_detected, version).
|
|
2049
2179
|
|
sourcecode/repository_ir.py
CHANGED
|
@@ -2872,12 +2872,35 @@ def _assemble(
|
|
|
2872
2872
|
else:
|
|
2873
2873
|
_security_model_asm = "unknown"
|
|
2874
2874
|
|
|
2875
|
+
# BUG #4 (Jenkins field test): the bare "unknown" string carried no reason or
|
|
2876
|
+
# sub-structure — inconsistent with the not_detected+reason pattern used
|
|
2877
|
+
# elsewhere (migrate-check hibernate). Emit a structured companion so a consumer
|
|
2878
|
+
# sees WHY, and never reads "unknown" as "unsecured". A repo with a custom
|
|
2879
|
+
# security SPI (Jenkins ACL/SecurityRealm) simply matched no RECOGNIZED pattern.
|
|
2880
|
+
_security_model_detail_asm = {
|
|
2881
|
+
"model": _security_model_asm,
|
|
2882
|
+
"status": "detected" if _security_model_asm != "unknown" else "not_detected",
|
|
2883
|
+
"spring_security_filter_chain_detected": _filter_based_asm,
|
|
2884
|
+
"annotation_policies_detected": _has_ann_sec_asm,
|
|
2885
|
+
"custom_spi_detected": [],
|
|
2886
|
+
"reason": (
|
|
2887
|
+
"recognized security model: " + _security_model_asm
|
|
2888
|
+
if _security_model_asm != "unknown"
|
|
2889
|
+
else "N/A — no Spring Security filter chain, no annotation-based policy, "
|
|
2890
|
+
"and no recognized custom security SPI matched. Absence of a "
|
|
2891
|
+
"recognized pattern is not evidence the repo is unsecured — it may "
|
|
2892
|
+
"use a framework-specific or custom access-control model this "
|
|
2893
|
+
"analyzer does not model."
|
|
2894
|
+
),
|
|
2895
|
+
}
|
|
2896
|
+
|
|
2875
2897
|
return {
|
|
2876
2898
|
**_base,
|
|
2877
2899
|
"route_surface": _route_surface,
|
|
2878
2900
|
"spring_events": _spring_events,
|
|
2879
2901
|
"analysis_gaps": _analysis_gaps,
|
|
2880
2902
|
"security_model": _security_model_asm,
|
|
2903
|
+
"security_model_detail": _security_model_detail_asm,
|
|
2881
2904
|
"audit": {
|
|
2882
2905
|
"dropped_fields": dropped_fields,
|
|
2883
2906
|
},
|
|
@@ -3510,6 +3533,15 @@ def build_repo_ir(
|
|
|
3510
3533
|
ir["security_model"] = "xml_or_filter_chain"
|
|
3511
3534
|
elif _sec_model in ("annotation_based", "mixed"):
|
|
3512
3535
|
ir["security_model"] = "mixed"
|
|
3536
|
+
# BUG #4: keep the structured detail consistent with the rewritten string.
|
|
3537
|
+
_detail = ir.get("security_model_detail")
|
|
3538
|
+
if isinstance(_detail, dict):
|
|
3539
|
+
_detail["model"] = ir["security_model"]
|
|
3540
|
+
_detail["status"] = "detected"
|
|
3541
|
+
_detail["spring_security_filter_chain_detected"] = True
|
|
3542
|
+
_detail["reason"] = ("XML/filter-chain security configuration detected "
|
|
3543
|
+
"(declarative filter mapping in web.xml or Spring "
|
|
3544
|
+
"security XML)")
|
|
3513
3545
|
# Retag route_surface entries that have no security (would become none_detected in CIR)
|
|
3514
3546
|
for _r in ir.get("route_surface") or []:
|
|
3515
3547
|
_r_sec = _r.get("security_annotations")
|
sourcecode/spring_impact.py
CHANGED
|
@@ -121,6 +121,8 @@ class ImpactChainResult:
|
|
|
121
121
|
resolution: str = "not_found" # "exact" | "class_expanded" | "partial" | "not_found"
|
|
122
122
|
direct_callers: list[str] = field(default_factory=list)
|
|
123
123
|
indirect_callers: list[str] = field(default_factory=list)
|
|
124
|
+
# BUG #2: count of own-class members dropped from callers (members, not callers).
|
|
125
|
+
self_referential_excluded: int = 0
|
|
124
126
|
implementations: list[str] = field(default_factory=list) # in-repo subtypes of queried interface/base
|
|
125
127
|
endpoints_affected: list[AffectedEndpoint] = field(default_factory=list)
|
|
126
128
|
transaction_boundary: Optional[dict] = None # TransactionBoundary.to_dict() or None
|
|
@@ -144,6 +146,7 @@ class ImpactChainResult:
|
|
|
144
146
|
"resolution": self.resolution,
|
|
145
147
|
"direct_callers": self.direct_callers,
|
|
146
148
|
"indirect_callers": self.indirect_callers,
|
|
149
|
+
"self_referential_excluded": self.self_referential_excluded,
|
|
147
150
|
"implementations": self.implementations,
|
|
148
151
|
"endpoints_affected": [ep.to_dict() for ep in self.endpoints_affected],
|
|
149
152
|
"transaction_boundary": self.transaction_boundary,
|
|
@@ -471,6 +474,15 @@ def _bfs_callers(
|
|
|
471
474
|
indirect: list[str] = []
|
|
472
475
|
was_truncated = False
|
|
473
476
|
|
|
477
|
+
# BUG #2 (v1.70.0): a class's OWN members are not "callers" of that class — they
|
|
478
|
+
# are members. When the seed is a class node, _edges_for folds in every method
|
|
479
|
+
# key of that class, so internal method→method calls (e.g.
|
|
480
|
+
# ConceptServiceImpl#purgeConcept → ConceptServiceImpl#saveConcept) were leaking
|
|
481
|
+
# into direct_callers and inflating the blast radius ~12×. Exclude any caller
|
|
482
|
+
# whose owning class is one of the seed classes.
|
|
483
|
+
seed_classes: set[str] = {normalize_owner_fqn(s) for s in seed_fqns}
|
|
484
|
+
self_excluded: int = 0
|
|
485
|
+
|
|
474
486
|
# BUG-004: index class FQN → list of method-level keys in reverse_graph.
|
|
475
487
|
# Callers of Foo#doWork are stored under reverse_graph["Foo#doWork"], never
|
|
476
488
|
# under reverse_graph["Foo"]. Without this index, BFS silently terminates
|
|
@@ -506,7 +518,9 @@ def _bfs_callers(
|
|
|
506
518
|
for seed in seed_fqns:
|
|
507
519
|
for etype, fqn_list in _edges_for(seed):
|
|
508
520
|
if etype not in _SKIP_EDGE_TYPES:
|
|
509
|
-
unique_direct_callers.update(
|
|
521
|
+
unique_direct_callers.update(
|
|
522
|
+
c for c in fqn_list if normalize_owner_fqn(c) not in seed_classes
|
|
523
|
+
)
|
|
510
524
|
|
|
511
525
|
effective_depth = 1 if len(unique_direct_callers) > _BFS_CALLER_CAP else max_depth
|
|
512
526
|
if effective_depth < max_depth:
|
|
@@ -516,8 +530,14 @@ def _bfs_callers(
|
|
|
516
530
|
queue: list[tuple[str, int]] = [(s, 0) for s in seed_fqns]
|
|
517
531
|
|
|
518
532
|
def _add_caller(caller: str, depth: int) -> None:
|
|
533
|
+
nonlocal self_excluded
|
|
519
534
|
if caller in visited:
|
|
520
535
|
return
|
|
536
|
+
# BUG #2: a member of a seed class is not an external caller — drop it.
|
|
537
|
+
if normalize_owner_fqn(caller) in seed_classes:
|
|
538
|
+
visited.add(caller)
|
|
539
|
+
self_excluded += 1
|
|
540
|
+
return
|
|
521
541
|
visited.add(caller)
|
|
522
542
|
if depth == 0:
|
|
523
543
|
direct.append(caller)
|
|
@@ -542,7 +562,7 @@ def _bfs_callers(
|
|
|
542
562
|
else:
|
|
543
563
|
_add_caller(caller, depth)
|
|
544
564
|
|
|
545
|
-
return direct, indirect, was_truncated
|
|
565
|
+
return direct, indirect, was_truncated, self_excluded
|
|
546
566
|
|
|
547
567
|
|
|
548
568
|
# ---------------------------------------------------------------------------
|
|
@@ -888,9 +908,15 @@ class ImpactOrchestrator:
|
|
|
888
908
|
)
|
|
889
909
|
|
|
890
910
|
# ── 2. BFS through reverse graph ─────────────────────────────────
|
|
891
|
-
direct_callers, indirect_callers, truncated = _bfs_callers(
|
|
911
|
+
direct_callers, indirect_callers, truncated, self_excluded = _bfs_callers(
|
|
892
912
|
seed_fqns, cir.reverse_graph, depth, impl_graph=impl_graph
|
|
893
913
|
)
|
|
914
|
+
if self_excluded:
|
|
915
|
+
warnings.append(
|
|
916
|
+
f"Self-referential exclusion (BUG #2): {self_excluded} member(s) of the "
|
|
917
|
+
f"analyzed class were dropped from callers — a class's own methods are "
|
|
918
|
+
f"members, not external callers (they do not count toward blast radius)."
|
|
919
|
+
)
|
|
894
920
|
if truncated:
|
|
895
921
|
warnings.append(
|
|
896
922
|
"Hub-class guard active: symbol has > 500 direct callers — "
|
|
@@ -1083,6 +1109,7 @@ class ImpactOrchestrator:
|
|
|
1083
1109
|
resolution=resolution,
|
|
1084
1110
|
direct_callers=direct_callers,
|
|
1085
1111
|
indirect_callers=indirect_callers,
|
|
1112
|
+
self_referential_excluded=self_excluded,
|
|
1086
1113
|
implementations=sorted(subtype_classes_added),
|
|
1087
1114
|
endpoints_affected=endpoints_affected,
|
|
1088
1115
|
transaction_boundary=tx_boundary,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sourcecode
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.71.0
|
|
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
|
|
@@ -411,7 +411,7 @@ Emits **structured, tool-agnostic** codebase views as plain JSON/YAML — the ki
|
|
|
411
411
|
|------|--------|
|
|
412
412
|
| `--by-directory` | One group per source directory, each symbol with a `source_file:line` reference. |
|
|
413
413
|
| `--module-graph` | `{nodes, edges, summary}` — directories as modules, inter-module dependencies rolled up from class-level relation edges with hit counts + edge types. |
|
|
414
|
-
| `--integrations` | Outbound integrations across Spring **and** plain-Java/Jakarta stacks: HTTP (`RestTemplate`, `WebClient`, `@FeignClient`, JDK `HttpClient`, Apache, OkHttp), LDAP (`LdapTemplate`, JNDI `InitialDirContext`/`LdapContext`), SMTP (`JavaMailSender`, JavaMail `Transport`/`MimeMessage`), and JMS — with `file:line` evidence and a literal `target` when present. Emits an explicit `confidence` (`observed` / `not_analyzed`)
|
|
414
|
+
| `--integrations` | Outbound integrations across Spring **and** plain-Java/Jakarta stacks: HTTP (`RestTemplate`, `WebClient`, `@FeignClient`, JDK `HttpClient`, Apache, OkHttp), LDAP (`LdapTemplate`, JNDI `InitialDirContext`/`LdapContext`), SMTP (`JavaMailSender`, JavaMail `Transport`/`MimeMessage`), and JMS — with `file:line` evidence and a literal `target` when present. Emits an explicit `confidence` (`observed` / `not_analyzed`), a structured `coverage_confidence` (`low` / `partial` / `high`) with `coverage_confidence_reason`, and a prose `coverage_note`: a count of `0` — or a low count on a large repo (≥300 files, <10 constructs) — is flagged `low`, so a low count is never read as low external coupling (custom protocol/SPI clients are not statically visible). |
|
|
415
415
|
| `--c4` | Unified document: `c4.{context, containers, components, code}` + `api_surface` + a `manifest` with per-directory content hashes for **incremental** consumers (skip directories whose hash is unchanged). `components.module_roots` rolls leaf source dirs up to architectural module roots and classifies each `layered` (DDD: ≥2 of `domain`/`application`/`infrastructure`) vs `flat` (legacy/flat package), with a verifiable `module_count` — so a consumer enumerates real modules instead of inferring boundaries from leaf directories. |
|
|
416
416
|
|
|
417
417
|
The section flags compose (pass several for one multi-section document); `--c4` assembles the full export on its own. URLs assembled at runtime yield `target: null` (honest absence, never a guess); containers are derived from build files (Maven/Gradle) and reported as a limitation when none are found.
|
|
@@ -734,9 +734,20 @@ evidence. `migrate-check` enforces:
|
|
|
734
734
|
`hibernate_rewrite` headline. An **unresolved** version degrades to a
|
|
735
735
|
low-confidence hypothesis with no headline blocker. See
|
|
736
736
|
`hibernate.effective_version` / `version_major` / `version_confidence`.
|
|
737
|
-
- **Boot3
|
|
738
|
-
|
|
739
|
-
|
|
737
|
+
- **Boot3 needs Spring _Boot_, not just Spring.** `boot3.applicable` requires
|
|
738
|
+
positive Spring Boot evidence — a `spring-boot*` coordinate / Gradle plugin (or
|
|
739
|
+
`@SpringBootApplication`), **or** a concrete `spring_boot_3` / `spring_security_6`
|
|
740
|
+
finding. A repo that merely carries a Spring library (e.g. `spring-security-web`
|
|
741
|
+
with no Boot — Jenkins core) reports `boot3` as N/A with a reason and is excluded
|
|
742
|
+
from the aggregate; `spring_present: true` but `spring_boot_present: false`. Quarkus
|
|
743
|
+
/ Micronaut / Helidon / Jakarta-pure repos likewise report `boot3` N/A. jakarta
|
|
744
|
+
findings alone never enable boot3.
|
|
745
|
+
- **Frozen-legacy `@Deprecated` shims aren't blockers.** A `javax→jakarta` finding
|
|
746
|
+
on a **class-level `@Deprecated`** type (kept for binary compat, replaced
|
|
747
|
+
elsewhere) is classified `code_context: "deprecated_shim"` and excluded from
|
|
748
|
+
`blocking_count` / readiness / effort — surfaced under `non_blocking` with a
|
|
749
|
+
"verify before removing" note. A `@Deprecated` **method** on a live class still
|
|
750
|
+
blocks.
|
|
740
751
|
- **Permanent `javax.*` are never jakarta debt.** JDK/JSR namespaces
|
|
741
752
|
(`javax.cache`, `javax.sql`, `javax.xml` JAXP, `javax.crypto`, `javax.naming`,
|
|
742
753
|
`javax.management`, `javax.security.auth`, `javax.annotation.processing`, …) are
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
sourcecode/__init__.py,sha256=
|
|
1
|
+
sourcecode/__init__.py,sha256=y7H6BpC2bXN93kgVpSIJ6MrHneDY6yl0VNPo_qOu4Ts,103
|
|
2
2
|
sourcecode/adaptive_scanner.py,sha256=XffluXKzJUXrMtjEiAOnSNPZnztdIcts17T9ouHeID0,10521
|
|
3
|
-
sourcecode/architecture_analyzer.py,sha256=
|
|
4
|
-
sourcecode/architecture_summary.py,sha256=
|
|
3
|
+
sourcecode/architecture_analyzer.py,sha256=r_xf-SWXwUm3nVQMCSXHJ3M8zKsQP7Ze8Nqf6TVLRq8,45998
|
|
4
|
+
sourcecode/architecture_summary.py,sha256=UaxmUU77oTlWxttMU3c6PxHrU-Q6q5N31TeU_NWWTdU,24297
|
|
5
5
|
sourcecode/ast_extractor.py,sha256=sa6CmLpn-k5G3_Hzxn8hAlZ5-TS-EVzXDD0Gvxd2jzs,50613
|
|
6
6
|
sourcecode/cache.py,sha256=1V3vsaODAa2UBJAC0xpvxpmRdriCezQx5Q8JCcfgziE,31892
|
|
7
7
|
sourcecode/canonical_ir.py,sha256=DEwucOPJguLsVtg5cV8mWXNi112l5jmBhv73KGGebVk,24849
|
|
@@ -28,13 +28,13 @@ sourcecode/format_contract.py,sha256=1cTNqwP8geA2hbQoBHUPgX3_vSh3l8guJT_jmgEnFF8
|
|
|
28
28
|
sourcecode/fqn_utils.py,sha256=XLU7zDkNBXz_RZkIUNfpPmp1nekWtqP-fxV92tDV1vg,2158
|
|
29
29
|
sourcecode/git_analyzer.py,sha256=JStxTQXNjBWi_wLdwhsZs9mT-v50cSJIz4Agzn6Kh9I,13362
|
|
30
30
|
sourcecode/graph_analyzer.py,sha256=DHR8fY69oU_Pi4SYaWboX6EoEFrctQKB9dsjpqwGMzw,62403
|
|
31
|
-
sourcecode/hibernate_strat.py,sha256=
|
|
32
|
-
sourcecode/integration_detector.py,sha256=
|
|
31
|
+
sourcecode/hibernate_strat.py,sha256=h0leIhlWvSjYq3F99LxvLIDLrJ-xPYxWAREG4LkqZ-4,61190
|
|
32
|
+
sourcecode/integration_detector.py,sha256=HlVdiVki7i62t3pJ0lRjTQQ7WZqPDcPLTqBGAfjFjVQ,17883
|
|
33
33
|
sourcecode/jdk_exports.py,sha256=fCrlwNAXUT9gge_joq6kMnY3zJxYB2pxqy-0w3o3MJI,874
|
|
34
34
|
sourcecode/license.py,sha256=wckiLuiwaE35KMCStUf1gYzleJuFe6qsSyxUQJvit3s,23500
|
|
35
35
|
sourcecode/mcp_nudge.py,sha256=5ELU_ixzh6uA83NXLOZT8h00OhL53okfQdji3jyKOjg,2917
|
|
36
36
|
sourcecode/metrics_analyzer.py,sha256=m0ENgtqKeBL17kUIK3fmGkgo7UfXBNHxCMj0H_Y5K7c,22750
|
|
37
|
-
sourcecode/migrate_check.py,sha256=
|
|
37
|
+
sourcecode/migrate_check.py,sha256=VyA-2n5J1Cqf1XxhQ143pf5Y7IvGwolMM1F4OCLOOaU,102628
|
|
38
38
|
sourcecode/openapi_surface.py,sha256=BTt0K-woZbkbWTN77IkqeBm_Okag9owR0848fmot8sk,16207
|
|
39
39
|
sourcecode/output_budget.py,sha256=Js9yUlfQtPhqBl9R6wn_9UHVjjJc3GtLcqyfjf5t50Q,9869
|
|
40
40
|
sourcecode/path_filters.py,sha256=VnaD9jxZVNzluackNSTCdIddwzAqIseuSCs_A-gpCDM,6898
|
|
@@ -47,7 +47,7 @@ sourcecode/redactor.py,sha256=SB4hwIvg8h-hvcqKcDWaZvA-aSyn-at-BIRwa0tUv5E,3227
|
|
|
47
47
|
sourcecode/relevance_scorer.py,sha256=0AgEt4KrV73nioMqBgjhGjtY7L2C7L7cSyKtj3IKcrw,9408
|
|
48
48
|
sourcecode/rename_refactor.py,sha256=h6dNFlB9aZ_3q6heeHBkgXQeXaT03nvPSsYH6P8qxFg,12965
|
|
49
49
|
sourcecode/repo_classifier.py,sha256=FG1vaWKdWXsWdl-S8hjVMiTqcwgaRXkDyvK4rPcOGtQ,22681
|
|
50
|
-
sourcecode/repository_ir.py,sha256=
|
|
50
|
+
sourcecode/repository_ir.py,sha256=tj3e_uxkVGTRWfzkf2YoGO-oGakSO4DmWrUQow0dFGk,231201
|
|
51
51
|
sourcecode/ris.py,sha256=RcqLVwC-doFcKKViYDkCjZLBqf_wzLES7-F6vHEeWzE,20419
|
|
52
52
|
sourcecode/runtime_classifier.py,sha256=uTAD6BDCiBLUZEDRfqk718kM4RTT_vAbfkcOI2_Xx58,18432
|
|
53
53
|
sourcecode/scanner.py,sha256=WdOQ78mMzjR1NjmKTlbxdgwinnCTfAhxCVLBEFQiFHU,8899
|
|
@@ -57,7 +57,7 @@ sourcecode/semantic_analyzer.py,sha256=4OdG6tTSnTvq3_dSWMbQu8Ad1ndSCKeG-b9qM4hIx
|
|
|
57
57
|
sourcecode/serializer.py,sha256=MSxYZ-_UYDPKMvg-hVk-MnKN-TfrmxXL202siCssa9U,129110
|
|
58
58
|
sourcecode/spring_event_topology.py,sha256=5_ON_21Le5zbG-1GRc5GLIi5HJfy_QjcXLVPC5WeUGQ,18055
|
|
59
59
|
sourcecode/spring_findings.py,sha256=G7Or2lKBUQbcTDqudLvSs9XvNg_YoAa-_lBOG_ULs8E,5457
|
|
60
|
-
sourcecode/spring_impact.py,sha256=
|
|
60
|
+
sourcecode/spring_impact.py,sha256=1Eu1vkdwTVsw92iBEJDe_i_FaSf4uGH9Rb0eSgIAAZc,57542
|
|
61
61
|
sourcecode/spring_model.py,sha256=zOAgFmrRbG4a6KLm1TJl55aWMyPNsz3OS3FSczqPG6A,16594
|
|
62
62
|
sourcecode/spring_security_audit.py,sha256=XtPJ1SXlZJ8k6VYmaWuAp7Bbir4UmreAL7doIGQ5I7o,20595
|
|
63
63
|
sourcecode/spring_semantic.py,sha256=O1nKSGVzlukuxLHQVuCPxc-XrcrMFxwlHA20_dmEGgM,13307
|
|
@@ -76,7 +76,7 @@ sourcecode/detectors/elixir.py,sha256=jCpvt5Yi6jvplc80ovRtWh17q-11ZGo9qX7o8b57TJ
|
|
|
76
76
|
sourcecode/detectors/go.py,sha256=2r66uRQfeTWsqxr4HDhT6vExZErby0t46QXLHVBRv9w,2782
|
|
77
77
|
sourcecode/detectors/heuristic.py,sha256=7cRxrip4yIaggYzZJB6ef8yHKh-gHgiH_pXMFcjlyFU,3723
|
|
78
78
|
sourcecode/detectors/hybrid.py,sha256=IGFRUVsAZ1ooRlFdznCeJAV6vy1yVDx-VyghvLtddXc,9101
|
|
79
|
-
sourcecode/detectors/java.py,sha256=
|
|
79
|
+
sourcecode/detectors/java.py,sha256=hyAlTo8_y-tzNVun3zvHmWN8RdG--sdLquCI7fQBAVc,34263
|
|
80
80
|
sourcecode/detectors/jvm_ext.py,sha256=EgHJ5W8EE-ZTN9V607mVzohyKgZE8Mc2jCi-DF8RAZU,2616
|
|
81
81
|
sourcecode/detectors/nodejs.py,sha256=Hg3Gmr7yIMJFiLoDwOTk2wtu00wxIs6kZf-oQujTFUA,13187
|
|
82
82
|
sourcecode/detectors/parsers.py,sha256=ugPg8yNUf0Ai1gA7Fnn6wAkYGFjTxRodSP3IeViYJJ4,2290
|
|
@@ -104,8 +104,8 @@ sourcecode/telemetry/consent.py,sha256=H5z2Wu63pZqbaKucRPoJQJ0zCo4cke9ZlBrJC-MaW
|
|
|
104
104
|
sourcecode/telemetry/events.py,sha256=LtzYfaX9Ilckj5PTvAcTpDa9mLqDsYPDUiDkRa58piY,2580
|
|
105
105
|
sourcecode/telemetry/filters.py,sha256=NHa5T-6DaZduQPFuC34jOqHWQgSizM-Ygq8aZ4j19ng,5834
|
|
106
106
|
sourcecode/telemetry/transport.py,sha256=4gGHsq0WeY9VywEZXA3vUxykfiYnw9uuqfjAAec7F8o,1681
|
|
107
|
-
sourcecode-1.
|
|
108
|
-
sourcecode-1.
|
|
109
|
-
sourcecode-1.
|
|
110
|
-
sourcecode-1.
|
|
111
|
-
sourcecode-1.
|
|
107
|
+
sourcecode-1.71.0.dist-info/METADATA,sha256=w75cRYv8E561ZW0S1KTAry6BealPGHv20TwuZgW-NGM,48339
|
|
108
|
+
sourcecode-1.71.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
109
|
+
sourcecode-1.71.0.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
|
|
110
|
+
sourcecode-1.71.0.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
|
|
111
|
+
sourcecode-1.71.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|