lint_fu 0.5.0 → 0.5.3
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.
- 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
|