<|start_filename|>bye-bye-jetifier/src/main/java/com/dipien/byebyejetifier/ProjectAnalyzer.kt<|end_filename|>
package com.dipien.byebyejetifier

import com.dipien.byebyejetifier.archive.Archive
import com.dipien.byebyejetifier.common.LoggerHelper
import com.dipien.byebyejetifier.scanner.ScanResult
import com.dipien.byebyejetifier.scanner.ScannerProcessor
import org.gradle.api.Project
import org.gradle.api.artifacts.Configuration
import org.gradle.api.artifacts.ProjectDependency
import org.gradle.api.artifacts.ResolvedDependency
import java.util.LinkedList
import java.util.Queue

class ProjectAnalyzer(
    private val project: Project,
    private var excludedConfigurations: List<String>,
    private var legacyGroupIdPrefixes: List<String>,
    private val scannerProcessor: ScannerProcessor,
    private val excludeSupportAnnotations: Boolean
) {

    fun analyze(projectAnalyzerResult: ProjectAnalyzerResult) {

        var includeSupportLibrary = false
        var thereAreSupportLibraryDependencies = false
        var hasExternalDependencies = false
        val projectScanResult = mutableMapOf<ExternalDependency, List<ScanResult>>()

        LoggerHelper.lifeCycle("")
        LoggerHelper.lifeCycle("=========================================")
        LoggerHelper.lifeCycle("Project: ${project.name}")
        LoggerHelper.lifeCycle("=========================================")

        val configurations = project.configurations
            .filter {
                !excludedConfigurations.contains(it.name)
            }
        LoggerHelper.log("Configurations to scan: $configurations")

        val legacyArtifactsFirstLevel = mutableListOf<ExternalDependency>()
        val allExternalDependencies = configurations
            .map {
                it.getExternalDependencies()
            }
            .flatten()

        allExternalDependencies
            // Gradle does not resolve the entire tree of dependencies for each Configuration.
            // Then an artifact can appear in an Configurations without children and in other with children.
            .groupBy { it.artifactDefinition }
            .forEach { (_, groupedExtDependencies) ->
                val externalDependency = groupedExtDependencies.first()

                if (!externalDependency.isAndroidX && !externalDependency.isLegacyAndroidSupport) {
                    val result = mutableListOf<ScanResult>()

                    externalDependency.moduleArtifacts.forEach {
                        hasExternalDependencies = true
                        val library = Archive.Builder.extract(it.file)
                        result.addAll(projectAnalyzerResult.scanResultsCache.getOrPut(library.relativePath) { filterSupportAnnotationsIfNeeded(scannerProcessor.scanLibrary(library)) })
                    }

                    groupedExtDependencies
                        .map { it.children }
                        .flatten()
                        .distinct()
                        .forEach {
                            if (it.isLegacyAndroidSupport) {
                                includeSupportLibrary = true
                                result.add(ScanResult("pom", it.artifactDefinition))
                            }
                    }

                    projectScanResult[externalDependency] = result
                } else if (externalDependency.isFirstLevel && externalDependency.isLegacyAndroidSupport) {
                    legacyArtifactsFirstLevel.add(externalDependency)
                }
            }

        projectScanResult.forEach { (dependencyToReport, scanResults) ->
            if (scanResults.isNotEmpty()) {
                LoggerHelper.lifeCycle("")
                LoggerHelper.lifeCycle("Scanning ${dependencyToReport.artifactDefinition}")

                // Artifact paths
                dependencyToReport.moduleArtifacts.forEach {
                    LoggerHelper.lifeCycle(" Absolute path: ${it.file}")
                }

                // All possible graphs
                LoggerHelper.lifeCycle(" Graphs to this dependency:")
                allExternalDependencies
                    .filter { it.artifactDefinition == dependencyToReport.artifactDefinition }
                    .map { it.buildDependencyGraphs() }
                    .flatten()
                    .distinctBy { graph -> graph.toString() }
                    .forEach { it.print() }

                // Issues found
                LoggerHelper.lifeCycle(" Issues found:")
                scanResults.forEach { scanResult ->
                    LoggerHelper.lifeCycle(" * ${scanResult.relativePath} -> ${scanResult.legacyDependency}")
                }

                thereAreSupportLibraryDependencies = true
            } else {
                LoggerHelper.info("")
                LoggerHelper.info("Scanning ${dependencyToReport.artifactDefinition}")
                LoggerHelper.info(" * No legacy android support usages found")
            }
        }

        if (legacyArtifactsFirstLevel.isNotEmpty()) {
            includeSupportLibrary = true
            LoggerHelper.lifeCycle("")
            LoggerHelper.lifeCycle("Explicit declarations of legacy support dependencies on this project:")
            legacyArtifactsFirstLevel.sortedBy { it.artifactDefinition }.forEach {
                LoggerHelper.lifeCycle(" * ${it.artifactDefinition}")
            }
        }

        if (LoggerHelper.verbose) {
            if (!includeSupportLibrary && !hasExternalDependencies) {
                LoggerHelper.info(" * No legacy android support usages found")
            }
        } else {
            if (!thereAreSupportLibraryDependencies && !includeSupportLibrary) {
                LoggerHelper.lifeCycle(" * No legacy android support usages found")
            }
        }

        if (includeSupportLibrary) {
            projectAnalyzerResult.includeSupportLibrary = true
        }
        if (thereAreSupportLibraryDependencies) {
            projectAnalyzerResult.thereAreSupportLibraryDependencies = true
        }
    }

    private fun filterSupportAnnotationsIfNeeded(results: List<ScanResult>): List<ScanResult> {
        if (excludeSupportAnnotations) {
            return results.filter { scanResult ->
                !scanResult.legacyDependency.startsWith("android/support/annotation") &&
                    !scanResult.legacyDependency.startsWith("android.support.annotation")
            }
        }
        return results
    }

    private fun Configuration.getExternalDependencies(): Set<ExternalDependency> {
        var firstLevelDependencies = emptySet<ResolvedDependency>()
        try {
            if (isCanBeResolved) {
                firstLevelDependencies = resolvedConfiguration.firstLevelModuleDependencies
            }
        } catch (e: Throwable) {
            if (name.endsWith("Metadata") || name.endsWith("archives")) {
                // TODO analyze errors from Configurations whose name ends in "metadata"
                // https://github.com/gradle/gradle/issues/5426
                LoggerHelper.info("Error when accessing configuration $name" + e.message)
            } else {
                LoggerHelper.warn("Error when accessing configuration $name")
                throw e
            }
        }

        val resolvedDependencies = mutableSetOf<ExternalDependency>()
        val projectDependencies = allDependencies.filterIsInstance(ProjectDependency::class.java)
        firstLevelDependencies
            .forEach { firstLevelDependency ->
                if (firstLevelDependency.isExternalDependency(projectDependencies)) {
                    val externalDependencyFirstLevel = ExternalDependency.FirstLevel(firstLevelDependency, legacyGroupIdPrefixes)
                    resolvedDependencies.add(externalDependencyFirstLevel)
                    resolvedDependencies.traverseAndAddChildren(externalDependencyFirstLevel)
                }
            }
        return resolvedDependencies
    }

    private data class QueueElement(val children: List<ExternalDependency>)

    private fun MutableSet<ExternalDependency>.traverseAndAddChildren(firstLevelDependency: ExternalDependency) {
        val queue: Queue<QueueElement> = LinkedList()

        queue.offer(QueueElement(firstLevelDependency.children))

        while (queue.isNotEmpty()) {
            val element = queue.poll()
            element.children.forEach { child ->
                if (!child.isAndroidX) {
                    add(child)
                    queue.offer(QueueElement(child.children))
                }
            }
        }
    }

    private fun ResolvedDependency.isExternalDependency(projectDependencies: List<ProjectDependency>): Boolean {
        return projectDependencies.none { projectDependency ->
            projectDependency.group == this.moduleGroup && projectDependency.name == this.moduleName
        }
    }
}


<|start_filename|>bye-bye-jetifier/src/main/java/com/dipien/byebyejetifier/ProjectAnalyzerResult.kt<|end_filename|>
package com.dipien.byebyejetifier

