rails_best_practices 0.5.6 → 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
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