forked from github/vscode-codeql
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsarif-utils.ts
More file actions
303 lines (272 loc) · 9.81 KB
/
sarif-utils.ts
File metadata and controls
303 lines (272 loc) · 9.81 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
import type { Location, Region, Result } from "sarif";
import type { HighlightedRegion } from "../variant-analysis/shared/analysis-result";
import type { UrlValueResolvable } from "./raw-result-types";
import { isEmptyPath } from "./bqrs-utils";
export interface SarifLink {
dest: number;
text: string;
}
// The type of a result that has no associated location.
// hint is a string intended for display to the user
// that explains why there is no location.
interface NoLocation {
hint: string;
}
type ParsedSarifLocation =
| (UrlValueResolvable & {
userVisibleFile: string;
})
// Resolvable locations have a `uri` field, but it will sometimes include
// a source location prefix, which contains build-specific information the user
// doesn't really need to see. We ensure that `userVisibleFile` will not contain
// that, and is appropriate for display in the UI.
| NoLocation;
type SarifMessageComponent = string | SarifLink;
/**
* Unescape "[", "]" and "\\" like in sarif plain text messages
*/
export function unescapeSarifText(message: string): string {
return message
.replace(/\\\[/g, "[")
.replace(/\\\]/g, "]")
.replace(/\\\\/g, "\\");
}
export function parseSarifPlainTextMessage(
message: string,
): SarifMessageComponent[] {
const results: SarifMessageComponent[] = [];
// We want something like "[linkText](4)", except that "[" and "]" may be escaped. The lookbehind asserts
// that the initial [ is not escaped. Then we parse a link text with "[" and "]" escaped. Then we parse the numerical target.
// Technically we could have any uri in the target but we don't output that yet.
// The possibility of escaping outside the link is not mentioned in the sarif spec but we always output sartif this way.
const linkRegex =
/(?<=(?<!\\)(\\\\)*)\[(?<linkText>([^\\\][]|\\\\|\\\]|\\\[)*)\]\((?<linkTarget>[0-9]+)\)/g;
let result: RegExpExecArray | null;
let curIndex = 0;
while ((result = linkRegex.exec(message)) !== null) {
results.push(unescapeSarifText(message.substring(curIndex, result.index)));
const linkText = result.groups!["linkText"];
const linkTarget = +result.groups!["linkTarget"];
results.push({ dest: linkTarget, text: unescapeSarifText(linkText) });
curIndex = result.index + result[0].length;
}
results.push(unescapeSarifText(message.substring(curIndex, message.length)));
return results;
}
/**
* Computes a path normalized to reflect conventional normalization
* of windows paths into zip archive paths.
* @param sourceLocationPrefix The source location prefix of a database. May be
* unix style `/foo/bar/baz` or windows-style `C:\foo\bar\baz`.
* @param sarifRelativeUri A uri relative to sourceLocationPrefix.
*
* @returns A URI string that is valid for the `.file` field of a `FivePartLocation`:
* directory separators are normalized, but drive letters `C:` may appear.
*/
export function getPathRelativeToSourceLocationPrefix(
sourceLocationPrefix: string,
sarifRelativeUri: string,
) {
// convert a platform specific path into encoded path uri segments
// need to be careful about drive letters and ensure that there
// is a starting '/'
let prefix = "";
if (sourceLocationPrefix[1] === ":") {
// assume this is a windows drive separator
prefix = sourceLocationPrefix.substring(0, 2);
sourceLocationPrefix = sourceLocationPrefix.substring(2);
}
const normalizedSourceLocationPrefix =
prefix +
sourceLocationPrefix
.replace(/\\/g, "/")
.split("/")
.map(encodeURIComponent)
.join("/");
const slashPrefix = normalizedSourceLocationPrefix.startsWith("/") ? "" : "/";
return `file:${
slashPrefix + normalizedSourceLocationPrefix
}/${sarifRelativeUri}`;
}
/**
*
* @param loc specifies the database-relative location of the source location
* @param sourceLocationPrefix a file path (usually a full path) to the database containing the source location.
*/
export function parseSarifLocation(
loc: Location,
sourceLocationPrefix: string,
): ParsedSarifLocation {
const physicalLocation = loc.physicalLocation;
if (physicalLocation === undefined) {
return { hint: "no physical location" };
}
if (physicalLocation.artifactLocation === undefined) {
return { hint: "no artifact location" };
}
if (physicalLocation.artifactLocation.uri === undefined) {
return { hint: "artifact location has no uri" };
}
if (isEmptyPath(physicalLocation.artifactLocation.uri)) {
return { hint: "artifact location has empty uri" };
}
// This is not necessarily really an absolute uri; it could either be a
// file uri or a relative uri.
const uri = physicalLocation.artifactLocation.uri;
const fileUriRegex = /^file:/;
const hasFilePrefix = uri.match(fileUriRegex);
const effectiveLocation = hasFilePrefix
? uri
: getPathRelativeToSourceLocationPrefix(sourceLocationPrefix, uri);
const userVisibleFile = decodeURIComponent(
hasFilePrefix ? uri.replace(fileUriRegex, "") : uri,
);
if (physicalLocation.region === undefined) {
// If the region property is absent, the physicalLocation object refers to the entire file.
// Source: https://docs.oasis-open.org/sarif/sarif/v2.1.0/cs01/sarif-v2.1.0-cs01.html#_Toc16012638.
return {
type: "wholeFileLocation",
uri: effectiveLocation,
userVisibleFile,
} as ParsedSarifLocation;
} else {
const region = parseSarifRegion(physicalLocation.region);
return {
type: "lineColumnLocation",
uri: effectiveLocation,
userVisibleFile,
...region,
};
}
}
export function parseSarifRegion(region: Region): {
startLine: number;
endLine: number;
startColumn: number;
endColumn: number;
} {
// The SARIF we're given should have a startLine, but we
// fall back to 1, just in case something has gone wrong.
const startLine = region.startLine ?? 1;
// These defaults are from SARIF 2.1.0 spec, section 3.30.2, "Text Regions"
// https://docs.oasis-open.org/sarif/sarif/v2.1.0/cs01/sarif-v2.1.0-cs01.html#_Ref493492556
const endLine = region.endLine === undefined ? startLine : region.endLine;
const startColumn = region.startColumn === undefined ? 1 : region.startColumn;
// Our tools should always supply `endColumn` field, which is fortunate, since
// the SARIF spec says that it defaults to the end of the line, whose
// length we don't know at this point in the code. We fall back to 1,
// just in case something has gone wrong.
//
// It is off by one with respect to the way vscode counts columns in selections.
const endColumn = (region.endColumn ?? 1) - 1;
return {
startLine,
startColumn,
endLine,
endColumn,
};
}
export function isNoLocation(loc: ParsedSarifLocation): loc is NoLocation {
return "hint" in loc;
}
// Some helpers for highlighting specific regions from a SARIF code snippet
/**
* Checks whether a particular line (determined by its line number in the original file)
* is part of the highlighted region of a SARIF code snippet.
*/
export function shouldHighlightLine(
lineNumber: number,
highlightedRegion: HighlightedRegion,
): boolean {
if (lineNumber < highlightedRegion.startLine) {
return false;
}
if (highlightedRegion.endLine === undefined) {
return lineNumber === highlightedRegion.startLine;
}
return lineNumber <= highlightedRegion.endLine;
}
/**
* A line of code split into: plain text before the highlighted section, the highlighted
* text itself, and plain text after the highlighted section.
*/
interface PartiallyHighlightedLine {
plainSection1: string;
highlightedSection: string;
plainSection2: string;
}
/**
* Splits a line of code into the highlighted and non-highlighted sections.
*/
export function parseHighlightedLine(
line: string,
lineNumber: number,
highlightedRegion: HighlightedRegion,
): PartiallyHighlightedLine {
const isSingleLineHighlight = highlightedRegion.endLine === undefined;
const isFirstHighlightedLine = lineNumber === highlightedRegion.startLine;
const isLastHighlightedLine = lineNumber === highlightedRegion.endLine;
const highlightStartColumn = isSingleLineHighlight
? highlightedRegion.startColumn
: isFirstHighlightedLine
? highlightedRegion.startColumn
: 0;
const highlightEndColumn = isSingleLineHighlight
? highlightedRegion.endColumn
: isLastHighlightedLine
? highlightedRegion.endColumn
: line.length + 1;
const plainSection1 = line.substring(0, highlightStartColumn - 1);
const highlightedSection = line.substring(
highlightStartColumn - 1,
highlightEndColumn - 1,
);
const plainSection2 = line.substring(highlightEndColumn - 1, line.length);
return { plainSection1, highlightedSection, plainSection2 };
}
/**
* Normalizes a file URI to a plain path for comparison purposes.
* Strips the `file:` scheme prefix and decodes URI components.
*/
export function normalizeFileUri(uri: string): string {
try {
const path = uri.replace(/^file:\/*/, "/");
return decodeURIComponent(path);
} catch {
return uri.replace(/^file:\/*/, "/");
}
}
interface ParsedResultLocation {
uri: string;
startLine?: number;
endLine?: number;
}
/**
* Extracts all locations from a SARIF result, including relatedLocations.
*/
export function getLocationsFromSarifResult(
result: Result,
sourceLocationPrefix: string,
): ParsedResultLocation[] {
const sarifLocations: Location[] = [
...(result.locations ?? []),
...(result.relatedLocations ?? []),
];
const parsed: ParsedResultLocation[] = [];
for (const loc of sarifLocations) {
const p = parseSarifLocation(loc, sourceLocationPrefix);
if ("hint" in p) {
continue;
}
if (p.type === "wholeFileLocation") {
parsed.push({ uri: p.uri });
} else if (p.type === "lineColumnLocation") {
parsed.push({
uri: p.uri,
startLine: p.startLine,
endLine: p.endLine,
});
}
}
return parsed;
}