rails_best_practices 0.5.6 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. data/README.md +161 -0
  2. data/lib/rails_best_practices.rb +158 -20
  3. data/lib/rails_best_practices/checks/add_model_virtual_attribute_check.rb +108 -34
  4. data/lib/rails_best_practices/checks/always_add_db_index_check.rb +148 -29
  5. data/lib/rails_best_practices/checks/check.rb +178 -75
  6. data/lib/rails_best_practices/checks/dry_bundler_in_capistrano_check.rb +26 -5
  7. data/lib/rails_best_practices/checks/isolate_seed_data_check.rb +66 -15
  8. data/lib/rails_best_practices/checks/keep_finders_on_their_own_model_check.rb +53 -12
  9. data/lib/rails_best_practices/checks/law_of_demeter_check.rb +59 -30
  10. data/lib/rails_best_practices/checks/move_code_into_controller_check.rb +35 -15
  11. data/lib/rails_best_practices/checks/move_code_into_helper_check.rb +56 -12
  12. data/lib/rails_best_practices/checks/move_code_into_model_check.rb +30 -32
  13. data/lib/rails_best_practices/checks/move_finder_to_named_scope_check.rb +45 -15
  14. data/lib/rails_best_practices/checks/move_model_logic_into_model_check.rb +31 -27
  15. data/lib/rails_best_practices/checks/needless_deep_nesting_check.rb +99 -38
  16. data/lib/rails_best_practices/checks/not_use_default_route_check.rb +43 -12
  17. data/lib/rails_best_practices/checks/overuse_route_customizations_check.rb +140 -28
  18. data/lib/rails_best_practices/checks/replace_complex_creation_with_factory_method_check.rb +44 -30
  19. data/lib/rails_best_practices/checks/replace_instance_variable_with_local_variable_check.rb +18 -7
  20. data/lib/rails_best_practices/checks/use_before_filter_check.rb +88 -18
  21. data/lib/rails_best_practices/checks/use_model_association_check.rb +61 -22
  22. data/lib/rails_best_practices/checks/use_observer_check.rb +125 -23
  23. data/lib/rails_best_practices/checks/use_query_attribute_check.rb +75 -47
  24. data/lib/rails_best_practices/checks/use_say_with_time_in_migrations_check.rb +59 -10
  25. data/lib/rails_best_practices/checks/use_scope_access_check.rb +78 -23
  26. data/lib/rails_best_practices/command.rb +19 -34
  27. data/lib/rails_best_practices/core.rb +4 -2
  28. data/lib/rails_best_practices/core/checking_visitor.rb +49 -19
  29. data/lib/rails_best_practices/core/error.rb +5 -2
  30. data/lib/rails_best_practices/core/runner.rb +79 -55
  31. data/lib/rails_best_practices/core/visitable_sexp.rb +325 -55
  32. data/lib/rails_best_practices/{core/core_ext.rb → core_ext/enumerable.rb} +3 -6
  33. data/lib/rails_best_practices/core_ext/nil_class.rb +8 -0
  34. data/lib/rails_best_practices/version.rb +1 -1
  35. data/rails_best_practices.yml +2 -2
  36. metadata +8 -7
  37. data/README.textile +0 -150
