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