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

Issue:
closeBracket affects end tag
<!--
  Before posting the issue, please confirm that the problem you're getting
  is not related with your code editor configuration.
  To make sure it's not, run: yarn eslint src/your-file.vue
-->

**Tell us about your environment**	
* **ESLint version:**	5.10.0
* **eslint-plugin-vue version:**	5.0.0
* **Node version:**	10.10.0

**Please show your full configuration:**
<!-- Paste content of your .eslintrc file -->
```js
module.exports = {
	root: true,
	plugins: ['vue'],
	extends: [
		'plugin:vue/essential',
	],
	rules: {
		'vue/html-indent': [
			'error',
			'tab',
			{
				closeBracket: 1,
			},
		],
	},
}
```

**What did you do?**	

<!-- Please include the actual source code causing the issue. -->
```vue
<template>
	<div>
		<a
			>alksjdklaj dklajsd klaj dklajs kldjaksljdkals jdklas
			jdkljaklsdjkalsjdklajskldjaskl jdklasjdklajslkdja</a
		>
	</div>
</template>
```

`yarn eslint --fix test.vue && cat test.vue`

The example deliberately has no whitespace between `<a>`/`</a>` and inner text (this is how it happens in real life - links don't typically have side spaces).

**What did you expect to happen?**
```vue
<template>
	<div>
		<a
			>alksjdklaj dklajsd klaj dklajs kldjaksljdkals jdklas
			jdkljaklsdjkalsjdklajskldjaskl jdklasjdklajslkdja</a
		>
	</div>
</template>
```

**What actually happened?**
<!-- Please include the actual, raw output from ESLint. -->
```
yarn run v1.10.1
$ /Users/semenov/xxx/node_modules/.bin/eslint --fix test.vue
✨  Done in 0.75s.
```

```vue
<template>
	<div>
		<a
			>alksjdklaj dklajsd klaj dklajs kldjaksljdkals jdklas
			jdkljaklsdjkalsjdklajskldjaskl jdklasjdklajslkdja</a
			>
	</div>
</template>
```

---

I would expect `closeBracket` not to affect the end tag's indentation as it doesn't make sense (the whole `<a>...</a>` element becomes unbalanced).

Alternatively, there should be an option to disable `closeBracket` for end tags separately.

Pull request:
Improved closing bracket indentation for `vue/html-indent` rule.
- Added `closeBracket.startTag`, `closeBracket.endTag` and `closeBracket.selfClosingTag` options to `vue/html-indent` rule.
  So that the closeBracket offset value can be set for each tag type.
- Changed `vue/html-indent` rule to calculate the base point of the indent offset of the closing bracket of the end tag by the start tag.

Close #728
Close #756 (PR)

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 61aee3ff..37356c9c 100644
--- a/lib/rules/no-unused-vars.js
+++ b/lib/rules/no-unused-vars.js
@@ -35,31 +35,31 @@ module.exports = {
 
   create(context) {
     const option = context.options[0] || {}
-    const pattern = option.ignorePattern
-    let regExp = null
-    if (pattern) {
-      regExp = new RegExp(pattern, 'u')
+    const ignorePattern = option.ignorePattern
+    let ignoreRegEx = null
+    if (ignorePattern) {
+      ignoreRegEx = new RegExp(ignorePattern, 'u')
     }
     return utils.defineTemplateBodyVisitor(context, {
       VElement(node) {
         const variables = node.variables
-
-        for (
-          let i = variables.length - 1;
-          i >= 0 &&
-          !variables[i].references.length &&
-          // eslint-disable-next-line no-unmodified-loop-condition
-          (regExp === null || !regExp.test(variables[i].id.name));
-          i--
-        ) {
+        for (let i = variables.length - 1; i >= 0; i--) {
           const variable = variables[i]
+
+          if (variable.references.length) {
+            break
+          }
+
+          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:
-              pattern === '^_'
+              ignorePattern === '^_'
                 ? [
                     {
                       desc: `Replace the ${variable.id.name} with _${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 bf8a9377..0c1b85e5 100644
--- a/tests/lib/rules/no-unused-vars.js
+++ b/tests/lib/rules/no-unused-vars.js
@@ -157,6 +157,11 @@ tester.run('no-unused-vars', rule, {
     {
       code: '<template><div v-for="_i in foo" ></div></template>',
       errors: ["'_i' is defined but never used."]
+    },
+    {
+      code: '<template><div v-for="(a, _i) in foo" ></div></template>',
+      options: [{ ignorePattern: '^_' }],
+      errors: ["'a' is defined but never used."]
     }
   ]
 })