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

Issue:
Rule proposal: vue/no-inline-styles
<!--
  Before proposing new rule, please make sure it hasn't been posted already.
  You can see all open propositions here:
  https://github.com/vuejs/eslint-plugin-vue/issues?q=is%3Aopen+is%3Aissue+label%3A%22new+rule+proposition%22
-->

**Please describe what the rule should do:**
<!-- A clear and concise description of what the rule should do. -->
This rule detect inline styles in components.
It's (subjectively) a good practice to separate styles from the view layout, when possible. This rule detects static inline style objects when they contain literal values. `v-bind:style` and `:style` are acceptable because it's sometimes necessary to set styles based on data, computed property or methods result.

**What category should the rule belong to?**
<!-- (place an "X" next to just one item) -->
- [x] Enforces code style
- [ ] Warns about a potential error
- [ ] Suggests an alternate way of doing something
- [ ] Other (please specify:)

**Provide 2-3 code examples that this rule should warn about:**

The following pattern is considered a warning:
```vue
<span style="background-color: #FFF">Hello {{ name }}</span>;
```

The following patterns are not considered a warning::
```vue
<span :style="background-color: color }">Hello {{ name }}</span>;
<span v-bind:style="background-color: color }">Hello {{ name }}</span>;
```

**Additional context**
The rule enforce team members to use SFC `<style scope>` instead of inline style attributes.


Pull request:
⭐️New: Add vue/no-static-inline-styles rule
This PR adds the `vue/no-static-inline-styles` rule.
`vue/no-static-inline-styles` rule reports the static inline style given to the element.

We recommend moving the reported inline styles to the `<style>` element.

---

Close #826

Patch:
diff --git a/docs/rules/README.md b/docs/rules/README.md
index cbef0077..3eee0cc6 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -159,6 +159,7 @@ For example:
 | [vue/no-empty-pattern](./no-empty-pattern.md) | disallow empty destructuring patterns |  |
 | [vue/no-reserved-component-names](./no-reserved-component-names.md) | disallow the use of reserved names in component definitions |  |
 | [vue/no-restricted-syntax](./no-restricted-syntax.md) | disallow specified syntax |  |
