diff --git a/extensions/ql-vscode/src/view/results/RawTableValue.tsx b/extensions/ql-vscode/src/view/results/RawTableValue.tsx
index 531ea0e91..2f336ec76 100644
--- a/extensions/ql-vscode/src/view/results/RawTableValue.tsx
+++ b/extensions/ql-vscode/src/view/results/RawTableValue.tsx
@@ -1,6 +1,6 @@
import * as React from "react";
-import { renderLocation } from "./result-table-utils";
+import { Location } from "./locations";
import { CellValue } from "../../common/bqrs-cli-types";
interface Props {
@@ -16,14 +16,15 @@ export default function RawTableValue(props: Props): JSX.Element {
typeof rawValue === "number" ||
typeof rawValue === "boolean"
) {
- return {renderLocation(undefined, rawValue.toString())};
+ return ;
}
- return renderLocation(
- rawValue.url,
- rawValue.label,
- props.databaseUri,
- undefined,
- props.onSelected,
+ return (
+
);
}
diff --git a/extensions/ql-vscode/src/view/results/alert-table.tsx b/extensions/ql-vscode/src/view/results/alert-table.tsx
index 34e7630fc..62fb30c52 100644
--- a/extensions/ql-vscode/src/view/results/alert-table.tsx
+++ b/extensions/ql-vscode/src/view/results/alert-table.tsx
@@ -1,11 +1,9 @@
-import { basename } from "path";
import * as React from "react";
import * as Sarif from "sarif";
import * as Keys from "./result-keys";
import { chevronDown, chevronRight, info, listUnordered } from "./octicons";
import {
className,
- renderLocation,
ResultTableProps,
selectableZebraStripe,
jumpToLocation,
@@ -18,15 +16,11 @@ import {
NavigationDirection,
SarifInterpretationData,
} from "../../common/interface-types";
-import {
- parseSarifPlainTextMessage,
- parseSarifLocation,
- isNoLocation,
-} from "../../common/sarif-utils";
-import { isWholeFileLoc, isLineColumnLoc } from "../../common/bqrs-utils";
+import { parseSarifLocation, isNoLocation } from "../../common/sarif-utils";
import { ScrollIntoViewHelper } from "./scroll-into-view-helper";
import { sendTelemetry } from "../common/telemetry";
import { AlertTableHeader } from "./alert-table-header";
+import { SarifLocation, SarifMessageWithLocations } from "./locations";
export type AlertTableProps = ResultTableProps & {
resultSet: InterpretedResultSet;
@@ -100,41 +94,6 @@ export class AlertTable extends React.Component<
const { numTruncatedResults, sourceLocationPrefix } =
resultSet.interpretation;
- function renderRelatedLocations(
- msg: string,
- relatedLocations: Sarif.Location[],
- resultKey: Keys.PathNode | Keys.Result | undefined,
- ): JSX.Element[] {
- const relatedLocationsById: { [k: string]: Sarif.Location } = {};
- for (const loc of relatedLocations) {
- relatedLocationsById[loc.id!] = loc;
- }
-
- // match things like `[link-text](related-location-id)`
- const parts = parseSarifPlainTextMessage(msg);
-
- return parts.map((part, i) => {
- if (typeof part === "string") {
- return {part};
- } else {
- const renderedLocation = renderSarifLocationWithText(
- part.text,
- relatedLocationsById[part.dest],
- resultKey,
- );
- return {renderedLocation};
- }
- });
- }
-
- function renderNonLocation(
- msg: string | undefined,
- locationHint: string,
- ): JSX.Element | undefined {
- if (msg === undefined) return undefined;
- return {msg};
- }
-
const updateSelectionCallback = (
resultKey: Keys.PathNode | Keys.Result | undefined,
) => {
@@ -147,65 +106,6 @@ export class AlertTable extends React.Component<
};
};
- function renderSarifLocationWithText(
- text: string | undefined,
- loc: Sarif.Location,
- resultKey: Keys.PathNode | Keys.Result | undefined,
- ): JSX.Element | undefined {
- const parsedLoc = parseSarifLocation(loc, sourceLocationPrefix);
- if ("hint" in parsedLoc) {
- return renderNonLocation(text, parsedLoc.hint);
- } else if (isWholeFileLoc(parsedLoc) || isLineColumnLoc(parsedLoc)) {
- return renderLocation(
- parsedLoc,
- text,
- databaseUri,
- undefined,
- updateSelectionCallback(resultKey),
- );
- } else {
- return undefined;
- }
- }
-
- /**
- * Render sarif location as a link with the text being simply a
- * human-readable form of the location itself.
- */
- function renderSarifLocation(
- loc: Sarif.Location,
- pathNodeKey: Keys.PathNode | Keys.Result | undefined,
- ): JSX.Element | undefined {
- const parsedLoc = parseSarifLocation(loc, sourceLocationPrefix);
- if ("hint" in parsedLoc) {
- return renderNonLocation("[no location]", parsedLoc.hint);
- } else if (isWholeFileLoc(parsedLoc)) {
- const shortLocation = `${basename(parsedLoc.userVisibleFile)}`;
- const longLocation = `${parsedLoc.userVisibleFile}`;
- return renderLocation(
- parsedLoc,
- shortLocation,
- databaseUri,
- longLocation,
- updateSelectionCallback(pathNodeKey),
- );
- } else if (isLineColumnLoc(parsedLoc)) {
- const shortLocation = `${basename(parsedLoc.userVisibleFile)}:${
- parsedLoc.startLine
- }:${parsedLoc.startColumn}`;
- const longLocation = `${parsedLoc.userVisibleFile}`;
- return renderLocation(
- parsedLoc,
- shortLocation,
- databaseUri,
- longLocation,
- updateSelectionCallback(pathNodeKey),
- );
- } else {
- return undefined;
- }
- }
-
const toggler: (keys: Keys.ResultKey[]) => (e: React.MouseEvent) => void = (
indices,
) => {
@@ -220,19 +120,32 @@ export class AlertTable extends React.Component<
(result, resultIndex) => {
const resultKey: Keys.Result = { resultIndex };
const text = result.message.text || "[no text]";
- const msg: JSX.Element[] =
- result.relatedLocations === undefined
- ? [{text}]
- : renderRelatedLocations(text, result.relatedLocations, resultKey);
+ const msg =
+ result.relatedLocations === undefined ? (
+ {text}
+ ) : (
+
+ );
const currentResultExpanded = this.state.expanded.has(
Keys.keyToString(resultKey),
);
const indicator = currentResultExpanded ? chevronDown : chevronRight;
- const location =
- result.locations !== undefined &&
- result.locations.length > 0 &&
- renderSarifLocation(result.locations[0], resultKey);
+ const location = result.locations !== undefined &&
+ result.locations.length > 0 && (
+
+ );
const locationCells = (
| {location} |
);
@@ -342,17 +255,32 @@ export class AlertTable extends React.Component<
const step = pathNodes[pathNodeIndex];
const msg =
step.location !== undefined &&
- step.location.message !== undefined
- ? renderSarifLocationWithText(
- step.location.message.text,
- step.location,
+ step.location.message !== undefined ? (
+
+ ) : (
+ "[no location]"
+ );
const additionalMsg =
- step.location !== undefined
- ? renderSarifLocation(step.location, pathNodeKey)
- : "";
+ step.location !== undefined ? (
+
+ ) : (
+ ""
+ );
const isSelected = Keys.equalsNotUndefined(
this.state.selectedItem,
pathNodeKey,
diff --git a/extensions/ql-vscode/src/view/results/locations.tsx b/extensions/ql-vscode/src/view/results/locations.tsx
new file mode 100644
index 000000000..055cfcf08
--- /dev/null
+++ b/extensions/ql-vscode/src/view/results/locations.tsx
@@ -0,0 +1,213 @@
+import * as React from "react";
+import * as Sarif from "sarif";
+import { ResolvableLocationValue, UrlValue } from "../../common/bqrs-cli-types";
+import { convertNonPrintableChars } from "../../common/text-utils";
+import {
+ isLineColumnLoc,
+ isStringLoc,
+ isWholeFileLoc,
+ tryGetResolvableLocation,
+} from "../../common/bqrs-utils";
+import {
+ parseSarifLocation,
+ parseSarifPlainTextMessage,
+} from "../../common/sarif-utils";
+import { basename } from "path";
+import { jumpToLocation } from "./result-table-utils";
+import { useCallback, useMemo } from "react";
+
+function NonLocation({
+ msg,
+ locationHint,
+}: {
+ msg?: string;
+ locationHint?: string;
+}) {
+ if (msg === undefined) return null;
+ return {msg};
+}
+
+function ClickableLocation({
+ loc,
+ label,
+ databaseUri,
+ title,
+ jumpToLocationCallback,
+}: {
+ loc: ResolvableLocationValue;
+ label: string;
+ databaseUri: string;
+ title?: string;
+ jumpToLocationCallback?: () => void;
+}): JSX.Element {
+ const jumpToLocationHandler = useCallback(
+ (e: React.MouseEvent) => {
+ jumpToLocation(loc, databaseUri);
+ e.preventDefault();
+ e.stopPropagation();
+ if (jumpToLocationCallback) {
+ jumpToLocationCallback();
+ }
+ },
+ [loc, databaseUri, jumpToLocationCallback],
+ );
+
+ return (
+ <>
+ {/*
+ eslint-disable-next-line
+ jsx-a11y/anchor-is-valid,
+ */}
+
+ {label}
+
+ >
+ );
+}
+
+/**
+ * A clickable location link.
+ */
+export function Location({
+ loc,
+ label,
+ databaseUri,
+ title,
+ jumpToLocationCallback,
+}: {
+ loc?: UrlValue;
+ label?: string;
+ databaseUri?: string;
+ title?: string;
+ jumpToLocationCallback?: () => void;
+}): JSX.Element {
+ const resolvableLoc = useMemo(() => tryGetResolvableLocation(loc), [loc]);
+ const displayLabel = useMemo(() => convertNonPrintableChars(label!), [label]);
+ if (loc === undefined) {
+ return ;
+ } else if (isStringLoc(loc)) {
+ return {loc};
+ } else if (databaseUri === undefined || resolvableLoc === undefined) {
+ return ;
+ } else {
+ return (
+
+ );
+ }
+}
+
+/**
+ * A clickable SARIF location link.
+ *
+ * Custom text can be provided, but otherwise the text will be
+ * a human-readable form of the location itself.
+ */
+export function SarifLocation({
+ text,
+ loc,
+ sourceLocationPrefix,
+ databaseUri,
+ jumpToLocationCallback,
+}: {
+ text?: string;
+ loc?: Sarif.Location;
+ sourceLocationPrefix: string;
+ databaseUri: string;
+ jumpToLocationCallback: () => void;
+}) {
+ const parsedLoc = useMemo(
+ () => loc && parseSarifLocation(loc, sourceLocationPrefix),
+ [loc, sourceLocationPrefix],
+ );
+ if (parsedLoc === undefined || "hint" in parsedLoc) {
+ return (
+
+ );
+ } else if (isWholeFileLoc(parsedLoc)) {
+ return (
+
+ );
+ } else if (isLineColumnLoc(parsedLoc)) {
+ return (
+
+ );
+ } else {
+ return null;
+ }
+}
+
+/**
+ * Parses a SARIF message and populates clickable locations.
+ */
+export function SarifMessageWithLocations({
+ msg,
+ relatedLocations,
+ sourceLocationPrefix,
+ databaseUri,
+ jumpToLocationCallback,
+}: {
+ msg: string;
+ relatedLocations: Sarif.Location[];
+ sourceLocationPrefix: string;
+ databaseUri: string;
+ jumpToLocationCallback: () => void;
+}) {
+ const relatedLocationsById: Map = new Map();
+ for (const loc of relatedLocations) {
+ if (loc.id !== undefined) {
+ relatedLocationsById.set(loc.id, loc);
+ }
+ }
+
+ return (
+ <>
+ {parseSarifPlainTextMessage(msg).map((part, i) => {
+ if (typeof part === "string") {
+ return {part};
+ } else {
+ return (
+
+ );
+ }
+ })}
+ >
+ );
+}
diff --git a/extensions/ql-vscode/src/view/results/result-table-utils.tsx b/extensions/ql-vscode/src/view/results/result-table-utils.tsx
index f757aa037..9b84c556d 100644
--- a/extensions/ql-vscode/src/view/results/result-table-utils.tsx
+++ b/extensions/ql-vscode/src/view/results/result-table-utils.tsx
@@ -1,6 +1,5 @@
import * as React from "react";
-import { UrlValue, ResolvableLocationValue } from "../../common/bqrs-cli-types";
-import { isStringLoc, tryGetResolvableLocation } from "../../common/bqrs-utils";
+import { ResolvableLocationValue } from "../../common/bqrs-cli-types";
import {
RawResultsSortState,
QueryMetadata,
@@ -9,7 +8,6 @@ import {
} from "../../common/interface-types";
import { assertNever } from "../../common/helpers-pure";
import { vscode } from "../vscode-api";
-import { convertNonPrintableChars } from "../../common/text-utils";
import { sendTelemetry } from "../common/telemetry";
export interface ResultTableProps {
@@ -44,21 +42,6 @@ export const oddRowClassName = "vscode-codeql__result-table-row--odd";
export const pathRowClassName = "vscode-codeql__result-table-row--path";
export const selectedRowClassName = "vscode-codeql__result-table-row--selected";
-export function jumpToLocationHandler(
- loc: ResolvableLocationValue,
- databaseUri: string,
- callback?: () => void,
-): (e: React.MouseEvent) => void {
- return (e) => {
- jumpToLocation(loc, databaseUri);
- e.preventDefault();
- e.stopPropagation();
- if (callback) {
- callback();
- }
- };
-}
-
export function jumpToLocation(
loc: ResolvableLocationValue,
databaseUri: string,
@@ -77,47 +60,6 @@ export function openFile(filePath: string): void {
});
}
-/**
- * Render a location as a link which when clicked displays the original location.
- */
-export function renderLocation(
- loc?: UrlValue,
- label?: string,
- databaseUri?: string,
- title?: string,
- callback?: () => void,
-): JSX.Element {
- const displayLabel = convertNonPrintableChars(label!);
-
- if (loc === undefined) {
- return {displayLabel};
- } else if (isStringLoc(loc)) {
- return {loc};
- }
-
- const resolvableLoc = tryGetResolvableLocation(loc);
- if (databaseUri !== undefined && resolvableLoc !== undefined) {
- return (
- <>
- {/*
- eslint-disable-next-line
- jsx-a11y/anchor-is-valid,
- */}
-
- {displayLabel}
-
- >
- );
- } else {
- return {displayLabel};
- }
-}
-
/**
* Returns the attributes for a zebra-striped table row at position `index`.
*/