@@ -64,25 +64,78 @@ static inline const char *cbm_lsp_bare_segment(const char *name) {
6464 return seg ;
6565}
6666
67+ /* Tail helper: return the start of the final two dot-separated segments
68+ * ("Class.method") or NULL when the QN is too short. */
69+ static inline const char * cbm_pipeline_qn_class_method_tail (const char * qn ) {
70+ if (!qn ) {
71+ return NULL ;
72+ }
73+ const char * last = strrchr (qn , '.' );
74+ if (!last || last == qn ) {
75+ return NULL ;
76+ }
77+ const char * second = last ;
78+ while (second > qn ) {
79+ second -- ;
80+ if (* second == '.' ) {
81+ if (second == qn ) {
82+ return qn ;
83+ }
84+ return second + 1 ;
85+ }
86+ }
87+ return qn ;
88+ }
89+
90+ static inline const char * cbm_pipeline_call_callee_leaf (const char * callee_name ) {
91+ return cbm_lsp_bare_segment (callee_name );
92+ }
93+
94+ /* Gate for the unique-`Class.method`-tail fallbacks below. Tail-matching by
95+ * leaf is safe where class-per-file package semantics hold — the JVM
96+ * languages (Java/Kotlin): the declared `package` is ground truth, a class
97+ * name is unique within a package, and mixed Gradle/Maven source roots
98+ * (`src/main/java` + `src/main/kotlin`) legitimately produce path-derived
99+ * module QNs that disagree with the package-shaped QNs the LSP emits, so
100+ * the tail is the only reliable join key. In other languages the same-name
101+ * guarantee does not exist (Python/TS re-export shims, Go internal clones,
102+ * C++ template instantiations), and a single wrong-module coincidence
103+ * would fabricate a CALLS edge — so the fallbacks stay off there. */
104+ static inline bool cbm_pipeline_lsp_allow_tail_match (CBMLanguage lang ) {
105+ return lang == CBM_LANG_JAVA || lang == CBM_LANG_KOTLIN ;
106+ }
107+
108+ static inline int cbm_pipeline_qn_class_method_tail_eq (const char * qn , const char * tail ) {
109+ const char * qt = cbm_pipeline_qn_class_method_tail (qn );
110+ return qt && tail && strcmp (qt , tail ) == 0 ;
111+ }
112+
67113/* Look up the highest-confidence LSP-resolved call entry whose caller QN
68114 * matches the textual call's enclosing function and whose callee QN
69115 * short-name matches the textual callee. Returns a pointer into `arr`
70116 * or NULL if no qualifying entry exists.
71117 *
72- * Match rule: the LSP emits CBMResolvedCall entries whose caller_qn
73- * matches the call's enclosing function and whose callee_qn ends with
74- * the textual callee_name as the last dot-separated segment. The
75- * pointer returned aliases into `arr` and stays valid as long as the
118+ * Match rule:
119+ * 1. exact caller_qn + callee short-name match wins first;
120+ * 2. if no exact caller match exists AND allow_tail_match is set
121+ * (JVM callers only, see cbm_pipeline_lsp_allow_tail_match), a
122+ * unique Class.method tail match between rc->caller_qn and
123+ * call->enclosing_func_qn may win;
124+ * 3. ambiguous tails return NULL so the registry fallback stays in
125+ * control.
126+ *
127+ * The pointer returned aliases into `arr` and stays valid as long as the
76128 * underlying CBMFileResult is alive. */
77129static inline const CBMResolvedCall * cbm_pipeline_find_lsp_resolution (
78- const CBMResolvedCallArray * arr , const CBMCall * call ) {
130+ const CBMResolvedCallArray * arr , const CBMCall * call , bool allow_tail_match ) {
79131 if (!arr || arr -> count == 0 || !call ) {
80132 return NULL ;
81133 }
82134 if (!call -> enclosing_func_qn || !call -> callee_name ) {
83135 return NULL ;
84136 }
85- const CBMResolvedCall * best = NULL ;
137+
138+ const CBMResolvedCall * best_exact = NULL ;
86139 for (int i = 0 ; i < arr -> count ; i ++ ) {
87140 const CBMResolvedCall * rc = & arr -> items [i ];
88141 if (!rc -> caller_qn || !rc -> callee_qn ) {
@@ -124,53 +177,120 @@ static inline const CBMResolvedCall *cbm_pipeline_find_lsp_resolution(
124177 continue ;
125178 }
126179 }
127- if (!best || rc -> confidence > best -> confidence ) {
128- best = rc ;
180+ if (!best_exact || rc -> confidence > best_exact -> confidence ) {
181+ best_exact = rc ;
182+ }
183+ }
184+ if (best_exact ) {
185+ return best_exact ;
186+ }
187+ if (!allow_tail_match ) {
188+ return NULL ;
189+ }
190+
191+ const char * call_tail = cbm_pipeline_qn_class_method_tail (call -> enclosing_func_qn );
192+ if (!call_tail ) {
193+ return NULL ;
194+ }
195+
196+ const CBMResolvedCall * best_tail = NULL ;
197+ for (int i = 0 ; i < arr -> count ; i ++ ) {
198+ const CBMResolvedCall * rc = & arr -> items [i ];
199+ if (!rc -> caller_qn || !rc -> callee_qn ) {
200+ continue ;
201+ }
202+ if (rc -> confidence < CBM_LSP_CONFIDENCE_FLOOR ) {
203+ continue ;
129204 }
205+ const char * short_name = strrchr (rc -> callee_qn , '.' );
206+ short_name = short_name ? short_name + SKIP_ONE : rc -> callee_qn ;
207+ const char * call_leaf = cbm_pipeline_call_callee_leaf (call -> callee_name );
208+ if (!call_leaf || strcmp (short_name , call_leaf ) != 0 ) {
209+ continue ;
210+ }
211+ if (!cbm_pipeline_qn_class_method_tail_eq (rc -> caller_qn , call_tail )) {
212+ continue ;
213+ }
214+ if (best_tail ) {
215+ return NULL ;
216+ }
217+ best_tail = rc ;
130218 }
131- return best ;
219+ return best_tail ;
132220}
133221
134222/* Resolve an LSP-emitted callee_qn to a graph-buffer node.
135223 *
136- * Per-file LSPs (notably py_lsp) sometimes emit `callee_qn` as the raw
137- * import-module path the source code uses (e.g. `greeter.Greeter` from
138- * `from greeter import Greeter`) rather than the project-qualified QN
139- * the gbuf actually stores (`<project>.greeter.Greeter`). This is
140- * unavoidable at the per-file LSP layer: the LSP cannot tell in-project
141- * imports (qualify) from external imports (don't qualify, e.g. `os.path`)
142- * without consulting the gbuf, which is built downstream.
143- *
144- * The fallback rule: try the LSP-emitted QN as-is first; on miss, retry
145- * with `<project>.<callee_qn>`. If that also misses, the target is
146- * external/unknown and the caller drops the edge — same as today.
224+ * Per-file LSPs sometimes emit `callee_qn` as the raw package-shaped
225+ * import path the source code uses rather than the project-qualified QN
226+ * the gbuf actually stores. The fallback rule is:
227+ * 1. try the LSP-emitted QN as-is;
228+ * 2. retry with `<project>.<callee_qn>` when needed;
229+ * 3. if both fail AND allow_tail_match is set (JVM callers only, see
230+ * cbm_pipeline_lsp_allow_tail_match), use the exact node-name index
231+ * to narrow candidates by short method name and accept exactly one
232+ * Function/Method whose qualified_name has the same Class.method
233+ * tail.
147234 *
148235 * Returns the matching node, or NULL if neither lookup hits. */
149236static inline const cbm_gbuf_node_t * cbm_pipeline_lsp_target_node (const cbm_gbuf_t * gbuf ,
150237 const char * project_name ,
151- const char * callee_qn ) {
238+ const char * callee_qn ,
239+ bool allow_tail_match ) {
152240 if (!gbuf || !callee_qn ) {
153241 return NULL ;
154242 }
155243 const cbm_gbuf_node_t * direct = cbm_gbuf_find_by_qn (gbuf , callee_qn );
156244 if (direct ) {
157245 return direct ;
158246 }
159- if (!project_name || !project_name [0 ]) {
247+ if (project_name && project_name [0 ]) {
248+ size_t proj_len = strlen (project_name );
249+ if (!(strncmp (callee_qn , project_name , proj_len ) == 0 && callee_qn [proj_len ] == '.' )) {
250+ char buf [CBM_SZ_1K ];
251+ int written = snprintf (buf , sizeof (buf ), "%s.%s" , project_name , callee_qn );
252+ if (written > 0 && (size_t )written < sizeof (buf )) {
253+ const cbm_gbuf_node_t * prefixed = cbm_gbuf_find_by_qn (gbuf , buf );
254+ if (prefixed ) {
255+ return prefixed ;
256+ }
257+ }
258+ }
259+ }
260+ if (!allow_tail_match ) {
160261 return NULL ;
161262 }
162- /* Skip the prefix retry if callee_qn is already project-qualified —
163- * avoids producing nonsense like `proj.proj.foo.Bar`. */
164- size_t proj_len = strlen (project_name );
165- if (strncmp (callee_qn , project_name , proj_len ) == 0 && callee_qn [proj_len ] == '.' ) {
263+
264+ const char * short_name = strrchr (callee_qn , '.' );
265+ short_name = short_name ? short_name + SKIP_ONE : callee_qn ;
266+ const char * callee_tail = cbm_pipeline_qn_class_method_tail (callee_qn );
267+ if (!callee_tail ) {
166268 return NULL ;
167269 }
168- char buf [ CBM_SZ_1K ] ;
169- int written = snprintf ( buf , sizeof ( buf ), "%s.%s" , project_name , callee_qn ) ;
170- if (written < 0 || ( size_t ) written >= sizeof ( buf ) ) {
270+ const cbm_gbuf_node_t * * hits = NULL ;
271+ int hit_count = 0 ;
272+ if (cbm_gbuf_find_by_name ( gbuf , short_name , & hits , & hit_count ) != 0 || hit_count == 0 ) {
171273 return NULL ;
172274 }
173- return cbm_gbuf_find_by_qn (gbuf , buf );
275+
276+ const cbm_gbuf_node_t * match = NULL ;
277+ for (int i = 0 ; i < hit_count ; i ++ ) {
278+ const cbm_gbuf_node_t * cand = hits [i ];
279+ if (!cand || !cand -> label || !cand -> qualified_name ) {
280+ continue ;
281+ }
282+ if (strcmp (cand -> label , "Function" ) != 0 && strcmp (cand -> label , "Method" ) != 0 ) {
283+ continue ;
284+ }
285+ if (!cbm_pipeline_qn_class_method_tail_eq (cand -> qualified_name , callee_tail )) {
286+ continue ;
287+ }
288+ if (match ) {
289+ return NULL ;
290+ }
291+ match = cand ;
292+ }
293+ return match ;
174294}
175295
176296#endif /* CBM_PIPELINE_LSP_RESOLVE_H */
0 commit comments