node_modules

This commit is contained in:
saeid
2019-07-15 13:07:31 +04:30
parent d3735c6d30
commit ce1a3ebf9c
35713 changed files with 3639219 additions and 1 deletions
+101
View File
@@ -0,0 +1,101 @@
/**
* @fileoverview Define a style for the props casing in templates.
* @author Armano
*/
'use strict'
const utils = require('../utils')
const casing = require('../utils/casing')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'enforce attribute naming style on custom components in template',
category: 'strongly-recommended',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/attribute-hyphenation.md'
},
fixable: 'code',
schema: [
{
enum: ['always', 'never']
},
{
type: 'object',
properties: {
'ignore': {
type: 'array',
items: {
allOf: [
{ type: 'string' },
{ not: { type: 'string', pattern: ':exit$' }},
{ not: { type: 'string', pattern: '^\\s*$' }}
]
},
uniqueItems: true,
additionalItems: false
}
},
additionalProperties: false
}
]
},
create (context) {
const sourceCode = context.getSourceCode()
const option = context.options[0]
const optionsPayload = context.options[1]
const useHyphenated = option !== 'never'
let ignoredAttributes = ['data-', 'aria-', 'slot-scope']
if (optionsPayload && optionsPayload.ignore) {
ignoredAttributes = ignoredAttributes.concat(optionsPayload.ignore)
}
const caseConverter = casing.getConverter(useHyphenated ? 'kebab-case' : 'camelCase')
function reportIssue (node, name) {
const text = sourceCode.getText(node.key)
context.report({
node: node.key,
loc: node.loc,
message: useHyphenated ? "Attribute '{{text}}' must be hyphenated." : "Attribute '{{text}}' can't be hyphenated.",
data: {
text
},
fix: fixer => fixer.replaceText(node.key, text.replace(name, caseConverter(name)))
})
}
function isIgnoredAttribute (value) {
const isIgnored = ignoredAttributes.some(function (attr) {
return value.indexOf(attr) !== -1
})
if (isIgnored) {
return true
}
return useHyphenated ? value.toLowerCase() === value : !/-/.test(value)
}
// ----------------------------------------------------------------------
// Public
// ----------------------------------------------------------------------
return utils.defineTemplateBodyVisitor(context, {
VAttribute (node) {
if (!utils.isCustomComponent(node.parent.parent)) return
const name = !node.directive ? node.key.rawName : node.key.name === 'bind' ? node.key.raw.argument : false
if (!name || isIgnoredAttribute(name)) return
reportIssue(node, name)
}
})
}
}
+129
View File
@@ -0,0 +1,129 @@
/**
* @fileoverview enforce ordering of attributes
* @author Erin Depew
*/
'use strict'
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
function getAttributeType (name, isDirective) {
if (isDirective) {
if (name === 'for') {
return 'LIST_RENDERING'
} else if (name === 'if' || name === 'else-if' || name === 'else' || name === 'show' || name === 'cloak') {
return 'CONDITIONALS'
} else if (name === 'pre' || name === 'once') {
return 'RENDER_MODIFIERS'
} else if (name === 'model' || name === 'bind') {
return 'BINDING'
} else if (name === 'on') {
return 'EVENTS'
} else if (name === 'html' || name === 'text') {
return 'CONTENT'
}
} else {
if (name === 'is') {
return 'DEFINITION'
} else if (name === 'id') {
return 'GLOBAL'
} else if (name === 'ref' || name === 'key' || name === 'slot' || name === 'slot-scope') {
return 'UNIQUE'
} else {
return 'OTHER_ATTR'
}
}
}
function getPosition (attribute, attributeOrder) {
const attributeType = getAttributeType(attribute.key.name, attribute.directive)
return attributeOrder.indexOf(attributeType)
}
function create (context) {
const sourceCode = context.getSourceCode()
let attributeOrder = ['DEFINITION', 'LIST_RENDERING', 'CONDITIONALS', 'RENDER_MODIFIERS', 'GLOBAL', 'UNIQUE', 'BINDING', 'OTHER_ATTR', 'EVENTS', 'CONTENT']
if (context.options[0] && context.options[0].order) {
attributeOrder = context.options[0].order
}
let currentPosition
let previousNode
function reportIssue (node, previousNode) {
const currentNode = sourceCode.getText(node.key)
const prevNode = sourceCode.getText(previousNode.key)
context.report({
node: node.key,
loc: node.loc,
message: `Attribute "${currentNode}" should go before "${prevNode}".`,
data: {
currentNode
},
fix (fixer) {
const attributes = node.parent.attributes
const shiftAttrs = attributes.slice(attributes.indexOf(previousNode), attributes.indexOf(node) + 1)
// If we can upgrade requirements to `eslint@>4.1.0`, this code can be replaced by:
// return shiftAttrs.map((attr, i) => {
// const text = attr === previousNode ? sourceCode.getText(node) : sourceCode.getText(shiftAttrs[i - 1])
// return fixer.replaceText(attr, text)
// })
const replaceDataList = shiftAttrs.map((attr, i) => {
const text = attr === previousNode ? sourceCode.getText(node) : sourceCode.getText(shiftAttrs[i - 1])
return {
range: attr.range,
text
}
})
const replaceRange = [previousNode.range[0], node.range[1]]
let text = sourceCode.text.slice(replaceRange[0], replaceRange[1])
replaceDataList.reverse().forEach((data) => {
const textRange = data.range.map(r => r - replaceRange[0])
text = text.slice(0, textRange[0]) + data.text + text.slice(textRange[1], text.length)
})
return fixer.replaceTextRange(replaceRange, text)
}
})
}
return utils.defineTemplateBodyVisitor(context, {
'VStartTag' () {
currentPosition = -1
previousNode = null
},
'VAttribute' (node) {
if ((currentPosition === -1) || (currentPosition <= getPosition(node, attributeOrder))) {
currentPosition = getPosition(node, attributeOrder)
previousNode = node
} else {
reportIssue(node, previousNode)
}
}
})
}
module.exports = {
meta: {
docs: {
description: 'enforce order of attributes',
category: 'recommended',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.5.0/docs/rules/attributes-order.md'
},
fixable: 'code',
schema: {
type: 'array',
properties: {
order: {
items: {
type: 'string'
},
maxItems: 10,
minItems: 10
}
}
}
},
create
}
+138
View File
@@ -0,0 +1,138 @@
/**
* @author Toru Nagashima <https://github.com/mysticatea>
*/
/* eslint-disable eslint-plugin/report-message-format, consistent-docs-description */
'use strict'
// -----------------------------------------------------------------------------
// Helpers
// -----------------------------------------------------------------------------
const COMMENT_DIRECTIVE_B = /^\s*(eslint-(?:en|dis)able)(?:\s+(\S|\S[\s\S]*\S))?\s*$/
const COMMENT_DIRECTIVE_L = /^\s*(eslint-disable(?:-next)?-line)(?:\s+(\S|\S[\s\S]*\S))?\s*$/
/**
* Parse a given comment.
* @param {RegExp} pattern The RegExp pattern to parse.
* @param {string} comment The comment value to parse.
* @returns {({type:string,rules:string[]})|null} The parsing result.
*/
function parse (pattern, comment) {
const match = pattern.exec(comment)
if (match == null) {
return null
}
const type = match[1]
const rules = (match[2] || '')
.split(',')
.map(s => s.trim())
.filter(Boolean)
return { type, rules }
}
/**
* Enable rules.
* @param {RuleContext} context The rule context.
* @param {{line:number,column:number}} loc The location information to enable.
* @param {string} group The group to enable.
* @param {string[]} rules The rule IDs to enable.
* @returns {void}
*/
function enable (context, loc, group, rules) {
if (rules.length === 0) {
context.report({ loc, message: '++ {{group}}', data: { group }})
} else {
context.report({ loc, message: '+ {{group}} {{rules}}', data: { group, rules: rules.join(' ') }})
}
}
/**
* Disable rules.
* @param {RuleContext} context The rule context.
* @param {{line:number,column:number}} loc The location information to disable.
* @param {string} group The group to disable.
* @param {string[]} rules The rule IDs to disable.
* @returns {void}
*/
function disable (context, loc, group, rules) {
if (rules.length === 0) {
context.report({ loc, message: '-- {{group}}', data: { group }})
} else {
context.report({ loc, message: '- {{group}} {{rules}}', data: { group, rules: rules.join(' ') }})
}
}
/**
* Process a given comment token.
* If the comment is `eslint-disable` or `eslint-enable` then it reports the comment.
* @param {RuleContext} context The rule context.
* @param {Token} comment The comment token to process.
* @returns {void}
*/
function processBlock (context, comment) {
const parsed = parse(COMMENT_DIRECTIVE_B, comment.value)
if (parsed != null) {
if (parsed.type === 'eslint-disable') {
disable(context, comment.loc.start, 'block', parsed.rules)
} else {
enable(context, comment.loc.start, 'block', parsed.rules)
}
}
}
/**
* Process a given comment token.
* If the comment is `eslint-disable-line` or `eslint-disable-next-line` then it reports the comment.
* @param {RuleContext} context The rule context.
* @param {Token} comment The comment token to process.
* @returns {void}
*/
function processLine (context, comment) {
const parsed = parse(COMMENT_DIRECTIVE_L, comment.value)
if (parsed != null && comment.loc.start.line === comment.loc.end.line) {
const line = comment.loc.start.line + (parsed.type === 'eslint-disable-line' ? 0 : 1)
const column = -1
disable(context, { line, column }, 'line', parsed.rules)
enable(context, { line: line + 1, column }, 'line', parsed.rules)
}
}
// -----------------------------------------------------------------------------
// Rule Definition
// -----------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'support comment-directives in `<template>`',
category: 'base',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/comment-directive.md'
},
schema: []
},
create (context) {
return {
Program (node) {
if (!node.templateBody) {
return
}
// Send directives to the post-process.
for (const comment of node.templateBody.comments) {
processBlock(context, comment)
processLine(context, comment)
}
// Send a clear mark to the post-process.
context.report({
loc: node.templateBody.loc.end,
message: 'clear'
})
}
}
}
}
@@ -0,0 +1,86 @@
/**
* @author Toru Nagashima
* @copyright 2016 Toru Nagashima. All rights reserved.
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------------------
function getPhrase (lineBreaks) {
switch (lineBreaks) {
case 0: return 'no line breaks'
case 1: return '1 line break'
default: return `${lineBreaks} line breaks`
}
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "require or disallow a line break before tag's closing brackets",
category: undefined,
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/html-closing-bracket-newline.md'
},
fixable: 'whitespace',
schema: [{
type: 'object',
properties: {
'singleline': { enum: ['always', 'never'] },
'multiline': { enum: ['always', 'never'] }
},
additionalProperties: false
}]
},
create (context) {
const options = context.options[0] || {}
const template = context.parserServices.getTemplateBodyTokenStore && context.parserServices.getTemplateBodyTokenStore()
return utils.defineTemplateBodyVisitor(context, {
'VStartTag, VEndTag' (node) {
const closingBracketToken = template.getLastToken(node)
if (closingBracketToken.type !== 'HTMLSelfClosingTagClose' && closingBracketToken.type !== 'HTMLTagClose') {
return
}
const prevToken = template.getTokenBefore(closingBracketToken)
const type = (node.loc.start.line === prevToken.loc.end.line) ? 'singleline' : 'multiline'
const expectedLineBreaks = (options[type] === 'always') ? 1 : 0
const actualLineBreaks = (closingBracketToken.loc.start.line - prevToken.loc.end.line)
if (actualLineBreaks !== expectedLineBreaks) {
context.report({
node,
loc: {
start: prevToken.loc.end,
end: closingBracketToken.loc.start
},
message: 'Expected {{expected}} before closing bracket, but {{actual}} found.',
data: {
expected: getPhrase(expectedLineBreaks),
actual: getPhrase(actualLineBreaks)
},
fix (fixer) {
const range = [prevToken.range[1], closingBracketToken.range[0]]
const text = '\n'.repeat(expectedLineBreaks)
return fixer.replaceTextRange(range, text)
}
})
}
}
})
}
}
@@ -0,0 +1,113 @@
/**
* @author Toru Nagashima <https://github.com/mysticatea>
*/
'use strict'
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
const utils = require('../utils')
// -----------------------------------------------------------------------------
// Helpers
// -----------------------------------------------------------------------------
/**
* Normalize options.
* @param {{startTag?:"always"|"never",endTag?:"always"|"never",selfClosingTag?:"always"|"never"}} options The options user configured.
* @param {TokenStore} tokens The token store of template body.
* @returns {{startTag:"always"|"never",endTag:"always"|"never",selfClosingTag:"always"|"never"}} The normalized options.
*/
function parseOptions (options, tokens) {
return Object.assign({
startTag: 'never',
endTag: 'never',
selfClosingTag: 'always',
detectType (node) {
const openType = tokens.getFirstToken(node).type
const closeType = tokens.getLastToken(node).type
if (openType === 'HTMLEndTagOpen' && closeType === 'HTMLTagClose') {
return this.endTag
}
if (openType === 'HTMLTagOpen' && closeType === 'HTMLTagClose') {
return this.startTag
}
if (openType === 'HTMLTagOpen' && closeType === 'HTMLSelfClosingTagClose') {
return this.selfClosingTag
}
return null
}
}, options)
}
// -----------------------------------------------------------------------------
// Rule Definition
// -----------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'require or disallow a space before tag\'s closing brackets',
category: undefined,
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/html-closing-bracket-spacing.md'
},
schema: [{
type: 'object',
properties: {
startTag: { enum: ['always', 'never'] },
endTag: { enum: ['always', 'never'] },
selfClosingTag: { enum: ['always', 'never'] }
},
additionalProperties: false
}],
fixable: 'whitespace'
},
create (context) {
const sourceCode = context.getSourceCode()
const tokens =
context.parserServices.getTemplateBodyTokenStore &&
context.parserServices.getTemplateBodyTokenStore()
const options = parseOptions(context.options[0], tokens)
return utils.defineTemplateBodyVisitor(context, {
'VStartTag, VEndTag' (node) {
const type = options.detectType(node)
const lastToken = tokens.getLastToken(node)
const prevToken = tokens.getLastToken(node, 1)
// Skip if EOF exists in the tag or linebreak exists before `>`.
if (type == null || prevToken == null || prevToken.loc.end.line !== lastToken.loc.start.line) {
return
}
// Check and report.
const hasSpace = (prevToken.range[1] !== lastToken.range[0])
if (type === 'always' && !hasSpace) {
context.report({
node,
loc: lastToken.loc,
message: "Expected a space before '{{bracket}}', but not found.",
data: { bracket: sourceCode.getText(lastToken) },
fix: (fixer) => fixer.insertTextBefore(lastToken, ' ')
})
} else if (type === 'never' && hasSpace) {
context.report({
node,
loc: {
start: prevToken.loc.end,
end: lastToken.loc.end
},
message: "Expected no space before '{{bracket}}', but found.",
data: { bracket: sourceCode.getText(lastToken) },
fix: (fixer) => fixer.removeRange([prevToken.range[1], lastToken.range[0]])
})
}
}
})
}
}
+59
View File
@@ -0,0 +1,59 @@
/**
* @author Toru Nagashima
* @copyright 2017 Toru Nagashima. All rights reserved.
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'enforce end tag style',
category: 'strongly-recommended',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/html-end-tags.md'
},
fixable: 'code',
schema: []
},
create (context) {
let hasInvalidEOF = false
return utils.defineTemplateBodyVisitor(context, {
VElement (node) {
if (hasInvalidEOF) {
return
}
const name = node.name
const isVoid = utils.isHtmlVoidElementName(name)
const isSelfClosing = node.startTag.selfClosing
const hasEndTag = node.endTag != null
if (!isVoid && !hasEndTag && !isSelfClosing) {
context.report({
node: node.startTag,
loc: node.startTag.loc,
message: "'<{{name}}>' should have end tag.",
data: { name },
fix: (fixer) => fixer.insertTextAfter(node, `</${name}>`)
})
}
}
}, {
Program (node) {
hasInvalidEOF = utils.hasInvalidEOF(node)
}
})
}
}
+66
View File
@@ -0,0 +1,66 @@
/**
* @author Toru Nagashima
* @copyright 2016 Toru Nagashima. All rights reserved.
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const indentCommon = require('../utils/indent-common')
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
create (context) {
const tokenStore =
context.parserServices.getTemplateBodyTokenStore &&
context.parserServices.getTemplateBodyTokenStore()
const visitor = indentCommon.defineVisitor(context, tokenStore, { baseIndent: 1 })
return utils.defineTemplateBodyVisitor(context, visitor)
},
meta: {
docs: {
description: 'enforce consistent indentation in `<template>`',
category: 'strongly-recommended',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/html-indent.md'
},
fixable: 'whitespace',
schema: [
{
anyOf: [
{ type: 'integer', minimum: 1 },
{ enum: ['tab'] }
]
},
{
type: 'object',
properties: {
'attribute': { type: 'integer', minimum: 0 },
'closeBracket': { type: 'integer', minimum: 0 },
'switchCase': { type: 'integer', minimum: 0 },
'alignAttributesVertically': { type: 'boolean' },
'ignores': {
type: 'array',
items: {
allOf: [
{ type: 'string' },
{ not: { type: 'string', pattern: ':exit$' }},
{ not: { type: 'string', pattern: '^\\s*$' }}
]
},
uniqueItems: true,
additionalItems: false
}
},
additionalProperties: false
}
]
}
}
+69
View File
@@ -0,0 +1,69 @@
/**
* @author Toru Nagashima
* @copyright 2017 Toru Nagashima. All rights reserved.
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'enforce quotes style of HTML attributes',
category: 'recommended',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/html-quotes.md'
},
fixable: 'code',
schema: [
{ enum: ['double', 'single'] }
]
},
create (context) {
const sourceCode = context.getSourceCode()
const double = context.options[0] !== 'single'
const quoteChar = double ? '"' : "'"
const quoteName = double ? 'double quotes' : 'single quotes'
const quotePattern = double ? /"/g : /'/g
const quoteEscaped = double ? '&quot;' : '&apos;'
let hasInvalidEOF
return utils.defineTemplateBodyVisitor(context, {
'VAttribute[value!=null]' (node) {
if (hasInvalidEOF) {
return
}
const text = sourceCode.getText(node.value)
const firstChar = text[0]
if (firstChar !== quoteChar) {
context.report({
node: node.value,
loc: node.value.loc,
message: 'Expected to be enclosed by {{kind}}.',
data: { kind: quoteName },
fix (fixer) {
const contentText = (firstChar === "'" || firstChar === '"') ? text.slice(1, -1) : text
const replacement = quoteChar + contentText.replace(quotePattern, quoteEscaped) + quoteChar
return fixer.replaceText(node.value, replacement)
}
})
}
}
}, {
Program (node) {
hasInvalidEOF = utils.hasInvalidEOF(node)
}
})
}
}
+179
View File
@@ -0,0 +1,179 @@
/**
* @author Toru Nagashima
* @copyright 2016 Toru Nagashima. All rights reserved.
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------------------
/**
* These strings wil be displayed in error messages.
*/
const ELEMENT_TYPE = Object.freeze({
NORMAL: 'HTML elements',
VOID: 'HTML void elements',
COMPONENT: 'Vue.js custom components',
SVG: 'SVG elements',
MATH: 'MathML elements'
})
/**
* Normalize the given options.
* @param {Object|undefined} options The raw options object.
* @returns {Object} Normalized options.
*/
function parseOptions (options) {
return {
[ELEMENT_TYPE.NORMAL]: (options && options.html && options.html.normal) || 'always',
[ELEMENT_TYPE.VOID]: (options && options.html && options.html.void) || 'never',
[ELEMENT_TYPE.COMPONENT]: (options && options.html && options.html.component) || 'always',
[ELEMENT_TYPE.SVG]: (options && options.svg) || 'always',
[ELEMENT_TYPE.MATH]: (options && options.math) || 'always'
}
}
/**
* Get the elementType of the given element.
* @param {VElement} node The element node to get.
* @returns {string} The elementType of the element.
*/
function getElementType (node) {
if (utils.isCustomComponent(node)) {
return ELEMENT_TYPE.COMPONENT
}
if (utils.isHtmlElementNode(node)) {
if (utils.isHtmlVoidElementName(node.name)) {
return ELEMENT_TYPE.VOID
}
return ELEMENT_TYPE.NORMAL
}
if (utils.isSvgElementNode(node)) {
return ELEMENT_TYPE.SVG
}
if (utils.isMathMLElementNode(node)) {
return ELEMENT_TYPE.MATH
}
return 'unknown elements'
}
/**
* Check whether the given element is empty or not.
* This ignores whitespaces, doesn't ignore comments.
* @param {VElement} node The element node to check.
* @param {SourceCode} sourceCode The source code object of the current context.
* @returns {boolean} `true` if the element is empty.
*/
function isEmpty (node, sourceCode) {
const start = node.startTag.range[1]
const end = (node.endTag != null) ? node.endTag.range[0] : node.range[1]
return sourceCode.text.slice(start, end).trim() === ''
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'enforce self-closing style',
category: 'strongly-recommended',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/html-self-closing.md'
},
fixable: 'code',
schema: {
definitions: {
optionValue: {
enum: ['always', 'never', 'any']
}
},
type: 'array',
items: [{
type: 'object',
properties: {
html: {
type: 'object',
properties: {
normal: { $ref: '#/definitions/optionValue' },
void: { $ref: '#/definitions/optionValue' },
component: { $ref: '#/definitions/optionValue' }
},
additionalProperties: false
},
svg: { $ref: '#/definitions/optionValue' },
math: { $ref: '#/definitions/optionValue' }
},
additionalProperties: false
}],
maxItems: 1
}
},
create (context) {
const sourceCode = context.getSourceCode()
const options = parseOptions(context.options[0])
let hasInvalidEOF = false
return utils.defineTemplateBodyVisitor(context, {
'VElement' (node) {
if (hasInvalidEOF) {
return
}
const elementType = getElementType(node)
const mode = options[elementType]
if (mode === 'always' && !node.startTag.selfClosing && isEmpty(node, sourceCode)) {
context.report({
node,
loc: node.loc,
message: 'Require self-closing on {{elementType}} (<{{name}}>).',
data: { elementType, name: node.rawName },
fix: (fixer) => {
const tokens = context.parserServices.getTemplateBodyTokenStore()
const close = tokens.getLastToken(node.startTag)
if (close.type !== 'HTMLTagClose') {
return null
}
return fixer.replaceTextRange([close.range[0], node.range[1]], '/>')
}
})
}
if (mode === 'never' && node.startTag.selfClosing) {
context.report({
node,
loc: node.loc,
message: 'Disallow self-closing on {{elementType}} (<{{name}}/>).',
data: { elementType, name: node.rawName },
fix: (fixer) => {
const tokens = context.parserServices.getTemplateBodyTokenStore()
const close = tokens.getLastToken(node.startTag)
if (close.type !== 'HTMLSelfClosingTagClose') {
return null
}
if (elementType === ELEMENT_TYPE.VOID) {
return fixer.replaceText(close, '>')
}
return fixer.replaceText(close, `></${node.rawName}>`)
}
})
}
}
}, {
Program (node) {
hasInvalidEOF = utils.hasInvalidEOF(node)
}
})
}
}
+69
View File
@@ -0,0 +1,69 @@
// the following rule is based on yannickcr/eslint-plugin-react
/**
The MIT License (MIT)
Copyright (c) 2014 Yannick Croissant
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
/**
* @fileoverview Prevent variables used in JSX to be marked as unused
* @author Yannick Croissant
*/
'use strict'
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'prevent variables used in JSX to be marked as unused', // eslint-disable-line consistent-docs-description
category: 'base',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/jsx-uses-vars.md'
},
schema: []
},
create (context) {
return {
JSXOpeningElement (node) {
let name
if (node.name.name) {
// <Foo>
name = node.name.name
} else if (node.name.object) {
// <Foo...Bar>
let parent = node.name.object
while (parent.object) {
parent = parent.object
}
name = parent.name
} else {
return
}
context.markVariableAsUsed(name)
}
}
}
}
+160
View File
@@ -0,0 +1,160 @@
/**
* @fileoverview Define the number of attributes allows per line
* @author Filipa Lacerda
*/
'use strict'
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const utils = require('../utils')
module.exports = {
meta: {
docs: {
description: 'enforce the maximum number of attributes per line',
category: 'strongly-recommended',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/max-attributes-per-line.md'
},
fixable: 'whitespace', // or "code" or "whitespace"
schema: [
{
type: 'object',
properties: {
singleline: {
anyOf: [
{
type: 'number',
minimum: 1
},
{
type: 'object',
properties: {
max: {
type: 'number',
minimum: 1
}
},
additionalProperties: false
}
]
},
multiline: {
anyOf: [
{
type: 'number',
minimum: 1
},
{
type: 'object',
properties: {
max: {
type: 'number',
minimum: 1
},
allowFirstLine: {
type: 'boolean'
}
},
additionalProperties: false
}
]
}
}
}
]
},
create: function (context) {
const configuration = parseOptions(context.options[0])
const multilineMaximum = configuration.multiline
const singlelinemMaximum = configuration.singleline
const canHaveFirstLine = configuration.allowFirstLine
return utils.defineTemplateBodyVisitor(context, {
'VStartTag' (node) {
const numberOfAttributes = node.attributes.length
if (!numberOfAttributes) return
if (utils.isSingleLine(node) && numberOfAttributes > singlelinemMaximum) {
showErrors(node.attributes.slice(singlelinemMaximum), node)
}
if (!utils.isSingleLine(node)) {
if (!canHaveFirstLine && node.attributes[0].loc.start.line === node.loc.start.line) {
showErrors([node.attributes[0]], node)
}
groupAttrsByLine(node.attributes)
.filter(attrs => attrs.length > multilineMaximum)
.forEach(attrs => showErrors(attrs.splice(multilineMaximum), node))
}
}
})
// ----------------------------------------------------------------------
// Helpers
// ----------------------------------------------------------------------
function parseOptions (options) {
const defaults = {
singleline: 1,
multiline: 1,
allowFirstLine: false
}
if (options) {
if (typeof options.singleline === 'number') {
defaults.singleline = options.singleline
} else if (options.singleline && options.singleline.max) {
defaults.singleline = options.singleline.max
}
if (options.multiline) {
if (typeof options.multiline === 'number') {
defaults.multiline = options.multiline
} else if (typeof options.multiline === 'object') {
if (options.multiline.max) {
defaults.multiline = options.multiline.max
}
if (options.multiline.allowFirstLine) {
defaults.allowFirstLine = options.multiline.allowFirstLine
}
}
}
}
return defaults
}
function showErrors (attributes, node) {
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
})
})
}
function groupAttrsByLine (attributes) {
const propsPerLine = [[attributes[0]]]
attributes.reduce((previous, current) => {
if (previous.loc.end.line === current.loc.start.line) {
propsPerLine[propsPerLine.length - 1].push(current)
} else {
propsPerLine.push([current])
}
return current
})
return propsPerLine
}
}
}
@@ -0,0 +1,99 @@
/**
* @fileoverview enforce unified spacing in mustache interpolations.
* @author Armano
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'enforce unified spacing in mustache interpolations',
category: 'strongly-recommended',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/mustache-interpolation-spacing.md'
},
fixable: 'whitespace',
schema: [
{
enum: ['always', 'never']
}
]
},
create (context) {
const options = context.options[0] || 'always'
const template =
context.parserServices.getTemplateBodyTokenStore &&
context.parserServices.getTemplateBodyTokenStore()
// ----------------------------------------------------------------------
// Public
// ----------------------------------------------------------------------
return utils.defineTemplateBodyVisitor(context, {
'VExpressionContainer[expression!=null]' (node) {
const openBrace = template.getFirstToken(node)
const closeBrace = template.getLastToken(node)
if (
!openBrace ||
!closeBrace ||
openBrace.type !== 'VExpressionStart' ||
closeBrace.type !== 'VExpressionEnd'
) {
return
}
const firstToken = template.getTokenAfter(openBrace, { includeComments: true })
const lastToken = template.getTokenBefore(closeBrace, { includeComments: true })
if (options === 'always') {
if (openBrace.range[1] === firstToken.range[0]) {
context.report({
node: openBrace,
message: "Expected 1 space after '{{', but not found.",
fix: (fixer) => fixer.insertTextAfter(openBrace, ' ')
})
}
if (closeBrace.range[0] === lastToken.range[1]) {
context.report({
node: closeBrace,
message: "Expected 1 space before '}}', but not found.",
fix: (fixer) => fixer.insertTextBefore(closeBrace, ' ')
})
}
} else {
if (openBrace.range[1] !== firstToken.range[0]) {
context.report({
loc: {
start: openBrace.loc.start,
end: firstToken.loc.start
},
message: "Expected no space after '{{', but found.",
fix: (fixer) => fixer.removeRange([openBrace.range[1], firstToken.range[0]])
})
}
if (closeBrace.range[0] !== lastToken.range[1]) {
context.report({
loc: {
start: lastToken.loc.end,
end: closeBrace.loc.end
},
message: "Expected no space before '}}', but found.",
fix: (fixer) => fixer.removeRange([lastToken.range[1], closeBrace.range[0]])
})
}
}
}
})
}
}
+62
View File
@@ -0,0 +1,62 @@
/**
* @fileoverview Requires specific casing for the name property in Vue components
* @author Armano
*/
'use strict'
const utils = require('../utils')
const casing = require('../utils/casing')
const allowedCaseOptions = ['PascalCase', 'kebab-case']
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'enforce specific casing for the name property in Vue components',
category: 'strongly-recommended',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/name-property-casing.md'
},
fixable: 'code', // or "code" or "whitespace"
schema: [
{
enum: allowedCaseOptions
}
]
},
create (context) {
const options = context.options[0]
const caseType = allowedCaseOptions.indexOf(options) !== -1 ? options : 'PascalCase'
// ----------------------------------------------------------------------
// Public
// ----------------------------------------------------------------------
return utils.executeOnVue(context, (obj) => {
const node = obj.properties
.find(item => (
item.type === 'Property' &&
item.key.name === 'name' &&
item.value.type === 'Literal'
))
if (!node) return
const value = casing.getConverter(caseType)(node.value.value)
if (value !== node.value.value) {
context.report({
node: node.value,
message: 'Property name "{{value}}" is not {{caseType}}.',
data: {
value: node.value.value,
caseType: caseType
},
fix: fixer => fixer.replaceText(node.value, node.value.raw.replace(node.value.value, value))
})
}
})
}
}
@@ -0,0 +1,156 @@
/**
* @fileoverview Check if there are no asynchronous actions inside computed properties.
* @author Armano
*/
'use strict'
const utils = require('../utils')
const PROMISE_FUNCTIONS = [
'then',
'catch',
'finally'
]
const PROMISE_METHODS = [
'all',
'race',
'reject',
'resolve'
]
const TIMED_FUNCTIONS = [
'setTimeout',
'setInterval',
'setImmediate',
'requestAnimationFrame'
]
function isTimedFunction (node) {
return (
node.type === 'CallExpression' &&
node.callee.type === 'Identifier' &&
TIMED_FUNCTIONS.indexOf(node.callee.name) !== -1
) || (
node.type === 'CallExpression' &&
node.callee.type === 'MemberExpression' &&
node.callee.object.type === 'Identifier' &&
node.callee.object.name === 'window' && (
TIMED_FUNCTIONS.indexOf(node.callee.property.name) !== -1
)
) && node.arguments.length
}
function isPromise (node) {
if (node.type === 'CallExpression' && node.callee.type === 'MemberExpression') {
return ( // hello.PROMISE_FUNCTION()
node.callee.property.type === 'Identifier' &&
PROMISE_FUNCTIONS.indexOf(node.callee.property.name) !== -1
) || ( // Promise.PROMISE_METHOD()
node.callee.object.type === 'Identifier' &&
node.callee.object.name === 'Promise' &&
PROMISE_METHODS.indexOf(node.callee.property.name) !== -1
)
}
return false
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'disallow asynchronous actions in computed properties',
category: 'essential',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/no-async-in-computed-properties.md'
},
fixable: null,
schema: []
},
create (context) {
const forbiddenNodes = []
const expressionTypes = {
promise: 'asynchronous action',
await: 'await operator',
async: 'async function declaration',
new: 'Promise object',
timed: 'timed function'
}
function onFunctionEnter (node) {
if (node.async) {
forbiddenNodes.push({
node: node,
type: 'async'
})
}
}
return Object.assign({},
{
FunctionDeclaration: onFunctionEnter,
FunctionExpression: onFunctionEnter,
ArrowFunctionExpression: onFunctionEnter,
NewExpression (node) {
if (node.callee.name === 'Promise') {
forbiddenNodes.push({
node: node,
type: 'new'
})
}
},
CallExpression (node) {
if (isPromise(node)) {
forbiddenNodes.push({
node: node,
type: 'promise'
})
}
if (isTimedFunction(node)) {
forbiddenNodes.push({
node: node,
type: 'timed'
})
}
},
AwaitExpression (node) {
forbiddenNodes.push({
node: node,
type: 'await'
})
}
},
utils.executeOnVue(context, (obj) => {
const computedProperties = utils.getComputedProperties(obj)
computedProperties.forEach(cp => {
forbiddenNodes.forEach(el => {
if (
cp.value &&
el.node.loc.start.line >= cp.value.loc.start.line &&
el.node.loc.end.line <= cp.value.loc.end.line
) {
context.report({
node: el.node,
message: 'Unexpected {{expressionName}} in "{{propertyName}}" computed property.',
data: {
expressionName: expressionTypes[el.type],
propertyName: cp.key
}
})
}
})
})
})
)
}
}
+63
View File
@@ -0,0 +1,63 @@
/**
* @author Toru Nagashima
* @copyright 2017 Toru Nagashima. All rights reserved.
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------------------
/**
* Check whether the given `v-if` node is using the variable which is defined by the `v-for` directive.
* @param {ASTNode} vIf The `v-if` attribute node to check.
* @returns {boolean} `true` if the `v-if` is using the variable which is defined by the `v-for` directive.
*/
function isUsingIterationVar (vIf) {
const element = vIf.parent.parent
return vIf.value.references.some(reference =>
element.variables.some(variable =>
variable.id.name === reference.id.name &&
variable.kind === 'v-for'
)
)
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'disallow confusing `v-for` and `v-if` on the same element',
category: 'recommended',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/no-confusing-v-for-v-if.md'
},
fixable: null,
schema: []
},
create (context) {
return utils.defineTemplateBodyVisitor(context, {
"VAttribute[directive=true][key.name='if']" (node) {
const element = node.parent.parent
if (utils.hasDirective(element, 'for') && !isUsingIterationVar(node)) {
context.report({
node,
loc: node.loc,
message: "This 'v-if' should be moved to the wrapper element."
})
}
}
})
}
}
+63
View File
@@ -0,0 +1,63 @@
/**
* @fileoverview Prevents duplication of field names.
* @author Armano
*/
'use strict'
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const GROUP_NAMES = ['props', 'computed', 'data', 'methods']
module.exports = {
meta: {
docs: {
description: 'disallow duplication of field names',
category: 'essential',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/no-dupe-keys.md'
},
fixable: null, // or "code" or "whitespace"
schema: [
{
type: 'object',
properties: {
groups: {
type: 'array'
}
},
additionalProperties: false
}
]
},
create (context) {
const options = context.options[0] || {}
const groups = new Set(GROUP_NAMES.concat(options.groups || []))
// ----------------------------------------------------------------------
// Public
// ----------------------------------------------------------------------
return utils.executeOnVue(context, (obj) => {
const usedNames = []
const properties = utils.iterateProperties(obj, groups)
for (const o of properties) {
if (usedNames.indexOf(o.name) !== -1) {
context.report({
node: o.node,
message: "Duplicated key '{{name}}'.",
data: {
name: o.name
}
})
}
usedNames.push(o.name)
}
})
}
}
+104
View File
@@ -0,0 +1,104 @@
/**
* @author Toru Nagashima
* @copyright 2017 Toru Nagashima. All rights reserved.
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------------------
/**
* Get the name of the given attribute node.
* @param {ASTNode} attribute The attribute node to get.
* @returns {string} The name of the attribute.
*/
function getName (attribute) {
if (!attribute.directive) {
return attribute.key.name
}
if (attribute.key.name === 'bind') {
return attribute.key.argument || null
}
return null
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'disallow duplication of attributes',
category: 'essential',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/no-duplicate-attributes.md'
},
fixable: null,
schema: [
{
type: 'object',
properties: {
allowCoexistClass: {
type: 'boolean'
},
allowCoexistStyle: {
type: 'boolean'
}
}
}
]
},
create (context) {
const options = context.options[0] || {}
const allowCoexistStyle = options.allowCoexistStyle !== false
const allowCoexistClass = options.allowCoexistClass !== false
const directiveNames = new Set()
const attributeNames = new Set()
function isDuplicate (name, isDirective) {
if ((allowCoexistStyle && name === 'style') || (allowCoexistClass && name === 'class')) {
return isDirective ? directiveNames.has(name) : attributeNames.has(name)
}
return directiveNames.has(name) || attributeNames.has(name)
}
return utils.defineTemplateBodyVisitor(context, {
'VStartTag' () {
directiveNames.clear()
attributeNames.clear()
},
'VAttribute' (node) {
const name = getName(node)
if (name == null) {
return
}
if (isDuplicate(name, node.directive)) {
context.report({
node,
loc: node.loc,
message: "Duplicate attribute '{{name}}'.",
data: { name }
})
}
if (node.directive) {
directiveNames.add(name)
} else {
attributeNames.add(name)
}
}
})
}
}
+69
View File
@@ -0,0 +1,69 @@
/**
* @fileoverview This rule warns about the usage of extra whitespaces between attributes
* @author Armano
*/
'use strict'
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'disallow multiple spaces',
category: 'strongly-recommended',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/no-multi-spaces.md'
},
fixable: 'whitespace', // or "code" or "whitespace"
schema: []
},
/**
* @param {RuleContext} context - The rule context.
* @returns {Object} AST event handlers.
*/
create (context) {
// ----------------------------------------------------------------------
// Public
// ----------------------------------------------------------------------
return {
Program (node) {
if (context.parserServices.getTemplateBodyTokenStore == null) {
context.report({
loc: { line: 1, column: 0 },
message: 'Use the latest vue-eslint-parser. See also https://github.com/vuejs/eslint-plugin-vue#what-is-the-use-the-latest-vue-eslint-parser-error.'
})
return
}
if (!node.templateBody) {
return
}
const sourceCode = context.getSourceCode()
const tokenStore = context.parserServices.getTemplateBodyTokenStore()
const tokens = tokenStore.getTokens(node.templateBody, { includeComments: true })
let prevToken = tokens.shift()
for (const token of tokens) {
const spaces = token.range[0] - prevToken.range[1]
if (spaces > 1 && token.loc.start.line === prevToken.loc.start.line) {
context.report({
node: token,
loc: {
start: prevToken.loc.end,
end: token.loc.start
},
message: "Multiple spaces found before '{{displayValue}}'.",
fix: (fixer) => fixer.replaceTextRange([prevToken.range[1], token.range[0]], ' '),
data: {
displayValue: sourceCode.getText(token)
}
})
}
prevToken = token
}
}
}
}
}
+105
View File
@@ -0,0 +1,105 @@
/**
* @author Toru Nagashima
* @copyright 2017 Toru Nagashima. All rights reserved.
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------------------
// https://html.spec.whatwg.org/multipage/parsing.html#parse-errors
const DEFAULT_OPTIONS = Object.freeze(Object.assign(Object.create(null), {
'abrupt-closing-of-empty-comment': true,
'absence-of-digits-in-numeric-character-reference': true,
'cdata-in-html-content': true,
'character-reference-outside-unicode-range': true,
'control-character-in-input-stream': true,
'control-character-reference': true,
'eof-before-tag-name': true,
'eof-in-cdata': true,
'eof-in-comment': true,
'eof-in-tag': true,
'incorrectly-closed-comment': true,
'incorrectly-opened-comment': true,
'invalid-first-character-of-tag-name': true,
'missing-attribute-value': true,
'missing-end-tag-name': true,
'missing-semicolon-after-character-reference': true,
'missing-whitespace-between-attributes': true,
'nested-comment': true,
'noncharacter-character-reference': true,
'noncharacter-in-input-stream': true,
'null-character-reference': true,
'surrogate-character-reference': true,
'surrogate-in-input-stream': true,
'unexpected-character-in-attribute-name': true,
'unexpected-character-in-unquoted-attribute-value': true,
'unexpected-equals-sign-before-attribute-name': true,
'unexpected-null-character': true,
'unexpected-question-mark-instead-of-tag-name': true,
'unexpected-solidus-in-tag': true,
'unknown-named-character-reference': true,
'end-tag-with-attributes': true,
'duplicate-attribute': true,
'end-tag-with-trailing-solidus': true,
'non-void-html-element-start-tag-with-trailing-solidus': false,
'x-invalid-end-tag': true,
'x-invalid-namespace': true
}))
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'disallow parsing errors in `<template>`',
category: 'essential',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/no-parsing-error.md'
},
fixable: null,
schema: [
{
type: 'object',
properties: Object.keys(DEFAULT_OPTIONS).reduce((ret, code) => {
ret[code] = { type: 'boolean' }
return ret
}, {}),
additionalProperties: false
}
]
},
create (context) {
const options = Object.assign({}, DEFAULT_OPTIONS, context.options[0] || {})
return {
Program (program) {
const node = program.templateBody
if (node == null || node.errors == null) {
return
}
for (const error of node.errors) {
if (error.code && !options[error.code]) {
continue
}
context.report({
node,
loc: { line: error.lineNumber, column: error.column },
message: 'Parsing error: {{message}}.',
data: {
message: error.message.endsWith('.')
? error.message.slice(0, -1)
: error.message
}
})
}
}
}
}
}
+72
View File
@@ -0,0 +1,72 @@
/**
* @fileoverview Prevent overwrite reserved keys
* @author Armano
*/
'use strict'
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const RESERVED_KEYS = require('../utils/vue-reserved.json')
const GROUP_NAMES = ['props', 'computed', 'data', 'methods']
module.exports = {
meta: {
docs: {
description: 'disallow overwriting reserved keys',
category: 'essential',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/no-reserved-keys.md'
},
fixable: null,
schema: [
{
type: 'object',
properties: {
reserved: {
type: 'array'
},
groups: {
type: 'array'
}
},
additionalProperties: false
}
]
},
create (context) {
const options = context.options[0] || {}
const reservedKeys = new Set(RESERVED_KEYS.concat(options.reserved || []))
const groups = new Set(GROUP_NAMES.concat(options.groups || []))
// ----------------------------------------------------------------------
// Public
// ----------------------------------------------------------------------
return utils.executeOnVue(context, (obj) => {
const properties = utils.iterateProperties(obj, groups)
for (const o of properties) {
if (o.groupName === 'data' && o.name[0] === '_') {
context.report({
node: o.node,
message: "Keys starting with with '_' are reserved in '{{name}}' group.",
data: {
name: o.name
}
})
} else if (reservedKeys.has(o.name)) {
context.report({
node: o.node,
message: "Key '{{name}}' is reserved.",
data: {
name: o.name
}
})
}
}
})
}
}
+84
View File
@@ -0,0 +1,84 @@
/**
* @fileoverview Enforces component's data property to be a function.
* @author Armano
*/
'use strict'
const utils = require('../utils')
function isOpenParen (token) {
return token.type === 'Punctuator' && token.value === '('
}
function isCloseParen (token) {
return token.type === 'Punctuator' && token.value === ')'
}
function getFirstAndLastTokens (node, sourceCode) {
let first = sourceCode.getFirstToken(node)
let last = sourceCode.getLastToken(node)
// If the value enclosed by parentheses, update the 'first' and 'last' by the parentheses.
while (true) {
const prev = sourceCode.getTokenBefore(first)
const next = sourceCode.getTokenAfter(last)
if (isOpenParen(prev) && isCloseParen(next)) {
first = prev
last = next
} else {
return { first, last }
}
}
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "enforce component's data property to be a function",
category: 'essential',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/no-shared-component-data.md'
},
fixable: 'code',
schema: []
},
create (context) {
const sourceCode = context.getSourceCode()
return utils.executeOnVueComponent(context, (obj) => {
obj.properties
.filter(p =>
p.type === 'Property' &&
p.key.type === 'Identifier' &&
p.key.name === 'data' &&
p.value.type !== 'FunctionExpression' &&
p.value.type !== 'ArrowFunctionExpression' &&
p.value.type !== 'Identifier'
)
.forEach(p => {
context.report({
node: p,
message: '`data` property in component must be a function.',
fix (fixer) {
const tokens = getFirstAndLastTokens(p.value, sourceCode)
// If we can upgrade requirements to `eslint@>4.1.0`, this code can be replaced by:
// return [
// fixer.insertTextBefore(tokens.first, 'function() {\nreturn '),
// fixer.insertTextAfter(tokens.last, ';\n}')
// ]
// See: https://eslint.org/blog/2017/06/eslint-v4.1.0-released#applying-multiple-autofixes-simultaneously
const range = [tokens.first.range[0], tokens.last.range[1]]
const valueText = sourceCode.text.slice(range[0], range[1])
const replacement = `function() {\nreturn ${valueText};\n}`
return fixer.replaceTextRange(range, replacement)
}
})
})
})
}
}
@@ -0,0 +1,73 @@
/**
* @fileoverview Don't introduce side effects in computed properties
* @author Michał Sajnóg
*/
'use strict'
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'disallow side effects in computed properties',
category: 'essential',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/no-side-effects-in-computed-properties.md'
},
fixable: null,
schema: []
},
create (context) {
const forbiddenNodes = []
return Object.assign({},
{
// this.xxx <=|+=|-=>
'AssignmentExpression' (node) {
if (node.left.type !== 'MemberExpression') return
if (utils.parseMemberExpression(node.left)[0] === 'this') {
forbiddenNodes.push(node)
}
},
// this.xxx <++|-->
'UpdateExpression > MemberExpression' (node) {
if (utils.parseMemberExpression(node)[0] === 'this') {
forbiddenNodes.push(node)
}
},
// this.xxx.func()
'CallExpression' (node) {
const code = utils.parseMemberOrCallExpression(node)
const MUTATION_REGEX = /(this.)((?!(concat|slice|map|filter)\().)[^\)]*((push|pop|shift|unshift|reverse|splice|sort|copyWithin|fill)\()/g
if (MUTATION_REGEX.test(code)) {
forbiddenNodes.push(node)
}
}
},
utils.executeOnVue(context, (obj) => {
const computedProperties = utils.getComputedProperties(obj)
computedProperties.forEach(cp => {
forbiddenNodes.forEach(node => {
if (
cp.value &&
node.loc.start.line >= cp.value.loc.start.line &&
node.loc.end.line <= cp.value.loc.end.line
) {
context.report({
node: node,
message: 'Unexpected side effect in "{{key}}" computed property.',
data: { key: cp.key }
})
}
})
})
})
)
}
}
+42
View File
@@ -0,0 +1,42 @@
/**
* @author Toru Nagashima
* @copyright 2016 Toru Nagashima. All rights reserved.
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'disallow `key` attribute on `<template>`',
category: 'essential',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/no-template-key.md'
},
fixable: null,
schema: []
},
create (context) {
return utils.defineTemplateBodyVisitor(context, {
"VElement[name='template']" (node) {
if (utils.hasAttribute(node, 'key') || utils.hasDirective(node, 'bind', 'key')) {
context.report({
node: node,
loc: node.loc,
message: "'<template>' cannot be keyed. Place the key on real elements instead."
})
}
}
})
}
}
+44
View File
@@ -0,0 +1,44 @@
/**
* @author Toru Nagashima
* @copyright 2017 Toru Nagashima. All rights reserved.
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'disallow mustaches in `<textarea>`',
category: 'essential',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/no-textarea-mustache.md'
},
fixable: null,
schema: []
},
create (context) {
return utils.defineTemplateBodyVisitor(context, {
"VElement[name='textarea'] VExpressionContainer" (node) {
if (node.parent.type !== 'VElement') {
return
}
context.report({
node,
loc: node.loc,
message: "Unexpected mustache. Use 'v-model' instead."
})
}
})
}
}
+45
View File
@@ -0,0 +1,45 @@
/**
* @fileoverview disallow unused variable definitions of v-for directives or scope attributes.
* @author 薛定谔的猫<hh_2013@foxmail.com>
*/
'use strict'
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'disallow unused variable definitions of v-for directives or scope attributes',
category: 'essential',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/no-unused-vars.md'
},
fixable: null,
schema: []
},
create (context) {
return utils.defineTemplateBodyVisitor(context, {
VElement (node) {
const variables = node.variables
for (
let i = variables.length - 1;
i >= 0 && !variables[i].references.length;
i--
) {
const variable = variables[i]
context.report({
node: variable.id,
loc: variable.id.loc,
message: `'{{name}}' is defined but never used.`,
data: variable.id
})
}
}
})
}
}
+99
View File
@@ -0,0 +1,99 @@
/**
* @author Yosuke Ota
*
* issue https://github.com/vuejs/eslint-plugin-vue/issues/403
* Style guide: https://vuejs.org/v2/style-guide/#Avoid-v-if-with-v-for-essential
*
* I implemented it with reference to `no-confusing-v-for-v-if`
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------------------
/**
* Check whether the given `v-if` node is using the variable which is defined by the `v-for` directive.
* @param {ASTNode} vIf The `v-if` attribute node to check.
* @returns {boolean} `true` if the `v-if` is using the variable which is defined by the `v-for` directive.
*/
function isUsingIterationVar (vIf) {
return !!getVForUsingIterationVar(vIf)
}
function getVForUsingIterationVar (vIf) {
const element = vIf.parent.parent
for (var i = 0; i < vIf.value.references.length; i++) {
const reference = vIf.value.references[i]
const targetVFor = element.variables.find(variable =>
variable.id.name === reference.id.name &&
variable.kind === 'v-for'
)
if (targetVFor) {
return targetVFor.id.parent
}
}
return undefined
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'disallow use v-if on the same element as v-for',
category: undefined,
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/no-use-v-if-with-v-for.md'
},
fixable: null,
schema: [{
type: 'object',
properties: {
allowUsingIterationVar: {
type: 'boolean'
}
}
}]
},
create (context) {
const options = context.options[0] || {}
const allowUsingIterationVar = options.allowUsingIterationVar === true // default false
return utils.defineTemplateBodyVisitor(context, {
"VAttribute[directive=true][key.name='if']" (node) {
const element = node.parent.parent
if (utils.hasDirective(element, 'for')) {
if (isUsingIterationVar(node)) {
if (!allowUsingIterationVar) {
const vFor = getVForUsingIterationVar(node)
context.report({
node,
loc: node.loc,
message: "The '{{iteratorName}}' variable inside 'v-for' directive should be replaced with a computed property that returns filtered array instead. You should not mix 'v-for' with 'v-if'.",
data: {
iteratorName: vFor.right.name
}
})
}
} else {
context.report({
node,
loc: node.loc,
message: "This 'v-if' should be moved to the wrapper element."
})
}
}
}
})
}
}
+33
View File
@@ -0,0 +1,33 @@
/**
* @fileoverview Restrict or warn use of v-html to prevent XSS attack
* @author Nathan Zeplowitz
*/
'use strict'
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definitionutilu
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'disallow use of v-html to prevent XSS attack',
category: undefined,
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/no-v-html.md'
},
fixable: null,
schema: []
},
create (context) {
return utils.defineTemplateBodyVisitor(context, {
"VAttribute[directive=true][key.name='html']" (node) {
context.report({
node,
loc: node.loc,
message: "'v-html' directive can lead to XSS attack."
})
}
})
}
}
+222
View File
@@ -0,0 +1,222 @@
/**
* @fileoverview Keep order of properties in components
* @author Michał Sajnóg
*/
'use strict'
const utils = require('../utils')
const Traverser = require('eslint/lib/util/traverser')
const defaultOrder = [
'el',
'name',
'parent',
'functional',
['delimiters', 'comments'],
['components', 'directives', 'filters'],
'extends',
'mixins',
'inheritAttrs',
'model',
['props', 'propsData'],
'data',
'computed',
'watch',
'LIFECYCLE_HOOKS',
'methods',
['template', 'render'],
'renderError'
]
const groups = {
LIFECYCLE_HOOKS: [
'beforeCreate',
'created',
'beforeMount',
'mounted',
'beforeUpdate',
'updated',
'activated',
'deactivated',
'beforeDestroy',
'destroyed'
]
}
function getOrderMap (order) {
const orderMap = new Map()
order.forEach((property, i) => {
if (Array.isArray(property)) {
property.forEach(p => orderMap.set(p, i))
} else {
orderMap.set(property, i)
}
})
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
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'enforce order of properties in components',
category: 'recommended',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/order-in-components.md'
},
fixable: 'code', // null or "code" or "whitespace"
schema: [
{
type: 'object',
properties: {
order: {
type: 'array'
}
},
additionalProperties: false
}
]
},
create (context) {
const options = context.options[0] || {}
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
.filter(property => property.type === 'Property')
.map(property => property.key)
properties.forEach((property, i) => {
const propertiesAbove = properties.slice(0, i)
const unorderedProperties = propertiesAbove
.filter(p => orderMap.get(p.name) > orderMap.get(property.name))
.sort((p1, p2) => orderMap.get(p1.name) > orderMap.get(p2.name))
const firstUnorderedProperty = unorderedProperties[0]
if (firstUnorderedProperty) {
const line = firstUnorderedProperty.loc.start.line
context.report({
node: property,
message: `The "{{name}}" property should be above the "{{firstUnorderedPropertyName}}" property on line {{line}}.`,
data: {
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)
}
})
}
})
}
return utils.executeOnVue(context, (obj) => {
checkOrder(obj.properties, orderMap)
})
}
}
+105
View File
@@ -0,0 +1,105 @@
/**
* @fileoverview Requires specific casing for the Prop name in Vue components
* @author Yu Kimura
*/
'use strict'
const utils = require('../utils')
const casing = require('../utils/casing')
const allowedCaseOptions = ['camelCase', 'snake_case']
function canFixPropertyName (node, originalName) {
// Can not fix of computed property names & shorthand
if (node.computed || node.shorthand) {
return false
}
const key = node.key
// Can not fix of unknown types
if (key.type !== 'Literal' && key.type !== 'Identifier') {
return false
}
// Can fix of ASCII printable characters
return originalName.match(/[ -~]+/)
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
function create (context) {
const options = context.options[0]
const caseType = allowedCaseOptions.indexOf(options) !== -1 ? options : 'camelCase'
const converter = casing.getConverter(caseType)
// ----------------------------------------------------------------------
// Public
// ----------------------------------------------------------------------
return utils.executeOnVue(context, (obj) => {
const node = obj.properties.find(p =>
p.type === 'Property' &&
p.key.type === 'Identifier' &&
p.key.name === 'props' &&
(p.value.type === 'ObjectExpression' || p.value.type === 'ArrayExpression')
)
if (!node) return
const items = node.value.type === 'ObjectExpression' ? node.value.properties : node.value.elements
for (const item of items) {
if (item.type !== 'Property') {
return
}
if (item.computed) {
if (item.key.type !== 'Literal') {
// TemplateLiteral | Identifier(variable) | Expression(s)
return
}
if (typeof item.key.value !== 'string') {
// (boolean | null | number | RegExp) Literal
return
}
}
const propName = item.key.type === 'Literal' ? item.key.value : item.key.name
const convertedName = converter(propName)
if (convertedName !== propName) {
context.report({
node: item,
message: 'Prop "{{name}}" is not in {{caseType}}.',
data: {
name: propName,
caseType: caseType
},
fix: canFixPropertyName(item, propName) ? fixer => {
return item.key.type === 'Literal'
? fixer.replaceText(item.key, item.key.raw.replace(item.key.value, convertedName))
: fixer.replaceText(item.key, convertedName)
} : undefined
})
}
}
})
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'enforce specific casing for the Prop name in Vue components',
category: undefined, // 'strongly-recommended'
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.5.0/docs/rules/prop-name-casing.md'
},
fixable: 'code', // null or "code" or "whitespace"
schema: [
{
enum: allowedCaseOptions
}
]
},
create
}
+42
View File
@@ -0,0 +1,42 @@
/**
* @author Toru Nagashima
* @copyright 2017 Toru Nagashima. All rights reserved.
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'require `v-bind:is` of `<component>` elements',
category: 'essential',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/require-component-is.md'
},
fixable: null,
schema: []
},
create (context) {
return utils.defineTemplateBodyVisitor(context, {
"VElement[name='component']" (node) {
if (!utils.hasDirective(node, 'bind', 'is')) {
context.report({
node,
loc: node.loc,
message: "Expected '<component>' elements to have 'v-bind:is' attribute."
})
}
}
})
}
}
+106
View File
@@ -0,0 +1,106 @@
/**
* @fileoverview Require default value for props
* @author Michał Sajnóg <msajnog93@gmail.com> (http://github.com/michalsnik)
*/
'use strict'
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'require default value for props',
category: 'strongly-recommended',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/require-default-prop.md'
},
fixable: null, // or "code" or "whitespace"
schema: []
},
create: function (context) {
// ----------------------------------------------------------------------
// Helpers
// ----------------------------------------------------------------------
/**
* Checks if the passed prop is required
* @param {Property} prop - Property AST node for a single prop
* @return {boolean}
*/
function propIsRequired (prop) {
const propRequiredNode = prop.value.properties
.find(p =>
p.type === 'Property' &&
p.key.name === 'required' &&
p.value.type === 'Literal' &&
p.value.value === true
)
return Boolean(propRequiredNode)
}
/**
* Checks if the passed prop has a default value
* @param {Property} prop - Property AST node for a single prop
* @return {boolean}
*/
function propHasDefault (prop) {
const propDefaultNode = prop.value.properties
.find(p =>
p.key &&
(p.key.name === 'default' || p.key.value === 'default')
)
return Boolean(propDefaultNode)
}
/**
* Finds all props that don't have a default value set
* @param {Property} propsNode - Vue component's "props" node
* @return {boolean}
*/
function findPropsWithoutDefaultValue (propsNode) {
return propsNode.value.properties
.filter(prop => prop.type === 'Property')
.filter(prop => {
if (prop.value.type !== 'ObjectExpression') {
return true
}
return !propIsRequired(prop) && !propHasDefault(prop)
})
}
// ----------------------------------------------------------------------
// Public
// ----------------------------------------------------------------------
return utils.executeOnVue(context, (obj) => {
const propsNode = obj.properties
.find(p =>
p.type === 'Property' &&
p.key.type === 'Identifier' &&
p.key.name === 'props' &&
p.value.type === 'ObjectExpression'
)
if (!propsNode) return
const propsWithoutDefault = findPropsWithoutDefaultValue(propsNode)
propsWithoutDefault.forEach(prop => {
context.report({
node: prop,
message: `Prop '{{propName}}' requires default value to be set.`,
data: {
propName: prop.key.name
}
})
})
})
}
}
+96
View File
@@ -0,0 +1,96 @@
/**
* @fileoverview Prop definitions should be detailed
* @author Armano
*/
'use strict'
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'require type definitions in props',
category: 'strongly-recommended',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/require-prop-types.md'
},
fixable: null, // or "code" or "whitespace"
schema: [
// fill in your schema
]
},
create (context) {
// ----------------------------------------------------------------------
// Helpers
// ----------------------------------------------------------------------
function objectHasType (node) {
const typeProperty = node.properties
.find(p =>
utils.getStaticPropertyName(p.key) === 'type' &&
(
p.value.type !== 'ArrayExpression' ||
p.value.elements.length > 0
)
)
const validatorProperty = node.properties
.find(p => utils.getStaticPropertyName(p.key) === 'validator')
return Boolean(typeProperty || validatorProperty)
}
function checkProperties (items) {
for (const cp of items) {
if (cp.type !== 'Property') {
return
}
let hasType = true
if (cp.value.type === 'ObjectExpression') { // foo: {
hasType = objectHasType(cp.value)
} else if (cp.value.type === 'ArrayExpression') { // foo: [
hasType = cp.value.elements.length > 0
} else if (cp.value.type === 'FunctionExpression' || cp.value.type === 'ArrowFunctionExpression') {
hasType = false
}
if (!hasType) {
context.report({
node: cp,
message: 'Prop "{{name}}" should define at least its type.',
data: {
name: cp.key.name
}
})
}
}
}
// ----------------------------------------------------------------------
// Public
// ----------------------------------------------------------------------
return utils.executeOnVue(context, (obj) => {
const node = obj.properties
.find(p =>
p.type === 'Property' &&
p.key.type === 'Identifier' &&
p.key.name === 'props'
)
if (!node) return
if (node.value.type === 'ObjectExpression') {
checkProperties(node.value.properties)
}
if (node.value.type === 'ArrayExpression') {
context.report({
node,
message: 'Props should at least define their types.'
})
}
})
}
}
+53
View File
@@ -0,0 +1,53 @@
/**
* @fileoverview Enforces render function to always return value.
* @author Armano
*/
'use strict'
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'enforce render function to always return value',
category: 'essential',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/require-render-return.md'
},
fixable: null, // or "code" or "whitespace"
schema: []
},
create (context) {
const forbiddenNodes = []
// ----------------------------------------------------------------------
// Public
// ----------------------------------------------------------------------
return Object.assign({},
utils.executeOnFunctionsWithoutReturn(true, node => {
forbiddenNodes.push(node)
}),
utils.executeOnVue(context, obj => {
const node = obj.properties.find(item => item.type === 'Property' &&
utils.getStaticPropertyName(item) === 'render' &&
(item.value.type === 'ArrowFunctionExpression' || item.value.type === 'FunctionExpression')
)
if (!node) return
forbiddenNodes.forEach(el => {
if (node.value === el) {
context.report({
node: node.key,
message: 'Expected to return a value in render function.'
})
}
})
})
)
}
}
+56
View File
@@ -0,0 +1,56 @@
/**
* @author Toru Nagashima
* @copyright 2017 Toru Nagashima. All rights reserved.
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'require `v-bind:key` with `v-for` directives',
category: 'essential',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/require-v-for-key.md'
},
fixable: null,
schema: []
},
create (context) {
/**
* Check the given element about `v-bind:key` attributes.
* @param {ASTNode} element The element node to check.
*/
function checkKey (element) {
if (element.name === 'template' || element.name === 'slot') {
for (const child of element.children) {
if (child.type === 'VElement') {
checkKey(child)
}
}
} else if (!utils.isCustomComponent(element) && !utils.hasDirective(element, 'bind', 'key')) {
context.report({
node: element.startTag,
loc: element.startTag.loc,
message: "Elements in iteration expect to have 'v-bind:key' directives."
})
}
}
return utils.defineTemplateBodyVisitor(context, {
"VAttribute[directive=true][key.name='for']" (node) {
checkKey(node.parent.parent)
}
})
}
}
+131
View File
@@ -0,0 +1,131 @@
/**
* @fileoverview Enforces props default values to be valid.
* @author Armano
*/
'use strict'
const utils = require('../utils')
const NATIVE_TYPES = new Set([
'String',
'Number',
'Boolean',
'Function',
'Object',
'Array',
'Symbol'
])
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'enforce props default values to be valid',
category: 'essential',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/require-valid-default-prop.md'
},
fixable: null,
schema: []
},
create (context) {
// ----------------------------------------------------------------------
// Helpers
// ----------------------------------------------------------------------
function isPropertyIdentifier (node) {
return node.type === 'Property' && node.key.type === 'Identifier'
}
function getPropertyNode (obj, name) {
return obj.properties.find(p =>
isPropertyIdentifier(p) &&
p.key.name === name
)
}
function getTypes (node) {
if (node.type === 'Identifier') {
return [node.name]
} else if (node.type === 'ArrayExpression') {
return node.elements
.filter(item => item.type === 'Identifier')
.map(item => item.name)
}
return []
}
function ucFirst (text) {
return text[0].toUpperCase() + text.slice(1)
}
function getValueType (node) {
if (node.type === 'CallExpression') { // Symbol(), Number() ...
if (node.callee.type === 'Identifier' && NATIVE_TYPES.has(node.callee.name)) {
return node.callee.name
}
} else if (node.type === 'TemplateLiteral') { // String
return 'String'
} else if (node.type === 'Literal') { // String, Boolean, Number
if (node.value === null) return null
const type = ucFirst(typeof node.value)
if (NATIVE_TYPES.has(type)) {
return type
}
} else if (node.type === 'ArrayExpression') { // Array
return 'Array'
} else if (node.type === 'ObjectExpression') { // Object
return 'Object'
}
// FunctionExpression, ArrowFunctionExpression
return null
}
// ----------------------------------------------------------------------
// Public
// ----------------------------------------------------------------------
return utils.executeOnVue(context, obj => {
const props = obj.properties.find(p =>
isPropertyIdentifier(p) &&
p.key.name === 'props' &&
p.value.type === 'ObjectExpression'
)
if (!props) return
const properties = props.value.properties.filter(p =>
isPropertyIdentifier(p) &&
p.value.type === 'ObjectExpression'
)
for (const prop of properties) {
const type = getPropertyNode(prop.value, 'type')
if (!type) continue
const typeNames = new Set(getTypes(type.value)
.map(item => item === 'Object' || item === 'Array' ? 'Function' : item) // Object and Array require function
.filter(item => NATIVE_TYPES.has(item)))
// There is no native types detected
if (typeNames.size === 0) continue
const def = getPropertyNode(prop.value, 'default')
if (!def) continue
const defType = getValueType(def.value)
if (!defType || typeNames.has(defType)) continue
context.report({
node: def,
message: "Type of the default value for '{{name}}' prop must be a {{types}}.",
data: {
name: prop.key.name,
types: Array.from(typeNames).join(' or ').toLowerCase()
}
})
}
})
}
}
@@ -0,0 +1,67 @@
/**
* @fileoverview Enforces that a return statement is present in computed property (return-in-computed-property)
* @author Armano
*/
'use strict'
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'enforce that a return statement is present in computed property',
category: 'essential',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/return-in-computed-property.md'
},
fixable: null, // or "code" or "whitespace"
schema: [
{
type: 'object',
properties: {
treatUndefinedAsUnspecified: {
type: 'boolean'
}
},
additionalProperties: false
}
]
},
create (context) {
const options = context.options[0] || {}
const treatUndefinedAsUnspecified = !(options.treatUndefinedAsUnspecified === false)
const forbiddenNodes = []
// ----------------------------------------------------------------------
// Public
// ----------------------------------------------------------------------
return Object.assign({},
utils.executeOnFunctionsWithoutReturn(treatUndefinedAsUnspecified, node => {
forbiddenNodes.push(node)
}),
utils.executeOnVue(context, properties => {
const computedProperties = utils.getComputedProperties(properties)
computedProperties.forEach(cp => {
forbiddenNodes.forEach(el => {
if (cp.value && cp.value.parent === el) {
context.report({
node: el,
message: 'Expected to return a value in "{{name}}" computed property.',
data: {
name: cp.key
}
})
}
})
})
})
)
}
}
+57
View File
@@ -0,0 +1,57 @@
/**
* @author Toru Nagashima
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const indentCommon = require('../utils/indent-common')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'enforce consistent indentation in `<script>`',
category: undefined,
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/script-indent.md'
},
fixable: 'whitespace',
schema: [
{
anyOf: [
{ type: 'integer', minimum: 1 },
{ enum: ['tab'] }
]
},
{
type: 'object',
properties: {
'baseIndent': { type: 'integer', minimum: 0 },
'switchCase': { type: 'integer', minimum: 0 },
'ignores': {
type: 'array',
items: {
allOf: [
{ type: 'string' },
{ not: { type: 'string', pattern: ':exit$' }},
{ not: { type: 'string', pattern: '^\\s*$' }}
]
},
uniqueItems: true,
additionalItems: false
}
},
additionalProperties: false
}
]
},
create (context) {
return indentCommon.defineVisitor(context, context.getSourceCode(), {})
}
}
+101
View File
@@ -0,0 +1,101 @@
/**
* @fileoverview enforce usage of `this` in template.
* @author Armano
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
const RESERVED_NAMES = new Set(require('../utils/js-reserved.json'))
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'enforce usage of `this` in template',
category: 'recommended',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/this-in-template.md'
},
fixable: null,
schema: [
{
enum: ['always', 'never']
}
]
},
/**
* Creates AST event handlers for this-in-template.
*
* @param {RuleContext} context - The rule context.
* @returns {Object} AST event handlers.
*/
create (context) {
const options = context.options[0] !== 'always' ? 'never' : 'always'
let scope = {
parent: null,
nodes: []
}
return utils.defineTemplateBodyVisitor(context, Object.assign({
VElement (node) {
scope = {
parent: scope,
nodes: scope.nodes.slice() // make copy
}
if (node.variables) {
for (const variable of node.variables) {
const varNode = variable.id
const name = varNode.name
if (!scope.nodes.some(node => node.name === name)) { // Prevent adding duplicates
scope.nodes.push(varNode)
}
}
}
},
'VElement:exit' (node) {
scope = scope.parent
}
}, options === 'never'
? {
'VExpressionContainer MemberExpression > ThisExpression' (node) {
const propertyName = utils.getStaticPropertyName(node.parent.property)
if (!propertyName ||
scope.nodes.some(el => el.name === propertyName) ||
RESERVED_NAMES.has(propertyName) || // this.class | this['class']
/^[0-9].*$|[^a-zA-Z0-9_]/.test(propertyName) // this['0aaaa'] | this['foo-bar bas']
) {
return
}
context.report({
node,
loc: node.loc,
message: "Unexpected usage of 'this'."
})
}
}
: {
'VExpressionContainer' (node) {
if (node.references) {
for (const reference of node.references) {
if (!scope.nodes.some(el => el.name === reference.id.name)) {
context.report({
node: reference.id,
loc: reference.id.loc,
message: "Expected 'this'."
})
}
}
}
}
}
))
}
}
+53
View File
@@ -0,0 +1,53 @@
/**
* @author Toru Nagashima
* @copyright 2017 Toru Nagashima. All rights reserved.
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'enforce `v-bind` directive style',
category: 'strongly-recommended',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/v-bind-style.md'
},
fixable: 'code',
schema: [
{ enum: ['shorthand', 'longform'] }
]
},
create (context) {
const shorthand = context.options[0] !== 'longform'
return utils.defineTemplateBodyVisitor(context, {
"VAttribute[directive=true][key.name='bind'][key.argument!=null]" (node) {
if (node.key.shorthand === shorthand) {
return
}
context.report({
node,
loc: node.loc,
message: shorthand
? "Unexpected 'v-bind' before ':'."
: "Expected 'v-bind' before ':'.",
fix: (fixer) => shorthand
? fixer.removeRange([node.range[0], node.range[0] + 6])
: fixer.insertTextBefore(node, 'v-bind')
})
}
})
}
}
+54
View File
@@ -0,0 +1,54 @@
/**
* @author Toru Nagashima
* @copyright 2017 Toru Nagashima. All rights reserved.
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'enforce `v-on` directive style',
category: 'strongly-recommended',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/v-on-style.md'
},
fixable: 'code',
schema: [
{ enum: ['shorthand', 'longform'] }
]
},
create (context) {
const shorthand = context.options[0] !== 'longform'
return utils.defineTemplateBodyVisitor(context, {
"VAttribute[directive=true][key.name='on'][key.argument!=null]" (node) {
if (node.key.shorthand === shorthand) {
return
}
const pos = node.range[0]
context.report({
node,
loc: node.loc,
message: shorthand
? "Expected '@' instead of 'v-on:'."
: "Expected 'v-on:' instead of '@'.",
fix: (fixer) => shorthand
? fixer.replaceTextRange([pos, pos + 5], '@')
: fixer.replaceTextRange([pos, pos + 1], 'v-on:')
})
}
})
}
}
+111
View File
@@ -0,0 +1,111 @@
/**
* @author Toru Nagashima
* @copyright 2017 Toru Nagashima. All rights reserved.
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'enforce valid template root',
category: 'essential',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/valid-template-root.md'
},
fixable: null,
schema: []
},
create (context) {
const sourceCode = context.getSourceCode()
return {
Program (program) {
const element = program.templateBody
if (element == null) {
return
}
const hasSrc = utils.hasAttribute(element, 'src')
const rootElements = []
let extraText = null
let extraElement = null
let vIf = false
for (const child of element.children) {
if (child.type === 'VElement') {
if (rootElements.length === 0 && !hasSrc) {
rootElements.push(child)
vIf = utils.hasDirective(child, 'if')
} else if (vIf && utils.hasDirective(child, 'else-if')) {
rootElements.push(child)
} else if (vIf && utils.hasDirective(child, 'else')) {
rootElements.push(child)
vIf = false
} else {
extraElement = child
}
} else if (sourceCode.getText(child).trim() !== '') {
extraText = child
}
}
if (hasSrc && (extraText != null || extraElement != null)) {
context.report({
node: extraText || extraElement,
loc: (extraText || extraElement).loc,
message: "The template root with 'src' attribute is required to be empty."
})
} else if (extraText != null) {
context.report({
node: extraText,
loc: extraText.loc,
message: 'The template root requires an element rather than texts.'
})
} else if (extraElement != null) {
context.report({
node: extraElement,
loc: extraElement.loc,
message: 'The template root requires exactly one element.'
})
} else if (rootElements.length === 0 && !hasSrc) {
context.report({
node: element,
loc: element.loc,
message: 'The template root requires exactly one element.'
})
} else {
for (const element of rootElements) {
const tag = element.startTag
const name = element.name
if (name === 'template' || name === 'slot') {
context.report({
node: tag,
loc: tag.loc,
message: "The template root disallows '<{{name}}>' elements.",
data: { name }
})
}
if (utils.hasDirective(element, 'for')) {
context.report({
node: tag,
loc: tag.loc,
message: "The template root disallows 'v-for' directives."
})
}
}
}
}
}
}
}
+59
View File
@@ -0,0 +1,59 @@
/**
* @author Toru Nagashima
* @copyright 2017 Toru Nagashima. All rights reserved.
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------------------
const VALID_MODIFIERS = new Set(['prop', 'camel', 'sync'])
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'enforce valid `v-bind` directives',
category: 'essential',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/valid-v-bind.md'
},
fixable: null,
schema: []
},
create (context) {
return utils.defineTemplateBodyVisitor(context, {
"VAttribute[directive=true][key.name='bind']" (node) {
for (const modifier of node.key.modifiers) {
if (!VALID_MODIFIERS.has(modifier)) {
context.report({
node,
loc: node.key.loc,
message: "'v-bind' directives don't support the modifier '{{name}}'.",
data: { name: modifier }
})
}
}
if (!utils.hasAttributeValue(node)) {
context.report({
node,
loc: node.loc,
message: "'v-bind' directives require an attribute value."
})
}
}
})
}
}
+56
View File
@@ -0,0 +1,56 @@
/**
* @author Toru Nagashima
* @copyright 2017 Toru Nagashima. All rights reserved.
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'enforce valid `v-cloak` directives',
category: 'essential',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/valid-v-cloak.md'
},
fixable: null,
schema: []
},
create (context) {
return utils.defineTemplateBodyVisitor(context, {
"VAttribute[directive=true][key.name='cloak']" (node) {
if (node.key.argument) {
context.report({
node,
loc: node.loc,
message: "'v-cloak' directives require no argument."
})
}
if (node.key.modifiers.length > 0) {
context.report({
node,
loc: node.loc,
message: "'v-cloak' directives require no modifier."
})
}
if (node.value) {
context.report({
node,
loc: node.loc,
message: "'v-cloak' directives require no attribute value."
})
}
}
})
}
}
+79
View File
@@ -0,0 +1,79 @@
/**
* @author Toru Nagashima
* @copyright 2017 Toru Nagashima. All rights reserved.
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'enforce valid `v-else-if` directives',
category: 'essential',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/valid-v-else-if.md'
},
fixable: null,
schema: []
},
create (context) {
return utils.defineTemplateBodyVisitor(context, {
"VAttribute[directive=true][key.name='else-if']" (node) {
const element = node.parent.parent
if (!utils.prevElementHasIf(element)) {
context.report({
node,
loc: node.loc,
message: "'v-else-if' directives require being preceded by the element which has a 'v-if' or 'v-else-if' directive."
})
}
if (utils.hasDirective(element, 'if')) {
context.report({
node,
loc: node.loc,
message: "'v-else-if' and 'v-if' directives can't exist on the same element."
})
}
if (utils.hasDirective(element, 'else')) {
context.report({
node,
loc: node.loc,
message: "'v-else-if' and 'v-else' directives can't exist on the same element."
})
}
if (node.key.argument) {
context.report({
node,
loc: node.loc,
message: "'v-else-if' directives require no argument."
})
}
if (node.key.modifiers.length > 0) {
context.report({
node,
loc: node.loc,
message: "'v-else-if' directives require no modifier."
})
}
if (!utils.hasAttributeValue(node)) {
context.report({
node,
loc: node.loc,
message: "'v-else-if' directives require that attribute value."
})
}
}
})
}
}
+79
View File
@@ -0,0 +1,79 @@
/**
* @author Toru Nagashima
* @copyright 2017 Toru Nagashima. All rights reserved.
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'enforce valid `v-else` directives',
category: 'essential',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/valid-v-else.md'
},
fixable: null,
schema: []
},
create (context) {
return utils.defineTemplateBodyVisitor(context, {
"VAttribute[directive=true][key.name='else']" (node) {
const element = node.parent.parent
if (!utils.prevElementHasIf(element)) {
context.report({
node,
loc: node.loc,
message: "'v-else' directives require being preceded by the element which has a 'v-if' or 'v-else' directive."
})
}
if (utils.hasDirective(element, 'if')) {
context.report({
node,
loc: node.loc,
message: "'v-else' and 'v-if' directives can't exist on the same element. You may want 'v-else-if' directives."
})
}
if (utils.hasDirective(element, 'else-if')) {
context.report({
node,
loc: node.loc,
message: "'v-else' and 'v-else-if' directives can't exist on the same element."
})
}
if (node.key.argument) {
context.report({
node,
loc: node.loc,
message: "'v-else' directives require no argument."
})
}
if (node.key.modifiers.length > 0) {
context.report({
node,
loc: node.loc,
message: "'v-else' directives require no modifier."
})
}
if (utils.hasAttributeValue(node)) {
context.report({
node,
loc: node.loc,
message: "'v-else' directives require no attribute value."
})
}
}
})
}
}
+191
View File
@@ -0,0 +1,191 @@
/**
* @author Toru Nagashima
* @copyright 2017 Toru Nagashima. All rights reserved.
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------------------
/**
* Check whether the given attribute is using the variables which are defined by `v-for` directives.
* @param {ASTNode} vFor The attribute node of `v-for` to check.
* @param {ASTNode} vBindKey The attribute node of `v-bind:key` to check.
* @returns {boolean} `true` if the node is using the variables which are defined by `v-for` directives.
*/
function isUsingIterationVar (vFor, vBindKey) {
if (vBindKey.value == null) {
return false
}
const references = vBindKey.value.references
const variables = vFor.parent.parent.variables
return references.some(reference =>
variables.some(variable =>
variable.id.name === reference.id.name &&
variable.kind === 'v-for'
)
)
}
/**
* Check the child element in tempalte v-for about `v-bind:key` attributes.
* @param {RuleContext} context The rule context to report.
* @param {ASTNode} vFor The attribute node of `v-for` to check.
* @param {ASTNode} child The child node to check.
*/
function checkChildKey (context, vFor, child) {
const childFor = utils.getDirective(child, 'for')
// if child has v-for, check if parent iterator is used in v-for
if (childFor != null) {
const childForRefs = childFor.value.references
const variables = vFor.parent.parent.variables
const usedInFor = childForRefs.some(cref =>
variables.some(variable =>
cref.id.name === variable.id.name &&
variable.kind === 'v-for'
)
)
// if parent iterator is used, skip other checks
// iterator usage will be checked later by child v-for
if (usedInFor) {
return
}
}
// otherwise, check if parent iterator is directly used in child's key
checkKey(context, vFor, child)
}
/**
* Check the given element about `v-bind:key` attributes.
* @param {RuleContext} context The rule context to report.
* @param {ASTNode} vFor The attribute node of `v-for` to check.
* @param {ASTNode} element The element node to check.
*/
function checkKey (context, vFor, element) {
if (element.name === 'template') {
for (const child of element.children) {
if (child.type === 'VElement') {
checkChildKey(context, vFor, child)
}
}
return
}
const vBindKey = utils.getDirective(element, 'bind', 'key')
if (utils.isCustomComponent(element) && vBindKey == null) {
context.report({
node: element.startTag,
loc: element.startTag.loc,
message: "Custom elements in iteration require 'v-bind:key' directives."
})
}
if (vBindKey != null && !isUsingIterationVar(vFor, vBindKey)) {
context.report({
node: vBindKey,
loc: vBindKey.loc,
message: "Expected 'v-bind:key' directive to use the variables which are defined by the 'v-for' directive."
})
}
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'enforce valid `v-for` directives',
category: 'essential',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/valid-v-for.md'
},
fixable: null,
schema: []
},
create (context) {
const sourceCode = context.getSourceCode()
return utils.defineTemplateBodyVisitor(context, {
"VAttribute[directive=true][key.name='for']" (node) {
const element = node.parent.parent
checkKey(context, node, element)
if (node.key.argument) {
context.report({
node,
loc: node.loc,
message: "'v-for' directives require no argument."
})
}
if (node.key.modifiers.length > 0) {
context.report({
node,
loc: node.loc,
message: "'v-for' directives require no modifier."
})
}
if (!utils.hasAttributeValue(node)) {
context.report({
node,
loc: node.loc,
message: "'v-for' directives require that attribute value."
})
return
}
const expr = node.value.expression
if (expr == null) {
return
}
if (expr.type !== 'VForExpression') {
context.report({
node: node.value,
loc: node.value.loc,
message: "'v-for' directives require the special syntax '<alias> in <expression>'."
})
return
}
const lhs = expr.left
const value = lhs[0]
const key = lhs[1]
const index = lhs[2]
if (value === null) {
context.report({
node: value || expr,
loc: value && value.loc,
message: "Invalid alias ''."
})
}
if (key !== undefined && (!key || key.type !== 'Identifier')) {
context.report({
node: key || expr,
loc: key && key.loc,
message: "Invalid alias '{{text}}'.",
data: { text: key ? sourceCode.getText(key) : '' }
})
}
if (index !== undefined && (!index || index.type !== 'Identifier')) {
context.report({
node: index || expr,
loc: index && index.loc,
message: "Invalid alias '{{text}}'.",
data: { text: index ? sourceCode.getText(index) : '' }
})
}
}
})
}
}
+56
View File
@@ -0,0 +1,56 @@
/**
* @author Toru Nagashima
* @copyright 2017 Toru Nagashima. All rights reserved.
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'enforce valid `v-html` directives',
category: 'essential',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/valid-v-html.md'
},
fixable: null,
schema: []
},
create (context) {
return utils.defineTemplateBodyVisitor(context, {
"VAttribute[directive=true][key.name='html']" (node) {
if (node.key.argument) {
context.report({
node,
loc: node.loc,
message: "'v-html' directives require no argument."
})
}
if (node.key.modifiers.length > 0) {
context.report({
node,
loc: node.loc,
message: "'v-html' directives require no modifier."
})
}
if (!utils.hasAttributeValue(node)) {
context.report({
node,
loc: node.loc,
message: "'v-html' directives require that attribute value."
})
}
}
})
}
}
+72
View File
@@ -0,0 +1,72 @@
/**
* @author Toru Nagashima
* @copyright 2017 Toru Nagashima. All rights reserved.
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'enforce valid `v-if` directives',
category: 'essential',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/valid-v-if.md'
},
fixable: null,
schema: []
},
create (context) {
return utils.defineTemplateBodyVisitor(context, {
"VAttribute[directive=true][key.name='if']" (node) {
const element = node.parent.parent
if (utils.hasDirective(element, 'else')) {
context.report({
node,
loc: node.loc,
message: "'v-if' and 'v-else' directives can't exist on the same element. You may want 'v-else-if' directives."
})
}
if (utils.hasDirective(element, 'else-if')) {
context.report({
node,
loc: node.loc,
message: "'v-if' and 'v-else-if' directives can't exist on the same element."
})
}
if (node.key.argument) {
context.report({
node,
loc: node.loc,
message: "'v-if' directives require no argument."
})
}
if (node.key.modifiers.length > 0) {
context.report({
node,
loc: node.loc,
message: "'v-if' directives require no modifier."
})
}
if (!utils.hasAttributeValue(node)) {
context.report({
node,
loc: node.loc,
message: "'v-if' directives require that attribute value."
})
}
}
})
}
}
+167
View File
@@ -0,0 +1,167 @@
/**
* @author Toru Nagashima
* @copyright 2017 Toru Nagashima. All rights reserved.
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------------------
const VALID_MODIFIERS = new Set(['lazy', 'number', 'trim'])
/**
* Check whether the given node is valid or not.
* @param {ASTNode} node The element node to check.
* @returns {boolean} `true` if the node is valid.
*/
function isValidElement (node) {
const name = node.name
return (
name === 'input' ||
name === 'select' ||
name === 'textarea' ||
(
name !== 'keep-alive' &&
name !== 'slot' &&
name !== 'transition' &&
name !== 'transition-group' &&
utils.isCustomComponent(node)
)
)
}
/**
* Check whether the given node can be LHS.
* @param {ASTNode} node The node to check.
* @returns {boolean} `true` if the node can be LHS.
*/
function isLhs (node) {
return node != null && (
node.type === 'Identifier' ||
node.type === 'MemberExpression'
)
}
/**
* Get the variable by names.
* @param {string} name The variable name to find.
* @param {ASTNode} leafNode The node to look up.
* @returns {Variable|null} The found variable or null.
*/
function getVariable (name, leafNode) {
let node = leafNode
while (node != null) {
const variables = node.variables
const variable = variables && variables.find(v => v.id.name === name)
if (variable != null) {
return variable
}
node = node.parent
}
return null
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'enforce valid `v-model` directives',
category: 'essential',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/valid-v-model.md'
},
fixable: null,
schema: []
},
create (context) {
return utils.defineTemplateBodyVisitor(context, {
"VAttribute[directive=true][key.name='model']" (node) {
const element = node.parent.parent
const name = element.name
if (!isValidElement(element)) {
context.report({
node,
loc: node.loc,
message: "'v-model' directives aren't supported on <{{name}}> elements.",
data: { name }
})
}
if (name === 'input' && utils.hasAttribute(element, 'type', 'file')) {
context.report({
node,
loc: node.loc,
message: "'v-model' directives don't support 'file' input type."
})
}
if (node.key.argument) {
context.report({
node,
loc: node.loc,
message: "'v-model' directives require no argument."
})
}
for (const modifier of node.key.modifiers) {
if (!VALID_MODIFIERS.has(modifier)) {
context.report({
node,
loc: node.loc,
message: "'v-model' directives don't support the modifier '{{name}}'.",
data: { name: modifier }
})
}
}
if (!utils.hasAttributeValue(node)) {
context.report({
node,
loc: node.loc,
message: "'v-model' directives require that attribute value."
})
}
if (node.value) {
if (!isLhs(node.value.expression)) {
context.report({
node,
loc: node.loc,
message: "'v-model' directives require the attribute value which is valid as LHS."
})
}
for (const reference of node.value.references) {
const id = reference.id
if (id.parent.type === 'MemberExpression' || id.parent.type === 'BinaryExpression') {
continue
}
const variable = getVariable(id.name, element)
if (variable != null) {
context.report({
node,
loc: node.loc,
message: "'v-model' directives cannot update the iteration variable 'x' itself."
})
}
}
}
}
})
}
}
+147
View File
@@ -0,0 +1,147 @@
/**
* @author Toru Nagashima
* @copyright 2017 Toru Nagashima. All rights reserved.
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------------------
const VALID_MODIFIERS = new Set([
'stop', 'prevent', 'capture', 'self', 'ctrl', 'shift', 'alt', 'meta',
'native', 'once', 'left', 'right', 'middle', 'passive', 'esc', 'tab',
'enter', 'space', 'up', 'left', 'right', 'down', 'delete', 'exact'
])
const VERB_MODIFIERS = new Set([
'stop', 'prevent'
])
// https://www.w3.org/TR/uievents-key/
const KEY_ALIASES = new Set([
'unidentified', 'alt', 'alt-graph', 'caps-lock', 'control', 'fn', 'fn-lock',
'meta', 'num-lock', 'scroll-lock', 'shift', 'symbol', 'symbol-lock', 'hyper',
'super', 'enter', 'tab', 'arrow-down', 'arrow-left', 'arrow-right',
'arrow-up', 'end', 'home', 'page-down', 'page-up', 'backspace', 'clear',
'copy', 'cr-sel', 'cut', 'delete', 'erase-eof', 'ex-sel', 'insert', 'paste',
'redo', 'undo', 'accept', 'again', 'attn', 'cancel', 'context-menu', 'escape',
'execute', 'find', 'help', 'pause', 'select', 'zoom-in', 'zoom-out',
'brightness-down', 'brightness-up', 'eject', 'log-off', 'power',
'print-screen', 'hibernate', 'standby', 'wake-up', 'all-candidates',
'alphanumeric', 'code-input', 'compose', 'convert', 'dead', 'final-mode',
'group-first', 'group-last', 'group-next', 'group-previous', 'mode-change',
'next-candidate', 'non-convert', 'previous-candidate', 'process',
'single-candidate', 'hangul-mode', 'hanja-mode', 'junja-mode', 'eisu',
'hankaku', 'hiragana', 'hiragana-katakana', 'kana-mode', 'kanji-mode',
'katakana', 'romaji', 'zenkaku', 'zenkaku-hankaku', 'f1', 'f2', 'f3', 'f4',
'f5', 'f6', 'f7', 'f8', 'f9', 'f10', 'f11', 'f12', 'soft1', 'soft2', 'soft3',
'soft4', 'channel-down', 'channel-up', 'close', 'mail-forward', 'mail-reply',
'mail-send', 'media-close', 'media-fast-forward', 'media-pause',
'media-play-pause', 'media-record', 'media-rewind', 'media-stop',
'media-track-next', 'media-track-previous', 'new', 'open', 'print', 'save',
'spell-check', 'key11', 'key12', 'audio-balance-left', 'audio-balance-right',
'audio-bass-boost-down', 'audio-bass-boost-toggle', 'audio-bass-boost-up',
'audio-fader-front', 'audio-fader-rear', 'audio-surround-mode-next',
'audio-treble-down', 'audio-treble-up', 'audio-volume-down',
'audio-volume-up', 'audio-volume-mute', 'microphone-toggle',
'microphone-volume-down', 'microphone-volume-up', 'microphone-volume-mute',
'speech-correction-list', 'speech-input-toggle', 'launch-application1',
'launch-application2', 'launch-calendar', 'launch-contacts', 'launch-mail',
'launch-media-player', 'launch-music-player', 'launch-phone',
'launch-screen-saver', 'launch-spreadsheet', 'launch-web-browser',
'launch-web-cam', 'launch-word-processor', 'browser-back',
'browser-favorites', 'browser-forward', 'browser-home', 'browser-refresh',
'browser-search', 'browser-stop', 'app-switch', 'call', 'camera',
'camera-focus', 'end-call', 'go-back', 'go-home', 'headset-hook',
'last-number-redial', 'notification', 'manner-mode', 'voice-dial', 't-v',
't-v3-d-mode', 't-v-antenna-cable', 't-v-audio-description',
't-v-audio-description-mix-down', 't-v-audio-description-mix-up',
't-v-contents-menu', 't-v-data-service', 't-v-input', 't-v-input-component1',
't-v-input-component2', 't-v-input-composite1', 't-v-input-composite2',
't-v-input-h-d-m-i1', 't-v-input-h-d-m-i2', 't-v-input-h-d-m-i3',
't-v-input-h-d-m-i4', 't-v-input-v-g-a1', 't-v-media-context', 't-v-network',
't-v-number-entry', 't-v-power', 't-v-radio-service', 't-v-satellite',
't-v-satellite-b-s', 't-v-satellite-c-s', 't-v-satellite-toggle',
't-v-terrestrial-analog', 't-v-terrestrial-digital', 't-v-timer',
'a-v-r-input', 'a-v-r-power', 'color-f0-red', 'color-f1-green',
'color-f2-yellow', 'color-f3-blue', 'color-f4-grey', 'color-f5-brown',
'closed-caption-toggle', 'dimmer', 'display-swap', 'd-v-r', 'exit',
'favorite-clear0', 'favorite-clear1', 'favorite-clear2', 'favorite-clear3',
'favorite-recall0', 'favorite-recall1', 'favorite-recall2',
'favorite-recall3', 'favorite-store0', 'favorite-store1', 'favorite-store2',
'favorite-store3', 'guide', 'guide-next-day', 'guide-previous-day', 'info',
'instant-replay', 'link', 'list-program', 'live-content', 'lock',
'media-apps', 'media-last', 'media-skip-backward', 'media-skip-forward',
'media-step-backward', 'media-step-forward', 'media-top-menu', 'navigate-in',
'navigate-next', 'navigate-out', 'navigate-previous', 'next-favorite-channel',
'next-user-profile', 'on-demand', 'pairing', 'pin-p-down', 'pin-p-move',
'pin-p-toggle', 'pin-p-up', 'play-speed-down', 'play-speed-reset',
'play-speed-up', 'random-toggle', 'rc-low-battery', 'record-speed-next',
'rf-bypass', 'scan-channels-toggle', 'screen-mode-next', 'settings',
'split-screen-toggle', 's-t-b-input', 's-t-b-power', 'subtitle', 'teletext',
'video-mode-next', 'wink', 'zoom-toggle', 'audio-volume-down',
'audio-volume-up', 'audio-volume-mute', 'browser-back', 'browser-forward',
'channel-down', 'channel-up', 'context-menu', 'eject', 'end', 'enter', 'home',
'media-fast-forward', 'media-play', 'media-play-pause', 'media-record',
'media-rewind', 'media-stop', 'media-next-track', 'media-pause',
'media-previous-track', 'power', 'unidentified'
])
function isValidModifier (modifier) {
return (
// built-in aliases
VALID_MODIFIERS.has(modifier) ||
// keyCode
Number.isInteger(parseInt(modifier, 10)) ||
// keyAlias (an Unicode character)
Array.from(modifier).length === 1 ||
// keyAlias (special keys)
KEY_ALIASES.has(modifier)
)
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'enforce valid `v-on` directives',
category: 'essential',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/valid-v-on.md'
},
fixable: null,
schema: []
},
create (context) {
return utils.defineTemplateBodyVisitor(context, {
"VAttribute[directive=true][key.name='on']" (node) {
for (const modifier of node.key.modifiers) {
if (!isValidModifier(modifier)) {
context.report({
node,
loc: node.loc,
message: "'v-on' directives don't support the modifier '{{modifier}}'.",
data: { modifier }
})
}
}
if (!utils.hasAttributeValue(node) && !node.key.modifiers.some(VERB_MODIFIERS.has, VERB_MODIFIERS)) {
context.report({
node,
loc: node.loc,
message: "'v-on' directives require that attribute value or verb modifiers."
})
}
}
})
}
}
+56
View File
@@ -0,0 +1,56 @@
/**
* @author Toru Nagashima
* @copyright 2017 Toru Nagashima. All rights reserved.
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'enforce valid `v-once` directives',
category: 'essential',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/valid-v-once.md'
},
fixable: null,
schema: []
},
create (context) {
return utils.defineTemplateBodyVisitor(context, {
"VAttribute[directive=true][key.name='once']" (node) {
if (node.key.argument) {
context.report({
node,
loc: node.loc,
message: "'v-once' directives require no argument."
})
}
if (node.key.modifiers.length > 0) {
context.report({
node,
loc: node.loc,
message: "'v-once' directives require no modifier."
})
}
if (node.value) {
context.report({
node,
loc: node.loc,
message: "'v-once' directives require no attribute value."
})
}
}
})
}
}
+56
View File
@@ -0,0 +1,56 @@
/**
* @author Toru Nagashima
* @copyright 2017 Toru Nagashima. All rights reserved.
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'enforce valid `v-pre` directives',
category: 'essential',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/valid-v-pre.md'
},
fixable: null,
schema: []
},
create (context) {
return utils.defineTemplateBodyVisitor(context, {
"VAttribute[directive=true][key.name='pre']" (node) {
if (node.key.argument) {
context.report({
node,
loc: node.loc,
message: "'v-pre' directives require no argument."
})
}
if (node.key.modifiers.length > 0) {
context.report({
node,
loc: node.loc,
message: "'v-pre' directives require no modifier."
})
}
if (node.value) {
context.report({
node,
loc: node.loc,
message: "'v-pre' directives require no attribute value."
})
}
}
})
}
}
+56
View File
@@ -0,0 +1,56 @@
/**
* @author Toru Nagashima
* @copyright 2017 Toru Nagashima. All rights reserved.
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'enforce valid `v-show` directives',
category: 'essential',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/valid-v-show.md'
},
fixable: null,
schema: []
},
create (context) {
return utils.defineTemplateBodyVisitor(context, {
"VAttribute[directive=true][key.name='show']" (node) {
if (node.key.argument) {
context.report({
node,
loc: node.loc,
message: "'v-show' directives require no argument."
})
}
if (node.key.modifiers.length > 0) {
context.report({
node,
loc: node.loc,
message: "'v-show' directives require no modifier."
})
}
if (!utils.hasAttributeValue(node)) {
context.report({
node,
loc: node.loc,
message: "'v-show' directives require that attribute value."
})
}
}
})
}
}
+56
View File
@@ -0,0 +1,56 @@
/**
* @author Toru Nagashima
* @copyright 2017 Toru Nagashima. All rights reserved.
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'enforce valid `v-text` directives',
category: 'essential',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/valid-v-text.md'
},
fixable: null,
schema: []
},
create (context) {
return utils.defineTemplateBodyVisitor(context, {
"VAttribute[directive=true][key.name='text']" (node) {
if (node.key.argument) {
context.report({
node,
loc: node.loc,
message: "'v-text' directives require no argument."
})
}
if (node.key.modifiers.length > 0) {
context.report({
node,
loc: node.loc,
message: "'v-text' directives require no modifier."
})
}
if (!utils.hasAttributeValue(node)) {
context.report({
node,
loc: node.loc,
message: "'v-text' directives require that attribute value."
})
}
}
})
}
}