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`. */