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,16 @@
1
+ module Wool
2
+ module SexpAnalysis
3
+ # A single signature in the Wool protocol system. This is just
4
+ # a simple specification of a method that an object can receive,
5
+ # either explicitly or implicitly defined, and the protocols of the
6
+ # return type and all arguments.
7
+ class Signature < Struct.new(:name, :return_protocol, :argument_protocols)
8
+ include Comparable
9
+
10
+ def <=>(other)
11
+ [self.name, self.return_protocol, self.argument_protocols] <=>
12
+ [other.name, other.return_protocol, other.argument_protocols]
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,10 @@
1
+ module Wool
2
+ module SexpAnalysis
3
+ # This class represents a Symbol in Ruby. It may have a known protocol (type),
4
+ # class, value (if constant!), and a variety of other details.
5
+ class Symbol < Struct.new(:protocol, :class_used, :value, :scope, :name)
6
+ include Comparable
7
+
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,36 @@
1
+ module Wool
2
+ module SexpAnalysis
3
+ # Visitor: a set of methods for visiting an AST. The
4
+ # default implementations visit each child and do no
5
+ # other processing. By including this module, and
6
+ # implementing certain methods, you can do your own
7
+ # processing on, say, every instance of a :rescue AST node.
8
+ # The default implementation will go arbitrarily deep in the AST
9
+ # tree until it hits a method you define.
10
+ module Visitor
11
+ def visit(node)
12
+ case node
13
+ when Sexp
14
+ case node[0]
15
+ when ::Symbol
16
+ send("visit_#{node[0]}", node)
17
+ when Array
18
+ node.each {|x| visit(x)}
19
+ end
20
+ end
21
+ end
22
+
23
+ def default_visit(node)
24
+ node.children.select {|x| Sexp === x}.each {|x| visit(x) }
25
+ end
26
+
27
+ def method_missing(meth, *args, &blk)
28
+ if meth.to_s[0,6] == 'visit_'
29
+ default_visit args.first
30
+ else
31
+ raise
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,47 @@
1
+ module Wool
2
+ module SexpAnalysis
3
+ # Wool representation of a class. I named it WoolClass so it wouldn't
4
+ # clash with regular Class. This links the class to its protocol. It
5
+ # has lists of methods, instance variables, and so on.
6
+ class WoolClass
7
+ attr_reader :path, :methods, :protocol, :scope, :class_object
8
+ attr_accessor :superclass
9
+
10
+ def initialize(full_path, scope = Scope::GlobalScope)
11
+ @path = full_path
12
+ @methods = {}
13
+ @protocol = Protocols::ClassProtocol.new(self)
14
+ @scope = scope
15
+ @class_object = Symbol.new(@protocol, self)
16
+ ProtocolRegistry.add_class_protocol(@protocol)
17
+ yield self if block_given?
18
+ end
19
+
20
+ def add_method(method)
21
+ @methods[method.name] = method
22
+ end
23
+
24
+ def signatures
25
+ @methods.values.map(&:signatures).flatten
26
+ end
27
+ end
28
+
29
+ # Wool representation of a method. This name is tweaked so it doesn't
30
+ # collide with ::Method.
31
+ class WoolMethod
32
+ extend ModuleExtensions
33
+ attr_reader :name, :signatures
34
+ attr_accessor_with_default :pure, false
35
+
36
+ def initialize(name)
37
+ @name = name
38
+ @signatures = []
39
+ yield self if block_given?
40
+ end
41
+
42
+ def add_signature(return_proto, arg_protos)
43
+ @signatures << Signature.new(self.name, return_proto, arg_protos)
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,42 @@
1
+ module Wool
2
+ module Rake
3
+ class WoolTask
4
+ class Settings < Struct.new(:libs, :extras, :options, :using, :fix)
5
+ def initialize(*args)
6
+ super
7
+ self.libs ||= []
8
+ self.extras ||= []
9
+ self.options ||= ''
10
+ self.using ||= []
11
+ self.fix ||= []
12
+ end
13
+ end
14
+
15
+ attr_accessor :settings
16
+
17
+ def initialize(task_name)
18
+ @settings = Settings.new
19
+ yield @settings if block_given?
20
+ @settings.using = [:all] if @settings.using.empty?
21
+ task task_name do
22
+ run
23
+ end
24
+ end
25
+
26
+ def run
27
+ files = []
28
+ if @settings.libs.any?
29
+ @settings.libs.each do |lib|
30
+ Dir["#{lib}/**/*.rb"].each do |file|
31
+ files << file
32
+ end
33
+ end
34
+ end
35
+ runner = Wool::Runner.new(self.settings.options.split(/\s/) + files)
36
+ runner.using = self.settings.using
37
+ runner.fix = self.settings.fix
38
+ runner.run
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,156 @@
1
+ module Wool
2
+ class Runner
3
+ attr_accessor :using, :fix
4
+
5
+ def initialize(argv)
6
+ @argv = argv
7
+ @using = [:all]
8
+ @fix = [:all]
9
+ end
10
+
11
+ def run
12
+ settings, files = collect_options_and_arguments
13
+ settings[:__using__] = warnings_to_consider
14
+ settings[:__fix__] = warnings_to_fix
15
+ scanner = Scanner.new(settings)
16
+ warnings = collect_warnings(files, scanner)
17
+ display_warnings(warnings, settings) if settings[:display]
18
+ end
19
+
20
+ def collect_options_and_arguments
21
+ swizzling_argv do
22
+ settings = get_settings
23
+ handle_global_options(settings)
24
+ p settings if settings[:debug]
25
+ files = ARGV.dup
26
+ [settings, files]
27
+ end
28
+ end
29
+
30
+ # Processes the global options, which includes picking which warnings to
31
+ # run against the source code. The settings provided determine what
32
+ # modifies the runner's settings.
33
+ #
34
+ # @param [Hash] settings the settings from the command-line to process.
35
+ # @option settings :only (String) a list of warning names or short names
36
+ # that will be the only warnings run. The names should be whitespace-delimited.
37
+ # @option settings :"line-length" (Integer) a maximum line length to
38
+ # generate a warning for. A common choice is 80/83.
39
+ def handle_global_options(settings)
40
+ if settings[:"line-length"]
41
+ @using << Wool.LineLengthWarning(settings[:"line-length"])
42
+ end
43
+ if (only_name = settings[:only])
44
+ @fix = @using = Warning.concrete_warnings.select do |w|
45
+ (w.name && w.name.index(only_name)) || (w.short_name && only_name.index(w.short_name))
46
+ end
47
+ end
48
+ ARGV.replace(['(stdin)']) if settings[:stdin]
49
+ end
50
+
51
+ # Parses the command-line options using Trollop
52
+ #
53
+ # @return [Hash{Symbol => Object}] the settings entered by the user
54
+ def get_settings
55
+ warning_opts = get_warning_options
56
+ Trollop::options do
57
+ banner 'Ask Peeves - the Ruby Linter'
58
+ opt :fix, 'Should errors be fixed in-line?', :short => '-f'
59
+ opt :display, 'Should errors be displayed?', :short => '-b', :default => true
60
+ opt :"report-fixed", 'Should fixed errors be reported anyway?', :short => '-r'
61
+ opt :"line-length", 'Warn at the given line length', :short => '-l', :type => :int
62
+ opt :only, 'Only consider the given warning (by short or full name)', :short => '-O', :type => :string
63
+ opt :stdin, 'Read Ruby code from standard input', :short => '-s'
64
+ warning_opts.each { |warning| opt(*warning) }
65
+ end
66
+ end
67
+
68
+ # Gets all the options from the warning plugins and collects them
69
+ # with overriding rules. The later the declaration is run, the higher the
70
+ # priority the option has.
71
+ def get_warning_options
72
+ all_options = Warning.all_warnings.inject({}) do |result, warning|
73
+ options = warning.options
74
+ options = [options] if options.any? && !options[0].is_a?(Array)
75
+ options.each do |option|
76
+ result[option.first] = option
77
+ end
78
+ result
79
+ end
80
+ all_options.values
81
+ end
82
+
83
+ # Converts a list of warnings and symbol shortcuts for warnings to just a
84
+ # list of warnings.
85
+ def convert_warning_list(list)
86
+ list.map do |list|
87
+ case list
88
+ when :all then Warning.all_warnings
89
+ when :whitespace
90
+ [ExtraBlankLinesWarning, ExtraWhitespaceWarning,
91
+ OperatorSpacing, MisalignedUnindentationWarning]
92
+ else list
93
+ end
94
+ end.flatten
95
+ end
96
+
97
+ # Returns the list of warnings the user has activated for use.
98
+ def warnings_to_consider
99
+ convert_warning_list(@using)
100
+ end
101
+
102
+ # Returns the list of warnings the user has selected for fixing
103
+ def warnings_to_fix
104
+ convert_warning_list(@fix)
105
+ end
106
+
107
+ # Sets the ARGV variable to the runner's arguments during the execution
108
+ # of the block.
109
+ def swizzling_argv
110
+ old_argv = ARGV.dup
111
+ ARGV.replace @argv
112
+ yield
113
+ ensure
114
+ ARGV.replace old_argv
115
+ end
116
+
117
+ # Collects warnings from all the provided files by running them through
118
+ # the scanner.
119
+ #
120
+ # @param [Array<String>] files the files to scan. If (stdin) is in the
121
+ # array, then data will be read from STDIN until EOF is reached.
122
+ # @param [Scanner] scanner the scanner that will look for warnings
123
+ # in the source text.
124
+ # @return [Array<Warning>] a set of warnings, ordered by file.
125
+ def collect_warnings(files, scanner)
126
+ full_list = files.map do |file|
127
+ data = file == '(stdin)' ? STDIN.read : File.read(file)
128
+ if scanner.settings[:fix]
129
+ scanner.settings[:output_file] = scanner.settings[:stdin] ? STDOUT : File.open(file, 'w')
130
+ end
131
+ results = scanner.scan(data, file)
132
+ scanner.settings[:output_file].close if scanner.settings[:fix] && !scanner.settings[:stdin]
133
+ results
134
+ end
135
+ full_list.flatten
136
+ end
137
+
138
+ # Displays warnings using user-provided settings.
139
+ #
140
+ # @param [Array<Warning>] warnings the warnings generated by the input
141
+ # files, ordered by file
142
+ # @param [Hash{Symbol => Object}] settings the user-set display settings
143
+ def display_warnings(warnings, settings)
144
+ num_fixable = warnings.select { |warn| warn.fixable? }.size
145
+ num_total = warnings.size
146
+
147
+ results = "#{num_total} warnings found. #{num_fixable} are fixable."
148
+ puts results
149
+ puts '=' * results.size
150
+
151
+ warnings.each do |warning|
152
+ puts "#{warning.file}:#{warning.line_number} #{warning.name} (#{warning.severity}) - #{warning.desc}"
153
+ end
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,160 @@
1
+ module Wool
2
+ class Scanner
3
+ attr_accessor :settings
4
+ attr_accessor :indent_stack
5
+
6
+ DEFAULT_SETTINGS = {:fix => false, :output => STDOUT, :indent_size => 2,
7
+ :__using__ => Wool::Warning.all_warnings,
8
+ :__fix__ => Wool::Warning.all_warnings}
9
+
10
+ # Initializes the scanner with the given settings
11
+ #
12
+ # @param [Hash] settings the settings to use to customize this scanner's
13
+ # scanning behavior
14
+ def initialize(settings = DEFAULT_SETTINGS)
15
+ @settings = DEFAULT_SETTINGS.merge(settings)
16
+ @settings[:__scanner__] = self
17
+ self.indent_stack = []
18
+ end
19
+
20
+ # Returns the list of warnings to use for scanning.
21
+ def using
22
+ @settings[:__using__]
23
+ end
24
+
25
+ # Should we use this warning?
26
+ def using?(warning)
27
+ @settings[:__using__].include? warning
28
+ end
29
+
30
+ # Returns the list of warnings to use for scanning.
31
+ def fix
32
+ @settings[:__fix__]
33
+ end
34
+
35
+ # Should we use this warning?
36
+ def fixing?(warning)
37
+ @settings[:__fix__].include? warning.class
38
+ end
39
+
40
+ # Scans the text for warnings.
41
+ #
42
+ # @param [String] text the input ruby file to scan
43
+ # @return [Array[Wool::Warnings]] the warnings generated by the code.
44
+ # If the code is clean, an empty array is returned.
45
+ def scan(text, filename='(none)')
46
+ warnings = scan_for_file_warnings(text, filename)
47
+ text = filter_fixable(warnings).inject(text) do |text, warning|
48
+ warning.fix(text)
49
+ end
50
+ with_fixing_piped_to_output do
51
+ text.split(/\n/).each_with_index do |line, number|
52
+ warnings.concat process_line(line, number + 1, filename)
53
+ end
54
+ end
55
+ warnings
56
+ end
57
+
58
+ def with_fixing_piped_to_output
59
+ self.settings[:output_lines] = []
60
+ yield
61
+ if @settings[:fix]
62
+ self.settings[:output_file].write self.settings[:output_lines].join("\n")
63
+ end
64
+ end
65
+
66
+ # Finds all matching warnings, and if the user wishes, fix a subset of them.
67
+ def process_line(line, line_number, filename)
68
+ warnings = all_warnings_for_line(line, line_number, filename)
69
+ fix_input(warnings, line, line_number, filename) if @settings[:fix]
70
+ warnings
71
+ end
72
+
73
+ # Tries to fix the given line with a set of matching warnings for that line.
74
+ # May recurse if there are multiple warnings on the same line.
75
+ def fix_input(warnings, line, line_number, filename)
76
+ fixable_warnings = filter_fixable warnings
77
+ if fixable_warnings.size == 1
78
+ self.settings[:output_lines] << fixable_warnings.first.fix rescue line
79
+ elsif fixable_warnings.size > 1
80
+ new_text = fixable_warnings.first.fix rescue line
81
+ process_line(new_text, line_number, filename)
82
+ else
83
+ self.settings[:output_lines] << line
84
+ end
85
+ end
86
+
87
+ # Returns all warnings that match the line
88
+ def all_warnings_for_line(line, line_number, filename)
89
+ new_warnings = check_for_indent_warnings!(line, filename)
90
+ new_warnings.concat scan_for_line_warnings(line, filename)
91
+ new_warnings.each {|warning| warning.line_number = line_number}
92
+ end
93
+
94
+ # Returns only the warnings that we should fix
95
+ def filter_fixable(warnings)
96
+ warnings.select {|warning| warning.fixable? && fixing?(warning) }
97
+ end
98
+
99
+ # Checks for new warnings based on indentation.
100
+ def check_for_indent_warnings!(line, filename)
101
+ return [] if line == ""
102
+ indent_size = get_indent_size line
103
+ if indent_size > current_indent
104
+ self.indent_stack.push indent_size
105
+ elsif indent_size < current_indent
106
+ previous = self.indent_stack.pop
107
+ if indent_size != current_indent &&
108
+ using.include?(MisalignedUnindentationWarning)
109
+ warnings_to_check = [MisalignedUnindentationWarning.new(filename, line, current_indent)]
110
+ return filtered_warnings_from_line(line, warnings_to_check)
111
+ end
112
+ end
113
+ []
114
+ end
115
+
116
+ # Gets the current indent size
117
+ def current_indent
118
+ self.indent_stack.last || 0
119
+ end
120
+
121
+ # Gets the indent size of a given line
122
+ def get_indent_size(line)
123
+ line.match(/^\s*/)[0].size
124
+ end
125
+
126
+ # Goes through all file warning subclasses and see what warnings the file
127
+ # generates as a whole.
128
+ def scan_for_file_warnings(file, filename)
129
+ scan_for_warnings(using & FileWarning.all_warnings, file, filename)
130
+ end
131
+
132
+ # Goes through all line warning subclasses and checks if we got some new
133
+ # warnings for a given line
134
+ def scan_for_line_warnings(line, filename)
135
+ warnings = scan_for_warnings(using & LineWarning.all_warnings, line, filename)
136
+ filtered_warnings_from_line(line, warnings)
137
+ end
138
+
139
+ private
140
+
141
+ # Filters the list of warnings by checking the line for warnings to
142
+ # ignore. The line should contain "wool: ignore ClassToIgnore" in a comment,
143
+ # though you can omit the space between "wool:" and "ignore".
144
+ def filtered_warnings_from_line(line, warnings)
145
+ match = line.match(/#.*wool:\s*ignore\s+(.*)$/)
146
+ return warnings unless match && ignore_label = match[1]
147
+ class_names = ignore_label.split
148
+ result = warnings.reject do |warning|
149
+ class_names.include?(warning.class.name.gsub(/.*::(.*)/, '\1')) ||
150
+ class_names.include?(warning.class.short_name)
151
+ end
152
+ result
153
+ end
154
+
155
+ def scan_for_warnings(warnings, content, filename)
156
+ warnings.map! { |warning| warning.new(filename, content, @settings)}
157
+ warnings.map { |warning| warning.generated_warnings(warning.body)}.flatten.uniq
158
+ end
159
+ end
160
+ end