+| [vue/no-static-inline-styles](./no-static-inline-styles.md) | disallow static inline `style` attributes |  |
 | [vue/no-unsupported-features](./no-unsupported-features.md) | disallow unsupported Vue.js syntax on the specified version | :wrench: |
 | [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 |  |
diff --git a/docs/rules/no-static-inline-styles.md b/docs/rules/no-static-inline-styles.md
new file mode 100644
index 00000000..88ab2883
--- /dev/null
+++ b/docs/rules/no-static-inline-styles.md
@@ -0,0 +1,69 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/no-static-inline-styles
+description: disallow static inline `style` attributes
+---
+# vue/no-static-inline-styles
+> disallow static inline `style` attributes
+
+## :book: Rule Details
+
+This rule reports static inline `style` bindings and `style` attributes.
+The styles reported in this rule mean that we recommend separating them into `<style>` tag.
+
+<eslint-code-block :rules="{'vue/no-static-inline-styles': ['error']}">
+
+```vue
+<template>
+  <!-- ✓ GOOD -->
+  <div :style="styleObject"></div>
+  <div :style="{ backgroundImage: 'url('+img+')' }"></div>
+
+  <!-- ✗ BAD -->
+  <div style="color: pink;"></div>
+  <div :style="{ color: 'pink' }"></div>
+  <div :style="[ { color: 'pink' }, { 'font-size': '85%' } ]"></div>
+  <div :style="{ backgroundImage, color: 'pink' }"></div>
+</template>
+```
+
+</eslint-code-block>
+
+## :wrench: Options
+
+```json
+{
+  "vue/no-static-inline-styles": ["error", {
+    "allowBinding": false
+  }]
+}
+```
+
+- allowBinding ... if `true`, allow binding static inline `style`. default `false`.
+
+### `"allowBinding": true`
+
+<eslint-code-block :rules="{'vue/no-static-inline-styles': ['error', {'allowBinding': true}]}">
+
+```vue
+<template>
+  <!-- ✓ GOOD -->
+  <div :style="{ transform: 'scale(0.5)' }"></div>
+  <div :style="[ { transform: 'scale(0.5)' }, { 'user-select': 'none' } ]"></div>
+
+  <!-- ✗ BAD -->
+  <div style="transform: scale(0.5);"></div>
+</template>
+```
+
+</eslint-code-block>
+
+## :books: Further reading
+
+- [Guide - Binding Inline Styles](https://vuejs.org/v2/guide/class-and-style.html#Binding-Inline-Styles)
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-static-inline-styles.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-static-inline-styles.js)
diff --git a/lib/index.js b/lib/index.js
index 5fa52ddd..d815da81 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -51,6 +51,7 @@ module.exports = {
     'no-shared-component-data': require('./rules/no-shared-component-data'),
     'no-side-effects-in-computed-properties': require('./rules/no-side-effects-in-computed-properties'),
     'no-spaces-around-equal-signs-in-attribute': require('./rules/no-spaces-around-equal-signs-in-attribute'),
+    'no-static-inline-styles': require('./rules/no-static-inline-styles'),
     'no-template-key': require('./rules/no-template-key'),
     'no-template-shadow': require('./rules/no-template-shadow'),
     'no-textarea-mustache': require('./rules/no-textarea-mustache'),
diff --git a/lib/rules/no-static-inline-styles.js b/lib/rules/no-static-inline-styles.js
new file mode 100644
index 00000000..6c064d27
--- /dev/null
+++ b/lib/rules/no-static-inline-styles.js
@@ -0,0 +1,138 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const utils = require('../utils')
+module.exports = {
+  meta: {
+    type: 'suggestion',
+    docs: {
+      description: 'disallow static inline `style` attributes',
+      category: undefined,
+      url: 'https://eslint.vuejs.org/rules/no-static-inline-styles.html'
+    },
+    fixable: null,
+    schema: [
+      {
+        type: 'object',
+        properties: {
+          allowBinding: {
+            type: 'boolean'
+          }
+        },
+        additionalProperties: false
+      }
+    ],
+    messages: {
+      forbiddenStaticInlineStyle: 'Static inline `style` are forbidden.',
+      forbiddenStyleAttr: '`style` attributes are forbidden.'
+    }
+  },
+  create (context) {
+    /**
+     * Checks whether if the given property node is a static value.
+     * @param {AssignmentProperty} prop property node to check
+     * @returns {boolean} `true` if the given property node is a static value.
+     */
+    function isStaticValue (prop) {
+      return (
+        !prop.computed &&
+        prop.value.type === 'Literal' &&
+        (prop.key.type === 'Identifier' || prop.key.type === 'Literal')
+      )
+    }
+
+    /**
+     * Gets the static properties of a given expression node.
+     * - If `SpreadElement` or computed property exists, it gets only the static properties before it.
+     *   `:style="{ color: 'red', display: 'flex', ...spread, width: '16px' }"`
+     *              ^^^^^^^^^^^^  ^^^^^^^^^^^^^^^
+     * - If non-static object exists, it gets only the static properties up to that object.
+     *   `:style="[ { color: 'red' }, { display: 'flex', color, width: '16px' }, { height: '16px' } ]"`
+     *                ^^^^^^^^^^^^      ^^^^^^^^^^^^^^^         ^^^^^^^^^^^^^
+     * - If all properties are static properties, it returns one root node.
+     *   `:style="[ { color: 'red' }, { display: 'flex', width: '16px' } ]"`
+     *    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+     * @param {VAttribute} node `:style` node to check
+     * @returns {AssignmentProperty[] | [VAttribute]} the static properties.
+     */
+    function getReportNodes (node) {
+      const { value } = node
+      if (!value) {
+        return []
+      }
+      const { expression } = value
+      if (!expression) {
+        return []
+      }
+
+      let elements
+      if (expression.type === 'ObjectExpression') {
+        elements = [expression]
+      } else if (expression.type === 'ArrayExpression') {
+        elements = expression.elements
+      } else {
+        return []
+      }
+      const staticProperties = []
+      for (const element of elements) {
+        if (!element) {
+          continue
+        }
+        if (element.type !== 'ObjectExpression') {
+          return staticProperties
+        }
+
+        let isAllStatic = true
+        for (const prop of element.properties) {
+          if (prop.type === 'SpreadElement' || prop.computed) {
+            // If `SpreadElement` or computed property exists, it gets only the static properties before it.
+            return staticProperties
+          }
+          if (isStaticValue(prop)) {
+            staticProperties.push(prop)
+          } else {
+            isAllStatic = false
+          }
+        }
+        if (!isAllStatic) {
+          // If non-static object exists, it gets only the static properties up to that object.
+          return staticProperties
+        }
+      }
+      // If all properties are static properties, it returns one root node.
+      return [node]
+    }
+
+    /**
+     * Reports if the value is static.
+     * @param {VAttribute} node `:style` node to check
+     */
+    function verifyVBindStyle (node) {
+      for (const n of getReportNodes(node)) {
+        context.report({
+          node: n,
+          messageId: 'forbiddenStaticInlineStyle'
+        })
+      }
+    }
+
+    const visitor = {
+      "VAttribute[directive=false][key.name='style']" (node) {
+        context.report({
+          node,
+          messageId: 'forbiddenStyleAttr'
+        })
+      }
+    }
+    if (!context.options[0] || !context.options[0].allowBinding) {
+      visitor[
+        "VAttribute[directive=true][key.name.name='bind'][key.argument.name='style']"
+      ] = verifyVBindStyle
+    }
+
+    return utils.defineTemplateBodyVisitor(context, visitor)
+  }
+}


Test patch:
diff --git a/tests/lib/rules/no-static-inline-styles.js b/tests/lib/rules/no-static-inline-styles.js
new file mode 100644
index 00000000..e82a8870
--- /dev/null
+++ b/tests/lib/rules/no-static-inline-styles.js
@@ -0,0 +1,291 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/no-static-inline-styles')
+
+const tester = new RuleTester({
+  parser: 'vue-eslint-parser',
+  parserOptions: {
+    ecmaVersion: 2019
+  }
+})
+
+tester.run('no-static-inline-styles', rule, {
+  valid: [
+    `
+    <template>
+      <div class="hidden">
+    </template>
+    `,
+    `
+    <template>
+      <div :style="prop">
+    </template>
+    `,
+    `
+    <template>
+      <div :style="{
+        backgroundImage: 'url('+data.img+')'
+      }">
+    </template>
+    `,
+    `
+    <template>
+      <div :style="{
+       [backgroundImage]: 'url(./img.png)'
+      }" }>
+    </template>
+    `,
+    `
+    <template>
+      <div v-bind:style>
+    </template>
+    `,
+    {
+      code: `
+      <template>
+        <div :style="{
+          backgroundImage: 'url(./img.png)'
+        }">
+      </template>
+      `,
+      options: [{ allowBinding: true }]
+    },
+    // has unknown key name
+    `
+    <template>
+      <div :style="{
+        [computed]: 'url(./img.png)',
+        'background-image': 'url(./img.png)',
+        color: 'red',
+      }">
+    </template>
+    `,
+    `
+    <template>
+      <div :style="{
+        ...spread,
+        'background-image': 'url(./img.png)',
+        color: 'red',
+      }">
+    </template>
+    `,
+    // array
+    `
+    <template>
+      <div :style="[
+        { ...propStyle },
+        { backgroundImage: 'url(./img.png)' },
+      ]">
+    </template>
+    `,
+    // parse error
+    `
+    <template>
+      <div v-bind:style="{
+        'background-image': 'url(./img.png)'
+        ">
+    </template>
+    `
+  ],
+  invalid: [
+    {
+      code: `
+      <template>
+        <div style="display: none;">
+      </template>`,
+      errors: [
+        {
+          message: '`style` attributes are forbidden.',
+          line: 3
+        }
+      ]
+    },
+    {
+      code: `
+      <template>
+        <div style>
+      </template>`,
+      errors: [
+        {
+          message: '`style` attributes are forbidden.',
+          line: 3
+        }
+      ]
+    },
+    {
+      code: `
+      <template>
+        <div :style="{
+          backgroundImage: 'url(./img.png)'
+        }">
+      </template>`,
+      errors: [
+        {
+          message: 'Static inline `style` are forbidden.',
+          line: 3
+        }
+      ]
+    },
+    {
+      code: `
+      <template>
+        <div :style="{
+          'background-image': 'url(./img.png)'
+        }">
+      </template>
+      `,
+      errors: [
+        {
+          message: 'Static inline `style` are forbidden.',
+          line: 3
+        }
+      ]
+    },
+    {
+      code: `
+      <template>
+        <div v-bind:style="{
+          'background-image': 'url(./img.png)'
+        }">
+      </template>
+      `,
+      errors: [
+        {
+          message: 'Static inline `style` are forbidden.',
+          line: 3
+        }
+      ]
+    },
+    // property
+    {
+      code: `
+      <template>
+        <div :style="{
+          color: color,
+          'background-image': 'url(./img.png)'
+        }">
+      </template>
+      `,
+      errors: [
+        {
+          message: 'Static inline `style` are forbidden.',
+          line: 5
+        }
+      ]
+    },
+    {
+      code: `
+      <template>
+        <div :style="{
+          'background-image': 'url(./img.png)',
+          color,
+        }">
+      </template>
+      `,
+      errors: [
+        {
+          message: 'Static inline `style` are forbidden.',
+          line: 4
+        }
+      ]
+    },
+    // array
+    {
+      code: `
+      <template>
+        <div :style="[
+          { 'background-image': 'url(./img.png)' },
+          { color: 'red' }
+        ]">
+      </template>
+      `,
+      errors: [
+        {
+          message: 'Static inline `style` are forbidden.',
+          line: 3
+        }
+      ]
+    },
+    {
+      code: `
+      <template>
+        <div :style="[
+          { backgroundImage: 'url(./img.png)' },
+          { ...propStyle },
+          { backgroundImage: 'url(./img.png)' },
+        ]">
+      </template>
+      `,
+      errors: [
+        {
+          message: 'Static inline `style` are forbidden.',
+          line: 4
+        }
+      ]
+    },
+    {
+      code: `
+      <template>
+        <div :style="[
+          { backgroundImage: 'url(./img.png)' },
+          { backgroundImage: 'url(./img.png)', color, font: 'xxx', },
+          { backgroundImage: 'url(./img.png)' },
+        ]">
+      </template>
+      `,
+      errors: [
+        {
+          message: 'Static inline `style` are forbidden.',
+          line: 4
+        },
+        {
+          message: 'Static inline `style` are forbidden.',
+          line: 5
+        },
+        {
+          message: 'Static inline `style` are forbidden.',
+          line: 5
+        }
+      ]
+    },
+    {
+      code: `
+      <template>
+        <div :style="[
+          { backgroundImage: 'url(./img.png)' },
+          ,
+          { backgroundImage: 'url(./img.png)' },
+        ]">
+      </template>
+      `,
+      errors: [
+        {
+          message: 'Static inline `style` are forbidden.',
+          line: 3
+        }
+      ]
+    },
+    {
+      code: `
+      <template>
+        <div :style="[
+          { backgroundImage: 'url(./img.png)' },
+          'string',
+          { backgroundImage: 'url(./img.png)' },
+        ]">
+      </template>
+      `,
+      errors: [
+        {
+          message: 'Static inline `style` are forbidden.',
+          line: 4
+        }
+      ]
+    }
+  ]
+})