Skip to content

⭐️New: Add vue/component-name-in-template-casing #397

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
98dbf8a
Add Vue.extend support, add missing info about Vue.mixin check in readme
michalsnik Jan 2, 2018
7c4a1d2
Docs: fixes wording in docs (#372)
samturrell Feb 1, 2018
cd22a28
Fix: fix script-indent to prevent removing <script> tag (fixes #367) …
mysticatea Feb 16, 2018
ee5cc0a
[Update] Make `vue/max-attributes-per-line` fixable (#380)
ota-meshi Feb 16, 2018
6861c81
Update: make `vue/order-in-components` fixable (#381)
ota-meshi Feb 17, 2018
2b4798b
[New] Add `vue/component-name-in-template-casing`
ota-meshi Feb 19, 2018
3f86365
[update] documents
ota-meshi Feb 19, 2018
3dd2038
[fix] require-meta-docs-url
ota-meshi Feb 19, 2018
e35ca6c
[fix] failed tests
ota-meshi Feb 19, 2018
243e61a
[fix] review contents
ota-meshi Feb 22, 2018
de29eb8
[fix] No deletes space and attributes of endTag
ota-meshi Feb 23, 2018
fea5dfc
[fix] Remove test unnecessary option
ota-meshi Feb 23, 2018
38e4849
Merge branch 'master' into add-component-name-in-template-casing
ota-meshi Feb 24, 2018
2ce184b
[fix] lint error caused by merging the master for conflict resolution
ota-meshi Feb 24, 2018
25c7e4d
Merge branch 'upstream/master' into add-component-name-in-template-ca…
ota-meshi Jul 18, 2018
54fb3fb
Add ignores option.
ota-meshi Jul 18, 2018
e8e0db0
Fixed that extra differences.
ota-meshi Jul 19, 2018
802a9a8
Merge branch 'master' into add-component-name-in-template-casing
ota-meshi Jul 27, 2018
4b300dc
update docs link
ota-meshi Jul 27, 2018
33b47d5
Merge branch 'master' into pr/397
michalsnik Jul 30, 2018
fb3d433
Update formatting
michalsnik Jul 30, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ module.exports = {
All component-related rules are being applied to code that passes any of the following checks:

* `Vue.component()` expression
* `Vue.extend()` expression
* `Vue.mixin()` expression
* `export default {}` in `.vue` or `.jsx` file

If you however want to take advantage of our rules in any of your custom objects that are Vue components, you might need to use special comment `// @vue/component` that marks object in the next line as a Vue component in any file, e.g.:
Expand Down Expand Up @@ -169,6 +171,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi
| | Rule ID | Description |
|:---|:--------|:------------|
| :wrench: | [vue/attribute-hyphenation](./docs/rules/attribute-hyphenation.md) | enforce attribute naming style in template |
| :wrench: | [vue/component-name-in-template-casing](./docs/rules/component-name-in-template-casing.md) | enforce specific casing for the component naming style in template |
| :wrench: | [vue/html-end-tags](./docs/rules/html-end-tags.md) | enforce end tag style |
| :wrench: | [vue/html-indent](./docs/rules/html-indent.md) | enforce consistent indentation in `<template>` |
| :wrench: | [vue/html-self-closing](./docs/rules/html-self-closing.md) | enforce self-closing style |
Expand Down
54 changes: 54 additions & 0 deletions docs/rules/component-name-in-template-casing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# enforce specific casing for the component naming style in template (vue/component-name-in-template-casing)

- :gear: This rule is included in `"plugin:vue/strongly-recommended"` and `"plugin:vue/recommended"`.
- :wrench: The `--fix` option on the [command line](http://eslint.org/docs/user-guide/command-line-interface#fix) can automatically fix some of the problems reported by this rule.

Define a style for the component name in template casing for consistency purposes.

## :book: Rule Details

:+1: Examples of **correct** code for `PascalCase`:

```html
<template>
<TheComponent />
</template>
```

:-1: Examples of **incorrect** code for `PascalCase`:

```html
<template>
<the-component />
<theComponent />
<The-component />
</template>
```

:+1: Examples of **correct** code for `kebab-case`:

```html
<template>
<the-component />
</template>
```

:-1: Examples of **incorrect** code for `kebab-case`:

```html
<template>
<TheComponent />
<theComponent />
<Thecomponent />
<The-component />
</template>
```

## :wrench: Options

Default casing is set to `PascalCase`.
Copy link

@Mouvedia Mouvedia Apr 26, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It shouldn't be the default for the reasons Iv listed on #250 (comment)
Please don't merge this until that's fixed.

Copy link

@samit4me samit4me Jun 22, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From my understanding, we can only lint Single File Components and according to the Vue Style Guide PascalCase is strongly recommended in SFC's, so this seems like a sensible default. If you want to use kebab-case it's configurable so it can be either. +1 for this PR, really looking forward to it being merged 🎉

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like this plugin uses the Style Guide as the source of truth, so If you believe kebab-case should be the default (a lot of people I work with agree) it might be more appropriate to raise a new issue over on the Style Guide repo itself, see https://github.com/vuejs/vuejs.org/blob/e93b8371d73e4467dd8152703ddf1db423f489a2/src/v2/style-guide/index.md#single-file-component-filename-casing-strongly-recommended


```
"vue/component-name-in-template-casing": ["error", "PascalCase|kebab-case"]
```

2 changes: 1 addition & 1 deletion docs/rules/no-multi-spaces.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

The `--fix` option on the command line can automatically fix some of the problems reported by this rule.

This rule aims to remove multiple spaces in a row between attributes witch are not used for indentation.
This rule aims to remove multiple spaces in a row between attributes which are not used for indentation.

## Rule Details

Expand Down
1 change: 1 addition & 0 deletions lib/configs/strongly-recommended.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ module.exports = {
extends: require.resolve('./essential'),
rules: {
'vue/attribute-hyphenation': 'error',
'vue/component-name-in-template-casing': 'error',
'vue/html-end-tags': 'error',
'vue/html-indent': 'error',
'vue/html-self-closing': 'error',
Expand Down
1 change: 1 addition & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ module.exports = {
rules: {
'attribute-hyphenation': require('./rules/attribute-hyphenation'),
'comment-directive': require('./rules/comment-directive'),
'component-name-in-template-casing': require('./rules/component-name-in-template-casing'),
'html-closing-bracket-newline': require('./rules/html-closing-bracket-newline'),
'html-closing-bracket-spacing': require('./rules/html-closing-bracket-spacing'),
'html-end-tags': require('./rules/html-end-tags'),
Expand Down
90 changes: 90 additions & 0 deletions lib/rules/component-name-in-template-casing.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/**
* @author Yosuke Ota
* issue https://github.com/vuejs/eslint-plugin-vue/issues/250
*/
'use strict'

// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------

const utils = require('../utils')
const casing = require('../utils/casing')

const allowedCaseOptions = ['PascalCase', 'kebab-case']
const defaultCase = 'PascalCase'

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------

module.exports = {
meta: {
docs: {
description: 'enforce specific casing for the component naming style in template',
category: undefined,
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.2.2/docs/rules/component-name-in-template-casing.md'
},
fixable: 'code',
schema: [
{
enum: allowedCaseOptions
}
]
},

create (context) {
const options = context.options[0]
const caseType = allowedCaseOptions.indexOf(options) !== -1 ? options : defaultCase
const tokens = context.parserServices.getTemplateBodyTokenStore && context.parserServices.getTemplateBodyTokenStore()
const sourceCode = context.getSourceCode()

let hasInvalidEOF = false

return utils.defineTemplateBodyVisitor(context, {
'VElement' (node) {
if (hasInvalidEOF) {
return
}

if (!utils.isCustomComponent(node)) {
return
}

const name = node.rawName
const casingName = casing.getConverter(caseType)(name)
if (casingName !== name) {
const startTag = node.startTag
const open = tokens.getFirstToken(startTag)

context.report({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add empty line above, for clarity

node: open,
loc: open.loc,
message: 'Component name "{{name}}" is not {{caseType}}.',
data: {
name,
caseType
},
fix: fixer => {
const endTag = node.endTag
if (!endTag) {
return fixer.replaceText(open, `<${casingName}`)
}
// If we can upgrade requirements to `eslint@>4.1.0`, this code can be replaced by:
// return [
// fixer.replaceText(open, `<${casingName}`),
// fixer.replaceText(endTag, `</${casingName}>`)
// ]
const code = `<${casingName}${sourceCode.text.slice(open.range[1], endTag.range[0])}</${casingName}>`
return fixer.replaceTextRange([open.range[0], endTag.range[1]], code)
}
})
}
}
}, {
Program (node) {
hasInvalidEOF = utils.hasInvalidEOF(node)
}
})
}
}
7 changes: 4 additions & 3 deletions lib/rules/max-attributes-per-line.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ module.exports = {
category: 'strongly-recommended',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.2.2/docs/rules/max-attributes-per-line.md'
},
fixable: null,
fixable: 'whitespace', // or "code" or "whitespace"
schema: [
{
type: 'object',
Expand Down Expand Up @@ -129,14 +129,15 @@ module.exports = {
}

function showErrors (attributes, node) {
attributes.forEach((prop) => {
attributes.forEach((prop, i) => {
context.report({
node: prop,
loc: prop.loc,
message: 'Attribute "{{propName}}" should be on a new line.',
data: {
propName: prop.key.name
}
},
fix: i === 0 ? (fixer) => fixer.insertTextBefore(prop, '\n') : undefined
})
})
}
Expand Down
102 changes: 101 additions & 1 deletion lib/rules/order-in-components.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
'use strict'

const utils = require('../utils')
const Traverser = require('eslint/lib/util/traverser')

const defaultOrder = [
'el',
Expand Down Expand Up @@ -56,6 +57,75 @@ function getOrderMap (order) {
return orderMap
}

function isComma (node) {
return node.type === 'Punctuator' && node.value === ','
}

const ARITHMETIC_OPERATORS = ['+', '-', '*', '/', '%', '**']
const BITWISE_OPERATORS = ['&', '|', '^', '~', '<<', '>>', '>>>']
const COMPARISON_OPERATORS = ['==', '!=', '===', '!==', '>', '>=', '<', '<=']
const RELATIONAL_OPERATORS = ['in', 'instanceof']
const ALL_BINARY_OPERATORS = [].concat(
ARITHMETIC_OPERATORS,
BITWISE_OPERATORS,
COMPARISON_OPERATORS,
RELATIONAL_OPERATORS
)
const LOGICAL_OPERATORS = ['&&', '||']

/*
* Result `true` if the node is sure that there are no side effects
*
* Currently known side effects types
*
* node.type === 'CallExpression'
* node.type === 'NewExpression'
* node.type === 'UpdateExpression'
* node.type === 'AssignmentExpression'
* node.type === 'TaggedTemplateExpression'
* node.type === 'UnaryExpression' && node.operator === 'delete'
*
* @param {ASTNode} node target node
* @param {Object} visitorKeys sourceCode.visitorKey
* @returns {Boolean} no side effects
*/
function isNotSideEffectsNode (node, visitorKeys) {
let result = true
new Traverser().traverse(node, {
visitorKeys,
enter (node, parent) {
if (
node.type === 'FunctionExpression' ||
node.type === 'Identifier' ||
node.type === 'Literal' ||
// es2015
node.type === 'ArrowFunctionExpression' ||
node.type === 'TemplateElement'
) {
// no side effects node
this.skip()
} else if (
node.type !== 'Property' &&
node.type !== 'ObjectExpression' &&
node.type !== 'ArrayExpression' &&
(node.type !== 'UnaryExpression' || ['!', '~', '+', '-', 'typeof'].indexOf(node.operator) < 0) &&
(node.type !== 'BinaryExpression' || ALL_BINARY_OPERATORS.indexOf(node.operator) < 0) &&
(node.type !== 'LogicalExpression' || LOGICAL_OPERATORS.indexOf(node.operator) < 0) &&
node.type !== 'MemberExpression' &&
node.type !== 'ConditionalExpression' &&
// es2015
node.type !== 'SpreadElement' &&
node.type !== 'TemplateLiteral'
) {
// Can not be sure that a node has no side effects
result = false
this.break()
}
}
})
return result
}

// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
Expand All @@ -67,7 +137,7 @@ module.exports = {
category: 'recommended',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.2.2/docs/rules/order-in-components.md'
},
fixable: null,
fixable: 'code', // null or "code" or "whitespace"
schema: [
{
type: 'object',
Expand All @@ -86,6 +156,7 @@ module.exports = {
const order = options.order || defaultOrder
const extendedOrder = order.map(property => groups[property] || property)
const orderMap = getOrderMap(extendedOrder)
const sourceCode = context.getSourceCode()

function checkOrder (propertiesNodes, orderMap) {
const properties = propertiesNodes
Expand All @@ -109,6 +180,35 @@ module.exports = {
name: property.name,
firstUnorderedPropertyName: firstUnorderedProperty.name,
line
},
fix (fixer) {
const propertyNode = property.parent
const firstUnorderedPropertyNode = firstUnorderedProperty.parent
const hasSideEffectsPossibility = propertiesNodes
.slice(
propertiesNodes.indexOf(firstUnorderedPropertyNode),
propertiesNodes.indexOf(propertyNode) + 1
)
.some((property) => !isNotSideEffectsNode(property, sourceCode.visitorKeys))
if (hasSideEffectsPossibility) {
return undefined
}
const comma = sourceCode.getTokenAfter(propertyNode)
const hasAfterComma = isComma(comma)

const codeStart = sourceCode.getTokenBefore(propertyNode).range[1] // to include comments
const codeEnd = hasAfterComma ? comma.range[1] : propertyNode.range[1]

const propertyCode = sourceCode.text.slice(codeStart, codeEnd) + (hasAfterComma ? '' : ',')
const insertTarget = sourceCode.getTokenBefore(firstUnorderedPropertyNode)
// If we can upgrade requirements to `eslint@>4.1.0`, this code can be replaced by:
// return [
// fixer.removeRange([codeStart, codeEnd]),
// fixer.insertTextAfter(insertTarget, propertyCode)
// ]
const insertStart = insertTarget.range[1]
const newCode = propertyCode + sourceCode.text.slice(insertStart, codeStart)
return fixer.replaceTextRange([insertStart, codeEnd], newCode)
}
})
}
Expand Down
10 changes: 9 additions & 1 deletion lib/utils/indent-common.js
Original file line number Diff line number Diff line change
Expand Up @@ -631,8 +631,16 @@ module.exports.defineVisitor = function create (context, tokenStore, defaultOpti
*/
function validateCore (token, expectedIndent, optionalExpectedIndent) {
const line = token.loc.start.line
const actualIndent = token.loc.start.column
const indentText = getIndentText(token)

// If there is no line terminator after the `<script>` start tag,
// `indentText` contains non-whitespace characters.
// In that case, do nothing in order to prevent removing the `<script>` tag.
if (indentText.trim() !== '') {
return
}

const actualIndent = token.loc.start.column
const unit = (options.indentChar === '\t' ? 'tab' : 'space')

for (let i = 0; i < indentText.length; ++i) {
Expand Down
2 changes: 1 addition & 1 deletion lib/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,7 @@ module.exports = {
callee.object.type === 'Identifier' &&
callee.object.name === 'Vue' &&
callee.property.type === 'Identifier' &&
(callee.property.name === 'component' || callee.property.name === 'mixin') &&
['component', 'mixin', 'extend'].indexOf(callee.property.name) > -1 &&
node.arguments.length &&
node.arguments.slice(-1)[0].type === 'ObjectExpression'

Expand Down
3 changes: 3 additions & 0 deletions tests/fixtures/script-indent/no-linebreak-script.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<!--{}-->
<script>var a
</script>
Loading