1 /*
2 * 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.lint.checks.infrastructure
18
19 import com.android.SdkConstants.DOT_JAVA
20 import com.android.SdkConstants.DOT_KT
21 import java.util.regex.Pattern
22
23 // Copy in metalava from lint to avoid compilation dependency directly on lint-tests
24
25 /**
26 * A pair of package name and class name inferred from Java or Kotlin
27 * source code. The [source] is the source code, and the [extension] is
28 * the file extension (including the leading dot) which states whether
29 * this is a Kotlin source file, a Java source file, a Groovy source
30 * file, etc.
31 */
32 class ClassName(source: String, extension: String = DOT_JAVA) {
33 val packageName: String?
34 val className: String?
35
36 init {
37 val withoutComments = stripComments(source, extension)
38 packageName = getPackage(withoutComments)
39 className = getClassName(withoutComments)
40 }
41
packageNameWithDefaultnull42 fun packageNameWithDefault() = packageName ?: ""
43 }
44
45 /**
46 * Strips line and block comments from the given Java or Kotlin source
47 * file.
48 */
49 @Suppress("LocalVariableName")
50 fun stripComments(source: String, extension: String, stripLineComments: Boolean = true): String {
51 val sb = StringBuilder(source.length)
52 var state = 0
53 val INIT = 0
54 val INIT_SLASH = 1
55 val LINE_COMMENT = 2
56 val BLOCK_COMMENT = 3
57 val BLOCK_COMMENT_ASTERISK = 4
58 val BLOCK_COMMENT_SLASH = 5
59 val IN_STRING = 6
60 val IN_STRING_ESCAPE = 7
61 val IN_CHAR = 8
62 val AFTER_CHAR = 9
63 var blockCommentDepth = 0
64 for (c in source) {
65 when (state) {
66 INIT -> {
67 when (c) {
68 '/' -> state = INIT_SLASH
69 '"' -> {
70 state = IN_STRING
71 sb.append(c)
72 }
73 '\'' -> {
74 state = IN_CHAR
75 sb.append(c)
76 }
77 else -> sb.append(c)
78 }
79 }
80 INIT_SLASH -> {
81 when {
82 c == '*' -> { blockCommentDepth++; state = BLOCK_COMMENT }
83 c == '/' && stripLineComments -> state = LINE_COMMENT
84 else -> {
85 state = INIT
86 sb.append('/') // because we skipped it in init
87 sb.append(c)
88 }
89 }
90 }
91 LINE_COMMENT -> {
92 when (c) {
93 '\n' -> state = INIT
94 }
95 }
96 BLOCK_COMMENT -> {
97 when (c) {
98 '*' -> state = BLOCK_COMMENT_ASTERISK
99 '/' -> state = BLOCK_COMMENT_SLASH
100 }
101 }
102
103 BLOCK_COMMENT_ASTERISK -> {
104 state = when (c) {
105 '/' -> {
106 blockCommentDepth--
107 if (blockCommentDepth == 0) {
108 INIT
109 } else {
110 BLOCK_COMMENT
111 }
112 }
113 '*' -> BLOCK_COMMENT_ASTERISK
114 else -> BLOCK_COMMENT
115 }
116 }
117 BLOCK_COMMENT_SLASH -> {
118 if (c == '*' && extension == DOT_KT) {
119 blockCommentDepth++
120 }
121 if (c != '/') {
122 state = BLOCK_COMMENT
123 }
124 }
125 IN_STRING -> {
126 when (c) {
127 '\\' -> state = IN_STRING_ESCAPE
128 '"' -> state = INIT
129 }
130 sb.append(c)
131 }
132 IN_STRING_ESCAPE -> {
133 sb.append(c)
134 state = IN_STRING
135 }
136 IN_CHAR -> {
137 if (c != '\\') {
138 state = AFTER_CHAR
139 }
140 sb.append(c)
141 }
142 AFTER_CHAR -> {
143 sb.append(c)
144 if (c == '\\') {
145 state = INIT
146 }
147 }
148 }
149 }
150
151 return sb.toString()
152 }
153
154 private val PACKAGE_PATTERN = Pattern.compile("""package\s+([\S&&[^;]]*)""")
155
156 private val CLASS_PATTERN = Pattern.compile(
157 """(\bclass\b|\binterface\b|\benum class\b|\benum\b|\bobject\b)+?\s*([^\s:(]+)""",
158 Pattern.MULTILINE
159 )
160
getPackagenull161 fun getPackage(source: String): String? {
162 val matcher = PACKAGE_PATTERN.matcher(source)
163 return if (matcher.find()) {
164 matcher.group(1).trim { it <= ' ' }
165 } else {
166 null
167 }
168 }
169
getClassNamenull170 fun getClassName(source: String): String? {
171 val matcher = CLASS_PATTERN.matcher(source.replace('\n', ' '))
172 var start = 0
173 while (matcher.find(start)) {
174 val cls = matcher.group(2)
175 val groupStart = matcher.start(1)
176
177 // Make sure this "class" reference isn't part of an annotation on the class
178 // referencing a class literal -- Foo.class, or in Kotlin, Foo::class.java)
179 if (groupStart == 0 || source[groupStart - 1] != '.' && source[groupStart - 1] != ':') {
180 val trimmed = cls.trim { it <= ' ' }
181 val typeParameter = trimmed.indexOf('<')
182 return if (typeParameter != -1) {
183 trimmed.substring(0, typeParameter)
184 } else {
185 trimmed
186 }
187 }
188 start = matcher.end(2)
189 }
190
191 return null
192 }
193