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
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