blob: 0ad8091f97a21ce148f62e14a55e78370769e960 [file] [log] [blame]
Adam Pardylfab9ad62019-08-27 02:07:16 +02001/*
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 Pardyl0f1b3d42019-08-19 15:24:11 +020017package com.android.protolog.tool
Adam Pardylfab9ad62019-08-27 02:07:16 +020018
Adam Pardyl0f1b3d42019-08-19 15:24:11 +020019import com.android.server.protolog.common.LogDataType
Adam Pardylfab9ad62019-08-27 02:07:16 +020020import com.github.javaparser.StaticJavaParser
21import com.github.javaparser.ast.CompilationUnit
22import com.github.javaparser.ast.NodeList
23import com.github.javaparser.ast.body.VariableDeclarator
24import com.github.javaparser.ast.expr.BooleanLiteralExpr
25import com.github.javaparser.ast.expr.CastExpr
Adam Pardyl0f1b3d42019-08-19 15:24:11 +020026import com.github.javaparser.ast.expr.Expression
Adam Pardylfab9ad62019-08-27 02:07:16 +020027import com.github.javaparser.ast.expr.FieldAccessExpr
28import com.github.javaparser.ast.expr.IntegerLiteralExpr
29import com.github.javaparser.ast.expr.MethodCallExpr
30import com.github.javaparser.ast.expr.NameExpr
31import com.github.javaparser.ast.expr.NullLiteralExpr
32import com.github.javaparser.ast.expr.SimpleName
Adam Pardyl0f1b3d42019-08-19 15:24:11 +020033import com.github.javaparser.ast.expr.TypeExpr
Adam Pardylfab9ad62019-08-27 02:07:16 +020034import com.github.javaparser.ast.expr.VariableDeclarationExpr
35import com.github.javaparser.ast.stmt.BlockStmt
36import com.github.javaparser.ast.stmt.ExpressionStmt
37import com.github.javaparser.ast.stmt.IfStmt
38import com.github.javaparser.ast.type.ArrayType
Adam Pardyl0f1b3d42019-08-19 15:24:11 +020039import com.github.javaparser.ast.type.ClassOrInterfaceType
40import com.github.javaparser.ast.type.PrimitiveType
41import com.github.javaparser.ast.type.Type
Adam Pardylfab9ad62019-08-27 02:07:16 +020042import com.github.javaparser.printer.PrettyPrinter
43import com.github.javaparser.printer.PrettyPrinterConfiguration
Adam Pardylfab9ad62019-08-27 02:07:16 +020044
45class SourceTransformer(
46 protoLogImplClassName: String,
Adrian Roos2ca5d862019-10-03 17:30:14 +020047 protoLogCacheClassName: String,
Adam Pardylfab9ad62019-08-27 02:07:16 +020048 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 Pardyl95509992019-09-27 10:09:40 +020075 val hash = CodeUtils.hash(fileName, messageString, level, group)
Adam Pardylfab9ad62019-08-27 02:07:16 +020076 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 Pardyl0f1b3d42019-08-19 15:24:11 +020085 val argTypes = LogDataType.parseFormatString(messageString)
86 val typeMask = LogDataType.logDataTypesToBitMask(argTypes)
Adam Pardylfab9ad62019-08-27 02:07:16 +020087 // 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 Pardyla4e572d2019-09-24 17:56:44 +020092 // Out: com.android.server.protolog.ProtoLogImpl.e(GROUP, 1234, null, arg)
Adam Pardylfab9ad62019-08-27 02:07:16 +020093 newCall.setScope(protoLogImplClassNode)
Adrian Roos2ca5d862019-10-03 17:30:14 +020094 // 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 Pardylfab9ad62019-08-27 02:07:16 +020097 if (argTypes.size != call.arguments.size - 2) {
98 throw InvalidProtoLogCallException(
Adam Pardyl95509992019-09-27 10:09:40 +020099 "Number of arguments (${argTypes.size} does not mach format" +
100 " string in: $call", ParsingContext(fileName, call))
Adam Pardylfab9ad62019-08-27 02:07:16 +0200101 }
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 Pardyl0f1b3d42019-08-19 15:24:11 +0200109 val declaration = VariableDeclarator(getASTTypeForDataType(type), varName,
110 getConversionForType(type)(newCall.arguments[idx + 4].clone()))
Adam Pardylfab9ad62019-08-27 02:07:16 +0200111 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 Pardyla4e572d2019-09-24 17:56:44 +0200122 // Out: if (com.android.server.protolog.ProtoLogImpl.isEnabled(GROUP)) {
Adam Pardylfab9ad62019-08-27 02:07:16 +0200123 // long protoLogParam0 = arg;
Adam Pardyla4e572d2019-09-24 17:56:44 +0200124 // com.android.server.protolog.ProtoLogImpl.e(GROUP, 1234, 0, null, protoLogParam0);
Adam Pardylfab9ad62019-08-27 02:07:16 +0200125 // }
Adam Pardyla4e572d2019-09-24 17:56:44 +0200126 ifStmt = IfStmt(isLogEnabled, blockStmt, null)
Adam Pardylfab9ad62019-08-27 02:07:16 +0200127 } 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 Roosfbcd32e2019-10-17 22:50:08 +0200136 val parentRange = parentStmt.range.get()
137 val newLines = parentRange.end.line - parentRange.begin.line
Adam Pardylfab9ad62019-08-27 02:07:16 +0200138 val newStmt = printedIfStmt.substringBeforeLast('}') + ("\n".repeat(newLines)) + '}'
Adam Pardyl3c6f3382019-09-12 14:19:48 +0200139 // pre-workaround code, see explanation below
140 /*
Adam Pardylfab9ad62019-08-27 02:07:16 +0200141 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 Pardyl3c6f3382019-09-12 14:19:48 +0200149 */
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 Pardylfab9ad62019-08-27 02:07:16 +0200170 }
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 Pardyl0f1b3d42019-08-19 15:24:11 +0200183 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 Pardylfab9ad62019-08-27 02:07:16 +0200211 private val protoLogImplClassNode =
212 StaticJavaParser.parseExpression<FieldAccessExpr>(protoLogImplClassName)
Adrian Roos2ca5d862019-10-03 17:30:14 +0200213 private val protoLogCacheClassNode =
214 StaticJavaParser.parseExpression<FieldAccessExpr>(protoLogCacheClassName)
Adam Pardyl3c6f3382019-09-12 14:19:48 +0200215 private var processedCode: MutableList<String> = mutableListOf()
216 private var offsets: IntArray = IntArray(0)
Adam Pardyla4e572d2019-09-24 17:56:44 +0200217 private var fileName: String = ""
Adam Pardylfab9ad62019-08-27 02:07:16 +0200218
Adam Pardyl3c6f3382019-09-12 14:19:48 +0200219 fun processClass(
220 code: String,
Adam Pardyla4e572d2019-09-24 17:56:44 +0200221 path: String,
Adam Pardyl3c6f3382019-09-12 14:19:48 +0200222 compilationUnit: CompilationUnit =
223 StaticJavaParser.parse(code)
224 ): String {
Adam Pardyla4e572d2019-09-24 17:56:44 +0200225 fileName = path
Adam Pardyl3c6f3382019-09-12 14:19:48 +0200226 processedCode = code.split('\n').toMutableList()
227 offsets = IntArray(processedCode.size)
Adam Pardyl95509992019-09-27 10:09:40 +0200228 protoLogCallProcessor.process(compilationUnit, this, fileName)
Adam Pardyl3c6f3382019-09-12 14:19:48 +0200229 return processedCode.joinToString("\n")
Adam Pardylfab9ad62019-08-27 02:07:16 +0200230 }
231}