import com.dipien.byebyejetifier.scanner.ScanResult
import java.nio.file.Path

class ProjectAnalyzerResult {
    var includeSupportLibrary = false
    var thereAreSupportLibraryDependencies = false
    val scanResultsCache = mutableMapOf<Path, List<ScanResult>>()
}


<|start_filename|>bye-bye-jetifier/src/main/java/com/dipien/byebyejetifier/scanner/resource/XmlResourceScanner.kt<|end_filename|>
package com.dipien.byebyejetifier.scanner.resource

import com.dipien.byebyejetifier.archive.ArchiveFile
import com.dipien.byebyejetifier.common.LoggerHelper
import com.dipien.byebyejetifier.core.type.JavaType
import com.dipien.byebyejetifier.core.type.PackageName
import com.dipien.byebyejetifier.scanner.ScanResult
import com.dipien.byebyejetifier.scanner.Scanner
import com.dipien.byebyejetifier.scanner.ScannerContext
import java.io.PrintWriter
import java.io.StringWriter
import java.nio.charset.Charset
import java.nio.charset.StandardCharsets
import java.nio.file.Path
import java.util.regex.Pattern
import javax.xml.stream.XMLInputFactory
import javax.xml.stream.XMLStreamException

class XmlResourceScanner(
    private val context: ScannerContext
) : Scanner {

    companion object {

        const val TAG = "XmlResourceScanner"

        const val PATTERN_TYPE_GROUP = 1

        /***
         * Matches anything that could be java type or package
         */
        val JAVA_TOKEN_MATCHER = "^[a-zA-Z0-9.\$_]+$".toRegex()
    }

    /**
     * List of regular expression patterns used to find support library types references in XML
     * files.
     *
     * Matches xml tags in form of:
     * 1. '<(/)prefix(SOMETHING)'.
     * 2. <view ... class="prefix(SOMETHING)" ...>
     * 3. >SOMETHING<
     * 4. {@link SOMETHING#method()}
     *
     * Note that this can also rewrite commented blocks of XML. But on a library level we don't care
     * much about comments.
     */
    private val patterns = listOf(
        Pattern.compile("</?([a-zA-Z0-9.]+)"), // </{candidate} or <{candidate}
        Pattern.compile("[a-zA-Z0-9:]+=\"([^\"]+)\""), // any="{candidate}"
        Pattern.compile(">\\s*([a-zA-Z0-9.\$_]+)<"), // >{candidate}<
        Pattern.compile("\\{@link\\s*([a-zA-Z0-9.\$_]+)(#[^}]*)?}") // @{link {candidate}#*}
    )

    override fun scan(archiveFile: ArchiveFile): List<ScanResult> {
        val charset = getCharset(archiveFile)
        val dataStr = archiveFile.data.toString(charset)

        return replaceWithPatterns(dataStr, patterns, archiveFile.relativePath)
    }

    private fun getCharset(file: ArchiveFile): Charset {
        try {
            file.data.inputStream().use {
                val xmlReader = XMLInputFactory.newInstance().createXMLStreamReader(it)

                xmlReader.encoding ?: return StandardCharsets.UTF_8 // Encoding was not detected

                val result = Charset.forName(xmlReader.encoding)
                if (result == null) {
                    LoggerHelper.error("Failed to find charset for encoding ${xmlReader.encoding}")
                    return StandardCharsets.UTF_8
                }
                return result
            }
        } catch (e: XMLStreamException) {
            // Workaround for b/111814958. A subset of the android.jar xml files has a header that
            // causes our encoding detection to crash. However these files are otherwise valid UTF-8
            // files so we at least try to recover by defaulting to UTF-8.
            LoggerHelper.warn("Received malformed sequence exception when trying to detect the encoding " +
                "for ${file.fileName}. Defaulting to UTF-8.")
            if (LoggerHelper.verbose) {
                val tracePrinter = StringWriter()
                e.printStackTrace(PrintWriter(tracePrinter))
                LoggerHelper.warn(tracePrinter.toString())
            }
            return StandardCharsets.UTF_8
        }
    }

    /**
     * For each pattern in [patterns] matching a portion of the string represented by [sb], applies
     * [mappingFunction] to the match and puts the result back into [sb].
     */
    private fun replaceWithPatterns(
        dataStr: String,
        patterns: List<Pattern>,
        filePath: Path
    ): List<ScanResult> {
        val result = mutableSetOf<ScanResult>()

        for (pattern in patterns) {
            val matcher = pattern.matcher(dataStr)
            while (matcher.find()) {
                val toReplace = matcher.group(PATTERN_TYPE_GROUP)
                val replacement =
                    if (toReplace.matches(JAVA_TOKEN_MATCHER)) {
                        if (isPackage(toReplace)) {
                            rewritePackage(toReplace, filePath)
                        } else {
                            rewriteType(toReplace)
                        }
                    } else {
                        toReplace
                    }

                if (replacement != toReplace) {
                    result.add(ScanResult(filePath.toString(), toReplace))
                }
            }
        }

        return result.toList()
    }

    private fun isPackage(token: String): Boolean {
        return !token.any { it.isUpperCase() }
    }

    private fun rewriteType(typeName: String): String {
        if (typeName.contains(" ")) {
            return typeName
        }

        val type = JavaType.fromDotVersion(typeName)
        val result = context.typeRewriter.rewriteType(type)
        if (result != null) {
            return result.toDotNotation()
        }

        context.reportNoMappingFoundFailure(TAG, type)
        return typeName
    }

    private fun rewritePackage(packageName: String, filePath: Path): String {
        if (!packageName.contains('.')) {
            // Single word packages are not something we need or should rewrite
            return packageName
        }

        val pckg = PackageName.fromDotVersion(packageName)

        val result = context.config.packageMap.getPackageFor(pckg)
        if (result != null) {
            LoggerHelper.debug("Map package: $packageName -> $result")
            return result.toDotNotation()
        }

        if (context.config.isEligibleForRewrite(pckg)) {
            LoggerHelper.error("No mapping for package '$packageName' in '$filePath', keeping identity")
        }

        return packageName
    }

    override fun canScan(archiveFile: ArchiveFile): Boolean =
        archiveFile.isLayoutResource() || archiveFile.isAndroidManifestFile()
}


<|start_filename|>bye-bye-jetifier/src/main/java/com/dipien/byebyejetifier/scanner/ScannerContext.kt<|end_filename|>
package com.dipien.byebyejetifier.scanner

import com.dipien.byebyejetifier.archive.ArchiveFile
import com.dipien.byebyejetifier.common.LoggerHelper
import com.dipien.byebyejetifier.common.toFilePath
import com.dipien.byebyejetifier.core.TypeRewriter
import com.dipien.byebyejetifier.core.config.Config
import com.dipien.byebyejetifier.core.type.JavaType

class ScannerContext(val config: Config, private val excludedFilesFromScanning: List<String>) {

    val typeRewriter: TypeRewriter = TypeRewriter(config)

    fun isExcludedFileFromScanning(archiveFile: ArchiveFile): Boolean =
        excludedFilesFromScanning
            .any { archiveFile.relativePath.toString().startsWith(it.toFilePath()) }

    fun reportNoMappingFoundFailure(tag: String, type: JavaType) {
        LoggerHelper.warn("No mapping for: $type", tag)
    }
}


<|start_filename|>bye-bye-jetifier/src/main/java/com/dipien/byebyejetifier/task/CanISayByeByeJetifierTask.kt<|end_filename|>
package com.dipien.byebyejetifier.task

import com.dipien.byebyejetifier.ProjectAnalyzer
import com.dipien.byebyejetifier.ProjectAnalyzerResult
import com.dipien.byebyejetifier.scanner.ScannerProcessor
import com.dipien.byebyejetifier.common.AbstractTask
import com.dipien.byebyejetifier.common.LoggerHelper
import com.dipien.byebyejetifier.core.config.ConfigParser
import com.dipien.byebyejetifier.scanner.ScannerContext
import com.dipien.byebyejetifier.scanner.bytecode.BytecodeScanner
import com.dipien.byebyejetifier.scanner.resource.XmlResourceScanner
import org.gradle.api.GradleException
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.Optional
import java.lang.RuntimeException

