Compare commits

...

40 Commits

Author SHA1 Message Date
Andrew Eisenberg
c6928d3159 Update changelog
Some checks failed
Build Extension / Build (ubuntu-latest) (push) Has been cancelled
Build Extension / Build (windows-latest) (push) Has been cancelled
Build Extension / Test (ubuntu-latest) (push) Has been cancelled
Build Extension / Test (windows-latest) (push) Has been cancelled
Release / Release (push) Has been cancelled
2020-05-13 10:25:06 -07:00
Andrew Eisenberg
fd26e02ed3 Update changelog 2020-05-13 08:10:38 -07:00
Andrew Eisenberg
de381804f6 Fix lint 2020-05-13 08:10:38 -07:00
Andrew Eisenberg
2f92477bd9 Move storagePath calculation to extension.ts 2020-05-13 08:10:38 -07:00
Andrew Eisenberg
926ab92dfe Add command to download, unzip, and open databases
New command that requests a URL and allows a user to install a
database from that url.

Closes #357
2020-05-13 08:10:38 -07:00
Andrew Eisenberg
36484fcea6 Formatting 2020-05-13 08:10:38 -07:00
Andrew Eisenberg
89e7b03d4a Add format and lint on commit 2020-05-12 10:15:48 -07:00
Andrew Eisenberg
c3e3390647 Extract BQRS locations from string results 2020-05-08 11:49:46 -07:00
Andrew Eisenberg
010ae64da3 Use inline-source-map
This gets a better debugging experience for webview.
2020-05-08 11:49:46 -07:00
Andrew Eisenberg
bd3702121f Never run format on save
This can lead to lots of non-semantic whitespace changes.
2020-05-08 11:49:46 -07:00
jcreedcmu
043d17d454 Merge pull request #356 from github/version/bump-to-v1.1.4
Bump version to v1.1.4
2020-05-08 12:49:35 -04:00
github-actions[bot]
1c7cad0151 Bump version to v1.1.4 2020-05-08 16:43:45 +00:00
jcreedcmu
e0383b3f9a Merge pull request #355 from jcreedcmu/jcreed/1.1.3
Some checks failed
Build Extension / Build (ubuntu-latest) (push) Has been cancelled
Build Extension / Build (windows-latest) (push) Has been cancelled
Build Extension / Test (ubuntu-latest) (push) Has been cancelled
Build Extension / Test (windows-latest) (push) Has been cancelled
Release / Release (push) Has been cancelled
Update CHANGELOG for release.
2020-05-08 12:41:15 -04:00
Jason Reed
0d972d7916 Update CHANGELOG for release. 2020-05-08 12:34:37 -04:00
jcreedcmu
ab020f24ae Merge pull request #354 from aeisenberg/aesienberg/database-commands
Rename database and open database directory
2020-05-08 12:30:57 -04:00
jcreedcmu
81cbf26910 Merge branch 'master' into aesienberg/database-commands 2020-05-08 12:08:59 -04:00
Andrew Eisenberg
2e2f101131 Update changelog 2020-05-07 21:58:16 -07:00
Andrew Eisenberg
610d40c99c Add a command to open a database directory externally 2020-05-07 15:51:00 -07:00
Andrew Eisenberg
adf6f66517 Add ability to rename database in database tree 2020-05-07 15:50:59 -07:00
Dave Bartolomeo
8f84989d98 Merge pull request #352 from jcreedcmu/jcreed/update-lsp
Update versions of json-rpc dependencies.
2020-05-07 12:02:30 -04:00
Jason Reed
22c9386123 Use ^versions not ~versions. 2020-05-07 11:32:17 -04:00
jcreedcmu
53e1794b50 Merge pull request #351 from jcreedcmu/jcreed/no-paginate
Don't paginate at all in experimental bqrs parsing codepath
2020-05-07 09:46:31 -04:00
Jason Reed
307d6d7c7f Update versions of json-rpc dependencies. 2020-05-07 09:45:23 -04:00
Jason Reed
a0e60fb154 Don't paginate at all in experimental bqrs parsing codepath 2020-05-06 12:07:47 -04:00
jcreedcmu
8b5bdbb6ef Merge pull request #350 from jcreedcmu/jcreed/cli-bqrs-parsing
Experimental: Enable parsing bqrs with the cli instead of in the webview
2020-05-06 11:34:35 -04:00
Jason Reed
0ad9cdd5ac Address review comments and fix formatting. 2020-05-06 10:39:27 -04:00
Jason Reed
c3b2e9d478 Add experimental use of cli bqrs parsing.
When `codeQL.experimentalBqrsParsing` is set to true, parse raw
results from the bqrs file using the cli, rather than doing it in the
webview.
2020-05-05 17:00:20 -04:00
Jason Reed
c20bbd9606 Fix formatting.
This is simultaneously compatible with eslint and tsfmt.
2020-05-05 16:21:58 -04:00
jcreedcmu
6080a0d585 Merge pull request #347 from jcreedcmu/jcreed/launch-config
internal: Revert specifying workspace in launch config
2020-05-05 13:08:28 -04:00
jcreedcmu
9fda320589 Merge pull request #340 from jcreedcmu/jcreed/no-qhelp-alias
Remove 'qhelp' as global alias for 'xml' filetype
2020-05-05 11:01:12 -04:00
Jason Reed
143b51ef82 Revert specifying workspace in launch config
The behavior without this line is to use whichever workspace was
opened last when testing. I find this more convenient, since I have
several (non-vscode-codeql-starter-workspace) local workspaces I use
for manual testing, and it's nice to have them persist from one run to
the next.
2020-05-05 10:58:54 -04:00
Alexander Eyers-Taylor
51d4c87af4 Merge pull request #346 from jcreedcmu/jcreed/fix-jump-to-def-bug
Jump-to-definition: Fix mistakenly always using the references query
2020-05-01 18:41:02 +01:00
Jason Reed
be5efc01ee Jump-to-definition: Fix mistakenly always using the references query 2020-05-01 13:17:17 -04:00
jcreedcmu
08a30c454a Merge pull request #345 from jcreedcmu/jcreed/better-empty-message
Add suggestive message to alerts view when 0 alerts, >0 raw results.
2020-05-01 09:01:25 -04:00
Jason Reed
1377969213 Add suggestive message to alerts view when 0 alerts, >0 raw results.
Fixes https://github.com/github/codeql-coreql-team/issues/383.
2020-05-01 08:17:51 -04:00
jcreedcmu
41f1aae71d Merge pull request #344 from github/shati/changelog-date
Changelog: Add release date
2020-04-28 13:49:41 -04:00
Shati Patel
62cae6ead1 Changelog: Add release date 2020-04-28 18:30:45 +01:00
jcreedcmu
39e3627e06 Merge pull request #343 from github/version/bump-to-v1.1.3
Bump version to v1.1.3
2020-04-28 11:40:48 -04:00
github-actions[bot]
43586c91d9 Bump version to v1.1.3 2020-04-28 15:34:08 +00:00
Jason Reed
31414b7506 Remove 'qhelp' as global alias for 'xml' filetype 2020-04-23 10:06:27 -04:00
28 changed files with 1070 additions and 161 deletions

