@@ -129,27 +129,36 @@ private static void projectCompilationUnit(RowBuilder b, String fileKey, NodeRef
129129 projectTypeBody (b , fileKey , fqn , typeRef , type );
130130 }
131131
132- // Imports: resolve to a known Type (gated) or to a Package node .
132+ // Imports: resolve single-type imports to a JType (project or external), wildcards to a Package.
133133 if (cu .getImports () != null ) {
134134 for (Import im : cu .getImports ()) {
135- projectImport (b , cuRef , im , typeKeys );
135+ projectImport (b , cuRef , im );
136136 }
137137 }
138138
139139 projectComments (b , cuRef , cu .getComments (), fileKey );
140140 }
141141
142- private static void projectImport (RowBuilder b , NodeRef cuRef , Import im , Set < String > typeKeys ) {
142+ private static void projectImport (RowBuilder b , NodeRef cuRef , Import im ) {
143143 String path = im .getPath ();
144144 if (path == null || path .isEmpty ()) {
145145 return ;
146146 }
147- Map <String , Object > props = map ("is_static" , im .isStatic (), "is_wildcard" , im .isWildcard ());
148- if (!im .isWildcard () && typeKeys .contains (path )) {
149- b .edgeToSymbol ("J_IMPORTS" , cuRef , path , props );
147+ // The full import path always rides on the edge so it round-trips regardless of target kind.
148+ Map <String , Object > props = map ("path" , path , "is_static" , im .isStatic (), "is_wildcard" , im .isWildcard ());
149+
150+ // A single-type (non-wildcard, non-static) import names a type: link to that JType so the full
151+ // type name round-trips and multiple imports from the same package stay distinct nodes (a
152+ // package node would collapse them — issue #157). The type may already be a project JType, or
153+ // it may be library/external — in which case materialize a bodyless JType keyed by its FQN
154+ // (re-seeing the same id merges, so a real declaration later fills in the remaining props).
155+ if (!im .isWildcard () && !im .isStatic ()) {
156+ NodeRef typeRef = b .node (symbolLabels ("JType" , false ), "id" , path ,
157+ map ("id" , path , "name" , simpleName (path ), "fqn" , path ));
158+ b .edge ("J_IMPORTS" , cuRef , typeRef , props );
150159 return ;
151160 }
152- // Otherwise model the imported package: the path's package portion (strip the trailing class) .
161+ // Wildcard (java.util.*) or static-member import: model the package portion of the path .
153162 String pkg = im .isWildcard () ? path : packageOf (path );
154163 if (pkg != null && !pkg .isEmpty ()) {
155164 NodeRef pkgRef = b .node (Collections .singletonList ("JPackage" ), "name" , pkg , map ("name" , pkg ));
@@ -177,8 +186,11 @@ private static void projectTypeBody(RowBuilder b, String fileKey, String fqn, No
177186 projectCallable (b , fileKey , fqn , typeRef , ce .getValue (), ce .getKey ());
178187 }
179188 }
180- for (Field f : safe (type .getFieldDeclarations ())) {
181- projectField (b , fileKey , fqn , typeRef , f );
189+ List <Field > fields = type .getFieldDeclarations ();
190+ if (fields != null ) {
191+ for (int i = 0 ; i < fields .size (); i ++) {
192+ projectField (b , fileKey , fqn , typeRef , fields .get (i ), i );
193+ }
182194 }
183195 for (EnumConstant ec : safe (type .getEnumConstants ())) {
184196 projectEnumConstant (b , fileKey , fqn , typeRef , ec );
@@ -281,8 +293,14 @@ private static void projectVariable(RowBuilder b, String fileKey, String callabl
281293 projectComment (b , ref , v .getComment (), fileKey );
282294 }
283295
284- private static void projectField (RowBuilder b , String fileKey , String ownerFqn , NodeRef owner , Field f ) {
285- String id = ownerFqn + "#field#" + f .getName ();
296+ private static void projectField (RowBuilder b , String fileKey , String ownerFqn , NodeRef owner , Field f , int index ) {
297+ // A Java field declaration is keyed by its `variables` list, not a single `name` (which the IR
298+ // leaves null for multi-declarator fields). Key the node id by the joined variable names so each
299+ // declaration is a distinct node; fall back to a positional index if no variables are present.
300+ String key = (f .getVariables () != null && !f .getVariables ().isEmpty ())
301+ ? String .join ("+" , f .getVariables ())
302+ : String .valueOf (index );
303+ String id = ownerFqn + "#field#" + key ;
286304 NodeRef ref = b .node (Collections .singletonList ("JField" ), "id" , id , RowBuilder .prune (
287305 map ("id" , id , "name" , f .getName (), "type" , f .getType (),
288306 "modifiers" , strList (f .getModifiers ()), "annotations" , strList (f .getAnnotations ()),
@@ -432,7 +450,15 @@ private static String vertexId(JsonObject vertex) {
432450 return null ;
433451 }
434452 String typeDecl = str (vertex , "type_declaration" );
435- String signature = str (vertex , "signature" );
453+ // Key off `callable_declaration`, not `signature`: the call-graph `signature` rewrites
454+ // <init>/<clinit> to the simple class name for readability (e.g. `Foo()` instead of
455+ // `<init>()`), which never matches the JCallable node id — keyed by the symbol-table signature
456+ // (`<init>(...)`). `callable_declaration` carries that raw signature verbatim, so constructor
457+ // call edges resolve to their nodes instead of being gated out (issue #158).
458+ String signature = str (vertex , "callable_declaration" );
459+ if (signature == null ) {
460+ signature = str (vertex , "signature" );
461+ }
436462 if (typeDecl == null || signature == null ) {
437463 return null ;
438464 }
0 commit comments