diff --git a/docs/rules/README.md b/docs/rules/README.md
index 97117319f..777c6caf6 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -155,6 +155,7 @@ For example:
| [vue/no-deprecated-scope-attribute](./no-deprecated-scope-attribute.md) | disallow deprecated `scope` attribute (in Vue.js 2.5.0+) | :wrench: |
| [vue/no-empty-pattern](./no-empty-pattern.md) | disallow empty destructuring patterns | |
| [vue/no-restricted-syntax](./no-restricted-syntax.md) | disallow specified syntax | |
+| [vue/no-unused-properties](./no-unused-properties.md) | disallow unused properties, data and computed properties | |
| [vue/object-curly-spacing](./object-curly-spacing.md) | enforce consistent spacing inside braces | :wrench: |
| [vue/require-direct-export](./require-direct-export.md) | require the component to be directly exported | |
| [vue/script-indent](./script-indent.md) | enforce consistent indentation in `
+```
+
+```vue
+/* ✗ BAD (`count` property not used) */
+
+
+ {{ cnt }}
+
+
+
+```
+
+```vue
+/* ✓ GOOD */
+
+
+```
+
+```vue
+/* ✓ BAD (`count` data not used) */
+
+
+```
+
+```vue
+/* ✓ GOOD */
+
+
+ {{ reversedMessage }}
+
+
+
+```
+
+```vue
+/* ✓ BAD (`reversedMessage` computed property not used) */
+
+
+ {{ message }}
+
+
+
+```
+
+## :wrench: Options
+
+None.
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-unused-properties.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-unused-properties.js)
diff --git a/lib/rules/no-unused-properties.js b/lib/rules/no-unused-properties.js
new file mode 100644
index 000000000..7897788b2
--- /dev/null
+++ b/lib/rules/no-unused-properties.js
@@ -0,0 +1,167 @@
+/**
+ * @fileoverview Disallow unused properties, data and computed properties.
+ * @author Learning Equality
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const remove = require('lodash/remove')
+const utils = require('../utils')
+
+// ------------------------------------------------------------------------------
+// Constants
+// ------------------------------------------------------------------------------
+
+const GROUP_PROPERTY = 'props'
+const GROUP_DATA = 'data'
+const GROUP_COMPUTED_PROPERTY = 'computed'
+const GROUP_WATCHER = 'watch'
+
+const PROPERTY_LABEL = {
+ [GROUP_PROPERTY]: 'property',
+ [GROUP_DATA]: 'data',
+ [GROUP_COMPUTED_PROPERTY]: 'computed property'
+}
+
+// ------------------------------------------------------------------------------
+// Helpers
+// ------------------------------------------------------------------------------
+
+/**
+ * Extract names from references objects.
+ */
+const getReferencesNames = references => {
+ if (!references || !references.length) {
+ return []
+ }
+
+ return references.map(reference => {
+ if (!reference.id || !reference.id.name) {
+ return
+ }
+
+ return reference.id.name
+ })
+}
+
+/**
+ * Report all unused properties.
+ */
+const reportUnusedProperties = (context, properties) => {
+ if (!properties || !properties.length) {
+ return
+ }
+
+ properties.forEach(property => {
+ context.report({
+ node: property.node,
+ message: `Unused ${PROPERTY_LABEL[property.groupName]} found: "${property.name}"`
+ })
+ })
+}
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+ meta: {
+ type: 'suggestion',
+ docs: {
+ description: 'disallow unused properties, data and computed properties',
+ category: undefined,
+ url: 'https://eslint.vuejs.org/rules/no-unused-properties.html'
+ },
+ fixable: null,
+ schema: []
+ },
+
+ create (context) {
+ let hasTemplate
+ let rootTemplateEnd
+ let unusedProperties = []
+ const thisExpressionsVariablesNames = []
+
+ const initialize = {
+ Program (node) {
+ if (context.parserServices.getTemplateBodyTokenStore == null) {
+ context.report({
+ loc: { line: 1, column: 0 },
+ message:
+ 'Use the latest vue-eslint-parser. See also https://vuejs.github.io/eslint-plugin-vue/user-guide/#what-is-the-use-the-latest-vue-eslint-parser-error.'
+ })
+ return
+ }
+
+ hasTemplate = Boolean(node.templateBody)
+ }
+ }
+
+ const scriptVisitor = Object.assign(
+ {},
+ {
+ 'MemberExpression[object.type="ThisExpression"][property.type="Identifier"][property.name]' (
+ node
+ ) {
+ thisExpressionsVariablesNames.push(node.property.name)
+ }
+ },
+ utils.executeOnVue(context, obj => {
+ unusedProperties = Array.from(
+ utils.iterateProperties(obj, new Set([GROUP_PROPERTY, GROUP_DATA, GROUP_COMPUTED_PROPERTY]))
+ )
+
+ const watchers = Array.from(utils.iterateProperties(obj, new Set([GROUP_WATCHER])))
+ const watchersNames = watchers.map(watcher => watcher.name)
+
+ remove(unusedProperties, property => {
+ return (
+ thisExpressionsVariablesNames.includes(property.name) ||
+ watchersNames.includes(property.name)
+ )
+ })
+
+ if (!hasTemplate && unusedProperties.length) {
+ reportUnusedProperties(context, unusedProperties)
+ }
+ })
+ )
+
+ const templateVisitor = {
+ 'VExpressionContainer[expression!=null][references]' (node) {
+ const referencesNames = getReferencesNames(node.references)
+
+ remove(unusedProperties, property => {
+ return referencesNames.includes(property.name)
+ })
+ },
+ // save root template end location - just a helper to be used
+ // for a decision if a parser reached the end of the root template
+ "VElement[name='template']" (node) {
+ if (rootTemplateEnd) {
+ return
+ }
+
+ rootTemplateEnd = node.loc.end
+ },
+ "VElement[name='template']:exit" (node) {
+ if (node.loc.end !== rootTemplateEnd) {
+ return
+ }
+
+ if (unusedProperties.length) {
+ reportUnusedProperties(context, unusedProperties)
+ }
+ }
+ }
+
+ return Object.assign(
+ {},
+ initialize,
+ utils.defineTemplateBodyVisitor(context, templateVisitor, scriptVisitor)
+ )
+ }
+}
diff --git a/tests/lib/rules/no-unused-properties.js b/tests/lib/rules/no-unused-properties.js
new file mode 100644
index 000000000..e80216468
--- /dev/null
+++ b/tests/lib/rules/no-unused-properties.js
@@ -0,0 +1,646 @@
+/**
+ * @fileoverview Disallow unused properties, data and computed properties.
+ * @author Learning Equality
+ */
+'use strict'
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/no-unused-properties')
+
+const tester = new RuleTester({
+ parser: 'vue-eslint-parser',
+ parserOptions: {
+ ecmaVersion: 2018,
+ sourceType: 'module'
+ }
+})
+
+tester.run('no-unused-properties', rule, {
+ valid: [
+ // a property used in a script expression
+ {
+ filename: 'test.vue',
+ code: `
+
+ `
+ },
+
+ // a property being watched
+ {
+ filename: 'test.vue',
+ code: `
+
+ `
+ },
+
+ // a property used as a template identifier
+ {
+ filename: 'test.vue',
+ code: `
+
+ {{ count }}
+
+
+ `
+ },
+
+ // properties used in a template expression
+ {
+ filename: 'test.vue',
+ code: `
+
+ {{ count1 + count2 }}
+
+
+ `
+ },
+
+ // a property used in v-if
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+ `
+ },
+
+ // a property used in v-for
+ {
+ filename: 'test.vue',
+ code: `
+
+ {{ color }}
+
+
+ `
+ },
+
+ // a property used in v-html
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+ `
+ },
+
+ // a property passed in a component
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+ `
+ },
+
+ // a property used in v-on
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+ `
+ },
+
+ // data used in a script expression
+ {
+ filename: 'test.vue',
+ code: `
+
+ `
+ },
+
+ // data being watched
+ {
+ filename: 'test.vue',
+ code: `
+
+ `
+ },
+
+ // data used as a template identifier
+ {
+ filename: 'test.vue',
+ code: `
+
+ {{ count }}
+
+
+ `
+ },
+
+ // data used in a template expression
+ {
+ filename: 'test.vue',
+ code: `
+
+ {{ count1 + count2 }}
+
+
+ `
+ },
+
+ // data used in v-if
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+ `
+ },
+
+ // data used in v-for
+ {
+ filename: 'test.vue',
+ code: `
+
+ {{ color }}
+
+
+ `
+ },
+
+ // data used in v-html
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+ `
+ },
+
+ // data used in v-model
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+ `
+ },
+
+ // data passed in a component
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+ `
+ },
+
+ // data used in v-on
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+ `
+ },
+
+ // computed property used in a script expression
+ {
+ filename: 'test.vue',
+ code: `
+
+ `
+ },
+
+ // computed property being watched
+ {
+ filename: 'test.vue',
+ code: `
+
+ `
+ },
+
+ // computed property used as a template identifier
+ {
+ filename: 'test.vue',
+ code: `
+
+ {{ count }}
+
+
+ `
+ },
+
+ // computed properties used in a template expression
+ {
+ filename: 'test.vue',
+ code: `
+
+ {{ count1 + count2 }}
+
+
+ `
+ },
+
+ // computed property used in v-if
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+ `
+ },
+
+ // computed property used in v-for
+ {
+ filename: 'test.vue',
+ code: `
+
+ {{ color }}
+
+
+ `
+ },
+
+ // computed property used in v-html
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+ `
+ },
+
+ // computed property used in v-model
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+ `
+ },
+
+ // computed property passed in a component
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+ `
+ },
+
+ // ignores unused data when marked with eslint-disable
+ {
+ filename: 'test.vue',
+ code: `
+
+ {{ cont }}
+
+
+ `
+ }
+ ],
+
+ invalid: [
+ // unused property
+ {
+ filename: 'test.vue',
+ code: `
+
+ {{ cont }}
+
+
+ `,
+ errors: [
+ {
+ message: 'Unused property found: "count"',
+ line: 7
+ }
+ ]
+ },
+
+ // unused data
+ {
+ filename: 'test.vue',
+ code: `
+
+ {{ cont }}
+
+
+ `,
+ errors: [
+ {
+ message: 'Unused data found: "count"',
+ line: 9
+ }
+ ]
+ },
+
+ // unused computed property
+ {
+ filename: 'test.vue',
+ code: `
+
+ {{ cont }}
+
+
+ `,
+ errors: [
+ {
+ message: 'Unused computed property found: "count"',
+ line: 8
+ }
+ ]
+ }
+ ]
+})