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.
Files changed (39) hide show
  1. data/README.rdoc +4 -2
  2. data/bin/lint_fu +2 -86
  3. data/lib/lint_fu/blessing.rb +40 -0
  4. data/lib/lint_fu/checker.rb +49 -3
  5. data/lib/lint_fu/cli/command.rb +48 -0
  6. data/lib/lint_fu/cli/prune.rb +78 -0
  7. data/lib/lint_fu/cli/scan.rb +78 -0
  8. data/lib/lint_fu/cli.rb +52 -0
  9. data/lib/lint_fu/eidos.rb +35 -0
  10. data/lib/lint_fu/{model_element_builder.rb → eidos_builder.rb} +8 -3
  11. data/lib/lint_fu/eidos_container.rb +25 -0
  12. data/lib/lint_fu/file_range.rb +43 -0
  13. data/lib/lint_fu/mixins/sexp_instance_methods.rb +57 -9
  14. data/lib/lint_fu/parser.rb +1 -2
  15. data/lib/lint_fu/plugins/action_pack/controller_eidos.rb +7 -0
  16. data/lib/lint_fu/{action_pack/model_controller_builder.rb → plugins/action_pack/controller_eidos_builder.rb} +6 -6
  17. data/lib/lint_fu/plugins/action_pack.rb +2 -0
  18. data/lib/lint_fu/{active_record/model_model.rb → plugins/active_record/model_eidos.rb} +3 -3
  19. data/lib/lint_fu/{active_record/model_model_builder.rb → plugins/active_record/model_eidos_builder.rb} +17 -11
  20. data/lib/lint_fu/plugins/active_record.rb +2 -0
  21. data/lib/lint_fu/{rails → plugins/rails}/buggy_eager_load_checker.rb +6 -5
  22. data/lib/lint_fu/{rails/scan_builder.rb → plugins/rails/issue_builder.rb} +9 -16
  23. data/lib/lint_fu/plugins/rails/model_application.rb +21 -0
  24. data/lib/lint_fu/plugins/rails/model_application_factory.rb +31 -0
  25. data/lib/lint_fu/{rails → plugins/rails}/sql_injection_checker.rb +9 -5
  26. data/lib/lint_fu/{rails → plugins/rails}/unsafe_find_checker.rb +17 -30
  27. data/lib/lint_fu/plugins/rails.rb +29 -0
  28. data/lib/lint_fu/plugins.rb +11 -0
  29. data/lib/lint_fu/scan.rb +1 -49
  30. data/lib/lint_fu.rb +13 -8
  31. data/lint_fu.gemspec +10 -7
  32. metadata +140 -24
  33. data/lib/lint_fu/action_pack/model_controller.rb +0 -7
  34. data/lib/lint_fu/action_pack.rb +0 -2
  35. data/lib/lint_fu/active_record.rb +0 -2
  36. data/lib/lint_fu/model_element.rb +0 -48
  37. data/lib/lint_fu/rails/model_application.rb +0 -16
  38. data/lib/lint_fu/rails/model_application_builder.rb +0 -32
  39. 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 ModelElementBuilder < SexpProcessor
3
- attr_reader :model_elements, :namespace
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
- @model_elements = []
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 the equivalent Ruby object.
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 StandardError.new("Cannot convert Sexp to Ruby object: " + self.to_s)
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].each { |sexp| return false unless sexp.constant? }
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
@@ -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}"
@@ -0,0 +1,7 @@
1
+ module LintFu::Plugins
2
+ module ActionPack
3
+ class ControllerEidos
4
+ include LintFu::Eidos
5
+ end
6
+ end
7
+ end
@@ -1,22 +1,22 @@
1
- module LintFu
1
+ module LintFu::Plugins
2
2
  module ActionPack
3
- class ModelControllerBuilder < ModelElementBuilder
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 @current_model_element
11
- @current_model_element = ModelController.new(sexp, self.namespace)
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.model_elements.push @current_model_element
19
- @current_model_element = nil
18
+ self.eide.push self.current_model_element
19
+ self.current_model_element = nil
20
20
  end
21
21
 
22
22
  return ret
@@ -0,0 +1,2 @@
1
+ require 'lint_fu/plugins/action_pack/controller_eidos'
2
+ require 'lint_fu/plugins/action_pack/controller_eidos_builder'
@@ -1,7 +1,7 @@
1
- module LintFu
1
+ module LintFu::Plugins
2
2
  module ActiveRecord
3
- class ModelModel
4
- include LintFu::ModelElement
3
+ class ModelEidos
4
+ include LintFu::Eidos
5
5
 
6
6
  attr_reader :associations
7
7
  attr_reader :named_scopes
@@ -1,6 +1,6 @@
1
- module LintFu
1
+ module LintFu::Plugins
2
2
  module ActiveRecord
3
- class ModelModelBuilder < ModelElementBuilder
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
- return super(sexp) unless sexp[2] && sexp[2] == SIGNATURE_SEXP
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 @current_model_element
15
- @current_model_element = ModelModel.new(sexp, self.namespace)
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.model_elements.push @current_model_element
23
- @current_model_element = nil
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 @current_model_element
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
- @current_model_element.associations[assoc_name] = assoc_class_name
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
- @current_model_element.named_scopes[scope_name] = scope_args
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
- @current_model_element.paranoid = true if (method == :acts_as_paranoid)
74
+ self.current_model_element.paranoid = true if (method == :acts_as_paranoid)
69
75
  end
70
76
  end
71
77
  end
@@ -0,0 +1,2 @@
1
+ require 'lint_fu/plugins/active_record/model_eidos'
2
+ require 'lint_fu/plugins/active_record/model_eidos_builder'
@@ -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 ScanBuilder
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 scan(context)
11
- scan = LintFu::Scan.new(@fs_root)
12
-
13
- models_dir = File.join(@fs_root, 'app', 'models')
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
- contents = File.read(filename)
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
- contents = File.read(filename)
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