Skip to content

Commit

Permalink
NEW (Extension) @W-16371431@ Poll ApexGuru API and show the response …
Browse files Browse the repository at this point in the history
…as diagnostic (#115)
  • Loading branch information
jag-j committed Aug 7, 2024
1 parent f8e2644 commit f1ea764
Show file tree
Hide file tree
Showing 10 changed files with 375 additions and 60 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@
},
{
"command": "sfca.runApexGuruAnalysisOnSelectedFile",
"title": "***SFDX: Run Apex Guru Analysis***"
"title": "***SFDX: Scan for Performance Issues with ApexGuru***"
}
],
"configuration": {
Expand Down
98 changes: 89 additions & 9 deletions src/apexguru/apex-guru-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@

import * as vscode from 'vscode';
import * as fspromises from 'fs/promises';
import { CoreExtensionService } from '../lib/core-extension-service';
import { CoreExtensionService, Connection, TelemetryService } from '../lib/core-extension-service';
import * as Constants from '../lib/constants';
import {messages} from '../lib/messages';
import { RuleResult, ApexGuruViolation } from '../types';
import { DiagnosticManager } from '../lib/diagnostics';
import { RunInfo } from '../extension';

export async function isApexGuruEnabledInOrg(outputChannel: vscode.LogOutputChannel): Promise<boolean> {
try {
Expand All @@ -29,22 +33,70 @@ export async function isApexGuruEnabledInOrg(outputChannel: vscode.LogOutputChan
}
}

export async function runApexGuruOnFile(selection: vscode.Uri, outputChannel: vscode.LogOutputChannel) {
export async function runApexGuruOnFile(selection: vscode.Uri, runInfo: RunInfo) {
const {
diagnosticCollection,
commandName,
outputChannel
} = runInfo;
const startTime = Date.now();
try {
const requestId = await initiateApexGuruRequest(selection, outputChannel);
// TODO: Logging the request Id for easy QA. Future stories will use this requestId to poll and retrieve the Apex Guru report.
outputChannel.appendLine('***Apex Guru request Id:***' + requestId);
await vscode.window.withProgress({
location: vscode.ProgressLocation.Notification
}, async (progress) => {
progress.report(messages.apexGuru.progress);
const connection = await CoreExtensionService.getConnection();
const requestId = await initiateApexGuruRequest(selection, outputChannel, connection);
outputChannel.appendLine('***Apex Guru request Id:***' + requestId);

const queryResponse: ApexGuruQueryResponse = await pollAndGetApexGuruResponse(connection, requestId, Constants.APEX_GURU_MAX_TIMEOUT_SECONDS, Constants.APEX_GURU_RETRY_INTERVAL_MILLIS);

const decodedReport = Buffer.from(queryResponse.report, 'base64').toString('utf8');
outputChannel.appendLine('***Retrieved analysis report from ApexGuru***:' + decodedReport);

const ruleResult = transformStringToRuleResult(selection.fsPath, decodedReport);
new DiagnosticManager().displayDiagnostics([selection.fsPath], [ruleResult], diagnosticCollection);
TelemetryService.sendCommandEvent(Constants.TELEM_SUCCESSFUL_APEX_GURU_FILE_ANALYSIS, {
executedCommand: commandName,
duration: (Date.now() - startTime).toString()
});
});
} catch (e) {
const errMsg = e instanceof Error ? e.message : e as string;
outputChannel.appendLine('***Apex Guru initiate request failed***');
outputChannel.appendLine(errMsg);
}
}

export async function initiateApexGuruRequest(selection: vscode.Uri, outputChannel: vscode.LogOutputChannel): Promise<string> {
export async function pollAndGetApexGuruResponse(connection: Connection, requestId: string, maxWaitTimeInSeconds: number, retryIntervalInMillis: number): Promise<ApexGuruQueryResponse> {
let queryResponse: ApexGuruQueryResponse;
let lastErrorMessage = '';
const startTime = Date.now();
while ((Date.now() - startTime) < maxWaitTimeInSeconds * 1000) {
try {
queryResponse = await connection.request({
method: 'GET',
url: `${Constants.APEX_GURU_REQUEST}/${requestId}`,
body: ''
});
if (queryResponse.status == 'success') {
return queryResponse;
}
} catch (error) {
lastErrorMessage = (error as Error).message;
}
await new Promise(resolve => setTimeout(resolve, retryIntervalInMillis));

}
if (queryResponse) {
return queryResponse;
}
throw new Error(`Failed to get a successful response from Apex Guru after maximum retries.${lastErrorMessage}`);
}

export async function initiateApexGuruRequest(selection: vscode.Uri, outputChannel: vscode.LogOutputChannel, connection: Connection): Promise<string> {
const fileContent = await fileSystem.readFile(selection.fsPath);
const base64EncodedContent = Buffer.from(fileContent).toString('base64');
const connection = await CoreExtensionService.getConnection();
const response: ApexGuruInitialResponse = await connection.request({
method: 'POST',
url: Constants.APEX_GURU_REQUEST,
Expand All @@ -66,6 +118,34 @@ export const fileSystem = {
readFile: (path: string) => fspromises.readFile(path, 'utf8')
};

export function transformStringToRuleResult(fileName: string, jsonString: string): RuleResult {
const reports = JSON.parse(jsonString) as ApexGuruReport[];

const ruleResult: RuleResult = {
engine: 'apexguru',
fileName: fileName,
violations: []
};

reports.forEach(parsed => {
const encodedClassAfter = parsed.properties.find((prop: ApexGuruProperty) => prop.name === 'code_after')?.value;

const violation: ApexGuruViolation = {
ruleName: parsed.type,
message: parsed.value,
severity: 1,
category: parsed.type, // Replace with actual category if available
line: parseInt(parsed.properties.find((prop: ApexGuruProperty) => prop.name === 'line_number')?.value),
column: 1,
suggestedCode: Buffer.from(encodedClassAfter, 'base64').toString('utf8')
};

ruleResult.violations.push(violation);
});

return ruleResult;
}

export type ApexGuruAuthResponse = {
status: string;
}
Expand All @@ -78,8 +158,8 @@ export type ApexGuruInitialResponse = {

export type ApexGuruQueryResponse = {
status: string;
message: string;
report: string;
message?: string;
report?: string;
}

export type ApexGuruProperty = {
Expand Down
17 changes: 16 additions & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import * as ApexGuruFunctions from './apexguru/apex-guru-service'
export type RunInfo = {
diagnosticCollection?: vscode.DiagnosticCollection;
commandName: string;
outputChannel?: vscode.LogOutputChannel;
}

/**
Expand Down Expand Up @@ -53,8 +54,9 @@ export async function activate(context: vscode.ExtensionContext): Promise<vscode
// We need to do this first in case any other services need access to those provided by the core extension.
await CoreExtensionService.loadDependencies(context, outputChannel);

const apexGuruEnabled = Constants.APEX_GURU_FEATURE_FLAG_ENABLED && await ApexGuruFunctions.isApexGuruEnabledInOrg(outputChannel);
// Set the necessary flags to control showing the command
await vscode.commands.executeCommand('setContext', 'sfca.apexGuruEnabled', Constants.APEX_GURU_FEATURE_FLAG_ENABLED && await ApexGuruFunctions.isApexGuruEnabledInOrg(outputChannel));
await vscode.commands.executeCommand('setContext', 'sfca.apexGuruEnabled', apexGuruEnabled);

// Define a diagnostic collection in the `activate()` scope so it can be used repeatedly.
diagnosticCollection = vscode.languages.createDiagnosticCollection('sfca');
Expand Down Expand Up @@ -137,6 +139,19 @@ export async function activate(context: vscode.ExtensionContext): Promise<vscode
await _runDfa(context);
});
context.subscriptions.push(runOnActiveFile, runOnSelected, runDfaOnSelectedMethodCmd, runDfaOnWorkspaceCmd, removeDiagnosticsOnActiveFile, removeDiagnosticsOnSelectedFile, removeDiagnosticsInRange);

if (apexGuruEnabled) {
const runApexGuruOnSelectedFile = vscode.commands.registerCommand(Constants.COMMAND_RUN_APEX_GURU_ON_FILE, async (selection: vscode.Uri, multiSelect?: vscode.Uri[]) => {
return await ApexGuruFunctions.runApexGuruOnFile(multiSelect && multiSelect.length > 0 ? multiSelect[0] : selection,
{
commandName: Constants.COMMAND_RUN_APEX_GURU_ON_FILE,
diagnosticCollection,
outputChannel: outputChannel
});
});
context.subscriptions.push(runApexGuruOnSelectedFile);
}

TelemetryService.sendExtensionActivationEvent(extensionHrStart);
outputChannel.appendLine(`Extension sfdx-code-analyzer-vscode activated.`);
return Promise.resolve(context);
Expand Down
6 changes: 4 additions & 2 deletions src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@ export const COMMAND_RUN_DFA = 'sfca.runDfa';
export const COMMAND_REMOVE_DIAGNOSTICS_ON_ACTIVE_FILE = 'sfca.removeDiagnosticsOnActiveFile';
export const COMMAND_REMOVE_DIAGNOSTICS_ON_SELECTED_FILE = 'sfca.removeDiagnosticsOnSelectedFile';
export const COMMAND_DIAGNOSTICS_IN_RANGE = 'sfca.removeDiagnosticsInRange'

export const COMMAND_RUN_APEX_GURU_ON_FILE = 'sfca.runApexGuruAnalysisOnSelectedFile'

// telemetry event keys
export const TELEM_SUCCESSFUL_STATIC_ANALYSIS = 'sfdx__codeanalyzer_static_run_complete';
export const TELEM_FAILED_STATIC_ANALYSIS = 'sfdx__codeanalyzer_static_run_failed';
export const TELEM_SUCCESSFUL_DFA_ANALYSIS = 'sfdx__codeanalyzer_dfa_run_complete';
export const TELEM_FAILED_DFA_ANALYSIS = 'sfdx__codeanalyzer_dfa_run_failed';

export const TELEM_SUCCESSFUL_APEX_GURU_FILE_ANALYSIS = 'sfdx__apexguru_file_run_complete';

// versioning
export const MINIMUM_REQUIRED_VERSION_CORE_EXTENSION = '58.4.1';
Expand All @@ -38,3 +38,5 @@ export const APEX_GURU_REQUEST = '/services/data/v62.0/apexguru/request'

// feature gates
export const APEX_GURU_FEATURE_FLAG_ENABLED = false;
export const APEX_GURU_MAX_TIMEOUT_SECONDS = 60;
export const APEX_GURU_RETRY_INTERVAL_MILLIS = 1000;
2 changes: 1 addition & 1 deletion src/lib/core-extension-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ interface WorkspaceContext {
alias(): string | undefined;
}

interface Connection {
export interface Connection {
instanceUrl: string;
getApiVersion(): string;
getUsername(): string | undefined;
Expand Down
8 changes: 7 additions & 1 deletion src/lib/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ export const messages = {
increment: 60
}
},
apexGuru: {
progress: {
message: "Code Analyzer running ApexGuru analysis."
}
},
info: {
finishedScan: (scannedCount: number, badFileCount: number, violationCount: number) => `Scan complete. Analyzed ${scannedCount} files. ${violationCount} violations found in ${badFileCount} files.`
},
Expand All @@ -33,7 +38,8 @@ export const messages = {
},
fixer: {
supressOnLine: "Suppress violations on this line.",
supressOnClass: "Suppress violations on this class."
supressOnClass: "Suppress violations on this class.",
fixWithApexGuruSuggestions: "***Fix violations with suggestions from Apex Guru***"
},
diagnostics: {
messageGenerator: (severity: number, message: string) => `Sev${severity}: ${message}`,
Expand Down
42 changes: 42 additions & 0 deletions src/test/suite/apex-lsp.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright (c) 2024, Salesforce, Inc.
* All rights reserved.
* SPDX-License-Identifier: BSD-3-Clause
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import Sinon = require('sinon');
import {expect} from 'chai';
import * as vscode from 'vscode';
import { ApexLsp } from '../../lib/apex-lsp';

suite('ScanRunner', () => {
let executeCommandStub: sinon.SinonStub;

setup(() => {
executeCommandStub = Sinon.stub(vscode.commands, 'executeCommand');
});

teardown(() => {
executeCommandStub.restore();
});

test('Should call vscode.executeDocumentSymbolProvider with the correct documentUri and return the symbols', async () => {
const documentUri = vscode.Uri.file('test.cls');
const symbols: vscode.DocumentSymbol[] = [
new vscode.DocumentSymbol(
'Some Class',
'Test Class',
vscode.SymbolKind.Class,
new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 1)),
new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 1))
)
];

executeCommandStub.resolves(symbols);

const result = await ApexLsp.getSymbols(documentUri);

expect(executeCommandStub.calledOnceWith('vscode.executeDocumentSymbolProvider', documentUri)).to.be.true;
expect(result).to.deep.equal(symbols);
});
});
Loading

0 comments on commit f1ea764

Please sign in to comment.