読者です 読者をやめる 読者になる 読者になる

Wondershake 開発者ブログ

Locari(ロカリ)の運営会社の開発者ブログです。

RailsアプリケーションのためのフロントエンドLint環境の整備(SCSS編)

こんにちは。フロントエンドエンジニアの佐々木です。

前回は JS 周りの Lint 環境を整備しました。

engineering.wondershake.com

今回はスタイルシート周りの Lint 環境を整備した話になります。

はじめに

スタイルシートはある程度のルールをチーム内で共有しておかないとすぐに雑多なコードになってしまいます。

コーディングルールを明文化してレビューで指摘するといった方法もありますが、できれば Lint ツールでコーディングスタイルを定義して、ルールに逸脱したコードは CI で落とすようにすると健全です。

今回は 2 年間運用されてきた scss ファイルに scss-lint を実行して、エラーを 0 にして CI で弾くことができる状態にするのがゴールになります。

なお scss-lint を導入するにあたりこちらの記事を参考にさせて頂きました。

自動検出と自動修正でCSSを保守する - Qiita

scss-lint

f:id:sskyu:20160725165603p:plain

インストール

scss-lint の Installation を見ると、

$ gem install scss_lint

でインストールするか、Gemfile に

gem 'scss_lint', require: false

を追記して bundle install することでインストールできます。

依存する gem は Gemfile に書いておきたいので、後者でインストールしました。

scss-lint を実行してみる

実行するには下記のコマンドを実行します。

$ bundle exec scss-lint app/assets/stylesheets/**/*.scss
app/assets/stylesheets/admin.scss:1:1 [W] Comment: Use `//` comments everywhere
app/assets/stylesheets/admin.scss:10:9 [W] StringQuotes: Prefer single quoted strings
app/assets/stylesheets/admin.scss:11:9 [W] StringQuotes: Prefer single quoted strings
# 以下膨大なエラーメッセージが続く

エラーが多すぎて原因がよくわからないので、オプションに --format=Status を付けてもう一度実行します。

$ bundle exec scss-lint --format=Stats app/assets/stylesheets/**/*.scss
 257  PropertySortOrder              (across 58 files)
 217  SpaceBeforeBrace               (across 58 files)
 175  NameFormat                     (across 46 files)
 154  StringQuotes                   (across 67 files)
 102  SelectorDepth                  (across 23 files)
  93  NestingDepth                   (across 28 files)
  75  QualifyingElement              (across 31 files)
  65  ColorVariable                  (across 31 files)
  59  EmptyLineBetweenBlocks         (across 21 files)
  47  SpaceAfterPropertyColon        (across 16 files)
  39  DeclarationOrder               (across 17 files)
  35  Shorthand                      (across 23 files)
  35  IdSelector                     (across 17 files)
  34  ColorKeyword                   (across 22 files)
  32  LeadingZero                    (across 11 files)
  30  PseudoElement                  (across  7 files)
  27  FinalNewline                   (across 27 files)
  26  SpaceAfterComma                (across  2 files)
  21  ImportantRule                  (across 11 files)
  19  Indentation                    (across  7 files)
  16  SelectorFormat                 (across  9 files)
  16  BorderZero                     (across 10 files)
  14  VendorPrefix                   (across  3 files)
  14  SingleLinePerSelector          (across  9 files)
  12  Comment                        (across  7 files)
  12  ZeroUnit                       (across  7 files)
   5  EmptyRule                      (across  4 files)
   4  UnnecessaryMantissa            (across  1 files)
   4  MergeableSelector              (across  3 files)
   3  TrailingSemicolon              (across  3 files)
   3  PlaceholderInExtend            (across  1 files)
   1  SpaceBetweenParens             (across  1 files)
   1  SpaceAfterVariableName         (across  1 files)
----  -----------------------        -----------------
1647  total                          (across 91 files)

今のところ 91 ファイルに渡って 1647 個のエラーがあることが分かります。

デフォルトでどんなルールが設定されているかというのはこちらのドキュメントにまとまっています。

scss-lint/README.md at master · brigade/scss-lint · GitHub

ここからエラーを 0 にする修正を行っていきます。

scss-lint.yml を作成する

