Display path provenance information in results view

This commit is contained in:
Dave Bartolomeo
2024-07-18 16:14:24 -04:00
parent a6225c5edf
commit 97b9c43ae1
20 changed files with 310 additions and 49 deletions

View File

@@ -147,6 +147,17 @@ interface SetStateMsg {
parsedResultSets: ParsedResultSets; parsedResultSets: ParsedResultSets;
} }
export interface UserSettings {
/** Whether to display links to the dataflow models that generated particular nodes in a flow path. */
shouldShowProvenance: boolean;
}
/** Message indicating that the user's configuration settings have changed. */
interface SetUserSettingsMsg {
t: "setUserSettings";
userSettings: UserSettings;
}
/** /**
* Message indicating that the results view should display interpreted * Message indicating that the results view should display interpreted
* results. * results.
@@ -191,6 +202,7 @@ interface UntoggleShowProblemsMsg {
export type IntoResultsViewMsg = export type IntoResultsViewMsg =
| ResultsUpdatingMsg | ResultsUpdatingMsg
| SetStateMsg | SetStateMsg
| SetUserSettingsMsg
| ShowInterpretedPageMsg | ShowInterpretedPageMsg
| NavigateMsg | NavigateMsg
| UntoggleShowProblemsMsg; | UntoggleShowProblemsMsg;
@@ -208,13 +220,15 @@ export type FromResultsViewMsg =
| OpenFileMsg; | OpenFileMsg;
/** /**
* Message from the results view to open a database source * Message from the results view to open a source
* file at the provided location. * file at the provided location.
*/ */
interface ViewSourceFileMsg { interface ViewSourceFileMsg {
t: "viewSourceFile"; t: "viewSourceFile";
loc: UrlValueResolvable; loc: UrlValueResolvable;
databaseUri: string; /** URI of the database whose source archive contains the file, or `undefined` to open a file from
* the local disk. The latter case is used for opening links to data extension model files. */
databaseUri: string | undefined;
} }
/** /**
@@ -341,7 +355,8 @@ interface ChangeCompareMessage {
export type ToCompareViewMessage = export type ToCompareViewMessage =
| SetComparisonQueryInfoMessage | SetComparisonQueryInfoMessage
| SetComparisonsMessage; | SetComparisonsMessage
| SetUserSettingsMsg;
/** /**
* Message to the compare view that sets the metadata of the compared queries. * Message to the compare view that sets the metadata of the compared queries.

View File

@@ -1,18 +1,20 @@
import type { Log, Tool } from "sarif"; import type { Log } from "sarif";
import { createReadStream } from "fs-extra"; import { createReadStream } from "fs-extra";
import { connectTo } from "stream-json/Assembler"; import { connectTo } from "stream-json/Assembler";
import { getErrorMessage } from "./helpers-pure"; import { getErrorMessage } from "./helpers-pure";
import { withParser } from "stream-json/filters/Pick"; import { withParser } from "stream-json/filters/Ignore";
const DUMMY_TOOL: Tool = { driver: { name: "" } };
export async function sarifParser( export async function sarifParser(
interpretedResultsPath: string, interpretedResultsPath: string,
): Promise<Log> { ): Promise<Log> {
try { try {
// Parse the SARIF file into token streams, filtering out only the results array. // Parse the SARIF file into token streams, filtering out some of the larger subtrees that we
// don't need.
const pipeline = createReadStream(interpretedResultsPath).pipe( const pipeline = createReadStream(interpretedResultsPath).pipe(
withParser({ filter: "runs.0.results" }), withParser({
// We don't need to run's `artifacts` property, nor the driver's `notifications` property.
filter: /^runs\.\d+\.(artifacts|tool\.driver\.notifications)/,
}),
); );
// Creates JavaScript objects from the token stream // Creates JavaScript objects from the token stream
@@ -38,15 +40,7 @@ export async function sarifParser(
}); });
asm.on("done", (asm) => { asm.on("done", (asm) => {
const log: Log = { const log: Log = asm.current;
version: "2.1.0",
runs: [
{
tool: DUMMY_TOOL,
results: asm.current ?? [],
},
],
};
resolve(log); resolve(log);
alreadyDone = true; alreadyDone = true;

View File

@@ -33,6 +33,7 @@ import {
getResultSetNames, getResultSetNames,
} from "./result-set-names"; } from "./result-set-names";
import { compareInterpretedResults } from "./interpreted-results"; import { compareInterpretedResults } from "./interpreted-results";
import { isCanary } from "../config";
interface ComparePair { interface ComparePair {
from: CompletedLocalQueryInfo; from: CompletedLocalQueryInfo;
@@ -116,6 +117,13 @@ export class CompareView extends AbstractWebview<
panel.reveal(undefined, true); panel.reveal(undefined, true);
await this.waitForPanelLoaded(); await this.waitForPanelLoaded();
await this.postMessage({
t: "setUserSettings",
userSettings: {
shouldShowProvenance: isCanary(),
},
});
await this.postMessage({ await this.postMessage({
t: "setComparisonQueryInfo", t: "setComparisonQueryInfo",
stats: { stats: {

View File

@@ -37,11 +37,12 @@ export const shownLocationLineDecoration =
/** /**
* Resolves the specified CodeQL location to a URI into the source archive. * Resolves the specified CodeQL location to a URI into the source archive.
* @param loc CodeQL location to resolve. Must have a non-empty value for `loc.file`. * @param loc CodeQL location to resolve. Must have a non-empty value for `loc.file`.
* @param databaseItem Database in which to resolve the file location. * @param databaseItem Database in which to resolve the file location, or `undefined` to resolve
* from the local file system.
*/ */
function resolveFivePartLocation( function resolveFivePartLocation(
loc: UrlValueLineColumnLocation, loc: UrlValueLineColumnLocation,
databaseItem: DatabaseItem, databaseItem: DatabaseItem | undefined,
): Location { ): Location {
// `Range` is a half-open interval, and is zero-based. CodeQL locations are closed intervals, and // `Range` is a half-open interval, and is zero-based. CodeQL locations are closed intervals, and
// are one-based. Adjust accordingly. // are one-based. Adjust accordingly.
@@ -52,7 +53,10 @@ function resolveFivePartLocation(
Math.max(1, loc.endColumn), Math.max(1, loc.endColumn),
); );
return new Location(databaseItem.resolveSourceFile(loc.uri), range); return new Location(
databaseItem?.resolveSourceFile(loc.uri) ?? Uri.parse(loc.uri),
range,
);
} }
/** /**
@@ -62,22 +66,26 @@ function resolveFivePartLocation(
*/ */
function resolveWholeFileLocation( function resolveWholeFileLocation(
loc: UrlValueWholeFileLocation, loc: UrlValueWholeFileLocation,
databaseItem: DatabaseItem, databaseItem: DatabaseItem | undefined,
): Location { ): Location {
// A location corresponding to the start of the file. // A location corresponding to the start of the file.
const range = new Range(0, 0, 0, 0); const range = new Range(0, 0, 0, 0);
return new Location(databaseItem.resolveSourceFile(loc.uri), range); return new Location(
databaseItem?.resolveSourceFile(loc.uri) ?? Uri.parse(loc.uri),
range,
);
} }
/** /**
* Try to resolve the specified CodeQL location to a URI into the source archive. If no exact location * Try to resolve the specified CodeQL location to a URI into the source archive. If no exact location
* can be resolved, returns `undefined`. * can be resolved, returns `undefined`.
* @param loc CodeQL location to resolve * @param loc CodeQL location to resolve
* @param databaseItem Database in which to resolve the file location. * @param databaseItem Database in which to resolve the file location, or `undefined` to resolve
* from the local file system.
*/ */
export function tryResolveLocation( export function tryResolveLocation(
loc: UrlValueResolvable | undefined, loc: UrlValueResolvable | undefined,
databaseItem: DatabaseItem, databaseItem: DatabaseItem | undefined,
): Location | undefined { ): Location | undefined {
if (!loc) { if (!loc) {
return; return;
@@ -95,7 +103,7 @@ export function tryResolveLocation(
export async function showResolvableLocation( export async function showResolvableLocation(
loc: UrlValueResolvable, loc: UrlValueResolvable,
databaseItem: DatabaseItem, databaseItem: DatabaseItem | undefined,
logger: Logger, logger: Logger,
): Promise<void> { ): Promise<void> {
try { try {
@@ -151,13 +159,14 @@ export async function showLocation(location?: Location) {
} }
export async function jumpToLocation( export async function jumpToLocation(
databaseUri: string, databaseUri: string | undefined,
loc: UrlValueResolvable, loc: UrlValueResolvable,
databaseManager: DatabaseManager, databaseManager: DatabaseManager,
logger: Logger, logger: Logger,
) { ) {
const databaseItem = databaseManager.findDatabaseItem(Uri.parse(databaseUri)); const databaseItem =
if (databaseItem !== undefined) { databaseUri !== undefined
await showResolvableLocation(loc, databaseItem, logger); ? databaseManager.findDatabaseItem(Uri.parse(databaseUri))
} : undefined;
await showResolvableLocation(loc, databaseItem, logger);
} }

View File

@@ -537,6 +537,14 @@ export class ResultsView extends AbstractWebview<
resultSetNames, resultSetNames,
}; };
await this.postMessage({
t: "setUserSettings",
userSettings: {
// Only show provenance info in canary mode for now.
shouldShowProvenance: isCanary(),
},
});
await this.postMessage({ await this.postMessage({
t: "setState", t: "setState",
interpretation: interpretationPage, interpretation: interpretationPage,

View File

@@ -5,6 +5,7 @@ import type {
ToCompareViewMessage, ToCompareViewMessage,
SetComparisonsMessage, SetComparisonsMessage,
SetComparisonQueryInfoMessage, SetComparisonQueryInfoMessage,
UserSettings,
} from "../../common/interface-types"; } from "../../common/interface-types";
import CompareSelector from "./CompareSelector"; import CompareSelector from "./CompareSelector";
import { vscode } from "../vscode-api"; import { vscode } from "../vscode-api";
@@ -31,6 +32,9 @@ export function Compare(_: Record<string, never>): React.JSX.Element {
const [comparison, setComparison] = useState<SetComparisonsMessage | null>( const [comparison, setComparison] = useState<SetComparisonsMessage | null>(
null, null,
); );
const [userSettings, setUserSettings] = useState<UserSettings>({
shouldShowProvenance: false,
});
const message = comparison?.message || "Empty comparison"; const message = comparison?.message || "Empty comparison";
const hasRows = const hasRows =
@@ -48,6 +52,9 @@ export function Compare(_: Record<string, never>): React.JSX.Element {
case "setComparisons": case "setComparisons":
setComparison(msg); setComparison(msg);
break; break;
case "setUserSettings":
setUserSettings(msg.userSettings);
break;
default: default:
assertNever(msg); assertNever(msg);
} }
@@ -85,6 +92,7 @@ export function Compare(_: Record<string, never>): React.JSX.Element {
<CompareTable <CompareTable
queryInfo={queryInfo} queryInfo={queryInfo}
comparison={comparison} comparison={comparison}
userSettings={userSettings}
></CompareTable> ></CompareTable>
) : ( ) : (
<Message>{message}</Message> <Message>{message}</Message>

View File

@@ -1,6 +1,7 @@
import type { import type {
SetComparisonQueryInfoMessage, SetComparisonQueryInfoMessage,
SetComparisonsMessage, SetComparisonsMessage,
UserSettings,
} from "../../common/interface-types"; } from "../../common/interface-types";
import { className } from "../results/result-table-utils"; import { className } from "../results/result-table-utils";
import { vscode } from "../vscode-api"; import { vscode } from "../vscode-api";
@@ -12,6 +13,7 @@ import { InterpretedCompareResultTable } from "./InterpretedCompareResultTable";
interface Props { interface Props {
queryInfo: SetComparisonQueryInfoMessage; queryInfo: SetComparisonQueryInfoMessage;
comparison: SetComparisonsMessage; comparison: SetComparisonsMessage;
userSettings: UserSettings;
} }
const OpenButton = styled(TextButton)` const OpenButton = styled(TextButton)`
@@ -29,7 +31,11 @@ const Table = styled.table`
} }
`; `;
export default function CompareTable({ queryInfo, comparison }: Props) { export default function CompareTable({
queryInfo,
comparison,
userSettings,
}: Props) {
const result = comparison.result!; const result = comparison.result!;
async function openQuery(kind: "from" | "to") { async function openQuery(kind: "from" | "to") {
@@ -78,6 +84,7 @@ export default function CompareTable({ queryInfo, comparison }: Props) {
{result.kind === "interpreted" && ( {result.kind === "interpreted" && (
<InterpretedCompareResultTable <InterpretedCompareResultTable
results={result.from} results={result.from}
userSettings={userSettings}
databaseUri={queryInfo.databaseUri} databaseUri={queryInfo.databaseUri}
sourceLocationPrefix={result.sourceLocationPrefix} sourceLocationPrefix={result.sourceLocationPrefix}
/> />
@@ -96,6 +103,7 @@ export default function CompareTable({ queryInfo, comparison }: Props) {
{result.kind === "interpreted" && ( {result.kind === "interpreted" && (
<InterpretedCompareResultTable <InterpretedCompareResultTable
results={result.to} results={result.to}
userSettings={userSettings}
databaseUri={queryInfo.databaseUri} databaseUri={queryInfo.databaseUri}
sourceLocationPrefix={result.sourceLocationPrefix} sourceLocationPrefix={result.sourceLocationPrefix}
/> />

View File

@@ -1,27 +1,32 @@
import type { Result } from "sarif"; import type { Result, Run } from "sarif";
import { AlertTable } from "../results/AlertTable"; import { AlertTable } from "../results/AlertTable";
import type { UserSettings } from "../../common/interface-types";
type Props = { type Props = {
results: Result[]; results: Result[];
databaseUri: string; databaseUri: string;
sourceLocationPrefix: string; sourceLocationPrefix: string;
run?: Run;
userSettings: UserSettings;
}; };
export const InterpretedCompareResultTable = ({ export const InterpretedCompareResultTable = ({
results, results,
databaseUri, databaseUri,
sourceLocationPrefix, sourceLocationPrefix,
userSettings,
}: Props) => { }: Props) => {
return ( return (
<AlertTable <AlertTable
results={results} results={results}
userSettings={userSettings}
databaseUri={databaseUri} databaseUri={databaseUri}
sourceLocationPrefix={sourceLocationPrefix} sourceLocationPrefix={sourceLocationPrefix}
header={ header={
<thead> <thead>
<tr> <tr>
<th colSpan={2}></th> <th colSpan={2}></th>
<th className={`vscode-codeql__alert-message-cell`} colSpan={3}> <th className={`vscode-codeql__alert-message-cell`} colSpan={4}>
Message Message
</th> </th>
</tr> </tr>

View File

@@ -1,4 +1,4 @@
import type { Location, Result } from "sarif"; import type { Location, Result, Run } from "sarif";
import type { import type {
PathNode, PathNode,
Result as ResultKeysResult, Result as ResultKeysResult,
@@ -7,7 +7,7 @@ import type {
import { getPath, getPathNode, getResult, keyToString } from "./result-keys"; import { getPath, getPathNode, getResult, keyToString } from "./result-keys";
import { className, jumpToLocation } from "./result-table-utils"; import { className, jumpToLocation } from "./result-table-utils";
import { onNavigation } from "./navigation"; import { onNavigation } from "./navigation";
import type { NavigateMsg } from "../../common/interface-types"; import type { NavigateMsg, UserSettings } from "../../common/interface-types";
import { NavigationDirection } from "../../common/interface-types"; import { NavigationDirection } from "../../common/interface-types";
import { isNoLocation, parseSarifLocation } from "../../common/sarif-utils"; import { isNoLocation, parseSarifLocation } from "../../common/sarif-utils";
import { sendTelemetry } from "../common/telemetry"; import { sendTelemetry } from "../common/telemetry";
@@ -21,6 +21,8 @@ type Props = {
results: Result[]; results: Result[];
databaseUri: string; databaseUri: string;
sourceLocationPrefix: string; sourceLocationPrefix: string;
run?: Run;
userSettings: UserSettings;
numTruncatedResults?: number; numTruncatedResults?: number;
header: ReactNode; header: ReactNode;
@@ -31,6 +33,8 @@ export function AlertTable({
results, results,
databaseUri, databaseUri,
sourceLocationPrefix, sourceLocationPrefix,
run,
userSettings,
numTruncatedResults, numTruncatedResults,
header, header,
noResults, noResults,
@@ -202,6 +206,8 @@ export function AlertTable({
selectedItem={selectedItem} selectedItem={selectedItem}
selectedItemRef={selectedItemRef} selectedItemRef={selectedItemRef}
databaseUri={databaseUri} databaseUri={databaseUri}
run={run}
userSettings={userSettings}
sourceLocationPrefix={sourceLocationPrefix} sourceLocationPrefix={sourceLocationPrefix}
updateSelectionCallback={updateSelectionCallback} updateSelectionCallback={updateSelectionCallback}
toggleExpanded={toggle} toggleExpanded={toggle}

View File

@@ -1,4 +1,12 @@
import type { ThreadFlowLocation } from "sarif"; import type {
Location as SarifLogLocation,
ReportingDescriptorReference,
ThreadFlowLocation,
Run,
PhysicalLocation,
ArtifactLocation,
ToolComponent,
} from "sarif";
import type { import type {
PathNode, PathNode,
Result as ResultKeysResult, Result as ResultKeysResult,
@@ -9,6 +17,151 @@ import { SarifLocation } from "./locations/SarifLocation";
import { selectableZebraStripe } from "./result-table-utils"; import { selectableZebraStripe } from "./result-table-utils";
import { useCallback, useMemo } from "react"; import { useCallback, useMemo } from "react";
import { VerticalRule } from "../common/VerticalRule"; import { VerticalRule } from "../common/VerticalRule";
import type { UserSettings } from "../../common/interface-types";
/** The definition of a taxon for a data extension model row. */
interface ModelTaxon {
location: SarifLogLocation;
}
/** Resolve an `ArtifactLocation` that might contain a relative reference instead of an absolute
* URI.
*/
function resolveArtifactLocation(
location: ArtifactLocation,
baseUri: URL,
): ArtifactLocation {
if (location.uri === undefined) {
// No URI at all. Just return the original location.
return location;
}
return {
...location,
uri: new URL(location.uri, baseUri).toString(),
};
}
/** Get the URI of the pack's local root directory, if available. */
function getLocalPackUri(extension: ToolComponent): URL | undefined {
if (extension.locations === undefined) {
return undefined;
}
const localPackLocation = extension.locations.find(
(loc) =>
loc.properties !== undefined &&
loc.properties.tags !== undefined &&
loc.properties.tags.includes("CodeQL/LocalPackRoot"),
);
if (localPackLocation === undefined || localPackLocation.uri === undefined) {
return undefined;
}
return new URL(localPackLocation.uri);
}
/** Resolve a `ReportingDescriptorReference` to the `ReportingDescriptor` for the taxon that it
* refers to.
*/
function resolveTaxonDefinition(
run: Run,
taxonRef: ReportingDescriptorReference,
): ModelTaxon | undefined {
const extensions = run.tool.extensions;
if (extensions === undefined) {
return undefined;
}
const extensionIndex = taxonRef.toolComponent?.index;
if (
extensionIndex === undefined ||
extensionIndex < 0 ||
extensionIndex >= extensions.length
) {
return undefined;
}
const extension = extensions[extensionIndex];
if (extension.taxa === undefined) {
return undefined;
}
const localPackUri = getLocalPackUri(extension);
if (localPackUri === undefined) {
return undefined;
}
const taxonIndex = taxonRef.index;
if (
taxonIndex === undefined ||
taxonIndex < 0 ||
taxonIndex >= extension.taxa.length
) {
return undefined;
}
const taxonDef = extension.taxa[taxonIndex];
if (taxonDef.properties === undefined) {
return undefined;
}
const location: PhysicalLocation =
taxonDef.properties["CodeQL/DataExtensionLocation"];
if (location === undefined || location.artifactLocation === undefined) {
return undefined;
}
return {
location: {
physicalLocation: {
...location,
artifactLocation: resolveArtifactLocation(
location.artifactLocation,
localPackUri,
),
},
},
};
}
/** Generate the React elements for each taxon. */
function taxaLocations(
taxa: ReportingDescriptorReference[] | undefined,
run: Run | undefined,
onClick: () => void,
) {
if (taxa === undefined || taxa.length === 0 || run === undefined) {
return [];
}
return taxa.flatMap((taxonRef, index) => {
if (taxonRef.properties === undefined) {
return [];
}
const role = taxonRef.properties["CodeQL/DataflowRole"];
if (typeof role !== "string") {
return [];
}
const taxonDef = resolveTaxonDefinition(run, taxonRef);
if (taxonDef === undefined) {
return [];
}
return (
<div key={index}>
{`(${role}) `}
<SarifLocation
loc={taxonDef.location}
databaseUri={undefined}
sourceLocationPrefix=""
onClick={onClick}
/>
</div>
);
});
}
interface Props { interface Props {
step: ThreadFlowLocation; step: ThreadFlowLocation;
@@ -19,6 +172,8 @@ interface Props {
selectedItemRef: React.RefObject<HTMLTableRowElement>; selectedItemRef: React.RefObject<HTMLTableRowElement>;
databaseUri: string; databaseUri: string;
sourceLocationPrefix: string; sourceLocationPrefix: string;
run?: Run;
userSettings: UserSettings;
updateSelectionCallback: ( updateSelectionCallback: (
resultKey: PathNode | ResultKeysResult | undefined, resultKey: PathNode | ResultKeysResult | undefined,
) => void; ) => void;
@@ -34,6 +189,8 @@ export function AlertTablePathNodeRow(props: Props) {
selectedItemRef, selectedItemRef,
databaseUri, databaseUri,
sourceLocationPrefix, sourceLocationPrefix,
run,
userSettings,
updateSelectionCallback, updateSelectionCallback,
} = props; } = props;
@@ -86,7 +243,21 @@ export function AlertTablePathNodeRow(props: Props) {
"[no location]" "[no location]"
)} )}
</td> </td>
<td {...selectableZebraStripe(isSelected, zebraIndex)}>{"model"}</td> <td
{...selectableZebraStripe(
isSelected,
zebraIndex,
"vscode-codeql__taxa-cell",
)}
>
{userSettings.shouldShowProvenance ? (
<div className="vscode-codeql__taxa-cell-div">
{taxaLocations(step.taxa, run, handleSarifLocationClicked)}
</div>
) : (
[]
)}
</td>
<td <td
{...selectableZebraStripe( {...selectableZebraStripe(
isSelected, isSelected,

View File

@@ -1,4 +1,4 @@
import type { ThreadFlow } from "sarif"; import type { Run, ThreadFlow } from "sarif";
import type { import type {
PathNode, PathNode,
Result as ResultKeysResult, Result as ResultKeysResult,
@@ -10,6 +10,7 @@ import { AlertTablePathNodeRow } from "./AlertTablePathNodeRow";
import { AlertTableDropdownIndicatorCell } from "./AlertTableDropdownIndicatorCell"; import { AlertTableDropdownIndicatorCell } from "./AlertTableDropdownIndicatorCell";
import { useCallback, useMemo } from "react"; import { useCallback, useMemo } from "react";
import { VerticalRule } from "../common/VerticalRule"; import { VerticalRule } from "../common/VerticalRule";
import type { UserSettings } from "../../common/interface-types";
interface Props { interface Props {
path: ThreadFlow; path: ThreadFlow;
@@ -20,6 +21,8 @@ interface Props {
selectedItemRef: React.RefObject<HTMLTableRowElement>; selectedItemRef: React.RefObject<HTMLTableRowElement>;
databaseUri: string; databaseUri: string;
sourceLocationPrefix: string; sourceLocationPrefix: string;
run?: Run;
userSettings: UserSettings;
updateSelectionCallback: ( updateSelectionCallback: (
resultKey: PathNode | ResultKeysResult | undefined, resultKey: PathNode | ResultKeysResult | undefined,
) => void; ) => void;

View File

@@ -1,4 +1,4 @@
import type { Result } from "sarif"; import type { Result, Run } from "sarif";
import type { import type {
PathNode, PathNode,
Result as ResultKeysResult, Result as ResultKeysResult,
@@ -12,6 +12,7 @@ import { useCallback, useMemo } from "react";
import { SarifLocation } from "./locations/SarifLocation"; import { SarifLocation } from "./locations/SarifLocation";
import { SarifMessageWithLocations } from "./locations/SarifMessageWithLocations"; import { SarifMessageWithLocations } from "./locations/SarifMessageWithLocations";
import { AlertTablePathRow } from "./AlertTablePathRow"; import { AlertTablePathRow } from "./AlertTablePathRow";
import type { UserSettings } from "../../common/interface-types";
interface Props { interface Props {
result: Result; result: Result;
@@ -21,6 +22,8 @@ interface Props {
selectedItemRef: React.RefObject<HTMLTableRowElement>; selectedItemRef: React.RefObject<HTMLTableRowElement>;
databaseUri: string; databaseUri: string;
sourceLocationPrefix: string; sourceLocationPrefix: string;
run?: Run;
userSettings: UserSettings;
updateSelectionCallback: ( updateSelectionCallback: (
resultKey: PathNode | ResultKeysResult | undefined, resultKey: PathNode | ResultKeysResult | undefined,
) => void; ) => void;

View File

@@ -6,7 +6,7 @@ import { AlertTableNoResults } from "./AlertTableNoResults";
import { AlertTableHeader } from "./AlertTableHeader"; import { AlertTableHeader } from "./AlertTableHeader";
export function ResultTable(props: ResultTableProps) { export function ResultTable(props: ResultTableProps) {
const { resultSet } = props; const { resultSet, userSettings } = props;
switch (resultSet.t) { switch (resultSet.t) {
case "RawResultSet": case "RawResultSet":
return <RawTable {...props} resultSet={resultSet.resultSet} />; return <RawTable {...props} resultSet={resultSet.resultSet} />;
@@ -21,6 +21,8 @@ export function ResultTable(props: ResultTableProps) {
sourceLocationPrefix={ sourceLocationPrefix={
resultSet.interpretation.sourceLocationPrefix resultSet.interpretation.sourceLocationPrefix
} }
run={data.runs[0]}
userSettings={userSettings}
numTruncatedResults={resultSet.interpretation.numTruncatedResults} numTruncatedResults={resultSet.interpretation.numTruncatedResults}
header={<AlertTableHeader sortState={data.sortState} />} header={<AlertTableHeader sortState={data.sortState} />}
noResults={ noResults={

View File

@@ -8,6 +8,7 @@ import type {
ResultSet, ResultSet,
ParsedResultSets, ParsedResultSets,
IntoResultsViewMsg, IntoResultsViewMsg,
UserSettings,
} from "../../common/interface-types"; } from "../../common/interface-types";
import { import {
ALERTS_TABLE_NAME, ALERTS_TABLE_NAME,
@@ -33,6 +34,7 @@ interface ResultTablesProps {
rawResultSets: readonly ResultSet[]; rawResultSets: readonly ResultSet[];
interpretation: Interpretation | undefined; interpretation: Interpretation | undefined;
database: DatabaseInfo; database: DatabaseInfo;
userSettings: UserSettings;
metadata?: QueryMetadata; metadata?: QueryMetadata;
resultsPath: string; resultsPath: string;
origResultsPaths: ResultsPaths; origResultsPaths: ResultsPaths;
@@ -94,6 +96,7 @@ export function ResultTables(props: ResultTablesProps) {
interpretation, interpretation,
database, database,
resultsPath, resultsPath,
userSettings,
metadata, metadata,
origResultsPaths, origResultsPaths,
isLoadingNewResults, isLoadingNewResults,
@@ -242,6 +245,7 @@ export function ResultTables(props: ResultTablesProps) {
<ResultTable <ResultTable
key={resultSetName} key={resultSetName}
resultSet={resultSet} resultSet={resultSet}
userSettings={userSettings}
databaseUri={database.databaseUri} databaseUri={database.databaseUri}
resultsPath={resultsPath} resultsPath={resultsPath}
sortState={sortStates.get(resultSetName)} sortState={sortStates.get(resultSetName)}

View File

@@ -9,6 +9,7 @@ import type {
ResultsPaths, ResultsPaths,
ParsedResultSets, ParsedResultSets,
ResultSet, ResultSet,
UserSettings,
} from "../../common/interface-types"; } from "../../common/interface-types";
import { import {
ALERTS_TABLE_NAME, ALERTS_TABLE_NAME,
@@ -77,6 +78,10 @@ export function ResultsApp() {
isExpectingResultsUpdate: true, isExpectingResultsUpdate: true,
}); });
const [userSettings, setUserSettings] = useState<UserSettings>({
shouldShowProvenance: false,
});
const updateStateWithNewResultsInfo = useCallback( const updateStateWithNewResultsInfo = useCallback(
(resultsInfo: ResultsInfo): void => { (resultsInfo: ResultsInfo): void => {
let results: Results | null = null; let results: Results | null = null;
@@ -110,6 +115,10 @@ export function ResultsApp() {
const handleMessage = useCallback( const handleMessage = useCallback(
(msg: IntoResultsViewMsg): void => { (msg: IntoResultsViewMsg): void => {
switch (msg.t) { switch (msg.t) {
case "setUserSettings":
setUserSettings(msg.userSettings);
break;
case "setState": case "setState":
updateStateWithNewResultsInfo({ updateStateWithNewResultsInfo({
resultsPath: msg.resultsPath, resultsPath: msg.resultsPath,
@@ -217,6 +226,7 @@ export function ResultsApp() {
? displayedResults.resultsInfo.interpretation ? displayedResults.resultsInfo.interpretation
: undefined : undefined
} }
userSettings={userSettings}
database={displayedResults.results.database} database={displayedResults.results.database}
origResultsPaths={displayedResults.resultsInfo.origResultsPaths} origResultsPaths={displayedResults.resultsInfo.origResultsPaths}
resultsPath={displayedResults.resultsInfo.resultsPath} resultsPath={displayedResults.resultsInfo.resultsPath}

View File

@@ -7,7 +7,7 @@ import type { UrlValueResolvable } from "../../../common/raw-result-types";
interface Props { interface Props {
loc: UrlValueResolvable; loc: UrlValueResolvable;
label: string; label: string;
databaseUri: string; databaseUri: string | undefined;
onClick?: () => void; onClick?: () => void;
} }

View File

@@ -26,17 +26,13 @@ export function Location({
const displayLabel = useMemo(() => convertNonPrintableChars(label), [label]); const displayLabel = useMemo(() => convertNonPrintableChars(label), [label]);
if (loc === undefined) { if (loc === undefined) {
return <NonClickableLocation msg={displayLabel} />; return <NonClickableLocation msg={displayLabel} locationHint={title} />;
} }
if (loc.type === "string") { if (loc.type === "string") {
return <a href={loc.value}>{loc.value}</a>; return <a href={loc.value}>{loc.value}</a>;
} }
if (databaseUri === undefined) {
return <NonClickableLocation msg={displayLabel} locationHint={title} />;
}
return ( return (
<ClickableLocation <ClickableLocation
loc={loc} loc={loc}

View File

@@ -8,7 +8,7 @@ interface Props {
text?: string; text?: string;
loc?: SarifLogLocation; loc?: SarifLogLocation;
sourceLocationPrefix: string; sourceLocationPrefix: string;
databaseUri: string; databaseUri: string | undefined;
onClick: () => void; onClick: () => void;
} }

View File

@@ -2,6 +2,7 @@ import type {
QueryMetadata, QueryMetadata,
RawResultsSortState, RawResultsSortState,
ResultSet, ResultSet,
UserSettings,
} from "../../common/interface-types"; } from "../../common/interface-types";
import { SortDirection } from "../../common/interface-types"; import { SortDirection } from "../../common/interface-types";
import { assertNever } from "../../common/helpers-pure"; import { assertNever } from "../../common/helpers-pure";
@@ -11,6 +12,7 @@ import type { UrlValueResolvable } from "../../common/raw-result-types";
export interface ResultTableProps { export interface ResultTableProps {
resultSet: ResultSet; resultSet: ResultSet;
databaseUri: string; databaseUri: string;
userSettings: UserSettings;
metadata?: QueryMetadata; metadata?: QueryMetadata;
resultsPath: string | undefined; resultsPath: string | undefined;
sortState?: RawResultsSortState; sortState?: RawResultsSortState;
@@ -41,7 +43,7 @@ export const selectedRowClassName = "vscode-codeql__result-table-row--selected";
export function jumpToLocation( export function jumpToLocation(
loc: UrlValueResolvable, loc: UrlValueResolvable,
databaseUri: string, databaseUri: string | undefined,
): void { ): void {
vscode.postMessage({ vscode.postMessage({
t: "viewSourceFile", t: "viewSourceFile",

View File

@@ -144,3 +144,12 @@ td.vscode-codeql__path-index-cell {
.vscode-codeql__location-cell { .vscode-codeql__location-cell {
text-align: right !important; text-align: right !important;
} }
.vscode-codeql__taxa-cell {
text-align: left !important;
}
.vscode-codeql__taxa-cell-div {
background-color: transparent;
display: grid;
}