Repository: vuejs/eslint-plugin-vue
Pull request #1164 (2020-05-26 08:36:00)

Fixed false negatives when `v-for` and `v-slot` mixed or use destructuring for `vue/no-unused-var` rule.


Patch:
diff --git a/docs/rules/html-indent.md b/docs/rules/html-indent.md
index 81e22585..d82ff86e 100644
--- a/docs/rules/html-indent.md
+++ b/docs/rules/html-indent.md
@@ -76,7 +76,11 @@ This rule enforces a consistent indentation style in `<template>`. The default s
 - `type` (`number | "tab"`) ... The type of indentation. Default is `2`. If this is a number, it's the number of spaces for one indent. If this is `"tab"`, it uses one tab for one indent.
 - `attribute` (`integer`) ... The multiplier of indentation for attributes. Default is `1`.
 - `baseIndent` (`integer`) ... The multiplier of indentation for top-level statements. Default is `1`.
-- `closeBracket` (`integer`) ... The multiplier of indentation for right brackets. Default is `0`.
+- `closeBracket` (`integer | object`) ... The multiplier of indentation for right brackets. Default is `0`.  
+  You can apply all of the following by setting a number value.
+  - `closeBracket.startTag` (`integer`) ... The multiplier of indentation for right brackets of start tags (`<div>`). Default is `0`.
+  - `closeBracket.endTag` (`integer`) ... The multiplier of indentation for right brackets of end tags (`</div>`). Default is `0`.
+  - `closeBracket.selfClosingTag` (`integer`) ... The multiplier of indentation for right brackets of start tags (`<div/>`). Default is `0`.
 - `alignAttributesVertically` (`boolean`) ... Condition for whether attributes should be vertically aligned to the first attribute in multiline case or not. Default is `true`
 - `ignores` (`string[]`) ... The selector to ignore nodes. The AST spec is [here](https://github.com/mysticatea/vue-eslint-parser/blob/master/docs/ast.md). You can use [esquery](https://github.com/estools/esquery#readme) to select nodes. Default is an empty array.
 
diff --git a/lib/rules/html-indent.js b/lib/rules/html-indent.js
index d62f6f3a..9cbea60c 100644
--- a/lib/rules/html-indent.js
+++ b/lib/rules/html-indent.js
@@ -44,7 +44,20 @@ module.exports = {
         properties: {
           attribute: { type: 'integer', minimum: 0 },
           baseIndent: { type: 'integer', minimum: 0 },
-          closeBracket: { type: 'integer', minimum: 0 },
+          closeBracket: {
+            anyOf: [
+              { type: 'integer', minimum: 0 },
+              {
+                type: 'object',
+                properties: {
+                  startTag: { type: 'integer', minimum: 0 },
+                  endTag: { type: 'integer', minimum: 0 },
+                  selfClosingTag: { type: 'integer', minimum: 0 }
+                },
+                additionalProperties: false
+              }
+            ]
+          },
           switchCase: { type: 'integer', minimum: 0 },
           alignAttributesVertically: { type: 'boolean' },
           ignores: {
diff --git a/lib/rules/no-unused-vars.js b/lib/rules/no-unused-vars.js
index 37356c9c..e808ee7f 100644
--- a/lib/rules/no-unused-vars.js
+++ b/lib/rules/no-unused-vars.js
@@ -6,6 +6,58 @@
 
 const utils = require('../utils')
 
+/**
+ * @typedef {import('vue-eslint-parser').AST.Node} Node
+ * @typedef {import('vue-eslint-parser').AST.VElement} VElement
+ * @typedef {import('vue-eslint-parser').AST.Variable} Variable
+ */
+/**
+ * @typedef {Variable['kind']} VariableKind
+ */
+
+// ------------------------------------------------------------------------------
+// Helpers
+// ------------------------------------------------------------------------------
+
+/**
+ * Groups variables by directive kind.
+ * @param {VElement} node The element node
+ * @returns { { [kind in VariableKind]?: Variable[] } } The variables of grouped by directive kind.
+ */
+function groupingVariables(node) {
+  /** @type { { [kind in VariableKind]?: Variable[] } } */
+  const result = {}
+  for (const variable of node.variables) {
+    const vars = result[variable.kind] || (result[variable.kind] = [])
+    vars.push(variable)
+  }
+  return result
+}
+
+/**
+ * Checks if the given variable was defined by destructuring.
+ * @param {Variable} variable the given variable to check
+ * @returns {boolean} `true` if the given variable was defined by destructuring.
+ */
+function isDestructuringVar(variable) {
+  const node = variable.id
+  /** @type {Node} */
+  let parent = node.parent
+  while (parent) {
+    if (
+      parent.type === 'VForExpression' ||
+      parent.type === 'VSlotScopeExpression'
+    ) {
+      return false
+    }
+    if (parent.type === 'Property' || parent.type === 'ArrayPattern') {
+      return true
+    }
+    parent = parent.parent
+  }
+  return false
+}
+
 // ------------------------------------------------------------------------------
 // Rule Definition
 // ------------------------------------------------------------------------------
@@ -41,38 +93,52 @@ module.exports = {
       ignoreRegEx = new RegExp(ignorePattern, 'u')
     }
     return utils.defineTemplateBodyVisitor(context, {
+      /**
+       * @param {VElement} node
+       */
       VElement(node) {
-        const variables = node.variables
-        for (let i = variables.length - 1; i >= 0; i--) {
-          const variable = variables[i]
+        const vars = groupingVariables(node)
+        for (const variables of Object.values(vars)) {
+          let hasAfterUsed = false
 
-          if (variable.references.length) {
-            break
-          }
+          for (let i = variables.length - 1; i >= 0; i--) {
+            const variable = variables[i]
 
-          if (ignoreRegEx != null && ignoreRegEx.test(variable.id.name)) {
-            continue
-          }
-          context.report({
-            node: variable.id,
-            loc: variable.id.loc,
-            message: `'{{name}}' is defined but never used.`,
-            data: variable.id,
-            suggest:
-              ignorePattern === '^_'
-                ? [
-                    {
-                      desc: `Replace the ${variable.id.name} with _${variable.id.name}`,
-                      fix(fixer) {
-                        return fixer.replaceText(
-                          variable.id,
-                          `_${variable.id.name}`
-                        )
+            if (variable.references.length) {
+              hasAfterUsed = true
+              continue
+            }
+
+            if (ignoreRegEx != null && ignoreRegEx.test(variable.id.name)) {
+              continue
+            }
+
+            if (hasAfterUsed && !isDestructuringVar(variable)) {
+              // If a variable after the variable is used, it will be skipped.
+              continue
+            }
+
+            context.report({
+              node: variable.id,
+              loc: variable.id.loc,
+              message: `'{{name}}' is defined but never used.`,
+              data: variable.id,
+              suggest:
+                ignorePattern === '^_'
+                  ? [
+                      {
+                        desc: `Replace the ${variable.id.name} with _${variable.id.name}`,
+                        fix(fixer) {
+                          return fixer.replaceText(
+                            variable.id,
+                            `_${variable.id.name}`
+                          )
+                        }
                       }
-                    }
-                  ]
-                : []
-          })
+                    ]
+                  : []
+            })
+          }
         }
       }
     })
diff --git a/lib/utils/indent-common.js b/lib/utils/indent-common.js
index 1c8c3838..a69d7a5d 100644
--- a/lib/utils/indent-common.js
+++ b/lib/utils/indent-common.js
@@ -21,12 +21,26 @@ const BLOCK_COMMENT_PREFIX = /^\s*\*/
 const ITERATION_OPTS = Object.freeze({ includeComments: true, filter: isNotWhitespace })
 const PREFORMATTED_ELEMENT_NAMES = ['pre', 'textarea']
 
+/**
+ * @typedef {object} IndentOptions
+ * @property { " " | "\t" } IndentOptions.indentChar
+ * @property {number} IndentOptions.indentSize
+ * @property {number} IndentOptions.baseIndent
+ * @property {number} IndentOptions.attribute
+ * @property {object} IndentOptions.closeBracket
+ * @property {number} IndentOptions.closeBracket.startTag
+ * @property {number} IndentOptions.closeBracket.endTag
+ * @property {number} IndentOptions.closeBracket.selfClosingTag
+ * @property {number} IndentOptions.switchCase
+ * @property {boolean} IndentOptions.alignAttributesVertically
+ * @property {string[]} IndentOptions.ignores
+ */
 /**
  * Normalize options.
  * @param {number|"tab"|undefined} type The type of indentation.
  * @param {Object} options Other options.
  * @param {Object} defaultOptions The default value of options.
- * @returns {{indentChar:" "|"\t",indentSize:number,baseIndent:number,attribute:number,closeBracket:number,switchCase:number,alignAttributesVertically:boolean,ignores:string[]}} Normalized options.
+ * @returns {IndentOptions} Normalized options.
  */
 function parseOptions (type, options, defaultOptions) {
   const ret = Object.assign({
@@ -34,7 +48,11 @@ function parseOptions (type, options, defaultOptions) {
     indentSize: 2,
     baseIndent: 0,
     attribute: 1,
-    closeBracket: 0,
+    closeBracket: {
+      startTag: 0,
+      endTag: 0,
+      selfClosingTag: 0
+    },
     switchCase: 0,
     alignAttributesVertically: true,
     ignores: []
@@ -54,7 +72,21 @@ function parseOptions (type, options, defaultOptions) {
     ret.attribute = options.attribute
   }
   if (Number.isSafeInteger(options.closeBracket)) {
-    ret.closeBracket = options.closeBracket
+    const num = options.closeBracket
+    ret.closeBracket = {
+      startTag: num,
+      endTag: num,
+      selfClosingTag: num
+    }
+  } else if (options.closeBracket) {
+    ret.closeBracket = Object.assign(
+      {
+        startTag: 0,
+        endTag: 0,
+        selfClosingTag: 0
+      },
+      options.closeBracket
+    )
   }
   if (Number.isSafeInteger(options.switchCase)) {
     ret.switchCase = options.switchCase
@@ -919,11 +951,12 @@ module.exports.defineVisitor = function create (context, tokenStore, defaultOpti
     },
 
     VEndTag (node) {
-      const openToken = tokenStore.getFirstToken(node)
+      const element = node.parent
+      const startTagOpenToken = tokenStore.getFirstToken(element.startTag)
       const closeToken = tokenStore.getLastToken(node)
 
       if (closeToken.type.endsWith('TagClose')) {
-        setOffset(closeToken, options.closeBracket, openToken)
+        setOffset(closeToken, options.closeBracket.endTag, startTagOpenToken)
       }
     },
 
@@ -996,7 +1029,10 @@ module.exports.defineVisitor = function create (context, tokenStore, defaultOpti
         options.alignAttributesVertically
       )
       if (closeToken != null && closeToken.type.endsWith('TagClose')) {
-        setOffset(closeToken, options.closeBracket, openToken)
+        const offset = closeToken.type !== 'HTMLSelfClosingTagClose'
+          ? options.closeBracket.startTag
+          : options.closeBracket.selfClosingTag
+        setOffset(closeToken, offset, openToken)
       }
     },
 


Test patch:
diff --git a/tests/fixtures/html-indent/close-bracket-01.vue b/tests/fixtures/html-indent/close-bracket-01.vue
new file mode 100644
index 00000000..3307b145
--- /dev/null
+++ b/tests/fixtures/html-indent/close-bracket-01.vue
@@ -0,0 +1,16 @@
+<!--{}-->
+<template>
+  <a
+    href="#"
+  >Link 1</a
+  >
+  <a
+    href="#"
+  >
+    Link 2</a
+  >
+  <div>Content</div
+  >
+  <div
+  />
+</template>
diff --git a/tests/fixtures/html-indent/close-bracket-02.vue b/tests/fixtures/html-indent/close-bracket-02.vue
new file mode 100644
index 00000000..3f171a69
--- /dev/null
+++ b/tests/fixtures/html-indent/close-bracket-02.vue
@@ -0,0 +1,16 @@
+<!--{"options":[2, { "closeBracket": 1 }]}-->
+<template>
+  <a
+    href="#"
+    >Link 1</a
+    >
+  <a
+    href="#"
+    >
+    Link 2</a
+    >
+  <div>Content</div
+    >
+  <div
+    />
+</template>
diff --git a/tests/fixtures/html-indent/close-bracket-03.vue b/tests/fixtures/html-indent/close-bracket-03.vue
new file mode 100644
index 00000000..518423c4
--- /dev/null
+++ b/tests/fixtures/html-indent/close-bracket-03.vue
@@ -0,0 +1,16 @@
+<!--{"options":[2, { "closeBracket": { "startTag": 1 } }]}-->
+<template>
+  <a
+    href="#"
+    >Link 1</a
+  >
+  <a
+    href="#"
+    >
+    Link 2</a
+  >
+  <div>Content</div
+  >
+  <div
+  />
+</template>
diff --git a/tests/fixtures/html-indent/close-bracket-04.vue b/tests/fixtures/html-indent/close-bracket-04.vue
new file mode 100644
index 00000000..f90ffc4d
--- /dev/null
+++ b/tests/fixtures/html-indent/close-bracket-04.vue
@@ -0,0 +1,16 @@
+<!--{"options":[2, { "closeBracket": { "endTag": 1 } }]}-->
+<template>
+  <a
+    href="#"
+  >Link 1</a
+    >
+  <a
+    href="#"
+  >
+    Link 2</a
+    >
+  <div>Content</div
+    >
+  <div
+  />
+</template>
diff --git a/tests/fixtures/html-indent/close-bracket-05.vue b/tests/fixtures/html-indent/close-bracket-05.vue
new file mode 100644
index 00000000..b5d48275
--- /dev/null
+++ b/tests/fixtures/html-indent/close-bracket-05.vue
@@ -0,0 +1,16 @@
+<!--{"options":[2, { "closeBracket": { "selfClosingTag": 1 } }]}-->
+<template>
+  <a
+    href="#"
+  >Link 1</a
+  >
+  <a
+    href="#"
+  >
+    Link 2</a
+  >
+  <div>Content</div
+  >
+  <div
+    />
+</template>
diff --git a/tests/fixtures/html-indent/close-bracket-06.vue b/tests/fixtures/html-indent/close-bracket-06.vue
new file mode 100644
index 00000000..d5bae8ed
--- /dev/null
+++ b/tests/fixtures/html-indent/close-bracket-06.vue
@@ -0,0 +1,16 @@
+<!--{"options":[2, { "closeBracket": {  "startTag": 1, "endTag": 2, "selfClosingTag": 3 } }]}-->
+<template>
+  <a
+    href="#"
+    >Link 1</a
+      >
+  <a
+    href="#"
+    >
+    Link 2</a
+      >
+  <div>Content</div
+      >
+  <div
+        />
+</template>
diff --git a/tests/lib/rules/no-unused-vars.js b/tests/lib/rules/no-unused-vars.js
index 0c1b85e5..81df799b 100644
--- a/tests/lib/rules/no-unused-vars.js
+++ b/tests/lib/rules/no-unused-vars.js
@@ -162,6 +162,58 @@ tester.run('no-unused-vars', rule, {
       code: '<template><div v-for="(a, _i) in foo" ></div></template>',
       options: [{ ignorePattern: '^_' }],
       errors: ["'a' is defined but never used."]
+    },
+    {
+      code:
+        '<template><my-component v-slot="a" >{{d}}</my-component></template>',
+      errors: ["'a' is defined but never used."]
+    },
+    {
+      code:
+        '<template><my-component v-for="i in foo" v-slot="a" >{{a}}</my-component></template>',
+      errors: ["'i' is defined but never used."]
+    },
+    {
+      code:
+        '<template><my-component v-for="i in foo" v-slot="a" >{{i}}</my-component></template>',
+      errors: ["'a' is defined but never used."]
+    },
+    {
+      code:
+        '<template><div v-for="({a, b}, [c, d], e, f) in foo" >{{f}}</div></template>',
+      errors: [
+        "'a' is defined but never used.",
+        "'b' is defined but never used.",
+        "'c' is defined but never used.",
+        "'d' is defined but never used."
+      ]
+    },
+    {
+      code:
+        '<template><div v-for="({a, b}, c, [d], e, f) in foo" >{{f}}</div></template>',
+      errors: [
+        "'a' is defined but never used.",
+        "'b' is defined but never used.",
+        "'d' is defined but never used."
+      ]
+    },
+    {
+      code:
+        '<template><my-component v-slot="{a, b, c, d}" >{{d}}</my-component></template>',
+      errors: [
+        "'a' is defined but never used.",
+        "'b' is defined but never used.",
+        "'c' is defined but never used."
+      ]
+    },
+    {
+      code:
+        '<template><div v-for="({a, b: bar}, c = 1, [d], e, f) in foo" >{{f}}</div></template>',
+      errors: [
+        "'a' is defined but never used.",
+        "'bar' is defined but never used.",
+        "'d' is defined but never used."
+      ]
     }
   ]
 })