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
2 changes: 1 addition & 1 deletion packages/code-analyzer-apexguru-engine/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@salesforce/code-analyzer-apexguru-engine",
"description": "ApexGuru Engine Package for the Salesforce Code Analyzer",
"version": "0.42.0",
"version": "0.43.0-SNAPSHOT",
"author": "The Salesforce Code Analyzer Team",
"license": "BSD-3-Clause",
"homepage": "https://developer.salesforce.com/docs/platform/salesforce-code-analyzer/overview",
Expand Down
50 changes: 24 additions & 26 deletions packages/code-analyzer-apexguru-engine/src/apexguru-rules.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@


import { RuleDescription, SeverityLevel } from '@salesforce/code-analyzer-engine-api';

export const DEV_PREVIEW_TAG_APEXGURU: string = 'DevPreviewApexGuru';
import { COMMON_TAGS, RuleDescription, SeverityLevel } from '@salesforce/code-analyzer-engine-api';

/**
* Known ApexGuru rules with descriptions and metadata.
Expand All @@ -18,15 +16,15 @@ export const APEXGURU_RULES: RuleDescription[] = [
{
name: 'SoqlInALoop',
severityLevel: SeverityLevel.High,
tags: [DEV_PREVIEW_TAG_APEXGURU],
tags: [COMMON_TAGS.RECOMMENDED, COMMON_TAGS.CATEGORIES.PERFORMANCE],
description: 'SOQL query inside a loop causes performance issues and can hit governor limits',
resourceUrls: ['https://help.salesforce.com/s/articleView?id=xcloud.apexguru_antipattern_soql_in_loop.htm&type=5']
},

{
name: 'DmlInALoop',
severityLevel: SeverityLevel.High,
tags: [DEV_PREVIEW_TAG_APEXGURU],
tags: [COMMON_TAGS.RECOMMENDED, COMMON_TAGS.CATEGORIES.PERFORMANCE],
description: 'DML statement inside a loop causes performance issues and can hit governor limits',
resourceUrls: ['https://help.salesforce.com/s/articleView?id=xcloud.apexguru_antipattern_dml_in_loop.htm&type=5']
},
Expand All @@ -38,15 +36,15 @@ export const APEXGURU_RULES: RuleDescription[] = [
{
name: 'SoqlInALoopOneHop',
severityLevel: SeverityLevel.High,
tags: [DEV_PREVIEW_TAG_APEXGURU],
tags: [COMMON_TAGS.RECOMMENDED, COMMON_TAGS.CATEGORIES.PERFORMANCE],
description: 'SOQL query reached one method-hop away inside a loop causes performance issues and can hit governor limits',
resourceUrls: ['https://help.salesforce.com/s/articleView?id=xcloud.apexguru_antipattern_soql_in_loop_one_hop.htm&type=5']
},

{
name: 'ExpensiveMethods',
severityLevel: SeverityLevel.High,
tags: [DEV_PREVIEW_TAG_APEXGURU],
tags: [COMMON_TAGS.RECOMMENDED, COMMON_TAGS.CATEGORIES.PERFORMANCE],
description: 'Method accounts for a large share of observed Apex CPU time and is a hotspot for performance work',
resourceUrls: ['https://help.salesforce.com/s/articleView?id=xcloud.apexguru_antipattern_expensive_methods.htm&type=5']
},
Expand All @@ -58,23 +56,23 @@ export const APEXGURU_RULES: RuleDescription[] = [
{
name: 'SoqlWithoutAWhereClauseOrLimitStatement',
severityLevel: SeverityLevel.Moderate,
tags: [DEV_PREVIEW_TAG_APEXGURU],
tags: [COMMON_TAGS.RECOMMENDED, COMMON_TAGS.CATEGORIES.PERFORMANCE],
description: 'SOQL query without WHERE clause or LIMIT statement can cause performance issues and heap size exceptions',
resourceUrls: ['https://help.salesforce.com/s/articleView?id=xcloud.apexguru_antipattern_soql_without_where_clause_or_limit_statement.htm&type=5']
},

{
name: 'SoqlWithWildcardFilter',
severityLevel: SeverityLevel.Moderate,
tags: [DEV_PREVIEW_TAG_APEXGURU],
tags: [COMMON_TAGS.RECOMMENDED, COMMON_TAGS.CATEGORIES.PERFORMANCE],
description: 'SOQL query using LIKE with leading wildcard is inefficient and cannot use indexes',
resourceUrls: ['https://help.salesforce.com/s/articleView?id=xcloud.apexguru_antipattern_soql_with_wildcard_filter.htm&type=5']
},

{
name: 'SchemaGetGlobalDescribeNotEfficient',
severityLevel: SeverityLevel.Moderate,
tags: [DEV_PREVIEW_TAG_APEXGURU],
tags: [COMMON_TAGS.RECOMMENDED, COMMON_TAGS.CATEGORIES.PERFORMANCE],
description: 'Using Schema.getGlobalDescribe() causes unnecessary overhead and decreases performance',
resourceUrls: ['https://help.salesforce.com/s/articleView?id=xcloud.apexguru_antipattern_schema_getglobaldescribe_not_efficient.htm&type=5']
},
Expand All @@ -86,55 +84,55 @@ export const APEXGURU_RULES: RuleDescription[] = [
{
name: 'Soql Aggregation',
severityLevel: SeverityLevel.Moderate,
tags: [DEV_PREVIEW_TAG_APEXGURU],
tags: [COMMON_TAGS.RECOMMENDED, COMMON_TAGS.CATEGORIES.PERFORMANCE],
description: 'Manual aggregation in Apex instead of using SOQL aggregate functions causes performance issues',
resourceUrls: ['https://help.salesforce.com/s/articleView?id=xcloud.apexguru_antipattern_aggregating_in_apex.htm&type=5']
},

{
name: 'SoqlWithApexFilter',
severityLevel: SeverityLevel.Moderate,
tags: [DEV_PREVIEW_TAG_APEXGURU],
tags: [COMMON_TAGS.RECOMMENDED, COMMON_TAGS.CATEGORIES.PERFORMANCE],
description: 'Filtering records in Apex instead of using SOQL WHERE clause causes performance issues',
resourceUrls: ['https://help.salesforce.com/s/articleView?id=xcloud.apexguru_antipattern_soql_with_apex_filter.htm&type=5']
},

{
name: 'CopyingListOrSetElementsUsingAForLoop',
severityLevel: SeverityLevel.Moderate,
tags: [DEV_PREVIEW_TAG_APEXGURU],
tags: [COMMON_TAGS.RECOMMENDED, COMMON_TAGS.CATEGORIES.PERFORMANCE],
description: 'Copying list or set elements using a for loop is inefficient - use addAll() instead',
resourceUrls: ['https://help.salesforce.com/s/articleView?id=xcloud.apexguru_antipattern_copying_elements_with_for_loop.htm&type=5']
},

{
name: 'Redundant Soql',
severityLevel: SeverityLevel.Moderate,
tags: [DEV_PREVIEW_TAG_APEXGURU],
tags: [COMMON_TAGS.RECOMMENDED, COMMON_TAGS.CATEGORIES.PERFORMANCE],
description: 'Multiple identical SOQL queries cause unnecessary database round trips',
resourceUrls: ['https://help.salesforce.com/s/articleView?id=xcloud.apexguru_antipattern_redundant_soql.htm&type=5']
},

{
name: 'SoqlWithNegativeExpressions',
severityLevel: SeverityLevel.Moderate,
tags: [DEV_PREVIEW_TAG_APEXGURU],
tags: [COMMON_TAGS.RECOMMENDED, COMMON_TAGS.CATEGORIES.PERFORMANCE],
description: 'SOQL queries using negative expressions (NOT IN, !=) don\'t use indexes and cause full table scans',
resourceUrls: ['https://help.salesforce.com/s/articleView?id=xcloud.apexguru_antipattern_soql_with_negative_expressions.htm&type=5']
},

{
name: 'SObjectMapInAForLoop',
severityLevel: SeverityLevel.Moderate,
tags: [DEV_PREVIEW_TAG_APEXGURU],
tags: [COMMON_TAGS.RECOMMENDED, COMMON_TAGS.CATEGORIES.PERFORMANCE],
description: 'Building Map<Id, SObject> using .put() in a for loop is inefficient - use map constructor or putAll()',
resourceUrls: ['https://help.salesforce.com/s/articleView?id=xcloud.apexguru_antipattern_sobject_map_in_for_loop.htm&type=5']
},

{
name: 'SoqlWithoutPlatformCache',
severityLevel: SeverityLevel.Moderate,
tags: [DEV_PREVIEW_TAG_APEXGURU],
tags: [COMMON_TAGS.RECOMMENDED, COMMON_TAGS.CATEGORIES.PERFORMANCE],
description: 'Frequently executed SOQL query whose results could be served from Platform Cache to reduce database load',
resourceUrls: ['https://help.salesforce.com/s/articleView?id=xcloud.apexguru_antipattern_soql_without_platform_cache.htm&type=5']
},
Expand All @@ -146,23 +144,23 @@ export const APEXGURU_RULES: RuleDescription[] = [
{
name: 'LimitsGetHeapsizeMethods',
severityLevel: SeverityLevel.Low,
tags: [DEV_PREVIEW_TAG_APEXGURU],
tags: [COMMON_TAGS.RECOMMENDED, COMMON_TAGS.CATEGORIES.PERFORMANCE],
description: 'Frequent Limits.getHeapSize() calls add runtime overhead',
resourceUrls: ['https://help.salesforce.com/s/articleView?id=xcloud.apexguru_antipattern_limits_getheapsize_methods.htm&type=5']
},

{
name: 'ExpensiveStringComparison',
severityLevel: SeverityLevel.Low,
tags: [DEV_PREVIEW_TAG_APEXGURU],
tags: [COMMON_TAGS.RECOMMENDED, COMMON_TAGS.CATEGORIES.PERFORMANCE],
description: 'Inefficient string comparison wastes CPU time',
resourceUrls: ['https://help.salesforce.com/s/articleView?id=xcloud.apexguru_antipattern_expensive_string_comparison.htm&type=5']
},

{
name: 'ExpensiveDebugStatements',
severityLevel: SeverityLevel.Low,
tags: [DEV_PREVIEW_TAG_APEXGURU],
tags: [COMMON_TAGS.RECOMMENDED, COMMON_TAGS.CATEGORIES.PERFORMANCE],
description: 'Expensive System.debug() statements add runtime overhead',
resourceUrls: ['https://help.salesforce.com/s/articleView?id=xcloud.apexguru_antipattern_expensive_debug_statements.htm&type=5']
},
Expand All @@ -174,7 +172,7 @@ export const APEXGURU_RULES: RuleDescription[] = [
{
name: 'UsingTheTestMethodKeyword',
severityLevel: SeverityLevel.Low,
tags: [DEV_PREVIEW_TAG_APEXGURU],
tags: [COMMON_TAGS.RECOMMENDED, COMMON_TAGS.CATEGORIES.PERFORMANCE],
description: 'The testMethod keyword is deprecated - use @isTest annotation instead',
resourceUrls: ['https://help.salesforce.com/s/articleView?id=xcloud.apexguru_test_case_antipattern_using_testmethod.htm&type=5']
},
Expand All @@ -186,31 +184,31 @@ export const APEXGURU_RULES: RuleDescription[] = [
{
name: 'SortingInApex',
severityLevel: SeverityLevel.Low,
tags: [DEV_PREVIEW_TAG_APEXGURU],
tags: [COMMON_TAGS.RECOMMENDED, COMMON_TAGS.CATEGORIES.PERFORMANCE],
description: 'Sorting records in Apex wastes CPU time and can exceed governor limits - use ORDER BY in SOQL',
resourceUrls: ['https://help.salesforce.com/s/articleView?id=xcloud.apexguru_antipattern_sorting_in_apex.htm&type=5']
},

{
name: 'BusyLoopDelay',
severityLevel: SeverityLevel.Low,
tags: [DEV_PREVIEW_TAG_APEXGURU],
tags: [COMMON_TAGS.RECOMMENDED, COMMON_TAGS.CATEGORIES.PERFORMANCE],
description: 'Using empty loops to delay execution wastes CPU time - use System.enqueueJob with delay parameter',
resourceUrls: ['https://help.salesforce.com/s/articleView?id=xcloud.apexguru_antipattern_busy_loop_delay.htm&type=5']
},

{
name: 'SoqlWithUnusedFields',
severityLevel: SeverityLevel.Low,
tags: [DEV_PREVIEW_TAG_APEXGURU],
tags: [COMMON_TAGS.RECOMMENDED, COMMON_TAGS.CATEGORIES.PERFORMANCE],
description: 'SOQL query selecting unused fields increases resource consumption unnecessarily',
resourceUrls: ['https://help.salesforce.com/s/articleView?id=xcloud.apexguru_antipattern_soql_with_unused_fields.htm&type=5']
},

{
name: 'WritingFillerStatements',
severityLevel: SeverityLevel.Low,
tags: [DEV_PREVIEW_TAG_APEXGURU],
tags: [COMMON_TAGS.RECOMMENDED, COMMON_TAGS.CATEGORIES.PERFORMANCE],
description: 'Filler statements written to inflate code coverage instead of testing real behavior',
resourceUrls: ['https://help.salesforce.com/s/articleView?id=xcloud.apexguru_test_case_antipattern_filler_statements.htm&type=5']
},
Expand All @@ -222,7 +220,7 @@ export const APEXGURU_RULES: RuleDescription[] = [
{
name: 'apexguru-other',
severityLevel: SeverityLevel.Moderate,
tags: [DEV_PREVIEW_TAG_APEXGURU],
tags: [COMMON_TAGS.RECOMMENDED, COMMON_TAGS.CATEGORIES.PERFORMANCE],
description: 'Other ApexGuru rules - covers new rules added by Salesforce that are not yet explicitly declared',
resourceUrls: ['https://help.salesforce.com/s/articleView?id=xcloud.apexguru.htm']
}
Expand Down
40 changes: 36 additions & 4 deletions packages/code-analyzer-apexguru-engine/src/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,15 @@ export class ApexGuruEngine extends EngineEventEmitter implements Engine {
} catch (error) {
const detail = error instanceof Error ? error.message : String(error);
this.apexGuruService.cleanup();
if (this.isInvalidSessionError(error)) {
return this.skipWithError('INVALID_SESSION',
'ApexGuru skipped the scan because your org session is invalid or expired. (401 Unauthorized: invalid session).\n' +
'Re-authenticate: sf org login web\n' +
'Then run the scan again.',
'');
}
return this.skipWithError('NO_ORG_CONNECTION',
`Failed to authenticate: ${detail}`,
detail,
"Please authenticate with 'sf org login web' or pass --target-org");
}

Expand Down Expand Up @@ -195,10 +202,16 @@ export class ApexGuruEngine extends EngineEventEmitter implements Engine {
} catch (error) {
// Catch API failures (5xx, timeout, connection refused) and unexpected errors
const detail = error instanceof Error ? error.message : String(error);
if (this.isScanTimeoutError(error)) {
return this.skipWithError('SCAN_TIMEOUT',
`Code Analyzer skipped ApexGuru scan because the workspace scan timed out after ${this.config.api_timeout_ms} ms.`,
'');
}
if (this.isApiUnavailableError(error)) {
return this.skipWithError('API_UNAVAILABLE',
`ApexGuru service is unavailable: ${detail}`,
'The ApexGuru service is temporarily unavailable. Please try again later.');
'Code Analyzer skipped ApexGuru scan because the service is unavailable right now. ' +
'Try again later. If the issue persists, contact Salesforce Support.',
'');
}
return this.skipWithError('UNEXPECTED_ERROR',
`An unexpected error occurred: ${detail}`,
Expand All @@ -216,7 +229,7 @@ export class ApexGuruEngine extends EngineEventEmitter implements Engine {
* this may be invoked from within a try-finally that already handles cleanup.
*/
private skipWithError(code: string, message: string, remediation: string): EngineRunResults {
this.emitLogEvent(LogLevel.Warn, `ApexGuru skipped: ${message}`);
this.emitLogEvent(LogLevel.Warn, message);
this.emitRunRulesProgressEvent(100);
return {
violations: [],
Expand All @@ -227,6 +240,25 @@ export class ApexGuruEngine extends EngineEventEmitter implements Engine {
};
}

/**
* Determines whether an error is an invalid/expired org session (401 Unauthorized).
* Surfaces from the Org JWT minting step during initialize() when the org access
* token is no longer valid.
*/
private isInvalidSessionError(error: unknown): boolean {
if (!(error instanceof Error)) return false;
const msg = error.message.toLowerCase();
return msg.includes('401') || msg.includes('invalid session') || msg.includes('session id');
}

/**
* Determines whether an error is the workspace scan timeout raised by ApexGuruService
* when the scan exceeds the configured api_timeout_ms.
*/
private isScanTimeoutError(error: unknown): boolean {
return error instanceof Error && error.message.includes('Workspace scan timed out');
}

/**
* Determines whether an error is an API unavailability issue (network/timeout/5xx).
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,11 @@ export class ApexGuruAuthService {
const errorMessage = err instanceof Error ? err.message : String(err);
this.emitLogEvent(LogLevel.Fine, `Failed to authenticate: No default org found: ${errorMessage}`);
throw new Error(
'No default org found. Please either:\n' +
'Code Analyzer skipped ApexGuru scan because no default org is set. ' +
'To continue, do one of the following:\n' +
' 1. Set a default org: sf config set target-org <org-alias>\n' +
' 2. Pass --target-org flag: sf code-analyzer run --target-org <org-alias> ...\n' +
' 3. Authenticate to an org: sf org login web'
' 2. Run the scan with a target org: sf code-analyzer run --target-org <org-alias> ...\n' +
' 3. Authenticate to your org: sf org login web'
);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import {
ApexGuruPollResponse,
ApexGuruResponseStatus,
ApexGuruScanMetadata,
ApexGuruViolation
ApexGuruViolation,
ApexGuruOrgResolveResponse
} from '../types';
import * as fs from 'node:fs';
import * as path from 'node:path';
Expand Down Expand Up @@ -159,16 +160,64 @@ export class ApexGuruService {
});
}

/**
* Resolve the caller's org to its production org id via the org-resolve
* Connect API (POST /services/data/v{version}/apexguru/org/resolve). The
* returned productionOrgId is forwarded as an optional `productionOrgId`
* multipart form field on the SFAP scan submit call. Uses the org access
* token (not the Org JWT),
* since this is a standard Salesforce org REST endpoint.
*/
private async resolveProductionOrgId(): Promise<string> {
const accessToken = this.authService.getAccessToken();
const instanceUrl = this.authService.getInstanceUrl();
const apiVersion = this.authService.getApiVersion();
const url = `${instanceUrl}/services/data/v${apiVersion}/apexguru/org/resolve`;

try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Authorization': `Bearer ${accessToken}`
}
});

