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[];
}
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 {
schema,
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
* the file path is empty. If so, we do not want to render this location
* Checks whether the file path is empty. If so, we do not want to render this location
* as a link.
*
* @param uri A file uri

View File

@@ -19,7 +19,7 @@ import { Logger } from '../logging';
import { CodeQLCliServer } from '../cli';
import { DatabaseManager } from '../databases';
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';
interface ComparePair {
@@ -256,7 +256,7 @@ export class CompareInterfaceManager extends DisposableObject {
resultsPath,
resultSetName
);
return adaptBqrs(schema, chunk);
return transformBqrsResultSet(schema, chunk);
}
private compareResults(

View File

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

View File

@@ -7,6 +7,9 @@ export interface SarifLink {
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 {
hint: string;
}
@@ -85,7 +88,6 @@ export function parseSarifLocation(
// file uri or a relative uri.
const uri = physicalLocation.artifactLocation.uri;
// FIXME: This is probably wrong
const fileUriRegex = /^file:/;
const effectiveLocation = uri.match(fileUriRegex)
? uri

View File

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

View File

@@ -132,7 +132,13 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
if ('hint' in parsedLoc) {
return renderNonLocation(text, parsedLoc.hint);
} else if (isWholeFileLoc(parsedLoc) || isLineColumnLoc(parsedLoc)) {
return renderLocation(parsedLoc, text, databaseUri, undefined, updateSelectionCallback(pathNodeKey));
return renderLocation(
parsedLoc,
text,
databaseUri,
undefined,
updateSelectionCallback(pathNodeKey)
);
} else {
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
* 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);
if ('hint' in parsedLoc) {
return renderNonLocation('[no location]', parsedLoc.hint);
} else if (isWholeFileLoc(parsedLoc)) {
const shortLocation = `${path.basename(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)) {
const shortLocation = `${path.basename(parsedLoc.userVisibleFile)}:${parsedLoc.startLine}:${parsedLoc.startColumn}`;
const longLocation = `${parsedLoc.userVisibleFile}`;
return renderLocation(parsedLoc, shortLocation, databaseUri, longLocation, updateSelectionCallback(pathNodeKey));
return renderLocation(
parsedLoc,
shortLocation,
databaseUri,
longLocation,
updateSelectionCallback(pathNodeKey)
);
} else {
return undefined;
}
@@ -163,9 +184,7 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
return (e) => this.toggle(e, indices);
};
if (resultSet.sarif.runs.length === 0 ||
resultSet.sarif.runs[0].results === undefined ||
resultSet.sarif.runs[0].results.length === 0) {
if (!resultSet.sarif.runs?.[0]?.results?.length) {
return this.renderNoResults();
}
@@ -202,7 +221,7 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
[expansionIndex];
rows.push(
<tr {...zebraStripe(resultIndex)}>
<tr {...zebraStripe(resultIndex)} key={resultIndex}>
<td className="vscode-codeql__icon-cell vscode-codeql__dropdown-cell" onMouseDown={toggler(indices)}>
{indicator}
</td>
@@ -223,7 +242,7 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
if (currentResultExpanded) {
const indicator = currentPathExpanded ? octicons.chevronDown : octicons.chevronRight;
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 vscode-codeql__dropdown-cell" onMouseDown={toggler([expansionIndex])}>{indicator}</td>
<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 zebraIndex = resultIndex + stepIndex;
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 {...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) {
rows.push(<tr><td colSpan={5} style={{ textAlign: 'center', fontStyle: 'italic' }}>
rows.push(
<tr key="truncatd-message">
<td colSpan={5} style={{ textAlign: 'center', fontStyle: 'italic' }}>
Too many results to show at once. {numTruncatedResults} result(s) omitted.
</td></tr>);
</td>
</tr>
);
}
return <table className={className}>

View File

@@ -1,6 +1,6 @@
import * as React from 'react';
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 { assertNever } from '../helpers-pure';
import { ResultSet } from '../interface-types';
@@ -60,29 +60,42 @@ export function jumpToLocation(loc: ResolvableLocationValue, databaseUri: string
/**
* Render a location as a link which when clicked displays the original location.
*/
export function renderLocation(loc: UrlValue | undefined, label: string | undefined,
databaseUri: string, title?: string, callback?: () => void): JSX.Element {
export function renderLocation(
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.
let displayLabel = label;
if (label === undefined || label === '')
if (!label) {
displayLabel = '[empty string]';
else if (label.match(/^\s+$/))
} else if (label.match(/^\s+$/)) {
displayLabel = `[whitespace: "${label}"]`;
}
if (loc !== undefined) {
const resolvableLoc = tryGetResolvableLocation(loc);
if (resolvableLoc !== undefined) {
return <a href="#"
return (
<a href="#"
className="vscode-codeql__result-table-location-link"
title={title}
onClick={jumpToLocationHandler(resolvableLoc, databaseUri, callback)}>{displayLabel}</a>;
onClick={jumpToLocationHandler(resolvableLoc, databaseUri, callback)}>
{displayLabel}
</a>
);
} else {
return <span title={title}>{displayLabel}</span>;
}
}
return <span />;
}
/**
* Returns the attributes for a zebra-striped table row at position `index`.

View File

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

View File

@@ -71,6 +71,23 @@ describe('interface-utils', () => {
describe('resolveWholeFileLocation', () => {
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 = ({
resolveSourceFile: sinon.stub().returns(vscode.Uri.file('abc')),
} as unknown) as DatabaseItem;