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 17 package com.android.protolog.tool 18 19 import com.android.internal.protolog.common.LogDataType 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 26 import com.github.javaparser.ast.expr.Expression 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 33 import com.github.javaparser.ast.expr.TypeExpr 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 39 import com.github.javaparser.ast.type.ClassOrInterfaceType 40 import com.github.javaparser.ast.type.PrimitiveType 41 import com.github.javaparser.ast.type.Type 42 import com.github.javaparser.printer.PrettyPrinter 43 import com.github.javaparser.printer.PrettyPrinterConfiguration 44 45 class SourceTransformer( 46 protoLogImplClassName: String, 47 protoLogCacheClassName: String, 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) { 75 val hash = CodeUtils.hash(packagePath, messageString, level, group) 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)) 85 val argTypes = LogDataType.parseFormatString(messageString) 86 val typeMask = LogDataType.logDataTypesToBitMask(argTypes) 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. 92 // Out: ProtoLogImpl.e(GROUP, 1234, null, arg) 93 newCall.setScope(protoLogImplClassNode) 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") 97 if (argTypes.size != call.arguments.size - 2) { 98 throw InvalidProtoLogCallException( 99 "Number of arguments (${argTypes.size} does not mach format" + 100 " string in: $call", ParsingContext(path, call)) 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" 109 val declaration = VariableDeclarator(getASTTypeForDataType(type), varName, 110 getConversionForType(type)(newCall.arguments[idx + 4].clone())) 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. 122 // Out: if (ProtoLogImpl.isEnabled(GROUP)) { 123 // long protoLogParam0 = arg; 124 // ProtoLogImpl.e(GROUP, 1234, 0, null, protoLogParam0); 125 // } 126 ifStmt = IfStmt(isLogEnabled, blockStmt, null) 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) 136 val parentRange = parentStmt.range.get() 137 val newLines = parentRange.end.line - parentRange.begin.line 138 val newStmt = printedIfStmt.substringBeforeLast('}') + ("\n".repeat(newLines)) + '}' 139 // pre-workaround code, see explanation below 140 /* 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 } 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 } 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 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 211 private val protoLogImplClassNode = 212 StaticJavaParser.parseExpression<FieldAccessExpr>(protoLogImplClassName) 213 private val protoLogCacheClassNode = 214 StaticJavaParser.parseExpression<FieldAccessExpr>(protoLogCacheClassName) 215 private var processedCode: MutableList<String> = mutableListOf() 216 private var offsets: IntArray = IntArray(0) 217 /** The path of the file being processed, relative to $ANDROID_BUILD_TOP */ 218 private var path: String = "" 219 /** The path of the file being processed, relative to the root package */ 220 private var packagePath: String = "" 221 222 fun processClass( 223 code: String, 224 path: String, 225 packagePath: String, 226 compilationUnit: CompilationUnit = 227 StaticJavaParser.parse(code) 228 ): String { 229 this.path = path 230 this.packagePath = packagePath 231 processedCode = code.split('\n').toMutableList() 232 offsets = IntArray(processedCode.size) 233 protoLogCallProcessor.process(compilationUnit, this, path) 234 return processedCode.joinToString("\n") 235 } 236 } 237