sourcecode 1.35.31__py3-none-any.whl → 1.35.33__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sourcecode/__init__.py +1 -1
- sourcecode/architecture_analyzer.py +1 -1
- sourcecode/classifier.py +4 -0
- sourcecode/cli.py +204 -187
- sourcecode/dependency_analyzer.py +22 -1
- sourcecode/file_chunker.py +54 -24
- sourcecode/migrate_check.py +23 -8
- sourcecode/path_filters.py +2 -1
- sourcecode/repository_ir.py +73 -8
- sourcecode/spring_impact.py +9 -3
- sourcecode/spring_model.py +37 -3
- sourcecode/spring_security_audit.py +3 -6
- sourcecode/spring_semantic.py +34 -7
- sourcecode/spring_tx_analyzer.py +1 -1
- {sourcecode-1.35.31.dist-info → sourcecode-1.35.33.dist-info}/METADATA +3 -3
- {sourcecode-1.35.31.dist-info → sourcecode-1.35.33.dist-info}/RECORD +19 -19
- {sourcecode-1.35.31.dist-info → sourcecode-1.35.33.dist-info}/WHEEL +0 -0
- {sourcecode-1.35.31.dist-info → sourcecode-1.35.33.dist-info}/entry_points.txt +0 -0
- {sourcecode-1.35.31.dist-info → sourcecode-1.35.33.dist-info}/licenses/LICENSE +0 -0
sourcecode/__init__.py
CHANGED
|
@@ -237,7 +237,7 @@ class ArchitectureAnalyzer:
|
|
|
237
237
|
return ArchitectureAnalysis(
|
|
238
238
|
requested=True,
|
|
239
239
|
pattern="unknown",
|
|
240
|
-
limitations=["
|
|
240
|
+
limitations=["Architecture not inferred: insufficient source files in project"],
|
|
241
241
|
evidence=[{"type": "none", "paths": [], "reason": "insufficient source files", "confidence": "high"}],
|
|
242
242
|
tentative=False,
|
|
243
243
|
)
|
sourcecode/classifier.py
CHANGED
sourcecode/cli.py
CHANGED
|
@@ -1227,159 +1227,164 @@ def main(
|
|
|
1227
1227
|
except Exception:
|
|
1228
1228
|
pass
|
|
1229
1229
|
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
)
|
|
1230
|
+
_excl_key = (
|
|
1231
|
+
",".join(sorted(e.strip() for e in exclude.split(",") if e.strip()))
|
|
1232
|
+
if exclude else ""
|
|
1233
|
+
)
|
|
1235
1234
|
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1235
|
+
# ── Core (analysis) flags: affect which analyzers run + scan config ──
|
|
1236
|
+
# Use effective_depth (not raw depth) so Java auto-adjustment is captured.
|
|
1237
|
+
# acv = ANALYZER_CACHE_VERSION — bumped only when analysis logic/schema
|
|
1238
|
+
# changes, NOT on every package release. Prevents patch-bump cache wipes.
|
|
1239
|
+
_core_flags_str = (
|
|
1240
|
+
f"acv={_cache_mod.ANALYZER_CACHE_VERSION},"
|
|
1241
|
+
f"dep={dependencies},gm={graph_modules},"
|
|
1242
|
+
f"docs={docs},fm={full_metrics},sem={semantics},"
|
|
1243
|
+
f"arch={architecture},gc={git_context},em={env_map},"
|
|
1244
|
+
f"cn={code_notes},"
|
|
1245
|
+
f"ex={_excl_key},depth={effective_depth}"
|
|
1246
|
+
)
|
|
1247
|
+
_core_h = _hashlib.sha256(_core_flags_str.encode()).hexdigest()[:8]
|
|
1248
|
+
if _git_sha and _git_root_str:
|
|
1249
1249
|
_core_key = f"{_git_sha}-{_core_h}"
|
|
1250
|
-
|
|
1251
|
-
#
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1250
|
+
else:
|
|
1251
|
+
# No git history (untracked/no-commit repo) — stable synthetic key
|
|
1252
|
+
# scoped per repo path via cache_dir(); invalidated by --no-cache or cache clear.
|
|
1253
|
+
_core_key = f"nogit-{_core_h}"
|
|
1254
|
+
|
|
1255
|
+
# ── View flags: output presentation only (no re-analysis needed) ──
|
|
1256
|
+
_view_flags_str = (
|
|
1257
|
+
f"c={compact},ag={agent},mode={mode},fmt={format},full={full},"
|
|
1258
|
+
f"co={changed_only},tree={tree},nt={no_tree},"
|
|
1259
|
+
f"rb={rank_by},sym={symbol},ep={entrypoints_only},"
|
|
1260
|
+
f"nr={no_redact},gd={graph_detail},dd={docs_depth},"
|
|
1261
|
+
f"mn={max_nodes},ge={graph_edges},mi={max_importers},"
|
|
1262
|
+
f"eg={emit_graph}"
|
|
1263
|
+
)
|
|
1264
|
+
_view_h = _hashlib.sha256(_view_flags_str.encode()).hexdigest()[:8]
|
|
1265
|
+
|
|
1266
|
+
# ── Lookup ──────────────────────────────────────────────────────
|
|
1267
|
+
# Step 1: try L1 to obtain the core_hash needed for L2 key
|
|
1268
|
+
_l1_result = _cache_mod.read_core(target, _core_key)
|
|
1269
|
+
|
|
1270
|
+
# P1-A: --env-map misses L1 when base (em=False) exists.
|
|
1271
|
+
# Try the base key so env analysis can be injected lazily (<1 s)
|
|
1272
|
+
# instead of triggering a 17 s full rescan.
|
|
1273
|
+
_l1_needs_env_inject = False
|
|
1274
|
+
if _l1_result is None and env_map:
|
|
1275
|
+
_base_flags = _core_flags_str.replace(",em=True,", ",em=False,")
|
|
1276
|
+
_base_h8 = _hashlib.sha256(_base_flags.encode()).hexdigest()[:8]
|
|
1277
|
+
_sha_prefix = _git_sha if _git_sha else "nogit"
|
|
1278
|
+
_base_key = f"{_sha_prefix}-{_base_h8}"
|
|
1279
|
+
_base_result = _cache_mod.read_core(target, _base_key)
|
|
1280
|
+
if _base_result is not None:
|
|
1281
|
+
_l1_result = _base_result
|
|
1282
|
+
_l1_needs_env_inject = True
|
|
1283
|
+
|
|
1284
|
+
if _l1_result is not None:
|
|
1285
|
+
_core_dict_l1, _core_hash = _l1_result
|
|
1286
|
+
_view_key = f"{_core_hash}-{_view_h}"
|
|
1287
|
+
|
|
1288
|
+
# Step 2: try L2 (exact view match).
|
|
1289
|
+
# Skip L2 for --changed-only: the stored view is a previous
|
|
1290
|
+
# diff snapshot that is stale for the current diff.
|
|
1291
|
+
if not changed_only:
|
|
1292
|
+
_cache_hit_content = _cache_mod.read_view(target, _view_key)
|
|
1293
|
+
|
|
1294
|
+
# Step 3: L1 hit but L2 miss → rebuild view from core dict
|
|
1295
|
+
if _cache_hit_content is None:
|
|
1296
|
+
try:
|
|
1297
|
+
from sourcecode.serializer import build_view_from_core as _bvfc
|
|
1298
|
+
_rebuilt = _bvfc(
|
|
1299
|
+
_core_dict_l1,
|
|
1300
|
+
compact=compact,
|
|
1301
|
+
agent=agent,
|
|
1302
|
+
full=full,
|
|
1303
|
+
no_tree=no_tree,
|
|
1304
|
+
tree=tree,
|
|
1305
|
+
)
|
|
1306
|
+
# P1-A: inject env analysis when base L1 (em=False) was used.
|
|
1307
|
+
# EnvAnalyzer walks only env/config files — typically <1 s.
|
|
1308
|
+
if _rebuilt is not None and _l1_needs_env_inject and compact:
|
|
1309
|
+
try:
|
|
1310
|
+
from sourcecode.env_analyzer import EnvAnalyzer as _EnvA_p1a
|
|
1311
|
+
_env_r_p1a, _env_s_p1a = _EnvA_p1a().analyze(target, {})
|
|
1312
|
+
if _env_s_p1a and (getattr(_env_s_p1a, "total", 0) or _env_r_p1a):
|
|
1313
|
+
_es_p1a: dict = {
|
|
1314
|
+
"total": getattr(_env_s_p1a, "total", 0),
|
|
1315
|
+
"required": getattr(_env_s_p1a, "required_count", 0),
|
|
1316
|
+
}
|
|
1317
|
+
_cats = getattr(_env_s_p1a, "categories", None)
|
|
1318
|
+
if _cats:
|
|
1319
|
+
_es_p1a["categories"] = _cats
|
|
1320
|
+
_rebuilt = dict(_rebuilt)
|
|
1321
|
+
_rebuilt["env_summary"] = _es_p1a
|
|
1322
|
+
if _env_r_p1a:
|
|
1323
|
+
_sorted_er = sorted(
|
|
1324
|
+
_env_r_p1a,
|
|
1325
|
+
key=lambda e: (
|
|
1326
|
+
not getattr(e, "required", False),
|
|
1327
|
+
getattr(e, "key", ""),
|
|
1328
|
+
),
|
|
1329
|
+
)
|
|
1330
|
+
_rebuilt["env_map"] = [
|
|
1331
|
+
{
|
|
1332
|
+
"key": getattr(e, "key", ""),
|
|
1333
|
+
**({"required": True} if getattr(e, "required", False) else {}),
|
|
1334
|
+
**({"category": getattr(e, "category", None)} if getattr(e, "category", None) else {}),
|
|
1335
|
+
}
|
|
1336
|
+
for e in _sorted_er[:15]
|
|
1337
|
+
]
|
|
1338
|
+
except Exception:
|
|
1339
|
+
pass # env inject failed — continue without env data
|
|
1340
|
+
if _rebuilt is not None:
|
|
1341
|
+
# Apply redaction
|
|
1342
|
+
if not no_redact:
|
|
1343
|
+
from sourcecode.redactor import redact_dict as _red_l1
|
|
1344
|
+
_rebuilt = _red_l1(_rebuilt)
|
|
1345
|
+
# Apply output budget
|
|
1346
|
+
if agent:
|
|
1347
|
+
from sourcecode.output_budget import (
|
|
1348
|
+
trim_to_budget as _trim_l1,
|
|
1349
|
+
BUDGET_AGENT,
|
|
1350
|
+
)
|
|
1351
|
+
_rebuilt = _trim_l1(_rebuilt, BUDGET_AGENT, label="agent")
|
|
1352
|
+
elif compact:
|
|
1353
|
+
from sourcecode.output_budget import (
|
|
1354
|
+
trim_to_budget as _trim_l1c,
|
|
1355
|
+
BUDGET_COMPACT,
|
|
1356
|
+
)
|
|
1357
|
+
_rebuilt = _trim_l1c(_rebuilt, BUDGET_COMPACT, label="compact")
|
|
1358
|
+
# Serialize
|
|
1359
|
+
if format == "yaml":
|
|
1360
|
+
from io import StringIO as _SIO_L1
|
|
1361
|
+
from ruamel.yaml import YAML as _YAML_L1
|
|
1362
|
+
_yl1 = _YAML_L1()
|
|
1363
|
+
_yl1.default_flow_style = False
|
|
1364
|
+
_yl1.representer.add_representer(
|
|
1365
|
+
type(None),
|
|
1366
|
+
lambda d, v: d.represent_scalar(
|
|
1367
|
+
"tag:yaml.org,2002:null", "null"
|
|
1368
|
+
),
|
|
1369
|
+
)
|
|
1370
|
+
_sl1 = _SIO_L1()
|
|
1371
|
+
_yl1.dump(_rebuilt, _sl1)
|
|
1372
|
+
_cache_hit_content = _sl1.getvalue()
|
|
1373
|
+
else:
|
|
1374
|
+
import json as _json_l1
|
|
1375
|
+
_cache_hit_content = _json_l1.dumps(
|
|
1376
|
+
_rebuilt, indent=2, ensure_ascii=False
|
|
1377
|
+
)
|
|
1378
|
+
# Cache rebuilt view in L2 (skip for --changed-only: stale diff)
|
|
1379
|
+
if _cache_hit_content and not changed_only:
|
|
1380
|
+
_cache_mod.write_view(
|
|
1381
|
+
target,
|
|
1382
|
+
_view_key,
|
|
1383
|
+
_cache_hit_content,
|
|
1384
|
+
fmt=format,
|
|
1385
|
+
)
|
|
1386
|
+
except Exception:
|
|
1387
|
+
_cache_hit_content = None # rebuild failed → full analysis
|
|
1383
1388
|
|
|
1384
1389
|
except Exception:
|
|
1385
1390
|
_core_key = ""
|
|
@@ -2290,6 +2295,11 @@ def main(
|
|
|
2290
2295
|
try:
|
|
2291
2296
|
from sourcecode.ris import _has_uncommitted_changes as _huc_fresh
|
|
2292
2297
|
_uncommitted_fresh = _huc_fresh(target)
|
|
2298
|
+
# _huc_fresh uses --untracked-files=no — it misses repos where all
|
|
2299
|
+
# files are untracked (no staged/unstaged changes to tracked files).
|
|
2300
|
+
# Promote to True when _allowed_changed_files contains untracked files.
|
|
2301
|
+
if not _uncommitted_fresh and _allowed_changed_files:
|
|
2302
|
+
_uncommitted_fresh = True
|
|
2293
2303
|
except Exception:
|
|
2294
2304
|
_uncommitted_fresh = False
|
|
2295
2305
|
import datetime as _dt
|
|
@@ -2335,44 +2345,51 @@ def main(
|
|
|
2335
2345
|
import atexit as _atexit
|
|
2336
2346
|
import threading as _threading
|
|
2337
2347
|
|
|
2338
|
-
#
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
_bg_target,
|
|
2363
|
-
_vk,
|
|
2364
|
-
_bg_content,
|
|
2365
|
-
fmt=_bg_format,
|
|
2366
|
-
layers=_compute_analyzer_fingerprints(),
|
|
2348
|
+
# Pre-compute core dict in the main thread — avoids the 5-second atexit
|
|
2349
|
+
# join race on large repos where core_view(sm) itself takes seconds.
|
|
2350
|
+
# Background thread only does gzip + disk I/O (fast, bounded latency).
|
|
2351
|
+
_bg_core_dict: dict | None = None
|
|
2352
|
+
try:
|
|
2353
|
+
from sourcecode.serializer import core_view as _core_view_fn
|
|
2354
|
+
_bg_core_dict = _core_view_fn(sm)
|
|
2355
|
+
except Exception:
|
|
2356
|
+
pass
|
|
2357
|
+
|
|
2358
|
+
if _bg_core_dict is not None:
|
|
2359
|
+
_bg_target = target
|
|
2360
|
+
_bg_core_key = _core_key
|
|
2361
|
+
_bg_view_key = _view_key
|
|
2362
|
+
_bg_view_flags_str = _view_flags_str
|
|
2363
|
+
_bg_content = content
|
|
2364
|
+
_bg_format = format
|
|
2365
|
+
_bg_hashlib = _hashlib
|
|
2366
|
+
_bg_cache_mod = _cache_mod
|
|
2367
|
+
|
|
2368
|
+
def _write_cache_async() -> None:
|
|
2369
|
+
try:
|
|
2370
|
+
_written_core_hash = _bg_cache_mod.write_core(
|
|
2371
|
+
_bg_target, _bg_core_key, _bg_core_dict
|
|
2367
2372
|
)
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2373
|
+
if _written_core_hash:
|
|
2374
|
+
_vk = _bg_view_key
|
|
2375
|
+
if not _vk:
|
|
2376
|
+
_wvh = _bg_hashlib.sha256(_bg_view_flags_str.encode()).hexdigest()[:8]
|
|
2377
|
+
_vk = f"{_written_core_hash}-{_wvh}"
|
|
2378
|
+
_bg_cache_mod.write_view(
|
|
2379
|
+
_bg_target,
|
|
2380
|
+
_vk,
|
|
2381
|
+
_bg_content,
|
|
2382
|
+
fmt=_bg_format,
|
|
2383
|
+
layers=_compute_analyzer_fingerprints(),
|
|
2384
|
+
)
|
|
2385
|
+
from sourcecode.cache import cache_dir as _cdir, _gc as _run_gc
|
|
2386
|
+
_run_gc(_cdir(_bg_target))
|
|
2387
|
+
except Exception:
|
|
2388
|
+
pass
|
|
2372
2389
|
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2390
|
+
_cache_write_thread = _threading.Thread(target=_write_cache_async, daemon=True)
|
|
2391
|
+
_cache_write_thread.start()
|
|
2392
|
+
_atexit.register(_cache_write_thread.join, 30.0)
|
|
2376
2393
|
|
|
2377
2394
|
# Update RIS with aggregated snapshot data (non-fatal side-effect).
|
|
2378
2395
|
# Update RIS whenever git is available, even when L1/L2 cache is skipped
|
|
@@ -1277,6 +1277,11 @@ class DependencyAnalyzer:
|
|
|
1277
1277
|
|
|
1278
1278
|
return records, limitations
|
|
1279
1279
|
|
|
1280
|
+
def _strip_gradle_comments(self, content: str) -> str:
|
|
1281
|
+
content = re.sub(r"/\*.*?\*/", "", content, flags=re.DOTALL)
|
|
1282
|
+
content = re.sub(r"//[^\n]*", "", content)
|
|
1283
|
+
return content
|
|
1284
|
+
|
|
1280
1285
|
def _analyze_gradle(self, root: Path) -> tuple[list[DependencyRecord], list[str]]:
|
|
1281
1286
|
for filename in ("build.gradle", "build.gradle.kts"):
|
|
1282
1287
|
gradle_file = root / filename
|
|
@@ -1287,7 +1292,22 @@ class DependencyAnalyzer:
|
|
|
1287
1292
|
return [], [f"gradle: error reading {filename}"]
|
|
1288
1293
|
props = self._parse_gradle_properties(root, content)
|
|
1289
1294
|
records = self._parse_gradle_dependencies(content, props, filename)
|
|
1290
|
-
|
|
1295
|
+
|
|
1296
|
+
# Follow apply from: 'other.gradle' directives (simple string literals only)
|
|
1297
|
+
apply_pat = re.compile(
|
|
1298
|
+
r"""apply\s+from\s*:\s*["']([^"'${}\s]+\.gradle(?:\.kts)?)["']"""
|
|
1299
|
+
)
|
|
1300
|
+
for m in apply_pat.finditer(self._strip_gradle_comments(content)):
|
|
1301
|
+
applied_path = root / m.group(1)
|
|
1302
|
+
if applied_path.exists() and applied_path.resolve() != gradle_file.resolve():
|
|
1303
|
+
try:
|
|
1304
|
+
applied_content = applied_path.read_text(encoding="utf-8", errors="replace")
|
|
1305
|
+
more = self._parse_gradle_dependencies(applied_content, props, m.group(1))
|
|
1306
|
+
records.extend(more)
|
|
1307
|
+
except OSError:
|
|
1308
|
+
pass
|
|
1309
|
+
|
|
1310
|
+
return self._dedupe(records), ["gradle: no compatible lockfile found; transitive dependencies unavailable"]
|
|
1291
1311
|
return [], []
|
|
1292
1312
|
|
|
1293
1313
|
def _parse_gradle_properties(self, root: Path, content: str) -> dict[str, str]:
|
|
@@ -1326,6 +1346,7 @@ class DependencyAnalyzer:
|
|
|
1326
1346
|
def _parse_gradle_dependencies(
|
|
1327
1347
|
self, content: str, props: dict[str, str], manifest_path: str
|
|
1328
1348
|
) -> list[DependencyRecord]:
|
|
1349
|
+
content = self._strip_gradle_comments(content)
|
|
1329
1350
|
_DIRECT = frozenset({
|
|
1330
1351
|
"implementation", "api", "compileOnly", "runtimeOnly", "compile", "provided",
|
|
1331
1352
|
"compileClasspath", "runtimeClasspath",
|
sourcecode/file_chunker.py
CHANGED
|
@@ -94,6 +94,7 @@ _CLASS_RE = re.compile(
|
|
|
94
94
|
)
|
|
95
95
|
_METHOD_RE = re.compile(
|
|
96
96
|
r'^\s*(?:(?:public|protected|private|static|final|synchronized|abstract|native|default|override)\s+)*'
|
|
97
|
+
r'(?:@\w+(?:\([^)]*\))?\s+)*' # optional return-type annotations (e.g. @ResponseBody)
|
|
97
98
|
r'(?:<[^>]+>\s+)?' # optional generic return type
|
|
98
99
|
r'(?:[\w<>\[\],?\s]+\s+)?' # return type (optional for constructors)
|
|
99
100
|
r'(\w+)\s*\(' # method/constructor name + opening paren
|
|
@@ -288,6 +289,8 @@ def chunk_java_file(
|
|
|
288
289
|
current_method_name = ""
|
|
289
290
|
current_method_type = ""
|
|
290
291
|
method_brace_start_depth = -1
|
|
292
|
+
# State for multi-line method signatures (name detected, waiting for opening '{')
|
|
293
|
+
_pending_method: Optional[tuple[str, str]] = None # (name, type)
|
|
291
294
|
|
|
292
295
|
for line_no_0, raw_line in enumerate(all_lines):
|
|
293
296
|
line_no = line_no_0 + 1 # 1-based
|
|
@@ -315,32 +318,16 @@ def chunk_java_file(
|
|
|
315
318
|
|
|
316
319
|
# Check if this line starts a method/constructor AT class_depth+1
|
|
317
320
|
if class_depth >= 0 and depth == class_depth + 1 and not current_method_name:
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
# Check if last N pending lines are annotations for this method
|
|
326
|
-
# Flush everything up to ann_buffer start
|
|
327
|
-
if ann_buffer:
|
|
328
|
-
ann_start_line = ann_buffer[0][0]
|
|
329
|
-
pre_ann_lines = pending_lines[:ann_start_line - pending_start]
|
|
330
|
-
if pre_ann_lines:
|
|
331
|
-
_flush_chunk(ann_start_line - 1)
|
|
332
|
-
# Move ann_buffer lines into the new method chunk
|
|
333
|
-
pending_start = ann_start_line
|
|
334
|
-
pending_lines = [al for _, al in ann_buffer]
|
|
335
|
-
ann_buffer = []
|
|
336
|
-
else:
|
|
337
|
-
_flush_chunk(line_no - 1)
|
|
338
|
-
pending_start = line_no
|
|
339
|
-
pending_lines = []
|
|
340
|
-
|
|
321
|
+
# Multi-line signature: we already detected the method name; this line
|
|
322
|
+
# should contain the opening brace that starts the method body.
|
|
323
|
+
# Guard: opens > closes ensures a net depth increase (not a balanced
|
|
324
|
+
# annotation arg like @RequestMapping({"v1","v2"})).
|
|
325
|
+
if _pending_method and opens > closes:
|
|
326
|
+
mname, mtype = _pending_method
|
|
327
|
+
_pending_method = None
|
|
341
328
|
current_method_name = mname
|
|
342
329
|
current_method_type = mtype
|
|
343
|
-
method_brace_start_depth = depth + opens - 1
|
|
330
|
+
method_brace_start_depth = depth + opens - 1
|
|
344
331
|
pending_type = mtype
|
|
345
332
|
pending_symbol = f"{class_name}#{mname}" if class_name else mname
|
|
346
333
|
pending_lines.append(raw_line)
|
|
@@ -348,6 +335,49 @@ def chunk_java_file(
|
|
|
348
335
|
ann_buffer = []
|
|
349
336
|
continue
|
|
350
337
|
|
|
338
|
+
if not _pending_method:
|
|
339
|
+
is_method, mname, mtype = _is_method_or_constructor_start(
|
|
340
|
+
stripped, class_name, depth, class_depth
|
|
341
|
+
)
|
|
342
|
+
if is_method:
|
|
343
|
+
# Flush anything accumulated as field_block / class_header
|
|
344
|
+
# Include annotation lines in the new method chunk
|
|
345
|
+
if pending_lines:
|
|
346
|
+
if ann_buffer:
|
|
347
|
+
ann_start_line = ann_buffer[0][0]
|
|
348
|
+
pre_ann_lines = pending_lines[:ann_start_line - pending_start]
|
|
349
|
+
if pre_ann_lines:
|
|
350
|
+
_flush_chunk(ann_start_line - 1)
|
|
351
|
+
# Move ann_buffer lines into the new method chunk
|
|
352
|
+
pending_start = ann_start_line
|
|
353
|
+
pending_lines = [al for _, al in ann_buffer]
|
|
354
|
+
ann_buffer = []
|
|
355
|
+
else:
|
|
356
|
+
_flush_chunk(line_no - 1)
|
|
357
|
+
pending_start = line_no
|
|
358
|
+
pending_lines = []
|
|
359
|
+
|
|
360
|
+
if "{" in raw_line:
|
|
361
|
+
# Single-line signature: method body opens on same line
|
|
362
|
+
current_method_name = mname
|
|
363
|
+
current_method_type = mtype
|
|
364
|
+
method_brace_start_depth = depth + opens - 1
|
|
365
|
+
pending_type = mtype
|
|
366
|
+
pending_symbol = f"{class_name}#{mname}" if class_name else mname
|
|
367
|
+
pending_lines.append(raw_line)
|
|
368
|
+
depth += opens - closes
|
|
369
|
+
ann_buffer = []
|
|
370
|
+
continue
|
|
371
|
+
else:
|
|
372
|
+
# Multi-line signature: record name, wait for '{' on later line
|
|
373
|
+
_pending_method = (mname, mtype)
|
|
374
|
+
pending_type = mtype
|
|
375
|
+
pending_symbol = f"{class_name}#{mname}" if class_name else mname
|
|
376
|
+
pending_lines.append(raw_line)
|
|
377
|
+
depth += opens - closes
|
|
378
|
+
ann_buffer = []
|
|
379
|
+
continue
|
|
380
|
+
|
|
351
381
|
# Update depth
|
|
352
382
|
depth += opens - closes
|
|
353
383
|
|
sourcecode/migrate_check.py
CHANGED
|
@@ -506,7 +506,7 @@ _LEGACY_API_RULES: list[_Rule] = [
|
|
|
506
506
|
),
|
|
507
507
|
fix_hint=(
|
|
508
508
|
"Replace Date with LocalDate/LocalDateTime/ZonedDateTime, "
|
|
509
|
-
"Calendar with java.time
|
|
509
|
+
"Calendar with LocalDate or ZonedDateTime (java.time — no Calendar equivalent), "
|
|
510
510
|
"SimpleDateFormat with DateTimeFormatter (thread-safe)."
|
|
511
511
|
),
|
|
512
512
|
migration_target="java_8_best_practice",
|
|
@@ -1299,16 +1299,31 @@ _STATIC_LIMITATIONS: list[str] = [
|
|
|
1299
1299
|
|
|
1300
1300
|
|
|
1301
1301
|
def _detect_spring_boot_2(root: Path) -> bool:
|
|
1302
|
-
"""Return True if any
|
|
1302
|
+
"""Return True if any build file in the repo declares spring-boot 2.x.
|
|
1303
|
+
|
|
1304
|
+
Checks root build files plus one level of child modules to handle monorepos
|
|
1305
|
+
where the Spring Boot parent version lives in a submodule pom.xml.
|
|
1306
|
+
"""
|
|
1303
1307
|
_SB2 = re.compile(
|
|
1304
|
-
|
|
1305
|
-
r"2\.\d
|
|
1306
|
-
r"<version>\s*2\.\d+[\.\d]*\s*</version
|
|
1307
|
-
|
|
1308
|
+
# Maven: <parent>...<artifactId>spring-boot-*</artifactId>...<version>2.x
|
|
1309
|
+
r"spring[.\-]?boot.*?<version>\s*2\.\d+|"
|
|
1310
|
+
r"<version>\s*2\.\d+[\.\d]*\s*</version>.*?spring[.\-]?boot|"
|
|
1311
|
+
# Maven properties: <spring.boot.version>2.x or spring-boot.version=2.x
|
|
1312
|
+
r"spring[.\-_]?boot[.\-_]?version\s*[=>\"'\s]+2\.\d+|"
|
|
1313
|
+
# Gradle plugin: id 'org.springframework.boot' version '2.x'
|
|
1314
|
+
r"org\.springframework\.boot[^\n]*?['\"']2\.\d+",
|
|
1308
1315
|
re.IGNORECASE | re.DOTALL,
|
|
1309
1316
|
)
|
|
1310
|
-
|
|
1311
|
-
|
|
1317
|
+
# Candidate build files: root + one level deep (child modules in monorepos)
|
|
1318
|
+
root_files = [
|
|
1319
|
+
root / name
|
|
1320
|
+
for name in ("pom.xml", "build.gradle", "build.gradle.kts", "gradle.properties")
|
|
1321
|
+
]
|
|
1322
|
+
child_poms = list(root.glob("*/pom.xml"))
|
|
1323
|
+
child_gradle = list(root.glob("*/build.gradle")) + list(root.glob("*/build.gradle.kts"))
|
|
1324
|
+
# Limit child scan to 30 files to stay fast on large monorepos
|
|
1325
|
+
candidates = root_files + (child_poms + child_gradle)[:30]
|
|
1326
|
+
for candidate in candidates:
|
|
1312
1327
|
try:
|
|
1313
1328
|
text = candidate.read_text(encoding="utf-8", errors="replace")
|
|
1314
1329
|
if _SB2.search(text):
|
sourcecode/path_filters.py
CHANGED
|
@@ -78,7 +78,8 @@ def is_test_path(path: str) -> bool:
|
|
|
78
78
|
or name.endswith(".spec.js")
|
|
79
79
|
or (name.endswith("test.java") and name != "test.java")
|
|
80
80
|
or name.endswith("tests.java")
|
|
81
|
-
or (name.startswith("test") and name.endswith(".java") and len(name) > 9
|
|
81
|
+
or (name.startswith("test") and name.endswith(".java") and len(name) > 9
|
|
82
|
+
and "/src/main/" not in norm)
|
|
82
83
|
):
|
|
83
84
|
return True
|
|
84
85
|
|
sourcecode/repository_ir.py
CHANGED
|
@@ -2756,8 +2756,8 @@ def _build_route_surface(
|
|
|
2756
2756
|
_route_entry["security_annotations"] = _sec
|
|
2757
2757
|
routes.append(_route_entry)
|
|
2758
2758
|
|
|
2759
|
-
# Phase 3: inheritance projection — subclasses with
|
|
2760
|
-
#
|
|
2759
|
+
# Phase 3: inheritance projection — subclasses with a class-level @RequestMapping
|
|
2760
|
+
# prefix inherit parent methods that they do not override (same HTTP verb + path suffix).
|
|
2761
2761
|
if extends_map:
|
|
2762
2762
|
fqn_to_simple: dict[str, str] = {d["fqn"]: s for s, d in class_info.items()}
|
|
2763
2763
|
simple_extends: dict[str, str] = {
|
|
@@ -2771,11 +2771,14 @@ def _build_route_surface(
|
|
|
2771
2771
|
}
|
|
2772
2772
|
|
|
2773
2773
|
for cls_simple, data in class_info.items():
|
|
2774
|
-
if data["own_endpoints"]:
|
|
2775
|
-
continue
|
|
2776
2774
|
if not any(data["prefixes"]):
|
|
2777
2775
|
continue
|
|
2778
2776
|
|
|
2777
|
+
# (verb, suffix) pairs declared on this subclass — these shadow parent methods.
|
|
2778
|
+
own_override_set: set[tuple[str, str]] = {
|
|
2779
|
+
(verb, suffix) for verb, suffix, _, _ in data["own_endpoints"]
|
|
2780
|
+
}
|
|
2781
|
+
|
|
2779
2782
|
chain = simple_extends.get(cls_simple)
|
|
2780
2783
|
visited: set[str] = {cls_simple}
|
|
2781
2784
|
depth = 1
|
|
@@ -2786,6 +2789,9 @@ def _build_route_surface(
|
|
|
2786
2789
|
break
|
|
2787
2790
|
if parent["own_endpoints"]:
|
|
2788
2791
|
for verb, suffix, declaring_sym, stable_id in parent["own_endpoints"]:
|
|
2792
|
+
# Skip methods the subclass overrides (same verb + path suffix).
|
|
2793
|
+
if (verb, suffix) in own_override_set:
|
|
2794
|
+
continue
|
|
2789
2795
|
for prefix in data["prefixes"]:
|
|
2790
2796
|
# P1 fix: collapse any number of consecutive slashes
|
|
2791
2797
|
full_path = re.sub(r"/+", "/", prefix + "/" + suffix).rstrip("/") or "/"
|
|
@@ -2903,6 +2909,24 @@ def build_repo_ir(
|
|
|
2903
2909
|
# Spring Data
|
|
2904
2910
|
'@Query', '@NamedQuery',
|
|
2905
2911
|
)
|
|
2912
|
+
# Pre-pass: collect custom meta-annotation names from @interface definitions
|
|
2913
|
+
# that compose known Spring stereotypes (e.g. @DomainService = @Service + @Transactional).
|
|
2914
|
+
# These names must be added to the marker set so classes using them aren't
|
|
2915
|
+
# filtered out by the fast pre-scan below.
|
|
2916
|
+
_custom_meta_markers: set[str] = set()
|
|
2917
|
+
for _rp in sorted(file_paths):
|
|
2918
|
+
try:
|
|
2919
|
+
_src = (root / _rp).read_text(encoding="utf-8", errors="replace")
|
|
2920
|
+
except OSError:
|
|
2921
|
+
continue
|
|
2922
|
+
if "@interface" not in _src:
|
|
2923
|
+
continue
|
|
2924
|
+
if not any(m in _src for m in _ANNOTATION_MARKERS):
|
|
2925
|
+
continue
|
|
2926
|
+
for _m in re.finditer(r'@interface\s+(\w+)', _src):
|
|
2927
|
+
_custom_meta_markers.add(f"@{_m.group(1)}")
|
|
2928
|
+
_effective_markers = _ANNOTATION_MARKERS + tuple(_custom_meta_markers)
|
|
2929
|
+
|
|
2906
2930
|
_per_file: list[tuple[str, str, str, list[str], list[SymbolRecord]]] = []
|
|
2907
2931
|
for rel_path in sorted(file_paths):
|
|
2908
2932
|
abs_path = root / rel_path
|
|
@@ -2915,7 +2939,7 @@ def build_repo_ir(
|
|
|
2915
2939
|
_meta_chars_read += len(source)
|
|
2916
2940
|
# Fast pre-scan: if file has no relevant annotations skip full extraction.
|
|
2917
2941
|
# Still register package/class name for same-package resolution.
|
|
2918
|
-
if not any(marker in source for marker in
|
|
2942
|
+
if not any(marker in source for marker in _effective_markers):
|
|
2919
2943
|
pkg_m = _PKG_RE.search(source)
|
|
2920
2944
|
_pkg = pkg_m.group(1) if pkg_m else ""
|
|
2921
2945
|
# Minimal class-name symbols for same-package map (no methods/fields)
|
|
@@ -3384,6 +3408,31 @@ def extract_java_endpoints(root: Path) -> "dict[str, Any]":
|
|
|
3384
3408
|
# No independent re-extraction here — _route_security_from_sym is the single
|
|
3385
3409
|
# source of truth for security policy extraction.
|
|
3386
3410
|
|
|
3411
|
+
# Detect interface-based Spring MVC controllers (implements Controller).
|
|
3412
|
+
# These predate annotation-style and have URL mapping in XML, not annotations.
|
|
3413
|
+
# We emit a synthetic "(xml-mapped)" entry so they appear in the endpoint surface.
|
|
3414
|
+
_SPRING_CONTROLLER_IFACE = "org.springframework.web.servlet.mvc.Controller"
|
|
3415
|
+
_annotated_classes = {
|
|
3416
|
+
route.get("effective_class", "").split(".")[-1]
|
|
3417
|
+
for route in routes
|
|
3418
|
+
}
|
|
3419
|
+
for sym in all_symbols:
|
|
3420
|
+
if sym.type != "class":
|
|
3421
|
+
continue
|
|
3422
|
+
if _SPRING_CONTROLLER_IFACE not in sym.imports_used:
|
|
3423
|
+
continue
|
|
3424
|
+
cls_simple = sym.symbol.split(".")[-1]
|
|
3425
|
+
if cls_simple in _annotated_classes:
|
|
3426
|
+
continue
|
|
3427
|
+
routes.append({
|
|
3428
|
+
"symbol": f"{sym.symbol}#handleRequest",
|
|
3429
|
+
"effective_class": sym.symbol,
|
|
3430
|
+
"method": "ANY",
|
|
3431
|
+
"path": "(xml-mapped)",
|
|
3432
|
+
"security_annotations": None,
|
|
3433
|
+
"note": "interface-based Spring MVC controller — URL mapped via XML",
|
|
3434
|
+
})
|
|
3435
|
+
|
|
3387
3436
|
endpoints: list[dict] = []
|
|
3388
3437
|
for route in routes:
|
|
3389
3438
|
handler = (
|
|
@@ -3970,7 +4019,9 @@ def compute_blast_radius(
|
|
|
3970
4019
|
)
|
|
3971
4020
|
risk_score = round(min(raw_score, 100.0), 2)
|
|
3972
4021
|
|
|
3973
|
-
if risk_score >=
|
|
4022
|
+
if risk_score >= 30 or (n_ep >= 5 and n_txn >= 2):
|
|
4023
|
+
risk_level = "critical"
|
|
4024
|
+
elif risk_score >= 20 or n_ep >= 3 or n_txn >= 2:
|
|
3974
4025
|
risk_level = "high"
|
|
3975
4026
|
elif risk_score >= 5 or n_ep >= 1 or n_txn >= 1:
|
|
3976
4027
|
risk_level = "medium"
|
|
@@ -4232,6 +4283,16 @@ def _resolve_target(
|
|
|
4232
4283
|
return "not_found", set()
|
|
4233
4284
|
|
|
4234
4285
|
|
|
4286
|
+
_BLAST_SKIP_EDGE_TYPES: frozenset[str] = frozenset({"contained_in", "imports"})
|
|
4287
|
+
# 'contained_in': structural membership (method→enclosing class), not a caller.
|
|
4288
|
+
# 'imports': Java import statements — any class that references the type in
|
|
4289
|
+
# an import declaration, including those that only use it as a
|
|
4290
|
+
# method-return type or catch-block type. Import presence does NOT
|
|
4291
|
+
# imply a runtime dependency; including it produces false-positive
|
|
4292
|
+
# callers (e.g. sibling service classes that share a utility interface).
|
|
4293
|
+
# Consistent with spring_impact._SKIP_EDGE_TYPES.
|
|
4294
|
+
|
|
4295
|
+
|
|
4235
4296
|
def _all_callers_from_rg(fqn: str, reverse_graph: dict[str, dict[str, list[str]]]) -> list[str]:
|
|
4236
4297
|
"""Return all callers of fqn from the reverse graph (all edge types).
|
|
4237
4298
|
|
|
@@ -4242,13 +4303,17 @@ def _all_callers_from_rg(fqn: str, reverse_graph: dict[str, dict[str, list[str]]
|
|
|
4242
4303
|
CH-002 fix: for 'injects' edges, normalize field/constructor FQNs to their
|
|
4243
4304
|
enclosing class. e.g. pkg.ConsolidacionService.calcularField → pkg.ConsolidacionService
|
|
4244
4305
|
so BFS can continue through DI injection chains and find controllers.
|
|
4306
|
+
|
|
4307
|
+
FP-001 fix: skip 'imports' edges — import declarations are not runtime
|
|
4308
|
+
dependencies and produce false-positive callers (e.g. sibling classes that share
|
|
4309
|
+
a utility interface but don't call the target).
|
|
4245
4310
|
"""
|
|
4246
4311
|
entry = reverse_graph.get(fqn) or {}
|
|
4247
4312
|
callers: list[str] = []
|
|
4248
4313
|
seen: set[str] = set()
|
|
4249
4314
|
for edge_type, fqn_list in entry.items():
|
|
4250
|
-
if edge_type
|
|
4251
|
-
continue
|
|
4315
|
+
if edge_type in _BLAST_SKIP_EDGE_TYPES:
|
|
4316
|
+
continue
|
|
4252
4317
|
for c in fqn_list:
|
|
4253
4318
|
normalized = _normalize_owner_fqn(c) if edge_type == "injects" else c
|
|
4254
4319
|
if normalized not in seen:
|
sourcecode/spring_impact.py
CHANGED
|
@@ -465,9 +465,9 @@ def _compute_risk(
|
|
|
465
465
|
caller_score = min(direct_callers * 2.0 + indirect_callers * 0.5, 20.0)
|
|
466
466
|
total = finding_score + endpoint_score + caller_score
|
|
467
467
|
|
|
468
|
-
if total >=
|
|
468
|
+
if total >= 25.0:
|
|
469
469
|
level = "critical"
|
|
470
|
-
elif total >=
|
|
470
|
+
elif total >= 12.0:
|
|
471
471
|
level = "high"
|
|
472
472
|
elif total >= 4.0:
|
|
473
473
|
level = "medium"
|
|
@@ -570,6 +570,9 @@ class ImpactOrchestrator:
|
|
|
570
570
|
# When the queried class is an interface, BFS should also start from
|
|
571
571
|
# implementation symbols so TX boundaries and callers on the impl are found.
|
|
572
572
|
impl_graph = getattr(cir, "implementation_graph", None)
|
|
573
|
+
# Track original seed classes BEFORE CH-001a expansion so CH-001b does not
|
|
574
|
+
# cascade through interfaces shared by impl classes added here (false positives).
|
|
575
|
+
original_seed_classes: set[str] = {_class_of(s) for s in seed_fqns}
|
|
573
576
|
if impl_graph is not None:
|
|
574
577
|
seed_classes_ch001 = {_class_of(s) for s in seed_fqns}
|
|
575
578
|
impl_seeds: list[str] = []
|
|
@@ -598,11 +601,14 @@ class ImpactOrchestrator:
|
|
|
598
601
|
# Callers typically inject the interface type, so reverse-graph edges live on
|
|
599
602
|
# the interface node, not on the implementation node. Without this expansion,
|
|
600
603
|
# querying 'OrderServiceImpl' finds 0 callers even though 36 classes inject it.
|
|
604
|
+
# IMPORTANT: only expand ORIGINAL user-query seeds, not classes added by CH-001a.
|
|
605
|
+
# Expanding CH-001a-added impls cascades through shared utility interfaces
|
|
606
|
+
# (e.g. RefByUuid) and produces false-positive callers from sibling implementors.
|
|
601
607
|
if impl_graph is not None:
|
|
602
608
|
current_seed_classes = {_class_of(s) for s in seed_fqns}
|
|
603
609
|
iface_seeds: list[str] = []
|
|
604
610
|
iface_classes_added: set[str] = set()
|
|
605
|
-
for seed_class in sorted(
|
|
611
|
+
for seed_class in sorted(original_seed_classes):
|
|
606
612
|
ifaces = impl_graph.interfaces_of(seed_class)
|
|
607
613
|
for iface_class in ifaces:
|
|
608
614
|
if iface_class in iface_classes_added or iface_class in current_seed_classes:
|
sourcecode/spring_model.py
CHANGED
|
@@ -45,6 +45,9 @@ _BEAN_ANNOTATIONS: frozenset[str] = frozenset({
|
|
|
45
45
|
"@Entity", "@MappedSuperclass", "@Embeddable",
|
|
46
46
|
})
|
|
47
47
|
|
|
48
|
+
# JPA stereotypes that are NOT Spring IoC beans — present in any JPA project (Quarkus, JEE, etc.)
|
|
49
|
+
_JPA_ONLY_STEREOTYPES: frozenset[str] = frozenset({"entity", "mappedsuperclass", "embeddable"})
|
|
50
|
+
|
|
48
51
|
_GENERIC_PARAM_RE = re.compile(r"<[A-Z][\w,\s<>?]*>")
|
|
49
52
|
|
|
50
53
|
|
|
@@ -153,18 +156,45 @@ class BeanGraph:
|
|
|
153
156
|
raw_ir = getattr(cir, "_raw_ir", {}) or {}
|
|
154
157
|
nodes = (raw_ir.get("graph") or {}).get("nodes") or []
|
|
155
158
|
|
|
159
|
+
# Pass 1: build meta-bean-annotation map from annotation-type nodes.
|
|
160
|
+
# e.g. @DomainService (annotated with @Service) maps "@DomainService" → "service"
|
|
161
|
+
_meta_bean_stereotype: dict[str, str] = {}
|
|
156
162
|
for node in nodes:
|
|
157
163
|
if not isinstance(node, dict):
|
|
158
164
|
continue
|
|
165
|
+
if (node.get("symbol_kind") or node.get("type") or "") != "annotation":
|
|
166
|
+
continue
|
|
167
|
+
_ann_set = set(node.get("annotations") or [])
|
|
168
|
+
_match = _ann_set & _BEAN_ANNOTATIONS
|
|
169
|
+
if not _match:
|
|
170
|
+
continue
|
|
171
|
+
_fqn = node.get("fqn") or ""
|
|
172
|
+
if not _fqn:
|
|
173
|
+
continue
|
|
174
|
+
_simple = "@" + _fqn.split(".")[-1]
|
|
175
|
+
_bean_ann = next(iter(_match))
|
|
176
|
+
_meta_bean_stereotype[_simple] = _bean_ann.lstrip("@").lower()
|
|
177
|
+
|
|
178
|
+
# Pass 2: collect all bean nodes (direct or via meta-annotation).
|
|
179
|
+
for node in nodes:
|
|
180
|
+
if not isinstance(node, dict):
|
|
181
|
+
continue
|
|
182
|
+
if (node.get("symbol_kind") or node.get("type") or "") == "annotation":
|
|
183
|
+
continue # annotation-type nodes are not beans
|
|
159
184
|
ann_set = set(node.get("annotations") or [])
|
|
160
185
|
match = ann_set & _BEAN_ANNOTATIONS
|
|
161
186
|
if not match:
|
|
162
|
-
|
|
187
|
+
meta_match = ann_set & set(_meta_bean_stereotype)
|
|
188
|
+
if not meta_match:
|
|
189
|
+
continue
|
|
190
|
+
ann = next(iter(meta_match))
|
|
191
|
+
stereotype = _meta_bean_stereotype[ann]
|
|
192
|
+
else:
|
|
193
|
+
ann = next(iter(match))
|
|
194
|
+
stereotype = ann.lstrip("@").lower()
|
|
163
195
|
fqn = node.get("fqn") or ""
|
|
164
196
|
if not fqn:
|
|
165
197
|
continue
|
|
166
|
-
ann = next(iter(match))
|
|
167
|
-
stereotype = ann.lstrip("@").lower()
|
|
168
198
|
beans[fqn] = BeanNode(
|
|
169
199
|
fqn=fqn,
|
|
170
200
|
stereotype=stereotype,
|
|
@@ -186,6 +216,10 @@ class BeanGraph:
|
|
|
186
216
|
def is_bean(self, fqn: str) -> bool:
|
|
187
217
|
return fqn in self.beans
|
|
188
218
|
|
|
219
|
+
def has_spring_beans(self) -> bool:
|
|
220
|
+
"""True only if Spring IoC beans exist — JPA entities do not count."""
|
|
221
|
+
return any(b.stereotype not in _JPA_ONLY_STEREOTYPES for b in self.beans.values())
|
|
222
|
+
|
|
189
223
|
def get_stereotype(self, fqn: str) -> str:
|
|
190
224
|
node = self.beans.get(fqn)
|
|
191
225
|
return node.stereotype if node else ""
|
|
@@ -21,7 +21,7 @@ from sourcecode.spring_findings import (
|
|
|
21
21
|
SEVERITY_ORDER,
|
|
22
22
|
deduplicate_findings,
|
|
23
23
|
)
|
|
24
|
-
from sourcecode.spring_model import InheritanceGraph, SpringSemanticModel
|
|
24
|
+
from sourcecode.spring_model import BeanGraph, InheritanceGraph, SpringSemanticModel
|
|
25
25
|
from sourcecode.spring_semantic import TransactionBoundaryIndex, build_tx_index
|
|
26
26
|
|
|
27
27
|
if TYPE_CHECKING:
|
|
@@ -472,11 +472,8 @@ def run_security_audit(
|
|
|
472
472
|
|
|
473
473
|
elapsed_ms = round((time.monotonic() - t0) * 1000, 1)
|
|
474
474
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
or tx_index.stats()["total"] > 0
|
|
478
|
-
or cir.metadata.get("security_model", "unknown") != "unknown"
|
|
479
|
-
)
|
|
475
|
+
_bean_graph = model.bean_graph if model is not None else BeanGraph.build(cir)
|
|
476
|
+
_spring_detected = _bean_graph.has_spring_beans() or tx_index.stats()["total"] > 0
|
|
480
477
|
|
|
481
478
|
_sec_limitations = [
|
|
482
479
|
"SEC-001: only emitted for annotation_based security model",
|
sourcecode/spring_semantic.py
CHANGED
|
@@ -293,26 +293,53 @@ def build_tx_index(cir: "CanonicalRepositoryIR") -> TransactionBoundaryIndex:
|
|
|
293
293
|
graph = raw_ir.get("graph") or {}
|
|
294
294
|
nodes = graph.get("nodes") or []
|
|
295
295
|
|
|
296
|
+
# Pass 1: build meta-@Transactional map from annotation-type nodes.
|
|
297
|
+
# e.g. @ReadOnlyTransaction (annotated with @Transactional(readOnly=true))
|
|
298
|
+
# maps "@ReadOnlyTransaction" → "readOnly = true"
|
|
299
|
+
_meta_tx_args: dict[str, str] = {}
|
|
296
300
|
for node in nodes:
|
|
297
301
|
if not isinstance(node, dict):
|
|
298
302
|
continue
|
|
303
|
+
if (node.get("symbol_kind") or node.get("type") or "") != "annotation":
|
|
304
|
+
continue
|
|
305
|
+
ann_set = set(node.get("annotations") or [])
|
|
306
|
+
if "@Transactional" not in ann_set:
|
|
307
|
+
continue
|
|
308
|
+
_fqn = node.get("fqn") or node.get("symbol") or ""
|
|
309
|
+
if not _fqn:
|
|
310
|
+
continue
|
|
311
|
+
_simple = "@" + _fqn.split(".")[-1]
|
|
312
|
+
_av = node.get("annotation_values") or {}
|
|
313
|
+
_meta_tx_args[_simple] = _av.get("@Transactional", "")
|
|
299
314
|
|
|
300
|
-
|
|
301
|
-
|
|
315
|
+
# Pass 2: index all @Transactional boundaries (direct or via meta-annotation).
|
|
316
|
+
for node in nodes:
|
|
317
|
+
if not isinstance(node, dict):
|
|
302
318
|
continue
|
|
303
319
|
|
|
320
|
+
symbol_kind = node.get("symbol_kind") or node.get("type") or ""
|
|
321
|
+
if symbol_kind == "annotation":
|
|
322
|
+
continue # annotation-type nodes are not TX boundaries
|
|
323
|
+
|
|
324
|
+
annotations = node.get("annotations") or []
|
|
325
|
+
ann_values = node.get("annotation_values") or {}
|
|
326
|
+
|
|
327
|
+
if "@Transactional" in annotations:
|
|
328
|
+
raw_args = ann_values.get("@Transactional", "")
|
|
329
|
+
else:
|
|
330
|
+
# Check meta-annotations (composed @Transactional)
|
|
331
|
+
meta_key = next((a for a in annotations if a in _meta_tx_args), None)
|
|
332
|
+
if meta_key is None:
|
|
333
|
+
continue
|
|
334
|
+
raw_args = _meta_tx_args[meta_key]
|
|
335
|
+
|
|
304
336
|
fqn = node.get("fqn") or node.get("symbol") or ""
|
|
305
337
|
if not fqn:
|
|
306
338
|
continue
|
|
307
339
|
|
|
308
|
-
symbol_kind = node.get("symbol_kind") or node.get("type") or ""
|
|
309
340
|
source_file = node.get("source_file") or node.get("declaring_file") or ""
|
|
310
341
|
modifiers = node.get("modifiers") or []
|
|
311
342
|
|
|
312
|
-
# annotation_values is stored per-symbol in the graph node
|
|
313
|
-
ann_values = node.get("annotation_values") or {}
|
|
314
|
-
raw_args = ann_values.get("@Transactional", "")
|
|
315
|
-
|
|
316
343
|
# Determine scope: class-level or method-level
|
|
317
344
|
if symbol_kind in ("class", "interface", "enum"):
|
|
318
345
|
scope = "class"
|
sourcecode/spring_tx_analyzer.py
CHANGED
|
@@ -722,7 +722,7 @@ def run_tx_audit(
|
|
|
722
722
|
|
|
723
723
|
elapsed_ms = round((time.monotonic() - t0) * 1000, 1)
|
|
724
724
|
|
|
725
|
-
_spring_detected = tx_index.stats()["total"] > 0 or
|
|
725
|
+
_spring_detected = tx_index.stats()["total"] > 0 or model.bean_graph.has_spring_beans()
|
|
726
726
|
|
|
727
727
|
_tx_limitations = [
|
|
728
728
|
"Self-invocation via this.method() not detected — requires AST-level analysis",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sourcecode
|
|
3
|
-
Version: 1.35.
|
|
3
|
+
Version: 1.35.33
|
|
4
4
|
Summary: Persistent structural context and ultra-fast repeated analysis for AI coding agents
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
Keywords: agents,ai,codebase,context,developer-tools,llm
|
|
@@ -40,7 +40,7 @@ Description-Content-Type: text/markdown
|
|
|
40
40
|
|
|
41
41
|
**Persistent structural context and ultra-fast repeated analysis for AI coding agents.**
|
|
42
42
|
|
|
43
|
-

|
|
44
44
|

|
|
45
45
|
|
|
46
46
|
---
|
|
@@ -114,7 +114,7 @@ pipx install sourcecode
|
|
|
114
114
|
|
|
115
115
|
```bash
|
|
116
116
|
sourcecode version
|
|
117
|
-
# sourcecode 1.35.
|
|
117
|
+
# sourcecode 1.35.33
|
|
118
118
|
|
|
119
119
|
**v1.35.28** — 7 bug fixes: `rename-class` cross-package disambiguation (BUG-4), `rename-class` collision detection (BUG-2), `find_java_files` false positive on `com/test/` package paths (BUG-1), `cold-start --compact` correct key names (BUG-6), `@EnableMethodSecurity` no longer suppresses SEC-001 (BUG-3), `explain` @Entity stereotype detection (BUG-5), XML+annotation mixed security retagging (BUG-7).
|
|
120
120
|
```
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
sourcecode/__init__.py,sha256=
|
|
1
|
+
sourcecode/__init__.py,sha256=iEPW1VKpIapMFx1-ORA_vdq1Ke_GeAEHxf6haX6UZ80,104
|
|
2
2
|
sourcecode/adaptive_scanner.py,sha256=XffluXKzJUXrMtjEiAOnSNPZnztdIcts17T9ouHeID0,10521
|
|
3
|
-
sourcecode/architecture_analyzer.py,sha256=
|
|
3
|
+
sourcecode/architecture_analyzer.py,sha256=liCwQmLgb5vplohy8arjYxs_HOIv5C9MjLh_gY6bc5Q,44115
|
|
4
4
|
sourcecode/architecture_summary.py,sha256=z34_6v7cSwy98cof2UVciGho7SCrZ93tiqMmq5WNzRQ,20405
|
|
5
5
|
sourcecode/ast_extractor.py,sha256=sa6CmLpn-k5G3_Hzxn8hAlZ5-TS-EVzXDD0Gvxd2jzs,50613
|
|
6
6
|
sourcecode/cache.py,sha256=wAyPrXN5DqiGivnMpeEuun2xHDKfBer2_oBsh6kj_vc,30447
|
|
7
7
|
sourcecode/canonical_ir.py,sha256=c_lYTVoegg-1W2dZ34_2s3tN8L0GVx7eiDRh9ghdSD8,24178
|
|
8
8
|
sourcecode/cir_graphs.py,sha256=rZi8JV4ZrAa2WSCeyNa4JIEKQ_yZzDZTsrvVz2KfuKA,8919
|
|
9
|
-
sourcecode/classifier.py,sha256=
|
|
10
|
-
sourcecode/cli.py,sha256=
|
|
9
|
+
sourcecode/classifier.py,sha256=hKzg-nQ47htqqIUzSGvYxv15cXrA3KgICTwJmdqal0o,8095
|
|
10
|
+
sourcecode/cli.py,sha256=3rbloccTkwexz8RdKpvMMBSSru27F0091unER2jZVDc,250580
|
|
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
|
|
@@ -15,13 +15,13 @@ sourcecode/context_summarizer.py,sha256=zlbm8ytdvJToohU108-dwBmEl52xl0gXpf6PZBOW
|
|
|
15
15
|
sourcecode/contract_model.py,sha256=nRxJKPMs1VHwFTa8AVXhGmaLjti3Lr2sjHDpWgv1bfE,3917
|
|
16
16
|
sourcecode/contract_pipeline.py,sha256=gvTdDniedm_mjq4vaHqnBY2UkQ0s00gtXqzTLILNXHc,28719
|
|
17
17
|
sourcecode/coverage_parser.py,sha256=q0LeZJaX1bnntLu-ImksdBsMlpsVmk_iUfSaB4eaJGo,19702
|
|
18
|
-
sourcecode/dependency_analyzer.py,sha256=
|
|
18
|
+
sourcecode/dependency_analyzer.py,sha256=qEnRiKFkleZJyLf_DyznJbWD1GJ881iG4RRDqH9oGQ4,61524
|
|
19
19
|
sourcecode/doc_analyzer.py,sha256=05bjTUbDbmnbajD_cgRnACzS8T7xxBKVX4CjkJlhZg8,24411
|
|
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
23
|
sourcecode/explain.py,sha256=N5189hO8Ydbunr431zWDpSueSTdgBZh9l2xU-fH-AO8,16832
|
|
24
|
-
sourcecode/file_chunker.py,sha256=
|
|
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
|
|
27
27
|
sourcecode/fqn_utils.py,sha256=XLU7zDkNBXz_RZkIUNfpPmp1nekWtqP-fxV92tDV1vg,2158
|
|
@@ -30,9 +30,9 @@ sourcecode/graph_analyzer.py,sha256=DHR8fY69oU_Pi4SYaWboX6EoEFrctQKB9dsjpqwGMzw,
|
|
|
30
30
|
sourcecode/license.py,sha256=3JCV2OeTVttKrOGBguU5uZC0c02Stig-KLB0mP2lNiY,22742
|
|
31
31
|
sourcecode/mcp_nudge.py,sha256=5ELU_ixzh6uA83NXLOZT8h00OhL53okfQdji3jyKOjg,2917
|
|
32
32
|
sourcecode/metrics_analyzer.py,sha256=m0ENgtqKeBL17kUIK3fmGkgo7UfXBNHxCMj0H_Y5K7c,22750
|
|
33
|
-
sourcecode/migrate_check.py,sha256=
|
|
33
|
+
sourcecode/migrate_check.py,sha256=vowVIAxVaHU8vhZUEt-HrWrWM38m6a5INHJQGjEg5E0,55390
|
|
34
34
|
sourcecode/output_budget.py,sha256=Js9yUlfQtPhqBl9R6wn_9UHVjjJc3GtLcqyfjf5t50Q,9869
|
|
35
|
-
sourcecode/path_filters.py,sha256=
|
|
35
|
+
sourcecode/path_filters.py,sha256=EN1RGZRvLq5EcPgpjYV_IyCKVlAQQn2bbpEisQ5LpGg,3780
|
|
36
36
|
sourcecode/pr_comment_renderer.py,sha256=smHslxiG14lrytCkq5nFrFu-qTHgA-t-LFYfdrfjz2o,14423
|
|
37
37
|
sourcecode/pr_impact.py,sha256=dCDVw83EDbyVf6F9ZmEQmsFz8ruVH7d4mpeKQCIZHM0,16805
|
|
38
38
|
sourcecode/prepare_context.py,sha256=nvBs5dCzTQMsm5jNEuXp7JHE5l9YIgUmv__pDWbLYgQ,222289
|
|
@@ -42,7 +42,7 @@ sourcecode/redactor.py,sha256=SB4hwIvg8h-hvcqKcDWaZvA-aSyn-at-BIRwa0tUv5E,3227
|
|
|
42
42
|
sourcecode/relevance_scorer.py,sha256=0AgEt4KrV73nioMqBgjhGjtY7L2C7L7cSyKtj3IKcrw,9408
|
|
43
43
|
sourcecode/rename_refactor.py,sha256=rWCsXoDxJNdsmkUXjPtHphT5CjYOgEPmcc817_8Gu-Y,12538
|
|
44
44
|
sourcecode/repo_classifier.py,sha256=FG1vaWKdWXsWdl-S8hjVMiTqcwgaRXkDyvK4rPcOGtQ,22681
|
|
45
|
-
sourcecode/repository_ir.py,sha256=
|
|
45
|
+
sourcecode/repository_ir.py,sha256=2urePvYhsOWtbe-RnT8kASTsSvlR3xqztnpPBU4tFdY,183359
|
|
46
46
|
sourcecode/ris.py,sha256=RcqLVwC-doFcKKViYDkCjZLBqf_wzLES7-F6vHEeWzE,20419
|
|
47
47
|
sourcecode/runtime_classifier.py,sha256=uTAD6BDCiBLUZEDRfqk718kM4RTT_vAbfkcOI2_Xx58,18432
|
|
48
48
|
sourcecode/scanner.py,sha256=WdOQ78mMzjR1NjmKTlbxdgwinnCTfAhxCVLBEFQiFHU,8899
|
|
@@ -51,11 +51,11 @@ sourcecode/semantic_analyzer.py,sha256=4OdG6tTSnTvq3_dSWMbQu8Ad1ndSCKeG-b9qM4hIx
|
|
|
51
51
|
sourcecode/serializer.py,sha256=GR1RcgY2abmanTIHiYzRXuOnhB2MidBVAiETat1w5cU,124724
|
|
52
52
|
sourcecode/spring_event_topology.py,sha256=5_ON_21Le5zbG-1GRc5GLIi5HJfy_QjcXLVPC5WeUGQ,18055
|
|
53
53
|
sourcecode/spring_findings.py,sha256=8V91iHOg9hFgg6tLLl4FSsgrF-dBqOcO2s-K5sD_goA,5417
|
|
54
|
-
sourcecode/spring_impact.py,sha256=
|
|
55
|
-
sourcecode/spring_model.py,sha256=
|
|
56
|
-
sourcecode/spring_security_audit.py,sha256=
|
|
57
|
-
sourcecode/spring_semantic.py,sha256=
|
|
58
|
-
sourcecode/spring_tx_analyzer.py,sha256=
|
|
54
|
+
sourcecode/spring_impact.py,sha256=QMsRiuIW76FrxtFjV6iJRh0uHl07vE0rQ3x9fbFLLZw,33456
|
|
55
|
+
sourcecode/spring_model.py,sha256=zOAgFmrRbG4a6KLm1TJl55aWMyPNsz3OS3FSczqPG6A,16594
|
|
56
|
+
sourcecode/spring_security_audit.py,sha256=XtPJ1SXlZJ8k6VYmaWuAp7Bbir4UmreAL7doIGQ5I7o,20595
|
|
57
|
+
sourcecode/spring_semantic.py,sha256=O1nKSGVzlukuxLHQVuCPxc-XrcrMFxwlHA20_dmEGgM,13307
|
|
58
|
+
sourcecode/spring_tx_analyzer.py,sha256=FdFcyqPp3aT9oJ-PKrnXcTA6s69wdvzG-NBm0GMGPTU,30717
|
|
59
59
|
sourcecode/summarizer.py,sha256=zgdps7yS2IktAbWe7IWz0oUcr3QIuNPRGrsScbZ4R1g,21797
|
|
60
60
|
sourcecode/tree_utils.py,sha256=8GAkIfQAsvtEudIeW1l4ooH_oRtrWR8cpJQJsEa_Pfw,2093
|
|
61
61
|
sourcecode/workspace.py,sha256=X_6NmNnitvT3_38V-JDChydo_sR68s249hLFlrQskU0,8271
|
|
@@ -96,8 +96,8 @@ sourcecode/telemetry/consent.py,sha256=wLMvGNJeSSyZoNkQXpoUioY6mMv4Qdvuw7S9jAEWn
|
|
|
96
96
|
sourcecode/telemetry/events.py,sha256=oEvvulfsv5GIDWG2174gSS6tNB95w38AIYiYeifGKlE,2294
|
|
97
97
|
sourcecode/telemetry/filters.py,sha256=Asa71oRl7q3Wt_FMwuufIZJFzSYdgRNKS8LHCIyFeYE,4805
|
|
98
98
|
sourcecode/telemetry/transport.py,sha256=QSslxIwij8YkRWcVvxykODDrkiN_GAAEu3dUP7KIWeE,1651
|
|
99
|
-
sourcecode-1.35.
|
|
100
|
-
sourcecode-1.35.
|
|
101
|
-
sourcecode-1.35.
|
|
102
|
-
sourcecode-1.35.
|
|
103
|
-
sourcecode-1.35.
|
|
99
|
+
sourcecode-1.35.33.dist-info/METADATA,sha256=t2-bN_WFdMDu1-wae-AEc3kfo13peCzTHER1jdvp7IE,21705
|
|
100
|
+
sourcecode-1.35.33.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
101
|
+
sourcecode-1.35.33.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
|
|
102
|
+
sourcecode-1.35.33.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
|
|
103
|
+
sourcecode-1.35.33.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|