* Resolve `module.M: ` by locating the `module "M"` declaration in the * reference's own directory, reading its `source` attribute, and looking the * child symbol up inside that directory.
( ref: UnresolvedRef, moduleName: string, child: string, refDir: string, context: ResolutionContext )
| 127 | * child symbol up inside that directory. |
| 128 | */ |
| 129 | function resolveScopedModuleRef( |
| 130 | ref: UnresolvedRef, |
| 131 | moduleName: string, |
| 132 | child: string, |
| 133 | refDir: string, |
| 134 | context: ResolutionContext |
| 135 | ): ResolvedRef | null { |
| 136 | const decls = context |
| 137 | .getNodesByQualifiedName(`module.${moduleName}`) |
| 138 | .filter((n) => n.kind === 'module'); |
| 139 | if (decls.length === 0) return null; |
| 140 | // Terraform scoping: the declaration lives in the reference's directory. |
| 141 | const decl = decls.find((d) => dirOf(d.filePath) === refDir) ?? (decls.length === 1 ? decls[0]! : null); |
| 142 | if (!decl) return null; |
| 143 | |
| 144 | const source = readModuleAttr(decl, 'source', context); |
| 145 | if (!source) return null; |
| 146 | |
| 147 | // --- cloudposse/atmos remote-state: module.M.outputs.X where M is the |
| 148 | // stack-config remote-state module reading another COMPONENT's state. The |
| 149 | // component name is static in the monorepo case (`component = "vpc"` or |
| 150 | // "eks/cluster"), so bridge to that component directory's own |
| 151 | // `output "X"` — but only when every gate holds: the module source is the |
| 152 | // remote-state module, the component is a string literal, and exactly ONE |
| 153 | // directory in the repo matches the component name and declares that |
| 154 | // output. Anything dynamic or ambiguous stays a visible boundary. |
| 155 | if (child.startsWith('remote-output.')) { |
| 156 | if (!/\/remote-state(\/|$)/.test(source)) return null; |
| 157 | let component = readModuleAttr(decl, 'component', context); |
| 158 | if (!component) { |
| 159 | // The other half of real-world declarations indirect through a |
| 160 | // variable with a literal default in the same directory |
| 161 | // (`component = var.vpc_component_name` + `default = "vpc"`) — the |
| 162 | // component's declared static wiring. One hop, same literal gate. |
| 163 | const viaVar = readNodeSpanMatch(decl, /^\s*component\s*=\s*var\.([A-Za-z0-9_-]+)\s*$/, context); |
| 164 | if (viaVar) { |
| 165 | const declared = context |
| 166 | .getNodesByQualifiedName(`var.${viaVar}`) |
| 167 | .filter((n) => dirOf(n.filePath) === dirOf(decl.filePath)); |
| 168 | if (declared.length === 1) { |
| 169 | component = readNodeSpanMatch(declared[0]!, /^\s*default\s*=\s*"([^"]+)"/, context); |
| 170 | } |
| 171 | } |
| 172 | } |
| 173 | if (!component) return null; |
| 174 | const outName = child.slice('remote-output.'.length); |
| 175 | const outs = context |
| 176 | .getNodesByQualifiedName(`output.${outName}`) |
| 177 | .filter((o) => { |
| 178 | const d = dirOf(o.filePath); |
| 179 | return d === component || d.endsWith('/' + component); |
| 180 | }); |
| 181 | if (outs.length === 0) return null; |
| 182 | const dirs = new Set(outs.map((o) => dirOf(o.filePath))); |
| 183 | if (dirs.size > 1) return null; // two directories claim this component name — never guess |
| 184 | return { original: ref, targetNodeId: outs[0]!.id, confidence: 0.9, resolvedBy: 'framework' }; |
| 185 | } |
| 186 |
no test coverage detected