Skip to content
Open
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 package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"color": "#ECECEC",
"theme": "light"
},
"version": "1.20.0",
"version": "1.21.0-SNAPSHOT",
"publisher": "salesforce",
"license": "BSD-3-Clause",
"engines": {
Expand Down
4 changes: 3 additions & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {ExternalServiceProvider} from "./lib/external-services/external-service-
import {Logger, LoggerImpl} from "./lib/logger";
import {TelemetryService} from "./lib/external-services/telemetry-service";
import {CodeAnalyzerRunAction} from "./lib/code-analyzer-run-action";
import {InsightsHandler} from "./lib/insights-handler";
import {A4DFixActionProvider} from "./lib/agentforce/a4d-fix-action-provider";
import {ScanManager} from './lib/scan-manager';
import {A4DFixAction} from './lib/agentforce/a4d-fix-action';
Expand Down Expand Up @@ -113,7 +114,8 @@ export async function activate(context: vscode.ExtensionContext): Promise<SFCAEx
const codeAnalyzer: CodeAnalyzer = new CodeAnalyzerImpl(cliCommandExecutor, settingsManager, display, fileHandler);

const diagnosticFactory = (diagnosticManager as DiagnosticManagerImpl).diagnosticFactory;
const codeAnalyzerRunAction: CodeAnalyzerRunAction = new CodeAnalyzerRunAction(taskWithProgressRunner, codeAnalyzer, diagnosticManager, diagnosticFactory, telemetryService, logger, display, windowManager);
const insightsHandler: InsightsHandler = new InsightsHandler(display, logger, externalServiceProvider, windowManager);
const codeAnalyzerRunAction: CodeAnalyzerRunAction = new CodeAnalyzerRunAction(taskWithProgressRunner, codeAnalyzer, diagnosticManager, diagnosticFactory, telemetryService, logger, display, windowManager, insightsHandler);

// For performance reasons, it's best to kick this off in the background instead of await the promise.
void performValidationAndCaching(codeAnalyzer, display);
Expand Down
12 changes: 9 additions & 3 deletions src/lib/code-analyzer-run-action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import {CodeAnalyzerDiagnostic, DiagnosticFactory, DiagnosticManager, normalizeV
import {messages} from "./messages";
import {TelemetryService} from "./external-services/telemetry-service";
import * as Constants from './constants';
import {CodeAnalyzer} from "./code-analyzer";
import {CodeAnalyzer, ScanResults} from "./code-analyzer";
import {Display} from "./display";
import {InsightsHandler} from "./insights-handler";
import {getErrorMessage, getErrorMessageWithStack} from "./utils";
import {ProgressReporter, TaskWithProgressRunner} from "./progress";
import {WindowManager} from "./vscode-api";
Expand All @@ -23,9 +24,10 @@ export class CodeAnalyzerRunAction {
private readonly logger: Logger;
private readonly display: Display;
private readonly windowManager: WindowManager;
private readonly insightsHandler: InsightsHandler;
private suppressedErrors: Set<string> = new Set();

constructor(taskWithProgressRunner: TaskWithProgressRunner, codeAnalyzer: CodeAnalyzer, diagnosticManager: DiagnosticManager, diagnosticFactory: DiagnosticFactory, telemetryService: TelemetryService, logger: Logger, display: Display, windowManager: WindowManager) {
constructor(taskWithProgressRunner: TaskWithProgressRunner, codeAnalyzer: CodeAnalyzer, diagnosticManager: DiagnosticManager, diagnosticFactory: DiagnosticFactory, telemetryService: TelemetryService, logger: Logger, display: Display, windowManager: WindowManager, insightsHandler: InsightsHandler) {
this.taskWithProgressRunner = taskWithProgressRunner;
this.codeAnalyzer = codeAnalyzer;
this.diagnosticManager = diagnosticManager;
Expand All @@ -34,6 +36,7 @@ export class CodeAnalyzerRunAction {
this.logger = logger;
this.display = display;
this.windowManager = windowManager;
this.insightsHandler = insightsHandler;
}

/**
Expand Down Expand Up @@ -64,7 +67,8 @@ export class CodeAnalyzerRunAction {
increment: 20
});
this.logger.log(messages.info.scanningWith(await this.codeAnalyzer.getVersion()));
const violations: Violation[] = await this.codeAnalyzer.scan(workspace);
const scanResults: ScanResults = await this.codeAnalyzer.scan(workspace);
const violations: Violation[] = scanResults.violations;

progressReporter.reportProgress({
message: messages.scanProgressReport.processingResults,
Expand Down Expand Up @@ -108,6 +112,8 @@ export class CodeAnalyzerRunAction {
this.diagnosticManager.addDiagnostics(diagnostics);
void this.displayResults(targetedFiles.length, violationsWithFileLocation);

this.insightsHandler.handleInsights(scanResults.insights, () => { void this.run(commandName, workspace, trigger); });

this.telemetryService.sendCommandEvent(Constants.TELEM_SUCCESSFUL_STATIC_ANALYSIS, {
commandName: commandName,
duration: (Date.now() - startTime).toString(),
Expand Down
24 changes: 21 additions & 3 deletions src/lib/code-analyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,21 @@ import * as path from 'node:path';
type ResultsJson = {
runDir: string;
violations: Violation[];
insights?: Record<string, EngineInsight>;
};

export type EngineInsight = {
status: 'completed' | 'skipped';
error?: {
code: string;
message: string;
remediation: string;
};
};

export type ScanResults = {
violations: Violation[];
insights?: Record<string, EngineInsight>;
};

type RulesJson = {
Expand All @@ -35,7 +50,7 @@ type RuleDescription = {

export interface CodeAnalyzer {
validateEnvironment(): Promise<void>;
scan(workspace: Workspace): Promise<Violation[]>;
scan(workspace: Workspace): Promise<ScanResults>;
getVersion(): Promise<string>;
getRuleDescriptionFor(engineName: string, ruleName: string): Promise<string>;
}
Expand Down Expand Up @@ -116,7 +131,7 @@ export class CodeAnalyzerImpl implements CodeAnalyzer {
return this.ruleDescriptionMap;
}

public async scan(workspace: Workspace): Promise<Violation[]> {
public async scan(workspace: Workspace): Promise<ScanResults> {
await this.validateEnvironment();

const ruleSelector: string = this.settingsManager.getCodeAnalyzerRuleSelectors();
Expand Down Expand Up @@ -159,7 +174,10 @@ export class CodeAnalyzerImpl implements CodeAnalyzer {

const resultsJsonStr: string = await fs.promises.readFile(outputFile, 'utf-8');
const resultsJson: ResultsJson = JSON.parse(resultsJsonStr) as ResultsJson;
return this.processResults(resultsJson);
return {
violations: this.processResults(resultsJson),
insights: resultsJson.insights
};
}

private processResults(resultsJson: ResultsJson): Violation[] {
Expand Down
16 changes: 12 additions & 4 deletions src/lib/display.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export type DisplayButton = {
}

export interface Display {
displayInfo(infoMsg: string): void;
displayInfo(infoMsg: string, ...buttons: DisplayButton[]): void;
displayWarning(warnMsg: string, ...buttons: DisplayButton[]): void;
displayError(errorMsg: string, ...buttons: DisplayButton[]): void;
}
Expand All @@ -19,9 +19,17 @@ export class VSCodeDisplay implements Display {
this.logger = logger;
}

displayInfo(infoMsg: string): void {
// Not waiting for promise because we didn't add buttons and don't care if user ignores the message.
void vscode.window.showInformationMessage(infoMsg);
displayInfo(infoMsg: string, ...buttons: DisplayButton[]): void {
if (buttons.length > 0) {
void vscode.window.showInformationMessage(infoMsg, ...buttons.map(b => b.text)).then(selectedText => {
const selectedButton: DisplayButton = buttons.find(b => b.text === selectedText);
if (selectedButton) {
selectedButton.callback();
}
});
} else {
void vscode.window.showInformationMessage(infoMsg);
}
this.logger.log(infoMsg);
}

Expand Down
122 changes: 122 additions & 0 deletions src/lib/insights-handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import * as vscode from "vscode";
import {EngineInsight} from "./code-analyzer";
import {Display, DisplayButton} from "./display";
import {Logger} from "./logger";
import {messages} from "./messages";
import {ExternalServiceProvider} from "./external-services/external-service-provider";
import {WindowManager} from "./vscode-api";

const ISSUES_URL = 'https://github.com/forcedotcom/sfdx-code-analyzer-vscode/issues/new';

export class InsightsHandler {
private readonly display: Display;
private readonly logger: Logger;
private readonly externalServiceProvider: ExternalServiceProvider;
private readonly windowManager: WindowManager;
private readonly suppressedErrorCodes: Set<string> = new Set();

constructor(display: Display, logger: Logger, externalServiceProvider: ExternalServiceProvider, windowManager: WindowManager) {
this.display = display;
this.logger = logger;
this.externalServiceProvider = externalServiceProvider;
this.windowManager = windowManager;
}

handleInsights(insights: Record<string, EngineInsight> | undefined, retriggerScan: () => void): void {
if (!insights || !insights.apexguru) {
return;
}

const apexguruInsight = insights.apexguru;
if (apexguruInsight.status !== 'skipped') {
return;
}

const error = apexguruInsight.error;
if (!error) {
return;
}

if (this.suppressedErrorCodes.has(error.code)) {
return;
}

this.suppressedErrorCodes.add(error.code);

switch (error.code) {
case 'NO_ORG_CONNECTION':
this.handleNoOrgConnection(error.message, error.remediation);
break;
case 'API_UNAVAILABLE':
this.handleApiUnavailable(error.message, error.remediation, retriggerScan);
break;
case 'UNEXPECTED_ERROR':
this.handleUnexpectedError(error.message, error.remediation);
break;
default:
this.logger.warn(`Unknown insight error code: ${error.code}`);
}
}

private handleNoOrgConnection(message: string, remediation: string): void {
const connectOrgButton: DisplayButton = {
text: messages.insights.buttons.connectOrg,
callback: () => this.triggerConnectOrg(remediation)
};

this.display.displayInfo(
messages.insights.apexGuruSkipped.noOrgConnection(remediation),
connectOrgButton
);
this.logger.log(message);
}

private handleApiUnavailable(message: string, remediation: string, retriggerScan: () => void): void {
const retryScanButton: DisplayButton = {
text: messages.insights.buttons.retryScan,
callback: retriggerScan
};
const detailsButton: DisplayButton = {
text: messages.insights.buttons.details,
callback: () => {
this.logger.log(remediation);
this.windowManager.showLogOutputWindow();
}
};

this.display.displayInfo(
messages.insights.apexGuruSkipped.apiUnavailable(message),
retryScanButton,
detailsButton
);
this.logger.log(message);
}

private handleUnexpectedError(message: string, _remediation: string): void {
const viewDetailsButton: DisplayButton = {
text: messages.insights.buttons.viewDetails,
callback: () => this.windowManager.showLogOutputWindow()
};
const reportIssueButton: DisplayButton = {
text: messages.insights.buttons.reportIssue,
callback: () => this.windowManager.showExternalUrl(ISSUES_URL)
};

this.display.displayInfo(
messages.insights.apexGuruSkipped.unexpectedError(message),
viewDetailsButton,
reportIssueButton
);
this.logger.log(message);
}

private triggerConnectOrg(remediation: string): void {
void this.externalServiceProvider.isOrgConnectionServiceAvailable().then(available => {
if (available) {
void vscode.commands.executeCommand('sfdx.authorize.org');
} else {
this.display.displayInfo(messages.insights.fallback.connectOrgManual(remediation));
}
});
}
}
17 changes: 17 additions & 0 deletions src/lib/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,23 @@ export const messages = {
sfMissing: "To use the Salesforce Code Analyzer extension, first install Salesforce CLI.",
coreExtensionServiceUninitialized: "CoreExtensionService.ts didn't initialize. Log a new issue on Salesforce Code Analyzer VS Code extension repo: https://github.com/forcedotcom/sfdx-code-analyzer-vscode/issues"
},
insights: {
apexGuruSkipped: {
noOrgConnection: (remediation: string) => `ApexGuru analysis was skipped because no org is connected. ${remediation}`,
apiUnavailable: (message: string) => `ApexGuru analysis was skipped because the service is currently unavailable. ${message}`,
unexpectedError: (message: string) => `ApexGuru analysis was skipped due to an unexpected error. ${message}`
},
buttons: {
connectOrg: 'Connect Org',
retryScan: 'Retry Scan',
details: 'Details',
viewDetails: 'View Details',
reportIssue: 'Report Issue'
},
fallback: {
connectOrgManual: (remediation: string) => `To connect an org manually, run the following in your terminal: ${remediation}`
}
},
buttons: {
learnMore: 'Learn more',
showError: 'Show error',
Expand Down
Loading
Loading