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

New rule: No non numeric dimensions #357

Merged
93 changes: 93 additions & 0 deletions src/rules/dimension-no-non-numeric-values/README.md
@@ -0,0 +1,93 @@
# dimension-no-non-numeric-values

Interpolating a value with a unit (e.g. `#{$value}px`) results in a
_string_ value, not as numeric value. This value then cannot be used in
numerical operations. It is better to use arithmetic to apply a unit to a
number (e.g. `$value * 1px`).

This rule requires that all interpolation for values should be in the format `$value * 1<unit>` instead of `#{value}<unit>`

```scss
$value: 4;

p {
padding: #{value}px;
// ↑ ↑
// should be $value * 1px
}
```

## Options

### `true`

The following patterns are considered violations:

```scss
$value: 4;

p {
padding: #{value}px;
}
```

The following patterns are _not_ considered violations:

```scss
$value: 4;

p {
padding: $value * 1px;
}
```

## List of units
Font-relative lengths ([link](https://www.w3.org/TR/css-values-4/#font-relative-lengths))
* em
* ex
* cap
* ch
* ic
* rem
* lh
* rlh

Viewport-relative lengths ([link](https://www.w3.org/TR/css-values-4/#viewport-relative-lengths))
* vw
* vh
* vi
* vb
* vmin
* vmax

Absolute lengths ([link](https://www.w3.org/TR/css-values-4/#absolute-lengths))
* cm
* mm
* Q
* in
* pc
* pt
* px

Angle units ([link](https://www.w3.org/TR/css-values-4/#angles))
* deg
* grad
* rad
* turn

Duration units ([link](https://www.w3.org/TR/css-values-4/#time))
* s
* ms

Frequency units ([link](https://www.w3.org/TR/css-values-4/#frequency))
* Hz
* kHz

Resolution units ([link](https://www.w3.org/TR/css-values-4/#resolution))
* dpi
* dpcm
* dppx
* x

Flexible lengths ([link](https://www.w3.org/TR/css-grid-1/#fr-unit))
* fr
59 changes: 59 additions & 0 deletions src/rules/dimension-no-non-numeric-values/__tests__/index.js
@@ -0,0 +1,59 @@
import rule, { ruleName, messages, units } from "..";

testRule(rule, {
ruleName,
config: [true],
syntax: "scss",
accept: loopOverUnits({
code: `
p {
padding: 1 * 1%unit%;
}
`,
description: "Accepts proper value interpolation with %unit%"
}).concat([
{
code: "$pad: 2; $doublePad: px#{$pad}px;",
description: "does not report when a unit is preceded by another string"
},
{
code: "$pad: 2; $doublePad: #{$pad}pxx;",
description: "does not report lint when no understood units are used"
},
{
code: `$pad: "2";
$string: "#{$pad}px";`,
description: "does not report lint when string is quoted"
}
]),
reject: loopOverUnits({
code: `
p {
padding: #{$value}%unit%;
}
`,
messages: messages.rejected,
description: "Rejects interpolation with %unit%"
}).concat([
{
code: "$pad: 2; $padAndMore: #{$pad + 5}px;",
description: "reports lint when expression used in interpolation",
messages: messages.rejected("px")
}
])
});

function loopOverUnits(codeBlock) {
return units.map(unit => {
const block = {
code: codeBlock.code.replace("%unit%", unit),
description: codeBlock.description.replace("%unit%", unit)
};

if (codeBlock.messages) {
block["messages"] = codeBlock.messages.call(unit);
}

return block;
});
}
124 changes: 124 additions & 0 deletions src/rules/dimension-no-non-numeric-values/index.js
@@ -0,0 +1,124 @@
import { utils } from "stylelint";
import { namespace } from "../../utils";
import valueParser from "postcss-value-parser";

export const ruleName = namespace("dimension-no-non-numeric-values");

export const messages = utils.ruleMessages(ruleName, {
rejected: unit =>
`Expected "$value * 1${unit}" instead of "#{value}${unit}". Consider writing "value" in terms of ${unit} originally.`
});

export const units = [
// Font-relative lengths:
// https://www.w3.org/TR/css-values-4/#font-relative-lengths
"em",
"ex",
"cap",
"ch",
"ic",
"rem",
"lh",
"rlh",

// Viewport-relative lengths:
// https://www.w3.org/TR/css-values-4/#viewport-relative-lengths
"vw",
"vh",
"vi",
"vb",
"vmin",
"vmax",

// Absolute lengths:
// https://www.w3.org/TR/css-values-4/#absolute-lengths
"cm",
"mm",
"Q",
"in",
"pc",
"pt",
"px",

// Angle units:
// https://www.w3.org/TR/css-values-4/#angles
"deg",
"grad",
"rad",
"turn",

// Duration units:
// https://www.w3.org/TR/css-values-4/#time
"s",
"ms",

// Frequency units:
// https://www.w3.org/TR/css-values-4/#frequency
"Hz",
"kHz",

// Resolution units:
// https://www.w3.org/TR/css-values-4/#resolution
"dpi",
"dpcm",
"dppx",
"x",

// Flexible lengths:
// https://www.w3.org/TR/css-grid-1/#fr-unit
"fr"
];

export default function rule(primary) {
return (root, result) => {
const validOptions = utils.validateOptions(result, ruleName, {
actual: primary
});

if (!validOptions) {
return;
}

root.walkDecls(decl => {
valueParser(decl.value).walk(node => {
// All words are non-quoted, while strings are quoted.
// If quoted, it's probably a deliberate non-numeric dimension.
if (node.type !== "word") {
return;
}

if (!isInterpolated(node.value)) {
return;
}

utils.report({
ruleName,
result,
message: messages.rejected,
node: decl
});
});
});
};
}

function isInterpolated(value) {
let boolean = false;

// ValueParser breaks up interpolation with math into multiple, fragmented
// segments (#{$value, +, 2}px). The easiest way to detect this is to look for a fragmented
// interpolated section.
if (value.match(/^#{\$[a-z]*$/)) {
return true;
}

units.forEach(unit => {
const regex = new RegExp("^#{[$a-z_0-9 +-]*}" + unit + ";?$");

if (value.match(regex)) {
boolean = true;
}
});

return boolean;
}
2 changes: 2 additions & 0 deletions src/rules/index.js
Expand Up @@ -19,6 +19,7 @@ import atEachKeyValue from "./at-each-key-value-single-line";
import atRuleNoUnknown from "./at-rule-no-unknown";
import declarationNestedProperties from "./declaration-nested-properties";
import declarationNestedPropertiesNoDividedGroups from "./declaration-nested-properties-no-divided-groups";
import dimensionNoNonNumeric from "./dimension-no-non-numeric-values";
import dollarVariableColonNewlineAfter from "./dollar-variable-colon-newline-after";
import dollarVariableColonSpaceAfter from "./dollar-variable-colon-space-after";
import dollarVariableColonSpaceBefore from "./dollar-variable-colon-space-before";
Expand Down Expand Up @@ -63,6 +64,7 @@ export default {
"at-rule-no-unknown": atRuleNoUnknown,
"declaration-nested-properties": declarationNestedProperties,
"declaration-nested-properties-no-divided-groups": declarationNestedPropertiesNoDividedGroups,
"dimension-no-non-numeric-values": dimensionNoNonNumeric,
"dollar-variable-colon-newline-after": dollarVariableColonNewlineAfter,
"dollar-variable-colon-space-after": dollarVariableColonSpaceAfter,
"dollar-variable-colon-space-before": dollarVariableColonSpaceBefore,
Expand Down