├── .gitignore ├── .rspec ├── .rubocop.yml ├── .travis.yml ├── Gemfile ├── LICENSE.md ├── README.md ├── Rakefile ├── bin └── phare ├── lib ├── phare.rb └── phare │ ├── check.rb │ ├── check │ ├── eslint.rb │ ├── rubocop.rb │ └── stylelint.rb │ ├── check_suite.rb │ ├── cli.rb │ ├── git.rb │ └── version.rb ├── phare.gemspec └── spec ├── phare ├── check │ ├── eslint_spec.rb │ ├── rubocop_spec.rb │ └── stylelint_spec.rb ├── check_suite_spec.rb ├── cli_spec.rb └── git_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | Include: 3 | - phare.gemspec 4 | 5 | Documentation: 6 | Enabled: false 7 | 8 | Encoding: 9 | Enabled: false 10 | 11 | LineLength: 12 | Max: 200 13 | 14 | ClassLength: 15 | Max: 200 16 | 17 | AccessModifierIndentation: 18 | EnforcedStyle: outdent 19 | 20 | IfUnlessModifier: 21 | Enabled: false 22 | 23 | CaseIndentation: 24 | IndentWhenRelativeTo: case 25 | IndentOneStep: true 26 | 27 | MethodLength: 28 | CountComments: false 29 | Max: 20 30 | 31 | SignalException: 32 | Enabled: false 33 | 34 | ColonMethodCall: 35 | Enabled: false 36 | 37 | AsciiComments: 38 | Enabled: false 39 | 40 | Lambda: 41 | Enabled: false 42 | 43 | RegexpLiteral: 44 | Enabled: false 45 | 46 | AssignmentInCondition: 47 | Enabled: false 48 | 49 | ClassAndModuleChildren: 50 | Enabled: false 51 | 52 | GuardClause: 53 | Enabled: false 54 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | 3 | rvm: 4 | - 2.4.2 5 | - 2.3 6 | - 2.2 7 | - 2.1 8 | - 2.0 9 | 10 | script: 11 | - 'echo "Checking code style" && ./bin/phare' 12 | - 'echo "Running specs" && bundle exec rake spec' 13 | 14 | notifications: 15 | hipchat: 16 | rooms: 17 | secure: "OjYHtwmsG6bDC8mewX85yulCKxsVpvGiic32e6lDRq2TYmMtot/atOQaZ16YlMMN0CQqiJ8jfpil3HWCSK1iPZxSLHzCEu//bL6uoDJdPc/oXNV2BPAj0CxyHiJPpV3gdgdnZBbYGNdDPtjxo8FuZnM+cWIDl3yW0Y2rQkE7u0k=" 18 | template: 19 | - '%{repository}#%{build_number} (%{branch} - %{commit} : %{author}): %{message} (Build/Changes)' 20 | format: 'html' 21 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gemspec 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014-2015, Mirego 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | - Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | - Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | - Neither the name of the Mirego nor the names of its contributors may 13 | be used to endorse or promote products derived from this software without 14 | specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 19 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 20 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 21 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 22 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 23 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 24 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 25 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 26 | POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Phare 4 | 5 |
6 | Phare looks into your files and check for coding style errors. 7 |

8 | 9 | 10 | 11 |

