Refactor: Change renderLocation in webview

* It is now more general and the logic is simplified
* Also, add more comments
* Rename `adaptBqrs` to `transformBqrsResultSet`
* Remove a react error for missing a key attribute in a list
This commit is contained in:
Andrew Eisenberg
2020-09-16 14:37:29 -07:00
parent 84028434e0
commit db6fc5d7f0
10 changed files with 112 additions and 57 deletions

View File

@@ -90,7 +90,13 @@ export interface RawResultSet {
readonly rows: readonly ResultRow[]; readonly rows: readonly ResultRow[];
} }
export function adaptBqrs(schema: ResultSetSchema, page: DecodedBqrsChunk): RawResultSet { // TODO: This function is not necessary. It generates a tuple that is slightly easier
// to handle than the ResultSetSchema and DecodedBqrsChunk. But perhaps it is unnecessary
// boilerplate.
export function transformBqrsResultSet(
schema: ResultSetSchema,
page: DecodedBqrsChunk
): RawResultSet {
return { return {
schema, schema,
rows: Array.from(page.tuples), rows: Array.from(page.tuples),

View File

@@ -63,8 +63,7 @@ function isWholeFileMatch(matches: RegExpExecArray): boolean {
} }
/** /**
* Checks whether the file path is empty. For now, just check whether * Checks whether the file path is empty. If so, we do not want to render this location
* the file path is empty. If so, we do not want to render this location
* as a link. * as a link.
* *
* @param uri A file uri * @param uri A file uri

View File

@@ -19,7 +19,7 @@ import { Logger } from '../logging';
import { CodeQLCliServer } from '../cli'; import { CodeQLCliServer } from '../cli';
import { DatabaseManager } from '../databases'; import { DatabaseManager } from '../databases';
import { getHtmlForWebview, jumpToLocation } from '../interface-utils'; import { getHtmlForWebview, jumpToLocation } from '../interface-utils';
import { adaptBqrs, RawResultSet, BQRSInfo } from '../bqrs-cli-types'; import { transformBqrsResultSet, RawResultSet, BQRSInfo } from '../bqrs-cli-types';
import resultsDiff from './resultsDiff'; import resultsDiff from './resultsDiff';
interface ComparePair { interface ComparePair {
@@ -256,7 +256,7 @@ export class CompareInterfaceManager extends DisposableObject {
resultsPath, resultsPath,
resultSetName resultSetName
); );
return adaptBqrs(schema, chunk); return transformBqrsResultSet(schema, chunk);
} }
private compareResults( private compareResults(

View File

@@ -46,7 +46,7 @@ import {
jumpToLocation, jumpToLocation,
} from './interface-utils'; } from './interface-utils';
import { getDefaultResultSetName, ParsedResultSets } from './interface-types'; import { getDefaultResultSetName, ParsedResultSets } from './interface-types';
import { RawResultSet, adaptBqrs, ResultSetSchema } from './bqrs-cli-types'; import { RawResultSet, transformBqrsResultSet, ResultSetSchema } from './bqrs-cli-types';
/** /**
* interface.ts * interface.ts
@@ -372,7 +372,7 @@ export class InterfaceManager extends DisposableObject {
pageSize: RAW_RESULTS_PAGE_SIZE pageSize: RAW_RESULTS_PAGE_SIZE
} }
); );
const resultSet = adaptBqrs(schema, chunk); const resultSet = transformBqrsResultSet(schema, chunk);
return { return {
pageNumber: 0, pageNumber: 0,
numPages: numPagesOfResultSet(resultSet), numPages: numPagesOfResultSet(resultSet),
@@ -485,7 +485,7 @@ export class InterfaceManager extends DisposableObject {
pageSize: RAW_RESULTS_PAGE_SIZE pageSize: RAW_RESULTS_PAGE_SIZE
} }
); );
const resultSet = adaptBqrs(schema, chunk); const resultSet = transformBqrsResultSet(schema, chunk);
const parsedResultSets: ParsedResultSets = { const parsedResultSets: ParsedResultSets = {
pageNumber, pageNumber,

View File

@@ -7,6 +7,9 @@ export interface SarifLink {
text: string; text: string;
} }
// The type of a result that has no associated location.
// hint is a string intended for display to the user
// that explains why there is no location.
interface NoLocation { interface NoLocation {
hint: string; hint: string;
} }
@@ -85,7 +88,6 @@ export function parseSarifLocation(
// file uri or a relative uri. // file uri or a relative uri.
const uri = physicalLocation.artifactLocation.uri; const uri = physicalLocation.artifactLocation.uri;
// FIXME: This is probably wrong
const fileUriRegex = /^file:/; const fileUriRegex = /^file:/;
const effectiveLocation = uri.match(fileUriRegex) const effectiveLocation = uri.match(fileUriRegex)
? uri ? uri

View File

@@ -2,7 +2,6 @@ import * as React from 'react';
import { renderLocation } from './result-table-utils'; import { renderLocation } from './result-table-utils';
import { ColumnValue } from '../bqrs-cli-types'; import { ColumnValue } from '../bqrs-cli-types';
import { isStringLoc, isWholeFileLoc, isLineColumnLoc } from '../bqrs-utils';
interface Props { interface Props {
value: ColumnValue; value: ColumnValue;
@@ -11,20 +10,13 @@ interface Props {
export default function RawTableValue(props: Props): JSX.Element { export default function RawTableValue(props: Props): JSX.Element {
const v = props.value; const v = props.value;
if (typeof v === 'string' if (
typeof v === 'string'
|| typeof v === 'number' || typeof v === 'number'
|| typeof v === 'boolean') { || typeof v === 'boolean'
) {
return <span>{v}</span>; return <span>{v}</span>;
} }
const loc = v.url; return renderLocation(v.url, v.label, props.databaseUri);
if (!loc) {
return <span />;
} else if (isStringLoc(loc)) {
return <a href={loc}>{loc}</a>;
} else if (isWholeFileLoc(loc) || isLineColumnLoc(loc)) {
return renderLocation(loc, v.label, props.databaseUri);
} else {
return <span />;
}
} }

View File

@@ -132,7 +132,13 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
if ('hint' in parsedLoc) { if ('hint' in parsedLoc) {
return renderNonLocation(text, parsedLoc.hint); return renderNonLocation(text, parsedLoc.hint);
} else if (isWholeFileLoc(parsedLoc) || isLineColumnLoc(parsedLoc)) { } else if (isWholeFileLoc(parsedLoc) || isLineColumnLoc(parsedLoc)) {
return renderLocation(parsedLoc, text, databaseUri, undefined, updateSelectionCallback(pathNodeKey)); return renderLocation(
parsedLoc,
text,
databaseUri,
undefined,
updateSelectionCallback(pathNodeKey)
);
} else { } else {
return undefined; return undefined;
} }
@@ -142,18 +148,33 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
* Render sarif location as a link with the text being simply a * Render sarif location as a link with the text being simply a
* human-readable form of the location itself. * human-readable form of the location itself.
*/ */
function renderSarifLocation(loc: Sarif.Location, pathNodeKey: Keys.PathNode | undefined): JSX.Element | undefined { function renderSarifLocation(
loc: Sarif.Location,
pathNodeKey: Keys.PathNode | undefined
): JSX.Element | undefined {
const parsedLoc = parseSarifLocation(loc, sourceLocationPrefix); const parsedLoc = parseSarifLocation(loc, sourceLocationPrefix);
if ('hint' in parsedLoc) { if ('hint' in parsedLoc) {
return renderNonLocation('[no location]', parsedLoc.hint); return renderNonLocation('[no location]', parsedLoc.hint);
} else if (isWholeFileLoc(parsedLoc)) { } else if (isWholeFileLoc(parsedLoc)) {
const shortLocation = `${path.basename(parsedLoc.userVisibleFile)}`; const shortLocation = `${path.basename(parsedLoc.userVisibleFile)}`;
const longLocation = `${parsedLoc.userVisibleFile}`; const longLocation = `${parsedLoc.userVisibleFile}`;
return renderLocation(parsedLoc, shortLocation, databaseUri, longLocation, updateSelectionCallback(pathNodeKey)); return renderLocation(
parsedLoc,
shortLocation,
databaseUri,
longLocation,
updateSelectionCallback(pathNodeKey)
);
} else if (isLineColumnLoc(parsedLoc)) { } else if (isLineColumnLoc(parsedLoc)) {
const shortLocation = `${path.basename(parsedLoc.userVisibleFile)}:${parsedLoc.startLine}:${parsedLoc.startColumn}`; const shortLocation = `${path.basename(parsedLoc.userVisibleFile)}:${parsedLoc.startLine}:${parsedLoc.startColumn}`;
const longLocation = `${parsedLoc.userVisibleFile}`; const longLocation = `${parsedLoc.userVisibleFile}`;
return renderLocation(parsedLoc, shortLocation, databaseUri, longLocation, updateSelectionCallback(pathNodeKey)); return renderLocation(
parsedLoc,
shortLocation,
databaseUri,
longLocation,
updateSelectionCallback(pathNodeKey)
);
} else { } else {
return undefined; return undefined;
} }
@@ -163,9 +184,7 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
return (e) => this.toggle(e, indices); return (e) => this.toggle(e, indices);
}; };
if (resultSet.sarif.runs.length === 0 || if (!resultSet.sarif.runs?.[0]?.results?.length) {
resultSet.sarif.runs[0].results === undefined ||
resultSet.sarif.runs[0].results.length === 0) {
return this.renderNoResults(); return this.renderNoResults();
} }
@@ -202,7 +221,7 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
[expansionIndex]; [expansionIndex];
rows.push( rows.push(
<tr {...zebraStripe(resultIndex)}> <tr {...zebraStripe(resultIndex)} key={resultIndex}>
<td className="vscode-codeql__icon-cell vscode-codeql__dropdown-cell" onMouseDown={toggler(indices)}> <td className="vscode-codeql__icon-cell vscode-codeql__dropdown-cell" onMouseDown={toggler(indices)}>
{indicator} {indicator}
</td> </td>
@@ -223,7 +242,7 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
if (currentResultExpanded) { if (currentResultExpanded) {
const indicator = currentPathExpanded ? octicons.chevronDown : octicons.chevronRight; const indicator = currentPathExpanded ? octicons.chevronDown : octicons.chevronRight;
rows.push( rows.push(
<tr {...zebraStripe(resultIndex)}> <tr {...zebraStripe(resultIndex)} key={`${resultIndex}-${pathIndex}`}>
<td className="vscode-codeql__icon-cell"><span className="vscode-codeql__vertical-rule"></span></td> <td className="vscode-codeql__icon-cell"><span className="vscode-codeql__vertical-rule"></span></td>
<td className="vscode-codeql__icon-cell vscode-codeql__dropdown-cell" onMouseDown={toggler([expansionIndex])}>{indicator}</td> <td className="vscode-codeql__icon-cell vscode-codeql__dropdown-cell" onMouseDown={toggler([expansionIndex])}>{indicator}</td>
<td className="vscode-codeql__text-center" colSpan={3}> <td className="vscode-codeql__text-center" colSpan={3}>
@@ -249,7 +268,7 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
const stepIndex = pathNodeIndex + 1; // Convert to 1-based const stepIndex = pathNodeIndex + 1; // Convert to 1-based
const zebraIndex = resultIndex + stepIndex; const zebraIndex = resultIndex + stepIndex;
rows.push( rows.push(
<tr className={isSelected ? 'vscode-codeql__selected-path-node' : undefined}> <tr className={isSelected ? 'vscode-codeql__selected-path-node' : undefined} key={`${resultIndex}-${pathIndex}-${pathNodeIndex}`}>
<td className="vscode-codeql__icon-cell"><span className="vscode-codeql__vertical-rule"></span></td> <td className="vscode-codeql__icon-cell"><span className="vscode-codeql__vertical-rule"></span></td>
<td className="vscode-codeql__icon-cell"><span className="vscode-codeql__vertical-rule"></span></td> <td className="vscode-codeql__icon-cell"><span className="vscode-codeql__vertical-rule"></span></td>
<td {...selectableZebraStripe(isSelected, zebraIndex, 'vscode-codeql__path-index-cell')}>{stepIndex}</td> <td {...selectableZebraStripe(isSelected, zebraIndex, 'vscode-codeql__path-index-cell')}>{stepIndex}</td>
@@ -264,9 +283,13 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
}); });
if (numTruncatedResults > 0) { if (numTruncatedResults > 0) {
rows.push(<tr><td colSpan={5} style={{ textAlign: 'center', fontStyle: 'italic' }}> rows.push(
Too many results to show at once. {numTruncatedResults} result(s) omitted. <tr key="truncatd-message">
</td></tr>); <td colSpan={5} style={{ textAlign: 'center', fontStyle: 'italic' }}>
Too many results to show at once. {numTruncatedResults} result(s) omitted.
</td>
</tr>
);
} }
return <table className={className}> return <table className={className}>

View File

@@ -1,6 +1,6 @@
import * as React from 'react'; import * as React from 'react';
import { UrlValue, ResolvableLocationValue } from '../bqrs-cli-types'; import { UrlValue, ResolvableLocationValue } from '../bqrs-cli-types';
import { tryGetResolvableLocation } from '../bqrs-utils'; import { isStringLoc, tryGetResolvableLocation } from '../bqrs-utils';
import { RawResultsSortState, QueryMetadata, SortDirection } from '../interface-types'; import { RawResultsSortState, QueryMetadata, SortDirection } from '../interface-types';
import { assertNever } from '../helpers-pure'; import { assertNever } from '../helpers-pure';
import { ResultSet } from '../interface-types'; import { ResultSet } from '../interface-types';
@@ -60,28 +60,41 @@ export function jumpToLocation(loc: ResolvableLocationValue, databaseUri: string
/** /**
* Render a location as a link which when clicked displays the original location. * Render a location as a link which when clicked displays the original location.
*/ */
export function renderLocation(loc: UrlValue | undefined, label: string | undefined, export function renderLocation(
databaseUri: string, title?: string, callback?: () => void): JSX.Element { loc: UrlValue | undefined,
label: string | undefined,
databaseUri: string,
title?: string,
callback?: () => void
): JSX.Element {
if (loc === undefined) {
return <span />;
} else if (isStringLoc(loc)) {
return <a href={loc}>{loc}</a>;
}
// If the label was empty, use a placeholder instead, so the link is still clickable. // If the label was empty, use a placeholder instead, so the link is still clickable.
let displayLabel = label; let displayLabel = label;
if (label === undefined || label === '') if (!label) {
displayLabel = '[empty string]'; displayLabel = '[empty string]';
else if (label.match(/^\s+$/)) } else if (label.match(/^\s+$/)) {
displayLabel = `[whitespace: "${label}"]`; displayLabel = `[whitespace: "${label}"]`;
}
if (loc !== undefined) { const resolvableLoc = tryGetResolvableLocation(loc);
const resolvableLoc = tryGetResolvableLocation(loc); if (resolvableLoc !== undefined) {
if (resolvableLoc !== undefined) { return (
return <a href="#" <a href="#"
className="vscode-codeql__result-table-location-link" className="vscode-codeql__result-table-location-link"
title={title} title={title}
onClick={jumpToLocationHandler(resolvableLoc, databaseUri, callback)}>{displayLabel}</a>; onClick={jumpToLocationHandler(resolvableLoc, databaseUri, callback)}>
} else { {displayLabel}
return <span title={title}>{displayLabel}</span>; </a>
} );
} else {
return <span title={title}>{displayLabel}</span>;
} }
return <span />;
} }
/** /**

View File

@@ -11,7 +11,7 @@ import {
QueryMetadata, QueryMetadata,
ResultsPaths, ResultsPaths,
ALERTS_TABLE_NAME, ALERTS_TABLE_NAME,
ParsedResultSets, ParsedResultSets
} from '../interface-types'; } from '../interface-types';
import { EventHandlers as EventHandlerList } from './event-handler-list'; import { EventHandlers as EventHandlerList } from './event-handler-list';
import { ResultTables } from './result-tables'; import { ResultTables } from './result-tables';
@@ -172,14 +172,17 @@ class App extends React.Component<{}, ResultsViewState> {
}); });
} }
private async getResultSets( private getResultSets(
resultsInfo: ResultsInfo resultsInfo: ResultsInfo
): Promise<readonly ResultSet[]> { ): readonly ResultSet[] {
const parsedResultSets = resultsInfo.parsedResultSets; const parsedResultSets = resultsInfo.parsedResultSets;
return [{ const resultSet = parsedResultSets.resultSet;
...parsedResultSets.resultSet, if (!resultSet.t) {
t: (parsedResultSets.resultSet.t ?? 'RawResultSet') as any throw new Error(
}]; 'Missing result set type. Should be either "SarifResultSet" or "RawResultSet".'
);
}
return [resultSet];
} }
private async loadResults(): Promise<void> { private async loadResults(): Promise<void> {
@@ -191,7 +194,7 @@ class App extends React.Component<{}, ResultsViewState> {
let results: Results | null = null; let results: Results | null = null;
let statusText = ''; let statusText = '';
try { try {
const resultSets = await this.getResultSets(resultsInfo); const resultSets = this.getResultSets(resultsInfo);
results = { results = {
resultSets, resultSets,
database: resultsInfo.database, database: resultsInfo.database,

View File

@@ -71,6 +71,23 @@ describe('interface-utils', () => {
describe('resolveWholeFileLocation', () => { describe('resolveWholeFileLocation', () => {
it('should resolve a whole file location', () => { it('should resolve a whole file location', () => {
const mockDatabaseItem: DatabaseItem = ({
resolveSourceFile: sinon.stub().returns(vscode.Uri.file('abc')),
} as unknown) as DatabaseItem;
expect(
tryResolveLocation(
'file://hucairz:0:0:0:0',
mockDatabaseItem
)
).to.deep.equal(
new vscode.Location(
vscode.Uri.file('abc'),
new vscode.Range(0, 0, 0, 0)
)
);
});
it('should resolve a five-part location edge case', () => {
const mockDatabaseItem: DatabaseItem = ({ const mockDatabaseItem: DatabaseItem = ({
resolveSourceFile: sinon.stub().returns(vscode.Uri.file('abc')), resolveSourceFile: sinon.stub().returns(vscode.Uri.file('abc')),
} as unknown) as DatabaseItem; } as unknown) as DatabaseItem;