data/README.md ADDED
@@ -0,0 +1,161 @@
1
+ rails_best_practices
2
+ ====================
3
+
4
+ rails_best_practices is a code metric tool to check the quality of rails codes.
5
+
6
+ Donate
7
+ ------
8
+
9
+ <a href='http://www.pledgie.com/campaigns/12057'><img alt='Click here to lend your support to: rails-bestpractices.com and make a donation at www.pledgie.com !' src='http://www.pledgie.com/campaigns/12057.png?skin_name=chrome' border='0' /></a>
10
+
11
+ Usage
12
+ -----
13
+
14
+ At the root directory of rails app
15
+
16
+ rails_best_practices .
17
+
18
+ By default rails_best_practices will do parse codes in vendor, spec, test and features directories. If you need, see the command options:
19
+
20
+ $ rails_best_practices -h
21
+ Usage: rails_best_practices [options]
22
+ -d, --debug Debug mode
23
+ --vendor include vendor files
24
+ --spec include spec files
25
+ --test include test files
26
+ --features include features files
27
+ -x, --exclude PATTERNS Don't analyze files matching a pattern
28
+ (comma-separated regexp list)
29
+ -g, --generate Generate configuration yaml
30
+ -v, --version Show this version
31
+ -h, --help Show this message
32
+
33
+ Resources
34
+ ---------
35
+
36
+ Homepage: <http://rails-bestpractices.com>
37
+
38
+ Github: <http://github.com/flyerhzm/rails_best_practices>
39
+
40
+ RDoc: <http://rdoc.rails-bestpractices.com>
41
+
42
+ Team Blog <http://rails-bestpractices.com/blog/posts>
43
+
44
+ Google Group: <https://groups.google.com/group/rails_best_practices>
45
+
46
+ Wiki: <http://github.com/flyerhzm/rails_best_practices/wiki>
47
+
48
+ Issue Tracker: <http://github.com/flyerhzm/rails_best_practices/issues>
49
+
50
+ Install
51
+ -------
52
+
53
+ gem install rails_best_practices
54
+
55
+ Issue
56
+ -----
57
+
58
+ If you got NoMethodError or any syntax error, you should use debug mode to detect which file rails_best_practices is parsing and getting the error.
59
+
60
+ rails_best_practices -d .
61
+
62
+ Then give me the error stack and the source code of the file that rails_best_practices is parsing error.
63
+
64
+ Customize Configuration
65
+ -----------------------
66
+
67
+ First run
68
+
69
+ rails_best_practices -g
70
+
71
+ to generate <code>rails_best_practices.yml</code> file.
72
+
73
+ Now you can customize this configuration file, the default configuration is as follows:
74
+
75
+ MoveFinderToNamedScopeCheck: { }
76
+ UseModelAssociationCheck: { }
77
+ UseScopeAccessCheck: { }
78
+ AddModelVirtualAttributeCheck: { }
79
+ ReplaceComplexCreationWithFactoryMethodCheck: { attribute_assignment_count: 2 }
80
+ MoveModelLogicIntoModelCheck: { use_count: 4 }
81
+ OveruseRouteCustomizationsCheck: { customize_count: 3 }
82
+ NeedlessDeepNestingCheck: { nested_count: 2 }
83
+ NotUseDefaultRouteCheck: { }
84
+ KeepFindersOnTheirOwnModelCheck: { }
85
+ LawOfDemeterCheck: { }
86
+ UseObserverCheck: { }
87
+ IsolateSeedDataCheck: { }
88
+ AlwaysAddDbIndexCheck: { }
89
+ UseBeforeFilterCheck: { }
90
+ MoveCodeIntoControllerCheck: { }
91
+ MoveCodeIntoModelCheck: { use_count: 2 }
92
+ MoveCodeIntoHelperCheck: { array_count: 3 }
93
+ ReplaceInstanceVariableWithLocalVariableCheck: { }
94
+ DryBundlerInCapistranoCheck: { }
95
+ UseSayWithTimeInMigrationsCheck: { }
96
+ UseQueryAttributeCheck: { }
97
+
98
+ You can remove or comment one check to disable it, and you can change the options.
99
+
100
+ Implementation
101
+ --------------
102
+
103
+ Move code from Controller to Model
104
+
105
+ 1. Move finder to named_scope (rails2 only)
106
+ 2. Use model association
107
+ 3. Use scope access
108
+ 4. Add model virtual attribute
109
+ 5. Replace Complex Creation with Factory Method
110
+ 6. Move Model Logic into the Model
111
+
112
+ RESTful Conventions
113
+
114
+ 1. Overuse route customizations
115
+ 2. Needless deep nesting
116
+ 3. Not use default route
117
+
118
+ Model
119
+
120
+ 1. Keep Finders on Their Own Model (rails2 only)
121
+ 2. the Law of Demeter
122
+ 3. Use Observer
123
+ 4. Use Query Attribute
124
+
125
+ Migration
126
+
127
+ 1. Isolating Seed Data
128
+ 2. Always add DB index
129
+ 3. Use Say with Time in Migrations
130
+
131
+ Controller
132
+
133
+ 1. Use before_filter
134
+
135
+ View
136
+
137
+ 1. Move code into controller
138
+ 2. Move code into model
139
+ 3. Move code into helper
140
+ 4. Replace instance variable with local variable
141
+
142
+ Deployment
143
+
144
+ 1. Dry bundler in capistrano
145
+
146
+ Contribute
147
+ ----------
148
+
149
+ If you want to add your rails best practices into the gem, please post your best practices on <http://rails-bestpractices.com>
150
+
151
+ Contact Us
152
+ ----------
153
+
154
+ We provide rails consulting services, you can contact us by twitter or email.
155
+
156
+ Follow us on twitter: <http://twitter.com/railsbp>
157
+
158
+ Send us email: <team@rails-bestpractices.com>
159
+
160
+
161
+ Copyright © 2010 Richard Huang (flyerhzm@gmail.com), released under the MIT license
@@ -1,47 +1,169 @@
1
1
  # encoding: utf-8
