* Java/Kotlin: infer a receiver's declared type by walking field declarations * in the class enclosing the call site. The field's `signature` is already in * the form " " (set by tree-sitter.ts extractField), so we * pull the type from there. Handles Spring `@Resource UserBO
( receiverName: string, ref: UnresolvedRef, context: ResolutionContext, )
| 1174 | * null when no matching field is in the enclosing class. |
| 1175 | */ |
| 1176 | function inferJavaFieldReceiverType( |
| 1177 | receiverName: string, |
| 1178 | ref: UnresolvedRef, |
| 1179 | context: ResolutionContext, |
| 1180 | ): string | null { |
| 1181 | const inFile = context.getNodesInFile(ref.filePath); |
| 1182 | if (inFile.length === 0) return null; |
| 1183 | |
| 1184 | // Find the class enclosing the call line (tightest match by latest start). |
| 1185 | let enclosing: Node | null = null; |
| 1186 | for (const n of inFile) { |
| 1187 | if (n.kind !== 'class' && n.kind !== 'interface') continue; |
| 1188 | if (n.language !== ref.language) continue; |
| 1189 | const end = n.endLine ?? n.startLine; |
| 1190 | if (n.startLine <= ref.line && end >= ref.line) { |
| 1191 | if (!enclosing || n.startLine >= enclosing.startLine) enclosing = n; |
| 1192 | } |
| 1193 | } |
| 1194 | if (!enclosing) return null; |
| 1195 | |
| 1196 | const enclosingEnd = enclosing.endLine ?? enclosing.startLine; |
| 1197 | const field = inFile.find( |
| 1198 | (n) => |
| 1199 | n.kind === 'field' && |
| 1200 | n.name === receiverName && |
| 1201 | n.language === ref.language && |
| 1202 | n.startLine >= enclosing.startLine && |
| 1203 | (n.endLine ?? n.startLine) <= enclosingEnd, |
| 1204 | ); |
| 1205 | if (!field || !field.signature) return null; |
| 1206 | |
| 1207 | // Signature shape: "<TypeName> <fieldName>" (extractField). Pull the type, |
| 1208 | // strip generics + dotted package, drop array/varargs markers. |
| 1209 | const beforeName = field.signature.slice( |
| 1210 | 0, |
| 1211 | field.signature.lastIndexOf(field.name), |
| 1212 | ); |
| 1213 | const typeRaw = beforeName.trim(); |
| 1214 | if (!typeRaw) return null; |
| 1215 | |
| 1216 | const typeNoGenerics = typeRaw.replace(/<[^>]*>/g, '').trim(); |
| 1217 | const typeNoArray = typeNoGenerics.replace(/\[\s*\]/g, '').replace(/\.\.\.$/, '').trim(); |
| 1218 | const parts = typeNoArray.split(/[.\s]+/).filter(Boolean); |
| 1219 | const lastPart = parts[parts.length - 1]; |
| 1220 | if (!lastPart) return null; |
| 1221 | if (!/^[A-Z]/.test(lastPart)) return null; // primitives / lowercase → skip |
| 1222 | return lastPart; |
| 1223 | } |
| 1224 | |
| 1225 | // ── Local-variable receiver-type inference (#1108) ────────────────────────── |
| 1226 | // |
no test coverage detected