Skip to content

Commit b5b7de7

Browse files
authored
SC-43473 Handle file-level issues in gRPC like AnalysisProcessor (#6572)
1 parent ec4ff13 commit b5b7de7

4 files changed

Lines changed: 147 additions & 49 deletions

File tree

packages/grpc/src/proto/language_analyzer.proto

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ package analyzer;
88

99
// Language Analyzer Service
1010
service LanguageAnalyzerService {
11-
// Analyze a file
11+
// Analyzes one or more source files in the context of a single analysis job.
12+
// A single request may contain multiple source files (multi-file analysis).
13+
// This RPC may be called concurrently by multiple clients — implementations must be thread-safe.
1214
rpc Analyze(AnalyzeRequest) returns (AnalyzeResponse);
1315
}
1416

@@ -28,10 +30,13 @@ message AnalyzeRequest {
2830
// When provided, it takes precedence over any scope deduced from the context.
2931
// When not provided, the scope should be deduced from the context if available.
3032
// When neither is provided, the analyzer should perform best effort to determine the scope.
33+
// sonar_language: the language key of the file (e.g., "java", "js", "ts"), following Sonar plugins definition. This is optional but recommended
34+
// for proper analysis. When not provided, the analyzer may attempt to detect the language.
3135
message SourceFile {
3236
string relative_path = 1;
3337
string content = 2;
3438
optional FileScope file_scope = 3;
39+
optional string sonar_language = 4;
3540
}
3641

3742
// Enum for file scope
@@ -47,6 +52,13 @@ message AnalyzeResponse {
4752
repeated FileMeasures measures = 3;
4853
}
4954

55+
// Represents a problem encountered during analysis
56+
message AnalysisProblem {
57+
AnalysisProblemType type = 1; // Type of the problem
58+
string message = 2; // Description of the problem
59+
string file_path = 3; // Optional path to the file where the problem occurred
60+
}
61+
5062
// Represents measures for a file
5163
message FileMeasures {
5264
string file_path = 1;
@@ -65,13 +77,6 @@ message Measure {
6577
}
6678
}
6779

68-
// Represents a problem encountered during analysis
69-
message AnalysisProblem {
70-
AnalysisProblemType type = 1; // Type of the problem
71-
string message = 2; // Description of the problem
72-
string file_path = 3; // Optional path to the file where the problem occurred
73-
}
74-
7580
// Enum for analysis problem types
7681
enum AnalysisProblemType {
7782
ANALYSIS_PROBLEM_TYPE_UNDEFINED = 0;
@@ -132,5 +137,5 @@ message Flow {
132137
message FlowLocation {
133138
TextRange text_range = 1;
134139
string message = 2; // Description of the step
135-
string file = 3; // File path where this location exists
140+
string file_path = 3; // File path where this location exists
136141
}

packages/grpc/src/transformers/response.ts

Lines changed: 72 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,34 @@ import type { NormalizedAbsolutePath } from '../../../jsts/src/rules/helpers/fil
3030
*/
3131
const PARSING_ERROR_RULE_KEY = 'S2260';
3232

33+
function isValidOneBasedLine(line: number): line is number {
34+
return line >= 1;
35+
}
36+
37+
function toTextRange(
38+
line: number,
39+
column: number,
40+
endLine: number | undefined,
41+
endColumn: number | undefined,
42+
): analyzer.ITextRange | undefined {
43+
if (!isValidOneBasedLine(line)) {
44+
return undefined;
45+
}
46+
47+
const resolvedEndLine = endLine ?? line;
48+
if (!isValidOneBasedLine(resolvedEndLine)) {
49+
return undefined;
50+
}
51+
52+
const startLineOffset = column ?? 0;
53+
return {
54+
startLine: line,
55+
startLineOffset,
56+
endLine: resolvedEndLine,
57+
endLineOffset: endColumn ?? startLineOffset,
58+
};
59+
}
60+
3361
/**
3462
* Transform a single Issue from the internal format to the gRPC Issue format.
3563
*
@@ -52,32 +80,31 @@ const PARSING_ERROR_RULE_KEY = 'S2260';
5280
* @returns gRPC Issue object ready for protobuf serialization
5381
*/
5482
function transformIssue(issue: JsTsIssue): analyzer.IIssue {
55-
const textRange: analyzer.ITextRange = {
56-
startLine: issue.line,
57-
startLineOffset: issue.column,
58-
endLine: issue.endLine ?? issue.line,
59-
endLineOffset: issue.endColumn ?? issue.column,
60-
};
83+
const textRange = toTextRange(issue.line, issue.column, issue.endLine, issue.endColumn);
6184

6285
// Transform secondary locations into flows
6386
const flows: analyzer.IFlow[] = [];
6487
if (issue.secondaryLocations && issue.secondaryLocations.length > 0) {
65-
const locations: analyzer.IFlowLocation[] = issue.secondaryLocations.map(loc => ({
66-
textRange: {
67-
startLine: loc.line,
68-
startLineOffset: loc.column,
69-
endLine: loc.endLine,
70-
endLineOffset: loc.endColumn,
71-
},
72-
message: loc.message ?? '',
73-
file: issue.filePath,
74-
}));
75-
76-
flows.push({
77-
type: analyzer.FlowType.FLOW_TYPE_DATA,
78-
description: '',
79-
locations,
80-
});
88+
const locations: analyzer.IFlowLocation[] = [];
89+
90+
for (const loc of issue.secondaryLocations) {
91+
const range = toTextRange(loc.line, loc.column, loc.endLine, loc.endColumn);
92+
if (range !== undefined) {
93+
locations.push({
94+
textRange: range,
95+
message: loc.message ?? '',
96+
filePath: issue.filePath,
97+
});
98+
}
99+
}
100+
101+
if (locations.length > 0) {
102+
flows.push({
103+
type: analyzer.FlowType.FLOW_TYPE_DATA,
104+
description: '',
105+
locations,
106+
});
107+
}
81108
}
82109

83110
const repo = issue.language === 'js' ? 'javascript' : 'typescript';
@@ -112,12 +139,27 @@ function transformCssIssue(issue: CssIssue, filePath: string): analyzer.IIssue {
112139
filePath,
113140
message: issue.message,
114141
rule: { repo: 'css', rule: sqKey },
115-
textRange: {
116-
startLine: issue.line,
117-
startLineOffset: issue.column,
118-
endLine: issue.endLine ?? issue.line,
119-
endLineOffset: issue.endColumn ?? issue.column,
120-
},
142+
textRange: toTextRange(issue.line, issue.column, issue.endLine, issue.endColumn),
143+
flows: [],
144+
};
145+
}
146+
147+
/**
148+
* Transform a parsing error into a gRPC issue.
149+
*
150+
* Mirrors Java-side behavior: when line is missing, the issue is file-level
151+
* and does not carry a text range.
152+
*/
153+
function transformParsingErrorIssue(
154+
message: string,
155+
filePath: NormalizedAbsolutePath,
156+
line: number | undefined,
157+
): analyzer.IIssue {
158+
return {
159+
filePath,
160+
message,
161+
rule: { repo: 'javascript', rule: PARSING_ERROR_RULE_KEY },
162+
textRange: line !== undefined ? toTextRange(line, 0, undefined, undefined) : undefined,
121163
flows: [],
122164
};
123165
}
@@ -158,6 +200,7 @@ function restorePath(filePath: NormalizedAbsolutePath, pathMap: Map<string, stri
158200
* ```
159201
*
160202
* @param output - The ProjectAnalysisOutput from analyzeProject()
203+
* @param pathMap - Optional mapping from normalized absolute paths to original request paths
161204
* @returns gRPC IAnalyzeResponse ready for protobuf serialization
162205
*/
163206
export function transformProjectOutputToResponse(
@@ -198,16 +241,7 @@ export function transformProjectOutputToResponse(
198241
});
199242

200243
issues.push(
201-
transformIssue({
202-
ruleId: PARSING_ERROR_RULE_KEY,
203-
message,
204-
line: line ?? 1,
205-
column: 0,
206-
language: 'js',
207-
secondaryLocations: [],
208-
ruleESLintKeys: [],
209-
filePath: originalPath as NormalizedAbsolutePath,
210-
}),
244+
transformParsingErrorIssue(message, originalPath as NormalizedAbsolutePath, line),
211245
);
212246
continue;
213247
}

packages/grpc/tests/transformers.test.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -621,6 +621,67 @@ describe('transformProjectOutputToResponse', () => {
621621
});
622622
});
623623

624+
it('should omit textRange for file-level JS/TS issues', () => {
625+
const output = makeOutput({
626+
'/project/src/file.js': {
627+
issues: [
628+
{
629+
ruleId: 'S1451',
630+
language: 'js',
631+
line: 0,
632+
column: 0,
633+
endLine: 0,
634+
endColumn: 0,
635+
message: 'Add or update the header of this file.',
636+
secondaryLocations: [
637+
{
638+
line: 0,
639+
column: 0,
640+
endLine: 0,
641+
endColumn: 0,
642+
message: 'Secondary',
643+
},
644+
],
645+
ruleESLintKeys: [],
646+
filePath: '/project/src/file.js',
647+
},
648+
],
649+
},
650+
});
651+
652+
const result = transformProjectOutputToResponse(output);
653+
654+
expect(result.issues?.length).toBe(1);
655+
expect(result.issues?.[0].textRange).toBeUndefined();
656+
expect(result.issues?.[0].flows).toEqual([]);
657+
});
658+
659+
it('should omit textRange for parsing error issues when parser returns line 0', () => {
660+
const output = makeOutput({
661+
'/project/src/broken.js': {
662+
parsingError: { message: 'Unexpected token', code: 'PARSING', line: 0 },
663+
},
664+
});
665+
666+
const result = transformProjectOutputToResponse(output);
667+
668+
expect(result.issues?.length).toBe(1);
669+
expect(result.issues?.[0].textRange).toBeUndefined();
670+
});
671+
672+
it('should omit textRange for parsing error issues when parser line is missing', () => {
673+
const output = makeOutput({
674+
'/project/src/broken.js': {
675+
parsingError: { message: 'Unexpected token', code: 'PARSING' },
676+
},
677+
});
678+
679+
const result = transformProjectOutputToResponse(output);
680+
681+
expect(result.issues?.length).toBe(1);
682+
expect(result.issues?.[0].textRange).toBeUndefined();
683+
});
684+
624685
it('should restore original paths using pathMap for JS/TS issues', () => {
625686
const output = makeOutput({
626687
'/app/src/file.ts': {

packages/jsts/src/linter/issues/issue.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,6 @@ import type { BaseIssue } from '../../../../shared/src/types/analysis.js';
3737
*/
3838
export interface JsTsIssue extends BaseIssue {
3939
language: JsTsLanguage;
40-
endLine?: number;
41-
endColumn?: number;
4240
cost?: number;
4341
secondaryLocations: Location[];
4442
quickFixes?: QuickFix[];

0 commit comments

Comments
 (0)