Skip to content

Commit 2a35e1a

Browse files
authored
Add autofix to selector-attribute-quotes (#5248)
1 parent 911b196 commit 2a35e1a

File tree

4 files changed

+137
-10
lines changed

4 files changed

+137
-10
lines changed

lib/rules/selector-attribute-quotes/README.md

+2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ Require or disallow quotes for attribute values.
99
* These quotes */
1010
```
1111

12+
The [`fix` option](../../../docs/user-guide/usage/options.md#fix) can automatically fix most of the problems reported by this rule.
13+
1214
## Options
1315

1416
`string`: `"always"|"never"`

lib/rules/selector-attribute-quotes/__tests__/index.js

+93
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ testRule({
66
ruleName,
77
config: ['always'],
88
skipBasicChecks: true,
9+
fix: true,
910

1011
accept: [
1112
{
@@ -54,51 +55,87 @@ testRule({
5455
code: 'html { --custom-property-set: {} }',
5556
description: 'custom property set in selector',
5657
},
58+
{
59+
code: `a[href="te's't"] { }`,
60+
description: 'double-quoted attribute contains single quote',
61+
},
62+
{
63+
code: `a[href='te"s"t'] { }`,
64+
description: 'single-quoted attribute contains double quote',
65+
},
5766
],
5867

5968
reject: [
6069
{
6170
code: 'a[title=flower] { }',
71+
fixed: 'a[title="flower"] { }',
6272
message: messages.expected('flower'),
6373
line: 1,
6474
column: 9,
6575
},
6676
{
6777
code: 'a[ title=flower ] { }',
78+
fixed: 'a[ title="flower" ] { }',
6879
message: messages.expected('flower'),
6980
line: 1,
7081
column: 10,
7182
},
7283
{
7384
code: '[class^=top] { }',
85+
fixed: '[class^="top"] { }',
7486
message: messages.expected('top'),
7587
line: 1,
7688
column: 9,
7789
},
7890
{
7991
code: '[class ^= top] { }',
92+
fixed: '[class ^= "top"] { }',
8093
message: messages.expected('top'),
8194
line: 1,
8295
column: 11,
8396
},
8497
{
8598
code: '[frame=hsides i] { }',
99+
fixed: '[frame="hsides" i] { }',
86100
message: messages.expected('hsides'),
87101
line: 1,
88102
column: 8,
89103
},
90104
{
91105
code: '[data-style=value][data-loading] { }',
106+
fixed: '[data-style="value"][data-loading] { }',
92107
message: messages.expected('value'),
93108
line: 1,
94109
column: 13,
95110
},
111+
{
112+
code: `[href=te\\'s\\"t] { }`,
113+
fixed: `[href="te's\\"t"] { }`,
114+
message: messages.expected(`te's"t`),
115+
line: 1,
116+
column: 7,
117+
},
118+
{
119+
code: '[href=\\"test\\"] { }',
120+
fixed: '[href="\\"test\\""] { }',
121+
message: messages.expected('"test"'),
122+
line: 1,
123+
column: 7,
124+
},
125+
{
126+
code: "[href=\\'test\\'] { }",
127+
fixed: `[href="'test'"] { }`,
128+
message: messages.expected("'test'"),
129+
line: 1,
130+
column: 7,
131+
},
96132
],
97133
});
98134

