1- import { basename , dirname , join } from "path" ;
1+ import { dirname , join } from "path" ;
22import { Uri , window , window as Window , workspace } from "vscode" ;
33import { CodeQLCliServer } from "../codeql-cli/cli" ;
44import { showAndLogExceptionWithTelemetry } from "../common/logging" ;
@@ -7,7 +7,10 @@ import {
77 getLanguageDisplayName ,
88 QueryLanguage ,
99} from "../common/query-language" ;
10- import { getFirstWorkspaceFolder } from "../common/vscode/workspace-folders" ;
10+ import {
11+ getFirstWorkspaceFolder ,
12+ getOnDiskWorkspaceFolders ,
13+ } from "../common/vscode/workspace-folders" ;
1114import { asError , getErrorMessage } from "../common/helpers-pure" ;
1215import { QlPackGenerator } from "./qlpack-generator" ;
1316import { DatabaseItem , DatabaseManager } from "../databases/local-databases" ;
@@ -25,12 +28,16 @@ import {
2528 isCodespacesTemplate ,
2629 setQlPackLocation ,
2730} from "../config" ;
28- import { lstat , pathExists } from "fs-extra" ;
31+ import { lstat , pathExists , readFile } from "fs-extra" ;
2932import { askForLanguage } from "../codeql-cli/query-language" ;
3033import { showInformationMessageWithAction } from "../common/vscode/dialog" ;
3134import { redactableError } from "../common/errors" ;
3235import { App } from "../common/app" ;
3336import { QueryTreeViewItem } from "../queries-panel/query-tree-view-item" ;
37+ import { containsPath } from "../common/files" ;
38+ import { getQlPackPath } from "../common/ql" ;
39+ import { load } from "js-yaml" ;
40+ import { QlPackFile } from "../packaging/qlpack-file" ;
3441
3542type QueryLanguagesToDatabaseMap = Record < string , string > ;
3643
@@ -48,6 +55,7 @@ export const QUERY_LANGUAGE_TO_DATABASE_REPO: QueryLanguagesToDatabaseMap = {
4855export class SkeletonQueryWizard {
4956 private fileName = "example.ql" ;
5057 private qlPackStoragePath : string | undefined ;
58+ private queryStoragePath : string | undefined ;
5159 private downloadPromise : Promise < void > | undefined ;
5260
5361 constructor (
@@ -61,10 +69,6 @@ export class SkeletonQueryWizard {
6169 private language : QueryLanguage | undefined = undefined ,
6270 ) { }
6371
64- private get folderName ( ) {
65- return `codeql-custom-queries-${ this . language } ` ;
66- }
67-
6872 /**
6973 * Wait for the download process to complete by waiting for the user to select
7074 * either "Download database" or closing the dialog. This is used for testing.
@@ -76,6 +80,14 @@ export class SkeletonQueryWizard {
7680 }
7781
7882 public async execute ( ) {
83+ // First try detecting the language based on the existing qlpacks.
84+ // This will override the selected language if there is an existing query pack.
85+ const detectedLanguage = await this . detectLanguage ( ) ;
86+ if ( detectedLanguage ) {
87+ this . language = detectedLanguage ;
88+ }
89+
90+ // If no existing qlpack was found, we need to ask the user for the language
7991 if ( ! this . language ) {
8092 // show quick pick to choose language
8193 this . language = await this . chooseLanguage ( ) ;
@@ -85,18 +97,39 @@ export class SkeletonQueryWizard {
8597 return ;
8698 }
8799
88- this . qlPackStoragePath = await this . determineStoragePath ( ) ;
100+ let createSkeletonQueryPack : boolean = false ;
89101
90- const skeletonPackAlreadyExists = await pathExists (
91- join ( this . qlPackStoragePath , this . folderName ) ,
92- ) ;
102+ if ( ! this . qlPackStoragePath ) {
103+ // This means no existing qlpack was detected in the selected folder, so we need
104+ // to find a new location to store the qlpack. This new location could potentially
105+ // already exist.
106+ const storagePath = await this . determineStoragePath ( ) ;
107+ this . qlPackStoragePath = join (
108+ storagePath ,
109+ `codeql-custom-queries-${ this . language } ` ,
110+ ) ;
93111
94- if ( skeletonPackAlreadyExists ) {
95- // just create a new example query file in skeleton QL pack
96- await this . createExampleFile ( ) ;
112+ // Try to detect if there is already a qlpack in this location. We will assume that
113+ // the user hasn't changed the language of the qlpack.
114+ const qlPackPath = await getQlPackPath ( this . qlPackStoragePath ) ;
115+
116+ // If we are creating or using a qlpack in the user's selected folder, we will also
117+ // create the query in that folder
118+ this . queryStoragePath = this . qlPackStoragePath ;
119+
120+ createSkeletonQueryPack = qlPackPath === undefined ;
97121 } else {
122+ // A query pack was detected in the selected folder or one of its ancestors, so we
123+ // directly use the selected folder as the storage path for the query.
124+ this . queryStoragePath = await this . determineStoragePathFromSelection ( ) ;
125+ }
126+
127+ if ( createSkeletonQueryPack ) {
98128 // generate a new skeleton QL pack with query file
99129 await this . createQlPack ( ) ;
130+ } else {
131+ // just create a new example query file in skeleton QL pack
132+ await this . createExampleFile ( ) ;
100133 }
101134
102135 // open the query file
@@ -113,13 +146,11 @@ export class SkeletonQueryWizard {
113146 }
114147
115148 private async openExampleFile ( ) {
116- if ( this . folderName === undefined || this . qlPackStoragePath === undefined ) {
149+ if ( this . queryStoragePath === undefined ) {
117150 throw new Error ( "Path to folder is undefined" ) ;
118151 }
119152
120- const queryFileUri = Uri . file (
121- join ( this . qlPackStoragePath , this . folderName , this . fileName ) ,
122- ) ;
153+ const queryFileUri = Uri . file ( join ( this . queryStoragePath , this . fileName ) ) ;
123154
124155 void workspace . openTextDocument ( queryFileUri ) . then ( ( doc ) => {
125156 void Window . showTextDocument ( doc , {
@@ -133,15 +164,7 @@ export class SkeletonQueryWizard {
133164 return this . determineRootStoragePath ( ) ;
134165 }
135166
136- const storagePath = await this . determineStoragePathFromSelection ( ) ;
137-
138- // If the user has selected a folder or file within a folder that matches the current
139- // folder name, we should create a query rather than a query pack
140- if ( basename ( storagePath ) === this . folderName ) {
141- return dirname ( storagePath ) ;
142- }
143-
144- return storagePath ;
167+ return this . determineStoragePathFromSelection ( ) ;
145168 }
146169
147170 private async determineStoragePathFromSelection ( ) : Promise < string > {
@@ -194,6 +217,62 @@ export class SkeletonQueryWizard {
194217 return storageFolder ;
195218 }
196219
220+ private async detectLanguage ( ) : Promise < QueryLanguage | undefined > {
221+ if ( this . selectedItems . length < 1 ) {
222+ return undefined ;
223+ }
224+
225+ this . progress ( {
226+ message : "Resolving existing query packs" ,
227+ step : 1 ,
228+ maxStep : 3 ,
229+ } ) ;
230+
231+ const storagePath = await this . determineStoragePathFromSelection ( ) ;
232+
233+ const queryPacks = await this . cliServer . resolveQlpacks (
234+ getOnDiskWorkspaceFolders ( ) ,
235+ false ,
236+ "query" ,
237+ ) ;
238+
239+ const matchingQueryPacks = Object . values ( queryPacks )
240+ . map ( ( paths ) => paths . find ( ( path ) => containsPath ( path , storagePath ) ) )
241+ . filter ( ( path ) : path is string => path !== undefined )
242+ // Find the longest matching path
243+ . sort ( ( a , b ) => b . length - a . length ) ;
244+
245+ if ( matchingQueryPacks . length === 0 ) {
246+ return undefined ;
247+ }
248+
249+ const matchingQueryPackPath = matchingQueryPacks [ 0 ] ;
250+
251+ const qlPackPath = await getQlPackPath ( matchingQueryPackPath ) ;
252+ if ( ! qlPackPath ) {
253+ return undefined ;
254+ }
255+
256+ const qlPack = load ( await readFile ( qlPackPath , "utf8" ) ) as
257+ | QlPackFile
258+ | undefined ;
259+ const dependencies = qlPack ?. dependencies ;
260+ if ( ! dependencies || typeof dependencies !== "object" ) {
261+ return ;
262+ }
263+
264+ const matchingLanguages = Object . values ( QueryLanguage ) . filter (
265+ ( language ) => `codeql/${ language } -all` in dependencies ,
266+ ) ;
267+ if ( matchingLanguages . length !== 1 ) {
268+ return undefined ;
269+ }
270+
271+ this . qlPackStoragePath = matchingQueryPackPath ;
272+
273+ return matchingLanguages [ 0 ] ;
274+ }
275+
197276 private async chooseLanguage ( ) {
198277 this . progress ( {
199278 message : "Choose language" ,
@@ -205,8 +284,8 @@ export class SkeletonQueryWizard {
205284 }
206285
207286 private async createQlPack ( ) {
208- if ( this . folderName === undefined ) {
209- throw new Error ( "Folder name is undefined" ) ;
287+ if ( this . qlPackStoragePath === undefined ) {
288+ throw new Error ( "Query pack storage path is undefined" ) ;
210289 }
211290 if ( this . language === undefined ) {
212291 throw new Error ( "Language is undefined" ) ;
@@ -220,7 +299,6 @@ export class SkeletonQueryWizard {
220299
221300 try {
222301 const qlPackGenerator = new QlPackGenerator (
223- this . folderName ,
224302 this . language ,
225303 this . cliServer ,
226304 this . qlPackStoragePath ,
@@ -235,7 +313,7 @@ export class SkeletonQueryWizard {
235313 }
236314
237315 private async createExampleFile ( ) {
238- if ( this . folderName === undefined ) {
316+ if ( this . qlPackStoragePath === undefined ) {
239317 throw new Error ( "Folder name is undefined" ) ;
240318 }
241319 if ( this . language === undefined ) {
@@ -251,13 +329,12 @@ export class SkeletonQueryWizard {
251329
252330 try {
253331 const qlPackGenerator = new QlPackGenerator (
254- this . folderName ,
255332 this . language ,
256333 this . cliServer ,
257334 this . qlPackStoragePath ,
258335 ) ;
259336
260- this . fileName = await this . determineNextFileName ( this . folderName ) ;
337+ this . fileName = await this . determineNextFileName ( ) ;
261338 await qlPackGenerator . createExampleQlFile ( this . fileName ) ;
262339 } catch ( e : unknown ) {
263340 void this . app . logger . log (
@@ -266,13 +343,18 @@ export class SkeletonQueryWizard {
266343 }
267344 }
268345
269- private async determineNextFileName ( folderName : string ) : Promise < string > {
270- if ( this . qlPackStoragePath === undefined ) {
271- throw new Error ( "QL Pack storage path is undefined" ) ;
346+ private async determineNextFileName ( ) : Promise < string > {
347+ if ( this . queryStoragePath === undefined ) {
348+ throw new Error ( "Query storage path is undefined" ) ;
272349 }
273350
274- const folderUri = Uri . file ( join ( this . qlPackStoragePath , folderName ) ) ;
351+ const folderUri = Uri . file ( this . queryStoragePath ) ;
275352 const files = await workspace . fs . readDirectory ( folderUri ) ;
353+ // If the example.ql file doesn't exist yet, use that name
354+ if ( ! files . some ( ( [ filename , _fileType ] ) => filename === this . fileName ) ) {
355+ return this . fileName ;
356+ }
357+
276358 const qlFiles = files . filter ( ( [ filename , _fileType ] ) =>
277359 filename . match ( / ^ e x a m p l e [ 0 - 9 ] * \. q l $ / ) ,
278360 ) ;
@@ -281,10 +363,6 @@ export class SkeletonQueryWizard {
281363 }
282364
283365 private async promptDownloadDatabase ( ) {
284- if ( this . qlPackStoragePath === undefined ) {
285- throw new Error ( "QL Pack storage path is undefined" ) ;
286- }
287-
288366 if ( this . language === undefined ) {
289367 throw new Error ( "Language is undefined" ) ;
290368 }
@@ -321,10 +399,6 @@ export class SkeletonQueryWizard {
321399 }
322400
323401 private async downloadDatabase ( progress : ProgressCallback ) {
324- if ( this . qlPackStoragePath === undefined ) {
325- throw new Error ( "QL Pack storage path is undefined" ) ;
326- }
327-
328402 if ( this . databaseStoragePath === undefined ) {
329403 throw new Error ( "Database storage path is undefined" ) ;
330404 }
@@ -362,10 +436,6 @@ export class SkeletonQueryWizard {
362436 throw new Error ( "Language is undefined" ) ;
363437 }
364438
365- if ( this . qlPackStoragePath === undefined ) {
366- throw new Error ( "QL Pack storage path is undefined" ) ;
367- }
368-
369439 const existingDatabaseItem =
370440 await SkeletonQueryWizard . findExistingDatabaseItem (
371441 this . language ,
@@ -393,15 +463,11 @@ export class SkeletonQueryWizard {
393463 }
394464
395465 private get openFileMarkdownLink ( ) {
396- if ( this . qlPackStoragePath === undefined ) {
466+ if ( this . queryStoragePath === undefined ) {
397467 throw new Error ( "QL Pack storage path is undefined" ) ;
398468 }
399469
400- const queryPath = join (
401- this . qlPackStoragePath ,
402- this . folderName ,
403- this . fileName ,
404- ) ;
470+ const queryPath = join ( this . queryStoragePath , this . fileName ) ;
405471 const queryPathUri = Uri . file ( queryPath ) ;
406472
407473 const openFileArgs = [ queryPathUri . toString ( true ) ] ;
0 commit comments