# frozen_string_literal: true# Released under the MIT License.# Copyright, 2018-2022, by Samuel Williams.require_relative'wrapper'require'thread'require'parser/current'moduleCovered# The source map, loads the source file, parses the AST to generate which lines contain executable code.classSource<WrapperScript=Struct.new(:path,:source,:line_offset)definitialize(output)super(output)@paths={}@mutex=Mutex.new@annotations={}begin@trace=TracePoint.new(:script_compiled)do|trace|instruction_sequence=trace.instruction_sequence# We only track source files which begin at line 1, as these represent whole files instead of monkey patches.ifinstruction_sequence.first_lineno<=1# Extract the source path and source itself and save it for later:ifpath=instruction_sequence.pathandsource=trace.eval_script@mutex.synchronizedo@paths[path]=Script.new(path,source,instruction_sequence.first_lineno)endendendendrescuewarn"Script coverage disabled: #{$!}"@trace=nilendenddefenablesuper@trace&.enableenddefdisable@trace&.disablesuperendattr:pathsEXECUTABLE={send: true,yield: true,# Line trace point is not triggered for return statements.# return: true,def: true,if: true,lvasgn: true,ivasgn: true,cvasgn: true,gvasgn: true,match_pattern: true,}defexecutable?(node)EXECUTABLE[node.type]endIGNORE={arg: true,}defignore?(node)node.nil?||IGNORE[node.type]endIGNORE_CHILDREN={hash: true,array: true,}defignore_children?(node)IGNORE_CHILDREN[node.type]endIGNORE_METHOD={freeze: true}defignore_method?(name)IGNORE_METHOD[name]enddefexpand(node,coverage,level=0)ifnode.is_a?Parser::AST::Nodeifignore?(node)# coverage.annotate(node.location.line, "ignoring #{node.type}")elsifnode.type==:begin# if last_child = node.children&.last# coverage.counts[last_child.location.line] ||= 0# endexpand(node.children,coverage,level+1)elsifnode.type==:sendifignore_method?(node.children[1])# coverage.annotate(node.location.line, "ignoring #{node.type}")returnelse# coverage.annotate(node.location.line, "accepting selector #{node.type}")coverage.counts[node.location.selector.line]||=0endelsifexecutable?(node)# coverage.annotate(node.location.line, "executable #{node.type}")coverage.counts[node.location.line]||=0endifignore_children?(node)# coverage.annotate(node.location.line, "ignoring #{node.type} children")elseexpand(node.children,coverage,level+1)endelsifnode.is_a?Arraynode.eachdo|child|expand(child,coverage,level)endelsereturnfalseendenddefadd(path,source=nil)ifcoverage=supertop=Parser::CurrentRuby.parse(source)self.expand(top,coverage)endreturncoverageenddefparse(path)ifscript=@paths[path]Parser::CurrentRuby.parse(script.source,script.path,script.line_offset)elsifFile.exist?(path)Parser::CurrentRuby.parse_file(path)else# warn "Couldn't parse #{path}, file doesn't exist?"endrescuewarn"Couldn't parse #{path}: #{$!}"enddefeach(&block)@output.eachdo|coverage|iftop=parse(coverage.path)self.expand(top,coverage)endyieldcoverage.freezeendendendend