デフォルトの設定だとかなり厳しいルールになっているので、幾つかのルールを無効化するように設定することにします。

プロジェクトルートに .scss-lint.yml を作成し、ドキュメントを見ながらルールを無効化していきます。

この段階ではこのような .scss-lint.yml が出来上がりました。

scss_files: 'app/assets/stylesheets/**/*.scss'

exclude: 'app/assets/stylesheets/lib/**'

linters:
  BorderZero:
    convention: none

  ColorKeyword:     # ルール名に対して
    enabled: false  # enabled: false を指定して無効化する

  ColorVariable:
    enabled: false

  Comment:
    enabled: false

  DeclarationOrder:
    enabled: false

  EmptyLineBetweenBlocks:
    enabled: false

  IdSelector:
    enabled: false

  LeadingZero:
    enabled: false

  NameFormat:
    convention: '^[a-z][-a-z0-9_]*$'

  NestingDepth:
    max_depth: 4

  PropertySortOrder:
    enabled: false

  QualifyingElement:
    allow_element_with_attribute: true
    allow_element_with_class: true

  SelectorDepth:
    max_depth: 4

  Shorthand:
    enabled: false

  SpaceBeforeBrace:
    allow_single_line_padding: true

  StringQuotes:
    style: double_quotes

設定ファイルを作った状態で scss-lint を実行してみます。

$ bundle exec scss-lint --format=Stats
217  SpaceBeforeBrace               (across 58 files)
 62  StringQuotes                   (across 12 files)
 47  SpaceAfterPropertyColon        (across 16 files)
 32  NestingDepth                   (across 13 files)
 30  SelectorDepth                  (across 10 files)
 30  PseudoElement                  (across  7 files)
 27  FinalNewline                   (across 27 files)
 26  SpaceAfterComma                (across  2 files)
 21  ImportantRule                  (across 11 files)
 19  Indentation                    (across  7 files)
 16  SelectorFormat                 (across  9 files)
 14  SingleLinePerSelector          (across  9 files)
 14  VendorPrefix                   (across  3 files)
 12  ZeroUnit                       (across  7 files)
  5  EmptyRule                      (across  4 files)
  4  UnnecessaryMantissa            (across  1 files)
  4  MergeableSelector              (across  3 files)
  3  NameFormat                     (across  1 files)
  3  TrailingSemicolon              (across  3 files)
  3  PlaceholderInExtend            (across  1 files)
  2  BorderZero                     (across  2 files)
  1  SpaceAfterVariableName         (across  1 files)
  1  SpaceBetweenParens             (across  1 files)
---  -----------------------        -----------------
593  total                          (across 77 files)

ソースを変更せず、ルールを緩くしたことで 593 まで減りました。

csscomb で自動フォーマットする

f:id:sskyu:20160725174344p:plain

上記出力結果の 217 SpaceBeforeBrace (across 58 files)154 StringQuotes (across 67 files) などは簡単な修正で直りそうです。
人間の手でやると間違ってしまう可能性があるので、ツールを使ってやると良いでしょう。

csscomb を使うとスタイルシートの自動フォーマットをすることができます。

CSScomb: Makes your code beautiful

インストール

csscombは npm で提供されているので、node.js がインストールされている必要があります。

開発時に使うモジュールなので -D (--save-dev) を付けてインストールします。

$ npm i -D csscomb

csscomb用の設定ファイルの作成

csscomb を使い、どのようにフォーマットするのか指定するための設定ファイルを作ります。

下記のページからどのようにフォーマットしたいか、選択形式で作ることができます。

CSScomb: Build config

f:id:sskyu:20160725175703p:plain

例えば一番初めの選択肢の場合、Remove empty rulesets を有効にするかどうかを聞かれています。
有効にする場合は左のコードをクリックして、無効にする場合は右のコードをクリックします。
これを 24 つ続けていくと .csscomb.json のサンプルコードが表示されるので、このコードをコピーしてプロジェクトルートに .csscomb.json を作成しましょう。

作成したら下記コマンドで実行します。
(※ 大量の scss ファイルの差分が発生するのでここまでの作業をコミットしておくと良いです。)

$ ./node_module/.bin/csscomb app/assets/stylesheets/**/*.scss

