wool 0.5.1

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