tailor 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. data/History.txt +19 -0
  2. data/Manifest.txt +36 -0
  3. data/PostInstall.txt +2 -0
  4. data/README.rdoc +62 -0
  5. data/Rakefile +78 -0
  6. data/bin/tailor +18 -0
  7. data/features/case_checking.feature +38 -0
  8. data/features/development.feature +13 -0
  9. data/features/spacing.feature +40 -0
  10. data/features/step_definitions/case_checking_steps.rb +42 -0
  11. data/features/step_definitions/common_steps.rb +175 -0
  12. data/features/step_definitions/spacing_steps.rb +93 -0
  13. data/features/support/1_file_with_camel_case_class/camel_case_class.rb +5 -0
  14. data/features/support/1_file_with_camel_case_method/camel_case_method.rb +3 -0
  15. data/features/support/1_file_with_hard_tabs/hard_tab.rb +3 -0
  16. data/features/support/1_file_with_long_lines/long_lines.rb +5 -0
  17. data/features/support/1_file_with_snake_case_class/snake_case_class.rb +5 -0
  18. data/features/support/1_file_with_snake_case_method/snake_case_method.rb +3 -0
  19. data/features/support/1_file_with_trailing_whitespace/trailing_whitespace.rb +5 -0
  20. data/features/support/1_good_simple_file/my_project.rb +7 -0
  21. data/features/support/common.rb +29 -0
  22. data/features/support/env.rb +15 -0
  23. data/features/support/matchers.rb +11 -0
  24. data/features/support/world.rb +53 -0
  25. data/lib/tailor.rb +143 -0
  26. data/lib/tailor/file_line.rb +228 -0
  27. data/lib/tailor/indentation_checker.rb +27 -0
  28. data/ruby-style-checker.rb +136 -0
  29. data/script/console +10 -0
  30. data/script/destroy +14 -0
  31. data/script/generate +14 -0
  32. data/spec/file_line_spec.rb +136 -0
  33. data/spec/spec.opts +1 -0
  34. data/spec/spec_helper.rb +10 -0
  35. data/spec/tailor_spec.rb +27 -0
  36. data/tasks/rspec.rake +21 -0
  37. metadata +218 -0
