1 /*
<lambda>null2 * Copyright (C) 2017 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.tools.metalava.model
18
19 import com.android.tools.metalava.model.item.FieldValue
20 import java.io.PrintWriter
21
22 @MetalavaApi
23 interface FieldItem : MemberItem, InheritableItem {
24 /** The property this field backs; inverse of [PropertyItem.backingField] */
25 val property: PropertyItem?
26 get() = null
27
28 /** The type of this field */
29 @MetalavaApi override fun type(): TypeItem
30
31 override fun findCorrespondingItemIn(
32 codebase: Codebase,
33 superMethods: Boolean,
34 duplicate: Boolean,
35 ) = containingClass().findCorrespondingItemIn(codebase)?.findField(name())
36
37 /**
38 * The optional value of this [FieldItem].
39 *
40 * This is called `legacy` because this an old, inconsistent representation of the field value
41 * that exposes implementation details. It will be replaced by a properly modelled value
42 * representation.
43 */
44 val legacyFieldValue: FieldValue?
45
46 /**
47 * The legacy initial/constant value, if any. If [requireConstant] the initial value will only
48 * be returned if it's constant.
49 *
50 * This is called `legacy` because this an old, inconsistent representation of the field value
51 * that exposes implementation details. It will be replaced by a properly modelled value
52 * representation.
53 */
54 fun legacyInitialValue(requireConstant: Boolean = true): Any?
55
56 /**
57 * An enum can contain both enum constants and fields; this method provides a way to distinguish
58 * between them.
59 */
60 fun isEnumConstant(): Boolean
61
62 /**
63 * Duplicates this field item.
64 *
65 * Override to specialize the return type.
66 */
67 override fun duplicate(targetContainingClass: ClassItem): FieldItem
68
69 override fun baselineElementId() = containingClass().qualifiedName() + "#" + name()
70
71 override fun accept(visitor: ItemVisitor) {
72 visitor.visit(this)
73 }
74
75 override fun equalsToItem(other: Any?): Boolean {
76 if (this === other) return true
77 if (other !is FieldItem) return false
78
79 return name() == other.name() && containingClass() == other.containingClass()
80 }
81
82 override fun hashCodeForItem(): Int {
83 return name().hashCode()
84 }
85
86 override fun toStringForItem() = "field ${containingClass().fullName()}.${name()}"
87
88 /**
89 * Check the declared value with a typed comparison, not a string comparison, to accommodate
90 * toolchains with different fp -> string conversions.
91 */
92 fun hasSameValue(other: FieldItem): Boolean {
93 val thisConstant = legacyInitialValue()
94 val otherConstant = other.legacyInitialValue()
95 if (thisConstant == null != (otherConstant == null)) {
96 return false
97 }
98
99 // Null values are considered equal
100 if (thisConstant == null) {
101 return true
102 }
103
104 if (type() != other.type()) {
105 return false
106 }
107
108 if (thisConstant == otherConstant) {
109 return true
110 }
111
112 if (thisConstant.toString() == otherConstant.toString()) {
113 // e.g. Integer(3) and Short(3) are the same; when comparing
114 // with signature files we sometimes don't have the right
115 // types from signatures
116 return true
117 }
118
119 return false
120 }
121
122 companion object {
123 val comparator: java.util.Comparator<FieldItem> = Comparator { a, b ->
124 a.name().compareTo(b.name())
125 }
126
127 /**
128 * Comparator that will order [FieldItem]s such that those for which
129 * [FieldItem.isEnumConstant] returns `true` will come before those for which it is `false`.
130 */
131 val comparatorEnumConstantFirst: java.util.Comparator<FieldItem> =
132 Comparator.comparing(FieldItem::isEnumConstant).reversed().thenComparing(comparator)
133 }
134
135 /**
136 * If this field has no initial value, it just writes ";", otherwise it writes " = value;" with
137 * the correct Java syntax for the initial value
138 */
139 fun writeValueWithSemicolon(
140 writer: PrintWriter,
141 allowDefaultValue: Boolean = false,
142 requireInitialValue: Boolean = false
143 ) {
144 val value =
145 legacyInitialValue(!allowDefaultValue)
146 ?: if (allowDefaultValue && !containingClass().isClass()) type().defaultValue()
147 else null
148 if (value != null) {
149 when (value) {
150 is Int -> {
151 writer.print(" = ")
152 writer.print(value)
153 writer.print("; // 0x")
154 writer.print(Integer.toHexString(value))
155 }
156 is String -> {
157 writer.print(" = ")
158 writer.print('"')
159 writer.print(javaEscapeString(value))
160 writer.print('"')
161 writer.print(";")
162 }
163 is Long -> {
164 writer.print(" = ")
165 writer.print(value)
166 writer.print(String.format("L; // 0x%xL", value))
167 }
168 is Boolean -> {
169 writer.print(" = ")
170 writer.print(value)
171 writer.print(";")
172 }
173 is Byte -> {
174 writer.print(" = ")
175 writer.print(value)
176 writer.print("; // 0x")
177 writer.print(Integer.toHexString(value.toInt()))
178 }
179 is Short -> {
180 writer.print(" = ")
181 writer.print(value)
182 writer.print("; // 0x")
183 writer.print(Integer.toHexString(value.toInt()))
184 }
185 is Float -> {
186 writer.print(" = ")
187 when {
188 value == Float.POSITIVE_INFINITY -> writer.print("(1.0f/0.0f);")
189 value == Float.NEGATIVE_INFINITY -> writer.print("(-1.0f/0.0f);")
190 java.lang.Float.isNaN(value) -> writer.print("(0.0f/0.0f);")
191 // Force MIN_NORMAL to use the String representation created by
192 // java.lang.Float.toString() before the bug fix in JDK 19 - see
193 // https://inside.java/2022/09/23/quality-heads-up/ for details.
194 value == java.lang.Float.MIN_NORMAL ->
195 writer.format("1.17549435E-38f;", value)
196 else -> {
197 writer.print(canonicalizeFloatingPointString(value.toString()))
198 writer.print("f;")
199 }
200 }
201 }
202 is Double -> {
203 writer.print(" = ")
204 when {
205 value == Double.POSITIVE_INFINITY -> writer.print("(1.0/0.0);")
206 value == Double.NEGATIVE_INFINITY -> writer.print("(-1.0/0.0);")
207 java.lang.Double.isNaN(value) -> writer.print("(0.0/0.0);")
208 else -> {
209 writer.print(canonicalizeFloatingPointString(value.toString()))
210 writer.print(";")
211 }
212 }
213 }
214 is Char -> {
215 writer.print(" = ")
216 val intValue = value.code
217 writer.print(intValue)
218 writer.print("; // ")
219 writer.print(
220 String.format("0x%04x '%s'", intValue, javaEscapeString(value.toString()))
221 )
222 }
223 else -> {
224 writer.print(';')
225 }
226 }
227 } else {
228 // in interfaces etc we must have an initial value
229 if (requireInitialValue && !containingClass().isClass()) {
230 writer.print(" = null")
231 }
232 writer.print(';')
233 }
234 }
235 }
236
javaEscapeStringnull237 fun javaEscapeString(str: String): String {
238 var result = ""
239 val n = str.length
240 for (i in 0 until n) {
241 val c = str[i]
242 result +=
243 when (c) {
244 '\\' -> "\\\\"
245 '\t' -> "\\t"
246 '\b' -> "\\b"
247 '\r' -> "\\r"
248 '\n' -> "\\n"
249 '\'' -> "\\'"
250 '\"' -> "\\\""
251 in ' '..'~' -> c
252 else -> String.format("\\u%04x", c.code)
253 }
254 }
255 return result
256 }
257
258 // From doclava1 TextFieldItem#javaUnescapeString
259 @Suppress("LocalVariableName")
javaUnescapeStringnull260 fun javaUnescapeString(str: String): String {
261 val n = str.length
262 var simple = true
263 for (i in 0 until n) {
264 val c = str[i]
265 if (c == '\\') {
266 simple = false
267 break
268 }
269 }
270 if (simple) {
271 return str
272 }
273
274 val buf = StringBuilder(str.length)
275 var escaped: Char = 0.toChar()
276 val START = 0
277 val CHAR1 = 1
278 val CHAR2 = 2
279 val CHAR3 = 3
280 val CHAR4 = 4
281 val ESCAPE = 5
282 var state = START
283
284 for (i in 0 until n) {
285 val c = str[i]
286 when (state) {
287 START ->
288 if (c == '\\') {
289 state = ESCAPE
290 } else {
291 buf.append(c)
292 }
293 ESCAPE ->
294 when (c) {
295 '\\' -> {
296 buf.append('\\')
297 state = START
298 }
299 't' -> {
300 buf.append('\t')
301 state = START
302 }
303 'b' -> {
304 buf.append('\b')
305 state = START
306 }
307 'r' -> {
308 buf.append('\r')
309 state = START
310 }
311 'n' -> {
312 buf.append('\n')
313 state = START
314 }
315 '\'' -> {
316 buf.append('\'')
317 state = START
318 }
319 '\"' -> {
320 buf.append('\"')
321 state = START
322 }
323 'u' -> {
324 state = CHAR1
325 escaped = 0.toChar()
326 }
327 }
328 CHAR1,
329 CHAR2,
330 CHAR3,
331 CHAR4 -> {
332 escaped = (escaped.code shl 4).toChar()
333 escaped =
334 when (c) {
335 in '0'..'9' -> (escaped.code or (c - '0')).toChar()
336 in 'a'..'f' -> (escaped.code or (10 + (c - 'a'))).toChar()
337 in 'A'..'F' -> (escaped.code or (10 + (c - 'A'))).toChar()
338 else ->
339 throw IllegalArgumentException(
340 "bad escape sequence: '" +
341 c +
342 "' at pos " +
343 i +
344 " in: \"" +
345 str +
346 "\""
347 )
348 }
349 if (state == CHAR4) {
350 buf.append(escaped)
351 state = START
352 } else {
353 state++
354 }
355 }
356 }
357 }
358 if (state != START) {
359 throw IllegalArgumentException("unfinished escape sequence: $str")
360 }
361 return buf.toString()
362 }
363
364 /**
365 * Returns a canonical string representation of a floating point number. The representation is
366 * suitable for use as Java source code. This method also addresses bug #4428022 in the Sun JDK.
367 */
368 // From doclava1
canonicalizeFloatingPointStringnull369 fun canonicalizeFloatingPointString(value: String): String {
370 var str = value
371 if (str.indexOf('E') != -1) {
372 return str
373 }
374
375 // 1.0 is the only case where a trailing "0" is allowed.
376 // 1.00 is canonicalized as 1.0.
377 var i = str.length - 1
378 val d = str.indexOf('.')
379 while (i >= d + 2 && str[i] == '0') {
380 str = str.substring(0, i--)
381 }
382 return str
383 }
384