Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: improve support for parsing Kotlin DSL, using KotlinEditor.
Browse files Browse the repository at this point in the history
autonomousapps committed Aug 6, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
1 parent d3dd6c0 commit f00ed64
Showing 23 changed files with 1,647 additions and 518 deletions.
21 changes: 17 additions & 4 deletions app/src/main/kotlin/com/squareup/sort/SortCommand.kt
Original file line number Diff line number Diff line change
@@ -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<Mode>().default(Mode.SORT)

val paths: List<Path> 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)
3 changes: 3 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@ dagp = "1.30.0"
java = "11"
junit5 = "5.7.2"
kotlin = "1.9.24"
kotlinEditor = "0.3"
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" }
2 changes: 2 additions & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
@@ -7,6 +7,8 @@ plugins {
id 'me.champeau.includegit' version '0.1.6'
}

includeBuild("../cash/kotlin-editor")

dependencyResolutionManagement {
repositories {
mavenCentral()
Original file line number Diff line number Diff line change
@@ -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 == """\
1 change: 1 addition & 0 deletions sort/build.gradle
Original file line number Diff line number Diff line change
@@ -8,6 +8,7 @@ kotlin {

dependencies {
api libs.grammar
api libs.kotlinEditor.core

testImplementation libs.spock
}
89 changes: 89 additions & 0 deletions sort/src/main/kotlin/com/squareup/sort/Configuration.kt
Original file line number Diff line number Diff line change
@@ -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<String, () -> 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
}
}
}
99 changes: 0 additions & 99 deletions sort/src/main/kotlin/com/squareup/sort/ConfigurationComparator.kt

This file was deleted.

65 changes: 15 additions & 50 deletions sort/src/main/kotlin/com/squareup/sort/DependencyComparator.kt
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
package com.squareup.sort

import com.autonomousapps.grammar.gradle.GradleScript.QuoteContext

internal class DependencyComparator : Comparator<DependencyDeclaration> {

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<DependencyDeclaration> {

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<DependencyDeclaration> {

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<DependencyDeclaration> {
// 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("'", "\"")
}
}
Loading

0 comments on commit f00ed64

Please sign in to comment.