Remove unused lint support (#2834)

If we want it at some point, we can revert this commit.
This commit is contained in:
Jake Wharton
2025-09-24 11:04:47 -04:00
committed by GitHub
parent 5ef4c5ae31
commit 480f9d0a34
32 changed files with 0 additions and 1517 deletions

View File

@@ -33,12 +33,6 @@ public final class app/cash/redwood/gradle/RedwoodGeneratorPlugin$Strategy : jav
public static fun values ()[Lapp/cash/redwood/gradle/RedwoodGeneratorPlugin$Strategy;
}
public final class app/cash/redwood/gradle/RedwoodLintPlugin : org/gradle/api/Plugin {
public fun <init> ()V
public synthetic fun apply (Ljava/lang/Object;)V
public fun apply (Lorg/gradle/api/Project;)V
}
public final class app/cash/redwood/gradle/RedwoodModifiersGeneratorPlugin : app/cash/redwood/gradle/RedwoodGeneratorPlugin {
public fun <init> ()V
}

View File

@@ -46,12 +46,6 @@ dependencies {
gradlePlugin {
plugins {
redwoodLint {
id = "app.cash.redwood.lint"
displayName = "Redwood lint"
description = "Redwood lint Gradle plugin"
implementationClass = "app.cash.redwood.gradle.RedwoodLintPlugin"
}
redwoodSchema {
id = "app.cash.redwood.schema"
displayName = "Redwood schema"
@@ -125,7 +119,6 @@ test {
dependsOn(':redwood-runtime:publishAllPublicationsToLocalMavenRepository')
dependsOn(':redwood-schema:publishAllPublicationsToLocalMavenRepository')
dependsOn(':redwood-tooling-codegen:publishAllPublicationsToLocalMavenRepository')
dependsOn(':redwood-tooling-lint:publishAllPublicationsToLocalMavenRepository')
dependsOn(':redwood-tooling-schema:publishAllPublicationsToLocalMavenRepository')
dependsOn(':redwood-ui-core-api:publishAllPublicationsToLocalMavenRepository')
dependsOn(':redwood-ui-core-modifiers:publishAllPublicationsToLocalMavenRepository')

View File

@@ -1,235 +0,0 @@
/*
* Copyright (C) 2022 Square, Inc.
*
* 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 app.cash.redwood.gradle
import com.android.build.gradle.AppExtension
import com.android.build.gradle.LibraryExtension
import java.io.File
import java.util.Locale.ROOT
import org.gradle.api.NamedDomainObjectProvider
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.artifacts.Configuration
import org.gradle.api.attributes.Usage
import org.gradle.api.attributes.Usage.JAVA_RUNTIME
import org.gradle.api.attributes.Usage.USAGE_ATTRIBUTE
import org.gradle.api.tasks.TaskProvider
import org.gradle.language.base.plugins.LifecycleBasePlugin.CHECK_TASK_NAME
import org.gradle.language.base.plugins.LifecycleBasePlugin.VERIFICATION_GROUP
import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation.Companion.MAIN_COMPILATION_NAME
import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType.androidJvm
import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType.common
import org.jetbrains.kotlin.gradle.plugin.KotlinTarget
private const val BASE_TASK_NAME = "redwoodLint"
@Suppress("unused") // Invoked reflectively by Gradle.
public class RedwoodLintPlugin : Plugin<Project> {
override fun apply(project: Project) {
val configuration = project.configurations.register("redwoodToolingLint")
project.afterEvaluate {
val androidPlugin = if (project.plugins.hasPlugin("com.android.application")) {
AndroidPlugin.Application
} else if (project.plugins.hasPlugin("com.android.library")) {
AndroidPlugin.Library
} else {
null
}
val task = if (project.plugins.hasPlugin("org.jetbrains.kotlin.multiplatform")) {
val rootTask = project.tasks.register(BASE_TASK_NAME) {
it.group = VERIFICATION_GROUP
it.description = taskDescription("all Kotlin targets")
}
if (androidPlugin != null) {
configureKotlinMultiplatformTargets(project, configuration, rootTask, skipAndroid = true)
configureKotlinAndroidVariants(project, configuration, rootTask, androidPlugin, prefix = true)
} else {
configureKotlinMultiplatformTargets(project, configuration, rootTask)
}
rootTask
} else if (project.plugins.hasPlugin("org.jetbrains.kotlin.jvm")) {
configureKotlinJvmProject(project, configuration)
} else if (project.plugins.hasPlugin("org.jetbrains.kotlin.android")) {
checkNotNull(androidPlugin) {
"Kotlin Android plugin requires either Android application or library plugin"
}
val rootTask = project.tasks.register(BASE_TASK_NAME) {
it.group = VERIFICATION_GROUP
it.description = taskDescription("all Kotlin targets")
}
configureKotlinAndroidVariants(
project,
configuration,
rootTask,
androidPlugin,
prefix = false,
)
rootTask
} else {
val name = if (project.path == ":") {
"root project"
} else {
"project ${project.path}"
}
throw IllegalStateException(
"'app.cash.redwood.lint' requires a compatible Kotlin plugin to be applied ($name)",
)
}
project.tasks.named(CHECK_TASK_NAME).configure {
it.dependsOn(task)
}
}
}
}
private enum class AndroidPlugin {
Application,
Library,
}
private fun configureKotlinAndroidVariants(
project: Project,
configuration: NamedDomainObjectProvider<Configuration>,
rootTask: TaskProvider<Task>,
android: AndroidPlugin,
prefix: Boolean,
) {
val extensions = project.extensions
val variants = when (android) {
AndroidPlugin.Application -> extensions.getByType(AppExtension::class.java).applicationVariants
AndroidPlugin.Library -> extensions.getByType(LibraryExtension::class.java).libraryVariants
}
variants.configureEach { variant ->
val taskName = buildString {
append(BASE_TASK_NAME)
if (prefix) {
append("Android")
}
append(variant.name.replaceFirstChar { it.titlecase(ROOT) })
}
val task = project.createRedwoodLintTask(
configuration,
taskName,
"Kotlin Android ${variant.name} variant",
sourceDirs = { variant.sourceSets.flatMap { it.kotlinDirectories } },
classpath = { variant.compileConfiguration },
)
rootTask.configure {
it.dependsOn(task)
}
}
}
private fun configureKotlinMultiplatformTargets(
project: Project,
configuration: NamedDomainObjectProvider<Configuration>,
rootTask: TaskProvider<Task>,
skipAndroid: Boolean = false,
) {
val kotlin = project.extensions.getByType(KotlinMultiplatformExtension::class.java)
kotlin.targets.configureEach { target ->
if (target.platformType == common) {
return@configureEach // All code ends up in platform targets.
}
if (target.platformType == androidJvm) {
if (skipAndroid) return@configureEach
throw AssertionError("Found Android Kotlin target but no Android plugin was detected")
}
val task = createKotlinTargetRedwoodLintTask(
project,
configuration,
target,
taskName = BASE_TASK_NAME + target.name.replaceFirstChar { it.titlecase(ROOT) },
)
rootTask.configure {
it.dependsOn(task)
}
}
}
private fun configureKotlinJvmProject(
project: Project,
configuration: NamedDomainObjectProvider<Configuration>,
): TaskProvider<out Task> {
val kotlin = project.extensions.getByType(KotlinJvmProjectExtension::class.java)
return createKotlinTargetRedwoodLintTask(
project,
configuration,
kotlin.target,
BASE_TASK_NAME,
)
}
private fun createKotlinTargetRedwoodLintTask(
project: Project,
configuration: NamedDomainObjectProvider<Configuration>,
target: KotlinTarget,
taskName: String,
): TaskProvider<out Task> {
val compilation = target.compilations.getByName(MAIN_COMPILATION_NAME)
return project.createRedwoodLintTask(
configuration,
taskName,
"Kotlin ${target.name} target",
sourceDirs = {
compilation.allKotlinSourceSets.flatMap { it.kotlin.sourceDirectories.files }
},
classpath = {
project.configurations.getByName(compilation.compileDependencyConfigurationName)
},
)
}
private fun Project.createRedwoodLintTask(
configuration: NamedDomainObjectProvider<Configuration>,
name: String,
descriptionTarget: String? = null,
sourceDirs: () -> Collection<File>,
classpath: () -> Configuration,
): TaskProvider<out Task> {
dependencies.add(configuration.name, project.redwoodDependency("redwood-tooling-lint"))
return tasks.register(name, RedwoodLintTask::class.java) { task ->
task.group = VERIFICATION_GROUP
task.description = taskDescription(descriptionTarget)
task.toolClasspath.setFrom(configuration.get().incoming.artifacts.artifactFiles)
task.projectDirectoryPath.set(project.projectDir.absolutePath)
task.sourceDirectories.set(sourceDirs())
task.classpath.setFrom(
classpath().incoming.artifactView {
it.attributes {
it.attribute(USAGE_ATTRIBUTE, objects.named(Usage::class.java, JAVA_RUNTIME))
}
}.artifacts.artifactFiles,
)
}
}
private fun taskDescription(target: String? = null) = buildString {
append("Run Redwood's Compose lint checks")
if (target != null) {
append(" on ")
append(target)
}
}

View File

@@ -1,93 +0,0 @@
/*
* Copyright (C) 2022 Square, Inc.
*
* 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 app.cash.redwood.gradle
import java.io.File
import javax.inject.Inject
import org.gradle.api.DefaultTask
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.CacheableTask
import org.gradle.api.tasks.Classpath
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.TaskAction
import org.gradle.process.ExecOperations
import org.gradle.workers.WorkAction
import org.gradle.workers.WorkParameters
import org.gradle.workers.WorkerExecutor
@CacheableTask
internal abstract class RedwoodLintTask @Inject constructor(
private val workerExecutor: WorkerExecutor,
) : DefaultTask() {
@get:Classpath
abstract val toolClasspath: ConfigurableFileCollection
@get:Input
abstract val projectDirectoryPath: Property<String>
@get:Input
abstract val sourceDirectories: ListProperty<File>
@get:Classpath
abstract val classpath: ConfigurableFileCollection
@TaskAction
fun run() {
val queue = workerExecutor.noIsolation()
queue.submit(RedwoodLintWorker::class.java) {
it.toolClasspath.from(toolClasspath)
it.projectDirectoryPath.set(projectDirectoryPath)
it.sourceDirectories.set(sourceDirectories)
it.classpath.setFrom(classpath)
}
}
}
private interface RedwoodLintParameters : WorkParameters {
val toolClasspath: ConfigurableFileCollection
val projectDirectoryPath: Property<String>
val sourceDirectories: ListProperty<File>
val classpath: ConfigurableFileCollection
}
private abstract class RedwoodLintWorker @Inject constructor(
private val execOperations: ExecOperations,
) : WorkAction<RedwoodLintParameters> {
override fun execute() {
execOperations.javaexec { exec ->
exec.classpath = parameters.toolClasspath
exec.mainClass.set("app.cash.redwood.tooling.lint.Main")
exec.args = mutableListOf<String>().apply {
add("check")
add(parameters.projectDirectoryPath.get())
for (file in parameters.sourceDirectories.get()) {
add("--sources")
add(file.toString())
}
val files = parameters.classpath.files
if (files.isNotEmpty()) {
add("--class-path")
add(files.joinToString(File.pathSeparator))
}
}
}
}
}

View File

@@ -1,23 +0,0 @@
buildscript {
dependencies {
classpath "app.cash.redwood:redwood-gradle-plugin:$redwoodVersion"
classpath libs.kotlin.gradlePlugin
classpath libs.androidGradlePlugin
}
repositories {
maven {
url "file://${rootDir.absolutePath}/../../../../../build/localMaven"
}
mavenCentral()
google()
}
}
apply plugin: 'com.android.library'
apply plugin: 'app.cash.redwood.lint'
android {
namespace = 'example.android'
compileSdk 33
}

View File

@@ -1 +0,0 @@
kotlin.mpp.stability.nowarn=true

View File

@@ -1,7 +0,0 @@
dependencyResolutionManagement {
versionCatalogs {
libs {
from(files('../../../../../gradle/libs.versions.toml'))
}
}
}

View File

@@ -1,32 +0,0 @@
buildscript {
dependencies {
classpath "app.cash.redwood:redwood-gradle-plugin:$redwoodVersion"
classpath libs.kotlin.gradlePlugin
classpath libs.androidGradlePlugin
}
repositories {
maven {
url "file://${rootDir.absolutePath}/../../../../../build/localMaven"
}
mavenCentral()
google()
}
}
apply plugin: 'com.android.library'
apply plugin: 'org.jetbrains.kotlin.android'
apply plugin: 'app.cash.redwood.lint'
android {
namespace = 'example.android'
compileSdk 33
}
repositories {
maven {
url "file://${rootDir.absolutePath}/../../../../../build/localMaven"
}
mavenCentral()
google()
}

View File

@@ -1 +0,0 @@
kotlin.mpp.stability.nowarn=true

View File

@@ -1,7 +0,0 @@
dependencyResolutionManagement {
versionCatalogs {
libs {
from(files('../../../../../gradle/libs.versions.toml'))
}
}
}

View File

@@ -1,24 +0,0 @@
buildscript {
dependencies {
classpath "app.cash.redwood:redwood-gradle-plugin:$redwoodVersion"
classpath libs.kotlin.gradlePlugin
}
repositories {
maven {
url "file://${rootDir.absolutePath}/../../../../../build/localMaven"
}
mavenCentral()
}
}
apply plugin: 'org.jetbrains.kotlin.jvm'
apply plugin: 'app.cash.redwood.lint'
repositories {
maven {
url "file://${rootDir.absolutePath}/../../../../../build/localMaven"
}
mavenCentral()
google()
}

View File

@@ -1 +0,0 @@
kotlin.mpp.stability.nowarn=true

View File

@@ -1,7 +0,0 @@
dependencyResolutionManagement {
versionCatalogs {
libs {
from(files('../../../../../gradle/libs.versions.toml'))
}
}
}

View File

@@ -1,37 +0,0 @@
buildscript {
dependencies {
classpath "app.cash.redwood:redwood-gradle-plugin:$redwoodVersion"
classpath libs.kotlin.gradlePlugin
classpath libs.androidGradlePlugin
}
repositories {
maven {
url "file://${rootDir.absolutePath}/../../../../../build/localMaven"
}
mavenCentral()
google()
}
}
apply plugin: 'org.jetbrains.kotlin.multiplatform'
apply plugin: 'com.android.library'
apply plugin: 'app.cash.redwood.lint'
kotlin {
androidTarget()
jvm()
}
android {
namespace = 'example.android'
compileSdk 33
}
repositories {
maven {
url "file://${rootDir.absolutePath}/../../../../../build/localMaven"
}
mavenCentral()
google()
}

View File

@@ -1 +0,0 @@
kotlin.mpp.stability.nowarn=true

View File

@@ -1,7 +0,0 @@
dependencyResolutionManagement {
versionCatalogs {
libs {
from(files('../../../../../gradle/libs.versions.toml'))
}
}
}

View File

@@ -1,29 +0,0 @@
buildscript {
dependencies {
classpath "app.cash.redwood:redwood-gradle-plugin:$redwoodVersion"
classpath libs.kotlin.gradlePlugin
}
repositories {
maven {
url "file://${rootDir.absolutePath}/../../../../../build/localMaven"
}
mavenCentral()
}
}
apply plugin: 'org.jetbrains.kotlin.multiplatform'
apply plugin: 'app.cash.redwood.lint'
kotlin {
jvm()
js()
}
repositories {
maven {
url "file://${rootDir.absolutePath}/../../../../../build/localMaven"
}
mavenCentral()
google()
}

View File

@@ -1 +0,0 @@
kotlin.mpp.stability.nowarn=true

View File

@@ -1,7 +0,0 @@
dependencyResolutionManagement {
versionCatalogs {
libs {
from(files('../../../../../gradle/libs.versions.toml'))
}
}
}

View File

@@ -1,14 +0,0 @@
buildscript {
dependencies {
classpath "app.cash.redwood:redwood-gradle-plugin:$redwoodVersion"
}
repositories {
maven {
url "file://${rootDir.absolutePath}/../../../../../build/localMaven"
}
mavenCentral()
}
}
apply plugin: 'app.cash.redwood.lint'

View File

@@ -1,7 +0,0 @@
dependencyResolutionManagement {
versionCatalogs {
libs {
from(files('../../../../../gradle/libs.versions.toml'))
}
}
}

View File

@@ -17,7 +17,6 @@ package app.cash.redwood.gradle
import assertk.assertThat
import assertk.assertions.contains
import assertk.assertions.containsExactly
import assertk.assertions.isEqualTo
import assertk.assertions.isNotEmpty
import assertk.assertions.prop
@@ -104,65 +103,6 @@ class FixtureTest {
fixtureGradleRunner(fixtureDir, "redwoodApiCheck").build()
}
@Test fun lintNoKotlinFails() {
val fixtureDir = File("src/test/fixture/lint-no-kotlin")
val result = fixtureGradleRunner(fixtureDir).buildAndFail()
assertThat(result.output).contains(
"'app.cash.redwood.lint' requires a compatible Kotlin plugin to be applied (root project)",
)
}
@Test fun lintAndroidNoKotlinFails() {
val fixtureDir = File("src/test/fixture/lint-android-no-kotlin")
val result = fixtureGradleRunner(fixtureDir).buildAndFail()
assertThat(result.output).contains(
"'app.cash.redwood.lint' requires a compatible Kotlin plugin to be applied (root project)",
)
}
@Test fun lintAndroid() {
val fixtureDir = File("src/test/fixture/lint-android")
val result = fixtureGradleRunner(fixtureDir).build()
val lintTasks = result.tasks.map { it.path }.filter { it.startsWith(":redwoodLint") }
assertThat(lintTasks).containsExactly(
":redwoodLintDebug",
":redwoodLintRelease",
":redwoodLint",
)
}
@Test fun lintJvm() {
val fixtureDir = File("src/test/fixture/lint-jvm")
val result = fixtureGradleRunner(fixtureDir).build()
val lintTasks = result.tasks.map { it.path }.filter { it.startsWith(":redwoodLint") }
assertThat(lintTasks).containsExactly(
":redwoodLint",
)
}
@Test fun lintMppAndroid() {
val fixtureDir = File("src/test/fixture/lint-mpp-android")
val result = fixtureGradleRunner(fixtureDir).build()
val lintTasks = result.tasks.map { it.path }.filter { it.startsWith(":redwoodLint") }
assertThat(lintTasks).containsExactly(
":redwoodLintAndroidDebug",
":redwoodLintAndroidRelease",
":redwoodLintJvm",
":redwoodLint",
)
}
@Test fun lintMppNoAndroid() {
val fixtureDir = File("src/test/fixture/lint-mpp-no-android")
val result = fixtureGradleRunner(fixtureDir).build()
val lintTasks = result.tasks.map { it.path }.filter { it.startsWith(":redwoodLint") }
assertThat(lintTasks).containsExactly(
":redwoodLintJs",
":redwoodLintJvm",
":redwoodLint",
)
}
@Test fun protocolWithoutModifiers() {
// When no layout modifier are present, the serialization codegen contains unused things.
// Ensure that it does not produce warnings (which are set to error in this build).

View File

@@ -1,11 +0,0 @@
public final class app/cash/redwood/tooling/lint/ApiMerger {
public fun <init> ()V
public final fun add (Ljava/lang/String;)Lapp/cash/redwood/tooling/lint/ApiMerger;
public final fun merge ()Ljava/lang/String;
public final synthetic fun plusAssign (Ljava/lang/String;)V
}
public final class app/cash/redwood/tooling/lint/Main {
public static final fun main ([Ljava/lang/String;)V
}

View File

@@ -1,35 +0,0 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile
import static app.cash.redwood.buildsupport.TargetGroup.Tooling
redwoodBuild {
targets(Tooling)
publishing()
cliApplication('redwood-lint', 'app.cash.redwood.tooling.lint.Main')
}
apply plugin: 'org.jetbrains.kotlin.plugin.serialization'
dependencies {
implementation libs.clikt
implementation libs.kotlinx.serialization.core
implementation libs.xmlutil.serialization
implementation libs.lint.core
testImplementation libs.kotlin.test
testImplementation libs.junit
testImplementation libs.assertk
testImplementation libs.jimfs
}
tasks.withType(JavaCompile).configureEach {
sourceCompatibility = JavaVersion.VERSION_11.toString()
targetCompatibility = JavaVersion.VERSION_11.toString()
}
tasks.withType(KotlinJvmCompile).configureEach {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_11)
}
}

View File

@@ -1,50 +0,0 @@
/*
* Copyright (C) 2022 Square, Inc.
*
* 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 app.cash.redwood.tooling.lint
import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.parameters.arguments.argument
import com.github.ajalt.clikt.parameters.arguments.help
import com.github.ajalt.clikt.parameters.arguments.multiple
import com.github.ajalt.clikt.parameters.options.help
import com.github.ajalt.clikt.parameters.options.option
import com.github.ajalt.clikt.parameters.options.required
import com.github.ajalt.clikt.parameters.types.path
import java.nio.file.FileSystem
import kotlin.io.path.readText
import kotlin.io.path.writeText
internal class ApiMergeCommand(
fileSystem: FileSystem,
) : CliktCommand(name = "api-merge") {
private val inputs by argument("FILE")
.help("One or more API definition XML documents")
.path(fileSystem = fileSystem)
.multiple(required = true)
private val output by option("-o", "--out", metavar = "FILE")
.help("Output file for the merged API definition XML document")
.path(fileSystem = fileSystem)
.required()
override fun run() {
val merger = ApiMerger()
for (input in inputs) {
merger += input.readText()
}
output.writeText(merger.merge())
}
}

View File

@@ -1,112 +0,0 @@
/*
* Copyright (C) 2022 Square, Inc.
*
* 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 app.cash.redwood.tooling.lint
/**
* Merges one or more API definitions into a single, merged API definition.
*
* Each API definition is an XML document in the following form:
* ```xml
* <api version="1">
* <widget tag="1" since="1">
* <trait tag="1" since="1"/>
* <trait tag="3" since="1"/>
* </widget>
* <layout-modifier tag="1" since="1">
* <property tag="1" since="1"/>
* <property tag="3" since="1"/>
* </layout-modifier>
* </api>
* ```
*/
public class ApiMerger {
private val apis = mutableListOf<RedwoodApi>()
/** Parse [xml] and add its API definition for merging. */
public fun add(xml: String): ApiMerger = apply {
val lintApi = RedwoodApi.deserialize(xml)
require(lintApi.version == 1U) {
"Only Redwood API XML version 1 is supported. Found: ${lintApi.version}"
}
apis += lintApi
}
@JvmSynthetic // Hide to Java callers.
public operator fun plusAssign(xml: String) {
add(xml)
}
/** Merge added API definitions into a single API definition XML. */
public fun merge(): String {
check(apis.isNotEmpty()) {
"One or more API definitions must be added in order to merge"
}
return apis.merge().serialize()
}
private companion object {
private fun List<RedwoodApi>.merge(): RedwoodApi {
return RedwoodApi(
version = 1U,
widgets = mergeItems({ it.widgets }, { it.tag }, { it.merge() }),
modifier = mergeItems({ it.modifier }, { it.tag }, { it.merge() }),
)
}
private fun List<RedwoodWidget>.merge(): RedwoodWidget {
return RedwoodWidget(
tag = first().tag,
since = maxOf { it.since },
traits = mergeItems({ it.traits }, { it.tag }, { it.merge() }),
)
}
private fun List<RedwoodWidgetTrait>.merge(): RedwoodWidgetTrait {
return RedwoodWidgetTrait(
tag = first().tag,
since = maxOf { it.since },
)
}
private fun List<RedwoodModifier>.merge(): RedwoodModifier {
return RedwoodModifier(
tag = first().tag,
since = maxOf { it.since },
properties = mergeItems({ it.properties }, { it.tag }, { it.merge() }),
)
}
private fun List<RedwoodModifierProperty>.merge(): RedwoodModifierProperty {
return RedwoodModifierProperty(
tag = first().tag,
since = maxOf { it.since },
)
}
private fun <T, I, G : Comparable<G>> List<T>.mergeItems(
itemSelector: (T) -> List<I>,
groupSelector: (I) -> G,
itemMerge: (List<I>) -> I,
): List<I> {
return flatMap(itemSelector)
.groupBy(groupSelector)
.values
.filter { it.size == size }
.map(itemMerge)
.sortedBy(groupSelector)
}
}
}

View File

@@ -1,204 +0,0 @@
/*
* Copyright (C) 2022 Square, Inc.
*
* 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.
*/
@file:JvmName("Main")
@file:Suppress("UnstableApiUsage") // Lint 🙄
package app.cash.redwood.tooling.lint
import com.android.tools.lint.LintResourceRepository.Companion.EmptyRepository
import com.android.tools.lint.LintStats
import com.android.tools.lint.UastEnvironment
import com.android.tools.lint.client.api.IssueRegistry
import com.android.tools.lint.client.api.LintClient
import com.android.tools.lint.client.api.LintDriver
import com.android.tools.lint.client.api.LintRequest
import com.android.tools.lint.client.api.ResourceRepositoryScope
import com.android.tools.lint.detector.api.CURRENT_API
import com.android.tools.lint.detector.api.Context
import com.android.tools.lint.detector.api.Desugaring
import com.android.tools.lint.detector.api.Incident
import com.android.tools.lint.detector.api.Issue
import com.android.tools.lint.detector.api.JavaContext
import com.android.tools.lint.detector.api.Platform
import com.android.tools.lint.detector.api.Project
import com.android.tools.lint.detector.api.Scope.Companion.JAVA_FILE_SCOPE
import com.android.tools.lint.detector.api.Severity
import com.android.tools.lint.detector.api.Severity.ERROR
import com.android.tools.lint.detector.api.TextFormat
import com.android.tools.lint.helpers.DefaultUastParser
import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.ProgramResult
import com.github.ajalt.clikt.parameters.arguments.argument
import com.github.ajalt.clikt.parameters.options.default
import com.github.ajalt.clikt.parameters.options.multiple
import com.github.ajalt.clikt.parameters.options.option
import com.github.ajalt.clikt.parameters.options.split
import com.github.ajalt.clikt.parameters.types.file
import com.intellij.pom.java.LanguageLevel
import com.intellij.pom.java.LanguageLevel.JDK_1_7
import java.io.File
import org.jetbrains.kotlin.cli.jvm.config.addJvmClasspathRoots
import org.jetbrains.kotlin.config.JVMConfigurationKeys
import org.jetbrains.kotlin.config.LanguageVersionSettings
import org.jetbrains.kotlin.config.languageVersionSettings
import org.jetbrains.kotlin.utils.PathUtil.getJdkClassesRootsFromCurrentJre
internal class LintCommand : CliktCommand(name = "check") {
private val projectDirectory by argument("PROJECT_DIR")
.file()
private val sourceDirectories by option("-s", "--sources", metavar = "DIR")
.file()
.multiple(required = true)
private val classpath by option("-cp", "--class-path")
.file()
.split(File.pathSeparator)
.default(emptyList())
override fun run() {
val uastConfig = UastEnvironment.Configuration.create().apply {
javaLanguageLevel = JDK_1_7
// UAST warns when you give it a directory that does not exist.
addSourceRoots(sourceDirectories.filter(File::exists))
addClasspathRoots(classpath)
kotlinCompilerConfig.addJvmClasspathRoots(getJdkClassesRootsFromCurrentJre())
kotlinCompilerConfig.put(JVMConfigurationKeys.NO_JDK, false)
}
val uastEnvironment = UastEnvironment.create(uastConfig)
val ideaProject = uastEnvironment.ideaProject
val sourceFiles = sourceDirectories.flatMap {
it.walk().filter(File::isFile)
}
val client = RedwoodLintClient(ideaProject, uastEnvironment, projectDirectory, sourceFiles)
val incidents = client.run()
val stats = LintStats.create(incidents, emptyList())
if (stats.errorCount > 0) {
for (incident in incidents) {
if (incident.severity.isError) {
System.err.println(incident)
}
}
throw ProgramResult(1)
}
}
}
private class RedwoodIssueRegistry : IssueRegistry() {
override val api get() = CURRENT_API
override val issues get() = emptyList<Issue>()
}
private class RedwoodProject(
client: LintClient,
projectDir: File,
private val sources: List<File>,
) : Project(client, projectDir, null) {
init {
library = true
directLibraries = emptyList()
}
override fun initialize() {
// Not calling super as it is for Android projects only.
}
override fun getJavaSourceFolders() = sources
override fun getSubset() = null // Check whole project.
override fun isGradleProject() = false
override fun isAndroidProject() = false
override fun getDesugaring() = Desugaring.NONE
override fun getManifestFiles() = emptyList<File>()
override fun getProguardFiles() = emptyList<File>()
override fun getResourceFolders() = emptyList<File>()
override fun getAssetFolders() = emptyList<File>()
override fun getGeneratedSourceFolders() = emptyList<File>()
override fun getGeneratedResourceFolders() = emptyList<File>()
override fun getTestSourceFolders() = emptyList<File>()
override fun getTestLibraries() = emptyList<File>()
override fun getTestFixturesSourceFolders() = emptyList<File>()
override fun getTestFixturesLibraries() = emptyList<File>()
override fun getPropertyFiles() = emptyList<File>()
override fun getGradleBuildScripts() = emptyList<File>()
override fun getJavaClassFolders() = emptyList<File>()
override fun getJavaLibraries(includeProvided: Boolean) = emptyList<File>()
}
private class RedwoodLintClient(
private val ideaProject: com.intellij.openapi.project.Project,
private val uastEnvironment: UastEnvironment,
private val projectDir: File,
private val sourceFiles: List<File>,
) : LintClient("redwood-lint") {
private var run = false
private val incidents = mutableListOf<Incident>()
fun run(): List<Incident> {
check(!run) { "run() can only be called once per client instance" }
run = true
val request = LintRequest(this, sourceFiles)
request.setProjects(listOf(RedwoodProject(this, projectDir, sourceFiles)))
request.setScope(JAVA_FILE_SCOPE)
request.setPlatform(Platform.UNSPECIFIED)
request.setReleaseMode(false)
val driver = LintDriver(RedwoodIssueRegistry(), this, request)
driver.analyze()
return incidents
}
override fun readFile(file: File) = file.readText()
override val xmlParser get() = throw UnsupportedOperationException()
override fun getGradleVisitor() = throw UnsupportedOperationException()
override fun getResources(project: Project, scope: ResourceRepositoryScope) = EmptyRepository
override fun getUastParser(project: Project?) = object : DefaultUastParser(project, ideaProject) {
override fun prepare(
contexts: List<JavaContext>,
javaLanguageLevel: LanguageLevel?,
kotlinLanguageLevel: LanguageVersionSettings?,
): Boolean {
if (kotlinLanguageLevel != null) {
uastEnvironment.kotlinCompilerConfig.languageVersionSettings = kotlinLanguageLevel
}
val kotlinFiles = contexts.map(JavaContext::file).filter { it.path.endsWith(".kt") }
uastEnvironment.analyzeFiles(kotlinFiles)
return super.prepare(contexts, javaLanguageLevel, kotlinLanguageLevel)
}
}
override fun report(
context: Context,
incident: Incident,
format: TextFormat,
) {
incidents += incident
}
override fun log(
severity: Severity,
exception: Throwable?,
format: String?,
vararg args: Any,
) {
val ps = if (severity == ERROR) System.err else System.out
ps.println("$severity ${format?.format(*args) ?: ""}")
}
}

View File

@@ -1,97 +0,0 @@
/*
* Copyright (C) 2022 Square, Inc.
*
* 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 app.cash.redwood.tooling.lint
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import nl.adaptivity.xmlutil.serialization.XML
@Serializable
@SerialName("api")
internal data class RedwoodApi(
val version: UInt,
val widgets: List<RedwoodWidget> = emptyList(),
val modifier: List<RedwoodModifier> = emptyList(),
) {
fun serialize() = xml.encodeToString(serializer(), this)
companion object {
private val xml = XML {
indent = 2
}
@JvmStatic
fun deserialize(string: String) = xml.decodeFromString(serializer(), string)
}
}
@Serializable
@SerialName("widget")
internal data class RedwoodWidget(
val tag: Int,
val since: UInt,
val traits: List<RedwoodWidgetTrait> = emptyList(),
) {
init {
val badTraits = traits.filter { it.since < since }
require(badTraits.isEmpty()) {
buildString {
appendLine("Trait since values must be greater than or equal to widget")
appendLine(""" Widget tag="$tag" since="$since"""")
for (badTrait in badTraits) {
appendLine(""" Trait tag="${badTrait.tag}" since="${badTrait.since}"""")
}
deleteCharAt(lastIndex) // Remove trailing newline.
}
}
}
}
@Serializable
@SerialName("trait")
internal data class RedwoodWidgetTrait(
val tag: Int,
val since: UInt,
)
@Serializable
@SerialName("layout-modifier")
internal data class RedwoodModifier(
val tag: Int,
val since: UInt,
val properties: List<RedwoodModifierProperty> = emptyList(),
) {
init {
val badProperties = properties.filter { it.since < since }
require(badProperties.isEmpty()) {
buildString {
appendLine("Property since values must be greater than or equal to layout modifier")
appendLine(""" Layout modifier tag="$tag" since="$since"""")
for (badProperty in badProperties) {
appendLine(""" Property tag="${badProperty.tag}" since="${badProperty.since}"""")
}
deleteCharAt(lastIndex) // Remove trailing newline.
}
}
}
}
@Serializable
@SerialName("property")
internal data class RedwoodModifierProperty(
val tag: Int,
val since: UInt,
)

View File

@@ -1,32 +0,0 @@
/*
* Copyright (C) 2023 Square, Inc.
*
* 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.
*/
@file:JvmName("Main")
package app.cash.redwood.tooling.lint
import com.github.ajalt.clikt.core.NoOpCliktCommand
import com.github.ajalt.clikt.core.main
import com.github.ajalt.clikt.core.subcommands
import java.nio.file.FileSystems
public fun main(vararg args: String) {
NoOpCliktCommand(name = "redwood-lint")
.subcommands(
ApiMergeCommand(FileSystems.getDefault()),
LintCommand(),
)
.main(args)
}

View File

@@ -1,71 +0,0 @@
/*
* Copyright (C) 2022 Square, Inc.
*
* 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 app.cash.redwood.tooling.lint
import assertk.assertThat
import assertk.assertions.isEqualTo
import com.github.ajalt.clikt.core.main
import com.google.common.jimfs.Configuration.unix
import com.google.common.jimfs.Jimfs
import kotlin.io.path.readText
import kotlin.io.path.writeText
import org.junit.Test
class ApiMergeCommandTest {
@Test fun mergesInputs() {
val fs = Jimfs.newFileSystem(unix())
val root = fs.rootDirectories.single()
val input1 = root.resolve("input1.xml")
input1.writeText(
"""
|<api version="1">
| <widget tag="1" since="2">
| <trait tag="1" since="2" />
| </widget>
|</api>
|
""".trimMargin(),
)
val input2 = root.resolve("input2.xml")
input2.writeText(
"""
|<api version="1">
| <widget tag="1" since="2">
| <trait tag="1" since="3" />
| </widget>
|</api>
|
""".trimMargin(),
)
val output = root.resolve("output.xml")
val command = ApiMergeCommand(fs)
command.main(listOf("--out", output.toString(), input1.toString(), input2.toString()))
assertThat(output.readText()).isEqualTo(
"""
|<api version="1">
| <widget tag="1" since="2">
| <trait tag="1" since="3" />
| </widget>
|</api>
""".trimMargin(),
)
}
}

View File

@@ -1,297 +0,0 @@
/*
* Copyright (C) 2022 Square, Inc.
*
* 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 app.cash.redwood.tooling.lint
import assertk.assertThat
import assertk.assertions.hasMessage
import assertk.assertions.isEqualTo
import kotlin.test.assertFailsWith
import org.junit.Test
class ApiMergerTest {
@Test fun requiresAtLeastOne() {
val merger = ApiMerger()
val t = assertFailsWith<IllegalStateException> {
merger.merge()
}
assertThat(t).hasMessage("One or more API definitions must be added in order to merge")
}
@Test fun version1Only() {
val merger = ApiMerger()
val t = assertFailsWith<IllegalArgumentException> {
merger.add("""<api version="2" />""")
}
assertThat(t).hasMessage("Only Redwood API XML version 1 is supported. Found: 2")
}
@Test fun widgetTakesHigherSince() {
val merger = ApiMerger()
merger += """
|<api version="1">
| <widget tag="1" since="2" />
|</api>
|
""".trimMargin()
merger += """
|<api version="1">
| <widget tag="1" since="3" />
|</api>
|
""".trimMargin()
val actual = merger.merge()
assertThat(actual).isEqualTo(
"""
|<api version="1">
| <widget tag="1" since="3" />
|</api>
""".trimMargin(),
)
}
@Test fun widgetRemovedIfNotPresentInAll() {
val merger = ApiMerger()
merger += """
|<api version="1">
| <widget tag="1" since="2" />
|</api>
|
""".trimMargin()
merger += """
|<api version="1" />
|
""".trimMargin()
val actual = merger.merge()
assertThat(actual).isEqualTo(
"""
|<api version="1" />
""".trimMargin(),
)
}
@Test fun widgetTraitTakesHigherSince() {
val merger = ApiMerger()
merger += """
|<api version="1">
| <widget tag="1" since="2">
| <trait tag="1" since="2" />
| </widget>
|</api>
|
""".trimMargin()
merger += """
|<api version="1">
| <widget tag="1" since="2">
| <trait tag="1" since="3" />
| </widget>
|</api>
|
""".trimMargin()
val actual = merger.merge()
assertThat(actual).isEqualTo(
"""
|<api version="1">
| <widget tag="1" since="2">
| <trait tag="1" since="3" />
| </widget>
|</api>
""".trimMargin(),
)
}
@Test fun widgetTraitRemovedIfNotPresentInAll() {
val merger = ApiMerger()
merger += """
|<api version="1">
| <widget tag="1" since="2">
| <trait tag="1" since="2" />
| </widget>
|</api>
|
""".trimMargin()
merger += """
|<api version="1">
| <widget tag="1" since="2" />
|</api>
|
""".trimMargin()
val actual = merger.merge()
assertThat(actual).isEqualTo(
"""
|<api version="1">
| <widget tag="1" since="2" />
|</api>
""".trimMargin(),
)
}
@Test fun modifierTakesHigherSince() {
val merger = ApiMerger()
merger += """
|<api version="1">
| <layout-modifier tag="1" since="2" />
|</api>
|
""".trimMargin()
merger += """
|<api version="1">
| <layout-modifier tag="1" since="3" />
|</api>
|
""".trimMargin()
val actual = merger.merge()
assertThat(actual).isEqualTo(
"""
|<api version="1">
| <layout-modifier tag="1" since="3" />
|</api>
""".trimMargin(),
)
}
@Test fun modifierRemovedIfNotPresentInAll() {
val merger = ApiMerger()
merger += """
|<api version="1">
| <layout-modifier tag="1" since="2" />
|</api>
|
""".trimMargin()
merger += """
|<api version="1" />
|
""".trimMargin()
val actual = merger.merge()
assertThat(actual).isEqualTo(
"""
|<api version="1" />
""".trimMargin(),
)
}
@Test fun modifierPropertyTakesHigherSince() {
val merger = ApiMerger()
merger += """
|<api version="1">
| <layout-modifier tag="1" since="2">
| <property tag="1" since="2" />
| </layout-modifier>
|</api>
|
""".trimMargin()
merger += """
|<api version="1">
| <layout-modifier tag="1" since="2">
| <property tag="1" since="3" />
| </layout-modifier>
|</api>
|
""".trimMargin()
val actual = merger.merge()
assertThat(actual).isEqualTo(
"""
|<api version="1">
| <layout-modifier tag="1" since="2">
| <property tag="1" since="3" />
| </layout-modifier>
|</api>
""".trimMargin(),
)
}
@Test fun modifierRemovedIfNotInAll() {
val merger = ApiMerger()
merger += """
|<api version="1">
| <layout-modifier tag="1" since="2">
| <property tag="1" since="2" />
| </layout-modifier>
|</api>
|
""".trimMargin()
merger += """
|<api version="1">
| <layout-modifier tag="1" since="2" />
|</api>
|
""".trimMargin()
val actual = merger.merge()
assertThat(actual).isEqualTo(
"""
|<api version="1">
| <layout-modifier tag="1" since="2" />
|</api>
""".trimMargin(),
)
}
@Test fun sortedByTag() {
val merger = ApiMerger()
merger += """
|<api version="1">
| <widget tag="2" since="1">
| <trait tag="4" since="1" />
| <trait tag="2" since="1" />
| </widget>
| <widget tag="1" since="1">
| <trait tag="3" since="1" />
| <trait tag="1" since="1" />
| </widget>
| <layout-modifier tag="2" since="1">
| <property tag="4" since="1" />
| <property tag="2" since="1" />
| </layout-modifier>
| <layout-modifier tag="1" since="1">
| <property tag="3" since="1" />
| <property tag="1" since="1" />
| </layout-modifier>
|</api>
|
""".trimMargin()
val actual = merger.merge()
assertThat(actual).isEqualTo(
"""
|<api version="1">
| <widget tag="1" since="1">
| <trait tag="1" since="1" />
| <trait tag="3" since="1" />
| </widget>
| <widget tag="2" since="1">
| <trait tag="2" since="1" />
| <trait tag="4" since="1" />
| </widget>
| <layout-modifier tag="1" since="1">
| <property tag="1" since="1" />
| <property tag="3" since="1" />
| </layout-modifier>
| <layout-modifier tag="2" since="1">
| <property tag="2" since="1" />
| <property tag="4" since="1" />
| </layout-modifier>
|</api>
""".trimMargin(),
)
}
}

View File

@@ -80,7 +80,6 @@ include ':redwood-schema'
include ':redwood-snapshot-testing'
include ':redwood-testing'
include ':redwood-tooling-codegen'
include ':redwood-tooling-lint'
include ':redwood-tooling-schema'
include ':redwood-treehouse'
include ':redwood-treehouse-guest'