コマンドが長いので package.json の scripts に登録しておきます。

  scripts: {
     "format:scss": "csscomb app/assets/stylesheets/**/*.scss"
  }

これで $ npm run format:scss で csscomb が動くようになりました。

いくつかの調整をして、.csscomb.json はこのような形になりました。

{
  "exclude": [
    "node_modules/**",
    "app/assets/stylesheets/lib/**",
    "app/assets/stylesheets/shared/colors.scss",
    "app/assets/stylesheets/web/mixins/z_indexes.scss"
  ],
  "always-semicolon": true,
  "color-case": "lower",
  "block-indent": "  ",
  "color-shorthand": true,
  "element-case": "lower",
  "eof-newline": true,
  "leading-zero": true,
  "quotes": "double",
  "space-before-colon": "",
  "space-after-colon": " ",
  "space-before-combinator": " ",
  "space-after-combinator": " ",
  "space-between-declarations": "\n",
  "space-before-opening-brace": " ",
  "space-after-opening-brace": "\n",
  "space-after-selector-delimiter": "\n",
  "space-before-selector-delimiter": "",
  "space-before-closing-brace": "\n",
  "strip-spaces": true,
  "tab-size": true,
  "unitless-zero": true
}

csscomb によるフォーマットを適用した状態で scss-lint を実行してみます。

$ bundle exec scss-lint --format=Stats
 32  NestingDepth                  (across 13 files)
 30  SelectorDepth                 (across 10 files)
 30  PseudoElement                 (across  7 files)
 26  SpaceAfterComma               (across  2 files)
 21  ImportantRule                 (across 11 files)
 16  SelectorFormat                (across  9 files)
 14  VendorPrefix                  (across  3 files)
  5  EmptyRule                     (across  4 files)
  4  MergeableSelector             (across  3 files)
  4  UnnecessaryMantissa           (across  1 files)
  3  PlaceholderInExtend           (across  1 files)
  3  NameFormat                    (across  1 files)
  2  BorderZero                    (across  2 files)
  1  SpaceAfterVariableName        (across  1 files)
  1  SpaceBetweenParens            (across  1 files)
---  ----------------------        -----------------
192  total                         (across 35 files)

フォーマットをして 192 個までエラーが減りました。

エラーを 0 にするまでの作業

ここからは地道に修正していくしかありません。

特別にやったことといえば、 VendorPrefix のルールに適合するため、autoprefixer-rails をいれて、ベンダープレフィックスは自動生成されるようにしたことくらいでしょうか。

GitHub - ai/autoprefixer-rails: Autoprefixer for Ruby and Ruby on Rails

特に難しかったのが NestingDepthSelectorDepth のルールをパスすることでした。

初期のソースは何故かネストが深い箇所が多々あり、まずはネストを一段下げてスタイル崩れが起きないか確認を頻繁に行いました。
ネストが深いとスタイルを上書きしたい場合にやりづらかったり、出力される css ファイルのサイズが肥大化したりと良いことがありません。
詳細度を上げてスタイルを作るよりも、クラス名にユニーク性を持たせてネストを下げるようにするべきです。

結局、設定で depth: 4 を指定しましたが、止むを得ず depth: 5 を指定して修正することになりました。

色々な妥協を経て、.scss-lint.yml はこのようになりました。

scss_files: 'app/assets/stylesheets/**/*.scss'

exclude: 'app/assets/stylesheets/lib/**'