open class CanISayByeByeJetifierTask : AbstractTask() {

    companion object {
        const val TASK_NAME = "canISayByeByeJetifier"
        const val ENABLE_JETIFIER_PROPERTY = "android.enableJetifier"

        private const val DEFAULT_CONFIG = "default.config"
    }

    @get:Input
    @get:Optional
    var legacyGroupIdPrefixes: List<String> = emptyList()

    @get:Input
    @get:Optional
    var excludedConfigurations: List<String> = emptyList()

    @get:Input
    @get:Optional
    var excludedFilesFromScanning: List<String> = emptyList()

    @get:Input
    @get:Optional
    var excludedProjectsFromScanning: List<String> = emptyList()

    @get:Input
    var excludeSupportAnnotations = true

    private val scannerProcessor by lazy {
        val inputStream = javaClass.classLoader.getResourceAsStream(DEFAULT_CONFIG)!!
        val config = ConfigParser.loadFromFile(inputStream)
        val scannerContext = ScannerContext(config, excludedFilesFromScanning)
        val scannerList = listOf(BytecodeScanner(scannerContext), XmlResourceScanner(scannerContext))
        ScannerProcessor(scannerContext, scannerList)
    }

    init {
        group = "Verification"
        description = "Verifies if you can keep Android Jetifier disabled"
    }

    override fun onExecute() {
        if (project.hasProperty(ENABLE_JETIFIER_PROPERTY) && project.property(ENABLE_JETIFIER_PROPERTY) == "true") {
            throw GradleException("This task needs to be run with Jetifier disabled: ./gradlew $TASK_NAME -P$ENABLE_JETIFIER_PROPERTY=false")
        }

        LoggerHelper.log("excludedConfigurations: $excludedConfigurations")
        LoggerHelper.log("excludedFilesFromScanning: $excludedFilesFromScanning")
        LoggerHelper.log("excludedProjectsFromScanning: $excludedProjectsFromScanning")
        LoggerHelper.log("excludeSupportAnnotations: $excludeSupportAnnotations")

        val projectAnalyzerResult = ProjectAnalyzerResult()
        project.allprojects.filter { !excludedProjectsFromScanning.contains(it.name) }.forEach {
            ProjectAnalyzer(it, excludedConfigurations, legacyGroupIdPrefixes, scannerProcessor, excludeSupportAnnotations).analyze(projectAnalyzerResult)
        }

        if (projectAnalyzerResult.thereAreSupportLibraryDependencies || projectAnalyzerResult.includeSupportLibrary) {
            throw RuntimeException("You can not say Bye Bye Jetifier")
        } else {
            LoggerHelper.lifeCycle("")
            LoggerHelper.lifeCycle("=====================================================================================")
            LoggerHelper.lifeCycle("* No dependencies with legacy android support usages! You can say Bye Bye Jetifier. *")
            LoggerHelper.lifeCycle("=====================================================================================")
        }
    }
}


<|start_filename|>bye-bye-jetifier/src/main/java/com/dipien/byebyejetifier/scanner/ScannerProcessor.kt<|end_filename|>
package com.dipien.byebyejetifier.scanner

import com.dipien.byebyejetifier.archive.Archive
import com.dipien.byebyejetifier.archive.ArchiveFile
import com.dipien.byebyejetifier.archive.ArchiveItemVisitor
import com.dipien.byebyejetifier.common.LoggerHelper

class ScannerProcessor(
    private val context: ScannerContext,
    private val scannerList: List<Scanner>
) : ArchiveItemVisitor {

    fun scanLibrary(archive: Archive): List<ScanResult> {
        val scanResults = mutableListOf<ScanResult>()
        archive.accept(this, scanResults)
        return scanResults
    }

    override fun visit(archive: Archive, scanResults: MutableList<ScanResult>) {
        archive.files.forEach {
            it.accept(this, scanResults)
        }
    }

    override fun visit(archiveFile: ArchiveFile, scanResults: MutableList<ScanResult>) {
        if (context.isExcludedFileFromScanning(archiveFile)) {
            LoggerHelper.debug("Excluded for scanning: ${archiveFile.relativePath}")
            return
        }

        var fileLogged = false

        scannerList.forEach {
            if (it.canScan(archiveFile)) {
                if (!fileLogged) {
                    fileLogged = true
                    LoggerHelper.debug("File: ${archiveFile.relativePath}")
                }
                scanResults.addAll(it.scan(archiveFile))
            }
        }
    }
}


<|start_filename|>sample/buildSrc/src/main/kotlin/BuildLibs.kt<|end_filename|>
object BuildLibs {

    const val ANDROID_GRADLE_PLUGIN = "com.android.tools.build:gradle:4.1.1"
    const val KOTLIN_GRADLE_PLUGIN = "org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.20"
    const val BYE_BYE_JETIFIER_GRADLE_PLUGIN = "com.dipien:bye-bye-jetifier:1.2.0-SNAPSHOT"
}


<|start_filename|>bye-bye-jetifier/src/main/java/com/dipien/byebyejetifier/scanner/bytecode/BytecodeScanner.kt<|end_filename|>
package com.dipien.byebyejetifier.scanner.bytecode

import com.dipien.byebyejetifier.archive.ArchiveFile
import com.dipien.byebyejetifier.scanner.ScanResult
import com.dipien.byebyejetifier.scanner.Scanner
import com.dipien.byebyejetifier.scanner.ScannerContext
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassWriter
import org.objectweb.asm.commons.ClassRemapper

class BytecodeScanner(private val context: ScannerContext) : Scanner {

    override fun scan(archiveFile: ArchiveFile): List<ScanResult> {
        val relativePath = archiveFile.relativePath.toString()

        val reader = ClassReader(archiveFile.data)
        val writer = ClassWriter(0 /* flags */)
        val customRemapper = CustomRemapper(CoreRemapper(context, relativePath))
        val visitor = ClassRemapper(writer, customRemapper)

        reader.accept(visitor, 0 /* flags */)

        return customRemapper.legacyDependencies.map { ScanResult(relativePath, it) }
    }

    override fun canScan(archiveFile: ArchiveFile): Boolean = archiveFile.isClassFile()
}


<|start_filename|>bye-bye-jetifier/src/main/java/com/dipien/byebyejetifier/ByeByeJetifierExtension.kt<|end_filename|>
package com.dipien.byebyejetifier

open class ByeByeJetifierExtension {

    var legacyGroupIdPrefixes: List<String> = listOf("android.arch", "com.android.support")

    var excludedConfigurations: List<String> = listOf("lintClassPath", "ktlint")

    var excludedFilesFromScanning: List<String> = listOf(
        // org.jetbrains.kotlin:kotlin-compiler-embeddable:1.4.20
        "org/jetbrains/kotlin/load/java/JvmAnnotationNamesKt",

        // org.jetbrains.kotlin:kotlin-reflect:1.4.20
        "kotlin/reflect/jvm/internal/impl/load/java/JvmAnnotationNamesKt",

        // org.jetbrains.kotlin:kotlin-android-extensions:1.4.20
        "org/jetbrains/kotlin/android/synthetic/AndroidConst",
        "org/jetbrains/kotlin/android/synthetic/codegen/AndroidIrTransformer",
        "org/jetbrains/kotlin/android/synthetic/codegen/ResourcePropertyStackValue",

        // org.jetbrains.kotlin:kotlin-compiler-embeddable:1.4.10
        "org/jetbrains/kotlin/com/intellij/codeInsight/NullableNotNullManager",

        // com.facebook.android:facebook-core:8.1.0
        "com/facebook/appevents/codeless/internal/ViewHierarchy",

        // com.squareup.leakcanary:shark-android:2.5
        "shark/AndroidReferenceMatchers",

        // com.squareup.leakcanary:leakcanary-object-watcher-android-support-fragments:2.5
        "leakcanary/internal/AndroidSupportFragmentDestroyWatcher"
    )

    var excludedProjectsFromScanning: List<String> = emptyList()

    var excludeSupportAnnotations = true

    var verbose = false
}


<|start_filename|>bye-bye-jetifier/src/main/java/com/dipien/byebyejetifier/core/config/ConfigParser.kt<|end_filename|>
package com.dipien.byebyejetifier.core.config

