forked from michaeledgar/laser
/
runner.rb
194 lines (178 loc) · 6.81 KB
/
runner.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
module Laser
class Runner
attr_accessor :using, :fix
def initialize(argv)
@argv = argv
@using = [:all]
@fix = [:all]
end
def run
settings, files = collect_options_and_arguments
# parse_forest: Array<Array<String, Sexp>>
parse_forest = files.zip(files.map { |file| Ripper.sexp(read_file(file))})
settings[:__using__] = warnings_to_consider
settings[:__fix__] = warnings_to_fix
scanner = Scanner.new(settings)
warnings = collect_warnings(files, scanner)
display_warnings(warnings, settings) if settings[:display]
print_modules if settings[:"list-modules"]
end
def collect_options_and_arguments
swizzling_argv do
settings = get_settings
handle_global_options(settings)
p settings if settings[:debug]
files = ARGV.dup
[settings, files]
end
end
# Processes the global options, which includes picking which warnings to
# run against the source code. The settings provided determine what
# modifies the runner's settings.
#
# @param [Hash] settings the settings from the command-line to process.
# @option settings :only (String) a list of warning names or short names
# that will be the only warnings run. The names should be whitespace-delimited.
# @option settings :"line-length" (Integer) a maximum line length to
# generate a warning for. A common choice is 80/83.
def handle_global_options(settings)
if settings[:"line-length"]
@using << Laser.LineLengthWarning(settings[:"line-length"])
end
if (only_name = settings[:only])
@fix = @using = Warning.concrete_warnings.select do |w|
classname = w.name && w.name.split('::').last
(classname && only_name.index(classname)) || (w.short_name && only_name.index(w.short_name))
end
end
if settings[:profile]
require 'benchmark'
require 'profile'
SETTINGS[:profile] = true
end
if settings[:include]
Laser::SETTINGS[:load_path] = settings[:include].reverse
end
ARGV.replace(['(stdin)']) if settings[:stdin]
end
# Parses the command-line options using Trollop
#
# @return [Hash{Symbol => Object}] the settings entered by the user
def get_settings
warning_opts = get_warning_options
Trollop::options do
banner 'LASER: Lexically- and Semantically-Enriched Ruby'
opt :fix, 'Should errors be fixed in-line?', short: '-f'
opt :display, 'Should errors be displayed?', short: '-b', default: true
opt :'report-fixed', 'Should fixed errors be reported anyway?', short: '-r'
opt :'line-length', 'Warn at the given line length', short: '-l', type: :int
opt :only, 'Only consider the given warning (by short or full name)', short: '-O', type: :string
opt :stdin, 'Read Ruby code from standard input', short: '-s'
opt :'list-modules', 'Print the discovered, loaded modules'
opt :profile, 'Run the profiler during execution'
opt :include, 'specify $LOAD_PATH directory (may be used more than once)', short: '-I', multi: true#, type: :string
opt :S, 'look for scripts using PATH environment variable', short: '-S'
warning_opts.each { |warning| opt(*warning) }
end
end
# Gets all the options from the warning plugins and collects them
# with overriding rules. The later the declaration is run, the higher the
# priority the option has.
def get_warning_options
all_options = Warning.all_warnings.inject({}) do |result, warning|
options = warning.options
options = [options] if options.any? && !options[0].is_a?(Array)
options.each do |option|
result[option.first] = option
end
result
end
all_options.values
end
# Prints the known modules after analysis.
def print_modules
Analysis::LaserModule.all_modules.map do |mod|
result = []
result << if Analysis::LaserClass === mod && mod.superclass
then "#{mod.path} < #{mod.superclass.path}"
else mod.name
end
result
end.sort.flatten.each { |name| puts name }
end
def read_file(file)
case file
when '(stdin)' then $stdin.read
else File.read(file)
end
end
# Converts a list of warnings and symbol shortcuts for warnings to just a
# list of warnings.
def convert_warning_list(list)
list.map do |list|
case list
when :all then Warning.all_warnings
when :whitespace
[ExtraBlankLinesWarning, ExtraWhitespaceWarning,
OperatorSpacing, MisalignedUnindentationWarning]
else list
end
end.flatten
end
# Returns the list of warnings the user has activated for use.
def warnings_to_consider
convert_warning_list(@using)
end
# Returns the list of warnings the user has selected for fixing
def warnings_to_fix
convert_warning_list(@fix)
end
# Sets the ARGV variable to the runner's arguments during the execution
# of the block.
def swizzling_argv
old_argv = ARGV.dup
ARGV.replace @argv
yield
ensure
ARGV.replace old_argv
end
# Collects warnings from all the provided files by running them through
# the scanner.
#
# @param [Array<String>] files the files to scan. If (stdin) is in the
# array, then data will be read from STDIN until EOF is reached.
# @param [Scanner] scanner the scanner that will look for warnings
# in the source text.
# @return [Array<Warning>] a set of warnings, ordered by file.
def collect_warnings(files, scanner)
full_list = files.map do |file|
data = file == '(stdin)' ? STDIN.read : File.read(file)
if scanner.settings[:fix]
scanner.settings[:output_file] = scanner.settings[:stdin] ? STDOUT : File.open(file, 'w')
end
results = scanner.scan(data, file)
if scanner.settings[:fix] && !scanner.settings[:stdin]
scanner.settings[:output_file].close
end
results
end
full_list.flatten
end
# Displays warnings using user-provided settings.
#
# @param [Array<Warning>] warnings the warnings generated by the input
# files, ordered by file
# @param [Hash{Symbol => Object}] settings the user-set display settings
def display_warnings(warnings, settings)
num_fixable = warnings.select { |warn| warn.fixable? }.size
num_total = warnings.size
results = "#{num_total} warnings found. #{num_fixable} are fixable."
puts results
puts '=' * results.size
warnings.each do |warning|
puts "#{warning.file}:#{warning.line_number} #{warning.name} " +
"(#{warning.severity}) - #{warning.desc}"
end
end
end
end