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.
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