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