Repository: vuejs/vue
Pull request: feat(warn): warn when an existing property starting with $ is not pro…

…perly accessed

Closes #8213

<!--
Please make sure to read the Pull Request Guidelines:
https://github.com/vuejs/vue/blob/dev/.github/CONTRIBUTING.md#pull-request-guidelines
-->

<!-- PULL REQUEST TEMPLATE -->
<!-- (Update "[ ]" to "[x]" to check a box) -->

**What kind of change does this PR introduce?** (check at least one)

- [ ] Bugfix
- [x] Feature
- [ ] Code style update
- [ ] Refactor
- [ ] Build-related changes
- [ ] Other, please describe:

**Does this PR introduce a breaking change?** (check one)

- [ ] Yes
- [x] No

If yes, please describe the impact and migration path for existing applications:

**The PR fulfills these requirements:**

- [x] It's submitted to the `dev` branch for v2.x (or to a previous version branch), _not_ the `master` branch
- [x] When resolving a specific issue, it's referenced in the PR's title (e.g. `fix #xxx[,#xxx]`, where "xxx" is the issue number)
- [x] All tests are passing: https://github.com/vuejs/vue/blob/dev/.github/CONTRIBUTING.md#development-setup
- [x] New/updated tests are included

If adding a **new feature**, the PR's description includes:
- [ ] A convincing reason for adding this feature (to avoid wasting your time, it's best to open a suggestion issue first and wait for approval before working on it)

**Other information:**


Changed files:
filename: src/core/instance/proxy.js
patch: @@ -24,6 +24,16 @@ if (process.env.NODE_ENV !== 'production') {
     )
   }
 
+  const warnReservedPrefix = (target, key) => {
+    warn(
+      `Property "${key}" must be accessed with "$data.${key}" because ` +
+      'properties starting with "$" or "_" are not proxied in the Vue instance to ' +
+      'prevent conflicts with Vue internals' +
+      'See: https://vuejs.org/v2/api/#data',
+      target
+    )
+  }
+
   const hasProxy =
     typeof Proxy !== 'undefined' && isNative(Proxy)
 
@@ -45,9 +55,11 @@ if (process.env.NODE_ENV !== 'production') {
   const hasHandler = {
     has (target, key) {
       const has = key in target
-      const isAllowed = allowedGlobals(key) || (typeof key === 'string' && key.charAt(0) === '_')
+      const isAllowed = allowedGlobals(key) ||
+        (typeof key === 'string' && key.charAt(0) === '_' && !(key in target.$data))
       if (!has && !isAllowed) {
-        warnNonPresent(target, key)
+        if (key in target.$data) warnReservedPrefix(target, key)
+        else warnNonPresent(target, key)
       }
       return has || !isAllowed
     }
@@ -56,7 +68,8 @@ if (process.env.NODE_ENV !== 'production') {
   const getHandler = {
     get (target, key) {
       if (typeof key === 'string' && !(key in target)) {
-        warnNonPresent(target, key)
+        if (key in target.$data) warnReservedPrefix(target, key)
+        else warnNonPresent(target, key)
       }
       return target[key]
     }

filename: test/unit/features/instance/render-proxy.spec.js
patch: @@ -45,5 +45,49 @@ if (typeof Proxy !== 'undefined') {
 
       expect(vm.$el.textContent).toBe('foo')
     })
+
+    it('should warn properties starting with $ when found', () => {
+      new Vue({
+        data: { $a: 'foo' },
+        template: `<div>{{ $a }}</div>`
+      }).$mount()
+      expect(`Property "$a" must be accessed with "$data.$a"`).toHaveBeenWarned()
+    })
+
+    it('should warn properties starting with _ when found', () => {
+      new Vue({
+        data: { _foo: 'foo' },
+        template: `<div>{{ _foo }}</div>`
+      }).$mount()
+      expect(`Property "_foo" must be accessed with "$data._foo"`).toHaveBeenWarned()
+    })
+
+    it('should warn properties starting with $ when not found', () => {
+      new Vue({
+        template: `<div>{{ $a }}</div>`
+      }).$mount()
+      expect(`Property or method "$a" is not defined`).toHaveBeenWarned()
+      expect(`Property "$a" must be accessed with "$data.$a"`).not.toHaveBeenWarned()
+    })
+
+    it('should warn properties starting with $ when not found (with stripped)', () => {
+      const render = function (h) {
+        return h('p', this.$a)
+      }
+      render._withStripped = true
+      new Vue({
+        data: { $a: 'foo' },
+        render
+      }).$mount()
+      expect(`Property "$a" must be accessed with "$data.$a"`).toHaveBeenWarned()
+    })
+
+    it('should not warn properties starting with $ when using $data to access', () => {
+      new Vue({
+        data: { $a: 'foo' },
+        template: `<div>{{ $data.$a }}</div>`
+      }).$mount()
+      expect(`Property or method "$a" is not defined`).not.toHaveBeenWarned()
+    })
   })
 }