sourcecode 1.71.0__py3-none-any.whl → 1.73.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 CHANGED
@@ -1,3 +1,3 @@
1
1
  """sourcecode — Deterministic codebase context maps for AI coding agents."""
2
2
 
3
- __version__ = "1.71.0"
3
+ __version__ = "1.73.0"
@@ -16,6 +16,7 @@ from __future__ import annotations
16
16
 
17
17
  import hashlib
18
18
  import json
19
+ import re
19
20
  from dataclasses import dataclass, field
20
21
  from pathlib import Path
21
22
  from typing import Any, Optional
@@ -37,6 +38,11 @@ IR_SCHEMA_VERSION = "1.0.0"
37
38
  # Edge types excluded from reverse graph (mirrors repository_ir._REVERSE_EXCLUDE)
38
39
  _REVERSE_EXCLUDE: frozenset[str] = frozenset({"annotated_with", "mapped_to"})
39
40
 
41
+ # A route path that looks like a Java FQN is framework dynamic-admin routing, not a
42
+ # real REST endpoint (mirrors the filter in repository_ir.extract_java_endpoints so
43
+ # the CIR endpoint count reconciles with the `endpoints` command). See BUG #7.
44
+ _FQN_PATH_RE: re.Pattern = re.compile(r"/(org|com|net|io|edu)\.[a-z][a-z0-9]*\.[a-zA-Z]")
45
+
40
46
 
41
47
  # ---------------------------------------------------------------------------
42
48
  # CanonicalSecurity
