Skip to content

Commit 786b31c

Browse files
committed
Update diagnostic action resolver
1 parent 0c74977 commit 786b31c

2 files changed

Lines changed: 169 additions & 55 deletions

File tree

src/solutions/problem-diagnostic-action-resolver.ts

Lines changed: 96 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@ export interface ProblemDiagnosticActionResult {
4444
code?: NonNullable<vscode.Diagnostic['code']>;
4545
}
4646

47+
type ProblemActionDescriptor =
48+
| { kind: 'merge'; localPath: string; updateLevel: MergeUpdateLevel; componentId?: string }
49+
| { kind: 'run-generator'; generator: string; context: string }
50+
| { kind: 'manage-components'; query: string }
51+
| { kind: 'find-in-files'; query: string };
52+
4753
const mergeMessagePatterns = [
4854
{
4955
pattern: /update\s+(required|recommended|suggested|mandatory)\s+for\s+file\s+'([^']+)'/i,
@@ -53,7 +59,10 @@ const mergeMessagePatterns = [
5359
] as const;
5460

5561
const mergeComponentRegex = /(?:for|from)\s+component\s+'([^']+)'/i;
56-
const generatorMissingPattern = /cgen file was not found,\s*run generator '([^']+)' for context '([^']+)'/i;
62+
const generatorMissingPatterns: readonly RegExp[] = [
63+
/cgen file was not found,\s*run generator '([^']+)' for context '([^']+)'/i,
64+
/(?:cgen\s+file\s+.*\s+)?run generator '([^']+)' for context '([^']+)'/i,
65+
];
5766

5867
const queryActionPatterns: ReadonlyArray<{ pattern: RegExp; action: 'components-packs' | 'find-in-files' }> = [
5968
{ pattern: /dependency validation for context '([^']+)' failed:/, action: 'components-packs' },
@@ -65,63 +74,93 @@ const queryActionPatterns: ReadonlyArray<{ pattern: RegExp; action: 'components-
6574

6675
export class ProblemDiagnosticActionResolver {
6776
public resolve(context: ProblemDiagnosticActionContext): ProblemDiagnosticActionResult | undefined {
77+
const descriptor = this.resolveDescriptor(context);
78+
if (!descriptor) {
79+
return undefined;
80+
}
81+
return this.toDiagnosticAction(descriptor);
82+
}
83+
84+
private resolveDescriptor(context: ProblemDiagnosticActionContext): ProblemActionDescriptor | undefined {
6885
return this.resolveMergeAction(context)
6986
?? this.resolveGeneratorMissingAction(context)
7087
?? this.resolveManageComponentsAction(context)
7188
?? this.resolveGenericSearchAction(context);
7289
}
7390

74-
public createMergeDiagnosticAction(message: string, diagnosticFilePath: string): ProblemDiagnosticActionResult | undefined {
75-
const merge = this.parseMergeMessage(message);
76-
if (!merge) {
77-
return undefined;
91+
private toDiagnosticAction(descriptor: ProblemActionDescriptor): ProblemDiagnosticActionResult {
92+
if (descriptor.kind === 'merge') {
93+
return {
94+
message: this.createMergeDiagnosticMessage(descriptor.localPath, descriptor.updateLevel, descriptor.componentId),
95+
code: {
96+
value: MERGE_VIEW_LINK_LABEL,
97+
target: this.createMergeCommandUri(descriptor.localPath),
98+
},
99+
};
78100
}
79101

80-
const componentId = mergeComponentRegex.exec(message)?.[1];
81-
const localPath = this.isAbsoluteFilePath(merge.localPath) ? merge.localPath : diagnosticFilePath;
102+
if (descriptor.kind === 'run-generator') {
103+
return {
104+
code: {
105+
value: 'Run Generator',
106+
target: this.createRunGeneratorCommandUri(descriptor.generator, descriptor.context),
107+
},
108+
};
109+
}
82110

111+
if (descriptor.kind === 'manage-components') {
112+
const args = this.encodeCommandArgs([{ type: 'context', value: descriptor.query }]);
113+
return {
114+
code: {
115+
value: 'Manage Components',
116+
target: vscode.Uri.parse(`command:${MANAGE_COMPONENTS_PACKS_COMMAND_ID}?${args}`),
117+
},
118+
};
119+
}
120+
121+
const args = this.encodeFindInFilesArgs(descriptor.query);
83122
return {
84-
message: this.createMergeDiagnosticMessage(localPath, merge.updateLevel, componentId),
85123
code: {
86-
value: MERGE_VIEW_LINK_LABEL,
87-
target: this.createMergeCommandUri(localPath),
124+
value: 'Find in Files',
125+
target: vscode.Uri.parse(`command:workbench.action.findInFiles?${args}`),
88126
},
89127
};
90128
}
91129

92-
public createMergeCommandUri(localPath: string): vscode.Uri {
93-
const args = this.encodeCommandArgs([localPath]);
94-
return vscode.Uri.parse(`command:${MERGE_FILE_COMMAND_ID}?${args}`);
95-
}
96-
97-
public isAbsoluteFilePath(filePath: string): boolean {
98-
return path.isAbsolute(filePath) || path.win32.isAbsolute(filePath);
99-
}
130+
private resolveMergeAction(context: ProblemDiagnosticActionContext): ProblemActionDescriptor | undefined {
131+
const merge = this.parseMergeMessage(context.message);
132+
if (!merge) {
133+
return undefined;
134+
}
100135

101-
private resolveMergeAction(context: ProblemDiagnosticActionContext): ProblemDiagnosticActionResult | undefined {
102-
return this.createMergeDiagnosticAction(context.message, context.diagnosticFilePath);
136+
const componentId = mergeComponentRegex.exec(context.message)?.[1];
137+
const localPath = this.isAbsoluteFilePath(merge.localPath) ? merge.localPath : context.diagnosticFilePath;
138+
return {
139+
kind: 'merge',
140+
localPath,
141+
updateLevel: merge.updateLevel,
142+
componentId,
143+
};
103144
}
104145

105-
private resolveGeneratorMissingAction(context: ProblemDiagnosticActionContext): ProblemDiagnosticActionResult | undefined {
146+
private resolveGeneratorMissingAction(context: ProblemDiagnosticActionContext): ProblemActionDescriptor | undefined {
106147
if (context.hasLocation) {
107148
return undefined;
108149
}
109150

110-
const match = context.message.match(generatorMissingPattern);
111-
if (!match) {
151+
const request = this.parseGeneratorRequest(context.message);
152+
if (!request) {
112153
return undefined;
113154
}
114155

115-
const [, generator, generatorContext] = match;
116156
return {
117-
code: {
118-
value: 'Run Generator',
119-
target: this.createRunGeneratorCommandUri(generator, generatorContext),
120-
},
157+
kind: 'run-generator',
158+
generator: request.generator,
159+
context: request.context,
121160
};
122161
}
123162

124-
private resolveManageComponentsAction(context: ProblemDiagnosticActionContext): ProblemDiagnosticActionResult | undefined {
163+
private resolveManageComponentsAction(context: ProblemDiagnosticActionContext): ProblemActionDescriptor | undefined {
125164
if (context.hasLocation) {
126165
return undefined;
127166
}
@@ -131,16 +170,13 @@ export class ProblemDiagnosticActionResolver {
131170
return undefined;
132171
}
133172

134-
const args = this.encodeCommandArgs([{ type: 'context', value: queryAction.query }]);
135173
return {
136-
code: {
137-
value: 'Manage Components',
138-
target: vscode.Uri.parse(`command:${MANAGE_COMPONENTS_PACKS_COMMAND_ID}?${args}`),
139-
},
174+
kind: 'manage-components',
175+
query: queryAction.query,
140176
};
141177
}
142178

143-
private resolveGenericSearchAction(context: ProblemDiagnosticActionContext): ProblemDiagnosticActionResult | undefined {
179+
private resolveGenericSearchAction(context: ProblemDiagnosticActionContext): ProblemActionDescriptor | undefined {
144180
if (context.hasLocation) {
145181
return undefined;
146182
}
@@ -150,15 +186,26 @@ export class ProblemDiagnosticActionResolver {
150186
return undefined;
151187
}
152188

153-
const args = this.encodeFindInFilesArgs(queryAction.query);
154189
return {
155-
code: {
156-
value: 'Find in Files',
157-
target: vscode.Uri.parse(`command:workbench.action.findInFiles?${args}`),
158-
},
190+
kind: 'find-in-files',
191+
query: queryAction.query,
159192
};
160193
}
161194

195+
private parseGeneratorRequest(message: string): { generator: string; context: string } | undefined {
196+
for (const pattern of generatorMissingPatterns) {
197+
const match = message.match(pattern);
198+
if (!match) {
199+
continue;
200+
}
201+
202+
const [, generator, context] = match;
203+
return { generator, context };
204+
}
205+
206+
return undefined;
207+
}
208+
162209
private parseMergeMessage(message: string): MergeMessageMatch | undefined {
163210
for (const item of mergeMessagePatterns) {
164211
const match = item.pattern.exec(message);
@@ -201,11 +248,20 @@ export class ProblemDiagnosticActionResolver {
201248
return `update ${updateLevel} for config file '${fileName}' from component '${componentDisplayName}'.`;
202249
}
203250

251+
private createMergeCommandUri(localPath: string): vscode.Uri {
252+
const args = this.encodeCommandArgs([localPath]);
253+
return vscode.Uri.parse(`command:${MERGE_FILE_COMMAND_ID}?${args}`);
254+
}
255+
204256
private createRunGeneratorCommandUri(generator: string, context: string): vscode.Uri {
205257
const args = this.encodeCommandArgs([{ generator, context }]);
206258
return vscode.Uri.parse(`command:${RUN_GENERATOR_COMMAND_ID}?${args}`);
207259
}
208260

261+
private isAbsoluteFilePath(filePath: string): boolean {
262+
return path.isAbsolute(filePath) || path.win32.isAbsolute(filePath);
263+
}
264+
209265
private encodeFindInFilesArgs(query: string): string {
210266
const args = {
211267
query: query,

src/solutions/solution-problems.test.ts

Lines changed: 73 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -266,18 +266,23 @@ describe('SolutionProblems', () => {
266266
});
267267

268268
it('creates a merge command uri with encoded local path', () => {
269-
const result = diagnosticActionResolver.createMergeCommandUri('/packs/Component/config.c');
270-
const [command, args] = result.toString().split('?');
269+
const result = diagnosticActionResolver.resolve({
270+
message: "update required for file '/packs/Component/config.c' from component 'Arm::Device@2.3.4'",
271+
diagnosticFilePath: layerPath,
272+
hasLocation: false,
273+
});
274+
const [command, args] = (result?.code as { target: vscode.Uri }).target.toString().split('?');
271275

272276
expect(command).toBe(`command:${MERGE_FILE_COMMAND_ID}`);
273277
expect(JSON.parse(decodeURIComponent(args))).toEqual(['/packs/Component/config.c']);
274278
});
275279

276280
it('creates merge diagnostic action for merge messages with component context', () => {
277-
const result = diagnosticActionResolver.createMergeDiagnosticAction(
278-
"update required for file '/packs/Component/config.c' from component 'Arm::Device@2.3.4'",
279-
layerPath,
280-
);
281+
const result = diagnosticActionResolver.resolve({
282+
message: "update required for file '/packs/Component/config.c' from component 'Arm::Device@2.3.4'",
283+
diagnosticFilePath: layerPath,
284+
hasLocation: false,
285+
});
281286

282287
expect(result).toEqual({
283288
message: "update required for config file 'config.c' from component 'Device'.",
@@ -290,10 +295,11 @@ describe('SolutionProblems', () => {
290295

291296
it('creates merge diagnostic action for current toolbox message wording', () => {
292297
const configPath = 'C:/CubeMX/CubeMX/RTE/CMSIS/RTX_Config.c';
293-
const result = diagnosticActionResolver.createMergeDiagnosticAction(
294-
`update recommended for file '${configPath}' from component 'CMSIS:RTOS2:Keil RTX5&Source'.\nMerge content from update file, rename update file to base file and remove previous base file`,
295-
layerPath,
296-
);
298+
const result = diagnosticActionResolver.resolve({
299+
message: `update recommended for file '${configPath}' from component 'CMSIS:RTOS2:Keil RTX5&Source'.\nMerge content from update file, rename update file to base file and remove previous base file`,
300+
diagnosticFilePath: layerPath,
301+
hasLocation: false,
302+
});
297303

298304
expect(result).toEqual({
299305
message: "update recommended for config file 'RTX_Config.c' from component 'CMSIS:RTOS2:Keil RTX5&Source'.",
@@ -305,16 +311,68 @@ describe('SolutionProblems', () => {
305311
});
306312

307313
it('treats Windows-style merge paths as absolute', () => {
308-
expect(diagnosticActionResolver.isAbsoluteFilePath('C:/CubeMX/CubeMX/RTE/CMSIS/RTX_Config.c')).toBe(true);
309-
expect(diagnosticActionResolver.isAbsoluteFilePath('relative-config.c')).toBe(false);
314+
const absolute = diagnosticActionResolver.resolve({
315+
message: "update required for file 'C:/CubeMX/CubeMX/RTE/CMSIS/RTX_Config.c' from component 'Arm::Device@2.3.4'",
316+
diagnosticFilePath: layerPath,
317+
hasLocation: false,
318+
});
319+
const relative = diagnosticActionResolver.resolve({
320+
message: "update required for file 'relative-config.c' from component 'Arm::Device@2.3.4'",
321+
diagnosticFilePath: layerPath,
322+
hasLocation: false,
323+
});
324+
325+
const absoluteArgs = JSON.parse(decodeURIComponent((absolute?.code as { target: vscode.Uri }).target.toString().split('?')[1]));
326+
const relativeArgs = JSON.parse(decodeURIComponent((relative?.code as { target: vscode.Uri }).target.toString().split('?')[1]));
327+
328+
expect(absoluteArgs).toEqual(['C:/CubeMX/CubeMX/RTE/CMSIS/RTX_Config.c']);
329+
expect(relativeArgs).toEqual([layerPath]);
310330
});
311331

312332
it('returns undefined merge diagnostic action for non-merge messages', () => {
313-
const result = diagnosticActionResolver.createMergeDiagnosticAction(
314-
"component 'Arm::Device@2.3.4' is missing",
315-
layerPath,
333+
const result = diagnosticActionResolver.resolve(
334+
{
335+
message: "component 'Arm::Device@2.3.4' is missing",
336+
diagnosticFilePath: layerPath,
337+
hasLocation: false,
338+
},
316339
);
317340

341+
expect((result?.code as { value: string }).value).toBe('Find in Files');
342+
});
343+
344+
it('creates run generator action for canonical generator error wording', () => {
345+
const result = diagnosticActionResolver.resolve({
346+
message: "cgen file was not found, run generator 'CubeMX2' for context 'CubeMX2.Debug+STM32C531CBT6'",
347+
diagnosticFilePath: layerPath,
348+
hasLocation: false,
349+
});
350+
351+
const code = result?.code as { value: string; target: vscode.Uri };
352+
const [command, args] = code.target.toString().split('?');
353+
354+
expect(code.value).toBe('Run Generator');
355+
expect(command).toContain('command:cmsis-csolution.runGenerator');
356+
expect(JSON.parse(decodeURIComponent(args))).toEqual([{ generator: 'CubeMX2', context: 'CubeMX2.Debug+STM32C531CBT6' }]);
357+
});
358+
359+
it('creates run generator action for alternate generator error wording', () => {
360+
const result = diagnosticActionResolver.resolve({
361+
message: "run generator 'CubeMX2' for context 'CubeMX2.Debug+STM32C531CBT6' to generate missing cgen artifacts",
362+
diagnosticFilePath: layerPath,
363+
hasLocation: false,
364+
});
365+
366+
expect((result?.code as { value: string }).value).toBe('Run Generator');
367+
});
368+
369+
it('does not offer run generator action when line or column is present', () => {
370+
const result = diagnosticActionResolver.resolve({
371+
message: "cgen file was not found, run generator 'CubeMX2' for context 'CubeMX2.Debug+STM32C531CBT6'",
372+
diagnosticFilePath: layerPath,
373+
hasLocation: true,
374+
});
375+
318376
expect(result).toBeUndefined();
319377
});
320378

0 commit comments

Comments
 (0)