Skip to content

Commit

Permalink
Add support for some math functions to `function-calc-no-unspaced-ope…
Browse files Browse the repository at this point in the history
…rator`

current: `calc`, `sin`, `cos`, `tan`, `exp`, `abs`, `sign`, `asin`, `acos`, `atan`, `sqrt`
planned: `min`, `max`, `hypot`, `clamp`, `round`, `mod`, `rem`, `atan2`, `pow`, `log`

ref https://www.w3.org/TR/css-values-4/#calc-syntax
  • Loading branch information
Mouvedia committed Apr 18, 2024
1 parent fca3269 commit d06a41e
Show file tree
Hide file tree
Showing 11 changed files with 117 additions and 61 deletions.
5 changes: 5 additions & 0 deletions .changeset/funny-jobs-work.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"stylelint": minor
---

Fixed: `function-calc-no-unspaced-operator` false negatives for some math functions
19 changes: 12 additions & 7 deletions lib/reference/functions.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,24 @@ const colorFunctions = new Set([
'rgba',
]);

const mathFunctions = new Set([
const singleArgumentMathFunctions = new Set([
'abs',
'acos',
'asin',
'atan',
'atan2',
'calc',
'clamp',
'cos',
'exp',
'sign',
'sin',
'sqrt',
'tan',
]);

const mathFunctions = new Set([
...singleArgumentMathFunctions,
'atan2',
'clamp',
'hypot',
'log',
'max',
Expand All @@ -48,12 +56,9 @@ const mathFunctions = new Set([
'pow',
'rem',
'round',
'sign',
'sin',
'sqrt',
'tan',
]);

exports.camelCaseFunctions = camelCaseFunctions;
exports.colorFunctions = colorFunctions;
exports.mathFunctions = mathFunctions;
exports.singleArgumentMathFunctions = singleArgumentMathFunctions;
18 changes: 11 additions & 7 deletions lib/reference/functions.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,24 @@ export const colorFunctions = new Set([
'rgba',
]);

export const mathFunctions = new Set([
export const singleArgumentMathFunctions = new Set([
'abs',
'acos',
'asin',
'atan',
'atan2',
'calc',
'clamp',
'cos',
'exp',
'sign',
'sin',
'sqrt',
'tan',
]);

export const mathFunctions = new Set([
...singleArgumentMathFunctions,
'atan2',
'clamp',
'hypot',
'log',
'max',
Expand All @@ -44,8 +52,4 @@ export const mathFunctions = new Set([
'pow',
'rem',
'round',
'sign',
'sin',
'sqrt',
'tan',
]);
7 changes: 6 additions & 1 deletion lib/rules/function-calc-no-unspaced-operator/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# function-calc-no-unspaced-operator

Disallow invalid unspaced operator within `calc` functions.
Disallow invalid unspaced operator within math functions that accept a single [`<calc-sum>`](https://www.w3.org/TR/css-values-4/#typedef-calc-sum) value, such as `calc()` or `abs()`.

<!-- prettier-ignore -->
```css
Expand Down Expand Up @@ -29,6 +29,11 @@ a { top: calc(1px+2px); }
a { top: calc(1px+ 2px); }
```

<!-- prettier-ignore -->
```css
a { transform: rotate(atan(-2+1)); }
```

The following patterns are _not_ considered problems:

<!-- prettier-ignore -->
Expand Down
44 changes: 40 additions & 4 deletions lib/rules/function-calc-no-unspaced-operator/__tests__/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,16 @@ testRule({
{
code: 'a { padding: calc(); }',
},
{
code: 'a { font-size: clamp(1rem, 2.5vw, 1rem+1rem); }',
description:
'multiple argument: functions that accept more than one argument are not supported yet',
},
{
code: 'a { width: min(25vw+25vw); }',
description:
'single argument: functions that accept more than one argument are not supported yet',
},
],

reject: [
Expand All @@ -240,8 +250,34 @@ testRule({
],
},
{
code: 'a { top: calc(1px +\t-1px)}',
fixed: 'a { top: calc(1px + -1px)}',
code: 'a { transform: rotate(acos(2+1)) }',
fixed: 'a { transform: rotate(acos(2 + 1)) }',
warnings: [
{ message: messages.expectedBefore('+'), line: 1, column: 29, endLine: 1, endColumn: 30 },
{ message: messages.expectedAfter('+'), line: 1, column: 30, endLine: 1, endColumn: 31 },
],
},
{
code: 'a { scale: rem(10+2, 1.7); }',
fixed: 'a { scale: rem(10 + 2, 1.7); }',
warnings: [
{ message: messages.expectedBefore('+'), line: 1, column: 18, endLine: 1, endColumn: 19 },
{ message: messages.expectedAfter('+'), line: 1, column: 19, endLine: 1, endColumn: 20 },
],
skip: true,
},
{
code: 'a { rotate: rem(10turn, 2turn+1turn); }',
fixed: 'a { rotate: rem(10turn, 2turn + 1turn); }',
warnings: [
{ message: messages.expectedBefore('+'), line: 1, endLine: 1 },
{ message: messages.expectedAfter('+'), line: 1, endLine: 1 },
],
skip: true,
},
{
code: 'a { top: calc(1px +\t-1px) }',
fixed: 'a { top: calc(1px + -1px) }',
description: 'tab before sign after operator',
message: messages.expectedAfter('+'),
line: 1,
Expand All @@ -250,8 +286,8 @@ testRule({
endColumn: 20,
},
{
code: 'a { top: calc(1px + -1px)}',
fixed: 'a { top: calc(1px + -1px)}',
code: 'a { top: calc(1px + -1px) }',
fixed: 'a { top: calc(1px + -1px) }',
description: 'multiple spaces before sign after operator',
message: messages.expectedAfter('+'),
line: 1,
Expand Down
17 changes: 11 additions & 6 deletions lib/rules/function-calc-no-unspaced-operator/index.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
'use strict';

const valueParser = require('postcss-value-parser');
const typeGuards = require('../../utils/typeGuards.cjs');
const validateTypes = require('../../utils/validateTypes.cjs');
const declarationValueIndex = require('../../utils/declarationValueIndex.cjs');
const getDeclarationValue = require('../../utils/getDeclarationValue.cjs');
const isStandardSyntaxValue = require('../../utils/isStandardSyntaxValue.cjs');
const report = require('../../utils/report.cjs');
const ruleMessages = require('../../utils/ruleMessages.cjs');
const setDeclarationValue = require('../../utils/setDeclarationValue.cjs');
const functions = require('../../reference/functions.cjs');
const validateOptions = require('../../utils/validateOptions.cjs');

const ruleName = 'function-calc-no-unspaced-operator';
Expand All @@ -28,7 +30,10 @@ const meta = {
const OPERATORS = new Set(['+', '-']);
const OPERATOR_REGEX = /[+-]/;
const ALL_OPERATORS = new Set([...OPERATORS, '*', '/']);
const CALC_FUNC_REGEX = /calc\(/i;
// #7618
const alternatives = [...functions.singleArgumentMathFunctions].join('|');
const FUNC_NAMES_REGEX = new RegExp(`^(?:${alternatives})$`, 'i');
const FUNC_CALLS_REGEX = new RegExp(`(?:${alternatives})\\(`, 'i');

/** @type {import('stylelint').Rule} */
const rule = (primary, _secondaryOptions, context) => {
Expand All @@ -54,7 +59,7 @@ const rule = (primary, _secondaryOptions, context) => {

if (!OPERATOR_REGEX.test(value)) return;

if (!CALC_FUNC_REGEX.test(value)) return;
if (!FUNC_CALLS_REGEX.test(value)) return;

let needsFix = false;
const valueIndex = declarationValueIndex(decl);
Expand All @@ -70,7 +75,7 @@ const rule = (primary, _secondaryOptions, context) => {
const operatorSourceIndex = operatorNode.sourceIndex;

if (currentNode && !isSingleSpace(currentNode)) {
if (currentNode.type === 'word') {
if (typeGuards.isValueWord(currentNode)) {
if (isBeforeOp) {
const lastChar = currentNode.value.slice(-1);

Expand Down Expand Up @@ -145,7 +150,7 @@ const rule = (primary, _secondaryOptions, context) => {
return true;
}

if (currentNode.type === 'function') {
if (typeGuards.isValueFunction(currentNode)) {
if (context.fix) {
needsFix = true;
currentNode.value = isBeforeOp ? `${currentNode.value} ` : ` ${currentNode.value}`;
Expand Down Expand Up @@ -290,7 +295,7 @@ const rule = (primary, _secondaryOptions, context) => {
const lastChar = node.value.slice(-1);
const firstChar = node.value.slice(0, 1);

if (node.type === 'word') {
if (typeGuards.isValueWord(node)) {
if (index === 0 && OPERATORS.has(lastChar)) {
if (context.fix) {
node.value = `${node.value.slice(0, -1)} ${lastChar}`;
Expand Down Expand Up @@ -318,7 +323,7 @@ const rule = (primary, _secondaryOptions, context) => {
}

parsedValue.walk((node) => {
if (node.type !== 'function' || node.value.toLowerCase() !== 'calc') return;
if (!typeGuards.isValueFunction(node) || !FUNC_NAMES_REGEX.test(node.value)) return;

const { nodes } = node;

Expand Down
17 changes: 11 additions & 6 deletions lib/rules/function-calc-no-unspaced-operator/index.mjs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import valueParser from 'postcss-value-parser';

import { isValueFunction as isFunction, isValueWord as isWord } from '../../utils/typeGuards.mjs';
import { assert } from '../../utils/validateTypes.mjs';
import declarationValueIndex from '../../utils/declarationValueIndex.mjs';
import getDeclarationValue from '../../utils/getDeclarationValue.mjs';
import isStandardSyntaxValue from '../../utils/isStandardSyntaxValue.mjs';
import report from '../../utils/report.mjs';
import ruleMessages from '../../utils/ruleMessages.mjs';
import setDeclarationValue from '../../utils/setDeclarationValue.mjs';
import { singleArgumentMathFunctions } from '../../reference/functions.mjs';
import validateOptions from '../../utils/validateOptions.mjs';

const ruleName = 'function-calc-no-unspaced-operator';
Expand All @@ -25,7 +27,10 @@ const meta = {
const OPERATORS = new Set(['+', '-']);
const OPERATOR_REGEX = /[+-]/;
const ALL_OPERATORS = new Set([...OPERATORS, '*', '/']);
const CALC_FUNC_REGEX = /calc\(/i;
// #7618
const alternatives = [...singleArgumentMathFunctions].join('|');
const FUNC_NAMES_REGEX = new RegExp(`^(?:${alternatives})$`, 'i');
const FUNC_CALLS_REGEX = new RegExp(`(?:${alternatives})\\(`, 'i');

/** @type {import('stylelint').Rule} */
const rule = (primary, _secondaryOptions, context) => {
Expand All @@ -51,7 +56,7 @@ const rule = (primary, _secondaryOptions, context) => {

if (!OPERATOR_REGEX.test(value)) return;

if (!CALC_FUNC_REGEX.test(value)) return;
if (!FUNC_CALLS_REGEX.test(value)) return;

let needsFix = false;
const valueIndex = declarationValueIndex(decl);
Expand All @@ -67,7 +72,7 @@ const rule = (primary, _secondaryOptions, context) => {
const operatorSourceIndex = operatorNode.sourceIndex;

if (currentNode && !isSingleSpace(currentNode)) {
if (currentNode.type === 'word') {
if (isWord(currentNode)) {
if (isBeforeOp) {
const lastChar = currentNode.value.slice(-1);

Expand Down Expand Up @@ -142,7 +147,7 @@ const rule = (primary, _secondaryOptions, context) => {
return true;
}

if (currentNode.type === 'function') {
if (isFunction(currentNode)) {
if (context.fix) {
needsFix = true;
currentNode.value = isBeforeOp ? `${currentNode.value} ` : ` ${currentNode.value}`;
Expand Down Expand Up @@ -287,7 +292,7 @@ const rule = (primary, _secondaryOptions, context) => {
const lastChar = node.value.slice(-1);
const firstChar = node.value.slice(0, 1);

if (node.type === 'word') {
if (isWord(node)) {
if (index === 0 && OPERATORS.has(lastChar)) {
if (context.fix) {
node.value = `${node.value.slice(0, -1)} ${lastChar}`;
Expand Down Expand Up @@ -315,7 +320,7 @@ const rule = (primary, _secondaryOptions, context) => {
}

parsedValue.walk((node) => {
if (node.type !== 'function' || node.value.toLowerCase() !== 'calc') return;
if (!isFunction(node) || !FUNC_NAMES_REGEX.test(node.value)) return;

const { nodes } = node;

Expand Down
19 changes: 3 additions & 16 deletions lib/rules/length-zero-no-unit/index.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
'use strict';

const valueParser = require('postcss-value-parser');
const typeGuards = require('../../utils/typeGuards.cjs');
const validateTypes = require('../../utils/validateTypes.cjs');
const atRuleParamIndex = require('../../utils/atRuleParamIndex.cjs');
const declarationValueIndex = require('../../utils/declarationValueIndex.cjs');
Expand Down Expand Up @@ -63,10 +64,10 @@ const rule = (primary, secondaryOptions, context) => {

if (isMathFunction(valueNode)) return false;

if (isFunction(valueNode) && optionsMatches(secondaryOptions, 'ignoreFunctions', value))
if (typeGuards.isValueFunction(valueNode) && optionsMatches(secondaryOptions, 'ignoreFunctions', value))
return false;

if (!isWord(valueNode)) return;
if (!typeGuards.isValueWord(valueNode)) return;

const numberUnit = valueParser.unit(value);

Expand Down Expand Up @@ -187,27 +188,13 @@ function isFlex(prop) {
return prop.toLowerCase() === 'flex';
}

/**
* @param {import('postcss-value-parser').Node} node
*/
function isWord({ type }) {
return type === 'word';
}

/**
* @param {string} unit
*/
function isLength(unit) {
return units.lengthUnits.has(unit.toLowerCase());
}

/**
* @param {import('postcss-value-parser').Node} node
*/
function isFunction({ type }) {
return type === 'function';
}

/**
* @param {string} unit
*/
Expand Down
15 changes: 1 addition & 14 deletions lib/rules/length-zero-no-unit/index.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import valueParser from 'postcss-value-parser';

import { isValueFunction as isFunction, isValueWord as isWord } from '../../utils/typeGuards.mjs';
import { isRegExp, isString } from '../../utils/validateTypes.mjs';
import atRuleParamIndex from '../../utils/atRuleParamIndex.mjs';
import declarationValueIndex from '../../utils/declarationValueIndex.mjs';
Expand Down Expand Up @@ -184,27 +185,13 @@ function isFlex(prop) {
return prop.toLowerCase() === 'flex';
}

/**
* @param {import('postcss-value-parser').Node} node
*/
function isWord({ type }) {
return type === 'word';
}

/**
* @param {string} unit
*/
function isLength(unit) {
return lengthUnits.has(unit.toLowerCase());
}

/**
* @param {import('postcss-value-parser').Node} node
*/
function isFunction({ type }) {
return type === 'function';
}

/**
* @param {string} unit
*/
Expand Down
Loading

0 comments on commit d06a41e

Please sign in to comment.