Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add vue/quote-props rule #1769

Merged
merged 1 commit into from Jan 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/rules/README.md
Expand Up @@ -399,6 +399,7 @@ The following rules extend the rules provided by ESLint itself and apply them to
| [vue/object-shorthand](./object-shorthand.md) | require or disallow method and property shorthand syntax for object literals in `<template>` | :wrench: |
| [vue/operator-linebreak](./operator-linebreak.md) | enforce consistent linebreak style for operators in `<template>` | :wrench: |
| [vue/prefer-template](./prefer-template.md) | require template literals instead of string concatenation in `<template>` | :wrench: |
| [vue/quote-props](./quote-props.md) | require quotes around object literal property names in `<template>` | :wrench: |
| [vue/space-in-parens](./space-in-parens.md) | enforce consistent spacing inside parentheses in `<template>` | :wrench: |
| [vue/space-infix-ops](./space-infix-ops.md) | require spacing around infix operators in `<template>` | :wrench: |
| [vue/space-unary-ops](./space-unary-ops.md) | enforce consistent spacing before or after unary operators in `<template>` | :wrench: |
Expand Down
27 changes: 27 additions & 0 deletions docs/rules/quote-props.md
@@ -0,0 +1,27 @@
---
pageClass: rule-details
sidebarDepth: 0
title: vue/quote-props
description: require quotes around object literal property names in `<template>`
---
# vue/quote-props

> require quotes around object literal property names in `<template>`

- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.

This rule is the same rule as core [quote-props] rule but it applies to the expressions in `<template>`.

## :books: Further Reading

- [quote-props]

[quote-props]: https://eslint.org/docs/rules/quote-props

## :mag: Implementation

- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/quote-props.js)
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/quote-props.js)

