diff --git a/lib/rules/selector-attribute-quotes/README.md b/lib/rules/selector-attribute-quotes/README.md index 38187fdf6c..c62a300de2 100644 --- a/lib/rules/selector-attribute-quotes/README.md +++ b/lib/rules/selector-attribute-quotes/README.md @@ -9,6 +9,8 @@ Require or disallow quotes for attribute values. * These quotes */ ``` +The [`fix` option](../../../docs/user-guide/usage/options.md#fix) can automatically fix most of the problems reported by this rule. + ## Options `string`: `"always"|"never"` diff --git a/lib/rules/selector-attribute-quotes/__tests__/index.js b/lib/rules/selector-attribute-quotes/__tests__/index.js index 30ff4f94ec..03ecf4829e 100644 --- a/lib/rules/selector-attribute-quotes/__tests__/index.js +++ b/lib/rules/selector-attribute-quotes/__tests__/index.js @@ -6,6 +6,7 @@ testRule({ ruleName, config: ['always'], skipBasicChecks: true, + fix: true, accept: [ { @@ -54,51 +55,87 @@ testRule({ code: 'html { --custom-property-set: {} }', description: 'custom property set in selector', }, + { + code: `a[href="te's't"] { }`, + description: 'double-quoted attribute contains single quote', + }, + { + code: `a[href='te"s"t'] { }`, + description: 'single-quoted attribute contains double quote', + }, ], reject: [ { code: 'a[title=flower] { }', + fixed: 'a[title="flower"] { }', message: messages.expected('flower'), line: 1, column: 9, }, { code: 'a[ title=flower ] { }', + fixed: 'a[ title="flower" ] { }', message: messages.expected('flower'), line: 1, column: 10, }, { code: '[class^=top] { }', + fixed: '[class^="top"] { }', message: messages.expected('top'), line: 1, column: 9, }, { code: '[class ^= top] { }', + fixed: '[class ^= "top"] { }', message: messages.expected('top'), line: 1, column: 11, }, { code: '[frame=hsides i] { }', + fixed: '[frame="hsides" i] { }', message: messages.expected('hsides'), line: 1, column: 8, }, { code: '[data-style=value][data-loading] { }', + fixed: '[data-style="value"][data-loading] { }', message: messages.expected('value'), line: 1, column: 13, }, + { + code: `[href=te\\'s\\"t] { }`, + fixed: `[href="te's\\"t"] { }`, + message: messages.expected(`te's"t`), + line: 1, + column: 7, + }, + { + code: '[href=\\"test\\"] { }', + fixed: '[href="\\"test\\""] { }', + message: messages.expected('"test"'), + line: 1, + column: 7, + }, + { + code: "[href=\\'test\\'] { }", + fixed: `[href="'test'"] { }`, + message: messages.expected("'test'"), + line: 1, + column: 7, + }, ], }); testRule({ ruleName, config: ['never'], + fix: true, accept: [ { @@ -116,63 +153,119 @@ testRule({ { code: '[data-style=value][data-loading] { }', }, + { + code: `a[href=te\\'s\\"t] { }`, + description: 'attribute contains inner quotes', + }, + { + code: '[href=\\"test\\"] { }', + description: 'escaped double-quotes are not considered as framing quotes', + }, + { + code: "[href=\\'test\\'] { }", + description: 'escaped single-quotes are not considered as framing quotes', + }, ], reject: [ { code: 'a[target="_blank"] { }', + fixed: 'a[target=_blank] { }', message: messages.rejected('_blank'), line: 1, column: 10, }, { code: 'a[ target="_blank" ] { }', + fixed: 'a[ target=_blank ] { }', message: messages.rejected('_blank'), line: 1, column: 11, }, { code: '[class|="top"] { }', + fixed: '[class|=top] { }', message: messages.rejected('top'), line: 1, column: 9, }, { code: '[class |= "top"] { }', + fixed: '[class |= top] { }', message: messages.rejected('top'), line: 1, column: 11, }, { code: "[title~='text'] { }", + fixed: '[title~=text] { }', message: messages.rejected('text'), line: 1, column: 9, }, { code: "[data-attribute='component'] { }", + fixed: '[data-attribute=component] { }', message: messages.rejected('component'), line: 1, column: 17, }, { code: '[frame="hsides" i] { }', + fixed: '[frame=hsides i] { }', message: messages.rejected('hsides'), line: 1, column: 8, }, { code: "[frame='hsides' i] { }", + fixed: '[frame=hsides i] { }', message: messages.rejected('hsides'), line: 1, column: 8, }, { code: "[data-style='value'][data-loading] { }", + fixed: '[data-style=value][data-loading] { }', message: messages.rejected('value'), line: 1, column: 13, }, + { + code: `[href="te'st"] { }`, + fixed: "[href=te\\'st] { }", + message: messages.rejected("te'st"), + line: 1, + column: 7, + }, + { + code: `[href='te"st'] { }`, + fixed: '[href=te\\"st] { }', + message: messages.rejected('te"st'), + line: 1, + column: 7, + }, + { + code: "[href='te\\'s\\'t'] { }", + fixed: "[href=te\\'s\\'t] { }", + message: messages.rejected("te's't"), + line: 1, + column: 7, + }, + { + code: '[href="te\\"s\\"t"] { }', + fixed: '[href=te\\"s\\"t] { }', + message: messages.rejected('te"s"t'), + line: 1, + column: 7, + }, + { + code: 'a[target="_blank"], /* comment */ a { }', + fixed: 'a[target=_blank], /* comment */ a { }', + message: messages.rejected('_blank'), + line: 1, + column: 10, + }, ], }); diff --git a/lib/rules/selector-attribute-quotes/index.js b/lib/rules/selector-attribute-quotes/index.js index ea1df10d91..b1c4db448f 100644 --- a/lib/rules/selector-attribute-quotes/index.js +++ b/lib/rules/selector-attribute-quotes/index.js @@ -2,6 +2,7 @@ 'use strict'; +const getRuleSelector = require('../../utils/getRuleSelector'); const isStandardSyntaxRule = require('../../utils/isStandardSyntaxRule'); const parseSelector = require('../../utils/parseSelector'); const report = require('../../utils/report'); @@ -15,7 +16,9 @@ const messages = ruleMessages(ruleName, { rejected: (value) => `Unexpected quotes around "${value}"`, }); -function rule(expectation) { +const acceptedQuoteMark = '"'; + +function rule(expectation, secondary, context) { return (root, result) => { const validOptions = validateOptions(result, ruleName, { actual: expectation, @@ -35,26 +38,42 @@ function rule(expectation) { return; } - parseSelector(ruleNode.selector, result, ruleNode, (selectorTree) => { + parseSelector(getRuleSelector(ruleNode), result, ruleNode, (selectorTree) => { + let selectorFixed = false; + selectorTree.walkAttributes((attributeNode) => { if (!attributeNode.operator) { return; } if (!attributeNode.quoted && expectation === 'always') { - complain( - messages.expected(attributeNode.value), - attributeNode.sourceIndex + attributeNode.offsetOf('value'), - ); + if (context.fix) { + selectorFixed = true; + attributeNode.quoteMark = acceptedQuoteMark; + } else { + complain( + messages.expected(attributeNode.value), + attributeNode.sourceIndex + attributeNode.offsetOf('value'), + ); + } } if (attributeNode.quoted && expectation === 'never') { - complain( - messages.rejected(attributeNode.value), - attributeNode.sourceIndex + attributeNode.offsetOf('value'), - ); + if (context.fix) { + selectorFixed = true; + attributeNode.quoteMark = null; + } else { + complain( + messages.rejected(attributeNode.value), + attributeNode.sourceIndex + attributeNode.offsetOf('value'), + ); + } } }); + + if (selectorFixed) { + ruleNode.selector = selectorTree.toString(); + } }); function complain(message, index) { diff --git a/lib/utils/getRuleSelector.js b/lib/utils/getRuleSelector.js new file mode 100644 index 0000000000..715cf1e350 --- /dev/null +++ b/lib/utils/getRuleSelector.js @@ -0,0 +1,13 @@ +'use strict'; + +const _ = require('lodash'); + +/** + * @param {import('postcss').Rule} ruleNode + * @returns {string} + */ +function getRuleSelector(ruleNode) { + return _.get(ruleNode, 'raws.selector.raw', ruleNode.selector); +} + +module.exports = getRuleSelector;