12 | 13 | ## Installation 14 | 15 | Add these lines to your application’s `Gemfile` as development dependencies: 16 | 17 | ```ruby 18 | group :development do 19 | gem 'rubocop' # to check Ruby code 20 | gem 'scss-lint' # to check SCSS code 21 | 22 | gem 'phare' 23 | end 24 | ``` 25 | 26 | ```shell 27 | $ bundle install 28 | ``` 29 | 30 | If you wish to check for JavaScript code style using JSHint and JSCS, you must 31 | specify them in your `package.json` file: 32 | 33 | ```json 34 | { 35 | "name": "foo", 36 | "version": "0.0.1", 37 | "devDependencies": { 38 | "jshint": "latest", 39 | "jscs": "latest" 40 | } 41 | } 42 | ``` 43 | 44 | ```shell 45 | $ npm install 46 | ``` 47 | 48 | ### Shims 49 | 50 | Phare uses top-level commands in its checks (eg. `$ rubocop` and not `$ bundle exec rubocop`). 51 | You’ll need to run these commands in order to use the shims provided by either 52 | Bundler or NPM. 53 | 54 | #### Bundler 55 | 56 | ```shell 57 | $ bundle install 58 | $ bundle binstub rubocop scss-lint 59 | $ export PATH="./bin:$PATH" 60 | ``` 61 | 62 | #### npm 63 | 64 | ```shell 65 | $ npm install 66 | $ export PATH="./node_modules/.bin:$PATH" 67 | ``` 68 | 69 | ## Usage 70 | 71 | Phare provides an executable named `phare`. You can just use it as is: 72 | 73 | ```bash 74 | $ phare 75 | ``` 76 | 77 | ### Version control hook 78 | 79 | One of the best ways to use Phare is by hooking it to your version control 80 | commit process. For example, with `git`: 81 | 82 | ```bash 83 | $ bundle binstubs phare 84 | $ ln -s "`pwd`/bin/phare" .git/hooks/pre-commit 85 | ``` 86 | 87 | That way, every time `git commit` is ran, `phare` will be executed and the 88 | commit will be aborted if there are some errors. However, you can skip this 89 | check altogether by specifying `SKIP_PHARE=1` before your command. 90 | 91 | ```bash 92 | $ git commit -m 'Add stuff' 93 | $ SKIP_PHARE=1 git commit -m 'Add stuff and I don’t care about Phare' 94 | ``` 95 | 96 | ### Options 97 | 98 | #### Command-line 99 | 100 | | Option | Description 101 | |-------------|------------------------------------------------------------------------------------------------------------------------- 102 | | `directory` | The directory in which to run the checks (default is the current directory 103 | | `only` | The specific checks to run (e.g. `--only=rubocop,jscs`) 104 | | `skip` | The checks to skip (e.g. `--skip=scsslint`) 105 | 106 | #### `.phare.yml` 107 | 108 | Instead of using command-line arguments when running the `phare` command, you 109 | can create a `.phare.yml` file at the root of your project and hard-code options 110 | in that file. 111 | 112 | ```yaml 113 | skip: 114 | - scsslint 115 | - jshint 116 | ``` 117 | 118 | ## Contributors 119 | 120 | * [@remiprev](https://github.com/remiprev) 121 | * [@garno](https://github.com/garno) 122 | 123 | ## License 124 | 125 | `Phare` is © 2014-2015 [Mirego](http://www.mirego.com) and may be freely distributed under the [New BSD license](http://opensource.org/licenses/BSD-3-Clause). See the [`LICENSE.md`](https://github.com/mirego/phare/blob/master/LICENSE.md) file. 126 | 127 | The lighthouse logo is based on [this lovely icon](http://thenounproject.com/term/lighthouse/11608/) by [Nick Lacke](http://thenounproject.com/nicklacke), from The Noun Project. 128 | 129 | ## About Mirego 130 | 131 | [Mirego](http://mirego.com) is a team of passionate people who believe that work is a place where you can innovate and have fun. We're a team of [talented people](http://life.mirego.com) who imagine and build beautiful Web and mobile applications. We come together to share ideas and [change the world](http://mirego.org). 132 | 133 | We also [love open-source software](http://open.mirego.com) and we try to give back to the community as much as we can. 134 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | require 'rake' 3 | require 'bundler/gem_tasks' 4 | require 'rspec/core/rake_task' 5 | 6 | task default: :spec 7 | 8 | desc 'Run all specs' 9 | RSpec::Core::RakeTask.new(:spec) do |task| 10 | task.pattern = 'spec/**/*_spec.rb' 11 | end 12 | -------------------------------------------------------------------------------- /bin/phare: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | $LOAD_PATH.unshift File.join(File.dirname(__FILE__), *%w(.. lib)) 4 | 5 | require 'phare' 6 | 7 | Phare::CLI.new(ENV, ARGV).run 8 | -------------------------------------------------------------------------------- /lib/phare.rb: -------------------------------------------------------------------------------- 1 | require 'English' 2 | require 'optparse' 3 | require 'yaml' 4 | require 'phare/version' 5 | 6 | require 'phare/cli' 7 | require 'phare/git' 8 | 9 | require 'phare/check' 10 | require 'phare/check/rubocop' 11 | require 'phare/check/stylelint' 12 | require 'phare/check/eslint' 13 | 14 | require 'phare/check_suite' 15 | 16 | module Phare 17 | def self.system(*args) 18 | Kernel.system(*args) 19 | end 20 | 21 | def self.system_output(args) 22 | `#{args}` 23 | end 24 | 25 | def self.last_exit_status 26 | $CHILD_STATUS.exitstatus 27 | end 28 | 29 | def self.puts(*args) 30 | STDOUT.puts(*args) 31 | end 32 | 33 | def self.banner(string) 34 | Phare.puts '-' * string.length 35 | Phare.puts string 36 | Phare.puts '-' * string.length 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/phare/check.rb: -------------------------------------------------------------------------------- 1 | module Phare 2 | class Check 3 | attr_reader :status, :command, :tree 4 | 5 | def initialize(_directory, options = {}) 6 | @tree = Git.new(@extensions, options) 7 | end 8 | 9 | def run 10 | if should_run? 11 | print_banner 12 | Phare.system(command) 13 | @status = Phare.last_exit_status 14 | 15 | if @status == 0 16 | print_success_message 17 | else 18 | print_error_message 19 | end 20 | 21 | Phare.puts '' 22 | else 23 | @status = 0 24 | end 25 | end 26 | 27 | def should_run? 28 | should_run = binary_exists? 29 | 30 | [:configuration_exists?, :arguments_exists?].each do |condition| 31 | should_run &&= send(condition) if respond_to?(condition, true) 32 | end 33 | 34 | if @options[:diff] 35 | # NOTE: If the tree hasn't changed or if there is no files 36 | # to check (e.g. they are all in the exclude list), 37 | # we skip the check. 38 | should_run &&= @tree.changed? && files_to_check.any? 39 | end 40 | 41 | should_run 42 | end 43 | 44 | protected 45 | 46 | def excluded_files 47 | @excluded_files ||= [Dir.glob(excluded_list || [])].flatten 48 | end 49 | 50 | def excluded_list 51 | [] # Should be overriden by children 52 | end 53 | 54 | def files_to_check 55 | @files_to_check ||= @tree.changes - excluded_files 56 | end 57 | 58 | def print_success_message 59 | Phare.puts('Everything looks good from here!') 60 | end 61 | 62 | def print_error_message 63 | Phare.puts("Something went wrong. Program exited with #{@status}.") 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/phare/check/eslint.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | module Phare 3 | class Check 4 | class Eslint < Check 5 | GLOBAL_BINARY = 'eslint'.freeze 6 | LOCAL_BINARY = 'node_modules/.bin/eslint'.freeze 7 | 8 | attr_reader :config, :path 9 | 10 | def initialize(directory, options = {}) 11 | @directory = directory 12 | @config = File.expand_path("#{directory}.eslintrc", __FILE__) 13 | @path = File.expand_path("#{directory}app/assets/javascripts", __FILE__) 14 | @extensions = %w(.js) 15 | @options = options 16 | 17 | super 18 | end 19 | 20 | def command 21 | "#{binary} #{input}" 22 | end 23 | 24 | protected 25 | 26 | def configuration_exists? 27 | File.exist?(@config) 28 | end 29 | 30 | def binary 31 | local_binary_exists? ? @directory + LOCAL_BINARY : GLOBAL_BINARY 32 | end 33 | 34 | def binary_exists? 35 | local_binary_exists? || global_binary_exists? 36 | end 37 | 38 | def local_binary_exists? 39 | !Phare.system_output("which #{@directory}#{LOCAL_BINARY}").empty? 40 | end 41 | 42 | def global_binary_exists? 43 | !Phare.system_output("which #{GLOBAL_BINARY}").empty? 44 | end 45 | 46 | def input 47 | @tree.changed? ? files_to_check.join(' ') : "'#{@path}/**/*#{@extensions.first}'" 48 | end 49 | 50 | def arguments_exists? 51 | @tree.changed? || Dir.exist?(@path) 52 | end 53 | 54 | def print_banner 55 | Phare.banner 'Running ESLint to check for JavaScript style…' 56 | end 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/phare/check/rubocop.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | module Phare 3 | class Check 4 | class Rubocop < Check 5 | def initialize(directory, options = {}) 6 | @path = directory 7 | @extensions = %w(.rb) 8 | @options = options 9 | 10 | super 11 | end 12 | 13 | def command 14 | if @tree.changed? 15 | "rubocop #{files_to_check.join(' ')}" 16 | else 17 | 'rubocop' 18 | end 19 | end 20 | 21 | protected 22 | 23 | def excluded_list 24 | configuration_file['AllCops']['Exclude'] if configuration_file['AllCops'] && configuration_file['AllCops']['Exclude'] 25 | end 26 | 27 | def configuration_file 28 | @configuration_file ||= File.exist?('.rubocop.yml') ? YAML::load(File.open('.rubocop.yml')) : {} 29 | end 30 | 31 | def binary_exists? 32 | !Phare.system_output('which rubocop').empty? 33 | end 34 | 35 | def print_banner 36 | Phare.banner 'Running Rubocop to check for Ruby style…' 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/phare/check/stylelint.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | module Phare 3 | class Check 4 | class Stylelint < Check 5 | GLOBAL_BINARY = 'stylelint'.freeze 6 | LOCAL_BINARY = 'node_modules/.bin/stylelint'.freeze 7 | 8 | attr_reader :config, :path 9 | 10 | def initialize(directory, options = {}) 11 | @directory = directory 12 | @config = File.expand_path("#{directory}.stylelintrc", __FILE__) 13 | @path = File.expand_path("#{directory}app/assets/stylesheets", __FILE__) 14 | @extensions = %w(.scss) 15 | @options = options 16 | 17 | super 18 | end 19 | 20 | def command 21 | "#{binary} #{input}" 22 | end 23 | 24 | protected 25 | 26 | def configuration_exists? 27 | File.exist?(@config) 28 | end 29 | 30 | def binary 31 | local_binary_exists? ? @directory + LOCAL_BINARY : GLOBAL_BINARY 32 | end 33 | 34 | def binary_exists? 35 | local_binary_exists? || global_binary_exists? 36 | end 37 | 38 | def local_binary_exists? 39 | !Phare.system_output("which #{@directory}#{LOCAL_BINARY}").empty? 40 | end 41 | 42 | def global_binary_exists? 43 | !Phare.system_output("which #{GLOBAL_BINARY}").empty? 44 | end 45 | 46 | def input 47 | @tree.changed? ? files_to_check.join(' ') : "'#{@path}/**/*#{@extensions.first}'" 48 | end 49 | 50 | def arguments_exists? 51 | @tree.changed? || Dir.exist?(@path) 52 | end 53 | 54 | def print_banner 55 | Phare.banner 'Running Stylelint to check for SCSS style…' 56 | end 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/phare/check_suite.rb: -------------------------------------------------------------------------------- 1 | module Phare 2 | class CheckSuite 3 | attr_reader :status 4 | 5 | DEFAULT_CHECKS = { 6 | rubocop: Check::Rubocop, 7 | stylelint: Check::Stylelint, 8 | eslint: Check::Eslint 9 | } 10 | 11 | def initialize(options = {}) 12 | @options = options 13 | 14 | @directory = options[:directory] 15 | @directory << '/' unless @directory.end_with?('/') 16 | 17 | @options[:skip] ||= [] 18 | @options[:only] ||= [] 19 | end 20 | 21 | def checks 22 | checks = DEFAULT_CHECKS.keys 23 | 24 | if @options[:only].any? 25 | checks &= @options[:only] 26 | elsif @options[:skip] 27 | checks - @options[:skip] 28 | else 29 | checks 30 | end 31 | end 32 | 33 | def run 34 | @checks = checks.map do |check| 35 | check = DEFAULT_CHECKS[check] 36 | check.new(@directory, @options).tap(&:run).status 37 | end 38 | 39 | @status = @checks.find { |status| status > 0 } || 0 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/phare/cli.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | module Phare 3 | class CLI 4 | attr_reader :suite 5 | 6 | def initialize(env, argv) 7 | @env = env 8 | @options = parsed_options(argv) 9 | 10 | @suite = Phare::CheckSuite.new(@options) 11 | end 12 | 13 | def run 14 | if @options[:version] 15 | Phare.puts Phare::VERSION 16 | exit 0 17 | elsif @env['SKIP_CODE_CHECK'] || @env['SKIP_PHARE'] 18 | Phare.banner 'Skipping code style checking… Really? Well alright then…' 19 | exit 0 20 | else 21 | exit run_suite 22 | end 23 | end 24 | 25 | protected 26 | 27 | def parsed_options(argv) 28 | options = { directory: Dir.getwd } 29 | options.merge! parsed_options_from_yaml(File.join(options[:directory], '.phare.yml')) 30 | options.merge! parsed_options_from_arguments(argv) 31 | symbolize_options!(options) 32 | 33 | options 34 | end 35 | 36 | def symbolize_options!(options) 37 | options[:skip].map!(&:to_sym) if options[:skip] 38 | options[:only].map!(&:to_sym) if options[:only] 39 | 40 | options 41 | end 42 | 43 | # rubocop:disable Metrics/AbcSize 44 | def parsed_options_from_arguments(argv) 45 | options_to_merge = {} 46 | 47 | OptionParser.new do |opts| 48 | opts.banner = 'Usage: phare [options]' 49 | 50 | opts.on('--version', 'Display Phare’s version') do 51 | options_to_merge[:version] = true 52 | end 53 | 54 | opts.on('--directory x', 'The directory in which to run the checks (default is the current directory') do |directory| 55 | options_to_merge[:directory] = directory 56 | end 57 | 58 | opts.on('--skip x,y,z', 'Skip checks') do |checks| 59 | options_to_merge[:skip] = checks.split(',') 60 | end 61 | 62 | opts.on('--only x,y,z', 'Only run the specified checks') do |checks| 63 | options_to_merge[:only] = checks.split(',') 64 | end 65 | 66 | opts.on('--diff', 'Only run checks on modified files') do 67 | options_to_merge[:diff] = true 68 | end 69 | end.parse! argv 70 | 71 | options_to_merge 72 | end 73 | # rubocop:enable Metrics/AbcSize 74 | 75 | def parsed_options_from_yaml(file) 76 | options_to_merge = {} 77 | 78 | if File.exist?(file) 79 | # Load YAML content 80 | content = YAML.load(File.read(file)) 81 | 82 | # Symbolize keys 83 | options_to_merge = content.reduce({}) do |memo, (key, value)| 84 | memo.merge! key.to_sym => value 85 | end 86 | end 87 | 88 | options_to_merge 89 | end 90 | 91 | def run_suite 92 | if @suite.tap(&:run).status == 0 93 | Phare.banner 'Everything looks good, keep on committing!' 94 | 0 95 | else 96 | Phare.banner 'Something’s wrong with your code style. Please fix it before committing.' 97 | 1 98 | end 99 | end 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /lib/phare/git.rb: -------------------------------------------------------------------------------- 1 | module Phare 2 | class Git 3 | def initialize(extensions, options) 4 | @extensions = extensions 5 | @options = options 6 | end 7 | 8 | def changed? 9 | @options[:diff] && changes.any? 10 | end 11 | 12 | def changes 13 | @changes ||= Phare.system_output('git status -s').split("\n").each_with_object([]) do |diff, memo| 14 | filename = diff.split(' ').last 15 | 16 | if diff =~ /^[A|M].*/ && @extensions.include?(File.extname(filename)) 17 | memo << filename 18 | else 19 | next 20 | end 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/phare/version.rb: -------------------------------------------------------------------------------- 1 | module Phare 2 | VERSION = '1.0.1' 3 | end 4 | -------------------------------------------------------------------------------- /phare.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'phare/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = 'phare' 8 | spec.version = Phare::VERSION 9 | spec.authors = ['Rémi Prévost'] 10 | spec.email = ['rprevost@mirego.com'] 11 | spec.description = 'Phare looks into your files and check for (Ruby, JavaScript and SCSS) coding style errors.' 12 | spec.summary = spec.description 13 | spec.homepage = 'https://github.com/mirego/phare' 14 | spec.license = 'BSD 3-Clause' 15 | 16 | spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR) 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 19 | spec.require_paths = ['lib'] 20 | 21 | spec.add_development_dependency 'bundler', '~> 1.3' 22 | spec.add_development_dependency 'rake' 23 | spec.add_development_dependency 'rspec' 24 | spec.add_development_dependency 'rubocop', '0.27' 25 | end 26 | -------------------------------------------------------------------------------- /spec/phare/check/eslint_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Phare::Check::Eslint do 4 | let(:described_class) { Phare::Check::Eslint } 5 | 6 | describe :should_run? do 7 | let(:check) { described_class.new('.') } 8 | let(:config_exists?) { true } 9 | let(:path_exists?) { true } 10 | let(:local_which_output) { '' } 11 | let(:global_which_output) { '' } 12 | before do 13 | allow(Phare).to receive(:system_output).with('which eslint').and_return(global_which_output) 14 | allow(Phare).to receive(:system_output).with('which .node_modules/.bin/eslint').and_return(local_which_output) 15 | allow(File).to receive(:exist?).with(check.config).and_return(config_exists?) 16 | allow(Dir).to receive(:exist?).with(check.path).and_return(path_exists?) 17 | end 18 | 19 | context 'with found eslint command' do 20 | context 'for a local eslint' do 21 | let(:local_which_output) { 'node_modules/.bin/eslint' } 22 | it { expect(check).to be_able_to_run } 23 | end 24 | 25 | context 'for a global eslint' do 26 | let(:global_which_output) { 'eslint' } 27 | it { expect(check).to be_able_to_run } 28 | end 29 | 30 | context 'with only excluded files and the --diff option' do 31 | let(:global_which_output) { 'eslint' } 32 | let(:check) { described_class.new('.', diff: true) } 33 | let(:files) { ['foo.js'] } 34 | 35 | before do 36 | expect(check).to receive(:excluded_files).and_return(files).once 37 | expect(check.tree).to receive(:changes).and_return(files).at_least(:once) 38 | end 39 | 40 | it { expect(check.should_run?).to be_falsey } 41 | end 42 | end 43 | 44 | context 'with unfound eslint command' do 45 | let(:config_exists?) { false } 46 | let(:path_exists?) { false } 47 | it { expect(check).to_not be_able_to_run } 48 | end 49 | end 50 | 51 | describe :run do 52 | let(:check) { described_class.new('.') } 53 | let(:run!) { check.run } 54 | 55 | context 'with available Eslint' do 56 | let(:command) { check.command } 57 | 58 | before do 59 | expect(check).to receive(:should_run?).and_return(true) 60 | expect(Phare).to receive(:system).with(command) 61 | expect(Phare).to receive(:last_exit_status).and_return(eslint_exit_status) 62 | expect(check).to receive(:print_banner) 63 | end 64 | 65 | context 'without check errors' do 66 | let(:eslint_exit_status) { 0 } 67 | before { expect(Phare).to receive(:puts).with('Everything looks good from here!') } 68 | it { expect { run! }.to change { check.status }.to(eslint_exit_status) } 69 | end 70 | 71 | context 'with check errors' do 72 | let(:eslint_exit_status) { 1 } 73 | before { expect(Phare).to receive(:puts).with("Something went wrong. Program exited with #{eslint_exit_status}.") } 74 | it { expect { run! }.to change { check.status }.to(eslint_exit_status) } 75 | end 76 | 77 | context 'with --diff option' do 78 | let(:check) { described_class.new('.', diff: true) } 79 | let(:files) { ['foo.rb', 'bar.rb'] } 80 | let(:eslint_exit_status) { 0 } 81 | 82 | context 'without exclusions' do 83 | let(:command) { "eslint #{files.join(' ')}" } 84 | 85 | before do 86 | expect(check.tree).to receive(:changes).and_return(files).at_least(:once) 87 | expect(check).to receive(:excluded_files).and_return([]).once 88 | end 89 | 90 | it { expect { run! }.to change { check.status }.to(eslint_exit_status) } 91 | end 92 | 93 | context 'with exclusions' do 94 | let(:command) { 'eslint bar.rb' } 95 | 96 | before do 97 | expect(check).to receive(:excluded_files).and_return(['foo.rb']).once 98 | expect(check.tree).to receive(:changes).and_return(files).at_least(:once) 99 | end 100 | 101 | it { expect { run! }.to change { check.status }.to(eslint_exit_status) } 102 | end 103 | end 104 | end 105 | 106 | context 'with unavailable Eslint' do 107 | before do 108 | expect(check).to receive(:should_run?).and_return(false) 109 | expect(Phare).to_not receive(:system).with(check.command) 110 | expect(check).to_not receive(:print_banner) 111 | end 112 | 113 | it { expect { run! }.to change { check.status }.to(0) } 114 | end 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /spec/phare/check/rubocop_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Phare::Check::Rubocop do 4 | let(:described_class) { Phare::Check::Rubocop } 5 | 6 | describe :should_run? do 7 | let(:check) { described_class.new('.') } 8 | before { expect(Phare).to receive(:system_output).with('which rubocop').and_return(which_output) } 9 | 10 | context 'with found rubocop command' do 11 | let(:which_output) { 'rubocop' } 12 | it { expect(check).to be_able_to_run } 13 | 14 | context 'with only excluded files and the --diff option' do 15 | let(:check) { described_class.new('.', diff: true) } 16 | let(:files) { ['foo.rb'] } 17 | 18 | before do 19 | expect(check).to receive(:excluded_files).and_return(files).once 20 | expect(check.tree).to receive(:changes).and_return(files).at_least(:once) 21 | end 22 | 23 | it { expect(check.should_run?).to be_falsey } 24 | end 25 | end 26 | 27 | context 'with unfound rubocop command' do 28 | let(:which_output) { '' } 29 | it { expect(check).to_not be_able_to_run } 30 | end 31 | end 32 | 33 | describe :run do 34 | let(:check) { described_class.new('.') } 35 | let(:run!) { check.run } 36 | 37 | context 'with available Rubocop' do 38 | let(:command) { check.command } 39 | 40 | before do 41 | expect(check).to receive(:should_run?).and_return(true) 42 | expect(Phare).to receive(:system).with(command) 43 | expect(Phare).to receive(:last_exit_status).and_return(rubocop_exit_status) 44 | expect(check).to receive(:print_banner) 45 | end 46 | 47 | context 'with check errors' do 48 | let(:rubocop_exit_status) { 0 } 49 | it { expect { run! }.to change { check.status }.to(0) } 50 | end 51 | 52 | context 'without check errors' do 53 | let(:rubocop_exit_status) { 1337 } 54 | before { expect(Phare).to receive(:puts).with("Something went wrong. Program exited with #{rubocop_exit_status}.") } 55 | it { expect { run! }.to change { check.status }.to(rubocop_exit_status) } 56 | end 57 | 58 | context 'with --diff option' do 59 | let(:check) { described_class.new('.', diff: true) } 60 | let(:files) { ['foo.rb', 'bar.rb'] } 61 | let(:rubocop_exit_status) { 1337 } 62 | 63 | context 'without exclusions' do 64 | let(:command) { "rubocop #{files.join(' ')}" } 65 | 66 | before do 67 | expect(check.tree).to receive(:changes).and_return(files).at_least(:once) 68 | expect(check).to receive(:excluded_files).and_return([]).once 69 | end 70 | 71 | it { expect { run! }.to change { check.status }.to(rubocop_exit_status) } 72 | end 73 | 74 | context 'with exclusions' do 75 | let(:command) { 'rubocop bar.rb' } 76 | 77 | before do 78 | expect(check).to receive(:excluded_files).and_return(['foo.rb']).once 79 | expect(check.tree).to receive(:changes).and_return(files).at_least(:once) 80 | end 81 | 82 | it { expect { run! }.to change { check.status }.to(rubocop_exit_status) } 83 | end 84 | end 85 | end 86 | 87 | context 'with unavailable Rubocop' do 88 | before do 89 | expect(check).to receive(:should_run?).and_return(false) 90 | expect(Phare).to_not receive(:system).with(check.command) 91 | expect(check).to_not receive(:print_banner) 92 | end 93 | 94 | it { expect { run! }.to change { check.status }.to(0) } 95 | end 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /spec/phare/check/stylelint_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Phare::Check::Stylelint do 4 | let(:described_class) { Phare::Check::Stylelint } 5 | 6 | describe :should_run? do 7 | let(:check) { described_class.new('.') } 8 | let(:config_exists?) { true } 9 | let(:path_exists?) { true } 10 | let(:local_which_output) { '' } 11 | let(:global_which_output) { '' } 12 | before do 13 | allow(Phare).to receive(:system_output).with('which stylelint').and_return(global_which_output) 14 | allow(Phare).to receive(:system_output).with('which .node_modules/.bin/stylelint').and_return(local_which_output) 15 | allow(File).to receive(:exist?).with(check.config).and_return(config_exists?) 16 | allow(Dir).to receive(:exist?).with(check.path).and_return(path_exists?) 17 | end 18 | 19 | context 'with found stylelint command' do 20 | context 'for a local stylelint' do 21 | let(:local_which_output) { 'node_modules/.bin/stylelint' } 22 | it { expect(check).to be_able_to_run } 23 | end 24 | 25 | context 'for a global stylelint' do 26 | let(:global_which_output) { 'stylelint' } 27 | it { expect(check).to be_able_to_run } 28 | end 29 | 30 | context 'with only excluded files and the --diff option' do 31 | let(:global_which_output) { 'stylelint' } 32 | let(:check) { described_class.new('.', diff: true) } 33 | let(:files) { ['foo.scss'] } 34 | 35 | before do 36 | expect(check).to receive(:excluded_files).and_return(files).once 37 | expect(check.tree).to receive(:changes).and_return(files).at_least(:once) 38 | end 39 | 40 | it { expect(check.should_run?).to be_falsey } 41 | end 42 | end 43 | 44 | context 'with unfound stylelint command' do 45 | let(:config_exists?) { false } 46 | let(:path_exists?) { false } 47 | it { expect(check).to_not be_able_to_run } 48 | end 49 | end 50 | 51 | describe :run do 52 | let(:check) { described_class.new('.') } 53 | let(:run!) { check.run } 54 | 55 | context 'with available Stylelint' do 56 | let(:command) { check.command } 57 | 58 | before do 59 | expect(check).to receive(:should_run?).and_return(true) 60 | expect(Phare).to receive(:system).with(command) 61 | expect(Phare).to receive(:last_exit_status).and_return(stylelint_exit_status) 62 | expect(check).to receive(:print_banner) 63 | end 64 | 65 | context 'without check errors' do 66 | let(:stylelint_exit_status) { 0 } 67 | before { expect(Phare).to receive(:puts).with('Everything looks good from here!') } 68 | it { expect { run! }.to change { check.status }.to(stylelint_exit_status) } 69 | end 70 | 71 | context 'with check errors' do 72 | let(:stylelint_exit_status) { 1 } 73 | before { expect(Phare).to receive(:puts).with("Something went wrong. Program exited with #{stylelint_exit_status}.") } 74 | it { expect { run! }.to change { check.status }.to(stylelint_exit_status) } 75 | end 76 | 77 | context 'with --diff option' do 78 | let(:check) { described_class.new('.', diff: true) } 79 | let(:files) { ['foo.rb', 'bar.rb'] } 80 | let(:stylelint_exit_status) { 0 } 81 | 82 | context 'without exclusions' do 83 | let(:command) { "stylelint #{files.join(' ')}" } 84 | 85 | before do 86 | expect(check.tree).to receive(:changes).and_return(files).at_least(:once) 87 | expect(check).to receive(:excluded_files).and_return([]).once 88 | end 89 | 90 | it { expect { run! }.to change { check.status }.to(stylelint_exit_status) } 91 | end 92 | 93 | context 'with exclusions' do 94 | let(:command) { 'stylelint bar.rb' } 95 | 96 | before do 97 | expect(check).to receive(:excluded_files).and_return(['foo.rb']).once 98 | expect(check.tree).to receive(:changes).and_return(files).at_least(:once) 99 | end 100 | 101 | it { expect { run! }.to change { check.status }.to(stylelint_exit_status) } 102 | end 103 | end 104 | end 105 | 106 | context 'with unavailable Stylelint' do 107 | before do 108 | expect(check).to receive(:should_run?).and_return(false) 109 | expect(Phare).to_not receive(:system).with(check.command) 110 | expect(check).to_not receive(:print_banner) 111 | end 112 | 113 | it { expect { run! }.to change { check.status }.to(0) } 114 | end 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /spec/phare/check_suite_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Phare::CheckSuite do 4 | let(:described_class) { Phare::CheckSuite } 5 | 6 | describe :run do 7 | let(:options) { { directory: '.' } } 8 | let(:suite) { described_class.new(options) } 9 | 10 | before do 11 | Phare::CheckSuite::DEFAULT_CHECKS.values.each_with_index do |check, index| 12 | exit_status = exit_status_proc.call(index) 13 | allow_any_instance_of(check).to receive(:run) 14 | allow_any_instance_of(check).to receive(:status).and_return(exit_status) 15 | end 16 | end 17 | 18 | context 'with a single failing check' do 19 | let(:exit_status_proc) do 20 | proc { |index| index == 2 ? 1337 : 0 } 21 | end 22 | 23 | it { expect { suite.run }.to change { suite.status }.to(1337) } 24 | end 25 | 26 | context 'with a multiple failing checks' do 27 | let(:exit_status_proc) do 28 | proc { |index| (index * 500) + 20 } 29 | end 30 | 31 | it { expect { suite.run }.to change { suite.status }.to(20) } 32 | end 33 | 34 | context 'with no failing check' do 35 | let(:exit_status_proc) { proc { 0 } } 36 | 37 | it { expect { suite.run }.to change { suite.status }.to(0) } 38 | end 39 | end 40 | 41 | describe :checks do 42 | let(:options) { { directory: '.', skip: skip, only: only } } 43 | let(:suite) { described_class.new(options) } 44 | let(:skip) { [] } 45 | let(:only) { [] } 46 | 47 | context 'with "only" option' do 48 | let(:only) { [:rubocop, :foo, :eslint] } 49 | it { expect(suite.checks).to eql [:rubocop, :eslint] } 50 | end 51 | 52 | context 'with "skip" option' do 53 | let(:skip) { [:stylelint, :foo] } 54 | it { expect(suite.checks).to eql [:rubocop, :eslint] } 55 | end 56 | 57 | context 'with both "only" and "skip" option' do 58 | let(:skip) { [:stylelint, :rubocop] } 59 | let(:only) { [:stylelint, :foo, :eslint] } 60 | it { expect(suite.checks).to eql [:stylelint, :eslint] } 61 | end 62 | 63 | context 'with both "only" and "skip" option' do 64 | it { expect(suite.checks).to eql Phare::CheckSuite::DEFAULT_CHECKS.keys } 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /spec/phare/cli_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Phare::CLI do 4 | let(:described_class) { Phare::CLI } 5 | 6 | let(:cli) { described_class.new(env, argv) } 7 | let(:argv) { [] } 8 | let(:run!) { cli.run } 9 | let(:env) { {} } 10 | 11 | describe :run do 12 | context 'with version flag' do 13 | let(:cli) { described_class.new(env, ['--version']) } 14 | 15 | before do 16 | expect(Phare).to receive(:puts).with Phare::VERSION 17 | end 18 | 19 | it { expect { run! }.to exit_with_code(0) } 20 | end 21 | 22 | context 'with legacy code check skipping' do 23 | let(:env) { { 'SKIP_CODE_CHECK' => '1' } } 24 | it { expect { run! }.to exit_with_code(0) } 25 | end 26 | 27 | context 'with code check skipping' do 28 | let(:env) { { 'SKIP_PHARE' => '1' } } 29 | it { expect { run! }.to exit_with_code(0) } 30 | end 31 | 32 | context 'without code check skipping' do 33 | let(:env) { {} } 34 | 35 | context 'with suite errors' do 36 | before do 37 | expect(cli.suite).to receive(:run) 38 | expect(cli.suite).to receive(:status).and_return(1337) 39 | end 40 | 41 | it { expect { run! }.to exit_with_code(1) } 42 | end 43 | 44 | context 'without suite errors' do 45 | before do 46 | expect(cli.suite).to receive(:run) 47 | expect(cli.suite).to receive(:status).and_return(0) 48 | end 49 | 50 | it { expect { run! }.to exit_with_code(0) } 51 | end 52 | end 53 | end 54 | 55 | describe :parsed_options do 56 | let(:parsed_options) { cli.send(:parsed_options, argv) } 57 | 58 | before do 59 | expect(cli).to receive(:parsed_options_from_yaml).and_return(skip: ['scsslint']) 60 | expect(cli).to receive(:parsed_options_from_arguments).and_return(skip: ['rubocop']) 61 | end 62 | 63 | it { expect(parsed_options[:directory]).to eql Dir.pwd } 64 | it { expect(parsed_options[:skip]).to eql [:rubocop] } 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /spec/phare/git_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Phare::Git do 4 | let(:described_class) { Phare::Git } 5 | 6 | let(:extensions) { ['.rb'] } 7 | let(:options) { { diff: true } } 8 | let(:git) { described_class.new(extensions, options) } 9 | 10 | describe :changed? do 11 | context 'with --diff options' do 12 | before { expect(git).to receive(:changes).and_return(changes) } 13 | 14 | context 'with changes' do 15 | let(:changes) { %w(foo.rb) } 16 | 17 | it { expect(git.changed?).to eq(true) } 18 | end 19 | 20 | context 'without changes' do 21 | let(:changes) { [] } 22 | 23 | it { expect(git.changed?).to eq(false) } 24 | end 25 | end 26 | 27 | context 'without --diff options' do 28 | let(:options) { { diff: false } } 29 | 30 | it { expect(git.changed?).to eq(false) } 31 | end 32 | end 33 | 34 | describe :changes do 35 | before { expect(Phare).to receive(:system_output).with('git status -s').and_return(tree) } 36 | 37 | context 'with empty tree' do 38 | let(:tree) { '' } 39 | 40 | it { expect(git.changes).to be_empty } 41 | end 42 | 43 | context 'with untracked file' do 44 | let(:tree) { '?? foo.rb' } 45 | 46 | it { expect(git.changes).to be_empty } 47 | end 48 | 49 | context 'with added file' do 50 | let(:tree) { "A foo.rb\nAM bar.rb" } 51 | 52 | it { expect(git.changes).to match_array(%w(foo.rb bar.rb)) } 53 | end 54 | 55 | context 'with modified file' do 56 | let(:tree) { "M foo.rb\nDM bar.rb\n M foobar.rb" } 57 | 58 | it { expect(git.changes).to match_array(%w(foo.rb)) } 59 | end 60 | 61 | context 'with deleted file' do 62 | let(:tree) { " D foo.rb\nMD bar.rb\n D foobar.rb" } 63 | 64 | it { expect(git.changes).to match_array(%w(bar.rb)) } 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 2 | 3 | require 'rspec' 4 | require 'phare' 5 | 6 | RSpec.configure do |config| 7 | config.before(:each) do 8 | # Disable all logging 9 | allow(Phare).to receive(:puts) 10 | end 11 | end 12 | 13 | RSpec::Matchers.define :be_able_to_run do 14 | match do |actual| 15 | actual.send(:should_run?) == true 16 | end 17 | end 18 | 19 | RSpec::Matchers.define :exit_with_code do |expected_code| 20 | actual = nil 21 | 22 | match do |block| 23 | begin 24 | block.call 25 | rescue SystemExit => e 26 | actual = e.status 27 | end 28 | 29 | actual && actual == expected_code 30 | end 31 | 32 | supports_block_expectations do 33 | true 34 | end 35 | 36 | failure_message do 37 | "expected block to call exit(#{expected_code}) but exit" + 38 | (actual.nil? ? ' not called' : "(#{actual}) was called") 39 | end 40 | 41 | failure_message_when_negated do 42 | "expected block not to call exit(#{expected_code})" 43 | end 44 | 45 | description do 46 | "expect block to call exit(#{expected_code})" 47 | end 48 | end 49 | --------------------------------------------------------------------------------