( ref: UnresolvedRef, context: ResolutionContext )
| 206 | * edge is worse than none. |
| 207 | */ |
| 208 | export function matchFunctionRef( |
| 209 | ref: UnresolvedRef, |
| 210 | context: ResolutionContext |
| 211 | ): ResolvedRef | null { |
| 212 | // `this.<member>` refs are resolved ONLY by the class-scoped resolver in |
| 213 | // resolveOne (resolveThisMemberFnRef) — never by name matching here. |
| 214 | if (ref.referenceName.startsWith('this.')) return null; |
| 215 | |
| 216 | // In JS/TS/Python a bare identifier can never be a method value (methods |
| 217 | // are only reachable through a receiver — `this.m` / `self.m` / |
| 218 | // `Cls.m`), so bare fn-refs match FUNCTIONS only. This also sidesteps the |
| 219 | // pre-existing TS quirk of class fields extracting as method-kind nodes, |
| 220 | // which otherwise soaked up local names passed as arguments (excalidraw |
| 221 | // A/B finding; same pattern in vendored docopt.py). Python's `self.m` |
| 222 | // form keeps method targets via its own capture shape. C++ likewise: a |
| 223 | // bare identifier can only be a FREE function (member values need |
| 224 | // `&Cls::method`). PHP string callables name global FUNCTIONS (methods |
| 225 | // need the `[$obj, 'm']` array form, which carries its own shape). Other |
| 226 | // languages keep method targets: C# method groups, Swift/Dart |
| 227 | // implicit-self, Java/Kotlin method references. |
| 228 | const bareFnOnly = |
| 229 | ref.language === 'typescript' || ref.language === 'tsx' || |
| 230 | ref.language === 'javascript' || ref.language === 'jsx' || |
| 231 | ref.language === 'arkts' || |
| 232 | ref.language === 'cpp' || ref.language === 'python' || |
| 233 | ref.language === 'php'; |
| 234 | |
| 235 | // Qualified member-pointer (`&Widget::on_click` → "Widget::on_click"): |
| 236 | // resolve the member ON THAT SCOPE — exempt from bareFnOnly (the `&Cls::m` |
| 237 | // shape is an explicit member reference). Unique-or-drop like everything else. |
| 238 | if (ref.referenceName.includes('::')) { |
| 239 | const memberName = ref.referenceName.slice(ref.referenceName.lastIndexOf('::') + 2); |
| 240 | const scoped = context |
| 241 | .getNodesByName(memberName) |
| 242 | .filter( |
| 243 | (n) => |
| 244 | (n.kind === 'function' || n.kind === 'method') && |
| 245 | sameLanguageFamily(n.language, ref.language) && |
| 246 | n.id !== ref.fromNodeId && |
| 247 | (n.qualifiedName === ref.referenceName || |
| 248 | n.qualifiedName.endsWith(`::${ref.referenceName}`)) |
| 249 | ); |
| 250 | if (scoped.length === 0) return null; |
| 251 | const sameFileScoped = scoped.filter((n) => n.filePath === ref.filePath); |
| 252 | const pool = sameFileScoped.length > 0 ? sameFileScoped : scoped; |
| 253 | if (sameFileScoped.length === 0 && scoped.length > 1) return null; |
| 254 | const target = pool.reduce((a, b) => (a.startLine <= b.startLine ? a : b)); |
| 255 | return { |
| 256 | original: ref, |
| 257 | targetNodeId: target.id, |
| 258 | confidence: 0.9, |
| 259 | resolvedBy: 'function-ref', |
| 260 | }; |
| 261 | } |
| 262 | |
| 263 | let candidates = context |
| 264 | .getNodesByName(ref.referenceName) |
| 265 | .filter( |
no test coverage detected