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