|
| 1 | +package dotty.tools.dotc |
| 2 | +package transform |
| 3 | + |
| 4 | +import java.io.File |
| 5 | +import java.util.concurrent.atomic.AtomicInteger |
| 6 | + |
| 7 | +import collection.mutable |
| 8 | +import core.Flags.JavaDefined |
| 9 | +import dotty.tools.dotc.core.Contexts.Context |
| 10 | +import dotty.tools.dotc.core.DenotTransformers.IdentityDenotTransformer |
| 11 | +import dotty.tools.dotc.coverage.Coverage |
| 12 | +import dotty.tools.dotc.coverage.Statement |
| 13 | +import dotty.tools.dotc.coverage.Serializer |
| 14 | +import dotty.tools.dotc.coverage.Location |
| 15 | +import dotty.tools.dotc.core.Symbols.defn |
| 16 | +import dotty.tools.dotc.core.Symbols.Symbol |
| 17 | +import dotty.tools.dotc.core.Decorators.toTermName |
| 18 | +import dotty.tools.dotc.util.SourcePosition |
| 19 | +import dotty.tools.dotc.core.Constants.Constant |
| 20 | +import dotty.tools.dotc.typer.LiftCoverage |
| 21 | + |
| 22 | +import scala.quoted |
| 23 | + |
| 24 | +/** Phase that implements code coverage, executed when the "-coverage |
| 25 | + * OUTPUT_PATH" is added to the compilation. |
| 26 | + */ |
| 27 | +class CoverageTransformMacro extends MacroTransform with IdentityDenotTransformer { |
| 28 | + import ast.tpd._ |
| 29 | + |
| 30 | + override def phaseName = "coverage" |
| 31 | + |
| 32 | + // Atomic counter used for assignation of IDs to difference statements |
| 33 | + val statementId = new AtomicInteger(0) |
| 34 | + |
| 35 | + var outputPath = "" |
| 36 | + |
| 37 | + // Main class used to store all instrumented statements |
| 38 | + val coverage = new Coverage |
| 39 | + |
| 40 | + override def run(using ctx: Context): Unit = { |
| 41 | + |
| 42 | + if (ctx.settings.coverageOutputDir.value.nonEmpty) { |
| 43 | + outputPath = ctx.settings.coverageOutputDir.value |
| 44 | + |
| 45 | + // Ensure the dir exists |
| 46 | + val dataDir = new File(outputPath) |
| 47 | + val newlyCreated = dataDir.mkdirs() |
| 48 | + |
| 49 | + if (!newlyCreated) { |
| 50 | + // If the directory existed before, let's clean it up. |
| 51 | + dataDir.listFiles |
| 52 | + .filter(_.getName.startsWith("scoverage")) |
| 53 | + .foreach(_.delete) |
| 54 | + } |
| 55 | + |
| 56 | + super.run |
| 57 | + |
| 58 | + |
| 59 | + Serializer.serialize(coverage, outputPath, ctx.settings.coverageSourceroot.value) |
| 60 | + } |
| 61 | + } |
| 62 | + |
| 63 | + protected def newTransformer(using Context): Transformer = |
| 64 | + new CoverageTransormer |
| 65 | + |
| 66 | + class CoverageTransormer extends Transformer { |
| 67 | + var instrumented = false |
| 68 | + |
| 69 | + override def transform(tree: Tree)(using Context): Tree = { |
| 70 | + tree match { |
| 71 | + case tree: If => |
| 72 | + cpy.If(tree)( |
| 73 | + cond = transform(tree.cond), |
| 74 | + thenp = instrument(transform(tree.thenp), branch = true), |
| 75 | + elsep = instrument(transform(tree.elsep), branch = true) |
| 76 | + ) |
| 77 | + case tree: Try => |
| 78 | + cpy.Try(tree)( |
| 79 | + expr = instrument(transform(tree.expr), branch = true), |
| 80 | + cases = instrumentCasees(tree.cases), |
| 81 | + finalizer = instrument(transform(tree.finalizer), true) |
| 82 | + ) |
| 83 | + case Apply(fun, _) |
| 84 | + if ( |
| 85 | + fun.symbol.exists && |
| 86 | + fun.symbol.isInstanceOf[Symbol] && |
| 87 | + fun.symbol == defn.Boolean_&& || fun.symbol == defn.Boolean_|| |
| 88 | + ) => |
| 89 | + super.transform(tree) |
| 90 | + case tree @ Apply(fun, args) if (fun.isInstanceOf[Apply]) => |
| 91 | + // We have nested apply, we have to lift all arguments |
| 92 | + // Example: def T(x:Int)(y:Int) |
| 93 | + // T(f())(1) // should not be changed to {val $x = f(); T($x)}(1) but to {val $x = f(); val $y = 1; T($x)($y)} |
| 94 | + liftApply(tree) |
| 95 | + case tree: Apply => |
| 96 | + if (LiftCoverage.needsLift(tree)) { |
| 97 | + liftApply(tree) |
| 98 | + } else { |
| 99 | + super.transform(tree) |
| 100 | + } |
| 101 | + case Select(qual, _) if (qual.symbol.exists && qual.symbol.is(JavaDefined)) => |
| 102 | + //Java class can't be used as a value, we can't instrument the |
| 103 | + //qualifier ({<Probe>;System}.xyz() is not possible !) instrument it |
| 104 | + //as it is |
| 105 | + instrument(tree) |
| 106 | + case tree: Select => |
| 107 | + if (tree.qualifier.isInstanceOf[New]) { |
| 108 | + instrument(tree) |
| 109 | + } else { |
| 110 | + cpy.Select(tree)(transform(tree.qualifier), tree.name) |
| 111 | + } |
| 112 | + case tree: CaseDef => instrumentCaseDef(tree) |
| 113 | + |
| 114 | + case tree: Literal => instrument(tree) |
| 115 | + case tree: Ident if (isWildcardArg(tree)) => |
| 116 | + // We don't want to instrument wildcard arguments. `var a = _` can't be instrumented |
| 117 | + tree |
| 118 | + case tree: New => instrument(tree) |
| 119 | + case tree: This => instrument(tree) |
| 120 | + case tree: Super => instrument(tree) |
| 121 | + case tree: PackageDef => |
| 122 | + // We don't instrument the pid of the package, but we do instrument the statements |
| 123 | + cpy.PackageDef(tree)(tree.pid, transform(tree.stats)) |
| 124 | + case tree: Assign => cpy.Assign(tree)(tree.lhs, transform(tree.rhs)) |
| 125 | + case tree: Template => |
| 126 | + // Don't instrument the parents (extends) of a template since it |
| 127 | + // causes problems if the parent constructor takes parameters |
| 128 | + cpy.Template(tree)( |
| 129 | + constr = super.transformSub(tree.constr), |
| 130 | + body = transform(tree.body) |
| 131 | + ) |
| 132 | + case tree: Import => tree |
| 133 | + // Catch EmptyTree since we can't match directly on it |
| 134 | + case tree: Thicket if tree.isEmpty => tree |
| 135 | + // For everything else just recurse and transform |
| 136 | + case _ => |
| 137 | + report.warning( |
| 138 | + "Unmatched: " + tree.getClass + " " + tree.symbol, |
| 139 | + tree.sourcePos |
| 140 | + ) |
| 141 | + super.transform(tree) |
| 142 | + } |
| 143 | + } |
| 144 | + |
| 145 | + def liftApply(tree: Apply)(using Context) = { |
| 146 | + val buffer = mutable.ListBuffer[Tree]() |
| 147 | + // NOTE: that if only one arg needs to be lifted, we just lift everything |
| 148 | + val lifted = LiftCoverage.liftForCoverage(buffer, tree) |
| 149 | + val instrumented = buffer.toList.map(transform) |
| 150 | + //We can now instrument the apply as it is with a custom position to point to the function |
| 151 | + Block( |
| 152 | + instrumented, |
| 153 | + instrument( |
| 154 | + lifted, |
| 155 | + tree.sourcePos, |
| 156 | + false |
| 157 | + ) |
| 158 | + ) |
| 159 | + } |
| 160 | + |
| 161 | + def instrumentCasees(cases: List[CaseDef])(using Context): List[CaseDef] = { |
| 162 | + cases.map(instrumentCaseDef) |
| 163 | + } |
| 164 | + |
| 165 | + def instrumentCaseDef(tree: CaseDef)(using Context): CaseDef = { |
| 166 | + cpy.CaseDef(tree)(tree.pat, transform(tree.guard), transform(tree.body)) |
| 167 | + } |
| 168 | + |
| 169 | + def instrument(tree: Tree, branch: Boolean = false)(using Context): Tree = { |
| 170 | + instrument(tree, tree.sourcePos, branch) |
| 171 | + } |
| 172 | + |
| 173 | + def instrument(tree: Tree, pos: SourcePosition, branch: Boolean)(using ctx: Context): Tree = { |
| 174 | + if (pos.exists && !pos.span.isZeroExtent && !tree.isType) { |
| 175 | + val id = statementId.incrementAndGet() |
| 176 | + val statement = new Statement( |
| 177 | + source = ctx.source.file.name, |
| 178 | + location = Location(tree), |
| 179 | + id = id, |
| 180 | + start = pos.start, |
| 181 | + end = pos.end, |
| 182 | + line = ctx.source.offsetToLine(pos.point), |
| 183 | + desc = tree.source.content.slice(pos.start, pos.end).mkString, |
| 184 | + symbolName = tree.symbol.name.toSimpleName.toString(), |
| 185 | + treeName = tree.getClass.getSimpleName, |
| 186 | + branch |
| 187 | + ) |
| 188 | + coverage.addStatement(statement) |
| 189 | + Block(List(invokeCall(id)), tree) |
| 190 | + } else { |
| 191 | + tree |
| 192 | + } |
| 193 | + } |
| 194 | + |
| 195 | + def invokeCall(id: Int)(using Context): Tree = { |
| 196 | + ref(defn.InvokerModuleRef) |
| 197 | + .select("invoked".toTermName) |
| 198 | + .appliedToArgs( |
| 199 | + List(Literal(Constant(id)), Literal(Constant(outputPath))) |
| 200 | + ) |
| 201 | + } |
| 202 | + } |
| 203 | + |
| 204 | +} |
0 commit comments