Move components for rendering locations to a separate file

This commit is contained in:
Robert
2023-06-30 12:24:43 +01:00
parent 2abc4d542b
commit a4c0365a95
4 changed files with 270 additions and 186 deletions

View File

@@ -1,6 +1,6 @@
import * as React from "react"; import * as React from "react";
import { renderLocation } from "./result-table-utils"; import { Location } from "./locations";
import { CellValue } from "../../common/bqrs-cli-types"; import { CellValue } from "../../common/bqrs-cli-types";
interface Props { interface Props {
@@ -16,14 +16,15 @@ export default function RawTableValue(props: Props): JSX.Element {
typeof rawValue === "number" || typeof rawValue === "number" ||
typeof rawValue === "boolean" typeof rawValue === "boolean"
) { ) {
return <span>{renderLocation(undefined, rawValue.toString())}</span>; return <Location label={rawValue.toString()} />;
} }
return renderLocation( return (
rawValue.url, <Location
rawValue.label, loc={rawValue.url}
props.databaseUri, label={rawValue.label}
undefined, databaseUri={props.databaseUri}
props.onSelected, jumpToLocationCallback={props.onSelected}
/>
); );
} }

View File

@@ -1,11 +1,9 @@
import { basename } from "path";
import * as React from "react"; import * as React from "react";
import * as Sarif from "sarif"; import * as Sarif from "sarif";
import * as Keys from "./result-keys"; import * as Keys from "./result-keys";
import { chevronDown, chevronRight, info, listUnordered } from "./octicons"; import { chevronDown, chevronRight, info, listUnordered } from "./octicons";
import { import {
className, className,
renderLocation,
ResultTableProps, ResultTableProps,
selectableZebraStripe, selectableZebraStripe,
jumpToLocation, jumpToLocation,
@@ -18,15 +16,11 @@ import {
NavigationDirection, NavigationDirection,
SarifInterpretationData, SarifInterpretationData,
} from "../../common/interface-types"; } from "../../common/interface-types";
import { import { parseSarifLocation, isNoLocation } from "../../common/sarif-utils";
parseSarifPlainTextMessage,
parseSarifLocation,
isNoLocation,
} from "../../common/sarif-utils";
import { isWholeFileLoc, isLineColumnLoc } from "../../common/bqrs-utils";
import { ScrollIntoViewHelper } from "./scroll-into-view-helper"; import { ScrollIntoViewHelper } from "./scroll-into-view-helper";
import { sendTelemetry } from "../common/telemetry"; import { sendTelemetry } from "../common/telemetry";
import { AlertTableHeader } from "./alert-table-header"; import { AlertTableHeader } from "./alert-table-header";
import { SarifLocation, SarifMessageWithLocations } from "./locations";
export type AlertTableProps = ResultTableProps & { export type AlertTableProps = ResultTableProps & {
resultSet: InterpretedResultSet<SarifInterpretationData>; resultSet: InterpretedResultSet<SarifInterpretationData>;
@@ -100,41 +94,6 @@ export class AlertTable extends React.Component<
const { numTruncatedResults, sourceLocationPrefix } = const { numTruncatedResults, sourceLocationPrefix } =
resultSet.interpretation; 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 <span key={i}>{part}</span>;
} else {
const renderedLocation = renderSarifLocationWithText(
part.text,
relatedLocationsById[part.dest],
resultKey,
);
return <span key={i}>{renderedLocation}</span>;
}
});
}
function renderNonLocation(
msg: string | undefined,
locationHint: string,
): JSX.Element | undefined {
if (msg === undefined) return undefined;
return <span title={locationHint}>{msg}</span>;
}
const updateSelectionCallback = ( const updateSelectionCallback = (
resultKey: Keys.PathNode | Keys.Result | undefined, 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 = ( const toggler: (keys: Keys.ResultKey[]) => (e: React.MouseEvent) => void = (
indices, indices,
) => { ) => {
@@ -220,19 +120,32 @@ export class AlertTable extends React.Component<
(result, resultIndex) => { (result, resultIndex) => {
const resultKey: Keys.Result = { resultIndex }; const resultKey: Keys.Result = { resultIndex };
const text = result.message.text || "[no text]"; const text = result.message.text || "[no text]";
const msg: JSX.Element[] = const msg =
result.relatedLocations === undefined result.relatedLocations === undefined ? (
? [<span key="0">{text}</span>] <span key="0">{text}</span>
: renderRelatedLocations(text, result.relatedLocations, resultKey); ) : (
<SarifMessageWithLocations
msg={text}
relatedLocations={result.relatedLocations}
sourceLocationPrefix={sourceLocationPrefix}
databaseUri={databaseUri}
jumpToLocationCallback={updateSelectionCallback(resultKey)}
/>
);
const currentResultExpanded = this.state.expanded.has( const currentResultExpanded = this.state.expanded.has(
Keys.keyToString(resultKey), Keys.keyToString(resultKey),
); );
const indicator = currentResultExpanded ? chevronDown : chevronRight; const indicator = currentResultExpanded ? chevronDown : chevronRight;
const location = const location = result.locations !== undefined &&
result.locations !== undefined && result.locations.length > 0 && (
result.locations.length > 0 && <SarifLocation
renderSarifLocation(result.locations[0], resultKey); loc={result.locations[0]}
sourceLocationPrefix={sourceLocationPrefix}
databaseUri={databaseUri}
jumpToLocationCallback={updateSelectionCallback(resultKey)}
/>
);
const locationCells = ( const locationCells = (
<td className="vscode-codeql__location-cell">{location}</td> <td className="vscode-codeql__location-cell">{location}</td>
); );
@@ -342,17 +255,32 @@ export class AlertTable extends React.Component<
const step = pathNodes[pathNodeIndex]; const step = pathNodes[pathNodeIndex];
const msg = const msg =
step.location !== undefined && step.location !== undefined &&
step.location.message !== undefined step.location.message !== undefined ? (
? renderSarifLocationWithText( <SarifLocation
step.location.message.text, text={step.location.message.text}
step.location, loc={step.location}
sourceLocationPrefix={sourceLocationPrefix}
databaseUri={databaseUri}
jumpToLocationCallback={updateSelectionCallback(
pathNodeKey, pathNodeKey,
) )}
: "[no location]"; />
) : (
"[no location]"
);
const additionalMsg = const additionalMsg =
step.location !== undefined step.location !== undefined ? (
? renderSarifLocation(step.location, pathNodeKey) <SarifLocation
: ""; loc={step.location}
sourceLocationPrefix={sourceLocationPrefix}
databaseUri={databaseUri}
jumpToLocationCallback={updateSelectionCallback(
pathNodeKey,
)}
/>
) : (
""
);
const isSelected = Keys.equalsNotUndefined( const isSelected = Keys.equalsNotUndefined(
this.state.selectedItem, this.state.selectedItem,
pathNodeKey, pathNodeKey,

View File

@@ -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 <span title={locationHint}>{msg}</span>;
}
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,
*/}
<a
href="#"
className="vscode-codeql__result-table-location-link"
title={title}
onClick={jumpToLocationHandler}
>
{label}
</a>
</>
);
}
/**
* 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 <NonLocation msg={displayLabel} />;
} else if (isStringLoc(loc)) {
return <a href={loc}>{loc}</a>;
} else if (databaseUri === undefined || resolvableLoc === undefined) {
return <NonLocation msg={displayLabel} locationHint={title} />;
} else {
return (
<ClickableLocation
loc={resolvableLoc}
label={displayLabel}
databaseUri={databaseUri}
title={title}
jumpToLocationCallback={jumpToLocationCallback}
/>
);
}
}
/**
* 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 (
<NonLocation
msg={text || "[no location]"}
locationHint={parsedLoc?.hint}
/>
);
} else if (isWholeFileLoc(parsedLoc)) {
return (
<Location
loc={parsedLoc}
label={text || `${basename(parsedLoc.userVisibleFile)}`}
databaseUri={databaseUri}
title={text ? undefined : `${parsedLoc.userVisibleFile}`}
jumpToLocationCallback={jumpToLocationCallback}
/>
);
} else if (isLineColumnLoc(parsedLoc)) {
return (
<Location
loc={parsedLoc}
label={
text ||
`${basename(parsedLoc.userVisibleFile)}:${parsedLoc.startLine}:${
parsedLoc.startColumn
}`
}
databaseUri={databaseUri}
title={text ? undefined : `${parsedLoc.userVisibleFile}`}
jumpToLocationCallback={jumpToLocationCallback}
/>
);
} 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<number, Sarif.Location> = 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 <span key={i}>{part}</span>;
} else {
return (
<SarifLocation
key={i}
text={part.text}
loc={relatedLocationsById.get(part.dest)}
sourceLocationPrefix={sourceLocationPrefix}
databaseUri={databaseUri}
jumpToLocationCallback={jumpToLocationCallback}
/>
);
}
})}
</>
);
}

View File

@@ -1,6 +1,5 @@
import * as React from "react"; import * as React from "react";
import { UrlValue, ResolvableLocationValue } from "../../common/bqrs-cli-types"; import { ResolvableLocationValue } from "../../common/bqrs-cli-types";
import { isStringLoc, tryGetResolvableLocation } from "../../common/bqrs-utils";
import { import {
RawResultsSortState, RawResultsSortState,
QueryMetadata, QueryMetadata,
@@ -9,7 +8,6 @@ import {
} from "../../common/interface-types"; } from "../../common/interface-types";
import { assertNever } from "../../common/helpers-pure"; import { assertNever } from "../../common/helpers-pure";
import { vscode } from "../vscode-api"; import { vscode } from "../vscode-api";
import { convertNonPrintableChars } from "../../common/text-utils";
import { sendTelemetry } from "../common/telemetry"; import { sendTelemetry } from "../common/telemetry";
export interface ResultTableProps { 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 pathRowClassName = "vscode-codeql__result-table-row--path";
export const selectedRowClassName = "vscode-codeql__result-table-row--selected"; 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( export function jumpToLocation(
loc: ResolvableLocationValue, loc: ResolvableLocationValue,
databaseUri: string, 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 <span>{displayLabel}</span>;
} else if (isStringLoc(loc)) {
return <a href={loc}>{loc}</a>;
}
const resolvableLoc = tryGetResolvableLocation(loc);
if (databaseUri !== undefined && resolvableLoc !== undefined) {
return (
<>
{/*
eslint-disable-next-line
jsx-a11y/anchor-is-valid,
*/}
<a
href="#"
className="vscode-codeql__result-table-location-link"
title={title}
onClick={jumpToLocationHandler(resolvableLoc, databaseUri, callback)}
>
{displayLabel}
</a>
</>
);
} else {
return <span title={title}>{displayLabel}</span>;
}
}
/** /**
* Returns the attributes for a zebra-striped table row at position `index`. * Returns the attributes for a zebra-striped table row at position `index`.
*/ */