* Infer a receiver's type from its local declaration/initializer in the * enclosing function body. Language-dispatched; returns null for languages * without patterns or when no declaration is found. Bounded to the enclosing * scope so a same-named variable in another function can't leak in.
( receiverName: string, ref: UnresolvedRef, context: ResolutionContext, )
| 1370 | * scope so a same-named variable in another function can't leak in. |
| 1371 | */ |
| 1372 | function inferLocalReceiverType( |
| 1373 | receiverName: string, |
| 1374 | ref: UnresolvedRef, |
| 1375 | context: ResolutionContext, |
| 1376 | ): string | null { |
| 1377 | // CFML scope prefixes: `variables.svc` / `this.svc` name a COMPONENT-scoped |
| 1378 | // field whose assignment or `property` declaration usually lives outside the |
| 1379 | // calling function (the init-pseudoconstructor / WireBox-injection pattern), |
| 1380 | // and `local.svc` is an explicit function-local. Strip the prefix so the |
| 1381 | // declaration patterns match (`variables.svc = new X()`, `property |
| 1382 | // name="svc" …`, `var svc = …` all bind the bare name), and widen the scan |
| 1383 | // to the whole file for the component-scoped forms — nearest-declaration- |
| 1384 | // backward still wins, so a function-local shadowing the field is preferred. |
| 1385 | let scanReceiver = receiverName; |
| 1386 | let componentScoped = false; |
| 1387 | if (ref.language === 'cfml' || ref.language === 'cfscript') { |
| 1388 | const scoped = receiverName.match(/^(variables|this|local|arguments)\.(.+)$/i); |
| 1389 | if (scoped) { |
| 1390 | scanReceiver = scoped[2]!; |
| 1391 | const scope = scoped[1]!.toLowerCase(); |
| 1392 | componentScoped = scope === 'variables' || scope === 'this'; |
| 1393 | } |
| 1394 | } |
| 1395 | // PHP `$this->prop` receiver — the property's declaration lives outside the |
| 1396 | // calling method (a promoted constructor parameter `private readonly Foo $prop`, |
| 1397 | // a typed property `private Foo $prop;`, or a classic constructor parameter |
| 1398 | // `Foo $prop` assigned in __construct). Strip the prefix and widen the scan to |
| 1399 | // the whole file (the constructor may sit below the calling method), but — |
| 1400 | // unlike CFML's scopes above — switch to PROPERTY-shaped patterns: a plain |
| 1401 | // `$prop` local or parameter lives in a different namespace than `$this->prop` |
| 1402 | // and can never shadow it, so the generic local patterns would type the |
| 1403 | // property from unrelated same-named variables in other methods (a wrong |
| 1404 | // 0.9-confidence edge, not a missing one). |
| 1405 | let phpProperty = false; |
| 1406 | if (ref.language === 'php') { |
| 1407 | const scoped = receiverName.match(/^this->(.+)$/); |
| 1408 | if (scoped) { |
| 1409 | scanReceiver = scoped[1]!; |
| 1410 | componentScoped = true; |
| 1411 | phpProperty = true; |
| 1412 | } |
| 1413 | } |
| 1414 | |
| 1415 | const escapedReceiver = scanReceiver.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); |
| 1416 | const patterns = phpProperty |
| 1417 | ? phpPropertyTypePatterns(escapedReceiver) |
| 1418 | : localReceiverTypePatterns(ref.language, escapedReceiver); |
| 1419 | if (patterns.length === 0) return null; |
| 1420 | |
| 1421 | // Split through the context's per-file lines cache when available: this runs |
| 1422 | // for EVERY `receiver.method()` ref, and re-splitting the whole file per ref |
| 1423 | // was ~20% of total index CPU on Java-heavy repos (#1122). |
| 1424 | const lines = context.getFileLines |
| 1425 | ? context.getFileLines(ref.filePath) |
| 1426 | : (context.readFile(ref.filePath)?.split(/\r?\n/) ?? null); |
| 1427 | if (!lines || lines.length === 0) return null; |
| 1428 | |
| 1429 | const callIdx = Math.max(0, Math.min(lines.length - 1, ref.line - 1)); |
no test coverage detected