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' ;
99import { Logger } from "../logger" ;
1010import { 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' ;
1212import { FileHandler } from '../fs-utils' ;
1313import { messages } from '../messages' ;
14+ import { EventEmitter } from 'node:stream' ;
1415
1516export const APEX_GURU_ENGINE_NAME : string = 'apexguru' ;
1617const APEX_GURU_MAX_TIMEOUT_SECONDS = 60 ;
@@ -24,16 +25,43 @@ const RESPONSE_STATUS = {
2425}
2526
2627export 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+
3156export 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
174251function addFile ( apexGuruLocation : CodeLocation , filePath : string ) : CodeLocation {
0 commit comments