2
+
3
+ #--
4
+ # Copyright (c) 2010 Richard Huang (flyerhzm@gmail.com)
5
+ #
6
+ # Permission is hereby granted, free of charge, to any person obtaining
7
+ # a copy of this software and associated documentation files (the
8
+ # "Software"), to deal in the Software without restriction, including
9
+ # without limitation the rights to use, copy, modify, merge, publish,
10
+ # distribute, sublicense, and/or sell copies of the Software, and to
11
+ # permit persons to whom the Software is furnished to do so, subject to
12
+ # the following conditions:
13
+ #
14
+ # The above copyright notice and this permission notice shall be
15
+ # included in all copies or substantial portions of the Software.
16
+ #
17
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
21
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
22
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
23
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24
+ #++
25
+ require 'rubygems'
26
+ require 'progressbar'
27
+ require 'colored'
2
28
  require 'rails_best_practices/checks'
3
29
  require 'rails_best_practices/core'
4
30
 
31
+ # RailsBestPractices helps you to analyze your rails code, according to best practices on http://rails-bestpractices.
32
+ # if it finds any violatioins to best practices, it will give you some readable suggestions.
33
+ #
34
+ # The analysis process is partitioned into two parts,
35
+ #
36
+ # 1. prepare process, it checks only model and mailer files, do some preparations, such as remember model names and associations.
37
+ # 2. review process, it checks all files, according to configuration, it really check if codes violate the best practices, if so, remember the violations.
38
+ #
39
+ # After analyzing, output the violations.
5
40
  module RailsBestPractices
6
41
 
42
+ DEFAULT_CONFIG = File.join(File.dirname(__FILE__), "..", "rails_best_practices.yml")
43
+
7
44
  class <<self
8
- def prepare_files(dir = '.')
9
- expand_dirs_to_files File.join(dir, 'app/models')
45
+ attr_writer :runner
46
+
47
+ # generate configuration yaml file.
48
+ #
49
+ # @param [String] path where to generate the configuration yaml file
50
+ def generate(path)
51
+ @path = path || '.'
52
+ FileUtils.cp DEFAULT_CONFIG, File.join(@path, 'config/rails_best_practices.yml')
10
53
  end
11
54
 
12
- def analyze_files(dir = '.', options = {})
13
- files = expand_dirs_to_files(dir)
14
- files = model_first_sort(files)
15
- ['vendor', 'spec', 'test', 'stories', 'features'].each do |pattern|
16
- files = ignore_files(files, "#{pattern}/") unless options[pattern]
55
+ # start checking rails codes.
56
+ #
57
+ # there are two steps to check rails codes,
58
+ #
59
+ # 1. prepare process, check all model and mailer files.
60
+ # 2. review process, check all files.
61
+ #
62
+ # if there are violations to rails best practices, output them.
63
+ #
64
+ # @param [String] path the directory of rails project
65
+ # @param [Hash] options
66
+ def start(path, options)
67
+ @path = path || '.'
68
+ @options = options
69
+ Core::Runner.base_path = @path
70
+ @runner = Core::Runner.new
71
+ @runner.debug = true if @options['debug']
72
+
73
+ if @runner.checks.find { |check| check.is_a? Checks::AlwaysAddDbIndexCheck } &&
74
+ !review_files.find { |file| file.index "db\/schema.rb" }
75
+ puts "AlwaysAddDbIndexCheck is disabled as there is no db/schema.rb file in your rails project.".blue
17
76
  end
