Skip to content

Commit eb7f5eb

Browse files
NEW: @W-18538308@: Make ApexGuru integration GA by removing pilot flag... (#283)
1 parent a19b549 commit eb7f5eb

File tree

13 files changed

+1706
-135
lines changed

13 files changed

+1706
-135
lines changed

package-lock.json

Lines changed: 1561 additions & 40 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
"jest": "^30.0.5",
6161
"jest-mock-vscode": "^4.6.0",
6262
"mocha": "^10.8.2",
63+
"npm-run-all": "^4.1.5",
6364
"ovsx": "^0.10.5",
6465
"proxyquire": "^2.1.3",
6566
"rimraf": "*",
@@ -140,11 +141,6 @@
140141
"type": "boolean",
141142
"default": false,
142143
"description": "Scan files on open automatically."
143-
},
144-
"codeAnalyzer.apexGuru.enabled": {
145-
"type": "boolean",
146-
"default": false,
147-
"description": "(Pilot) Discover critical problems and performance issues in your Apex code with ApexGuru, which analyzes your Apex files for you. This feature is in a closed pilot; contact your account representative to learn more."
148144
}
149145
}
150146
},
@@ -188,7 +184,7 @@
188184
},
189185
{
190186
"command": "sfca.runApexGuruAnalysisOnCurrentFile",
191-
"when": "sfca.extensionActivated && sfca.apexGuruEnabled && resourceExtname =~ /\\.cls|\\.trigger|\\.apex/"
187+
"when": "sfca.extensionActivated && sfca.shouldShowApexGuruButtons && resourceExtname =~ /\\.cls|\\.trigger|\\.apex/"
192188
}
193189
],
194190
"editor/context": [
@@ -202,7 +198,7 @@
202198
},
203199
{
204200
"command": "sfca.runApexGuruAnalysisOnCurrentFile",
205-
"when": "sfca.extensionActivated && sfca.apexGuruEnabled && resourceExtname =~ /\\.cls|\\.trigger|\\.apex/"
201+
"when": "sfca.extensionActivated && sfca.shouldShowApexGuruButtons && resourceExtname =~ /\\.cls|\\.trigger|\\.apex/"
206202
}
207203
],
208204
"explorer/context": [
@@ -216,7 +212,7 @@
216212
},
217213
{
218214
"command": "sfca.runApexGuruAnalysisOnSelectedFile",
219-
"when": "sfca.extensionActivated && sfca.apexGuruEnabled && explorerResourceIsFolder == false && resourceExtname =~ /\\.cls|\\.trigger|\\.apex/"
215+
"when": "sfca.extensionActivated && sfca.shouldShowApexGuruButtons && explorerResourceIsFolder == false && resourceExtname =~ /\\.cls|\\.trigger|\\.apex/"
220216
}
221217
]
222218
},

src/extension.ts

Lines changed: 7 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import {PMDSupressionsCodeActionProvider} from './lib/pmd/pmd-suppressions-code-
3333
import {ApplyViolationFixesActionProvider} from './lib/apply-violation-fixes-action-provider';
3434
import {ApplyViolationFixesAction} from './lib/apply-violation-fixes-action';
3535
import {ViolationSuggestionsHoverProvider} from './lib/violation-suggestions-hover-provider';
36-
import {ApexGuruAccess, ApexGuruAvailability, ApexGuruService, LiveApexGuruService} from './lib/apexguru/apex-guru-service';
36+
import {ApexGuruAccess, ApexGuruService, LiveApexGuruService} from './lib/apexguru/apex-guru-service';
3737
import {ApexGuruRunAction} from './lib/apexguru/apex-guru-run-action';
3838
import {OrgConnectionService} from './lib/external-services/org-connection-service';
3939

