-
Notifications
You must be signed in to change notification settings - Fork 11
Expand file tree
/
Copy pathsource_file.cr
More file actions
375 lines (307 loc) · 10.1 KB
/
source_file.cr
File metadata and controls
375 lines (307 loc) · 10.1 KB
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
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
require "compiler/crystal/syntax/*"
require "digest"
require "file_utils"
require "./extensions"
require "./macro_utils"
class Coverage::SourceFile < Crystal::Visitor
# List of keywords which are trouble with variable
# name. Some keywoards are not and won't be present in this
# list.
# Since this can break the code replacing the variable by a underscored
# version of it, and I'm not sure about this list, we will need to add/remove
# stuff to not break the code.
CRYSTAL_KEYWORDS = %w(
abstract do if nil? self unless
alias else of sizeof until
as elsif include struct when
as? end instance_sizeof pointerof super while
asm ensure is_a? private then with
begin enum lib protected true yield
break extend macro require
case false module rescue typeof
class for next return uninitialized
def fun nil select union
)
class_getter file_list = [] of Coverage::SourceFile
class_getter already_covered_file_name = Set(String).new
class_getter! project_path : String
class_getter require_expanders = [] of Array(Coverage::SourceFile)
class_property outputter : String = "Coverage::Outputter::HtmlReport"
class_property use_require : String = "coverage/runtime"
getter! astree : Crystal::ASTNode
getter id : Int32 = 0
getter path : String
getter md5_signature : String
getter lines = [] of Int32
getter already_covered_locations = Set(Crystal::Location?).new
getter source : String
getter! enriched_source : String
getter required_at : Int32
include MacroUtils
def self.register_file(f)
@@already_covered_file_name.add(f.path)
@@file_list << f
@@file_list.size - 1
end
def self.relative_path_to_project(path)
@@project_path ||= FileUtils.pwd
path.gsub(/^#{Coverage::SourceFile.project_path}\//, "")
end
def self.cover_file(file)
unless already_covered_file_name.includes?(relative_path_to_project(file))
already_covered_file_name.add(relative_path_to_project(file))
yield
end
end
def initialize(@path, @source, @required_at = 0)
@path = Coverage::SourceFile.relative_path_to_project(File.expand_path(@path, "."))
@md5_signature = Digest::MD5.hexdigest(@source)
@id = Coverage::SourceFile.register_file(self)
end
# Inject in AST tree if required.
def process
unless @astree
@astree = Crystal::Parser.parse(self.source)
astree.accept(self)
end
end
def to_covered_source
if @enriched_source.nil?
io = String::Builder.new(capacity: 32_768)
# call process to enrich AST before
# injection of cover head dependencies
process
# Inject the location of the zero line of current file
io << inject_location << "\n"
io << unfold_required(inject_line_traces(astree.to_s))
@enriched_source = io.to_s
else
@enriched_source.not_nil!
end
end
private def unfold_required(output)
output.gsub(/require[ \t]+\"\$([0-9]+)\"/) do |_str, matcher|
expansion_id = matcher[1].to_i
file_list = @@require_expanders[expansion_id]
if file_list.any?
io = String::Builder.new(capacity: (2 ** 20))
file_list.each do |file|
io << "#" << "require of `" << file.path
io << "` from `" << self.path << ":#{file.required_at}" << "`" << "\n"
io << file.to_covered_source
io << "\n"
io << inject_location(self.path, file.required_at)
io << "\n"
end
io.to_s
else
""
end
end
end
private def inject_location(file = @path, line = 0, column = 0)
%(#<loc:"#{file}",#{[line, 0].max},#{[column, 0].max}>)
end
def self.prelude_operations
file_maps = @@file_list.map do |f|
if f.lines.any?
"::Coverage::File.new(\"#{f.path}\", \"#{f.md5_signature}\",[#{f.lines.join(", ")}])"
else
"::Coverage::File.new(\"#{f.path}\", \"#{f.md5_signature}\",[] of Int32)"
end
end.join("\n")
<<-RAW
require "#{Coverage::SourceFile.use_require}"
#{file_maps}
RAW
end
def self.final_operations
"\n::Coverage.get_results(#{@@outputter}.new)"
end
# Inject line tracer for easy debugging.
# add `;` after the Coverage instrumentation
# to avoid some with macros
private def inject_line_traces(output)
output.gsub(/\:\:Coverage\[([0-9]+),[ ]*([0-9]+)\](.*)/) do |_str, match|
[
"::Coverage[", match[1],
", ", match[2], "]; ",
match[3],
inject_location(@path, @lines[match[2].to_i] - 1),
].join("")
end
end
private def source_map_index(line_number)
@lines << line_number
@lines.size - 1
end
private def inject_coverage_tracker(node)
if location = node.location
lnum = location.line_number
lidx = source_map_index(lnum)
n = Crystal::Call.new(Crystal::Global.new("::Coverage"), "[]",
[Crystal::NumberLiteral.new(@id),
Crystal::NumberLiteral.new(lidx)].unsafe_as(Array(Crystal::ASTNode)))
n
else
node
end
end
private def force_inject_cover(node : Crystal::ASTNode, location = nil)
location ||= node.location
return node if @already_covered_locations.includes?(location)
already_covered_locations << location
Crystal::Expressions.from([inject_coverage_tracker(node), node].unsafe_as(Array(Crystal::ASTNode)))
end
def inject_cover(node : Crystal::ASTNode)
return node if already_covered_locations.includes?(node.location)
case node
when Crystal::OpAssign, Crystal::Assign, Crystal::BinaryOp
# We cover assignment
force_inject_cover(node)
when Crystal::Call
# Ignore call to COVERAGE_DOT_CR
obj = node.obj
if (node.obj && obj.is_a?(Crystal::Global) && obj.name == "::Coverage")
return node
end
# Be ready to cover the calls
force_inject_cover(node)
when Crystal::Break
force_inject_cover(node)
else
node
end
end
# Management of required file is nasty and should be improved
# Since I've hard time to replace node on visit,
# I change the file argument to a number linked to an array of files
# Then on finalization, we replace each require "xxx" by the proper file.
def visit(node : Crystal::Require)
file = node.string
# we cover only files which are relative to current file
if file[0] == '.'
current_directory = Coverage::SourceFile.relative_path_to_project(File.dirname(@path))
files_to_load = File.expand_path(file, current_directory)
if files_to_load =~ /\*$/
# Case when we want to require a directory and subdirectories
if files_to_load.size > 1 && files_to_load[-2..-1] == "**"
files_to_load += "/*.cr"
else
files_to_load += ".cr"
end
elsif files_to_load !~ /\.cr$/
files_to_load = files_to_load + ".cr" # << Add the extension for the crystal file.
end
idx = Coverage::SourceFile.require_expanders.size
list_of_required_file = [] of Coverage::SourceFile
Coverage::SourceFile.require_expanders << list_of_required_file
Dir[files_to_load].sort.each do |file_load|
next if file_load !~ /\.cr$/
Coverage::SourceFile.cover_file(file_load) do
line_number = node.location.not_nil!.line_number
required_file = Coverage::SourceFile.new(path: file_load, source: ::File.read(file_load),
required_at: line_number)
required_file.process # Process on load, since it can change the requirement order
list_of_required_file << required_file
end
end
node.string = "$#{idx}"
end
false
end
# Do not visit sub elements of inlined computations
def visit(node : Crystal::OpAssign | Crystal::BinaryOp)
true
end
def visit(node : Crystal::Arg)
name = node.name
if CRYSTAL_KEYWORDS.includes?(name)
node.external_name = node.name = "_#{name}"
end
true
end
# Placeholder for bug #XXX
def visit(node : Crystal::Assign)
target = node.target
value = node.value
if target.is_a?(Crystal::InstanceVar) &&
value.is_a?(Crystal::Var)
if CRYSTAL_KEYWORDS.includes?(value.name)
value.name = "_#{value.name}"
end
end
true
end
def visit(node : Macro)
node.body.accept(self)
false
end
def visit(node : Crystal::Expressions)
node.expressions = node.expressions.map { |elm| inject_cover(elm) }.flatten
true
end
def visit(node : Crystal::Block | Crystal::While)
node.body = force_inject_cover(node.body)
true
end
def visit(node : Crystal::MacroExpression)
false
end
def visit(node : Crystal::MacroLiteral)
false
end
def visit(node : Crystal::MacroIf)
# Fix the non-location issue on macro.
return false if node.location.nil?
propagate_location_in_macro(node, node.location.not_nil!)
node.then = force_inject_cover(node.then)
node.else = force_inject_cover(node.else)
true
end
def visit(node : Crystal::MacroFor)
# Fix the non-location issue on macro.
return false if node.location.nil?
propagate_location_in_macro(node, node.location.not_nil!)
node.body = force_inject_cover(node.body)
false
end
def visit(node : Crystal::MacroVar)
false
end
def visit(node : Crystal::Asm)
false
end
def visit(node : Crystal::Def)
unless node.macro_def?
node.body = force_inject_cover(node.body)
end
true
end
def visit(node : Crystal::Select)
node.whens = node.whens.map { |w| Crystal::Select::When.new(body: force_inject_cover(w.body), condition: w.condition) }
true
end
def visit(node : Crystal::Case)
node.whens = node.whens.map { |w| Crystal::When.new(w.conds, force_inject_cover(w.body)) }
node.else = force_inject_cover(node.else.not_nil!) if node.else
true
end
def visit(node : Crystal::If)
unless node.ternary?
node.then = force_inject_cover(node.then)
node.else = force_inject_cover(node.else)
end
true
end
def visit(node : Crystal::Unless)
node.then = force_inject_cover(node.then)
node.else = force_inject_cover(node.else)
true
end
# Ignore other nodes for now
def visit(node : Crystal::ASTNode)
# puts "#{node.class.name} => " + node.inspect
true
end
end