Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 990e68a

Browse files
tofrankieota-meshi
andauthored
Add extraClassAttributes option to match additional class-like attributes in templates (#413)
* feat(no-unused-selector): support customClassAttributes option * feat: case-insensitive * feat: rename customClassAttributes to extraClassAttributes * Create afraid-bikes-smile.md --------- Co-authored-by: Yosuke Ota <otameshiyo23@gmail.com>
1 parent 8399873 commit 990e68a

File tree

7 files changed

+190
-15
lines changed

7 files changed

+190
-15
lines changed

‎.changeset/afraid-bikes-smile.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"eslint-plugin-vue-scoped-css": minor
3+
---
4+
5+
Add `extraClassAttributes` option to match additional class-like attributes in templates, to `vue-scoped-css/no-unused-selector` rule

‎docs/rules/no-unused-selector.md‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,13 +91,15 @@ This is a limitation of this rule. Without this limitation, the root element can
9191
"ignoreBEMModifier": false,
9292
"captureClassesFromDoc": [],
9393
"checkUnscoped": false,
94+
"extraClassAttributes": [],
9495
}]
9596
}
9697
```
9798

9899
- `ignoreBEMModifier` ... Set `true` if you want to ignore the `BEM` modifier. Default is false.
99100
- `captureClassesFromDoc` ... Specifies the regexp that extracts the class name from the documentation in the comments. Even if there is no matching element, no error is reported if the document of a class name exists in the comments.
100101
- `checkUnscoped` ... The rule only checks `<style scoped>` by default, but if set to `true` it will also check `<style>` without the scoped attribute. If you set it to `true`, be very careful that the warned CSS may actually be used outside the `.vue` file.
102+
- `extraClassAttributes` ... Specifies an array of custom attribute names to check for class names in addition to the standard `class` attribute. Useful for frameworks that use custom attributes like `hover-class`, `placeholder-class`, etc. Default is an empty array.
101103

102104
### `"ignoreBEMModifier": true`
103105

@@ -149,6 +151,38 @@ a.button.star {
149151

150152
</eslint-code-block>
151153

154+
### `"extraClassAttributes": ["hover-class", "placeholder-class"]`
155+
156+
<eslint-code-block :rules="{'vue-scoped-css/no-unused-selector': ['error', {extraClassAttributes: ['hover-class', 'placeholder-class']}]}">
157+
158+
```vue
159+
<template>
160+
<div>
161+
<!-- These attributes will be checked for class names -->
162+
<button class="button" hover-class="button-hover">Button</button>
163+
<input placeholder-class="input-placeholder" >
164+
</div>
165+
</template>
166+
<style scoped>
167+
/* ✓ GOOD - These selectors are used in custom attributes */
168+
.button {}
169+
.button-hover {}
170+
.input-placeholder {}
171+
172+
/* ✗ BAD - This selector is not used anywhere */
173+
.unused-class {}
174+
</style>
175+
<script>
176+
export default {
177+
data() {
178+
return {
179+
dynamicHoverClass: 'button-hover',
180+
}
181+
},
182+
}
183+
</script>
184+
```
185+
152186
## :books: Further reading
153187

154188
- [vue-scoped-css/require-selector-used-inside]

‎lib/options.ts‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ import { toRegExp } from "./utils/regexp";
33
export interface QueryOptions {
44
ignoreBEMModifier?: boolean;
55
captureClassesFromDoc?: string[];
6+
extraClassAttributes?: string[];
67
}
78

89
export interface ParsedQueryOptions {
910
ignoreBEMModifier: boolean;
1011
captureClassesFromDoc: RegExp[];
12+
extraClassAttributes: string[];
1113
}
1214

1315
/**
@@ -16,11 +18,13 @@ export interface ParsedQueryOptions {
1618
export function parseQueryOptions(
1719
options: QueryOptions | undefined,
1820
): ParsedQueryOptions {
19-
const { ignoreBEMModifier, captureClassesFromDoc } = options || {};
21+
const { ignoreBEMModifier, captureClassesFromDoc, extraClassAttributes } =
22+
options || {};
2023

2124
return {
2225
ignoreBEMModifier: ignoreBEMModifier ?? false,
2326
captureClassesFromDoc:
2427
captureClassesFromDoc?.map((s) => toRegExp(s, "g")) ?? [],
28+
extraClassAttributes: extraClassAttributes ?? [],
2529
};
2630
}

‎lib/rules/no-unused-selector.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,14 @@ export = {
123123
checkUnscoped: {
124124
type: "boolean",
125125
},
126+
extraClassAttributes: {
127+
type: "array",
128+
items: {
129+
type: "string",
130+
},
131+
minItems: 0,
132+
uniqueItems: true,
133+
},
126134
},
127135
additionalProperties: false,
128136
},

‎lib/styles/selectors/query/attribute-tracker.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,15 @@ export function getAttributeValueNodes(
1616
context: RuleContext,
1717
): AttributeValueExpressions[] | null {
1818
const results: AttributeValueExpressions[] = [];
19+
const lowedName = name.toLowerCase();
1920
const { startTag } = element;
2021
for (const attr of startTag.attributes) {
2122
if (!isVDirective(attr)) {
2223
const { key, value } = attr;
2324
if (value == null) {
2425
continue;
2526
}
26-
if (key.name === name) {
27+
if (key.name === lowedName) {
2728
results.push(value);
2829
}
2930
} else {
@@ -39,7 +40,7 @@ export function getAttributeValueNodes(
3940
// bind name is unknown.
4041
return null;
4142
}
42-
if (bindArg !== name) {
43+
if (bindArg !== lowedName) {
4344
continue;
4445
}
4546
const { expression } = value;

‎lib/styles/selectors/query/index.ts‎

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import lodash from "lodash";
12
import {
23
isTypeSelector,
34
isIDSelector,
@@ -762,16 +763,13 @@ function matchClassName(
762763
return true;
763764
}
764765
}
765-
const nodes = getAttributeValueNodes(element, "class", document.context);
766-
if (nodes == null) {
767-
return true;
768-
}
769-
for (const node of nodes) {
770-
if (node.type === "VLiteral") {
771-
if (includesClassName(node.value, className)) {
772-
return true;
773-
}
774-
} else if (matchClassNameExpression(node, className, document)) {
766+
767+
const uniquedAttrs = lodash.uniq([
768+
"class",
769+
...document.options.extraClassAttributes,
770+
]);
771+
for (const attrName of uniquedAttrs) {
772+
if (matchClassNameForAttribute(element, attrName, className, document)) {
775773
return true;
776774
}
777775
}
@@ -793,6 +791,31 @@ function matchClassName(
793791
return false;
794792
}
795793

794+
/**
795+
* Checks whether the given element matches the given class name for a specific attribute.
796+
*/
797+
function matchClassNameForAttribute(
798+
element: AST.VElement,
799+
attrName: string,
800+
className: Template,
801+
document: VueDocumentQueryContext,
802+
): boolean {
803+
const nodes = getAttributeValueNodes(element, attrName, document.context);
804+
if (nodes == null) {
805+
return true;
806+
}
807+
for (const node of nodes) {
808+
if (node.type === "VLiteral") {
809+
if (includesClassName(node.value, className)) {
810+
return true;
811+
}
812+
} else if (matchClassNameExpression(node, className, document)) {
813+
return true;
814+
}
815+
}
816+
return false;
817+
}
818+
796819
/**
797820
* Gets the ref name.
798821
*/

‎tests/lib/rules/no-unused-selector.ts‎

Lines changed: 102 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,7 @@ tester.run("no-unused-selector", rule as any, {
326326
</template>
327327
<style scoped lang="scss">
328328
/* ✓ GOOD */
329-
329+
330330
// A button suitable for giving a star to someone.
331331
//
332332
// :hover - Subtle hover highlight.
@@ -473,6 +473,106 @@ tester.run("no-unused-selector", rule as any, {
473473
}
474474
</style>
475475
`,
476+
// extraClassAttributes option
477+
{
478+
code: `
479+
<template>
480+
<div hover-class="foo"></div>
481+
</template>
482+
<style scoped>
483+
.foo {}
484+
</style>
485+
`,
486+
options: [{ extraClassAttributes: ["hover-class"] }],
487+
},
488+
{
489+
code: `
490+
<template>
491+
<div :hover-class="dynamicClass"></div>
492+
</template>
493+
<style scoped>
494+
.foo {}
495+
</style>
496+
<script>
497+
export default {
498+
data () {
499+
return {
500+
dynamicClass: 'foo'
501+
}
502+
}
503+
}
504+
</script>
505+
`,
506+
options: [{ extraClassAttributes: ["hover-class"] }],
507+
},
508+
{
509+
code: `
510+
<template>
511+
<div hover-class="foo" placeholder-class="bar"></div>
512+
</template>
513+
<style scoped>
514+
.foo {}
515+
.bar {}
516+
</style>
517+
`,
518+
options: [{ extraClassAttributes: ["hover-class", "placeholder-class"] }],
519+
},
520+
{
521+
code: `
522+
<template>
523+
<div :hover-class="['foo', 'bar']" :placeholder-class="{baz: true}"></div>
524+
</template>
525+
<style scoped>
526+
.foo {}
527+
.bar {}
528+
.baz {}
529+
</style>
530+
`,
531+
options: [{ extraClassAttributes: ["hover-class", "placeholder-class"] }],
532+
},
533+
{
534+
code: `
535+
<template>
536+
<div data-class="foo"></div>
537+
</template>
538+
<style scoped>
539+
.foo {}
540+
</style>
541+
`,
542+
options: [{ extraClassAttributes: ["data-class"] }],
543+
},
544+
{
545+
code: `
546+
<template>
547+
<div v-bind:data-class="dynamicClass"></div>
548+
</template>
549+
<style scoped>
550+
.foo {}
551+
</style>
552+
<script>
553+
export default {
554+
data () {
555+
return {
556+
dynamicClass: 'foo'
557+
}
558+
}
559+
}
560+
</script>
561+
`,
562+
options: [{ extraClassAttributes: ["data-class"] }],
563+
},
564+
{
565+
code: `
566+
<template>
567+
<div class="foo" hover-class="bar"></div>
568+
</template>
569+
<style scoped>
570+
.foo {}
571+
.bar {}
572+
</style>
573+
`,
574+
options: [{ extraClassAttributes: ["hover-class"] }],
575+
},
476576
],
477577
invalid: [
478578
{
@@ -756,7 +856,7 @@ tester.run("no-unused-selector", rule as any, {
756856
</template>
757857
<style scoped lang="scss">
758858
/* ✓ GOOD */
759-
859+
760860
// A button suitable for giving a star to someone.
761861
//
762862
// :hover - Subtle hover highlight.

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /