Skip to content

Commit b1463a3

Browse files
authored
Add vue-scoped-css/require-v-deep-argument rule (#41)
* Add `vue-scoped-css/require-v-deep-arguments` rule * rename * update
1 parent af14bc8 commit b1463a3

File tree

10 files changed

+300
-10
lines changed

10 files changed

+300
-10
lines changed

.github/workflows/NodeCI.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ jobs:
1010
lint:
1111
runs-on: ubuntu-latest
1212
steps:
13-
- uses: actions/checkout@v1
13+
- uses: actions/checkout@v2
1414
- uses: actions/setup-node@v1
1515
with:
1616
node-version: 14
@@ -52,7 +52,7 @@ jobs:
5252
test-and-coverage:
5353
runs-on: ubuntu-latest
5454
steps:
55-
- uses: actions/checkout@v1
55+
- uses: actions/checkout@v2
5656
- uses: actions/setup-node@v1
5757
- name: Install Packages
5858
run: npm install

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ Enforce all the rules in this category with:
103103
| [vue-scoped-css/no-unused-keyframes](https://future-architect.github.io/eslint-plugin-vue-scoped-css/rules/no-unused-keyframes.html) | Reports the `@keyframes` is not used in Scoped CSS. | |
104104
| [vue-scoped-css/no-unused-selector](https://future-architect.github.io/eslint-plugin-vue-scoped-css/rules/no-unused-selector.html) | Reports selectors defined in Scoped CSS not used in `<template>`. | |
105105
| [vue-scoped-css/require-scoped](https://future-architect.github.io/eslint-plugin-vue-scoped-css/rules/require-scoped.html) | Enforce the `<style>` tags to has the `scoped` attribute. | |
106+
| [vue-scoped-css/require-v-deep-argument](https://future-architect.github.io/eslint-plugin-vue-scoped-css/rules/require-v-deep-argument.html) | require selector argument to be passed to `::v-deep()`. | :wrench: |
106107

107108
## Recommended for Vue.js 2.x
108109

docs/.vuepress/categories.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,8 @@ const isCategoryTest = {
1414
recommended: ({ deprecated, docs: { categories } }) =>
1515
!deprecated &&
1616
categories.length &&
17-
categories.every(
18-
(cat) => cat === "recommended" || cat === "vue3-recommended"
19-
),
17+
categories.some((cat) => cat === "recommended") &&
18+
categories.some((cat) => cat === "vue3-recommended"),
2019
"vue2-recommended": ({ deprecated, docs: { categories } }) =>
2120
!deprecated &&
2221
categories.length &&

docs/rules/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ Enforce all the rules in this category with:
2323
| [vue-scoped-css/no-unused-keyframes](./no-unused-keyframes.md) | Reports the `@keyframes` is not used in Scoped CSS. | |
2424
| [vue-scoped-css/no-unused-selector](./no-unused-selector.md) | Reports selectors defined in Scoped CSS not used in `<template>`. | |
2525
| [vue-scoped-css/require-scoped](./require-scoped.md) | Enforce the `<style>` tags to has the `scoped` attribute. | |
26+
| [vue-scoped-css/require-v-deep-argument](./require-v-deep-argument.md) | require selector argument to be passed to `::v-deep()`. | :wrench: |
2627

2728
## Recommended for Vue.js 2.x
2829

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
---
2+
pageClass: "rule-details"
3+
sidebarDepth: 0
4+
title: "vue-scoped-css/require-v-deep-argument"
5+
description: "require selector argument to be passed to `::v-deep()`."
6+
---
7+
# vue-scoped-css/require-v-deep-argument
8+
9+
> require selector argument to be passed to `::v-deep()`.
10+
11+
- :gear: This rule is included in `"plugin:vue-scoped-css/vue3-recommended"` and `"plugin:vue-scoped-css/all"`.
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+
## :book: Rule Details
15+
16+
This rule reports `::v-deep` pseudo-element with no selector argument passed.
17+
18+
<eslint-code-block fix :rules="{'vue-scoped-css/require-v-deep-argument': ['error']}">
19+
20+
```vue
21+
<style scoped>
22+
/* ✗ BAD */
23+
.baz .qux ::v-deep .foo .bar {}
24+
.baz .qux ::v-deep() .foo .bar {}
25+
26+
/* ✓ GOOD */
27+
.baz .qux ::v-deep(.foo .bar) {}
28+
</style>
29+
```
30+
31+
</eslint-code-block>
32+
33+
## :wrench: Options
34+
35+
Nothing.
36+
37+
## Implementation
38+
39+
- [Rule source](https://github.com/future-architect/eslint-plugin-vue-scoped-css/blob/master/lib/rules/require-v-deep-argument.ts)
40+
- [Test source](https://github.com/future-architect/eslint-plugin-vue-scoped-css/blob/master/tests/lib/rules/require-v-deep-argument.js)
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import {
2+
getStyleContexts,
3+
getCommentDirectivesReporter,
4+
StyleContext,
5+
ValidStyleContext,
6+
} from "../styles/context"
7+
import type { RuleContext, Rule, Range } from "../types"
8+
import {
9+
isVDeepPseudoV2,
10+
isVDeepPseudo,
11+
VDeepPseudo,
12+
isPseudoEmptyArguments,
13+
} from "../styles/utils/selectors"
14+
import type { VCSSSelectorNode, VCSSAtRule, VCSSStyleRule } from "../styles/ast"
15+
import {
16+
hasSelectorNodes,
17+
isVCSSAtRule,
18+
isVCSSDeclarationProperty,
19+
isVCSSComment,
20+
} from "../styles/utils/css-nodes"
21+
22+
declare const module: {
23+
exports: Rule
24+
}
25+
26+
module.exports = {
27+
meta: {
28+
docs: {
29+
description:
30+
"require selector argument to be passed to `::v-deep()`.",
31+
categories: ["vue3-recommended"],
32+
default: "warn",
33+
url:
34+
"https://future-architect.github.io/eslint-plugin-vue-scoped-css/rules/require-v-deep-argument.html",
35+
},
36+
fixable: "code",
37+
messages: {
38+
missingArguments:
39+
"Need to pass argument to the `::v-deep` pseudo-element.",
40+
},
41+
schema: [],
42+
type: "suggestion", // "problem",
43+
},
44+
create(context: RuleContext) {
45+
const styles = getStyleContexts(context)
46+
.filter(StyleContext.isValid)
47+
.filter((style) => style.scoped)
48+
if (!styles.length) {
49+
return {}
50+
}
51+
const reporter = getCommentDirectivesReporter(context)
52+
53+
/**
54+
* Find VCSSStyleRule or nest VCSSAtRule
55+
*/
56+
function findHasSelectorsNode(
57+
node: VCSSSelectorNode,
58+
):
59+
| (VCSSAtRule & { name: "nest"; selectors: VCSSSelectorNode[] })
60+
| VCSSStyleRule
61+
| null {
62+
if (hasSelectorNodes(node.parent)) {
63+
return node.parent
64+
}
65+
if (isVCSSAtRule(node.parent)) {
66+
return null
67+
}
68+
return findHasSelectorsNode(node.parent)
69+
}
70+
71+
/**
72+
* Reports the given node
73+
* @param {ASTNode} node node to report
74+
*/
75+
function report(node: VDeepPseudo) {
76+
reporter.report({
77+
node,
78+
loc: node.loc,
79+
messageId: "missingArguments",
80+
fix(fixer) {
81+
if (!isVDeepPseudoV2(node)) {
82+
return null
83+
}
84+
const nodes = node.parent.nodes
85+
const selectorIndex = nodes.indexOf(node)
86+
const nextNode = nodes[selectorIndex + 1]
87+
if (!nextNode) {
88+
return null
89+
}
90+
const betweenRange: Range = [
91+
node.range[0] + node.value.length,
92+
nextNode.range[0],
93+
]
94+
if (
95+
context
96+
.getSourceCode()
97+
.text.slice(...betweenRange)
98+
.trim()
99+
) {
100+
// ::v-deep /* comment */ .foo
101+
return null
102+
}
103+
104+
const ruleNode = findHasSelectorsNode(node)
105+
if (
106+
!ruleNode?.nodes.every(
107+
(n) =>
108+
isVCSSDeclarationProperty(n) ||
109+
isVCSSComment(n),
110+
)
111+
) {
112+
// Maybe includes nesting
113+
return null
114+
}
115+
116+
const last = nodes[nodes.length - 1]
117+
return [
118+
fixer.removeRange(betweenRange),
119+
fixer.insertTextAfterRange(betweenRange, "("),
120+
fixer.insertTextAfterRange(last.range, ")"),
121+
]
122+
},
123+
})
124+
}
125+
126+
/**
127+
* Verify the style
128+
*/
129+
function verify(style: ValidStyleContext) {
130+
style.traverseSelectorNodes({
131+
enterNode(node) {
132+
if (isVDeepPseudoV2(node)) {
133+
report(node)
134+
} else if (
135+
isVDeepPseudo(node) &&
136+
isPseudoEmptyArguments(node)
137+
) {
138+
report(node)
139+
}
140+
},
141+
})
142+
}
143+
144+
return {
145+
"Program:exit"() {
146+
for (const style of styles) {
147+
verify(style)
148+
}
149+
},
150+
}
151+
},
152+
}

lib/styles/utils/css-nodes.ts

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,33 @@
1-
import {
1+
import type {
22
VCSSAtRule,
33
VCSSNode,
44
VCSSStyleRule,
55
VCSSContainerNode,
66
VCSSStyleSheet,
77
VCSSSelectorNode,
8+
VCSSSelector,
9+
VCSSSelectorPseudo,
10+
VCSSDeclarationProperty,
11+
VCSSComment,
812
} from "../ast"
913
import { isNestingAtRule } from "./selectors"
1014

1115
/**
1216
* Checks whether the given node is VCSSAtRule
1317
* @param node node to check
1418
*/
15-
export function isVCSSAtRule(node: VCSSNode | null): node is VCSSAtRule {
19+
export function isVCSSAtRule(
20+
node: VCSSNode | VCSSSelector | VCSSSelectorPseudo | null,
21+
): node is VCSSAtRule {
1622
return node?.type === "VCSSAtRule"
1723
}
1824
/**
1925
* Checks whether the given node is VCSSStyleRule
2026
* @param node node to check
2127
*/
22-
export function isVCSSStyleRule(node: VCSSNode | null): node is VCSSStyleRule {
28+
export function isVCSSStyleRule(
29+
node: VCSSNode | VCSSSelector | VCSSSelectorPseudo | null,
30+
): node is VCSSStyleRule {
2331
return node?.type === "VCSSStyleRule"
2432
}
2533
/**
@@ -31,6 +39,22 @@ export function isVCSSStyleSheet(
3139
): node is VCSSStyleSheet {
3240
return node?.type === "VCSSStyleSheet"
3341
}
42+
/**
43+
* Checks whether the given node is VCSSDeclarationProperty
44+
* @param node node to check
45+
*/
46+
export function isVCSSDeclarationProperty(
47+
node: VCSSNode | null,
48+
): node is VCSSDeclarationProperty {
49+
return node?.type === "VCSSDeclarationProperty"
50+
}
51+
/**
52+
* Checks whether the given node is VCSSComment
53+
* @param node node to check
54+
*/
55+
export function isVCSSComment(node: VCSSNode | null): node is VCSSComment {
56+
return node?.type === "VCSSComment" || node?.type === "VCSSInlineComment"
57+
}
3458
/**
3559
* Checks whether the given node has nodes node
3660
* @param node node to check
@@ -50,7 +74,7 @@ export function isVCSSContainerNode(
5074
* Checks whether the given node has selectors.
5175
*/
5276
export function hasSelectorNodes(
53-
node: VCSSNode,
77+
node: VCSSNode | VCSSSelector | VCSSSelectorPseudo,
5478
): node is
5579
| (VCSSAtRule & { name: "nest"; selectors: VCSSSelectorNode[] })
5680
| VCSSStyleRule {

lib/styles/utils/selectors.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,17 @@ export function isVGlobalPseudo(
158158
return false
159159
}
160160

161+
/**
162+
* Checks whether the given pseudo node is empty arguments
163+
* @param node node to check
164+
*/
165+
export function isPseudoEmptyArguments(node: VCSSSelectorPseudo): boolean {
166+
return (
167+
node.nodes.length === 0 ||
168+
(node.nodes.length === 1 && node.nodes[0].nodes.length === 0)
169+
)
170+
}
171+
161172
/**
162173
* Checks whether the given node is VCSSTypeSelector
163174
* @param node node to check
@@ -287,7 +298,7 @@ export function isDeepCombinator(
287298
* @param node node to check
288299
*/
289300
export function isNestingAtRule(
290-
node: VCSSNode | null,
301+
node: VCSSNode | VCSSSelector | VCSSSelectorPseudo | null,
291302
): node is VCSSAtRule & { name: "nest"; selectors: VCSSSelectorNode[] } {
292303
if (node == null) {
293304
return false

lib/utils/rules.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ const baseRules = [
3131
ruleName: "require-selector-used-inside",
3232
ruleId: "vue-scoped-css/require-selector-used-inside",
3333
},
34+
{
35+
rule: require("../rules/require-v-deep-argument"),
36+
ruleName: "require-v-deep-argument",
37+
ruleId: "vue-scoped-css/require-v-deep-argument",
38+
},
3439
]
3540

3641
export const rules = baseRules.map((obj) => {

0 commit comments

Comments
 (0)