3
.vscode/launch.json vendored
View File

@@ -8,8 +8,7 @@
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"--extensionDevelopmentPath=${workspaceRoot}/dist/vscode-codeql",
"${workspaceRoot}/../vscode-codeql-starter/vscode-codeql-starter.code-workspace"
"--extensionDevelopmentPath=${workspaceRoot}/dist/vscode-codeql"
],
"stopOnEntry": false,
"sourceMaps": true,

View File

@@ -32,5 +32,7 @@
"eslint.options": {
// This is necessary so that eslint can properly resolve its plugins
"resolvePluginsRelativeTo": "./extensions/ql-vscode"
}
},
// Force this to false since this will cause too many changes on each commit
"editor.formatOnSave": false
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,18 @@
# CodeQL for Visual Studio Code: Changelog
## 1.1.2
## 1.1.4 - 13 May 2020
- Add the ability to download and install databases archives from the internet.
## 1.1.3 - 8 May 2020
- Add a suggestion in alerts view to view raw results, when there are
raw results but no alerts.
- Add the ability to rename databases in the database view.
- Add the ability to open the directory in the filesystem
of a database.
## 1.1.2 - 28 April 2020
- Implement syntax highlighting for the new `unique` aggregate.
- Implement XML syntax highlighting for `.qhelp` files.
@@ -49,7 +61,7 @@
## 1.0.3 - 13 January 2020
- Reduce the frequency of CodeQL CLI update checks to help avoid hitting GitHub API limits of 60 requests per
hour for unauthenticated IPs.
hour for unauthenticated IPs.
- Fix sorting of result sets with names containing special characters.
## 1.0.2 - 13 December 2019
@@ -58,8 +70,7 @@ hour for unauthenticated IPs.
- Allow customization of query history labels from settings and from
query history view context menu.
- Show number of results in results view.
- Add commands `CodeQL: Show Next Step on Path` and `CodeQL: Show
Previous Step on Path` for navigating the steps on the currently
- Add commands `CodeQL: Show Next Step on Path` and `CodeQL: Show Previous Step on Path` for navigating the steps on the currently
shown path result.
## 1.0.1 - 21 November 2019

View File

@@ -10,7 +10,7 @@ export const config: webpack.Configuration = {
path: path.resolve(__dirname, '..', 'out'),
filename: "[name].js"
},
devtool: 'source-map',
devtool: "inline-source-map",
resolve: {
extensions: ['.js', '.ts', '.tsx', '.json']
},

View File

@@ -4,7 +4,7 @@
"description": "CodeQL for Visual Studio Code",
"author": "GitHub",
"private": true,
"version": "1.1.2",
"version": "1.1.4",
"publisher": "GitHub",
"license": "MIT",
"icon": "media/VS-marketplace-CodeQL-icon.png",
@@ -29,6 +29,7 @@
"onCommand:codeQL.checkForUpdatesToCLI",
"onCommand:codeQL.chooseDatabase",
"onCommand:codeQL.setCurrentDatabase",
"onCommand:codeQL.downloadDatabase",
"onCommand:codeQLDatabases.chooseDatabase",
"onCommand:codeQLDatabases.setCurrentDatabase",
"onCommand:codeQL.quickQuery",
@@ -80,9 +81,6 @@
},
{
"id": "xml",
"aliases": [
"qhelp"
],
"extensions": [
".qhelp"
]
@@ -206,6 +204,18 @@
"command": "codeQLDatabases.upgradeDatabase",
"title": "Upgrade Database"
},
{
"command": "codeQLDatabases.renameDatabase",
"title": "Rename Database"
},
{
"command": "codeQLDatabases.openDatabaseFolder",
"title": "Show Database Directory"
},
{
"command": "codeQL.downloadDatabase",
"title": "CodeQL: Download database"
},
{
"command": "codeQLDatabases.sortByName",
"title": "Sort by Name",
@@ -305,6 +315,16 @@
"group": "9_qlCommands",
"when": "view == codeQLDatabases"
},
{
"command": "codeQLDatabases.renameDatabase",
"group": "9_qlCommands",
"when": "view == codeQLDatabases"
},
{
"command": "codeQLDatabases.openDatabaseFolder",
"group": "9_qlCommands",
"when": "view == codeQLDatabases"
},
{
"command": "codeQLQueryHistory.openQuery",
"group": "9_qlCommands",
@@ -358,6 +378,10 @@
"command": "codeQL.runQuery",
"when": "resourceLangId == ql && resourceExtname == .ql"
},
{
"command": "codeQL.downloadDatabase",
"when": "true"
},
{
"command": "codeQL.quickEval",
"when": "editorLangId == ql"
@@ -370,6 +394,14 @@
"command": "codeQLDatabases.setCurrentDatabase",
"when": "false"
},
{
"command": "codeQLDatabases.renameDatabase",
"when": "false"
},
{
"command": "codeQLDatabases.openDatabaseFolder",
"when": "false"
},
{
"command": "codeQLDatabases.sortByName",
"when": "false"
@@ -450,7 +482,8 @@
"update-vscode": "node ./node_modules/vscode/bin/install",
"postinstall": "node ./node_modules/vscode/bin/install",
"format": "tsfmt -r",
"lint": "eslint src test --ext .ts,.tsx"
"lint": "eslint src test --ext .ts,.tsx",
"lint-staged": "lint-staged"
},
"dependencies": {
"child-process-promise": "^2.2.1",
@@ -467,8 +500,8 @@
"tmp": "^0.1.0",
"tree-kill": "~1.2.2",
"unzipper": "~0.10.5",
"vscode-jsonrpc": "^4.0.0",
"vscode-languageclient": "^5.2.1",
"vscode-jsonrpc": "^5.0.1",
"vscode-languageclient": "^6.1.3",
"vscode-test-adapter-api": "~1.7.0",
"vscode-test-adapter-util": "~0.7.0",
"minimist": "~1.2.5"
@@ -527,6 +560,23 @@
"@types/sinon-chai": "~3.2.3",
"proxyquire": "~2.1.3",
"@types/proxyquire": "~1.3.28",
"eslint-plugin-react": "~7.19.0"
"eslint-plugin-react": "~7.19.0",
"husky": "~4.2.5",
"lint-staged": "~10.2.2",
"prettier": "~2.0.5"
},
"husky": {
"hooks": {
"pre-commit": "npm run lint-staged"
}
},
"lint-staged": {
"./**/*.{json,css,scss,md}": [
"prettier --write"
],
"./**/*.{ts,tsx}": [
"eslint --fix --debug",
"tsfmt -r"
]
}
}

