Skip to content

Commit 5c883db

Browse files
authored
Merge pull request #291 from forcedotcom/release-1.11.0
RELEASE @W-19432331@ Conducting v1.11.0 release
2 parents 23cda30 + 99c695c commit 5c883db

24 files changed

Lines changed: 2999 additions & 680 deletions

.git2gus/config.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,6 @@
1111
"type:security": "BUG P0",
1212
"type:feedback": "",
1313
"type:duplicate": ""
14-
}
14+
},
15+
"statusWhenClosed": "CLOSED"
1516
}

.husky/pre-commit

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
npm run lint

SHA256.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ make sure that their SHA values match the values in the list below.
1515
shasum -a 256 <location_of_the_downloaded_file>
1616

1717
3. Confirm that the SHA in your output matches the value in this list of SHAs.
18-
a26fa56962cc53dfd3deeb0a32f184218e71b0110eb96b1a56966d363538fee6 ./extensions/sfdx-code-analyzer-vscode-1.9.0.vsix
18+
92d8b8bf23aec05328349ceb9b471fd004fe3d530f584b066d56cca6bf41c009 ./extensions/sfdx-code-analyzer-vscode-1.10.0.vsix
1919
4. Change the filename extension for the file that you downloaded from .zip to
2020
.vsix.
2121

package-lock.json

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

package.json

Lines changed: 19 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"color": "#ECECEC",
1616
"theme": "light"
1717
},
18-
"version": "1.10.0",
18+
"version": "1.11.0",
1919
"publisher": "salesforce",
2020
"license": "BSD-3-Clause",
2121
"engines": {
@@ -35,38 +35,40 @@
3535
"dependencies": {
3636
"@salesforce/vscode-service-provider": "^1.5.0",
3737
"@types/jest": "^30.0.0",
38-
"@types/semver": "^7.7.0",
38+
"@types/semver": "^7.7.1",
3939
"@types/tmp": "^0.2.6",
4040
"diff": "^5.2.0",
4141
"glob": "^11.0.3",
4242
"semver": "^7.7.2",
43-
"tmp": "^0.2.4"
43+
"tmp": "^0.2.5"
4444
},
4545
"devDependencies": {
46-
"@eslint/js": "^9.32.0",
47-
"@types/diff": "^5.2.3",
46+
"@eslint/js": "^9.35.0",
4847
"@types/chai": "^4.3.20",
48+
"@types/diff": "^5.2.3",
4949
"@types/mocha": "^10.0.10",
50-
"@types/node": "^22.16.4",
50+
"@types/node": "^22.18.5",
5151
"@types/sinon": "^10.0.20",
5252
"@types/vscode": "^1.90.0",
5353
"@vscode/test-cli": "^0.0.11",
5454
"@vscode/test-electron": "^2.5.2",
5555
"@vscode/vsce": "^3.6.0",
5656
"chai": "^4.5.0",
57-
"esbuild": "^0.25.8",
58-
"eslint": "^9.32.0",
59-
"jest": "^30.0.5",
60-
"jest-mock-vscode": "^4.6.0",
57+
"esbuild": "^0.25.9",
58+
"eslint": "^9.35.0",
59+
"husky": "^9.1.7",
60+
"jest": "^30.1.3",
61+
"jest-mock-vscode": "^4.7.0",
6162
"mocha": "^10.8.2",
63+
"npm-run-all": "^4.1.5",
6264
"ovsx": "^0.10.5",
6365
"proxyquire": "^2.1.3",
6466
"rimraf": "*",
6567
"sinon": "^15.2.0",
66-
"ts-jest": "^29.4.1",
68+
"ts-jest": "^29.4.2",
6769
"ts-node": "^10.9.2",
6870
"typescript": "^5.9.2",
69-
"typescript-eslint": "^8.39.0"
71+
"typescript-eslint": "^8.44.0"
7072
},
7173
"extensionDependencies": [
7274
"salesforce.salesforcedx-vscode-core"
@@ -89,7 +91,8 @@
8991
"clean": "npm run precompile && rimraf coverage",
9092
"showcoverage": "npm run showcoverage-unit && npm run showcoverage-legacy",
9193
"showcoverage-unit": "open ./coverage/unit/lcov-report/index.html",
92-
"showcoverage-legacy": "open ./coverage/legacy/lcov-report/index.html"
94+
"showcoverage-legacy": "open ./coverage/legacy/lcov-report/index.html",
95+
"prepare": "husky"
9396
},
9497
"activationEvents": [
9598
"workspaceContains:sfdx-project.json",
@@ -138,11 +141,6 @@
138141
"type": "boolean",
139142
"default": false,
140143
"description": "Scan files on open automatically."
141-
},
142-
"codeAnalyzer.apexGuru.enabled": {
143-
"type": "boolean",
144-
"default": false,
145-
"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."
146144
}
147145
}
148146
},
@@ -186,7 +184,7 @@
186184
},
187185
{
188186
"command": "sfca.runApexGuruAnalysisOnCurrentFile",
189-
"when": "sfca.extensionActivated && sfca.apexGuruEnabled && resourceExtname =~ /\\.cls|\\.trigger|\\.apex/"
187+
"when": "sfca.extensionActivated && sfca.shouldShowApexGuruButtons && resourceExtname =~ /\\.cls|\\.trigger|\\.apex/"
190188
}
191189
],
192190
"editor/context": [
@@ -200,7 +198,7 @@
200198
},
201199
{
202200
"command": "sfca.runApexGuruAnalysisOnCurrentFile",
203-
"when": "sfca.extensionActivated && sfca.apexGuruEnabled && resourceExtname =~ /\\.cls|\\.trigger|\\.apex/"
201+
"when": "sfca.extensionActivated && sfca.shouldShowApexGuruButtons && resourceExtname =~ /\\.cls|\\.trigger|\\.apex/"
204202
}
205203
],
206204
"explorer/context": [
@@ -214,7 +212,7 @@
214212
},
215213
{
216214
"command": "sfca.runApexGuruAnalysisOnSelectedFile",
217-
"when": "sfca.extensionActivated && sfca.apexGuruEnabled && explorerResourceIsFolder == false && resourceExtname =~ /\\.cls|\\.trigger|\\.apex/"
215+
"when": "sfca.extensionActivated && sfca.shouldShowApexGuruButtons && explorerResourceIsFolder == false && resourceExtname =~ /\\.cls|\\.trigger|\\.apex/"
218216
}
219217
]
220218
},

