Skip to content

Commit 255d8e9

Browse files
committed
fix(lsp/java): add parser for parsing api-versions.xml
1 parent 8a14377 commit 255d8e9

8 files changed

Lines changed: 603 additions & 14 deletions

File tree

lsp/java/build.gradle.kts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,13 +64,15 @@ dependencies {
6464
implementation(projects.subprojects.javacServices)
6565

6666
implementation(libs.composite.javac)
67-
implementation(libs.composite.jdkJdeps)
6867
implementation(libs.composite.javapoet)
68+
implementation(libs.composite.jaxp)
69+
implementation(libs.composite.jdkJdeps)
6970
implementation(libs.composite.googleJavaFormat)
7071

7172
implementation(libs.androidx.core.ktx)
7273
implementation(libs.common.kotlin)
7374

75+
testImplementation(projects.testing.common)
7476
testImplementation(projects.testing.lsp)
7577
androidTestImplementation(projects.testing.android)
7678
androidTestImplementation(projects.common)

lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/JavaJarModelBuilder.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
package com.itsaky.androidide.lsp.java.indexing
1919

20+
import androidx.annotation.VisibleForTesting
2021
import com.itsaky.androidide.lsp.java.indexing.classfile.AnnotationAnnotationElementValue
2122
import com.itsaky.androidide.lsp.java.indexing.classfile.AnnotationElement
2223
import com.itsaky.androidide.lsp.java.indexing.classfile.AnnotationElement.Companion.newAnnotationElement
@@ -68,6 +69,7 @@ class JavaJarModelBuilder(private val jar: File) {
6869

6970
// for testing purposes
7071
// prefer using consumeTypes(Function)
72+
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
7173
fun buildTypes(): List<IJavaType<*, *>> {
7274
val types = mutableListOf<IJavaType<*, *>>()
7375
consumeTypes { types.add(it) }

lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/apiinfo/ApiInfo.kt

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,16 @@ open class ApiInfo : ISharedJavaIndexable, ICloneable {
4141
override var id: Int? = null
4242

4343
@RealmField("since")
44-
private var since: Int = 1
44+
var since: Int = 1
45+
private set
4546

4647
@RealmField("deprecatedIn")
47-
private var deprecatedIn: Int = 0
48+
var deprecatedIn: Int = 0
49+
private set
4850

4951
@RealmField("removedIn")
50-
private var removedIn: Int = 0
52+
var removedIn: Int = 0
53+
private set
5154

5255
override fun computeId() {
5356
this.id = Objects.hashCode(this.since, this.removedIn, this.deprecatedIn)
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
/*
2+
* This file is part of AndroidIDE.
3+
*
4+
* AndroidIDE is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation, either version 3 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* AndroidIDE is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU General Public License
15+
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
16+
*/
17+
18+
package com.itsaky.androidide.lsp.java.indexing.apiinfo
19+
20+
import androidx.collection.mutableIntObjectMapOf
21+
import jaxp.xml.namespace.QName
22+
import jaxp.xml.stream.XMLInputFactory
23+
import jaxp.xml.stream.events.Attribute
24+
import jaxp.xml.stream.events.EndElement
25+
import jaxp.xml.stream.events.StartElement
26+
import org.slf4j.LoggerFactory
27+
import java.io.InputStream
28+
29+
/**
30+
* Parser for parsing `api-versions.xml` file from the Android SDK and building [ApiInfo] models.
31+
*
32+
* @author Akash Yadav
33+
*/
34+
class ApiInfoParser {
35+
36+
private val apiInfos = mutableIntObjectMapOf<ApiInfo>()
37+
private val apiInfo = HashMap<String, Pair<ApiInfo, HashMap<String, ApiInfo>>>()
38+
39+
private var apiVersion: Int? = null
40+
private var currentClass: String? = null
41+
42+
companion object {
43+
private val log = LoggerFactory.getLogger(ApiInfoParser::class.java)
44+
}
45+
46+
fun parse(apiVersionsXml: InputStream) {
47+
if (apiVersionsXml.available() <= 0) {
48+
log.warn("api-versions.xml InputStream is empty")
49+
return
50+
}
51+
52+
val inputFactory = XMLInputFactory.newInstance()
53+
val reader = inputFactory.createXMLEventReader(apiVersionsXml)
54+
55+
while (reader.hasNext()) {
56+
val event = reader.nextEvent()
57+
if (event.isStartElement) {
58+
consumeStartElement(event.asStartElement())
59+
continue
60+
}
61+
62+
if (event.isEndElement) {
63+
consumeEndElement(event.asEndElement())
64+
}
65+
}
66+
}
67+
68+
/**
69+
* Returns [ApiInfo] for the given class.
70+
*/
71+
fun getClassInfo(className: String): ApiInfo? {
72+
return apiInfo[className]?.first
73+
}
74+
75+
/**
76+
* Returns [ApiInfo] for the given class and member.
77+
*/
78+
fun getMemberInfo(className: String, memberName: String): ApiInfo? {
79+
return apiInfo[className]?.second?.get(memberName)
80+
}
81+
82+
/**
83+
* Removes and returns the [ApiInfo] for the given class and member. This also removes the [ApiInfo]
84+
* for the class if the all the members of the class have been removed.
85+
*/
86+
fun removeMemberInfo(className: String, memberName: String): ApiInfo? {
87+
return apiInfo[className]?.second?.remove(memberName).also {
88+
if (apiInfo[className]?.second?.isEmpty() == true) {
89+
apiInfo.remove(className)
90+
}
91+
}
92+
}
93+
94+
private fun consumeStartElement(event: StartElement) {
95+
when (event.name.localPart) {
96+
"api" -> apiVersion = event.getAttributeByName(QName("version")).value.toInt()
97+
"class" -> consumeClass(event)
98+
"field" -> consumeMember(event, "field")
99+
"method" -> consumeMember(event, "method")
100+
}
101+
}
102+
103+
private fun consumeClass(event: StartElement) {
104+
checkNotNull(apiVersion) {
105+
"<class> element must be inside <api> element"
106+
}
107+
108+
check(currentClass == null) {
109+
"<class> elements cannot be nested"
110+
}
111+
112+
val (name, versions) = event.parseAttrs()
113+
check(!apiInfo.containsKey(name)) {
114+
"Duplicate class entry: $name"
115+
}
116+
117+
val apiInfo = apiInfos.getOrPut(versions) {
118+
createApiInfo(versions)
119+
}
120+
121+
this.apiInfo[name] = apiInfo to HashMap()
122+
this.currentClass = name
123+
}
124+
125+
private fun consumeMember(event: StartElement, memberType: String) {
126+
val (name, versions) = event.parseAttrs()
127+
val currentClass = checkNotNull(currentClass) {
128+
"<${memberType}> element must be inside <class> element"
129+
}
130+
check(!apiInfo.containsKey(name)) {
131+
"Duplicate $memberType entry in class $currentClass: $name"
132+
}
133+
134+
val (_, members) = apiInfo[currentClass]!!
135+
val existing = members.put(name, createApiInfo(versions))
136+
check(existing == null) {
137+
"Duplicate $memberType entry in class $currentClass: $name"
138+
}
139+
}
140+
141+
private fun consumeEndElement(element: EndElement) {
142+
when (element.name.localPart) {
143+
"api" -> apiVersion = null
144+
"class" -> currentClass = null
145+
}
146+
}
147+
148+
private fun StartElement.parseAttrs(): Pair<String, Int> {
149+
var name: String? = null
150+
var since = 1
151+
var deprecated = 0
152+
var removed = 0
153+
154+
attributes.forEach { attribute ->
155+
attribute as Attribute
156+
157+
when (attribute.name.localPart) {
158+
"name" -> name = attribute.value
159+
"since" -> since = attribute.value.toInt()
160+
"deprecated" -> deprecated = attribute.value.toInt()
161+
"removed" -> removed = attribute.value.toInt()
162+
}
163+
}
164+
165+
checkNotNull(name) {
166+
"Missing name attribute"
167+
}
168+
169+
// Android API versions would not exceed 255, right?
170+
check(since in 1..255 && deprecated in 0..255 && removed in 0..255) {
171+
"Invalid version: $since, $deprecated, $removed"
172+
}
173+
174+
val version = (since shl 16) + (deprecated shl 8) + removed
175+
return name!! to version
176+
}
177+
178+
private fun createApiInfo(versions: Int): ApiInfo {
179+
val since = (versions shr 16) and 0x000000FF
180+
val deprecated = (versions shr 8) and 0x000000FF
181+
val removed = versions and 0x000000FF
182+
return ApiInfo.newInstance(since = since, deprecatedIn = deprecated, removedIn = removed)
183+
}
184+
}

0 commit comments

Comments
 (0)