From c2ae703239b211b2f0e893e603b5184a227fca8f Mon Sep 17 00:00:00 2001 From: doing-art Date: Wed, 14 Apr 2021 22:15:48 +0300 Subject: [PATCH 1/5] Add autofix to selector-attribute-quotes --- lib/rules/selector-attribute-quotes/index.js | 36 +++++++++++++++----- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/lib/rules/selector-attribute-quotes/index.js b/lib/rules/selector-attribute-quotes/index.js index ea1df10d91..0069c7aeba 100644 --- a/lib/rules/selector-attribute-quotes/index.js +++ b/lib/rules/selector-attribute-quotes/index.js @@ -15,7 +15,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, @@ -36,25 +38,41 @@ function rule(expectation) { } parseSelector(ruleNode.selector, 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) { From fd2596cac3150150e56e571a052ce63773a3ffed Mon Sep 17 00:00:00 2001 From: doing-art Date: Wed, 14 Apr 2021 23:21:55 +0300 Subject: [PATCH 2/5] Cover autofix of selector-attribute-quotes rule with unit tests --- .../__tests__/index.js | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/lib/rules/selector-attribute-quotes/__tests__/index.js b/lib/rules/selector-attribute-quotes/__tests__/index.js index 30ff4f94ec..a795942482 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,41 +55,55 @@ 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, @@ -99,6 +114,7 @@ testRule({ testRule({ ruleName, config: ['never'], + fix: true, accept: [ { @@ -116,59 +132,71 @@ testRule({ { code: '[data-style=value][data-loading] { }', }, + { + code: `a[href=te\\'s\\"t] { }`, + }, ], 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, From ce7575e2be1df8566e314a53d8a627e2285328a6 Mon Sep 17 00:00:00 2001 From: doing-art Date: Thu, 15 Apr 2021 10:17:14 +0300 Subject: [PATCH 3/5] Cover autofix of selector-attribute-quotes rule with additional tests --- .../__tests__/index.js | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/lib/rules/selector-attribute-quotes/__tests__/index.js b/lib/rules/selector-attribute-quotes/__tests__/index.js index a795942482..f10e2f569c 100644 --- a/lib/rules/selector-attribute-quotes/__tests__/index.js +++ b/lib/rules/selector-attribute-quotes/__tests__/index.js @@ -108,6 +108,27 @@ testRule({ 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, + }, ], }); @@ -134,6 +155,15 @@ testRule({ }, { 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', }, ], @@ -201,6 +231,34 @@ testRule({ 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, + }, ], }); From d022e8ab9526c4b215a3883c426e538406a8dab0 Mon Sep 17 00:00:00 2001 From: doing-art Date: Thu, 15 Apr 2021 10:24:37 +0300 Subject: [PATCH 4/5] Update documentation of selector-attribute-quotes rule --- lib/rules/selector-attribute-quotes/README.md | 2 ++ 1 file changed, 2 insertions(+) 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"` From 24f89d947f7c3dcd922a2e2d90993b805bd5873b Mon Sep 17 00:00:00 2001 From: doing-art Date: Wed, 21 Apr 2021 20:35:11 +0300 Subject: [PATCH 5/5] Add getRuleSelector util --- .../selector-attribute-quotes/__tests__/index.js | 7 +++++++ lib/rules/selector-attribute-quotes/index.js | 3 ++- lib/utils/getRuleSelector.js | 13 +++++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 lib/utils/getRuleSelector.js diff --git a/lib/rules/selector-attribute-quotes/__tests__/index.js b/lib/rules/selector-attribute-quotes/__tests__/index.js index f10e2f569c..03ecf4829e 100644 --- a/lib/rules/selector-attribute-quotes/__tests__/index.js +++ b/lib/rules/selector-attribute-quotes/__tests__/index.js @@ -259,6 +259,13 @@ testRule({ 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 0069c7aeba..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'); @@ -37,7 +38,7 @@ function rule(expectation, secondary, context) { return; } - parseSelector(ruleNode.selector, result, ruleNode, (selectorTree) => { + parseSelector(getRuleSelector(ruleNode), result, ruleNode, (selectorTree) => { let selectorFixed = false; selectorTree.walkAttributes((attributeNode) => { 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;