├── .gemtest ├── .gitignore ├── .rspec ├── .tailor ├── .travis.yml ├── Gemfile ├── Gemfile.lock ├── History.md ├── README.md ├── Rakefile ├── bin └── tailor ├── features ├── configurable.feature ├── continuous_integration.feature ├── indentation.feature ├── step_definitions │ └── indentation_steps.rb ├── support │ ├── env.rb │ ├── hooks.rb │ ├── legacy │ │ ├── bad_op_spacing.rb │ │ ├── bad_ternary_colon_spacing.rb │ │ └── long_file_with_indentation.rb │ └── world.rb └── valid_ruby.feature ├── lib ├── ext │ └── string_ext.rb ├── tailor.rb └── tailor │ ├── cli.rb │ ├── cli │ └── options.rb │ ├── composite_observable.rb │ ├── configuration.rb │ ├── configuration │ ├── file_set.rb │ └── style.rb │ ├── critic.rb │ ├── formatter.rb │ ├── formatters │ ├── text.rb │ └── yaml.rb │ ├── lexed_line.rb │ ├── lexer.rb │ ├── lexer │ ├── lexer_constants.rb │ └── token.rb │ ├── logger.rb │ ├── problem.rb │ ├── rake_task.rb │ ├── reporter.rb │ ├── ruler.rb │ ├── rulers.rb │ ├── rulers │ ├── allow_camel_case_methods_ruler.rb │ ├── allow_conditional_parentheses.rb │ ├── allow_hard_tabs_ruler.rb │ ├── allow_invalid_ruby_ruler.rb │ ├── allow_screaming_snake_case_classes_ruler.rb │ ├── allow_trailing_line_spaces_ruler.rb │ ├── allow_unnecessary_double_quotes_ruler.rb │ ├── allow_unnecessary_interpolation_ruler.rb │ ├── indentation_spaces_ruler.rb │ ├── indentation_spaces_ruler │ │ ├── argument_alignment.rb │ │ ├── ast_xml.rb │ │ ├── indentation_manager.rb │ │ └── line_continuations.rb │ ├── max_code_lines_in_class_ruler.rb │ ├── max_code_lines_in_method_ruler.rb │ ├── max_line_length_ruler.rb │ ├── spaces_after_comma_ruler.rb │ ├── spaces_after_conditional_ruler.rb │ ├── spaces_after_lbrace_ruler.rb │ ├── spaces_after_lbracket_ruler.rb │ ├── spaces_after_lparen_ruler.rb │ ├── spaces_before_comma_ruler.rb │ ├── spaces_before_lbrace_ruler.rb │ ├── spaces_before_rbrace_ruler.rb │ ├── spaces_before_rbracket_ruler.rb │ ├── spaces_before_rparen_ruler.rb │ ├── spaces_in_empty_braces_ruler.rb │ └── trailing_newlines_ruler.rb │ ├── runtime_error.rb │ ├── tailorrc.erb │ └── version.rb ├── spec ├── functional │ ├── conditional_parentheses_spec.rb │ ├── conditional_spacing_spec.rb │ ├── configuration_spec.rb │ ├── horizontal_spacing │ │ ├── braces_spec.rb │ │ ├── brackets_spec.rb │ │ ├── comma_spacing_spec.rb │ │ ├── hard_tabs_spec.rb │ │ ├── long_lines_spec.rb │ │ ├── long_methods_spec.rb │ │ ├── parens_spec.rb │ │ └── trailing_whitespace_spec.rb │ ├── horizontal_spacing_spec.rb │ ├── indentation_spacing │ │ ├── argument_alignment_spec.rb │ │ ├── bad_indentation_spec.rb │ │ └── line_continuations_spec.rb │ ├── indentation_spacing_spec.rb │ ├── naming │ │ ├── camel_case_methods_spec.rb │ │ └── screaming_snake_case_classes_spec.rb │ ├── naming_spec.rb │ ├── rake_task_spec.rb │ ├── string_interpolation_spec.rb │ ├── string_quoting_spec.rb │ ├── vertical_spacing │ │ ├── class_length_spec.rb │ │ └── method_length_spec.rb │ └── vertical_spacing_spec.rb ├── spec_helper.rb ├── support │ ├── argument_alignment_cases.rb │ ├── bad_indentation_cases.rb │ ├── conditional_parentheses_cases.rb │ ├── conditional_spacing_cases.rb │ ├── good_indentation_cases.rb │ ├── horizontal_spacing_cases.rb │ ├── line_indentation_cases.rb │ ├── naming_cases.rb │ ├── rake_task_config_no_problems.rb │ ├── rake_task_config_problems.rb │ ├── string_interpolation_cases.rb │ ├── string_quoting_cases.rb │ └── vertical_spacing_cases.rb └── unit │ ├── tailor │ ├── cli │ │ └── options_spec.rb │ ├── cli_spec.rb │ ├── composite_observable_spec.rb │ ├── configuration │ │ ├── file_set_spec.rb │ │ └── style_spec.rb │ ├── configuration_spec.rb │ ├── critic_spec.rb │ ├── formatter_spec.rb │ ├── formatters │ │ └── yaml_spec.rb │ ├── lexed_line_spec.rb │ ├── lexer │ │ └── token_spec.rb │ ├── lexer_spec.rb │ ├── problem_spec.rb │ ├── reporter_spec.rb │ ├── ruler_spec.rb │ ├── rulers │ │ ├── indentation_spaces_ruler │ │ │ └── indentation_manager_spec.rb │ │ ├── indentation_spaces_ruler_spec.rb │ │ ├── spaces_after_comma_ruler_spec.rb │ │ ├── spaces_after_lbrace_ruler_spec.rb │ │ ├── spaces_before_lbrace_ruler_spec.rb │ │ └── spaces_before_rbrace_ruler_spec.rb │ ├── rulers_spec.rb │ └── version_spec.rb │ └── tailor_spec.rb └── tailor.gemspec /.gemtest: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/turboladen/tailor/9e89b9132db5734299c7026500093b5124b840bd/.gemtest -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .autotest 2 | .DS_Store 3 | .loadpath 4 | .project 5 | .ruby-version 6 | .yardoc/ 7 | logic.txt 8 | output.txt 9 | test.rb 10 | 11 | .bundle/ 12 | .idea/ 13 | coverage/ 14 | doc/ 15 | pkg/ 16 | tmp/ 17 | 18 | */.DS_Store 19 | atlassian-ide-plugin.xml 20 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --tty 2 | --color 3 | -------------------------------------------------------------------------------- /.tailor: -------------------------------------------------------------------------------- 1 | Tailor.config do |config| 2 | config.formatters "text" 3 | config.file_set 'lib/**/*.rb' 4 | 5 | config.file_set 'features/**/*.rb', :features do |style| 6 | style.max_line_length 90, level: :warn 7 | end 8 | 9 | config.file_set 'spec/**/*.rb', :spec do |style| 10 | style.max_line_length 105, level: :warn 11 | style.spaces_after_lbrace 1, level: :warn 12 | style.spaces_before_lbrace 1, level: :warn 13 | style.spaces_before_rbrace 1, level: :warn 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - ruby-2.0.0 4 | - ruby-2.1.4 5 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gemspec 3 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | tailor (1.4.1) 5 | log_switch (~> 0.3.0) 6 | nokogiri (>= 1.6.0) 7 | term-ansicolor (>= 1.0.5) 8 | text-table (>= 1.2.2) 9 | 10 | GEM 11 | remote: https://rubygems.org/ 12 | specs: 13 | aruba (0.6.1) 14 | childprocess (>= 0.3.6) 15 | cucumber (>= 1.1.1) 16 | rspec-expectations (>= 2.7.0) 17 | builder (3.2.2) 18 | childprocess (0.5.5) 19 | ffi (~> 1.0, >= 1.0.11) 20 | cucumber (1.3.17) 21 | builder (>= 2.1.2) 22 | diff-lcs (>= 1.1.3) 23 | gherkin (~> 2.12) 24 | multi_json (>= 1.7.5, < 2.0) 25 | multi_test (>= 0.1.1) 26 | diff-lcs (1.2.5) 27 | docile (1.1.5) 28 | fakefs (0.6.0) 29 | ffi (1.11.2) 30 | gherkin (2.12.2) 31 | multi_json (~> 1.3) 32 | log_switch (0.3.0) 33 | mini_portile2 (2.4.0) 34 | multi_json (1.10.1) 35 | multi_test (0.1.1) 36 | nokogiri (1.10.8) 37 | mini_portile2 (~> 2.4.0) 38 | rake (13.0.1) 39 | rspec (3.1.0) 40 | rspec-core (~> 3.1.0) 41 | rspec-expectations (~> 3.1.0) 42 | rspec-mocks (~> 3.1.0) 43 | rspec-core (3.1.7) 44 | rspec-support (~> 3.1.0) 45 | rspec-expectations (3.1.2) 46 | diff-lcs (>= 1.2.0, < 2.0) 47 | rspec-support (~> 3.1.0) 48 | rspec-its (1.1.0) 49 | rspec-core (>= 3.0.0) 50 | rspec-expectations (>= 3.0.0) 51 | rspec-mocks (3.1.3) 52 | rspec-support (~> 3.1.0) 53 | rspec-support (3.1.2) 54 | simplecov (0.9.1) 55 | docile (~> 1.1.0) 56 | multi_json (~> 1.0) 57 | simplecov-html (~> 0.8.0) 58 | simplecov-html (0.8.0) 59 | term-ansicolor (1.3.0) 60 | tins (~> 1.0) 61 | text-table (1.2.3) 62 | tins (1.3.3) 63 | yard (0.9.20) 64 | 65 | PLATFORMS 66 | ruby 67 | 68 | DEPENDENCIES 69 | aruba 70 | bundler 71 | cucumber (>= 1.0.2) 72 | fakefs (>= 0.4.2) 73 | rake 74 | rspec (~> 3.1) 75 | rspec-its 76 | simplecov (>= 0.4.0) 77 | tailor! 78 | yard (>= 0.7.0) 79 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'cucumber' 3 | require 'cucumber/rake/task' 4 | require 'rspec/core/rake_task' 5 | require 'yard' 6 | require_relative 'lib/tailor/rake_task' 7 | 8 | 9 | Tailor::RakeTask.new do |task| 10 | task.formatters = %w[yaml] 11 | task.tailor_opts = %w[--output-file=output.yml] 12 | end 13 | 14 | 15 | #------------------------------------------------------------------------------ 16 | # spec 17 | #------------------------------------------------------------------------------ 18 | RSpec::Core::RakeTask.new 19 | 20 | namespace :spec do 21 | desc 'Run specs with Ruby warnings turned on' 22 | RSpec::Core::RakeTask.new(:warn) do |t| 23 | t.ruby_opts = %w(-w) 24 | end 25 | 26 | desc 'Run unit tests' 27 | RSpec::Core::RakeTask.new(:unit) do |t| 28 | t.pattern = './spec/unit/**/*_spec.rb' 29 | end 30 | 31 | desc 'Run functional tests' 32 | RSpec::Core::RakeTask.new(:functional) do |t| 33 | t.pattern = './spec/functional/**/*_spec.rb' 34 | end 35 | end 36 | 37 | #------------------------------------------------------------------------------ 38 | # features 39 | #------------------------------------------------------------------------------ 40 | Cucumber::Rake::Task.new(:features) do |t| 41 | t.cucumber_opts = %w(--format progress features --tags ~@wip) 42 | end 43 | 44 | #------------------------------------------------------------------------------ 45 | # yard 46 | #------------------------------------------------------------------------------ 47 | YARD::Rake::YardocTask.new do |t| 48 | t.files = %w(lib/**/*.rb - History.md) 49 | t.options = %w(--private --protected --verbose) 50 | end 51 | 52 | 53 | desc 'Run RSpec examples and Cucumber features' 54 | task test: [:spec, :features] 55 | task default: [:test] 56 | -------------------------------------------------------------------------------- /bin/tailor: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require_relative '../lib/tailor/cli' 4 | require_relative '../lib/tailor/runtime_error' 5 | 6 | 7 | Tailor::Logger.log = false 8 | 9 | begin 10 | failure = Tailor::CLI.run(ARGV.dup) 11 | exit(1) if failure 12 | rescue Tailor::RuntimeError => ex 13 | STDERR.puts ex.message 14 | STDERR.puts ex.backtrace.join("\n") 15 | rescue SystemExit => ex 16 | exit(ex.status) 17 | rescue Exception => ex 18 | STDERR.puts("#{ex.message} (#{ex.class})") 19 | STDERR.puts(ex.backtrace.join("\n")) 20 | exit(1) 21 | end 22 | -------------------------------------------------------------------------------- /features/configurable.feature: -------------------------------------------------------------------------------- 1 | Feature: Configurable 2 | As a Ruby developer 3 | I want to be able to configure tailor to my style likings 4 | So that tailor only detects the problems that I care about. 5 | 6 | Scenario: Print configuration when no config file exists 7 | Given a file named ".tailor" should not exist 8 | When I successfully run `tailor --show-config` 9 | Then the output should match /Configuration/ 10 | And the output should match /Formatters.+|.+text.+|/ 11 | And the output should match /Label.+|.+default.+|/ 12 | And the output should match /Style.+|/ 13 | And the output should match /File List.+|/ 14 | And the exit status should be 0 15 | 16 | Scenario: Print configuration when .tailor exists 17 | Given a file named ".tailorrc" with: 18 | """ 19 | Tailor::Configuration::Style.define_property :some_ruler 20 | 21 | Tailor.config do |config| 22 | config.formatters 'test' 23 | config.file_set 'test/**/*.rb' do |style| 24 | style.some_ruler 1234 25 | end 26 | end 27 | """ 28 | When I successfully run `tailor --show-config` 29 | Then the output should match /Formatters.+|.+test.+|/ 30 | And the output should match /some_ruler.+|.+1234.+|/ 31 | 32 | @wip 33 | Scenario: Pass in configuration file at runtime 34 | Given a file named "some_config.yml" with: 35 | """ 36 | --- 37 | :indentation: 38 | :spaces: 7 39 | :vertical_whitespace: 40 | :trailing_newlines: 13 41 | """ 42 | When I successfully run `tailor --config some_config.yml` 43 | Then the output should contain: 44 | """ 45 | +-------------------------+------------------+ 46 | | Configuration | 47 | +-------------------------+------------------+ 48 | | Indentation | 49 | +-------------------------+------------------+ 50 | | spaces | 7 | 51 | +-------------------------+------------------+ 52 | | Vertical whitespace | 53 | +-------------------------+------------------+ 54 | | trailing_newlines | 13 | 55 | +-------------------------+------------------+ 56 | """ 57 | 58 | 59 | -------------------------------------------------------------------------------- /features/continuous_integration.feature: -------------------------------------------------------------------------------- 1 | Feature: Continuous Integration 2 | As a Ruby developer, I want builds to fail when my project encounters tailor 3 | errors so I can be sure to fix those errors as soon as possible. 4 | 5 | Scenario: tailor executable, warnings found, but no errors 6 | Given my configuration file ".tailor" looks like: 7 | """ 8 | Tailor.config do |config| 9 | config.file_set do |style| 10 | style.trailing_newlines 0, level: :warn 11 | end 12 | end 13 | """ 14 | And a file named "warnings.rb" with: 15 | """ 16 | puts 'hi' 17 | 18 | 19 | """ 20 | When I successfully run `tailor -d -c .tailor warnings.rb` 21 | Then the output should match /File has 2 trailing newlines/ 22 | And the exit status should be 0 23 | 24 | Scenario: tailor executable, errors found 25 | Given my configuration file ".tailor" looks like: 26 | """ 27 | Tailor.config do |config| 28 | config.file_set do |style| 29 | style.trailing_newlines 0, level: :error 30 | end 31 | end 32 | """ 33 | And a file named "errors.rb" with: 34 | """ 35 | puts 'hi' 36 | 37 | 38 | """ 39 | When I run `tailor -d -c .tailor errors.rb` 40 | Then the output should match /File has 2 trailing newlines/ 41 | And the output should not match /SystemExit/ 42 | And the exit status should be 1 43 | 44 | Scenario: Rake task, warnings found, but no errors 45 | Given a file named "warnings.rb" with: 46 | """ 47 | puts 'hi' 48 | 49 | 50 | """ 51 | And my configuration file ".tailor" looks like: 52 | """ 53 | Tailor.config do |config| 54 | config.file_set 'warnings.rb' do |style| 55 | style.trailing_newlines 0, level: :warn 56 | end 57 | end 58 | """ 59 | And a file named "Rakefile" with: 60 | """ 61 | require 'tailor/rake_task' 62 | 63 | Tailor::RakeTask.new 64 | """ 65 | When I successfully run `rake tailor` 66 | Then the output should match /File has 2 trailing newlines/ 67 | And the exit status should be 0 68 | 69 | Scenario: Rake task, errors found 70 | Given a file named "errors.rb" with: 71 | """ 72 | puts 'hi' 73 | 74 | 75 | """ 76 | And my configuration file ".tailor" looks like: 77 | """ 78 | Tailor.config do |config| 79 | config.file_set 'errors.rb' do |style| 80 | style.trailing_newlines 0, level: :error 81 | end 82 | end 83 | """ 84 | And a file named "Rakefile" with: 85 | """ 86 | require 'tailor/rake_task' 87 | 88 | Tailor::RakeTask.new 89 | """ 90 | When I run `rake tailor` 91 | Then the output should match /File has 2 trailing newlines/ 92 | And the output should not match /SystemExit/ 93 | And the exit status should be 1 94 | 95 | Scenario: Rake task, override config file 96 | Given a file named "errors.rb" with: 97 | """ 98 | puts 'hi' 99 | 100 | 101 | """ 102 | And my configuration file ".tailor" looks like: 103 | """ 104 | Tailor.config do |config| 105 | config.file_set 'errors.rb' do |style| 106 | style.trailing_newlines 0, level: :error 107 | end 108 | end 109 | """ 110 | And a file named "Rakefile" with: 111 | """ 112 | require 'tailor/rake_task' 113 | 114 | Tailor::RakeTask.new do |task| 115 | task.file_set 'errors.rb' do |style| 116 | style.trailing_newlines 2, level: :error 117 | end 118 | end 119 | """ 120 | When I successfully run `rake tailor` 121 | Then the output should match /errors\.rb\s+|\s+0/ 122 | 123 | Scenario: Rake task, missing config file 124 | Given a file named "errors.rb" with: 125 | """ 126 | puts 'hi' 127 | 128 | 129 | """ 130 | And my configuration file ".tailor" looks like: 131 | """ 132 | Tailor.config do |config| 133 | config.file_set 'errors.rb' do |style| 134 | style.trailing_newlines 0, level: :error 135 | end 136 | end 137 | """ 138 | And a file named "Rakefile" with: 139 | """ 140 | require 'tailor/rake_task' 141 | 142 | Tailor::RakeTask.new do |t| 143 | t.config_file = 'asdfasdfasdfasdfadsfasdfasdfadsfadsfadsfasdfasdfasdfsad' 144 | end 145 | """ 146 | When I run `rake tailor` 147 | Then the output should match /No config file found at/ 148 | And the output should not match /SystemExit/ 149 | And the exit status should be 1 150 | -------------------------------------------------------------------------------- /features/indentation.feature: -------------------------------------------------------------------------------- 1 | @wip 2 | Feature: Indentation check 3 | As a Ruby developer 4 | I want to check the indentation of my Ruby code 5 | So that I follow Ruby indentation conventions. 6 | 7 | Scenario: No indentation problems with this project 8 | Given my configuration file ".tailor" looks like: 9 | """ 10 | Tailor.config do |config| 11 | config.file_set do |style| 12 | style.trailing_newlines 0 13 | end 14 | end 15 | """ 16 | When I successfully run `tailor -d -c .tailor ../../lib` 17 | Then the output should contain "problem count: 0" 18 | And the exit status should be 0 19 | -------------------------------------------------------------------------------- /features/step_definitions/indentation_steps.rb: -------------------------------------------------------------------------------- 1 | Given /^(.+) exists with(\w*) a newline at the end$/ do |file_name, no_newline| 2 | file_contents = get_file_contents(file_name) 3 | expect(file_contents).to_not be_nil 4 | 5 | if no_newline.empty? 6 | file_contents << "\n" unless file_contents[-1] == "\n" 7 | else 8 | file_contents[-1] = '' if file_contents[-1] == "\n" 9 | end 10 | 11 | write_file(file_name, file_contents) 12 | end 13 | 14 | Given /^my configuration file "([^"]*)" looks like:$/ do |file_name, string| 15 | write_file(file_name, string) 16 | end 17 | -------------------------------------------------------------------------------- /features/support/env.rb: -------------------------------------------------------------------------------- 1 | require 'aruba/cucumber' 2 | require 'simplecov' 3 | 4 | SimpleCov.start do 5 | add_group 'Features', 'features/' 6 | add_group 'Lib', 'lib' 7 | end 8 | -------------------------------------------------------------------------------- /features/support/hooks.rb: -------------------------------------------------------------------------------- 1 | Before do 2 | @original_home = ENV['HOME'] 3 | ENV['HOME'] = '.' 4 | end 5 | 6 | After do 7 | ENV['HOME'] = @original_home 8 | end 9 | -------------------------------------------------------------------------------- /features/support/legacy/bad_op_spacing.rb: -------------------------------------------------------------------------------- 1 | # Comment with a + 2 | # Comment with a - 3 | # Comment with a * 4 | # Comment with a / 5 | # Comment with a = 6 | # Comment with a += 7 | # Comment with a -= 8 | # Comment with a *= 9 | # Comment with a /= 10 | # Comment with a %= 11 | # Comment with a **= 12 | # Comment with a ||= 13 | # Comment with a &&= 14 | # Comment with a == 15 | # Comment with a === 16 | # Comment with a != 17 | # Comment with a > 18 | # Comment with a < 19 | # Comment with a >= 20 | # Comment with a <= 21 | # Comment with a <=> 22 | # Comment with a && 23 | # Comment with a || 24 | # Comment with a ? 25 | # Comment with a : 26 | # Comment with a =~ 27 | 28 | class BadOps 29 | def +@ 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /features/support/legacy/bad_ternary_colon_spacing.rb: -------------------------------------------------------------------------------- 1 | # Perfect spacing around ternary colon 2 | bobo = true ? true : false 3 | 4 | # No space after ternary colon 5 | bobo = true ? true :false 6 | 7 | # No space before ternary colon 8 | bobo = true ? true: false 9 | 10 | # No space before or after ternary colon 11 | bobo = true ? true:false 12 | 13 | # 2 spaces after ternary colon 14 | bobo = true ? true : false 15 | 16 | # 2 spaces before ternary colon 17 | bobo = true ? true : false 18 | 19 | # Skip when colon is part of a symbol or namespace operator 20 | bobo = { thing: ":clown" } 21 | bobo[:thing] == :dog ? bobo[:thing] : Math::PI 22 | 23 | # Skip when colon is part of Regexp class 24 | bobo[:thing].scan(/[:alpha:]/) 25 | 26 | # Skip when setting load path 27 | $:.unshift File.dirname(__FILE__) 28 | 29 | # Skip when question mark method is followed by a symbol 30 | if bobo[:thing].eql? :clown 31 | end 32 | -------------------------------------------------------------------------------- /features/support/legacy/long_file_with_indentation.rb: -------------------------------------------------------------------------------- 1 | # This file contains 1 method, no class, and is properly indented 2 | # in order to test a related scenario. 3 | module MyModule 4 | 5 | # This is a class! 6 | class AnotherThing 7 | 8 | # This is a method! 9 | def a_method 10 | case 11 | when 1 12 | 1..10.each { |num| puts num } 13 | when 2 14 | else 15 | while false 16 | # Don't do anything 17 | # And stuff 18 | "meow".scan(/woem/) 19 | array = [1 ,2 ,3] 20 | other_thing = [ 21 | 4, 22 | 5, 23 | 6 24 | ] 25 | end 26 | end 27 | 28 | an_array = Array.new 29 | a_hash = Hash.new 30 | 31 | # This is another comment 32 | an_array = [1, 2, 3] 33 | a_hash = { 34 | one: 1, 35 | two: 2 36 | } 37 | 38 | if true 39 | # Let's return! 40 | return true 41 | elsif false 42 | return false 43 | else 44 | return nil 45 | end 46 | 47 | # Now how about a block... 48 | 1..10.times do |number| 49 | begin 50 | rescue 51 | ensure 52 | end 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /features/support/world.rb: -------------------------------------------------------------------------------- 1 | module IndentationHelpers 2 | 3 | # Used for getting the faked file contexts from the indentation_cases.rb file. 4 | # 5 | # @param [String] file_name The name of the fake file to get. 6 | # @return [String] 7 | def get_file_contents(file_name) 8 | path_chunks = file_name.split('/') 9 | const_name = path_chunks.first(2).each { |c| c.upcase! }.join('_') 10 | const = Kernel.const_get(const_name) 11 | 12 | const[path_chunks.last.to_sym] 13 | end 14 | end 15 | World(IndentationHelpers) 16 | -------------------------------------------------------------------------------- /features/valid_ruby.feature: -------------------------------------------------------------------------------- 1 | Feature: Valid Ruby 2 | As a Ruby developer, I want to check my project files to make sure they're 3 | valid Ruby, so when I see other tailor problems, I know why those problems 4 | might be there. 5 | 6 | Scenario: Extra 'end' 7 | Given a file named "extra_end.rb" with: 8 | """ 9 | def a_method 10 | puts 'stuff' 11 | end 12 | end 13 | 14 | """ 15 | When I run `tailor -d extra_end.rb` 16 | Then the output should match /TOTAL.*1/ 17 | And the output should match /File contains invalid Ruby/ 18 | -------------------------------------------------------------------------------- /lib/ext/string_ext.rb: -------------------------------------------------------------------------------- 1 | class String 2 | # Borrowed from ActiveSupport, this converts camel-case Strings to 3 | # snake-case. 4 | # 5 | # @return [String] 6 | def underscore 7 | self.gsub(/::/, '/'). 8 | gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2'). 9 | gsub(/([a-z\d])([A-Z])/, '\1_\2'). 10 | tr('-', '_'). 11 | downcase 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/tailor.rb: -------------------------------------------------------------------------------- 1 | require_relative 'tailor/configuration' 2 | 3 | class Tailor 4 | def self.config 5 | configuration = Tailor::Configuration.new 6 | yield configuration if block_given? 7 | 8 | configuration 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/tailor/cli.rb: -------------------------------------------------------------------------------- 1 | require_relative 'configuration' 2 | require_relative 'critic' 3 | require_relative 'cli/options' 4 | require_relative 'logger' 5 | require_relative 'reporter' 6 | 7 | 8 | class Tailor 9 | 10 | # The Command-Line Interface worker. Execution from the command line 11 | # comes through here. 12 | class CLI 13 | include LogSwitch::Mixin 14 | 15 | # @return [Tailor::Configuration] 16 | attr_reader :configuration 17 | 18 | # The main method of execution from the command line. 19 | # 20 | # @param [Array] args Arguments from the command-line. 21 | def self.run(args) 22 | new(args).execute! 23 | end 24 | 25 | # @param [Array] args Arguments from the command-line. 26 | def initialize(args) 27 | options = Options.parse!(args) 28 | @configuration = Configuration.new(args, options) 29 | @configuration.load! 30 | 31 | if options.show_config 32 | @configuration.show 33 | exit 34 | end 35 | 36 | @critic = Critic.new 37 | @reporter = Reporter.new(@configuration.formatters) 38 | end 39 | 40 | # This checks all of the files detected during the configuration gathering 41 | # process, then hands results over to the {Tailor::Reporter} to be reported. 42 | # 43 | # @return [Boolean] +true+ if no problems were detected; false if there 44 | # were. 45 | def execute! 46 | @critic.critique(@configuration.file_sets) do |problems_for_file, label| 47 | @reporter.file_report(problems_for_file, label) 48 | end 49 | 50 | @reporter.summary_report(@critic.problems, 51 | output_file: @configuration.output_file) 52 | 53 | @critic.problem_count(:error) > 0 54 | end 55 | 56 | # Critiques all file sets, then returns the problems found as a result. 57 | # 58 | # @return [Hash{String => Array}] The list of problems, where the keys are 59 | # the file names in which the problems were found, and the values are the 60 | # respective lists of problems for each file. 61 | def result 62 | @critic.critique(@configuration.file_sets) 63 | @critic.problems 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/tailor/composite_observable.rb: -------------------------------------------------------------------------------- 1 | class Tailor 2 | 3 | # Used by {Tailor::Lexer} to provide publishing methods for observers. 4 | # Any Ruler that wants to subscribe to Lexer events must use the methods 5 | # defined here. 6 | module CompositeObservable 7 | 8 | # Defines three instance methods that provide for observing a 9 | # {Tailor::Lexer} object. If +name+ was passed "test": 10 | # 11 | # * +#add_test_observer+ 12 | # * +#test_update+ 13 | # * +#notify_test_observers+ 14 | # 15 | # @param [String] name The name of event to observe/subscribe to. 16 | def self.define_observer(name) 17 | define_method("add_#{name}_observer") do |observer| 18 | @notifiers = {} unless defined? @notifiers 19 | @notifiers[name] = {} unless @notifiers[name] 20 | 21 | call_back = "#{name}_update".to_sym 22 | 23 | unless observer.respond_to? call_back 24 | raise NoMethodError, 25 | "observer '#{observer}' does not respond to '#{call_back}'" 26 | end 27 | 28 | @notifiers[name][observer] = call_back 29 | end 30 | 31 | define_method("notify_#{name}_observers") do |*args| 32 | if defined? @notifier_state and @notifier_state 33 | if @notifier_state[name] 34 | if defined? @notifiers and @notifiers[name] 35 | @notifiers[name].each do |k, v| 36 | k.send(v, *args) 37 | end 38 | end 39 | end 40 | end 41 | end 42 | 43 | define_method("#{name}_changed") do 44 | @notifier_state = {} unless defined? @notifier_state 45 | @notifier_state[name] = true 46 | end 47 | end 48 | 49 | define_observer :comma 50 | define_observer :comment 51 | define_observer :const 52 | define_observer :embexpr_beg 53 | define_observer :embexpr_end 54 | define_observer :file_beg 55 | define_observer :file_end 56 | define_observer :ident 57 | define_observer :ignored_nl 58 | define_observer :kw 59 | define_observer :lbrace 60 | define_observer :lbracket 61 | define_observer :lparen 62 | define_observer :nl 63 | define_observer :period 64 | define_observer :rbrace 65 | define_observer :rbracket 66 | define_observer :rparen 67 | define_observer :sp 68 | define_observer :tstring_beg 69 | define_observer :tstring_end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/tailor/configuration/file_set.rb: -------------------------------------------------------------------------------- 1 | require_relative '../runtime_error' 2 | require_relative '../logger' 3 | require_relative 'style' 4 | 5 | class Tailor 6 | class Configuration 7 | class FileSet < Hash 8 | include Tailor::Logger::Mixin 9 | 10 | DEFAULT_GLOB = 'lib/**/*.rb' 11 | 12 | attr_reader :style 13 | attr_accessor :file_list 14 | 15 | # @param [Hash] style Style options to merge into the default Style 16 | # settings. 17 | # @param [String,Array] file_expression 18 | def initialize(file_expression=nil, style=nil) 19 | @style = if style 20 | Style.new.to_hash.merge(style) 21 | else 22 | Style.new.to_hash 23 | end 24 | 25 | self[:style] = @style 26 | 27 | file_expression ||= DEFAULT_GLOB 28 | @file_list = build_file_list(file_expression) 29 | self[:file_list] = @file_list 30 | end 31 | 32 | def update_file_list(file_expression) 33 | new_list = build_file_list(file_expression) 34 | @file_list.concat(new_list).uniq! 35 | end 36 | 37 | def update_style(new_style) 38 | @style.to_hash.merge!(new_style) 39 | end 40 | 41 | def [](key) 42 | if key == :style 43 | @style.to_hash 44 | elsif key == :file_list 45 | @file_list 46 | else 47 | raise Tailor::RuntimeError, "Invalid key requested: #{key}" 48 | end 49 | end 50 | 51 | def file_list=(file_expression) 52 | @file_list = build_file_list(file_expression) 53 | end 54 | 55 | private 56 | 57 | # The list of the files in the project to check. 58 | # 59 | # @param [String] file_expression Path to the file, directory or file_expression to check. 60 | # @return [Array] The list of files to check. 61 | def build_file_list(file_expression) 62 | files_in_project = if file_expression.is_a? Array 63 | log "Configured file_expression is an Array: #{file_expression}" 64 | 65 | file_expression.map do |e| 66 | if File.directory?(e) 67 | all_files_in_dir(e) 68 | else 69 | e 70 | end 71 | end.flatten.uniq 72 | elsif File.directory? file_expression 73 | log "Configured file_expression is an directory: #{file_expression}" 74 | all_files_in_dir(file_expression) 75 | elsif File.file? file_expression 76 | log "Configured file_expression is a single-file: #{file_expression}" 77 | [file_expression] 78 | else 79 | log "Configured file_expression is a glob: #{file_expression}" 80 | Dir.glob file_expression 81 | end 82 | 83 | list_with_absolute_paths = [] 84 | 85 | files_in_project.each do |file| 86 | new_file = File.expand_path(file) 87 | log "file: #{new_file}" 88 | 89 | if File.exists? new_file 90 | list_with_absolute_paths << new_file 91 | end 92 | end 93 | 94 | list_with_absolute_paths.sort 95 | end 96 | 97 | # Gets a list of only files that are in +base_dir+. 98 | # 99 | # @param [String] base_dir The directory to get the file list for. 100 | # @return [Array] The List of files. 101 | def all_files_in_dir(base_dir) 102 | Dir.glob(File.join(base_dir, '**', '*')).find_all do |file| 103 | file if File.file?(file) 104 | end 105 | end 106 | end 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /lib/tailor/configuration/style.rb: -------------------------------------------------------------------------------- 1 | class Tailor 2 | class Configuration 3 | class Style 4 | 5 | # Adds a style property to a Style object. If you're planning on creating 6 | # your own {Ruler}, you need to register the property here. 7 | # 8 | # Defines a method from +name+ that takes 2 parameters: +value+ and 9 | # +options+. +value+ is the value to use for the {Ruler} of the same +name+ 10 | # for checking style. +options+ can include anything that's necessary for 11 | # style checking. A +:level+ option key is used to determine the 12 | # {Tailor::Problem} level: 13 | # * +:error+ results in a exit status of 1. 14 | # * +:warn+ results in an exit status of 0, but gets printed in the 15 | # report. 16 | # 17 | # Example: 18 | # Tailor::Configuration::Style.define_property(:my_style_property) 19 | # style = Tailor::Configuration::Style.new 20 | # style.my_style_property(100, level: :warn) 21 | def self.define_property(name) 22 | define_method(name) do |value, *options| 23 | options = options.first || { level: :error } 24 | instance_variable_set("@#{name}".to_sym, [value, options]) 25 | end 26 | end 27 | 28 | define_property :allow_camel_case_methods 29 | define_property :allow_conditional_parentheses 30 | define_property :allow_hard_tabs 31 | define_property :allow_screaming_snake_case_classes 32 | define_property :allow_trailing_line_spaces 33 | define_property :allow_unnecessary_double_quotes 34 | define_property :allow_invalid_ruby 35 | define_property :allow_unnecessary_interpolation 36 | define_property :indentation_spaces 37 | define_property :max_code_lines_in_class 38 | define_property :max_code_lines_in_method 39 | define_property :max_line_length 40 | define_property :spaces_after_comma 41 | define_property :spaces_after_conditional 42 | define_property :spaces_after_lbrace 43 | define_property :spaces_after_lbracket 44 | define_property :spaces_after_lparen 45 | define_property :spaces_before_comma 46 | define_property :spaces_before_lbrace 47 | define_property :spaces_before_rbrace 48 | define_property :spaces_before_rbracket 49 | define_property :spaces_before_rparen 50 | define_property :spaces_in_empty_braces 51 | define_property :trailing_newlines 52 | 53 | # Sets up default values. 54 | def initialize 55 | allow_camel_case_methods(false, level: :error) 56 | allow_conditional_parentheses(false, level: :warn) 57 | allow_hard_tabs(false, level: :error) 58 | allow_screaming_snake_case_classes(false, level: :error) 59 | allow_trailing_line_spaces(false, level: :error) 60 | allow_unnecessary_interpolation(false, level: :warn) 61 | allow_unnecessary_double_quotes(false, level: :warn) 62 | allow_invalid_ruby(false, level: :warn) 63 | indentation_spaces(2, level: :error) 64 | max_code_lines_in_class(300, level: :error) 65 | max_code_lines_in_method(30, level: :error) 66 | max_line_length(80, level: :error) 67 | spaces_after_comma(1, level: :error) 68 | spaces_after_conditional(1, level: :error) 69 | spaces_after_lbrace(1, level: :error) 70 | spaces_after_lbracket(0, level: :error) 71 | spaces_after_lparen(0, level: :error) 72 | spaces_before_comma(0, level: :error) 73 | spaces_before_lbrace(1, level: :error) 74 | spaces_before_rbrace(1, level: :error) 75 | spaces_before_rbracket(0, level: :error) 76 | spaces_before_rparen(0, level: :error) 77 | spaces_in_empty_braces(0, level: :error) 78 | trailing_newlines(1, level: :error) 79 | end 80 | 81 | # Returns the current style as a Hash. 82 | # 83 | # @return [Hash] 84 | def to_hash 85 | instance_variables.inject({}) do |result, ivar| 86 | result[ivar.to_s.sub(/@/, '').to_sym] = instance_variable_get(ivar) 87 | 88 | result 89 | end 90 | end 91 | 92 | # Yields each property and values. 93 | def each 94 | to_hash.each do |property, values| 95 | yield property, values 96 | end 97 | end 98 | end 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /lib/tailor/critic.rb: -------------------------------------------------------------------------------- 1 | require_relative 'lexer' 2 | require_relative 'logger' 3 | require_relative 'ruler' 4 | require_relative 'rulers' 5 | 6 | 7 | class Tailor 8 | 9 | # An object of this type provides for starting the process of critiquing 10 | # files. It handles initializing the Ruler objects it needs based on the 11 | # configuration given to it. 12 | class Critic 13 | include LogSwitch::Mixin 14 | include Tailor::Rulers 15 | 16 | # The instance method that starts the process of looking for problems in 17 | # files. It checks for problems in each file in each file set. It yields 18 | # the problems found and the label they were found for. 19 | # 20 | # @param [Hash] file_sets The file sets to critique. 21 | # @yieldparam [Hash] problems The problems found for the label. 22 | # @yieldparam [Symbol] label The label the problems were found for. 23 | def critique(file_sets) 24 | log "file sets keys: #{file_sets.keys}" 25 | 26 | file_sets.each do |label, file_set| 27 | log "Critiquing file_set: #{file_set}" 28 | 29 | file_set[:file_list].each do |file| 30 | log "Critiquing file: #{file}" 31 | 32 | begin 33 | problems = check_file(file, file_set[:style]) 34 | rescue => ex 35 | $stderr.puts "Error while parsing file #{file}" 36 | raise(ex) 37 | end 38 | 39 | yield [problems, label] if block_given? 40 | end 41 | end 42 | end 43 | 44 | # @return [Hash{String => Array}] The list of problems, where the keys are 45 | # the file names in which the problems were found, and the values are the 46 | # respective lists of problems for each file. 47 | def problems 48 | @problems ||= {} 49 | end 50 | 51 | # @return [Fixnum] The number of problems found so far. 52 | def problem_count(type=nil) 53 | if type.nil? 54 | problems.values.flatten.size 55 | else 56 | problems.values.flatten.find_all { |v| v[:level] == :error }.size 57 | end 58 | end 59 | 60 | # Adds problems found from Lexing to the +#problems+ list. 61 | # 62 | # @param [String] file The file to open, read, and check. 63 | # @return [Hash] The Problems for that file. 64 | def check_file(file, style) 65 | log "<#{self.class}> Checking style of file: #{file}." 66 | lexer = Tailor::Lexer.new(file) 67 | ruler = Ruler.new 68 | log 'Style:' 69 | style.each { |property, values| log "#{property}: #{values}" } 70 | init_rulers(style, lexer, ruler) 71 | 72 | lexer.lex 73 | problems[file] = ruler.problems 74 | 75 | { file => problems[file] } 76 | end 77 | 78 | private 79 | 80 | # Creates Rulers for each ruler given in +style+ and adds the Ruler's 81 | # defined observers to the given +lexer+. 82 | # 83 | # @param [Hash] style The Hash that defines the style to be measured 84 | # against. 85 | # @param [Tailor::Lexer] lexer The Lexer object the Rulers should observe. 86 | # @param [Tailor::Ruler] parent_ruler The main Ruler to add the child 87 | # Rulers to. 88 | def init_rulers(style, lexer, parent_ruler) 89 | style.each do |ruler_name, values| 90 | ruler = "Tailor::Rulers::#{camelize(ruler_name.to_s)}Ruler" 91 | 92 | if values.last[:level] == :off || values.last[:level] == 'off' 93 | msg = "Style option set to '#{values.last[:level]}'; " 94 | log msg << "skipping init of '#{ruler}'" 95 | next 96 | end 97 | 98 | log "Initializing ruler: #{ruler}" 99 | ruler = instance_eval("#{ruler}.new(#{values.first}, #{values.last})") 100 | parent_ruler.add_child_ruler(ruler) 101 | 102 | ruler.lexer_observers.each do |observer| 103 | log "Adding #{observer} to lexer..." 104 | meth = "add_#{observer}_observer".to_sym 105 | lexer.send(meth, ruler) 106 | end 107 | end 108 | end 109 | 110 | # Converts a snake-case String to a camel-case String. 111 | # 112 | # @param [String] string The String to convert. 113 | # @return [String] The converted String. 114 | def camelize(string) 115 | string.split(/_/).map { |word| word.capitalize }.join 116 | end 117 | end 118 | end 119 | -------------------------------------------------------------------------------- /lib/tailor/formatter.rb: -------------------------------------------------------------------------------- 1 | class Tailor 2 | 3 | # This is really just a base class for defining other Formatter types. 4 | class Formatter 5 | def initialize 6 | @pwd = Pathname(Dir.pwd) 7 | end 8 | 9 | # This method gets called by {Tailor::Reporter} after each file is 10 | # critiqued. Redefine this to do what you want for that part of your 11 | # report. 12 | def file_report(file_problems, label) 13 | # Redefine this for your formatter... 14 | end 15 | 16 | # This method gets called by {Tailor::Reporter} after all files are 17 | # critiqued. Redefine this to do what you want for that part of your 18 | # report. 19 | def summary_report(all_problems) 20 | # Redefine this for your formatter... 21 | end 22 | 23 | # @param [Hash] problems 24 | # @param [Symbol] level The level of problem to find. 25 | # @return [Array] Problem list at the given level. 26 | def problems_at_level(problems, level) 27 | problems.values.flatten.find_all { |v| v[:level] == level } 28 | end 29 | 30 | # Gets a list of all types of problems included in the problem set. 31 | # 32 | # @param [Array] problems 33 | # @return [Array] The list of problem types. 34 | def problem_levels(problems) 35 | problems.values.flatten.collect { |v| v[:level] }.uniq 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/tailor/formatters/yaml.rb: -------------------------------------------------------------------------------- 1 | require 'pathname' 2 | require 'yaml' 3 | require_relative '../formatter' 4 | 5 | class Tailor 6 | module Formatters 7 | class Yaml < Tailor::Formatter 8 | attr_reader :accepts_output_file 9 | 10 | def initialize 11 | @accepts_output_file = true 12 | super 13 | end 14 | 15 | # Prints the report on all of the files that just got checked. 16 | # 17 | # @param [Hash] report Values are filenames; keys are problems for each 18 | # of those files. 19 | def summary_report(report) 20 | build_hash(report).to_yaml 21 | end 22 | 23 | private 24 | 25 | # @param [Hash] report The list of problems found by Tailor::CLI. 26 | # @return [Hash] The Hash of problems to be converted to YAML. 27 | def build_hash(report) 28 | report.reject! { |_, v| v.empty? } 29 | 30 | report.inject({}) do |result, problem_set| 31 | file_name = problem_set.first 32 | problems = problem_set.last 33 | 34 | problems.each do |problem| 35 | result[file_name] ||= [] 36 | 37 | result[file_name] << { 38 | type: problem[:type], 39 | line: problem[:line], 40 | column: problem[:column], 41 | message: problem[:message], 42 | level: problem[:level] 43 | } 44 | end 45 | 46 | result 47 | end 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/tailor/lexer/lexer_constants.rb: -------------------------------------------------------------------------------- 1 | require 'set' 2 | 3 | class Tailor 4 | 5 | # These are important tokens that key certain styling events. They are taken 6 | # from: 7 | # * https://github.com/ruby/ruby/blob/trunk/ext/ripper/eventids2.c 8 | # * https://github.com/ruby/ruby/blob/trunk/parse.y 9 | module LexerConstants 10 | KEYWORDS_TO_INDENT = Set.new %w[ 11 | begin 12 | case 13 | class 14 | def 15 | do 16 | else 17 | elsif 18 | ensure 19 | for 20 | if 21 | module 22 | rescue 23 | unless 24 | until 25 | when 26 | while 27 | ] 28 | 29 | CONTINUATION_KEYWORDS = Set.new %w[ 30 | elsif 31 | else 32 | ensure 33 | rescue 34 | when 35 | ] 36 | 37 | KEYWORDS_AND_MODIFIERS = Set.new %w[ 38 | if 39 | unless 40 | until 41 | while 42 | ] 43 | 44 | MODIFIERS = { 45 | 'if' => :if_mod, 46 | 'rescue' => :rescue_mod, 47 | 'unless' => :unless_mod, 48 | 'until' => :until_mod, 49 | 'while' => :while_mod 50 | } 51 | 52 | #noinspection RubyLiteralArrayInspection 53 | MULTILINE_OPERATORS = Set.new [ 54 | '+', '-', '*', '**', '/', '%', # +, -, tSTAR, tPOW, /, % 55 | '<', '>', '<=', '>=', # <, >, tLEQ, tGEQ 56 | '=', '+=', '-=', '*=', '**=', '/=', '%=', 57 | '&&=', '||=', '<<=', # ...tOP_ASGN... 58 | '>>', '<<', # tRSHFT, tLSHFT 59 | '!', '&', '?', ':', '^', '~', # !, tAMPER, ?, :, ^, ~ 60 | #'|', 61 | '&&', '||', # tANDOP, tOROP 62 | '==', '===', '<=>', '!=', # tEQ, tEQQ, tCMP, tNEQ 63 | '=~', '!~', # tMATCH, tNMATCH 64 | '..', '...', # tDOT2, tDOT3 65 | '::', # tCOLON2 (not sure about tCOLON3) 66 | #'[]', '[]=', # tAREF, tASET (not sure about these) 67 | '=>', # tASSOC 68 | '->', # tLAMBDA 69 | '~>' # gem_version op 70 | ] 71 | 72 | LOOP_KEYWORDS = Set.new %w[ 73 | for 74 | until 75 | while 76 | ] 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /lib/tailor/lexer/token.rb: -------------------------------------------------------------------------------- 1 | require 'ripper' 2 | require_relative 'lexer_constants' 3 | require_relative '../logger' 4 | 5 | class Tailor 6 | class Lexer < ::Ripper::Lexer 7 | 8 | # Helper methods for tokens that are parsed by {Tailor::Lexer}. 9 | class Token < String 10 | include Tailor::LexerConstants 11 | include Tailor::Logger::Mixin 12 | 13 | # @param [String] the_token 14 | # @param [Hash] options 15 | def initialize(the_token, options={}) 16 | super(the_token) 17 | @options = options 18 | end 19 | 20 | # Checks if +self+ is in +{KEYWORDS_TO_INDENT}+. 21 | # 22 | # @return [Boolean] 23 | def keyword_to_indent? 24 | KEYWORDS_TO_INDENT.include? self 25 | end 26 | 27 | # Checks if +self+ is in +{CONTINUATION_KEYWORDS}+. 28 | # 29 | # @return [Boolean] 30 | def continuation_keyword? 31 | CONTINUATION_KEYWORDS.include? self 32 | end 33 | 34 | # @return [Boolean] 35 | def ends_with_newline? 36 | self =~ /\n$/ 37 | end 38 | 39 | # Checks if +self+ is "do" and +@options[:loop_with_do] is true. 40 | # 41 | # @return [Boolean] 42 | def do_is_for_a_loop? 43 | self == 'do' && @options[:loop_with_do] 44 | end 45 | 46 | # @return [Boolean] 47 | def screaming_snake_case? 48 | self =~ /[A-Z].*_/ 49 | end 50 | 51 | # @return [Boolean] 52 | def contains_capital_letter? 53 | self =~ /[A-Z]/ 54 | end 55 | 56 | # @return [Boolean] 57 | def contains_hard_tab? 58 | self =~ /\t/ 59 | end 60 | 61 | # Checks the current line to see if +self+ is being used as a modifier. 62 | # 63 | # @return [Boolean] True if there's a modifier in the current line that 64 | # is the same type as +token+. 65 | def modifier_keyword? 66 | return false unless keyword_to_indent? 67 | 68 | line_of_text = @options[:full_line_of_text] 69 | log "Line of text: #{line_of_text}" 70 | 71 | catch(:result) do 72 | sexp_line = Ripper.sexp(line_of_text) 73 | 74 | if sexp_line.nil? 75 | msg = 'sexp line was nil. ' 76 | msg << 'Perhaps that line is part of a multi-line statement?' 77 | log msg 78 | log 'Trying again with the last char removed from the line...' 79 | line_of_text.chop! 80 | sexp_line = Ripper.sexp(line_of_text) 81 | end 82 | 83 | if sexp_line.nil? 84 | log 'sexp line was nil again.' 85 | log 'Trying 1 more time with the last char removed from the line...' 86 | line_of_text.chop! 87 | sexp_line = Ripper.sexp(line_of_text) 88 | end 89 | 90 | if sexp_line.is_a? Array 91 | log "sexp_line.flatten: #{sexp_line.flatten}" 92 | log "sexp_line.last.first: #{sexp_line.last.first}" 93 | 94 | begin 95 | throw(:result, sexp_line.flatten.compact.any? do |s| 96 | s == MODIFIERS[self] 97 | end) 98 | rescue NoMethodError 99 | end 100 | end 101 | end 102 | end 103 | 104 | # @return [Boolean] 105 | def fake_backslash_line_end? 106 | self =~ /^# TAILOR REMOVED BACKSLASH\n?$/ 107 | end 108 | end 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /lib/tailor/logger.rb: -------------------------------------------------------------------------------- 1 | require 'log_switch' 2 | 3 | class Tailor 4 | class Logger 5 | extend LogSwitch 6 | 7 | # Overrides the LogSwitch Logger to custom format the time format in log 8 | # messages. 9 | def self.logger 10 | return @logger if @logger 11 | @logger ||= ::Logger.new $stdout 12 | 13 | def @logger.format_message(_, time, _, msg) 14 | "[#{time.strftime('%Y-%m-%d %H:%M:%S')}] #{msg}\n" 15 | end 16 | 17 | @logger 18 | end 19 | 20 | # Provides an .included hook to insert the name of the class for each log 21 | # message in the class that includes the Mixin. 22 | module Mixin 23 | def self.included(_) 24 | define_method :log do |*args| 25 | class_minus_main_name = self.class.to_s.sub(/^.*::/, '') 26 | args.first.insert(0, "<#{class_minus_main_name}> ") 27 | Tailor::Logger.log(*args) 28 | end 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/tailor/problem.rb: -------------------------------------------------------------------------------- 1 | require_relative 'logger' 2 | require_relative 'runtime_error' 3 | 4 | class Tailor 5 | 6 | # A Hashed data structure that simply defines the data needed to report a 7 | # problem 8 | class Problem < Hash 9 | include LogSwitch::Mixin 10 | 11 | # @param [Symbol] type The problem type. 12 | # @param [Fixnum] line The line of the file the problem was found on. 13 | # @param [Fixnum] column The column of the file line the problem was found on. 14 | # @param [String] message The message to tell the user about the problem. 15 | # @param [Fixnum] level The severity of the problem. 16 | def initialize(type, line, column, message, level) 17 | @type = type 18 | @line = line 19 | @column = column 20 | @message = message 21 | @level = level 22 | set_values 23 | subclass_name = self.class.to_s.sub(/^Tailor::/, '') 24 | msg = "<#{subclass_name}> #{self[:line]}[#{self[:column]}]: " 25 | msg << "#{@level.upcase}[:#{self[:type]}] #{self[:message]}" 26 | log msg 27 | end 28 | 29 | # Sets the standard values for the problem based on the type and binding. 30 | def set_values 31 | self[:type] = @type 32 | self[:line] = @line 33 | self[:column] = @column 34 | self[:message] = @message 35 | self[:level] = @level 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/tailor/reporter.rb: -------------------------------------------------------------------------------- 1 | class Tailor 2 | # Objects of this type are responsible for sending the right data to report 3 | # formatters. 4 | class Reporter 5 | attr_reader :formatters 6 | 7 | # For each in +formats+, it creates a new 8 | # +Tailor::Formatter::#{formatter.capitalize}+ object and adds it to 9 | # +@formatters+. 10 | # 11 | # @param [Array] formats A list of formatters to use for generating reports. 12 | def initialize(*formats) 13 | formats = %w(text) if formats.nil? || formats.empty? 14 | 15 | @formatters = formats.flatten.map do |formatter| 16 | retried = false 17 | 18 | begin 19 | Tailor::Formatters.const_get(formatter.capitalize).new 20 | rescue NameError 21 | require_relative "formatters/#{formatter}" 22 | 23 | if retried 24 | next 25 | else 26 | retried = true 27 | retry 28 | end 29 | end 30 | end.uniq 31 | 32 | @formatters.compact! 33 | end 34 | 35 | # Sends the data to each +@formatters+ to generate the report of problems 36 | # for the file that was just critiqued. A problem is in the format: 37 | # 38 | # { 'path/to/file.rb' => [Problem1, Problem2, etc.]} 39 | # 40 | # ...where Problem1 and Problem2 are of type {Tailor::Problem}. 41 | # 42 | # @param [Hash] file_problems 43 | # @param [Symbol,String] label The label of the file_set that defines the 44 | # problems in +file_problems+. 45 | def file_report(file_problems, label) 46 | @formatters.each do |formatter| 47 | formatter.file_report(file_problems, label) 48 | end 49 | end 50 | 51 | # Sends the data to each +@formatters+ to generate the reports of problems 52 | # for all files that were just critiqued. 53 | # 54 | # @param [Hash] all_problems 55 | def summary_report(all_problems, opts={}) 56 | @formatters.each do |formatter| 57 | summary = formatter.summary_report(all_problems) 58 | if formatter.respond_to?(:accepts_output_file) && 59 | formatter.accepts_output_file && 60 | !opts[:output_file].empty? 61 | File.open(opts[:output_file], 'w') { |f| f.puts summary } 62 | end 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/tailor/ruler.rb: -------------------------------------------------------------------------------- 1 | require_relative 'logger' 2 | require_relative 'problem' 3 | require_relative 'runtime_error' 4 | require_relative '../ext/string_ext' 5 | 6 | class Tailor 7 | 8 | # This is a composite class, geared for getting at or managing the Rulers 9 | # that should be used for measuring style. To do so, create a new object of 10 | # this type, then add child rulers to that object using +#add_child_ruler+. 11 | # After using the Ruler, you'll have access to all of the problems found by 12 | # all of the child rulers via +#problems+. 13 | # 14 | # Example: 15 | # ruler = Ruler.new 16 | # file_length_ruler = FileLengthRuler.new 17 | # ruler.add_child_ruler(file_length_ruler) 18 | # # use ruler 19 | # ruler.problems # => [{ all of your file length problems... }] 20 | # 21 | # There's really no measuring functionality in this base class--it's merely 22 | # for aggregating child data and for providing a base class to create child 23 | # Rulers from. Speaking of... if you want, you can create your own rulers. 24 | # A Ruler requires a few things: 25 | # 26 | # First, it needs a list of Lexer events to observer. Tailor uses its Lexer 27 | # to publish events (in this case, characters or string Ruby constructs) of 28 | # interest to its observers. Rulers subscribe to those events so that they 29 | # can detect the problems they're looking for. These are defined as a Set in 30 | # +@lexer_observers+. Adding to that list means the Ruler will subscribe to 31 | # those events. 32 | # 33 | # Example: 34 | # class MyRuler < Tailor::Ruler 35 | # def initialize 36 | # add_lexer_observers = :nl_observer, :kw_observer 37 | # end 38 | # end 39 | class Ruler 40 | include Tailor::Logger::Mixin 41 | 42 | attr_reader :lexer_observers 43 | 44 | # @param [Object] config 45 | # @param [Hash] options 46 | def initialize(config=nil, options={ level: :error }) 47 | @config = config 48 | @options = options 49 | @do_measurement = true 50 | log "Ruler initialized with style setting: #{@config}" 51 | log "Ruler initialized with problem level setting: #{@options[:level]}" 52 | 53 | @child_rulers = [] 54 | @lexer_observers = [] 55 | @problems = [] 56 | end 57 | 58 | # Adds the {Tailor::Ruler} object to the list of child rulers. 59 | # 60 | # @param [Tailor::Ruler] ruler 61 | def add_child_ruler(ruler) 62 | @child_rulers << ruler 63 | log "Added child ruler: #{ruler}" 64 | end 65 | 66 | # Gets all of the problems from all child rulers. 67 | # 68 | # @return [Array] The list of problems. 69 | def problems 70 | @problems = @child_rulers.inject(@problems) do |problems, ruler| 71 | problems + ruler.problems 72 | end 73 | 74 | @problems.sort_by! { |problem| problem[:line].to_i } 75 | end 76 | 77 | # Each ruler should redefine this for its needs. 78 | def measure(*args) 79 | raise RuntimeError, 80 | 'Ruler#measure called, but should be redefined by a real ruler.' 81 | end 82 | 83 | # Converts the {Tailor::Ruler} name to snake case. 84 | # 85 | # @return [String] The ruler name as snake-case that represents the problem 86 | # that was found. 87 | def problem_type 88 | self.class.to_s =~ /^.+::(\S+)Ruler$/ 89 | 90 | $1.underscore 91 | end 92 | 93 | private 94 | 95 | def add_lexer_observers(*lexer_observer) 96 | @lexer_observers = lexer_observer 97 | end 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /lib/tailor/rulers.rb: -------------------------------------------------------------------------------- 1 | Dir.glob(File.dirname(__FILE__) + '/rulers/*', &method(:require)) 2 | 3 | class Tailor 4 | module Rulers 5 | 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/tailor/rulers/allow_camel_case_methods_ruler.rb: -------------------------------------------------------------------------------- 1 | require_relative '../ruler' 2 | 3 | class Tailor 4 | module Rulers 5 | class AllowCamelCaseMethodsRuler < Tailor::Ruler 6 | def initialize(style, options) 7 | super(style, options) 8 | add_lexer_observers :ident 9 | end 10 | 11 | def ident_update(token, lexed_line, lineno, column) 12 | find_event = lexed_line.find { |e| e[1] == :on_kw && e.last == 'def' } 13 | 14 | if find_event && find_event.any? 15 | measure(token, lineno, column) 16 | end 17 | end 18 | 19 | # Checks to see if the method name contains capital letters. 20 | # 21 | # @param [Fixnum] token The method name. 22 | # @param [Fixnum] lineno Line the problem was found on. 23 | # @param [Fixnum] column Column the problem was found on. 24 | def measure(token, lineno, column) 25 | if token.contains_capital_letter? 26 | problem_message = 'Camel-case method name found.' 27 | 28 | @problems << Problem.new(problem_type, lineno, column, 29 | problem_message, @options[:level]) 30 | end 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/tailor/rulers/allow_conditional_parentheses.rb: -------------------------------------------------------------------------------- 1 | require_relative '../ruler' 2 | 3 | class Tailor 4 | module Rulers 5 | class AllowConditionalParenthesesRuler < Tailor::Ruler 6 | def initialize(style, options) 7 | super(style, options) 8 | add_lexer_observers :nl 9 | end 10 | 11 | def nl_update(current_lexed_line, lineno, _) 12 | measure(current_lexed_line, lineno) 13 | end 14 | 15 | # Checks to see if a conditional is unnecessarily wrapped in parentheses. 16 | # 17 | # @param [Fixnum] line The current lexed line. 18 | # @param [Fixnum] lineno Line the problem was found on. 19 | def measure(line, lineno) 20 | return if @config 21 | return unless line.any? { |t| conditional?(t) } 22 | if tokens_before_lparen?(line) and ! tokens_after_rparen?(line) 23 | column = lparen_column(line) 24 | @problems << Problem.new('conditional_parentheses', lineno, column, 25 | "Parentheses around conditional expression at column #{column}.", 26 | @options[:level]) 27 | end 28 | end 29 | 30 | private 31 | 32 | def conditional?(token) 33 | token[1] == :on_kw and %w{case if unless while}.include?(token[2]) 34 | end 35 | 36 | def lparen?(token) 37 | token[1] == :on_lparen 38 | end 39 | 40 | def lparen_column(tokens) 41 | tokens.find { |t| lparen?(t) }[0][1] + 1 42 | end 43 | 44 | def tokens_before_lparen?(tokens) 45 | without_spaces( 46 | tokens.select do |t| 47 | true if (conditional?(t))..(lparen?(t)) 48 | end.tap { |t| t.shift; t.pop } 49 | ).empty? 50 | end 51 | 52 | def tokens_after_rparen?(tokens) 53 | without_spaces( 54 | tokens.reverse.tap do |nl| 55 | nl.shift 56 | end.take_while { |t| t[1] != :on_rparen } 57 | ).any? 58 | end 59 | 60 | def without_spaces(tokens) 61 | tokens.reject { |t| t[1] == :on_sp } 62 | end 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/tailor/rulers/allow_hard_tabs_ruler.rb: -------------------------------------------------------------------------------- 1 | require_relative '../ruler' 2 | 3 | class Tailor 4 | module Rulers 5 | class AllowHardTabsRuler < Tailor::Ruler 6 | def initialize(config, options) 7 | super(config, options) 8 | add_lexer_observers :sp 9 | end 10 | 11 | def sp_update(token, lineno, column) 12 | measure(token, lineno, column) 13 | end 14 | 15 | # Checks to see if the space(s) contains hard tabs. 16 | # 17 | # @param [Fixnum] token The space(s). 18 | # @param [Fixnum] lineno Line the problem was found on. 19 | # @param [Fixnum] column Column the problem was found on. 20 | def measure(token, lineno, column) 21 | if token.contains_hard_tab? 22 | problem_message = 'Hard tab found.' 23 | 24 | @problems << Problem.new(problem_type, lineno, column, 25 | problem_message, @options[:level]) 26 | end 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/tailor/rulers/allow_invalid_ruby_ruler.rb: -------------------------------------------------------------------------------- 1 | require_relative '../ruler' 2 | 3 | 4 | class Tailor 5 | module Rulers 6 | class AllowInvalidRubyRuler < Tailor::Ruler 7 | def initialize(config, options) 8 | super(config, options) 9 | add_lexer_observers :file_beg 10 | end 11 | 12 | def file_beg_update(file_name) 13 | @file_name = file_name 14 | measure 15 | end 16 | 17 | # @return [Boolean] 18 | def invalid_ruby? 19 | log 'Checking for valid Ruby...' 20 | result = `"#{Gem.ruby}" -c "#{@file_name}"` 21 | 22 | result.size.zero? 23 | end 24 | 25 | def measure 26 | if invalid_ruby? && @config == false 27 | lineno = 0 28 | column = 0 29 | msg = 'File contains invalid Ruby; run `ruby -c [your_file.rb]` ' 30 | msg << 'for more details.' 31 | 32 | @problems << Problem.new(problem_type, lineno, column, msg, 33 | @options[:level]) 34 | end 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/tailor/rulers/allow_screaming_snake_case_classes_ruler.rb: -------------------------------------------------------------------------------- 1 | require_relative '../ruler' 2 | 3 | class Tailor 4 | module Rulers 5 | class AllowScreamingSnakeCaseClassesRuler < Tailor::Ruler 6 | def initialize(config, options) 7 | super(config, options) 8 | add_lexer_observers :const 9 | end 10 | 11 | def const_update(token, lexed_line, lineno, column) 12 | ident_index = lexed_line.event_index(column) 13 | previous_event = lexed_line.event_at(ident_index - 2) 14 | log "previous event: #{previous_event}" 15 | 16 | return if previous_event.nil? 17 | 18 | if previous_event[1] == :on_kw && 19 | (previous_event.last == 'class' || previous_event.last == 'module') 20 | measure(token, lineno, column) 21 | end 22 | end 23 | 24 | # Checks to see if the class name matches /[A-Z].*_/. 25 | # 26 | # @param [Fixnum] token The space(s). 27 | # @param [Fixnum] lineno Line the potential problem is on. 28 | # @param [Fixnum] column Column the potential problem is on. 29 | def measure(token, lineno, column) 30 | if token.screaming_snake_case? 31 | problem_message = 'Screaming-snake-case class/module found.' 32 | 33 | @problems << Problem.new(problem_type, lineno, column, 34 | problem_message, @options[:level]) 35 | end 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/tailor/rulers/allow_trailing_line_spaces_ruler.rb: -------------------------------------------------------------------------------- 1 | require_relative '../ruler' 2 | 3 | class Tailor 4 | module Rulers 5 | class AllowTrailingLineSpacesRuler < Tailor::Ruler 6 | def initialize(config, options) 7 | super(config, options) 8 | add_lexer_observers :ignored_nl, :nl 9 | end 10 | 11 | def ignored_nl_update(lexed_line, lineno, column) 12 | log "Last event: #{lexed_line.last_non_line_feed_event}" 13 | log "Line ends with space: #{lexed_line.ends_with_sp?}" 14 | 15 | measure(lexed_line, lineno, column) 16 | end 17 | 18 | def nl_update(lexed_line, lineno, column) 19 | ignored_nl_update(lexed_line, lineno, column) 20 | end 21 | 22 | # Checks to see if the line contains trailing spaces. 23 | # 24 | # @param [LexedLine] lexed_line The line to check for trailing spaces. 25 | # @param [Fixnum] lineno Line the potential problem is on. 26 | # @param [Fixnum] column Column the potential problem is on. 27 | def measure(lexed_line, lineno, column) 28 | if lexed_line.ends_with_sp? 29 | actual = lexed_line.last_non_line_feed_event.last.size 30 | problem_message = "Line has #{actual} trailing spaces." 31 | 32 | @problems << Problem.new(problem_type, lineno, column, 33 | problem_message, @options[:level]) 34 | end 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/tailor/rulers/allow_unnecessary_double_quotes_ruler.rb: -------------------------------------------------------------------------------- 1 | require_relative '../ruler' 2 | 3 | class Tailor 4 | module Rulers 5 | class AllowUnnecessaryDoubleQuotesRuler < Tailor::Ruler 6 | def initialize(config, options) 7 | super(config, options) 8 | add_lexer_observers :nl 9 | end 10 | 11 | def nl_update(lexed_line, lineno, _) 12 | quotes(lexed_line).each do |quote| 13 | unless contains_embedded_expression?(quote) || 14 | contains_escape_sequence?(quote) 15 | measure(lineno, column(quote.first)) 16 | end 17 | end 18 | end 19 | 20 | # Checks to see if the double_quotes are unnecessary. 21 | # 22 | # @param [Fixnum] lineno Line the problem was found on. 23 | # @param [Fixnum] column Column the problem was found on. 24 | def measure(lineno, column) 25 | @problems << Problem.new('unnecessary_double_quotes', lineno, column, 26 | "Unnecessary double quotes at column #{column}, " + 27 | 'expected single quotes.', @options[:level]) 28 | end 29 | 30 | private 31 | 32 | def contains_embedded_expression?(tokens) 33 | tokens.any? { |t| t[1] == :on_embexpr_beg } 34 | end 35 | 36 | def contains_escape_sequence?(tokens) 37 | tokens.any? do |t| 38 | t[1] == :on_tstring_content and t[2].match(/\\[a-z]+/) 39 | end 40 | end 41 | 42 | def quotes(tokens) 43 | tokens.select do |t| 44 | true if (double_quote_start?(t))..(double_quote_end?(t)) 45 | end.slice_before { |t| double_quote_start?(t) }.reject { |q| q.empty? } 46 | end 47 | 48 | def column(token) 49 | token[0][1] 50 | end 51 | 52 | def double_quote_start?(token) 53 | token[1] == :on_tstring_beg and token[2] == '"' 54 | end 55 | 56 | def double_quote_end?(token) 57 | token[1] == :on_tstring_end and token[2] == '"' 58 | end 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/tailor/rulers/allow_unnecessary_interpolation_ruler.rb: -------------------------------------------------------------------------------- 1 | require_relative '../ruler' 2 | 3 | class Tailor 4 | module Rulers 5 | class AllowUnnecessaryInterpolationRuler < Tailor::Ruler 6 | 7 | EVENTS = [ 8 | :on_embexpr_beg, 9 | :on_embexpr_end, 10 | :on_rbrace, 11 | :on_tstring_beg, 12 | :on_tstring_content, 13 | :on_tstring_end 14 | ] 15 | 16 | def initialize(config, options) 17 | super(config, options) 18 | reset_tokens 19 | add_lexer_observers :ignored_nl, :nl 20 | end 21 | 22 | def ignored_nl_update(lexed_line, _, _) 23 | add_string_tokens(lexed_line) 24 | end 25 | 26 | def nl_update(lexed_line, _, _) 27 | add_string_tokens(lexed_line) 28 | each_string(@tokens).each do |string| 29 | measure(line_number(@tokens.first), string) 30 | end 31 | 32 | reset_tokens 33 | end 34 | 35 | # Checks if variables are interpolated unnecessarily. 36 | # 37 | # @param [Array] tokens The filtered tokens. 38 | def measure(lineno, tokens) 39 | return if @config 40 | if no_content?(tokens) and one_expression?(tokens) 41 | @problems << Problem.new('unnecessary_string_interpolation', lineno, 42 | column(tokens.first), 'Variable interpolated unnecessarily', 43 | @options[:level]) 44 | end 45 | end 46 | 47 | private 48 | 49 | def add_string_tokens(lexed_line) 50 | @tokens += string_tokens(lexed_line) 51 | end 52 | 53 | def column(token) 54 | token.first.last + 1 55 | end 56 | 57 | def each_string(tokens) 58 | tokens.select do |t| 59 | true if (t[1] == :on_tstring_beg)..(t[1] == :on_tstring_end) 60 | end.slice_before { |t| t[1] == :on_tstring_beg } 61 | end 62 | 63 | def line_number(token) 64 | token.first.first 65 | end 66 | 67 | def no_content?(tokens) 68 | ! tokens.map { |t| t[1] }.include?(:on_tstring_content) 69 | end 70 | 71 | def one_expression?(tokens) 72 | tokens.select { |t| t[1] == :on_embexpr_beg }.size == 1 and 73 | tokens.select do |t| 74 | t[1] == :on_embexpr_end or t[1] == :on_rbrace 75 | end.any? 76 | end 77 | 78 | def reset_tokens 79 | @tokens = [] 80 | end 81 | 82 | def string_tokens(lexed_line) 83 | lexed_line.select { |t| EVENTS.include?(t[1]) } 84 | end 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /lib/tailor/rulers/indentation_spaces_ruler/argument_alignment.rb: -------------------------------------------------------------------------------- 1 | require 'ripper' 2 | require_relative './ast_xml' 3 | 4 | class Tailor 5 | module Rulers 6 | class IndentationSpacesRuler < Tailor::Ruler 7 | 8 | # Determines whether arguments spread across lines are correctly 9 | # aligned. 10 | # 11 | # Function calls with parentheses could be determined easily from 12 | # the lexed line only but we need to support calls without parentheses 13 | # too. 14 | class ArgumentAlignment 15 | 16 | include AstXml 17 | 18 | def initialize(file_name) 19 | @ast = build_xml(Ripper::SexpBuilder.new(File.read(file_name)).parse) 20 | @lex = Ripper.lex(File.read(file_name)) 21 | end 22 | 23 | def expected_column(lineno, should_be_at) 24 | column = call_column(lineno) || declaration_column(lineno) 25 | correct_for_literals(lineno, column) || should_be_at 26 | end 27 | 28 | private 29 | 30 | # sexp column offsets for string literals do not include the quote 31 | def correct_for_literals(lineno, column) 32 | tstring_index = @lex.index do |pos, token| 33 | pos[0] == lineno and pos[1] == column and 34 | token == :on_tstring_content 35 | end 36 | 37 | tstring_index ? @lex[tstring_index -1][0][1] : column 38 | end 39 | 40 | def call_column(lineno) 41 | [ 42 | first_argument(:command_call, :args_add_block, lineno), 43 | first_argument(:method_add_arg, :args_add_block, lineno) 44 | ].compact.min 45 | end 46 | 47 | def declaration_column(lineno) 48 | first_argument(:def, :params, lineno) 49 | end 50 | 51 | def first_argument(parent, child, lineno) 52 | method_defs = @ast.xpath("//#{parent}") 53 | method_defs.map do |d| 54 | d.xpath("descendant::#{child}[descendant::pos[@line = #{lineno} 55 | ]]/descendant::pos[position() = 1 and @line != #{lineno}]/ 56 | @column").first.to_s.to_i 57 | end.reject { |c| c == 0 }.min 58 | end 59 | 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/tailor/rulers/indentation_spaces_ruler/ast_xml.rb: -------------------------------------------------------------------------------- 1 | class Tailor 2 | module Rulers 3 | class IndentationSpacesRuler < Tailor::Ruler 4 | 5 | # XXX: Reproducing the ast querying functions from foodcritic here. We 6 | # either need to re-implement the queries not to rely on these functions 7 | # or extract these functions to a shared gem. 8 | module AstXml 9 | 10 | def xml_array_node(doc, xml_node, child) 11 | n = xml_create_node(doc, child) 12 | xml_node.add_child(build_xml(child, doc, n)) 13 | end 14 | 15 | def xml_create_node(doc, c) 16 | Nokogiri::XML::Node.new(c.first.to_s.gsub(/[^a-z_]/, ''), doc) 17 | end 18 | 19 | def xml_document(doc, xml_node) 20 | if doc.nil? 21 | doc = Nokogiri::XML('') 22 | xml_node = doc.root 23 | end 24 | [doc, xml_node] 25 | end 26 | 27 | def xml_hash_node(doc, xml_node, child) 28 | child.each do |c| 29 | n = xml_create_node(doc, c) 30 | c.drop(1).each do |a| 31 | xml_node.add_child(build_xml(a, doc, n)) 32 | end 33 | end 34 | end 35 | 36 | def xml_position_node(doc, xml_node, child) 37 | pos = Nokogiri::XML::Node.new('pos', doc) 38 | pos['line'] = child.first.to_s 39 | pos['column'] = child[1].to_s 40 | xml_node.add_child(pos) 41 | end 42 | 43 | def ast_hash_node?(node) 44 | node.first.respond_to?(:first) and node.first.first == :assoc_new 45 | end 46 | 47 | def ast_node_has_children?(node) 48 | node.respond_to?(:first) and ! node.respond_to?(:match) 49 | end 50 | 51 | # If the provided node is the line / column information. 52 | def position_node?(node) 53 | node.respond_to?(:length) and node.length == 2 and 54 | node.respond_to?(:all?) and node.all? do |child| 55 | child.respond_to?(:to_i) 56 | end 57 | end 58 | 59 | def build_xml(node, doc = nil, xml_node=nil) 60 | doc, xml_node = xml_document(doc, xml_node) 61 | 62 | if node.respond_to?(:each) 63 | # First child is the node name 64 | node.drop(1) if node.first.is_a?(Symbol) 65 | node.each do |child| 66 | if position_node?(child) 67 | xml_position_node(doc, xml_node, child) 68 | else 69 | if ast_node_has_children?(child) 70 | # The AST structure is different for hashes so we have to treat 71 | # them separately. 72 | if ast_hash_node?(child) 73 | xml_hash_node(doc, xml_node, child) 74 | else 75 | xml_array_node(doc, xml_node, child) 76 | end 77 | else 78 | xml_node['value'] = child.to_s unless child.nil? 79 | end 80 | end 81 | end 82 | end 83 | xml_node 84 | end 85 | 86 | end 87 | end 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /lib/tailor/rulers/indentation_spaces_ruler/line_continuations.rb: -------------------------------------------------------------------------------- 1 | require 'ripper' 2 | require_relative './ast_xml' 3 | 4 | class Tailor 5 | module Rulers 6 | class IndentationSpacesRuler < Tailor::Ruler 7 | 8 | # Determines whether a line is a continuation of previous lines. For ease 9 | # of implementation we use the s-exp support in Ripper, rather than trying 10 | # to work it out solely from the token events on the ruler. 11 | class LineContinuations 12 | 13 | include AstXml 14 | 15 | def initialize(file_name) 16 | @ast = build_xml(Ripper::SexpBuilder.new(File.read(file_name)).parse) 17 | end 18 | 19 | # Is the specified line actually a previous line continuing? 20 | def line_is_continuation?(lineno) 21 | @continuations ||= begin 22 | statements = @ast.xpath('//stmts_add/*[ 23 | preceding-sibling::stmts_new | preceding-sibling::stmts_add]') 24 | statements.reject do |stmt| 25 | s = stmt.xpath('ancestor::stmts_add').size 26 | stmt.xpath("descendant::pos[count(ancestor::stmts_add) = #{s}]/ 27 | @line").map { |s| s.to_s.to_i }.uniq.size < 2 28 | end.reject { |stmt| stmt.name == 'case' }.map do |stmt| 29 | lines_nesting = lines_with_nesting_level(stmt) 30 | lines_nesting.shift 31 | min_level = lines_nesting.values.min 32 | lines_nesting.reject { |_, n| n > min_level }.keys 33 | end.flatten 34 | end 35 | 36 | @continuations.include?(lineno) 37 | end 38 | 39 | # Are there statements further nested below this line? 40 | def line_has_nested_statements?(lineno) 41 | @ast.xpath("//pos[@line='#{lineno}']/ 42 | ancestor::*[parent::stmts_add][1]/ 43 | descendant::stmts_add[descendant::pos[@line > #{lineno}]]").any? 44 | end 45 | 46 | private 47 | 48 | # Returns a hash of line numbers => nesting level 49 | def lines_with_nesting_level(node) 50 | Hash[node.xpath('descendant::pos/@line').map do |ln| 51 | [ln.to_s.to_i, ln.xpath('count(ancestor::stmts_add)').to_i] 52 | end.sort.uniq] 53 | end 54 | 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/tailor/rulers/max_code_lines_in_class_ruler.rb: -------------------------------------------------------------------------------- 1 | require_relative '../ruler' 2 | require_relative '../lexer/lexer_constants' 3 | 4 | class Tailor 5 | module Rulers 6 | class MaxCodeLinesInClassRuler < Tailor::Ruler 7 | include Tailor::LexerConstants 8 | 9 | def initialize(config, options) 10 | super(config, options) 11 | add_lexer_observers(:ignored_nl, :kw, :nl) 12 | @class_start_lines = [] 13 | @kw_start_lines = [] 14 | @end_last_class = false 15 | end 16 | 17 | def ignored_nl_update(lexed_line, _, _) 18 | return if @class_start_lines.empty? 19 | return if lexed_line.only_spaces? 20 | return if lexed_line.comment_line? 21 | 22 | @class_start_lines.each do |line| 23 | line[:count] += 1 24 | log "Class from line #{line[:lineno]} now at #{line[:count]} lines." 25 | end 26 | 27 | if @end_last_class 28 | measure(@class_start_lines.last[:count], 29 | @class_start_lines.last[:lineno], 30 | @class_start_lines.last[:column]) 31 | @class_start_lines.pop 32 | @end_last_class = false 33 | end 34 | end 35 | 36 | def kw_update(token, _, lineno, column) 37 | if token == 'class' || token == 'module' 38 | @class_start_lines << { lineno: lineno, column: column, count: 0 } 39 | log "Class start lines: #{@class_start_lines}" 40 | end 41 | 42 | unless token.modifier_keyword? || 43 | !token.keyword_to_indent? || 44 | token.do_is_for_a_loop? || 45 | token.continuation_keyword? 46 | @kw_start_lines << lineno 47 | log "Keyword start lines: #{@kw_start_lines}" 48 | end 49 | 50 | if token == 'end' 51 | log "Got 'end' of class/module." 52 | 53 | unless @class_start_lines.empty? 54 | if @class_start_lines.last[:lineno] == @kw_start_lines.last 55 | msg = "Class/module from line #{@class_start_lines.last[:lineno]}" 56 | msg << " was #{@class_start_lines.last[:count]} lines long." 57 | log msg 58 | @end_last_class = true 59 | end 60 | end 61 | 62 | @kw_start_lines.pop 63 | log "End of keyword statement. Keywords: #{@kw_start_lines}" 64 | end 65 | end 66 | 67 | def nl_update(lexed_line, lineno, column) 68 | ignored_nl_update(lexed_line, lineno, column) 69 | end 70 | 71 | # Checks to see if the actual count of code lines in the class is greater 72 | # than the value in +@config+. 73 | # 74 | # @param [Fixnum] actual_count The number of code lines found. 75 | # @param [Fixnum] lineno The line the potential problem is on. 76 | # @param [Fixnum] column The column the potential problem is on. 77 | def measure(actual_count, lineno, column) 78 | if actual_count > @config 79 | msg = "Class/module has #{actual_count} code lines, but " 80 | msg << "should have no more than #{@config}." 81 | 82 | @problems << Problem.new(problem_type, lineno, column, msg, 83 | @options[:level]) 84 | end 85 | end 86 | end 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /lib/tailor/rulers/max_code_lines_in_method_ruler.rb: -------------------------------------------------------------------------------- 1 | require_relative '../ruler' 2 | require_relative '../lexer/lexer_constants' 3 | 4 | class Tailor 5 | module Rulers 6 | class MaxCodeLinesInMethodRuler < Tailor::Ruler 7 | include Tailor::LexerConstants 8 | 9 | def initialize(config, options) 10 | super(config, options) 11 | add_lexer_observers(:ignored_nl, :kw, :nl) 12 | @method_start_lines = [] 13 | @kw_start_lines = [] 14 | @end_last_method = false 15 | end 16 | 17 | def ignored_nl_update(lexed_line, _, _) 18 | return if @method_start_lines.empty? 19 | return if lexed_line.only_spaces? 20 | return if lexed_line.comment_line? 21 | 22 | @method_start_lines.each do |line| 23 | line[:count] += 1 24 | log "Method from line #{line[:lineno]} now at #{line[:count]} lines." 25 | end 26 | 27 | if @end_last_method 28 | measure(@method_start_lines.last[:count], 29 | @method_start_lines.last[:lineno], 30 | @method_start_lines.last[:column]) 31 | @method_start_lines.pop 32 | @end_last_method = false 33 | end 34 | end 35 | 36 | def kw_update(token, _, lineno, column) 37 | if token == 'def' 38 | @method_start_lines << { lineno: lineno, column: column, count: 0 } 39 | log "Method start lines: #{@method_start_lines}" 40 | end 41 | 42 | unless token.modifier_keyword? || 43 | !token.keyword_to_indent? || 44 | token.do_is_for_a_loop? || 45 | token.continuation_keyword? 46 | @kw_start_lines << lineno 47 | log "Keyword start lines: #{@kw_start_lines}" 48 | end 49 | 50 | if token == 'end' 51 | log "Got 'end' of method." 52 | 53 | unless @method_start_lines.empty? 54 | if @method_start_lines.last[:lineno] == @kw_start_lines.last 55 | #msg = "Method from line #{@method_start_lines.last[:lineno]}" 56 | #msg << " was #{@method_start_lines.last[:count]} lines long." 57 | #log msg 58 | @end_last_method = true 59 | end 60 | end 61 | 62 | @kw_start_lines.pop 63 | log "End of keyword statement. Keywords: #{@kw_start_lines}" 64 | end 65 | end 66 | 67 | def nl_update(lexed_line, lineno, column) 68 | ignored_nl_update(lexed_line, lineno, column) 69 | end 70 | 71 | # Checks to see if the actual count of code lines in the method is greater 72 | # than the value in +@config+. 73 | # 74 | # @param [Fixnum] actual_count The number of code lines found. 75 | # @param [Fixnum] lineno The line the potential problem is on. 76 | # @param [Fixnum] column The column the potential problem is on. 77 | def measure(actual_count, lineno, column) 78 | if actual_count > @config 79 | msg = "Method has #{actual_count} code lines, but " 80 | msg << "should have no more than #{@config}." 81 | 82 | @problems << Problem.new(problem_type, lineno, column, msg, 83 | @options[:level]) 84 | end 85 | end 86 | end 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /lib/tailor/rulers/max_line_length_ruler.rb: -------------------------------------------------------------------------------- 1 | require_relative '../ruler' 2 | 3 | class Tailor 4 | module Rulers 5 | class MaxLineLengthRuler < Tailor::Ruler 6 | def initialize(config, options) 7 | super(config, options) 8 | add_lexer_observers :ignored_nl, :nl 9 | end 10 | 11 | def ignored_nl_update(lexed_line, lineno, column) 12 | log "<#{self.class}> Line length: #{lexed_line.line_length}" 13 | measure(lexed_line, lineno, column) 14 | end 15 | 16 | def nl_update(lexed_line, lineno, column) 17 | ignored_nl_update(lexed_line, lineno, column) 18 | end 19 | 20 | # Checks to see if the line has more characters that given at +@config+. 21 | # 22 | # @param [Fixnum] lexed_line The line to measure. 23 | # @param [Fixnum] lineno Line the potential problem is on. 24 | # @param [Fixnum] column Column the potential problem is on 25 | def measure(lexed_line, lineno, column) 26 | if lexed_line.line_length > @config 27 | msg = "Line is #{lexed_line.line_length} chars long, " 28 | msg << "but should be #{@config}." 29 | 30 | @problems << Problem.new(problem_type, lineno, column, msg, 31 | @options[:level]) 32 | end 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/tailor/rulers/spaces_after_comma_ruler.rb: -------------------------------------------------------------------------------- 1 | require_relative '../ruler' 2 | 3 | class Tailor 4 | module Rulers 5 | 6 | # Looks for spaces after a ',' as given by +@config+. It skips checking 7 | # when: 8 | # * the char after it is a '\n'. 9 | # * it's at the end of a line, followed by a trailing comment. 10 | class SpacesAfterCommaRuler < Tailor::Ruler 11 | def initialize(config, options) 12 | super(config, options) 13 | add_lexer_observers :comma, :comment, :ignored_nl, :nl 14 | @comma_columns = [] 15 | end 16 | 17 | def comma_update(_, _, column) 18 | @comma_columns << column 19 | end 20 | 21 | def comment_update(token, lexed_line, _, lineno, column) 22 | if token =~ /\n$/ 23 | log 'Found comment with trailing newline.' 24 | ignored_nl_update(lexed_line, lineno, column) 25 | end 26 | end 27 | 28 | def ignored_nl_update(lexed_line, lineno, _) 29 | check_spaces_after_comma(lexed_line, lineno) 30 | end 31 | 32 | def nl_update(lexed_line, lineno, column) 33 | ignored_nl_update(lexed_line, lineno, column) 34 | end 35 | 36 | # Checks to see if the actual_spaces after a comma equals the value 37 | # at +@config+. 38 | # 39 | # @param [Fixnum] actual_spaces The number of spaces after the comma. 40 | # @param [Fixnum] lineno Line the problem is on. 41 | # @param [Fixnum] column Column the potential problem is on. 42 | def measure(actual_spaces, lineno, column) 43 | if actual_spaces != @config 44 | msg = "Line has #{actual_spaces} space(s) after a comma, " 45 | msg << "but should have #{@config}." 46 | 47 | @problems << Problem.new(problem_type, lineno, column + 1, msg, 48 | @options[:level]) 49 | end 50 | end 51 | 52 | def check_spaces_after_comma(lexed_line, lineno) 53 | log "Commas found at: #{@comma_columns}" unless @comma_columns.empty? 54 | 55 | @comma_columns.each do |c| 56 | event_index = lexed_line.event_index(c) 57 | if event_index.nil? 58 | log 'Event index is nil. Weird...' 59 | break 60 | end 61 | 62 | next_event = lexed_line.at(event_index + 1) 63 | if next_event.nil? 64 | log 'Looks like there is no next event (this is last in the line).' 65 | break 66 | end 67 | 68 | if next_event[1] == :on_nl || next_event[1] == :on_ignored_nl 69 | log 'Next event is a newline.' 70 | break 71 | end 72 | 73 | second_next_event = lexed_line.at(event_index + 2) 74 | if second_next_event.nil? 75 | log 'Second next event is nil.' 76 | next 77 | end 78 | 79 | if second_next_event[1] == :on_comment 80 | log 'Event + 2 is a comment.' 81 | next 82 | end 83 | 84 | actual_spaces = next_event[1] != :on_sp ? 0 : next_event.last.size 85 | measure(actual_spaces, lineno, c) 86 | end 87 | 88 | @comma_columns.clear 89 | end 90 | end 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /lib/tailor/rulers/spaces_after_conditional_ruler.rb: -------------------------------------------------------------------------------- 1 | require_relative '../ruler' 2 | 3 | class Tailor 4 | module Rulers 5 | class SpacesAfterConditionalRuler < Tailor::Ruler 6 | 7 | def initialize(config, options) 8 | super(config, options) 9 | add_lexer_observers :nl 10 | end 11 | 12 | def nl_update(current_lexed_line, lineno, _) 13 | measure(current_lexed_line, lineno) 14 | end 15 | 16 | # Checks to see if spacing is present after conditionals 17 | # 18 | # @param [Array] lexed_line The lexed line with a conditional 19 | # @param [Fixnum] lineno Line the problem was found on. 20 | def measure(lexed_line, lineno) 21 | 22 | idx = lexed_line.index do |_, token, name| 23 | token == :on_kw and %w{if unless case}.include?(name) 24 | end 25 | 26 | expected_spaces = @config 27 | spaces = expected_spaces 28 | 29 | if idx 30 | column = lexed_line[idx].first.last 31 | pos, token, _ = lexed_line[idx + 1] 32 | spaces = case token 33 | when :on_lparen then 0 34 | when :on_sp 35 | next_token = lexed_line[idx + 2] 36 | next_token.first.last - pos.last 37 | end 38 | end 39 | 40 | if expected_spaces != spaces 41 | @problems << Problem.new(problem_type, lineno, column, 42 | "#{spaces} spaces after conditional at column #{column}, " + 43 | "expected #{expected_spaces}.", @options[:level]) 44 | end 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/tailor/rulers/spaces_after_lbrace_ruler.rb: -------------------------------------------------------------------------------- 1 | require_relative '../ruler' 2 | 3 | class Tailor 4 | module Rulers 5 | 6 | # Checks for spaces after a +{+ as given by +@config+. It skips checking 7 | # when: 8 | # * it's at the end of a line. 9 | # * the next char is a '}' 10 | # * it's at the end of a line, followed by a trailing comment. 11 | class SpacesAfterLbraceRuler < Tailor::Ruler 12 | def initialize(config, options) 13 | super(config, options) 14 | add_lexer_observers :comment, :ignored_nl, :lbrace, :nl 15 | @lbrace_columns = [] 16 | end 17 | 18 | def comment_update(token, lexed_line, _, lineno, column) 19 | if token =~ /\n$/ 20 | log 'Found comment with trailing newline.' 21 | ignored_nl_update(lexed_line, lineno, column) 22 | end 23 | end 24 | 25 | def ignored_nl_update(lexed_line, lineno, _) 26 | check_spaces_after_lbrace(lexed_line, lineno) 27 | end 28 | 29 | def lbrace_update(_, _, column) 30 | @lbrace_columns << column 31 | end 32 | 33 | def nl_update(lexed_line, lineno, column) 34 | ignored_nl_update(lexed_line, lineno, column) 35 | end 36 | 37 | # Checks to see if the number of spaces after an lbrace equals the value 38 | # at +@config+. 39 | # 40 | # @param [Fixnum] actual_spaces The number of spaces after the lbrace. 41 | # @param [Fixnum] lineno Line the potential problem is on. 42 | # @param [Fixnum] column Column the potential problem is on. 43 | def measure(actual_spaces, lineno, column) 44 | if actual_spaces != @config 45 | msg = "Line has #{actual_spaces} space(s) after a {, " 46 | msg << "but should have #{@config}." 47 | 48 | @problems << Problem.new(problem_type, lineno, column + 1, msg, 49 | @options[:level]) 50 | end 51 | end 52 | 53 | def check_spaces_after_lbrace(lexed_line, lineno) 54 | log "lbraces found at: #{@lbrace_columns}" unless @lbrace_columns.empty? 55 | 56 | @lbrace_columns.each do |column| 57 | actual_spaces = count_spaces(lexed_line, column) 58 | next if actual_spaces.nil? 59 | 60 | if !@do_measurement 61 | log 'Skipping measurement.' 62 | else 63 | measure(actual_spaces, lineno, column) 64 | end 65 | 66 | @do_measurement = true 67 | end 68 | 69 | @lbrace_columns.clear 70 | end 71 | 72 | # Counts the number of spaces after the lbrace. 73 | # 74 | # @param [LexedLine] lexed_line The LexedLine that contains the context 75 | # the lbrace was found in. 76 | # @param [Fixnum] column Column the lbrace was found at. 77 | # @return [Fixnum] The number of spaces found after the lbrace. 78 | def count_spaces(lexed_line, column) 79 | event_index = lexed_line.event_index(column) 80 | 81 | if event_index.nil? 82 | log 'No lbrace in this line. Moving on...' 83 | @do_measurement = false 84 | return 85 | end 86 | 87 | next_event = lexed_line.at(event_index + 1) 88 | 89 | if next_event.nil? 90 | log 'lbrace must be at the end of the line. Moving on.' 91 | @do_measurement = false 92 | return 0 93 | end 94 | 95 | if next_event[1] == :on_nl || next_event[1] == :on_ignored_nl 96 | log "lbrace is followed by a '#{next_event[1]}'. Moving on." 97 | @do_measurement = false 98 | return 0 99 | end 100 | 101 | if next_event[1] == :on_rbrace 102 | log 'lbrace is followed by an rbrace. Moving on.' 103 | @do_measurement = false 104 | return 0 105 | end 106 | 107 | second_next_event = lexed_line.at(event_index + 2) 108 | if second_next_event[1] == :on_comment 109 | log 'Event + 2 is a comment.' 110 | @do_measurement = false 111 | return next_event.last.size 112 | end 113 | 114 | next_event[1] != :on_sp ? 0 : next_event.last.size 115 | end 116 | end 117 | end 118 | end 119 | -------------------------------------------------------------------------------- /lib/tailor/rulers/spaces_after_lbracket_ruler.rb: -------------------------------------------------------------------------------- 1 | require_relative '../ruler' 2 | 3 | class Tailor 4 | module Rulers 5 | 6 | # Detects spaces after a '[' as given by +@config+. It skips checking 7 | # when: 8 | # * it's the last char in line. 9 | # * the char after it is a ']'. 10 | # * the char after it is space, then a '{'. 11 | class SpacesAfterLbracketRuler < Tailor::Ruler 12 | def initialize(config, options) 13 | super(config, options) 14 | add_lexer_observers :comment, :ignored_nl, :lbracket, :nl 15 | @lbracket_columns = [] 16 | end 17 | 18 | def comment_update(token, lexed_line, _, lineno, column) 19 | if token =~ /\n$/ 20 | log 'Found comment with trailing newline.' 21 | ignored_nl_update(lexed_line, lineno, column) 22 | end 23 | end 24 | 25 | def ignored_nl_update(lexed_line, lineno, _) 26 | check_spaces_after_lbracket(lexed_line, lineno) 27 | end 28 | 29 | def lbracket_update(_, _, column) 30 | @lbracket_columns << column 31 | end 32 | 33 | def nl_update(lexed_line, lineno, column) 34 | ignored_nl_update(lexed_line, lineno, column) 35 | end 36 | 37 | # Checks to see if the actual_spaces after an lbracket equals the value 38 | # at +@config+. 39 | # 40 | # @param [Fixnum] actual_spaces The number of spaces after the lbracket. 41 | # @param [Fixnum] lineno Line the problem was found on. 42 | # @param [Fixnum] column Column the problem was found on. 43 | def measure(actual_spaces, lineno, column) 44 | if actual_spaces != @config 45 | msg = "Line has #{actual_spaces} space(s) after a [, " 46 | msg << "but should have #{@config}." 47 | 48 | @problems << Problem.new(problem_type, lineno, column + 1, msg, 49 | @options[:level]) 50 | end 51 | end 52 | 53 | def check_spaces_after_lbracket(lexed_line, lineno) 54 | unless @lbracket_columns.empty? 55 | log "lbracket found at: #{@lbracket_columns}" 56 | end 57 | 58 | @lbracket_columns.each do |column| 59 | actual_spaces = count_spaces(lexed_line, column) 60 | next if actual_spaces.nil? 61 | 62 | if !@do_measurement 63 | log 'Skipping measurement.' 64 | else 65 | measure(actual_spaces, lineno, column) 66 | end 67 | 68 | @do_measurement = true 69 | end 70 | 71 | @lbracket_columns.clear 72 | end 73 | 74 | # Counts the number of spaces after the lbracket. 75 | # 76 | # @param [LexedLine] lexed_line The LexedLine that contains the context 77 | # the lbracket was found in. 78 | # @param [Fixnum] column Column the lbracket was found at. 79 | # @return [Fixnum] The number of spaces found after the lbracket. 80 | def count_spaces(lexed_line, column) 81 | event_index = lexed_line.event_index(column) 82 | 83 | if event_index.nil? 84 | log 'No lbracket in this line. Moving on...' 85 | @do_measurement = false 86 | return 87 | end 88 | 89 | next_event = lexed_line.at(event_index + 1) 90 | log "Next event: #{next_event}" 91 | 92 | if next_event.nil? 93 | log 'lbracket must be at the end of the line.' 94 | @do_measurement = false 95 | return 0 96 | end 97 | 98 | [:on_nl, :on_ignored_nl].each do |event| 99 | if next_event[1] == event 100 | log "lbracket is followed by a '#{event}'. Moving on." 101 | @do_measurement = false 102 | return 0 103 | end 104 | end 105 | 106 | if next_event[1] == :on_rbracket 107 | log 'lbracket is followed by a rbracket. Moving on.' 108 | @do_measurement = false 109 | return 0 110 | end 111 | 112 | second_next_event = lexed_line.at(event_index + 2) 113 | log "Event + 2: #{second_next_event}" 114 | 115 | [:on_comment, :on_lbrace].each do |event| 116 | if second_next_event[1] == event 117 | log "Event + 2 is a #{event}. Moving on." 118 | @do_measurement = false 119 | return next_event.last.size 120 | end 121 | end 122 | 123 | next_event[1] != :on_sp ? 0 : next_event.last.size 124 | end 125 | end 126 | end 127 | end 128 | -------------------------------------------------------------------------------- /lib/tailor/rulers/spaces_after_lparen_ruler.rb: -------------------------------------------------------------------------------- 1 | require_relative '../ruler' 2 | 3 | class Tailor 4 | module Rulers 5 | class SpacesAfterLparenRuler < Tailor::Ruler 6 | def initialize(config, options) 7 | super(config, options) 8 | add_lexer_observers :comment, :ignored_nl, :lparen, :nl 9 | @lparen_columns = [] 10 | end 11 | 12 | def comment_update(token, lexed_line, _, lineno, column) 13 | if token =~ /\n$/ 14 | log 'Found comment with trailing newline.' 15 | ignored_nl_update(lexed_line, lineno, column) 16 | end 17 | end 18 | 19 | def ignored_nl_update(lexed_line, lineno, _) 20 | check_spaces_after_lparen(lexed_line, lineno) 21 | end 22 | 23 | def lparen_update(_, column) 24 | @lparen_columns << column 25 | end 26 | 27 | def nl_update(lexed_line, lineno, column) 28 | ignored_nl_update(lexed_line, lineno, column) 29 | end 30 | 31 | # Checks to see if the number of spaces after an lparen equals the value 32 | # at +@config+. 33 | # 34 | # @param [Fixnum] actual_spaces The number of spaces after the lparen. 35 | # @param [Fixnum] lineno Line the potential problem is on. 36 | # @param [Fixnum] column Column the potential problem is on. 37 | def measure(actual_spaces, lineno, column) 38 | if actual_spaces != @config 39 | msg = "Line has #{actual_spaces} space(s) after a (, " 40 | msg << "but should have #{@config}." 41 | 42 | @problems << Problem.new(problem_type, lineno, column + 1, msg, 43 | @options[:level]) 44 | end 45 | end 46 | 47 | def check_spaces_after_lparen(lexed_line, lineno) 48 | unless @lparen_columns.empty? 49 | log "lparens found at: #{@lparen_columns}" 50 | end 51 | 52 | @lparen_columns.each do |column| 53 | actual_spaces = count_spaces(lexed_line, column) 54 | next if actual_spaces.nil? 55 | 56 | if !@do_measurement 57 | log 'Skipping measurement.' 58 | else 59 | measure(actual_spaces, lineno, column) 60 | end 61 | 62 | @do_measurement = true 63 | end 64 | 65 | @lparen_columns.clear 66 | end 67 | 68 | # Counts the number of spaces after the lparen. 69 | # 70 | # @param [LexedLine] lexed_line The LexedLine that contains the context 71 | # the lparen was found in. 72 | # @param [Fixnum] column Column the lparen was found at. 73 | # @return [Fixnum] The number of spaces found after the lparen. 74 | def count_spaces(lexed_line, column) 75 | event_index = lexed_line.event_index(column) 76 | 77 | if event_index.nil? 78 | log 'No lparen in this line. Moving on...' 79 | @do_measurement = false 80 | return 81 | end 82 | 83 | next_event = lexed_line.at(event_index + 1) 84 | log "Next event: #{next_event}" 85 | 86 | if next_event.nil? 87 | log 'lparen must be at the end of the line.' 88 | @do_measurement = false 89 | return 0 90 | end 91 | 92 | [:on_nl, :on_ignored_nl].each do |event| 93 | if next_event[1] == event 94 | log "lparen is followed by a '#{event}'. Moving on." 95 | return 0 96 | end 97 | end 98 | 99 | if next_event[1] == :on_rparen 100 | log 'lparen is followed by an rparen. Moving on.' 101 | @do_measurement = false 102 | return 0 103 | end 104 | 105 | second_next_event = lexed_line.at(event_index + 2) 106 | log "Event + 2: #{second_next_event}" 107 | 108 | [:on_comment, :on_lbrace, :on_lparen].each do |event| 109 | if second_next_event[1] == event 110 | log "Event + 2 is a #{event}. Moving on." 111 | @do_measurement = false 112 | return next_event.last.size 113 | end 114 | end 115 | 116 | next_event[1] != :on_sp ? 0 : next_event.last.size 117 | end 118 | end 119 | end 120 | end 121 | -------------------------------------------------------------------------------- /lib/tailor/rulers/spaces_before_comma_ruler.rb: -------------------------------------------------------------------------------- 1 | require_relative '../ruler' 2 | 3 | class Tailor 4 | module Rulers 5 | 6 | # Checks for spaces before a ',' as given by +@config+. 7 | class SpacesBeforeCommaRuler < Tailor::Ruler 8 | def initialize(config, options) 9 | super(config, options) 10 | add_lexer_observers :comma, :comment, :ignored_nl, :nl 11 | @comma_columns = [] 12 | end 13 | 14 | def comma_update(_, _, column) 15 | @comma_columns << column 16 | end 17 | 18 | def comment_update(token, lexed_line, _, lineno, column) 19 | if token =~ /\n$/ 20 | log 'Found comment with trailing newline.' 21 | ignored_nl_update(lexed_line, lineno, column) 22 | end 23 | end 24 | 25 | # Checks to see if the number of spaces before a comma equals the value 26 | # at +@config+. 27 | # 28 | # @param [Fixnum] actual_spaces The number of spaces before the comma. 29 | # @param [Fixnum] lineno Line the potential problem is on. 30 | # @param [Fixnum] column Column the potential problem is on. 31 | def measure(actual_spaces, lineno, column) 32 | if actual_spaces != @config 33 | msg = "Line has #{actual_spaces} space(s) before a comma, " 34 | msg << "but should have #{@config}." 35 | 36 | @problems << Problem.new(problem_type, lineno, column - 1, msg, 37 | @options[:level]) 38 | end 39 | end 40 | 41 | def check_spaces_before_comma(lexed_line, lineno) 42 | @comma_columns.each do |c| 43 | event_index = lexed_line.event_index(c) 44 | if event_index.nil? 45 | log 'Event index is nil. Weird...' 46 | next 47 | end 48 | 49 | previous_event = lexed_line.at(event_index - 1) 50 | actual_spaces = if previous_event[1] != :on_sp 51 | 0 52 | else 53 | previous_event.last.size 54 | end 55 | 56 | measure(actual_spaces, lineno, c) 57 | end 58 | 59 | @comma_columns.clear 60 | end 61 | 62 | def ignored_nl_update(lexed_line, lineno, _) 63 | check_spaces_before_comma(lexed_line, lineno) 64 | end 65 | 66 | def nl_update(lexed_line, lineno, column) 67 | ignored_nl_update(lexed_line, lineno, column) 68 | end 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/tailor/rulers/spaces_before_lbrace_ruler.rb: -------------------------------------------------------------------------------- 1 | require_relative '../ruler' 2 | 3 | class Tailor 4 | module Rulers 5 | # Detects spaces before a +{+ as given by +@config+. It skips checking 6 | # when: 7 | # * it's the first char in the line. 8 | # * the char before it is a '#{'. 9 | # * the char before it is a '('. 10 | # * the char before it is a '['. 11 | # * it's only preceded by spaces. 12 | class SpacesBeforeLbraceRuler < Tailor::Ruler 13 | def initialize(config, options) 14 | super(config, options) 15 | add_lexer_observers :lbrace 16 | end 17 | 18 | # Counts the spaces before the '{'. 19 | # 20 | # @param [LexedLine] lexed_line 21 | # @param [Fixnum] column 22 | # @return [Fixnum] The number of spaces before the lbrace. 23 | def count_spaces(lexed_line, column) 24 | current_index = lexed_line.event_index(column) 25 | log "Current event index: #{current_index}" 26 | previous_event = lexed_line.at(current_index - 1) 27 | log "Previous event: #{previous_event}" 28 | 29 | if column.zero? || previous_event.nil? 30 | log 'lbrace must be at the beginning of the line.' 31 | @do_measurement = false 32 | return 0 33 | end 34 | 35 | if previous_event[1] == :on_embexpr_beg 36 | log "lbrace comes after a '\#{'." 37 | @do_measurement = false 38 | return 0 39 | end 40 | 41 | if previous_event[1] == :on_lparen 42 | log "lbrace comes after a '('." 43 | @do_measurement = false 44 | return 0 45 | end 46 | 47 | if previous_event[1] == :on_lbracket 48 | log "lbrace comes after a '['." 49 | @do_measurement = false 50 | return 0 51 | end 52 | 53 | return 0 if previous_event[1] != :on_sp 54 | 55 | if current_index - 2 < 0 56 | log 'lbrace comes at the beginning of an indented line.' 57 | @do_measurement = false 58 | return previous_event.last.size 59 | end 60 | 61 | previous_event.last.size 62 | end 63 | 64 | # Called by {Lexer} when :on_lbrace is encountered. 65 | # 66 | # @param [LexedLine] lexed_line 67 | # @param [Fixnum] lineno 68 | # @param [Fixnum] column 69 | def lbrace_update(lexed_line, lineno, column) 70 | count = count_spaces(lexed_line, column) 71 | log "Found #{count} space(s) before lbrace." 72 | 73 | if !@do_measurement 74 | log 'Skipping measurement.' 75 | else 76 | measure(count, lineno, column) 77 | end 78 | 79 | @do_measurement = true 80 | end 81 | 82 | # Checks to see if the counted spaces before an lbrace equals the value 83 | # at +@config+. 84 | # 85 | # @param [Fixnum] actual_spaces The number of spaces before the lbrace. 86 | # @param [Fixnum] lineno Line the potential problem is on. 87 | # @param [Fixnum] column Column the potential problem is on. 88 | def measure(actual_spaces, lineno, column) 89 | if actual_spaces != @config 90 | msg = "Line has #{actual_spaces} space(s) before a {, " 91 | msg << "but should have #{@config}." 92 | 93 | @problems << Problem.new(problem_type, lineno, column, msg, 94 | @options[:level]) 95 | end 96 | end 97 | end 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /lib/tailor/rulers/spaces_before_rbrace_ruler.rb: -------------------------------------------------------------------------------- 1 | require_relative '../ruler' 2 | 3 | class Tailor 4 | module Rulers 5 | # Checks for spaces before a +}+ as given by +@config+. It skips checking 6 | # when: 7 | # * it's the first char in the line. 8 | # * it's the first char in the line, preceded by spaces. 9 | # * it's directly preceded by a +{+. 10 | class SpacesBeforeRbraceRuler < Tailor::Ruler 11 | def initialize(config, options) 12 | super(config, options) 13 | add_lexer_observers :embexpr_beg, :lbrace, :rbrace 14 | @lbrace_nesting = [] 15 | @embexpr_nesting = [] 16 | end 17 | 18 | # @param [LexedLine] lexed_line 19 | # @param [Fixnum] column 20 | # @return [Fixnum] the number of spaces before the rbrace. 21 | def count_spaces(lexed_line, column) 22 | current_index = lexed_line.event_index(column) 23 | log "Current event index: #{current_index}" 24 | previous_event = lexed_line.at(current_index - 1) 25 | log "Previous event: #{previous_event}" 26 | 27 | if column.zero? || previous_event.nil? 28 | log 'rbrace is at the beginning of the line.' 29 | @do_measurement = false 30 | return 0 31 | end 32 | 33 | if previous_event[1] == :on_lbrace 34 | log "rbrace comes after a '{'" 35 | @do_measurement = false 36 | return 0 37 | end 38 | 39 | return 0 if previous_event[1] != :on_sp 40 | 41 | if current_index - 2 < 0 42 | log 'rbrace is at the beginning of an indented line. Moving on.' 43 | @do_measurement = false 44 | return previous_event.last.size 45 | end 46 | 47 | previous_event.last.size 48 | end 49 | 50 | def embexpr_beg_update(_, _, _) 51 | if RUBY_VERSION < '2.0.0' 52 | @lbrace_nesting << :embexpr_beg 53 | end 54 | end 55 | 56 | def lbrace_update(_, _, _) 57 | @lbrace_nesting << :lbrace 58 | end 59 | 60 | # Checks to see if the number of spaces before an rbrace equals the value 61 | # at +@config+. 62 | # 63 | # @param [Fixnum] actual_spaces The number of spaces after the rbrace. 64 | # @param [Fixnum] lineno Line the problem was found on. 65 | # @param [Fixnum] column Column the problem was found on. 66 | def measure(actual_spaces, lineno, column) 67 | if actual_spaces != @config 68 | msg = "Line has #{actual_spaces} space(s) before a }, " 69 | msg << "but should have #{@config}." 70 | 71 | @problems << Problem.new(problem_type, lineno, column, msg, 72 | @options[:level]) 73 | end 74 | end 75 | 76 | # For Ruby versions < 2.0.0-p0, this has to keep track of +{+'s and only 77 | # follow through with the check if the +{+ was an lbrace because Ripper 78 | # doesn't scan the +}+ of an embedded expression (embexpr_end) as such. 79 | # 80 | # @param [Tailor::LexedLine] lexed_line 81 | # @param [Fixnum] lineno 82 | # @param [Fixnum] column 83 | def rbrace_update(lexed_line, lineno, column) 84 | if RUBY_VERSION < '2.0.0' && @lbrace_nesting.last == :embexpr_beg 85 | @lbrace_nesting.pop 86 | return 87 | end 88 | 89 | @lbrace_nesting.pop 90 | 91 | count = count_spaces(lexed_line, column) 92 | log "Found #{count} space(s) before rbrace." 93 | 94 | if !@do_measurement 95 | log 'Skipping measurement.' 96 | else 97 | measure(count, lineno, column) 98 | end 99 | 100 | @do_measurement = true 101 | end 102 | end 103 | end 104 | end 105 | -------------------------------------------------------------------------------- /lib/tailor/rulers/spaces_before_rbracket_ruler.rb: -------------------------------------------------------------------------------- 1 | require_relative '../ruler' 2 | 3 | class Tailor 4 | module Rulers 5 | 6 | # Checks for spaces before a +]+ as given by +@config+. It skips checking 7 | # when: 8 | # * it's the first char in the line. 9 | # * it's directly preceded by a +[+. 10 | # * it's directly preceded by spaces, then a +[+. 11 | class SpacesBeforeRbracketRuler < Tailor::Ruler 12 | def initialize(config, options) 13 | super(config, options) 14 | add_lexer_observers :rbracket 15 | end 16 | 17 | # @param [LexedLine] lexed_line 18 | # @param [Fixnum] column 19 | # @return [Fixnum] The number of spaces before the rbracket. 20 | def count_spaces(lexed_line, column) 21 | current_index = lexed_line.event_index(column) 22 | log "Current event index: #{current_index}" 23 | previous_event = lexed_line.at(current_index - 1) 24 | log "Previous event: #{previous_event}" 25 | 26 | if column.zero? || previous_event.nil? || 27 | previous_event[1] == :on_lbracket 28 | return nil 29 | end 30 | 31 | return 0 if previous_event[1] != :on_sp 32 | return nil if current_index - 2 < 0 33 | 34 | second_previous_event = lexed_line.at(current_index - 2) 35 | return nil if second_previous_event[1] == :on_lbracket 36 | 37 | previous_event.last.size 38 | end 39 | 40 | # Checks to see if the counted spaces before an rbracket equals the value 41 | # at +@config+. 42 | # 43 | # @param [Fixnum] actual_spaces The number of spaces before the rbracket. 44 | # @param [Fixnum] lineno Line the potential problem is on. 45 | # @param [Fixnum] column Column the potential problem is on. 46 | def measure(actual_spaces, lineno, column) 47 | if actual_spaces != @config 48 | msg = "Line has #{actual_spaces} space(s) before a ], " 49 | msg << "but should have #{@config}." 50 | 51 | @problems << Problem.new(problem_type, lineno, column, msg, 52 | @options[:level]) 53 | end 54 | end 55 | 56 | # This has to keep track of +{+s and only follow through with the check 57 | # if the +{+ was an lbrace because Ripper doesn't scan the +}+ of an 58 | # embedded expression (embexpr_end) as such. 59 | # 60 | # @param [Tailor::LexedLine] lexed_line 61 | # @param [Fixnum] lineno 62 | # @param [Fixnum] column 63 | def rbracket_update(lexed_line, lineno, column) 64 | count = count_spaces(lexed_line, column) 65 | 66 | if count.nil? 67 | log 'rbracket must be at the beginning of the line.' 68 | return 69 | else 70 | log "Found #{count} space(s) before rbracket." 71 | end 72 | 73 | measure(count, lineno, column) 74 | end 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /lib/tailor/rulers/spaces_before_rparen_ruler.rb: -------------------------------------------------------------------------------- 1 | require_relative '../ruler' 2 | 3 | class Tailor 4 | module Rulers 5 | 6 | # Checks for spaces before a +)+ as given by +@config+. It skips checking 7 | # when: 8 | # * it's the first char in the line. 9 | # * it's directly preceded by a +(+. 10 | # * it's directly preceded by spaces, then a +(+. 11 | class SpacesBeforeRparenRuler < Tailor::Ruler 12 | def initialize(config, options) 13 | super(config, options) 14 | add_lexer_observers :rparen 15 | end 16 | 17 | # @param [LexedLine] lexed_line 18 | # @param [Fixnum] column 19 | # @return [Fixnum] the number of spaces before the rparen. 20 | def count_spaces(lexed_line, column) 21 | current_index = lexed_line.event_index(column) 22 | log "Current event index: #{current_index}" 23 | previous_event = lexed_line.at(current_index - 1) 24 | log "Previous event: #{previous_event}" 25 | 26 | if column.zero? || previous_event.nil? || 27 | previous_event[1] == :on_lparen 28 | return nil 29 | end 30 | 31 | return 0 if previous_event[1] != :on_sp 32 | return nil if current_index - 2 < 0 33 | 34 | second_previous_event = lexed_line.at(current_index - 2) 35 | return nil if second_previous_event[1] == :on_lparen 36 | 37 | previous_event.last.size 38 | end 39 | 40 | # Checks to see if the counted spaces before an rparen equals the value 41 | # at +@config+. 42 | # 43 | # @param [Fixnum] actual_spaces The number of spaces before the rparen. 44 | # @param [Fixnum] lineno Line the potential problem is on. 45 | # @param [Fixnum] column Column the potential problem is on. 46 | def measure(actual_spaces, lineno, column) 47 | if actual_spaces != @config 48 | msg = "Line has #{actual_spaces} space(s) before a ), " 49 | msg << "but should have #{@config}." 50 | 51 | @problems << Problem.new(problem_type, lineno, column, msg, 52 | @options[:level]) 53 | end 54 | end 55 | 56 | # This has to keep track of +{+s and only follow through with the check 57 | # if the +{+ was an lbrace because Ripper doesn't scan the +}+ of an 58 | # embedded expression (embexpr_end) as such. 59 | # 60 | # @param [Tailor::LexedLine] lexed_line 61 | # @param [Fixnum] lineno 62 | # @param [Fixnum] column 63 | def rparen_update(lexed_line, lineno, column) 64 | count = count_spaces(lexed_line, column) 65 | 66 | if count.nil? 67 | log 'rparen must be at the beginning of the line.' 68 | return 69 | else 70 | log "Found #{count} space(s) before rparen." 71 | end 72 | 73 | measure(count, lineno, column) 74 | end 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /lib/tailor/rulers/spaces_in_empty_braces_ruler.rb: -------------------------------------------------------------------------------- 1 | require_relative '../ruler' 2 | 3 | class Tailor 4 | module Rulers 5 | # Checks for spaces that exist between a +{+ and +}+ when there is only 6 | # space in between them. 7 | class SpacesInEmptyBracesRuler < Tailor::Ruler 8 | def initialize(config, options) 9 | super(config, options) 10 | add_lexer_observers :embexpr_beg, :lbrace, :rbrace 11 | @lbrace_nesting = [] 12 | end 13 | 14 | # @param [LexedLine] lexed_line 15 | # @param [Fixnum] column 16 | # @return [Fixnum] The number of spaces before the rbrace. 17 | def count_spaces(lexed_line, column) 18 | current_index = lexed_line.event_index(column) 19 | log "Current event index: #{current_index}" 20 | previous_event = lexed_line.at(current_index - 1) 21 | log "Previous event: #{previous_event}" 22 | 23 | if column.zero? || previous_event.nil? 24 | return 25 | end 26 | 27 | if previous_event[1] == :on_lbrace 28 | return 0 29 | end 30 | 31 | if previous_event[1] == :on_sp 32 | second_previous_event = lexed_line.at(current_index - 2) 33 | 34 | if second_previous_event[1] == :on_lbrace 35 | previous_event.last.size 36 | else 37 | nil 38 | end 39 | end 40 | end 41 | 42 | def embexpr_beg_update(_, _, _) 43 | @lbrace_nesting << :embexpr_beg 44 | end 45 | 46 | def lbrace_update(_, _, _) 47 | @lbrace_nesting << :lbrace 48 | end 49 | 50 | def nl_update(lexed_line, lineno, column) 51 | ignored_nl_update(lexed_line, lineno, column) 52 | end 53 | 54 | # Checks to see if the counted spaces between an lbrace and an rbrace 55 | # equals the value at +@config+. 56 | # 57 | # @param [Fixnum] actual_spaces The number of spaces before the lbrace. 58 | # @param [Fixnum] lineno Line the potential problem is on. 59 | # @param [Fixnum] column Column the potential problem is on. 60 | def measure(actual_spaces, lineno, column) 61 | if actual_spaces != @config 62 | msg = "Line has #{actual_spaces} space(s) in between empty " 63 | msg << "braces, but should have #{@config}." 64 | 65 | @problems << Problem.new(problem_type, lineno, column, msg, 66 | @options[:level]) 67 | end 68 | end 69 | 70 | # This has to keep track of +{+s and only follow through with the check 71 | # if the +{+ was an lbrace because Ripper doesn't scan the +}+ of an 72 | # embedded expression (embexpr_end) as such. 73 | # 74 | # @param [Tailor::LexedLine] lexed_line 75 | # @param [Fixnum] lineno 76 | # @param [Fixnum] column 77 | def rbrace_update(lexed_line, lineno, column) 78 | if @lbrace_nesting.last == :embexpr_beg 79 | @lbrace_nesting.pop 80 | return 81 | end 82 | 83 | @lbrace_nesting.pop 84 | count = count_spaces(lexed_line, column) 85 | 86 | if count.nil? 87 | log "Braces aren't empty. Moving on." 88 | return 89 | else 90 | log "Found #{count} space(s) before rbrace." 91 | end 92 | 93 | measure(count, lineno, column) 94 | end 95 | end 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /lib/tailor/rulers/trailing_newlines_ruler.rb: -------------------------------------------------------------------------------- 1 | require_relative '../ruler' 2 | 3 | 4 | class Tailor 5 | module Rulers 6 | class TrailingNewlinesRuler < Tailor::Ruler 7 | def initialize(config, options) 8 | super(config, options) 9 | add_lexer_observers :file_end 10 | end 11 | 12 | # Checks to see if the number of newlines at the end of the file is not 13 | # equal to the value at +@config+. 14 | # 15 | # @param [Fixnum] trailing_newline_count The number of newlines at the end 16 | # of the file. 17 | def measure(trailing_newline_count) 18 | if trailing_newline_count != @config 19 | lineno = '' 20 | column = '' 21 | msg = "File has #{trailing_newline_count} trailing " 22 | msg << "newlines, but should have #{@config}." 23 | 24 | @problems << Problem.new(problem_type, lineno, column, msg, 25 | @options[:level]) 26 | end 27 | end 28 | 29 | # Checks to see if the file's final character is a \n. If it is, it just 30 | # returns the text that was passed in. If it's not, it adds a \n, since 31 | # the current indentation-checking algorithm only checks indent levels when 32 | # it parses a newline character (without this, indentation problems on the 33 | # final line won't ever get caught). 34 | # 35 | # @param [Fixnum] trailing_newline_count 36 | def file_end_update(trailing_newline_count) 37 | measure(trailing_newline_count) 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/tailor/runtime_error.rb: -------------------------------------------------------------------------------- 1 | class Tailor 2 | class RuntimeError < RuntimeError; end 3 | end 4 | -------------------------------------------------------------------------------- /lib/tailor/version.rb: -------------------------------------------------------------------------------- 1 | class Tailor 2 | VERSION = '1.4.1' 3 | end 4 | -------------------------------------------------------------------------------- /spec/functional/horizontal_spacing/brackets_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'tailor/critic' 3 | require 'tailor/configuration/style' 4 | 5 | BRACKETS = {} 6 | BRACKETS['space_in_empty_array'] = %([ ]) 7 | BRACKETS['simple_array_space_after_lbracket'] = %([ 1, 2, 3]) 8 | BRACKETS['simple_array_space_before_rbracket'] = %([1, 2, 3 ]) 9 | BRACKETS['hash_key_ref_space_before_rbracket'] = %(thing[:one ]) 10 | BRACKETS['hash_key_ref_space_after_lbracket'] = %(thing[ :one]) 11 | BRACKETS['two_d_array_space_after_lbrackets'] = 12 | %([ [1, 2, 3], [ 'a', 'b', 'c']]) 13 | BRACKETS['two_d_array_space_before_rbrackets'] = 14 | %([[1, 2, 3 ], [ 'a', 'b', 'c'] ]) 15 | 16 | describe 'Detection of spaces around brackets' do 17 | before do 18 | allow(Tailor::Logger).to receive(:log) 19 | FakeFS.activate! 20 | File.open(file_name, 'w') { |f| f.write contents } 21 | critic.check_file(file_name, style.to_hash) 22 | end 23 | 24 | let(:critic) do 25 | Tailor::Critic.new 26 | end 27 | 28 | let(:contents) { BRACKETS[file_name] } 29 | 30 | let(:style) do 31 | style = Tailor::Configuration::Style.new 32 | style.trailing_newlines 0, level: :off 33 | style.allow_invalid_ruby true, level: :off 34 | 35 | style 36 | end 37 | 38 | context 'Arrays' do 39 | context 'empty with space inside' do 40 | let(:file_name) { 'space_in_empty_array' } 41 | specify { expect(critic.problems[file_name].size).to eq 1 } 42 | specify { expect(critic.problems[file_name].first[:type]).to eq 'spaces_after_lbracket' } 43 | specify { expect(critic.problems[file_name].first[:line]).to eq 1 } 44 | specify { expect(critic.problems[file_name].first[:column]).to eq 1 } 45 | specify { expect(critic.problems[file_name].first[:level]).to eq :error } 46 | end 47 | 48 | context 'space after lbracket' do 49 | let(:file_name) { 'simple_array_space_after_lbracket' } 50 | specify { expect(critic.problems[file_name].size).to eq 1 } 51 | specify { expect(critic.problems[file_name].first[:type]).to eq 'spaces_after_lbracket' } 52 | specify { expect(critic.problems[file_name].first[:line]).to eq 1 } 53 | specify { expect(critic.problems[file_name].first[:column]).to eq 1 } 54 | specify { expect(critic.problems[file_name].first[:level]).to eq :error } 55 | end 56 | 57 | context 'space before rbracket' do 58 | let(:file_name) { 'simple_array_space_before_rbracket' } 59 | specify { expect(critic.problems[file_name].size).to eq 1 } 60 | specify { expect(critic.problems[file_name].first[:type]).to eq 'spaces_before_rbracket' } 61 | specify { expect(critic.problems[file_name].first[:line]).to eq 1 } 62 | specify { expect(critic.problems[file_name].first[:column]).to eq 9 } 63 | specify { expect(critic.problems[file_name].first[:level]).to eq :error } 64 | end 65 | end 66 | 67 | context 'Hash key references' do 68 | context 'space before rbracket' do 69 | let(:file_name) { 'hash_key_ref_space_before_rbracket' } 70 | specify { expect(critic.problems[file_name].size).to eq 1 } 71 | specify { expect(critic.problems[file_name].first[:type]).to eq 'spaces_before_rbracket' } 72 | specify { expect(critic.problems[file_name].first[:line]).to eq 1 } 73 | specify { expect(critic.problems[file_name].first[:column]).to eq 11 } 74 | specify { expect(critic.problems[file_name].first[:level]).to eq :error } 75 | end 76 | 77 | context 'space after lbracket' do 78 | let(:file_name) { 'hash_key_ref_space_after_lbracket' } 79 | specify { expect(critic.problems[file_name].size).to eq 1 } 80 | specify { expect(critic.problems[file_name].first[:type]).to eq 'spaces_after_lbracket' } 81 | specify { expect(critic.problems[file_name].first[:line]).to eq 1 } 82 | specify { expect(critic.problems[file_name].first[:column]).to eq 6 } 83 | specify { expect(critic.problems[file_name].first[:level]).to eq :error } 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /spec/functional/horizontal_spacing/comma_spacing_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'tailor/critic' 3 | require 'tailor/configuration/style' 4 | 5 | COMMA_SPACING = {} 6 | COMMA_SPACING['no_space_after_comma'] = %([1,2]) 7 | COMMA_SPACING['two_spaces_after_comma'] = %([1, 2]) 8 | COMMA_SPACING['one_space_before_comma'] = %([1 , 2]) 9 | COMMA_SPACING['two_spaces_before_comma'] = %([1 , 2]) 10 | COMMA_SPACING['two_spaces_before_comma_twice'] = %([1 , 2 , 3]) 11 | COMMA_SPACING['two_spaces_after_comma_twice'] = %([1, 2, 3]) 12 | 13 | COMMA_SPACING['spaces_before_with_trailing_comments'] = %([ 14 | 1 , # Comment! 15 | 2 , # Another comment. 16 | ) 17 | 18 | describe 'Spacing around comma detection' do 19 | before do 20 | allow(Tailor::Logger).to receive(:log) 21 | FakeFS.activate! 22 | File.open(file_name, 'w') { |f| f.write contents } 23 | critic.check_file(file_name, style.to_hash) 24 | end 25 | 26 | let(:critic) do 27 | Tailor::Critic.new 28 | end 29 | 30 | let(:contents) { COMMA_SPACING[file_name] } 31 | 32 | let(:style) do 33 | style = Tailor::Configuration::Style.new 34 | style.trailing_newlines 0, level: :off 35 | style.allow_invalid_ruby true, level: :off 36 | 37 | style 38 | end 39 | 40 | context 'no space after a comma' do 41 | let(:file_name) { 'no_space_after_comma' } 42 | specify { expect(critic.problems[file_name].size).to eq 1 } 43 | specify { expect(critic.problems[file_name].first[:type]).to eq 'spaces_after_comma' } 44 | specify { expect(critic.problems[file_name].first[:line]).to eq 1 } 45 | specify { expect(critic.problems[file_name].first[:column]).to eq 3 } 46 | specify { expect(critic.problems[file_name].first[:level]).to eq :error } 47 | end 48 | 49 | context 'two spaces after a comma' do 50 | let(:file_name) { 'two_spaces_after_comma' } 51 | specify { expect(critic.problems[file_name].size).to eq 1 } 52 | specify { expect(critic.problems[file_name].first[:type]).to eq 'spaces_after_comma' } 53 | specify { expect(critic.problems[file_name].first[:line]).to eq 1 } 54 | specify { expect(critic.problems[file_name].first[:column]).to eq 3 } 55 | specify { expect(critic.problems[file_name].first[:level]).to eq :error } 56 | end 57 | 58 | context 'one space before comma' do 59 | let(:file_name) { 'one_space_before_comma' } 60 | specify { expect(critic.problems[file_name].size).to eq 1 } 61 | specify { expect(critic.problems[file_name].first[:type]).to eq 'spaces_before_comma' } 62 | specify { expect(critic.problems[file_name].first[:line]).to eq 1 } 63 | specify { expect(critic.problems[file_name].first[:column]).to eq 2 } 64 | specify { expect(critic.problems[file_name].first[:level]).to eq :error } 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /spec/functional/horizontal_spacing/hard_tabs_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'tailor/critic' 3 | require 'tailor/configuration/style' 4 | 5 | #------------------------------------------------------------------------------- 6 | # Hard tabs 7 | #------------------------------------------------------------------------------- 8 | HARD_TABS = {} 9 | 10 | HARD_TABS['hard_tab'] = 11 | %(def something 12 | \tputs 'something' 13 | end) 14 | 15 | HARD_TABS['hard_tab_with_spaces'] = 16 | %(class Thing 17 | def something 18 | \t puts 'something' 19 | end 20 | end) 21 | 22 | # This only reports the hard tab problem (and not the indentation problem) 23 | # because a hard tab is counted as 1 space; here, this is 4 spaces, so it 24 | # looks correct to the parser. I'm leaving this behavior, as detecting the 25 | # hard tab should signal the problem. If you fix the hard tab and don't 26 | # fix indentation, tailor will flag you on the indentation on the next run. 27 | HARD_TABS['hard_tab_with_1_indented_space'] = 28 | %(class Thing 29 | def something 30 | \t puts 'something' 31 | end 32 | end) 33 | 34 | HARD_TABS['hard_tab_with_2_indented_spaces'] = 35 | %(class Thing 36 | def something 37 | \t puts 'something' 38 | end 39 | end) 40 | 41 | describe 'Hard tab detection' do 42 | before do 43 | allow(Tailor::Logger).to receive(:log) 44 | FakeFS.activate! 45 | File.open(file_name, 'w') { |f| f.write contents } 46 | critic.check_file(file_name, style.to_hash) 47 | end 48 | 49 | let(:critic) do 50 | Tailor::Critic.new 51 | end 52 | 53 | let(:contents) { HARD_TABS[file_name] } 54 | 55 | let(:style) do 56 | style = Tailor::Configuration::Style.new 57 | style.trailing_newlines 0, level: :off 58 | style.allow_invalid_ruby true, level: :off 59 | 60 | style 61 | end 62 | 63 | context '1 hard tab' do 64 | let(:file_name) { 'hard_tab' } 65 | specify { expect(critic.problems[file_name].size).to eq 2 } 66 | specify { expect(critic.problems[file_name].first[:type]).to eq 'allow_hard_tabs' } 67 | specify { expect(critic.problems[file_name].first[:line]).to eq 2 } 68 | specify { expect(critic.problems[file_name].first[:column]).to eq 0 } 69 | specify { expect(critic.problems[file_name].first[:level]).to eq :error } 70 | specify { expect(critic.problems[file_name].last[:type]).to eq 'indentation_spaces' } 71 | specify { expect(critic.problems[file_name].last[:line]).to eq 2 } 72 | specify { expect(critic.problems[file_name].last[:column]).to eq 1 } 73 | specify { expect(critic.problems[file_name].last[:level]).to eq :error } 74 | end 75 | 76 | context '1 hard tab with 2 spaces after it' do 77 | let(:file_name) { 'hard_tab_with_spaces' } 78 | specify { expect(critic.problems[file_name].size).to eq 2 } 79 | specify { expect(critic.problems[file_name].first[:type]).to eq 'allow_hard_tabs' } 80 | specify { expect(critic.problems[file_name].first[:line]).to eq 3 } 81 | specify { expect(critic.problems[file_name].first[:column]).to eq 0 } 82 | specify { expect(critic.problems[file_name].first[:level]).to eq :error } 83 | specify { expect(critic.problems[file_name].last[:type]).to eq 'indentation_spaces' } 84 | specify { expect(critic.problems[file_name].last[:line]).to eq 3 } 85 | specify { expect(critic.problems[file_name].last[:column]).to eq 3 } 86 | specify { expect(critic.problems[file_name].last[:level]).to eq :error } 87 | end 88 | 89 | context '1 hard tab with 3 spaces after it' do 90 | let(:file_name) { 'hard_tab_with_1_indented_space' } 91 | specify { expect(critic.problems[file_name].size).to eq 1 } 92 | specify { expect(critic.problems[file_name].first[:type]).to eq 'allow_hard_tabs' } 93 | specify { expect(critic.problems[file_name].first[:line]).to eq 3 } 94 | specify { expect(critic.problems[file_name].first[:column]).to eq 0 } 95 | specify { expect(critic.problems[file_name].first[:level]).to eq :error } 96 | end 97 | 98 | context '1 hard tab with 4 spaces after it' do 99 | let(:file_name) { 'hard_tab_with_2_indented_spaces' } 100 | specify { expect(critic.problems[file_name].size).to eq 2 } 101 | specify { expect(critic.problems[file_name].first[:type]).to eq 'allow_hard_tabs' } 102 | specify { expect(critic.problems[file_name].first[:line]).to eq 3 } 103 | specify { expect(critic.problems[file_name].first[:column]).to eq 0 } 104 | specify { expect(critic.problems[file_name].first[:level]).to eq :error } 105 | specify { expect(critic.problems[file_name].last[:type]).to eq 'indentation_spaces' } 106 | specify { expect(critic.problems[file_name].last[:line]).to eq 3 } 107 | specify { expect(critic.problems[file_name].last[:column]).to eq 5 } 108 | specify { expect(critic.problems[file_name].last[:level]).to eq :error } 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /spec/functional/horizontal_spacing/long_lines_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'tailor/critic' 3 | require 'tailor/configuration/style' 4 | 5 | LONG_LINE = {} 6 | LONG_LINE['long_line_no_newline'] = %('#{'#' * 79}') 7 | LONG_LINE['long_line_newline_at_82'] = %('#{'#' * 79}' 8 | ) 9 | 10 | describe 'Long line detection' do 11 | before do 12 | allow(Tailor::Logger).to receive(:log) 13 | FakeFS.activate! 14 | File.open(file_name, 'w') { |f| f.write contents } 15 | critic.check_file(file_name, style.to_hash) 16 | end 17 | 18 | let(:critic) do 19 | Tailor::Critic.new 20 | end 21 | 22 | let(:contents) { LONG_LINE[file_name] } 23 | 24 | let(:style) do 25 | style = Tailor::Configuration::Style.new 26 | style.trailing_newlines 0, level: :off 27 | style.allow_invalid_ruby true, level: :off 28 | 29 | style 30 | end 31 | 32 | context 'line is 81 chars, no newline' do 33 | let(:file_name) { 'long_line_no_newline' } 34 | specify { expect(critic.problems[file_name].size).to eq 1 } 35 | specify { expect(critic.problems[file_name].first[:type]).to eq 'max_line_length' } 36 | specify { expect(critic.problems[file_name].first[:line]).to eq 1 } 37 | specify { expect(critic.problems[file_name].first[:column]).to eq 81 } 38 | specify { expect(critic.problems[file_name].first[:level]).to eq :error } 39 | end 40 | 41 | context 'line is 81 chars, plus a newline' do 42 | let(:file_name) { 'long_line_newline_at_82' } 43 | specify { expect(critic.problems[file_name].size).to eq 1 } 44 | specify { expect(critic.problems[file_name].first[:type]).to eq 'max_line_length' } 45 | specify { expect(critic.problems[file_name].first[:line]).to eq 1 } 46 | specify { expect(critic.problems[file_name].first[:column]).to eq 81 } 47 | specify { expect(critic.problems[file_name].first[:level]).to eq :error } 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /spec/functional/horizontal_spacing/long_methods_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'tailor/critic' 3 | require 'tailor/configuration/style' 4 | 5 | LONG_METHOD_IN_CLASS = {} 6 | LONG_METHOD_IN_CLASS['ok_with_equals'] = <<-METH 7 | class Test 8 | def method1 9 | [1, 2, 3, 4].each do |uuid| 10 | next if (@profiles[uuid].to_s.start_with? "SM" || @profiles[uuid] == 11 | :SystemLogger) 12 | end 13 | end 14 | 15 | def method2 16 | puts 'Do not ever get here.' 17 | end 18 | end 19 | METH 20 | 21 | describe 'Long method detection' do 22 | before do 23 | allow(Tailor::Logger).to receive(:log) 24 | FakeFS.activate! 25 | File.open('long_method.rb', 'w') { |f| f.write contents } 26 | subject.check_file(file_name, style.to_hash) 27 | end 28 | 29 | subject do 30 | Tailor::Critic.new 31 | end 32 | 33 | let(:contents) { LONG_METHOD_IN_CLASS[file_name] } 34 | 35 | let(:style) do 36 | style = Tailor::Configuration::Style.new 37 | style.trailing_newlines 0, level: :off 38 | style.indentation_spaces 2, level: :off 39 | style.allow_invalid_ruby true, level: :off 40 | style.max_code_lines_in_method 3 41 | 42 | style 43 | end 44 | 45 | context 'methods are within limits' do 46 | context 'method ends with line that ends with ==' do 47 | let(:file_name) { 'ok_with_equals' } 48 | specify do 49 | pending 'https://github.com/turboladen/tailor/issues/112' 50 | 51 | expect(subject.problems[file_name].size).to eq 1 52 | end 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /spec/functional/horizontal_spacing/trailing_whitespace_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require_relative '../../support/horizontal_spacing_cases' 3 | require 'tailor/critic' 4 | require 'tailor/configuration/style' 5 | 6 | TRAILING_WHITESPACE = {} 7 | TRAILING_WHITESPACE['empty_line_with_spaces'] = %( ) 8 | TRAILING_WHITESPACE['empty_line_with_spaces_in_method'] = %(def thing 9 | 10 | puts 'something' 11 | end) 12 | 13 | TRAILING_WHITESPACE['trailing_spaces_on_def'] = %(def thing 14 | puts 'something' 15 | end) 16 | 17 | describe 'Trailing whitespace detection' do 18 | before do 19 | allow(Tailor::Logger).to receive(:log) 20 | FakeFS.activate! 21 | File.open(file_name, 'w') { |f| f.write contents } 22 | critic.check_file(file_name, style.to_hash) 23 | end 24 | 25 | let(:critic) do 26 | Tailor::Critic.new 27 | end 28 | 29 | let(:contents) { TRAILING_WHITESPACE[file_name] } 30 | 31 | let(:style) do 32 | style = Tailor::Configuration::Style.new 33 | style.trailing_newlines 0, level: :off 34 | style.allow_invalid_ruby true, level: :off 35 | 36 | style 37 | end 38 | 39 | context 'line is empty spaces' do 40 | let(:file_name) { 'empty_line_with_spaces' } 41 | specify { critic.problems[file_name].size.should be 1 } 42 | specify { critic.problems[file_name].first[:type].should == 'allow_trailing_line_spaces' } 43 | specify { critic.problems[file_name].first[:line].should be 1 } 44 | specify { critic.problems[file_name].first[:column].should be 2 } 45 | specify { critic.problems[file_name].first[:level].should be :error } 46 | end 47 | 48 | context 'method contains an empty line with spaces' do 49 | let(:file_name) { 'empty_line_with_spaces_in_method' } 50 | specify { critic.problems[file_name].size.should be 1 } 51 | specify { critic.problems[file_name].first[:type].should == 'allow_trailing_line_spaces' } 52 | specify { critic.problems[file_name].first[:line].should be 2 } 53 | specify { critic.problems[file_name].first[:column].should be 2 } 54 | specify { critic.problems[file_name].first[:level].should be :error } 55 | end 56 | 57 | context 'def line ends with spaces' do 58 | let(:file_name) { 'trailing_spaces_on_def' } 59 | specify { critic.problems[file_name].size.should be 1 } 60 | specify { critic.problems[file_name].first[:type].should == 'allow_trailing_line_spaces' } 61 | specify { critic.problems[file_name].first[:line].should be 1 } 62 | specify { critic.problems[file_name].first[:column].should be 11 } 63 | specify { critic.problems[file_name].first[:level].should be :error } 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /spec/functional/horizontal_spacing_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require_relative '../support/horizontal_spacing_cases' 3 | require 'tailor/critic' 4 | require 'tailor/configuration/style' 5 | 6 | describe 'Horizontal Space problem detection' do 7 | before do 8 | allow(Tailor::Logger).to receive(:log) 9 | FakeFS.activate! 10 | end 11 | 12 | let(:critic) do 13 | Tailor::Critic.new 14 | end 15 | 16 | let(:style) do 17 | style = Tailor::Configuration::Style.new 18 | style.trailing_newlines 0, level: :off 19 | style.allow_invalid_ruby true, level: :off 20 | 21 | style 22 | end 23 | 24 | H_SPACING_OK.each do |file_name, contents| 25 | before do 26 | FileUtils.touch file_name 27 | File.open(file_name, 'w') { |f| f.write contents } 28 | end 29 | 30 | it 'is OK' do 31 | critic.check_file(file_name, style.to_hash) 32 | expect(critic.problems).to eq(file_name => []) 33 | end 34 | end 35 | 36 | context 'line ends with a backslash' do 37 | let(:file_name) { 'line_split_by_backslash' } 38 | 39 | before do 40 | FileUtils.touch file_name 41 | File.open(file_name, 'w') { |f| f.write contents } 42 | end 43 | 44 | context 'no problems' do 45 | let(:contents) do 46 | %(execute 'myscript' do 47 | command \\ 48 | '/some/line/that/is/not/over/eighty/chars.sh' 49 | only_if { something } 50 | end) 51 | end 52 | 53 | it 'is OK' do 54 | critic.check_file(file_name, style.to_hash) 55 | expect(critic.problems).to eq(file_name => []) 56 | end 57 | end 58 | 59 | context 'line after backslash is too long' do 60 | let(:contents) do 61 | %(execute 'myscript' do 62 | command \\ 63 | '#{'*' * 75}' 64 | only_if { something } 65 | end) 66 | end 67 | 68 | it 'is OK' do 69 | critic.check_file(file_name, style.to_hash) 70 | expect(critic.problems).to eq( 71 | file_name => [ 72 | { 73 | type: 'max_line_length', 74 | line: 3, 75 | column: 81, 76 | message: 'Line is 81 chars long, but should be 80.', 77 | level: :error 78 | } 79 | ]) 80 | end 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /spec/functional/indentation_spacing_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require_relative '../support/good_indentation_cases' 3 | require 'tailor/critic' 4 | require 'tailor/configuration/style' 5 | 6 | describe 'Indentation spacing problem detection' do 7 | before do 8 | allow(Tailor::Logger).to receive(:log) 9 | FakeFS.activate! 10 | end 11 | 12 | let(:critic) do 13 | Tailor::Critic.new 14 | end 15 | 16 | let(:style) do 17 | style = Tailor::Configuration::Style.new 18 | style.trailing_newlines 0, level: :off 19 | style.allow_invalid_ruby true, level: :off 20 | 21 | style 22 | end 23 | 24 | let(:contents) { INDENT_1[file_name] } 25 | 26 | INDENT_OK.each do |file_name, contents| 27 | before do 28 | FileUtils.touch file_name 29 | File.open(file_name, 'w') { |f| f.write contents } 30 | end 31 | 32 | it 'is OK' do 33 | critic.check_file(file_name, style.to_hash) 34 | expect(critic.problems).to eq(file_name => []) 35 | end 36 | end 37 | 38 | context 'case statement with indented whens' do 39 | let(:file_name) { 'case_whens_in' } 40 | 41 | let(:contents) do 42 | %(def my_method 43 | case true 44 | when true 45 | puts "stuff" 46 | when false 47 | puts "blah blah" 48 | end 49 | end) 50 | end 51 | 52 | it 'is OK' do 53 | skip 'Implementation of the option to allow for this' 54 | end 55 | end 56 | 57 | context 'method with rparen on following line' do 58 | let(:file_name) { 'method_closing_lonely_paren' } 59 | 60 | let(:contents) do 61 | %{def your_thing(one 62 | ) 63 | end} 64 | end 65 | 66 | it 'is OK' do 67 | skip 'Implementation' 68 | end 69 | end 70 | 71 | context 'lonely rparen and do on the same line' do 72 | let(:file_name) { 'rparen_and_do_same_line' } 73 | 74 | let(:contents) do 75 | %{opt.on('-c', '--config-file FILE', 76 | "Use a specific config file.") do |config| 77 | options.config_file = config 78 | end} 79 | end 80 | 81 | it 'is OK' do 82 | skip 'Implementation' 83 | end 84 | end 85 | 86 | context 'block chained on a block' do 87 | let(:file_name) { 'block_chain' } 88 | 89 | let(:contents) do 90 | %({ 91 | a: 1 92 | }.each do |k, v| 93 | puts k, v 94 | end) 95 | end 96 | 97 | it 'is OK' do 98 | critic.check_file(file_name, style.to_hash) 99 | expect(critic.problems).to eq(file_name => []) 100 | end 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /spec/functional/naming/camel_case_methods_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'tailor/critic' 3 | require 'tailor/configuration/style' 4 | 5 | CAMEL_CASE_METHODS = {} 6 | 7 | CAMEL_CASE_METHODS['one_caps_camel_case_method'] = 8 | %(def thingOne 9 | end) 10 | 11 | CAMEL_CASE_METHODS['one_caps_camel_case_method_trailing_comment'] = 12 | %(def thingOne # comment 13 | end) 14 | 15 | describe 'Detection of camel case methods' do 16 | before do 17 | Tailor::Logger.stub(:log) 18 | FakeFS.activate! 19 | File.open(file_name, 'w') { |f| f.write contents } 20 | critic.check_file(file_name, style.to_hash) 21 | end 22 | 23 | let(:critic) do 24 | Tailor::Critic.new 25 | end 26 | 27 | let(:contents) { CAMEL_CASE_METHODS[file_name] } 28 | 29 | let(:style) do 30 | style = Tailor::Configuration::Style.new 31 | style.trailing_newlines 0, level: :off 32 | style.allow_invalid_ruby true, level: :off 33 | 34 | style 35 | end 36 | 37 | context 'standard camel case method' do 38 | let(:file_name) { 'one_caps_camel_case_method' } 39 | specify { critic.problems[file_name].size.should be 1 } 40 | specify { critic.problems[file_name].first[:type].should == 'allow_camel_case_methods' } 41 | specify { critic.problems[file_name].first[:line].should be 1 } 42 | specify { critic.problems[file_name].first[:column].should be 4 } 43 | specify { critic.problems[file_name].first[:level].should be :error } 44 | end 45 | 46 | context 'standard camel case method, trailing comment' do 47 | let(:file_name) { 'one_caps_camel_case_method_trailing_comment' } 48 | specify { critic.problems[file_name].size.should be 1 } 49 | specify { critic.problems[file_name].first[:type].should == 'allow_camel_case_methods' } 50 | specify { critic.problems[file_name].first[:line].should be 1 } 51 | specify { critic.problems[file_name].first[:column].should be 4 } 52 | specify { critic.problems[file_name].first[:level].should be :error } 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /spec/functional/naming/screaming_snake_case_classes_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'tailor/critic' 3 | require 'tailor/configuration/style' 4 | 5 | SCREAMING_SNAKE_CASE_CLASSES = {} 6 | 7 | SCREAMING_SNAKE_CASE_CLASSES['one_screaming_snake_case_class'] = 8 | %(class Thing_One 9 | end) 10 | 11 | SCREAMING_SNAKE_CASE_CLASSES['one_screaming_snake_case_module'] = 12 | %(module Thing_One 13 | end) 14 | 15 | SCREAMING_SNAKE_CASE_CLASSES['double_screaming_snake_case_class'] = 16 | %(class Thing_One_Again 17 | end) 18 | 19 | SCREAMING_SNAKE_CASE_CLASSES['double_screaming_snake_case_module'] = 20 | %(module Thing_One_Again 21 | end) 22 | 23 | describe 'Detection of camel case methods' do 24 | before do 25 | Tailor::Logger.stub(:log) 26 | FakeFS.activate! 27 | File.open(file_name, 'w') { |f| f.write contents } 28 | critic.check_file(file_name, style.to_hash) 29 | end 30 | 31 | let(:critic) do 32 | Tailor::Critic.new 33 | end 34 | 35 | let(:contents) { SCREAMING_SNAKE_CASE_CLASSES[file_name] } 36 | 37 | let(:style) do 38 | style = Tailor::Configuration::Style.new 39 | style.trailing_newlines 0, level: :off 40 | style.allow_invalid_ruby true, level: :off 41 | 42 | style 43 | end 44 | 45 | context 'standard screaming snake case class' do 46 | let(:file_name) { 'one_screaming_snake_case_class' } 47 | specify { expect(critic.problems[file_name].size).to eq 1 } 48 | specify { expect(critic.problems[file_name].first[:type]).to eq 'allow_screaming_snake_case_classes' } 49 | specify { expect(critic.problems[file_name].first[:line]).to eq 1 } 50 | specify { expect(critic.problems[file_name].first[:column]).to eq 6 } 51 | specify { expect(critic.problems[file_name].first[:level]).to eq :error } 52 | end 53 | 54 | context 'standard screaming snake case module' do 55 | let(:file_name) { 'one_screaming_snake_case_module' } 56 | specify { expect(critic.problems[file_name].size).to eq 1 } 57 | specify { expect(critic.problems[file_name].first[:type]).to eq 'allow_screaming_snake_case_classes' } 58 | specify { expect(critic.problems[file_name].first[:line]).to eq 1 } 59 | specify { expect(critic.problems[file_name].first[:column]).to eq 7 } 60 | specify { expect(critic.problems[file_name].first[:level]).to eq :error } 61 | end 62 | 63 | context 'double screaming snake case class' do 64 | let(:file_name) { 'double_screaming_snake_case_class' } 65 | specify { expect(critic.problems[file_name].size).to eq 1 } 66 | specify { expect(critic.problems[file_name].first[:type]).to eq 'allow_screaming_snake_case_classes' } 67 | specify { expect(critic.problems[file_name].first[:line]).to eq 1 } 68 | specify { expect(critic.problems[file_name].first[:column]).to eq 6 } 69 | specify { expect(critic.problems[file_name].first[:level]).to eq :error } 70 | end 71 | 72 | context 'double screaming snake case module' do 73 | let(:file_name) { 'double_screaming_snake_case_module' } 74 | specify { expect(critic.problems[file_name].size).to eq 1 } 75 | specify { expect(critic.problems[file_name].first[:type]).to eq 'allow_screaming_snake_case_classes' } 76 | specify { expect(critic.problems[file_name].first[:line]).to eq 1 } 77 | specify { expect(critic.problems[file_name].first[:column]).to eq 7 } 78 | specify { expect(critic.problems[file_name].first[:level]).to eq :error } 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /spec/functional/naming_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require_relative '../support/naming_cases' 3 | require 'tailor/critic' 4 | require 'tailor/configuration/style' 5 | 6 | 7 | describe 'Naming problem detection' do 8 | before do 9 | allow(Tailor::Logger).to receive(:log) 10 | FakeFS.activate! 11 | end 12 | 13 | let(:critic) do 14 | Tailor::Critic.new 15 | end 16 | 17 | let(:style) do 18 | style = Tailor::Configuration::Style.new 19 | style.trailing_newlines 0, level: :off 20 | style.allow_invalid_ruby true, level: :off 21 | 22 | style 23 | end 24 | 25 | NAMING_OK.each do |file_name, contents| 26 | before do 27 | FileUtils.touch file_name 28 | File.open(file_name, 'w') { |f| f.write contents } 29 | end 30 | 31 | it 'is OK' do 32 | critic.check_file(file_name, style.to_hash) 33 | expect(critic.problems).to eq(file_name => []) 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/functional/rake_task_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'tailor/rake_task' 3 | 4 | describe Tailor::RakeTask do 5 | let(:rake) do 6 | Rake::Application.new 7 | end 8 | 9 | before do 10 | FakeFS.deactivate! 11 | Rake.application = rake 12 | end 13 | 14 | describe 'rake tailor' do 15 | context 'with problematic files' do 16 | subject do 17 | Tailor::RakeTask.new do |t| 18 | t.config_file = File.expand_path 'spec/support/rake_task_config_problems.rb' 19 | end 20 | end 21 | 22 | it 'finds problems' do 23 | subject 24 | expect { rake['tailor'].invoke }.to raise_error SystemExit 25 | end 26 | end 27 | 28 | context 'with OK files' do 29 | subject do 30 | Tailor::RakeTask.new do |t| 31 | t.config_file = File.expand_path 'spec/support/rake_task_config_no_problems.rb' 32 | end 33 | end 34 | 35 | it 'does not find problems' do 36 | subject 37 | expect { rake['tailor'].invoke }.to_not raise_error 38 | end 39 | end 40 | end 41 | 42 | context 'using a custom task name' do 43 | subject do 44 | Tailor::RakeTask.new(task_name) do |t| 45 | t.config_file = File.expand_path 'spec/support/rake_task_config_problems.rb' 46 | end 47 | end 48 | 49 | let(:task_name) { 'my_neat_task' } 50 | 51 | it 'runs the task' do 52 | subject 53 | expect { rake[task_name].invoke }.to raise_exception SystemExit 54 | end 55 | end 56 | 57 | context 'overriding tailor opts within the task' do 58 | subject do 59 | Tailor::RakeTask.new do |t| 60 | t.config_file = File.expand_path 'spec/support/rake_task_config_problems.rb' 61 | t.tailor_opts = %w(--max-line-length=1000) 62 | end 63 | end 64 | 65 | it 'uses the options from the rake task' do 66 | subject 67 | expect { rake['tailor'].invoke }.to_not raise_error 68 | end 69 | end 70 | 71 | context 'adding file sets within the task' do 72 | let(:test_dir) do 73 | File.expand_path(File.dirname(__FILE__) + '/../dir') 74 | end 75 | 76 | before do 77 | require 'fileutils' 78 | 79 | FileUtils.mkdir(test_dir) unless File.exist?(test_dir) 80 | expect(File.directory?(test_dir)).to eq true 81 | 82 | File.open(test_dir + '/test.rb', 'w') do |f| 83 | f.write <<-CONTENTS 84 | puts 'I no have end quote 85 | CONTENTS 86 | end 87 | 88 | expect(File.exist?(test_dir + '/test.rb')).to eq true 89 | end 90 | 91 | after do 92 | FileUtils.rm_rf test_dir 93 | end 94 | 95 | subject do 96 | Tailor::RakeTask.new do |t| 97 | t.config_file = File.expand_path 'spec/support/rake_task_config_problems.rb' 98 | 99 | t.file_set('dir/**/*.rb', 'dir') do |style| 100 | style.max_line_length 1, level: :error 101 | end 102 | end 103 | end 104 | 105 | it 'uses the options from the rake task' do 106 | subject 107 | expect { rake['tailor'].invoke }.to raise_error 108 | end 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /spec/functional/string_interpolation_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require_relative '../support/string_interpolation_cases' 3 | require 'tailor/critic' 4 | require 'tailor/configuration/style' 5 | 6 | describe 'String interpolation cases' do 7 | def file_name 8 | self.class.description 9 | end 10 | 11 | def contents 12 | INTERPOLATION[file_name] || begin 13 | raise "Example not found: #{file_name}" 14 | end 15 | end 16 | 17 | before do 18 | allow(Tailor::Logger).to receive(:log) 19 | FakeFS.activate! 20 | FileUtils.touch file_name 21 | File.open(file_name, 'w') { |f| f.write contents } 22 | end 23 | 24 | let(:critic) { Tailor::Critic.new } 25 | 26 | let(:style) do 27 | style = Tailor::Configuration::Style.new 28 | style.trailing_newlines 0, level: :off 29 | style.allow_invalid_ruby true, level: :off 30 | style.allow_unnecessary_double_quotes true, level: :off 31 | 32 | style 33 | end 34 | 35 | context :one_variable_interpolated_only do 36 | it 'warns when interpolation is used unnecessarily' do 37 | critic.check_file(file_name, style.to_hash) 38 | expect(critic.problems[file_name]).to eql [{ 39 | type: 'unnecessary_string_interpolation', 40 | line: 1, 41 | column: 6, 42 | message: 'Variable interpolated unnecessarily', 43 | level: :warn 44 | }] 45 | end 46 | end 47 | 48 | context :mixed_content_and_expression do 49 | it 'does not warn' do 50 | critic.check_file(file_name, style.to_hash) 51 | expect(critic.problems[file_name]).to be_empty 52 | end 53 | end 54 | 55 | context :no_string do 56 | it 'does not warn' do 57 | critic.check_file(file_name, style.to_hash) 58 | expect(critic.problems[file_name]).to be_empty 59 | end 60 | end 61 | 62 | context :two_variables do 63 | it 'does not warn' do 64 | critic.check_file(file_name, style.to_hash) 65 | expect(critic.problems[file_name]).to be_empty 66 | end 67 | end 68 | 69 | context :two_strings_with_unnecessary_interpolation do 70 | it 'warns against both strings' do 71 | critic.check_file(file_name, style.to_hash) 72 | expect(critic.problems[file_name]).to eql [ 73 | { 74 | type: 'unnecessary_string_interpolation', 75 | line: 1, 76 | column: 6, 77 | message: 'Variable interpolated unnecessarily', 78 | level: :warn 79 | }, 80 | { 81 | type: 'unnecessary_string_interpolation', 82 | line: 1, 83 | column: 17, 84 | message: 'Variable interpolated unnecessarily', 85 | level: :warn 86 | } 87 | ] 88 | end 89 | end 90 | 91 | context :multiline_string_with_unnecessary_interpolation do 92 | it 'warns against the first line' do 93 | critic.check_file(file_name, style.to_hash) 94 | expect(critic.problems[file_name]).to eql [{ 95 | type: 'unnecessary_string_interpolation', 96 | line: 1, 97 | column: 6, 98 | message: 'Variable interpolated unnecessarily', 99 | level: :warn 100 | }] 101 | end 102 | end 103 | 104 | context :multiline_word_list do 105 | it 'does not warn' do 106 | critic.check_file(file_name, style.to_hash) 107 | expect(critic.problems[file_name]).to be_empty 108 | end 109 | end 110 | 111 | context :nested_interpolation do 112 | it 'does not warn' do 113 | critic.check_file(file_name, style.to_hash) 114 | expect(critic.problems[file_name]).to be_empty 115 | end 116 | end 117 | end 118 | -------------------------------------------------------------------------------- /spec/functional/string_quoting_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require_relative '../support/string_quoting_cases' 3 | require 'tailor/critic' 4 | require 'tailor/configuration/style' 5 | 6 | describe 'String Quoting' do 7 | 8 | def file_name 9 | self.class.description 10 | end 11 | 12 | def contents 13 | QUOTING[file_name] || begin 14 | raise "Example not found: #{file_name}" 15 | end 16 | end 17 | 18 | before do 19 | allow(Tailor::Logger).to receive(:log) 20 | FakeFS.activate! 21 | FileUtils.touch file_name 22 | File.open(file_name, 'w') { |f| f.write contents } 23 | end 24 | 25 | let(:critic) { Tailor::Critic.new } 26 | 27 | let(:style) do 28 | style = Tailor::Configuration::Style.new 29 | style.trailing_newlines 0, level: :off 30 | style.allow_invalid_ruby true, level: :off 31 | style 32 | end 33 | 34 | context :single_quotes_no_interpolation do 35 | it 'does not warn' do 36 | critic.check_file(file_name, style.to_hash) 37 | expect(critic.problems[file_name]).to be_empty 38 | end 39 | end 40 | 41 | context :double_quotes_with_interpolation do 42 | it 'does not warn' do 43 | critic.check_file(file_name, style.to_hash) 44 | expect(critic.problems[file_name]).to be_empty 45 | end 46 | end 47 | 48 | context :double_quotes_no_interpolation do 49 | it 'warns that double quotes are unnecessary' do 50 | critic.check_file(file_name, style.to_hash) 51 | expect(critic.problems[file_name]).to eql [{ 52 | type: 'unnecessary_double_quotes', 53 | line: 1, 54 | column: 6, 55 | message: 'Unnecessary double quotes at column 6, expected single quotes.', 56 | level: :warn 57 | }] 58 | end 59 | end 60 | 61 | context :double_quotes_no_interpolation_twice do 62 | it 'warns that double quotes are unnecessary' do 63 | critic.check_file(file_name, style.to_hash) 64 | expect(critic.problems[file_name]).to eql [ 65 | { 66 | type: 'unnecessary_double_quotes', 67 | line: 1, 68 | column: 6, 69 | message: 'Unnecessary double quotes at column 6, expected single quotes.', 70 | level: :warn 71 | }, 72 | { 73 | type: 'unnecessary_double_quotes', 74 | line: 1, 75 | column: 14, 76 | message: 'Unnecessary double quotes at column 14, expected single quotes.', 77 | level: :warn 78 | } 79 | ] 80 | end 81 | end 82 | 83 | context :nested_quotes do 84 | it 'does not warn' do 85 | critic.check_file(file_name, style.to_hash) 86 | expect(critic.problems[file_name]).to be_empty 87 | end 88 | end 89 | 90 | context :escape_sequence do 91 | it 'does not warn when a double quoted string contains a newline' do 92 | critic.check_file(file_name, style.to_hash) 93 | expect(critic.problems[file_name]).to be_empty 94 | end 95 | end 96 | 97 | end 98 | -------------------------------------------------------------------------------- /spec/functional/vertical_spacing/class_length_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'tailor/critic' 3 | require 'tailor/configuration/style' 4 | 5 | CLASS_LENGTH = {} 6 | CLASS_LENGTH['class_too_long'] = 7 | %(class Party 8 | include Clowns 9 | include Pizza 10 | 11 | def barrel_roll 12 | puts 'DOABARRELROLL!' 13 | end 14 | end) 15 | 16 | CLASS_LENGTH['parent_class_too_long'] = 17 | %(class Party 18 | 19 | class Pizza 20 | include Cheese 21 | include Yumminess 22 | end 23 | end) 24 | 25 | describe 'Detection of class length' do 26 | before do 27 | Tailor::Logger.stub(:log) 28 | FakeFS.activate! 29 | File.open(file_name, 'w') { |f| f.write contents } 30 | critic.check_file(file_name, style.to_hash) 31 | end 32 | 33 | let(:critic) do 34 | Tailor::Critic.new 35 | end 36 | 37 | let(:contents) { CLASS_LENGTH[file_name] } 38 | 39 | let(:style) do 40 | style = Tailor::Configuration::Style.new 41 | style.trailing_newlines 0, level: :off 42 | style.allow_invalid_ruby true, level: :off 43 | style.max_code_lines_in_class 5 44 | 45 | style 46 | end 47 | 48 | context 'single class' do 49 | let(:file_name) { 'class_too_long' } 50 | specify { critic.problems[file_name].size.should be 1 } 51 | specify { critic.problems[file_name].first[:type].should == 'max_code_lines_in_class' } 52 | specify { critic.problems[file_name].first[:line].should be 1 } 53 | specify { critic.problems[file_name].first[:column].should be 0 } 54 | specify { critic.problems[file_name].first[:level].should be :error } 55 | end 56 | 57 | context 'class in a class' do 58 | let(:file_name) { 'parent_class_too_long' } 59 | specify { critic.problems[file_name].size.should be 1 } 60 | specify { critic.problems[file_name].first[:type].should == 'max_code_lines_in_class' } 61 | specify { critic.problems[file_name].first[:line].should be 1 } 62 | specify { critic.problems[file_name].first[:column].should be 0 } 63 | specify { critic.problems[file_name].first[:level].should be :error } 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /spec/functional/vertical_spacing/method_length_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'tailor/critic' 3 | require 'tailor/configuration/style' 4 | 5 | METHOD_LENGTH = {} 6 | METHOD_LENGTH['method_too_long'] = 7 | %(def thing 8 | puts 9 | puts 10 | end) 11 | 12 | METHOD_LENGTH['parent_method_too_long'] = 13 | %(def thing 14 | puts 15 | def inner_thing; print '1'; end 16 | puts 17 | end) 18 | 19 | describe 'Detection of method length' do 20 | before do 21 | allow(Tailor::Logger).to receive(:log) 22 | FakeFS.activate! 23 | File.open(file_name, 'w') { |f| f.write contents } 24 | critic.check_file(file_name, style.to_hash) 25 | end 26 | 27 | let(:critic) do 28 | Tailor::Critic.new 29 | end 30 | 31 | let(:contents) { METHOD_LENGTH[file_name] } 32 | 33 | let(:style) do 34 | style = Tailor::Configuration::Style.new 35 | style.trailing_newlines 0, level: :off 36 | style.allow_invalid_ruby true, level: :off 37 | style.max_code_lines_in_method 3 38 | 39 | style 40 | end 41 | 42 | context 'single class too long' do 43 | let(:file_name) { 'method_too_long' } 44 | specify { expect(critic.problems[file_name].size).to eq 1 } 45 | specify { expect(critic.problems[file_name].first[:type]).to eq 'max_code_lines_in_method' } 46 | specify { expect(critic.problems[file_name].first[:line]).to eq 1 } 47 | specify { expect(critic.problems[file_name].first[:column]).to eq 0 } 48 | specify { expect(critic.problems[file_name].first[:level]).to eq :error } 49 | end 50 | 51 | context 'method in a method' do 52 | let(:file_name) { 'method_too_long' } 53 | specify { expect(critic.problems[file_name].size).to eq 1 } 54 | specify { expect(critic.problems[file_name].first[:type]).to eq 'max_code_lines_in_method' } 55 | specify { expect(critic.problems[file_name].first[:line]).to eq 1 } 56 | specify { expect(critic.problems[file_name].first[:column]).to eq 0 } 57 | specify { expect(critic.problems[file_name].first[:level]).to eq :error } 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /spec/functional/vertical_spacing_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require_relative '../support/vertical_spacing_cases' 3 | require 'tailor/critic' 4 | require 'tailor/configuration/style' 5 | 6 | 7 | describe 'Vertical Space problem detection' do 8 | before do 9 | allow(Tailor::Logger).to receive(:log) 10 | FakeFS.activate! 11 | end 12 | 13 | let(:critic) do 14 | Tailor::Critic.new 15 | end 16 | 17 | let(:style) do 18 | style = Tailor::Configuration::Style.new 19 | style.trailing_newlines 0, level: :off 20 | style.allow_invalid_ruby true, level: :off 21 | 22 | style 23 | end 24 | 25 | V_SPACING_OK.each do |file_name, contents| 26 | before do 27 | FileUtils.touch file_name 28 | File.open(file_name, 'w') { |f| f.write contents } 29 | end 30 | 31 | it 'is OK' do 32 | critic.check_file(file_name, style.to_hash) 33 | expect(critic.problems).to eq(file_name => []) 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'fakefs/spec_helpers' 2 | require 'rspec/its' 3 | require 'simplecov' 4 | 5 | SimpleCov.start 6 | 7 | 8 | RSpec.configure do |config| 9 | config.include FakeFS::SpecHelpers 10 | 11 | # Run specs in random order to surface order dependencies. If you find an 12 | # order dependency and want to debug it, you can fix the order by providing 13 | # the seed, which is printed after each run. 14 | # --seed 1234 15 | config.order = 'random' 16 | end 17 | -------------------------------------------------------------------------------- /spec/support/argument_alignment_cases.rb: -------------------------------------------------------------------------------- 1 | ARG_INDENT = {} 2 | 3 | ARG_INDENT['def_no_arguments'] = 4 | %(def foo 5 | true 6 | end) 7 | 8 | ARG_INDENT['def_arguments_fit_on_one_line'] = 9 | %(def foo(foo, bar, baz) 10 | true 11 | end) 12 | 13 | ARG_INDENT['def_arguments_aligned'] = 14 | %(def something(waka, baka, bing, 15 | bla, goop, foop) 16 | stuff 17 | end 18 | ) 19 | 20 | ARG_INDENT['def_arguments_indented'] = 21 | %(def something(waka, baka, bing, 22 | bla, goop, foop) 23 | stuff 24 | end 25 | ) 26 | 27 | ARG_INDENT['call_no_arguments'] = 28 | %(bla = method()) 29 | 30 | ARG_INDENT['call_arguments_fit_on_one_line'] = 31 | %(bla = method(foo, bar, baz, bing, ding)) 32 | 33 | ARG_INDENT['call_arguments_aligned'] = 34 | %(bla = Something::SomethingElse::SomeClass.method(foo, bar, baz, 35 | bing, ding) 36 | ) 37 | 38 | ARG_INDENT['call_arguments_aligned_args_are_integer_literals'] = 39 | %(bla = Something::SomethingElse::SomeClass.method(1, 2, 3, 40 | 4, 5) 41 | ) 42 | 43 | ARG_INDENT['call_arguments_aligned_args_are_string_literals'] = 44 | %(bla = Something::SomethingElse::SomeClass.method('foo', 'bar', 'baz', 45 | 'bing', 'ding') 46 | ) 47 | 48 | ARG_INDENT['call_arguments_aligned_args_have_parens'] = 49 | %(bla = Something::SomethingElse::SomeClass.method(foo, bar(), baz, 50 | bing, ding) 51 | ) 52 | 53 | ARG_INDENT['call_arguments_aligned_no_parens'] = 54 | %(bla = Something::SomethingElse::SomeClass.method foo, bar, baz, 55 | bing, ding 56 | ) 57 | 58 | ARG_INDENT['call_arguments_aligned_multiple_lines'] = 59 | %(bla = Something::SomethingElse::SomeClass.method(foo, bar, baz, 60 | bing, ding, 61 | ginb, gind) 62 | ) 63 | 64 | ARG_INDENT['call_arguments_indented'] = 65 | %(bla = Something::SomethingElse::SomeClass.method(foo, bar, baz, 66 | bing, ding) 67 | ) 68 | 69 | ARG_INDENT['call_arguments_indented_separate_line'] = 70 | %(bla = Something::SomethingElse::SomeClass.method( 71 | foo, bar, baz, 72 | bing, ding 73 | )) 74 | 75 | ARG_INDENT['call_arguments_on_next_line'] = 76 | %(some_long_method_that_goes_out_to_the_end_of_the_line( 77 | foo, bar) 78 | ) 79 | 80 | ARG_INDENT['call_arguments_on_next_line_nested'] = 81 | %(if some_long_method_that_goes_out_to_the_end_of_the_line( 82 | foo, bar) 83 | my_nested_expression 84 | end) 85 | 86 | ARG_INDENT['call_arguments_on_next_line_multiple'] = 87 | %(some_long_method_that_goes_out_to_the_end_of_the_line( 88 | foo, bar) 89 | 90 | if diff_long_method_that_goes_out_to_the_end_of_the_line( 91 | foo, bar) 92 | my_nested_expression 93 | end) 94 | -------------------------------------------------------------------------------- /spec/support/conditional_parentheses_cases.rb: -------------------------------------------------------------------------------- 1 | CONDITIONAL_PARENTHESES = {} 2 | 3 | CONDITIONAL_PARENTHESES['no_parentheses'] = 4 | %(if foo 5 | end) 6 | 7 | CONDITIONAL_PARENTHESES['with_parentheses'] = 8 | %(if (foo) 9 | end) 10 | 11 | CONDITIONAL_PARENTHESES['with_parentheses_no_space'] = 12 | %(if(foo) 13 | end) 14 | 15 | CONDITIONAL_PARENTHESES['method_call'] = 16 | %(if foo(bar) 17 | end) 18 | 19 | CONDITIONAL_PARENTHESES['indented_method_call'] = 20 | %(foo do 21 | if foo(bar) 22 | end 23 | end) 24 | 25 | CONDITIONAL_PARENTHESES['method_call_on_parens'] = 26 | %(unless (foo & bar).sort 27 | end 28 | ) 29 | 30 | CONDITIONAL_PARENTHESES['double_parens'] = 31 | %(if ((bar)) 32 | end) 33 | 34 | CONDITIONAL_PARENTHESES['unless_no_parentheses'] = 35 | %(unless bar 36 | end) 37 | 38 | CONDITIONAL_PARENTHESES['unless_with_parentheses'] = 39 | %(unless (bar) 40 | end) 41 | 42 | CONDITIONAL_PARENTHESES['case_no_parentheses'] = 43 | %(case bar 44 | when 1 then 'a' 45 | when 2 then 'b' 46 | end) 47 | 48 | CONDITIONAL_PARENTHESES['case_with_parentheses'] = 49 | %(case (bar) 50 | when 1 then 'a' 51 | when 2 then 'b' 52 | end) 53 | 54 | CONDITIONAL_PARENTHESES['while_no_parentheses'] = 55 | %(while bar 56 | end) 57 | 58 | CONDITIONAL_PARENTHESES['while_with_parentheses'] = 59 | %(while (bar) 60 | end) 61 | -------------------------------------------------------------------------------- /spec/support/conditional_spacing_cases.rb: -------------------------------------------------------------------------------- 1 | CONDITIONAL_SPACING = {} 2 | 3 | CONDITIONAL_SPACING['no_space_after_if'] = 4 | %q{if(foo) 5 | end} 6 | 7 | CONDITIONAL_SPACING['space_after_if'] = 8 | %q{if (foo) 9 | end} 10 | 11 | CONDITIONAL_SPACING['no_parens'] = 12 | %q{if foo 13 | end} 14 | 15 | CONDITIONAL_SPACING['nested_parens'] = 16 | %q{if(foo(bar)) 17 | end} 18 | 19 | CONDITIONAL_SPACING['no_space_after_unless'] = 20 | %q{unless(foo) 21 | end} 22 | 23 | CONDITIONAL_SPACING['space_after_unless'] = 24 | %q{unless (foo) 25 | end} 26 | 27 | CONDITIONAL_SPACING['no_space_after_case'] = 28 | %q{puts case(true) 29 | when true then 'a' 30 | when false then 'b' 31 | end} 32 | 33 | CONDITIONAL_SPACING['space_after_case'] = 34 | %q{puts case (true) 35 | when true then 'a' 36 | when false then 'b' 37 | end} 38 | -------------------------------------------------------------------------------- /spec/support/horizontal_spacing_cases.rb: -------------------------------------------------------------------------------- 1 | H_SPACING_OK = {} 2 | 3 | H_SPACING_OK['short_line_no_newline'] = '#' * 79 4 | H_SPACING_OK['short_line_newline_at_81'] = 5 | %('#{'#' * 78}' 6 | ) 7 | 8 | =begin 9 | H_SPACING_OK['line_split_by_backslash'] = 10 | %Q{execute 'myscript' do 11 | command \\ 12 | '/some/really/long/path/that/would/be/over/eight/chars.sh' 13 | only_if { something } 14 | end} 15 | =end 16 | 17 | #------------------------------------------------------------------------------- 18 | # Comma spacing 19 | #------------------------------------------------------------------------------- 20 | H_SPACING_OK['space_after_comma_in_array'] = %([1, 2]) 21 | 22 | H_SPACING_OK['trailing_comma'] = %(def thing(one, two, 23 | three) 24 | end) 25 | 26 | H_SPACING_OK['trailing_comma_with_trailing_comment'] = 27 | %(def thing(one, two, # Comment! 28 | three) 29 | end) 30 | 31 | H_SPACING_OK['no_before_comma_in_array'] = %([1, 2]) 32 | H_SPACING_OK['line_ends_with_backslash'] = 33 | %({ :thing => a_thing,\\ 34 | :thing2 => another_thing }) 35 | 36 | #------------------------------------------------------------------------------- 37 | # Braces 38 | #------------------------------------------------------------------------------- 39 | H_SPACING_OK['empty_hash'] = %({}) 40 | H_SPACING_OK['single_line_hash'] = %({ :one => 'one' }) 41 | H_SPACING_OK['single_line_hash_lonely_braces'] = %({ 42 | :one => 'one' 43 | }) 44 | 45 | H_SPACING_OK['hash_as_param_in_parens'] = 46 | %(add_headers({ content_length: new_body.length })) 47 | 48 | H_SPACING_OK['two_line_hash'] = %({ :one => 49 | 'one' }) 50 | 51 | H_SPACING_OK['two_line_hash_trailing_comment'] = %({ :one => # comment 52 | 'one' }) 53 | 54 | H_SPACING_OK['three_line_hash'] = %({ :one => 55 | 'one', :two => 56 | 'two' }) 57 | 58 | H_SPACING_OK['single_line_block'] = %(1..10.times { |n| puts number }) 59 | H_SPACING_OK['multi_line_braces_block'] = %(1..10.times { |n| 60 | puts number }) 61 | 62 | H_SPACING_OK['multi_line_qword_using_braces'] = %(%w{ 63 | foo 64 | bar 65 | baz 66 | }.each do |whatevs| 67 | bla 68 | end) 69 | 70 | H_SPACING_OK['empty_hash_in_multi_line_statement'] = 71 | %(if true 72 | {} 73 | end) 74 | 75 | H_SPACING_OK['multi_line_hash_in_multi_line_statement'] = 76 | %(if true 77 | options = { 78 | one: 1 79 | } 80 | end) 81 | 82 | H_SPACING_OK['single_line_string_interp'] = %(`\#{IFCONFIG} | grep \#{ip}`) 83 | H_SPACING_OK['single_line_block_in_string_interp'] = 84 | %("I did this \#{1..10.times { |n| n }} times.") 85 | 86 | H_SPACING_OK['empty_hash_in_string_in_block'] = 87 | %([1].map { |n| { :first => "\#{n}-\#{{}}" } }) 88 | 89 | H_SPACING_OK['string_interp_with_colonop'] = 90 | %("\#{::Rails.root + 'file'}") 91 | 92 | 93 | 94 | #------------------------------------------------------------------------------- 95 | # Brackets 96 | #------------------------------------------------------------------------------- 97 | H_SPACING_OK['empty_array'] = %([]) 98 | H_SPACING_OK['simple_array'] = %([1, 2, 3]) 99 | H_SPACING_OK['two_d_array'] = %([[1, 2, 3], ['a', 'b', 'c']]) 100 | H_SPACING_OK['hash_key_reference'] = %(thing[:one]) 101 | H_SPACING_OK['array_of_symbols'] = 102 | %(transition [:active, :reactivated] => :opened) 103 | H_SPACING_OK['array_of_hashes'] = 104 | %([ { :one => [[1, 2, 3], ['a', 'b', 'c']] }, 105 | { :two => [[4, 5, 6], ['d', 'e', 'f']] }]) 106 | 107 | H_SPACING_OK['simple_array_lonely_brackets'] = 108 | %([ 109 | 1, 2, 110 | 3 111 | ]) 112 | 113 | H_SPACING_OK['simple_nested_array_lonely_brackets'] = 114 | %(def thing 115 | [ 116 | 1, 2, 117 | 3 118 | ] 119 | end) 120 | 121 | H_SPACING_OK['empty_array_in_multi_line_statement'] = 122 | %(if true 123 | [] 124 | end) 125 | 126 | #------------------------------------------------------------------------------- 127 | # Parens 128 | #------------------------------------------------------------------------------- 129 | H_SPACING_OK['empty_parens'] = %(def thing(); end) 130 | H_SPACING_OK['simple_method_call'] = %(thing(one, two)) 131 | H_SPACING_OK['multi_line_method_call'] = %(thing(one, 132 | two)) 133 | H_SPACING_OK['multi_line_method_call_lonely_parens'] = %(thing( 134 | one, two 135 | )) 136 | -------------------------------------------------------------------------------- /spec/support/line_indentation_cases.rb: -------------------------------------------------------------------------------- 1 | LINE_INDENT = {} 2 | 3 | LINE_INDENT['hash_spans_lines'] = 4 | %(db_connection = { :host => "localhost", :username => 'root', 5 | :password => node['db']['password'] }) 6 | 7 | LINE_INDENT['if_else'] = 8 | %(case "foo" 9 | when "foo" 10 | if node["foo"]["version"].to_f >= 5.5 11 | default['foo']['service_name'] = "foo" 12 | default['foo']['pid_file'] = "/var/run/foo/foo.pid" 13 | else 14 | default['foo']['service_name'] = "food" 15 | default['foo']['pid_file'] = "/var/run/food/food.pid" 16 | end 17 | end) 18 | 19 | LINE_INDENT['line_continues_at_same_indentation'] = 20 | %(if someconditional_that == is_really_long.function().stuff() or 21 | another_condition == some_thing 22 | puts "boop" 23 | end) 24 | 25 | LINE_INDENT['line_continues_further_indented'] = 26 | %(if someconditional_that == is_really_long.function().stuff() or 27 | another_condition == some_thing 28 | puts "boop" 29 | end) 30 | 31 | LINE_INDENT['line_continues_without_nested_statements'] = 32 | %(attribute "foo/password", 33 | :display_name => "Password", 34 | :description => "Randomly generated password", 35 | :default => "randomly generated") 36 | 37 | LINE_INDENT['minitest_test_cases'] = 38 | %q(describe "foo" do 39 | it 'includes the disk_free_limit configuration setting' do 40 | file("#{node['foo']['config_root']}/foo.config"). 41 | must_match /\{disk_free_limit, \{mem_relative, #{node['foo']['df']}/ 42 | end 43 | it 'includes the vm_memory_high_watermark configuration setting' do 44 | file("#{node['foo']['config_root']}/foo.config"). 45 | must_match /\{vm_memory_high_watermark, #{node['foo']['vm']}/ 46 | end 47 | end) 48 | 49 | LINE_INDENT['nested_blocks'] = 50 | %(node['foo']['client']['packages'].each do |foo_pack| 51 | package foo_pkg do 52 | action :install 53 | end 54 | end) 55 | 56 | LINE_INDENT['one_assignment_per_line'] = 57 | %(default['foo']['bar']['apt_key_id'] = 'BD2EFD2A' 58 | default['foo']['bar']['apt_uri'] = "http://repo.example.com/apt" 59 | default['foo']['bar']['apt_keyserver'] = "keys.example.net") 60 | 61 | LINE_INDENT['parameters_continuation_indent_across_lines'] = 62 | %(def something(waka, baka, bing, 63 | bla, goop, foop) 64 | stuff 65 | end) 66 | 67 | LINE_INDENT['parameters_no_continuation_indent_across_lines'] = 68 | %(def something(waka, baka, bing, 69 | bla, goop, foop) 70 | stuff 71 | end) 72 | -------------------------------------------------------------------------------- /spec/support/naming_cases.rb: -------------------------------------------------------------------------------- 1 | NAMING_OK = {} 2 | 3 | NAMING_OK['single_word_method'] = 4 | %(def thing 5 | end) 6 | 7 | NAMING_OK['two_word_method'] = 8 | %(def thing_one 9 | end) 10 | 11 | #------------------------------------------------------------------------------- 12 | NAMING_OK['single_word_class'] = 13 | %(class Thing 14 | end) 15 | 16 | NAMING_OK['single_word_module'] = 17 | %(module Thing 18 | end) 19 | 20 | NAMING_OK['two_word_class'] = 21 | %(class ThingOne 22 | end) 23 | 24 | NAMING_OK['two_word_module'] = 25 | %(module ThingOne 26 | end) 27 | -------------------------------------------------------------------------------- /spec/support/rake_task_config_no_problems.rb: -------------------------------------------------------------------------------- 1 | Tailor.config do |config| 2 | config.file_set 'spec/support/bad_indentation_cases.rb', :test do |style| 3 | style.max_line_length 1000, level: :error 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/support/rake_task_config_problems.rb: -------------------------------------------------------------------------------- 1 | Tailor.config do |config| 2 | config.file_set 'spec/support/bad_indentation_cases.rb', :test do |style| 3 | style.max_line_length 1, level: :error 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/support/string_interpolation_cases.rb: -------------------------------------------------------------------------------- 1 | INTERPOLATION = {} 2 | 3 | INTERPOLATION['one_variable_interpolated_only'] = 4 | %q(puts "#{bing}" 5 | ) 6 | 7 | INTERPOLATION['mixed_content_and_expression'] = 8 | %q(puts "hello: #{bing}" 9 | ) 10 | 11 | INTERPOLATION['no_string'] = 12 | %q(puts bing 13 | ) 14 | 15 | INTERPOLATION['two_variables'] = 16 | %q(puts "#{bing}#{bar}" 17 | ) 18 | 19 | INTERPOLATION['two_strings_with_unnecessary_interpolation'] = 20 | %q(puts "#{foo}" + "#{bar}" 21 | ) 22 | 23 | INTERPOLATION['multiline_string_with_unnecessary_interpolation'] = 24 | %q(puts "#{foo + 25 | bar - 26 | baz}" 27 | ) 28 | 29 | INTERPOLATION['multiline_word_list'] = 30 | %q(%w{ 31 | foo 32 | bar 33 | baz 34 | }) 35 | 36 | INTERPOLATION['nested_interpolation'] = 37 | %q(def friendly_time(time) 38 | if hours < 24 39 | "#{(hours > 0) ? "#{hours} hour" : '' }#{(hours > 1) ? 's' : ''}" + 40 | " #{(mins > 0) ? "#{mins} minute" : '' }#{(mins > 1) ? 's' : ''}" + 41 | " #{seconds} second#{(seconds > 1) ? 's' : ''} ago" 42 | else 43 | time.to_s 44 | end 45 | end) 46 | -------------------------------------------------------------------------------- /spec/support/string_quoting_cases.rb: -------------------------------------------------------------------------------- 1 | QUOTING = {} 2 | 3 | QUOTING['single_quotes_no_interpolation'] = 4 | %q(foo = 'bar' 5 | ) 6 | 7 | QUOTING['double_quotes_with_interpolation'] = 8 | %q(foo = "bar#{baz}" 9 | ) 10 | 11 | QUOTING['double_quotes_no_interpolation'] = 12 | %q(foo = "bar" 13 | ) 14 | 15 | QUOTING['double_quotes_no_interpolation_twice'] = 16 | %q(foo = "bar" + "baz" 17 | ) 18 | 19 | QUOTING['escape_sequence'] = 20 | %q(foo = "bar\n" 21 | ) 22 | 23 | QUOTING['nested_quotes'] = 24 | %q(foo = "foo#{bar('baz')}" 25 | ) 26 | -------------------------------------------------------------------------------- /spec/support/vertical_spacing_cases.rb: -------------------------------------------------------------------------------- 1 | V_SPACING_OK = {} 2 | 3 | #------------------------------------------------------------------------------- 4 | # Class length 5 | #------------------------------------------------------------------------------- 6 | V_SPACING_OK['class_five_code_lines'] = 7 | %(class Party 8 | include Clowns 9 | 10 | def barrel_roll 11 | end 12 | end) 13 | 14 | V_SPACING_OK['embedded_class_five_code_lines'] = 15 | %(class Party 16 | class Pizza 17 | include Cheese 18 | end 19 | end) 20 | 21 | #------------------------------------------------------------------------------- 22 | # Method length 23 | #------------------------------------------------------------------------------- 24 | V_SPACING_OK['method_3_code_lines'] = 25 | %(def thing 26 | 27 | 28 | puts 'hi' 29 | end) 30 | 31 | V_SPACING_OK['embedded_method_3_code_lines'] = 32 | %(def outter_thing 33 | def thing; puts 'hi'; end 34 | 35 | 36 | end) 37 | -------------------------------------------------------------------------------- /spec/unit/tailor/cli/options_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'tailor/cli/options' 3 | 4 | # Module to use in tests. 5 | module OptionHelpers 6 | def cli_option(name) 7 | "--#{name.to_s.gsub('_', '-')}" 8 | end 9 | 10 | def option_value(name, value) 11 | options = Tailor::CLI::Options.parse!([cli_option(name), value]) 12 | options.style[name] 13 | end 14 | end 15 | 16 | describe Tailor::CLI::Options do 17 | include OptionHelpers 18 | 19 | describe '#parse!' do 20 | [ 21 | :indentation_spaces, 22 | :max_code_lines_in_class, 23 | :max_code_lines_in_method, 24 | :max_line_length, 25 | :spaces_after_comma, 26 | :spaces_after_lbrace, 27 | :spaces_after_lbracket, 28 | :spaces_after_lparen, 29 | :spaces_before_comma, 30 | :spaces_before_lbrace, 31 | :spaces_before_rbrace, 32 | :spaces_before_rbracket, 33 | :spaces_before_rparen, 34 | :spaces_in_empty_braces, 35 | :trailing_newlines 36 | ].each do |o| 37 | it 'parses a valid numeric argument correct' do 38 | expect(option_value(o, '1')).to eq 1 39 | end 40 | 41 | it 'marks the ruler as off if the option is specified as "off"' do 42 | expect(option_value(o, 'off')).to eq :off 43 | end 44 | 45 | it 'marks a ruler as off if the option is specified as "false"' do 46 | expect(option_value(o, 'false')).to eq :off 47 | end 48 | 49 | it 'raises if the argument is otherwise not an integer' do 50 | expect do 51 | option_value(o, 'not-an-integer') 52 | end.to raise_error(OptionParser::InvalidArgument) 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/unit/tailor/cli_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'tailor/cli' 3 | 4 | describe Tailor::CLI do 5 | let(:args) { [] } 6 | let(:options) { double 'Options', show_config: false } 7 | 8 | let(:config) do 9 | double 'Tailor::Configuration', file_sets: nil, formatters: nil, load!: nil 10 | end 11 | 12 | before do 13 | allow(Tailor::Configuration).to receive(:new).and_return config 14 | allow(Tailor::Critic).to receive(:new) 15 | allow(Tailor::Reporter).to receive(:new) 16 | end 17 | 18 | subject { Tailor::CLI.new(args) } 19 | 20 | describe '::run' do 21 | it "creates an instance of Tailor::CLI and calls that object's #execute!" do 22 | cli = double 'Tailor::CLI' 23 | expect(cli).to receive(:execute!) 24 | expect(Tailor::CLI).to receive(:new).and_return cli 25 | Tailor::CLI.run([]) 26 | end 27 | end 28 | 29 | describe '#initialize' do 30 | let(:args) { %w(last) } 31 | 32 | it 'uses Options to parse the args' do 33 | allow(Tailor::Configuration).to receive(:new).and_return config 34 | allow(Tailor::Critic).to receive(:new) 35 | allow(Tailor::Reporter).to receive(:new) 36 | expect(Tailor::CLI::Options).to receive(:parse!). 37 | with(args).and_return options 38 | 39 | Tailor::CLI.new(args) 40 | end 41 | 42 | it 'creates a new Configuration from the file/dir and options' do 43 | allow(Tailor::CLI::Options).to receive(:parse!).and_return(options) 44 | expect(Tailor::Configuration).to receive(:new).with(args, options). 45 | and_return config 46 | Tailor::Critic.stub(:new) 47 | 48 | Tailor::CLI.new(args) 49 | end 50 | 51 | context 'options.show_config is true' do 52 | pending 53 | end 54 | 55 | context 'options.show_config is false' do 56 | pending 57 | end 58 | end 59 | 60 | describe '#execute!' do 61 | let(:reporter) { double 'Tailor::Reporter' } 62 | let(:critic) { double 'Tailor::Critic', problem_count: 0 } 63 | 64 | before do 65 | allow(Tailor::Critic).to receive(:new).and_return(critic) 66 | allow(Tailor::Reporter).to receive(:new).and_return(reporter) 67 | subject.instance_variable_set(:@critic, critic) 68 | subject.instance_variable_set(:@reporter, reporter) 69 | end 70 | 71 | it 'calls @critic.critique and yields file problems and the label' do 72 | problems_for_file = {} 73 | label = :test 74 | expect(config).to receive(:output_file) 75 | allow(critic).to receive(:problem_count).and_return 1 76 | allow(critic).to receive(:problems) 77 | allow(critic).to receive(:critique).and_yield(problems_for_file, label) 78 | allow(reporter).to receive(:summary_report) 79 | expect(reporter).to receive(:file_report).with(problems_for_file, label) 80 | 81 | subject.execute! 82 | end 83 | end 84 | 85 | describe '#result' do 86 | let(:critic) { double 'Tailor::Critic', problem_count: 0 } 87 | 88 | before do 89 | allow(Tailor::Critic).to receive(:new).and_return(critic) 90 | subject.instance_variable_set(:@critic, critic) 91 | end 92 | 93 | it 'calls @critic.critique and return @critique.problems hash' do 94 | problems = {} 95 | expect(critic).to receive(:critique) 96 | expect(critic).to receive(:problems).and_return(problems) 97 | 98 | expect(subject.result).to eq problems 99 | end 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /spec/unit/tailor/composite_observable_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'tailor/composite_observable' 3 | 4 | # Class to use for tests. 5 | class Tester 6 | include Tailor::CompositeObservable 7 | end 8 | 9 | describe Tailor::CompositeObservable do 10 | subject { Tester.new } 11 | 12 | describe '.define_observer' do 13 | context "observer = 'pants'" do 14 | before { Tailor::CompositeObservable.define_observer 'pants' } 15 | 16 | context 'observer responds to #pants_update' do 17 | it "defines an instance method 'add_pants_observer' that takes 1 arg" do 18 | observer = double 'Observer', respond_to?: true 19 | subject.add_pants_observer(observer) 20 | end 21 | end 22 | 23 | context 'observer does not respond to #pants_update' do 24 | it "defines an instance method 'add_pants_observer' that takes 1 arg" do 25 | observer = double 'Observer', respond_to?: false 26 | expect { subject.add_pants_observer(observer) }. 27 | to raise_error NoMethodError 28 | end 29 | end 30 | 31 | it 'defines an instance method #notify_pants_observers' do 32 | expect { subject.notify_pants_observers }. 33 | to_not raise_error 34 | end 35 | 36 | it 'defines an instance method #pants_changed' do 37 | expect { subject.pants_changed }.to_not raise_error 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /spec/unit/tailor/configuration/file_set_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'tailor/configuration/file_set' 3 | 4 | describe Tailor::Configuration::FileSet do 5 | describe '#build_file_list' do 6 | context 'param is a file name' do 7 | context 'the file exists' do 8 | before do 9 | FileUtils.touch './test.rb' 10 | end 11 | 12 | it 'builds an Array with that file\'s expanded path' do 13 | new_list = subject.instance_eval { build_file_list('./test.rb') } 14 | expect(new_list).to be_an Array 15 | expect(new_list.first).to match(%r{/test.rb$}) 16 | end 17 | end 18 | 19 | context 'the file does not exist' do 20 | it 'returns an empty Array' do 21 | expect(subject.instance_eval { build_file_list('test.rb') }).to eq [] 22 | end 23 | end 24 | end 25 | 26 | context 'when param is an Array' do 27 | before do 28 | FileUtils.touch './test.rb' 29 | end 30 | 31 | it 'returns the Array with expanded file paths' do 32 | expect(subject.instance_eval { build_file_list(['test.rb']) }.first). 33 | to match(%r{/test.rb$}) 34 | end 35 | end 36 | 37 | context 'when param is a directory' do 38 | before do 39 | FileUtils.mkdir 'test' 40 | FileUtils.touch 'test/test.rb' 41 | end 42 | 43 | it 'returns the expanded file paths in that directory' do 44 | list = subject.instance_eval { build_file_list('test') } 45 | expect(list.size).to eq 1 46 | expect(list.first).to match(/.+\/test.rb/) 47 | end 48 | end 49 | end 50 | 51 | describe '#update_file_list' do 52 | before do 53 | subject.instance_variable_set(:@file_list, ['first.rb']) 54 | FileUtils.touch 'test2.rb' 55 | end 56 | 57 | it 'builds the file list and concats that to @file_list' do 58 | subject.update_file_list('test2.rb') 59 | expect(subject.instance_variable_get(:@file_list).size).to eq 2 60 | expect(subject.instance_variable_get(:@file_list).last). 61 | to match(%r{/test2.rb}) 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /spec/unit/tailor/critic_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'tailor/critic' 3 | 4 | 5 | describe Tailor::Critic do 6 | before { Tailor::Logger.stub(:log) } 7 | 8 | describe '#check_file' do 9 | let(:lexer) { double 'Lexer' } 10 | let(:ruler) { double 'Ruler' } 11 | let(:style) { double 'Style', each: nil } 12 | let(:file_name) { 'this_file.rb' } 13 | 14 | before do 15 | subject.stub(:init_rulers) 16 | end 17 | 18 | it 'lexes the file' do 19 | lexer.should_receive(:lex) 20 | lexer.stub(:check_added_newline) 21 | Tailor::Lexer.should_receive(:new).with(file_name).and_return lexer 22 | subject.stub_chain(:problems, :[]=) 23 | subject.stub_chain(:problems, :[]) 24 | 25 | subject.check_file(file_name, style) 26 | end 27 | 28 | it 'adds problems for the file to the main list of problems' do 29 | lexer.stub(:lex) 30 | lexer.stub(:check_added_newline) 31 | Tailor::Lexer.stub(:new).and_return lexer 32 | subject.problems.should_receive(:[]=).with(file_name, []) 33 | 34 | subject.check_file(file_name, style) 35 | end 36 | end 37 | 38 | describe '#problems' do 39 | specify { subject.problems.should be_a Hash } 40 | specify { subject.problems.should be_empty } 41 | end 42 | 43 | describe '#problem_count' do 44 | context '#problems is empty' do 45 | it 'returns 0' do 46 | subject.instance_variable_set(:@problems, {}) 47 | subject.problem_count.should == 0 48 | end 49 | end 50 | 51 | context '#problems contains valid values' do 52 | it 'adds the number of each problem together' do 53 | probs = { 54 | one: { type: :indentation, line: 1, message: '' }, 55 | two: { type: :indentation, line: 2, message: '' }, 56 | thre: { type: :indentation, line: 27, message: '' } 57 | } 58 | subject.instance_variable_set(:@problems, probs) 59 | subject.problem_count.should == 3 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /spec/unit/tailor/formatter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'tailor/formatter' 3 | 4 | describe Tailor::Formatter do 5 | describe '#problems_at_level' do 6 | let(:problems) do 7 | msg = 'File contains invalid Ruby; ' 8 | msg << 'run `ruby -c [your_file.rb]` for more details.' 9 | 10 | { 11 | 'some_file.rb' => [ 12 | { 13 | type: 'allow_invalid_ruby', 14 | line: 0, 15 | column: 0, 16 | message: msg, 17 | level: :warn 18 | } 19 | ] 20 | } 21 | end 22 | 23 | context 'problems are empty' do 24 | it 'returns an empty Array' do 25 | expect(subject.problems_at_level({}, :error)).to eq [] 26 | end 27 | end 28 | 29 | context 'the level asked for exists in the problems' do 30 | it 'returns the problem' do 31 | msg = 'File contains invalid Ruby; ' 32 | msg << 'run `ruby -c [your_file.rb]` for more details.' 33 | 34 | expect(subject.problems_at_level(problems, :warn)).to eq [ 35 | { 36 | type: 'allow_invalid_ruby', 37 | line: 0, 38 | column: 0, 39 | message: msg, 40 | level: :warn 41 | } 42 | ] 43 | end 44 | end 45 | 46 | context 'the level asked for does not exist in the problems' do 47 | it 'returns an empty Array' do 48 | expect(subject.problems_at_level(problems, :error)).to eq [] 49 | end 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /spec/unit/tailor/formatters/yaml_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'tailor/formatters/yaml' 3 | require 'yaml' 4 | 5 | describe Tailor::Formatters::Yaml do 6 | describe '#summary_report' do 7 | context 'no files have problems' do 8 | let(:problems) do 9 | { 10 | '/path_to/file1.rb' => [], 11 | '/path_to/file2.rb' => [] 12 | } 13 | end 14 | 15 | it 'returns YAML with no body' do 16 | result = subject.summary_report(problems) 17 | hash = YAML.load(result) 18 | expect(hash).to eq({}) 19 | end 20 | end 21 | 22 | context 'one file has one problem' do 23 | let(:problems) do 24 | { 25 | '/path_to/file1.rb' => [{ 26 | type: 'type1', line: 23, column: 1, 27 | message: 'Some message', level: :error 28 | }], 29 | '/path_to/file2.rb' => [] 30 | } 31 | end 32 | 33 | it 'returns YAML that contains the problem file and its problem' do 34 | result = subject.summary_report(problems) 35 | hash = YAML.load(result) 36 | expect(hash.keys.size).to eq 1 37 | expect(hash.keys.first).to eq '/path_to/file1.rb' 38 | expect(hash).to_not include '/path_to/file2.rb' 39 | expect(hash['/path_to/file1.rb'].first[:type]).to eq 'type1' 40 | end 41 | end 42 | 43 | context 'one file has one problem, another has two problems' do 44 | let(:problems) do 45 | { 46 | '/path_to/file1.rb' => [{ 47 | type: 'type1', line: 23, column: 1, 48 | message: 'Some message', level: :error 49 | }], 50 | '/path_to/file2.rb' => [], 51 | '/path_to/file3.rb' => [{ 52 | type: 'type2', line: 45, column: 1, 53 | message: 'file 3', level: :off 54 | }, { 55 | type: 'type3', line: 45, column: 2, 56 | message: 'file 3', level: :off 57 | }] 58 | } 59 | end 60 | 61 | it 'returns YAML that contains the problem files and their problems' do 62 | result = subject.summary_report(problems) 63 | hash = YAML.load(result) 64 | expect(hash.keys.size).to eq 2 65 | expect(hash.keys.first).to eq '/path_to/file1.rb' 66 | expect(hash.keys.last).to eq '/path_to/file3.rb' 67 | expect(hash).to_not include '/path_to/file2.rb' 68 | expect(hash['/path_to/file1.rb'].first[:type]).to eq 'type1' 69 | expect(hash['/path_to/file3.rb'].first[:type]).to eq 'type2' 70 | expect(hash['/path_to/file3.rb'].last[:type]).to eq 'type3' 71 | end 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /spec/unit/tailor/lexer/token_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'tailor/lexer/token' 3 | 4 | describe Tailor::Lexer::Token do 5 | before do 6 | allow(Tailor::Logger).to receive(:log) 7 | end 8 | 9 | describe '#modifier_keyword?' do 10 | subject do 11 | options = { full_line_of_text: full_line_of_text } 12 | Tailor::Lexer::Token.new('if', options) 13 | end 14 | 15 | context 'the current line has a keyword that is also a modifier' do 16 | context 'the keyword is acting as a modifier' do 17 | let!(:full_line_of_text) { %(puts "hi" if true == true) } 18 | 19 | it 'returns true' do 20 | expect(subject.modifier_keyword?).to eq true 21 | end 22 | end 23 | 24 | context 'they keyword is NOT acting as a modifier' do 25 | let!(:full_line_of_text) { %(if true == true; puts "hi"; end) } 26 | 27 | it 'returns false' do 28 | expect(subject.modifier_keyword?).to eq false 29 | end 30 | end 31 | end 32 | 33 | context 'the current line does not have a keyword' do 34 | let!(:full_line_of_text) { %(puts true) } 35 | 36 | subject do 37 | options = { full_line_of_text: full_line_of_text } 38 | Tailor::Lexer::Token.new('puts', options) 39 | end 40 | 41 | it 'returns false' do 42 | expect(subject.modifier_keyword?).to eq false 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /spec/unit/tailor/lexer_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'tailor/lexer' 3 | 4 | describe Tailor::Lexer do 5 | let!(:file_text) { '' } 6 | let(:style) { {} } 7 | let(:indentation_ruler) { double 'IndentationSpacesRuler' } 8 | 9 | subject do 10 | r = Tailor::Lexer.new(file_text) 11 | r.instance_variable_set(:@buf, []) 12 | allow(r).to receive(:log) 13 | 14 | r 15 | end 16 | 17 | before do 18 | allow_any_instance_of(Tailor::Lexer).to receive(:ensure_trailing_newline). 19 | and_return(file_text) 20 | end 21 | 22 | describe '#initialize' do 23 | context 'name of file is passed in' do 24 | let(:file_name) { 'test' } 25 | 26 | before do 27 | File.open(file_name, 'w') { |f| f.write 'some text' } 28 | end 29 | 30 | it 'opens and reads the file by the name passed in' do 31 | file = double 'File' 32 | expect(file).to receive(:read).and_return file_text 33 | expect(File).to receive(:open).with('test', 'r').and_return file 34 | Tailor::Lexer.new(file_name) 35 | end 36 | end 37 | 38 | context 'text to lex is passed in' do 39 | let(:text) { 'some text' } 40 | 41 | it 'does not try to open a file' do 42 | expect(File).to_not receive(:open) 43 | Tailor::Lexer.new(text) 44 | end 45 | end 46 | end 47 | 48 | describe '#on_sp' do 49 | context 'token is a backslash then newline' do 50 | it 'calls #notify_ignored_nl_observers' do 51 | expect(subject).to receive(:notify_ignored_nl_observers) 52 | subject.on_sp("\\\n") 53 | end 54 | end 55 | end 56 | 57 | describe '#current_line_of_text' do 58 | before do 59 | subject.instance_variable_set(:@file_text, file_text) 60 | allow(subject).to receive(:lineno).and_return 1 61 | end 62 | 63 | context '@file_text is 1 line with 0 \ns' do 64 | let(:file_text) { "puts 'code'" } 65 | 66 | it 'returns the line' do 67 | expect(subject.current_line_of_text).to eq file_text 68 | end 69 | end 70 | 71 | context '@file_text is 1 empty line with 0 \ns' do 72 | let(:file_text) { '' } 73 | 74 | it 'returns the an empty string' do 75 | expect(subject.current_line_of_text).to eq file_text 76 | end 77 | end 78 | 79 | context '@file_text is 1 empty line with 1 \n' do 80 | let(:file_text) { "\n" } 81 | 82 | it 'returns an empty string' do 83 | expect(subject.current_line_of_text).to eq '' 84 | end 85 | end 86 | end 87 | 88 | describe '#count_trailing_newlines' do 89 | context 'text contains 0 trailing \n' do 90 | let(:text) { 'text' } 91 | specify { expect(subject.count_trailing_newlines(text)).to be_zero } 92 | end 93 | 94 | context 'text contains 1 trailing \n' do 95 | let(:text) { "text\n" } 96 | specify { expect(subject.count_trailing_newlines(text)).to eq 1 } 97 | end 98 | end 99 | 100 | describe '#ensure_trailing_newline' do 101 | before do 102 | Tailor::Lexer.any_instance.unstub(:ensure_trailing_newline) 103 | end 104 | 105 | context 'text contains a trailing newline already' do 106 | let!(:text) { "text\n" } 107 | 108 | before do 109 | allow(subject).to receive(:count_trailing_newlines).and_return 1 110 | end 111 | 112 | it 'does not alter the text' do 113 | expect(subject.ensure_trailing_newline(text)).to eq text 114 | end 115 | end 116 | 117 | context 'text does not contain a trailing newline' do 118 | let!(:text) { 'text' } 119 | 120 | it 'adds a newline at the end' do 121 | expect(subject.ensure_trailing_newline(text)).to eq(text + "\n") 122 | end 123 | end 124 | end 125 | 126 | describe '#sub_line_ending_backslashes' do 127 | let!(:text) do 128 | %(command \\ 129 | 'something') 130 | end 131 | 132 | before do 133 | def subject.sub_publicly(file_text) 134 | sub_line_ending_backslashes(file_text) 135 | end 136 | end 137 | 138 | it 'replaces all line-ending backslashes with a comment' do 139 | expect(subject.sub_publicly(text)).to eq %(command # TAILOR REMOVED BACKSLASH 140 | 'something') 141 | end 142 | end 143 | end 144 | -------------------------------------------------------------------------------- /spec/unit/tailor/problem_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'tailor/problem' 3 | 4 | describe Tailor::Problem do 5 | before do 6 | allow_any_instance_of(Tailor::Problem).to receive(:log) 7 | end 8 | 9 | let(:lineno) { 10 } 10 | let(:column) { 11 } 11 | 12 | describe '#set_values' do 13 | before do 14 | allow_any_instance_of(Tailor::Problem).to receive(:message) 15 | end 16 | 17 | it 'sets self[:type] to the type param' do 18 | expect(Tailor::Problem.new(:test, lineno, column, '', :b)). 19 | to include(type: :test) 20 | end 21 | 22 | it 'sets self[:line] to the lineno param' do 23 | expect(Tailor::Problem.new(:test, lineno, column, '', :c)). 24 | to include(line: lineno) 25 | end 26 | 27 | it 'sets self[:column] to the column param' do 28 | expect(Tailor::Problem.new(:test, lineno, column, '', :d)). 29 | to include(column: column) 30 | end 31 | 32 | it 'sets self[:message] to the message param' do 33 | expect(Tailor::Problem.new(:test, lineno, column, 'test', :d)). 34 | to include(message: 'test') 35 | end 36 | 37 | it 'sets self[:level] to the level param' do 38 | expect(Tailor::Problem.new(:test, lineno, column, 'test', :d)). 39 | to include(level: :d) 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/unit/tailor/reporter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'tailor/reporter' 3 | 4 | describe Tailor::Reporter do 5 | describe '#initialize' do 6 | context 'text formatter' do 7 | let(:formats) { ['text'] } 8 | 9 | it 'creates a new Formatter object of the type passed in' do 10 | reporter = Tailor::Reporter.new(formats) 11 | expect(reporter.formatters.first).to be_a Tailor::Formatters::Text 12 | end 13 | end 14 | end 15 | 16 | describe '#file_report' do 17 | let(:file_problems) { double 'file problems' } 18 | let(:formatter) { double 'Tailor::Formatters::SomeFormatter' } 19 | 20 | subject do 21 | t = Tailor::Reporter.new 22 | t.instance_variable_set(:@formatters, [formatter]) 23 | 24 | t 25 | end 26 | 27 | it 'calls #file_report on each @formatters' do 28 | label = :some_label 29 | expect(formatter).to receive(:file_report).with(file_problems, label) 30 | 31 | subject.file_report(file_problems, label) 32 | end 33 | end 34 | 35 | describe '#summary_report' do 36 | let(:all_problems) { double 'all problems' } 37 | let(:formatter) { double 'Tailor::Formatters::SomeFormatter' } 38 | 39 | subject do 40 | t = Tailor::Reporter.new 41 | t.instance_variable_set(:@formatters, [formatter]) 42 | 43 | t 44 | end 45 | 46 | context 'without output file' do 47 | it 'calls #file_report on each @formatters' do 48 | expect(formatter).to receive(:summary_report).with(all_problems) 49 | expect(File).to_not receive(:open) 50 | 51 | subject.summary_report(all_problems) 52 | end 53 | end 54 | 55 | context 'with output file' do 56 | let(:output_file) { 'output.whatever' } 57 | before do 58 | expect(formatter).to receive(:respond_to?).with(:accepts_output_file). 59 | and_return(true) 60 | expect(formatter).to receive(:accepts_output_file).and_return(true) 61 | end 62 | 63 | it 'calls #summary_report on each @formatters' do 64 | expect(formatter).to receive(:summary_report).with(all_problems) 65 | expect(File).to receive(:open).with(output_file, 'w') 66 | 67 | subject.summary_report(all_problems, output_file: output_file) 68 | end 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /spec/unit/tailor/ruler_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'tailor/ruler' 3 | 4 | describe Tailor::Ruler do 5 | before { allow(Tailor::Logger).to receive(:log) } 6 | 7 | describe '#add_child_ruler' do 8 | it 'adds new rulers to @child_rulers' do 9 | ruler = double 'Ruler' 10 | subject.add_child_ruler(ruler) 11 | expect(subject.instance_variable_get(:@child_rulers).first).to eq ruler 12 | end 13 | end 14 | 15 | describe '#problems' do 16 | context 'no child_rulers' do 17 | context '@problems is empty' do 18 | specify { expect(subject.problems).to be_empty } 19 | end 20 | 21 | context '@problems.size is 1' do 22 | before do 23 | problem = double 'Problem' 24 | expect(problem).to receive(:[]).with :line 25 | subject.instance_variable_set(:@problems, [problem]) 26 | end 27 | 28 | specify { expect(subject.problems.size).to eq 1 } 29 | end 30 | end 31 | 32 | context 'child_rulers have problems' do 33 | before do 34 | problem = double 'Problem' 35 | expect(problem).to receive(:[]).with :line 36 | child_ruler = double 'Ruler' 37 | allow(child_ruler).to receive(:problems).and_return([problem]) 38 | subject.instance_variable_set(:@child_rulers, [child_ruler]) 39 | end 40 | 41 | context '@problems is empty' do 42 | specify { expect(subject.problems.size).to eq 1 } 43 | end 44 | 45 | context '@problems.size is 1' do 46 | before do 47 | problem = double 'Problem' 48 | expect(problem).to receive(:[]).with :line 49 | subject.instance_variable_set(:@problems, [problem]) 50 | end 51 | 52 | specify { expect(subject.problems.size).to eq 2 } 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/unit/tailor/rulers/indentation_spaces_ruler_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'tailor/rulers/indentation_spaces_ruler' 3 | require 'ripper' 4 | 5 | describe Tailor::Rulers::IndentationSpacesRuler do 6 | let!(:spaces) { 5 } 7 | let(:lexed_line) { double 'LexedLine' } 8 | 9 | subject do 10 | Tailor::Rulers::IndentationSpacesRuler.new(spaces, level: :error) 11 | end 12 | 13 | describe '#comment_update' do 14 | context 'token does not contain a trailing newline' do 15 | pending 16 | end 17 | 18 | context 'token contains a trailing newline' do 19 | context 'lexed_line is spaces then a comment' do 20 | pending 21 | end 22 | 23 | context 'lexed_line is no spaces and a comment' do 24 | pending 25 | end 26 | 27 | context 'lexed_line ends with an operator' do 28 | pending 29 | end 30 | 31 | context 'lexed_line ends with a comma' do 32 | pending 33 | end 34 | end 35 | end 36 | 37 | describe '#embexpr_beg_update' do 38 | it 'sets @embexpr_nesting to [true]' do 39 | subject.instance_variable_set(:@embexpr_nesting, []) 40 | subject.embexpr_beg_update(lexed_line, 1, 1) 41 | expect(subject.instance_variable_get(:@embexpr_nesting)).to eq [true] 42 | end 43 | end 44 | 45 | describe '#embexpr_end_update' do 46 | before do 47 | expect(lexed_line).to receive(:only_embexpr_end?).and_return(false) 48 | end 49 | 50 | it 'pops @embexpr_nesting' do 51 | subject.instance_variable_set(:@embexpr_nesting, [true]) 52 | subject.embexpr_end_update(lexed_line, 1, 1) 53 | expect(subject.instance_variable_get(:@embexpr_nesting)).to eq [] 54 | end 55 | end 56 | 57 | describe '#ignored_nl_update' do 58 | pending 59 | end 60 | 61 | describe '#kw_update' do 62 | pending 63 | end 64 | 65 | describe '#lbrace_update' do 66 | pending 67 | end 68 | 69 | describe '#lbracket_update' do 70 | pending 71 | end 72 | 73 | describe '#lparen_update' do 74 | pending 75 | end 76 | 77 | describe '#nl_update' do 78 | pending 79 | end 80 | 81 | describe '#period_update' do 82 | pending 83 | end 84 | 85 | describe '#rbrace_update' do 86 | pending 87 | end 88 | 89 | describe '#rbracket_update' do 90 | pending 91 | end 92 | 93 | describe '#rparen_update' do 94 | pending 95 | end 96 | 97 | describe '#tstring_beg_update' do 98 | let(:manager) { double 'IndentationManager' } 99 | 100 | it 'calls #stop on the indentation_manager object' do 101 | expect(manager).to receive(:update_actual_indentation).with lexed_line 102 | expect(manager).to receive(:stop) 103 | subject.instance_variable_set(:@manager, manager) 104 | subject.tstring_beg_update(lexed_line, 1) 105 | end 106 | 107 | it 'adds the lineno to @tstring_nesting' do 108 | allow(manager).to receive(:update_actual_indentation) 109 | allow(manager).to receive(:stop) 110 | subject.instance_variable_set(:@manager, manager) 111 | subject.tstring_beg_update(lexed_line, 1) 112 | expect(subject.instance_variable_get(:@tstring_nesting)).to eq [1] 113 | end 114 | end 115 | 116 | describe '#tstring_end_update' do 117 | context '@tstring_nesting is not empty' do 118 | let(:manager) { double 'IndentationManager' } 119 | 120 | it 'calls #start' do 121 | expect(manager).to receive(:start) 122 | subject.instance_variable_set(:@manager, manager) 123 | subject.tstring_end_update(2) 124 | end 125 | 126 | it 'removes the lineno to @tstring_nesting then calls @manager.start' do 127 | expect(manager).to receive(:actual_indentation) 128 | expect(manager).to receive(:start) 129 | subject.instance_variable_set(:@manager, manager) 130 | subject.instance_variable_set(:@tstring_nesting, [1]) 131 | expect(subject).to receive(:measure) 132 | subject.tstring_end_update(2) 133 | expect(subject.instance_variable_get(:@tstring_nesting)).to be_empty 134 | end 135 | end 136 | end 137 | end 138 | -------------------------------------------------------------------------------- /spec/unit/tailor/rulers/spaces_after_comma_ruler_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'tailor/rulers/spaces_after_comma_ruler' 3 | 4 | describe Tailor::Rulers::SpacesAfterCommaRuler do 5 | subject { Tailor::Rulers::SpacesAfterCommaRuler.new(nil, {}) } 6 | 7 | describe '#comma_update' do 8 | it 'adds the column number to @comma_columns' do 9 | subject.comma_update(',', 2, 1) 10 | expect(subject.instance_variable_get(:@comma_columns)).to eq [1] 11 | end 12 | end 13 | 14 | describe '#check_spaces_after_comma' do 15 | context 'no event after comma' do 16 | let(:lexed_line) do 17 | l = double 'LexedLine' 18 | allow(l).to receive(:event_at) 19 | allow(l).to receive(:index) 20 | 21 | l 22 | end 23 | 24 | it 'does not detect any problems' do 25 | expect(Tailor::Problem).to_not receive(:new) 26 | expect { subject.check_spaces_after_comma(lexed_line, 1) }. 27 | to_not raise_error 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/unit/tailor/rulers/spaces_after_lbrace_ruler_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'tailor/rulers/spaces_after_lbrace_ruler' 3 | 4 | describe Tailor::Rulers::SpacesAfterLbraceRuler do 5 | subject { Tailor::Rulers::SpacesAfterLbraceRuler.new('', {}) } 6 | 7 | describe '#comment_update' do 8 | context 'token has a trailing newline' do 9 | it 'calls #ignored_nl_update' do 10 | expect(subject).to receive(:ignored_nl_update) 11 | subject.comment_update("\n", '', '', 1, 1) 12 | end 13 | end 14 | 15 | context 'token does not have a trailing newline' do 16 | it 'does not call #ignored_nl_update' do 17 | expect(subject).to_not receive(:ignored_nl_update) 18 | subject.comment_update('# comment', '', '', 1, 1) 19 | end 20 | end 21 | end 22 | 23 | describe '#ignored_nl_update' do 24 | it 'calls #check_spaces_after_lbrace' do 25 | expect(subject).to receive(:check_spaces_after_lbrace) 26 | subject.ignored_nl_update('', 1, 1) 27 | end 28 | end 29 | 30 | describe '#lbrace_update' do 31 | it 'adds column to @lbrace_columns' do 32 | subject.lbrace_update('', 1, 1) 33 | expect(subject.instance_variable_get(:@lbrace_columns)).to eq [1] 34 | end 35 | end 36 | 37 | describe '#nl_update' do 38 | it 'calls #ignored_nl_update' do 39 | expect(subject).to receive(:ignored_nl_update) 40 | subject.nl_update('', 1, 1) 41 | end 42 | end 43 | 44 | describe '#count_spaces' do 45 | context 'lexed_line.event_index returns nil' do 46 | let(:lexed_line) do 47 | l = double 'LexedLine' 48 | l.stub(:event_index).and_return nil 49 | 50 | l 51 | end 52 | 53 | it 'breaks from the loop and returns nil' do 54 | expect(lexed_line).to_not receive(:at) 55 | expect(subject.count_spaces(lexed_line, 1)).to be_nil 56 | end 57 | end 58 | 59 | context 'lexed_line.at returns nil' do 60 | let(:lexed_line) do 61 | l = double 'LexedLine' 62 | l.stub(:event_index).and_return 1 63 | l.stub(:at).and_return nil 64 | 65 | l 66 | end 67 | 68 | it 'returns 0' do 69 | expect(subject.count_spaces(lexed_line, 1)).to be_zero 70 | end 71 | end 72 | 73 | context 'next_event is a :on_nl' do 74 | let!(:next_event) do 75 | [[1, 1], :on_nl, '\n'] 76 | end 77 | 78 | let(:lexed_line) do 79 | l = double 'LexedLine' 80 | allow(l).to receive(:event_index).and_return 1 81 | expect(l).to receive(:at).with(2).and_return next_event 82 | 83 | l 84 | end 85 | 86 | it 'returns 0' do 87 | expect(subject.count_spaces(lexed_line, 1)).to be_zero 88 | end 89 | end 90 | 91 | context 'next_event is a :on_ignored_nl' do 92 | let!(:next_event) do 93 | [[1, 1], :on_ignored_nl, '\n'] 94 | end 95 | 96 | let(:lexed_line) do 97 | l = double 'LexedLine' 98 | allow(l).to receive(:event_index).and_return 1 99 | expect(l).to receive(:at).with(2).and_return next_event 100 | 101 | l 102 | end 103 | 104 | it 'breaks from the loop and returns nil' do 105 | subject.count_spaces(lexed_line, 1) 106 | end 107 | end 108 | 109 | context 'next_event is a non-space event' do 110 | let!(:next_event) do 111 | [[1, 1], :on_kw, 'def'] 112 | end 113 | 114 | let(:lexed_line) do 115 | l = double 'LexedLine' 116 | allow(l).to receive(:event_index).and_return 1 117 | allow(l).to receive(:at).and_return next_event 118 | 119 | l 120 | end 121 | 122 | it 'returns 0' do 123 | expect(subject.count_spaces(lexed_line, 1)).to be_zero 124 | end 125 | end 126 | 127 | context 'next_event is :on_sp' do 128 | let!(:next_event) do 129 | [[1, 1], :on_sp, ' '] 130 | end 131 | 132 | let(:lexed_line) do 133 | l = double 'LexedLine' 134 | allow(l).to receive(:event_index).and_return 1 135 | allow(l).to receive(:at).and_return next_event 136 | 137 | l 138 | end 139 | 140 | it 'returns 2' do 141 | expect(subject.count_spaces(lexed_line, 1)).to eq 2 142 | end 143 | end 144 | end 145 | end 146 | -------------------------------------------------------------------------------- /spec/unit/tailor/rulers/spaces_before_lbrace_ruler_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'tailor/rulers/spaces_before_lbrace_ruler' 3 | 4 | describe Tailor::Rulers::SpacesBeforeLbraceRuler do 5 | subject { Tailor::Rulers::SpacesBeforeLbraceRuler.new(nil, {}) } 6 | before { Tailor::Logger.stub(:log) } 7 | 8 | describe '#count_spaces' do 9 | context 'lexed_line.event_index is 0' do 10 | let(:lexed_line) do 11 | l = double 'LexedLine' 12 | l.stub(:event_index).and_return 0 13 | l.stub(:at).and_return nil 14 | 15 | l 16 | end 17 | 18 | specify { expect(subject.count_spaces(lexed_line, 1)).to be_zero } 19 | 20 | it 'sets @do_measurement to false' do 21 | expect { subject.count_spaces(lexed_line, 1) }. 22 | to change { subject.instance_variable_get(:@do_measurement) }. 23 | from(true).to(false) 24 | end 25 | end 26 | 27 | context 'no space before lbrace' do 28 | let(:lexed_line) do 29 | l = double 'LexedLine' 30 | l.stub(:event_index).and_return 1 31 | l.stub(:at).and_return [[10, 0], :on_const, 'HI'] 32 | 33 | l 34 | end 35 | 36 | specify { expect(subject.count_spaces(lexed_line, 1)).to be_zero } 37 | end 38 | 39 | context '1 space before lbrace' do 40 | let(:lexed_line) do 41 | l = double 'LexedLine' 42 | l.stub(:event_index).and_return 1 43 | l.stub(:at).and_return [[10, 0], :on_sp, ' '] 44 | 45 | l 46 | end 47 | 48 | specify { expect(subject.count_spaces(lexed_line, 1)).to eq 1 } 49 | end 50 | 51 | context '> 1 space before lbrace' do 52 | let(:lexed_line) do 53 | l = double 'LexedLine' 54 | l.stub(:event_index).and_return 1 55 | l.stub(:at).and_return [[10, 1], :on_sp, ' '] 56 | 57 | l 58 | end 59 | 60 | specify { expect(subject.count_spaces(lexed_line, 3)).to eq 2 } 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /spec/unit/tailor/rulers/spaces_before_rbrace_ruler_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'tailor/rulers/spaces_before_rbrace_ruler' 3 | 4 | describe Tailor::Rulers::SpacesBeforeRbraceRuler do 5 | subject { Tailor::Rulers::SpacesBeforeRbraceRuler.new(nil, {}) } 6 | before { Tailor::Logger.stub(:log) } 7 | 8 | describe '#count_spaces' do 9 | context 'lexed_line.event_index is 0' do 10 | let(:lexed_line) do 11 | l = double 'LexedLine' 12 | l.stub(:event_index).and_return 0 13 | l.stub(:at).and_return nil 14 | 15 | l 16 | end 17 | 18 | specify { expect(subject.count_spaces(lexed_line, 1)).to be_zero } 19 | 20 | it 'sets @do_measurement to false' do 21 | expect { subject.count_spaces(lexed_line, 1) }. 22 | to change { subject.instance_variable_get(:@do_measurement) }. 23 | from(true).to(false) 24 | end 25 | end 26 | 27 | context 'no space before rbrace' do 28 | let(:lexed_line) do 29 | l = double 'LexedLine' 30 | l.stub(:event_index).and_return 1 31 | l.stub(:at).and_return [[10, 0], :on_const, 'HI'] 32 | 33 | l 34 | end 35 | 36 | specify { expect(subject.count_spaces(lexed_line, 1)).to be_zero } 37 | end 38 | 39 | context '1 space before rbrace' do 40 | let(:lexed_line) do 41 | l = double 'LexedLine' 42 | l.stub(:event_index).and_return 1 43 | l.stub(:at).and_return [[10, 0], :on_sp, ' '] 44 | 45 | l 46 | end 47 | 48 | specify { expect(subject.count_spaces(lexed_line, 1)).to eq 1 } 49 | end 50 | 51 | context '> 1 space before rbrace' do 52 | let(:lexed_line) do 53 | l = double 'LexedLine' 54 | l.stub(:event_index).and_return 1 55 | l.stub(:at).and_return [[10, 0], :on_sp, ' '] 56 | 57 | l 58 | end 59 | 60 | specify { expect(subject.count_spaces(lexed_line, 1)).to eq 2 } 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /spec/unit/tailor/rulers_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'tailor/rulers' 3 | 4 | describe Tailor::Rulers do 5 | it 'requires all of its children' do 6 | # if it does one, it'll have done them all. 7 | expect(subject.const_get('AllowCamelCaseMethodsRuler')).to be_truthy 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/unit/tailor/version_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'tailor/version' 3 | 4 | describe Tailor::VERSION do 5 | it { is_expected.to eq '1.4.1' } 6 | end 7 | -------------------------------------------------------------------------------- /spec/unit/tailor_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'tailor' 3 | 4 | describe Tailor do 5 | before { Tailor::Logger.log = false } 6 | 7 | describe '.config' do 8 | it 'creates a new Configuration object' do 9 | expect(Tailor::Configuration).to receive(:new) 10 | Tailor.config 11 | end 12 | 13 | it 'returns a Configuration object' do 14 | expect(Tailor.config).to be_a Tailor::Configuration 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /tailor.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path('../lib', __FILE__) 3 | require 'tailor/version' 4 | 5 | Gem::Specification.new do |s| 6 | s.name = 'tailor' 7 | s.version = Tailor::VERSION 8 | 9 | s.author = 'Steve Loveless' 10 | s.summary = 'A Ruby style & complexity measurer' 11 | s.description = <<-DESC 12 | tailor parses Ruby files and measures them with some style and static analysis 13 | "rulers". Default values for the Rulers are based on a number of style guides 14 | in the Ruby community as well as what seems to be common. More on this here 15 | http://wiki.github.com/turboladen/tailor. 16 | 17 | tailor's goal is to help you be consistent with your code, throughout your 18 | project, whatever style that may be. 19 | DESC 20 | s.email = 'steve.loveless@gmail.com' 21 | s.homepage = 'http://github.com/turboladen/tailor' 22 | s.license = 'MIT' 23 | 24 | s.extra_rdoc_files = %w(History.md README.md) 25 | s.files = `git ls-files`.split("\n") 26 | s.test_files = `git ls-files -- {spec,features}/*`.split("\n") 27 | s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) } 28 | 29 | s.add_runtime_dependency 'log_switch', '~> 0.3.0' 30 | s.add_runtime_dependency 'nokogiri', '>= 1.6.0' 31 | s.add_runtime_dependency 'term-ansicolor', '>= 1.0.5' 32 | s.add_runtime_dependency 'text-table', '>= 1.2.2' 33 | 34 | s.add_development_dependency 'aruba' 35 | s.add_development_dependency 'bundler' 36 | s.add_development_dependency 'cucumber', '>= 1.0.2' 37 | s.add_development_dependency 'fakefs', '>= 0.4.2' 38 | s.add_development_dependency 'rake' 39 | s.add_development_dependency 'rspec', '~> 3.1' 40 | s.add_development_dependency 'rspec-its' 41 | s.add_development_dependency 'simplecov', '>= 0.4.0' 42 | s.add_development_dependency 'yard', '>= 0.7.0' 43 | end 44 | --------------------------------------------------------------------------------