Repository: vuejs/eslint-plugin-vue
Pull request #1844 (2022-04-12 04:57:26)

Fix 1836
Addresses #1836 


Patch:
diff --git a/docs/rules/README.md b/docs/rules/README.md
index b4ebfefd..3d2d113d 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -317,6 +317,7 @@ For example:
 | [vue/html-comment-content-spacing](./html-comment-content-spacing.md) | enforce unified spacing in HTML comments | :wrench: |
 | [vue/html-comment-indent](./html-comment-indent.md) | enforce consistent indentation in HTML comments | :wrench: |
 | [vue/match-component-file-name](./match-component-file-name.md) | require component name property to match its file name |  |
+| [vue/match-component-import-name](./match-component-import-name.md) | require the registered component name to match the imported component name |  |
 | [vue/new-line-between-multi-line-property](./new-line-between-multi-line-property.md) | enforce new lines between multi-line properties in Vue components | :wrench: |
 | [vue/next-tick-style](./next-tick-style.md) | enforce Promise or callback style in `nextTick` | :wrench: |
 | [vue/no-bare-strings-in-template](./no-bare-strings-in-template.md) | disallow the use of bare strings in `<template>` |  |
diff --git a/docs/rules/component-tags-order.md b/docs/rules/component-tags-order.md
index cc4aebbf..4fc8d245 100644
--- a/docs/rules/component-tags-order.md
+++ b/docs/rules/component-tags-order.md
@@ -26,7 +26,7 @@ This rule warns about the order of the top-level tags, such as `<script>`, `<tem
 }
 ```
 
-- `order` (`(string|string[])[]`) ... The order of top-level element names. default `[ [ "script", "template" ], "style" ]`. May also be CSS selectors, such as `script[setup]` and `i18n:not([lang=en])`.
+- `order` (`(string|string[])[]`) ... The order of top-level element names. default `[ [ "script", "template" ], "style" ]`. May also be CSS selectors, such as `script[setup]` and `i18n:not([locale=en])`.
 
 ### `{ "order": [ [ "script", "template" ], "style" ] }` (default)
 
@@ -161,26 +161,26 @@ This rule warns about the order of the top-level tags, such as `<script>`, `<tem
 
 </eslint-code-block>
 
-### `{ 'order': ['template', 'i18n:not([lang=en])', 'i18n[lang=en]'] }`
+### `{ 'order': ['template', 'i18n:not([locale=en])', 'i18n[locale=en]'] }`
 
-<eslint-code-block fix :rules="{'vue/component-tags-order': ['error', { 'order': ['template', 'i18n:not([lang=en])', 'i18n[lang=en]'] }]}">
+<eslint-code-block fix :rules="{'vue/component-tags-order': ['error', { 'order': ['template', 'i18n:not([locale=en])', 'i18n[locale=en]'] }]}">
 
 ```vue
 <!-- ✓ GOOD -->
 <template>...</template>
-<i18n lang="ja">/* ... */</i18n>
-<i18n lang="en">/* ... */</i18n>
+<i18n locale="ja">/* ... */</i18n>
+<i18n locale="en">/* ... */</i18n>
 ```
 
 </eslint-code-block>
 
-<eslint-code-block fix :rules="{'vue/component-tags-order': ['error', { 'order': ['template', 'i18n:not([lang=en])', 'i18n[lang=en]'] }]}">
+<eslint-code-block fix :rules="{'vue/component-tags-order': ['error', { 'order': ['template', 'i18n:not([locale=en])', 'i18n[locale=en]'] }]}">
 
 ```vue
 <!-- ✗ BAD -->
 <template>...</template>
