Repository: vuejs/eslint-plugin-vue (js)

Issue:
`no-setup-props-reactivity-loss` should report template string expressions
**Checklist**

- [x] I have tried restarting my IDE and the issue persists.
- [x] I have read the [FAQ](https://eslint.vuejs.org/user-guide/#faq) and my problem is not listed.

**Tell us about your environment**

- **ESLint version:** 8.57.0
- **eslint-plugin-vue version:** 9.26.0
- **Vue version:** 3.4.27
- **Node version:** 20.12.2
- **Operating System:** macOS Sonoma 14.5

**Please show your full configuration:**
<!-- Paste content of your .eslintrc file -->
```json5
{
  rules: {
    'vue/no-setup-props-reactivity-loss': 'error'
  }
}
```

**What did you do?**	
<!-- Please include the actual source code causing the issue. -->
```vue
<template>{{ foo }}</template>

<script setup lang="ts">
const props = defineProps<{
  foo: string
}>()

const foo = `${props.foo}`
</script>
```

**What did you expect to happen?**

Following code should throw an ESLint error

```ts
const foo = `${props.foo}`
```

**What actually happened?**

No error

**Link to reproduce this issue**

https://ota-meshi.github.io/eslint-plugin-vue-demo/#eJxdz9sKwjAMBuBXCUXYBOvAS083IvgQu9jYMizMpKTtUErf3VJExLscvsCfqAYeUe3V0Q1irD+3ZB6WxUOEESdDeOHcE5KHBJPwA6olYHVoCZ/FZdWH2f/rOrYE4NAHW1th69ZQJgADk/Nw7xe8irDACcp+OzEffoW7c5jH24/rVvFLU1dw2rSU1rk8Np8H1EZJmNGpfVQ5aUOsSwpdTrVgP3izGP/SM7usdim9AQOUXZw=


Pull request description:
fix(no-setup-props-reactivity-loss): report template literal
Keen for feedback 🫡 , fix #2470


Patch:
diff --git a/docs/.vitepress/build-system/build.mts b/docs/.vitepress/build-system/build.mts
index 79521b52..9dce53fa 100644
--- a/docs/.vitepress/build-system/build.mts
+++ b/docs/.vitepress/build-system/build.mts
@@ -2,7 +2,7 @@
  * Pre-build cjs packages that cannot be bundled well.
  */
 import esbuild from 'esbuild'
-import path from 'path'
+import path from 'pathe'
 import fs from 'fs'
 import { fileURLToPath } from 'url'
 
diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts
index c15f0233..77c052a5 100644
--- a/docs/.vitepress/config.mts
+++ b/docs/.vitepress/config.mts
@@ -1,6 +1,6 @@
 import type { DefaultTheme } from 'vitepress'
 import { defineConfig } from 'vitepress'
-import path from 'path'
+import path from 'pathe'
 import { fileURLToPath } from 'url'
 import { viteCommonjs, vitePluginRequireResolve } from './vite-plugin.mjs'
 
diff --git a/docs/.vitepress/vite-plugin.mts b/docs/.vitepress/vite-plugin.mts
index ba1ca7db..cd6811cb 100644
--- a/docs/.vitepress/vite-plugin.mts
+++ b/docs/.vitepress/vite-plugin.mts
@@ -1,5 +1,5 @@
 import type { UserConfig } from 'vitepress'
-import path from 'path'
+import path from 'pathe'
 import { fileURLToPath } from 'url'
 import esbuild from 'esbuild'
 type Plugin = Extract<
diff --git a/docs/user-guide/index.md b/docs/user-guide/index.md
index df713852..56195056 100644
--- a/docs/user-guide/index.md
+++ b/docs/user-guide/index.md
@@ -146,6 +146,48 @@ If you want to use custom parsers such as [@babel/eslint-parser](https://www.npm
   }
 ```
 
+Full example:
+
+::: code-group
+
+```json [.eslintrc]
+{
+  "root": true,
+  "plugins": ["@typescript-eslint"],
+  "extends": [
+    "eslint:recommended",
+    "plugin:@typescript-eslint/recommended",
+    "plugin:vue/vue3-recommended"
+  ],
+  "parser": "vue-eslint-parser",
+  "parserOptions": {
+    "parser": "@typescript-eslint/parser"
+  }
+}
+```
+
+```js [eslint.config.js]
+import js from '@eslint/js'
+import eslintPluginVue from 'eslint-plugin-vue'
+import ts from 'typescript-eslint'
+
+export default ts.config(
+  js.configs.recommended,
+  ...ts.configs.recommended,
+  ...eslintPluginVue.configs['flat/recommended'],
+  {
+    files: ['*.vue', '**/*.vue'],
+    languageOptions: {
+      parserOptions: {
+        parser: '@typescript-eslint/parser'
+      }
+    }
+  }
+)
+```
+
+:::
+
 The `parserOptions.parser` option can also specify an object to specify multiple parsers. See [vue-eslint-parser README](https://github.com/vuejs/vue-eslint-parser#readme) for more details.
 
 ### How does ESLint detect components?
@@ -441,8 +483,7 @@ In [Nuxt 3](https://nuxt.com/) or with [`unplugin-auto-import`](https://github.c
 
 ::: code-group
 
-```json [Legacy Config]
-// .eslintrc
+```json [.eslintrc]
 {
   "globals": {
     "ref": "readonly",
@@ -454,8 +495,7 @@ In [Nuxt 3](https://nuxt.com/) or with [`unplugin-auto-import`](https://github.c
 }
 ```
 
-```js [Flat Config]
-// eslint.config.js
+```js [eslint.config.js]
 export default [
   {
     languageOptions: {
diff --git a/lib/rules/no-setup-props-reactivity-loss.js b/lib/rules/no-setup-props-reactivity-loss.js
index b8e89607..4c3be8b9 100644
--- a/lib/rules/no-setup-props-reactivity-loss.js
+++ b/lib/rules/no-setup-props-reactivity-loss.js
@@ -79,18 +79,35 @@ module.exports = {
         left.type !== 'ArrayPattern' &&
         left.type !== 'ObjectPattern' &&
         rightNode.type !== 'MemberExpression' &&
-        rightNode.type !== 'ConditionalExpression'
+        rightNode.type !== 'ConditionalExpression' &&
+        rightNode.type !== 'TemplateLiteral'
       ) {
         return
       }
 
-      /** @type {Expression | Super} */
-      let rightId = rightNode
+      if (rightNode.type === 'TemplateLiteral') {
+        rightNode.expressions.some((expression) =>
+          checkMemberAccess(expression, propsReferences, left, right)
+        )
+      } else {
+        checkMemberAccess(rightNode, propsReferences, left, right)
+      }
+    }
+
+    /**
+     * @param {Expression | Super} rightId
+     * @param {ScopePropsReferences} propsReferences
+     * @param {Pattern} left
+     * @param {Expression} right
+     * @return {boolean}
+     */
+    function checkMemberAccess(rightId, propsReferences, left, right) {
       while (rightId.type === 'MemberExpression') {
         rightId = utils.skipChainExpression(rightId.object)
       }
       if (rightId.type === 'Identifier' && propsReferences.refs.has(rightId)) {
         report(left, 'getProperty', propsReferences.scopeName)
+        return true
       }
       if (
         rightId.type === 'ConditionalExpression' &&
@@ -99,7 +116,9 @@ module.exports = {
           isPropsMemberAccessed(rightId.alternate, propsReferences))
       ) {
         report(right, 'getProperty', propsReferences.scopeName)
+        return true
       }
+      return false
     }
 
     /**
diff --git a/lib/rules/order-in-components.js b/lib/rules/order-in-components.js
index 38624d00..22d90920 100644
--- a/lib/rules/order-in-components.js
+++ b/lib/rules/order-in-components.js
@@ -259,6 +259,7 @@ module.exports = {
     }
 
     /**
+     * @param {RuleFixer} fixer
      * @param {Property} propertyNode
      * @param {Property} unorderedPropertyNode
      */
diff --git a/lib/rules/require-toggle-inside-transition.js b/lib/rules/require-toggle-inside-transition.js
index fc46f75f..423d4beb 100644
--- a/lib/rules/require-toggle-inside-transition.js
+++ b/lib/rules/require-toggle-inside-transition.js
@@ -6,6 +6,20 @@
 
 const utils = require('../utils')
 
+/**
+ * @param {VDirective} vBindAppear
+ */
+function isValidBindAppear(vBindAppear) {
+  if (
+    vBindAppear.value?.expression &&
+    vBindAppear.value.expression.type === 'Literal'
+  ) {
+    return vBindAppear.value.expression?.value !== false
+  }
+
+  return true
+}
+
 module.exports = {
   meta: {
     type: 'problem',
@@ -35,7 +49,11 @@ module.exports = {
 
       /** @type VElement */ // @ts-expect-error
       const parent = element.parent
-      if (utils.hasAttribute(parent, 'appear')) {
+      const vBindAppear = utils.getDirective(parent, 'bind', 'appear')
+      if (
+        utils.hasAttribute(parent, 'appear') ||
+        (vBindAppear && isValidBindAppear(vBindAppear))
+      ) {
         return
       }
 
diff --git a/package.json b/package.json
index 8cb321f2..b35fd5da 100644
--- a/package.json
+++ b/package.json
@@ -61,7 +61,7 @@
     "nth-check": "^2.1.1",
     "postcss-selector-parser": "^6.0.15",
     "semver": "^7.6.0",
-    "vue-eslint-parser": "^9.4.2",
+    "vue-eslint-parser": "^9.4.3",
     "xml-name-validator": "^4.0.0"
   },
   "devDependencies": {
@@ -92,6 +92,7 @@
     "markdownlint-cli": "^0.41.0",
     "mocha": "^10.3.0",
     "nyc": "^17.0.0",
+    "pathe": "^1.1.2",
     "prettier": "^3.2.5",
     "typescript": "^5.3.3",
     "vitepress": "^1.0.0-rc.42"


Test patch:
diff --git a/tests/lib/rules/no-setup-props-reactivity-loss.js b/tests/lib/rules/no-setup-props-reactivity-loss.js
index 6321ea57..edf721d8 100644
--- a/tests/lib/rules/no-setup-props-reactivity-loss.js
+++ b/tests/lib/rules/no-setup-props-reactivity-loss.js
@@ -144,6 +144,7 @@ tester.run('no-setup-props-reactivity-loss', rule, {
           const {x} = noProps
           ({y} = noProps)
           const z = noProps.z
+          const foo = \`\${noProp.foo}\`
         }
       }
       </script>
@@ -717,6 +718,43 @@ tester.run('no-setup-props-reactivity-loss', rule, {
           line: 4
         }
       ]
+    },
+    {
+      // https://github.com/vuejs/eslint-plugin-vue/issues/2470
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        setup(p) {
+          const foo = \`\${p.x}\`
+        }
+      }
+      </script>
+      `,
+      errors: [
+        {
+          messageId: 'getProperty',
+          line: 5
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        setup(p) {
+          const foo = \`bar\${p.x}bar\${p.y}\`
+        }
+      }
+      </script>
+      `,
+      errors: [
+        {
+          messageId: 'getProperty',
+          line: 5
+        }
+      ]
     }
   ]
 })
diff --git a/tests/lib/rules/require-toggle-inside-transition.js b/tests/lib/rules/require-toggle-inside-transition.js
index 815f5de3..d40ad474 100644
--- a/tests/lib/rules/require-toggle-inside-transition.js
+++ b/tests/lib/rules/require-toggle-inside-transition.js
@@ -68,6 +68,15 @@ tester.run('require-toggle-inside-transition', rule, {
     {
       filename: 'test.vue',
       code: '<template><transition appear><div /></transition></template>'
+    },
+    {
+      // https://github.com/vuejs/eslint-plugin-vue/issues/2467
+      filename: 'test.vue',
+      code: '<template><transition :appear="foo"><div /></transition></template>'
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><transition :appear="true"><div /></transition></template>'
     }
   ],
   invalid: [
@@ -113,6 +122,16 @@ tester.run('require-toggle-inside-transition', rule, {
       filename: 'test.vue',
       code: '<template><Transition>  <div /></Transition></template>',
       errors: [{ messageId: 'expected' }]
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><transition :appear="false"><div /></transition></template>',
+      errors: [{ messageId: 'expected' }]
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><transition @appear="isLoaded"><div /></transition></template>',
+      errors: [{ messageId: 'expected' }]
     }
   ]
 })