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 * as Constants from '../constants' ;
9- import { CodeLocation , Violation } from '../diagnostics' ;
8+ import { CodeLocation , Fix , Suggestion , Violation } from '../diagnostics' ;
109import { Logger } from "../logger" ;
1110import { getErrorMessage , indent } from '../utils' ;
1211import { HttpMethods , HttpRequest , OrgConnectionService } from '../external-services/org-connection-service' ;
1312import { FileHandler } from '../fs-utils' ;
1413import { messages } from '../messages' ;
1514
1615export const APEX_GURU_ENGINE_NAME : string = 'apexguru' ;
16+ const APEX_GURU_MAX_TIMEOUT_SECONDS = 60 ;
17+ const APEX_GURU_RETRY_INTERVAL_MILLIS = 1000 ;
1718
1819const RESPONSE_STATUS = {
1920 NEW : "new" ,
@@ -37,8 +38,8 @@ export class LiveApexGuruService implements ApexGuruService {
3738 orgConnectionService : OrgConnectionService ,
3839 fileHandler : FileHandler ,
3940 logger : Logger ,
40- maxTimeoutSeconds : number = Constants . APEX_GURU_MAX_TIMEOUT_SECONDS ,
41- retryIntervalMillis : number = Constants . APEX_GURU_RETRY_INTERVAL_MILLIS ) {
41+ maxTimeoutSeconds : number = APEX_GURU_MAX_TIMEOUT_SECONDS ,
42+ retryIntervalMillis : number = APEX_GURU_RETRY_INTERVAL_MILLIS ) {
4243 this . orgConnectionService = orgConnectionService ;
4344 this . fileHandler = fileHandler ;
4445 this . logger = logger ;
@@ -50,7 +51,7 @@ export class LiveApexGuruService implements ApexGuruService {
5051 if ( ! this . orgConnectionService . isAuthed ( ) ) {
5152 return false ;
5253 }
53- const response : ApexGuruResponse = await this . request ( 'GET' , Constants . APEX_GURU_VALIDATE_ENDPOINT ) ;
54+ const response : ApexGuruResponse = await this . request ( 'GET' , await this . getValidateEndpoint ( ) ) ;
5455 return response . status === RESPONSE_STATUS . SUCCESS ;
5556 }
5657
@@ -59,16 +60,22 @@ export class LiveApexGuruService implements ApexGuruService {
5960 const requestId = await this . initiateRequest ( fileContent ) ;
6061 this . logger . debug ( `Initialized ApexGuru Analysis with Request Id: ${ requestId } ` ) ;
6162 const queryResponse : ApexGuruQueryResponse = await this . waitForResponse ( requestId ) ;
62- this . logger . debug ( `ApexGuru Analysis completed for Request Id: ${ requestId } ` ) ;
63- const reports : ApexGuruReport [ ] = toReportArray ( queryResponse ) ;
64- return reports . map ( r => toViolation ( r , absFileToScan ) ) ;
63+ const payloadStr : string = decodeFromBase64 ( queryResponse . report ) ;
64+ this . logger . debug ( `ApexGuru Analysis completed for Request Id: ${ requestId } \n\nDecoded Response Payload:\n${ payloadStr } ` ) ;
65+ const apexGuruViolations : ApexGuruViolation [ ] = parsePayload ( payloadStr ) ;
66+ return apexGuruViolations . map ( v => toViolation ( v , absFileToScan ) ) ;
6567 }
6668
6769 private async initiateRequest ( fileContent : string ) : Promise < string > {
68- const base64EncodedContent = Buffer . from ( fileContent ) . toString ( 'base64' ) ;
69- const response : ApexGuruInitialResponse = await this . request ( 'POST' , Constants . APEX_GURU_REQUEST_ENDPOINT ,
70- JSON . stringify ( { classContent : base64EncodedContent } ) ) ;
71- if ( ! response . requestId || response . status != RESPONSE_STATUS . NEW ) {
70+ const requestBody : ApexGuruRequestBody = {
71+ classContent : encodeToBase64 ( fileContent )
72+ } ;
73+ const response : ApexGuruInitialResponse = await this . request ( 'POST' , await this . getRequestEndpoint ( ) ,
74+ JSON . stringify ( requestBody ) ) ;
75+
76+ if ( response . status == RESPONSE_STATUS . FAILED ) {
77+ throw new Error ( messages . apexGuru . errors . unableToAnalyzeFile ( response . message ?? '' ) ) ;
78+ } else if ( ! response . requestId || response . status != RESPONSE_STATUS . NEW ) {
7279 throw Error ( messages . apexGuru . errors . returnedUnexpectedResponse ( JSON . stringify ( response , null , 2 ) ) ) ;
7380 }
7481 return response . requestId ;
@@ -81,11 +88,11 @@ export class LiveApexGuruService implements ApexGuruService {
8188 if ( queryResponse ) { // After the first attempt, we pause each time between requests
8289 await new Promise ( resolve => setTimeout ( resolve , this . retryIntervalMillis ) ) ;
8390 }
84- queryResponse = await this . request ( 'GET' , ` ${ Constants . APEX_GURU_REQUEST_ENDPOINT } / ${ requestId } ` ) ;
91+ queryResponse = await this . request ( 'GET' , await this . getRequestEndpoint ( requestId ) ) ;
8592 if ( queryResponse . status === RESPONSE_STATUS . SUCCESS && queryResponse . report ) {
8693 return queryResponse ;
87- } else if ( queryResponse . status === RESPONSE_STATUS . FAILED ) { // TODO: I would love a failure message - but the response's report just gives back the file content and nothing else.
88- throw new Error ( messages . apexGuru . errors . unableToAnalyzeFile ) ;
94+ } else if ( queryResponse . status === RESPONSE_STATUS . FAILED ) {
95+ throw new Error ( messages . apexGuru . errors . unableToAnalyzeFile ( queryResponse . message ?? '' ) ) ;
8996 } else if ( queryResponse . status === RESPONSE_STATUS . ERROR && queryResponse . message ) {
9097 throw new Error ( messages . apexGuru . errors . returnedUnexpectedError ( queryResponse . message ) ) ;
9198 }
@@ -117,73 +124,73 @@ export class LiveApexGuruService implements ApexGuruService {
117124 this . logger . trace ( 'Call to ApexGuru Service failed:' + getErrorMessage ( err ) ) ;
118125 return {
119126 status : RESPONSE_STATUS . ERROR ,
120- message : getErrorMessage ( err )
127+ message : getErrorMessage ( err ) ,
121128 } as T ;
122129 }
123130 }
131+
132+ private async getValidateEndpoint ( ) : Promise < string > {
133+ const apiVersion : string = await this . orgConnectionService . getApiVersion ( ) ;
134+ return `/services/data/v${ apiVersion } /apexguru/validate` ;
135+ }
136+
137+ private async getRequestEndpoint ( requestId ?: string ) : Promise < string > {
138+ const apiVersion : string = await this . orgConnectionService . getApiVersion ( ) ;
139+ return `/services/data/v${ apiVersion } /apexguru/request` + ( requestId ? `/${ requestId } ` : '' ) ;
140+ }
124141}
125142
126143
127- export function toReportArray ( response : ApexGuruQueryResponse ) : ApexGuruReport [ ] {
128- // TODO: This will change soon enough - once we receive the actual response
129- const report : string = Buffer . from ( response . report , 'base64' ) . toString ( 'utf-8' ) ;
144+ export function parsePayload ( payloadStr : string ) : ApexGuruViolation [ ] {
130145 try {
131- return JSON . parse ( report ) as ApexGuruReport [ ] ;
146+ return JSON . parse ( payloadStr ) as ApexGuruViolation [ ] ;
132147 } catch ( err ) {
133- throw new Error ( `Unable to parse response from ApexGuru.\n\n` +
134- `Error:\n${ indent ( getErrorMessage ( err ) ) } \n\nDecoded report:\n${ indent ( report ) } ` ) ;
148+ throw new Error ( messages . apexGuru . errors . unableToParsePayload ( indent ( getErrorMessage ( err ) ) ) ) ;
135149 }
136150}
137151
138- function toViolation ( parsed : ApexGuruReport , file : string ) : Violation {
139- // IMPORTANT: AS OF 08/13/2024 THIS ALL FAILS IN PRODUCTION BECAUSE THE NEW ApexGuru Service NOW HAS A NEW
140- // RESPONSE. TODO: W-19053527
141- const encodedCodeAfter = parsed . properties . find ( ( prop : ApexGuruProperty ) => prop . name === 'code_after' ) ?. value
142- ?? parsed . properties . find ( ( prop : ApexGuruProperty ) => prop . name === 'class_after' ) ?. value
143- ?? '' ;
144- const suggestedCode : string = Buffer . from ( encodedCodeAfter , 'base64' ) . toString ( 'utf8' ) ;
145-
146- const lineNumber = parseInt ( parsed . properties . find ( ( prop : ApexGuruProperty ) => prop . name === 'line_number' ) ?. value ) ;
147-
148- const violationLocation : CodeLocation = {
149- file : file ,
150- startLine : lineNumber ,
151- startColumn : 1
152+ function toViolation ( apexGuruViolation : ApexGuruViolation , file : string ) : Violation {
153+ const codeAnalyzerViolation : Violation = {
154+ rule : apexGuruViolation . rule ,
155+ engine : APEX_GURU_ENGINE_NAME ,
156+ message : apexGuruViolation . message ,
157+ severity : apexGuruViolation . severity ,
158+ locations : apexGuruViolation . locations . map ( l => addFile ( l , file ) ) ,
159+ primaryLocationIndex : apexGuruViolation . primaryLocationIndex ,
160+ tags : [ ] , // Currently not used
161+ resources : apexGuruViolation . resources ?? [ ] ,
162+ suggestions : apexGuruViolation . suggestions ?. map ( s => {
163+ s . location = addFile ( s . location , file ) ;
164+ return s ;
165+ } ) ,
166+ fixes : apexGuruViolation . fixes ?. map ( f => {
167+ f . location = addFile ( f . location , file ) ;
168+ return f ;
169+ } )
152170 } ;
171+ return codeAnalyzerViolation ;
172+ }
153173
154- const violation : Violation = {
155- rule : parsed . type ,
156- engine : APEX_GURU_ENGINE_NAME ,
157- message : parsed . value ,
158- severity : 1 , // TODO: Should this really be critical level violation? This seems off.
159- locations : [ violationLocation ] ,
160- primaryLocationIndex : 0 ,
161- tags : [ ] ,
162- resources : [
163- 'https://help.salesforce.com/s/articleView?id=sf.apexguru_antipatterns.htm&type=5'
164- ]
174+ function addFile ( apexGuruLocation : CodeLocation , filePath : string ) : CodeLocation {
175+ return {
176+ ...apexGuruLocation ,
177+ file : filePath
165178 } ;
179+ }
166180
167- // TODO: Soon we'll be receiving a different looking payload which will help us differentiate between fixes and suggestions.
168- // For now, we are going to treat suggestedCode as a fix and a suggestion (as the current pilot code does)
169- if ( suggestedCode . length > 0 ) {
170- violation . fixes = [
171- {
172- location : violationLocation ,
173- fixedCode : `/*\n//ApexGuru Suggestions: \n${ suggestedCode } \n*/`
174- }
175- ]
176- violation . suggestions = [
177- {
178- location : violationLocation ,
179- // This message is temporary and will be improved as we get a better response back and unify the suggestions experience
180- message : suggestedCode
181- }
182- ]
183- }
184- return violation ;
181+ function encodeToBase64 ( value : string ) : string {
182+ return Buffer . from ( value ) . toString ( 'base64' ) ;
185183}
186184
185+ function decodeFromBase64 ( value : string ) : string {
186+ return Buffer . from ( value , 'base64' ) . toString ( 'utf-8' ) ;
187+ }
188+
189+
190+ type ApexGuruRequestBody = {
191+ // Must be base64 encoded
192+ classContent : string
193+ }
187194
188195type ApexGuruResponse = {
189196 status : string ;
@@ -195,17 +202,21 @@ type ApexGuruInitialResponse = ApexGuruResponse & {
195202}
196203
197204type ApexGuruQueryResponse = ApexGuruResponse & {
205+ // Is returned with base64 encoding
198206 report : string ;
199207}
200208
201- type ApexGuruProperty = {
202- name : string ;
203- value : string ;
204- } ;
209+ type ApexGuruViolation = {
210+ rule : string ;
211+ message : string ;
212+
213+ // Note that none of these location objects from ApexGuru will have a "file" field on it
214+ locations : CodeLocation [ ] ;
215+ primaryLocationIndex : number ;
216+ severity : number ;
217+ resources : string [ ] ;
205218
206- type ApexGuruReport = {
207- id : string ;
208- type : string ;
209- value : string ;
210- properties : ApexGuruProperty [ ] ;
219+ // Note that each suggestion and fix location from ApexGuru will not have a "file" field on it
220+ suggestions ?: Suggestion [ ] ;
221+ fixes ?: Fix [ ] ;
211222}
0 commit comments