-<i18n lang="en">/* ... */</i18n>
-<i18n lang="ja">/* ... */</i18n>
+<i18n locale="en">/* ... */</i18n>
+<i18n locale="ja">/* ... */</i18n>
 ```
 
 </eslint-code-block>
diff --git a/docs/rules/match-component-import-name.md b/docs/rules/match-component-import-name.md
new file mode 100644
index 00000000..083f6b32
--- /dev/null
+++ b/docs/rules/match-component-import-name.md
@@ -0,0 +1,45 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/match-component-import-name
+description: require the registered component name to match the imported component name
+---
+# vue/match-component-import-name
+
+> require the registered component name to match the imported component name
+
+- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
+
+## :book: Rule Details
+
+By default, this rule will validate that the imported name matches the name of the components object property identifer. Note that "matches" means that the imported name matches either the PascalCase or kebab-case version of the components object property identifer. If you would like to enforce that it must match only one of PascalCase or kebab-case, use this rule in conjunction with the rule [vue/component-definition-name-casing](./component-definition-name-casing.md).
+
+<eslint-code-block :rules="{'vue/match-component-file-name': ['error']}">
+
+```vue
+<script>
+export default {
+  components: {
+    /* ✓ GOOD */
+    AppButton,
+    AppButton: AppButton,
+    'app-button': AppButton,
+
+    /* ✗ BAD */
+    SomeOtherName: AppButton,
+    'some-other-name': AppButton
+  }
+}
+</script>
+```
+
+</eslint-code-block>
+
+## :couple: Related Rules
+
+- [vue/component-definition-name-casing](./component-definition-name-casing.md)
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/match-component-import-name.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/match-component-import-name.js)
diff --git a/lib/index.js b/lib/index.js
index 22fe9a18..d412a6bf 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -47,6 +47,7 @@ module.exports = {
     'key-spacing': require('./rules/key-spacing'),
     'keyword-spacing': require('./rules/keyword-spacing'),
     'match-component-file-name': require('./rules/match-component-file-name'),
+    'match-component-import-name': require('./rules/match-component-import-name'),
     'max-attributes-per-line': require('./rules/max-attributes-per-line'),
     'max-len': require('./rules/max-len'),
     'multi-word-component-names': require('./rules/multi-word-component-names'),
diff --git a/lib/rules/component-tags-order.js b/lib/rules/component-tags-order.js
index 09740d11..dfd8482b 100644
--- a/lib/rules/component-tags-order.js
+++ b/lib/rules/component-tags-order.js
@@ -9,7 +9,15 @@
 // ------------------------------------------------------------------------------
 
 const utils = require('../utils')
-const parser = require('postcss-selector-parser')
+const { parseSelector } = require('../utils/selector')
+
+/**
+ * @typedef {import('../utils/selector').VElementSelector} VElementSelector
+ */
+
+// ------------------------------------------------------------------------------
+// Helpers
+// ------------------------------------------------------------------------------
 
 const DEFAULT_ORDER = Object.freeze([['script', 'template'], 'style'])
 
@@ -55,24 +63,38 @@ module.exports = {
    * @returns {RuleListener} AST event handlers.
    */
   create(context) {
-    /** @type {Map<string, number>} */
-    const orderMap = new Map()
+    /**
+     * @typedef {object} OrderElement
+     * @property {string} selectorText
+     * @property {VElementSelector} selector
+     * @property {number} index
+     */
+    /** @type {OrderElement[]} */
+    const orders = []
     /** @type {(string|string[])[]} */
     const orderOptions =
       (context.options[0] && context.options[0].order) || DEFAULT_ORDER
-    orderOptions.forEach((nameOrNames, index) => {
-      if (Array.isArray(nameOrNames)) {
-        for (const name of nameOrNames) {
-          orderMap.set(name, index)
+    orderOptions.forEach((selectorOrSelectors, index) => {
+      if (Array.isArray(selectorOrSelectors)) {
+        for (const selector of selectorOrSelectors) {
+          orders.push({
+            selectorText: selector,
+            selector: parseSelector(selector, context),
+            index
+          })
         }
       } else {
-        orderMap.set(nameOrNames, index)
+        orders.push({
+          selectorText: selectorOrSelectors,
+          selector: parseSelector(selectorOrSelectors, context),
+          index
+        })
       }
     })
 
     /**
      * @param {VElement} element
-     * @return {String}
+     * @return {string}
      */
     function getAttributeString(element) {
       return element.startTag.attributes
@@ -90,54 +112,11 @@ module.exports = {
         .join(' ')
     }
 
-    /**
-     * @param {String} ordering
-     * @param {VElement} element
-     * @return {Boolean} true if the element matches the selector, false otherwise
-     */
-    function matches(ordering, element) {
-      let attributeMatches = true
-      let isNegated = false
-      let tagMatches = true
-
-      parser((selectors) => {
-        selectors.walk((selector) => {
-          switch (selector.type) {
-            case 'tag':
-              tagMatches = selector.value === element.name
-              break
-            case 'pseudo':
-              isNegated = selector.value === ':not'
-              break
-            case 'attribute':
-              attributeMatches = utils.hasAttribute(
-                element,
-                selector.qualifiedAttribute,
-                selector.value
-              )
-              break
-          }
-        })
-      }).processSync(ordering)
-
-      if (isNegated) {
-        return tagMatches && !attributeMatches
-      } else {
-        return tagMatches && attributeMatches
-      }
-    }
-
     /**
      * @param {VElement} element
      */
-    function getOrderPosition(element) {
-      for (const [ordering, index] of orderMap.entries()) {
-        if (matches(ordering, element)) {
-          return index
-        }
-      }
-
-      return -1
+    function getOrderElement(element) {
+      return orders.find((o) => o.selector.test(element))
     }
     const documentFragment =
       context.parserServices.getDocumentFragment &&
@@ -156,18 +135,21 @@ module.exports = {
           return
         }
         const elements = getTopLevelHTMLElements()
+
+        const elementWithOrders = elements.flatMap((element) => {
+          const order = getOrderElement(element)
+          return order ? [{ order, element }] : []
+        })
         const sourceCode = context.getSourceCode()
-        elements.forEach((element, index) => {
-          const expectedIndex = getOrderPosition(element)
-          if (expectedIndex < 0) {
-            return
-          }
-          const firstUnordered = elements
+        elementWithOrders.forEach(({ order: expected, element }, index) => {
+          const firstUnordered = elementWithOrders
             .slice(0, index)
-            .filter((e) => expectedIndex < getOrderPosition(e))
-            .sort((e1, e2) => getOrderPosition(e1) - getOrderPosition(e2))[0]
+            .filter(({ order }) => expected.index < order.index)
+            .sort((e1, e2) => e1.order.index - e2.order.index)[0]
           if (firstUnordered) {
-            const firstUnorderedttributes = getAttributeString(firstUnordered)
+            const firstUnorderedAttributes = getAttributeString(
+              firstUnordered.element
+            )
             const elementAttributes = getAttributeString(element)
 
             context.report({
@@ -179,16 +161,16 @@ module.exports = {
                 elementAttributes: elementAttributes
                   ? ` ${elementAttributes}`
                   : '',
-                firstUnorderedName: firstUnordered.name,
-                firstUnorderedAttributes: firstUnorderedttributes
-                  ? ` ${firstUnorderedttributes}`
+                firstUnorderedName: firstUnordered.element.name,
+                firstUnorderedAttributes: firstUnorderedAttributes
+                  ? ` ${firstUnorderedAttributes}`
                   : '',
-                line: firstUnordered.loc.start.line
+                line: firstUnordered.element.loc.start.line
               },
               *fix(fixer) {
                 // insert element before firstUnordered
                 const fixedElements = elements.flatMap((it) => {
-                  if (it === firstUnordered) {
+                  if (it === firstUnordered.element) {
                     return [element, it]
                   } else if (it === element) {
                     return []
diff --git a/lib/rules/match-component-import-name.js b/lib/rules/match-component-import-name.js
new file mode 100644
index 00000000..5babc8dc
--- /dev/null
+++ b/lib/rules/match-component-import-name.js
@@ -0,0 +1,79 @@
+/**
+ * @author Doug Wade <douglas.b.wade@gmail.com>
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const utils = require('../utils')
+const casing = require('../utils/casing')
+
+module.exports = {
+  meta: {
+    type: 'problem',
+    schema: [],
+    docs: {
+      description:
+        'require the registered component name to match the imported component name',
+      categories: undefined,
+      url: 'https://eslint.vuejs.org/rules/match-component-import-name.html'
+    },
+    fixable: null,
+    messages: {
+      unexpected:
+        'Component alias {{importedName}} should be one of: {{expectedName}}.'
+    }
+  },
+  /**
+   * @param {RuleContext} context
+   * @returns {RuleListener}
+   */
+  create(context) {
+    /**
+     * @param {Identifier} identifier
+     * @return {Array<String>}
+     */
+    function getExpectedNames(identifier) {
+      return [
+        casing.pascalCase(identifier.name),
+        casing.kebabCase(identifier.name)
+      ]
+    }
+
+    return utils.executeOnVueComponent(context, (obj) => {
+      const components = utils.findProperty(obj, 'components')
+      if (
+        !components ||
+        !components.value ||
+        components.value.type !== 'ObjectExpression'
+      ) {
+        return
+      }
+
+      components.value.properties.forEach(
+        /** @param {Property | SpreadElement} property */
+        (property) => {
+          if (
+            property.type === 'SpreadElement' ||
+            property.value.type !== 'Identifier' ||
+            property.computed === true
+          ) {
+            return
+          }
+
+          const importedName = utils.getStaticPropertyName(property) || ''
+          const expectedNames = getExpectedNames(property.value)
+          if (!expectedNames.includes(importedName)) {
+            context.report({
+              node: property,
+              messageId: 'unexpected',
+              data: {
+                importedName,
+                expectedName: expectedNames.join(', ')
+              }
+            })
+          }
+        }
+      )
+    })
+  }
+}
diff --git a/lib/rules/multi-word-component-names.js b/lib/rules/multi-word-component-names.js
index 4e4e7db2..6f73de29 100644
--- a/lib/rules/multi-word-component-names.js
+++ b/lib/rules/multi-word-component-names.js
@@ -60,7 +60,7 @@ module.exports = {
         ignores.add(casing.kebabCase(ignore))
       }
     }
-    let hasVue = false
+    let hasVue = utils.isScriptSetup(context)
     let hasName = false
 
     /**
diff --git a/lib/utils/selector.js b/lib/utils/selector.js
new file mode 100644
index 00000000..6c90e9ca
--- /dev/null
+++ b/lib/utils/selector.js
@@ -0,0 +1,605 @@
+'use strict'
+
+const parser = require('postcss-selector-parser')
+const { default: nthCheck } = require('nth-check')
+const { getAttribute, isVElement } = require('.')
+
+/**
+ * @typedef {object} VElementSelector
+ * @property {(element: VElement)=>boolean} test
+ */
+
+module.exports = {
+  parseSelector
+}
+
+/**
+ * Parses CSS selectors and returns an object with a function that tests VElement.
+ * @param {string} selector CSS selector
+ * @param {RuleContext} context - The rule context.
+ * @returns {VElementSelector}
+ */
+function parseSelector(selector, context) {
+  let astSelector
+  try {
+    astSelector = parser().astSync(selector)
+  } catch (e) {
+    context.report({
+      loc: { line: 0, column: 0 },
+      message: `Cannot parse selector: ${selector}.`
+    })
+    return {
+      test: () => false
+    }
+  }
+
+  try {
+    const test = selectorsToVElementMatcher(astSelector.nodes)
+
+    return {
+      test(element) {
+        return test(element, null)
+      }
+    }
+  } catch (e) {
+    if (e instanceof SelectorError) {
+      context.report({
+        loc: { line: 0, column: 0 },
+        message: e.message
+      })
+      return {
+        test: () => false
+      }
+    }
+    throw e
+  }
+}
+
+class SelectorError extends Error {}
+
+/**
+ * @typedef {(element: VElement, subject: VElement | null )=>boolean} VElementMatcher
+ * @typedef {Exclude<parser.Selector['nodes'][number], {type:'comment'|'root'}>} ChildNode
+ */
+
+/**
+ * Convert nodes to VElementMatcher
+ * @param {parser.Selector[]} selectorNodes
+ * @returns {VElementMatcher}
+ */
+function selectorsToVElementMatcher(selectorNodes) {
+  const selectors = selectorNodes.map((n) =>
+    selectorToVElementMatcher(cleanSelectorChildren(n))
+  )
+  return (element, subject) => selectors.some((sel) => sel(element, subject))
+}
+
+/**
+ * Clean and get the selector child nodes.
+ * @param {parser.Selector} selector
+ * @returns {ChildNode[]}
+ */
+function cleanSelectorChildren(selector) {
+  /** @type {ChildNode[]} */
+  const nodes = []
+  /** @type {ChildNode|null} */
+  let last = null
+  for (const node of selector.nodes) {
+    if (node.type === 'root') {
+      throw new SelectorError('Unexpected state type=root')
+    }
+    if (node.type === 'comment') {
+      continue
+    }
+    if (
+      (last == null || last.type === 'combinator') &&
+      isDescendantCombinator(node)
+    ) {
+      // Ignore descendant combinator
+      continue
+    }
+    if (isDescendantCombinator(last) && node.type === 'combinator') {
+      // Replace combinator
+      nodes.pop()
+    }
+    nodes.push(node)
+    last = node
+  }
+  if (isDescendantCombinator(last)) {
+    nodes.pop()
+  }
+  return nodes
+
+  /**
+   * @param {parser.Node|null} node
+   * @returns {node is parser.Combinator}
+   */
+  function isDescendantCombinator(node) {
+    return Boolean(node && node.type === 'combinator' && !node.value.trim())
+  }
+}
+/**
+ * Convert Selector child nodes to VElementMatcher
+ * @param {ChildNode[]} selectorChildren
+ * @returns {VElementMatcher}
+ */
+function selectorToVElementMatcher(selectorChildren) {
+  const nodes = [...selectorChildren]
+  let node = nodes.shift()
+  /**
+   * @type {VElementMatcher | null}
+   */
+  let result = null
+  while (node) {
+    if (node.type === 'combinator') {
+      const combinator = node.value
+      node = nodes.shift()
+      if (!node) {
+        throw new SelectorError(`Expected selector after '${combinator}'.`)
+      }
+      if (node.type === 'combinator') {
+        throw new SelectorError(`Unexpected combinator '${node.value}'.`)
+      }
+      const right = nodeToVElementMatcher(node)
+      result = combination(
+        result ||
+          // for :has()
+          ((element, subject) => element === subject),
+        combinator,
+        right
+      )
+    } else {
+      const sel = nodeToVElementMatcher(node)
+      result = result ? compound(result, sel) : sel
+    }
+    node = nodes.shift()
+  }
+  if (!result) {
+    throw new SelectorError(`Unexpected empty selector.`)
+  }
+  return result
+}
+
+/**
+ * @param {VElementMatcher} left
+ * @param {string} combinator
+ * @param {VElementMatcher} right
+ * @returns {VElementMatcher}
+ */
+function combination(left, combinator, right) {
+  switch (combinator.trim()) {
+    case '':
+      // descendant
+      return (element, subject) => {
+        if (right(element, null)) {
+          let parent = element.parent
+          while (parent.type === 'VElement') {
+            if (left(parent, subject)) {
+              return true
+            }
+            parent = parent.parent
+          }
+        }
+        return false
+      }
+    case '>':
+      // child
+      return (element, subject) => {
+        if (right(element, null)) {
+          const parent = element.parent
+          if (parent.type === 'VElement') {
+            return left(parent, subject)
+          }
+        }
+        return false
+      }
+    case '+':
+      // adjacent
+      return (element, subject) => {
+        if (right(element, null)) {
+          const before = getBeforeElement(element)
+          if (before) {
+            return left(before, subject)
+          }
+        }
+        return false
+      }
+    case '~':
+      // sibling
+      return (element, subject) => {
+        if (right(element, null)) {
+          for (const before of getBeforeElements(element)) {
+            if (left(before, subject)) {
+              return true
+            }
+          }
+        }
+        return false
+      }
+    default:
+      throw new SelectorError(`Unknown combinator: ${combinator}.`)
+  }
+}
+
+/**
+ * Convert node to VElementMatcher
+ * @param {Exclude<parser.Node, {type:'combinator'|'comment'|'root'|'selector'}>} selector
+ * @returns {VElementMatcher}
+ */
+function nodeToVElementMatcher(selector) {
+  switch (selector.type) {
+    case 'attribute':
+      return attributeNodeToVElementMatcher(selector)
+    case 'class':
+      return classNameNodeToVElementMatcher(selector)
+    case 'id':
+      return identifierNodeToVElementMatcher(selector)
+    case 'tag':
+      return tagNodeToVElementMatcher(selector)
+    case 'universal':
+      return universalNodeToVElementMatcher(selector)
+    case 'pseudo':
+      return pseudoNodeToVElementMatcher(selector)
+    case 'nesting':
+      throw new SelectorError('Unsupported nesting selector.')
+    case 'string':
+      throw new SelectorError(`Unknown selector: ${selector.value}.`)
+    default:
+      throw new SelectorError(
+        `Unknown selector: ${/** @type {any}*/ (selector).value}.`
+      )
+  }
+}
+
+/**
+ * Convert Attribute node to VElementMatcher
+ * @param {parser.Attribute} selector
+ * @returns {VElementMatcher}
+ */
+function attributeNodeToVElementMatcher(selector) {
+  const key = selector.attribute
+  if (!selector.operator) {
+    return (element) => getAttributeValue(element, key) != null
+  }
+  const value = selector.value || ''
+
+  switch (selector.operator) {
+    case '=':
+      return buildVElementMatcher(value, (attr, val) => attr === val)
+    case '~=':
+      // words
+      return buildVElementMatcher(value, (attr, val) =>
+        attr.split(/\s+/gu).includes(val)
+      )
+    case '|=':
+      // immediately followed by hyphen
+      return buildVElementMatcher(
+        value,
+        (attr, val) => attr === val || attr.startsWith(`${val}-`)
+      )
+    case '^=':
+      // prefixed
+      return buildVElementMatcher(value, (attr, val) => attr.startsWith(val))
+    case '$=':
+      // suffixed
+      return buildVElementMatcher(value, (attr, val) => attr.endsWith(val))
+    case '*=':
+      // contains
+      return buildVElementMatcher(value, (attr, val) => attr.includes(val))
+    default:
+      throw new SelectorError(`Unsupported operator: ${selector.operator}.`)
+  }
+
+  /**
+   * @param {string} selectorValue
+   * @param {(attrValue:string, selectorValue: string)=>boolean} test
+   * @returns {VElementMatcher}
+   */
+  function buildVElementMatcher(selectorValue, test) {
+    const val = selector.insensitive
+      ? selectorValue.toLowerCase()
+      : selectorValue
+    return (element) => {
+      const attrValue = getAttributeValue(element, key)
+      if (attrValue == null) {
+        return false
+      }
+      return test(
+        selector.insensitive ? attrValue.toLowerCase() : attrValue,
+        val
+      )
+    }
+  }
+}
+
+/**
+ * Convert ClassName node to VElementMatcher
+ * @param {parser.ClassName} selector
+ * @returns {VElementMatcher}
+ */
+function classNameNodeToVElementMatcher(selector) {
+  const className = selector.value
+  return (element) => {
+    const attrValue = getAttributeValue(element, 'class')
+    if (attrValue == null) {
+      return false
+    }
+    return attrValue.split(/\s+/gu).includes(className)
+  }
+}
+
+/**
+ * Convert Identifier node to VElementMatcher
+ * @param {parser.Identifier} selector
+ * @returns {VElementMatcher}
+ */
+function identifierNodeToVElementMatcher(selector) {
+  const id = selector.value
+  return (element) => {
+    const attrValue = getAttributeValue(element, 'id')
+    if (attrValue == null) {
+      return false
+    }
+    return attrValue === id
+  }
+}
+
+/**
+ * Convert Tag node to VElementMatcher
+ * @param {parser.Tag} selector
+ * @returns {VElementMatcher}
+ */
+function tagNodeToVElementMatcher(selector) {
+  const name = selector.value
+  return (element) => element.rawName === name
+}
+
+/**
+ * Convert Universal node to VElementMatcher
+ * @param {parser.Universal} _selector
+ * @returns {VElementMatcher}
+ */
+function universalNodeToVElementMatcher(_selector) {
+  return () => true
+}
+/**
+ * Convert Pseudo node to VElementMatcher
+ * @param {parser.Pseudo} selector
+ * @returns {VElementMatcher}
+ */
+function pseudoNodeToVElementMatcher(selector) {
+  const pseudo = selector.value
+  switch (pseudo) {
+    case ':not': {
+      // https://developer.mozilla.org/en-US/docs/Web/CSS/:not
+      const selectors = selectorsToVElementMatcher(selector.nodes)
+      return (element, subject) => {
+        return !selectors(element, subject)
+      }
+    }
+    case ':is':
+    case ':where':
+      // https://developer.mozilla.org/en-US/docs/Web/CSS/:is
+      // https://developer.mozilla.org/en-US/docs/Web/CSS/:where
+      return selectorsToVElementMatcher(selector.nodes)
+    case ':has':
+      // https://developer.mozilla.org/en-US/docs/Web/CSS/:has
+      return pseudoHasSelectorsToVElementMatcher(selector.nodes)
+    case ':empty':
+      // https://developer.mozilla.org/en-US/docs/Web/CSS/:empty
+      return (element) =>
+        element.children.every(
+          (child) => child.type === 'VText' && !child.value.trim()
+        )
+    case ':nth-child': {
+      // https://developer.mozilla.org/en-US/docs/Web/CSS/:nth-child
+      const nth = parseNth(selector)
+      return buildPseudoNthVElementMatcher(nth)
+    }
+    case ':nth-last-child': {
+      // https://developer.mozilla.org/en-US/docs/Web/CSS/:nth-last-child
+      const nth = parseNth(selector)
+      return buildPseudoNthVElementMatcher((index, length) =>
+        nth(length - index - 1)
+      )
+    }
+    case ':first-child':
+      // https://developer.mozilla.org/en-US/docs/Web/CSS/:first-child
+      return buildPseudoNthVElementMatcher((index) => index === 0)
+    case ':last-child':
+      // https://developer.mozilla.org/en-US/docs/Web/CSS/:last-child
+      return buildPseudoNthVElementMatcher(
+        (index, length) => index === length - 1
+      )
+    case ':only-child':
+      // https://developer.mozilla.org/en-US/docs/Web/CSS/:only-child
+      return buildPseudoNthVElementMatcher(
+        (index, length) => index === 0 && length === 1
+      )
+    case ':nth-of-type': {
+      // https://developer.mozilla.org/en-US/docs/Web/CSS/:nth-of-type
+      const nth = parseNth(selector)
+      return buildPseudoNthOfTypeVElementMatcher(nth)
+    }
+    case ':nth-last-of-type': {
+      // https://developer.mozilla.org/en-US/docs/Web/CSS/:nth-last-of-type
+      const nth = parseNth(selector)
+      return buildPseudoNthOfTypeVElementMatcher((index, length) =>
+        nth(length - index - 1)
+      )
+    }
+    case ':first-of-type':
+      // https://developer.mozilla.org/en-US/docs/Web/CSS/:first-of-type
+      return buildPseudoNthOfTypeVElementMatcher((index) => index === 0)
+    case ':last-of-type':
+      // https://developer.mozilla.org/en-US/docs/Web/CSS/:last-of-type
+      return buildPseudoNthOfTypeVElementMatcher(
+        (index, length) => index === length - 1
+      )
+    case ':only-of-type':
+      // https://developer.mozilla.org/en-US/docs/Web/CSS/:only-of-type
+      return buildPseudoNthOfTypeVElementMatcher(
+        (index, length) => index === 0 && length === 1
+      )
+    default:
+      throw new SelectorError(`Unsupported pseudo selector: ${pseudo}.`)
+  }
+}
+
+/**
+ * Convert :has() selector nodes to VElementMatcher
+ * @param {parser.Selector[]} selectorNodes
+ * @returns {VElementMatcher}
+ */
+function pseudoHasSelectorsToVElementMatcher(selectorNodes) {
+  const selectors = selectorNodes.map((n) =>
+    pseudoHasSelectorToVElementMatcher(n)
+  )
+  return (element, subject) => selectors.some((sel) => sel(element, subject))
+}
+/**
+ * Convert :has() selector node to VElementMatcher
+ * @param {parser.Selector} selector
+ * @returns {VElementMatcher}
+ */
+function pseudoHasSelectorToVElementMatcher(selector) {
+  const nodes = cleanSelectorChildren(selector)
+  const selectors = selectorToVElementMatcher(nodes)
+  const firstNode = nodes[0]
+  if (
+    firstNode.type === 'combinator' &&
+    (firstNode.value === '+' || firstNode.value === '~')
+  ) {
+    // adjacent or sibling
+    return buildVElementMatcher(selectors, (element) =>
+      getAfterElements(element)
+    )
+  }
+  // descendant or child
+  return buildVElementMatcher(selectors, (element) =>
+    element.children.filter(isVElement)
+  )
+
+  /**
+   * @param {VElementMatcher} selectors
+   * @param {(element: VElement) => VElement[]} getStartElements
+   * @returns {VElementMatcher}
+   */
+  function buildVElementMatcher(selectors, getStartElements) {
+    return (element) => {
+      const elements = [...getStartElements(element)]
+      /** @type {VElement|undefined} */
+      let curr
+      while ((curr = elements.shift())) {
+        const el = curr
+        if (selectors(el, element)) {
+          return true
+        }
+        elements.push(...el.children.filter(isVElement))
+      }
+      return false
+    }
+  }
+}
+
+/**
+ * Parse <nth>
+ * @param {parser.Pseudo} pseudoNode
+ * @returns {(index: number)=>boolean}
+ */
+function parseNth(pseudoNode) {
+  const argumentsText = pseudoNode
+    .toString()
+    .slice(pseudoNode.value.length)
+    .toLowerCase()
+  const openParenIndex = argumentsText.indexOf('(')
+  const closeParenIndex = argumentsText.lastIndexOf(')')
+  if (openParenIndex < 0 || closeParenIndex < 0) {
+    throw new SelectorError(
+      `Cannot parse An+B micro syntax (:nth-xxx() argument): ${argumentsText}.`
+    )
+  }
+
+  const argument = argumentsText
+    .slice(openParenIndex + 1, closeParenIndex)
+    .trim()
+  try {
+    return nthCheck(argument)
+  } catch (e) {
+    throw new SelectorError(
+      `Cannot parse An+B micro syntax (:nth-xxx() argument): '${argument}'.`
+    )
+  }
+}
+
+/**
+ * Build VElementMatcher for :nth-xxx()
+ * @param {(index: number, length: number)=>boolean} testIndex
+ * @returns {VElementMatcher}
+ */
+function buildPseudoNthVElementMatcher(testIndex) {
+  return (element) => {
+    const elements = element.parent.children.filter(isVElement)
+    return testIndex(elements.indexOf(element), elements.length)
+  }
+}
+
+/**
+ * Build VElementMatcher for :nth-xxx-of-type()
+ * @param {(index: number, length: number)=>boolean} testIndex
+ * @returns {VElementMatcher}
+ */
+function buildPseudoNthOfTypeVElementMatcher(testIndex) {
+  return (element) => {
+    const elements = element.parent.children
+      .filter(isVElement)
+      .filter((e) => e.rawName === element.rawName)
+    return testIndex(elements.indexOf(element), elements.length)
+  }
+}
+
+/**
+ * @param {VElement} element
+ */
+function getBeforeElement(element) {
+  return getBeforeElements(element).pop() || null
+}
+/**
+ * @param {VElement} element
+ */
+function getBeforeElements(element) {
+  const parent = element.parent
+  const index = parent.children.indexOf(element)
+  return parent.children.slice(0, index).filter(isVElement)
+}
+
+/**
+ * @param {VElement} element
+ */
+function getAfterElements(element) {
+  const parent = element.parent
+  const index = parent.children.indexOf(element)
+  return parent.children.slice(index + 1).filter(isVElement)
+}
+
+/**
+ * @param {VElementMatcher} a
+ * @param {VElementMatcher} b
+ * @returns {VElementMatcher}
+ */
+function compound(a, b) {
+  return (element, subject) => a(element, subject) && b(element, subject)
+}
+
+/**
+ * Get attribute value from given element.
+ * @param {VElement} element The element node.
+ * @param {string} attribute The attribute name.
+ */
+function getAttributeValue(element, attribute) {
+  const attr = getAttribute(element, attribute)
+  if (attr) {
+    return (attr.value && attr.value.value) || ''
+  }
+  return null
+}
diff --git a/package.json b/package.json
index 4bc07c92..9ddbb72a 100644
--- a/package.json
+++ b/package.json
@@ -56,6 +56,7 @@
   "dependencies": {
     "eslint-utils": "^3.0.0",
     "natural-compare": "^1.4.0",
+    "nth-check": "^2.0.1",
     "postcss-selector-parser": "^6.0.9",
     "semver": "^7.3.5",
     "vue-eslint-parser": "^8.0.1"


Test patch:
diff --git a/tests/fixtures/utils/selector/attr-contains/result.json b/tests/fixtures/utils/selector/attr-contains/result.json
new file mode 100644
index 00000000..a2627437
--- /dev/null
+++ b/tests/fixtures/utils/selector/attr-contains/result.json
@@ -0,0 +1,9 @@
+{
+    "selector": "a[href*=\"example\"]",
+    "matches": [
+        "<a href=\"http://example.com\">",
+        "<a href=\"http://example.org\">",
+        "<a href=\"https://example.org\">"
+    ],
+    "errors": []
+}
\ No newline at end of file
diff --git a/tests/fixtures/utils/selector/attr-contains/source.vue b/tests/fixtures/utils/selector/attr-contains/source.vue
new file mode 100644
index 00000000..6c9a89dd
--- /dev/null
+++ b/tests/fixtures/utils/selector/attr-contains/source.vue
@@ -0,0 +1,10 @@
+<!-- a[href*="example"] -->
+<template>
+  <ul>
+    <li><a href="#internal">Internal link</a></li>
+    <li><a href="http://example.com">Example link</a></li>
+    <li><a href="#InSensitive">Insensitive internal link</a></li>
+    <li><a href="http://example.org">Example org link</a></li>
+    <li><a href="https://example.org">Example https org link</a></li>
+  </ul>
+</template>
diff --git a/tests/fixtures/utils/selector/attr-empty-value/result.json b/tests/fixtures/utils/selector/attr-empty-value/result.json
new file mode 100644
index 00000000..1f304292
--- /dev/null
+++ b/tests/fixtures/utils/selector/attr-empty-value/result.json
@@ -0,0 +1,8 @@
+{
+    "selector": "[foo=]",
+    "matches": [
+        "<p foo>",
+        "<div foo=\"\">"
+    ],
+    "errors": []
+}
\ No newline at end of file
diff --git a/tests/fixtures/utils/selector/attr-empty-value/source.vue b/tests/fixtures/utils/selector/attr-empty-value/source.vue
new file mode 100644
index 00000000..4a73c151
--- /dev/null
+++ b/tests/fixtures/utils/selector/attr-empty-value/source.vue
@@ -0,0 +1,11 @@
+<!-- [foo=] -->
+<template>
+  <div foo="foo">
+    <div foo=bar></div>
+    <div foo="foo bar"></div>
+    <p foo></p>
+    <input bar="foo">
+    <input foo="foo">
+    <div foo=""></div>
+  </div>
+</template>
diff --git a/tests/fixtures/utils/selector/attr-eq/result.json b/tests/fixtures/utils/selector/attr-eq/result.json
new file mode 100644
index 00000000..89694b04
--- /dev/null
+++ b/tests/fixtures/utils/selector/attr-eq/result.json
@@ -0,0 +1,8 @@
+{
+    "selector": "[foo=foo]",
+    "matches": [
+        "<div foo=\"foo\">",
+        "<input foo=\"foo\">"
+    ],
+    "errors": []
+}
\ No newline at end of file
diff --git a/tests/fixtures/utils/selector/attr-eq/source.vue b/tests/fixtures/utils/selector/attr-eq/source.vue
new file mode 100644
index 00000000..fdfe34e2
--- /dev/null
+++ b/tests/fixtures/utils/selector/attr-eq/source.vue
@@ -0,0 +1,11 @@
+<!-- [foo=foo] -->
+<template>
+  <div foo="foo">
+    <div foo=bar></div>
+    <div foo="foo bar"></div>
+    <p foo></p>
+    <input bar="foo">
+    <input foo="foo">
+    <div foo=""></div>
+  </div>
+</template>
diff --git a/tests/fixtures/utils/selector/attr-has/result.json b/tests/fixtures/utils/selector/attr-has/result.json
new file mode 100644
index 00000000..6af6140b
--- /dev/null
+++ b/tests/fixtures/utils/selector/attr-has/result.json
@@ -0,0 +1,8 @@
+{
+    "selector": "[foo]",
+    "matches": [
+        "<div foo=\"foo\">",
+        "<p foo>"
+    ],
+    "errors": []
+}
\ No newline at end of file
diff --git a/tests/fixtures/utils/selector/attr-has/source.vue b/tests/fixtures/utils/selector/attr-has/source.vue
new file mode 100644
index 00000000..aaf7b1dd
--- /dev/null
+++ b/tests/fixtures/utils/selector/attr-has/source.vue
@@ -0,0 +1,8 @@
+<!-- [foo] -->
+<template>
+  <div foo="foo">
+    <div bar></div>
+    <p foo></p>
+    <input bar="foo">
+  </div>
+</template>
diff --git a/tests/fixtures/utils/selector/attr-insensitive/result.json b/tests/fixtures/utils/selector/attr-insensitive/result.json
new file mode 100644
index 00000000..5d630529
--- /dev/null
+++ b/tests/fixtures/utils/selector/attr-insensitive/result.json
@@ -0,0 +1,7 @@
+{
+    "selector": "a[href*=\"insensitive\" i]",
+    "matches": [
+        "<a href=\"#InSensitive\">"
+    ],
+    "errors": []
+}
\ No newline at end of file
diff --git a/tests/fixtures/utils/selector/attr-insensitive/source.vue b/tests/fixtures/utils/selector/attr-insensitive/source.vue
new file mode 100644
index 00000000..129fcab0
--- /dev/null
+++ b/tests/fixtures/utils/selector/attr-insensitive/source.vue
@@ -0,0 +1,10 @@
+<!-- a[href*="insensitive" i] -->
+<template>
+  <ul>
+    <li><a href="#internal">Internal link</a></li>
+    <li><a href="http://example.com">Example link</a></li>
+    <li><a href="#InSensitive">Insensitive internal link</a></li>
+    <li><a href="http://example.org">Example org link</a></li>
+    <li><a href="https://example.org">Example https org link</a></li>
+  </ul>
+</template>
diff --git a/tests/fixtures/utils/selector/attr-lang/result.json b/tests/fixtures/utils/selector/attr-lang/result.json
new file mode 100644
index 00000000..f8c9d266
--- /dev/null
+++ b/tests/fixtures/utils/selector/attr-lang/result.json
@@ -0,0 +1,9 @@
+{
+    "selector": "div[lang|=\"zh\"]",
+    "matches": [
+        "<div lang=\"zh\">",
+        "<div lang=\"zh-CN\">",
+        "<div lang=\"zh-TW\">"
+    ],
+    "errors": []
+}
\ No newline at end of file
diff --git a/tests/fixtures/utils/selector/attr-lang/source.vue b/tests/fixtures/utils/selector/attr-lang/source.vue
new file mode 100644
index 00000000..fa88b875
--- /dev/null
+++ b/tests/fixtures/utils/selector/attr-lang/source.vue
@@ -0,0 +1,9 @@
+<!-- div[lang|="zh"] -->
+<template>
+  <div lang="en-us en-gb en-au en-nz">Hello World!</div>
+  <div lang="pt">Olá Mundo!</div>
+  <div lang="zh">世界您好！</div>
+  <div lang="zh-CN">世界您好！</div>
+  <div lang="zh-TW">世界您好！</div>
+  <div data-lang="zh-TW">世界您好！</div>
+</template>
diff --git a/tests/fixtures/utils/selector/attr-not-insensitive/result.json b/tests/fixtures/utils/selector/attr-not-insensitive/result.json
new file mode 100644
index 00000000..41f6b6dd
--- /dev/null
+++ b/tests/fixtures/utils/selector/attr-not-insensitive/result.json
@@ -0,0 +1,5 @@
+{
+    "selector": "a[href*=\"insensitive\"]",
+    "matches": [],
+    "errors": []
+}
\ No newline at end of file
diff --git a/tests/fixtures/utils/selector/attr-not-insensitive/source.vue b/tests/fixtures/utils/selector/attr-not-insensitive/source.vue
new file mode 100644
index 00000000..2fb3e903
--- /dev/null
+++ b/tests/fixtures/utils/selector/attr-not-insensitive/source.vue
@@ -0,0 +1,11 @@
+<!-- a[href*="insensitive"] -->
+<template>
+  <!-- Test for not insensitive -->
+  <ul>
+    <li><a href="#internal">Internal link</a></li>
+    <li><a href="http://example.com">Example link</a></li>
+    <li><a href="#InSensitive">Insensitive internal link</a></li>
+    <li><a href="http://example.org">Example org link</a></li>
+    <li><a href="https://example.org">Example https org link</a></li>
+  </ul>
+</template>
diff --git a/tests/fixtures/utils/selector/attr-prefixed/result.json b/tests/fixtures/utils/selector/attr-prefixed/result.json
new file mode 100644
index 00000000..9f11bd4e
--- /dev/null
+++ b/tests/fixtures/utils/selector/attr-prefixed/result.json
@@ -0,0 +1,8 @@
+{
+    "selector": "a[href^=\"#\"]",
+    "matches": [
+        "<a href=\"#internal\">",
+        "<a href=\"#InSensitive\">"
+    ],
+    "errors": []
+}
\ No newline at end of file
diff --git a/tests/fixtures/utils/selector/attr-prefixed/source.vue b/tests/fixtures/utils/selector/attr-prefixed/source.vue
new file mode 100644
index 00000000..2c1dc2eb
--- /dev/null
+++ b/tests/fixtures/utils/selector/attr-prefixed/source.vue
@@ -0,0 +1,10 @@
+<!-- a[href^="#"] -->
+<template>
+  <ul>
+    <li><a href="#internal">Internal link</a></li>
+    <li><a href="http://example.com">Example link</a></li>
+    <li><a href="#InSensitive">Insensitive internal link</a></li>
+    <li><a href="http://example.org">Example org link</a></li>
+    <li><a href="https://example.org">Example https org link</a></li>
+  </ul>
+</template>
diff --git a/tests/fixtures/utils/selector/attr-suffixed/result.json b/tests/fixtures/utils/selector/attr-suffixed/result.json
new file mode 100644
index 00000000..d1a9e622
--- /dev/null
+++ b/tests/fixtures/utils/selector/attr-suffixed/result.json
@@ -0,0 +1,8 @@
+{
+    "selector": "a[href$=\".org\"]",
+    "matches": [
+        "<a href=\"http://example.org\">",
+        "<a href=\"https://example.org\">"
+    ],
+    "errors": []
+}
\ No newline at end of file
diff --git a/tests/fixtures/utils/selector/attr-suffixed/source.vue b/tests/fixtures/utils/selector/attr-suffixed/source.vue
new file mode 100644
index 00000000..7b27ad2e
--- /dev/null
+++ b/tests/fixtures/utils/selector/attr-suffixed/source.vue
@@ -0,0 +1,10 @@
+<!-- a[href$=".org"] -->
+<template>
+  <ul>
+    <li><a href="#internal">Internal link</a></li>
+    <li><a href="http://example.com">Example link</a></li>
+    <li><a href="#InSensitive">Insensitive internal link</a></li>
+    <li><a href="http://example.org">Example org link</a></li>
+    <li><a href="https://example.org">Example https org link</a></li>
+  </ul>
+</template>
diff --git a/tests/fixtures/utils/selector/attr-words/result.json b/tests/fixtures/utils/selector/attr-words/result.json
new file mode 100644
index 00000000..3fdea22c
--- /dev/null
+++ b/tests/fixtures/utils/selector/attr-words/result.json
@@ -0,0 +1,8 @@
+{
+    "selector": "div[lang~=\"en-us\"]",
+    "matches": [
+        "<div lang=\"en-us en-gb en-au en-nz\">",
+        "<div lang=\"en-us\">"
+    ],
+    "errors": []
+}
\ No newline at end of file
diff --git a/tests/fixtures/utils/selector/attr-words/source.vue b/tests/fixtures/utils/selector/attr-words/source.vue
new file mode 100644
index 00000000..c9558519
--- /dev/null
+++ b/tests/fixtures/utils/selector/attr-words/source.vue
@@ -0,0 +1,11 @@
+<!-- div[lang~="en-us"] -->
+<template>
+  <div lang="en-us en-gb en-au en-nz">Hello World!</div>
+  <div lang="en-us">Hello World!</div>
+  <div lang="en-us-unknown">Unknown</div>
+  <div lang="pt">Olá Mundo!</div>
+  <div lang="zh">世界您好！</div>
+  <div lang="zh-CN">世界您好！</div>
+  <div lang="zh-TW">世界您好！</div>
+  <div data-lang="zh-TW">世界您好！</div>
+</template>
diff --git a/tests/fixtures/utils/selector/class/result.json b/tests/fixtures/utils/selector/class/result.json
new file mode 100644
index 00000000..6fd93bcd
--- /dev/null
+++ b/tests/fixtures/utils/selector/class/result.json
@@ -0,0 +1,8 @@
+{
+    "selector": ".foo",
+    "matches": [
+        "<div class=\"foo\">",
+        "<input class=\"foo bar\">"
+    ],
+    "errors": []
+}
\ No newline at end of file
diff --git a/tests/fixtures/utils/selector/class/source.vue b/tests/fixtures/utils/selector/class/source.vue
new file mode 100644
index 00000000..b9616a44
--- /dev/null
+++ b/tests/fixtures/utils/selector/class/source.vue
@@ -0,0 +1,8 @@
+<!-- .foo -->
+<template>
+  <div class="foo">
+    <div class="f"></div>
+    <p class="bar"></p>
+    <input class="foo bar">
+  </div>
+</template>
diff --git a/tests/fixtures/utils/selector/combinator-adjacent/result.json b/tests/fixtures/utils/selector/combinator-adjacent/result.json
new file mode 100644
index 00000000..ef2e8413
--- /dev/null
+++ b/tests/fixtures/utils/selector/combinator-adjacent/result.json
@@ -0,0 +1,8 @@
+{
+    "selector": ".foo+.bar",
+    "matches": [
+        "<div class=\"bar 3\">",
+        "<div class=\"bar 4\">"
+    ],
+    "errors": []
+}
\ No newline at end of file
diff --git a/tests/fixtures/utils/selector/combinator-adjacent/source.vue b/tests/fixtures/utils/selector/combinator-adjacent/source.vue
new file mode 100644
index 00000000..489c0d7e
--- /dev/null
+++ b/tests/fixtures/utils/selector/combinator-adjacent/source.vue
@@ -0,0 +1,14 @@
+<!-- .foo+.bar -->
+<template>
+  <div class="bar">
+    <div class="foo">
+      <div class="foo 2">
+        <div class="bar 2"></div>
+      </div>
+      <div class="bar 3"></div>
+    </div>
+    <div class="foo 3"></div>
+    <div class="bar 4"></div>
+    <div class="bar 5"></div>
+  </div>
+</template>
diff --git a/tests/fixtures/utils/selector/combinator-child01/result.json b/tests/fixtures/utils/selector/combinator-child01/result.json
new file mode 100644
index 00000000..841f6965
--- /dev/null
+++ b/tests/fixtures/utils/selector/combinator-child01/result.json
@@ -0,0 +1,9 @@
+{
+    "selector": ".foo > .bar",
+    "matches": [
+        "<div class=\"bar 2\">",
+        "<div class=\"bar 3\">",
+        "<div class=\"bar\" exact>"
+    ],
+    "errors": []
+}
\ No newline at end of file
diff --git a/tests/fixtures/utils/selector/combinator-child01/source.vue b/tests/fixtures/utils/selector/combinator-child01/source.vue
new file mode 100644
index 00000000..5eaa8df6
--- /dev/null
+++ b/tests/fixtures/utils/selector/combinator-child01/source.vue
@@ -0,0 +1,14 @@
+<!-- .foo > .bar -->
+<template>
+  <div class="bar">
+    <div class="foo">
+      <div class="foo 2">
+        <div class="bar 2"></div>
+      </div>
+      <div class="bar 3">
+        <div class="bar 4"></div>
+      </div>
+      <div class="bar" exact></div>
+    </div>
+  </div>
+</template>
diff --git a/tests/fixtures/utils/selector/combinator-child02/result.json b/tests/fixtures/utils/selector/combinator-child02/result.json
new file mode 100644
index 00000000..ca0084bb
--- /dev/null
+++ b/tests/fixtures/utils/selector/combinator-child02/result.json
@@ -0,0 +1,10 @@
+{
+    "selector": ".a > .b > .c",
+    "matches": [
+        "<div class=\"c a-a-b-c\">",
+        "<div class=\"c a-b-c\">",
+        "<div class=\"c b-a-b-c\">",
+        "<div class=\"c c-a-b-c\">"
+    ],
+    "errors": []
+}
\ No newline at end of file
diff --git a/tests/fixtures/utils/selector/combinator-child02/source.vue b/tests/fixtures/utils/selector/combinator-child02/source.vue
new file mode 100644
index 00000000..cf2ede6e
--- /dev/null
+++ b/tests/fixtures/utils/selector/combinator-child02/source.vue
@@ -0,0 +1,162 @@
+<!-- .a > .b > .c -->
+<template>
+  <div class="a">
+    <div class="a a-a">
+      <div class="a a-a-a">
+        <div class="a a-a-a-a"></div>
+        <div class="b a-a-a-b"></div>
+        <div class="c a-a-a-c"></div>
+      </div>
+      <div class="b a-a-b">
+        <div class="a a-a-b-a"></div>
+        <div class="b a-a-b-b"></div>
+        <div class="c a-a-b-c"></div>
+      </div>
+      <div class="c a-a-c">
+        <div class="a a-a-c-a"></div>
+        <div class="b a-a-c-b"></div>
+        <div class="c a-a-c-c"></div>
+      </div>
+    </div>
+    <div class="b a-b">
+      <div class="a a-b-a">
+        <div class="a a-b-a-a"></div>
+        <div class="b a-b-a-b"></div>
+        <div class="c a-b-a-c"></div>
+      </div>
+      <div class="b a-b-b">
+        <div class="a a-b-b-a"></div>
+        <div class="b a-b-b-b"></div>
+        <div class="c a-b-b-c"></div>
+      </div>
+      <div class="c a-b-c">
+        <div class="a a-b-c-a"></div>
+        <div class="b a-b-c-b"></div>
+        <div class="c a-b-c-c"></div>
+      </div>
+    </div>
+    <div class="c a-c">
+      <div class="a a-c-a">
+        <div class="a a-c-a-a"></div>
+        <div class="b a-c-a-b"></div>
+        <div class="c a-c-a-c"></div>
+      </div>
+      <div class="b a-c-b">
+        <div class="a a-c-b-a"></div>
+        <div class="b a-c-b-b"></div>
+        <div class="c a-c-b-c"></div>
+      </div>
+      <div class="c a-c-c">
+        <div class="a a-c-c-a"></div>
+        <div class="b a-c-c-b"></div>
+        <div class="c a-c-c-c"></div>
+      </div>
+    </div>
+  </div>
+  <div class="b">
+    <div class="a b-a">
+      <div class="a b-a-a">
+        <div class="a b-a-a-a"></div>
+        <div class="b b-a-a-b"></div>
+        <div class="c b-a-a-c"></div>
+      </div>
+      <div class="b b-a-b">
+        <div class="a b-a-b-a"></div>
+        <div class="b b-a-b-b"></div>
+        <div class="c b-a-b-c"></div>
+      </div>
+      <div class="c b-a-c">
+        <div class="a b-a-c-a"></div>
+        <div class="b b-a-c-b"></div>
+        <div class="c b-a-c-c"></div>
+      </div>
+    </div>
+    <div class="b b-b">
+      <div class="a b-b-a">
+        <div class="a b-b-a-a"></div>
+        <div class="b b-b-a-b"></div>
+        <div class="c b-b-a-c"></div>
+      </div>
+      <div class="b b-b-b">
+        <div class="a b-b-b-a"></div>
+        <div class="b b-b-b-b"></div>
+        <div class="c b-b-b-c"></div>
+      </div>
+      <div class="c b-b-c">
+        <div class="a b-b-c-a"></div>
+        <div class="b b-b-c-b"></div>
+        <div class="c b-b-c-c"></div>
+      </div>
+    </div>
+    <div class="c b-c">
+      <div class="a b-c-a">
+        <div class="a b-c-a-a"></div>
+        <div class="b b-c-a-b"></div>
+        <div class="c b-c-a-c"></div>
+      </div>
+      <div class="b b-c-b">
+        <div class="a b-c-b-a"></div>
+        <div class="b b-c-b-b"></div>
+        <div class="c b-c-b-c"></div>
+      </div>
+      <div class="c b-c-c">
+        <div class="a b-c-c-a"></div>
+        <div class="b b-c-c-b"></div>
+        <div class="c b-c-c-c"></div>
+      </div>
+    </div>
+  </div>
+  <div class="c">
+    <div class="a c-a">
+      <div class="a c-a-a">
+        <div class="a c-a-a-a"></div>
+        <div class="b c-a-a-b"></div>
+        <div class="c c-a-a-c"></div>
+      </div>
+      <div class="b c-a-b">
+        <div class="a c-a-b-a"></div>
+        <div class="b c-a-b-b"></div>
+        <div class="c c-a-b-c"></div>
+      </div>
+      <div class="c c-a-c">
+        <div class="a c-a-c-a"></div>
+        <div class="b c-a-c-b"></div>
+        <div class="c c-a-c-c"></div>
+      </div>
+    </div>
+    <div class="b c-b">
+      <div class="a c-b-a">
+        <div class="a c-b-a-a"></div>
+        <div class="b c-b-a-b"></div>
+        <div class="c c-b-a-c"></div>
+      </div>
+      <div class="b c-b-b">
+        <div class="a c-b-b-a"></div>
+        <div class="b c-b-b-b"></div>
+        <div class="c c-b-b-c"></div>
+      </div>
+      <div class="c c-b-c">
+        <div class="a c-b-c-a"></div>
+        <div class="b c-b-c-b"></div>
+        <div class="c c-b-c-c"></div>
+      </div>
+    </div>
+    <div class="c c-c">
+      <div class="a c-c-a">
+        <div class="a c-c-a-a"></div>
+        <div class="b c-c-a-b"></div>
+        <div class="c c-c-a-c"></div>
+      </div>
+      <div class="b c-c-b">
+        <div class="a c-c-b-a"></div>
+        <div class="b c-c-b-b"></div>
+        <div class="c c-c-b-c"></div>
+      </div>
+      <div class="c c-c-c">
+        <div class="a c-c-c-a"></div>
+        <div class="b c-c-c-b"></div>
+        <div class="c c-c-c-c"></div>
+      </div>
+    </div>
+  </div>
+</template>
diff --git a/tests/fixtures/utils/selector/combinator-descendant01/result.json b/tests/fixtures/utils/selector/combinator-descendant01/result.json
new file mode 100644
index 00000000..fb3463b3
--- /dev/null
+++ b/tests/fixtures/utils/selector/combinator-descendant01/result.json
@@ -0,0 +1,10 @@
+{
+    "selector": ".foo .bar",
+    "matches": [
+        "<div class=\"bar 2\">",
+        "<div class=\"bar 3\">",
+        "<div class=\"bar 4\">",
+        "<div class=\"bar\" exact>"
+    ],
+    "errors": []
+}
\ No newline at end of file
diff --git a/tests/fixtures/utils/selector/combinator-descendant01/source.vue b/tests/fixtures/utils/selector/combinator-descendant01/source.vue
new file mode 100644
index 00000000..b122dae6
--- /dev/null
+++ b/tests/fixtures/utils/selector/combinator-descendant01/source.vue
@@ -0,0 +1,14 @@
+<!-- .foo .bar -->
+<template>
+  <div class="bar">
+    <div class="foo">
+      <div class="foo 2">
+        <div class="bar 2"></div>
+      </div>
+      <div class="bar 3">
+        <div class="bar 4"></div>
+      </div>
+      <div class="bar" exact></div>
+    </div>
+  </div>
+</template>
diff --git a/tests/fixtures/utils/selector/combinator-descendant02/result.json b/tests/fixtures/utils/selector/combinator-descendant02/result.json
new file mode 100644
index 00000000..3d308db8
--- /dev/null
+++ b/tests/fixtures/utils/selector/combinator-descendant02/result.json
@@ -0,0 +1,14 @@
+{
+    "selector": ".a .b .c",
+    "matches": [
+        "<div class=\"c a-a-b-c\">",
+        "<div class=\"c a-b-a-c\">",
+        "<div class=\"c a-b-b-c\">",
+        "<div class=\"c a-b-c\">",
+        "<div class=\"c a-b-c-c\">",
+        "<div class=\"c a-c-b-c\">",
+        "<div class=\"c b-a-b-c\">",
+        "<div class=\"c c-a-b-c\">"
+    ],
+    "errors": []
+}
\ No newline at end of file
diff --git a/tests/fixtures/utils/selector/combinator-descendant02/source.vue b/tests/fixtures/utils/selector/combinator-descendant02/source.vue
new file mode 100644
index 00000000..d89b8622
--- /dev/null
+++ b/tests/fixtures/utils/selector/combinator-descendant02/source.vue
@@ -0,0 +1,162 @@
+<!-- .a .b .c -->
+<template>
+  <div class="a">
+    <div class="a a-a">
+      <div class="a a-a-a">
+        <div class="a a-a-a-a"></div>
+        <div class="b a-a-a-b"></div>
+        <div class="c a-a-a-c"></div>
+      </div>
+      <div class="b a-a-b">
+        <div class="a a-a-b-a"></div>
+        <div class="b a-a-b-b"></div>
+        <div class="c a-a-b-c"></div>
+      </div>
+      <div class="c a-a-c">
+        <div class="a a-a-c-a"></div>
+        <div class="b a-a-c-b"></div>
+        <div class="c a-a-c-c"></div>
+      </div>
+    </div>
+    <div class="b a-b">
+      <div class="a a-b-a">
+        <div class="a a-b-a-a"></div>
+        <div class="b a-b-a-b"></div>
+        <div class="c a-b-a-c"></div>
+      </div>
+      <div class="b a-b-b">
+        <div class="a a-b-b-a"></div>
+        <div class="b a-b-b-b"></div>
+        <div class="c a-b-b-c"></div>
+      </div>
+      <div class="c a-b-c">
+        <div class="a a-b-c-a"></div>
+        <div class="b a-b-c-b"></div>
+        <div class="c a-b-c-c"></div>
+      </div>
+    </div>
+    <div class="c a-c">
+      <div class="a a-c-a">
+        <div class="a a-c-a-a"></div>
+        <div class="b a-c-a-b"></div>
+        <div class="c a-c-a-c"></div>
+      </div>
+      <div class="b a-c-b">
+        <div class="a a-c-b-a"></div>
+        <div class="b a-c-b-b"></div>
+        <div class="c a-c-b-c"></div>
+      </div>
+      <div class="c a-c-c">
+        <div class="a a-c-c-a"></div>
+        <div class="b a-c-c-b"></div>
+        <div class="c a-c-c-c"></div>
+      </div>
+    </div>
+  </div>
+  <div class="b">
+    <div class="a b-a">
+      <div class="a b-a-a">
+        <div class="a b-a-a-a"></div>
+        <div class="b b-a-a-b"></div>
+        <div class="c b-a-a-c"></div>
+      </div>
+      <div class="b b-a-b">
+        <div class="a b-a-b-a"></div>
+        <div class="b b-a-b-b"></div>
+        <div class="c b-a-b-c"></div>
+      </div>
+      <div class="c b-a-c">
+        <div class="a b-a-c-a"></div>
+        <div class="b b-a-c-b"></div>
+        <div class="c b-a-c-c"></div>
+      </div>
+    </div>
+    <div class="b b-b">
+      <div class="a b-b-a">
+        <div class="a b-b-a-a"></div>
+        <div class="b b-b-a-b"></div>
+        <div class="c b-b-a-c"></div>
+      </div>
+      <div class="b b-b-b">
+        <div class="a b-b-b-a"></div>
+        <div class="b b-b-b-b"></div>
+        <div class="c b-b-b-c"></div>
+      </div>
+      <div class="c b-b-c">
+        <div class="a b-b-c-a"></div>
+        <div class="b b-b-c-b"></div>
+        <div class="c b-b-c-c"></div>
+      </div>
+    </div>
+    <div class="c b-c">
+      <div class="a b-c-a">
+        <div class="a b-c-a-a"></div>
+        <div class="b b-c-a-b"></div>
+        <div class="c b-c-a-c"></div>
+      </div>
+      <div class="b b-c-b">
+        <div class="a b-c-b-a"></div>
+        <div class="b b-c-b-b"></div>
+        <div class="c b-c-b-c"></div>
+      </div>
+      <div class="c b-c-c">
+        <div class="a b-c-c-a"></div>
+        <div class="b b-c-c-b"></div>
+        <div class="c b-c-c-c"></div>
+      </div>
+    </div>
+  </div>
+  <div class="c">
+    <div class="a c-a">
+      <div class="a c-a-a">
+        <div class="a c-a-a-a"></div>
+        <div class="b c-a-a-b"></div>
+        <div class="c c-a-a-c"></div>
+      </div>
+      <div class="b c-a-b">
+        <div class="a c-a-b-a"></div>
+        <div class="b c-a-b-b"></div>
+        <div class="c c-a-b-c"></div>
+      </div>
+      <div class="c c-a-c">
+        <div class="a c-a-c-a"></div>
+        <div class="b c-a-c-b"></div>
+        <div class="c c-a-c-c"></div>
+      </div>
+    </div>
+    <div class="b c-b">
+      <div class="a c-b-a">
+        <div class="a c-b-a-a"></div>
+        <div class="b c-b-a-b"></div>
+        <div class="c c-b-a-c"></div>
+      </div>
+      <div class="b c-b-b">
+        <div class="a c-b-b-a"></div>
+        <div class="b c-b-b-b"></div>
+        <div class="c c-b-b-c"></div>
+      </div>
+      <div class="c c-b-c">
+        <div class="a c-b-c-a"></div>
+        <div class="b c-b-c-b"></div>
+        <div class="c c-b-c-c"></div>
+      </div>
+    </div>
+    <div class="c c-c">
+      <div class="a c-c-a">
+        <div class="a c-c-a-a"></div>
+        <div class="b c-c-a-b"></div>
+        <div class="c c-c-a-c"></div>
+      </div>
+      <div class="b c-c-b">
+        <div class="a c-c-b-a"></div>
+        <div class="b c-c-b-b"></div>
+        <div class="c c-c-b-c"></div>
+      </div>
+      <div class="c c-c-c">
+        <div class="a c-c-c-a"></div>
+        <div class="b c-c-c-b"></div>
+        <div class="c c-c-c-c"></div>
+      </div>
+    </div>
+  </div>
+</template>
diff --git a/tests/fixtures/utils/selector/combinator-sibling/result.json b/tests/fixtures/utils/selector/combinator-sibling/result.json
new file mode 100644
index 00000000..efc9bed4
--- /dev/null
+++ b/tests/fixtures/utils/selector/combinator-sibling/result.json
@@ -0,0 +1,9 @@
+{
+    "selector": ".foo~.bar",
+    "matches": [
+        "<div class=\"bar 3\">",
+        "<div class=\"bar 4\">",
+        "<div class=\"bar 5\">"
+    ],
+    "errors": []
+}
\ No newline at end of file
diff --git a/tests/fixtures/utils/selector/combinator-sibling/source.vue b/tests/fixtures/utils/selector/combinator-sibling/source.vue
new file mode 100644
index 00000000..381e6750
--- /dev/null
+++ b/tests/fixtures/utils/selector/combinator-sibling/source.vue
@@ -0,0 +1,14 @@
+<!-- .foo~.bar -->
+<template>
+  <div class="bar">
+    <div class="foo">
+      <div class="foo 2">
+        <div class="bar 2"></div>
+      </div>
+      <div class="bar 3"></div>
+    </div>
+    <div class="foo 3"></div>
+    <div class="bar 4"></div>
+    <div class="bar 5"></div>
+  </div>
+</template>
diff --git a/tests/fixtures/utils/selector/comment01/result.json b/tests/fixtures/utils/selector/comment01/result.json
new file mode 100644
index 00000000..7592577f
--- /dev/null
+++ b/tests/fixtures/utils/selector/comment01/result.json
@@ -0,0 +1,7 @@
+{
+    "selector": ".foo /* comment */ :is(.bar /* comment */)",
+    "matches": [
+        "<div class=\"bar\">"
+    ],
+    "errors": []
+}
\ No newline at end of file
diff --git a/tests/fixtures/utils/selector/comment01/source.vue b/tests/fixtures/utils/selector/comment01/source.vue
new file mode 100644
index 00000000..d388a1b9
--- /dev/null
+++ b/tests/fixtures/utils/selector/comment01/source.vue
@@ -0,0 +1,7 @@
+<!-- .foo /* comment */ :is(.bar /* comment */) -->
+<template>
+  <div class="foo">
+    <div class="bar">
+    </div>
+  </div>
+</template>
diff --git a/tests/fixtures/utils/selector/comment02/result.json b/tests/fixtures/utils/selector/comment02/result.json
new file mode 100644
index 00000000..3d70892a
--- /dev/null
+++ b/tests/fixtures/utils/selector/comment02/result.json
@@ -0,0 +1,7 @@
+{
+    "selector": ".foo /* comment */ > /* comment */ /* comment */ .bar /* comment */",
+    "matches": [
+        "<div class=\"bar b\">"
+    ],
+    "errors": []
+}
\ No newline at end of file
diff --git a/tests/fixtures/utils/selector/comment02/source.vue b/tests/fixtures/utils/selector/comment02/source.vue
new file mode 100644
index 00000000..dc71c6ba
--- /dev/null
+++ b/tests/fixtures/utils/selector/comment02/source.vue
@@ -0,0 +1,13 @@
+<!-- .foo /* comment */ > /* comment */ /* comment */ .bar /* comment */ -->
+<template>
+  <div class="foo a">
+    <div class="bar b">
+    </div>
+  </div>
+  <div class="foo c">
+    <div>
+      <div class="bar d">
+      </div>
+    </div>
+  </div>
+</template>
diff --git a/tests/fixtures/utils/selector/comment03/result.json b/tests/fixtures/utils/selector/comment03/result.json
new file mode 100644
index 00000000..1358c04e
--- /dev/null
+++ b/tests/fixtures/utils/selector/comment03/result.json
@@ -0,0 +1,7 @@
+{
+    "selector": ".foo /* comment */ /* comment */ > /* comment */ .bar /* comment */",
+    "matches": [
+        "<div class=\"bar b\">"
+    ],
+    "errors": []
+}
\ No newline at end of file
diff --git a/tests/fixtures/utils/selector/comment03/source.vue b/tests/fixtures/utils/selector/comment03/source.vue
new file mode 100644
index 00000000..a8381a88
--- /dev/null
+++ b/tests/fixtures/utils/selector/comment03/source.vue
@@ -0,0 +1,13 @@
+<!-- .foo /* comment */ /* comment */ > /* comment */ .bar /* comment */ -->
+<template>
+  <div class="foo a">
+    <div class="bar b">
+    </div>
+  </div>
+  <div class="foo c">
+    <div>
+      <div class="bar d">
+      </div>
+    </div>
+  </div>
+</template>
diff --git a/tests/fixtures/utils/selector/id/result.json b/tests/fixtures/utils/selector/id/result.json
new file mode 100644
index 00000000..616a429c
--- /dev/null
+++ b/tests/fixtures/utils/selector/id/result.json
@@ -0,0 +1,7 @@
+{
+    "selector": "#foo",
+    "matches": [
+        "<div id=\"foo\">"
+    ],
+    "errors": []
+}
\ No newline at end of file
diff --git a/tests/fixtures/utils/selector/id/source.vue b/tests/fixtures/utils/selector/id/source.vue
new file mode 100644
index 00000000..88bdd7cb
--- /dev/null
+++ b/tests/fixtures/utils/selector/id/source.vue
@@ -0,0 +1,8 @@
+<!-- #foo -->
+<template>
+  <div id="1">
+    <div id="foo"></div>
+    <p id="3"></p>
+    <input id="4">
+  </div>
+</template>
diff --git a/tests/fixtures/utils/selector/invalid-attr-op/result.json b/tests/fixtures/utils/selector/invalid-attr-op/result.json
new file mode 100644
index 00000000..edc492df
--- /dev/null
+++ b/tests/fixtures/utils/selector/invalid-attr-op/result.json
@@ -0,0 +1,7 @@
+{
+    "selector": "a[href==\"example\"]",
+    "matches": [],
+    "errors": [
+        "Unsupported operator: ==."
+    ]
+}
\ No newline at end of file
diff --git a/tests/fixtures/utils/selector/invalid-attr-op/source.vue b/tests/fixtures/utils/selector/invalid-attr-op/source.vue
new file mode 100644
index 00000000..dc39fc6b
--- /dev/null
+++ b/tests/fixtures/utils/selector/invalid-attr-op/source.vue
@@ -0,0 +1,4 @@
+<!-- a[href=="example"] -->
+<template>
+  <a href="example"></a>
+</template>
diff --git a/tests/fixtures/utils/selector/invalid-combinators/result.json b/tests/fixtures/utils/selector/invalid-combinators/result.json
new file mode 100644
index 00000000..7f1e135e
--- /dev/null
+++ b/tests/fixtures/utils/selector/invalid-combinators/result.json
@@ -0,0 +1,7 @@
+{
+    "selector": ".foo > > .bar",
+    "matches": [],
+    "errors": [
+        "Unexpected combinator '>'."
+    ]
+}
\ No newline at end of file
diff --git a/tests/fixtures/utils/selector/invalid-combinators/source.vue b/tests/fixtures/utils/selector/invalid-combinators/source.vue
new file mode 100644
index 00000000..2c768628
--- /dev/null
+++ b/tests/fixtures/utils/selector/invalid-combinators/source.vue
@@ -0,0 +1,6 @@
+<!-- .foo > > .bar -->
+<template>
+  <div class="foo">
+    <div class="bar"></div>
+  </div>
+</template>
diff --git a/tests/fixtures/utils/selector/invalid-empty/result.json b/tests/fixtures/utils/selector/invalid-empty/result.json
new file mode 100644
index 00000000..0352970a
--- /dev/null
+++ b/tests/fixtures/utils/selector/invalid-empty/result.json
@@ -0,0 +1,7 @@
+{
+    "selector": ".foo /* comment */ :is(.bar, /* comment */)",
+    "matches": [],
+    "errors": [
+        "Unexpected empty selector."
+    ]
+}
\ No newline at end of file
diff --git a/tests/fixtures/utils/selector/invalid-empty/source.vue b/tests/fixtures/utils/selector/invalid-empty/source.vue
new file mode 100644
index 00000000..c6444bda
--- /dev/null
+++ b/tests/fixtures/utils/selector/invalid-empty/source.vue
@@ -0,0 +1,6 @@
+<!-- .foo /* comment */ :is(.bar, /* comment */) -->
+<template>
+  <div class="foo">
+    <div class="bar"></div>
+  </div>
+</template>
diff --git a/tests/fixtures/utils/selector/invalid-last-combinator/result.json b/tests/fixtures/utils/selector/invalid-last-combinator/result.json
new file mode 100644
index 00000000..25d9ded5
--- /dev/null
+++ b/tests/fixtures/utils/selector/invalid-last-combinator/result.json
@@ -0,0 +1,7 @@
+{
+    "selector": ".foo >",
+    "matches": [],
+    "errors": [
+        "Expected selector after '>'."
+    ]
+}
\ No newline at end of file
diff --git a/tests/fixtures/utils/selector/invalid-last-combinator/source.vue b/tests/fixtures/utils/selector/invalid-last-combinator/source.vue
new file mode 100644
index 00000000..193c8fd1
--- /dev/null
+++ b/tests/fixtures/utils/selector/invalid-last-combinator/source.vue
@@ -0,0 +1,6 @@
+<!-- .foo > -->
+<template>
+  <div class="foo">
+    <div class="bar"></div>
+  </div>
+</template>
diff --git a/tests/fixtures/utils/selector/invalid-nesting/result.json b/tests/fixtures/utils/selector/invalid-nesting/result.json
new file mode 100644
index 00000000..3e8d09db
--- /dev/null
+++ b/tests/fixtures/utils/selector/invalid-nesting/result.json
@@ -0,0 +1,7 @@
+{
+    "selector": "& .foo > .bar",
+    "matches": [],
+    "errors": [
+        "Unsupported nesting selector."
+    ]
+}
\ No newline at end of file
diff --git a/tests/fixtures/utils/selector/invalid-nesting/source.vue b/tests/fixtures/utils/selector/invalid-nesting/source.vue
new file mode 100644
index 00000000..69338be4
--- /dev/null
+++ b/tests/fixtures/utils/selector/invalid-nesting/source.vue
@@ -0,0 +1,6 @@
+<!-- & .foo > .bar -->
+<template>
+  <div class="foo">
+    <div class="bar"></div>
+  </div>
+</template>
diff --git a/tests/fixtures/utils/selector/invalid-nth/result.json b/tests/fixtures/utils/selector/invalid-nth/result.json
new file mode 100644
index 00000000..248758f1
--- /dev/null
+++ b/tests/fixtures/utils/selector/invalid-nth/result.json
@@ -0,0 +1,7 @@
+{
+    "selector": ":nth-child()",
+    "matches": [],
+    "errors": [
+        "Cannot parse An+B micro syntax (:nth-xxx() argument): ''."
+    ]
+}
\ No newline at end of file
diff --git a/tests/fixtures/utils/selector/invalid-nth/source.vue b/tests/fixtures/utils/selector/invalid-nth/source.vue
new file mode 100644
index 00000000..4e2e356c
--- /dev/null
+++ b/tests/fixtures/utils/selector/invalid-nth/source.vue
@@ -0,0 +1,4 @@
+<!-- :nth-child() -->
+<template>
+  <a href="example"></a>
+</template>
diff --git a/tests/fixtures/utils/selector/invalid-selector/result.json b/tests/fixtures/utils/selector/invalid-selector/result.json
new file mode 100644
index 00000000..b8b057b0
--- /dev/null
+++ b/tests/fixtures/utils/selector/invalid-selector/result.json
@@ -0,0 +1,7 @@
+{
+    "selector": ":not(",
+    "matches": [],
+    "errors": [
+        "Cannot parse selector: :not(."
+    ]
+}
\ No newline at end of file
diff --git a/tests/fixtures/utils/selector/invalid-selector/source.vue b/tests/fixtures/utils/selector/invalid-selector/source.vue
new file mode 100644
index 00000000..cc6ae2d4
--- /dev/null
+++ b/tests/fixtures/utils/selector/invalid-selector/source.vue
@@ -0,0 +1,4 @@
+<!-- :not( -->
+<template>
+  <a href="example"></a>
+</template>
diff --git a/tests/fixtures/utils/selector/invalid-string/result.json b/tests/fixtures/utils/selector/invalid-string/result.json
new file mode 100644
index 00000000..542d12c1
--- /dev/null
+++ b/tests/fixtures/utils/selector/invalid-string/result.json
@@ -0,0 +1,7 @@
+{
+    "selector": ".foo > \".bar\"",
+    "matches": [],
+    "errors": [
+        "Unknown selector: \".bar\"."
+    ]
+}
\ No newline at end of file
diff --git a/tests/fixtures/utils/selector/invalid-string/source.vue b/tests/fixtures/utils/selector/invalid-string/source.vue
new file mode 100644
index 00000000..3a51d640
--- /dev/null
+++ b/tests/fixtures/utils/selector/invalid-string/source.vue
@@ -0,0 +1,6 @@
+<!-- .foo > ".bar" -->
+<template>
+  <div class="foo">
+    <div class="bar"></div>
+  </div>
+</template>
diff --git a/tests/fixtures/utils/selector/invalid-unknown-combinator/result.json b/tests/fixtures/utils/selector/invalid-unknown-combinator/result.json
new file mode 100644
index 00000000..486eae4f
--- /dev/null
+++ b/tests/fixtures/utils/selector/invalid-unknown-combinator/result.json
@@ -0,0 +1,7 @@
+{
+    "selector": ".foo /unknown/ .bar",
+    "matches": [],
+    "errors": [
+        "Unknown combinator: /unknown/."
+    ]
+}
\ No newline at end of file
diff --git a/tests/fixtures/utils/selector/invalid-unknown-combinator/source.vue b/tests/fixtures/utils/selector/invalid-unknown-combinator/source.vue
new file mode 100644
index 00000000..08d148f7
--- /dev/null
+++ b/tests/fixtures/utils/selector/invalid-unknown-combinator/source.vue
@@ -0,0 +1,6 @@
+<!-- .foo /unknown/ .bar -->
+<template>
+  <div class="foo">
+    <div class="bar"></div>
+  </div>
+</template>
diff --git a/tests/fixtures/utils/selector/invalid-unknown-pseudo/result.json b/tests/fixtures/utils/selector/invalid-unknown-pseudo/result.json
new file mode 100644
index 00000000..6171ea02
--- /dev/null
+++ b/tests/fixtures/utils/selector/invalid-unknown-pseudo/result.json
@@ -0,0 +1,7 @@
+{
+    "selector": ".foo :unknown(.bar)",
+    "matches": [],
+    "errors": [
+        "Unsupported pseudo selector: :unknown."
+    ]
+}
\ No newline at end of file
diff --git a/tests/fixtures/utils/selector/invalid-unknown-pseudo/source.vue b/tests/fixtures/utils/selector/invalid-unknown-pseudo/source.vue
new file mode 100644
index 00000000..5d1449ab
--- /dev/null
+++ b/tests/fixtures/utils/selector/invalid-unknown-pseudo/source.vue
@@ -0,0 +1,6 @@
+<!-- .foo :unknown(.bar) -->
+<template>
+  <div class="foo">
+    <div class="bar"></div>
+  </div>
+</template>
diff --git a/tests/fixtures/utils/selector/pseudo-empty/result.json b/tests/fixtures/utils/selector/pseudo-empty/result.json
new file mode 100644
index 00000000..feefab5e
--- /dev/null
+++ b/tests/fixtures/utils/selector/pseudo-empty/result.json
@@ -0,0 +1,8 @@
+{
+    "selector": ".box:empty",
+    "matches": [
+        "<div class=\"box a\">",
+        "<div class=\"box c\">"
+    ],
+    "errors": []
+}
\ No newline at end of file
diff --git a/tests/fixtures/utils/selector/pseudo-empty/source.vue b/tests/fixtures/utils/selector/pseudo-empty/source.vue
new file mode 100644
index 00000000..9a47c2a5
--- /dev/null
+++ b/tests/fixtures/utils/selector/pseudo-empty/source.vue
@@ -0,0 +1,11 @@
+<!-- .box:empty -->
+<template>
+  <div class="box a"></div>
+  <div class="box b">Content</div>
+  <div class="box c">
+    <!-- comment. -->
+  </div>
+  <div class="box d">
+    <p></p>
+  </div>
+</template>
diff --git a/tests/fixtures/utils/selector/pseudo-first-child01/result.json b/tests/fixtures/utils/selector/pseudo-first-child01/result.json
new file mode 100644
index 00000000..df66f755
--- /dev/null
+++ b/tests/fixtures/utils/selector/pseudo-first-child01/result.json
@@ -0,0 +1,7 @@
+{
+    "selector": "p:first-child",
+    "matches": [
+        "<p class=\"b\">"
+    ],
+    "errors": []
+}
\ No newline at end of file
diff --git a/tests/fixtures/utils/selector/pseudo-first-child01/source.vue b/tests/fixtures/utils/selector/pseudo-first-child01/source.vue
new file mode 100644
index 00000000..52ab2eb9
--- /dev/null
+++ b/tests/fixtures/utils/selector/pseudo-first-child01/source.vue
@@ -0,0 +1,14 @@
+<!-- p:first-child -->
+<template>
+  <div>
+    <div class="a">
+      <p class="b">This text is selected!</p>
+      <p class="c">This text isn't selected.</p>
+    </div>
+
+    <div class="d">
+      <h2 class="e">This text isn't selected: it's not a `p`.</h2>
+      <p class="f">This text isn't selected.</p>
+    </div>
+  </div>
+</template>
diff --git a/tests/fixtures/utils/selector/pseudo-first-child02/result.json b/tests/fixtures/utils/selector/pseudo-first-child02/result.json
new file mode 100644
index 00000000..e991fe99
--- /dev/null
+++ b/tests/fixtures/utils/selector/pseudo-first-child02/result.json
@@ -0,0 +1,8 @@
+{
+    "selector": "ul li:first-child",
+    "matches": [
+        "<li class=\"b\">",
+        "<li class=\"f\">"
+    ],
+    "errors": []
+}
\ No newline at end of file
diff --git a/tests/fixtures/utils/selector/pseudo-first-child02/source.vue b/tests/fixtures/utils/selector/pseudo-first-child02/source.vue
new file mode 100644
index 00000000..90c77ad9
--- /dev/null
+++ b/tests/fixtures/utils/selector/pseudo-first-child02/source.vue
@@ -0,0 +1,14 @@
+<!-- ul li:first-child -->
+<template>
+  <ul class="a">
+    <li class="b">Item 1</li>
+    <li class="c">Item 2</li>
+    <li class="d">Item 3
+      <ul class="e">
+        <li class="f">Item 3.1</li>
+        <li class="g">Item 3.2</li>
+        <li class="h">Item 3.3</li>
+      </ul>
+    </li>
+  </ul>
+</template>
diff --git a/tests/fixtures/utils/selector/pseudo-first-of-type01/result.json b/tests/fixtures/utils/selector/pseudo-first-of-type01/result.json
new file mode 100644
index 00000000..408762a9
--- /dev/null
+++ b/tests/fixtures/utils/selector/pseudo-first-of-type01/result.json
@@ -0,0 +1,7 @@
+{
+    "selector": "p:first-of-type",
+    "matches": [
+        "<p class=\"b\">"
+    ],
+    "errors": []
+}
\ No newline at end of file
diff --git a/tests/fixtures/utils/selector/pseudo-first-of-type01/source.vue b/tests/fixtures/utils/selector/pseudo-first-of-type01/source.vue
new file mode 100644
index 00000000..c9a012a1
--- /dev/null
+++ b/tests/fixtures/utils/selector/pseudo-first-of-type01/source.vue
@@ -0,0 +1,8 @@
+<!-- p:first-of-type -->
+<template>
+  <div>
+    <h2 class="a">Heading</h2>
+    <p class="b">Paragraph 1</p>
+    <p class="c">Paragraph 2</p>
+  </div>
+</template>
diff --git a/tests/fixtures/utils/selector/pseudo-first-of-type02/result.json b/tests/fixtures/utils/selector/pseudo-first-of-type02/result.json
new file mode 100644
index 00000000..aabc2d16
--- /dev/null
+++ b/tests/fixtures/utils/selector/pseudo-first-of-type02/result.json
@@ -0,0 +1,8 @@
+{
+    "selector": ".root > :first-of-type",
+    "matches": [
+        "<h2 class=\"a\">",
+        "<p class=\"b\">"
+    ],
+    "errors": []
+}
\ No newline at end of file
diff --git a/tests/fixtures/utils/selector/pseudo-first-of-type02/source.vue b/tests/fixtures/utils/selector/pseudo-first-of-type02/source.vue
new file mode 100644
index 00000000..515de7e9
--- /dev/null
+++ b/tests/fixtures/utils/selector/pseudo-first-of-type02/source.vue
@@ -0,0 +1,8 @@
+<!-- .root > :first-of-type -->
+<template>
+  <div class="root">
+    <h2 class="a">Heading</h2>
+    <p class="b">Paragraph 1</p>
+    <p class="c">Paragraph 2</p>
+  </div>
+</template>
diff --git a/tests/fixtures/utils/selector/pseudo-has-complex01/result.json b/tests/fixtures/utils/selector/pseudo-has-complex01/result.json
new file mode 100644
index 00000000..21d9decf
--- /dev/null
+++ b/tests/fixtures/utils/selector/pseudo-has-complex01/result.json
@@ -0,0 +1,8 @@
+{
+    "selector": "div:has(>.foo > .foo)",
+    "matches": [
+        "<div class=\"foo\" id=\"root-1\">",
+        "<div class=\"foo nest-1\">"
+    ],
+    "errors": []
+}
\ No newline at end of file
diff --git a/tests/fixtures/utils/selector/pseudo-has-complex01/source.vue b/tests/fixtures/utils/selector/pseudo-has-complex01/source.vue
new file mode 100644
index 00000000..abc928b8
--- /dev/null
+++ b/tests/fixtures/utils/selector/pseudo-has-complex01/source.vue
@@ -0,0 +1,31 @@
+<!-- div:has(>.foo > .foo) -->
+<template>
+  <div class="foo" id="root-1">
+    <div class="foo nest-1">
+      <div class="foo nest-2">
+        <div class="foo nest-3">
+        </div>
+      </div>
+    </div>
+  </div>
+  <div class="foo" id="root-2">
+    <div class="bar nest-1">
+      <div class="foo nest-2">
+        <div class="bar nest-3">
+        </div>
+      </div>
+    </div>
+  </div>
+  <div class="foo" id="root-3">
+    <div class="foo nest-1">
+      <div class="bar nest-2">
+        <div class="foo nest-3">
+        </div>
+      </div>
+    </div>
+  </div>
+  <div class="foo" id="root-4">
+    <div class="foo nest-1">
+    </div>
+  </div>
+</template>
diff --git a/tests/fixtures/utils/selector/pseudo-has-complex02/result.json b/tests/fixtures/utils/selector/pseudo-has-complex02/result.json
new file mode 100644
index 00000000..b8c779ec
--- /dev/null
+++ b/tests/fixtures/utils/selector/pseudo-has-complex02/result.json
@@ -0,0 +1,9 @@
+{
+    "selector": "div:has(>.foo .foo)",
+    "matches": [
+        "<div class=\"foo\" id=\"root-1\">",
+        "<div class=\"foo nest-1\">",
+        "<div class=\"foo\" id=\"root-3\">"
+    ],
+    "errors": []
+}
\ No newline at end of file
diff --git a/tests/fixtures/utils/selector/pseudo-has-complex02/source.vue b/tests/fixtures/utils/selector/pseudo-has-complex02/source.vue
new file mode 100644
index 00000000..f0e1b8da
--- /dev/null
+++ b/tests/fixtures/utils/selector/pseudo-has-complex02/source.vue
@@ -0,0 +1,31 @@
+<!-- div:has(>.foo .foo) -->
+<template>
+  <div class="foo" id="root-1">
+    <div class="foo nest-1">
+      <div class="foo nest-2">
+        <div class="foo nest-3">
+        </div>
+      </div>
+    </div>
+  </div>
+  <div class="foo" id="root-2">
+    <div class="bar nest-1">
+      <div class="foo nest-2">
+        <div class="bar nest-3">
+        </div>
+      </div>
+    </div>
+  </div>
+  <div class="foo" id="root-3">
+    <div class="foo nest-1">
+      <div class="bar nest-2">
+        <div class="foo nest-3">
+        </div>
+      </div>
+    </div>
+  </div>
+  <div class="foo" id="root-4">
+    <div class="foo nest-1">
+    </div>
+  </div>
+</template>
diff --git a/tests/fixtures/utils/selector/pseudo-has01/result.json b/tests/fixtures/utils/selector/pseudo-has01/result.json
new file mode 100644
index 00000000..af3f7bc3
--- /dev/null
+++ b/tests/fixtures/utils/selector/pseudo-has01/result.json
@@ -0,0 +1,9 @@
+{
+    "selector": "div:has(.foo)",
+    "matches": [
+        "<div class=\"foo\" id=\"root-1\">",
+        "<div class=\"foo\" id=\"root-2\">",
+        "<div class=\"foo\" id=\"root-3\">"
+    ],
+    "errors": []
+}
\ No newline at end of file
diff --git a/tests/fixtures/utils/selector/pseudo-has01/source.vue b/tests/fixtures/utils/selector/pseudo-has01/source.vue
new file mode 100644
index 00000000..11defe7d
--- /dev/null
+++ b/tests/fixtures/utils/selector/pseudo-has01/source.vue
@@ -0,0 +1,25 @@
+<!-- div:has(.foo) -->
+<template>
+  <div class="foo" id="root-1">
+    <p class="foo">
+      <span class="foo">
+      </span>
+    </p>
+  </div>
+  <div class="foo" id="root-2">
+    <p class="bar">
+      <span class="foo">
+      </span>
+    </p>
+  </div>
+  <div class="foo" id="root-3">
+    <p class="foo">
+      <span class="bar">
+      </span>
+    </p>
+  </div>
+  <div class="foo" id="root-4">
+    <p class="bar">
+    </p>
+  </div>
+</template>
diff --git a/tests/fixtures/utils/selector/pseudo-has02/result.json b/tests/fixtures/utils/selector/pseudo-has02/result.json
new file mode 100644
index 00000000..41326746
--- /dev/null
+++ b/tests/fixtures/utils/selector/pseudo-has02/result.json
@@ -0,0 +1,8 @@
+{
+    "selector": "div:has(>.foo)",
+    "matches": [
+        "<div class=\"foo\" id=\"root-1\">",
+        "<div class=\"foo\" id=\"root-3\">"
+    ],
+    "errors": []
+}
\ No newline at end of file
diff --git a/tests/fixtures/utils/selector/pseudo-has02/source.vue b/tests/fixtures/utils/selector/pseudo-has02/source.vue
new file mode 100644
index 00000000..7fcde7d5
--- /dev/null
+++ b/tests/fixtures/utils/selector/pseudo-has02/source.vue
@@ -0,0 +1,25 @@
+<!-- div:has(>.foo) -->
+<template>
+  <div class="foo" id="root-1">
+    <p class="foo">
+      <span class="foo">
+      </span>
+    </p>
+  </div>
+  <div class="foo" id="root-2">
+    <p class="bar">
+      <span class="foo">
+      </span>
+    </p>
+  </div>
+  <div class="foo" id="root-3">
+    <p class="foo">
+      <span class="bar">
+      </span>
+    </p>
+  </div>
+  <div class="foo" id="root-4">
+    <p class="bar">
+    </p>
+  </div>
+</template>
diff --git a/tests/fixtures/utils/selector/pseudo-has03/result.json b/tests/fixtures/utils/selector/pseudo-has03/result.json
new file mode 100644
index 00000000..d818eb80
--- /dev/null
+++ b/tests/fixtures/utils/selector/pseudo-has03/result.json
@@ -0,0 +1,8 @@
+{
+    "selector": "div:has(+.foo)",
+    "matches": [
+        "<div class=\"bar\" id=\"root-2\">",
+        "<div class=\"foo\" id=\"root-3\">"
+    ],
+    "errors": []
+}
\ No newline at end of file
diff --git a/tests/fixtures/utils/selector/pseudo-has03/source.vue b/tests/fixtures/utils/selector/pseudo-has03/source.vue
new file mode 100644
index 00000000..2157d535
--- /dev/null
+++ b/tests/fixtures/utils/selector/pseudo-has03/source.vue
@@ -0,0 +1,13 @@
+<!-- div:has(+.foo)  -->
+<template>
+  <div class="foo" id="root-1">
+  </div>
+  <div class="bar" id="root-2">
+  </div>
+  <div class="foo" id="root-3">
+  </div>
+  <div class="foo" id="root-4">
+  </div>
+  <div class="bar" id="root-5">
+  </div>
+</template>
diff --git a/tests/fixtures/utils/selector/pseudo-has04/result.json b/tests/fixtures/utils/selector/pseudo-has04/result.json
new file mode 100644
index 00000000..ca52f286
--- /dev/null
+++ b/tests/fixtures/utils/selector/pseudo-has04/result.json
@@ -0,0 +1,9 @@
+{
+    "selector": "div:has(~.foo)",
+    "matches": [
+        "<div class=\"foo\" id=\"root-1\">",
+        "<div class=\"bar\" id=\"root-2\">",
+        "<div class=\"foo\" id=\"root-3\">"
+    ],
+    "errors": []
+}
\ No newline at end of file
diff --git a/tests/fixtures/utils/selector/pseudo-has04/source.vue b/tests/fixtures/utils/selector/pseudo-has04/source.vue
new file mode 100644
index 00000000..9eeebaa2
--- /dev/null
+++ b/tests/fixtures/utils/selector/pseudo-has04/source.vue
@@ -0,0 +1,13 @@
+<!-- div:has(~.foo)  -->
+<template>
+  <div class="foo" id="root-1">
+  </div>
+  <div class="bar" id="root-2">
+  </div>
+  <div class="foo" id="root-3">
+  </div>
+  <div class="foo" id="root-4">
+  </div>
+  <div class="bar" id="root-5">
+  </div>
+</template>
diff --git a/tests/fixtures/utils/selector/pseudo-is/result.json b/tests/fixtures/utils/selector/pseudo-is/result.json
new file mode 100644
index 00000000..a0893ac9
--- /dev/null
+++ b/tests/fixtures/utils/selector/pseudo-is/result.json
@@ -0,0 +1,12 @@
+{
+    "selector": ":where(header, main, footer) p",
+    "matches": [
+        "<p class=\"b\">",
+        "<p class=\"f\">",
+        "<p class=\"g\">",
+        "<p class=\"i\">",
+        "<p class=\"j\">",
+        "<p class=\"l\">"
+    ],
+    "errors": []
+}
\ No newline at end of file
diff --git a/tests/fixtures/utils/selector/pseudo-is/source.vue b/tests/fixtures/utils/selector/pseudo-is/source.vue
new file mode 100644
index 00000000..944038e6
--- /dev/null
+++ b/tests/fixtures/utils/selector/pseudo-is/source.vue
@@ -0,0 +1,17 @@
+<!-- :where(header, main, footer) p -->
+<template>
+  <header class="a">
+    <p class="b">This is my header paragraph</p>
+  </header>
+
+  <main class="c">
+    <ul class="d">
+      <li class="e"><p class="f">This is my first</p><p class="g">list item</p></li>
+      <li class="h"><p class="i">This is my second</p><p class="j">list item</p></li>
+    </ul>
+  </main>
+
+  <footer class="k">
+    <p class="l">This is my footer paragraph</p>
+  </footer>
+</template>
diff --git a/tests/fixtures/utils/selector/pseudo-last-child/result.json b/tests/fixtures/utils/selector/pseudo-last-child/result.json
new file mode 100644
index 00000000..39202780
--- /dev/null
+++ b/tests/fixtures/utils/selector/pseudo-last-child/result.json
@@ -0,0 +1,8 @@
+{
+    "selector": "ul li:last-child",
+    "matches": [
+        "<li class=\"d\">",
+        "<li class=\"h\">"
+    ],
+    "errors": []
+}
\ No newline at end of file
diff --git a/tests/fixtures/utils/selector/pseudo-last-child/source.vue b/tests/fixtures/utils/selector/pseudo-last-child/source.vue
new file mode 100644
index 00000000..04110e17
--- /dev/null
+++ b/tests/fixtures/utils/selector/pseudo-last-child/source.vue
@@ -0,0 +1,14 @@
+<!-- ul li:last-child -->
+<template>
+  <ul class="a">
+    <li class="b">Item 1</li>
+    <li class="c">Item 2</li>
+    <li class="d">Item 3
+      <ul class="e">
+        <li class="f">Item 3.1</li>
+        <li class="g">Item 3.2</li>
+        <li class="h">Item 3.3</li>
+      </ul>
+    </li>
+  </ul>
+</template>
diff --git a/tests/fixtures/utils/selector/pseudo-last-of-type01/result.json b/tests/fixtures/utils/selector/pseudo-last-of-type01/result.json
new file mode 100644
index 00000000..e566e599
--- /dev/null
+++ b/tests/fixtures/utils/selector/pseudo-last-of-type01/result.json
@@ -0,0 +1,7 @@
+{
+    "selector": "p:last-of-type",
+    "matches": [
+        "<p class=\"c\">"
+    ],
+    "errors": []
+}
\ No newline at end of file
diff --git a/tests/fixtures/utils/selector/pseudo-last-of-type01/source.vue b/tests/fixtures/utils/selector/pseudo-last-of-type01/source.vue
new file mode 100644
index 00000000..eab09787
--- /dev/null
+++ b/tests/fixtures/utils/selector/pseudo-last-of-type01/source.vue
@@ -0,0 +1,8 @@
+<!-- p:last-of-type -->
+<template>
+  <div>
+    <h2 class="a">Heading</h2>
+    <p class="b">Paragraph 1</p>
+    <p class="c">Paragraph 2</p>
+  </div>
+</template>
diff --git a/tests/fixtures/utils/selector/pseudo-last-of-type02/result.json b/tests/fixtures/utils/selector/pseudo-last-of-type02/result.json
new file mode 100644
index 00000000..b23c5b68
--- /dev/null
+++ b/tests/fixtures/utils/selector/pseudo-last-of-type02/result.json
@@ -0,0 +1,8 @@
+{
+    "selector": ".root > :last-of-type",
+    "matches": [
+        "<h2 class=\"a\">",
+        "<p class=\"c\">"
+    ],
+    "errors": []
+}
\ No newline at end of file
diff --git a/tests/fixtures/utils/selector/pseudo-last-of-type02/source.vue b/tests/fixtures/utils/selector/pseudo-last-of-type02/source.vue
new file mode 100644
index 00000000..06e2e0f5
--- /dev/null
+++ b/tests/fixtures/utils/selector/pseudo-last-of-type02/source.vue
@@ -0,0 +1,8 @@
+<!-- .root > :last-of-type -->
+<template>
+  <div class="root">
+    <h2 class="a">Heading</h2>
+    <p class="b">Paragraph 1</p>
+    <p class="c">Paragraph 2</p>
+  </div>
+</template>
diff --git a/tests/fixtures/utils/selector/pseudo-not/result.json b/tests/fixtures/utils/selector/pseudo-not/result.json
new file mode 100644
index 00000000..42e685b4
--- /dev/null
+++ b/tests/fixtures/utils/selector/pseudo-not/result.json
@@ -0,0 +1,9 @@
+{
+    "selector": ":not(.foo)",
+    "matches": [
+        "<template>",
+        "<div class=\"f\">",
+        "<p class=\"bar\">"
+    ],
+    "errors": []
+}
\ No newline at end of file
diff --git a/tests/fixtures/utils/selector/pseudo-not/source.vue b/tests/fixtures/utils/selector/pseudo-not/source.vue
new file mode 100644
index 00000000..3c282fda
--- /dev/null
+++ b/tests/fixtures/utils/selector/pseudo-not/source.vue
@@ -0,0 +1,8 @@
+<!-- :not(.foo) -->
+<template>
+  <div class="foo">
+    <div class="f"></div>
+    <p class="bar"></p>
+    <input class="foo bar">
+  </div>
+</template>
diff --git a/tests/fixtures/utils/selector/pseudo-nth-child01/result.json b/tests/fixtures/utils/selector/pseudo-nth-child01/result.json
new file mode 100644
index 00000000..be67bd4a
--- /dev/null
+++ b/tests/fixtures/utils/selector/pseudo-nth-child01/result.json
@@ -0,0 +1,11 @@
+{
+    "selector": "span:nth-child(2n+1)",
+    "matches": [
+        "<span class=\"x1\">",
+        "<span class=\"x3\">",
+        "<span class=\"x5\">",
+        "<span class=\"x7\">",
+        "<span class=\"x9\">"
+    ],
+    "errors": []
+}
\ No newline at end of file
diff --git a/tests/fixtures/utils/selector/pseudo-nth-child01/source.vue b/tests/fixtures/utils/selector/pseudo-nth-child01/source.vue
new file mode 100644
index 00000000..18ed75b7
--- /dev/null
+++ b/tests/fixtures/utils/selector/pseudo-nth-child01/source.vue
@@ -0,0 +1,14 @@
+<!-- span:nth-child(2n+1) -->
+<template>
+  <div>
+    <span class="x1"></span>
+    <span class="x2"></span>
+    <span class="x3"></span>
+    <span class="x4"></span>
+    <span class="x5"></span>
+    <span class="x6"></span>
+    <span class="x7"></span>
+    <span class="x8"></span>
+    <span class="x9"></span>
+  </div>
+</template>
diff --git a/tests/fixtures/utils/selector/pseudo-nth-child02/result.json b/tests/fixtures/utils/selector/pseudo-nth-child02/result.json
new file mode 100644
index 00000000..8bddde0f
--- /dev/null
+++ b/tests/fixtures/utils/selector/pseudo-nth-child02/result.json
@@ -0,0 +1,8 @@
+{
+    "selector": ".foo:nth-child(odd)",
+    "matches": [
+        "<div class=\"foo a\">",
+        "<div class=\"foo c\">"
+    ],
+    "errors": []
+}
\ No newline at end of file
diff --git a/tests/fixtures/utils/selector/pseudo-nth-child02/source.vue b/tests/fixtures/utils/selector/pseudo-nth-child02/source.vue
new file mode 100644
index 00000000..9533e330
--- /dev/null
+++ b/tests/fixtures/utils/selector/pseudo-nth-child02/source.vue
@@ -0,0 +1,14 @@
+<!-- .foo:nth-child(odd) -->
+<template>
+  <div>
+    <div class="foo a"></div>
+    <div class="bar a"></div>
+    <div class="baz a"></div>
+    <div class="foo b"></div>
+    <div class="bar b"></div>
+    <div class="baz b"></div>
+    <div class="foo c"></div>
+    <div class="bar c"></div>
+    <div class="baz c"></div>
+  </div>
+</template>
diff --git a/tests/fixtures/utils/selector/pseudo-nth-child03/result.json b/tests/fixtures/utils/selector/pseudo-nth-child03/result.json
new file mode 100644
index 00000000..d17057d8
--- /dev/null
+++ b/tests/fixtures/utils/selector/pseudo-nth-child03/result.json
@@ -0,0 +1,7 @@
+{
+    "selector": ".foo:nth-child(even)",
+    "matches": [
+        "<div class=\"foo b\">"
+    ],
+    "errors": []
+}
\ No newline at end of file
diff --git a/tests/fixtures/utils/selector/pseudo-nth-child03/source.vue b/tests/fixtures/utils/selector/pseudo-nth-child03/source.vue
new file mode 100644
index 00000000..a1814b8d
--- /dev/null
+++ b/tests/fixtures/utils/selector/pseudo-nth-child03/source.vue
@@ -0,0 +1,14 @@
+<!-- .foo:nth-child(even) -->
+<template>
+  <div>
+    <div class="foo a"></div>
+    <div class="bar a"></div>
+    <div class="baz a"></div>
+    <div class="foo b"></div>
+    <div class="bar b"></div>
+    <div class="baz b"></div>
+    <div class="foo c"></div>
+    <div class="bar c"></div>
+    <div class="baz c"></div>
+  </div>
+</template>
diff --git a/tests/fixtures/utils/selector/pseudo-nth-last-child01/result.json b/tests/fixtures/utils/selector/pseudo-nth-last-child01/result.json
new file mode 100644
index 00000000..fecc3d71
--- /dev/null
+++ b/tests/fixtures/utils/selector/pseudo-nth-last-child01/result.json
@@ -0,0 +1,9 @@
+{
+    "selector": "tr:nth-last-child(-n+3)",
+    "matches": [
+        "<tr class=\"tr3\">",
+        "<tr class=\"tr4\">",
+        "<tr class=\"tr5\">"
+    ],
+    "errors": []
+}
\ No newline at end of file
diff --git a/tests/fixtures/utils/selector/pseudo-nth-last-child01/source.vue b/tests/fixtures/utils/selector/pseudo-nth-last-child01/source.vue
new file mode 100644
index 00000000..d812f4cd
--- /dev/null
+++ b/tests/fixtures/utils/selector/pseudo-nth-last-child01/source.vue
@@ -0,0 +1,21 @@
+<!-- tr:nth-last-child(-n+3) -->
+<template><table>
+  <tbody>
+    <tr class="tr1">
+      <td>First line</td>
+    </tr>
+    <tr class="tr2">
+      <td>Second line</td>
+    </tr>
+    <tr class="tr3">
+      <td>Third line</td>
+    </tr>
+    <tr class="tr4">
+      <td>Fourth line</td>
+    </tr>
+    <tr class="tr5">
+      <td>Fifth line</td>
+    </tr>
+  </tbody>
+</table>
+</template>
diff --git a/tests/fixtures/utils/selector/pseudo-nth-last-child02/result.json b/tests/fixtures/utils/selector/pseudo-nth-last-child02/result.json
new file mode 100644
index 00000000..056a0cef
--- /dev/null
+++ b/tests/fixtures/utils/selector/pseudo-nth-last-child02/result.json
@@ -0,0 +1,10 @@
+{
+    "selector": "tr:nth-last-child(n+2)",
+    "matches": [
+        "<tr class=\"tr1\">",
+        "<tr class=\"tr2\">",
+        "<tr class=\"tr3\">",
+        "<tr class=\"tr4\">"
+    ],
+    "errors": []
+}
\ No newline at end of file
diff --git a/tests/fixtures/utils/selector/pseudo-nth-last-child02/source.vue b/tests/fixtures/utils/selector/pseudo-nth-last-child02/source.vue
new file mode 100644
index 00000000..d1dd955a
--- /dev/null
+++ b/tests/fixtures/utils/selector/pseudo-nth-last-child02/source.vue
@@ -0,0 +1,21 @@
+<!-- tr:nth-last-child(n+2) -->
+<template><table>
+  <tbody>
+    <tr class="tr1">
+      <td>First line</td>
+    </tr>
+    <tr class="tr2">
+      <td>Second line</td>
+    </tr>
+    <tr class="tr3">
+      <td>Third line</td>
+    </tr>
+    <tr class="tr4">
+      <td>Fourth line</td>
+    </tr>
+    <tr class="tr5">
+      <td>Fifth line</td>
+    </tr>
+  </tbody>
+</table>
+</template>
diff --git a/tests/fixtures/utils/selector/pseudo-nth-last-child03/result.json b/tests/fixtures/utils/selector/pseudo-nth-last-child03/result.json
new file mode 100644
index 00000000..2689bca7
--- /dev/null
+++ b/tests/fixtures/utils/selector/pseudo-nth-last-child03/result.json
@@ -0,0 +1,7 @@
+{
+    "selector": "tr:nth-last-child(2)",
+    "matches": [
+        "<tr class=\"tr4\">"
+    ],
+    "errors": []
+}
\ No newline at end of file
diff --git a/tests/fixtures/utils/selector/pseudo-nth-last-child03/source.vue b/tests/fixtures/utils/selector/pseudo-nth-last-child03/source.vue
new file mode 100644
index 00000000..2ed0c08e
--- /dev/null
+++ b/tests/fixtures/utils/selector/pseudo-nth-last-child03/source.vue
@@ -0,0 +1,21 @@
+<!-- tr:nth-last-child(2) -->
+<template><table>
+  <tbody>
+    <tr class="tr1">
+      <td>First line</td>
+    </tr>
+    <tr class="tr2">
+      <td>Second line</td>
+    </tr>
+    <tr class="tr3">
+      <td>Third line</td>
+    </tr>
+    <tr class="tr4">
+      <td>Fourth line</td>
+    </tr>
+    <tr class="tr5">
+      <td>Fifth line</td>
+    </tr>
+  </tbody>
+</table>
+</template>
diff --git a/tests/fixtures/utils/selector/pseudo-nth-last-of-type01/result.json b/tests/fixtures/utils/selector/pseudo-nth-last-of-type01/result.json
new file mode 100644
index 00000000..5551832f
--- /dev/null
+++ b/tests/fixtures/utils/selector/pseudo-nth-last-of-type01/result.json
@@ -0,0 +1,7 @@
+{
+    "selector": "span:nth-last-of-type(2)",
+    "matches": [
+        "<span class=\"e\">"
+    ],
+    "errors": []
+}
\ No newline at end of file
diff --git a/tests/fixtures/utils/selector/pseudo-nth-last-of-type01/source.vue b/tests/fixtures/utils/selector/pseudo-nth-last-of-type01/source.vue
new file mode 100644
index 00000000..f70494be
--- /dev/null
+++ b/tests/fixtures/utils/selector/pseudo-nth-last-of-type01/source.vue
@@ -0,0 +1,11 @@
+<!-- span:nth-last-of-type(2) -->
+<template>
+  <div class="a">
+    <span class="b">This is a span.</span>
+    <span class="c">This is another span.</span>
+    <em class="d">This is emphasized.</em>
+    <span class="e">Wow, this span gets limed!!!</span>
+    <del class="f">This is struck through.</del>
+    <span class="g">Here is one last span.</span>
+  </div>
+</template>
diff --git a/tests/fixtures/utils/selector/pseudo-nth-last-of-type02/result.json b/tests/fixtures/utils/selector/pseudo-nth-last-of-type02/result.json
new file mode 100644
index 00000000..9562919e
--- /dev/null
+++ b/tests/fixtures/utils/selector/pseudo-nth-last-of-type02/result.json
@@ -0,0 +1,7 @@
+{
+    "selector": ":nth-last-of-type(2)",
+    "matches": [
+        "<span class=\"e\">"
+    ],
+    "errors": []
+}
\ No newline at end of file
diff --git a/tests/fixtures/utils/selector/pseudo-nth-last-of-type02/source.vue b/tests/fixtures/utils/selector/pseudo-nth-last-of-type02/source.vue
new file mode 100644
index 00000000..7f97ff7d
--- /dev/null
+++ b/tests/fixtures/utils/selector/pseudo-nth-last-of-type02/source.vue
@@ -0,0 +1,11 @@
+<!-- :nth-last-of-type(2) -->
+<template>
+  <div class="a">
+    <span class="b">This is a span.</span>
+    <span class="c">This is another span.</span>
+    <em class="d">This is emphasized.</em>
+    <span class="e">Wow, this span gets limed!!!</span>
+    <del class="f">This is struck through.</del>
+    <span class="g">Here is one last span.</span>
+  </div>
+</template>
diff --git a/tests/fixtures/utils/selector/pseudo-nth-of-type01/result.json b/tests/fixtures/utils/selector/pseudo-nth-of-type01/result.json
new file mode 100644
index 00000000..95f6d860
--- /dev/null
+++ b/tests/fixtures/utils/selector/pseudo-nth-of-type01/result.json
@@ -0,0 +1,8 @@
+{
+    "selector": "p:nth-of-type(2n+1)",
+    "matches": [
+        "<p class=\"c\">",
+        "<p class=\"fancy f\">"
+    ],
+    "errors": []
+}
\ No newline at end of file
diff --git a/tests/fixtures/utils/selector/pseudo-nth-of-type01/source.vue b/tests/fixtures/utils/selector/pseudo-nth-of-type01/source.vue
new file mode 100644
index 00000000..292700ec
--- /dev/null
+++ b/tests/fixtures/utils/selector/pseudo-nth-of-type01/source.vue
@@ -0,0 +1,11 @@
+<!-- p:nth-of-type(2n+1) -->
+<template>
+  <div class="a">
+    <div class="b">This element isn't counted.</div>
+    <p class="c">1st paragraph.</p>
+    <p class="fancy d">2nd paragraph.</p>
+    <div class="e">This element isn't counted.</div>
+    <p class="fancy f">3rd paragraph.</p>
+    <p class="g">4th paragraph.</p>
+  </div>
+</template>
diff --git a/tests/fixtures/utils/selector/pseudo-nth-of-type02/result.json b/tests/fixtures/utils/selector/pseudo-nth-of-type02/result.json
new file mode 100644
index 00000000..3a601673
--- /dev/null
+++ b/tests/fixtures/utils/selector/pseudo-nth-of-type02/result.json
@@ -0,0 +1,8 @@
+{
+    "selector": "p:nth-of-type(2n)",
+    "matches": [
+        "<p class=\"fancy d\">",
+        "<p class=\"g\">"
+    ],
+    "errors": []
+}
\ No newline at end of file
diff --git a/tests/fixtures/utils/selector/pseudo-nth-of-type02/source.vue b/tests/fixtures/utils/selector/pseudo-nth-of-type02/source.vue
new file mode 100644
index 00000000..27a68245
--- /dev/null
+++ b/tests/fixtures/utils/selector/pseudo-nth-of-type02/source.vue
@@ -0,0 +1,11 @@
+<!-- p:nth-of-type(2n) -->
+<template>
+  <div class="a">
+    <div class="b">This element isn't counted.</div>
+    <p class="c">1st paragraph.</p>
+    <p class="fancy d">2nd paragraph.</p>
+    <div class="e">This element isn't counted.</div>
+    <p class="fancy f">3rd paragraph.</p>
+    <p class="g">4th paragraph.</p>
+  </div>
+</template>
diff --git a/tests/fixtures/utils/selector/pseudo-nth-of-type03/result.json b/tests/fixtures/utils/selector/pseudo-nth-of-type03/result.json
new file mode 100644
index 00000000..3ff49b5a
--- /dev/null
+++ b/tests/fixtures/utils/selector/pseudo-nth-of-type03/result.json
@@ -0,0 +1,7 @@
+{
+    "selector": "p.fancy:nth-of-type(2n+1)",
+    "matches": [
+        "<p class=\"fancy f\">"
+    ],
+    "errors": []
+}
\ No newline at end of file
diff --git a/tests/fixtures/utils/selector/pseudo-nth-of-type03/source.vue b/tests/fixtures/utils/selector/pseudo-nth-of-type03/source.vue
new file mode 100644
index 00000000..d2c88382
--- /dev/null
+++ b/tests/fixtures/utils/selector/pseudo-nth-of-type03/source.vue
@@ -0,0 +1,11 @@
+<!-- p.fancy:nth-of-type(2n+1) -->
+<template>
+  <div class="a">
+    <div class="b">This element isn't counted.</div>
+    <p class="c">1st paragraph.</p>
+    <p class="fancy d">2nd paragraph.</p>
+    <div class="e">This element isn't counted.</div>
+    <p class="fancy f">3rd paragraph.</p>
+    <p class="g">4th paragraph.</p>
+  </div>
+</template>
diff --git a/tests/fixtures/utils/selector/pseudo-nth-of-type04/result.json b/tests/fixtures/utils/selector/pseudo-nth-of-type04/result.json
new file mode 100644
index 00000000..8570c4cc
--- /dev/null
+++ b/tests/fixtures/utils/selector/pseudo-nth-of-type04/result.json
@@ -0,0 +1,9 @@
+{
+    "selector": ":nth-of-type(2n)",
+    "matches": [
+        "<p class=\"fancy d\">",
+        "<div class=\"e\">",
+        "<p class=\"g\">"
+    ],
+    "errors": []
+}
\ No newline at end of file
diff --git a/tests/fixtures/utils/selector/pseudo-nth-of-type04/source.vue b/tests/fixtures/utils/selector/pseudo-nth-of-type04/source.vue
new file mode 100644
index 00000000..e8837158
--- /dev/null
+++ b/tests/fixtures/utils/selector/pseudo-nth-of-type04/source.vue
@@ -0,0 +1,11 @@
+<!-- :nth-of-type(2n) -->
+<template>
+  <div class="a">
+    <div class="b">This element isn't counted.</div>
+    <p class="c">1st paragraph.</p>
+    <p class="fancy d">2nd paragraph.</p>
+    <div class="e">This element isn't counted.</div>
+    <p class="fancy f">3rd paragraph.</p>
+    <p class="g">4th paragraph.</p>
+  </div>
+</template>
diff --git a/tests/fixtures/utils/selector/pseudo-only-child/result.json b/tests/fixtures/utils/selector/pseudo-only-child/result.json
new file mode 100644
index 00000000..b70fc9b1
--- /dev/null
+++ b/tests/fixtures/utils/selector/pseudo-only-child/result.json
@@ -0,0 +1,7 @@
+{
+    "selector": "li:only-child",
+    "matches": [
+        "<li class=\"d\">"
+    ],
+    "errors": []
+}
\ No newline at end of file
diff --git a/tests/fixtures/utils/selector/pseudo-only-child/source.vue b/tests/fixtures/utils/selector/pseudo-only-child/source.vue
new file mode 100644
index 00000000..ccf73b80
--- /dev/null
+++ b/tests/fixtures/utils/selector/pseudo-only-child/source.vue
@@ -0,0 +1,17 @@
+<!-- li:only-child -->
+<template>
+  <ol class="a">
+    <li class="b">First
+      <ul class="c">
+        <li class="d">This list has just one element.</li>
+      </ul>
+    </li>
+    <li class="e">Second
+      <ul class="f">
+        <li class="g">This list has three elements.</li>
+        <li class="h">This list has three elements.</li>
+        <li class="i">This list has three elements.</li>
+      </ul>
+    </li>
+  </ol>
+</template>
diff --git a/tests/fixtures/utils/selector/pseudo-only-of-type/result.json b/tests/fixtures/utils/selector/pseudo-only-of-type/result.json
new file mode 100644
index 00000000..330fcd4b
--- /dev/null
+++ b/tests/fixtures/utils/selector/pseudo-only-of-type/result.json
@@ -0,0 +1,8 @@
+{
+    "selector": "main :only-of-type",
+    "matches": [
+        "<p class=\"b\">",
+        "<i class=\"e\">"
+    ],
+    "errors": []
+}
\ No newline at end of file
diff --git a/tests/fixtures/utils/selector/pseudo-only-of-type/source.vue b/tests/fixtures/utils/selector/pseudo-only-of-type/source.vue
new file mode 100644
index 00000000..6a356941
--- /dev/null
+++ b/tests/fixtures/utils/selector/pseudo-only-of-type/source.vue
@@ -0,0 +1,13 @@
+<!-- main :only-of-type -->
+<template>
+  <main class="root">
+    <div class="a">I am `div` #1.</div>
+    <p class="b">I am the only `p` among my siblings.</p>
+    <div class="c">I am `div` #2.</div>
+    <div class="d">I am `div` #3.
+      <i class="e">I am the only `i` child.</i>
+      <em class="f">I am `em` #1.</em>
+      <em class="g">I am `em` #2.</em>
+    </div>
+  </main>
+</template>
diff --git a/tests/fixtures/utils/selector/pseudo-where/result.json b/tests/fixtures/utils/selector/pseudo-where/result.json
new file mode 100644
index 00000000..f7a3daad
--- /dev/null
+++ b/tests/fixtures/utils/selector/pseudo-where/result.json
@@ -0,0 +1,12 @@
+{
+    "selector": ":is(header, main, footer) p",
+    "matches": [
+        "<p class=\"b\">",
+        "<p class=\"f\">",
+        "<p class=\"g\">",
+        "<p class=\"i\">",
+        "<p class=\"j\">",
+        "<p class=\"l\">"
+    ],
+    "errors": []
+}
\ No newline at end of file
diff --git a/tests/fixtures/utils/selector/pseudo-where/source.vue b/tests/fixtures/utils/selector/pseudo-where/source.vue
new file mode 100644
index 00000000..f2147e21
--- /dev/null
+++ b/tests/fixtures/utils/selector/pseudo-where/source.vue
@@ -0,0 +1,17 @@
+<!-- :is(header, main, footer) p -->
+<template>
+  <header class="a">
+    <p class="b">This is my header paragraph</p>
+  </header>
+
+  <main class="c">
+    <ul class="d">
+      <li class="e"><p class="f">This is my first</p><p class="g">list item</p></li>
+      <li class="h"><p class="i">This is my second</p><p class="j">list item</p></li>
+    </ul>
+  </main>
+
+  <footer class="k">
+    <p class="l">This is my footer paragraph</p>
+  </footer>
+</template>
diff --git a/tests/fixtures/utils/selector/tag01/result.json b/tests/fixtures/utils/selector/tag01/result.json
new file mode 100644
index 00000000..a92bea02
--- /dev/null
+++ b/tests/fixtures/utils/selector/tag01/result.json
@@ -0,0 +1,9 @@
+{
+    "selector": "div",
+    "matches": [
+        "<div id=\"1\">",
+        "<div id=\"2\">",
+        "<div class=\"block\">"
+    ],
+    "errors": []
+}
\ No newline at end of file
diff --git a/tests/fixtures/utils/selector/tag01/source.vue b/tests/fixtures/utils/selector/tag01/source.vue
new file mode 100644
index 00000000..695f8978
--- /dev/null
+++ b/tests/fixtures/utils/selector/tag01/source.vue
@@ -0,0 +1,9 @@
+<!-- div -->
+<template>
+  <div id="1">
+    <div id="2"></div>
+    <p id="3"></p>
+    <input id="4">
+  </div>
+</template>
+<div class="block"></div>
diff --git a/tests/fixtures/utils/selector/tag02/result.json b/tests/fixtures/utils/selector/tag02/result.json
new file mode 100644
index 00000000..be68df38
--- /dev/null
+++ b/tests/fixtures/utils/selector/tag02/result.json
@@ -0,0 +1,7 @@
+{
+    "selector": "p",
+    "matches": [
+        "<p id=\"3\">"
+    ],
+    "errors": []
+}
\ No newline at end of file
diff --git a/tests/fixtures/utils/selector/tag02/source.vue b/tests/fixtures/utils/selector/tag02/source.vue
new file mode 100644
index 00000000..43985e61
--- /dev/null
+++ b/tests/fixtures/utils/selector/tag02/source.vue
@@ -0,0 +1,9 @@
+<!-- p -->
+<template>
+  <div id="1">
+    <div id="2"></div>
+    <p id="3"></p>
+    <input id="4">
+  </div>
+</template>
+<div class="block"></div>
diff --git a/tests/fixtures/utils/selector/universal/result.json b/tests/fixtures/utils/selector/universal/result.json
new file mode 100644
index 00000000..d679697d
--- /dev/null
+++ b/tests/fixtures/utils/selector/universal/result.json
@@ -0,0 +1,12 @@
+{
+    "selector": "*",
+    "matches": [
+        "<template>",
+        "<div id=\"1\">",
+        "<div id=\"foo\">",
+        "<p id=\"3\">",
+        "<input id=\"4\">",
+        "<div class=\"block\">"
+    ],
+    "errors": []
+}
\ No newline at end of file
diff --git a/tests/fixtures/utils/selector/universal/source.vue b/tests/fixtures/utils/selector/universal/source.vue
new file mode 100644
index 00000000..edc4361d
--- /dev/null
+++ b/tests/fixtures/utils/selector/universal/source.vue
@@ -0,0 +1,9 @@
+<!-- * -->
+<template>
+  <div id="1">
+    <div id="foo"></div>
+    <p id="3"></p>
+    <input id="4">
+  </div>
+</template>
+<div class="block"></div>
diff --git a/tests/lib/rules/component-tags-order.js b/tests/lib/rules/component-tags-order.js
index 474d04bf..94ba7a1b 100644
--- a/tests/lib/rules/component-tags-order.js
+++ b/tests/lib/rules/component-tags-order.js
@@ -147,7 +147,7 @@ tester.run('component-tags-order', rule, {
       ]
     },
     {
-      code: '<template></template><script></script><script setup></script><style scoped></style><style></style><i18n lang="ja"></i18n><i18n lang="en"></i18n>',
+      code: '<template></template><script></script><script setup></script><style scoped></style><style></style><i18n locale="ja"></i18n><i18n locale="en"></i18n>',
       output: null,
       options: [
         {
@@ -155,14 +155,14 @@ tester.run('component-tags-order', rule, {
             ['script:not([setup])', 'script[setup]', 'template'],
             'style[scoped]',
             'style:not([scoped])',
-            'i18n:not([lang=en])',
-            'i18n:not([lang=ja])'
+            'i18n:not([locale=en])',
+            'i18n:not([locale=ja])'
           ]
         }
       ]
     },
     {
-      code: '<template></template><script></script><script setup></script><style scoped></style><style></style><i18n lang="en"></i18n><i18n lang="ja"></i18n>',
+      code: '<template></template><script></script><script setup></script><style scoped></style><style></style><i18n locale="en"></i18n><i18n locale="ja"></i18n>',
       output: null,
       options: [
         {
@@ -172,8 +172,8 @@ tester.run('component-tags-order', rule, {
             'script[setup]',
             'style[scoped]',
             'style:not([scoped])',
-            'i18n[lang=en]',
-            'i18n[lang=ja]'
+            'i18n[locale=en]',
+            'i18n[locale=ja]'
           ]
         }
       ]
diff --git a/tests/lib/rules/match-component-import-name.js b/tests/lib/rules/match-component-import-name.js
new file mode 100644
index 00000000..a05bfec1
--- /dev/null
+++ b/tests/lib/rules/match-component-import-name.js
@@ -0,0 +1,73 @@
+/**
+ * @author Doug Wade
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/match-component-import-name')
+
+const tester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: {
+    ecmaVersion: 2020,
+    sourceType: 'module'
+  }
+})
+
+tester.run('match-component-import-name', rule, {
+  valid: [
+    {
+      filename: 'test.vue',
+      code: `
+      <script> export default { components: { ValidImport } } </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script> export default { components: { ValidImport: ValidImport } } </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script> export default { components: { 'valid-import': ValidImport } } </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script> export default { components: { ValidImport, ...SpreadImport } } </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script> export default { components: { 'valid-import': ValidImport, [computedImport]: ComputedImport } } </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script> export default { components: { ValidImport, [differentComputedImport]: ComputedImport } } </script>
+      `
+    }
+  ],
+  invalid: [
+    {
+      filename: 'test.vue',
+      code: `
+      <script> export default { components: { InvalidExport: SomeRandomName } } </script>
+      `,
+      errors: [
+        {
+          message:
+            'Component alias InvalidExport should be one of: SomeRandomName, some-random-name.',
+          line: 2,
+          column: 47
+        }
+      ]
+    }
+  ]
+})
diff --git a/tests/lib/rules/multi-word-component-names.js b/tests/lib/rules/multi-word-component-names.js
index bb0fba77..d11b11d3 100644
--- a/tests/lib/rules/multi-word-component-names.js
+++ b/tests/lib/rules/multi-word-component-names.js
@@ -202,6 +202,17 @@ tester.run('multi-word-component-names', rule, {
           },
       }).$mount('#app')
       `
