@@ -16,6 +16,8 @@ import getRandomPort from 'get-port'
16
16
import type tsStatic from 'typescript'
17
17
import { fileURLToPath } from 'node:url'
18
18
import { execaNode , execa } from 'execa'
19
+ import { parseImports } from 'parse-imports'
20
+ import { type SgNode } from '@ast-grep/napi'
19
21
import { importDefault } from '@poppinss/utils'
20
22
import { copyFile , mkdir } from 'node:fs/promises'
21
23
import { EnvLoader , EnvParser } from '@adonisjs/env'
@@ -24,7 +26,7 @@ import { type UnWrapLazyImport } from '@poppinss/utils/types'
24
26
import { basename , dirname , isAbsolute , join , relative } from 'node:path'
25
27
26
28
import debug from './debug.ts'
27
- import type { RunScriptOptions } from './types/common.ts'
29
+ import type { Import , RunScriptOptions } from './types/common.ts'
28
30
import {
29
31
type WatcherHooks ,
30
32
type BundlerHooks ,
@@ -318,3 +320,181 @@ export function throttle<Args extends any[]>(
318
320
319
321
return throttled
320
322
}
323
+
324
+ /**
325
+ * Finds an import reference inside a code snippet
326
+ */
327
+ export async function findImport ( code : string , importReference : string ) : Promise < Import | null > {
328
+ const importIdentifier = importReference . split ( '.' ) [ 0 ]
329
+
330
+ for ( const $import of await parseImports ( code , { } ) ) {
331
+ /**
332
+ * An import without any clause
333
+ */
334
+ if ( ! $import . importClause ) {
335
+ continue
336
+ }
337
+
338
+ /**
339
+ * An import without any clause
340
+ */
341
+ if ( ! $import . moduleSpecifier . value ) {
342
+ continue
343
+ }
344
+
345
+ /**
346
+ * Import identifier matches a default import
347
+ */
348
+ if ( $import . importClause . default === importIdentifier ) {
349
+ return {
350
+ specifier : $import . moduleSpecifier . value ,
351
+ isConstant : $import . moduleSpecifier . isConstant ,
352
+ clause : {
353
+ type : 'default' ,
354
+ value : importIdentifier ,
355
+ } ,
356
+ } satisfies Import
357
+ }
358
+
359
+ /**
360
+ * Import identifier matches a namespace import
361
+ */
362
+ if ( $import . importClause . namespace === importIdentifier ) {
363
+ return {
364
+ specifier : $import . moduleSpecifier . value ,
365
+ isConstant : $import . moduleSpecifier . isConstant ,
366
+ clause : {
367
+ type : 'namespace' ,
368
+ value : importIdentifier ,
369
+ } ,
370
+ } satisfies Import
371
+ }
372
+
373
+ const namedImport = $import . importClause . named . find ( ( { binding } ) => {
374
+ return binding === importIdentifier
375
+ } )
376
+
377
+ /**
378
+ * Import identifier matches a named import
379
+ */
380
+ if ( namedImport ) {
381
+ return {
382
+ specifier : $import . moduleSpecifier . value ,
383
+ isConstant : $import . moduleSpecifier . isConstant ,
384
+ clause : {
385
+ type : 'named' ,
386
+ value : namedImport . specifier ,
387
+ ...( namedImport . binding !== namedImport . specifier
388
+ ? {
389
+ alias : namedImport . binding ,
390
+ }
391
+ : { } ) ,
392
+ } ,
393
+ } satisfies Import
394
+ }
395
+ }
396
+
397
+ return null
398
+ }
399
+
400
+ /**
401
+ * Returns an array of SgNodes for class methods. The class name
402
+ * could be fetched using the `KCLASS` identifier.
403
+ */
404
+ export function inspectClassMethods ( node : SgNode ) : SgNode [ ] {
405
+ return node . findAll ( {
406
+ rule : {
407
+ kind : 'method_definition' ,
408
+ inside : {
409
+ pattern : {
410
+ selector : 'class_declaration' ,
411
+ context : 'class $KCLASS {$$$}' ,
412
+ } ,
413
+ stopBy : 'end' ,
414
+ } ,
415
+ } ,
416
+ } )
417
+ }
418
+
419
+ /**
420
+ * Converts an array of SgNode to plain text by removing whitespaces,
421
+ * identation and comments in between. Tested with the following
422
+ * children nodes only.
423
+ *
424
+ * - MemberExpression
425
+ * - Identifer
426
+ */
427
+ export function nodeToPlainText ( node : SgNode ) {
428
+ let out : string [ ] = [ ]
429
+ function toText ( one : SgNode ) {
430
+ const children = one . children ( )
431
+ if ( ! children . length ) {
432
+ out . push ( one . text ( ) )
433
+ } else {
434
+ children . forEach ( ( child ) => toText ( child ) )
435
+ }
436
+ }
437
+
438
+ toText ( node )
439
+ return out . join ( '' )
440
+ }
441
+
442
+ /**
443
+ * Inspects arguments for one or more method calls. If you want to
444
+ * scope the search within a specific context, then make sure to
445
+ * first narrow down the AST and pass a specific SgNode.
446
+ *
447
+ * For example: In case of validators, we will first find the Controller
448
+ * method for which we want the validation method calls.
449
+ */
450
+ export function inspectMethodArguments ( node : SgNode , methodCalls : string [ ] ) : SgNode [ ] {
451
+ const matchingExpressions = node . findAll ( {
452
+ rule : {
453
+ any : methodCalls . map ( ( methodCall ) => {
454
+ return {
455
+ pattern : {
456
+ context : `${ methodCall } ($$$ARGUMENTS)` ,
457
+ selector : 'call_expression' ,
458
+ } ,
459
+ }
460
+ } ) ,
461
+ } ,
462
+ } )
463
+
464
+ return matchingExpressions . flatMap ( ( matchingExpression ) => {
465
+ return matchingExpression . findAll ( { rule : { kind : 'arguments' } } )
466
+ } )
467
+ }
468
+
469
+ /**
470
+ * Inspect the validator direct usage code snippets. A member expression
471
+ * calling the ".validate" method is considered as direct usage of
472
+ * the validator.
473
+ */
474
+ export function searchValidatorDirectUsage ( node : SgNode ) : SgNode [ ] {
475
+ const matchingExpressions = node . findAll ( {
476
+ rule : {
477
+ pattern : {
478
+ context : '$$$VALIDATOR.validate($$$)' ,
479
+ selector : 'call_expression' ,
480
+ } ,
481
+ } ,
482
+ } )
483
+
484
+ return matchingExpressions . flatMap ( ( expression ) => {
485
+ /**
486
+ * Since we capture all "$$$.validate" calls, the first node
487
+ * within the call expression will be a member expression
488
+ * and its property identifier will be "validate".
489
+ *
490
+ * What we need is the text of this member expression without the
491
+ * comments
492
+ */
493
+ const firstMemberExpression = expression . getMultipleMatches ( 'VALIDATOR' )
494
+ return firstMemberExpression . flatMap ( ( exp ) => {
495
+ return exp . find ( {
496
+ rule : { any : [ { kind : 'identifier' } , { kind : 'member_expression' } ] } ,
497
+ } ) !
498
+ } )
499
+ } )
500
+ }
0 commit comments