(ref: UnresolvedRef, context: ResolutionContext)
| 56 | }, |
| 57 | |
| 58 | resolve(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null { |
| 59 | if (ref.language !== 'terraform') return null; |
| 60 | |
| 61 | const qname = ref.referenceName; |
| 62 | const refDir = dirOf(ref.filePath); |
| 63 | |
| 64 | // --- module-boundary bridge: module.M:file / module.M:var.X / module.M:output.X --- |
| 65 | const scoped = qname.match(/^module\.([^.:\s]+):(.+)$/); |
| 66 | if (scoped) { |
| 67 | return resolveScopedModuleRef(ref, scoped[1]!, scoped[2]!, refDir, context); |
| 68 | } |
| 69 | |
| 70 | const candidates = context.getNodesByQualifiedName(qname); |
| 71 | if (candidates.length === 0) return null; |
| 72 | |
| 73 | // 1. Same directory — the only scope Terraform can actually reference. |
| 74 | const sameDir = candidates.filter((c) => dirOf(c.filePath) === refDir); |
| 75 | if (sameDir.length > 0) { |
| 76 | return { |
| 77 | original: ref, |
| 78 | targetNodeId: sameDir[0]!.id, |
| 79 | confidence: 0.95, |
| 80 | resolvedBy: 'framework', |
| 81 | }; |
| 82 | } |
| 83 | |
| 84 | // 2. `.tfvars` assignments set ROOT module variables, and var-files are |
| 85 | // routinely kept in a subdirectory (`envs/prod.tfvars`). Walk up to |
| 86 | // the nearest ancestor directory that declares the variable. |
| 87 | if (ref.filePath.endsWith('.tfvars') && qname.startsWith('var.')) { |
| 88 | const up = nearestAncestorMatch(candidates, refDir); |
| 89 | if (up) { |
| 90 | return { original: ref, targetNodeId: up.id, confidence: 0.9, resolvedBy: 'framework' }; |
| 91 | } |
| 92 | } |
| 93 | |
| 94 | // 2b. Provider configurations are the one construct Terraform inherits |
| 95 | // across the module tree: they're declared in the root (or a parent) |
| 96 | // module and passed down, so `provider = aws.east` inside a child |
| 97 | // module legitimately names a configuration declared above it. |
| 98 | if (qname.startsWith('provider.')) { |
| 99 | const configs = candidates.filter((c) => c.kind === 'namespace'); |
| 100 | const up = nearestAncestorMatch(configs, refDir); |
| 101 | if (up) { |
| 102 | return { original: ref, targetNodeId: up.id, confidence: 0.9, resolvedBy: 'framework' }; |
| 103 | } |
| 104 | return null; |
| 105 | } |
| 106 | |
| 107 | // 3. No same-directory declaration → no edge. A candidate in another |
| 108 | // module directory is never the real target (cross-module access only |
| 109 | // exists through module.M inputs/outputs, bridged above), and a wrong |
| 110 | // edge is worse than none. |
| 111 | return null; |
| 112 | }, |
| 113 | }; |
| 114 | |
| 115 | /** Nearest candidate walking UP the directory tree from refDir (exclusive). */ |
nothing calls this directly
no test coverage detected