Skip to content

Commit 0aff0b2

Browse files
add InspectionCopilot to inspect provided code (#1313)
* add `InspectionCopilot` to inspect provided code: provide prompts, process input code and extract output inspections from comments. * use `sendInfo` to attach properties to current telemetry event. * chore: fix miscellaneous issues
1 parent 5bd3ed7 commit 0aff0b2

File tree

3 files changed

+288
-1
lines changed

3 files changed

+288
-1
lines changed

src/copilot/Copilot.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export default class Copilot {
5252
complete = await _send('continue where you left off.');
5353
}
5454
logger.debug('rounds', rounds);
55-
sendInfo('java.copilot.sendRequest.rounds', { rounds: rounds });
55+
sendInfo('java.copilot.sendRequest.info', { rounds: rounds });
5656
return answer.replace(this.endMark, "");
5757
}
5858

src/copilot/inspect/Inspection.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { TextDocument } from "vscode";
2+
3+
export interface Inspection {
4+
document?: TextDocument;
5+
problem: {
6+
/**
7+
* short description of the problem
8+
*/
9+
description: string;
10+
position: {
11+
/**
12+
* real line number to the start of the document, will change
13+
*/
14+
line: number;
15+
/**
16+
* relative line number to the start of the symbol(method/class), won't change
17+
*/
18+
relativeLine: number;
19+
/**
20+
* code of the first line of the problematic code block
21+
*/
22+
code: string;
23+
};
24+
/**
25+
* symbol name of the problematic code block, e.g. method name/class name, keywork, etc.
26+
*/
27+
symbol: string;
28+
}
29+
solution: string;
30+
severity: string;
31+
}
Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
import { instrumentSimpleOperation, sendInfo } from "vscode-extension-telemetry-wrapper";
2+
import Copilot from "../Copilot";
3+
import { logger } from "../utils";
4+
import { Inspection } from "./Inspection";
5+
6+
export default class InspectionCopilot extends Copilot {
7+
8+
public static readonly SYSTEM_MESSAGE = `
9+
You are expert at Java and code refactoring. Please identify code blocks that can be rewritten with
10+
Java latest features/syntaxes/grammar sugars to make them more **readable**, **efficient** and **concise** for given code.
11+
I prefer \`Stream\` to loop, \`Optional\` to null, \`record\` to POJO, \`switch\` to if-else, etc.
12+
Please comment on the rewritable code directly in the original source code in the following format:
13+
\`\`\`
14+
other code...
15+
// @PROBLEM: problem of the code in less than 10 words, should be as short as possible, starts with a gerund/noun word, e.g., "Using".
16+
// @SOLUTION: solution to fix the problem in less than 10 words, should be as short as possible, starts with a verb.
17+
// @SYMBOL: symbol of the problematic code block, must be a single word contained by the problematic code. it's usually a Java keyword, a method/field/variable name, or a value(e.g. magic number)... but NOT multiple, '<null>' if cannot be identified
18+
// @SEVERITY: severity of the problem, must be one of **[HIGH, MIDDLE, LOW]**, *HIGH* for Probable bugs, Security risks, Exception handling or Resource management(e.g. memory leaks); *MIDDLE* for Error handling, Performance, Reflective accesses issues and Verbose or redundant code; *LOW* for others
19+
the original problematic code...
20+
\`\`\`
21+
The comment must be placed directly above the problematic code, and the problematic code must be kept unchanged.
22+
Your reply must be the complete original code sent to you plus your comments, without any other modifications.
23+
Never comment on undertermined problems.
24+
Never comment on code that is well-written or simple enough.
25+
Don't add any explanation, don't format logger. Don't output markdown.
26+
You must end your response with "//${Copilot.DEFAULT_END_MARK}".
27+
`;
28+
public static readonly EXAMPLE_USER_MESSAGE = `
29+
@Entity
30+
public class EmployeePojo implements Employee {
31+
public final String name;
32+
public EmployeePojo(String name) {
33+
this.name = name;
34+
}
35+
public String getRole() {
36+
String result = '';
37+
if (this.name.equals("Miller")) {
38+
result = "Senior";
39+
} else if (this.name.equals("Mike")) {
40+
result = "HR";
41+
} else {
42+
result = "FTE";
43+
}
44+
return result;
45+
}
46+
public void test(String[] arr) {
47+
try {
48+
Integer.parseInt(arr[0]);
49+
} catch (Exception e) {
50+
e.printStackTrace();
51+
}
52+
}
53+
}
54+
`;
55+
public static readonly EXAMPLE_ASSISTANT_MESSAGE = `
56+
@Entity
57+
// @PROBLEM: Using a traditional POJO
58+
// @SOLUTION: transform into a record
59+
// @SYMBOL: EmployeePojo
60+
// @SEVERITY: MIDDLE
61+
public class EmployeePojo implements Employee {
62+
public final String name;
63+
public EmployeePojo(String name) {
64+
this.name = name;
65+
}
66+
public String getRole() {
67+
String result = '';
68+
// @PROBLEM: Using if-else statements to check the type of animal
69+
// @SOLUTION: Use switch expression
70+
// @SYMBOL: if
71+
// @SEVERITY: MIDDLE
72+
if (this.name.equals("Miller")) {
73+
result = "Senior";
74+
} else if (this.name.equals("Mike")) {
75+
result = "HR";
76+
} else {
77+
result = "FTE";
78+
}
79+
return result;
80+
}
81+
public void test(String[] arr) {
82+
try {
83+
Integer.parseInt(arr[0]);
84+
} catch (Exception e) {
85+
// @PROBLEM: Print stack trace in case of an exception
86+
// @SOLUTION: Log errors to a logger
87+
// @SYMBOL: ex.printStackTrace
88+
// @SEVERITY: LOW
89+
e.printStackTrace();
90+
}
91+
}
92+
}
93+
//${Copilot.DEFAULT_END_MARK}
94+
`;
95+
96+
// Initialize regex patterns
97+
private static readonly PROBLEM_PATTERN: RegExp = /\/\/ @PROBLEM: (.*)/;
98+
private static readonly SOLUTION_PATTERN: RegExp = /\/\/ @SOLUTION: (.*)/;
99+
private static readonly SYMBOL_PATTERN: RegExp = /\/\/ @SYMBOL: (.*)/;
100+
private static readonly LEVEL_PATTERN: RegExp = /\/\/ @SEVERITY: (.*)/;
101+
102+
private readonly debounceMap = new Map<string, NodeJS.Timeout>();
103+
104+
public constructor() {
105+
const messages: { role: string, content: string }[] = [
106+
{ role: "system", content: InspectionCopilot.SYSTEM_MESSAGE },
107+
{ role: "user", content: InspectionCopilot.EXAMPLE_USER_MESSAGE },
108+
{ role: "assistant", content: InspectionCopilot.EXAMPLE_ASSISTANT_MESSAGE },
109+
];
110+
super(messages);
111+
}
112+
113+
/**
114+
* inspect the given code (debouncely if `key` is provided) using copilot and return the inspections
115+
* @param code code to inspect
116+
* @param key key to debounce the inspecting, which is used to support multiple debouncing. Consider
117+
* the case that we have multiple documents, and we only want to debounce the method calls on the
118+
* same document (identified by `key`).
119+
* @param wait debounce time in milliseconds, default is 3000ms
120+
* @returns inspections provided by copilot
121+
*/
122+
public inspectCode(code: string, key?: string, wait: number = 3000): Promise<Inspection[]> {
123+
const _doInspectCode: (code: string) => Promise<Inspection[]> = instrumentSimpleOperation("java.copilot.inspect.code", this.doInspectCode.bind(this));
124+
if (!key) { // inspect code immediately without debounce
125+
return _doInspectCode(code);
126+
}
127+
// inspect code with debounce if key is provided
128+
if (this.debounceMap.has(key)) {
129+
clearTimeout(this.debounceMap.get(key) as NodeJS.Timeout);
130+
logger.debug(`debounced`, key);
131+
}
132+
return new Promise<Inspection[]>((resolve) => {
133+
this.debounceMap.set(key, setTimeout(() => {
134+
void _doInspectCode(code).then(inspections => {
135+
this.debounceMap.delete(key);
136+
resolve(inspections);
137+
});
138+
}, wait <= 0 ? 3000 : wait));
139+
});
140+
}
141+
142+
private async doInspectCode(code: string): Promise<Inspection[]> {
143+
const originalLines: string[] = code.split(/\r?\n/);
144+
// code lines without empty lines and comments
145+
const codeLines: { originalLineIndex: number, content: string }[] = this.extractCodeLines(originalLines)
146+
const codeLinesContent = codeLines.map(l => l.content).join('\n');
147+
148+
if (codeLines.length < 1) {
149+
return Promise.resolve([]);
150+
}
151+
152+
const codeWithInspectionComments = await this.send(codeLinesContent);
153+
const inspections = this.extractInspections(codeWithInspectionComments, codeLines);
154+
// add properties for telemetry
155+
sendInfo('java.copilot.inspect.code', {
156+
codeLength: code.length,
157+
codeLines: codeLines.length,
158+
insectionsCount: inspections.length,
159+
problems: inspections.map(i => i.problem.description).join(',')
160+
});
161+
return inspections;
162+
}
163+
164+
/**
165+
* extract inspections from the code with inspection comments
166+
* @param codeWithInspectionComments response from the copilot, code with inspection comments
167+
* @param codeLines code lines without empty lines and comments
168+
*/
169+
private extractInspections(codeWithInspectionComments: string, codeLines: { originalLineIndex: number, content: string }[]): Inspection[] {
170+
const lines = codeWithInspectionComments.split('\n').filter(line => line.trim().length > 0);
171+
const inspections: Inspection[] = [];
172+
let inspectionCommentLineCount = 0;
173+
174+
for (let i = 0; i < lines.length;) {
175+
const problemMatch = lines[i].match(InspectionCopilot.PROBLEM_PATTERN);
176+
if (problemMatch) {
177+
const inspection: Inspection = this.extractInspection(i, lines);
178+
const codeLineIndex = i - inspectionCommentLineCount;
179+
// relative line number to the start of the code inspected, which will be ajusted relative to the start of container symbol later when caching.
180+
inspection.problem.position.relativeLine = codeLines[codeLineIndex].originalLineIndex ?? -1;
181+
inspection.problem.position.code = codeLines[codeLineIndex].content;
182+
inspections.push(inspection);
183+
i += 4; // inspection comment has 4 lines
184+
inspectionCommentLineCount += 4;
185+
continue;
186+
}
187+
i++;
188+
}
189+
190+
return inspections.filter(i => i.problem.symbol.trim() !== '<null>').sort((a, b) => a.problem.position.relativeLine - b.problem.position.relativeLine);
191+
}
192+
193+
/**
194+
* Extract inspection from the 4 line starting at the given index
195+
* @param index the index of the first line of the inspection comment
196+
* @param lines all lines of the code with inspection comments
197+
* @returns inspection object
198+
*/
199+
private extractInspection(index: number, lines: string[]): Inspection {
200+
const inspection: Inspection = {
201+
problem: {
202+
description: '',
203+
position: { line: -1, relativeLine: -1, code: '' },
204+
symbol: ''
205+
},
206+
solution: '',
207+
severity: ''
208+
};
209+
const problemMatch = lines[index + 0].match(InspectionCopilot.PROBLEM_PATTERN);
210+
const solutionMatch = lines[index + 1].match(InspectionCopilot.SOLUTION_PATTERN);
211+
const symbolMatch = lines[index + 2].match(InspectionCopilot.SYMBOL_PATTERN);
212+
const severityMatch = lines[index + 3].match(InspectionCopilot.LEVEL_PATTERN);
213+
if (problemMatch) {
214+
inspection.problem.description = problemMatch[1].trim();
215+
}
216+
if (solutionMatch) {
217+
inspection.solution = solutionMatch[1].trim();
218+
}
219+
if (symbolMatch) {
220+
inspection.problem.symbol = symbolMatch[1].trim();
221+
}
222+
if (severityMatch) {
223+
inspection.severity = severityMatch[1].trim();
224+
}
225+
return inspection;
226+
}
227+
228+
/**
229+
* Extract code lines only without empty and comment lines
230+
* @param originalLines original code lines with comments and empty lines
231+
* @returns code lines (including line content and corresponding original line index) without empty lines and comments
232+
*/
233+
private extractCodeLines(originalLines: string[]): { originalLineIndex: number, content: string }[] {
234+
const codeLines: { originalLineIndex: number, content: string }[] = [];
235+
let inBlockComment = false;
236+
for (let originalLineIndex = 0; originalLineIndex < originalLines.length; originalLineIndex++) {
237+
const trimmedLine = originalLines[originalLineIndex].trim();
238+
239+
// Check for block comment start
240+
if (trimmedLine.startsWith('/*')) {
241+
inBlockComment = true;
242+
}
243+
244+
// If we're not in a block comment, add the line to the output
245+
if (trimmedLine !== '' && !inBlockComment && !trimmedLine.startsWith('//')) {
246+
codeLines.push({ content: originalLines[originalLineIndex], originalLineIndex });
247+
}
248+
249+
// Check for block comment end
250+
if (trimmedLine.endsWith('*/')) {
251+
inBlockComment = false;
252+
}
253+
}
254+
return codeLines;
255+
}
256+
}

0 commit comments

Comments
 (0)