Home | History | Annotate | Download | only in metalava
      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.metalava
     18 
     19 import com.android.SdkConstants.ATTR_VALUE
     20 import com.android.tools.metalava.Severity.ERROR
     21 import com.android.tools.metalava.Severity.HIDDEN
     22 import com.android.tools.metalava.Severity.INHERIT
     23 import com.android.tools.metalava.Severity.LINT
     24 import com.android.tools.metalava.Severity.WARNING
     25 import com.android.tools.metalava.doclava1.Errors
     26 import com.android.tools.metalava.model.AnnotationArrayAttributeValue
     27 import com.android.tools.metalava.model.Item
     28 import com.android.tools.metalava.model.psi.PsiConstructorItem
     29 import com.android.tools.metalava.model.psi.PsiItem
     30 import com.android.tools.metalava.model.text.TextItem
     31 import com.intellij.openapi.util.TextRange
     32 import com.intellij.openapi.vfs.StandardFileSystems
     33 import com.intellij.openapi.vfs.VfsUtilCore
     34 import com.intellij.openapi.vfs.VirtualFile
     35 import com.intellij.psi.PsiCompiledElement
     36 import com.intellij.psi.PsiElement
     37 import com.intellij.psi.impl.light.LightElement
     38 import java.io.File
     39 
     40 var reporter = Reporter()
     41 
     42 enum class Severity(private val displayName: String) {
     43     INHERIT("inherit"),
     44 
     45     HIDDEN("hidden"),
     46 
     47     /**
     48      * Lint level means that we encountered inconsistent or broken documentation.
     49      * These should be resolved, but don't impact API compatibility.
     50      */
     51     LINT("lint"),
     52 
     53     /**
     54      * Warning level means that we encountered some incompatible or inconsistent
     55      * API change. These must be resolved to preserve API compatibility.
     56      */
     57     WARNING("warning"),
     58 
     59     /**
     60      * Error level means that we encountered severe trouble and were unable to
     61      * output the requested documentation.
     62      */
     63     ERROR("error");
     64 
     65     override fun toString(): String = displayName
     66 }
     67 
     68 open class Reporter(private val rootFolder: File? = null) {
     69     var hasErrors = false
     70 
     71     fun error(item: Item?, message: String, id: Errors.Error? = null) {
     72         error(item?.psi(), message, id)
     73     }
     74 
     75     fun warning(item: Item?, message: String, id: Errors.Error? = null) {
     76         warning(item?.psi(), message, id)
     77     }
     78 
     79     fun error(element: PsiElement?, message: String, id: Errors.Error? = null) {
     80         // Using lowercase since that's the convention doclava1 is using
     81         report(ERROR, element, message, id)
     82     }
     83 
     84     fun warning(element: PsiElement?, message: String, id: Errors.Error? = null) {
     85         report(WARNING, element, message, id)
     86     }
     87 
     88     fun report(id: Errors.Error, element: PsiElement?, message: String) {
     89         report(id.level, element, message, id)
     90     }
     91 
     92     fun report(id: Errors.Error, file: File?, message: String) {
     93         report(id.level, file?.path, message, id)
     94     }
     95 
     96     fun report(id: Errors.Error, item: Item?, message: String) {
     97         if (isSuppressed(id, item)) {
     98             return
     99         }
    100 
    101         when (item) {
    102             is PsiItem -> {
    103                 var psi = item.psi()
    104 
    105                 // If no PSI element, is this a synthetic/implicit constructor? If so
    106                 // grab the parent class' PSI element instead for file/location purposes
    107 
    108                 if (item is PsiConstructorItem && item.implicitConstructor &&
    109                     psi?.containingFile?.virtualFile == null
    110                 ) {
    111                     psi = item.containingClass().psi()
    112                 }
    113 
    114                 report(id.level, psi, message, id)
    115             }
    116             is TextItem -> report(id.level, (item as? TextItem)?.position.toString(), message, id)
    117             else -> report(id.level, "<unknown location>", message, id)
    118         }
    119     }
    120 
    121     private fun isSuppressed(id: Errors.Error, item: Item?): Boolean {
    122         item ?: return false
    123 
    124         if (id.level == LINT || id.level == WARNING || id.level == ERROR) {
    125             val id1 = "Doclava${id.code}"
    126             val id2 = id.name
    127             val annotation = item.modifiers.findAnnotation("android.annotation.SuppressLint")
    128             if (annotation != null) {
    129                 val attribute = annotation.findAttribute(ATTR_VALUE)
    130                 if (attribute != null) {
    131                     val value = attribute.value
    132                     if (value is AnnotationArrayAttributeValue) {
    133                         // Example: @SuppressLint({"DocLava1", "DocLava2"})
    134                         for (innerValue in value.values) {
    135                             val string = innerValue.value()
    136                             if (id1 == string || id2 != null && id2 == string) {
    137                                 return true
    138                             }
    139                         }
    140                     } else {
    141                         // Example: @SuppressLint("DocLava1")
    142                         val string = value.value()
    143                         if (id1 == string || id2 != null && id2 == string) {
    144                             return true
    145                         }
    146                     }
    147                 }
    148             }
    149         }
    150 
    151         return false
    152     }
    153 
    154     private fun getTextRange(element: PsiElement): TextRange? {
    155         var range: TextRange? = null
    156 
    157         if (element is PsiCompiledElement) {
    158             if (element is LightElement) {
    159                 range = (element as PsiElement).textRange
    160             }
    161             if (range == null || TextRange.EMPTY_RANGE == range) {
    162                 return null
    163             }
    164         } else {
    165             range = element.textRange
    166         }
    167 
    168         return range
    169     }
    170 
    171     private fun elementToLocation(element: PsiElement?): String? {
    172         element ?: return null
    173         val psiFile = element.containingFile ?: return null
    174         val virtualFile = psiFile.virtualFile ?: return null
    175         val file = VfsUtilCore.virtualToIoFile(virtualFile)
    176 
    177         val path =
    178             if (rootFolder != null) {
    179                 val root: VirtualFile? = StandardFileSystems.local().findFileByPath(rootFolder.path)
    180                 if (root != null) VfsUtilCore.getRelativePath(virtualFile, root) ?: file.path else file.path
    181             } else {
    182                 file.path
    183             }
    184 
    185         val range = getTextRange(element)
    186         return if (range == null) {
    187             // No source offsets, just use filename
    188             path
    189         } else {
    190             val lineNumber = getLineNumber(psiFile.text, range.startOffset) + 1
    191             "$path:$lineNumber"
    192         }
    193     }
    194 
    195     /** Returns the 0-based line number */
    196     private fun getLineNumber(text: String, offset: Int): Int {
    197         var line = 0
    198         var curr = 0
    199         val target = Math.min(offset, text.length)
    200         while (curr < target) {
    201             if (text[curr++] == '\n') {
    202                 line++
    203             }
    204         }
    205         return line
    206     }
    207 
    208     open fun report(severity: Severity, element: PsiElement?, message: String, id: Errors.Error? = null) {
    209         if (severity == HIDDEN) {
    210             return
    211         }
    212 
    213         report(severity, elementToLocation(element), message, id)
    214     }
    215 
    216     open fun report(
    217         severity: Severity,
    218         location: String?,
    219         message: String,
    220         id: Errors.Error? = null,
    221         color: Boolean = options.color
    222     ) {
    223         if (severity == HIDDEN) {
    224             return
    225         }
    226 
    227         val effectiveSeverity =
    228             if (severity == LINT && options.lintsAreErrors)
    229                 ERROR
    230             else if (severity == WARNING && options.warningsAreErrors) {
    231                 ERROR
    232             } else {
    233                 severity
    234             }
    235 
    236         if (severity == ERROR) {
    237             hasErrors = true
    238         }
    239 
    240         val sb = StringBuilder(100)
    241 
    242         if (color) {
    243             sb.append(terminalAttributes(bold = true))
    244             if (!options.omitLocations) {
    245                 location?.let { sb.append(it).append(": ") }
    246             }
    247             when (effectiveSeverity) {
    248                 LINT -> sb.append(terminalAttributes(foreground = TerminalColor.CYAN)).append("lint: ")
    249                 WARNING -> sb.append(terminalAttributes(foreground = TerminalColor.YELLOW)).append("warning: ")
    250                 ERROR -> sb.append(terminalAttributes(foreground = TerminalColor.RED)).append("error: ")
    251                 INHERIT, HIDDEN -> {
    252                 }
    253             }
    254             sb.append(resetTerminal())
    255             sb.append(message)
    256             id?.let { sb.append(" [").append(if (it.name != null) it.name else it.code).append("]") }
    257         } else {
    258             if (!options.omitLocations) {
    259                 location?.let { sb.append(it).append(": ") }
    260             }
    261             if (compatibility.oldErrorOutputFormat) {
    262                 // according to doclava1 there are some people or tools parsing old format
    263                 when (effectiveSeverity) {
    264                     LINT -> sb.append("lint ")
    265                     WARNING -> sb.append("warning ")
    266                     ERROR -> sb.append("error ")
    267                     INHERIT, HIDDEN -> {
    268                     }
    269                 }
    270                 id?.let { sb.append(if (it.name != null) it.name else it.code).append(": ") }
    271                 sb.append(message)
    272             } else {
    273                 when (effectiveSeverity) {
    274                     LINT -> sb.append("lint: ")
    275                     WARNING -> sb.append("warning: ")
    276                     ERROR -> sb.append("error: ")
    277                     INHERIT, HIDDEN -> {
    278                     }
    279                 }
    280                 sb.append(message)
    281                 id?.let {
    282                     sb.append(" [")
    283                     if (it.name != null) {
    284                         sb.append(it.name).append(":")
    285                     }
    286                     sb.append(it.code)
    287                     sb.append("]")
    288                 }
    289             }
    290         }
    291         print(sb.toString())
    292     }
    293 
    294     open fun print(message: String) {
    295         options.stdout.println()
    296         options.stdout.print(message.trim())
    297         options.stdout.flush()
    298     }
    299 
    300     fun hasErrors(): Boolean = hasErrors
    301 }