18
77
 
19
- # Exclude files based on exclude regexes if the option is set.
20
- for pattern in options[:exclude]
21
- files = ignore_files(files, pattern)
78
+ @bar = ProgressBar.new('Analyzing', prepare_files.size + review_files.size)
79
+ process("prepare")
80
+ process("review")
81
+ @bar.finish
82
+
83
+ output_errors
84
+ exit @runner.errors.size
85
+ end
86
+
87
+ # process prepare or reivew.
88
+ #
89
+ # get all files for the process, analyze each file,
90
+ # and increment progress bar unless debug.
91
+ #
92
+ # @param [String] process the process name, prepare or review.
93
+ def process(process)
94
+ files = send("#{process}_files")
95
+ files.each do |file|
96
+ @runner.send("#{process}_file", file)
97
+ @bar.inc unless @options['debug']
22
98
  end
99
+ end
23
100
 
24
- files
101
+ # get all files for prepare process.
102
+ #
103
+ # @return [Array] all files for prepare process
104
+ def prepare_files
105
+ @prepare_files ||= begin
106
+ files = []
107
+ ['models', 'mailers'].each do |name|
108
+ files += expand_dirs_to_files(File.join(@path, 'app', name))
109
+ end
110
+ files.compact
111
+ end
25
112
  end
26
113
 
114
+ # get all files for review process.
115
+ #
116
+ # @return [Array] all files for review process
117
+ def review_files
118
+ @review_files ||= begin
119
+ files = expand_dirs_to_files(@path)
120
+ files = file_sort(files)
121
+ ['vendor', 'spec', 'test', 'features'].each do |pattern|
122
+ files = file_ignore(files, "#{pattern}/") unless @options[pattern]
123
+ end
124
+
125
+ # Exclude files based on exclude regexes if the option is set.
126
+ for pattern in @options[:exclude]
127
+ files = file_ignore(files, pattern)
128
+ end
129
+
130
+ files.compact
131
+ end
132
+ end
133
+
134
+ # expand all files with extenstion rb, erb, haml and builder under the dirs
135
+ #
136
+ # @param [Array] dirs what directories to expand
137
+ # @return [Array] all files expanded
27
138
  def expand_dirs_to_files *dirs
28
139
  extensions = ['rb', 'erb', 'haml', 'builder']
29
140
 
