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,24 @@
1
+ # Warning for using semicolons outside of class declarations.
2
+ class Wool::SemicolonWarning < Wool::LineWarning
3
+ type :style
4
+ short_desc 'Semicolon for multiple statements'
5
+ desc 'The line uses a semicolon to separate multiple statements outside of a class declaration.'
6
+
7
+ def initialize(*args)
8
+ super
9
+ self.severity = line =~ /['"]/ ? 2 : 4
10
+ end
11
+
12
+ def match?(line = self.body)
13
+ !!(find_token(line, :on_semicolon) && !find_keyword(line, :class))
14
+ end
15
+
16
+ def fix(line = self.body)
17
+ left, right = split_on_token(line, :on_semicolon)
18
+ return line if right.empty?
19
+ return right[1..-1] if left.empty?
20
+
21
+ right = fix(right[1..-1])
22
+ "#{indent left}\n#{indent right}"
23
+ end
24
+ end
@@ -0,0 +1,37 @@
1
+ # Warning for using semicolons outside of class declarations.
2
+ class Wool::UselessDoubleQuotesWarning < Wool::FileWarning
3
+ type :style
4
+ severity 1
5
+ short_desc 'Useless double quotes'
6
+ setting_accessor :quoted_string, :uses_q_braces
7
+ desc do
8
+ if uses_q_braces
9
+ then "The string %q{#{quoted_string}} can be written with lowercase q for efficiency."
10
+ else "The string '#{quoted_string}' can be wrapped in single quotes for efficiency."
11
+ end
12
+ end
13
+
14
+ def match?(body = self.body)
15
+ list = find_sexps(:string_content)
16
+ list.map do |sym, *parts|
17
+ next if parts.size != 1 # ignore multiparts as they're fine
18
+ inner_sym, text, pos = parts.first
19
+ # skip if the string has a backslash or an apostrophe in it.
20
+ next unless inner_sym == :@tstring_content && text !~ /(\\)|(')/
21
+
22
+ previous_char = body.lines.to_a[pos[0] - 1][pos[1]-1,1]
23
+ uses_q_braces = (previous_char == '{' && body.lines.to_a[pos[0] - 1][pos[1]-3,2] == '%Q')
24
+ if previous_char == '"' || uses_q_braces
25
+ warning = Wool::UselessDoubleQuotesWarning.new(
26
+ file, body, :quoted_string => text, :uses_q_braces => uses_q_braces)
27
+ warning.line_number = pos[0]
28
+ warning
29
+ end
30
+ end.compact
31
+ end
32
+
33
+ def fix(body = self.body)
34
+ body.gsub("\"#{quoted_string}\"", "'#{quoted_string}'").
35
+ gsub("%Q{#{quoted_string}}", "%q{#{quoted_string}}")
36
+ end
37
+ end
@@ -0,0 +1,69 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe Advice do
4
+ context '#before_advice' do
5
+ before do
6
+ @class = Class.new do
7
+ extend Advice
8
+ attr_accessor :closed_over
9
+ define_method :silly do
10
+ self.closed_over ||= 1
11
+ self.closed_over += 5
12
+ end
13
+ define_method :checkin do
14
+ self.closed_over ||= 1
15
+ self.closed_over *= 2
16
+ end
17
+ before_advice :silly, :checkin
18
+ end
19
+ end
20
+
21
+ it 'causes the advised method to run the suggested advice before running' do
22
+ @class.new.silly.should == 7
23
+ end
24
+ end
25
+
26
+ context '#after_advice' do
27
+ before do
28
+ @class = Class.new do
29
+ extend Advice
30
+ attr_accessor :closed_over
31
+ define_method :silly do
32
+ self.closed_over ||= 1
33
+ self.closed_over += 5
34
+ end
35
+ define_method :checkout do
36
+ self.closed_over ||= 1
37
+ self.closed_over *= 2
38
+ end
39
+ after_advice :silly, :checkout
40
+ end
41
+ end
42
+
43
+ it 'causes the advised method to run the suggested advice after running' do
44
+ object = @class.new
45
+ object.silly
46
+ object.closed_over.should == 12
47
+ end
48
+ end
49
+
50
+ context '#argument_advice' do
51
+ before do
52
+ @class = Class.new do
53
+ extend Advice
54
+ attr_accessor :closed_over
55
+ define_method :silly do |arg|
56
+ arg + 5
57
+ end
58
+ define_method :twiddle do |arg|
59
+ arg * 2
60
+ end
61
+ argument_advice :silly, :twiddle
62
+ end
63
+ end
64
+
65
+ it 'causes the advised method to run, rewriting the arguments' do
66
+ @class.new.silly(1).should == 7
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,38 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe Advice::CommentAdvice do
4
+ before do
5
+ @class = Class.new(Warning) do
6
+ include Advice::CommentAdvice
7
+
8
+ def match?(body = self.body, settings={})
9
+ body
10
+ end
11
+ remove_comments
12
+ end
13
+ end
14
+
15
+ context '#remove_comments' do
16
+ it 'Returns the empty string unmodified' do
17
+ @class.new('(stdin)', '').match?('').should == ''
18
+ end
19
+
20
+ it 'Turns a comment into the empty string' do
21
+ @class.new('(stdin)', '# hello').match?('# hello').should == ''
22
+ end
23
+
24
+ it 'strips the comments from the end of a string of code' do
25
+ @class.new('(stdin)', 'a + b # adding').match?('a + b # adding').should == 'a + b'
26
+ end
27
+
28
+ SAMPLES = [['', ''], ['#hello', ''], [' # hello', ''],
29
+ [' a + b # hello', ' a + b'], ['(a + b) #', '(a + b)'],
30
+ ['"#" + number', '"#" + number'],
31
+ ['" hello \\"mam\\"" # comment', '" hello \\"mam\\""']]
32
+ SAMPLES.each do |input, output|
33
+ it "should turn #{input} into #{output}" do
34
+ @class.new('(stdin)', input).match?(input).should == output
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
@@ -0,0 +1,47 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+ require 'set'
3
+ describe NextPrevAnnotation do
4
+ it 'adds the #next and #prev methods to Sexp' do
5
+ Sexp.instance_methods.should include(:next)
6
+ Sexp.instance_methods.should include(:prev)
7
+ end
8
+
9
+ it 'adds next and prevs to each node with a toy example' do
10
+ tree = Sexp.new([:abc, Sexp.new([:def, 1, 2]),
11
+ Sexp.new([:zzz, Sexp.new([:return]), "hi", Sexp.new([:silly, 4])])])
12
+ NextPrevAnnotation::Annotator.new.annotate!(tree)
13
+ tree[1].prev.should == nil
14
+ tree[1].next.should == tree[2]
15
+ tree[2].prev.should == tree[1]
16
+ tree[2].next.should == nil
17
+ tree[2][1].prev.should == nil
18
+ tree[2][1].next.should == tree[2][2]
19
+ tree[2][3].prev.should == tree[2][2]
20
+ tree[2][3].next.should == nil
21
+ end
22
+
23
+ # This will actually verify that every node in the tree has a
24
+ # proper parent set. It's a complex, but thorough test.
25
+ it 'adds next and prevs to each node with a real-world parse result' do
26
+ tree = Sexp.new(Ripper.sexp('x = proc {|x, *rst, &blk| p x ** rst[0]; blk.call(rst[1..-1])}'))
27
+ tree.next.should == nil
28
+ tree.prev.should == nil
29
+ visited = Set.new
30
+ to_visit = tree.children
31
+ while to_visit.any?
32
+ todo = to_visit.pop
33
+ next unless is_sexp?(todo)
34
+
35
+ todo.prev.next.should == todo if is_sexp?(todo.prev)
36
+ todo.next.prev.should == todo if is_sexp?(todo.next)
37
+
38
+ visited << todo
39
+ case todo[0]
40
+ when Array
41
+ to_visit.concat todo
42
+ when Symbol
43
+ to_visit.concat todo.children
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,41 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+ require 'set'
3
+ describe ParentAnnotation do
4
+ it 'adds the #parent method to Sexp' do
5
+ Sexp.instance_methods.should include(:parent)
6
+ end
7
+
8
+ it 'adds parents to each node with a toy example' do
9
+ tree = Sexp.new([:abc, Sexp.new([:def, 1, 2]),
10
+ Sexp.new([:zzz, Sexp.new([:return]), "hi", Sexp.new([:silly, 4])])])
11
+ ParentAnnotation::Annotator.new.annotate!(tree)
12
+ tree.parent.should == nil
13
+ tree[1].parent.should == tree
14
+ tree[2].parent.should == tree
15
+ tree[2][1].parent.should == tree[2]
16
+ tree[2][3].parent.should == tree[2]
17
+ end
18
+
19
+ # This will actually verify that every node in the tree has a
20
+ # proper parent set. It's a complex, but thorough test.
21
+ it 'adds parents to each node with a real-world parse result' do
22
+ tree = Sexp.new(Ripper.sexp('x = proc {|x, *rst, &blk| p x ** rst[0]; blk.call(rst[1..-1])}'))
23
+ tree.parent.should == nil
24
+ tree.children[0].parent.should == tree
25
+ visited = Set.new
26
+ to_visit = tree.children
27
+ while to_visit.any?
28
+ todo = to_visit.pop
29
+ next unless Sexp === todo
30
+ todo.parent.children.should include(todo)
31
+ visited << todo
32
+
33
+ case todo[0]
34
+ when Array
35
+ to_visit.concat todo
36
+ when Symbol
37
+ to_visit.concat todo.children
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,5 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ def is_sexp?(sexp)
4
+ SexpAnalysis::Sexp === sexp
5
+ end
@@ -0,0 +1,179 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe LexicalAnalysis do
4
+ before do
5
+ @class = Class.new do
6
+ include LexicalAnalysis
7
+ attr_accessor :body
8
+ def initialize(body)
9
+ self.body = body
10
+ end
11
+ end
12
+ end
13
+
14
+ context '#lex' do
15
+ it 'lexes its body' do
16
+ @class.new('a').lex.should == [LexicalAnalysis::Token.new([[1,0], :on_ident, 'a'])]
17
+ end
18
+ end
19
+
20
+ context '#text_between_token_positions' do
21
+ it 'finds the exclusive text between two simple tokens from a body text' do
22
+ body = "def initialize(body)\n self.body = body\nend"
23
+ left = LexicalAnalysis::Token.new([[2, 4], :on_kw, "self"])
24
+ right = LexicalAnalysis::Token.new([[2, 20], :on_nl, "\n"])
25
+ @class.new('').text_between_token_positions(body, left, right).should ==
26
+ '.body = body'
27
+ end
28
+
29
+ it 'allows including the left token with the inclusive :left hash option' do
30
+ body = "def initialize(body)\n self.body = body\nend"
31
+ left = LexicalAnalysis::Token.new([[2, 4], :on_kw, "self"])
32
+ right = LexicalAnalysis::Token.new([[2, 20], :on_nl, "\n"])
33
+ @class.new('').text_between_token_positions(body, left, right, :left).should ==
34
+ 'self.body = body'
35
+ end
36
+
37
+ it 'allows including the right token with the inclusive :right hash option' do
38
+ body = "def initialize(body)\n self.body = body\nend"
39
+ left = LexicalAnalysis::Token.new([[2, 4], :on_kw, "self"])
40
+ right = LexicalAnalysis::Token.new([[2, 20], :on_nl, "\n"])
41
+ @class.new('').text_between_token_positions(body, left, right, :right).should ==
42
+ ".body = body\n"
43
+ end
44
+
45
+ it 'allows including both tokens with the inclusive :both hash option' do
46
+ body = "def initialize(body)\n self.body = body\nend"
47
+ left = LexicalAnalysis::Token.new([[2, 4], :on_kw, "self"])
48
+ right = LexicalAnalysis::Token.new([[2, 20], :on_nl, "\n"])
49
+ @class.new('').text_between_token_positions(body, left, right, :both).should ==
50
+ "self.body = body\n"
51
+ end
52
+
53
+ it 'allows explicitly to exclude the tokens with the inclusive :none option' do
54
+ body = "def initialize(body)\n self.body = body\nend"
55
+ left = LexicalAnalysis::Token.new([[2, 4], :on_kw, "self"])
56
+ right = LexicalAnalysis::Token.new([[2, 20], :on_nl, "\n"])
57
+ @class.new('').text_between_token_positions(body, left, right, :none).should ==
58
+ '.body = body'
59
+ end
60
+
61
+ context 'spanning multiple lines' do
62
+ it 'allows including the left token with the inclusive :left hash option' do
63
+ body = "def initialize(body)\n self.body = body\nend # a * b"
64
+ left = LexicalAnalysis::Token.new([[1, 14], :on_lparen, "("])
65
+ right = LexicalAnalysis::Token.new([[3, 3], :on_sp, " "])
66
+ @class.new('').text_between_token_positions(body, left, right, :left).should ==
67
+ "(body)\n self.body = body\nend"
68
+ end
69
+
70
+ it 'allows including the right token with the inclusive :right hash option' do
71
+ body = "def initialize(body)\n self.body = body\nend # a * b"
72
+ left = LexicalAnalysis::Token.new([[1, 14], :on_lparen, "("])
73
+ right = LexicalAnalysis::Token.new([[3, 3], :on_sp, " "])
74
+ @class.new('').text_between_token_positions(body, left, right, :right).should ==
75
+ "body)\n self.body = body\nend "
76
+ end
77
+
78
+ it 'allows including both tokens with the inclusive :both hash option' do
79
+ body = "def initialize(body)\n self.body = body\nend # a * b"
80
+ left = LexicalAnalysis::Token.new([[1, 14], :on_lparen, "("])
81
+ right = LexicalAnalysis::Token.new([[3, 3], :on_sp, " "])
82
+ @class.new('').text_between_token_positions(body, left, right, :both).should ==
83
+ "(body)\n self.body = body\nend "
84
+ end
85
+
86
+ it 'allows explicitly to exclude the tokens with the inclusive :none option' do
87
+ body = "def initialize(body)\n self.body = body\nend # a * b"
88
+ left = LexicalAnalysis::Token.new([[1, 14], :on_lparen, "("])
89
+ right = LexicalAnalysis::Token.new([[3, 3], :on_sp, " "])
90
+ @class.new('').text_between_token_positions(body, left, right, :none).should ==
91
+ "body)\n self.body = body\nend"
92
+ end
93
+ end
94
+ end
95
+
96
+ context '#find_token' do
97
+ it 'lexes its body' do
98
+ @class.new('a + b').find_token(:on_op).should be_true
99
+ end
100
+
101
+ it 'returns falsy if token not found' do
102
+ @class.new('a + b').find_token(:on_kw).should be_false
103
+ end
104
+
105
+ it 'works with multiple token options' do
106
+ result = @class.new('a + b # hello').find_token(:on_op, :on_comment)
107
+ result.type.should == :on_op
108
+ end
109
+
110
+ it 'is not triggered by symbols' do
111
+ @class.new(':+').find_token(:on_op).should be_false
112
+ end
113
+ end
114
+
115
+ context '#find_keyword' do
116
+ it 'lexes its body' do
117
+ @class.new('class A < B').find_keyword(:class).should be_true
118
+ end
119
+
120
+ it 'returns falsy if token not found' do
121
+ @class.new('class A < B').find_keyword(:end).should be_false
122
+ end
123
+
124
+ it 'returns the actual token if it is found' do
125
+ @class.new('class A < B').find_keyword(:class).should ==
126
+ LexicalAnalysis::Token.new([[1,0], :on_kw, 'class'])
127
+ end
128
+
129
+ it 'works with multiple keyword options' do
130
+ result = @class.new('class A < B; end').find_keyword(:class, :end)
131
+ result.body.should == 'class'
132
+ end
133
+
134
+ it 'is not triggered by symbols' do
135
+ @class.new(':unless').find_keyword(:unless).should be_false
136
+ end
137
+ end
138
+
139
+ context '#split_on_token' do
140
+ it 'splits the input into two parts based on the token searched' do
141
+ left, right = @class.new('a + b; c + d').split_on_token(:on_semicolon)
142
+ left.should == 'a + b'
143
+ right.should == '; c + d'
144
+ end
145
+
146
+ it 'works with multiple searched tokens' do
147
+ left, right = @class.new('a + b; c + d').split_on_token(:on_semicolon, :on_op)
148
+ left.should == 'a '
149
+ right.should == '+ b; c + d'
150
+ end
151
+
152
+ it 'matches its own documentation' do
153
+ left, right = @class.new('').split_on_token('x = 5 unless y == 2', :on_kw)
154
+ left.should == 'x = 5 '
155
+ right.should == 'unless y == 2'
156
+ end
157
+ end
158
+
159
+
160
+ context '#split_on_keyword' do
161
+ it 'splits the input into two parts based on the token searched' do
162
+ left, right = @class.new('rescue x if y').split_on_keyword(:if)
163
+ left.should == 'rescue x '
164
+ right.should == 'if y'
165
+ end
166
+
167
+ it 'works with multiple searched tokens' do
168
+ left, right = @class.new('rescue x if y').split_on_keyword(:if, :rescue)
169
+ left.should == ''
170
+ right.should == 'rescue x if y'
171
+ end
172
+
173
+ it 'matches its own documentation' do
174
+ left, right = @class.new('').split_on_keyword('x = 5 unless y == 2', :unless)
175
+ left.should == 'x = 5 '
176
+ right.should == 'unless y == 2'
177
+ end
178
+ end
179
+ end