Skip to content

Commit f527e27

Browse files
authored
Add vue/quote-props rule (#1769)
1 parent 32d1fb7 commit f527e27

File tree

8 files changed

+337
-43
lines changed

8 files changed

+337
-43
lines changed

docs/rules/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,7 @@ The following rules extend the rules provided by ESLint itself and apply them to
399399
| [vue/object-shorthand](./object-shorthand.md) | require or disallow method and property shorthand syntax for object literals in `<template>` | :wrench: |
400400
| [vue/operator-linebreak](./operator-linebreak.md) | enforce consistent linebreak style for operators in `<template>` | :wrench: |
401401
| [vue/prefer-template](./prefer-template.md) | require template literals instead of string concatenation in `<template>` | :wrench: |
402+
| [vue/quote-props](./quote-props.md) | require quotes around object literal property names in `<template>` | :wrench: |
402403
| [vue/space-in-parens](./space-in-parens.md) | enforce consistent spacing inside parentheses in `<template>` | :wrench: |
403404
| [vue/space-infix-ops](./space-infix-ops.md) | require spacing around infix operators in `<template>` | :wrench: |
404405
| [vue/space-unary-ops](./space-unary-ops.md) | enforce consistent spacing before or after unary operators in `<template>` | :wrench: |

docs/rules/quote-props.md

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
pageClass: rule-details
3+
sidebarDepth: 0
4+
title: vue/quote-props
5+
description: require quotes around object literal property names in `<template>`
6+
---
7+
# vue/quote-props
8+
9+
> require quotes around object literal property names in `<template>`
10+
11+
- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
12+
- :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.
13+
14+
This rule is the same rule as core [quote-props] rule but it applies to the expressions in `<template>`.
15+
16+
## :books: Further Reading
17+
18+
- [quote-props]
19+
20+
[quote-props]: https://eslint.org/docs/rules/quote-props
21+
22+
## :mag: Implementation
23+
24+
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/quote-props.js)
25+
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/quote-props.js)
26+
27+
<sup>Taken with ❤️ [from ESLint core](https://eslint.org/docs/rules/quote-props)</sup>

lib/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ module.exports = {
160160
'prefer-separate-static-class': require('./rules/prefer-separate-static-class'),
161161
'prefer-template': require('./rules/prefer-template'),
162162
'prop-name-casing': require('./rules/prop-name-casing'),
163+
'quote-props': require('./rules/quote-props'),
163164
'require-component-is': require('./rules/require-component-is'),
164165
'require-default-prop': require('./rules/require-default-prop'),
165166
'require-direct-export': require('./rules/require-direct-export'),

lib/rules/quote-props.js

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/**
2+
* @author Yosuke Ota
3+
* See LICENSE file in root directory for full license.
4+
*/
5+
'use strict'
6+
7+
const { wrapCoreRule, flatten } = require('../utils')
8+
9+
// eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories
10+
module.exports = wrapCoreRule('quote-props', {
11+
skipDynamicArguments: true,
12+
preprocess(context, { wrapContextToOverrideProperties, defineVisitor }) {
13+
const sourceCode = context.getSourceCode()
14+
/**
15+
* @type {'"' | "'" | null}
16+
*/
17+
let htmlQuote = null
18+
defineVisitor({
19+
/** @param {VExpressionContainer} node */
20+
'VAttribute > VExpressionContainer.value'(node) {
21+
const text = sourceCode.getText(node)
22+
const firstChar = text[0]
23+
htmlQuote = firstChar === "'" || firstChar === '"' ? firstChar : null
24+
},
25+
'VAttribute > VExpressionContainer.value:exit'() {
26+
htmlQuote = null
27+
}
28+
})
29+
30+
wrapContextToOverrideProperties({
31+
// Override the report method and replace the quotes in the fixed text with safe quotes.
32+
report(descriptor) {
33+
if (htmlQuote) {
34+
const expectedQuote = htmlQuote === '"' ? "'" : '"'
35+
context.report({
36+
...descriptor,
37+
*fix(fixer) {
38+
for (const fix of flatten(
39+
descriptor.fix && descriptor.fix(fixer)
40+
)) {
41+
yield fixer.replaceTextRange(
42+
fix.range,
43+
fix.text.replace(/["']/gu, expectedQuote)
44+
)
45+
}
46+
}
47+
})
48+
} else {
49+
context.report(descriptor)
50+
}
51+
}
52+
})
53+
}
54+
})

lib/utils/index.js

+127-40
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,41 @@ function getCoreRule(name) {
7979
return map.get(name) || null
8080
}
8181

82+
/**
83+
* @template {object} T
84+
* @param {T} target
85+
* @param {Partial<T>[]} propsArray
86+
* @returns {T}
87+
*/
88+
function newProxy(target, ...propsArray) {
89+
const result = new Proxy(
90+
{},
91+
{
92+
get(_object, key) {
93+
for (const props of propsArray) {
94+
if (key in props) {
95+
// @ts-expect-error
96+
return props[key]
97+
}
98+
}
99+
// @ts-expect-error
100+
return target[key]
101+
},
102+
103+
has(_object, key) {
104+
return key in target
105+
},
106+
ownKeys(_object) {
107+
return Reflect.ownKeys(target)
108+
},
109+
getPrototypeOf(_object) {
110+
return Reflect.getPrototypeOf(target)
111+
}
112+
}
113+
)
114+
return /** @type {T} */ (result)
115+
}
116+
82117
/**
83118
* Wrap the rule context object to override methods which access to tokens (such as getTokenAfter).
84119
* @param {RuleContext} context The rule context object.
@@ -147,18 +182,16 @@ function wrapContextToOverrideTokenMethods(context, tokenStore, options) {
147182
})
148183
return result
149184
}
150-
const sourceCode = new Proxy(Object.assign({}, eslintSourceCode), {
151-
get(_object, key) {
152-
if (key === 'tokensAndComments') {
185+
const sourceCode = newProxy(
186+
eslintSourceCode,
187+
{
188+
get tokensAndComments() {
153189
return getTokensAndComments()
154-
}
155-
if (key === 'getNodeByRangeIndex') {
156-
return getNodeByRangeIndex
157-
}
158-
// @ts-expect-error
159-
return key in tokenStore ? tokenStore[key] : eslintSourceCode[key]
160-
}
161-
})
190+
},
191+
getNodeByRangeIndex
192+
},
193+
tokenStore
194+
)
162195

163196
const containerScopes = new WeakMap()
164197

@@ -183,23 +216,13 @@ function wrapContextToOverrideTokenMethods(context, tokenStore, options) {
183216
const eslintScope = createRequire(require.resolve('eslint'))(
184217
'eslint-scope'
185218
)
186-
const expStmt = new Proxy(exprContainer, {
187-
get(_object, key) {
188-
if (key === 'type') {
189-
return 'ExpressionStatement'
190-
}
191-
// @ts-expect-error
192-
return exprContainer[key]
193-
}
219+
const expStmt = newProxy(exprContainer, {
220+
// @ts-expect-error
221+
type: 'ExpressionStatement'
194222
})
195-
const scopeProgram = new Proxy(programNode, {
196-
get(_object, key) {
197-
if (key === 'body') {
198-
return [expStmt]
199-
}
200-
// @ts-expect-error
201-
return programNode[key]
202-
}
223+
const scopeProgram = newProxy(programNode, {
224+
// @ts-expect-error
225+
body: [expStmt]
203226
})
204227
const scope = eslintScope.analyze(scopeProgram, {
205228
ignoreEval: true,
@@ -218,9 +241,7 @@ function wrapContextToOverrideTokenMethods(context, tokenStore, options) {
218241

219242
return null
220243
}
221-
return {
222-
// @ts-expect-error
223-
__proto__: context,
244+
return newProxy(context, {
224245
getSourceCode() {
225246
return sourceCode
226247
},
@@ -232,7 +253,7 @@ function wrapContextToOverrideTokenMethods(context, tokenStore, options) {
232253

233254
return context.getDeclaredVariables(node)
234255
}
235-
}
256+
})
236257
}
237258

238259
/**
@@ -262,9 +283,7 @@ function wrapContextToOverrideReportMethodToSkipDynamicArgument(context) {
262283
leaveNode() {}
263284
})
264285

265-
return {
266-
// @ts-expect-error
267-
__proto__: context,
286+
return newProxy(context, {
268287
report(descriptor, ...args) {
269288
let range = null
270289
if (descriptor.loc) {
@@ -289,7 +308,7 @@ function wrapContextToOverrideReportMethodToSkipDynamicArgument(context) {
289308
}
290309
context.report(descriptor, ...args)
291310
}
292-
}
311+
})
293312
}
294313

295314
// ------------------------------------------------------------------------------
@@ -322,6 +341,25 @@ module.exports = {
322341
*/
323342
defineDocumentVisitor,
324343

344+
/**
345+
* @callback WrapCoreRuleCreate
346+
* @param {RuleContext} ruleContext
347+
* @param {WrapCoreRuleCreateContext} wrapContext
348+
* @returns {TemplateListener}
349+
*
350+
* @typedef {object} WrapCoreRuleCreateContext
351+
* @property {RuleListener} coreHandlers
352+
*/
353+
/**
354+
* @callback WrapCoreRulePreprocess
355+
* @param {RuleContext} ruleContext
356+
* @param {WrapCoreRulePreprocessContext} wrapContext
357+
* @returns {void}
358+
*
359+
* @typedef {object} WrapCoreRulePreprocessContext
360+
* @property { (override: Partial<RuleContext>) => RuleContext } wrapContextToOverrideProperties Wrap the rule context object to override
361+
* @property { (visitor: TemplateListener) => void } defineVisitor Define template body visitor
362+
*/
325363
/**
326364
* Wrap a given core rule to apply it to Vue.js template.
327365
* @param {string} coreRuleName The name of the core rule implementation to wrap.
@@ -330,7 +368,8 @@ module.exports = {
330368
* @param {boolean} [options.skipDynamicArguments] If `true`, skip validation within dynamic arguments.
331369
* @param {boolean} [options.skipDynamicArgumentsReport] If `true`, skip report within dynamic arguments.
332370
* @param {boolean} [options.applyDocument] If `true`, apply check to document fragment.
333-
* @param { (context: RuleContext, options: { coreHandlers: RuleListener }) => TemplateListener } [options.create] If define, extend core rule.
371+
* @param {WrapCoreRulePreprocess} [options.preprocess] Preprocess to calling create of core rule.
372+
* @param {WrapCoreRuleCreate} [options.create] If define, extend core rule.
334373
* @returns {RuleModule} The wrapped rule implementation.
335374
*/
336375
wrapCoreRule(coreRuleName, options) {
@@ -366,6 +405,7 @@ module.exports = {
366405
skipDynamicArguments,
367406
skipDynamicArgumentsReport,
368407
applyDocument,
408+
preprocess,
369409
create
370410
} = options || {}
371411
return {
@@ -387,12 +427,25 @@ module.exports = {
387427
wrapContextToOverrideReportMethodToSkipDynamicArgument(context)
388428
}
389429

390-
// Move `Program` handlers to `VElement[parent.type!='VElement']`
430+
/** @type {TemplateListener} */
431+
const handlers = {}
432+
433+
if (preprocess) {
434+
preprocess(context, {
435+
wrapContextToOverrideProperties(override) {
436+
context = newProxy(context, override)
437+
return context
438+
},
439+
defineVisitor(visitor) {
440+
compositingVisitors(handlers, visitor)
441+
}
442+
})
443+
}
444+
391445
const coreHandlers = coreRule.create(context)
446+
compositingVisitors(handlers, coreHandlers)
392447

393-
const handlers = /** @type {TemplateListener} */ (
394-
Object.assign({}, coreHandlers)
395-
)
448+
// Move `Program` handlers to `VElement[parent.type!='VElement']`
396449
if (handlers.Program) {
397450
handlers[
398451
applyDocument
@@ -462,6 +515,13 @@ module.exports = {
462515
* @returns {v is T}
463516
*/
464517
isDef,
518+
/**
519+
* Flattens arrays, objects and iterable objects.
520+
* @template T
521+
* @param {T | Iterable<T> | null | undefined} v
522+
* @returns {T[]}
523+
*/
524+
flatten,
465525
/**
466526
* Get the previous sibling element of the given element.
467527
* @param {VElement} node The element node to get the previous sibling element.
@@ -1837,6 +1897,33 @@ function isDef(v) {
18371897
return v != null
18381898
}
18391899

1900+
/**
1901+
* Flattens arrays, objects and iterable objects.
1902+
* @template T
1903+
* @param {T | Iterable<T> | null | undefined} v
1904+
* @returns {T[]}
1905+
*/
1906+
function flatten(v) {
1907+
/** @type {T[]} */
1908+
const result = []
1909+
if (v) {
1910+
if (isIterable(v)) {
1911+
result.push(...v)
1912+
} else {
1913+
result.push(v)
1914+
}
1915+
}
1916+
return result
1917+
}
1918+
1919+
/**
1920+
* @param {*} v
1921+
* @returns {v is Iterable<any>}
1922+
*/
1923+
function isIterable(v) {
1924+
return v && Symbol.iterator in v
1925+
}
1926+
18401927
// ------------------------------------------------------------------------------
18411928
// Nodejs Helpers
18421929
// ------------------------------------------------------------------------------

0 commit comments

Comments
 (0)