+    },
+    {
+      filename: 'MultiWord.vue',
+      code: `
+      <template>
+        <AppButton />
+      </template>
+      
+      <script setup lang="ts">
+      import AppButton from "@/components/AppButton.vue";
+      </script>`
     }
   ],
   invalid: [
@@ -309,6 +320,19 @@ tester.run('multi-word-component-names', rule, {
           line: 4
         }
       ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup lang="ts">
+      import Item from "@/components/Item.vue";
+      </script>`,
+      errors: [
+        {
+          message: 'Component name "test" should always be multi-word.',
+          line: 1
+        }
+      ]
     }
   ]
 })
diff --git a/tests/lib/utils/selector.js b/tests/lib/utils/selector.js
new file mode 100644
index 00000000..6677a9fa
--- /dev/null
+++ b/tests/lib/utils/selector.js
@@ -0,0 +1,88 @@
+'use strict'
+
+const fs = require('fs')
+const path = require('path')
+const assert = require('assert')
+
+const Linter = require('eslint').Linter
+
+const selector = require('../../../lib/utils/selector')
+const utils = require('../../../lib/utils')
+
+const FIXTURE_ROOT = path.resolve(__dirname, '../../fixtures/utils/selector')
+
+/**
+ * Load test patterns from fixtures.
+ *
+ * @returns {object} The loaded patterns.
+ */
+function loadPatterns() {
+  return fs.readdirSync(FIXTURE_ROOT).map((name) => {
+    const code0 = fs.readFileSync(
+      path.join(FIXTURE_ROOT, name, 'source.vue'),
+      'utf8'
+    )
+    const code = code0.replace(/^<!--(.+?)-->/, `<!--${name}-->`)
+    const inputSelector = /^<!--(.+?)-->/.exec(code0)[1].trim()
+    return { code, name, inputSelector }
+  })
+}
+
+function extractElements(code, inputSelector) {
+  const linter = new Linter()
+  const matches = []
+
+  linter.defineRule('vue/selector-test', (context) => {
+    const parsed = selector.parseSelector(inputSelector, context)
+    return utils.defineDocumentVisitor(context, {
+      VElement(node) {
+        if (parsed.test(node)) {
+          matches.push(
+            context.getSourceCode().text.slice(...node.startTag.range)
+          )
+        }
+      }
+    })
+  })
+  linter.defineParser('vue-eslint-parser', require('vue-eslint-parser'))
+  const messages = linter.verify(
+    code,
+    {
+      parser: 'vue-eslint-parser',
+      parserOptions: { ecmaVersion: 2018 },
+      rules: { 'vue/selector-test': 'error' }
+    },
+    undefined,
+    true
+  )
+
+  return {
+    selector: inputSelector,
+    matches,
+    errors: messages.map((message) => message.message)
+  }
+}
+
+describe('parseSelector()', () => {
+  for (const { name, code, inputSelector } of loadPatterns()) {
+    describe(`'test/fixtures/utils/selector/${name}/source.vue'`, () => {
+      it('should to parse the selector to match the valid elements.', () => {
+        const elements = extractElements(code, inputSelector)
+        const actual = JSON.stringify(elements, null, 4)
+
+        // update fixture
+        // fs.writeFileSync(
+        //   path.join(FIXTURE_ROOT, name, 'result.json'),
+        //   actual,
+        //   'utf8'
+        // )
+
+        const expected = fs.readFileSync(
+          path.join(FIXTURE_ROOT, name, 'result.json'),
+          'utf8'
+        )
+        assert.strictEqual(actual, expected)
+      })
+    })
+  }
+})