linters:
  # border: 0 -> border: none
  BorderZero:
    convention: none

  # color: white などの利用を禁止
  ColorKeyword:
    enabled: false

  # color: $body-color のように変数を強制する
  ColorVariable:
    enabled: false

  # /* */ でのコメントを禁止
  Comment:
    enabled: false

  # @extend, @include, @content の記述順をスコープの先頭に強制する
  DeclarationOrder:
    enabled: false

  # ルール間に改行を強制する
  EmptyLineBetweenBlocks:
    enabled: false

  # 空のルールを禁止する
  EmptyRule:
    enabled: false

  # IDセレクタに対するスタイル付けを禁止する
  IdSelector:
    enabled: false

  # !importantを禁止する
  ImportantRule:
    enabled: false

  # opacity: 0.5 -> opacity: .5
  LeadingZero:
    enabled: false

  # 変数名の命名規則を定義する
  NameFormat:
    convention: '^[a-z][-a-z0-9_]*$'

  # ネストの深さを制限する (default: 3)
  NestingDepth:
    max_depth: 5

  # プロパティの記述順を強制する
  PropertySortOrder:
    enabled: false

  # div.selector などの書き方を制限する
  QualifyingElement:
    allow_element_with_attribute: true
    allow_element_with_class: true

  # セレクタの深さを制限する (default: 3)
  SelectorDepth:
    max_depth: 5

  # セレクタの名前のルールを定義する (default: hyphenated_lowercase)
  SelectorFormat:
    ignored_names:
      - 'fb_iframe_widget'  # facebook widget
      - 'field_with_errors' # generated by rails
    ignored_types:
      - 'id'

  # color: #ff0000 -> color: #f00 のように短い記述を強制する
  Shorthand:
    enabled: false

  SpaceAfterComma:
    exclude:
      - 'app/assets/stylesheets/shared/colors.scss'

  # p{} -> p {} のようにスペースを強制する
  SpaceBeforeBrace:
    allow_single_line_padding: true

  SpaceBetweenParens:
    exclude:
      - 'app/assets/stylesheets/shared/colors.scss'

  # 文字列を表すクォートの種類を指定する (default: single_quotes)
  StringQuotes:
    style: double_quotes

  # 必要のないvendor-prefixの記述を禁止する
  VendorPrefix:
    enabled: false

この状態で scss-lint を実行します。

$ bundle exec scss-lint --format=Stats
$

何も出力が表示されないということで、エラーを 0 にすることができました。

scss-lint を npm scripts に登録する

CI で実行するときに、npm test を叩けばフロントエンドのテストが走る状態にしたいので、bundle exec scss-lint も npm scripts に登録しておきます。

前回の内容も含めると、下記のようになります。

  "scripts": {
    "format:scss": "csscomb app/assets/stylesheets/**/*.scss",
    "test": "npm run test:lint",
    "test:lint": "npm run test:eslint && npm run test:coffeelint && npm run test:scsslint",
    "test:eslint": "eslint --ext .js,.jsx app/assets/javascripts",
    "test:coffeelint": "coffeelint -f .coffeelint.json app/assets/javascripts",
    "test:scsslint": "bundle exec scss-lint"
  },

これで最低限の品質を担保する仕組みが整いました。

おまけ

エディタに Lint 環境を整備する

私は ATOM Editor を使っているのですが、エディタが各種 Linter に対応しているとリアルタイムにエラーを報告してくれて便利なので是非設定しましょう。

ATOM に linter-scss-lint をセットアップする

まず linterlinter-scss-lint というパッケージをインストールします。

インストールしたら linter-scss-lint の Setting を開きます。

(ここからは ruby のバージョン管理に RVM を利用している場合の紹介になります。)

公式ドキュメントにある通り、まず scss-lint コマンドが使えるようになっている必要があります。

$ gem install scss_lint

次にコマンドのパスを取得します。

$ which scss-lint
/Users/sasaki_yutaka/.rvm/gems/ruby-2.3.1/bin/scss-lint

最後に linter-scss-lint の Settings の Executable Path に、上記パスの binwrappers に置換したパスを入力します。

/Users/sasaki_yutaka/.rvm/gems/ruby-2.3.1/wrappers/scss-lint

f:id:sskyu:20160726161007p:plain

この状態で .scss ファイルを編集するとリアルタイムに lint 結果が表示されて作業が捗ります。

f:id:sskyu:20160726161520p:plain

同様に、linter-coffeelintlinter-eslint も設定しておくと良いでしょう。

まとめ

この記事では Rails アプリケーションに scss-lint を設定する一例をご紹介しました。

エラーを 0 にして CI で回せることをゴールに設定したため妥協してルールを緩くしました。
ここからは緩くしたルールを厳しくして、リファクタに努めていくフェーズとなります。

Wondershake ではサーバーサイドエンジニアや iOSAndroid ディベロッパーを募集しています。
興味をお持ちの方は是非こちらからご応募下さい!

www.wantedly.com