Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RELEASE @W-16382721@: Conducting 1.1.0 release #124

Merged
merged 11 commits into from
Aug 27, 2024
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
2 changes: 1 addition & 1 deletion SHA256.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ make sure that their SHA values match the values in the list below.
shasum -a 256 <location_of_the_downloaded_file>

3. Confirm that the SHA in your output matches the value in this list of SHAs.
7ddd2068fbe9a58bbb8fdf81f80964bd645e7a3112aaace72b3da1ca3dad05f5 ./extensions/sfdx-code-analyzer-vscode-0.7.0.vsix
172c7b2973ea3d66feaae5017e7eae0de384340a137d59305ef3c1bc4114a04f ./extensions/sfdx-code-analyzer-vscode-1.0.0.vsix
4. Change the filename extension for the file that you downloaded from .zip to
.vsix.

Expand Down
23 changes: 20 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"color": "#ECECEC",
"theme": "light"
},
"version": "1.0.0",
"version": "1.1.0",
"publisher": "salesforce",
"license": "BSD-3-Clause",
"engines": {
Expand Down Expand Up @@ -105,7 +105,11 @@
},
{
"command": "sfca.runApexGuruAnalysisOnSelectedFile",
"title": "***SFDX: Run Apex Guru Analysis***"
"title": "SFDX: Scan Selected File for Performance Issues with ApexGuru"
},
{
"command": "sfca.runApexGuruAnalysisOnCurrentFile",
"title": "SFDX: Scan Current File for Performance Issues with ApexGuru"
}
],
"configuration": {
Expand Down Expand Up @@ -158,6 +162,11 @@
"codeAnalyzer.rules.category": {
"type": "string",
"description": "The categories of rules to run. Specify multiple values as a comma-separated list. Run 'sf scanner rule list -e' in the terminal for the list of categories associated with a specific engine."
},
"codeAnalyzer.apexGuru.enabled": {
"type": "boolean",
"default": false,
"description": "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."
}
}
},
Expand Down Expand Up @@ -190,6 +199,10 @@
{
"command": "sfca.runApexGuruAnalysisOnSelectedFile",
"when": "false"
},
{
"command": "sfca.runApexGuruAnalysisOnCurrentFile",
"when": "sfca.apexGuruEnabled && resourceExtname =~ /\\.cls|\\.trigger|\\.apex/"
}
],
"editor/context": [
Expand All @@ -201,6 +214,10 @@
},
{
"command": "sfca.removeDiagnosticsOnActiveFile"
},
{
"command": "sfca.runApexGuruAnalysisOnCurrentFile",
"when": "sfca.apexGuruEnabled && resourceExtname =~ /\\.cls|\\.trigger|\\.apex/"
}
],
"explorer/context": [
Expand All @@ -213,7 +230,7 @@
},
{
"command": "sfca.runApexGuruAnalysisOnSelectedFile",
"when": "sfca.apexGuruEnabled"
"when": "sfca.apexGuruEnabled && resourceExtname =~ /\\.cls|\\.trigger|\\.apex/"
}
]
},
Expand Down
185 changes: 185 additions & 0 deletions src/apexguru/apex-guru-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
/*
* 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 * as vscode from 'vscode';
import * as fspromises from 'fs/promises';
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 {
const connection = await CoreExtensionService.getConnection();
const response:ApexGuruAuthResponse = await connection.request({
method: 'GET',
url: Constants.APEX_GURU_AUTH_ENDPOINT,
body: ''
});
return response.status == 'Success';
} catch(e) {
// This could throw an error for a variety of reasons. The API endpoint has not been deployed to the instance, org has no perms, timeouts etc,.
// In all of these scenarios, we return false.
const errMsg = e instanceof Error ? e.message : e as string;
outputChannel.error('Apex Guru perm check failed with error:' + errMsg);
outputChannel.show();
return false;
}
}

export async function runApexGuruOnFile(selection: vscode.Uri, runInfo: RunInfo) {
const {
diagnosticCollection,
commandName,
outputChannel
} = runInfo;
const startTime = Date.now();
try {
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('Code Analyzer with ApexGuru 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');

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()
});
void vscode.window.showInformationMessage(messages.apexGuru.finishedScan(ruleResult.violations.length));
});
} catch (e) {
const errMsg = e instanceof Error ? e.message : e as string;
outputChannel.error('Initial Code Analyzer with ApexGuru request failed.');
outputChannel.appendLine(errMsg);
}
}

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 response: ApexGuruInitialResponse = await connection.request({
method: 'POST',
url: Constants.APEX_GURU_REQUEST,
body: JSON.stringify({
classContent: base64EncodedContent
})
});

if (response.status != 'new' && response.status != 'success') {
outputChannel.warn('Code Analyzer with Apex Guru returned unexpected response:' + response.status);
throw Error('Code Analyzer with Apex Guru returned unexpected response:' + response.status);
}

const requestId = response.requestId;
return requestId;
}

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 encodedCodeBefore =
parsed.properties.find((prop: ApexGuruProperty) => prop.name === 'code_before')?.value
?? parsed.properties.find((prop: ApexGuruProperty) => prop.name === 'class_before')?.value
?? '';
const encodedCodeAfter =
parsed.properties.find((prop: ApexGuruProperty) => prop.name === 'code_after')?.value
?? parsed.properties.find((prop: ApexGuruProperty) => prop.name === 'class_after')?.value
?? '';
const lineNumber = parseInt(parsed.properties.find((prop: ApexGuruProperty) => prop.name === 'line_number')?.value);

const violation: ApexGuruViolation = {
ruleName: parsed.type,
message: parsed.value,
severity: 1,
category: parsed.type, // Replace with actual category if available
line: lineNumber,
column: 1,
currentCode: Buffer.from(encodedCodeBefore, 'base64').toString('utf8'),
suggestedCode: Buffer.from(encodedCodeAfter, 'base64').toString('utf8'),
url: fileName
};

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

return ruleResult;
}

export type ApexGuruAuthResponse = {
status: string;
}

export type ApexGuruInitialResponse = {
status: string;
requestId: string;
message: string;
}

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

export type ApexGuruProperty = {
name: string;
value: string;
};

export type ApexGuruReport = {
id: string;
type: string;
value: string;
properties: ApexGuruProperty[];
}
49 changes: 28 additions & 21 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,20 @@ import {ScanRunner} from './lib/scanner';
import {SettingsManager} from './lib/settings';
import {SfCli} from './lib/sf-cli';

import {RuleResult, ApexGuruAuthResponse} from './types';
import {RuleResult} from './types';
import {DiagnosticManager} from './lib/diagnostics';
import {messages} from './lib/messages';
import {Fixer} from './lib/fixer';
import { CoreExtensionService, TelemetryService } from './lib/core-extension-service';
import * as Constants from './lib/constants';
import * as path from 'path';
import { SIGKILL } from 'constants';
import * as ApexGuruFunctions from './apexguru/apex-guru-service'

export type RunInfo = {
diagnosticCollection?: vscode.DiagnosticCollection;
commandName: string;
outputChannel?: vscode.LogOutputChannel;
}

/**
Expand Down Expand Up @@ -52,8 +54,10 @@ 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 apexGuruFeatureFlag = SettingsManager.getApexGuruEnabled();
const apexGuruEnabled = apexGuruFeatureFlag && 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 _isApexGuruEnabledInOrg());
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 @@ -136,30 +140,33 @@ 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
});
});
const runApexGuruOnCurrentFile = vscode.commands.registerCommand(Constants.COMMAND_RUN_APEX_GURU_ON_ACTIVE_FILE, async () => {
const targets: string[] = await targeting.getTargets([]);
return await ApexGuruFunctions.runApexGuruOnFile(vscode.Uri.file(targets[0]),
{
commandName: Constants.COMMAND_RUN_APEX_GURU_ON_ACTIVE_FILE,
diagnosticCollection,
outputChannel: outputChannel
});
});
context.subscriptions.push(runApexGuruOnSelectedFile, runApexGuruOnCurrentFile);
}

TelemetryService.sendExtensionActivationEvent(extensionHrStart);
outputChannel.appendLine(`Extension sfdx-code-analyzer-vscode activated.`);
return Promise.resolve(context);
}

export async function _isApexGuruEnabledInOrg(): Promise<boolean> {
try {
const connection = await CoreExtensionService.getConnection();
const response:ApexGuruAuthResponse = await connection.request({
method: 'GET',
url: Constants.APEX_GURU_AUTH_ENDPOINT,
body: ''
});
return response.status == 'Success';
} catch(e) {
// This could throw an error for a variety of reasons. The API endpoint has not been deployed to the instance, org has no perms, timeouts etc,.
// In all of these scenarios, we return false.
const errMsg = e instanceof Error ? e.message : e as string;
outputChannel.error('***ApexGuru perm check failed with error:***' + errMsg);
outputChannel.show();
return false;
}
}

async function _runDfa(context: vscode.ExtensionContext) {
if (violationsCacheExists()) {
const choice = await vscode.window.showQuickPick(
Expand Down
11 changes: 7 additions & 4 deletions src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,16 @@ export const COMMAND_RUN_DFA_ON_SELECTED_METHOD = 'sfca.runDfaOnSelectedMethod';
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_DIAGNOSTICS_IN_RANGE = 'sfca.removeDiagnosticsInRange';
export const COMMAND_RUN_APEX_GURU_ON_FILE = 'sfca.runApexGuruAnalysisOnSelectedFile';
export const COMMAND_RUN_APEX_GURU_ON_ACTIVE_FILE = 'sfca.runApexGuruAnalysisOnCurrentFile';

// 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 @@ -34,6 +35,8 @@ export const WORKSPACE_DFA_PROCESS = 'dfaScanProcess';

// apex guru APIS
export const APEX_GURU_AUTH_ENDPOINT = '/services/data/v62.0/apexguru/validate'
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;
Loading