From d3dd6c035112e346c0e66bd7eba691599c807244 Mon Sep 17 00:00:00 2001 From: Tony Robalik Date: Mon, 5 Aug 2024 09:38:25 -0700 Subject: [PATCH 1/2] chore: update Gradle to 8.9. --- gradle/wrapper/gradle-wrapper.jar | Bin 43453 -> 43504 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 7 +++++-- gradlew.bat | 2 ++ 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index e6441136f3d4ba8a0da8d277868979cfbc8ad796..2c3521197d7c4586c843d1d3e9090525f1898cde 100644 GIT binary patch delta 8703 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 Ojj7^59?Sdh$^Qpp*~F>< delta 8662 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index b82aa23..09523c0 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index 1aa94a4..f5feea6 100755 --- a/gradlew +++ b/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -84,7 +86,8 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum diff --git a/gradlew.bat b/gradlew.bat index 25da30d..9d21a21 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## From f25b85f3a7defa3ccc641d9f81e989b7681b5277 Mon Sep 17 00:00:00 2001 From: Tony Robalik Date: Mon, 5 Aug 2024 13:00:16 -0700 Subject: [PATCH 2/2] feat: improve support for parsing Kotlin DSL, using KotlinEditor. --- .../kotlin/com/squareup/sort/SortCommand.kt | 21 +- gradle/libs.versions.toml | 3 + settings.gradle | 22 - .../com/squareup/sort/FunctionalSpec.groovy | 2 +- sort/build.gradle | 1 + .../kotlin/com/squareup/sort/Configuration.kt | 89 +++ .../squareup/sort/ConfigurationComparator.kt | 99 --- .../com/squareup/sort/DependencyComparator.kt | 65 +- .../squareup/sort/DependencyDeclaration.kt | 101 ++- .../main/kotlin/com/squareup/sort/Sorter.kt | 259 +------ .../main/kotlin/com/squareup/sort/Texts.kt | 6 + .../groovy/GroovyConfigurationComparator.kt | 12 + .../groovy/GroovyDependencyDeclaration.kt | 93 +++ .../com/squareup/sort/groovy/GroovySorter.kt | 264 +++++++ .../kotlin/KotlinConfigurationComparator.kt | 12 + .../kotlin/KotlinDependencyDeclaration.kt | 62 ++ .../com/squareup/sort/kotlin/KotlinSorter.kt | 229 +++++++ .../kotlin/com/squareup/utils/collections.kt | 7 + .../sort/ConfigurationComparatorSpec.groovy | 46 -- .../squareup/sort/ConfigurationSpec.groovy | 45 ++ .../sort/DependencyComparatorTest.groovy | 71 ++ ...terSpec.groovy => GroovySorterSpec.groovy} | 31 +- .../com/squareup/sort/KotlinSorterSpec.groovy | 645 ++++++++++++++++++ 23 files changed, 1645 insertions(+), 540 deletions(-) create mode 100644 sort/src/main/kotlin/com/squareup/sort/Configuration.kt delete mode 100644 sort/src/main/kotlin/com/squareup/sort/ConfigurationComparator.kt create mode 100644 sort/src/main/kotlin/com/squareup/sort/Texts.kt create mode 100644 sort/src/main/kotlin/com/squareup/sort/groovy/GroovyConfigurationComparator.kt create mode 100644 sort/src/main/kotlin/com/squareup/sort/groovy/GroovyDependencyDeclaration.kt create mode 100644 sort/src/main/kotlin/com/squareup/sort/groovy/GroovySorter.kt create mode 100644 sort/src/main/kotlin/com/squareup/sort/kotlin/KotlinConfigurationComparator.kt create mode 100644 sort/src/main/kotlin/com/squareup/sort/kotlin/KotlinDependencyDeclaration.kt create mode 100644 sort/src/main/kotlin/com/squareup/sort/kotlin/KotlinSorter.kt create mode 100644 sort/src/main/kotlin/com/squareup/utils/collections.kt delete mode 100644 sort/src/test/groovy/com/squareup/sort/ConfigurationComparatorSpec.groovy create mode 100644 sort/src/test/groovy/com/squareup/sort/ConfigurationSpec.groovy create mode 100644 sort/src/test/groovy/com/squareup/sort/DependencyComparatorTest.groovy rename sort/src/test/groovy/com/squareup/sort/{SorterSpec.groovy => GroovySorterSpec.groovy} (95%) create mode 100644 sort/src/test/groovy/com/squareup/sort/KotlinSorterSpec.groovy diff --git a/app/src/main/kotlin/com/squareup/sort/SortCommand.kt b/app/src/main/kotlin/com/squareup/sort/SortCommand.kt index 0d345a7..03573a1 100644 --- a/app/src/main/kotlin/com/squareup/sort/SortCommand.kt +++ b/app/src/main/kotlin/com/squareup/sort/SortCommand.kt @@ -17,6 +17,7 @@ import com.squareup.parse.BuildScriptParseException import com.squareup.sort.Status.NOT_SORTED import com.squareup.sort.Status.PARSE_ERROR import com.squareup.sort.Status.SUCCESS +import com.squareup.sort.groovy.GroovySorter import org.slf4j.Logger import org.slf4j.LoggerFactory import java.nio.file.FileSystem @@ -39,7 +40,15 @@ class SortCommand( ) { init { - context { helpFormatter = { context -> MordantHelpFormatter(context = context, showDefaultValues = true, showRequiredTag = true) } } + context { + helpFormatter = { context -> + MordantHelpFormatter( + context = context, + showDefaultValues = true, + showRequiredTag = true, + ) + } + } } private val verbose by option("-v", "--verbose", help = "Verbose mode. All logs are printed.") @@ -50,7 +59,11 @@ class SortCommand( help = "Flag to control whether file tree walking looks in build and hidden directories. True by default.", ).flag("--no-skip-hidden-and-build-dirs", default = true) - val mode by option("-m", "--mode", help = "Mode: [sort, check]. Defaults to 'sort'. Check will report if a file is already sorted") + val mode by option( + "-m", + "--mode", + help = "Mode: [sort, check]. Defaults to 'sort'. Check will report if a file is already sorted" + ) .enum().default(Mode.SORT) val paths: List by argument(help = "Path(s) to sort. Required.") @@ -102,7 +115,7 @@ class SortCommand( var alreadySortedCount = 0 filesToSort.parallelStream().forEach { file -> try { - val newContent = Sorter.sorterFor(file).rewritten() + val newContent = Sorter.of(file).rewritten() file.writeText(newContent, Charsets.UTF_8, StandardOpenOption.TRUNCATE_EXISTING) logger.trace("Successfully sorted: ${file.pathString} ") successCount++ @@ -144,7 +157,7 @@ class SortCommand( filesToSort.parallelStream().forEach { file -> try { - val sorter = Sorter.sorterFor(file) + val sorter = Sorter.of(file) if (!sorter.isSorted() && !sorter.hasParseErrors()) { logger.trace("Not ordered: ${file.pathString} ") notSorted.add(file) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index db48370..7fd7ed1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,6 +4,7 @@ dagp = "1.30.0" java = "11" junit5 = "5.7.2" kotlin = "1.9.24" +kotlinEditor = "0.4" mavenPublish = "0.28.0" moshi = "1.14.0" retrofit = "2.9.0" @@ -14,6 +15,8 @@ antlr-core = { module = "org.antlr:antlr4", version.ref = "antlr" } antlr-runtime = { module = "org.antlr:antlr4-runtime", version.ref = "antlr" } clikt = "com.github.ajalt.clikt:clikt:4.2.2" grammar = "com.autonomousapps:gradle-script-grammar:0.3" +kotlinEditor-core = { module = "app.cash.kotlin-editor:core", version.ref = "kotlinEditor" } +kotlinEditor-grammar = { module = "app.cash.kotlin-editor:grammar", version.ref = "kotlinEditor" } kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin"} okhttp3 = "com.squareup.okhttp3:okhttp:4.9.0" moshi-core = { module = "com.squareup.moshi:moshi", version.ref = "moshi" } diff --git a/settings.gradle b/settings.gradle index 543dd3d..1f7e83f 100644 --- a/settings.gradle +++ b/settings.gradle @@ -4,7 +4,6 @@ pluginManagement { plugins { id 'com.gradle.develocity' version '3.17.1' - id 'me.champeau.includegit' version '0.1.6' } dependencyResolutionManagement { @@ -13,27 +12,6 @@ dependencyResolutionManagement { } } -// Configure 'me.champeau.includegit' plugin to enable concurrent development on the gradle-script-grammar project. -// See also https://github.com/melix/includegit-gradle-plugin and -// https://melix.github.io/includegit-gradle-plugin/latest/index.html. -if (grammarAsSource()) { - gitRepositories { - include('gradleScriptGrammar') { - uri = 'git@github.com:autonomousapps/gradle-script-grammar.git' - // optional, set what branch to use - branch = 'main' - } - } -} - -// Run build with `-PincludeGrammar=true`, -// or add `-PincludeGrammar=true` to gradle.properties or ~/.gradle/gradle.properties -boolean grammarAsSource() { - return providers.gradleProperty('includeGrammar') - .map { it.toBoolean() } - .getOrElse(false) -} - rootProject.name = 'gradle-dependencies-sorter' develocity { diff --git a/sort-dependencies-gradle-plugin/src/test/groovy/com/squareup/sort/FunctionalSpec.groovy b/sort-dependencies-gradle-plugin/src/test/groovy/com/squareup/sort/FunctionalSpec.groovy index c72f432..1ac8bfb 100644 --- a/sort-dependencies-gradle-plugin/src/test/groovy/com/squareup/sort/FunctionalSpec.groovy +++ b/sort-dependencies-gradle-plugin/src/test/groovy/com/squareup/sort/FunctionalSpec.groovy @@ -62,7 +62,7 @@ final class FunctionalSpec extends Specification { Files.writeString(buildScript, BUILD_SCRIPT) when: 'We sort dependencies' - build(dir, 'sortDependencies') + build(dir, 'sortDependencies', '--verbose') then: 'Dependencies are sorted' buildScript.text == """\ diff --git a/sort/build.gradle b/sort/build.gradle index 693494e..b2bac38 100644 --- a/sort/build.gradle +++ b/sort/build.gradle @@ -8,6 +8,7 @@ kotlin { dependencies { api libs.grammar + api libs.kotlinEditor.core testImplementation libs.spock } diff --git a/sort/src/main/kotlin/com/squareup/sort/Configuration.kt b/sort/src/main/kotlin/com/squareup/sort/Configuration.kt new file mode 100644 index 0000000..77b119f --- /dev/null +++ b/sort/src/main/kotlin/com/squareup/sort/Configuration.kt @@ -0,0 +1,89 @@ +package com.squareup.sort + +internal class Configuration( + private val configuration: String, + val level: Int, + /** + * Android support. A "variant" configuration looks like "debugApi", "releaseImplementation", etc. + * The variant will be "debug", "release", etc. + */ + var variant: String? = null +) { + + companion object { + val values = listOf( + "api" to { Configuration("api", 0) }, + "implementation" to { Configuration("implementation", 1) }, + "compileOnlyApi" to { Configuration("compileOnlyApi", 2) }, + "compileOnly" to { Configuration("compileOnly", 3) }, + "runtimeOnly" to { Configuration("runtimeOnly", 4) }, + "annotationProcessor" to { Configuration("annotationProcessor", 5) }, + "kapt" to { Configuration("kapt", 6) }, + "testImplementation" to { Configuration("testImplementation", 7) }, + "testCompileOnly" to { Configuration("testCompileOnly", 8) }, + "testRuntimeOnly" to { Configuration("testRuntimeOnly", 9) }, + "androidTestImplementation" to { Configuration("androidTestImplementation", 10) }, + ) + + fun of(configuration: String): Configuration? { + fun findConfiguration( + predicate: (Pair Configuration>) -> Boolean + ): Configuration? { + return values.find(predicate)?.second?.invoke() + } + + // Try to find an exact match + var matchingConfiguration = findConfiguration { it.first == configuration } + + // If that failed, look for a variant + if (matchingConfiguration == null) { + matchingConfiguration = findConfiguration { configuration.endsWith(it.first, true) } + if (matchingConfiguration != null) { + matchingConfiguration.variant = configuration.substring( + 0, + configuration.length - matchingConfiguration.configuration.length + ) + } + } + + // Look for a variant again + if (matchingConfiguration == null) { + matchingConfiguration = findConfiguration { configuration.startsWith(it.first, true) } + if (matchingConfiguration != null) { + matchingConfiguration.variant = configuration.substring( + configuration.length - matchingConfiguration.configuration.length, + configuration.length + ) + } + } + + return matchingConfiguration + } + + @JvmStatic + fun stringCompare( + left: String, + right: String + ): Int { + val leftC = of(left) + val rightC = of(right) + + // Null means they don't map to a known configuration. So, compare by String natural order. + if (leftC == null && rightC == null) return left.compareTo(right) + // Unknown configuration is "higher than" known + if (rightC == null) return 1 + if (leftC == null) return -1 + + val c = leftC.level.compareTo(rightC.level) + + // If each maps to a known configuration, and they're different, we can return that value + if (c != 0) return c + // If each maps to the same configuration, we now differentiate based on whether variants are + // involved. Non-variants are "higher than" variants. + if (leftC.variant != null && rightC.variant != null) { + return rightC.variant!!.compareTo(leftC.variant!!) + } + return if (rightC.variant != null) return -1 else 1 + } + } +} diff --git a/sort/src/main/kotlin/com/squareup/sort/ConfigurationComparator.kt b/sort/src/main/kotlin/com/squareup/sort/ConfigurationComparator.kt deleted file mode 100644 index 1a1def8..0000000 --- a/sort/src/main/kotlin/com/squareup/sort/ConfigurationComparator.kt +++ /dev/null @@ -1,99 +0,0 @@ -package com.squareup.sort - -internal object ConfigurationComparator : - Comparator>> { - - private class Configuration( - private val configuration: String, - val level: Int, - /** - * Android support. A "variant" configuration looks like "debugApi", "releaseImplementation", etc. - * The variant will be "debug", "release", etc. - */ - var variant: String? = null - ) { - - companion object { - val values = listOf( - "api" to { Configuration("api", 0) }, - "implementation" to { Configuration("implementation", 1) }, - "compileOnlyApi" to { Configuration("compileOnlyApi", 2) }, - "compileOnly" to { Configuration("compileOnly", 3) }, - "runtimeOnly" to { Configuration("runtimeOnly", 4) }, - "annotationProcessor" to { Configuration("annotationProcessor", 5) }, - "kapt" to { Configuration("kapt", 6) }, - "testImplementation" to { Configuration("testImplementation", 7) }, - "testCompileOnly" to { Configuration("testCompileOnly", 8) }, - "testRuntimeOnly" to { Configuration("testRuntimeOnly", 9) }, - "androidTestImplementation" to { Configuration("androidTestImplementation", 10) }, - ) - - fun of(configuration: String): Configuration? { - fun findConfiguration( - predicate: (Pair Configuration>) -> Boolean - ): Configuration? { - return values.find(predicate)?.second?.invoke() - } - - // Try to find an exact match - var matchingConfiguration = findConfiguration { it.first == configuration } - - // If that failed, look for a variant - if (matchingConfiguration == null) { - matchingConfiguration = findConfiguration { configuration.endsWith(it.first, true) } - if (matchingConfiguration != null) { - matchingConfiguration.variant = configuration.substring( - 0, - configuration.length - matchingConfiguration.configuration.length - ) - } - } - - // Look for a variant again - if (matchingConfiguration == null) { - matchingConfiguration = findConfiguration { configuration.startsWith(it.first, true) } - if (matchingConfiguration != null) { - matchingConfiguration.variant = configuration.substring( - configuration.length - matchingConfiguration.configuration.length, - configuration.length - ) - } - } - - return matchingConfiguration - } - } - } - - override fun compare( - left: MutableMap.MutableEntry>, - right: MutableMap.MutableEntry> - ): Int = stringCompare(left.key, right.key) - - /** Visible for testing. */ - @JvmStatic - fun stringCompare( - left: String, - right: String - ): Int { - val leftC = Configuration.of(left) - val rightC = Configuration.of(right) - - // Null means they don't map to a known configuration. So, compare by String natural order. - if (leftC == null && rightC == null) return left.compareTo(right) - // Unknown configuration is "higher than" known - if (rightC == null) return 1 - if (leftC == null) return -1 - - val c = leftC.level.compareTo(rightC.level) - - // If each maps to a known configuration, and they're different, we can return that value - if (c != 0) return c - // If each maps to the same configuration, we now differentiate based on whether variants are - // involved. Non-variants are "higher than" variants. - if (leftC.variant != null && rightC.variant != null) { - return rightC.variant!!.compareTo(leftC.variant!!) - } - return if (rightC.variant != null) return -1 else 1 - } -} diff --git a/sort/src/main/kotlin/com/squareup/sort/DependencyComparator.kt b/sort/src/main/kotlin/com/squareup/sort/DependencyComparator.kt index bdcae78..2d83748 100644 --- a/sort/src/main/kotlin/com/squareup/sort/DependencyComparator.kt +++ b/sort/src/main/kotlin/com/squareup/sort/DependencyComparator.kt @@ -1,18 +1,22 @@ package com.squareup.sort -import com.autonomousapps.grammar.gradle.GradleScript.QuoteContext - internal class DependencyComparator : Comparator { override fun compare( left: DependencyDeclaration, - right: DependencyDeclaration + right: DependencyDeclaration, ): Int { - if (left.isPlatformDeclaration() && right.isPlatformDeclaration()) return compareDeclaration(left, right) + if (left.isPlatformDeclaration() && right.isPlatformDeclaration()) return compareDeclaration( + left, + right + ) if (left.isPlatformDeclaration()) return -1 if (right.isPlatformDeclaration()) return 1 - if (left.isTestFixturesDeclaration() && right.isTestFixturesDeclaration()) return compareDeclaration(left, right) + if (left.isTestFixturesDeclaration() && right.isTestFixturesDeclaration()) return compareDeclaration( + left, + right + ) if (left.isTestFixturesDeclaration()) return -1 if (right.isTestFixturesDeclaration()) return 1 @@ -21,9 +25,12 @@ internal class DependencyComparator : Comparator { private fun compareDeclaration( left: DependencyDeclaration, - right: DependencyDeclaration + right: DependencyDeclaration, ): Int { - if (left.isProjectDependency() && right.isProjectDependency()) return compareDependencies(left, right) + if (left.isProjectDependency() && right.isProjectDependency()) return compareDependencies( + left, + right + ) if (left.isProjectDependency()) return -1 if (right.isProjectDependency()) return 1 @@ -36,7 +43,7 @@ internal class DependencyComparator : Comparator { private fun compareDependencies( left: DependencyDeclaration, - right: DependencyDeclaration + right: DependencyDeclaration, ): Int { val leftText = left.comparisonText() val rightText = right.comparisonText() @@ -54,46 +61,4 @@ internal class DependencyComparator : Comparator { // No quotes on either -> return natural sort order return c } - - /** - * Returns `true` if the dependency component is surrounded by quotation marks. Consider: - * 1. implementation deps.foo // no quotes - * 2. implementation 'com.foo:bar:1.0' // quotes - * - * We want 2 to be sorted above 1. This is arbitrary. - */ - private fun DependencyDeclaration.hasQuotes(): Boolean { - val i = declaration.children.indexOf(dependency) - return declaration.getChild(i - 1) is QuoteContext && declaration.getChild(i + 1) is QuoteContext - } - - private fun DependencyDeclaration.comparisonText(): String { - val text = when { - isProjectDependency() -> with(dependency.projectDependency()) { - // If project(path: 'foo') syntax is used, take the path value. - // Else, if project('foo') syntax is used, take the ID. - projectMapEntry().firstOrNull { it.key.text == "path:" }?.value?.text - ?: ID().text - } - - isFileDependency() -> dependency.fileDependency().ID().text - else -> dependency.externalDependency().ID().text - } - - /* - * Colons should sort "higher" than hyphens. The comma's ASCII value - * is 44, the hyphen's is 45, and the colon's is 58. We replace - * colons with commas and then rely on natural sort order from - * there. - * - * For example, consider ':foo-bar' vs. ':foo:bar'. Before this - * transformation, ':foo-bar' will appear before ':foo:bar'. But - * after it, we compare ',foo,bar' to ',foo-bar', which gives the - * desired sort ordering. - * - * Similarly, single and double quotes have different ASCII values, - * but we don't care about that for our purposes. - */ - return text.replace(':', ',').replace("'", "\"") - } } diff --git a/sort/src/main/kotlin/com/squareup/sort/DependencyDeclaration.kt b/sort/src/main/kotlin/com/squareup/sort/DependencyDeclaration.kt index 23e2623..81011ae 100644 --- a/sort/src/main/kotlin/com/squareup/sort/DependencyDeclaration.kt +++ b/sort/src/main/kotlin/com/squareup/sort/DependencyDeclaration.kt @@ -1,62 +1,47 @@ package com.squareup.sort -import com.autonomousapps.grammar.gradle.GradleScript.DependencyContext -import com.autonomousapps.grammar.gradle.GradleScript.NormalDeclarationContext -import com.autonomousapps.grammar.gradle.GradleScript.PlatformDeclarationContext -import com.autonomousapps.grammar.gradle.GradleScript.TestFixturesDeclarationContext -import org.antlr.v4.runtime.ParserRuleContext - -/** - * To sort a dependency declaration, we care what kind of declaration it is ("normal", "platform", "test fixtures"), as - * well as what kind of dependency it is (GAV, project, file/files, catalog-like). - */ -internal class DependencyDeclaration( - val declaration: ParserRuleContext, - val dependency: DependencyContext, - private val declarationKind: DeclarationKind, - private val dependencyKind: DependencyKind, -) { - - enum class DeclarationKind { - NORMAL, PLATFORM, TEST_FIXTURES - } - - enum class DependencyKind { - NORMAL, PROJECT, FILE; - - companion object { - fun of(dependency: DependencyContext, filePath: String): DependencyKind { - return if (dependency.externalDependency() != null) NORMAL - else if (dependency.projectDependency() != null) PROJECT - else if (dependency.fileDependency() != null) FILE - else error("Unknown dependency kind. Was <${dependency.text}> for $filePath") - } - } - } - - fun isPlatformDeclaration() = declarationKind == DeclarationKind.PLATFORM - fun isTestFixturesDeclaration() = declarationKind == DeclarationKind.TEST_FIXTURES - - fun isProjectDependency() = dependencyKind == DependencyKind.PROJECT - fun isFileDependency() = dependencyKind == DependencyKind.FILE - - companion object { - fun of(declaration: ParserRuleContext, filePath: String): DependencyDeclaration { - val (dependency, declarationKind) = when (declaration) { - is NormalDeclarationContext -> declaration.dependency() to DeclarationKind.NORMAL - is PlatformDeclarationContext -> declaration.dependency() to DeclarationKind.PLATFORM - is TestFixturesDeclarationContext -> declaration.dependency() to DeclarationKind.TEST_FIXTURES - else -> error("Unknown declaration kind. Was ${declaration.text}.") - } - - val dependencyKind = when (declaration) { - is NormalDeclarationContext -> DependencyKind.of(declaration.dependency(), filePath) - is PlatformDeclarationContext -> DependencyKind.of(declaration.dependency(), filePath) - is TestFixturesDeclarationContext -> DependencyKind.of(declaration.dependency(), filePath) - else -> error("Unknown declaration kind. Was ${declaration.text}.") - } - - return DependencyDeclaration(declaration, dependency, declarationKind, dependencyKind) - } +internal interface DependencyDeclaration { + + fun fullText(): String + fun precedingComment(): String? + + fun isPlatformDeclaration(): Boolean + fun isTestFixturesDeclaration(): Boolean + + fun isFileDependency(): Boolean + fun isProjectDependency(): Boolean + + /** + * Returns `true` if the dependency component is surrounded by quotation marks. Consider: + * 1. implementation deps.foo // no quotes + * 2. implementation 'com.foo:bar:1.0' // quotes + * + * We want 2 to be sorted above 1. This is arbitrary. + */ + fun hasQuotes(): Boolean + + /** + * Used by [DependencyComparator][com.squareup.sort.DependencyComparator] to sort declarations. We + * sort by identifier within a configuration. + */ + fun comparisonText(): String + + /** + * Colons should sort "higher" than hyphens. The comma's ASCII value + * is 44, the hyphen's is 45, and the colon's is 58. We replace + * colons with commas and then rely on natural sort order from + * there. + * + * For example, consider ':foo-bar' vs. ':foo:bar'. Before this + * transformation, ':foo-bar' will appear before ':foo:bar'. But + * after it, we compare ',foo,bar' to ',foo-bar', which gives the + * desired sort ordering. + * + * Similarly, single and double quotes have different ASCII values, + * but we don't care about that for our purposes. + */ + fun String.replaceHyphens(): String { + // TODO maybe I should make this an ABC and this function protected. + return replace(':', ',').replace("'", "\"") } } diff --git a/sort/src/main/kotlin/com/squareup/sort/Sorter.kt b/sort/src/main/kotlin/com/squareup/sort/Sorter.kt index f1222cd..e6c1085 100644 --- a/sort/src/main/kotlin/com/squareup/sort/Sorter.kt +++ b/sort/src/main/kotlin/com/squareup/sort/Sorter.kt @@ -1,256 +1,25 @@ package com.squareup.sort -import com.autonomousapps.grammar.gradle.GradleScript -import com.autonomousapps.grammar.gradle.GradleScript.DependenciesContext -import com.autonomousapps.grammar.gradle.GradleScript.NormalDeclarationContext -import com.autonomousapps.grammar.gradle.GradleScript.PlatformDeclarationContext -import com.autonomousapps.grammar.gradle.GradleScript.TestFixturesDeclarationContext -import com.autonomousapps.grammar.gradle.GradleScriptBaseListener -import com.autonomousapps.grammar.gradle.GradleScriptLexer -import com.squareup.parse.AbstractErrorListener -import com.squareup.parse.AlreadyOrderedException import com.squareup.parse.BuildScriptParseException -import org.antlr.v4.runtime.CharStreams -import org.antlr.v4.runtime.CommonTokenStream -import org.antlr.v4.runtime.ParserRuleContext -import org.antlr.v4.runtime.RecognitionException -import org.antlr.v4.runtime.Recognizer -import org.antlr.v4.runtime.TokenStreamRewriter -import org.antlr.v4.runtime.tree.ParseTreeWalker -import java.nio.file.Files +import com.squareup.sort.groovy.GroovySorter +import com.squareup.sort.kotlin.KotlinSorter import java.nio.file.Path -import java.nio.file.StandardOpenOption -import kotlin.io.path.absolutePathString +import kotlin.io.path.pathString -public class Sorter private constructor( - private val tokens: CommonTokenStream, - private val rewriter: TokenStreamRewriter, - private val errorListener: RewriterErrorListener, - private val filePath: String, -) : GradleScriptBaseListener() { +public interface Sorter { - // We use a default of two spaces, but update it at most once later on. - private var smartIndentSet = false - private var indent = " " - - // TODO we can probably sort this block too. - private var isInBuildScriptBlock = false - - private val dependencyComparator = DependencyComparator() - private val dependenciesByConfiguration = mutableMapOf>() - private val ordering = Ordering(tokens) - - private fun collectDependency( - configuration: String, - dependencyDeclaration: DependencyDeclaration - ) { - setIndent(dependencyDeclaration.declaration) - ordering.collectDependency(dependencyDeclaration) - dependenciesByConfiguration.merge(configuration, mutableListOf(dependencyDeclaration)) { acc, inc -> - acc.apply { addAll(inc) } - } - } - - private fun setIndent(ctx: ParserRuleContext) { - if (smartIndentSet) return - - tokens.getHiddenTokensToLeft(ctx.start.tokenIndex, GradleScriptLexer.WHITESPACE) - ?.firstOrNull()?.text?.replace("\n", "")?.let { - smartIndentSet = true - indent = it - } - } - - /** - * Returns the sorted build script. - * - * Throws [BuildScriptParseException] if the script has some idiosyncrasy that impairs parsing. - * - * Throws [AlreadyOrderedException] if the script is already sorted correctly. - */ - @Throws(BuildScriptParseException::class, AlreadyOrderedException::class) - public fun rewritten(): String { - errorListener.errorMessages.ifNotEmpty { - throw BuildScriptParseException.withErrors(errorListener.errorMessages) - } - if (isSorted()) throw AlreadyOrderedException() - - return rewriter.text - } - - /** Returns `true` if this file's dependencies are already sorted correctly, or if there are no dependencies. */ - public fun isSorted(): Boolean = ordering.isAlreadyOrdered() - - /** Returns `true` if there were errors parsing the build script. */ - public fun hasParseErrors(): Boolean = errorListener.errorMessages.isNotEmpty() - - /** Returns the parse exception if there is one, otherwise null. */ - public fun getParseError(): BuildScriptParseException? { - return if (errorListener.errorMessages.isNotEmpty()) { - BuildScriptParseException.withErrors(errorListener.errorMessages) - } else { - null - } - } - - override fun enterBuildscript(ctx: GradleScript.BuildscriptContext?) { - isInBuildScriptBlock = true - } - - override fun exitBuildscript(ctx: GradleScript.BuildscriptContext?) { - isInBuildScriptBlock = false - } - - override fun enterNormalDeclaration(ctx: NormalDeclarationContext) { - if (isInBuildScriptBlock) return - collectDependency(tokens.getText(ctx.configuration()), DependencyDeclaration.of(ctx, filePath)) - } - - override fun enterPlatformDeclaration(ctx: PlatformDeclarationContext) { - if (isInBuildScriptBlock) return - collectDependency(tokens.getText(ctx.configuration()), DependencyDeclaration.of(ctx, filePath)) - } - - override fun enterTestFixturesDeclaration(ctx: TestFixturesDeclarationContext) { - if (isInBuildScriptBlock) return - collectDependency(tokens.getText(ctx.configuration()), DependencyDeclaration.of(ctx, filePath)) - } - - override fun exitDependencies(ctx: DependenciesContext) { - if (isInBuildScriptBlock) return - rewriter.replace(ctx.start, ctx.stop, dependenciesBlock()) - - // Whenever we exit a dependencies block, clear this map. Each block will be treated separately. - dependenciesByConfiguration.clear() - } - - private fun dependenciesBlock() = buildString { - val newOrder = mutableListOf() - - appendLine("dependencies {") - dependenciesByConfiguration.entries.sortedWith(ConfigurationComparator) - .forEachIndexed { i, entry -> - if (i != 0) appendLine() - entry.value.sortedWith(dependencyComparator) - .map { dependency -> - dependency to Texts( - comment = precedingComment(dependency), - declarationText = tokens.getText(dependency.declaration), - ) - } - .distinctBy { (_, texts) -> texts } - .forEach { (declaration, texts) -> - newOrder += declaration - - // Write preceding comments if there are any - if (texts.comment != null) appendLine(texts.comment) - - append(indent) - appendLine(texts.declarationText) - } - } - append("}") - - // If the new ordering matches the old ordering, we shouldn't rewrite the file. This accounts for multiple - // dependencies blocks - ordering.checkOrdering(newOrder) - } - - private fun precedingComment(dependency: DependencyDeclaration) = tokens.getHiddenTokensToLeft( - dependency.declaration.start.tokenIndex, - GradleScriptLexer.COMMENTS - )?.joinToString(separator = "") { - "$indent${it.text}" - }?.trimEnd() + public fun rewritten(): String + public fun isSorted(): Boolean + public fun hasParseErrors(): Boolean + public fun getParseError(): BuildScriptParseException? public companion object { - @JvmStatic - public fun sorterFor(file: Path): Sorter { - val input = Files.newInputStream(file, StandardOpenOption.READ).use { - CharStreams.fromStream(it) - } - val lexer = GradleScriptLexer(input) - val tokens = CommonTokenStream(lexer) - val parser = GradleScript(tokens) - - // Remove default error listeners to prevent insane console output - lexer.removeErrorListeners() - parser.removeErrorListeners() - - val errorListener = RewriterErrorListener() - parser.addErrorListener(errorListener) - lexer.addErrorListener(errorListener) - - val walker = ParseTreeWalker() - val listener = Sorter( - tokens = tokens, - rewriter = TokenStreamRewriter(tokens), - errorListener = errorListener, - filePath = file.absolutePathString() - ) - val tree = parser.script() - walker.walk(listener, tree) - - return listener - } - } -} - -internal class RewriterErrorListener : AbstractErrorListener() { - val errorMessages = mutableListOf() - - override fun syntaxError( - recognizer: Recognizer<*, *>, - offendingSymbol: Any, - line: Int, - charPositionInLine: Int, - msg: String, - e: RecognitionException? - ) { - errorMessages.add(msg) - } -} - -private class Ordering( - private val tokens: CommonTokenStream, -) { - - private val dependenciesInOrder = mutableListOf() - private val orderedBlocks = mutableListOf() - - fun isAlreadyOrdered(): Boolean = orderedBlocks.all { it } - - fun collectDependency(dependency: DependencyDeclaration) { - dependenciesInOrder += dependency - } - - /** - * Checks ordering as we leave a dependencies block. Clears the list of dependencies to prepare for the potential next - * block. - */ - fun checkOrdering(newOrder: List) { - orderedBlocks += isSameOrder(dependenciesInOrder, newOrder) - dependenciesInOrder.clear() - } - - private fun isSameOrder( - first: List, - second: List - ): Boolean { - if (first.size != second.size) return false - return first.zip(second).all { (l, r) -> - tokens.getText(l.declaration) == tokens.getText(r.declaration) + public fun of(file: Path): Sorter = if (file.pathString.endsWith(".gradle")) { + GroovySorter.of(file) + } else if (file.pathString.endsWith(".gradle.kts")) { + KotlinSorter.of(file) + } else { + error("Expected '.gradle' or '.gradle.kts' extension. Was ${file.pathString}") } } } - -private data class Texts( - val comment: String?, - val declarationText: String -) - -private inline fun C.ifNotEmpty(block: (C) -> Unit) where C : Collection<*> { - if (isNotEmpty()) { - block(this) - } -} diff --git a/sort/src/main/kotlin/com/squareup/sort/Texts.kt b/sort/src/main/kotlin/com/squareup/sort/Texts.kt new file mode 100644 index 0000000..fbb593c --- /dev/null +++ b/sort/src/main/kotlin/com/squareup/sort/Texts.kt @@ -0,0 +1,6 @@ +package com.squareup.sort + +internal data class Texts( + val comment: String?, + val declarationText: String, +) diff --git a/sort/src/main/kotlin/com/squareup/sort/groovy/GroovyConfigurationComparator.kt b/sort/src/main/kotlin/com/squareup/sort/groovy/GroovyConfigurationComparator.kt new file mode 100644 index 0000000..ec2ab44 --- /dev/null +++ b/sort/src/main/kotlin/com/squareup/sort/groovy/GroovyConfigurationComparator.kt @@ -0,0 +1,12 @@ +package com.squareup.sort.groovy + +import com.squareup.sort.Configuration + +internal object GroovyConfigurationComparator : + Comparator>> { + + override fun compare( + left: MutableMap.MutableEntry>, + right: MutableMap.MutableEntry> + ): Int = Configuration.stringCompare(left.key, right.key) +} diff --git a/sort/src/main/kotlin/com/squareup/sort/groovy/GroovyDependencyDeclaration.kt b/sort/src/main/kotlin/com/squareup/sort/groovy/GroovyDependencyDeclaration.kt new file mode 100644 index 0000000..400bfbf --- /dev/null +++ b/sort/src/main/kotlin/com/squareup/sort/groovy/GroovyDependencyDeclaration.kt @@ -0,0 +1,93 @@ +package com.squareup.sort.groovy + +import com.autonomousapps.grammar.gradle.GradleScript.DependencyContext +import com.autonomousapps.grammar.gradle.GradleScript.NormalDeclarationContext +import com.autonomousapps.grammar.gradle.GradleScript.PlatformDeclarationContext +import com.autonomousapps.grammar.gradle.GradleScript.QuoteContext +import com.autonomousapps.grammar.gradle.GradleScript.TestFixturesDeclarationContext +import com.squareup.sort.DependencyDeclaration +import org.antlr.v4.runtime.ParserRuleContext + +/** + * To sort a dependency declaration, we care what kind of declaration it is ("normal", "platform", "test fixtures"), as + * well as what kind of dependency it is (GAV, project, file/files, catalog-like). + */ +internal class GroovyDependencyDeclaration( + val declaration: ParserRuleContext, + private val dependency: DependencyContext, + private val declarationKind: DeclarationKind, + private val dependencyKind: DependencyKind, +) : DependencyDeclaration { + + enum class DeclarationKind { + NORMAL, PLATFORM, TEST_FIXTURES + } + + enum class DependencyKind { + NORMAL, PROJECT, FILE; + + companion object { + fun of(dependency: DependencyContext, filePath: String): DependencyKind { + return if (dependency.externalDependency() != null) NORMAL + else if (dependency.projectDependency() != null) PROJECT + else if (dependency.fileDependency() != null) FILE + else error("Unknown dependency kind. Was <${dependency.text}> for $filePath") + } + } + } + + override fun fullText(): String { + throw UnsupportedOperationException("Use tokens.getText(dependency.declaration) instead") + } + + override fun precedingComment(): String? { + throw UnsupportedOperationException("Use precedingComment() instead") + } + + override fun isPlatformDeclaration() = declarationKind == DeclarationKind.PLATFORM + override fun isTestFixturesDeclaration() = declarationKind == DeclarationKind.TEST_FIXTURES + + override fun isProjectDependency() = dependencyKind == DependencyKind.PROJECT + override fun isFileDependency() = dependencyKind == DependencyKind.FILE + + override fun hasQuotes(): Boolean { + val i = declaration.children.indexOf(dependency) + return declaration.getChild(i - 1) is QuoteContext && declaration.getChild(i + 1) is QuoteContext + } + + override fun comparisonText(): String { + val text = when { + isProjectDependency() -> with(dependency.projectDependency()) { + // If project(path: 'foo') syntax is used, take the path value. + // Else, if project('foo') syntax is used, take the ID. + projectMapEntry().firstOrNull { it.key.text == "path:" }?.value?.text + ?: ID().text + } + + isFileDependency() -> dependency.fileDependency().ID().text + else -> dependency.externalDependency().ID().text + } + + return text.replaceHyphens() + } + + companion object { + fun of(declaration: ParserRuleContext, filePath: String): GroovyDependencyDeclaration { + val (dependency, declarationKind) = when (declaration) { + is NormalDeclarationContext -> declaration.dependency() to DeclarationKind.NORMAL + is PlatformDeclarationContext -> declaration.dependency() to DeclarationKind.PLATFORM + is TestFixturesDeclarationContext -> declaration.dependency() to DeclarationKind.TEST_FIXTURES + else -> error("Unknown declaration kind. Was ${declaration.text}.") + } + + val dependencyKind = when (declaration) { + is NormalDeclarationContext -> DependencyKind.of(declaration.dependency(), filePath) + is PlatformDeclarationContext -> DependencyKind.of(declaration.dependency(), filePath) + is TestFixturesDeclarationContext -> DependencyKind.of(declaration.dependency(), filePath) + else -> error("Unknown declaration kind. Was ${declaration.text}.") + } + + return GroovyDependencyDeclaration(declaration, dependency, declarationKind, dependencyKind) + } + } +} diff --git a/sort/src/main/kotlin/com/squareup/sort/groovy/GroovySorter.kt b/sort/src/main/kotlin/com/squareup/sort/groovy/GroovySorter.kt new file mode 100644 index 0000000..7d88896 --- /dev/null +++ b/sort/src/main/kotlin/com/squareup/sort/groovy/GroovySorter.kt @@ -0,0 +1,264 @@ +package com.squareup.sort.groovy + +import com.autonomousapps.grammar.gradle.GradleScript +import com.autonomousapps.grammar.gradle.GradleScript.BuildscriptContext +import com.autonomousapps.grammar.gradle.GradleScript.DependenciesContext +import com.autonomousapps.grammar.gradle.GradleScript.NormalDeclarationContext +import com.autonomousapps.grammar.gradle.GradleScript.PlatformDeclarationContext +import com.autonomousapps.grammar.gradle.GradleScript.TestFixturesDeclarationContext +import com.autonomousapps.grammar.gradle.GradleScriptBaseListener +import com.autonomousapps.grammar.gradle.GradleScriptLexer +import com.squareup.parse.AbstractErrorListener +import com.squareup.parse.AlreadyOrderedException +import com.squareup.parse.BuildScriptParseException +import com.squareup.sort.DependencyComparator +import com.squareup.sort.Sorter +import com.squareup.sort.Texts +import com.squareup.utils.ifNotEmpty +import org.antlr.v4.runtime.CharStreams +import org.antlr.v4.runtime.CommonTokenStream +import org.antlr.v4.runtime.ParserRuleContext +import org.antlr.v4.runtime.RecognitionException +import org.antlr.v4.runtime.Recognizer +import org.antlr.v4.runtime.TokenStreamRewriter +import org.antlr.v4.runtime.tree.ParseTreeWalker +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.StandardOpenOption +import kotlin.io.path.absolutePathString + +public class GroovySorter private constructor( + private val tokens: CommonTokenStream, + private val rewriter: TokenStreamRewriter, + private val errorListener: RewriterErrorListener, + private val filePath: String, +) : Sorter, GradleScriptBaseListener() { + + // We use a default of two spaces, but update it at most once later on. + private var smartIndentSet = false + private var indent = " " + + // TODO we can probably sort this block too. + private var isInBuildScriptBlock = false + + private val dependencyComparator = DependencyComparator() + private val dependenciesByConfiguration = + mutableMapOf>() + private val ordering = Ordering(tokens) + + private fun collectDependency( + configuration: String, + dependencyDeclaration: GroovyDependencyDeclaration + ) { + setIndent(dependencyDeclaration.declaration) + ordering.collectDependency(dependencyDeclaration) + dependenciesByConfiguration.merge( + configuration, + mutableListOf(dependencyDeclaration) + ) { acc, inc -> + acc.apply { addAll(inc) } + } + } + + private fun setIndent(ctx: ParserRuleContext) { + if (smartIndentSet) return + + tokens.getHiddenTokensToLeft(ctx.start.tokenIndex, GradleScriptLexer.WHITESPACE) + ?.firstOrNull()?.text?.replace("\n", "")?.let { + smartIndentSet = true + indent = it + } + } + + /** + * Returns the sorted build script. + * + * Throws [BuildScriptParseException] if the script has some idiosyncrasy that impairs parsing. + * + * Throws [AlreadyOrderedException] if the script is already sorted correctly. + */ + @Throws(BuildScriptParseException::class, AlreadyOrderedException::class) + override fun rewritten(): String { + errorListener.errorMessages.ifNotEmpty { + throw BuildScriptParseException.withErrors(errorListener.errorMessages) + } + if (isSorted()) throw AlreadyOrderedException() + + return rewriter.text + } + + /** Returns `true` if this file's dependencies are already sorted correctly, or if there are no dependencies. */ + override fun isSorted(): Boolean = ordering.isAlreadyOrdered() + + /** Returns `true` if there were errors parsing the build script. */ + override fun hasParseErrors(): Boolean = errorListener.errorMessages.isNotEmpty() + + /** Returns the parse exception if there is one, otherwise null. */ + override fun getParseError(): BuildScriptParseException? { + return if (errorListener.errorMessages.isNotEmpty()) { + BuildScriptParseException.withErrors(errorListener.errorMessages) + } else { + null + } + } + + override fun enterBuildscript(ctx: BuildscriptContext) { + isInBuildScriptBlock = true + } + + override fun exitBuildscript(ctx: BuildscriptContext) { + isInBuildScriptBlock = false + } + + override fun enterNormalDeclaration(ctx: NormalDeclarationContext) { + if (isInBuildScriptBlock) return + collectDependency( + tokens.getText(ctx.configuration()), + GroovyDependencyDeclaration.of(ctx, filePath) + ) + } + + override fun enterPlatformDeclaration(ctx: PlatformDeclarationContext) { + if (isInBuildScriptBlock) return + collectDependency( + tokens.getText(ctx.configuration()), + GroovyDependencyDeclaration.of(ctx, filePath) + ) + } + + override fun enterTestFixturesDeclaration(ctx: TestFixturesDeclarationContext) { + if (isInBuildScriptBlock) return + collectDependency( + tokens.getText(ctx.configuration()), + GroovyDependencyDeclaration.of(ctx, filePath) + ) + } + + override fun exitDependencies(ctx: DependenciesContext) { + if (isInBuildScriptBlock) return + rewriter.replace(ctx.start, ctx.stop, dependenciesBlock()) + + // Whenever we exit a dependencies block, clear this map. Each block will be treated separately. + dependenciesByConfiguration.clear() + } + + private fun dependenciesBlock() = buildString { + val newOrder = mutableListOf() + + appendLine("dependencies {") + dependenciesByConfiguration.entries.sortedWith(GroovyConfigurationComparator) + .forEachIndexed { i, entry -> + if (i != 0) appendLine() + entry.value.sortedWith(dependencyComparator) + .map { dependency -> + dependency to Texts( + comment = precedingComment(dependency), + declarationText = tokens.getText(dependency.declaration), + ) + } + .distinctBy { (_, texts) -> texts } + .forEach { (declaration, texts) -> + newOrder += declaration + + // Write preceding comments if there are any + if (texts.comment != null) appendLine(texts.comment) + + append(indent) + appendLine(texts.declarationText) + } + } + append("}") + + // If the new ordering matches the old ordering, we shouldn't rewrite the file. This accounts for multiple + // dependencies blocks + ordering.checkOrdering(newOrder) + } + + private fun precedingComment(dependency: GroovyDependencyDeclaration) = + tokens.getHiddenTokensToLeft( + dependency.declaration.start.tokenIndex, + GradleScriptLexer.COMMENTS + )?.joinToString(separator = "") { + "$indent${it.text}" + }?.trimEnd() + + public companion object { + @JvmStatic + public fun of(file: Path): GroovySorter { + val input = Files.newInputStream(file, StandardOpenOption.READ).use { + CharStreams.fromStream(it) + } + val lexer = GradleScriptLexer(input) + val tokens = CommonTokenStream(lexer) + val parser = GradleScript(tokens) + + // Remove default error listeners to prevent insane console output + lexer.removeErrorListeners() + parser.removeErrorListeners() + + val errorListener = RewriterErrorListener() + parser.addErrorListener(errorListener) + lexer.addErrorListener(errorListener) + + val walker = ParseTreeWalker() + val listener = GroovySorter( + tokens = tokens, + rewriter = TokenStreamRewriter(tokens), + errorListener = errorListener, + filePath = file.absolutePathString() + ) + val tree = parser.script() + walker.walk(listener, tree) + + return listener + } + } +} + +internal class RewriterErrorListener : AbstractErrorListener() { + val errorMessages = mutableListOf() + + override fun syntaxError( + recognizer: Recognizer<*, *>, + offendingSymbol: Any, + line: Int, + charPositionInLine: Int, + msg: String, + e: RecognitionException? + ) { + errorMessages.add(msg) + } +} + +private class Ordering( + private val tokens: CommonTokenStream, +) { + + private val dependenciesInOrder = mutableListOf() + private val orderedBlocks = mutableListOf() + + fun isAlreadyOrdered(): Boolean = orderedBlocks.all { it } + + fun collectDependency(dependency: GroovyDependencyDeclaration) { + dependenciesInOrder += dependency + } + + /** + * Checks ordering as we leave a dependencies block. Clears the list of dependencies to prepare for the potential next + * block. + */ + fun checkOrdering(newOrder: List) { + orderedBlocks += isSameOrder(dependenciesInOrder, newOrder) + dependenciesInOrder.clear() + } + + private fun isSameOrder( + first: List, + second: List + ): Boolean { + if (first.size != second.size) return false + return first.zip(second).all { (l, r) -> + tokens.getText(l.declaration) == tokens.getText(r.declaration) + } + } +} diff --git a/sort/src/main/kotlin/com/squareup/sort/kotlin/KotlinConfigurationComparator.kt b/sort/src/main/kotlin/com/squareup/sort/kotlin/KotlinConfigurationComparator.kt new file mode 100644 index 0000000..e8eed46 --- /dev/null +++ b/sort/src/main/kotlin/com/squareup/sort/kotlin/KotlinConfigurationComparator.kt @@ -0,0 +1,12 @@ +package com.squareup.sort.kotlin + +import com.squareup.sort.Configuration + +internal object KotlinConfigurationComparator : + Comparator>> { + + override fun compare( + left: MutableMap.MutableEntry>, + right: MutableMap.MutableEntry> + ): Int = Configuration.stringCompare(left.key, right.key) +} diff --git a/sort/src/main/kotlin/com/squareup/sort/kotlin/KotlinDependencyDeclaration.kt b/sort/src/main/kotlin/com/squareup/sort/kotlin/KotlinDependencyDeclaration.kt new file mode 100644 index 0000000..946cb18 --- /dev/null +++ b/sort/src/main/kotlin/com/squareup/sort/kotlin/KotlinDependencyDeclaration.kt @@ -0,0 +1,62 @@ +package com.squareup.sort.kotlin + +import cash.grammar.kotlindsl.model.DependencyDeclaration.Capability +import cash.grammar.kotlindsl.model.DependencyDeclaration.Type +import com.squareup.sort.DependencyDeclaration +import cash.grammar.kotlindsl.model.DependencyDeclaration as ModelDeclaration + +internal class KotlinDependencyDeclaration( + private val base: ModelDeclaration, +) : DependencyDeclaration { + + val configuration = base.configuration + + override fun fullText(): String = base.fullText + + override fun precedingComment(): String? = base.precedingComment + + override fun isPlatformDeclaration(): Boolean { + return base.capability == Capability.PLATFORM + } + + override fun isTestFixturesDeclaration(): Boolean { + return base.capability == Capability.TEST_FIXTURES + } + + override fun isFileDependency(): Boolean { + return base.type == Type.FILE + } + + override fun isProjectDependency(): Boolean { + return base.type == Type.PROJECT + } + + override fun hasQuotes(): Boolean { + return base.identifier.path.startsWith("'") || base.identifier.path.startsWith("\"") + } + + override fun comparisonText(): String { + // TODO: this may not exactly match the Groovy DSL behavior + return base.identifier.path.replaceHyphens() + } + + override fun toString(): String { + return fullText() + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + if (!super.equals(other)) return false + + other as KotlinDependencyDeclaration + + return base == other.base + } + + override fun hashCode(): Int { + var result = super.hashCode() + result = 31 * result + base.hashCode() + return result + } +} diff --git a/sort/src/main/kotlin/com/squareup/sort/kotlin/KotlinSorter.kt b/sort/src/main/kotlin/com/squareup/sort/kotlin/KotlinSorter.kt new file mode 100644 index 0000000..2c2a88a --- /dev/null +++ b/sort/src/main/kotlin/com/squareup/sort/kotlin/KotlinSorter.kt @@ -0,0 +1,229 @@ +package com.squareup.sort.kotlin + +import cash.grammar.kotlindsl.model.gradle.DependencyContainer +import cash.grammar.kotlindsl.parse.Parser +import cash.grammar.kotlindsl.utils.Blocks.isDependencies +import cash.grammar.kotlindsl.utils.CollectingErrorListener +import cash.grammar.kotlindsl.utils.DependencyExtractor +import cash.grammar.kotlindsl.utils.Whitespace +import com.squareup.cash.grammar.KotlinParser.NamedBlockContext +import com.squareup.cash.grammar.KotlinParserBaseListener +import com.squareup.parse.AlreadyOrderedException +import com.squareup.parse.BuildScriptParseException +import com.squareup.sort.DependencyComparator +import com.squareup.sort.Sorter +import com.squareup.sort.Texts +import com.squareup.utils.ifNotEmpty +import org.antlr.v4.runtime.CharStream +import org.antlr.v4.runtime.CommonTokenStream +import org.antlr.v4.runtime.TokenStreamRewriter +import java.nio.file.Path +import kotlin.io.path.absolutePathString + +public class KotlinSorter private constructor( + private val input: CharStream, + private val tokens: CommonTokenStream, + private val errorListener: CollectingErrorListener, +) : Sorter, KotlinParserBaseListener() { + + private val rewriter = TokenStreamRewriter(tokens) + + private val indent = Whitespace.computeIndent(tokens, input) + private val dependencyExtractor = DependencyExtractor( + input = input, + tokens = tokens, + indent = indent, + ) + + private val dependencyComparator = DependencyComparator() + private val mutableDependencies = MutableDependencies( + mutableMapOf(), + mutableListOf() + ) + private val ordering = Ordering() + + private var level = 0 + + /** + * Returns the sorted build script. + * + * Throws [BuildScriptParseException] if the script has some idiosyncrasy that impairs parsing. + * + * Throws [AlreadyOrderedException] if the script is already sorted correctly. + */ + @Throws(BuildScriptParseException::class, AlreadyOrderedException::class) + override fun rewritten(): String { + errorListener.getErrorMessages().ifNotEmpty { + throw BuildScriptParseException.withErrors(it) + } + if (isSorted()) throw AlreadyOrderedException() + + return rewriter.text + } + + /** Returns `true` if this file's dependencies are already sorted correctly, or if there are no dependencies. */ + override fun isSorted(): Boolean = ordering.isAlreadyOrdered() + + /** Returns `true` if there were errors parsing the build script. */ + override fun hasParseErrors(): Boolean = errorListener.getErrorMessages().isNotEmpty() + + /** Returns the parse exception if there is one, otherwise null. */ + override fun getParseError(): BuildScriptParseException? { + return if (errorListener.getErrorMessages().isNotEmpty()) { + BuildScriptParseException.withErrors(errorListener.getErrorMessages()) + } else { + null + } + } + + override fun enterNamedBlock(ctx: NamedBlockContext) { + dependencyExtractor.onEnterBlock() + level++ + + if (ctx.isDependencies) { + collectDependencies(dependencyExtractor.collectDependencies(ctx)) + } + } + + override fun exitNamedBlock(ctx: NamedBlockContext) { + if (ctx.isDependencies) { + rewriter.replace(ctx.start, ctx.stop, dependenciesBlock()) + + // Whenever we exit a dependencies block, clear this map. Each block will be treated separately. + mutableDependencies.clear() + } + + dependencyExtractor.onExitBlock() + level-- + } + + private fun collectDependencies(container: DependencyContainer) { + val declarations = container.getDependencyDeclarations().map { KotlinDependencyDeclaration(it) } + mutableDependencies.nonDeclarations += container.getNonDeclarations() + + ordering.collectDependencies(declarations) + + declarations.forEach { decl -> + mutableDependencies.dependenciesByConfiguration.merge( + decl.configuration, + mutableListOf(decl) + ) { acc, inc -> + acc.apply { addAll(inc) } + } + } + } + + private fun dependenciesBlock() = buildString { + val newOrder = mutableListOf() + var didWrite = false + + appendLine("dependencies {") + + // not-easily-modelable elements + mutableDependencies.nonDeclarations.forEach { + append(indent.repeat(level)) + appendLine(it) + + didWrite = true + } + + if (didWrite && mutableDependencies.declarations().isNotEmpty()) { + appendLine() + } + + // declarations + mutableDependencies.declarations().sortedWith(KotlinConfigurationComparator) + .forEachIndexed { i, entry -> + if (i != 0) appendLine() + + entry.value.sortedWith(dependencyComparator) + .map { dependency -> + dependency to Texts( + comment = dependency.precedingComment(), + declarationText = dependency.fullText(), + ) + } + .distinctBy { (_, texts) -> texts } + .forEach { (declaration, texts) -> + newOrder += declaration + + // Write preceding comments if there are any + if (texts.comment != null) appendLine(texts.comment) + + append(indent.repeat(level)) + appendLine(texts.declarationText) + } + } + + append(indent.repeat(level - 1)) + append("}") + + // If the new ordering matches the old ordering, we shouldn't rewrite the file. This accounts for multiple + // dependencies blocks + ordering.checkOrdering(newOrder) + } + + public companion object { + @JvmStatic + public fun of(file: Path): KotlinSorter { + val errorListener = CollectingErrorListener() + + return Parser( + file = Parser.readOnlyInputStream(file), + errorListener = errorListener, + startRule = { it.script() }, + listenerFactory = { input, tokens, parser -> + KotlinSorter( + input = input, + tokens = tokens, + errorListener = errorListener, + ) + } + ).listener() + } + } +} + +private class MutableDependencies( + val dependenciesByConfiguration: MutableMap>, + val nonDeclarations: MutableList, +) { + + fun declarations() = dependenciesByConfiguration.entries + + fun clear() { + dependenciesByConfiguration.clear() + nonDeclarations.clear() + } +} + +private class Ordering { + + private val dependenciesInOrder = mutableListOf() + private val orderedBlocks = mutableListOf() + + fun isAlreadyOrdered(): Boolean = orderedBlocks.all { it } + + fun collectDependencies(dependencies: List) { + dependenciesInOrder += dependencies + } + + /** + * Checks ordering as we leave a dependencies block. Clears the list of dependencies to prepare for the potential next + * block. + */ + fun checkOrdering(newOrder: List) { + orderedBlocks += isSameOrder(dependenciesInOrder, newOrder) + dependenciesInOrder.clear() + } + + private fun isSameOrder( + first: List, + second: List, + ): Boolean { + if (first.size != second.size) return false + return first.zip(second).all { (l, r) -> + l == r + } + } +} diff --git a/sort/src/main/kotlin/com/squareup/utils/collections.kt b/sort/src/main/kotlin/com/squareup/utils/collections.kt new file mode 100644 index 0000000..e240be1 --- /dev/null +++ b/sort/src/main/kotlin/com/squareup/utils/collections.kt @@ -0,0 +1,7 @@ +package com.squareup.utils + +internal inline fun C.ifNotEmpty(block: (C) -> Unit) where C : Collection<*> { + if (isNotEmpty()) { + block(this) + } +} diff --git a/sort/src/test/groovy/com/squareup/sort/ConfigurationComparatorSpec.groovy b/sort/src/test/groovy/com/squareup/sort/ConfigurationComparatorSpec.groovy deleted file mode 100644 index 7694de8..0000000 --- a/sort/src/test/groovy/com/squareup/sort/ConfigurationComparatorSpec.groovy +++ /dev/null @@ -1,46 +0,0 @@ -package com.squareup.sort - - -import spock.lang.Specification - -import static com.google.common.truth.Truth.assertThat - -final class ConfigurationComparatorSpec extends Specification { - - def "comparisons work"() { - given: - def configurations = [ - 'implementation', 'api', 'releaseImplementation', 'debugApi', 'fooApi', 'kapt', - 'annotationProcessor', 'runtimeOnly', 'compileOnly', 'compileOnlyApi', 'testRuntimeOnly', - 'testCompileOnly', 'testImplementation', 'androidTestImplementation', - 'antlr', 'foo', 'bar', 'baz' - ] - - when: - configurations.sort(true) { left, right -> - ConfigurationComparator.stringCompare(left, right) - } - - then: - assertThat(configurations).containsExactly( - 'antlr', - 'bar', - 'baz', - 'foo', - 'api', - 'fooApi', - 'debugApi', - 'implementation', - 'releaseImplementation', - 'compileOnlyApi', - 'compileOnly', - 'runtimeOnly', - 'annotationProcessor', - 'kapt', - 'testImplementation', - 'testCompileOnly', - 'testRuntimeOnly', - 'androidTestImplementation', - ).inOrder() - } -} diff --git a/sort/src/test/groovy/com/squareup/sort/ConfigurationSpec.groovy b/sort/src/test/groovy/com/squareup/sort/ConfigurationSpec.groovy new file mode 100644 index 0000000..3b32735 --- /dev/null +++ b/sort/src/test/groovy/com/squareup/sort/ConfigurationSpec.groovy @@ -0,0 +1,45 @@ +package com.squareup.sort + +import spock.lang.Specification + +import static com.google.common.truth.Truth.assertThat + +final class ConfigurationSpec extends Specification { + + def "comparisons work"() { + given: + def configurations = [ + 'implementation', 'api', 'releaseImplementation', 'debugApi', 'fooApi', 'kapt', + 'annotationProcessor', 'runtimeOnly', 'compileOnly', 'compileOnlyApi', 'testRuntimeOnly', + 'testCompileOnly', 'testImplementation', 'androidTestImplementation', + 'antlr', 'foo', 'bar', 'baz' + ] + + when: + configurations.sort(true) { left, right -> + Configuration.stringCompare(left, right) + } + + then: + assertThat(configurations).containsExactly( + 'antlr', + 'bar', + 'baz', + 'foo', + 'api', + 'fooApi', + 'debugApi', + 'implementation', + 'releaseImplementation', + 'compileOnlyApi', + 'compileOnly', + 'runtimeOnly', + 'annotationProcessor', + 'kapt', + 'testImplementation', + 'testCompileOnly', + 'testRuntimeOnly', + 'androidTestImplementation', + ).inOrder() + } +} diff --git a/sort/src/test/groovy/com/squareup/sort/DependencyComparatorTest.groovy b/sort/src/test/groovy/com/squareup/sort/DependencyComparatorTest.groovy new file mode 100644 index 0000000..838366d --- /dev/null +++ b/sort/src/test/groovy/com/squareup/sort/DependencyComparatorTest.groovy @@ -0,0 +1,71 @@ +package com.squareup.sort + +import cash.grammar.kotlindsl.model.DependencyDeclaration.Capability +import cash.grammar.kotlindsl.model.DependencyDeclaration.Type +import com.squareup.sort.kotlin.KotlinDependencyDeclaration +import spock.lang.Specification +import cash.grammar.kotlindsl.model.DependencyDeclaration.Identifier + +class DependencyComparatorTest extends Specification { + + def "can sort dependency declarations"() { + given: + def deps = [ + moduleDependency('implementation', '"heart:of-gold:1.0"', 'implementation("heart:of-gold:1.0")'), + moduleDependency('implementation', '"b:1.0"', 'implementation("b:1.0")'), + moduleDependency('implementation', '"a:1.0"', 'implementation("a:1.0")'), + moduleDependency('implementation', 'deps.foo', 'implementation(deps.foo)'), + moduleDependency('implementation', 'deps.bar', 'implementation(deps.bar)', ' /*\n * Here\'s a multiline comment.\n */'), + projectDependency('implementation', '":milliways"', 'implementation(project(":milliways"))'), + ] + + when: + Collections.sort(deps, new DependencyComparator()) + + then: + deps[0].base.identifier.path == '":milliways"' + deps[1].base.identifier.path == '"a:1.0"' + deps[2].base.identifier.path == '"b:1.0"' + deps[3].base.identifier.path == '"heart:of-gold:1.0"' + deps[4].base.identifier.path == 'deps.bar' + deps[5].base.identifier.path == 'deps.foo' + } + + private static KotlinDependencyDeclaration moduleDependency( + String configuration, + String identifier, + String fullText, + String comment = null, + Capability capability = Capability.DEFAULT + ) { + def base = new cash.grammar.kotlindsl.model.DependencyDeclaration( + configuration, + new Identifier(identifier), + capability, + Type.MODULE, + fullText, + comment + ) + + return new KotlinDependencyDeclaration(base) + } + + private static KotlinDependencyDeclaration projectDependency( + String configuration, + String identifier, + String fullText, + String comment = null, + Capability capability = Capability.DEFAULT + ) { + def base = new cash.grammar.kotlindsl.model.DependencyDeclaration( + configuration, + new Identifier(identifier), + capability, + Type.PROJECT, + fullText, + comment + ) + + return new KotlinDependencyDeclaration(base) + } +} diff --git a/sort/src/test/groovy/com/squareup/sort/SorterSpec.groovy b/sort/src/test/groovy/com/squareup/sort/GroovySorterSpec.groovy similarity index 95% rename from sort/src/test/groovy/com/squareup/sort/SorterSpec.groovy rename to sort/src/test/groovy/com/squareup/sort/GroovySorterSpec.groovy index e93e9c1..a45d011 100644 --- a/sort/src/test/groovy/com/squareup/sort/SorterSpec.groovy +++ b/sort/src/test/groovy/com/squareup/sort/GroovySorterSpec.groovy @@ -2,6 +2,7 @@ package com.squareup.sort import com.squareup.parse.AlreadyOrderedException import com.squareup.parse.BuildScriptParseException +import com.squareup.sort.groovy.GroovySorter import spock.lang.Specification import spock.lang.TempDir @@ -10,7 +11,7 @@ import java.nio.file.Path import static com.google.common.truth.Truth.assertThat -final class SorterSpec extends Specification { +final class GroovySorterSpec extends Specification { @TempDir Path dir @@ -64,7 +65,7 @@ final class SorterSpec extends Specification { println 'hello, world' '''.stripIndent()) - def sorter = Sorter.sorterFor(buildScript) + def sorter = GroovySorter.of(buildScript) expect: assertThat(trimmedLinesOf(sorter.rewritten())).containsExactlyElementsIn(trimmedLinesOf( @@ -142,7 +143,7 @@ final class SorterSpec extends Specification { api 'zzz:yyy:1.0' } '''.stripIndent()) - def sorter = Sorter.sorterFor(buildScript) + def sorter = GroovySorter.of(buildScript) expect: assertThat(trimmedLinesOf(sorter.rewritten())).containsExactlyElementsIn(trimmedLinesOf( @@ -181,7 +182,7 @@ final class SorterSpec extends Specification { api project(":marvin:robot:so-sad") } '''.stripIndent()) - def sorter = Sorter.sorterFor(buildScript) + def sorter = GroovySorter.of(buildScript) expect: assertThat(trimmedLinesOf(sorter.rewritten())).containsExactlyElementsIn(trimmedLinesOf( @@ -204,7 +205,7 @@ final class SorterSpec extends Specification { api project(":b") } '''.stripIndent()) - def sorter = Sorter.sorterFor(buildScript) + def sorter = GroovySorter.of(buildScript) when: sorter.rewritten() @@ -228,7 +229,7 @@ final class SorterSpec extends Specification { '''.stripIndent()) when: - def newScript = Sorter.sorterFor(buildScript).rewritten() + def newScript = GroovySorter.of(buildScript).rewritten() then: notThrown(BuildScriptParseException) @@ -286,7 +287,7 @@ final class SorterSpec extends Specification { println 'hello, world' '''.stripIndent()) - def sorter = Sorter.sorterFor(buildScript) + def sorter = GroovySorter.of(buildScript) when: sorter.rewritten() @@ -311,7 +312,7 @@ final class SorterSpec extends Specification { api project(path: ":trillian") } '''.stripIndent()) - def sorter = Sorter.sorterFor(buildScript) + def sorter = GroovySorter.of(buildScript) expect: assertThat(trimmedLinesOf(sorter.rewritten())).containsExactlyElementsIn(trimmedLinesOf( @@ -338,7 +339,7 @@ final class SorterSpec extends Specification { id 'foo' } '''.stripIndent()) - def sorter = Sorter.sorterFor(buildScript) + def sorter = GroovySorter.of(buildScript) expect: sorter.isSorted() @@ -352,7 +353,7 @@ final class SorterSpec extends Specification { dependencies { } '''.stripIndent()) - def sorter = Sorter.sorterFor(buildScript) + def sorter = GroovySorter.of(buildScript) expect: sorter.isSorted() @@ -375,7 +376,7 @@ final class SorterSpec extends Specification { '''.stripIndent()) when: - def newScript = Sorter.sorterFor(buildScript).rewritten() + def newScript = GroovySorter.of(buildScript).rewritten() then: notThrown(BuildScriptParseException) @@ -415,7 +416,7 @@ final class SorterSpec extends Specification { '''.stripIndent()) when: - def newScript = Sorter.sorterFor(buildScript).rewritten() + def newScript = GroovySorter.of(buildScript).rewritten() then: notThrown(BuildScriptParseException) @@ -456,7 +457,7 @@ final class SorterSpec extends Specification { '''.stripIndent()) when: - def newScript = Sorter.sorterFor(buildScript).rewritten() + def newScript = GroovySorter.of(buildScript).rewritten() then: notThrown(BuildScriptParseException) @@ -495,7 +496,7 @@ final class SorterSpec extends Specification { } '''.stripIndent()) when: - def newScript = Sorter.sorterFor(buildScript).rewritten() + def newScript = GroovySorter.of(buildScript).rewritten() then: notThrown(BuildScriptParseException) @@ -574,7 +575,7 @@ final class SorterSpec extends Specification { ) when: - def newScript = Sorter.sorterFor(buildScript).rewritten() + def newScript = GroovySorter.of(buildScript).rewritten() then: assertThat(newScript).isEqualTo( diff --git a/sort/src/test/groovy/com/squareup/sort/KotlinSorterSpec.groovy b/sort/src/test/groovy/com/squareup/sort/KotlinSorterSpec.groovy new file mode 100644 index 0000000..099589f --- /dev/null +++ b/sort/src/test/groovy/com/squareup/sort/KotlinSorterSpec.groovy @@ -0,0 +1,645 @@ +package com.squareup.sort + +import com.squareup.parse.AlreadyOrderedException +import com.squareup.parse.BuildScriptParseException +import com.squareup.sort.kotlin.KotlinSorter +import spock.lang.Specification +import spock.lang.TempDir + +import java.nio.file.Files +import java.nio.file.Path + +import static com.google.common.truth.Truth.assertThat + +class KotlinSorterSpec extends Specification { + + @TempDir + Path dir + + def "can sort build script"() { + given: + def buildScript = dir.resolve('build.gradle.kts') + Files.writeString(buildScript, + '''\ + import foo + import static bar; + + plugins { + id("foo") + } + + repositories { + google() + mavenCentral() + } + + apply(plugin = "bar") + val magic by extra(42) + + android { + whatever = "" + } + + dependencies { + implementation("heart:of-gold:1.0") + api(project(":marvin")) + + implementation("b:1.0") + implementation("a:1.0") + // Here's a multi-line comment + // Here's the second line of the comment + implementation(deps.foo) + + /* + * Here's a multiline comment. + */ + implementation(deps.bar) + + testImplementation("pan-galactic:gargle-blaster:2.0-SNAPSHOT") { + because = "life's too short not to" + } + + implementation(project(":milliways")) + api("zzz:yyy:1.0") + } + + println("hello, world") + '''.stripIndent()) + def sorter = KotlinSorter.of(buildScript) + + expect: + assertThat(trimmedLinesOf(sorter.rewritten())).containsExactlyElementsIn(trimmedLinesOf( + '''\ + import foo + import static bar; + + plugins { + id("foo") + } + + repositories { + google() + mavenCentral() + } + + apply(plugin = "bar") + val magic by extra(42) + + android { + whatever = "" + } + + dependencies { + api(project(":marvin")) + api("zzz:yyy:1.0") + + implementation(project(":milliways")) + implementation("a:1.0") + implementation("b:1.0") + implementation("heart:of-gold:1.0") + /* + * Here's a multiline comment. + */ + implementation(deps.bar) + // Here's a multi-line comment + // Here's the second line of the comment + implementation(deps.foo) + + testImplementation("pan-galactic:gargle-blaster:2.0-SNAPSHOT") { + because = "life's too short not to" + } + } + + println("hello, world") + '''.stripIndent() + )).inOrder() + } + + def "can sort build script with four-space tabs"() { + given: + def buildScript = dir.resolve('build.gradle.kts') + Files.writeString(buildScript, + '''\ + dependencies { + implementation("heart:of-gold:1.0") + api(project(":marvin")) + + implementation("b:1.0") + implementation("a:1.0") + // Here's a multi-line comment + // Here's the second line of the comment + implementation(deps.foo) + + /* + * Here's a multiline comment. + */ + implementation(deps.bar) + + testImplementation("pan-galactic:gargle-blaster:2.0-SNAPSHOT") { + because = "life's too short not to" + } + + implementation(project(":milliways")) + api("zzz:yyy:1.0") + } + '''.stripIndent()) + def sorter = KotlinSorter.of(buildScript) + + expect: + assertThat(trimmedLinesOf(sorter.rewritten())).containsExactlyElementsIn(trimmedLinesOf( + '''\ + dependencies { + api(project(":marvin")) + api("zzz:yyy:1.0") + + implementation(project(":milliways")) + implementation("a:1.0") + implementation("b:1.0") + implementation("heart:of-gold:1.0") + /* + * Here's a multiline comment. + */ + implementation(deps.bar) + // Here's a multi-line comment + // Here's the second line of the comment + implementation(deps.foo) + + testImplementation("pan-galactic:gargle-blaster:2.0-SNAPSHOT") { + because = "life's too short not to" + } + } + '''.stripIndent() + )).inOrder() + } + + def "colons have higher precedence than hyphen"() { + given: + def buildScript = dir.resolve('build.gradle.kts') + Files.writeString(buildScript, + '''\ + dependencies { + api(project(":marvin-robot:so-sad")) + api(project(":marvin:robot:so-sad")) + } + '''.stripIndent()) + def sorter = KotlinSorter.of(buildScript) + + expect: + assertThat(trimmedLinesOf(sorter.rewritten())).containsExactlyElementsIn(trimmedLinesOf( + '''\ + dependencies { + api(project(":marvin:robot:so-sad")) + api(project(":marvin-robot:so-sad")) + } + '''.stripIndent() + )).inOrder() + } + + // We have observed that, given the start "dependencies{" (no space), and a project dependency, the + // parser fails. For some reason this combination was confusing the lexer, which treated + // "dependencies{" as if it matched the 'text' rule, rather than the 'dependencies' rule. + def "can sort a dependencies{ block"() { + given: + def buildScript = dir.resolve('build.gradle.kts') + Files.writeString(buildScript, + '''\ + dependencies{ + api(project(":nu-metal")) + api(project(":magic")) + } + '''.stripIndent()) + + when: + def newScript = KotlinSorter.of(buildScript).rewritten() + + then: + notThrown(BuildScriptParseException) + + and: + assertThat(trimmedLinesOf(newScript)).containsExactlyElementsIn(trimmedLinesOf( + '''\ + dependencies { + api(project(":magic")) + api(project(":nu-metal")) + } + '''.stripIndent() + )).inOrder() + } + + def "will not sort already sorted build script"() { + given: + def buildScript = dir.resolve('build.gradle.kts') + Files.writeString(buildScript, + '''\ + import foo + import static bar; + + plugins { + id("foo") + } + + repositories { + google() + mavenCentral() + } + + apply(plugin = "bar") + val magic by extra(42) + + android { + whatever = "" + } + + dependencies { + api(project(":marvin")) + api("zzz:yyy:1.0") + + implementation(project(":milliways")) + implementation("a:1.0") + implementation("b:1.0") + implementation("heart:of-gold:1.0") + implementation(deps.bar) + implementation(deps.foo) + + testImplementation("pan-galactic:gargle-blaster:2.0-SNAPSHOT") { + because = "life's too short not to" + } + } + + println("hello, world") + '''.stripIndent()) + def sorter = KotlinSorter.of(buildScript) + + when: + sorter.rewritten() + + then: + thrown(AlreadyOrderedException) + } + + def "sort can handle 'path:' notation"() { + given: + def buildScript = dir.resolve('build.gradle.kts') + Files.writeString(buildScript, + '''\ + dependencies { + api(project(":path:path")) + api(project(":zaphod")) + api(project(path = ":beeblebrox", configuration = "solipsism")) + api(project(path = ":path")) + + api(project(":eddie")) + api(project(path = ":trillian")) + api(project(":eddie:eddie")) + } + '''.stripIndent()) + def sorter = KotlinSorter.of(buildScript) + + expect: + assertThat(trimmedLinesOf(sorter.rewritten())).containsExactlyElementsIn(trimmedLinesOf( + '''\ + dependencies { + api(project(path = ":beeblebrox", configuration = "solipsism")) + api(project(":eddie")) + api(project(":eddie:eddie")) + api(project(path = ":path")) + api(project(":path:path")) + api(project(path = ":trillian")) + api(project(":zaphod")) + } + '''.stripIndent() + )).inOrder() + } + + def "sort can handle 'files'-like notation"() { + given: + def buildScript = dir.resolve('build.gradle.kts') + Files.writeString(buildScript, + '''\ + dependencies { + implementation(files("a.jar")) + api(file("another.jar")) + compileOnly(fileTree("libs") { include("*.jar") }) + } + '''.stripIndent()) + def sorter = KotlinSorter.of(buildScript) + + expect: + assertThat(trimmedLinesOf(sorter.rewritten())).containsExactlyElementsIn(trimmedLinesOf( + '''\ + dependencies { + api(file("another.jar")) + + implementation(files("a.jar")) + + compileOnly(fileTree("libs") { include("*.jar") }) + } + '''.stripIndent() + )).inOrder() + } + + def "a script without dependencies is already sorted"() { + given: + def buildScript = dir.resolve('build.gradle.kts') + Files.writeString(buildScript, + '''\ + plugins { + id("foo") + } + '''.stripIndent()) + def sorter = KotlinSorter.of(buildScript) + + expect: + sorter.isSorted() + } + + def "a script with an empty dependencies is already sorted"() { + given: + def buildScript = dir.resolve('build.gradle.kts') + Files.writeString(buildScript, + '''\ + dependencies { + } + '''.stripIndent()) + def sorter = KotlinSorter.of(buildScript) + + expect: + sorter.isSorted() + } + + def "dedupe identical dependencies"() { + given: + def buildScript = dir.resolve('build.gradle.kts') + Files.writeString(buildScript, + '''\ + dependencies { + implementation(projects.foo) + implementation(projects.bar) + implementation(projects.foo) + + api(projects.foo) + api(projects.bar) + api(projects.foo) + } + '''.stripIndent()) + + when: + def newScript = KotlinSorter.of(buildScript).rewritten() + + then: + notThrown(BuildScriptParseException) + + and: + assertThat(trimmedLinesOf(newScript)).containsExactlyElementsIn(trimmedLinesOf( + '''\ + dependencies { + api(projects.bar) + api(projects.foo) + + implementation(projects.bar) + implementation(projects.foo) + } + '''.stripIndent() + )).inOrder() + } + + def "keep identical dependencies that have non-identical comments"() { + given: + def buildScript = dir.resolve('build.gradle.kts') + Files.writeString(buildScript, + '''\ + dependencies { + // Foo implementation + implementation(projects.foo) + implementation(projects.bar) + // Foo implementation + implementation(projects.foo) + + // Foo api 1st + api(projects.foo) + api(projects.bar) + // Foo api 2nd + api(projects.foo) + } + '''.stripIndent()) + + when: + def newScript = KotlinSorter.of(buildScript).rewritten() + + then: + notThrown(BuildScriptParseException) + + and: + assertThat(trimmedLinesOf(newScript)).containsExactlyElementsIn(trimmedLinesOf( + '''\ + dependencies { + api(projects.bar) + // Foo api 1st + api(projects.foo) + // Foo api 2nd + api(projects.foo) + + implementation(projects.bar) + // Foo implementation + implementation(projects.foo) + } + '''.stripIndent() + )).inOrder() + } + + def "sort add function call in dependencies"() { + given: + def buildScript = dir.resolve('build.gradle.kts') + Files.writeString(buildScript, + '''\ + dependencies { + implementation(projects.foo) + implementation(projects.bar) + + api(projects.foo) + api(projects.bar) + + add("debugImplementation", projects.foo) + add(releaseImplementation, projects.foo) + } + '''.stripIndent()) + + when: + def newScript = KotlinSorter.of(buildScript).rewritten() + + then: + notThrown(BuildScriptParseException) + + and: + assertThat(trimmedLinesOf(newScript)).containsExactlyElementsIn(trimmedLinesOf('''\ + dependencies { + add("debugImplementation", projects.foo) + add(releaseImplementation, projects.foo) + + api(projects.bar) + api(projects.foo) + + implementation(projects.bar) + implementation(projects.foo) + } + '''.stripIndent())).inOrder() + } + + def "can sort dependencies with artifact type specified"() { + given: + def buildScript = dir.resolve('build.gradle.kts') + Files.writeString(buildScript, + '''\ + dependencies { + implementation(projects.foo.internal) + implementation(projects.bar.public) + implementation(libs.baz.ui) { + artifact { + type = "aar" + } + } + implementation(libs.androidx.constraintLayout) + implementation(libs.common.view) + implementation(projects.core) + } + '''.stripIndent()) + when: + def newScript = KotlinSorter.of(buildScript).rewritten() + + then: + notThrown(BuildScriptParseException) + + and: + assertThat(trimmedLinesOf(newScript)).containsExactlyElementsIn(trimmedLinesOf( + '''\ + dependencies { + implementation(libs.androidx.constraintLayout) + implementation(libs.baz.ui) { + artifact { + type = "aar" + } + } + implementation(libs.common.view) + implementation(projects.bar.public) + implementation(projects.core) + implementation(projects.foo.internal) + } + '''.stripIndent() + )).inOrder() + } + + // https://github.com/square/gradle-dependencies-sorter/issues/59 + def "can sort multiple semantically different dependencies blocks"() { + given: + def buildScript = dir.resolve('build.gradle.kts') + Files.writeString(buildScript, + """\ + import app.cash.redwood.buildsupport.FlexboxHelpers + + apply(plugin = "com.android.library") + apply(plugin = "org.jetbrains.kotlin.multiplatform") + apply(plugin = "org.jetbrains.kotlin.plugin.serialization") + apply(plugin = "app.cash.paparazzi") + apply(plugin = "com.vanniktech.maven.publish") + apply(plugin = "org.jetbrains.dokka") // Must be applied here for publish plugin. + apply(plugin = "app.cash.redwood.build.compose") + + kotlin { + android { + publishLibraryVariants("release") + } + + iosArm64() + iosX64() + iosSimulatorArm64() + + jvm() + + macosArm64() + macosX64() + + sourceSets { + commonMain { + kotlin.srcDir(FlexboxHelpers.get(tasks, "app.cash.redwood.layout.composeui").get()) + dependencies { + api(projects.redwoodLayoutWidget) + implementation(projects.redwoodFlexbox) + implementation(projects.redwoodWidgetCompose) + implementation(libs.jetbrains.compose.foundation) + } + } + + androidUnitTest { + dependencies { + implementation(projects.redwoodLayoutSharedTest) + } + } + } + } + + android { + namespace = "app.cash.redwood.layout.composeui" + }""".stripIndent() + ) + + when: + def newScript = KotlinSorter.of(buildScript).rewritten() + + then: + assertThat(newScript).isEqualTo( + """\ + import app.cash.redwood.buildsupport.FlexboxHelpers + + apply(plugin = "com.android.library") + apply(plugin = "org.jetbrains.kotlin.multiplatform") + apply(plugin = "org.jetbrains.kotlin.plugin.serialization") + apply(plugin = "app.cash.paparazzi") + apply(plugin = "com.vanniktech.maven.publish") + apply(plugin = "org.jetbrains.dokka") // Must be applied here for publish plugin. + apply(plugin = "app.cash.redwood.build.compose") + + kotlin { + android { + publishLibraryVariants("release") + } + + iosArm64() + iosX64() + iosSimulatorArm64() + + jvm() + + macosArm64() + macosX64() + + sourceSets { + commonMain { + kotlin.srcDir(FlexboxHelpers.get(tasks, "app.cash.redwood.layout.composeui").get()) + dependencies { + api(projects.redwoodLayoutWidget) + + implementation(libs.jetbrains.compose.foundation) + implementation(projects.redwoodFlexbox) + implementation(projects.redwoodWidgetCompose) + } + } + + androidUnitTest { + dependencies { + implementation(projects.redwoodLayoutSharedTest) + } + } + } + } + + android { + namespace = "app.cash.redwood.layout.composeui" + }""".stripIndent() + ) + } + + private static List trimmedLinesOf(CharSequence content) { + // to lines and trim whitespace off end + return content.readLines().collect { it.replaceFirst('\\s+\$', '') } + } +}