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