Repository: vuejs/eslint-plugin-vue
Pull request #2042 (2022-11-21 16:14:51)

Improve indent rule to support more ts syntax
This PR adds support for more typescript syntax to indent rules.

```ts
// Type parameters given to call expression, and new expressions
foo<T>(arg)
// ^^^
new Foo<T>(arg)
//     ^^^

// Instantiation expression
const ErrorMap = Map<string, Error>
//               ^^^^^^^^^^^^^^^^^^

// Satisfies operator
var a = {} satisfies Foo
//         ^^^^^^^^^

// Accessor property
class Foo {
  accessor foo;
//^^^^^^^^^^^^
}
```


Patch:
diff --git a/docs/rules/array-element-newline.md b/docs/rules/array-element-newline.md
new file mode 100644
index 00000000..024dcafe
--- /dev/null
+++ b/docs/rules/array-element-newline.md
@@ -0,0 +1,31 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/array-element-newline
+description: Enforce line breaks after each array element in `<template>`
+---
+# vue/array-element-newline
+
+> Enforce line breaks after each array element in `<template>`
+
+- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
+- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
+
+This rule is the same rule as core [array-element-newline] rule but it applies to the expressions in `<template>`.
+
+## :books: Further Reading
+
+- [array-bracket-spacing]
+
+[array-bracket-spacing]: https://eslint.org/docs/rules/array-bracket-spacing
+
+- [array-bracket-newline]
+
+[array-bracket-newline]: https://eslint.org/docs/rules/array-bracket-newline
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/array-element-newline.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/array-element-newline.js)
+
+<sup>Taken with ❤️ [from ESLint core](https://eslint.org/docs/rules/array-element-newline)</sup>
diff --git a/docs/rules/index.md b/docs/rules/index.md
index 224b4031..2e21edc5 100644
--- a/docs/rules/index.md
+++ b/docs/rules/index.md
@@ -279,6 +279,7 @@ The following rules extend the rules provided by ESLint itself and apply them to
 |:--------|:------------|:--:|:--:|
 | [vue/array-bracket-newline](./array-bracket-newline.md) | Enforce linebreaks after opening and before closing array brackets in `<template>` | :wrench: | :lipstick: |
 | [vue/array-bracket-spacing](./array-bracket-spacing.md) | Enforce consistent spacing inside array brackets in `<template>` | :wrench: | :lipstick: |
+| [vue/array-element-newline](./array-element-newline.md) | Enforce line breaks after each array element in `<template>` | :wrench: | :lipstick: |
 | [vue/arrow-spacing](./arrow-spacing.md) | Enforce consistent spacing before and after the arrow in arrow functions in `<template>` | :wrench: | :lipstick: |
 | [vue/block-spacing](./block-spacing.md) | Disallow or enforce spaces inside of blocks after opening block and before closing block in `<template>` | :wrench: | :lipstick: |
 | [vue/brace-style](./brace-style.md) | Enforce consistent brace style for blocks in `<template>` | :wrench: | :lipstick: |
diff --git a/lib/configs/no-layout-rules.js b/lib/configs/no-layout-rules.js
index 58e62b60..b1486f15 100644
--- a/lib/configs/no-layout-rules.js
+++ b/lib/configs/no-layout-rules.js
@@ -7,6 +7,7 @@ module.exports = {
   rules: {
     'vue/array-bracket-newline': 'off',
     'vue/array-bracket-spacing': 'off',
+    'vue/array-element-newline': 'off',
     'vue/arrow-spacing': 'off',
     'vue/block-spacing': 'off',
     'vue/block-tag-newline': 'off',
diff --git a/lib/index.js b/lib/index.js
index 2d14cd40..f5da9a70 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -9,6 +9,7 @@ module.exports = {
   rules: {
     'array-bracket-newline': require('./rules/array-bracket-newline'),
     'array-bracket-spacing': require('./rules/array-bracket-spacing'),
+    'array-element-newline': require('./rules/array-element-newline'),
     'arrow-spacing': require('./rules/arrow-spacing'),
     'attribute-hyphenation': require('./rules/attribute-hyphenation'),
     'attributes-order': require('./rules/attributes-order'),
diff --git a/lib/rules/array-element-newline.js b/lib/rules/array-element-newline.js
new file mode 100644
index 00000000..251d72de
--- /dev/null
+++ b/lib/rules/array-element-newline.js
@@ -0,0 +1,11 @@
+/**
+ * @author alshyra
+ */
+'use strict'
+
+const { wrapCoreRule } = require('../utils')
+
+// eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories
+module.exports = wrapCoreRule('array-element-newline', {
+  skipDynamicArguments: true
+})
diff --git a/lib/utils/indent-common.js b/lib/utils/indent-common.js
index 79858b49..4db6dea7 100644
--- a/lib/utils/indent-common.js
+++ b/lib/utils/indent-common.js
@@ -1171,11 +1171,18 @@ module.exports.defineVisitor = function create(
       const firstToken = tokenStore.getFirstToken(node)
       const rightToken = tokenStore.getLastToken(node)
       const leftToken = /** @type {Token} */ (
-        tokenStore.getTokenAfter(node.callee, isOpeningParenToken)
+        tokenStore.getTokenAfter(
+          node.typeParameters || node.callee,
+          isOpeningParenToken
+        )
       )
 
+      if (node.typeParameters) {
+        setOffset(tokenStore.getFirstToken(node.typeParameters), 1, firstToken)
+      }
+
       for (const optionalToken of tokenStore.getTokensBetween(
-        tokenStore.getLastToken(node.callee),
+        tokenStore.getLastToken(node.typeParameters || node.callee),
         leftToken,
         isOptionalToken
       )) {
@@ -1692,12 +1699,16 @@ module.exports.defineVisitor = function create(
       const rightToken = tokenStore.getLastToken(node)
       const leftToken = isClosingParenToken(rightToken)
         ? tokenStore.getFirstTokenBetween(
-            node.callee,
+            node.typeParameters || node.callee,
             rightToken,
             isOpeningParenToken
           )
         : null
 
+      if (node.typeParameters) {
+        setOffset(tokenStore.getFirstToken(node.typeParameters), 1, calleeToken)
+      }
+
       setOffset(calleeToken, 1, newToken)
       if (leftToken != null) {
         setOffset(leftToken, 1, calleeToken)
diff --git a/lib/utils/indent-ts.js b/lib/utils/indent-ts.js
index f2f5b69c..4c046f49 100644
--- a/lib/utils/indent-ts.js
+++ b/lib/utils/indent-ts.js
@@ -29,6 +29,7 @@ const { isTypeNode } = require('./ts-ast-utils')
  * @typedef {import('@typescript-eslint/types').TSESTree.TSImportEqualsDeclaration} TSImportEqualsDeclaration
  * @typedef {import('@typescript-eslint/types').TSESTree.TSAbstractMethodDefinition} TSAbstractMethodDefinition
  * @typedef {import('@typescript-eslint/types').TSESTree.TSAbstractPropertyDefinition} TSAbstractPropertyDefinition
+ * @typedef {import('@typescript-eslint/types').TSESTree.TSAbstractAccessorProperty} TSAbstractAccessorProperty
  * @typedef {import('@typescript-eslint/types').TSESTree.TSEnumMember} TSEnumMember
  * @typedef {import('@typescript-eslint/types').TSESTree.TSPropertySignature} TSPropertySignature
  * @typedef {import('@typescript-eslint/types').TSESTree.TSIndexSignature} TSIndexSignature
@@ -50,6 +51,10 @@ const { isTypeNode } = require('./ts-ast-utils')
  * @typedef {import('@typescript-eslint/types').TSESTree.TSInferType} TSInferType
  * @typedef {import('@typescript-eslint/types').TSESTree.TSOptionalType} TSOptionalType
  * @typedef {import('@typescript-eslint/types').TSESTree.TSNonNullExpression} TSNonNullExpression
+ * @typedef {import('@typescript-eslint/types').TSESTree.TSAsExpression} TSAsExpression
+ * @typedef {import('@typescript-eslint/types').TSESTree.TSSatisfiesExpression} TSSatisfiesExpression
+ * @typedef {import('@typescript-eslint/types').TSESTree.TSTypeReference} TSTypeReference
+ * @typedef {import('@typescript-eslint/types').TSESTree.TSInstantiationExpression} TSInstantiationExpression
  * @typedef {import('@typescript-eslint/types').TSESTree.JSXChild} JSXChild
  * @typedef {import('@typescript-eslint/types').TSESTree.TypeNode} TypeNode
  *
@@ -206,15 +211,16 @@ function defineVisitor({
      *   | TSImportEqualsDeclaration
      *   | TSAbstractMethodDefinition
      *   | TSAbstractPropertyDefinition
-     *   | TSAbstractClassProperty
+     *   | TSAbstractAccessorProperty
      *   | TSEnumMember
-     *   | ClassProperty
      *   | TSPropertySignature
      *   | TSIndexSignature
-     *   | TSMethodSignature} node
+     *   | TSMethodSignature
+     *   | ClassProperty
+     *   | TSAbstractClassProperty} node
      */
     ['TSTypeAliasDeclaration, TSCallSignatureDeclaration, TSConstructSignatureDeclaration, TSImportEqualsDeclaration,' +
-      'TSAbstractMethodDefinition, TSAbstractPropertyDefinition, TSEnumMember,' +
+      'TSAbstractMethodDefinition, TSAbstractPropertyDefinition, TSAbstractAccessorProperty, TSEnumMember,' +
       'TSPropertySignature, TSIndexSignature, TSMethodSignature,' +
       // Deprecated in @typescript-eslint/parser v5
       'ClassProperty, TSAbstractClassProperty'](node) {
@@ -279,40 +285,57 @@ function defineVisitor({
       }
     },
     /**
-     * Process as expression
+     * Process as expression or satisfies expression
      *
      * e.g.
      * ```
      * var foo = bar as boolean
      * //        ^^^^^^^^^^^^^^
      * ```
+     *
+     * e.g.
+     * ```
+     * var foo = bar satisfies Bar
+     * //        ^^^^^^^^^^^^^^^^^
+     * ```
+     *
+     * @param {TSAsExpression | TSSatisfiesExpression} node
      */
-    TSAsExpression(node) {
+    'TSAsExpression, TSSatisfiesExpression'(node) {
       const expressionTokens = getFirstAndLastTokens(node.expression)
-      const asToken = tokenStore.getTokenAfter(expressionTokens.lastToken)
+      const asOrSatisfiesToken = tokenStore.getTokenAfter(
+        expressionTokens.lastToken
+      )
       setOffset(
-        [asToken, getFirstAndLastTokens(node.typeAnnotation).firstToken],
+        [
+          asOrSatisfiesToken,
+          getFirstAndLastTokens(node.typeAnnotation).firstToken
+        ],
         1,
         expressionTokens.firstToken
       )
     },
     /**
-     * Process type reference
+     * Process type reference and instantiation expression
      *
      * e.g.
      * ```
      * const foo: Type<P>
      * //         ^^^^^^^
      * ```
+     *
+     * e.g.
+     * ```
+     * const ErrorMap = Map<string, Error>;
+     * //               ^^^^^^^^^^^^^^^^^^
+     * ```
+     *
+     * @param {TSTypeReference | TSInstantiationExpression} node
      */
-    TSTypeReference(node) {
+    'TSTypeReference, TSInstantiationExpression'(node) {
       if (node.typeParameters) {
-        const typeNameTokens = getFirstAndLastTokens(node.typeName)
-        setOffset(
-          tokenStore.getFirstToken(node.typeParameters),
-          1,
-          typeNameTokens.firstToken
-        )
+        const firstToken = tokenStore.getFirstToken(node)
+        setOffset(tokenStore.getFirstToken(node.typeParameters), 1, firstToken)
       }
     },
     /**
@@ -1053,10 +1076,10 @@ function defineVisitor({
      * //         ^^^^^^^
      * ```
      *
-     * @param {TSAbstractMethodDefinition | TSAbstractPropertyDefinition | TSEnumMember | TSAbstractClassProperty | ClassProperty} node
+     * @param {TSAbstractMethodDefinition | TSAbstractPropertyDefinition | TSAbstractAccessorProperty | TSEnumMember | TSAbstractClassProperty | ClassProperty} node
      *
      */
-    ['TSAbstractMethodDefinition, TSAbstractPropertyDefinition, TSEnumMember,' +
+    ['TSAbstractMethodDefinition, TSAbstractPropertyDefinition, TSAbstractAccessorProperty, TSEnumMember,' +
       // Deprecated in @typescript-eslint/parser v5
       'ClassProperty, TSAbstractClassProperty'](node) {
       const { keyNode, valueNode } =
@@ -1302,6 +1325,42 @@ function defineVisitor({
         setOffset(atToken, 0, tokenStore.getFirstToken(decorators[0]))
       }
     },
+    AccessorProperty(node) {
+      const keyNode = node.key
+      const valueNode = node.value
+      const firstToken = tokenStore.getFirstToken(node)
+      const keyTokens = getFirstAndLastTokens(keyNode)
+      const prefixTokens = tokenStore.getTokensBetween(
+        firstToken,
+        keyTokens.firstToken
+      )
+      if (node.computed) {
+        prefixTokens.pop() // pop opening bracket character (`[`)
+      }
+      setOffset(prefixTokens, 0, firstToken)
+      let lastKeyToken
+      if (node.computed) {
+        const leftBracketToken = tokenStore.getTokenBefore(keyTokens.firstToken)
+        const rightBracketToken = (lastKeyToken = tokenStore.getTokenAfter(
+          keyTokens.lastToken
+        ))
+        setOffset(leftBracketToken, 0, firstToken)
+        processNodeList([keyNode], leftBracketToken, rightBracketToken, 1)
+      } else {
+        setOffset(keyTokens.firstToken, 0, firstToken)
+        lastKeyToken = keyTokens.lastToken
+      }
+
+      if (valueNode != null) {
+        const initToken = tokenStore.getFirstToken(valueNode)
+        setOffset(
+          [...tokenStore.getTokensBetween(lastKeyToken, initToken), initToken],
+          1,
+          lastKeyToken
+        )
+      }
+      processSemicolons(node)
+    },
     ImportAttribute(node) {
       const firstToken = tokenStore.getFirstToken(node)
       const keyTokens = getFirstAndLastTokens(node.key)
diff --git a/package.json b/package.json
index 3f2fb728..da3d9633 100644
--- a/package.json
+++ b/package.json
@@ -71,7 +71,7 @@
     "@types/node": "^13.13.5",
     "@types/semver": "^7.3.9",
     "@types/xml-name-validator": "^4.0.0",
-    "@typescript-eslint/parser": "^5.23.0",
+    "@typescript-eslint/parser": "^5.45.0",
     "assert": "^2.0.0",
     "env-cmd": "^10.1.0",
     "esbuild": "^0.15.15",
@@ -90,7 +90,7 @@
     "mocha": "^10.0.0",
     "nyc": "^15.1.0",
     "prettier": "^2.6.2",
-    "typescript": "^4.6.4",
+    "typescript": "^4.9.3",
     "vitepress": "^1.0.0-alpha.29"
   }
 }
diff --git a/typings/eslint-plugin-vue/util-types/ast/es-ast.ts b/typings/eslint-plugin-vue/util-types/ast/es-ast.ts
index b3d5b6e2..80094341 100644
--- a/typings/eslint-plugin-vue/util-types/ast/es-ast.ts
+++ b/typings/eslint-plugin-vue/util-types/ast/es-ast.ts
@@ -525,6 +525,7 @@ export interface NewExpression extends HasParentNode {
   type: 'NewExpression'
   callee: Expression
   arguments: (Expression | SpreadElement)[]
+  typeParameters?: TSTypeParameterInstantiation
 }
 interface BaseMemberExpression extends HasParentNode {
   type: 'MemberExpression'


Test patch:
diff --git a/tests/fixtures/script-indent/ts-abstract-accessor-property-01.vue b/tests/fixtures/script-indent/ts-abstract-accessor-property-01.vue
new file mode 100644
index 00000000..563b1030
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-abstract-accessor-property-01.vue
@@ -0,0 +1,12 @@
+<!--{"parserOptions": {"parser":"@typescript-eslint/parser"}, "requirements": { "@typescript-eslint/parser": ">=5.44.1-alpha.15"}}-->
+<script lang="ts">
+abstract class Foo {
+  abstract accessor
+  foo:
+    number
+  // parser v5 does not parse value.
+  //    =
+  //    1
+  ;
+}
+</script>
diff --git a/tests/fixtures/script-indent/ts-accessor-property-01.vue b/tests/fixtures/script-indent/ts-accessor-property-01.vue
new file mode 100644
index 00000000..c7fa3c38
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-accessor-property-01.vue
@@ -0,0 +1,8 @@
+<!--{"parserOptions": {"parser":"@typescript-eslint/parser"}, "requirements": { "@typescript-eslint/parser": ">=5.44.1-alpha.15"}}-->
+<script lang="ts">
+class Foo {
+  accessor
+  foo
+  ;
+}
+</script>
diff --git a/tests/fixtures/script-indent/ts-accessor-property-02.vue b/tests/fixtures/script-indent/ts-accessor-property-02.vue
new file mode 100644
index 00000000..a0936919
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-accessor-property-02.vue
@@ -0,0 +1,10 @@
+<!--{"parserOptions": {"parser":"@typescript-eslint/parser"}, "requirements": { "@typescript-eslint/parser": ">=5.44.1-alpha.15"}}-->
+<script lang="ts">
+class Foo {
+  accessor
+  foo
+    =
+    2
+  ;
+}
+</script>
diff --git a/tests/fixtures/script-indent/ts-accessor-property-03.vue b/tests/fixtures/script-indent/ts-accessor-property-03.vue
new file mode 100644
index 00000000..856502bb
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-accessor-property-03.vue
@@ -0,0 +1,9 @@
+<!--{"parserOptions": {"parser":"@typescript-eslint/parser"}, "requirements": { "@typescript-eslint/parser": ">=5.44.1-alpha.15"}}-->
+<script lang="ts">
+class Foo {
+  declare accessor
+  foo:
+    number
+  ;
+}
+</script>
diff --git a/tests/fixtures/script-indent/ts-accessor-property-04.vue b/tests/fixtures/script-indent/ts-accessor-property-04.vue
new file mode 100644
index 00000000..f0ba8795
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-accessor-property-04.vue
@@ -0,0 +1,10 @@
+<!--{"parserOptions": {"parser":"@typescript-eslint/parser"}, "requirements": { "@typescript-eslint/parser": ">=5.44.1-alpha.15"}}-->
+<script lang="ts">
+class Foo {
+  override accessor
+  foo
+    =
+    2
+  ;
+}
+</script>
diff --git a/tests/fixtures/script-indent/ts-accessor-property-05.vue b/tests/fixtures/script-indent/ts-accessor-property-05.vue
new file mode 100644
index 00000000..4328991d
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-accessor-property-05.vue
@@ -0,0 +1,12 @@
+<!--{"parserOptions": {"parser":"@typescript-eslint/parser"}, "requirements": { "@typescript-eslint/parser": ">=5.44.1-alpha.15"}}-->
+<script lang="ts">
+class Foo {
+  accessor
+  [
+    1
+  ]
+    =
+    2
+  ;
+}
+</script>
diff --git a/tests/fixtures/script-indent/ts-call-expression-01.vue b/tests/fixtures/script-indent/ts-call-expression-01.vue
new file mode 100644
index 00000000..20600bc1
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-call-expression-01.vue
@@ -0,0 +1,12 @@
+<!--{"parserOptions": {"parser":"@typescript-eslint/parser"}}-->
+<script lang="ts">
+foo
+  <
+    T
+    ,
+    U
+  >
+  (
+    arg
+  )
+</script>
diff --git a/tests/fixtures/script-indent/ts-instantiation-expression-01.vue b/tests/fixtures/script-indent/ts-instantiation-expression-01.vue
new file mode 100644
index 00000000..b5860254
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-instantiation-expression-01.vue
@@ -0,0 +1,8 @@
+<!--{"parserOptions": {"parser":"@typescript-eslint/parser"}, "requirements": { "@typescript-eslint/parser": ">=5.44.0"}}-->
+<script lang="ts">
+const ErrorMap = Map
+  <
+    string,
+    Error
+  >;
+</script>
diff --git a/tests/fixtures/script-indent/ts-new-expression-01.vue b/tests/fixtures/script-indent/ts-new-expression-01.vue
new file mode 100644
index 00000000..8559f778
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-new-expression-01.vue
@@ -0,0 +1,13 @@
+<!--{"parserOptions": {"parser":"@typescript-eslint/parser"}}-->
+<script lang="ts">
+const foo = new
+  Foo
+    <
+      T
+      ,
+      U
+    >
+    (
+      arg
+    )
+</script>
diff --git a/tests/fixtures/script-indent/ts-satisfies-operators-01.vue b/tests/fixtures/script-indent/ts-satisfies-operators-01.vue
new file mode 100644
index 00000000..fbb78af6
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-satisfies-operators-01.vue
@@ -0,0 +1,8 @@
+<!--{"parserOptions": {"parser":"@typescript-eslint/parser"}, "requirements": { "@typescript-eslint/parser": ">=5.44.0" } }-->
+<script lang="ts">
+var a =
+  {} satisfies
+    Foo
+var b =
+  {} satisfies Bar
+</script>
diff --git a/tests/lib/rules/array-element-newline.js b/tests/lib/rules/array-element-newline.js
new file mode 100644
index 00000000..54e60f83
--- /dev/null
+++ b/tests/lib/rules/array-element-newline.js
@@ -0,0 +1,270 @@
+/**
+ * @author alshyra
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/array-element-newline')
+
+const tester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: {
+    ecmaVersion: 2020,
+    sourceType: 'module'
+  }
+})
+
+tester.run('array-element-newline', rule, {
+  valid: [
+    '<template><div :attr="[]" /></template>',
+    '<template><div :attr="[a]" /></template>',
+    `
+      <template>
+        <div :attr="[a,
+                     b,
+                     c]" />
+      </template>`,
+    `<template>
+      <div :attr="[a,
+                   b,
+                   c
+                  ]" />
+    </template>`,
+    '<template><div :[attr]="a" /></template>',
+    '<template><div :[[attr]]="a" /></template>',
+    `<template>
+      <div :attr="[
+                   a,
+                   b,
+                   c
+                  ]" />
+    </template>`,
+    `
+      <template>
+        <div :attr="[a
+                     b]" />
+      </template>`,
+    {
+      code: `
+        <template>
+          <div :attr="[a,
+                       b,
+                       c]" />
+        </template>`,
+      options: ['always']
+    },
+    {
+      code: '<template><div :attr="[a]" /></template>',
+      options: ['never']
+    },
+    {
+      code: '<template><div :attr="[a,b,c]" /></template>',
+      options: ['never']
+    },
+    {
+      code: '<template><div :attr="[a, b, c]" /></template>',
+      options: [{ multiline: true }]
+    },
+    {
+      code: `
+        <template>
+          <div :attr="[a, 
+                      {
+                        b:c
+                      }]" />
+        </template>`,
+      options: [{ multiline: true }]
+    },
+    {
+      code: '<template><div :attr="[a, b, c]" /></template>',
+      options: ['consistent']
+    },
+    {
+      code: `
+        <template>
+          <div :attr="[a,
+                       b,
+                       c
+                      ]" />
+        </template>`,
+      options: ['consistent']
+    },
+    {
+      code: '<template><div :attr="[a,b]" /></template>',
+      options: [{ minItems: 3 }]
+    }
+  ],
+  invalid: [
+    {
+      code: `
+        <template>
+          <div :attr="[a, b]" />
+        </template>`,
+      output: `
+        <template>
+          <div :attr="[a,
+b]" />
+        </template>`,
+      errors: [
+        {
+          message: 'There should be a linebreak after this element.',
+          line: 3,
+          column: 26,
+          endLine: 3,
+          endColumn: 27
+        }
+      ]
+    },
+    {
+      code: `
+        <template>
+          <div :attr="[a,b,c]" />
+        </template>`,
+      output: `
+        <template>
+          <div :attr="[a,
+b,
+c]" />
+        </template>`,
+      options: ['always'],
+      errors: [
+        {
+          message: 'There should be a linebreak after this element.',
+          line: 3,
+          column: 26,
+          endLine: 3,
+          endColumn: 26
+        },
+        {
+          message: 'There should be a linebreak after this element.',
+          line: 3,
+          column: 28,
+          endLine: 3,
+          endColumn: 28
+        }
+      ]
+    },
+    {
+      code: `
+        <template>
+          <div :attr="[a,
+                      b,c]" />
+        </template>`,
+      output: `
+        <template>
+          <div :attr="[a,
+                      b,
+c]" />
+        </template>`,
+      options: ['always'],
+      errors: [
+        {
+          message: 'There should be a linebreak after this element.',
+          line: 4,
+          column: 25,
+          endLine: 4,
+          endColumn: 25
+        }
+      ]
+    },
+    {
+      code: `
+        <template>
+          <div :attr="[a,
+                       b,c]" />
+        </template>`,
+      output: `
+        <template>
+          <div :attr="[a,
+                       b,
+c]" />
+        </template>`,
+      options: ['consistent'],
+      errors: [
+        {
+          message: 'There should be a linebreak after this element.',
+          line: 4,
+          column: 26,
+          endLine: 4,
+          endColumn: 26
+        }
+      ]
+    },
+    {
+      code: `
+        <template>
+          <div :attr="[a,
+                       b, c]" />
+        </template>`,
+      output: `
+        <template>
+          <div :attr="[a, b, c]" />
+        </template>`,
+      options: [{ multiline: true }],
+      errors: [
+        {
+          message: 'There should be no linebreak here.',
+          line: 3,
+          column: 26,
+          endLine: 4,
+          endColumn: 24
+        }
+      ]
+    },
+    {
+      code: `
+        <template>
+          <div :attr="[a, {
+                            b:c
+                          }]" />
+        </template>`,
+      output: `
+        <template>
+          <div :attr="[a,
+{
+                            b:c
+                          }]" />
+        </template>`,
+      options: [{ multiline: true }],
+      errors: [
+        {
+          message: 'There should be a linebreak after this element.',
+          line: 3,
+          column: 26,
+          endLine: 3,
+          endColumn: 27
+        }
+      ]
+    },
+    {
+      code: `
+        <template>
+          <div :attr="[a,b,c]" />
+        </template>`,
+      output: `
+        <template>
+          <div :attr="[a,
+b,
+c]" />
+        </template>`,
+      options: [{ minItems: 2 }],
+      errors: [
+        {
+          message: 'There should be a linebreak after this element.',
+          line: 3,
+          column: 26,
+          endLine: 3,
+          endColumn: 26
+        },
+        {
+          message: 'There should be a linebreak after this element.',
+          line: 3,
+          column: 28,
+          endLine: 3,
+          endColumn: 28
+        }
+      ]
+    }
+  ]
+})