Skip to content
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

Add function-url-quotes autofix #6558

Merged
merged 10 commits into from Jan 11, 2023
5 changes: 5 additions & 0 deletions .changeset/rude-dryers-fry.md
@@ -0,0 +1,5 @@
---
"stylelint": minor
---

Added: `function-url-quotes` autofix
142 changes: 142 additions & 0 deletions lib/rules/function-url-quotes/__tests__/index.js
Expand Up @@ -710,6 +710,148 @@ testRule({
],
});

testRule({
mattxwang marked this conversation as resolved.
Show resolved Hide resolved
ruleName,
config: ['always'],
fix: true,

reject: [
{
code: '@import url(foo.css);',
fixed: '@import url("foo.css");',
message: messages.expected('url'),
},
{
code: '@import url( foo.css );',
fixed: '@import url( "foo.css" );',
message: messages.expected('url'),
},
{
code: '@document url(http://www.w3.org/);',
fixed: '@document url("http://www.w3.org/");',
message: messages.expected('url'),
},
{
code: '@document url( http://www.w3.org/ );',
fixed: '@document url( "http://www.w3.org/" );',
message: messages.expected('url'),
},
{
code: '@document url( http://www.w3.org/ );',
fixed: '@document url( "http://www.w3.org/" );',
message: messages.expected('url'),
description: 'multiple spaces after name',
},
{
code: '@document url-prefix(http://www.w3.org/Style);',
fixed: '@document url-prefix("http://www.w3.org/Style");',
message: messages.expected('url-prefix'),
},
{
code: '@document url-prefix( http://www.w3.org/Style );',
fixed: '@document url-prefix( "http://www.w3.org/Style" );',
message: messages.expected('url-prefix'),
},
{
code: '@document domain(mozilla.org);',
fixed: '@document domain("mozilla.org");',
message: messages.expected('domain'),
},
{
code: 'a { background: url(foo.css); }',
fixed: 'a { background: url("foo.css"); }',
message: messages.expected('url'),
},
{
code: 'a { background : url(foo.css); }',
fixed: 'a { background : url("foo.css"); }',
message: messages.expected('url'),
description: 'multiple spaces in between',
},
{
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'),
},
],
});

testRule({
ruleName,
config: ['never'],
fix: true,

reject: [
{
code: '@import url("foo.css");',
fixed: '@import url(foo.css);',
message: messages.rejected('url'),
},
{
code: '@import url( "foo.css" );',
fixed: '@import url( foo.css );',
message: messages.rejected('url'),
},
{
code: "@import url('foo.css');",
fixed: '@import url(foo.css);',
message: messages.rejected('url'),
},
{
code: "@import url( 'foo.css' );",
fixed: '@import url( foo.css );',
message: messages.rejected('url'),
},
{
code: '@document url("http://www.w3.org/");',
fixed: '@document url(http://www.w3.org/);',
message: messages.rejected('url'),
},
{
code: '@document url( "http://www.w3.org/" );',
fixed: '@document url( http://www.w3.org/ );',
message: messages.rejected('url'),
},
{
code: '@document url( "http://www.w3.org/" );',
fixed: '@document url( http://www.w3.org/ );',
message: messages.rejected('url'),
description: 'multiple spaces after name',
},
{
code: "@document url-prefix('http://www.w3.org/Style');",
fixed: '@document url-prefix(http://www.w3.org/Style);',
message: messages.rejected('url-prefix'),
},
{
code: '@document domain("mozilla.org");',
fixed: '@document domain(mozilla.org);',
message: messages.rejected('domain'),
},
{
code: 'a { background: url("foo.css"); }',
fixed: 'a { background: url(foo.css); }',
message: messages.rejected('url'),
},
{
code: "a { background: url('foo.css'); }",
fixed: 'a { background: url(foo.css); }',
message: messages.rejected('url'),
},
{
code: "a { background : url('foo.css'); }",
fixed: 'a { background : url(foo.css); }',
message: messages.rejected('url'),
description: 'multiple spaces in between',
},
{
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'),
},
],
});

testRule({
ruleName,
config: ['always'],
Expand Down
84 changes: 76 additions & 8 deletions lib/rules/function-url-quotes/index.js
Expand Up @@ -19,8 +19,22 @@ const meta = {
url: 'https://stylelint.io/user-guide/rules/function-url-quotes',
mattxwang marked this conversation as resolved.
Show resolved Hide resolved
};

/**
* @param {import('postcss').Declaration} decl
* @returns {number}
*/
function getDeclarationValueIndex(decl) {
mattxwang marked this conversation as resolved.
Show resolved Hide resolved
let index = decl.prop.length;

if (decl.raws.between) {
index += decl.raws.between.length;
}

return index;
}

/** @type {import('stylelint').Rule} */
const rule = (primary, secondaryOptions) => {
const rule = (primary, secondaryOptions, context) => {
return (root, result) => {
const validOptions = validateOptions(
result,
Expand Down Expand Up @@ -49,7 +63,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');
});
}
Expand All @@ -58,22 +72,64 @@ 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').Declaration | import('postcss').AtRule} node
* @param {string} args
* @param {import('postcss').Node} node
* @param {number} index
*/

function addQuotes(node, args, index) {
const fixedName = `"${args}"`;
const openIndex =
'params' in node ? index - atRuleParamIndex(node) : index - getDeclarationValueIndex(node);
mattxwang marked this conversation as resolved.
Show resolved Hide resolved
const closeIndex = openIndex + args.length;

if ('params' in node) {
node.params =
node.params.substring(0, openIndex) + fixedName + node.params.substring(closeIndex);

return;
}

node.value =
node.value.substring(0, openIndex) + fixedName + node.value.substring(closeIndex);
}

/**
* @param {import('postcss').Declaration | import('postcss').AtRule} node
* @param {string} args
* @param {number} index
*/

function removeQuotes(node, args, index) {
const fixedName = args.slice(1, args.length - 1);
const openIndex =
'params' in node ? index - atRuleParamIndex(node) : index - getDeclarationValueIndex(node);
const closeIndex = openIndex + args.length;

if ('params' in node) {
node.params = node.params.slice(0, openIndex) + fixedName + node.params.slice(closeIndex);

return;
}

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
*/
Expand Down Expand Up @@ -102,12 +158,24 @@ const rule = (primary, secondaryOptions) => {
return;
}

if (context.fix) {
addQuotes(node, trimmedArg, complaintIndex);

return;
}

complain(messages.expected(functionName), node, complaintIndex, complaintEndIndex);
} else {
if (!hasQuotes) {
return;
}

if (context.fix) {
removeQuotes(node, trimmedArg, complaintIndex);

return;
}

complain(messages.rejected(functionName), node, complaintIndex, complaintEndIndex);
}
}
Expand Down