Skip to content

Commit 66a3fc6

Browse files
committed
Merge branch 'topic/vscode-gnatdas' into 'master'
Add robustness in parsing GNAT DAS XML files Closes #1712 See merge request eng/ide/ada_language_server!2115
2 parents 50979c8 + 4f04fd7 commit 66a3fc6

File tree

2 files changed

+92
-49
lines changed

2 files changed

+92
-49
lines changed

integration/vscode/ada/src/gnatcov.ts

Lines changed: 65 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,14 @@ import { cpus } from 'os';
55
import * as path from 'path';
66
import * as vscode from 'vscode';
77
import { CancellationToken } from 'vscode-languageclient';
8-
import { getMatchingPrefixes, parallelize, staggerProgress, toPosix } from './helpers';
8+
import { logger } from './extension';
9+
import {
10+
getMatchingPrefixes,
11+
parallelize,
12+
showErrorMessageWithOpenLogButton,
13+
staggerProgress,
14+
toPosix,
15+
} from './helpers';
916

1017
/**
1118
* Parsing GNATcoverage XML reports.
@@ -25,7 +32,8 @@ import { getMatchingPrefixes, parallelize, staggerProgress, toPosix } from './he
2532
* where we can give the XML paths that should always be parsed as lists, even
2633
* when one tag is encountered. However if no tags are encountered, the parser
2734
* simply doesn't create the corresponding property in the result object, and
28-
* accessing the property yields `undefined`.
35+
* accessing the property yields `undefined`. For this reason all array
36+
* properties are typed as optional.
2937
*
3038
* Similarly, the parser is configured with a function
3139
* `attributeValueProcessor` allowing to convert attributes to more specific
@@ -55,7 +63,7 @@ type coverage_info_type = {
5563
traces: traces_type;
5664
};
5765
type traces_type = {
58-
trace: trace_type[];
66+
trace?: trace_type[];
5967
};
6068
type trace_type = {
6169
'@_filename': string;
@@ -66,13 +74,13 @@ type trace_type = {
6674
};
6775
type trace_kind_type = 'binary' | 'source';
6876
type coverage_summary_type = {
69-
metric: metric_type[];
70-
obligation_stats: obligation_stats_type[];
71-
file: file_type[];
77+
metric?: metric_type[];
78+
obligation_stats?: obligation_stats_type[];
79+
file?: file_type[];
7280
};
7381
type file_type = {
74-
metric: metric_type[];
75-
obligation_stats: obligation_stats_type[];
82+
metric?: metric_type[];
83+
obligation_stats?: obligation_stats_type[];
7684
'@_name'?: string;
7785
};
7886
type coverage_level_type =
@@ -85,7 +93,7 @@ type coverage_level_type =
8593

8694
type sources_type = {
8795
source?: source_type[];
88-
'xi:include': xi_include_type[];
96+
'xi:include'?: xi_include_type[];
8997
};
9098

9199
type xi_include_type = {
@@ -96,13 +104,13 @@ type xi_include_type = {
96104
export type source_type = {
97105
'@_file': string;
98106
'@_coverage_level': coverage_level_type;
99-
scope_metric: scope_metric_type[];
100-
src_mapping: src_mapping_type[];
107+
scope_metric?: scope_metric_type[];
108+
src_mapping?: src_mapping_type[];
101109
};
102110
type scope_metric_type = {
103-
metric: metric_type[];
104-
obligation_stats: obligation_stats_type[];
105-
scope_metric: scope_metric_type[];
111+
metric?: metric_type[];
112+
obligation_stats?: obligation_stats_type[];
113+
scope_metric?: scope_metric_type[];
106114
'@_scope_name': string;
107115
'@_scope_line': number;
108116
};
@@ -124,18 +132,18 @@ type metric_kind_type =
124132
| 'exempted_undetermined_coverage'
125133
| 'exempted';
126134
type obligation_stats_type = {
127-
metric: metric_type[];
135+
metric?: metric_type[];
128136
'@_kind': string;
129137
};
130138
export type src_mapping_type = {
131139
src: src_type;
132-
statement: statement_type[] | undefined;
133-
decision: decision_type[] | undefined;
134-
message: message_type[] | undefined;
140+
statement?: statement_type[];
141+
decision?: decision_type[];
142+
message?: message_type[];
135143
'@_coverage': coverage_type;
136144
};
137145
type src_type = {
138-
line: line_type[];
146+
line?: line_type[];
139147
};
140148
export type line_type = {
141149
'@_num': number;
@@ -192,7 +200,7 @@ export type coverage_type = (typeof coverage_type_values)[number];
192200

193201
type decision_type = {
194202
src?: src_type;
195-
condition: condition_type[];
203+
condition?: condition_type[];
196204
'@_coverage': coverage_type;
197205
'@_id': number;
198206
'@_text': string;
@@ -357,7 +365,7 @@ export async function addCoverageData(run: vscode.TestRun, covDir: string) {
357365
title: 'Loading GNATcoverage report',
358366
},
359367
async (progress, token) => {
360-
const array = data.coverage_report.coverage_summary!.file;
368+
const array = data.coverage_report.coverage_summary?.file ?? [];
361369
let done: number = 0;
362370
let lastProgress = 0;
363371
const totalFiles = array.length;
@@ -537,9 +545,22 @@ export async function addCoverageData(run: vscode.TestRun, covDir: string) {
537545
`Could not find the file in the workspace: ${file['@_name']}`,
538546
);
539547

540-
const fileReportBasename = data.coverage_report.sources!['xi:include'].find(
548+
const fileReportBasename = data.coverage_report.sources?.[
549+
'xi:include'
550+
]?.find(
541551
(inc) => inc['@_href'] == `${path.posix.basename(srcUri.path)}.xml`,
542-
)!['@_href'];
552+
)?.['@_href'];
553+
554+
if (!fileReportBasename) {
555+
const msg = `Malformed GNATcoverage report ${indexPath}`;
556+
void showErrorMessageWithOpenLogButton(msg);
557+
logger.warn(
558+
`${msg}: cannot find <xi:include> element for source file ` +
559+
`${path.posix.basename(srcUri.path)}`,
560+
);
561+
return undefined;
562+
}
563+
543564
const fileReportPath = path.join(covDir, fileReportBasename);
544565

545566
const stmtStats = getStats(file, 'Stmt') ?? { covered: 0, total: 0 };
@@ -661,13 +682,14 @@ function getStats(
661682
file: file_type,
662683
level: 'Stmt' | 'Decision' | 'MCDC',
663684
): vscode.TestCoverageCount | undefined {
664-
const stats = file.obligation_stats.find((s) => s['@_kind'] == level);
685+
const stats = file.obligation_stats?.find((s) => s['@_kind'] == level);
665686
if (stats) {
666687
const total =
667688
stats?.metric?.find((m) => m['@_kind'] == 'total_obligations_of_relevance')?.[
668689
'@_count'
669690
] ?? 0;
670-
const covered = stats?.metric?.find((m) => m['@_kind'] == 'fully_covered')!['@_count'] ?? 0;
691+
const covered =
692+
stats?.metric?.find((m) => m['@_kind'] == 'fully_covered')?.['@_count'] ?? 0;
671693
return { covered, total };
672694
} else {
673695
return undefined;
@@ -678,16 +700,18 @@ export function convertSourceReport(
678700
data: source_type,
679701
token?: CancellationToken,
680702
): vscode.StatementCoverage[] {
681-
return data.src_mapping
682-
.flatMap((src_mapping) => {
683-
if (token?.isCancellationRequested) {
684-
throw new vscode.CancellationError();
685-
}
703+
return (
704+
data.src_mapping
705+
?.flatMap((src_mapping) => {
706+
if (token?.isCancellationRequested) {
707+
throw new vscode.CancellationError();
708+
}
686709

687-
return convertSrcMapping(src_mapping);
688-
})
689-
.flat()
690-
.filter((v) => !!v);
710+
return convertSrcMapping(src_mapping);
711+
})
712+
.flat()
713+
.filter((v) => !!v) ?? []
714+
);
691715
}
692716

693717
export function convertSrcMapping(src_mapping: src_mapping_type): vscode.StatementCoverage[] {
@@ -785,9 +809,9 @@ export function convertSrcMapping(src_mapping: src_mapping_type): vscode.Stateme
785809
/**
786810
* Add <condition> reports as more branches
787811
*/
788-
const conditions = decision.condition
789-
?.filter((c) => c['@_coverage'] != '.')
790-
?.flatMap((condition) => {
812+
const conditions: vscode.BranchCoverage[] = (decision.condition ?? [])
813+
.filter((c) => c['@_coverage'] != '.')
814+
.flatMap((condition) => {
791815
assert(condition.src);
792816
const mergedText = mergeText(condition.src);
793817
const messages = getMessages(condition, src_mapping);
@@ -799,7 +823,7 @@ export function convertSrcMapping(src_mapping: src_mapping_type): vscode.Stateme
799823
(m) =>
800824
new vscode.BranchCoverage(
801825
m['@_kind'] == 'notice',
802-
toRange(condition.src!),
826+
condition.src ? toRange(condition.src) : undefined,
803827

804828
`condition ${m['@_kind']}: '${mergedText}' ${m['@_message']}`,
805829
),
@@ -845,7 +869,7 @@ function getMessages(item: { '@_id': number }, src_mapping: src_mapping_type) {
845869
* @returns the joined lines spanned by the src object
846870
*/
847871
function mergeText(src: src_type | undefined): string {
848-
return src?.line.map((l) => l['@_src'].trim()).join(' ') ?? '';
872+
return src?.line?.map((l) => l['@_src'].trim()).join(' ') ?? '';
849873
}
850874

851875
/**
@@ -858,9 +882,9 @@ function toRange(src: src_type): vscode.Range {
858882
* The <src> object may contain multiple <line>s, so we need to compute the
859883
* region start and end based on the first and last lines.
860884
*/
861-
const firstLine = src.line.at(0);
885+
const firstLine = src.line?.at(0);
862886
assert(firstLine);
863-
const lastLine = src.line.at(-1);
887+
const lastLine = src.line?.at(-1);
864888
assert(lastLine);
865889
const range = new vscode.Range(
866890
firstLine['@_num'] - 1,

integration/vscode/ada/src/gnattest.ts

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,32 +34,51 @@ let fileLoadController: vscode.TestController;
3434
* generated by src/test-mapping.adb in the libadalang-tools repository. However
3535
* these types do not describe the entire XML structure. Only the elements used
3636
* are described.
37+
*
38+
* Moreover, fast-xml-parser does very basic parsing which gives little
39+
* guarantees over the returned structure. For instance, if a <parent> is meant
40+
* to have 0 or more <child> elements, then the parsed parent object can be
41+
* in the following cases:
42+
*
43+
* a. if there are 0 children, the parent object has no "child" property.
44+
* b. if there is 1 child, the parent object has a "child" property which is
45+
* the parsed child object.
46+
* c. if there are 2 or more children, the parent object has a "child" property
47+
* which is an array of parsed child objects.
48+
*
49+
* To alleviate that, the {@link alwaysArray} list contains the XML paths that
50+
* should always be treated as arrays. This allows both cases b. and c. to be
51+
* treated uniformly as arrays. However in case a. the parent object will not
52+
* have a "child" property at all and there is no way to change that behavior
53+
* to get an array.
54+
*
55+
* Consequently, all array properties in the types below are typed as optional.
3756
*/
3857
export type Root = {
3958
tests_mapping: TestMapping;
4059
};
4160

4261
type TestMapping = {
4362
'@_mode': string;
44-
unit: Unit[];
45-
additional_tests: object[];
63+
unit?: Unit[];
64+
additional_tests?: object[];
4665
};
4766

4867
type Unit = {
4968
'@_source_file': string;
50-
test_unit: TestUnit[];
69+
test_unit?: TestUnit[];
5170
};
5271

5372
type TestUnit = {
5473
'@_target_file': string;
55-
tested: Tested[];
74+
tested?: Tested[];
5675
};
5776

5877
type Tested = {
5978
'@_line': string;
6079
'@_column': string;
6180
'@_name': string;
62-
test_case: TestCase[];
81+
test_case?: TestCase[];
6382
};
6483

6584
type TestCase = {
@@ -225,7 +244,7 @@ export async function addTestsRootLevel() {
225244
if (fs.existsSync(await getGnatTestXmlPath())) {
226245
const xmlDoc: Root = await parseGnatTestXml();
227246
const rootNode = xmlDoc.tests_mapping;
228-
for (const u of rootNode.unit) {
247+
for (const u of rootNode.unit ?? []) {
229248
await addUnitItem(u);
230249
}
231250
}
@@ -291,7 +310,7 @@ async function addUnitItem(unit: Unit) {
291310
* @param unit - the corresponding Unit node from the GNATtest XML
292311
*/
293312
function resolveUnitItem(testItem: TestItem, unit: Unit) {
294-
for (const t of unit.test_unit.flatMap((u) => u.tested)) {
313+
for (const t of unit.test_unit?.flatMap((u) => u.tested ?? []) ?? []) {
295314
addTestedItem(testItem, t);
296315
}
297316
}
@@ -344,7 +363,7 @@ function addTestedItem(parentTestItem: vscode.TestItem, tested: Tested) {
344363
* @param tested - the corresponding "Tested" node in the GNATtest XML
345364
*/
346365
async function resolveTestedItem(testItem: TestItem, tested: Tested) {
347-
for (const e of tested.test_case) {
366+
for (const e of tested.test_case ?? []) {
348367
await addTestCaseItem(testItem, e);
349368
}
350369
}

0 commit comments

Comments
 (0)