sourcecode 2.5.3__py3-none-any.whl → 2.5.4__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 +101 -21
- sourcecode/classifier.py +4 -2
- sourcecode/detectors/java.py +27 -10
- sourcecode/reconciliation.py +201 -0
- sourcecode/repository_ir.py +255 -39
- sourcecode/spring_impact.py +157 -17
- sourcecode/summarizer.py +35 -14
- {sourcecode-2.5.3.dist-info → sourcecode-2.5.4.dist-info}/METADATA +1 -1
- {sourcecode-2.5.3.dist-info → sourcecode-2.5.4.dist-info}/RECORD +13 -12
- {sourcecode-2.5.3.dist-info → sourcecode-2.5.4.dist-info}/WHEEL +0 -0
- {sourcecode-2.5.3.dist-info → sourcecode-2.5.4.dist-info}/entry_points.txt +0 -0
- {sourcecode-2.5.3.dist-info → sourcecode-2.5.4.dist-info}/licenses/LICENSE +0 -0
sourcecode/repository_ir.py
CHANGED
|
@@ -130,9 +130,15 @@ _ANN_WITH_ARGS_RE = re.compile(
|
|
|
130
130
|
|
|
131
131
|
_CLASS_DECL_RE = re.compile(
|
|
132
132
|
r'(?:^|(?<=\s))'
|
|
133
|
-
r'(?P<kind>class|interface|enum|@interface)\s+'
|
|
133
|
+
r'(?P<kind>class|interface|enum|record|@interface)\s+'
|
|
134
134
|
r'(?P<name>[A-Z]\w*)'
|
|
135
135
|
r'(?:\s*<[^{;]*?(?=>|\{))?'
|
|
136
|
+
# P1-F: record component list `record Point(int x, int y)`. `[^{;]*` is greedy
|
|
137
|
+
# to the LAST ')' before the body brace, so annotated components with
|
|
138
|
+
# argument parens (`@JsonProperty("x") String name`) stay inside the group.
|
|
139
|
+
# `record` is a contextual keyword: requiring `Name(` + a terminal '{' keeps
|
|
140
|
+
# identifiers named record (variables, methods) from matching.
|
|
141
|
+
r'(?:\s*\((?P<components>[^{;]*)\))?'
|
|
136
142
|
r'(?:\s+extends\s+(?P<extends>[\w.<>?,\s]+?))?'
|
|
137
143
|
r'(?:\s+implements\s+(?P<implements>[\w.<>?,\s]+?))?'
|
|
138
144
|
r'(?:\s+permits\s+[\w,\s]+?)?'
|
|
@@ -145,7 +151,7 @@ _CLASS_DECL_RE = re.compile(
|
|
|
145
151
|
# edges the impact graph needs. `[^{;]*?` keeps the match within a single declaration
|
|
146
152
|
# head (stops at the body `{` or a `;`), matching the precision of _CLASS_DECL_RE.
|
|
147
153
|
_INHERIT_PRESCAN_RE = re.compile(
|
|
148
|
-
r'\b(?:class|interface)\s+[A-Z]\w*[^{;]*?\b(?:extends|implements)\b'
|
|
154
|
+
r'\b(?:class|interface|record)\s+[A-Z]\w*[^{;]*?\b(?:extends|implements)\b'
|
|
149
155
|
)
|
|
150
156
|
|
|
151
157
|
_METHOD_DECL_RE = re.compile(
|
|
@@ -197,6 +203,84 @@ _JAXRS_HTTP_ANNOTATIONS: frozenset[str] = frozenset({
|
|
|
197
203
|
# Annotations whose args contain a path value and must be captured.
|
|
198
204
|
_PATH_ANNOTATIONS: frozenset[str] = frozenset({"@Path"})
|
|
199
205
|
|
|
206
|
+
# P1-E cheap pre-filter: only a zero-symbol file containing a type keyword
|
|
207
|
+
# anywhere pays the linear mask + declaration check (reconciliation rule).
|
|
208
|
+
_TYPE_DECL_KEYWORD_RE = re.compile(r"\b(?:class|interface|enum|record)\b")
|
|
209
|
+
|
|
210
|
+
# P1-G: an inline-CLOSED block comment inside a declaration line.
|
|
211
|
+
_INLINE_BLOCK_COMMENT_RE = re.compile(r"/\*.*?\*/")
|
|
212
|
+
|
|
213
|
+
# SIM-1: `return new X(...)` inside a method body — the concrete type a locator
|
|
214
|
+
# method instantiates when its DECLARED return type is unusable (`Object`).
|
|
215
|
+
# Tolerates a package-qualified head (`return new org.x.Y(...)`).
|
|
216
|
+
_RETURN_NEW_RE = re.compile(r"\breturn\s+new\s+(?:[a-z_]\w*\.)*([A-Z]\w*)\s*[(<]")
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _note_return_new(annotation_values: dict, text: str) -> None:
|
|
220
|
+
"""Accumulate distinct instantiated-return types on a method's annotation_values."""
|
|
221
|
+
for _rm in _RETURN_NEW_RE.finditer(text):
|
|
222
|
+
_t = _rm.group(1)
|
|
223
|
+
_prev = annotation_values.get("_returns_new", "")
|
|
224
|
+
_seen = _prev.split(",") if _prev else []
|
|
225
|
+
if _t not in _seen:
|
|
226
|
+
annotation_values["_returns_new"] = ",".join(_seen + [_t])
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _scan_line_comments(line: str, in_block: bool) -> tuple[str, bool]:
|
|
230
|
+
"""Remove block-comment spans from one line, STRING-LITERAL AWARE (P1-G).
|
|
231
|
+
|
|
232
|
+
Naive `"/*" in line` checks misread comment markers inside string literals
|
|
233
|
+
(`@Consumes("*/*")`, eureka DiscoveryJerseyProvider) and poison the block-
|
|
234
|
+
comment state for the rest of the file. This scanner tracks quotes so
|
|
235
|
+
markers inside strings/chars are code, drops `//` tails, and returns the
|
|
236
|
+
residual code text plus the new in-block state. Callers fast-path: only a
|
|
237
|
+
line containing a marker (or scanned while inside a block) pays this loop.
|
|
238
|
+
"""
|
|
239
|
+
out: list[str] = []
|
|
240
|
+
i, n = 0, len(line)
|
|
241
|
+
in_str = in_chr = False
|
|
242
|
+
while i < n:
|
|
243
|
+
c = line[i]
|
|
244
|
+
if in_block:
|
|
245
|
+
if c == "*" and i + 1 < n and line[i + 1] == "/":
|
|
246
|
+
in_block = False
|
|
247
|
+
i += 2
|
|
248
|
+
continue
|
|
249
|
+
i += 1
|
|
250
|
+
continue
|
|
251
|
+
if in_str or in_chr:
|
|
252
|
+
out.append(c)
|
|
253
|
+
if c == "\\" and i + 1 < n:
|
|
254
|
+
out.append(line[i + 1])
|
|
255
|
+
i += 2
|
|
256
|
+
continue
|
|
257
|
+
if in_str and c == '"':
|
|
258
|
+
in_str = False
|
|
259
|
+
elif in_chr and c == "'":
|
|
260
|
+
in_chr = False
|
|
261
|
+
i += 1
|
|
262
|
+
continue
|
|
263
|
+
if c == '"':
|
|
264
|
+
in_str = True
|
|
265
|
+
out.append(c)
|
|
266
|
+
i += 1
|
|
267
|
+
continue
|
|
268
|
+
if c == "'":
|
|
269
|
+
in_chr = True
|
|
270
|
+
out.append(c)
|
|
271
|
+
i += 1
|
|
272
|
+
continue
|
|
273
|
+
if c == "/" and i + 1 < n:
|
|
274
|
+
if line[i + 1] == "/":
|
|
275
|
+
break # line comment — rest of the line is not code
|
|
276
|
+
if line[i + 1] == "*":
|
|
277
|
+
in_block = True
|
|
278
|
+
i += 2
|
|
279
|
+
continue
|
|
280
|
+
out.append(c)
|
|
281
|
+
i += 1
|
|
282
|
+
return "".join(out), in_block
|
|
283
|
+
|
|
200
284
|
# Security / authorization annotations whose args must be captured.
|
|
201
285
|
# Includes standard Jakarta EE, JAX-RS, Quarkus/MicroProfile, and custom patterns.
|
|
202
286
|
_PERMISSION_ANNOTATIONS: frozenset[str] = frozenset({
|
|
@@ -563,7 +647,7 @@ _ANN_PREFIX_RE = re.compile(r'^(?:@\w+\s*(?:\([^)]*\))?\s*)+')
|
|
|
563
647
|
_STRING_LITERAL_RE = re.compile(r'"(?:[^"\\]|\\.)*"|\'(?:[^\'\\]|\\.)*\'')
|
|
564
648
|
|
|
565
649
|
# Module-level cache for class-keyword detection (avoids recompilation per _extract_symbols call)
|
|
566
|
-
_CLASS_KW_RE = re.compile(r'\b(?:class|interface|enum)\s+[A-Z]')
|
|
650
|
+
_CLASS_KW_RE = re.compile(r'\b(?:class|interface|enum|record)\s+[A-Z]')
|
|
567
651
|
|
|
568
652
|
|
|
569
653
|
# ---------------------------------------------------------------------------
|
|
@@ -774,6 +858,9 @@ def _extract_symbols(
|
|
|
774
858
|
pending_anns: list[str] = []
|
|
775
859
|
pending_ann_values: dict[str, str] = {}
|
|
776
860
|
in_block_comment = False
|
|
861
|
+
# SIM-1: (symbol index, declaration-line depth) of the method whose body the
|
|
862
|
+
# scan is currently inside — target for `return new X(...)` attribution.
|
|
863
|
+
_ret_new_owner: Optional[tuple[int, int]] = None
|
|
777
864
|
|
|
778
865
|
# BUG-PARSER-001: normalize multi-line class declarations where the opening brace
|
|
779
866
|
# appears on a continuation line (e.g. "implements A,\n B, C {").
|
|
@@ -837,14 +924,18 @@ def _extract_symbols(
|
|
|
837
924
|
cur_line = _normalized_src_lines[_ni] if _ni < len(_normalized_src_lines) else None
|
|
838
925
|
stripped = line.strip()
|
|
839
926
|
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
in_block_comment = True
|
|
927
|
+
# P1-G: comment handling. Old code dropped ANY line containing a block-
|
|
928
|
+
# comment marker, silently erasing declarations glued to comments
|
|
929
|
+
# (`class Proc /* impl */{`, `*/public class X {`) — and a naive marker
|
|
930
|
+
# check misreads markers inside string literals (`@Consumes("*/*")`).
|
|
931
|
+
# Fast path: a line with no marker while outside a block skips the scan.
|
|
932
|
+
if in_block_comment and "*/" not in stripped:
|
|
847
933
|
continue
|
|
934
|
+
if in_block_comment or "/*" in stripped:
|
|
935
|
+
stripped, in_block_comment = _scan_line_comments(stripped, in_block_comment)
|
|
936
|
+
stripped = stripped.strip()
|
|
937
|
+
if not stripped:
|
|
938
|
+
continue
|
|
848
939
|
if stripped.startswith("//") or stripped.startswith("*"):
|
|
849
940
|
continue
|
|
850
941
|
|
|
@@ -878,27 +969,38 @@ def _extract_symbols(
|
|
|
878
969
|
modifiers = _extract_modifiers(stripped[:cls_m.start()])
|
|
879
970
|
extends_str = (cls_m.group("extends") or "").strip()
|
|
880
971
|
implements_str = (cls_m.group("implements") or "").strip()
|
|
972
|
+
# P1-F: record components carry the type's state — their types are
|
|
973
|
+
# real dependencies (imports_used) and the list belongs in the
|
|
974
|
+
# signature so consumers see the shape without re-reading source.
|
|
975
|
+
components_str = (cls_m.group("components") or "").strip()
|
|
881
976
|
|
|
882
977
|
sig_types = ([extends_str] if extends_str else []) + (
|
|
883
978
|
[s.strip() for s in implements_str.split(",") if s.strip()]
|
|
884
979
|
if implements_str else []
|
|
885
980
|
)
|
|
886
|
-
used = _resolve_types_from_text(
|
|
981
|
+
used = _resolve_types_from_text(
|
|
982
|
+
" ".join(sig_types) + (" " + components_str if components_str else ""),
|
|
983
|
+
import_map,
|
|
984
|
+
)
|
|
887
985
|
|
|
888
986
|
sym_type = "interface" if kind_kw == "interface" else "class"
|
|
889
987
|
|
|
890
|
-
# symbol_kind distinguishes enum/annotation from class/interface
|
|
988
|
+
# symbol_kind distinguishes enum/annotation/record from class/interface
|
|
891
989
|
if kind_kw == "enum":
|
|
892
990
|
sym_kind = "enum"
|
|
893
991
|
elif kind_kw == "@interface":
|
|
894
992
|
sym_kind = "annotation"
|
|
895
993
|
elif kind_kw == "interface":
|
|
896
994
|
sym_kind = "interface"
|
|
995
|
+
elif kind_kw == "record":
|
|
996
|
+
sym_kind = "record"
|
|
897
997
|
else:
|
|
898
998
|
sym_kind = "class"
|
|
899
999
|
|
|
900
1000
|
_stable_id = _compute_stable_id(package, name, sym_kind, name)
|
|
901
1001
|
_sig_parts = [kind_kw, name]
|
|
1002
|
+
if components_str:
|
|
1003
|
+
_sig_parts.append(f"({components_str})")
|
|
902
1004
|
if extends_str:
|
|
903
1005
|
_sig_parts.append(f"extends {extends_str}")
|
|
904
1006
|
if implements_str:
|
|
@@ -1008,6 +1110,10 @@ def _extract_symbols(
|
|
|
1008
1110
|
))
|
|
1009
1111
|
pending_anns = []
|
|
1010
1112
|
pending_ann_values = {}
|
|
1113
|
+
# SIM-1: remember this method as the enclosing body for
|
|
1114
|
+
# `return new X(...)` attribution (locator child binding).
|
|
1115
|
+
_ret_new_owner = (len(symbols) - 1, depth)
|
|
1116
|
+
_note_return_new(symbols[-1].annotation_values, stripped)
|
|
1011
1117
|
depth += net
|
|
1012
1118
|
_pop_closed(class_stack, depth)
|
|
1013
1119
|
continue
|
|
@@ -1089,6 +1195,15 @@ def _extract_symbols(
|
|
|
1089
1195
|
_pop_closed(class_stack, depth)
|
|
1090
1196
|
continue
|
|
1091
1197
|
|
|
1198
|
+
# SIM-1: attribute `return new X(...)` body statements to the enclosing
|
|
1199
|
+
# method. Owner is cleared as soon as a line at (or above) the method's
|
|
1200
|
+
# declaration depth is seen — the method body has closed.
|
|
1201
|
+
if _ret_new_owner is not None:
|
|
1202
|
+
if depth <= _ret_new_owner[1]:
|
|
1203
|
+
_ret_new_owner = None
|
|
1204
|
+
elif "return new" in stripped:
|
|
1205
|
+
_note_return_new(symbols[_ret_new_owner[0]].annotation_values, stripped)
|
|
1206
|
+
|
|
1092
1207
|
pending_anns = []
|
|
1093
1208
|
pending_ann_values = {}
|
|
1094
1209
|
depth += net
|
|
@@ -1243,7 +1358,7 @@ _ANN_REF_TOKEN_RE = re.compile(r'\b[A-Z]\w*(?:\.\w+)+')
|
|
|
1243
1358
|
# the class whose body encloses them.
|
|
1244
1359
|
_ANN_OR_DECL_OR_BRACE_RE = re.compile(
|
|
1245
1360
|
r'@([A-Z]\w*)\s*\('
|
|
1246
|
-
r'|\b(?:class|interface|enum)\s+([A-Z]\w*)'
|
|
1361
|
+
r'|\b(?:class|interface|enum|record)\s+([A-Z]\w*)'
|
|
1247
1362
|
r'|([{}])'
|
|
1248
1363
|
)
|
|
1249
1364
|
|
|
@@ -2016,6 +2131,38 @@ def _file_import_map(source: str) -> dict[str, str]:
|
|
|
2016
2131
|
return out
|
|
2017
2132
|
|
|
2018
2133
|
|
|
2134
|
+
def _record_component_pairs(components: str) -> list[tuple[str, str]]:
|
|
2135
|
+
"""(name, declared_type) pairs from a record component list (P1-F).
|
|
2136
|
+
|
|
2137
|
+
Depth-aware comma split so generic commas (`Map<String, X> m`) and
|
|
2138
|
+
annotation-argument parens don't break components apart; leading
|
|
2139
|
+
annotations stripped; varargs normalized to arrays.
|
|
2140
|
+
"""
|
|
2141
|
+
parts: list[str] = []
|
|
2142
|
+
buf: list[str] = []
|
|
2143
|
+
d = 0
|
|
2144
|
+
for ch in components:
|
|
2145
|
+
if ch in "<(":
|
|
2146
|
+
d += 1
|
|
2147
|
+
elif ch in ">)":
|
|
2148
|
+
d -= 1
|
|
2149
|
+
if ch == "," and d == 0:
|
|
2150
|
+
parts.append("".join(buf))
|
|
2151
|
+
buf = []
|
|
2152
|
+
else:
|
|
2153
|
+
buf.append(ch)
|
|
2154
|
+
if buf:
|
|
2155
|
+
parts.append("".join(buf))
|
|
2156
|
+
out: list[tuple[str, str]] = []
|
|
2157
|
+
for p in parts:
|
|
2158
|
+
p = re.sub(r"@[\w.]+(?:\([^)]*\))?\s*", "", p).strip()
|
|
2159
|
+
p = p.replace("...", "[] ")
|
|
2160
|
+
toks = p.rsplit(None, 1)
|
|
2161
|
+
if len(toks) == 2 and toks[1].isidentifier():
|
|
2162
|
+
out.append((toks[1], toks[0].strip()))
|
|
2163
|
+
return out
|
|
2164
|
+
|
|
2165
|
+
|
|
2019
2166
|
def _extract_field_types(
|
|
2020
2167
|
source: str, *, imports: Optional[dict[str, str]] = None
|
|
2021
2168
|
) -> dict[str, dict[str, str]]:
|
|
@@ -2044,14 +2191,14 @@ def _extract_field_types(
|
|
|
2044
2191
|
in_block = False
|
|
2045
2192
|
for raw in source.splitlines():
|
|
2046
2193
|
stripped = raw.strip()
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
in_block = False
|
|
2050
|
-
continue
|
|
2051
|
-
if stripped.startswith("/*"):
|
|
2052
|
-
if "*/" not in stripped:
|
|
2053
|
-
in_block = True
|
|
2194
|
+
# P1-G: string-literal-aware comment handling (see _extract_symbols).
|
|
2195
|
+
if in_block and "*/" not in stripped:
|
|
2054
2196
|
continue
|
|
2197
|
+
if in_block or "/*" in stripped:
|
|
2198
|
+
stripped, in_block = _scan_line_comments(stripped, in_block)
|
|
2199
|
+
stripped = stripped.strip()
|
|
2200
|
+
if not stripped:
|
|
2201
|
+
continue
|
|
2055
2202
|
if not stripped or stripped.startswith("//") or stripped.startswith("*"):
|
|
2056
2203
|
continue
|
|
2057
2204
|
if stripped.startswith("@"):
|
|
@@ -2073,6 +2220,16 @@ def _extract_field_types(
|
|
|
2073
2220
|
fqn = f"{class_stack[-1][0]}.{name}" if class_stack else (
|
|
2074
2221
|
f"{package}.{name}" if package else name)
|
|
2075
2222
|
class_stack.append((fqn, depth))
|
|
2223
|
+
# P1-F: record components are implicit final fields — register them
|
|
2224
|
+
# so a `component.method()` receiver binds to its declared type the
|
|
2225
|
+
# same way an ordinary injected field does.
|
|
2226
|
+
_comps = (cls_m.group("components") or "").strip()
|
|
2227
|
+
if cls_m.group("kind") == "record" and _comps:
|
|
2228
|
+
for _cn, _ct in _record_component_pairs(_comps):
|
|
2229
|
+
_outer, _ = _type_tokens(_ct)
|
|
2230
|
+
if _cn and _outer and _cn not in _JAVA_KEYWORDS:
|
|
2231
|
+
out.setdefault(fqn, {}).setdefault(
|
|
2232
|
+
_cn, imports.get(_outer, _outer))
|
|
2076
2233
|
depth += net
|
|
2077
2234
|
_pop_closed(class_stack, depth)
|
|
2078
2235
|
continue
|
|
@@ -2865,14 +3022,22 @@ def _build_jaxrs_locator_map(symbols: list["SymbolRecord"]) -> dict[str, list[tu
|
|
|
2865
3022
|
locator_path = _parse_route_path(sym.annotation_values.get("@Path", ""))
|
|
2866
3023
|
|
|
2867
3024
|
ret = sym.return_type.strip()
|
|
2868
|
-
if not ret:
|
|
2869
|
-
continue
|
|
2870
3025
|
# Simplify: strip package prefix and generics
|
|
2871
|
-
ret_simple = ret.split(".")[-1].split("<")[0].strip()
|
|
2872
|
-
if not ret_simple or ret_simple in _LOCATOR_SKIP_RETURN_TYPES
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
3026
|
+
ret_simple = ret.split(".")[-1].split("<")[0].strip() if ret else ""
|
|
3027
|
+
if (not ret_simple or ret_simple in _LOCATOR_SKIP_RETURN_TYPES
|
|
3028
|
+
or ret_simple[0].islower()):
|
|
3029
|
+
# SIM-1: JAX-RS locators commonly declare `Object` while the body
|
|
3030
|
+
# instantiates the concrete sub-resource (keycloak
|
|
3031
|
+
# `@Path("auth") public Object auth() { return new
|
|
3032
|
+
# AuthorizationEndpoint(...); }`). When the declared type is
|
|
3033
|
+
# unusable, bind through the instantiated type — but only when it
|
|
3034
|
+
# is UNAMBIGUOUS (exactly one distinct `return new` type; a
|
|
3035
|
+
# dynamic/multi-branch locator stays unresolved, honestly).
|
|
3036
|
+
_rn = sym.annotation_values.get("_returns_new", "")
|
|
3037
|
+
_cands = [t for t in _rn.split(",") if t] if _rn else []
|
|
3038
|
+
if len(set(_cands)) != 1 or _cands[0] in _LOCATOR_SKIP_RETURN_TYPES:
|
|
3039
|
+
continue
|
|
3040
|
+
ret_simple = _cands[0]
|
|
2876
3041
|
|
|
2877
3042
|
parent_fqn = _enclosing_class(sym.symbol)
|
|
2878
3043
|
parent_simple = parent_fqn.split(".")[-1]
|
|
@@ -2904,27 +3069,40 @@ def _resolve_jaxrs_prefixes(
|
|
|
2904
3069
|
return own_prefixes
|
|
2905
3070
|
|
|
2906
3071
|
new_visited = visited | {cls_simple}
|
|
2907
|
-
|
|
3072
|
+
# Rooted = composed through a parent that has a resolvable prefix (or a
|
|
3073
|
+
# class-level @Path). Unrooted = the parent chain dead-ends in a class with
|
|
3074
|
+
# no static mount point (keycloak: OIDCLoginProtocolService is mounted via a
|
|
3075
|
+
# fully dynamic `getProtocol` SPI locator).
|
|
3076
|
+
rooted_prefixes: list[str] = []
|
|
3077
|
+
unrooted_prefixes: list[str] = []
|
|
2908
3078
|
|
|
2909
3079
|
for parent_simple, locator_path in locator_map[cls_simple]:
|
|
2910
3080
|
parent_full = _resolve_jaxrs_prefixes(parent_simple, class_info, locator_map, new_visited)
|
|
2911
|
-
#
|
|
2912
|
-
#
|
|
2913
|
-
# (e.g. DefaultClientsApi implements ClientsApi)
|
|
2914
|
-
#
|
|
2915
|
-
#
|
|
2916
|
-
#
|
|
3081
|
+
# Impl/unrooted parent split: a parent resolving to only empty prefixes
|
|
3082
|
+
# with no class-level @Path is either a concrete impl class duplicating
|
|
3083
|
+
# its interface's locator (e.g. DefaultClientsApi implements ClientsApi)
|
|
3084
|
+
# or a genuinely unrooted chain. When ANY rooted composition exists it
|
|
3085
|
+
# wins and the unrooted ones are dropped (the impl-duplicate case, as
|
|
3086
|
+
# before). When NONE exists, the RELATIVE composition is still far more
|
|
3087
|
+
# truthful than collapsing 39 endpoints to path "/" (SIM-1): emit
|
|
3088
|
+
# locator_path + own path with the unresolved head simply absent.
|
|
2917
3089
|
_parent_has_path_ann = class_info.get(parent_simple, {}).get("has_path_ann", False)
|
|
2918
3090
|
_non_empty_parent = [p for p in parent_full if p]
|
|
2919
|
-
if not _non_empty_parent and not _parent_has_path_ann:
|
|
2920
|
-
continue
|
|
2921
3091
|
use_parent_paths = _non_empty_parent if _non_empty_parent else parent_full
|
|
3092
|
+
_bucket = (
|
|
3093
|
+
rooted_prefixes if (_non_empty_parent or _parent_has_path_ann)
|
|
3094
|
+
else unrooted_prefixes
|
|
3095
|
+
)
|
|
2922
3096
|
for pp in use_parent_paths:
|
|
2923
3097
|
for op in own_prefixes:
|
|
2924
3098
|
combined = _join_path_segments(pp, locator_path, op)
|
|
2925
|
-
|
|
3099
|
+
_bucket.append(combined)
|
|
2926
3100
|
|
|
2927
|
-
|
|
3101
|
+
if rooted_prefixes:
|
|
3102
|
+
return rooted_prefixes
|
|
3103
|
+
if unrooted_prefixes:
|
|
3104
|
+
return unrooted_prefixes
|
|
3105
|
+
return own_prefixes
|
|
2928
3106
|
|
|
2929
3107
|
|
|
2930
3108
|
# ---------------------------------------------------------------------------
|
|
@@ -3650,6 +3828,7 @@ def _compute_analysis_gaps(
|
|
|
3650
3828
|
spring_summary: dict,
|
|
3651
3829
|
route_surface: list[dict],
|
|
3652
3830
|
relations: list[RelationEdge],
|
|
3831
|
+
unparsed_type_files: Optional[list[str]] = None,
|
|
3653
3832
|
) -> list[dict]:
|
|
3654
3833
|
"""Compute structural analysis gaps — real system failures, not cosmetic issues."""
|
|
3655
3834
|
gaps: list[dict] = []
|
|
@@ -3662,6 +3841,26 @@ def _compute_analysis_gaps(
|
|
|
3662
3841
|
})
|
|
3663
3842
|
return gaps
|
|
3664
3843
|
|
|
3844
|
+
# P1-E: parse-coverage reconciliation (file inventory × symbol index). A file
|
|
3845
|
+
# that declares a type but produced zero symbols is silently invisible to the
|
|
3846
|
+
# graph, callers and endpoints — an agent reading "0 callers" cannot know the
|
|
3847
|
+
# defining or calling code was never modeled unless this gap says so.
|
|
3848
|
+
if unparsed_type_files:
|
|
3849
|
+
from sourcecode.reconciliation import reconcile_extraction_evidence
|
|
3850
|
+
extracted = frozenset(s.declaring_file for s in symbols)
|
|
3851
|
+
for rf in reconcile_extraction_evidence(
|
|
3852
|
+
type_decl_files=frozenset(unparsed_type_files),
|
|
3853
|
+
extracted_files=extracted,
|
|
3854
|
+
):
|
|
3855
|
+
_sample = list(rf.symbols[:5])
|
|
3856
|
+
gaps.append({
|
|
3857
|
+
"area": rf.rule,
|
|
3858
|
+
"reason": rf.detail + " Files: " + ", ".join(_sample)
|
|
3859
|
+
+ (", …" if len(rf.symbols) > 5 else ""),
|
|
3860
|
+
"impact": "high",
|
|
3861
|
+
"files": list(rf.symbols),
|
|
3862
|
+
})
|
|
3863
|
+
|
|
3665
3864
|
controllers = spring_summary.get("controllers", [])
|
|
3666
3865
|
if controllers and not route_surface:
|
|
3667
3866
|
gaps.append({
|
|
@@ -3712,6 +3911,7 @@ def _assemble(
|
|
|
3712
3911
|
spring_summary: dict, # noqa: ARG001 — used internally via _spring_role on symbols
|
|
3713
3912
|
route_diffs: list[dict] | None = None,
|
|
3714
3913
|
custom_security: "tuple[CustomSecuritySpec, ...]" = (),
|
|
3914
|
+
unparsed_type_files: Optional[list[str]] = None,
|
|
3715
3915
|
) -> dict:
|
|
3716
3916
|
"""Phase 5: Final assembly — single deterministic output contract."""
|
|
3717
3917
|
sorted_syms = sorted(symbols, key=lambda s: s.symbol)
|
|
@@ -3976,7 +4176,10 @@ def _assemble(
|
|
|
3976
4176
|
_route_surface = _build_route_surface(
|
|
3977
4177
|
sorted_syms, route_diffs, extends_map=_extends_map, custom_security=custom_security
|
|
3978
4178
|
)
|
|
3979
|
-
_analysis_gaps = _compute_analysis_gaps(
|
|
4179
|
+
_analysis_gaps = _compute_analysis_gaps(
|
|
4180
|
+
sorted_syms, spring_summary, _route_surface, sorted_rels,
|
|
4181
|
+
unparsed_type_files=unparsed_type_files,
|
|
4182
|
+
)
|
|
3980
4183
|
|
|
3981
4184
|
# Detect filter-based security model for the assembled IR.
|
|
3982
4185
|
# Stored here so CIR projections (project_endpoint_surface) can read it without
|
|
@@ -4676,6 +4879,9 @@ def build_repo_ir(
|
|
|
4676
4879
|
# (package, imports, annotation-ref-pairs) for the reference-graph post-pass,
|
|
4677
4880
|
# collected from BOTH fully-parsed and pre-scan-skipped files (BUG #2).
|
|
4678
4881
|
_ann_ref_sources: list[tuple[str, list[str], list[tuple[str, str]]]] = []
|
|
4882
|
+
# P1-E: files whose masked source declares a type but extraction yielded
|
|
4883
|
+
# zero symbols — fed to the parse_coverage reconciliation rule at assembly.
|
|
4884
|
+
_unparsed_type_files: list[str] = []
|
|
4679
4885
|
for rel_path in sorted(file_paths):
|
|
4680
4886
|
abs_path = root / rel_path
|
|
4681
4887
|
try:
|
|
@@ -4689,6 +4895,15 @@ def build_repo_ir(
|
|
|
4689
4895
|
source, rel_path, extra_capture=_extra_capture
|
|
4690
4896
|
)
|
|
4691
4897
|
all_symbols.extend(symbols)
|
|
4898
|
+
# P1-E (parse-coverage reconciliation): a file that declares a type but
|
|
4899
|
+
# yielded zero symbols is silently invisible to the graph. Record the
|
|
4900
|
+
# candidate here (source is already in memory; masking only runs for the
|
|
4901
|
+
# rare zero-symbol file) — the reconciliation rule turns the set into an
|
|
4902
|
+
# analysis_gaps entry at assembly.
|
|
4903
|
+
if not symbols and _TYPE_DECL_KEYWORD_RE.search(source):
|
|
4904
|
+
from sourcecode.reconciliation import declares_java_type
|
|
4905
|
+
if declares_java_type(_mask_java(source)):
|
|
4906
|
+
_unparsed_type_files.append(rel_path)
|
|
4692
4907
|
_per_file.append((rel_path, source, package, raw_imports, symbols))
|
|
4693
4908
|
if "@" in source:
|
|
4694
4909
|
_parsed_refs = _extract_annotation_type_refs(source, package)
|
|
@@ -4806,6 +5021,7 @@ def build_repo_ir(
|
|
|
4806
5021
|
ir = _assemble(
|
|
4807
5022
|
all_symbols, unique_relations, all_changed, spring_summary, route_diffs_arg,
|
|
4808
5023
|
custom_security=_custom_sec_tuple,
|
|
5024
|
+
unparsed_type_files=_unparsed_type_files,
|
|
4809
5025
|
)
|
|
4810
5026
|
|
|
4811
5027
|
# Fase 21: merge OpenAPI-spec-recovered routes for interface-defined controllers
|