Repository: vuejs/eslint-plugin-vue
Pull request #1842 (2022-04-11 04:35:57)

Improve usage of postcss-selector-parser
This PR followup for #1825.

~~Also, fixing the linting errors. (Circle CI didn't seem to work well 🤔)~~ Fixed #1843
Also, I made a small change in the documentation.

ota-meshi: > Circle CI didn't seem to work well 🤔

The CI for pull requests from some forks doesn't seem to work. But I don't know the rules. The settings appear to be set to build pull requests from forks as well.
ota-meshi: Thank you for checking this PR.
I changed this pull request. So please check it again?

Patch:
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/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/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/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)
+      })
+    })
+  }
+})