sourcecode 1.67.0__py3-none-any.whl → 1.69.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/classifier.py +121 -1
- sourcecode/cli.py +21 -0
- sourcecode/detectors/java.py +36 -1
- sourcecode/explain.py +83 -1
- sourcecode/integration_detector.py +109 -10
- sourcecode/jdk_exports.py +26 -0
- sourcecode/migrate_check.py +119 -6
- sourcecode/repository_ir.py +138 -9
- {sourcecode-1.67.0.dist-info → sourcecode-1.69.0.dist-info}/METADATA +1 -1
- {sourcecode-1.67.0.dist-info → sourcecode-1.69.0.dist-info}/RECORD +14 -13
- {sourcecode-1.67.0.dist-info → sourcecode-1.69.0.dist-info}/WHEEL +0 -0
- {sourcecode-1.67.0.dist-info → sourcecode-1.69.0.dist-info}/entry_points.txt +0 -0
- {sourcecode-1.67.0.dist-info → sourcecode-1.69.0.dist-info}/licenses/LICENSE +0 -0
sourcecode/__init__.py
CHANGED
sourcecode/classifier.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import re
|
|
3
4
|
from collections.abc import Iterable, Sequence
|
|
4
5
|
from dataclasses import replace
|
|
5
6
|
from typing import Any, Literal
|
|
@@ -104,6 +105,21 @@ class TypeClassifier:
|
|
|
104
105
|
stack_names = {stack.stack for stack in stacks}
|
|
105
106
|
framework_names = {framework.name for stack in stacks for framework in stack.frameworks}
|
|
106
107
|
|
|
108
|
+
# BUG #4 (JobRunr field test): a framework present only in a small optional
|
|
109
|
+
# integration/adapter submodule must NOT label the whole repo as that
|
|
110
|
+
# framework's app type. JobRunr is a framework-agnostic background-job
|
|
111
|
+
# LIBRARY whose `core` module holds ~85% of the code; Quarkus/Micronaut/Spring
|
|
112
|
+
# appear only in tiny per-framework adapter modules — yet presence-based
|
|
113
|
+
# classification returned project_type="api"+Quarkus. We weight by code
|
|
114
|
+
# locality: if the DOMINANT source module (the one with the most source files)
|
|
115
|
+
# contains no evidence of the app-defining framework, the framework is an
|
|
116
|
+
# optional adapter and the repo is a library. A monolithic Spring app is
|
|
117
|
+
# unaffected — its dominant module *does* use the framework. Drop such
|
|
118
|
+
# localized frameworks from the set that drives the app-type decision below.
|
|
119
|
+
_app_frameworks = framework_names & (_WEB_FRAMEWORKS | _API_FRAMEWORKS)
|
|
120
|
+
_localized = self._localized_adapter_frameworks(file_tree, stacks, _app_frameworks)
|
|
121
|
+
framework_names = framework_names - _localized
|
|
122
|
+
|
|
107
123
|
if len(stack_names) >= 2 and self._is_fullstack(stacks):
|
|
108
124
|
return "fullstack"
|
|
109
125
|
|
|
@@ -129,9 +145,16 @@ class TypeClassifier:
|
|
|
129
145
|
if framework_names & _API_FRAMEWORKS:
|
|
130
146
|
return "api"
|
|
131
147
|
|
|
148
|
+
# All app-defining frameworks were localized to optional adapter submodules
|
|
149
|
+
# (multi-module library with per-framework integrations) — report library,
|
|
150
|
+
# never "unknown", when there is clearly source code present.
|
|
151
|
+
if _localized and not (framework_names & (_WEB_FRAMEWORKS | _API_FRAMEWORKS)):
|
|
152
|
+
return "library"
|
|
153
|
+
|
|
154
|
+
# Strong CLI signals: a CLI framework or an explicit cli entry point.
|
|
132
155
|
if framework_names & _CLI_FRAMEWORKS or any(
|
|
133
156
|
entry.kind == "cli" for entry in entry_points
|
|
134
|
-
)
|
|
157
|
+
):
|
|
135
158
|
return "cli"
|
|
136
159
|
|
|
137
160
|
if stack_names:
|
|
@@ -141,8 +164,105 @@ class TypeClassifier:
|
|
|
141
164
|
if single in {"cpp", "dotnet"} and any(entry.kind == "cli" for entry in entry_points):
|
|
142
165
|
return "cli"
|
|
143
166
|
|
|
167
|
+
# BUG #4 (JobRunr field test): a multi-module JVM repo with no app-defining
|
|
168
|
+
# web/API framework is a library/toolkit, not an "unknown" — never let the
|
|
169
|
+
# first command of an audit emit a vacuous classification for a clearly
|
|
170
|
+
# structured codebase (e.g. JobRunr: core + per-framework adapter modules).
|
|
171
|
+
# This is checked BEFORE the weak `bin/`-directory CLI heuristic so a build
|
|
172
|
+
# output / wrapper `bin/` dir does not mislabel a library as a CLI.
|
|
173
|
+
if stack_names & {"java", "kotlin", "scala"} and self._is_multi_module(file_tree):
|
|
174
|
+
return "library"
|
|
175
|
+
|
|
176
|
+
# Weak CLI heuristic: a top-level bin/ directory (only when nothing stronger).
|
|
177
|
+
if any(path.startswith("bin/") for path in flat_paths):
|
|
178
|
+
return "cli"
|
|
179
|
+
|
|
144
180
|
return "unknown" if stacks else None
|
|
145
181
|
|
|
182
|
+
def _is_multi_module(self, file_tree: dict[str, Any]) -> bool:
|
|
183
|
+
"""True when the repo has >1 source module (distinct `*/src/...` roots)."""
|
|
184
|
+
_CODE_EXTS = (".java", ".kt", ".kts", ".scala", ".groovy")
|
|
185
|
+
modules = {
|
|
186
|
+
self._module_of(p)
|
|
187
|
+
for p in flatten_file_tree(file_tree)
|
|
188
|
+
if p.endswith(_CODE_EXTS)
|
|
189
|
+
}
|
|
190
|
+
modules.discard("")
|
|
191
|
+
return len(modules) >= 2
|
|
192
|
+
|
|
193
|
+
@staticmethod
|
|
194
|
+
def _module_of(path: str) -> str:
|
|
195
|
+
"""Group a source path into its module root.
|
|
196
|
+
|
|
197
|
+
For Maven/Gradle layouts the module is everything before `/src/`
|
|
198
|
+
(e.g. `framework-support/jobrunr-quarkus/src/main/java/...` →
|
|
199
|
+
`framework-support/jobrunr-quarkus`). Otherwise the top-level directory.
|
|
200
|
+
"""
|
|
201
|
+
norm = path.replace("\\", "/")
|
|
202
|
+
idx = norm.find("/src/")
|
|
203
|
+
if idx > 0:
|
|
204
|
+
return norm[:idx]
|
|
205
|
+
head, _, tail = norm.partition("/")
|
|
206
|
+
return head if tail else ""
|
|
207
|
+
|
|
208
|
+
_EVIDENCE_PATH_RE = re.compile(r"\(([^()]+)\)\s*$")
|
|
209
|
+
|
|
210
|
+
def _localized_adapter_frameworks(
|
|
211
|
+
self,
|
|
212
|
+
file_tree: dict[str, Any],
|
|
213
|
+
stacks: Sequence[StackDetection],
|
|
214
|
+
candidate_frameworks: set[str],
|
|
215
|
+
) -> set[str]:
|
|
216
|
+
"""Frameworks confined to a minority module while a framework-agnostic
|
|
217
|
+
module dominates the codebase (library + per-framework adapters).
|
|
218
|
+
|
|
219
|
+
Returns the subset of ``candidate_frameworks`` that should NOT drive the
|
|
220
|
+
project-type decision. A framework qualifies only when (a) the repo is
|
|
221
|
+
multi-module, (b) the framework's evidence files are all outside the
|
|
222
|
+
dominant source module, and (c) the framework has located evidence files
|
|
223
|
+
(a manifest-only/root detection applies repo-wide and never localizes).
|
|
224
|
+
"""
|
|
225
|
+
if not candidate_frameworks:
|
|
226
|
+
return set()
|
|
227
|
+
|
|
228
|
+
_CODE_EXTS = (".java", ".kt", ".kts", ".scala", ".groovy")
|
|
229
|
+
module_file_counts: dict[str, int] = {}
|
|
230
|
+
for p in flatten_file_tree(file_tree):
|
|
231
|
+
if not p.endswith(_CODE_EXTS):
|
|
232
|
+
continue
|
|
233
|
+
mod = self._module_of(p)
|
|
234
|
+
module_file_counts[mod] = module_file_counts.get(mod, 0) + 1
|
|
235
|
+
|
|
236
|
+
# Need a genuine multi-module repo to reason about locality.
|
|
237
|
+
if len(module_file_counts) < 2:
|
|
238
|
+
return set()
|
|
239
|
+
dominant_module = max(module_file_counts, key=lambda m: module_file_counts[m])
|
|
240
|
+
|
|
241
|
+
# Collect evidence file paths per framework from detected_via.
|
|
242
|
+
evidence: dict[str, set[str]] = {}
|
|
243
|
+
for stack in stacks:
|
|
244
|
+
for fw in stack.frameworks:
|
|
245
|
+
if fw.name not in candidate_frameworks:
|
|
246
|
+
continue
|
|
247
|
+
paths = evidence.setdefault(fw.name, set())
|
|
248
|
+
for ev in fw.detected_via:
|
|
249
|
+
if ev.startswith("manifest:"):
|
|
250
|
+
continue
|
|
251
|
+
m = self._EVIDENCE_PATH_RE.search(ev)
|
|
252
|
+
if m:
|
|
253
|
+
paths.add(m.group(1).strip())
|
|
254
|
+
|
|
255
|
+
localized: set[str] = set()
|
|
256
|
+
for fw_name in candidate_frameworks:
|
|
257
|
+
files = evidence.get(fw_name) or set()
|
|
258
|
+
if not files:
|
|
259
|
+
# No locatable evidence (manifest-only) → applies repo-wide.
|
|
260
|
+
continue
|
|
261
|
+
modules = {self._module_of(f) for f in files}
|
|
262
|
+
if dominant_module not in modules:
|
|
263
|
+
localized.add(fw_name)
|
|
264
|
+
return localized
|
|
265
|
+
|
|
146
266
|
def _is_fullstack(self, stacks: Sequence[StackDetection]) -> bool:
|
|
147
267
|
has_web = False
|
|
148
268
|
has_api = False
|
sourcecode/cli.py
CHANGED
|
@@ -5777,14 +5777,32 @@ def modernize_cmd(
|
|
|
5777
5777
|
"statically_unreferenced": len(dead_zones),
|
|
5778
5778
|
"framework_dispatched": len(framework_dispatched),
|
|
5779
5779
|
}
|
|
5780
|
+
# BUG #6 (v1.68.0): `member_count` counts ALL graph members in the subsystem —
|
|
5781
|
+
# classes, methods AND fields — so it runs ~5x higher than the class count and
|
|
5782
|
+
# is not comparable to the repo-level `total_classes` (which counts only
|
|
5783
|
+
# class/interface nodes). Surface an unambiguous `class_count` alongside it,
|
|
5784
|
+
# computed against the same class/interface node set as total_classes, so a
|
|
5785
|
+
# reader does not have to reverse-engineer the unit. `member_count` is retained
|
|
5786
|
+
# for backward compatibility but is now documented as the graph-member tally.
|
|
5787
|
+
_class_fqns = {n["fqn"] for n in graph_nodes if n.get("type") in ("class", "interface")}
|
|
5788
|
+
|
|
5789
|
+
def _class_count(members: list) -> int:
|
|
5790
|
+
return sum(1 for m in members if m in _class_fqns)
|
|
5791
|
+
|
|
5780
5792
|
_subsystem_summary = [
|
|
5781
5793
|
{
|
|
5782
5794
|
"label": s.get("label") or s.get("name") or "",
|
|
5783
5795
|
"package_prefix": s.get("package_prefix") or s.get("pkg") or "",
|
|
5796
|
+
"class_count": _class_count(s.get("members") or []),
|
|
5784
5797
|
"member_count": len(s.get("members") or []),
|
|
5785
5798
|
}
|
|
5786
5799
|
for s in subsystems[:15]
|
|
5787
5800
|
]
|
|
5801
|
+
_subsystem_summary_note = (
|
|
5802
|
+
"class_count = class/interface declarations only (comparable to "
|
|
5803
|
+
"summary.total_classes). member_count = all graph members in the subsystem "
|
|
5804
|
+
"including methods and fields, so it is larger and not a class tally."
|
|
5805
|
+
)
|
|
5788
5806
|
|
|
5789
5807
|
if (not _mod_is_pro) and _mod_large(str(root)):
|
|
5790
5808
|
# Large monolith, free tier: structural discovery preview only — no dead
|
|
@@ -5801,6 +5819,7 @@ def modernize_cmd(
|
|
|
5801
5819
|
),
|
|
5802
5820
|
"summary": _summary,
|
|
5803
5821
|
"subsystem_summary": _subsystem_summary,
|
|
5822
|
+
"subsystem_summary_note": _subsystem_summary_note,
|
|
5804
5823
|
"hotspot_candidates": hotspots[:3],
|
|
5805
5824
|
"high_coupling_nodes": [
|
|
5806
5825
|
{"fqn": n["fqn"], "in_degree": n.get("in_degree", 0), "role": n.get("role", "other")}
|
|
@@ -5833,9 +5852,11 @@ def modernize_cmd(
|
|
|
5833
5852
|
for n in framework_dispatched
|
|
5834
5853
|
],
|
|
5835
5854
|
"subsystem_summary": _subsystem_summary,
|
|
5855
|
+
"subsystem_summary_note": _subsystem_summary_note,
|
|
5836
5856
|
"cross_module_tangles": [
|
|
5837
5857
|
{
|
|
5838
5858
|
"label": s.get("label") or s.get("name") or "",
|
|
5859
|
+
"class_count": _class_count(s.get("members") or []),
|
|
5839
5860
|
"member_count": len(s.get("members") or []),
|
|
5840
5861
|
}
|
|
5841
5862
|
for s in tangle_modules
|
sourcecode/detectors/java.py
CHANGED
|
@@ -90,6 +90,19 @@ _GRADLE_JAVA_VERSION_RE = re.compile(
|
|
|
90
90
|
_GRADLE_JAVA_ENUM_RE = re.compile(
|
|
91
91
|
r"""(?:sourceCompatibility|targetCompatibility)\s*=\s*JavaVersion\.VERSION_(\d+)"""
|
|
92
92
|
)
|
|
93
|
+
# BUG #4: a gradle line that genuinely declares a dependency or plugin. Matches a
|
|
94
|
+
# quoted Maven coordinate (group:artifact, requires the colon), a plugins-block
|
|
95
|
+
# `id '...'`, an `apply plugin:`, or a `classpath`/`platform(`/`enforcedPlatform(`
|
|
96
|
+
# declaration. Deliberately does NOT match `it.name.contains('quarkus')`-style
|
|
97
|
+
# subproject filters (a bare quoted name with no colon and no plugin/dep keyword).
|
|
98
|
+
_GRADLE_DEP_LINE_RE = re.compile(
|
|
99
|
+
r"""['"][\w.\-]+:[\w.\-]+""" # maven coordinate "group:artifact..."
|
|
100
|
+
r"""|\bclasspath\b"""
|
|
101
|
+
r"""|\b(?:enforced)?platform\s*\("""
|
|
102
|
+
r"""|\bapply\s+plugin\s*:"""
|
|
103
|
+
r"""|\bid\s*[('"]""",
|
|
104
|
+
re.IGNORECASE,
|
|
105
|
+
)
|
|
93
106
|
|
|
94
107
|
|
|
95
108
|
class JavaDetector(AbstractDetector):
|
|
@@ -279,7 +292,29 @@ class JavaDetector(AbstractDetector):
|
|
|
279
292
|
original = "\n".join(read_text_lines(path))
|
|
280
293
|
content = original.lower()
|
|
281
294
|
sb_version = self._extract_gradle_sb_version(original)
|
|
282
|
-
|
|
295
|
+
# BUG #4 (JobRunr field test): framework tokens must be matched only inside
|
|
296
|
+
# genuine dependency / plugin declarations, never anywhere in the file. A
|
|
297
|
+
# multi-module root build.gradle that EXCLUDES a subproject by name —
|
|
298
|
+
# `configure(subprojects.findAll { !it.name.contains('quarkus') })` — must
|
|
299
|
+
# not be read as "this project uses Quarkus". Restrict the scan to lines
|
|
300
|
+
# carrying a Maven coordinate / plugin id; the exclusion filter has neither.
|
|
301
|
+
dep_text = self._gradle_dependency_text(content)
|
|
302
|
+
return self._detect_jvm_frameworks(dep_text, "build.gradle", sb_version=sb_version)
|
|
303
|
+
|
|
304
|
+
@staticmethod
|
|
305
|
+
def _gradle_dependency_text(content: str) -> str:
|
|
306
|
+
"""Keep only gradle lines that declare a dependency or plugin.
|
|
307
|
+
|
|
308
|
+
A real dependency carries a Maven coordinate (`group:artifact`...) and a
|
|
309
|
+
plugin carries an `id`/`plugin`/`classpath` token. Subproject-name filters
|
|
310
|
+
and arbitrary prose are dropped, so a framework substring there cannot
|
|
311
|
+
manufacture a phantom framework detection.
|
|
312
|
+
"""
|
|
313
|
+
kept: list[str] = []
|
|
314
|
+
for line in content.splitlines():
|
|
315
|
+
if _GRADLE_DEP_LINE_RE.search(line):
|
|
316
|
+
kept.append(line)
|
|
317
|
+
return "\n".join(kept)
|
|
283
318
|
|
|
284
319
|
def _extract_gradle_sb_version(self, content: str) -> str | None:
|
|
285
320
|
m = _GRADLE_SB_PLUGIN_RE.search(content)
|
sourcecode/explain.py
CHANGED
|
@@ -223,7 +223,89 @@ def _build_purpose(
|
|
|
223
223
|
if role_anns:
|
|
224
224
|
desc = f"{role_anns[0]} bean"
|
|
225
225
|
|
|
226
|
-
|
|
226
|
+
if desc:
|
|
227
|
+
return desc
|
|
228
|
+
|
|
229
|
+
# BUG #4 (JobRunr field test): with no recognized framework annotation, the old
|
|
230
|
+
# fallback returned "No stereotype detected — may be a plain class or utility",
|
|
231
|
+
# which is actively misleading for a central domain class. Libraries and clean/
|
|
232
|
+
# hexagonal architectures model rich roles WITHOUT DI annotations. Infer a
|
|
233
|
+
# low-confidence structural role from signals the tool already computes:
|
|
234
|
+
# in-degree (coupling), lifecycle methods, and naming convention.
|
|
235
|
+
structural = _structural_purpose(class_fqn, raw_nodes, cir)
|
|
236
|
+
if structural:
|
|
237
|
+
return structural
|
|
238
|
+
return "No stereotype detected — may be a plain class or utility."
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
# Lifecycle method names that signal an orchestrator/component managing state.
|
|
242
|
+
_LIFECYCLE_METHODS = frozenset({
|
|
243
|
+
"start", "stop", "init", "initialize", "shutdown", "close",
|
|
244
|
+
"pause", "resume", "run", "destroy", "open", "restart",
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
# Class-name suffix → inferred role (no annotation required).
|
|
248
|
+
_NAME_ROLE_SUFFIXES: tuple[tuple[str, str], ...] = (
|
|
249
|
+
("Server", "server/orchestrator"),
|
|
250
|
+
("Manager", "manager/coordinator"),
|
|
251
|
+
("Controller", "controller"),
|
|
252
|
+
("Service", "service"),
|
|
253
|
+
("Repository", "repository/data access"),
|
|
254
|
+
("Factory", "factory"),
|
|
255
|
+
("Builder", "builder"),
|
|
256
|
+
("Handler", "handler"),
|
|
257
|
+
("Listener", "listener"),
|
|
258
|
+
("Provider", "provider"),
|
|
259
|
+
("Registry", "registry"),
|
|
260
|
+
("Scheduler", "scheduler"),
|
|
261
|
+
("Dispatcher", "dispatcher"),
|
|
262
|
+
("Processor", "processor"),
|
|
263
|
+
("Filter", "filter"),
|
|
264
|
+
("Interceptor", "interceptor"),
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _structural_purpose(
|
|
269
|
+
class_fqn: str,
|
|
270
|
+
raw_nodes: list[dict],
|
|
271
|
+
cir: "CanonicalRepositoryIR",
|
|
272
|
+
) -> str:
|
|
273
|
+
"""Infer a low-confidence stereotype from structural signals (no annotations)."""
|
|
274
|
+
try:
|
|
275
|
+
in_degree = len(_build_callers(class_fqn, cir))
|
|
276
|
+
except Exception:
|
|
277
|
+
in_degree = 0
|
|
278
|
+
|
|
279
|
+
try:
|
|
280
|
+
method_names = {m.split("(")[0].lower() for m in _build_public_methods(class_fqn, raw_nodes)}
|
|
281
|
+
except Exception:
|
|
282
|
+
method_names = set()
|
|
283
|
+
lifecycle = sorted(method_names & _LIFECYCLE_METHODS)
|
|
284
|
+
|
|
285
|
+
simple = _simple(class_fqn)
|
|
286
|
+
role = ""
|
|
287
|
+
for suffix, label in _NAME_ROLE_SUFFIXES:
|
|
288
|
+
if simple.endswith(suffix):
|
|
289
|
+
role = label
|
|
290
|
+
break
|
|
291
|
+
if not role and lifecycle:
|
|
292
|
+
role = "orchestrator/lifecycle component"
|
|
293
|
+
|
|
294
|
+
# Require at least one real signal — otherwise stay honestly silent.
|
|
295
|
+
if not role and in_degree < 3:
|
|
296
|
+
return ""
|
|
297
|
+
|
|
298
|
+
head = f"Likely {role} (no DI annotations found)" if role else \
|
|
299
|
+
"Likely a structurally significant class (no DI annotations found)"
|
|
300
|
+
signals: list[str] = []
|
|
301
|
+
if in_degree >= 3:
|
|
302
|
+
signals.append(f"high in-degree ({in_degree})")
|
|
303
|
+
elif in_degree:
|
|
304
|
+
signals.append(f"in-degree {in_degree}")
|
|
305
|
+
if lifecycle:
|
|
306
|
+
signals.append(f"lifecycle methods detected ({'/'.join(lifecycle)})")
|
|
307
|
+
suffix = f" — {', '.join(signals)}" if signals else ""
|
|
308
|
+
return f"{head}{suffix} — inferred from structure, not annotations (low confidence)."
|
|
227
309
|
|
|
228
310
|
|
|
229
311
|
def _build_public_methods(class_fqn: str, raw_nodes: list[dict]) -> list[str]:
|
|
@@ -7,8 +7,14 @@ deterministic source-text matching (same approach as the JNDI datasource scan in
|
|
|
7
7
|
|
|
8
8
|
Covered clients:
|
|
9
9
|
|
|
10
|
-
* HTTP — ``RestTemplate``, ``WebClient``, ``@FeignClient`` (declarative)
|
|
11
|
-
|
|
10
|
+
* HTTP — ``RestTemplate``, ``WebClient``, ``@FeignClient`` (declarative),
|
|
11
|
+
JDK/Apache/OkHttp clients
|
|
12
|
+
* LDAP — ``LdapTemplate``, JNDI ``InitialLdapContext``/``LdapContext``
|
|
13
|
+
* DNS — JNDI ``DirContext`` configured with ``DnsContextFactory`` (BUG #2:
|
|
14
|
+
``DirContext`` is protocol-agnostic and is classified by its
|
|
15
|
+
``INITIAL_CONTEXT_FACTORY``, not assumed to be LDAP)
|
|
16
|
+
* SMTP — JavaMail / Jakarta Mail (BUG #1: gated on a mail import so the bare
|
|
17
|
+
word "Transport" in a log string is not a false positive)
|
|
12
18
|
* JMS — ``JmsTemplate``, ActiveMQ connection factories
|
|
13
19
|
|
|
14
20
|
Each hit is reported with a ``file:line`` evidence anchor and, when a literal URL
|
|
@@ -28,6 +34,24 @@ _URL_RE = re.compile(r'"((?:https?|ldaps?|tcp|amqp|jms|nio)://[^"]*)"')
|
|
|
28
34
|
# First string literal on a line (fallback target, e.g. WebClient.create("x")).
|
|
29
35
|
_STR_RE = re.compile(r'"([^"]+)"')
|
|
30
36
|
|
|
37
|
+
# BUG #1 (v1.68.0): a JavaMail/Jakarta-Mail import is required before an SMTP token
|
|
38
|
+
# (Transport, MimeMessage) is trusted. The bare word "Transport" is also a common
|
|
39
|
+
# English noun that appears in log strings ( logger.warn("Transport initialization
|
|
40
|
+
# failure") in non-mail code), so a mail-import gate plus a string-literal skip
|
|
41
|
+
# stops those false positives.
|
|
42
|
+
_MAIL_IMPORT_RE = re.compile(
|
|
43
|
+
r"^\s*import\s+(?:(?:javax|jakarta)\.mail|org\.springframework\.mail)\b",
|
|
44
|
+
re.MULTILINE,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# BUG #2 (v1.68.0): javax.naming.directory.{DirContext,InitialDirContext} is NOT
|
|
48
|
+
# LDAP-specific — the actual protocol is decided by the value bound to
|
|
49
|
+
# Context.INITIAL_CONTEXT_FACTORY. com.sun.jndi.dns.DnsContextFactory means DNS
|
|
50
|
+
# (SRV/A record lookups), com.sun.jndi.ldap.LdapCtxFactory means LDAP. Classify by
|
|
51
|
+
# the factory class present in the file instead of defaulting to LDAP.
|
|
52
|
+
_DNS_FACTORY_RE = re.compile(r"jndi\.dns|DnsContextFactory", re.IGNORECASE)
|
|
53
|
+
_LDAP_FACTORY_RE = re.compile(r"jndi\.ldap|LdapCtxFactory", re.IGNORECASE)
|
|
54
|
+
|
|
31
55
|
# Declarative HTTP client. Attrs may span multiple lines, so matched on full text.
|
|
32
56
|
_FEIGN_RE = re.compile(r"@FeignClient\s*\(([^)]*)\)", re.DOTALL)
|
|
33
57
|
_ATTR_URL_RE = re.compile(r'url\s*=\s*"([^"]*)"')
|
|
@@ -82,6 +106,39 @@ def _extract_target(line: str) -> Optional[str]:
|
|
|
82
106
|
return None
|
|
83
107
|
|
|
84
108
|
|
|
109
|
+
def _in_string_literal(line: str, idx: int) -> bool:
|
|
110
|
+
"""True if char offset ``idx`` falls inside a double-quoted string on ``line``.
|
|
111
|
+
|
|
112
|
+
BUG #1 (v1.68.0): a Java type token never legitimately appears inside a string
|
|
113
|
+
literal, so a match there (e.g. the word "Transport" in a log message) is noise,
|
|
114
|
+
not a real client construct. Counts unescaped quotes before ``idx``.
|
|
115
|
+
"""
|
|
116
|
+
quote_count = 0
|
|
117
|
+
i = 0
|
|
118
|
+
while i < idx and i < len(line):
|
|
119
|
+
ch = line[i]
|
|
120
|
+
if ch == "\\":
|
|
121
|
+
i += 2
|
|
122
|
+
continue
|
|
123
|
+
if ch == '"':
|
|
124
|
+
quote_count += 1
|
|
125
|
+
i += 1
|
|
126
|
+
return quote_count % 2 == 1
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _classify_naming_factory(text: str) -> Optional[str]:
|
|
130
|
+
"""Classify a javax.naming.directory usage by its INITIAL_CONTEXT_FACTORY.
|
|
131
|
+
|
|
132
|
+
Returns ``"dns"``, ``"ldap"``, or ``None`` (factory not statically resolvable).
|
|
133
|
+
See BUG #2 — DirContext alone does not imply LDAP.
|
|
134
|
+
"""
|
|
135
|
+
if _DNS_FACTORY_RE.search(text):
|
|
136
|
+
return "dns"
|
|
137
|
+
if _LDAP_FACTORY_RE.search(text):
|
|
138
|
+
return "ldap"
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
|
|
85
142
|
def detect_integrations(file_paths: "list[str]", root: Path) -> dict:
|
|
86
143
|
"""Detect outbound integrations across ``file_paths`` (relative to ``root``).
|
|
87
144
|
|
|
@@ -93,18 +150,31 @@ def detect_integrations(file_paths: "list[str]", root: Path) -> dict:
|
|
|
93
150
|
seen: "set[tuple[str, str, Optional[str], str]]" = set()
|
|
94
151
|
records: "list[dict]" = []
|
|
95
152
|
|
|
96
|
-
def _add(
|
|
153
|
+
def _add(
|
|
154
|
+
kind: str,
|
|
155
|
+
client: str,
|
|
156
|
+
target: Optional[str],
|
|
157
|
+
rel: str,
|
|
158
|
+
line: int,
|
|
159
|
+
confidence: Optional[str] = None,
|
|
160
|
+
) -> None:
|
|
97
161
|
evidence = f"{rel}:{line}"
|
|
98
162
|
key = (kind, client, target, evidence)
|
|
99
163
|
if key in seen:
|
|
100
164
|
return
|
|
101
165
|
seen.add(key)
|
|
102
|
-
|
|
166
|
+
rec = {
|
|
103
167
|
"kind": kind,
|
|
104
168
|
"client": client,
|
|
105
169
|
"target": target,
|
|
106
170
|
"evidence": evidence,
|
|
107
|
-
}
|
|
171
|
+
}
|
|
172
|
+
# Per-record confidence is emitted only when the classification is uncertain
|
|
173
|
+
# (e.g. a JNDI DirContext whose factory could not be resolved). Confident hits
|
|
174
|
+
# stay schema-clean without the field.
|
|
175
|
+
if confidence is not None:
|
|
176
|
+
rec["confidence"] = confidence
|
|
177
|
+
records.append(rec)
|
|
108
178
|
|
|
109
179
|
for rel in file_paths:
|
|
110
180
|
try:
|
|
@@ -126,6 +196,10 @@ def detect_integrations(file_paths: "list[str]", root: Path) -> dict:
|
|
|
126
196
|
)
|
|
127
197
|
_add("http", "feign", target, rel, _line_of(text, m.start()))
|
|
128
198
|
|
|
199
|
+
# Per-file context used to disambiguate / gate uncertain tokens (BUG #1/#2).
|
|
200
|
+
has_mail_import = bool(_MAIL_IMPORT_RE.search(text))
|
|
201
|
+
naming_factory = _classify_naming_factory(text)
|
|
202
|
+
|
|
129
203
|
# Token clients — per line, skipping imports/package/comment noise.
|
|
130
204
|
# First pass records the declaration site and any variable name bound to
|
|
131
205
|
# the client, so a later call site (where the URL literal usually lives)
|
|
@@ -146,7 +220,28 @@ def detect_integrations(file_paths: "list[str]", root: Path) -> dict:
|
|
|
146
220
|
m = token_re.search(line)
|
|
147
221
|
if not m:
|
|
148
222
|
continue
|
|
149
|
-
|
|
223
|
+
# BUG #1: a token matched inside a string literal (log message, doc
|
|
224
|
+
# comment text) is never a real client construct — skip it.
|
|
225
|
+
if _in_string_literal(line, m.start()):
|
|
226
|
+
continue
|
|
227
|
+
confidence: Optional[str] = None
|
|
228
|
+
# BUG #1: SMTP tokens require a JavaMail/Jakarta-Mail import to be
|
|
229
|
+
# trusted — "Transport" / "MimeMessage" are too generic otherwise.
|
|
230
|
+
if kind == "smtp" and not has_mail_import:
|
|
231
|
+
continue
|
|
232
|
+
# BUG #2: javax.naming.directory.{Dir,Initial}Context is protocol-
|
|
233
|
+
# agnostic. Reclassify by the configured context factory; default to
|
|
234
|
+
# an explicit low-confidence "unknown" rather than assuming LDAP.
|
|
235
|
+
if client == "jndi-ldap" and "Dir" in token_re.pattern:
|
|
236
|
+
if naming_factory == "dns":
|
|
237
|
+
kind, client = "dns", "jndi-dns"
|
|
238
|
+
elif naming_factory == "ldap":
|
|
239
|
+
kind, client = "ldap", "jndi-ldap"
|
|
240
|
+
else:
|
|
241
|
+
kind, client, confidence = (
|
|
242
|
+
"naming-directory-unknown", "jndi-dircontext", "low",
|
|
243
|
+
)
|
|
244
|
+
_add(kind, client, _extract_target(line), rel, lineno, confidence)
|
|
150
245
|
tok = m.group(0)
|
|
151
246
|
# `Type name` (field/local decl) and `name = new Type(` forms.
|
|
152
247
|
decl = re.search(re.escape(tok) + r"\s+(\w+)\b", line)
|
|
@@ -186,9 +281,13 @@ def detect_integrations(file_paths: "list[str]", root: Path) -> dict:
|
|
|
186
281
|
"confidence": confidence,
|
|
187
282
|
"coverage_note": (
|
|
188
283
|
"Detects HTTP (RestTemplate/WebClient/JDK/Apache/OkHttp), LDAP (Spring "
|
|
189
|
-
"+ JNDI),
|
|
190
|
-
"
|
|
191
|
-
"
|
|
192
|
-
"statically
|
|
284
|
+
"+ JNDI), DNS (JNDI DirContext w/ DnsContextFactory), SMTP (JavaMail, "
|
|
285
|
+
"import-gated), and JMS client constructs by source-text matching. JNDI "
|
|
286
|
+
"DirContext usage is classified by its INITIAL_CONTEXT_FACTORY (dns vs "
|
|
287
|
+
"ldap); when the factory is not statically resolvable the kind is "
|
|
288
|
+
"'naming-directory-unknown' with confidence='low', never assumed LDAP. A "
|
|
289
|
+
"count of 0 means no such construct was found, not that the system has no "
|
|
290
|
+
"outbound integrations — runtime/DI-wired clients are not statically "
|
|
291
|
+
"visible."
|
|
193
292
|
),
|
|
194
293
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""GENERATED FILE — do not edit by hand.
|
|
2
|
+
|
|
3
|
+
Allowlist of sun.* / com.sun.* packages exported UNCONDITIONALLY by the
|
|
4
|
+
JDK (no `--add-exports` / `--add-opens` required on classpath or module
|
|
5
|
+
path). Consumed by migrate-check MIG-011 to suppress false positives.
|
|
6
|
+
|
|
7
|
+
Regenerate with: python scripts/generate_jdk_exports.py > src/sourcecode/jdk_exports.py
|
|
8
|
+
Generated from: java version "21.0.10" 2026-01-20 LTS
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
JDK_UNCONDITIONAL_EXPORTS: frozenset[str] = frozenset(
|
|
14
|
+
{
|
|
15
|
+
"com.sun.java.accessibility.util",
|
|
16
|
+
"com.sun.management",
|
|
17
|
+
"com.sun.net.httpserver",
|
|
18
|
+
"com.sun.net.httpserver.spi",
|
|
19
|
+
"com.sun.nio.sctp",
|
|
20
|
+
"com.sun.security.auth",
|
|
21
|
+
"com.sun.security.auth.callback",
|
|
22
|
+
"com.sun.security.auth.login",
|
|
23
|
+
"com.sun.security.auth.module",
|
|
24
|
+
"com.sun.security.jgss",
|
|
25
|
+
}
|
|
26
|
+
)
|
sourcecode/migrate_check.py
CHANGED
|
@@ -23,6 +23,7 @@ from pathlib import Path
|
|
|
23
23
|
from typing import Optional, TYPE_CHECKING
|
|
24
24
|
|
|
25
25
|
from sourcecode.path_filters import is_test_or_fixture_path
|
|
26
|
+
from sourcecode.jdk_exports import JDK_UNCONDITIONAL_EXPORTS
|
|
26
27
|
|
|
27
28
|
if TYPE_CHECKING:
|
|
28
29
|
from sourcecode.hibernate_strat import HibernateStratification
|
|
@@ -381,15 +382,22 @@ _JAVA_9_RULES: list[_Rule] = [
|
|
|
381
382
|
severity="high",
|
|
382
383
|
title="JDK internal API imports (sun.* / com.sun.net.*) — strong encapsulation since Java 9",
|
|
383
384
|
explanation=(
|
|
384
|
-
"Imports from sun.* and com.sun
|
|
385
|
-
"not part of the public specification. Since Java 9
|
|
386
|
-
"strongly encapsulated and require --add-exports /
|
|
387
|
-
"which are cumbersome and may be removed in future Java
|
|
385
|
+
"Imports from sun.* and com.sun.* (tools/jdi/source internals) reference "
|
|
386
|
+
"JDK-internal APIs that are not part of the public specification. Since Java 9 "
|
|
387
|
+
"(JPMS), these packages are strongly encapsulated and require --add-exports / "
|
|
388
|
+
"--add-opens JVM flags, which are cumbersome and may be removed in future Java "
|
|
389
|
+
"releases. Packages the JDK exports UNCONDITIONALLY (e.g. com.sun.net.httpserver "
|
|
390
|
+
"in jdk.httpserver, com.sun.management in jdk.management) are NOT flagged: they "
|
|
391
|
+
"need no JVM flags on any classpath or module path."
|
|
388
392
|
),
|
|
389
393
|
fix_hint=(
|
|
390
394
|
"Replace internal API usage with public equivalents. "
|
|
391
|
-
"For
|
|
392
|
-
"
|
|
395
|
+
"For sun.misc.Unsafe migrate to java.lang.invoke.VarHandle; for com.sun.tools.* "
|
|
396
|
+
"use the public javax.tools / java.compiler API. "
|
|
397
|
+
"Add '--add-exports java.base/sun.misc=ALL-UNNAMED' only as a last resort. "
|
|
398
|
+
"Note: unconditionally-exported packages (com.sun.net.httpserver, "
|
|
399
|
+
"com.sun.management, com.sun.security.auth, ...) are auto-excluded — they are "
|
|
400
|
+
"public, stable, and require no migration."
|
|
393
401
|
),
|
|
394
402
|
migration_target="java_9_plus",
|
|
395
403
|
openrewrite_recipe=None,
|
|
@@ -585,6 +593,45 @@ def _is_no_migrate_javax(fqn: str) -> bool:
|
|
|
585
593
|
return any(fqn.startswith(p) for p in _JAKARTA_NO_MIGRATE_PREFIXES)
|
|
586
594
|
|
|
587
595
|
|
|
596
|
+
# BUG #1 (JobRunr field test): MIG-011 flags `sun.*` / `com.sun.*` imports as
|
|
597
|
+
# strongly-encapsulated JDK internals on a pure PREFIX heuristic. That is wrong
|
|
598
|
+
# for packages the JDK exports UNCONDITIONALLY (no `to` clause) — e.g.
|
|
599
|
+
# `com.sun.net.httpserver` (module jdk.httpserver, public since Java 6, the basis
|
|
600
|
+
# of JEP 408) or `com.sun.management` (jdk.management, JMX/diagnostics). These
|
|
601
|
+
# need NO `--add-exports` / `--add-opens` on any classpath or module path, so they
|
|
602
|
+
# must not be flagged `high` / `manual_migration`. The allowlist is generated from
|
|
603
|
+
# the running JDK by scripts/generate_jdk_exports.py (see sourcecode/jdk_exports.py),
|
|
604
|
+
# never hand-maintained. Genuinely-internal packages (sun.misc.Unsafe, com.sun.tools.*,
|
|
605
|
+
# com.sun.jdi.*, com.sun.source.*) are NOT in the allowlist and keep `high` severity.
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
def _import_package(fqn: str) -> str:
|
|
609
|
+
"""Extract the Java package of an import FQN.
|
|
610
|
+
|
|
611
|
+
Packages are lowercase by convention and types are Capitalized, so the
|
|
612
|
+
package is the maximal prefix of non-type segments. Handles wildcard
|
|
613
|
+
(`a.b.*`) and `static` imports. Conservative: an unrecognized shape yields
|
|
614
|
+
the leading lowercase run, never a broader prefix — so a sub-package like
|
|
615
|
+
`com.sun.management.internal` is never confused with `com.sun.management`.
|
|
616
|
+
"""
|
|
617
|
+
fqn = fqn.strip().rstrip(";").strip()
|
|
618
|
+
if fqn.startswith("static "):
|
|
619
|
+
fqn = fqn[len("static "):].strip()
|
|
620
|
+
if fqn.endswith(".*"):
|
|
621
|
+
return fqn[:-2]
|
|
622
|
+
pkg_parts: list[str] = []
|
|
623
|
+
for seg in fqn.split("."):
|
|
624
|
+
if seg[:1].isupper(): # first type segment — package ends here
|
|
625
|
+
break
|
|
626
|
+
pkg_parts.append(seg)
|
|
627
|
+
return ".".join(pkg_parts)
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
def _is_jdk_unconditional_export(fqn: str) -> bool:
|
|
631
|
+
"""True if an import targets a package the JDK exports unconditionally."""
|
|
632
|
+
return _import_package(fqn) in JDK_UNCONDITIONAL_EXPORTS
|
|
633
|
+
|
|
634
|
+
|
|
588
635
|
# BUG #8: autogenerated source markers — path fragments and the JSR-250 marker.
|
|
589
636
|
_GENERATED_PATH_FRAGMENTS: tuple[str, ...] = (
|
|
590
637
|
"/generated-sources/", "/generated/", "/target/generated",
|
|
@@ -1631,6 +1678,53 @@ def _refine_mig006(matched_imports: list[str]) -> tuple[str, str]:
|
|
|
1631
1678
|
)
|
|
1632
1679
|
|
|
1633
1680
|
|
|
1681
|
+
# ---------------------------------------------------------------------------
|
|
1682
|
+
# BUG #3 (v1.68.0): framework-gated narrative.
|
|
1683
|
+
# MIG-* explanations are written assuming a Spring Boot 2→3 upgrade because that is
|
|
1684
|
+
# the overwhelmingly common Jakarta-migration context. On a repo where the tool has
|
|
1685
|
+
# already determined spring_present=False (plain Jakarta EE, JAX-RS/Jersey, Quarkus,
|
|
1686
|
+
# Guice, ...), the underlying javax→jakarta finding is still valid, but prose that
|
|
1687
|
+
# says "Spring Boot 3 requires ..." is factually wrong for that repo and misleads any
|
|
1688
|
+
# reader who trusts the narrative over the structured spring_present field.
|
|
1689
|
+
#
|
|
1690
|
+
# Treat the narrative as a pure function of the structured data: when spring_present
|
|
1691
|
+
# is False, rewrite Spring-specific framing into framework-neutral Jakarta-EE /
|
|
1692
|
+
# servlet-container framing. Ordered phrase map first (keeps nice sentences for the
|
|
1693
|
+
# common phrasings), then a catch-all so no "Spring Boot" string can survive.
|
|
1694
|
+
# ---------------------------------------------------------------------------
|
|
1695
|
+
_NON_SPRING_EXPLANATION_SUBS: tuple[tuple[str, str], ...] = (
|
|
1696
|
+
("Spring Boot 3 bundles Jakarta Servlet 6.0.",
|
|
1697
|
+
"Modern servlet containers (Jetty 12+, Tomcat 11+) require Jakarta Servlet 6.0."),
|
|
1698
|
+
("Spring Boot 3 requires Jakarta Servlet 5.0+",
|
|
1699
|
+
"Jakarta EE 9+ servlet containers require Jakarta Servlet 5.0+"),
|
|
1700
|
+
("Spring Boot 3 uses Jakarta EE 9 which moved",
|
|
1701
|
+
"Jakarta EE 9 moved"),
|
|
1702
|
+
("Spring Boot 3 uses Hibernate Validator 8.x which implements jakarta.validation.",
|
|
1703
|
+
"Jakarta EE 9+ uses Hibernate Validator 8.x which implements jakarta.validation."),
|
|
1704
|
+
("Spring Boot 3 depends on Jakarta Transactions (jakarta.transaction).",
|
|
1705
|
+
"Jakarta EE 9+ uses Jakarta Transactions (jakarta.transaction)."),
|
|
1706
|
+
("Spring Boot 3 requires Hibernate 6",
|
|
1707
|
+
"Jakarta Persistence (JPA 3.x) requires Hibernate 6"),
|
|
1708
|
+
("Spring Boot 3 cache abstraction. Spring Boot 3 requires EhCache 3.x",
|
|
1709
|
+
"Jakarta-EE cache abstraction. The modern JCache provider requires EhCache 3.x"),
|
|
1710
|
+
# Catch-all: any remaining Spring-Boot framing → generic Jakarta EE 9+.
|
|
1711
|
+
("Spring Boot 3", "Jakarta EE 9+"),
|
|
1712
|
+
("Spring Boot 2", "the pre-Jakarta (javax) baseline"),
|
|
1713
|
+
)
|
|
1714
|
+
|
|
1715
|
+
|
|
1716
|
+
def _neutralize_non_spring_explanation(text: str) -> str:
|
|
1717
|
+
"""Rewrite Spring-specific migration prose into framework-neutral Jakarta framing.
|
|
1718
|
+
|
|
1719
|
+
Applied only when the report's spring_present is False. Deterministic phrase
|
|
1720
|
+
substitution — no model, no guess. Guarantees the returned text contains no
|
|
1721
|
+
"Spring Boot" framing that the structured data does not support.
|
|
1722
|
+
"""
|
|
1723
|
+
for needle, repl in _NON_SPRING_EXPLANATION_SUBS:
|
|
1724
|
+
text = text.replace(needle, repl)
|
|
1725
|
+
return text
|
|
1726
|
+
|
|
1727
|
+
|
|
1634
1728
|
def _scan_file(
|
|
1635
1729
|
source: str,
|
|
1636
1730
|
rel_path: str,
|
|
@@ -1651,6 +1745,16 @@ def _scan_file(
|
|
|
1651
1745
|
# javax.annotation.processing.*, ...). These do NOT migrate to jakarta.
|
|
1652
1746
|
if matches and rule.migration_target == "jakarta":
|
|
1653
1747
|
matches = [m for m in matches if not _is_no_migrate_javax(m.group(1).strip())]
|
|
1748
|
+
# BUG #1: MIG-011 prefix heuristic must not flag sun.*/com.sun.* packages
|
|
1749
|
+
# the JDK exports unconditionally (no --add-exports/--add-opens needed).
|
|
1750
|
+
# Drop those imports; if none remain, the file produces no MIG-011 finding
|
|
1751
|
+
# (so it never inflates blocking_count / effort). Genuinely-internal
|
|
1752
|
+
# packages are absent from the allowlist and survive as `high`.
|
|
1753
|
+
if matches and rule.id == "MIG-011":
|
|
1754
|
+
matches = [
|
|
1755
|
+
m for m in matches
|
|
1756
|
+
if not _is_jdk_unconditional_export(m.group(1).strip())
|
|
1757
|
+
]
|
|
1654
1758
|
if matches:
|
|
1655
1759
|
import_first_line = source[: matches[0].start()].count("\n") + 1
|
|
1656
1760
|
matched_imports = [m.group(1) for m in matches]
|
|
@@ -1822,6 +1926,15 @@ def run_migrate_check(
|
|
|
1822
1926
|
for f in all_findings:
|
|
1823
1927
|
f.code_context = _classify_code_context(f)
|
|
1824
1928
|
|
|
1929
|
+
# BUG #3 (v1.68.0): on a non-Spring repo, rewrite Spring-Boot-specific framing in
|
|
1930
|
+
# every finding's explanation into framework-neutral Jakarta-EE framing. The
|
|
1931
|
+
# finding itself (javax→jakarta) stays; only the prose is corrected so it matches
|
|
1932
|
+
# the structured spring_present=False signal in the same report.
|
|
1933
|
+
if not spring_present:
|
|
1934
|
+
for f in all_findings:
|
|
1935
|
+
if f.explanation:
|
|
1936
|
+
f.explanation = _neutralize_non_spring_explanation(f.explanation)
|
|
1937
|
+
|
|
1825
1938
|
# Hibernate 5→6 stratification (independent of min_severity — it is its own
|
|
1826
1939
|
# risk model, not a severity-filtered finding stream).
|
|
1827
1940
|
from sourcecode.hibernate_strat import analyze_hibernate
|
sourcecode/repository_ir.py
CHANGED
|
@@ -20,7 +20,7 @@ import subprocess
|
|
|
20
20
|
from collections import deque
|
|
21
21
|
from dataclasses import dataclass, field
|
|
22
22
|
from pathlib import Path
|
|
23
|
-
from typing import Any, Optional
|
|
23
|
+
from typing import Any, Iterable, Optional
|
|
24
24
|
|
|
25
25
|
from sourcecode.fqn_utils import normalize_owner_fqn as _normalize_owner_fqn
|
|
26
26
|
from sourcecode.path_filters import is_test_path as _is_test_path
|
|
@@ -3366,9 +3366,20 @@ def build_repo_ir(
|
|
|
3366
3366
|
and not _INHERIT_PRESCAN_RE.search(source):
|
|
3367
3367
|
pkg_m = _PKG_RE.search(source)
|
|
3368
3368
|
_pkg = pkg_m.group(1) if pkg_m else ""
|
|
3369
|
-
# Minimal class-name symbols for same-package map (no methods/fields)
|
|
3369
|
+
# Minimal class-name symbols for same-package map (no methods/fields).
|
|
3370
|
+
# BUG #2 (JobRunr field test): this fast-path regex previously ran over RAW
|
|
3371
|
+
# source, so prose inside Javadoc/comments and string literals (e.g.
|
|
3372
|
+
# "This class provides the entry point", "...the interface is Serializable
|
|
3373
|
+
# ...instead.") was tokenized into phantom type symbols like
|
|
3374
|
+
# `org.jobrunr.configuration.provides`. Those leaked into the symbol graph
|
|
3375
|
+
# and every consumer of it (modernize statically_unreferenced /
|
|
3376
|
+
# framework_dispatched, impact, export --c4). Strip comments AND string
|
|
3377
|
+
# literals before scanning so only real declarations are captured. The name
|
|
3378
|
+
# must also start uppercase ([A-Z]) — Java type convention, matching the
|
|
3379
|
+
# precision of the full _CLASS_DECL_RE used on annotated files.
|
|
3380
|
+
_decl_source = _STRING_LITERAL_RE.sub('', _strip_java_comments(source))
|
|
3370
3381
|
_min_syms: list[SymbolRecord] = []
|
|
3371
|
-
for _cm in re.finditer(r'(?:class|interface|enum)\s+(\w
|
|
3382
|
+
for _cm in re.finditer(r'\b(?:class|interface|enum)\s+([A-Z]\w*)', _decl_source):
|
|
3372
3383
|
_cls_name = _cm.group(1)
|
|
3373
3384
|
_fqn = f"{_pkg}.{_cls_name}" if _pkg else _cls_name
|
|
3374
3385
|
_min_syms.append(SymbolRecord(
|
|
@@ -4023,6 +4034,27 @@ def extract_java_endpoints(root: Path) -> "dict[str, Any]":
|
|
|
4023
4034
|
_SERVLET_RE = _re.compile(r"\bextends\s+\w*HttpServlet\b")
|
|
4024
4035
|
_nonspring: dict[str, int] = {"webscripts": 0, "jax_rs": 0, "servlets": 0}
|
|
4025
4036
|
|
|
4037
|
+
# BUG #3 (JobRunr field test): imperative router-DSL routes. Lightweight HTTP
|
|
4038
|
+
# frameworks that deliberately avoid Spring/JAX-RS (JobRunr's own dashboard
|
|
4039
|
+
# handler, Javalin, Spark Java, hand-rolled routers) register routes as method
|
|
4040
|
+
# calls `get("/path", handler)` / `post(...)` instead of annotations, so the
|
|
4041
|
+
# annotation surface above never sees them and `endpoints` returns 0 — a silent
|
|
4042
|
+
# total false negative that also disables the `validation` command downstream.
|
|
4043
|
+
# Detection is by SYNTACTIC SHAPE (no framework knowledge): an HTTP-verb method
|
|
4044
|
+
# name, a first argument that is a string literal looking like a path (starts
|
|
4045
|
+
# with "/", may contain :param / {param}), AND a second argument (the trailing
|
|
4046
|
+
# comma) — the comma is what distinguishes a route registration from a 1-arg
|
|
4047
|
+
# getter such as Map.get("/k"). Matches both bare `get(...)` (static-import /
|
|
4048
|
+
# Spark style) and `app.get(...)` (Javalin style); the lookbehind only rejects
|
|
4049
|
+
# an identifier char so `forget(` is not mistaken for `get(`. Reported at
|
|
4050
|
+
# confidence "medium" — an occasional flagged false positive beats a total
|
|
4051
|
+
# silent false negative.
|
|
4052
|
+
_DSL_ROUTE_RE = _re.compile(
|
|
4053
|
+
r'(?<![A-Za-z0-9_])(get|post|put|delete|patch|head|options)\s*\(\s*'
|
|
4054
|
+
r'"(/[^"\s]*)"\s*,'
|
|
4055
|
+
)
|
|
4056
|
+
_dsl_routes: list[dict] = []
|
|
4057
|
+
|
|
4026
4058
|
for jf in java_files:
|
|
4027
4059
|
try:
|
|
4028
4060
|
source = jf.read_text(encoding="utf-8", errors="replace")
|
|
@@ -4051,6 +4083,27 @@ def extract_java_endpoints(root: Path) -> "dict[str, Any]":
|
|
|
4051
4083
|
if m:
|
|
4052
4084
|
extends_map[sym.symbol] = m.group(1)
|
|
4053
4085
|
|
|
4086
|
+
# BUG #3: scan for imperative router-DSL route registrations. Strip comments
|
|
4087
|
+
# first so commented-out / example routes don't leak in. The enclosing class
|
|
4088
|
+
# is the first class/interface symbol in the file (route DSLs live in one
|
|
4089
|
+
# handler class); fall back to the file stem when none was extracted.
|
|
4090
|
+
_dsl_src = _strip_java_comments(source)
|
|
4091
|
+
_dsl_matches = list(_DSL_ROUTE_RE.finditer(_dsl_src))
|
|
4092
|
+
if _dsl_matches:
|
|
4093
|
+
_cls_fqn = next(
|
|
4094
|
+
(s.symbol for s in symbols if s.type in ("class", "interface")), None
|
|
4095
|
+
)
|
|
4096
|
+
_cls_simple = (
|
|
4097
|
+
_cls_fqn.split(".")[-1] if _cls_fqn else Path(rel).stem
|
|
4098
|
+
)
|
|
4099
|
+
for _m in _dsl_matches:
|
|
4100
|
+
_dsl_routes.append({
|
|
4101
|
+
"method": _m.group(1).upper(),
|
|
4102
|
+
"path": _m.group(2),
|
|
4103
|
+
"controller": _cls_simple,
|
|
4104
|
+
"effective_class": _cls_fqn or _cls_simple,
|
|
4105
|
+
})
|
|
4106
|
+
|
|
4054
4107
|
routes = _build_route_surface(
|
|
4055
4108
|
all_symbols, route_diffs=None, extends_map=extends_map,
|
|
4056
4109
|
custom_security=_custom_sec_tuple,
|
|
@@ -4226,6 +4279,32 @@ def extract_java_endpoints(root: Path) -> "dict[str, Any]":
|
|
|
4226
4279
|
if e.get("security", {}).get("policy") == "none_detected"
|
|
4227
4280
|
)
|
|
4228
4281
|
|
|
4282
|
+
# BUG #3: merge imperative router-DSL endpoints. Dedup against the annotation
|
|
4283
|
+
# surface (method+path) in case a handler is both annotated and DSL-registered.
|
|
4284
|
+
# These carry confidence "medium" (no unambiguous annotation) and explicit
|
|
4285
|
+
# provenance so a consumer can tell shape-detected routes from annotated ones.
|
|
4286
|
+
if _dsl_routes:
|
|
4287
|
+
_seen_mp = {(e.get("method"), e.get("path")) for e in endpoints}
|
|
4288
|
+
for _r in _dsl_routes:
|
|
4289
|
+
_key = (_r["method"], _r["path"])
|
|
4290
|
+
if _key in _seen_mp:
|
|
4291
|
+
continue
|
|
4292
|
+
_seen_mp.add(_key)
|
|
4293
|
+
endpoints.append({
|
|
4294
|
+
"method": _r["method"],
|
|
4295
|
+
"path": _r["path"],
|
|
4296
|
+
"controller": _r["controller"],
|
|
4297
|
+
"handler": _r["controller"],
|
|
4298
|
+
"return_type": "unknown",
|
|
4299
|
+
"security": {"policy": "none_detected"},
|
|
4300
|
+
"confidence": "medium",
|
|
4301
|
+
"source": "router_dsl",
|
|
4302
|
+
})
|
|
4303
|
+
no_security_signal = sum(
|
|
4304
|
+
1 for e in endpoints
|
|
4305
|
+
if e.get("security", {}).get("policy") == "none_detected"
|
|
4306
|
+
)
|
|
4307
|
+
|
|
4229
4308
|
# Append spec-recovered endpoints AFTER the security-model heuristics (which
|
|
4230
4309
|
# are about annotation/filter/XML coverage of scanned source) so spec-sourced
|
|
4231
4310
|
# entries don't skew those signals. They carry their own source provenance.
|
|
@@ -4700,9 +4779,16 @@ def compute_blast_radius(
|
|
|
4700
4779
|
)
|
|
4701
4780
|
if is_mapper and fqn not in _seen_mapper_fqns:
|
|
4702
4781
|
_seen_mapper_fqns.add(fqn)
|
|
4782
|
+
# BUG #5 (v1.68.0): distinguish a CONFIRMED persistence mapper (a
|
|
4783
|
+
# @Repository/DAO role or a MyBatis @Mapper interface) from a class that
|
|
4784
|
+
# only matched the name pattern (e.g. AzToRegionMapper — an AWS topology
|
|
4785
|
+
# mapper with no data layer). Only confirmed entries may be described as
|
|
4786
|
+
# "persistence" in the narrative.
|
|
4787
|
+
_is_persistence = role in _MAPPER_ROLES or symbol_kind == "mapper_interface"
|
|
4703
4788
|
_mapper_entry: dict = {
|
|
4704
4789
|
"fqn": fqn,
|
|
4705
4790
|
"role": role or ("mapper" if symbol_kind == "mapper_interface" else "repository"),
|
|
4791
|
+
"mapper_kind": "persistence" if _is_persistence else "name_heuristic",
|
|
4706
4792
|
"source_file": node_dict.get("source_file") or "",
|
|
4707
4793
|
}
|
|
4708
4794
|
if canonical != fqn:
|
|
@@ -4841,18 +4927,33 @@ def compute_blast_radius(
|
|
|
4841
4927
|
_parts.append(f"{n_ep} endpoint{'s' if n_ep != 1 else ''} exposed")
|
|
4842
4928
|
if n_txn:
|
|
4843
4929
|
_parts.append(f"{n_txn} transactional boundary{'s' if n_txn != 1 else ''} touched")
|
|
4844
|
-
|
|
4845
|
-
|
|
4930
|
+
# BUG #5 (v1.68.0): only confirmed persistence mappers earn the word "persistence".
|
|
4931
|
+
# Name-heuristic matches (e.g. *Mapper utility classes with no data layer) are
|
|
4932
|
+
# surfaced as neutral "data-mapping class(es)" so the prose never invents a
|
|
4933
|
+
# persistence tier the repo does not have.
|
|
4934
|
+
_n_persist = sum(1 for m in mappers_affected if m.get("mapper_kind") == "persistence")
|
|
4935
|
+
_n_heur = n_mappers - _n_persist
|
|
4936
|
+
if _n_persist:
|
|
4937
|
+
_parts.append(f"{_n_persist} persistence path{'s' if _n_persist != 1 else ''} in blast cone")
|
|
4938
|
+
if _n_heur:
|
|
4939
|
+
_parts.append(f"{_n_heur} data-mapping class{'es' if _n_heur != 1 else ''} in blast cone")
|
|
4846
4940
|
if n_sec:
|
|
4847
4941
|
_parts.append(f"{n_sec} security-gated endpoint{'s' if n_sec != 1 else ''} affected")
|
|
4848
4942
|
if n_modules > 1:
|
|
4849
4943
|
_parts.append(f"impact crosses {n_modules} modules")
|
|
4850
4944
|
|
|
4945
|
+
# BUG #4 (v1.68.0): derive the DI-framework label ONCE from caller evidence and
|
|
4946
|
+
# reuse it for both the short explanation and the long via_interface_note, so the
|
|
4947
|
+
# two never disagree (the old code hardcoded "Spring/CDI" in one and "Spring/CDI/
|
|
4948
|
+
# Guice" in the other). When the actual framework is identifiable from the caller
|
|
4949
|
+
# FQNs (e.g. *.guice.* packages on a Guice repo), name it specifically instead of
|
|
4950
|
+
# a generic guess.
|
|
4951
|
+
_di_label = _detect_di_framework_label(direct_callers)
|
|
4851
4952
|
if _iface_bridging:
|
|
4852
4953
|
_iface_names = [b["interface"].split(".")[-1] for b in _iface_bridging]
|
|
4853
4954
|
_parts.append(
|
|
4854
4955
|
f"callers resolved via interface{'s' if len(_iface_names) > 1 else ''} "
|
|
4855
|
-
f"({', '.join(_iface_names)}) —
|
|
4956
|
+
f"({', '.join(_iface_names)}) — {_di_label} DI pattern"
|
|
4856
4957
|
)
|
|
4857
4958
|
|
|
4858
4959
|
# Transparency: hub-class BFS truncation must appear in explanation so the
|
|
@@ -4924,9 +5025,9 @@ def compute_blast_radius(
|
|
|
4924
5025
|
if _iface_bridging:
|
|
4925
5026
|
out["via_interface_resolution"] = _iface_bridging
|
|
4926
5027
|
out["via_interface_note"] = (
|
|
4927
|
-
"Target is a concrete class injected via interface(s) in
|
|
4928
|
-
"
|
|
4929
|
-
"interface(s) — these are the real production dependents."
|
|
5028
|
+
"Target is a concrete class injected via interface(s) in a "
|
|
5029
|
+
f"{_di_label} DI framework. direct_callers includes callers of the "
|
|
5030
|
+
"implemented interface(s) — these are the real production dependents."
|
|
4930
5031
|
)
|
|
4931
5032
|
if _bfs_truncated:
|
|
4932
5033
|
out["bfs_truncation_reason"] = "hub_class_depth_cap"
|
|
@@ -5106,3 +5207,31 @@ def _all_callers_from_rg(fqn: str, reverse_graph: dict[str, dict[str, list[str]]
|
|
|
5106
5207
|
def _simple_name(fqn: str) -> str:
|
|
5107
5208
|
"""Extract the simple class name from a fully-qualified name."""
|
|
5108
5209
|
return fqn.split(".")[-1].split("#")[0]
|
|
5210
|
+
|
|
5211
|
+
|
|
5212
|
+
def _detect_di_framework_label(caller_fqns: "Iterable[str]") -> str:
|
|
5213
|
+
"""Name the DI framework(s) evidenced by the caller FQNs (BUG #4, v1.68.0).
|
|
5214
|
+
|
|
5215
|
+
Returns a "/"-joined label of the frameworks actually identifiable from the
|
|
5216
|
+
callers' package names (e.g. "Guice" when callers live under a ``*.guice.*``
|
|
5217
|
+
package, "Spring" under ``org.springframework``). When nothing is identifiable
|
|
5218
|
+
the label degrades to the generic "Spring/CDI/Guice" rather than asserting a
|
|
5219
|
+
framework the evidence does not support. The same label feeds both the short
|
|
5220
|
+
explanation and the long via_interface_note so they cannot disagree.
|
|
5221
|
+
"""
|
|
5222
|
+
frameworks: set[str] = set()
|
|
5223
|
+
for fqn in caller_fqns:
|
|
5224
|
+
low = fqn.lower()
|
|
5225
|
+
if "guice" in low or "com.google.inject" in low:
|
|
5226
|
+
frameworks.add("Guice")
|
|
5227
|
+
if "springframework" in low or ".spring." in low:
|
|
5228
|
+
frameworks.add("Spring")
|
|
5229
|
+
if "jakarta.enterprise" in low or "javax.enterprise" in low or ".cdi." in low:
|
|
5230
|
+
frameworks.add("CDI")
|
|
5231
|
+
if "micronaut" in low:
|
|
5232
|
+
frameworks.add("Micronaut")
|
|
5233
|
+
if "dagger" in low:
|
|
5234
|
+
frameworks.add("Dagger")
|
|
5235
|
+
if frameworks:
|
|
5236
|
+
return "/".join(sorted(frameworks))
|
|
5237
|
+
return "Spring/CDI/Guice"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
sourcecode/__init__.py,sha256=
|
|
1
|
+
sourcecode/__init__.py,sha256=mxqKQ416I6lEnKsI6lwjPPRQY5VpPWrIc3-FB4IRfK8,103
|
|
2
2
|
sourcecode/adaptive_scanner.py,sha256=XffluXKzJUXrMtjEiAOnSNPZnztdIcts17T9ouHeID0,10521
|
|
3
3
|
sourcecode/architecture_analyzer.py,sha256=liCwQmLgb5vplohy8arjYxs_HOIv5C9MjLh_gY6bc5Q,44115
|
|
4
4
|
sourcecode/architecture_summary.py,sha256=z34_6v7cSwy98cof2UVciGho7SCrZ93tiqMmq5WNzRQ,20405
|
|
@@ -6,8 +6,8 @@ sourcecode/ast_extractor.py,sha256=sa6CmLpn-k5G3_Hzxn8hAlZ5-TS-EVzXDD0Gvxd2jzs,5
|
|
|
6
6
|
sourcecode/cache.py,sha256=1V3vsaODAa2UBJAC0xpvxpmRdriCezQx5Q8JCcfgziE,31892
|
|
7
7
|
sourcecode/canonical_ir.py,sha256=DEwucOPJguLsVtg5cV8mWXNi112l5jmBhv73KGGebVk,24849
|
|
8
8
|
sourcecode/cir_graphs.py,sha256=9G0HHj1kw2325IDyzo2OpX73BNswEckecf4MZUXB4JM,12078
|
|
9
|
-
sourcecode/classifier.py,sha256=
|
|
10
|
-
sourcecode/cli.py,sha256=
|
|
9
|
+
sourcecode/classifier.py,sha256=YTTCoRdcLEFRVcql9Ow1dE7eYQj0jq2rgx32bRDnb1k,13852
|
|
10
|
+
sourcecode/cli.py,sha256=VqAiGRauTO-s8yBJh2JMldha3VKx0jTXC0tOvRpWQkI,283297
|
|
11
11
|
sourcecode/code_notes_analyzer.py,sha256=EJemNCNc9Dn-1RZYu-aNbK0ELzmsyC4s6FdHi3XyNEI,9392
|
|
12
12
|
sourcecode/confidence_analyzer.py,sha256=_jckZSxksV-OU38vbkxfVNBnWCtlCq8Vwfg23x1uspA,19054
|
|
13
13
|
sourcecode/context_scorer.py,sha256=QpChSpsmaAYz91rXA4Ue5xzQmNz_ZboZN09YOHScq1U,14679
|
|
@@ -20,7 +20,7 @@ sourcecode/doc_analyzer.py,sha256=05bjTUbDbmnbajD_cgRnACzS8T7xxBKVX4CjkJlhZg8,24
|
|
|
20
20
|
sourcecode/entrypoint_classifier.py,sha256=jhTYlyqDJH2AtdEcLVaRU3lYRTJuF8DkxVzl4-W3zWE,5322
|
|
21
21
|
sourcecode/env_analyzer.py,sha256=aNTyYgQk5noJDfJU6FmasmESOHfiomyJw5EvZqjy6qc,22213
|
|
22
22
|
sourcecode/error_schema.py,sha256=uwosfNaSujtYm11_732Hu92z5ITV040fQDaIyefSvR4,1683
|
|
23
|
-
sourcecode/explain.py,sha256=
|
|
23
|
+
sourcecode/explain.py,sha256=GbcruAyzlmseV3o2rjeyxGQxToCfSYHJjKG3N05NVbQ,19897
|
|
24
24
|
sourcecode/file_chunker.py,sha256=3vkM3mDQ5eE_yTPvUgjyjpGFBIjkW6_mrBmIbrylnA8,16444
|
|
25
25
|
sourcecode/file_classifier.py,sha256=A0fEABqtfVu1MfoaxnPAvGpZgneGgVXlJDhT74NYXxE,15314
|
|
26
26
|
sourcecode/flow_analyzer.py,sha256=dSiuY4w49k29jW_EPXUOND9B5uVbuCA7kjnuHi-pIWA,28781
|
|
@@ -29,11 +29,12 @@ 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
31
|
sourcecode/hibernate_strat.py,sha256=ZyI0sikxh7IemU8uqn1Z73z-tHeFeCJuYhJnyQL8pzU,57765
|
|
32
|
-
sourcecode/integration_detector.py,sha256=
|
|
32
|
+
sourcecode/integration_detector.py,sha256=ygZwXHJXJ4QM6oiqKb5S6WIfElByDYdz9moyuFlZpYI,12997
|
|
33
|
+
sourcecode/jdk_exports.py,sha256=fCrlwNAXUT9gge_joq6kMnY3zJxYB2pxqy-0w3o3MJI,874
|
|
33
34
|
sourcecode/license.py,sha256=wckiLuiwaE35KMCStUf1gYzleJuFe6qsSyxUQJvit3s,23500
|
|
34
35
|
sourcecode/mcp_nudge.py,sha256=5ELU_ixzh6uA83NXLOZT8h00OhL53okfQdji3jyKOjg,2917
|
|
35
36
|
sourcecode/metrics_analyzer.py,sha256=m0ENgtqKeBL17kUIK3fmGkgo7UfXBNHxCMj0H_Y5K7c,22750
|
|
36
|
-
sourcecode/migrate_check.py,sha256=
|
|
37
|
+
sourcecode/migrate_check.py,sha256=QNyFmrEqYUSqmi0iIQ1P6oI63g8prOluSvYA5fUJ7Ow,95040
|
|
37
38
|
sourcecode/openapi_surface.py,sha256=BTt0K-woZbkbWTN77IkqeBm_Okag9owR0848fmot8sk,16207
|
|
38
39
|
sourcecode/output_budget.py,sha256=Js9yUlfQtPhqBl9R6wn_9UHVjjJc3GtLcqyfjf5t50Q,9869
|
|
39
40
|
sourcecode/path_filters.py,sha256=VnaD9jxZVNzluackNSTCdIddwzAqIseuSCs_A-gpCDM,6898
|
|
@@ -46,7 +47,7 @@ sourcecode/redactor.py,sha256=SB4hwIvg8h-hvcqKcDWaZvA-aSyn-at-BIRwa0tUv5E,3227
|
|
|
46
47
|
sourcecode/relevance_scorer.py,sha256=0AgEt4KrV73nioMqBgjhGjtY7L2C7L7cSyKtj3IKcrw,9408
|
|
47
48
|
sourcecode/rename_refactor.py,sha256=h6dNFlB9aZ_3q6heeHBkgXQeXaT03nvPSsYH6P8qxFg,12965
|
|
48
49
|
sourcecode/repo_classifier.py,sha256=FG1vaWKdWXsWdl-S8hjVMiTqcwgaRXkDyvK4rPcOGtQ,22681
|
|
49
|
-
sourcecode/repository_ir.py,sha256=
|
|
50
|
+
sourcecode/repository_ir.py,sha256=tP9L8v9K2xg-xb93N1aELJP-pHHxww8537GZ7Sf0_94,229324
|
|
50
51
|
sourcecode/ris.py,sha256=RcqLVwC-doFcKKViYDkCjZLBqf_wzLES7-F6vHEeWzE,20419
|
|
51
52
|
sourcecode/runtime_classifier.py,sha256=uTAD6BDCiBLUZEDRfqk718kM4RTT_vAbfkcOI2_Xx58,18432
|
|
52
53
|
sourcecode/scanner.py,sha256=WdOQ78mMzjR1NjmKTlbxdgwinnCTfAhxCVLBEFQiFHU,8899
|
|
@@ -75,7 +76,7 @@ sourcecode/detectors/elixir.py,sha256=jCpvt5Yi6jvplc80ovRtWh17q-11ZGo9qX7o8b57TJ
|
|
|
75
76
|
sourcecode/detectors/go.py,sha256=2r66uRQfeTWsqxr4HDhT6vExZErby0t46QXLHVBRv9w,2782
|
|
76
77
|
sourcecode/detectors/heuristic.py,sha256=7cRxrip4yIaggYzZJB6ef8yHKh-gHgiH_pXMFcjlyFU,3723
|
|
77
78
|
sourcecode/detectors/hybrid.py,sha256=IGFRUVsAZ1ooRlFdznCeJAV6vy1yVDx-VyghvLtddXc,9101
|
|
78
|
-
sourcecode/detectors/java.py,sha256=
|
|
79
|
+
sourcecode/detectors/java.py,sha256=V9Eb2EMqEdonIhoegvFN2-z95R4Nu9q2y9G54baWShc,32298
|
|
79
80
|
sourcecode/detectors/jvm_ext.py,sha256=EgHJ5W8EE-ZTN9V607mVzohyKgZE8Mc2jCi-DF8RAZU,2616
|
|
80
81
|
sourcecode/detectors/nodejs.py,sha256=Hg3Gmr7yIMJFiLoDwOTk2wtu00wxIs6kZf-oQujTFUA,13187
|
|
81
82
|
sourcecode/detectors/parsers.py,sha256=ugPg8yNUf0Ai1gA7Fnn6wAkYGFjTxRodSP3IeViYJJ4,2290
|
|
@@ -103,8 +104,8 @@ sourcecode/telemetry/consent.py,sha256=H5z2Wu63pZqbaKucRPoJQJ0zCo4cke9ZlBrJC-MaW
|
|
|
103
104
|
sourcecode/telemetry/events.py,sha256=LtzYfaX9Ilckj5PTvAcTpDa9mLqDsYPDUiDkRa58piY,2580
|
|
104
105
|
sourcecode/telemetry/filters.py,sha256=NHa5T-6DaZduQPFuC34jOqHWQgSizM-Ygq8aZ4j19ng,5834
|
|
105
106
|
sourcecode/telemetry/transport.py,sha256=4gGHsq0WeY9VywEZXA3vUxykfiYnw9uuqfjAAec7F8o,1681
|
|
106
|
-
sourcecode-1.
|
|
107
|
-
sourcecode-1.
|
|
108
|
-
sourcecode-1.
|
|
109
|
-
sourcecode-1.
|
|
110
|
-
sourcecode-1.
|
|
107
|
+
sourcecode-1.69.0.dist-info/METADATA,sha256=s7Ko18CIs4SrMOMsNrqCkc-nFZLcxTnC0M_r0zxXycU,47341
|
|
108
|
+
sourcecode-1.69.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
109
|
+
sourcecode-1.69.0.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
|
|
110
|
+
sourcecode-1.69.0.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
|
|
111
|
+
sourcecode-1.69.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|