@@ -339,6 +339,156 @@ export function defineCustomBlocksVisitor(
339339 return compositingVisitors ( jsonVisitor , yamlVisitor )
340340}
341341
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+
342492function compositingVisitors (
343493 visitor : RuleListener ,
344494 ...visitors : RuleListener [ ]
@@ -361,3 +511,115 @@ function compositingVisitors(
361511 }
362512 return visitor
363513}
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