if (!response.ok) {
const errorText = await response.text();
throw new Error(formatHttpError(response.status, errorText));
}

const resolveResponse = await response.json() as ApexGuruOrgResolveResponse;

if (!resolveResponse.productionOrgId) {
throw new Error('Org resolve response missing productionOrgId field');
}
return resolveResponse.productionOrgId;
} catch (error) {
// The productionOrgId is optional on the scan submit call, so a failure here is non-fatal.
// Log a warning and return an empty string so the scan can proceed without it.
const errorMessage = error instanceof Error ? error.message : String(error);
this.emitLogEvent(LogLevel.Warn, `Failed to resolve production org id: ${errorMessage}`);
return '';
}
}

/**
* Submit workspace zip to SFAP API
*/
private async submitScan(zipBuffer: Buffer): Promise<ApexGuruSubmitResponse> {
const orgJwt = await this.authService.mintOrgJwt();
const productionOrgId = await this.resolveProductionOrgId();
const url = `${this.sfapBaseUrl}/apex-guru/scan`;

const form = new FormData();
form.append('file', zipBuffer, { filename: 'project.zip', contentType: 'application/zip' });
form.append('analysisModeHint', 'full');
// productionOrgId is optional — only include it when the org-resolve call returned one.
if (productionOrgId) {
form.append('productionOrgId', productionOrgId);
}

try {
const response = await fetch(url, {
Expand Down
Loading
Loading