src/extension.ts

Lines changed: 7 additions & 23 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 {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,15 +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 () => settingsManager.getApexGuruEnabled() &&
252-
// Currently we don't watch for changes here when a user has apex guru enabled already. That is,
253-
// if the user logs into an org post activation of this extension, it won't show the command until they
254-
// refresh or toggle the "ApexGuru enabled" setting off and back on. At some point we might want to see
255-
// if it is possible to monitor changes to the users org so we can re-trigger this check.
256-
await apexGuruService.isApexGuruAvailable();
257-
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
258255

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

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

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { TelemetryService } from "../external-services/telemetry-service";
66
import { Display } from "../display";
77
import { messages } from "../messages";
88
import { getErrorMessage, getErrorMessageWithStack } from "../utils";
9-
import { APEX_GURU_ENGINE_NAME, ApexGuruService } from "./apex-guru-service";
9+
import { APEX_GURU_ENGINE_NAME, ApexGuruAccess, ApexGuruAvailability, ApexGuruService } from "./apex-guru-service";
1010

1111
export class ApexGuruRunAction {
1212
private readonly taskWithProgressRunner: TaskWithProgressRunner;
@@ -33,6 +33,16 @@ export class ApexGuruRunAction {
3333
const startTime: number = Date.now();
3434

3535
try {
36+
const availability: ApexGuruAvailability = this.apexGuruService.getAvailability();
37+
if (availability.access !== ApexGuruAccess.ENABLED) {
38+
this.display.displayError(availability.message);
39+
this.telemetryService.sendCommandEvent(Constants.TELEM_APEX_GURU_FILE_ANALYSIS_NOT_ENABLED, {
40+
executedCommand: commandName,
41+
access: availability.access
42+
});
43+
return;
44+
}
45+
3646
progressReporter.reportProgress({
3747
message: messages.apexGuru.runningAnalysis
3848
});

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

Lines changed: 86 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@
55
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66
*/
77

8-
import {CodeLocation, Fix, Suggestion, Violation} from '../diagnostics';
8+
import {CodeLocation, Fix, normalizeViolation, 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,16 +25,43 @@ const RESPONSE_STATUS = {
2425
}
2526

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

34+
export type ApexGuruAvailability = {
35+
access: ApexGuruAccess,
36+
message: string
37+
}
38+
39+
export enum ApexGuruAccess {
40+
// In this case, ApexGuru scans are allowed
41+
ENABLED = "enabled",
42+
43+
// In this case, the org is eligible to be enabled, but an admin hasn't set the permissions yet, so we should still
44+
// show the scan button but then show a message with the instructions sent from the validate endpoint.
45+
ELIGIBLE = "eligible-but-not-enabled",
46+
47+
// In this case, the org is not eligible for ApexGuru at all, so we should not show the scan button at all.
48+
INELIGIBLE = "ineligible",
49+
50+
// In this case, the user has not authed into an org, so we should not show the scan button at all.
51+
NOT_AUTHED = "not-authed"
52+
}
53+
54+
const ACCESS_CHANGED_EVENT = "apexGuruAccessChanged";
55+
3156
export class LiveApexGuruService implements ApexGuruService {
3257
private readonly orgConnectionService: OrgConnectionService;
3358
private readonly fileHandler: FileHandler;
3459
private readonly logger: Logger;
3560
private readonly maxTimeoutSeconds: number;
3661
private readonly retryIntervalMillis: number;
62+
private readonly eventEmitter: EventEmitter = new EventEmitter();
63+
private availability?: ApexGuruAvailability;
64+
3765
constructor(
3866
orgConnectionService: OrgConnectionService,
3967
fileHandler: FileHandler,
@@ -45,14 +73,61 @@ export class LiveApexGuruService implements ApexGuruService {
4573
this.logger = logger;
4674
this.maxTimeoutSeconds = maxTimeoutSeconds;
4775
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+
});
81+
}
82+
83+
getAvailability(): ApexGuruAvailability {
84+
if (this.availability === undefined) {
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');
88+
}
89+
return this.availability;
90+
}
91+
92+
onAccessChange(callback: (access: ApexGuruAccess) => void): void {
93+
this.eventEmitter.addListener(ACCESS_CHANGED_EVENT, callback);
4894
}
4995

50-
async isApexGuruAvailable(): Promise<boolean> {
96+
async updateAvailability(): Promise<void> {
5197
if (!this.orgConnectionService.isAuthed()) {
52-
return false;
98+
this.setAvailability({
99+
access: ApexGuruAccess.NOT_AUTHED,
100+
message: messages.apexGuru.noOrgAuthed
101+
});
102+
return;
53103
}
104+
54105
const response: ApexGuruResponse = await this.request('GET', await this.getValidateEndpoint());
55-
return response.status === RESPONSE_STATUS.SUCCESS;
106+
107+
if (response.status === RESPONSE_STATUS.SUCCESS) {
108+
this.setAvailability({
109+
access: ApexGuruAccess.ENABLED,
110+
111+
// This message isn't used anywhere except for debugging purposes and it allows us to make message field
112+
// a string instead of a string | undefined.
113+
message: "ApexGuru access is enabled."
114+
});
115+
} else {
116+
this.setAvailability({
117+
access: response.status === RESPONSE_STATUS.FAILED ? ApexGuruAccess.ELIGIBLE : ApexGuruAccess.INELIGIBLE,
118+
119+
// There should always be a message on failed and error responses, but adding this here just in case
120+
message: response.message ?? `ApexGuru access is not enabled. Response: ${JSON.stringify(response)}`
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);
130+
}
56131
}
57132

58133
async scan(absFileToScan: string): Promise<Violation[]> {
@@ -63,7 +138,9 @@ export class LiveApexGuruService implements ApexGuruService {
63138
const payloadStr: string = decodeFromBase64(queryResponse.report);
64139
this.logger.debug(`ApexGuru Analysis completed for Request Id: ${requestId}\n\nDecoded Response Payload:\n${payloadStr}`);
65140
const apexGuruViolations: ApexGuruViolation[] = parsePayload(payloadStr);
66-
return apexGuruViolations.map(v => toViolation(v, absFileToScan));
141+
142+
const lineLengths: number[] = fileContent.split(/\r?\n/).map(l => l.length);
143+
return apexGuruViolations.map(v => toViolation(v, absFileToScan, lineLengths));
67144
}
68145

69146
private async initiateRequest(fileContent: string): Promise<string> {
@@ -149,7 +226,7 @@ export function parsePayload(payloadStr: string): ApexGuruViolation[] {
149226
}
150227
}
151228

152-
function toViolation(apexGuruViolation: ApexGuruViolation, file: string): Violation {
229+
function toViolation(apexGuruViolation: ApexGuruViolation, file: string, lineLengths: number[]): Violation {
153230
const codeAnalyzerViolation: Violation = {
154231
rule: apexGuruViolation.rule,
155232
engine: APEX_GURU_ENGINE_NAME,
@@ -168,7 +245,7 @@ function toViolation(apexGuruViolation: ApexGuruViolation, file: string): Violat
168245
return f;
169246
})
170247
};
171-
return codeAnalyzerViolation;
248+
return normalizeViolation(codeAnalyzerViolation, lineLengths);
172249
}
173250

174251
function addFile(apexGuruLocation: CodeLocation, filePath: string): CodeLocation {

src/lib/cli-commands.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,10 @@ export class CliCommandExecutorImpl implements CliCommandExecutor {
106106

107107
let childProcess: cp.ChildProcessWithoutNullStreams;
108108
try {
109-
childProcess = IS_WINDOWS ? cp.spawn(command, wrapArgsWithSpacesWithQuotes(args), {shell: true}) :
110-
cp.spawn(command, args);
109+
childProcess =
110+
IS_WINDOWS
111+
? cp.spawn(command, wrapArgsWithSpacesWithQuotes(args), {shell: true, env: {...process.env, NO_COLOR: '1'}})
112+
: cp.spawn(command, args, {env: {...process.env, NO_COLOR: '1'}});
111113
} catch (err) {
112114
this.logger.logAtLevel(vscode.LogLevel.Error, `Failed to execute the following command:\n` +
113115
indent(`${command} ${wrapArgsWithSpacesWithQuotes(args).join(' ')}`) + `\n\n` +

0 commit comments

Comments
 (0)