Adam Pardyl | fab9ad6 | 2019-08-27 02:07:16 +0200 | [diff] [blame] | 1 | /* |
| 2 | * Copyright (C) 2019 The Android Open Source Project |
| 3 | * |
| 4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | * you may not use this file except in compliance with the License. |
| 6 | * You may obtain a copy of the License at |
| 7 | * |
| 8 | * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | * |
| 10 | * Unless required by applicable law or agreed to in writing, software |
| 11 | * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | * See the License for the specific language governing permissions and |
| 14 | * limitations under the License. |
| 15 | */ |
| 16 | |
Adam Pardyl | 0f1b3d4 | 2019-08-19 15:24:11 +0200 | [diff] [blame] | 17 | package com.android.protolog.tool |
Adam Pardyl | fab9ad6 | 2019-08-27 02:07:16 +0200 | [diff] [blame] | 18 | |
Adam Pardyl | 0f1b3d4 | 2019-08-19 15:24:11 +0200 | [diff] [blame] | 19 | import com.android.server.protolog.common.LogDataType |
Adam Pardyl | fab9ad6 | 2019-08-27 02:07:16 +0200 | [diff] [blame] | 20 | import com.github.javaparser.StaticJavaParser |
| 21 | import com.github.javaparser.ast.CompilationUnit |
| 22 | import com.github.javaparser.ast.NodeList |
| 23 | import com.github.javaparser.ast.body.VariableDeclarator |
| 24 | import com.github.javaparser.ast.expr.BooleanLiteralExpr |
| 25 | import com.github.javaparser.ast.expr.CastExpr |
Adam Pardyl | 0f1b3d4 | 2019-08-19 15:24:11 +0200 | [diff] [blame] | 26 | import com.github.javaparser.ast.expr.Expression |
Adam Pardyl | fab9ad6 | 2019-08-27 02:07:16 +0200 | [diff] [blame] | 27 | import com.github.javaparser.ast.expr.FieldAccessExpr |
| 28 | import com.github.javaparser.ast.expr.IntegerLiteralExpr |
| 29 | import com.github.javaparser.ast.expr.MethodCallExpr |
| 30 | import com.github.javaparser.ast.expr.NameExpr |
| 31 | import com.github.javaparser.ast.expr.NullLiteralExpr |
| 32 | import com.github.javaparser.ast.expr.SimpleName |
Adam Pardyl | 0f1b3d4 | 2019-08-19 15:24:11 +0200 | [diff] [blame] | 33 | import com.github.javaparser.ast.expr.TypeExpr |
Adam Pardyl | fab9ad6 | 2019-08-27 02:07:16 +0200 | [diff] [blame] | 34 | import com.github.javaparser.ast.expr.VariableDeclarationExpr |
| 35 | import com.github.javaparser.ast.stmt.BlockStmt |
| 36 | import com.github.javaparser.ast.stmt.ExpressionStmt |
| 37 | import com.github.javaparser.ast.stmt.IfStmt |
| 38 | import com.github.javaparser.ast.type.ArrayType |
Adam Pardyl | 0f1b3d4 | 2019-08-19 15:24:11 +0200 | [diff] [blame] | 39 | import com.github.javaparser.ast.type.ClassOrInterfaceType |
| 40 | import com.github.javaparser.ast.type.PrimitiveType |
| 41 | import com.github.javaparser.ast.type.Type |
Adam Pardyl | fab9ad6 | 2019-08-27 02:07:16 +0200 | [diff] [blame] | 42 | import com.github.javaparser.printer.PrettyPrinter |
| 43 | import com.github.javaparser.printer.PrettyPrinterConfiguration |
Adam Pardyl | fab9ad6 | 2019-08-27 02:07:16 +0200 | [diff] [blame] | 44 | |
| 45 | class SourceTransformer( |
| 46 | protoLogImplClassName: String, |
Adrian Roos | 2ca5d86 | 2019-10-03 17:30:14 +0200 | [diff] [blame] | 47 | protoLogCacheClassName: String, |
Adam Pardyl | fab9ad6 | 2019-08-27 02:07:16 +0200 | [diff] [blame] | 48 | private val protoLogCallProcessor: ProtoLogCallProcessor |
| 49 | ) : ProtoLogCallVisitor { |
| 50 | override fun processCall( |
| 51 | call: MethodCallExpr, |
| 52 | messageString: String, |
| 53 | level: LogLevel, |
| 54 | group: LogGroup |
| 55 | ) { |
| 56 | // Input format: ProtoLog.e(GROUP, "msg %d", arg) |
| 57 | if (!call.parentNode.isPresent) { |
| 58 | // Should never happen |
| 59 | throw RuntimeException("Unable to process log call $call " + |
| 60 | "- no parent node in AST") |
| 61 | } |
| 62 | if (call.parentNode.get() !is ExpressionStmt) { |
| 63 | // Should never happen |
| 64 | throw RuntimeException("Unable to process log call $call " + |
| 65 | "- parent node in AST is not an ExpressionStmt") |
| 66 | } |
| 67 | val parentStmt = call.parentNode.get() as ExpressionStmt |
| 68 | if (!parentStmt.parentNode.isPresent) { |
| 69 | // Should never happen |
| 70 | throw RuntimeException("Unable to process log call $call " + |
| 71 | "- no grandparent node in AST") |
| 72 | } |
| 73 | val ifStmt: IfStmt |
| 74 | if (group.enabled) { |
Adam Pardyl | 9550999 | 2019-09-27 10:09:40 +0200 | [diff] [blame] | 75 | val hash = CodeUtils.hash(fileName, messageString, level, group) |
Adam Pardyl | fab9ad6 | 2019-08-27 02:07:16 +0200 | [diff] [blame] | 76 | val newCall = call.clone() |
| 77 | if (!group.textEnabled) { |
| 78 | // Remove message string if text logging is not enabled by default. |
| 79 | // Out: ProtoLog.e(GROUP, null, arg) |
| 80 | newCall.arguments[1].replace(NameExpr("null")) |
| 81 | } |
| 82 | // Insert message string hash as a second argument. |
| 83 | // Out: ProtoLog.e(GROUP, 1234, null, arg) |
| 84 | newCall.arguments.add(1, IntegerLiteralExpr(hash)) |
Adam Pardyl | 0f1b3d4 | 2019-08-19 15:24:11 +0200 | [diff] [blame] | 85 | val argTypes = LogDataType.parseFormatString(messageString) |
| 86 | val typeMask = LogDataType.logDataTypesToBitMask(argTypes) |
Adam Pardyl | fab9ad6 | 2019-08-27 02:07:16 +0200 | [diff] [blame] | 87 | // Insert bitmap representing which Number parameters are to be considered as |
| 88 | // floating point numbers. |
| 89 | // Out: ProtoLog.e(GROUP, 1234, 0, null, arg) |
| 90 | newCall.arguments.add(2, IntegerLiteralExpr(typeMask)) |
| 91 | // Replace call to a stub method with an actual implementation. |
Adam Pardyl | a4e572d | 2019-09-24 17:56:44 +0200 | [diff] [blame] | 92 | // Out: com.android.server.protolog.ProtoLogImpl.e(GROUP, 1234, null, arg) |
Adam Pardyl | fab9ad6 | 2019-08-27 02:07:16 +0200 | [diff] [blame] | 93 | newCall.setScope(protoLogImplClassNode) |
Adrian Roos | 2ca5d86 | 2019-10-03 17:30:14 +0200 | [diff] [blame] | 94 | // Create a call to ProtoLog$Cache.GROUP_enabled |
| 95 | // Out: com.android.server.protolog.ProtoLog$Cache.GROUP_enabled |
| 96 | val isLogEnabled = FieldAccessExpr(protoLogCacheClassNode, "${group.name}_enabled") |
Adam Pardyl | fab9ad6 | 2019-08-27 02:07:16 +0200 | [diff] [blame] | 97 | if (argTypes.size != call.arguments.size - 2) { |
| 98 | throw InvalidProtoLogCallException( |
Adam Pardyl | 9550999 | 2019-09-27 10:09:40 +0200 | [diff] [blame] | 99 | "Number of arguments (${argTypes.size} does not mach format" + |
| 100 | " string in: $call", ParsingContext(fileName, call)) |
Adam Pardyl | fab9ad6 | 2019-08-27 02:07:16 +0200 | [diff] [blame] | 101 | } |
| 102 | val blockStmt = BlockStmt() |
| 103 | if (argTypes.isNotEmpty()) { |
| 104 | // Assign every argument to a variable to check its type in compile time |
| 105 | // (this is assignment is optimized-out by dex tool, there is no runtime impact)/ |
| 106 | // Out: long protoLogParam0 = arg |
| 107 | argTypes.forEachIndexed { idx, type -> |
| 108 | val varName = "protoLogParam$idx" |
Adam Pardyl | 0f1b3d4 | 2019-08-19 15:24:11 +0200 | [diff] [blame] | 109 | val declaration = VariableDeclarator(getASTTypeForDataType(type), varName, |
| 110 | getConversionForType(type)(newCall.arguments[idx + 4].clone())) |
Adam Pardyl | fab9ad6 | 2019-08-27 02:07:16 +0200 | [diff] [blame] | 111 | blockStmt.addStatement(ExpressionStmt(VariableDeclarationExpr(declaration))) |
| 112 | newCall.setArgument(idx + 4, NameExpr(SimpleName(varName))) |
| 113 | } |
| 114 | } else { |
| 115 | // Assign (Object[])null as the vararg parameter to prevent allocating an empty |
| 116 | // object array. |
| 117 | val nullArray = CastExpr(ArrayType(objectType), NullLiteralExpr()) |
| 118 | newCall.addArgument(nullArray) |
| 119 | } |
| 120 | blockStmt.addStatement(ExpressionStmt(newCall)) |
| 121 | // Create an IF-statement with the previously created condition. |
Adam Pardyl | a4e572d | 2019-09-24 17:56:44 +0200 | [diff] [blame] | 122 | // Out: if (com.android.server.protolog.ProtoLogImpl.isEnabled(GROUP)) { |
Adam Pardyl | fab9ad6 | 2019-08-27 02:07:16 +0200 | [diff] [blame] | 123 | // long protoLogParam0 = arg; |
Adam Pardyl | a4e572d | 2019-09-24 17:56:44 +0200 | [diff] [blame] | 124 | // com.android.server.protolog.ProtoLogImpl.e(GROUP, 1234, 0, null, protoLogParam0); |
Adam Pardyl | fab9ad6 | 2019-08-27 02:07:16 +0200 | [diff] [blame] | 125 | // } |
Adam Pardyl | a4e572d | 2019-09-24 17:56:44 +0200 | [diff] [blame] | 126 | ifStmt = IfStmt(isLogEnabled, blockStmt, null) |
Adam Pardyl | fab9ad6 | 2019-08-27 02:07:16 +0200 | [diff] [blame] | 127 | } else { |
| 128 | // Surround with if (false). |
| 129 | val newCall = parentStmt.clone() |
| 130 | ifStmt = IfStmt(BooleanLiteralExpr(false), BlockStmt(NodeList(newCall)), null) |
| 131 | newCall.setBlockComment(" ${group.name} is disabled ") |
| 132 | } |
| 133 | // Inline the new statement. |
| 134 | val printedIfStmt = inlinePrinter.print(ifStmt) |
| 135 | // Append blank lines to preserve line numbering in file (to allow debugging) |
Adrian Roos | fbcd32e | 2019-10-17 22:50:08 +0200 | [diff] [blame] | 136 | val parentRange = parentStmt.range.get() |
| 137 | val newLines = parentRange.end.line - parentRange.begin.line |
Adam Pardyl | fab9ad6 | 2019-08-27 02:07:16 +0200 | [diff] [blame] | 138 | val newStmt = printedIfStmt.substringBeforeLast('}') + ("\n".repeat(newLines)) + '}' |
Adam Pardyl | 3c6f338 | 2019-09-12 14:19:48 +0200 | [diff] [blame] | 139 | // pre-workaround code, see explanation below |
| 140 | /* |
Adam Pardyl | fab9ad6 | 2019-08-27 02:07:16 +0200 | [diff] [blame] | 141 | val inlinedIfStmt = StaticJavaParser.parseStatement(newStmt) |
| 142 | LexicalPreservingPrinter.setup(inlinedIfStmt) |
| 143 | // Replace the original call. |
| 144 | if (!parentStmt.replace(inlinedIfStmt)) { |
| 145 | // Should never happen |
| 146 | throw RuntimeException("Unable to process log call $call " + |
| 147 | "- unable to replace the call.") |
| 148 | } |
Adam Pardyl | 3c6f338 | 2019-09-12 14:19:48 +0200 | [diff] [blame] | 149 | */ |
| 150 | /** Workaround for a bug in JavaParser (AST tree invalid after replacing a node when using |
| 151 | * LexicalPreservingPrinter (https://github.com/javaparser/javaparser/issues/2290). |
| 152 | * Replace the code below with the one commended-out above one the issue is resolved. */ |
| 153 | if (!parentStmt.range.isPresent) { |
| 154 | // Should never happen |
| 155 | throw RuntimeException("Unable to process log call $call " + |
| 156 | "- unable to replace the call.") |
| 157 | } |
| 158 | val range = parentStmt.range.get() |
| 159 | val begin = range.begin.line - 1 |
| 160 | val oldLines = processedCode.subList(begin, range.end.line) |
| 161 | val oldCode = oldLines.joinToString("\n") |
| 162 | val newCode = oldCode.replaceRange( |
| 163 | offsets[begin] + range.begin.column - 1, |
| 164 | oldCode.length - oldLines.lastOrNull()!!.length + |
| 165 | range.end.column + offsets[range.end.line - 1], newStmt) |
| 166 | newCode.split("\n").forEachIndexed { idx, line -> |
| 167 | offsets[begin + idx] += line.length - processedCode[begin + idx].length |
| 168 | processedCode[begin + idx] = line |
| 169 | } |
Adam Pardyl | fab9ad6 | 2019-08-27 02:07:16 +0200 | [diff] [blame] | 170 | } |
| 171 | |
| 172 | private val inlinePrinter: PrettyPrinter |
| 173 | private val objectType = StaticJavaParser.parseClassOrInterfaceType("Object") |
| 174 | |
| 175 | init { |
| 176 | val config = PrettyPrinterConfiguration() |
| 177 | config.endOfLineCharacter = " " |
| 178 | config.indentSize = 0 |
| 179 | config.tabWidth = 1 |
| 180 | inlinePrinter = PrettyPrinter(config) |
| 181 | } |
| 182 | |
Adam Pardyl | 0f1b3d4 | 2019-08-19 15:24:11 +0200 | [diff] [blame] | 183 | companion object { |
| 184 | private val stringType: ClassOrInterfaceType = |
| 185 | StaticJavaParser.parseClassOrInterfaceType("String") |
| 186 | |
| 187 | fun getASTTypeForDataType(type: Int): Type { |
| 188 | return when (type) { |
| 189 | LogDataType.STRING -> stringType.clone() |
| 190 | LogDataType.LONG -> PrimitiveType.longType() |
| 191 | LogDataType.DOUBLE -> PrimitiveType.doubleType() |
| 192 | LogDataType.BOOLEAN -> PrimitiveType.booleanType() |
| 193 | else -> { |
| 194 | // Should never happen. |
| 195 | throw RuntimeException("Invalid LogDataType") |
| 196 | } |
| 197 | } |
| 198 | } |
| 199 | |
| 200 | fun getConversionForType(type: Int): (Expression) -> Expression { |
| 201 | return when (type) { |
| 202 | LogDataType.STRING -> { expr -> |
| 203 | MethodCallExpr(TypeExpr(StaticJavaParser.parseClassOrInterfaceType("String")), |
| 204 | SimpleName("valueOf"), NodeList(expr)) |
| 205 | } |
| 206 | else -> { expr -> expr } |
| 207 | } |
| 208 | } |
| 209 | } |
| 210 | |
Adam Pardyl | fab9ad6 | 2019-08-27 02:07:16 +0200 | [diff] [blame] | 211 | private val protoLogImplClassNode = |
| 212 | StaticJavaParser.parseExpression<FieldAccessExpr>(protoLogImplClassName) |
Adrian Roos | 2ca5d86 | 2019-10-03 17:30:14 +0200 | [diff] [blame] | 213 | private val protoLogCacheClassNode = |
| 214 | StaticJavaParser.parseExpression<FieldAccessExpr>(protoLogCacheClassName) |
Adam Pardyl | 3c6f338 | 2019-09-12 14:19:48 +0200 | [diff] [blame] | 215 | private var processedCode: MutableList<String> = mutableListOf() |
| 216 | private var offsets: IntArray = IntArray(0) |
Adam Pardyl | a4e572d | 2019-09-24 17:56:44 +0200 | [diff] [blame] | 217 | private var fileName: String = "" |
Adam Pardyl | fab9ad6 | 2019-08-27 02:07:16 +0200 | [diff] [blame] | 218 | |
Adam Pardyl | 3c6f338 | 2019-09-12 14:19:48 +0200 | [diff] [blame] | 219 | fun processClass( |
| 220 | code: String, |
Adam Pardyl | a4e572d | 2019-09-24 17:56:44 +0200 | [diff] [blame] | 221 | path: String, |
Adam Pardyl | 3c6f338 | 2019-09-12 14:19:48 +0200 | [diff] [blame] | 222 | compilationUnit: CompilationUnit = |
| 223 | StaticJavaParser.parse(code) |
| 224 | ): String { |
Adam Pardyl | a4e572d | 2019-09-24 17:56:44 +0200 | [diff] [blame] | 225 | fileName = path |
Adam Pardyl | 3c6f338 | 2019-09-12 14:19:48 +0200 | [diff] [blame] | 226 | processedCode = code.split('\n').toMutableList() |
| 227 | offsets = IntArray(processedCode.size) |
Adam Pardyl | 9550999 | 2019-09-27 10:09:40 +0200 | [diff] [blame] | 228 | protoLogCallProcessor.process(compilationUnit, this, fileName) |
Adam Pardyl | 3c6f338 | 2019-09-12 14:19:48 +0200 | [diff] [blame] | 229 | return processedCode.joinToString("\n") |
Adam Pardyl | fab9ad6 | 2019-08-27 02:07:16 +0200 | [diff] [blame] | 230 | } |
| 231 | } |