diff --git a/.changeset/rude-dryers-fry.md b/.changeset/rude-dryers-fry.md new file mode 100644 index 0000000000..bf8c2bf790 --- /dev/null +++ b/.changeset/rude-dryers-fry.md @@ -0,0 +1,5 @@ +--- +"stylelint": minor +--- + +Added: `function-url-quotes` autofix diff --git a/docs/user-guide/rules.md b/docs/user-guide/rules.md index 3d30827be2..0350b7adf6 100644 --- a/docs/user-guide/rules.md +++ b/docs/user-guide/rules.md @@ -238,7 +238,7 @@ Enforce naming conventions with these `pattern` rules. Require or disallow quotes with these `quotes` rules. - [`font-family-name-quotes`](../../lib/rules/font-family-name-quotes/README.md): Require or disallow quotes for font family names (Autofixable). -- [`function-url-quotes`](../../lib/rules/function-url-quotes/README.md): Require or disallow quotes for urls. +- [`function-url-quotes`](../../lib/rules/function-url-quotes/README.md): Require or disallow quotes for urls (Autofixable). - [`selector-attribute-quotes`](../../lib/rules/selector-attribute-quotes/README.md): Require or disallow quotes for attribute values (Autofixable). ### Redundant diff --git a/lib/rules/function-url-quotes/README.md b/lib/rules/function-url-quotes/README.md index 344a75217d..714b132d52 100644 --- a/lib/rules/function-url-quotes/README.md +++ b/lib/rules/function-url-quotes/README.md @@ -9,6 +9,8 @@ a { background: url("x.jpg") } * 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/function-url-quotes/__tests__/index.js b/lib/rules/function-url-quotes/__tests__/index.js index aa6393996a..ec2ec574a9 100644 --- a/lib/rules/function-url-quotes/__tests__/index.js +++ b/lib/rules/function-url-quotes/__tests__/index.js @@ -5,6 +5,7 @@ const { messages, ruleName } = require('..'); testRule({ ruleName, config: ['always'], + fix: true, accept: [ { @@ -180,6 +181,7 @@ testRule({ reject: [ { code: '@import url(foo.css);', + fixed: '@import url("foo.css");', message: messages.expected('url'), line: 1, column: 13, @@ -188,6 +190,7 @@ testRule({ }, { code: '@import url( foo.css );', + fixed: '@import url( "foo.css" );', message: messages.expected('url'), line: 1, column: 14, @@ -196,6 +199,7 @@ testRule({ }, { code: '@document url-prefix(http://www.w3.org/Style);', + fixed: '@document url-prefix("http://www.w3.org/Style");', message: messages.expected('url-prefix'), line: 1, column: 22, @@ -204,6 +208,7 @@ testRule({ }, { code: '@document url-prefix( http://www.w3.org/Style );', + fixed: '@document url-prefix( "http://www.w3.org/Style" );', message: messages.expected('url-prefix'), line: 1, column: 23, @@ -212,6 +217,7 @@ testRule({ }, { code: "@font-face { font-family: 'foo'; src: url(foo.ttf); }", + fixed: '@font-face { font-family: \'foo\'; src: url("foo.ttf"); }', message: messages.expected('url'), line: 1, column: 43, @@ -220,6 +226,7 @@ testRule({ }, { code: "@font-face { font-family: 'foo'; src: url( foo.ttf ); }", + fixed: '@font-face { font-family: \'foo\'; src: url( "foo.ttf" ); }', message: messages.expected('url'), line: 1, column: 44, @@ -228,6 +235,7 @@ testRule({ }, { code: 'a { cursor: url(foo.png); }', + fixed: 'a { cursor: url("foo.png"); }', message: messages.expected('url'), line: 1, column: 17, @@ -236,6 +244,7 @@ testRule({ }, { code: 'a { background-image: url(foo.css), url("bar.css"), url("baz.css"); }', + fixed: 'a { background-image: url("foo.css"), url("bar.css"), url("baz.css"); }', message: messages.expected('url'), line: 1, column: 27, @@ -244,6 +253,7 @@ testRule({ }, { code: 'a { background-image: url( foo.css ), url("bar.css"), url("baz.css"); }', + fixed: 'a { background-image: url( "foo.css" ), url("bar.css"), url("baz.css"); }', message: messages.expected('url'), line: 1, column: 28, @@ -252,6 +262,7 @@ testRule({ }, { code: 'a { background-image: url("foo.css"), url(bar.css), url("baz.css"); }', + fixed: 'a { background-image: url("foo.css"), url("bar.css"), url("baz.css"); }', message: messages.expected('url'), line: 1, column: 43, @@ -260,6 +271,7 @@ testRule({ }, { code: 'a { background-image: url("foo.css"), url( bar.css ), url("baz.css"); }', + fixed: 'a { background-image: url("foo.css"), url( "bar.css" ), url("baz.css"); }', message: messages.expected('url'), line: 1, column: 44, @@ -268,6 +280,7 @@ testRule({ }, { code: 'a { background-image: url("foo.css"), url("bar.css"), url(baz.css); }', + fixed: 'a { background-image: url("foo.css"), url("bar.css"), url("baz.css"); }', message: messages.expected('url'), line: 1, column: 59, @@ -276,6 +289,7 @@ testRule({ }, { code: 'a { background-image: url("foo.css"), url("bar.css"), url( baz.css ); }', + fixed: 'a { background-image: url("foo.css"), url("bar.css"), url( "baz.css" ); }', message: messages.expected('url'), line: 1, column: 60, @@ -288,6 +302,7 @@ testRule({ testRule({ ruleName, config: ['never'], + fix: true, accept: [ { @@ -397,6 +412,7 @@ testRule({ reject: [ { code: '@import url("foo.css");', + fixed: '@import url(foo.css);', message: messages.rejected('url'), line: 1, column: 13, @@ -405,6 +421,7 @@ testRule({ }, { code: '@import uRl("foo.css");', + fixed: '@import uRl(foo.css);', message: messages.rejected('url'), line: 1, column: 13, @@ -413,6 +430,7 @@ testRule({ }, { code: '@import URL("foo.css");', + fixed: '@import URL(foo.css);', message: messages.rejected('url'), line: 1, column: 13, @@ -421,6 +439,7 @@ testRule({ }, { code: '@import url( "foo.css" );', + fixed: '@import url( foo.css );', message: messages.rejected('url'), line: 1, column: 14, @@ -429,6 +448,7 @@ testRule({ }, { code: "@import url('foo.css');", + fixed: '@import url(foo.css);', message: messages.rejected('url'), line: 1, column: 13, @@ -437,6 +457,7 @@ testRule({ }, { code: "@import url( 'foo.css' );", + fixed: '@import url( foo.css );', message: messages.rejected('url'), line: 1, column: 14, @@ -445,6 +466,7 @@ testRule({ }, { code: '@document url("http://www.w3.org/");', + fixed: '@document url(http://www.w3.org/);', message: messages.rejected('url'), line: 1, column: 15, @@ -453,6 +475,7 @@ testRule({ }, { code: '@document url( "http://www.w3.org/" );', + fixed: '@document url( http://www.w3.org/ );', message: messages.rejected('url'), line: 1, column: 16, @@ -461,6 +484,7 @@ testRule({ }, { code: "@document url-prefix('http://www.w3.org/Style');", + fixed: '@document url-prefix(http://www.w3.org/Style);', message: messages.rejected('url-prefix'), line: 1, column: 22, @@ -469,6 +493,7 @@ testRule({ }, { code: "@document url-prefix( 'http://www.w3.org/Style' );", + fixed: '@document url-prefix( http://www.w3.org/Style );', message: messages.rejected('url-prefix'), line: 1, column: 23, @@ -477,6 +502,7 @@ testRule({ }, { code: '@document domain("mozilla.org");', + fixed: '@document domain(mozilla.org);', message: messages.rejected('domain'), line: 1, column: 18, @@ -485,6 +511,7 @@ testRule({ }, { code: '@document domain( "mozilla.org" );', + fixed: '@document domain( mozilla.org );', message: messages.rejected('domain'), line: 1, column: 19, @@ -493,6 +520,7 @@ testRule({ }, { code: "@font-face { font-family: foo; src: url('foo.ttf'); }", + fixed: '@font-face { font-family: foo; src: url(foo.ttf); }', message: messages.rejected('url'), line: 1, column: 41, @@ -501,6 +529,7 @@ testRule({ }, { code: "@font-face { font-family: foo; src: url( 'foo.ttf' ); }", + fixed: '@font-face { font-family: foo; src: url( foo.ttf ); }', message: messages.rejected('url'), line: 1, column: 42, @@ -509,6 +538,7 @@ testRule({ }, { code: 'a { background: url("foo.css"); }', + fixed: 'a { background: url(foo.css); }', message: messages.rejected('url'), line: 1, column: 21, @@ -517,6 +547,7 @@ testRule({ }, { code: 'a { background: uRl("foo.css"); }', + fixed: 'a { background: uRl(foo.css); }', message: messages.rejected('url'), line: 1, column: 21, @@ -525,6 +556,7 @@ testRule({ }, { code: 'a { background: URL("foo.css"); }', + fixed: 'a { background: URL(foo.css); }', message: messages.rejected('url'), line: 1, column: 21, @@ -533,6 +565,7 @@ testRule({ }, { code: 'a { background: url( "foo.css" ); }', + fixed: 'a { background: url( foo.css ); }', message: messages.rejected('url'), line: 1, column: 22, @@ -541,6 +574,7 @@ testRule({ }, { code: 'a { background: url( "foo.css" ); }', + fixed: 'a { background: url( foo.css ); }', message: messages.rejected('url'), line: 1, column: 23, @@ -549,6 +583,7 @@ testRule({ }, { code: 'a { cursor: url("foo.png"); }', + fixed: 'a { cursor: url(foo.png); }', message: messages.rejected('url'), line: 1, column: 17, @@ -557,6 +592,7 @@ testRule({ }, { code: "a { background-image: url('foo.css'), url(bar.css), url(baz.css); }", + fixed: 'a { background-image: url(foo.css), url(bar.css), url(baz.css); }', message: messages.rejected('url'), line: 1, column: 27, @@ -565,6 +601,7 @@ testRule({ }, { code: "a { background-image: url( 'foo.css' ), url(bar.css), url(baz.css); }", + fixed: 'a { background-image: url( foo.css ), url(bar.css), url(baz.css); }', message: messages.rejected('url'), line: 1, column: 28, @@ -573,6 +610,7 @@ testRule({ }, { code: "a { background-image: url(foo.css), url('bar.css'), url(baz.css); }", + fixed: 'a { background-image: url(foo.css), url(bar.css), url(baz.css); }', message: messages.rejected('url'), line: 1, column: 41, @@ -581,6 +619,7 @@ testRule({ }, { code: "a { background-image: url(foo.css), url( 'bar.css' ), url(baz.css); }", + fixed: 'a { background-image: url(foo.css), url( bar.css ), url(baz.css); }', message: messages.rejected('url'), line: 1, column: 42, @@ -589,6 +628,7 @@ testRule({ }, { code: "a { background-image: url(foo.css), url(bar.css), url('baz.css'); }", + fixed: 'a { background-image: url(foo.css), url(bar.css), url(baz.css); }', message: messages.rejected('url'), line: 1, column: 55, @@ -597,6 +637,7 @@ testRule({ }, { code: "a { background-image: url(foo.css), url(bar.css), url( 'baz.css' ); }", + fixed: 'a { background-image: url(foo.css), url(bar.css), url( baz.css ); }', message: messages.rejected('url'), line: 1, column: 56, @@ -605,6 +646,7 @@ testRule({ }, { code: 'a { background: url("/images/my_image@2x.png") }', + fixed: 'a { background: url(/images/my_image@2x.png) }', message: messages.rejected('url'), line: 1, column: 21, diff --git a/lib/rules/function-url-quotes/index.js b/lib/rules/function-url-quotes/index.js index dda521c49d..fef44047d8 100644 --- a/lib/rules/function-url-quotes/index.js +++ b/lib/rules/function-url-quotes/index.js @@ -1,6 +1,7 @@ 'use strict'; const atRuleParamIndex = require('../../utils/atRuleParamIndex'); +const declarationValueIndex = require('../../utils/declarationValueIndex'); const functionArgumentsSearch = require('../../utils/functionArgumentsSearch'); const isStandardSyntaxUrl = require('../../utils/isStandardSyntaxUrl'); const optionsMatches = require('../../utils/optionsMatches'); @@ -17,10 +18,11 @@ const messages = ruleMessages(ruleName, { const meta = { url: 'https://stylelint.io/user-guide/rules/function-url-quotes', + fixable: true, }; /** @type {import('stylelint').Rule} */ -const rule = (primary, secondaryOptions) => { +const rule = (primary, secondaryOptions, context) => { return (root, result) => { const validOptions = validateOptions( result, @@ -49,7 +51,7 @@ const rule = (primary, secondaryOptions) => { * @param {import('postcss').Declaration} decl */ function checkDeclParams(decl) { - functionArgumentsSearch(decl.toString().toLowerCase(), 'url', (args, index) => { + functionArgumentsSearch(decl.toString(), /^url$/i, (args, index) => { checkArgs(args, decl, index, 'url'); }); } @@ -58,22 +60,76 @@ const rule = (primary, secondaryOptions) => { * @param {import('postcss').AtRule} atRule */ function checkAtRuleParams(atRule) { - const atRuleParamsLowerCase = atRule.params.toLowerCase(); - - functionArgumentsSearch(atRuleParamsLowerCase, 'url', (args, index) => { + functionArgumentsSearch(atRule.params, /^url$/i, (args, index) => { checkArgs(args, atRule, index + atRuleParamIndex(atRule), 'url'); }); - functionArgumentsSearch(atRuleParamsLowerCase, 'url-prefix', (args, index) => { + functionArgumentsSearch(atRule.params, /^url-prefix$/i, (args, index) => { checkArgs(args, atRule, index + atRuleParamIndex(atRule), 'url-prefix'); }); - functionArgumentsSearch(atRuleParamsLowerCase, 'domain', (args, index) => { + functionArgumentsSearch(atRule.params, /^domain$/i, (args, index) => { checkArgs(args, atRule, index + atRuleParamIndex(atRule), 'domain'); }); } /** + * @param {import('postcss').AtRule} node * @param {string} args - * @param {import('postcss').Node} node + * @param {number} index + */ + function addQuotesForAtRule(node, args, index) { + const fixedName = `"${args}"`; + const openIndex = index - atRuleParamIndex(node); + const closeIndex = openIndex + args.length; + + node.params = + node.params.substring(0, openIndex) + fixedName + node.params.substring(closeIndex); + } + + /** + * @param {import('postcss').Declaration} node + * @param {string} args + * @param {number} index + */ + function addQuotesForDecl(node, args, index) { + const fixedName = `"${args}"`; + const openIndex = index - declarationValueIndex(node); + const closeIndex = openIndex + args.length; + + node.value = + node.value.substring(0, openIndex) + fixedName + node.value.substring(closeIndex); + } + + /** + * @param {import('postcss').AtRule} node + * @param {string} args + * @param {number} index + */ + + function removeQuotesForAtRule(node, args, index) { + const fixedName = args.slice(1, args.length - 1); + const openIndex = index - atRuleParamIndex(node); + const closeIndex = openIndex + args.length; + + node.params = node.params.slice(0, openIndex) + fixedName + node.params.slice(closeIndex); + } + + /** + * @param {import('postcss').Declaration} node + * @param {string} args + * @param {number} index + */ + + function removeQuotesForDecl(node, args, index) { + const fixedName = args.slice(1, args.length - 1); + const openIndex = index - declarationValueIndex(node); + const closeIndex = openIndex + args.length; + + node.value = node.value.slice(0, openIndex) + fixedName + node.value.slice(closeIndex); + } + + /** + * @param {string} args + * @param {import('postcss').Declaration | import('postcss').AtRule} node * @param {number} index * @param {string} functionName */ @@ -102,12 +158,32 @@ const rule = (primary, secondaryOptions) => { return; } + if (context.fix) { + if (node.type === 'atrule') { + addQuotesForAtRule(node, trimmedArg, complaintIndex); + } else { + addQuotesForDecl(node, trimmedArg, complaintIndex); + } + + return; + } + complain(messages.expected(functionName), node, complaintIndex, complaintEndIndex); } else { if (!hasQuotes) { return; } + if (context.fix) { + if (node.type === 'atrule') { + removeQuotesForAtRule(node, trimmedArg, complaintIndex); + } else { + removeQuotesForDecl(node, trimmedArg, complaintIndex); + } + + return; + } + complain(messages.rejected(functionName), node, complaintIndex, complaintEndIndex); } } diff --git a/system-tests/002/__snapshots__/fs.test.js.snap b/system-tests/002/__snapshots__/fs.test.js.snap index a84109f3a2..015378d2e2 100644 --- a/system-tests/002/__snapshots__/fs.test.js.snap +++ b/system-tests/002/__snapshots__/fs.test.js.snap @@ -113,6 +113,7 @@ Object { "url": "https://stylelint.io/user-guide/rules/function-url-no-scheme-relative", }, "function-url-quotes": Object { + "fixable": true, "url": "https://stylelint.io/user-guide/rules/function-url-quotes", }, "max-empty-lines": Object { diff --git a/system-tests/002/__snapshots__/no-fs.test.js.snap b/system-tests/002/__snapshots__/no-fs.test.js.snap index 324c84ba11..f103e70013 100644 --- a/system-tests/002/__snapshots__/no-fs.test.js.snap +++ b/system-tests/002/__snapshots__/no-fs.test.js.snap @@ -113,6 +113,7 @@ Object { "url": "https://stylelint.io/user-guide/rules/function-url-no-scheme-relative", }, "function-url-quotes": Object { + "fixable": true, "url": "https://stylelint.io/user-guide/rules/function-url-quotes", }, "max-empty-lines": Object {