wool 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (88) hide show
  1. data/.document +5 -0
  2. data/.gitignore +23 -0
  3. data/LICENSE +45 -0
  4. data/README.rdoc +17 -0
  5. data/Rakefile +77 -0
  6. data/TODO.md +17 -0
  7. data/VERSION +1 -0
  8. data/bin/wool +4 -0
  9. data/features/step_definitions/wool_steps.rb +39 -0
  10. data/features/support/env.rb +14 -0
  11. data/features/support/testdata/1_input +1 -0
  12. data/features/support/testdata/1_output +1 -0
  13. data/features/support/testdata/2_input +4 -0
  14. data/features/support/testdata/2_output +4 -0
  15. data/features/support/testdata/3_input +8 -0
  16. data/features/support/testdata/3_output +11 -0
  17. data/features/support/testdata/4_input +5 -0
  18. data/features/support/testdata/4_output +5 -0
  19. data/features/wool.feature +24 -0
  20. data/lib/wool.rb +40 -0
  21. data/lib/wool/advice/advice.rb +42 -0
  22. data/lib/wool/advice/comment_advice.rb +37 -0
  23. data/lib/wool/analysis/annotations.rb +34 -0
  24. data/lib/wool/analysis/annotations/next_annotation.rb +26 -0
  25. data/lib/wool/analysis/annotations/parent_annotation.rb +20 -0
  26. data/lib/wool/analysis/annotations/scope_annotation.rb +37 -0
  27. data/lib/wool/analysis/lexical_analysis.rb +165 -0
  28. data/lib/wool/analysis/protocol_registry.rb +32 -0
  29. data/lib/wool/analysis/protocols.rb +82 -0
  30. data/lib/wool/analysis/scope.rb +13 -0
  31. data/lib/wool/analysis/sexp_analysis.rb +98 -0
  32. data/lib/wool/analysis/signature.rb +16 -0
  33. data/lib/wool/analysis/symbol.rb +10 -0
  34. data/lib/wool/analysis/visitor.rb +36 -0
  35. data/lib/wool/analysis/wool_class.rb +47 -0
  36. data/lib/wool/rake/task.rb +42 -0
  37. data/lib/wool/runner.rb +156 -0
  38. data/lib/wool/scanner.rb +160 -0
  39. data/lib/wool/support/module_extensions.rb +84 -0
  40. data/lib/wool/third_party/trollop.rb +845 -0
  41. data/lib/wool/warning.rb +145 -0
  42. data/lib/wool/warnings/comment_spacing.rb +30 -0
  43. data/lib/wool/warnings/extra_blank_lines.rb +29 -0
  44. data/lib/wool/warnings/extra_whitespace.rb +15 -0
  45. data/lib/wool/warnings/line_length.rb +113 -0
  46. data/lib/wool/warnings/misaligned_unindentation.rb +16 -0
  47. data/lib/wool/warnings/operator_spacing.rb +63 -0
  48. data/lib/wool/warnings/rescue_exception.rb +41 -0
  49. data/lib/wool/warnings/semicolon.rb +24 -0
  50. data/lib/wool/warnings/useless_double_quotes.rb +37 -0
  51. data/spec/advice_specs/advice_spec.rb +69 -0
  52. data/spec/advice_specs/comment_advice_spec.rb +38 -0
  53. data/spec/advice_specs/spec_helper.rb +1 -0
  54. data/spec/analysis_specs/annotations_specs/next_prev_annotation_spec.rb +47 -0
  55. data/spec/analysis_specs/annotations_specs/parent_annotation_spec.rb +41 -0
  56. data/spec/analysis_specs/annotations_specs/spec_helper.rb +5 -0
  57. data/spec/analysis_specs/lexical_analysis_spec.rb +179 -0
  58. data/spec/analysis_specs/protocol_registry_spec.rb +58 -0
  59. data/spec/analysis_specs/protocols_spec.rb +49 -0
  60. data/spec/analysis_specs/scope_spec.rb +20 -0
  61. data/spec/analysis_specs/sexp_analysis_spec.rb +134 -0
  62. data/spec/analysis_specs/spec_helper.rb +2 -0
  63. data/spec/analysis_specs/visitor_spec.rb +53 -0
  64. data/spec/analysis_specs/wool_class_spec.rb +54 -0
  65. data/spec/rake_specs/spec_helper.rb +1 -0
  66. data/spec/rake_specs/task_spec.rb +67 -0
  67. data/spec/runner_spec.rb +171 -0
  68. data/spec/scanner_spec.rb +75 -0
  69. data/spec/spec.opts +1 -0
  70. data/spec/spec_helper.rb +93 -0
  71. data/spec/support_specs/module_extensions_spec.rb +91 -0
  72. data/spec/support_specs/spec_helper.rb +1 -0
  73. data/spec/warning_spec.rb +95 -0
  74. data/spec/warning_specs/comment_spacing_spec.rb +57 -0
  75. data/spec/warning_specs/extra_blank_lines_spec.rb +70 -0
  76. data/spec/warning_specs/extra_whitespace_spec.rb +33 -0
  77. data/spec/warning_specs/line_length_spec.rb +165 -0
  78. data/spec/warning_specs/misaligned_unindentation_spec.rb +35 -0
  79. data/spec/warning_specs/operator_spacing_spec.rb +101 -0
  80. data/spec/warning_specs/rescue_exception_spec.rb +105 -0
  81. data/spec/warning_specs/semicolon_spec.rb +58 -0
  82. data/spec/warning_specs/spec_helper.rb +1 -0
  83. data/spec/warning_specs/useless_double_quotes_spec.rb +62 -0
  84. data/spec/wool_spec.rb +8 -0
  85. data/status_reports/2010/12/2010-12-14.md +163 -0
  86. data/test/third_party_tests/test_trollop.rb +1181 -0
  87. data/wool.gemspec +173 -0
  88. metadata +235 -0
