Skip to content

Commit fc77a52

Browse files
Merge pull request #2614 from github/robertbrignull/Locations
Move components for rendering locations to a separate file
2 parents 5c12a4b + 76a7a26 commit fc77a52

8 files changed

Lines changed: 294 additions & 187 deletions

File tree

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as React from "react";
22

3-
import { renderLocation } from "./result-table-utils";
3+
import { Location } from "./locations/Location";
44
import { CellValue } from "../../common/bqrs-cli-types";
55

66
interface Props {
@@ -16,14 +16,15 @@ export default function RawTableValue(props: Props): JSX.Element {
1616
typeof rawValue === "number" ||
1717
typeof rawValue === "boolean"
1818
) {
19-
return <span>{renderLocation(undefined, rawValue.toString())}</span>;
19+
return <Location label={rawValue.toString()} />;
2020
}
2121

22-
return renderLocation(
23-
rawValue.url,
24-
rawValue.label,
25-
props.databaseUri,
26-
undefined,
27-
props.onSelected,
22+
return (
23+
<Location
24+
loc={rawValue.url}
25+
label={rawValue.label}
26+
databaseUri={props.databaseUri}
27+
onClick={props.onSelected}
28+
/>
2829
);
2930
}

extensions/ql-vscode/src/view/results/alert-table.tsx

Lines changed: 45 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
1-
import { basename } from "path";
21
import * as React from "react";
32
import * as Sarif from "sarif";
43
import * as Keys from "./result-keys";
54
import { chevronDown, chevronRight, info, listUnordered } from "./octicons";
65
import {
76
className,
8-
renderLocation,
97
ResultTableProps,
108
selectableZebraStripe,
119
jumpToLocation,
@@ -18,15 +16,12 @@ import {
1816
NavigationDirection,
1917
SarifInterpretationData,
2018
} from "../../common/interface-types";
21-
import {
22-
parseSarifPlainTextMessage,
23-
parseSarifLocation,
24-
isNoLocation,
25-
} from "../../common/sarif-utils";
26-
import { isWholeFileLoc, isLineColumnLoc } from "../../common/bqrs-utils";
19+
import { parseSarifLocation, isNoLocation } from "../../common/sarif-utils";
2720
import { ScrollIntoViewHelper } from "./scroll-into-view-helper";
2821
import { sendTelemetry } from "../common/telemetry";
2922
import { AlertTableHeader } from "./alert-table-header";
23+
import { SarifMessageWithLocations } from "./locations/SarifMessageWithLocations";
24+
import { SarifLocation } from "./locations/SarifLocation";
3025

3126
export type AlertTableProps = ResultTableProps & {
3227
resultSet: InterpretedResultSet<SarifInterpretationData>;
@@ -100,41 +95,6 @@ export class AlertTable extends React.Component<
10095
const { numTruncatedResults, sourceLocationPrefix } =
10196
resultSet.interpretation;
10297

103-
function renderRelatedLocations(
104-
msg: string,
105-
relatedLocations: Sarif.Location[],
106-
resultKey: Keys.PathNode | Keys.Result | undefined,
107-
): JSX.Element[] {
108-
const relatedLocationsById: { [k: string]: Sarif.Location } = {};
109-
for (const loc of relatedLocations) {
110-
relatedLocationsById[loc.id!] = loc;
111-
}
112-
113-
// match things like `[link-text](related-location-id)`
114-
const parts = parseSarifPlainTextMessage(msg);
115-
116-
return parts.map((part, i) => {
117-
if (typeof part === "string") {
118-
return <span key={i}>{part}</span>;
119-
} else {
120-
const renderedLocation = renderSarifLocationWithText(
121-
part.text,
122-
relatedLocationsById[part.dest],
123-
resultKey,
124-
);
125-
return <span key={i}>{renderedLocation}</span>;
126-
}
127-
});
128-
}
129-
130-
function renderNonLocation(
131-
msg: string | undefined,
132-
locationHint: string,
133-
): JSX.Element | undefined {
134-
if (msg === undefined) return undefined;
135-
return <span title={locationHint}>{msg}</span>;
136-
}
137-
13898
const updateSelectionCallback = (
13999
resultKey: Keys.PathNode | Keys.Result | undefined,
140100
) => {
@@ -147,65 +107,6 @@ export class AlertTable extends React.Component<
147107
};
148108
};
149109

150-
function renderSarifLocationWithText(
151-
text: string | undefined,
152-
loc: Sarif.Location,
153-
resultKey: Keys.PathNode | Keys.Result | undefined,
154-
): JSX.Element | undefined {
155-
const parsedLoc = parseSarifLocation(loc, sourceLocationPrefix);
156-
if ("hint" in parsedLoc) {
157-
return renderNonLocation(text, parsedLoc.hint);
158-
} else if (isWholeFileLoc(parsedLoc) || isLineColumnLoc(parsedLoc)) {
159-
return renderLocation(
160-
parsedLoc,
161-
text,
162-
databaseUri,
163-
undefined,
164-
updateSelectionCallback(resultKey),
165-
);
166-
} else {
167-
return undefined;
168-
}
169-
}
170-
171-
/**
172-
* Render sarif location as a link with the text being simply a
173-
* human-readable form of the location itself.
174-
*/
175-
function renderSarifLocation(
176-
loc: Sarif.Location,
177-
pathNodeKey: Keys.PathNode | Keys.Result | undefined,
178-
): JSX.Element | undefined {
179-
const parsedLoc = parseSarifLocation(loc, sourceLocationPrefix);
180-
if ("hint" in parsedLoc) {
181-
return renderNonLocation("[no location]", parsedLoc.hint);
182-
} else if (isWholeFileLoc(parsedLoc)) {
183-
const shortLocation = `${basename(parsedLoc.userVisibleFile)}`;
184-
const longLocation = `${parsedLoc.userVisibleFile}`;
185-
return renderLocation(
186-
parsedLoc,
187-
shortLocation,
188-
databaseUri,
189-
longLocation,
190-
updateSelectionCallback(pathNodeKey),
191-
);
192-
} else if (isLineColumnLoc(parsedLoc)) {
193-
const shortLocation = `${basename(parsedLoc.userVisibleFile)}:${
194-
parsedLoc.startLine
195-
}:${parsedLoc.startColumn}`;
196-
const longLocation = `${parsedLoc.userVisibleFile}`;
197-
return renderLocation(
198-
parsedLoc,
199-
shortLocation,
200-
databaseUri,
201-
longLocation,
202-
updateSelectionCallback(pathNodeKey),
203-
);
204-
} else {
205-
return undefined;
206-
}
207-
}
208-
209110
const toggler: (keys: Keys.ResultKey[]) => (e: React.MouseEvent) => void = (
210111
indices,
211112
) => {
@@ -220,19 +121,32 @@ export class AlertTable extends React.Component<
220121
(result, resultIndex) => {
221122
const resultKey: Keys.Result = { resultIndex };
222123
const text = result.message.text || "[no text]";
223-
const msg: JSX.Element[] =
224-
result.relatedLocations === undefined
225-
? [<span key="0">{text}</span>]
226-
: renderRelatedLocations(text, result.relatedLocations, resultKey);
124+
const msg =
125+
result.relatedLocations === undefined ? (
126+
<span key="0">{text}</span>
127+
) : (
128+
<SarifMessageWithLocations
129+
msg={text}
130+
relatedLocations={result.relatedLocations}
131+
sourceLocationPrefix={sourceLocationPrefix}
132+
databaseUri={databaseUri}
133+
onClick={updateSelectionCallback(resultKey)}
134+
/>
135+
);
227136

228137
const currentResultExpanded = this.state.expanded.has(
229138
Keys.keyToString(resultKey),
230139
);
231140
const indicator = currentResultExpanded ? chevronDown : chevronRight;
232-
const location =
233-
result.locations !== undefined &&
234-
result.locations.length > 0 &&
235-
renderSarifLocation(result.locations[0], resultKey);
141+
const location = result.locations !== undefined &&
142+
result.locations.length > 0 && (
143+
<SarifLocation
144+
loc={result.locations[0]}
145+
sourceLocationPrefix={sourceLocationPrefix}
146+
databaseUri={databaseUri}
147+
onClick={updateSelectionCallback(resultKey)}
148+
/>
149+
);
236150
const locationCells = (
237151
<td className="vscode-codeql__location-cell">{location}</td>
238152
);
@@ -342,17 +256,28 @@ export class AlertTable extends React.Component<
342256
const step = pathNodes[pathNodeIndex];
343257
const msg =
344258
step.location !== undefined &&
345-
step.location.message !== undefined
346-
? renderSarifLocationWithText(
347-
step.location.message.text,
348-
step.location,
349-
pathNodeKey,
350-
)
351-
: "[no location]";
259+
step.location.message !== undefined ? (
260+
<SarifLocation
261+
text={step.location.message.text}
262+
loc={step.location}
263+
sourceLocationPrefix={sourceLocationPrefix}
264+
databaseUri={databaseUri}
265+
onClick={updateSelectionCallback(pathNodeKey)}
266+
/>
267+
) : (
268+
"[no location]"
269+
);
352270
const additionalMsg =
353-
step.location !== undefined
354-
? renderSarifLocation(step.location, pathNodeKey)
355-
: "";
271+
step.location !== undefined ? (
272+
<SarifLocation
273+
loc={step.location}
274+
sourceLocationPrefix={sourceLocationPrefix}
275+
databaseUri={databaseUri}
276+
onClick={updateSelectionCallback(pathNodeKey)}
277+
/>
278+
) : (
279+
""
280+
);
356281
const isSelected = Keys.equalsNotUndefined(
357282
this.state.selectedItem,
358283
pathNodeKey,
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import * as React from "react";
2+
import { useCallback } from "react";
3+
import { ResolvableLocationValue } from "../../../common/bqrs-cli-types";
4+
import { jumpToLocation } from "../result-table-utils";
5+
6+
interface Props {
7+
loc: ResolvableLocationValue;
8+
label: string;
9+
databaseUri: string;
10+
title?: string;
11+
onClick?: () => void;
12+
}
13+
14+
/**
15+
* A clickable location link.
16+
*/
17+
export function ClickableLocation({
18+
loc,
19+
label,
20+
databaseUri,
21+
title,
22+
onClick: onClick,
23+
}: Props): JSX.Element {
24+
const handleClick = useCallback(
25+
(e: React.MouseEvent) => {
26+
e.preventDefault();
27+
e.stopPropagation();
28+
jumpToLocation(loc, databaseUri);
29+
onClick?.();
30+
},
31+
[loc, databaseUri, onClick],
32+
);
33+
34+
return (
35+
<>
36+
{/*
37+
eslint-disable-next-line
38+
jsx-a11y/anchor-is-valid,
39+
*/}
40+
<a
41+
href="#"
42+
className="vscode-codeql__result-table-location-link"
43+
title={title}
44+
onClick={handleClick}
45+
>
46+
{label}
47+
</a>
48+
</>
49+
);
50+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import * as React from "react";
2+
import { useMemo } from "react";
3+
4+
import { UrlValue } from "../../../common/bqrs-cli-types";
5+
import {
6+
isStringLoc,
7+
tryGetResolvableLocation,
8+
} from "../../../common/bqrs-utils";
9+
import { convertNonPrintableChars } from "../../../common/text-utils";
10+
import { NonClickableLocation } from "./NonClickableLocation";
11+
import { ClickableLocation } from "./ClickableLocation";
12+
13+
interface Props {
14+
loc?: UrlValue;
15+
label?: string;
16+
databaseUri?: string;
17+
title?: string;
18+
onClick?: () => void;
19+
}
20+
21+
/**
22+
* A location link. Will be clickable if a location URL and database URI are provided.
23+
*/
24+
export function Location({
25+
loc,
26+
label,
27+
databaseUri,
28+
title,
29+
onClick,
30+
}: Props): JSX.Element {
31+
const resolvableLoc = useMemo(() => tryGetResolvableLocation(loc), [loc]);
32+
const displayLabel = useMemo(() => convertNonPrintableChars(label), [label]);
33+
34+
if (loc === undefined) {
35+
return <NonClickableLocation msg={displayLabel} />;
36+
}
37+
38+
if (isStringLoc(loc)) {
39+
return <a href={loc}>{loc}</a>;
40+
}
41+
42+
if (databaseUri === undefined || resolvableLoc === undefined) {
43+
return <NonClickableLocation msg={displayLabel} locationHint={title} />;
44+
}
45+
46+
return (
47+
<ClickableLocation
48+
loc={resolvableLoc}
49+
label={displayLabel}
50+
databaseUri={databaseUri}
51+
title={title}
52+
onClick={onClick}
53+
/>
54+
);
55+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import * as React from "react";
2+
3+
interface Props {
4+
msg?: string;
5+
locationHint?: string;
6+
}
7+
8+
/**
9+
* A non-clickable location for when there isn't a valid link.
10+
* Designed to fit in with the other types of location components.
11+
*/
12+
export function NonClickableLocation({ msg, locationHint }: Props) {
13+
if (msg === undefined) return null;
14+
return <span title={locationHint}>{msg}</span>;
15+
}

0 commit comments

Comments
 (0)