(ref: UnresolvedRef, context: ResolutionContext)
| 58 | }, |
| 59 | |
| 60 | resolve(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null { |
| 61 | // Spring config-key references — `@Value("${key}")` (single leaf) and |
| 62 | // `@ConfigurationProperties(prefix="X")` (entire subtree, marked with the |
| 63 | // `:prefix` suffix in extractSpringValueBindings). Lookup goes through |
| 64 | // Spring's relaxed binding (kebab/camel/snake → canonical lowercase). |
| 65 | if (ref.referenceName.endsWith(':prefix')) { |
| 66 | const prefix = ref.referenceName.slice(0, -':prefix'.length); |
| 67 | const canonPrefix = canonicalConfigKey(prefix); |
| 68 | // Prefer an exact prefix match (one node = the prefix subtree). Without |
| 69 | // node-level subtree representation we map to the closest matching key. |
| 70 | const candidates = context.getNodesByKind('constant').filter( |
| 71 | (n) => (n.language === 'yaml' || n.language === 'properties') |
| 72 | && canonicalConfigKey(n.qualifiedName).startsWith(canonPrefix), |
| 73 | ); |
| 74 | if (candidates.length === 0) return null; |
| 75 | // Pick the SHORTEST canonical name — it's the closest binding point |
| 76 | // (`app.cache` over `app.cache.name.user-token` for prefix=`app.cache`). |
| 77 | const best = candidates.reduce((a, b) => |
| 78 | canonicalConfigKey(a.qualifiedName).length <= canonicalConfigKey(b.qualifiedName).length ? a : b, |
| 79 | ); |
| 80 | return { original: ref, targetNodeId: best.id, confidence: 0.85, resolvedBy: 'framework' }; |
| 81 | } |
| 82 | if (ref.referenceName.includes('.') && ref.language !== 'java' && ref.language !== 'kotlin') { |
| 83 | // Spring config dotted key — only when the source language is Java/Kotlin |
| 84 | // (the bindings come from `@Value`). Skip non-Spring refs that happen to |
| 85 | // have dots in them. |
| 86 | } |
| 87 | // Spring config-key resolution: `@Value("${a.b.c}")` and |
| 88 | // `@ConfigurationProperties`. Gate on the `references` kind — those bindings |
| 89 | // are emitted as `references` by extractSpringValueBindings, whereas the far |
| 90 | // more numerous method-call refs (`list.add()`, `builder.build()`, every |
| 91 | // `receiver.method()`) are `calls`. A config key is NEVER a `calls` ref, so |
| 92 | // this loses no resolution. Without the gate, every dotted `calls` ref fell |
| 93 | // into the uncached getNodesByKind('constant') scan below — an |
| 94 | // O(dotted-calls × constant-nodes) cost that dominated resolution and made a |
| 95 | // full index take ~1h on large Java/Kotlin (Spring) monorepos (#1180). The |
| 96 | // old `split('.').length >= 2` heuristic couldn't separate keys from calls; |
| 97 | // the kind check does it exactly. |
| 98 | if ( |
| 99 | ref.referenceKind === 'references' && |
| 100 | (ref.language === 'java' || ref.language === 'kotlin') && |
| 101 | ref.referenceName.includes('.') && |
| 102 | !ref.referenceName.includes('::') |
| 103 | ) { |
| 104 | const canonRef = canonicalConfigKey(ref.referenceName); |
| 105 | const candidates = context.getNodesByKind('constant').filter( |
| 106 | (n) => n.kind === 'constant' |
| 107 | && (n.language === 'yaml' || n.language === 'properties') |
| 108 | && canonicalConfigKey(n.qualifiedName) === canonRef, |
| 109 | ); |
| 110 | if (candidates.length === 1) { |
| 111 | return { original: ref, targetNodeId: candidates[0]!.id, confidence: 0.9, resolvedBy: 'framework' }; |
| 112 | } |
| 113 | if (candidates.length > 1) { |
| 114 | // Multiple profile-specific files (application-dev.yml + |
| 115 | // application-prod.yml) can define the same key. Prefer the one with |
| 116 | // the shortest profile suffix (the base `application.yml` wins over |
| 117 | // profile variants when both exist), then by alphabetical path so the |
nothing calls this directly
no test coverage detected