@@ -246,18 +246,12 @@ export async function activate(context: vscode.ExtensionContext): Promise<SFCAEx
246246
// =================================================================================================================
247247
const apexGuruService: ApexGuruService = new LiveApexGuruService(orgConnectionService, fileHandler, logger);
248248
const apexGuruRunAction: ApexGuruRunAction = new ApexGuruRunAction(taskWithProgressRunner, apexGuruService, diagnosticManager, telemetryService, display);
249-
250-
// TODO: This is temporary and will change soon when we remove pilot flag and instead add a watch to org auth changes
251-
const isApexGuruEnabled: () => Promise<boolean> = async () => {
252-
if (!settingsManager.getApexGuruEnabled()) {
253-
return false;
254-
}
255-
const availability: ApexGuruAvailability = await apexGuruService.getAvailability();
256-
if (availability.access === ApexGuruAccess.ENABLED || availability.access === ApexGuruAccess.ELIGIBLE) {
257-
return true;
258-
}
259-
};
260-
await establishVariableInContext(Constants.CONTEXT_VAR_APEX_GURU_ENABLED, isApexGuruEnabled);
249+
apexGuruService.onAccessChange((access: ApexGuruAccess) => {
250+
logger.debug(`Access to ApexGuru has been set '${access}'.`);
251+
void vscode.commands.executeCommand('setContext', Constants.CONTEXT_VAR_SHOULD_SHOW_APEX_GURU_BUTTONS,
252+
access === ApexGuruAccess.ENABLED || access === ApexGuruAccess.ELIGIBLE);
253+
});
254+
void apexGuruService.updateAvailability(); // This asyncronously triggers the first AccessChanged Event to establish the context variable
261255