99135
testRule({
100136
ruleName,
101137
config: ['never'],
138+
fix: true,
102139

103140
accept: [
104141
{
@@ -116,63 +153,119 @@ testRule({
116153
{
117154
code: '[data-style=value][data-loading] { }',
118155
},
156+
{
157+
code: `a[href=te\\'s\\"t] { }`,
158+
description: 'attribute contains inner quotes',
159+
},
160+
{
161+
code: '[href=\\"test\\"] { }',
162+
description: 'escaped double-quotes are not considered as framing quotes',
163+
},
164+
{
165+
code: "[href=\\'test\\'] { }",
166+
description: 'escaped single-quotes are not considered as framing quotes',
167+
},
119168
],
120169

121170
reject: [
122171
{
123172
code: 'a[target="_blank"] { }',
173+
fixed: 'a[target=_blank] { }',
124174
message: messages.rejected('_blank'),
125175
line: 1,
126176
column: 10,
127177
},
128178
{
129179
code: 'a[ target="_blank" ] { }',
180+
fixed: 'a[ target=_blank ] { }',
130181
message: messages.rejected('_blank'),
131182
line: 1,
132183
column: 11,
133184
},
134185
{
135186
code: '[class|="top"] { }',
187+
fixed: '[class|=top] { }',
136188
message: messages.rejected('top'),
137189
line: 1,
138190
column: 9,
139191
},
140192
{
141193
code: '[class |= "top"] { }',
194+
fixed: '[class |= top] { }',
142195
message: messages.rejected('top'),
143196
line: 1,
144197
column: 11,
145198
},
146199
{
147200
code: "[title~='text'] { }",
201+
fixed: '[title~=text] { }',
148202
message: messages.rejected('text'),
149203
line: 1,
150204
column: 9,
151205
},
152206
{
153207
code: "[data-attribute='component'] { }",
208+
fixed: '[data-attribute=component] { }',
154209
message: messages.rejected('component'),
155210
line: 1,
156211
column: 17,
157212
},
158213
{
159214
code: '[frame="hsides" i] { }',
215+
fixed: '[frame=hsides i] { }',
160216
message: messages.rejected('hsides'),
161217
line: 1,
162218
column: 8,
163219
},
164220
{
165221
code: "[frame='hsides' i] { }",
222+
fixed: '[frame=hsides i] { }',
166223
message: messages.rejected('hsides'),
167224
line: 1,
168225
column: 8,
169226
},
170227
{
171228
code: "[data-style='value'][data-loading] { }",
229+
fixed: '[data-style=value][data-loading] { }',
172230
message: messages.rejected('value'),
173231
line: 1,
174232
column: 13,
175233
},
234+
{
235+
code: `[href="te'st"] { }`,
236+
fixed: "[href=te\\'st] { }",
237+
message: messages.rejected("te'st"),
238+
line: 1,
239+
column: 7,
240+
},
241+
{
242+
code: `[href='te"st'] { }`,
243+
fixed: '[href=te\\"st] { }',
244+
message: messages.rejected('te"st'),
245+
line: 1,
246+
column: 7,
247+
},
248+
{
249+
code: "[href='te\\'s\\'t'] { }",
250+
fixed: "[href=te\\'s\\'t] { }",
251+
message: messages.rejected("te's't"),
252+
line: 1,
253+
column: 7,
254+
},
255+
{
256+
code: '[href="te\\"s\\"t"] { }',
257+
fixed: '[href=te\\"s\\"t] { }',
258+
message: messages.rejected('te"s"t'),
259+
line: 1,
260+
column: 7,
261+
},
262+
{
263+
code: 'a[target="_blank"], /* comment */ a { }',
264+
fixed: 'a[target=_blank], /* comment */ a { }',
265+
message: messages.rejected('_blank'),
266+
line: 1,
267+
column: 10,
268+
},
176269
],
177270
});
178271

lib/rules/selector-attribute-quotes/index.js

+29-10
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
'use strict';
44

5+
const getRuleSelector = require('../../utils/getRuleSelector');
56
const isStandardSyntaxRule = require('../../utils/isStandardSyntaxRule');
67
const parseSelector = require('../../utils/parseSelector');
78
const report = require('../../utils/report');
@@ -15,7 +16,9 @@ const messages = ruleMessages(ruleName, {
1516
rejected: (value) => `Unexpected quotes around "${value}"`,
1617
});
1718

18-
function rule(expectation) {
19+
const acceptedQuoteMark = '"';
20+
21+
function rule(expectation, secondary, context) {
1922
return (root, result) => {
2023
const validOptions = validateOptions(result, ruleName, {
2124
actual: expectation,
@@ -35,26 +38,42 @@ function rule(expectation) {
3538
return;
3639
}
3740

38-
parseSelector(ruleNode.selector, result, ruleNode, (selectorTree) => {
41+
parseSelector(getRuleSelector(ruleNode), result, ruleNode, (selectorTree) => {
42+
let selectorFixed = false;
43+
3944
selectorTree.walkAttributes((attributeNode) => {
4045
if (!attributeNode.operator) {
4146
return;
4247
}
4348

4449
if (!attributeNode.quoted && expectation === 'always') {
45-
complain(
46-
messages.expected(attributeNode.value),
47-
attributeNode.sourceIndex + attributeNode.offsetOf('value'),
48-
);
50+
if (context.fix) {
51+
selectorFixed = true;
52+
attributeNode.quoteMark = acceptedQuoteMark;
53+
} else {
54+
complain(
55+
messages.expected(attributeNode.value),
56+
attributeNode.sourceIndex + attributeNode.offsetOf('value'),
57+
);
58+
}
4959
}
5060

5161
if (attributeNode.quoted && expectation === 'never') {
52-
complain(
53-
messages.rejected(attributeNode.value),
54-
attributeNode.sourceIndex + attributeNode.offsetOf('value'),
55-
);
62+
if (context.fix) {
63+
selectorFixed = true;
64+
attributeNode.quoteMark = null;
65+
} else {
66+
complain(
67+
messages.rejected(attributeNode.value),
68+
attributeNode.sourceIndex + attributeNode.offsetOf('value'),
69+
);
70+
}
5671
}
5772
});
73+
74+
if (selectorFixed) {
75+
ruleNode.selector = selectorTree.toString();
76+
}
5877
});
5978

6079
function complain(message, index) {

lib/utils/getRuleSelector.js

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
'use strict';
2+
3+
const _ = require('lodash');
4+
5+
/**
6+
* @param {import('postcss').Rule} ruleNode
7+
* @returns {string}
8+
*/
9+
function getRuleSelector(ruleNode) {
10+
return _.get(ruleNode, 'raws.selector.raw', ruleNode.selector);
11+
}
12+
13+
module.exports = getRuleSelector;

0 commit comments

Comments
 (0)