import com.dipien.byebyejetifier.common.LoggerHelper
import com.google.gson.GsonBuilder
import java.io.InputStream
import java.io.InputStreamReader
import java.io.Reader
import java.nio.charset.StandardCharsets

object ConfigParser {

    private val gson = GsonBuilder().setPrettyPrinting().create()

    fun loadFromFile(configInputStream: InputStream): Config {
        return parseFromString(readText(configInputStream)) ?: throw RuntimeException("Failed to parseFromString the config file")
    }

    private fun parseFromString(inputText: String): Config? {
        LoggerHelper.info("Parsing config file")
        return gson.fromJson(inputText, Config.JsonData::class.java).toConfig()
    }

    private fun readText(configInputStream: InputStream): String {
        val bufferSize = 1024
        val buffer = CharArray(bufferSize)
        val output = StringBuilder()
        val input: Reader = InputStreamReader(configInputStream, StandardCharsets.UTF_8)
        var charsRead: Int
        while (input.read(buffer, 0, buffer.size).also { charsRead = it } > 0) {
            output.append(buffer, 0, charsRead)
        }
        return output.toString()
    }
}


<|start_filename|>bye-bye-jetifier/src/main/java/com/dipien/byebyejetifier/scanner/bytecode/CustomRemapper.kt<|end_filename|>
package com.dipien.byebyejetifier.scanner.bytecode

import com.dipien.byebyejetifier.core.type.JavaType
import org.objectweb.asm.commons.Remapper

class CustomRemapper(
    private val remapper: CoreRemapper
) : Remapper() {

    var legacyDependencies = mutableSetOf<String>()

    override fun map(typeName: String): String {
        verifyType(JavaType(typeName))
        return super.map(typeName)
    }

    override fun mapPackageName(name: String): String {
        verifyType(JavaType(name))
        return super.mapPackageName(name)
    }

    override fun mapValue(value: Any?): Any? {
        val stringVal = value as? String
        if (stringVal == null) {
            return super.mapValue(value)
        }

        fun mapPoolReferenceType(typeDeclaration: String): String {
            if (!typeDeclaration.contains(".")) {
                verifyType(JavaType(typeDeclaration))
                return typeDeclaration
            }

            if (typeDeclaration.contains("/")) {
                // Mixed "." and "/"  - not something we know how to handle
                return typeDeclaration
            }

            val toRewrite = typeDeclaration.replace(".", "/")
            verifyType(JavaType(toRewrite))
            return typeDeclaration
        }

        if (stringVal.startsWith("L") && stringVal.endsWith(";")) {
            // L denotes a type declaration. For some reason there are references in the constant
            // pool that ASM skips.
            val typeDeclaration = stringVal.substring(1, stringVal.length - 1)
            if (typeDeclaration.isEmpty()) {
                return value
            }

            if (typeDeclaration.contains(";L")) {
                // We have array of constants
                typeDeclaration
                    .split(";L")
                    .forEach { mapPoolReferenceType(it) }
                return super.mapValue(value)
            }

            mapPoolReferenceType(typeDeclaration)
            return super.mapValue(value)
        }

        verifyString(stringVal)
        return super.mapValue(value)
    }

    private fun verifyType(type: JavaType) {
        if (remapper.rewriteType(type) != type) {
            legacyDependencies.add(type.fullName)
        }
    }

    private fun verifyString(value: String) {
        if (remapper.rewriteString(value) != value) {
            legacyDependencies.add(value)
        }
    }
}


<|start_filename|>bye-bye-jetifier/src/main/java/com/dipien/byebyejetifier/core/rule/RewriteRule.kt<|end_filename|>
package com.dipien.byebyejetifier.core.rule

import com.dipien.byebyejetifier.core.type.JavaType
import com.google.gson.annotations.SerializedName
import java.util.regex.Pattern

/**
 * Rule that rewrites a Java type based on the given arguments.
 *
 * @param from Regular expression where packages are separated via '/' and inner class separator
 * is "$". Used to match the input type.
 * @param to A string to be used as a replacement if the 'from' pattern is matched. It can also
 * apply groups matched from the original pattern using {x} annotation, e.g. {0}.
 */
class RewriteRule(private val from: String, private val to: String) {

    companion object {
        const val IGNORE_RUNTIME = "ignore"
    }

    // We escape '$' so we don't conflict with regular expression symbols.
    private val inputPattern = Pattern.compile("^${from.replace("$", "\\$")}$")
    private val outputPattern = to.replace("$", "\$")

    /*
     * Whether this is any type of an ignore rule.
     */
    fun isIgnoreRule() = to == IGNORE_RUNTIME

    /**
     * Rewrites the given java type. Returns null if this rule is not applicable for the given type.
     */
    fun apply(input: JavaType): TypeRewriteResult {
        val matcher = inputPattern.matcher(input.fullName)
        if (!matcher.matches()) {
            return TypeRewriteResult.NOT_APPLIED
        }

        if (isIgnoreRule()) {
            return TypeRewriteResult.IGNORED
        }

        var result = outputPattern
        for (i in 0 until matcher.groupCount()) {
            result = result.replace("{$i}", matcher.group(i + 1))
        }

        return TypeRewriteResult(JavaType(result))
    }

    override fun toString(): String {
        return "$inputPattern -> $outputPattern "
    }

    /**
     * JSON data model for [RewriteRule].
     */
    data class JsonData(
        @SerializedName("from")
        val from: String,

        @SerializedName("to")
        val to: String
    ) {

        /** Creates instance of [RewriteRule] */
        fun toRule(): RewriteRule {
            return RewriteRule(from, to)
        }
    }

    /**
     * Result of java type rewrite using [RewriteRule]
     */
    data class TypeRewriteResult(val result: JavaType?, val isIgnored: Boolean = false) {

        companion object {
            val NOT_APPLIED = TypeRewriteResult(result = null, isIgnored = false)

            val IGNORED = TypeRewriteResult(result = null, isIgnored = true)
        }
    }
}


<|start_filename|>bye-bye-jetifier/src/main/java/com/dipien/byebyejetifier/archive/Archive.kt<|end_filename|>
package com.dipien.byebyejetifier.archive

import com.dipien.byebyejetifier.scanner.ScanResult
import java.io.File
import java.io.FileInputStream
import java.io.IOException
import java.io.InputStream
import java.nio.file.Path
import java.nio.file.Paths
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream

/**
 * Represents an archive (zip, jar, aar ...)
 */
class Archive(
    override val relativePath: Path,
    files: List<ArchiveItem>
) : ArchiveItem {

    private val _files: MutableList<ArchiveItem> = files.toMutableList()

    val files: List<ArchiveItem> = _files

    companion object {
        /** Defines file extensions that are recognized as archives */
        val ARCHIVE_EXTENSIONS = listOf(".jar", ".zip", ".aar")
    }

    override val fileName: String = relativePath.fileName.toString()

    override fun accept(visitor: ArchiveItemVisitor, scanResults: MutableList<ScanResult>) {
        visitor.visit(this, scanResults)
    }

    object Builder {

        /**
         * @param recursive Whether nested archives should be also extracted.
         */
        @Throws(IOException::class)
        fun extract(archiveFile: File, recursive: Boolean = true): Archive {
            val inputStream = FileInputStream(archiveFile)
            inputStream.use {
                return extractArchive(
                    it, archiveFile.toPath(), recursive
                )
            }
        }

        @Throws(IOException::class)
        private fun extractArchive(inputStream: InputStream, relativePath: Path, recursive: Boolean): Archive {
            val zipIn = ZipInputStream(inputStream)
            val files = mutableListOf<ArchiveItem>()

            var entry: ZipEntry? = zipIn.nextEntry
            // iterates over entries in the zip file
            while (entry != null) {
                if (!entry.isDirectory) {
                    val entryPath = Paths.get(entry.name)
                    if (isArchive(entry) && recursive) {
                        files.add(extractArchive(zipIn, entryPath, recursive))
                    } else {
                        files.add(extractFile(zipIn, entryPath))
                    }
                }
                zipIn.closeEntry()
                entry = zipIn.nextEntry
            }
            // Cannot close the zip stream at this moment as that would close also any parent zip
            // streams in case we are processing a nested archive.
            return Archive(relativePath, files.toList())
        }

        @Throws(IOException::class)
        private fun extractFile(zipIn: ZipInputStream, relativePath: Path): ArchiveFile {
            val data = zipIn.readBytes()
            return ArchiveFile(relativePath, data)
        }

        private fun isArchive(zipEntry: ZipEntry): Boolean {
            return ARCHIVE_EXTENSIONS.any { zipEntry.name.endsWith(it, ignoreCase = true) }
        }
    }
}


