|
6 | 6 | import java.util.ArrayList; |
7 | 7 | import java.util.Collection; |
8 | 8 | import java.util.Collections; |
| 9 | +import java.util.HashMap; |
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; |
@@ -120,6 +122,29 @@ else if (end instanceof Unmergeable) { |
120 | 122 | "will resolve end against the original source with parent pushed"); |
121 | 123 |
|
122 | 124 | sourceForEnd = source.pushParent(replaceable); |
| 125 | + |
| 126 | + // Per the HOCON spec, a substitution hidden by a value that |
| 127 | + // cannot be merged with it is never evaluated. When merging |
| 128 | + // objects field-by-field the merged object may already contain, |
| 129 | + // at some keys, values that ignore fallbacks (e.g. a resolved |
| 130 | + // non-object shadowing a substitution below). Any substitution |
| 131 | + // at those same keys in a lower-priority object on the stack |
| 132 | + // would be discarded by the subsequent merge anyway, so we |
| 133 | + // prune those keys before resolving to avoid evaluating |
| 134 | + // substitutions that cannot contribute to the result. |
| 135 | + if (merged instanceof AbstractConfigObject && end instanceof AbstractConfigObject |
| 136 | + && !(end instanceof Unmergeable)) { |
| 137 | + AbstractConfigObject prunedEnd = pruneShadowedKeys( |
| 138 | + (AbstractConfigObject) end, (AbstractConfigObject) merged); |
| 139 | + if (prunedEnd == null) { |
| 140 | + if (ConfigImpl.traceSubstitutionsEnabled()) |
| 141 | + ConfigImpl.trace(newContext.depth(), |
| 142 | + "all keys in end are shadowed by merged, skipping"); |
| 143 | + count += 1; |
| 144 | + continue; |
| 145 | + } |
| 146 | + end = prunedEnd; |
| 147 | + } |
123 | 148 | } |
124 | 149 |
|
125 | 150 | if (ConfigImpl.traceSubstitutionsEnabled()) { |
@@ -152,6 +177,38 @@ else if (end instanceof Unmergeable) { |
152 | 177 | return ResolveResult.make(newContext, merged); |
153 | 178 | } |
154 | 179 |
|
| 180 | + // Returns a copy of 'end' with keys removed when 'merged' already has a |
| 181 | + // value at that key which ignores fallbacks. Returns null if every key in |
| 182 | + // 'end' is shadowed. Returns 'end' unchanged if no pruning applies or if |
| 183 | + // we cannot safely inspect merged (e.g. unresolved CDMO). |
| 184 | + private static AbstractConfigObject pruneShadowedKeys(AbstractConfigObject end, |
| 185 | + AbstractConfigObject merged) { |
| 186 | + if (!(end instanceof SimpleConfigObject)) |
| 187 | + return end; |
| 188 | + SimpleConfigObject simple = (SimpleConfigObject) end; |
| 189 | + Map<String, AbstractConfigValue> kept = new HashMap<String, AbstractConfigValue>(); |
| 190 | + boolean pruned = false; |
| 191 | + for (String key : simple.keySet()) { |
| 192 | + AbstractConfigValue mergedValue; |
| 193 | + try { |
| 194 | + mergedValue = merged.attemptPeekWithPartialResolve(key); |
| 195 | + } catch (ConfigException.NotResolved e) { |
| 196 | + return end; |
| 197 | + } |
| 198 | + if (mergedValue != null && mergedValue.ignoresFallbacks()) { |
| 199 | + pruned = true; |
| 200 | + } else { |
| 201 | + kept.put(key, simple.attemptPeekWithPartialResolve(key)); |
| 202 | + } |
| 203 | + } |
| 204 | + if (!pruned) |
| 205 | + return end; |
| 206 | + if (kept.isEmpty()) |
| 207 | + return null; |
| 208 | + return new SimpleConfigObject(end.origin(), kept, |
| 209 | + ResolveStatus.fromValues(kept.values()), false); |
| 210 | + } |
| 211 | + |
155 | 212 | @Override |
156 | 213 | public AbstractConfigValue makeReplacement(ResolveContext context, int skipping) { |
157 | 214 | return ConfigDelayedMerge.makeReplacement(context, stack, skipping); |
|
0 commit comments