<sup>Taken with ❤️ [from ESLint core](https://eslint.org/docs/rules/quote-props)</sup>
1 change: 1 addition & 0 deletions lib/index.js
Expand Up @@ -160,6 +160,7 @@ module.exports = {
'prefer-separate-static-class': require('./rules/prefer-separate-static-class'),
'prefer-template': require('./rules/prefer-template'),
'prop-name-casing': require('./rules/prop-name-casing'),
'quote-props': require('./rules/quote-props'),
'require-component-is': require('./rules/require-component-is'),
'require-default-prop': require('./rules/require-default-prop'),
'require-direct-export': require('./rules/require-direct-export'),
Expand Down
54 changes: 54 additions & 0 deletions lib/rules/quote-props.js
@@ -0,0 +1,54 @@
/**
* @author Yosuke Ota
* See LICENSE file in root directory for full license.
*/
'use strict'

const { wrapCoreRule, flatten } = require('../utils')

// eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories
module.exports = wrapCoreRule('quote-props', {
skipDynamicArguments: true,
preprocess(context, { wrapContextToOverrideProperties, defineVisitor }) {
const sourceCode = context.getSourceCode()
/**
* @type {'"' | "'" | null}
*/
let htmlQuote = null
defineVisitor({
/** @param {VExpressionContainer} node */
'VAttribute > VExpressionContainer.value'(node) {
const text = sourceCode.getText(node)
const firstChar = text[0]
htmlQuote = firstChar === "'" || firstChar === '"' ? firstChar : null
},
'VAttribute > VExpressionContainer.value:exit'() {
htmlQuote = null
}
})

wrapContextToOverrideProperties({
// Override the report method and replace the quotes in the fixed text with safe quotes.
report(descriptor) {
if (htmlQuote) {
const expectedQuote = htmlQuote === '"' ? "'" : '"'
context.report({
...descriptor,
*fix(fixer) {
for (const fix of flatten(
descriptor.fix && descriptor.fix(fixer)
)) {
yield fixer.replaceTextRange(
fix.range,
fix.text.replace(/["']/gu, expectedQuote)
)
}
}
})
} else {
context.report(descriptor)
}
}
})
}
})
167 changes: 127 additions & 40 deletions lib/utils/index.js
Expand Up @@ -79,6 +79,41 @@ function getCoreRule(name) {
return map.get(name) || null
}

/**
* @template {object} T
* @param {T} target
* @param {Partial<T>[]} propsArray
* @returns {T}
*/
function newProxy(target, ...propsArray) {
const result = new Proxy(
{},
{
get(_object, key) {
for (const props of propsArray) {
if (key in props) {
// @ts-expect-error
return props[key]
}
}
// @ts-expect-error
return target[key]
},

has(_object, key) {
return key in target
},
ownKeys(_object) {
return Reflect.ownKeys(target)
},
getPrototypeOf(_object) {
return Reflect.getPrototypeOf(target)
}
}
)
return /** @type {T} */ (result)
}

/**
* Wrap the rule context object to override methods which access to tokens (such as getTokenAfter).
* @param {RuleContext} context The rule context object.
Expand Down Expand Up @@ -147,18 +182,16 @@ function wrapContextToOverrideTokenMethods(context, tokenStore, options) {
})
return result
}
const sourceCode = new Proxy(Object.assign({}, eslintSourceCode), {
get(_object, key) {
if (key === 'tokensAndComments') {
const sourceCode = newProxy(
eslintSourceCode,
{
get tokensAndComments() {
return getTokensAndComments()
}
if (key === 'getNodeByRangeIndex') {
return getNodeByRangeIndex
}
// @ts-expect-error
return key in tokenStore ? tokenStore[key] : eslintSourceCode[key]
}
})
},
getNodeByRangeIndex
},
tokenStore
)

const containerScopes = new WeakMap()

Expand All @@ -183,23 +216,13 @@ function wrapContextToOverrideTokenMethods(context, tokenStore, options) {
const eslintScope = createRequire(require.resolve('eslint'))(
'eslint-scope'
)
const expStmt = new Proxy(exprContainer, {
get(_object, key) {
if (key === 'type') {
return 'ExpressionStatement'
}
// @ts-expect-error
return exprContainer[key]
}
const expStmt = newProxy(exprContainer, {
// @ts-expect-error
type: 'ExpressionStatement'
})
const scopeProgram = new Proxy(programNode, {
get(_object, key) {
if (key === 'body') {
return [expStmt]
}
// @ts-expect-error
return programNode[key]
}
const scopeProgram = newProxy(programNode, {
// @ts-expect-error
body: [expStmt]
})
const scope = eslintScope.analyze(scopeProgram, {
ignoreEval: true,
Expand All @@ -218,9 +241,7 @@ function wrapContextToOverrideTokenMethods(context, tokenStore, options) {

return null
}
return {
// @ts-expect-error
__proto__: context,
return newProxy(context, {
getSourceCode() {
return sourceCode
},
Expand All @@ -232,7 +253,7 @@ function wrapContextToOverrideTokenMethods(context, tokenStore, options) {

return context.getDeclaredVariables(node)
}
}
})
}

/**
Expand Down Expand Up @@ -262,9 +283,7 @@ function wrapContextToOverrideReportMethodToSkipDynamicArgument(context) {
leaveNode() {}
})

return {
// @ts-expect-error
__proto__: context,
return newProxy(context, {
report(descriptor, ...args) {
let range = null
if (descriptor.loc) {
Expand All @@ -289,7 +308,7 @@ function wrapContextToOverrideReportMethodToSkipDynamicArgument(context) {
}
context.report(descriptor, ...args)
}
}
})
}

// ------------------------------------------------------------------------------
Expand Down Expand Up @@ -322,6 +341,25 @@ module.exports = {
*/
defineDocumentVisitor,

/**
* @callback WrapCoreRuleCreate
* @param {RuleContext} ruleContext
* @param {WrapCoreRuleCreateContext} wrapContext
* @returns {TemplateListener}
*
* @typedef {object} WrapCoreRuleCreateContext
* @property {RuleListener} coreHandlers
*/
/**
* @callback WrapCoreRulePreprocess
* @param {RuleContext} ruleContext
* @param {WrapCoreRulePreprocessContext} wrapContext
* @returns {void}
*
* @typedef {object} WrapCoreRulePreprocessContext
* @property { (override: Partial<RuleContext>) => RuleContext } wrapContextToOverrideProperties Wrap the rule context object to override
* @property { (visitor: TemplateListener) => void } defineVisitor Define template body visitor
*/
/**
* Wrap a given core rule to apply it to Vue.js template.
* @param {string} coreRuleName The name of the core rule implementation to wrap.
Expand All @@ -330,7 +368,8 @@ module.exports = {
* @param {boolean} [options.skipDynamicArguments] If `true`, skip validation within dynamic arguments.
* @param {boolean} [options.skipDynamicArgumentsReport] If `true`, skip report within dynamic arguments.
* @param {boolean} [options.applyDocument] If `true`, apply check to document fragment.
* @param { (context: RuleContext, options: { coreHandlers: RuleListener }) => TemplateListener } [options.create] If define, extend core rule.
* @param {WrapCoreRulePreprocess} [options.preprocess] Preprocess to calling create of core rule.
* @param {WrapCoreRuleCreate} [options.create] If define, extend core rule.
* @returns {RuleModule} The wrapped rule implementation.
*/
wrapCoreRule(coreRuleName, options) {
Expand Down Expand Up @@ -366,6 +405,7 @@ module.exports = {
skipDynamicArguments,
skipDynamicArgumentsReport,
applyDocument,
preprocess,
create
} = options || {}
return {
Expand All @@ -387,12 +427,25 @@ module.exports = {
wrapContextToOverrideReportMethodToSkipDynamicArgument(context)
}

// Move `Program` handlers to `VElement[parent.type!='VElement']`
/** @type {TemplateListener} */
const handlers = {}

if (preprocess) {
preprocess(context, {
wrapContextToOverrideProperties(override) {
context = newProxy(context, override)
return context
},
defineVisitor(visitor) {
compositingVisitors(handlers, visitor)
}
})
}

const coreHandlers = coreRule.create(context)
compositingVisitors(handlers, coreHandlers)

const handlers = /** @type {TemplateListener} */ (
Object.assign({}, coreHandlers)
)
// Move `Program` handlers to `VElement[parent.type!='VElement']`
if (handlers.Program) {
handlers[
applyDocument
Expand Down Expand Up @@ -462,6 +515,13 @@ module.exports = {
* @returns {v is T}
*/
isDef,
/**
* Flattens arrays, objects and iterable objects.
* @template T
* @param {T | Iterable<T> | null | undefined} v
* @returns {T[]}
*/
flatten,
/**
* Get the previous sibling element of the given element.
* @param {VElement} node The element node to get the previous sibling element.
Expand Down Expand Up @@ -1837,6 +1897,33 @@ function isDef(v) {
return v != null
}

/**
* Flattens arrays, objects and iterable objects.
* @template T
* @param {T | Iterable<T> | null | undefined} v
* @returns {T[]}
*/
function flatten(v) {
/** @type {T[]} */
const result = []
if (v) {
if (isIterable(v)) {
result.push(...v)
} else {
result.push(v)
}
}
return result
}

/**
* @param {*} v
* @returns {v is Iterable<any>}
*/
function isIterable(v) {
return v && Symbol.iterator in v
}

// ------------------------------------------------------------------------------
// Nodejs Helpers
// ------------------------------------------------------------------------------
Expand Down