<|start_filename|>bye-bye-jetifier/src/main/java/com/dipien/byebyejetifier/common/AbstractTask.kt<|end_filename|>
package com.dipien.byebyejetifier.common

import com.dipien.byebyejetifier.ByeByeJetifierGradlePlugin
import com.dipien.byebyejetifier.ByeByeJetifierExtension
import org.gradle.api.DefaultTask
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.TaskAction

abstract class AbstractTask : DefaultTask() {

    @get:Input
    var verbose = false

    @TaskAction
    fun doExecute() {

        LoggerHelper.logger = logger
        LoggerHelper.verbose = verbose

        onExecute()
    }

    @Internal
    protected fun getExtension(): ByeByeJetifierExtension {
        return project.extensions.getByName(ByeByeJetifierGradlePlugin.EXTENSION_NAME) as ByeByeJetifierExtension
    }

    protected abstract fun onExecute()
}


<|start_filename|>bye-bye-jetifier/src/main/java/com/dipien/byebyejetifier/core/config/Config.kt<|end_filename|>
package com.dipien.byebyejetifier.core.config

import com.dipien.byebyejetifier.core.PackageMap
import com.dipien.byebyejetifier.core.rule.RewriteRule
import com.dipien.byebyejetifier.core.rule.RewriteRulesMap
import com.dipien.byebyejetifier.core.type.JavaType
import com.dipien.byebyejetifier.core.type.PackageName
import com.dipien.byebyejetifier.core.type.TypesMap
import com.google.gson.annotations.SerializedName
import java.util.regex.Pattern

/**
 * @param restrictToPackagePrefixes Package prefixes that limit the scope of the rewriting. In most
 *  cases the rules have priority over this. We use this mainly to determine if we are actually
 *  missing a rule in case we fail to rewrite.
 * @param rulesMap Rules to rewrite java types.
 * @param typesMap Map of all java types and fields to be used to rewrite java types.
 * @param packageMap Package map to be used to rewrite packages.
 * @param stringsMap String map to be used to rewrite string values.
 */
class Config(
    val restrictToPackagePrefixes: Set<String>,
    val rulesMap: RewriteRulesMap,
    val typesMap: TypesMap,
    val packageMap: PackageMap,
    val stringsMap: TypesMap
) {

    // Merges all packages prefixes into one regEx pattern
    private val packagePrefixPattern = Pattern.compile(
        "^(" + restrictToPackagePrefixes.map { "($it)" }.joinToString("|") + ").*$"
    )

    /**
     * Returns whether the given type is eligible for rewrite.
     *
     * If not, the transformers should ignore it.
     */
    fun isEligibleForRewrite(type: JavaType): Boolean {
        if (!isEligibleForRewriteInternal(type.fullName)) {
            return false
        }

        val isIgnored = rulesMap.ignoreRules
            .any { it.apply(type) == RewriteRule.TypeRewriteResult.IGNORED }
        return !isIgnored
    }

    private fun isEligibleForRewriteInternal(type: String): Boolean {
        if (restrictToPackagePrefixes.isEmpty()) {
            return false
        }
        return packagePrefixPattern.matcher(type).matches()
    }

    fun isEligibleForRewrite(type: PackageName): Boolean {
        if (!isEligibleForRewriteInternal(type.fullName + "/")) {
            return false
        }

        val javaType = JavaType(type.fullName + "/")
        val isIgnored = rulesMap.ignoreRules
            .any { it.apply(javaType) == RewriteRule.TypeRewriteResult.IGNORED }
        return !isIgnored
    }

    /**
     * JSON data model for [Config].
     */
    data class JsonData(
        @SerializedName("restrictToPackagePrefixes")
        val restrictToPackages: List<String?>,

        @SerializedName("rules")
        val rules: List<RewriteRule.JsonData?>?,

        @SerializedName("packageMap")
        val packageMap: List<PackageMap.PackageRule.JsonData?>,

        @SerializedName("map")
        val mappings: TypesMap.JsonData? = null,

        @SerializedName("stringsMap")
        val stringsMap: TypesMap.JsonData? = null
    ) {

        /** Creates instance of [Config] */
        fun toConfig(): Config {
            return Config(
                restrictToPackagePrefixes = restrictToPackages.filterNotNull().toSet(),
                rulesMap = RewriteRulesMap(
                    rules.orEmpty().filterNotNull().map { it.toRule() }.toList()
                ),
                packageMap = PackageMap(
                    packageMap.filterNotNull().map { it.toMappings() }.toList()
                ),
                typesMap = mappings?.toMappings() ?: TypesMap.EMPTY,
                stringsMap = stringsMap?.toMappings() ?: TypesMap.EMPTY
            )
        }
    }
}


<|start_filename|>bye-bye-jetifier/src/main/java/com/dipien/byebyejetifier/core/type/JavaType.kt<|end_filename|>
package com.dipien.byebyejetifier.core.type

/**
 * Wrapper for Java type declaration.
 *
 * For packages use [PackageName].
 */
data class JavaType(val fullName: String) {

    init {
        if (fullName.contains('.')) {
            throw IllegalArgumentException(
                "The type does not support '.' as package separator! Received '$fullName'."
            )
        }
    }

    companion object {
        /** Creates the type from notation where packages are separated using '.' */
        fun fromDotVersion(fullName: String): JavaType {
            if (fullName.contains("/")) {
                throw IllegalArgumentException(
                    "Type containing '/' cannot be passed to the factory expecting dot " +
                        "separators! Received '$fullName'."
                )
            }

            return JavaType(fullName.replace('.', '/'))
        }
    }

    /** Returns the type as a string where packages are separated using '.' */
    fun toDotNotation(): String {
        return fullName.replace('/', '.')
    }

    /** Whether this type references to an inner type (e.g. MyClass$Inner) */
    fun hasInnerType() = fullName.contains('$')

    /**
     * Returns the root type of this type stripped from any inner types (e.g. for MyClass$Inner
     * returns MyClass)
     */
    fun getRootType(): JavaType {
        if (!hasInnerType()) {
            return this
        }

        return JavaType(fullName.split('$').first())
    }

    /**
     * Returns this type with its root top level type replaced with the give root type.
     */
    fun remapWithNewRootType(root: JavaType): JavaType {
        if (root.hasInnerType()) {
            throw IllegalArgumentException("Cannot remap type with a nested types as a root!")
        }

        val tokens = fullName.split('$').toMutableList()
        tokens[0] = root.fullName
        return JavaType(tokens.joinToString("$"))
    }

    /**
     * Returns parent type of this types (e.g. for test.Class.InnerClass -> returns test.Class). For
     * top level packages returns identity.
     */
    fun getParentType(): JavaType {
        if (fullName.contains("/")) {
            return JavaType(fullName.substringBeforeLast('/'))
        }
        return this
    }

    override fun toString() = fullName
}


<|start_filename|>bye-bye-jetifier/src/main/java/com/dipien/byebyejetifier/scanner/bytecode/CoreRemapper.kt<|end_filename|>
package com.dipien.byebyejetifier.scanner.bytecode

import com.dipien.byebyejetifier.common.LoggerHelper
import com.dipien.byebyejetifier.core.type.JavaType
import com.dipien.byebyejetifier.scanner.ScannerContext

