| /* |
| * Copyright (C) 2023 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.android.screenshot.cli |
| |
| import com.android.screenshot.cli.util.CODE_ERROR |
| import com.android.screenshot.cli.util.CODE_FAILURE |
| import com.android.screenshot.cli.util.CODE_NO_PREVIEWS |
| import com.android.screenshot.cli.util.CODE_SUCCESS |
| import com.android.screenshot.cli.util.Decompressor |
| import com.android.screenshot.cli.util.PreviewResult |
| import com.android.screenshot.cli.util.Response |
| import com.android.tools.idea.AndroidPsiUtils |
| import com.android.tools.idea.compose.preview.ComposePreviewElement |
| import com.android.tools.idea.compose.preview.getPreviewNodes |
| import com.android.tools.idea.util.toVirtualFile |
| import com.android.tools.lint.CliConfiguration |
| import com.android.tools.lint.LintCliClient |
| import com.android.tools.lint.LintCliFlags |
| import com.android.tools.lint.LintCliFlags.ERRNO_ERRORS |
| import com.android.tools.lint.LintCliFlags.ERRNO_INVALID_ARGS |
| import com.android.tools.lint.ProjectMetadata |
| import com.android.tools.lint.client.api.Configuration |
| import com.android.tools.lint.client.api.ConfigurationHierarchy |
| import com.android.tools.lint.client.api.IssueRegistry |
| import com.android.tools.lint.client.api.LintClient.Companion.clientName |
| import com.android.tools.lint.client.api.LintDriver |
| import com.android.tools.lint.client.api.LintRequest |
| import com.android.tools.lint.client.api.LintXmlConfiguration.Companion.create |
| import com.android.tools.lint.client.api.Vendor |
| import com.android.tools.lint.computeMetadata |
| import com.android.tools.lint.detector.api.Context |
| import com.android.tools.lint.detector.api.Incident |
| import com.android.tools.lint.detector.api.Issue |
| import com.android.tools.lint.detector.api.LintModelModuleProject |
| import com.android.tools.lint.detector.api.LintModelModuleProject.Companion.resolveDependencies |
| import com.android.tools.lint.detector.api.Location |
| import com.android.tools.lint.detector.api.Location.Companion.create |
| import com.android.tools.lint.detector.api.Project |
| import com.android.tools.lint.detector.api.Scope |
| import com.android.tools.lint.detector.api.Severity |
| import com.android.tools.lint.detector.api.guessGradleLocation |
| import com.android.tools.lint.detector.api.isJreFolder |
| import com.android.tools.lint.detector.api.splitPath |
| import com.android.tools.lint.model.LintModelModule |
| import com.android.tools.lint.model.LintModelSerialization |
| import com.android.tools.lint.model.LintModelSourceProvider |
| import com.android.tools.lint.model.LintModelVariant |
| import com.android.tools.lint.model.PathVariables |
| import com.android.utils.XmlUtils |
| import com.google.common.io.ByteStreams |
| import com.intellij.core.CoreApplicationEnvironment |
| import com.intellij.openapi.application.runReadAction |
| import com.intellij.openapi.extensions.Extensions |
| import com.intellij.pom.java.LanguageLevel |
| import com.intellij.psi.util.PsiTreeUtil |
| import org.jetbrains.kotlin.config.LanguageVersionSettings |
| import org.jetbrains.kotlin.psi.KtAnnotationEntry |
| import org.jetbrains.kotlin.utils.PathUtil |
| import org.jetbrains.uast.UAnnotation |
| import org.jetbrains.uast.getContainingUMethod |
| import org.jetbrains.uast.toUElementOfType |
| import org.w3c.dom.Document |
| import org.xml.sax.SAXException |
| import java.io.File |
| import java.io.IOException |
| import java.nio.file.Path |
| import java.util.EnumSet |
| import java.util.zip.ZipEntry |
| import java.util.zip.ZipException |
| import java.util.zip.ZipFile |
| import kotlin.system.exitProcess |
| |
| class Main { |
| |
| private val ARG_CLIENT_ID = "--client-id" |
| private val ARG_CLIENT_NAME = "--client-name" |
| private val ARG_CLIENT_VERSION = "--client-version" |
| private val ARG_SDK_HOME = "--sdk-home" |
| private val ARG_JDK_HOME = "--jdk-home" |
| private val ARG_LINT_MODEL = "--lint-model" |
| private val ARG_LINT_RULE_JARS = "--lint-rule-jars" |
| private val ARG_CACHE_DIR = "--cache-dir" |
| private val ARG_OUTPUT_LOCATION = "--output-location" |
| private val ARG_GOLDEN_LOCATION = "--golden-location" |
| private val ARG_FILE_PATH = "--file-path" |
| private val ARG_ROOT_LINT_MODEL = "--root-lint-model" |
| private val ARG_RECORD_GOLDENS = "--record-golden" |
| private val ARG_EXTRACTION_DIR = "--extraction-dir" |
| private val ARG_JAR_LOCATION = "--jar-location" |
| |
| private var sdkHomePath: File? = null |
| private var jdkHomePath: File? = null |
| |
| private val flags = LintCliFlags() |
| companion object { |
| @JvmStatic |
| fun main(args: Array<String>) { |
| Main().run(args) |
| } |
| } |
| fun run(args: Array<String>) { |
| val argumentState = ArgumentState() |
| try { |
| val client: LintCliClient = MainLintClient(flags, argumentState) |
| parseArguments(args, client, argumentState) |
| if (argumentState.extractionDir != null && argumentState.jarLocation != null){ |
| extractJar(argumentState) |
| } |
| initializePathVariables(argumentState, client) |
| initializeConfigurations(client, argumentState) |
| setupPaths(argumentState) |
| val projects: List<Project> = configureProject(client, argumentState) |
| val driver: LintDriver = createDriver(projects, client as MainLintClient) |
| client.initializeProjects(driver, projects) |
| |
| ComposeApplication.setupEnvVars(argumentState.extractionDir) |
| CoreApplicationEnvironment.registerExtensionPointAndExtensions(PathUtil.getResourcePathForClass(this::class.java).toPath(), "plugin.xml", |
| Extensions.getRootArea()) |
| |
| driver.computeDetectors(projects[0]) |
| ProjectDriver(driver, projects[0]).prepareUastFileList() |
| initializeEnv(projects, client) |
| val dependencies = Dependencies(projects[0], argumentState.rootModule!!) |
| val screenshot = ScreenshotProvider(projects[0], sdkHomePath!!.absolutePath, dependencies) |
| val results = screenshot.verifyScreenshot(findPreviewNodes(projects[0], argumentState.filePath!!), |
| argumentState.goldenLocation!!, |
| argumentState.outputLocation!!, |
| argumentState.recordGoldens, |
| argumentState.rootModule!!) |
| argumentState.extractionDir?.let { deleteTempFiles(it) } |
| val response = processResults(results) |
| saveResults(response, argumentState.outputLocation!!) |
| exitProcess(response.status) |
| } catch (e: Exception) { |
| val response = Response(2, e.message!!, null) |
| saveResults(response, argumentState.outputLocation!!) |
| exitProcess(response.status) |
| } |
| } |
| |
| private fun saveResults(response: Response, outputLocation: String) { |
| //TODO - find a XMLParser to be used here |
| val xmlString = response.toString() |
| val outputFile = File("$outputLocation/response.xml") |
| if (!outputFile.exists()) { |
| outputFile.createNewFile() |
| } |
| outputFile.writeText(xmlString) |
| } |
| |
| private fun processResults(results: List<PreviewResult>): Response { |
| if (results.isEmpty()) { |
| return Response(CODE_NO_PREVIEWS, "Unable to find previews in file", null) |
| } |
| if (!results.none { it.responseCode == CODE_ERROR }) { |
| return Response(CODE_ERROR, "Error rendering 1 or more previews", results) |
| } |
| if (!results.none { it.responseCode == CODE_FAILURE }) { |
| return Response(CODE_FAILURE, "One or more previews failed to match reference", results) |
| } |
| return Response(CODE_SUCCESS, "Test run successfully", results) |
| } |
| |
| private fun setupPaths(argumentState: Main.ArgumentState) { |
| val output = File(argumentState.outputLocation!!) |
| val golden = File(argumentState.goldenLocation!!) |
| if (!output.exists()) { |
| output.mkdirs() |
| } |
| if (!golden.exists()) { |
| golden.mkdirs() |
| } |
| |
| } |
| |
| private fun deleteTempFiles(extractionDir: String) { |
| val outputDir = Path.of(extractionDir).resolve("system").normalize() |
| try { |
| outputDir.toFile().deleteRecursively() |
| } catch (e: Exception) { |
| // do nothing |
| } |
| |
| |
| } |
| |
| private fun extractJar(argumentState: Main.ArgumentState) { |
| val jarPath = Path.of(argumentState.jarLocation!!) |
| val outputDir = Path.of(argumentState.extractionDir!!) |
| val outputDirLayoutLib = Path.of(argumentState.extractionDir!!).resolve("plugins/design-tools/resources/").normalize() |
| val outputDirAppInfo = Path.of(argumentState.extractionDir!!).resolve("META-INF").normalize() |
| val filterLayouutLib = { file: File?, name: String -> |
| name =="layoutlib" || file?.absolutePath?.contains("layoutlib") ?: false } |
| val filterAppInfo = { file: File?, name: String -> |
| name == "ApplicationInfo.xml" && file?.absolutePath?.startsWith(outputDirAppInfo.toString()) ?: false } |
| |
| Decompressor.Zip(jarPath).filter(Decompressor.FileFilterAdapter.wrap(outputDirLayoutLib, filterLayouutLib)).removePrefixPath("prebuilts/studio/").overwrite(false).extract(outputDirLayoutLib) |
| Decompressor.Zip(jarPath).filter(Decompressor.FileFilterAdapter.wrap(outputDir, filterAppInfo)).overwrite(true).extract(outputDir) |
| } |
| |
| private fun findPreviewNodes(project: Project, file: String) : List<ComposePreviewElement> { |
| val vFile = File(file) |
| val psiFile = AndroidPsiUtils.getPsiFileSafely(project.ideaProject!!, vFile.toVirtualFile()!!) |
| val annotationEntry = PsiTreeUtil.findChildrenOfType(psiFile, KtAnnotationEntry::class.java).asSequence() |
| val annotations = annotationEntry.mapNotNull { it.psiOrParent.toUElementOfType<UAnnotation>() } |
| val uMethods = annotations.mapNotNull { it.getContainingUMethod() }.toSet() |
| val previewNodes = uMethods.flatMap { runReadAction { getPreviewNodes(it, null, false) } }.filterIsInstance<ComposePreviewElement>().toList() |
| return previewNodes |
| } |
| |
| /** |
| * Configure project with idea project and configure CoreAppEnv |
| */ |
| private fun initializeEnv(projects: Collection<Project>, client: MainLintClient) { |
| for (project in projects) { |
| project.ideaProject = client.uastEnvironment!!.ideaProject |
| project.env = client.uastEnvironment!!.coreAppEnv |
| } |
| } |
| |
| @Suppress("UNCHECKED_CAST") |
| private fun createDriver(projects: List<Project>, client: MainLintClient): LintDriver { |
| val emptyIssueRegistry = |
| object : IssueRegistry() { |
| override val vendor: Vendor = AOSP_VENDOR |
| override val issues: List<Issue> |
| get() = listOf() |
| } |
| val roots = resolveDependencies(projects as List<LintModelModuleProject>, false) |
| |
| val lintRequest = LintRequest(client, emptyList()) |
| lintRequest.setProjects(roots) |
| |
| return client.createDriver(emptyIssueRegistry, lintRequest) |
| } |
| |
| private fun configureProject(client: LintCliClient, argumentState: ArgumentState): List<Project> { |
| val modules: List<LintModelModule> = argumentState.modules |
| val projects: MutableList<Project> = ArrayList() |
| if (modules.isNotEmpty()) { |
| for (module: LintModelModule in modules) { |
| val dir = module.dir |
| val variant: LintModelVariant? = module.defaultVariant() |
| assert(variant != null) |
| val project = LintModelModuleProject( |
| client, dir, dir, |
| (variant)!!, null |
| ) |
| client.registerProject(project.dir, project) |
| projects.add(project) |
| } |
| } |
| return projects |
| } |
| |
| private fun initializeConfigurations( |
| client: LintCliClient, |
| argumentState: ArgumentState |
| ) { |
| val configurations = client.configurations |
| val overrideConfig = flags.overrideLintConfig |
| if (overrideConfig != null) { |
| val config: Configuration = create(configurations, overrideConfig) |
| configurations.addGlobalConfigurations(null, config) |
| } |
| val override = CliConfiguration(configurations, flags, flags.isFatalOnly) |
| val defaultConfiguration = flags.lintConfig |
| configurations.addGlobalConfigurationFromFile(defaultConfiguration, override) |
| client.syncConfigOptions() |
| if (argumentState.modules.isNotEmpty()) { |
| val dir = argumentState.modules[0].dir |
| override.associatedLocation = create(dir) |
| } |
| } |
| |
| private fun initializePathVariables( |
| argumentState: ArgumentState, client: LintCliClient |
| ) { |
| val pathVariables = client.pathVariables |
| for (module in argumentState.modules) { |
| // Add project directory path variable |
| pathVariables.add( |
| "{" + module.modulePath + "*projectDir}", module.dir, false |
| ) |
| // Add build directory path variable |
| pathVariables.add( |
| "{" + module.modulePath + "*buildDir}", module.buildFolder, false |
| ) |
| for (variant in module.variants) { |
| for ((sourceProviderIndex, sourceProvider) in variant.sourceProviders.withIndex()) { |
| addSourceProviderPathVariables( |
| pathVariables, |
| sourceProvider, |
| "sourceProvider", |
| sourceProviderIndex, |
| module.modulePath, |
| variant.name |
| ) |
| } |
| for ((testSourceProviderIndex, testSourceProvider) in variant.testSourceProviders.withIndex()) { |
| addSourceProviderPathVariables( |
| pathVariables, |
| testSourceProvider, |
| "testSourceProvider", |
| testSourceProviderIndex, |
| module.modulePath, |
| variant.name |
| ) |
| } |
| for ((testFixturesSourceProviderIndex, testFixturesSourceProvider) in variant.testFixturesSourceProviders.withIndex()) { |
| addSourceProviderPathVariables( |
| pathVariables, |
| testFixturesSourceProvider, |
| "testFixturesSourceProvider", |
| testFixturesSourceProviderIndex, |
| module.modulePath, |
| variant.name |
| ) |
| } |
| } |
| } |
| pathVariables.sort() |
| } |
| |
| /** Adds necessary path variables to pathVariables. */ |
| private fun addSourceProviderPathVariables( |
| pathVariables: PathVariables, |
| sourceProvider: LintModelSourceProvider, |
| sourceProviderType: String, |
| sourceProviderIndex: Int, |
| modulePath: String, |
| variantName: String |
| ) { |
| addSourceProviderPathVariables( |
| pathVariables, |
| sourceProvider.manifestFiles, |
| modulePath, |
| variantName, |
| sourceProviderType, |
| sourceProviderIndex, |
| "manifest" |
| ) |
| addSourceProviderPathVariables( |
| pathVariables, |
| sourceProvider.javaDirectories, |
| modulePath, |
| variantName, |
| sourceProviderType, |
| sourceProviderIndex, |
| "javaDir" |
| ) |
| addSourceProviderPathVariables( |
| pathVariables, |
| sourceProvider.resDirectories, |
| modulePath, |
| variantName, |
| sourceProviderType, |
| sourceProviderIndex, |
| "resDir" |
| ) |
| addSourceProviderPathVariables( |
| pathVariables, |
| sourceProvider.assetsDirectories, |
| modulePath, |
| variantName, |
| sourceProviderType, |
| sourceProviderIndex, |
| "assetsDir" |
| ) |
| } |
| |
| /** Adds necessary path variables to pathVariables. */ |
| private fun addSourceProviderPathVariables( |
| pathVariables: PathVariables, |
| files: Collection<File>, |
| modulePath: String, |
| variantName: String, |
| sourceProviderType: String, |
| sourceProviderIndex: Int, |
| sourceType: String |
| ) { |
| for ((index, file) in files.withIndex()) { |
| val name = ("{" |
| + modulePath |
| + "*" |
| + variantName |
| + "*" |
| + sourceProviderType |
| + "*" |
| + sourceProviderIndex |
| + "*" |
| + sourceType |
| + "*" |
| + index |
| + "}") |
| pathVariables.add(name, file, false) |
| } |
| } |
| |
| private fun parseArguments( |
| args: Array<String>, |
| client: LintCliClient, |
| argumentState: ArgumentState |
| ) { |
| var index = 0 |
| while (index < args.size) { |
| val arg = args[index] |
| if (arg == ARG_CLIENT_ID) { |
| if (index == args.size - 1) { |
| throw InvalidArgumentException("Missing client id") |
| } |
| clientName = args[++index] |
| } else if (arg == ARG_CLIENT_NAME) { |
| if (index == args.size - 1) { |
| throw InvalidArgumentException("Missing client name") |
| } |
| argumentState.clientName = args[++index] |
| } else if (arg == ARG_OUTPUT_LOCATION) { |
| if (index == args.size - 1) { |
| throw InvalidArgumentException("Missing argument location") |
| } |
| argumentState.outputLocation = args[++index] |
| } else if (arg == ARG_GOLDEN_LOCATION) { |
| if (index == args.size - 1) { |
| throw InvalidArgumentException("Missing argument location") |
| } |
| argumentState.goldenLocation = args[++index] |
| } else if (arg == ARG_RECORD_GOLDENS) { |
| argumentState.recordGoldens = true |
| } else if (arg == ARG_FILE_PATH) { |
| if (index == args.size - 1) { |
| throw InvalidArgumentException("Missing input file path") |
| } |
| argumentState.filePath = args[++index] |
| } else if (arg == ARG_CLIENT_VERSION) { |
| if (index == args.size - 1) { |
| throw InvalidArgumentException("Missing client version") |
| } |
| argumentState.clientVersion = args[++index] |
| } else if (arg == ARG_EXTRACTION_DIR) { |
| if (index == args.size - 1) { |
| throw InvalidArgumentException("Missing extraction directory") |
| } |
| argumentState.extractionDir = args[++index] |
| } else if (arg == ARG_JAR_LOCATION) { |
| if (index == args.size - 1) { |
| throw InvalidArgumentException("Missing jar location") |
| } |
| argumentState.jarLocation = args[++index] |
| } else if (arg == ARG_ROOT_LINT_MODEL) { |
| if (index == args.size - 1) { |
| throw InvalidArgumentException("Missing lint model argument after $ARG_LINT_MODEL") |
| } |
| val paths = args[++index] |
| for (path: String in splitPath(paths)) { |
| val input: File = getInArgumentPath(path) |
| if (!input.exists()) { |
| throw InvalidArgumentException("Lint model $input does not exist.") |
| } |
| if (!input.isDirectory) { |
| throw InvalidArgumentException( |
| "Lint model " |
| + input |
| + " should be a folder containing the XML descriptor files" |
| + if (input.isDirectory) ", not a file" else "" |
| ) |
| } |
| try { |
| val reader = LintModelSerialization |
| val module = reader.readModule(input, null, true, client.pathVariables) |
| argumentState.rootModule = module |
| } catch (error: Throwable) { |
| throw InvalidArgumentException( |
| ("Could not deserialize " |
| + input |
| + " to a lint model: " |
| + error.toString()) |
| ) |
| } |
| } |
| } else if (arg == ARG_SDK_HOME) { |
| if (index == args.size - 1) { |
| throw InvalidArgumentException("Missing SDK home directory") |
| } |
| sdkHomePath = File(args[++index]) |
| if (!sdkHomePath!!.isDirectory) { |
| throw InvalidArgumentException(sdkHomePath.toString() + " is not a directory") |
| } |
| } else if (arg == ARG_JDK_HOME) { |
| if (index == args.size - 1) { |
| throw InvalidArgumentException("Missing JDK home directory") |
| } |
| jdkHomePath = File(args[++index]) |
| if (!jdkHomePath!!.isDirectory) { |
| throw InvalidArgumentException(jdkHomePath.toString() + " is not a directory") |
| } |
| if (!isJreFolder(jdkHomePath!!)) { |
| throw InvalidArgumentException(jdkHomePath.toString() + " is not a JRE/JDK") |
| } |
| } else if (arg == ARG_LINT_MODEL) { |
| if (index == args.size - 1) { |
| throw InvalidArgumentException("Missing lint model argument after $ARG_LINT_MODEL") |
| } |
| val paths = args[++index] |
| for (path: String in splitPath(paths)) { |
| val input: File = getInArgumentPath(path) |
| if (!input.exists()) { |
| throw InvalidArgumentException("Lint model $input does not exist.") |
| } |
| if (!input.isDirectory) { |
| throw InvalidArgumentException( |
| "Lint model " |
| + input |
| + " should be a folder containing the XML descriptor files" |
| ) |
| } |
| try { |
| val reader = LintModelSerialization |
| val module = reader.readModule(input, null, true, client.pathVariables) |
| argumentState.modules.add(module) |
| } catch (error: Throwable) { |
| throw InvalidArgumentException( |
| ("Could not deserialize " |
| + input |
| + " to a lint model: " |
| + error.toString()) |
| ) |
| } |
| } |
| } else if ((arg == ARG_CACHE_DIR)) { |
| if (index == args.size - 1) { |
| throw InvalidArgumentException("Missing cache directory") |
| } |
| val path = args[++index] |
| val input: File = getInArgumentPath(path) |
| flags.setCacheDir(input) |
| } else { |
| throw InvalidArgumentException("Unexpected argument: " + args[index]) |
| } |
| index++ |
| } |
| argumentState.validate() |
| } |
| |
| /** |
| * Converts a relative or absolute command-line argument into an input file. |
| * |
| * @param filename The filename given as a command-line argument. |
| * @return A File matching filename, either absolute or relative to lint.workdir if defined. |
| */ |
| private fun getInArgumentPath(filename: String): File { |
| var file = File(filename) |
| if (!file.isAbsolute) { |
| if (!file.isAbsolute) { |
| file = file.absoluteFile |
| } |
| } |
| return file |
| } |
| |
| inner class ArgumentState { |
| |
| fun validate() { |
| var errorMessage = "" |
| if (!this::goldenLocation.isInitialized) |
| errorMessage += "Missing Golden Directory;" |
| if (!this::clientName.isInitialized) |
| errorMessage += "Missing Client Name;" |
| if (!this::clientVersion.isInitialized) |
| errorMessage += "Missing Client Version;" |
| if (!this::filePath.isInitialized) |
| errorMessage += "Missing File Path;" |
| if (jarLocation != null && extractionDir == null) |
| errorMessage += "Missing Extraction Directory;" |
| if (!recordGoldens && outputLocation == null) |
| errorMessage += "Missing output directory to save diff images;" |
| if (errorMessage != "") { |
| throw InvalidArgumentException(errorMessage) |
| } |
| } |
| |
| var recordGoldens: Boolean = false |
| lateinit var goldenLocation: String |
| var jarLocation: String? = null |
| var extractionDir: String? = null |
| lateinit var rootModule: LintModelModule |
| lateinit var clientVersion: String |
| lateinit var clientName: String |
| lateinit var filePath: String |
| var outputLocation: String? = null |
| var modules: MutableList<LintModelModule> = mutableListOf() |
| } |
| |
| inner class MainLintClient(flags: LintCliFlags, private val argumentState: ArgumentState) : |
| LintCliClient(flags, CLIENT_CLI) { |
| |
| private var unexpectedGradleProject: Project? = null |
| |
| public override fun createDriver( |
| registry: IssueRegistry, request: LintRequest |
| ): LintDriver { |
| val driver: LintDriver = super.createDriver(registry, request) |
| val project: Project? = unexpectedGradleProject |
| if (project != null) { |
| val message = java.lang.String.format( |
| "\"`%1\$s`\" is a Gradle project. To correctly " |
| + "analyze Gradle projects, you should run \"`gradlew lint`\" " |
| + "instead.", |
| project.name |
| ) |
| val location: Location = guessGradleLocation(this, project.dir, null) |
| report( |
| this, IssueRegistry.LINT_ERROR, message, driver, project, location, null |
| ) |
| } |
| return driver |
| } |
| |
| override fun createProject(dir: File, referenceDir: File): Project { |
| val project: Project = super.createProject(dir, referenceDir) |
| if (project.isGradleProject) { |
| // Can't report error yet; stash it here so we can report it after the |
| // driver has been created |
| unexpectedGradleProject = project |
| } |
| return project |
| } |
| |
| override fun getConfiguration( |
| project: Project, driver: LintDriver? |
| ): Configuration { |
| if (project.isGradleProject && project !is LintModelModuleProject) { |
| // Don't report any issues when analyzing a Gradle project from the |
| // non-Gradle runner; they are likely to be false, and will hide the |
| // real problem reported above. We also need to turn off overrides |
| // and fallbacks such that we don't inherit any re-enabled issues etc. |
| val configurations: ConfigurationHierarchy = configurations |
| configurations.overrides = null |
| configurations.fallback = null |
| return object : CliConfiguration(configurations, flags, true) { |
| override fun getDefinedSeverity( |
| issue: Issue, |
| source: Configuration, |
| visibleDefault: Severity |
| ): Severity { |
| return if (issue === IssueRegistry.LINT_ERROR) Severity.FATAL else Severity.IGNORE |
| } |
| |
| override fun isIgnored(context: Context, incident: Incident): Boolean { |
| // If you've deliberately ignored IssueRegistry.LINT_ERROR |
| // don't flag that one either |
| val issue: Issue = incident.issue |
| if ((issue === IssueRegistry.LINT_ERROR |
| && LintCliClient(flags, clientName) |
| .isSuppressed(IssueRegistry.LINT_ERROR)) |
| ) { |
| return true |
| } else if ((issue === IssueRegistry.LINT_WARNING |
| && LintCliClient(flags, clientName) |
| .isSuppressed(IssueRegistry.LINT_WARNING)) |
| ) { |
| return true |
| } |
| return (issue !== IssueRegistry.LINT_ERROR |
| && issue !== IssueRegistry.LINT_WARNING) |
| } |
| } |
| } |
| return super.getConfiguration(project, driver) |
| } |
| |
| private fun readSrcJar(file: File): ByteArray? { |
| val path = file.path |
| val srcJarIndex = path.indexOf("srcjar!") |
| if (srcJarIndex != -1) { |
| val jarFile = File(path.substring(0, srcJarIndex + 6)) |
| if (jarFile.exists()) { |
| try { |
| ZipFile(jarFile).use { zipFile -> |
| val name: String = |
| path.substring(srcJarIndex + 8).replace(File.separatorChar, '/') |
| val entry: ZipEntry? = zipFile.getEntry(name) |
| if (entry != null) { |
| try { |
| zipFile.getInputStream(entry).use { `is` -> |
| return ByteStreams.toByteArray( |
| `is` |
| ) |
| } |
| } catch (e: Exception) { |
| log(e, null) |
| } |
| } |
| } |
| } catch (e: ZipException) { |
| // com.android.tools.lint.Main.this.log(e, "Could not unzip %1$s", jarFile); |
| } catch (e: IOException) { |
| // com.android.tools.lint.Main.this.log(e, "Could not read %1$s", jarFile); |
| } |
| } |
| } |
| return null |
| } |
| |
| override fun readFile(file: File): CharSequence { |
| // .srcjar file handle? |
| val srcJarBytes = readSrcJar(file) |
| return if (srcJarBytes != null) { |
| String(srcJarBytes, Charsets.UTF_8) |
| } else super.readFile(file) |
| } |
| |
| @Throws(IOException::class) |
| override fun readBytes(file: File): ByteArray { |
| // .srcjar file handle? |
| val srcJarBytes = readSrcJar(file) |
| return srcJarBytes ?: super.readBytes(file) |
| } |
| |
| private var metadata: ProjectMetadata? = null |
| override fun configureLintRequest(lintRequest: LintRequest) { |
| super.configureLintRequest(lintRequest) |
| val descriptor: File? = flags.projectDescriptorOverride |
| if (descriptor != null) { |
| metadata = computeMetadata(this, descriptor) |
| val clientName: String? = metadata!!.clientName |
| if (clientName != null) { |
| LintCliClient(clientName) // constructor has side effect |
| } |
| val projects: List<Project> = metadata!!.projects |
| if (projects.isNotEmpty()) { |
| lintRequest.setProjects(projects) |
| if (metadata!!.sdk != null) { |
| sdkHomePath = metadata!!.sdk |
| } |
| if (metadata!!.jdk!= null) { |
| jdkHomePath = metadata!!.jdk |
| } |
| if (metadata!!.baseline != null) { |
| flags.baselineFile = metadata!!.baseline |
| } |
| val scope: EnumSet<Scope> = EnumSet.copyOf(Scope.ALL) |
| if (metadata!!.incomplete) { |
| scope.remove(Scope.ALL_CLASS_FILES) |
| scope.remove(Scope.ALL_JAVA_FILES) |
| scope.remove(Scope.ALL_RESOURCE_FILES) |
| } |
| lintRequest.setScope(scope) |
| lintRequest.setPlatform(metadata!!.platforms) |
| } |
| } |
| } |
| |
| override fun findRuleJars(project: Project): Iterable<File> { |
| if (metadata != null) { |
| val jars: List<File>? = metadata!!.lintChecks[project] |
| if (jars != null) { |
| return jars |
| } |
| } |
| return super.findRuleJars(project) |
| } |
| |
| override fun findGlobalRuleJars(driver: LintDriver?, warnDeprecated: Boolean): List<File> { |
| if (metadata != null) { |
| val jars: List<File> = metadata!!.globalLintChecks |
| if (jars.isNotEmpty()) { |
| return jars |
| } |
| } |
| return super.findGlobalRuleJars(driver, warnDeprecated) |
| } |
| |
| override fun getCacheDir(name: String?, create: Boolean): File? { |
| if (metadata != null) { |
| var dir: File? = metadata!!.cache |
| if (dir != null) { |
| if (name != null) { |
| dir = File(dir, name) |
| } |
| if (create && !dir.exists()) { |
| if (!dir.mkdirs()) { |
| return null |
| } |
| } |
| return dir |
| } |
| } |
| return super.getCacheDir(name, create) |
| } |
| |
| override fun getMergedManifest(project: Project): Document { |
| if (metadata != null) { |
| val manifest: File? = metadata!!.mergedManifests[project] |
| if (manifest != null && manifest.exists()) { |
| try { |
| // We can't call |
| // resolveMergeManifestSources(document, manifestReportFile) |
| // here since we don't have the merging log. |
| return XmlUtils.parseUtfXmlFile(manifest, true) |
| } catch (e: IOException) { |
| log(e, "Could not read/parse %1\$s", manifest) |
| } catch (e: SAXException) { |
| log(e, "Could not read/parse %1\$s", manifest) |
| } |
| } |
| } |
| return super.getMergedManifest(project)!! |
| } |
| |
| override fun getSdkHome(): File? { |
| return if (sdkHomePath != null) { |
| sdkHomePath |
| } else super.getSdkHome() |
| } |
| |
| override fun getJdkHome(project: Project?): File? { |
| return if (jdkHomePath != null) { |
| jdkHomePath |
| } else super.getJdkHome(project) |
| } |
| |
| override fun getBootClassPath(knownProjects: Collection<Project>): Set<File>? = when { |
| metadata == null || metadata!!.jdkBootClasspath.isEmpty() -> |
| super.getBootClassPath(knownProjects) |
| !knownProjects.any(Project::isAndroidProject) -> metadata!!.jdkBootClasspath.toSet() |
| else -> super.getBootClassPath(knownProjects) ?: metadata!!.jdkBootClasspath.toSet() |
| } |
| |
| override fun getExternalAnnotations(projects: Collection<Project>): List<File> { |
| val externalAnnotations: MutableList<File> = super.getExternalAnnotations(projects).toMutableList() |
| if (metadata != null) { |
| externalAnnotations.addAll(metadata!!.externalAnnotations) |
| } |
| return externalAnnotations |
| } |
| } |
| } |