@@ -0,0 +1,145 @@
1
+ module Wool
2
+ class Warning < Struct.new(:name, :file, :body, :line_number, :severity)
3
+ extend Advice
4
+ extend ModuleExtensions
5
+ include LexicalAnalysis
6
+ include SexpAnalysis
7
+
8
+ cattr_accessor :short_name
9
+ cattr_accessor_with_default :match_filters, []
10
+ cattr_get_and_setter :severity, :short_desc, :desc
11
+ attr_accessor :settings
12
+
13
+ desc { "#{self.class.name} #{file}:#{line_number} (#{severity})" }
14
+
15
+ # This tracks all subclasses (and subclasses of subclasses, etc). Plus, this
16
+ # method is inherited, so Wool::LineWarning.all_subclasses will have all
17
+ # subclasses of Wool::LineWarning!
18
+ def self.all_warnings
19
+ @all_warnings ||= [self]
20
+ end
21
+
22
+ # Returns all "concrete" warnings, that is, those that have an actual
23
+ # implementation. No meta-warnings like FileWarning/LineWarning.
24
+ #
25
+ # @return [Array<Class>] the concrete warnings you might want to use
26
+ def self.concrete_warnings
27
+ all_warnings - [self, FileWarning, LineWarning]
28
+ end
29
+
30
+ # All types should be shared and modified by *all* subclasses. This makes
31
+ # Wool::Warning.all_types a global registry.
32
+ def self.all_types
33
+ @@all_types ||= Hash.new {|h,k| h[k] = []}
34
+ end
35
+
36
+ # When a Warning subclass is subclassed, store the subclass and inform the
37
+ # next superclass up the inheritance hierarchy.
38
+ def self.inherited(klass)
39
+ self.all_warnings << klass
40
+ next_klass = self.superclass
41
+ while next_klass != Wool::Warning.superclass
42
+ next_klass.send(:inherited, klass)
43
+ next_klass = next_klass.superclass
44
+ end
45
+ end
46
+
47
+ # Override in subclasses to provide a list of options to send to Trollop
48
+ def self.options
49
+ @options ||= [:debug, "Shows debug output from wool's scanner", {:short => '-d'}]
50
+ end
51
+
52
+ # Adds an option in Trollop format.
53
+ def self.opt(*args)
54
+ self.options << args
55
+ end
56
+
57
+ # Modified cattr_get_and_setter that updates the class's short_name and
58
+ # registers the class as a member of the given type.
59
+ def self.type(*args)
60
+ if args.any?
61
+ @type = args.first.to_s
62
+ all_types[@type] << self
63
+ self.short_name = @type[0,2].upcase + all_types[@type].size.to_s
64
+ else
65
+ @type
66
+ end
67
+ end
68
+
69
+ # Adds an instance method that extracts a key from the settings of
70
+ # the warning. This is a simple way of storing metadata about the
71
+ # discovered error/issue for presentational purposes.
72
+ def self.setting_accessor(*syms)
73
+ syms.each { |sym| class_eval("def #{sym}\n @settings[#{sym.inspect}]\nend") }
74
+ end
75
+
76
+ # Default initializer.
77
+ def initialize(file, body, settings={})
78
+ super(self.class.short_desc, file, body, 0, self.class.severity)
79
+ @settings = settings
80
+ end
81
+
82
+ def match?(body = self.body)
83
+ false
84
+ end
85
+
86
+ def generated_warnings(*args)
87
+ case match_result = match?(*args)
88
+ when Array then match_result
89
+ when false, nil then []
90
+ else [self]
91
+ end
92
+ end
93
+
94
+ def fix
95
+ self.body
96
+ end
97
+
98
+ def fixable?
99
+ self.fix != self.body rescue false
100
+ end
101
+
102
+ def desc
103
+ case desc = self.class.desc
104
+ when String then desc
105
+ when Proc then instance_eval(&self.class.desc)
106
+ end
107
+ end
108
+
109
+ def indent(string, amt=nil)
110
+ amt ||= self.body.match(/^(\s*)/)[1].size
111
+ ' ' * amt + string.lstrip
112
+ end
113
+
114
+ def count_occurrences(string, substring)
115
+ count = 0
116
+ 0.upto(string.size - substring.size) do |start|
117
+ if string[start,substring.size] == substring
118
+ count += 1
119
+ end
120
+ end
121
+ count
122
+ end
123
+
124
+ def get_indent(line = self.body)
125
+ line =~ /^(\s*).*$/ ? $1 : ''
126
+ end
127
+ end
128
+
129
+ class LineWarning < Warning
130
+ alias_method :line, :body
131
+ def self.options
132
+ @options ||= []
133
+ end
134
+ end
135
+
136
+ class FileWarning < Warning
137
+ def self.options
138
+ @options ||= []
139
+ end
140
+ end
141
+ end
142
+
143
+ Dir[File.expand_path(File.join(File.dirname(__FILE__), 'warnings', '**', '*.rb'))].each do |file|
144
+ load file
145
+ end
@@ -0,0 +1,30 @@
1
+ # Warning for insufficient space between inline comments and code
2
+ class Wool::InlineCommentSpaceWarning < Wool::LineWarning
3
+ OPTION_KEY = :inline_comment_space
4
+ DEFAULT_SPACE = 2
5
+ type :style
6
+ short_desc 'Inline comment spacing error'
7
+ desc { "Inline comments must be at least #{@settings[OPTION_KEY]} spaces from code." }
8
+ opt OPTION_KEY, 'Number of spaces between code and inline comments', :default => DEFAULT_SPACE
9
+
10
+ def match?(line = self.body)
11
+ return false unless comment_token = find_token(:on_comment)
12
+ comment_pos = comment_token.col - 1
13
+ left_of_comment = line[0..comment_pos]
14
+ stripped = left_of_comment.rstrip
15
+ return false if stripped.empty?
16
+ padding_size = left_of_comment.size - stripped.size
17
+ return spacing != padding_size
18
+ end
19
+
20
+ def fix
21
+ comment_token = find_token(:on_comment)
22
+ comment_pos = comment_token.col - 1
23
+ left_of_comment = line[0..comment_pos].rstrip
24
+ left_of_comment + (' ' * spacing) + comment_token.body
25
+ end
26
+
27
+ def spacing
28
+ @settings[OPTION_KEY] || DEFAULT_SPACE
29
+ end
30
+ end
@@ -0,0 +1,29 @@
1
+ # Warning for using semicolons outside of class declarations.
2
+ class Wool::ExtraBlankLinesWarning < Wool::FileWarning
3
+ EXTRA_LINE = /\n[\t ]*\Z/
4
+ type :style
5
+ severity 1
6
+ short_desc 'Extra blank lines'
7
+ desc { "This file has #{count_extra_lines} blank lines at the end of it." }
8
+
9
+ def match?(body = self.body)
10
+ body =~ EXTRA_LINE
11
+ end
12
+
13
+ def fix
14
+ body.gsub(/\s*\Z/, '')
15
+ end
16
+
17
+ # Counts how many extra lines there are at the end of the file.
18
+ def count_extra_lines
19
+ # We use this logic because #lines ignores blank lines at the end, and
20
+ # split(/\n/) does as well.
21
+ count = 0
22
+ working_body = self.body.dup
23
+ while working_body =~ EXTRA_LINE
24
+ working_body.sub!(EXTRA_LINE, '')
25
+ count += 1
26
+ end
27
+ count
28
+ end
29
+ end
@@ -0,0 +1,15 @@
1
+ # Warning for having extra space at the end of a line.
2
+ class Wool::ExtraWhitespaceWarning < Wool::LineWarning
3
+ type :style
4
+ severity 2
5
+ short_desc 'Extra Whitespace'
6
+ desc 'The line has trailing whitespace.'
7
+
8
+ def match?(body = self.body)
9
+ /\s+$/ === line
10
+ end
11
+
12
+ def fix
13
+ self.line.gsub(/\s+$/, '')
14
+ end
15
+ end
@@ -0,0 +1,113 @@
1
+ class Wool::GenericLineLengthWarning < Wool::LineWarning
2
+ cattr_accessor_with_default :line_length_limit, 80000
3
+ type :style
4
+ short_desc 'Line too long'
5
+ desc { "Line length: #{line.size} > #{self.class.line_length_limit} (max)" }
6
+
7
+ def self.inspect
8
+ "Wool::GenericLineLengthWarning<#{line_length_limit}>"
9
+ end
10
+
11
+ def match?(body = self.body)
12
+ !!(line.rstrip.size > self.class.line_length_limit)
13
+ end
14
+
15
+ def fix(content_stack = nil)
16
+ result = handle_long_comments(self.line)
17
+ return result if result
18
+ result = try_to_fix_guarded_lines(self.line)
19
+ return result if result
20
+ self.line
21
+ end
22
+
23
+ def try_to_fix_guarded_lines(line)
24
+ return nil if line !~ /\b(if|unless)\s/ # quick fast check
25
+ code, guard = split_on_keyword(:if, :unless)
26
+ code.rstrip!
27
+ return nil if code.empty? || guard.empty? || code.strip == 'end'
28
+ # check guard for closing braces
29
+ return nil if count_occurrences(guard, '}') != count_occurrences(guard, '{')
30
+ indent = get_indent(line)
31
+ indent_unit = ' ' * @settings[:indent_size]
32
+
33
+ result = code
34
+ until guard.empty?
35
+ condition = indent + guard.strip
36
+ body = result.split(/\n/).map { |line| indent_unit + line}.join("\n")
37
+ new_condition, guard = split_on_keyword(condition[indent.size+1..-1], :if, :unless)
38
+ if new_condition.empty?
39
+ new_condition, guard = guard.rstrip, ''
40
+ else
41
+ new_condition = "#{condition[indent.size,1]}#{new_condition.rstrip}"
42
+ end
43
+ condition = indent + new_condition unless guard.empty?
44
+ result = condition + "\n" + body + "\n" + indent + 'end'
45
+ end
46
+
47
+ result
48
+ end
49
+
50
+ def handle_long_comments(line)
51
+ code, comment = split_on_token(line, :on_comment)
52
+ return nil if comment.empty?
53
+ indent, code = code.match(/^(\s*)(.*)$/)[1..2]
54
+ hashes, comment = comment.match(/^(#+\s*)(.*)$/)[1..2]
55
+ comment_cleaned = fix_long_comment(indent + hashes + comment)
56
+ code_cleaned = !code.strip.empty? ? "\n" + indent + code.rstrip : ''
57
+ comment_cleaned + code_cleaned
58
+ end
59
+
60
+ def fix_long_comment(text)
61
+ # Must have no leading text
62
+ return nil unless text =~ /^(\s*)(#+\s*)(.*)\Z/
63
+ indent, hashes, comment = $1, $2, $3
64
+ indent_size = indent.size
65
+ # The "+ 2" is (indent)#(single space)
66
+ space_for_text_per_line = self.class.line_length_limit - (indent_size + hashes.size)
67
+ lines = ['']
68
+ words = comment.split(/\s/)
69
+ quota = space_for_text_per_line
70
+ current_line = 0
71
+ while words.any?
72
+ word = words.shift
73
+ # break on word big enough to make a new line, unless its the first word
74
+ if quota - (word.size + 1) < 0 && quota < space_for_text_per_line
75
+ current_line += 1
76
+ lines << ''
77
+ quota = space_for_text_per_line
78
+ end
79
+ unless lines[current_line].empty?
80
+ lines[current_line] << ' '
81
+ quota -= 1
82
+ end
83
+ lines[current_line] << word
84
+ quota -= word.size
85
+ end
86
+ lines.map { |line| indent + hashes + line }.join("\n")
87
+ end
88
+ end
89
+
90
+ module Wool
91
+ def LineLengthCustomSeverity(size, severity)
92
+ Wool.class_eval do
93
+ if const_defined?("GenericLineLengthWarning_#{size}_#{severity}")
94
+ return const_get("GenericLineLengthWarning_#{size}_#{severity}")
95
+ end
96
+ new_warning = Class.new(Wool::GenericLineLengthWarning)
97
+ const_set("GenericLineLengthWarning_#{size}_#{severity}", new_warning)
98
+ new_warning.line_length_limit = size
99
+ new_warning.severity = severity
100
+ new_warning.desc = Wool::GenericLineLengthWarning.desc
101
+ new_warning
102
+ end
103
+ end
104
+
105
+ def LineLengthMaximum(size)
106
+ (@table ||= {})[size] ||= LineLengthCustomSeverity(size, 8)
107
+ end
108
+
109
+ def LineLengthWarning(size)
110
+ (@table ||= {})[size] ||= LineLengthCustomSeverity(size, 3)
111
+ end
112
+ module_function :LineLengthMaximum, :LineLengthWarning, :LineLengthCustomSeverity
113
+ end
@@ -0,0 +1,16 @@
1
+ # This warning is used when
2
+ class Wool::MisalignedUnindentationWarning < Wool::LineWarning
3
+ type :style
4
+ severity 2
5
+ short_desc 'Misaligned Unindentation'
6
+ desc { "Expected #{@expectation} spaces, but instead found #{get_indent.size}" }
7
+
8
+ def initialize(file, line, expectation)
9
+ super(file, line)
10
+ @expectation = expectation
11
+ end
12
+
13
+ def fix
14
+ indent self.line, @expectation
15
+ end
16
+ end
@@ -0,0 +1,63 @@
1
+ # Warning for not putting space around operators
2
+ class Wool::OperatorSpacing < Wool::LineWarning
3
+ include Wool::Advice::CommentAdvice
4
+ OPERATORS = %w(+ - / * != !== = == === ~= !~ += -= *= /= ** **= ||= || && &&= &= |= | & ^)
5
+
6
+ type :style
7
+ severity 5
8
+ short_desc 'No operator spacing'
9
+ desc { "Insufficient spacing around #{self.match?(self.line).body}" }
10
+
11
+ def match?(line = self.body)
12
+ working_line = ignore_block_params line
13
+ working_line = ignore_splat_args working_line
14
+ working_line = ignore_to_proc_args working_line
15
+ working_line = ignore_array_splat_idiom working_line
16
+ lexed = lex(working_line)
17
+ lexed.each_with_index do |token, idx|
18
+ next unless token.type == :on_op
19
+ next if idx == lexed.size - 1 # Last token on line (continuation) is ok
20
+ next if token.body == '-' && [:on_float, :on_int].include?(lexed[idx+1].type)
21
+ return token if lexed[idx+1].type != :on_sp && lexed[idx+1].type != :on_op
22
+ return token if idx > 0 && ![:on_sp, :on_op].include?(lexed[idx-1].type)
23
+ end
24
+ nil
25
+ end
26
+
27
+ def generated_warnings(*args)
28
+ match?(*args) ? [self] : []
29
+ end
30
+
31
+ def ignore_block_params(line)
32
+ line.gsub(/(\{|(do))\s*\|.*\|/, '\1')
33
+ end
34
+
35
+ def ignore_splat_args(line)
36
+ line.gsub(/(\(|(, ))\&([a-z][A-Za-z0-9_]*)((, )|\)|\Z)/, '\1')
37
+ end
38
+
39
+ def ignore_array_splat_idiom(line)
40
+ line.gsub(/\[\*([a-z][A-Za-z0-9_]*)\]/, '\1')
41
+ end
42
+
43
+ def ignore_to_proc_args(line)
44
+ line.gsub(/(\(|(, ))\*([a-z][A-Za-z0-9_]*)((, )|\)|\Z)/, '\1')
45
+ end
46
+
47
+ def is_block_line?(line)
48
+ line =~ /do\s*\|/ || line =~ /\{\s*\|/
49
+ end
50
+
51
+ def fix
52
+ line = self.line.dup
53
+ OPERATORS.each do |op|
54
+ next if op == '==' && line =~ /!==/
55
+ next if op == '=' && line =~ /!=/
56
+ next if op == '|' && self.is_block_line?(line)
57
+ embed = op.gsub(/(\+|\-|\*|\||\^)/, '\\\\\\1')
58
+ line.gsub!(/([A-Za-z0-9_]!|[A-Za-z0-9_?])(#{embed})/, '\1 \2')
59
+ line.gsub!(/(#{embed})([$A-Za-z0-9_?!])/, '\1 \2')
60
+ end
61
+ line
62
+ end
63
+ end
@@ -0,0 +1,41 @@
1
+ # Warning for rescuing "Exception" or "Object".
2
+ class Wool::RescueExceptionWarning < Wool::FileWarning
3
+ severity 5
4
+ type :dangerous
5
+ short_desc 'rescue Exception is dangerous'
6
+ desc 'The line rescues "Exception" or "Object", which is too broad. Rescue StandardError instead.'
7
+ setting_accessor :position
8
+
9
+ def match?(file = self.body)
10
+ find_sexps(:rescue).map do |_, types, name|
11
+ case types[0]
12
+ when :mrhs_new_from_args
13
+ list = types[1] + types[2..-1]
14
+ when Array
15
+ list = types
16
+ end
17
+ list.map do |type|
18
+ if type[0] == :var_ref &&
19
+ type[1][0] == :@const && type[1][1] == "Exception"
20
+ warning = RescueExceptionWarning.new(file, body, :position => type[1][2])
21
+ warning.position[0] -= 1
22
+ warning.line_number = type[1][2][1]
23
+ warning
24
+ end
25
+ end.compact
26
+ end.flatten
27
+ end
28
+
29
+ def fix
30
+ result = ""
31
+ all_lines = self.body.lines.to_a
32
+ result << all_lines[0..position[0]-1].join if position[0]-1 >= 0
33
+ result << all_lines[position[0]][0,position[1]]
34
+ result << 'StandardError'
35
+ if trailing = all_lines[position[0]][position[1] + 'Exception'.size .. -1]
36
+ result << trailing
37
+ end
38
+ result << all_lines[position[0]+1..-1].join if position[0]+1 < all_lines.size
39
+ result
40
+ end
41
+ end