class CoreRemapper(
    private val context: ScannerContext,
    private val archiveFilePath: String
) {

    companion object {

        const val TAG = "CoreRemapper"

        val AMBIGUOUS_STRINGS = setOf(
            JavaType.fromDotVersion("android.support.v4"),
            JavaType.fromDotVersion("android.support.v4.content"),
            JavaType.fromDotVersion("android.support.v4.widget"),
            JavaType.fromDotVersion("android.support.v4.view"),
            JavaType.fromDotVersion("android.support.v4.media"),
            JavaType.fromDotVersion("android.support.v13"),
            JavaType.fromDotVersion("android.support.v13.view"),
            JavaType.fromDotVersion("android.support.v13.app"),
            JavaType.fromDotVersion("android.support.design.widget")
        )
    }

    fun rewriteType(type: JavaType): JavaType {
        val result = context.typeRewriter.rewriteType(type)
        if (result != null) {
            return result
        }

        context.reportNoMappingFoundFailure(TAG, type)
        return type
    }

    fun rewriteString(value: String): String {
        val hasDotSeparators = value.contains(".")
        val hasSlashSeparators = value.contains("/")

        if (hasDotSeparators && hasSlashSeparators) {
            // We do not support mix of both separators
            return value
        }

        val type = if (hasDotSeparators) {
            JavaType.fromDotVersion(value)
        } else {
            JavaType(value)
        }

        if (!context.config.isEligibleForRewrite(type)) {
            return value
        }

        // Verify that we did not make an ambiguous mapping, see b/116745353
        if (AMBIGUOUS_STRINGS.contains(type)) {
            LoggerHelper.warn(
                "The file '$archiveFilePath' contains a string literal " +
                    "with a package reference '$value'. " +
                    "Libraries using reflection such as annotation processors need to be " +
                    "updated manually to add support for androidx. " +
                    "But if you are sure there is no need to transform this package reference " +
                    "you can ignore this message or exclude this file from scanning using " +
                    "the 'excludedFilesFromScanning' property."
            )
            return value
        }

        // Strings map has a priority over types map
        val mappedString = context.config.stringsMap.mapType(type)
        if (mappedString != null) {
            LoggerHelper.debug("Map string: '$type' -> '$mappedString'")
            return if (hasDotSeparators) mappedString.toDotNotation() else mappedString.fullName
        }

        val mappedType = context.config.typesMap.mapType(type)
        if (mappedType != null) {
            LoggerHelper.debug("Map string: '$type' -> '$mappedType'")
            return if (hasDotSeparators) mappedType.toDotNotation() else mappedType.fullName
        }

        // We might be working with an internal type or field reference, e.g.
        // AccessibilityNodeInfoCompat.PANE_TITLE_KEY. So we try to remove last segment to help it.
        if (value.contains(".")) {
            val subTypeResult = context.config.typesMap.mapType(type.getParentType())
            if (subTypeResult != null) {
                val result = subTypeResult.toDotNotation() + '.' + value.substringAfterLast('.')
                LoggerHelper.debug("Map string: '$value' -> '$result' via type fallback")
                return result
            }
        }

        // Try rewrite rules
        val rewrittenType = context.config.rulesMap.rewriteType(type)
        if (rewrittenType != null) {
            LoggerHelper.debug("Map string: '$value' -> '$rewrittenType' via fallback")
            return if (hasDotSeparators) {
                rewrittenType.toDotNotation()
            } else {
                rewrittenType.fullName
            }
        }

        // We do not treat string content mismatches as errors
        LoggerHelper.debug("Found string '$value' but failed to rewrite")
        return value
    }
}


<|start_filename|>bye-bye-jetifier/src/main/java/com/dipien/byebyejetifier/ByeByeJetifierGradlePlugin.kt<|end_filename|>
package com.dipien.byebyejetifier

import com.dipien.byebyejetifier.task.CanISayByeByeJetifierTask
import org.gradle.api.Plugin
import org.gradle.api.Project

class ByeByeJetifierGradlePlugin : Plugin<Project> {

    companion object {
        const val EXTENSION_NAME = "byeByeJetifier"
    }

    override fun apply(project: Project) {
        val extension = project.extensions.create(EXTENSION_NAME, ByeByeJetifierExtension::class.java)

        val canISayByeByeJetifierTask = project.tasks.create(CanISayByeByeJetifierTask.TASK_NAME, CanISayByeByeJetifierTask::class.java)
        project.afterEvaluate {
            canISayByeByeJetifierTask.legacyGroupIdPrefixes = extension.legacyGroupIdPrefixes
            canISayByeByeJetifierTask.excludedConfigurations = extension.excludedConfigurations
            canISayByeByeJetifierTask.excludedFilesFromScanning = extension.excludedFilesFromScanning
            canISayByeByeJetifierTask.excludeSupportAnnotations = extension.excludeSupportAnnotations
            canISayByeByeJetifierTask.excludedProjectsFromScanning = extension.excludedProjectsFromScanning
            canISayByeByeJetifierTask.verbose = extension.verbose
        }
    }
}


<|start_filename|>bye-bye-jetifier/src/main/java/com/dipien/byebyejetifier/ExternalDependency.kt<|end_filename|>
package com.dipien.byebyejetifier

import com.dipien.byebyejetifier.common.LoggerHelper
import org.gradle.api.artifacts.ResolvedArtifact
import org.gradle.api.artifacts.ResolvedDependency

sealed class ExternalDependency(
    private val dependency: ResolvedDependency,
    private val legacyGroupIdPrefixes: List<String>
) {

    data class FirstLevel(
        val dependency: ResolvedDependency,
        val legacyGroupIdPrefixes: List<String>
    ) : ExternalDependency(dependency, legacyGroupIdPrefixes)

    data class Child(
        val dependency: ResolvedDependency,
        val legacyGroupIdPrefixes: List<String>
    ) : ExternalDependency(dependency, legacyGroupIdPrefixes)

    val artifactDefinition: String by lazy {
        "${dependency.moduleGroup}:${dependency.moduleName}:${dependency.moduleVersion}"
    }

    val isLegacyAndroidSupport: Boolean by lazy {
        legacyGroupIdPrefixes.any { dependency.moduleGroup.startsWith(it) }
    }

    val moduleArtifacts: List<ResolvedArtifact> by lazy {
        dependency.moduleArtifacts.toList()
    }

    val children: List<ExternalDependency> by lazy {
        dependency.children.map { Child(it, legacyGroupIdPrefixes) }
    }

    val isFirstLevel: Boolean by lazy {
        this is FirstLevel
    }

    val isAndroidX: Boolean by lazy {
        dependency.moduleGroup.startsWith("androidx")
    }

    fun buildDependencyGraphs(): List<DependencyGraph> {
        return recursiveBuildDependencyGraphs(dependency, DependencyGraph(mutableListOf()))
    }

    private fun recursiveBuildDependencyGraphs(dependency: ResolvedDependency, graph: DependencyGraph): List<DependencyGraph> {
        return if (dependency.parents.isEmpty()) {
            // Root is ignored because it is a project module
            listOf(graph)
        } else {
            val result = mutableListOf<DependencyGraph>()
            graph.add(dependency.name)
            for (parent in dependency.parents) {
                val newGraph = graph.clone()
                result.addAll(recursiveBuildDependencyGraphs(parent, newGraph))
            }
            result
        }
    }

    data class DependencyGraph(private val nodes: MutableList<String>) {

        fun add(node: String) {
            nodes.add(node)
        }

        fun clone(): DependencyGraph =
            DependencyGraph(nodes.toMutableList())

        fun print() {
            for ((level, node) in nodes.reversed().withIndex()) {
                val separator: String = if (level == 0) " " else "     ".repeat(level)
                LoggerHelper.lifeCycle("$separator+---$node")
            }
        }

        override fun toString(): String {
            return "DependencyGraph(nodes=$nodes)"
        }
    }
}


<|start_filename|>bye-bye-jetifier/src/main/java/com/dipien/byebyejetifier/core/type/TypesMap.kt<|end_filename|>
package com.dipien.byebyejetifier.core.type

import java.util.SortedMap

/**
 * Contains all the mappings needed to rewrite java types.
 */
data class TypesMap(private val types: Map<JavaType, JavaType>) {

    companion object {
        val EMPTY = TypesMap(emptyMap())
    }

    init {
        val containsNestedTypes = types.any { it.key.hasInnerType() || it.value.hasInnerType() }
        if (containsNestedTypes) {
            throw IllegalArgumentException("Types map does not support nested types!")
        }
    }

