lint_fu 0.5.0 → 0.5.3
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +4 -2
- data/bin/lint_fu +2 -86
- data/lib/lint_fu/blessing.rb +40 -0
- data/lib/lint_fu/checker.rb +49 -3
- data/lib/lint_fu/cli/command.rb +48 -0
- data/lib/lint_fu/cli/prune.rb +78 -0
- data/lib/lint_fu/cli/scan.rb +78 -0
- data/lib/lint_fu/cli.rb +52 -0
- data/lib/lint_fu/eidos.rb +35 -0
- data/lib/lint_fu/{model_element_builder.rb → eidos_builder.rb} +8 -3
- data/lib/lint_fu/eidos_container.rb +25 -0
- data/lib/lint_fu/file_range.rb +43 -0
- data/lib/lint_fu/mixins/sexp_instance_methods.rb +57 -9
- data/lib/lint_fu/parser.rb +1 -2
- data/lib/lint_fu/plugins/action_pack/controller_eidos.rb +7 -0
- data/lib/lint_fu/{action_pack/model_controller_builder.rb → plugins/action_pack/controller_eidos_builder.rb} +6 -6
- data/lib/lint_fu/plugins/action_pack.rb +2 -0
- data/lib/lint_fu/{active_record/model_model.rb → plugins/active_record/model_eidos.rb} +3 -3
- data/lib/lint_fu/{active_record/model_model_builder.rb → plugins/active_record/model_eidos_builder.rb} +17 -11
- data/lib/lint_fu/plugins/active_record.rb +2 -0
- data/lib/lint_fu/{rails → plugins/rails}/buggy_eager_load_checker.rb +6 -5
- data/lib/lint_fu/{rails/scan_builder.rb → plugins/rails/issue_builder.rb} +9 -16
- data/lib/lint_fu/plugins/rails/model_application.rb +21 -0
- data/lib/lint_fu/plugins/rails/model_application_factory.rb +31 -0
- data/lib/lint_fu/{rails → plugins/rails}/sql_injection_checker.rb +9 -5
- data/lib/lint_fu/{rails → plugins/rails}/unsafe_find_checker.rb +17 -30
- data/lib/lint_fu/plugins/rails.rb +29 -0
- data/lib/lint_fu/plugins.rb +11 -0
- data/lib/lint_fu/scan.rb +1 -49
- data/lib/lint_fu.rb +13 -8
- data/lint_fu.gemspec +10 -7
- metadata +140 -24
- data/lib/lint_fu/action_pack/model_controller.rb +0 -7
- data/lib/lint_fu/action_pack.rb +0 -2
- data/lib/lint_fu/active_record.rb +0 -2
- data/lib/lint_fu/model_element.rb +0 -48
- data/lib/lint_fu/rails/model_application.rb +0 -16
- data/lib/lint_fu/rails/model_application_builder.rb +0 -32
- data/lib/lint_fu/rails.rb +0 -6
@@ -0,0 +1,35 @@
|
|
1
|
+
module LintFu
|
2
|
+
# An eidos (plural: eide) holds information about a Ruby class, module or other
|
3
|
+
# relevant piece of code. The name comes from Plato's theory of forms; the
|
4
|
+
# eidos is the universal abstraction of an entire class of objects.
|
5
|
+
#
|
6
|
+
# Eide are built during the first pass of static analysis by parsing all source
|
7
|
+
# files in the project. On the second pass, the checkers parse selected bits of
|
8
|
+
# code, using the eide to better understand the code they are parsing.
|
9
|
+
module Eidos
|
10
|
+
attr_accessor :parent_eidos
|
11
|
+
attr_reader :modeled_class_name, :modeled_class_superclass_name, :parse_tree
|
12
|
+
|
13
|
+
VALID_SEXPS = Set.new([:class, :module])
|
14
|
+
|
15
|
+
#sexp:: [:class, <classname>, <superclass|nil>, <CLASS DEFS>]
|
16
|
+
#namespace:: Array of enclosing module names for this class
|
17
|
+
def initialize(sexp, namespace=nil)
|
18
|
+
unless VALID_SEXPS.include?(sexp[0])
|
19
|
+
raise ArgumentError, "Must be constructed from a class-definition Sexp"
|
20
|
+
end
|
21
|
+
|
22
|
+
@sexp = sexp
|
23
|
+
if namespace
|
24
|
+
@modeled_class_name = namespace.join('::') + (namespace.empty? ? '' : '::') + sexp[1].to_s
|
25
|
+
else
|
26
|
+
@modeled_class_name = sexp[1]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
#Have a pretty string representation
|
31
|
+
def to_s
|
32
|
+
"<<model of #{self.modeled_class_name}>>"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -1,10 +1,11 @@
|
|
1
1
|
module LintFu
|
2
|
-
class
|
3
|
-
attr_reader
|
2
|
+
class EidosBuilder < SexpProcessor
|
3
|
+
attr_reader :namespace, :eide
|
4
|
+
attr_accessor :current_model_element
|
4
5
|
|
5
6
|
def initialize(namespace=nil)
|
6
7
|
super()
|
7
|
-
@
|
8
|
+
@eide = []
|
8
9
|
@namespace = namespace || []
|
9
10
|
self.require_empty = false
|
10
11
|
self.auto_shift_type = false
|
@@ -25,5 +26,9 @@ module LintFu
|
|
25
26
|
@namespace.pop
|
26
27
|
return sexp
|
27
28
|
end
|
29
|
+
|
30
|
+
def build
|
31
|
+
raise NotImplemented
|
32
|
+
end
|
28
33
|
end
|
29
34
|
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module LintFu
|
2
|
+
# An element of a static analysis model that contains, or consists of, eide. For instance,
|
3
|
+
# an Application might consists of Models, Controllers and Views.
|
4
|
+
module EidosContainer
|
5
|
+
def eide
|
6
|
+
return [].freeze unless @eide
|
7
|
+
@eide.dup.freeze
|
8
|
+
end
|
9
|
+
|
10
|
+
def each_eidos(&block)
|
11
|
+
@eide ||= Set.new()
|
12
|
+
@eide.each(&block)
|
13
|
+
end
|
14
|
+
|
15
|
+
def add_eidos(sub)
|
16
|
+
@eide ||= Set.new()
|
17
|
+
@eide << sub
|
18
|
+
end
|
19
|
+
|
20
|
+
def remove_eidos(sub)
|
21
|
+
@eide ||= Set.new()
|
22
|
+
@eide.delete sub
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module LintFu
|
2
|
+
class FileRange
|
3
|
+
attr_reader :filename, :min_line, :max_line
|
4
|
+
|
5
|
+
def initialize(filename, min_line, max_line=nil, content=nil)
|
6
|
+
@filename = File.expand_path(filename)
|
7
|
+
@min_line = min_line
|
8
|
+
@max_line = max_line || min_line
|
9
|
+
@content = content
|
10
|
+
end
|
11
|
+
|
12
|
+
def line
|
13
|
+
min_line
|
14
|
+
end
|
15
|
+
|
16
|
+
def content
|
17
|
+
unless @content
|
18
|
+
#TODO optimize
|
19
|
+
all_content = File.readlines(self.file)
|
20
|
+
|
21
|
+
min_line = [self.min_line - 1, 0].max
|
22
|
+
max_line = [self.max_line - 1, all_content.size - 1].max
|
23
|
+
@content = all_content[min_line..max_line]
|
24
|
+
end
|
25
|
+
|
26
|
+
return @content
|
27
|
+
end
|
28
|
+
|
29
|
+
def ==(other)
|
30
|
+
(self.filename == other.filename) &&
|
31
|
+
(self.min_line == other.min_line) &&
|
32
|
+
(self.max_line == other.max_line)
|
33
|
+
end
|
34
|
+
|
35
|
+
def include?(range)
|
36
|
+
(self.filename == range.filename) &&
|
37
|
+
(self.min_line <= range.min_line) &&
|
38
|
+
(self.max_line >= range.max_line)
|
39
|
+
end
|
40
|
+
|
41
|
+
NONE = FileRange.new('/dev/null', 0)
|
42
|
+
end
|
43
|
+
end
|
@@ -1,25 +1,32 @@
|
|
1
1
|
module LintFu
|
2
|
+
class TranslationFailure < Exception
|
3
|
+
|
4
|
+
end
|
5
|
+
|
2
6
|
module Mixins
|
3
7
|
module SexpInstanceMethods
|
8
|
+
WHOLE_LINE_COMMENT = /^\s*#/
|
9
|
+
|
4
10
|
def deep_clone
|
5
11
|
Marshal.load(Marshal.dump(self))
|
6
12
|
end
|
7
13
|
|
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
|
14
|
+
# Return a version of this Sexp that preserves the structure of the original, but with
|
15
|
+
# any specific names, quantities or other values replaced with nil. The fingerprint of
|
16
|
+
# a given chunk of code will tend to remain the same over time, even if variable names
|
17
|
+
# or other inconsequential details are changed.
|
13
18
|
def fingerprint
|
19
|
+
#TODO actually implement this method
|
14
20
|
self
|
15
21
|
end
|
16
22
|
|
17
|
-
#Generate a human-readable description for this sexp that is similar to source code.
|
23
|
+
# Generate a human-readable description for this sexp that is similar to source code.
|
18
24
|
def to_ruby_string
|
19
25
|
Ruby2Ruby.new.process(self.deep_clone)
|
20
26
|
end
|
21
27
|
|
22
|
-
# Translate a sexp containing a Ruby data literal (string, int, array, hash, etc) into
|
28
|
+
# Translate a sexp containing a Ruby data literal (string, int, array, hash, etc) into
|
29
|
+
# the equivalent Ruby object.
|
23
30
|
def to_ruby(options={})
|
24
31
|
typ = self[0]
|
25
32
|
|
@@ -48,7 +55,7 @@ module LintFu
|
|
48
55
|
return result
|
49
56
|
else
|
50
57
|
return options[:partial] if options.has_key?(:partial)
|
51
|
-
raise
|
58
|
+
raise TranslationFailure.new("Cannot convert Sexp to Ruby object (expression at #{self.file}:#{self.line}): " + self.to_s)
|
52
59
|
end
|
53
60
|
end
|
54
61
|
|
@@ -59,7 +66,9 @@ module LintFu
|
|
59
66
|
when :true, :false, :lit, :str
|
60
67
|
return true
|
61
68
|
when :array, :arglist
|
62
|
-
self[1..-1]
|
69
|
+
contents = self[1..-1]
|
70
|
+
return false if contents.empty?
|
71
|
+
contents.each { |sexp| return false unless sexp.constant? }
|
63
72
|
return true
|
64
73
|
when :hash
|
65
74
|
result = {}
|
@@ -102,6 +111,45 @@ module LintFu
|
|
102
111
|
return nil if results.empty?
|
103
112
|
return results
|
104
113
|
end
|
114
|
+
|
115
|
+
def preceding_comment_range
|
116
|
+
unless @preceding_comment_range
|
117
|
+
cont = File.readlines(self.file)
|
118
|
+
|
119
|
+
comments = ''
|
120
|
+
|
121
|
+
max_line = self.line - 1 - 1
|
122
|
+
max_line = 0 if max_line < 0
|
123
|
+
min_line = max_line
|
124
|
+
|
125
|
+
while cont[min_line] =~ WHOLE_LINE_COMMENT && min_line >= 0
|
126
|
+
min_line -= 1
|
127
|
+
end
|
128
|
+
|
129
|
+
if cont[max_line] =~ WHOLE_LINE_COMMENT
|
130
|
+
min_line +=1 unless min_line == max_line
|
131
|
+
@preceding_comment_range = FileRange.new(self.file, min_line+1, max_line+1, cont[min_line..max_line])
|
132
|
+
else
|
133
|
+
@preceding_comment_range = FileRange::NONE
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
if @preceding_comment_range == FileRange::NONE
|
138
|
+
return nil
|
139
|
+
else
|
140
|
+
return @preceding_comment_range
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def preceding_comments
|
145
|
+
range = preceding_comment_range
|
146
|
+
return [] unless range
|
147
|
+
return range.content
|
148
|
+
end
|
149
|
+
|
150
|
+
def blessings
|
151
|
+
@blessings ||= Blessing.parse(self.preceding_comments, self)
|
152
|
+
end
|
105
153
|
end
|
106
154
|
end
|
107
155
|
end
|
data/lib/lint_fu/parser.rb
CHANGED
@@ -2,8 +2,7 @@ module LintFu
|
|
2
2
|
module Parser
|
3
3
|
def self.parse_ruby(filename)
|
4
4
|
contents = File.read(filename)
|
5
|
-
sexp = RubyParser.new.parse(contents)
|
6
|
-
sexp.file = filename
|
5
|
+
sexp = RubyParser.new.parse(contents, filename)
|
7
6
|
return sexp
|
8
7
|
rescue SyntaxError => e
|
9
8
|
e2 = SyntaxError.new "In #{filename}: #{e.message}"
|
@@ -1,22 +1,22 @@
|
|
1
|
-
module LintFu
|
1
|
+
module LintFu::Plugins
|
2
2
|
module ActionPack
|
3
|
-
class
|
3
|
+
class ControllerEidosBuilder < LintFu::EidosBuilder
|
4
4
|
SIGNATURE_SEXP = s(:colon2, s(:const, :ActionController), :Base)
|
5
5
|
|
6
6
|
#sexp:: [:class, <classname>, <superclass|nil>, <CLASS DEFS>]
|
7
7
|
def process_class(sexp)
|
8
8
|
return super(sexp) unless sexp[2] && sexp[2] == SIGNATURE_SEXP
|
9
9
|
|
10
|
-
unless
|
11
|
-
|
10
|
+
unless self.current_model_element
|
11
|
+
self.current_model_element = ControllerEidos.new(sexp, self.namespace)
|
12
12
|
did_element = true
|
13
13
|
end
|
14
14
|
|
15
15
|
ret = super(sexp)
|
16
16
|
|
17
17
|
if did_element
|
18
|
-
self.
|
19
|
-
|
18
|
+
self.eide.push self.current_model_element
|
19
|
+
self.current_model_element = nil
|
20
20
|
end
|
21
21
|
|
22
22
|
return ret
|
@@ -1,6 +1,6 @@
|
|
1
|
-
module LintFu
|
1
|
+
module LintFu::Plugins
|
2
2
|
module ActiveRecord
|
3
|
-
class
|
3
|
+
class ModelEidosBuilder < LintFu::EidosBuilder
|
4
4
|
SIGNATURE_SEXP = s(:colon2, s(:const, :ActiveRecord), :Base)
|
5
5
|
|
6
6
|
SINGULAR_ASSOCS = Set.new([:belongs_to, :has_one])
|
@@ -9,18 +9,20 @@ module LintFu
|
|
9
9
|
|
10
10
|
#sexp:: [:class, <classname>, <superclass|nil>, <CLASS DEFS>]
|
11
11
|
def process_class(sexp)
|
12
|
-
|
12
|
+
#always assume everything we see is a model
|
13
|
+
#cheap way to deal with model class hierarchy!
|
14
|
+
#return super(sexp) unless sexp[2] && sexp[2] == SIGNATURE_SEXP
|
13
15
|
|
14
|
-
unless
|
15
|
-
|
16
|
+
unless self.current_model_element
|
17
|
+
self.current_model_element = ModelEidos.new(sexp, self.namespace)
|
16
18
|
did_element = true
|
17
19
|
end
|
18
20
|
|
19
21
|
ret = super(sexp)
|
20
22
|
|
21
23
|
if did_element
|
22
|
-
self.
|
23
|
-
|
24
|
+
self.eide.push self.current_model_element
|
25
|
+
self.current_model_element = nil
|
24
26
|
end
|
25
27
|
|
26
28
|
return ret
|
@@ -28,7 +30,7 @@ module LintFu
|
|
28
30
|
|
29
31
|
#s(:call, nil, :belongs_to, s(:arglist, s(:lit, :relation_name)))
|
30
32
|
def process_call(sexp)
|
31
|
-
return sexp unless
|
33
|
+
return sexp unless self.current_model_element
|
32
34
|
|
33
35
|
callee, method, arglist = sexp[1], sexp[2], sexp[3]
|
34
36
|
arglist = nil unless arglist[0] == :arglist
|
@@ -51,8 +53,10 @@ module LintFu
|
|
51
53
|
assoc_class_name = assoc_name.to_s.singularize.camelize
|
52
54
|
end
|
53
55
|
|
54
|
-
|
56
|
+
self.current_model_element.associations[assoc_name] = assoc_class_name
|
55
57
|
end
|
58
|
+
rescue LintFu::TranslationFailure => e
|
59
|
+
# TODO: log a warning
|
56
60
|
end
|
57
61
|
|
58
62
|
def discover_named_scopes(callee, method, arglist)
|
@@ -60,12 +64,14 @@ module LintFu
|
|
60
64
|
if (callee == nil && method == :named_scope) && arglist
|
61
65
|
scope_name = arglist[1].to_ruby
|
62
66
|
scope_args = arglist[2..-1].to_ruby(:partial=>nil)
|
63
|
-
|
67
|
+
self.current_model_element.named_scopes[scope_name] = scope_args
|
64
68
|
end
|
69
|
+
rescue LintFu::TranslationFailure => e
|
70
|
+
# TODO: log a warning
|
65
71
|
end
|
66
72
|
|
67
73
|
def discover_paranoia(callee, method, arglist)
|
68
|
-
|
74
|
+
self.current_model_element.paranoid = true if (method == :acts_as_paranoid)
|
69
75
|
end
|
70
76
|
end
|
71
77
|
end
|
@@ -1,6 +1,6 @@
|
|
1
|
-
module LintFu
|
1
|
+
module LintFu::Plugins
|
2
2
|
module Rails
|
3
|
-
class BuggyEagerLoad < Issue
|
3
|
+
class BuggyEagerLoad < LintFu::Issue
|
4
4
|
def initialize(scan, file, sexp, subject)
|
5
5
|
super(scan, file, sexp)
|
6
6
|
@subject = subject
|
@@ -16,7 +16,7 @@ h4. What is it?
|
|
16
16
|
|
17
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
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.
|
19
|
+
The acts_as_paranoid plugin does not correctly handle 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
20
|
|
21
21
|
h4. When does it happen?
|
22
22
|
|
@@ -39,13 +39,14 @@ EOF
|
|
39
39
|
end
|
40
40
|
|
41
41
|
# Visit a Rails controller looking for troublesome stuff
|
42
|
-
class BuggyEagerLoadChecker < Checker
|
42
|
+
class BuggyEagerLoadChecker < LintFu::Checker
|
43
43
|
FINDER_REGEXP = /^(find|first|all)(_or_initialize)?(_by_.*)?/
|
44
44
|
|
45
45
|
#sexp:: s(:call, <target>, <method_name>, s(:arglist))
|
46
46
|
def observe_call(sexp)
|
47
|
+
super(sexp)
|
47
48
|
return unless finder?(sexp)
|
48
|
-
if finder?(sexp) && (si = spotty_includes(sexp[3]))
|
49
|
+
if finder?(sexp) && (si = spotty_includes(sexp[3])) && !suppressed?(UnsafeFind)
|
49
50
|
scan.issues << BuggyEagerLoad.new(scan, self.file, sexp, si.modeled_class_name)
|
50
51
|
end
|
51
52
|
end
|
@@ -1,24 +1,20 @@
|
|
1
|
-
module LintFu
|
1
|
+
module LintFu::Plugins
|
2
2
|
module Rails
|
3
|
-
class
|
3
|
+
class IssueBuilder
|
4
4
|
attr_reader :fs_root
|
5
5
|
|
6
6
|
def initialize(fs_root)
|
7
7
|
@fs_root = fs_root
|
8
8
|
end
|
9
9
|
|
10
|
-
def
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
controllers_dir = File.join(@fs_root, 'app', 'controllers')
|
15
|
-
views_dir = File.join(@fs_root, 'app', 'views')
|
10
|
+
def build(context, scan)
|
11
|
+
models_dir = File.join(scan.fs_root, 'app', 'models')
|
12
|
+
controllers_dir = File.join(scan.fs_root, 'app', 'controllers')
|
13
|
+
views_dir = File.join(scan.fs_root, 'app', 'views')
|
16
14
|
|
17
15
|
#Scan controllers
|
18
16
|
Dir.glob(File.join(controllers_dir, '**', '*.rb')).each do |filename|
|
19
|
-
|
20
|
-
parser = RubyParser.new
|
21
|
-
sexp = parser.parse(contents, filename)
|
17
|
+
sexp = LintFu::Parser.parse_ruby(filename)
|
22
18
|
visitor = LintFu::Visitor.new
|
23
19
|
visitor.observers << BuggyEagerLoadChecker.new(scan, context, filename)
|
24
20
|
visitor.observers << SqlInjectionChecker.new(scan, context, filename)
|
@@ -28,15 +24,12 @@ module LintFu
|
|
28
24
|
|
29
25
|
#Scan models
|
30
26
|
Dir.glob(File.join(models_dir, '**', '*.rb')).each do |filename|
|
31
|
-
|
32
|
-
parser = RubyParser.new
|
33
|
-
sexp = parser.parse(contents, filename)
|
27
|
+
sexp = LintFu::Parser.parse_ruby(filename)
|
34
28
|
visitor = LintFu::Visitor.new
|
35
29
|
visitor.observers << SqlInjectionChecker.new(scan, context, filename, 0.2)
|
30
|
+
visitor.observers << UnsafeFindChecker.new(scan, context, filename)
|
36
31
|
visitor.process(sexp)
|
37
32
|
end
|
38
|
-
|
39
|
-
return scan
|
40
33
|
end
|
41
34
|
end
|
42
35
|
end
|