View File

@@ -0,0 +1,103 @@
import { DecodedBqrsChunk, ResultSetSchema, ColumnKind, Column, ColumnValue } from "./bqrs-cli-types";
import { LocationValue, ResultSetSchema as AdaptedSchema, ColumnSchema, ColumnType, LocationStyle } from 'semmle-bqrs';
// FIXME: This is a temporary bit of impedance matching to convert
// from the types provided by ./bqrs-cli-types, to the types used by
// the view layer.
//
// The reason that it is benign for now is that it is only used by
// feature-flag-guarded codepaths that won't be encountered by normal
// users. It is not yet guaranteed to produce correct output for raw
// results.
//
// Eventually, the view layer should be refactored to directly accept data
// of types coming from bqrs-cli-types, and this file can be deleted.
export type ResultRow = ResultValue[];
export interface ResultElement {
label: string;
location?: LocationValue;
}
export interface ResultUri {
uri: string;
}
export type ResultValue = ResultElement | ResultUri | string;
export interface RawResultSet {
readonly schema: AdaptedSchema;
readonly rows: readonly ResultRow[];
}
function adaptKind(kind: ColumnKind): ColumnType {
// XXX what about 'u'?
if (kind === 'e') {
return { type: 'e', primitiveType: 's', locationStyle: LocationStyle.FivePart, hasLabel: true }
}
else {
return { type: kind };
}
}
function adaptColumn(col: Column): ColumnSchema {
return { name: col.name!, type: adaptKind(col.kind) };
}
export function adaptSchema(schema: ResultSetSchema): AdaptedSchema {
return {
columns: schema.columns.map(adaptColumn),
name: schema.name,
tupleCount: schema.rows,
version: 0,
}
}
export function adaptValue(val: ColumnValue): ResultValue {
// XXX taking a lot of incorrect shortcuts here
if (typeof val === 'string') {
return val;
}
if (typeof val === 'number' || typeof val === 'boolean') {
return val + '';
}
const url = val.url;
if (typeof url === 'string') {
return url;
}
if (url === undefined) {
return 'none';
}
return {
label: val.label || '',
location: {
t: LocationStyle.FivePart,
lineStart: url.startLine,
lineEnd: url.endLine,
colStart: url.startColumn,
colEnd: url.endColumn,
// FIXME: This seems definitely wrong. Should we be using
// something like the code in sarif-utils.ts?
file: url.uri.replace(/file:/, ''),
}
}
}
export function adaptRow(row: ColumnValue[]): ResultRow {
return row.map(adaptValue);
}
export function adaptBqrs(schema: AdaptedSchema, page: DecodedBqrsChunk): RawResultSet {
return {
schema,
rows: page.tuples.map(adaptRow),
}
}

View File

@@ -50,6 +50,9 @@ const ROOT_SETTING = new Setting('codeQL');
*/
export const EXPERIMENTAL_FEATURES_SETTING = new Setting('experimentalFeatures', ROOT_SETTING);
/* Advanced setting: used to enable bqrs parsing in the cli instead of in the webview. */
export const EXPERIMENTAL_BQRS_SETTING = new Setting('experimentalBqrsParsing', ROOT_SETTING);
// Distribution configuration
const DISTRIBUTION_SETTING = new Setting('cli', ROOT_SETTING);

View File

@@ -0,0 +1,111 @@
import * as fetch from "node-fetch";
import * as unzipper from "unzipper";
import { Uri, ProgressOptions, ProgressLocation, commands, window } from "vscode";
import * as fs from "fs-extra";
import * as path from "path";
import { DatabaseManager } from "./databases";
import { ProgressCallback, showAndLogErrorMessage, withProgress } from "./helpers";
export default async function promptFetchDatabase(dbm: DatabaseManager, storagePath: string) {
try {
const databaseUrl = await window.showInputBox({
prompt: 'Enter URL of zipfile of database to download'
});
if (databaseUrl) {
validateUrl(databaseUrl);
const progressOptions: ProgressOptions = {
location: ProgressLocation.Notification,
title: 'Adding database from URL',
cancellable: false,
};
await withProgress(progressOptions, async progress => await databaseFetcher(databaseUrl, dbm, storagePath, progress));
commands.executeCommand('codeQLDatabases.focus');
}
} catch (e) {
showAndLogErrorMessage(e.message);
}
}
async function databaseFetcher(
databaseUrl: string,
databasesManager: DatabaseManager,
storagePath: string,
progressCallback: ProgressCallback
): Promise<void> {
progressCallback({
maxStep: 3,
message: 'Downloading database',
step: 1
});
if (!storagePath) {
throw new Error("No storage path specified.");
}
const unzipPath = await getStorageFolder(storagePath, databaseUrl);
const response = await fetch.default(databaseUrl);
const unzipStream = unzipper.Extract({
path: unzipPath
});
progressCallback({
maxStep: 3,
message: 'Unzipping database',
step: 2
});
await new Promise((resolve, reject) => {
response.body.on('error', reject);
unzipStream.on('error', reject);
unzipStream.on('close', resolve);
response.body.pipe(unzipStream);
});
progressCallback({
maxStep: 3,
message: 'Opening database',
step: 3
});
// if there is a single directory inside, then assume that's what we want to import
const dirs = await fs.readdir(unzipPath);
const dbPath = dirs?.length === 1 && (await fs.stat(path.join(unzipPath, dirs[0]))).isDirectory
? path.join(unzipPath, dirs[0])
: unzipPath;
// might need to upgrade before importing...
const item = await databasesManager.openDatabase(Uri.parse(dbPath));
databasesManager.setCurrentDatabaseItem(item);
}
async function getStorageFolder(storagePath: string, urlStr: string) {
const url = Uri.parse(urlStr);
let lastName = path.basename(url.path).substring(0, 255);
if (lastName.endsWith(".zip")) {
lastName = lastName.substring(0, lastName.length - 4);
}
const realpath = await fs.realpath(storagePath);
let folderName = path.join(realpath, lastName);
let counter = 0;
while (await fs.pathExists(folderName)) {
counter++;
folderName = path.join(realpath, `${lastName}-${counter}`);
if (counter > 100) {
throw new Error("Could not find a unique name for downloaded database.");
}
}
return folderName;
}
function validateUrl(databaseUrl: string) {
let uri;
try {
uri = Uri.parse(databaseUrl, true);
} catch (e) {
throw new Error(`Invalid url: ${databaseUrl}`);
}
if (uri.scheme !== 'https') {
throw new Error('Must use https for downloading a database.');
}
}

View File