@@ -324,9 +330,17 @@ def ir_dict_to_canonical(
324
330
  # Build canonical endpoints from route_surface — stable ordering
325
331
  route_surface = ir.get("route_surface") or []
326
332
  # Deduplicate by endpoint id (route_surface can have duplicates from multi-prefix)
333
+ # BUG #7 (Broadleaf field test): the `endpoints` command (extract_java_endpoints)
334
+ # drops routes whose path looks like a Java FQN — framework dynamic-admin routing
335
+ # (Broadleaf @AdminSection registers entity class FQNs as URL segments) is not a
336
+ # real REST surface. The CIR endpoint list feeds `spring-audit` (endpoints_analyzed),
337
+ # `impact` and `validation`; it must apply the SAME filter so its count reconciles
338
+ # with `endpoints` instead of being silently inflated by those pseudo-routes.
327
339
  _seen_ids: set[str] = set()
328
340
  raw_endpoints: list[CanonicalEndpoint] = []
329
341
  for r in route_surface:
342
+ if _FQN_PATH_RE.search(r.get("path", "") or ""):
343
+ continue
330
344
  ep = _route_to_canonical_endpoint(r)
331
345
  if ep.id not in _seen_ids:
332
346
  _seen_ids.add(ep.id)
sourcecode/cli.py CHANGED
@@ -4221,6 +4221,7 @@ def _build_c4_export(
4221
4221
  edges: "list[dict]",
4222
4222
  endpoints: "list[dict]",
4223
4223
  integrations: "dict",
4224
+ endpoint_meta: "Optional[dict]" = None,
4224
4225
  ) -> "dict":
4225
4226
  """Assemble a unified, tool-agnostic C4 architecture export + incremental manifest.
4226
4227
 
@@ -4236,6 +4237,15 @@ def _build_c4_export(
4236
4237
  # consumer counts/classifies modules instead of inferring them from leaf dirs.
4237
4238
  module_graph["module_roots"] = _detect_module_roots(by_directory)
4238
4239
  api_surface = _group_endpoints_by_controller(endpoints)
4240
+ # BUG #7 (Alfresco field test): carry the endpoint extractor's structured
4241
+ # zero-result explanation and non-Spring REST surface into api_surface, so a
4242
+ # C4 consumer seeing 0 controllers understands WHY (unrecognized routing, e.g.
4243
+ # WebScripts) instead of concluding the system has no public API.
4244
+ if endpoint_meta:
4245
+ if endpoint_meta.get("zero_result_reason"):
4246
+ api_surface["zero_result_reason"] = endpoint_meta["zero_result_reason"]
4247
+ if endpoint_meta.get("non_spring_rest_surface"):
4248
+ api_surface["non_spring_rest_surface"] = endpoint_meta["non_spring_rest_surface"]
4239
4249
  containers = _detect_containers(root)
4240
4250
 
4241
4251
  limitations: "list[str]" = []
@@ -4364,7 +4374,8 @@ def export_cmd(
4364
4374
  from sourcecode.repository_ir import extract_java_endpoints
4365
4375
  ir = build_repo_ir(file_list, root)
4366
4376
  graph = ir.get("graph", {})
4367
- endpoints = extract_java_endpoints(root).get("endpoints", [])
4377
+ _ep_data = extract_java_endpoints(root)
4378
+ endpoints = _ep_data.get("endpoints", [])
4368
4379
  data = _build_c4_export(
4369
4380
  root,
4370
4381
  file_list,
@@ -4372,6 +4383,7 @@ def export_cmd(
4372
4383
  graph.get("edges", []),
4373
4384
  endpoints,
4374
4385
  detect_integrations(file_list, root),
4386
+ endpoint_meta=_ep_data,
4375
4387
  )
4376
4388
  output = _serialize_dict(data, format)
4377
4389
  _emit_command_output(output, output_path, copy,
@@ -5215,14 +5227,16 @@ def explain_cmd(
5215
5227
  Path("."),
5216
5228
  help="Repository root (default: current directory)",
5217
5229
  ),
5218
- format: str = typer.Option(
5219
- "text", "--format", "-f",
5220
- help="Output format: text (default) or json.",
5221
- show_default=True,
5230
+ format: Optional[str] = typer.Option(
5231
+ None, "--format", "-f",
5232
+ help="Output format: text (default) or json. If unset and --output ends "
5233
+ "in .json, JSON is written automatically.",
5222
5234
  ),
5223
5235
  output_path: Optional[Path] = typer.Option(
5224
5236
  None, "--output", "-o",
5225
- help="Write output to a file instead of stdout.",
5237
+ help="Write output to a file instead of stdout. NOTE: the default format is "
5238
+ "human-readable text/Markdown, NOT JSON — pass --format json (or use a "
5239
+ ".json output path) for machine-readable output.",
5226
5240
  ),
5227
5241
  copy: bool = typer.Option(
5228
5242
  False, "--copy", "-c",
@@ -5231,6 +5245,11 @@ def explain_cmd(
5231
5245
  ) -> None:
5232
5246
  """Human-readable architectural summary for a class.
5233
5247
 
5248
+ \b
5249
+ OUTPUT FORMAT: defaults to text/Markdown (human-oriented). Unlike most
5250
+ subcommands this is NOT JSON by default — pass --format json, or give an
5251
+ --output path ending in .json, for a machine-readable envelope.
5252
+
5234
5253
  \b
5235
5254
  Generates a structured explanation derived entirely from static analysis:
5236
5255
  - Purpose and Spring stereotype
@@ -5278,6 +5297,15 @@ def explain_cmd(
5278
5297
  )
5279
5298
  raise typer.Exit(code=1)
5280
5299
 
5300
+ # BUG #2 (Alfresco field test): `explain -o out.json` previously wrote Markdown
5301
+ # to a .json file, breaking any programmatic consumer that assumed JSON parity
5302
+ # with the other subcommands. Infer JSON from a .json output extension when
5303
+ # --format is unset; an explicit --format always wins.
5304
+ if format is None:
5305
+ if output_path and str(output_path).lower().endswith(".json"):
5306
+ format = "json"
5307
+ else:
5308
+ format = "text"
5281
5309
  _enforce_format("explain", format)
5282
5310
 
5283
5311
  file_list = find_java_files(target)
@@ -5764,11 +5792,63 @@ def modernize_cmd(
5764
5792
  key=lambda h: (-h["hotspot_score"], h["fqn"]),
5765
5793
  )[:15]
5766
5794
 
5767
- # Cross-module tangles: subsystems with high member count
5768
- tangle_modules = sorted(
5769
- [s for s in subsystems if len(s.get("members") or []) >= 5],
5770
- key=lambda s: -len(s.get("members") or []),
5771
- )[:10]
5795
+ # Cross-module tangles: actual inter-subsystem coupling measured from the
5796
+ # dependency graph, NOT a re-labelled subsystem list. For every structural
5797
+ # edge whose endpoints live in two different subsystems we tally a directed
5798
+ # (from_subsystem → to_subsystem) coupling count; pairs coupled in BOTH
5799
+ # directions are the real tangles (bidirectional/cyclic module coupling).
5800
+ _graph_edges: list = (ir.get("graph") or {}).get("edges") or []
5801
+ # Structural coupling edge types. annotated_with / mapped_to / event edges are
5802
+ # excluded (metadata + separate topology, not module-decomposition coupling).
5803
+ _TANGLE_EDGE_TYPES = frozenset({
5804
+ "imports", "injects", "extends", "implements",
5805
+ "references", "calls", "instantiates", "returns",
5806
+ })
5807
+ _fqn_to_pkg: dict[str, str] = {}
5808
+ _pkg_to_label: dict[str, str] = {}
5809
+ for _s in subsystems:
5810
+ _pp = _s.get("package_prefix") or _s.get("pkg") or ""
5811
+ if not _pp:
5812
+ continue
5813
+ _pkg_to_label[_pp] = _s.get("label") or _s.get("name") or _pp
5814
+ for _m in (_s.get("members") or []):
5815
+ _fqn_to_pkg[_m] = _pp
5816
+
5817
+ from collections import Counter as _Counter
5818
+ _pair_counts: "_Counter[tuple[str, str]]" = _Counter()
5819
+ _pair_example: dict[tuple[str, str], str] = {}
5820
+ for _e in _graph_edges:
5821
+ if _e.get("type") not in _TANGLE_EDGE_TYPES:
5822
+ continue
5823
+ _a = _fqn_to_pkg.get(_e.get("from"))
5824
+ _b = _fqn_to_pkg.get(_e.get("to"))
5825
+ if not _a or not _b or _a == _b:
5826
+ continue
5827
+ _pair_counts[(_a, _b)] += 1
5828
+ _pair_example.setdefault((_a, _b), f"{_e.get('from')} → {_e.get('to')}")
5829
+
5830
+ _cross_module_tangles = []
5831
+ for (_a, _b), _cnt in sorted(
5832
+ _pair_counts.items(), key=lambda kv: (-kv[1], kv[0])
5833
+ ):
5834
+ _reverse = _pair_counts.get((_b, _a), 0)
5835
+ _cross_module_tangles.append({
5836
+ "from_subsystem": _pkg_to_label.get(_a, _a),
5837
+ "to_subsystem": _pkg_to_label.get(_b, _b),
5838
+ "from_package": _a,
5839
+ "to_package": _b,
5840
+ "edge_count": _cnt,
5841
+ "reverse_edge_count": _reverse,
5842
+ # A mutual (cyclic) dependency is the actual "tangle" — both modules
5843
+ # reference each other, so neither can be extracted independently.
5844
+ "mutual": _reverse > 0,
5845
+ "example": _pair_example.get((_a, _b), ""),
5846
+ })
5847
+ # Surface mutual tangles first (highest decomposition risk), then by strength.
5848
+ _cross_module_tangles.sort(
5849
+ key=lambda t: (not t["mutual"], -t["edge_count"], t["from_package"])
5850
+ )
5851
+ _cross_module_tangles = _cross_module_tangles[:15]
5772
5852
 
5773
5853
  _summary = {
5774
5854
  "total_classes": len([n for n in graph_nodes if n.get("type") in ("class", "interface")]),
@@ -5837,6 +5917,17 @@ def modernize_cmd(
5837
5917
  {"fqn": n["fqn"], "in_degree": n.get("in_degree", 0), "role": n.get("role", "other")}
5838
5918
  for n in coupling_nodes
5839
5919
  ],
5920
+ # BUG #6 (Broadleaf field test): make the in_degree metric self-describing
5921
+ # so it is not confused with `explain`'s caller count. They differ by design.
5922
+ "high_coupling_nodes_note": (
5923
+ "in_degree = raw count of incoming graph edges across ALL edge types "
5924
+ "(imports, injects, extends/implements, references, annotations), "
5925
+ "counted at symbol level. This is deliberately larger than and NOT "
5926
+ "equal to `sourcecode explain`'s caller count, which reports DISTINCT "
5927
+ "dependent classes (DI dependents + reverse-call-graph callers, "
5928
+ "deduplicated to class level). Use in_degree for blast-radius ranking, "
5929
+ "explain's caller list for the concrete dependents to inspect."
5930
+ ),
5840
5931
  "statically_unreferenced": [
5841
5932
  {"fqn": n["fqn"], "type": n.get("type", ""), "role": n.get("role", "other")}
5842
5933
  for n in dead_zones
@@ -5853,14 +5944,15 @@ def modernize_cmd(
5853
5944
  ],
5854
5945
  "subsystem_summary": _subsystem_summary,
5855
5946
  "subsystem_summary_note": _subsystem_summary_note,
5856
- "cross_module_tangles": [
5857
- {
5858
- "label": s.get("label") or s.get("name") or "",
5859
- "class_count": _class_count(s.get("members") or []),
5860
- "member_count": len(s.get("members") or []),
5861
- }
5862
- for s in tangle_modules
5863
- ],
5947
+ "cross_module_tangles": _cross_module_tangles,
5948
+ "cross_module_tangles_note": (
5949
+ "Directed inter-subsystem coupling measured from structural graph "
5950
+ "edges (imports/injects/extends/implements/references/calls). "
5951
+ "edge_count = edges from_package→to_package; mutual=true marks a "
5952
+ "bidirectional (cyclic) dependency — the actual tangle that blocks "
5953
+ "independent module extraction. Empty means no cross-subsystem "
5954
+ "structural coupling was detected."
5955
+ ),
5864
5956
  # BUG-05 fix: don't recommend "Start with hotspot_candidates" when the list is empty.
5865
5957
  "recommendation": (
5866
5958
  (
@@ -10,7 +10,11 @@ from sourcecode.detectors.base import (
10
10
  EntryPoint,
11
11
  StackDetection,
12
12
  )
13
- from sourcecode.detectors.parsers import read_text_lines, unique_strings
13
+ from sourcecode.detectors.parsers import (
14
+ read_text_lines,
15
+ substitute_maven_properties as _resolve_maven_property,
16
+ unique_strings,
17
+ )
14
18
  from sourcecode.schema import FrameworkDetection
15
19
  from sourcecode.tree_utils import flatten_file_tree
16
20
 
@@ -55,6 +59,14 @@ _JAX_RS_PROVIDER_RE = re.compile(r'@Provider\b')
55
59
 
56
60
  # --- CDI / Jakarta EE scopes ---
57
61
  _CDI_SCOPED_RE = re.compile(r'@(?:ApplicationScoped|RequestScoped|SessionScoped|Singleton|Dependent)\b')
62
+ # BUG #3: a genuine bootstrap entry declares a real main() or a bootstrap annotation
63
+ # / WAR initializer supertype — never merely a `*Application.java` filename.
64
+ _MAIN_METHOD_RE = re.compile(r'\bstatic\b[^;{}()]*\bmain\s*\(\s*(?:final\s+)?String')
65
+ _BOOTSTRAP_ANNOTATION_RE = re.compile(
66
+ r'@(?:SpringBootApplication|QuarkusMain|MicronautApplication|'
67
+ r'EnableAutoConfiguration|SpringBootConfiguration)\b'
68
+ r'|\bextends\s+SpringBootServletInitializer\b'
69
+ )
58
70
 
59
71
  # --- Keycloak / Quarkus SPI ---
60
72
  # Matches "implements EventListenerProvider" or "implements Foo, EventListenerProvider, Bar"
@@ -151,6 +163,18 @@ class JavaDetector(AbstractDetector):
151
163
  # Spring profiles — check src/main/options/, src/main/resources/
152
164
  spring_profiles = self._detect_spring_profiles(context.root, all_paths)
153
165
 
166
+ # BUG #4 (Alfresco field test): a `org.mybatis` coordinate in the pom is not
167
+ # proof the app USES MyBatis mappers. Alfresco declares org.mybatis:mybatis
168
+ # yet has zero @Mapper interfaces and zero *Mapper.xml — its SQL layer is
169
+ # iBatis-style *-SqlMap.xml (surfaced separately). Emitting "MyBatis" as an
170
+ # architecture framework then contradicted the tool's own evidence block
171
+ # (mapper_interfaces:0, xml_files:0) and mislabeled bean DTO mappers. Keep
172
+ # the label only with real usage evidence: a *Mapper.xml or an @Mapper interface.
173
+ if any(f.name == "MyBatis" for f in frameworks) and not self._has_mybatis_usage(
174
+ context.root, all_paths
175
+ ):
176
+ frameworks = [f for f in frameworks if f.name != "MyBatis"]
177
+
154
178
  entry_points = self._collect_entry_points(context)
155
179
  transactional_classes = self._collect_transactional_classes(context, all_paths)
156
180
  stack = StackDetection(
@@ -211,6 +235,17 @@ class JavaDetector(AbstractDetector):
211
235
  break
212
236
  break
213
237
 
238
+ # BUG #1 (Alfresco field test): a picked value can be a Maven property
239
+ # reference — Alfresco's <maven.compiler.source>${java.version}</...> chains
240
+ # to <java.version>21</java.version> in the SAME <properties> block. Resolve
241
+ # ${...} references against the pom's own properties so --compact reports the
242
+ # concrete version ("21") instead of the literal "${java.version}", matching
243
+ # what migrate-check already surfaces. Bounded iteration guards ref cycles.
244
+ if "language_version" in result:
245
+ result["language_version"] = _resolve_maven_property(
246
+ result["language_version"], props
247
+ )
248
+
214
249
  return result
215
250
 
216
251
  def _detect_spring_profiles(self, root: Path, all_paths: list[str]) -> list[str]:
@@ -425,15 +460,25 @@ class JavaDetector(AbstractDetector):
425
460
  # Exclude test trees: test helpers like AdminApplication.java in
426
461
  # integration/src/test/java/ must not be treated as production entrypoints.
427
462
  from sourcecode.path_filters import is_test_path as _is_test_path
463
+ # BUG #3 (Alfresco field test): a `*Application.java` / `*Main.java` NAME does
464
+ # not make a file a bootstrap entry point. Alfresco has XSD-generated JAXB
465
+ # `Application.java` model classes (no main(), no bootstrap annotation) in a
466
+ # WAR with no own entry point — flagging them as `application` polluted the
467
+ # executive summary. Emit `kind="application"` ONLY when the file actually
468
+ # declares a `main(String[])` method or a bootstrap annotation.
428
469
  app_candidates = [
429
- p for p in all_java
430
- if p.endswith(("Application.java", "Main.java"))
431
- and not _is_test_path(p)
432
- ]
433
- entry_points: list[EntryPoint] = [
434
- EntryPoint(path=p, stack="java", kind="application", source="manifest")
435
- for p in unique_strings(app_candidates)
470
+ p for p in unique_strings([
471
+ q for q in all_java
472
+ if q.endswith(("Application.java", "Main.java")) and not _is_test_path(q)
473
+ ])
436
474
  ]
475
+ entry_points: list[EntryPoint] = []
476
+ for p in app_candidates:
477
+ verified = self._verify_bootstrap_entry(context.root / p)
478
+ if verified:
479
+ entry_points.append(EntryPoint(
480
+ path=p, stack="java", kind="application", source=verified,
481
+ ))
437
482
 
438
483
  # 2. Annotation-based scan: @RestController, @WebFilter, FilterRegistrationBean
439
484
  # Prioritize Controller-named files so all REST controllers are detected
@@ -481,6 +526,52 @@ class JavaDetector(AbstractDetector):
481
526
  unique_eps.append(ep)
482
527
  return unique_eps
483
528
 
529
+ def _has_mybatis_usage(self, root: Path, all_paths: list[str]) -> bool:
530
+ """True when the repo shows real MyBatis usage, not just a pom coordinate.
531
+
532
+ Evidence = a *Mapper.xml file OR a *Mapper.java carrying the MyBatis
533
+ @Mapper annotation. iBatis-style *-SqlMap.xml is deliberately NOT counted
534
+ here — it is a distinct legacy pattern surfaced elsewhere (BUG #4).
535
+ """
536
+ if any(p.endswith("Mapper.xml") for p in all_paths):
537
+ return True
538
+ from sourcecode.path_filters import is_test_path as _is_test_path
539
+ mapper_java = [
540
+ p for p in all_paths
541
+ if p.endswith("Mapper.java") and not _is_test_path(p)
542
+ ]
543
+ for rel in mapper_java[:_MAX_JAVA_ENTRY_SCAN]:
544
+ try:
545
+ abs_path = root / rel
546
+ if abs_path.stat().st_size > _MAX_FILE_SIZE:
547
+ continue
548
+ content = abs_path.read_text(encoding="utf-8", errors="replace")
549
+ except OSError:
550
+ continue
551
+ if "org.apache.ibatis.annotations.Mapper" in content or "@Mapper" in content:
552
+ return True
553
+ return False
554
+
555
+ def _verify_bootstrap_entry(self, abs_path: Path) -> "str | None":
556
+ """Return the evidence source when *abs_path* is a real bootstrap entry.
557
+
558
+ A file is a genuine application entry point only if it declares a
559
+ `main(String[])` method OR carries a recognized bootstrap annotation /
560
+ WAR initializer supertype. Returns "main_method" / "bootstrap_annotation"
561
+ accordingly, else None (name alone is never sufficient — see BUG #3).
562
+ """
563
+ try:
564
+ if abs_path.stat().st_size > _MAX_FILE_SIZE:
565
+ return None
566
+ content = abs_path.read_text(encoding="utf-8", errors="replace")
567
+ except OSError:
568
+ return None
569
+ if _BOOTSTRAP_ANNOTATION_RE.search(content):
570
+ return "bootstrap_annotation"
571
+ if _MAIN_METHOD_RE.search(content):
572
+ return "main_method"
573
+ return None
574
+
484
575
  def _augment_deep_java_controllers(self, context: DetectionContext, all_java: list[str]) -> None:
485
576
  """Scan standard Java source roots for *Controller*.java files not in all_java.
486
577
 
@@ -1,10 +1,33 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import json
4
+ import re
4
5
  from collections.abc import Iterable
5
6
  from pathlib import Path
6
7
  from typing import Any, Optional
7
8
 
9
+ _MAVEN_PROP_REF_RE = re.compile(r'\$\{([\w.\-]+)\}')
10
+
11
+
12
+ def substitute_maven_properties(text: str, props: "dict[str, str]") -> str:
13
+ """Resolve Maven ${prop} references in *text* against *props*.
14
+
15
+ Shared single source of truth for Maven property resolution across
16
+ subcommands (BUG #1, Alfresco field test): both the --compact Java-stack
17
+ detector and migrate-check must resolve `${java.version}` → `21` identically.
18
+ Multi-level references (${a} where a=${b}) resolve in up to 3 passes; a
19
+ reference with no matching property is left verbatim (honest, not blanked).
20
+ """
21
+ if not props or "${" not in text:
22
+ return text
23
+ resolved = text
24
+ for _ in range(3):
25
+ new = _MAVEN_PROP_REF_RE.sub(lambda m: props.get(m.group(1), m.group(0)), resolved)
26
+ if new == resolved:
27
+ break
28
+ resolved = new
29
+ return resolved
30
+
8
31
 
9
32
  def load_json_file(path: Path) -> Optional[dict[str, Any]]:
10
33
  """Carga un fichero JSON sin lanzar si el contenido es invalido."""
sourcecode/explain.py CHANGED
@@ -298,10 +298,15 @@ def _structural_purpose(
298
298
  head = f"Likely {role} (no DI annotations found)" if role else \
299
299
  "Likely a structurally significant class (no DI annotations found)"
300
300
  signals: list[str] = []
301
+ # BUG #6 (Broadleaf field test): this counts DISTINCT dependent classes (DI
302
+ # dependents + reverse-call-graph callers, deduped to class level), which is a
303
+ # different metric from modernize's node.in_degree (raw incoming-edge count
304
+ # across ALL edge types, symbol-level). Name it explicitly so the two numbers
305
+ # are not read as a contradiction when cross-referenced.
301
306
  if in_degree >= 3:
302
- signals.append(f"high in-degree ({in_degree})")
307
+ signals.append(f"high fan-in ({in_degree} distinct caller classes)")
303
308
  elif in_degree:
304
- signals.append(f"in-degree {in_degree}")
309
+ signals.append(f"{in_degree} distinct caller class{'es' if in_degree != 1 else ''}")
305
310
  if lifecycle:
306
311
  signals.append(f"lifecycle methods detected ({'/'.join(lifecycle)})")
307
312
  suffix = f" — {', '.join(signals)}" if signals else ""
@@ -83,6 +83,17 @@ _TOKEN_CLIENTS: "tuple[tuple[str, str, str], ...]" = (
83
83
  ("InitialLdapContext", "ldap", "jndi-ldap"),
84
84
  ("InitialDirContext", "ldap", "jndi-ldap"),
85
85
  ("LdapContext", "ldap", "jndi-ldap"),
86
+ # Spring Security LDAP authentication (BUG #5, Broadleaf field test): the
87
+ # idiomatic auth integration is a UserDetails mapper that extends
88
+ # LdapUserDetailsMapper and consumes DirContextOperations — neither uses
89
+ # LdapTemplate/ContextSource directly, so the connection-type tokens above
90
+ # miss it entirely. These types come from spring-security-ldap / spring-ldap
91
+ # and are unambiguous LDAP integration points.
92
+ ("LdapUserDetailsMapper", "ldap", "spring-security-ldap"),
93
+ ("DirContextOperations", "ldap", "spring-ldap"),
94
+ ("LdapContextSource", "ldap", "spring-ldap"),
95
+ ("DefaultSpringSecurityContextSource", "ldap", "spring-security-ldap"),
96
+ ("BindAuthenticator", "ldap", "spring-security-ldap"),
86
97
  # Mail / SMTP (JavaMail / Jakarta Mail)
87
98
  ("JavaMailSender", "smtp", "spring-mail"),
88
99
  ("MimeMessage", "smtp", "javamail"),
@@ -90,12 +101,33 @@ _TOKEN_CLIENTS: "tuple[tuple[str, str, str], ...]" = (
90
101
  # JMS / messaging
91
102
  ("JmsTemplate", "jms", "jmstemplate"),
92
103
  ("ActiveMQConnectionFactory", "jms", "activemq"),
104
+ # BUG #5 (Alfresco field test): the connection-factory family beyond the plain
105
+ # ActiveMQConnectionFactory — Alfresco wires ActiveMQSslConnectionFactory and
106
+ # pooled/caching factories as Spring @Beans, none of which were recognized.
107
+ ("ActiveMQSslConnectionFactory", "jms", "activemq"),
108
+ ("PooledConnectionFactory", "jms", "activemq-pooled"),
109
+ ("CachingConnectionFactory", "jms", "spring-jms"),
93
110
  )
94
111
  _TOKEN_RES = tuple(
95
112
  (re.compile(r"\b" + re.escape(tok) + r"\b"), kind, client)
96
113
  for tok, kind, client in _TOKEN_CLIENTS
97
114
  )
98
115
 
116
+ # BUG #5: Apache Camel route detection. Only applied inside a confirmed Camel file.
117
+ _CAMEL_CONTEXT_RE = re.compile(r"org\.apache\.camel\b|\bextends\s+RouteBuilder\b")
118
+ # A from()/to() endpoint with a literal scheme URI, e.g. from("activemq:queue:foo").
119
+ _CAMEL_URI_RE = re.compile(r'\b(from|to)\s*\(\s*"([a-z][a-z0-9+.\-]*):([^"]*)"')
120
+ # A route anchored by from(<non-string>) — endpoint URI comes from a variable/property.
121
+ _CAMEL_VAR_ROUTE_RE = re.compile(r'\bfrom\s*\(\s*[A-Za-z_]')
122
+ # Camel URI scheme → integration kind (jms-family messaging schemes collapse to "jms").
123
+ _CAMEL_SCHEME_KIND: "dict[str, str]" = {
124
+ "activemq": "jms", "amq": "jms", "jms": "jms", "sjms": "jms", "sjms2": "jms",
125
+ "amqp": "jms", "stomp": "jms",
126
+ "kafka": "kafka", "mqtt": "mqtt", "paho": "mqtt",
127
+ "http": "http", "https": "http", "http4": "http", "rest": "http",
128
+ "smtp": "smtp", "smtps": "smtp",
129
+ }
130
+
99
131
 
100
132
  def _line_of(text: str, idx: int) -> int:
101
133
  """1-based line number of character offset ``idx`` in ``text``."""
@@ -312,6 +344,28 @@ def detect_integrations(file_paths: "list[str]", root: Path) -> dict:
312
344
  _add(kind, client, url.group(1), rel, lineno)
313
345
  break
314
346
 
347
+ # BUG #5 (Alfresco field test): Apache Camel routes are messaging/integration
348
+ # endpoints declared as a fluent DSL (from(...)/to(...)) inside a RouteBuilder,
349
+ # NOT as JmsTemplate/connection-factory constructs — so they were invisible.
350
+ # Only trust from()/to() inside a confirmed Camel file (import org.apache.camel
351
+ # or `extends RouteBuilder`) to avoid matching unrelated fluent `.to(...)`.
352
+ if _CAMEL_CONTEXT_RE.search(text):
353
+ for m in _CAMEL_URI_RE.finditer(text):
354
+ scheme = m.group(2).lower()
355
+ uri = f"{scheme}:{m.group(3)}"
356
+ kind = _CAMEL_SCHEME_KIND.get(scheme, "messaging")
357
+ _add(kind, f"camel-{scheme}", uri, rel, _line_of(text, m.start()))
358
+ # Routes whose endpoint URI is a variable/property (Alfresco's
359
+ # from(sourceQueue).to(targetTopic)) cannot yield a literal target, but the
360
+ # route IS a real messaging integration point — record it honestly with a
361
+ # null target and low confidence rather than dropping it.
362
+ if not any(r["client"].startswith("camel-") and r["evidence"].startswith(f"{rel}:")
363
+ for r in records):
364
+ vm = _CAMEL_VAR_ROUTE_RE.search(text)
365
+ if vm:
366
+ _add("messaging", "camel-route", None, rel,
367
+ _line_of(text, vm.start()), confidence="low")
368
+
315
369
  records.sort(key=lambda r: (r["kind"], r["client"], r["evidence"]))
316
370
 
317
371
  by_kind: "dict[str, int]" = {}
@@ -645,6 +645,18 @@ def _classify_code_context(finding: "MigrationFinding") -> str:
645
645
  if is_test_or_fixture_path(path):
646
646
  return "test"
647
647
  norm = path.replace("\\", "/").lower()
648
+ # BUG #4 (Broadleaf field test): a Spring XML config file can live under
649
+ # src/main/resources yet be test-only scaffolding loaded exclusively by
650
+ # test @ContextConfiguration (e.g. bl-applicationContext-test-security.xml).
651
+ # is_test_or_fixture_path deliberately ignores the filename, so a `test`
652
+ # marker in the config file's own name is not seen. For XML config, a `test`
653
+ # token in the base filename is a strong, low-FP test-scope signal —
654
+ # production context files are not named `-test-`. Keep it out of
655
+ # blocking_count so test scaffolding never inflates the headline blocker.
656
+ if norm.endswith(".xml"):
657
+ base = norm.rsplit("/", 1)[-1]
658
+ if re.search(r"(?:^|[-_.])test(?:[-_.]|$)", base):
659
+ return "test"
648
660
  if any(frag in norm for frag in _GENERATED_PATH_FRAGMENTS):
649
661
  return "generated"
650
662
  # javax.annotation.Generated is the marker emitted into autogenerated code.
@@ -936,6 +948,42 @@ def _find_non_java_files(
936
948
  return xml_files, build_files
937
949
 
938
950
 
951
+ def _xml_explanation_for(rule: "_XmlRule", matches: list) -> str:
952
+ """Return an explanation reflecting the sub-pattern that actually matched.
953
+
954
+ BUG #4 (Broadleaf field test): MIG-031's pattern is an OR of two independent
955
+ triggers — old-style `<http auto-config="true">` and a versioned legacy schema
956
+ (spring-security-[2-5].x.xsd). The static template asserted BOTH, so a file
957
+ that only used auto-config (its schema line already being the unversioned
958
+ spring-security.xsd) got a factually wrong explanation claiming a versioned
959
+ schema. Emit only the condition(s) present in this file.
960
+ """
961
+ if rule.id != "MIG-031":
962
+ return rule.explanation
963
+ matched_text = " ".join(m.group(0).lower() for m in matches)
964
+ has_autoconfig = "auto-config" in matched_text
965
+ has_versioned_schema = bool(re.search(r"spring-security-[2345]\.", matched_text))
966
+ triggers: list[str] = []
967
+ if has_autoconfig:
968
+ triggers.append(
969
+ "uses the old-style <http auto-config='true'> shortcut, which was "
970
+ "removed in Spring Security 6"
971
+ )
972
+ if has_versioned_schema:
973
+ triggers.append(
974
+ "references a versioned legacy schema (spring-security-[2-5].x.xsd) "
975
+ "whose namespace attributes changed in Spring Security 6"
976
+ )
977
+ if not triggers: # defensive — pattern matched but neither branch classified
978
+ return rule.explanation
979
+ return (
980
+ "XML-based Spring Security configuration in this file "
981
+ + " and ".join(triggers)
982
+ + ". Spring Security 6 (Spring Boot 3) requires migrating this to "
983
+ "Java-based @Configuration with a SecurityFilterChain @Bean."
984
+ )
985
+
986
+
939
987
  def _scan_xml_file(text: str, rel_path: str) -> list["MigrationFinding"]:
940
988
  """Apply XML rules to raw XML text. Returns one finding per matched rule."""
941
989
  findings: list[MigrationFinding] = []
@@ -947,6 +995,7 @@ def _scan_xml_file(text: str, rel_path: str) -> list["MigrationFinding"]:
947
995
  continue
948
996
  first_line = text[: matches[0].start()].count("\n") + 1
949
997
  snippets = [m.group(0)[:120].strip() for m in matches[:5]]
998
+ explanation = _xml_explanation_for(rule, matches)
950
999
  findings.append(
951
1000
  MigrationFinding(
952
1001
  id=MigrationFinding.make_id(rule.id, rel_path),
@@ -956,7 +1005,7 @@ def _scan_xml_file(text: str, rel_path: str) -> list["MigrationFinding"]:
956
1005
  source_file=rel_path,
957
1006
  first_line=first_line,
958
1007
  imports_found=snippets,
959
- explanation=rule.explanation,
1008
+ explanation=explanation,
960
1009
  fix_hint=rule.fix_hint,
961
1010
  migration_target=rule.migration_target,
962
1011
  openrewrite_recipe=rule.openrewrite_recipe,
@@ -1119,18 +1168,10 @@ def _resolve_maven_properties(text: str) -> str:
1119
1168
  props: dict[str, str] = {}
1120
1169
  for m in re.finditer(r'<([A-Za-z][\w.\-]*)>\s*([^<${}]+?)\s*</\1>', text):
1121
1170
  props[m.group(1)] = m.group(2).strip()
1122
- if not props:
1123
- return text
1124
-
1125
- resolved = text
1126
- for _ in range(3):
1127
- def _sub(m: re.Match) -> str: # noqa: E306
1128
- return props.get(m.group(1), m.group(0))
1129
- resolved_new = re.sub(r'\$\{([\w.\-]+)\}', _sub, resolved)
1130
- if resolved_new == resolved:
1131
- break
1132
- resolved = resolved_new
1133
- return resolved
1171
+ # Shared ${...} substitution core (BUG #1): identical resolution semantics as
1172
+ # the --compact Java-stack detector.
1173
+ from sourcecode.detectors.parsers import substitute_maven_properties
1174
+ return substitute_maven_properties(text, props)
1134
1175
 
1135
1176
 
1136
1177
  def _scan_dep_file(text: str, rel_path: str) -> list["MigrationFinding"]:
@@ -387,6 +387,59 @@ def _strip_java_comments(source: str) -> str:
387
387
  return source
388
388
 
389
389
 
390
+ # Declaration keyword OR a brace — matched in source order to track nesting when
391
+ # building minimal symbols for pre-scan-skipped files (see _minimal_class_symbols).
392
+ _MIN_DECL_OR_BRACE_RE = re.compile(r'\b(?:class|interface|enum)\s+([A-Z]\w*)|([{}])')
393
+
394
+
395
+ def _minimal_class_symbols(
396
+ source: str, package: str, rel_path: str
397
+ ) -> "list[SymbolRecord]":
398
+ """Nesting-aware class/interface/enum symbols for pre-scan-skipped files.
399
+
400
+ Tracks brace depth so a NESTED type gets a fully-qualified `pkg.Outer.Inner`
401
+ FQN (matching _extract_symbols), never a package-only `pkg.Inner`.
402
+
403
+ BUG #2 (Broadleaf field test): the previous fast path emitted every declared
404
+ type as `f"{pkg}.{name}"`, dropping the enclosing class. Two distinct nested
405
+ types with the same simple name under different outer classes in one package
406
+ (Broadleaf has 25 `GroupName` nested classes) therefore collapsed onto ONE
407
+ colliding FQN, and — since pre-scan-skipped files build no relations — those
408
+ zero-degree phantom nodes were reported as false `statically_unreferenced`
409
+ dead code. Qualifying by the enclosing class keeps them distinct and correct.
410
+ """
411
+ decl_source = _STRING_LITERAL_RE.sub('', _strip_java_comments(source))
412
+ syms: "list[SymbolRecord]" = []
413
+ stack: "list[tuple[str, int]]" = [] # (fqn, brace-depth at which body opened)
414
+ depth = 0
415
+ pending_fqn: "Optional[str]" = None
416
+ for m in _MIN_DECL_OR_BRACE_RE.finditer(decl_source):
417
+ name = m.group(1)
418
+ if name is not None:
419
+ enclosing = stack[-1][0] if stack else None
420
+ if enclosing:
421
+ pending_fqn = f"{enclosing}.{name}"
422
+ else:
423
+ pending_fqn = f"{package}.{name}" if package else name
424
+ continue
425
+ brace = m.group(2)
426
+ if brace == '{':
427
+ depth += 1
428
+ if pending_fqn is not None:
429
+ stack.append((pending_fqn, depth))
430
+ syms.append(SymbolRecord(
431
+ symbol=pending_fqn, type="class", confidence="medium",
432
+ declaring_file=rel_path,
433
+ ))
434
+ pending_fqn = None
435
+ else: # '}'
436
+ while stack and stack[-1][1] == depth:
437
+ stack.pop()
438
+ if depth > 0:
439
+ depth -= 1
440
+ return syms
441
+
442
+
390
443
  def _parse_annotation_line(line: str) -> tuple[str, str]:
391
444
  """Parse annotation name and args from a line starting with '@'.
392
445
 
@@ -1127,6 +1180,234 @@ def _build_same_package_map(symbols: list[SymbolRecord]) -> dict[str, dict[str,
1127
1180
  return result
1128
1181
 
1129
1182
 
1183
+ # A type-qualified member reference inside an annotation argument, e.g. the
1184
+ # `GroupName.General` / `FieldOrder.NAME` in
1185
+ # `@AdminPresentation(group = GroupName.General, order = FieldOrder.NAME)`, or the
1186
+ # fully-qualified nested form `SystemPropertyAdminPresentation.GroupOrder.General`.
1187
+ # group(0) is the whole dotted chain (UpperCamel head + `.member`… tail); the
1188
+ # resolver walks the chain through known nested types so a reference to
1189
+ # `Outer.Nested.CONST` credits the NESTED holder, not just the outer type.
1190
+ _ANN_REF_TOKEN_RE = re.compile(r'\b[A-Z]\w*(?:\.\w+)+')
1191
+ # Combined token scanner: an annotation-with-args head, a type declaration, or a
1192
+ # brace — matched in source order so annotation references can be attributed to
1193
+ # the class whose body encloses them.
1194
+ _ANN_OR_DECL_OR_BRACE_RE = re.compile(
1195
+ r'@([A-Z]\w*)\s*\('
1196
+ r'|\b(?:class|interface|enum)\s+([A-Z]\w*)'
1197
+ r'|([{}])'
1198
+ )
1199
+
1200
+
1201
+ def _extract_annotation_type_refs(
1202
+ source: str, package: str
1203
+ ) -> "list[tuple[str, str]]":
1204
+ """Return (enclosing_class_fqn, referenced_type_simple_name) pairs.
1205
+
1206
+ Walks the comment/string-stripped source tracking the nesting stack so each
1207
+ type reference found inside an annotation argument list is attributed to the
1208
+ class whose body encloses the annotated member. Member-level annotations sit
1209
+ inside the class body (stack has the class); this is the common case for
1210
+ `@AdminPresentation(group = GroupName.General)` on entity fields.
1211
+ """
1212
+ text = _STRING_LITERAL_RE.sub('""', _strip_java_comments(source))
1213
+ refs: "list[tuple[str, str]]" = []
1214
+ stack: "list[tuple[str, int]]" = []
1215
+ depth = 0
1216
+ pending: "Optional[str]" = None
1217
+ # Type refs found in a CLASS-LEVEL annotation (which precedes the class body,
1218
+ # so the stack does not yet contain the owning class) are held here and
1219
+ # attributed to the class when its body opens — e.g. an interface annotated
1220
+ # `@AdminTabPresentation(name = TabName.General)` that references its OWN
1221
+ # nested constant holders.
1222
+ pending_class_refs: "list[str]" = []
1223
+ n = len(text)
1224
+ idx = 0
1225
+ while idx < n:
1226
+ m = _ANN_OR_DECL_OR_BRACE_RE.search(text, idx)
1227
+ if not m:
1228
+ break
1229
+ ann, cname, brace = m.group(1), m.group(2), m.group(3)
1230
+ if ann is not None:
1231
+ # Balance the annotation's parens and scan its argument text.
1232
+ popen = m.end() - 1
1233
+ d = 0
1234
+ j = popen
1235
+ while j < n:
1236
+ c = text[j]
1237
+ if c == '(':
1238
+ d += 1
1239
+ elif c == ')':
1240
+ d -= 1
1241
+ if d == 0:
1242
+ break
1243
+ j += 1
1244
+ arg_text = text[popen + 1:j]
1245
+ cur_class = stack[-1][0] if stack else ""
1246
+ for tm in _ANN_REF_TOKEN_RE.finditer(arg_text):
1247
+ if cur_class:
1248
+ refs.append((cur_class, tm.group(0)))
1249
+ else:
1250
+ pending_class_refs.append(tm.group(0))
1251
+ idx = j + 1
1252
+ continue
1253
+ if cname is not None:
1254
+ enc = stack[-1][0] if stack else None
1255
+ pending = f"{enc}.{cname}" if enc else (
1256
+ f"{package}.{cname}" if package else cname
1257
+ )
1258
+ idx = m.end()
1259
+ continue
1260
+ if brace == '{':
1261
+ depth += 1
1262
+ if pending is not None:
1263
+ stack.append((pending, depth))
1264
+ # A class-level annotation on this class referenced these types.
1265
+ for head in pending_class_refs:
1266
+ refs.append((pending, head))
1267
+ pending_class_refs = []
1268
+ pending = None
1269
+ else: # '}'
1270
+ while stack and stack[-1][1] == depth:
1271
+ stack.pop()
1272
+ if depth > 0:
1273
+ depth -= 1
1274
+ idx = m.end()
1275
+ return refs
1276
+
1277
+
1278
+ def _annotation_reference_edges(
1279
+ ref_sources: "list[tuple[str, list[str], list[tuple[str, str]]]]",
1280
+ all_symbols: "list[SymbolRecord]",
1281
+ relations: "list[RelationEdge]",
1282
+ same_pkg_map: "dict[str, dict[str, str]]",
1283
+ ) -> "list[RelationEdge]":
1284
+ """Emit `references` edges for static constants read inside annotation args.
1285
+
1286
+ BUG #2 (Broadleaf field test): `@AdminPresentation(group = GroupName.General)`
1287
+ is a real reference to the constant-holder nested class `GroupName`, but the
1288
+ static call-graph never counted annotation-argument reads as edges. The
1289
+ referenced nested classes therefore had zero in-degree and were reported as
1290
+ false `statically_unreferenced` dead code (~95% of that list on Broadleaf).
1291
+
1292
+ Resolution for a bare head type T referenced from class C:
1293
+ import / same-package / wildcard → direct FQN, else
1294
+ nested-type lookup: a unique repo nested type named T, or — when the simple
1295
+ name is ambiguous (Broadleaf has 25 `GroupName`s) — the one declared by C or
1296
+ one of C's (transitive) supertypes. Unresolvable / ambiguous refs are
1297
+ dropped (honest: no guessed edge).
1298
+ """
1299
+ class_fqns: set[str] = {
1300
+ s.symbol for s in all_symbols
1301
+ if s.type in ("class", "interface") and "#" not in s.symbol
1302
+ }
1303
+ # enclosing_of[nested_fqn] = outer_fqn; nested_by_simple[simple] = {fqns}.
1304
+ enclosing_of: dict[str, str] = {}
1305
+ nested_by_simple: dict[str, set[str]] = {}
1306
+ for fqn in class_fqns:
1307
+ prefix = fqn
1308
+ while True:
1309
+ cut = prefix.rfind(".")
1310
+ if cut < 0:
1311
+ break
1312
+ prefix = prefix[:cut]
1313
+ if prefix in class_fqns:
1314
+ enclosing_of[fqn] = prefix
1315
+ nested_by_simple.setdefault(fqn[len(prefix) + 1:], set()).add(fqn)
1316
+ break
1317
+
1318
+ direct_super: dict[str, set[str]] = {}
1319
+ for e in relations:
1320
+ if e.type in ("extends", "implements"):
1321
+ direct_super.setdefault(e.from_symbol, set()).add(e.to_symbol)
1322
+
1323
+ def _super_closure(fqn: str) -> set[str]:
1324
+ seen: set[str] = set()
1325
+ stack = [fqn]
1326
+ while stack:
1327
+ cur = stack.pop()
1328
+ for sup in direct_super.get(cur, ()):
1329
+ if sup not in seen:
1330
+ seen.add(sup)
1331
+ stack.append(sup)
1332
+ return seen
1333
+
1334
+ edges: list[RelationEdge] = []
1335
+ seen_edges: set[tuple[str, str]] = set()
1336
+ for package, raw_imports, ref_pairs in ref_sources:
1337
+ if not ref_pairs:
1338
+ continue
1339
+ import_map: dict[str, str] = {}
1340
+ wildcard_pkgs: list[str] = []
1341
+ for fq in raw_imports:
1342
+ parts = fq.split(".")
1343
+ if parts[-1] == "*":
1344
+ wildcard_pkgs.append(fq[:-2])
1345
+ else:
1346
+ import_map[parts[-1]] = fq
1347
+ pkg_types = same_pkg_map.get(package, {})
1348
+
1349
+ def _resolve_head(head: str, cur_class: str) -> "Optional[str]":
1350
+ fq = import_map.get(head) or pkg_types.get(head)
1351
+ if fq:
1352
+ return fq
1353
+ for wp in wildcard_pkgs:
1354
+ cand = same_pkg_map.get(wp, {}).get(head)
1355
+ if cand:
1356
+ return cand
1357
+ # cur_class itself may BE the head (self-qualified nested ref).
1358
+ if cur_class.rsplit(".", 1)[-1] == head:
1359
+ return cur_class
1360
+ cands = nested_by_simple.get(head)
1361
+ if not cands:
1362
+ return None
1363
+ if len(cands) == 1:
1364
+ return next(iter(cands))
1365
+ allowed = {cur_class} | _super_closure(cur_class)
1366
+ matched = [c for c in cands if enclosing_of.get(c) in allowed]
1367
+ if len(matched) == 1:
1368
+ return matched[0]
1369
+ self_nested = f"{cur_class}.{head}"
1370
+ if self_nested in cands:
1371
+ return self_nested
1372
+ return None
1373
+
1374
+ def _resolve_chain(chain: str, cur_class: str) -> "Optional[str]":
1375
+ parts = chain.split(".")
1376
+ fqn = _resolve_head(parts[0], cur_class)
1377
+ if not fqn:
1378
+ return None
1379
+ # Walk the remaining segments through KNOWN nested types so a
1380
+ # `Outer.Nested.CONST` reference credits the nested holder actually read,
1381
+ # not merely the outer type. Stop at the first non-type segment (the
1382
+ # constant/field), which is the deepest resolvable class.
1383
+ for seg in parts[1:]:
1384
+ nxt = f"{fqn}.{seg}"
1385
+ if nxt in class_fqns:
1386
+ fqn = nxt
1387
+ else:
1388
+ break
1389
+ return fqn
1390
+
1391
+ for cur_class, chain in ref_pairs:
1392
+ if cur_class not in class_fqns:
1393
+ continue
1394
+ target = _resolve_chain(chain, cur_class)
1395
+ if not target or target == cur_class or target not in class_fqns:
1396
+ continue
1397
+ key = (cur_class, target)
1398
+ if key in seen_edges:
1399
+ continue
1400
+ seen_edges.add(key)
1401
+ edges.append(RelationEdge(
1402
+ from_symbol=cur_class,
1403
+ to_symbol=target,
1404
+ type="references",
1405
+ confidence="high",
1406
+ evidence={"type": "annotation_value", "value": chain},
1407
+ ))
1408
+ return edges
1409
+
1410
+
1130
1411
  # Reserved words that read like calls (`if (`, `for (`, …). Can never be sibling
1131
1412
  # method names (Java reserves them), but guard the intra-class scan anyway.
1132
1413
  _CALL_KEYWORDS: frozenset[str] = frozenset({
@@ -3374,6 +3655,9 @@ def build_repo_ir(
3374
3655
  )
3375
3656
 
3376
3657
  _per_file: list[tuple[str, str, str, list[str], list[SymbolRecord]]] = []
3658
+ # (package, imports, annotation-ref-pairs) for the reference-graph post-pass,
3659
+ # collected from BOTH fully-parsed and pre-scan-skipped files (BUG #2).
3660
+ _ann_ref_sources: list[tuple[str, list[str], list[tuple[str, str]]]] = []
3377
3661
  for rel_path in sorted(file_paths):
3378
3662
  abs_path = root / rel_path
3379
3663
  try:
@@ -3400,23 +3684,31 @@ def build_repo_ir(
3400
3684
  # literals before scanning so only real declarations are captured. The name
3401
3685
  # must also start uppercase ([A-Z]) — Java type convention, matching the
3402
3686
  # precision of the full _CLASS_DECL_RE used on annotated files.
3403
- _decl_source = _STRING_LITERAL_RE.sub('', _strip_java_comments(source))
3404
- _min_syms: list[SymbolRecord] = []
3405
- for _cm in re.finditer(r'\b(?:class|interface|enum)\s+([A-Z]\w*)', _decl_source):
3406
- _cls_name = _cm.group(1)
3407
- _fqn = f"{_pkg}.{_cls_name}" if _pkg else _cls_name
3408
- _min_syms.append(SymbolRecord(
3409
- symbol=_fqn, type="class", confidence="medium",
3410
- declaring_file=rel_path,
3411
- ))
3687
+ _min_syms = _minimal_class_symbols(source, _pkg, rel_path)
3412
3688
  all_symbols.extend(_min_syms)
3413
- # No relations needed for non-annotated files
3689
+ # No relations needed for non-annotated files. BUG #2: but a pre-scan-
3690
+ # skipped file can still hold annotation-argument constant references —
3691
+ # e.g. a presentation interface whose class-level @AdminTabPresentation
3692
+ # references its own nested TabName/TabOrder holders. Capture those refs
3693
+ # (cheaply, no full parse) so the reference-graph post-pass can resolve
3694
+ # them and the holders are not flagged as false dead code.
3695
+ if "@" in source:
3696
+ _skipped_refs = _extract_annotation_type_refs(source, _pkg)
3697
+ if _skipped_refs:
3698
+ _imports = re.findall(
3699
+ r'^\s*import\s+(?:static\s+)?([\w.]+)\s*;', source, re.MULTILINE
3700
+ )
3701
+ _ann_ref_sources.append((_pkg, _imports, _skipped_refs))
3414
3702
  continue
3415
3703
  package, symbols, raw_imports = _extract_symbols(
3416
3704
  source, rel_path, extra_capture=_extra_capture
3417
3705
  )
3418
3706
  all_symbols.extend(symbols)
3419
3707
  _per_file.append((rel_path, source, package, raw_imports, symbols))
3708
+ if "@" in source:
3709
+ _parsed_refs = _extract_annotation_type_refs(source, package)
3710
+ if _parsed_refs:
3711
+ _ann_ref_sources.append((package, raw_imports, _parsed_refs))
3420
3712
 
3421
3713
  # Build {package: {simple_name: FQN}} from every class/interface found.
3422
3714
  _same_pkg_map: dict[str, dict[str, str]] = _build_same_package_map(all_symbols)
@@ -3454,6 +3746,15 @@ def build_repo_ir(
3454
3746
 
3455
3747
  all_relations.extend(relations)
3456
3748
 
3749
+ # BUG #2 (Broadleaf): annotation-argument static-constant reads
3750
+ # (@Anno(attr = SomeType.CONST)) are real references — emit them as edges so
3751
+ # constant-holder types are not reported as zero-caller dead code. Runs as a
3752
+ # global post-pass because bare inherited-nested references (e.g. `GroupName`
3753
+ # from an implemented interface) need the repo-wide supertype/nested index.
3754
+ all_relations.extend(
3755
+ _annotation_reference_edges(_ann_ref_sources, all_symbols, all_relations, _same_pkg_map)
3756
+ )
3757
+
3457
3758
  spring_summary = _build_spring_summary(all_symbols)
3458
3759
 
3459
3760
  # Deduplicate relations
@@ -4434,6 +4735,41 @@ def extract_java_endpoints(root: Path) -> "dict[str, Any]":
4434
4735
  f"endpoints only."
4435
4736
  )
4436
4737
  result.setdefault("warnings", []).append(_msg)
4738
+
4739
+ # BUG #7 (Alfresco field test): a bare "total: 0" must never be read by an
4740
+ # automated consumer as "this service exposes no public API". Emit a first-class
4741
+ # structured `zero_result_reason` (mirroring integrations.coverage_note /
4742
+ # security_model_detail.reason) naming the routing patterns that WERE searched and
4743
+ # any non-Spring surface detected, so a 0 is understood as "no RECOGNIZED pattern",
4744
+ # not "no surface". Only when the Spring/annotation surface is genuinely empty.
4745
+ if not endpoints:
4746
+ _searched = (
4747
+ "@RestController/@Controller + @RequestMapping/@GetMapping/@PostMapping/"
4748
+ "@PutMapping/@DeleteMapping/@PatchMapping, JAX-RS @Path, functional "
4749
+ "RouterFunction routing, and OpenAPI-spec-recovered routes"
4750
+ )
4751
+ if _nonspring_total:
4752
+ _fw_names = {
4753
+ "webscripts": "Alfresco WebScripts (XML descriptor + handler class)",
4754
+ "jax_rs": "JAX-RS",
4755
+ "servlets": "mapped Servlets",
4756
+ }
4757
+ _detected = ", ".join(
4758
+ _fw_names.get(k, k) for k, v in _nonspring.items() if v
4759
+ )
4760
+ result["zero_result_reason"] = (
4761
+ f"0 endpoints recognized. Searched: {_searched}. A NON-Spring REST "
4762
+ f"surface WAS detected ({_detected}) but is not statically modeled — "
4763
+ f"do NOT read this as 'no public API'. Inspect that framework's routing "
4764
+ f"(e.g. *.desc.xml descriptors for WebScripts) to enumerate the surface."
4765
+ )
4766
+ else:
4767
+ result["zero_result_reason"] = (
4768
+ f"0 endpoints recognized. Searched: {_searched}. No recognized routing "
4769
+ f"construct was found; a framework this analyzer does not model may "
4770
+ f"still expose an HTTP surface — verify manually before concluding the "
4771
+ f"service has no public API."
4772
+ )
4437
4773
  return result
4438
4774
 
4439
4775
 
sourcecode/serializer.py CHANGED
@@ -702,7 +702,12 @@ def _bootstrap_structured(eps: list) -> "Optional[dict[str, Any]]":
702
702
  kind = getattr(ep, "kind", "")
703
703
  stem = _Path(path).stem
704
704
 
705
- if kind == "application" or any(k in stem for k in ("Application", "Main", "Initializer", "Bootstrap")):
705
+ # BUG #3 (Alfresco field test): bootstrap membership is driven by the
706
+ # VERIFIED entry kind ("application" is emitted only when the detector
707
+ # confirmed a main()/bootstrap annotation), never by a `*Application*`
708
+ # filename alone — XSD-generated Application.java model classes must not
709
+ # appear here.
710
+ if kind == "application":
706
711
  if path not in seen_b:
707
712
  seen_b.add(path)
708
713
  bootstrap.append(path)
@@ -491,6 +491,17 @@ def run_security_audit(
491
491
  limitations=_sec_limitations,
492
492
  metadata={
493
493
  "endpoints_analyzed": len(cir.endpoints),
494
+ "endpoints_analyzed_note": (
495
+ "Count of canonical endpoints analyzed for security: Spring-MVC/JAX-RS "
496
+ "annotated handlers plus OpenAPI-spec-recovered routes. Built from the "
497
+ "SAME route surface as the `endpoints` command and with the SAME "
498
+ "exclusion of framework dynamic-admin FQN-shaped paths, but "
499
+ "DEDUPLICATED by (method, path, controller, handler) — so this value is "
500
+ "<= the `endpoints` command total, the small difference being "
501
+ "multi-prefix routes that collapse to one canonical endpoint here. It "
502
+ "does not include the non-Spring REST surface (`endpoints` reports that "
503
+ "separately under non_spring_rest_surface)."
504
+ ),
494
505
  "security_model": cir.metadata.get("security_model", "unknown"),
495
506
  "analysis_time_ms": elapsed_ms,
496
507
  },
@@ -56,13 +56,61 @@ _WRITE_METHOD_RE = re.compile(
56
56
  # _CATCH_SWALLOW_RE is retained for reference but replaced by brace-counting
57
57
  # extraction in _has_swallowed_exception to avoid false positives from nested
58
58
  # braces (nested if/try blocks inside catch terminating the match prematurely).
59
- _CATCH_HEADER_RE = re.compile(r'\bcatch\s*\([^)]+\)\s*\{')
59
+ _CATCH_HEADER_RE = re.compile(r'\bcatch\s*\(([^)]+)\)\s*\{')
60
60
  _LOG_IN_CATCH_RE = re.compile(r'\b(?:log|logger|LOG|System\.out|e\.print)\b')
61
61
  _RETHROW_IN_CATCH_RE = re.compile(r'\bthrow\b')
62
62
  # Non-trivial return (method call) inside a catch block indicates recovery, not
63
63
  # silent swallowing — e.g. `return findNextId(idType)` after creating a missing row.
64
64
  _RECOVERY_RETURN_RE = re.compile(r'\breturn\s+\w[\w.<>]*\s*\(')
65
65
 
66
+ # Exception types whose catch is a documented part of normal control flow, not a
67
+ # masked failure. Catching these is the standard JPA/Spring-Data idiom (e.g.
68
+ # Query.getSingleResult() throws NoResultException by contract when 0 rows match;
69
+ # a tolerant insert catches EntityExistsException on a race). Catching one of
70
+ # these must never trigger TX-005. (Broadleaf field test: 4/4 TX-005 findings
71
+ # were this idiom — 100% FP rate.)
72
+ _EXPECTED_CONTROL_FLOW_EXCEPTIONS = frozenset({
73
+ "NoResultException",
74
+ "NonUniqueResultException",
75
+ "EntityExistsException",
76
+ "EntityNotFoundException",
77
+ "EmptyResultDataAccessException",
78
+ "IncorrectResultSizeDataAccessException",
79
+ })
80
+ # A caught type is "expected" if any of its simple-name tokens is in the set above.
81
+ _CATCH_TYPE_TOKEN_RE = re.compile(r'[A-Za-z_][A-Za-z0-9_.]*')
82
+ # Lexical markers of a deliberate, developer-documented no-op inside a catch block.
83
+ # When the catch body's leading comment says the swallow is intentional, TX-005 is
84
+ # not reporting a bug — it is second-guessing an explicit decision.
85
+ _INTENTIONAL_SWALLOW_COMMENT_RE = re.compile(
86
+ r'(?://|/\*|\*)[^\n]*?\b('
87
+ r'intentional|intentionally|acceptable|expected|'
88
+ r'do\s+nothing|no[\s\-]?op|noop|ignore[ds]?|ignoring|'
89
+ r'on\s+purpose|safe\s+to\s+ignore|by\s+design|not\s+an?\s+error'
90
+ r')\b',
91
+ re.IGNORECASE,
92
+ )
93
+
94
+
95
+ def _caught_types(catch_header_inner: str) -> list[str]:
96
+ """Extract caught exception simple-name tokens from a catch header inner text.
97
+
98
+ `catch_header_inner` is the text inside the parens, e.g.
99
+ "NoResultException nre" or "IllegalStateException | IOException e".
100
+ Returns simple names only (last dotted segment), lowercase-preserving.
101
+ """
102
+ # Drop the trailing variable name; split multi-catch on '|'.
103
+ types: list[str] = []
104
+ for alt in catch_header_inner.split('|'):
105
+ toks = _CATCH_TYPE_TOKEN_RE.findall(alt)
106
+ # First token(s) are the type; the last bare identifier is the var name.
107
+ # A type token contains '.' or starts uppercase; the var name is lowercase.
108
+ for tok in toks:
109
+ simple = tok.rsplit('.', 1)[-1]
110
+ if simple and simple[0].isupper():
111
+ types.append(simple)
112
+ return types
113
+
66
114
 
67
115
  def _extract_method_body(source: str, method_name: str) -> str:
68
116
  """Extract the first method body matching method_name using brace counting.
@@ -597,7 +645,7 @@ class _TX005ExceptionSwallowing:
597
645
  depth -= 1
598
646
  i += 1
599
647
  body = source[brace_pos:i]
600
- for catch_block in self._extract_catch_blocks(body):
648
+ for caught_header, catch_block in self._extract_catch_blocks(body):
601
649
  if not _LOG_IN_CATCH_RE.search(catch_block):
602
650
  continue
603
651
  if _RETHROW_IN_CATCH_RE.search(catch_block):
@@ -605,18 +653,31 @@ class _TX005ExceptionSwallowing:
605
653
  # Non-trivial return (method call) indicates recovery, not swallowing.
606
654
  if _RECOVERY_RETURN_RE.search(catch_block):
607
655
  continue
656
+ # Catching an exception that is part of normal control flow (JPA
657
+ # getSingleResult / tolerant insert) is not error-masking — skip.
658
+ caught = _caught_types(caught_header)
659
+ if any(t in _EXPECTED_CONTROL_FLOW_EXCEPTIONS for t in caught):
660
+ continue
661
+ # A developer-documented deliberate no-op is an explicit decision,
662
+ # not a bug the tool should flag.
663
+ if _INTENTIONAL_SWALLOW_COMMENT_RE.search(catch_block):
664
+ continue
608
665
  return True
609
666
  return False
610
667
 
611
668
  @staticmethod
612
- def _extract_catch_blocks(body: str) -> list[str]:
613
- """Extract full catch block bodies from a method body using brace counting.
614
-
615
- Handles nested braces correctly unlike a simple [^}]* regex which
616
- terminates at the first nested '}' inside the catch block.
669
+ def _extract_catch_blocks(body: str) -> list[tuple[str, str]]:
670
+ """Extract (caught-type-header, catch-body) pairs from a method body.
671
+
672
+ Uses brace counting so nested braces inside the catch body do not
673
+ terminate the match prematurely (unlike a simple [^}]* regex). The
674
+ caught-type-header is the text inside the catch parens (e.g.
675
+ "NoResultException nre"), used to distinguish expected-control-flow
676
+ exceptions from genuinely swallowed failures.
617
677
  """
618
- blocks: list[str] = []
678
+ blocks: list[tuple[str, str]] = []
619
679
  for m in _CATCH_HEADER_RE.finditer(body):
680
+ header_inner = m.group(1)
620
681
  brace_pos = m.end() - 1
621
682
  depth = 1
622
683
  i = brace_pos + 1
@@ -627,7 +688,7 @@ class _TX005ExceptionSwallowing:
627
688
  elif c == '}':
628
689
  depth -= 1
629
690
  i += 1
630
- blocks.append(body[brace_pos:i])
691
+ blocks.append((header_inner, body[brace_pos:i]))
631
692
  return blocks
632
693
 
633
694
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 1.71.0
3
+ Version: 1.73.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
@@ -1,13 +1,13 @@
1
- sourcecode/__init__.py,sha256=y7H6BpC2bXN93kgVpSIJ6MrHneDY6yl0VNPo_qOu4Ts,103
1
+ sourcecode/__init__.py,sha256=5AfqhGbJzMplmuXdrkAIi2e7-XhaUzzC_uaeeU_260s,103
2
2
  sourcecode/adaptive_scanner.py,sha256=XffluXKzJUXrMtjEiAOnSNPZnztdIcts17T9ouHeID0,10521
3
3
  sourcecode/architecture_analyzer.py,sha256=r_xf-SWXwUm3nVQMCSXHJ3M8zKsQP7Ze8Nqf6TVLRq8,45998
4
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
- sourcecode/canonical_ir.py,sha256=DEwucOPJguLsVtg5cV8mWXNi112l5jmBhv73KGGebVk,24849
7
+ sourcecode/canonical_ir.py,sha256=IftvTlokcSLbZYHjXQu55FnS_X1LU9bXGsGxm6ujPtw,25793
8
8
  sourcecode/cir_graphs.py,sha256=9G0HHj1kw2325IDyzo2OpX73BNswEckecf4MZUXB4JM,12078
9
9
  sourcecode/classifier.py,sha256=YTTCoRdcLEFRVcql9Ow1dE7eYQj0jq2rgx32bRDnb1k,13852
10
- sourcecode/cli.py,sha256=VqAiGRauTO-s8yBJh2JMldha3VKx0jTXC0tOvRpWQkI,283297
10
+ sourcecode/cli.py,sha256=UTcQWF3BGvEZ9I-gPyU9LYDGa-8AmNa_jQYO96hTAqw,288485
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=GbcruAyzlmseV3o2rjeyxGQxToCfSYHJjKG3N05NVbQ,19897
23
+ sourcecode/explain.py,sha256=2Og-r74yUm38wAF6qD28Gt0jxi1Ygdx33hwp6-NH0xs,20348
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,12 +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=h0leIhlWvSjYq3F99LxvLIDLrJ-xPYxWAREG4LkqZ-4,61190
32
- sourcecode/integration_detector.py,sha256=HlVdiVki7i62t3pJ0lRjTQQ7WZqPDcPLTqBGAfjFjVQ,17883
32
+ sourcecode/integration_detector.py,sha256=9_SeZQXcatdoCfxl9ciSbRJ84IQkSJooAcv0Efh9FgM,21376
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=VyA-2n5J1Cqf1XxhQ143pf5Y7IvGwolMM1F4OCLOOaU,102628
37
+ sourcecode/migrate_check.py,sha256=Fz12IrMTQWjvbT7melwJGf1aNqQDtzbyRPd_bDA7hdw,105025
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,21 +47,21 @@ 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=tj3e_uxkVGTRWfzkf2YoGO-oGakSO4DmWrUQow0dFGk,231201
50
+ sourcecode/repository_ir.py,sha256=YUuyWIFeUve26RGaWPIk1Qbj04u6sTyR1-qPs-gYznM,245995
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
54
54
  sourcecode/schema.py,sha256=aHNXDf8LGyUC8ZDE_VS9kiskC2-Oswhi_WnpdGy6HDw,24897
55
55
  sourcecode/security_config.py,sha256=_m8oQAy2gP7Ho2I-IIMTFFjFVWbJIGlLEOiAWK2TDjs,3503
56
56
  sourcecode/semantic_analyzer.py,sha256=4OdG6tTSnTvq3_dSWMbQu8Ad1ndSCKeG-b9qM4hIxkw,89176
57
- sourcecode/serializer.py,sha256=MSxYZ-_UYDPKMvg-hVk-MnKN-TfrmxXL202siCssa9U,129110
57
+ sourcecode/serializer.py,sha256=hv5pQQQa20XxPwcKLlLAgvXwWubm-B-cgQept6x-H7U,129373
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
60
  sourcecode/spring_impact.py,sha256=1Eu1vkdwTVsw92iBEJDe_i_FaSf4uGH9Rb0eSgIAAZc,57542
61
61
  sourcecode/spring_model.py,sha256=zOAgFmrRbG4a6KLm1TJl55aWMyPNsz3OS3FSczqPG6A,16594
62
- sourcecode/spring_security_audit.py,sha256=XtPJ1SXlZJ8k6VYmaWuAp7Bbir4UmreAL7doIGQ5I7o,20595
62
+ sourcecode/spring_security_audit.py,sha256=xrdv3MaR_m2wblA6a8MRWM7attvZeoPXKsoRO_R2lY4,21401
63
63
  sourcecode/spring_semantic.py,sha256=O1nKSGVzlukuxLHQVuCPxc-XrcrMFxwlHA20_dmEGgM,13307
64
- sourcecode/spring_tx_analyzer.py,sha256=FdFcyqPp3aT9oJ-PKrnXcTA6s69wdvzG-NBm0GMGPTU,30717
64
+ sourcecode/spring_tx_analyzer.py,sha256=iH9mK0Tgvhgy_Ee2r13KA_RMf8HKN-pjCKQGuyCCBjM,33812
65
65
  sourcecode/summarizer.py,sha256=hV-kPFXXh15GnIvmLZnmVWTEbmr35C-yKVtPiR25U2g,21802
66
66
  sourcecode/tree_utils.py,sha256=8GAkIfQAsvtEudIeW1l4ooH_oRtrWR8cpJQJsEa_Pfw,2093
67
67
  sourcecode/validation_surface.py,sha256=2Ojfzhw9R4XpemAFqb9RXf4FKyI3DYjGo4Dp0AHZMQo,22600
@@ -76,10 +76,10 @@ 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=hyAlTo8_y-tzNVun3zvHmWN8RdG--sdLquCI7fQBAVc,34263
79
+ sourcecode/detectors/java.py,sha256=xlp4mqd26QWJf3MWqmTbD0IND_VEVzPHlGaLC4MNXNA,39028
80
80
  sourcecode/detectors/jvm_ext.py,sha256=EgHJ5W8EE-ZTN9V607mVzohyKgZE8Mc2jCi-DF8RAZU,2616
81
81
  sourcecode/detectors/nodejs.py,sha256=Hg3Gmr7yIMJFiLoDwOTk2wtu00wxIs6kZf-oQujTFUA,13187
82
- sourcecode/detectors/parsers.py,sha256=ugPg8yNUf0Ai1gA7Fnn6wAkYGFjTxRodSP3IeViYJJ4,2290
82
+ sourcecode/detectors/parsers.py,sha256=ug9K31tyHqinmv0HkIVQVjdTZpBv67FYKAEf52YXOSM,3178
83
83
  sourcecode/detectors/php.py,sha256=W_AQD0WMVDdWHa9h_ilX6W8XSpz0X4ctpMK2WXfXf1I,1887
84
84
  sourcecode/detectors/project.py,sha256=ghWWOlqg2_uywzeZX573CCkFWQPlc8bBGvvX1BfIDYI,8315
85
85
  sourcecode/detectors/python.py,sha256=i2_Wtk_p0BJx5R8gBQ8NaQByzJ8zEfZkw9NNpKlvOYM,10486
@@ -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.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,,
107
+ sourcecode-1.73.0.dist-info/METADATA,sha256=s089V_1fTpsg8m9aRjhv4mv0miXocG_qV5uAyzr2L60,48339
108
+ sourcecode-1.73.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
109
+ sourcecode-1.73.0.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
110
+ sourcecode-1.73.0.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
111
+ sourcecode-1.73.0.dist-info/RECORD,,