sourcecode 1.72.0__py3-none-any.whl → 2.0.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 +6 -2
- sourcecode/architecture_analyzer.py +63 -55
- sourcecode/ast_extractor.py +130 -135
- sourcecode/call_surface.py +112 -0
- sourcecode/canonical_ir.py +62 -1
- sourcecode/cli.py +254 -198
- sourcecode/context_graph.py +1022 -0
- sourcecode/contract_pipeline.py +16 -1
- sourcecode/detectors/java.py +99 -8
- sourcecode/detectors/parsers.py +23 -0
- sourcecode/dynamic_argument_surface.py +54 -0
- sourcecode/endpoint_literals.py +71 -0
- sourcecode/evidence_provider.py +90 -0
- sourcecode/explain.py +28 -13
- sourcecode/graph_analyzer.py +82 -8
- sourcecode/license.py +42 -13
- sourcecode/mcp/onboarding/applier.py +14 -6
- sourcecode/mcp/registry.py +22 -22
- sourcecode/mcp/runner.py +2 -2
- sourcecode/mcp/server.py +24 -24
- sourcecode/mcp_nudge.py +1 -1
- sourcecode/migrate_check.py +4 -12
- sourcecode/pr_comment_renderer.py +7 -6
- sourcecode/prepare_context.py +10 -13
- sourcecode/repository_ir.py +917 -141
- sourcecode/ris.py +1 -1
- sourcecode/semantic_analyzer.py +146 -9
- sourcecode/semantic_impact_engine.py +403 -0
- sourcecode/semantic_integration_engine.py +299 -0
- sourcecode/semantic_services.py +229 -0
- sourcecode/serializer.py +30 -33
- sourcecode/telemetry/consent.py +1 -1
- sourcecode/type_usage_surface.py +101 -0
- sourcecode/validation_surface.py +173 -205
- {sourcecode-1.72.0.dist-info → sourcecode-2.0.0.dist-info}/METADATA +125 -110
- {sourcecode-1.72.0.dist-info → sourcecode-2.0.0.dist-info}/RECORD +39 -32
- {sourcecode-1.72.0.dist-info → sourcecode-2.0.0.dist-info}/entry_points.txt +1 -0
- sourcecode/flow_analyzer.py +0 -671
- sourcecode/integration_detector.py +0 -392
- {sourcecode-1.72.0.dist-info → sourcecode-2.0.0.dist-info}/WHEEL +0 -0
- {sourcecode-1.72.0.dist-info → sourcecode-2.0.0.dist-info}/licenses/LICENSE +0 -0
sourcecode/__init__.py
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""ASK Engine (CLI: ``ask``) — Deterministic codebase context maps for AI coding agents.
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
ASK Engine is the product. ``ask`` is the canonical CLI command; ``sourcecode`` is
|
|
4
|
+
the legacy compatibility alias and the Python/PyPI package name. See
|
|
5
|
+
docs/PRODUCT_IDENTITY.md (normative)."""
|
|
6
|
+
|
|
7
|
+
__version__ = "2.0.0"
|
|
@@ -960,75 +960,83 @@ class ArchitectureAnalyzer:
|
|
|
960
960
|
# Java annotation / base-class secondary scanner (C4)
|
|
961
961
|
# ------------------------------------------------------------------
|
|
962
962
|
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
)
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
963
|
+
# Annotations never reported as "custom": Java/Spring/JPA/bean-validation
|
|
964
|
+
# built-ins. (Kept from the historical text scan; the counts now come from
|
|
965
|
+
# the ContextGraph's node-grounded `annotated_with` relations.)
|
|
966
|
+
_BUILTIN_ANNOTATIONS: frozenset = frozenset({
|
|
967
|
+
"Override", "SuppressWarnings", "Deprecated", "FunctionalInterface",
|
|
968
|
+
"SafeVarargs", "Retention", "Target", "Documented", "Inherited",
|
|
969
|
+
"RestController", "Service", "Repository", "Component", "Controller",
|
|
970
|
+
"Autowired", "Value", "Bean", "Configuration", "SpringBootApplication",
|
|
971
|
+
"EnableAutoConfiguration", "Transactional", "RequestMapping",
|
|
972
|
+
"GetMapping", "PostMapping", "PutMapping", "DeleteMapping",
|
|
973
|
+
"PathVariable", "RequestBody", "RequestParam", "ResponseBody",
|
|
974
|
+
"NotNull", "NotBlank", "Size", "Min", "Max", "Valid",
|
|
975
|
+
})
|
|
976
|
+
# @interface member return types the historical scan reported as parameters.
|
|
977
|
+
_ANNOTATION_PARAM_TYPES: frozenset = frozenset({"String", "int", "boolean", "Class"})
|
|
974
978
|
|
|
975
979
|
def _scan_java_patterns(
|
|
976
980
|
self,
|
|
977
981
|
root: Path,
|
|
978
982
|
java_paths: list[str],
|
|
979
983
|
) -> tuple[list[dict], list[dict]]:
|
|
980
|
-
"""
|
|
984
|
+
"""Derive custom @interface annotations and common base classes from the
|
|
985
|
+
ContextGraph (the single structural access layer — no source parsing).
|
|
986
|
+
|
|
987
|
+
Counts are node-grounded: an annotation usage is an `annotated_with`
|
|
988
|
+
relation on a real symbol, and a subclass is an `extends` relation —
|
|
989
|
+
occurrences inside string literals, comments, or dead text never count
|
|
990
|
+
(the historical text scan could).
|
|
981
991
|
|
|
982
992
|
Returns (custom_annotations, base_classes) lists for ArchitectureAnalysis.
|
|
983
993
|
"""
|
|
984
|
-
from sourcecode.
|
|
994
|
+
from sourcecode.context_graph import ContextGraph
|
|
995
|
+
|
|
996
|
+
graph = ContextGraph.build(java_paths, root)
|
|
985
997
|
|
|
986
|
-
annotation_decls: dict[str, dict] = {} # name → {params,
|
|
987
|
-
annotation_usage: dict[str, int] = {} # name →
|
|
998
|
+
annotation_decls: dict[str, dict] = {} # name → {params, usage_count, purpose}
|
|
999
|
+
annotation_usage: dict[str, int] = {} # name → node-grounded usages
|
|
988
1000
|
extends_counts: dict[str, int] = {} # base_class_name → subclass count
|
|
989
1001
|
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
1002
|
+
# Custom annotation declarations: @interface nodes + their members with
|
|
1003
|
+
# the historically-reported parameter return types.
|
|
1004
|
+
for ann in graph.annotation_types():
|
|
1005
|
+
ann_name = ann.fqn.rsplit(".", 1)[-1]
|
|
1006
|
+
if ann_name in annotation_decls:
|
|
995
1007
|
continue
|
|
1008
|
+
prefix = ann.fqn + "#"
|
|
1009
|
+
# Member return type from the node signature "(params)->ReturnType".
|
|
1010
|
+
params = [
|
|
1011
|
+
m.fqn[len(prefix):]
|
|
1012
|
+
for m in graph.symbols(kind="method")
|
|
1013
|
+
if m.fqn.startswith(prefix)
|
|
1014
|
+
and (m.signature or "").rsplit("->", 1)[-1] in self._ANNOTATION_PARAM_TYPES
|
|
1015
|
+
]
|
|
1016
|
+
annotation_decls[ann_name] = {
|
|
1017
|
+
"name": ann_name,
|
|
1018
|
+
"parameters": params,
|
|
1019
|
+
"usage_count": 0,
|
|
1020
|
+
"purpose": "",
|
|
1021
|
+
}
|
|
996
1022
|
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
ann_name = m.group(1)
|
|
1015
|
-
# Skip common Java built-ins
|
|
1016
|
-
if ann_name in {
|
|
1017
|
-
"Override", "SuppressWarnings", "Deprecated", "FunctionalInterface",
|
|
1018
|
-
"SafeVarargs", "Retention", "Target", "Documented", "Inherited",
|
|
1019
|
-
"RestController", "Service", "Repository", "Component", "Controller",
|
|
1020
|
-
"Autowired", "Value", "Bean", "Configuration", "SpringBootApplication",
|
|
1021
|
-
"EnableAutoConfiguration", "Transactional", "RequestMapping",
|
|
1022
|
-
"GetMapping", "PostMapping", "PutMapping", "DeleteMapping",
|
|
1023
|
-
"PathVariable", "RequestBody", "RequestParam", "ResponseBody",
|
|
1024
|
-
"NotNull", "NotBlank", "Size", "Min", "Max", "Valid",
|
|
1025
|
-
}:
|
|
1026
|
-
continue
|
|
1027
|
-
annotation_usage[ann_name] = annotation_usage.get(ann_name, 0) + 1
|
|
1028
|
-
|
|
1029
|
-
# extends BaseClass — count subclasses per base
|
|
1030
|
-
for m in self._EXTENDS_RE.finditer(content):
|
|
1031
|
-
base = m.group(1)
|
|
1023
|
+
# Annotation usage frequency — one count per annotated_with relation.
|
|
1024
|
+
for rel in graph.relations(kind="annotated_with"):
|
|
1025
|
+
ann_name = rel.target.lstrip("@").rsplit(".", 1)[-1]
|
|
1026
|
+
if not ann_name or not ann_name[0].isupper():
|
|
1027
|
+
continue
|
|
1028
|
+
if ann_name in self._BUILTIN_ANNOTATIONS:
|
|
1029
|
+
continue
|
|
1030
|
+
annotation_usage[ann_name] = annotation_usage.get(ann_name, 0) + 1
|
|
1031
|
+
|
|
1032
|
+
# extends BaseClass — count subclasses per base (class extends only,
|
|
1033
|
+
# matching the historical `class X extends Y` scan).
|
|
1034
|
+
class_fqns = {s.fqn for s in graph.symbols(kind="class")}
|
|
1035
|
+
for rel in graph.relations(kind="extends"):
|
|
1036
|
+
if rel.source not in class_fqns:
|
|
1037
|
+
continue # interface-extends-interface is not a subclass count
|
|
1038
|
+
base = re.sub(r"<.*", "", rel.target).rsplit(".", 1)[-1].strip()
|
|
1039
|
+
if base and base[0].isupper():
|
|
1032
1040
|
extends_counts[base] = extends_counts.get(base, 0) + 1
|
|
1033
1041
|
|
|
1034
1042
|
# Merge usage counts into annotation_decls
|
sourcecode/ast_extractor.py
CHANGED
|
@@ -940,171 +940,136 @@ def _extract_python(path: str, source: str) -> FileContract:
|
|
|
940
940
|
|
|
941
941
|
|
|
942
942
|
# ---------------------------------------------------------------------------
|
|
943
|
+
# Java extraction — projected from ContextGraph (single source of truth)
|
|
943
944
|
# ---------------------------------------------------------------------------
|
|
944
|
-
#
|
|
945
|
-
#
|
|
945
|
+
#
|
|
946
|
+
# The per-file Java contract is no longer parsed with a private regex pass.
|
|
947
|
+
# It is projected from the ContextGraph symbol/relation model — the same model
|
|
948
|
+
# repository_ir already builds — so the codebase carries exactly one Java parse
|
|
949
|
+
# path. ContractPipeline builds one repo-level graph and injects it via
|
|
950
|
+
# `AstExtractor.set_repo_graph`; a standalone `extract()` (no injected graph)
|
|
951
|
+
# falls back to a single-file graph build. Either way the projection below is
|
|
952
|
+
# the only code path, so behaviour is identical whether shared or per-file.
|
|
946
953
|
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
# Single annotation on one line (captures name + optional parens args)
|
|
950
|
-
_JAVA_ANNO_LINE_RE = re.compile(r'^\s*(@[\w.]+(?:\s*\([^)]*\))?)\s*$')
|
|
951
|
-
# Class/interface/enum declaration line (public or package-private)
|
|
952
|
-
_JAVA_CLASS_LINE_RE = re.compile(
|
|
953
|
-
r'(?:public\s+)?(?:(?:abstract|final|static|sealed)\s+)*'
|
|
954
|
-
r'(class|interface|enum|@interface)\s+(\w+)'
|
|
955
|
-
r'(?:\s+extends\s+([\w.]+))?'
|
|
956
|
-
r'(?:\s+implements\s+([\w.,\s<>]+?))?'
|
|
957
|
-
r'(?=\s*[\{<])'
|
|
958
|
-
)
|
|
959
|
-
# Public method: up to 12 leading spaces, return type, name, open paren
|
|
960
|
-
_JAVA_PUB_METHOD_LINE_RE = re.compile(
|
|
961
|
-
r'^\s{0,12}public\s+(?:(?:static|final|synchronized|abstract|default)\s+)*'
|
|
962
|
-
r'[\w<>\[\]?,\s]+?\s+(\w+)\s*\(',
|
|
963
|
-
re.MULTILINE,
|
|
964
|
-
)
|
|
965
|
-
# @Autowired or @Inject field
|
|
966
|
-
_JAVA_FIELD_DECL_RE = re.compile(
|
|
967
|
-
r'^\s*(?:private|protected|public)?\s*'
|
|
968
|
-
r'([\w<>.,\[\]? ]+?)\s+(\w+)\s*[;=]'
|
|
969
|
-
)
|
|
954
|
+
_JAVA_DI_FIELD_ANNS: frozenset[str] = frozenset({"@Autowired", "@Inject"})
|
|
970
955
|
|
|
971
956
|
|
|
972
|
-
def
|
|
973
|
-
"""
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
while i >= 0:
|
|
977
|
-
stripped = lines[i].strip()
|
|
978
|
-
if not stripped or stripped.startswith("//") or stripped.startswith("*"):
|
|
979
|
-
i -= 1
|
|
980
|
-
continue
|
|
981
|
-
m = re.match(r'(@[\w.]+(?:\s*\([^)]*\))?)', stripped)
|
|
982
|
-
if m:
|
|
983
|
-
annotations.insert(0, m.group(1))
|
|
984
|
-
i -= 1
|
|
985
|
-
else:
|
|
986
|
-
break
|
|
987
|
-
return annotations
|
|
957
|
+
def _java_simple_name(fqn: str) -> str:
|
|
958
|
+
"""Declared name from a symbol FQN. For a member (`pkg.Type#member`) keep
|
|
959
|
+
the part after `#`; for a type or field keep the last dotted segment."""
|
|
960
|
+
return fqn.rsplit("#", 1)[-1].rsplit(".", 1)[-1]
|
|
988
961
|
|
|
989
962
|
|
|
990
|
-
def
|
|
991
|
-
|
|
992
|
-
types: list[TypeDefinition] = []
|
|
993
|
-
functions: list[FunctionSignature] = []
|
|
994
|
-
imports: list[ImportRecord] = []
|
|
995
|
-
autowired_fields: list[dict] = []
|
|
963
|
+
def _java_contract_from_graph(rel_path: str, graph: "Any") -> FileContract:
|
|
964
|
+
"""Project a Java FileContract for one file from the ContextGraph.
|
|
996
965
|
|
|
997
|
-
|
|
966
|
+
Structural facts (types, members, imports, DI fields) are read from graph
|
|
967
|
+
nodes/edges instead of re-parsing source. Method signatures are the graph's
|
|
968
|
+
canonical compact form (`(Type)->Return`); annotations are node-grounded.
|
|
969
|
+
"""
|
|
970
|
+
file_syms = [s for s in graph.symbols() if s.source_file == rel_path]
|
|
971
|
+
type_syms = [s for s in file_syms if s.kind in ("class", "interface", "enum", "annotation")]
|
|
972
|
+
type_fqns = {s.fqn for s in type_syms}
|
|
973
|
+
|
|
974
|
+
# extends / implements edges out of this file's types (targets are simple
|
|
975
|
+
# names for out-of-repo supertypes, resolved FQNs for in-repo ones — the
|
|
976
|
+
# same forms the old regex captured for the unresolved case).
|
|
977
|
+
extends_of: dict[str, list[str]] = {}
|
|
978
|
+
implements_of: dict[str, list[str]] = {}
|
|
979
|
+
import_targets: set[str] = set()
|
|
980
|
+
for r in graph.relations():
|
|
981
|
+
if r.source not in type_fqns:
|
|
982
|
+
continue
|
|
983
|
+
if r.kind == "extends":
|
|
984
|
+
extends_of.setdefault(r.source, []).append(r.target)
|
|
985
|
+
elif r.kind == "implements":
|
|
986
|
+
implements_of.setdefault(r.source, []).append(r.target)
|
|
987
|
+
elif r.kind == "imports":
|
|
988
|
+
import_targets.add(r.target)
|
|
998
989
|
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
for
|
|
1002
|
-
|
|
1003
|
-
if
|
|
1004
|
-
|
|
1005
|
-
|
|
990
|
+
exports: list[ExportRecord] = []
|
|
991
|
+
types: list[TypeDefinition] = []
|
|
992
|
+
for s in sorted(type_syms, key=lambda s: s.fqn):
|
|
993
|
+
name = _java_simple_name(s.fqn)
|
|
994
|
+
export_kind = "class" if s.kind == "annotation" else s.kind
|
|
995
|
+
parents = extends_of.get(s.fqn, [])
|
|
996
|
+
impls = sorted(implements_of.get(s.fqn, []))
|
|
997
|
+
exports.append(ExportRecord(
|
|
998
|
+
name=name,
|
|
999
|
+
kind=export_kind,
|
|
1000
|
+
annotations=list(s.annotations),
|
|
1001
|
+
extends=parents[0] if parents else None,
|
|
1002
|
+
implements=impls,
|
|
1003
|
+
))
|
|
1004
|
+
types.append(TypeDefinition(
|
|
1005
|
+
name=name,
|
|
1006
|
+
kind=export_kind,
|
|
1007
|
+
fields=[],
|
|
1008
|
+
extends=sorted(parents),
|
|
1009
|
+
))
|
|
1006
1010
|
|
|
1007
|
-
#
|
|
1008
|
-
|
|
1011
|
+
# Public methods (match the historical `public`-keyword scope). Constructors
|
|
1012
|
+
# are a distinct node kind and stay excluded, as the old return-type regex
|
|
1013
|
+
# excluded them. Dedup by name, keeping first in FQN order.
|
|
1014
|
+
functions: list[FunctionSignature] = []
|
|
1009
1015
|
seen_methods: set[str] = set()
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
for idx, line in enumerate(lines):
|
|
1013
|
-
stripped = line.strip()
|
|
1014
|
-
|
|
1015
|
-
# Detect @Autowired / @Inject annotations
|
|
1016
|
-
if stripped.startswith("@Autowired") or stripped.startswith("@Inject"):
|
|
1017
|
-
autowired_pending = True
|
|
1016
|
+
for s in sorted((m for m in file_syms if m.kind == "method"), key=lambda s: s.fqn):
|
|
1017
|
+
if "public" not in s.modifiers:
|
|
1018
1018
|
continue
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
if autowired_pending and stripped and not stripped.startswith("@"):
|
|
1022
|
-
autowired_pending = False
|
|
1023
|
-
fm = _JAVA_FIELD_DECL_RE.match(line)
|
|
1024
|
-
if fm:
|
|
1025
|
-
type_name = fm.group(1).strip().split()[-1] # last word = simple type
|
|
1026
|
-
field_name = fm.group(2).strip()
|
|
1027
|
-
if field_name and type_name and field_name not in {"class", "interface"}:
|
|
1028
|
-
autowired_fields.append({"type": type_name, "name": field_name})
|
|
1029
|
-
elif stripped and not stripped.startswith("@"):
|
|
1030
|
-
autowired_pending = False
|
|
1031
|
-
|
|
1032
|
-
# Class/interface/enum declaration
|
|
1033
|
-
cm = _JAVA_CLASS_LINE_RE.search(stripped)
|
|
1034
|
-
if cm and ("class " in stripped or "interface " in stripped or "enum " in stripped):
|
|
1035
|
-
kind_kw = cm.group(1) # class | interface | enum | @interface
|
|
1036
|
-
name = cm.group(2)
|
|
1037
|
-
extends_str = cm.group(3)
|
|
1038
|
-
implements_str = cm.group(4)
|
|
1039
|
-
|
|
1040
|
-
annotations = _java_collect_preceding_annotations(lines, idx)
|
|
1041
|
-
all_extends: list[str] = []
|
|
1042
|
-
if extends_str:
|
|
1043
|
-
all_extends.append(extends_str.strip())
|
|
1044
|
-
|
|
1045
|
-
implements_list: list[str] = []
|
|
1046
|
-
if implements_str:
|
|
1047
|
-
implements_list = [i.strip() for i in implements_str.split(",") if i.strip()]
|
|
1048
|
-
|
|
1049
|
-
export_kind = "class" if kind_kw in ("class", "@interface") else kind_kw
|
|
1050
|
-
exports.append(ExportRecord(
|
|
1051
|
-
name=name,
|
|
1052
|
-
kind=export_kind,
|
|
1053
|
-
annotations=annotations,
|
|
1054
|
-
extends=extends_str.strip() if extends_str else None,
|
|
1055
|
-
implements=implements_list,
|
|
1056
|
-
))
|
|
1057
|
-
types.append(TypeDefinition(
|
|
1058
|
-
name=name,
|
|
1059
|
-
kind=export_kind,
|
|
1060
|
-
fields=[],
|
|
1061
|
-
extends=all_extends,
|
|
1062
|
-
))
|
|
1063
|
-
class_names.add(name)
|
|
1064
|
-
|
|
1065
|
-
# Pass 3: public methods with their preceding annotations
|
|
1066
|
-
for m in _JAVA_PUB_METHOD_LINE_RE.finditer(source):
|
|
1067
|
-
sig_text = m.group(0).strip()
|
|
1068
|
-
mname = m.group(1)
|
|
1069
|
-
if (mname in class_names or mname in seen_methods
|
|
1070
|
-
or mname in {"if", "for", "while", "switch", "return", "new"}):
|
|
1019
|
+
mname = _java_simple_name(s.fqn)
|
|
1020
|
+
if mname in seen_methods:
|
|
1071
1021
|
continue
|
|
1072
1022
|
seen_methods.add(mname)
|
|
1073
|
-
# Find line index for this match to collect preceding annotations
|
|
1074
|
-
line_start = source.count("\n", 0, m.start())
|
|
1075
|
-
annotations = _java_collect_preceding_annotations(lines, line_start)
|
|
1076
1023
|
exports.append(ExportRecord(
|
|
1077
1024
|
name=mname,
|
|
1078
1025
|
kind="method",
|
|
1079
|
-
annotations=annotations,
|
|
1080
|
-
signature=
|
|
1026
|
+
annotations=list(s.annotations),
|
|
1027
|
+
signature=s.signature or None,
|
|
1081
1028
|
))
|
|
1082
1029
|
functions.append(FunctionSignature(
|
|
1083
1030
|
name=mname,
|
|
1084
|
-
signature=
|
|
1031
|
+
signature=s.signature or "",
|
|
1085
1032
|
async_=False,
|
|
1086
1033
|
exported=True,
|
|
1087
1034
|
return_type=None,
|
|
1088
1035
|
))
|
|
1089
1036
|
|
|
1037
|
+
# DI fields: @Autowired / @Inject field nodes → {type, name}. The field
|
|
1038
|
+
# node's signature is "Type name"; take the type's simple last token.
|
|
1039
|
+
autowired_fields: list[dict] = []
|
|
1040
|
+
for s in sorted((f for f in file_syms if f.kind == "field"), key=lambda s: s.fqn):
|
|
1041
|
+
if not any(a in _JAVA_DI_FIELD_ANNS for a in s.annotations):
|
|
1042
|
+
continue
|
|
1043
|
+
sig = s.signature or ""
|
|
1044
|
+
type_tok = sig.split()[0] if sig else ""
|
|
1045
|
+
type_name = re.sub(r"<.*", "", type_tok).rsplit(".", 1)[-1]
|
|
1046
|
+
field_name = _java_simple_name(s.fqn)
|
|
1047
|
+
if field_name and type_name:
|
|
1048
|
+
autowired_fields.append({"type": type_name, "name": field_name})
|
|
1049
|
+
|
|
1050
|
+
imports = [
|
|
1051
|
+
ImportRecord(source=fqn, kind="named", symbols=[])
|
|
1052
|
+
for fqn in sorted(import_targets)
|
|
1053
|
+
]
|
|
1054
|
+
|
|
1090
1055
|
# External deps: top-2 package segments, skip java.* / javax.*
|
|
1091
1056
|
deps = sorted({
|
|
1092
|
-
".".join(
|
|
1093
|
-
for
|
|
1094
|
-
if not
|
|
1095
|
-
and len(
|
|
1057
|
+
".".join(fqn.split(".")[:2])
|
|
1058
|
+
for fqn in import_targets
|
|
1059
|
+
if not fqn.startswith("java.") and not fqn.startswith("javax.")
|
|
1060
|
+
and len(fqn.split(".")) >= 2
|
|
1096
1061
|
})
|
|
1097
1062
|
|
|
1098
1063
|
return FileContract(
|
|
1099
|
-
path=
|
|
1064
|
+
path=rel_path,
|
|
1100
1065
|
language="java",
|
|
1101
1066
|
exports=exports,
|
|
1102
|
-
imports=
|
|
1067
|
+
imports=imports[:30],
|
|
1103
1068
|
functions=sorted(functions, key=lambda f: f.name)[:20],
|
|
1104
1069
|
types=sorted(types, key=lambda t: t.name),
|
|
1105
1070
|
dependencies=deps[:20],
|
|
1106
1071
|
autowired_fields=autowired_fields[:20],
|
|
1107
|
-
extraction_method="
|
|
1072
|
+
extraction_method="graph",
|
|
1108
1073
|
)
|
|
1109
1074
|
|
|
1110
1075
|
|
|
@@ -1245,6 +1210,26 @@ class AstExtractor:
|
|
|
1245
1210
|
self.max_file_size = max_file_size
|
|
1246
1211
|
self._ts_checked = False
|
|
1247
1212
|
self._ts_ok = False
|
|
1213
|
+
# Repo-level ContextGraph for Java projection, injected by
|
|
1214
|
+
# ContractPipeline so all files share one parse. (root, graph).
|
|
1215
|
+
self._repo_graph_root: Optional[Path] = None
|
|
1216
|
+
self._repo_graph: Optional[Any] = None
|
|
1217
|
+
|
|
1218
|
+
def set_repo_graph(self, graph: "Any", root: Path) -> None:
|
|
1219
|
+
"""Inject a pre-built repo-level ContextGraph so per-file Java contracts
|
|
1220
|
+
project from it instead of each building a single-file graph."""
|
|
1221
|
+
self._repo_graph = graph
|
|
1222
|
+
self._repo_graph_root = root
|
|
1223
|
+
|
|
1224
|
+
def _java_graph_for(self, root: Path) -> "Any":
|
|
1225
|
+
"""Return the shared graph when it covers `root`, else build a
|
|
1226
|
+
single-file-scope graph for standalone extraction."""
|
|
1227
|
+
if self._repo_graph is not None and self._repo_graph_root == root:
|
|
1228
|
+
return self._repo_graph
|
|
1229
|
+
from sourcecode.context_graph import ContextGraph
|
|
1230
|
+
from sourcecode.repository_ir import find_java_files
|
|
1231
|
+
files = find_java_files(root)
|
|
1232
|
+
return ContextGraph.build(files, root)
|
|
1248
1233
|
|
|
1249
1234
|
def _ensure_ts(self) -> bool:
|
|
1250
1235
|
if not self._ts_checked:
|
|
@@ -1268,12 +1253,13 @@ class AstExtractor:
|
|
|
1268
1253
|
if language is None:
|
|
1269
1254
|
return None
|
|
1270
1255
|
|
|
1256
|
+
rel_path = str(path.relative_to(root)).replace("\\", "/") if root else path.name
|
|
1257
|
+
|
|
1271
1258
|
try:
|
|
1272
1259
|
stat = path.stat()
|
|
1273
1260
|
if stat.st_size > self.max_file_size:
|
|
1274
|
-
rel = str(path.relative_to(root)) if root else path.name
|
|
1275
1261
|
return FileContract(
|
|
1276
|
-
path=
|
|
1262
|
+
path=str(path.relative_to(root)) if root else path.name,
|
|
1277
1263
|
language=language,
|
|
1278
1264
|
extraction_method="heuristic",
|
|
1279
1265
|
limitations=[f"file_too_large: {stat.st_size} bytes > {self.max_file_size}"],
|
|
@@ -1281,17 +1267,26 @@ class AstExtractor:
|
|
|
1281
1267
|
except OSError:
|
|
1282
1268
|
return None
|
|
1283
1269
|
|
|
1270
|
+
# Java is projected from the ContextGraph, not re-parsed from source.
|
|
1271
|
+
# Use the repo-level graph ContractPipeline injected when it covers this
|
|
1272
|
+
# root; otherwise build a single-file graph (standalone `extract`).
|
|
1273
|
+
if language == "java" and root is not None:
|
|
1274
|
+
graph = self._java_graph_for(root)
|
|
1275
|
+
contract = _java_contract_from_graph(rel_path, graph)
|
|
1276
|
+
contract.role = _detect_role(rel_path, contract)
|
|
1277
|
+
return contract
|
|
1278
|
+
|
|
1284
1279
|
try:
|
|
1285
1280
|
source = path.read_text(encoding="utf-8", errors="replace")
|
|
1286
1281
|
except OSError:
|
|
1287
1282
|
return None
|
|
1288
1283
|
|
|
1289
|
-
rel_path = str(path.relative_to(root)).replace("\\", "/") if root else path.name
|
|
1290
|
-
|
|
1291
1284
|
if language == "python":
|
|
1292
1285
|
contract = _extract_python(rel_path, source)
|
|
1293
1286
|
elif language == "java":
|
|
1294
|
-
|
|
1287
|
+
# No root to resolve a graph against — degrade to an empty contract
|
|
1288
|
+
# rather than reintroduce a regex parser.
|
|
1289
|
+
contract = FileContract(path=rel_path, language="java", extraction_method="graph")
|
|
1295
1290
|
else:
|
|
1296
1291
|
if self._ensure_ts():
|
|
1297
1292
|
lang_obj = _get_ts_lang(language)
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Outgoing call surface — the first consumer built on the Semantic IR P-A
|
|
2
|
+
primitives (ADR-0001 §6, P5.1 proof point).
|
|
3
|
+
|
|
4
|
+
An *outgoing call surface* is, for one method/constructor body, the complete set
|
|
5
|
+
of things it calls and instantiates — in source order, with receiver, argument
|
|
6
|
+
count, and resolution status. Computing it naively requires parsing method
|
|
7
|
+
bodies; here it is a pure query over the Body-Reference atoms
|
|
8
|
+
(`ContextGraph.invocations_in` / `instantiations_in`). This module reads **no
|
|
9
|
+
source** (INV-1): its only input is a built `ContextGraph`.
|
|
10
|
+
|
|
11
|
+
Why this is not already derivable from the L1 `calls` edge set: L1 edges are
|
|
12
|
+
dedup, class/method-level, and record only *resolved* links — an external call
|
|
13
|
+
like `repo.save(o)` (where `repo` is an injected field of a type outside the
|
|
14
|
+
module) produces **no** `calls` edge, yet it is part of the method's real call
|
|
15
|
+
surface. The P-A atoms retain exactly that discarded knowledge: unresolved
|
|
16
|
+
external calls, call ordering, argument counts, and duplicate call sites. This
|
|
17
|
+
module surfaces it.
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
from typing import TYPE_CHECKING, Optional
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING: # avoid import cycle; only a type hint
|
|
25
|
+
from sourcecode.context_graph import ContextGraph
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class OutgoingCall:
|
|
30
|
+
"""One method-call occurrence leaving a body. `resolved` is True when the IR
|
|
31
|
+
could bind the callee to a concrete symbol (`resolved_fqn` set); False for
|
|
32
|
+
honest-unresolved external / overloaded / dynamic calls."""
|
|
33
|
+
|
|
34
|
+
callee: str
|
|
35
|
+
receiver: Optional[str]
|
|
36
|
+
arg_count: int
|
|
37
|
+
resolved_fqn: Optional[str]
|
|
38
|
+
occurrence: int
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def resolved(self) -> bool:
|
|
42
|
+
return self.resolved_fqn is not None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class OutgoingInstantiation:
|
|
47
|
+
"""One `new Type(...)` occurrence leaving a body."""
|
|
48
|
+
|
|
49
|
+
type: str
|
|
50
|
+
arg_count: int
|
|
51
|
+
resolved_fqn: Optional[str]
|
|
52
|
+
occurrence: int
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def resolved(self) -> bool:
|
|
56
|
+
return self.resolved_fqn is not None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(frozen=True)
|
|
60
|
+
class CallSurface:
|
|
61
|
+
"""The full outgoing surface of one owner (method/constructor), ordered by
|
|
62
|
+
source occurrence within each stream."""
|
|
63
|
+
|
|
64
|
+
owner_fqn: str
|
|
65
|
+
calls: tuple[OutgoingCall, ...]
|
|
66
|
+
instantiations: tuple[OutgoingInstantiation, ...]
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def resolved_calls(self) -> tuple[OutgoingCall, ...]:
|
|
70
|
+
return tuple(c for c in self.calls if c.resolved)
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def unresolved_calls(self) -> tuple[OutgoingCall, ...]:
|
|
74
|
+
"""External / overloaded / dynamic calls the IR does not bind — the
|
|
75
|
+
blind spot the L1 `calls` edge set drops entirely."""
|
|
76
|
+
return tuple(c for c in self.calls if not c.resolved)
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def is_empty(self) -> bool:
|
|
80
|
+
return not self.calls and not self.instantiations
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def outgoing_call_surface(graph: "ContextGraph", owner_fqn: str) -> CallSurface:
|
|
84
|
+
"""Outgoing call surface for a single method/constructor owner. Returns an
|
|
85
|
+
empty (non-None) surface when the owner has no body atoms."""
|
|
86
|
+
calls = tuple(
|
|
87
|
+
OutgoingCall(
|
|
88
|
+
callee=i.callee, receiver=i.receiver, arg_count=i.arg_count,
|
|
89
|
+
resolved_fqn=i.resolved_fqn, occurrence=i.occurrence,
|
|
90
|
+
)
|
|
91
|
+
for i in graph.invocations_in(owner_fqn)
|
|
92
|
+
)
|
|
93
|
+
insts = tuple(
|
|
94
|
+
OutgoingInstantiation(
|
|
95
|
+
type=n.type, arg_count=n.arg_count,
|
|
96
|
+
resolved_fqn=n.resolved_fqn, occurrence=n.occurrence,
|
|
97
|
+
)
|
|
98
|
+
for n in graph.instantiations_in(owner_fqn)
|
|
99
|
+
)
|
|
100
|
+
return CallSurface(owner_fqn=owner_fqn, calls=calls, instantiations=insts)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def call_surfaces(graph: "ContextGraph") -> dict[str, CallSurface]:
|
|
104
|
+
"""Outgoing call surface for every method/constructor with body atoms, keyed
|
|
105
|
+
by owner FQN and sorted for determinism. Owners with an empty surface are
|
|
106
|
+
omitted."""
|
|
107
|
+
out: dict[str, CallSurface] = {}
|
|
108
|
+
for s in graph.symbols(kind="method") + graph.symbols(kind="constructor"):
|
|
109
|
+
surface = outgoing_call_surface(graph, s.fqn)
|
|
110
|
+
if not surface.is_empty:
|
|
111
|
+
out[s.fqn] = surface
|
|
112
|
+
return {k: out[k] for k in sorted(out)}
|