    /** Maps the given type using this map. */
    fun mapType(type: JavaType): JavaType? {
        if (type.hasInnerType()) {
            val rootMapResult = types[type.getRootType()] ?: return null
            return type.remapWithNewRootType(rootMapResult)
        }
        return types[type]
    }

    /**
     * JSON data model for [TypesMap].
     */
    data class JsonData(val types: SortedMap<String, String>) {

        /** Creates instance of [TypesMap] */
        fun toMappings(): TypesMap {
            return TypesMap(
                types = types
                    .orEmpty()
                    .map { JavaType(it.key) to JavaType(it.value) }
                    .toMap()
            )
        }
    }
}


<|start_filename|>bye-bye-jetifier/src/main/java/com/dipien/byebyejetifier/scanner/Scanner.kt<|end_filename|>
package com.dipien.byebyejetifier.scanner

import com.dipien.byebyejetifier.archive.ArchiveFile

interface Scanner {

    fun scan(archiveFile: ArchiveFile): List<ScanResult>

    fun canScan(archiveFile: ArchiveFile): Boolean
}


<|start_filename|>bye-bye-jetifier/src/main/java/com/dipien/byebyejetifier/core/TypeRewriter.kt<|end_filename|>
package com.dipien.byebyejetifier.core

import com.dipien.byebyejetifier.common.LoggerHelper
import com.dipien.byebyejetifier.core.config.Config
import com.dipien.byebyejetifier.core.type.JavaType

class TypeRewriter(private val config: Config) {

    fun rewriteType(type: JavaType): JavaType? {
        val result = config.typesMap.mapType(type)
        if (result != null) {
            LoggerHelper.debug("Map: $type -> $result")
            return result
        }

        if (!config.isEligibleForRewrite(type)) {
            return type
        }

        val rulesResult = config.rulesMap.rewriteType(type)
        if (rulesResult != null) {
            LoggerHelper.debug("Using fallback: $type -> $rulesResult")
            return rulesResult
        }

        return null
    }
}


<|start_filename|>bye-bye-jetifier/src/main/java/com/dipien/byebyejetifier/archive/ArchiveItem.kt<|end_filename|>
package com.dipien.byebyejetifier.archive

import com.dipien.byebyejetifier.scanner.ScanResult
import java.nio.file.Path

/**
 * Abstraction to represent archive and its files as a one thing before and after transformation
 * together with information if any changes happened during the transformation.
 */
interface ArchiveItem {

    /**
     * Relative path of the item according to its location in the archive.
     *
     * Files in a nested archive have a path relative to that archive not to the parent of
     * the archive. The root archive has the file system path set as its relative path.
     */
    val relativePath: Path

    val fileName: String

    fun accept(visitor: ArchiveItemVisitor, scanResults: MutableList<ScanResult>)
}


<|start_filename|>bye-bye-jetifier/src/main/java/com/dipien/byebyejetifier/archive/ArchiveFile.kt<|end_filename|>
package com.dipien.byebyejetifier.archive

import com.dipien.byebyejetifier.common.toFilePath
import com.dipien.byebyejetifier.scanner.ScanResult
import java.nio.file.Path

/**
 * Represents a file in the archive that is not an archive.
 */
class ArchiveFile(relativePath: Path, data: ByteArray) : ArchiveItem {

    companion object {
        val RES_LAYOUT_PATH = "res/layout".toFilePath()
    }

    override var relativePath = relativePath
        private set

    override var fileName: String = relativePath.fileName.toString()
        private set

    var data: ByteArray = data
        private set

    override fun accept(visitor: ArchiveItemVisitor, scanResults: MutableList<ScanResult>) {
        visitor.visit(this, scanResults)
    }

    fun isLayoutResource() = relativePath.toString().startsWith(RES_LAYOUT_PATH, ignoreCase = true) &&
        fileName.endsWith(".xml", ignoreCase = true)

    fun isAndroidManifestFile() = fileName.endsWith("AndroidManifest.xml", ignoreCase = true)

    fun isClassFile() = fileName.endsWith(".class", ignoreCase = true)
}


<|start_filename|>sample/buildSrc/src/main/kotlin/Libs.kt<|end_filename|>
object Libs {

    const val FIREBASE_ADS = "com.google.firebase:firebase-ads:19.6.0"
    const val FIREABSE_AUTH = "com.google.firebase:firebase-auth:20.0.1"
    const val FIREBASE_APPINDEXING = "com.google.firebase:firebase-appindexing:19.1.0"
    const val FIREBASE_ANALYTICS = "com.google.firebase:firebase-analytics-ktx:18.0.0"
    const val FIREBASE_CONFIG = "com.google.firebase:firebase-config:20.0.1"
    const val FIREBASE_REMOTE_CONFIG = "com.google.firebase:firebase-config-ktx:20.0.1"
    const val FIREBASE_CRASHLYTICS = "com.google.firebase:firebase-crashlytics:17.3.0"
    const val FIREBASE_DATABASE = "com.google.firebase:firebase-database:19.5.1"
    const val FIREBASE_DYNAMIC_LINKS = "com.google.firebase:firebase-dynamic-links:19.1.1"
    const val FIREBASE_MESSAGING = "com.google.firebase:firebase-messaging:21.0.0"
    const val FIREBASE_PERF = "com.google.firebase:firebase-perf:19.0.10"

    const val ANDROIDX_ANNOTATION = "androidx.annotation:annotation:1.1.0"
    const val ANDROIDX_APPCOMPAT = "androidx.appcompat:appcompat:1.2.0"
    const val ANDROIDX_BROWSER = "androidx.browser:browser:1.2.0"
    const val ANDROIDX_CARDVIEW = "androidx.cardview:cardview:1.0.0"
    const val ANDROIDX_CONSTRAINTLAYOUT = "androidx.constraintlayout:constraintlayout:2.0.4"
    const val ANDROIDX_CORE = "androidx.core:core:1.3.2"
    const val ANDROIDX_CORE_KTX = "androidx.core:core-ktx:1.3.2"
    const val ANDROIDX_COORDINATOR_LAYOUT = "androidx.coordinatorlayout:coordinatorlayout:1.1.0"
    const val ANDROIDX_CORE_TESTING = "androidx.arch.core:core-testing:2.1.0"
    const val ANDROIDX_DRAWER_LAYOUT = "androidx.drawerlayout:drawerlayout:1.1.1"
    const val ANDROIDX_ESPRESSO_CORE = "androidx.test.espresso:espresso-core:3.3.0"
    const val ANDROIDX_ESPRESSO_CONTRIB = "androidx.test.espresso:espresso-contrib:3.3.0"
    const val ANDROIDX_ESPRESSO_INTENTS = "androidx.test.espresso:espresso-intents:3.3.0"
    const val ANDROIDX_EXIFIINTERFACE = "androidx.exifinterface:exifinterface:1.3.1"
    const val ANDROIDX_FRAGMENT_KTX = "androidx.fragment:fragment-ktx:1.2.5"
    const val ANDROIDX_JUNIT = "androidx.test.ext:junit:1.1.2"
    const val ANDROIDX_LIFECYCLE_VIEWMODEL_KTX = "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
    const val ANDROIDX_LIFECYCLE_COMPILER = "androidx.lifecycle:lifecycle-compiler:2.2.0"
    const val ANDROIDX_LIFECYCLE_PROCESS = "androidx.lifecycle:lifecycle-process:2.2.0"
    const val ANDROIDX_LIFECYCLE_REACTIVE_STREAMS = "androidx.lifecycle:lifecycle-reactivestreams:2.2.0"
    const val ANDROIDX_LIFECYCLE_LIVEDATA_KTX = "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0"
    const val ANDROIDX_NAVIGATION_FRAGMENT = "androidx.navigation:navigation-fragment:2.3.1"
    const val ANDROIDX_NAVIGATION_FRAGMENT_KTX = "androidx.navigation:navigation-fragment-ktx:2.3.1"
    const val ANDROIDX_NAVIGATION_UI_KTX = "androidx.navigation:navigation-ui-ktx:2.3.1"
    const val ANDROIDX_PALETTE = "androidx.palette:palette:1.0.0"
    const val ANDROIDX_PREFERENCE = "androidx.preference:preference-ktx:1.1.1"
    const val ANDROIDX_RECYCLER_VIEW = "androidx.recyclerview:recyclerview:1.1.0"
    const val ANDROIDX_ROOM_RUNTIME = "androidx.room:room-runtime:2.2.5"
    const val ANDROIDX_ROOM_RXJAVA2 = "androidx.room:room-rxjava2:2.2.5"
    const val ANDROIDX_ROOM_COMPILER = "androidx.room:room-compiler:2.2.5"
    const val ANDROIDX_ROOM_KTX = "androidx.room:room-ktx:2.2.5"
    const val ANDROIDX_ROOM_TESTING = "androidx.room:room-testing:2.2.5"
    const val ANDROIDX_SWIPEREFRESHLAYOUT = "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
    const val ANDROIDX_UIAUTOMATOR = "androidx.test.uiautomator:uiautomator:2.2.0"
    const val ANDROIDX_TEST_CORE = "androidx.test:core:1.3.0"
    const val ANDROIDX_TEST_RUNNER = "androidx.test:runner:1.3.0"
    const val ANDROIDX_VECTOR_DRAWABLE = "androidx.vectordrawable:vectordrawable:1.1.0"
    const val ANDROIDX_VIEWPAGER2 = "androidx.viewpager2:viewpager2:1.0.0"
    const val ANDROIDX_WORK_MANAGER_RUNTIME_KTX = "androidx.work:work-runtime-ktx:2.4.0"
    const val ANDROIDX_WORK_MANAGER_TESTING = "androidx.work:work-testing:2.4.0"

