Merge pull request #3669 from github/dbartol/provenance

Display path provenance information in results view
This commit is contained in:
Dave Bartolomeo
2024-07-30 16:37:10 -04:00
committed by GitHub
22 changed files with 340 additions and 52 deletions

View File

@@ -147,6 +147,21 @@ interface SetStateMsg {
parsedResultSets: ParsedResultSets;
}
export interface UserSettings {
/** Whether to display links to the dataflow models that generated particular nodes in a flow path. */
shouldShowProvenance: boolean;
}
export const DEFAULT_USER_SETTINGS: UserSettings = {
shouldShowProvenance: false,
};
/** 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
* results.
@@ -191,6 +206,7 @@ interface UntoggleShowProblemsMsg {
export type IntoResultsViewMsg =
| ResultsUpdatingMsg
| SetStateMsg
| SetUserSettingsMsg
| ShowInterpretedPageMsg
| NavigateMsg
| UntoggleShowProblemsMsg;
@@ -208,13 +224,15 @@ export type FromResultsViewMsg =
| 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.
*/
interface ViewSourceFileMsg {
t: "viewSourceFile";
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 +359,8 @@ interface ChangeCompareMessage {
export type ToCompareViewMessage =
| SetComparisonQueryInfoMessage
| SetComparisonsMessage;
| SetComparisonsMessage
| SetUserSettingsMsg;
/**
* 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 { connectTo } from "stream-json/Assembler";
import { getErrorMessage } from "./helpers-pure";
import { withParser } from "stream-json/filters/Pick";
const DUMMY_TOOL: Tool = { driver: { name: "" } };
import { withParser } from "stream-json/filters/Ignore";
export async function sarifParser(
interpretedResultsPath: string,
): Promise<Log> {
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(
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
@@ -38,15 +40,17 @@ export async function sarifParser(
});
asm.on("done", (asm) => {
const log: Log = {
version: "2.1.0",
runs: [
{
tool: DUMMY_TOOL,
results: asm.current ?? [],
},
],
};
const log = asm.current;
// Do some trivial validation. This isn't a full validation of the SARIF file, but it's at
// least enough to ensure that we're not trying to parse complete garbage later.
if (log.runs === undefined || log.runs.length < 1) {
reject(
new Error(
"Invalid SARIF file: expecting at least one run with result.",
),
);
}
resolve(log);
alreadyDone = true;

View File

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

View File

@@ -37,11 +37,12 @@ export const shownLocationLineDecoration =
/**
* 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 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(
loc: UrlValueLineColumnLocation,
databaseItem: DatabaseItem,
databaseItem: DatabaseItem | undefined,
): Location {
// `Range` is a half-open interval, and is zero-based. CodeQL locations are closed intervals, and
// are one-based. Adjust accordingly.
@@ -52,7 +53,10 @@ function resolveFivePartLocation(
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(
loc: UrlValueWholeFileLocation,
databaseItem: DatabaseItem,
databaseItem: DatabaseItem | undefined,
): Location {
// A location corresponding to the start of the file.
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
* can be resolved, returns `undefined`.
* @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(
loc: UrlValueResolvable | undefined,
databaseItem: DatabaseItem,
databaseItem: DatabaseItem | undefined,
): Location | undefined {
if (!loc) {
return;
@@ -95,7 +103,7 @@ export function tryResolveLocation(
export async function showResolvableLocation(
loc: UrlValueResolvable,
databaseItem: DatabaseItem,
databaseItem: DatabaseItem | undefined,
logger: Logger,
): Promise<void> {
try {
@@ -151,13 +159,14 @@ export async function showLocation(location?: Location) {
}
export async function jumpToLocation(
databaseUri: string,
databaseUri: string | undefined,
loc: UrlValueResolvable,
databaseManager: DatabaseManager,
logger: Logger,
) {
const databaseItem = databaseManager.findDatabaseItem(Uri.parse(databaseUri));
if (databaseItem !== undefined) {
const databaseItem =
databaseUri !== undefined
? databaseManager.findDatabaseItem(Uri.parse(databaseUri))
: undefined;
await showResolvableLocation(loc, databaseItem, logger);
}
}

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
import type {
SetComparisonQueryInfoMessage,
SetComparisonsMessage,
UserSettings,
} from "../../common/interface-types";
import { className } from "../results/result-table-utils";
import { vscode } from "../vscode-api";
@@ -12,6 +13,7 @@ import { InterpretedCompareResultTable } from "./InterpretedCompareResultTable";
interface Props {
queryInfo: SetComparisonQueryInfoMessage;
comparison: SetComparisonsMessage;
userSettings: UserSettings;
}
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!;
async function openQuery(kind: "from" | "to") {
@@ -78,6 +84,7 @@ export default function CompareTable({ queryInfo, comparison }: Props) {
{result.kind === "interpreted" && (
<InterpretedCompareResultTable
results={result.from}
userSettings={userSettings}
databaseUri={queryInfo.databaseUri}
sourceLocationPrefix={result.sourceLocationPrefix}
/>
@@ -96,6 +103,7 @@ export default function CompareTable({ queryInfo, comparison }: Props) {
{result.kind === "interpreted" && (
<InterpretedCompareResultTable
results={result.to}
userSettings={userSettings}
databaseUri={queryInfo.databaseUri}
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 type { UserSettings } from "../../common/interface-types";
type Props = {
results: Result[];
databaseUri: string;
sourceLocationPrefix: string;
run?: Run;
userSettings: UserSettings;
};
export const InterpretedCompareResultTable = ({
results,
databaseUri,
sourceLocationPrefix,
userSettings,
}: Props) => {
return (
<AlertTable
results={results}
userSettings={userSettings}
databaseUri={databaseUri}
sourceLocationPrefix={sourceLocationPrefix}
header={
<thead>
<tr>
<th colSpan={2}></th>
<th className={`vscode-codeql__alert-message-cell`} colSpan={3}>
<th className={`vscode-codeql__alert-message-cell`} colSpan={4}>
Message
</th>
</tr>

View File

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

View File

@@ -45,7 +45,7 @@ export function AlertTableHeader({
<th colSpan={2}></th>
<th
className={`${sortClass()} vscode-codeql__alert-message-cell`}
colSpan={3}
colSpan={4}
onClick={toggleSortStateForColumn}
>
Message

View File

@@ -1,4 +1,4 @@
import type { ThreadFlowLocation } from "sarif";
import type { ThreadFlowLocation, Run } from "sarif";
import type {
PathNode,
Result as ResultKeysResult,
@@ -9,6 +9,8 @@ import { SarifLocation } from "./locations/SarifLocation";
import { selectableZebraStripe } from "./result-table-utils";
import { useCallback, useMemo } from "react";
import { VerticalRule } from "../common/VerticalRule";
import type { UserSettings } from "../../common/interface-types";
import { TaxaLocations } from "./locations/TaxaLocations";
interface Props {
step: ThreadFlowLocation;
@@ -19,6 +21,8 @@ interface Props {
selectedItemRef: React.RefObject<HTMLTableRowElement>;
databaseUri: string;
sourceLocationPrefix: string;
run?: Run;
userSettings: UserSettings;
updateSelectionCallback: (
resultKey: PathNode | ResultKeysResult | undefined,
) => void;
@@ -34,6 +38,8 @@ export function AlertTablePathNodeRow(props: Props) {
selectedItemRef,
databaseUri,
sourceLocationPrefix,
run,
userSettings,
updateSelectionCallback,
} = props;
@@ -86,6 +92,23 @@ export function AlertTablePathNodeRow(props: Props) {
"[no location]"
)}
</td>
<td
{...selectableZebraStripe(
isSelected,
zebraIndex,
"vscode-codeql__taxa-cell",
)}
>
{userSettings.shouldShowProvenance ? (
<div className="vscode-codeql__taxa-cell-contents">
<TaxaLocations
taxa={step.taxa}
run={run}
onClick={handleSarifLocationClicked}
/>
</div>
) : null}
</td>
<td
{...selectableZebraStripe(
isSelected,

View File

@@ -1,4 +1,4 @@
import type { ThreadFlow } from "sarif";
import type { Run, ThreadFlow } from "sarif";
import type {
PathNode,
Result as ResultKeysResult,
@@ -10,6 +10,7 @@ import { AlertTablePathNodeRow } from "./AlertTablePathNodeRow";
import { AlertTableDropdownIndicatorCell } from "./AlertTableDropdownIndicatorCell";
import { useCallback, useMemo } from "react";
import { VerticalRule } from "../common/VerticalRule";
import type { UserSettings } from "../../common/interface-types";
interface Props {
path: ThreadFlow;
@@ -20,6 +21,8 @@ interface Props {
selectedItemRef: React.RefObject<HTMLTableRowElement>;
databaseUri: string;
sourceLocationPrefix: string;
run?: Run;
userSettings: UserSettings;
updateSelectionCallback: (
resultKey: PathNode | ResultKeysResult | undefined,
) => void;
@@ -61,7 +64,7 @@ export function AlertTablePathRow(props: Props) {
expanded={currentPathExpanded}
onClick={handleDropdownClick}
/>
<td className="vscode-codeql__text-center" colSpan={3}>
<td className="vscode-codeql__text-center" colSpan={4}>
Path
</td>
</tr>

View File

@@ -1,4 +1,4 @@
import type { Result } from "sarif";
import type { Result, Run } from "sarif";
import type {
PathNode,
Result as ResultKeysResult,
@@ -12,6 +12,7 @@ import { useCallback, useMemo } from "react";
import { SarifLocation } from "./locations/SarifLocation";
import { SarifMessageWithLocations } from "./locations/SarifMessageWithLocations";
import { AlertTablePathRow } from "./AlertTablePathRow";
import type { UserSettings } from "../../common/interface-types";
interface Props {
result: Result;
@@ -21,6 +22,8 @@ interface Props {
selectedItemRef: React.RefObject<HTMLTableRowElement>;
databaseUri: string;
sourceLocationPrefix: string;
run?: Run;
userSettings: UserSettings;
updateSelectionCallback: (
resultKey: PathNode | ResultKeysResult | undefined,
) => void;
@@ -90,7 +93,7 @@ export function AlertTableResultRow(props: Props) {
{result.codeFlows === undefined ? (
<>
<td className="vscode-codeql__icon-cell">{info}</td>
<td colSpan={3}>{msg}</td>
<td colSpan={4}>{msg}</td>
</>
) : (
<>
@@ -99,7 +102,7 @@ export function AlertTableResultRow(props: Props) {
onClick={handleDropdownClick}
/>
<td className="vscode-codeql__icon-cell">{listUnordered}</td>
<td colSpan={2}>{msg}</td>
<td colSpan={3}>{msg}</td>
</>
)}
<td className="vscode-codeql__location-cell">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,159 @@
import type {
Location as SarifLogLocation,
ArtifactLocation,
PhysicalLocation,
ReportingDescriptorReference,
Run,
ToolComponent,
} from "sarif";
import { SarifLocation } from "./SarifLocation";
/** 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,
),
},
},
};
}
interface Props {
taxa: ReportingDescriptorReference[] | undefined;
run: Run | undefined;
onClick: () => void;
}
/** Generate the React elements for each taxon. */
export function TaxaLocations({
taxa,
run,
onClick,
}: Props): React.JSX.Element[] {
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>
);
});
}

View File

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

View File

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