Skip to content

Commit 7039283

Browse files
committed
Update to the latest gRPC A3S proto spec
1 parent a2d1e57 commit 7039283

7 files changed

Lines changed: 202 additions & 115 deletions

File tree

packages/grpc/src/proto/language_analyzer.proto

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,26 +9,54 @@ package analyzer;
99
// Language Analyzer Service
1010
service LanguageAnalyzerService {
1111
// Analyze a file
12-
rpc AnalyzeFile(AnalyzeFileRequest) returns (AnalyzeFileResponse);
12+
rpc Analyze(AnalyzeRequest) returns (AnalyzeResponse);
1313
}
1414

1515
// Request to analyze a file
16-
message AnalyzeFileRequest {
17-
map<string, string> context_ids = 1; // Map of context ID by kind
18-
repeated SourceFile source_files = 2; // List of source files to analyze
19-
repeated ActiveRule active_rules = 3; // List of active rules
16+
message AnalyzeRequest {
17+
string analysis_id = 1; // A unique UUID identifier for each analysis
18+
map<string, string> context_ids = 2; // Map of context ID by kind
19+
repeated SourceFile source_files = 3; // List of source files to analyze
20+
repeated ActiveRule active_rules = 4; // List of active rules
2021
}
2122

22-
// Represents a source file
23+
// Represents a source file to be analyzed.
24+
//
25+
// relative_path: the relative path of the file
26+
// content: the content of the file
27+
// file_scope: the scope of the file (MAIN or TEST). This is optional.
28+
// When provided, it takes precedence over any scope deduced from the context.
29+
// When not provided, the scope should be deduced from the context if available.
30+
// When neither is provided, the analyzer should perform best effort to determine the scope.
2331
message SourceFile {
24-
string relative_path = 1; // The repo-relative path and filename
25-
string content = 2; // The full content of the file
32+
string relative_path = 1;
33+
string content = 2;
34+
optional FileScope file_scope = 3;
35+
}
36+
37+
// Enum for file scope
38+
enum FileScope {
39+
MAIN = 0;
40+
TEST = 1;
2641
}
2742

2843
// Response containing analysis results
29-
message AnalyzeFileResponse {
44+
message AnalyzeResponse {
3045
repeated Issue issues = 1;
31-
repeated string analysis_problems = 2;
46+
repeated AnalysisProblem analysis_problems = 2;
47+
}
48+
49+
// Represents a problem encountered during analysis
50+
message AnalysisProblem {
51+
AnalysisProblemType type = 1; // Type of the problem
52+
string message = 2; // Description of the problem
53+
string file_path = 3; // Optional path to the file where the problem occurred
54+
}
55+
56+
// Enum for analysis problem types
57+
enum AnalysisProblemType {
58+
ANALYSIS_PROBLEM_TYPE_UNDEFINED = 0;
59+
ANALYSIS_PROBLEM_TYPE_PARSING = 1;
3260
}
3361

3462
// Represents an active rule with configuration
@@ -61,9 +89,18 @@ message TextRange {
6189
int32 end_offset = 4; // 0-indexed character offset
6290
}
6391

92+
// Flow type enumeration
93+
enum FlowType {
94+
FLOW_TYPE_UNDEFINED = 0;
95+
FLOW_TYPE_DATA = 1;
96+
FLOW_TYPE_EXECUTION = 2;
97+
}
98+
6499
// Represents a flow (sequence of locations)
65100
message Flow {
66-
repeated FlowLocation locations = 1;
101+
FlowType type = 1;
102+
string description = 2;
103+
repeated FlowLocation locations = 3;
67104
}
68105

69106
// Represents a location in a flow

packages/grpc/src/server.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,16 @@ const HEALTH_SERVICE_NAME = 'grpc.health.v1.Health';
2929
*/
3030
function createAnalyzerServiceDefinition(): grpc.ServiceDefinition {
3131
return {
32-
AnalyzeFile: {
33-
path: `/${ANALYZER_SERVICE_NAME}/AnalyzeFile`,
32+
Analyze: {
33+
path: `/${ANALYZER_SERVICE_NAME}/Analyze`,
3434
requestStream: false,
3535
responseStream: false,
36-
requestSerialize: (value: analyzer.IAnalyzeFileRequest) =>
37-
Buffer.from(analyzer.AnalyzeFileRequest.encode(value).finish()),
38-
requestDeserialize: (buffer: Buffer) => analyzer.AnalyzeFileRequest.decode(buffer),
39-
responseSerialize: (value: analyzer.IAnalyzeFileResponse) =>
40-
Buffer.from(analyzer.AnalyzeFileResponse.encode(value).finish()),
41-
responseDeserialize: (buffer: Buffer) => analyzer.AnalyzeFileResponse.decode(buffer),
36+
requestSerialize: (value: analyzer.IAnalyzeRequest) =>
37+
Buffer.from(analyzer.AnalyzeRequest.encode(value).finish()),
38+
requestDeserialize: (buffer: Buffer) => analyzer.AnalyzeRequest.decode(buffer),
39+
responseSerialize: (value: analyzer.IAnalyzeResponse) =>
40+
Buffer.from(analyzer.AnalyzeResponse.encode(value).finish()),
41+
responseDeserialize: (buffer: Buffer) => analyzer.AnalyzeResponse.decode(buffer),
4242
},
4343
};
4444
}
@@ -72,9 +72,9 @@ export function createGrpcServer(): grpc.Server {
7272

7373
const analyzerServiceDefinition = createAnalyzerServiceDefinition();
7474
const analyzerImplementation: grpc.UntypedServiceImplementation = {
75-
AnalyzeFile: (
76-
call: grpc.ServerUnaryCall<analyzer.IAnalyzeFileRequest, analyzer.IAnalyzeFileResponse>,
77-
callback: grpc.sendUnaryData<analyzer.IAnalyzeFileResponse>,
75+
Analyze: (
76+
call: grpc.ServerUnaryCall<analyzer.IAnalyzeRequest, analyzer.IAnalyzeResponse>,
77+
callback: grpc.sendUnaryData<analyzer.IAnalyzeResponse>,
7878
) => {
7979
analyzeFileHandler(call, callback);
8080
},

packages/grpc/src/service.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,18 @@ import { analyzeProject } from '../../jsts/src/analysis/projectAnalysis/analyzeP
2424
import { info, error as logError } from '../../shared/src/helpers/logging.js';
2525

2626
/**
27-
* gRPC handler for the AnalyzeFile RPC
27+
* gRPC handler for the Analyze RPC
2828
*/
2929
export async function analyzeFileHandler(
30-
call: grpc.ServerUnaryCall<analyzer.IAnalyzeFileRequest, analyzer.IAnalyzeFileResponse>,
31-
callback: grpc.sendUnaryData<analyzer.IAnalyzeFileResponse>,
30+
call: grpc.ServerUnaryCall<analyzer.IAnalyzeRequest, analyzer.IAnalyzeResponse>,
31+
callback: grpc.sendUnaryData<analyzer.IAnalyzeResponse>,
3232
): Promise<void> {
3333
const request = call.request;
3434

3535
try {
36-
info(`Received AnalyzeFile request with ${request.sourceFiles?.length ?? 0} files`);
36+
info(
37+
`Received Analyze request (${request.analysisId ?? 'no id'}) with ${request.sourceFiles?.length ?? 0} files`,
38+
);
3739

3840
const projectInput = transformRequestToProjectInput(request);
3941

@@ -45,7 +47,7 @@ export async function analyzeFileHandler(
4547
callback(null, response);
4648
} catch (err) {
4749
const errorMessage = err instanceof Error ? err.message : String(err);
48-
logError(`AnalyzeFile error: ${errorMessage}`);
50+
logError(`Analyze error: ${errorMessage}`);
4951
callback({
5052
code: grpc.status.INTERNAL,
5153
message: errorMessage,

packages/grpc/src/transformers/request.ts

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* You should have received a copy of the Sonar Source-Available License
1515
* along with this program; if not, see https://sonarsource.com/license/ssal/
1616
*/
17-
import type { analyzer } from '../proto/language_analyzer.js';
17+
import { analyzer } from '../proto/language_analyzer.js';
1818
import type {
1919
ProjectAnalysisInput,
2020
JsTsFiles,
@@ -75,12 +75,13 @@ function parseParamValue(value: string, defaultValue: unknown) {
7575
/**
7676
* Transform source files from gRPC protobuf format to the internal JsTsFiles format.
7777
*
78-
* The gRPC request contains an array of `ISourceFile` objects with `relativePath` and `content`.
79-
* This function converts them into a dictionary keyed by relative path, which is the format
80-
* expected by the `analyzeProject` function.
78+
* The gRPC request contains an array of `ISourceFile` objects with `relativePath`, `content`,
79+
* and optionally `fileScope`. This function converts them into a dictionary keyed by relative
80+
* path, which is the format expected by the `analyzeProject` function.
8181
*
82-
* Note: File type is defaulted to 'MAIN'. In the future, this could be extended to accept
83-
* file type metadata from the gRPC request to properly distinguish test files.
82+
* File scope handling:
83+
* - If fileScope is explicitly set in the request, use it (MAIN or TEST)
84+
* - Otherwise, default to 'MAIN' (context-based inference could be added in the future)
8485
*
8586
* @param sourceFiles - Array of source files from the gRPC request
8687
* @returns Dictionary of files keyed by relative path
@@ -90,10 +91,18 @@ function transformSourceFiles(sourceFiles: analyzer.ISourceFile[]): JsTsFiles {
9091

9192
for (const sourceFile of sourceFiles) {
9293
const relativePath = sourceFile.relativePath ?? '';
94+
let fileType: FileType = 'MAIN';
95+
96+
if (sourceFile.fileScope !== null && sourceFile.fileScope !== undefined) {
97+
fileType = sourceFile.fileScope === analyzer.FileScope.TEST ? 'TEST' : 'MAIN';
98+
} else {
99+
// TODO: Infer file scope from context when not explicitly provided by the caller
100+
}
101+
93102
files[relativePath] = {
94103
filePath: relativePath,
95104
fileContent: sourceFile.content ?? '',
96-
fileType: 'MAIN', // Default to MAIN, we will need metadata from context to know for sure
105+
fileType,
97106
};
98107
}
99108

@@ -365,7 +374,7 @@ function transformActiveRules(activeRules: analyzer.IActiveRule[]): RuleConfig[]
365374
}
366375

367376
/**
368-
* Transform a gRPC AnalyzeFileRequest into the ProjectAnalysisInput format.
377+
* Transform a gRPC AnalyzeRequest into the ProjectAnalysisInput format.
369378
*
370379
* This is the main entry point for request transformation in the gRPC workflow.
371380
* It converts the protobuf-based request format into the internal format expected
@@ -378,18 +387,18 @@ function transformActiveRules(activeRules: analyzer.IActiveRule[]): RuleConfig[]
378387
*
379388
* **Transformation flow:**
380389
* ```
381-
* IAnalyzeFileRequest
390+
* IAnalyzeRequest
382391
* ├── sourceFiles[] ──→ transformSourceFiles() ──→ JsTsFiles (keyed by path)
383392
* └── activeRules[] ──→ transformActiveRules() ──→ RuleConfig[] (one per rule+language)
384393
* ```
385394
*
386-
* @param request - The gRPC AnalyzeFileRequest containing source files and active rules
395+
* @param request - The gRPC AnalyzeRequest containing source files and active rules
387396
* @returns ProjectAnalysisInput ready to pass to analyzeProject()
388397
*
389398
* @see docs/DEV.md "External workflow (gRPC - without SonarQube)" section
390399
*/
391400
export function transformRequestToProjectInput(
392-
request: analyzer.IAnalyzeFileRequest,
401+
request: analyzer.IAnalyzeRequest,
393402
): ProjectAnalysisInput {
394403
// Handle empty/undefined arrays from proto3
395404
const sourceFiles = request.sourceFiles || [];

packages/grpc/src/transformers/response.ts

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* You should have received a copy of the Sonar Source-Available License
1515
* along with this program; if not, see https://sonarsource.com/license/ssal/
1616
*/
17-
import type { analyzer } from '../proto/language_analyzer.js';
17+
import { analyzer } from '../proto/language_analyzer.js';
1818
import type { ProjectAnalysisOutput } from '../../../jsts/src/analysis/projectAnalysis/projectAnalysis.js';
1919
import type { Issue } from '../../../jsts/src/linter/issues/issue.js';
2020

@@ -41,7 +41,7 @@ const PARSING_ERROR_RULE_KEY = 'S2260';
4141
*
4242
* **Secondary Locations:**
4343
* Secondary locations provide additional context for issues (e.g., "this variable was declared here").
44-
* They are grouped into a single flow in the gRPC format.
44+
* They are grouped into a single flow in the gRPC format with type FLOW_TYPE_DATA.
4545
*
4646
* @param issue - Internal Issue object from the linter
4747
* @returns gRPC IIssue object ready for protobuf serialization
@@ -57,7 +57,6 @@ function transformIssue(issue: Issue): analyzer.IIssue {
5757
// Transform secondary locations into flows
5858
const flows: analyzer.IFlow[] = [];
5959
if (issue.secondaryLocations && issue.secondaryLocations.length > 0) {
60-
// Group secondary locations into a single flow
6160
const locations: analyzer.IFlowLocation[] = issue.secondaryLocations.map(loc => ({
6261
textRange: {
6362
startLine: loc.line,
@@ -69,7 +68,11 @@ function transformIssue(issue: Issue): analyzer.IIssue {
6968
file: issue.filePath,
7069
}));
7170

72-
flows.push({ locations });
71+
flows.push({
72+
type: analyzer.FlowType.FLOW_TYPE_DATA,
73+
description: '',
74+
locations,
75+
});
7376
}
7477

7578
return {
@@ -83,7 +86,7 @@ function transformIssue(issue: Issue): analyzer.IIssue {
8386
}
8487

8588
/**
86-
* Transform the ProjectAnalysisOutput into a gRPC AnalyzeFileResponse.
89+
* Transform the ProjectAnalysisOutput into a gRPC AnalyzeResponse.
8790
*
8891
* This is the main entry point for response transformation in the gRPC workflow.
8992
* It processes the analysis results for all files and converts them into the
@@ -94,41 +97,58 @@ function transformIssue(issue: Issue): analyzer.IIssue {
9497
* result can be one of three types:
9598
*
9699
* 1. **Error** (`'error' in result`): Analysis failed for this file
97-
* - Added to `analysisProblems` array with file path context
100+
* - Added to `analysisProblems` array with UNDEFINED type
98101
*
99102
* 2. **Parsing Error** (`'parsingError' in result`): File could not be parsed
100-
* - Converted to an issue with rule S2260 (parsing error rule)
101-
* - Includes the error message and line number
103+
* - Added to `analysisProblems` array with PARSING type
104+
* - Also converted to an issue with rule S2260
102105
*
103106
* 3. **Success** (`'issues' in result`): Analysis completed successfully
104107
* - All issues are transformed and added to the response
105108
*
106109
* **Response Structure:**
107110
* ```
108-
* IAnalyzeFileResponse
111+
* IAnalyzeResponse
109112
* ├── issues[] ──────────── All issues from successful analyses + parsing errors
110-
* └── analysisProblems[] ── Warnings from meta + error messages for failed files
113+
* └── analysisProblems[] ── Structured problems with type, message, and file path
111114
* ```
112115
*
113116
* @param output - The ProjectAnalysisOutput from analyzeProject()
114-
* @returns gRPC IAnalyzeFileResponse ready for protobuf serialization
117+
* @returns gRPC IAnalyzeResponse ready for protobuf serialization
115118
*/
116119
export function transformProjectOutputToResponse(
117120
output: ProjectAnalysisOutput,
118-
): analyzer.IAnalyzeFileResponse {
121+
): analyzer.IAnalyzeResponse {
119122
const issues: analyzer.IIssue[] = [];
120-
const analysisProblems: string[] = [...output.meta.warnings];
123+
const analysisProblems: analyzer.IAnalysisProblem[] = [];
124+
125+
for (const warning of output.meta.warnings) {
126+
analysisProblems.push({
127+
type: analyzer.AnalysisProblemType.ANALYSIS_PROBLEM_TYPE_UNDEFINED,
128+
message: warning,
129+
filePath: '',
130+
});
131+
}
121132

122-
// Process each file result
123133
for (const [filePath, fileResult] of Object.entries(output.files)) {
124134
if ('error' in fileResult) {
125-
analysisProblems.push(`Error analyzing ${filePath}: ${fileResult.error}`);
135+
analysisProblems.push({
136+
type: analyzer.AnalysisProblemType.ANALYSIS_PROBLEM_TYPE_UNDEFINED,
137+
message: fileResult.error,
138+
filePath,
139+
});
126140
continue;
127141
}
128142

129143
if ('parsingError' in fileResult) {
130-
// Report parsing errors as issues with rule S2260
131144
const { message, line } = fileResult.parsingError;
145+
146+
analysisProblems.push({
147+
type: analyzer.AnalysisProblemType.ANALYSIS_PROBLEM_TYPE_PARSING,
148+
message,
149+
filePath,
150+
});
151+
132152
issues.push(
133153
transformIssue({
134154
ruleId: PARSING_ERROR_RULE_KEY,
@@ -144,7 +164,6 @@ export function transformProjectOutputToResponse(
144164
continue;
145165
}
146166

147-
// Extract issues from successful analysis
148167
if ('issues' in fileResult) {
149168
for (const issue of fileResult.issues) {
150169
issues.push(transformIssue(issue));

0 commit comments

Comments
 (0)