262256
// COMMAND_RUN_APEX_GURU_ON_FILE: Invokable by 'explorer/context' menu only when: "sfca.apexGuruEnabled && explorerResourceIsFolder == false && resourceExtname =~ /\\.cls|\\.trigger|\\.apex/"
263257
registerCommand(Constants.COMMAND_RUN_APEX_GURU_ON_FILE, async (selection: vscode.Uri, multiSelect?: vscode.Uri[]) => {
@@ -322,19 +316,6 @@ export function _isValidFileForAnalysis(documentUri: vscode.Uri): boolean {
322316
return allowedFileTypes.includes(path.extname(documentUri.fsPath));
323317
}
324318

325-
// TODO: This is only used by apex guru right now and is tied to the pilot setting. Soon we will be removing the pilot
326-
// setting and instead we should be adding a watch to the onOrgChange event of the OrgConnectionService instead.
327-
// Inside our package.json you'll see things like:
328-
// "when": "sfca.apexGuruEnabled"
329-
// which helps determine when certain commands and menus are available.
330-
// To make these "context variables" set and stay updated when settings change, use this helper function:
331-
async function establishVariableInContext(varUsedInPackageJson: string, getValueFcn: () => Promise<boolean>): Promise<void> {
332-
await vscode.commands.executeCommand('setContext', varUsedInPackageJson, await getValueFcn());
333-
vscode.workspace.onDidChangeConfiguration(async () => {
334-
await vscode.commands.executeCommand('setContext', varUsedInPackageJson, await getValueFcn());
335-
});
336-
}
337-
338319
async function getActiveDocument(): Promise<vscode.TextDocument | null> {
339320
// Note that the active editor window could be the output window instead of the actual file editor, so we
340321
// force focus it first to ensure we are getting the correct editor

src/lib/apexguru/apex-guru-run-action.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export class ApexGuruRunAction {
3333
const startTime: number = Date.now();
3434

3535
try {
36-
const availability: ApexGuruAvailability = await this.apexGuruService.getAvailability();
36+
const availability: ApexGuruAvailability = this.apexGuruService.getAvailability();
3737
if (availability.access !== ApexGuruAccess.ENABLED) {
3838
this.display.displayError(availability.message);
3939
this.telemetryService.sendCommandEvent(Constants.TELEM_APEX_GURU_FILE_ANALYSIS_NOT_ENABLED, {

src/lib/apexguru/apex-guru-service.ts

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@
88
import {CodeLocation, Fix, Suggestion, Violation} from '../diagnostics';
99
import {Logger} from "../logger";
1010
import {getErrorMessage, indent} from '../utils';
11-
import {HttpMethods, HttpRequest, OrgConnectionService} from '../external-services/org-connection-service';
11+
import {HttpMethods, HttpRequest, OrgConnectionService, OrgUserInfo} from '../external-services/org-connection-service';
1212
import {FileHandler} from '../fs-utils';
1313
import { messages } from '../messages';
14+
import { EventEmitter } from 'node:stream';
1415

1516
export const APEX_GURU_ENGINE_NAME: string = 'apexguru';
1617
const APEX_GURU_MAX_TIMEOUT_SECONDS = 60;
@@ -24,7 +25,9 @@ const RESPONSE_STATUS = {
2425
}
2526

2627
export interface ApexGuruService {
27-
getAvailability(): Promise<ApexGuruAvailability>;
28+
getAvailability(): ApexGuruAvailability;
29+
updateAvailability(): Promise<void>;
30+
onAccessChange(callback: (access: ApexGuruAccess) => void): void;
2831
scan(absFileToScan: string): Promise<Violation[]>;
2932
}
3033

@@ -48,12 +51,15 @@ export enum ApexGuruAccess {
4851
NOT_AUTHED = "not-authed"
4952
}
5053

54+
const ACCESS_CHANGED_EVENT = "apexGuruAccessChanged";
55+
5156
export class LiveApexGuruService implements ApexGuruService {
5257
private readonly orgConnectionService: OrgConnectionService;
5358
private readonly fileHandler: FileHandler;
5459
private readonly logger: Logger;
5560
private readonly maxTimeoutSeconds: number;
5661
private readonly retryIntervalMillis: number;
62+
private readonly eventEmitter: EventEmitter = new EventEmitter();
5763
private availability?: ApexGuruAvailability;
5864

5965
constructor(
@@ -67,42 +73,60 @@ export class LiveApexGuruService implements ApexGuruService {
6773
this.logger = logger;
6874
this.maxTimeoutSeconds = maxTimeoutSeconds;
6975
this.retryIntervalMillis = retryIntervalMillis;
76+
77+
// Every time an org is changed (authed or unauthed) then we recalculate the availability asyncronously
78+
orgConnectionService.onOrgChange((_orgUserInfo: OrgUserInfo) => {
79+
void this.updateAvailability();
80+
});
7081
}
7182

72-
async getAvailability(): Promise<ApexGuruAvailability> {
83+
getAvailability(): ApexGuruAvailability {
7384
if (this.availability === undefined) {
74-
await this.updateAvailability();
85+
// This should never happen in production because updateAvailability must be called prior to enabling
86+
// the user to even have access to any of the ApexGuru scan buttons. If it does, we should investigate.
87+
throw new Error('The getAvailability method should not be called until updateAvailability is first called');
7588
}
7689
return this.availability;
7790
}
7891

79-
// TODO: Soon with W-18538308 we will be using the connection.onOrgChange to wire up to this
80-
private async updateAvailability(): Promise<void> {
92+
onAccessChange(callback: (access: ApexGuruAccess) => void): void {
93+
this.eventEmitter.addListener(ACCESS_CHANGED_EVENT, callback);
94+
}
95+
96+
async updateAvailability(): Promise<void> {
8197
if (!this.orgConnectionService.isAuthed()) {
82-
this.availability = {
98+
this.setAvailability({
8399
access: ApexGuruAccess.NOT_AUTHED,
84100
message: messages.apexGuru.noOrgAuthed
85-
};
101+
});
86102
return;
87103
}
88104

89105
const response: ApexGuruResponse = await this.request('GET', await this.getValidateEndpoint());
90106

91107
if (response.status === RESPONSE_STATUS.SUCCESS) {
92-
this.availability = {
108+
this.setAvailability({
93109
access: ApexGuruAccess.ENABLED,
94110

95111
// This message isn't used anywhere except for debugging purposes and it allows us to make message field
96112
// a string instead of a string | undefined.
97113
message: "ApexGuru access is enabled."
98-
};
114+
});
99115
} else {
100-
this.availability = {
116+
this.setAvailability({
101117
access: response.status === RESPONSE_STATUS.FAILED ? ApexGuruAccess.ELIGIBLE : ApexGuruAccess.INELIGIBLE,
102118

103119
// There should always be a message on failed and error responses, but adding this here just in case
104120
message: response.message ?? `ApexGuru access is not enabled. Response: ${JSON.stringify(response)}`
105-
};
121+
});
122+
}
123+
}
124+
125+
private setAvailability(availability: ApexGuruAvailability) {
126+
const oldAccess: ApexGuruAccess | undefined = this.availability?.access;
127+
this.availability = availability;
128+
if (availability.access !== oldAccess) {
129+
this.eventEmitter.emit(ACCESS_CHANGED_EVENT, availability.access);
106130
}
107131
}
108132

src/lib/constants.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export const ABSOLUTE_MINIMUM_REQUIRED_CODE_ANALYZER_CLI_PLUGIN_VERSION = '5.0.0
5858

5959
// Context variables (dynamically set but consumed by the "when" conditions in the package.json "contributes" sections)
6060
export const CONTEXT_VAR_EXTENSION_ACTIVATED = 'sfca.extensionActivated';
61-
export const CONTEXT_VAR_APEX_GURU_ENABLED = 'sfca.apexGuruEnabled';
61+
export const CONTEXT_VAR_SHOULD_SHOW_APEX_GURU_BUTTONS = 'sfca.shouldShowApexGuruButtons';
6262

6363
// Documentation URLs
6464
export const DOCS_SETUP_LINK = 'https://developer.salesforce.com/docs/platform/salesforce-code-analyzer/guide/analyze-vscode.html#install-and-configure-code-analyzer-vs-code-extension';

src/lib/external-services/org-connection-service.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import * as vscode from "vscode";
33
export interface OrgConnectionService {
44
isAuthed(): boolean;
55
getApiVersion(): Promise<string>;
6-
onOrgChange(callback: () => void): void;
6+
onOrgChange(callback: (orgUserInfo: OrgUserInfo) => void): void;
77
request<T>(requestOptions: HttpRequest): Promise<T>;
88
}
99

@@ -21,7 +21,7 @@ export class NoOpOrgConnectionService implements OrgConnectionService {
2121
throw new Error(`Cannot get the api verison because no org is authed.`);
2222
}
2323

24-
onOrgChange(_callback: () => void): void {
24+
onOrgChange(_callback: (orgUserInfo: OrgUserInfo) => void): void {
2525
// No-op
2626
}
2727

@@ -50,7 +50,7 @@ export class LiveOrgConnectionService implements OrgConnectionService {
5050
}
5151

5252

53-
onOrgChange(callback: (event: OrgUserInfo) => void): void {
53+
onOrgChange(callback: (orgUserInfo: OrgUserInfo) => void): void {
5454
this.workpaceContext.onOrgChange(callback);
5555
}
5656

src/lib/settings.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ export interface SettingsManager {
1010
// General Settings
1111
getAnalyzeOnOpen(): boolean;
1212
getAnalyzeOnSave(): boolean;
13-
getApexGuruEnabled(): boolean;
1413

1514
// Configuration Settings
1615
getCodeAnalyzerConfigFile(): string;
@@ -32,10 +31,6 @@ export class SettingsManagerImpl implements SettingsManager {
3231
return vscode.workspace.getConfiguration('codeAnalyzer.analyzeOnSave').get('enabled');
3332
}
3433

35-
public getApexGuruEnabled(): boolean {
36-
return vscode.workspace.getConfiguration('codeAnalyzer.apexGuru').get('enabled');
37-
}
38-
3934
// =================================================================================================================
4035
// ==== Configuration Settings
4136
// =================================================================================================================

src/test/legacy/settings.test.ts

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -50,19 +50,4 @@ suite('SettingsManager Test Suite', () => {
5050
expect(result).to.equal(mockAnalyzeOnOpenEnabled);
5151
expect(getConfigurationStub.calledOnceWith('codeAnalyzer.analyzeOnOpen')).to.equal(true);
5252
});
53-
54-
test('getApexGuruEnabled should return the apexGuru enabled setting', () => {
55-
// ===== SETUP =====
56-
const mockAnalyzeOnSaveEnabled = true;
57-
getConfigurationStub.withArgs('codeAnalyzer.apexGuru').returns({
58-
get: Sinon.stub().returns(mockAnalyzeOnSaveEnabled)
59-
});
60-
61-
// ===== TEST =====
62-
const result = new SettingsManagerImpl().getApexGuruEnabled();
63-
64-
// ===== ASSERTIONS =====
65-
expect(result).to.equal(mockAnalyzeOnSaveEnabled);
66-
expect(getConfigurationStub.calledOnceWith('codeAnalyzer.apexGuru')).to.equal(true);
67-
});
6853
});

0 commit comments

Comments
 (0)