    const val GOOGLE_MATERIAL = "com.google.android.material:material:1.2.1"

    const val DAGGER_ANDROID = "com.google.dagger:dagger-android-support:2.29.1"
    const val DAGGER_ANDROID_PROCESSOR = "com.google.dagger:dagger-android-processor:2.29.1"
    const val DAGGER_COMPILER = "com.google.dagger:dagger-compiler:2.29.1"

    const val PLAY_CORE = "com.google.android.play:core:1.9.0"

    const val PLAY_SERVICES_AUTH = "com.google.android.gms:play-services-auth:18.1.0"
    const val PLAY_SERVICES_BASE = "com.google.android.gms:play-services-base:17.5.0"
    const val PLAY_SERVICES_GAMES = "com.google.android.gms:play-services-games:20.0.1"
    const val PLAY_SERVICES_MAPS = "com.google.android.gms:play-services-maps:17.0.0"

    const val BILLING = "com.android.billingclient:billing:3.0.2"

    const val KOTLIN = "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.4.20"
    const val KOTLIN_REFLECT = "org.jetbrains.kotlin:kotlin-reflect:1.4.20"
    const val KOTLIN_COROUTINES_ANDROID = "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.1"
    const val KOTLIN_COROUTINES_TEST = "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.4.1"
    const val KOTLINX_COROUTINES_RX2 = "org.jetbrains.kotlinx:kotlinx-coroutines-rx2:1.4.1"

    const val JUNIT = "junit:junit:4.13"
    const val TRUTH = "com.google.truth:truth:1.0.1"

    const val ROBOLECTRIC = "org.robolectric:robolectric:4.5.1"

    const val LEAKCANARY_OBJECT_WATCHER = "com.squareup.leakcanary:leakcanary-object-watcher-android-support-fragments:2.5"
    const val LEAKCANARY_SHARK_ANDROID = "com.squareup.leakcanary:shark-android:2.5"

    const val FACEBOOK_ANDROID = "com.facebook.android:facebook-core:8.1.0"
}


<|start_filename|>buildSrc/src/main/kotlin/Libs.kt<|end_filename|>
object Libs {

    // https://asm.ow2.io/versions.html
    const val ASM = "org.ow2.asm:asm:9.2"
    const val ASM_UTILS = "org.ow2.asm:asm-util:9.2"
    const val ASM_COMMONS = "org.ow2.asm:asm-commons:9.2"

    // https://github.com/junit-team/junit4/releases
    const val JUNIT = "junit:junit:4.13.2"

    // https://github.com/google/gson/releases
    const val GSON = "com.google.code.gson:gson:2.8.7"
}


<|start_filename|>bye-bye-jetifier/src/main/java/com/dipien/byebyejetifier/core/rule/RewriteRulesMap.kt<|end_filename|>
package com.dipien.byebyejetifier.core.rule

import com.dipien.byebyejetifier.core.type.JavaType

/**
 * Contains all [RewriteRule]s.
 */
class RewriteRulesMap(private val rewriteRules: List<RewriteRule>) {

    val ignoreRules = rewriteRules.filter { it.isIgnoreRule() }.toSet()

    /**
     * Tries to rewrite the given given type using the rules. If
     */
    fun rewriteType(type: JavaType): JavaType? {
        // Try to find a rule
        for (rule in rewriteRules) {
            val typeRewriteResult = rule.apply(type)
            if (typeRewriteResult.result == null) {
                continue
            }
            return typeRewriteResult.result
        }

        return null
    }
}


<|start_filename|>bye-bye-jetifier/src/main/java/com/dipien/byebyejetifier/core/type/PackageName.kt<|end_filename|>
package com.dipien.byebyejetifier.core.type

/**
 * Wrapper for Java package name declaration.
 */
data class PackageName(val fullName: String) {

    init {
        if (fullName.contains('.')) {
            throw IllegalArgumentException("The type does not support '.' as a package separator!")
        }
    }

    companion object {
        /** Creates the package from notation where packages are separated using '.' */
        fun fromDotVersion(fullName: String): PackageName {
            return PackageName(fullName.replace('.', '/'))
        }
    }

    /** Returns the package as a string where packages are separated using '.' */
    fun toDotNotation(): String {
        return fullName.replace('/', '.')
    }

    override fun toString() = fullName
}


<|start_filename|>bye-bye-jetifier/src/main/java/com/dipien/byebyejetifier/archive/ArchiveItemVisitor.kt<|end_filename|>
package com.dipien.byebyejetifier.archive

import com.dipien.byebyejetifier.scanner.ScanResult

/**
 * Visitor for [ArchiveItem]
 */
interface ArchiveItemVisitor {

    fun visit(archive: Archive, scanResults: MutableList<ScanResult>)

    fun visit(archiveFile: ArchiveFile, scanResults: MutableList<ScanResult>)
}


<|start_filename|>bye-bye-jetifier/src/main/java/com/dipien/byebyejetifier/core/PackageMap.kt<|end_filename|>
package com.dipien.byebyejetifier.core

import com.dipien.byebyejetifier.core.type.PackageName
import com.google.gson.annotations.SerializedName

/**
 * Package map to be used to rewrite packages. The rewrite rules allow duplicities where the
 * artifact name prefix defined in a rule determines if such rule should be used or skipped.
 * The priority is determined only by the order (top to bottom). Having a rule with no file prefix
 * as first means that it is always applied.
 */
class PackageMap(private val rules: List<PackageRule>) {

    /**
     * Returns a new package name for the given [fromPackage].
     */
    fun getPackageFor(fromPackage: PackageName): PackageName? {
        val rule = rules.find { it.from == fromPackage.fullName }
        if (rule != null) {
            return PackageName(rule.to)
        }
        return null
    }

    data class PackageRule(val from: String, val to: String) {

        /**
         * JSON data model for [PackageRule].
         */
        data class JsonData(
            @SerializedName("from")
            val from: String,
            @SerializedName("to")
            val to: String
        ) {
            /** Creates instance of [PackageRule] */
            fun toMappings(): PackageRule {
                return PackageRule(from, to)
            }
        }
    }
}


<|start_filename|>bye-bye-jetifier/src/main/java/com/dipien/byebyejetifier/common/StringExtensions.kt<|end_filename|>
package com.dipien.byebyejetifier.common

import java.io.File

fun String.toFilePath(): String {
    return this.replace('/', File.separatorChar)
}


<|start_filename|>bye-bye-jetifier/src/main/java/com/dipien/byebyejetifier/scanner/ScanResult.kt<|end_filename|>
package com.dipien.byebyejetifier.scanner

data class ScanResult(val relativePath: String, val legacyDependency: String)
