Stylelint
Why and How to Lint CSS
Andrey Sitnik
Evil Martians



Our Projects
Russia
Our Open Source

Linters History

Source code
Parser
Analysis
Errors list
Linter
1978: First Linter, the Lint

1995: JavaScript
// true
1 == "1"
// easy to forget var
count = 1

Linters
CoffeeScript
vs


CoffeeScript :-(
JS Linters Evolution



JSLint
JSHint
ESLint
→
→
ESLint: Whitelist
module.exports = {
'rules': {
'space-before-function-paren': [2],
'no-shadow-restricted-names': [2],
'computed-property-spacing': [2],
'no-empty-character-class': [2],
'no-irregular-whitespace': [2],
'no-unexpected-multiline': [2],
'no-multiple-empty-lines': [2],
'no-constant-condition': [2],
…
ESLint: Plugins
Source code
Parser
Plugin 1
Errors list
Plugin 2
ESLint: Fixing
if(foo) {
bar()}
if (foo) {
bar();
}
eslint --fix *.js
The Most Popular Dependencies
- mocha
- chai
- lodash
- grunt
- gulp
- eslint
- babel-preset-es2015
- request
- async
- should
CSS Linters

Syntax Support
SCSS
CSSLint
SCSS Lint
CSSComb
CSS
Less
Features
Whitelist
CSSLint
SCSS Lint
CSSComb
Plugins
Fixing
Rules Count
CSSLint
SCSS Lint
CSSComb
38
26
62

Stylelint

Source code
Parser
Plugin 1
Errors list
Plugin 2
ESLint Plugins Look Like …

One Framework to Rule Them All
cssnext
cssnano
Stylelint
PreCSS
CSS Modules
Changeable Parsers
CSS
SCSS
Less
SugarSS
Broken CSS
postcss-browser-reporter

Stylelint Rule is a PostCSS Plugin
const ruleName = "comment-no-empty"
(root, result) => {
root.walkComments(comment => {
if ( comment.text && comment.text.length === 0 ) {
report({ … })
}
}
}
Rules White List
module.exports = {
'rules': {
'at-rule-name-case': 'lower',
'at-rule-semicolon-newline-after': 'always',
'block-closing-brace-newline-after': 'always',
'color-hex-case': 'lower',
'color-hex-length': 'short',
'color-hex-length': 'short',
'color-no-invalid-hex': true,
'indentation': 2,
…
Fixing
.foo {
color:black}
.foo {
color:black;
}
stylelint --fix *.css
SCSS
CSSLint
SCSS Lint
CSSComb
CSS
Less
Stylelint
Whitelist
CSSLint
SCSS Lint
CSSComb
Plugins
Fixing
Stylelint
CSSLint
SCSS Lint
CSSComb
38
26
62
Stylelint
176
Easy to Write Rules
Stylelint Users

The Most Popular in npm

Why


Senior
Junior
Code Review


Senior
Junior
1. Less Time for Code Review
Stylelint


2. Robot’s Criticism is Less Agressive
3. Sharing Best Practices
module.exports = {
'extend': 'stylelint-config-wordpress'
}
4. Preventing Errors
'color-no-invalid-hex',
'function-linear-gradient-no-nonstandard-direction',
'time-no-imperceptible',
'property-no-unknown',
'property-no-vendor-prefix',
'declaration-block-no-duplicate-properties',
'declaration-block-no-ignored-properties',
'declaration-block-no-shorthand-property-overrides',
'selector-class-pattern',
'selector-max-compound-selectors',
'selector-pseudo-element-no-unknown',
'selector-type-no-unknown',
'media-feature-no-missing-punctuation',
'no-unsupported-browser-features',
'no-unknown-animations',
'no-indistinguishable-colors',
'no-descending-specificity',
'no-browser-hacks'
#container .foo {
top: 10px;
}
…
.foo {
/* Expected selector .foo to come before #container .foo */
top: 0;
}
.foo {
opacity: 1;
/* Unsupported in IE 8 */
}
.foo {
display: inline;
width: 100px;
/* Unexpected width with display: inline */
}
.foo {
margin-top: 10px;
margin: 0 auto;
/* Unexpected shorthand margin after margin-top */
}
.foo {
color: black;
}
…
.bar {
background: #010101;
/* Unexpected indistinguishable colors #010101 and black */
}
How

1. Linter in Text Editor

2. lint-staged in Pre-Commit
"scripts": {
"lint-staged": "lint-staged",
},
"lint-staged": {
"*.css": "stylelint"
},
"pre-commit": ["lint-staged"]
3. Linting in CI

4. Start with Popular Config
module.exports = {
'extend': 'stylelint-config-standard'
}
5. Use Third-Party Plugins
module.exports = {
'plugins': [
'stylelint-scss'
],
'rules': {
'scss/selector-no-redundant-nesting-selector': true
}
}
dustinspecker/awesome-eslint

6. Write Custom Rules
import { utils } from "stylelint"
export const ruleName = namespace("NAME")
export const messages = utils.ruleMessages(ruleName, {
expected: "ERROR DESCRIPTION",
})
export default function () {
return (root, result) => {
/* BODY */
}
}
Repeating Colors to Variables
colors = { }
return (root, result) => {
root.walkDecls(decl => {
decl.value.match(/#[0-9a-f]{3,6}/, color => {
if ( colors[color] ) {
utils.report({ … })
} else {
colors[color] = true
}
})
})
}
Facebook Custom Rules
-
slow-css-properties
-
filters-with-svg-files
-
use-variables
-
mobile-flexbox
7. Happened Twice — Write a Rule

8. Lint Everything
gulp.task('default', ['lint',
'test',
'all-translated',
'spell-check',
'security-audit',
'file-size'])
postcss-sorting
div {
-moz-box-sizing: border-box;
width: 100%;
box-sizing: border-box;
position: absolute;
-webkit-box-sizing: border-box;
}
div {
position: absolute;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
width: 100%;
}
$ yaspeller /home/ai/Dev/postcss/README.md
✗ /home/ai/Dev/postcss/README.md 453 ms
-----
Typos: 1
1. transorming (suggest: transforming, transporting)
-----
Node Security Platform
> nsp check
(+) 1 vulnerabilities found
┌───────────────┬───────────────────────────────────────────────────────┐
│ │ ReDoS via long string of semicolons │
├───────────────┼───────────────────────────────────────────────────────┤
│ Name │ tough-cookie │
├───────────────┼───────────────────────────────────────────────────────┤
│ Installed │ 2.2.2 │
├───────────────┼───────────────────────────────────────────────────────┤
│ Vulnerable │ >=0.9.7 <=2.2.2 │
├───────────────┼───────────────────────────────────────────────────────┤
│ Patched │ >=2.3.0 │
├───────────────┼───────────────────────────────────────────────────────┤
│ Path │ my-test-project@undefined > honeybadger@1.1.2 > requ… │
├───────────────┼───────────────────────────────────────────────────────┤
│ More Info │ https://nodesecurity.io/advisories/130 │
└───────────────┴───────────────────────────────────────────────────────┘
Links
Stylelint: Why and How to Lint CSS
By Andrey Sitnik
Stylelint: Why and How to Lint CSS
- 15,954