@@ -339,6 +339,156 @@ export function defineCustomBlocksVisitor(
339
339
return compositingVisitors ( jsonVisitor , yamlVisitor )
340
340
}
341
341
342
+ export type VueObjectType =
343
+ | 'mark'
344
+ | 'export'
345
+ | 'definition'
346
+ | 'instance'
347
+ | 'variable'
348
+ | 'components-option'
349
+ /**
350
+ * If the given object is a Vue component or instance, returns the Vue definition type.
351
+ * @param context The ESLint rule context object.
352
+ * @param node Node to check
353
+ * @returns The Vue definition type.
354
+ */
355
+ export function getVueObjectType (
356
+ context : RuleContext ,
357
+ node : VAST . ESLintObjectExpression
358
+ ) : VueObjectType | null {
359
+ if ( node . type !== 'ObjectExpression' || ! node . parent ) {
360
+ return null
361
+ }
362
+ const parent = node . parent
363
+ if ( parent . type === 'ExportDefaultDeclaration' ) {
364
+ // export default {} in .vue || .jsx
365
+ const ext = extname ( context . getFilename ( ) ) . toLowerCase ( )
366
+ if (
367
+ ( ext === '.vue' || ext === '.jsx' || ! ext ) &&
368
+ skipTSAsExpression ( parent . declaration ) === node
369
+ ) {
370
+ const scriptSetup = getScriptSetupElement ( context )
371
+ if (
372
+ scriptSetup &&
373
+ scriptSetup . range [ 0 ] <= parent . range [ 0 ] &&
374
+ parent . range [ 1 ] <= scriptSetup . range [ 1 ]
375
+ ) {
376
+ // `export default` in `<script setup>`
377
+ return null
378
+ }
379
+ return 'export'
380
+ }
381
+ } else if ( parent . type === 'CallExpression' ) {
382
+ // Vue.component('xxx', {}) || component('xxx', {})
383
+ if (
384
+ getVueComponentDefinitionType ( node ) != null &&
385
+ skipTSAsExpression ( parent . arguments . slice ( - 1 ) [ 0 ] ) === node
386
+ ) {
387
+ return 'definition'
388
+ }
389
+ } else if ( parent . type === 'NewExpression' ) {
390
+ // new Vue({})
391
+ if (
392
+ isVueInstance ( parent ) &&
393
+ skipTSAsExpression ( parent . arguments [ 0 ] ) === node
394
+ ) {
395
+ return 'instance'
396
+ }
397
+ } else if ( parent . type === 'VariableDeclarator' ) {
398
+ // This is a judgment method that eslint-plugin-vue does not have.
399
+ // If the variable name is PascalCase, it is considered to be a Vue component. e.g. MyComponent = {}
400
+ if (
401
+ parent . init === node &&
402
+ parent . id . type === 'Identifier' &&
403
+ / ^ [ A - Z ] [ a - z A - Z \d ] + / u. test ( parent . id . name ) &&
404
+ parent . id . name . toUpperCase ( ) !== parent . id . name
405
+ ) {
406
+ return 'variable'
407
+ }
408
+ } else if ( parent . type === 'Property' ) {
409
+ // This is a judgment method that eslint-plugin-vue does not have.
410
+ // If set to components, it is considered to be a Vue component.
411
+ const componentsCandidate = parent . parent as VAST . ESLintObjectExpression
412
+ const pp = componentsCandidate . parent
413
+ if (
414
+ pp &&
415
+ pp . type === 'Property' &&
416
+ pp . value === componentsCandidate &&
417
+ ! pp . computed &&
418
+ ( pp . key . type === 'Identifier'
419
+ ? pp . key . name
420
+ : pp . key . type === 'Literal'
421
+ ? pp . key . value + ''
422
+ : '' ) === 'components'
423
+ ) {
424
+ return 'components-option'
425
+ }
426
+ }
427
+ if (
428
+ getComponentComments ( context ) . some (
429
+ el => el . loc . end . line === node . loc . start . line - 1
430
+ )
431
+ ) {
432
+ return 'mark'
433
+ }
434
+ return null
435
+ }
436
+
437
+ /**
438
+ * Gets the element of `<script setup>`
439
+ * @param context The ESLint rule context object.
440
+ * @returns the element of `<script setup>`
441
+ */
442
+ export function getScriptSetupElement (
443
+ context : RuleContext
444
+ ) : VAST . VElement | null {
445
+ const df =
446
+ context . parserServices . getDocumentFragment &&
447
+ context . parserServices . getDocumentFragment ( )
448
+ if ( ! df ) {
449
+ return null
450
+ }
451
+ const scripts = df . children
452
+ . filter ( isVElement )
453
+ . filter ( e => e . name === 'script' )
454
+ if ( scripts . length === 2 ) {
455
+ return scripts . find ( e => getAttribute ( e , 'setup' ) ) || null
456
+ } else {
457
+ const script = scripts [ 0 ]
458
+ if ( script && getAttribute ( script , 'setup' ) ) {
459
+ return script
460
+ }
461
+ }
462
+ return null
463
+ }
464
+ /**
465
+ * Checks whether the given node is VElement.
466
+ * @param node
467
+ */
468
+ export function isVElement (
469
+ node : VAST . VElement | VAST . VExpressionContainer | VAST . VText
470
+ ) : node is VAST . VElement {
471
+ return node . type === 'VElement'
472
+ }
473
+
474
+ /**
475
+ * Retrieve `TSAsExpression#expression` value if the given node a `TSAsExpression` node. Otherwise, pass through it.
476
+ * @template T Node type
477
+ * @param node The node to address.
478
+ * @returns The `TSAsExpression#expression` value if the node is a `TSAsExpression` node. Otherwise, the node.
479
+ */
480
+ export function skipTSAsExpression < T extends VAST . Node > ( node : T ) : T {
481
+ if ( ! node ) {
482
+ return node
483
+ }
484
+ // @ts -expect-error -- ignore
485
+ if ( node . type === 'TSAsExpression' ) {
486
+ // @ts -expect-error -- ignore
487
+ return skipTSAsExpression ( node . expression )
488
+ }
489
+ return node
490
+ }
491
+
342
492
function compositingVisitors (
343
493
visitor : RuleListener ,
344
494
...visitors : RuleListener [ ]
@@ -361,3 +511,115 @@ function compositingVisitors(
361
511
}
362
512
return visitor
363
513
}
514
+
515
+ /**
516
+ * Get the Vue component definition type from given node
517
+ * Vue.component('xxx', {}) || component('xxx', {})
518
+ * @param node Node to check
519
+ * @returns {'component' | 'mixin' | 'extend' | 'createApp' | 'defineComponent' | null }
520
+ */
521
+ function getVueComponentDefinitionType ( node : VAST . ESLintObjectExpression ) {
522
+ const parent = node . parent
523
+ if ( parent && parent . type === 'CallExpression' ) {
524
+ const callee = parent . callee
525
+
526
+ if ( callee . type === 'MemberExpression' ) {
527
+ const calleeObject = skipTSAsExpression ( callee . object )
528
+
529
+ if ( calleeObject . type === 'Identifier' ) {
530
+ const propName =
531
+ ! callee . computed &&
532
+ callee . property . type === 'Identifier' &&
533
+ callee . property . name
534
+ if ( calleeObject . name === 'Vue' ) {
535
+ // for Vue.js 2.x
536
+ // Vue.component('xxx', {}) || Vue.mixin({}) || Vue.extend('xxx', {})
537
+ const maybeFullVueComponentForVue2 =
538
+ propName && isObjectArgument ( parent )
539
+
540
+ return maybeFullVueComponentForVue2 &&
541
+ ( propName === 'component' ||
542
+ propName === 'mixin' ||
543
+ propName === 'extend' )
544
+ ? propName
545
+ : null
546
+ }
547
+
548
+ // for Vue.js 3.x
549
+ // app.component('xxx', {}) || app.mixin({})
550
+ const maybeFullVueComponent = propName && isObjectArgument ( parent )
551
+
552
+ return maybeFullVueComponent &&
553
+ ( propName === 'component' || propName === 'mixin' )
554
+ ? propName
555
+ : null
556
+ }
557
+ }
558
+
559
+ if ( callee . type === 'Identifier' ) {
560
+ if ( callee . name === 'component' ) {
561
+ // for Vue.js 2.x
562
+ // component('xxx', {})
563
+ const isDestructedVueComponent = isObjectArgument ( parent )
564
+ return isDestructedVueComponent ? 'component' : null
565
+ }
566
+ if ( callee . name === 'createApp' ) {
567
+ // for Vue.js 3.x
568
+ // createApp({})
569
+ const isAppVueComponent = isObjectArgument ( parent )
570
+ return isAppVueComponent ? 'createApp' : null
571
+ }
572
+ if ( callee . name === 'defineComponent' ) {
573
+ // for Vue.js 3.x
574
+ // defineComponent({})
575
+ const isDestructedVueComponent = isObjectArgument ( parent )
576
+ return isDestructedVueComponent ? 'defineComponent' : null
577
+ }
578
+ }
579
+ }
580
+
581
+ return null
582
+
583
+ function isObjectArgument ( node : VAST . ESLintCallExpression ) {
584
+ return (
585
+ node . arguments . length > 0 &&
586
+ skipTSAsExpression ( node . arguments . slice ( - 1 ) [ 0 ] ) . type ===
587
+ 'ObjectExpression'
588
+ )
589
+ }
590
+ }
591
+
592
+ /**
593
+ * Check whether given node is new Vue instance
594
+ * new Vue({})
595
+ * @param node Node to check
596
+ */
597
+ function isVueInstance ( node : VAST . ESLintNewExpression ) {
598
+ const callee = node . callee
599
+ return Boolean (
600
+ node . type === 'NewExpression' &&
601
+ callee . type === 'Identifier' &&
602
+ callee . name === 'Vue' &&
603
+ node . arguments . length &&
604
+ skipTSAsExpression ( node . arguments [ 0 ] ) . type === 'ObjectExpression'
605
+ )
606
+ }
607
+
608
+ const componentComments = new WeakMap < RuleContext , VAST . Token [ ] > ( )
609
+ /**
610
+ * Gets the component comments of a given context.
611
+ * @param context The ESLint rule context object.
612
+ * @return The the component comments.
613
+ */
614
+ function getComponentComments ( context : RuleContext ) {
615
+ let tokens = componentComments . get ( context )
616
+ if ( tokens ) {
617
+ return tokens
618
+ }
619
+ const sourceCode = context . getSourceCode ( )
620
+ tokens = sourceCode
621
+ . getAllComments ( )
622
+ . filter ( comment => / @ v u e \/ c o m p o n e n t / g. test ( comment . value ) )
623
+ componentComments . set ( context , tokens )
624
+ return tokens
625
+ }
0 commit comments