Skip to content

Commit 22afde9

Browse files
committed
Add a language label next to databases in the UI
This change will only work on databases created by cli >= 2.4.1. In that version, a new `primaryLanguage` field in the `codeql-database.yml` file. We use this property as the language. This change also includes a refactoring of the logic around extracting database information heuristically based on file location. As much as possible, it is extracted to the `helpers` module. Also, the initial quick query text is generated based on the language (if known) otherwise it falls back to the old style of generation.
1 parent 3c08baf commit 22afde9

12 files changed

Lines changed: 175 additions & 76 deletions

File tree

extensions/ql-vscode/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
- Fix bug when removing databases where sometimes the source folder would not be removed from the workspace or the database files would not be removed from the workspace storage location. [#692](https://github.com/github/vscode-codeql/pull/692)
66
- Query results with no string representation will now be displayed with placeholder text in query results. Previously, they were omitted. [#694](https://github.com/github/vscode-codeql/pull/694)
7+
- Add a label for the language of a database in the databases view. This will only take effect for new databases created with the CodeQL CLI v2.4.1 or later. [#697](https://github.com/github/vscode-codeql/pull/697)
78

89
## 1.3.7 - 24 November 2020
910

extensions/ql-vscode/src/cli.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export interface DbInfo {
5050
sourceArchiveRoot: string;
5151
datasetFolder: string;
5252
logsFolder: string;
53+
primaryLanguage: string;
5354
}
5455

5556
/**

extensions/ql-vscode/src/contextual/queryResolver.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@ export async function qlpackOfDatabase(cli: CodeQLCliServer, db: DatabaseItem):
1616
if (db.contents === undefined)
1717
return undefined;
1818
const datasetPath = db.contents.datasetUri.fsPath;
19-
const { qlpack } = await helpers.resolveDatasetFolder(cli, datasetPath);
20-
return qlpack;
19+
return await helpers.getQlPackForDbscheme(cli, datasetPath);
2120
}
2221

2322

extensions/ql-vscode/src/databases-ui.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,15 @@ import {
1818
DatabaseItem,
1919
DatabaseManager,
2020
getUpgradesDirectories,
21-
isLikelyDatabaseRoot,
22-
isLikelyDbLanguageFolder,
2321
} from './databases';
2422
import {
2523
commandRunner,
2624
commandRunnerWithProgress,
2725
getOnDiskWorkspaceFolders,
2826
ProgressCallback,
29-
showAndLogErrorMessage
27+
showAndLogErrorMessage,
28+
isLikelyDatabaseRoot,
29+
isLikelyDbLanguageFolder
3030
} from './helpers';
3131
import { logger } from './logging';
3232
import { clearCacheInDatabase } from './run-queries';
@@ -143,6 +143,7 @@ class DatabaseTreeDataProvider extends DisposableObject
143143
);
144144
}
145145
item.tooltip = element.databaseUri.fsPath;
146+
item.description = element.language;
146147
return item;
147148
}
148149

extensions/ql-vscode/src/databases.ts

Lines changed: 29 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,13 @@ import * as path from 'path';
44
import * as vscode from 'vscode';
55
import * as cli from './cli';
66
import { ExtensionContext } from 'vscode';
7-
import { showAndLogErrorMessage, showAndLogWarningMessage, showAndLogInformationMessage } from './helpers';
7+
import {
8+
showAndLogErrorMessage,
9+
showAndLogWarningMessage,
10+
showAndLogInformationMessage,
11+
getPrimaryLanguage,
12+
isLikelyDatabaseRoot
13+
} from './helpers';
814
import { zipArchiveScheme, encodeArchiveBasePath, decodeSourceArchiveUri, encodeSourceArchiveUri } from './archive-filesystem-provider';
915
import { DisposableObject } from './vscode-utils/disposable-object';
1016
import { QueryServerConfig } from './config';
@@ -36,11 +42,13 @@ export interface DatabaseOptions {
3642
displayName?: string;
3743
ignoreSourceArchive?: boolean;
3844
dateAdded?: number | undefined;
45+
language?: string;
3946
}
4047

4148
export interface FullDatabaseOptions extends DatabaseOptions {
4249
ignoreSourceArchive: boolean;
4350
dateAdded: number | undefined;
51+
language: string;
4452
}
4553

4654
interface PersistedDatabaseItem {
@@ -193,6 +201,9 @@ export interface DatabaseItem {
193201
readonly databaseUri: vscode.Uri;
194202
/** The name of the database to be displayed in the UI */
195203
name: string;
204+
205+
/** The primary language of the database or empty string if unknown */
206+
readonly language: string;
196207
/** The URI of the database's source archive, or `undefined` if no source archive is to be used. */
197208
readonly sourceArchive: vscode.Uri | undefined;
198209
/**
@@ -427,6 +438,10 @@ export class DatabaseItemImpl implements DatabaseItem {
427438
return dbInfo.datasetFolder;
428439
}
429440

441+
public get language() {
442+
return this.options.language || '';
443+
}
444+
430445
/**
431446
* Returns the root uri of the virtual filesystem for this database's source archive.
432447
*/
@@ -493,18 +508,17 @@ export class DatabaseManager extends DisposableObject {
493508
}
494509

495510
public async openDatabase(
496-
uri: vscode.Uri, options?: DatabaseOptions
511+
uri: vscode.Uri
497512
): Promise<DatabaseItem> {
498-
499513
const contents = await resolveDatabaseContents(uri);
500-
const realOptions = options || {};
501514
// Ignore the source archive for QLTest databases by default.
502515
const isQLTestDatabase = path.extname(uri.fsPath) === '.testproj';
503516
const fullOptions: FullDatabaseOptions = {
504-
ignoreSourceArchive: (realOptions.ignoreSourceArchive !== undefined) ?
505-
realOptions.ignoreSourceArchive : isQLTestDatabase,
506-
displayName: realOptions.displayName,
507-
dateAdded: realOptions.dateAdded || Date.now()
517+
ignoreSourceArchive: isQLTestDatabase,
518+
// displayName is only set if a user explicitly renames a database
519+
displayName: undefined,
520+
dateAdded: Date.now(),
521+
language: await getPrimaryLanguage(uri.fsPath)
508522
};
509523
const databaseItem = new DatabaseItemImpl(uri, contents, fullOptions, (event) => {
510524
this._onDidChangeDatabaseItem.fire(event);
@@ -561,6 +575,7 @@ export class DatabaseManager extends DisposableObject {
561575
let displayName: string | undefined = undefined;
562576
let ignoreSourceArchive = false;
563577
let dateAdded = undefined;
578+
let language = '';
564579
if (state.options) {
565580
if (typeof state.options.displayName === 'string') {
566581
displayName = state.options.displayName;
@@ -571,11 +586,16 @@ export class DatabaseManager extends DisposableObject {
571586
if (typeof state.options.dateAdded === 'number') {
572587
dateAdded = state.options.dateAdded;
573588
}
589+
if (state.options.language) {
590+
language = state.options.language;
591+
}
574592
}
593+
575594
const fullOptions: FullDatabaseOptions = {
576595
ignoreSourceArchive,
577596
displayName,
578-
dateAdded
597+
dateAdded,
598+
language
579599
};
580600
const item = new DatabaseItemImpl(vscode.Uri.parse(state.uri, true), undefined, fullOptions,
581601
(event) => {
@@ -737,23 +757,3 @@ export function getUpgradesDirectories(scripts: string[]): vscode.Uri[] {
737757
const uniqueParentDirs = new Set(parentDirs);
738758
return Array.from(uniqueParentDirs).map(filePath => vscode.Uri.file(filePath));
739759
}
740-
741-
742-
// TODO: Get the list of supported languages from a list that will be auto-updated.
743-
744-
export async function isLikelyDatabaseRoot(fsPath: string) {
745-
const [a, b, c] = (await Promise.all([
746-
// databases can have either .dbinfo or codeql-database.yml.
747-
fs.pathExists(path.join(fsPath, '.dbinfo')),
748-
fs.pathExists(path.join(fsPath, 'codeql-database.yml')),
749-
750-
// they *must* have a db-language folder
751-
(await fs.readdir(fsPath)).some(isLikelyDbLanguageFolder)
752-
]));
753-
754-
return (a || b) && c;
755-
}
756-
757-
export function isLikelyDbLanguageFolder(dbPath: string) {
758-
return !!path.basename(dbPath).startsWith('db-');
759-
}

extensions/ql-vscode/src/helpers.ts

Lines changed: 71 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -358,12 +358,6 @@ function createRateLimitedResult(): RateLimitedResult {
358358
};
359359
}
360360

361-
362-
export type DatasetFolderInfo = {
363-
dbscheme: string;
364-
qlpack: string;
365-
}
366-
367361
export async function getQlPackForDbscheme(cliServer: CodeQLCliServer, dbschemePath: string): Promise<string> {
368362
const qlpacks = await cliServer.resolveQlpacks(getOnDiskWorkspaceFolders());
369363
const packs: { packDir: string | undefined; packName: string }[] =
@@ -391,7 +385,7 @@ export async function getQlPackForDbscheme(cliServer: CodeQLCliServer, dbschemeP
391385
throw new Error(`Could not find qlpack file for dbscheme ${dbschemePath}`);
392386
}
393387

394-
export async function resolveDatasetFolder(cliServer: CodeQLCliServer, datasetFolder: string): Promise<DatasetFolderInfo> {
388+
export async function getPrimaryDbscheme(datasetFolder: string): Promise<string> {
395389
const dbschemes = await glob(path.join(datasetFolder, '*.dbscheme'));
396390

397391
if (dbschemes.length < 1) {
@@ -400,12 +394,11 @@ export async function resolveDatasetFolder(cliServer: CodeQLCliServer, datasetFo
400394

401395
dbschemes.sort();
402396
const dbscheme = dbschemes[0];
397+
403398
if (dbschemes.length > 1) {
404399
Window.showErrorMessage(`Found multiple dbschemes in ${datasetFolder} during quick query; arbitrarily choosing the first, ${dbscheme}, to decide what library to use.`);
405400
}
406-
407-
const qlpack = await getQlPackForDbscheme(cliServer, dbscheme);
408-
return { dbscheme, qlpack };
401+
return dbscheme;
409402
}
410403

411404
/**
@@ -464,3 +457,71 @@ export class CachedOperation<U> {
464457
}
465458
}
466459
}
460+
461+
462+
463+
/**
464+
* The following functions al heuristically determine metadata about databases.
465+
*/
466+
467+
const dbSchemeToLanguage = {
468+
'semmlecode.javascript.dbscheme': 'javascript',
469+
'semmlecode.cpp.dbscheme': 'cpp',
470+
'semmlecode.dbscheme': 'java',
471+
'semmlecode.python.dbscheme': 'pyhton',
472+
'semmlecode.csharp.dbscheme': 'csharp',
473+
'go.dbscheme': 'go'
474+
};
475+
476+
/**
477+
* Returns the initial contents for an empty query, based on the language of the selected
478+
* databse.
479+
*
480+
* First try to get the contents text based on language. if that fails, try to get based on
481+
* dbscheme. Otherwise return no import statement.
482+
*
483+
* @param language the database language or empty string if unknown
484+
* @param dbscheme path to the dbscheme file
485+
*
486+
* @returns an import and empty select statement appropriate for the selected language
487+
*/
488+
export function getInitialQueryContents(language: string, dbscheme: string) {
489+
if (!language) {
490+
const dbschemeBase = path.basename(dbscheme) as keyof typeof dbSchemeToLanguage;
491+
language = dbSchemeToLanguage[dbschemeBase];
492+
}
493+
494+
return language
495+
? `import ${language}\n\nselect ""`
496+
: 'select ""';
497+
}
498+
499+
export async function isLikelyDatabaseRoot(maybeRoot: string) {
500+
const [a, b, c] = (await Promise.all([
501+
// databases can have either .dbinfo or codeql-database.yml.
502+
fs.pathExists(path.join(maybeRoot, '.dbinfo')),
503+
fs.pathExists(path.join(maybeRoot, 'codeql-database.yml')),
504+
505+
// they *must* have a db-{language} folder
506+
glob('db-*/', { cwd: maybeRoot })
507+
]));
508+
509+
return !!((a || b) && c);
510+
}
511+
512+
export function isLikelyDbLanguageFolder(dbPath: string) {
513+
return !!path.basename(dbPath).startsWith('db-');
514+
}
515+
516+
export async function getPrimaryLanguage(root: string) {
517+
try {
518+
const metadataFile = path.join(root, 'codeql-database.yml');
519+
if (await fs.pathExists(metadataFile)) {
520+
const metadata = yaml.safeLoad(await fs.readFile(metadataFile, 'utf8')) as { primaryLanguage: string | undefined };
521+
return metadata.primaryLanguage || '';
522+
}
523+
} catch (e) {
524+
// could not determine language
525+
}
526+
return '';
527+
}

extensions/ql-vscode/src/pure/helpers-pure.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* helpers-pure.ts
33
* ------------
44
*
5-
* Helper functions that don't depend on vscode and therefore can be used by the front-end and pure unit tests.
5+
* Helper functions that don't depend on vscode or the CLI and therefore can be used by the front-end and pure unit tests.
66
*/
77

88
/**

extensions/ql-vscode/src/quick-query.ts

Lines changed: 17 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,16 @@ import { CancellationToken, ExtensionContext, window as Window, workspace, Uri }
55
import { ErrorCodes, ResponseError } from 'vscode-languageclient';
66
import { CodeQLCliServer } from './cli';
77
import { DatabaseUI } from './databases-ui';
8-
import * as helpers from './helpers';
98
import { logger } from './logging';
9+
import {
10+
getInitialQueryContents,
11+
getPrimaryDbscheme,
12+
getQlPackForDbscheme,
13+
ProgressCallback,
14+
showAndLogErrorMessage,
15+
showBinaryChoiceDialog,
16+
UserCancellationException
17+
} from './helpers';
1018

1119
const QUICK_QUERIES_DIR_NAME = 'quick-queries';
1220
const QUICK_QUERY_QUERY_NAME = 'quick-query.ql';
@@ -16,21 +24,6 @@ export function isQuickQueryPath(queryPath: string): boolean {
1624
return path.basename(queryPath) === QUICK_QUERY_QUERY_NAME;
1725
}
1826

19-
/**
20-
* `getBaseText` heuristically returns an appropriate import statement
21-
* prelude based on the filename of the dbscheme file given. TODO: add
22-
* a 'default import' field to the qlpack itself, and use that.
23-
*/
24-
function getBaseText(dbschemeBase: string) {
25-
if (dbschemeBase == 'semmlecode.javascript.dbscheme') return 'import javascript\n\nselect ""';
26-
if (dbschemeBase == 'semmlecode.cpp.dbscheme') return 'import cpp\n\nselect ""';
27-
if (dbschemeBase == 'semmlecode.dbscheme') return 'import java\n\nselect ""';
28-
if (dbschemeBase == 'semmlecode.python.dbscheme') return 'import python\n\nselect ""';
29-
if (dbschemeBase == 'semmlecode.csharp.dbscheme') return 'import csharp\n\nselect ""';
30-
if (dbschemeBase == 'go.dbscheme') return 'import go\n\nselect ""';
31-
return 'select ""';
32-
}
33-
3427
function getQuickQueriesDir(ctx: ExtensionContext): string {
3528
const storagePath = ctx.storagePath;
3629
if (storagePath === undefined) {
@@ -51,7 +44,7 @@ export async function displayQuickQuery(
5144
ctx: ExtensionContext,
5245
cliServer: CodeQLCliServer,
5346
databaseUI: DatabaseUI,
54-
progress: helpers.ProgressCallback,
47+
progress: ProgressCallback,
5548
token: CancellationToken
5649
) {
5750

@@ -85,7 +78,7 @@ export async function displayQuickQuery(
8578
// being undefined) just let the user know that they're in for a
8679
// restart.
8780
if (workspace.workspaceFile === undefined) {
88-
const makeMultiRoot = await helpers.showBinaryChoiceDialog('Quick query requires multiple folders in the workspace. Reload workspace as multi-folder workspace?');
81+
const makeMultiRoot = await showBinaryChoiceDialog('Quick query requires multiple folders in the workspace. Reload workspace as multi-folder workspace?');
8982
if (makeMultiRoot) {
9083
updateQuickQueryDir(queriesDir, workspaceFolders.length, 0);
9184
}
@@ -105,7 +98,9 @@ export async function displayQuickQuery(
10598
}
10699

107100
const datasetFolder = await dbItem.getDatasetFolder(cliServer);
108-
const { qlpack, dbscheme } = await helpers.resolveDatasetFolder(cliServer, datasetFolder);
101+
const dbscheme = await getPrimaryDbscheme(datasetFolder);
102+
const qlpack = await getQlPackForDbscheme(cliServer, dbscheme);
103+
109104
const quickQueryQlpackYaml: any = {
110105
name: 'quick-query',
111106
version: '1.0.0',
@@ -114,21 +109,21 @@ export async function displayQuickQuery(
114109

115110
const qlFile = path.join(queriesDir, QUICK_QUERY_QUERY_NAME);
116111
const qlPackFile = path.join(queriesDir, 'qlpack.yml');
117-
await fs.writeFile(qlFile, getBaseText(path.basename(dbscheme)), 'utf8');
112+
await fs.writeFile(qlFile, getInitialQueryContents(dbItem.language, dbscheme), 'utf8');
118113
await fs.writeFile(qlPackFile, yaml.safeDump(quickQueryQlpackYaml), 'utf8');
119114
Window.showTextDocument(await workspace.openTextDocument(qlFile));
120115
}
121116

122117
// TODO: clean up error handling for top-level commands like this
123118
catch (e) {
124-
if (e instanceof helpers.UserCancellationException) {
119+
if (e instanceof UserCancellationException) {
125120
logger.log(e.message);
126121
}
127122
else if (e instanceof ResponseError && e.code == ErrorCodes.RequestCancelled) {
128123
logger.log(e.message);
129124
}
130125
else if (e instanceof Error)
131-
helpers.showAndLogErrorMessage(e.message);
126+
showAndLogErrorMessage(e.message);
132127
else
133128
throw e;
134129
}

0 commit comments

Comments
 (0)