@@ -1,9 +1,9 @@
import * as path from 'path';
import { DisposableObject } from 'semmle-vscode-utils';
import { commands, Event, EventEmitter, ExtensionContext, ProviderResult, TreeDataProvider, TreeItem, Uri, window } from 'vscode';
import { commands, Event, EventEmitter, ExtensionContext, ProviderResult, TreeDataProvider, TreeItem, Uri, window, env } from 'vscode';
import * as cli from './cli';
import { DatabaseItem, DatabaseManager, getUpgradesDirectories } from './databases';
import { getOnDiskWorkspaceFolders } from './helpers';
import { getOnDiskWorkspaceFolders, showAndLogErrorMessage } from './helpers';
import { logger } from './logging';
import { clearCacheInDatabase, UserCancellationException } from './run-queries';
import * as qsClient from './queryserver-client';
@@ -94,7 +94,7 @@ class DatabaseTreeDataProvider extends DisposableObject
public getChildren(element?: DatabaseItem): ProviderResult<DatabaseItem[]> {
if (element === undefined) {
return this.databaseManager.databaseItems.slice(0).sort((db1, db2) => {
switch(this.sortOrder) {
switch (this.sortOrder) {
case SortOrder.NameAsc:
return db1.name.localeCompare(db2.name);
case SortOrder.NameDesc:
@@ -180,6 +180,8 @@ export class DatabaseUI extends DisposableObject {
ctx.subscriptions.push(commands.registerCommand('codeQLDatabases.sortByDateAdded', this.handleSortByDateAdded));
ctx.subscriptions.push(commands.registerCommand('codeQLDatabases.removeDatabase', this.handleRemoveDatabase));
ctx.subscriptions.push(commands.registerCommand('codeQLDatabases.upgradeDatabase', this.handleUpgradeDatabase));
ctx.subscriptions.push(commands.registerCommand('codeQLDatabases.renameDatabase', this.handleRenameDatabase));
ctx.subscriptions.push(commands.registerCommand('codeQLDatabases.openDatabaseFolder', this.handleOpenFolder));
}
private handleMakeCurrentDatabase = async (databaseItem: DatabaseItem): Promise<void> => {
@@ -272,6 +274,29 @@ export class DatabaseUI extends DisposableObject {
this.databaseManager.removeDatabaseItem(databaseItem);
}
private handleRenameDatabase = async (databaseItem: DatabaseItem): Promise<void> => {
try {
const newName = await window.showInputBox({
prompt: 'Choose new database name',
value: databaseItem.name
});
if (newName) {
this.databaseManager.renameDatabaseItem(databaseItem, newName);
}
} catch (e) {
showAndLogErrorMessage(e.message);
}
}
private handleOpenFolder = async (databaseItem: DatabaseItem): Promise<void> => {
try {
await env.openExternal(databaseItem.databaseUri);
} catch (e) {
showAndLogErrorMessage(e.message);
}
}
/**
* Return the current database directory. If we don't already have a
* current database, ask the user for one, and return that, or

View File

@@ -109,8 +109,9 @@ async function findDataset(parentDirectory: string): Promise<vscode.Uri> {
return vscode.Uri.file(dbAbsolutePath);
}
async function findSourceArchive(databasePath: string, silent = false):
Promise<vscode.Uri | undefined> {
async function findSourceArchive(
databasePath: string, silent = false
): Promise<vscode.Uri | undefined> {
const relativePaths = ['src', 'output/src_archive']
@@ -203,7 +204,7 @@ export interface DatabaseItem {
/** The URI of the database */
readonly databaseUri: vscode.Uri;
/** The name of the database to be displayed in the UI */
readonly name: string;
name: string;
/** The URI of the database's source archive, or `undefined` if no source archive is to be used. */
readonly sourceArchive: vscode.Uri | undefined;
/**
@@ -287,6 +288,10 @@ class DatabaseItemImpl implements DatabaseItem {
}
}
public set name(newName: string) {
this.options.displayName = newName;
}
public get sourceArchive(): vscode.Uri | undefined {
if (this.options.ignoreSourceArchive || (this._contents === undefined)) {
return undefined;
@@ -459,12 +464,11 @@ function eventFired<T>(event: vscode.Event<T>, timeoutMs = 1000): Promise<T | un
}
export class DatabaseManager extends DisposableObject {
private readonly _onDidChangeDatabaseItem =
this.push(new vscode.EventEmitter<DatabaseItem | undefined>());
private readonly _onDidChangeDatabaseItem = this.push(new vscode.EventEmitter<DatabaseItem | undefined>());
readonly onDidChangeDatabaseItem = this._onDidChangeDatabaseItem.event;
private readonly _onDidChangeCurrentDatabaseItem =
this.push(new vscode.EventEmitter<DatabaseItem | undefined>());
private readonly _onDidChangeCurrentDatabaseItem = this.push(new vscode.EventEmitter<DatabaseItem | undefined>());
readonly onDidChangeCurrentDatabaseItem = this._onDidChangeCurrentDatabaseItem.event;
private readonly _databaseItems: DatabaseItemImpl[] = [];
@@ -642,6 +646,12 @@ export class DatabaseManager extends DisposableObject {
this._onDidChangeDatabaseItem.fire(undefined);
}
public async renameDatabaseItem(item: DatabaseItem, newName: string) {
item.name = newName;
this.updatePersistedDatabaseList();
this._onDidChangeDatabaseItem.fire(item);
}
public removeDatabaseItem(item: DatabaseItem) {
if (this._currentDatabaseItem == item)
this._currentDatabaseItem = undefined;
@@ -658,6 +668,14 @@ export class DatabaseManager extends DisposableObject {
vscode.workspace.updateWorkspaceFolders(folderIndex, 1);
}
// Delete folder from file system only if it is controlled by the extension
if (this.isExtensionControlledLocation(item.databaseUri)) {
logger.log(`Deleting database from filesystem.`);
fs.remove(item.databaseUri.path).then(
() => logger.log(`Deleted '${item.databaseUri.path}'`),
e => logger.log(`Failed to delete '${item.databaseUri.path}'. Reason: ${e.message}`));
}
this._onDidChangeDatabaseItem.fire(undefined);
}
@@ -669,6 +687,11 @@ export class DatabaseManager extends DisposableObject {
private updatePersistedDatabaseList(): void {
this.ctx.workspaceState.update(DB_LIST, this._databaseItems.map(item => item.getPersistedState()));
}
private isExtensionControlledLocation(uri: vscode.Uri) {
const storagePath = this.ctx.storagePath || this.ctx.globalStoragePath;
return uri.path.startsWith(storagePath);
}
}
/**

View File

@@ -70,7 +70,7 @@ export class TemplateQueryDefinitionProvider implements vscode.DefinitionProvide
}
async getDefinitions(uriString: string): Promise<vscode.LocationLink[]> {
return getLinksForUriString(this.cli, this.qs, this.dbm, uriString, (src, _dest) => src === uriString);
return getLinksForUriString(this.cli, this.qs, this.dbm, uriString, KeyType.DefinitionQuery, (src, _dest) => src === uriString);
}
async provideDefinition(document: vscode.TextDocument, position: vscode.Position, _token: vscode.CancellationToken): Promise<vscode.LocationLink[]> {
@@ -97,7 +97,7 @@ export class TemplateQueryReferenceProvider implements vscode.ReferenceProvider
}
async getReferences(uriString: string): Promise<FullLocationLink[]> {
return getLinksForUriString(this.cli, this.qs, this.dbm, uriString, (_src, dest) => dest === uriString);
return getLinksForUriString(this.cli, this.qs, this.dbm, uriString, KeyType.ReferenceQuery, (_src, dest) => dest === uriString);
}
async provideReferences(document: vscode.TextDocument, position: vscode.Position, _context: vscode.ReferenceContext, _token: vscode.CancellationToken): Promise<vscode.Location[]> {
@@ -146,6 +146,7 @@ async function getLinksForUriString(
qs: QueryServerClient,
dbm: DatabaseManager,
uriString: string,
keyType: KeyType,
filter: (src: string, dest: string) => boolean
) {
const uri = decodeSourceArchiveUri(vscode.Uri.parse(uriString));
@@ -158,7 +159,7 @@ async function getLinksForUriString(
throw new Error("Can't infer qlpack from database source archive");
}
const links: FullLocationLink[] = []
for (const query of await resolveQueries(cli, qlpack, KeyType.ReferenceQuery)) {
for (const query of await resolveQueries(cli, qlpack, keyType)) {
const templates: messages.TemplateDefinitions = {
[TEMPLATE_NAME]: {
values: {

View File

@@ -6,7 +6,7 @@ import * as unzipper from "unzipper";
import * as url from "url";
import { ExtensionContext, Event } from "vscode";
import { DistributionConfig } from "./config";
import { InvocationRateLimiter, InvocationRateLimiterResultKind, ProgressUpdate, showAndLogErrorMessage } from "./helpers";
import { InvocationRateLimiter, InvocationRateLimiterResultKind, showAndLogErrorMessage } from "./helpers";
import { logger } from "./logging";
import * as helpers from "./helpers";
import { getCodeQlCliVersion, tryParseVersionString, Version } from "./cli-version";
@@ -171,7 +171,7 @@ export class DistributionManager implements DistributionProvider {
* Returns a failed promise if an unexpected error occurs during installation.
*/
public installExtensionManagedDistributionRelease(release: Release,
progressCallback?: (p: ProgressUpdate) => void): Promise<void> {
progressCallback?: helpers.ProgressCallback): Promise<void> {
return this._extensionSpecificDistributionManager.installDistributionRelease(release, progressCallback);
}
@@ -253,14 +253,14 @@ class ExtensionSpecificDistributionManager {
* Returns a failed promise if an unexpected error occurs during installation.
*/
public async installDistributionRelease(release: Release,
progressCallback?: (p: ProgressUpdate) => void): Promise<void> {
progressCallback?: helpers.ProgressCallback): Promise<void> {
await this.downloadDistribution(release, progressCallback);
// Store the installed release within the global extension state.
this.storeInstalledRelease(release);
}
private async downloadDistribution(release: Release,
progressCallback?: (p: ProgressUpdate) => void): Promise<void> {
progressCallback?: helpers.ProgressCallback): Promise<void> {
try {
await this.removeDistribution();
} catch (e) {

View File

@@ -20,6 +20,7 @@ import { displayQuickQuery } from './quick-query';
import { compileAndRunQueryAgainstDatabase, tmpDirDisposal, UserCancellationException } from './run-queries';
import { QLTestAdapterFactory } from './test-adapter';
import { TestUIService } from './test-ui';
import promptFetchDatabase from './databaseFetcher';
/**
* extension.ts
@@ -60,8 +61,9 @@ function registerErrorStubs(excludedCommands: string[], stubGenerator: (command:
const extensionId = 'GitHub.vscode-codeql'; // TODO: Is there a better way of obtaining this?
const extension = extensions.getExtension(extensionId);
if (extension === undefined)
if (extension === undefined) {
throw new Error(`Can't find extension ${extensionId}`);
}
const stubbedCommands: string[]
= extension.packageJSON.contributes.commands.map((entry: { command: string }) => entry.command);
@@ -333,6 +335,7 @@ async function activateWithInstalledDistribution(ctx: ExtensionContext, distribu
await qs.restartQueryServer();
helpers.showAndLogInformationMessage('CodeQL Query Server restarted.', { outputLogger: queryServerLogger });
}));
ctx.subscriptions.push(commands.registerCommand('codeQL.downloadDatabase', () => promptFetchDatabase(dbm, getContextStoragePath(ctx))));
ctx.subscriptions.push(client.start());
@@ -348,10 +351,15 @@ async function activateWithInstalledDistribution(ctx: ExtensionContext, distribu
}
}
function getContextStoragePath(ctx: ExtensionContext) {
return ctx.storagePath || ctx.globalStoragePath;
}
function initializeLogging(ctx: ExtensionContext): void {
logger.init(ctx);
queryServerLogger.init(ctx);
ideServerLogger.init(ctx);
const storagePath = getContextStoragePath(ctx);
logger.init(storagePath);
queryServerLogger.init(storagePath);
ideServerLogger.init(storagePath);
ctx.subscriptions.push(logger);
ctx.subscriptions.push(queryServerLogger);
ctx.subscriptions.push(ideServerLogger);

View File

@@ -22,6 +22,8 @@ export interface ProgressUpdate {
message: string;
}
export type ProgressCallback = (p: ProgressUpdate) => void;
/**
* This mediates between the kind of progress callbacks we want to
* write (where we *set* current progress position and give

View File

@@ -1,5 +1,6 @@
import * as sarif from 'sarif';
import { ResolvableLocationValue } from 'semmle-bqrs';
import { RawResultSet } from './adapt';
/**
* Only ever show this many results per run in interpreted results.
@@ -77,6 +78,12 @@ export interface SetStateMsg {
* This is useful to prevent properties like scroll state being lost when rendering the sorted results after sorting a column.
*/
shouldKeepOldResultsWhileRendering: boolean;
/**
* An experimental way of providing results from the extension.
* Should be undefined unless config.EXPERIMENTAL_BQRS_SETTING is set to true.
*/
resultSets?: RawResultSet[];
}
/** Advance to the next or previous path no in the path viewer */

View File

@@ -16,6 +16,8 @@ import * as messages from './messages';
import { CompletedQuery, interpretResults } from './query-results';
import { QueryInfo, tmpDir } from './run-queries';
import { parseSarifLocation, parseSarifPlainTextMessage } from './sarif-utils';
import { adaptSchema, adaptBqrs, RawResultSet } from './adapt';
import { EXPERIMENTAL_BQRS_SETTING } from './config';
/**
* interface.ts
@@ -349,9 +351,7 @@ export class InterfaceManager extends DisposableObject {
const showButton = "View Results";
const queryName = results.queryName;
const resultPromise = vscode.window.showInformationMessage(
`Finished running query ${
queryName.length > 0 ? `${queryName}` : ""
}.`,
`Finished running query ${queryName.length > 0 ? ` "${queryName}"` : ""}.`,
showButton
);
// Address this click asynchronously so we still update the
@@ -363,6 +363,19 @@ export class InterfaceManager extends DisposableObject {
});
}
let resultSets: RawResultSet[] | undefined;
if (EXPERIMENTAL_BQRS_SETTING.getValue()) {
resultSets = [];
const schemas = await this.cliServer.bqrsInfo(results.query.resultsPaths.resultsPath);
for (const schema of schemas["result-sets"]) {
const chunk = await this.cliServer.bqrsDecode(results.query.resultsPaths.resultsPath, schema.name);
const adaptedSchema = adaptSchema(schema);
const resultSet = adaptBqrs(adaptedSchema, chunk);
resultSets.push(resultSet);
}
}
await this.postMessage({
t: "setState",
interpretation,
@@ -370,6 +383,7 @@ export class InterfaceManager extends DisposableObject {
resultsPath: this.convertPathToWebviewUri(
results.query.resultsPaths.resultsPath
),
resultSets,
sortedResultsMap,
database: results.database,
shouldKeepOldResultsWhileRendering,

View File

@@ -1,4 +1,4 @@
import { window as Window, OutputChannel, Progress, ExtensionContext, Disposable } from 'vscode';
import { window as Window, OutputChannel, Progress, Disposable } from 'vscode';
import { DisposableObject } from 'semmle-vscode-utils';
import * as fs from 'fs-extra';
import * as path from 'path';
@@ -47,8 +47,8 @@ export class OutputChannelLogger extends DisposableObject implements Logger {
this.push(this.outputChannel);
}
init(ctx: ExtensionContext): void {
this.additionalLogLocationPath = path.join(ctx.storagePath || ctx.globalStoragePath, this.title);
init(storagePath: string): void {
this.additionalLogLocationPath = path.join(storagePath, this.title);
// clear out any old state from previous runs
fs.remove(this.additionalLogLocationPath);

View File

@@ -87,8 +87,7 @@ export class QLTestAdapter extends DisposableObject implements TestAdapter {
private readonly _tests = this.push(
new EventEmitter<TestLoadStartedEvent | TestLoadFinishedEvent>());
private readonly _testStates = this.push(
new EventEmitter<TestRunStartedEvent | TestRunFinishedEvent | TestSuiteEvent |
TestEvent>());
new EventEmitter<TestRunStartedEvent | TestRunFinishedEvent | TestSuiteEvent | TestEvent>());
private readonly _autorun = this.push(new EventEmitter<void>());
private runningTask?: vscode.CancellationTokenSource = undefined;
@@ -108,9 +107,7 @@ export class QLTestAdapter extends DisposableObject implements TestAdapter {
return this._tests.event;
}
public get testStates(): Event<TestRunStartedEvent | TestRunFinishedEvent | TestSuiteEvent |
TestEvent> {
public get testStates(): Event<TestRunStartedEvent | TestRunFinishedEvent | TestSuiteEvent | TestEvent> {
return this._testStates.event;
}
@@ -118,9 +115,7 @@ export class QLTestAdapter extends DisposableObject implements TestAdapter {
return this._autorun.event;
}
private static createTestOrSuiteInfos(testNodes: readonly QLTestNode[]):
(TestSuiteInfo | TestInfo)[] {
private static createTestOrSuiteInfos(testNodes: readonly QLTestNode[]): (TestSuiteInfo | TestInfo)[] {
return testNodes.map((childNode) => {
return QLTestAdapter.createTestOrSuiteInfo(childNode);
});
@@ -129,11 +124,9 @@ export class QLTestAdapter extends DisposableObject implements TestAdapter {
private static createTestOrSuiteInfo(testNode: QLTestNode): TestSuiteInfo | TestInfo {
if (testNode instanceof QLTestFile) {
return QLTestAdapter.createTestInfo(testNode);
}
else if (testNode instanceof QLTestDirectory) {
} else if (testNode instanceof QLTestDirectory) {
return QLTestAdapter.createTestSuiteInfo(testNode, testNode.name);
}
else {
} else {
throw new Error('Unexpected test type.');
}
}
@@ -148,9 +141,7 @@ export class QLTestAdapter extends DisposableObject implements TestAdapter {
};
}
private static createTestSuiteInfo(testDirectory: QLTestDirectory, label: string):
TestSuiteInfo {
private static createTestSuiteInfo(testDirectory: QLTestDirectory, label: string): TestSuiteInfo {
return {
type: 'suite',
id: testDirectory.path,

View File

@@ -69,6 +69,14 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
});
}
renderNoResults(): JSX.Element {
if (this.props.nonemptyRawResults) {
return <span>No Alerts. See <a href='#' onClick={this.props.showRawResults}>raw results</a>.</span>;
} else {
return <span>No Alerts</span>;
}
}
render(): JSX.Element {
const { databaseUri, resultSet } = this.props;
@@ -156,13 +164,14 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
return (e) => this.toggle(e, indices);
};
const noResults = <span>No Results</span>; // TODO: Maybe make this look nicer
if (resultSet.sarif.runs.length === 0 ||
resultSet.sarif.runs[0].results === undefined ||
resultSet.sarif.runs[0].results.length === 0) {
return this.renderNoResults();
}
let expansionIndex = 0;
if (resultSet.sarif.runs.length === 0) return noResults;
if (resultSet.sarif.runs[0].results === undefined) return noResults;
resultSet.sarif.runs[0].results.forEach((result, resultIndex) => {
const text = result.message.text || '[no text]';
const msg: JSX.Element[] =

View File

@@ -1,6 +1,7 @@
import * as React from "react";
import { renderLocation, ResultTableProps, zebraStripe, className, nextSortDirection } from "./result-table-utils";
import { RawTableResultSet, ResultValue, vscode } from "./results";
import { RawTableResultSet, vscode } from "./results";
import { ResultValue } from "../adapt";
import { SortDirection, RAW_RESULTS_LIMIT, RawResultsSortState } from "../interface-types";
export type RawTableProps = ResultTableProps & {

View File

@@ -10,6 +10,18 @@ export interface ResultTableProps {
metadata?: QueryMetadata;
resultsPath: string | undefined;
sortState?: RawResultsSortState;
/**
* Holds if there are any raw results. When that is the case, we
* want to direct users to pay attention to raw results if
* interpreted results are empty.
*/
nonemptyRawResults: boolean;
/**
* Callback to show raw results.
*/
showRawResults: () => void;
}
export const className = 'vscode-codeql__result-table';

View File

@@ -123,6 +123,7 @@ export class ResultTables
const resultSets = this.getResultSets();
const resultSet = resultSets.find(resultSet => resultSet.schema.name == selectedTable);
const nonemptyRawResults = resultSets.some(resultSet => resultSet.t == 'RawResultSet' && resultSet.rows.length > 0);
const numberOfResults = resultSet && renderResultCountString(resultSet);
return <div>
@@ -149,7 +150,9 @@ export class ResultTables
<ResultTable key={resultSet.schema.name} resultSet={resultSet}
databaseUri={this.props.database.databaseUri}
resultsPath={this.props.resultsPath}
sortState={this.props.sortStates.get(resultSet.schema.name)} />
sortState={this.props.sortStates.get(resultSet.schema.name)}
nonemptyRawResults={nonemptyRawResults}
showRawResults={() => { this.setState({ selectedTable: SELECT_TABLE_NAME }) }} />
}
</div>;
}

View File

@@ -1,11 +1,12 @@
import * as React from 'react';
import * as Rdom from 'react-dom';
import * as bqrs from 'semmle-bqrs';
import { ElementBase, LocationValue, PrimitiveColumnValue, PrimitiveTypeKind, ResultSetSchema, tryGetResolvableLocation } from 'semmle-bqrs';
import { ElementBase, PrimitiveColumnValue, PrimitiveTypeKind, ResultSetSchema, tryGetResolvableLocation } from 'semmle-bqrs';
import { assertNever } from '../helpers-pure';
import { DatabaseInfo, FromResultsViewMsg, Interpretation, IntoResultsViewMsg, SortedResultSetInfo, RawResultsSortState, NavigatePathMsg, QueryMetadata, ResultsPaths } from '../interface-types';
import { EventHandlers as EventHandlerList } from './event-handler-list';
import { ResultTables } from './result-tables';
import { RawResultSet, ResultValue, ResultRow } from '../adapt';
/**
* results.tsx
@@ -23,19 +24,6 @@ interface VsCodeApi {
declare const acquireVsCodeApi: () => VsCodeApi;
export const vscode = acquireVsCodeApi();
export interface ResultElement {
label: string;
location?: LocationValue;
}
export interface ResultUri {
uri: string;
}
export type ResultValue = ResultElement | ResultUri | string;
export type ResultRow = ResultValue[];
export type RawTableResultSet = { t: 'RawResultSet' } & RawResultSet;
export type PathTableResultSet = { t: 'SarifResultSet'; readonly schema: ResultSetSchema; name: string } & Interpretation;
@@ -43,11 +31,6 @@ export type ResultSet =
| RawTableResultSet
| PathTableResultSet;
export interface RawResultSet {
readonly schema: ResultSetSchema;
readonly rows: readonly ResultRow[];
}
async function* getChunkIterator(response: Response): AsyncIterableIterator<Uint8Array> {
if (!response.ok) {
throw new Error(`Failed to load results: (${response.status}) ${response.statusText}`);
@@ -62,9 +45,7 @@ async function* getChunkIterator(response: Response): AsyncIterableIterator<Uint
}
}
function translatePrimitiveValue(value: PrimitiveColumnValue, type: PrimitiveTypeKind):
ResultValue {
function translatePrimitiveValue(value: PrimitiveColumnValue, type: PrimitiveTypeKind): ResultValue {
switch (type) {
case 'i':
case 'f':
@@ -127,6 +108,7 @@ async function parseResultSets(response: Response): Promise<readonly ResultSet[]
interface ResultsInfo {
resultsPath: string;
resultSets: ResultSet[] | undefined;
origResultsPaths: ResultsPaths;
database: DatabaseInfo;
interpretation: Interpretation | undefined;
@@ -187,6 +169,7 @@ class App extends React.Component<{}, ResultsViewState> {
case 'setState':
this.updateStateWithNewResultsInfo({
resultsPath: msg.resultsPath,
resultSets: msg.resultSets?.map(x => ({ t: 'RawResultSet', ...x })),
origResultsPaths: msg.origResultsPaths,
sortedResultsMap: new Map(Object.entries(msg.sortedResultsMap)),
database: msg.database,
@@ -247,8 +230,9 @@ class App extends React.Component<{}, ResultsViewState> {
let results: Results | null = null;
let statusText = '';
try {
const resultSets = resultsInfo.resultSets || await this.getResultSets(resultsInfo);
results = {
resultSets: await this.getResultSets(resultsInfo),
resultSets,
database: resultsInfo.database,
sortStates: this.getSortStates(resultsInfo)
};

View File

@@ -1,6 +1,6 @@
import { expect } from 'chai';
import 'mocha';
import { LocationStyle, StringLocation, tryGetWholeFileLocation } from 'semmle-bqrs';
import { LocationStyle, StringLocation, tryGetResolvableLocation } from 'semmle-bqrs';
describe('processing string locations', function () {
it('should detect Windows whole-file locations', function () {
@@ -8,7 +8,7 @@ describe('processing string locations', function () {
t: LocationStyle.String,
loc: 'file://C:/path/to/file.ext:0:0:0:0'
};
const wholeFileLoc = tryGetWholeFileLocation(loc);
const wholeFileLoc = tryGetResolvableLocation(loc);
expect(wholeFileLoc).to.eql({t: LocationStyle.WholeFile, file: 'C:/path/to/file.ext'});
});
it('should detect Unix whole-file locations', function () {
@@ -16,12 +16,27 @@ describe('processing string locations', function () {
t: LocationStyle.String,
loc: 'file:///path/to/file.ext:0:0:0:0'
};
const wholeFileLoc = tryGetWholeFileLocation(loc);
const wholeFileLoc = tryGetResolvableLocation(loc);
expect(wholeFileLoc).to.eql({t: LocationStyle.WholeFile, file: '/path/to/file.ext'});
});
it('should detect Unix 5-part locations', function () {
const loc: StringLocation = {
t: LocationStyle.String,
loc: 'file:///path/to/file.ext:1:2:3:4'
};
const wholeFileLoc = tryGetResolvableLocation(loc);
expect(wholeFileLoc).to.eql({
t: LocationStyle.FivePart,
file: '/path/to/file.ext',
lineStart: 1,
colStart: 2,
lineEnd: 3,
colEnd: 4
});
});
it('should ignore other string locations', function () {
for (const loc of ['file:///path/to/file.ext', 'I am not a location']) {
const wholeFileLoc = tryGetWholeFileLocation({
const wholeFileLoc = tryGetResolvableLocation({
t: LocationStyle.String,
loc: loc
});

View File

@@ -48,18 +48,7 @@ describe('OutputChannelLogger tests', () => {
});
it('should create a side log in the workspace area', async () => {
await sideLogTest('storagePath', 'globalStoragePath');
});
it('should create a side log in the global area', async () => {
await sideLogTest('globalStoragePath', 'storagePath');
});
async function sideLogTest(expectedArea: string, otherArea: string): Promise<void> {
logger.init({
[expectedArea]: tempFolders[expectedArea].name,
[otherArea]: undefined
});
logger.init(tempFolders.storagePath.name);
await logger.log('xxx', { additionalLogLocation: 'first' });
await logger.log('yyy', { additionalLogLocation: 'second' });
@@ -67,19 +56,16 @@ describe('OutputChannelLogger tests', () => {
await logger.log('aaa');
// expect 2 side logs
const testLoggerFolder = path.join(tempFolders[expectedArea].name, 'test-logger');
const testLoggerFolder = path.join(tempFolders.storagePath.name, 'test-logger');
expect(fs.readdirSync(testLoggerFolder).length).to.equal(2);
expect(fs.readdirSync(tempFolders[otherArea].name).length).to.equal(0);
// contents
expect(fs.readFileSync(path.join(testLoggerFolder, 'first'), 'utf8')).to.equal('xxx\nzzz');
expect(fs.readFileSync(path.join(testLoggerFolder, 'second'), 'utf8')).to.equal('yyy\n');
}
});
it('should delete side logs on dispose', async () => {
logger.init({
storagePath: tempFolders.storagePath.name
});
logger.init(tempFolders.storagePath.name);
await logger.log('xxx', { additionalLogLocation: 'first' });
await logger.log('yyy', { additionalLogLocation: 'second' });
@@ -94,9 +80,7 @@ describe('OutputChannelLogger tests', () => {
});
it('should remove an additional log location', async () => {
logger.init({
storagePath: tempFolders.storagePath.name
});
logger.init(tempFolders.storagePath.name);
await logger.log('xxx', { additionalLogLocation: 'first' });
await logger.log('yyy', { additionalLogLocation: 'second' });
@@ -112,9 +96,7 @@ describe('OutputChannelLogger tests', () => {
it('should delete an existing folder on init', async () => {
fs.createFileSync(path.join(tempFolders.storagePath.name, 'test-logger', 'xxx'));
logger.init({
storagePath: tempFolders.storagePath.name
});
logger.init(tempFolders.storagePath.name);
// should be empty dir
const testLoggerFolder = path.join(tempFolders.storagePath.name, 'test-logger');

View File

@@ -1,4 +1,4 @@
import { LocationStyle } from './bqrs-schema';
import { LocationStyle } from "./bqrs-schema";
// See https://help.semmle.com/QL/learn-ql/ql/locations.html for how these are used.
export interface FivePartLocation {
@@ -31,54 +31,69 @@ export type LocationValue = RawLocationValue | WholeFileLocation;
/** A location that may be resolved to a source code element. */
export type ResolvableLocationValue = FivePartLocation | WholeFileLocation;
/**
* The CodeQL filesystem libraries use this pattern in `getURL()` predicates
* to describe the location of an entire filesystem resource.
* Such locations appear as `StringLocation`s instead of `FivePartLocation`s.
*
*
* Folder resources also get similar URLs, but with the `folder` scheme.
* They are deliberately ignored here, since there is no suitable location to show the user.
*/
const WHOLE_FILE_LOCATION_REGEX = /file:\/\/(.+):0:0:0:0/;
const FILE_LOCATION_REGEX = /file:\/\/(.+):([0-9]+):([0-9]+):([0-9]+):([0-9]+)/;
/**
* Gets a resolvable source file location for the specified `LocationValue`, if possible.
* @param loc The location to test.
*/
export function tryGetResolvableLocation(loc: LocationValue | undefined): ResolvableLocationValue | undefined {
export function tryGetResolvableLocation(
loc: LocationValue | undefined
): ResolvableLocationValue | undefined {
if (loc === undefined) {
return undefined;
}
else if ((loc.t === LocationStyle.FivePart) && loc.file) {
} else if (loc.t === LocationStyle.FivePart && loc.file) {
return loc;
}
else if ((loc.t === LocationStyle.WholeFile) && loc.file) {
} else if (loc.t === LocationStyle.WholeFile && loc.file) {
return loc;
}
else if ((loc.t === LocationStyle.String) && loc.loc) {
return tryGetWholeFileLocation(loc);
}
else {
} else if (loc.t === LocationStyle.String && loc.loc) {
return tryGetLocationFromString(loc);
} else {
return undefined;
}
}
export function tryGetWholeFileLocation(loc: StringLocation): WholeFileLocation | undefined {
const matches = WHOLE_FILE_LOCATION_REGEX.exec(loc.loc);
export function tryGetLocationFromString(
loc: StringLocation
): ResolvableLocationValue | undefined {
const matches = FILE_LOCATION_REGEX.exec(loc.loc);
if (matches && matches.length > 1 && matches[1]) {
// Whole-file location.
// We could represent this as a FivePartLocation with all numeric fields set to zero,
// but that would be a deliberate misuse as those fields are intended to be 1-based.
return {
t: LocationStyle.WholeFile,
file: matches[1]
};
if (isWholeFileMatch(matches)) {
return {
t: LocationStyle.WholeFile,
file: matches[1],
};
} else {
return {
t: LocationStyle.FivePart,
file: matches[1],
lineStart: Number(matches[2]),
colStart: Number(matches[3]),
lineEnd: Number(matches[4]),
colEnd: Number(matches[5]),
}
}
} else {
return undefined;
}
}
function isWholeFileMatch(matches: RegExpExecArray): boolean {
return (
matches[2] === "0" &&
matches[3] === "0" &&
matches[4] === "0" &&
matches[5] === "0"
);
}
export interface ElementBase {
id: PrimitiveColumnValue;
label?: string;
@@ -93,8 +108,7 @@ export interface ElementWithLocation extends ElementBase {
location: LocationValue;
}
export interface Element extends Required<ElementBase> {
}
export interface Element extends Required<ElementBase> {}
export type PrimitiveColumnValue = string | boolean | number | Date;
export type ColumnValue = PrimitiveColumnValue | ElementBase;

View File

@@ -1,5 +1,6 @@
{
"newLineCharacter": "\n",
"convertTabsToSpaces": true,
"indentStyle": 2,
"insertSpaceAfterCommaDelimiter": true,
"insertSpaceAfterSemicolonInForStatements": true,