lint_fu 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +20 -0
- data/README.rdoc +20 -0
- data/bin/lint_fu +92 -0
- data/lib/lint_fu/action_pack/model_controller.rb +7 -0
- data/lib/lint_fu/action_pack/model_controller_builder.rb +26 -0
- data/lib/lint_fu/action_pack.rb +2 -0
- data/lib/lint_fu/active_record/model_model.rb +21 -0
- data/lib/lint_fu/active_record/model_model_builder.rb +72 -0
- data/lib/lint_fu/active_record.rb +2 -0
- data/lib/lint_fu/checker.rb +11 -0
- data/lib/lint_fu/issue.rb +45 -0
- data/lib/lint_fu/mixins/file_class_methods.rb +16 -0
- data/lib/lint_fu/mixins/sexp_instance_methods.rb +111 -0
- data/lib/lint_fu/mixins/symbol_instance_methods.rb +13 -0
- data/lib/lint_fu/mixins.rb +3 -0
- data/lib/lint_fu/model_element.rb +48 -0
- data/lib/lint_fu/model_element_builder.rb +29 -0
- data/lib/lint_fu/parser.rb +14 -0
- data/lib/lint_fu/rails/buggy_eager_load_checker.rb +110 -0
- data/lib/lint_fu/rails/model_application.rb +16 -0
- data/lib/lint_fu/rails/model_application_builder.rb +32 -0
- data/lib/lint_fu/rails/scan_builder.rb +43 -0
- data/lib/lint_fu/rails/sql_injection_checker.rb +133 -0
- data/lib/lint_fu/rails/unsafe_find_checker.rb +122 -0
- data/lib/lint_fu/rails.rb +6 -0
- data/lib/lint_fu/report.rb +239 -0
- data/lib/lint_fu/scan.rb +64 -0
- data/lib/lint_fu/source_control/git.rb +53 -0
- data/lib/lint_fu/source_control_provider.rb +58 -0
- data/lib/lint_fu/visitor.rb +36 -0
- data/lib/lint_fu.rb +34 -0
- data/lint_fu.gemspec +35 -0
- metadata +144 -0
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Tony Spataro
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
= LintFu
|
2
|
+
|
3
|
+
Lint-Fu uses some very basic static analysis tricks to find bugs in your Rails
|
4
|
+
app. It produces a report (HTML format by default) explaining what it found.
|
5
|
+
|
6
|
+
Although Lint-Fu was built for Rails applications, it is modular and extensible
|
7
|
+
and rules can be developed for any framework or toolkit, or even for Ruby code
|
8
|
+
in general.
|
9
|
+
|
10
|
+
Lint-Fu's capabilities are limited to semantic and structural analysis of the
|
11
|
+
code; it does model data or control flow. This means it spots a *very small*
|
12
|
+
subset of all potential bugs that it knows how to look for. Furthermore, it
|
13
|
+
will sometimes report false positives.
|
14
|
+
|
15
|
+
= Example
|
16
|
+
|
17
|
+
$ rake lint
|
18
|
+
|
19
|
+
|
20
|
+
Copyright (c) 2009 Tony Spataro, released under the MIT license
|
data/bin/lint_fu
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
basedir = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))
|
4
|
+
$LOAD_PATH.unshift(basedir) unless $LOAD_PATH.include?(basedir)
|
5
|
+
|
6
|
+
require 'lint_fu'
|
7
|
+
|
8
|
+
def timed(activity)
|
9
|
+
print activity, '...'
|
10
|
+
STDOUT.flush
|
11
|
+
t0 = Time.now.to_i
|
12
|
+
yield
|
13
|
+
t1 = Time.now.to_i
|
14
|
+
dt = t1-t0
|
15
|
+
if dt > 0
|
16
|
+
puts "done (#{t1-t0} sec)"
|
17
|
+
STDOUT.flush
|
18
|
+
else
|
19
|
+
puts "done"
|
20
|
+
STDOUT.flush
|
21
|
+
end
|
22
|
+
rescue Exception => e
|
23
|
+
puts 'error!' #ensure we print a newline
|
24
|
+
raise e
|
25
|
+
end
|
26
|
+
|
27
|
+
app_root = File.expand_path('.')
|
28
|
+
|
29
|
+
#Only define the Rake tasks if the plugin loaded successfully
|
30
|
+
@scm = LintFu::SourceControlProvider.for_directory(app_root)
|
31
|
+
|
32
|
+
#Build a model of the application we are scanning.
|
33
|
+
timed("Build a model of the application") do
|
34
|
+
builder = LintFu::Rails::ModelApplicationBuilder.new(app_root)
|
35
|
+
@application = builder.model_elements[0]
|
36
|
+
raise LintFu::ProviderError.new("Unable to identify the source control provider for #{app_root}") unless @scm
|
37
|
+
end
|
38
|
+
|
39
|
+
#Using the model we built, scan the controllers for security bugs.
|
40
|
+
timed("Scan the application") do
|
41
|
+
builder = LintFu::Rails::ScanBuilder.new(app_root)
|
42
|
+
@scan = builder.scan(@application)
|
43
|
+
end
|
44
|
+
|
45
|
+
@genuine_issues = @scan.issues.select { |i| !@scan.blessed?(i) }
|
46
|
+
if @genuine_issues.empty?
|
47
|
+
puts "Clean scan: no issues found. Skipping report."
|
48
|
+
exit(0)
|
49
|
+
end
|
50
|
+
|
51
|
+
#CruiseControl.rb integration: write our report to the CC build artifacts folder
|
52
|
+
output_dir = ENV['CC_BUILD_ARTIFACTS'] || app_root
|
53
|
+
mkdir_p output_dir unless File.directory?(output_dir)
|
54
|
+
|
55
|
+
flavor = ENV['FORMAT'] || 'html'
|
56
|
+
typename = "#{flavor}_report".camelize
|
57
|
+
|
58
|
+
#Use a filename (or STDOUT) for our report that corresponds to its format
|
59
|
+
case flavor
|
60
|
+
when 'html'
|
61
|
+
output_name = File.join(output_dir, 'lint.html')
|
62
|
+
output = File.open(output_name, 'w')
|
63
|
+
when 'text'
|
64
|
+
output = STDOUT
|
65
|
+
else
|
66
|
+
puts "Unrecognized output format #{flavor} (undefined type #{typename})"
|
67
|
+
exit -1
|
68
|
+
end
|
69
|
+
|
70
|
+
klass = LintFu.const_get(typename.to_sym)
|
71
|
+
|
72
|
+
timed("Generate report") do
|
73
|
+
klass.new(@scan, @scm, @genuine_issues).generate(output)
|
74
|
+
output.close
|
75
|
+
end
|
76
|
+
|
77
|
+
#Support automation jobs that need to distinguish between failure due to
|
78
|
+
#broken environment and failure to due issues that were genuinely found by
|
79
|
+
#the lint task.
|
80
|
+
if ENV['STATUS_IF_ISSUES']
|
81
|
+
if(@genuine_issues.size > 0)
|
82
|
+
retval = ENV['STATUS_IF_ISSUES'].to_i
|
83
|
+
else
|
84
|
+
retval = 0
|
85
|
+
end
|
86
|
+
else
|
87
|
+
retval = [@genuine_issues.size, 255].min
|
88
|
+
end
|
89
|
+
|
90
|
+
system("open #{output_name}") if (output != STDOUT && STDOUT.tty?)
|
91
|
+
|
92
|
+
exit( retval )
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module LintFu
|
2
|
+
module ActionPack
|
3
|
+
class ModelControllerBuilder < ModelElementBuilder
|
4
|
+
SIGNATURE_SEXP = s(:colon2, s(:const, :ActionController), :Base)
|
5
|
+
|
6
|
+
#sexp:: [:class, <classname>, <superclass|nil>, <CLASS DEFS>]
|
7
|
+
def process_class(sexp)
|
8
|
+
return super(sexp) unless sexp[2] && sexp[2] == SIGNATURE_SEXP
|
9
|
+
|
10
|
+
unless @current_model_element
|
11
|
+
@current_model_element = ModelController.new(sexp, self.namespace)
|
12
|
+
did_element = true
|
13
|
+
end
|
14
|
+
|
15
|
+
ret = super(sexp)
|
16
|
+
|
17
|
+
if did_element
|
18
|
+
self.model_elements.push @current_model_element
|
19
|
+
@current_model_element = nil
|
20
|
+
end
|
21
|
+
|
22
|
+
return ret
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module LintFu
|
2
|
+
module ActiveRecord
|
3
|
+
class ModelModel
|
4
|
+
include LintFu::ModelElement
|
5
|
+
|
6
|
+
attr_reader :associations
|
7
|
+
attr_reader :named_scopes
|
8
|
+
attr_writer :paranoid
|
9
|
+
|
10
|
+
def initialize(sexp, namespace=nil)
|
11
|
+
super(sexp, namespace)
|
12
|
+
@associations = {}
|
13
|
+
@named_scopes = {}
|
14
|
+
end
|
15
|
+
|
16
|
+
def paranoid?
|
17
|
+
!!@paranoid
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module LintFu
|
2
|
+
module ActiveRecord
|
3
|
+
class ModelModelBuilder < ModelElementBuilder
|
4
|
+
SIGNATURE_SEXP = s(:colon2, s(:const, :ActiveRecord), :Base)
|
5
|
+
|
6
|
+
SINGULAR_ASSOCS = Set.new([:belongs_to, :has_one])
|
7
|
+
PLURAL_ASSOCS = Set.new([:has_many, :has_and_belongs_to_many])
|
8
|
+
ASSOCS = SINGULAR_ASSOCS + PLURAL_ASSOCS
|
9
|
+
|
10
|
+
#sexp:: [:class, <classname>, <superclass|nil>, <CLASS DEFS>]
|
11
|
+
def process_class(sexp)
|
12
|
+
return super(sexp) unless sexp[2] && sexp[2] == SIGNATURE_SEXP
|
13
|
+
|
14
|
+
unless @current_model_element
|
15
|
+
@current_model_element = ModelModel.new(sexp, self.namespace)
|
16
|
+
did_element = true
|
17
|
+
end
|
18
|
+
|
19
|
+
ret = super(sexp)
|
20
|
+
|
21
|
+
if did_element
|
22
|
+
self.model_elements.push @current_model_element
|
23
|
+
@current_model_element = nil
|
24
|
+
end
|
25
|
+
|
26
|
+
return ret
|
27
|
+
end
|
28
|
+
|
29
|
+
#s(:call, nil, :belongs_to, s(:arglist, s(:lit, :relation_name)))
|
30
|
+
def process_call(sexp)
|
31
|
+
return sexp unless @current_model_element
|
32
|
+
|
33
|
+
callee, method, arglist = sexp[1], sexp[2], sexp[3]
|
34
|
+
arglist = nil unless arglist[0] == :arglist
|
35
|
+
discover_associations(callee, method, arglist)
|
36
|
+
discover_named_scopes(callee, method, arglist)
|
37
|
+
discover_paranoia(callee, method, arglist)
|
38
|
+
return sexp
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
def discover_associations(callee, method, arglist)
|
43
|
+
#Is the call declaring an ActiveRecord association?
|
44
|
+
if (callee == nil) && ASSOCS.include?(method) && arglist
|
45
|
+
assoc_name = arglist[1].to_ruby
|
46
|
+
|
47
|
+
# TODO grok :class_name option
|
48
|
+
if SINGULAR_ASSOCS.include?(method)
|
49
|
+
assoc_class_name = assoc_name.to_s.camelize
|
50
|
+
else
|
51
|
+
assoc_class_name = assoc_name.to_s.singularize.camelize
|
52
|
+
end
|
53
|
+
|
54
|
+
@current_model_element.associations[assoc_name] = assoc_class_name
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def discover_named_scopes(callee, method, arglist)
|
59
|
+
#Is the call declaring a named scope?
|
60
|
+
if (callee == nil && method == :named_scope) && arglist
|
61
|
+
scope_name = arglist[1].to_ruby
|
62
|
+
scope_args = arglist[2..-1].to_ruby(:partial=>nil)
|
63
|
+
@current_model_element.named_scopes[scope_name] = scope_args
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def discover_paranoia(callee, method, arglist)
|
68
|
+
@current_model_element.paranoid = true if (method == :acts_as_paranoid)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module LintFu
|
2
|
+
class Issue
|
3
|
+
attr_reader :file, :sexp, :confidence
|
4
|
+
|
5
|
+
def initialize(scan, file, sexp, confidence=1.0)
|
6
|
+
@scan = scan
|
7
|
+
@file = file
|
8
|
+
@sexp = sexp.deep_clone
|
9
|
+
self.confidence = confidence
|
10
|
+
end
|
11
|
+
|
12
|
+
def line
|
13
|
+
@sexp.line
|
14
|
+
end
|
15
|
+
|
16
|
+
def relative_file
|
17
|
+
File.relative_path(@scan.fs_root, file)
|
18
|
+
end
|
19
|
+
|
20
|
+
def brief
|
21
|
+
self.class.name.split('::')[-1].underscore.gsub('_', ' ').titleize
|
22
|
+
end
|
23
|
+
|
24
|
+
def detail
|
25
|
+
"There is an issue at #{file_basename}:#{line}."
|
26
|
+
end
|
27
|
+
|
28
|
+
def reference_info
|
29
|
+
"No reference information is available for #{brief}."
|
30
|
+
end
|
31
|
+
|
32
|
+
def file_basename
|
33
|
+
File.basename(file)
|
34
|
+
end
|
35
|
+
|
36
|
+
def issue_hash()
|
37
|
+
Digest::SHA1.hexdigest("#{self.class.name} - #{self.relative_file} - #{sexp.fingerprint}")
|
38
|
+
end
|
39
|
+
|
40
|
+
def confidence=(confidence)
|
41
|
+
raise ArgumentError, "Confidence must be a real number in the range (0..1)" unless (0..1).include?(confidence)
|
42
|
+
@confidence = confidence
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module LintFu
|
2
|
+
module Mixins
|
3
|
+
module FileClassMethods
|
4
|
+
def relative_path(base, path)
|
5
|
+
base = File.expand_path(base)
|
6
|
+
path = File.expand_path(path)
|
7
|
+
raise Errno::ENOENT unless path.index(base) == 0
|
8
|
+
return path[base.length+1..-1]
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class <<File
|
15
|
+
include LintFu::Mixins::FileClassMethods
|
16
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
module LintFu
|
2
|
+
module Mixins
|
3
|
+
module SexpInstanceMethods
|
4
|
+
def deep_clone
|
5
|
+
Marshal.load(Marshal.dump(self))
|
6
|
+
end
|
7
|
+
|
8
|
+
#Return a version of this Sexp that preserves the structure of the original, but with
|
9
|
+
#any specific names, quantities or other values replaced with nil. The fingerprint of
|
10
|
+
#a given chunk of code will tend to remain the same over time, even if variable names
|
11
|
+
#or other inconsequential details are changed.
|
12
|
+
#TODO actually implement this method
|
13
|
+
def fingerprint
|
14
|
+
self
|
15
|
+
end
|
16
|
+
|
17
|
+
#Generate a human-readable description for this sexp that is similar to source code.
|
18
|
+
def to_ruby_string
|
19
|
+
Ruby2Ruby.new.process(self.deep_clone)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Translate a sexp containing a Ruby data literal (string, int, array, hash, etc) into the equivalent Ruby object.
|
23
|
+
def to_ruby(options={})
|
24
|
+
typ = self[0]
|
25
|
+
|
26
|
+
case typ
|
27
|
+
when :true
|
28
|
+
return true
|
29
|
+
when :false
|
30
|
+
return false
|
31
|
+
when :lit, :str
|
32
|
+
return self[1]
|
33
|
+
when :array, :arglist
|
34
|
+
return self[1..-1].collect { |x| x.to_ruby(options) }
|
35
|
+
when :hash
|
36
|
+
result = {}
|
37
|
+
key, value = nil, nil
|
38
|
+
flipflop = false
|
39
|
+
self[1..-1].each do |token|
|
40
|
+
if flipflop
|
41
|
+
value = token
|
42
|
+
result[key.to_ruby(options)] = value.to_ruby(options)
|
43
|
+
else
|
44
|
+
key = token
|
45
|
+
end
|
46
|
+
flipflop = !flipflop
|
47
|
+
end
|
48
|
+
return result
|
49
|
+
else
|
50
|
+
return options[:partial] if options.has_key?(:partial)
|
51
|
+
raise StandardError.new("Cannot convert Sexp to Ruby object: " + self.to_s)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def constant?
|
56
|
+
typ = self[0]
|
57
|
+
|
58
|
+
case typ
|
59
|
+
when :true, :false, :lit, :str
|
60
|
+
return true
|
61
|
+
when :array, :arglist
|
62
|
+
self[1..-1].each { |sexp| return false unless sexp.constant? }
|
63
|
+
return true
|
64
|
+
when :hash
|
65
|
+
result = {}
|
66
|
+
key, value = nil, nil
|
67
|
+
flipflop = false
|
68
|
+
self[1..-1].each do |token|
|
69
|
+
if flipflop
|
70
|
+
value = token
|
71
|
+
return false unless key.constant? && value.constant?
|
72
|
+
else
|
73
|
+
key = token
|
74
|
+
end
|
75
|
+
flipflop = !flipflop
|
76
|
+
end
|
77
|
+
return true
|
78
|
+
else
|
79
|
+
return false
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def find_recursively(&test)
|
84
|
+
return self if test.call(self)
|
85
|
+
|
86
|
+
self.each do |child|
|
87
|
+
found = child.find_recursively(&test) if (Sexp === child)
|
88
|
+
return found if found
|
89
|
+
end
|
90
|
+
|
91
|
+
return nil
|
92
|
+
end
|
93
|
+
|
94
|
+
def find_all_recursively(results=nil, &test)
|
95
|
+
results ||= []
|
96
|
+
results << self if test.call(self)
|
97
|
+
|
98
|
+
self.each do |child|
|
99
|
+
child.find_all_recursively(results, &test) if (Sexp === child)
|
100
|
+
end
|
101
|
+
|
102
|
+
return nil if results.empty?
|
103
|
+
return results
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
Sexp.instance_eval do
|
110
|
+
include LintFu::Mixins::SexpInstanceMethods
|
111
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module LintFu
|
2
|
+
# An element of a static analysis model that contains, or consists of, submodels. For instance,
|
3
|
+
# an Application might consists of Models, Controllers and Views.
|
4
|
+
module SuperModel
|
5
|
+
def submodels
|
6
|
+
return [].freeze unless @submodels
|
7
|
+
@submodels.dup.freeze
|
8
|
+
end
|
9
|
+
|
10
|
+
def each_submodel(&block)
|
11
|
+
@submodels ||= Set.new()
|
12
|
+
@submodels.each(&block)
|
13
|
+
end
|
14
|
+
|
15
|
+
def add_submodel(sub)
|
16
|
+
@submodels ||= Set.new()
|
17
|
+
@submodels << sub
|
18
|
+
end
|
19
|
+
|
20
|
+
def remove_submodel(sub)
|
21
|
+
@submodels ||= Set.new()
|
22
|
+
@submodels.delete sub
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# An element of the static analysis model being created; generally corresponds to a
|
27
|
+
# class (e.g. model, controller or view) within the application being scanned.
|
28
|
+
module ModelElement
|
29
|
+
attr_accessor :supermodel
|
30
|
+
attr_reader :modeled_class_name, :modeled_class_superclass_name, :parse_tree
|
31
|
+
|
32
|
+
#sexp:: [:class, <classname>, <superclass|nil>, <CLASS DEFS>]
|
33
|
+
#namespace:: Array of enclosing module names for this class
|
34
|
+
def initialize(sexp, namespace=nil)
|
35
|
+
@parse_tree = sexp
|
36
|
+
if namespace
|
37
|
+
@modeled_class_name = namespace.join('::') + (namespace.empty? ? '' : '::') + sexp[1].to_s
|
38
|
+
else
|
39
|
+
@modeled_class_name = sexp[1]
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
#Have a pretty string representation
|
44
|
+
def to_s
|
45
|
+
"<<model of #{modeled_class_name}>>"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module LintFu
|
2
|
+
class ModelElementBuilder < SexpProcessor
|
3
|
+
attr_reader :model_elements, :namespace
|
4
|
+
|
5
|
+
def initialize(namespace=nil)
|
6
|
+
super()
|
7
|
+
@model_elements = []
|
8
|
+
@namespace = namespace || []
|
9
|
+
self.require_empty = false
|
10
|
+
self.auto_shift_type = false
|
11
|
+
end
|
12
|
+
|
13
|
+
#sexp:: [:module, <modulename>, <:scope - MODULE DEFS>]
|
14
|
+
def process_module(sexp)
|
15
|
+
@namespace.push sexp[1]
|
16
|
+
process(sexp[2])
|
17
|
+
@namespace.pop
|
18
|
+
return sexp
|
19
|
+
end
|
20
|
+
|
21
|
+
#sexp:: [:class, <classname>, <superclass|nil>, <:scope - CLASS DEFS>]
|
22
|
+
def process_class(sexp)
|
23
|
+
@namespace.push sexp[1]
|
24
|
+
process(sexp[3])
|
25
|
+
@namespace.pop
|
26
|
+
return sexp
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module LintFu
|
2
|
+
module Parser
|
3
|
+
def self.parse_ruby(filename)
|
4
|
+
contents = File.read(filename)
|
5
|
+
sexp = RubyParser.new.parse(contents)
|
6
|
+
sexp.file = filename
|
7
|
+
return sexp
|
8
|
+
rescue SyntaxError => e
|
9
|
+
e2 = SyntaxError.new "In #{filename}: #{e.message}"
|
10
|
+
e2.set_backtrace(e.backtrace)
|
11
|
+
raise e2
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
module LintFu
|
2
|
+
module Rails
|
3
|
+
class BuggyEagerLoad < Issue
|
4
|
+
def initialize(scan, file, sexp, subject)
|
5
|
+
super(scan, file, sexp)
|
6
|
+
@subject = subject
|
7
|
+
end
|
8
|
+
|
9
|
+
def detail
|
10
|
+
"Instances of the paranoid model <code>#{@subject}</code> are being eager-loaded. This may cause unexpected results."
|
11
|
+
end
|
12
|
+
|
13
|
+
def reference_info
|
14
|
+
return <<EOF
|
15
|
+
h4. What is it?
|
16
|
+
|
17
|
+
A buggy eager load happens when an ActiveRecord finder performs eager loading of a @:has_many@ association and the "target" of the association acts as paranoid.
|
18
|
+
|
19
|
+
The acts_as_paranoid plugin does not correctly eager loads that use a @JOIN@ strategy. If a paranoid model is eager loaded in this way, _all_ models -- even deleted ones -- will be loaded.
|
20
|
+
|
21
|
+
h4. When does it happen?
|
22
|
+
|
23
|
+
A finder with any of the following properties will cause Rails to eager-load using @JOIN@
|
24
|
+
|
25
|
+
* complex @:conditions@ option (containing SQL fragments, or referring to tables using strings)
|
26
|
+
* complex @:order@
|
27
|
+
* complex @:join@
|
28
|
+
* complex @:include@
|
29
|
+
* use of named scopes (which almost always add complex options to the query)
|
30
|
+
|
31
|
+
If your find exhibits any of these properties and it @:include@s a paranoid model, then you have a problem.
|
32
|
+
|
33
|
+
h4. How do I fix it?
|
34
|
+
|
35
|
+
Avoid doing complex finds at the same time you @:include@ a paranoid model.
|
36
|
+
|
37
|
+
EOF
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Visit a Rails controller looking for troublesome stuff
|
42
|
+
class BuggyEagerLoadChecker < Checker
|
43
|
+
FINDER_REGEXP = /^(find|first|all)(_or_initialize)?(_by_.*)?/
|
44
|
+
|
45
|
+
#sexp:: s(:call, <target>, <method_name>, s(:arglist))
|
46
|
+
def observe_call(sexp)
|
47
|
+
return unless finder?(sexp)
|
48
|
+
if finder?(sexp) && (si = spotty_includes(sexp[3]))
|
49
|
+
scan.issues << BuggyEagerLoad.new(scan, self.file, sexp, si.modeled_class_name)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
protected
|
54
|
+
|
55
|
+
def finder?(sexp)
|
56
|
+
!!(sexp[2].to_s =~ FINDER_REGEXP)
|
57
|
+
end
|
58
|
+
|
59
|
+
def spotty_includes(sexp)
|
60
|
+
#transform the sexp (if it's an arglist) into a hash for easier scrutiny
|
61
|
+
arglist = ( sexp && (sexp[0] == :arglist) && sexp.to_ruby(:partial=>nil) )
|
62
|
+
#no dice unless we're looking at an arglist
|
63
|
+
return nil unless arglist
|
64
|
+
|
65
|
+
#no options hash in arglist? no problem!
|
66
|
+
return nil unless (options = arglist.last).is_a?(Hash)
|
67
|
+
|
68
|
+
does_eager_loading = options.has_key?(:include)
|
69
|
+
has_complexity_prone_params =
|
70
|
+
options.has_key?(:conditions) ||
|
71
|
+
options.has_key?(:order) ||
|
72
|
+
options.has_key?(:joins) ||
|
73
|
+
options.has_key?(:group)
|
74
|
+
|
75
|
+
#no eager loading, or no complexity-prone params? no problem!
|
76
|
+
return nil unless does_eager_loading && has_complexity_prone_params
|
77
|
+
|
78
|
+
gather_includes(arglist.last[:include]).each do |inc|
|
79
|
+
plural = inc.to_s
|
80
|
+
singular = plural.singularize
|
81
|
+
class_name = singular.camelize
|
82
|
+
type = self.context.models.detect { |m| m.modeled_class_name == class_name }
|
83
|
+
# If we're eager loading a 1:1 association, don't bother to scream; it's likely
|
84
|
+
# that use the user would want to load the deleted thing anyway.
|
85
|
+
# TODO replace this clever hack, which infers a :has_many association using plurality
|
86
|
+
if !type || ( type.paranoid? && (plural != singular) )
|
87
|
+
return type
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
return nil
|
92
|
+
end
|
93
|
+
|
94
|
+
def gather_includes(data)
|
95
|
+
out = []
|
96
|
+
case data
|
97
|
+
when Array
|
98
|
+
data.each { |elem| out += gather_includes(elem) }
|
99
|
+
when Hash
|
100
|
+
data.keys.each { |elem| out += gather_includes(elem) }
|
101
|
+
data.values.each { |elem| out += gather_includes(elem) }
|
102
|
+
when Symbol
|
103
|
+
out << data
|
104
|
+
end
|
105
|
+
|
106
|
+
return out
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|