diff --git a/lib/rules/function-name-arguments-allowed-list/README.md b/lib/rules/function-name-arguments-allowed-list/README.md new file mode 100644 index 0000000000..805efe726f --- /dev/null +++ b/lib/rules/function-name-arguments-allowed-list/README.md @@ -0,0 +1,86 @@ +# function-name-arguments-allowed-list + +Specify an list of allowed property and value pairs within declarations. + + +```css +a { background: url('http://www.example.com/file.jpg'); } +/** ↑ ↑ + * These properties and these values */ +``` + + +```css +a { background-image: url('http://www.example.com/file.jpg'); } +/** ↑ ↑ + * These properties and these values */ +``` + +## Options + +`object`: `{"unprefixed-function-name": ["/regex/", /regex/] }` + +Given: + +``` +['/^data:/', '/^images/', '/^http/', '/^vendor/'] +``` + +The following patterns are considered violations: + + +```css +a { background-image: url(data/x.jpg); } +``` + + +```css +a { background: url(#fff) } +``` + + +```css +a { background-image: url('//example.com/file.jpg'); } +``` + + +```css +a { background-image: url('./path/to/file.jpg'); } +``` + + +```css +a { background-image: url('vendor/file.jpg'); } +``` + +The following patterns are _not_ considered violations: + + +```css +a { background: url(images/x.jpg); } +``` + + +```css +a { background-image: url(vendor/x.jpg); } +``` + + +```css +a { background: url(https://image/1.png); } +``` + + +```css +a { background-image: url(data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs=); } +``` + + +```css +a { background-image: #fff url(images/select2.png) no-repeat 100% -22px } +``` + + +```css +a {background-image: url(images/select2.png) no-repeat 100% -22px, -moz-linear-gradient(bottom, #fff 85%, #eee 99%);} +``` diff --git a/lib/rules/function-name-arguments-allowed-list/__tests__/index.js b/lib/rules/function-name-arguments-allowed-list/__tests__/index.js new file mode 100644 index 0000000000..07b40c17ee --- /dev/null +++ b/lib/rules/function-name-arguments-allowed-list/__tests__/index.js @@ -0,0 +1,57 @@ +'use strict'; + +const { messages, ruleName } = require('..'); + +testRule({ + ruleName, + + config: [ + { + '/^background/': ['/^data:/', '/^images/', '/^http/', '/^vendor/'], + }, + ], + + accept: [ + { + code: 'a { background: url(images/x.jpg); }', + }, + { + code: 'a { background-image: url(vendor/x.jpg); }', + }, + { + code: 'a { background: url(https://image/1.png); }', + }, + { + code: + 'a { background-image: url(data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs=); }', + }, + { + code: 'a { background-image: #fff url(images/select2.png) no-repeat 100% -22px }', + }, + { + code: + 'a {background-image: url(images/select2.png) no-repeat 100% -22px, -moz-linear-gradient(bottom, #fff 85%, #eee 99%);}', + }, + ], + + reject: [ + { + code: 'a { background-image: url(data/x.jpg); }', + message: messages.rejected('background-image', 'data/x.jpg'), + line: 1, + column: 27, + }, + { + code: 'a { background-image: #fff url(magic/select2.png) no-repeat 100% -22px }', + message: messages.rejected('background-image', 'magic/select2.png'), + line: 1, + column: 32, + }, + { + code: 'a { background: url(#fff) }', + message: messages.rejected('background', '#fff'), + line: 1, + column: 21, + }, + ], +}); diff --git a/lib/rules/function-name-arguments-allowed-list/index.js b/lib/rules/function-name-arguments-allowed-list/index.js new file mode 100644 index 0000000000..49eec48dd2 --- /dev/null +++ b/lib/rules/function-name-arguments-allowed-list/index.js @@ -0,0 +1,71 @@ +// @ts-nocheck + +'use strict'; + +const _ = require('lodash'); +const declarationValueIndex = require('../../utils/declarationValueIndex'); +const matchesStringOrRegExp = require('../../utils/matchesStringOrRegExp'); +const postcss = require('postcss'); +const report = require('../../utils/report'); +const ruleMessages = require('../../utils/ruleMessages'); +const validateOptions = require('../../utils/validateOptions'); +const valueParser = require('postcss-value-parser'); +const ruleName = 'function-name-arguments-allowed-list'; + +const messages = ruleMessages(ruleName, { + rejected: (property, value) => `Unexpected value "${value}" for property "${property}"`, +}); + +function rule(whitelist) { + return (root, result) => { + const validOptions = validateOptions(result, ruleName, { + actual: whitelist, + possible: [_.isObject], + }); + + if (!validOptions) { + return; + } + + root.walkDecls((decl) => { + const prop = decl.prop; + const value = decl.value; + + const unprefixedProp = postcss.vendor.unprefixed(prop); + + const propWhitelist = _.find(whitelist, (list, propIdentifier) => + matchesStringOrRegExp(unprefixedProp, propIdentifier), + ); + + if (_.isEmpty(propWhitelist)) { + return; + } + + valueParser(value).walk((node) => { + if (node.value !== 'url') { + return; + } + + const nodes = node.nodes.length && node.nodes[0]; + + const { value, sourceIndex } = nodes; + + if (matchesStringOrRegExp(value, propWhitelist)) { + return; + } + + report({ + message: messages.rejected(prop, value), + node: decl, + index: declarationValueIndex(decl) + sourceIndex, + result, + ruleName, + }); + }); + }); + }; +} + +rule.ruleName = ruleName; +rule.messages = messages; +module.exports = rule; diff --git a/lib/rules/index.js b/lib/rules/index.js index 4846a04021..7e6987032d 100644 --- a/lib/rules/index.js +++ b/lib/rules/index.js @@ -185,6 +185,9 @@ const rules = { 'function-whitespace-after': importLazy(() => require('./function-whitespace-after'))(), // Renamed to function-allowed-list 'function-whitelist': importLazy(() => require('./function-allowed-list'))(), + 'function-name-arguments-allowed-list': importLazy(() => + require('./function-name-arguments-allowed-list'), + )(), 'hue-degree-notation': importLazy(() => require('./hue-degree-notation'))(), 'keyframe-declaration-no-important': importLazy(() => require('./keyframe-declaration-no-important'),