|
6 | 6 | import java.util.ArrayList; |
7 | 7 | import java.util.Collection; |
8 | 8 | import java.util.Collections; |
| 9 | +import java.util.LinkedHashMap; |
9 | 10 | import java.util.List; |
| 11 | +import java.util.Map; |
10 | 12 |
|
11 | 13 | import com.typesafe.config.ConfigException; |
12 | 14 | import com.typesafe.config.ConfigOrigin; |
@@ -82,6 +84,16 @@ static ResolveResult<? extends AbstractConfigValue> resolveSubstitutions(Replace |
82 | 84 | int count = 0; |
83 | 85 | AbstractConfigValue merged = null; |
84 | 86 | for (AbstractConfigValue end : stack) { |
| 87 | + // Per the HOCON spec, a substitution hidden by a value that |
| 88 | + // cannot be merged with it is never evaluated. If merged already |
| 89 | + // ignores fallbacks, nothing below can contribute, so stop. |
| 90 | + if (merged != null && merged.ignoresFallbacks()) { |
| 91 | + if (ConfigImpl.traceSubstitutionsEnabled()) |
| 92 | + ConfigImpl.trace(newContext.depth(), |
| 93 | + "merged ignores fallbacks, skipping remaining stack"); |
| 94 | + break; |
| 95 | + } |
| 96 | + |
85 | 97 | // the end value may or may not be resolved already |
86 | 98 |
|
87 | 99 | ResolveSource sourceForEnd; |
@@ -120,6 +132,23 @@ else if (end instanceof Unmergeable) { |
120 | 132 | "will resolve end against the original source with parent pushed"); |
121 | 133 |
|
122 | 134 | sourceForEnd = source.pushParent(replaceable); |
| 135 | + |
| 136 | + // Same spec rule as the short-circuit above, applied per-key: |
| 137 | + // keys in the lower-priority 'end' object that are already |
| 138 | + // shadowed in 'merged' by a value ignoring fallbacks would be |
| 139 | + // discarded by the subsequent merge, so don't resolve them. |
| 140 | + if (merged instanceof AbstractConfigObject && end instanceof AbstractConfigObject) { |
| 141 | + AbstractConfigObject prunedEnd = pruneShadowedKeys( |
| 142 | + (AbstractConfigObject) end, (AbstractConfigObject) merged); |
| 143 | + if (prunedEnd == null) { |
| 144 | + if (ConfigImpl.traceSubstitutionsEnabled()) |
| 145 | + ConfigImpl.trace(newContext.depth(), |
| 146 | + "all keys in end are shadowed by merged, skipping"); |
| 147 | + count += 1; |
| 148 | + continue; |
| 149 | + } |
| 150 | + end = prunedEnd; |
| 151 | + } |
123 | 152 | } |
124 | 153 |
|
125 | 154 | if (ConfigImpl.traceSubstitutionsEnabled()) { |
@@ -152,6 +181,38 @@ else if (end instanceof Unmergeable) { |
152 | 181 | return ResolveResult.make(newContext, merged); |
153 | 182 | } |
154 | 183 |
|
| 184 | + // Returns a copy of 'end' with keys removed when 'merged' already has a |
| 185 | + // value at that key which ignores fallbacks. Returns null if every key in |
| 186 | + // 'end' is shadowed. Returns 'end' unchanged if no pruning applies or if |
| 187 | + // we cannot safely inspect merged (e.g. unresolved CDMO). |
| 188 | + private static AbstractConfigObject pruneShadowedKeys(AbstractConfigObject end, |
| 189 | + AbstractConfigObject merged) { |
| 190 | + if (!(end instanceof SimpleConfigObject)) |
| 191 | + return end; |
| 192 | + SimpleConfigObject simple = (SimpleConfigObject) end; |
| 193 | + Map<String, AbstractConfigValue> kept = new LinkedHashMap<String, AbstractConfigValue>(); |
| 194 | + boolean pruned = false; |
| 195 | + for (String key : simple.keySet()) { |
| 196 | + AbstractConfigValue mergedValue; |
| 197 | + try { |
| 198 | + mergedValue = merged.attemptPeekWithPartialResolve(key); |
| 199 | + } catch (ConfigException.NotResolved e) { |
| 200 | + return end; |
| 201 | + } |
| 202 | + if (mergedValue != null && mergedValue.ignoresFallbacks()) { |
| 203 | + pruned = true; |
| 204 | + } else { |
| 205 | + kept.put(key, simple.attemptPeekWithPartialResolve(key)); |
| 206 | + } |
| 207 | + } |
| 208 | + if (!pruned) |
| 209 | + return end; |
| 210 | + if (kept.isEmpty()) |
| 211 | + return null; |
| 212 | + return new SimpleConfigObject(end.origin(), kept, |
| 213 | + ResolveStatus.fromValues(kept.values()), false); |
| 214 | + } |
| 215 | + |
155 | 216 | @Override |
156 | 217 | public AbstractConfigValue makeReplacement(ResolveContext context, int skipping) { |
157 | 218 | return ConfigDelayedMerge.makeReplacement(context, stack, skipping); |
|
0 commit comments