Skip to content

Commit e08d57e

Browse files
authored
Merge branch 'main' into nightly-dep-graph
2 parents 124ea10 + c7b53e4 commit e08d57e

11 files changed

Lines changed: 448 additions & 169 deletions

.github/workflows/tpip.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ jobs:
7272

7373
- name: Commit Changes
7474
if: ${{ steps.excerpt.outputs.update == 1 }}
75-
uses: EndBug/add-and-commit@a94899bca583c204427a224a7af87c02f9b325d5
75+
uses: EndBug/add-and-commit@290ea2c423ad77ca9c62ae0f5b224379612c0321
7676
with:
7777
author_name: monty-bot
7878
author_email: monty-bot@arm.com

src/solutions/solution-problems.test.ts

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ describe('SolutionProblems', () => {
245245
logMessages: {
246246
success: true,
247247
errors: [],
248-
warnings: ["mylayer.clayer.yml - file '/packs/Component/config.c' update required from component 'Arm::Device@2.3.4'"],
248+
warnings: ["mylayer.clayer.yml - update required for file '/packs/Component/config.c' from component 'Arm::Device@2.3.4'"],
249249
info: [],
250250
},
251251
});
@@ -272,7 +272,7 @@ describe('SolutionProblems', () => {
272272

273273
it('creates merge diagnostic action for merge messages with component context', () => {
274274
const result = solutionProblems['createMergeDiagnosticAction'](
275-
"file '/packs/Component/config.c' update required from component 'Arm::Device@2.3.4'",
275+
"update required for file '/packs/Component/config.c' from component 'Arm::Device@2.3.4'",
276276
layerPath,
277277
);
278278

@@ -285,6 +285,27 @@ describe('SolutionProblems', () => {
285285
});
286286
});
287287

288+
it('creates merge diagnostic action for current toolbox message wording', () => {
289+
const configPath = 'C:/CubeMX/CubeMX/RTE/CMSIS/RTX_Config.c';
290+
const result = solutionProblems['createMergeDiagnosticAction'](
291+
`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`,
292+
layerPath,
293+
);
294+
295+
expect(result).toEqual({
296+
message: "update recommended for config file 'RTX_Config.c' from component 'CMSIS:RTOS2:Keil RTX5&Source'.",
297+
code: {
298+
value: 'Open in Merge View',
299+
target: vscode.Uri.parse(`command:${MERGE_FILE_COMMAND_ID}?${encodeURIComponent(JSON.stringify([configPath]))}`),
300+
},
301+
});
302+
});
303+
304+
it('treats Windows-style merge paths as absolute', () => {
305+
expect(solutionProblems['isAbsoluteFilePath']('C:/CubeMX/CubeMX/RTE/CMSIS/RTX_Config.c')).toBe(true);
306+
expect(solutionProblems['isAbsoluteFilePath']('relative-config.c')).toBe(false);
307+
});
308+
288309
it('returns undefined merge diagnostic action for non-merge messages', () => {
289310
const result = solutionProblems['createMergeDiagnosticAction'](
290311
"component 'Arm::Device@2.3.4' is missing",
@@ -304,7 +325,7 @@ describe('SolutionProblems', () => {
304325
logMessages: {
305326
success: true,
306327
errors: [],
307-
warnings: ["mylayer.clayer.yml - file 'relative-config.c' update recommended"],
328+
warnings: ["mylayer.clayer.yml - update recommended for file 'relative-config.c'"],
308329
info: [],
309330
},
310331
});
@@ -323,7 +344,7 @@ describe('SolutionProblems', () => {
323344
});
324345

325346
it.each(['required', 'recommended', 'suggested', 'mandatory'] as const)(
326-
'renders merge diagnostics for %s update levels',
347+
'renders merge diagnostics for current toolbox wording with %s update levels',
327348
async updateLevel => {
328349
await solutionProblems.activate({ subscriptions: [] } as unknown as ExtensionContext);
329350
const setSpy = jest.spyOn(vscode.languages.createDiagnosticCollection(), 'set');
@@ -334,7 +355,7 @@ describe('SolutionProblems', () => {
334355
logMessages: {
335356
success: true,
336357
errors: [],
337-
warnings: [`mylayer.clayer.yml - file '/packs/Component/${updateLevel}.c' update ${updateLevel}`],
358+
warnings: [`mylayer.clayer.yml - update ${updateLevel} for file '/packs/Component/${updateLevel}.c'; merge content from update file, rename update file to base file and remove previous base file`],
338359
info: [],
339360
},
340361
});

src/solutions/solution-problems.ts

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,13 @@ export const enrichLogMessagesFromToolOutput = async (logMessages: LogMessages,
103103

104104
export const MERGE_VIEW_LINK_LABEL = 'Open in Merge View';
105105
export type MergeUpdateLevel = 'required' | 'recommended' | 'suggested' | 'mandatory';
106-
const mergeMessageRegex = /file\s+'([^']+)'\s+update\s+(required|recommended|suggested|mandatory)/i;
106+
const mergeMessagePatterns = [
107+
{
108+
pattern: /update\s+(required|recommended|suggested|mandatory)\s+for\s+file\s+'([^']+)'/i,
109+
getLocalPath: (match: RegExpExecArray) => match[2],
110+
getUpdateLevel: (match: RegExpExecArray) => match[1],
111+
},
112+
] as const;
107113
const mergeComponentRegex = /(?:for|from)\s+component\s+'([^']+)'/i;
108114
export interface MergeMessageMatch {
109115
localPath: string;
@@ -311,17 +317,21 @@ export class SolutionProblemsImpl implements SolutionProblems {
311317
}
312318

313319
private parseMergeMessage(line: string): MergeMessageMatch | undefined {
314-
const match = mergeMessageRegex.exec(line);
315-
if (!match || match.index === undefined) {
316-
return undefined;
320+
for (const item of mergeMessagePatterns) {
321+
const match = item.pattern.exec(line);
322+
if (!match || match.index === undefined) {
323+
continue;
324+
}
325+
326+
return {
327+
localPath: item.getLocalPath(match),
328+
updateLevel: item.getUpdateLevel(match).toLowerCase() as MergeUpdateLevel,
329+
matchStart: match.index,
330+
matchLength: match[0].length,
331+
};
317332
}
318333

319-
return {
320-
localPath: match[1],
321-
updateLevel: match[2].toLowerCase() as MergeUpdateLevel,
322-
matchStart: match.index,
323-
matchLength: match[0].length,
324-
};
334+
return undefined;
325335
}
326336

327337
private createMergeDiagnosticMessage(localPath: string, updateLevel: MergeUpdateLevel, componentId: string | undefined): string {
@@ -340,14 +350,18 @@ export class SolutionProblemsImpl implements SolutionProblems {
340350
return vscode.Uri.parse(`command:${MERGE_FILE_COMMAND_ID}?${args}`);
341351
}
342352

353+
private isAbsoluteFilePath(filePath: string): boolean {
354+
return path.isAbsolute(filePath) || path.win32.isAbsolute(filePath);
355+
}
356+
343357
private createMergeDiagnosticAction(message: string, diagnosticFilePath: string): { message: string; code: NonNullable<vscode.Diagnostic['code']> } | undefined {
344358
const merge = this.parseMergeMessage(message);
345359
if (!merge) {
346360
return undefined;
347361
}
348362

349363
const componentId = mergeComponentRegex.exec(message)?.[1];
350-
const localPath = path.isAbsolute(merge.localPath) ? merge.localPath : diagnosticFilePath;
364+
const localPath = this.isAbsoluteFilePath(merge.localPath) ? merge.localPath : diagnosticFilePath;
351365
const formattedMessage = this.createMergeDiagnosticMessage(localPath, merge.updateLevel, componentId);
352366

353367
return {
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/**
2+
* Copyright 2026 Arm Limited
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import path from 'path';
18+
import { TestDataHandler } from '../__test__/test-data';
19+
import { writeTextFile } from './fs-utils';
20+
import { configWizardAnnotationChecker } from './config-wizard-annotation-checker';
21+
22+
describe('configWizardAnnotationChecker', () => {
23+
const testDataHandler = new TestDataHandler();
24+
let testFolder: string;
25+
26+
beforeEach(() => {
27+
testFolder = testDataHandler.tmpDir;
28+
});
29+
30+
afterEach(() => {
31+
testDataHandler.rmTmpDir();
32+
});
33+
34+
afterAll(() => {
35+
testDataHandler.dispose();
36+
});
37+
38+
it('returns true when start marker is within first 100 lines', async () => {
39+
const filePath = path.join(testFolder, 'within-100.h');
40+
const lines = Array.from({ length: 99 }, () => 'int x = 0;');
41+
lines.push('// <<< Use Configuration Wizard in Context Menu >>>');
42+
writeTextFile(filePath, lines.join('\n'));
43+
44+
await expect(configWizardAnnotationChecker.hasAnnotations(filePath)).resolves.toBe(true);
45+
});
46+
47+
it('returns false when start marker appears after first 100 lines', async () => {
48+
const filePath = path.join(testFolder, 'after-100.h');
49+
const lines = Array.from({ length: 100 }, () => 'int y = 1;');
50+
lines.push('// <<< Use Configuration Wizard in Context Menu >>>');
51+
writeTextFile(filePath, lines.join('\n'));
52+
53+
await expect(configWizardAnnotationChecker.hasAnnotations(filePath)).resolves.toBe(false);
54+
});
55+
56+
it('returns true when start marker is within first 100 lines and end marker is after 100 lines', async () => {
57+
const filePath = path.join(testFolder, 'end-after-100.h');
58+
const lines = Array.from({ length: 39 }, () => 'int cfg = 3;');
59+
lines.push('// <<< Use Configuration Wizard in Context Menu >>>');
60+
lines.push(...Array.from({ length: 90 }, () => 'int body = 4;'));
61+
lines.push('// <<< end of configuration section >>>');
62+
writeTextFile(filePath, lines.join('\n'));
63+
64+
await expect(configWizardAnnotationChecker.hasAnnotations(filePath)).resolves.toBe(true);
65+
});
66+
67+
it('returns false when only end marker is present', async () => {
68+
const filePath = path.join(testFolder, 'end-only.h');
69+
writeTextFile(filePath, '// <<< end of configuration section >>>\nint z = 2;\n');
70+
71+
await expect(configWizardAnnotationChecker.hasAnnotations(filePath)).resolves.toBe(false);
72+
});
73+
74+
it('returns true for decorated marker lines used in template headers', async () => {
75+
const filePath = path.join(testFolder, 'decorated-marker.h');
76+
const lines = [
77+
'/* header */',
78+
'//-------- <<< Use Configuration Wizard in Context Menu >>> --------------------',
79+
'int cfg = 0;'
80+
];
81+
writeTextFile(filePath, lines.join('\n'));
82+
83+
await expect(configWizardAnnotationChecker.hasAnnotations(filePath)).resolves.toBe(true);
84+
});
85+
});
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/**
2+
* Copyright 2026 Arm Limited
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import * as fs from 'node:fs';
18+
import * as readline from 'node:readline';
19+
20+
export interface ConfigWizardAnnotationChecker {
21+
hasAnnotations(filePath: string): Promise<boolean>;
22+
}
23+
24+
class ConfigWizardAnnotationCheckerImpl implements ConfigWizardAnnotationChecker {
25+
private static readonly MAX_LINES_TO_SCAN = 100;
26+
private static readonly wizardStartMarkerRegex = /^\s*\/\/.*<<<\s*use configuration wizard in context menu\s*>>>.*$/i;
27+
28+
public async hasAnnotations(filePath: string): Promise<boolean> {
29+
if (!fs.existsSync(filePath)) {
30+
return false;
31+
}
32+
33+
const stream = fs.createReadStream(filePath, { encoding: 'utf8' });
34+
const lineReader = readline.createInterface({
35+
input: stream,
36+
crlfDelay: Infinity,
37+
});
38+
39+
let lineCount = 0;
40+
41+
try {
42+
for await (const line of lineReader) {
43+
if (ConfigWizardAnnotationCheckerImpl.wizardStartMarkerRegex.test(line)) {
44+
return true;
45+
}
46+
47+
lineCount += 1;
48+
if (lineCount >= ConfigWizardAnnotationCheckerImpl.MAX_LINES_TO_SCAN) {
49+
return false;
50+
}
51+
}
52+
53+
return false;
54+
} finally {
55+
lineReader.close();
56+
stream.destroy();
57+
}
58+
}
59+
}
60+
61+
export const configWizardAnnotationChecker: ConfigWizardAnnotationChecker =
62+
new ConfigWizardAnnotationCheckerImpl();

src/utils/config-wizard-checker.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* Copyright 2026 Arm Limited
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
export type { ConfigWizardAnnotationChecker } from './config-wizard-annotation-checker';
18+
export { configWizardAnnotationChecker } from './config-wizard-annotation-checker';

0 commit comments

Comments
 (0)