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

Add extraClassAttributes option to match additional class-like attributes in templates #413

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

Merged
ota-meshi merged 4 commits into future-architect:master from toFrankie:master
Sep 4, 2025
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/afraid-bikes-smile.md
View file Open in desktop
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eslint-plugin-vue-scoped-css": minor
---

Add `extraClassAttributes` option to match additional class-like attributes in templates, to `vue-scoped-css/no-unused-selector` rule
34 changes: 34 additions & 0 deletions docs/rules/no-unused-selector.md
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,15 @@ This is a limitation of this rule. Without this limitation, the root element can
"ignoreBEMModifier": false,
"captureClassesFromDoc": [],
"checkUnscoped": false,
"extraClassAttributes": [],
}]
}
```

- `ignoreBEMModifier` ... Set `true` if you want to ignore the `BEM` modifier. Default is false.
- `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.
- `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.
- `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.

### `"ignoreBEMModifier": true`

Expand Down Expand Up @@ -149,6 +151,38 @@ a.button.star {

</eslint-code-block>

### `"extraClassAttributes": ["hover-class", "placeholder-class"]`

<eslint-code-block :rules="{'vue-scoped-css/no-unused-selector': ['error', {extraClassAttributes: ['hover-class', 'placeholder-class']}]}">

```vue
<template>
<div>
<!-- These attributes will be checked for class names -->
<button class="button" hover-class="button-hover">Button</button>
<input placeholder-class="input-placeholder" >
</div>
</template>
<style scoped>
/* ✓ GOOD - These selectors are used in custom attributes */
.button {}
.button-hover {}
.input-placeholder {}

/* ✗ BAD - This selector is not used anywhere */
.unused-class {}
</style>
<script>
export default {
data() {
return {
dynamicHoverClass: 'button-hover',
}
},
}
</script>
```

## :books: Further reading

- [vue-scoped-css/require-selector-used-inside]
Expand Down
6 changes: 5 additions & 1 deletion lib/options.ts
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import { toRegExp } from "./utils/regexp";
export interface QueryOptions {
ignoreBEMModifier?: boolean;
captureClassesFromDoc?: string[];
extraClassAttributes?: string[];
}

export interface ParsedQueryOptions {
ignoreBEMModifier: boolean;
captureClassesFromDoc: RegExp[];
extraClassAttributes: string[];
}

/**
Expand All @@ -16,11 +18,13 @@ export interface ParsedQueryOptions {
export function parseQueryOptions(
options: QueryOptions | undefined,
): ParsedQueryOptions {
const { ignoreBEMModifier, captureClassesFromDoc } = options || {};
const { ignoreBEMModifier, captureClassesFromDoc, extraClassAttributes } =
options || {};

return {
ignoreBEMModifier: ignoreBEMModifier ?? false,
captureClassesFromDoc:
captureClassesFromDoc?.map((s) => toRegExp(s, "g")) ?? [],
extraClassAttributes: extraClassAttributes ?? [],
};
}
8 changes: 8 additions & 0 deletions lib/rules/no-unused-selector.ts
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,14 @@ export = {
checkUnscoped: {
type: "boolean",
},
extraClassAttributes: {
type: "array",
items: {
type: "string",
},
minItems: 0,
uniqueItems: true,
},
},
additionalProperties: false,
},
Expand Down
5 changes: 3 additions & 2 deletions lib/styles/selectors/query/attribute-tracker.ts
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,15 @@ export function getAttributeValueNodes(
context: RuleContext,
): AttributeValueExpressions[] | null {
const results: AttributeValueExpressions[] = [];
const lowedName = name.toLowerCase();
const { startTag } = element;
for (const attr of startTag.attributes) {
if (!isVDirective(attr)) {
const { key, value } = attr;
if (value == null) {
continue;
}
if (key.name === name) {
if (key.name === lowedName) {
results.push(value);
}
} else {
Expand All @@ -39,7 +40,7 @@ export function getAttributeValueNodes(
// bind name is unknown.
return null;
}
if (bindArg !== name) {
if (bindArg !== lowedName) {
continue;
}
const { expression } = value;
Expand Down
43 changes: 33 additions & 10 deletions lib/styles/selectors/query/index.ts
View file Open in desktop
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import lodash from "lodash";
import {
isTypeSelector,
isIDSelector,
Expand Down Expand Up @@ -762,16 +763,13 @@ function matchClassName(
return true;
}
}
const nodes = getAttributeValueNodes(element, "class", document.context);
if (nodes == null) {
return true;
}
for (const node of nodes) {
if (node.type === "VLiteral") {
if (includesClassName(node.value, className)) {
return true;
}
} else if (matchClassNameExpression(node, className, document)) {

const uniquedAttrs = lodash.uniq([
"class",
...document.options.extraClassAttributes,
]);
for (const attrName of uniquedAttrs) {
if (matchClassNameForAttribute(element, attrName, className, document)) {
return true;
}
}
Expand All @@ -793,6 +791,31 @@ function matchClassName(
return false;
}

/**
* Checks whether the given element matches the given class name for a specific attribute.
*/
function matchClassNameForAttribute(
element: AST.VElement,
attrName: string,
className: Template,
document: VueDocumentQueryContext,
): boolean {
const nodes = getAttributeValueNodes(element, attrName, document.context);
if (nodes == null) {
return true;
}
for (const node of nodes) {
if (node.type === "VLiteral") {
if (includesClassName(node.value, className)) {
return true;
}
} else if (matchClassNameExpression(node, className, document)) {
return true;
}
}
return false;
}

/**
* Gets the ref name.
*/
Expand Down
104 changes: 102 additions & 2 deletions tests/lib/rules/no-unused-selector.ts
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@ tester.run("no-unused-selector", rule as any, {
</template>
<style scoped lang="scss">
/* ✓ GOOD */

// A button suitable for giving a star to someone.
//
// :hover - Subtle hover highlight.
Expand Down Expand Up @@ -473,6 +473,106 @@ tester.run("no-unused-selector", rule as any, {
}
</style>
`,
// extraClassAttributes option
{
code: `
<template>
<div hover-class="foo"></div>
</template>
<style scoped>
.foo {}
</style>
`,
options: [{ extraClassAttributes: ["hover-class"] }],
},
{
code: `
<template>
<div :hover-class="dynamicClass"></div>
</template>
<style scoped>
.foo {}
</style>
<script>
export default {
data () {
return {
dynamicClass: 'foo'
}
}
}
</script>
`,
options: [{ extraClassAttributes: ["hover-class"] }],
},
{
code: `
<template>
<div hover-class="foo" placeholder-class="bar"></div>
</template>
<style scoped>
.foo {}
.bar {}
</style>
`,
options: [{ extraClassAttributes: ["hover-class", "placeholder-class"] }],
},
{
code: `
<template>
<div :hover-class="['foo', 'bar']" :placeholder-class="{baz: true}"></div>
</template>
<style scoped>
.foo {}
.bar {}
.baz {}
</style>
`,
options: [{ extraClassAttributes: ["hover-class", "placeholder-class"] }],
},
{
code: `
<template>
<div data-class="foo"></div>
</template>
<style scoped>
.foo {}
</style>
`,
options: [{ extraClassAttributes: ["data-class"] }],
},
{
code: `
<template>
<div v-bind:data-class="dynamicClass"></div>
</template>
<style scoped>
.foo {}
</style>
<script>
export default {
data () {
return {
dynamicClass: 'foo'
}
}
}
</script>
`,
options: [{ extraClassAttributes: ["data-class"] }],
},
{
code: `
<template>
<div class="foo" hover-class="bar"></div>
</template>
<style scoped>
.foo {}
.bar {}
</style>
`,
options: [{ extraClassAttributes: ["hover-class"] }],
},
],
invalid: [
{
Expand Down Expand Up @@ -756,7 +856,7 @@ tester.run("no-unused-selector", rule as any, {
</template>
<style scoped lang="scss">
/* ✓ GOOD */

// A button suitable for giving a star to someone.
//
// :hover - Subtle hover highlight.
Expand Down
Loading

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