Skip to content

Commit db6fc5d

Browse files
committed
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
1 parent 8402843 commit db6fc5d

10 files changed

Lines changed: 112 additions & 57 deletions

File tree

extensions/ql-vscode/src/bqrs-cli-types.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,13 @@ export interface RawResultSet {
9090
readonly rows: readonly ResultRow[];
9191
}
9292

93-
export function adaptBqrs(schema: ResultSetSchema, page: DecodedBqrsChunk): RawResultSet {
93+
// TODO: This function is not necessary. It generates a tuple that is slightly easier
94+
// to handle than the ResultSetSchema and DecodedBqrsChunk. But perhaps it is unnecessary
95+
// boilerplate.
96+
export function transformBqrsResultSet(
97+
schema: ResultSetSchema,
98+
page: DecodedBqrsChunk
99+
): RawResultSet {
94100
return {
95101
schema,
96102
rows: Array.from(page.tuples),

extensions/ql-vscode/src/bqrs-utils.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,7 @@ function isWholeFileMatch(matches: RegExpExecArray): boolean {
6363
}
6464

6565
/**
66-
* Checks whether the file path is empty. For now, just check whether
67-
* the file path is empty. If so, we do not want to render this location
66+
* Checks whether the file path is empty. If so, we do not want to render this location
6867
* as a link.
6968
*
7069
* @param uri A file uri

extensions/ql-vscode/src/compare/compare-interface.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { Logger } from '../logging';
1919
import { CodeQLCliServer } from '../cli';
2020
import { DatabaseManager } from '../databases';
2121
import { getHtmlForWebview, jumpToLocation } from '../interface-utils';
22-
import { adaptBqrs, RawResultSet, BQRSInfo } from '../bqrs-cli-types';
22+
import { transformBqrsResultSet, RawResultSet, BQRSInfo } from '../bqrs-cli-types';
2323
import resultsDiff from './resultsDiff';
2424

2525
interface ComparePair {
@@ -256,7 +256,7 @@ export class CompareInterfaceManager extends DisposableObject {
256256
resultsPath,
257257
resultSetName
258258
);
259-
return adaptBqrs(schema, chunk);
259+
return transformBqrsResultSet(schema, chunk);
260260
}
261261

262262
private compareResults(

extensions/ql-vscode/src/interface.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ import {
4646
jumpToLocation,
4747
} from './interface-utils';
4848
import { getDefaultResultSetName, ParsedResultSets } from './interface-types';
49-
import { RawResultSet, adaptBqrs, ResultSetSchema } from './bqrs-cli-types';
49+
import { RawResultSet, transformBqrsResultSet, ResultSetSchema } from './bqrs-cli-types';
5050

5151
/**
5252
* interface.ts
@@ -372,7 +372,7 @@ export class InterfaceManager extends DisposableObject {
372372
pageSize: RAW_RESULTS_PAGE_SIZE
373373
}
374374
);
375-
const resultSet = adaptBqrs(schema, chunk);
375+
const resultSet = transformBqrsResultSet(schema, chunk);
376376
return {
377377
pageNumber: 0,
378378
numPages: numPagesOfResultSet(resultSet),
@@ -485,7 +485,7 @@ export class InterfaceManager extends DisposableObject {
485485
pageSize: RAW_RESULTS_PAGE_SIZE
486486
}
487487
);
488-
const resultSet = adaptBqrs(schema, chunk);
488+
const resultSet = transformBqrsResultSet(schema, chunk);
489489

490490
const parsedResultSets: ParsedResultSets = {
491491
pageNumber,

extensions/ql-vscode/src/sarif-utils.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ export interface SarifLink {
77
text: string;
88
}
99

10+
// The type of a result that has no associated location.
11+
// hint is a string intended for display to the user
12+
// that explains why there is no location.
1013
interface NoLocation {
1114
hint: string;
1215
}
@@ -85,7 +88,6 @@ export function parseSarifLocation(
8588
// file uri or a relative uri.
8689
const uri = physicalLocation.artifactLocation.uri;
8790

88-
// FIXME: This is probably wrong
8991
const fileUriRegex = /^file:/;
9092
const effectiveLocation = uri.match(fileUriRegex)
9193
? uri

extensions/ql-vscode/src/view/RawTableValue.tsx

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import * as React from 'react';
22

33
import { renderLocation } from './result-table-utils';
44
import { ColumnValue } from '../bqrs-cli-types';
5-
import { isStringLoc, isWholeFileLoc, isLineColumnLoc } from '../bqrs-utils';
65

76
interface Props {
87
value: ColumnValue;
@@ -11,20 +10,13 @@ interface Props {
1110

1211
export default function RawTableValue(props: Props): JSX.Element {
1312
const v = props.value;
14-
if (typeof v === 'string'
13+
if (
14+
typeof v === 'string'
1515
|| typeof v === 'number'
16-
|| typeof v === 'boolean') {
16+
|| typeof v === 'boolean'
17+
) {
1718
return <span>{v}</span>;
1819
}
1920

20-
const loc = v.url;
21-
if (!loc) {
22-
return <span />;
23-
} else if (isStringLoc(loc)) {
24-
return <a href={loc}>{loc}</a>;
25-
} else if (isWholeFileLoc(loc) || isLineColumnLoc(loc)) {
26-
return renderLocation(loc, v.label, props.databaseUri);
27-
} else {
28-
return <span />;
29-
}
21+
return renderLocation(v.url, v.label, props.databaseUri);
3022
}

extensions/ql-vscode/src/view/alert-table.tsx

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,13 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
132132
if ('hint' in parsedLoc) {
133133
return renderNonLocation(text, parsedLoc.hint);
134134
} else if (isWholeFileLoc(parsedLoc) || isLineColumnLoc(parsedLoc)) {
135-
return renderLocation(parsedLoc, text, databaseUri, undefined, updateSelectionCallback(pathNodeKey));
135+
return renderLocation(
136+
parsedLoc,
137+
text,
138+
databaseUri,
139+
undefined,
140+
updateSelectionCallback(pathNodeKey)
141+
);
136142
} else {
137143
return undefined;
138144
}
@@ -142,18 +148,33 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
142148
* Render sarif location as a link with the text being simply a
143149
* human-readable form of the location itself.
144150
*/
145-
function renderSarifLocation(loc: Sarif.Location, pathNodeKey: Keys.PathNode | undefined): JSX.Element | undefined {
151+
function renderSarifLocation(
152+
loc: Sarif.Location,
153+
pathNodeKey: Keys.PathNode | undefined
154+
): JSX.Element | undefined {
146155
const parsedLoc = parseSarifLocation(loc, sourceLocationPrefix);
147156
if ('hint' in parsedLoc) {
148157
return renderNonLocation('[no location]', parsedLoc.hint);
149158
} else if (isWholeFileLoc(parsedLoc)) {
150159
const shortLocation = `${path.basename(parsedLoc.userVisibleFile)}`;
151160
const longLocation = `${parsedLoc.userVisibleFile}`;
152-
return renderLocation(parsedLoc, shortLocation, databaseUri, longLocation, updateSelectionCallback(pathNodeKey));
161+
return renderLocation(
162+
parsedLoc,
163+
shortLocation,
164+
databaseUri,
165+
longLocation,
166+
updateSelectionCallback(pathNodeKey)
167+
);
153168
} else if (isLineColumnLoc(parsedLoc)) {
154169
const shortLocation = `${path.basename(parsedLoc.userVisibleFile)}:${parsedLoc.startLine}:${parsedLoc.startColumn}`;
155170
const longLocation = `${parsedLoc.userVisibleFile}`;
156-
return renderLocation(parsedLoc, shortLocation, databaseUri, longLocation, updateSelectionCallback(pathNodeKey));
171+
return renderLocation(
172+
parsedLoc,
173+
shortLocation,
174+
databaseUri,
175+
longLocation,
176+
updateSelectionCallback(pathNodeKey)
177+
);
157178
} else {
158179
return undefined;
159180
}
@@ -163,9 +184,7 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
163184
return (e) => this.toggle(e, indices);
164185
};
165186

166-
if (resultSet.sarif.runs.length === 0 ||
167-
resultSet.sarif.runs[0].results === undefined ||
168-
resultSet.sarif.runs[0].results.length === 0) {
187+
if (!resultSet.sarif.runs?.[0]?.results?.length) {
169188
return this.renderNoResults();
170189
}
171190

@@ -202,7 +221,7 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
202221
[expansionIndex];
203222

204223
rows.push(
205-
<tr {...zebraStripe(resultIndex)}>
224+
<tr {...zebraStripe(resultIndex)} key={resultIndex}>
206225
<td className="vscode-codeql__icon-cell vscode-codeql__dropdown-cell" onMouseDown={toggler(indices)}>
207226
{indicator}
208227
</td>
@@ -223,7 +242,7 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
223242
if (currentResultExpanded) {
224243
const indicator = currentPathExpanded ? octicons.chevronDown : octicons.chevronRight;
225244
rows.push(
226-
<tr {...zebraStripe(resultIndex)}>
245+
<tr {...zebraStripe(resultIndex)} key={`${resultIndex}-${pathIndex}`}>
227246
<td className="vscode-codeql__icon-cell"><span className="vscode-codeql__vertical-rule"></span></td>
228247
<td className="vscode-codeql__icon-cell vscode-codeql__dropdown-cell" onMouseDown={toggler([expansionIndex])}>{indicator}</td>
229248
<td className="vscode-codeql__text-center" colSpan={3}>
@@ -249,7 +268,7 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
249268
const stepIndex = pathNodeIndex + 1; // Convert to 1-based
250269
const zebraIndex = resultIndex + stepIndex;
251270
rows.push(
252-
<tr className={isSelected ? 'vscode-codeql__selected-path-node' : undefined}>
271+
<tr className={isSelected ? 'vscode-codeql__selected-path-node' : undefined} key={`${resultIndex}-${pathIndex}-${pathNodeIndex}`}>
253272
<td className="vscode-codeql__icon-cell"><span className="vscode-codeql__vertical-rule"></span></td>
254273
<td className="vscode-codeql__icon-cell"><span className="vscode-codeql__vertical-rule"></span></td>
255274
<td {...selectableZebraStripe(isSelected, zebraIndex, 'vscode-codeql__path-index-cell')}>{stepIndex}</td>
@@ -264,9 +283,13 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
264283
});
265284

266285
if (numTruncatedResults > 0) {
267-
rows.push(<tr><td colSpan={5} style={{ textAlign: 'center', fontStyle: 'italic' }}>
268-
Too many results to show at once. {numTruncatedResults} result(s) omitted.
269-
</td></tr>);
286+
rows.push(
287+
<tr key="truncatd-message">
288+
<td colSpan={5} style={{ textAlign: 'center', fontStyle: 'italic' }}>
289+
Too many results to show at once. {numTruncatedResults} result(s) omitted.
290+
</td>
291+
</tr>
292+
);
270293
}
271294

272295
return <table className={className}>

extensions/ql-vscode/src/view/result-table-utils.tsx

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as React from 'react';
22
import { UrlValue, ResolvableLocationValue } from '../bqrs-cli-types';
3-
import { tryGetResolvableLocation } from '../bqrs-utils';
3+
import { isStringLoc, tryGetResolvableLocation } from '../bqrs-utils';
44
import { RawResultsSortState, QueryMetadata, SortDirection } from '../interface-types';
55
import { assertNever } from '../helpers-pure';
66
import { ResultSet } from '../interface-types';
@@ -60,28 +60,41 @@ export function jumpToLocation(loc: ResolvableLocationValue, databaseUri: string
6060
/**
6161
* Render a location as a link which when clicked displays the original location.
6262
*/
63-
export function renderLocation(loc: UrlValue | undefined, label: string | undefined,
64-
databaseUri: string, title?: string, callback?: () => void): JSX.Element {
63+
export function renderLocation(
64+
loc: UrlValue | undefined,
65+
label: string | undefined,
66+
databaseUri: string,
67+
title?: string,
68+
callback?: () => void
69+
): JSX.Element {
70+
71+
if (loc === undefined) {
72+
return <span />;
73+
} else if (isStringLoc(loc)) {
74+
return <a href={loc}>{loc}</a>;
75+
}
6576

6677
// If the label was empty, use a placeholder instead, so the link is still clickable.
6778
let displayLabel = label;
68-
if (label === undefined || label === '')
79+
if (!label) {
6980
displayLabel = '[empty string]';
70-
else if (label.match(/^\s+$/))
81+
} else if (label.match(/^\s+$/)) {
7182
displayLabel = `[whitespace: "${label}"]`;
83+
}
7284

73-
if (loc !== undefined) {
74-
const resolvableLoc = tryGetResolvableLocation(loc);
75-
if (resolvableLoc !== undefined) {
76-
return <a href="#"
85+
const resolvableLoc = tryGetResolvableLocation(loc);
86+
if (resolvableLoc !== undefined) {
87+
return (
88+
<a href="#"
7789
className="vscode-codeql__result-table-location-link"
7890
title={title}
79-
onClick={jumpToLocationHandler(resolvableLoc, databaseUri, callback)}>{displayLabel}</a>;
80-
} else {
81-
return <span title={title}>{displayLabel}</span>;
82-
}
91+
onClick={jumpToLocationHandler(resolvableLoc, databaseUri, callback)}>
92+
{displayLabel}
93+
</a>
94+
);
95+
} else {
96+
return <span title={title}>{displayLabel}</span>;
8397
}
84-
return <span />;
8598
}
8699

87100
/**

extensions/ql-vscode/src/view/results.tsx

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
QueryMetadata,
1212
ResultsPaths,
1313
ALERTS_TABLE_NAME,
14-
ParsedResultSets,
14+
ParsedResultSets
1515
} from '../interface-types';
1616
import { EventHandlers as EventHandlerList } from './event-handler-list';
1717
import { ResultTables } from './result-tables';
@@ -172,14 +172,17 @@ class App extends React.Component<{}, ResultsViewState> {
172172
});
173173
}
174174

175-
private async getResultSets(
175+
private getResultSets(
176176
resultsInfo: ResultsInfo
177-
): Promise<readonly ResultSet[]> {
177+
): readonly ResultSet[] {
178178
const parsedResultSets = resultsInfo.parsedResultSets;
179-
return [{
180-
...parsedResultSets.resultSet,
181-
t: (parsedResultSets.resultSet.t ?? 'RawResultSet') as any
182-
}];
179+
const resultSet = parsedResultSets.resultSet;
180+
if (!resultSet.t) {
181+
throw new Error(
182+
'Missing result set type. Should be either "SarifResultSet" or "RawResultSet".'
183+
);
184+
}
185+
return [resultSet];
183186
}
184187

185188
private async loadResults(): Promise<void> {
@@ -191,7 +194,7 @@ class App extends React.Component<{}, ResultsViewState> {
191194
let results: Results | null = null;
192195
let statusText = '';
193196
try {
194-
const resultSets = await this.getResultSets(resultsInfo);
197+
const resultSets = this.getResultSets(resultsInfo);
195198
results = {
196199
resultSets,
197200
database: resultsInfo.database,

extensions/ql-vscode/src/vscode-tests/no-workspace/interface-utils.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,23 @@ describe('interface-utils', () => {
7171

7272
describe('resolveWholeFileLocation', () => {
7373
it('should resolve a whole file location', () => {
74+
const mockDatabaseItem: DatabaseItem = ({
75+
resolveSourceFile: sinon.stub().returns(vscode.Uri.file('abc')),
76+
} as unknown) as DatabaseItem;
77+
expect(
78+
tryResolveLocation(
79+
'file://hucairz:0:0:0:0',
80+
mockDatabaseItem
81+
)
82+
).to.deep.equal(
83+
new vscode.Location(
84+
vscode.Uri.file('abc'),
85+
new vscode.Range(0, 0, 0, 0)
86+
)
87+
);
88+
});
89+
90+
it('should resolve a five-part location edge case', () => {
7491
const mockDatabaseItem: DatabaseItem = ({
7592
resolveSourceFile: sinon.stub().returns(vscode.Uri.file('abc')),
7693
} as unknown) as DatabaseItem;

0 commit comments

Comments
 (0)