@@ -0,0 +1,93 @@
1
+ $:.unshift(File.dirname(__FILE__) + '/../../lib')
2
+ require 'tailor/indentation_checker'
3
+ require 'tailor/file_line'
4
+
5
+ include Tailor
6
+ include Tailor::IndentationChecker
7
+
8
+ #-----------------------------------------------------------------------------
9
+ # "Given" statements
10
+ #-----------------------------------------------------------------------------
11
+ Given /^that file contains lines with hard tabs$/ do
12
+ @ruby_source = File.open(@file_list[0], 'r')
13
+ contains_hard_tabs = false
14
+ @ruby_source.each_line do |line|
15
+ source_line = Tailor::FileLine.new line
16
+ if source_line.hard_tabbed?
17
+ contains_hard_tabs = true
18
+ break
19
+ end
20
+ end
21
+ contains_hard_tabs.should be_true
22
+ end
23
+
24
+ Given /^that file does not contain any "([^\"]*)" statements$/ do |keyword|
25
+ @ruby_source = File.open(@file_list[0], 'r')
26
+
27
+ count = count_keywords(@ruby_source, keyword)
28
+ count.should == 0
29
+ end
30
+
31
+ Given /^that file is indented properly$/ do
32
+ @file_list.each do |file|
33
+ Tailor::IndentationChecker.validate_indentation file
34
+ end
35
+ end
36
+
37
+ Given /^that file contains lines with trailing whitespace$/ do
38
+ @ruby_source = File.open(@file_list[0], 'r')
39
+
40
+ @ruby_source.each_line do |line|
41
+ source_line = Tailor::FileLine.new line
42
+
43
+ @whitespace_count = source_line.trailing_whitespace_count
44
+
45
+ @whitespace_count.should > 0
46
+ end
47
+ end
48
+
49
+ Given /^that file contains lines longer than 80 characters$/ do
50
+ @ruby_source = File.open(@file_list[0], 'r')
51
+
52
+ @ruby_source.each_line do |line|
53
+ source_line = Tailor::FileLine.new line
54
+
55
+ if source_line.too_long?
56
+ too_long = true
57
+ break
58
+ else
59
+ too_long = false
60
+ end
61
+
62
+ too_long.should be_true
63
+ end
64
+ end
65
+
66
+
67
+ #-----------------------------------------------------------------------------
68
+ # "When" statements
69
+ #-----------------------------------------------------------------------------
70
+ When "I run the checker on the project" do
71
+ @result = `#{@tailor} #{@project_dir}`
72
+ end
73
+
74
+ #-----------------------------------------------------------------------------
75
+ # "Then" statements
76
+ #-----------------------------------------------------------------------------
77
+ Then /^the checker should tell me each line that has a hard tab$/ do
78
+ @result.should include("Line is hard-tabbed")
79
+ end
80
+
81
+ Then "the checker should tell me my indentation is OK" do
82
+ pending
83
+ end
84
+
85
+ Then /^the checker should tell me each line has trailing whitespace$/ do
86
+ message= "Line contains #{@whitespace_count} trailing whitespace(s)"
87
+ @result.should include message
88
+ end
89
+
90
+ Then /^the checker should tell me each line is too long$/ do
91
+ msg = "Line is greater than #{Tailor::FileLine::LINE_LENGTH_MAX} characters"
92
+ @result.should include msg
93
+ end
@@ -0,0 +1,5 @@
1
+ class CamelCase
2
+ def my_method
3
+ return
4
+ end
5
+ end
@@ -0,0 +1,3 @@
1
+ def doSomething
2
+ return
3
+ end
@@ -0,0 +1,3 @@
1
+ def my_method
2
+ return
3
+ end
@@ -0,0 +1,5 @@
1
+ # This method has a line in it that is way too freaking long. Seriously, why would you do
2
+ # something like this?
3
+ def do_something and_stuff
4
+ this_is_a_long_variable_name = Hash.new(:thing1 => 'something', :thing2 => 'something')
5
+ end
@@ -0,0 +1,5 @@
1
+ class A_Class
2
+ def do_something
3
+ return
4
+ end
5
+ end
@@ -0,0 +1,3 @@
1
+ def do_something
2
+ return
3
+ end
@@ -0,0 +1,5 @@
1
+ class MyClass
2
+ def do_something
3
+ return
4
+ end
5
+ end
@@ -0,0 +1,7 @@
1
+ # This file contains 1 method, no class, and is properly indented
2
+ # in order to test a related scenario.
3
+ def a_method
4
+ an_array = Array.new
5
+ a_hash = Hash.new
6
+ end
7
+
@@ -0,0 +1,29 @@
1
+ module CommonHelpers
2
+ def in_tmp_folder(&block)
3
+ FileUtils.chdir(@tmp_root, &block)
4
+ end
5
+
6
+ def in_project_folder(&block)
7
+ project_folder = @active_project_folder || @tmp_root
8
+ FileUtils.chdir(project_folder, &block)
9
+ end
10
+
11
+ def in_home_folder(&block)
12
+ FileUtils.chdir(@home_path, &block)
13
+ end
14
+
15
+ def force_local_lib_override(project_name = @project_name)
16
+ rakefile = File.read(File.join(project_name, 'Rakefile'))
17
+ File.open(File.join(project_name, 'Rakefile'), "w+") do |f|
18
+ f << "$:.unshift('#{@lib_path}')\n"
19
+ f << rakefile
20
+ end
21
+ end
22
+
23
+ def setup_active_project_folder project_name
24
+ @active_project_folder = File.join(@tmp_root, project_name)
25
+ @project_name = project_name
26
+ end
27
+ end
28
+
29
+ World(CommonHelpers)
@@ -0,0 +1,15 @@
1
+ require File.dirname(__FILE__) + "/../../lib/tailor"
2
+
3
+ gem 'cucumber'
4
+ require 'cucumber'
5
+ gem 'rspec'
6
+ require 'spec'
7
+
8
+ Before do
9
+ @tmp_root = File.dirname(__FILE__) + "/../../tmp"
10
+ @home_path = File.expand_path(File.join(@tmp_root, "home"))
11
+ @lib_path = File.expand_path(File.dirname(__FILE__) + "/../../lib")
12
+ FileUtils.rm_rf @tmp_root
13
+ FileUtils.mkdir_p @home_path
14
+ ENV['HOME'] = @home_path
15
+ end
@@ -0,0 +1,11 @@
1
+ module Matchers
2
+ def contain(expected)
3
+ simple_matcher("contain #{expected.inspect}") do |given, matcher|
4
+ matcher.failure_message = "expected #{given.inspect} to contain #{expected.inspect}"
5
+ matcher.negative_failure_message = "expected #{given.inspect} not to contain #{expected.inspect}"
6
+ given.index expected
7
+ end
8
+ end
9
+ end
10
+
11
+ World(Matchers)
@@ -0,0 +1,53 @@
1
+ $:.unshift(File.dirname(__FILE__) + '/../../lib')
2
+
3
+ require 'tailor'
4
+
5
+ include Tailor
6
+
7
+
8
+ # Counts keywords in the file provided.
9
+ #
10
+ # @param [String] file Path to the file to check
11
+ # @param [String] keyword Keyword to count
12
+ # @return [Number] Number of keywords counted
13
+ def count_keywords file, keyword
14
+ ruby_source = File.open(file, 'r')
15
+
16
+ count = 0
17
+ ruby_source.each_line do |line|
18
+ if line =~ /^#{keyword}/
19
+ count =+ 1
20
+ end
21
+ end
22
+ ruby_source.close
23
+ count
24
+ end
25
+
26
+
27
+ # Prep for the testing
28
+ Before do
29
+ @tailor = "#{File.dirname(__FILE__)}/../../bin/tailor"
30
+ end
31
+
32
+
33
+ #-----------------------------------------------------------------------------
34
+ # "Given" statements
35
+ #-----------------------------------------------------------------------------
36
+ Given /^I have a project directory "([^\"]*)"$/ do |project_dir|
37
+ project_dir = "support/#{project_dir}"
38
+ File.exists?(project_dir).should be_true
39
+ File.directory?(project_dir).should be_true
40
+ @project_dir = project_dir
41
+ end
42
+
43
+ Given /^the file contains only "([^\"]*)" "([^\"]*)" statement$/ do
44
+ |count_in_spec, keyword|
45
+ #count_in_file = count_keywords(@ruby_source, keyword)
46
+ count_in_file = count_keywords(@file_list[0], keyword)
47
+ count_in_file.should == count_in_spec.to_i
48
+ end
49
+
50
+ Given /^I have "([^\"]*)" file in my project$/ do |file_count|
51
+ @file_list = Dir.glob("#{@project_dir}/*")
52
+ @file_list.length.should == file_count.to_i
53
+ end
data/lib/tailor.rb ADDED
@@ -0,0 +1,143 @@
1
+ $:.unshift(File.dirname(__FILE__)) unless
2
+ $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
3
+
4
+ require 'fileutils'
5
+ require 'pathname'
6
+ require 'tailor/file_line'
7
+
8
+ module Tailor
9
+ VERSION = '0.0.2'
10
+
11
+ RUBY_KEYWORDS_WITH_END = [
12
+ 'begin',
13
+ 'case',
14
+ 'class',
15
+ 'def',
16
+ 'do',
17
+ 'if',
18
+ 'unless',
19
+ 'until',
20
+ 'while'
21
+ ]
22
+
23
+ # Check all files in a directory for style problems.
24
+ #
25
+ # @param [String] project_base_dir Path to a directory to recurse into and
26
+ # look for problems in.
27
+ # @return [Hash] Returns a hash that contains file_name => problem_count.
28
+ def self.check project_base_dir
29
+ # Get the list of files to process
30
+ ruby_files_in_project = project_file_list(project_base_dir)
31
+
32
+ files_and_problems = Hash.new
33
+
34
+ # Process each file
35
+ ruby_files_in_project.each do |file_name|
36
+ problems = find_problems file_name
37
+ files_and_problems[file_name] = problems
38
+ end
39
+
40
+ files_and_problems
41
+ end
42
+
43
+ # Gets a list of .rb files in the project. This gets each file's absolute
44
+ # path in order to alleviate any possible confusion.
45
+ #
46
+ # @param [String] base_dir Directory to start recursing from to look for .rb
47
+ # files
48
+ # @return [Array] Sorted list of absolute file paths in the project
49
+ def self.project_file_list base_dir
50
+ if File.directory? base_dir
51
+ FileUtils.cd base_dir
52
+ end
53
+
54
+ # Get the .rb files
55
+ ruby_files_in_project = Dir.glob(File.join('*', '**', '*.rb'))
56
+ Dir.glob(File.join('*.rb')).each { |f| ruby_files_in_project << f }
57
+
58
+ # Expand paths to all files in the list
59
+ list_with_absolute_paths = Array.new
60
+ ruby_files_in_project.each do |file|
61
+ list_with_absolute_paths << File.expand_path(file)
62
+ end
63
+
64
+ list_with_absolute_paths.sort
65
+ end
66
+
67
+ # Checks a sing file for all defined styling parameters.
68
+ #
69
+ # @param [String] file_name Path to a file to check styling on.
70
+ # @return [Number] Returns the number of errors on the file.
71
+ def self.find_problems file_name
72
+ source = File.open(file_name, 'r')
73
+ file_path = Pathname.new(file_name)
74
+
75
+ puts
76
+ puts "#-------------------------------------------------------------------"
77
+ puts "# Looking for bad style in:"
78
+ puts "# \t'#{file_path}'"
79
+ puts "#-------------------------------------------------------------------"
80
+
81
+ problem_count = 0
82
+ line_number = 1
83
+ source.each_line do |source_line|
84
+ line = FileLine.new source_line
85
+
86
+ # Check for hard tabs
87
+ if line.hard_tabbed?
88
+ puts "Line is hard-tabbed:"
89
+ puts "\t#{file_path.relative_path_from(Pathname.pwd)}: #{line_number}"
90
+ problem_count += 1
91
+ end
92
+
93
+ # Check for camel-cased methods
94
+ if line.method? and line.camel_case_method?
95
+ puts "Method name uses camel case:"
96
+ puts "\t#{file_path.relative_path_from(Pathname.pwd)}: #{line_number}"
97
+ problem_count += 1
98
+ end
99
+
100
+ # Check for non-camel-cased classes
101
+ if line.class? and !line.camel_case_class?
102
+ puts "Class name does NOT use camel case:"
103
+ puts "\t#{file_path.relative_path_from(Pathname.pwd)}: #{line_number}"
104
+ problem_count += 1
105
+ end
106
+
107
+ # Check for trailing whitespace
108
+ count = line.trailing_whitespace_count
109
+ if count > 0
110
+ puts "Line contains #{count} trailing whitespace(s):"
111
+ puts "\t#{file_path.relative_path_from(Pathname.pwd)}: #{line_number}"
112
+ problem_count += 1
113
+ end
114
+
115
+ # Check for long lines
116
+ if line.too_long?
117
+ puts "Line is greater than #{FileLine::LINE_LENGTH_MAX} characters:"
118
+ puts "\t#{file_path.relative_path_from(Pathname.pwd)}: #{line_number}"
119
+ problem_count += 1
120
+ end
121
+
122
+ line_number += 1
123
+ end
124
+
125
+ problem_count
126
+ end
127
+
128
+ # Prints a summary report that shows which files had how many problems.
129
+ #
130
+ # @param [Hash] files_and_problems Returns a hash that contains
131
+ # file_name => problem_count.
132
+ def self.print_report files_and_problems
133
+ puts
134
+ puts "The following files are out of style:"
135
+
136
+ files_and_problems.each_pair do |file, problem_count|
137
+ file_path = Pathname.new(file)
138
+ unless problem_count == 0
139
+ puts "\t#{file_path.relative_path_from(Pathname.pwd)}: #{problem_count} problems"
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,228 @@
1
+ module Tailor
2
+
3
+ # Calling modules will get the Ruby file to check, then read by line. This
4
+ # class allows for checking of line-specific style by Represents a single
5
+ # line of a file of Ruby code. Inherits from String so "self" can be used.
6
+ class FileLine < String
7
+
8
+ LINE_LENGTH_MAX = 80
9
+
10
+ # This passes the line of code to String (the parent) so that it can act
11
+ # like a standard string.
12
+ #
13
+ # @param [String] line_of_code Line from a Ruby file that will be checked
14
+ # for styling.
15
+ # @return [String] Returns a String that includes all of the methods
16
+ # defined here.
17
+ def initialize line_of_code
18
+ super line_of_code
19
+ end
20
+
21
+ # Determines the number of spaces the line is indented.
22
+ #
23
+ # @return [Number] Returns the number of spaces the line is indented.
24
+ def indented_spaces
25
+ # Find out how many spaces exist at the beginning of the line
26
+ spaces = self.scan(/^\x20+/).first
27
+
28
+ unless spaces.nil?
29
+ return spaces.length
30
+ else
31
+ return 0
32
+ end
33
+ end
34
+
35
+ # Checks to see if the source code line is tabbed
36
+ #
37
+ # @return [Boolean] Returns true if the file contains a tab
38
+ # character before any others in that line.
39
+ def hard_tabbed?
40
+ result = case self
41
+ # The line starts with a tab
42
+ when /^\t/ then true
43
+ # The line starts with spaces, then has a tab
44
+ when /^\s+\t/ then true
45
+ else false
46
+ end
47
+ end
48
+
49
+ # Checks to see if the method is using camel case.
50
+ #
51
+ # @return [Boolean] Returns true if the method name is camel case.
52
+ def camel_case_method?
53
+ words = self.split(/ /)
54
+
55
+ # If we're dealing with a method, check for uppercase chars
56
+ if self.method?
57
+
58
+ # The 2nd word is the method name, so evaluate that for caps chars.
59
+ if words[1] =~ /[A-Z]/
60
+ return true
61
+ else
62
+ return false
63
+ end
64
+ # If we're dealing with a class, check for an underscore.
65
+ else
66
+ return nil
67
+ end
68
+ end
69
+
70
+ # Checks to see if the class is using camel case.
71
+ #
72
+ # @return [Boolean] Returns true if the class name is camel case.
73
+ def camel_case_class?
74
+ words = self.split(/ /)
75
+
76
+ # If we're dealing with a class, check for an underscore.
77
+ if self.class?
78
+ if words[1] =~ /_/
79
+ return false
80
+ else
81
+ return true
82
+ end
83
+ else
84
+ return nil
85
+ end
86
+ end
87
+
88
+ # Checks to see if the line is the start of a method's definition.
89
+ #
90
+ # @return [Boolean] Returns true if the line contains 'def' and the second
91
+ # word begins with a lowercase letter.
92
+ def method?
93
+ words = self.split(/ /)
94
+ if words[0].eql? "def" and starts_with_lowercase?(words[1])
95
+ return true
96
+ else
97
+ return false
98
+ end
99
+ end
100
+
101
+ # Checks to see if the line is the start of a class's definition.
102
+ #
103
+ # @return [Boolean] Returns true if the line contains 'class' and the
104
+ # second word begins with a uppercase letter.
105
+ def class?
106
+ words = self.split(/ /)
107
+ if words[0].eql? "class" and starts_with_uppercase?(words[1])
108
+ return true
109
+ else
110
+ return false
111
+ end
112
+ end
113
+
114
+ # Checks to see if the whole line is a basic comment line. This doesn't
115
+ # check for trailing-line comments (@see #trailing_comment?).
116
+ #
117
+ # @return [Boolean] Returns true if the line begins with a pound symbol.
118
+ def line_comment?
119
+ if self.scan(/\s+#/).empty?
120
+ return false
121
+ else
122
+ return true
123
+ end
124
+ end
125
+
126
+ # Checks to see if the line has trailing whitespace at the end of it. Note
127
+ # that this excludes empty lines that have spaces on them!
128
+ #
129
+ # @return [Number] Returns the number of trailing spaces at the end of the
130
+ # line.
131
+ def trailing_whitespace_count
132
+ spaces = self.scan(/(\x20+|\x09+)$/)
133
+ if spaces.first.eql? nil
134
+ return 0
135
+ else
136
+ return spaces.first.first.length
137
+ end
138
+ end
139
+
140
+ # Checks to see if a single space exists after a comma in uncomented code.
141
+ # This method doesn't check if the line is a comment, so this should be
142
+ # done before calling this method. @see #line_comment?.
143
+ #
144
+ # @return [Boolean] Returns true if only 1 space exists after a comma.
145
+ def two_or_more_spaces_after_comma?
146
+ if self.scan(/\w\,\s{2,}/).empty?
147
+ return false
148
+ else
149
+ return true
150
+ end
151
+ end
152
+
153
+ ##
154
+ # Checks to see if there's no spaces after a comma.
155
+ #
156
+ # @return [Boolean] Returns true if there isn't a space after a comma.
157
+ def no_space_after_comma?
158
+ if self.scan(/\w\,\w/).empty?
159
+ return false
160
+ else
161
+ return true
162
+ end
163
+ end
164
+
165
+ ##
166
+ # Checks to see if there's no space before a comma.
167
+ #
168
+ # @return [Boolean] Returns true if there's no space before a comma.
169
+ def no_space_before_comma?
170
+ if self.scan(/\w\s\,/)
171
+ return true
172
+ else
173
+ return false
174
+ end
175
+ end
176
+
177
+ # Counts the number of spaces around a comma and returns before and after
178
+ # values as a hash.
179
+ #
180
+ # @return [Hash<:before,:after>] Returns a Hash with values for :before and
181
+ # :after.
182
+ def spaces_around_comma
183
+ spaces = Hash.new
184
+
185
+ spaces[:before] = self.scan(/(\x20+),/)
186
+ spaces[:after] = self.scan(/,(\x20+)/)
187
+
188
+ spaces
189
+ end
190
+
191
+ ##
192
+ # Checks to see if the line is greater than the defined max (80 chars is
193
+ # default).
194
+ #
195
+ # @return [Boolean] Returns true if the line length exceeds the allowed
196
+ # length.
197
+ def too_long?
198
+ self.length > LINE_LENGTH_MAX ? true : false
199
+ end
200
+
201
+ #-----------------------------------------------------------------
202
+ # Private methods
203
+ #-----------------------------------------------------------------
204
+ private
205
+
206
+ # Checks to see if a word begins with a lowercase letter.
207
+ #
208
+ # @param [String] word The word to check case on.
209
+ def starts_with_lowercase? word
210
+ if word =~ /^[a-z]/
211
+ return true
212
+ else
213
+ return false
214
+ end
215
+ end
216
+
217
+ # Checks to see if a word begins with an uppercase letter.
218
+ #
219
+ # @param [String] word The word to check case on.
220
+ def starts_with_uppercase? word
221
+ if word =~ /^[A-Z]/
222
+ return true
223
+ else
224
+ return false
225
+ end
226
+ end
227
+ end
228
+ end