Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 21 additions & 15 deletions src/vs/workbench/services/extensions/common/extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,11 @@ let _proposedApiEnabledResolver: ProposedApiEnabledResolver | undefined;
*/
export const enabledApiProposalsFallbackExperimentName = 'extensionEnabledApiProposalsFallback';

/**
* Experiment value that explicitly blocks all proposals reaching the fallback.
*/
export const enabledApiProposalsFallbackNone = 'none';

/**
* Resolves the value of the {@link enabledApiProposalsFallbackExperimentName}-experiment, or
* `undefined` when it does not apply (non-`stable` quality) or cannot be read in time.
Expand Down Expand Up @@ -424,31 +429,32 @@ export async function resolveEnabledApiProposalsFallbackExperiment(assignmentSer
*
* @param value A comma-separated list of `publisher.extension:proposalName` entries. Any combination
* that appears here will have {@link isProposedApiEnabled} return `true` even when the extension has
* not declared that particular proposal.
* not declared that particular proposal. When unset, all proposals are allowed;
* {@link enabledApiProposalsFallbackNone} blocks all proposals that reach the fallback.
* @param quality The product quality. The experiment only takes effect when this is `stable`.
*/
export function setEnabledApiProposalsFallbackExperiment(value: string | undefined, quality: string | undefined): IDisposable {
if (quality !== 'stable' || !value) {
if (quality !== 'stable') {
return Disposable.None;
}

const allowed = new Set<string>();
for (const entry of value.split(',')) {
const trimmed = entry.trim();
const idx = trimmed.indexOf(':');
if (idx <= 0 || idx === trimmed.length - 1) {
continue;
if (value !== undefined && value !== enabledApiProposalsFallbackNone) {
for (const entry of value.split(',')) {
const trimmed = entry.trim();
const idx = trimmed.indexOf(':');
Comment thread
alexr00 marked this conversation as resolved.
if (idx <= 0 || idx === trimmed.length - 1) {
continue;
}
const extensionId = ExtensionIdentifier.toKey(trimmed.slice(0, idx));
const proposal = trimmed.slice(idx + 1);
allowed.add(`${extensionId}:${proposal}`);
}
const extensionId = ExtensionIdentifier.toKey(trimmed.slice(0, idx));
const proposal = trimmed.slice(idx + 1);
allowed.add(`${extensionId}:${proposal}`);
}

if (allowed.size === 0) {
return Disposable.None;
}

const resolver: ProposedApiEnabledResolver = (extension, proposal) => allowed.has(`${ExtensionIdentifier.toKey(extension.identifier)}:${proposal}`);
const resolver: ProposedApiEnabledResolver = value === undefined
? () => true
: (extension, proposal) => allowed.has(`${ExtensionIdentifier.toKey(extension.identifier)}:${proposal}`);
Comment thread
alexr00 marked this conversation as resolved.
_proposedApiEnabledResolver = resolver;
return toDisposable(() => {
if (_proposedApiEnabledResolver === resolver) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { URI } from '../../../../../base/common/uri.js';
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
import { ExtensionIdentifier, IExtensionDescription, TargetPlatform } from '../../../../../platform/extensions/common/extensions.js';
import { ApiProposalName } from '../../../../../platform/extensions/common/extensionsApiProposals.js';
import { isProposedApiEnabled, setEnabledApiProposalsFallbackExperiment } from '../../common/extensions.js';
import { enabledApiProposalsFallbackNone, isProposedApiEnabled, setEnabledApiProposalsFallbackExperiment } from '../../common/extensions.js';

suite('isProposedApiEnabled (extensionEnabledApiProposalsFallback experiment)', () => {

Expand Down Expand Up @@ -63,6 +63,29 @@ suite('isProposedApiEnabled (extensionEnabledApiProposalsFallback experiment)',
assert.strictEqual(isProposedApiEnabled(noProposals, 'someProposal' as ApiProposalName), false);
});

test('missing experiment allows all proposals on stable by default', () => {
Comment thread
alexr00 marked this conversation as resolved.
const withProposals = desc('test.withProposals', ['unrelatedProposal']);
const withoutProposals = desc('test.withoutProposals', undefined);
store.add(setEnabledApiProposalsFallbackExperiment(undefined, 'stable'));

assert.deepStrictEqual(
{
withProposals: isProposedApiEnabled(withProposals, 'someProposal' as ApiProposalName),
withoutProposals: isProposedApiEnabled(withoutProposals, 'someProposal' as ApiProposalName),
},
{
withProposals: true,
withoutProposals: false,
}
);
});

test('none experiment blocks all proposals that reach the fallback', () => {
const missing = desc('test.missing', ['unrelatedProposal']);
store.add(setEnabledApiProposalsFallbackExperiment(enabledApiProposalsFallbackNone, 'stable'));
assert.strictEqual(isProposedApiEnabled(missing, 'someProposal' as ApiProposalName), false);
});

test('experiment has no effect on non-stable builds', () => {
const missing = desc('test.missing', ['unrelatedProposal']);
store.add(setEnabledApiProposalsFallbackExperiment('test.missing:someProposal', 'insider'));
Expand Down
Loading