30
- dirs.flatten.map { |p|
31
- if File.directory? p
32
- Dir[File.join(p, '**', "*.{#{extensions.join(',')}}")]
141
+ dirs.flatten.map { |entry|
142
+ next unless File.exist? entry
143
+ if File.directory? entry
144
+ Dir[File.join(entry, '**', "*.{#{extensions.join(',')}}")]
33
145
  else
34
- p
146
+ entry
35
147
  end
36
148
  }.flatten
37
149
  end
38
150
 
39
- # for law_of_demeter_check
40
- def model_first_sort files
151
+
152
+ # sort files, models first, then mailers, and sort other files by characters.
153
+ #
154
+ # models and mailers first as for prepare process.
155
+ #
156
+ # @param [Array] files
157
+ # @return [Array] sorted files
158
+ def file_sort files
41
159
  files.sort { |a, b|
42
- if a =~ /models\/.*rb/
160
+ if a =~ Checks::Check::MODEL_FILES
43
161
  -1
44
- elsif b =~ /models\/.*rb/
162
+ elsif b =~ Checks::Check::MODEL_FILES
163
+ 1
164
+ elsif a =~ Checks::Check::MAILER_FILES
165
+ -1
166
+ elsif b =~ Checks::Check::MAILER_FILES
45
167
  1
46
168
  else
47
169
  a <=> b
@@ -49,8 +171,24 @@ module RailsBestPractices
49
171
  }
50
172
  end
51
173
 
52
- def ignore_files files, pattern
174
+ # ignore specific files.
175
+ #
176
+ # @param [Array] files
177
+ # @param [Regexp] pattern files match the pattern will be ignored
178
+ # @return [Array] files that not match the pattern
179
+ def file_ignore files, pattern
53
180
  files.reject { |file| file.index(pattern) }
54
181
  end
182
+
183
+ # output errors if exist.
184
+ def output_errors
185
+ @runner.errors.each { |error| puts error.to_s.red }
186
+ puts "\nPlease go to http://rails-bestpractices.com to see more useful Rails Best Practices.".green
187
+ if @runner.errors.empty?
188
+ puts "\nNo error found. Cool!".green
189
+ else
190
+ puts "\nFound #{@runner.errors.size} errors.".red
191
+ end
192
+ end
55
193
  end
56
194
  end
@@ -3,22 +3,55 @@ require 'rails_best_practices/checks/check'
3
3
 
4
4
  module RailsBestPractices
5
5
  module Checks
6
- # Check a controller to make sure adding a model virual attribute to simplify model creation.
6
+ # Make sure to add a model virual attribute to simplify model creation.
7
7
  #
8
- # Implementation: check arguments of params#[]= before calling save,
9
- # if they have duplicated arguments, then the model may need to add a model virtual attribute.
8
+ # See the best practice details here http://rails-bestpractices.com/posts/4-add-model-virtual-attribute
9
+ #
10
+ # Implementation:
11
+ #
12
+ # Prepare process:
13
+ # none
14
+ #
15
+ # Review process:
16
+ # check method define nodes in all controller files,
17
+ # if there are more than one [] method calls with the same subject and arguments,
18
+ # but assigned to one model's different attribute.
19
+ # and after these method calls, there is a save method call for that model, like
20
+ #
21
+ # def create
22
+ # @user = User.new(params[:user])
23
+ # @user.first_name = params[:full_name].split(' ', 2).first
24
+ # @user.last_name = params[:full_name].split(' ', 2).last
25
+ # @user.save
26
+ # end
27
+ #
28
+ # then the model needs to add a virtual attribute.
10
29
  class AddModelVirtualAttributeCheck < Check
11
-
12
- def interesting_nodes
30
+
31
+ def interesting_review_nodes
13
32
  [:defn]
14
33
  end
15
-
16
- def interesting_files
34
+
35
+ def interesting_review_files
17
36
  CONTROLLER_FILES
18
37
  end
19
38
 
20
- def evaluate_start(node)
21
- @variables = {}
39
+ # check method define nodes to see if there are some attribute assignments that can use model virtual attribute instead in review process.
40
+ #
41
+ # it will check every attribute assignment nodes and call node of message :save or :save!, if
42
+ #
43
+ # 1. there are more than one arguments who contain call node with messages :[] in attribute assignment nodes, e.g.
44
+ # @user.first_name = params[:full_name].split(' ').first
45
+ # @user.last_name = params[:full_name].split(' ').last
46
+ # 2. the messages of attribute assignment nodes housld be different (:first_name= , :last_name=)
47
+ # 3. the argument of call nodes with message :[] should be same (:full_name)
48
+ # 4. there should be a call node with message :save or :save! after attribute assignment nodes
49
+ # @user.save
50
+ # 5. and the subject of save or save! call node should be the same with the subject of attribute assignment nodes
51
+ #
52
+ # then the attribute assignment nodes can add model virtual attribute instead.
53
+ def review_start_defn(node)
54
+ @attrasgns = {}
22
55
  node.recursive_children do |child|
23
56
  case child.node_type
24
57
  when :attrasgn
@@ -28,36 +61,77 @@ module RailsBestPractices
28
61
  else
29
62
  end
30
63
  end
31
- @variables = nil
32
64
  end
33
-
65
+
34
66
  private
35
-
36
- def attribute_assignment(node)
37
- variable = node.subject
38
- arguments_node = nil
39
- node.arguments.recursive_children do |child|
40
- if :[] == child.message
41
- arguments_node = child
42
- break
67
+ # check an attribute assignment node, if there is a :[] message of call node in the attribute assignment node,
68
+ # then remember this attribute assignment.
69
+ #
70
+ # s(:attrasgn, s(:ivar, :@user), :first_name=,
71
+ # s(:arglist,
72
+ # s(:call,
73
+ # s(:call,
74
+ # s(:call, s(:call, nil, :params, s(:arglist)), :[], s(:arglist, s(:lit, :full_name))),
75
+ # :split,
76
+ # s(:arglist, s(:str, " "), s(:lit, 2))
77
+ # ),
78
+ # :first,
79
+ # s(:arglist)
80
+ # )
81
+ # )
82
+ # )
83
+ #
84
+ # The remember attribute assignments (@attrasgns) are as follows
85
+ #
86
+ # {
87
+ # s(:ivar, :@user) =>
88
+ # [{
89
+ # :message=>:first_name=,
90
+ # :arguments=>s(:call, s(:call, nil, :params, s(:arglist)), :[], s(:arglist, s(:lit, :full_name)))
91
+ # }]
92
+ # }
93
+ def attribute_assignment(node)
94
+ subject = node.subject
95
+ arguments_node = node.arguments.grep_node(:message => :[])
96
+ return if subject.nil? or arguments_node.nil?
97
+ @attrasgns[subject] ||= []
98
+ @attrasgns[subject] << {:message => node.message, :arguments => arguments_node}
99
+ end
100
+
101
+ # check a call node with message :save or :save!,
102
+ # if there exists an attribute assignment for the subject of this call node,
103
+ # and if the arguments of this attribute assignments has duplicated entries (different message and same arguments),
104
+ # then this node needs to add a virtual attribute.
105
+ #
106
+ # e.g. this is @attrasgns
107
+ # {
108
+ # s(:ivar, :@user)=>
109
+ # [{
110
+ # :message=>:first_name=,
111
+ # :arguments=>s(:call, s(:call, nil, :params, s(:arglist)), :[], s(:arglist, s(:lit, :full_name)))
112
+ # }, {
113
+ # :message=>:last_name=,
114
+ # :arguments=>s(:call, s(:call, nil, :params, s(:arglist)), :[], s(:arglist, s(:lit, :full_name)))
115
+ # }]
116
+ # }
117
+ # and this is the call node
118
+ # s(:call, s(:ivar, :@user), :save, s(:arglist))
119
+ #
120
+ # The message of call node is :save,
121
+ # and the key of @attrasgns is the same as the subject of call node,
122
+ # and the value of @aatrasgns has different message and same arguments.
123
+ def call_assignment(node)
124
+ if [:save, :save!].include? node.message
125
+ subject = node.subject
126
+ add_error "add model virtual attribute (for #{subject})" if params_dup?(@attrasgns[subject].collect {|h| h[:arguments]})
43
127
  end
44
128
  end
45
- return if variable.nil? or arguments_node.nil?
46
- @variables[variable] ||= []
47
- @variables[variable] << {:message => node.message, :arguments => arguments_node}
48
- end
49
-
50
- def call_assignment(node)
51
- if node.message == :save
52
- variable = node.subject
53
- add_error "add model virtual attribute (for #{variable})" if params_dup?(@variables[variable].collect {|h| h[:arguments]})
129
+
130
+ # if the nodes are duplicated.
131
+ def params_dup?(nodes)
132
+ return false if nodes.nil?
133
+ !nodes.dups.empty?
54
134
  end
55
- end
56
-
57
- def params_dup?(nodes)
58
- return false if nodes.nil?
59
- !nodes.dups.empty?
60
- end
61
135
  end
62
136
  end
63
137
  end