config: fix nested config resolution strings not getting resolved sometimes (#248724)

See https://github.com/microsoft/vscode/issues/245798#issuecomment-2871753176

Fixes #245798
This commit is contained in:
Connor Peet
2025-05-12 06:57:36 -07:00
committed by GitHub
2 changed files with 64 additions and 7 deletions

View File

@@ -65,7 +65,7 @@ interface IReplacementLocation {
export class ConfigurationResolverExpression<T> implements IConfigurationResolverExpression<T> {
public static readonly VARIABLE_LHS = '${';
private locations = new Map<string, IReplacementLocation>();
private readonly locations = new Map<string, IReplacementLocation>();
private root: T;
private stringRoot: boolean;
/**
@@ -173,17 +173,14 @@ export class ConfigurationResolverExpression<T> implements IConfigurationResolve
}
for (const [key, value] of Object.entries(obj)) {
this.parseString(obj, key, key, true); // parse key
if (typeof value === 'string') {
this.parseString(obj, key, value);
} else {
this.parseObject(value);
}
}
// only after all values are marked for replacement, we can collect keys that have to be replaced
for (const [key] of Object.entries(obj)) {
this.parseString(obj, key, key, true);
}
}
private parseString(object: any, propertyName: string | number, value: string, replaceKeyName?: boolean, replacementPath?: string[]): void {
@@ -281,15 +278,26 @@ export class ConfigurationResolverExpression<T> implements IConfigurationResolve
const newKey = propertyName.replaceAll(replacement.id, data.value);
delete object[propertyName];
object[newKey] = value;
this._renameKeyInLocations(object, propertyName, newKey);
this.parseString(object, newKey, data.value, true, path);
} else {
this.parseString(object, propertyName, data.value, false, path);
object[propertyName] = object[propertyName].replaceAll(replacement.id, data.value);
this.parseString(object, propertyName, data.value, false, path);
}
path.pop();
}
private _renameKeyInLocations(obj: object, oldKey: string, newKey: string) {
for (const location of this.locations.values()) {
for (const loc of location.locations) {
if (loc.object === obj && loc.propertyName === oldKey) {
loc.propertyName = newKey;
}
}
}
}
public toObject(): T {
// If we wrapped a string, unwrap it
if (this.stringRoot) {

View File

@@ -1010,4 +1010,53 @@ suite('ConfigurationResolverExpression', () => {
'key that is username: testuser': 'cool!'
});
});
test('resolves nested values 2 (#245798)', () => {
const expr = ConfigurationResolverExpression.parse({
env: {
SITE: "${input:site}",
TLD: "${input:tld}",
HOST: "${input:host}",
},
});
for (const r of expr.unresolved()) {
if (r.arg === 'site') {
expr.resolve(r, 'example');
} else if (r.arg === 'tld') {
expr.resolve(r, 'com');
} else if (r.arg === 'host') {
expr.resolve(r, 'local.${input:site}.${input:tld}');
}
}
assert.deepStrictEqual(expr.toObject(), {
env: {
SITE: 'example',
TLD: 'com',
HOST: 'local.example.com'
}
});
});
test('out-of-order key resolution (#248550)', () => {
const expr = ConfigurationResolverExpression.parse({
'${input:key}': "${input:value}",
});
for (const r of expr.unresolved()) {
if (r.arg === 'key') {
expr.resolve(r, 'the-key');
}
}
for (const r of expr.unresolved()) {
if (r.arg === 'value') {
expr.resolve(r, 'the-value');
}
}
assert.deepStrictEqual(expr.toObject(), {
'the-key': 'the-value'
});
});
});